From a6eaea3b3e92dbc1d7a037bcd737aae1db8b350f Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Mon, 30 Dec 2024 16:15:40 -0800 Subject: [PATCH 01/19] CreatePlateSetAction: support create/add --- .../src/org/labkey/assay/PlateController.java | 153 ++---------------- .../org/labkey/assay/plate/PlateManager.java | 128 +++++++++------ .../labkey/assay/plate/PlateManagerTest.java | 53 +++--- .../plate/model/CreatePlateSetOptions.java | 43 +++++ .../assay/plate/model/ReformatOptions.java | 44 +++-- 5 files changed, 189 insertions(+), 232 deletions(-) create mode 100644 assay/src/org/labkey/assay/plate/model/CreatePlateSetOptions.java diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index a8d7b632519..0e362cb2646 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -79,6 +79,8 @@ import org.labkey.assay.plate.PlateSetImpl; import org.labkey.assay.plate.PlateUrls; import org.labkey.assay.plate.TsvPlateLayoutHandler; +import org.labkey.assay.plate.layout.ArrayOperation; +import org.labkey.assay.plate.model.CreatePlateSetOptions; import org.labkey.assay.plate.model.ReformatOptions; import org.labkey.assay.view.AssayGWTView; import org.springframework.validation.BindException; @@ -956,160 +958,23 @@ public Object execute(GetPlateForm form, BindException errors) throws Exception } } - public static class CreatePlateSetForm - { - private String _description; - private String _name; - private List _plates = new ArrayList<>(); - private Integer _parentPlateSetId; - private String _selectionKey; - private Boolean _template; - private PlateSetType _type; - - public String getDescription() - { - return _description; - } - - public void setDescription(String description) - { - _description = description; - } - - public String getName() - { - return _name; - } - - public void setName(String name) - { - _name = name; - } - - public List getPlates() - { - return _plates; - } - - public void setPlates(List plates) - { - _plates = plates; - } - - public Integer getParentPlateSetId() - { - return _parentPlateSetId; - } - - public void setParentPlateSetId(Integer parentPlateSetId) - { - _parentPlateSetId = parentPlateSetId; - } - - public PlateSetType getType() - { - return _type; - } - - public void setType(PlateSetType type) - { - _type = type; - } - - public String getSelectionKey() - { - return _selectionKey; - } - - public void setSelectionKey(String selectionKey) - { - _selectionKey = selectionKey; - } - - public boolean isReplateCase() - { - return _parentPlateSetId != null && _selectionKey == null && _plates.isEmpty(); - } - - public boolean isRearrayCase() - { - return _selectionKey != null && !_plates.isEmpty(); - } - - public boolean isEmptyCase() - { - return _plates.isEmpty() && _selectionKey == null; - } - - public boolean isDefaultCase() - { - return !_plates.isEmpty() && _selectionKey == null; - } - - public Boolean getTemplate() - { - return _template; - } - - public void setTemplate(Boolean template) - { - _template = template; - } - } - @RequiresPermission(InsertPermission.class) - public static class CreatePlateSetAction extends MutatingApiAction + public static class CreatePlateSetAction extends MutatingApiAction { @Override - public void validateForm(CreatePlateSetForm form, Errors errors) - { - if (!form.isReplateCase() && !form.isRearrayCase() && !form.isEmptyCase() && !form.isDefaultCase()) - errors.reject(ERROR_GENERIC, "Invalid parameters."); - } - - @Override - public Object execute(CreatePlateSetForm form, BindException errors) throws Exception + public Object execute(CreatePlateSetOptions options, BindException errors) throws Exception { try { - PlateSetImpl plateSet = new PlateSetImpl(); - plateSet.setDescription(form.getDescription()); - plateSet.setName(form.getName()); - plateSet.setType(form.getType()); - if (form.getTemplate() != null) - plateSet.setTemplate(form.getTemplate()); - - if (form.isReplateCase()) - { - plateSet = (PlateSetImpl) PlateManager.get().replatePlateSet(getContainer(), getUser(), plateSet, form.getParentPlateSetId()); - } - else - { - List plates = form.getPlates(); - if (form.isRearrayCase()) - { - String selectionKey = StringUtils.trimToNull(form.getSelectionKey()); - if (selectionKey == null) - { - errors.reject(ERROR_REQUIRED, "Specifying a \"selectionKey\" is required for this configuration."); - return null; - } - - plates = PlateManager.get().reArrayFromSelection(getContainer(), getUser(), plates, selectionKey); - } - else - { - plates = PlateManager.get().preparePlateData(getContainer(), getUser(), plates); - } - - plateSet = PlateManager.get().createPlateSet(getContainer(), getUser(), plateSet, plates, form.getParentPlateSetId()); - } - + PlateSet plateSet = PlateManager.get().createOrAddToPlateSet(getContainer(), getUser(), options); return success(plateSet); } catch (Exception e) { - errors.reject(ERROR_GENERIC, e.getMessage() != null ? e.getMessage() : "Failed to create plate set. An error has occurred."); + String message = "Failed to create plate set."; + if (e.getMessage() != null) + message += " " + e.getMessage(); + errors.reject(ERROR_GENERIC, message); } return null; diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 9ef4d994e73..c71dfdf05f6 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -131,6 +131,7 @@ import org.labkey.assay.plate.layout.LayoutEngine; import org.labkey.assay.plate.layout.LayoutOperation; import org.labkey.assay.plate.layout.WellLayout; +import org.labkey.assay.plate.model.CreatePlateSetOptions; import org.labkey.assay.plate.model.PlateBean; import org.labkey.assay.plate.model.PlateSetAssays; import org.labkey.assay.plate.model.PlateSetLineage; @@ -2662,26 +2663,26 @@ public PlateSetImpl createPlateSet( ) throws Exception { if (!container.hasPermission(user, InsertPermission.class)) - throw new UnauthorizedException("Failed to create plate set. Insufficient permissions."); + throw new UnauthorizedException("Insufficient permissions."); if (!plateSet.isNew()) - throw new ValidationException(String.format("Failed to create plate set. Cannot create plate set with rowId (%d).", plateSet.getRowId())); + throw new ValidationException(String.format("Cannot create plate set with rowId (%d).", plateSet.getRowId())); if (plates != null && plates.size() > MAX_PLATES) - throw new ValidationException(String.format("Failed to create plate set. Plate sets can have a maximum of %d plates.", MAX_PLATES)); + throw new ValidationException(String.format("Plate sets can have a maximum of %d plates.", MAX_PLATES)); PlateSetImpl parentPlateSet = null; if (parentPlateSetId != null) { if (plateSet.isTemplate()) - throw new ValidationException("Failed to create plate set. Template plate sets do not support specifying a parent plate set."); + throw new ValidationException("Template plate sets do not support specifying a parent plate set."); parentPlateSet = (PlateSetImpl) getPlateSet(getPlateLookupContainerFilter(container, user), parentPlateSetId); if (parentPlateSet == null) - throw new ValidationException(String.format("Failed to create plate set. Parent plate set with rowId (%d) is not available.", parentPlateSetId)); + throw new ValidationException(String.format("Parent plate set with rowId (%d) is not available.", parentPlateSetId)); if (parentPlateSet.isTemplate()) - throw new ValidationException(String.format("Failed to create plate set. Parent plate set with \"%s\" is a template plate set. Template plate sets are not supported as a parent plate set.", parentPlateSet.getName())); + throw new ValidationException(String.format("Parent plate set with \"%s\" is a template plate set. Template plate sets are not supported as a parent plate set.", parentPlateSet.getName())); if (parentPlateSet.getRootPlateSetId() == null) - throw new ValidationException(String.format("Failed to create plate set. Parent plate set with rowId (%d) does not have a root plate set specified.", parentPlateSetId)); + throw new ValidationException(String.format("Parent plate set with rowId (%d) does not have a root plate set specified.", parentPlateSetId)); } if (plateSet.getType() == null) @@ -2714,28 +2715,37 @@ public PlateSetImpl createPlateSet( return plateSet; } - public PlateSet replatePlateSet( - Container container, - User user, - @NotNull PlateSetImpl plateSet, - Integer sourcePlateSetRowId - ) throws Exception + public PlateSet createOrAddToPlateSet(Container container, User user, CreatePlateSetOptions options) throws Exception { - PlateSetImpl parentPlateSet = (PlateSetImpl) requirePlateSet(container, sourcePlateSetRowId, "Failed to create plate set."); + if (!container.hasPermission(user, InsertPermission.class)) + throw new UnauthorizedException("Insufficient permissions."); - Integer parentId = parentPlateSet.isStandalone() ? null : parentPlateSet.getRowId(); + PlateSetImpl targetPlateSet = getTargetPlateSet(container, options); + List plates = options.getPlates(); - try (DbScope.Transaction tx = ensureTransaction()) + if (options.getSelectionKey() != null) { - PlateSet newPlateSet = createPlateSet(container, user, plateSet, null, parentId); + String selectionKey = StringUtils.trimToNull(options.getSelectionKey()); + if (selectionKey == null) + throw new ValidationException("Invalid selection key."); - for (Plate plate : parentPlateSet.getPlates()) - copyPlate(container, user, plate.getRowId(), false, newPlateSet.getRowId(), null, null, true); + // Re-array samples onto plates + plates = reArrayFromSelection(container, user, plates, selectionKey, options.getOperation()); + } + else + { + // Fully hydrate plate data that may be sourced from a plate template + plates = preparePlateData(container, user, plates); + } - tx.commit(); + // Create a new plate set + if (targetPlateSet.isNew()) + return createPlateSet(container, user, targetPlateSet, plates, options.getParentPlateSetId()); - return getPlateSet(container, newPlateSet.getRowId()); - } + // Update an existing plate set + addPlatesToPlateSet(container, user, targetPlateSet.getRowId(), targetPlateSet.isTemplate(), plates); + + return getPlateSet(container, targetPlateSet.getRowId()); } private void savePlateSetHeritage(Integer plateSetId, PlateSetType plateSetType, @Nullable PlateSetImpl parentPlateSet) @@ -3260,20 +3270,34 @@ Pair>> getWellSampleData( @NotNull List sampleIds, Integer rowCount, Integer columnCount, - int sampleIdsCounter - ) + int sampleIdsCounter, + @Nullable ReformatOptions.ReformatOperation operation + ) throws ValidationException { if (sampleIds.isEmpty()) - throw new IllegalArgumentException("No samples are in the current selection."); + throw new ValidationException("No samples are in the current selection."); + + if (operation == null) + operation = ReformatOptions.ReformatOperation.arrayByRow; + + Set supportedOperations = Set.of( + ReformatOptions.ReformatOperation.arrayByColumn, + ReformatOptions.ReformatOperation.arrayByRow + ); + if (!supportedOperations.contains(operation)) + throw new ValidationException(String.format("The operation \"%s\" is not supported.", operation.name())); List> wellSampleDataForPlate = new ArrayList<>(); - for (int rowIdx = 0; rowIdx < rowCount; rowIdx++) - { - for (int colIdx = 0; colIdx < columnCount; colIdx++) - { + boolean iterateByColumn = ReformatOptions.ReformatOperation.arrayByColumn.equals(operation); + + for (int outerIdx = 0; outerIdx < (iterateByColumn ? columnCount : rowCount); outerIdx++) { + for (int innerIdx = 0; innerIdx < (iterateByColumn ? rowCount : columnCount); innerIdx++) { if (sampleIdsCounter >= sampleIds.size()) return Pair.of(sampleIdsCounter, wellSampleDataForPlate); + int rowIdx = iterateByColumn ? innerIdx : outerIdx; + int colIdx = iterateByColumn ? outerIdx : innerIdx; + wellSampleDataForPlate.add(CaseInsensitiveHashMap.of( WellTable.Column.SampleID.name(), sampleIds.get(sampleIdsCounter), WellTable.Column.Type.name(), WellGroup.Type.SAMPLE.name(), @@ -3287,7 +3311,7 @@ WELL_LOCATION, createPosition(c, rowIdx, colIdx).getDescription() } /** Prepares the plate data for plates that specify a "templateId". */ - public List preparePlateData(Container container, User user, Collection plates) + private List preparePlateData(Container container, User user, Collection plates) { if (plates == null || plates.isEmpty()) return emptyList(); @@ -3335,13 +3359,17 @@ WELL_LOCATION, createPosition(container, rowIdx, colIdx).getDescription() * This is a re-array operation, so take the plate sources and apply the selected samples * according to each plate's layout. */ - public List reArrayFromSelection( + private List reArrayFromSelection( Container container, User user, List plates, - @NotNull String selectionKey + @NotNull String selectionKey, + @Nullable ReformatOptions.ReformatOperation operation ) throws ValidationException { + if (plates.isEmpty()) + throw new ValidationException("Failed to generate plate data. No plates specified."); + List selectedSampleIds = getSelection(selectionKey).stream().sorted().toList(); if (selectedSampleIds.isEmpty()) throw new ValidationException("Failed to generate plate data. No samples selected."); @@ -3374,7 +3402,7 @@ public List reArrayFromSelection( { // Iterate through sorted samples array and place them in ascending order in each plate's wells Pair>> pair; - pair = getWellSampleData(container, selectedSampleIds, plateType.getRows(), plateType.getColumns(), sampleIdsCounter); + pair = getWellSampleData(container, selectedSampleIds, plateType.getRows(), plateType.getColumns(), sampleIdsCounter, operation); platesData.add(new PlateData(plate.name, plateType.getRowId(), null, null, pair.second)); sampleIdsCounter = pair.first; } @@ -4028,7 +4056,7 @@ public record ReformatResult( if (options.getOperation() == null) throw new ValidationException("An \"operation\" must be specified."); - PlateSetImpl destinationPlateSet = getReformatDestinationPlateSet(container, options); + PlateSetImpl targetPlateSet = getReformatTargetPlateSet(container, options); Pair> source = getReformatSourcePlates(container, options); PlateSetImpl sourcePlateSet = (PlateSetImpl) source.first; List sourcePlates = source.second; @@ -4048,7 +4076,7 @@ else if (targetTemplate != null) } List wellLayouts = engine.run(container, user); - int availablePlateCount = destinationPlateSet.availablePlateCount(); + int availablePlateCount = targetPlateSet.availablePlateCount(); if (availablePlateCount < wellLayouts.size()) { @@ -4073,18 +4101,18 @@ else if (targetTemplate != null) String plateSetName; List newPlates; - if (destinationPlateSet.isNew()) + if (targetPlateSet.isNew()) { - PlateSet newPlateSet = createPlateSet(container, user, destinationPlateSet, plateData, getReformatParentPlateSetId(sourcePlateSet)); + PlateSet newPlateSet = createPlateSet(container, user, targetPlateSet, plateData, getReformatParentPlateSetId(sourcePlateSet)); plateSetRowId = newPlateSet.getRowId(); plateSetName = newPlateSet.getName(); newPlates = newPlateSet.getPlates(); } else { - plateSetRowId = destinationPlateSet.getRowId(); - plateSetName = destinationPlateSet.getName(); - newPlates = addPlatesToPlateSet(container, user, plateSetRowId, destinationPlateSet.isTemplate(), plateData); + plateSetRowId = targetPlateSet.getRowId(); + plateSetName = targetPlateSet.getName(); + newPlates = addPlatesToPlateSet(container, user, plateSetRowId, targetPlateSet.isTemplate(), plateData); } List plateRowIds = newPlates.stream().map(Plate::getRowId).toList(); @@ -4130,9 +4158,9 @@ else if (plateRowId < 1) return plateRowIds; } - private @NotNull PlateSetImpl getReformatDestinationPlateSet(Container container, ReformatOptions options) throws ValidationException + private @NotNull PlateSetImpl getReformatTargetPlateSet(Container container, ReformatOptions options) throws ValidationException { - ReformatOptions.ReformatPlateSet targetPlateSetOptions = options.getTargetPlateSet(); + ReformatOptions.TargetPlateSet targetPlateSetOptions = options.getTargetPlateSet(); if (targetPlateSetOptions == null) throw new ValidationException("A \"targetPlateSet\" must be specified."); @@ -4144,8 +4172,13 @@ else if (plateRowId < 1) else if (!hasRowId && !hasType) throw new ValidationException("Either a \"rowId\" or a \"type\" must be specified for \"targetPlateSet\"."); + return getTargetPlateSet(container, targetPlateSetOptions); + } + + private @NotNull PlateSetImpl getTargetPlateSet(Container container, ReformatOptions.TargetPlateSet targetPlateSetOptions) throws ValidationException + { PlateSetImpl plateSet; - if (hasRowId) + if (targetPlateSetOptions.getRowId() != null && targetPlateSetOptions.getRowId() > 0) { plateSet = (PlateSetImpl) requirePlateSet(container, targetPlateSetOptions.getRowId(), null); if (plateSet.isArchived()) @@ -4165,6 +4198,9 @@ else if (!hasRowId && !hasType) String description = StringUtils.trimToNull(targetPlateSetOptions.getDescription()); if (description != null) plateSet.setDescription(description); + + if (Boolean.TRUE.equals(targetPlateSetOptions.isTemplate())) + plateSet.setTemplate(true); } return plateSet; @@ -4175,7 +4211,7 @@ else if (!hasRowId && !hasType) PlateType targetPlateType = null; Plate targetTemplate = null; - ReformatOptions.ReformatPlateSource plateSource = options.getTargetPlateSource(); + ReformatOptions.TargetPlateSource plateSource = options.getTargetPlateSource(); if (plateSource != null) { if (plateSource.getSourceType() == null) @@ -4183,9 +4219,9 @@ else if (!hasRowId && !hasType) if (plateSource.getRowId() == null || plateSource.getRowId() < 1) throw new ValidationException("A \"rowId\" must be specified for \"targetPlateSource\"."); - if (ReformatOptions.ReformatPlateSource.SourceType.type.equals(plateSource.getSourceType())) + if (ReformatOptions.TargetPlateSource.SourceType.type.equals(plateSource.getSourceType())) targetPlateType = requirePlateType(plateSource.getRowId(), null); - else if (ReformatOptions.ReformatPlateSource.SourceType.template.equals(plateSource.getSourceType())) + else if (ReformatOptions.TargetPlateSource.SourceType.template.equals(plateSource.getSourceType())) { targetTemplate = requirePlate(container, plateSource.getRowId(), null); if (!targetTemplate.isTemplate()) diff --git a/assay/src/org/labkey/assay/plate/PlateManagerTest.java b/assay/src/org/labkey/assay/plate/PlateManagerTest.java index 4d847be0799..f0df82fc31d 100644 --- a/assay/src/org/labkey/assay/plate/PlateManagerTest.java +++ b/assay/src/org/labkey/assay/plate/PlateManagerTest.java @@ -37,6 +37,7 @@ import org.labkey.api.query.FieldKey; import org.labkey.api.query.QueryService; import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; import org.labkey.api.util.JunitUtil; import org.labkey.api.util.Pair; @@ -547,12 +548,12 @@ else if (row == 1) } @Test - public void testGetWellSampleData() + public void testGetWellSampleData() throws Exception { // Act List sampleIds = List.of(0, 3, 5, 8, 10, 11, 12, 13, 15, 17, 19); - Pair>> wellSampleDataFilledFull = PlateManager.get().getWellSampleData(container, sampleIds, 2, 3, 0); - Pair>> wellSampleDataFilledPartial = PlateManager.get().getWellSampleData(container, sampleIds, 2, 3, 6); + Pair>> wellSampleDataFilledFull = PlateManager.get().getWellSampleData(container, sampleIds, 2, 3, 0, null); + Pair>> wellSampleDataFilledPartial = PlateManager.get().getWellSampleData(container, sampleIds, 2, 3, 6, null); // Assert assertEquals(wellSampleDataFilledFull.first, 6, 0); @@ -575,10 +576,10 @@ public void testGetWellSampleData() // Act try { - PlateManager.get().getWellSampleData(container, Collections.emptyList(), 2, 3, 0); + PlateManager.get().getWellSampleData(container, Collections.emptyList(), 2, 3, 0, null); } // Assert - catch (IllegalArgumentException e) + catch (ValidationException e) { assertEquals("Expected validation exception", "No samples are in the current selection.", e.getMessage()); } @@ -976,7 +977,7 @@ private ReformatOptions defaultOptions() { return new ReformatOptions() .setOperation(ReformatOptions.ReformatOperation.stamp) - .setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(EMPTY_PLATE_SET_ID)); + .setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(EMPTY_PLATE_SET_ID)); } @Test @@ -988,19 +989,19 @@ public void testReformatTargetPlateSet() assertReformatThrows("A \"targetPlateSet\" must be specified.", defaultOptions().setTargetPlateSet(null)); assertReformatThrows( "Either a \"rowId\" or a \"type\" must be specified for \"targetPlateSet\".", - defaultOptions().setTargetPlateSet(new ReformatOptions.ReformatPlateSet()) + defaultOptions().setTargetPlateSet(new ReformatOptions.TargetPlateSet()) ); assertReformatThrows( "Either a \"rowId\" or a \"type\" must be specified for \"targetPlateSet\".", - defaultOptions().setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(null)) + defaultOptions().setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(null)) ); assertReformatThrows( "Either a \"rowId\" or a \"type\" must be specified for \"targetPlateSet\".", - defaultOptions().setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(0)) + defaultOptions().setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(0)) ); assertReformatThrows( "Either a \"rowId\" or a \"type\" can be specified for \"targetPlateSet\" but not both.", - defaultOptions().setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(1).setType(PlateSetType.assay)) + defaultOptions().setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(1).setType(PlateSetType.assay)) ); PlateSet archivedPlateSet = PlateManager.get().getPlateSet(container, ARCHIVED_PLATE_SET_ID); @@ -1008,7 +1009,7 @@ public void testReformatTargetPlateSet() assertReformatThrows( String.format("Plate Set \"%s\" is archived and cannot be modified.", archivedPlateSet.getName()), - defaultOptions().setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(ARCHIVED_PLATE_SET_ID)) + defaultOptions().setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(ARCHIVED_PLATE_SET_ID)) ); PlateSet fullPlateSet = PlateManager.get().getPlateSet(container, FULL_PLATE_SET_ID); @@ -1016,7 +1017,7 @@ public void testReformatTargetPlateSet() assertReformatThrows( String.format("Plate Set \"%s\" is full and cannot include additional plates.", fullPlateSet.getName()), - defaultOptions().setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(FULL_PLATE_SET_ID)) + defaultOptions().setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(FULL_PLATE_SET_ID)) ); } @@ -1074,8 +1075,8 @@ public void testReformatQuadrant() throws Exception var options = new ReformatOptions() .setOperation(ReformatOptions.ReformatOperation.quadrant) .setPlateRowIds(List.of(sourcePlate1.getRowId(), sourcePlate2.getRowId(), sourcePlate3.getRowId())) - .setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setType(PlateSetType.assay)) - .setTargetPlateSource(new ReformatOptions.ReformatPlateSource(PLATE_TYPE_384_WELLS)) + .setTargetPlateSet(new ReformatOptions.TargetPlateSet().setType(PlateSetType.assay)) + .setTargetPlateSource(new ReformatOptions.TargetPlateSource(PLATE_TYPE_384_WELLS)) .setPreview(true); // Act (preview) @@ -1155,8 +1156,8 @@ public void testReformatReverseQuadrant() throws Exception ReformatOptions options = new ReformatOptions() .setOperation(ReformatOptions.ReformatOperation.reverseQuadrant) .setPlateRowIds(List.of(sourcePlate.getRowId())) - .setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(targetPlateSetId)) - .setTargetPlateSource(new ReformatOptions.ReformatPlateSource(PLATE_TYPE_96_WELLS)) + .setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(targetPlateSetId)) + .setTargetPlateSource(new ReformatOptions.TargetPlateSource(PLATE_TYPE_96_WELLS)) .setPreview(true); // Act (preview) @@ -1234,8 +1235,8 @@ public void testReformatCompressByColumn() throws Exception ReformatOptions options = new ReformatOptions() .setOperation(ReformatOptions.ReformatOperation.columnCompression) .setPlateRowIds(List.of(sourcePlate.getRowId())) - .setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(targetPlateSetId)) - .setTargetPlateSource(new ReformatOptions.ReformatPlateSource(PLATE_TYPE_12_WELLS)) + .setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(targetPlateSetId)) + .setTargetPlateSource(new ReformatOptions.TargetPlateSource(PLATE_TYPE_12_WELLS)) .setPreview(true); // Act (preview) @@ -1315,8 +1316,8 @@ public void testReformatCompressByRow() throws Exception ReformatOptions options = new ReformatOptions() .setOperation(ReformatOptions.ReformatOperation.rowCompression) .setPlateRowIds(List.of(sourcePlate.getRowId())) - .setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(targetPlateSetId)) - .setTargetPlateSource(new ReformatOptions.ReformatPlateSource(PLATE_TYPE_12_WELLS)) + .setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(targetPlateSetId)) + .setTargetPlateSource(new ReformatOptions.TargetPlateSource(PLATE_TYPE_12_WELLS)) .setPreview(true); // Act (preview) @@ -1434,8 +1435,8 @@ public void testReformatArrayByColumn() throws Exception ReformatOptions options = new ReformatOptions() .setOperation(ReformatOptions.ReformatOperation.arrayByColumn) .setPlateRowIds(context.sourcePlates.stream().map(Plate::getRowId).toList()) - .setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(context.targetPlateSetId)) - .setTargetPlateSource(new ReformatOptions.ReformatPlateSource(PLATE_TYPE_12_WELLS)) + .setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(context.targetPlateSetId)) + .setTargetPlateSource(new ReformatOptions.TargetPlateSource(PLATE_TYPE_12_WELLS)) .setPreview(true); // Act (preview) @@ -1500,8 +1501,8 @@ public void testReformatArrayByRow() throws Exception ReformatOptions options = new ReformatOptions() .setOperation(ReformatOptions.ReformatOperation.arrayByRow) .setPlateRowIds(context.sourcePlates.stream().map(Plate::getRowId).toList()) - .setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(context.targetPlateSetId)) - .setTargetPlateSource(new ReformatOptions.ReformatPlateSource(PLATE_TYPE_12_WELLS)) + .setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(context.targetPlateSetId)) + .setTargetPlateSource(new ReformatOptions.TargetPlateSource(PLATE_TYPE_12_WELLS)) .setPreview(true); // Act (preview) @@ -1585,8 +1586,8 @@ public void testReformatArrayFromTemplate() throws Exception ReformatOptions options = new ReformatOptions() .setOperation(ReformatOptions.ReformatOperation.arrayFromTemplate) .setPlateRowIds(context.sourcePlates.stream().map(Plate::getRowId).toList()) - .setTargetPlateSet(new ReformatOptions.ReformatPlateSet().setRowId(context.targetPlateSetId)) - .setTargetPlateSource(new ReformatOptions.ReformatPlateSource(template)) + .setTargetPlateSet(new ReformatOptions.TargetPlateSet().setRowId(context.targetPlateSetId)) + .setTargetPlateSource(new ReformatOptions.TargetPlateSource(template)) .setPreview(true); // Act (preview) diff --git a/assay/src/org/labkey/assay/plate/model/CreatePlateSetOptions.java b/assay/src/org/labkey/assay/plate/model/CreatePlateSetOptions.java new file mode 100644 index 00000000000..0f6dae10cfb --- /dev/null +++ b/assay/src/org/labkey/assay/plate/model/CreatePlateSetOptions.java @@ -0,0 +1,43 @@ +package org.labkey.assay.plate.model; + +import org.labkey.assay.plate.PlateManager; + +import java.util.ArrayList; +import java.util.List; + +public class CreatePlateSetOptions extends ReformatOptions.TargetPlateSet +{ + private ReformatOptions.ReformatOperation _operation; + private List _plates = new ArrayList<>(); + private String _selectionKey; + + public ReformatOptions.ReformatOperation getOperation() + { + return _operation; + } + + public void setOperation(ReformatOptions.ReformatOperation operation) + { + _operation = operation; + } + + public List getPlates() + { + return _plates; + } + + public void setPlates(List plates) + { + _plates = plates; + } + + public String getSelectionKey() + { + return _selectionKey; + } + + public void setSelectionKey(String selectionKey) + { + _selectionKey = selectionKey; + } +} diff --git a/assay/src/org/labkey/assay/plate/model/ReformatOptions.java b/assay/src/org/labkey/assay/plate/model/ReformatOptions.java index 5fe72c8919a..9305f4ae194 100644 --- a/assay/src/org/labkey/assay/plate/model/ReformatOptions.java +++ b/assay/src/org/labkey/assay/plate/model/ReformatOptions.java @@ -21,12 +21,13 @@ public enum ReformatOperation stamp } - public static class ReformatPlateSet + public static class TargetPlateSet { private Integer _rowId; private String _description; private String _name; private Integer _parentPlateSetId; + private Boolean _template; private PlateSetType _type; public Integer getRowId() @@ -34,7 +35,7 @@ public Integer getRowId() return _rowId; } - public ReformatPlateSet setRowId(Integer rowId) + public TargetPlateSet setRowId(Integer rowId) { _rowId = rowId; return this; @@ -45,7 +46,7 @@ public String getDescription() return _description; } - public ReformatPlateSet setDescription(String description) + public TargetPlateSet setDescription(String description) { _description = description; return this; @@ -56,7 +57,7 @@ public String getName() return _name; } - public ReformatPlateSet setName(String name) + public TargetPlateSet setName(String name) { _name = name; return this; @@ -67,7 +68,7 @@ public PlateSetType getType() return _type; } - public ReformatPlateSet setType(PlateSetType type) + public TargetPlateSet setType(PlateSetType type) { _type = type; return this; @@ -78,14 +79,25 @@ public Integer getParentPlateSetId() return _parentPlateSetId; } - public ReformatPlateSet setParentPlateSetId(Integer parentPlateSetId) + public TargetPlateSet setParentPlateSetId(Integer parentPlateSetId) { _parentPlateSetId = parentPlateSetId; return this; } + + public Boolean isTemplate() + { + return _template; + } + + public TargetPlateSet setTemplate(Boolean template) + { + _template = template; + return this; + } } - public static class ReformatPlateSource + public static class TargetPlateSource { public enum SourceType { @@ -96,17 +108,17 @@ public enum SourceType private Integer _rowId; private SourceType _sourceType; - public ReformatPlateSource() + public TargetPlateSource() { } - public ReformatPlateSource(@NotNull PlateType plateType) + public TargetPlateSource(@NotNull PlateType plateType) { _sourceType = SourceType.type; _rowId = plateType.getRowId(); } - public ReformatPlateSource(@NotNull Plate template) + public TargetPlateSource(@NotNull Plate template) { _sourceType = SourceType.template; _rowId = template.getRowId(); @@ -138,8 +150,8 @@ public void setSourceType(SourceType sourceType) private String _plateSelectionKey; private Boolean _preview = false; private Boolean _previewData = true; - private ReformatPlateSet _targetPlateSet; - private ReformatPlateSource _targetPlateSource; + private TargetPlateSet _targetPlateSet; + private TargetPlateSource _targetPlateSource; public ReformatOperation getOperation() { @@ -195,23 +207,23 @@ public void setPreviewData(Boolean previewData) _previewData = previewData; } - public ReformatPlateSet getTargetPlateSet() + public TargetPlateSet getTargetPlateSet() { return _targetPlateSet; } - public ReformatOptions setTargetPlateSet(ReformatPlateSet targetPlateSet) + public ReformatOptions setTargetPlateSet(TargetPlateSet targetPlateSet) { _targetPlateSet = targetPlateSet; return this; } - public ReformatPlateSource getTargetPlateSource() + public TargetPlateSource getTargetPlateSource() { return _targetPlateSource; } - public ReformatOptions setTargetPlateSource(ReformatPlateSource targetPlateSource) + public ReformatOptions setTargetPlateSource(TargetPlateSource targetPlateSource) { _targetPlateSource = targetPlateSource; return this; From 1bc5365bc90ffe72a2ccdc29865c6ef7e7b127c9 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Tue, 31 Dec 2024 10:04:59 -0800 Subject: [PATCH 02/19] Reformat: Initial support for sourcing from sampleIds, plate metadata --- .../org/labkey/assay/plate/PlateManager.java | 33 +++++---- .../assay/plate/layout/ArrayOperation.java | 70 ++++++++++++++++--- .../assay/plate/layout/LayoutEngine.java | 40 +++++++---- .../assay/plate/layout/LayoutOperation.java | 28 ++++++-- .../assay/plate/layout/QuadrantOperation.java | 4 +- .../layout/ReverseQuadrantOperation.java | 4 +- .../assay/plate/model/ReformatOptions.java | 24 +++++++ 7 files changed, 161 insertions(+), 42 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index c71dfdf05f6..0ecf253ae3e 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -4058,22 +4058,16 @@ public record ReformatResult( PlateSetImpl targetPlateSet = getReformatTargetPlateSet(container, options); Pair> source = getReformatSourcePlates(container, options); + Pair targetPlateSource = getReformatTargetPlateSource(container, options); PlateSetImpl sourcePlateSet = (PlateSetImpl) source.first; List sourcePlates = source.second; - Pair targetPlateSource = getReformatTargetPlateSource(container, options); - PlateType targetPlateType = targetPlateSource.first; - Plate targetTemplate = targetPlateSource.second; - - LayoutEngine engine = new LayoutEngine(options, sourcePlates, getPlateTypes()); - - if (targetPlateType != null) - engine.setTargetPlateType(targetPlateType); - else if (targetTemplate != null) - { - List targetTemplateWellData = getWellData(container, user, targetTemplate.getRowId(), false, false); - engine.setTargetTemplate(targetTemplate, targetTemplateWellData); - } + LayoutEngine engine = new LayoutEngine(options, getPlateTypes()); + engine.setSourcePlates(sourcePlates); + engine.setSampleIds(getSelectedSampleIds(options)); + engine.setTargetPlateData(options.getPlates()); + engine.setTargetPlateType(targetPlateSource.first); + engine.setTargetTemplate(targetPlateSource.second); List wellLayouts = engine.run(container, user); int availablePlateCount = targetPlateSet.availablePlateCount(); @@ -4261,6 +4255,19 @@ else if (!Objects.equals(sourcePlateSet.getRowId(), plateSet.getRowId())) return Pair.of(sourcePlateSet, sourcePlates); } + private Collection getSelectedSampleIds(ReformatOptions options) throws ValidationException + { + String selectionKey = StringUtils.trimToNull(options.getSampleSelectionKey()); + if (selectionKey == null) + return Collections.emptyList(); + + List sampleIds = getSelection(selectionKey).stream().toList(); + if (sampleIds.isEmpty()) + throw new ValidationException("Empty sample selection."); + + return sampleIds; + } + private @NotNull Pair, Integer> hydratePlateDataFromWellLayout( Container container, User user, diff --git a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java index 81628795206..dcc2a8435da 100644 --- a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java @@ -12,6 +12,7 @@ import org.labkey.assay.plate.data.WellData; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -30,6 +31,7 @@ public enum Layout private final Layout _layout; private Map _sampleWells; + private List _targetTemplateWellData; public ArrayOperation(@NotNull Layout layout) { @@ -43,15 +45,18 @@ public List execute(ExecutionContext context) throws ValidationExcep return emptyList(); if (Layout.Column.equals(_layout) || Layout.Row.equals(_layout)) - return executeRowColumnLayout(context.targetPlateType()); + return executeRowColumnLayout(context); else if (Layout.Template.equals(_layout)) - return executeTemplateLayout(context.targetTemplate(), context.targetTemplateWellData()); + return executeTemplateLayout(context.targetTemplate(), _targetTemplateWellData); throw new UnsupportedOperationException(String.format("The layout \"%s\" is not supported.", _layout)); } - private List executeRowColumnLayout(PlateType targetPlateType) + private List executeRowColumnLayout(ExecutionContext context) { + PlateType targetPlateType = context.targetPlateType(); + List plateData = context.plateData(); + List layouts = new ArrayList<>(); WellLayout target = null; boolean isColumnLayout = Layout.Column.equals(_layout); @@ -65,10 +70,32 @@ private List executeRowColumnLayout(PlateType targetPlateType) for (Map.Entry entry : _sampleWells.entrySet()) { if (target == null) - target = new WellLayout(targetPlateType, true, null); + { + if (plateData != null && plateData.size() > layouts.size()) + { + PlateManager.PlateData targetPlateData = plateData.get(layouts.size() - 1); + if (targetPlateData.plateType() != null && targetPlateData.plateType() > 0) + { + PlateType targetPlateDataType = context.resolvePlateType(targetPlateData.plateType()); + if (targetPlateDataType != null) + { + target = new WellLayout(targetPlateDataType, true, null); + targetCols = targetPlateDataType.getColumns(); + targetRows = targetPlateDataType.getRows(); + } + } + } + + if (target == null) + { + target = new WellLayout(targetPlateType, true, null); + targetCols = targetPlateType.getColumns(); + targetRows = targetPlateType.getRows(); + } + } WellLayout.Well sourceWell = entry.getValue(); - target.setWell(targetRowIdx, targetColIdx, sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx()); + target.setWell(targetRowIdx, targetColIdx, sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sourceWell.sourceSampleId()); if (isColumnLayout) { @@ -186,12 +213,33 @@ else if (isSampleOrReplicate) } @Override - public void init(Container container, User user, ExecutionContext context, List allPlateTypes) + public void init(Container container, User user, ExecutionContext context) throws ValidationException { - _sampleWells = getSampleWellsFromSourcePlates(container, user, context.sourcePlates()); + if (!context.sourcePlates().isEmpty()) + _sampleWells = generateSampleWellsFromSourcePlates(container, user, context.sourcePlates()); + else if (context.sampleIds() != null && !context.sampleIds().isEmpty()) + _sampleWells = generateSampleWellsFromSampleIds(context.sampleIds()); + else + throw new ValidationException("Invalid configuration. Either source plates or source samples must be provided."); + + if (context.targetTemplate() != null) + _targetTemplateWellData = PlateManager.get().getWellData(container, user, context.targetTemplate().getRowId(), false, false); } - private Map getSampleWellsFromSourcePlates(Container container, User user, @NotNull List sourcePlates) + private Map generateSampleWellsFromSampleIds(Collection sampleIds) + { + LinkedHashMap sampleWells = new LinkedHashMap<>(); + + for (Integer sampleId : sampleIds) + { + if (!sampleWells.containsKey(sampleId)) + sampleWells.put(sampleId, new WellLayout.Well(-1, -1, -1, -1, -1, sampleId)); + } + + return sampleWells; + } + + private Map generateSampleWellsFromSourcePlates(Container container, User user, @NotNull List sourcePlates) { LinkedHashMap sampleWells = new LinkedHashMap<>(); @@ -213,6 +261,12 @@ private Map getSampleWellsFromSourcePlates(Container c return sampleWells; } + @Override + public boolean requiresSourcePlates() + { + return false; + } + @Override public boolean requiresTargetPlateType() { diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java index 72682145754..317fdb05174 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java @@ -5,9 +5,10 @@ import org.labkey.api.data.Container; import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; -import org.labkey.assay.plate.data.WellData; +import org.labkey.assay.plate.PlateManager; import org.labkey.assay.plate.model.ReformatOptions; +import java.util.Collection; import java.util.List; public class LayoutEngine @@ -15,24 +16,23 @@ public class LayoutEngine private final List _allPlateTypes; private final LayoutOperation _operation; private final ReformatOptions _options; - private final List _sourcePlates; + private Collection _sampleIds; + private List _sourcePlates; + private List _targetPlateData; private PlateType _targetPlateType; private Plate _targetTemplate; - private List _targetTemplateWellData; - public LayoutEngine(ReformatOptions options, List sourcePlates, List allPlateTypes) + public LayoutEngine(ReformatOptions options, List allPlateTypes) { _operation = layoutOperationFactory(options); _options = options; - _sourcePlates = sourcePlates; _allPlateTypes = allPlateTypes; } public List run(Container container, User user) throws ValidationException { - if (_sourcePlates.isEmpty()) + if (_operation.requiresSourcePlates() && _sourcePlates.isEmpty()) throw new ValidationException("Invalid configuration. Source plates are required to run the layout engine."); - if (_operation.requiresTargetPlateType() && _targetPlateType == null) throw new ValidationException("A target plate type is required for this operation."); if (_operation.requiresTargetTemplate() && _targetTemplate == null) @@ -40,13 +40,15 @@ public List run(Container container, User user) throws ValidationExc LayoutOperation.ExecutionContext context = new LayoutOperation.ExecutionContext( _options, - _sourcePlates, + _allPlateTypes, _targetPlateType, + _sourcePlates, _targetTemplate, - _targetTemplateWellData + _targetPlateData, + _sampleIds ); - _operation.init(container, user, context, _allPlateTypes); + _operation.init(container, user, context); return _operation.execute(context); } @@ -71,14 +73,28 @@ private static LayoutOperation layoutOperationFactory(ReformatOptions reformatOp }; } + public void setSampleIds(Collection sampleIds) + { + _sampleIds = sampleIds; + } + + public void setSourcePlates(List sourcePlates) + { + _sourcePlates = sourcePlates; + } + + public void setTargetPlateData(List targetPlateData) + { + _targetPlateData = targetPlateData; + } + public void setTargetPlateType(PlateType targetPlateType) { _targetPlateType = targetPlateType; } - public void setTargetTemplate(Plate targetTemplate, List targetTemplateWellData) + public void setTargetTemplate(Plate targetTemplate) { _targetTemplate = targetTemplate; - _targetTemplateWellData = targetTemplateWellData; } } diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java index f7b280dd590..3987bac0ddc 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java @@ -1,20 +1,22 @@ package org.labkey.assay.plate.layout; +import org.jetbrains.annotations.Nullable; import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.PlateType; import org.labkey.api.data.Container; import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; -import org.labkey.assay.plate.data.WellData; +import org.labkey.assay.plate.PlateManager; import org.labkey.assay.plate.model.ReformatOptions; +import java.util.Collection; import java.util.List; public interface LayoutOperation { List execute(ExecutionContext context) throws ValidationException; - default void init(Container container, User user, ExecutionContext context, List allPlateTypes) throws ValidationException + default void init(Container container, User user, ExecutionContext context) throws ValidationException { } @@ -23,6 +25,11 @@ default boolean produceEmptyPlates() return false; } + default boolean requiresSourcePlates() + { + return true; + } + default boolean requiresTargetPlateType() { return false; @@ -35,9 +42,20 @@ default boolean requiresTargetTemplate() record ExecutionContext( ReformatOptions options, - List sourcePlates, + List allPlateTypes, PlateType targetPlateType, + List sourcePlates, Plate targetTemplate, - List targetTemplateWellData - ) {}; + List plateData, + Collection sampleIds + ) + { + public @Nullable PlateType resolvePlateType(Integer plateTypeRowId) + { + if (allPlateTypes == null || allPlateTypes.isEmpty()) + return null; + + return allPlateTypes.stream().filter(plateType -> plateType.getRowId().equals(plateTypeRowId)).findFirst().orElse(null); + } + } } diff --git a/assay/src/org/labkey/assay/plate/layout/QuadrantOperation.java b/assay/src/org/labkey/assay/plate/layout/QuadrantOperation.java index 22552fc64ca..8ead65c9c79 100644 --- a/assay/src/org/labkey/assay/plate/layout/QuadrantOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/QuadrantOperation.java @@ -63,10 +63,10 @@ else if (quadrant == 3) } @Override - public void init(Container container, User user, ExecutionContext context, List allPlateTypes) throws ValidationException + public void init(Container container, User user, ExecutionContext context) throws ValidationException { _sourcePlateType = getSourcePlateType(context.sourcePlates()); - _targetPlateType = getTargetPlateType(_sourcePlateType, allPlateTypes); + _targetPlateType = getTargetPlateType(_sourcePlateType, context.allPlateTypes()); } private @NotNull PlateType getSourcePlateType(@NotNull List sourcePlates) throws ValidationException diff --git a/assay/src/org/labkey/assay/plate/layout/ReverseQuadrantOperation.java b/assay/src/org/labkey/assay/plate/layout/ReverseQuadrantOperation.java index 2a21477c010..9586b967ee7 100644 --- a/assay/src/org/labkey/assay/plate/layout/ReverseQuadrantOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ReverseQuadrantOperation.java @@ -64,12 +64,12 @@ else if (c > lastTargetCol) } @Override - public void init(Container container, User user, ExecutionContext context, List allPlateTypes) throws ValidationException + public void init(Container container, User user, ExecutionContext context) throws ValidationException { if (context.sourcePlates().size() != 1) throw new ValidationException("The reverse quadrant operation requires a single source plate."); - _targetPlateType = getTargetPlateType(context.sourcePlates().get(0).getPlateType(), allPlateTypes); + _targetPlateType = getTargetPlateType(context.sourcePlates().get(0).getPlateType(), context.allPlateTypes()); } private @NotNull PlateType getTargetPlateType(@NotNull PlateType sourcePlateType, List allPlateTypes) throws ValidationException diff --git a/assay/src/org/labkey/assay/plate/model/ReformatOptions.java b/assay/src/org/labkey/assay/plate/model/ReformatOptions.java index 9305f4ae194..62b1bf15d63 100644 --- a/assay/src/org/labkey/assay/plate/model/ReformatOptions.java +++ b/assay/src/org/labkey/assay/plate/model/ReformatOptions.java @@ -4,6 +4,7 @@ import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.PlateSetType; import org.labkey.api.assay.plate.PlateType; +import org.labkey.assay.plate.PlateManager; import java.util.List; @@ -146,10 +147,12 @@ public void setSourceType(SourceType sourceType) } private ReformatOperation _operation; + private List _plates; private List _plateRowIds; private String _plateSelectionKey; private Boolean _preview = false; private Boolean _previewData = true; + private String _sampleSelectionKey; private TargetPlateSet _targetPlateSet; private TargetPlateSource _targetPlateSource; @@ -164,6 +167,16 @@ public ReformatOptions setOperation(ReformatOperation operation) return this; } + public List getPlates() + { + return _plates; + } + + public void setPlates(List plates) + { + _plates = plates; + } + public List getPlateRowIds() { return _plateRowIds; @@ -207,6 +220,17 @@ public void setPreviewData(Boolean previewData) _previewData = previewData; } + public String getSampleSelectionKey() + { + return _sampleSelectionKey; + } + + public ReformatOptions setSampleSelectionKey(String sampleSelectionKey) + { + _sampleSelectionKey = sampleSelectionKey; + return this; + } + public TargetPlateSet getTargetPlateSet() { return _targetPlateSet; From c45a7aee9203ac63d6d1eeaab0a7843249ba41cf Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Tue, 31 Dec 2024 12:02:42 -0800 Subject: [PATCH 03/19] Reformat: support plate metadata, array individualized plate layouts --- .../org/labkey/assay/plate/PlateManager.java | 240 +++++++++++------- .../labkey/assay/plate/PlateManagerTest.java | 2 +- .../assay/plate/layout/ArrayOperation.java | 222 ++++++++-------- .../assay/plate/layout/LayoutEngine.java | 2 + .../assay/plate/layout/LayoutOperation.java | 9 + 5 files changed, 272 insertions(+), 203 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 0ecf253ae3e..77252dbe8a7 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -3290,8 +3290,10 @@ Pair>> getWellSampleData( List> wellSampleDataForPlate = new ArrayList<>(); boolean iterateByColumn = ReformatOptions.ReformatOperation.arrayByColumn.equals(operation); - for (int outerIdx = 0; outerIdx < (iterateByColumn ? columnCount : rowCount); outerIdx++) { - for (int innerIdx = 0; innerIdx < (iterateByColumn ? rowCount : columnCount); innerIdx++) { + for (int outerIdx = 0; outerIdx < (iterateByColumn ? columnCount : rowCount); outerIdx++) + { + for (int innerIdx = 0; innerIdx < (iterateByColumn ? rowCount : columnCount); innerIdx++) + { if (sampleIdsCounter >= sampleIds.size()) return Pair.of(sampleIdsCounter, wellSampleDataForPlate); @@ -4056,22 +4058,25 @@ public record ReformatResult( if (options.getOperation() == null) throw new ValidationException("An \"operation\" must be specified."); + // Initialize / validate engine configuration + LayoutEngine engine = new LayoutEngine(options, getPlateTypes()); + PlateSetImpl targetPlateSet = getReformatTargetPlateSet(container, options); - Pair> source = getReformatSourcePlates(container, options); - Pair targetPlateSource = getReformatTargetPlateSource(container, options); + Pair> source = getReformatSourcePlates(container, options, engine.getOperation()); PlateSetImpl sourcePlateSet = (PlateSetImpl) source.first; List sourcePlates = source.second; - - LayoutEngine engine = new LayoutEngine(options, getPlateTypes()); engine.setSourcePlates(sourcePlates); - engine.setSampleIds(getSelectedSampleIds(options)); - engine.setTargetPlateData(options.getPlates()); + + Pair targetPlateSource = getReformatTargetPlateSource(container, options); engine.setTargetPlateType(targetPlateSource.first); engine.setTargetTemplate(targetPlateSource.second); + engine.setSampleIds(getSelectedSampleIds(options)); + engine.setTargetPlateData(options.getPlates()); + // Execute plate layout List wellLayouts = engine.run(container, user); - int availablePlateCount = targetPlateSet.availablePlateCount(); + int availablePlateCount = targetPlateSet.availablePlateCount(); if (availablePlateCount < wellLayouts.size()) { throw new ValidationException(String.format( @@ -4081,7 +4086,8 @@ public record ReformatResult( )); } - Pair, Integer> hydratedResults = hydratePlateDataFromWellLayout(container, user, wellLayouts, engine.getOperation()); + // Populate plate data from well layouts + Pair, Integer> hydratedResults = hydratePlateDataFromWellLayout(container, user, wellLayouts, options.getPlates(), engine.getOperation()); List plateData = hydratedResults.first; Integer platedSampleCount = hydratedResults.second; @@ -4120,7 +4126,7 @@ public record ReformatResult( return null; } - private @NotNull List getReformatPlateRowIds(ReformatOptions options) throws ValidationException + private @NotNull List getSourcePlateRowIds(ReformatOptions options, LayoutOperation layoutOperation) throws ValidationException { boolean hasPlateRowIds = options.getPlateRowIds() != null && !options.getPlateRowIds().isEmpty(); @@ -4129,16 +4135,16 @@ public record ReformatResult( if (hasPlateRowIds && hasPlateSelectionKey) throw new ValidationException("Either \"plateRowIds\" or \"plateSelectionKey\" can be specified but not both."); - else if (!hasPlateRowIds && !hasPlateSelectionKey) - throw new ValidationException("Either \"plateRowIds\" or \"plateSelectionKey\" must be specified."); + else if (!hasPlateRowIds && !hasPlateSelectionKey && layoutOperation.requiresSourcePlates()) + throw new ValidationException("Either \"plateRowIds\" or \"plateSelectionKey\" must be specified for this operation."); - List plateRowIds; + List plateRowIds = emptyList(); if (hasPlateRowIds) plateRowIds = options.getPlateRowIds(); - else + else if (selectionKey != null) plateRowIds = getSelection(selectionKey).stream().toList(); - if (plateRowIds.isEmpty()) + if (plateRowIds.isEmpty() && layoutOperation.requiresSourcePlates()) throw new ValidationException("No source plates are specified."); for (Integer plateRowId : plateRowIds) @@ -4230,11 +4236,15 @@ else if (ReformatOptions.TargetPlateSource.SourceType.template.equals(plateSourc return Pair.of(targetPlateType, targetTemplate); } - private Pair> getReformatSourcePlates(Container container, ReformatOptions options) throws ValidationException + private Pair> getReformatSourcePlates( + Container container, + ReformatOptions options, + LayoutOperation layoutOperation + ) throws ValidationException { List sourcePlates = new ArrayList<>(); PlateSet sourcePlateSet = null; - for (Integer plateRowId : getReformatPlateRowIds(options)) + for (Integer plateRowId : getSourcePlateRowIds(options, layoutOperation)) { Plate sourcePlate = requirePlate(container, plateRowId, null); PlateSet plateSet = sourcePlate.getPlateSet(); @@ -4268,10 +4278,119 @@ private Collection getSelectedSampleIds(ReformatOptions options) throws return sampleIds; } + private void hydrateFromPlate( + Container container, + User user, + WellLayout wellLayout, + Set platedSampleIds, + Map> sourceWellDataMap, + List> targetWellData + ) + { + for (WellLayout.Well well : wellLayout.getWells()) + { + if (well == null) + continue; + + int sourcePlateId = well.sourcePlateId(); + + if (sourcePlateId > 0) + { + List sourceWellData = sourceWellDataMap.computeIfAbsent( + sourcePlateId, + (plateRowId) -> getWellData(container, user, plateRowId, true, true) + ); + + for (WellData wellData : sourceWellData) + { + if (!wellData.hasData()) + continue; + + if (wellData.getRow() == well.sourceRowIdx() && wellData.getCol() == well.sourceColIdx()) + { + Position p = new PositionImpl(container, well.destinationRowIdx(), well.destinationColIdx()); + + WellData d = new WellData(); + d.setPosition(p.getDescription()); + d.setSampleId(wellData.getSampleId()); + + if (wellLayout.isSampleOnly()) + { + d.setType(WellGroup.Type.SAMPLE); + if (d.getSampleId() != null) + platedSampleIds.add(d.getSampleId()); + } + else + { + d.setMetadata(wellData.getMetadata()); + d.setWellGroup(wellData.getWellGroup()); + d.setType(wellData.getType()); + } + + targetWellData.add(d.getData()); + break; + } + } + } + else if (well.sourceSampleId() != null) + { + Position p = new PositionImpl(container, well.destinationRowIdx(), well.destinationColIdx()); + + WellData d = new WellData(); + d.setPosition(p.getDescription()); + d.setType(WellGroup.Type.SAMPLE); + d.setSampleId(well.sourceSampleId()); + + targetWellData.add(d.getData()); + } + } + } + + private void hydrateFromPlateTemplate( + Container container, + User user, + WellLayout wellLayout, + Set platedSampleIds, + Map> sourceWellDataMap, + List> targetWellData + ) + { + List templateWellData = sourceWellDataMap.computeIfAbsent( + wellLayout.getTargetTemplateId(), + (templateRowId) -> getWellData(container, user, templateRowId, false, true) + ); + + for (WellData wellData : templateWellData) + { + WellData d = new WellData(); + + int rowIdx = wellData.getRow(); + int colIdx = wellData.getCol(); + Position p = new PositionImpl(container, rowIdx, colIdx); + d.setPosition(p.getDescription()); + + WellLayout.Well well = wellLayout.getWell(rowIdx, colIdx); + if (well != null) + { + Integer sampleId = well.sourceSampleId(); + d.setSampleId(sampleId); + if (sampleId != null) + platedSampleIds.add(sampleId); + } + + d.setMetadata(wellData.getMetadata()); + d.setWellGroup(wellData.getWellGroup()); + d.setType(wellData.getType()); + + targetWellData.add(d.getData()); + } + } + private @NotNull Pair, Integer> hydratePlateDataFromWellLayout( Container container, User user, List wellLayouts, + List plateData, LayoutOperation operation ) { @@ -4281,90 +4400,35 @@ private Collection getSelectedSampleIds(ReformatOptions options) throws List plates = new ArrayList<>(); Map> sourceWellDataMap = new HashMap<>(); Set platedSampleIds = new HashSet<>(); + int plateDataIndex = 0; for (WellLayout wellLayout : wellLayouts) { List> targetWellData = new ArrayList<>(); if (wellLayout.getTargetTemplateId() != null) - { - List templateWellData = sourceWellDataMap.computeIfAbsent( - wellLayout.getTargetTemplateId(), - (templateRowId) -> getWellData(container, user, templateRowId, false, true) - ); - - for (WellData wellData : templateWellData) - { - WellData d = new WellData(); - - int rowIdx = wellData.getRow(); - int colIdx = wellData.getCol(); - Position p = new PositionImpl(container, rowIdx, colIdx); - d.setPosition(p.getDescription()); - - WellLayout.Well well = wellLayout.getWell(rowIdx, colIdx); - if (well != null) - { - Integer sampleId = well.sourceSampleId(); - d.setSampleId(sampleId); - if (sampleId != null) - platedSampleIds.add(sampleId); - } - - d.setMetadata(wellData.getMetadata()); - d.setWellGroup(wellData.getWellGroup()); - d.setType(wellData.getType()); - - targetWellData.add(d.getData()); - } - } + hydrateFromPlateTemplate(container, user, wellLayout, platedSampleIds, sourceWellDataMap, targetWellData); else + hydrateFromPlate(container, user, wellLayout, platedSampleIds, sourceWellDataMap, targetWellData); + + if (operation.produceEmptyPlates() || !targetWellData.isEmpty()) { - for (WellLayout.Well well : wellLayout.getWells()) + String name = null; + String barcode = null; + if (plateData != null && plateData.size() > plateDataIndex) { - if (well == null) - continue; - - List sourceWellData = sourceWellDataMap.computeIfAbsent( - well.sourcePlateId(), - (plateRowId) -> getWellData(container, user, plateRowId, true, true) - ); - - for (WellData wellData : sourceWellData) + PlateData data = plateData.get(plateDataIndex); + if (data != null) { - if (!wellData.hasData()) - continue; - - if (wellData.getRow() == well.sourceRowIdx() && wellData.getCol() == well.sourceColIdx()) - { - Position p = new PositionImpl(container, well.destinationRowIdx(), well.destinationColIdx()); - - WellData d = new WellData(); - d.setPosition(p.getDescription()); - d.setSampleId(wellData.getSampleId()); - - if (wellLayout.isSampleOnly()) - { - d.setType(WellGroup.Type.SAMPLE); - if (d.getSampleId() != null) - platedSampleIds.add(d.getSampleId()); - } - else - { - d.setMetadata(wellData.getMetadata()); - d.setWellGroup(wellData.getWellGroup()); - d.setType(wellData.getType()); - } - - targetWellData.add(d.getData()); - break; - } + name = data.name(); + barcode = data.barcode(); } } + + plates.add(new PlateData(name, wellLayout.getPlateType().getRowId(), null, barcode, targetWellData)); } - if (operation.produceEmptyPlates() || !targetWellData.isEmpty()) - plates.add(new PlateData(null, wellLayout.getPlateType().getRowId(), null, null, targetWellData)); + plateDataIndex++; } return Pair.of(plates, platedSampleIds.isEmpty() ? null : platedSampleIds.size()); diff --git a/assay/src/org/labkey/assay/plate/PlateManagerTest.java b/assay/src/org/labkey/assay/plate/PlateManagerTest.java index f0df82fc31d..e7ac5ad220e 100644 --- a/assay/src/org/labkey/assay/plate/PlateManagerTest.java +++ b/assay/src/org/labkey/assay/plate/PlateManagerTest.java @@ -1029,7 +1029,7 @@ public void testReformatSourcePlates() throws Exception defaultOptions().setPlateRowIds(List.of(1234)).setPlateSelectionKey("1234") ); assertReformatThrows( - "Either \"plateRowIds\" or \"plateSelectionKey\" must be specified.", + "Either \"plateRowIds\" or \"plateSelectionKey\" must be specified for this operation.", defaultOptions().setPlateRowIds(null).setPlateSelectionKey(" ") ); assertReformatThrows("No source plates are specified.", defaultOptions().setPlateSelectionKey("1234")); diff --git a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java index dcc2a8435da..b1e0b9e69d9 100644 --- a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java @@ -18,8 +18,6 @@ import java.util.List; import java.util.Map; -import static java.util.Collections.emptyList; - public class ArrayOperation implements LayoutOperation { public enum Layout @@ -31,7 +29,6 @@ public enum Layout private final Layout _layout; private Map _sampleWells; - private List _targetTemplateWellData; public ArrayOperation(@NotNull Layout layout) { @@ -41,61 +38,80 @@ public ArrayOperation(@NotNull Layout layout) @Override public List execute(ExecutionContext context) throws ValidationException { - if (_sampleWells.isEmpty()) - return emptyList(); + int sampleIndex = 0; + List layouts = new ArrayList<>(); + Map, Integer> groupSampleMap = new HashMap<>(); + + List sampleIds = new ArrayList<>(); + for (Map.Entry entry : _sampleWells.entrySet()) + sampleIds.add(entry.getKey()); - if (Layout.Column.equals(_layout) || Layout.Row.equals(_layout)) - return executeRowColumnLayout(context); - else if (Layout.Template.equals(_layout)) - return executeTemplateLayout(context.targetTemplate(), _targetTemplateWellData); + while (sampleIndex < sampleIds.size()) + { + WellLayout wellLayout = getNextWellLayout(context, layouts.size()); + Pair result; + + if (wellLayout.getTargetTemplateId() != null) + result = executeTemplateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleIndex); + else + result = executeRowColumnLayout(wellLayout, sampleIds, sampleIndex); - throw new UnsupportedOperationException(String.format("The layout \"%s\" is not supported.", _layout)); + layouts.add(result.second); + sampleIndex = result.first; + } + + // TODO: Does this need to generate additional plates or can that be done at hydration station? + + return layouts; } - private List executeRowColumnLayout(ExecutionContext context) + private @NotNull WellLayout getNextWellLayout(ExecutionContext context, int numLayouts) { - PlateType targetPlateType = context.targetPlateType(); + WellLayout layout = null; List plateData = context.plateData(); - List layouts = new ArrayList<>(); - WellLayout target = null; - boolean isColumnLayout = Layout.Column.equals(_layout); + if (plateData != null && plateData.size() > numLayouts) + { + PlateManager.PlateData targetPlateData = plateData.get(numLayouts - 1); + if (targetPlateData.plateType() != null && targetPlateData.plateType() > 0) + { + PlateType targetPlateDataType = context.resolvePlateType(targetPlateData.plateType()); + if (targetPlateDataType != null) + { + if (targetPlateData.templateId() != null) + layout = new WellLayout(targetPlateDataType, false, targetPlateData.templateId()); + else + layout = new WellLayout(targetPlateDataType, true, null); + } + } + } + + if (layout == null) + { + if (context.targetTemplate() != null) + layout = new WellLayout(context.targetTemplate().getPlateType(), false, context.targetTemplate().getRowId()); + else + layout = new WellLayout(context.targetPlateType(), true, null); + } + + return layout; + } + private Pair executeRowColumnLayout(WellLayout target, List sampleIds, int sampleIndex) + { + PlateType targetPlateType = target.getPlateType(); + boolean isColumnLayout = Layout.Column.equals(_layout); int targetCols = targetPlateType.getColumns(); int targetRows = targetPlateType.getRows(); - int targetColIdx = 0; int targetRowIdx = 0; + int sampleCounter = 0; - for (Map.Entry entry : _sampleWells.entrySet()) + for (int i = sampleIndex; i < sampleIds.size(); i++) { - if (target == null) - { - if (plateData != null && plateData.size() > layouts.size()) - { - PlateManager.PlateData targetPlateData = plateData.get(layouts.size() - 1); - if (targetPlateData.plateType() != null && targetPlateData.plateType() > 0) - { - PlateType targetPlateDataType = context.resolvePlateType(targetPlateData.plateType()); - if (targetPlateDataType != null) - { - target = new WellLayout(targetPlateDataType, true, null); - targetCols = targetPlateDataType.getColumns(); - targetRows = targetPlateDataType.getRows(); - } - } - } - - if (target == null) - { - target = new WellLayout(targetPlateType, true, null); - targetCols = targetPlateType.getColumns(); - targetRows = targetPlateType.getRows(); - } - } - - WellLayout.Well sourceWell = entry.getValue(); + WellLayout.Well sourceWell = _sampleWells.get(sampleIds.get(i)); target.setWell(targetRowIdx, targetColIdx, sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sourceWell.sourceSampleId()); + sampleCounter++; if (isColumnLayout) { @@ -106,11 +122,7 @@ private List executeRowColumnLayout(ExecutionContext context) targetColIdx++; if (targetColIdx == targetCols) - { - layouts.add(target); - target = null; - targetColIdx = 0; - } + break; } } else @@ -122,94 +134,79 @@ private List executeRowColumnLayout(ExecutionContext context) targetRowIdx++; if (targetRowIdx == targetRows) - { - layouts.add(target); - target = null; - targetRowIdx = 0; - } + break; } } } - if (target != null) - layouts.add(target); - - return layouts; + return Pair.of(sampleIndex + sampleCounter, target); } - private List executeTemplateLayout(Plate targetTemplate, List targetTemplateWellData) throws ValidationException + private Pair executeTemplateLayout( + ExecutionContext context, + WellLayout target, + List sampleIds, + Map, Integer> groupSampleMap, + int sampleIndex + ) throws ValidationException { - int counter = 0; - List layouts = new ArrayList<>(); - Map, Integer> groupSampleMap = new HashMap<>(); + int startIndex = sampleIndex; - List sampleIds = new ArrayList<>(); - for (Map.Entry entry : _sampleWells.entrySet()) - sampleIds.add(entry.getKey()); - - while (counter < sampleIds.size()) + for (WellData wellData : context.getWellData(target.getTargetTemplateId(), false, false)) { - int startCounter = counter; - WellLayout layout = new WellLayout(targetTemplate.getPlateType(), false, targetTemplate.getRowId()); + boolean isSampleWell = wellData.isSample(); + boolean isReplicateWell = wellData.isReplicate(); + boolean isSampleOrReplicate = isSampleWell || isReplicateWell; - for (WellData wellData : targetTemplateWellData) + Pair groupKey = null; + if (isSampleOrReplicate && wellData.getWellGroup() != null) { - boolean isSampleWell = wellData.isSample(); - boolean isReplicateWell = wellData.isReplicate(); - boolean isSampleOrReplicate = isSampleWell || isReplicateWell; + WellGroup.Type type = isSampleWell ? WellGroup.Type.SAMPLE : WellGroup.Type.REPLICATE; + groupKey = Pair.of(type, wellData.getWellGroup()); + } - Pair groupKey = null; - if (isSampleOrReplicate && wellData.getWellGroup() != null) + if (sampleIndex >= sampleIds.size()) + { + // Fill remaining group wells + if (isSampleOrReplicate && groupKey != null && groupSampleMap.containsKey(groupKey)) { - WellGroup.Type type = isSampleWell ? WellGroup.Type.SAMPLE : WellGroup.Type.REPLICATE; - groupKey = Pair.of(type, wellData.getWellGroup()); + Integer sampleId = groupSampleMap.get(groupKey); + WellLayout.Well sourceWell = _sampleWells.get(sampleId); + target.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); } + } + else if (isSampleOrReplicate) + { + Integer sampleId = sampleIds.get(sampleIndex); - if (counter >= sampleIds.size()) + if (groupKey != null) { - // Fill remaining group wells - if (isSampleOrReplicate && groupKey != null && groupSampleMap.containsKey(groupKey)) + if (groupSampleMap.containsKey(groupKey)) { - Integer sampleId = groupSampleMap.get(groupKey); - WellLayout.Well sourceWell = _sampleWells.get(sampleId); - layout.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); - } - } - else if (isSampleOrReplicate) - { - Integer sampleId = sampleIds.get(counter); - - if (groupKey != null) - { - if (groupSampleMap.containsKey(groupKey)) - { - // Do not increment counter as this reuses the same sample within a group - sampleId = groupSampleMap.get(groupKey); - } - else - { - groupSampleMap.put(groupKey, sampleId); - counter++; - } + // Do not increment counter as this reuses the same sample within a group + sampleId = groupSampleMap.get(groupKey); } else { - counter++; + groupSampleMap.put(groupKey, sampleId); + sampleIndex++; } - - WellLayout.Well sourceWell = _sampleWells.get(sampleId); - layout.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); } - } - - // The counter did not advance for this well layout meaning we did not plate any additional samples. - if (startCounter == counter) - throw new ValidationException(String.format("There are %d selected samples and only %d unique sample regions are available in \"%s\".", sampleIds.size(), counter, targetTemplate.getName())); + else + { + sampleIndex++; + } - layouts.add(layout); + WellLayout.Well sourceWell = _sampleWells.get(sampleId); + target.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); + } } - return layouts; + // The counter did not advance for this well layout meaning we did not plate any additional samples. + if (startIndex == sampleIndex) + throw new ValidationException(String.format("There are %d selected samples and only %d unique sample regions are available in \"%s\".", sampleIds.size(), sampleIndex, context.targetTemplate().getName())); + + return Pair.of(sampleIndex, target); } @Override @@ -221,9 +218,6 @@ else if (context.sampleIds() != null && !context.sampleIds().isEmpty()) _sampleWells = generateSampleWellsFromSampleIds(context.sampleIds()); else throw new ValidationException("Invalid configuration. Either source plates or source samples must be provided."); - - if (context.targetTemplate() != null) - _targetTemplateWellData = PlateManager.get().getWellData(container, user, context.targetTemplate().getRowId(), false, false); } private Map generateSampleWellsFromSampleIds(Collection sampleIds) diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java index 317fdb05174..10ffb064a22 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java @@ -39,6 +39,8 @@ public List run(Container container, User user) throws ValidationExc throw new ValidationException("A target plate template is required for this operation."); LayoutOperation.ExecutionContext context = new LayoutOperation.ExecutionContext( + container, + user, _options, _allPlateTypes, _targetPlateType, diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java index 3987bac0ddc..76e86abd17c 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java @@ -1,5 +1,6 @@ package org.labkey.assay.plate.layout; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.PlateType; @@ -7,6 +8,7 @@ import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; import org.labkey.assay.plate.PlateManager; +import org.labkey.assay.plate.data.WellData; import org.labkey.assay.plate.model.ReformatOptions; import java.util.Collection; @@ -41,6 +43,8 @@ default boolean requiresTargetTemplate() } record ExecutionContext( + Container container, + User user, ReformatOptions options, List allPlateTypes, PlateType targetPlateType, @@ -57,5 +61,10 @@ record ExecutionContext( return allPlateTypes.stream().filter(plateType -> plateType.getRowId().equals(plateTypeRowId)).findFirst().orElse(null); } + + public @NotNull List getWellData(int plateRowId, boolean includeSamples, boolean includeMetadata) + { + return PlateManager.get().getWellData(container, user, plateRowId, includeSamples, includeMetadata); + } } } From ae8c842eda263863232efc4f6d719a6cac226321 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Tue, 31 Dec 2024 12:36:29 -0800 Subject: [PATCH 04/19] ArrayOperation: process additional plates --- .../assay/plate/layout/ArrayOperation.java | 74 +++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java index b1e0b9e69d9..638e26709c3 100644 --- a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java @@ -1,6 +1,7 @@ package org.labkey.assay.plate.layout; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.PlateType; import org.labkey.api.assay.plate.WellGroup; @@ -46,13 +47,20 @@ public List execute(ExecutionContext context) throws ValidationExcep for (Map.Entry entry : _sampleWells.entrySet()) sampleIds.add(entry.getKey()); + // Plate all samples while (sampleIndex < sampleIds.size()) { WellLayout wellLayout = getNextWellLayout(context, layouts.size()); Pair result; if (wellLayout.getTargetTemplateId() != null) + { result = executeTemplateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleIndex); + + // The counter did not advance for this well layout meaning we did not plate any additional samples. + if (result.first == sampleIndex) + throw new ValidationException(String.format("There are %d selected samples and only %d unique sample regions are available in \"%s\".", sampleIds.size(), sampleIndex, context.targetTemplate().getName())); + } else result = executeRowColumnLayout(wellLayout, sampleIds, sampleIndex); @@ -60,41 +68,63 @@ public List execute(ExecutionContext context) throws ValidationExcep sampleIndex = result.first; } - // TODO: Does this need to generate additional plates or can that be done at hydration station? + // Layout any further plates that have been requested (if any) + List plateData = context.plateData(); + if (plateData != null && plateData.size() > layouts.size()) + { + while (layouts.size() < plateData.size()) + { + WellLayout wellLayout = getNextWellLayout(context, layouts.size()); + + if (wellLayout.getTargetTemplateId() != null) + { + Pair result = executeTemplateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleIndex); + layouts.add(result.second); + } + else + layouts.add(wellLayout); + } + } return layouts; } private @NotNull WellLayout getNextWellLayout(ExecutionContext context, int numLayouts) { - WellLayout layout = null; + WellLayout layout = getPlateDataWellLayout(context, Math.max(0, numLayouts - 1)); + + if (layout == null) + { + if (context.targetTemplate() != null) + layout = new WellLayout(context.targetTemplate().getPlateType(), false, context.targetTemplate().getRowId()); + else + layout = new WellLayout(context.targetPlateType(), true, null); + } + + return layout; + } + + private @Nullable WellLayout getPlateDataWellLayout(ExecutionContext context, int plateIndex) + { List plateData = context.plateData(); - if (plateData != null && plateData.size() > numLayouts) + if (plateData != null && plateData.size() > plateIndex) { - PlateManager.PlateData targetPlateData = plateData.get(numLayouts - 1); + PlateManager.PlateData targetPlateData = plateData.get(plateIndex); if (targetPlateData.plateType() != null && targetPlateData.plateType() > 0) { PlateType targetPlateDataType = context.resolvePlateType(targetPlateData.plateType()); if (targetPlateDataType != null) { if (targetPlateData.templateId() != null) - layout = new WellLayout(targetPlateDataType, false, targetPlateData.templateId()); + return new WellLayout(targetPlateDataType, false, targetPlateData.templateId()); else - layout = new WellLayout(targetPlateDataType, true, null); + return new WellLayout(targetPlateDataType, true, null); } } } - if (layout == null) - { - if (context.targetTemplate() != null) - layout = new WellLayout(context.targetTemplate().getPlateType(), false, context.targetTemplate().getRowId()); - else - layout = new WellLayout(context.targetPlateType(), true, null); - } - - return layout; + return null; } private Pair executeRowColumnLayout(WellLayout target, List sampleIds, int sampleIndex) @@ -148,10 +178,8 @@ private Pair executeTemplateLayout( List sampleIds, Map, Integer> groupSampleMap, int sampleIndex - ) throws ValidationException + ) { - int startIndex = sampleIndex; - for (WellData wellData : context.getWellData(target.getTargetTemplateId(), false, false)) { boolean isSampleWell = wellData.isSample(); @@ -202,10 +230,6 @@ else if (isSampleOrReplicate) } } - // The counter did not advance for this well layout meaning we did not plate any additional samples. - if (startIndex == sampleIndex) - throw new ValidationException(String.format("There are %d selected samples and only %d unique sample regions are available in \"%s\".", sampleIds.size(), sampleIndex, context.targetTemplate().getName())); - return Pair.of(sampleIndex, target); } @@ -255,6 +279,12 @@ private Map generateSampleWellsFromSourcePlates(Contai return sampleWells; } + @Override + public boolean produceEmptyPlates() + { + return true; + } + @Override public boolean requiresSourcePlates() { From 5aebf43bca80e57e2185ad2b9caf3023f30d19ce Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Tue, 31 Dec 2024 13:42:59 -0800 Subject: [PATCH 05/19] Cache well data in execution context --- assay/src/org/labkey/assay/PlateController.java | 4 ---- .../src/org/labkey/assay/plate/PlateManager.java | 1 + .../labkey/assay/plate/layout/ArrayOperation.java | 15 ++++----------- .../labkey/assay/plate/layout/LayoutEngine.java | 4 +++- .../assay/plate/layout/LayoutOperation.java | 8 ++++++-- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index 0e362cb2646..c7441e9928f 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -16,7 +16,6 @@ package org.labkey.assay; import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.json.JSONArray; @@ -37,7 +36,6 @@ import org.labkey.api.assay.plate.PlateCustomField; import org.labkey.api.assay.plate.PlateService; import org.labkey.api.assay.plate.PlateSet; -import org.labkey.api.assay.plate.PlateSetType; import org.labkey.api.assay.plate.PlateType; import org.labkey.api.assay.security.DesignAssayPermission; import org.labkey.api.collections.RowMapFactory; @@ -76,10 +74,8 @@ import org.labkey.assay.plate.PlateImpl; import org.labkey.assay.plate.PlateManager; import org.labkey.assay.plate.PlateSetExport; -import org.labkey.assay.plate.PlateSetImpl; import org.labkey.assay.plate.PlateUrls; import org.labkey.assay.plate.TsvPlateLayoutHandler; -import org.labkey.assay.plate.layout.ArrayOperation; import org.labkey.assay.plate.model.CreatePlateSetOptions; import org.labkey.assay.plate.model.ReformatOptions; import org.labkey.assay.view.AssayGWTView; diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 77252dbe8a7..d561266b72e 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -4340,6 +4340,7 @@ else if (well.sourceSampleId() != null) d.setPosition(p.getDescription()); d.setType(WellGroup.Type.SAMPLE); d.setSampleId(well.sourceSampleId()); + platedSampleIds.add(well.sourceSampleId()); targetWellData.add(d.getData()); } diff --git a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java index 638e26709c3..63dc46a4f0d 100644 --- a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java @@ -237,7 +237,7 @@ else if (isSampleOrReplicate) public void init(Container container, User user, ExecutionContext context) throws ValidationException { if (!context.sourcePlates().isEmpty()) - _sampleWells = generateSampleWellsFromSourcePlates(container, user, context.sourcePlates()); + _sampleWells = generateSampleWellsFromSourcePlates(context); else if (context.sampleIds() != null && !context.sampleIds().isEmpty()) _sampleWells = generateSampleWellsFromSampleIds(context.sampleIds()); else @@ -257,16 +257,15 @@ private Map generateSampleWellsFromSampleIds(Collectio return sampleWells; } - private Map generateSampleWellsFromSourcePlates(Container container, User user, @NotNull List sourcePlates) + private Map generateSampleWellsFromSourcePlates(ExecutionContext context) { LinkedHashMap sampleWells = new LinkedHashMap<>(); - for (Plate sourcePlate : sourcePlates) + for (Plate sourcePlate : context.sourcePlates()) { int sourceRowId = sourcePlate.getRowId(); - List sourceWellData = PlateManager.get().getWellData(container, user, sourceRowId, true, false); - for (WellData wellData : sourceWellData) + for (WellData wellData : context.getWellData(sourceRowId, true, false)) { Integer wellSampleId = wellData.getSampleId(); if (wellSampleId != null && !sampleWells.containsKey(wellSampleId) && (wellData.isSample() || wellData.isReplicate())) @@ -291,12 +290,6 @@ public boolean requiresSourcePlates() return false; } - @Override - public boolean requiresTargetPlateType() - { - return Layout.Column.equals(_layout) || Layout.Row.equals(_layout); - } - @Override public boolean requiresTargetTemplate() { diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java index 10ffb064a22..7077732b2e4 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java @@ -9,6 +9,7 @@ import org.labkey.assay.plate.model.ReformatOptions; import java.util.Collection; +import java.util.HashMap; import java.util.List; public class LayoutEngine @@ -47,7 +48,8 @@ public List run(Container container, User user) throws ValidationExc _sourcePlates, _targetTemplate, _targetPlateData, - _sampleIds + _sampleIds, + new HashMap<>() ); _operation.init(container, user, context); diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java index 76e86abd17c..96d53a43046 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java @@ -13,6 +13,7 @@ import java.util.Collection; import java.util.List; +import java.util.Map; public interface LayoutOperation { @@ -42,6 +43,8 @@ default boolean requiresTargetTemplate() return false; } + record WellDataCacheKey(int plateRowId, boolean includeSamples, boolean includeMetadata) {} + record ExecutionContext( Container container, User user, @@ -51,7 +54,8 @@ record ExecutionContext( List sourcePlates, Plate targetTemplate, List plateData, - Collection sampleIds + Collection sampleIds, + Map> wellDataCache ) { public @Nullable PlateType resolvePlateType(Integer plateTypeRowId) @@ -64,7 +68,7 @@ record ExecutionContext( public @NotNull List getWellData(int plateRowId, boolean includeSamples, boolean includeMetadata) { - return PlateManager.get().getWellData(container, user, plateRowId, includeSamples, includeMetadata); + return wellDataCache.computeIfAbsent(new WellDataCacheKey(plateRowId, includeSamples, includeMetadata), (k) -> PlateManager.get().getWellData(container, user, k.plateRowId, k.includeSamples, k.includeMetadata)); } } } From b7516fb8e2829e691b8972148fc40fea9e0fb097 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Wed, 1 Jan 2025 16:49:08 -0800 Subject: [PATCH 06/19] Improve error logging --- .../src/org/labkey/assay/PlateController.java | 7 ++++++- .../org/labkey/assay/plate/PlateManager.java | 18 +++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index c7441e9928f..a86bfb4153b 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -63,6 +63,7 @@ import org.labkey.api.util.JsonUtil; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.URLHelper; +import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.ActionURL; import org.labkey.api.view.DataViewSnapshotSelectionForm; import org.labkey.api.view.HtmlView; @@ -99,7 +100,7 @@ public class PlateController extends SpringActionController { private static final SpringActionController.DefaultActionResolver _actionResolver = new DefaultActionResolver(PlateController.class); - private static final Logger _log = LogManager.getLogger(PlateController.class); + private static final Logger LOG = LogHelper.getLogger(PlateController.class, "Controller for plate related actions"); public PlateController() { @@ -1636,6 +1637,10 @@ public Object execute(ReformatOptions options, BindException errors) throws Exce } catch (Exception e) { + if (e instanceof ValidationException ve) + LOG.debug("Request failed due to a validation exception", ve); + else + LOG.error("Request failed due to an exception", e); String message = "Failed to reformat plates."; if (e.getMessage() != null) message += " " + e.getMessage(); diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index d561266b72e..55ad09e8967 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -4071,7 +4071,7 @@ public record ReformatResult( engine.setTargetPlateType(targetPlateSource.first); engine.setTargetTemplate(targetPlateSource.second); engine.setSampleIds(getSelectedSampleIds(options)); - engine.setTargetPlateData(options.getPlates()); + engine.setTargetPlateData(getReformatTargetPlateData(options, targetPlateSet)); // Execute plate layout List wellLayouts = engine.run(container, user); @@ -4119,9 +4119,9 @@ public record ReformatResult( return new ReformatResult(null, plateRowIds.size(), plateSetRowId, plateSetName, plateRowIds, platedSampleCount); } - private @Nullable Integer getReformatParentPlateSetId(@NotNull PlateSet sourcePlateSet) + private @Nullable Integer getReformatParentPlateSetId(PlateSet sourcePlateSet) { - if (sourcePlateSet.isPrimary() || !sourcePlateSet.isStandalone()) + if (sourcePlateSet != null && (sourcePlateSet.isPrimary() || !sourcePlateSet.isStandalone())) return sourcePlateSet.getRowId(); return null; } @@ -4265,6 +4265,18 @@ else if (!Objects.equals(sourcePlateSet.getRowId(), plateSet.getRowId())) return Pair.of(sourcePlateSet, sourcePlates); } + private @NotNull List getReformatTargetPlateData(ReformatOptions options, @NotNull PlateSetImpl targetPlateSet) throws ValidationException + { + List plateData = options.getPlates(); + if (plateData == null || plateData.isEmpty()) + return emptyList(); + + if (targetPlateSet.isPrimary() && plateData.stream().anyMatch(data -> data.templateId != null)) + throw new ValidationException(String.format("Plate templates are not supported for %s plate sets.", PlateSetType.primary.name())); + + return plateData; + } + private Collection getSelectedSampleIds(ReformatOptions options) throws ValidationException { String selectionKey = StringUtils.trimToNull(options.getSampleSelectionKey()); From 67f47027dd631c9b8b37dcf8190a77fe14326e7c Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Wed, 1 Jan 2025 17:12:34 -0800 Subject: [PATCH 07/19] Adjust indexing --- assay/src/org/labkey/assay/plate/layout/ArrayOperation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java index 63dc46a4f0d..422db593cde 100644 --- a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java @@ -91,7 +91,7 @@ public List execute(ExecutionContext context) throws ValidationExcep private @NotNull WellLayout getNextWellLayout(ExecutionContext context, int numLayouts) { - WellLayout layout = getPlateDataWellLayout(context, Math.max(0, numLayouts - 1)); + WellLayout layout = getPlateDataWellLayout(context, Math.max(0, numLayouts)); if (layout == null) { From 2ea7ed0e3c75336824ae915069586db566a91c64 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 2 Jan 2025 16:14:32 -0800 Subject: [PATCH 08/19] ArrayOperation: strictly layout against "plates" when provided --- .../org/labkey/assay/plate/PlateManager.java | 73 ++++++++++++++++++- .../labkey/assay/plate/PlateManagerTest.java | 4 +- .../assay/plate/layout/ArrayOperation.java | 25 ++++--- .../labkey/assay/plate/query/PlateTable.java | 44 ++++++++--- 4 files changed, 121 insertions(+), 25 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 55ad09e8967..6291160f030 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -4022,9 +4022,27 @@ else if (!well.getSampleId().equals(sampleId)) } } + @JsonInclude(JsonInclude.Include.NON_NULL) + public record PreviewPlateData( + String name, + Integer plateType, + Integer templateId, + String barcode, + List> data, + Integer wellCount, + Integer wellsEmpty, + Integer wellsFilled, + Integer sampleCount + ) { + static PreviewPlateData create(PlateData plateData, Integer wellCount, Integer wellsEmpty, Integer wellsFilled, Integer sampleCount) + { + return new PreviewPlateData(plateData.name, plateData.plateType, plateData.templateId, plateData.barcode, plateData.data, wellCount, wellsEmpty, wellsFilled, sampleCount); + } + } + @JsonInclude(JsonInclude.Include.NON_NULL) public record ReformatResult( - List previewData, + List previewData, Integer plateCount, Integer plateSetRowId, String plateSetName, @@ -4059,7 +4077,8 @@ public record ReformatResult( throw new ValidationException("An \"operation\" must be specified."); // Initialize / validate engine configuration - LayoutEngine engine = new LayoutEngine(options, getPlateTypes()); + List allPlateTypes = getPlateTypes(); + LayoutEngine engine = new LayoutEngine(options, allPlateTypes); PlateSetImpl targetPlateSet = getReformatTargetPlateSet(container, options); Pair> source = getReformatSourcePlates(container, options, engine.getOperation()); @@ -4092,7 +4111,10 @@ public record ReformatResult( Integer platedSampleCount = hydratedResults.second; if (options.isPreview()) - return new ReformatResult(options.isPreviewData() ? plateData : null, plateData.size(), null, null, null, platedSampleCount); + { + List previewData = getPreviewData(options, plateData, allPlateTypes); + return new ReformatResult(previewData, plateData.size(), null, null, null, platedSampleCount); + } if (plateData.isEmpty()) throw new ValidationException("This operation as configured does not create any plates."); @@ -4119,6 +4141,51 @@ public record ReformatResult( return new ReformatResult(null, plateRowIds.size(), plateSetRowId, plateSetName, plateRowIds, platedSampleCount); } + private @Nullable List getPreviewData(ReformatOptions options, List plateData, List allPlateTypes) + { + if (!options.isPreviewData()) + return null; + + List previewData = new ArrayList<>(); + Map plateTypes = new HashMap<>(); + + for (PlateType type : allPlateTypes) + plateTypes.put(type.getRowId(), type); + + for (PlateData plate : plateData) + { + Integer wellCount = null; + Integer wellsEmpty = null; + Integer wellsFilled = null; + Integer sampleCount = null; + + if (plate.plateType != null && plateTypes.containsKey(plate.plateType) && plate.data != null) + { + PlateType type = plateTypes.get(plate.plateType); + wellCount = type.getWellCount(); + wellsFilled = 0; + Set sampleIds = new HashSet<>(); + + for (Map row : plate.data) + { + Integer sampleId = (Integer) row.get("SampleID"); + if (sampleId != null) + { + wellsFilled++; + sampleIds.add(sampleId); + } + } + + sampleCount = sampleIds.size(); + wellsEmpty = wellCount - wellsFilled; + } + + previewData.add(PreviewPlateData.create(plate, wellCount, wellsEmpty, wellsFilled, sampleCount)); + } + + return previewData; + } + private @Nullable Integer getReformatParentPlateSetId(PlateSet sourcePlateSet) { if (sourcePlateSet != null && (sourcePlateSet.isPrimary() || !sourcePlateSet.isStandalone())) diff --git a/assay/src/org/labkey/assay/plate/PlateManagerTest.java b/assay/src/org/labkey/assay/plate/PlateManagerTest.java index e7ac5ad220e..b656c4a201b 100644 --- a/assay/src/org/labkey/assay/plate/PlateManagerTest.java +++ b/assay/src/org/labkey/assay/plate/PlateManagerTest.java @@ -1685,11 +1685,11 @@ public void testReformatArrayFromTemplate() throws Exception } } - private @NotNull Set getSamples(List plateData) + private @NotNull Set getSamples(List plateData) { Set sampleIds = new HashSet<>(); - for (PlateManager.PlateData data : plateData) + for (PlateManager.PreviewPlateData data : plateData) { for (Map well : data.data()) { diff --git a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java index 422db593cde..83100f76bc3 100644 --- a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java @@ -51,6 +51,9 @@ public List execute(ExecutionContext context) throws ValidationExcep while (sampleIndex < sampleIds.size()) { WellLayout wellLayout = getNextWellLayout(context, layouts.size()); + if (wellLayout == null) + throw new ValidationException(String.format("Only %d of %d samples could be plated with this configuration.", sampleIndex, sampleIds.size())); + Pair result; if (wellLayout.getTargetTemplateId() != null) @@ -74,7 +77,9 @@ public List execute(ExecutionContext context) throws ValidationExcep { while (layouts.size() < plateData.size()) { - WellLayout wellLayout = getNextWellLayout(context, layouts.size()); + WellLayout wellLayout = getPlateDataWellLayout(context, layouts.size()); + if (wellLayout == null) + throw new ValidationException(String.format("Failed to resolve plate at index %d.", layouts.size())); if (wellLayout.getTargetTemplateId() != null) { @@ -89,17 +94,15 @@ public List execute(ExecutionContext context) throws ValidationExcep return layouts; } - private @NotNull WellLayout getNextWellLayout(ExecutionContext context, int numLayouts) + private @Nullable WellLayout getNextWellLayout(ExecutionContext context, int numLayouts) { - WellLayout layout = getPlateDataWellLayout(context, Math.max(0, numLayouts)); - - if (layout == null) - { - if (context.targetTemplate() != null) - layout = new WellLayout(context.targetTemplate().getPlateType(), false, context.targetTemplate().getRowId()); - else - layout = new WellLayout(context.targetPlateType(), true, null); - } + WellLayout layout; + if (context.plateData() != null && !context.plateData().isEmpty()) + layout = getPlateDataWellLayout(context, numLayouts); + else if (context.targetTemplate() != null) + layout = new WellLayout(context.targetTemplate().getPlateType(), false, context.targetTemplate().getRowId()); + else + layout = new WellLayout(context.targetPlateType(), true, null); return layout; } diff --git a/assay/src/org/labkey/assay/plate/query/PlateTable.java b/assay/src/org/labkey/assay/plate/query/PlateTable.java index 859fb411556..10c000fa988 100644 --- a/assay/src/org/labkey/assay/plate/query/PlateTable.java +++ b/assay/src/org/labkey/assay/plate/query/PlateTable.java @@ -103,6 +103,8 @@ public enum Column Properties, RowId, Template, + WellCount, + WellsEmpty, WellsFilled; public FieldKey fieldKey() @@ -138,7 +140,7 @@ public void addColumns() { super.addColumns(); addColumn(createPropertiesColumn()); - addWellsFilledColumn(); + addWellCountColumns(); } @Override @@ -185,15 +187,39 @@ private MutableColumnInfo createPropertiesColumn() return col; } - private void addWellsFilledColumn() + private void addWellCountColumns() { - SQLFragment sql = new SQLFragment("(SELECT COUNT(*) AS wellsFilled FROM ") - .append(AssayDbSchema.getInstance().getTableInfoWell(), "") - .append(" WHERE PlateId = " + STR_TABLE_ALIAS + ".RowId") - .append(" AND sampleId IS NOT NULL)"); - ExprColumn countCol = new ExprColumn(this, Column.WellsFilled.name(), sql, JdbcType.INTEGER); - countCol.setDescription("The number of wells that have samples for this plate."); - addColumn(countCol); + // WellCount + { + SQLFragment sql = new SQLFragment("(SELECT COUNT(*) AS wellCount FROM ") + .append(AssayDbSchema.getInstance().getTableInfoWell(), "") + .append(" WHERE PlateId = " + STR_TABLE_ALIAS + ".RowId)"); + ExprColumn totalWellCount = new ExprColumn(this, Column.WellCount.name(), sql, JdbcType.INTEGER); + totalWellCount.setDescription("The total number of wells for this plate."); + addColumn(totalWellCount); + } + + // WellsFilled + { + SQLFragment sql = new SQLFragment("(SELECT COUNT(*) AS wellsFilled FROM ") + .append(AssayDbSchema.getInstance().getTableInfoWell(), "") + .append(" WHERE PlateId = " + STR_TABLE_ALIAS + ".RowId") + .append(" AND sampleId IS NOT NULL)"); + ExprColumn wellsFilled = new ExprColumn(this, Column.WellsFilled.name(), sql, JdbcType.INTEGER); + wellsFilled.setDescription("The number of wells that have samples for this plate."); + addColumn(wellsFilled); + } + + // WellsEmpty + { + SQLFragment sql = new SQLFragment("(SELECT COUNT(*) AS wellsEmpty FROM ") + .append(AssayDbSchema.getInstance().getTableInfoWell(), "") + .append(" WHERE PlateId = " + STR_TABLE_ALIAS + ".RowId") + .append(" AND sampleId IS NULL)"); + ExprColumn wellsEmpty = new ExprColumn(this, Column.WellsEmpty.name(), sql, JdbcType.INTEGER); + wellsEmpty.setDescription("The number of wells that do not have samples for this plate."); + addColumn(wellsEmpty); + } } @Override From e68ab5a86261f1d67031011dc3d6b3d4a92bc774 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Fri, 3 Jan 2025 14:25:44 -0800 Subject: [PATCH 09/19] Primary plate sets: coalesce duplicate wells error messaging --- .../org/labkey/assay/plate/PlateManager.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 6291160f030..71e959e27fe 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -3254,9 +3254,29 @@ public void validatePrimaryPlateSetUniqueSamples(Set wellRowIds, BatchV var duplicates = new SqlSelector(dbSchema.getSchema(), nonUniqueSamplesPerPrimaryPlateSetSQL).getMapCollection(); - for (var duplicate : duplicates) + if (!duplicates.isEmpty()) { - errors.addRowError(new ValidationException(String.format("Sample \"%s\" is recorded in more than one well in Primary Plate Set \"%s\".", duplicate.get("SampleName"), duplicate.get("PlateSetName")))); + Map> duplicateMap = new HashMap<>(); + + for (var duplicate : duplicates) + { + var plateSetName = (String) duplicate.get("PlateSetName"); + duplicateMap.computeIfAbsent(plateSetName, (n) -> new HashSet<>()).add((String) duplicate.get("SampleName")); + } + + for (var entry : duplicateMap.entrySet()) + { + var plateSetName = entry.getKey(); + var sampleNames = entry.getValue(); + + ValidationException ve; + if (sampleNames.size() == 1) + ve = new ValidationException(String.format("Sample \"%s\" is recorded in more than one well in Primary Plate Set \"%s\".", sampleNames.stream().findFirst().get(), plateSetName)); + else + ve = new ValidationException(String.format("There are %d samples recorded in more than one well in Primary Plate Set \"%s\".", sampleNames.size(), plateSetName)); + + errors.addRowError(ve); + } } } From fdd4a3361c4a85c86f20787b5af849ebe819c08b Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Sat, 4 Jan 2025 23:39:47 -0800 Subject: [PATCH 10/19] Reformat: support "fillExistingWells" --- .../org/labkey/assay/plate/PlateManager.java | 290 +++++++++++++----- .../org/labkey/assay/plate/data/WellData.java | 36 +++ .../assay/plate/layout/ArrayOperation.java | 167 +++++++++- .../assay/plate/layout/LayoutEngine.java | 14 +- .../assay/plate/layout/LayoutOperation.java | 17 +- .../labkey/assay/plate/layout/WellLayout.java | 11 +- .../assay/plate/model/ReformatOptions.java | 12 + 7 files changed, 440 insertions(+), 107 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 71e959e27fe..efd980a3db8 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -169,6 +169,7 @@ import java.util.stream.Stream; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; import static java.util.Collections.unmodifiableList; import static org.labkey.api.assay.plate.PlateSet.MAX_PLATES; import static org.labkey.assay.plate.query.WellTable.WELL_LOCATION; @@ -1186,7 +1187,7 @@ private int savePlateImpl( ) throws ValidationException { if (rawWellData == null || rawWellData.isEmpty()) - return Collections.emptyMap(); + return emptyMap(); Set keywords = CaseInsensitiveHashSet.of( WellTable.Column.Col.name(), @@ -1730,7 +1731,7 @@ private void copyWellData(User user, @NotNull Plate source, @NotNull Plate copy, else { wellMetadataFields = Collections.emptySet(); - sourceMetaData = Collections.emptyMap(); + sourceMetaData = emptyMap(); } List> newWellData = new ArrayList<>(); @@ -4049,21 +4050,32 @@ public record PreviewPlateData( Integer templateId, String barcode, List> data, + Integer plateRowId, Integer wellCount, Integer wellsEmpty, Integer wellsFilled, - Integer sampleCount + Integer sampleCount, + Integer samplesAdded ) { - static PreviewPlateData create(PlateData plateData, Integer wellCount, Integer wellsEmpty, Integer wellsFilled, Integer sampleCount) + static PreviewPlateData create( + PlateData plateData, + Integer plateRowId, + Integer wellCount, + Integer wellsEmpty, + Integer wellsFilled, + Integer sampleCount, + Integer samplesAdded + ) { - return new PreviewPlateData(plateData.name, plateData.plateType, plateData.templateId, plateData.barcode, plateData.data, wellCount, wellsEmpty, wellsFilled, sampleCount); + return new PreviewPlateData(plateData.name, plateData.plateType, plateData.templateId, plateData.barcode, plateData.data, plateRowId, wellCount, wellsEmpty, wellsFilled, sampleCount, samplesAdded); } } @JsonInclude(JsonInclude.Include.NON_NULL) public record ReformatResult( List previewData, - Integer plateCount, + Integer plateCountCreated, + Integer plateCountUpdated, Integer plateSetRowId, String plateSetName, List plateRowIds, @@ -4075,12 +4087,14 @@ public record ReformatResult( * @return A ReformatResult which will contain different data when previewing versus saving (not previewing). * - preview: * The previewData contains the preview data. Null if the "previewData" flag is false. - * The plateCount is the number of plates that will be created. + * The plateCountCreated is the number of plates that will be created. + * The plateCountUpdated is the number of plates that will be updated. * The platedSampleCount is the number of samples that will be plated for re-array operations. * The plateSetRowId, plateSetName and plateRowIds will be null. * - saving (not preview): * The previewData is null. - * The plateCount is the number of plates that have been created. + * The plateCountCreated is the number of plates that have been created. + * The plateCountUpdated is the number of plates that have been updated. * The platedSampleCount is the number of samples that have been plated for re-array operations. * The plateSetRowId is the rowId of the target plate set. * The plateSetName is the name of the target plate set. @@ -4110,10 +4124,12 @@ public record ReformatResult( engine.setTargetPlateType(targetPlateSource.first); engine.setTargetTemplate(targetPlateSource.second); engine.setSampleIds(getSelectedSampleIds(options)); + engine.setTargetPlates(getReformatTargetPlates(targetPlateSet)); engine.setTargetPlateData(getReformatTargetPlateData(options, targetPlateSet)); // Execute plate layout - List wellLayouts = engine.run(container, user); + WellData.Cache wellDataCache = new WellData.Cache(container, user); + List wellLayouts = engine.run(container, user, wellDataCache); int availablePlateCount = targetPlateSet.availablePlateCount(); if (availablePlateCount < wellLayouts.size()) @@ -4126,18 +4142,19 @@ public record ReformatResult( } // Populate plate data from well layouts - Pair, Integer> hydratedResults = hydratePlateDataFromWellLayout(container, user, wellLayouts, options.getPlates(), engine.getOperation()); - List plateData = hydratedResults.first; - Integer platedSampleCount = hydratedResults.second; + HydrateContext hydrateContext = new HydrateContext(container, user, wellLayouts, options, engine.getOperation(), new HashSet<>(), wellDataCache); + HydratedResult hydratedResults = hydratePlateDataFromWellLayout(hydrateContext); + List plateData = hydratedResults.plateData(); + List> existingPlates = hydratedResults.existingPlates(); if (options.isPreview()) { - List previewData = getPreviewData(options, plateData, allPlateTypes); - return new ReformatResult(previewData, plateData.size(), null, null, null, platedSampleCount); + List previewData = getPreviewData(options, existingPlates, plateData, allPlateTypes); + return new ReformatResult(previewData, plateData.size(), existingPlates.size(), null, null, null, hydratedResults.platedSampleCount()); } - if (plateData.isEmpty()) - throw new ValidationException("This operation as configured does not create any plates."); + if (plateData.isEmpty() && existingPlates.isEmpty()) + throw new ValidationException("This operation as configured does not create or update any plates."); Integer plateSetRowId; String plateSetName; @@ -4152,16 +4169,40 @@ public record ReformatResult( } else { - plateSetRowId = targetPlateSet.getRowId(); - plateSetName = targetPlateSet.getName(); - newPlates = addPlatesToPlateSet(container, user, plateSetRowId, targetPlateSet.isTemplate(), plateData); + try (DbScope.Transaction tx = ensureTransaction()) + { + if (!existingPlates.isEmpty()) + { + QueryUpdateService qus = requiredUpdateService(getWellTable(container, user)); + + List> rows = new ArrayList<>(); + for (Pair entry : existingPlates) + rows.addAll(entry.getValue().data()); + + BatchValidationException errors = new BatchValidationException(); + qus.updateRows(user, container, rows, null, errors, null, null); + if (errors.hasErrors()) + throw errors; + } + + plateSetRowId = targetPlateSet.getRowId(); + plateSetName = targetPlateSet.getName(); + newPlates = addPlatesToPlateSet(container, user, plateSetRowId, targetPlateSet.isTemplate(), plateData); + + tx.commit(); + } } List plateRowIds = newPlates.stream().map(Plate::getRowId).toList(); - return new ReformatResult(null, plateRowIds.size(), plateSetRowId, plateSetName, plateRowIds, platedSampleCount); + return new ReformatResult(null, plateRowIds.size(), existingPlates.size(), plateSetRowId, plateSetName, plateRowIds, hydratedResults.platedSampleCount()); } - private @Nullable List getPreviewData(ReformatOptions options, List plateData, List allPlateTypes) + private @Nullable List getPreviewData( + ReformatOptions options, + List> existingPlates, + List newPlateData, + List allPlateTypes + ) { if (!options.isPreviewData()) return null; @@ -4172,7 +4213,53 @@ public record ReformatResult( for (PlateType type : allPlateTypes) plateTypes.put(type.getRowId(), type); - for (PlateData plate : plateData) + for (Pair entry : existingPlates) + { + Plate plate = entry.getKey(); + PlateData plateData = entry.getValue(); + + Integer wellCount = plate.getPlateType().getWellCount(); + Integer wellsEmpty = 0; + Integer wellsFilled = 0; + Integer samplesAdded = 0; + Set updatedPositions = new HashSet<>(); + Set sampleIds = new HashSet<>(); + + for (Map row : plateData.data) + { + Integer sampleId = (Integer) row.get(WellTable.Column.SampleID.name()); + if (sampleId != null) + { + wellsFilled++; + if (!sampleIds.contains(sampleId)) + samplesAdded++; + sampleIds.add(sampleId); + + String position = (String) row.get(WellTable.WELL_LOCATION); + if (position == null) + throw new IllegalStateException("Failed to resolve position from well data"); + updatedPositions.add(position); + } + } + + for (Well well : plate.getWells()) + { + if (updatedPositions.contains(well.getDescription())) + continue; + + if (well.getSampleId() != null) + { + wellsFilled++; + sampleIds.add(well.getSampleId()); + } + else + wellsEmpty++; + } + + previewData.add(PreviewPlateData.create(plateData, plate.getRowId(), wellCount, wellsEmpty, wellsFilled, sampleIds.size(), samplesAdded)); + } + + for (PlateData plate : newPlateData) { Integer wellCount = null; Integer wellsEmpty = null; @@ -4188,7 +4275,7 @@ public record ReformatResult( for (Map row : plate.data) { - Integer sampleId = (Integer) row.get("SampleID"); + Integer sampleId = (Integer) row.get(WellTable.Column.SampleID.name()); if (sampleId != null) { wellsFilled++; @@ -4200,7 +4287,7 @@ public record ReformatResult( wellsEmpty = wellCount - wellsFilled; } - previewData.add(PreviewPlateData.create(plate, wellCount, wellsEmpty, wellsFilled, sampleCount)); + previewData.add(PreviewPlateData.create(plate, null, wellCount, wellsEmpty, wellsFilled, sampleCount, sampleCount)); } return previewData; @@ -4352,6 +4439,14 @@ else if (!Objects.equals(sourcePlateSet.getRowId(), plateSet.getRowId())) return Pair.of(sourcePlateSet, sourcePlates); } + private @NotNull List getReformatTargetPlates(@NotNull PlateSetImpl targetPlateSet) + { + if (targetPlateSet.isNew()) + return emptyList(); + + return getPlatesForPlateSet(targetPlateSet); + } + private @NotNull List getReformatTargetPlateData(ReformatOptions options, @NotNull PlateSetImpl targetPlateSet) throws ValidationException { List plateData = options.getPlates(); @@ -4377,14 +4472,47 @@ private Collection getSelectedSampleIds(ReformatOptions options) throws return sampleIds; } - private void hydrateFromPlate( - Container container, - User user, - WellLayout wellLayout, - Set platedSampleIds, - Map> sourceWellDataMap, - List> targetWellData - ) + private PlateData hydrateFromExistingPlate(HydrateContext context, WellLayout wellLayout, @NotNull Plate existingPlate) + { + List> targetWellData = new ArrayList<>(); + List existingPlateData = context.wellDataCache().getData(existingPlate.getRowId(), true, true); + boolean isPreview = context.options().isPreview(); + + for (WellLayout.Well well : wellLayout.getWells()) + { + if (well == null) + continue; + + for (WellData wellData : existingPlateData) + { + if (wellData.getRow() == well.destinationRowIdx() && wellData.getCol() == well.destinationColIdx()) + { + Position p = new PositionImpl(context.container(), well.destinationRowIdx(), well.destinationColIdx()); + + WellData d = new WellData(); + d.setSampleId(well.sourceSampleId()); + + if (isPreview) + { + d.setPosition(p.getDescription()); + d.setWellGroup(wellData.getWellGroup()); + d.setType(wellData.getType()); + } + else + d.setRowId(wellData.getRowId()); + + if (d.getSampleId() != null) + context.platedSampleIds().add(d.getSampleId()); + + targetWellData.add(d.getData(true)); + } + } + } + + return new PlateData(existingPlate.getName(), existingPlate.getPlateType().getRowId(), null, existingPlate.getBarcode(), targetWellData); + } + + private void hydrateFromPlate(HydrateContext context, WellLayout wellLayout, List> targetWellData) { for (WellLayout.Well well : wellLayout.getWells()) { @@ -4395,10 +4523,7 @@ private void hydrateFromPlate( if (sourcePlateId > 0) { - List sourceWellData = sourceWellDataMap.computeIfAbsent( - sourcePlateId, - (plateRowId) -> getWellData(container, user, plateRowId, true, true) - ); + List sourceWellData = context.wellDataCache().getData(sourcePlateId, true, true); for (WellData wellData : sourceWellData) { @@ -4407,7 +4532,7 @@ private void hydrateFromPlate( if (wellData.getRow() == well.sourceRowIdx() && wellData.getCol() == well.sourceColIdx()) { - Position p = new PositionImpl(container, well.destinationRowIdx(), well.destinationColIdx()); + Position p = new PositionImpl(context.container(), well.destinationRowIdx(), well.destinationColIdx()); WellData d = new WellData(); d.setPosition(p.getDescription()); @@ -4417,7 +4542,7 @@ private void hydrateFromPlate( { d.setType(WellGroup.Type.SAMPLE); if (d.getSampleId() != null) - platedSampleIds.add(d.getSampleId()); + context.platedSampleIds().add(d.getSampleId()); } else { @@ -4433,32 +4558,22 @@ private void hydrateFromPlate( } else if (well.sourceSampleId() != null) { - Position p = new PositionImpl(container, well.destinationRowIdx(), well.destinationColIdx()); + Position p = new PositionImpl(context.container(), well.destinationRowIdx(), well.destinationColIdx()); WellData d = new WellData(); d.setPosition(p.getDescription()); d.setType(WellGroup.Type.SAMPLE); d.setSampleId(well.sourceSampleId()); - platedSampleIds.add(well.sourceSampleId()); + context.platedSampleIds().add(well.sourceSampleId()); targetWellData.add(d.getData()); } } } - private void hydrateFromPlateTemplate( - Container container, - User user, - WellLayout wellLayout, - Set platedSampleIds, - Map> sourceWellDataMap, - List> targetWellData - ) + private void hydrateFromPlateTemplate(HydrateContext context, WellLayout wellLayout, List> targetWellData) { - List templateWellData = sourceWellDataMap.computeIfAbsent( - wellLayout.getTargetTemplateId(), - (templateRowId) -> getWellData(container, user, templateRowId, false, true) - ); + List templateWellData = context.wellDataCache().getData(wellLayout.getTargetTemplateId(), false, true); for (WellData wellData : templateWellData) { @@ -4466,7 +4581,7 @@ private void hydrateFromPlateTemplate( int rowIdx = wellData.getRow(); int colIdx = wellData.getCol(); - Position p = new PositionImpl(container, rowIdx, colIdx); + Position p = new PositionImpl(context.container(), rowIdx, colIdx); d.setPosition(p.getDescription()); WellLayout.Well well = wellLayout.getWell(rowIdx, colIdx); @@ -4475,7 +4590,7 @@ private void hydrateFromPlateTemplate( Integer sampleId = well.sourceSampleId(); d.setSampleId(sampleId); if (sampleId != null) - platedSampleIds.add(sampleId); + context.platedSampleIds().add(sampleId); } d.setMetadata(wellData.getMetadata()); @@ -4486,52 +4601,67 @@ private void hydrateFromPlateTemplate( } } - private @NotNull Pair, Integer> hydratePlateDataFromWellLayout( + private record HydrateContext( Container container, User user, List wellLayouts, - List plateData, - LayoutOperation operation - ) + ReformatOptions options, + LayoutOperation operation, + Set platedSampleIds, + WellData.Cache wellDataCache + ) {} + + private record HydratedResult(List plateData, @Nullable Integer platedSampleCount, List> existingPlates) {} + + private @NotNull HydratedResult hydratePlateDataFromWellLayout(HydrateContext context) throws ValidationException { - if (wellLayouts.isEmpty()) - return Pair.of(emptyList(), null); + if (context.wellLayouts().isEmpty()) + return new HydratedResult(emptyList(), null, emptyList()); List plates = new ArrayList<>(); - Map> sourceWellDataMap = new HashMap<>(); - Set platedSampleIds = new HashSet<>(); + List plateData = context.options().getPlates(); + List> existingPlates = new ArrayList<>(); int plateDataIndex = 0; - for (WellLayout wellLayout : wellLayouts) + for (WellLayout wellLayout : context.wellLayouts()) { - List> targetWellData = new ArrayList<>(); - - if (wellLayout.getTargetTemplateId() != null) - hydrateFromPlateTemplate(container, user, wellLayout, platedSampleIds, sourceWellDataMap, targetWellData); + if (wellLayout.getTargetPlateId() != null) + { + Plate plate = requirePlate(context.container(), wellLayout.getTargetPlateId(), null); + PlateData targetPlateData = hydrateFromExistingPlate(context, wellLayout, plate); + existingPlates.add(Pair.of(plate, targetPlateData)); + } else - hydrateFromPlate(container, user, wellLayout, platedSampleIds, sourceWellDataMap, targetWellData); - - if (operation.produceEmptyPlates() || !targetWellData.isEmpty()) { - String name = null; - String barcode = null; - if (plateData != null && plateData.size() > plateDataIndex) + List> targetWellData = new ArrayList<>(); + + if (wellLayout.getTargetTemplateId() != null) + hydrateFromPlateTemplate(context, wellLayout, targetWellData); + else + hydrateFromPlate(context, wellLayout, targetWellData); + + if (context.operation().produceEmptyPlates() || !targetWellData.isEmpty()) { - PlateData data = plateData.get(plateDataIndex); - if (data != null) + String name = null; + String barcode = null; + if (plateData != null && plateData.size() > plateDataIndex) { - name = data.name(); - barcode = data.barcode(); + PlateData data = plateData.get(plateDataIndex); + if (data != null) + { + name = data.name(); + barcode = data.barcode(); + } } - } - plates.add(new PlateData(name, wellLayout.getPlateType().getRowId(), null, barcode, targetWellData)); + plates.add(new PlateData(name, wellLayout.getPlateType().getRowId(), null, barcode, targetWellData)); + } } plateDataIndex++; } - return Pair.of(plates, platedSampleIds.isEmpty() ? null : platedSampleIds.size()); + return new HydratedResult(plates, context.platedSampleIds().isEmpty() ? null : context.platedSampleIds().size(), existingPlates); } private class BulkPlateIndexer extends Thread diff --git a/assay/src/org/labkey/assay/plate/data/WellData.java b/assay/src/org/labkey/assay/plate/data/WellData.java index fc782e2673f..885defcf067 100644 --- a/assay/src/org/labkey/assay/plate/data/WellData.java +++ b/assay/src/org/labkey/assay/plate/data/WellData.java @@ -2,9 +2,14 @@ import org.labkey.api.assay.plate.WellGroup; import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.data.Container; +import org.labkey.api.security.User; +import org.labkey.assay.plate.PlateManager; import org.labkey.assay.plate.query.WellTable; import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; public class WellData @@ -25,8 +30,15 @@ public WellData() } public Map getData() + { + return getData(false); + } + + public Map getData(boolean includeWellId) { Map data = new CaseInsensitiveHashMap<>(); + if (includeWellId && _rowId != null) + data.put(WellTable.Column.RowId.name(), _rowId); if (_position != null) data.put(WellTable.WELL_LOCATION, _position); if (_sampleId != null) @@ -150,4 +162,28 @@ public void setWellGroup(String wellGroup) { _wellGroup = wellGroup; } + + record CacheKey(int plateRowId, boolean includeSamples, boolean includeMetadata) {} + + public static class Cache + { + private final Map> cache; + private final Container container; + private final User user; + + public Cache(Container container, User user) + { + cache = new HashMap<>(); + this.container = container; + this.user = user; + } + + public List getData(int plateRowId, boolean includeSamples, boolean includeMetadata) + { + return cache.computeIfAbsent( + new CacheKey(plateRowId, includeSamples, includeMetadata), + (k) -> PlateManager.get().getWellData(container, user, k.plateRowId, k.includeSamples, k.includeMetadata) + ); + } + } } diff --git a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java index 83100f76bc3..e71aad60efe 100644 --- a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java @@ -47,22 +47,48 @@ public List execute(ExecutionContext context) throws ValidationExcep for (Map.Entry entry : _sampleWells.entrySet()) sampleIds.add(entry.getKey()); + List targetLayouts = new ArrayList<>(); + + // TODO: Document how this is handled. + // We look back at plates when populating existing which effectively means we inherit grouped samples and plate + // them on subsequent new plates so that the rules are upheld. This may not be what users are expecting. + // Does this need to be a choice? It's difficult to grasp. + if (context.options().isFillExistingWells() && context.targetPlates() != null) + { + for (Plate plate : context.targetPlates()) + { + populateGroupSampleMap(context, plate, groupSampleMap); + targetLayouts.add(new WellLayout(plate.getPlateType(), false, null, plate.getRowId())); + } + } + + // Remove samples that are already plated in this plate set? + // sampleIds.removeAll(groupSampleMap.values()); + // Plate all samples while (sampleIndex < sampleIds.size()) { - WellLayout wellLayout = getNextWellLayout(context, layouts.size()); + WellLayout wellLayout = getNextWellLayout(context, targetLayouts, layouts.size()); if (wellLayout == null) throw new ValidationException(String.format("Only %d of %d samples could be plated with this configuration.", sampleIndex, sampleIds.size())); Pair result; - if (wellLayout.getTargetTemplateId() != null) + if (wellLayout.getTargetPlateId() != null) + { + result = executeTargetPlateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleIndex); + + // The counter may not advance here and that is OK since the plate sample/replicate wells may be full. + if (result.first == sampleIndex) + continue; + } + else if (wellLayout.getTargetTemplateId() != null) { result = executeTemplateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleIndex); // The counter did not advance for this well layout meaning we did not plate any additional samples. if (result.first == sampleIndex) - throw new ValidationException(String.format("There are %d selected samples and only %d unique sample regions are available in \"%s\".", sampleIds.size(), sampleIndex, context.targetTemplate().getName())); + throw new ValidationException(String.format("There are %d selected samples and only %d unique sample regions are available in template \"%s\".", sampleIds.size(), sampleIndex, context.targetTemplate().getName())); } else result = executeRowColumnLayout(wellLayout, sampleIds, sampleIndex); @@ -72,7 +98,7 @@ public List execute(ExecutionContext context) throws ValidationExcep } // Layout any further plates that have been requested (if any) - List plateData = context.plateData(); + List plateData = context.targetPlateData(); if (plateData != null && plateData.size() > layouts.size()) { while (layouts.size() < plateData.size()) @@ -94,22 +120,25 @@ public List execute(ExecutionContext context) throws ValidationExcep return layouts; } - private @Nullable WellLayout getNextWellLayout(ExecutionContext context, int numLayouts) + private @Nullable WellLayout getNextWellLayout(ExecutionContext context, List targetLayouts, int numLayouts) { WellLayout layout; - if (context.plateData() != null && !context.plateData().isEmpty()) + if (!targetLayouts.isEmpty()) + return targetLayouts.remove(0); + + if (context.targetPlateData() != null && !context.targetPlateData().isEmpty()) layout = getPlateDataWellLayout(context, numLayouts); else if (context.targetTemplate() != null) - layout = new WellLayout(context.targetTemplate().getPlateType(), false, context.targetTemplate().getRowId()); + layout = new WellLayout(context.targetTemplate().getPlateType(), false, context.targetTemplate().getRowId(), null); else - layout = new WellLayout(context.targetPlateType(), true, null); + layout = new WellLayout(context.targetPlateType(), true, null, null); return layout; } private @Nullable WellLayout getPlateDataWellLayout(ExecutionContext context, int plateIndex) { - List plateData = context.plateData(); + List plateData = context.targetPlateData(); if (plateData != null && plateData.size() > plateIndex) { @@ -120,9 +149,9 @@ else if (context.targetTemplate() != null) if (targetPlateDataType != null) { if (targetPlateData.templateId() != null) - return new WellLayout(targetPlateDataType, false, targetPlateData.templateId()); + return new WellLayout(targetPlateDataType, false, targetPlateData.templateId(), null); else - return new WellLayout(targetPlateDataType, true, null); + return new WellLayout(targetPlateDataType, true, null, null); } } } @@ -175,6 +204,92 @@ private Pair executeRowColumnLayout(WellLayout target, List return Pair.of(sampleIndex + sampleCounter, target); } + private Pair executeTargetPlateLayout( + ExecutionContext context, + WellLayout target, + List sampleIds, + Map, Integer> groupSampleMap, + int sampleIndex + ) + { + List plateWellData = context.wellDataCache().getData(target.getTargetPlateId(), true, false); + PlateType targetPlateType = target.getPlateType(); + boolean isColumnLayout = Layout.Column.equals(_layout); + int columnCount = targetPlateType.getColumns(); + int rowCount = targetPlateType.getRows(); + + for (int outerIdx = 0; outerIdx < (isColumnLayout ? columnCount : rowCount); outerIdx++) + { + for (int innerIdx = 0; innerIdx < (isColumnLayout ? rowCount : columnCount); innerIdx++) + { + int rowIdx = isColumnLayout ? innerIdx : outerIdx; + int colIdx = isColumnLayout ? outerIdx : innerIdx; + int wellIdx = rowIdx * columnCount + colIdx; + WellData wellData = plateWellData.get(wellIdx); + Integer wellSampleId = wellData.getSampleId(); + if (wellSampleId == null) + { + boolean isSampleWell = wellData.isSample(); + boolean isReplicateWell = wellData.isReplicate(); + boolean isSampleOrReplicate = isSampleWell || isReplicateWell; + + Pair groupKey = null; + if (isSampleOrReplicate && wellData.getWellGroup() != null) + { + WellGroup.Type type = isSampleWell ? WellGroup.Type.SAMPLE : WellGroup.Type.REPLICATE; + groupKey = Pair.of(type, wellData.getWellGroup()); + } + + if (sampleIndex >= sampleIds.size()) + { + // Fill remaining group wells + if (isSampleOrReplicate && groupKey != null && groupSampleMap.containsKey(groupKey)) + { + Integer sampleId = groupSampleMap.get(groupKey); + WellLayout.Well sourceWell = _sampleWells.get(sampleId); + target.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); + } + } + else if (isSampleOrReplicate) + { + Integer sampleId = sampleIds.get(sampleIndex); + + if (groupKey != null) + { + if (groupSampleMap.containsKey(groupKey)) + { + // Do not increment counter as this reuses the same sample within a group + sampleId = groupSampleMap.get(groupKey); + } + else + { + groupSampleMap.put(groupKey, sampleId); + sampleIndex++; + } + } + else + { + sampleIndex++; + } + + WellLayout.Well sourceWell = _sampleWells.get(sampleId); + target.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); + } + else if (wellData.getType() == null) + { + Integer sampleId = sampleIds.get(sampleIndex); + sampleIndex++; + + WellLayout.Well sourceWell = _sampleWells.get(sampleId); + target.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); + } + } + } + } + + return Pair.of(sampleIndex, target); + } + private Pair executeTemplateLayout( ExecutionContext context, WellLayout target, @@ -183,7 +298,7 @@ private Pair executeTemplateLayout( int sampleIndex ) { - for (WellData wellData : context.getWellData(target.getTargetTemplateId(), false, false)) + for (WellData wellData : context.wellDataCache().getData(target.getTargetTemplateId(), false, false)) { boolean isSampleWell = wellData.isSample(); boolean isReplicateWell = wellData.isReplicate(); @@ -236,6 +351,26 @@ else if (isSampleOrReplicate) return Pair.of(sampleIndex, target); } + private void populateGroupSampleMap(ExecutionContext context, Plate plate, Map, Integer> groupSampleMap) + { + for (WellData wellData : context.wellDataCache().getData(plate.getRowId(), true, false)) + { + Integer sampleId = wellData.getSampleId(); + if (sampleId == null) + continue; + + boolean isSampleWell = wellData.isSample(); + boolean isReplicateWell = wellData.isReplicate(); + boolean isSampleOrReplicate = isSampleWell || isReplicateWell; + + if (isSampleOrReplicate && wellData.getWellGroup() != null) + { + WellGroup.Type type = isSampleWell ? WellGroup.Type.SAMPLE : WellGroup.Type.REPLICATE; + groupSampleMap.put(Pair.of(type, wellData.getWellGroup()), sampleId); + } + } + } + @Override public void init(Container container, User user, ExecutionContext context) throws ValidationException { @@ -268,7 +403,7 @@ private Map generateSampleWellsFromSourcePlates(Execut { int sourceRowId = sourcePlate.getRowId(); - for (WellData wellData : context.getWellData(sourceRowId, true, false)) + for (WellData wellData : context.wellDataCache().getData(sourceRowId, true, false)) { Integer wellSampleId = wellData.getSampleId(); if (wellSampleId != null && !sampleWells.containsKey(wellSampleId) && (wellData.isSample() || wellData.isReplicate())) @@ -298,4 +433,10 @@ public boolean requiresTargetTemplate() { return Layout.Template.equals(_layout); } + + @Override + public boolean supportsFillExistingWells() + { + return true; + } } diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java index 7077732b2e4..395cc46dd4f 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java @@ -6,6 +6,7 @@ import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; import org.labkey.assay.plate.PlateManager; +import org.labkey.assay.plate.data.WellData; import org.labkey.assay.plate.model.ReformatOptions; import java.util.Collection; @@ -19,6 +20,7 @@ public class LayoutEngine private final ReformatOptions _options; private Collection _sampleIds; private List _sourcePlates; + private List _targetPlates; private List _targetPlateData; private PlateType _targetPlateType; private Plate _targetTemplate; @@ -30,7 +32,7 @@ public LayoutEngine(ReformatOptions options, List allPlateT _allPlateTypes = allPlateTypes; } - public List run(Container container, User user) throws ValidationException + public List run(Container container, User user, WellData.Cache wellDataCache) throws ValidationException { if (_operation.requiresSourcePlates() && _sourcePlates.isEmpty()) throw new ValidationException("Invalid configuration. Source plates are required to run the layout engine."); @@ -38,6 +40,8 @@ public List run(Container container, User user) throws ValidationExc throw new ValidationException("A target plate type is required for this operation."); if (_operation.requiresTargetTemplate() && _targetTemplate == null) throw new ValidationException("A target plate template is required for this operation."); + if (_options.isFillExistingWells() && !_operation.supportsFillExistingWells()) + throw new ValidationException("Filling existing wells is not supported for this operation."); LayoutOperation.ExecutionContext context = new LayoutOperation.ExecutionContext( container, @@ -47,9 +51,10 @@ public List run(Container container, User user) throws ValidationExc _targetPlateType, _sourcePlates, _targetTemplate, + _targetPlates, _targetPlateData, _sampleIds, - new HashMap<>() + wellDataCache ); _operation.init(container, user, context); @@ -87,6 +92,11 @@ public void setSourcePlates(List sourcePlates) _sourcePlates = sourcePlates; } + public void setTargetPlates(List targetPlates) + { + _targetPlates = targetPlates; + } + public void setTargetPlateData(List targetPlateData) { _targetPlateData = targetPlateData; diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java index 96d53a43046..6c43a43d6ae 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java @@ -1,6 +1,5 @@ package org.labkey.assay.plate.layout; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.PlateType; @@ -13,7 +12,6 @@ import java.util.Collection; import java.util.List; -import java.util.Map; public interface LayoutOperation { @@ -43,7 +41,10 @@ default boolean requiresTargetTemplate() return false; } - record WellDataCacheKey(int plateRowId, boolean includeSamples, boolean includeMetadata) {} + default boolean supportsFillExistingWells() + { + return false; + } record ExecutionContext( Container container, @@ -53,9 +54,10 @@ record ExecutionContext( PlateType targetPlateType, List sourcePlates, Plate targetTemplate, - List plateData, + List targetPlates, + List targetPlateData, Collection sampleIds, - Map> wellDataCache + WellData.Cache wellDataCache ) { public @Nullable PlateType resolvePlateType(Integer plateTypeRowId) @@ -65,10 +67,5 @@ record ExecutionContext( return allPlateTypes.stream().filter(plateType -> plateType.getRowId().equals(plateTypeRowId)).findFirst().orElse(null); } - - public @NotNull List getWellData(int plateRowId, boolean includeSamples, boolean includeMetadata) - { - return wellDataCache.computeIfAbsent(new WellDataCacheKey(plateRowId, includeSamples, includeMetadata), (k) -> PlateManager.get().getWellData(container, user, k.plateRowId, k.includeSamples, k.includeMetadata)); - } } } diff --git a/assay/src/org/labkey/assay/plate/layout/WellLayout.java b/assay/src/org/labkey/assay/plate/layout/WellLayout.java index 5e3899c6320..b9b7ad833bb 100644 --- a/assay/src/org/labkey/assay/plate/layout/WellLayout.java +++ b/assay/src/org/labkey/assay/plate/layout/WellLayout.java @@ -10,22 +10,29 @@ public record Well(int destinationRowIdx, int destinationColIdx, int sourcePlate private final PlateType _plateType; private final boolean _sampleOnly; + private final Integer _targetPlateId; private final Integer _targetTemplateId; private final Well[] _wells; public WellLayout(@NotNull PlateType plateType) { - this(plateType, false, null); + this(plateType, false, null, null); } - public WellLayout(@NotNull PlateType plateType, boolean sampleOnly, @Nullable Integer targetTemplateId) + public WellLayout(@NotNull PlateType plateType, boolean sampleOnly, @Nullable Integer targetTemplateId, @Nullable Integer targetPlateId) { _plateType = plateType; _sampleOnly = sampleOnly; + _targetPlateId = targetPlateId; _targetTemplateId = targetTemplateId; _wells = new Well[plateType.getWellCount()]; } + public @Nullable Integer getTargetPlateId() + { + return _targetPlateId; + } + public @Nullable Integer getTargetTemplateId() { return _targetTemplateId; diff --git a/assay/src/org/labkey/assay/plate/model/ReformatOptions.java b/assay/src/org/labkey/assay/plate/model/ReformatOptions.java index 62b1bf15d63..0c9a1efd995 100644 --- a/assay/src/org/labkey/assay/plate/model/ReformatOptions.java +++ b/assay/src/org/labkey/assay/plate/model/ReformatOptions.java @@ -146,6 +146,7 @@ public void setSourceType(SourceType sourceType) } } + private Boolean _fillExistingWells = false; private ReformatOperation _operation; private List _plates; private List _plateRowIds; @@ -156,6 +157,17 @@ public void setSourceType(SourceType sourceType) private TargetPlateSet _targetPlateSet; private TargetPlateSource _targetPlateSource; + public Boolean isFillExistingWells() + { + return _fillExistingWells; + } + + public ReformatOptions setFillExistingWells(Boolean fillExistingWells) + { + _fillExistingWells = fillExistingWells; + return this; + } + public ReformatOperation getOperation() { return _operation; From 0b5c781ad5873dca7e66815fe07e58f47e55a431 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Tue, 7 Jan 2025 08:09:41 -0800 Subject: [PATCH 11/19] Change plate processing --- .../org/labkey/assay/plate/PlateManager.java | 5 +- .../assay/plate/layout/ArrayOperation.java | 52 +++++++++---------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index efd980a3db8..4eb07315d78 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -4132,12 +4132,13 @@ public record ReformatResult( List wellLayouts = engine.run(container, user, wellDataCache); int availablePlateCount = targetPlateSet.availablePlateCount(); - if (availablePlateCount < wellLayouts.size()) + long newPlateCount = wellLayouts.stream().filter(layout -> layout.getTargetPlateId() == null).count(); + if (availablePlateCount < newPlateCount) { throw new ValidationException(String.format( "This plate set has space for %d more plates. This operation will generate %d plates.", availablePlateCount, - wellLayouts.size() + newPlateCount )); } diff --git a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java index e71aad60efe..94c05e9e274 100644 --- a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java @@ -49,7 +49,6 @@ public List execute(ExecutionContext context) throws ValidationExcep List targetLayouts = new ArrayList<>(); - // TODO: Document how this is handled. // We look back at plates when populating existing which effectively means we inherit grouped samples and plate // them on subsequent new plates so that the rules are upheld. This may not be what users are expecting. // Does this need to be a choice? It's difficult to grasp. @@ -62,13 +61,15 @@ public List execute(ExecutionContext context) throws ValidationExcep } } - // Remove samples that are already plated in this plate set? + // TODO: Remove samples that are already plated in this plate set? // sampleIds.removeAll(groupSampleMap.values()); + List targetPlateData = new ArrayList<>(context.targetPlateData()); + // Plate all samples while (sampleIndex < sampleIds.size()) { - WellLayout wellLayout = getNextWellLayout(context, targetLayouts, layouts.size()); + WellLayout wellLayout = getNextWellLayout(context, targetLayouts, targetPlateData); if (wellLayout == null) throw new ValidationException(String.format("Only %d of %d samples could be plated with this configuration.", sampleIndex, sampleIds.size())); @@ -98,15 +99,12 @@ else if (wellLayout.getTargetTemplateId() != null) } // Layout any further plates that have been requested (if any) - List plateData = context.targetPlateData(); - if (plateData != null && plateData.size() > layouts.size()) + while (!targetPlateData.isEmpty()) { - while (layouts.size() < plateData.size()) - { - WellLayout wellLayout = getPlateDataWellLayout(context, layouts.size()); - if (wellLayout == null) - throw new ValidationException(String.format("Failed to resolve plate at index %d.", layouts.size())); + WellLayout wellLayout = getPlateDataWellLayout(context, targetPlateData); + if (wellLayout != null) + { if (wellLayout.getTargetTemplateId() != null) { Pair result = executeTemplateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleIndex); @@ -120,14 +118,18 @@ else if (wellLayout.getTargetTemplateId() != null) return layouts; } - private @Nullable WellLayout getNextWellLayout(ExecutionContext context, List targetLayouts, int numLayouts) + private @Nullable WellLayout getNextWellLayout( + ExecutionContext context, + List targetLayouts, + List targetPlateData + ) { WellLayout layout; if (!targetLayouts.isEmpty()) return targetLayouts.remove(0); - if (context.targetPlateData() != null && !context.targetPlateData().isEmpty()) - layout = getPlateDataWellLayout(context, numLayouts); + if (targetPlateData != null && !targetPlateData.isEmpty()) + layout = getPlateDataWellLayout(context, targetPlateData); else if (context.targetTemplate() != null) layout = new WellLayout(context.targetTemplate().getPlateType(), false, context.targetTemplate().getRowId(), null); else @@ -136,23 +138,21 @@ else if (context.targetTemplate() != null) return layout; } - private @Nullable WellLayout getPlateDataWellLayout(ExecutionContext context, int plateIndex) + private @Nullable WellLayout getPlateDataWellLayout(ExecutionContext context, @NotNull List plateData) { - List plateData = context.targetPlateData(); + if (plateData.isEmpty()) + return null; - if (plateData != null && plateData.size() > plateIndex) + PlateManager.PlateData targetPlateData = plateData.remove(0); + if (targetPlateData != null && targetPlateData.plateType() != null && targetPlateData.plateType() > 0) { - PlateManager.PlateData targetPlateData = plateData.get(plateIndex); - if (targetPlateData.plateType() != null && targetPlateData.plateType() > 0) + PlateType targetPlateDataType = context.resolvePlateType(targetPlateData.plateType()); + if (targetPlateDataType != null) { - PlateType targetPlateDataType = context.resolvePlateType(targetPlateData.plateType()); - if (targetPlateDataType != null) - { - if (targetPlateData.templateId() != null) - return new WellLayout(targetPlateDataType, false, targetPlateData.templateId(), null); - else - return new WellLayout(targetPlateDataType, true, null, null); - } + if (targetPlateData.templateId() != null) + return new WellLayout(targetPlateDataType, false, targetPlateData.templateId(), null); + else + return new WellLayout(targetPlateDataType, true, null, null); } } From 8c3befb755a84c5eb10c375fa013bf75541057fa Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Tue, 7 Jan 2025 17:34:59 -0800 Subject: [PATCH 12/19] ArrayOperation: check plate run counts --- .../src/org/labkey/assay/PlateController.java | 1 - .../org/labkey/assay/plate/PlateManager.java | 155 +++++++++++++++--- .../assay/plate/layout/ArrayOperation.java | 52 +++--- 3 files changed, 166 insertions(+), 42 deletions(-) diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index a86bfb4153b..49d53f5b504 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -16,7 +16,6 @@ package org.labkey.assay; import org.apache.commons.lang3.ArrayUtils; -import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.json.JSONArray; import org.json.JSONObject; diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 4eb07315d78..9ee17f6678e 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -146,6 +146,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.math.BigDecimal; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; @@ -170,6 +172,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; import static java.util.Collections.unmodifiableList; import static org.labkey.api.assay.plate.PlateSet.MAX_PLATES; import static org.labkey.assay.plate.query.WellTable.WELL_LOCATION; @@ -385,7 +388,7 @@ private void deriveCustomFieldsFromWellData( { TableInfo wellTable = getWellTable(container, user); TableInfo metadataTable = getPlateMetadataTable(container, user); - Set metadataFields = Collections.emptySet(); + Set metadataFields = emptySet(); if (metadataTable != null) metadataFields = metadataTable.getColumns().stream().map(ColumnInfo::getFieldKey).collect(Collectors.toSet()); @@ -504,17 +507,98 @@ public int getRunCountUsingPlate(@NotNull Container c, @NotNull User user, @NotN if (se != null) count += (int) se.getRowCount(); - count += getRunIdsUsingPlateInResults(c, user, plate).size(); + count += getRunCountUsingPlateInResults(c, user, plate); return count; } - private @NotNull List getRunIdsUsingPlateInResults(@NotNull Container c, @NotNull User user, @NotNull Plate plate) + /** + * @return A map of plate rowId to total number of runs across all plate-based assay runs in the + * container/user scope for the specified plates. + */ + public Map getPlateRunCounts(@NotNull Container c, @NotNull User user, @NotNull Collection plates) + { + if (plates.isEmpty()) + return emptyMap(); + + Map resultMap = new HashMap<>(); + for (Plate plate : plates) + { + if (plate.getRowId() != null) + resultMap.put(plate.getRowId(), 0L); + } + + AssayProvider provider = AssayService.get().getProvider(TsvAssayProvider.NAME); + if (provider == null) + return resultMap; + + List protocols = AssayService.get().getAssayProtocols(c, provider) + .stream().filter(provider::isPlateMetadataEnabled).toList(); + + // get the runIds for each protocol, query against its assay results table + List fragments = new ArrayList<>(); + TableInfo runTable = ExperimentService.get().getTinfoExperimentRun(); + TableInfo dataTable = ExperimentService.get().getTinfoData(); + Set plateRowIds = resultMap.keySet(); + + for (ExpProtocol protocol : protocols) + { + AssayProtocolSchema assayProtocolSchema = provider.createProtocolSchema(user, protocol.getContainer(), protocol, null); + TableInfo assayDataTable = assayProtocolSchema.createDataTable(ContainerFilter.EVERYTHING, false); + if (assayDataTable != null) + { + ColumnInfo dataIdCol = assayDataTable.getColumn("DataId"); + if (dataIdCol != null) + { + SQLFragment dataTableSql = assayDataTable.getFromSQL("AD", Set.of(FieldKey.fromParts("DataId"), FieldKey.fromParts("Plate"))); + SQLFragment sql = new SQLFragment("SELECT AD.Plate, COUNT(DISTINCT D.RunId) AS RunCount\n") + .append(" FROM ").append(dataTable, "D\n") + .append(" INNER JOIN ").append(runTable, "R").append(" ON D.RunId = R.RowId\n") + .append(" INNER JOIN ").append(dataTableSql).append(" ON AD.DataId = D.RowId\n") + .append(" WHERE R.ReplacedByRunId IS NULL AND AD.Plate").appendInClause(plateRowIds, dataTable.getSqlDialect()).append("\n") + .append(" GROUP BY AD.Plate\n"); + fragments.add(sql); + } + } + } + + if (fragments.isEmpty()) + return resultMap; + + SQLFragment sql = new SQLFragment(); + String union = null; + for (SQLFragment fragment : fragments) + { + if (union == null) + union = "UNION\n"; + else + sql.append(union); + sql.append(fragment); + } + + try (ResultSet rs = new SqlSelector(ExperimentService.get().getSchema(), sql).getResultSet()) + { + while (rs.next()) + { + Integer plateRowId = rs.getInt("Plate"); + Long runCount = rs.getLong("RunCount"); + resultMap.put(plateRowId, resultMap.get(plateRowId) + runCount); + } + } + catch (SQLException e) + { + throw UnexpectedException.wrap(e); + } + + return resultMap; + } + + private int getRunCountUsingPlateInResults(@NotNull Container c, @NotNull User user, @NotNull Plate plate) { // first, get the list of GPAT protocols in the container AssayProvider provider = AssayService.get().getProvider(TsvAssayProvider.NAME); if (provider == null) - return emptyList(); + return 0; List protocols = AssayService.get().getAssayProtocols(c, provider) .stream().filter(provider::isPlateMetadataEnabled).toList(); @@ -535,7 +619,7 @@ public int getRunCountUsingPlate(@NotNull Container c, @NotNull User user, @NotN .append(" WHERE AD.Plate = ?") .add(plate.getRowId()); - SQLFragment sql = new SQLFragment("SELECT DISTINCT D.RunId FROM\n") + SQLFragment sql = new SQLFragment("SELECT COUNT(DISTINCT D.RunId) AS RunCount FROM\n") .append(ExperimentService.get().getTinfoData(), "D") .append(" INNER JOIN ") .append(ExperimentService.get().getTinfoExperimentRun(), "R") @@ -548,20 +632,22 @@ public int getRunCountUsingPlate(@NotNull Container c, @NotNull User user, @NotN } if (fragments.isEmpty()) - return emptyList(); + return 0; - SQLFragment sql = new SQLFragment(); + SQLFragment unionSql = new SQLFragment(); String union = null; for (SQLFragment fragment : fragments) { if (union == null) union = "UNION\n"; else - sql.append(union); - sql.append(fragment); + unionSql.append(union); + unionSql.append(fragment); } - return new SqlSelector(ExperimentService.get().getSchema(), sql).getArrayList(Integer.class); + SQLFragment sql = new SQLFragment("SELECT SUM(RunCount) AS RunCountSum FROM (").append(unionSql).append(")"); + + return ((BigDecimal) new SqlSelector(ExperimentService.get().getSchema(), sql).getMap().get("RunCountSum")).intValueExact(); } private @Nullable SqlSelector selectRunUsingPlateTemplate(@NotNull Container c, @NotNull User user, @NotNull Plate plate) @@ -1730,7 +1816,7 @@ private void copyWellData(User user, @NotNull Plate source, @NotNull Plate copy, } else { - wellMetadataFields = Collections.emptySet(); + wellMetadataFields = emptySet(); sourceMetaData = emptyMap(); } @@ -4079,7 +4165,8 @@ public record ReformatResult( Integer plateSetRowId, String plateSetName, List plateRowIds, - Integer platedSampleCount + Integer platedSampleCount, + Integer selectedSampleCount ) {} /** @@ -4123,7 +4210,12 @@ public record ReformatResult( Pair targetPlateSource = getReformatTargetPlateSource(container, options); engine.setTargetPlateType(targetPlateSource.first); engine.setTargetTemplate(targetPlateSource.second); - engine.setSampleIds(getSelectedSampleIds(options)); + + // Resolve selected sample configuration (if any) + Pair, Integer> sampleSelection = resolveSelectedSamples(options.getSampleSelectionKey(), targetPlateSet); + engine.setSampleIds(sampleSelection.first); + Integer selectedSampleCount = sampleSelection.second; + engine.setTargetPlates(getReformatTargetPlates(targetPlateSet)); engine.setTargetPlateData(getReformatTargetPlateData(options, targetPlateSet)); @@ -4151,7 +4243,7 @@ public record ReformatResult( if (options.isPreview()) { List previewData = getPreviewData(options, existingPlates, plateData, allPlateTypes); - return new ReformatResult(previewData, plateData.size(), existingPlates.size(), null, null, null, hydratedResults.platedSampleCount()); + return new ReformatResult(previewData, plateData.size(), existingPlates.size(), null, null, null, hydratedResults.platedSampleCount(), selectedSampleCount); } if (plateData.isEmpty() && existingPlates.isEmpty()) @@ -4195,7 +4287,7 @@ public record ReformatResult( } List plateRowIds = newPlates.stream().map(Plate::getRowId).toList(); - return new ReformatResult(null, plateRowIds.size(), existingPlates.size(), plateSetRowId, plateSetName, plateRowIds, hydratedResults.platedSampleCount()); + return new ReformatResult(null, plateRowIds.size(), existingPlates.size(), plateSetRowId, plateSetName, plateRowIds, hydratedResults.platedSampleCount(), selectedSampleCount); } private @Nullable List getPreviewData( @@ -4460,17 +4552,40 @@ else if (!Objects.equals(sourcePlateSet.getRowId(), plateSet.getRowId())) return plateData; } - private Collection getSelectedSampleIds(ReformatOptions options) throws ValidationException + public @NotNull Pair, Integer> resolveSelectedSamples(String sampleSelectionKey, @NotNull PlateSetImpl targetPlateSet) throws ValidationException { - String selectionKey = StringUtils.trimToNull(options.getSampleSelectionKey()); + String selectionKey = StringUtils.trimToNull(sampleSelectionKey); if (selectionKey == null) - return Collections.emptyList(); + return Pair.of(emptyList(), null); - List sampleIds = getSelection(selectionKey).stream().toList(); + Collection sampleIds = getSelection(selectionKey).stream().toList(); if (sampleIds.isEmpty()) throw new ValidationException("Empty sample selection."); - return sampleIds; + int selectedSampleCount = sampleIds.size(); + + if (targetPlateSet.isPrimary() && !targetPlateSet.isNew()) + { + AssayDbSchema schema = AssayDbSchema.getInstance(); + + SQLFragment sql = new SQLFragment("SELECT DISTINCT W.SampleId FROM ").append(schema.getTableInfoWell(), "W") + .append(" INNER JOIN ").append(schema.getTableInfoPlate(), "P").append(" ON P.RowId = W.PlateId") + .append(" INNER JOIN ").append(schema.getTableInfoPlateSet(), "PS").append(" ON PS.RowID = P.PlateSet") + .append(" WHERE PS.RowId = ?").add(targetPlateSet.getRowId()) + .append(" AND W.SampleID ").appendInClause(sampleIds, schema.getScope().getSqlDialect()); + + List overlap = new SqlSelector(schema.getSchema(), sql).getArrayList(Integer.class); + if (!overlap.isEmpty()) + { + sampleIds = new ArrayList<>(sampleIds); + sampleIds.removeAll(overlap); + + if (sampleIds.isEmpty()) + throw new ValidationException(String.format("All %d selected samples are already plated in plate set \"%s\".", selectedSampleCount, targetPlateSet.getName())); + } + } + + return Pair.of(sampleIds, selectedSampleCount); } private PlateData hydrateFromExistingPlate(HydrateContext context, WellLayout wellLayout, @NotNull Plate existingPlate) diff --git a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java index 94c05e9e274..8d5f83ef48c 100644 --- a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java @@ -42,9 +42,10 @@ public List execute(ExecutionContext context) throws ValidationExcep int sampleIndex = 0; List layouts = new ArrayList<>(); Map, Integer> groupSampleMap = new HashMap<>(); + Map sampleWells = new LinkedHashMap<>(_sampleWells); List sampleIds = new ArrayList<>(); - for (Map.Entry entry : _sampleWells.entrySet()) + for (Map.Entry entry : sampleWells.entrySet()) sampleIds.add(entry.getKey()); List targetLayouts = new ArrayList<>(); @@ -54,16 +55,17 @@ public List execute(ExecutionContext context) throws ValidationExcep // Does this need to be a choice? It's difficult to grasp. if (context.options().isFillExistingWells() && context.targetPlates() != null) { + Map plateRunCounts = PlateManager.get().getPlateRunCounts(context.container(), context.user(), context.targetPlates()); + for (Plate plate : context.targetPlates()) { - populateGroupSampleMap(context, plate, groupSampleMap); - targetLayouts.add(new WellLayout(plate.getPlateType(), false, null, plate.getRowId())); + populateGroupSampleMap(context, plate, groupSampleMap, sampleWells); + + if (plateRunCounts.get(plate.getRowId()) == 0) + targetLayouts.add(new WellLayout(plate.getPlateType(), false, null, plate.getRowId())); } } - // TODO: Remove samples that are already plated in this plate set? - // sampleIds.removeAll(groupSampleMap.values()); - List targetPlateData = new ArrayList<>(context.targetPlateData()); // Plate all samples @@ -77,7 +79,7 @@ public List execute(ExecutionContext context) throws ValidationExcep if (wellLayout.getTargetPlateId() != null) { - result = executeTargetPlateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleIndex); + result = executeTargetPlateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleWells, sampleIndex); // The counter may not advance here and that is OK since the plate sample/replicate wells may be full. if (result.first == sampleIndex) @@ -85,14 +87,14 @@ public List execute(ExecutionContext context) throws ValidationExcep } else if (wellLayout.getTargetTemplateId() != null) { - result = executeTemplateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleIndex); + result = executeTemplateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleWells, sampleIndex); // The counter did not advance for this well layout meaning we did not plate any additional samples. if (result.first == sampleIndex) throw new ValidationException(String.format("There are %d selected samples and only %d unique sample regions are available in template \"%s\".", sampleIds.size(), sampleIndex, context.targetTemplate().getName())); } else - result = executeRowColumnLayout(wellLayout, sampleIds, sampleIndex); + result = executeRowColumnLayout(wellLayout, sampleWells, sampleIds, sampleIndex); layouts.add(result.second); sampleIndex = result.first; @@ -107,7 +109,7 @@ else if (wellLayout.getTargetTemplateId() != null) { if (wellLayout.getTargetTemplateId() != null) { - Pair result = executeTemplateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleIndex); + Pair result = executeTemplateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleWells, sampleIndex); layouts.add(result.second); } else @@ -118,7 +120,7 @@ else if (wellLayout.getTargetTemplateId() != null) return layouts; } - private @Nullable WellLayout getNextWellLayout( + private static @Nullable WellLayout getNextWellLayout( ExecutionContext context, List targetLayouts, List targetPlateData @@ -138,7 +140,7 @@ else if (context.targetTemplate() != null) return layout; } - private @Nullable WellLayout getPlateDataWellLayout(ExecutionContext context, @NotNull List plateData) + private static @Nullable WellLayout getPlateDataWellLayout(ExecutionContext context, @NotNull List plateData) { if (plateData.isEmpty()) return null; @@ -159,7 +161,7 @@ else if (context.targetTemplate() != null) return null; } - private Pair executeRowColumnLayout(WellLayout target, List sampleIds, int sampleIndex) + private Pair executeRowColumnLayout(WellLayout target, Map sampleWells, List sampleIds, int sampleIndex) { PlateType targetPlateType = target.getPlateType(); boolean isColumnLayout = Layout.Column.equals(_layout); @@ -171,7 +173,7 @@ private Pair executeRowColumnLayout(WellLayout target, List for (int i = sampleIndex; i < sampleIds.size(); i++) { - WellLayout.Well sourceWell = _sampleWells.get(sampleIds.get(i)); + WellLayout.Well sourceWell = sampleWells.get(sampleIds.get(i)); target.setWell(targetRowIdx, targetColIdx, sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sourceWell.sourceSampleId()); sampleCounter++; @@ -209,6 +211,7 @@ private Pair executeTargetPlateLayout( WellLayout target, List sampleIds, Map, Integer> groupSampleMap, + Map sampleWells, int sampleIndex ) { @@ -246,7 +249,7 @@ private Pair executeTargetPlateLayout( if (isSampleOrReplicate && groupKey != null && groupSampleMap.containsKey(groupKey)) { Integer sampleId = groupSampleMap.get(groupKey); - WellLayout.Well sourceWell = _sampleWells.get(sampleId); + WellLayout.Well sourceWell = sampleWells.get(sampleId); target.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); } } @@ -272,7 +275,7 @@ else if (isSampleOrReplicate) sampleIndex++; } - WellLayout.Well sourceWell = _sampleWells.get(sampleId); + WellLayout.Well sourceWell = sampleWells.get(sampleId); target.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); } else if (wellData.getType() == null) @@ -280,7 +283,7 @@ else if (wellData.getType() == null) Integer sampleId = sampleIds.get(sampleIndex); sampleIndex++; - WellLayout.Well sourceWell = _sampleWells.get(sampleId); + WellLayout.Well sourceWell = sampleWells.get(sampleId); target.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); } } @@ -290,11 +293,12 @@ else if (wellData.getType() == null) return Pair.of(sampleIndex, target); } - private Pair executeTemplateLayout( + private static Pair executeTemplateLayout( ExecutionContext context, WellLayout target, List sampleIds, Map, Integer> groupSampleMap, + Map sampleWells, int sampleIndex ) { @@ -317,7 +321,7 @@ private Pair executeTemplateLayout( if (isSampleOrReplicate && groupKey != null && groupSampleMap.containsKey(groupKey)) { Integer sampleId = groupSampleMap.get(groupKey); - WellLayout.Well sourceWell = _sampleWells.get(sampleId); + WellLayout.Well sourceWell = sampleWells.get(sampleId); target.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); } } @@ -343,7 +347,7 @@ else if (isSampleOrReplicate) sampleIndex++; } - WellLayout.Well sourceWell = _sampleWells.get(sampleId); + WellLayout.Well sourceWell = sampleWells.get(sampleId); target.setWell(wellData.getRow(), wellData.getCol(), sourceWell.sourcePlateId(), sourceWell.sourceRowIdx(), sourceWell.sourceColIdx(), sampleId); } } @@ -351,7 +355,12 @@ else if (isSampleOrReplicate) return Pair.of(sampleIndex, target); } - private void populateGroupSampleMap(ExecutionContext context, Plate plate, Map, Integer> groupSampleMap) + private static void populateGroupSampleMap( + ExecutionContext context, + Plate plate, + Map, Integer> groupSampleMap, + Map sampleWells + ) { for (WellData wellData : context.wellDataCache().getData(plate.getRowId(), true, false)) { @@ -367,6 +376,7 @@ private void populateGroupSampleMap(ExecutionContext context, Plate plate, Map

Date: Wed, 8 Jan 2025 10:06:44 -0800 Subject: [PATCH 13/19] Match target plate specifications --- .../assay/plate/layout/ArrayOperation.java | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java index 8d5f83ef48c..e7d0be0f539 100644 --- a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java @@ -67,10 +67,17 @@ public List execute(ExecutionContext context) throws ValidationExcep } List targetPlateData = new ArrayList<>(context.targetPlateData()); + boolean hasTargetPlateData = !targetPlateData.isEmpty(); + int initialSampleCount = sampleIds.size(); // Plate all samples while (sampleIndex < sampleIds.size()) { + // If target plates are specified, then require that those plate configurations are enough to plate all + // the samples. Otherwise, when target plates are not specified, generate additional plates. + if (hasTargetPlateData && targetPlateData.isEmpty()) + throw new ValidationException(String.format("Only %d of %d samples could be plated with this configuration.", sampleIndex + 1, initialSampleCount)); + WellLayout wellLayout = getNextWellLayout(context, targetLayouts, targetPlateData); if (wellLayout == null) throw new ValidationException(String.format("Only %d of %d samples could be plated with this configuration.", sampleIndex, sampleIds.size())); @@ -85,16 +92,21 @@ public List execute(ExecutionContext context) throws ValidationExcep if (result.first == sampleIndex) continue; } - else if (wellLayout.getTargetTemplateId() != null) + else { - result = executeTemplateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleWells, sampleIndex); + if (wellLayout.getTargetTemplateId() != null) + { + result = executeTemplateLayout(context, wellLayout, sampleIds, groupSampleMap, sampleWells, sampleIndex); - // The counter did not advance for this well layout meaning we did not plate any additional samples. - if (result.first == sampleIndex) - throw new ValidationException(String.format("There are %d selected samples and only %d unique sample regions are available in template \"%s\".", sampleIds.size(), sampleIndex, context.targetTemplate().getName())); + // The counter did not advance for this well layout meaning we did not plate any additional samples. + if (result.first == sampleIndex) + throw new ValidationException(String.format("There are %d selected samples and only %d unique sample regions are available in template \"%s\".", sampleIds.size(), sampleIndex, context.targetTemplate().getName())); + } + else + { + result = executeRowColumnLayout(wellLayout, sampleWells, sampleIds, sampleIndex); + } } - else - result = executeRowColumnLayout(wellLayout, sampleWells, sampleIds, sampleIndex); layouts.add(result.second); sampleIndex = result.first; From f7cf4c61355c2e184b120ee99220e18317cf7174 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 9 Jan 2025 12:38:30 -0800 Subject: [PATCH 14/19] Support fillPlatesOnly --- .../assay/plate/layout/ArrayOperation.java | 18 ++++++++++++------ .../assay/plate/layout/LayoutEngine.java | 2 ++ .../assay/plate/layout/LayoutOperation.java | 5 +++++ .../assay/plate/model/ReformatOptions.java | 11 +++++++++++ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java index e7d0be0f539..db04546ef2e 100644 --- a/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/ArrayOperation.java @@ -67,20 +67,20 @@ public List execute(ExecutionContext context) throws ValidationExcep } List targetPlateData = new ArrayList<>(context.targetPlateData()); - boolean hasTargetPlateData = !targetPlateData.isEmpty(); + boolean isFillPlatesOnly = context.options().isFillPlatesOnly(); int initialSampleCount = sampleIds.size(); // Plate all samples while (sampleIndex < sampleIds.size()) { - // If target plates are specified, then require that those plate configurations are enough to plate all - // the samples. Otherwise, when target plates are not specified, generate additional plates. - if (hasTargetPlateData && targetPlateData.isEmpty()) - throw new ValidationException(String.format("Only %d of %d samples could be plated with this configuration.", sampleIndex + 1, initialSampleCount)); + // If isFillPlatesOnly is true, then require that those target plate configurations are enough to plate all + // the samples. Otherwise, generate additional plates. + if (isFillPlatesOnly && targetPlateData.isEmpty() && targetLayouts.isEmpty()) + throw new ValidationException(String.format("%s%d of %d samples could be plated with this configuration.", sampleIndex == 0 ? "" : "Only ", sampleIndex, initialSampleCount)); WellLayout wellLayout = getNextWellLayout(context, targetLayouts, targetPlateData); if (wellLayout == null) - throw new ValidationException(String.format("Only %d of %d samples could be plated with this configuration.", sampleIndex, sampleIds.size())); + throw new ValidationException(String.format("%s%d of %d samples could be plated with this configuration.", sampleIndex == 0 ? "" : "Only ", sampleIndex, sampleIds.size())); Pair result; @@ -461,4 +461,10 @@ public boolean supportsFillExistingWells() { return true; } + + @Override + public boolean supportsFillPlatesOnly() + { + return true; + } } diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java index 395cc46dd4f..4f9b3e8aa06 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutEngine.java @@ -42,6 +42,8 @@ public List run(Container container, User user, WellData.Cache wellD throw new ValidationException("A target plate template is required for this operation."); if (_options.isFillExistingWells() && !_operation.supportsFillExistingWells()) throw new ValidationException("Filling existing wells is not supported for this operation."); + if (_options.isFillPlatesOnly() && !_operation.supportsFillPlatesOnly()) + throw new ValidationException("Filling plates only is not supported for this operation."); LayoutOperation.ExecutionContext context = new LayoutOperation.ExecutionContext( container, diff --git a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java index 6c43a43d6ae..25f5a4bbe1d 100644 --- a/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java +++ b/assay/src/org/labkey/assay/plate/layout/LayoutOperation.java @@ -46,6 +46,11 @@ default boolean supportsFillExistingWells() return false; } + default boolean supportsFillPlatesOnly() + { + return false; + } + record ExecutionContext( Container container, User user, diff --git a/assay/src/org/labkey/assay/plate/model/ReformatOptions.java b/assay/src/org/labkey/assay/plate/model/ReformatOptions.java index 0c9a1efd995..b1b1508373e 100644 --- a/assay/src/org/labkey/assay/plate/model/ReformatOptions.java +++ b/assay/src/org/labkey/assay/plate/model/ReformatOptions.java @@ -147,6 +147,7 @@ public void setSourceType(SourceType sourceType) } private Boolean _fillExistingWells = false; + private Boolean _fillPlatesOnly = false; private ReformatOperation _operation; private List _plates; private List _plateRowIds; @@ -168,6 +169,16 @@ public ReformatOptions setFillExistingWells(Boolean fillExistingWells) return this; } + public Boolean isFillPlatesOnly() + { + return _fillPlatesOnly; + } + + public void setFillPlatesOnly(Boolean fillPlatesOnly) + { + _fillPlatesOnly = fillPlatesOnly; + } + public ReformatOperation getOperation() { return _operation; From ff68a965d0a68c088389404fa0274b054ae47753 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Fri, 10 Jan 2025 11:49:32 -0800 Subject: [PATCH 15/19] Support re-plate plate set --- .../org/labkey/assay/plate/PlateManager.java | 73 +++++++++++++++++-- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 9ee17f6678e..248fdea71b3 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -828,6 +828,15 @@ private Object require(Object object, @NotNull String error, @Nullable String er ); } + public @NotNull PlateSet requirePlateSet(Container container, ContainerFilter cf, int plateSetRowId, @Nullable String errorPrefix) throws ValidationException + { + return (PlateSet) require( + getPlateSet(cf, plateSetRowId), + String.format("Plate set with rowId (%d) is not available in %s.", plateSetRowId, container.getPath()), + errorPrefix + ); + } + private @NotNull PlateSet requirePlateSet(@NotNull Plate plate, @Nullable String errorPrefix) throws ValidationException { return (PlateSet) require( @@ -2809,15 +2818,23 @@ public PlateSet createOrAddToPlateSet(Container container, User user, CreatePlat PlateSetImpl targetPlateSet = getTargetPlateSet(container, options); List plates = options.getPlates(); + String selectionKey = options.getSelectionKey(); - if (options.getSelectionKey() != null) + if (targetPlateSet.isNew() && options.getParentPlateSetId() != null && selectionKey == null && plates.isEmpty()) { - String selectionKey = StringUtils.trimToNull(options.getSelectionKey()); - if (selectionKey == null) + // Re-plate into a new plate set. In this specific configuration we support copying the + // parent plate set plates into a new plate set. + return replatePlateSet(container, user, targetPlateSet, options.getParentPlateSetId()); + } + + if (selectionKey != null) + { + String selectionKey_ = StringUtils.trimToNull(selectionKey); + if (selectionKey_ == null) throw new ValidationException("Invalid selection key."); // Re-array samples onto plates - plates = reArrayFromSelection(container, user, plates, selectionKey, options.getOperation()); + plates = reArrayFromSelection(container, user, plates, selectionKey_, options.getOperation()); } else { @@ -2835,6 +2852,30 @@ public PlateSet createOrAddToPlateSet(Container container, User user, CreatePlat return getPlateSet(container, targetPlateSet.getRowId()); } + private PlateSet replatePlateSet( + Container container, + User user, + @NotNull PlateSetImpl targetPlateSet, + Integer sourcePlateSetRowId + ) throws Exception + { + PlateSetImpl parentPlateSet = (PlateSetImpl) requirePlateSet(container, sourcePlateSetRowId, "Failed to create plate set."); + + Integer parentId = parentPlateSet.isStandalone() ? null : parentPlateSet.getRowId(); + + try (DbScope.Transaction tx = ensureTransaction()) + { + PlateSet newPlateSet = createPlateSet(container, user, targetPlateSet, null, parentId); + + for (Plate plate : parentPlateSet.getPlates()) + copyPlate(container, user, plate.getRowId(), false, newPlateSet.getRowId(), null, null, true); + + tx.commit(); + + return getPlateSet(container, newPlateSet.getRowId()); + } + } + private void savePlateSetHeritage(Integer plateSetId, PlateSetType plateSetType, @Nullable PlateSetImpl parentPlateSet) { assert requireActiveTransaction(); @@ -4255,7 +4296,10 @@ public record ReformatResult( if (targetPlateSet.isNew()) { - PlateSet newPlateSet = createPlateSet(container, user, targetPlateSet, plateData, getReformatParentPlateSetId(sourcePlateSet)); + PlateSet parentPlateSet = resolveParentPlateSet(container, user, options, sourcePlateSet); + Integer parentPlateSetId = parentPlateSet != null ? parentPlateSet.getRowId() : null; + + PlateSet newPlateSet = createPlateSet(container, user, targetPlateSet, plateData, parentPlateSetId); plateSetRowId = newPlateSet.getRowId(); plateSetName = newPlateSet.getName(); newPlates = newPlateSet.getPlates(); @@ -4386,10 +4430,23 @@ public record ReformatResult( return previewData; } - private @Nullable Integer getReformatParentPlateSetId(PlateSet sourcePlateSet) + private @Nullable PlateSet resolveParentPlateSet( + Container container, + User user, + ReformatOptions options, + @Nullable PlateSet sourcePlateSet + ) throws ValidationException { - if (sourcePlateSet != null && (sourcePlateSet.isPrimary() || !sourcePlateSet.isStandalone())) - return sourcePlateSet.getRowId(); + if (options.getTargetPlateSet() != null && options.getTargetPlateSet().getParentPlateSetId() != null) + { + // If a parent rowId is specified, then require that it resolves in this container scope + PlateSet parentPlateSet = requirePlateSet(container, getPlateLookupContainerFilter(container, user), options.getTargetPlateSet().getParentPlateSetId(), null); + if (parentPlateSet.isPrimary() || !parentPlateSet.isStandalone()) + return parentPlateSet; + } + else if (sourcePlateSet != null && (sourcePlateSet.isPrimary() || !sourcePlateSet.isStandalone())) + return sourcePlateSet; + return null; } From cbe6d6aeab8691fc6e82bea1f4699e5eeb54ce89 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Fri, 10 Jan 2025 11:49:42 -0800 Subject: [PATCH 16/19] return this --- assay/src/org/labkey/assay/plate/model/ReformatOptions.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/model/ReformatOptions.java b/assay/src/org/labkey/assay/plate/model/ReformatOptions.java index b1b1508373e..bcfcec89984 100644 --- a/assay/src/org/labkey/assay/plate/model/ReformatOptions.java +++ b/assay/src/org/labkey/assay/plate/model/ReformatOptions.java @@ -174,9 +174,10 @@ public Boolean isFillPlatesOnly() return _fillPlatesOnly; } - public void setFillPlatesOnly(Boolean fillPlatesOnly) + public ReformatOptions setFillPlatesOnly(Boolean fillPlatesOnly) { _fillPlatesOnly = fillPlatesOnly; + return this; } public ReformatOperation getOperation() @@ -195,9 +196,10 @@ public List getPlates() return _plates; } - public void setPlates(List plates) + public ReformatOptions setPlates(List plates) { _plates = plates; + return this; } public List getPlateRowIds() From ba342860e4aa88004cd860d5e8e1f77278732954 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Mon, 13 Jan 2025 08:21:58 -0800 Subject: [PATCH 17/19] Remove now redundant error prefixing --- assay/src/org/labkey/assay/plate/PlateManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 248fdea71b3..44af0070d62 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -2859,7 +2859,7 @@ private PlateSet replatePlateSet( Integer sourcePlateSetRowId ) throws Exception { - PlateSetImpl parentPlateSet = (PlateSetImpl) requirePlateSet(container, sourcePlateSetRowId, "Failed to create plate set."); + PlateSetImpl parentPlateSet = (PlateSetImpl) requirePlateSet(container, sourcePlateSetRowId, null); Integer parentId = parentPlateSet.isStandalone() ? null : parentPlateSet.getRowId(); From 7e4004bd6363b7de8e4d077442a43c5f38126f4d Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Mon, 13 Jan 2025 12:22:23 -0800 Subject: [PATCH 18/19] validatePrimaryPlateSetUniqueSamples: skip left joining samples --- .../org/labkey/assay/plate/PlateManager.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 44af0070d62..d18dbd950ec 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -3372,36 +3372,41 @@ public void validatePrimaryPlateSetUniqueSamples(Set wellRowIds, BatchV .append(" WHERE PS.Type = ?").add("primary").append(" AND W.RowId ").appendInClause(wellRowIds, dialect); // From the set of primary plate sets determine if any sample exists in more than one well within the entire plate set - SQLFragment nonUniqueSamplesPerPrimaryPlateSetSQL = new SQLFragment("SELECT PS.Name AS PlateSetName, M.Name AS SampleName FROM ") + SQLFragment nonUniqueSamplesPerPrimaryPlateSetSQL = new SQLFragment("SELECT PS.Name AS PlateSetName, W.SampleId FROM ") .append(wellTable, "W") .append(" INNER JOIN ").append(plateTable, "P").append(" ON P.RowId = W.PlateId") .append(" INNER JOIN ").append(plateSetTable, "PS").append(" ON PS.RowId = P.PlateSet") - .append(" LEFT JOIN ").append(ExperimentService.get().getTinfoMaterial(), "M").append(" ON M.RowId = W.SampleId") .append(" WHERE W.SampleId IS NOT NULL AND PS.RowId IN (").append(primaryPlateSetsFromWellRowIdsSQL).append(")") - .append(" GROUP BY PS.RowId, M.Name, W.SampleId, PS.Name HAVING COUNT(W.SampleId) > 1"); + .append(" GROUP BY PS.RowId, W.SampleId, PS.Name HAVING COUNT(W.SampleId) > 1"); var duplicates = new SqlSelector(dbSchema.getSchema(), nonUniqueSamplesPerPrimaryPlateSetSQL).getMapCollection(); if (!duplicates.isEmpty()) { - Map> duplicateMap = new HashMap<>(); + Map> duplicateMap = new HashMap<>(); for (var duplicate : duplicates) { var plateSetName = (String) duplicate.get("PlateSetName"); - duplicateMap.computeIfAbsent(plateSetName, (n) -> new HashSet<>()).add((String) duplicate.get("SampleName")); + duplicateMap.computeIfAbsent(plateSetName, (n) -> new HashSet<>()).add((Integer) duplicate.get("SampleId")); } for (var entry : duplicateMap.entrySet()) { var plateSetName = entry.getKey(); - var sampleNames = entry.getValue(); + var sampleIds = entry.getValue(); ValidationException ve; - if (sampleNames.size() == 1) - ve = new ValidationException(String.format("Sample \"%s\" is recorded in more than one well in Primary Plate Set \"%s\".", sampleNames.stream().findFirst().get(), plateSetName)); + if (sampleIds.size() == 1) + { + var sampleRowId = sampleIds.stream().findFirst().get(); + var expMaterial = ExperimentService.get().getExpMaterial(sampleRowId); + var sampleName = expMaterial == null ? "unknown" : expMaterial.getName(); + + ve = new ValidationException(String.format("Sample \"%s\" is recorded in more than one well in Primary Plate Set \"%s\".", sampleName, plateSetName)); + } else - ve = new ValidationException(String.format("There are %d samples recorded in more than one well in Primary Plate Set \"%s\".", sampleNames.size(), plateSetName)); + ve = new ValidationException(String.format("There are %d samples recorded in more than one well in Primary Plate Set \"%s\".", sampleIds.size(), plateSetName)); errors.addRowError(ve); } From 839fdd196c829f20fc8d529c5cc1d969d3c4c3fa Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Wed, 15 Jan 2025 16:01:00 -0800 Subject: [PATCH 19/19] Match container scope --- .../src/org/labkey/assay/plate/PlateManager.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index d18dbd950ec..aae2256451e 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -4253,7 +4253,7 @@ public record ReformatResult( List sourcePlates = source.second; engine.setSourcePlates(sourcePlates); - Pair targetPlateSource = getReformatTargetPlateSource(container, options); + Pair targetPlateSource = getReformatTargetPlateSource(container, user, options); engine.setTargetPlateType(targetPlateSource.first); engine.setTargetTemplate(targetPlateSource.second); @@ -4535,7 +4535,11 @@ else if (!hasRowId && !hasType) return plateSet; } - private @NotNull Pair getReformatTargetPlateSource(Container container, ReformatOptions options) throws ValidationException + private @NotNull Pair getReformatTargetPlateSource( + Container container, + User user, + ReformatOptions options + ) throws ValidationException { PlateType targetPlateType = null; Plate targetTemplate = null; @@ -4552,11 +4556,13 @@ else if (!hasRowId && !hasType) targetPlateType = requirePlateType(plateSource.getRowId(), null); else if (ReformatOptions.TargetPlateSource.SourceType.template.equals(plateSource.getSourceType())) { - targetTemplate = requirePlate(container, plateSource.getRowId(), null); + targetTemplate = getPlate(getPlateLookupContainerFilter(container, user), plateSource.getRowId()); + if (targetTemplate == null) + throw new ValidationException(String.format("Unable to plate template with rowId (%d).", plateSource.getRowId())); if (!targetTemplate.isTemplate()) - throw new ValidationException("Plate \"%s\" is not a valid template.", targetTemplate.getName()); + throw new ValidationException(String.format("Plate \"%s\" is not a valid template.", targetTemplate.getName())); if (targetTemplate.isArchived()) - throw new ValidationException("Template \"%s\" is archived and cannot be used for reformatting.", targetTemplate.getName()); + throw new ValidationException(String.format("Template \"%s\" is archived and cannot be used for reformatting.", targetTemplate.getName())); } else throw new ValidationException("A valid \"type\" must be specified for \"targetPlateSource\".");