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));
+ }
}