All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ucar.nc2.ft.fmrc.FmrcInv Maven / Gradle / Ivy

Go to download

The NetCDF-Java Library is a Java interface to NetCDF files, as well as to many other types of scientific data formats.

The newest version!
/*
 * Copyright (c) 1998 - 2010. University Corporation for Atmospheric Research/Unidata
 * Portions of this software were developed by the Unidata Program at the
 * University Corporation for Atmospheric Research.
 *
 * Access and use of this software shall impose the following obligations
 * and understandings on the user. The user is granted the right, without
 * any fee or cost, to use, copy, modify, alter, enhance and distribute
 * this software, and any derivative works thereof, and its supporting
 * documentation for any purpose whatsoever, provided that this entire
 * notice appears in all copies of the software, derivative works and
 * supporting documentation.  Further, UCAR requests that the user credit
 * UCAR/Unidata in any publications that result from the use of this
 * software or in any product that includes this software. The names UCAR
 * and/or Unidata, however, may not be used in any advertising or publicity
 * to endorse or promote any products or commercial entity unless specific
 * written permission is obtained from UCAR/Unidata. The user also
 * understands that UCAR/Unidata is not obligated to provide the user with
 * any support, consulting, training or assistance of any kind with regard
 * to the use, operation and performance of this software nor to provide
 * the user with any updates, revisions, new versions or "bug fixes."
 *
 * THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL,
 * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package ucar.nc2.ft.fmrc;

import net.jcip.annotations.Immutable;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.time.CalendarPeriod;

import java.util.*;

/**
 * Inventory for a Forecast Model Run Collection = a series of Forecast Model Runs.
 *
 * Create rectangular representation of var(runtime, time) of data(ens, vert, x, y).
 * For each Grid, the vert, time and ens coordinates are created as the union of the components.
 * Make sure to share coordinates across grids where they are equivilent.
 * 

* We are thus making a rectangular array var(runtime, time, ens, level). * So obviously we have to tolerate missing data. * Keeps track of what inventory exists, and where it is. *

* * @author caron * @since Jan 11, 2010 */ @Immutable public class FmrcInv { static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FmrcInv.class); /* public static CalendarDate addHour(CalendarDate d, double hour) { long msecs = d.getTime(); msecs += hour * 3600 * 1000; return new Date(msecs); } */ ////////////////////////////////////////////////////////////////////////////////////////////////////// private final String name; // name of ForecastModelRunCollection // this is where the time coordinates are normalized and missing data may occur private final List runSeqs = new ArrayList(); // list of unique EnsCoord private final List ensCoords = new ArrayList(); // list of unique VertCoord private final List vertCoords = new ArrayList(); // the list of runs private final List fmrList; // the list of grids private final List uberGridList; // sorted list of UberGrid // all run times private final List runTimeList; // sorted list of Date: all run times // track offsets and bounds private final CalendarDate baseDate; // first runtime : offsetsAll calculated from here // all forecast times private final List forecastTimeList; // sorted list of Date : all forecast times private Calendar cal; // use on motherlode to regularize the missing inventory private final boolean regularize; ///////////////////////////////////////////////////// /** * Construct the inventory from a set of FmrInv, one for each model run. * * @param name name of collection * @param fmrList the component runs FmrInv * @param regularize regularize time coords based on offset from 0Z */ FmrcInv(String name, List fmrList, boolean regularize) { this.name = name; this.regularize = regularize; this.fmrList = new ArrayList(fmrList); runTimeList = new ArrayList(); CalendarDate firstDate = null; Map uvHash = new HashMap(); Set offsetHash = new HashSet(); Set intervalHash = new HashSet(); Set forecastTimeHash = new HashSet(); for (FmrInv fmrInv : fmrList) { runTimeList.add(fmrInv.getRunDate()); if (firstDate == null) firstDate = fmrInv.getRunDate(); // hour of this runDate int hour = fmrInv.getRunDate().getHourOfDay(); // for each GridVariable, add to the UberGrid for (FmrInv.GridVariable fmrGrid : fmrInv.getGrids()) { UberGrid uv = uvHash.get(fmrGrid.getName()); if (uv == null) { uv = new UberGrid(fmrGrid.getName()); uvHash.put(fmrGrid.getName(), uv); } uv.addGridVariable(fmrGrid, hour); } // track overall list of times, offsets, and intervals for (TimeCoord tc : fmrInv.getTimeCoords()) { if (tc.isInterval()) { double[] bounds1 = tc.getBound1(); double[] bounds2 = tc.getBound2(); for (int i=0; i(uvHash.values()); Collections.sort(uberGridList); for (UberGrid uv : uberGridList) { uv.finish(); } // assign sequence ids int seqno = 0; for (RunSeq seq : runSeqs) { seq.id = seqno++; } // create the overall list of forecast times forecastTimeList = Arrays.asList((CalendarDate[]) forecastTimeHash.toArray(new CalendarDate[forecastTimeHash.size()])); Collections.sort(forecastTimeList); // create the overall list of offsets - may be zero List offsetsAll = Arrays.asList((Double[]) offsetHash.toArray(new Double[offsetHash.size()])); Collections.sort(offsetsAll); int counto = 0; double[] offs = new double[offsetsAll.size()]; for (double off : offsetsAll) offs[counto++] = off; tcOffAll = new TimeCoord(baseDate); tcOffAll.setOffsetTimes(offs); // create the overall list of offsets - may be zero List intervalAll = Arrays.asList((TimeCoord.Tinv[]) intervalHash.toArray(new TimeCoord.Tinv[intervalHash.size()])); Collections.sort(intervalAll); tcIntAll = new TimeCoord(baseDate); tcIntAll.setBounds(intervalAll); } // not really needed private final TimeCoord tcOffAll; // all offsets in this collection, reletive to baseDate private final TimeCoord tcIntAll; // all intervals in this collection, reletive to baseDate // public for debugging public List getFmrList() { return fmrList; } /* private UberGrid findVar(String varName) { for (UberGrid uv : uberGridList) { if (uv.gridName.equals(varName)) return uv; } return null; } public int findFmrIndex(Date runDate) { for (int i = 0; i < fmrList.size(); i++) { FmrInv fmr = fmrList.get(i); if (fmr.getRunDate().equals(runDate)) return i; } return -1; } public List getRunTimes() { return runTimeList; } */ public String getName() { return name; } public List getRunSeqs() { return runSeqs; } public List getEnsCoords() { return ensCoords; } public List getVertCoords() { return vertCoords; } public List getUberGrids() { return uberGridList; } public UberGrid findUberGrid(String name) { for (UberGrid grid : uberGridList) if (grid.getName().equals(name)) return grid; return null; } public List getForecastTimes() { return forecastTimeList; } public List getFmrInv() { return fmrList; } public CalendarDate getBaseDate() { return baseDate; } /* public TimeCoord getTimeCoordsAll() { return tcAll; } */ /** * Find the difference between two dates in hours * @param base date1 * @param forecast date2 * @return (forecast minus base) difference in hours */ public static double getOffsetInHours(CalendarDate base, CalendarDate forecast) { long diff = forecast.getDifferenceInMsecs(base); return diff / 1000.0 / 60.0 / 60.0; } /** * Create a date from base and hour offset * @param base base date * @param offset hourss * @return base + offset as a Date */ public static CalendarDate makeOffsetDate(CalendarDate base, double offset) { return base.add(offset, CalendarPeriod.Field.Hour); } //////////////////////////////////////////////////////////////////// // The collection of GridVariable for one variable, across all runs in the collection // immutable after finish() is called. public class UberGrid implements Comparable { private final String gridName; private final List runs = new ArrayList(); private VertCoord vertCoordUnion = null; private EnsCoord ensCoordUnion = null; private RunSeq runSeq = null; UberGrid(String name) { this.gridName = name; } void addGridVariable(FmrInv.GridVariable grid, int hour) { runs.add(grid); } public String getName() { return gridName; } public String toString() { return gridName; } // the union of all offset hours, ignoring rundate public TimeCoord getUnionTimeCoord() { return runSeq.getUnionTimeCoord(); } public boolean isInterval() { return getUnionTimeCoord().isInterval(); } public String getTimeCoordName() { return runSeq.getName(); } public String getVertCoordName() { return (vertCoordUnion == null) ? "" : vertCoordUnion.getName(); } public List getRuns() { return runs; } public int compareTo(UberGrid o) { return gridName.compareTo(o.gridName); } public int countTotal() { int total = 0; for (FmrInv.GridVariable grid : runs) total += grid.countTotal(); return total; } public int countExpected() { int nvert = (vertCoordUnion == null) ? 1 : vertCoordUnion.getSize(); int ntimes = 0; for (FmrInv.GridVariable grid : runs) { // maybe should use the runseq ??? TimeCoord exp = grid.getTimeExpected(); ntimes += exp.getNCoords(); } return ntimes * nvert; } void finish() { if (runs.size() == 1) { FmrInv.GridVariable grid = runs.get(0); ensCoordUnion = EnsCoord.findEnsCoord(getEnsCoords(), grid.ensCoordUnion); vertCoordUnion = VertCoord.findVertCoord(getVertCoords(), grid.vertCoordUnion); //timeCoordUnion = TimeCoord.findTimeCoord(getTimeCoords(), grid.timeCoordUnion); //timeCoordUnion.addGridVariable(this); grid.timeExpected = grid.timeCoordUnion; // if only one, not much else to do this.runSeq = findRunSeq(runs); this.runSeq.addVariable(this); return; } // run over all ensCoords and construct the union List ensList = new ArrayList(); EnsCoord ec_union = null; for (FmrInv.GridVariable grid : runs) { EnsCoord ec = grid.ensCoordUnion; if (ec == null) continue; if (ec_union == null) ec_union = new EnsCoord(ec); else if (!ec_union.equalsData(ec)) ensList.add(ec); } if (ec_union != null) { if (ensList.size() > 0) EnsCoord.normalize(ec_union, ensList); // add the other coords ensCoordUnion = EnsCoord.findEnsCoord(getEnsCoords(), ec_union); // find unique within collection } // run over all vertCoords and construct the union List vertList = new ArrayList(); VertCoord vc_union = null; for (FmrInv.GridVariable grid : runs) { VertCoord vc = grid.vertCoordUnion; if (vc == null) continue; if (vc_union == null) vc_union = new VertCoord(vc); else if (!vc_union.equalsData(vc)) { // log.warn(name+" Grid "+ gridName +" has different vert coords in run " + grid.getRunDate()); vertList.add(vc); } } if (vc_union != null) { if (vertList.size() > 0) VertCoord.normalize(vc_union, vertList); // add the other coords vertCoordUnion = VertCoord.findVertCoord(getVertCoords(), vc_union); // now find unique within collection } // optionally calculate expected inventory based on matching run hours if (regularize) { // create groups of runs with the same runtime hour (integer offset from 0Z) Map hourMap = new HashMap(); for (FmrInv.GridVariable grid : runs) { CalendarDate runDate = grid.getRunDate(); int hour = runDate.getHourOfDay(); HourGroup hg = hourMap.get(hour); if (hg == null) { hg = new HourGroup(hour); hourMap.put(hour, hg); } hg.runs.add(grid); } // assume each hour group should have the same set of forecast time coords, as represented by their offset for (HourGroup hg : hourMap.values()) { // run over all timeCoords in this group and construct the union List timeListExp = new ArrayList(); for (FmrInv.GridVariable run : hg.runs) timeListExp.add(run.timeCoordUnion); // note that in this case, the baseDates of the TimeCoords in timeListExp are not the same // we are just using this routine to get the union of offset hours or intervals hg.expected = TimeCoord.makeUnion(timeListExp, baseDate); // add the other coords if (hg.expected.isInterval()) { // we discard the resulting TimeCoord and just use the union of intervals. for (FmrInv.GridVariable grid : hg.runs) { grid.timeExpected = new TimeCoord(grid.getRunDate()); grid.timeExpected.setBounds(hg.expected.getBound1(), hg.expected.getBound2()); } } else { // we discard the resulting TimeCoord and just use the union of offsets for (FmrInv.GridVariable grid : hg.runs) grid.timeExpected = new TimeCoord(grid.getRunDate(), hg.expected.getOffsetTimes()); } } // now find the RunSeq, based on the timeExpected this.runSeq = findRunSeq(runs); this.runSeq.addVariable(this); } else { // otherwise expected == actual for (FmrInv.GridVariable grid : runs) grid.timeExpected = grid.timeCoordUnion; // now find the RunSeq, based on the timeExpected this.runSeq = findRunSeq(runs); this.runSeq.addVariable(this); /* run over all timeCoords and construct the union List timeList = new ArrayList(); for (FmrInv.GridVariable grid : runs) timeList.add(grid.timeCoordUnion); TimeCoord tc_union = TimeCoord.makeUnion(timeList, baseDate); // create the union of all time coords used by this grid this.timeCoordUnion = TimeCoord.findTimeCoord(getRunSeqs(), tc_union); // track unique ones in the FmrcInv */ } } } // end UberGrid // immutable after UberGrid.finish() is called. private class HourGroup { final int hour; final List runs = new ArrayList(); private TimeCoord expected; HourGroup(int hour) { this.hour = hour; } } private RunSeq findRunSeq(List runs) { for (RunSeq seq : runSeqs) { if (seq.equalsData(runs)) return seq; } // make a new one RunSeq result = new RunSeq(runs); runSeqs.add(result); return result; } /** * Represents a sequence of Runs, each run has a particular TimeCoord. * this is where the time coordinates may be regularized * keep track of all the Variables with the same RunSeq */ // immutable after UberGrid.finish() is called. public class RunSeq { private final HashMap coordMap; // runDate, timeExpected private final List vars = new ArrayList(); // list of UberGrid that use this private int id; private List timeList = null; // timeList has differing runDates private TimeCoord timeCoordUnion = null; // union of all offset hours private boolean isInterval; RunSeq(List runs) { this.coordMap = new HashMap(2 * runs.size()); // make sure every date has a slot for (CalendarDate d : runTimeList) this.coordMap.put(d, TimeCoord.EMPTY); // overwrite with actual coords boolean first = true; for (FmrInv.GridVariable grid : runs) { this.coordMap.put(grid.getRunDate(), grid.getTimeExpected()); // match on timeExpected // check intervals are consistent if (first) isInterval = grid.getTimeCoord().isInterval(); else if (isInterval != grid.getTimeCoord().isInterval()) { log.error("mixed intervals for grid "+grid. getName()); throw new IllegalArgumentException("mixed intervals for grid "+grid. getName()); } first = false; } } public boolean isInterval() { return isInterval; } public List getTimes() { if (timeList == null) getUnionTimeCoord(); return timeList; } public String getName() { return id == 0 ? "time" : "time" + id; } public int getNTimeOffsets() { int n = 0; for (TimeCoord tc : coordMap.values()) n = Math.max(n, tc.getNCoords()); return n; } // the union of all offset hours, ignoring rundate, so its the rectangularization of the // offsets for each run // has the side effect of constructing timeList, the list of expected TimeCoords, one for each run public TimeCoord getUnionTimeCoord() { if (timeCoordUnion == null) { // deferred creation // eliminate the empties timeList = new ArrayList(); for (TimeCoord tc : coordMap.values()) { if ((tc != null) && (tc != TimeCoord.EMPTY)) timeList.add(tc); } // sort by run Date Collections.sort(timeList, new Comparator() { public int compare(TimeCoord o1, TimeCoord o2) { if (o1 == null || o1.getRunDate() == null) return -1; if (o2 == null || o2.getRunDate() == null) return 1; return o1.getRunDate().compareTo(o2.getRunDate()); } }); // here again the timeList has differing runDates timeCoordUnion = TimeCoord.makeUnion(timeList, baseDate); // create the union of all offsets used by this grid } return timeCoordUnion; } /** * Decide if this RunSeq is equivalent to the list of Runs. * * @param oruns list of FmrInv.GridVariable, use their expected times to match * @return true if it has an equivalent set of runs. */ boolean equalsData(List oruns) { List okAdd = null; // may need to fill in the gaps for (FmrInv.GridVariable grid : oruns) { TimeCoord run = coordMap.get(grid.getRunDate()); if (run == null) { if (okAdd == null) okAdd = new ArrayList(); okAdd.add(grid); } else { TimeCoord orune = grid.getTimeExpected(); if (!run.equalsData(orune)) return false; } } // everything else matches, ok to expand this runSeq with missing runs if (okAdd != null) for (FmrInv.GridVariable grid : okAdd) this.coordMap.put(grid.getRunDate(), grid.getTimeExpected()); return true; } // keep track of all the Variables that have this RunSeq void addVariable(UberGrid uv) { vars.add(uv); } public List getUberGrids() { return vars; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /* debugging - Fmrc2 Panel public void showRuntimeOffsetMatrix(Formatter out) { out.format(" Forecast Time Offset %n"); out.format(" RunTime "); for (double offsetHour : tcOffAll.getOffsetTimes()) { out.format("%10.0f", offsetHour); } out.format("%n"); int count = 0; for (int i = 0; i < fmrList.size(); i++) { FmrInv fmr = fmrList.get(i); Date runDate = fmr.getRunDate(); out.format("%2d %s ", i, dateFormatter.toDateTimeStringISO(runDate)); for (double offsetHour : tcOffAll.getOffsetTimes()) { Inventory inv = getInventory(fmr, offsetHour); count += inv.showInventory( out); } out.format("%n"); } out.format("%ntotal inventory count = %d%n", count); } // for a given offset hour and Fmr, find the expected and actual inventory over all grids private Inventory getInventory(FmrInv fmr, double hour) { int actual = 0, expected = 0; for (FmrInv.GridVariable grid : fmr.getGrids()) { TimeCoord tExpect = grid.getTimeExpected(); if (tExpect.findIndex(hour) >= 0) expected += grid.getNVerts(); for (GridDatasetInv.Grid inv : grid.getInventory()) { TimeCoord tc = inv.getTimeCoord(); if (tc.findIndex(hour) >= 0) actual += inv.getVertCoordLength(); } } return new Inventory(actual, expected); } @Immutable public static class Inventory { public final int actual; public final int expected; public Inventory(int actual, int expected) { this.actual = actual; this.expected = expected; } public int showInventory(Formatter out) { if (actual != expected) { out.format("%10s", actual + "/" + expected); } else if (actual == 0) out.format(" "); // blank else out.format("%10d", actual); return actual; } } private class TimeInventory { final TimeCoord tc; // all the TimeCoords possible final FmrcInv.UberGrid ugrid; // for this grid final FmrInv.GridVariable[] useGrid; // use this run and grid for ith offset final int[] runIndex; // the index of the run in the fmrList. ie findFmrIndex() TimeInventory(TimeCoord tc, FmrcInv.UberGrid ugrid) { this.tc = tc; this.ugrid = ugrid; this.useGrid = new FmrInv.GridVariable[tc.getNCoords()]; this.runIndex = new int[tc.getNCoords()]; } private void setOffset(double offsetHour, FmrInv.GridVariable grid, int runIndex) { int offsetIndex = tc.findIndex(offsetHour); if (offsetIndex < 0) throw new IllegalStateException("FmrSnapshot cant find hour " + offsetHour + " in " + tc); this.useGrid[offsetIndex] = grid; this.runIndex[offsetIndex] = runIndex; } } ////////////////////////////////////// // 1D subsets public void showBest(Formatter out) { out.format("%nRun used in best dataset%n"); out.format(" Forecast Time Offset %n"); out.format("Grid "); for (double offsetHour : tcOffAll.getOffsetTimes()) out.format("%4.0f ", offsetHour); out.format("%n"); for (FmrcInv.UberGrid ugrid : getUberGrids()) { TimeInventory inv = makeBestInventory(ugrid); String name = ugrid.getName(); if (name.length() > 27) name = name.substring(0, 27); out.format(" %-27s ", name); showInv(inv, out); out.format("%n"); } } public void showBest2(Formatter out) { out.format("%nRun used in best dataset by RunSeq%n"); out.format("Seq Forecast Time Offset %n "); for (double offsetHour : tcOffAll.getOffsetTimes()) out.format("%4.0f ", offsetHour); out.format("%n"); int count = 0; for (RunSeq seq : getRunSeqs()) { out.format("%3d ", count++); List ugrids = seq.getUberGrids(); TimeInventory inv = makeBestInventory(ugrids.get(0)); showInv(inv, out); for (FmrcInv.UberGrid ugrid : seq.getUberGrids()) { TimeInventory inv2 = makeBestInventory(ugrid); out.format(", %s ", ugrid.getName()); if (!testInv(inv, inv2)) out.format("BAD "); } out.format("%n"); } } // select best inventory for each forecast time private TimeInventory makeBestInventory(FmrcInv.UberGrid ugrid) { return ugrid.isInterval() ? makeBestInventoryFromIntervals(ugrid) : makeBestInventoryFromOffsets( ugrid); } private TimeInventory makeBestInventoryFromIntervals(FmrcInv.UberGrid ugrid) { TimeInventory inv = new TimeInventory(tcIntAll, ugrid); for (FmrInv.GridVariable grid : ugrid.getRuns()) { double forecastOffset = getOffsetInHours(baseDate, grid.getRunDate()); int runIndex = findFmrIndex(grid.getRunDate()); TimeCoord tc = grid.getTimeCoord(); // forecast times for this run for (double offset : tc.getOffsetTimes()) { inv.setOffset(forecastOffset + offset, grid, runIndex); // later ones override } } return inv; } // select best inventory for each forecast time private TimeInventory makeBestInventoryFromOffsets(FmrcInv.UberGrid ugrid) { TimeInventory inv = new TimeInventory(tcOffAll, ugrid); for (FmrInv.GridVariable grid : ugrid.getRuns()) { double forecastOffset = getOffsetInHours(baseDate, grid.getRunDate()); int runIndex = findFmrIndex(grid.getRunDate()); TimeCoord tc = grid.getTimeCoord(); // forecast times for this run for (double offset : tc.getOffsetTimes()) { inv.setOffset(forecastOffset + offset, grid, runIndex); // later ones override } } return inv; } // select best inventory for each forecast time public void showBest(FmrcInv.UberGrid ugrid, Formatter out) { out.format("%nRun used in best dataset for grid %s %n", ugrid.getName()); out.format(" Forecast Time Offset %n"); out.format("Run "); for (double offsetHour : tcOffAll.getOffsetTimes()) { out.format("%3.0f ", offsetHour); } out.format("%n"); TimeInventory inv = new TimeInventory(tcOffAll, ugrid); for (FmrInv.GridVariable grid : ugrid.getRuns()) { double forecastOffset = getOffsetInHours(baseDate, grid.getRunDate()); int runIndex = findFmrIndex(grid.getRunDate()); TimeCoord tc = grid.getTimeCoord(); // forecast times for this run TimeInventory invRun = new TimeInventory(tcOffAll, ugrid); for (double offset : tc.getOffsetTimes()) { inv.setOffset(forecastOffset + offset, grid, runIndex); // later ones override invRun.setOffset(forecastOffset + offset, grid, runIndex); // later ones override } out.format(" %-27s ", dateFormatter.toDateTimeString(grid.getRunDate())); showInv(invRun, out); out.format("%n"); } String name = ugrid.getName(); if (name.length() > 27) name = name.substring(0, 27); out.format(" %-27s ", "result"); showInv(inv, out); out.format("%n"); } private void showInv(TimeInventory tinv, Formatter out) { for (int i = 0; i < tinv.useGrid.length; i++) { FmrInv.GridVariable grid = tinv.useGrid[i]; if (grid == null) out.format(" "); else out.format("%4d ", tinv.runIndex[i]); } } private boolean testInv(TimeInventory tinv1, TimeInventory tinv2) { if (tinv1.useGrid.length != tinv2.useGrid.length) return false; for (int i = 0; i < tinv1.useGrid.length; i++) { if (tinv1.runIndex[i] != tinv2.runIndex[i]) return false; } return true; } */ }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy