diff --git a/.github/config/checks.xml b/.github/config/checks.xml index bf65fcacf..cfd73a51d 100644 --- a/.github/config/checks.xml +++ b/.github/config/checks.xml @@ -40,11 +40,6 @@ - - - - - diff --git a/src/arcade/potts/env/location/PottsLocation.java b/src/arcade/potts/env/location/PottsLocation.java index ddb5b1d94..ee15838a4 100644 --- a/src/arcade/potts/env/location/PottsLocation.java +++ b/src/arcade/potts/env/location/PottsLocation.java @@ -10,6 +10,9 @@ import arcade.core.env.location.LocationContainer; import arcade.core.util.Plane; import arcade.core.util.Utilities; +import arcade.core.util.Vector; +import arcade.potts.util.PottsEnums.Direction; +import arcade.potts.util.PottsEnums.Region; import static arcade.potts.util.PottsEnums.Direction; import static arcade.potts.util.PottsEnums.Region; @@ -605,6 +608,16 @@ void updateCenter(int x, int y, int z, int change) { */ abstract ArrayList getSelected(Voxel focus, double n); + /** + * Gets the voxel at specified percentage offsets along the location's axes with the provided + * ApicalAxis considered to be pointing up the Y axis. + * + * @param offsets the percent offsets along the location's axes + * @param apicalAxis the axis considered to be pointing up along the Y axis + * @return the voxel at the specified offset in the frame of the apical axis + */ + abstract Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis); + /** * Gets the direction of the slice orthagonal to the direction with the smallest diameter. * diff --git a/src/arcade/potts/env/location/PottsLocation2D.java b/src/arcade/potts/env/location/PottsLocation2D.java index 0a8fcc986..f61165147 100644 --- a/src/arcade/potts/env/location/PottsLocation2D.java +++ b/src/arcade/potts/env/location/PottsLocation2D.java @@ -1,7 +1,10 @@ package arcade.potts.env.location; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import arcade.core.util.Vector; import static arcade.potts.util.PottsEnums.Direction; /** Concrete implementation of {@link PottsLocation} for 2D. */ @@ -64,4 +67,57 @@ Direction getSlice(Direction direction, HashMap diameters) { ArrayList getSelected(Voxel focus, double n) { return Location2D.getSelected(voxels, focus, n); } + + /** + * Gets the voxel at specified percentage offsets along the location's X and Y axes with the + * provided apicalAxis considered to be pointing up the Y axis. Returns null if this + * PottsLocation2D contains no voxels. + * + * @param offsets the percent offsets along the location's X and Y axes + * @param apicalAxis the axis considered to be pointing up along the Y axis + * @return the voxel through which the plane of division will pass + */ + @Override + public Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis) { + if (voxels.isEmpty()) { + return null; + } + if (offsets == null || offsets.size() != 2) { + throw new IllegalArgumentException("Offsets must be 2 integers."); + } + + // Normalize axes + Vector yAxis = Vector.normalizeVector(apicalAxis); + Vector xAxis = Vector.normalizeVector(new Vector(apicalAxis.getY(), -apicalAxis.getX(), 0)); + + // Project voxels onto apical axis and group by rounded projection + HashMap> apicalBands = new HashMap<>(); + ArrayList apicalKeys = new ArrayList<>(); + + for (Voxel v : voxels) { + Vector pos = new Vector(v.x, v.y, 0); + double apicalProj = Vector.dotProduct(pos, yAxis); + int roundedProj = (int) Math.round(apicalProj); + apicalBands.computeIfAbsent(roundedProj, k -> new ArrayList<>()).add(v); + apicalKeys.add(roundedProj); + } + + // Sort apical keys and choose percentile + Collections.sort(apicalKeys); + int yIndex = + Math.min( + apicalKeys.size() - 1, + (int) ((offsets.get(1) / 100.0) * apicalKeys.size())); + int targetApicalKey = apicalKeys.get(yIndex); + + ArrayList band = apicalBands.get(targetApicalKey); + if (band == null || band.isEmpty()) { + return null; + } + // Project to orthogonal axis within the band and sort + band.sort( + Comparator.comparingDouble(v -> Vector.dotProduct(new Vector(v.x, v.y, 0), xAxis))); + int xIndex = Math.min(band.size() - 1, (int) ((offsets.get(0) / 100.0) * band.size())); + return band.get(xIndex); + } } diff --git a/src/arcade/potts/env/location/PottsLocation3D.java b/src/arcade/potts/env/location/PottsLocation3D.java index b9e9c1aec..c6ad40cd1 100644 --- a/src/arcade/potts/env/location/PottsLocation3D.java +++ b/src/arcade/potts/env/location/PottsLocation3D.java @@ -2,6 +2,8 @@ import java.util.ArrayList; import java.util.HashMap; +import arcade.core.util.Vector; +import arcade.potts.util.PottsEnums.Direction; import static arcade.potts.util.PottsEnums.Direction; /** Concrete implementation of {@link PottsLocation} for 3D. */ @@ -64,4 +66,10 @@ Direction getSlice(Direction direction, HashMap diameters) { ArrayList getSelected(Voxel focus, double n) { return Location3D.getSelected(voxels, focus, n); } + + @Override + Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis) { + throw new UnsupportedOperationException( + "getOffsetInApicalFrame is not implemented for PottsLocation3D"); + } } diff --git a/src/arcade/potts/env/location/PottsLocations.java b/src/arcade/potts/env/location/PottsLocations.java index 1b829e426..4ae66311e 100644 --- a/src/arcade/potts/env/location/PottsLocations.java +++ b/src/arcade/potts/env/location/PottsLocations.java @@ -6,6 +6,8 @@ import ec.util.MersenneTwisterFast; import arcade.core.env.location.Location; import arcade.core.env.location.LocationContainer; +import arcade.core.util.Vector; +import arcade.potts.util.PottsEnums.Region; import static arcade.potts.util.PottsEnums.Region; /** @@ -286,4 +288,10 @@ Location separateVoxels( return splitLocation; } + + @Override + Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis) { + throw new UnsupportedOperationException( + "getOffsetInApicalFrame is not implemented for PottsLocations"); + } } diff --git a/src/arcade/potts/env/location/PottsLocations2D.java b/src/arcade/potts/env/location/PottsLocations2D.java index 7877b24f9..52b5721db 100644 --- a/src/arcade/potts/env/location/PottsLocations2D.java +++ b/src/arcade/potts/env/location/PottsLocations2D.java @@ -2,6 +2,9 @@ import java.util.ArrayList; import java.util.HashMap; +import arcade.core.util.Vector; +import arcade.potts.util.PottsEnums.Direction; +import arcade.potts.util.PottsEnums.Region; import static arcade.potts.util.PottsEnums.Direction; import static arcade.potts.util.PottsEnums.Region; @@ -70,4 +73,10 @@ Direction getSlice(Direction direction, HashMap diameters) { ArrayList getSelected(Voxel focus, double n) { return Location2D.getSelected(locations.get(Region.DEFAULT).voxels, focus, n); } + + @Override + Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis) { + throw new UnsupportedOperationException( + "getOffsetInApicalFrame is not implemented for PottsLocations2D"); + } } diff --git a/test/arcade/potts/env/location/PottsLocation2DTest.java b/test/arcade/potts/env/location/PottsLocation2DTest.java index 81f861ab3..da5d53c6a 100644 --- a/test/arcade/potts/env/location/PottsLocation2DTest.java +++ b/test/arcade/potts/env/location/PottsLocation2DTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import ec.util.MersenneTwisterFast; +import arcade.core.util.Vector; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import static arcade.potts.env.location.Voxel.VOXEL_COMPARATOR; @@ -278,4 +279,92 @@ public void split_balanceableLocationRandomOne_returnsList() { assertEquals(locVoxels, loc.voxels); assertEquals(splitVoxels, split.voxels); } + + @Test + public void getOffsetInApicalFrame2D_offsetAtCenter_returnsExpectedVoxel() { + ArrayList voxels = new ArrayList<>(); + // 3x3 grid centered at (0,0) + for (int x = -1; x <= 1; x++) { + for (int y = -1; y <= 1; y++) { + voxels.add(new Voxel(x, y, 0)); + } + } + PottsLocation2D loc = new PottsLocation2D(voxels); + + Vector apicalAxis = new Vector(0, 1, 0); // Y-axis + ArrayList offsets = new ArrayList<>(); + offsets.add(50); // middle of X axis + offsets.add(50); // middle of Y axis + + Voxel result = loc.getOffsetInApicalFrame(offsets, apicalAxis); + assertEquals(new Voxel(0, 0, 0), result); + } + + @Test + public void getOffsetInApicalFrame2D_offsetUpperRight_returnsExpectedVoxel() { + ArrayList voxels = new ArrayList<>(); + for (int x = 0; x <= 4; x++) { + for (int y = 0; y <= 4; y++) { + voxels.add(new Voxel(x, y, 0)); + } + } + PottsLocation2D loc = new PottsLocation2D(voxels); + + Vector apicalAxis = new Vector(0, 1, 0); // Y-axis + ArrayList offsets = new ArrayList<>(); + offsets.add(100); // far right of X axis + offsets.add(100); // top of Y axis + + Voxel result = loc.getOffsetInApicalFrame(offsets, apicalAxis); + assertEquals(new Voxel(4, 4, 0), result); + } + + @Test + public void getOffsetInApicalFrame2D_emptyVoxels_returnsNull() { + PottsLocation2D loc = new PottsLocation2D(new ArrayList<>()); + + Vector apicalAxis = new Vector(1, 0, 0); + ArrayList offsets = new ArrayList<>(); + offsets.add(50); + offsets.add(50); + + Voxel result = loc.getOffsetInApicalFrame(offsets, apicalAxis); + assertNull(result); + } + + @Test + public void getOffsetInApicalFrame2D_invalidOffset_throwsException() { + ArrayList voxels = new ArrayList<>(); + voxels.add(new Voxel(0, 0, 0)); + PottsLocation2D loc = new PottsLocation2D(voxels); + + Vector apicalAxis = new Vector(1, 0, 0); + + ArrayList badOffset = new ArrayList<>(); + badOffset.add(50); // only one element + + assertThrows( + IllegalArgumentException.class, + () -> { + loc.getOffsetInApicalFrame(badOffset, apicalAxis); + }); + } + + @Test + public void getOffsetInApicalFrame2D_nonOrthogonalAxis_returnsExpected() { + ArrayList voxels = new ArrayList<>(); + voxels.add(new Voxel(0, 0, 0)); + voxels.add(new Voxel(1, 1, 0)); + voxels.add(new Voxel(2, 2, 0)); + voxels.add(new Voxel(3, 3, 0)); + PottsLocation2D loc = new PottsLocation2D(voxels); + + Vector apicalAxis = new Vector(1, 1, 0); // diagonal + ArrayList offsets = new ArrayList<>(); + offsets.add(0); // lowest orthogonal axis + offsets.add(100); // farthest along apical + + Voxel result = loc.getOffsetInApicalFrame(offsets, apicalAxis); + assertEquals(new Voxel(3, 3, 0), result); + } } diff --git a/test/arcade/potts/env/location/PottsLocation3DTest.java b/test/arcade/potts/env/location/PottsLocation3DTest.java index 9b5742c41..a971382d3 100644 --- a/test/arcade/potts/env/location/PottsLocation3DTest.java +++ b/test/arcade/potts/env/location/PottsLocation3DTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import ec.util.MersenneTwisterFast; +import arcade.core.util.Vector; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import static arcade.potts.env.location.Voxel.VOXEL_COMPARATOR; @@ -298,4 +299,14 @@ public void split_balanceableLocationRandomOne_returnsList() { assertEquals(locVoxels, loc.voxels); assertEquals(splitVoxels, split.voxels); } + + @Test + public void getOffsetInApicalFrame_called_raisesUnsupportedOperationException() { + PottsLocation3D loc = new PottsLocation3D(voxelListAB); + Vector apicalAxis = new Vector(0, 1, 0); + ArrayList offsets = new ArrayList<>(); + assertThrows( + UnsupportedOperationException.class, + () -> loc.getOffsetInApicalFrame(offsets, apicalAxis)); + } } diff --git a/test/arcade/potts/env/location/PottsLocationTest.java b/test/arcade/potts/env/location/PottsLocationTest.java index ef48c8f5b..00a90c1a3 100644 --- a/test/arcade/potts/env/location/PottsLocationTest.java +++ b/test/arcade/potts/env/location/PottsLocationTest.java @@ -169,6 +169,11 @@ Direction getSlice(Direction direction, HashMap diameters) { ArrayList getSelected(Voxel center, double n) { return new ArrayList<>(); } + + @Override + Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis) { + return new Voxel(0, 0, 0); + } } @Test diff --git a/test/arcade/potts/env/location/PottsLocationsTest.java b/test/arcade/potts/env/location/PottsLocationsTest.java index 6c082948a..343902f7c 100644 --- a/test/arcade/potts/env/location/PottsLocationsTest.java +++ b/test/arcade/potts/env/location/PottsLocationsTest.java @@ -6,6 +6,10 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import ec.util.MersenneTwisterFast; +import arcade.core.util.Vector; +import arcade.potts.env.location.PottsLocationTest.PottsLocationMock; +import arcade.potts.util.PottsEnums.Direction; +import arcade.potts.util.PottsEnums.Region; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import static arcade.core.ARCADETestUtilities.*; @@ -963,4 +967,14 @@ public void separateVoxels_validListsWithRegions_updatesLists() { assertEquals(2, split.locations.get(Region.DEFAULT).voxels.size()); assertEquals(2, split.locations.get(Region.DEFAULT).voxels.size()); } + + @Test + public void getOffsetInApicalFrame_called_raisesUnsupportedOperationException() { + PottsLocationsMock loc = new PottsLocationsMock(voxelListForMultipleRegionsA); + Vector apicalAxis = new Vector(0, 1, 0); + ArrayList offsets = new ArrayList<>(); + assertThrows( + UnsupportedOperationException.class, + () -> loc.getOffsetInApicalFrame(offsets, apicalAxis)); + } }