diff --git a/plugin/build.xml b/plugin/build.xml index 52ef137e9..8a383c53c 100644 --- a/plugin/build.xml +++ b/plugin/build.xml @@ -39,8 +39,8 @@ - - + + @@ -79,11 +79,12 @@ + - + @@ -92,91 +93,93 @@ - - + + + + + + --> + + + + - + - - - - - - - + + + + + + + - - - - - + + + + + - + - + - - + + - + - + - + - - - - - + + + + + - - + + - + - -
+ +
Test Summary ============
- - - - - - - - -
- - - + - + - - - - - - - - - + + + + + + + + + @@ -187,10 +190,11 @@ + - + - + @@ -225,6 +229,7 @@ + @@ -251,7 +256,7 @@ - + @@ -277,6 +282,7 @@ + @@ -295,7 +301,7 @@ --> - + @@ -307,11 +313,11 @@ - + - - - + + + diff --git a/plugin/src/org/aavso/tools/vstar/external/lib/PiecewiseLinearModel.java b/plugin/src/org/aavso/tools/vstar/external/lib/PiecewiseLinearModel.java new file mode 100644 index 000000000..ab97e6aac --- /dev/null +++ b/plugin/src/org/aavso/tools/vstar/external/lib/PiecewiseLinearModel.java @@ -0,0 +1,317 @@ +/** + * VStar: a statistical analysis tool for variable star data. + * Copyright (C) 2009 AAVSO (http://www.aavso.org/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.aavso.tools.vstar.external.lib; + +import java.util.ArrayList; +import java.util.List; + +import org.aavso.tools.vstar.data.ValidObservation; +import org.aavso.tools.vstar.exception.AlgorithmError; +import org.aavso.tools.vstar.ui.model.plot.ContinuousModelFunction; +import org.aavso.tools.vstar.ui.model.plot.ICoordSource; +import org.aavso.tools.vstar.util.AbstractExtremaFinder; +import org.aavso.tools.vstar.util.locale.LocaleProps; +import org.aavso.tools.vstar.util.model.AbstractModel; +import org.aavso.tools.vstar.util.prefs.NumericPrecisionPrefs; +import org.apache.commons.math.analysis.UnivariateRealFunction; +import org.apache.commons.math.optimization.GoalType; + +/** + * This class represents a piecewise linear model. + */ +public class PiecewiseLinearModel extends AbstractModel { + + private static final String DESC = "Piecewise linear model"; + + private PiecewiseLinearFunction piecewiseFunction; + + public PiecewiseLinearModel(List obs, List meanObs) { + super(obs); + + // Create piecewise linear model + piecewiseFunction = new PiecewiseLinearFunction(meanObs, timeCoordSource); + } + + @Override + public void execute() throws AlgorithmError { + + fit = new ArrayList(); + residuals = new ArrayList(); + + String comment = DESC; + + for (int i = 0; i < obs.size() && !interrupted; i++) { + ValidObservation ob = obs.get(i); + + double x = timeCoordSource.getXCoord(i, obs); + double y = piecewiseFunction.value(x); + + collectObs(y, ob, comment); + } + + rootMeanSquare(); + informationCriteria(piecewiseFunction.numberOfFunctions()); + fitMetrics(); + + PiecewiseLinearFunctionExtremaFinder finder = new PiecewiseLinearFunctionExtremaFinder(fit, piecewiseFunction, + timeCoordSource); + + String extremaStr = finder.toString(); + + if (extremaStr != null) { + String title = LocaleProps.get("MODEL_INFO_EXTREMA_TITLE"); + + functionStrMap.put(title, extremaStr); + } + + functionStrings(); + } + + @Override + public boolean hasFuncDesc() { + return true; + } + + public String toVeLaString() { + String strRepr = functionStrMap.get(LocaleProps.get("MODEL_INFO_FUNCTION_TITLE")); + + if (strRepr == null) { + strRepr = piecewiseFunction.toVeLaString(); + } + + return strRepr; + } + + @Override + public String getDescription() { + return DESC; + } + + @Override + public String getKind() { + return "Piecewise linear model"; + } + + @Override + public ContinuousModelFunction getModelFunction() { + return new ContinuousModelFunction(piecewiseFunction, fit, 0); + } + + public static class PiecewiseLinearFunction implements UnivariateRealFunction { + private List functions; + private LinearFunction currFunc; + private int funIndex; + + public PiecewiseLinearFunction(List obs, ICoordSource timeCoordSource) { + functions = new ArrayList(); + + for (int i = 0; i < obs.size() - 1; i++) { + ValidObservation ob1 = obs.get(i); + ValidObservation ob2 = obs.get(i + 1); + double t1 = timeCoordSource.getXCoord(i, obs); + double t2 = timeCoordSource.getXCoord(i + 1, obs); + functions.add(new LinearFunction(t1, t2, ob1.getMag(), ob2.getMag())); + } + + currFunc = functions.get(0); + funIndex = 0; + } + + @Override + public double value(double t) { + if (t > currFunc.endTime() && funIndex < functions.size() - 1) { + funIndex++; + currFunc = functions.get(funIndex); + } + + return currFunc.value(t); + } + + /** + * @return The list of functions. + */ + public List getFunctions() { + return functions; + } + + /** + * Find the index of the function to which the target time corresponds. + * + * @param t The target time. + * @return The index of the function or -1 if t does not correspond to the time + * range of any linear function. + */ + public int seekFunction(double t) { + int index = -1; + + for (int i = 0; i < functions.size() - 1; i++) { + LinearFunction linearFunc = functions.get(i); + if (t >= linearFunc.t1 && t < linearFunc.t2) { + index = i; + break; + } + } + + return index; + } + + public int numberOfFunctions() { + return functions.size(); + } + + public String toVeLaString() { + String strRepr = ""; + + strRepr += "f(t:real) : real {\n"; + strRepr += " when\n"; + for (int i = 0; i < functions.size(); i++) { + LinearFunction function = functions.get(i); + boolean first = i == 0; + boolean last = i == functions.size() - 1; + strRepr += " " + function.toVeLaString(first, last); + } + strRepr += "\n}"; + + return strRepr; + } + } + + /** + * Represents the function for a line segment + */ + public static class LinearFunction implements UnivariateRealFunction { + + private double t1; + private double t2; + private double mag1; + private double mag2; + private double m; + private double c; + + public LinearFunction(double t1, double t2, double mag1, double mag2) { + this.t1 = t1; + this.t2 = t2; + this.mag1 = mag1; + this.mag2 = mag2; + + // y = mx + c + m = slope(); + c = mag1 - m * t1; + } + + public double startTime() { + return t1; + } + + public double endTime() { + return t2; + } + + public double startMag() { + return mag1; + } + + public double endMag() { + return mag2; + } + + public double slope() { + return (mag2 - mag1) / (t2 - t1); + } + + public double yIntercept() { + return c; + } + + @Override + public double value(double t) { + return m * t + c; + } + + public String toVeLaString(boolean first, boolean last) { + // For the first line segment, we only need to check + // the second bound. For the last line segment, we don't + // need to check either bound. + StringBuffer buf = new StringBuffer(); + + if (!first && !last) { + buf.append("t >= "); + buf.append(NumericPrecisionPrefs.formatTimeLocaleIndependent(t1)); + buf.append(" and "); + } + + if (!last) { + buf.append("t < "); + buf.append(NumericPrecisionPrefs.formatTimeLocaleIndependent(t2)); + } else { + buf.append("true "); + } + + buf.append(" -> "); + + buf.append(NumericPrecisionPrefs.formatOtherLocaleIndependent(m)); + buf.append("*t + "); + buf.append(NumericPrecisionPrefs.formatOtherLocaleIndependent(c)); + + if (!last) + buf.append("\n"); + + return buf.toString(); + } + } + + public static class PiecewiseLinearFunctionExtremaFinder extends AbstractExtremaFinder { + PiecewiseLinearFunction plf; + + public PiecewiseLinearFunctionExtremaFinder(List obs, PiecewiseLinearFunction function, + ICoordSource timeCoordSource) { + super(obs, function, timeCoordSource, 0); + plf = function; + } + + @Override + public void find(GoalType goal, int[] bracketRange) throws AlgorithmError { + extremeTime = Double.POSITIVE_INFINITY; + extremeMag = Double.POSITIVE_INFINITY; + + double firstJD = obs.get(bracketRange[0]).getJD(); + double lastJD = obs.get(bracketRange[1]).getJD(); + + int firstIndex = plf.seekFunction(firstJD); + int lastIndex = plf.seekFunction(lastJD); + + // extrema should be at the meeting point of two linear functions + boolean found = false; + + if (lastIndex == firstIndex + 1) { + if (goal == GoalType.MINIMIZE && plf.functions.get(firstIndex).slope() < 0 + && plf.functions.get(lastIndex).slope() > 0) { + found = true; + } else if (goal == GoalType.MAXIMIZE && plf.functions.get(firstIndex).slope() > 0 + && plf.functions.get(lastIndex).slope() < 0) { + found = true; + } + } + + if (found) { + extremeTime = plf.functions.get(lastIndex).t1; + extremeMag = plf.functions.get(lastIndex).value(extremeTime); + } + } + } +} diff --git a/plugin/src/org/aavso/tools/vstar/external/plugin/AoVPeriodSearch.java b/plugin/src/org/aavso/tools/vstar/external/plugin/AoVPeriodSearch.java index 4bfa4ee41..88baff19d 100644 --- a/plugin/src/org/aavso/tools/vstar/external/plugin/AoVPeriodSearch.java +++ b/plugin/src/org/aavso/tools/vstar/external/plugin/AoVPeriodSearch.java @@ -33,6 +33,7 @@ import org.aavso.tools.vstar.data.ValidObservation; import org.aavso.tools.vstar.exception.AlgorithmError; import org.aavso.tools.vstar.exception.CancellationException; +import org.aavso.tools.vstar.external.lib.PiecewiseLinearModel; import org.aavso.tools.vstar.plugin.PluginComponentFactory; import org.aavso.tools.vstar.plugin.period.PeriodAnalysisComponentFactory; import org.aavso.tools.vstar.plugin.period.PeriodAnalysisDialogBase; @@ -50,6 +51,7 @@ import org.aavso.tools.vstar.ui.mediator.message.NewStarMessage; import org.aavso.tools.vstar.ui.mediator.message.PeriodAnalysisSelectionMessage; import org.aavso.tools.vstar.ui.model.list.PeriodAnalysisDataTableModel; +import org.aavso.tools.vstar.ui.model.plot.ObservationAndMeanPlotModel; import org.aavso.tools.vstar.ui.model.plot.PeriodAnalysis2DPlotModel; import org.aavso.tools.vstar.ui.model.plot.PhaseTimeElementEntity; import org.aavso.tools.vstar.util.comparator.StandardPhaseComparator; @@ -85,564 +87,563 @@ */ public class AoVPeriodSearch extends PeriodAnalysisPluginBase { - private final static int MAX_TOP_HITS = 20; + private final static int MAX_TOP_HITS = 20; - private boolean firstInvocation; - private boolean interrupted; - private boolean cancelled; - private boolean legalParams; + private boolean firstInvocation; + private boolean interrupted; + private boolean cancelled; + private boolean legalParams; - private int bins; - private Double minPeriod, maxPeriod, resolution; + private List obs; - private IPeriodAnalysisAlgorithm algorithm; + private int bins; + private Double minPeriod, maxPeriod, resolution; - private PeriodAnalysisCoordinateType F_STATISTIC; - private PeriodAnalysisCoordinateType P_VALUE; + private IPeriodAnalysisAlgorithm algorithm; - /** - * Constructor - */ - public AoVPeriodSearch() { - super(); - firstInvocation = true; - reset(); - } - - @Override - public String getDescription() { - return "AoV period search"; - } - - @Override - public String getDisplayName() { - return "AoV with Period Range"; - } - - /** - * @see org.aavso.tools.vstar.plugin.IPlugin#getDocName() - */ - @Override - public String getDocName() { - return "AoV Period Analysis Plug-In.pdf"; - } - - @Override - public void executeAlgorithm(List obs) - throws AlgorithmError, CancellationException { - - if (firstInvocation) { - Mediator.getInstance().getNewStarNotifier() - .addListener(getNewStarListener()); - - F_STATISTIC = PeriodAnalysisCoordinateType.create("F-statistic"); - P_VALUE = PeriodAnalysisCoordinateType.create("p-value"); - - firstInvocation = false; - } - - algorithm = new AoVAlgorithm(obs); - algorithm.execute(); - } - - @Override - public JDialog getDialog(SeriesType sourceSeriesType) { - return interrupted || cancelled ? null : new PeriodAnalysisDialog( - sourceSeriesType); - } - - @SuppressWarnings("serial") - class PeriodAnalysisDialog extends PeriodAnalysisDialogBase implements - Listener { - - private double period; - private SeriesType sourceSeriesType; - private IPeriodAnalysisDatum selectedDataPoint; - - private PeriodAnalysisDataTablePane resultsTablePane; - private PeriodAnalysisTopHitsTablePane topHitsTablePane; - private PeriodAnalysis2DChartPane plotPane; - private PeriodAnalysis2DChartPane topHitsPlotPane; - - public PeriodAnalysisDialog(SeriesType sourceSeriesType) { - super("AoV", false, true, false); - - this.sourceSeriesType = sourceSeriesType; - - prepareDialog(); - - this.setNewPhasePlotButtonState(false); - - startup(); // Note: why does base class not call this in - // prepareDialog()? - } - - @Override - protected Component createContent() { - String title = "AoV Periodogram"; - - PeriodAnalysis2DPlotModel dataPlotModel = new PeriodAnalysis2DPlotModel( - algorithm.getResultSeries(), - PeriodAnalysisCoordinateType.PERIOD, F_STATISTIC, false); - - plotPane = PeriodAnalysisComponentFactory.createLinePlot(title, - sourceSeriesType.getDescription(), dataPlotModel, false); - - PeriodAnalysis2DPlotModel topHitsPlotModel = new PeriodAnalysis2DPlotModel( - algorithm.getTopHits(), - PeriodAnalysisCoordinateType.PERIOD, F_STATISTIC, false); - - topHitsPlotPane = PeriodAnalysisComponentFactory.createScatterPlot( - title, sourceSeriesType.getDescription(), topHitsPlotModel, - false); - - // Add the above line plot's model to the scatter plot. - // Render the scatter plot last so the "handles" will be - // the first items selected by the mouse. - JFreeChart chart = topHitsPlotPane.getChart(); - chart.getXYPlot().setDataset(PeriodAnalysis2DChartPane.DATA_SERIES, - dataPlotModel); - chart.getXYPlot().setDataset( - PeriodAnalysis2DChartPane.TOP_HIT_SERIES, topHitsPlotModel); - chart.getXYPlot().setRenderer( - PeriodAnalysis2DChartPane.DATA_SERIES, - plotPane.getChart().getXYPlot().getRenderer()); - chart.getXYPlot().setDatasetRenderingOrder( - DatasetRenderingOrder.REVERSE); - - plotPane = topHitsPlotPane; - - // Full results table - PeriodAnalysisCoordinateType[] columns = { - PeriodAnalysisCoordinateType.FREQUENCY, - PeriodAnalysisCoordinateType.PERIOD, F_STATISTIC, P_VALUE }; - - PeriodAnalysisDataTableModel dataTableModel = new PeriodAnalysisDataTableModel( - columns, algorithm.getResultSeries()); - resultsTablePane = new NoModelPeriodAnalysisDataTablePane( - dataTableModel, algorithm); - - PeriodAnalysisDataTableModel topHitsModel = new PeriodAnalysisDataTableModel( - columns, algorithm.getTopHits()); - topHitsTablePane = new NoModelPeriodAnalysisTopHitsTablePane( - topHitsModel, dataTableModel, algorithm); - - // Return tabbed pane of plot and period display component. - return PluginComponentFactory.createTabs(new NamedComponent( - "Periodogram", plotPane), new NamedComponent("Results", - resultsTablePane), new NamedComponent("Top Hits", - topHitsTablePane)); - } - - // Send a period change message when the new-phase-plot button is - // clicked. - @Override - protected void newPhasePlotButtonAction() { - sendPeriodChangeMessage(period); - } - - @Override - public void startup() { - Mediator.getInstance().getPeriodAnalysisSelectionNotifier() - .addListener(this); - - resultsTablePane.startup(); - topHitsTablePane.startup(); - plotPane.startup(); - } - - @Override - public void cleanup() { - Mediator.getInstance().getPeriodAnalysisSelectionNotifier() - .removeListenerIfWilling(this); - - resultsTablePane.cleanup(); - topHitsTablePane.cleanup(); - plotPane.cleanup(); - } - - // Next two methods are for Listener - - @Override - public boolean canBeRemoved() { - return false; - } - - @Override - public void update(PeriodAnalysisSelectionMessage info) { - period = info.getDataPoint().getPeriod(); - selectedDataPoint = info.getDataPoint(); - setNewPhasePlotButtonState(true); - } - - // ** No model result and top-hit panes ** - - class NoModelPeriodAnalysisDataTablePane extends - PeriodAnalysisDataTablePane { - - public NoModelPeriodAnalysisDataTablePane( - PeriodAnalysisDataTableModel model, - IPeriodAnalysisAlgorithm algorithm) { - super(model, algorithm); - } - - @Override - protected JPanel createButtonPanel() { - return new JPanel(); - } - - @Override - protected void enableButtons() { - // Do nothing - } - } - - class NoModelPeriodAnalysisTopHitsTablePane extends - PeriodAnalysisTopHitsTablePane { - - public NoModelPeriodAnalysisTopHitsTablePane( - PeriodAnalysisDataTableModel topHitsModel, - PeriodAnalysisDataTableModel fullDataModel, - IPeriodAnalysisAlgorithm algorithm) { - super(topHitsModel, fullDataModel, algorithm); - } - - @Override - protected JPanel createButtonPanel() { - return new JPanel(); - } - - @Override - protected void enableButtons() { - // Do nothing - } - } - - @Override - protected void findHarmonicsButtonAction() { - // Do nothing since we don't include a find-harmonics button for - // AoV. - } - } - - // The AoV algorithm implementation. - class AoVAlgorithm implements IPeriodAnalysisAlgorithm { - - private List obs; - - private List frequencies; - private ArrayList orderedFrequencies; - - private List periods; - private ArrayList orderedPeriods; - - private List fValues; - private ArrayList orderedFValues; - - private List pValues; - private ArrayList orderedPValues; - - // private double smallestFValue; - // private int smallestValueIndex; - - public AoVAlgorithm(List obs) { - this.obs = obs; - - frequencies = new ArrayList(); - orderedFrequencies = new ArrayList(); - - periods = new ArrayList(); - orderedPeriods = new ArrayList(); - - fValues = new ArrayList(); - orderedFValues = new ArrayList(); - - pValues = new ArrayList(); - orderedPValues = new ArrayList(); - - // smallestFValue = Double.MAX_VALUE; - // smallestValueIndex = 0; - } - - @Override - public String getRefineByFrequencyName() { - return "None"; - } - - @Override - public Map> getResultSeries() { - Map> results = new LinkedHashMap>(); - - results.put(PeriodAnalysisCoordinateType.FREQUENCY, frequencies); - results.put(PeriodAnalysisCoordinateType.PERIOD, periods); - results.put(F_STATISTIC, fValues); - results.put(P_VALUE, pValues); - - return results; - } - - @Override - public Map> getTopHits() { - // TODO: create top hits by sorting doubles in descending order - // pairs of doubles; - // limit to MAX_TOP_HITS = 100 - - Map> topHits = new LinkedHashMap>(); - - topHits.put(PeriodAnalysisCoordinateType.FREQUENCY, - orderedFrequencies); - topHits.put(PeriodAnalysisCoordinateType.PERIOD, orderedPeriods); - topHits.put(F_STATISTIC, orderedFValues); - topHits.put(P_VALUE, orderedPValues); - - return topHits; - } - - @Override - public void multiPeriodicFit(List harmonics, - PeriodAnalysisDerivedMultiPeriodicModel model) - throws AlgorithmError { - } - - @Override - public List refineByFrequency( - List freqs, List variablePeriods, - List lockedPeriod) throws AlgorithmError { - return null; - } - - @Override - public void execute() throws AlgorithmError { - // Request parameters - // TODO: move this to top-level execute method and just pass actual - // parameters to this class? - while (!areParametersLegal(obs) && !cancelled) - ; - - if (!cancelled) { - // Duplicate the obs (just JD and mag) so we can set phases - // without disturbing the original observation object. - List phObs = new ArrayList(); - - // TODO: cache these by JD range between new star resets... - - interrupted = false; - - // TODO: for a multi-threaded range-subset approach, we would - // need to do this once for each thread - for (ValidObservation ob : obs) { - if (interrupted) - break; - - ValidObservation phOb = new ValidObservation(); - - double jd = ob.getDateInfo().getJulianDay(); - phOb.setDateInfo(new DateInfo(jd)); - - Magnitude mag = new Magnitude(ob.getMagnitude() - .getMagValue(), ob.getMagnitude().getUncertainty()); - phOb.setMagnitude(mag); - - phObs.add(phOb); - } - - // Choose an epoch value. - double epoch = PhaseCalcs.epochStrategyMap.get("alpha") - .determineEpoch(phObs); - - // Iterate over the periods in the range at the specified - // resolution. - - // TODO: multi-core approach => iterate over a subset of the - // period range but over all observations, where the full set - // is copied for each core (set phases, sort mutate obs and - // list...); top-hits will have to be combined and ordered once - // at end as part of or before prune operation; instead, could - // just iterate over a subset of observations; this would only - // give a large speedup if many observations; such a for-loop - // unrolling would be simpler and less memory intensive though; - // may be worth trying first - - for (double period = minPeriod; period <= maxPeriod; period += resolution) { - if (interrupted) - break; - - PhaseCalcs.setPhases(phObs, epoch, period); - - Collections.sort(phObs, StandardPhaseComparator.instance); - - // Note: 1 / bins = 1 cycle divided into N bins - BinningResult binningResult = DescStats - .createSymmetricBinnedObservations(phObs, - PhaseTimeElementEntity.instance, 1.0 / bins); - - // Collect results - // PMAK, Issue #152: - // Use fixInf() to prevent - // 'java.lang.IllegalArgumentException: Must be finite' - // error in AoV chart when period = 0 - frequencies.add(fixInf(1.0 / period)); - periods.add(period); - fValues.add(fixInf(binningResult.getFValue())); - pValues.add(fixInf(binningResult.getPValue())); - - updateOrderedValues(); - } - - pruneTopHits(); - } - } - - // replace +-Infinity by NaN - private double fixInf(double v) { - if (Double.isInfinite(v)) - return Double.NaN; - else - return v; - } - - private void updateOrderedValues() { - if (orderedFrequencies.isEmpty()) { - orderedFrequencies.add(frequencies.get(0)); - orderedPeriods.add(periods.get(0)); - orderedFValues.add(fValues.get(0)); - orderedPValues.add(pValues.get(0)); - } else { - int i = periods.size() - 1; - - double frequency = frequencies.get(i); - double period = periods.get(i); - double fValue = fValues.get(i); - double pValue = pValues.get(i); - - // Starting from highest fValue, find index to insert value - // and... - int index = 0; - for (int j = 0; j < orderedFValues.size(); j++) { - if (fValue < orderedFValues.get(j)) { - // Insertion index is one after the matched element's - // index since the list's elements are in descending - // order. - index++; - } - } - - // ...apply to all ordered collections. - if (index >= 0) { - orderedFrequencies.add(index, frequency); - orderedPeriods.add(index, period); - orderedFValues.add(index, fValue); - orderedPValues.add(index, pValue); - } else { - orderedFrequencies.add(0, frequency); - orderedPeriods.add(0, period); - orderedFValues.add(0, fValue); - orderedPValues.add(0, pValue); - } - } - } - - private void pruneTopHits() { - if (periods.size() > MAX_TOP_HITS) { - orderedFrequencies = new ArrayList( - orderedFrequencies.subList(0, MAX_TOP_HITS)); - - orderedPeriods = new ArrayList(orderedPeriods.subList( - 0, MAX_TOP_HITS)); - - orderedFValues = new ArrayList(orderedFValues.subList( - 0, MAX_TOP_HITS)); - - orderedPValues = new ArrayList(orderedPValues.subList( - 0, MAX_TOP_HITS)); - } - } - - @Override - public void interrupt() { - interrupted = true; - } - } - - // Ask user for period min, max, resolution and number of bins. - private boolean areParametersLegal(List obs) { - legalParams = true; - - List> fields = new ArrayList>(); - - // / double days = obs.get(obs.size() - 1).getJD() - obs.get(0).getJD(); - DoubleField minPeriodField = new DoubleField("Minimum Period", 0.0, - null, minPeriod); - fields.add(minPeriodField); - - DoubleField maxPeriodField = new DoubleField("Maximum Period", 0.0, - null, maxPeriod); - fields.add(maxPeriodField); - - DoubleField resolutionField = new DoubleField("Resolution", 0.0, 1.0, - resolution); - fields.add(resolutionField); - - IntegerField binsField = new IntegerField("Bins", 0, 50, bins); - fields.add(binsField); - - MultiEntryComponentDialog dlg = new MultiEntryComponentDialog( - "AoV Parameters", fields); - - cancelled = dlg.isCancelled(); - - if (!cancelled) { - - try { - bins = binsField.getValue(); - if (bins <= 0) { - MessageBox.showErrorDialog("AoV Parameters", - "Number of bins must be greater than zero"); - legalParams = false; - } - } catch (Exception e) { - legalParams = false; - } - - minPeriod = minPeriodField.getValue(); - maxPeriod = maxPeriodField.getValue(); - resolution = resolutionField.getValue(); - - if (minPeriod >= maxPeriod) { - MessageBox - .showErrorDialog("AoV Parameters", - "Minimum period must be less than or equal to maximum period"); - legalParams = false; - } - - if (resolution <= 0.0) { - MessageBox.showErrorDialog("AoV Parameters", - "Resolution must be between 0 and 1"); - legalParams = false; - } - } - - return legalParams; - } - - @Override - public void interrupt() { - interrupted = true; - } - - @Override - protected void newStarAction(NewStarMessage message) { - reset(); - } - - @Override - public void reset() { - cancelled = false; - legalParams = false; - interrupted = false; - minPeriod = 0.0; - maxPeriod = 0.0; - resolution = 0.1; - bins = 10; - } + private PeriodAnalysisCoordinateType F_STATISTIC; + private PeriodAnalysisCoordinateType P_VALUE; + + /** + * Constructor + */ + public AoVPeriodSearch() { + super(); + firstInvocation = true; + reset(); + } + + @Override + public String getDescription() { + return "AoV period search"; + } + + @Override + public String getDisplayName() { + return "AoV with Period Range"; + } + + /** + * @see org.aavso.tools.vstar.plugin.IPlugin#getDocName() + */ + @Override + public String getDocName() { + return "AoV Period Analysis Plug-In.pdf"; + } + + @Override + public void executeAlgorithm(List obs) throws AlgorithmError, CancellationException { + + this.obs = obs; + + if (firstInvocation) { + Mediator.getInstance().getNewStarNotifier().addListener(getNewStarListener()); + + F_STATISTIC = PeriodAnalysisCoordinateType.create("F-statistic"); + P_VALUE = PeriodAnalysisCoordinateType.create("p-value"); + + firstInvocation = false; + } + + algorithm = new AoVAlgorithm(obs); + algorithm.execute(); + } + + @Override + public JDialog getDialog(SeriesType sourceSeriesType) { + return interrupted || cancelled ? null : new PeriodAnalysisDialog(sourceSeriesType); + } + + @SuppressWarnings("serial") + class PeriodAnalysisDialog extends PeriodAnalysisDialogBase implements Listener { + + private double period; + private SeriesType sourceSeriesType; + private IPeriodAnalysisDatum selectedDataPoint; + + private PeriodAnalysisDataTablePane resultsTablePane; + private AoVPeriodAnalysisTopHitsTablePane topHitsTablePane; + private PeriodAnalysis2DChartPane plotPane; + private PeriodAnalysis2DChartPane topHitsPlotPane; + + public PeriodAnalysisDialog(SeriesType sourceSeriesType) { + super("AoV", false, true, false); + + this.sourceSeriesType = sourceSeriesType; + + prepareDialog(); + + this.setNewPhasePlotButtonState(false); + + startup(); // Note: why does base class not call this in + // prepareDialog()? + } + + @Override + protected Component createContent() { + String title = "AoV Periodogram"; + + PeriodAnalysis2DPlotModel dataPlotModel = new PeriodAnalysis2DPlotModel(algorithm.getResultSeries(), + PeriodAnalysisCoordinateType.PERIOD, F_STATISTIC, false); + + plotPane = PeriodAnalysisComponentFactory.createLinePlot(title, sourceSeriesType.getDescription(), + dataPlotModel, false); + + PeriodAnalysis2DPlotModel topHitsPlotModel = new PeriodAnalysis2DPlotModel(algorithm.getTopHits(), + PeriodAnalysisCoordinateType.PERIOD, F_STATISTIC, false); + + topHitsPlotPane = PeriodAnalysisComponentFactory.createScatterPlot(title, sourceSeriesType.getDescription(), + topHitsPlotModel, false); + + // Add the above line plot's model to the scatter plot. + // Render the scatter plot last so the "handles" will be + // the first items selected by the mouse. + JFreeChart chart = topHitsPlotPane.getChart(); + chart.getXYPlot().setDataset(PeriodAnalysis2DChartPane.DATA_SERIES, dataPlotModel); + chart.getXYPlot().setDataset(PeriodAnalysis2DChartPane.TOP_HIT_SERIES, topHitsPlotModel); + chart.getXYPlot().setRenderer(PeriodAnalysis2DChartPane.DATA_SERIES, + plotPane.getChart().getXYPlot().getRenderer()); + chart.getXYPlot().setDatasetRenderingOrder(DatasetRenderingOrder.REVERSE); + + plotPane = topHitsPlotPane; + + // Full results table + PeriodAnalysisCoordinateType[] columns = { PeriodAnalysisCoordinateType.FREQUENCY, + PeriodAnalysisCoordinateType.PERIOD, F_STATISTIC, P_VALUE }; + + PeriodAnalysisDataTableModel dataTableModel = new PeriodAnalysisDataTableModel(columns, + algorithm.getResultSeries()); + resultsTablePane = new NoModelPeriodAnalysisDataTablePane(dataTableModel, algorithm); + + PeriodAnalysisDataTableModel topHitsModel = new PeriodAnalysisDataTableModel(columns, + algorithm.getTopHits()); + topHitsTablePane = new AoVPeriodAnalysisTopHitsTablePane(obs, topHitsModel, dataTableModel, algorithm); + + // Return tabbed pane of plot and table components. + return PluginComponentFactory.createTabs(new NamedComponent("Periodogram", plotPane), + new NamedComponent("Results", resultsTablePane), new NamedComponent("Top Hits", topHitsTablePane)); + } + + // Send a period change message when the new-phase-plot button is + // clicked and enable the model button so a model can be created. + @Override + protected void newPhasePlotButtonAction() { + sendPeriodChangeMessage(period); + topHitsTablePane.setModelButtonState(true); + } + + // TODO: need the next two or use base class versipns? + + @Override + public void startup() { + Mediator.getInstance().getPeriodAnalysisSelectionNotifier().addListener(this); + + resultsTablePane.startup(); + topHitsTablePane.startup(); + plotPane.startup(); + } + + @Override + public void cleanup() { + Mediator.getInstance().getPeriodAnalysisSelectionNotifier().removeListenerIfWilling(this); + + resultsTablePane.cleanup(); + topHitsTablePane.cleanup(); + plotPane.cleanup(); + } + + // Next two methods are for Listener + + @Override + public boolean canBeRemoved() { + return false; + } + + @Override + public void update(PeriodAnalysisSelectionMessage info) { + period = info.getDataPoint().getPeriod(); + selectedDataPoint = info.getDataPoint(); + setNewPhasePlotButtonState(true); + } + + // ** No model result and top-hit panes ** + + class NoModelPeriodAnalysisDataTablePane extends PeriodAnalysisDataTablePane { + + public NoModelPeriodAnalysisDataTablePane(PeriodAnalysisDataTableModel model, + IPeriodAnalysisAlgorithm algorithm) { + super(model, algorithm); + } + + @Override + protected JPanel createButtonPanel() { + return new JPanel(); + } + + @Override + protected void enableButtons() { + // Do nothing + } + } + + class AoVPeriodAnalysisTopHitsTablePane extends PeriodAnalysisTopHitsTablePane { + + private List obs; + + public AoVPeriodAnalysisTopHitsTablePane(List obs, + PeriodAnalysisDataTableModel topHitsModel, PeriodAnalysisDataTableModel fullDataModel, + IPeriodAnalysisAlgorithm algorithm) { + super(topHitsModel, fullDataModel, algorithm); + this.obs = obs; + setModelButtonState(false); + } + + @Override + protected void enableButtons() { + // Override base class to not enable model button. + // The intent is to allow the dialog to which an instance + // of this class will exist to control whether the model + // button is enabled. + } + + public void setModelButtonState(boolean state) { + modelButton.setEnabled(state); + } + + @Override + protected void modelAction(List dataPoints) { + final JPanel parent = this; + + try { + // This will only be invoked when a phase plot has been created (see + // setModelButtonState()). + Mediator mediator = Mediator.getInstance(); + ObservationAndMeanPlotModel plotModel = mediator + .getObservationPlotModel(mediator.getAnalysisType()); + PiecewiseLinearModel model = new PiecewiseLinearModel(obs, plotModel.getMeanObsList()); + mediator.performModellingOperation(model); + } catch (Exception ex) { + MessageBox.showErrorDialog(parent, "Modelling", ex.getLocalizedMessage()); + } + } + } + + @Override + protected void findHarmonicsButtonAction() { + // Do nothing since we don't include a find-harmonics button for + // AoV. + } + } + + // The AoV algorithm implementation. + class AoVAlgorithm implements IPeriodAnalysisAlgorithm { + + private List obs; + + private List frequencies; + private ArrayList orderedFrequencies; + + private List periods; + private ArrayList orderedPeriods; + + private List fValues; + private ArrayList orderedFValues; + + private List pValues; + private ArrayList orderedPValues; + + // private double smallestFValue; + // private int smallestValueIndex; + + public AoVAlgorithm(List obs) { + this.obs = obs; + + frequencies = new ArrayList(); + orderedFrequencies = new ArrayList(); + + periods = new ArrayList(); + orderedPeriods = new ArrayList(); + + fValues = new ArrayList(); + orderedFValues = new ArrayList(); + + pValues = new ArrayList(); + orderedPValues = new ArrayList(); + + // smallestFValue = Double.MAX_VALUE; + // smallestValueIndex = 0; + } + + @Override + public String getRefineByFrequencyName() { + return null; + } + + @Override + public Map> getResultSeries() { + Map> results = new LinkedHashMap>(); + + results.put(PeriodAnalysisCoordinateType.FREQUENCY, frequencies); + results.put(PeriodAnalysisCoordinateType.PERIOD, periods); + results.put(F_STATISTIC, fValues); + results.put(P_VALUE, pValues); + + return results; + } + + @Override + public Map> getTopHits() { + // TODO: create top hits by sorting doubles in descending order + // pairs of doubles; + // limit to MAX_TOP_HITS = 100 + + Map> topHits = new LinkedHashMap>(); + + topHits.put(PeriodAnalysisCoordinateType.FREQUENCY, orderedFrequencies); + topHits.put(PeriodAnalysisCoordinateType.PERIOD, orderedPeriods); + topHits.put(F_STATISTIC, orderedFValues); + topHits.put(P_VALUE, orderedPValues); + + return topHits; + } + + @Override + public void multiPeriodicFit(List harmonics, PeriodAnalysisDerivedMultiPeriodicModel model) + throws AlgorithmError { + // TODO: msg box: unsupported + } + + @Override + public List refineByFrequency(List freqs, List variablePeriods, + List lockedPeriod) throws AlgorithmError { + return null; + } + + @Override + public void execute() throws AlgorithmError { + // Request parameters + // TODO: move this to top-level execute method and just pass actual + // parameters to this class? + while (!areParametersLegal(obs) && !cancelled) + ; + + if (!cancelled) { + // TODO: cache these by JD range between new star resets... + + interrupted = false; + + // Duplicate the obs (just JD and mag) so we can set phases + // without disturbing the original observation object. + // TODO: for a multi-threaded range-subset approach, we would + // need to do this once for each thread + List phObs = copyObs(obs); + + // Choose an epoch value. + double epoch = PhaseCalcs.epochStrategyMap.get("alpha").determineEpoch(phObs); + + // Iterate over the periods in the range at the specified + // resolution. + + // TODO: multi-core approach => iterate over a subset of the + // period range but over all observations, where the full set + // is copied for each core (set phases, sort mutate obs and + // list...); top-hits will have to be combined and ordered once + // at end as part of or before prune operation; instead, could + // just iterate over a subset of observations; this would only + // give a large speedup if many observations; such a for-loop + // unrolling would be simpler and less memory intensive though; + // may be worth trying first + + for (double period = minPeriod; period <= maxPeriod; period += resolution) { + if (interrupted) + break; + + PhaseCalcs.setPhases(phObs, epoch, period); + + Collections.sort(phObs, StandardPhaseComparator.instance); + + // Note: 1 / bins = 1 cycle divided into N bins + BinningResult binningResult = DescStats.createSymmetricBinnedObservations(phObs, + PhaseTimeElementEntity.instance, 1.0 / bins); + + // Collect results + // PMAK, Issue #152: + // Use fixInf() to prevent + // 'java.lang.IllegalArgumentException: Must be finite' + // error in AoV chart when period = 0 + frequencies.add(fixInf(1.0 / period)); + periods.add(period); + fValues.add(fixInf(binningResult.getFValue())); + pValues.add(fixInf(binningResult.getPValue())); + + updateOrderedValues(); + } + + pruneTopHits(); + } + } + + // replace +-Infinity by NaN + private double fixInf(double v) { + if (Double.isInfinite(v)) + return Double.NaN; + else + return v; + } + + private void updateOrderedValues() { + if (orderedFrequencies.isEmpty()) { + orderedFrequencies.add(frequencies.get(0)); + orderedPeriods.add(periods.get(0)); + orderedFValues.add(fValues.get(0)); + orderedPValues.add(pValues.get(0)); + } else { + int i = periods.size() - 1; + + double frequency = frequencies.get(i); + double period = periods.get(i); + double fValue = fValues.get(i); + double pValue = pValues.get(i); + + // Starting from highest fValue, find index to insert value + // and... + int index = 0; + for (int j = 0; j < orderedFValues.size(); j++) { + if (fValue < orderedFValues.get(j)) { + // Insertion index is one after the matched element's + // index since the list's elements are in descending + // order. + index++; + } + } + + // ...apply to all ordered collections. + if (index >= 0) { + orderedFrequencies.add(index, frequency); + orderedPeriods.add(index, period); + orderedFValues.add(index, fValue); + orderedPValues.add(index, pValue); + } else { + orderedFrequencies.add(0, frequency); + orderedPeriods.add(0, period); + orderedFValues.add(0, fValue); + orderedPValues.add(0, pValue); + } + } + } + + private void pruneTopHits() { + if (periods.size() > MAX_TOP_HITS) { + orderedFrequencies = new ArrayList(orderedFrequencies.subList(0, MAX_TOP_HITS)); + + orderedPeriods = new ArrayList(orderedPeriods.subList(0, MAX_TOP_HITS)); + + orderedFValues = new ArrayList(orderedFValues.subList(0, MAX_TOP_HITS)); + + orderedPValues = new ArrayList(orderedPValues.subList(0, MAX_TOP_HITS)); + } + } + + @Override + public void interrupt() { + interrupted = true; + } + } + + // Return a copy of the specified observation list + private List copyObs(List obs) { + List copiedObs = new ArrayList(); + + for (ValidObservation ob : obs) { + if (interrupted) + break; + + ValidObservation copiedOb = new ValidObservation(); + + double jd = ob.getDateInfo().getJulianDay(); + copiedOb.setDateInfo(new DateInfo(jd)); + + Magnitude mag = new Magnitude(ob.getMagnitude().getMagValue(), ob.getMagnitude().getUncertainty()); + copiedOb.setMagnitude(mag); + + copiedObs.add(copiedOb); + } + + return copiedObs; + } + + // Ask user for period min, max, resolution and number of bins. + private boolean areParametersLegal(List obs) { + legalParams = true; + + List> fields = new ArrayList>(); + + // / double days = obs.get(obs.size() - 1).getJD() - obs.get(0).getJD(); + DoubleField minPeriodField = new DoubleField("Minimum Period", 0.0, null, minPeriod); + fields.add(minPeriodField); + + DoubleField maxPeriodField = new DoubleField("Maximum Period", 0.0, null, maxPeriod); + fields.add(maxPeriodField); + + DoubleField resolutionField = new DoubleField("Resolution", 0.0, 1.0, resolution); + fields.add(resolutionField); + + IntegerField binsField = new IntegerField("Bins", 0, 50, bins); + fields.add(binsField); + + MultiEntryComponentDialog dlg = new MultiEntryComponentDialog("AoV Parameters", fields); + + cancelled = dlg.isCancelled(); + + if (!cancelled) { + + try { + bins = binsField.getValue(); + if (bins <= 0) { + MessageBox.showErrorDialog("AoV Parameters", "Number of bins must be greater than zero"); + legalParams = false; + } + } catch (Exception e) { + legalParams = false; + } + + minPeriod = minPeriodField.getValue(); + maxPeriod = maxPeriodField.getValue(); + resolution = resolutionField.getValue(); + + if (minPeriod >= maxPeriod) { + MessageBox.showErrorDialog("AoV Parameters", + "Minimum period must be less than or equal to maximum period"); + legalParams = false; + } + + if (resolution <= 0.0) { + MessageBox.showErrorDialog("AoV Parameters", "Resolution must be between 0 and 1"); + legalParams = false; + } + } + + return legalParams; + } + + @Override + public void interrupt() { + interrupted = true; + } + + @Override + protected void newStarAction(NewStarMessage message) { + reset(); + } + + @Override + public void reset() { + cancelled = false; + legalParams = false; + interrupted = false; + minPeriod = 0.0; + maxPeriod = 0.0; + resolution = 0.1; + bins = 10; + } } diff --git a/plugin/src/org/aavso/tools/vstar/external/plugin/ApacheCommonsLoessFitter.java b/plugin/src/org/aavso/tools/vstar/external/plugin/ApacheCommonsLoessFitter.java index ce576cf50..8d2c14994 100644 --- a/plugin/src/org/aavso/tools/vstar/external/plugin/ApacheCommonsLoessFitter.java +++ b/plugin/src/org/aavso/tools/vstar/external/plugin/ApacheCommonsLoessFitter.java @@ -18,7 +18,6 @@ package org.aavso.tools.vstar.external.plugin; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -35,9 +34,7 @@ import org.aavso.tools.vstar.ui.model.plot.JDCoordSource; import org.aavso.tools.vstar.ui.model.plot.StandardPhaseCoordSource; import org.aavso.tools.vstar.util.locale.LocaleProps; -import org.aavso.tools.vstar.util.model.IModel; -import org.aavso.tools.vstar.util.model.PeriodFitParameters; -import org.aavso.tools.vstar.util.prefs.NumericPrecisionPrefs; +import org.aavso.tools.vstar.util.model.AbstractModel; import org.apache.commons.math.MathException; import org.apache.commons.math.analysis.interpolation.LoessInterpolator; import org.apache.commons.math.analysis.polynomials.PolynomialFunction; @@ -52,311 +49,252 @@ */ public class ApacheCommonsLoessFitter extends ModelCreatorPluginBase { - public ApacheCommonsLoessFitter() { - super(); - } - - @Override - public String getDescription() { - return "Loess Fit"; - } - - @Override - public String getDisplayName() { - return getDescription(); - } - - /** - * @see org.aavso.tools.vstar.plugin.IPlugin#getDocName() - */ - @Override - public String getDocName() { - return "ApacheCommonsLoessFilter.pdf"; - } - - @Override - public IModel getModel(List obs) { - LoessFitCreator fitCreator = new LoessFitCreator(obs); - return fitCreator.createModel(); - } - - class LoessFitCreator { - private List obs; - - LoessFitCreator(List obs) { - this.obs = obs; - } - - // Create a model representing a polynomial fit of the requested degree. - IModel createModel() { - - // TODO: create a dialog to permit entry of params for other - // forms of ctor (Loess algorithm variants). - - return new IModel() { - boolean interrupted = false; - List fit; - List residuals; - PolynomialSplineFunction function; - Map functionStrMap = new LinkedHashMap(); - double aic = Double.NaN; - double bic = Double.NaN; - - // TODO: why am I not using this in model creation method!? (as - // I do for polyfit) - // final double zeroPoint = DescStats.calcTimeElementMean(obs, - // JDTimeElementEntity.instance); - - @Override - public String getDescription() { - return getKind() + " for " + obs.get(0).getBand() - + " series"; - } - - @Override - public List getFit() { - return fit; - } - - @Override - public String getKind() { - return "Loess Fit"; - } - - @Override - public List getParameters() { - // None for a Loess fit. - return null; - } - - @Override - public List getResiduals() { - return residuals; - } - - @Override - public boolean hasFuncDesc() { - return true; - } - - public String toString() { - String strRepr = functionStrMap.get(LocaleProps - .get("MODEL_INFO_FUNCTION_TITLE")); - - if (strRepr == null) { - /* - strRepr = "f(t:real) : real {\n"; - - double constCoeff = 0; - - for (PolynomialFunction f : function.getPolynomials()) { - double[] coeffs = f.getCoefficients(); - for (int i = coeffs.length - 1; i >= 1; i--) { - strRepr += " " + NumericPrecisionPrefs.formatPolyCoef(coeffs[i]); - strRepr += "*t^" + i + "+\n"; - } - constCoeff += coeffs[0]; - } - strRepr += " " + NumericPrecisionPrefs.formatPolyCoef(constCoeff); - strRepr += "\n}"; - */ - strRepr = Mediator.NOT_IMPLEMENTED_YET; - } - - return strRepr; - } - - public String toExcelString() { - String strRepr = functionStrMap.get(LocaleProps - .get("MODEL_INFO_EXCEL_TITLE")); - - if (strRepr == null) { - /* - strRepr = "=SUM("; - - double constCoeff = 0; - - for (PolynomialFunction f : function.getPolynomials()) { - double[] coeffs = f.getCoefficients(); - for (int i = coeffs.length - 1; i >= 1; i--) { - strRepr += NumericPrecisionPrefs.formatPolyCoef(coeffs[i]); - strRepr += "*A1^" + i + NumericPrecisionPrefs.getExcelFormulaSeparator() + "\n"; - } - constCoeff += coeffs[0]; - } - - strRepr += NumericPrecisionPrefs.formatPolyCoef(constCoeff) + ")"; - */ - strRepr = Mediator.NOT_IMPLEMENTED_YET; - } - - return strRepr; - } - - // Note: There is already a Loess fit function in R, so it - // would be interesting to compare the results of that and this - // plugin. - // toRString must be locale-independent! - public String toRString() { - String strRepr = functionStrMap.get(LocaleProps - .get("MODEL_INFO_R_TITLE")); - - if (strRepr == null) { - /* - strRepr = "model <- function(t)\n"; - - double constCoeff = 0; - - for (PolynomialFunction f : function.getPolynomials()) { - double[] coeffs = f.getCoefficients(); - for (int i = coeffs.length - 1; i >= 1; i--) { - strRepr += NumericPrecisionPrefs.formatPolyCoefLocaleIndependent(coeffs[i]); - strRepr += "*t^" + i + "+\n"; - } - constCoeff += coeffs[0]; - } - - strRepr += NumericPrecisionPrefs.formatPolyCoefLocaleIndependent(constCoeff); - */ - strRepr = Mediator.NOT_IMPLEMENTED_YET; - } - - return strRepr; - } - - @Override - public ContinuousModelFunction getModelFunction() { - return new ContinuousModelFunction(function, fit); - } - - @Override - public void execute() throws AlgorithmError { - - // The Loess fitter requires a strictly increasing sequence - // on the domain (i.e. JD values), i.e. no duplicates. - Map jdToMagMap = new TreeMap(); - - for (int i = 0; i < obs.size(); i++) { - ValidObservation ob = obs.get(i); - // This means that the last magnitude for a JD wins! - jdToMagMap.put(ob.getJD(), ob.getMag()); - } - - double[] xvals = new double[jdToMagMap.size()]; - double[] yvals = new double[jdToMagMap.size()]; - - int index = 0; - for (Double jd : jdToMagMap.keySet()) { - xvals[index] = jd; - yvals[index++] = jdToMagMap.get(jd); - } - - try { - final LoessInterpolator interpolator = new LoessInterpolator(); - function = interpolator.interpolate(xvals, yvals); - - fit = new ArrayList(); - residuals = new ArrayList(); - double sumSqResiduals = 0; - - String comment = "From Loess fit"; - - // Create fit and residual observations and - // compute the sum of squares of residuals for - // Akaike and Bayesean Information Criteria. - for (int i = 0; i < xvals.length && !interrupted; i++) { - double jd = xvals[i]; - double mag = yvals[i]; - - double y = function.value(jd); - - ValidObservation fitOb = new ValidObservation(); - fitOb.setDateInfo(new DateInfo(jd)); - fitOb.setMagnitude(new Magnitude(y, 0)); - fitOb.setBand(SeriesType.Model); - fitOb.setComments(comment); - fit.add(fitOb); - - ValidObservation resOb = new ValidObservation(); - resOb.setDateInfo(new DateInfo(jd)); - double residual = mag - y; - resOb.setMagnitude(new Magnitude(residual, 0)); - resOb.setBand(SeriesType.Residuals); - resOb.setComments(comment); - residuals.add(resOb); - - sumSqResiduals += (residual * residual); - } - - // TODO: what to use for degree (or N) here? - double degree = 0; - - for (PolynomialFunction f : function.getPolynomials()) { - degree += f.getCoefficients().length; - } - - // Fit metrics (AIC, BIC). - int n = residuals.size(); - if (n != 0 && sumSqResiduals / n != 0) { - double commonIC = n * Math.log(sumSqResiduals / n); - aic = commonIC + 2 * degree; - bic = commonIC + degree * Math.log(n); - } - - ICoordSource timeCoordSource = null; - switch (Mediator.getInstance().getAnalysisType()) { - case RAW_DATA: - timeCoordSource = JDCoordSource.instance; - break; - - case PHASE_PLOT: - timeCoordSource = StandardPhaseCoordSource.instance; - break; - } - - // Minimum/maximum. - // TODO: use derivative approach - // ApacheCommonsBrentOptimiserExtremaFinder finder = new - // ApacheCommonsBrentOptimiserExtremaFinder( - // fit, function, timeCoordSource, 0); - // - // String extremaStr = finder.toString(); - // - // if (extremaStr != null) { - // String title = LocaleProps - // .get("MODEL_INFO_EXTREMA_TITLE"); - // - // functionStrMap.put(title, extremaStr); - // } - - // Excel, R equations. - // TODO: consider Python, e.g. for use with matplotlib. - // functionStrMap.put("Function", toString()); - functionStrMap.put( - LocaleProps.get("MODEL_INFO_FUNCTION_TITLE"), - toString()); - functionStrMap.put( - LocaleProps.get("MODEL_INFO_EXCEL_TITLE"), - toExcelString()); - functionStrMap.put( - LocaleProps.get("MODEL_INFO_R_TITLE"), - toRString()); - - } catch (MathException e) { - throw new AlgorithmError(e.getLocalizedMessage()); - } - } - - @Override - public void interrupt() { - interrupted = true; - } - - @Override - public Map getFunctionStrings() { - return functionStrMap; - } - }; - } - } + public ApacheCommonsLoessFitter() { + super(); + } + + @Override + public String getDescription() { + return "Loess Fit"; + } + + @Override + public String getDisplayName() { + return getDescription(); + } + + /** + * @see org.aavso.tools.vstar.plugin.IPlugin#getDocName() + */ + @Override + public String getDocName() { + return "ApacheCommonsLoessFilter.pdf"; + } + + @Override + public AbstractModel getModel(List obs) { + return new LoessFitCreator(obs); + } + + class LoessFitCreator extends AbstractModel { + + LoessFitCreator(List obs) { + super(obs); + } + + // TODO: create a dialog to permit entry of params for other + // forms of ctor (Loess algorithm variants). + + PolynomialSplineFunction function; + double aic = Double.NaN; + double bic = Double.NaN; + + @Override + public String getDescription() { + return getKind() + " for " + obs.get(0).getBand() + " series"; + } + + @Override + public String getKind() { + return "Loess Fit"; + } + + @Override + public boolean hasFuncDesc() { + return false; + } + + @Override + public String toString() { + return toVeLaString(); + } + + @Override + public String toVeLaString() { + String strRepr = functionStrMap.get(LocaleProps.get("MODEL_INFO_FUNCTION_TITLE")); + + if (strRepr == null) { + /* + * strRepr = "f(t:real) : real {\n"; + * + * double constCoeff = 0; + * + * for (PolynomialFunction f : function.getPolynomials()) { double[] coeffs = + * f.getCoefficients(); for (int i = coeffs.length - 1; i >= 1; i--) { strRepr + * += " " + NumericPrecisionPrefs.formatPolyCoef(coeffs[i]); strRepr += "*t^" + * + i + "+\n"; } constCoeff += coeffs[0]; } strRepr += " " + + * NumericPrecisionPrefs.formatPolyCoef(constCoeff); strRepr += "\n}"; + */ + strRepr = Mediator.NOT_IMPLEMENTED_YET; + } + + return strRepr; + } + + public String toExcelString() { + String strRepr = functionStrMap.get(LocaleProps.get("MODEL_INFO_EXCEL_TITLE")); + + if (strRepr == null) { + /* + * strRepr = "=SUM("; + * + * double constCoeff = 0; + * + * for (PolynomialFunction f : function.getPolynomials()) { double[] coeffs = + * f.getCoefficients(); for (int i = coeffs.length - 1; i >= 1; i--) { strRepr + * += NumericPrecisionPrefs.formatPolyCoef(coeffs[i]); strRepr += "*A1^" + i + + * NumericPrecisionPrefs.getExcelFormulaSeparator() + "\n"; } constCoeff += + * coeffs[0]; } + * + * strRepr += NumericPrecisionPrefs.formatPolyCoef(constCoeff) + ")"; + */ + strRepr = Mediator.NOT_IMPLEMENTED_YET; + } + + return strRepr; + } + + // Note: There is already a Loess fit function in R, so it + // would be interesting to compare the results of that and this + // plugin. + // toRString must be locale-independent! + public String toRString() { + String strRepr = functionStrMap.get(LocaleProps.get("MODEL_INFO_R_TITLE")); + + if (strRepr == null) { + /* + * strRepr = "model <- function(t)\n"; + * + * double constCoeff = 0; + * + * for (PolynomialFunction f : function.getPolynomials()) { double[] coeffs = + * f.getCoefficients(); for (int i = coeffs.length - 1; i >= 1; i--) { strRepr + * += NumericPrecisionPrefs.formatPolyCoefLocaleIndependent(coeffs[i]); strRepr + * += "*t^" + i + "+\n"; } constCoeff += coeffs[0]; } + * + * strRepr += NumericPrecisionPrefs.formatPolyCoefLocaleIndependent(constCoeff); + */ + strRepr = Mediator.NOT_IMPLEMENTED_YET; + } + + return strRepr; + } + + @Override + public ContinuousModelFunction getModelFunction() { + return new ContinuousModelFunction(function, fit); + } + + @Override + public void execute() throws AlgorithmError { + + // The Loess fitter requires a strictly increasing sequence + // on the domain (i.e. JD values), i.e. no duplicates. + Map jdToMagMap = new TreeMap(); + + for (int i = 0; i < obs.size(); i++) { + ValidObservation ob = obs.get(i); + // This means that the last magnitude for a JD wins! + jdToMagMap.put(ob.getJD(), ob.getMag()); + } + + double[] xvals = new double[jdToMagMap.size()]; + double[] yvals = new double[jdToMagMap.size()]; + + int index = 0; + for (Double jd : jdToMagMap.keySet()) { + xvals[index] = jd; + yvals[index++] = jdToMagMap.get(jd); + } + + try { + final LoessInterpolator interpolator = new LoessInterpolator(); + function = interpolator.interpolate(xvals, yvals); + + fit = new ArrayList(); + residuals = new ArrayList(); + double sumSqResiduals = 0; + + String comment = "From Loess fit"; + + // Create fit and residual observations and + // compute the sum of squares of residuals for + // Akaike and Bayesean Information Criteria. + for (int i = 0; i < xvals.length && !interrupted; i++) { + double jd = xvals[i]; + double mag = yvals[i]; + + double y = function.value(jd); + + ValidObservation fitOb = new ValidObservation(); + fitOb.setDateInfo(new DateInfo(jd)); + fitOb.setMagnitude(new Magnitude(y, 0)); + fitOb.setBand(SeriesType.Model); + fitOb.setComments(comment); + fit.add(fitOb); + + ValidObservation resOb = new ValidObservation(); + resOb.setDateInfo(new DateInfo(jd)); + double residual = mag - y; + resOb.setMagnitude(new Magnitude(residual, 0)); + resOb.setBand(SeriesType.Residuals); + resOb.setComments(comment); + residuals.add(resOb); + + sumSqResiduals += (residual * residual); + } + + // TODO: what to use for degree (or N) here? + double degree = 0; + + for (PolynomialFunction f : function.getPolynomials()) { + degree += f.getCoefficients().length; + } + + // Fit metrics (AIC, BIC). + int n = residuals.size(); + if (n != 0 && sumSqResiduals / n != 0) { + double commonIC = n * Math.log(sumSqResiduals / n); + aic = commonIC + 2 * degree; + bic = commonIC + degree * Math.log(n); + } + + ICoordSource timeCoordSource = null; + switch (Mediator.getInstance().getAnalysisType()) { + case RAW_DATA: + timeCoordSource = JDCoordSource.instance; + break; + + case PHASE_PLOT: + timeCoordSource = StandardPhaseCoordSource.instance; + break; + } + + // Minimum/maximum. + // TODO: use derivative approach + // ApacheCommonsBrentOptimiserExtremaFinder finder = new + // ApacheCommonsBrentOptimiserExtremaFinder( + // fit, function, timeCoordSource, 0); + // + // String extremaStr = finder.toString(); + // + // if (extremaStr != null) { + // String title = LocaleProps + // .get("MODEL_INFO_EXTREMA_TITLE"); + // + // functionStrMap.put(title, extremaStr); + // } + + // Excel, R equations. + // TODO: consider Python, e.g. for use with matplotlib. + // functionStrMap.put("Function", toString()); + functionStrMap.put(LocaleProps.get("MODEL_INFO_FUNCTION_TITLE"), toString()); + functionStrMap.put(LocaleProps.get("MODEL_INFO_EXCEL_TITLE"), toExcelString()); + functionStrMap.put(LocaleProps.get("MODEL_INFO_R_TITLE"), toRString()); + + } catch (MathException e) { + throw new AlgorithmError(e.getLocalizedMessage()); + } + } + } } diff --git a/plugin/src/org/aavso/tools/vstar/external/plugin/FourierModelCreator.java b/plugin/src/org/aavso/tools/vstar/external/plugin/FourierModelCreator.java index f3263a5c7..3b2fbf3f1 100644 --- a/plugin/src/org/aavso/tools/vstar/external/plugin/FourierModelCreator.java +++ b/plugin/src/org/aavso/tools/vstar/external/plugin/FourierModelCreator.java @@ -21,7 +21,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import org.aavso.tools.vstar.data.ValidObservation; import org.aavso.tools.vstar.plugin.ModelCreatorPluginBase; diff --git a/plugin/src/org/aavso/tools/vstar/external/plugin/PiecewiseLinearMeanSeriesModel.java b/plugin/src/org/aavso/tools/vstar/external/plugin/PiecewiseLinearMeanSeriesModel.java new file mode 100644 index 000000000..a8d76b901 --- /dev/null +++ b/plugin/src/org/aavso/tools/vstar/external/plugin/PiecewiseLinearMeanSeriesModel.java @@ -0,0 +1,58 @@ +/** + * VStar: a statistical analysis tool for variable star data. + * Copyright (C) 2009 AAVSO (http://www.aavso.org/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.aavso.tools.vstar.external.plugin; + +import java.util.List; + +import org.aavso.tools.vstar.data.ValidObservation; +import org.aavso.tools.vstar.external.lib.PiecewiseLinearModel; +import org.aavso.tools.vstar.plugin.ModelCreatorPluginBase; +import org.aavso.tools.vstar.ui.mediator.Mediator; +import org.aavso.tools.vstar.ui.model.plot.ObservationAndMeanPlotModel; +import org.aavso.tools.vstar.util.model.AbstractModel; + +/** + * This plug-in creates a piecewise linear model from the current means series. + */ +public class PiecewiseLinearMeanSeriesModel extends ModelCreatorPluginBase { + + private final String DESC = "Piecewise linear model from Means"; + + public PiecewiseLinearMeanSeriesModel() { + super(); + } + + @Override + public AbstractModel getModel(List obs) { + // Get the mean observation list for the current mode + Mediator mediator = Mediator.getInstance(); + ObservationAndMeanPlotModel plotModel = mediator.getObservationPlotModel(mediator.getAnalysisType()); + + return new PiecewiseLinearModel(obs, plotModel.getMeanObsList()); + } + + @Override + public String getDescription() { + return DESC; + } + + @Override + public String getDisplayName() { + return DESC; + } +} \ No newline at end of file diff --git a/plugin/src/org/aavso/tools/vstar/external/plugin/VeLaModelCreator.java b/plugin/src/org/aavso/tools/vstar/external/plugin/VeLaModelCreator.java index 1f055f7a0..ac0ed5432 100644 --- a/plugin/src/org/aavso/tools/vstar/external/plugin/VeLaModelCreator.java +++ b/plugin/src/org/aavso/tools/vstar/external/plugin/VeLaModelCreator.java @@ -2,10 +2,7 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -19,17 +16,11 @@ import org.aavso.tools.vstar.ui.mediator.AnalysisType; import org.aavso.tools.vstar.ui.mediator.Mediator; import org.aavso.tools.vstar.ui.model.plot.ContinuousModelFunction; -import org.aavso.tools.vstar.ui.model.plot.ICoordSource; -import org.aavso.tools.vstar.ui.model.plot.JDCoordSource; -import org.aavso.tools.vstar.ui.model.plot.StandardPhaseCoordSource; import org.aavso.tools.vstar.ui.vela.VeLaDialog; import org.aavso.tools.vstar.util.ApacheCommonsDerivativeBasedExtremaFinder; import org.aavso.tools.vstar.util.Tolerance; -import org.aavso.tools.vstar.util.comparator.JDComparator; -import org.aavso.tools.vstar.util.comparator.StandardPhaseComparator; import org.aavso.tools.vstar.util.locale.LocaleProps; -import org.aavso.tools.vstar.util.model.IModel; -import org.aavso.tools.vstar.util.model.PeriodFitParameters; +import org.aavso.tools.vstar.util.model.AbstractModel; import org.aavso.tools.vstar.util.prefs.NumericPrecisionPrefs; import org.aavso.tools.vstar.vela.Operand; import org.aavso.tools.vstar.vela.Type; @@ -44,375 +35,286 @@ */ public class VeLaModelCreator extends ModelCreatorPluginBase { - private static final String FUNC_NAME = "F"; - private static final String DERIV_FUNC_NAME = "DF"; - private static final String RESOLUTION_VAR = "RESOLUTION"; - - private static VeLaDialog velaDialog; - - private ICoordSource timeCoordSource; - private Comparator timeComparator; - - public VeLaModelCreator() { - super(); - } - - @Override - public String getDescription() { - return "VeLa model creator"; - } - - @Override - public String getDisplayName() { - return "VeLa Model"; - } - - /** - * @see org.aavso.tools.vstar.plugin.IPlugin#getDocName() - */ - @Override - public String getDocName() { - return "VeLa Model Creator Plug-In.pdf"; - } - - @Override - public IModel getModel(List obs) { - VeLaModel velaModel = new VeLaModel(obs); - return velaModel.createModel(); - } - - class VeLaUnivariateRealFunction implements - DifferentiableUnivariateRealFunction { - - private VeLaInterpreter vela; - private String funcName; - - public VeLaUnivariateRealFunction(VeLaInterpreter vela, String funcName) { - this.vela = vela; - this.funcName = funcName; - } - - /** - * Return the value of the model function or its derivative. - * - * @param t - * The time value. - * @return The model value at time t. - * @throws FunctionEvaluationException - * If there is an error during function evaluation. - */ - @Override - public double value(double t) throws FunctionEvaluationException { - String funCall = funcName + "(" + NumericPrecisionPrefs.formatTime(t) + ")"; - Optional result = vela.program(funCall); - if (result.isPresent()) { - return result.get().doubleVal(); - } else { - throw new FunctionEvaluationException(t); - } - } - - /** - * If the derivative (df) function doesn't exist, this will never be - * called since we will bypass extrema determination. - */ - @Override - public UnivariateRealFunction derivative() { - return new VeLaUnivariateRealFunction(vela, DERIV_FUNC_NAME); - } - } - - class VeLaModel { - private List obs; - private double zeroPoint; - private VeLaInterpreter vela; - - VeLaModel(List obs) { - // Create a VeLa interpreter instance. - vela = new VeLaInterpreter(); - - // Select time mode (JD or phase). - switch (Mediator.getInstance().getAnalysisType()) { - case RAW_DATA: - timeCoordSource = JDCoordSource.instance; - timeComparator = JDComparator.instance; - this.obs = obs; - // zeroPoint = DescStats.calcTimeElementMean(obs, - // JDTimeElementEntity.instance); - zeroPoint = 0; - List jdList = obs.stream() - .map(ob -> new Operand(Type.REAL, ob.getJD())) - .collect(Collectors.toList()); - vela.bind("TIMES", new Operand(Type.LIST, jdList), true); - break; - - case PHASE_PLOT: - timeCoordSource = StandardPhaseCoordSource.instance; - timeComparator = StandardPhaseComparator.instance; - this.obs = new ArrayList(obs); - Collections.sort(this.obs, timeComparator); - zeroPoint = 0; - List phaseList = this.obs - .stream() - .map(ob -> new Operand(Type.REAL, ob.getStandardPhase())) - .collect(Collectors.toList()); - vela.bind("TIMES", new Operand(Type.LIST, phaseList), true); - break; - } - - List magList = this.obs.stream() - .map(ob -> new Operand(Type.REAL, ob.getMag())) - .collect(Collectors.toList()); - Operand mags = new Operand(Type.LIST, magList); - vela.bind("MAGS", mags, true); - } - - // Create a VeLa model. - IModel createModel() { - IModel model = null; - String modelFuncStr = null; - String modelNameStr = null; - boolean ok = false; - - if (inTestMode()) { - modelFuncStr = getModelFunc(); - modelNameStr = getModelName(); - ok = true; - } else { - if (velaDialog == null) { - velaDialog = new VeLaDialog( - "Function Code [model: f(t), optional derivative: df(t)]"); - } else { - velaDialog.showDialog(); - } - - if (!velaDialog.isCancelled()) { - modelFuncStr = velaDialog.getCode(); - modelNameStr = velaDialog.getPath(); - ok = true; - } - } - - if (ok) { - String velaModelFunctionStr = modelFuncStr; - String modelName = modelNameStr; - // double resolution = resolutionField.getValue(); - - model = new IModel() { - boolean interrupted = false; - List fit; - List residuals; - UnivariateRealFunction function; - Map functionStrMap = new LinkedHashMap(); - - @Override - public String getDescription() { - return modelName + " applied to " - + obs.get(0).getBand() + " series"; - } - - @Override - public List getFit() { - return fit; - } - - @Override - public List getResiduals() { - return residuals; - } - - @Override - public String getKind() { - return "VeLa Model"; - } - - @Override - public List getParameters() { - // None for a VeLa model. - return null; - } - - @Override - public boolean hasFuncDesc() { - return true; - } - - @Override - public String toString() { - return velaModelFunctionStr; - } - - @Override - public ContinuousModelFunction getModelFunction() { - return new ContinuousModelFunction(function, fit, - zeroPoint); - } - - @Override - public void execute() throws AlgorithmError { - if (!interrupted) { - try { - // Evaluate the VeLa model code. - // A univariate function f(t:real):real is - // assumed to exist after this completes. - vela.program(velaModelFunctionStr); - - String funcName = FUNC_NAME; - - // Has a model function been defined? - if (!vela.lookupFunctions(FUNC_NAME) - .isPresent()) { - MessageBox.showErrorDialog( - "VeLa Model Error", - "f(t:real):real undefined"); - } else { - function = new VeLaUnivariateRealFunction( - vela, funcName); - - fit = new ArrayList(); - residuals = new ArrayList(); - - String comment = "\n" - + velaModelFunctionStr; - - // Create fit and residual observations. - for (int i = 0; i < obs.size() - && !interrupted; i++) { - ValidObservation ob = obs.get(i); - - // Push an environment that makes the - // observation available to VeLa code. - vela.pushEnvironment(new VeLaValidObservationEnvironment( - ob)); - - double x = timeCoordSource.getXCoord(i, - obs); - - // double zeroedX = x - zeroPoint; - double y = function.value(x); - - ValidObservation fitOb = new ValidObservation(); - fitOb.setDateInfo(new DateInfo(ob - .getJD())); - if (Mediator.getInstance() - .getAnalysisType() == AnalysisType.PHASE_PLOT) { - fitOb.setPreviousCyclePhase(ob - .getPreviousCyclePhase()); - fitOb.setStandardPhase(ob - .getStandardPhase()); - } - fitOb.setMagnitude(new Magnitude(y, 0)); - fitOb.setBand(SeriesType.Model); - fitOb.setComments(comment); - fit.add(fitOb); - - ValidObservation resOb = new ValidObservation(); - resOb.setDateInfo(new DateInfo(ob - .getJD())); - if (Mediator.getInstance() - .getAnalysisType() == AnalysisType.PHASE_PLOT) { - resOb.setPreviousCyclePhase(ob - .getPreviousCyclePhase()); - resOb.setStandardPhase(ob - .getStandardPhase()); - } - double residual = ob.getMag() - y; - resOb.setMagnitude(new Magnitude( - residual, 0)); - resOb.setBand(SeriesType.Residuals); - resOb.setComments(comment); - residuals.add(resOb); - - // Pop the observation environment. - vela.popEnvironment(); - } - - functionStrMap.put(LocaleProps - .get("MODEL_INFO_FUNCTION_TITLE"), - toString()); - - // Has a derivative function been defined? - // If so, carry out extrema determination. - if (vela.lookupFunctions(DERIV_FUNC_NAME) - .isPresent()) { - // Use a real VeLa resolution variable - // if it exists, else use a value of - // 0.1. - double resolution = 0.1; - Optional resVar = vela - .lookupBinding(RESOLUTION_VAR); - if (resVar.isPresent()) { - switch (resVar.get().getType()) { - case REAL: - resolution = resVar.get() - .doubleVal(); - break; - case INTEGER: - resolution = resVar.get() - .intVal(); - break; - default: - MessageBox - .showErrorDialog( - "VeLa Model Error", - "Resolution must be numeric"); - break; - } - } - - ApacheCommonsDerivativeBasedExtremaFinder finder = new ApacheCommonsDerivativeBasedExtremaFinder( - fit, - (DifferentiableUnivariateRealFunction) function, - timeCoordSource, zeroPoint, - resolution); - - String extremaStr = finder.toString(); - - if (extremaStr != null) { - String title = LocaleProps - .get("MODEL_INFO_EXTREMA_TITLE"); - - functionStrMap.put(title, - extremaStr); - } - } - } - } catch (FunctionEvaluationException e) { - throw new AlgorithmError( - e.getLocalizedMessage()); - } - } - } - - @Override - public void interrupt() { - interrupted = true; - } - - @Override - public Map getFunctionStrings() { - return functionStrMap; - } - }; - } - - return model; - } - } - - // Plug-in test + private static final String FUNC_NAME = "F"; + private static final String DERIV_FUNC_NAME = "DF"; + private static final String RESOLUTION_VAR = "RESOLUTION"; + + private static VeLaDialog velaDialog; + + public VeLaModelCreator() { + super(); + } + + @Override + public String getDescription() { + return "VeLa model creator"; + } + + @Override + public String getDisplayName() { + return "VeLa Model"; + } + + /** + * @see org.aavso.tools.vstar.plugin.IPlugin#getDocName() + */ + @Override + public String getDocName() { + return "VeLa Model Creator Plug-In.pdf"; + } + + @Override + public AbstractModel getModel(List obs) { + return new VeLaModel(obs); + } + + class VeLaUnivariateRealFunction implements DifferentiableUnivariateRealFunction { + + private VeLaInterpreter vela; + private String funcName; + + public VeLaUnivariateRealFunction(VeLaInterpreter vela, String funcName) { + this.vela = vela; + this.funcName = funcName; + } + + /** + * Return the value of the model function or its derivative. + * + * @param t The time value. + * @return The model value at time t. + * @throws FunctionEvaluationException If there is an error during function + * evaluation. + */ + @Override + public double value(double t) throws FunctionEvaluationException { + String funCall = funcName + "(" + NumericPrecisionPrefs.formatTime(t) + ")"; + Optional result = vela.program(funCall); + if (result.isPresent()) { + return result.get().doubleVal(); + } else { + throw new FunctionEvaluationException(t); + } + } + + /** + * If the derivative (df) function doesn't exist, this will never be called + * since we will bypass extrema determination. + */ + @Override + public UnivariateRealFunction derivative() { + return new VeLaUnivariateRealFunction(vela, DERIV_FUNC_NAME); + } + } + + class VeLaModel extends AbstractModel { + double zeroPoint; + UnivariateRealFunction function; + VeLaInterpreter vela; + String velaModelFunctionStr; + String modelName; + + VeLaModel(List obs) { + super(obs); + + // Create a VeLa interpreter instance. + vela = new VeLaInterpreter(); + + // Select time mode (JD or phase). + switch (Mediator.getInstance().getAnalysisType()) { + case RAW_DATA: + zeroPoint = 0; + List jdList = obs.stream().map(ob -> new Operand(Type.REAL, ob.getJD())) + .collect(Collectors.toList()); + vela.bind("TIMES", new Operand(Type.LIST, jdList), true); + break; + + case PHASE_PLOT: + Collections.sort(this.obs, timeComparator); + zeroPoint = 0; + List phaseList = this.obs.stream().map(ob -> new Operand(Type.REAL, ob.getStandardPhase())) + .collect(Collectors.toList()); + vela.bind("TIMES", new Operand(Type.LIST, phaseList), true); + break; + } + + List magList = this.obs.stream().map(ob -> new Operand(Type.REAL, ob.getMag())) + .collect(Collectors.toList()); + Operand mags = new Operand(Type.LIST, magList); + vela.bind("MAGS", mags, true); + + String modelFuncStr = null; + String modelNameStr = null; + + if (inTestMode()) { + modelFuncStr = getTestModelFunc(); + modelNameStr = getTestModelName(); + } else { + if (velaDialog == null) { + velaDialog = new VeLaDialog("Function Code [model: f(t), optional derivative: df(t)]"); + } else { + velaDialog.showDialog(); + } + + if (!velaDialog.isCancelled()) { + modelFuncStr = velaDialog.getCode(); + modelNameStr = velaDialog.getPath(); + } + } + + velaModelFunctionStr = modelFuncStr; + modelName = modelNameStr; + } + + @Override + public String getDescription() { + return modelName + " applied to " + obs.get(0).getBand() + " series"; + } + + @Override + public String getKind() { + return "VeLa Model"; + } + + @Override + public boolean hasFuncDesc() { + return true; + } + + @Override + public String toString() { + return toVeLaString(); + } + + @Override + public String toVeLaString() { + return velaModelFunctionStr; + } + + @Override + public ContinuousModelFunction getModelFunction() { + return new ContinuousModelFunction(function, fit, zeroPoint); + } + + @Override + public void execute() throws AlgorithmError { + if (!interrupted) { + try { + // Evaluate the VeLa model code. + // A univariate function f(t:real):real is + // assumed to exist after this completes. + vela.program(velaModelFunctionStr); + + String funcName = FUNC_NAME; + + // Has a model function been defined? + if (!vela.lookupFunctions(FUNC_NAME).isPresent()) { + MessageBox.showErrorDialog("VeLa Model Error", "f(t:real):real undefined"); + } else { + function = new VeLaUnivariateRealFunction(vela, funcName); + + fit = new ArrayList(); + residuals = new ArrayList(); + + String comment = "\n" + velaModelFunctionStr; + + // Create fit and residual observations. + for (int i = 0; i < obs.size() && !interrupted; i++) { + ValidObservation ob = obs.get(i); + + // Push an environment that makes the + // observation available to VeLa code. + vela.pushEnvironment(new VeLaValidObservationEnvironment(ob)); + + double x = timeCoordSource.getXCoord(i, obs); + + // double zeroedX = x - zeroPoint; + double y = function.value(x); + + ValidObservation fitOb = new ValidObservation(); + fitOb.setDateInfo(new DateInfo(ob.getJD())); + if (Mediator.getInstance().getAnalysisType() == AnalysisType.PHASE_PLOT) { + fitOb.setPreviousCyclePhase(ob.getPreviousCyclePhase()); + fitOb.setStandardPhase(ob.getStandardPhase()); + } + fitOb.setMagnitude(new Magnitude(y, 0)); + fitOb.setBand(SeriesType.Model); + fitOb.setComments(comment); + fit.add(fitOb); + + ValidObservation resOb = new ValidObservation(); + resOb.setDateInfo(new DateInfo(ob.getJD())); + if (Mediator.getInstance().getAnalysisType() == AnalysisType.PHASE_PLOT) { + resOb.setPreviousCyclePhase(ob.getPreviousCyclePhase()); + resOb.setStandardPhase(ob.getStandardPhase()); + } + double residual = ob.getMag() - y; + resOb.setMagnitude(new Magnitude(residual, 0)); + resOb.setBand(SeriesType.Residuals); + resOb.setComments(comment); + residuals.add(resOb); + + // Pop the observation environment. + vela.popEnvironment(); + } + + functionStrMap.put(LocaleProps.get("MODEL_INFO_FUNCTION_TITLE"), toString()); + + // Has a derivative function been defined? + // If so, carry out extrema determination. + if (vela.lookupFunctions(DERIV_FUNC_NAME).isPresent()) { + // Use a real VeLa resolution variable + // if it exists, else use a value of + // 0.1. + double resolution = 0.1; + Optional resVar = vela.lookupBinding(RESOLUTION_VAR); + if (resVar.isPresent()) { + switch (resVar.get().getType()) { + case REAL: + resolution = resVar.get().doubleVal(); + break; + case INTEGER: + resolution = resVar.get().intVal(); + break; + default: + MessageBox.showErrorDialog("VeLa Model Error", "Resolution must be numeric"); + break; + } + } + + ApacheCommonsDerivativeBasedExtremaFinder finder = new ApacheCommonsDerivativeBasedExtremaFinder( + fit, (DifferentiableUnivariateRealFunction) function, timeCoordSource, zeroPoint, + resolution); + + String extremaStr = finder.toString(); + + if (extremaStr != null) { + String title = LocaleProps.get("MODEL_INFO_EXTREMA_TITLE"); + + functionStrMap.put(title, extremaStr); + } + } + } + } catch (FunctionEvaluationException e) { + throw new AlgorithmError(e.getLocalizedMessage()); + } + } + } + } + + // Plug-in test @Override public Boolean test() { boolean success = true; - + setTestMode(true); - + try { - IModel model = getModel(createObs()); + AbstractModel model = getModel(createObs()); model.execute(); success &= model.hasFuncDesc(); - String desc = getModelName() + " applied to Visual series"; + String desc = getTestModelName() + " applied to Visual series"; success &= model.getDescription().equals(desc); success &= !model.getFit().isEmpty(); success &= !model.getResiduals().isEmpty(); @@ -426,8 +328,8 @@ public Boolean test() { return success; } - - private String getModelFunc() { + + private String getTestModelFunc() { String func = ""; func += "f(t:real) : real {\n"; @@ -435,14 +337,14 @@ private String getModelFunc() { func += " -0.6588158 * cos(2*PI*0.0017177*(t-2451700))\n"; func += " +1.3908874 * sin(2*PI*0.0017177*(t-2451700))"; func += "}\n"; - + return func; } - - private String getModelName() { + + private String getTestModelName() { return "test model"; } - + private List createObs() { List obs = new ArrayList(); diff --git a/plugin/test/org/aavso/tools/vstar/external/plugin/AllTests.java b/plugin/test/org/aavso/tools/vstar/external/plugin/AllTests.java index 631b02374..b614cd4e8 100644 --- a/plugin/test/org/aavso/tools/vstar/external/plugin/AllTests.java +++ b/plugin/test/org/aavso/tools/vstar/external/plugin/AllTests.java @@ -33,6 +33,7 @@ public static Test suite() { // $JUnit-BEGIN$ suite.addTestSuite(PluginTest.class); + suite.addTestSuite(PiecewiseLinearModelTest.class); // $JUnit-END$ return suite; diff --git a/plugin/test/org/aavso/tools/vstar/external/plugin/PiecewiseLinearModelTest.java b/plugin/test/org/aavso/tools/vstar/external/plugin/PiecewiseLinearModelTest.java new file mode 100644 index 000000000..c9f25d9c9 --- /dev/null +++ b/plugin/test/org/aavso/tools/vstar/external/plugin/PiecewiseLinearModelTest.java @@ -0,0 +1,113 @@ +/** + * VStar: a statistical analysis tool for variable star data. + * Copyright (C) 2009 AAVSO (http://www.aavso.org/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.aavso.tools.vstar.external.plugin; + +import java.util.ArrayList; +import java.util.List; + +import org.aavso.tools.vstar.data.DateInfo; +import org.aavso.tools.vstar.data.Magnitude; +import org.aavso.tools.vstar.data.ValidObservation; +import org.aavso.tools.vstar.external.lib.PiecewiseLinearModel.LinearFunction; +import org.aavso.tools.vstar.external.lib.PiecewiseLinearModel.PiecewiseLinearFunction; +import org.aavso.tools.vstar.ui.model.plot.JDCoordSource; +import org.aavso.tools.vstar.util.Tolerance; + +import junit.framework.TestCase; + +/** + * Unit tests for piecewise linear model plug-in library. + */ +public class PiecewiseLinearModelTest extends TestCase { + + public PiecewiseLinearModelTest(String name) { + super(name); + } + + public void testLinearFunction() { + double DELTA = 1e-6; + + LinearFunction function = new LinearFunction(2459645, 2459640, 10, 12.5); + + double m = -0.5; + assertTrue(Tolerance.areClose(m, function.slope(), DELTA, true)); + assertTrue(Tolerance.areClose(10 - (m * 2459645), function.yIntercept(), DELTA, true)); + assertTrue(Tolerance.areClose(m * 2459642 + function.yIntercept(), function.value(2459642), DELTA, true)); + } + + public void testPiecewiseLinearFunction() { + double DELTA = 1e-6; + + List meanObs = getTestMeanObs(); + PiecewiseLinearFunction plf = new PiecewiseLinearFunction(meanObs, JDCoordSource.instance); + + List obs = getTestObs(); + + double t1 = obs.get(0).getJD(); + LinearFunction function1 = plf.getFunctions().get(0); + assertTrue(Tolerance.areClose(function1.slope() * t1 + function1.yIntercept(), plf.value(t1), DELTA, true)); + + double t2 = obs.get(1).getJD(); + LinearFunction function2 = plf.getFunctions().get(1); + assertTrue(Tolerance.areClose(function2.slope() * t2 + function2.yIntercept(), plf.value(t2), DELTA, true)); + } + + // Helpers + + private List getTestMeanObs() { + List obs = new ArrayList(); + + ValidObservation ob1 = new ValidObservation(); + ob1.setDateInfo(new DateInfo(2459644)); + ob1.setMagnitude(new Magnitude(4.5, 0)); + obs.add(ob1); + + ValidObservation ob2 = new ValidObservation(); + ob2.setDateInfo(new DateInfo(2459645.5)); + ob2.setMagnitude(new Magnitude(5.5, 0)); + obs.add(ob2); + + ValidObservation ob3 = new ValidObservation(); + ob3.setDateInfo(new DateInfo(22459645.5)); + ob3.setMagnitude(new Magnitude(5.5, 0)); + obs.add(ob3); + + ValidObservation ob4 = new ValidObservation(); + ob4.setDateInfo(new DateInfo(2459647)); + ob4.setMagnitude(new Magnitude(7, 0)); + obs.add(ob4); + + return obs; + } + + private List getTestObs() { + List obs = new ArrayList(); + + ValidObservation ob1 = new ValidObservation(); + ob1.setDateInfo(new DateInfo(2459645.1134785)); + ob1.setMagnitude(new Magnitude(5, 0)); + obs.add(ob1); + + ValidObservation ob2 = new ValidObservation(); + ob2.setDateInfo(new DateInfo(2459646.2)); + ob2.setMagnitude(new Magnitude(6, 0)); + obs.add(ob2); + + return obs; + } +} diff --git a/script/VeLa/Y/Y.vela b/script/VeLa/Y/Y.vela index bcd3b32b6..62eefe837 100644 --- a/script/VeLa/Y/Y.vela +++ b/script/VeLa/Y/Y.vela @@ -3,7 +3,7 @@ # nor are function parameter and return types fully # specified (just "function") yet -Y is λ(h : function) : function { +Y is λ(h : λ) : function { λ(f : function) : function { f(f) } (λ(f : function) : function { diff --git a/script/VeLa/piecewise_linear_model.vl b/script/VeLa/piecewise_linear_model.vl index ddd0de861..55432a9a9 100644 --- a/script/VeLa/piecewise_linear_model.vl +++ b/script/VeLa/piecewise_linear_model.vl @@ -4,23 +4,23 @@ slope(x1:real y1:real x2:real y2:real) : real { (y2-y1) / (x2-x1) } --- piecewise linear model +# piecewise linear model genf() : function { i <- 0 f(t:real) : real { - -- is time coordinate beyond end of current line segment? + # is time coordinate beyond end of current line segment? if t > nth(times i+1) and i < length(times)-1 then { i <- i+1 } - -- obtain coordinates of point at ends of line segment + # obtain coordinates of point at ends of line segment t0 <- nth(times i) mag0 <- nth(mags i) t1 <- nth(times i+1) mag1 <- nth(mags i+1) - -- create linear model for segment and compute y value + # create linear model for segment and compute y value m <- slope(t1 t0 mag1 mag0) c <- mag1 - m*t1 y <- m*t + c @@ -31,23 +31,23 @@ genf() : function { f } --- derivative of model +# derivative of model gendf() : function { i <- 0 df(t:real) : real { - -- is time coordinate beyond end of current line segment? + # is time coordinate beyond end of current line segment? if t > nth(times i+1) and i < length(times)-1 then { i <- i+1 } - -- obtain coordinates of point at ends of line segment + # obtain coordinates of point at ends of line segment t0 <- nth(times i) mag0 <- nth(mags i) t1 <- nth(times i+1) mag1 <- nth(mags i+1) - -- return slope at the coordinate + # return slope at the coordinate slope(t1 t0 mag1 mag0) } @@ -57,16 +57,20 @@ gendf() : function { f <- genf() df <- gendf() ---println("slope: " slope(nth(times 0) nth(mags 0) nth(times 1) nth(mags 1))) +#println("slope: " slope(nth(times 0) nth(mags 0) nth(times 1) nth(mags 1))) ---series <- "Unspecified" +series <- "Unspecified" ---times <- [0.045 0.141 0.239 0.343 0.44 0.543 0.641 0.741 0.842 0.941 1] ---times <- getPhases(series) ---times <- getTimes(series) +times <- [0.045 0.141 0.239 0.343 0.44 0.543 0.641 0.741 0.842 0.941 1.0] +#times <- getPhases(series) +#times <- getTimes(series) ---mags <- [3.678 3.776 3.866 3.943 4 4.062 4.117 4.089 3.883 3.651 3.653] ---mags <- getMags(series) +mags <- [3.678 3.776 3.866 3.943 4.0 4.062 4.117 4.089 3.883 3.651 3.653] +#mags <- getMags(series) ---println(map(function(n:real):real{n*n} mags)) ---println(map(f times)) +#println(map(function(n:real):real{n*n} mags)) +#println(map(f times)) + +model is map(f times) +println(model) +scatter("Mean Model" "t" "mag" times mags) diff --git a/src/org/aavso/tools/vstar/plugin/model/impl/ApacheCommonsPolynomialFitCreatorPlugin.java b/src/org/aavso/tools/vstar/plugin/model/impl/ApacheCommonsPolynomialFitCreatorPlugin.java index 83a2bc43e..0c7484046 100644 --- a/src/org/aavso/tools/vstar/plugin/model/impl/ApacheCommonsPolynomialFitCreatorPlugin.java +++ b/src/org/aavso/tools/vstar/plugin/model/impl/ApacheCommonsPolynomialFitCreatorPlugin.java @@ -18,34 +18,22 @@ package org.aavso.tools.vstar.plugin.model.impl; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.aavso.tools.vstar.data.DateInfo; import org.aavso.tools.vstar.data.Magnitude; -import org.aavso.tools.vstar.data.SeriesType; import org.aavso.tools.vstar.data.ValidObservation; import org.aavso.tools.vstar.exception.AlgorithmError; import org.aavso.tools.vstar.plugin.ModelCreatorPluginBase; import org.aavso.tools.vstar.ui.dialog.PolynomialDegreeDialog; -import org.aavso.tools.vstar.ui.mediator.AnalysisType; -import org.aavso.tools.vstar.ui.mediator.Mediator; import org.aavso.tools.vstar.ui.model.plot.ContinuousModelFunction; -import org.aavso.tools.vstar.ui.model.plot.ICoordSource; -import org.aavso.tools.vstar.ui.model.plot.JDCoordSource; -import org.aavso.tools.vstar.ui.model.plot.JDTimeElementEntity; -import org.aavso.tools.vstar.ui.model.plot.StandardPhaseCoordSource; import org.aavso.tools.vstar.util.ApacheCommonsDerivativeBasedExtremaFinder; -import org.aavso.tools.vstar.util.comparator.JDComparator; -import org.aavso.tools.vstar.util.comparator.StandardPhaseComparator; +import org.aavso.tools.vstar.util.Tolerance; import org.aavso.tools.vstar.util.locale.LocaleProps; -import org.aavso.tools.vstar.util.model.IModel; +import org.aavso.tools.vstar.util.model.AbstractModel; import org.aavso.tools.vstar.util.model.PeriodFitParameters; import org.aavso.tools.vstar.util.prefs.NumericPrecisionPrefs; -import org.aavso.tools.vstar.util.stats.DescStats; import org.apache.commons.math.ConvergenceException; import org.apache.commons.math.FunctionEvaluationException; import org.apache.commons.math.analysis.DifferentiableUnivariateRealFunction; @@ -59,442 +47,346 @@ * A polynomial model creator plugin that uses an Apache Commons polynomial * fitter. */ -public class ApacheCommonsPolynomialFitCreatorPlugin extends - ModelCreatorPluginBase { - - private boolean needGUI = true; - - private int degree; - - private ICoordSource timeCoordSource; - private Comparator timeComparator; - - private PolynomialFitCreator fitCreator; - - public ApacheCommonsPolynomialFitCreatorPlugin() { - super(); - } - - @Override - public String getDescription() { - return LocaleProps.get("ANALYSIS_MENU_POLYNOMIAL_FIT"); - } - - @Override - public String getDisplayName() { - return LocaleProps.get("ANALYSIS_MENU_POLYNOMIAL_FIT"); - } - - @Override - public IModel getModel(List obs) { - fitCreator = new PolynomialFitCreator(obs); - return fitCreator.createModel(); - } - - /** - * This is intended for setting parameters from the scripting API. - */ - @Override - public void setParams(Object[] params) { - assert (params.length == 1); - double degree = (double) params[0]; - setDegree((int) degree); - needGUI = false; - } - - private void setDegree(int degree) { - this.degree = degree; - } - - public int getDegree() { - return degree; - } - - private int getMinDegree() { - return 0; - } - - private int getMaxDegree() { - // TODO: make this a preference - return 30; - } - - class PolynomialFitCreator { - private List obs; - private double zeroPoint; - - PolynomialFitCreator(List obs) { - // TODO: the code in this block should be refactored into the model - // creator base class or elsewhere - - // Select time mode (JD or phase). - switch (Mediator.getInstance().getAnalysisType()) { - case RAW_DATA: - timeCoordSource = JDCoordSource.instance; - timeComparator = JDComparator.instance; - this.obs = obs; - zeroPoint = DescStats.calcTimeElementMean(obs, - JDTimeElementEntity.instance); - break; - - case PHASE_PLOT: - timeCoordSource = StandardPhaseCoordSource.instance; - timeComparator = StandardPhaseComparator.instance; - this.obs = new ArrayList(obs); - Collections.sort(this.obs, timeComparator); - zeroPoint = 0; - break; - } - } - - // Create a model representing a polynomial fit of the requested degree. - IModel createModel() { - IModel model = null; - - int minDegree = getMinDegree(); - int maxDegree = getMaxDegree(); - - boolean cancelled = false; - - if (needGUI) { - PolynomialDegreeDialog polyDegreeDialog = new PolynomialDegreeDialog( - minDegree, maxDegree); - - setDegree(polyDegreeDialog.getDegree()); - - cancelled = polyDegreeDialog.isCancelled(); - } - - if (!cancelled) { - final AbstractLeastSquaresOptimizer optimizer = new LevenbergMarquardtOptimizer(); - - final PolynomialFitter fitter = new PolynomialFitter( - getDegree(), optimizer); - - model = new IModel() { - boolean interrupted = false; - List fit; - List residuals; - PolynomialFunction function; - Map functionStrMap = new LinkedHashMap(); - double aic = Double.NaN; - double bic = Double.NaN; - - @Override - public String getDescription() { - return LocaleProps - .get("MODEL_INFO_POLYNOMIAL_DEGREE_DESC") - + degree - + " for " - + obs.get(0).getBand() - + " series"; - } - - @Override - public List getFit() { - return fit; - } - - @Override - public List getResiduals() { - return residuals; - } - - @Override - public String getKind() { - return LocaleProps.get("ANALYSIS_MENU_POLYNOMIAL_FIT"); - } - - // TODO: if this is not generalisable, it should be removed - // as a requirement from base class - @Override - public List getParameters() { - // None for a polynomial fit. - return null; - } - - @Override - public boolean hasFuncDesc() { - return true; - } - - public String toFitMetricsString() throws AlgorithmError { - String strRepr = functionStrMap - .get(LocaleProps.get("MODEL_INFO_FIT_METRICS_TITLE")); - - if (strRepr == null) { - // Goodness of fit - strRepr = "RMS: " - + NumericPrecisionPrefs - .formatOther(optimizer.getRMS()); - - // Akaike and Bayesean Information Criteria - if (aic != Double.NaN && bic != Double.NaN) { - strRepr += "\nAIC: " - + NumericPrecisionPrefs - .formatOther(aic); - strRepr += "\nBIC: " - + NumericPrecisionPrefs - .formatOther(bic); - } - } - - return strRepr; - } - - @Override - public String toString() { - String strRepr = functionStrMap.get(LocaleProps - .get("MODEL_INFO_FUNCTION_TITLE")); - - if (strRepr == null) { - strRepr = "zeroPoint is " - + NumericPrecisionPrefs - .formatTime(zeroPoint) + "\n\n"; - - strRepr += "f(t:real) : real {\n"; - - double[] coeffs = function.getCoefficients(); - for (int i = coeffs.length - 1; i >= 1; i--) { - strRepr += " " + NumericPrecisionPrefs.formatPolyCoef(coeffs[i]); - strRepr += "*(t-zeroPoint)^" + i + " +\n"; - } - strRepr += " " + NumericPrecisionPrefs.formatPolyCoef(coeffs[0]); - strRepr += "\n}"; - } - - return strRepr; - } - - public String toExcelString() { - String strRepr = functionStrMap.get(LocaleProps - .get("MODEL_INFO_EXCEL_TITLE")); - - if (strRepr == null) { - strRepr = "="; - - double[] coeffs = function.getCoefficients(); - for (int i = coeffs.length - 1; i >= 1; i--) { - strRepr += NumericPrecisionPrefs.formatPolyCoef(coeffs[i]); - strRepr += "*(A1-" - + NumericPrecisionPrefs - .formatTime(zeroPoint) + ")^" - + i + "+\n"; - } - strRepr += NumericPrecisionPrefs.formatPolyCoef(coeffs[0]); - } - - return strRepr; - } - - // toRString must be locale-independent! - public String toRString() { - String strRepr = functionStrMap.get(LocaleProps - .get("MODEL_INFO_R_TITLE")); - - if (strRepr == null) { - strRepr = "zeroPoint <- " - + NumericPrecisionPrefs - .formatTimeLocaleIndependent(zeroPoint) + "\n\n"; - - strRepr += "model <- function(t)\n"; - - double[] coeffs = function.getCoefficients(); - for (int i = coeffs.length - 1; i >= 1; i--) { - strRepr += NumericPrecisionPrefs.formatPolyCoefLocaleIndependent(coeffs[i]); - strRepr += "*(t-zeroPoint)^" + i + " +\n"; - } - strRepr += NumericPrecisionPrefs.formatPolyCoefLocaleIndependent(coeffs[0]); - } - - return strRepr; - } - - @Override - public ContinuousModelFunction getModelFunction() { - // UnivariateRealFunction func = new - // UnivariateRealFunction() { - // @Override - // public double value(double x) - // throws FunctionEvaluationException { - // double y = 0; - // double[] coeffs = function.getCoefficients(); - // for (int i = coeffs.length - 1; i >= 1; i--) { - // y += coeffs[i] * Math.pow(x, i); - // } - // y += coeffs[0]; - // return y; - // } - // }; - - return new ContinuousModelFunction(function, fit, - zeroPoint); - } - - // An alternative implementation for getModelFunction() that - // uses Horner's method to avoid exponentiation. - public UnivariateRealFunction getModelFunctionHorner() { - UnivariateRealFunction func = new UnivariateRealFunction() { - @Override - public double value(double x) - throws FunctionEvaluationException { - // Compute the value of the polynomial for x via - // Horner's method. - double y = 0; - double[] coeffs = function.getCoefficients(); - for (double coeff : coeffs) { - y = y * x + coeff; - } - return y; - } - }; - - return func; - } - - @Override - public void execute() throws AlgorithmError { - - for (int i = 0; i < obs.size() && !interrupted; i++) { - fitter.addObservedPoint(1.0, - timeCoordSource.getXCoord(i, obs) - - zeroPoint, obs.get(i).getMag()); - } - - if (!interrupted) { - try { - function = fitter.fit(); - - fit = new ArrayList(); - residuals = new ArrayList(); - double sumSqResiduals = 0; - - String comment = LocaleProps - .get("MODEL_INFO_POLYNOMIAL_DEGREE_DESC") - + degree; - - // Create fit and residual observations and - // compute the sum of squares of residuals for - // Akaike and Bayesean Information Criteria. - for (int i = 0; i < obs.size() && !interrupted; i++) { - ValidObservation ob = obs.get(i); - - double x = timeCoordSource - .getXCoord(i, obs); - double zeroedX = x - zeroPoint; - double y = function.value(zeroedX); - - ValidObservation fitOb = new ValidObservation(); - fitOb.setDateInfo(new DateInfo(ob.getJD())); - if (Mediator.getInstance() - .getAnalysisType() == AnalysisType.PHASE_PLOT) { - fitOb.setPreviousCyclePhase(ob - .getPreviousCyclePhase()); - fitOb.setStandardPhase(ob - .getStandardPhase()); - } - fitOb.setMagnitude(new Magnitude(y, 0)); - fitOb.setBand(SeriesType.Model); - fitOb.setComments(comment); - fit.add(fitOb); - - ValidObservation resOb = new ValidObservation(); - resOb.setDateInfo(new DateInfo(ob.getJD())); - if (Mediator.getInstance() - .getAnalysisType() == AnalysisType.PHASE_PLOT) { - resOb.setPreviousCyclePhase(ob - .getPreviousCyclePhase()); - resOb.setStandardPhase(ob - .getStandardPhase()); - } - double residual = ob.getMag() - y; - resOb.setMagnitude(new Magnitude(residual, - 0)); - resOb.setBand(SeriesType.Residuals); - resOb.setComments(comment); - residuals.add(resOb); - - sumSqResiduals += (residual * residual); - } - - // Fit metrics (AIC, BIC). - int n = residuals.size(); - if (n != 0 && sumSqResiduals / n != 0) { - double commonIC = n - * Math.log(sumSqResiduals / n); - aic = commonIC + 2 * degree; - bic = commonIC + degree * Math.log(n); - } - - functionStrMap.put(LocaleProps - .get("MODEL_INFO_FIT_METRICS_TITLE"), - toFitMetricsString()); - - ApacheCommonsDerivativeBasedExtremaFinder finder = new ApacheCommonsDerivativeBasedExtremaFinder( - fit, - (DifferentiableUnivariateRealFunction) function, - timeCoordSource, zeroPoint); - - String extremaStr = finder.toString(); - - if (extremaStr != null) { - String title = LocaleProps - .get("MODEL_INFO_EXTREMA_TITLE"); - - functionStrMap.put(title, extremaStr); - } - - // Minimum/maximum. - // ApacheCommonsBrentOptimiserExtremaFinder - // finder = new - // ApacheCommonsBrentOptimiserExtremaFinder( - // fit, function, timeCoordSource, - // zeroPoint); - // - // String extremaStr = finder.toString(); - // - // if (extremaStr != null) { - // String title = LocaleProps - // .get("MODEL_INFO_EXTREMA_TITLE"); - // - // functionStrMap.put(title, extremaStr); - // } - - // VeLa, Excel, R equations. - // TODO: consider Python, e.g. for use with - // matplotlib. - functionStrMap.put(LocaleProps - .get("MODEL_INFO_FUNCTION_TITLE"), - toString()); - - functionStrMap.put(LocaleProps - .get("MODEL_INFO_EXCEL_TITLE"), - toExcelString()); - - functionStrMap.put( - LocaleProps.get("MODEL_INFO_R_TITLE"), - toRString()); - - } catch (ConvergenceException e) { - throw new AlgorithmError( - e.getLocalizedMessage()); - } - } - } - - @Override - public void interrupt() { - interrupted = true; - } - - @Override - public Map getFunctionStrings() { - return functionStrMap; - } - }; - } - - return model; - } - } +public class ApacheCommonsPolynomialFitCreatorPlugin extends ModelCreatorPluginBase { + + private boolean needGUI = true; + + private int degree; + + public ApacheCommonsPolynomialFitCreatorPlugin() { + super(); + } + + @Override + public String getDescription() { + return LocaleProps.get("ANALYSIS_MENU_POLYNOMIAL_FIT"); + } + + @Override + public String getDisplayName() { + return LocaleProps.get("ANALYSIS_MENU_POLYNOMIAL_FIT"); + } + + @Override + public AbstractModel getModel(List obs) { + return new PolynomialFitModel(obs); + } + + /** + * This is intended for setting parameters from the scripting API or plug-in + * test. + */ + @Override + public void setParams(Object[] params) { + assert (params.length == 1); + double degree = (double) params[0]; + setDegree((int) degree); + needGUI = false; + } + + private void setDegree(int degree) { + this.degree = degree; + } + + public int getDegree() { + return degree; + } + + private int getMinDegree() { + return 0; + } + + private int getMaxDegree() { + // TODO: make this a preference + return 30; + } + + class PolynomialFitModel extends AbstractModel { + PolynomialFunction function; + PolynomialFitter fitter; + AbstractLeastSquaresOptimizer optimizer; + + PolynomialFitModel(List obs) { + super(obs); + + int minDegree = getMinDegree(); + int maxDegree = getMaxDegree(); + + boolean cancelled = false; + + if (needGUI) { + PolynomialDegreeDialog polyDegreeDialog = new PolynomialDegreeDialog(minDegree, maxDegree); + + setDegree(polyDegreeDialog.getDegree()); + + cancelled = polyDegreeDialog.isCancelled(); + } + + if (!cancelled) { + optimizer = new LevenbergMarquardtOptimizer(); + fitter = new PolynomialFitter(getDegree(), optimizer); + } + } + + @Override + public String getDescription() { + return LocaleProps.get("MODEL_INFO_POLYNOMIAL_DEGREE_DESC") + degree + " for " + obs.get(0).getBand() + + " series"; + } + + @Override + public String getKind() { + return LocaleProps.get("ANALYSIS_MENU_POLYNOMIAL_FIT"); + } + + // TODO: if this is not generalisable, it should be removed + // as a requirement from base class or the name changed to + // getPeriodFitParameters() + @Override + public List getParameters() { + // None for a polynomial fit. + return null; + } + + @Override + public boolean hasFuncDesc() { + return true; + } + + @Override + public void rootMeanSquare() { + rms = optimizer.getRMS(); + } + + @Override + public Map getFunctionStrings() { + return functionStrMap; + } + + @Override + public String toString() { + return toVeLaString(); + } + + @Override + public String toVeLaString() { + String strRepr = functionStrMap.get(LocaleProps.get("MODEL_INFO_FUNCTION_TITLE")); + + if (strRepr == null) { + strRepr = "zeroPoint is " + NumericPrecisionPrefs.formatTime(zeroPoint) + "\n\n"; + + strRepr += "f(t:real) : real {\n"; + + double[] coeffs = function.getCoefficients(); + for (int i = coeffs.length - 1; i >= 1; i--) { + strRepr += " " + NumericPrecisionPrefs.formatCoef(coeffs[i]); + strRepr += "*(t-zeroPoint)^" + i + " +\n"; + } + strRepr += " " + NumericPrecisionPrefs.formatCoef(coeffs[0]); + strRepr += "\n}"; + } + + return strRepr; + } + + public String toExcelString() { + String strRepr = functionStrMap.get(LocaleProps.get("MODEL_INFO_EXCEL_TITLE")); + + if (strRepr == null) { + strRepr = "="; + + double[] coeffs = function.getCoefficients(); + for (int i = coeffs.length - 1; i >= 1; i--) { + strRepr += NumericPrecisionPrefs.formatCoef(coeffs[i]); + strRepr += "*(A1-" + NumericPrecisionPrefs.formatTime(zeroPoint) + ")^" + i + "+\n"; + } + strRepr += NumericPrecisionPrefs.formatCoef(coeffs[0]); + } + + return strRepr; + } + + // toRString must be locale-independent! + public String toRString() { + String strRepr = functionStrMap.get(LocaleProps.get("MODEL_INFO_R_TITLE")); + + if (strRepr == null) { + strRepr = "zeroPoint <- " + NumericPrecisionPrefs.formatTimeLocaleIndependent(zeroPoint) + "\n\n"; + + strRepr += "model <- function(t)\n"; + + double[] coeffs = function.getCoefficients(); + for (int i = coeffs.length - 1; i >= 1; i--) { + strRepr += NumericPrecisionPrefs.formatCoefLocaleIndependent(coeffs[i]); + strRepr += "*(t-zeroPoint)^" + i + " +\n"; + } + strRepr += NumericPrecisionPrefs.formatCoefLocaleIndependent(coeffs[0]); + } + + return strRepr; + } + + @Override + public void functionStrings() { + super.functionStrings(); + functionStrMap.put(LocaleProps.get("MODEL_INFO_EXCEL_TITLE"), toExcelString()); + functionStrMap.put(LocaleProps.get("MODEL_INFO_R_TITLE"), toRString()); + } + + @Override + public ContinuousModelFunction getModelFunction() { + // UnivariateRealFunction func = new + // UnivariateRealFunction() { + // @Override + // public double value(double x) + // throws FunctionEvaluationException { + // double y = 0; + // double[] coeffs = function.getCoefficients(); + // for (int i = coeffs.length - 1; i >= 1; i--) { + // y += coeffs[i] * Math.pow(x, i); + // } + // y += coeffs[0]; + // return y; + // } + // }; + + return new ContinuousModelFunction(function, fit, zeroPoint); + } + + // An alternative implementation for getModelFunction() that + // uses Horner's method to avoid exponentiation. + public UnivariateRealFunction getModelFunctionHorner() { + UnivariateRealFunction func = new UnivariateRealFunction() { + @Override + public double value(double x) throws FunctionEvaluationException { + // Compute the value of the polynomial for x via + // Horner's method. + double y = 0; + double[] coeffs = function.getCoefficients(); + for (double coeff : coeffs) { + y = y * x + coeff; + } + return y; + } + }; + + return func; + } + + @Override + public void execute() throws AlgorithmError { + + for (int i = 0; i < obs.size() && !interrupted; i++) { + fitter.addObservedPoint(1.0, timeCoordSource.getXCoord(i, obs) - zeroPoint, obs.get(i).getMag()); + } + + if (!interrupted) { + try { + function = fitter.fit(); + + fit = new ArrayList(); + residuals = new ArrayList(); + + String comment = LocaleProps.get("MODEL_INFO_POLYNOMIAL_DEGREE_DESC") + degree; + + for (int i = 0; i < obs.size() && !interrupted; i++) { + ValidObservation ob = obs.get(i); + + double x = timeCoordSource.getXCoord(i, obs); + double zeroedX = x - zeroPoint; + double y = function.value(zeroedX); + + collectObs(y, ob, comment); + } + + rootMeanSquare(); + informationCriteria(degree); + fitMetrics(); + + ApacheCommonsDerivativeBasedExtremaFinder finder = new ApacheCommonsDerivativeBasedExtremaFinder( + fit, (DifferentiableUnivariateRealFunction) function, timeCoordSource, zeroPoint); + + String extremaStr = finder.toString(); + + if (extremaStr != null) { + String title = LocaleProps.get("MODEL_INFO_EXTREMA_TITLE"); + + functionStrMap.put(title, extremaStr); + } + + functionStrings(); + + } catch (ConvergenceException e) { + throw new AlgorithmError(e.getLocalizedMessage()); + } + } + } + } + + // Test + // see also TSPolynomialFitterTest (under test directory) + + @Override + public Boolean test() { + boolean result = true; + + setTestMode(true); + needGUI = false; + + result &= testPolynomialFit(); + + setTestMode(false); + + return result; + } + + private boolean testPolynomialFit() { + boolean result = true; + + List obs = getTestObs(); + + setDegree(9); + + AbstractModel model = getModel(obs); + + try { + model.execute(); + + double DELTA = 1e-6; + + List fit = model.getFit(); + ValidObservation fitOb = fit.get(0); + result &= fitOb.getJD() == 2459301.0; + result &= Tolerance.areClose(0.629248, fitOb.getMag(), DELTA, true); + + List residuals = model.getResiduals(); + ValidObservation resOb = residuals.get(0); + result &= resOb.getJD() == 2459301.0; + result &= Tolerance.areClose(0.000073, resOb.getMag(), DELTA, true); + + result &= Tolerance.areClose(0.0000162266724849, model.getRMS(), DELTA, true); + result &= Tolerance.areClose(-7923.218889035116, model.getAIC(), DELTA, true); + result &= Tolerance.areClose(-7888.243952752065, model.getBIC(), DELTA, true); + + } catch (AlgorithmError e) { + result = false; + } + + return result; + } + + private List getTestObs() { + List obs = new ArrayList(); + + for (int t = 1; t <= 360; t++) { + double time = 2459300 + t; + double mag = Math.sin(Math.toRadians(time)); + ValidObservation ob = new ValidObservation(); + ob.setDateInfo(new DateInfo(time)); + ob.setMagnitude(new Magnitude(mag, 0)); + obs.add(ob); + } + + return obs; + } } diff --git a/src/org/aavso/tools/vstar/ui/dialog/period/PeriodAnalysisDataTablePane.java b/src/org/aavso/tools/vstar/ui/dialog/period/PeriodAnalysisDataTablePane.java index 19718e147..dc83a55cf 100644 --- a/src/org/aavso/tools/vstar/ui/dialog/period/PeriodAnalysisDataTablePane.java +++ b/src/org/aavso/tools/vstar/ui/dialog/period/PeriodAnalysisDataTablePane.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.swing.BoxLayout; import javax.swing.JButton; @@ -59,296 +60,311 @@ @SuppressWarnings("serial") public class PeriodAnalysisDataTablePane extends JPanel implements ListSelectionListener, IStartAndCleanup { - protected JTable table; - protected PeriodAnalysisDataTableModel model; - protected TableRowSorter rowSorter; + protected JTable table; + protected PeriodAnalysisDataTableModel model; + protected TableRowSorter rowSorter; - protected JButton modelButton; + protected JButton modelButton; - protected IPeriodAnalysisAlgorithm algorithm; + protected IPeriodAnalysisAlgorithm algorithm; - protected boolean wantModelButton; + protected boolean wantModelButton; - protected Map> freqToHarmonicsMap; + protected Map> freqToHarmonicsMap; + + protected Listener harmonicSearchResultListener; + protected Listener periodAnalysisSelectionListener; + + private String tablePaneID = null; + + private boolean valueChangedDisabledState = false; + + /** + * Constructor + * + * @param model The period analysis table model. + * @param algorithm The period analysis algorithm. + * @param wantModelButton Add a model button? + */ + public PeriodAnalysisDataTablePane(PeriodAnalysisDataTableModel model, IPeriodAnalysisAlgorithm algorithm, + boolean wantModelButton) { + super(new GridLayout(1, 1)); + + this.model = model; + this.algorithm = algorithm; + this.wantModelButton = wantModelButton; + + freqToHarmonicsMap = new HashMap>(); + + table = new JTable(model); + JScrollPane scrollPane = new JScrollPane(table); + + this.add(scrollPane); + + table.getSelectionModel().addListSelectionListener(this); + + table.setColumnSelectionAllowed(false); + table.setRowSelectionAllowed(true); + + table.setAutoCreateRowSorter(true); + FormattedDoubleComparator comparator = FormattedDoubleComparator.getInstance(); + rowSorter = new TableRowSorter(model); + for (int i = 0; i < model.getColumnCount(); i++) { + rowSorter.setComparator(i, comparator); + } + table.setRowSorter(rowSorter); + + setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); + add(createButtonPanel()); + } + + /** + * Constructor for a period analysis data table pane with a model button. + * + * @param model The period analysis table model. + * @param algorithm The period analysis algorithm. + */ + public PeriodAnalysisDataTablePane(PeriodAnalysisDataTableModel model, IPeriodAnalysisAlgorithm algorithm) { + this(model, algorithm, true); + } + + protected JPanel createButtonPanel() { + JPanel buttonPane = new JPanel(); + + modelButton = new JButton(LocaleProps.get("CREATE_MODEL_BUTTON")); + modelButton.setEnabled(false); + modelButton.addActionListener(createModelButtonHandler()); + + if (!wantModelButton) { + modelButton.setVisible(false); + } + + buttonPane.add(modelButton, BorderLayout.LINE_END); + + return buttonPane; + } + + /** + * We send a period analysis selection message when the table selection value + * has "settled". This event could be consumed by other views such as plots. + */ + public void valueChanged(ListSelectionEvent e) { + if (isValueChangeDisabled()) + return; + + if (e.getSource() == table.getSelectionModel() && table.getRowSelectionAllowed() && !e.getValueIsAdjusting()) { + int row = table.getSelectedRow(); + + if (row >= 0) { + row = table.convertRowIndexToModel(row); + + PeriodAnalysisSelectionMessage message = new PeriodAnalysisSelectionMessage(this, + model.getDataPointFromRow(row), row); + message.setTag(Mediator.getParentDialogName(this)); + Mediator.getInstance().getPeriodAnalysisSelectionNotifier().notifyListeners(message); + } + } + } + + // Model button listener. + protected ActionListener createModelButtonHandler() { + return new ActionListener() { + public void actionPerformed(ActionEvent e) { + // TODO: add a user selected frequencies protected method + // that can be called from an abstract method that implements + // the modelling operation + // TODO: only this vcaries: userSelectedFreqs.add(dataPoint.getFrequency()); + + List dataPoints = new ArrayList(); + int[] selectedTableRowIndices = table.getSelectedRows(); + if (selectedTableRowIndices.length < 1) { + MessageBox.showMessageDialog(LocaleProps.get("CREATE_MODEL_BUTTON"), "Please select a row"); + return; + } + for (int row : selectedTableRowIndices) { + int modelRow = table.convertRowIndexToModel(row); + + PeriodAnalysisDataPoint dataPoint = model.getDataPointFromRow(modelRow); + dataPoints.add(dataPoint); + } + + modelAction(dataPoints); + } + }; + } + + /** + * Modelling action. Subclasses can override.
+ * Note: should really make this class abstract on this method. + * + * @param dataPoints The selected data points. + */ + protected void modelAction(List dataPoints) { + final JPanel parent = this; + + List userSelectedFreqs = dataPoints.stream().map(point -> point.getFrequency()).collect(Collectors.toList()); + + HarmonicInputDialog dialog = new HarmonicInputDialog(parent, userSelectedFreqs, freqToHarmonicsMap); + + if (!dialog.isCancelled()) { + List harmonics = dialog.getHarmonics(); + if (!harmonics.isEmpty()) { + try { + PeriodAnalysisDerivedMultiPeriodicModel model = new PeriodAnalysisDerivedMultiPeriodicModel( + dataPoints.get(0), harmonics, algorithm); + + Mediator.getInstance().performModellingOperation(model); + } catch (Exception ex) { + MessageBox.showErrorDialog(parent, "Modelling", ex.getLocalizedMessage()); + } + } else { + MessageBox.showErrorDialog("Create Model", "Period list error"); + } + } + } + + /** + * A listener to store the latest harmonic search result in a mapping from + * (fundamental) frequency to harmonics. + */ + protected Listener createHarmonicSearchResultListener() { + final PeriodAnalysisDataTablePane tablePane = this; + return new Listener() { + @Override + public void update(HarmonicSearchResultMessage info) { + if (!Mediator.isMsgForDialog(Mediator.getParentDialog(tablePane), info)) + return; + freqToHarmonicsMap.put(info.getDataPoint().getFrequency(), info.getHarmonics()); + + String id = tablePane.getTablePaneID(); + String currentID = info.getIDstring(); + if (currentID != null && currentID.equals(id)) { + if (info.getHarmonics().size() > 0) { + new HarmonicInfoDialog(info, tablePane); + } else { + MessageBox.showMessageDialog("Harmonics", "No top hit for this frequency"); + } + } + } + + @Override + public boolean canBeRemoved() { + return true; + } + }; + } + + /** + * Select the row in the table corresponding to the period analysis selection. + * We also enable the "refine" button. + */ + protected Listener createPeriodAnalysisListener() { + final Component parent = this; + + return new Listener() { + @Override + public void update(PeriodAnalysisSelectionMessage info) { + if (!Mediator.isMsgForDialog(Mediator.getParentDialog(PeriodAnalysisDataTablePane.this), info)) + return; + if (info.getSource() != parent) { + // Find data point in table. + int row = -1; + for (int i = 0; i < model.getRowCount(); i++) { + if (model.getDataPointFromRow(i).equals(info.getDataPoint())) { + row = i; + break; + } + } + + // Note that the row may not correspond to anything in the + // data table, e.g. in the case of period analysis + // refinement. + if (row != -1) { + // Convert to view index! + row = table.convertRowIndexToView(row); + + // Scroll to an arbitrary column (zeroth) within + // the selected row, then select that row. + // Assumption: we are specifying the zeroth cell + // within row i as an x,y coordinate relative to + // the top of the table pane. + // Note that we could call this on the scroll + // pane, which would then forward the request to + // the table pane anyway. + int colWidth = (int) table.getCellRect(row, 0, true).getWidth(); + int rowHeight = table.getRowHeight(row); + table.scrollRectToVisible(new Rectangle(colWidth, rowHeight * row, colWidth, rowHeight)); + + boolean state = disableValueChangeEvent(); + try { + table.setRowSelectionInterval(row, row); + } finally { + setValueChangedDisabledState(state); + } + enableButtons(); + } + } else { + enableButtons(); + } + } + + @Override + public boolean canBeRemoved() { + return true; + } + }; + } + + /** + * Enable the buttons on this pane. + */ + protected void enableButtons() { + modelButton.setEnabled(true); + } + + @Override + public void startup() { + harmonicSearchResultListener = createHarmonicSearchResultListener(); + Mediator.getInstance().getHarmonicSearchNotifier().addListener(harmonicSearchResultListener); + + periodAnalysisSelectionListener = createPeriodAnalysisListener(); + Mediator.getInstance().getPeriodAnalysisSelectionNotifier().addListener(periodAnalysisSelectionListener); + } + + @Override + public void cleanup() { + Mediator.getInstance().getHarmonicSearchNotifier().removeListenerIfWilling(harmonicSearchResultListener); + Mediator.getInstance().getPeriodAnalysisSelectionNotifier() + .removeListenerIfWilling(periodAnalysisSelectionListener); + } + + public void setTablePaneID(String tablePaneID) { + this.tablePaneID = tablePaneID; + } + + public String getTablePaneID() { + return tablePaneID; + } + + /** + * @return the table + */ + public JTable getTable() { + return table; + } + + public boolean disableValueChangeEvent() { + boolean state = valueChangedDisabledState; + valueChangedDisabledState = true; + return state; + } + + public void setValueChangedDisabledState(boolean state) { + valueChangedDisabledState = state; + } + + public boolean isValueChangeDisabled() { + return valueChangedDisabledState; + } - protected Listener harmonicSearchResultListener; - protected Listener periodAnalysisSelectionListener; - - private String tablePaneID = null; - - private boolean valueChangedDisabledState = false; - - /** - * Constructor - * - * @param model The period analysis table model. - * @param algorithm The period analysis algorithm. - * @param wantModelButton Add a model button? - */ - public PeriodAnalysisDataTablePane(PeriodAnalysisDataTableModel model, IPeriodAnalysisAlgorithm algorithm, - boolean wantModelButton) { - super(new GridLayout(1, 1)); - - this.model = model; - this.algorithm = algorithm; - this.wantModelButton = wantModelButton; - - freqToHarmonicsMap = new HashMap>(); - - table = new JTable(model); - JScrollPane scrollPane = new JScrollPane(table); - - this.add(scrollPane); - - table.getSelectionModel().addListSelectionListener(this); - - table.setColumnSelectionAllowed(false); - table.setRowSelectionAllowed(true); - - table.setAutoCreateRowSorter(true); - FormattedDoubleComparator comparator = FormattedDoubleComparator.getInstance(); - rowSorter = new TableRowSorter(model); - for (int i = 0; i < model.getColumnCount(); i++) { - rowSorter.setComparator(i, comparator); - } - table.setRowSorter(rowSorter); - - setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); - add(createButtonPanel()); - } - - /** - * Constructor for a period analysis data table pane with a model button. - * - * @param model The period analysis table model. - * @param algorithm The period analysis algorithm. - */ - public PeriodAnalysisDataTablePane(PeriodAnalysisDataTableModel model, IPeriodAnalysisAlgorithm algorithm) { - this(model, algorithm, true); - } - - protected JPanel createButtonPanel() { - JPanel buttonPane = new JPanel(); - - modelButton = new JButton(LocaleProps.get("CREATE_MODEL_BUTTON")); - modelButton.setEnabled(false); - modelButton.addActionListener(createModelButtonHandler()); - - if (!wantModelButton) { - modelButton.setVisible(false); - } - - buttonPane.add(modelButton, BorderLayout.LINE_END); - - return buttonPane; - } - - /** - * We send a period analysis selection message when the table selection value - * has "settled". This event could be consumed by other views such as plots. - */ - public void valueChanged(ListSelectionEvent e) { - if (isValueChangeDisabled()) - return; - - if (e.getSource() == table.getSelectionModel() && table.getRowSelectionAllowed() && !e.getValueIsAdjusting()) { - int row = table.getSelectedRow(); - - if (row >= 0) { - row = table.convertRowIndexToModel(row); - - PeriodAnalysisSelectionMessage message = new PeriodAnalysisSelectionMessage(this, - model.getDataPointFromRow(row), row); - message.setTag(Mediator.getParentDialogName(this)); - Mediator.getInstance().getPeriodAnalysisSelectionNotifier().notifyListeners(message); - } - } - } - - // Model button listener. - protected ActionListener createModelButtonHandler() { - final JPanel parent = this; - - return new ActionListener() { - public void actionPerformed(ActionEvent e) { - List dataPoints = new ArrayList(); - List userSelectedFreqs = new ArrayList(); - int[] selectedTableRowIndices = table.getSelectedRows(); - if (selectedTableRowIndices.length < 1) { - MessageBox.showMessageDialog(LocaleProps.get("CREATE_MODEL_BUTTON"), "Please select a row"); - return; - } - for (int row : selectedTableRowIndices) { - int modelRow = table.convertRowIndexToModel(row); - - PeriodAnalysisDataPoint dataPoint = model.getDataPointFromRow(modelRow); - dataPoints.add(dataPoint); - userSelectedFreqs.add(dataPoint.getFrequency()); - } - - HarmonicInputDialog dialog = new HarmonicInputDialog(parent, userSelectedFreqs, freqToHarmonicsMap); - - if (!dialog.isCancelled()) { - List harmonics = dialog.getHarmonics(); - if (!harmonics.isEmpty()) { - try { - PeriodAnalysisDerivedMultiPeriodicModel model = new PeriodAnalysisDerivedMultiPeriodicModel( - dataPoints.get(0), harmonics, algorithm); - - Mediator.getInstance().performModellingOperation(model); - } catch (Exception ex) { - MessageBox.showErrorDialog(parent, "Modelling", ex.getLocalizedMessage()); - } - } else { - MessageBox.showErrorDialog("Create Model", "Period list error"); - } - } - } - }; - } - - /** - * A listener to store the latest harmonic search result in a mapping from - * (fundamental) frequency to harmonics. - */ - protected Listener createHarmonicSearchResultListener() { - final PeriodAnalysisDataTablePane tablePane = this; - return new Listener() { - @Override - public void update(HarmonicSearchResultMessage info) { - if (!Mediator.isMsgForDialog(Mediator.getParentDialog(tablePane), info)) - return; - freqToHarmonicsMap.put(info.getDataPoint().getFrequency(), info.getHarmonics()); - - String id = tablePane.getTablePaneID(); - String currentID = info.getIDstring(); - if (currentID != null && currentID.equals(id)) { - if (info.getHarmonics().size() > 0) { - new HarmonicInfoDialog(info, tablePane); - } else { - MessageBox.showMessageDialog("Harmonics", "No top hit for this frequency"); - } - } - } - - @Override - public boolean canBeRemoved() { - return true; - } - }; - } - - /** - * Select the row in the table corresponding to the period analysis selection. - * We also enable the "refine" button. - */ - protected Listener createPeriodAnalysisListener() { - final Component parent = this; - - return new Listener() { - @Override - public void update(PeriodAnalysisSelectionMessage info) { - if (!Mediator.isMsgForDialog(Mediator.getParentDialog(PeriodAnalysisDataTablePane.this), info)) - return; - if (info.getSource() != parent) { - // Find data point in table. - int row = -1; - for (int i = 0; i < model.getRowCount(); i++) { - if (model.getDataPointFromRow(i).equals(info.getDataPoint())) { - row = i; - break; - } - } - - // Note that the row may not correspond to anything in the - // data table, e.g. in the case of period analysis - // refinement. - if (row != -1) { - // Convert to view index! - row = table.convertRowIndexToView(row); - - // Scroll to an arbitrary column (zeroth) within - // the selected row, then select that row. - // Assumption: we are specifying the zeroth cell - // within row i as an x,y coordinate relative to - // the top of the table pane. - // Note that we could call this on the scroll - // pane, which would then forward the request to - // the table pane anyway. - int colWidth = (int) table.getCellRect(row, 0, true).getWidth(); - int rowHeight = table.getRowHeight(row); - table.scrollRectToVisible(new Rectangle(colWidth, rowHeight * row, colWidth, rowHeight)); - - boolean state = disableValueChangeEvent(); - try { - table.setRowSelectionInterval(row, row); - } finally { - setValueChangedDisabledState(state); - } - enableButtons(); - } - } else { - enableButtons(); - } - } - - @Override - public boolean canBeRemoved() { - return true; - } - }; - } - - /** - * Enable the buttons on this pane. - */ - protected void enableButtons() { - modelButton.setEnabled(true); - } - - @Override - public void startup() { - harmonicSearchResultListener = createHarmonicSearchResultListener(); - Mediator.getInstance().getHarmonicSearchNotifier().addListener(harmonicSearchResultListener); - - periodAnalysisSelectionListener = createPeriodAnalysisListener(); - Mediator.getInstance().getPeriodAnalysisSelectionNotifier().addListener(periodAnalysisSelectionListener); - } - - @Override - public void cleanup() { - Mediator.getInstance().getHarmonicSearchNotifier().removeListenerIfWilling(harmonicSearchResultListener); - Mediator.getInstance().getPeriodAnalysisSelectionNotifier() - .removeListenerIfWilling(periodAnalysisSelectionListener); - } - - public void setTablePaneID(String tablePaneID) { - this.tablePaneID = tablePaneID; - } - - public String getTablePaneID() { - return tablePaneID; - } - - /** - * @return the table - */ - public JTable getTable() { - return table; - } - - public boolean disableValueChangeEvent() { - boolean state = valueChangedDisabledState; - valueChangedDisabledState = true; - return state; - } - - public void setValueChangedDisabledState(boolean state) { - valueChangedDisabledState = state; - } - - public boolean isValueChangeDisabled() { - return valueChangedDisabledState; - } - } diff --git a/src/org/aavso/tools/vstar/ui/dialog/period/PeriodAnalysisTopHitsTablePane.java b/src/org/aavso/tools/vstar/ui/dialog/period/PeriodAnalysisTopHitsTablePane.java index 88c21e828..54bd4c68a 100644 --- a/src/org/aavso/tools/vstar/ui/dialog/period/PeriodAnalysisTopHitsTablePane.java +++ b/src/org/aavso/tools/vstar/ui/dialog/period/PeriodAnalysisTopHitsTablePane.java @@ -52,286 +52,262 @@ @SuppressWarnings("serial") public class PeriodAnalysisTopHitsTablePane extends PeriodAnalysisDataTablePane { - private Set refinedDataPoints; - private Set resultantDataPoints; - - private JButton refineButton; - - private Listener periodAnalysisRefinementListener; - - /** - * Constructor. - * - * @param topHitsModel - * The top hits data model. - * @param fullDataModel - * The full data data model. - * @param algorithm - * The period analysis algorithm. - */ - public PeriodAnalysisTopHitsTablePane( - PeriodAnalysisDataTableModel topHitsModel, - PeriodAnalysisDataTableModel fullDataModel, - IPeriodAnalysisAlgorithm algorithm) { - super(topHitsModel, algorithm); - - refinedDataPoints = new TreeSet( - PeriodAnalysisDataPointComparator.instance); - - resultantDataPoints = new TreeSet( - PeriodAnalysisDataPointComparator.instance); - } - - protected JPanel createButtonPanel() { - JPanel buttonPane = super.createButtonPanel(); - - refineButton = new JButton(algorithm.getRefineByFrequencyName()); - refineButton.setEnabled(false); - refineButton.addActionListener(createRefineButtonHandler()); - buttonPane.add(refineButton, BorderLayout.LINE_START); - - return buttonPane; - } - - // Refine button listener. - private ActionListener createRefineButtonHandler() { - final JPanel parent = this; - return new ActionListener() { - public void actionPerformed(ActionEvent e) { - // Collect frequencies to be used in refinement, ensuring that - // we don't try to use a frequency that has already been used - // for refinement. We also do not want to use the result of a - // previous refinement. - List freqs = new ArrayList(); - int[] selectedTableRowIndices = table.getSelectedRows(); - List inputDataPoints = new ArrayList(); - - for (int row : selectedTableRowIndices) { - int modelRow = table.convertRowIndexToModel(row); - PeriodAnalysisDataPoint dataPoint = model - .getDataPointFromRow(modelRow); - - if (!refinedDataPoints.contains(dataPoint)) { - inputDataPoints.add(dataPoint); - freqs.add(dataPoint.getFrequency()); - } else { - String msg = String.format("Top Hit with frequency %s" - + " and power %s" - + " has previously been used.", - NumericPrecisionPrefs.formatOther(dataPoint - .getFrequency()), NumericPrecisionPrefs - .formatOther(dataPoint.getPower())); - MessageBox.showErrorDialog(parent, algorithm - .getRefineByFrequencyName(), msg); - freqs.clear(); - break; - } - - if (resultantDataPoints.contains(dataPoint)) { - String msg = String.format("Top Hit with frequency %s" - + " and power %s" - + " was generated by %s so cannot be used.", - dataPoint.getFrequency(), dataPoint.getPower(), - algorithm.getRefineByFrequencyName()); - MessageBox.showErrorDialog(parent, algorithm - .getRefineByFrequencyName(), msg); - freqs.clear(); - break; - } - } - - if (!freqs.isEmpty()) { - try { - RefinementParameterDialog dialog = new RefinementParameterDialog( - parent, freqs, 6); - if (!dialog.isCancelled()) { - List variablePeriods = dialog - .getVariablePeriods(); - List lockedPeriods = dialog - .getLockedPeriods(); - - // Perform a refinement operation and get the new - // top-hits resulting from the refinement. - List newTopHits = algorithm - .refineByFrequency(freqs, variablePeriods, - lockedPeriods); - // Mark input frequencies as refined so we don't - // try to refine them again. - refinedDataPoints.addAll(inputDataPoints); - - // Update the model and tell anyone else who might - // be interested. - Map> data = algorithm - .getResultSeries(); - Map> topHits = algorithm - .getTopHits(); - - model.setData(topHits); - - PeriodAnalysisRefinementMessage msg = new PeriodAnalysisRefinementMessage( - this, data, topHits, newTopHits); - msg.setTag(Mediator.getParentDialogName(PeriodAnalysisTopHitsTablePane.this)); - Mediator.getInstance() - .getPeriodAnalysisRefinementNotifier() - .notifyListeners(msg); - } - } catch (AlgorithmError ex) { - MessageBox.showErrorDialog(parent, algorithm - .getRefineByFrequencyName(), ex - .getLocalizedMessage()); - } catch (InterruptedException ex) { - // Do nothing; just return. - } - } - } - }; - } - - /** - * Select the row in the table corresponding to the period analysis - * selection. We also enable the "refine" button. - */ - protected Listener createPeriodAnalysisListener() { - final Component parent = this; - - return new Listener() { - @Override - public void update(PeriodAnalysisSelectionMessage info) { - if (!Mediator.isMsgForDialog(Mediator.getParentDialog(PeriodAnalysisTopHitsTablePane.this), info)) - return; - if (info.getSource() != parent) { - // Find data point in top hits table. - int row = -1; - for (int i = 0; i < model.getRowCount(); i++) { - if (model.getDataPointFromRow(i).equals( - info.getDataPoint())) { - row = i; - break; - } - } - - // Note that the row may not correspond to anything in the - // top hits table since there's more data in the full - // dataset than there is here! - if (row != -1) { - // Convert to view index! - row = table.convertRowIndexToView(row); - - // Scroll to an arbitrary column (zeroth) within - // the selected row, then select that row. - // Assumption: we are specifying the zeroth cell - // within row i as an x,y coordinate relative to - // the top of the table pane. - // Note that we could call this on the scroll - // pane, which would then forward the request to - // the table pane anyway. - int colWidth = (int) table.getCellRect(row, 0, true) - .getWidth(); - int rowHeight = table.getRowHeight(row); - table.scrollRectToVisible(new Rectangle(colWidth, - rowHeight * row, colWidth, rowHeight)); - - boolean state = disableValueChangeEvent(); - try { - table.setRowSelectionInterval(row, row); - } finally { - setValueChangedDisabledState(state); - } - enableButtons(); - } else { - boolean state = disableValueChangeEvent(); - try { - table.clearSelection(); - } finally { - setValueChangedDisabledState(state); - } - } - } else { - enableButtons(); - } - } - - @Override - public boolean canBeRemoved() { - return true; - } - }; - } - - /** - * @see org.aavso.tools.vstar.ui.dialog.period.PeriodAnalysisDataTablePane#enableButtons() - */ - @Override - protected void enableButtons() { - super.enableButtons(); - refineButton.setEnabled(true); - } - - /** - * We send a period analysis selection message when the table selection - * value has "settled". This event could be consumed by other views such as - * plots. - */ - @Override - public void valueChanged(ListSelectionEvent e) { - if (isValueChangeDisabled()) - return; - - if (e.getSource() == table.getSelectionModel() - && table.getRowSelectionAllowed() && !e.getValueIsAdjusting()) { - // Which row in the top hits table was selected? - int row = table.getSelectedRow(); - - if (row >= 0) { - row = table.convertRowIndexToModel(row); - PeriodAnalysisSelectionMessage message = new PeriodAnalysisSelectionMessage( - this, model.getDataPointFromRow(row), row); - message.setTag(Mediator.getParentDialogName(this)); - Mediator.getInstance().getPeriodAnalysisSelectionNotifier() - .notifyListeners(message); - } - } - } - - // Create a period analysis refinement listener which adds refinement - // results to a collection that is checked to ensure that the user does not - // select them (or the originating data row) again. - private Listener createRefinementListener() { - return new Listener() { - @Override - public void update(PeriodAnalysisRefinementMessage info) { - if (!Mediator.isMsgForDialog(Mediator.getParentDialog(PeriodAnalysisTopHitsTablePane.this), info)) - return; - resultantDataPoints.addAll(info.getNewTopHits()); - } - - @Override - public boolean canBeRemoved() { - return true; - } - }; - } - - /** - * @see org.aavso.tools.vstar.ui.dialog.period.PeriodAnalysisDataTablePane#startup() - */ - @Override - public void startup() { - super.startup(); - - periodAnalysisRefinementListener = createRefinementListener(); - Mediator.getInstance().getPeriodAnalysisRefinementNotifier() - .addListener(periodAnalysisRefinementListener); - } - - /** - * @see org.aavso.tools.vstar.ui.dialog.period.PeriodAnalysisDataTablePane#cleanup() - */ - @Override - public void cleanup() { - super.cleanup(); - - Mediator.getInstance().getPeriodAnalysisRefinementNotifier() - .removeListenerIfWilling(periodAnalysisRefinementListener); - } + private Set refinedDataPoints; + private Set resultantDataPoints; + + private JButton refineButton; + + private Listener periodAnalysisRefinementListener; + + /** + * Constructor. + * + * @param topHitsModel The top hits data model. + * @param fullDataModel The full data data model. + * @param algorithm The period analysis algorithm. + */ + public PeriodAnalysisTopHitsTablePane(PeriodAnalysisDataTableModel topHitsModel, + PeriodAnalysisDataTableModel fullDataModel, IPeriodAnalysisAlgorithm algorithm) { + super(topHitsModel, algorithm); + + refinedDataPoints = new TreeSet(PeriodAnalysisDataPointComparator.instance); + + resultantDataPoints = new TreeSet(PeriodAnalysisDataPointComparator.instance); + } + + protected JPanel createButtonPanel() { + JPanel buttonPane = super.createButtonPanel(); + + String refineName = algorithm.getRefineByFrequencyName(); + if (refineName != null) { + refineButton = new JButton(refineName); + refineButton.setEnabled(false); + refineButton.addActionListener(createRefineButtonHandler()); + buttonPane.add(refineButton, BorderLayout.LINE_START); + } + + return buttonPane; + } + + // Refine button listener. + private ActionListener createRefineButtonHandler() { + final JPanel parent = this; + return new ActionListener() { + public void actionPerformed(ActionEvent e) { + // Collect frequencies to be used in refinement, ensuring that + // we don't try to use a frequency that has already been used + // for refinement. We also do not want to use the result of a + // previous refinement. + List freqs = new ArrayList(); + int[] selectedTableRowIndices = table.getSelectedRows(); + List inputDataPoints = new ArrayList(); + + for (int row : selectedTableRowIndices) { + int modelRow = table.convertRowIndexToModel(row); + PeriodAnalysisDataPoint dataPoint = model.getDataPointFromRow(modelRow); + + if (!refinedDataPoints.contains(dataPoint)) { + inputDataPoints.add(dataPoint); + freqs.add(dataPoint.getFrequency()); + } else { + String msg = String.format( + "Top Hit with frequency %s" + " and power %s" + " has previously been used.", + NumericPrecisionPrefs.formatOther(dataPoint.getFrequency()), + NumericPrecisionPrefs.formatOther(dataPoint.getPower())); + MessageBox.showErrorDialog(parent, algorithm.getRefineByFrequencyName(), msg); + freqs.clear(); + break; + } + + if (resultantDataPoints.contains(dataPoint)) { + String msg = String.format( + "Top Hit with frequency %s" + " and power %s" + + " was generated by %s so cannot be used.", + dataPoint.getFrequency(), dataPoint.getPower(), algorithm.getRefineByFrequencyName()); + MessageBox.showErrorDialog(parent, algorithm.getRefineByFrequencyName(), msg); + freqs.clear(); + break; + } + } + + if (!freqs.isEmpty()) { + try { + RefinementParameterDialog dialog = new RefinementParameterDialog(parent, freqs, 6); + if (!dialog.isCancelled()) { + List variablePeriods = dialog.getVariablePeriods(); + List lockedPeriods = dialog.getLockedPeriods(); + + // Perform a refinement operation and get the new + // top-hits resulting from the refinement. + List newTopHits = algorithm.refineByFrequency(freqs, + variablePeriods, lockedPeriods); + // Mark input frequencies as refined so we don't + // try to refine them again. + refinedDataPoints.addAll(inputDataPoints); + + // Update the model and tell anyone else who might + // be interested. + Map> data = algorithm.getResultSeries(); + Map> topHits = algorithm.getTopHits(); + + model.setData(topHits); + + PeriodAnalysisRefinementMessage msg = new PeriodAnalysisRefinementMessage(this, data, + topHits, newTopHits); + msg.setTag(Mediator.getParentDialogName(PeriodAnalysisTopHitsTablePane.this)); + Mediator.getInstance().getPeriodAnalysisRefinementNotifier().notifyListeners(msg); + } + } catch (AlgorithmError ex) { + MessageBox.showErrorDialog(parent, algorithm.getRefineByFrequencyName(), + ex.getLocalizedMessage()); + } catch (InterruptedException ex) { + // Do nothing; just return. + } + } + } + }; + } + + /** + * Select the row in the table corresponding to the period analysis selection. + * We also enable the "refine" button. + */ + protected Listener createPeriodAnalysisListener() { + final Component parent = this; + + return new Listener() { + @Override + public void update(PeriodAnalysisSelectionMessage info) { + if (!Mediator.isMsgForDialog(Mediator.getParentDialog(PeriodAnalysisTopHitsTablePane.this), info)) + return; + if (info.getSource() != parent) { + // Find data point in top hits table. + int row = -1; + for (int i = 0; i < model.getRowCount(); i++) { + if (model.getDataPointFromRow(i).equals(info.getDataPoint())) { + row = i; + break; + } + } + + // Note that the row may not correspond to anything in the + // top hits table since there's more data in the full + // dataset than there is here! + if (row != -1) { + // Convert to view index! + row = table.convertRowIndexToView(row); + + // Scroll to an arbitrary column (zeroth) within + // the selected row, then select that row. + // Assumption: we are specifying the zeroth cell + // within row i as an x,y coordinate relative to + // the top of the table pane. + // Note that we could call this on the scroll + // pane, which would then forward the request to + // the table pane anyway. + int colWidth = (int) table.getCellRect(row, 0, true).getWidth(); + int rowHeight = table.getRowHeight(row); + table.scrollRectToVisible(new Rectangle(colWidth, rowHeight * row, colWidth, rowHeight)); + + boolean state = disableValueChangeEvent(); + try { + table.setRowSelectionInterval(row, row); + } finally { + setValueChangedDisabledState(state); + } + enableButtons(); + } else { + boolean state = disableValueChangeEvent(); + try { + table.clearSelection(); + } finally { + setValueChangedDisabledState(state); + } + } + } else { + enableButtons(); + } + } + + @Override + public boolean canBeRemoved() { + return true; + } + }; + } + + /** + * @see org.aavso.tools.vstar.ui.dialog.period.PeriodAnalysisDataTablePane#enableButtons() + */ + @Override + protected void enableButtons() { + super.enableButtons(); + if (refineButton != null) { + refineButton.setEnabled(true); + } + } + + /** + * We send a period analysis selection message when the table selection value + * has "settled". This event could be consumed by other views such as plots. + */ + @Override + public void valueChanged(ListSelectionEvent e) { + if (isValueChangeDisabled()) + return; + + if (e.getSource() == table.getSelectionModel() && table.getRowSelectionAllowed() && !e.getValueIsAdjusting()) { + // Which row in the top hits table was selected? + int row = table.getSelectedRow(); + + if (row >= 0) { + row = table.convertRowIndexToModel(row); + PeriodAnalysisSelectionMessage message = new PeriodAnalysisSelectionMessage(this, + model.getDataPointFromRow(row), row); + message.setTag(Mediator.getParentDialogName(this)); + Mediator.getInstance().getPeriodAnalysisSelectionNotifier().notifyListeners(message); + } + } + } + + // Create a period analysis refinement listener which adds refinement + // results to a collection that is checked to ensure that the user does not + // select them (or the originating data row) again. + private Listener createRefinementListener() { + return new Listener() { + @Override + public void update(PeriodAnalysisRefinementMessage info) { + if (!Mediator.isMsgForDialog(Mediator.getParentDialog(PeriodAnalysisTopHitsTablePane.this), info)) + return; + resultantDataPoints.addAll(info.getNewTopHits()); + } + + @Override + public boolean canBeRemoved() { + return true; + } + }; + } + + /** + * @see org.aavso.tools.vstar.ui.dialog.period.PeriodAnalysisDataTablePane#startup() + */ + @Override + public void startup() { + super.startup(); + + periodAnalysisRefinementListener = createRefinementListener(); + Mediator.getInstance().getPeriodAnalysisRefinementNotifier().addListener(periodAnalysisRefinementListener); + } + + /** + * @see org.aavso.tools.vstar.ui.dialog.period.PeriodAnalysisDataTablePane#cleanup() + */ + @Override + public void cleanup() { + super.cleanup(); + + Mediator.getInstance().getPeriodAnalysisRefinementNotifier() + .removeListenerIfWilling(periodAnalysisRefinementListener); + } } diff --git a/src/org/aavso/tools/vstar/util/AbstractExtremaFinder.java b/src/org/aavso/tools/vstar/util/AbstractExtremaFinder.java index 5c09c825e..bc7508245 100644 --- a/src/org/aavso/tools/vstar/util/AbstractExtremaFinder.java +++ b/src/org/aavso/tools/vstar/util/AbstractExtremaFinder.java @@ -60,7 +60,7 @@ public AbstractExtremaFinder(List obs, * @param goal * Minimum or maximum? * @param bracketRange - * The inclusive time range within which to look for the + * The inclusive time range indices within which to look for the * extremum. */ abstract public void find(GoalType goal, int[] bracketRange) diff --git a/src/org/aavso/tools/vstar/util/model/AbstractModel.java b/src/org/aavso/tools/vstar/util/model/AbstractModel.java new file mode 100644 index 000000000..6433f2f17 --- /dev/null +++ b/src/org/aavso/tools/vstar/util/model/AbstractModel.java @@ -0,0 +1,296 @@ +/** + * VStar: a statistical analysis tool for variable star data. + * Copyright (C) 2010 AAVSO (http://www.aavso.org/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.aavso.tools.vstar.util.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.aavso.tools.vstar.data.DateInfo; +import org.aavso.tools.vstar.data.Magnitude; +import org.aavso.tools.vstar.data.SeriesType; +import org.aavso.tools.vstar.data.ValidObservation; +import org.aavso.tools.vstar.exception.AlgorithmError; +import org.aavso.tools.vstar.ui.mediator.AnalysisType; +import org.aavso.tools.vstar.ui.mediator.Mediator; +import org.aavso.tools.vstar.ui.model.plot.ContinuousModelFunction; +import org.aavso.tools.vstar.ui.model.plot.ICoordSource; +import org.aavso.tools.vstar.ui.model.plot.JDCoordSource; +import org.aavso.tools.vstar.ui.model.plot.JDTimeElementEntity; +import org.aavso.tools.vstar.ui.model.plot.StandardPhaseCoordSource; +import org.aavso.tools.vstar.util.comparator.JDComparator; +import org.aavso.tools.vstar.util.comparator.StandardPhaseComparator; +import org.aavso.tools.vstar.util.locale.LocaleProps; +import org.aavso.tools.vstar.util.prefs.NumericPrecisionPrefs; +import org.aavso.tools.vstar.util.stats.DescStats; + +/** + * Model classes can use this abstract base class to implement common code + * instead of implementing the IModel interface. + */ +public abstract class AbstractModel implements IModel { + + protected boolean interrupted; + + protected List obs; + + protected List fit; + protected List residuals; + + protected Map functionStrMap; + + protected ICoordSource timeCoordSource; + protected Comparator timeComparator; + + protected double zeroPoint; + + protected double sumSqResiduals = 0; + + protected double aic = Double.NaN; + protected double bic = Double.NaN; + protected double rms = Double.NaN; + + public AbstractModel(List obs) { + this.obs = obs; + fit = new ArrayList(); + residuals = new ArrayList(); + functionStrMap = new TreeMap(); + interrupted = false; + + // Select time mode (JD or phase). + switch (Mediator.getInstance().getAnalysisType()) { + case RAW_DATA: + timeCoordSource = JDCoordSource.instance; + timeComparator = JDComparator.instance; + this.obs = obs; + zeroPoint = DescStats.calcTimeElementMean(obs, JDTimeElementEntity.instance); + break; + + case PHASE_PLOT: + timeCoordSource = StandardPhaseCoordSource.instance; + timeComparator = StandardPhaseComparator.instance; + this.obs = new ArrayList(obs); + Collections.sort(this.obs, timeComparator); + zeroPoint = 0; + break; + } + } + + /** + * Default behaviour for model run interrupt: set flag. + */ + @Override + public void interrupt() { + interrupted = true; + } + + /** + * Collect fit and residual observations and incrementally compute the sum of + * squared residuals for use in fit metrics. + * + * @param modelValue A model-computed value + * @param ob The observation (at time t) being modeled + */ + public void collectObs(double modelValue, ValidObservation ob, String comment) { + ValidObservation fitOb = new ValidObservation(); + fitOb.setDateInfo(new DateInfo(ob.getJD())); + if (Mediator.getInstance().getAnalysisType() == AnalysisType.PHASE_PLOT) { + fitOb.setPreviousCyclePhase(ob.getPreviousCyclePhase()); + fitOb.setStandardPhase(ob.getStandardPhase()); + } + fitOb.setMagnitude(new Magnitude(modelValue, 0)); + fitOb.setBand(SeriesType.Model); + fitOb.setComments(comment); + fit.add(fitOb); + + ValidObservation resOb = new ValidObservation(); + resOb.setDateInfo(new DateInfo(ob.getJD())); + if (Mediator.getInstance().getAnalysisType() == AnalysisType.PHASE_PLOT) { + resOb.setPreviousCyclePhase(ob.getPreviousCyclePhase()); + resOb.setStandardPhase(ob.getStandardPhase()); + } + double residual = ob.getMag() - modelValue; + resOb.setMagnitude(new Magnitude(residual, 0)); + resOb.setBand(SeriesType.Residuals); + resOb.setComments(comment); + residuals.add(resOb); + + sumSqResiduals += (residual * residual); + } + + /** + * Return the fitted observations, after having executed the algorithm. + * + * @return A list of observations that represent the fit. + */ + public List getFit() { + return fit; + } + + /** + * Return the residuals as observations, after having executed the algorithm. + * + * @return A list of observations that represent the residuals. + */ + public List getResiduals() { + return residuals; + } + + /** + * Return the list of coefficients that gives rise to the model. May return + * null. + * + * @return A list of fit coefficients or null if none available. + */ + public List getParameters() { + return null; + } + + /** + * Does this model have a function-based description? + * + * @return True or false. + */ + public boolean hasFuncDesc() { + return false; + } + + /** + * Return a mapping from names to strings representing model functions. + * + * @return The model function string map. + */ + public Map getFunctionStrings() { + return functionStrMap; + } + + /** + * Returns the model function and context. This is required for creating a line + * plot to show the model as a continuous function. If a model creator cannot + * sensibly return such a function, it may return null and no such plot will be + * possible. It could also be useful for analysis purposes, e.g. analytic + * extrema finding.
+ * + * @return The function object. + */ + public ContinuousModelFunction getModelFunction() { + return null; + } + + /** + * Compute the root mean square. + * + * pre-condition: assumes sum of squared residuals has been computed + */ + public void rootMeanSquare() { + rms = Math.sqrt(sumSqResiduals / residuals.size()); + } + + /** + * Compute the Bayesian and Aikake Information Criteria (BIC and AIC) fit + * metrics + * + * pre-condition: assumes sum of squared residuals has been computed + * + * @param numberOfEstimatedParams The number of estimated parameters, e.g. + * polynomial degree + */ + public void informationCriteria(double numberOfEstimatedParams) { + int n = residuals.size(); + if (n != 0 && sumSqResiduals / n != 0) { + double commonIC = n * Math.log(sumSqResiduals / n); + aic = commonIC + 2 * numberOfEstimatedParams; + bic = commonIC + numberOfEstimatedParams * Math.log(n); + } + } + + /** + * Gather fit metrics string. + * + * @throws AlgorithmError + */ + public void fitMetrics() throws AlgorithmError { + String strRepr = functionStrMap.get(LocaleProps.get("MODEL_INFO_FIT_METRICS_TITLE")); + + if (strRepr == null) { + // Goodness of fit + if (rms != Double.NaN) { + strRepr = "RMS: " + NumericPrecisionPrefs.formatOther(rms); + } + + // Akaike and Bayesean Information Criteria + if (aic != Double.NaN && bic != Double.NaN) { + strRepr += "\nAIC: " + NumericPrecisionPrefs.formatOther(aic); + strRepr += "\nBIC: " + NumericPrecisionPrefs.formatOther(bic); + } + } + + functionStrMap.put(LocaleProps.get("MODEL_INFO_FIT_METRICS_TITLE"), strRepr); + } + + /** + * @return a string representing the model as a VeLa function + */ + abstract public String toVeLaString(); + + /** + * Put function strings into the map + */ + public void functionStrings() { + functionStrMap.put(LocaleProps.get("MODEL_INFO_FUNCTION_TITLE"), toVeLaString()); + } + + /** + * @return Aikake Information Criteria + */ + public double getAIC() { + return aic; + } + + /** + * @return Bayesian Information Criteria + */ + public double getBIC() { + return bic; + } + + /** + * @return Root mean square + */ + public double getRMS() { + return rms; + } + + /** + * Return a human-readable description of this model. + * + * @return The model description. + */ + abstract public String getDescription(); + + /** + * Return a human-readable 'kind' string (e.g. "Model", "Polynomial Fit") + * indicating what kind of a model this is. + * + * @return The 'kind' string. + */ + abstract public String getKind(); +} diff --git a/src/org/aavso/tools/vstar/util/prefs/NumericPrecisionPrefs.java b/src/org/aavso/tools/vstar/util/prefs/NumericPrecisionPrefs.java index 8530ee8f2..f5879de96 100644 --- a/src/org/aavso/tools/vstar/util/prefs/NumericPrecisionPrefs.java +++ b/src/org/aavso/tools/vstar/util/prefs/NumericPrecisionPrefs.java @@ -71,7 +71,7 @@ public String toString() { private static int DEFAULT_TIME_DECIMAL_PLACES = 5; private static int DEFAULT_MAG_DECIMAL_PLACES = 6; - private static int DEFAULT_OTHER_DECIMAL_PLACES = 6; + private static int DEFAULT_OTHER_DECIMAL_PLACES = 12; // Current decimal place values. @@ -209,14 +209,18 @@ public static String getOtherInputFormat() { // 2) long number strings without decimal separator cannot be parsed too (i.e. -608516245008941) // General format has both issues; scientific one has the (1) only. // However, the general format looks more natural. + // + // DJB (2025-01-09): + // This generalises to other coefficients, e.g. those required for a + // piecewise linear model. - // PolyCoefFormat + // CoefFormat - public static String formatPolyCoef(double num) { + public static String formatCoef(double num) { return formatScientific(num); } - public static String formatPolyCoefLocaleIndependent(double num) { + public static String formatCoefLocaleIndependent(double num) { return formatScientificLocaleIndependent(num); } diff --git a/test/org/aavso/tools/vstar/util/polyfit/TSPolynomialFitterTest.java b/test/org/aavso/tools/vstar/util/polyfit/TSPolynomialFitterTest.java index 1d9612e7e..c28901260 100644 --- a/test/org/aavso/tools/vstar/util/polyfit/TSPolynomialFitterTest.java +++ b/test/org/aavso/tools/vstar/util/polyfit/TSPolynomialFitterTest.java @@ -2101,14 +2101,6 @@ public TSPolynomialFitterTest(String name) { } } - protected void setUp() throws Exception { - super.setUp(); - } - - protected void tearDown() throws Exception { - super.tearDown(); - } - // Polynomial fit test using delta Cephei data. public void testDegree2() { TSPolynomialFitter fitter = new TSPolynomialFitter(obs);