Skip to content
Merged
54 changes: 54 additions & 0 deletions src/arcade/potts/env/location/PottsLocation2D.java
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -64,4 +67,55 @@ Direction getSlice(Direction direction, HashMap<Direction, Integer> diameters) {
ArrayList<Voxel> 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.
*
* @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
*/
public Voxel getOffsetInApicalFrame2D(ArrayList<Integer> 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<Integer, ArrayList<Voxel>> apicalBands = new HashMap<>();
ArrayList<Integer> 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<Voxel> 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);
}
}
89 changes: 89 additions & 0 deletions test/arcade/potts/env/location/PottsLocation2DTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -278,4 +279,92 @@ public void split_balanceableLocationRandomOne_returnsList() {
assertEquals(locVoxels, loc.voxels);
assertEquals(splitVoxels, split.voxels);
}

@Test
public void getOffsetInApicalFrame2D_offsetAtCenter_returnsExpectedVoxel() {
ArrayList<Voxel> 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<Integer> offsets = new ArrayList<>();
offsets.add(50); // middle of X axis
offsets.add(50); // middle of Y axis

Voxel result = loc.getOffsetInApicalFrame2D(offsets, apicalAxis);
assertEquals(new Voxel(0, 0, 0), result);
}

@Test
public void getOffsetInApicalFrame2D_offsetUpperRight_returnsExpectedVoxel() {
ArrayList<Voxel> 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<Integer> offsets = new ArrayList<>();
offsets.add(100); // far right of X axis
offsets.add(100); // top of Y axis

Voxel result = loc.getOffsetInApicalFrame2D(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<Integer> offsets = new ArrayList<>();
offsets.add(50);
offsets.add(50);

Voxel result = loc.getOffsetInApicalFrame2D(offsets, apicalAxis);
assertNull(result);
}

@Test
public void getOffsetInApicalFrame2D_invalidOffset_throwsException() {
ArrayList<Voxel> voxels = new ArrayList<>();
voxels.add(new Voxel(0, 0, 0));
PottsLocation2D loc = new PottsLocation2D(voxels);

Vector apicalAxis = new Vector(1, 0, 0);

ArrayList<Integer> badOffset = new ArrayList<>();
badOffset.add(50); // only one element

assertThrows(
IllegalArgumentException.class,
() -> {
loc.getOffsetInApicalFrame2D(badOffset, apicalAxis);
});
}

@Test
public void getOffsetInApicalFrame2D_nonOrthogonalAxis_returnsExpected() {
ArrayList<Voxel> 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<Integer> offsets = new ArrayList<>();
offsets.add(0); // lowest orthogonal axis
offsets.add(100); // farthest along apical

Voxel result = loc.getOffsetInApicalFrame2D(offsets, apicalAxis);
assertEquals(new Voxel(3, 3, 0), result);
}
}
Loading