/* Copyright 2006 by Sean Luke and George Mason University Licensed under the Academic Free License version 3.0 See the file "LICENSE" for more information */ package sim.app.mousetraps; import sim.util.*; import sim.engine.*; import sim.field.continuous.Continuous3D; import sim.field.grid.*; import ec.util.*; public class MouseTraps extends SimState { private static final long serialVersionUID = 1; /** the number of balls a trap will throw in the air when triggered */ public static final int BALLS_PER_TRAP = 2; /** the initial velocity of a ball when thrown by a trap * I was going to model trap's dispensed energy * E = 0.5 * BALLS_PER_TRAP * ball's_weight * INITIAL_VELOCITY^2 * But since all are contants, i skip the math and * set INITIAL_VELOCITY and save some math, * without loss in generality */ public final double initialVelocity; public static final double GRAVITY_ACC = 9.8; public static final double TWO_OVER_G = 2.0/GRAVITY_ACC; public static final double TIME_STEP_DURATION = 1.0/64; public static final double TIME_STEP_FREQUENCY = 1.0/TIME_STEP_DURATION; public static final double TWO_PI = Math.PI * 2.0; public static final double HALF_PI = Math.PI * 0.5; public final boolean toroidalWorld; public final boolean modelBalls; /** @todo handle realocation of grids when these two are changed */ public final int trapGridHeight; public final int trapGridWidth; public final double spaceWidth, spaceHeight, spaceLength; public final double oneOverSpaceWidth, oneOverSpaceHeight, oneOverSpaceLength; public final double trapSizeX, trapSizeY; public IntGrid2D trapStateGrid; public Continuous3D ballSpace; public static final int ARMED_TRAP = 0; public static final int OFF_TRAP = 1; /** Creates a HeatBugs simulation with the given random number seed. */ public MouseTraps(long seed) { this(seed, 0.7, 100, 100, true); } public MouseTraps(long seed, double initialVelocity, int width, int height, boolean toroidal) { super(seed); this.initialVelocity = initialVelocity; toroidalWorld = toroidal; modelBalls = false; trapGridWidth = width; trapGridHeight = height; spaceWidth = 1; spaceHeight = 1; spaceLength = 1; createGrids(); trapSizeX = spaceWidth/trapGridWidth; trapSizeY = spaceHeight/trapGridHeight; oneOverSpaceHeight = 1.0/spaceHeight; oneOverSpaceWidth = 1.0/spaceWidth; oneOverSpaceLength = 1.0/spaceLength; } public MouseTraps( long seed, double initialVelocity, int trapsX, int trapsY, double width, double height, boolean toroidal) { super(seed); this.initialVelocity = initialVelocity; toroidalWorld = toroidal; trapGridWidth = trapsX; trapGridHeight = trapsY; modelBalls = true; spaceWidth = width; spaceHeight = height; spaceLength = computeFishTankCeiling(); createGrids(); trapSizeX = spaceWidth/trapGridWidth; trapSizeY = spaceHeight/trapGridHeight; oneOverSpaceHeight = 1.0/spaceHeight; oneOverSpaceWidth = 1.0/spaceWidth; oneOverSpaceLength = 1.0/spaceLength; } /** computes how high should be the ceiling of the * fishtank so the balls don't hit it., even if they * are shot straight up. * * Y = V0T-.5gT^2 //y0=0 * V = V0-gT * => Ttop = V0/g * => Ytop = .5V0^2/g; * * or in one line using energy conservation * */ public double computeFishTankCeiling() { return 0.5 * initialVelocity * initialVelocity / GRAVITY_ACC; } void createGrids() { trapStateGrid = new IntGrid2D(trapGridWidth, trapGridHeight,ARMED_TRAP); if(modelBalls) ballSpace = new Continuous3D(Math.max(trapGridHeight, trapGridWidth)*2, spaceWidth, spaceHeight, spaceLength); } /** * determines on what trap a certain location belongs to * @param position = distance in continuous space. * @return [0..width-1] trap index * * note: boundary between traps belong to the one in the right, * * if space is toroidal, wrap "distance" around the space using % * if is not, the ball would bounce off the wall => * - it is like it did not bounce, but it entered a mirrored grid * (this explains trapGridWidth-i) * - after two bounces everything is back, so I wrap the distance around 2 grids * (this explains i %= (2*trapGridWidth)) */ public int discretizeX(double position) { int i =(int)(position* oneOverSpaceWidth* trapGridWidth); if(toroidalWorld) return (i+trapGridWidth) % trapGridWidth; i+=2*trapGridWidth; i %= (2*trapGridWidth); if( i<trapGridWidth) return i; return trapGridWidth-i; } /** * determines on what trap a certain location belongs to * @param position = distance in continuous space. * @return [0..height-1] trap index * * @see scretizeX */ public int discretizeY(double position) { int i =(int)(position* oneOverSpaceHeight* trapGridHeight); if(toroidalWorld) return (i+trapGridHeight) % trapGridHeight; i+=2*trapGridHeight; i %= (2*trapGridHeight); if( i<trapGridHeight) return i; return trapGridHeight-i; } public int discretizeX(double offset, int location) { return discretizeX(offset+(0.5+location)*trapSizeX); //offset is measured from the center of the trap } public int discretizeY(double offset, int location) { return discretizeY(offset+(0.5+location)*trapSizeY); //offset is measured from the center of the trap } public double trapPosX(int x) { return (0.5+x)*trapSizeX; } public double trapPosY(int y) { return (0.5+y)*trapSizeY; } public void triggerTrap(int posx, int posy) { if(trapStateGrid.get(posx, posy)== OFF_TRAP) return; trapStateGrid.set(posx, posy, OFF_TRAP); double spacePosX = (0.5+posx)*trapSizeX; double spacePosY = (0.5+posy)*trapSizeY; for(int i=0; i< MouseTraps.BALLS_PER_TRAP; i++) { //decide the componnents of initial speed. /** * http://www.phy6.org/stargaze/Scelcoor.htm * * The angle f is measured in a horizontal plane, is known as azimuth and is measured from the * north direction. A rotating table allows the telescope to be pointed in any azimuth. * * The angle l is called elevation and is the angle by which the telescope is lifted above the * horizontal (if it looks down, l is negative). The two angles together can in principle specify * any direction: f ranges from 0 to 360, and l from -90 (straight down or "nadir") to +90 * (straight up or "zenith"). * * Again, one needs to decide from what direction is the azimuth measured--that is, where is azimuth * zero? The rotation of the heavens (and the fact most humanity lives north of the equator) suggests * (for surveyor-type measurements) the northward direction, and this is indeed the usual zero point. * The azimuth angle (viewed from the north) is measured counterclockwise. */ double azimuth, elevation; azimuth = random.nextDouble()*TWO_PI; elevation = random.nextDouble()*HALF_PI;//balls are thrown UP double cos_elevation = Math.cos(elevation); double sin_elevation = Math.sqrt(1 - cos_elevation*cos_elevation); //[sin(elevation) is positive, anyway] double cos_azimuth = Math.cos(azimuth); double sin_azimuth = Math.sin(azimuth); //[sin(azimiuth] is not always positive, so the sin(elevation) trick does not hold double vz = initialVelocity * sin_elevation; double vxy = initialVelocity * cos_elevation; double vx = vxy * cos_azimuth; double vy = vxy * sin_azimuth; if(! modelBalls) { double landing_time = vz * TWO_OVER_G; double landing_dx = vx* landing_time; double landing_dy = vy* landing_time; schedule.scheduleOnce( schedule.getTime()+landing_time , new MouseTrap(discretizeX(landing_dx, posx),discretizeY(landing_dy, posy))); } else { Ball b = new Ball(spacePosX,spacePosY,0.0, vx, vy, vz); ballSpace.setObjectLocation(b,new Double3D( spacePosX,spacePosY,0)); schedule.scheduleOnce(schedule.getTime()+1, b); } } } /** Resets and starts a simulation */ public void start() { super.start(); // clear out the schedule // make new grids createGrids(); int posx = trapGridWidth/2; int posy = trapGridHeight/2; if(modelBalls) { double x = (0.5+posx)*trapSizeX; double y = (0.5+posy)*trapSizeY; double z = computeFishTankCeiling(); Ball b = new Ball(x,y,z, 0.0, 0.0, 0.0); ballSpace.setObjectLocation(b,new Double3D( x,y,z)); schedule.scheduleOnce(Schedule.EPOCH, b); } else schedule.scheduleOnce(initialVelocity* GRAVITY_ACC, new MouseTrap(posx, posy)); } public static void main(String[] args) { doLoop(MouseTraps.class, args); System.exit(0); } }