001 package edu.nrao.sss.model.resource.evla; 002 003 import edu.nrao.sss.measure.ArcUnits; 004 import edu.nrao.sss.measure.Angle; 005 import edu.nrao.sss.measure.Latitude; 006 import edu.nrao.sss.measure.Longitude; 007 import edu.nrao.sss.measure.TimeDuration; 008 import edu.nrao.sss.measure.TimeUnits; 009 import edu.nrao.sss.model.project.scan.AntennaWrap; 010 011 import java.math.BigDecimal; 012 import java.util.ArrayList; 013 import java.util.List; 014 015 import org.apache.log4j.Logger; 016 017 /** 018 * TODO: Verify ALL constants returned by this class!! 019 */ 020 public class EvlaTelescopeMotionSimulator 021 { 022 private static final Logger log = Logger.getLogger(EvlaTelescopeMotionSimulator.class); 023 024 private static final Angle MIN_AZ = new Angle("-85.0"); 025 private static final Angle MAX_AZ = new Angle("445.0"); 026 private static final Angle MIN_EL = new Angle( "8.0"); 027 private static final Angle MAX_EL = new Angle("125.0"); 028 029 private static final Angle MIN_AZ_plus_360 = MIN_AZ.clone().add("360.0"); 030 private static final Angle MAX_AZ_less_360 = MAX_AZ.clone().subtract("360.0"); 031 032 //Assume deg/s for rates and deg/s/s for acc. 033 private static final double VEL_AZ = 40.0/60; 034 private static final double VEL_EL = 20.0/60; 035 036 private static final double ACC_AZ = 1000; 037 private static final double ACC_EL = 1000; 038 039 private static final TimeDuration MIN_SETUP_TIME = 040 new TimeDuration("0.0", TimeUnits.SECOND); 041 042 private static final TimeDuration SETTLING_TIME = 043 new TimeDuration("7.0", TimeUnits.SECOND); 044 045 //Default currentAz/El to midpoint of their ranges. 046 private Angle currentAz = new Angle("225.0"); 047 private Angle currentEl = new Angle( "35.0"); 048 049 //These variables keep track of what the new Az/El will be set to during the 050 //moveTime calculation 051 private Angle newAz = null; 052 private Angle newEl = null; 053 054 private List<Error> errors = new ArrayList<Error>(); 055 056 /** 057 * Creates a EvlaTelescopeMotionSimulator 058 */ 059 public EvlaTelescopeMotionSimulator() {} 060 061 /** 062 * Creates a EvlaTelescopeMotionSimulator with an initial position of {@code 063 * startAz} and {@code startEl}. 064 * 065 * @throws IllegalArgumentException if startAz or startEl are out of range. 066 */ 067 public EvlaTelescopeMotionSimulator(Angle startAz, Angle startEl) 068 { 069 setCurrentAntennaAzimuth(startAz); 070 setCurrentAntennaElevation(startEl); 071 } 072 073 /** 074 * Creates a EvlaTelescopeMotionSimulator with an initial position of {@code 075 * startAz} and {@code startEl} that is on wrap {@code wrap}. 076 * 077 * @throws IllegalArgumentException if startEl is out of range. 078 */ 079 public EvlaTelescopeMotionSimulator(Longitude startAz, Latitude startEl, AntennaWrap wrap) 080 { 081 setCurrentAntennaAzimuth(startAz, wrap); 082 setCurrentAntennaElevation(startEl); 083 } 084 085 /** 086 * Returns the estimated move time from our current saved position to {@code 087 * az, el}. For the very first calculation, a starting position due south at 088 * 65 degrees elevation is assumed. The method takes into account antenna 089 * wrap by calculating 4 different move times and returning the smallest 090 * <em>valid</em> move time. The 4 cases are as follows: 091 * 092 * <ol> 093 * <li> 094 * The position we're moving to is on the COUNTERCLOCKWISE (left) wrap. We 095 * simple rotate the antenna to that position and see how long it takes. 096 * This is a valid option only if {@code wrap} is not CLOCKWISE and {@code 097 * goOverTheTop} is false. 098 * </li> 099 * <li> 100 * The position is on the CLOCKWISE (right) wrap. Again, we calculate how 101 * long it will take to rotate the antenna to that position on that wrap. 102 * This is a valid option only if {@code wrap} is not COUNTERCLOCKWISE and 103 * {@code goOverTheTop} is false. 104 * </li> 105 * <li> 106 * We rotate the antenna 180 degrees and then go over the top to get to our 107 * target position. (i.e. we're subtracting 180 degrees from the AZ in case 108 * 1 above.) This is only valid if {@code goOverTheTop} is true and the 109 * elevation is greater than 55 degrees (180 - MAX_EL). 110 * </li> 111 * <li> 112 * The same as 3 above, but we <em>add</em> 180 to the az instead of 113 * subtracting. 114 * </li> 115 * </ol> 116 * 117 * This method also clears and refills the list of errors encountered while 118 * attempting this move. 119 */ 120 public TimeDuration moveTo(Longitude azStart, Latitude elStart, 121 Longitude azEnd, Latitude elEnd, 122 AntennaWrap wrap, boolean goOverTheTop) 123 { 124 this.newAz = null; 125 this.newEl = null; 126 this.errors.clear(); 127 128 //Initialize min to something large say, 1 day. 129 TimeDuration min = new TimeDuration("24.0"); 130 131 //Initialize wrap to a default if it was null 132 AntennaWrap w = (wrap == null)? AntennaWrap.NO_PREFERENCE : wrap; 133 134 Angle toAz = azStart.toAngle().convertTo(ArcUnits.DEGREE); 135 Angle toAzEnd = azEnd.toAngle().convertTo(ArcUnits.DEGREE); 136 137 //Put the toAz on the left or COUNTERCLOCKWISE wrap. 138 //if toAz > minaz + 360: toAz -= 360; 139 if (toAz.compareTo(MIN_AZ_plus_360) > 0) 140 toAz.subtract("360.0"); 141 142 // Make sure the end position is on the same wrap! This is checked 143 // separately from the above because we aren't guaranteed that just because 144 // we need to subtract 360 from the start pos that we need to do so to the 145 // end as well. This is mainly a concern for scans that cross the 0d/360d 146 // boundary. 147 if (toAzEnd.compareTo(MIN_AZ_plus_360) > 0) 148 toAzEnd.subtract("360.0"); 149 150 Angle toEl = elStart.toAngle().convertTo(ArcUnits.DEGREE); 151 Angle toElEnd = elEnd.toAngle().convertTo(ArcUnits.DEGREE); 152 153 //log.debug("Moving : (" + toAz + ", " + toEl + ") : (" + toAzEnd + ", " + toElEnd + ")"); 154 155 //Whereas Longitude is always gaurunteed to be in bounds of our motion, 156 //Latitude (elevation) is not, so we have to check that here before going 157 //any further. We only check the min, because the max a Latitude can hold 158 //is 90 and our MAX_EL is 125 or so. 159 if (toEl.compareTo(MIN_EL) < 0) 160 { 161 this.errors.add(Error.ELEVATION_OUT_OF_RANGE); 162 toEl = MIN_EL.clone(); 163 } 164 165 // We need to do the same check for the elevation END position too! 166 if (toElEnd.compareTo(MIN_EL) < 0) 167 { 168 this.errors.add(Error.SCAN_END_ELEVATION_OUT_OF_RANGE); 169 toElEnd = MIN_EL.clone(); 170 } 171 172 // This checks to make sure that the if the user selected a CLOCKWISE 173 // preference, but the source is in the region where the wraps do NOT 174 // overlap, we still get a valid calculation. The reason this comes up is 175 // because for much of the logic below we're considering a wrap to be a 176 // full 360 degrees when it is only actually 265. So, a source could be in 177 // the CLOCKWISE wrap (180 - 445 degrees) but be missed in the logic below 178 // if it's position is less than 275 degrees. (which is within the range 179 // (-85, -85 + 360) 180 if (AntennaWrap.CLOCKWISE.equals(w) && toAz.compareTo(new Angle("85.0")) > 0) 181 w = AntennaWrap.NO_PREFERENCE; 182 183 184 //tmp variables for calculations 185 Angle tmpAz = toAz.clone(); 186 Angle tmpAzEnd = toAzEnd.clone(); 187 Angle tmpEl = toEl.clone(); 188 Angle tmpElEnd = toElEnd.clone(); 189 190 if (!AntennaWrap.CLOCKWISE.equals(w)) 191 { 192 //Case 3: 193 if (goOverTheTop) 194 { 195 tmpAz = toAz.clone().subtract("180.0"); 196 tmpAzEnd = toAzEnd.clone().subtract("180.0"); 197 tmpEl = new Angle("180.0").subtract(toEl); 198 tmpElEnd = new Angle("180.0").subtract(toElEnd); 199 200 if (tmpAz.compareTo(MIN_AZ) >= 0 && tmpEl.compareTo(MAX_EL) <= 0) 201 { 202 if (updateMin(min, tmpAz, tmpEl)) 203 { 204 this.newAz = tmpAzEnd; 205 this.newEl = tmpElEnd; 206 } 207 } 208 } 209 210 //Case 1: 211 else 212 { 213 if (updateMin(min, tmpAz, tmpEl)) 214 { 215 this.newAz = tmpAzEnd; 216 this.newEl = tmpElEnd; 217 //log.debug("Case 1 min = " + min); 218 } 219 } 220 } 221 222 if (!AntennaWrap.COUNTERCLOCKWISE.equals(w)) 223 { 224 //Case 4: 225 if (goOverTheTop) 226 { 227 tmpAz = toAz.clone().add("180.0"); 228 tmpEl = new Angle("180.0").subtract(toEl); 229 tmpAzEnd = toAzEnd.clone().add("180.0"); 230 tmpElEnd = new Angle("180.0").subtract(toElEnd); 231 232 if (tmpAz.compareTo(MAX_AZ) <= 0 && tmpEl.compareTo(MAX_EL) <= 0) 233 { 234 if (updateMin(min, tmpAz, tmpEl)) 235 { 236 this.newAz = tmpAzEnd; 237 this.newEl = tmpElEnd; 238 } 239 } 240 } 241 242 //Case 2: 243 else 244 { 245 tmpAz = toAz.clone().add("360.0"); 246 tmpAzEnd = toAzEnd.clone().add("360.0"); 247 248 tmpEl = toEl.clone(); 249 tmpElEnd = toElEnd.clone(); 250 251 if (tmpAz.compareTo(MAX_AZ) < 0) 252 { 253 if (updateMin(min, tmpAz, tmpEl)) 254 { 255 this.newAz = tmpAzEnd; 256 this.newEl = tmpElEnd; 257 //log.debug("Case 2 min = " + min); 258 } 259 } 260 } 261 } 262 263 if (this.newAz == null || this.newEl == null) 264 { 265 this.errors.add(Error.MOVE_NOT_POSSIBLE); 266 } 267 268 else 269 { 270 if (MIN_AZ.compareTo(this.newAz) > 0) 271 { 272 //log.debug("newAz < min: " + this.newAz); 273 this.errors.add(Error.SCAN_END_AZIMUTH_OUT_OF_RANGE); 274 this.currentAz = MIN_AZ.clone(); 275 } 276 277 else if (MAX_AZ.compareTo(this.newAz) < 0) 278 { 279 //log.debug("newAz > max: " + this.newAz); 280 this.errors.add(Error.SCAN_END_AZIMUTH_OUT_OF_RANGE); 281 this.currentAz = MAX_AZ.clone(); 282 } 283 284 else 285 this.currentAz = this.newAz; 286 287 if (MIN_EL.compareTo(this.newEl) > 0) 288 { 289 this.errors.add(Error.SCAN_END_ELEVATION_OUT_OF_RANGE); 290 this.currentEl = MIN_EL.clone(); 291 } 292 293 else if (MAX_EL.compareTo(this.newEl) < 0) 294 { 295 this.errors.add(Error.SCAN_END_ELEVATION_OUT_OF_RANGE); 296 this.currentEl = MAX_EL.clone(); 297 } 298 299 else 300 this.currentEl = this.newEl; 301 302 //Add any settling time necessary. 303 min.add(SETTLING_TIME); 304 305 //If the slew took less time than the min. time it takes to prepare the 306 //antenna for observing, use that min. instead. 307 if (min.compareTo(MIN_SETUP_TIME) < 0) 308 min = MIN_SETUP_TIME.clone(); 309 } 310 311 return min; 312 } 313 314 /** 315 * Returns a list of any errrors that may have occured during the lastest 316 * call to the {@code moveTo} method. 317 */ 318 public List<Error> getErrors() 319 { 320 return new ArrayList<Error>(this.errors); 321 } 322 323 /** 324 * Returns which AntennaWrap this simulator is currently on using the 325 * following rules. If the currentAz is greater than 275 degrees (MIN_AZ + 326 * 360) then we're in the CLOCKWISE (right) wrap. Otherwise we're in the 327 * COUNTERCLOCKWISE (left) wrap. XXX This could be changed to split the 328 * range in half: greater than 180 degrees is CLOCKWISE, the rest is 329 * COUNTERCLOCKWISE. As it is now, you are only in the CLOCKWISE wrap if you 330 * are in the overlapping region. 331 */ 332 public AntennaWrap getCurrentAntennaWrap() 333 { 334 return (currentAz.compareTo(new Angle("180.0")) > 0)? 335 AntennaWrap.CLOCKWISE : 336 AntennaWrap.COUNTERCLOCKWISE; 337 } 338 339 /** -85 to 445 degrees */ 340 public Angle getCurrentAntennaAzimuth() 341 { 342 return this.currentAz.clone(); 343 } 344 345 /** sets the current Antenna Az to a clone of {@code a} if a is within range. */ 346 public void setCurrentAntennaAzimuth(Angle a) 347 { 348 if (a != null && MIN_AZ.compareTo(a) <= 0 && MAX_AZ.compareTo(a) >= 0) 349 this.currentAz = a.clone().convertTo(ArcUnits.DEGREE); 350 351 else 352 throw new IllegalArgumentException("Invalid Antenna Azimuth: " + a); 353 } 354 355 /** sets the current Antenna Az to a Angle equivalent to {@code a} at wrap {@code w}. */ 356 public void setCurrentAntennaAzimuth(Longitude a, AntennaWrap w) 357 { 358 setCurrentAntennaAzimuth(toAntennaAzimuth(a, w)); 359 } 360 361 /** 8 to 125 degrees */ 362 public Angle getCurrentAntennaElevation() 363 { 364 return this.currentEl.clone(); 365 } 366 367 /** sets the current Antenna El to a clone of {@code a} if a is within range. */ 368 public void setCurrentAntennaElevation(Angle a) 369 { 370 if (a != null && MIN_EL.compareTo(a) <= 0 && MAX_EL.compareTo(a) >= 0) 371 this.currentEl = a.clone().convertTo(ArcUnits.DEGREE); 372 373 else 374 throw new IllegalArgumentException("Invalid Antenna Elevation: " + a); 375 } 376 377 /** sets the current Antenna Az to a Angle equivalent to {@code a} at wrap {@code w}. */ 378 public void setCurrentAntennaElevation(Latitude a) 379 { 380 setCurrentAntennaElevation(a.toAngle().convertTo(ArcUnits.DEGREE)); 381 } 382 383 /** 384 * Returns an Angle in degrees between -85 and 445 degrees that represents 385 * {@code az} at AntennaWrap {@code w}. 386 */ 387 public static Angle toAntennaAzimuth(Longitude az, AntennaWrap w) 388 { 389 Angle antennaAz = null; 390 if (az != null) 391 { 392 Angle a = az.toAngle().convertTo(ArcUnits.DEGREE); 393 394 //az is 0 - 360 degrees 395 switch (w) 396 { 397 //180 to 445 degrees 398 case CLOCKWISE: 399 //if a < (maxaz - 360): a += 360; 400 if (a.compareTo(MAX_AZ_less_360) < 0) 401 a.add("360.0"); 402 403 antennaAz = a; 404 break; 405 406 //-85 to 180 degrees 407 case COUNTERCLOCKWISE: 408 //if a > minaz + 360: a -= 360; 409 if (a.compareTo(MIN_AZ_plus_360) > 0) 410 a.subtract("360.0"); 411 412 antennaAz = a; 413 break; 414 415 default: 416 } 417 } 418 419 return antennaAz; 420 } 421 422 /** Returns the minimum azimuth value for EVLA antenna pointings. */ 423 public static Angle getAzimuthMinimum() 424 { 425 return MIN_AZ.clone(); 426 } 427 428 /** Returns the maximum azimuth value for EVLA antenna pointings. */ 429 public static Angle getAzimuthMaximum() 430 { 431 return MAX_AZ.clone(); 432 } 433 434 /** Returns the default azimuth value for EVLA antenna pointings. */ 435 public static Angle getAzimuthDefault() 436 { 437 return new Angle("225.0"); 438 } 439 440 /** Returns the minimum elevation value for EVLA antenna pointings. */ 441 public static Angle getElevationMinimum() 442 { 443 return MIN_EL.clone(); 444 } 445 446 /** Returns the maximum elevation value for EVLA antenna pointings. */ 447 public static Angle getElevationMaximum() 448 { 449 return MAX_EL.clone(); 450 } 451 452 /** Returns the default elevation value for EVLA antenna pointings. */ 453 public static Angle getElevationDefault() 454 { 455 return new Angle("35.0"); 456 } 457 458 /** 459 * This method updates the passed in {@code currentMin} variable with the 460 * minimum value of currentMin, the time it takes to move from currentAz to 461 * az, and the time it takes to move from currentEl to el. If the currentMin 462 * is changed, we return true. 463 */ 464 private boolean updateMin(TimeDuration currentMin, Angle az, Angle el) 465 { 466 //log.debug("updateMin: (" + az + ", " + el + ") " + currentMin); 467 TimeDuration taz = calcAzMoveTime(currentAz, az); 468 TimeDuration tel = calcElMoveTime(currentEl, el); 469 470 //t is the larger of taz and tel 471 TimeDuration t = (taz.compareTo(tel) > 0)? taz : tel; 472 473 //if currentMin is > t 474 if (currentMin.compareTo(t) > 0) 475 { 476 currentMin.set(t); 477 return true; 478 } 479 480 return false; 481 } 482 483 /** 484 * calculates the move time from angle {@code f} to angle {@code t} in 485 * Azimuth. 486 */ 487 private TimeDuration calcAzMoveTime(Angle f, Angle t) 488 { 489 double azd = t.toUnits(ArcUnits.DEGREE).subtract( 490 f.toUnits(ArcUnits.DEGREE)).abs().doubleValue(); 491 492 //Time it takes to reach full speed 493 //Accounts for both acceleration & decceleration. 494 double timeAccAz = 2 * VEL_AZ / ACC_AZ; 495 496 //Distance it takes to reach full speed 497 double distAccAz = (VEL_AZ * VEL_AZ) / ACC_AZ; 498 499 double telSlewAz; 500 501 //If the antenna never reaches full speed, use this equation. 502 if (azd < distAccAz) 503 { 504 telSlewAz = 2 * Math.sqrt(azd / ACC_AZ); 505 } 506 507 //otherwise, use this equation. 508 else 509 { 510 telSlewAz = timeAccAz + (azd - distAccAz) / VEL_AZ; 511 } 512 513 return new TimeDuration(BigDecimal.valueOf(telSlewAz), TimeUnits.SECOND); 514 } 515 516 /** 517 * calculates the move time from angle {@code f} to angle {@code t} in 518 * Elevation. 519 */ 520 private TimeDuration calcElMoveTime(Angle f, Angle t) 521 { 522 double eld = t.toUnits(ArcUnits.DEGREE).subtract( 523 f.toUnits(ArcUnits.DEGREE)).abs().doubleValue(); 524 525 //Time it takes to reach full speed 526 //Accounts for both acceleration & decceleration. 527 double timeAccEl = 2 * VEL_EL / ACC_EL; 528 529 //Distance it takes to reach full speed 530 double distAccEl = (VEL_EL * VEL_EL) / ACC_EL; 531 532 double telSlewEl; 533 534 //If the antenna never reaches full speed, use this equation. 535 if (eld < distAccEl) 536 { 537 telSlewEl = 2 * Math.sqrt(eld / ACC_EL); 538 } 539 540 //otherwise, use this equation. 541 else 542 { 543 telSlewEl = timeAccEl + (eld - distAccEl) / VEL_EL; 544 } 545 546 return new TimeDuration(BigDecimal.valueOf(telSlewEl), TimeUnits.SECOND); 547 } 548 549 public static enum Error 550 { 551 INVALID_ANTENNA_WRAP_REQUESTED, 552 ELEVATION_OUT_OF_RANGE, 553 SCAN_END_ELEVATION_OUT_OF_RANGE, 554 SCAN_END_AZIMUTH_OUT_OF_RANGE, 555 MOVE_NOT_POSSIBLE; 556 } 557 }