From fbf3c6d0ecaed2ca75eaa2c0ca44bdf4adde8b4e Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Fri, 9 Sep 2022 14:24:57 +0300 Subject: [PATCH 01/14] v1.97 --- src/main/java/pulse/AbstractData.java | 2 +- src/main/java/pulse/DiscreteInput.java | 45 ++ src/main/java/pulse/HeatingCurve.java | 31 +- src/main/java/pulse/Response.java | 25 + .../pulse/baseline/AdjustableBaseline.java | 69 ++- src/main/java/pulse/baseline/Baseline.java | 99 +--- .../java/pulse/baseline/FlatBaseline.java | 22 +- .../java/pulse/baseline/LinearBaseline.java | 95 +-- .../pulse/baseline/SinusoidalBaseline.java | 461 ++++++++++----- .../java/pulse/input/ExperimentalData.java | 198 ++----- src/main/java/pulse/input/IndexRange.java | 27 +- src/main/java/pulse/input/Range.java | 84 ++- .../io/export/ResidualStatisticExporter.java | 10 +- .../pulse/io/readers/NetzschCSVReader.java | 101 ++-- src/main/java/pulse/math/FFTTransformer.java | 136 +++++ src/main/java/pulse/math/Harmonic.java | 266 +++++++++ src/main/java/pulse/math/Parameter.java | 91 +++ .../java/pulse/math/ParameterIdentifier.java | 53 ++ src/main/java/pulse/math/ParameterVector.java | 248 +++----- src/main/java/pulse/math/Segment.java | 2 + src/main/java/pulse/math/Window.java | 60 ++ src/main/java/pulse/math/ZScore.java | 134 +++++ .../math/filters/AssignmentListener.java | 7 + src/main/java/pulse/math/filters/Filter.java | 14 + .../math/filters/HalfTimeCalculator.java | 95 +++ .../math/filters/OptimisablePolyline.java | 62 ++ .../math/filters/OptimisedRunningAverage.java | 26 + .../pulse/math/filters/PolylineOptimiser.java | 90 +++ .../java/pulse/math/filters/Randomiser.java | 26 + .../pulse/math/filters/RunningAverage.java | 131 +++++ src/main/java/pulse/math/linear/Vector.java | 5 +- .../math/transforms/InvLenSqTransform.java | 27 - .../math/transforms/InvLenTransform.java | 26 - .../math/transforms/PeriodicTransform.java | 38 ++ .../transforms/StandardTransformations.java | 3 + .../pulse/math/transforms/StickTransform.java | 2 - .../pulse/problem/laser/DiscretePulse.java | 9 +- .../pulse/problem/laser/NumericPulse.java | 6 +- .../pulse/problem/laser/NumericPulseData.java | 21 +- .../schemes/CoupledImplicitScheme.java | 4 +- .../problem/schemes/DifferenceScheme.java | 9 +- .../problem/schemes/FixedPointIterations.java | 27 +- .../pulse/problem/schemes/ImplicitScheme.java | 4 +- .../schemes/TridiagonalMatrixAlgorithm.java | 2 +- .../solvers/ExplicitCoupledSolver.java | 4 +- .../solvers/ImplicitCoupledSolver.java | 4 +- .../solvers/ImplicitDiathermicSolver.java | 4 +- .../solvers/ImplicitLinearisedSolver.java | 16 +- .../solvers/ImplicitTwoTemperatureSolver.java | 226 ++++++++ .../schemes/solvers/SolverException.java | 23 +- .../problem/statements/ClassicalProblem.java | 22 +- .../statements/ClassicalProblem2D.java | 27 +- .../problem/statements/DiathermicMedium.java | 32 +- .../problem/statements/NonlinearProblem.java | 25 +- .../statements/ParticipatingMedium.java | 6 +- .../statements/PenetrationProblem.java | 15 +- .../pulse/problem/statements/Problem.java | 115 ++-- .../java/pulse/problem/statements/Pulse.java | 10 +- .../statements/TwoTemperatureModel.java | 176 ++++++ .../statements/model/AbsorptionModel.java | 30 +- .../model/BeerLambertAbsorption.java | 9 + .../pulse/problem/statements/model/Gas.java | 75 +++ .../problem/statements/model/Helium.java | 14 + .../problem/statements/model/Insulator.java | 14 + .../problem/statements/model/Nitrogen.java | 14 + .../statements/model/ThermalProperties.java | 11 +- .../model/ThermoOpticalProperties.java | 21 +- .../model/TwoTemperatureProperties.java | 110 ++++ src/main/java/pulse/properties/Flag.java | 12 +- .../pulse/properties/NumericProperty.java | 3 - .../properties/NumericPropertyKeyword.java | 34 +- src/main/java/pulse/search/GeneralTask.java | 217 +++++++ src/main/java/pulse/search/Optimisable.java | 10 +- .../pulse/search/SimpleOptimisationTask.java | 93 +++ .../java/pulse/search/SimpleResponse.java | 35 ++ .../pulse/search/direction/ActiveFlags.java | 56 +- .../pulse/search/direction/BFGSOptimiser.java | 4 +- .../pulse/search/direction/ComplexPath.java | 7 +- .../direction/CompositePathOptimiser.java | 27 +- .../direction/GradientBasedOptimiser.java | 77 ++- .../search/direction/GradientGuidedPath.java | 11 +- .../direction/HessianDirectionSolver.java | 3 +- .../search/direction/IterativeState.java | 5 + .../pulse/search/direction/LMOptimiser.java | 75 +-- .../java/pulse/search/direction/LMPath.java | 6 +- .../pulse/search/direction/PathOptimiser.java | 10 +- .../pulse/search/direction/SR1Optimiser.java | 3 +- .../direction/SteepestDescentOptimiser.java | 9 +- .../direction/pso/ConstrictionMover.java | 49 ++ .../pulse/search/direction/pso/FIPSMover.java | 33 +- .../pulse/search/direction/pso/Mover.java | 4 +- .../pulse/search/direction/pso/Particle.java | 5 +- .../search/direction/pso/ParticleState.java | 31 +- .../direction/pso/ParticleSwarmOptimiser.java | 84 +-- .../direction/pso/StaticTopologies.java | 2 +- .../search/direction/pso/SwarmState.java | 36 +- .../search/linear/GoldenSectionOptimiser.java | 12 +- .../pulse/search/linear/LinearOptimiser.java | 33 +- .../pulse/search/linear/WolfeOptimiser.java | 10 +- .../search/statistics/AbsoluteDeviations.java | 9 +- .../statistics/AndersonDarlingTest.java | 10 +- .../search/statistics/CorrelationTest.java | 2 +- .../pulse/search/statistics/EmptyTest.java | 6 +- .../java/pulse/search/statistics/FTest.java | 16 - .../java/pulse/search/statistics/KSTest.java | 9 +- .../statistics/ModelSelectionCriterion.java | 10 +- .../search/statistics/NormalityTest.java | 3 +- .../pulse/search/statistics/RSquaredTest.java | 38 +- .../RangePenalisedLeastSquares.java | 60 ++ .../statistics/RegularisedLeastSquares.java | 24 +- .../search/statistics/ResidualStatistic.java | 110 ++-- .../pulse/search/statistics/Statistic.java | 7 +- .../pulse/search/statistics/SumOfSquares.java | 10 +- src/main/java/pulse/tasks/Calculation.java | 41 +- src/main/java/pulse/tasks/SearchTask.java | 542 ++++++++---------- src/main/java/pulse/tasks/TaskManager.java | 58 +- .../pulse/tasks/logs/CorrelationLogEntry.java | 14 +- .../java/pulse/tasks/logs/DataLogEntry.java | 37 +- src/main/java/pulse/tasks/logs/Log.java | 61 +- src/main/java/pulse/tasks/logs/Status.java | 20 +- .../java/pulse/tasks/processing/Buffer.java | 15 +- .../tasks/processing/CorrelationBuffer.java | 55 +- .../java/pulse/tasks/processing/Result.java | 3 +- src/main/java/pulse/ui/Launcher.java | 2 +- .../pulse/ui/components/CalculationTable.java | 16 +- src/main/java/pulse/ui/components/Chart.java | 33 +- .../java/pulse/ui/components/DataLoader.java | 8 +- .../java/pulse/ui/components/LogPane.java | 2 +- .../java/pulse/ui/components/ProblemTree.java | 7 +- .../pulse/ui/components/PulseMainMenu.java | 4 +- .../pulse/ui/components/RangeTextFields.java | 29 +- .../pulse/ui/components/ResidualsChart.java | 4 +- .../java/pulse/ui/components/ResultTable.java | 63 +- .../pulse/ui/components/TaskPopupMenu.java | 19 +- .../components/buttons/ExecutionButton.java | 4 +- .../controllers/InstanceCellEditor.java | 6 +- .../components/models/ResultTableModel.java | 9 +- .../ui/components/models/TaskTableModel.java | 11 +- .../ui/components/panels/ChartToolbar.java | 10 +- .../ui/components/panels/ProblemToolbar.java | 8 +- .../java/pulse/ui/frames/HistogramFrame.java | 5 +- .../java/pulse/ui/frames/MainGraphFrame.java | 5 +- .../ui/frames/ProblemStatementFrame.java | 154 +++-- .../pulse/ui/frames/SearchOptionsFrame.java | 10 +- .../pulse/ui/frames/TaskControlFrame.java | 8 +- src/main/java/pulse/util/Group.java | 6 +- .../java/pulse/util/UpwardsNavigable.java | 11 +- 147 files changed, 4497 insertions(+), 1989 deletions(-) create mode 100644 src/main/java/pulse/DiscreteInput.java create mode 100644 src/main/java/pulse/Response.java create mode 100644 src/main/java/pulse/math/FFTTransformer.java create mode 100644 src/main/java/pulse/math/Harmonic.java create mode 100644 src/main/java/pulse/math/Parameter.java create mode 100644 src/main/java/pulse/math/ParameterIdentifier.java create mode 100644 src/main/java/pulse/math/Window.java create mode 100644 src/main/java/pulse/math/ZScore.java create mode 100644 src/main/java/pulse/math/filters/AssignmentListener.java create mode 100644 src/main/java/pulse/math/filters/Filter.java create mode 100644 src/main/java/pulse/math/filters/HalfTimeCalculator.java create mode 100644 src/main/java/pulse/math/filters/OptimisablePolyline.java create mode 100644 src/main/java/pulse/math/filters/OptimisedRunningAverage.java create mode 100644 src/main/java/pulse/math/filters/PolylineOptimiser.java create mode 100644 src/main/java/pulse/math/filters/Randomiser.java create mode 100644 src/main/java/pulse/math/filters/RunningAverage.java delete mode 100644 src/main/java/pulse/math/transforms/InvLenSqTransform.java delete mode 100644 src/main/java/pulse/math/transforms/InvLenTransform.java create mode 100644 src/main/java/pulse/math/transforms/PeriodicTransform.java create mode 100644 src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java create mode 100644 src/main/java/pulse/problem/statements/TwoTemperatureModel.java create mode 100644 src/main/java/pulse/problem/statements/model/Gas.java create mode 100644 src/main/java/pulse/problem/statements/model/Helium.java create mode 100644 src/main/java/pulse/problem/statements/model/Nitrogen.java create mode 100644 src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java create mode 100644 src/main/java/pulse/search/GeneralTask.java create mode 100644 src/main/java/pulse/search/SimpleOptimisationTask.java create mode 100644 src/main/java/pulse/search/SimpleResponse.java create mode 100644 src/main/java/pulse/search/direction/pso/ConstrictionMover.java create mode 100644 src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index 1fd6264..a1d276c 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -306,4 +306,4 @@ public boolean equals(Object o) { } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/DiscreteInput.java b/src/main/java/pulse/DiscreteInput.java new file mode 100644 index 0000000..c6bb8d8 --- /dev/null +++ b/src/main/java/pulse/DiscreteInput.java @@ -0,0 +1,45 @@ +package pulse; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import pulse.input.IndexRange; +import pulse.math.Segment; + +public interface DiscreteInput { + + public List getX(); + public List getY(); + public IndexRange getIndexRange(); + + public static List convert(double[] x, double[] y) { + + var ps = new ArrayList(); + + for(int i = 0, size = x.length; i < size; i++) { + ps.add(new Point2D.Double(x[i], y[i])); + } + + return ps; + + } + + public static List convert(List x, List y) { + + var ps = new ArrayList(); + + for(int i = 0, size = x.size(); i < size; i++) { + ps.add(new Point2D.Double(x.get(i), y.get(i))); + } + + return ps; + + } + + public default Segment bounds() { + var ir = getIndexRange(); + var x = getX(); + return new Segment(x.get(ir.getLowerBound()), x.get(ir.getUpperBound())); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/HeatingCurve.java b/src/main/java/pulse/HeatingCurve.java index 8fb0f55..86e1462 100644 --- a/src/main/java/pulse/HeatingCurve.java +++ b/src/main/java/pulse/HeatingCurve.java @@ -10,6 +10,7 @@ import static pulse.properties.NumericPropertyKeyword.TIME_SHIFT; import java.util.ArrayList; +import static java.util.Collections.max; import java.util.List; import java.util.Set; @@ -48,8 +49,8 @@ public class HeatingCurve extends AbstractData { private final List listeners = new ArrayList<>(); - private UnivariateInterpolator splineInterpolator; - private UnivariateFunction splineInterpolation; + private UnivariateInterpolator interpolator; + private UnivariateFunction interpolation; protected HeatingCurve(List time, List signal, final double startTime, String name) { super(time, name); @@ -64,7 +65,7 @@ protected HeatingCurve(List time, List signal, final double star public HeatingCurve() { super(); adjustedSignal = new ArrayList<>((int) this.getNumPoints().getValue()); - splineInterpolator = new SplineInterpolator(); + interpolator = new SplineInterpolator(); } /** @@ -78,8 +79,8 @@ public HeatingCurve(HeatingCurve c) { super(c); this.adjustedSignal = new ArrayList<>(c.adjustedSignal); this.startTime = c.startTime; - splineInterpolator = new SplineInterpolator(); - if (c.splineInterpolation != null) { + interpolator = new SplineInterpolator(); + if (c.interpolation != null) { this.refreshInterpolation(); } } @@ -100,7 +101,7 @@ public HeatingCurve(NumericProperty count) { adjustedSignal = new ArrayList<>((int) count.getValue()); startTime = (double) def(TIME_SHIFT).getValue(); - splineInterpolator = new SplineInterpolator(); + interpolator = new SplineInterpolator(); } //TODO @@ -163,7 +164,7 @@ public double signalAt(int index) { */ public void scale(double scale) { final int count = this.actualNumPoints(); - for (int i = 0; i < count; i++) { + for (int i = 0, max = Math.min(count, signal.size()); i < max; i++) { signal.set(i, signal.get(i) * scale); } var dataEvent = new CurveEvent(RESCALED, this); @@ -201,7 +202,8 @@ private void refreshInterpolation() { /* * Submit to spline interpolation */ - splineInterpolation = splineInterpolator.interpolate(timeExtended, adjustedSignalExtended); + + interpolation = interpolator.interpolate(timeExtended, adjustedSignalExtended); } /** @@ -346,8 +348,8 @@ public void setTimeShift(NumericProperty startTime) { firePropertyChanged(this, startTime); } - public UnivariateFunction getSplineInterpolation() { - return splineInterpolation; + public UnivariateFunction getInterpolation() { + return interpolation; } public List getBaselineCorrectedData() { @@ -377,5 +379,12 @@ public boolean equals(Object o) { return super.equals(o) && adjustedSignal.containsAll(((HeatingCurve) o).adjustedSignal); } + + public double interpolateSignalAt(double x) { + double min = this.timeAt(0); + double max = timeLimit(); + return min < x && max > x ? interpolation.value(x) + : (x < min ? signalAt(0) : signalAt(actualNumPoints() - 1)); + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/Response.java b/src/main/java/pulse/Response.java new file mode 100644 index 0000000..cc4f23c --- /dev/null +++ b/src/main/java/pulse/Response.java @@ -0,0 +1,25 @@ +package pulse; + +import pulse.math.Segment; +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; +import pulse.search.statistics.OptimiserStatistic; + +public interface Response { + + public double evaluate(double t); + public Segment accessibleRange(); + + /** + * Calculates the value of the objective function used to identify + * the current state of the optimiser. + * @param task + * @return the value of the objective function in the current state + * @throws pulse.problem.schemes.solvers.SolverException + */ + + public double objectiveFunction(GeneralTask task) throws SolverException; + + public OptimiserStatistic getOptimiserStatistic(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/AdjustableBaseline.java b/src/main/java/pulse/baseline/AdjustableBaseline.java index bb69ca9..12464c7 100644 --- a/src/main/java/pulse/baseline/AdjustableBaseline.java +++ b/src/main/java/pulse/baseline/AdjustableBaseline.java @@ -1,16 +1,16 @@ package pulse.baseline; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; import java.util.List; +import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.properties.Flag; +import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; +import static pulse.properties.NumericProperty.requireType; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.BASELINE_SLOPE; import pulse.util.PropertyHolder; /** @@ -21,27 +21,31 @@ public abstract class AdjustableBaseline extends Baseline { private double intercept; + private double slope; /** * Creates a flat baseline equal to the argument. * * @param intercept the constant baseline value. */ - public AdjustableBaseline(double intercept) { + public AdjustableBaseline(double intercept, double slope) { this.intercept = intercept; + this.slope = slope; } /** - * @return the constant value of this {@code FlatBaseline} + * Calculates the linear function {@code g(x) = intercept + slope*time} + * + * @param x the argument of the linear function + * @return the result of this simple calculation */ @Override public double valueAt(double x) { - return intercept; + return intercept + x * slope; } protected double mean(List x) { - double sum = x.stream().reduce((a, b) -> a + b).get(); - return sum / x.size(); + return x.stream().mapToDouble(d -> d).average().getAsDouble(); } /** @@ -69,6 +73,29 @@ public void setIntercept(NumericProperty intercept) { firePropertyChanged(this, intercept); } + /** + * Provides getter accessibility to the slope as a NumericProperty + * + * @return a NumericProperty derived from + * NumericPropertyKeyword.BASELINE_SLOPE with a value equal to slop + */ + public NumericProperty getSlope() { + return derive(BASELINE_SLOPE, slope); + } + + /** + * Checks whether {@code slope} is a baseline slope property and updates the + * respective value of this baseline. + * + * @param slope a {@code NumericProperty} of the {@code BASELINE_SLOPE} type + * @see set + */ + public void setSlope(NumericProperty slope) { + requireType(slope, BASELINE_SLOPE); + this.slope = (double) slope.getValue(); + firePropertyChanged(this, slope); + } + /** * Lists the {@code intercept} as accessible property for this * {@code FlatBaseline}. @@ -91,13 +118,17 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - for (int i = 0, size = output.dimension(); i < size; i++) { + public void optimisationVector(ParameterVector output) { + for (Parameter p : output.getParameters()) { + + if (p != null) { + + var key = p.getIdentifier().getKeyword(); - var key = output.getIndex(i); + if (key == BASELINE_INTERCEPT) { + p.setValue(intercept); + } - if (key == BASELINE_INTERCEPT) { - output.set(i, intercept, key); } } @@ -106,10 +137,12 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) { - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - if (params.getIndex(i) == BASELINE_INTERCEPT) { - setIntercept(derive(BASELINE_INTERCEPT, params.get(i))); + if (p.getIdentifier().getKeyword() == BASELINE_INTERCEPT) { + setIntercept( + derive(BASELINE_INTERCEPT, p.inverseTransform()) + ); } } diff --git a/src/main/java/pulse/baseline/Baseline.java b/src/main/java/pulse/baseline/Baseline.java index 20c2ab9..d42c31d 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -1,15 +1,11 @@ package pulse.baseline; -import static java.lang.Double.NEGATIVE_INFINITY; -import static java.lang.Math.min; - -import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import pulse.AbstractData; +import pulse.DiscreteInput; import pulse.input.ExperimentalData; import pulse.input.IndexRange; +import pulse.input.Range; import pulse.search.Optimisable; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -27,6 +23,8 @@ */ public abstract class Baseline extends PropertyHolder implements Reflexive, Optimisable { + public final static int MIN_BASELINE_POINTS = 15; + public abstract Baseline copy(); /** @@ -48,74 +46,10 @@ public abstract class Baseline extends PropertyHolder implements Reflexive, Opti * values, triggering whatever events are associated with them. *

* - * @param x a list of independent variable values - * @param y a list of dependent variable values - * @param size the size of the region + * @param x + * @param y */ - protected abstract void doFit(List x, List y, int size); - - /** - * Selects part of the {@code data} that can be used for baseline estimation - * (typically, this means selecting 'negative' time values and the - * corresponding signal) data and runs the fitting algorithms, - * - * @param data the experimental data - * @param rangeMin the minimum of the time range - * @param rangeMax the maximum of the time range - */ - public void fitTo(ExperimentalData data, double rangeMin, double rangeMax) { - var indexRange = data.getIndexRange(); - - Objects.requireNonNull(indexRange); - - if (!indexRange.isValid()) { - throw new IllegalArgumentException("Index range not valid: " + indexRange); - } - - List x = new ArrayList<>(); - List y = new ArrayList<>(); - - int size = 0; - - for (int i = IndexRange.closestLeft(rangeMin, data.getTimeSequence()) + 1, max = min(indexRange.getLowerBound(), - IndexRange.closestRight(rangeMax, data.getTimeSequence())); i < max; i++, size++) { - - x.add(data.timeAt(i)); - y.add(data.signalAt(i)); - - } - - if (size > 0) // do fitting only if data is present - { - doFit(x, y, size); - } - - } - - /** - * Fit to an abstract set of data, using only the subset corresponding to the negative time range. - * @param data a dataset - */ - - public void fitNegative(AbstractData data) { - final int MIN_POINTS = 15; - - var time = data.getTimeSequence(); - var signal = data.getSignalData(); - - var subsetTime = new ArrayList(); - var subsetSignal = new ArrayList(); - - int i; - - for(i = 0; time.get(i) < 0; i++) { - subsetTime.add(time.get(i)); - subsetSignal.add(signal.get(i)); - } - - if(i > MIN_POINTS) - doFit(subsetTime, subsetSignal, i); - } + protected abstract void doFit(List x, List y); /** * Calls {@code fitTo} using the default time range for the data: @@ -125,9 +59,20 @@ public void fitNegative(AbstractData data) { * @param data the experimental data stretching to negative time values * @see fitTo(ExperimentalData,double,double) */ - public void fitTo(ExperimentalData data) { - final double ZERO_LEFT = -1E-5; - fitTo(data, NEGATIVE_INFINITY, ZERO_LEFT); + public void fitTo(DiscreteInput data) { + var filtered = Range.NEGATIVE.filter(data); + if(filtered[0].size() > MIN_BASELINE_POINTS) { + doFit(filtered[0], filtered[1]); + } + } + + public void fitTo(List x, List y) { + int index = IndexRange.closestLeft(0, x); + var xx = x.subList(0, index + 1); + var yy = y.subList(0, index + 1); + if(xx.size() > MIN_BASELINE_POINTS) { + doFit(xx, yy); + } } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/FlatBaseline.java b/src/main/java/pulse/baseline/FlatBaseline.java index 56e1d7c..9500c8f 100644 --- a/src/main/java/pulse/baseline/FlatBaseline.java +++ b/src/main/java/pulse/baseline/FlatBaseline.java @@ -1,18 +1,3 @@ -/* - * Copyright 2021 Artem Lunev . - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package pulse.baseline; import static java.lang.String.format; @@ -22,7 +7,6 @@ /** * A flat baseline. - * @author Artem Lunev */ public class FlatBaseline extends AdjustableBaseline { @@ -41,12 +25,12 @@ public FlatBaseline() { * @param intercept the constant baseline value. */ public FlatBaseline(double intercept) { - super(intercept); + super(intercept, 0.0); } @Override - protected void doFit(List x, List y, int size) { + protected void doFit(List x, List y) { double intercept = mean(y); set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); } @@ -61,4 +45,4 @@ public String toString() { return getClass().getSimpleName() + " = " + format("%3.2f", getIntercept().getValue()); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/LinearBaseline.java b/src/main/java/pulse/baseline/LinearBaseline.java index c6381eb..d3d899e 100644 --- a/src/main/java/pulse/baseline/LinearBaseline.java +++ b/src/main/java/pulse/baseline/LinearBaseline.java @@ -2,15 +2,13 @@ import static java.lang.String.format; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.BASELINE_SLOPE; import java.util.List; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; @@ -31,43 +29,26 @@ */ public class LinearBaseline extends AdjustableBaseline { - private double slope; - /** * A primitive constructor, which initialises a {@code CONSTANT} baseline * with zero intercept and slope. */ public LinearBaseline() { - super(0.0); + super(0.0, 0.0); } - - /** - * A constructor, which allows to specify all three parameters in one go. - * - * @param intercept the intercept is the value of the Baseline's linear - * function at {@code x = 0} - * @param slope the slope determines the inclination angle of the Baseline's - * graph. - */ + public LinearBaseline(double intercept, double slope) { - super(intercept); - this.slope = slope; + super(intercept, slope); } - - /** - * Calculates the linear function {@code g(x) = intercept + slope*time} - * - * @param x the argument of the linear function - * @return the result of this simple calculation - */ - @Override - public double valueAt(double x) { - final double intercept = (double) getIntercept().getValue(); - return intercept + x * slope; + + public LinearBaseline(LinearBaseline baseline) { + super( (double) baseline.getIntercept().getValue(), + (double) baseline.getSlope().getValue() + ); } @Override - protected void doFit(List x, List y, int size) { + protected void doFit(List x, List y) { double meanx = mean(x); double meany = mean(y); @@ -76,46 +57,25 @@ protected void doFit(List x, List y, int size) { double xxbar = 0.0; double xybar = 0.0; - for (int i = 0; i < size; i++) { + for (int i = 0, size = x.size(); i < size; i++) { x1 = x.get(i); y1 = y.get(i); xxbar += (x1 - meanx) * (x1 - meanx); xybar += (x1 - meanx) * (y1 - meany); } - - slope = xybar / xxbar; + + double slope = xybar / xxbar; double intercept = meany - slope * meanx; set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); set(BASELINE_SLOPE, derive(BASELINE_SLOPE, slope)); } - /** - * Provides getter accessibility to the slope as a NumericProperty - * - * @return a NumericProperty derived from - * NumericPropertyKeyword.BASELINE_SLOPE with a value equal to slop - */ - public NumericProperty getSlope() { - return derive(BASELINE_SLOPE, slope); - } - - /** - * Checks whether {@code slope} is a baseline slope property and updates the - * respective value of this baseline. - * - * @param slope a {@code NumericProperty} of the {@code BASELINE_SLOPE} type - * @see set - */ - public void setSlope(NumericProperty slope) { - requireType(slope, BASELINE_SLOPE); - this.slope = (double) slope.getValue(); - firePropertyChanged(this, slope); - } - @Override public String toString() { - return getClass().getSimpleName() + " = " + format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); + var slope = getSlope().getValue(); + return getClass().getSimpleName() + " = " + + format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); } @Override @@ -129,15 +89,16 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); if (key == BASELINE_SLOPE) { - output.set(i, slope, BASELINE_SLOPE); + double slope = (double) getSlope().getValue(); + p.setValue(slope); } } @@ -157,10 +118,12 @@ public void optimisationVector(ParameterVector output, List flags) { public void assign(ParameterVector params) { super.assign(params); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - if (params.getIndex(i) == BASELINE_SLOPE) { - setSlope(derive(BASELINE_SLOPE, params.get(i))); + var key = p.getIdentifier().getKeyword(); + + if (key == BASELINE_SLOPE) { + setSlope( derive(BASELINE_SLOPE, p.inverseTransform() )); } } @@ -180,7 +143,7 @@ public Set listedKeywords() { @Override public Baseline copy() { - return new LinearBaseline((double) this.getIntercept().getValue(), this.slope); + return new LinearBaseline(this); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/baseline/SinusoidalBaseline.java b/src/main/java/pulse/baseline/SinusoidalBaseline.java index c55ec0e..9074cdc 100644 --- a/src/main/java/pulse/baseline/SinusoidalBaseline.java +++ b/src/main/java/pulse/baseline/SinusoidalBaseline.java @@ -1,197 +1,400 @@ package pulse.baseline; -import static java.lang.Math.sin; -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.BASELINE_AMPLITUDE; -import static pulse.properties.NumericPropertyKeyword.BASELINE_FREQUENCY; -import static pulse.properties.NumericPropertyKeyword.BASELINE_PHASE_SHIFT; +import pulse.math.FFTTransformer; +import pulse.math.Harmonic; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; - +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.input.IndexRange; +import pulse.input.Range; import pulse.math.ParameterVector; -import pulse.math.Segment; -import pulse.math.transforms.StickTransform; +import pulse.math.ZScore; +import pulse.math.filters.Filter; +import pulse.math.filters.OptimisedRunningAverage; +import pulse.math.filters.Randomiser; +import pulse.math.filters.RunningAverage; import pulse.properties.Flag; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.BASELINE_AMPLITUDE; +import static pulse.properties.NumericPropertyKeyword.BASELINE_FREQUENCY; +import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; +import static pulse.properties.NumericPropertyKeyword.BASELINE_PHASE_SHIFT; +import static pulse.properties.NumericPropertyKeyword.BASELINE_SLOPE; +import pulse.search.SimpleOptimisationTask; +import pulse.search.SimpleResponse; +import pulse.search.direction.ActiveFlags; +import pulse.search.statistics.SumOfSquares; +import pulse.util.Group; +import static pulse.properties.NumericPropertyKeyword.MAX_HIGH_FREQ_WAVES; +import static pulse.properties.NumericPropertyKeyword.MAX_LOW_FREQ_WAVES; /** - * A simple sinusoidal baseline. - *

- * It is given by the expression y = y0 + - * A sin(2πf t + φ) , where f is the - * frequency (in Hz), A is the amplitude, φ is the phase shift. - * Extends the {@code FlatBaseline} class and thus inherits the - * {@code BASELINE_INTERCEPT} property. The sinusoidal baseline is useful to - * mitigate electromagnetic interferences, with the frequencies usually in the - * range of 25 to 60 Hz. - *

+ * A multiple-harmonic baseline. Replaces the Sinusoidal baseline in previous + * version. * */ -public class SinusoidalBaseline extends AdjustableBaseline { +public class SinusoidalBaseline extends LinearBaseline { + + private List hiFreq; + private List loFreq; + private List active; - private double frequency; - private double phaseShift; - private double amplitude; - private final static double _2PI = 2.0 * Math.PI; + private int maxHighFreqHarmonics; + private int maxLowFreqHarmonics; + + private final static double FREQUENCY_THRESHOLD = 400; /** * Creates a sinusoidal baseline with default properties. */ public SinusoidalBaseline() { - super(0.0); - setFrequency(def(BASELINE_FREQUENCY)); - setAmplitude(def(BASELINE_AMPLITUDE)); - setPhaseShift(def(BASELINE_PHASE_SHIFT)); + super(0.0, 0.0); + maxHighFreqHarmonics = (int) def(MAX_HIGH_FREQ_WAVES).getValue(); + maxLowFreqHarmonics = (int) def(MAX_LOW_FREQ_WAVES).getValue(); + hiFreq = new ArrayList<>(); + active = new ArrayList<>(); + loFreq = new ArrayList<>(); } @Override public double valueAt(double x) { - var intercept = (double) getIntercept().getValue(); - return intercept + amplitude * sin(_2PI * x * frequency + phaseShift); + return super.valueAt(x) + + active.stream().mapToDouble(h -> h.valueAt(x)).sum(); } - /** - * Listed properties include the frequency, amplitude, phase shift, and - * intercept. - */ @Override - public Set listedKeywords() { - var set = super.listedKeywords(); - set.add(BASELINE_FREQUENCY); - set.add(BASELINE_AMPLITUDE); - set.add(BASELINE_PHASE_SHIFT); - return set; + public Baseline copy() { + var baseline = new SinusoidalBaseline(); + baseline.setIntercept(this.getIntercept()); + baseline.setSlope(this.getSlope()); + baseline.hiFreq = new ArrayList<>(); + baseline.maxHighFreqHarmonics = this.maxHighFreqHarmonics; + baseline.maxLowFreqHarmonics = this.maxLowFreqHarmonics; + for (Harmonic h : active) { + var newH = new Harmonic(h); + baseline.active.add(newH); + newH.setParent(baseline); + } + for (Harmonic h : hiFreq) { + baseline.hiFreq.add(new Harmonic(h)); + } + for (Harmonic h : loFreq) { + baseline.loFreq.add(new Harmonic(h)); + } + return baseline; } @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + active.forEach(h -> h.optimisationVector(output) ); + } - switch (type) { - case BASELINE_FREQUENCY: - setFrequency(property); - break; - case BASELINE_PHASE_SHIFT: - setPhaseShift(property); - break; - case BASELINE_AMPLITUDE: - setAmplitude(property); - break; - default: - super.set(type, property); + @Override + public void assign(ParameterVector output) { + super.assign(output); + active.forEach(h + -> h.assign(output) + ); + } + + private void guessHarmonics(double[] x, double[] y) { + var fft = new FFTTransformer(y); + fft.transform(); + double[] sampling = fft.sampling(x); + + var amplitude = fft.getAmpltiudeSpectrum(); + var phase = fft.getPhaseSpectrum(); + + var zscore = new ZScore(); + zscore.process(amplitude); + + var signals = zscore.getSignals(); + double maxAmp = 0; + + hiFreq = new ArrayList<>(); + + double span = x[x.length - 1] - x[0]; + double lowerFrequency = 4.0 / span; + + for (int i = 0; i < sampling.length; i++) { + if (signals[i] > 0) { + if (sampling[i] < FREQUENCY_THRESHOLD && sampling[i] > lowerFrequency) { + var h = new Harmonic(amplitude[i], sampling[i], phase[i]); + hiFreq.add(h); + maxAmp = Math.max(maxAmp, amplitude[i]); + } + } } + active.addAll(sort(hiFreq, maxHighFreqHarmonics)); } - public NumericProperty getFrequency() { - return derive(BASELINE_FREQUENCY, frequency); + private List sort(List hs, int limit) { + var tmp = new ArrayList<>(hs); + tmp.sort(null); + Collections.reverse(tmp); + //leave out a maximum of n harmonics + return tmp.subList(0, Math.min(tmp.size(), limit)); } - public NumericProperty getAmplitude() { - return derive(BASELINE_AMPLITUDE, amplitude); + private void labelActive() { + for (int i = 0, size = active.size(); i < size; i++) { + active.get(i).setRank(i); + active.get(i).setParent(this); + } } - public NumericProperty getPhaseShift() { - return derive(BASELINE_PHASE_SHIFT, phaseShift); - } + private void fitHarmonics(DiscreteInput input) { - public void setFrequency(NumericProperty frequency) { - requireType(frequency, BASELINE_FREQUENCY); - this.frequency = (double) frequency.getValue(); - firePropertyChanged(this, frequency); - } + var sos = new SumOfSquares() { - public void setAmplitude(NumericProperty amplitude) { - requireType(amplitude, BASELINE_AMPLITUDE); - this.amplitude = (double) amplitude.getValue(); - firePropertyChanged(this, amplitude); - } + @Override + public void calculateResiduals(DiscreteInput reference, Response estimate) { + int min = 0; + int max = reference.getX().size(); + calculateResiduals(reference, estimate, min, max); + } + + }; + + SimpleResponse response = new SimpleResponse(sos) { + + @Override + public double evaluate(double t) { + return valueAt(t); + } + + }; + + var task = new SimpleOptimisationTask(this, input) { + + @Override + public Response getResponse() { + return response; + } + + }; + + //adjust optimisation flags + var flagList = new ArrayList(); + flagList.add(new Flag(BASELINE_AMPLITUDE, false)); + flagList.add(new Flag(BASELINE_FREQUENCY, true)); + flagList.add(new Flag(BASELINE_PHASE_SHIFT, true)); + flagList.add(new Flag(BASELINE_INTERCEPT, false)); + flagList.add(new Flag(BASELINE_SLOPE, true)); + + var oldState = ActiveFlags.storeState(); + ActiveFlags.loadState(flagList); + + CompletableFuture.runAsync(task).thenRun(() -> { + flagList.stream().filter(f -> f.getType() == BASELINE_AMPLITUDE) + .findFirst().get().setValue(true); + task.run(); + ActiveFlags.loadState(oldState); + } + ); - public void setPhaseShift(NumericProperty phaseShift) { - requireType(phaseShift, BASELINE_PHASE_SHIFT); - this.phaseShift = (double) phaseShift.getValue(); - firePropertyChanged(this, phaseShift); } /** - * The optimisation vector can include the amplitude, frequency and phase - * shift of a sinusoid, and a baseline intercept value of the superclass. + * @return a set containing {@code BASELINE_INTERCEPT} and + * {@code BASELINE_SLOPE} keywords */ @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); - - switch (key) { - case BASELINE_FREQUENCY: - output.set(i, frequency, BASELINE_FREQUENCY); - break; - case BASELINE_PHASE_SHIFT: - output.set(i, phaseShift, BASELINE_PHASE_SHIFT); - break; - case BASELINE_AMPLITUDE: - output.set(i, amplitude, BASELINE_AMPLITUDE); - break; - default: - continue; + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(MAX_HIGH_FREQ_WAVES); + set.add(MAX_LOW_FREQ_WAVES); + return set; + } + + @Override + public List subgroups() { + return getHarmonics() == null ? new ArrayList<>() + : getHarmonics().stream().map(h -> (Group) h).collect(Collectors.toList()); + } + + public List getHarmonics() { + return active; + } + + public NumericProperty getHiFreqMax() { + return derive(MAX_HIGH_FREQ_WAVES, maxHighFreqHarmonics); + } + + public void setHiFreqMax(NumericProperty maxHarmonics) { + NumericProperty.requireType(maxHarmonics, MAX_HIGH_FREQ_WAVES); + int oldValue = this.maxHighFreqHarmonics; + + if ((int) maxHarmonics.getValue() != oldValue) { + + var lowFreq = new ArrayList(); + int size = active.size(); + + if(maxHighFreqHarmonics < size) { + lowFreq = new ArrayList<>(active.subList(maxHighFreqHarmonics, size)); } - output.setTransform(i, new StickTransform(output.getParameterBounds(i))); - + this.maxHighFreqHarmonics = (int) maxHarmonics.getValue(); + active.clear(); + active.addAll(sort(hiFreq, maxHighFreqHarmonics)); + active.addAll(lowFreq); + this.labelActive(); + this.firePropertyChanged(this, maxHarmonics); + } + + } + + public NumericProperty getLowFreqMax() { + return derive(MAX_LOW_FREQ_WAVES, maxLowFreqHarmonics); + } + public void setLowFreqMax(NumericProperty maxHarmonics) { + NumericProperty.requireType(maxHarmonics, MAX_LOW_FREQ_WAVES); + int oldValue = this.maxLowFreqHarmonics; + if ((int) maxHarmonics.getValue() != oldValue) { + this.maxLowFreqHarmonics = (int) maxHarmonics.getValue(); + active = active.subList(0, maxHighFreqHarmonics); + active.addAll(this.sort(loFreq, maxLowFreqHarmonics)); + this.labelActive(); + this.firePropertyChanged(this, maxHarmonics); + } } @Override - public void assign(ParameterVector params) { - super.assign(params); - - for (int i = 0, size = params.dimension(); i < size; i++) { - - switch (params.getIndex(i)) { - case BASELINE_FREQUENCY: - setFrequency(derive(BASELINE_FREQUENCY, params.inverseTransform(i))); - break; - case BASELINE_PHASE_SHIFT: - setPhaseShift(derive(BASELINE_PHASE_SHIFT, params.inverseTransform(i))); - break; - case BASELINE_AMPLITUDE: - setAmplitude(derive(BASELINE_AMPLITUDE, params.inverseTransform(i))); - break; - default: - break; - } + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + switch (type) { + + case MAX_HIGH_FREQ_WAVES: + setHiFreqMax(property); + break; + case MAX_LOW_FREQ_WAVES: + setLowFreqMax(property); + break; + default: } } @Override - public Baseline copy() { - var baseline = new SinusoidalBaseline(); - baseline.setIntercept(this.getIntercept()); - baseline.amplitude = this.amplitude; - baseline.frequency = this.frequency; - baseline.phaseShift = this.phaseShift; - return baseline; + public void fitTo(DiscreteInput input) { + //fit the linear part first + super.fitTo(input); + //then fit the harmonics -- full range is needed here + + DiscreteInputImpl filtered = (DiscreteInputImpl) filter(input); + + var x = filtered.getXasArray(); + var y = filtered.getYasArray(); + + active.clear(); + guessHarmonics(x, y); + labelActive(); + fitHarmonics(new DiscreteInputImpl(x, y)); + addLowFreq(input); + labelActive(); } - @Override - protected void doFit(List x, List y, int size) { - var flatBaseline = new FlatBaseline(); - flatBaseline.doFit(x, y, size); - //TODO Fourier transform + private DiscreteInput filter(DiscreteInput full) { + var x = full.getX().stream().mapToDouble(d -> d).toArray(); + var y = full.getY().stream().mapToDouble(d -> d).toArray(); + + Filter f = new OptimisedRunningAverage(); + Filter fr = new Randomiser(1.0); + var runningAverage = fr.process(f.process(full)); + + var xAv = runningAverage.stream().mapToDouble(p -> p.getX()).toArray(); + var yAv = runningAverage.stream().mapToDouble(p -> p.getY()).toArray(); + + var spline = new SplineInterpolator(); + var interp = spline.interpolate(xAv, yAv); + + for (int i = 0; i < x.length; i++) { + y[i] -= interp.value(x[i]); + //System.err.println(x[i] + " " + interp.value(x[i]) + " " + y[i]); + } + + return new DiscreteInputImpl(x, y); + + } + + private void addLowFreq(DiscreteInput input) { + double amp = !hiFreq.isEmpty() + ? (double) hiFreq.get(0).getAmplitude().getValue() + : Collections.max(input.getY()) / 2.0; + + double span = input.getX().get(input.getX().size() - 1) - input.getX().get(0); + double freq = RunningAverage.DEFAULT_BINS / span; + + loFreq.clear(); + + /* + These harmonics are inaccessible by FFT + */ + + for (double f = freq; f > 1.0 / (2.0 * span); f /= 2.0) { + loFreq.add(new Harmonic(amp, f, 0.0)); + } + + active.addAll(loFreq.subList(0, Math.min(loFreq.size(), maxLowFreqHarmonics))); } @Override public String toString() { return getClass().getSimpleName(); - } + } + + private class DiscreteInputImpl implements DiscreteInput { + + private final double[] x; + private final double[] y; + + public DiscreteInputImpl(double[] x, double[] y) { + this.x = x; + this.y = y; + } + + @Override + public List getX() { + return convert(x); + } + + @Override + public List getY() { + return convert(y); + } + + public double[] getXasArray() { + return x; + } + + public double[] getYasArray() { + return y; + } + + private List convert(double[] a) { + return DoubleStream.of(a).boxed().collect(Collectors.toList()); + } + + @Override + public IndexRange getIndexRange() { + return new IndexRange(getX(), Range.UNLIMITED); + } + } } diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 15776db..f38a8e0 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -1,25 +1,20 @@ package pulse.input; -import static java.lang.Double.valueOf; -import static java.util.Collections.max; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; -import java.awt.geom.Point2D; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; -import java.util.stream.Collectors; import pulse.AbstractData; -import pulse.baseline.FlatBaseline; +import pulse.DiscreteInput; import pulse.input.listeners.DataEvent; import pulse.input.listeners.DataEventType; import pulse.input.listeners.DataListener; -import pulse.ui.Messages; +import pulse.math.filters.HalfTimeCalculator; import pulse.util.PropertyHolderListener; /** @@ -30,13 +25,13 @@ * {@code CurveReader}s. Any manipulation (e.g. truncation) of the data triggers * an event associated with this {@code ExperimentalData}. */ -public class ExperimentalData extends AbstractData { +public class ExperimentalData extends AbstractData implements DiscreteInput { + private HalfTimeCalculator calculator; private Metadata metadata; private IndexRange indexRange; private Range range; private List dataListeners; - private double halfTime; /** * This is the cutoff factor which is used as a criterion for data @@ -45,22 +40,6 @@ public class ExperimentalData extends AbstractData { */ public final static double CUTOFF_FACTOR = 7.2; - /** - * The binning factor used to build a crude approximation of the heating - * curve. Described in Lunev, A., & Heymer, R. (2020). Review of - * Scientific Instruments, 91(6), 064902. - */ - public final static int REDUCTION_FACTOR = 32; - - public final static int MAX_REDUCTION_FACTOR = 256; - - /** - * A fail-safe factor. - */ - public final static double FAIL_SAFE_FACTOR = 10.0; - - private static Comparator pointComparator = (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); - /** * Constructs an {@code ExperimentalData} object using the superclass * constructor and creating a new list of data listeners. The number of @@ -73,9 +52,12 @@ public ExperimentalData() { dataListeners = new ArrayList<>(); setPrefix("RawData"); setNumPoints(derive(NUMPOINTS, 0)); - indexRange = new IndexRange(); - this.addDataListener((DataEvent e) -> calculateHalfTime() ); - + indexRange = new IndexRange(0,0); + this.addDataListener((DataEvent e) -> { + if (e.getType() == DataEventType.DATA_LOADED) { + preprocess(); + } + }); } public final void addDataListener(DataListener listener) { @@ -127,127 +109,6 @@ public void addPoint(double time, double signal) { incrementCount(); } - /** - * Constructs a deliberately crude representation of this heating curve by - * calculating a running average. - *

- * This is done using a binning algorithm, which will group the - * time-temperature data associated with this {@code ExperimentalData} in - * {@code count/reductionFactor - 1} bins, calculate the average value for - * time and temperature within each bin, and collect those values in a - * {@code List}. This is useful to cancel out the effect of signal - * outliers, e.g. when calculating the half-rise time. - *

- * - * The algorithm is described in more detail in Lunev, A., & Heymer, R. - * (2020). Review of Scientific Instruments, 91(6), 064902. - * - * @param reductionFactor the factor, by which the number of points - * {@code count} will be reduced for this {@code ExperimentalData}. - * @return a {@code List}, representing the degraded - * {@code ExperimentalData}. - * @see halfRiseTime() - * @see pulse.AbstractData.maxTemperature() - */ - public List runningAverage(int reductionFactor) { - - int count = (int) getNumPoints().getValue(); - - List crudeAverage = new ArrayList<>(count / reductionFactor); - - int start = indexRange.getLowerBound(); - int end = indexRange.getUpperBound(); - - int step = (end - start) / (count / reductionFactor); - double av = 0; - - int i1, i2; - - for (int i = 0, max = (count / reductionFactor) - 1; i < max; i++) { - i1 = start + step * i; - i2 = i1 + step; - - av = 0; - - for (int j = i1; j < i2; j++) { - av += signalAt(j); - } - - av /= step; - - crudeAverage.add(new Point2D.Double(timeAt((i1 + i2) / 2), av)); - - } - - return crudeAverage; - - } - - /** - * Instead of returning the simple maximum (which can be an outlier!) of the - * temperature, this overriden method calculates the maximum of the - * {@code runningAverage} using the default reduction factor - * {@value REDUCTION_FACTOR}. - * - * @return a {@code Point2D} object containing the coordinates of the - * adjusted maximum. - * @see - * pulse.problem.statements.Problem.estimateSignalRange(ExperimentalData) - */ - public Point2D maxAdjustedSignal() { - var degraded = runningAverage(REDUCTION_FACTOR); - return max(degraded, pointComparator); - } - - /** - * Calculates the approximate half-rise time used for crude estimation of - * thermal diffusivity. - *

- * This uses the {@code runningAverage} method by applying the default - * reduction factor of {@value REDUCTION_FACTOR}. The calculation is based - * on finding the approximate value corresponding to the half-maximum of the - * temperature. The latter is calculated using the running average curve. - * The index corresponding to the closest temperature value available for - * that curve is used to retrieve the half-rise time (which also has the - * same index). If this fails, i.e. the associated index is less than 1, - * this will print out a warning message and still assign a value to the - * half-time variable equal to the acquisition time divided by a fail-safe factor - * {@value FAIL_SAFE_FACTOR}. - *

- * @see getHalfTime() - */ - public void calculateHalfTime() { - var baseline = new FlatBaseline(); - baseline.fitTo(this); - - int curRedFactor = REDUCTION_FACTOR/2; // reduced twofold since first operation - // in the while loop will increase it likewise - int cutoffIndex = 0; - List degraded = null; //running average - Point2D max = null; - - do { - curRedFactor *= 2; - degraded = runningAverage(curRedFactor); - max = (max(degraded, pointComparator)); - cutoffIndex = degraded.indexOf(max); - } while(cutoffIndex < 1 && curRedFactor < MAX_REDUCTION_FACTOR); - - double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; - degraded = degraded.subList(0, cutoffIndex); - - int index = IndexRange.closestLeft(halfMax, - degraded.stream().map(point -> point.getY()).collect(Collectors.toList())); - - if (index < 1) { - System.err.println(Messages.getString("ExperimentalData.HalfRiseError")); - halfTime = max(getTimeSequence()) / FAIL_SAFE_FACTOR; - } - else - halfTime = degraded.get(index).getX(); - - } - /** * Retrieves the {@code Metadata} object for this {@code ExperimentalData}. * @@ -284,7 +145,7 @@ public boolean equals(Object o) { * threshold, {@code false} otherwise. */ public boolean isAcquisitionTimeSensible() { - final double cutoff = CUTOFF_FACTOR * halfTime; + final double cutoff = CUTOFF_FACTOR * calculator.getHalfTime(); final int count = (int) getNumPoints().getValue(); double d = getTimeSequence().get(count - 1); return getTimeSequence().get(count - 1) < cutoff; @@ -306,7 +167,7 @@ public boolean isAcquisitionTimeSensible() { * @see fireDataChanged */ public void truncate() { - final double cutoff = CUTOFF_FACTOR * halfTime; + final double cutoff = CUTOFF_FACTOR * calculator.getHalfTime(); this.range.setUpperBound(derive(UPPER_BOUND, cutoff)); } @@ -335,16 +196,6 @@ private void doSetMetadata() { } } - - /** - * Retrieves the half-time value of this dataset, which is equal to the - * time needed to reach half of the signal maximum. - * @return the half-time value. - */ - - public double getHalfTime() { - return halfTime; - } /** * Gets the time sequence element corresponding to the lower bound of the @@ -382,6 +233,7 @@ public Range getRange() { * * @return the index range */ + @Override public IndexRange getIndexRange() { return indexRange; } @@ -422,6 +274,28 @@ private void doSetRange() { @Override public double timeLimit() { return timeAt(indexRange.getUpperBound()); - } + } + + public HalfTimeCalculator getHalfTimeCalculator() { + return calculator; + } + + public void preprocess() { + if (calculator == null) { + calculator = new HalfTimeCalculator(this); + } + + calculator.calculate(); + } + + @Override + public List getX() { + return this.getTimeSequence(); + } + + @Override + public List getY() { + return this.getSignalData(); + } } \ No newline at end of file diff --git a/src/main/java/pulse/input/IndexRange.java b/src/main/java/pulse/input/IndexRange.java index bdf4288..93a9280 100644 --- a/src/main/java/pulse/input/IndexRange.java +++ b/src/main/java/pulse/input/IndexRange.java @@ -20,13 +20,14 @@ public class IndexRange { private int iStart; private int iEnd; - /** - * Construct an empty index range where the start index is set to -1 and the - * end index is set to 0. - */ - public IndexRange() { - iStart = -1; - iEnd = 0; + public IndexRange(IndexRange other) { + iStart = other.iStart; + iEnd = other.iEnd; + } + + public IndexRange(int start, int end) { + this.iStart = start; + this.iEnd = end; } /** @@ -73,7 +74,7 @@ protected void reset(List data) { * @see closestLeft * @see closestRight */ - public void setLowerBound(List data, double a) { + public final void setLowerBound(List data, double a) { iStart = a > 0 ? closestLeft(a, data) : closestRight(0, data); } @@ -90,7 +91,7 @@ public void setLowerBound(List data, double a) { * @see closestLeft * @see closestRight */ - public void setUpperBound(List data, double b) { + public final void setUpperBound(List data, double b) { iEnd = closestRight(b, data); } @@ -104,9 +105,9 @@ public void setUpperBound(List data, double b) { * @see setLowerBound * @see setUpperBound */ - public void set(List data, Range range) { + public final void set(List data, Range range) { var segment = range.getSegment(); - setLowerBound(data, Math.max(0.0, segment.getMinimum())); + setLowerBound(data, segment.getMinimum()); setUpperBound(data, segment.getMaximum()); } @@ -116,7 +117,7 @@ public void set(List data, Range range) { * * @return the start index */ - public int getLowerBound() { + public final int getLowerBound() { return iStart; } @@ -126,7 +127,7 @@ public int getLowerBound() { * * @return the end index */ - public int getUpperBound() { + public final int getUpperBound() { return iEnd; } diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index ab95fa0..fd6e4e9 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -1,6 +1,7 @@ package pulse.input; import static java.lang.Math.max; +import java.util.ArrayList; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; @@ -10,6 +11,8 @@ import java.util.List; import java.util.Set; +import pulse.DiscreteInput; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -30,7 +33,11 @@ public class Range extends PropertyHolder implements Optimisable { private Segment segment; - + + public final static Range UNLIMITED = new Range (Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + public final static Range NEGATIVE = new Range(Double.NEGATIVE_INFINITY, -1E-16); + public final static Range POSITIVE = new Range(1e-16, Double.POSITIVE_INFINITY); + /** * Constructs a {@code Range} from the minimum and maximum values of * {@code data}. @@ -53,6 +60,45 @@ public Range(List data) { public Range(double a, double b) { this.segment = new Segment(a, b); } + + /** + * Contains a data double array ([0] - x, [1] - y), + * where the data points have been filtered so that + * each x fits into this range. + * @param input + * @return a [2][...] array containing filtered x and y values + */ + + public List[] filter(DiscreteInput input) { + var x = input.getX(); + var y = input.getY(); + + if(x.size() != y.size()) { + throw new IllegalArgumentException("x.length != y.length"); + } + + var xf = new ArrayList(); + var yf = new ArrayList(); + + double min = segment.getMinimum(); + double max = segment.getMaximum(); + + final double eps = 1E-10; + + for(int i = 0, size = x.size(); i < size; i++) { + + if(x.get(i) > min && x.get(i) < max + eps) { + + xf.add(x.get(i)); + yf.add(y.get(i)); + + } + + } + + return new List[]{xf, yf}; + + } /** * Resets the minimum and maximum values of this range to those specified by @@ -181,7 +227,7 @@ public Segment boundLimits(boolean isUpperBound) { var curve = (ExperimentalData) this.getParent(); var seq = curve.getTimeSequence(); - double tHalf = curve.getHalfTime(); + double tHalf = curve.getHalfTimeCalculator().getHalfTime(); Segment result = null; if(isUpperBound) @@ -201,25 +247,25 @@ public Segment boundLimits(boolean isUpperBound) { * absolute constraints equal to a fourth of their values. * * @param output the vector to be updated - * @param flags a list of active flags */ @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { Segment bounds; - for (int i = 0, size = output.dimension(); i < size; i++) { - - var key = output.getIndex(i); + for (Parameter p : output.getParameters()) { + var key = p.getIdentifier().getKeyword(); + double value; + switch (key) { case UPPER_BOUND: - output.set(i, segment.getMaximum()); bounds = boundLimits(true); + value = segment.getMaximum(); break; case LOWER_BOUND: - output.set(i, segment.getMinimum()); bounds = boundLimits(false); + value = segment.getMinimum(); break; default: continue; @@ -227,8 +273,9 @@ public void optimisationVector(ParameterVector output, List flags) { var transform = new StickTransform(bounds); - output.setParameterBounds(i, bounds); - output.setTransform(i, transform); + p.setBounds(bounds); + p.setTransform(transform); + p.setValue(value); } @@ -242,18 +289,17 @@ public void optimisationVector(ParameterVector output, List flags) { */ @Override public void assign(ParameterVector params) throws SolverException { - NumericProperty p = null; - - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - p = derive( params.getIndex(i), params.inverseTransform(i) ); + var key = p.getIdentifier().getKeyword(); + var np = derive( key, p.inverseTransform() ); - switch (params.getIndex(i)) { + switch (key) { case UPPER_BOUND: - setUpperBound(p); + setUpperBound(np); break; case LOWER_BOUND: - setLowerBound(p); + setLowerBound(np); break; default: } @@ -267,4 +313,4 @@ public String toString() { return "Range given by: " + segment.toString(); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/io/export/ResidualStatisticExporter.java b/src/main/java/pulse/io/export/ResidualStatisticExporter.java index 1de1e4b..4430ec5 100644 --- a/src/main/java/pulse/io/export/ResidualStatisticExporter.java +++ b/src/main/java/pulse/io/export/ResidualStatisticExporter.java @@ -59,6 +59,7 @@ public Extension[] getSupportedExtensions() { private void printHTML(ResidualStatistic hc, FileOutputStream fos) { try (var stream = new PrintStream(fos)) { + var time = hc.getTimeSequence(); var residuals = hc.getResiduals(); int residualsLength = residuals == null ? 0 : residuals.size(); stream.print(getString("ResultTableExporter.style")); @@ -71,8 +72,8 @@ private void printHTML(ResidualStatistic hc, FileOutputStream fos) { stream.print(""); for (int i = 0; i < residualsLength; i++) { - double tr = residuals.get(i)[0]; - double Tr = residuals.get(i)[1]; + double tr = time.get(i); + double Tr = residuals.get(i); stream.printf("%n%.8f%.8f", tr, Tr); } @@ -83,6 +84,7 @@ private void printHTML(ResidualStatistic hc, FileOutputStream fos) { private void printCSV(ResidualStatistic hc, FileOutputStream fos) { try (var stream = new PrintStream(fos)) { + var time = hc.getTimeSequence(); var residuals = hc.getResiduals(); int residualsLength = residuals == null ? 0 : residuals.size(); final String TIME_LABEL = getString("HeatingCurve.6"); @@ -90,9 +92,9 @@ private void printCSV(ResidualStatistic hc, FileOutputStream fos) { stream.print(TIME_LABEL + "\t" + RESIDUAL_LABEL + "\t"); double tr, Tr; for (int i = 0; i < residualsLength; i++) { - tr = residuals.get(i)[0]; + tr = time.get(i); stream.printf("%n%3.8f", tr); - Tr = residuals.get(i)[1]; + Tr = residuals.get(i); stream.printf("\t%3.8f", Tr); } } diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index 4445add..85c4f1c 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -47,23 +47,25 @@ public class NetzschCSVReader implements CurveReader { private final static String THICKNESS = "Thickness_RT"; private final static String DETECTOR_SPOT_SIZE = "Spotsize"; private final static String DIAMETER = "Diameter"; + private final static String L_PULSE_WIDTH = "Laser_pulse_width"; + private final static String PULSE_WIDTH = "Pulse_width"; /** * Note comma is included as a delimiter character here. */ private final static String ENGLISH_DELIMS = "[#(),;/°Cx%^]+"; private final static String GERMAN_DELIMS = "[#();/°Cx%^]+"; - + private static String delims; //default number format (British format) private static Locale locale; - + private static NumberFormat format; - + private NetzschCSVReader() { //do nothing } - + protected void setDefaultLocale() { delims = ENGLISH_DELIMS; locale = Locale.ENGLISH; @@ -105,26 +107,25 @@ public String getSupportedExtension() { public List read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); ExperimentalData curve = new ExperimentalData(); - + setDefaultLocale(); //always start with a default locale - - //gets the number format for this locale + //gets the number format for this locale try (BufferedReader reader = new BufferedReader(new FileReader(file))) { int shotId = determineShotID(reader, file); - - String spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, false); - + + String spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, false); + double spotSize = 0; - if(spot != null) { + if (spot != null) { var spotTokens = spot.split(delims); spotSize = format.parse(spotTokens[spotTokens.length - 1]).doubleValue() * TO_METRES; } - + String tempLine = findLineByLabel(reader, THICKNESS, false); var tempTokens = tempLine.split(delims); - + final double thickness = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; tempTokens = findLineByLabel(reader, DIAMETER, false).split(delims); @@ -133,6 +134,19 @@ public List read(File file) throws IOException { tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, false).split(delims); final double sampleTemperature = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() + TO_KELVIN; + var line = findLineByLabel(reader, L_PULSE_WIDTH, DETECTOR, false); + if (line == null) { + line = findLineByLabel(reader, PULSE_WIDTH, DETECTOR, false); + } + + double pulseWidth = 0; + + if (line != null) { + tempTokens = line.split(delims); + pulseWidth = format.parse(tempTokens[tempTokens.length - 1]) + .doubleValue() * TO_SECONDS; + } + /* * Finds the detector keyword. */ @@ -147,6 +161,9 @@ public List read(File file) throws IOException { populate(curve, reader); var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); + if (pulseWidth > 1e-10) { + met.set(NumericPropertyKeyword.PULSE_WIDTH, derive(NumericPropertyKeyword.PULSE_WIDTH, pulseWidth)); + } met.set(NumericPropertyKeyword.THICKNESS, derive(NumericPropertyKeyword.THICKNESS, thickness)); met.set(NumericPropertyKeyword.DIAMETER, derive(NumericPropertyKeyword.DIAMETER, diameter)); met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, spotSize != 0 ? spotSize : 0.85 * diameter)); @@ -154,7 +171,7 @@ public List read(File file) throws IOException { curve.setMetadata(met); curve.setRange(new Range(curve.getTimeSequence())); - return new ArrayList<>(Arrays.asList(curve)); + return new ArrayList<>(Arrays.asList(curve)); } catch (ParseException ex) { Logger.getLogger(NetzschCSVReader.class.getName()).log(Level.SEVERE, null, ex); @@ -163,26 +180,24 @@ public List read(File file) throws IOException { return null; } - + /** * Note: the {@code line} must contain a decimal-separated number. - * @param line a line containing number with a decimal separator + * + * @param line a line containing number with a decimal separator */ - private static void guessLocaleAndFormat(String line) { - - if(line.contains(".")) { + + if (line.contains(".")) { delims = ENGLISH_DELIMS; locale = Locale.ENGLISH; - } - - else { + } else { delims = GERMAN_DELIMS; locale = Locale.GERMAN; } - + format = DecimalFormat.getInstance(locale); - format.setGroupingUsed(false); + format.setGroupingUsed(false); } protected static void populate(AbstractData data, BufferedReader reader) throws IOException, ParseException { @@ -192,12 +207,12 @@ protected static void populate(AbstractData data, BufferedReader reader) throws for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { tokens = line.split(delims); - - if(tokens.length < 2) { + + if (tokens.length < 2) { guessLocaleAndFormat(line); tokens = line.split(delims); } - + time = format.parse(tokens[0]).doubleValue() * NetzschCSVReader.TO_SECONDS; power = format.parse(tokens[1]).doubleValue(); data.addPoint(time, power); @@ -210,7 +225,7 @@ protected static int determineShotID(BufferedReader reader, File file) throws IO String[] shotID = shotIDLine.split(delims); int id; - + //check if first entry makes sense if (!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) { throw new IllegalArgumentException(file.getName() @@ -226,37 +241,39 @@ protected static int determineShotID(BufferedReader reader, File file) throws IO protected static String findLineByLabel(BufferedReader reader, String label, boolean ignoreLocale) throws IOException { return findLineByLabel(reader, label, "!!!", ignoreLocale); } - + protected static String findLineByLabel(BufferedReader reader, String label, String stopLabel, boolean ignoreLocale) throws IOException { String line = ""; String[] tokens; reader.mark(1000); - + //find keyword outer: for (line = reader.readLine(); line != null; line = reader.readLine()) { - if(line.isBlank()) + if (line.isBlank()) { continue; - - if(!ignoreLocale) + } + + if (!ignoreLocale) { guessLocaleAndFormat(line); + } tokens = line.split(delims); for (String token : tokens) { - + if (token.equalsIgnoreCase(label)) { break outer; } - - if(token.equalsIgnoreCase(stopLabel)) { + + if (token.equalsIgnoreCase(stopLabel)) { line = null; reader.reset(); break outer; } - + } } @@ -264,7 +281,7 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str return line; } - + /** * As this class uses the singleton pattern, only one instance is created * using an empty no-argument constructor. @@ -274,14 +291,14 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str public static CurveReader getInstance() { return instance; } - + /** * Get the standard delimiter chars. + * * @return delims */ - public static String getDelims() { return delims; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/FFTTransformer.java b/src/main/java/pulse/math/FFTTransformer.java new file mode 100644 index 0000000..7e5390d --- /dev/null +++ b/src/main/java/pulse/math/FFTTransformer.java @@ -0,0 +1,136 @@ +package pulse.math; + +import org.apache.commons.math3.complex.Complex; +import org.apache.commons.math3.transform.DftNormalization; +import org.apache.commons.math3.transform.FastFourierTransformer; +import org.apache.commons.math3.transform.TransformType; + +public class FFTTransformer { + + private double[] amplitudeSpec; + private double[] phaseSpec; + + private int n; //number of input points + private Complex[] buffer; + + private Window window; + + public FFTTransformer(double[] realInput) { + this(Window.HANN, realInput, new double[realInput.length]); + } + + public FFTTransformer(Window window, double[] realInput) { + this(window, realInput, new double[realInput.length]); + } + + public FFTTransformer(Window window, double[] realInput, double[] imagInput) { + this.window = window; + n = realInput.length; + + if (realInput.length != imagInput.length) { + throw new IllegalArgumentException( + String.format("Invalid data array lengths: %5d and %5d", + realInput.length, imagInput.length)); + } + + //if the input array is a power of two, simply make a shallow copy of the input array + if (IsPowerOfTwo(realInput.length)) { + buffer = new Complex[realInput.length]; + fill(realInput, imagInput, realInput.length); + } else { + int pow2 = numBits(realInput.length); + int nextPowerOfTwo = (int) Math.pow(2, pow2 + 1); + int previousPowerOfTwo = (int) Math.pow(2, pow2); + + final double TOLERANCE_FACTOR = 0.25; + + /* + * if we cut the tails, do we end up removing less elements than the number + * of zeros we had to add to reach next power of two? + */ + if ((nextPowerOfTwo - realInput.length + > realInput.length - previousPowerOfTwo) + && //in this case, do we have to add too many zeros? + (nextPowerOfTwo - realInput.length + > TOLERANCE_FACTOR * realInput.length)) { + cutTails(realInput, imagInput, previousPowerOfTwo); + } else { + zeroPad(realInput, imagInput, nextPowerOfTwo); + } + + } + //create power and phase arrays + amplitudeSpec = new double[buffer.length / 2]; + phaseSpec = new double[buffer.length / 2]; + + } + + public double[] sampling(double[] x) { + final double totalTime = x[n - 2] - x[0]; + double[] sample = new double[buffer.length / 2]; + double fs = n/totalTime; //sampling rate + for (int i = 0; i < sample.length; i++) { + sample[i] = i * fs / buffer.length; + } + return sample; + } + + public void transform() { + FastFourierTransformer fft = new FastFourierTransformer(DftNormalization.STANDARD); + + Complex[] result = fft.transform(buffer, TransformType.FORWARD); + + final double _2_N = 2.0 / amplitudeSpec.length; + + amplitudeSpec[0] = result[0].abs() / amplitudeSpec.length; + phaseSpec[0] = result[0].getArgument(); + + for (int i = 1; i < amplitudeSpec.length; i++) { + amplitudeSpec[i] = _2_N * result[i].abs(); + phaseSpec[i] = result[i].getArgument(); + } + + } + + private void fill(double[] realInput, double[] imagInput, int size) { + for (int i = 0; i < size; i++) { + buffer[i] = new Complex( + window.evaluate(i, realInput.length) * realInput[i], + imagInput[i]); + } + } + + private void cutTails(double[] realInput, double[] imagInput, int previousPowerOfTwo) { + buffer = new Complex[previousPowerOfTwo]; + fill(realInput, imagInput, previousPowerOfTwo); + } + + private void zeroPad(double[] realInput, double[] imagInput, int nextPowerOfTwo) { + buffer = new Complex[nextPowerOfTwo]; + fill(realInput, imagInput, realInput.length); + for (int i = realInput.length; i < nextPowerOfTwo; i++) { + buffer[i] = new Complex(0.0, 0.0); + } + } + + /** + * Checks if the argument (positive integer) is a power of 2. Returns trues + * if the argument is zero. + */ + private static boolean IsPowerOfTwo(int x) { + return x > 0 && ((x & (x - 1)) == 0); + } + + private int numBits(int value) { + return (int) (Math.log(value) / Math.log(2)); + } + + public double[] getAmpltiudeSpectrum() { + return amplitudeSpec; + } + + public double[] getPhaseSpectrum() { + return phaseSpec; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/Harmonic.java b/src/main/java/pulse/math/Harmonic.java new file mode 100644 index 0000000..06e3604 --- /dev/null +++ b/src/main/java/pulse/math/Harmonic.java @@ -0,0 +1,266 @@ +package pulse.math; + +import static java.lang.Math.cos; +import java.util.Set; +import pulse.math.transforms.PeriodicTransform; +import pulse.math.transforms.StandardTransformations; +import pulse.math.transforms.StickTransform; +import pulse.math.transforms.Transformable; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import static pulse.properties.NumericProperty.requireType; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.BASELINE_AMPLITUDE; +import static pulse.properties.NumericPropertyKeyword.BASELINE_FREQUENCY; +import static pulse.properties.NumericPropertyKeyword.BASELINE_PHASE_SHIFT; +import pulse.search.Optimisable; +import pulse.util.PropertyHolder; + +/** + * + * Harmonic class. + *

+ * It is given by the expression y = y0 + + * A cos(2πf t + φ) , where f is the + * frequency (in Hz), A is the amplitude, φ is the phase shift. + *

+ * + * + */ +public class Harmonic extends PropertyHolder implements Optimisable, Comparable { + + private int rank = -1; + + private double amplitude; + private double frequency; + private double phaseShift; + + private final static double _2PI = 2.0 * Math.PI; + + public Harmonic() { + setFrequency(def(BASELINE_FREQUENCY)); + setAmplitude(def(BASELINE_AMPLITUDE)); + setPhaseShift(def(BASELINE_PHASE_SHIFT)); + } + + public Harmonic(double amplitude, double frequency, double phaseShift) { + this.amplitude = amplitude; + this.frequency = frequency; + this.phaseShift = phaseShift; + } + + public Harmonic(Harmonic h) { + this.amplitude = h.amplitude; + this.frequency = h.frequency; + this.phaseShift = h.phaseShift; + this.rank = h.rank; + } + + public NumericProperty getFrequency() { + return derive(BASELINE_FREQUENCY, frequency); + } + + public NumericProperty getAmplitude() { + return derive(BASELINE_AMPLITUDE, amplitude); + } + + public NumericProperty getPhaseShift() { + return derive(BASELINE_PHASE_SHIFT, phaseShift); + } + + public final void setFrequency(NumericProperty frequency) { + requireType(frequency, BASELINE_FREQUENCY); + this.frequency = (double) frequency.getValue(); + firePropertyChanged(this, frequency); + } + + public final void setAmplitude(NumericProperty amplitude) { + requireType(amplitude, BASELINE_AMPLITUDE); + this.amplitude = (double) amplitude.getValue(); + firePropertyChanged(this, amplitude); + } + + public final void setPhaseShift(NumericProperty phaseShift) { + requireType(phaseShift, BASELINE_PHASE_SHIFT); + this.phaseShift = (double) phaseShift.getValue(); + firePropertyChanged(this, phaseShift); + } + + /** + * Amplitude form of the Fourier harmonic + * + * @param x + * @return + */ + public double valueAt(double x) { + return amplitude * cos(_2PI * x * frequency + phaseShift); + } + + /** + * Listed properties include the frequency, amplitude, phase shift, and + * intercept. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(BASELINE_FREQUENCY); + set.add(BASELINE_AMPLITUDE); + set.add(BASELINE_PHASE_SHIFT); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + + switch (type) { + case BASELINE_FREQUENCY: + setFrequency(property); + break; + case BASELINE_PHASE_SHIFT: + setPhaseShift(property); + break; + case BASELINE_AMPLITUDE: + setAmplitude(property); + break; + } + + } + + /** + * The optimisation vector can include the amplitude, frequency and phase + * shift of a sinusoid, and a baseline intercept value of the superclass. + */ + @Override + public void optimisationVector(ParameterVector output) { + + var params = output.getParameters(); + + for (int i = 0, size = params.size(); i < size; i++) { + + var p = params.get(i); + var id = p.getIdentifier(); + var bounds = Segment.boundsFrom(id.getKeyword()); + + double value; + + Transformable transform = null; + + switch (id.getKeyword()) { + case BASELINE_FREQUENCY: + value = frequency; + transform = StandardTransformations.ABS; + break; + case BASELINE_PHASE_SHIFT: + value = phaseShift; + transform = new PeriodicTransform(bounds); + break; + case BASELINE_AMPLITUDE: + value = amplitude; + transform = new StickTransform(bounds); + break; + default: + continue; + } + + var newId = new ParameterIdentifier(id.getKeyword(), rank); + + if (id.getIndex() == rank) { + p.setBounds(bounds); + p.setTransform(transform); + p.setValue(value); + } else if (rank > -1) { + + boolean matchFound = output.getParameters().stream().anyMatch(pp -> { + var key = pp.getIdentifier().getKeyword(); + int index = pp.getIdentifier().getIndex(); + return key == id.getKeyword() && rank == index; + }); + + if (!matchFound) { + + var newParam = new Parameter(newId, transform, bounds); + newParam.setValue(value); + params.add(newParam); + + } + + } + + } + + } + + @Override + public void assign(ParameterVector params) { + + for (Parameter p : params.getParameters()) { + + var id = p.getIdentifier(); + + if (id.getIndex() == rank) { + + switch (id.getKeyword()) { + case BASELINE_FREQUENCY: + setFrequency(derive(BASELINE_FREQUENCY, p.inverseTransform())); + break; + case BASELINE_PHASE_SHIFT: + setPhaseShift(derive(BASELINE_PHASE_SHIFT, p.inverseTransform())); + break; + case BASELINE_AMPLITUDE: + setAmplitude(derive(BASELINE_AMPLITUDE, p.inverseTransform())); + break; + default: + break; + } + + } + + } + + } + + public void setRank(int rank) { + this.rank = rank; + } + + public int getRank() { + return rank; + } + + public Harmonic increaseAmplitudeBy(int amplitudeFactor) { + var h = new Harmonic(this); + h.amplitude *= amplitudeFactor; + return h; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Harmonic)) { + return false; + } + + Harmonic oH = (Harmonic) o; + + final double tolerance = 1E-3; + + return Math.abs(oH.amplitude - this.amplitude) + / Math.abs(oH.amplitude + this.amplitude) < tolerance + && Math.abs(oH.frequency - this.frequency) + / Math.abs(oH.frequency + this.frequency) < tolerance + && Math.abs(oH.phaseShift - this.phaseShift) + / Math.abs(oH.phaseShift + this.phaseShift) < tolerance; + } + + @Override + public int compareTo(Harmonic o) { + return this.getAmplitude().compareTo(o.getAmplitude()); + } + + @Override + public String toString() { + return String.format("[%1d]: f = %3.2f, A = %3.2f, phi = %3.2f", + rank, frequency, amplitude, phaseShift); + } + +} diff --git a/src/main/java/pulse/math/Parameter.java b/src/main/java/pulse/math/Parameter.java new file mode 100644 index 0000000..5556cd1 --- /dev/null +++ b/src/main/java/pulse/math/Parameter.java @@ -0,0 +1,91 @@ +package pulse.math; + +import pulse.math.transforms.Transformable; + +/** + * Parameter class + */ +public class Parameter { + + private ParameterIdentifier index; + private Transformable transform; + private Segment bound; + private double value; + + public Parameter(ParameterIdentifier index, Transformable transform, Segment bound) { + this.index = index; + this.transform = transform; + this.bound = bound; + } + + public Parameter(ParameterIdentifier index) { + if(index.getKeyword() != null) { + bound = Segment.boundsFrom(index.getKeyword()); + } + this.index = index; + } + + public Parameter(Parameter p) { + this.index = p.index; + this.transform = p.transform; + this.bound = p.bound; + this.value = p.value; + } + + public ParameterIdentifier getIdentifier() { + return index; + } + + public void setBounds(Segment bounds) { + this.bound = bounds; + } + + public Segment getBounds() { + return bound; + } + + /** + * If transform of {@code i} is not null, applies the transformation to the + * component bounds + * + * @param i the index of the component + * @return the transformed bounds + */ + public Segment getTransformedBounds() { + return transform != null + ? new Segment(transform.transform(bound.getMinimum()), + transform.transform(bound.getMaximum())) + : bound; + } + + public Transformable getTransform() { + return transform; + } + + public void setTransform(Transformable transform) { + this.transform = transform; + } + + public double inverseTransform() { + return transform != null ? transform.inverse(value) : value; + } + + public Parameter copy() { + return new Parameter(index, transform, bound); + } + + public double getApparentValue() { + return value; + } + + public void setValue(double value, boolean ignoreTransform) { + this.value = transform == null || ignoreTransform + ? value + : transform.transform(value); + } + + public void setValue(double value) { + setValue(value, false); + } + +} diff --git a/src/main/java/pulse/math/ParameterIdentifier.java b/src/main/java/pulse/math/ParameterIdentifier.java new file mode 100644 index 0000000..b96847d --- /dev/null +++ b/src/main/java/pulse/math/ParameterIdentifier.java @@ -0,0 +1,53 @@ +package pulse.math; + +import pulse.properties.NumericPropertyKeyword; + +public class ParameterIdentifier { + + private NumericPropertyKeyword keyword; + private int index; + + public ParameterIdentifier(NumericPropertyKeyword keyword, int index) { + this.keyword = keyword; + this.index = index; + } + + public ParameterIdentifier(NumericPropertyKeyword keyword) { + this(keyword, 0); + } + + public ParameterIdentifier(int index) { + this.index = index; + } + + public NumericPropertyKeyword getKeyword() { + return keyword; + } + + public int getIndex() { + return index; + } + + @Override + public boolean equals(Object id) { + if(!id.getClass().equals(ParameterIdentifier.class)) { + return false; + } + + var pid = (ParameterIdentifier) id; + + boolean result = true; + + if(keyword != pid.keyword || index != pid.index) + result = false; + + return result; + + } + + @Override + public String toString() { + return keyword + " # " + index; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index 4b85221..6bd1737 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -1,51 +1,50 @@ package pulse.math; import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import pulse.math.linear.Vector; -import pulse.math.transforms.Transformable; import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; /** - * A wrapper subclass that assigns {@code NumericPropertyKeyword}s to specific + * A wrapper subclass that assigns {@code ParameterIdentifier}s to specific * components of the vector. Used when constructing the optimisation vector. */ -public class ParameterVector extends Vector { +public class ParameterVector { - private NumericPropertyKeyword[] indices; - private Transformable[] transforms; - private Segment[] bounds; + private List params; /** * Constructs an {@code IndexedVector} with the specified list of keywords. * * @param indices a list of keywords */ - public ParameterVector(List indices) { - this(indices.size()); - assign(indices); + public ParameterVector(List indices) { + params = indices.stream().map(ind + -> new Parameter(ind)).collect(Collectors.toList()); } /** * Constructs an {@code IndexedVector} based on {@code v} and a list of * keyword {@code indices} * + * @param proto prototype vector * @param v the vector to be copied - * @param prototype the prototype of the parameter vector */ public ParameterVector(ParameterVector proto, Vector v) { - super(v); - this.indices = new NumericPropertyKeyword[proto.indices.length]; - System.arraycopy(proto.indices, 0, this.indices, 0, proto.indices.length); - this.bounds = new Segment[proto.bounds.length]; - System.arraycopy(proto.bounds, 0, this.bounds, 0, proto.bounds.length); - this.transforms = new Transformable[proto.transforms.length]; - System.arraycopy(proto.transforms, 0, this.transforms, 0, proto.transforms.length); - + params = new ArrayList<>(); + var protoParams = proto.params; + for (Parameter p : protoParams) { + var pp = new Parameter(p); //copy + pp.setValue(v.get( + protoParams.indexOf(p))); //set new value + params.add(pp); //add + } } /** @@ -54,198 +53,79 @@ public ParameterVector(ParameterVector proto, Vector v) { * @param v another vector */ public ParameterVector(ParameterVector v) { - this(v.dimension()); - final int n = dimension(); - for (int i = 0; i < n; i++) { - this.set(i, v.get(i)); + params = new ArrayList<>(v.params); + for (Parameter p : params) { + p.setValue(p.getApparentValue(), true); } - System.arraycopy(v.indices, 0, indices, 0, n); - System.arraycopy(v.transforms, 0, transforms, 0, n); - System.arraycopy(v.bounds, 0, bounds, 0, n); - } - - /** - * Creates an empty ParameterVector with a dimension of {@code n} - * - * @param n dimension - */ - private ParameterVector(final int n) { - super(n); - indices = new NumericPropertyKeyword[n]; - transforms = new Transformable[n]; - bounds = new Segment[n]; - } - - @Override - public void set(final int i, final double x) { - set(i, x, false); - } - - /** - * Sets the i-th parameter value to {@code x} without applying the - * transform. Sets the bound for this value as the default bound for {@code key}. - * @param i the index of the parameter - * @param x value to be set - * @param key type of property - */ - - public void set(final int i, final double x, NumericPropertyKeyword key) { - set(i, x); - setParameterBounds(i, Segment.boundsFrom(key)); - } - - /** - * Sets the i-component of this vector to {@code x} or - * its corresponding transform, if the latter is defined and - * {@code ignoreTransform} is {@code false}. - * - * @param i index of the value and its transform - * @param x the non-transformed value, which needs to be assigned to the - * i-th component - * @param ignoreTransform if {@code} false, will ignore exiting transform. - */ - public void set(final int i, final double x, boolean ignoreTransform) { - final double t = ignoreTransform || transforms[i] == null ? x : transforms[i].transform(x); - super.set(i, t); - } - - /** - * Retrieves the keyword associated with the {@code dataIndex} - * - * @param dataIndex an index pointing to a component of this vector - * @return a keyword describing this component - */ - public NumericPropertyKeyword getIndex(final int dataIndex) { - return indices[dataIndex]; - } - - /** - * Gets the data index that corresponds to the keyword {@code index} - * - * @param index a keyword-index of the component - * @return a numeric index associated with the original {@code Vector} - */ - private int indexOf(NumericPropertyKeyword index) { - return getIndices().indexOf(index); - } - - /** - * Gets the component at this {@code index} - * - * @param index a keyword-index of a component - * @return the respective component - */ - public double getParameterValue(NumericPropertyKeyword index) { - return super.get(indexOf(index)); } - /** - * Performs an inverse transform corresponding to the index {@code i} of - * this vector. - * - * @param i the index of the transform - * @return the inverse transform of {@code get(i) } if the transform is - * defined, {@code get(i)} otherwise. - */ - public double inverseTransform(final int i) { - return transforms[i] != null ? transforms[i].inverse(get(i)) : get(i); + public void add(Parameter p) { + params.add(p); } - /** - * Gets the transformable of the i-th component - * - * @param i index of the component - * @return the corresponding {@code Transforamble} - */ - public Transformable getTransform(final int i) { - return transforms[i]; - } - - public void setTransform(final int i, Transformable transformable) { - transforms[i] = transformable; - } - - public Segment getParameterBounds(final int i) { - return bounds[i]; - } - - /** - * If transform of {@code i} is not null, applies the transformation to the - * component bounds - * - * @param i the index of the component - * @return the transformed bounds - */ - public Segment getTransformedBounds(final int i) { - return transforms[i] != null - ? new Segment(transforms[i].transform(bounds[i].getMinimum()), - transforms[i].transform(bounds[i].getMaximum())) - : getParameterBounds(i); - } - - /** - * Sets the bounds of i-th component of this vector. - * - * @param i the index of the component - * @param segment new parameter bounds - */ - public void setParameterBounds(int i, Segment segment) { - bounds[i] = segment; - } - - /** - * Gets the full list of indices recognised by this {@code IndexedVector}. - * - * @return the full list of {@code NumericPropertyKeyword} indices. - */ - public List getIndices() { - return Arrays.asList(indices); - } - - /** - * This will assign a new list of indices to this vector - * - * @param indices a list of indices - */ - private void assign(List indices) { - this.indices = indices.toArray(new NumericPropertyKeyword[indices.size()]); - bounds = new Segment[this.indices.length]; - transforms = new Transformable[this.indices.length]; + public double getParameterValue(NumericPropertyKeyword key, int index) { + return params.stream().filter(p -> { + var pid = p.getIdentifier(); + return pid.getKeyword() == key && pid.getIndex() == index; + } + ).findAny().get().getApparentValue(); } @Override public String toString() { var sb = new StringBuilder(); sb.append("Indices: "); - for (var key : indices) { - sb.append(key).append(" ; "); + for (var key : params) { + sb.append(key.getIdentifier()).append(" ; "); } sb.append(System.lineSeparator()); sb.append(" Values: ").append(super.toString()); return sb.toString(); } - + /** * Finds any elements of this vector which do not pass sanity checks. + * * @return a list of malformed numeric properties * @see pulse.properties.NumericProperties.isValueSensible() */ - public List findMalformedElements() { var list = new ArrayList(); - - for (int i = 0; i < dimension(); i++) { - var property = NumericProperties.derive(getIndex(i), inverseTransform(i)); - if (!property.validate()) { - list.add(property); - } + + params.stream().filter(p -> (p.getIdentifier().getKeyword() != null)) + .map(p -> NumericProperties.derive(p.getIdentifier().getKeyword(), + p.inverseTransform())) + .filter(property -> (!property.validate())) + .forEachOrdered(property -> { + list.add(property); + }); + + return list; + } + + public void setValues(Vector v) { + int dim = v.dimension(); + if (dim != this.dimension()) { + throw new IllegalArgumentException("Illegal vector dimension: " + + dim + " != " + this.dimension()); } - return list; + for(int i = 0; i < dim; i++) { + params.get(i).setValue(v.get(i)); + } + + } + + public int dimension() { + return params.size(); + } + + public List getParameters() { + return params; } - public Segment[] getBounds() { - return bounds; + public Vector toVector() { + return new Vector(params.stream().mapToDouble(p -> p.inverseTransform()).toArray()); } } diff --git a/src/main/java/pulse/math/Segment.java b/src/main/java/pulse/math/Segment.java index 9691ab3..c1e0c76 100644 --- a/src/main/java/pulse/math/Segment.java +++ b/src/main/java/pulse/math/Segment.java @@ -13,6 +13,8 @@ public class Segment { private double a; private double b; + + public final static Segment UNBOUNDED = new Segment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); /** * Creates a {@code Segment} bounded by {@code a} and {@code b}. diff --git a/src/main/java/pulse/math/Window.java b/src/main/java/pulse/math/Window.java new file mode 100644 index 0000000..48fbd4f --- /dev/null +++ b/src/main/java/pulse/math/Window.java @@ -0,0 +1,60 @@ +package pulse.math; + +public interface Window { + + public final static Window NONE = (n, N) -> 1.0; + public final static Window HANN = (n, N) -> Math.pow( Math.sin(Math.PI * n / ((double) N)), 2); + public final static Window HAMMING = (n, N) -> 0.54 + 0.46*Math.cos(2.0 * Math.PI * n / ((double) N)); + public final static Window BLACKMANN_HARRIS = (n, N) -> { + final double x = 2.0*Math.PI*n/ ((double)N); + return 0.35875 - 0.48829*Math.cos(x) + 0.14128*Math.cos(2.0*x) - 0.01168*Math.cos(3.0*x); + }; + public final static Window FLAT_TOP = (n, N) -> { + final double x = 2.0*Math.PI*n/ ((double)N); + return 0.21557895 - 0.41663158*Math.cos(x) + 0.277263158*Math.cos(2.0*x) + - 0.083578947*Math.cos(3.0*x) + 0.006947368 * Math.cos(4.0 * x); + }; + public final static Window TUKEY = new Window() { + + private final static double alpha = 0.6; + + @Override + public double evaluate(int n, int N) { + + double result = 0; + + if(n < 0.5*alpha*N) { + result = 0.5 * ( 1 - Math.cos(2.0*Math.PI*n/(alpha*N))); + } + + else if(n <= N/2) { + result = 1.0; + } + + else { + result = TUKEY.evaluate(N - n,N); + } + + return result; + + } + }; + + public final static Window HANN_POISSON = (n, N) -> { + + final double alpha = 2.0; + return HANN.evaluate(n, N) * Math.exp( - alpha * (N - 2 * n) / N); + + }; + + public default double[] apply(double[] input) { + double[] output = new double[input.length]; + for(int i = 0; i < output.length; i++) { + output[i] = input[i] * evaluate(i, input.length); + } + return output; + } + + public abstract double evaluate(int n, int N); + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/ZScore.java b/src/main/java/pulse/math/ZScore.java new file mode 100644 index 0000000..fa13fad --- /dev/null +++ b/src/main/java/pulse/math/ZScore.java @@ -0,0 +1,134 @@ +package pulse.math; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; + +/** + * This class finds peaks in data using the Z-score algorithm: + * https://en.wikipedia.org/wiki/Standard_score This splits the data into a + * number of population defined by the 'lag' number. A standard score is + * calculated as the difference of the current value and population mean divided + * by the population standard deviation. + */ + +public class ZScore { + + private double[] avgFilter; + private double[] stdFilter; + private int[] signals; + + private int lag; + private double threshold; + private double influence; + + public ZScore(int lag, double threshold, double influence) { + this.lag = lag; + this.threshold = threshold; + this.influence = influence; + } + + public ZScore() { + this(40, 3.5, 0.3); + } + + public void process(double[] input) { + + signals = new int[input.length]; + List filteredY = DoubleStream.of(input).boxed().collect(Collectors.toList()); + + var initialWindow = filteredY.subList(input.length - lag, input.length - 1); + + avgFilter = new double[input.length]; + stdFilter = new double[input.length]; + + avgFilter[input.length - lag + 1] = mean(initialWindow); + stdFilter[input.length - lag + 1] = stdev(initialWindow); + + for (int i = input.length - lag; i > 0; i--) { + + if (Math.abs(input[i] - avgFilter[i + 1]) > threshold * stdFilter[i + 1]) { + + signals[i] = (input[i] > avgFilter[i + 1]) ? 1 : -1; + filteredY.set(i, influence * input[i] + + (1 - influence) * filteredY.get(i + 1)); + + } else { + + signals[i] = 0; + filteredY.set(i, input[i]); + + } + + // Update rolling average and deviation + var slidingWindow = filteredY.subList(i, i + lag - 1); + + avgFilter[i] = mean(slidingWindow); + stdFilter[i] = stdev(slidingWindow); + } + + } + + private static double mean(List list) { + return list.stream().mapToDouble(d -> d).average().getAsDouble(); + } + + private static double stdev(List values) { + double ret = 0; + int size = values.size(); + if (size > 0) { + double avg = mean(values); + double sum = values.stream().mapToDouble(d -> Math.pow(d - avg, 2)).sum(); + ret = Math.sqrt(sum / (size - 1)); + } + return ret; + } + + public int[] getSignals() { + return signals; + } + + public double[] getFilteredAverage() { + return avgFilter; + } + + public double[] getFilteredStdev() { + return stdFilter; + } + + /* + public static void main(String[] args) { + Scanner sc = null; + try { + sc = new Scanner(new File("fft.txt")); + } catch (FileNotFoundException ex) { + Logger.getLogger(ZScore.class.getName()).log(Level.SEVERE, null, ex); + } + + // we just need to use \\Z as delimiter + sc.useDelimiter("\\n"); + + var list = new ArrayList(); + + while(sc.hasNext()) { + list.add(sc.nextDouble()); + } + + var zscore = new ZScore(); + zscore.process(list.stream().mapToDouble(d -> d).toArray()); + var signals = zscore.getSignals(); + + for(int i = 0; i < signals.length; i++) { + System.out.println(list.get(i) + " " + signals[i]); + } + + } + */ + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/AssignmentListener.java b/src/main/java/pulse/math/filters/AssignmentListener.java new file mode 100644 index 0000000..251884a --- /dev/null +++ b/src/main/java/pulse/math/filters/AssignmentListener.java @@ -0,0 +1,7 @@ +package pulse.math.filters; + +public interface AssignmentListener { + + public void onValueAssigned(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/Filter.java b/src/main/java/pulse/math/filters/Filter.java new file mode 100644 index 0000000..067ab3e --- /dev/null +++ b/src/main/java/pulse/math/filters/Filter.java @@ -0,0 +1,14 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.List; +import pulse.DiscreteInput; + +public interface Filter { + + public List process(List input); + public default List process(DiscreteInput input) { + return process(DiscreteInput.convert(input.getX(), input.getY())); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/HalfTimeCalculator.java b/src/main/java/pulse/math/filters/HalfTimeCalculator.java new file mode 100644 index 0000000..e2ca9ce --- /dev/null +++ b/src/main/java/pulse/math/filters/HalfTimeCalculator.java @@ -0,0 +1,95 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import static java.lang.Double.valueOf; +import static java.util.Collections.max; +import java.util.Comparator; +import java.util.stream.Collectors; +import pulse.DiscreteInput; +import pulse.baseline.FlatBaseline; +import pulse.input.IndexRange; + +public class HalfTimeCalculator { + + private final Filter filter; + private final DiscreteInput data; + private Point2D max; + private double halfTime; + + /** + * A fail-safe factor. + */ + public final static double FAIL_SAFE_FACTOR = 10.0; + + private static final Comparator pointComparator = + (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); + + public HalfTimeCalculator(DiscreteInput input) { + this.data = input; + this.filter = new RunningAverage(); + } + + /** + * Calculates the approximate half-rise time used for crude estimation of + * thermal diffusivity. + *

+ * This uses the {@code runningAverage} method by applying the default + * reduction factor of {@value REDUCTION_FACTOR}. The calculation is based + * on finding the approximate value corresponding to the half-maximum of the + * temperature. The latter is calculated using the running average curve. + * The index corresponding to the closest temperature value available for + * that curve is used to retrieve the half-rise time (which also has the + * same index). If this fails, i.e. the associated index is less than 1, + * this will print out a warning message and still assign a value to the + * half-time variable equal to the acquisition time divided by a fail-safe factor + * {@value FAIL_SAFE_FACTOR}. + *

+ * @see getHalfTime() + */ + public void calculate() { + var baseline = new FlatBaseline(); + baseline.fitTo(data); + + var filtered = filter.process(data); + + max = max(filtered, pointComparator); + double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; + + int indexLeft = IndexRange.closestLeft(halfMax, + filtered.stream().map(point -> point.getY()) + .collect(Collectors.toList())); + + if (indexLeft < 1 || indexLeft > filtered.size() - 2) { + halfTime = filtered.get(filtered.size() - 1).getX() / FAIL_SAFE_FACTOR; + } + else { + //extrapolate + Point2D p1 = filtered.get(indexLeft); + Point2D p2 = filtered.get(indexLeft + 1); + + halfTime = (halfMax - p1.getY())/(p2.getY() - p1.getY()) + *(p2.getX() - p1.getX()) + p1.getX(); + } + + } + + + /** + * Retrieves the half-time value of this dataset, which is equal to the + * time needed to reach half of the signal maximum. + * @return the half-time value. + */ + + public final double getHalfTime() { + return halfTime; + } + + public final Point2D getFilteredMaximum() { + return max; + } + + public DiscreteInput getData() { + return data; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/OptimisablePolyline.java b/src/main/java/pulse/math/filters/OptimisablePolyline.java new file mode 100644 index 0000000..4d7a8d6 --- /dev/null +++ b/src/main/java/pulse/math/filters/OptimisablePolyline.java @@ -0,0 +1,62 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import pulse.DiscreteInput; +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.Optimisable; +import pulse.util.PropertyHolder; + +public class OptimisablePolyline extends PropertyHolder implements Optimisable { + + private final double[] x; + private final double[] y; + private final List listeners; + + public OptimisablePolyline(List data) { + x = data.stream().mapToDouble(d -> d.getX()).toArray(); + y = data.stream().mapToDouble(d -> d.getY()).toArray(); + listeners = new ArrayList<>(); + } + + @Override + public void assign(ParameterVector input) throws SolverException { + var ps = input.getParameters(); + for(int i = 0, size = ps.size(); i < size; i++) { + y[i] = ps.get(i).getApparentValue(); + } + listeners.stream().forEach(l -> l.onValueAssigned()); + } + + @Override + public void optimisationVector(ParameterVector output) { + output.setValues(new Vector(y)); + } + + public List points() { + return DiscreteInput.convert(x, y); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + public double[] getX() { + return x; + } + + public double[] getY() { + return y; + } + + public void addAssignmentListener(AssignmentListener listener) { + listeners.add(listener); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/OptimisedRunningAverage.java b/src/main/java/pulse/math/filters/OptimisedRunningAverage.java new file mode 100644 index 0000000..46c004b --- /dev/null +++ b/src/main/java/pulse/math/filters/OptimisedRunningAverage.java @@ -0,0 +1,26 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.List; +import pulse.DiscreteInput; + +public class OptimisedRunningAverage extends RunningAverage { + + public OptimisedRunningAverage() { + super(); + } + + public OptimisedRunningAverage(int reductionFactor) { + super(reductionFactor); + } + + @Override + public List process(DiscreteInput input) { + var p = super.process(input); + var optimisableCurve = new OptimisablePolyline(p); + var task = new PolylineOptimiser(input, optimisableCurve); + task.run(); + return optimisableCurve.points(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/PolylineOptimiser.java b/src/main/java/pulse/math/filters/PolylineOptimiser.java new file mode 100644 index 0000000..0624698 --- /dev/null +++ b/src/main/java/pulse/math/filters/PolylineOptimiser.java @@ -0,0 +1,90 @@ +package pulse.math.filters; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.interpolation.UnivariateInterpolator; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.math.ParameterIdentifier; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.linear.Vector; +import pulse.search.SimpleOptimisationTask; +import pulse.search.SimpleResponse; +import pulse.search.direction.BFGSOptimiser; +import pulse.search.statistics.AbsoluteDeviations; +import pulse.search.statistics.OptimiserStatistic; + +public class PolylineOptimiser extends SimpleOptimisationTask { + + private final OptimiserStatistic sos; + private final PolylineResponse response; + private final OptimisablePolyline optimisableCurve; + + public PolylineOptimiser(DiscreteInput di, OptimisablePolyline optimisableCurve) { + super(optimisableCurve, di); + this.sos = new AbsoluteDeviations() { + + @Override + public void calculateResiduals(DiscreteInput reference, Response estimate) { + int min = 0; + int max = reference.getX().size(); + calculateResiduals(reference, estimate, min, max); + } + + }; + this.optimisableCurve = optimisableCurve; + response = new PolylineResponse(sos); + optimisableCurve.addAssignmentListener(() -> response.update(optimisableCurve)); + } + + @Override + public void setDefaultOptimiser() { + setOptimiser(BFGSOptimiser.getInstance()); + } + + @Override + public Response getResponse() { + return response; + } + + @Override + public ParameterVector searchVector() { + var y = optimisableCurve.getY(); + List ids + = IntStream.range(0, optimisableCurve.getX().length).sequential() + .mapToObj(i -> new ParameterIdentifier(i)) + .collect(Collectors.toList()); + var pv = new ParameterVector(ids); + pv.setValues(new Vector(y)); + var pvParams = pv.getParameters(); + for (int i = 0; i < pv.dimension(); i++) { + pvParams.get(i).setBounds(new Segment(y[i] - 2, y[i] + 2)); + } + return pv; + } + + public class PolylineResponse extends SimpleResponse { + + UnivariateInterpolator interp; + UnivariateFunction func; + + public PolylineResponse(OptimiserStatistic os) { + super(os); + } + + public void update(OptimisablePolyline impl) { + interp = new SplineInterpolator(); + func = interp.interpolate(impl.getX(), impl.getY()); + } + + @Override + public double evaluate(double t) { + return func.value(t); + } + } + +} diff --git a/src/main/java/pulse/math/filters/Randomiser.java b/src/main/java/pulse/math/filters/Randomiser.java new file mode 100644 index 0000000..3d15453 --- /dev/null +++ b/src/main/java/pulse/math/filters/Randomiser.java @@ -0,0 +1,26 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.List; + +public class Randomiser implements Filter { + + private final double amplitude; + + public Randomiser(double amplitude) { + this.amplitude = amplitude; + } + + @Override + public List process(List input) { + input.forEach(p -> + ((Point2D.Double)p).y += (Math.random() - 0.5) * amplitude + ); + return input; + } + + public double getAmplitude() { + return amplitude; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/RunningAverage.java b/src/main/java/pulse/math/filters/RunningAverage.java new file mode 100644 index 0000000..37a08a2 --- /dev/null +++ b/src/main/java/pulse/math/filters/RunningAverage.java @@ -0,0 +1,131 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; +import pulse.DiscreteInput; + +public class RunningAverage implements Filter { + + private int bins; + + /** + * The binning factor used to build a crude approximation of the heating + * curve. Described in Lunev, A., & Heymer, R. (2020). Review of + * Scientific Instruments, 91(6), 064902. + */ + + public static final int DEFAULT_BINS = 16; + public final static int MIN_BINS = 4; + + /** + * @param reductionFactor the factor, by which the number of points + * {@code count} will be reduced for this {@code ExperimentalData}. + */ + + public RunningAverage(int reductionFactor) { + this.bins = reductionFactor; + } + + public RunningAverage() { + this.bins = DEFAULT_BINS; + } + + /** + * Constructs a deliberately crude representation of this heating curve by + * calculating a running average. + *

+ * This is done using a binning algorithm, which will group the + * time-temperature data associated with this {@code ExperimentalData} in + * {@code count/reductionFactor - 1} bins, calculate the average value for + * time and temperature within each bin, and collect those values in a + * {@code List}. This is useful to cancel out the effect of signal + * outliers, e.g. when calculating the half-rise time. + *

+ * + * The algorithm is described in more detail in Lunev, A., & Heymer, R. + * (2020). Review of Scientific Instruments, 91(6), 064902. + * + * @param points + * @param input + * @return a {@code List}, representing the degraded + * {@code ExperimentalData}. + * @see halfRiseTime() + * @see pulse.AbstractData.maxTemperature() + */ + + @Override + public List process(List points) { + var x = points.stream().mapToDouble(p -> p.getX()).toArray(); + var y = points.stream().mapToDouble(p -> p.getY()).toArray(); + + int size = x.length; + int step = size / bins; + List movingAverage = new ArrayList<>(bins); + + for (int i = 0; i < bins; i++) { + int i1 = step*i; + int i2 = step*(i+1); + + double av = 0; + int j; + + for (j = i1; j < i2 && j < size; j++) { + av += y[j]; + } + + av /= j - i1; + i2 = j - 1; + + movingAverage.add(new Point2D.Double( + (x[i1] + x[i2])/ 2.0, av)); + + } + + addBoundaryPoints(movingAverage, x[0], x[size - 1]); + + /* + for(int i = 0; i < movingAverage.size(); i++) { + System.err.println(movingAverage.get(i)); + } + */ + + return movingAverage; + + } + + private static void addBoundaryPoints(List d, double minTime, double maxTime) { + int max = d.size(); + + d.add( + extrapolate(d.get(max - 1), + d.get(max - 2), + maxTime) + ); + + d.add( 0, + extrapolate(d.get(0), + d.get(1), + minTime) + ); + + } + + private static Point2D extrapolate(Point2D a, Point2D b, double x) { + double y1 = a.getY(); + double y2 = b.getY(); + double x1 = a.getX(); + double x2 = b.getX(); + + return new Point2D.Double(x, y1 + (x - x1)/(x2 - x1)*(y2 - y1)); + } + + public final int getNumberOfBins() { + return bins; + } + + public final void setNumberOfBins(int no) { + this.bins = no > MIN_BINS - 1 ? no : MIN_BINS; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/math/linear/Vector.java b/src/main/java/pulse/math/linear/Vector.java index 3b49382..39d0653 100644 --- a/src/main/java/pulse/math/linear/Vector.java +++ b/src/main/java/pulse/math/linear/Vector.java @@ -2,6 +2,7 @@ import static java.lang.Math.abs; import static java.lang.Math.sqrt; +import java.util.List; import static pulse.math.linear.ArithmeticOperations.DIFFERENCE; import static pulse.math.linear.ArithmeticOperations.DIFF_SQUARED; import static pulse.math.linear.ArithmeticOperations.PRODUCT; @@ -63,7 +64,7 @@ public Vector inverted() { * * @return the integer dimension */ - public int dimension() { + public final int dimension() { return x.length; } @@ -121,7 +122,7 @@ public static Vector random(int n, double min, double max) { } return v; } - + /** * Component-wise vector multiplication */ diff --git a/src/main/java/pulse/math/transforms/InvLenSqTransform.java b/src/main/java/pulse/math/transforms/InvLenSqTransform.java deleted file mode 100644 index 8b51287..0000000 --- a/src/main/java/pulse/math/transforms/InvLenSqTransform.java +++ /dev/null @@ -1,27 +0,0 @@ -package pulse.math.transforms; - -import pulse.problem.statements.model.ThermalProperties; - -/** - * A transform that simply divides the value by the squared length of the - * sample. - */ -public class InvLenSqTransform implements Transformable { - - private double l; - - public InvLenSqTransform(ThermalProperties tp) { - this.l = (double) tp.getSampleThickness().getValue(); - } - - @Override - public double transform(double value) { - return Math.abs(value) / (l * l); - } - - @Override - public double inverse(double t) { - return Math.abs(t) * (l * l); - } - -} diff --git a/src/main/java/pulse/math/transforms/InvLenTransform.java b/src/main/java/pulse/math/transforms/InvLenTransform.java deleted file mode 100644 index 571bdd8..0000000 --- a/src/main/java/pulse/math/transforms/InvLenTransform.java +++ /dev/null @@ -1,26 +0,0 @@ -package pulse.math.transforms; - -import pulse.problem.statements.model.ThermalProperties; - -/** - * A transform that simply divides the value by the length of the sample. - */ -public class InvLenTransform implements Transformable { - - private double l; - - public InvLenTransform(ThermalProperties tp) { - l = (double) tp.getSampleThickness().getValue(); - } - - @Override - public double transform(double value) { - return value / l; - } - - @Override - public double inverse(double t) { - return t * l; - } - -} diff --git a/src/main/java/pulse/math/transforms/PeriodicTransform.java b/src/main/java/pulse/math/transforms/PeriodicTransform.java new file mode 100644 index 0000000..31cee06 --- /dev/null +++ b/src/main/java/pulse/math/transforms/PeriodicTransform.java @@ -0,0 +1,38 @@ +package pulse.math.transforms; + +import pulse.math.Segment; + +public class PeriodicTransform extends BoundedParameterTransform { + + /** + * Only the upper bound of the argument is used. + * + * @param bounds the {@code bounda.getMaximum()} is used in the transforms + */ + public PeriodicTransform(Segment bounds) { + super(bounds); + } + + /** + * @param a + * @see pulse.math.MathUtils.atanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double transform(double a) { + double max = getBounds().getMaximum(); + double min = getBounds().getMinimum(); + double len = max - min; + + return a > max ? transform(a - len) : (a < min ? transform(a + len) : a); + } + + /** + * @see pulse.math.MathUtils.tanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double inverse(double t) { + return t; + } +} \ No newline at end of file diff --git a/src/main/java/pulse/math/transforms/StandardTransformations.java b/src/main/java/pulse/math/transforms/StandardTransformations.java index a8206fd..c5b9503 100644 --- a/src/main/java/pulse/math/transforms/StandardTransformations.java +++ b/src/main/java/pulse/math/transforms/StandardTransformations.java @@ -19,6 +19,9 @@ public class StandardTransformations { @Override public double transform(double a) { + if(a < 0) { + System.err.println(a); + } return log(a); } diff --git a/src/main/java/pulse/math/transforms/StickTransform.java b/src/main/java/pulse/math/transforms/StickTransform.java index 00239cb..f548761 100644 --- a/src/main/java/pulse/math/transforms/StickTransform.java +++ b/src/main/java/pulse/math/transforms/StickTransform.java @@ -15,8 +15,6 @@ */ package pulse.math.transforms; -import static java.lang.Math.tanh; -import static pulse.math.MathUtils.atanh; import pulse.math.Segment; /** diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index b27e4eb..e1a283f 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -7,8 +7,6 @@ import pulse.problem.schemes.Grid; import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import pulse.tasks.SearchTask; /** @@ -36,7 +34,7 @@ public class DiscretePulse { * tc * is the time factor defined in the {@code Problem} class. */ - private final static int WIDTH_TOLERANCE_FACTOR = 1000; + private final static int WIDTH_TOLERANCE_FACTOR = 10000; /** * This creates a one-dimensional discrete pulse on a {@code grid}. @@ -58,7 +56,8 @@ public DiscretePulse(Problem problem, Grid grid) { = Objects.requireNonNull(problem.specificAncestor(SearchTask.class), "Problem has not been assigned to a SearchTask"); - ExperimentalData data = ((SearchTask) ancestor).getExperimentalCurve(); + ExperimentalData data = + (ExperimentalData) ( ((SearchTask) ancestor).getInput() ); init(data); pulse.addListener(e -> { @@ -119,6 +118,8 @@ public final void recalculate() { setDiscreteWidth(nominalWidth); } + invTotalEnergy = 1.0/totalEnergy(); + } /** diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java index 265ac2a..e834f02 100644 --- a/src/main/java/pulse/problem/laser/NumericPulse.java +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -13,6 +13,7 @@ import pulse.tasks.SearchTask; import pulse.baseline.FlatBaseline; +import pulse.tasks.Calculation; /** * A numeric pulse is given by a set of discrete {@code NumericPulseData} @@ -58,7 +59,8 @@ public void init(ExperimentalData data, DiscretePulse pulse) { baselineSubtractedFrom(data); //notify host pulse object of a new pulse width - var problem = ((SearchTask) data.getParent()).getCurrentCalculation().getProblem(); + var problem = ( (Calculation) ((SearchTask) data.getParent()) + .getResponse() ).getProblem(); setPulseWidthOf(problem); //convert to dimensionless time and interpolate @@ -77,7 +79,7 @@ private void baselineSubtractedFrom(ExperimentalData data) { //subtracts a horizontal baseline from the pulse data var baseline = new FlatBaseline(); - baseline.fitNegative(pulseData); + baseline.fitTo(pulseData); for(int i = 0, size = pulseData.getTimeSequence().size(); i < size; i++) pulseData.setSignalAt(i, diff --git a/src/main/java/pulse/problem/laser/NumericPulseData.java b/src/main/java/pulse/problem/laser/NumericPulseData.java index e23c023..5a53fd0 100644 --- a/src/main/java/pulse/problem/laser/NumericPulseData.java +++ b/src/main/java/pulse/problem/laser/NumericPulseData.java @@ -1,6 +1,10 @@ package pulse.problem.laser; +import java.util.List; import pulse.AbstractData; +import pulse.DiscreteInput; +import pulse.input.IndexRange; +import pulse.input.Range; /** * An instance of the {@code AbstractData} class, which also declares an @@ -8,7 +12,7 @@ * measurement imported from an external source. * */ -public class NumericPulseData extends AbstractData { +public class NumericPulseData extends AbstractData implements DiscreteInput { private final int externalID; @@ -55,4 +59,19 @@ public double pulseWidth() { return super.timeLimit(); } + @Override + public List getX() { + return getTimeSequence(); + } + + @Override + public List getY() { + return getSignalData(); + } + + @Override + public IndexRange getIndexRange() { + return new IndexRange(this.getTimeSequence(), Range.UNLIMITED); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java index 879a51c..f4577d9 100644 --- a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java @@ -6,6 +6,7 @@ import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.RTE_SOLVER_ERROR; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; @@ -58,7 +59,8 @@ public final RTECalculationStatus getCalculationStatus() { public final void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.calculationStatus = calculationStatus; if (calculationStatus != RTECalculationStatus.NORMAL) { - throw new SolverException(calculationStatus.toString()); + throw new SolverException(calculationStatus.toString(), + RTE_SOLVER_ERROR); } } diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 64e33e1..59c9220 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -12,8 +12,6 @@ import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; -import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -93,11 +91,8 @@ public void copyFrom(DifferenceScheme df) { protected void prepare(Problem problem) throws SolverException { if (discretePulse == null) { discretePulse = problem.discretePulseOn(grid); - } - else { - discretePulse.recalculate(); - } - + } + discretePulse.recalculate(); clearArrays(); } diff --git a/src/main/java/pulse/problem/schemes/FixedPointIterations.java b/src/main/java/pulse/problem/schemes/FixedPointIterations.java index f2c3a1a..1c359a4 100644 --- a/src/main/java/pulse/problem/schemes/FixedPointIterations.java +++ b/src/main/java/pulse/problem/schemes/FixedPointIterations.java @@ -3,6 +3,7 @@ import static java.lang.Math.abs; import java.util.Arrays; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.FINITE_DIFFERENCE_ERROR; /** * @see Wiki @@ -13,14 +14,15 @@ public interface FixedPointIterations { /** * Performs iterations until the convergence criterion is satisfied.The - latter consists in having a difference two consequent iterations of V - less than the specified error. At the end of each iteration, calls - {@code finaliseIteration()}. + * latter consists in having a difference two consequent iterations of V + * less than the specified error. At the end of each iteration, calls + * {@code finaliseIteration()}. * * @param V the calculation array * @param error used in the convergence criterion * @param m time step - * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed + * @throws pulse.problem.schemes.solvers.SolverException if the calculation + * failed * @see finaliseIteration() * @see iteration() */ @@ -28,8 +30,9 @@ public default void doIterations(double[] V, final double error, final int m) th final int N = V.length - 1; - for (double V_0 = error + 1, V_N = error + 1; abs(V[0] - V_0) > error - || abs(V[N] - V_N) > error; finaliseIteration(V)) { + for (double V_0 = error + 1, V_N = error + 1; + abs(V[0] - V_0)/abs(V[0] + V_0 + 1e-16) > error + || abs(V[N] - V_N)/abs(V[N] + V_N + 1e-16) > error; finaliseIteration(V)) { V_N = V[N]; V_0 = V[0]; @@ -42,7 +45,8 @@ public default void doIterations(double[] V, final double error, final int m) th * Performs an iteration at time {@code m} * * @param m time step - * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed + * @throws pulse.problem.schemes.solvers.SolverException if the calculation + * failed */ public void iteration(final int m) throws SolverException; @@ -50,13 +54,16 @@ public default void doIterations(double[] V, final double error, final int m) th * Finalises the current iteration.By default, does nothing. * * @param V the current iteration - * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed + * @throws pulse.problem.schemes.solvers.SolverException if the calculation + * failed */ public default void finaliseIteration(double[] V) throws SolverException { final double threshold = 1E6; double sum = Arrays.stream(V).sum(); - if( sum > threshold || !Double.isFinite(sum) ) - throw new SolverException("Invalid solution values in V array"); + if (sum > threshold || !Double.isFinite(sum)) { + throw new SolverException("Invalid solution values in V array", + FINITE_DIFFERENCE_ERROR); + } } } diff --git a/src/main/java/pulse/problem/schemes/ImplicitScheme.java b/src/main/java/pulse/problem/schemes/ImplicitScheme.java index 8d85284..217cb28 100644 --- a/src/main/java/pulse/problem/schemes/ImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ImplicitScheme.java @@ -77,14 +77,14 @@ protected void prepare(Problem problem) throws SolverException { @Override public void timeStep(final int m) throws SolverException { - leftBoundary(); + leftBoundary(m); final var V = getCurrentSolution(); final int N = V.length - 1; setSolutionAt(N, evalRightBoundary(tridiagonal.getAlpha()[N], tridiagonal.getBeta()[N])); tridiagonal.sweep(V); } - public void leftBoundary() { + public void leftBoundary(int m) { tridiagonal.setBeta(1, firstBeta()); tridiagonal.evaluateBeta(getPreviousSolution()); } diff --git a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java index 88c7c65..9d98c30 100644 --- a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java @@ -3,7 +3,7 @@ /** * Implements the tridiagonal matrix algorithm (Thomas algorithms) for solving * systems of linear equations. Applicable to such systems where the forming - * matrix has a tridiagonal form. + * matrix has a tridiagonal form: Ai*xi-1 - Bi xi + Ci xi+1 = -Fi. * */ public class TridiagonalMatrixAlgorithm { diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index d50b442..39e504c 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -9,6 +9,7 @@ import pulse.problem.schemes.RadiativeTransferCoupling; import pulse.problem.schemes.rte.Fluxes; import pulse.problem.schemes.rte.RTECalculationStatus; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.RTE_SOLVER_ERROR; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.problem.statements.model.ThermoOpticalProperties; @@ -147,7 +148,8 @@ public Class[] domain() { public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.status = calculationStatus; if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString()); + throw new SolverException(status.toString(), + RTE_SOLVER_ERROR); } } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java index 98c3dbb..e9e4f60 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java @@ -10,6 +10,7 @@ import pulse.problem.schemes.rte.Fluxes; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.rte.RadiativeTransferSolver; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.RTE_SOLVER_ERROR; import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; @@ -103,7 +104,8 @@ public void solve(ParticipatingMedium problem) throws SolverException { var status = getCalculationStatus(); if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString()); + throw new SolverException(status.toString(), + RTE_SOLVER_ERROR); } } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java index 920e4fb..5343958 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java @@ -69,10 +69,10 @@ public void prepare(Problem problem) throws SolverException { } @Override - public void leftBoundary() { + public void leftBoundary(int m) { var tridiagonal = (BlockMatrixAlgorithm) getTridiagonalMatrixAlgorithm(); tridiagonal.setGamma(1, -zN_1 / z0); - super.leftBoundary(); + super.leftBoundary(m); } @Override diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java index 5020d2c..16a80d1 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java @@ -42,17 +42,19 @@ * both the heat equation and the boundary conditions. *

* + * @param a subclass of ClassicalProblem * @see super.solve(Problem) */ -public class ImplicitLinearisedSolver extends ImplicitScheme implements Solver { - - private double Bi1HTAU; +public class ImplicitLinearisedSolver extends ImplicitScheme + implements Solver { private int N; - private double tau; - - private double HH; - private double _2HTAU; + + protected double Bi1HTAU; + protected double tau; + protected double HH; + protected double _2HTAU; + private double zeta; public ImplicitLinearisedSolver() { diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java new file mode 100644 index 0000000..1a09949 --- /dev/null +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java @@ -0,0 +1,226 @@ +package pulse.problem.schemes.solvers; + +import java.util.Set; +import static pulse.problem.schemes.DistributedDetection.evaluateSignal; +import static pulse.problem.statements.model.SpectralRange.LASER; +import static pulse.ui.Messages.getString; + +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.FixedPointIterations; +import pulse.problem.schemes.ImplicitScheme; +import pulse.problem.schemes.TridiagonalMatrixAlgorithm; +import pulse.problem.statements.Problem; +import pulse.problem.statements.TwoTemperatureModel; +import pulse.problem.statements.model.AbsorptionModel; +import pulse.problem.statements.model.TwoTemperatureProperties; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; + +public class ImplicitTwoTemperatureSolver extends ImplicitScheme + implements Solver, FixedPointIterations { + + private AbsorptionModel absorption; + private TridiagonalMatrixAlgorithm gasSolver; + + private int N; + private double hBi; + private double hBiPrime; + private double HH; + private double tau; + private double _05HH_TAU; + + private double[] gasTemp; + + private double diffRatio; + private double g; + private double gPrime; + + private double nonlinearPrecision; + + public ImplicitTwoTemperatureSolver() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + public ImplicitTwoTemperatureSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + private void initSolidPart() { + var grid = getGrid(); + final double hx = grid.getXStep(); + + var solid = new TridiagonalMatrixAlgorithm(grid) { + + @Override + public double phi(final int i) { + return getCurrentPulseValue() * absorption.absorption(LASER, i * hx) + + g * gasTemp[i]; + } + + }; + + solid.setCoefA(1.0 / HH); + solid.setCoefB(1.0 / tau + 2.0 / HH + g); + solid.setCoefC(1.0 / HH); + + solid.setAlpha(1, 1.0 / (1.0 + hBi + _05HH_TAU + 0.5 * HH * g)); + solid.evaluateAlpha(); + setTridiagonalMatrixAlgorithm(solid); + } + + private void initGasPart() { + var grid = getGrid(); + + gasTemp = new double[N + 1]; + var solidTemp = this.getCurrentSolution(); + + gasSolver = new TridiagonalMatrixAlgorithm(grid) { + + @Override + public double phi(final int i) { + return gPrime * solidTemp[i]; + } + + @Override + public void evaluateAlpha() { + setAlpha(1, 1.0 / (1.0 + hBiPrime + diffRatio * (_05HH_TAU + 0.5 * HH * gPrime))); + super.evaluateAlpha(); + } + + @Override + public void evaluateBeta(final double[] U, final int start, final int endExclusive) { + setBeta(1, diffRatio * (0.5 * HH * phi(0) + _05HH_TAU * U[0]) * getAlpha()[1]); + super.evaluateBeta(U, start, endExclusive); + } + + }; + + double invDiffRatio = 1.0 / diffRatio; + gasSolver.setCoefA(invDiffRatio / HH); + gasSolver.setCoefB(1.0 / tau + gPrime + 2.0 / HH * invDiffRatio); + gasSolver.setCoefC(invDiffRatio / HH); + + gasSolver.evaluateAlpha(); + } + + @Override + public void prepare(Problem problem) throws SolverException { + if (!(problem instanceof TwoTemperatureModel)) { + throw new IllegalArgumentException("Illegal model type"); + } + + super.prepare(problem); + var model = (TwoTemperatureModel) problem; + var ttp = (TwoTemperatureProperties) model.getProperties(); + + double hx = getGrid().getXStep(); + tau = getGrid().getTimeStep(); + N = (int) getGrid().getGridDensity().getValue(); + + HH = hx * hx; + _05HH_TAU = 0.5 * HH / tau; + hBi = (double) ttp.getHeatLoss().getValue() * hx; + hBiPrime = (double) ttp.getGasHeatLoss().getValue() * hx; + + g = (double) ttp.getSolidExchangeCoefficient().getValue(); + absorption = model.getAbsorptionModel(); + + diffRatio = model.diffusivityRatio(); + gPrime = (double) ttp.getGasExchangeCoefficient().getValue(); + + initGasPart(); + initSolidPart(); + } + + @Override + public void solve(TwoTemperatureModel problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public void timeStep(final int m) throws SolverException { + doIterations(gasTemp, nonlinearPrecision, m); + } + + @Override + public void iteration(int m) throws SolverException { + //first solve for the solid + super.timeStep(m); + //then for the gas + gasSolver.evaluateBeta(gasTemp); + gasTemp[N] = (diffRatio * (0.5 * HH * gasSolver.phi(N) + _05HH_TAU * gasTemp[N]) + + gasSolver.getBeta()[N]) / (1.0 + diffRatio * (_05HH_TAU + 0.5 * HH * gPrime) + + hBiPrime - gasSolver.getAlpha()[N]); + gasSolver.sweep(gasTemp); + } + + @Override + public double signal() { + return evaluateSignal(absorption, getGrid(), getCurrentSolution()); + } + + @Override + public double evalRightBoundary(final double alphaN, final double betaN) { + var tridiagonal = this.getTridiagonalMatrixAlgorithm(); + return (_05HH_TAU * getPreviousSolution()[N] + 0.5 * HH * tridiagonal.phi(N) + betaN) + / (1.0 + _05HH_TAU + 0.5 * HH * g + hBi - alphaN); + } + + @Override + public double firstBeta() { + var tridiagonal = this.getTridiagonalMatrixAlgorithm(); + return (_05HH_TAU * getPreviousSolution()[0] + 0.5 * HH * tridiagonal.phi(0)) + * tridiagonal.getAlpha()[1]; + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitTwoTemperatureSolver(grid.getGridDensity(), + grid.getTimeFactor(), getTimeLimit()); + } + + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ImplicitScheme.4"); + } + + @Override + public Class[] domain() { + return new Class[]{TwoTemperatureModel.class}; + } + + public NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(NONLINEAR_PRECISION); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/SolverException.java b/src/main/java/pulse/problem/schemes/solvers/SolverException.java index 28bddf4..8aacbe0 100644 --- a/src/main/java/pulse/problem/schemes/solvers/SolverException.java +++ b/src/main/java/pulse/problem/schemes/solvers/SolverException.java @@ -2,9 +2,28 @@ @SuppressWarnings("serial") public class SolverException extends Exception { + + private final SolverExceptionType type; - public SolverException(String status) { + public SolverException(String status, SolverExceptionType type) { super(status); + this.type = type; + } + + public SolverException(SolverExceptionType type) { + this(type.toString(), type); + } + + public SolverExceptionType getType() { + return type; + } + + public enum SolverExceptionType { + RTE_SOLVER_ERROR, + OPTIMISATION_ERROR, + OPTIMISATION_TIMEOUT, + FINITE_DIFFERENCE_ERROR, + ILLEGAL_PARAMETERS, } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem.java b/src/main/java/pulse/problem/statements/ClassicalProblem.java index 72796cc..7243985 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem.java @@ -1,7 +1,7 @@ package pulse.problem.statements; -import java.util.List; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.StickTransform; @@ -9,7 +9,6 @@ import pulse.problem.schemes.solvers.ImplicitLinearisedSolver; import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; @@ -95,19 +94,18 @@ public void set(NumericPropertyKeyword type, NumericProperty value) { } @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { - super.optimisationVector(output, flags); + super.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); if (key == SOURCE_GEOMETRIC_FACTOR) { var bounds = Segment.boundsFrom(SOURCE_GEOMETRIC_FACTOR); - output.setParameterBounds(i, bounds); - output.setTransform(i, new StickTransform(bounds)); - output.set(i, bias); + p.setTransform(new StickTransform(bounds)); + p.setValue(bias); } } @@ -117,10 +115,10 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { super.assign(params); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - double value = params.get(i); - var key = params.getIndex(i); + double value = p.inverseTransform(); + var key = p.getIdentifier().getKeyword(); if (key == SOURCE_GEOMETRIC_FACTOR) { setGeometricFactor(derive(SOURCE_GEOMETRIC_FACTOR, value)); diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index 278fe8e..74b74e0 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -3,7 +3,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; -import java.util.List; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -19,7 +19,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_SIDE; import pulse.ui.Messages; @@ -69,14 +68,14 @@ public DiscretePulse discretePulseOn(Grid grid) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); var properties = (ExtendedThermalProperties) getProperties(); double value; - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); Transformable transform = new InvDiamTransform(properties); var bounds = Segment.boundsFrom(key); @@ -102,9 +101,9 @@ public void optimisationVector(ParameterVector output, List flags) { continue; } - output.setTransform(i, transform); - output.setParameterBounds(i, bounds); - output.set(i, value); + p.setTransform(transform); + p.setBounds(bounds); + p.setValue(value); } @@ -116,20 +115,20 @@ public void assign(ParameterVector params) throws SolverException { var properties = (ExtendedThermalProperties) getProperties(); // TODO one-to-one mapping for FOV and SPOT_DIAMETER - for (int i = 0, size = params.dimension(); i < size; i++) { - var type = params.getIndex(i); + for (Parameter p : params.getParameters()) { + var type = p.getIdentifier().getKeyword(); switch (type) { case FOV_OUTER: case FOV_INNER: case HEAT_LOSS_SIDE: case HEAT_LOSS_COMBINED: - properties.set(type, derive(type, params.inverseTransform(i))); + properties.set(type, derive(type, p.inverseTransform())); break; case SPOT_DIAMETER: - ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, params.inverseTransform(i))); + ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, + p.inverseTransform())); break; default: - continue; } } } diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index 488eafd..dc4bf9b 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -3,7 +3,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; -import java.util.List; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -13,7 +13,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.DiathermicProperties; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_CONVECTIVE; import pulse.ui.Messages; @@ -34,7 +33,6 @@ */ public class DiathermicMedium extends ClassicalProblem { - public DiathermicMedium() { super(); } @@ -54,15 +52,15 @@ public void initProperties(ThermalProperties properties) { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); var properties = (DiathermicProperties) this.getProperties(); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); - Segment bounds = null; - double value = 0; + var key = p.getIdentifier().getKeyword(); + Segment bounds; + double value; switch (key) { case DIATHERMIC_COEFFICIENT: @@ -83,9 +81,9 @@ public void optimisationVector(ParameterVector output, List flags) { continue; } - output.setTransform(i, new StickTransform(bounds)); - output.set(i, value); - output.setParameterBounds(i, bounds); + p.setTransform(new StickTransform(bounds)); + p.setValue(value); + p.setBounds(bounds); } @@ -96,17 +94,19 @@ public void assign(ParameterVector params) throws SolverException { super.assign(params); var properties = (DiathermicProperties) this.getProperties(); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - var key = params.getIndex(i); + var key = p.getIdentifier().getKeyword(); switch (key) { case DIATHERMIC_COEFFICIENT: - properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, params.inverseTransform(i))); + properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, + p.inverseTransform())); break; case HEAT_LOSS_CONVECTIVE: - properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, params.inverseTransform(i))); + properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, + p.inverseTransform())); break; default: } diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index 28b2857..f21b3b8 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -1,6 +1,5 @@ package pulse.problem.statements; -import java.util.List; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; import static pulse.properties.NumericPropertyKeyword.DENSITY; @@ -11,13 +10,13 @@ import java.util.Set; import pulse.input.ExperimentalData; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.StickTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ImplicitScheme; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.LASER_ENERGY; @@ -82,10 +81,10 @@ public void assign(ParameterVector params) throws SolverException { super.assign(params); getProperties().calculateEmissivity(); - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - double value = params.inverseTransform(i); - NumericPropertyKeyword key = params.getIndex(i); + double value = p.inverseTransform(); + NumericPropertyKeyword key = p.getIdentifier().getKeyword(); if (key == LASER_ENERGY) { this.getPulse().setLaserEnergy(derive(key, value)); @@ -104,18 +103,18 @@ public void assign(ParameterVector params) throws SolverException { */ @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); if(key == LASER_ENERGY) { var bounds = Segment.boundsFrom(LASER_ENERGY); - output.setParameterBounds(i, bounds); - output.setTransform(i, new StickTransform(bounds)); - output.set(i, (double) getPulse().getLaserEnergy().getValue()); + p.setBounds(bounds); + p.setTransform(new StickTransform(bounds)); + p.setValue( (double) getPulse().getLaserEnergy().getValue()); } } @@ -132,4 +131,4 @@ public Problem copy() { return new NonlinearProblem(this); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index ad0d177..e8497e2 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -33,10 +33,10 @@ public String toString() { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); var properties = (ThermoOpticalProperties) getProperties(); - properties.optimisationVector(output, flags); + properties.optimisationVector(output); } @Override diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index b93e68f..d08d17c 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -9,7 +9,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.AbsorptionModel; import pulse.problem.statements.model.BeerLambertAbsorption; -import pulse.properties.Flag; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.properties.Property; @@ -21,20 +20,22 @@ public class PenetrationProblem extends ClassicalProblem { private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( "Absorption Model Selector", AbsorptionModel.class); - private AbsorptionModel absorption = instanceDescriptor.newInstance(AbsorptionModel.class); + private AbsorptionModel absorption; public PenetrationProblem() { super(); instanceDescriptor.setSelectedDescriptor(BeerLambertAbsorption.class.getSimpleName()); instanceDescriptor.addListener(() -> initAbsorption()); + absorption = instanceDescriptor.newInstance(AbsorptionModel.class); absorption.setParent(this); } public PenetrationProblem(PenetrationProblem p) { super(p); - instanceDescriptor.setSelectedDescriptor((String) p.getAbsorptionSelector().getValue()); + instanceDescriptor.setSelectedDescriptor(BeerLambertAbsorption.class.getSimpleName()); instanceDescriptor.addListener(() -> initAbsorption()); - initAbsorption(); + this.absorption = p.getAbsorptionModel().copy(); + this.absorption.setParent(this); } private void initAbsorption() { @@ -70,9 +71,9 @@ public InstanceDescriptor getAbsorptionSelector() { } @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - absorption.optimisationVector(output, flags); + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + absorption.optimisationVector(output); } @Override diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index d2bde96..f228515 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -7,23 +7,25 @@ import java.util.List; import java.util.Set; +import java.util.concurrent.Executors; import java.util.stream.Collectors; import pulse.HeatingCurve; import pulse.baseline.Baseline; +import pulse.baseline.FlatBaseline; import pulse.baseline.LinearBaseline; import pulse.input.ExperimentalData; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; -import pulse.math.transforms.InvLenSqTransform; import pulse.math.transforms.StickTransform; import pulse.problem.laser.DiscretePulse; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.ILLEGAL_PARAMETERS; import pulse.problem.statements.model.ThermalProperties; -import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; @@ -57,9 +59,9 @@ public abstract class Problem extends PropertyHolder implements Reflexive, Optim private static boolean hideDetailedAdjustment = true; private ProblemComplexity complexity = ProblemComplexity.LOW; - private InstanceDescriptor instanceDescriptor + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( - "Baseline Selector", Baseline.class); + "Baseline Selector", Baseline.class); /** * Creates a {@code Problem} with default parameters (as found in the .XML @@ -74,10 +76,8 @@ public abstract class Problem extends PropertyHolder implements Reflexive, Optim protected Problem() { initProperties(); setHeatingCurve(new HeatingCurve()); - - instanceDescriptor.attemptUpdate(LinearBaseline.class.getSimpleName()); addListeners(); - initBaseline(); + instanceDescriptor.attemptUpdate(LinearBaseline.class.getSimpleName()); } /** @@ -88,13 +88,10 @@ protected Problem() { */ public Problem(Problem p) { initProperties(p.getProperties().copy()); - setHeatingCurve(new HeatingCurve(p.getHeatingCurve())); curve.setNumPoints(p.getHeatingCurve().getNumPoints()); - - instanceDescriptor.attemptUpdate(p.getBaseline().getClass().getSimpleName()); + setBaseline(p.getBaseline()); addListeners(); - setBaseline( p.getBaseline().copy() ); } public abstract Problem copy(); @@ -133,7 +130,7 @@ private void addListeners() { public final List availableSolutions() { var allSchemes = Reflexive.instancesOf(DifferenceScheme.class); return allSchemes.stream().filter(scheme -> scheme instanceof Solver) - .filter(s -> Arrays.asList(s.domain()).contains(this.getClass()) ) + .filter(s -> Arrays.asList(s.domain()).contains(this.getClass())) .collect(Collectors.toList()); } @@ -165,7 +162,7 @@ public final Pulse getPulse() { */ public final void setPulse(Pulse pulse) { this.pulse = pulse; - pulse.setParent(this); + this.pulse.setParent(this); } /** @@ -176,7 +173,7 @@ public final void setPulse(Pulse pulse) { * @param c the {@code ExperimentalData} object */ public void retrieveData(ExperimentalData c) { - baseline.fitTo(c); // used to estimate the floor of the signal range + baseline.fitTo(c); estimateSignalRange(c); updateProperties(this, c.getMetadata()); properties.useTheoreticalEstimates(c); @@ -194,9 +191,11 @@ public void retrieveData(ExperimentalData c) { * @see pulse.input.ExperimentalData.maxTemperature() */ public void estimateSignalRange(ExperimentalData c) { - var maxPoint = c.maxAdjustedSignal(); - final double signalHeight = maxPoint.getY() - baseline.valueAt(maxPoint.getX()); - properties.setMaximumTemperature(derive(MAXTEMP, signalHeight)); + var maxPoint = c.getHalfTimeCalculator().getFilteredMaximum(); + var flatBaseline = new FlatBaseline(); + flatBaseline.fitTo(c); + final double signalSpan = maxPoint.getY() - flatBaseline.valueAt(maxPoint.getX()); + properties.setMaximumTemperature(derive(MAXTEMP, signalSpan)); } /** @@ -217,29 +216,25 @@ public void estimateSignalRange(ExperimentalData c) { * class, or putting them in the XML file */ @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { - baseline.optimisationVector(output, flags); + baseline.optimisationVector(output); - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); Segment bounds = Segment.boundsFrom(key); - double value = 0; - + double value; + switch (key) { case THICKNESS: value = (double) properties.getSampleThickness().getValue(); break; case DIFFUSIVITY: - final double a = (double) properties.getDiffusivity().getValue(); - output.setTransform(i, new InvLenSqTransform(properties)); - bounds = new Segment(0.01 * a, 20.0 * a); - output.setParameterBounds(i, bounds); - output.set(i, a); - //custom transform here -- skip assigning StickTransform - continue; + value = (double) properties.getDiffusivity().getValue(); + bounds = new Segment(0.01 * value, 20.0 * value); + break; case MAXTEMP: final double signalHeight = (double) properties.getMaximumTemperature().getValue(); bounds = new Segment(0.5 * signalHeight, 1.5 * signalHeight); @@ -247,7 +242,7 @@ public void optimisationVector(ParameterVector output, List flags) { break; case HEAT_LOSS: value = (double) properties.getHeatLoss().getValue(); - output.setTransform(i, new StickTransform(bounds)); + p.setTransform(new StickTransform(bounds)); break; case TIME_SHIFT: double magnitude = 0.25 * properties.timeFactor(); @@ -257,11 +252,11 @@ public void optimisationVector(ParameterVector output, List flags) { default: continue; } - - output.setTransform(i, new StickTransform(bounds)); - output.setParameterBounds(i, bounds); - output.set(i, value); - + + p.setTransform(new StickTransform(bounds)); + p.setBounds(bounds); + p.setValue(value); + } } @@ -276,25 +271,25 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { baseline.assign(params); - + List malformedList = params.findMalformedElements(); - - if(!malformedList.isEmpty()) { + + if (!malformedList.isEmpty()) { StringBuilder sb = new StringBuilder("Cannot assign values: "); - malformedList.forEach(p -> - sb.append(String.format("%n %-25s", p.toString())) + malformedList.forEach(p + -> sb.append(String.format("%n %-25s", p.toString())) ); - throw new SolverException(sb.toString()); + throw new SolverException(sb.toString(), ILLEGAL_PARAMETERS); } - - for (int i = 0, size = params.dimension(); i < size; i++) { - double value = params.inverseTransform(i); - var key = params.getIndex(i); + for (Parameter p : params.getParameters()) { + + double value = p.inverseTransform(); + var key = p.getIdentifier().getKeyword(); switch (key) { case THICKNESS: - properties.setSampleThickness(derive(THICKNESS, value )); + properties.setSampleThickness(derive(THICKNESS, value)); break; case DIFFUSIVITY: properties.setDiffusivity(derive(DIFFUSIVITY, value)); @@ -412,16 +407,10 @@ public Baseline getBaseline() { * @see pulse.baseline.Baseline.apply(Baseline) */ public final void setBaseline(Baseline baseline) { - this.baseline = baseline; - curve.apply(baseline); - - baseline.setParent(this); - - var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); - if (searchTask != null) { - var experimentalData = searchTask.getExperimentalCurve(); - baseline.fitTo(experimentalData); - } + instanceDescriptor.setSelectedDescriptor(baseline.getClass().getSimpleName()); + this.baseline = baseline.copy(); + this.baseline.setParent(this); + curve.apply(this.baseline); } public final InstanceDescriptor getBaselineDescriptor() { @@ -429,8 +418,14 @@ public final InstanceDescriptor getBaselineDescriptor() { } private void initBaseline() { - var baseline = instanceDescriptor.newInstance(Baseline.class); - setBaseline(baseline); + setBaseline(instanceDescriptor.newInstance(Baseline.class)); + var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); + if (searchTask != null) { + var experimentalData = (ExperimentalData) searchTask.getInput(); + Executors.newSingleThreadExecutor().submit(() + -> baseline.fitTo(experimentalData) + ); + } parameterListChanged(); } @@ -451,4 +446,4 @@ public final void setProperties(ThermalProperties properties) { public abstract boolean isReady(); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index d2b5324..1b0fbf3 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -7,6 +7,7 @@ import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import java.util.List; +import java.util.Objects; import java.util.Set; import pulse.input.ExperimentalData; @@ -92,13 +93,12 @@ private void addListeners() { .derive(NumericPropertyKeyword.LOWER_BOUND, (Number) np.getValue()); - var range = corrTask.getExperimentalCurve().getRange(); + var range = ( (ExperimentalData) corrTask.getInput() ).getRange(); if( range.getLowerBound().compareTo(pw) < 0 ) { //update lower bound of the range for that SearchTask - corrTask.getExperimentalCurve().getRange() - .setLowerBound(pw); + range.setLowerBound(pw); } @@ -141,9 +141,9 @@ public void setPulseWidth(NumericProperty pulseWidth) { //validate -- do not update if the new pulse width is greater than 2 half-times SearchTask task = (SearchTask) this.specificAncestor(SearchTask.class); - ExperimentalData data = task.getExperimentalCurve(); + ExperimentalData data = (ExperimentalData) task.getInput(); - if(newValue < 2.0 * data.getHalfTime()) { + if(newValue < 2.0 * data.getHalfTimeCalculator().getHalfTime()) { this.pulseWidth = (double) pulseWidth.getValue(); firePropertyChanged(this, pulseWidth); } diff --git a/src/main/java/pulse/problem/statements/TwoTemperatureModel.java b/src/main/java/pulse/problem/statements/TwoTemperatureModel.java new file mode 100644 index 0000000..72298d7 --- /dev/null +++ b/src/main/java/pulse/problem/statements/TwoTemperatureModel.java @@ -0,0 +1,176 @@ +package pulse.problem.statements; + +import java.util.List; +import pulse.math.Parameter; +import static pulse.properties.NumericProperties.derive; + +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.StickTransform; +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.solvers.ImplicitTwoTemperatureSolver; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.statements.model.Gas; +import pulse.problem.statements.model.Helium; +import pulse.problem.statements.model.ThermalProperties; +import pulse.problem.statements.model.TwoTemperatureProperties; +import pulse.properties.NumericProperty; +import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; +import pulse.properties.Property; +import pulse.ui.Messages; +import pulse.util.InstanceDescriptor; +import pulse.util.PropertyEvent; + +public class TwoTemperatureModel extends PenetrationProblem { + + private Gas gas; + + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor<>("Gas Selector", Gas.class); + + public TwoTemperatureModel() { + super(); + setComplexity(ProblemComplexity.MODERATE); + instanceDescriptor.setSelectedDescriptor(Helium.class.getSimpleName()); + setGas(instanceDescriptor.newInstance(Gas.class)); + addListeners(); + gas.evaluate((double) this.getProperties().getTestTemperature().getValue()); + } + + public TwoTemperatureModel(TwoTemperatureModel p) { + super(p); + this.gas = p.gas; + instanceDescriptor.setSelectedDescriptor(gas.getClass().getSimpleName()); + addListeners(); + gas.evaluate((double) this.getProperties().getTestTemperature().getValue()); + } + + private void addListeners() { + instanceDescriptor.addListener(() -> setGas(instanceDescriptor.newInstance(Gas.class))); + this.getProperties().addListener((PropertyEvent event) -> { + pulse.properties.Property p1 = event.getProperty(); + if (p1 instanceof NumericProperty) { + pulse.properties.NumericPropertyKeyword npType = ((NumericProperty) p1).getType(); + if (npType == TEST_TEMPERATURE) { + gas.evaluate((double) p1.getValue()); + } + } + }); + } + + @Override + public void initProperties() { + setProperties(new TwoTemperatureProperties()); + } + + @Override + public void initProperties(ThermalProperties properties) { + setProperties(new TwoTemperatureProperties(properties)); + } + + @Override + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + var ttp = (TwoTemperatureProperties) getProperties(); + + for (Parameter p : output.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + Segment bounds = Segment.boundsFrom(p.getIdentifier().getKeyword()); + double value; + switch (key) { + case SOLID_EXCHANGE_COEFFICIENT: + value = (double) ttp.getSolidExchangeCoefficient().getValue(); + break; + case GAS_EXCHANGE_COEFFICIENT: + value = (double) ttp.getGasExchangeCoefficient().getValue(); + break; + case HEAT_LOSS_GAS: + value = (double) ttp.getGasHeatLoss().getValue(); + break; + default: + continue; + } + + p.setTransform(new StickTransform(bounds)); + p.setValue(value); + p.setBounds(bounds); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + var ttp = (TwoTemperatureProperties) getProperties(); + + for (Parameter p : params.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + var np = derive(key, p.inverseTransform()); + + switch (key) { + case SOLID_EXCHANGE_COEFFICIENT: + ttp.setSolidExchangeCoefficient(np); + break; + case GAS_EXCHANGE_COEFFICIENT: + ttp.setGasExchangeCoefficient(np); + break; + case HEAT_LOSS_GAS: + ttp.setGasHeatLoss(np); + break; + default: + } + + } + + } + + @Override + public String toString() { + return Messages.getString("TwoTemperatureModel.Descriptor"); + } + + @Override + public Class defaultScheme() { + return ImplicitTwoTemperatureSolver.class; + } + + @Override + public TwoTemperatureModel copy() { + return new TwoTemperatureModel(this); + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + public InstanceDescriptor getGasSelector() { + return instanceDescriptor; + } + + public Gas getGas() { + return gas; + } + + public final void setGas(Gas gas) { + this.gas = gas; + gas.evaluate((double) getProperties().getTestTemperature().getValue()); + firePropertyChanged(this, instanceDescriptor); + } + + /** + * Diffusivity of solid over diffusivity of gas + * + * @return + */ + public double diffusivityRatio() { + return (double) getProperties().getDiffusivity().getValue() + / gas.thermalDiffusivity(); + } + +} diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index 3560faf..9165b08 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -7,15 +7,13 @@ import static pulse.properties.NumericPropertyKeyword.THERMAL_ABSORPTIVITY; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; -import pulse.math.Segment; import static pulse.math.transforms.StandardTransformations.ABS; import pulse.math.transforms.Transformable; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; @@ -35,6 +33,11 @@ protected AbsorptionModel() { absorptionMap.put(LASER, def(LASER_ABSORPTIVITY)); absorptionMap.put(THERMAL, def(THERMAL_ABSORPTIVITY)); } + + protected AbsorptionModel(AbsorptionModel c) { + this.absorptionMap = new HashMap<>(); + this.absorptionMap.putAll(c.absorptionMap); + } public abstract double absorption(SpectralRange range, double x); @@ -105,9 +108,9 @@ public Set listedKeywords() { } @Override - public void optimisationVector(ParameterVector output, List flags) { - for (int i = 0, size = output.dimension(); i < size; i++) { - var key = output.getIndex(i); + public void optimisationVector(ParameterVector output) { + for (Parameter p : output.getParameters()) { + var key = p.getIdentifier().getKeyword(); double value = 0; Transformable transform = ABS; @@ -127,9 +130,8 @@ public void optimisationVector(ParameterVector output, List flags) { } //do this for the listed key values - output.setParameterBounds(i, Segment.boundsFrom(key)); - output.setTransform(i, transform); - output.set(i, value); + p.setTransform(transform); + p.setValue(value); } @@ -139,14 +141,14 @@ public void optimisationVector(ParameterVector output, List flags) { public void assign(ParameterVector params) throws SolverException { double value; - for (int i = 0, size = params.dimension(); i < size; i++) { - var key = params.getIndex(i); + for (Parameter p : params.getParameters()) { + var key = p.getIdentifier().getKeyword(); switch (key) { case LASER_ABSORPTIVITY: case THERMAL_ABSORPTIVITY: case COMBINED_ABSORPTIVITY: - value = params.inverseTransform(i); + value = p.inverseTransform(); break; default: continue; @@ -156,5 +158,7 @@ public void assign(ParameterVector params) throws SolverException { } } + + public abstract AbsorptionModel copy(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java index 3171335..3aebab0 100644 --- a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java +++ b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java @@ -6,10 +6,19 @@ public BeerLambertAbsorption() { super(); } + public BeerLambertAbsorption(AbsorptionModel m) { + super(m); + } + @Override public double absorption(SpectralRange range, double y) { double a = (double) (this.getAbsorptivity(range).getValue()); return a * Math.exp(-a * y); } + @Override + public AbsorptionModel copy() { + return new BeerLambertAbsorption(this); + } + } diff --git a/src/main/java/pulse/problem/statements/model/Gas.java b/src/main/java/pulse/problem/statements/model/Gas.java new file mode 100644 index 0000000..7442ebd --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Gas.java @@ -0,0 +1,75 @@ +package pulse.problem.statements.model; + +import pulse.util.Descriptive; +import pulse.util.Reflexive; + +public abstract class Gas implements Reflexive, Descriptive { + + private double conductivity; + private double thermalMass; + private final int atoms; + private final double mass; + + /** + * Universal gas constant. + */ + + public final static double R = 8.314; //J/K/mol + + private final static double ROOM_TEMPERATURE = 300; + private final static double NORMAL_PRESSURE = 1E5; + + public Gas(int atoms, double atomicWeight) { + evaluate(ROOM_TEMPERATURE, NORMAL_PRESSURE); + this.atoms = atoms; + this.mass = atoms * atomicWeight/1e3; + } + + public final void evaluate(double temperature, double pressure) { + this.conductivity = thermalConductivity(temperature); + this.thermalMass = cp() * density(temperature, pressure); + } + + public final void evaluate(double temperature) { + evaluate(temperature, NORMAL_PRESSURE); + } + + public final double thermalDiffusivity() { + return conductivity/thermalMass; + } + + public abstract double thermalConductivity(double t); + + public double cp() { + return (1.5 + atoms) * R / mass; + } + + public double density(double temperature, double pressure) { + return pressure * mass / (R * temperature); + } + + public double getThermalMass() { + return thermalMass; + } + + public double getConductivity() { + return conductivity; + } + + public double getNumberOfAtoms() { + return atoms; + } + + public double getMolarMass() { + return mass; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getSimpleName()); + sb.append(String.format(" : conductivity = %3.4f; thermal mass = %3.4f; ", conductivity, thermalMass)); + sb.append(String.format("atoms per molecule = %d; atomic weight = %1.4f", atoms, mass)); + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/Helium.java b/src/main/java/pulse/problem/statements/model/Helium.java new file mode 100644 index 0000000..96b71f0 --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Helium.java @@ -0,0 +1,14 @@ +package pulse.problem.statements.model; + +public class Helium extends Gas { + + public Helium() { + super(1, 4); + } + + @Override + public double thermalConductivity(double t) { + return 0.415 + 0.283E-3 * (t - 1200); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/Insulator.java b/src/main/java/pulse/problem/statements/model/Insulator.java index 0cbba0c..88c5972 100644 --- a/src/main/java/pulse/problem/statements/model/Insulator.java +++ b/src/main/java/pulse/problem/statements/model/Insulator.java @@ -17,6 +17,15 @@ public Insulator() { super(); R = (double) def(REFLECTANCE).getValue(); } + + public Insulator(AbsorptionModel m) { + super(m); + if(m instanceof Insulator) { + R = (double) ((Insulator) m).getReflectance().getValue(); + } else { + R = (double) def(REFLECTANCE).getValue(); + } + } @Override public double absorption(SpectralRange spectrum, double x) { @@ -48,4 +57,9 @@ public Set listedKeywords() { return set; } + @Override + public AbsorptionModel copy() { + return new Insulator(this); + } + } diff --git a/src/main/java/pulse/problem/statements/model/Nitrogen.java b/src/main/java/pulse/problem/statements/model/Nitrogen.java new file mode 100644 index 0000000..acaef03 --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Nitrogen.java @@ -0,0 +1,14 @@ +package pulse.problem.statements.model; + +public class Nitrogen extends Gas { + + public Nitrogen() { + super(2, 14); + } + + @Override + public double thermalConductivity(double t) { + return Math.sqrt(t) * (-92.39/t + 1.647 + 5.255E-4*t) * 1E-3; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index c3691eb..153faa1 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -66,6 +66,7 @@ public ThermalProperties(ThermalProperties p) { this.a = p.a; this.Bi = p.Bi; this.T = p.T; + this.signalHeight = p.signalHeight; this.emissivity = p.emissivity; initListeners(); fill(); @@ -280,7 +281,7 @@ public Set listedKeywords() { } public final double thermalConductivity() { - return a * cP * rho; + return a * getThermalMass(); } public NumericProperty getThermalConductivity() { @@ -327,6 +328,10 @@ public double maxRadiationBiot() { public double timeFactor() { return l * l / a; } + + public double getThermalMass() { + return cP * rho; + } /** * Calculates the half-rise time t1/2 of {@code c} and @@ -338,7 +343,7 @@ public double timeFactor() { * @see pulse.input.ExperimentalData.halfRiseTime() */ public void useTheoreticalEstimates(ExperimentalData c) { - final double t0 = c.getHalfTime(); + final double t0 = c.getHalfTimeCalculator().getHalfTime(); this.a = PARKERS_COEFFICIENT * l * l / t0; if (areThermalPropertiesLoaded()) { Bi = radiationBiot(); @@ -352,7 +357,7 @@ public final boolean areThermalPropertiesLoaded() { public double maximumHeating(Pulse2D pulse) { final double Q = (double) pulse.getLaserEnergy().getValue(); final double dLas = (double) pulse.getSpotDiameter().getValue(); - return 4.0 * emissivity * Q / (PI * dLas * dLas * l * cP * rho); + return 4.0 * emissivity * Q / (PI * dLas * dLas * l * getThermalMass() ); } public NumericProperty getEmissivity() { diff --git a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java index 67db88b..371fadc 100644 --- a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -1,6 +1,5 @@ package pulse.problem.statements.model; -import java.util.List; import static pulse.math.MathUtils.fastPowLoop; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperty.requireType; @@ -10,12 +9,12 @@ import java.util.Set; import pulse.input.ExperimentalData; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.transforms.StickTransform; import pulse.math.transforms.Transformable; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -189,14 +188,14 @@ public String toString() { } @Override - public void optimisationVector(ParameterVector output, List flags) { + public void optimisationVector(ParameterVector output) { Segment bounds = null; double value; Transformable transform; - for (int i = 0, size = output.dimension(); i < size; i++) { + for (Parameter p : output.getParameters()) { - var key = output.getIndex(i); + var key = p.getIdentifier().getKeyword(); switch (key) { case PLANCK_NUMBER: @@ -230,9 +229,9 @@ public void optimisationVector(ParameterVector output, List flags) { } transform = new StickTransform(bounds); - output.setTransform(i, transform); - output.set(i, value); - output.setParameterBounds(i, bounds); + p.setTransform(transform); + p.setValue(value); + p.setBounds(bounds); } @@ -241,9 +240,9 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { - for (int i = 0, size = params.dimension(); i < size; i++) { + for (Parameter p : params.getParameters()) { - var type = params.getIndex(i); + var type = p.getIdentifier().getKeyword(); switch (type) { @@ -252,7 +251,7 @@ public void assign(ParameterVector params) throws SolverException { case SCATTERING_ANISOTROPY: case OPTICAL_THICKNESS: case HEAT_LOSS_CONVECTIVE: - set(type, derive(type, params.inverseTransform(i))); + set(type, derive(type, p.inverseTransform())); break; default: break; diff --git a/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java b/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java new file mode 100644 index 0000000..c879327 --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java @@ -0,0 +1,110 @@ +package pulse.problem.statements.model; + +import java.util.Set; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.GAS_EXCHANGE_COEFFICIENT; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_GAS; +import static pulse.properties.NumericPropertyKeyword.SOLID_EXCHANGE_COEFFICIENT; + +public class TwoTemperatureProperties extends ThermalProperties { + + private double exchangeSolid; + private double exchangeGas; + private double gasHeatLoss; + + public TwoTemperatureProperties() { + super(); + exchangeSolid = (double) def(SOLID_EXCHANGE_COEFFICIENT).getValue(); + exchangeGas = (double) def(GAS_EXCHANGE_COEFFICIENT).getValue(); + gasHeatLoss = (double) def(HEAT_LOSS_GAS).getValue(); + } + + public TwoTemperatureProperties(ThermalProperties p) { + super(p); + if (p instanceof TwoTemperatureProperties) { + var np = (TwoTemperatureProperties) p; + this.exchangeSolid = np.exchangeSolid; + this.exchangeGas = np.exchangeGas; + this.gasHeatLoss = np.gasHeatLoss; + } + else { + exchangeSolid = (double) def(SOLID_EXCHANGE_COEFFICIENT).getValue(); + exchangeGas = (double) def(GAS_EXCHANGE_COEFFICIENT).getValue(); + gasHeatLoss = (double) def(HEAT_LOSS_GAS).getValue(); + } + } + + @Override + public ThermalProperties copy() { + return new TwoTemperatureProperties(this); + } + + /** + * Used to change the parameter values of this {@code Problem}. It is only + * allowed to use those types of {@code NumericPropery} that are listed by + * the {@code listedParameters()}. + * + * @param type + * @param value + * @see listedTypes() + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + switch (type) { + case SOLID_EXCHANGE_COEFFICIENT: + setSolidExchangeCoefficient(value); + break; + case GAS_EXCHANGE_COEFFICIENT: + setGasExchangeCoefficient(value); + break; + case HEAT_LOSS_GAS: + setGasHeatLoss(value); + break; + default: + super.set(type, value); + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(HEAT_LOSS_GAS); + set.add(SOLID_EXCHANGE_COEFFICIENT); + set.add(GAS_EXCHANGE_COEFFICIENT); + return set; + } + + public NumericProperty getSolidExchangeCoefficient() { + return derive(SOLID_EXCHANGE_COEFFICIENT, exchangeSolid); + } + + public NumericProperty getGasExchangeCoefficient() { + return derive(GAS_EXCHANGE_COEFFICIENT, exchangeGas); + } + + public void setSolidExchangeCoefficient(NumericProperty p) { + NumericProperty.requireType(p, SOLID_EXCHANGE_COEFFICIENT); + this.exchangeSolid = (double) p.getValue(); + firePropertyChanged(this, p); + } + + public void setGasExchangeCoefficient(NumericProperty p) { + NumericProperty.requireType(p, GAS_EXCHANGE_COEFFICIENT); + this.exchangeGas = (double) p.getValue(); + firePropertyChanged(this, p); + } + + public NumericProperty getGasHeatLoss() { + return derive(HEAT_LOSS_GAS, gasHeatLoss); + } + + public void setGasHeatLoss(NumericProperty p) { + NumericProperty.requireType(p, HEAT_LOSS_GAS); + this.gasHeatLoss = (double) p.getValue(); + firePropertyChanged(this, p); + } + +} diff --git a/src/main/java/pulse/properties/Flag.java b/src/main/java/pulse/properties/Flag.java index 830f8ab..94b6ce3 100644 --- a/src/main/java/pulse/properties/Flag.java +++ b/src/main/java/pulse/properties/Flag.java @@ -24,9 +24,17 @@ public class Flag implements Property { * {@code Flag} */ public Flag(NumericPropertyKeyword type) { - this.index = type; - value = false; + this(type, false); + } + + public Flag(Flag f) { + this(f.index, f.value); } + + public Flag(NumericPropertyKeyword type, boolean flag) { + this.index = type; + this.value = flag; + } /** * Creates a {@code Flag} with the following pre-specified parameters: type diff --git a/src/main/java/pulse/properties/NumericProperty.java b/src/main/java/pulse/properties/NumericProperty.java index 10aee7e..734a70f 100644 --- a/src/main/java/pulse/properties/NumericProperty.java +++ b/src/main/java/pulse/properties/NumericProperty.java @@ -1,8 +1,5 @@ package pulse.properties; -import java.text.ParseException; -import java.util.logging.Level; -import java.util.logging.Logger; import pulse.math.Segment; import static pulse.properties.NumericProperties.compare; import static pulse.properties.NumericProperties.derive; diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 539a832..a657171 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -360,8 +360,38 @@ public enum NumericPropertyKeyword { * compared to rear face. Can be a number between zero and unity. */ - SOURCE_GEOMETRIC_FACTOR; - + SOURCE_GEOMETRIC_FACTOR, + + /** + * Max. no. of high-frequency waves in the sinusoidal baseline. + */ + + MAX_HIGH_FREQ_WAVES, + + /** + * Max. no. of low-frequency waves in the sinusoidal baseline. + */ + + MAX_LOW_FREQ_WAVES, + + /** + * Energy exchange coefficient in the two-temperature model (g). + */ + + SOLID_EXCHANGE_COEFFICIENT, + + /** + * Energy exchange coefficient in the two-temperature model (g'). + */ + + GAS_EXCHANGE_COEFFICIENT, + + /** + * Heat loss for the gas in the 2T-model. + */ + + HEAT_LOSS_GAS; + public static Optional findAny(String key) { return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); } diff --git a/src/main/java/pulse/search/GeneralTask.java b/src/main/java/pulse/search/GeneralTask.java new file mode 100644 index 0000000..44c64ac --- /dev/null +++ b/src/main/java/pulse/search/GeneralTask.java @@ -0,0 +1,217 @@ +package pulse.search; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.math.ParameterVector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.direction.IterativeState; +import pulse.search.direction.PathOptimiser; +import pulse.tasks.processing.Buffer; +import static pulse.tasks.processing.Buffer.getSize; +import pulse.util.Accessible; + +public abstract class GeneralTask + extends Accessible implements Runnable { + + private IterativeState path; //current sate + private IterativeState best; //best state + + private final Buffer buffer; + private PathOptimiser optimiser; + + public GeneralTask() { + buffer = new Buffer(); + buffer.setParent(this); + } + + public abstract List activeParameters(); + + /** + * Creates a search vector populated by parameters that + * are included in the optimisation routine. + * @return the parameter vector with optimisation parameters + */ + + public abstract ParameterVector searchVector(); + + /** + * Tries to assign a selected set of parameters to the search vector + * used in optimisation. + * @param pv a parameter vector containing all of the optimisation parameters + * whose values will be assigned to this task + * @throws SolverException + */ + + public abstract void assign(ParameterVector pv) throws SolverException; + + /** + *

+ * Runs this task if is either {@code READY} or {@code QUEUED}. Otherwise, + * will do nothing. After making some preparatory steps, will initiate a + * loop with successive calls to {@code PathSolver.iteration(this)}, filling + * the buffer and notifying any data change listeners in parallel. This loop + * will go on until either converging results are obtained, or a timeout is + * reached, or if an execution error happens. Whether the run has been + * successful will be determined by comparing the associated + * R2 value with the {@code SUCCESS_CUTOFF}. + *

+ */ + @Override + public void run() { + setDefaultOptimiser(); + setIterativeState( optimiser.initState(this) ); + + var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); + int bufferSize = (Integer) getSize().getValue(); + buffer.init(); + //correlationBuffer.clear(); + + /* search cycle */ + /* sets an independent thread for manipulating the buffer */ + List> bufferFutures = new ArrayList<>(bufferSize); + var singleThreadExecutor = Executors.newSingleThreadExecutor(); + + var response = getResponse(); + + try { + response.objectiveFunction(this); + } catch (SolverException e1) { + onSolverException(e1); + } + + outer: + do { + + bufferFutures.clear(); + + for (var i = 0; i < bufferSize; i++) { + + try { + for (boolean finished = false; !finished;) { + finished = optimiser.iteration(this); + } + } catch (SolverException e) { + onSolverException(e); + break outer; + } + + //if global best is better than the converged value + if (best != null && best.getCost() < path.getCost()) { + try { + //assign the global best parameters + assign(path.getParameters()); + //and try to re-calculate + response.objectiveFunction(this); + } catch (SolverException ex) { + onSolverException(ex); + } + } + + final var j = i; + + bufferFutures.add(CompletableFuture.runAsync(() -> { + buffer.fill(this, j); + intermediateProcessing(); + }, singleThreadExecutor)); + + } + + bufferFutures.forEach(future -> future.join()); + + } while (buffer.isErrorTooHigh(errorTolerance) + && isInProgress()); + + singleThreadExecutor.shutdown(); + + if (isInProgress()) { + postProcessing(); + } + + } + + public abstract boolean isInProgress(); + + /** + * Override this to add intermediate processing of results e.g. + * with a correlation test. + */ + + public void intermediateProcessing() { + //empty + } + + /** + * Specifies what should be done when a solver exception is encountered. + * Empty by default + * @param e1 a solver exception + */ + + public void onSolverException(SolverException e1) { + //empty + } + + /** + * Override this to add post-processing checks + * e.g. normality tests or range checking. + */ + + public void postProcessing() { + //empty + } + + public final Buffer getBuffer() { + return buffer; + } + + public void setIterativeState(IterativeState state) { + this.path = state; + } + + public IterativeState getIterativeState() { + return path; + } + + public IterativeState getBestState() { + return best; + } + + /** + * Update the best state. The instance of this class stores two objects of + * the type IterativeState: the current state of the optimiser and the + * global best state. Calling this method will check if a new global best is + * found, and if so, this will store its parameters in the corresponding + * variable. This will then be used at the final stage of running the search + * task, comparing the converged result to the global best, and selecting + * whichever has the lowest cost. Such routine is required due to the + * possibility of some optimisers going uphill. + */ + public void storeState() { + if (best == null || best.getCost() > path.getCost()) { + best = new IterativeState(path); + } + } + + public final void setOptimiser(PathOptimiser optimiser) { + this.optimiser = optimiser; + } + + public void setDefaultOptimiser() { + var instance = PathOptimiser.getInstance(); + if(optimiser == null || optimiser != instance) { + setOptimiser(PathOptimiser.getInstance()); + } + } + + public double objectiveFunction() throws SolverException { + return getResponse().objectiveFunction(this); + } + + public abstract I getInput(); + public abstract R getResponse(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/Optimisable.java b/src/main/java/pulse/search/Optimisable.java index 2bbf6b9..44666f5 100644 --- a/src/main/java/pulse/search/Optimisable.java +++ b/src/main/java/pulse/search/Optimisable.java @@ -1,10 +1,8 @@ package pulse.search; -import java.util.List; import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; /** * An interface for dealing with optimisation variables. The variables are @@ -19,13 +17,13 @@ public interface Optimisable { * updated, the types of which are listed as indices in the {@code params} * vector. * - * @param params the optimisation vector, containing a similar set of + * @param input the optimisation vector, containing a similar set of * parameters to this {@code Problem} * @throws SolverException if {@code params} contains invalid parameter * values * @see pulse.util.PropertyHolder.listedTypes() */ - public void assign(ParameterVector params) throws SolverException; + public void assign(ParameterVector input) throws SolverException; /** * Calculates the vector argument defined on @@ -33,9 +31,7 @@ public interface Optimisable { * to the scalar objective function for this {@code Optimisable}. * * @param output the output vector where the result will be stored - * @param flags a list of {@code Flag} objects, which determine the basis of - * the search */ - public void optimisationVector(ParameterVector output, List flags); + public void optimisationVector(ParameterVector output); } diff --git a/src/main/java/pulse/search/SimpleOptimisationTask.java b/src/main/java/pulse/search/SimpleOptimisationTask.java new file mode 100644 index 0000000..4fbce66 --- /dev/null +++ b/src/main/java/pulse/search/SimpleOptimisationTask.java @@ -0,0 +1,93 @@ +package pulse.search; + +import java.util.List; +import java.util.stream.Collectors; +import pulse.DiscreteInput; +import pulse.math.ParameterIdentifier; +import pulse.math.ParameterVector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.Flag; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.direction.ActiveFlags; +import static pulse.search.direction.ActiveFlags.selectActiveAndListed; +import pulse.search.direction.LMOptimiser; +import pulse.search.direction.PathOptimiser; +import pulse.util.PropertyHolder; + +/** + * Generic optimisation class. + * + * @param an optimisable object + */ +public abstract class SimpleOptimisationTask + extends GeneralTask { + + private final T optimisable; + private final DiscreteInput input; + + public SimpleOptimisationTask(T optimisable, DiscreteInput input) { + this.input = input; + this.optimisable = optimisable; + } + + @Override + public void run() { + var optimiser = PathOptimiser.getInstance(); + if(optimiser == null) { + PathOptimiser.setInstance(LMOptimiser.getInstance()); + } + super.run(); + } + + /** + * Generates a search vector (= optimisation vector) using the search flags + * set by the {@code PathSolver}. + * + * @return an {@code IndexedVector} with search parameters of this + * {@code SearchTaks} + * @see pulse.search.direction.PathSolver.getSearchFlags() + * @see pulse.problem.statements.Problem.optimisationVector(List) + */ + @Override + public ParameterVector searchVector() { + var ids = activeParameters().stream().map(id + -> new ParameterIdentifier(id)).collect(Collectors.toList()); + var optimisationVector = new ParameterVector(ids); + + optimisable.optimisationVector(optimisationVector); + + return optimisationVector; + } + + @Override + public void assign(ParameterVector pv) throws SolverException { + optimisable.assign(pv); + } + + @Override + public boolean isInProgress() { + return false; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + optimisable.set(type, property); + } + + @Override + public List activeParameters() { + return selectActiveAndListed(ActiveFlags.getAllFlags(), optimisable); + } + + @Override + public void setDefaultOptimiser() { + setOptimiser(LMOptimiser.getInstance()); + } + + @Override + public DiscreteInput getInput() { + return input; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/SimpleResponse.java b/src/main/java/pulse/search/SimpleResponse.java new file mode 100644 index 0000000..9b65871 --- /dev/null +++ b/src/main/java/pulse/search/SimpleResponse.java @@ -0,0 +1,35 @@ +package pulse.search; + +import pulse.Response; +import pulse.math.Segment; +import pulse.search.statistics.OptimiserStatistic; + +public abstract class SimpleResponse implements Response { + + private OptimiserStatistic rs; + + public SimpleResponse(OptimiserStatistic rs) { + setOptimiserStatistic(rs); + } + + @Override + public final OptimiserStatistic getOptimiserStatistic() { + return rs; + } + + public final void setOptimiserStatistic(OptimiserStatistic statistic) { + this.rs = statistic; + } + + @Override + public double objectiveFunction(GeneralTask task) { + rs.evaluate(task); + return (double) rs.getStatistic().getValue(); + } + + @Override + public Segment accessibleRange() { + return Segment.UNBOUNDED; + } + +} diff --git a/src/main/java/pulse/search/direction/ActiveFlags.java b/src/main/java/pulse/search/direction/ActiveFlags.java index e13af6e..81103d1 100644 --- a/src/main/java/pulse/search/direction/ActiveFlags.java +++ b/src/main/java/pulse/search/direction/ActiveFlags.java @@ -3,12 +3,13 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import pulse.problem.statements.Problem; +import pulse.input.ExperimentalData; import pulse.properties.Flag; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.util.PropertyHolder; @@ -41,12 +42,12 @@ public static Set availableProperties() { return set; } - var p = t.getCurrentCalculation().getProblem(); + var p = ( (Calculation) t.getResponse() ).getProblem(); if (p != null) { var fullList = p.listedKeywords(); - fullList.addAll(t.getExperimentalCurve().listedKeywords()); + fullList.addAll( ( (ExperimentalData) t.getInput() ).listedKeywords()); NumericPropertyKeyword key; for (Flag property : flags) { @@ -61,28 +62,45 @@ public static Set availableProperties() { return set; } - + + public static Flag get(NumericPropertyKeyword key) { + var flag = flags.stream().filter(f -> f.getType() == key).findAny(); + return flag.isPresent() ? flag.get() : null; + } + /** - * Finds what properties are being altered in the search - * - * @param t task for which the active parameters should be listed - * @return a {@code List} of property types represented by - * {@code NumericPropertyKeyword}s + * Creates a deep copy of the flags collection. + * @return a deep copy of the flags */ - public static List activeParameters(SearchTask t) { - var c = t.getCurrentCalculation(); - //problem dependent - var allActiveParams = selectActiveAndListed(flags, c.getProblem()); - //problem independent (lower/upper bound) - var listed = selectActiveAndListed(flags, t.getExperimentalCurve().getRange() ); - allActiveParams.addAll(listed); - return allActiveParams; + + public static List storeState() { + var copy = new ArrayList(); + for(Flag f : flags) { + copy.add(new Flag(f)); + } + return copy; + } + + /** + * Loads the argument into the current list of flags. + * This will update any matching flags and assign values correpon + * @param flags + */ + + public static void loadState(List flags) { + for(Flag f : ActiveFlags.flags) { + Optional existingFlag = flags.stream().filter(fl -> + fl.getType() == f.getType()).findFirst(); + if(existingFlag.isPresent()) { + f.setValue((boolean) existingFlag.get().getValue()); + } + } } public static List selectActiveAndListed(List flags, PropertyHolder listed) { //return empty list if(listed == null) { - return new ArrayList(); + return new ArrayList<>(); } return selectActiveTypes(flags).stream() diff --git a/src/main/java/pulse/search/direction/BFGSOptimiser.java b/src/main/java/pulse/search/direction/BFGSOptimiser.java index 239760e..273b936 100644 --- a/src/main/java/pulse/search/direction/BFGSOptimiser.java +++ b/src/main/java/pulse/search/direction/BFGSOptimiser.java @@ -6,7 +6,7 @@ import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.ui.Messages; /** @@ -53,7 +53,7 @@ private BFGSOptimiser() { * @throws SolverException */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { var p = (ComplexPath) task.getIterativeState(); Vector dir = p.getDirection(); //p[k] diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index da33ddd..e6fc8a3 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -3,6 +3,7 @@ import static pulse.math.linear.Matrices.createIdentityMatrix; import pulse.math.linear.SquareMatrix; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; /** @@ -18,7 +19,7 @@ public class ComplexPath extends GradientGuidedPath { private SquareMatrix hessian; private SquareMatrix inverseHessian; - protected ComplexPath(SearchTask task) { + protected ComplexPath(GeneralTask task) { super(task); } @@ -29,8 +30,8 @@ protected ComplexPath(SearchTask task) { * @param task */ @Override - public void configure(SearchTask task) { - hessian = createIdentityMatrix(ActiveFlags.activeParameters(task).size()); + public void configure(GeneralTask task) { + hessian = createIdentityMatrix(this.getParameters().dimension()); inverseHessian = createIdentityMatrix(hessian.getData().length); super.configure(task); } diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 3b78913..7976646 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -7,17 +7,17 @@ import pulse.math.ParameterVector; import static pulse.math.linear.Matrices.createIdentityMatrix; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_TIMEOUT; import pulse.properties.Property; +import pulse.search.GeneralTask; import pulse.search.linear.LinearOptimiser; import pulse.search.linear.WolfeOptimiser; -import pulse.tasks.SearchTask; -import pulse.tasks.logs.Status; import pulse.util.InstanceDescriptor; public abstract class CompositePathOptimiser extends GradientBasedOptimiser { private InstanceDescriptor instanceDescriptor - = new InstanceDescriptor( + = new InstanceDescriptor<>( "Linear Optimiser Selector", LinearOptimiser.class); private LinearOptimiser linearSolver; @@ -46,7 +46,7 @@ private void initLinearOptimiser() { } @Override - public boolean iteration(SearchTask task) throws SolverException { + public boolean iteration(GeneralTask task) throws SolverException { var p = (GradientGuidedPath) task.getIterativeState(); // the previous state of the task boolean accept = true; @@ -56,11 +56,11 @@ public boolean iteration(SearchTask task) throws SolverException { */ if (compare(p.getIteration(), getMaxIterations()) > 0) { - task.setStatus(Status.TIMEOUT); + throw new SolverException(OPTIMISATION_TIMEOUT); } else { - double initialCost = task.solveProblemAndCalculateCost(); + double initialCost = task.getResponse().objectiveFunction(task); var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -70,11 +70,15 @@ public boolean iteration(SearchTask task) throws SolverException { p.setLinearStep(step); // new set of parameters determined through search - var candidateParams = parameters.sum(dir.multiply(step)); + var candidateParams = parameters.toVector().sum(dir.multiply(step)); + var candidateVector = new ParameterVector(parameters, candidateParams); - task.assign(new ParameterVector(parameters, candidateParams)); // assign new parameters - - double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + if(candidateVector.findMalformedElements().isEmpty()) { + task.assign(candidateVector); // assign new parameters + } + + double newCost = task.getResponse().objectiveFunction(task); + // calculate the sum of squared residuals if (newCost > initialCost - EPS && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS @@ -134,8 +138,7 @@ public List listedTypes() { * @return a {@code Path} instance */ @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); + public GradientGuidedPath initState(GeneralTask t) { return new ComplexPath(t); } diff --git a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java index a3db7fc..abee584 100644 --- a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java +++ b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java @@ -2,7 +2,6 @@ import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperties.isDiscrete; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; @@ -14,15 +13,15 @@ import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; public abstract class GradientBasedOptimiser extends PathOptimiser { private double gradientResolution; private double gradientStep; - - private final static double resolutionHigh = (double)def(GRADIENT_RESOLUTION).getValue(); - private final static double resolutionLow = 5E-2; //TODO + + private final static double RESOLUTION_HIGH = (double) def(GRADIENT_RESOLUTION).getValue(); + private final static double RESOLUTION_LOW = 5E-2; //TODO /** * Abstract constructor that sets up the default @@ -34,6 +33,7 @@ public abstract class GradientBasedOptimiser extends PathOptimiser { */ protected GradientBasedOptimiser() { super(); + this.gradientResolution = gradientStep = RESOLUTION_HIGH; } /** @@ -44,9 +44,11 @@ protected GradientBasedOptimiser() { * * @see pulse.properties.Flag.defaultList() */ + @Override public void reset() { super.reset(); - gradientResolution = (double) def(GRADIENT_RESOLUTION).getValue(); + gradientResolution = RESOLUTION_HIGH; + gradientStep = gradientResolution; } /** @@ -74,26 +76,29 @@ public void reset() { * @return the gradient of the target function * @throws SolverException */ - public Vector gradient(SearchTask task) throws SolverException { + public Vector gradient(GeneralTask task) throws SolverException { final var params = task.searchVector(); + final var pVector = params.toVector(); var grad = new Vector(params.dimension()); - - for (int i = 0; i < params.dimension(); i++) { - NumericProperty defProp = NumericProperties.def(params.getIndex(i)); - double dx = dx(defProp, params.get(i)); + final var ps = params.getParameters(); + + for (int i = 0, size = params.dimension(); i < size; i++) { + var key = ps.get(i).getIdentifier().getKeyword(); + var defProp = key != null ? NumericProperties.def(key) : null; + double dx = dx(defProp, ps.get(i).inverseTransform()); final var shift = new Vector(params.dimension()); shift.set(i, 0.5 * dx); - task.assign(new ParameterVector(params, params.sum(shift))); - final double ss2 = task.solveProblemAndCalculateCost(); + var shiftVector = new ParameterVector(params, pVector.sum(shift)); + task.assign(shiftVector); + final double ss2 = task.objectiveFunction(); - task.assign(new ParameterVector(params, params.subtract(shift))); - final double ss1 = task.solveProblemAndCalculateCost(); + task.assign(new ParameterVector(params, pVector.subtract(shift))); + final double ss1 = task.objectiveFunction(); grad.set(i, (ss2 - ss1) / dx); - } task.assign(params); @@ -101,35 +106,29 @@ public Vector gradient(SearchTask task) throws SolverException { return grad; } - + /** - * Calculates the gradient step. Ensures dx is not zero even if the parameter values is. - * Applicable to discrete properties. - * @param defProp the default property + * Calculates the gradient step. Ensures dx is not zero even if the + * parameter values is. Applicable to discrete properties. + * + * @param defProp the default property * @param value the value of the parameter under the optimisation vector * @return the gradient step */ - protected double dx(NumericProperty defProp, double value) { - boolean discrete = defProp.isDiscrete(); - return (discrete ? resolutionLow : resolutionHigh) - * (Math.abs(value) < 1E-20 - ? defProp.getMaximum().doubleValue() - : value); - } + double result; + + if (defProp == null) { + result = gradientResolution * (Math.abs(value) < 1E-20 ? 0.01 : value); + } else { + boolean discrete = defProp.isDiscrete(); + result = (discrete ? RESOLUTION_LOW : gradientResolution) + * (Math.abs(value) < 1E-20 + ? defProp.getMaximum().doubleValue() + : value); + } - /** - * Checks whether a discrete property is being optimised and selects the - * gradient step best suited to the optimisation strategy. Should be called - * before creating the optimisation path. - * - * @param task the search task defining the search vector - */ - public void configure(SearchTask task) { - var params = task.searchVector(); - boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); - final double dxGrid = task.getCurrentCalculation().getScheme().getGrid().getXStep(); - gradientStep = discreteGradient ? dxGrid : (double) getGradientResolution().getValue(); + return result; } public void setGradientResolution(NumericProperty resolution) { diff --git a/src/main/java/pulse/search/direction/GradientGuidedPath.java b/src/main/java/pulse/search/direction/GradientGuidedPath.java index 9dcab90..7b9a06c 100644 --- a/src/main/java/pulse/search/direction/GradientGuidedPath.java +++ b/src/main/java/pulse/search/direction/GradientGuidedPath.java @@ -4,6 +4,8 @@ import java.util.logging.Logger; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_ERROR; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.tasks.logs.Status; @@ -32,7 +34,8 @@ public class GradientGuidedPath extends IterativeState { private Vector gradient; private double minimumPoint; - protected GradientGuidedPath(SearchTask t) { + protected GradientGuidedPath(GeneralTask t) { + super(t); configure(t); } @@ -41,15 +44,15 @@ protected GradientGuidedPath(SearchTask t) { * direction of search.Sets the minimum point to 0.0. * * @param t the {@code SearchTask}, for which this {@code Path} is created. - * @throws pulse.problem.schemes.solvers.SolverException * @see pulse.search.direction.PathSolver.direction(Path) */ - public void configure(SearchTask t) { + public void configure(GeneralTask t) { super.reset(); try { this.gradient = ((GradientBasedOptimiser) PathOptimiser.getInstance()).gradient(t); } catch (SolverException ex) { - t.notifyFailedStatus(ex); + t.onSolverException( new SolverException("Gradient calculation error", OPTIMISATION_ERROR)); + ex.printStackTrace(); } minimumPoint = 0.0; } diff --git a/src/main/java/pulse/search/direction/HessianDirectionSolver.java b/src/main/java/pulse/search/direction/HessianDirectionSolver.java index 15b9f9a..7aee842 100644 --- a/src/main/java/pulse/search/direction/HessianDirectionSolver.java +++ b/src/main/java/pulse/search/direction/HessianDirectionSolver.java @@ -5,6 +5,7 @@ import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_ERROR; public interface HessianDirectionSolver extends DirectionSolver { @@ -38,7 +39,7 @@ public static Vector solve(ComplexPath cp, Vector rhs) throws SolverException { var dirv = new DMatrixRMaj(dimg, 1); if (!CommonOps_DDRM.solve(hess, antigrad, dirv)) { - throw new SolverException("Singular matrix!"); + throw new SolverException("Singular matrix!", OPTIMISATION_ERROR); } result = new Vector(dirv.getData()); diff --git a/src/main/java/pulse/search/direction/IterativeState.java b/src/main/java/pulse/search/direction/IterativeState.java index 1db8ba2..05fbaca 100644 --- a/src/main/java/pulse/search/direction/IterativeState.java +++ b/src/main/java/pulse/search/direction/IterativeState.java @@ -5,6 +5,7 @@ import static pulse.properties.NumericPropertyKeyword.ITERATION; import pulse.properties.NumericProperty; +import pulse.search.GeneralTask; public class IterativeState { @@ -23,6 +24,10 @@ public IterativeState(IterativeState other) { this.cost = other.cost; } + public IterativeState(GeneralTask t) { + this.parameters = t.searchVector(); + } + //default constructor public IterativeState() {} diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 80bd5e5..4c3247a 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -16,15 +16,16 @@ import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.ILLEGAL_PARAMETERS; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_ERROR; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.OPTIMISATION_TIMEOUT; import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import static pulse.search.direction.CompositePathOptimiser.EPS; import pulse.search.statistics.OptimiserStatistic; -import pulse.search.statistics.ResidualStatistic; import pulse.search.statistics.SumOfSquares; -import pulse.tasks.SearchTask; -import pulse.tasks.logs.Status; import pulse.ui.Messages; /** @@ -35,7 +36,7 @@ */ public class LMOptimiser extends GradientBasedOptimiser { - private static LMOptimiser instance = new LMOptimiser(); + private static final LMOptimiser instance = new LMOptimiser(); private double dampingRatio; /** @@ -53,7 +54,7 @@ private LMOptimiser() { } @Override - public boolean iteration(SearchTask task) throws SolverException { + public boolean iteration(GeneralTask task) throws SolverException { var p = (LMPath) task.getIterativeState(); // the previous path of the task boolean accept = true; //accept the step by default @@ -63,11 +64,11 @@ public boolean iteration(SearchTask task) throws SolverException { */ if (compare(p.getIteration(), getMaxIterations()) > 0) { - task.setStatus(Status.TIMEOUT); + throw new SolverException(OPTIMISATION_TIMEOUT); } else { - double initialCost = task.solveProblemAndCalculateCost(); + double initialCost = task.objectiveFunction(); var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -76,16 +77,17 @@ public boolean iteration(SearchTask task) throws SolverException { var lmDirection = getSolver().direction(p); - var candidate = parameters.sum(lmDirection); + var candidate = parameters.toVector().sum(lmDirection); if( Arrays.stream( candidate.getData() ).anyMatch(el -> !Double.isFinite(el) ) ) { - throw new SolverException("Illegal candidate parameters: not finite! " + p.getIteration()); + throw new SolverException("Illegal candidate parameters: not finite! " + + p.getIteration(), ILLEGAL_PARAMETERS); } task.assign(new ParameterVector( parameters, candidate)); // assign new parameters - double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals + double newCost = task.objectiveFunction(); // calculate the sum of squared residuals /* * Delayed gratification @@ -115,11 +117,12 @@ public boolean iteration(SearchTask task) throws SolverException { * Hessian matrix. */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { var p = (LMPath) task.getIterativeState(); + var rs = task.getResponse().getOptimiserStatistic(); //store residual vector at current parameters - p.setResidualVector(new Vector(residualVector(task.getCurrentCalculation().getOptimiserStatistic()))); + p.setResidualVector(new Vector(rs.residualsArray())); // Calculate the Jacobian -- if needed if (p.isComputeJacobian()) { @@ -132,7 +135,8 @@ public void prepare(SearchTask task) throws SolverException { p.setGradient(g1); if(Arrays.stream(g1.getData()).anyMatch(v -> !Double.isFinite(v))) { - throw new SolverException("Could not calculate objective function gradient"); + throw new SolverException("Could not calculate objective function gradient", + OPTIMISATION_ERROR); } // the Hessian is then regularised by adding labmda*I @@ -166,39 +170,51 @@ public void prepare(SearchTask task) throws SolverException { * @throws SolverException * @see pulse.search.statistics.ResidualStatistic.calculateResiduals() */ - public RectangularMatrix jacobian(SearchTask task) throws SolverException { + public RectangularMatrix jacobian(GeneralTask task) throws SolverException { - var residualCalculator = task.getCurrentCalculation().getOptimiserStatistic(); + var residualCalculator = task.getResponse().getOptimiserStatistic(); var p = ((LMPath) task.getIterativeState()); final var params = p.getParameters(); + final var pVector = params.toVector(); final int numPoints = p.getResidualVector().dimension(); final int numParams = params.dimension(); var jacobian = new double[numPoints][numParams]; - + var ps = params.getParameters(); + for (int i = 0; i < numParams; i++) { - double dx = dx( NumericProperties.def(params.getIndex(i)), params.get(i)); + var key = ps.get(i).getIdentifier().getKeyword(); + double dx = dx( + key != null ? NumericProperties.def(key) : null, + ps.get(i).inverseTransform()); final var shift = new Vector(numParams); shift.set(i, 0.5 * dx); // + shift - task.assign(new ParameterVector(params, params.sum(shift))); - task.solveProblemAndCalculateCost(); - var r1 = residualVector(residualCalculator); + task.assign(new ParameterVector(params, pVector.sum(shift))); + task.objectiveFunction(); + var r = residualCalculator.getResiduals(); + + for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); + j < realNumPoints; j++) { + jacobian[j][i] = r.get(j) / dx; + + } + // - shift - task.assign(new ParameterVector(params, params.subtract(shift))); - task.solveProblemAndCalculateCost(); - var r2 = residualVector(residualCalculator); + task.assign(new ParameterVector(params, pVector.subtract(shift))); + task.objectiveFunction(); - for (int j = 0, realNumPoints = Math.min(numPoints, r2.length); j < realNumPoints; j++) { + for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); + j < realNumPoints; j++) { - jacobian[j][i] = (r1[j] - r2[j]) / dx; + jacobian[j][i] -= r.get(j) / dx; } @@ -210,14 +226,9 @@ public RectangularMatrix jacobian(SearchTask task) throws SolverException { return Matrices.createMatrix(jacobian); } - - private static double[] residualVector(ResidualStatistic rs) { - return rs.getResiduals().stream().mapToDouble(array -> array[1]).toArray(); - } - + @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); + public GradientGuidedPath initState(GeneralTask t) { return new LMPath(t); } diff --git a/src/main/java/pulse/search/direction/LMPath.java b/src/main/java/pulse/search/direction/LMPath.java index 3f006e1..2528968 100644 --- a/src/main/java/pulse/search/direction/LMPath.java +++ b/src/main/java/pulse/search/direction/LMPath.java @@ -3,7 +3,7 @@ import pulse.math.linear.RectangularMatrix; import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; class LMPath extends ComplexPath { @@ -13,12 +13,12 @@ class LMPath extends ComplexPath { private double lambda; private boolean computeJacobian; - public LMPath(SearchTask t) { + public LMPath(GeneralTask t) { super(t); } @Override - public void configure(SearchTask t) { + public void configure(GeneralTask t) { super.configure(t); this.lambda = 1.0; computeJacobian = true; diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index d357115..cb5d4fa 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -14,8 +14,8 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; +import pulse.search.GeneralTask; import pulse.search.statistics.OptimiserStatistic; -import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -93,15 +93,15 @@ public void reset() { * @see direction(Path) * @see pulse.search.linear.LinearOptimiser */ - public abstract boolean iteration(SearchTask task) throws SolverException; - + public abstract boolean iteration(GeneralTask task) throws SolverException; + /** * Defines a set of procedures to be run at the end of the search iteration. * * @param task the {@code SearchTask} undergoing optimisation * @throws SolverException */ - public abstract void prepare(SearchTask task) throws SolverException; + public abstract void prepare(GeneralTask task) throws SolverException; public NumericProperty getErrorTolerance() { return derive(ERROR_TOLERANCE, errorTolerance); @@ -232,6 +232,6 @@ public boolean compatibleWith(OptimiserStatistic os) { * @param t the task, the optimisation path of which will be tracked * @return a {@code Path} instance */ - public abstract IterativeState initState(SearchTask t); + public abstract IterativeState initState(GeneralTask t); } diff --git a/src/main/java/pulse/search/direction/SR1Optimiser.java b/src/main/java/pulse/search/direction/SR1Optimiser.java index bf89b07..8b2f708 100644 --- a/src/main/java/pulse/search/direction/SR1Optimiser.java +++ b/src/main/java/pulse/search/direction/SR1Optimiser.java @@ -7,6 +7,7 @@ import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -35,7 +36,7 @@ private SR1Optimiser() { * @throws SolverException */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { var p = (ComplexPath) task.getIterativeState(); Vector dir = p.getDirection(); diff --git a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java index 390437f..8fcbc31 100644 --- a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java +++ b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java @@ -2,7 +2,7 @@ import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.ui.Messages; /** @@ -36,7 +36,7 @@ private SteepestDescentOptimiser() { * @throws SolverException */ @Override - public void prepare(SearchTask task) throws SolverException { + public void prepare(GeneralTask task) throws SolverException { ((GradientGuidedPath) task.getIterativeState()).setGradient(gradient(task)); } @@ -63,9 +63,8 @@ public static SteepestDescentOptimiser getInstance() { * @return a {@code Path} instance */ @Override - public GradientGuidedPath initState(SearchTask t) { - this.configure(t); + public GradientGuidedPath initState(GeneralTask t) { return new GradientGuidedPath(t); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/ConstrictionMover.java b/src/main/java/pulse/search/direction/pso/ConstrictionMover.java new file mode 100644 index 0000000..f19d824 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/ConstrictionMover.java @@ -0,0 +1,49 @@ +package pulse.search.direction.pso; + +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; + +public class ConstrictionMover implements Mover { + + private double c1; //social + private double c2; //cognitive + private double chi; + public final static double DEFAULT_CHI = 0.7298; + public final static double DEFAULT_C = 1.49618; + + public ConstrictionMover() { + chi = DEFAULT_CHI; + c1 = c2 = DEFAULT_C; + } + + @Override + public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest) { + var current = p.getCurrentState(); + var curPos = current.getPosition(); + var curPosV = curPos.toVector(); + + final int n = curPos.dimension(); + Vector nsum = new Vector(n); + + var localBest = p.getBestState().getPosition(); //best position by local particle + var localBestV = localBest.toVector(); + var globalBest = gBest.getPosition(); //best position by any particle + var globalBestV = globalBest.toVector(); + + nsum = nsum.sum(Vector.random(n, 0.0, c1) + .multComponents(localBestV.subtract(curPosV)) + ); + + nsum = nsum.sum(Vector.random(n, 0.0, c2) + .multComponents(globalBestV.subtract(curPosV)) + ); + + var newVelocity = (current.getVelocity().toVector().sum(nsum)).multiply(chi); + var newPosition = curPosV.sum(newVelocity); + + return new ParticleState( + new ParameterVector(curPos, newPosition), + new ParameterVector(curPos, newVelocity)); + } + +} diff --git a/src/main/java/pulse/search/direction/pso/FIPSMover.java b/src/main/java/pulse/search/direction/pso/FIPSMover.java index ab4ca1f..b6869ec 100644 --- a/src/main/java/pulse/search/direction/pso/FIPSMover.java +++ b/src/main/java/pulse/search/direction/pso/FIPSMover.java @@ -16,29 +16,30 @@ public FIPSMover() { } @Override - public ParticleState attemptMove(Particle p, Particle[] neighbours) { + public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest) { var current = p.getCurrentState(); + var curPos = current.getPosition(); + var curPosV = curPos.toVector(); + + final int n = curPos.dimension(); + final double nLength = (double) neighbours.length; - var pos = current.getPosition(); - - final int n = pos.dimension(); - var nsum = new Vector(n); + Vector nsum = new Vector(n); for (var neighbour : neighbours) { - var nPos = neighbour.getCurrentState().getPosition(); - nsum = nsum.sum(Vector.random(n, 0.0, phi).multComponents(nPos.subtract(pos))); + var nBestPos = neighbour.getBestState().getPosition(); //best position ever achieved so far by the neighbour + nsum = nsum.sum(Vector.random(n, 0.0, phi/nLength) + .multComponents(nBestPos.toVector().subtract(curPosV)) + ); } - nsum = nsum.multiply(1.0 / ((double) neighbours.length)); - - var newVelocity = (current.getVelocity().sum(nsum)).multiply(chi); - var newPosition = pos.sum(newVelocity); - System.out.println(newPosition); - + var newVelocity = (current.getVelocity().toVector().sum(nsum)).multiply(chi); + var newPosition = curPosV.sum(newVelocity); + return new ParticleState( - new ParameterVector(pos, newPosition), - new ParameterVector(pos, newVelocity)); + new ParameterVector(curPos, newPosition), + new ParameterVector(curPos, newVelocity)); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/Mover.java b/src/main/java/pulse/search/direction/pso/Mover.java index 6dd7387..d1db1d0 100644 --- a/src/main/java/pulse/search/direction/pso/Mover.java +++ b/src/main/java/pulse/search/direction/pso/Mover.java @@ -2,6 +2,6 @@ public interface Mover { - public ParticleState attemptMove(Particle p, Particle[] neighbours); + public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/pso/Particle.java b/src/main/java/pulse/search/direction/pso/Particle.java index e538450..d5031db 100644 --- a/src/main/java/pulse/search/direction/pso/Particle.java +++ b/src/main/java/pulse/search/direction/pso/Particle.java @@ -19,6 +19,7 @@ package pulse.search.direction.pso; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; /** @@ -43,10 +44,10 @@ public void adopt(ParticleState state) { this.current = state; } - public void evaluate(SearchTask t) throws SolverException { + public void evaluate(GeneralTask t) throws SolverException { var params = t.searchVector(); t.assign(current.getPosition()); - current.setFitness(t.solveProblemAndCalculateCost()); + current.setFitness(t.objectiveFunction()); t.assign(params); if (current.isBetterThan(pbest)) { diff --git a/src/main/java/pulse/search/direction/pso/ParticleState.java b/src/main/java/pulse/search/direction/pso/ParticleState.java index e5fcdc0..68f68c1 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleState.java +++ b/src/main/java/pulse/search/direction/pso/ParticleState.java @@ -1,6 +1,7 @@ package pulse.search.direction.pso; import pulse.math.ParameterVector; +import pulse.math.linear.Vector; public class ParticleState { @@ -13,10 +14,8 @@ public ParticleState(ParameterVector cur) { this.velocity = new ParameterVector(cur); //set initial velocity to zero - for (int i = 0, n = velocity.dimension(); i < n; i++) { - velocity.set(i, 0.0); - } - + velocity.setValues(new Vector(cur.dimension())); + this.fitness = Double.MAX_VALUE; } @@ -35,22 +34,16 @@ public boolean isBetterThan(ParticleState s) { return this.fitness < s.fitness; } - public void randomise(ParameterVector pos) { - - this.position = new ParameterVector(pos); - - for (int i = 0, n = position.dimension(); i < n; i++) { - - var bounds = position.getBounds(); - - double max = bounds[i].getMaximum(); - double min = bounds[i].getMinimum(); - - double value = min + Math.random() * (max - min); - position.set(i, value); - - } + public final void randomise(ParameterVector pos) { + double[] randomValues = pos.getParameters().stream().mapToDouble(p -> { + double min = p.getBounds().getMinimum(); + double max = p.getBounds().getMaximum(); + return min + Math.random() * (max - min); + }).toArray(); + + Vector randomVector = new Vector(randomValues); + position.setValues(randomVector); } public ParameterVector getPosition() { diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java index badabe9..6f39f8b 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -1,58 +1,72 @@ package pulse.search.direction.pso; -public class ParticleSwarmOptimiser //extends PathOptimiser -{ +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; +import pulse.search.direction.IterativeState; +import pulse.search.direction.PathOptimiser; +import pulse.search.statistics.OptimiserStatistic; + +public class ParticleSwarmOptimiser extends PathOptimiser { private SwarmState swarmState; private Mover mover; public ParticleSwarmOptimiser() { swarmState = new SwarmState(); - mover = new FIPSMover(); + mover = new ConstrictionMover(); } protected void moveParticles() { var topology = swarmState.getNeighborhoodTopology(); for (var p : swarmState.getParticles()) { - p.adopt(mover.attemptMove(p, topology.neighbours(p, swarmState))); + p.adopt(mover.attemptMove(p, + topology.neighbours(p, swarmState), + swarmState.getBestSoFar())); + var data = p.getCurrentState().getPosition().toVector().getData(); + StringBuilder sb = new StringBuilder().append(p.getId()).append(" "); + for(var d : data) { + sb.append(d).append(" "); + } + System.err.println(sb.toString()); } } /** * Iterates the swarm. * - * @param max_iterations max number of iterations to be computed by the - * swarm. */ + @Override + public boolean iteration(GeneralTask task) throws SolverException { + this.prepare(task); - /* - @Override - public boolean iteration(SearchTask task) throws SolverException { - this.prepare(task); - - swarmState.evaluate(task); - moveParticles(); - - swarmState.incrementStep(); - - task.assign( swarmState.bestSoFar().getPosition() ); - task.solveProblemAndCalculateCost(); - - return true; - } - */ + swarmState.evaluate(task); + swarmState.bestSoFar(); + moveParticles(); + + swarmState.incrementStep(); + + task.assign(swarmState.getBestSoFar().getPosition()); + task.objectiveFunction(); + + return true; + } + + @Override + public void prepare(GeneralTask task) throws SolverException { + swarmState.prepare(task); + } + + @Override + public IterativeState initState(GeneralTask t) { + swarmState.prepare(t); + swarmState.create(); + return swarmState; + } + + //TODO + @Override + public boolean compatibleWith(OptimiserStatistic os) { + return false; + } - /* - @Override - public void prepare(SearchTask task) throws SolverException { - swarmState.prepare(task); - } - - @Override - public IterativeState initState(SearchTask t) { - swarmState.prepare(t); - swarmState.create(); - return swarmState; - } - */ } diff --git a/src/main/java/pulse/search/direction/pso/StaticTopologies.java b/src/main/java/pulse/search/direction/pso/StaticTopologies.java index 99f31bd..8fa5c24 100644 --- a/src/main/java/pulse/search/direction/pso/StaticTopologies.java +++ b/src/main/java/pulse/search/direction/pso/StaticTopologies.java @@ -32,7 +32,7 @@ public class StaticTopologies { final int latticeParameter = (int) Math.sqrt(ps.length); - final int row = i % latticeParameter; + final int row = i / latticeParameter; final int column = i - row * latticeParameter; final int above = column + (row > 0 diff --git a/src/main/java/pulse/search/direction/pso/SwarmState.java b/src/main/java/pulse/search/direction/pso/SwarmState.java index 4a2f424..7baa955 100644 --- a/src/main/java/pulse/search/direction/pso/SwarmState.java +++ b/src/main/java/pulse/search/direction/pso/SwarmState.java @@ -2,8 +2,8 @@ import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.search.direction.IterativeState; -import pulse.tasks.SearchTask; public class SwarmState extends IterativeState { @@ -12,13 +12,13 @@ public class SwarmState extends IterativeState { private Particle[] particles; private NeighbourhoodTopology neighborhoodTopology; - private Particle bestSoFar; + private ParticleState bestSoFar; private int bestSoFarIndex; private final static int DEFAULT_PARTICLES = 16; public SwarmState() { - this(DEFAULT_PARTICLES, StaticTopologies.GLOBAL); + this(DEFAULT_PARTICLES, StaticTopologies.RING); } public SwarmState(int numberOfParticles, NeighbourhoodTopology neighborhoodTopology) { @@ -28,13 +28,13 @@ public SwarmState(int numberOfParticles, NeighbourhoodTopology neighborhoodTopol this.bestSoFarIndex = -1; } - public void evaluate(SearchTask t) throws SolverException { - for (var p : particles) { + public void evaluate(GeneralTask t) throws SolverException { + for (Particle p : particles) { p.evaluate(t); } } - public void prepare(SearchTask t) { + public void prepare(GeneralTask t) { seed = t.searchVector(); } @@ -47,9 +47,8 @@ public void create() { /** * Returns the best state achieved by any particle so far. * - * @return State object. */ - public ParticleState bestSoFar() { + public void bestSoFar() { int bestIndex = 0; double fitness = 0; @@ -65,11 +64,16 @@ public ParticleState bestSoFar() { } } - - this.bestSoFar = particles[bestIndex]; - this.bestSoFarIndex = bestIndex; - - return bestSoFar.getBestState(); + + //determine the current best + ParticleState curBest = particles[bestIndex].getCurrentState(); + + //is curBest the best so far? + if(bestSoFar == null || curBest.isBetterThan(bestSoFar) ) { + this.bestSoFar = curBest; + this.bestSoFarIndex = bestIndex; + } + } public NeighbourhoodTopology getNeighborhoodTopology() { @@ -93,11 +97,11 @@ public void setParticles(Particle[] particles) { this.particles = particles; } - public Particle getBestSoFar() { + public ParticleState getBestSoFar() { return bestSoFar; } - public void setBestSoFar(Particle bestSoFar) { + public void setBestSoFar(ParticleState bestSoFar) { this.bestSoFar = bestSoFar; } @@ -109,4 +113,4 @@ public void setBestSoFarIndex(int bestSoFarIndex) { this.bestSoFarIndex = bestSoFarIndex; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java index a4229d2..4c9f1be 100644 --- a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java +++ b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java @@ -3,6 +3,7 @@ import pulse.math.ParameterVector; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.search.direction.GradientGuidedPath; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -46,11 +47,12 @@ private GoldenSectionOptimiser() { * @throws SolverException */ @Override - public double linearStep(SearchTask task) throws SolverException { + public double linearStep(GeneralTask task) throws SolverException { final double EPS = 1e-14; final var params = task.searchVector(); + var vParams = params.toVector(); final Vector direction = ((GradientGuidedPath) task.getIterativeState()).getDirection(); var segment = domain(params, direction); @@ -61,13 +63,13 @@ public double linearStep(SearchTask task) throws SolverException { final double alpha = segment.getMinimum() + t; final double one_minus_alpha = segment.getMaximum() - t; - final var newParams1 = params.sum(direction.multiply(alpha)); // alpha + final var newParams1 = vParams.sum(direction.multiply(alpha)); // alpha task.assign(new ParameterVector(params, newParams1)); - final double ss2 = task.solveProblemAndCalculateCost(); // f(alpha) + final double ss2 = task.objectiveFunction(); // f(alpha) - final var newParams2 = params.sum(direction.multiply(one_minus_alpha)); // 1 - alpha + final var newParams2 = vParams.sum(direction.multiply(one_minus_alpha)); // 1 - alpha task.assign(new ParameterVector(params, newParams2)); - final double ss1 = task.solveProblemAndCalculateCost(); // f(1-alpha) + final double ss1 = task.objectiveFunction(); // f(1-alpha) task.assign(new ParameterVector(params, newParams2)); // return to old position diff --git a/src/main/java/pulse/search/linear/LinearOptimiser.java b/src/main/java/pulse/search/linear/LinearOptimiser.java index 891b42b..a82b918 100644 --- a/src/main/java/pulse/search/linear/LinearOptimiser.java +++ b/src/main/java/pulse/search/linear/LinearOptimiser.java @@ -6,6 +6,7 @@ import static pulse.properties.NumericPropertyKeyword.LINEAR_RESOLUTION; import java.util.Set; +import pulse.math.Parameter; import pulse.math.ParameterVector; import pulse.math.Segment; @@ -13,6 +14,7 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -45,7 +47,7 @@ protected LinearOptimiser() { * to a lower SSR value of this {@code task} * @throws SolverException */ - public abstract double linearStep(SearchTask task) throws SolverException; + public abstract double linearStep(GeneralTask task) throws SolverException; /** * Sets the domain for this linear search on {@code p}. @@ -68,29 +70,32 @@ public static Segment domain(ParameterVector x, Vector p) { double alphaMax = Double.POSITIVE_INFINITY; double alpha; - for (int i = 0; i < x.dimension(); i++) { + var params = x.getParameters(); - final double component = p.get(i); + for (Parameter xp : params) { + + final double component = p.get(params.indexOf(xp)); //check if zero - if (component < EPS && component > -EPS) { - continue; - } + if (Math.abs(component) > EPS) { - var bound = x.getTransformedBounds(i); + var bound = xp.getTransformedBounds(); - alpha = abs( - ((component > 0 ? bound.getMaximum() : bound.getMinimum()) - x.get(i)) - / component); + alpha = abs( + ((component > 0 ? bound.getMaximum() + : bound.getMinimum()) - xp.inverseTransform()) + / component); + + if (Double.isFinite(alpha) && alpha < alphaMax) { + alphaMax = alpha; + } - if (Double.isFinite(alpha) && alpha < alphaMax) { - alphaMax = alpha; } } //check that alphaMax is not zero! otherwise the optimise will crash - return new Segment(0.0, + return new Segment(0.0, Math.max(alphaMax, 1E-10)); } @@ -135,7 +140,7 @@ public Set listedKeywords() { set.add(LINEAR_RESOLUTION); return set; } - + @Override public void set(NumericPropertyKeyword type, NumericProperty property) { if (type == LINEAR_RESOLUTION) { diff --git a/src/main/java/pulse/search/linear/WolfeOptimiser.java b/src/main/java/pulse/search/linear/WolfeOptimiser.java index 751d7d6..af3a2cd 100644 --- a/src/main/java/pulse/search/linear/WolfeOptimiser.java +++ b/src/main/java/pulse/search/linear/WolfeOptimiser.java @@ -6,6 +6,7 @@ import pulse.math.Segment; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; import pulse.search.direction.GradientBasedOptimiser; import pulse.search.direction.GradientGuidedPath; import pulse.search.direction.PathOptimiser; @@ -64,7 +65,7 @@ private WolfeOptimiser() { * @throws SolverException */ @Override - public double linearStep(SearchTask task) throws SolverException { + public double linearStep(GeneralTask task) throws SolverException { GradientGuidedPath p = (GradientGuidedPath) task.getIterativeState(); @@ -75,9 +76,10 @@ public double linearStep(SearchTask task) throws SolverException { final double G1P_ABS = abs(G1P); var params = task.searchVector(); + var vParams = params.toVector(); Segment segment = domain(params, direction); - double cost1 = task.solveProblemAndCalculateCost(); + double cost1 = task.objectiveFunction(); double randomConfinedValue = 0; double g2p; @@ -88,11 +90,11 @@ public double linearStep(SearchTask task) throws SolverException { randomConfinedValue = segment.randomValue(); - final var newParams = params.sum(direction.multiply(randomConfinedValue)); + final var newParams = vParams.sum(direction.multiply(randomConfinedValue)); task.assign(new ParameterVector(params, newParams)); - final double cost2 = task.solveProblemAndCalculateCost(); + final double cost2 = task.objectiveFunction(); /** * Checks if the first Armijo inequality is not satisfied. In this diff --git a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java index 948adfe..7113c14 100644 --- a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java +++ b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java @@ -1,10 +1,9 @@ package pulse.search.statistics; -import static java.lang.Math.abs; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; +import pulse.search.GeneralTask; -import pulse.tasks.SearchTask; /** * A statistical optimality criterion relying on absolute deviations or the L1 @@ -26,11 +25,13 @@ public AbsoluteDeviations(AbsoluteDeviations another) { /** * Calculates the L1 norm statistic, which simply sums up the absolute * values of residuals. + * @param t */ @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { calculateResiduals(t); - final double statistic = getResiduals().stream().map(r -> abs(r[1])).reduce(Double::sum).get() / getResiduals().size(); + final double statistic = getResiduals().stream() + .mapToDouble(a -> Math.abs(a)).average().getAsDouble(); setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } diff --git a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java index 99ac004..be69593 100644 --- a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java +++ b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java @@ -4,8 +4,8 @@ import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; +import pulse.search.GeneralTask; -import pulse.tasks.SearchTask; import umontreal.ssj.gof.GofStat; import umontreal.ssj.probdist.NormalDist; @@ -21,12 +21,14 @@ public class AndersonDarlingTest extends NormalityTest { * test with the input parameters formed by the {@code task} residuals and a * normal distribution with zero mean and variance equal to the residuals * variance. + * @param task + * @return */ @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { calculateResiduals(task); - double[] residuals = super.transformResiduals(); + double[] residuals = residualsArray(); var nd = new NormalDist(0.0, (new StandardDeviation()).evaluate(residuals)); var testResult = GofStat.andersonDarling(residuals, nd); @@ -42,7 +44,7 @@ public String getDescriptor() { } @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { test(t); } diff --git a/src/main/java/pulse/search/statistics/CorrelationTest.java b/src/main/java/pulse/search/statistics/CorrelationTest.java index c977544..670d7e5 100644 --- a/src/main/java/pulse/search/statistics/CorrelationTest.java +++ b/src/main/java/pulse/search/statistics/CorrelationTest.java @@ -20,7 +20,7 @@ public abstract class CorrelationTest extends PropertyHolder implements Reflexiv "Correlation Test Selector", CorrelationTest.class); static { - instanceDescriptor.setSelectedDescriptor(PearsonCorrelation.class.getSimpleName()); + instanceDescriptor.setSelectedDescriptor(EmptyCorrelationTest.class.getSimpleName()); } public CorrelationTest() { diff --git a/src/main/java/pulse/search/statistics/EmptyTest.java b/src/main/java/pulse/search/statistics/EmptyTest.java index f492501..573280c 100644 --- a/src/main/java/pulse/search/statistics/EmptyTest.java +++ b/src/main/java/pulse/search/statistics/EmptyTest.java @@ -1,6 +1,6 @@ package pulse.search.statistics; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; public class EmptyTest extends NormalityTest { @@ -8,7 +8,7 @@ public class EmptyTest extends NormalityTest { * Always returns true */ @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { return true; } @@ -18,7 +18,7 @@ public String getDescriptor() { } @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { // deliberately empty } diff --git a/src/main/java/pulse/search/statistics/FTest.java b/src/main/java/pulse/search/statistics/FTest.java index 672bdc1..9a39bc4 100644 --- a/src/main/java/pulse/search/statistics/FTest.java +++ b/src/main/java/pulse/search/statistics/FTest.java @@ -1,18 +1,3 @@ -/* - * Copyright 2021 Artem Lunev . - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package pulse.search.statistics; import org.apache.commons.math3.distribution.FDistribution; @@ -21,7 +6,6 @@ /** * A static class for testing two calculations based on the Fischer test (F-Test) * implemented in Apache Commons Math. - * @author Artem Lunev */ public class FTest { diff --git a/src/main/java/pulse/search/statistics/KSTest.java b/src/main/java/pulse/search/statistics/KSTest.java index ceb4ceb..7d5a854 100644 --- a/src/main/java/pulse/search/statistics/KSTest.java +++ b/src/main/java/pulse/search/statistics/KSTest.java @@ -6,8 +6,7 @@ import org.apache.commons.math3.distribution.NormalDistribution; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; import org.apache.commons.math3.stat.inference.TestUtils; - -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * The Kolmogorov-Smirnov normality test as implemented in @@ -20,7 +19,7 @@ public class KSTest extends NormalityTest { private NormalDistribution nd; @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { evaluate(task); this.setStatistic(derive(TEST_STATISTIC, @@ -29,9 +28,9 @@ public boolean test(SearchTask task) { } @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { calculateResiduals(t); - residuals = transformResiduals(); + residuals = residualsArray(); final double sd = (new StandardDeviation()).evaluate(residuals); nd = new NormalDistribution(0.0, sd); // null hypothesis: normal distribution with zero mean and empirical diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index 324acc5..fbba1cc 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -12,7 +12,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.util.PropertyEvent; /** @@ -38,8 +38,8 @@ public ModelSelectionCriterion(ModelSelectionCriterion another) { } @Override - public void evaluate(SearchTask t) { - kq = t.alteredParameters().size(); //number of parameters + public void evaluate(GeneralTask t) { + kq = t.searchVector().dimension(); //number of parameters calcCriterion(); } @@ -104,11 +104,11 @@ public OptimiserStatistic getOptimiserStatistic() { return os; } - public void setOptimiserStatistic(OptimiserStatistic os) { + public final void setOptimiserStatistic(OptimiserStatistic os) { this.os = os; } - public void setStatistic(NumericProperty p) { + public final void setStatistic(NumericProperty p) { requireType(p, MODEL_CRITERION); this.criterion = (double) p.getValue(); } diff --git a/src/main/java/pulse/search/statistics/NormalityTest.java b/src/main/java/pulse/search/statistics/NormalityTest.java index a8de54c..f383a83 100644 --- a/src/main/java/pulse/search/statistics/NormalityTest.java +++ b/src/main/java/pulse/search/statistics/NormalityTest.java @@ -8,6 +8,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; /** @@ -44,7 +45,7 @@ public static void setStatisticalSignificance(NumericProperty alpha) { NormalityTest.significance = (double) alpha.getValue(); } - public abstract boolean test(SearchTask task); + public abstract boolean test(GeneralTask task); @Override public NumericProperty getStatistic() { diff --git a/src/main/java/pulse/search/statistics/RSquaredTest.java b/src/main/java/pulse/search/statistics/RSquaredTest.java index b29f6b1..0feceed 100644 --- a/src/main/java/pulse/search/statistics/RSquaredTest.java +++ b/src/main/java/pulse/search/statistics/RSquaredTest.java @@ -1,13 +1,13 @@ package pulse.search.statistics; import static java.lang.Math.pow; +import java.util.List; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.SIGNIFICANCE; import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; -import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * The coefficient of determination represents the goodness of fit that a @@ -25,7 +25,7 @@ public RSquaredTest() { } @Override - public boolean test(SearchTask task) { + public boolean test(GeneralTask task) { evaluate(task); sos = new SumOfSquares(); return getStatistic().compareTo(signifiance) > 0; @@ -48,35 +48,25 @@ public boolean test(SearchTask task) { * page
*/ @Override - public void evaluate(SearchTask t) { - var reference = t.getExperimentalCurve(); - + public void evaluate(GeneralTask t) { + var yr = t.getInput().getY(); sos.evaluate(t); - final int start = reference.getIndexRange().getLowerBound(); - final int end = reference.getIndexRange().getUpperBound(); - - final double mean = mean(reference, start, end); + final double mean = mean(yr); double TSS = 0; - - for (int i = start; i < end; i++) { - TSS += pow(reference.signalAt(i) - mean, 2); + int size = yr.size(); + + for (int i = 0; i < size; i++) { + TSS += pow(yr.get(i) - mean, 2); } - TSS /= (end - start); - + TSS /= size; + setStatistic(derive(TEST_STATISTIC, (1. - (double) sos.getStatistic().getValue() / TSS))); } - private double mean(ExperimentalData data, final int start, final int end) { - double mean = 0; - - for (int i = start; i < end; i++) { - mean += data.signalAt(i); - } - - mean /= (end - start); - return mean; + private double mean(List input) { + return input.stream().mapToDouble(d -> d).average().getAsDouble(); } public SumOfSquares getSumOfSquares() { diff --git a/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java b/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java new file mode 100644 index 0000000..af8710f --- /dev/null +++ b/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java @@ -0,0 +1,60 @@ +package pulse.search.statistics; + +import pulse.input.IndexRange; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; +import pulse.search.GeneralTask; + +/** + * This is an experimental feature. + * + */ +public class RangePenalisedLeastSquares extends SumOfSquares { + + private double lambda = 0.1; + + public RangePenalisedLeastSquares() { + super(); + } + + public RangePenalisedLeastSquares(RangePenalisedLeastSquares rls) { + super(rls); + this.lambda = rls.lambda; + } + + /** + * The lambda is the regularisation strength. + * + * @return the lambda factor. + */ + public double getLambda() { + return lambda; + } + + public void setLambda(double lambda) { + this.lambda = lambda; + } + + @Override + public void evaluate(GeneralTask t) { + calculateResiduals(t); + super.evaluate(t); + final double ssr = (double) getStatistic().getValue(); + var x = t.getInput().getX(); + double partialRange = t.getInput().bounds().length(); + double fullRange = x.get(x.size() - 1) - x.get(IndexRange.closestLeft(0.0, x)); + final double statistic = ssr + lambda * (fullRange - partialRange)/fullRange; + setStatistic(derive(OPTIMISER_STATISTIC, statistic)); + } + + @Override + public String getDescriptor() { + return "Range-Penalised Least Squares"; + } + + @Override + public OptimiserStatistic copy() { + return new RangePenalisedLeastSquares(this); + } + +} diff --git a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java index a220525..6fcd893 100644 --- a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java +++ b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java @@ -2,8 +2,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; - -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * This is an experimental feature. The objective function here is equal to the @@ -12,19 +11,16 @@ * dimensionality are favoured. * */ -public class RegularisedLeastSquares extends OptimiserStatistic { +public class RegularisedLeastSquares extends SumOfSquares { private double lambda = 1e-4; - private SumOfSquares sos; - + public RegularisedLeastSquares() { super(); - sos = new SumOfSquares(); } public RegularisedLeastSquares(RegularisedLeastSquares rls) { super(rls); - sos = new SumOfSquares(rls.sos); this.lambda = rls.lambda; } @@ -47,10 +43,11 @@ public void setLambda(double lambda) { * @see pulse.search.statistics.SumOfSquares */ @Override - public void evaluate(SearchTask t) { - sos.evaluate(t); - final double ssr = (double) sos.getStatistic().getValue(); - final double statistic = ssr + lambda * t.searchVector().lengthSq(); + public void evaluate(GeneralTask t) { + calculateResiduals(t); + super.evaluate(t); + final double ssr = (double) getStatistic().getValue(); + final double statistic = ssr + lambda * t.searchVector().toVector().lengthSq(); setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } @@ -59,11 +56,6 @@ public String getDescriptor() { return "L2 Regularised Least Squares"; } - @Override - public double variance() { - return (double) sos.getStatistic().getValue(); - } - @Override public OptimiserStatistic copy() { return new RegularisedLeastSquares(this); diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index 01ed160..a00ad58 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -1,19 +1,18 @@ package pulse.search.statistics; -import static java.lang.Math.max; -import static java.lang.Math.min; -import static pulse.input.IndexRange.closestLeft; -import static pulse.input.IndexRange.closestRight; +import java.util.ArrayList; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; -import java.util.ArrayList; import java.util.List; +import pulse.DiscreteInput; +import pulse.Response; +import pulse.input.IndexRange; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * An abstract statistic (= a numeric value resulting from a statistical @@ -29,30 +28,29 @@ public abstract class ResidualStatistic extends Statistic { private double statistic; - private List residuals; + private List rx; + private List ry; public ResidualStatistic() { super(); - residuals = new ArrayList<>(); + ry = new ArrayList<>(); + rx = new ArrayList<>(); setPrefix("Residuals"); } public ResidualStatistic(ResidualStatistic another) { this.statistic = another.statistic; - this.residuals = new ArrayList<>(another.residuals); - } - - public double[] transformResiduals() { - return getResiduals().stream().mapToDouble(doubleArray -> doubleArray[1]).toArray(); + ry = new ArrayList<>(); + rx = new ArrayList<>(); } /** * This will calculate the residuals for the {@code task} using the time - * sequence defined by the {@code ExperimentalData} object. The residuals - * are calculated between the model, which was previously used to populate - * the {@code HeatingCurve} and the experimental data. The temperature value - * of the model at the reference time is - * ti. and unknown a + * sequence defined by the {@code ExperimentalData} object.The residuals are + * calculated between the model, which was previously used to populate the + * {@code HeatingCurve}and the experimental data.The temperature value of + * the model at the reference time is + * ti.and unknown a * priori. Therefore, it needs to be interpolated based on the discrete * dataset generated by the solver. The interpolation is currently done * using natural cubic splines, which are re-constructed each time a new @@ -62,50 +60,72 @@ public double[] transformResiduals() { * {@code ExperimentalData} reference. The output of this method is stored * in the field of the {@code residuals} object. * - * @param task the optimisation task + * @param reference + * @param estimate * @see pulse.input.ExperimentalData * @see pulse.HeatingCurve */ - public void calculateResiduals(SearchTask task) { - var estimate = task.getCurrentCalculation().getProblem().getHeatingCurve(); - var reference = task.getExperimentalCurve(); + public final void calculateResiduals(DiscreteInput reference, Response estimate, int min, int max) { + var y = reference.getY(); + var x = reference.getX(); + + //if size has not changed, use the old list + + if (ry.size() == max - min + 1) { + + for (int i = min; i < max; i++) { - residuals.clear(); - var indexRange = reference.getIndexRange(); - var time = reference.getTimeSequence(); + ry.set(i - min, y.get(i) - estimate.evaluate(x.get(i))); - var s = estimate.getSplineInterpolation(); + } - int startIndex = max(closestLeft(estimate.timeAt(0), time), indexRange.getLowerBound()); - int endIndex = min(closestRight(estimate.timeLimit(), time), indexRange.getUpperBound()); + } + + //else create a new list + + else { - double interpolated; + rx = x.subList(min, max); + ry.clear(); - for (int i = startIndex; i <= endIndex; i++) { - /* - * find the point on the calculated heating curve which has the closest time - * value smaller than the experimental points' time value - */ + for (int i = min; i < max; i++) { - interpolated = s.value(reference.timeAt(i)); + ry.add(y.get(i) - estimate.evaluate(x.get(i))); - residuals.add(new double[]{reference.timeAt(i), - reference.signalAt(i) - interpolated}); // y_exp - y* + } } } + + public void calculateResiduals(DiscreteInput reference, Response estimate) { + var y = reference.getY(); + var x = reference.getX(); + + var estimateRange = estimate.accessibleRange(); + + int min = (int) Math.max(reference.getIndexRange().getLowerBound(), + IndexRange.closestLeft(estimateRange.getMinimum(), x) ); + int max = (int) Math.min(reference.getIndexRange().getUpperBound(), + IndexRange.closestRight(estimateRange.getMaximum(), x) ); + + calculateResiduals(reference, estimate, min, max); + } + + public double[] residualsArray() { + return ry.stream().mapToDouble(d -> d).toArray(); + } - public List getResiduals() { - return residuals; + public final void calculateResiduals(GeneralTask task) { + calculateResiduals(task.getInput(), task.getResponse()); } - public double residualUpperBound() { - return residuals.stream().map(array -> array[1]).reduce((a, b) -> b > a ? b : a).get(); + public List getResiduals() { + return ry; } - public double residualLowerBound() { - return residuals.stream().map(array -> array[1]).reduce((a, b) -> a < b ? a : b).get(); + public List getTimeSequence() { + return rx; } public NumericProperty getStatistic() { @@ -117,10 +137,6 @@ public void setStatistic(NumericProperty statistic) { this.statistic = (double) statistic.getValue(); } - public void incrementStatistic(final double increment) { - this.statistic += increment; - } - @Override public void set(NumericPropertyKeyword type, NumericProperty property) { if (type == OPTIMISER_STATISTIC) { diff --git a/src/main/java/pulse/search/statistics/Statistic.java b/src/main/java/pulse/search/statistics/Statistic.java index 7a3c5e1..46a7f21 100644 --- a/src/main/java/pulse/search/statistics/Statistic.java +++ b/src/main/java/pulse/search/statistics/Statistic.java @@ -1,5 +1,6 @@ package pulse.search.statistics; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -11,6 +12,6 @@ */ public abstract class Statistic extends PropertyHolder implements Reflexive { - public abstract void evaluate(SearchTask t); - -} + public abstract void evaluate(GeneralTask t); + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/SumOfSquares.java b/src/main/java/pulse/search/statistics/SumOfSquares.java index c4905ef..f264ed8 100644 --- a/src/main/java/pulse/search/statistics/SumOfSquares.java +++ b/src/main/java/pulse/search/statistics/SumOfSquares.java @@ -2,8 +2,7 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; - -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** * The standard optimality criterion of the L2 norm condition, or simply @@ -39,11 +38,12 @@ public SumOfSquares(SumOfSquares sos) { * @param t The task containing the reference and calculated curves * @see calculateResiduals() */ + @Override - public void evaluate(SearchTask t) { + public void evaluate(GeneralTask t) { calculateResiduals(t); - final double statistic = getResiduals().stream().map(r -> r[1] * r[1]) - .reduce(Double::sum).get() / getResiduals().size(); + final double statistic = getResiduals().stream().mapToDouble(r -> r * r) + .average().getAsDouble(); setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index af33470..4755a1d 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -10,15 +10,19 @@ import java.util.List; import java.util.stream.Collectors; +import pulse.Response; import pulse.input.ExperimentalData; import pulse.input.Metadata; +import pulse.math.Segment; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.ILLEGAL_PARAMETERS; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.search.statistics.BICStatistic; import pulse.search.statistics.FTest; import pulse.search.statistics.ModelSelectionCriterion; @@ -30,7 +34,7 @@ import pulse.util.PropertyEvent; import pulse.util.PropertyHolder; -public class Calculation extends PropertyHolder implements Comparable { +public class Calculation extends PropertyHolder implements Comparable, Response { private Status status; public final static double RELATIVE_TIME_MARGIN = 1.01; @@ -96,7 +100,6 @@ public void setProblem(Problem problem, ExperimentalData curve) { this.problem = problem; problem.setParent(this); problem.removeHeatingCurveListeners(); - problem.retrieveData(curve); addProblemListeners(problem, curve); } @@ -165,7 +168,7 @@ public void process() throws SolverException { list.forEach(np -> sb.append(String.format("%n %-25s", np)) ); - throw new SolverException(sb.toString()); + throw new SolverException(sb.toString(), ILLEGAL_PARAMETERS); } ((Solver) scheme).solve(problem); } @@ -248,6 +251,7 @@ public void setOptimiserStatistic(OptimiserStatistic os) { initModelCriterion(); } + @Override public OptimiserStatistic getOptimiserStatistic() { return os; } @@ -351,4 +355,33 @@ public void setResult(Result result) { } } -} + @Override + public double evaluate(double t) { + return problem.getHeatingCurve().interpolateSignalAt(t); + } + + @Override + public Segment accessibleRange() { + var hc = problem.getHeatingCurve(); + return new Segment(hc.timeAt(0), hc.timeLimit()); + } + + /** + * This will use the current {@code DifferenceScheme} to solve the + * {@code Problem} for this {@code SearchTask} and calculate the SSR value + * showing how well (or bad) the calculated solution describes the + * {@code ExperimentalData}. + * + * @param task + * @return the value of SSR (sum of squared residuals). + * @throws pulse.problem.schemes.solvers.SolverException + */ + + @Override + public double objectiveFunction(GeneralTask task) throws SolverException { + process(); + os.evaluate(task); + return (double) os.getStatistic().getValue(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index b17a6db..733a939 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -2,12 +2,10 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; -import static pulse.search.direction.ActiveFlags.activeParameters; import static pulse.search.direction.PathOptimiser.getInstance; import static pulse.tasks.logs.Details.ABNORMAL_DISTRIBUTION_OF_RESIDUALS; import static pulse.tasks.logs.Details.INCOMPATIBLE_OPTIMISER; import static pulse.tasks.logs.Details.INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT; -import static pulse.tasks.logs.Details.MAX_ITERATIONS_REACHED; import static pulse.tasks.logs.Details.MISSING_BUFFER; import static pulse.tasks.logs.Details.MISSING_DIFFERENCE_SCHEME; import static pulse.tasks.logs.Details.MISSING_HEATING_CURVE; @@ -21,25 +19,23 @@ import static pulse.tasks.logs.Status.INCOMPLETE; import static pulse.tasks.logs.Status.IN_PROGRESS; import static pulse.tasks.logs.Status.READY; -import static pulse.tasks.processing.Buffer.getSize; import static pulse.util.Reflexive.instantiate; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; import java.util.stream.Collectors; import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; +import pulse.math.ParameterIdentifier; import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.search.direction.ActiveFlags; -import pulse.search.direction.IterativeState; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; import pulse.tasks.listeners.DataCollectionListener; @@ -47,16 +43,12 @@ import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.logs.CorrelationLogEntry; import pulse.tasks.logs.DataLogEntry; -import pulse.tasks.logs.Details; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; import pulse.tasks.logs.StateEntry; import pulse.tasks.logs.Status; -import pulse.tasks.processing.Buffer; import pulse.tasks.processing.CorrelationBuffer; -import pulse.util.Accessible; import static pulse.tasks.logs.Status.AWAITING_TERMINATION; -import static pulse.tasks.logs.Status.TERMINATED; /** * A {@code SearchTask} is the most important class in {@code PULsE}. It @@ -68,15 +60,11 @@ * * @see pulse.tasks.TaskManager */ -public class SearchTask extends Accessible implements Runnable { +public class SearchTask extends GeneralTask { private Calculation current; private List stored; private ExperimentalData curve; - - private IterativeState path; //current sate - private IterativeState best; //best state - private Buffer buffer; private Log log; private final CorrelationBuffer correlationBuffer; @@ -89,8 +77,8 @@ public class SearchTask extends Accessible implements Runnable { * lower than this constant, the result will be considered * {@code AMBIGUOUS}. */ - private List listeners; - private List statusChangeListeners; + private final List listeners; + private final List statusChangeListeners; /** *

@@ -104,6 +92,7 @@ public class SearchTask extends Accessible implements Runnable { * @param curve the {@code ExperimentalData} */ public SearchTask(ExperimentalData curve) { + super(); this.statusChangeListeners = new CopyOnWriteArrayList<>(); this.listeners = new CopyOnWriteArrayList<>(); current = new Calculation(this); @@ -114,23 +103,7 @@ public SearchTask(ExperimentalData curve) { clear(); addListeners(); } - - /** - * Update the best state. The instance of this class stores two objects of - * the type IterativeState: the current state of the optimiser and the - * global best state. Calling this method will check if a new global best is - * found, and if so, this will store its parameters in the corresponding - * variable. This will then be used at the final stage of running the search - * task, comparing the converged result to the global best, and selecting - * whichever has the lowest cost. Such routine is required due to the - * possibility of some optimisers going uphill. - */ - public void storeState() { - if (best == null || best.getCost() > path.getCost()) { - best = new IterativeState(path); - } - } - + private void addListeners() { InterpolationDataset.addListener(e -> { if (current.getProblem() != null) { @@ -171,218 +144,22 @@ private void addListeners() { public void clear() { stored = new ArrayList<>(); curve.resetRanges(); - buffer = new Buffer(); correlationBuffer.clear(); - buffer.setParent(this); log = new Log(this); initCorrelationTest(); initNormalityTest(); - this.path = null; + //this.path = null; current.clear(); this.checkProblems(true); } - - /** - * This will use the current {@code DifferenceScheme} to solve the - * {@code Problem} for this {@code SearchTask} and calculate the SSR value - * showing how well (or bad) the calculated solution describes the - * {@code ExperimentalData}. - * - * @return the value of SSR (sum of squared residuals). - * @throws SolverException - */ - public double solveProblemAndCalculateCost() throws SolverException { - current.process(); - var rs = current.getOptimiserStatistic(); - rs.evaluate(this); - return (double) rs.getStatistic().getValue(); - } - + public List alteredParameters() { - return activeParameters(this).stream().map(key -> this.numericProperty(key)).collect(Collectors.toList()); - } - - /** - * Generates a search vector (= optimisation vector) using the search flags - * set by the {@code PathSolver}. - * - * @return an {@code IndexedVector} with search parameters of this - * {@code SearchTaks} - * @see pulse.search.direction.PathSolver.getSearchFlags() - * @see pulse.problem.statements.Problem.optimisationVector(List) - */ - public ParameterVector searchVector() { - var flags = ActiveFlags.getAllFlags(); - var keywords = activeParameters(this); - var optimisationVector = new ParameterVector(keywords); - - current.getProblem().optimisationVector(optimisationVector, flags); - curve.getRange().optimisationVector(optimisationVector, flags); - - return optimisationVector; - } - - /** - * Assigns the values of the parameters of this {@code SearchTask} to - * {@code searchParameters}. - * - * @param searchParameters an {@code IndexedVector} with relevant search - * parameters - * @throws pulse.problem.schemes.solvers.SolverException - * @see pulse.problem.statements.Problem.assign(IndexedVector) - */ - public void assign(ParameterVector searchParameters) throws SolverException { - current.getProblem().assign(searchParameters); - curve.getRange().assign(searchParameters); - } - - /** - *

- * Runs this task if is either {@code READY} or {@code QUEUED}. Otherwise, - * will do nothing. After making some preparatory steps, will initiate a - * loop with successive calls to {@code PathSolver.iteration(this)}, filling - * the buffer and notifying any data change listeners in parallel. This loop - * will go on until either converging results are obtained, or a timeout is - * reached, or if an execution error happens. Whether the run has been - * successful will be determined by comparing the associated - * R2 value with the {@code SUCCESS_CUTOFF}. - *

- */ - @Override - public void run() { - - current.setResult(null); - - /* check of status */ - switch (current.getStatus()) { - case READY: - case QUEUED: - setStatus(IN_PROGRESS); - break; - default: - return; - } - - /* preparatory steps */ - current.getProblem().parameterListChanged(); // get updated list of parameters - - var optimiser = getInstance(); - - path = optimiser.initState(this); - - var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); - int bufferSize = (Integer) getSize().getValue(); - buffer.init(); - correlationBuffer.clear(); - - /* search cycle */ - /* sets an independent thread for manipulating the buffer */ - List> bufferFutures = new ArrayList<>(bufferSize); - var singleThreadExecutor = Executors.newSingleThreadExecutor(); - - try { - solveProblemAndCalculateCost(); - } catch (SolverException e1) { - notifyFailedStatus(e1); - } - - outer: - do { - - bufferFutures.clear(); - - for (var i = 0; i < bufferSize; i++) { - - try { - for (boolean finished = false; !finished;) { - finished = optimiser.iteration(this); - } - } catch (SolverException e) { - notifyFailedStatus(e); - break outer; - } - - //if global best is better than the converged value - if (best != null && best.getCost() < path.getCost()) { - try { - //assign the global best parameters - assign(path.getParameters()); - //and try to re-calculate - solveProblemAndCalculateCost(); - } catch (SolverException ex) { - notifyFailedStatus(ex); - } - } - - final var j = i; - - bufferFutures.add(CompletableFuture.runAsync(() -> { - buffer.fill(this, j); - correlationBuffer.inflate(this); - notifyDataListeners(new DataLogEntry(this)); - }, singleThreadExecutor)); - - } - - bufferFutures.forEach(future -> future.join()); - - } while (buffer.isErrorTooHigh(errorTolerance) - && current.getStatus() == IN_PROGRESS); - - singleThreadExecutor.shutdown(); - - if (current.getStatus() == IN_PROGRESS) { - runChecks(); - } - - } - - private void runChecks() { - - if (!normalityTest.test(this)) { // first, check if the residuals are normally-distributed - var status = FAILED; - status.setDetails(ABNORMAL_DISTRIBUTION_OF_RESIDUALS); - setStatus(status); - } else { - - var test = correlationBuffer.test(correlationTest); // second, check there are no unexpected - // correlations - notifyDataListeners(new CorrelationLogEntry(this)); - - if (test) { - var status = AMBIGUOUS; - status.setDetails(SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); - setStatus(status); - } else { - // lastly, check if the parameter values estimated in this procedure are - // reasonable - - var properties = alteredParameters(); - - if (properties.stream().anyMatch(np -> !np.validate())) { - var status = FAILED; - status.setDetails(PARAMETER_VALUES_NOT_SENSIBLE); - setStatus(status); - } else { - current.getModelSelectionCriterion().evaluate(this); - setStatus(DONE); - } - - } - - } - } - - public void notifyFailedStatus(SolverException e1) { - var status = Status.FAILED; - status.setDetails(Details.SOLVER_ERROR); - status.setDetailedMessage(e1.getMessage()); - e1.printStackTrace(); - setStatus(status); - } + return activeParameters().stream().map(key -> + this.numericProperty(key)).collect(Collectors.toList()); + } public void addTaskListener(DataCollectionListener toAdd) { listeners.add(toAdd); @@ -404,15 +181,7 @@ public void removeStatusChangeListeners() { public String toString() { return getIdentifier().toString(); } - - public ExperimentalData getExperimentalCurve() { - return curve; - } - - public IterativeState getIterativeState() { - return path; - } - + /** * Adopts the {@code curve} by this {@code SearchTask}. * @@ -427,28 +196,6 @@ public void setExperimentalCurve(ExperimentalData curve) { } - /** - * Will return {@code true} if status could be updated. - * - * @param status the status of the task - * @return {@code} true if status has been updated. {@code false} if the - * status was already set to {@code status} previously, or if it could not - * be updated at this time. - * @see Calculation.setStatus() - */ - public boolean setStatus(Status status) { - Objects.requireNonNull(status); - - Status oldStatus = current.getStatus(); - boolean changed = current.setStatus(status) - && (oldStatus != current.getStatus()); - if (changed) { - notifyStatusListeners(new StateEntry(this, status)); - } - - return changed; - } - /** *

* Checks if this {@code SearchTask} is ready to be run.Performs basic check @@ -465,7 +212,7 @@ public boolean setStatus(Status status) { * @param updateStatus */ public void checkProblems(boolean updateStatus) { - var status = current.getStatus(); + var status = getStatus(); if (status == DONE) { return; @@ -484,7 +231,7 @@ public void checkProblems(boolean updateStatus) { s.setDetails(MISSING_HEATING_CURVE); } else if (pathSolver == null) { s.setDetails(MISSING_OPTIMISER); - } else if (buffer == null) { + } else if (getBuffer() == null) { s.setDetails(MISSING_BUFFER); } else if (!getInstance().compatibleWith(current.getOptimiserStatistic())) { s.setDetails(INCOMPATIBLE_OPTIMISER); @@ -516,24 +263,28 @@ private void notifyStatusListeners(StateEntry e) { l.onStatusChange(e); } } - + @Override - public String describe() { - - var sb = new StringBuilder(); - sb.append(TaskManager.getManagerInstance().getSampleName()); - sb.append("_Task_"); - var extId = curve.getMetadata().getExternalID(); - if (extId < 0) { - sb.append("IntID_").append(identifier.getValue()); - } else { - sb.append("ExtID_").append(extId); + public void run() { + correlationBuffer.clear(); + current.setResult(null); + + /* check of status */ + switch (getStatus()) { + case READY: + case QUEUED: + setStatus(IN_PROGRESS); + break; + default: + return; } - - return sb.toString(); - + + current.getProblem().parameterListChanged(); // get updated list of parameters + setDefaultOptimiser(); + + super.run(); } - + /** * If the current task is either {@code IN_PROGRESS}, {@code QUEUED}, or * {@code READY}, terminates it by setting its status to {@code TERMINATED}. @@ -541,39 +292,15 @@ public String describe() { * running). */ public void terminate() { - switch (current.getStatus()) { + switch (getStatus()) { case IN_PROGRESS: case QUEUED: - case READY: setStatus(AWAITING_TERMINATION); break; default: } } - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally left blank - } - - /** - * A {@code SearchTask} is deemed equal to another one if it has the same - * {@code ExperimentalData}. - */ - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - - if (!(o instanceof SearchTask)) { - return false; - } - - return curve.equals(((SearchTask) o).getExperimentalCurve()); - - } - public NormalityTest getNormalityTest() { return normalityTest; } @@ -595,22 +322,17 @@ public CorrelationBuffer getCorrelationBuffer() { public CorrelationTest getCorrelationTest() { return correlationTest; } - - public Calculation getCurrentCalculation() { - return current; - } - + public List getStoredCalculations() { return this.stored; - } + } public void storeCalculation() { var copy = new Calculation(current); stored.add(copy); - } + } public void switchTo(Calculation calc) { - current.setParent(null); current = calc; current.setParent(this); var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); @@ -641,5 +363,191 @@ private void fireRepositoryEvent(TaskRepositoryEvent e) { l.onTaskListChanged(e); } } + + @Override + public boolean isInProgress() { + return getStatus() == IN_PROGRESS; + } + + @Override + public void intermediateProcessing() { + correlationBuffer.inflate(this); + notifyDataListeners(new DataLogEntry(this)); + } + + @Override + public void onSolverException(SolverException e) { + setStatus(Status.troubleshoot(e)); + } + + /** + * Generates a search vector (= optimisation vector) using the search flags + * set by the {@code PathSolver}. + * + * @return an {@code IndexedVector} with search parameters of this + * {@code SearchTaks} + * @see pulse.search.direction.PathSolver.getSearchFlags() + * @see pulse.problem.statements.Problem.optimisationVector(List) + */ + @Override + public ParameterVector searchVector() { + var ids = activeParameters().stream().map(id -> + new ParameterIdentifier(id)).collect(Collectors.toList()); + var optimisationVector = new ParameterVector(ids); + + current.getProblem().optimisationVector(optimisationVector); + curve.getRange().optimisationVector(optimisationVector); + + return optimisationVector; + } + + /** + * Assigns the values of the parameters of this {@code SearchTask} to + * {@code searchParameters}. + * + * @param searchParameters an {@code IndexedVector} with relevant search + * parameters + * @throws pulse.problem.schemes.solvers.SolverException + * @see pulse.problem.statements.Problem.assign(IndexedVector) + */ + @Override + public void assign(ParameterVector searchParameters) throws SolverException { + current.getProblem().assign(searchParameters); + curve.getRange().assign(searchParameters); + } + + @Override + public void postProcessing() { + + if (!normalityTest.test(this)) { // first, check if the residuals are normally-distributed + var status = FAILED; + status.setDetails(ABNORMAL_DISTRIBUTION_OF_RESIDUALS); + setStatus(status); + } else { + + var test = correlationBuffer.test(correlationTest); // second, check there are no unexpected + // correlations + notifyDataListeners(new CorrelationLogEntry(this)); + + if (test) { + var status = AMBIGUOUS; + status.setDetails(SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); + setStatus(status); + } else { + // lastly, check if the parameter values estimated in this procedure are + // reasonable + + var properties = this.getIterativeState().getParameters(); + + if (properties.findMalformedElements().size() > 0) { + var status = FAILED; + status.setDetails(PARAMETER_VALUES_NOT_SENSIBLE); + setStatus(status); + } else { + current.getModelSelectionCriterion().evaluate(this); + setStatus(DONE); + } + + } + + } + } + + + /** + * Finds what properties are being altered in the search of this SearchTask. + * + * @return a {@code List} of property types represented by + * {@code NumericPropertyKeyword}s + */ + @Override + public List activeParameters() { + var flags = ActiveFlags.getAllFlags(); + //problem dependent + var allActiveParams = ActiveFlags.selectActiveAndListed + (flags, current.getProblem()); + //problem independent (lower/upper bound) + var listed = ActiveFlags.selectActiveAndListed + (flags, curve.getRange() ); + allActiveParams.addAll(listed); + return allActiveParams; + } + + /** + * Will return {@code true} if status could be updated. + * + * @param status the status of the task + * @return {@code} true if status has been updated. {@code false} if the + * status was already set to {@code status} previously, or if it could not + * be updated at this time. + * @see Calculation.setStatus() + */ + + public boolean setStatus(Status status) { + Objects.requireNonNull(status); + + Status oldStatus = getStatus(); + boolean changed = current.setStatus(status) + && (oldStatus != getStatus()); + if (changed) { + notifyStatusListeners(new StateEntry(this, status)); + } + + return changed; + } + + public Status getStatus() { + return current.getStatus(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + /** + * A {@code SearchTask} is deemed equal to another one if it has the same + * {@code ExperimentalData}. + */ + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof SearchTask)) { + return false; + } + + return curve.equals(((SearchTask) o).curve); + + } + + @Override + public String describe() { + + var sb = new StringBuilder(); + sb.append(TaskManager.getManagerInstance().getSampleName()); + sb.append("_Task_"); + var extId = curve.getMetadata().getExternalID(); + if (extId < 0) { + sb.append("IntID_").append(identifier.getValue()); + } else { + sb.append("ExtID_").append(extId); + } + + return sb.toString(); + + } + + @Override + public ExperimentalData getInput() { + return curve; + } + + @Override + public Calculation getResponse() { + return current; + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index f8801c2..358403d 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -95,14 +95,6 @@ private TaskManager() { selectionListeners = new CopyOnWriteArrayList<>(); taskRepositoryListeners = new CopyOnWriteArrayList<>(); addHierarchyListener(statementListener); - /* - Calculate the half-time once data is loaded. - */ - addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { - getTask(e.getId()).getExperimentalCurve().calculateHalfTime(); - } - }); } /** @@ -123,35 +115,40 @@ public static TaskManager getManagerInstance() { * @param t a {@code SearchTask} that will be executed */ public void execute(SearchTask t) { - t.checkProblems(t.getCurrentCalculation().getStatus() != Status.DONE); + t.checkProblems(t.getStatus() != Status.DONE); //try to start cmputation // notify listeners computation is about to start if (!t.setStatus(QUEUED)) { return; } - + // notify listeners calculation started notifyListeners(new TaskRepositoryEvent(TASK_SUBMITTED, t.getIdentifier())); - + // run task t -- after task completed, write result and trigger listeners CompletableFuture.runAsync(t).thenRun(() -> { - var current = t.getCurrentCalculation(); + Calculation current = (Calculation)t.getResponse(); var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); - if (current.getStatus() == DONE) { - current.setResult(new Result(t, ResultFormat.getInstance())); - //notify listeners before the task is re-assigned + if (null == current.getStatus()) { notifyListeners(e); - t.storeCalculation(); } - else if(current.getStatus() == AWAITING_TERMINATION) { - t.setStatus(Status.TERMINATED); - } - else { - notifyListeners(e); + else switch (current.getStatus()) { + case DONE: + current.setResult(new Result(t, ResultFormat.getInstance())); + //notify listeners before the task is re-assigned + notifyListeners(e); + t.storeCalculation(); + break; + case AWAITING_TERMINATION: + t.setStatus(Status.TERMINATED); + break; + default: + notifyListeners(e); + break; } }); - + } /** @@ -174,7 +171,7 @@ public void notifyListeners(TaskRepositoryEvent e) { public void executeAll() { var queue = tasks.stream().filter(t -> { - switch (t.getCurrentCalculation().getStatus()) { + switch (t.getStatus()) { case IN_PROGRESS: case EXECUTION_ERROR: return false; @@ -198,7 +195,7 @@ public void executeAll() { */ public boolean isTaskQueueEmpty() { return !tasks.stream().anyMatch(t -> { - var status = t.getCurrentCalculation().getStatus(); + var status = t.getStatus(); return status == QUEUED || status == IN_PROGRESS; }); } @@ -262,7 +259,8 @@ public SampleName getSampleName() { return null; } - return optional.get().getExperimentalCurve().getMetadata().getSampleName(); + return ( (ExperimentalData) optional.get().getInput() ) + .getMetadata().getSampleName(); } /** @@ -308,7 +306,8 @@ public SearchTask getTask(Identifier id) { */ public SearchTask getTask(int externalId) { var o = tasks.stream().filter(t - -> Integer.compare(t.getExperimentalCurve().getMetadata().getExternalID(), + -> Integer.compare( ( (ExperimentalData) t.getInput()) + .getMetadata().getExternalID(), externalId) == 0).findFirst(); return o.isPresent() ? o.get() : null; } @@ -340,7 +339,7 @@ public void generateTask(File file) { curves.stream().forEach((ExperimentalData curve) -> { var task = new SearchTask(curve); addTask(task); - var data = task.getExperimentalCurve(); + var data = (ExperimentalData) task.getInput(); if (!data.isAcquisitionTimeSensible()) { data.truncate(); } @@ -374,7 +373,6 @@ public void generateTasks(List files) { }; Executors.newSingleThreadExecutor().submit(loader); - } /** @@ -512,8 +510,8 @@ public String describe() { public void evaluate() { tasks.stream().forEach(t -> { - var properties = t.getCurrentCalculation().getProblem().getProperties(); - var c = t.getExperimentalCurve(); + var properties = ( (Calculation) t.getResponse() ).getProblem().getProperties(); + var c = (ExperimentalData)t.getInput(); properties.useTheoreticalEstimates(c); }); } diff --git a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java index cacac2f..f6785bd 100644 --- a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java +++ b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java @@ -1,8 +1,8 @@ package pulse.tasks.logs; +import pulse.math.ParameterIdentifier; import static pulse.properties.NumericProperties.def; -import pulse.properties.NumericPropertyKeyword; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.util.ImmutablePair; @@ -32,16 +32,20 @@ public String toString() { sb.append("

"); sb.append(""); - for (ImmutablePair key : map.keySet()) { + for (ImmutablePair key : map.keySet()) { sb.append("
Correlation table
x y Correlation
"); - sb.append(def(key.getFirst()).getAbbreviation(false)); + sb.append(def(key.getFirst().getKeyword()).getAbbreviation(false)); + if(key.getFirst().getIndex() > 0) + sb.append(" - ").append(key.getFirst().getIndex()); sb.append(""); - sb.append(def(key.getSecond()).getAbbreviation(false)); + sb.append(def(key.getSecond().getKeyword()).getAbbreviation(false)); + if(key.getSecond().getIndex() > 0) + sb.append(" - ").append(key.getSecond().getIndex()); sb.append(""); if (test.compareToThreshold(map.get(key))) { sb.append(""); } - sb.append("" + String.format("%3.2f", map.get(key)) + ""); + sb.append("").append(String.format("%3.2f", map.get(key))).append(""); if (test.compareToThreshold(map.get(key))) { sb.append(""); } diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 8448b5f..2fad022 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -1,10 +1,11 @@ package pulse.tasks.logs; import java.lang.reflect.InvocationTargetException; -import java.util.Collections; import java.util.List; +import pulse.math.Parameter; +import pulse.math.ParameterIdentifier; +import pulse.properties.NumericProperties; -import pulse.properties.NumericProperty; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.ui.Messages; @@ -19,7 +20,7 @@ */ public class DataLogEntry extends LogEntry { - private List entry; + private List entry; /** * Creates a new {@code DataLogEntry} based on the current values of the @@ -52,13 +53,14 @@ public DataLogEntry(SearchTask task) { */ private void fill() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { var task = TaskManager.getManagerInstance().getTask(getIdentifier()); - - entry = task.alteredParameters(); - Collections.sort(entry, (p1, p2) -> p1.getDescriptor(false).compareTo(p2.getDescriptor(false))); - entry.add(0, task.getIterativeState().getIteration()); + entry = task.searchVector().getParameters(); + var pval = task.getIterativeState().getIteration(); + var pid = new Parameter(new ParameterIdentifier(pval.getType())); + pid.setValue( (int) pval.getValue() ); + entry.add(0, pid); } - public List getData() { + public List getData() { return entry; } @@ -83,13 +85,26 @@ public String toString() { */ sb.append(""); - for (NumericProperty p : entry) { + for (Parameter p : entry) { sb.append("<
"); diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index c8780ba..5b51953 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -9,7 +9,6 @@ import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.LogEntryListener; -import pulse.tasks.listeners.StatusChangeListener; import pulse.ui.Messages; import pulse.util.Group; @@ -23,8 +22,8 @@ public class Log extends Group { private List logEntries; private LocalTime start; private LocalTime end; - private Identifier id; - private List listeners; + private final Identifier id; + private final List listeners; private static boolean verbose = false; /** @@ -49,38 +48,32 @@ public Log(SearchTask task) { /** * Do these actions each time data has been collected for this task. */ - if (task.getCurrentCalculation().getStatus() != Status.INCOMPLETE && verbose) { + if (task.getStatus() != Status.INCOMPLETE && verbose) { logEntries.add(le); notifyListeners(le); } }); - task.addStatusChangeListener(new StatusChangeListener() { - - /** - * Do these actions every time the task status has changed. - */ - @Override - public void onStatusChange(StateEntry e) { - logEntries.add(e); - - if (e.getStatus() == Status.IN_PROGRESS) { - start = e.getTime(); - end = null; - } else { - end = e.getTime(); - } - - notifyListeners(e); - - if (e.getState() == Status.DONE) { - logFinished(); - } - + task.addStatusChangeListener((StateEntry e) -> { + logEntries.add(e); + + if (e.getStatus() == Status.IN_PROGRESS) { + start = e.getTime(); + end = null; + } else { + end = e.getTime(); } - - }); + + notifyListeners(e); + + if (e.getState() == Status.DONE) { + logFinished(); + } + } /** + * Do these actions every time the task status has changed. + */ + ); } @@ -92,15 +85,15 @@ private void notifyListeners(LogEntry logEntry) { listeners.stream().forEach(l -> l.onNewEntry(logEntry)); } - public List getListeners() { + public final List getListeners() { return listeners; } - public void addListener(LogEntryListener l) { + public final void addListener(LogEntryListener l) { listeners.add(l); } - public Identifier getIdentifier() { + public final Identifier getIdentifier() { return id; } @@ -128,10 +121,12 @@ public String toString() { sb.append(newLine); sb.append(newLine); - for (LogEntry le : logEntries) { + logEntries.stream().map(le -> { sb.append(le); + return le; + }).forEachOrdered(_item -> { sb.append(newLine); - } + }); return sb.toString(); diff --git a/src/main/java/pulse/tasks/logs/Status.java b/src/main/java/pulse/tasks/logs/Status.java index 87e226d..e2d7017 100644 --- a/src/main/java/pulse/tasks/logs/Status.java +++ b/src/main/java/pulse/tasks/logs/Status.java @@ -1,6 +1,9 @@ package pulse.tasks.logs; import java.awt.Color; +import java.util.Objects; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.schemes.solvers.SolverException.SolverExceptionType; /** * An enum that represents the different states in which a {@code SearchTask} @@ -130,5 +133,20 @@ public String getMessage() { } return sb.toString(); } + + public static Status troubleshoot(SolverException e1) { + Objects.requireNonNull(e1, "Solver exception cannot be null when calling troubleshoot!"); + Status status = null; + if(e1.getType() != SolverExceptionType.OPTIMISATION_TIMEOUT) { + status = Status.FAILED; + status.setDetails(Details.SOLVER_ERROR); + status.setDetailedMessage(e1.getMessage()); + } + else { + status = Status.TIMEOUT; + status.setDetails(Details.MAX_ITERATIONS_REACHED); + } + return status; + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index bcfbac0..e3a1684 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -13,7 +13,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.util.PropertyHolder; /** @@ -61,8 +61,9 @@ public void init() { * @param t the {@code SearchTask} * @param bufferElement the {@code bufferElement} which will be written over */ - public void fill(SearchTask t, int bufferElement) { - statistic[bufferElement] = (double) t.getCurrentCalculation().getOptimiserStatistic().getStatistic().getValue(); + public final void fill(GeneralTask t, int bufferElement) { + statistic[bufferElement] = (double) t.getResponse() + .getOptimiserStatistic().getStatistic().getValue(); data[bufferElement] = t.searchVector(); } @@ -80,7 +81,7 @@ public boolean isErrorTooHigh(double errorTolerance) { boolean result = false; for (int i = 0; i < e.length && (!result); i++) { - var index = data[0].getIndex(i); + var index = data[0].getParameters().get(i).getIdentifier().getKeyword(); final double av = average(index); e[i] = variance(index) / (av * av); @@ -105,7 +106,7 @@ public double average(NumericPropertyKeyword index) { double av = 0; for (ParameterVector v : data) { - av += v.getParameterValue(index); + av += v.getParameterValue(index, 0); } return av / data.length; @@ -142,7 +143,7 @@ public double variance(NumericPropertyKeyword index) { double av = average(index); for (ParameterVector v : data) { - final double s = v.getParameterValue(index) - av; + final double s = v.getParameterValue(index, 0) - av; sd += s * s; } @@ -188,7 +189,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { */ @Override public List listedTypes() { - return new ArrayList(Arrays.asList(def(BUFFER_SIZE))); + return new ArrayList<>(Arrays.asList(def(BUFFER_SIZE))); } } diff --git a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java index 31d3ef1..fc0c0f7 100644 --- a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java +++ b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java @@ -7,8 +7,10 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import pulse.math.ParameterIdentifier; import pulse.math.ParameterVector; +import pulse.math.linear.Vector; import pulse.properties.NumericPropertyKeyword; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.EmptyCorrelationTest; @@ -18,9 +20,9 @@ public class CorrelationBuffer { - private List params; - private static Set> excludePairList; - private static Set excludeSingleList; + private final List params; + private static final Set> excludePairList; + private static final Set excludeSingleList; private final static double DEFAULT_THRESHOLD = 1E-3; @@ -58,17 +60,18 @@ private void truncate(double threshold) { for(i = 0; i < size - 1; i = i + 2) { - ParameterVector diff = new ParameterVector( params.get(i), params.get(i + 1).subtract(params.get(i) )); - if(diff.lengthSq()/params.get(i).lengthSq() < thresholdSq) + Vector vParams = params.get(i).toVector(); + Vector vPlusOneParams = params.get(i + 1).toVector(); + Vector vDiff = vPlusOneParams.subtract(vParams); + if(vDiff.lengthSq()/vParams.lengthSq() < thresholdSq) break; } for(int j = size - 1; j > i; j--) - params.remove(j); - + params.remove(j); } - public Map, Double> evaluate(CorrelationTest t) { + public Map, Double> evaluate(CorrelationTest t) { if (params.isEmpty()) { throw new IllegalStateException("Zero number of entries in parameter list"); } @@ -79,24 +82,40 @@ public Map, Double> evaluate(CorrelationTe truncate(DEFAULT_THRESHOLD); - var indices = params.get(0).getIndices(); - var map = indices.stream() - .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble(v -> v.getParameterValue(index)).toArray())) + List indices = params.get(0).getParameters().stream() + .map(ps -> ps.getIdentifier()).collect(Collectors.toList()); + Map map = indices.stream() + .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble( + v -> v.getParameterValue(index.getKeyword(), index.getIndex())).toArray())) .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); int indicesSize = indices.size(); - var correlationMap = new HashMap, Double>(); - ImmutablePair pair = null; + var correlationMap = new HashMap, Double>(); + ImmutablePair pair; for (int i = 0; i < indicesSize; i++) { - if (!excludeSingleList.contains(indices.get(i))) { + var iKey = indices.get(i).getKeyword(); + + if (!excludeSingleList.contains(iKey)) { + for (int j = i + 1; j < indicesSize; j++) { - pair = new ImmutablePair<>(indices.get(i), indices.get(j)); - if (!excludeSingleList.contains(indices.get(j)) && !excludePairList.contains(pair)) { - correlationMap.put(pair, t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); + + var jKey = indices.get(j).getKeyword(); + + pair = new ImmutablePair<>(iKey, jKey); + + if (!excludeSingleList.contains(jKey) + && !excludePairList.contains(pair)) { + + correlationMap.put( + new ImmutablePair<>(indices.get(i), indices.get(j)), + t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); + } + } + } } @@ -112,6 +131,8 @@ public boolean test(CorrelationTest t) { return false; } + var values = map.values(); + return map.values().stream().anyMatch(d -> t.compareToThreshold(d)); } diff --git a/src/main/java/pulse/tasks/processing/Result.java b/src/main/java/pulse/tasks/processing/Result.java index 8350aed..626e292 100644 --- a/src/main/java/pulse/tasks/processing/Result.java +++ b/src/main/java/pulse/tasks/processing/Result.java @@ -1,5 +1,6 @@ package pulse.tasks.processing; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -28,7 +29,7 @@ public Result(SearchTask task, ResultFormat format) throws IllegalArgumentExcept throw new IllegalArgumentException(Messages.getString("Result.NullTaskError")); } - setParent(task.getCurrentCalculation()); + setParent((Calculation)task.getResponse()); format.getKeywords().stream().forEach(key -> addProperty(task.numericProperty(key))); diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index acadc0b..de97832 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -87,7 +87,7 @@ public static void main(String[] args) { }); } else { - System.out.println("An instance of PULsE is already running!"); + System.out.println(Messages.getString("msg.running")); } } diff --git a/src/main/java/pulse/ui/components/CalculationTable.java b/src/main/java/pulse/ui/components/CalculationTable.java index 9d966b4..95c4ab1 100644 --- a/src/main/java/pulse/ui/components/CalculationTable.java +++ b/src/main/java/pulse/ui/components/CalculationTable.java @@ -4,6 +4,8 @@ import static pulse.ui.frames.MainGraphFrame.getChart; import java.awt.Dimension; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import javax.swing.JTable; import javax.swing.SwingUtilities; @@ -23,9 +25,11 @@ public class CalculationTable extends JTable { private final static int HEADER_HEIGHT = 30; private TaskTableRenderer taskTableRenderer; + private ExecutorService plotExecutor; public CalculationTable() { super(); + plotExecutor = Executors.newSingleThreadExecutor(); setDefaultEditor(Object.class, null); taskTableRenderer = new TaskTableRenderer(); this.setRowSelectionAllowed(true); @@ -67,7 +71,7 @@ public void update(SearchTask t) { } public void identifySelection(SearchTask t) { - int modelIndex = t.getStoredCalculations().indexOf(t.getCurrentCalculation()); + int modelIndex = t.getStoredCalculations().indexOf(t.getResponse()); if (modelIndex > -1) { this.getSelectionModel().setSelectionInterval(modelIndex, modelIndex); } @@ -81,10 +85,12 @@ public void initListeners() { var task = TaskManager.getManagerInstance().getSelectedTask(); var id = convertRowIndexToModel(this.getSelectedRow()); if (!lsm.getValueIsAdjusting() && id > -1 && id < task.getStoredCalculations().size()) { - - task.switchTo(task.getStoredCalculations().get(id)); - getChart().plot(task, true); - + + plotExecutor.submit(() -> { + task.switchTo(task.getStoredCalculations().get(id)); + getChart().plot(task, true); + }); + } }); diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index 753b878..68dda2d 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -41,6 +41,7 @@ import pulse.input.IndexRange; import pulse.input.Range; import pulse.input.listeners.DataEvent; +import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperties; import static pulse.properties.NumericPropertyKeyword.LOWER_BOUND; import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; @@ -90,10 +91,9 @@ public void mouseDragged(MouseEvent e) { } SwingUtilities.invokeLater(() -> { - + //process dragged events - Range range = instance.getSelectedTask() - .getExperimentalCurve().getRange(); + Range range = ((ExperimentalData) (instance.getSelectedTask().getInput())).getRange(); double value = xCoord(e) / factor; //convert to seconds back from ms -- if needed if (lowerMarker.getState() != MovableValueMarker.State.IDLE) { @@ -107,7 +107,7 @@ public void mouseDragged(MouseEvent e) { } else { super.mouseDragged(e); } - + }); } @@ -118,12 +118,13 @@ public void mouseDragged(MouseEvent e) { //for each new task var eventTask = instance.getTask(e.getId()); if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { + var data = (ExperimentalData) eventTask.getInput(); //add passive data listener - eventTask.getExperimentalCurve().addDataListener((DataEvent e1) -> { + data.addDataListener((DataEvent e1) -> { //that will be triggered only when this task is selected if (instance.getSelectedTask() == eventTask) { //update marker values - var segment = eventTask.getExperimentalCurve().getRange().getSegment(); + var segment = data.getRange().getSegment(); lowerMarker.setValue(segment.getMinimum() * factor); //convert to ms -- if needed upperMarker.setValue(segment.getMaximum() * factor); //convert to ms -- if needed } @@ -221,7 +222,7 @@ public void plot(SearchTask task, boolean extendedCurve) { plot.setDataset(i, null); } - var rawData = task.getExperimentalCurve(); + var rawData = (ExperimentalData) task.getInput(); var segment = rawData.getRange().getSegment(); adjustAxisLabel(segment.getMaximum()); @@ -239,7 +240,7 @@ public void plot(SearchTask task, boolean extendedCurve) { lowerMarker = new MovableValueMarker(segment.getMinimum() * factor); upperMarker = new MovableValueMarker(segment.getMaximum() * factor); - final double margin = (lowerMarker.getValue() + upperMarker.getValue())/20.0; + final double margin = (lowerMarker.getValue() + upperMarker.getValue()) / 20.0; //add listener to handle range adjustment var lowerMarkerListener = new MouseOnMarkerListener(this, lowerMarker, upperMarker, margin); @@ -251,7 +252,7 @@ public void plot(SearchTask task, boolean extendedCurve) { plot.addDomainMarker(upperMarker); plot.addDomainMarker(lowerMarker); - var calc = task.getCurrentCalculation(); + var calc = (Calculation) task.getResponse(); var problem = calc.getProblem(); if (problem != null) { @@ -259,6 +260,15 @@ public void plot(SearchTask task, boolean extendedCurve) { var solution = problem.getHeatingCurve(); var scheme = calc.getScheme(); + if (solution != null && !solution.isFull()) { + try { + calc.process(); + } catch (SolverException ex) { + System.out.println("Could not plot solution! See details in debug."); + ex.printStackTrace(); + } + } + if (solution != null && scheme != null) { var solutionDataset = new XYSeriesCollection(); @@ -298,7 +308,7 @@ public void plot(SearchTask task, boolean extendedCurve) { public void plotSingle(HeatingCurve curve) { requireNonNull(curve); - var plot = chart.getXYPlot(); + plot = chart.getXYPlot(); var classicDataset = new XYSeriesCollection(); @@ -339,6 +349,7 @@ public XYSeries residuals(Calculation calc) { var problem = calc.getProblem(); var baseline = problem.getBaseline(); + var time = calc.getOptimiserStatistic().getTimeSequence(); var residuals = calc.getOptimiserStatistic().getResiduals(); var size = residuals.size(); @@ -348,7 +359,7 @@ public XYSeries residuals(Calculation calc) { var series = new XYSeries(format("Residuals (offset %3.2f)", offset)); for (var i = 0; i < size; i++) { - series.add(factor * residuals.get(i)[0], (Number) (residuals.get(i)[1] + offset)); + series.add(factor * time.get(i), (Number) (residuals.get(i) + offset)); } return series; diff --git a/src/main/java/pulse/ui/components/DataLoader.java b/src/main/java/pulse/ui/components/DataLoader.java index d6b6539..34f9f97 100644 --- a/src/main/java/pulse/ui/components/DataLoader.java +++ b/src/main/java/pulse/ui/components/DataLoader.java @@ -15,12 +15,14 @@ import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.filechooser.FileNameExtensionFilter; +import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; import pulse.input.InterpolationDataset.StandartType; import pulse.io.readers.MetaFilePopulator; import pulse.io.readers.ReaderManager; import pulse.problem.laser.NumericPulse; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; @@ -99,7 +101,7 @@ public static void loadMetadataDialog() { // attempt to fill metadata and problem for (SearchTask task : instance.getTaskList()) { - var data = task.getExperimentalCurve(); + var data = (ExperimentalData) task.getInput(); try { handler.populate(file, data.getMetadata()); @@ -109,7 +111,7 @@ public static void loadMetadataDialog() { e.printStackTrace(); } - var p = task.getCurrentCalculation().getProblem(); + var p = ( (Calculation) task.getResponse() ).getProblem(); if (p != null) { p.retrieveData(data); } @@ -146,7 +148,7 @@ public static void loadPulseDialog() { if (task != null) { pool.submit(() -> { - var metadata = task.getExperimentalCurve().getMetadata(); + var metadata = ((ExperimentalData) task.getInput()).getMetadata(); metadata.setPulseData(pulseData); metadata.getPulseDescriptor() .setSelectedDescriptor( diff --git a/src/main/java/pulse/ui/components/LogPane.java b/src/main/java/pulse/ui/components/LogPane.java index 0fb1716..5af4215 100644 --- a/src/main/java/pulse/ui/components/LogPane.java +++ b/src/main/java/pulse/ui/components/LogPane.java @@ -90,7 +90,7 @@ public void printAll() { log.getLogEntries().stream().forEach(entry -> post(entry)); - if (task.getCurrentCalculation().getStatus() == DONE) { + if (task.getStatus() == DONE) { printTimeTaken(log); } diff --git a/src/main/java/pulse/ui/components/ProblemTree.java b/src/main/java/pulse/ui/components/ProblemTree.java index 54513d9..e4c2e0d 100644 --- a/src/main/java/pulse/ui/components/ProblemTree.java +++ b/src/main/java/pulse/ui/components/ProblemTree.java @@ -14,6 +14,7 @@ import pulse.problem.statements.Problem; import pulse.problem.statements.ProblemComplexity; +import pulse.tasks.Calculation; import pulse.ui.components.controllers.ProblemCellRenderer; import pulse.ui.components.listeners.ProblemSelectionEvent; import pulse.ui.components.listeners.ProblemSelectionListener; @@ -21,7 +22,7 @@ @SuppressWarnings("serial") public class ProblemTree extends JTree { - private List selectionListeners; + private final List selectionListeners; public ProblemTree(List allProblems) { super(); @@ -66,7 +67,7 @@ private void addListeners() { }); instance.addSelectionListener(e -> { - var current = instance.getSelectedTask().getCurrentCalculation().getProblem(); + var current = ( (Calculation) instance.getSelectedTask().getResponse() ).getProblem(); // select appropriate problem type from list setSelectedProblem(current); @@ -108,7 +109,7 @@ public void setSelectedProblem(Problem p) { }); } - public void addProblemSelectionListener(ProblemSelectionListener l) { + public final void addProblemSelectionListener(ProblemSelectionListener l) { selectionListeners.add(l); } diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index 74aeeab..216770e 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -44,6 +44,7 @@ import pulse.search.statistics.NormalityTest; import pulse.search.statistics.OptimiserStatistic; import pulse.search.statistics.SumOfSquares; +import pulse.tasks.Calculation; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.processing.Buffer; import pulse.ui.components.listeners.ExitRequestListener; @@ -244,7 +245,8 @@ private JMenu initAnalysisSubmenu() { if (((AbstractButton) e.getItem()).isSelected()) { var text = ((AbstractButton) e.getItem()).getText(); setSelectedOptimiserDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> t.getCurrentCalculation().initOptimiser()); + getManagerInstance().getTaskList().stream().forEach(t -> + ( (Calculation) t.getResponse() ).initOptimiser()); } }); diff --git a/src/main/java/pulse/ui/components/RangeTextFields.java b/src/main/java/pulse/ui/components/RangeTextFields.java index 47cacf1..a146e55 100644 --- a/src/main/java/pulse/ui/components/RangeTextFields.java +++ b/src/main/java/pulse/ui/components/RangeTextFields.java @@ -1,30 +1,15 @@ -/* - * Copyright 2021 Artem Lunev . - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package pulse.ui.components; import java.awt.Color; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.text.DecimalFormat; -import java.text.NumberFormat; import java.text.ParseException; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JFormattedTextField; import javax.swing.text.NumberFormatter; +import pulse.input.ExperimentalData; import pulse.input.Range; import pulse.input.listeners.DataEvent; import pulse.tasks.SearchTask; @@ -36,7 +21,6 @@ /** * Two JFormattedTextFields used to display the range of the currently * selected task. - * @author Artem Lunev */ public final class RangeTextFields { @@ -69,7 +53,7 @@ public RangeTextFields() { //when a new task is selected instance.addSelectionListener((TaskSelectionEvent e) -> { var task = instance.getSelectedTask(); - var segment = task.getExperimentalCurve().getRange().getSegment(); + var segment = ( (ExperimentalData) task.getInput() ).getRange().getSegment(); //update the textfield values lowerLimitField.setValue(segment.getMinimum()); upperLimitField.setValue(segment.getMaximum()); @@ -109,8 +93,8 @@ private NumberFormatter initFormatter() { */ private static boolean isEditValid(JFormattedTextField jtf, boolean upperBound) { - Range range = TaskManager.getManagerInstance().getSelectedTask() - .getExperimentalCurve().getRange(); + Range range = ( (ExperimentalData) TaskManager.getManagerInstance().getSelectedTask() + .getInput() ).getRange(); double candidateValue = 0.0; try { @@ -195,10 +179,11 @@ public void focusLost(FocusEvent arg0) { } private void updateTextfieldsFromTask(SearchTask newTask) { + var data = (ExperimentalData) newTask.getInput(); //add data listeners in case when the range of the selected task is changed - newTask.getExperimentalCurve().addDataListener((DataEvent e1) -> { + data.addDataListener((DataEvent e1) -> { if (TaskManager.getManagerInstance().getSelectedTask() == newTask) { - var segment = newTask.getExperimentalCurve().getRange().getSegment(); + var segment = data.getRange().getSegment(); lowerLimitField.setValue(segment.getMinimum()); upperLimitField.setValue(segment.getMaximum()); } diff --git a/src/main/java/pulse/ui/components/ResidualsChart.java b/src/main/java/pulse/ui/components/ResidualsChart.java index 614d786..7a78ee2 100644 --- a/src/main/java/pulse/ui/components/ResidualsChart.java +++ b/src/main/java/pulse/ui/components/ResidualsChart.java @@ -30,10 +30,10 @@ public void plot(ResidualStatistic stat) { var pulseDataset = new HistogramDataset(); pulseDataset.setType(HistogramType.RELATIVE_FREQUENCY); - var residuals = stat.transformResiduals(); + var residuals = stat.residualsArray(); if (residuals.length > 0) { - pulseDataset.addSeries("H1", stat.transformResiduals(), binCount); + pulseDataset.addSeries("H1", residuals, binCount); } getPlot().setDataset(0, pulseDataset); diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index 520aa0e..d718926 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -17,6 +17,7 @@ import javax.swing.table.TableRowSorter; import pulse.properties.NumericProperty; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; @@ -77,35 +78,41 @@ public ResultTable(ResultFormat fmt) { */ TaskManager.getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { var t = instance.getTask(e.getId()); - switch (e.getState()) { - case TASK_FINISHED: - var r = t.getCurrentCalculation().getResult(); - var resultTableModel = (ResultTableModel) getModel(); - Objects.requireNonNull(r, "Task finished with a null result!"); - invokeLater(() -> resultTableModel.addRow(r)); - break; - case TASK_REMOVED: - case TASK_RESET: - ((ResultTableModel) getModel()).removeAll(e.getId()); - getSelectionModel().clearSelection(); - break; - case BEST_MODEL_SELECTED: - for (var c : t.getStoredCalculations()) { - if (c.getResult() != null && c != t.getCurrentCalculation()) { - ((ResultTableModel) getModel()).remove(c.getResult()); + + if(t != null) { + + var cc = (Calculation) t.getResponse(); + + switch (e.getState()) { + case TASK_FINISHED: + var r = cc.getResult(); + var resultTableModel = (ResultTableModel) getModel(); + Objects.requireNonNull(r, "Task finished with a null result!"); + invokeLater(() -> resultTableModel.addRow(r)); + break; + case TASK_REMOVED: + case TASK_RESET: + ((ResultTableModel) getModel()).removeAll(e.getId()); + getSelectionModel().clearSelection(); + break; + case BEST_MODEL_SELECTED: + for (var c : t.getStoredCalculations()) { + if (c.getResult() != null && c != cc) { + ((ResultTableModel) getModel()).remove(c.getResult()); + } } - } - this.select(t.getCurrentCalculation().getResult()); - break; - case TASK_MODEL_SWITCH: - var c = t.getCurrentCalculation(); - this.getSelectionModel().clearSelection(); - if (c != null && c.getResult() != null) { - select(c.getResult()); - } - break; - default: - break; + this.select(cc.getResult()); + break; + case TASK_MODEL_SWITCH: + this.getSelectionModel().clearSelection(); + if (cc != null && cc.getResult() != null) { + select(cc.getResult()); + } + break; + default: + break; + } + } }); diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index d89e43d..5dc2c26 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -27,9 +27,11 @@ import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JSeparator; +import pulse.input.ExperimentalData; import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.processing.Result; @@ -69,8 +71,9 @@ public TaskPopupMenu() { getString("TaskTablePopupMenu.EmptySelection2"), //$NON-NLS-1$ getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ } else { + var input = (ExperimentalData) t.getInput(); showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.getExperimentalCurve().getMetadata().toString(), "Metadata", PLAIN_MESSAGE); + input.getMetadata().toString(), "Metadata", PLAIN_MESSAGE); } }); @@ -78,14 +81,14 @@ public TaskPopupMenu() { instance.addSelectionListener(event -> { instance.getSelectedTask().checkProblems(false); - var details = instance.getSelectedTask().getCurrentCalculation().getStatus().getDetails(); + var details = instance.getSelectedTask().getStatus().getDetails(); itemShowStatus.setEnabled((details != null) & (details != NONE)); }); itemShowStatus.addActionListener((ActionEvent e) -> { var t = instance.getSelectedTask(); if (t != null) { - var d = t.getCurrentCalculation().getStatus().getDetails(); + var d = t.getStatus().getDetails(); showMessageDialog(getWindowAncestor((Component) e.getSource()), "This is due to " + d.toString() + "", "Problems with " + t, INFORMATION_MESSAGE); } @@ -100,7 +103,7 @@ public TaskPopupMenu() { getString("TaskTablePopupMenu.ErrorTitle"), ERROR_MESSAGE); //$NON-NLS-1$ } else { t.checkProblems(true); - var status = t.getCurrentCalculation().getStatus(); + var status = t.getStatus(); if (status == DONE) { var dialogButton = YES_NO_OPTION; @@ -115,7 +118,7 @@ public TaskPopupMenu() { } } else if (status != READY) { showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.toString() + " is " + t.getCurrentCalculation().getStatus().getMessage(), //$NON-NLS-1$ + t.toString() + " is " + t.getStatus().getMessage(), //$NON-NLS-1$ getString("TaskTablePopupMenu.TaskNotReady"), //$NON-NLS-1$ ERROR_MESSAGE); } else { @@ -136,7 +139,7 @@ public TaskPopupMenu() { if (t == null) { return; } - var current = t.getCurrentCalculation(); + var current = (Calculation) t.getResponse(); if (current != null) { var r = new Result(t, getInstance()); current.setResult(r); @@ -175,8 +178,8 @@ public void plot(boolean extended) { getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ } else { - var calc = t.getCurrentCalculation(); - var statusDetails = calc.getStatus().getDetails(); + var calc = (Calculation) t.getResponse(); + var statusDetails = t.getStatus().getDetails(); if (statusDetails == MISSING_HEATING_CURVE) { diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 37b84d5..30ea042 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -50,12 +50,12 @@ public ExecutionButton() { } var problematicTask = instance.getTaskList().stream().filter(t -> { t.checkProblems(true); - return t.getCurrentCalculation().getStatus() == INCOMPLETE; + return t.getStatus() == INCOMPLETE; }).findFirst(); if (problematicTask.isPresent()) { var t = problematicTask.get(); showMessageDialog(getWindowAncestor((Component) e.getSource()), - t + " is " + t.getCurrentCalculation().getStatus().getMessage(), "Problems found", + t + " is " + t.getStatus().getMessage(), "Problems found", ERROR_MESSAGE); } else { instance.executeAll(); diff --git a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java index ac0c3bd..526823f 100644 --- a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java +++ b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java @@ -32,8 +32,10 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean try { descriptor.attemptUpdate(e.getItem()); } catch(NullPointerException npe) { - System.out.println("Error updating " + descriptor.getDescriptor(false) - + ". Cannot be set to " + e.getItem()); + String text = "Error updating " + descriptor.getDescriptor(false) + + ". Cannot be set to " + e.getItem(); + System.out.println(text); + npe.printStackTrace(); } } }); diff --git a/src/main/java/pulse/ui/components/models/ResultTableModel.java b/src/main/java/pulse/ui/components/models/ResultTableModel.java index ffd35db..8592285 100644 --- a/src/main/java/pulse/ui/components/models/ResultTableModel.java +++ b/src/main/java/pulse/ui/components/models/ResultTableModel.java @@ -223,8 +223,7 @@ public void addRow(AbstractResult result) { if (result instanceof Result) { //result must have a valid ancestor! - var ancestor = Objects.requireNonNull( - result.specificAncestor(SearchTask.class), + var ancestor = Objects.requireNonNull(result.specificAncestor(SearchTask.class), "Result " + result.toString() + " does not belong a SearchTask!"); //the ancestor then has the SearchTask type @@ -232,8 +231,7 @@ public void addRow(AbstractResult result) { //any old result asssociated withis this task var oldResult = results.stream().filter(r - -> r.specificAncestor( - SearchTask.class) == parentTask).findAny(); + -> r.specificAncestor(SearchTask.class) == parentTask).findAny(); //check the following only if the old result is present if (oldResult.isPresent()) { @@ -249,7 +247,8 @@ public void addRow(AbstractResult result) { Status status = Status.DONE; //better result than already present -- update table - if (parentTask.getCurrentCalculation().isBetterThan(oldCalculation.get())) { + var c = (Calculation) parentTask.getResponse(); + if (c.isBetterThan(oldCalculation.get())) { remove(oldResultExisting); status.setDetails(Details.BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED); parentTask.setStatus(status); diff --git a/src/main/java/pulse/ui/components/models/TaskTableModel.java b/src/main/java/pulse/ui/components/models/TaskTableModel.java index 77d51b8..86eef58 100644 --- a/src/main/java/pulse/ui/components/models/TaskTableModel.java +++ b/src/main/java/pulse/ui/components/models/TaskTableModel.java @@ -11,6 +11,8 @@ import static pulse.ui.Messages.getString; import javax.swing.table.DefaultTableModel; +import pulse.input.ExperimentalData; +import pulse.tasks.Calculation; import pulse.tasks.Identifier; import pulse.tasks.SearchTask; @@ -51,11 +53,12 @@ public TaskTableModel() { } public void addTask(SearchTask t) { - var temperature = t.getExperimentalCurve() + var temperature = ( (ExperimentalData) t.getInput() ) .getMetadata().numericProperty(TEST_TEMPERATURE); + var calc = (Calculation) t.getResponse(); var data = new Object[]{t.getIdentifier(), temperature, - t.getCurrentCalculation().getOptimiserStatistic().getStatistic(), - t.getNormalityTest().getStatistic(), t.getCurrentCalculation().getStatus()}; + calc.getOptimiserStatistic().getStatistic(), + t.getNormalityTest().getStatistic(), t.getStatus()}; invokeLater(() -> super.addRow(data)); @@ -68,7 +71,7 @@ public void addTask(SearchTask t) { }); t.addTaskListener((LogEntry e) -> { - setValueAt(t.getCurrentCalculation().getOptimiserStatistic() + setValueAt(calc.getOptimiserStatistic() .getStatistic(), searchRow(t.getIdentifier()), SEARCH_STATISTIC_COLUMN); }); diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index 011d2f7..b4d5e54 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -26,6 +26,7 @@ import pulse.input.ExperimentalData; import pulse.input.Range; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.Messages; import pulse.ui.components.RangeTextFields; @@ -64,12 +65,13 @@ public final void initComponents() { pdfBtn.addActionListener(e -> { var task = TaskManager.getManagerInstance().getSelectedTask(); + var calc = (Calculation) task.getResponse(); - if (task != null && task.getCurrentCalculation().getModelSelectionCriterion() != null) { + if (task != null && calc.getModelSelectionCriterion() != null) { chFrame.setLocationRelativeTo(null); chFrame.setVisible(true); - chFrame.plot(task.getCurrentCalculation().getOptimiserStatistic()); + chFrame.plot(calc.getOptimiserStatistic()); } @@ -130,7 +132,7 @@ private void validateRange(double a, double b) { return; } - var expCurve = task.getExperimentalCurve(); + var expCurve = (ExperimentalData) task.getInput(); if (expCurve == null) { return; @@ -169,7 +171,7 @@ private void validateRange(double a, double b) { // set range for all available experimental datasets TaskManager.getManagerInstance().getTaskList() .stream().forEach((aTask) - -> setRange(aTask.getExperimentalCurve(), a, b) + -> setRange( (ExperimentalData) aTask.getInput(), a, b) ); } diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index 0aeedc8..92e42ca 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -20,6 +20,7 @@ import pulse.problem.schemes.solvers.Solver; import pulse.problem.schemes.solvers.SolverException; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.buttons.LoaderButton; import pulse.ui.frames.MainGraphFrame; @@ -61,15 +62,16 @@ public static void plot(ActionEvent e) { var t = instance.getSelectedTask(); - var calc = t.getCurrentCalculation(); + var calc = (Calculation) t.getResponse(); t.checkProblems(true); - var status = t.getCurrentCalculation().getStatus(); + var status = t.getStatus(); if (status == INCOMPLETE && !status.checkProblemStatementSet()) { getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), calc.getStatus().getMessage(), + showMessageDialog(getWindowAncestor((Component) e.getSource()), + calc.getStatus().getMessage(), getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ ERROR_MESSAGE); diff --git a/src/main/java/pulse/ui/frames/HistogramFrame.java b/src/main/java/pulse/ui/frames/HistogramFrame.java index f91f8a7..9debcdc 100644 --- a/src/main/java/pulse/ui/frames/HistogramFrame.java +++ b/src/main/java/pulse/ui/frames/HistogramFrame.java @@ -9,6 +9,7 @@ import javax.swing.JSlider; import pulse.search.statistics.ResidualStatistic; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.AuxPlotter; import pulse.ui.components.ResidualsChart; @@ -29,7 +30,9 @@ public HistogramFrame(AuxPlotter chart, int width, int height getContentPane().add(panel, SOUTH); slider.addChangeListener(e -> { ((ResidualsChart) chart).setBinCount(slider.getValue()); - plot(TaskManager.getManagerInstance().getSelectedTask().getCurrentCalculation().getOptimiserStatistic()); + var c = (Calculation) TaskManager.getManagerInstance().getSelectedTask() + .getResponse(); + plot(c.getOptimiserStatistic()); info.setText("Number of bins: " + slider.getValue()); }); } diff --git a/src/main/java/pulse/ui/frames/MainGraphFrame.java b/src/main/java/pulse/ui/frames/MainGraphFrame.java index fff33ec..e4400d4 100644 --- a/src/main/java/pulse/ui/frames/MainGraphFrame.java +++ b/src/main/java/pulse/ui/frames/MainGraphFrame.java @@ -4,8 +4,11 @@ import static java.awt.BorderLayout.LINE_END; import static java.awt.BorderLayout.PAGE_END; import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.JInternalFrame; +import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.TaskManager; import pulse.tasks.logs.Status; @@ -46,7 +49,7 @@ private void initComponents() { public void plot() { var task = TaskManager.getManagerInstance().getSelectedTask(); //do not plot tasks that are not finished - if (task != null && task.getCurrentCalculation().getStatus() != Status.IN_PROGRESS) { + if (task != null && task.getStatus() != Status.IN_PROGRESS) { Executors.newSingleThreadExecutor().submit(() -> chart.plot(task, false)); } } diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 41d5bbf..6acd7da 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -14,8 +14,10 @@ import java.awt.BorderLayout; import java.awt.GridLayout; +import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; @@ -32,9 +34,11 @@ import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.TreePath; +import pulse.input.ExperimentalData; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.statements.Problem; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskSelectionEvent; @@ -44,6 +48,7 @@ import pulse.ui.components.panels.ProblemToolbar; import pulse.ui.components.panels.SettingsToolBar; import pulse.ui.frames.TaskControlFrame.Mode; +import pulse.ui.frames.dialogs.ProgressDialog; @SuppressWarnings("serial") public class ProblemStatementFrame extends JInternalFrame { @@ -108,22 +113,25 @@ public ProblemStatementFrame() { /* * Scheme list and scroller */ - schemeSelectionList = new JList(); + schemeSelectionList = new JList<>(); schemeSelectionList.setSelectionMode(SINGLE_SELECTION); - schemeSelectionList.setModel(new DefaultListModel()); + schemeSelectionList.setModel(new DefaultListModel<>()); schemeSelectionList.addListSelectionListener((ListSelectionEvent arg0) -> { - if (TaskControlFrame.getInstance().getMode() != Mode.PROBLEM) { - return; - } + if (TaskControlFrame.getInstance().getMode() == Mode.PROBLEM) { - var selectedValue = schemeSelectionList.getSelectedValue(); + var selectedValue = schemeSelectionList.getSelectedValue(); + + if (selectedValue != null) { + + if (arg0.getValueIsAdjusting() || !(selectedValue instanceof DifferenceScheme)) { + ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); + } else { + changeSchemes(selectedValue); + } + + } - if (arg0.getValueIsAdjusting() || !(selectedValue instanceof DifferenceScheme)) { - ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); - } - else { - changeSchemes(selectedValue); } }); @@ -195,7 +203,7 @@ public void setSelectionPath(TreePath path) { //for all tasks instance.getTaskList().stream(). //select the problem statement of the current calculation - map(t -> t.getCurrentCalculation().getProblem()) + map(t -> ((Calculation) t.getResponse()).getProblem()) //that is non-null .filter(problem -> problem != null) //for each problem, update its properties in a separete thread @@ -215,13 +223,9 @@ public void update() { } private void update(SearchTask selectedTask) { - - if(selectedTask == null) - return; - - var calc = selectedTask.getCurrentCalculation(); - var selectedProblem = selectedTask == null ? null : calc.getProblem(); - var selectedScheme = selectedTask == null ? null : calc.getScheme(); + var calc = (Calculation) selectedTask.getResponse(); + var selectedProblem = calc.getProblem(); + var selectedScheme = calc.getScheme(); // problem if (selectedProblem == null) { @@ -237,95 +241,139 @@ private void update(SearchTask selectedTask) { setSelectedElement(schemeSelectionList, selectedScheme); schemeTable.setPropertyHolder(selectedScheme); } - } private void changeSchemes(DifferenceScheme newScheme) { var instance = TaskManager.getManagerInstance(); var selectedTask = instance.getSelectedTask(); + + var schemeLoaderTracker = new ProgressDialog(); + schemeLoaderTracker.setTitle("Initialising solution schemes..."); + schemeLoaderTracker.setLocationRelativeTo(null); + schemeLoaderTracker.setAlwaysOnTop(true); + + List> callableList; + if (instance.isSingleStatement()) { - var callableList = instance.getTaskList().stream().map(t -> new Callable() { + callableList = instance.getTaskList().stream().map(t -> new Callable() { @Override public DifferenceScheme call() throws Exception { changeScheme(t, newScheme); - return t.getCurrentCalculation().getScheme(); + schemeLoaderTracker.incrementProgress(); + return ((Calculation) t.getResponse()).getScheme(); } }).collect(Collectors.toList()); + } else { + callableList = Arrays.asList(() -> { + changeScheme(selectedTask, newScheme); + return selectedTask.getResponse().getScheme(); + }); + } + + schemeLoaderTracker.trackProgress(callableList.size() - 1); + + CompletableFuture.runAsync(() -> { try { schemeListExecutor.invokeAll(callableList); } catch (InterruptedException ex) { - Logger.getLogger(ProblemStatementFrame.class.getName()).log(Level.SEVERE, null, ex); + ex.printStackTrace(); } + }).thenRun(() -> { - } else { - changeScheme(selectedTask, newScheme); - } - schemeTable.setPropertyHolder(selectedTask.getCurrentCalculation().getScheme()); - if (selectedTask.getCurrentCalculation().getProblem().getComplexity() == HIGH) { - showMessageDialog(null, getString("complexity.warning"), "High complexity", INFORMATION_MESSAGE); - } + var c = (Calculation) selectedTask.getResponse(); + schemeTable.setPropertyHolder(c.getScheme()); + if (c.getProblem().getComplexity() == HIGH) { + showMessageDialog(null, getString("complexity.warning"), + "High complexity", INFORMATION_MESSAGE); + } + Executors.newSingleThreadExecutor().submit(() -> ProblemToolbar.plot(null)); + }); } private void changeProblems(Problem newlySelectedProblem, Object source) { var instance = TaskManager.getManagerInstance(); - var selectedTask = instance.getSelectedTask(); + var task = instance.getSelectedTask(); + var selectedCalc = ((Calculation) task.getResponse()); + + var problemLoaderTracker = new ProgressDialog(); + problemLoaderTracker.setTitle("Changing problem statements..."); + problemLoaderTracker.setLocationRelativeTo(null); + problemLoaderTracker.setAlwaysOnTop(true); + + List> callableList; if (source != instance) { + //apply to all tasks if (instance.isSingleStatement()) { - var callableList = instance.getTaskList().stream().map(t -> new Callable() { + callableList = instance.getTaskList().stream().map(t -> new Callable() { @Override public Problem call() throws Exception { changeProblem(t, newlySelectedProblem); - return t.getCurrentCalculation().getProblem(); + var result = ((Calculation) t.getResponse()).getProblem(); + problemLoaderTracker.incrementProgress(); + return result; } }).collect(Collectors.toList()); + + } //apply only to this task + else { + callableList = Arrays.asList(() -> { + changeProblem(task, newlySelectedProblem); + return ((Calculation) task.getResponse()).getProblem(); + }); + } + + problemLoaderTracker.trackProgress(callableList.size() - 1); + + CompletableFuture.runAsync(() -> { try { problemListExecutor.invokeAll(callableList); } catch (InterruptedException ex) { - Logger.getLogger(ProblemStatementFrame.class.getName()).log(Level.SEVERE, null, ex); + ex.printStackTrace(); } - - } else { - changeProblem(selectedTask, newlySelectedProblem); } + ).thenRun(() -> { + problemTable.setPropertyHolder(selectedCalc.getProblem()); + // after problem is selected for this task, show available difference schemes + var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); + defaultModel.clear(); + var schemes = newlySelectedProblem.availableSolutions(); + schemes.forEach(s -> defaultModel.addElement(s)); + selectDefaultScheme(schemeSelectionList, selectedCalc.getProblem()); + schemeSelectionList.setToolTipText(null); + }); } - problemTable.setPropertyHolder(selectedTask.getCurrentCalculation().getProblem()); - // after problem is selected for this task, show available difference schemes - var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); - defaultModel.clear(); - var schemes = newlySelectedProblem.availableSolutions(); - schemes.forEach(s -> defaultModel.addElement(s)); - selectDefaultScheme(schemeSelectionList, selectedTask.getCurrentCalculation().getProblem()); - schemeSelectionList.setToolTipText(null); - - Executors.newSingleThreadExecutor().submit(() -> ProblemToolbar.plot(null)); - } private void changeProblem(SearchTask task, Problem newProblem) { - var data = task.getExperimentalCurve(); - var calc = task.getCurrentCalculation(); + var data = (ExperimentalData) task.getInput(); + var calc = (Calculation) task.getResponse(); var oldProblem = calc.getProblem(); // stores previous information var np = newProblem.copy(); if (oldProblem != null) { np.initProperties(oldProblem.getProperties().copy()); np.getPulse().initFrom(oldProblem.getPulse()); + np.setBaseline(oldProblem.getBaseline()); + np.updateProperties(np, data.getMetadata()); } calc.setProblem(np, data); // copies information from old problem to new problem type + if (oldProblem == null) { + np.retrieveData(data); + } + task.checkProblems(true); toolbar.highlightButtons(!np.isReady()); - } private static void selectDefaultScheme(JList list, Problem p) { @@ -348,8 +396,8 @@ private static void selectDefaultScheme(JList list, Problem p) private void changeScheme(SearchTask task, DifferenceScheme newScheme) { // TODO - var calc = task.getCurrentCalculation(); - var data = task.getExperimentalCurve(); + var calc = (Calculation) task.getResponse(); + var data = (ExperimentalData) task.getInput(); if (calc.getScheme() == null) { calc.setScheme(newScheme.copy(), data); diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index 2d7edda..567c449 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -32,6 +32,7 @@ import pulse.search.direction.LMOptimiser; import pulse.search.direction.PathOptimiser; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.PropertyHolderTable; import pulse.ui.components.controllers.SearchListRenderer; @@ -132,15 +133,14 @@ public void update() { //model for the flags list already created if (rightTblModel instanceof SelectedKeysModel) { - var searchKeys = ActiveFlags.activeParameters(activeTask); + var searchKeys = activeTask.activeParameters(); ((ParameterTableModel)leftTable.getModel()).populateWithAllProperties(); ((SelectedKeysModel) rightTblModel).update(searchKeys); } //Create a new model for the flags list else { - if (activeTask != null - && activeTask.getCurrentCalculation() != null - && activeTask.getCurrentCalculation().getProblem() != null) { - var searchKeys = ActiveFlags.activeParameters(activeTask); + var c = (Calculation)activeTask.getResponse(); + if (c != null && c.getProblem() != null) { + var searchKeys = activeTask.activeParameters(); rightTable.setModel(new SelectedKeysModel(searchKeys, mandatorySelection)); /* diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 3bcccea..f04f050 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -113,8 +113,12 @@ private void initListeners() { @Override public void onProblemStatementShowRequest() { - problemStatementFrame.update(); - setProblemStatementFrameVisible(true); + if (TaskManager.getManagerInstance().getSelectedTask() != null) { + problemStatementFrame.update(); + setProblemStatementFrameVisible(true); + } else { + System.out.println("Please select a task"); + } } @Override diff --git a/src/main/java/pulse/util/Group.java b/src/main/java/pulse/util/Group.java index 07936b1..2b1f888 100644 --- a/src/main/java/pulse/util/Group.java +++ b/src/main/java/pulse/util/Group.java @@ -25,7 +25,9 @@ public List subgroups() { var methods = this.getClass().getMethods(); for (var m : methods) { - if (m.getParameterCount() > 0 || !Group.class.isAssignableFrom(m.getReturnType()) + + if (m.getParameterCount() > 0 + || !Group.class.isAssignableFrom(m.getReturnType()) || m.getReturnType().isAssignableFrom(getClass())) { continue; } @@ -39,7 +41,7 @@ public List subgroups() { e.printStackTrace(); } - /* Ignore null, factor/instance methods returning same accessibles */ + /* Ignore null, factory/instance methods returning same accessibles */ if (a == null || a.getDescriptor().equals(getDescriptor())) { continue; } diff --git a/src/main/java/pulse/util/UpwardsNavigable.java b/src/main/java/pulse/util/UpwardsNavigable.java index 70bc5e2..ab8e744 100644 --- a/src/main/java/pulse/util/UpwardsNavigable.java +++ b/src/main/java/pulse/util/UpwardsNavigable.java @@ -19,21 +19,21 @@ public abstract class UpwardsNavigable implements Descriptive { private UpwardsNavigable parent; - private List listeners = new ArrayList(); + private final List listeners = new ArrayList<>(); - public void removeHierarchyListeners() { + public final void removeHierarchyListeners() { this.listeners.clear(); } - public void removeHierarchyListener(HierarchyListener l) { + public final void removeHierarchyListener(HierarchyListener l) { this.listeners.remove(l); } - public void addHierarchyListener(HierarchyListener l) { + public final void addHierarchyListener(HierarchyListener l) { this.listeners.add(l); } - public List getHierarchyListeners() { + public final List getHierarchyListeners() { return listeners; } @@ -76,7 +76,6 @@ public UpwardsNavigable specificAncestor(Class aClas if (aClass.equals(this.getClass())) { return this; } - var parent = this.getParent(); UpwardsNavigable result = null; if (parent != null) { result = parent.getClass().equals(aClass) ? parent : parent.specificAncestor(aClass); From 00ba73373c4069dc48dc15d6246927442cee9ae0 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Fri, 9 Sep 2022 14:35:04 +0300 Subject: [PATCH 02/14] Pull update --- pom.xml | 2 +- .../pulse/math/filters/PolylineOptimiser.java | 2 +- .../transforms/StandardTransformations.java | 3 - src/main/resources/NumericProperty.xml | 46 +- src/main/resources/Version.txt | 2 +- src/main/resources/messages.properties | 4 +- src/main/resources/test/fft.txt | 1024 +++++++++++++++++ 7 files changed, 1069 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/test/fft.txt diff --git a/pom.xml b/pom.xml index ecbe713..e511886 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.95 + 1.97 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/math/filters/PolylineOptimiser.java b/src/main/java/pulse/math/filters/PolylineOptimiser.java index 0624698..dde0142 100644 --- a/src/main/java/pulse/math/filters/PolylineOptimiser.java +++ b/src/main/java/pulse/math/filters/PolylineOptimiser.java @@ -87,4 +87,4 @@ public double evaluate(double t) { } } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/math/transforms/StandardTransformations.java b/src/main/java/pulse/math/transforms/StandardTransformations.java index c5b9503..a8206fd 100644 --- a/src/main/java/pulse/math/transforms/StandardTransformations.java +++ b/src/main/java/pulse/math/transforms/StandardTransformations.java @@ -19,9 +19,6 @@ public class StandardTransformations { @Override public double transform(double a) { - if(a < 0) { - System.err.println(a); - } return log(a); } diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 3d4b61a..edf471e 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -21,21 +21,31 @@ dimensionfactor="1" discreet="false" keyword="MODEL_WEIGHT" maximum="1" minimum="0" primitive-type="double" value="0"> + + + + + maximum="5" minimum="0" primitive-type="double" value="0.1" default-search-variable="false"> + + - + + Diathermic Sample with Grey Walls (1D) LinearizedProblem2D.Descriptor=Classical 2D Problem Statement
  • Based on 1D formulation, except:
  • Allows heat losses from side surface
  • Allows radial heat flow
UniformlyCoatedSample.Descriptor=Core-Shell 2D Problem Statement
  • Based on the classical 2D problem, except:
  • Explicitly accounts for a coating that covers front, rear, and side surfaces
  • Allows for axial, radial, and circumferential heat fluxes
NonlinearProblem.Descriptor=Nonlinear Heat Sink (1D) Problem Statement
  • Precise calculation of heat losses (front and rear only)
  • Cp and ρ data required
+TwoTemperatureModel.Descriptor=Two-Temperature Penetration Model (1D) Problem Statement
  • Different temperatures for solid and gas phase
  • Energy exchange between phases
  • Extended light penetration
  • Cp and ρ data required
Problem.6=Laser Pulse Problem.7=Heating Curve DATReader.0=dat @@ -289,4 +290,5 @@ MixedScheme2.4=Increased Accuracy Semi-implicit Scheme
  • Order of approximation O(h4 + &tau2)
  • Unconditionally stable
  • Steps are computationally more expensive but their number is fewer compared to other schemes
  • Heat equation and BC are linear while RTE has a nonlinear emission term processed with a fixed iteration algorithm
TextWrap.0=

TextWrap.1=

-TextWrap.2=

\ No newline at end of file +TextWrap.2=

+msg.running=An instance of PULsE appears to be running. Please switch back to the running version or delete the pulse.lock file found in the PULsE directory. \ No newline at end of file diff --git a/src/main/resources/test/fft.txt b/src/main/resources/test/fft.txt new file mode 100644 index 0000000..194936e --- /dev/null +++ b/src/main/resources/test/fft.txt @@ -0,0 +1,1024 @@ +1.8623883939948125 +1.440191140077873 +0.0856911188761609 +0.3177405081698117 +0.10853351481231108 +0.11844768772657704 +0.14359180808670602 +0.09056412538979447 +0.10023722372433143 +0.05012708645229069 +0.06281734105869234 +0.049631166773682844 +0.05334748215923658 +0.07407767757133589 +0.03971008132098835 +0.04416964802114836 +0.040107973059110145 +0.033383489903923126 +0.053510587820655715 +0.029764726896007627 +0.028897396016806746 +0.028375440254179138 +0.02608494923958421 +0.02848092345249004 +0.03098610667355402 +0.019704730700161342 +0.02205971150558122 +0.021542133187751365 +0.021717439637497112 +0.020225860751051376 +0.019475979475189267 +0.014608596264644893 +0.01835870883217293 +0.017665011148277704 +0.01752138440835296 +0.015525900169506836 +0.015595410516248704 +0.015566570463898585 +0.015953238738805293 +0.01599587158897621 +0.013360695667264524 +0.015002089502846793 +0.014731537902388517 +0.013800515283259545 +0.014315157889845456 +0.013575152863046239 +0.012352341075970283 +0.012855780651818575 +0.012137860345243811 +0.012335928227005669 +0.011322310696797096 +0.011236261387870374 +0.011574641436270015 +0.011701043926932157 +0.011357150931082414 +0.009886326266142687 +0.009786382245722685 +0.010208387344388233 +0.009714230475277096 +0.009956827366738348 +0.009664725534625166 +0.009191624259773164 +0.009872561989745792 +0.009201888705471856 +0.008827948804843581 +0.008959893796151698 +0.00945443404186388 +0.008745592717618387 +0.009110905334280571 +0.008596123252820742 +0.008184929886419213 +0.008608854455027131 +0.008307320168098539 +0.008117648970713836 +0.008024758407971955 +0.007628751198035526 +0.007083062409124776 +0.007751030347403762 +0.007249154231345055 +0.007175520037985863 +0.00707064335177111 +0.00711871897750612 +0.007305437461793674 +0.007072070255306337 +0.006707680163861008 +0.006303859175803904 +0.007416579081448052 +0.0070619057661997445 +0.006854023369349145 +0.006455665945038045 +0.006835208781953551 +0.0065725374488924135 +0.006131013167403963 +0.006444591376811081 +0.0056394266037035545 +0.006066231052847979 +0.006303991327230198 +0.005949270797656248 +0.0063450056763406275 +0.006196167505123584 +0.005858153424762702 +0.0053913180909300855 +0.0058747255763572604 +0.006090643974924875 +0.005589115745925811 +0.005505074654993644 +0.005588245995291544 +0.005250392382850327 +0.005369156668180935 +0.005194106014717803 +0.005358717441455797 +0.005321043318596023 +0.005012422951166697 +0.005505553842319726 +0.0053432974110541155 +0.004971133806504812 +0.005037290685910062 +0.004765936894177503 +0.005141737892182391 +0.005410339758037131 +0.004885897265174704 +0.004922942306263837 +0.0045790685496497835 +0.00458229361635885 +0.004708660507341414 +0.0048042899663832475 +0.004625127808914531 +0.004743025594503575 +0.004598808630414814 +0.004626386499782302 +0.004459853592652658 +0.004377955368359365 +0.0046513163879235405 +0.0043256859045455115 +0.004512982427236372 +0.004588981897250833 +0.004295974465089781 +0.0043551312599052466 +0.004100096307988168 +0.004353555159813722 +0.00413193523732321 +0.004119654488401856 +0.0035986600134537334 +0.00430772639995229 +0.004058684144930945 +0.004194927813093337 +0.004135454329628857 +0.003707416777758124 +0.004024729209517794 +0.004139371147329603 +0.004167089013471642 +0.003761231388360764 +0.003952355678601933 +0.004066041106296513 +0.004136911700384993 +0.003932359264558167 +0.003542614608770456 +0.003746570018068218 +0.00395090631076722 +0.003537222144720005 +0.0035297026799832177 +0.003925348847332404 +0.0036757151959072244 +0.003602224343978626 +0.0036161028088550077 +0.0036307723158452575 +0.00368144084363016 +0.0036600294498210996 +0.00363782935999298 +0.003405756152194927 +0.003431252110629916 +0.0035649169954907745 +0.003236389910029101 +0.0034433473114116307 +0.0034932650584519062 +0.0035634560282200027 +0.0034296977806162633 +0.0034319461285292714 +0.003312308846263632 +0.003722618636988596 +0.0032536729682297472 +0.0031759378052392275 +0.00314708262406618 +0.003381194017340103 +0.0034226632338035484 +0.003201761963155763 +0.0034019628513818913 +0.0032004395904046603 +0.003102610835125887 +0.003323853973090416 +0.00301143619457104 +0.0033706851832347637 +0.002997554103076301 +0.0034025072727111427 +0.003206086938481629 +0.0031066962307518854 +0.002786437309626743 +0.0030835387904124883 +0.003031043385463557 +0.003011272769112621 +0.003026614277513354 +0.0031038064873766267 +0.002914867534237071 +0.0032106014711124264 +0.002960737750691456 +0.002848084333438608 +0.0029144693542782463 +0.002917497771325989 +0.0027129790526007544 +0.002612885902673647 +0.0029181831060856893 +0.0029055373553426543 +0.002714169194398066 +0.002863600775792233 +0.002684118883481437 +0.0028921148049106745 +0.0028302475704821105 +0.0028594112091045497 +0.0028091941754447122 +0.002761917483210222 +0.002831998224106552 +0.0029082049049603256 +0.0025433232627721726 +0.002744779635531415 +0.002610372762493025 +0.00269604297513204 +0.002522668560461302 +0.0027325824152729257 +0.0024340793422731313 +0.0026319425441122444 +0.002568112067959159 +0.0027683254158293864 +0.0024581501074080517 +0.00272139350102542 +0.0024097746095871524 +0.0026713014097986495 +0.002608528482915483 +0.002757952020574316 +0.002473940039239647 +0.0025698875343990245 +0.0024206272171112667 +0.002527643420140039 +0.002485348316653174 +0.002501106850185462 +0.0028036894150320207 +0.0023487069284162656 +0.002494732633732202 +0.002543346916671682 +0.002434448718146823 +0.002353903482083443 +0.0024849214295775495 +0.002280687444001982 +0.0021460731943987506 +0.0022744284159968734 +0.002355282996760084 +0.002374879375501902 +0.0025636565520782252 +0.0021429362225892585 +0.002512462546679564 +0.002457432015923903 +0.0022275839408261297 +0.002050220061718769 +0.0023897363016175868 +0.002280185276629757 +0.002551552138995319 +0.0020281057349859953 +0.0021776305132253397 +0.0021716442295787192 +0.0022024922812540998 +0.0022643433828360574 +0.0023171079460284592 +0.002209610418455076 +0.002091761906587569 +0.0024699303868929044 +0.0021924456147592924 +0.0025132238234213977 +0.0021413493664871534 +0.0023797515636802095 +0.002116464198887065 +0.0021010965208245326 +0.002200893207135162 +0.0022888366645033406 +0.0022506744392961795 +0.0020200323013424798 +0.0020269379217901146 +0.002235674958617841 +0.0021589634253941837 +0.002007694399579814 +0.002028468326489692 +0.001973178947286009 +0.0021722784832706923 +0.002152321588552352 +0.0020541661208640333 +0.0018443215090906614 +0.0023040970417671667 +0.0022070756163091905 +0.0021458535206313593 +0.0021234981725089773 +0.0020196842506682777 +0.002081325554159696 +0.0019622403312532185 +0.0018956765850871688 +0.002011758678159863 +0.002048584744472413 +0.001967968629034845 +0.0017890536700308419 +0.0018274200657948042 +0.0019495255858672576 +0.0019763604713690084 +0.002007665612620955 +0.002038019646262002 +0.0019323145911434045 +0.0018931617505309171 +0.0017176101935718177 +0.002035717753676598 +0.001997221376476268 +0.00208445761810857 +0.0018213990827714713 +0.0020335566706469687 +0.0019222800995692716 +0.0018730106550425844 +0.0018957021931545856 +0.0020131587529492274 +0.0019006911064656951 +0.0019006385154010802 +0.0018669836596750347 +0.0018387111891236968 +0.001860803645995648 +0.001875487905261806 +0.0019805330604071637 +0.0019333574999443324 +0.0017493923270269086 +0.0018635151864636905 +0.0018839497921766995 +0.001779170384859654 +0.0017369124667888725 +0.0018692706792106512 +0.0018555909898186722 +0.0019861219387481703 +0.0016768772425769986 +0.00173425585005027 +0.0017368104770272986 +0.0018444509218926088 +0.0018344780148905706 +0.0017421482008388048 +0.0019574501495635828 +0.0018485551997456673 +0.0016478397506516388 +0.0019006059767611814 +0.00178908230798387 +0.0017626449686498368 +0.001732149103600908 +0.0017420128347331559 +0.0016694600796223398 +0.0016892814923382899 +0.0017537019545246677 +0.0018336543315263922 +0.0018812436665203323 +0.0017781051942758364 +0.0016246718077691606 +0.0016673577866322385 +0.001759831952508731 +0.0017473637258567304 +0.00173104355552501 +0.001805140539073113 +0.0015973095237779042 +0.0015894420732170923 +0.001654768691098651 +0.0017328672602817553 +0.0018102375834557266 +0.0016478489084596247 +0.0016625501301660536 +0.001755668369649966 +0.0015363102784954816 +0.0017291672122388942 +0.0017614055969386695 +0.001711801977088871 +0.0016645828526783177 +0.0017291523364988042 +0.0017553659401122418 +0.0017300557169120786 +0.0016118911737583585 +0.0016013197537592275 +0.0017380041852213207 +0.0014933333031327699 +0.001559188637981803 +0.0017126653445467563 +0.0015807009808334307 +0.0016628973432414047 +0.0016047005870487945 +0.0017559387040599565 +0.0016243236914338785 +0.0015765644690180533 +0.0015828719203620945 +0.0015398177507824152 +0.0015032959585347633 +0.0016490341843532685 +0.001617396634735605 +0.0015957274117611951 +0.0015794738352842284 +0.0015388573781546031 +0.0016154513019219219 +0.0014816121634873749 +0.0015287905271643165 +0.0014617249768153456 +0.0015711554607759192 +0.0014682188208443925 +0.0015796458655902039 +0.001689735159818995 +0.001433704731383778 +0.0016166759882060668 +0.0014391892059119086 +0.0013712295490970739 +0.0016812167351267391 +0.0015319778370590715 +0.0014236368531158066 +0.0015087839252398833 +0.0016085941140473177 +0.0014820936165921505 +0.0014688802804647845 +0.0014354622391669247 +0.0015967583162015352 +0.0014216344753546506 +0.0015161040951119505 +0.0015096607386122969 +0.001384397608374938 +0.0015245746520588476 +0.0014081823605333326 +0.0016024041035034328 +0.001452871449554933 +0.0014308607906196105 +0.001522136599814874 +0.001481124047817481 +0.0015255580662159325 +0.001309843943187161 +0.0014586938481273024 +0.0014656459708439035 +0.0012726184983539426 +0.001367916494362877 +0.0013395100945080495 +0.0014389087123870105 +0.0015167920773429093 +0.0014722043865967177 +0.0014019882776771784 +0.0014049321693303425 +0.0013847814076527866 +0.0012478724504164254 +0.001459103728716397 +0.001406587374258575 +0.0014418827202656941 +0.0014096520586717536 +0.0014492409154258758 +0.001409394534879737 +0.0014705862502728947 +0.0012983605478659292 +0.001359781891861031 +0.0013736087171087959 +0.0015844689960084271 +0.001284557520511467 +0.0014926221038714146 +0.0013154469364888553 +0.0013159171219375757 +0.0013195726347157337 +0.0013224667986831112 +0.0013970767109930781 +0.0013792554501893374 +0.001302973911800565 +0.0014168082576272456 +0.0012576820832382774 +0.001542675666003404 +0.001307463747149029 +0.0012699109134222467 +0.0014539429326068782 +0.0013921268253928327 +0.0012829264103424237 +0.0014453813185194514 +0.0013361946194903225 +0.0012859127293610348 +0.001219864587298067 +0.0013729822159914303 +0.0013387240271219942 +0.001269166882994578 +0.0013816792929721383 +0.0013093666697172612 +0.0013326007495184746 +0.001334993151008571 +0.001235037425193347 +0.0011362030225542878 +0.0014377371705801458 +0.0013853954016113952 +0.0013471913518237851 +0.0012495283227950647 +0.001266562246997323 +0.0013227908159747438 +0.0012995488131381518 +0.0013904367286179792 +0.0012906469430955612 +0.0013315397480469341 +0.0012934821261761714 +0.0013250673575971874 +0.0013469150077112225 +0.0013202875781665269 +0.0012146174018993268 +0.00122881580627127 +0.0014129481339015366 +0.0011903810318464286 +0.0012449686959895507 +0.0012651176467834173 +0.0013046110131927327 +0.001237171432692225 +0.0011075767342333515 +0.0012758702965103852 +0.0011920086165592336 +0.0011866092015483018 +0.0012707921781073572 +0.001219579012657507 +0.0013616560614411427 +0.0012936725062240194 +0.00123301890710047 +0.0013025018941970037 +0.0011961146586798811 +0.0012808758077574244 +0.001190999826367067 +0.0012141207065111215 +0.0011843613569923832 +0.001335136462186405 +0.0011539681513207233 +0.0013632022792263935 +0.0013805744039978088 +0.0011881868755503888 +0.0011939077843169824 +0.0011270966583446721 +0.0012274172725854615 +0.0012672896373819246 +0.0012091250192438592 +0.001260421275046162 +0.0013151663232838697 +0.0012249004166542665 +0.00117899397310745 +0.0011734446482045359 +0.0012624596819513108 +0.0012764495812893665 +0.0010964856284655289 +0.001252761077726981 +0.0012529236282346054 +0.0012716297422796393 +0.0012861540922519037 +0.0011956481563782898 +0.0011121730631293352 +0.0012784895399416258 +0.0012202804971150033 +0.001045606751447345 +0.0013908900453571335 +0.001197171130379206 +0.0011705475688395964 +0.001269776840408273 +0.0011822952280807005 +0.0011066157789244133 +0.0012798660547691063 +0.0012809823962816405 +0.001109278093988003 +0.0011861159004747741 +0.0010835188502740056 +0.0011746191332898408 +0.001230977749834832 +0.0010795055496552108 +0.0012635419825219678 +0.0012756553936316552 +0.0011898806099122639 +0.0011608250848784943 +0.0012017305666559433 +0.001133310479022337 +0.0012358558242260653 +0.0011754317432781062 +0.0011033932179006088 +0.0011222424420222205 +0.0010375205291658766 +0.0012348160575316306 +0.0011826927315491012 +0.0010671410415516137 +0.0011711615448849848 +0.0011226251500075733 +0.00109441076055885 +0.001094864463168803 +0.0012022274437243083 +0.0011448479964942628 +0.001177852369182545 +0.0011002194983556388 +0.0011536796847610848 +0.0012020752603471268 +0.0010927386293982887 +0.00119823947194137 +0.0010685209532565016 +0.0011808365130782394 +0.0011372096566268058 +0.0011557260888021865 +0.0011304444819921215 +0.0011073395461831943 +0.0011146527005709973 +0.0011737068014750476 +0.001196673453050192 +0.001099569016246178 +0.0010796933440538885 +0.001043599943288889 +0.001153815061186113 +0.00131400849101086 +0.0011597212267390755 +0.0012451816466449049 +0.0010969945781010115 +0.0011004892551955102 +0.0011490190194486621 +0.0012283203493037387 +0.0010586820817033659 +0.0011891430899622388 +0.0011420664067813631 +0.0010229478025710005 +0.0010691985495450338 +0.0012006413404640646 +0.0011247227156228196 +0.0011907207706990607 +0.0010624252987184526 +0.0010228569653310151 +0.0011311460363274618 +0.0010831867629922491 +0.001078372575538281 +0.0011211665776477088 +0.0011605348662726803 +0.0010468906446104121 +0.001111427826815859 +0.0010188359106680675 +0.0011832758640857235 +0.0010596513093145692 +0.0011528677436051875 +0.0011282187354133004 +0.0010452271074060323 +0.0011074716717346302 +0.0011205642580078064 +9.867064467871667E-4 +0.001075272432523398 +9.872992855691574E-4 +0.0011157348160055031 +0.0011666183330647387 +0.001134882543661718 +0.0010855425213214857 +9.486135179973545E-4 +0.001022739916990013 +0.0011230768811140068 +0.0011034279475247624 +0.0011256491710883978 +0.0010678857770924672 +0.0010462878905578591 +0.001126936721613097 +0.0010518572445067302 +0.00100850428956523 +0.0011262068223730414 +0.0011005048073512705 +0.0011543348908842977 +0.0010939783368006336 +0.001022253007938055 +9.327735142685685E-4 +0.0011040492611731388 +0.001031427321278705 +0.0011174208044228532 +0.0010711547316751779 +0.0010263748860923343 +9.271388117916817E-4 +0.0010119339016748938 +0.0010729189004375487 +0.001017358352006335 +0.0012325543901770487 +0.0010559322713433975 +0.0010388280188947873 +0.0010112348874334633 +0.0011041523292409918 +0.001139488720022 +9.478706332814315E-4 +0.0010618470148916553 +0.0010535115515057064 +0.0011410732587352308 +0.0010390307941775675 +0.0011029457801466742 +0.00113809700552862 +0.0010230097035513453 +9.54459369975072E-4 +9.575993594545408E-4 +0.0010089179546782145 +0.0010742472276899845 +0.0010508183691489978 +0.001015706339611075 +9.63931367881156E-4 +9.892104999012375E-4 +0.0011395912499243275 +0.001027489144123793 +9.938574333827525E-4 +0.0010141638067099764 +0.0010850567240691322 +0.0010567209240592832 +0.0010546186206946042 +0.0010591449258845588 +0.0010788103431173524 +0.001001327795102536 +0.0010537475604692989 +9.776562827771178E-4 +0.0010301238724556479 +9.999903298850048E-4 +9.996640973755774E-4 +9.982351690180635E-4 +0.0010282243521071899 +9.793735100862085E-4 +9.937214763986947E-4 +0.0010560997662210044 +0.001064940358969091 +9.321880074986722E-4 +0.0010448277305721279 +9.336692191825015E-4 +0.001083214198713888 +0.0010289052566263427 +0.0010878579899422471 +9.628443269300946E-4 +9.148432953590753E-4 +0.0010009806857296107 +0.001064634768220053 +0.0010917456341344672 +0.001039660018309393 +9.640620972111818E-4 +9.473501095787988E-4 +8.898399478407448E-4 +0.0010752641810434242 +0.0010225882851613213 +0.001031186267761093 +0.001032388300708419 +9.692852639524189E-4 +0.0010636290585673226 +0.0011157012652095268 +9.445338312394765E-4 +9.796550027818063E-4 +9.380874632594835E-4 +9.171090871220963E-4 +0.0010338819871365346 +9.817352980271506E-4 +0.001035548610723382 +0.0010366089040039462 +0.0010023017609384433 +0.0010350418131665217 +0.0010078566274789762 +8.68331298898284E-4 +8.974661539826759E-4 +9.705357497685943E-4 +0.0010949970765370904 +9.495183412072032E-4 +9.737454775958202E-4 +0.0010217995814455307 +9.522448537898191E-4 +9.779127147104816E-4 +9.86232475982084E-4 +0.0010298898005799344 +0.0010131585301009062 +0.0010574032640673276 +9.48474613345645E-4 +0.0010620791774603989 +9.276998623420052E-4 +9.204849451858387E-4 +0.0010399404511545424 +9.264433947557337E-4 +9.089041102056008E-4 +8.985993184669925E-4 +0.0010315295220899943 +0.0010147607153646264 +9.377201100029269E-4 +9.537576069215683E-4 +0.0010009682437537631 +9.99695015129562E-4 +9.242726798056872E-4 +9.24635450522338E-4 +9.956002157419526E-4 +0.0010611750592811704 +0.0010763265510399733 +9.299215963015973E-4 +9.559145806317533E-4 +8.895396926441966E-4 +0.0010214904500228136 +0.0010665758733511713 +9.38328912522674E-4 +9.979567946888498E-4 +9.265333791378834E-4 +9.839671786840895E-4 +0.001028895479833451 +8.506542503867044E-4 +0.0010133621193351137 +8.787126554211259E-4 +9.963469146455363E-4 +9.615126345157179E-4 +9.85958165485008E-4 +9.00999168671934E-4 +9.22395160595154E-4 +9.847387915653623E-4 +9.550681987435555E-4 +0.0010157534718449773 +9.033087045773685E-4 +0.0010196936306393514 +9.886304333696812E-4 +9.621123908614798E-4 +9.961358429027196E-4 +9.185365383486628E-4 +9.802937997516852E-4 +0.0010325104413239838 +8.856756388963145E-4 +9.561355684135135E-4 +9.657359083146397E-4 +9.274753354606738E-4 +9.817087748994818E-4 +9.290601376764568E-4 +9.549805031394914E-4 +0.0010053984111506784 +0.0010171563035885468 +8.490199507419586E-4 +9.56482243200863E-4 +9.605373194334563E-4 +0.0010119157832749685 +8.622504120430778E-4 +9.827176707024432E-4 +9.373533932955863E-4 +9.675303640829435E-4 +0.0010249931272057681 +9.61126280284176E-4 +9.184629421665977E-4 +0.0010050193148524005 +9.787415813740507E-4 +8.972025579781401E-4 +9.687533583789295E-4 +8.925667325497291E-4 +9.642009967372524E-4 +9.053263729012275E-4 +9.921545067133972E-4 +8.908052742353586E-4 +9.467385459143673E-4 +8.734896034648221E-4 +0.001006776295415798 +9.648248427972446E-4 +0.0010178447964271453 +0.0010334043744897557 +9.623872532366822E-4 +8.08721329670184E-4 +8.981291571899335E-4 +8.965830799635602E-4 +9.411649803398181E-4 +9.233563139416925E-4 +9.427443681121117E-4 +9.942239823302458E-4 +9.088242408932741E-4 +9.540261346786916E-4 +9.741512437633195E-4 +9.305784543443058E-4 +8.936810810671528E-4 +8.922716098607077E-4 +9.078055011613465E-4 +9.380441154268395E-4 +9.293086318591539E-4 +9.975803217654006E-4 +8.729079377154703E-4 +8.944054849967194E-4 +9.603926066129352E-4 +9.359497708437436E-4 +8.584287049822975E-4 +9.084124785513469E-4 +9.690922729424672E-4 +9.577562696746035E-4 +8.787112588322701E-4 +9.25262999202752E-4 +9.069952161436504E-4 +8.553040119121029E-4 +9.441837660562317E-4 +8.881766062200255E-4 +9.182443930143199E-4 +9.210270704674465E-4 +0.0010207690003883872 +9.212511483642543E-4 +9.697071438700654E-4 +8.728908036006088E-4 +0.0010346924699559093 +9.98847382922486E-4 +9.243479071810528E-4 +8.86103374277685E-4 +8.924994979962734E-4 +8.810799504326165E-4 +0.0010062064982125846 +9.448553865605595E-4 +9.382799635233933E-4 +9.304974738814165E-4 +0.0010457539120059794 +8.357527786592987E-4 +9.217727903643321E-4 +9.179572581649916E-4 +9.168779812731295E-4 +9.622433504513817E-4 +9.32675306480942E-4 +8.553908706400959E-4 +9.83197977629328E-4 +8.380823090224836E-4 +9.098683129447309E-4 +9.521735462902145E-4 +9.234869018250256E-4 +9.101871270324486E-4 +9.571374853617023E-4 +9.014654684161298E-4 +9.454236030554885E-4 +8.743210842609578E-4 +0.001009335913540478 +8.942776328813976E-4 +9.46445773117879E-4 +9.261167832501032E-4 +9.36900494161455E-4 +0.0010312031257503273 +9.900728853623566E-4 +8.952595563375166E-4 +9.834533909209228E-4 +8.663718595624797E-4 +9.043261594713736E-4 +9.279968431679345E-4 +8.750985392750564E-4 +9.371156720928401E-4 +8.549939108391614E-4 +9.106702956319355E-4 +9.733885260125103E-4 +9.251988897112464E-4 +9.163237448419129E-4 +9.763413853652612E-4 +8.17850953219026E-4 +9.578846196673777E-4 +8.365217856229663E-4 +0.0010026936481388117 +7.928175701311635E-4 +8.893189554431385E-4 +9.017435287623222E-4 +8.783729172775835E-4 +8.669714891290667E-4 +8.950098150254109E-4 +9.102292374057512E-4 +8.678038819327828E-4 +9.072342094860982E-4 +9.204681033012889E-4 +9.140102265789373E-4 +9.679079074939713E-4 +8.97266035411225E-4 +9.253785287178447E-4 +8.87548440924595E-4 +8.982166442089941E-4 +9.331063397107384E-4 +9.793688190172947E-4 +8.571873137309112E-4 +7.653171049075817E-4 +9.437126053661114E-4 +9.130674200589031E-4 +9.921485769821788E-4 +9.309642233887024E-4 +9.060627123548954E-4 +8.980914435685737E-4 +9.652877389717347E-4 +9.047117932972571E-4 +9.667537814851369E-4 +9.535766035259877E-4 +8.941375274267972E-4 +8.984040549051778E-4 +8.652377377593744E-4 +9.20640529777493E-4 +9.180878222942158E-4 +9.559498677603602E-4 +9.036754182526869E-4 +8.948543811167884E-4 +8.577419937245519E-4 +9.328693608319587E-4 +9.448588934203852E-4 +8.874946478023135E-4 +9.201634267392057E-4 +8.218524536406166E-4 +9.554481063991601E-4 +8.727141872122932E-4 +0.0010440163697008847 +8.73836572999781E-4 +8.474705098276823E-4 +9.242788766551806E-4 +9.6628533033844E-4 +9.552653779506281E-4 +8.541963891119773E-4 +9.447177794880269E-4 +8.826859272186537E-4 +8.329850473356033E-4 +8.450126312152971E-4 +8.878943253854854E-4 +9.061636989099398E-4 +9.572319794719509E-4 +9.622037024296709E-4 +9.500723242444433E-4 +9.32405414821388E-4 +8.565420408414186E-4 +8.399901318746076E-4 +9.695118362994088E-4 +8.550086248119928E-4 +8.940615541457234E-4 +0.0010099358754753034 +9.203713767686853E-4 +8.886241205073025E-4 +9.380363126489836E-4 +8.853380504739757E-4 +9.403908197696909E-4 +8.631159768530599E-4 +9.595645004578227E-4 +9.477911078217122E-4 +8.587532061341414E-4 +9.110737725752171E-4 +8.817375500176881E-4 +8.506326823980858E-4 +0.0010001005385404018 +9.101279499969762E-4 +8.321551714226473E-4 +9.509451733575742E-4 +8.358075345119111E-4 +9.362701841463229E-4 +8.750119106584959E-4 +8.856057393331677E-4 +9.750659213866944E-4 +9.699331289208207E-4 +8.291897553302293E-4 +9.72959515072471E-4 \ No newline at end of file From 4cb45c7b3553843301d0f849e1bc583ef3b5cda1 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Fri, 9 Sep 2022 16:11:46 +0300 Subject: [PATCH 03/14] Updated splash screen --- src/main/resources/images/splash.png | Bin 67897 -> 67108 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png index d8ce89ad9ca02f2e8a043e095b8ab888cbce8813..c9583b6ef18dc2c1130fd57b2d9804f368219730 100644 GIT binary patch literal 67108 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 literal 67897 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 N@O1TaS?83{1OQJbFdhH^ From f1cb16876f68efd30db61bd4b3db6e6360fb702e Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Fri, 9 Sep 2022 16:11:46 +0300 Subject: [PATCH 04/14] Updated splash screen --- src/main/resources/images/splash.png | Bin 67897 -> 67108 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png index d8ce89ad9ca02f2e8a043e095b8ab888cbce8813..c9583b6ef18dc2c1130fd57b2d9804f368219730 100644 GIT binary patch literal 67108 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 literal 67897 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 N@O1TaS?83{1OQJbFdhH^ From 475d09b60f8ffbfcdba2fb774e8a59d830dc2874 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Sun, 25 Dec 2022 12:17:04 +0300 Subject: [PATCH 05/14] - Made an AbstractLogger superclass - Moved text logging capabilities to TextLogPaneExporter - Added reference to specific Calculation in log entries - Separated MVC functions in logging --- ...Exporter.java => TextLogPaneExporter.java} | 24 ++-- .../java/pulse/search/statistics/FTest.java | 2 +- .../java/pulse/tasks/logs/DataLogEntry.java | 1 + src/main/java/pulse/tasks/logs/Log.java | 13 ++ src/main/java/pulse/tasks/logs/LogEntry.java | 9 +- .../pulse/ui/components/AbstractLogger.java | 52 +++++++ .../java/pulse/ui/components/LogPane.java | 136 ------------------ .../java/pulse/ui/components/TextLogPane.java | 108 ++++++++++++++ src/main/java/pulse/ui/frames/LogFrame.java | 28 ++-- .../pulse/ui/frames/TaskControlFrame.java | 4 +- 10 files changed, 212 insertions(+), 165 deletions(-) rename src/main/java/pulse/io/export/{LogPaneExporter.java => TextLogPaneExporter.java} (67%) create mode 100644 src/main/java/pulse/ui/components/AbstractLogger.java delete mode 100644 src/main/java/pulse/ui/components/LogPane.java create mode 100644 src/main/java/pulse/ui/components/TextLogPane.java diff --git a/src/main/java/pulse/io/export/LogPaneExporter.java b/src/main/java/pulse/io/export/TextLogPaneExporter.java similarity index 67% rename from src/main/java/pulse/io/export/LogPaneExporter.java rename to src/main/java/pulse/io/export/TextLogPaneExporter.java index ade0e0f..e6ea39e 100644 --- a/src/main/java/pulse/io/export/LogPaneExporter.java +++ b/src/main/java/pulse/io/export/TextLogPaneExporter.java @@ -4,22 +4,23 @@ import java.io.FileOutputStream; import java.io.IOException; +import javax.swing.JEditorPane; import javax.swing.text.BadLocationException; import javax.swing.text.html.HTMLEditorKit; -import pulse.ui.components.LogPane; +import pulse.ui.components.TextLogPane; /** * Similar to a {@code LogExporter}, except that it works only on the contents * of a {@code LogPane} currently being displayed to the user. * */ -public class LogPaneExporter implements Exporter { +public class TextLogPaneExporter implements Exporter { - private static LogPaneExporter instance = new LogPaneExporter(); + private static TextLogPaneExporter instance = new TextLogPaneExporter(); - private LogPaneExporter() { + private TextLogPaneExporter() { // intentionally blank } @@ -29,10 +30,11 @@ private LogPaneExporter() { * argument is ignored. After exporting, the stream is explicitly closed. */ @Override - public void printToStream(LogPane pane, FileOutputStream fos, Extension extension) { - var kit = (HTMLEditorKit) pane.getEditorKit(); + public void printToStream(TextLogPane pane, FileOutputStream fos, Extension extension) { + var editorPane = (JEditorPane) pane.getGUIComponent(); + var kit = (HTMLEditorKit) editorPane.getEditorKit(); try { - kit.write(fos, pane.getDocument(), 0, pane.getDocument().getLength()); + kit.write(fos, editorPane.getDocument(), 0, editorPane.getDocument().getLength()); } catch (IOException | BadLocationException e) { System.err.println("Could not export the log pane!"); e.printStackTrace(); @@ -50,7 +52,7 @@ public void printToStream(LogPane pane, FileOutputStream fos, Extension extensio * * @return an instance of{@code LogPaneExporter}. */ - public static LogPaneExporter getInstance() { + public static TextLogPaneExporter getInstance() { return instance; } @@ -58,8 +60,8 @@ public static LogPaneExporter getInstance() { * @return {@code LogPane.class}. */ @Override - public Class target() { - return LogPane.class; + public Class target() { + return TextLogPane.class; } /** @@ -70,4 +72,4 @@ public Extension[] getSupportedExtensions() { return new Extension[]{HTML}; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/FTest.java b/src/main/java/pulse/search/statistics/FTest.java index 4d9da69..4723f5f 100644 --- a/src/main/java/pulse/search/statistics/FTest.java +++ b/src/main/java/pulse/search/statistics/FTest.java @@ -137,4 +137,4 @@ public static Calculation findNested(Calculation a, Calculation b) { return aParams > bParams ? b : a; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 2fad022..4e17c87 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -2,6 +2,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.List; +import pulse.Response; import pulse.math.Parameter; import pulse.math.ParameterIdentifier; import pulse.properties.NumericProperties; diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index 5b51953..1153499 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -1,6 +1,8 @@ package pulse.tasks.logs; import java.time.LocalTime; +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.SECONDS; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; @@ -186,5 +188,16 @@ public static boolean isVerbose() { public static void setVerbose(boolean verbose) { Log.verbose = verbose; } + + /** + * Time taken where the first array element contains seconds [0] and the second contains milliseconds [1]. + * @return an array of long values that sum um to the time taken to process a task + */ + + public long[] timeTaken() { + var seconds = SECONDS.between(getStart(), getEnd()); + var ms = MILLIS.between(getStart(), getEnd()) - 1000L * seconds; + return new long[] {seconds, ms}; + } } diff --git a/src/main/java/pulse/tasks/logs/LogEntry.java b/src/main/java/pulse/tasks/logs/LogEntry.java index 7841a91..bfd84fb 100644 --- a/src/main/java/pulse/tasks/logs/LogEntry.java +++ b/src/main/java/pulse/tasks/logs/LogEntry.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Objects; +import pulse.Response; import pulse.tasks.Identifier; import pulse.tasks.SearchTask; @@ -21,7 +22,8 @@ public class LogEntry { private Identifier identifier; private LocalTime time; - + private final Response response; + /** *

* Creates a {@code LogEntry} from this {@code SearchTask}. The data of the @@ -34,6 +36,11 @@ public LogEntry(SearchTask t) { Objects.requireNonNull(t, Messages.getString("LogEntry.NullTaskError")); time = LocalDateTime.now().toLocalTime(); identifier = t.getIdentifier(); + this.response = t.getResponse(); + } + + public Response getResponse() { + return response; } public Identifier getIdentifier() { diff --git a/src/main/java/pulse/ui/components/AbstractLogger.java b/src/main/java/pulse/ui/components/AbstractLogger.java new file mode 100644 index 0000000..916e678 --- /dev/null +++ b/src/main/java/pulse/ui/components/AbstractLogger.java @@ -0,0 +1,52 @@ +package pulse.ui.components; + +import java.util.concurrent.ExecutorService; +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import javax.swing.JComponent; +import pulse.tasks.TaskManager; +import pulse.tasks.logs.Log; +import pulse.tasks.logs.LogEntry; +import pulse.util.Descriptive; + +public abstract class AbstractLogger implements Descriptive { + + private final ExecutorService updateExecutor = newSingleThreadExecutor(); + + public synchronized void update() { + var task = TaskManager.getManagerInstance().getSelectedTask(); + + if (task == null) { + return; + } + + var log = task.getLog(); + + if (!log.isStarted()) { + return; + } + + post(log.lastEntry()); + } + + public ExecutorService getUpdateExecutor() { + return updateExecutor; + } + + public synchronized void callUpdate() { + updateExecutor.submit(() -> update()); + } + + public abstract JComponent getGUIComponent(); + public abstract void printTimeTaken(Log log); + public abstract void post(LogEntry logEntry); + public abstract void post(String text); + public abstract void postAll(); + public abstract void clear(); + public abstract boolean isEmpty(); + + @Override + public String describe() { + return "Log_" + TaskManager.getManagerInstance().getSelectedTask().getIdentifier().getValue(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/LogPane.java b/src/main/java/pulse/ui/components/LogPane.java deleted file mode 100644 index 5af4215..0000000 --- a/src/main/java/pulse/ui/components/LogPane.java +++ /dev/null @@ -1,136 +0,0 @@ -package pulse.ui.components; - -import static java.lang.System.err; -import static java.time.temporal.ChronoUnit.MILLIS; -import static java.time.temporal.ChronoUnit.SECONDS; -import static java.util.concurrent.Executors.newSingleThreadExecutor; -import static javax.swing.text.DefaultCaret.ALWAYS_UPDATE; -import static pulse.tasks.logs.Status.DONE; -import static pulse.ui.Messages.getString; - -import java.io.IOException; -import java.util.concurrent.ExecutorService; - -import javax.swing.JEditorPane; -import javax.swing.text.BadLocationException; -import javax.swing.text.DefaultCaret; -import javax.swing.text.html.HTMLDocument; -import javax.swing.text.html.HTMLEditorKit; - -import pulse.tasks.TaskManager; -import pulse.tasks.logs.Log; -import pulse.tasks.logs.LogEntry; -import pulse.util.Descriptive; - -@SuppressWarnings("serial") -public class LogPane extends JEditorPane implements Descriptive { - - private ExecutorService updateExecutor = newSingleThreadExecutor(); - - public LogPane() { - super(); - setContentType("text/html"); - setEditable(false); - var c = (DefaultCaret) getCaret(); - c.setUpdatePolicy(ALWAYS_UPDATE); - } - - private void post(LogEntry logEntry) { - post(logEntry.toString()); - } - - /* - private void postError(String text) { - var sb = new StringBuilder(); - sb.append(getString("DataLogEntry.FontTagError")); - sb.append(text); - sb.append(getString("DataLogEntry.FontTagClose")); - post(sb.toString()); - }*/ - private void post(String text) { - - final var doc = (HTMLDocument) getDocument(); - final var kit = (HTMLEditorKit) this.getEditorKit(); - try { - kit.insertHTML(doc, doc.getLength(), text, 0, 0, null); - } catch (BadLocationException e) { - err.println(getString("LogPane.InsertError")); //$NON-NLS-1$ - e.printStackTrace(); - } catch (IOException e) { - err.println(getString("LogPane.PrintError")); //$NON-NLS-1$ - e.printStackTrace(); - } - - } - - public void printTimeTaken(Log log) { - var seconds = SECONDS.between(log.getStart(), log.getEnd()); - var ms = MILLIS.between(log.getStart(), log.getEnd()) - 1000L * seconds; - var sb = new StringBuilder(); - sb.append(getString("LogPane.TimeTaken")); //$NON-NLS-1$ - sb.append(seconds + getString("LogPane.Seconds")); //$NON-NLS-1$ - sb.append(ms + getString("LogPane.Milliseconds")); //$NON-NLS-1$ - post(sb.toString()); - } - - public synchronized void callUpdate() { - updateExecutor.submit(() -> update()); - } - - public void printAll() { - clear(); - - var task = TaskManager.getManagerInstance().getSelectedTask(); - - if (task != null) { - - var log = task.getLog(); - - if (log.isStarted()) { - - log.getLogEntries().stream().forEach(entry -> post(entry)); - - if (task.getStatus() == DONE) { - printTimeTaken(log); - } - - } - - } - - } - - private synchronized void update() { - var task = TaskManager.getManagerInstance().getSelectedTask(); - - if (task == null) { - return; - } - - var log = task.getLog(); - - if (!log.isStarted()) { - return; - } - - post(log.lastEntry()); - } - - public void clear() { - try { - getDocument().remove(0, getDocument().getLength()); - } catch (BadLocationException e) { - e.printStackTrace(); - } - } - - public ExecutorService getUpdateExecutor() { - return updateExecutor; - } - - @Override - public String describe() { - return "Log_" + TaskManager.getManagerInstance().getSelectedTask().getIdentifier().getValue(); - } - -} diff --git a/src/main/java/pulse/ui/components/TextLogPane.java b/src/main/java/pulse/ui/components/TextLogPane.java new file mode 100644 index 0000000..aaf318a --- /dev/null +++ b/src/main/java/pulse/ui/components/TextLogPane.java @@ -0,0 +1,108 @@ +package pulse.ui.components; + +import static java.lang.System.err; +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.SECONDS; +import static javax.swing.text.DefaultCaret.ALWAYS_UPDATE; +import static pulse.tasks.logs.Status.DONE; +import static pulse.ui.Messages.getString; + +import java.io.IOException; +import javax.swing.JComponent; + +import javax.swing.JEditorPane; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultCaret; +import javax.swing.text.html.HTMLDocument; +import javax.swing.text.html.HTMLEditorKit; + +import pulse.tasks.TaskManager; +import pulse.tasks.logs.Log; +import pulse.tasks.logs.LogEntry; + +@SuppressWarnings("serial") +public class TextLogPane extends AbstractLogger { + + private final JEditorPane editor; + + public TextLogPane() { + editor = new JEditorPane(); + editor.setContentType("text/html"); + editor.setEditable(false); + ( (DefaultCaret) editor.getCaret() ).setUpdatePolicy(ALWAYS_UPDATE); + } + + @Override + public void post(LogEntry logEntry) { + post(logEntry.toString()); + } + + @Override + public void post(String text) { + + final var doc = (HTMLDocument) editor.getDocument(); + final var kit = (HTMLEditorKit) editor.getEditorKit(); + try { + kit.insertHTML(doc, doc.getLength(), text, 0, 0, null); + } catch (BadLocationException e) { + err.println(getString("LogPane.InsertError")); //$NON-NLS-1$ + } catch (IOException e) { + err.println(getString("LogPane.PrintError")); //$NON-NLS-1$ + } + + } + + + public void printTimeTaken(Log log) { + var time = log.timeTaken(); + var sb = new StringBuilder(); + sb.append(getString("LogPane.TimeTaken")); //$NON-NLS-1$ + sb.append(time[0]).append(getString("LogPane.Seconds")); //$NON-NLS-1$ + sb.append(time[1]).append(getString("LogPane.Milliseconds")); //$NON-NLS-1$ + post(sb.toString()); + } + + @Override + public void postAll() { + clear(); + + var task = TaskManager.getManagerInstance().getSelectedTask(); + + if (task != null) { + + var log = task.getLog(); + + if (log.isStarted()) { + + log.getLogEntries().stream().forEach(entry -> post(entry)); + + if (task.getStatus() == DONE) { + printTimeTaken(log); + } + + } + + } + + } + + @Override + public void clear() { + try { + editor.getDocument().remove(0, editor.getDocument().getLength()); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + + @Override + public JComponent getGUIComponent() { + return editor; + } + + @Override + public boolean isEmpty() { + return editor.getDocument().getLength() < 1; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index 6410fb7..4f45590 100644 --- a/src/main/java/pulse/ui/frames/LogFrame.java +++ b/src/main/java/pulse/ui/frames/LogFrame.java @@ -22,14 +22,15 @@ import pulse.tasks.listeners.LogEntryListener; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; -import pulse.ui.components.LogPane; +import pulse.ui.components.AbstractLogger; +import pulse.ui.components.TextLogPane; import pulse.ui.components.panels.LogToolbar; import pulse.ui.components.panels.SystemPanel; @SuppressWarnings("serial") public class LogFrame extends JInternalFrame { - private LogPane logTextPane; + private AbstractLogger logger; public LogFrame() { super("Log", true, false, true, true); @@ -39,9 +40,9 @@ public LogFrame() { } private void initComponents() { - logTextPane = new LogPane(); + logger = new TextLogPane(); var logScroller = new JScrollPane(); - logScroller.setViewportView(logTextPane); + logScroller.setViewportView(logger.getGUIComponent()); getContentPane().setLayout(new BorderLayout()); getContentPane().add(logScroller, CENTER); @@ -54,8 +55,8 @@ private void initComponents() { var logToolbar = new LogToolbar(); logToolbar.addLogExportListener(() -> { - if (logTextPane.getDocument().getLength() > 0) { - askToExport(logTextPane, (JFrame) getWindowAncestor(this), + if (!logger.isEmpty()) { + askToExport(logger, (JFrame) getWindowAncestor(this), getString("LogToolBar.FileFormatDescriptor")); } }); @@ -65,7 +66,7 @@ private void initComponents() { private void scheduleLogEvents() { var instance = TaskManager.getManagerInstance(); - instance.addSelectionListener(e -> logTextPane.printAll()); + instance.addSelectionListener(e -> logger.postAll()); instance.addTaskRepositoryListener(event -> { if (event.getState() != TASK_ADDED) { @@ -81,13 +82,12 @@ public void onLogFinished(Log log) { if (instance.getSelectedTask() == task) { try { - logTextPane.getUpdateExecutor().awaitTermination(10, MILLISECONDS); + logger.getUpdateExecutor().awaitTermination(10, MILLISECONDS); } catch (InterruptedException e) { err.println("Log not finished in time"); - e.printStackTrace(); } - logTextPane.printTimeTaken(log); + logger.printTimeTaken(log); } } @@ -95,7 +95,7 @@ public void onLogFinished(Log log) { @Override public void onNewEntry(LogEntry e) { if (instance.getSelectedTask() == task) { - logTextPane.callUpdate(); + logger.callUpdate(); } } @@ -105,8 +105,8 @@ public void onNewEntry(LogEntry e) { }); } - public LogPane getLogTextPane() { - return logTextPane; + public AbstractLogger getLogger() { + return logger; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index f04f050..880e10d 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -157,13 +157,13 @@ public void onRemoveRequest() { @Override public void onClearRequest() { - logFrame.getLogTextPane().clear(); + logFrame.getLogger().clear(); resultsFrame.getResultTable().clear(); } @Override public void onResetRequest() { - logFrame.getLogTextPane().clear(); + logFrame.getLogger().clear(); resultsFrame.getResultTable().removeAll(); } From 0434e4cb0b25d7104b646b6f09023e06808542f7 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Sun, 25 Dec 2022 12:22:38 +0300 Subject: [PATCH 06/14] Moved AbstractLogger to logs package --- .../pulse/{ui/components => tasks/logs}/AbstractLogger.java | 4 +--- src/main/java/pulse/ui/components/TextLogPane.java | 1 + src/main/java/pulse/ui/frames/LogFrame.java | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) rename src/main/java/pulse/{ui/components => tasks/logs}/AbstractLogger.java (93%) diff --git a/src/main/java/pulse/ui/components/AbstractLogger.java b/src/main/java/pulse/tasks/logs/AbstractLogger.java similarity index 93% rename from src/main/java/pulse/ui/components/AbstractLogger.java rename to src/main/java/pulse/tasks/logs/AbstractLogger.java index 916e678..bed70f7 100644 --- a/src/main/java/pulse/ui/components/AbstractLogger.java +++ b/src/main/java/pulse/tasks/logs/AbstractLogger.java @@ -1,11 +1,9 @@ -package pulse.ui.components; +package pulse.tasks.logs; import java.util.concurrent.ExecutorService; import static java.util.concurrent.Executors.newSingleThreadExecutor; import javax.swing.JComponent; import pulse.tasks.TaskManager; -import pulse.tasks.logs.Log; -import pulse.tasks.logs.LogEntry; import pulse.util.Descriptive; public abstract class AbstractLogger implements Descriptive { diff --git a/src/main/java/pulse/ui/components/TextLogPane.java b/src/main/java/pulse/ui/components/TextLogPane.java index aaf318a..e8f18ff 100644 --- a/src/main/java/pulse/ui/components/TextLogPane.java +++ b/src/main/java/pulse/ui/components/TextLogPane.java @@ -1,5 +1,6 @@ package pulse.ui.components; +import pulse.tasks.logs.AbstractLogger; import static java.lang.System.err; import static java.time.temporal.ChronoUnit.MILLIS; import static java.time.temporal.ChronoUnit.SECONDS; diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index 4f45590..996f1f3 100644 --- a/src/main/java/pulse/ui/frames/LogFrame.java +++ b/src/main/java/pulse/ui/frames/LogFrame.java @@ -22,7 +22,7 @@ import pulse.tasks.listeners.LogEntryListener; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; -import pulse.ui.components.AbstractLogger; +import pulse.tasks.logs.AbstractLogger; import pulse.ui.components.TextLogPane; import pulse.ui.components.panels.LogToolbar; import pulse.ui.components.panels.SystemPanel; From 18b3dd0e401be9a77a09cf4384609f847a7035bf Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Sat, 31 Dec 2022 18:59:57 +0300 Subject: [PATCH 07/14] Graphical log pane & new LAF - New Look and Feel (faster and better-looking) - Graphical logs - No "verbose" log option anymore ("on" by default in text mode) - Fixed glitches with GUI and task execution --- .../listeners/{LogExportListener.java => LogListener.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/pulse/ui/components/listeners/{LogExportListener.java => LogListener.java} (100%) diff --git a/src/main/java/pulse/ui/components/listeners/LogExportListener.java b/src/main/java/pulse/ui/components/listeners/LogListener.java similarity index 100% rename from src/main/java/pulse/ui/components/listeners/LogExportListener.java rename to src/main/java/pulse/ui/components/listeners/LogListener.java From a92b260eb75f56b9553a95fed552d04309f2417d Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Sat, 31 Dec 2022 18:59:57 +0300 Subject: [PATCH 08/14] Graphical log pane & new LAF - New Look and Feel (faster and better-looking) - Graphical logs - No "verbose" log option anymore ("on" by default in text mode) - Fixed glitches with GUI and task execution --- .../java/pulse/math/ParameterIdentifier.java | 31 ++- .../properties/NumericPropertyKeyword.java | 8 +- src/main/java/pulse/search/GeneralTask.java | 1 + .../pulse/search/direction/ComplexPath.java | 1 - .../direction/CompositePathOptimiser.java | 4 +- .../search/direction/IterativeState.java | 1 + .../pulse/search/direction/LMOptimiser.java | 4 +- .../direction/pso/ParticleSwarmOptimiser.java | 3 +- src/main/java/pulse/tasks/Calculation.java | 15 +- src/main/java/pulse/tasks/SearchTask.java | 7 +- .../java/pulse/tasks/logs/AbstractLogger.java | 66 ++++-- .../java/pulse/tasks/logs/DataLogEntry.java | 20 +- src/main/java/pulse/tasks/logs/Log.java | 18 +- .../java/pulse/tasks/processing/Buffer.java | 18 +- src/main/java/pulse/ui/ColorGenerator.java | 45 ++++ src/main/java/pulse/ui/Launcher.java | 8 +- .../java/pulse/ui/components/AuxPlotter.java | 87 +++++--- src/main/java/pulse/ui/components/Chart.java | 19 +- .../pulse/ui/components/GraphicalLogPane.java | 92 ++++++++ .../java/pulse/ui/components/LogChart.java | 199 ++++++++++++++++++ .../java/pulse/ui/components/PulseChart.java | 7 +- .../pulse/ui/components/ResidualsChart.java | 15 +- .../java/pulse/ui/components/TaskBox.java | 4 +- .../java/pulse/ui/components/TextLogPane.java | 34 +-- .../ui/components/buttons/LoaderButton.java | 27 ++- .../controllers/AccessibleTableRenderer.java | 2 - .../controllers/InstanceCellEditor.java | 14 +- .../controllers/NumericPropertyRenderer.java | 17 +- .../controllers/ProblemCellRenderer.java | 8 +- .../controllers/SearchListRenderer.java | 4 +- .../controllers/TaskTableRenderer.java | 3 - .../listeners/LogExportListener.java | 7 - .../ui/components/listeners/LogListener.java | 8 + .../ui/components/panels/ChartToolbar.java | 2 +- .../components/panels/DoubleTablePanel.java | 17 -- .../ui/components/panels/LogToolbar.java | 31 +-- .../ui/components/panels/ModelToolbar.java | 2 +- .../ui/components/panels/ProblemToolbar.java | 1 - src/main/java/pulse/ui/frames/LogFrame.java | 48 +++-- .../java/pulse/ui/frames/PreviewFrame.java | 10 +- .../pulse/ui/frames/SearchOptionsFrame.java | 2 - .../pulse/ui/frames/TaskControlFrame.java | 20 +- 42 files changed, 665 insertions(+), 265 deletions(-) create mode 100644 src/main/java/pulse/ui/ColorGenerator.java create mode 100644 src/main/java/pulse/ui/components/GraphicalLogPane.java create mode 100644 src/main/java/pulse/ui/components/LogChart.java delete mode 100644 src/main/java/pulse/ui/components/listeners/LogExportListener.java create mode 100644 src/main/java/pulse/ui/components/listeners/LogListener.java diff --git a/src/main/java/pulse/math/ParameterIdentifier.java b/src/main/java/pulse/math/ParameterIdentifier.java index b96847d..3fb7bc4 100644 --- a/src/main/java/pulse/math/ParameterIdentifier.java +++ b/src/main/java/pulse/math/ParameterIdentifier.java @@ -1,5 +1,6 @@ package pulse.math; +import java.util.Objects; import pulse.properties.NumericPropertyKeyword; public class ParameterIdentifier { @@ -15,6 +16,14 @@ public ParameterIdentifier(NumericPropertyKeyword keyword, int index) { public ParameterIdentifier(NumericPropertyKeyword keyword) { this(keyword, 0); } + + @Override + public int hashCode() { + int hash = 7; + hash = 29 * hash + Objects.hashCode(this.keyword); + hash = 29 * hash + this.index; + return hash; + } public ParameterIdentifier(int index) { this.index = index; @@ -30,24 +39,28 @@ public int getIndex() { @Override public boolean equals(Object id) { - if(!id.getClass().equals(ParameterIdentifier.class)) { + if(id.getClass() == null) { return false; } - var pid = (ParameterIdentifier) id; - - boolean result = true; + var classA = id.getClass(); + var classB = this.getClass(); - if(keyword != pid.keyword || index != pid.index) - result = false; - - return result; + if(classA != classB) { + return false; + } + var pid = (ParameterIdentifier) id; + return keyword == pid.keyword && Math.abs(index - pid.index) < 1; } @Override public String toString() { - return keyword + " # " + index; + StringBuilder sb = new StringBuilder("").append(keyword); + if(index > 0) { + sb.append(" # ").append(index); + } + return sb.toString(); } } \ No newline at end of file diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index a657171..36b1666 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -390,7 +390,13 @@ public enum NumericPropertyKeyword { * Heat loss for the gas in the 2T-model. */ - HEAT_LOSS_GAS; + HEAT_LOSS_GAS, + + /** + * Value of objective function. + */ + + OBJECTIVE_FUNCTION; public static Optional findAny(String key) { return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); diff --git a/src/main/java/pulse/search/GeneralTask.java b/src/main/java/pulse/search/GeneralTask.java index 44c64ac..3bd42d0 100644 --- a/src/main/java/pulse/search/GeneralTask.java +++ b/src/main/java/pulse/search/GeneralTask.java @@ -64,6 +64,7 @@ public GeneralTask() { @Override public void run() { setDefaultOptimiser(); + best = null; setIterativeState( optimiser.initState(this) ); var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index e6fc8a3..bfe1ef8 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -4,7 +4,6 @@ import pulse.math.linear.SquareMatrix; import pulse.search.GeneralTask; -import pulse.tasks.SearchTask; /** *

diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 7976646..48766c5 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -61,6 +61,7 @@ public boolean iteration(GeneralTask task) throws SolverException { } else { double initialCost = task.getResponse().objectiveFunction(task); + p.setCost(initialCost); var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -94,6 +95,7 @@ public boolean iteration(GeneralTask task) throws SolverException { task.storeState(); p.resetFailedAttempts(); this.prepare(task); // update gradients, Hessians, etc. -> for the next step, [k + 1] + p.setCost(newCost); p.incrementStep(); // increment the counter of successful steps } @@ -142,4 +144,4 @@ public GradientGuidedPath initState(GeneralTask t) { return new ComplexPath(t); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/IterativeState.java b/src/main/java/pulse/search/direction/IterativeState.java index 05fbaca..9045d46 100644 --- a/src/main/java/pulse/search/direction/IterativeState.java +++ b/src/main/java/pulse/search/direction/IterativeState.java @@ -41,6 +41,7 @@ public void setCost(double cost) { public void reset() { iteration = 0; + setCost(Double.POSITIVE_INFINITY); } public NumericProperty getIteration() { diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 4c3247a..86ee9e4 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -69,6 +69,7 @@ public boolean iteration(GeneralTask task) throws SolverException { } else { double initialCost = task.objectiveFunction(); + p.setCost(initialCost); var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -88,7 +89,7 @@ public boolean iteration(GeneralTask task) throws SolverException { parameters, candidate)); // assign new parameters double newCost = task.objectiveFunction(); // calculate the sum of squared residuals - + /* * Delayed gratification */ @@ -103,6 +104,7 @@ public boolean iteration(GeneralTask task) throws SolverException { p.resetFailedAttempts(); p.setLambda(p.getLambda() / 3.0); p.setComputeJacobian(false); + p.setCost(newCost); p.incrementStep(); // increment the counter of successful steps } diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java index 6f39f8b..323dfab 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -46,7 +46,8 @@ public boolean iteration(GeneralTask task) throws SolverException { swarmState.incrementStep(); task.assign(swarmState.getBestSoFar().getPosition()); - task.objectiveFunction(); + double cost = task.objectiveFunction(); + swarmState.setCost(cost); return true; } diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 51bc76e..5c673d3 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -34,6 +34,7 @@ import pulse.util.InstanceDescriptor; import pulse.util.PropertyEvent; import pulse.util.PropertyHolder; +import pulse.util.UpwardsNavigable; public class Calculation extends PropertyHolder implements Comparable, Response { @@ -78,14 +79,14 @@ public Calculation(Calculation c) { instanceDescriptor.addListener(() -> initModelCriterion(rs)); } - public void assumeOwnership() { - problem.setParent(this); - scheme.setParent(this); - rs.setParent(this); - os.setParent(this); - result.setParent(this); + public void conformTo(UpwardsNavigable owner) { + problem.setParent(owner); + scheme.setParent(owner); + rs.setParent(owner); + os.setParent(owner); + result.setParent(owner); } - + public void clear() { this.status = INCOMPLETE; this.problem = null; diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index bc8277a..91633c8 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -280,8 +280,7 @@ public void run() { } current.getProblem().parameterListChanged(); // get updated list of parameters - setDefaultOptimiser(); - + super.run(); } @@ -333,9 +332,11 @@ public void storeCalculation() { } public void switchTo(Calculation calc) { + current.setParent(null); + current.conformTo(null); current = new Calculation(calc); - current.assumeOwnership(); current.setParent(this); + calc.conformTo(calc); current.setStatus(Status.READY); var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); fireRepositoryEvent(e); diff --git a/src/main/java/pulse/tasks/logs/AbstractLogger.java b/src/main/java/pulse/tasks/logs/AbstractLogger.java index bed70f7..f128837 100644 --- a/src/main/java/pulse/tasks/logs/AbstractLogger.java +++ b/src/main/java/pulse/tasks/logs/AbstractLogger.java @@ -4,12 +4,17 @@ import static java.util.concurrent.Executors.newSingleThreadExecutor; import javax.swing.JComponent; import pulse.tasks.TaskManager; +import static pulse.tasks.logs.Status.DONE; import pulse.util.Descriptive; public abstract class AbstractLogger implements Descriptive { - private final ExecutorService updateExecutor = newSingleThreadExecutor(); - + private final ExecutorService updateExecutor; + + public AbstractLogger() { + updateExecutor = newSingleThreadExecutor(); + } + public synchronized void update() { var task = TaskManager.getManagerInstance().getSelectedTask(); @@ -19,32 +24,59 @@ public synchronized void update() { var log = task.getLog(); - if (!log.isStarted()) { - return; + if (log.isStarted()) { + post(log.lastEntry()); } - - post(log.lastEntry()); + } - + public ExecutorService getUpdateExecutor() { return updateExecutor; } - + public synchronized void callUpdate() { updateExecutor.submit(() -> update()); } - + + public void postAll() { + clear(); + + var task = TaskManager.getManagerInstance().getSelectedTask(); + + if (task != null) { + + var log = task.getLog(); + + if (log.isStarted()) { + + log.getLogEntries().stream().forEach(entry -> post(entry)); + + if (task.getStatus() == DONE) { + printTimeTaken(log); + } + + } + + } + + } + + @Override + public String describe() { + var task = TaskManager.getManagerInstance().getSelectedTask(); + return "Log" + (task == null ? "" : "_" + task.getIdentifier().getValue()); + } + public abstract JComponent getGUIComponent(); + public abstract void printTimeTaken(Log log); + public abstract void post(LogEntry logEntry); + public abstract void post(String text); - public abstract void postAll(); + public abstract void clear(); + public abstract boolean isEmpty(); - - @Override - public String describe() { - return "Log_" + TaskManager.getManagerInstance().getSelectedTask().getIdentifier().getValue(); - } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 4e17c87..c3c5ab3 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -2,10 +2,11 @@ import java.lang.reflect.InvocationTargetException; import java.util.List; -import pulse.Response; import pulse.math.Parameter; import pulse.math.ParameterIdentifier; import pulse.properties.NumericProperties; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericPropertyKeyword.OBJECTIVE_FUNCTION; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; @@ -55,10 +56,19 @@ public DataLogEntry(SearchTask task) { private void fill() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { var task = TaskManager.getManagerInstance().getTask(getIdentifier()); entry = task.searchVector().getParameters(); + //iteration var pval = task.getIterativeState().getIteration(); var pid = new Parameter(new ParameterIdentifier(pval.getType())); - pid.setValue( (int) pval.getValue() ); + pid.setValue((int) pval.getValue()); + //cost + var costId = new Parameter(new ParameterIdentifier(OBJECTIVE_FUNCTION)); + var costval = task.getIterativeState().getCost(); + // entry.add(0, pid); + if (NumericProperties.isValueSensible(def(OBJECTIVE_FUNCTION), costval)) { + costId.setValue(costval); + entry.add(costId); + } } public List getData() { @@ -91,15 +101,15 @@ public String toString() { var def = NumericProperties.def(p.getIdentifier().getKeyword()); boolean b = def.getValue() instanceof Integer; Number val; - if(b) { + if (b) { val = (int) Math.rint(p.getApparentValue()); - } else{ + } else { val = p.getApparentValue(); } def.setValue(val); sb.append(def.getAbbreviation(false)); int index = p.getIdentifier().getIndex(); - if(index > 0) { + if (index > 0) { sb.append(" - ").append(index); } sb.append("<

"); - sb.append(p.getAbbreviation(false)); + var def = NumericProperties.def(p.getIdentifier().getKeyword()); + boolean b = def.getValue() instanceof Integer; + Number val; + if(b) { + val = (int) Math.rint(p.getApparentValue()); + } else{ + val = p.getApparentValue(); + } + def.setValue(val); + sb.append(def.getAbbreviation(false)); + int index = p.getIdentifier().getIndex(); + if(index > 0) { + sb.append(" - ").append(index); + } sb.append(""); sb.append(Messages.getString("DataLogEntry.FontTagNumber")); //$NON-NLS-1$ sb.append(""); - sb.append(p.formattedOutput()); + sb.append(def.formattedOutput()); sb.append(""); sb.append(Messages.getString("DataLogEntry.FontTagClose")); //$NON-NLS-1$ sb.append("
"); diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index 1153499..21a32df 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -26,7 +26,7 @@ public class Log extends Group { private LocalTime end; private final Identifier id; private final List listeners; - private static boolean verbose = false; + private static boolean graphical = true; /** * Creates a {@code Log} for this {@code task} that will automatically store @@ -50,7 +50,7 @@ public Log(SearchTask task) { /** * Do these actions each time data has been collected for this task. */ - if (task.getStatus() != Status.INCOMPLETE && verbose) { + if (task.getStatus() != Status.INCOMPLETE) { logEntries.add(le); notifyListeners(le); } @@ -107,7 +107,7 @@ public final Identifier getIdentifier() { * @return {@code true} if the start time is not {@code null} */ public boolean isStarted() { - return start != null; + return logEntries.size() > 0; } /** @@ -175,18 +175,18 @@ public LogEntry lastEntry() { * * @return {@code true} if the verbose flag is on */ - public static boolean isVerbose() { - return verbose; + public static boolean isGraphicalLog() { + return graphical; } /** * Sets the verbose flag to {@code verbose} * * @param verbose the new value of the flag - * @see isVerbose() + * @see #isGraphicalLog() */ - public static void setVerbose(boolean verbose) { - Log.verbose = verbose; + public static void setGraphicalLog(boolean verbose) { + Log.graphical = verbose; } /** @@ -200,4 +200,4 @@ public long[] timeTaken() { return new long[] {seconds, ms}; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index e3a1684..90ef5bd 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -8,10 +8,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; +import org.apache.commons.math3.stat.regression.SimpleRegression; +import pulse.math.ParameterIdentifier; import pulse.math.ParameterVector; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.OBJECTIVE_FUNCTION; import pulse.properties.Property; import pulse.search.GeneralTask; import pulse.util.PropertyHolder; @@ -48,7 +52,7 @@ public ParameterVector[] getData() { /* * Re-inits the storage. */ - public void init() { + public final void init() { this.data = new ParameterVector[size]; statistic = new double[size]; } @@ -105,11 +109,17 @@ public double average(NumericPropertyKeyword index) { double av = 0; - for (ParameterVector v : data) { - av += v.getParameterValue(index, 0); + if (index == OBJECTIVE_FUNCTION) { + av = Arrays.stream(statistic).average().getAsDouble(); + } else { + + for (ParameterVector v : data) { + av += v.getParameterValue(index, 0); + } + av /= data.length; } - return av / data.length; + return av; } diff --git a/src/main/java/pulse/ui/ColorGenerator.java b/src/main/java/pulse/ui/ColorGenerator.java new file mode 100644 index 0000000..2e2388d --- /dev/null +++ b/src/main/java/pulse/ui/ColorGenerator.java @@ -0,0 +1,45 @@ +package pulse.ui; + +import java.awt.Color; +import static java.awt.Color.BLUE; +import static java.awt.Color.GREEN; +import static java.awt.Color.RED; +import java.util.ArrayList; +import java.util.Collections; + +public class ColorGenerator { + + private Color a, b, c; + + public ColorGenerator() { + a = RED; + b = GREEN; + c = BLUE; + } + + public Color[] random(int number) { + var list = new ArrayList(); + for(int i = 0; i < number; i++) { + list.add(sample(i/(double)(number - 1))); + } + //Collections.shuffle(list); + return list.toArray(new Color[list.size()]); + } + + public Color sample(double seed) { + return seed < 0.5 ? + mix(a, b, (float) (seed*2)) + : mix(b, c,(float)((seed-0.5)*2)); + } + + private static Color mix(Color a, Color b, float ratio) { + float[] aRgb = a.getRGBComponents(null); + float[] bRgb = b.getRGBComponents(null); + float[] cRgb = new float[3]; + for(int i = 0; i < cRgb.length; i++) { + cRgb[i] = aRgb[i] * (1.0f - ratio) + bRgb[i] * ratio; + } + return new Color(cRgb[0], cRgb[1], cRgb[2]); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index de97832..ee238ac 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -17,8 +17,7 @@ import javax.swing.JOptionPane; import javax.swing.UIManager; -import com.alee.laf.WebLookAndFeel; -import com.alee.skin.dark.WebDarkSkin; +import com.formdev.flatlaf.FlatDarkLaf; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -65,9 +64,10 @@ public static void main(String[] args) { splashScreen(); - WebLookAndFeel.install(WebDarkSkin.class); + //WebLookAndFeel.install(WebDarkSkin.class); + FlatDarkLaf.setup(); try { - UIManager.setLookAndFeel(new WebLookAndFeel()); + UIManager.setLookAndFeel(new FlatDarkLaf()); } catch (Exception ex) { System.err.println("Failed to initialize LaF"); } diff --git a/src/main/java/pulse/ui/components/AuxPlotter.java b/src/main/java/pulse/ui/components/AuxPlotter.java index 8150c4c..602a3d6 100644 --- a/src/main/java/pulse/ui/components/AuxPlotter.java +++ b/src/main/java/pulse/ui/components/AuxPlotter.java @@ -1,58 +1,85 @@ package pulse.ui.components; +import java.awt.Color; import java.awt.Font; +import javax.swing.JLabel; import javax.swing.UIManager; +import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; +import org.jfree.chart.plot.CombinedDomainXYPlot; +import static org.jfree.chart.plot.PlotOrientation.VERTICAL; import org.jfree.chart.plot.XYPlot; public abstract class AuxPlotter { - + private ChartPanel chartPanel; private JFreeChart chart; private XYPlot plot; - + + public AuxPlotter() { + //empty + } + public AuxPlotter(String xLabel, String yLabel) { - createChart(xLabel, yLabel); - chart.setBackgroundPaint(UIManager.getColor("Panel.background")); + setChart( ChartFactory.createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false) ); + + setPlot( chart.getXYPlot() ); + chart.removeLegend(); - plot = chart.getXYPlot(); setFonts(); - - chart.removeLegend(); - chartPanel = new ChartPanel(chart); } - - public void setFonts() { - var fontLabel = new Font("Arial", Font.PLAIN, 20); - var fontTicks = new Font("Arial", Font.PLAIN, 16); - var plot = getPlot(); - plot.getDomainAxis().setLabelFont(fontLabel); - plot.getDomainAxis().setTickLabelFont(fontTicks); - plot.getRangeAxis().setLabelFont(fontLabel); - plot.getRangeAxis().setTickLabelFont(fontTicks); + + public final void setFonts() { + var jlabel = new JLabel(); + var label = jlabel.getFont().deriveFont(20f); + var ticks = jlabel.getFont().deriveFont(16f); + chart.getTitle().setFont(jlabel.getFont().deriveFont(20f)); + + if (plot instanceof CombinedDomainXYPlot) { + var combinedPlot = (CombinedDomainXYPlot) plot; + combinedPlot.getSubplots().stream().forEach(sp -> setFontsForPlot((XYPlot)sp, label, ticks)); + } else { + setFontsForPlot(plot, label, ticks); + } + } - - public abstract void createChart(String xLabel, String yLabel); - + + private void setFontsForPlot(XYPlot p, Font label, Font ticks) { + var foreColor = UIManager.getColor("Label.foreground"); + var domainAxis = p.getDomainAxis(); + Chart.setAxisFontColor(domainAxis, foreColor); + var rangeAxis = p.getRangeAxis(); + Chart.setAxisFontColor(rangeAxis, foreColor); + } + public abstract void plot(T t); - - public ChartPanel getChartPanel() { + + public final ChartPanel getChartPanel() { return chartPanel; } - - public JFreeChart getChart() { + + public final JFreeChart getChart() { return chart; } - - public XYPlot getPlot() { + + public final XYPlot getPlot() { return plot; } - - public void setChart(JFreeChart chart) { + + public final void setPlot(XYPlot plot) { + this.plot = plot; + plot.setBackgroundPaint(chart.getBackgroundPaint()); + } + + public final void setChart(JFreeChart chart) { this.chart = chart; + var color = UIManager.getLookAndFeelDefaults().getColor("TextPane.background"); + chart.setBackgroundPaint(color); + chartPanel = new ChartPanel(chart); + this.plot = chart.getXYPlot(); } - -} + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index 68dda2d..7fe372e 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -26,6 +26,7 @@ import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.annotations.XYTitleAnnotation; +import org.jfree.chart.axis.Axis; import org.jfree.chart.block.BlockBorder; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; @@ -81,7 +82,7 @@ public Chart() { final TaskManager instance = TaskManager.getManagerInstance(); chart.removeLegend(); - chart.setBackgroundPaint(UIManager.getColor("Panel.background")); + chart.setBackgroundPaint(UIManager.getColor("TextPane.background")); chartPanel = new ChartPanel(chart) { @Override @@ -146,16 +147,18 @@ public double xCoord(MouseEvent e) { } private void setFonts() { - var fontLabel = new Font("Arial", Font.PLAIN, 20); - var fontTicks = new Font("Arial", Font.PLAIN, 14); - plot.getDomainAxis().setLabelFont(fontLabel); - plot.getDomainAxis().setTickLabelFont(fontTicks); - plot.getRangeAxis().setLabelFont(fontLabel); - plot.getRangeAxis().setTickLabelFont(fontTicks); + var foreColor = UIManager.getColor("Label.foreground"); + setAxisFontColor(plot.getDomainAxis(), foreColor); + setAxisFontColor(plot.getRangeAxis(), foreColor); + } + + public static void setAxisFontColor(Axis axis, Color color) { + axis.setLabelPaint(color); + axis.setTickLabelPaint(color); } private void setBackgroundAndGrid() { - // plot.setBackgroundPaint(UIManager.getColor("Panel.background")); + plot.setBackgroundPaint(UIManager.getColor("TextPane.background")); plot.setRangeGridlinesVisible(true); plot.setRangeGridlinePaint(GRAY); diff --git a/src/main/java/pulse/ui/components/GraphicalLogPane.java b/src/main/java/pulse/ui/components/GraphicalLogPane.java new file mode 100644 index 0000000..a50e494 --- /dev/null +++ b/src/main/java/pulse/ui/components/GraphicalLogPane.java @@ -0,0 +1,92 @@ +package pulse.ui.components; + +import javax.swing.JComponent; +import static pulse.properties.NumericPropertyKeyword.ITERATION; +import pulse.tasks.TaskManager; +import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.tasks.logs.AbstractLogger; +import pulse.tasks.logs.DataLogEntry; +import pulse.tasks.logs.Log; +import pulse.tasks.logs.LogEntry; +import static pulse.tasks.logs.Status.DONE; + +@SuppressWarnings("serial") +public class GraphicalLogPane extends AbstractLogger { + + private final LogChart chart; + + public GraphicalLogPane() { + chart = new LogChart(); + TaskManager.getManagerInstance().addTaskRepositoryListener( e -> { + if(e.getState() == TaskRepositoryEvent.State.TASK_SUBMITTED) { + chart.clear(); + } + }); + } + + @Override + public JComponent getGUIComponent() { + return chart.getChartPanel(); + } + + @Override + public void printTimeTaken(Log log) { + long[] time = log.timeTaken(); + StringBuilder sb = new StringBuilder("Finished in "); + sb.append(time[0]).append(" s ").append(time[1]).append(" ms."); + } + + @Override + public void post(LogEntry logEntry) { + if(logEntry instanceof DataLogEntry) { + var dle = (DataLogEntry) logEntry; + double iteration = dle.getData().stream() + .filter(p -> p.getIdentifier().getKeyword() == ITERATION) + .findAny().get().getApparentValue(); + chart.changeAxis(true); + chart.plot((DataLogEntry)logEntry, iteration); + } + } + + @Override + public void postAll() { + clear(); + + var task = TaskManager.getManagerInstance().getSelectedTask(); + + if (task != null) { + + var log = task.getLog(); + + if (log.isStarted()) { + + chart.clear(); + chart.changeAxis(false); + chart.plot(log); + + if (task.getStatus() == DONE) { + printTimeTaken(log); + } + + } + + } + + } + + @Override + public void post(String text) { + //not supported + } + + @Override + public void clear() { + chart.clear(); + } + + @Override + public boolean isEmpty() { + return false; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/LogChart.java b/src/main/java/pulse/ui/components/LogChart.java new file mode 100644 index 0000000..1f180ca --- /dev/null +++ b/src/main/java/pulse/ui/components/LogChart.java @@ -0,0 +1,199 @@ +package pulse.ui.components; + +import static java.util.Objects.requireNonNull; + +import java.awt.BasicStroke; +import java.awt.Color; +import static java.awt.Color.WHITE; +import static java.awt.Color.black; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import javax.swing.SwingUtilities; + +import org.jfree.chart.JFreeChart; +import org.jfree.chart.annotations.XYTitleAnnotation; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.axis.NumberTickUnit; +import org.jfree.chart.block.BlockBorder; +import org.jfree.chart.plot.CombinedDomainXYPlot; +import org.jfree.chart.plot.Plot; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.chart.title.LegendTitle; +import org.jfree.chart.ui.RectangleAnchor; +import org.jfree.chart.ui.RectangleEdge; +import org.jfree.data.Range; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; +import pulse.Response; +import pulse.math.ParameterIdentifier; + +import static pulse.properties.NumericPropertyKeyword.ITERATION; +import pulse.tasks.SearchTask; +import pulse.tasks.TaskManager; +import pulse.tasks.logs.DataLogEntry; +import pulse.tasks.logs.Log; +import pulse.tasks.logs.Status; +import pulse.tasks.processing.Buffer; +import pulse.ui.ColorGenerator; + +public class LogChart extends AuxPlotter { + + private final Map plots; + private Color[] colors; + private static final ColorGenerator cg = new ColorGenerator(); + private Response r; + + public LogChart() { + var plot = new CombinedDomainXYPlot(new NumberAxis("Iteration")); + plot.setGap(10.0); + plot.setOrientation(PlotOrientation.VERTICAL); + var chart = new JFreeChart("", JFreeChart.DEFAULT_TITLE_FONT, plot, true); + setChart(chart); + plots = new HashMap<>(); + getChart().removeLegend(); + } + + public final void clear() { + var p = (CombinedDomainXYPlot) getPlot(); + p.getDomainAxis().setAutoRange(true); + if (p != null) { + plots.values().stream().forEach(pp -> p.remove(pp)); + } + plots.clear(); + colors = new Color[0]; + r = null; + } + + private void setLegendTitle(Plot plot) { + var lt = new LegendTitle(plot); + lt.setBackgroundPaint(new Color(200, 200, 255, 100)); + lt.setFrame(new BlockBorder(black)); + lt.setPosition(RectangleEdge.RIGHT); + var ta = new XYTitleAnnotation(0.0, 0.8, lt, RectangleAnchor.LEFT); + ta.setMaxWidth(0.58); + ((XYPlot) plot).addAnnotation(ta); + } + + public final void add(ParameterIdentifier key, int no) { + var plot = new XYPlot(); + var axis = new NumberAxis(); + axis.setAutoRangeIncludesZero(false); + plot.setRangeAxis(axis); + + plot.setBackgroundPaint(getChart().getBackgroundPaint()); + + plots.put(key, plot); + ((CombinedDomainXYPlot) getPlot()).add(plot); + + var dataset = new XYSeriesCollection(); + var series = new XYSeries(key.toString()); + + dataset.addSeries(series); + dataset.addSeries(new XYSeries("Running average")); + plot.setDataset(dataset); + setLegendTitle(plot); + + setRenderer(plot, colors[no]); + setFonts(); + } + + private void setRenderer(XYPlot plt, Color clr) { + var renderer = new XYLineAndShapeRenderer(true, false); + renderer.setSeriesPaint(0, clr); + renderer.setSeriesPaint(1, WHITE); + var dashed = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, + BasicStroke.JOIN_MITER, 10.0f, new float[]{10.0f}, 0.0f); + renderer.setSeriesStroke(1, dashed); + renderer.setSeriesVisibleInLegend(1, Boolean.FALSE); + plt.setRenderer(renderer); + } + + public void changeAxis(boolean iterationMode) { + var domainAxis = (NumberAxis) getPlot().getDomainAxis(); + domainAxis.setLabel(iterationMode ? "Iteration" : "Time (ms)"); + domainAxis.setAutoRange(!iterationMode); + if(iterationMode) { + domainAxis.setTickUnit(new NumberTickUnit(1)); + } else { + domainAxis.setAutoTickUnitSelection(true); + } + } + + @Override + public void plot(Log l) { + requireNonNull(l); + + l.getLogEntries().stream() + .filter(le -> le instanceof DataLogEntry) + .forEach(d -> plot((DataLogEntry) d, + Duration.between(l.getStart(), d.getTime()).toMillis())); + } + + private static void adjustRange(XYPlot pl, int iteration, int bufSize) { + int lower = (iteration / bufSize) * bufSize; + + var domainAxis = pl.getDomainAxis(); + var r = domainAxis.getRange(); + var newR = new Range(lower, lower + bufSize); + + if (!r.equals(newR) && iteration > lower) { + ((XYPlot) pl).getDomainAxis().setRange(lower, lower + bufSize); + } + } + + public final void plot(DataLogEntry dle, double iterationOrTime) { + requireNonNull(dle); + + var data = dle.getData(); + int size = data.size(); + + if (colors == null || colors.length < size) { + colors = cg.random(size - 1); + } + + SearchTask task = TaskManager.getManagerInstance().getTask(dle.getIdentifier()); + Buffer buf = task.getBuffer(); + final int bufSize = buf.getData().length; + + for (int i = 0, j = 0; i < size; i++) { + var p = data.get(i); + var np = p.getIdentifier(); + + if (np.getKeyword() == ITERATION) { + continue; + } + + double value = p.getApparentValue(); + + if (!plots.containsKey(np)) { + add(np, j++); + } + + Plot pl = plots.get(np); + + var dataset = (XYSeriesCollection) ((XYPlot) pl).getDataset(); + XYSeries series = (XYSeries) dataset.getSeries(0); + series.add(iterationOrTime, value); + + if (task.getStatus() == Status.IN_PROGRESS) { + + XYSeries runningAverage = dataset.getSeries(1); + if (iterationOrTime > buf.getData().length - 1) { + runningAverage.add(iterationOrTime, buf.average(np.getKeyword())); + } + + SwingUtilities.invokeLater(() -> adjustRange((XYPlot)pl, (int)iterationOrTime, bufSize)); + + } else { + var domainAxis = ((XYPlot) pl).getDomainAxis(); + domainAxis.setAutoRange(true); + } + + } + + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/PulseChart.java b/src/main/java/pulse/ui/components/PulseChart.java index bb33646..db6e747 100644 --- a/src/main/java/pulse/ui/components/PulseChart.java +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -41,16 +41,11 @@ private void setRenderer() { getPlot().setRenderer(rendererPulse); } - @Override - public void createChart(String xLabel, String yLabel) { - setChart(ChartFactory.createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false)); - } - private void setLegendTitle() { var plot = getPlot(); var lt = new LegendTitle(plot); lt.setItemFont(new Font("Dialog", PLAIN, 16)); - //lt.setBackgroundPaint(new Color(200, 200, 255, 100)); + lt.setBackgroundPaint(new Color(200, 200, 255, 100)); lt.setFrame(new BlockBorder(black)); lt.setPosition(RectangleEdge.RIGHT); var ta = new XYTitleAnnotation(0.5, 0.2, lt, RectangleAnchor.CENTER); diff --git a/src/main/java/pulse/ui/components/ResidualsChart.java b/src/main/java/pulse/ui/components/ResidualsChart.java index 7a78ee2..7543707 100644 --- a/src/main/java/pulse/ui/components/ResidualsChart.java +++ b/src/main/java/pulse/ui/components/ResidualsChart.java @@ -1,9 +1,8 @@ package pulse.ui.components; import static java.util.Objects.requireNonNull; -import static org.jfree.chart.plot.PlotOrientation.VERTICAL; - import org.jfree.chart.ChartFactory; +import static org.jfree.chart.plot.PlotOrientation.VERTICAL; import org.jfree.data.statistics.HistogramDataset; import org.jfree.data.statistics.HistogramType; @@ -14,15 +13,13 @@ public class ResidualsChart extends AuxPlotter { private int binCount; public ResidualsChart(String xLabel, String yLabel) { - super(xLabel, yLabel); - binCount = 32; - } - - @Override - public void createChart(String xLabel, String yLabel) { setChart(ChartFactory.createHistogram("", xLabel, yLabel, null, VERTICAL, true, true, false)); + setPlot(getChart().getXYPlot()); + getChart().removeLegend(); + setFonts(); + binCount = 32; } - + @Override public void plot(ResidualStatistic stat) { requireNonNull(stat); diff --git a/src/main/java/pulse/ui/components/TaskBox.java b/src/main/java/pulse/ui/components/TaskBox.java index 9be85ed..13b8445 100644 --- a/src/main/java/pulse/ui/components/TaskBox.java +++ b/src/main/java/pulse/ui/components/TaskBox.java @@ -1,6 +1,5 @@ package pulse.ui.components; -import static java.awt.Color.WHITE; import static java.awt.event.ItemEvent.SELECTED; import static pulse.ui.Messages.getString; @@ -46,11 +45,10 @@ public TaskBox() { }); } - public void init() { + public final void init() { setMaximumSize(new Dimension(32767, 24)); setMinimumSize(new Dimension(250, 20)); setToolTipText(getString("TaskBox.DefaultText")); //$NON-NLS-1$ - setBackground(WHITE); } } diff --git a/src/main/java/pulse/ui/components/TextLogPane.java b/src/main/java/pulse/ui/components/TextLogPane.java index e8f18ff..f1b7dfc 100644 --- a/src/main/java/pulse/ui/components/TextLogPane.java +++ b/src/main/java/pulse/ui/components/TextLogPane.java @@ -2,22 +2,19 @@ import pulse.tasks.logs.AbstractLogger; import static java.lang.System.err; -import static java.time.temporal.ChronoUnit.MILLIS; -import static java.time.temporal.ChronoUnit.SECONDS; import static javax.swing.text.DefaultCaret.ALWAYS_UPDATE; -import static pulse.tasks.logs.Status.DONE; import static pulse.ui.Messages.getString; import java.io.IOException; import javax.swing.JComponent; import javax.swing.JEditorPane; +import javax.swing.JScrollPane; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultCaret; import javax.swing.text.html.HTMLDocument; import javax.swing.text.html.HTMLEditorKit; -import pulse.tasks.TaskManager; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; @@ -25,12 +22,15 @@ public class TextLogPane extends AbstractLogger { private final JEditorPane editor; + private final JScrollPane pane; public TextLogPane() { editor = new JEditorPane(); editor.setContentType("text/html"); editor.setEditable(false); ( (DefaultCaret) editor.getCaret() ).setUpdatePolicy(ALWAYS_UPDATE); + pane = new JScrollPane(); + pane.setViewportView(editor); } @Override @@ -63,30 +63,6 @@ public void printTimeTaken(Log log) { post(sb.toString()); } - @Override - public void postAll() { - clear(); - - var task = TaskManager.getManagerInstance().getSelectedTask(); - - if (task != null) { - - var log = task.getLog(); - - if (log.isStarted()) { - - log.getLogEntries().stream().forEach(entry -> post(entry)); - - if (task.getStatus() == DONE) { - printTimeTaken(log); - } - - } - - } - - } - @Override public void clear() { try { @@ -98,7 +74,7 @@ public void clear() { @Override public JComponent getGUIComponent() { - return editor; + return pane; } @Override diff --git a/src/main/java/pulse/ui/components/buttons/LoaderButton.java b/src/main/java/pulse/ui/components/buttons/LoaderButton.java index 46c9b43..cad1066 100644 --- a/src/main/java/pulse/ui/components/buttons/LoaderButton.java +++ b/src/main/java/pulse/ui/components/buttons/LoaderButton.java @@ -19,7 +19,6 @@ import java.io.File; import java.io.IOException; -import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.UIManager; @@ -27,7 +26,6 @@ import org.apache.commons.math3.exception.OutOfRangeException; import pulse.input.InterpolationDataset; -import pulse.ui.Messages; import pulse.util.ImageUtils; @SuppressWarnings("serial") @@ -36,8 +34,8 @@ public class LoaderButton extends JButton { private InterpolationDataset.StandartType dataType; private static File dir; - private final static Color NOT_HIGHLIGHTED = UIManager.getColor("Button.background"); - private final static Color HIGHLIGHTED = ImageUtils.blend(NOT_HIGHLIGHTED, Color.red, 0.75f); + private final static Color NOT_HIGHLIGHTED = UIManager.getColor("Button.background"); + private final static Color HIGHLIGHTED = ImageUtils.blend(NOT_HIGHLIGHTED, Color.red, 0.35f); public LoaderButton() { super(); @@ -49,7 +47,7 @@ public LoaderButton(String str) { init(); } - public void init() { + public final void init() { InterpolationDataset.addListener(e -> { if (dataType == e) { @@ -86,18 +84,17 @@ public void init() { showMessageDialog(getWindowAncestor((Component) arg0.getSource()), getString("LoaderButton.ReadError"), //$NON-NLS-1$ getString("LoaderButton.IOError"), //$NON-NLS-1$ ERROR_MESSAGE); - } - catch(OutOfRangeException ofre) { + } catch (OutOfRangeException ofre) { getDefaultToolkit().beep(); StringBuilder sb = new StringBuilder(getString("TextWrap.0")); - sb.append(getString("LoaderButton.OFRErrorDescriptor") ); + sb.append(getString("LoaderButton.OFRErrorDescriptor")); sb.append(ofre.getMessage()); sb.append(getString("LoaderButton.OFRErrorDescriptor2")); sb.append(getString("TextWrap.1")); - showMessageDialog(getWindowAncestor((Component) arg0.getSource()), - sb.toString(), + showMessageDialog(getWindowAncestor((Component) arg0.getSource()), + sb.toString(), getString("LoaderButton.OFRError"), //$NON-NLS-1$ - ERROR_MESSAGE); + ERROR_MESSAGE); } var size = getDataset(dataType).getData().size(); var label = ""; @@ -113,8 +110,10 @@ public void init() { default: throw new IllegalStateException("Unknown data type: " + dataType); } + StringBuilder sb = new StringBuilder(""); + sb.append(label).append(" data loaded! A total of ").append(size).append(" data points loaded."); showMessageDialog(getWindowAncestor((Component) arg0.getSource()), - "" + label + " data loaded! A total of " + size + " data points loaded.", + sb.toString(), "Data loaded", INFORMATION_MESSAGE); }); } @@ -124,11 +123,11 @@ public void setDataType(InterpolationDataset.StandartType dataType) { } public void highlight(boolean highlighted) { - setBorder(highlighted ? BorderFactory.createLineBorder(HIGHLIGHTED) : null); + setBackground(highlighted ? HIGHLIGHTED : NOT_HIGHLIGHTED); } public void highlightIfNeeded() { highlight(getDataset(dataType) == null); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java index b297d1a..04727c0 100644 --- a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java @@ -1,6 +1,5 @@ package pulse.ui.components.controllers; -import static java.awt.Color.RED; import java.awt.Component; import java.awt.Font; @@ -8,7 +7,6 @@ import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JTable; -import javax.swing.UIManager; import pulse.properties.Flag; import pulse.properties.NumericProperty; diff --git a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java index 526823f..e9611d7 100644 --- a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java +++ b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java @@ -1,13 +1,11 @@ package pulse.ui.components.controllers; -import com.alee.utils.swing.PopupMenuAdapter; import java.awt.Component; import java.awt.event.ItemEvent; import javax.swing.DefaultCellEditor; import javax.swing.JComboBox; import javax.swing.JTable; -import javax.swing.event.PopupMenuEvent; import pulse.util.InstanceDescriptor; @@ -39,17 +37,7 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean } } }); - - combobox.addPopupMenuListener(new PopupMenuAdapter() { - - @Override - public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { - fireEditingCanceled(); - } - - } - ); - + return combobox; } diff --git a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java index e655f9b..5f05afc 100644 --- a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java @@ -7,10 +7,8 @@ import javax.swing.JTable; import javax.swing.UIManager; import javax.swing.table.DefaultTableCellRenderer; -import pulse.math.Segment; import pulse.properties.NumericProperty; -import pulse.properties.Property; import pulse.properties.NumericPropertyFormatter; @SuppressWarnings("serial") @@ -29,25 +27,22 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole if (value instanceof NumericProperty) { var jtf = initTextField((NumericProperty) value, table.isRowSelected(row)); - if (table.getEditorComponent() != null) { result = jtf; } else { - result = new JLabel(jtf.getText(), JLabel.RIGHT); - jtf = null; + result = (JLabel) super.getTableCellRendererComponent(table, + jtf.getText(), isSelected, hasFocus, row, column); + ((JLabel) result).setHorizontalAlignment(RIGHT); } - } else { var superRenderer = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); superRenderer.setHorizontalAlignment(JLabel.LEFT); - superRenderer.setBackground( - isSelected - ? UIManager.getColor("JFormattedTextField.selectionBackground") - : UIManager.getColor("JFormattedTextField.background")); result = superRenderer; } + + result.setForeground(UIManager.getColor("List.foreground")); return result; } @@ -59,4 +54,4 @@ private static JFormattedTextField initTextField(NumericProperty np, boolean row return jtf; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java index 7c36077..4827624 100644 --- a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java @@ -4,20 +4,20 @@ import javax.swing.ImageIcon; import javax.swing.JTree; -import javax.swing.UIManager; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; -import com.alee.managers.icon.LazyIcon; +//import com.alee.managers.icon.LazyIcon; import pulse.problem.statements.Problem; import pulse.util.ImageUtils; +import static pulse.util.ImageUtils.loadIcon; @SuppressWarnings("serial") public class ProblemCellRenderer extends DefaultTreeCellRenderer { - private static ImageIcon defaultIcon = (ImageIcon) ((LazyIcon) UIManager.getIcon("Tree.leafIcon")).getIcon(); - + private static ImageIcon defaultIcon = loadIcon("leaf.png", 16); + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { diff --git a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java index e74951a..cfc884c 100644 --- a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java @@ -2,7 +2,6 @@ import static javax.swing.BorderFactory.createEmptyBorder; -import java.awt.Color; import java.awt.Component; import javax.swing.DefaultListCellRenderer; @@ -18,8 +17,7 @@ public Component getListCellRendererComponent(JList list, Object value, int i var renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); ((JComponent) renderer).setBorder(createEmptyBorder(10, 10, 10, 10)); - renderer.setForeground(isSelected ? Color.DARK_GRAY : Color.white); - + return renderer; } diff --git a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java index 85d3ba6..33ccdcb 100644 --- a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java @@ -3,16 +3,13 @@ import static java.awt.Font.BOLD; import java.awt.Component; -import java.awt.Font; import javax.swing.JLabel; import javax.swing.JTable; import pulse.properties.NumericProperty; -import pulse.properties.Property; import pulse.tasks.Identifier; import pulse.tasks.logs.Status; -import pulse.util.PropertyHolder; @SuppressWarnings("serial") public class TaskTableRenderer extends NumericPropertyRenderer { diff --git a/src/main/java/pulse/ui/components/listeners/LogExportListener.java b/src/main/java/pulse/ui/components/listeners/LogExportListener.java deleted file mode 100644 index 99a32ae..0000000 --- a/src/main/java/pulse/ui/components/listeners/LogExportListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package pulse.ui.components.listeners; - -public interface LogExportListener { - - public void onLogExportRequest(); - -} diff --git a/src/main/java/pulse/ui/components/listeners/LogListener.java b/src/main/java/pulse/ui/components/listeners/LogListener.java new file mode 100644 index 0000000..5a07f5d --- /dev/null +++ b/src/main/java/pulse/ui/components/listeners/LogListener.java @@ -0,0 +1,8 @@ +package pulse.ui.components.listeners; + +public interface LogListener { + + public void onLogExportRequest(); + public void onLogModeChanged(boolean graphical); + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index b4d5e54..dca6e62 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -38,7 +38,7 @@ public final class ChartToolbar extends JToolBar { private final static int ICON_SIZE = 16; - private List listeners; + private final List listeners; private RangeTextFields rtf; diff --git a/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java index 11a5c19..ebb6bf6 100644 --- a/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java +++ b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java @@ -1,18 +1,3 @@ -/* - * Copyright 2021 kotik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package pulse.ui.components.panels; import java.awt.GridBagConstraints; @@ -87,7 +72,6 @@ public void initComponents(JTable leftTable, String titleLeft, JTable rightTable var borderLeft = createTitledBorder(titleLeft); leftScroller.setBorder(borderLeft); - borderLeft.setTitleColor(java.awt.Color.WHITE); leftTable.setRowHeight(80); @@ -102,7 +86,6 @@ public void initComponents(JTable leftTable, String titleLeft, JTable rightTable var borderRight = createTitledBorder(titleRight); rightScroller.setBorder(borderRight); - borderRight.setTitleColor(java.awt.Color.WHITE); rightTable.setRowHeight(80); rightScroller.setViewportView(rightTable); diff --git a/src/main/java/pulse/ui/components/panels/LogToolbar.java b/src/main/java/pulse/ui/components/panels/LogToolbar.java index a9fd6a1..5db207b 100644 --- a/src/main/java/pulse/ui/components/panels/LogToolbar.java +++ b/src/main/java/pulse/ui/components/panels/LogToolbar.java @@ -1,7 +1,5 @@ package pulse.ui.components.panels; -import static pulse.tasks.logs.Log.isVerbose; -import static pulse.tasks.logs.Log.setVerbose; import static pulse.ui.Messages.getString; import static pulse.util.ImageUtils.loadIcon; @@ -14,13 +12,15 @@ import javax.swing.JCheckBox; import javax.swing.JToolBar; -import pulse.ui.components.listeners.LogExportListener; +import static pulse.tasks.logs.Log.setGraphicalLog; +import static pulse.tasks.logs.Log.isGraphicalLog; +import pulse.ui.components.listeners.LogListener; @SuppressWarnings("serial") public class LogToolbar extends JToolBar { private final static int ICON_SIZE = 16; - private List listeners; + private List listeners; public LogToolbar() { super(); @@ -35,24 +35,25 @@ public void initComponents() { var saveLogBtn = new JButton(loadIcon("save.png", ICON_SIZE, Color.white)); saveLogBtn.setToolTipText("Save"); - var verboseCheckBox = new JCheckBox(getString("LogToolBar.Verbose")); //$NON-NLS-1$ - verboseCheckBox.setSelected(isVerbose()); - verboseCheckBox.setHorizontalAlignment(CENTER); + var logmodeCheckbox = new JCheckBox(getString("LogToolBar.Verbose")); //$NON-NLS-1$ + logmodeCheckbox.setSelected(isGraphicalLog()); + logmodeCheckbox.setHorizontalAlignment(CENTER); - verboseCheckBox.addActionListener(event -> setVerbose(verboseCheckBox.isSelected())); + logmodeCheckbox.addActionListener(event -> { + boolean selected = logmodeCheckbox.isSelected(); + setGraphicalLog(selected); + listeners.stream().forEach(l -> l.onLogModeChanged(selected)); + }); - saveLogBtn.addActionListener(e -> notifyLog()); + saveLogBtn.addActionListener(e -> listeners.stream().forEach(l -> l.onLogExportRequest())); add(saveLogBtn); - add(verboseCheckBox); - } - public void notifyLog() { - listeners.stream().forEach(l -> l.onLogExportRequest()); + add(logmodeCheckbox); } - public void addLogExportListener(LogExportListener l) { + public void addLogListener(LogListener l) { listeners.add(l); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/panels/ModelToolbar.java b/src/main/java/pulse/ui/components/panels/ModelToolbar.java index 7c0b550..770734e 100644 --- a/src/main/java/pulse/ui/components/panels/ModelToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ModelToolbar.java @@ -18,7 +18,7 @@ @SuppressWarnings("serial") public class ModelToolbar extends JToolBar { - private final static int ICON_SIZE = 20; + private final static int ICON_SIZE = 16; public ModelToolbar() { super(); diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index 92e42ca..e06abd0 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -13,7 +13,6 @@ import java.awt.Component; import java.awt.GridLayout; import java.awt.event.ActionEvent; -import java.util.concurrent.Executors; import javax.swing.JButton; import javax.swing.JToolBar; diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index 996f1f3..7eeae66 100644 --- a/src/main/java/pulse/ui/frames/LogFrame.java +++ b/src/main/java/pulse/ui/frames/LogFrame.java @@ -6,7 +6,6 @@ import static java.awt.GridBagConstraints.WEST; import static java.lang.System.err; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static javax.swing.SwingUtilities.getWindowAncestor; import static pulse.io.export.ExportManager.askToExport; import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_ADDED; import static pulse.ui.Messages.getString; @@ -14,16 +13,17 @@ import java.awt.BorderLayout; import java.awt.GridBagConstraints; -import javax.swing.JFrame; import javax.swing.JInternalFrame; -import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; import pulse.tasks.TaskManager; import pulse.tasks.listeners.LogEntryListener; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; import pulse.tasks.logs.AbstractLogger; +import pulse.ui.components.GraphicalLogPane; import pulse.ui.components.TextLogPane; +import pulse.ui.components.listeners.LogListener; import pulse.ui.components.panels.LogToolbar; import pulse.ui.components.panels.SystemPanel; @@ -31,6 +31,8 @@ public class LogFrame extends JInternalFrame { private AbstractLogger logger; + private final static AbstractLogger graphical = new GraphicalLogPane(); + private final static AbstractLogger text = new TextLogPane(); public LogFrame() { super("Log", true, false, true, true); @@ -40,12 +42,10 @@ public LogFrame() { } private void initComponents() { - logger = new TextLogPane(); - var logScroller = new JScrollPane(); - logScroller.setViewportView(logger.getGUIComponent()); + logger = Log.isGraphicalLog() ? graphical : text; getContentPane().setLayout(new BorderLayout()); - getContentPane().add(logScroller, CENTER); + getContentPane().add(logger.getGUIComponent(), CENTER); var gridBagConstraints = new GridBagConstraints(); gridBagConstraints.anchor = WEST; @@ -54,12 +54,24 @@ private void initComponents() { getContentPane().add(new SystemPanel(), PAGE_END); var logToolbar = new LogToolbar(); - logToolbar.addLogExportListener(() -> { - if (!logger.isEmpty()) { - askToExport(logger, (JFrame) getWindowAncestor(this), - getString("LogToolBar.FileFormatDescriptor")); + + var lel = new LogListener() { + @Override + public void onLogExportRequest() { + if (logger == text) { + askToExport(logger, null, getString("LogToolBar.FileFormatDescriptor")); + } else { + System.out.println("To export the log entries, please switch to text mode first!"); + } } - }); + + @Override + public void onLogModeChanged(boolean graphical) { + SwingUtilities.invokeLater(() -> setGraphicalLogger(graphical)); + } + }; + + logToolbar.addLogListener(lel); getContentPane().add(logToolbar, NORTH); } @@ -109,4 +121,16 @@ public AbstractLogger getLogger() { return logger; } + private void setGraphicalLogger(boolean graphicalLog) { + var old = logger; + logger = graphicalLog ? graphical : text; + + if (old != logger) { + getContentPane().remove(old.getGUIComponent()); + getContentPane().add(logger.getGUIComponent(), BorderLayout.CENTER); + logger.postAll(); + } + + } + } \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/PreviewFrame.java b/src/main/java/pulse/ui/frames/PreviewFrame.java index 76a249d..508d5a7 100644 --- a/src/main/java/pulse/ui/frames/PreviewFrame.java +++ b/src/main/java/pulse/ui/frames/PreviewFrame.java @@ -15,6 +15,7 @@ import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; +import static java.awt.Color.WHITE; import java.awt.GridLayout; import java.awt.Rectangle; import java.util.ArrayList; @@ -46,6 +47,7 @@ import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import pulse.tasks.processing.ResultFormat; +import pulse.ui.components.Chart; @SuppressWarnings("serial") public class PreviewFrame extends JInternalFrame { @@ -93,7 +95,6 @@ private void init() { toolbar.add(selectX); selectXBox = new JComboBox<>(); - selectXBox.setFont(selectXBox.getFont().deriveFont(11)); toolbar.add(selectXBox); toolbar.add(new JSeparator()); @@ -102,7 +103,6 @@ private void init() { toolbar.add(selectY); selectYBox = new JComboBox<>(); - selectYBox.setFont(selectYBox.getFont().deriveFont(11)); toolbar.add(selectYBox); var drawSmoothBtn = new JToggleButton(); @@ -229,6 +229,9 @@ private static ChartPanel createEmptyPanel() { //plot.setRangeGridlinesVisible(false); //plot.setDomainGridlinesVisible(false); + var fore = UIManager.getColor("Label.foreground"); + plot.setDomainGridlinePaint(fore); + plot.getRenderer(1).setSeriesPaint(1, SMOOTH_COLOR); plot.getRenderer(0).setSeriesPaint(0, RESULT_COLOR); plot.getRenderer(0).setSeriesShape(0, @@ -244,6 +247,9 @@ private static ChartPanel createEmptyPanel() { cp.setMinimumDrawHeight(10); chart.setBackgroundPaint(UIManager.getColor("Panel.background")); + plot.setBackgroundPaint(chart.getBackgroundPaint()); + Chart.setAxisFontColor(plot.getDomainAxis(), fore); + Chart.setAxisFontColor(plot.getRangeAxis(), fore); return cp; } diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index 567c449..f638f4d 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -48,7 +48,6 @@ public class SearchOptionsFrame extends JInternalFrame { private final JTable rightTable; private final PathSolversList pathList; - private final static Font FONT = new Font(getString("PropertyHolderTable.FontName"), ITALIC, 16); private final static List pathSolvers = instancesOf(PathOptimiser.class); private final NumericPropertyKeyword[] mandatorySelection = new NumericPropertyKeyword[]{MAXTEMP}; @@ -196,7 +195,6 @@ public PathOptimiser getElementAt(int index) { } }); - setFont(FONT); setSelectionMode(SINGLE_SELECTION); setCellRenderer(new SearchListRenderer()); diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 880e10d..f56d6ed 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -55,6 +55,8 @@ public class TaskControlFrame extends JFrame { private InternalGraphFrame pulseFrame; private PulseMainMenu mainMenu; + + private final static int ICON_SIZE = 16; public static TaskControlFrame getInstance() { return instance; @@ -188,26 +190,26 @@ private void initComponents() { setJMenuBar(mainMenu); logFrame = new LogFrame(); - logFrame.setFrameIcon(loadIcon("log.png", 20, Color.white)); + logFrame.setFrameIcon(loadIcon("log.png", ICON_SIZE, Color.white)); resultsFrame = new ResultFrame(); - resultsFrame.setFrameIcon(loadIcon("result.png", 20, Color.white)); + resultsFrame.setFrameIcon(loadIcon("result.png", ICON_SIZE, Color.white)); previewFrame = new PreviewFrame(); - previewFrame.setFrameIcon(loadIcon("preview.png", 20, Color.white)); + previewFrame.setFrameIcon(loadIcon("preview.png", ICON_SIZE, Color.white)); taskManagerFrame = new TaskManagerFrame(); - taskManagerFrame.setFrameIcon(loadIcon("task_manager.png", 20, Color.white)); + taskManagerFrame.setFrameIcon(loadIcon("task_manager.png", ICON_SIZE, Color.white)); graphFrame = MainGraphFrame.getInstance(); - graphFrame.setFrameIcon(loadIcon("curves.png", 20, Color.white)); + graphFrame.setFrameIcon(loadIcon("curves.png", ICON_SIZE, Color.white)); problemStatementFrame = new ProblemStatementFrame(); - problemStatementFrame.setFrameIcon(loadIcon("heat_problem.png", 20, Color.white)); + problemStatementFrame.setFrameIcon(loadIcon("heat_problem.png", ICON_SIZE, Color.white)); modelFrame = new ModelSelectionFrame(); - modelFrame.setFrameIcon(loadIcon("stored.png", 20, Color.white)); + modelFrame.setFrameIcon(loadIcon("stored.png", ICON_SIZE, Color.white)); searchOptionsFrame = new SearchOptionsFrame(); - searchOptionsFrame.setFrameIcon(loadIcon("optimiser.png", 20, Color.white)); + searchOptionsFrame.setFrameIcon(loadIcon("optimiser.png", ICON_SIZE, Color.white)); pulseFrame = new InternalGraphFrame("Pulse Shape", new PulseChart("Time (ms)", "Laser Power (a. u.)")); - pulseFrame.setFrameIcon(loadIcon("pulse.png", 20, Color.white)); + pulseFrame.setFrameIcon(loadIcon("pulse.png", ICON_SIZE, Color.white)); pulseFrame.setVisible(false); /* From caf3237e7539d5bdd2a4c8130c17a5a0d2684413 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 9 Jan 2023 00:19:47 +0300 Subject: [PATCH 09/14] Pulse & logs - Better handling of pulse discretisation - Fixed graphical log updates - Better pulse visualisation --- pom.xml | 13 +- .../pulse/problem/laser/DiscretePulse.java | 172 +++++++++++------- .../pulse/problem/laser/DiscretePulse2D.java | 4 +- .../pulse/problem/laser/NumericPulse.java | 2 +- .../pulse/problem/laser/RectangularPulse.java | 4 +- .../pulse/problem/laser/TrapezoidalPulse.java | 2 +- .../problem/schemes/DifferenceScheme.java | 7 +- src/main/java/pulse/problem/schemes/Grid.java | 34 +--- .../java/pulse/problem/schemes/Grid2D.java | 1 - .../problem/statements/AdiabaticSolution.java | 2 +- .../pulse/problem/statements/Problem.java | 5 +- .../java/pulse/problem/statements/Pulse.java | 4 +- .../statements/model/ThermalProperties.java | 2 +- .../statistics/ModelSelectionCriterion.java | 29 +++ .../search/statistics/ResidualStatistic.java | 36 +++- src/main/java/pulse/tasks/Calculation.java | 55 +++--- .../java/pulse/tasks/logs/DataLogEntry.java | 2 +- src/main/java/pulse/tasks/logs/LogEntry.java | 20 +- .../pulse/ui/components/GraphicalLogPane.java | 8 +- .../java/pulse/ui/components/LogChart.java | 43 +++-- .../java/pulse/ui/components/PulseChart.java | 10 +- src/main/resources/NumericProperty.xml | 9 +- src/main/resources/Version.txt | 2 +- src/main/resources/images/leaf.png | Bin 0 -> 24476 bytes src/main/resources/messages.properties | 2 +- 25 files changed, 294 insertions(+), 174 deletions(-) create mode 100644 src/main/resources/images/leaf.png diff --git a/pom.xml b/pom.xml index e511886..9605d72 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.97 + 1.97b PULsE Processing Unit for Laser flash Experiments @@ -18,8 +18,7 @@ The Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0.txt - - + org.jfree @@ -27,11 +26,11 @@ 1.5.0 - com.weblookandfeel - weblaf-ui - 1.2.13 + com.formdev + flatlaf + 3.0 - + org.apache.commons commons-math3 3.6.1 diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index 51f5805..8891324 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -7,7 +7,10 @@ import pulse.problem.schemes.Grid; import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; import pulse.tasks.SearchTask; +import pulse.util.PropertyHolderListener; /** * A {@code DiscretePulse} is an object that acts as a medium between the @@ -20,9 +23,10 @@ public class DiscretePulse { private final Grid grid; private final Pulse pulse; - + private final ExperimentalData data; + private double widthOnGrid; - private double timeConversionFactor; + private double characteristicTime; private double invTotalEnergy; //normalisation factor /** @@ -49,29 +53,28 @@ public class DiscretePulse { */ public DiscretePulse(Problem problem, Grid grid) { this.grid = grid; - timeConversionFactor = problem.getProperties().timeFactor(); + characteristicTime = problem.getProperties().characteristicTime(); this.pulse = problem.getPulse(); Object ancestor = Objects.requireNonNull(problem.specificAncestor(SearchTask.class), "Problem has not been assigned to a SearchTask"); - ExperimentalData data = - (ExperimentalData) ( ((SearchTask) ancestor).getInput() ); - init(data); - + data = (ExperimentalData) (((SearchTask) ancestor).getInput()); + init(); + + PropertyHolderListener phl = e -> { + characteristicTime = problem.getProperties().characteristicTime(); + widthOnGrid = 0; + init(); + }; + pulse.addListener(e -> { - timeConversionFactor = problem.getProperties().timeFactor(); - init(data); + widthOnGrid = 0; + init(); }); - - } - - private void init(ExperimentalData data) { - widthOnGrid = 0; - recalculate(); - pulse.getPulseShape().init(data, this); - invTotalEnergy = 1.0/totalEnergy(); + problem.addListener(phl); + } /** @@ -91,39 +94,77 @@ public double laserPowerAt(double time) { * * @see pulse.problem.schemes.Grid.gridTime(double,double) */ - public final void recalculate() { - final double nominalWidth = ((Number) pulse.getPulseWidth().getValue()).doubleValue(); - final double resolvedWidth = timeConversionFactor / getWidthToleranceFactor(); + public final void init() { + final double nominalWidth = ((Number) pulse.getPulseWidth().getValue()).doubleValue(); + final double resolvedWidth = resolvedPulseWidthSeconds(); final double EPS = 1E-10; - + + double oldValue = widthOnGrid; + this.widthOnGrid = pulseWidthGrid(); + /** * The pulse is too short, which makes calculations too expensive. Can * we replace it with a rectangular pulse shape instead? */ - - if (nominalWidth < resolvedWidth - EPS && widthOnGrid < EPS) { + if (nominalWidth < resolvedWidth - EPS && oldValue < EPS) { //change shape to rectangular var shape = new RectangularPulse(); - pulse.setPulseShape(shape); - //change pulse width - setDiscreteWidth(resolvedWidth); + pulse.setPulseShape(shape); shape.init(null, this); - //adjust the pulse object to update the visualised pulse - } else if(nominalWidth > resolvedWidth + EPS) { - setDiscreteWidth(nominalWidth); - } - - invTotalEnergy = 1.0/totalEnergy(); - + } else { + pulse.getPulseShape().init(data, this); + } + + invTotalEnergy = 1.0 / totalEnergy(); } - + /** - * Calculates the total pulse energy using a numerical integrator.The - * normalisation factor is then equal to the inverse total energy. - * @return the total pulse energy, assuming sample area fully covered by the beam + * Optimises the {@code Grid} parameters so that the timestep is + * sufficiently small to enable accurate pulse correction. + *

+ * This can change the {@code tauFactor} and {@code tau} variables in the + * {@code Grid} object if {@code discretePulseWidth/(M - 1) < grid.tau}, + * where M is the required number of pulse calculations. + *

+ * + * @see PulseTemporalShape.getRequiredDiscretisation() */ + public double pulseWidthGrid() { + //minimum number of points for pulse calculation + int reqPoints = pulse.getPulseShape().getRequiredDiscretisation(); + //physical pulse width in time units + double experimentalWidth = (double) pulse.getPulseWidth().getValue(); + + //minimum resolved pulse width in time units for that specific problem + double resolvedWidth = resolvedPulseWidthSeconds(); + + double pWidth = Math.max(experimentalWidth, resolvedWidth); + + final double EPS = 1E-10; + double newTau = pWidth / characteristicTime / reqPoints; + + double result = 0; + + if (newTau < grid.getTimeStep() - EPS) { + double newTauFactor = (double) grid.getTimeFactor().getValue() / 2.0; + grid.setTimeFactor(derive(TAU_FACTOR, newTauFactor)); + result = pulseWidthGrid(); + } else { + result = grid.gridTime(pWidth, characteristicTime); + } + + return result; + } + + /** + * Calculates the total pulse energy using a numerical integrator.The + * normalisation factor is then equal to the inverse total energy. + * + * @return the total pulse energy, assuming sample area fully covered by the + * beam + */ public final double totalEnergy() { var pulseShape = pulse.getPulseShape(); @@ -140,27 +181,22 @@ public double integrand(double... vars) { } /** - * Gets the discrete dimensionless pulse width, which is a multiplier of the current - * grid timestep. The pulse width is converted to the dimensionless pulse width by - * dividing the real value by l2/a. + * Gets the discrete dimensionless pulse width, which is a multiplier of the + * current grid timestep. The pulse width is converted to the dimensionless + * pulse width by dividing the real value by l2/a. * * @return the dimensionless pulse width mapped to the grid. */ public double getDiscreteWidth() { return widthOnGrid; } - - private void setDiscreteWidth(double width) { - widthOnGrid = grid.gridTime(width, timeConversionFactor); - grid.adjustTimeStep(this); - } /** * Gets the physical {@code Pulse} * * @return the {@code Pulse} object */ - public Pulse getPulse() { + public Pulse getPhysicalPulse() { return pulse; } @@ -172,36 +208,38 @@ public Pulse getPulse() { public Grid getGrid() { return grid; } - + /** - * Gets the dimensional factor required to convert real time variable into - * a dimensional variable, defined in the {@code Problem} class + * Gets the dimensional factor required to convert real time variable into a + * dimensional variable, defined in the {@code Problem} class + * * @return the conversion factor */ - - public double getConversionFactor() { - return timeConversionFactor; + public double getCharacteristicTime() { + return characteristicTime; } - + /** - * Gets the minimal resolved pulse width defined by the {@code WIDTH_TOLERANCE_FACTOR} - * and the characteristic time given by the {@code getConversionFactor}. - * @return + * Gets the minimal resolved pulse width defined by the + * {@code WIDTH_TOLERANCE_FACTOR} and the characteristic time given by the + * {@code getConversionFactor}. + * + * @return */ - - public double resolvedPulseWidth() { - return timeConversionFactor / getWidthToleranceFactor(); + public double resolvedPulseWidthSeconds() { + return characteristicTime / getWidthToleranceFactor(); } - - /** - * Assuming a characteristic time is divided by the return value of this method - * and is set to the minimal resolved pulse width, shows how small a pulse width - * can be to enable finite pulse correction. - * @return the smallest fraction of a characteristic time resolved as a finite pulse. + + /** + * Assuming a characteristic time is divided by the return value of this + * method and is set to the minimal resolved pulse width, shows how small a + * pulse width can be to enable finite pulse correction. + * + * @return the smallest fraction of a characteristic time resolved as a + * finite pulse. */ - public int getWidthToleranceFactor() { return WIDTH_TOLERANCE_FACTOR; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/DiscretePulse2D.java b/src/main/java/pulse/problem/laser/DiscretePulse2D.java index 00f884f..02a60f0 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse2D.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse2D.java @@ -26,7 +26,7 @@ public class DiscretePulse2D extends DiscretePulse { * This had to be decreased for the 2d pulses. */ - private final static int WIDTH_TOLERANCE_FACTOR = 2000; + private final static int WIDTH_TOLERANCE_FACTOR = 1000; /** * The constructor for {@code DiscretePulse2D}. @@ -90,7 +90,7 @@ private void calcPulseSpot(ExtendedThermalProperties properties) { * @see pulse.problem.schemes.Grid2D.gridRadialDistance(double,double) */ public final void evalPulseSpot() { - var pulse = (Pulse2D) getPulse(); + var pulse = (Pulse2D) getPhysicalPulse(); var grid2d = (Grid2D) getGrid(); final double spotRadius = (double) pulse.getSpotDiameter().getValue() / 2.0; discretePulseSpot = grid2d.gridRadialDistance(spotRadius, sampleRadius); diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java index e834f02..9dbaadb 100644 --- a/src/main/java/pulse/problem/laser/NumericPulse.java +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -64,7 +64,7 @@ public void init(ExperimentalData data, DiscretePulse pulse) { setPulseWidthOf(problem); //convert to dimensionless time and interpolate - double timeFactor = problem.getProperties().timeFactor(); + double timeFactor = problem.getProperties().characteristicTime(); doInterpolation(timeFactor); } diff --git a/src/main/java/pulse/problem/laser/RectangularPulse.java b/src/main/java/pulse/problem/laser/RectangularPulse.java index 2ba6b9d..583a92e 100644 --- a/src/main/java/pulse/problem/laser/RectangularPulse.java +++ b/src/main/java/pulse/problem/laser/RectangularPulse.java @@ -14,7 +14,7 @@ */ public class RectangularPulse extends PulseTemporalShape { - private final static int MIN_POINTS = 4; + private final static int MIN_POINTS = 2; /** * @param time the time measured from the start of the laser pulse. @@ -41,4 +41,4 @@ public int getRequiredDiscretisation() { return MIN_POINTS; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java index c305b81..a061a87 100644 --- a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java +++ b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java @@ -22,7 +22,7 @@ public class TrapezoidalPulse extends PulseTemporalShape { private double fall; private double h; - private final static int MIN_POINTS = 6; + private final static int MIN_POINTS = 8; /** * Constructs a trapezoidal pulse using a default segmentation principle. diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 59c9220..8ad8dfd 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -1,5 +1,6 @@ package pulse.problem.schemes; +import java.util.Objects; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; @@ -92,7 +93,7 @@ protected void prepare(Problem problem) throws SolverException { if (discretePulse == null) { discretePulse = problem.discretePulseOn(grid); } - discretePulse.recalculate(); + discretePulse.init(); clearArrays(); } @@ -114,13 +115,13 @@ public void runTimeSequence(Problem problem, final double offset, final double e int numPoints = (int) curve.getNumPoints().getValue(); final double startTime = (double) curve.getTimeShift().getValue(); - final double timeSegment = (endTime - startTime - offset) / problem.getProperties().timeFactor(); + final double timeSegment = (endTime - startTime - offset) / problem.getProperties().characteristicTime(); double tau = grid.getTimeStep(); final double dt = timeSegment / (numPoints - 1); timeInterval = Math.max( (int) (dt / tau), 1); - double wFactor = timeInterval * tau * problem.getProperties().timeFactor(); + double wFactor = timeInterval * tau * problem.getProperties().characteristicTime(); // First point (index = 0) is always (0.0, 0.0) curve.addPoint(0.0, 0.0); diff --git a/src/main/java/pulse/problem/schemes/Grid.java b/src/main/java/pulse/problem/schemes/Grid.java index f68467b..2d7128d 100644 --- a/src/main/java/pulse/problem/schemes/Grid.java +++ b/src/main/java/pulse/problem/schemes/Grid.java @@ -11,6 +11,7 @@ import java.util.Set; import pulse.problem.laser.DiscretePulse; +import pulse.problem.statements.Pulse; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.util.PropertyHolder; @@ -63,37 +64,6 @@ public Grid copy() { return new Grid(getGridDensity(), getTimeFactor()); } - /** - * Optimises the {@code Grid} parameters so that the timestep is - * sufficiently small to enable accurate pulse correction. - *

- * This can change the {@code tauFactor} and {@code tau} variables in the - * {@code Grid} object if {@code discretePulseWidth/(M - 1) < grid.tau}, - * where M is the required number of pulse calculations. - *

- * - * @param pulse the discrete pulse representation - * @see PulseTemporalShape.getRequiredDiscretisation() - */ - public final void adjustTimeStep(DiscretePulse pulse) { - double timeFactor = pulse.getConversionFactor(); - - final int reqPoints = pulse.getPulse().getPulseShape().getRequiredDiscretisation(); - - double pNominalWidth = (double) pulse.getPulse().getPulseWidth().getValue(); - double pResolvedWidth = pulse.resolvedPulseWidth(); - double pWidth = pNominalWidth < pResolvedWidth ? pResolvedWidth : pNominalWidth; - - double newTau = pWidth / timeFactor / (reqPoints > 1 ? reqPoints - 1 : 1); - double newTauFactor = newTau / (hx * hx); - - final double EPS = 1E-10; - if (newTauFactor < tauFactor - EPS) { - setTimeFactor(derive(TAU_FACTOR, newTauFactor)); - } - - } - /** * The listed properties include {@code GRID_DENSITY} and * {@code TAU_FACTOR}. @@ -223,7 +193,7 @@ public void setTimeFactor(NumericProperty timeFactor) { * @return a double representing the time on the finite grid */ public final double gridTime(double time, double dimensionFactor) { - return rint((time / dimensionFactor) / tau) * tau; + return ( (int) (time / dimensionFactor / tau) ) * tau; } /** diff --git a/src/main/java/pulse/problem/schemes/Grid2D.java b/src/main/java/pulse/problem/schemes/Grid2D.java index 6b10419..af2284b 100644 --- a/src/main/java/pulse/problem/schemes/Grid2D.java +++ b/src/main/java/pulse/problem/schemes/Grid2D.java @@ -68,7 +68,6 @@ public void adjustStepSize(DiscretePulse pulse) { adjustStepSize(pulse); } - adjustTimeStep(pulse); } @Override diff --git a/src/main/java/pulse/problem/statements/AdiabaticSolution.java b/src/main/java/pulse/problem/statements/AdiabaticSolution.java index a335fac..5fbb5a8 100644 --- a/src/main/java/pulse/problem/statements/AdiabaticSolution.java +++ b/src/main/java/pulse/problem/statements/AdiabaticSolution.java @@ -75,7 +75,7 @@ public static HeatingCurve classicSolution(Problem p, double timeLimit, int prec private final static double solutionAt(ThermalProperties p, double time, int precision) { final double EPS = 1E-8; - final double Fo = time / p.timeFactor(); + final double Fo = time / p.characteristicTime(); if (time < EPS) { return 0; diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index f228515..a9752e2 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -6,6 +6,7 @@ import static pulse.properties.NumericPropertyKeyword.TIME_SHIFT; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Executors; import java.util.stream.Collectors; @@ -245,7 +246,7 @@ public void optimisationVector(ParameterVector output) { p.setTransform(new StickTransform(bounds)); break; case TIME_SHIFT: - double magnitude = 0.25 * properties.timeFactor(); + double magnitude = 0.25 * properties.characteristicTime(); bounds = new Segment(-magnitude, magnitude); value = (double) curve.getTimeShift().getValue(); break; @@ -446,4 +447,4 @@ public final void setProperties(ThermalProperties properties) { public abstract boolean isReady(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index 1b0fbf3..eac31f9 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -7,7 +7,6 @@ import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import java.util.List; -import java.util.Objects; import java.util.Set; import pulse.input.ExperimentalData; @@ -217,7 +216,8 @@ public PulseTemporalShape getPulseShape() { public void setPulseShape(PulseTemporalShape pulseShape) { this.pulseShape = pulseShape; - pulseShape.setParent(this); + pulseShape.setParent(this); + } } diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 153faa1..295f6b8 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -325,7 +325,7 @@ public double maxRadiationBiot() { * * @return the time factor */ - public double timeFactor() { + public double characteristicTime() { return l * l / a; } diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index fbba1cc..6951845 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -37,6 +37,35 @@ public ModelSelectionCriterion(ModelSelectionCriterion another) { this.criterion = another.criterion; } + @Override + public int hashCode() { + int hash = 7; + hash = 43 * hash + this.kq; + hash = 43 * hash + (int) (Double.doubleToLongBits(this.criterion) ^ (Double.doubleToLongBits(this.criterion) >>> 32)); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ModelSelectionCriterion other = (ModelSelectionCriterion) obj; + if (this.kq != other.kq) { + return false; + } + if (Double.doubleToLongBits(this.criterion) != Double.doubleToLongBits(other.criterion)) { + return false; + } + return true; + } + @Override public void evaluate(GeneralTask t) { kq = t.searchVector().dimension(); //number of parameters diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index e4c92a3..5b9d3e5 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -6,6 +6,7 @@ import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; import java.util.List; +import java.util.Objects; import pulse.DiscreteInput; import pulse.Response; import pulse.input.IndexRange; @@ -31,6 +32,39 @@ public abstract class ResidualStatistic extends Statistic { private List rx; private List ry; + @Override + public int hashCode() { + int hash = 5; + hash = 53 * hash + (int) (Double.doubleToLongBits(this.statistic) ^ (Double.doubleToLongBits(this.statistic) >>> 32)); + hash = 53 * hash + Objects.hashCode(this.rx); + hash = 53 * hash + Objects.hashCode(this.ry); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ResidualStatistic other = (ResidualStatistic) obj; + if (Double.doubleToLongBits(this.statistic) != Double.doubleToLongBits(other.statistic)) { + return false; + } + if (!Objects.equals(this.rx, other.rx)) { + return false; + } + if (!Objects.equals(this.ry, other.ry)) { + return false; + } + return true; + } + public ResidualStatistic() { super(); ry = new ArrayList<>(); @@ -144,4 +178,4 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 5c673d3..2e38a79 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -9,6 +9,7 @@ import static pulse.util.Reflexive.instantiate; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import pulse.Response; @@ -330,27 +331,6 @@ public int compareTo(Calculation arg0) { return sThis.compareTo(sAnother); } - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - - if (o == null) { - return false; - } - - if (!(o instanceof Calculation)) { - return false; - } - - var c = (Calculation) o; - - return (os.getStatistic().equals(c.getOptimiserStatistic().getStatistic()) - && rs.getStatistic().equals(c.getModelSelectionCriterion().getStatistic())); - - } - public static InstanceDescriptor getModelSelectionDescriptor() { return instanceDescriptor; } @@ -395,4 +375,37 @@ public double objectiveFunction(GeneralTask task) throws SolverException { return (double) os.getStatistic().getValue(); } + @Override + public int hashCode() { + int hash = 7; + hash = 79 * hash + Objects.hashCode(this.problem); + hash = 79 * hash + Objects.hashCode(this.scheme); + hash = 79 * hash + Objects.hashCode(this.result); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Calculation other = (Calculation) obj; + if (!Objects.equals(this.problem, other.problem)) { + return false; + } + if (!Objects.equals(this.scheme, other.scheme)) { + return false; + } + if (!Objects.equals(this.result, other.result)) { + return false; + } + return true; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index c3c5ab3..94b8774 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -127,4 +127,4 @@ public String toString() { } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/logs/LogEntry.java b/src/main/java/pulse/tasks/logs/LogEntry.java index bfd84fb..f311fd1 100644 --- a/src/main/java/pulse/tasks/logs/LogEntry.java +++ b/src/main/java/pulse/tasks/logs/LogEntry.java @@ -20,9 +20,9 @@ */ public class LogEntry { - private Identifier identifier; - private LocalTime time; - private final Response response; + private final Identifier identifier; + private final LocalTime time; + private final LogEntry previous; /** *

@@ -36,11 +36,17 @@ public LogEntry(SearchTask t) { Objects.requireNonNull(t, Messages.getString("LogEntry.NullTaskError")); time = LocalDateTime.now().toLocalTime(); identifier = t.getIdentifier(); - this.response = t.getResponse(); + var list = t.getLog().getLogEntries(); + if(list != null && !list.isEmpty()) { + previous = list.get(list.size() - 1); + } + else { + previous = null; + } } - public Response getResponse() { - return response; + public LogEntry getPreviousEntry() { + return previous; } public Identifier getIdentifier() { @@ -51,4 +57,4 @@ public LocalTime getTime() { return time; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/GraphicalLogPane.java b/src/main/java/pulse/ui/components/GraphicalLogPane.java index a50e494..82f8424 100644 --- a/src/main/java/pulse/ui/components/GraphicalLogPane.java +++ b/src/main/java/pulse/ui/components/GraphicalLogPane.java @@ -1,6 +1,8 @@ package pulse.ui.components; import javax.swing.JComponent; +import javax.swing.JScrollPane; +import javax.swing.ScrollPaneConstants; import static pulse.properties.NumericPropertyKeyword.ITERATION; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; @@ -14,9 +16,13 @@ public class GraphicalLogPane extends AbstractLogger { private final LogChart chart; + private final JScrollPane pane; public GraphicalLogPane() { + pane = new JScrollPane(); + pane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); chart = new LogChart(); + pane.setViewportView(chart.getChartPanel()); TaskManager.getManagerInstance().addTaskRepositoryListener( e -> { if(e.getState() == TaskRepositoryEvent.State.TASK_SUBMITTED) { chart.clear(); @@ -26,7 +32,7 @@ public GraphicalLogPane() { @Override public JComponent getGUIComponent() { - return chart.getChartPanel(); + return pane; } @Override diff --git a/src/main/java/pulse/ui/components/LogChart.java b/src/main/java/pulse/ui/components/LogChart.java index 1f180ca..ba40ae4 100644 --- a/src/main/java/pulse/ui/components/LogChart.java +++ b/src/main/java/pulse/ui/components/LogChart.java @@ -6,9 +6,14 @@ import java.awt.Color; import static java.awt.Color.WHITE; import static java.awt.Color.black; +import java.awt.Dimension; import java.time.Duration; +import java.time.LocalTime; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.swing.SwingUtilities; import org.jfree.chart.JFreeChart; @@ -35,6 +40,7 @@ import pulse.tasks.TaskManager; import pulse.tasks.logs.DataLogEntry; import pulse.tasks.logs.Log; +import pulse.tasks.logs.StateEntry; import pulse.tasks.logs.Status; import pulse.tasks.processing.Buffer; import pulse.ui.ColorGenerator; @@ -44,7 +50,8 @@ public class LogChart extends AuxPlotter { private final Map plots; private Color[] colors; private static final ColorGenerator cg = new ColorGenerator(); - private Response r; + public final static int HEIGHT_FACTOR = 75; + public final static int MARGIN = 10; public LogChart() { var plot = new CombinedDomainXYPlot(new NumberAxis("Iteration")); @@ -58,13 +65,14 @@ public LogChart() { public final void clear() { var p = (CombinedDomainXYPlot) getPlot(); - p.getDomainAxis().setAutoRange(true); if (p != null) { + if (p.getDomainAxis() != null) { + p.getDomainAxis().setAutoRange(true); + } plots.values().stream().forEach(pp -> p.remove(pp)); } plots.clear(); colors = new Color[0]; - r = null; } private void setLegendTitle(Plot plot) { @@ -88,6 +96,11 @@ public final void add(ParameterIdentifier key, int no) { plots.put(key, plot); ((CombinedDomainXYPlot) getPlot()).add(plot); + int height = HEIGHT_FACTOR * plots.size(); + int width = getChartPanel().getParent().getWidth() - MARGIN; + getChartPanel().setPreferredSize(new Dimension(width, height)); + getChartPanel().revalidate(); + var dataset = new XYSeriesCollection(); var series = new XYSeries(key.toString()); @@ -115,7 +128,7 @@ public void changeAxis(boolean iterationMode) { var domainAxis = (NumberAxis) getPlot().getDomainAxis(); domainAxis.setLabel(iterationMode ? "Iteration" : "Time (ms)"); domainAxis.setAutoRange(!iterationMode); - if(iterationMode) { + if (iterationMode) { domainAxis.setTickUnit(new NumberTickUnit(1)); } else { domainAxis.setAutoTickUnitSelection(true); @@ -126,10 +139,18 @@ public void changeAxis(boolean iterationMode) { public void plot(Log l) { requireNonNull(l); - l.getLogEntries().stream() - .filter(le -> le instanceof DataLogEntry) - .forEach(d -> plot((DataLogEntry) d, - Duration.between(l.getStart(), d.getTime()).toMillis())); + List startTimes = l.getLogEntries().stream() + .filter(le -> le instanceof DataLogEntry && le.getPreviousEntry() instanceof StateEntry) + .map(entry -> entry.getTime()).collect(Collectors.toList()); + + if (!startTimes.isEmpty()) { + var recentStart = startTimes.get(startTimes.size() - 1); + l.getLogEntries().stream().filter(le -> le.getTime().isAfter(recentStart)) + .filter(e -> e instanceof DataLogEntry).forEach(dle + -> plot((DataLogEntry) dle, + Duration.between(recentStart, dle.getTime()).toMillis())); + } + } private static void adjustRange(XYPlot pl, int iteration, int bufSize) { @@ -138,7 +159,7 @@ private static void adjustRange(XYPlot pl, int iteration, int bufSize) { var domainAxis = pl.getDomainAxis(); var r = domainAxis.getRange(); var newR = new Range(lower, lower + bufSize); - + if (!r.equals(newR) && iteration > lower) { ((XYPlot) pl).getDomainAxis().setRange(lower, lower + bufSize); } @@ -185,7 +206,7 @@ public final void plot(DataLogEntry dle, double iterationOrTime) { runningAverage.add(iterationOrTime, buf.average(np.getKeyword())); } - SwingUtilities.invokeLater(() -> adjustRange((XYPlot)pl, (int)iterationOrTime, bufSize)); + SwingUtilities.invokeLater(() -> adjustRange((XYPlot) pl, (int) iterationOrTime, bufSize)); } else { var domainAxis = ((XYPlot) pl).getDomainAxis(); @@ -196,4 +217,4 @@ public final void plot(DataLogEntry dle, double iterationOrTime) { } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/PulseChart.java b/src/main/java/pulse/ui/components/PulseChart.java index db6e747..5b4151a 100644 --- a/src/main/java/pulse/ui/components/PulseChart.java +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -4,13 +4,11 @@ import static java.awt.Color.black; import static java.awt.Font.PLAIN; import static java.util.Objects.requireNonNull; -import static org.jfree.chart.plot.PlotOrientation.VERTICAL; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; -import org.jfree.chart.ChartFactory; import org.jfree.chart.annotations.XYTitleAnnotation; import org.jfree.chart.block.BlockBorder; import org.jfree.chart.renderer.xy.XYDifferenceRenderer; @@ -63,8 +61,8 @@ public void plot(Calculation c) { var pulseDataset = new XYSeriesCollection(); - pulseDataset.addSeries(series(problem.getPulse(), c.getScheme().getGrid().getTimeStep(), - problem.getProperties().timeFactor(), startTime)); + pulseDataset.addSeries(series(problem.getPulse(), c.getScheme().getGrid().getTimeStep()/20.0, + problem.getProperties().characteristicTime(), startTime)); getPlot().setDataset(0, pulseDataset); } @@ -76,8 +74,8 @@ private static XYSeries series(Pulse pulse, double dx, double timeFactor, double double timeLimit = pulseShape.getPulseWidth(); double x = startTime/timeFactor; - series.add(TO_MILLIS * (startTime - dx * timeFactor / 10.), 0.0); - series.add(TO_MILLIS * (startTime + timeFactor*(timeLimit + dx / 10.)), 0.0); + series.add(TO_MILLIS * (startTime - dx * timeFactor / 100.), 0.0); + series.add(TO_MILLIS * (startTime + timeFactor*(timeLimit + dx / 100.)), 0.0); for (int i = 0, numPoints = (int) (timeLimit/dx); i < numPoints; i++) { series.add(x * timeFactor * TO_MILLIS, pulseShape.evaluateAt(x - startTime/timeFactor)); diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 9640ce2..06c47f7 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -1,5 +1,10 @@ + + 1l_GFkuWY3;; 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
Time taken: LogToolBar.FileFormatDescriptor=HTML Log Files LogToolBar.SaveButton=Save Log -LogToolBar.Verbose=Verbose log +LogToolBar.Verbose=Graphical NumberEditor.EditText=Edit NumberEditor.IllegalTableEntry=Illegal table entry NumberEditor.InvalidText=Invalid Text Entered From dd0a6aa8f76740bedc81d770d6ddc2dc5eddaa22 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Wed, 11 Jan 2023 12:44:35 +0300 Subject: [PATCH 10/14] General usability changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed sample name parsing in Netzsch files - Made some changes to the text displayed in the GUI - Tooltips now more informative and well-formatted - Added missing SwingUtilities.invokeLater(…) method when updating graphical logs - Added safety checks for graphical logging - Added ‘finished’ boolean in the Log class, which helps graphical logs to be displayed correctly - Migrated to JfreeChart 1.5.4, which fixes issues with graphs in resized windows - Switched from using ExecutionServices and ForkJoinPools to using CompletableFutures. Removed redundant blocks where concurrency has been poorly handled. - Removed argument from checkProblems() in SearchTask - Fixed execution of queued tasks hanging because of conflicting ForkJoinPools - Fixed tracker dialogs not closing on end of scheme selection - Fixed focus lost problem in ProblemStatementFrame --- pom.xml | 4 +- src/main/java/pulse/input/Metadata.java | 4 +- .../pulse/io/readers/NetzschCSVReader.java | 6 + src/main/java/pulse/problem/schemes/Grid.java | 12 +- .../java/pulse/problem/schemes/Grid2D.java | 5 - .../java/pulse/problem/statements/Pulse.java | 10 +- .../pulse/problem/statements/Pulse2D.java | 3 +- .../statements/model/AbsorptionModel.java | 5 +- .../java/pulse/properties/SampleName.java | 34 +++-- .../pulse/search/direction/LMOptimiser.java | 2 +- src/main/java/pulse/tasks/Calculation.java | 70 ++++++---- src/main/java/pulse/tasks/SearchTask.java | 117 ++++++++-------- src/main/java/pulse/tasks/TaskManager.java | 65 ++++----- src/main/java/pulse/tasks/logs/Log.java | 7 + .../pulse/ui/components/GraphicalLogPane.java | 24 ++-- .../java/pulse/ui/components/LogChart.java | 10 +- .../pulse/ui/components/TaskPopupMenu.java | 4 +- .../components/buttons/ExecutionButton.java | 2 +- .../controllers/AccessibleTableRenderer.java | 44 +++--- .../ui/components/panels/ProblemToolbar.java | 2 +- src/main/java/pulse/ui/frames/LogFrame.java | 5 +- .../ui/frames/ProblemStatementFrame.java | 132 ++++++++---------- .../pulse/ui/frames/SearchOptionsFrame.java | 2 +- src/main/java/pulse/util/Accessible.java | 19 +-- .../java/pulse/util/InstanceDescriptor.java | 5 +- src/main/resources/NumericProperty.xml | 6 +- 26 files changed, 293 insertions(+), 306 deletions(-) diff --git a/pom.xml b/pom.xml index 9605d72..f7140a2 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.97b + 1.97c PULsE Processing Unit for Laser flash Experiments @@ -23,7 +23,7 @@ org.jfree jfreechart - 1.5.0 + 1.5.4 com.formdev diff --git a/src/main/java/pulse/input/Metadata.java b/src/main/java/pulse/input/Metadata.java index cd9369c..ae00b92 100644 --- a/src/main/java/pulse/input/Metadata.java +++ b/src/main/java/pulse/input/Metadata.java @@ -61,7 +61,7 @@ public class Metadata extends PropertyHolder implements Reflexive { * experimental setup. */ public Metadata(NumericProperty temperature, int externalId) { - sampleName = new SampleName(); + sampleName = new SampleName(null); setExternalID(externalId); pulseDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); data = new TreeSet<>(); @@ -186,7 +186,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { @Override public List listedTypes() { List list = super.listedTypes(); - list.add(new SampleName()); + list.add(new SampleName("")); list.add(pulseDescriptor); return list; } diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index 85c4f1c..d9837f1 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -23,6 +23,7 @@ import pulse.input.Metadata; import pulse.input.Range; import pulse.properties.NumericPropertyKeyword; +import pulse.properties.SampleName; import pulse.ui.Messages; /** @@ -49,6 +50,7 @@ public class NetzschCSVReader implements CurveReader { private final static String DIAMETER = "Diameter"; private final static String L_PULSE_WIDTH = "Laser_pulse_width"; private final static String PULSE_WIDTH = "Pulse_width"; + private final static String MATERIAL = "Material"; /** * Note comma is included as a delimiter character here. @@ -115,6 +117,9 @@ public List read(File file) throws IOException { int shotId = determineShotID(reader, file); + String name = findLineByLabel(reader, MATERIAL, DETECTOR_SPOT_SIZE, true) + .substring(MATERIAL.length() + 1) + .replaceAll(delims, ""); String spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, false); double spotSize = 0; @@ -164,6 +169,7 @@ public List read(File file) throws IOException { if (pulseWidth > 1e-10) { met.set(NumericPropertyKeyword.PULSE_WIDTH, derive(NumericPropertyKeyword.PULSE_WIDTH, pulseWidth)); } + met.setSampleName(new SampleName(name)); met.set(NumericPropertyKeyword.THICKNESS, derive(NumericPropertyKeyword.THICKNESS, thickness)); met.set(NumericPropertyKeyword.DIAMETER, derive(NumericPropertyKeyword.DIAMETER, diameter)); met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, spotSize != 0 ? spotSize : 0.85 * diameter)); diff --git a/src/main/java/pulse/problem/schemes/Grid.java b/src/main/java/pulse/problem/schemes/Grid.java index 2d7128d..7bbfc9a 100644 --- a/src/main/java/pulse/problem/schemes/Grid.java +++ b/src/main/java/pulse/problem/schemes/Grid.java @@ -211,15 +211,9 @@ public final double gridAxialDistance(double distance, double lengthFactor) { @Override public String toString() { - var sb = new StringBuilder(); - sb.append(""). - append(getClass().getSimpleName()) - .append(": hx=") - .append(format("%3.2e", hx)) - .append("; "). - append("τ=") - .append(format("%3.2e", tau)) - .append("; "); + var sb = new StringBuilder("Grid"); + sb.append(String.format("%n %-25s", this.getGridDensity())); + sb.append(String.format("%n %-25s", this.getTimeFactor())); return sb.toString(); } diff --git a/src/main/java/pulse/problem/schemes/Grid2D.java b/src/main/java/pulse/problem/schemes/Grid2D.java index af2284b..e35a6e3 100644 --- a/src/main/java/pulse/problem/schemes/Grid2D.java +++ b/src/main/java/pulse/problem/schemes/Grid2D.java @@ -99,11 +99,6 @@ public double gridRadialDistance(double radial, double lengthFactor) { return rint((radial / lengthFactor) / hy) * hy; } - @Override - public String toString() { - return super.toString() + "hy=" + format("%3.3f", hy); - } - public double getYStep() { return hy; } diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index eac31f9..2b5b988 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -163,12 +163,10 @@ public void setLaserEnergy(NumericProperty laserEnergy) { @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(getPulseShape()); - sb.append(" ; "); - sb.append(getPulseWidth()); - sb.append(" ; "); - sb.append(getLaserEnergy()); + StringBuilder sb = new StringBuilder("Pulse:"); + sb.append(String.format("%n %-25s", getPulseShape())); + sb.append(String.format("%n %-25s", getPulseWidth())); + sb.append(String.format("%n %-25s", getLaserEnergy())); return sb.toString(); } diff --git a/src/main/java/pulse/problem/statements/Pulse2D.java b/src/main/java/pulse/problem/statements/Pulse2D.java index 16895b6..11d4bef 100644 --- a/src/main/java/pulse/problem/statements/Pulse2D.java +++ b/src/main/java/pulse/problem/statements/Pulse2D.java @@ -60,8 +60,7 @@ public void setSpotDiameter(NumericProperty spotDiameter) { @Override public String toString() { StringBuilder sb = new StringBuilder(super.toString()); - sb.append(" ; "); - sb.append(getSpotDiameter()); + sb.append(String.format("%n %-25s", getSpotDiameter())); return sb.toString(); } diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index 9165b08..aacbd12 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -95,7 +95,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { @Override public String toString() { - return getClass().getSimpleName() + " : " + absorptionMap.get(LASER) + " ; " + absorptionMap.get(THERMAL); + var sb = new StringBuilder(getSimpleName()); + sb.append(String.format("%n %-25s", absorptionMap.get(LASER))); + sb.append(String.format("%n %-25s", absorptionMap.get(THERMAL))); + return sb.toString(); } @Override diff --git a/src/main/java/pulse/properties/SampleName.java b/src/main/java/pulse/properties/SampleName.java index 5ef7419..2f9f6cc 100644 --- a/src/main/java/pulse/properties/SampleName.java +++ b/src/main/java/pulse/properties/SampleName.java @@ -6,8 +6,8 @@ public class SampleName implements Property { private String name; - public SampleName() { - //null name + public SampleName(String name) { + this.name = name; } @Override @@ -41,22 +41,28 @@ public String toString() { } @Override - public boolean equals(Object o) { - if (o == this) { + public int hashCode() { + int hash = 5; + hash = 43 * hash + Objects.hashCode(this.name); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { return true; } - - if (o == null) { + if (obj == null) { return false; } - - boolean result = false; - - if (o instanceof SampleName) { - result = name.equals(((SampleName) o).getValue()); + if (getClass() != obj.getClass()) { + return false; } - - return result; + final SampleName other = (SampleName) obj; + if (!Objects.equals(this.name, other.name)) { + return false; + } + return true; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 86ee9e4..d3b22c7 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -43,7 +43,7 @@ public class LMOptimiser extends GradientBasedOptimiser { * Up to {@value MAX_FAILED_ATTEMPTS} failed attempts are allowed. */ - public final static int MAX_FAILED_ATTEMPTS = 4; + public final static int MAX_FAILED_ATTEMPTS = 5; private LMOptimiser() { super(); diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 2e38a79..40d857d 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -79,7 +79,7 @@ public Calculation(Calculation c) { } instanceDescriptor.addListener(() -> initModelCriterion(rs)); } - + public void conformTo(UpwardsNavigable owner) { problem.setParent(owner); scheme.setParent(owner); @@ -87,13 +87,13 @@ public void conformTo(UpwardsNavigable owner) { os.setParent(owner); result.setParent(owner); } - + public void clear() { this.status = INCOMPLETE; this.problem = null; this.scheme = null; } - + /** *

* After setting and adopting the {@code problem} by this @@ -201,31 +201,40 @@ public Status getStatus() { */ public boolean setStatus(Status status) { - boolean changeStatus = true; - - switch (this.status) { - case QUEUED: - case IN_PROGRESS: - switch (status) { - case QUEUED: - case READY: - case INCOMPLETE: - changeStatus = false; - break; - default: - } - break; - case FAILED: - case EXECUTION_ERROR: - case INCOMPLETE: - //if the TaskManager attempts to run this calculation - changeStatus = status != Status.QUEUED; - break; - default: - } + boolean changeStatus = false; + + if (this.getStatus() != status) { + + changeStatus = true; + + //current status is given by ** this.status ** + //new status is the ** argument ** of this method + switch (this.status) { + case QUEUED: + //do not change status to queued, ready or incomplete if already in progress + case IN_PROGRESS: + switch (status) { + case QUEUED: + case READY: + case INCOMPLETE: + changeStatus = false; + break; + default: + } + break; + case FAILED: + case EXECUTION_ERROR: + case INCOMPLETE: + //if the TaskManager attempts to run this calculation + changeStatus = status != Status.QUEUED; + break; + default: + } + + if (changeStatus) { + this.status = status; + } - if (changeStatus) { - this.status = status; } return changeStatus; @@ -350,14 +359,14 @@ public void setResult(Result result) { public double evaluate(double t) { return problem.getHeatingCurve().interpolateSignalAt(t); } - + @Override public Segment accessibleRange() { var hc = problem.getHeatingCurve(); return new Segment(hc.timeAt(0), hc.timeLimit()); } - /** + /** * This will use the current {@code DifferenceScheme} to solve the * {@code Problem} for this {@code SearchTask} and calculate the SSR value * showing how well (or bad) the calculated solution describes the @@ -367,7 +376,6 @@ public Segment accessibleRange() { * @return the value of SSR (sum of squared residuals). * @throws pulse.problem.schemes.solvers.SolverException */ - @Override public double objectiveFunction(GeneralTask task) throws SolverException { process(); @@ -408,4 +416,4 @@ public boolean equals(Object obj) { return true; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 91633c8..9cf3e22 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -103,7 +103,7 @@ public SearchTask(ExperimentalData curve) { clear(); addListeners(); } - + private void addListeners() { InterpolationDataset.addListener(e -> { if (current.getProblem() != null) { @@ -153,13 +153,13 @@ public void clear() { //this.path = null; current.clear(); - this.checkProblems(true); + this.checkProblems(); } - + public List alteredParameters() { - return activeParameters().stream().map(key -> - this.numericProperty(key)).collect(Collectors.toList()); - } + return activeParameters().stream().map(key + -> this.numericProperty(key)).collect(Collectors.toList()); + } public void addTaskListener(DataCollectionListener toAdd) { listeners.add(toAdd); @@ -181,7 +181,7 @@ public void removeStatusChangeListeners() { public String toString() { return getIdentifier().toString(); } - + /** * Adopts the {@code curve} by this {@code SearchTask}. * @@ -209,39 +209,37 @@ public void setExperimentalCurve(ExperimentalData curve) { * using the {@code status.getDetails()} method. *

* - * @param updateStatus */ - public void checkProblems(boolean updateStatus) { + public void checkProblems() { var status = getStatus(); - if (status == DONE) { - return; - } - - var pathSolver = getInstance(); - var s = INCOMPLETE; - - if (current.getProblem() == null) { - s.setDetails(MISSING_PROBLEM_STATEMENT); - } else if (!current.getProblem().isReady()) { - s.setDetails(INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT); - } else if (current.getScheme() == null) { - s.setDetails(MISSING_DIFFERENCE_SCHEME); - } else if (curve == null) { - s.setDetails(MISSING_HEATING_CURVE); - } else if (pathSolver == null) { - s.setDetails(MISSING_OPTIMISER); - } else if (getBuffer() == null) { - s.setDetails(MISSING_BUFFER); - } else if (!getInstance().compatibleWith(current.getOptimiserStatistic())) { - s.setDetails(INCOMPATIBLE_OPTIMISER); - } else { - s = READY; - } + if (status != DONE) { + + var pathSolver = getInstance(); + var s = INCOMPLETE; + + if (current.getProblem() == null) { + s.setDetails(MISSING_PROBLEM_STATEMENT); + } else if (!current.getProblem().isReady()) { + s.setDetails(INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT); + } else if (current.getScheme() == null) { + s.setDetails(MISSING_DIFFERENCE_SCHEME); + } else if (curve == null) { + s.setDetails(MISSING_HEATING_CURVE); + } else if (pathSolver == null) { + s.setDetails(MISSING_OPTIMISER); + } else if (getBuffer() == null) { + s.setDetails(MISSING_BUFFER); + } else if (!getInstance().compatibleWith(current.getOptimiserStatistic())) { + s.setDetails(INCOMPATIBLE_OPTIMISER); + } else { + s = READY; + } - if (updateStatus) { setStatus(s); + } + } public Identifier getIdentifier() { @@ -263,12 +261,12 @@ private void notifyStatusListeners(StateEntry e) { l.onStatusChange(e); } } - + @Override public void run() { correlationBuffer.clear(); current.setResult(null); - + /* check of status */ switch (getStatus()) { case READY: @@ -278,12 +276,12 @@ public void run() { default: return; } - + current.getProblem().parameterListChanged(); // get updated list of parameters super.run(); } - + /** * If the current task is either {@code IN_PROGRESS}, {@code QUEUED}, or * {@code READY}, terminates it by setting its status to {@code TERMINATED}. @@ -321,15 +319,15 @@ public CorrelationBuffer getCorrelationBuffer() { public CorrelationTest getCorrelationTest() { return correlationTest; } - + public List getStoredCalculations() { return this.stored; - } + } public void storeCalculation() { var copy = new Calculation(current); stored.add(copy); - } + } public void switchTo(Calculation calc) { current.setParent(null); @@ -366,19 +364,19 @@ private void fireRepositoryEvent(TaskRepositoryEvent e) { l.onTaskListChanged(e); } } - + @Override public boolean isInProgress() { return getStatus() == IN_PROGRESS; } - + @Override public void intermediateProcessing() { correlationBuffer.inflate(this); notifyDataListeners(new DataLogEntry(this)); } - - @Override + + @Override public void onSolverException(SolverException e) { setStatus(Status.troubleshoot(e)); } @@ -394,8 +392,8 @@ public void onSolverException(SolverException e) { */ @Override public ParameterVector searchVector() { - var ids = activeParameters().stream().map(id -> - new ParameterIdentifier(id)).collect(Collectors.toList()); + var ids = activeParameters().stream().map(id + -> new ParameterIdentifier(id)).collect(Collectors.toList()); var optimisationVector = new ParameterVector(ids); current.getProblem().optimisationVector(optimisationVector); @@ -455,8 +453,7 @@ public void postProcessing() { } } - - + /** * Finds what properties are being altered in the search of this SearchTask. * @@ -467,16 +464,14 @@ public void postProcessing() { public List activeParameters() { var flags = ActiveFlags.getAllFlags(); //problem dependent - var allActiveParams = ActiveFlags.selectActiveAndListed - (flags, current.getProblem()); + var allActiveParams = ActiveFlags.selectActiveAndListed(flags, current.getProblem()); //problem independent (lower/upper bound) - var listed = ActiveFlags.selectActiveAndListed - (flags, curve.getRange() ); - allActiveParams.addAll(listed); + var listed = ActiveFlags.selectActiveAndListed(flags, curve.getRange()); + allActiveParams.addAll(listed); return allActiveParams; } - - /** + + /** * Will return {@code true} if status could be updated. * * @param status the status of the task @@ -485,20 +480,18 @@ public List activeParameters() { * be updated at this time. * @see Calculation.setStatus() */ - public boolean setStatus(Status status) { Objects.requireNonNull(status); Status oldStatus = getStatus(); - boolean changed = current.setStatus(status) - && (oldStatus != getStatus()); + boolean changed = current.setStatus(status) && oldStatus != status; if (changed) { notifyStatusListeners(new StateEntry(this, status)); } return changed; } - + public Status getStatus() { return current.getStatus(); } @@ -525,7 +518,7 @@ public boolean equals(Object o) { return curve.equals(((SearchTask) o).curve); } - + @Override public String describe() { @@ -553,4 +546,4 @@ public Calculation getResponse() { return current; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index 358403d..397971d 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -24,7 +24,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; -import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,8 +65,6 @@ public class TaskManager extends UpwardsNavigable { private boolean singleStatement = true; - private final ForkJoinPool taskPool; - private final List selectionListeners; private final List taskRepositoryListeners; @@ -91,7 +88,6 @@ public class TaskManager extends UpwardsNavigable { private TaskManager() { tasks = new ArrayList<>(); - taskPool = new ForkJoinPool(ResourceMonitor.getInstance().getThreadsAvailable()); selectionListeners = new CopyOnWriteArrayList<>(); taskRepositoryListeners = new CopyOnWriteArrayList<>(); addHierarchyListener(statementListener); @@ -115,40 +111,41 @@ public static TaskManager getManagerInstance() { * @param t a {@code SearchTask} that will be executed */ public void execute(SearchTask t) { - t.checkProblems(t.getStatus() != Status.DONE); + t.checkProblems(); - //try to start cmputation + // try to start computation // notify listeners computation is about to start - if (!t.setStatus(QUEUED)) { + if (t.getStatus() != QUEUED && !t.setStatus(QUEUED)) { return; } - + // notify listeners calculation started notifyListeners(new TaskRepositoryEvent(TASK_SUBMITTED, t.getIdentifier())); - + // run task t -- after task completed, write result and trigger listeners CompletableFuture.runAsync(t).thenRun(() -> { - Calculation current = (Calculation)t.getResponse(); + Calculation current = (Calculation) t.getResponse(); var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); if (null == current.getStatus()) { notifyListeners(e); - } - else switch (current.getStatus()) { - case DONE: - current.setResult(new Result(t, ResultFormat.getInstance())); - //notify listeners before the task is re-assigned - notifyListeners(e); - t.storeCalculation(); - break; - case AWAITING_TERMINATION: - t.setStatus(Status.TERMINATED); - break; - default: - notifyListeners(e); - break; + } else { + switch (current.getStatus()) { + case DONE: + current.setResult(new Result(t, ResultFormat.getInstance())); + //notify listeners before the task is re-assigned + notifyListeners(e); + t.storeCalculation(); + break; + case AWAITING_TERMINATION: + t.setStatus(Status.TERMINATED); + break; + default: + notifyListeners(e); + break; + } } }); - + } /** @@ -170,7 +167,7 @@ public void notifyListeners(TaskRepositoryEvent e) { */ public void executeAll() { - var queue = tasks.stream().filter(t -> { + tasks.stream().filter(t -> { switch (t.getStatus()) { case IN_PROGRESS: case EXECUTION_ERROR: @@ -178,11 +175,9 @@ public void executeAll() { default: return true; } - }).collect(toList()); - - for (SearchTask t : queue) { - taskPool.submit(() -> execute(t)); - } + }).forEach(t -> { + execute(t); + }); } @@ -259,7 +254,7 @@ public SampleName getSampleName() { return null; } - return ( (ExperimentalData) optional.get().getInput() ) + return ((ExperimentalData) optional.get().getInput()) .getMetadata().getSampleName(); } @@ -306,7 +301,7 @@ public SearchTask getTask(Identifier id) { */ public SearchTask getTask(int externalId) { var o = tasks.stream().filter(t - -> Integer.compare( ( (ExperimentalData) t.getInput()) + -> Integer.compare(((ExperimentalData) t.getInput()) .getMetadata().getExternalID(), externalId) == 0).findFirst(); return o.isPresent() ? o.get() : null; @@ -510,8 +505,8 @@ public String describe() { public void evaluate() { tasks.stream().forEach(t -> { - var properties = ( (Calculation) t.getResponse() ).getProblem().getProperties(); - var c = (ExperimentalData)t.getInput(); + var properties = ((Calculation) t.getResponse()).getProblem().getProperties(); + var c = (ExperimentalData) t.getInput(); properties.useTheoreticalEstimates(c); }); } diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index 21a32df..b4c7436 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -27,6 +27,7 @@ public class Log extends Group { private final Identifier id; private final List listeners; private static boolean graphical = true; + private boolean finished; /** * Creates a {@code Log} for this {@code task} that will automatically store @@ -80,10 +81,12 @@ public Log(SearchTask task) { } private void logFinished() { + finished = true; listeners.stream().forEach(l -> l.onLogFinished(this)); } private void notifyListeners(LogEntry logEntry) { + finished = false; listeners.stream().forEach(l -> l.onNewEntry(logEntry)); } @@ -109,6 +112,10 @@ public final Identifier getIdentifier() { public boolean isStarted() { return logEntries.size() > 0; } + + public boolean isFinished() { + return finished; + } /** * Outputs all log entries consecutively. diff --git a/src/main/java/pulse/ui/components/GraphicalLogPane.java b/src/main/java/pulse/ui/components/GraphicalLogPane.java index 82f8424..bc69d23 100644 --- a/src/main/java/pulse/ui/components/GraphicalLogPane.java +++ b/src/main/java/pulse/ui/components/GraphicalLogPane.java @@ -14,17 +14,17 @@ @SuppressWarnings("serial") public class GraphicalLogPane extends AbstractLogger { - + private final LogChart chart; private final JScrollPane pane; - + public GraphicalLogPane() { pane = new JScrollPane(); pane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); chart = new LogChart(); pane.setViewportView(chart.getChartPanel()); - TaskManager.getManagerInstance().addTaskRepositoryListener( e -> { - if(e.getState() == TaskRepositoryEvent.State.TASK_SUBMITTED) { + TaskManager.getManagerInstance().addTaskRepositoryListener(e -> { + if (e.getState() == TaskRepositoryEvent.State.TASK_SUBMITTED) { chart.clear(); } }); @@ -44,16 +44,18 @@ public void printTimeTaken(Log log) { @Override public void post(LogEntry logEntry) { - if(logEntry instanceof DataLogEntry) { + if (logEntry instanceof DataLogEntry) { var dle = (DataLogEntry) logEntry; double iteration = dle.getData().stream() .filter(p -> p.getIdentifier().getKeyword() == ITERATION) .findAny().get().getApparentValue(); + chart.changeAxis(true); - chart.plot((DataLogEntry)logEntry, iteration); + chart.plot((DataLogEntry) logEntry, iteration); + } } - + @Override public void postAll() { clear(); @@ -64,18 +66,18 @@ public void postAll() { var log = task.getLog(); - if (log.isStarted()) { + if (log.isStarted() && log.isFinished()) { chart.clear(); chart.changeAxis(false); chart.plot(log); - + if (task.getStatus() == DONE) { printTimeTaken(log); } } - + } } @@ -95,4 +97,4 @@ public boolean isEmpty() { return false; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/LogChart.java b/src/main/java/pulse/ui/components/LogChart.java index ba40ae4..f1c896c 100644 --- a/src/main/java/pulse/ui/components/LogChart.java +++ b/src/main/java/pulse/ui/components/LogChart.java @@ -157,11 +157,13 @@ private static void adjustRange(XYPlot pl, int iteration, int bufSize) { int lower = (iteration / bufSize) * bufSize; var domainAxis = pl.getDomainAxis(); - var r = domainAxis.getRange(); - var newR = new Range(lower, lower + bufSize); + if (domainAxis != null) { + var r = domainAxis.getRange(); + var newR = new Range(lower, lower + bufSize); - if (!r.equals(newR) && iteration > lower) { - ((XYPlot) pl).getDomainAxis().setRange(lower, lower + bufSize); + if (!r.equals(newR) && iteration > lower) { + ((XYPlot) pl).getDomainAxis().setRange(lower, lower + bufSize); + } } } diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index 5dc2c26..c29818f 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -80,7 +80,7 @@ public TaskPopupMenu() { var itemShowStatus = new JMenuItem("What is missing?", ICON_MISSING); instance.addSelectionListener(event -> { - instance.getSelectedTask().checkProblems(false); + instance.getSelectedTask().checkProblems(); var details = instance.getSelectedTask().getStatus().getDetails(); itemShowStatus.setEnabled((details != null) & (details != NONE)); }); @@ -102,7 +102,7 @@ public TaskPopupMenu() { getString("TaskTablePopupMenu.EmptySelection"), //$NON-NLS-1$ getString("TaskTablePopupMenu.ErrorTitle"), ERROR_MESSAGE); //$NON-NLS-1$ } else { - t.checkProblems(true); + t.checkProblems(); var status = t.getStatus(); if (status == DONE) { diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 30ea042..5a06530 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -49,7 +49,7 @@ public ExecutionButton() { return; } var problematicTask = instance.getTaskList().stream().filter(t -> { - t.checkProblems(true); + t.checkProblems(); return t.getStatus() == INCOMPLETE; }).findFirst(); if (problematicTask.isPresent()) { diff --git a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java index 04727c0..6f98ec3 100644 --- a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java @@ -26,33 +26,29 @@ public AccessibleTableRenderer() { public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { - Component renderer = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - + Component result = null; + if (value instanceof Flag) { - renderer = new IconCheckBox((boolean) ((Property) value).getValue()); - ((IconCheckBox) renderer).setHorizontalAlignment(CENTER); - } else if (value instanceof PropertyHolder) { - renderer = initButton(value.toString()); + result = new IconCheckBox((boolean) ((Property) value).getValue()); + ((IconCheckBox) result).setHorizontalAlignment(CENTER); } - else if (value instanceof NumericProperty) { - //default - } - else if (value instanceof Property) { - var label = (JLabel) super.getTableCellRendererComponent(table, - ((Property) value).getDescriptor(true), isSelected, - hasFocus, row, column); - label.setHorizontalAlignment(JLabel.CENTER); - label.setFont(label.getFont().deriveFont(Font.BOLD)); - return label; + + else if (value instanceof PropertyHolder) { + var sb = new StringBuilder("Click to Edit/View "); + sb.append(((PropertyHolder) value).getSimpleName()); + sb.append("..."); + result = new JButton(sb.toString()); + ((JButton)result).setToolTipText(value.toString()); + ((JButton)result).setHorizontalAlignment(LEFT); } - return renderer; - } - - private JButton initButton(String str) { - var button = new JButton(str); - button.setToolTipText(str); - return button; + else { + result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + } + + return result; + } + -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index e06abd0..ca36f67 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -63,7 +63,7 @@ public static void plot(ActionEvent e) { var calc = (Calculation) t.getResponse(); - t.checkProblems(true); + t.checkProblems(); var status = t.getStatus(); if (status == INCOMPLETE && !status.checkProblemStatementSet()) { diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index 7eeae66..096bac2 100644 --- a/src/main/java/pulse/ui/frames/LogFrame.java +++ b/src/main/java/pulse/ui/frames/LogFrame.java @@ -78,7 +78,8 @@ public void onLogModeChanged(boolean graphical) { private void scheduleLogEvents() { var instance = TaskManager.getManagerInstance(); - instance.addSelectionListener(e -> logger.postAll()); + instance.addSelectionListener( + e -> SwingUtilities.invokeLater(() -> logger.postAll())); instance.addTaskRepositoryListener(event -> { if (event.getState() != TASK_ADDED) { @@ -128,7 +129,7 @@ private void setGraphicalLogger(boolean graphicalLog) { if (old != logger) { getContentPane().remove(old.getGUIComponent()); getContentPane().add(logger.getGUIComponent(), BorderLayout.CENTER); - logger.postAll(); + SwingUtilities.invokeLater(() -> logger.postAll()); } } diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 6acd7da..525ac3e 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -14,14 +14,10 @@ import java.awt.BorderLayout; import java.awt.GridLayout; -import java.util.Arrays; import java.util.List; -import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.stream.Collectors; import javax.swing.DefaultListModel; @@ -247,98 +243,62 @@ private void changeSchemes(DifferenceScheme newScheme) { var instance = TaskManager.getManagerInstance(); var selectedTask = instance.getSelectedTask(); - var schemeLoaderTracker = new ProgressDialog(); - schemeLoaderTracker.setTitle("Initialising solution schemes..."); - schemeLoaderTracker.setLocationRelativeTo(null); - schemeLoaderTracker.setAlwaysOnTop(true); + var tracker = new ProgressDialog(); + tracker.setTitle("Initialising solution schemes..."); + tracker.setLocationRelativeTo(null); + tracker.setAlwaysOnTop(true); - List> callableList; + tracker.trackProgress(instance.isSingleStatement() ? instance.getTaskList().size() : 1); + + Runnable finishingTouch = () -> { + var c = (Calculation) selectedTask.getResponse(); + schemeTable.setPropertyHolder(c.getScheme()); + if (c.getProblem().getComplexity() == HIGH) { + showMessageDialog(null, getString("complexity.warning"), + "High complexity", INFORMATION_MESSAGE); + } + ProblemToolbar.plot(null); + tracker.setVisible(false); + problemTable.requestFocus(); + }; if (instance.isSingleStatement()) { - callableList = instance.getTaskList().stream().map(t -> new Callable() { + var runnables = instance.getTaskList().stream().map(t -> new Runnable() { @Override - public DifferenceScheme call() throws Exception { + public void run() { changeScheme(t, newScheme); - schemeLoaderTracker.incrementProgress(); - return ((Calculation) t.getResponse()).getScheme(); + tracker.incrementProgress(); } }).collect(Collectors.toList()); + CompletableFuture.runAsync(() + -> runnables.parallelStream().forEach(c -> c.run())) + .thenRun(finishingTouch); + } else { - callableList = Arrays.asList(() -> { - changeScheme(selectedTask, newScheme); - return selectedTask.getResponse().getScheme(); - }); + CompletableFuture.runAsync(() -> changeScheme(selectedTask, newScheme)).thenRun(finishingTouch); } - schemeLoaderTracker.trackProgress(callableList.size() - 1); - - CompletableFuture.runAsync(() -> { - try { - schemeListExecutor.invokeAll(callableList); - } catch (InterruptedException ex) { - ex.printStackTrace(); - } - }).thenRun(() -> { - - var c = (Calculation) selectedTask.getResponse(); - schemeTable.setPropertyHolder(c.getScheme()); - if (c.getProblem().getComplexity() == HIGH) { - showMessageDialog(null, getString("complexity.warning"), - "High complexity", INFORMATION_MESSAGE); - } - Executors.newSingleThreadExecutor().submit(() -> ProblemToolbar.plot(null)); - }); } private void changeProblems(Problem newlySelectedProblem, Object source) { var instance = TaskManager.getManagerInstance(); - var task = instance.getSelectedTask(); - var selectedCalc = ((Calculation) task.getResponse()); - - var problemLoaderTracker = new ProgressDialog(); - problemLoaderTracker.setTitle("Changing problem statements..."); - problemLoaderTracker.setLocationRelativeTo(null); - problemLoaderTracker.setAlwaysOnTop(true); + var selectedProblem = instance.getSelectedTask(); - List> callableList; + var tracker = new ProgressDialog(); + tracker.setTitle("Changing problem statements..."); + tracker.setLocationRelativeTo(null); + tracker.setAlwaysOnTop(true); if (source != instance) { - //apply to all tasks - if (instance.isSingleStatement()) { - - callableList = instance.getTaskList().stream().map(t -> new Callable() { - @Override - public Problem call() throws Exception { - changeProblem(t, newlySelectedProblem); - var result = ((Calculation) t.getResponse()).getProblem(); - problemLoaderTracker.incrementProgress(); - return result; - } - }).collect(Collectors.toList()); - - } //apply only to this task - else { - callableList = Arrays.asList(() -> { - changeProblem(task, newlySelectedProblem); - return ((Calculation) task.getResponse()).getProblem(); - }); - } + tracker.trackProgress(instance.isSingleStatement() ? instance.getTaskList().size() : 1); - problemLoaderTracker.trackProgress(callableList.size() - 1); - - CompletableFuture.runAsync(() -> { - try { - problemListExecutor.invokeAll(callableList); - } catch (InterruptedException ex) { - ex.printStackTrace(); - } - } - ).thenRun(() -> { + Runnable finishingTouch = () -> { + var selectedCalc = (Calculation) selectedProblem.getResponse(); problemTable.setPropertyHolder(selectedCalc.getProblem()); // after problem is selected for this task, show available difference schemes var defaultModel = (DefaultListModel) (schemeSelectionList.getModel()); @@ -347,7 +307,27 @@ public Problem call() throws Exception { schemes.forEach(s -> defaultModel.addElement(s)); selectDefaultScheme(schemeSelectionList, selectedCalc.getProblem()); schemeSelectionList.setToolTipText(null); - }); + tracker.setVisible(false); + }; + + if (instance.isSingleStatement()) { + + var runnables = instance.getTaskList().stream().map(t -> new Runnable() { + @Override + public void run() { + changeProblem(t, newlySelectedProblem); + tracker.incrementProgress(); + } + + }).collect(Collectors.toList()); + + CompletableFuture.runAsync(() + -> runnables.parallelStream().forEach(c -> c.run())) + .thenRun(finishingTouch); + + } else { + CompletableFuture.runAsync(() -> changeProblem(selectedProblem, newlySelectedProblem)).thenRun(finishingTouch); + } } @@ -372,7 +352,7 @@ private void changeProblem(SearchTask task, Problem newProblem) { np.retrieveData(data); } - task.checkProblems(true); + task.checkProblems(); toolbar.highlightButtons(!np.isReady()); } @@ -413,7 +393,7 @@ private void changeScheme(SearchTask task, DifferenceScheme newScheme) { } - task.checkProblems(true); + task.checkProblems(); } diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index f638f4d..7d2c9db 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -209,7 +209,7 @@ public PathOptimiser getElementAt(int index) { pathTable.setPropertyHolder(optimiser); for (var t : TaskManager.getManagerInstance().getTaskList()) { - t.checkProblems(true); + t.checkProblems(); } }); diff --git a/src/main/java/pulse/util/Accessible.java b/src/main/java/pulse/util/Accessible.java index b9cdf8c..9cc2832 100644 --- a/src/main/java/pulse/util/Accessible.java +++ b/src/main/java/pulse/util/Accessible.java @@ -101,17 +101,18 @@ public List genericProperties() { var methods = this.getClass().getMethods(); for (var m : methods) { - if (m.getParameterCount() > 0) { - continue; - } + //getters only + if (m.getParameterCount() == 0) { - if (Property.class.isAssignableFrom(m.getReturnType()) - && !NumericProperty.class.isAssignableFrom(m.getReturnType())) + if (Property.class.isAssignableFrom(m.getReturnType()) + && !NumericProperty.class.isAssignableFrom(m.getReturnType())) try { - fields.add((Property) m.invoke(this)); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - err.println("Error invoking method " + m); - e.printStackTrace(); + fields.add((Property) m.invoke(this)); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + err.println("Error invoking method " + m); + e.printStackTrace(); + } + } } diff --git a/src/main/java/pulse/util/InstanceDescriptor.java b/src/main/java/pulse/util/InstanceDescriptor.java index bead937..644ad02 100644 --- a/src/main/java/pulse/util/InstanceDescriptor.java +++ b/src/main/java/pulse/util/InstanceDescriptor.java @@ -55,9 +55,10 @@ public boolean attemptUpdate(Object object) { return false; } - if(!allDescriptors.contains(string)) + if(!allDescriptors.contains(string)) { throw new IllegalArgumentException("Unknown descriptor: " + selectedDescriptor); - + } + this.selectedDescriptor = string; listeners.stream().forEach(l -> l.onDescriptorChanged()); return true; diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 06c47f7..cfc9984 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -441,10 +441,10 @@ descriptor="Buffer size" dimensionfactor="1" keyword="BUFFER_SIZE" maximum="32" minimum="4" value="5" primitive-type="int" discreet="false"/> - Date: Wed, 11 Jan 2023 14:15:15 +0300 Subject: [PATCH 11/14] Minor changes - Fixed buffer size not updating after commit to buffer dialog - Fixed baseline descriptor --- src/main/java/pulse/baseline/Baseline.java | 5 +++++ src/main/java/pulse/ui/components/PulseMainMenu.java | 7 ++++--- src/main/resources/Version.txt | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/pulse/baseline/Baseline.java b/src/main/java/pulse/baseline/Baseline.java index d42c31d..1efbda2 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -75,4 +75,9 @@ public void fitTo(List x, List y) { } } + @Override + public String getDescriptor() { + return "Baseline"; + } + } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index 216770e..31d0544 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -71,8 +71,8 @@ public class PulseMainMenu extends JMenuBar { private static JMenuItem loadPulseItem; private static JMenuItem modelSettingsItem; - private static ExportDialog exportDialog = new ExportDialog(); - private static FormattedInputDialog bufferDialog = new FormattedInputDialog(def(BUFFER_SIZE)); + private static final ExportDialog exportDialog = new ExportDialog(); + private static final FormattedInputDialog bufferDialog = new FormattedInputDialog(def(BUFFER_SIZE)); private static File dir; @@ -80,7 +80,8 @@ public class PulseMainMenu extends JMenuBar { private List exitListeners; public PulseMainMenu() { - bufferDialog.setConfirmAction(() -> Buffer.setSize(def(BUFFER_SIZE))); + bufferDialog.setConfirmAction(() -> + Buffer.setSize(derive(BUFFER_SIZE, bufferDialog.value()))); initComponents(); initListeners(); diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt index 2578964..9688281 100644 --- a/src/main/resources/Version.txt +++ b/src/main/resources/Version.txt @@ -1 +1 @@ -1.97b \ No newline at end of file +1.97c \ No newline at end of file From 1eccdb275506132cab21f5e23be86fbeb111d65d Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 13 Feb 2023 12:21:23 +0300 Subject: [PATCH 12/14] Added serialization - Added serialization capabilities to PULsE sessions - Modified the GUI (main menu) to accomodate the new capabilities of saving/loading sessions --- src/main/java/pulse/AbstractData.java | 4 +- src/main/java/pulse/DiscreteInput.java | 37 +++-- src/main/java/pulse/HeatingCurve.java | 61 +++++-- src/main/java/pulse/HeatingCurveListener.java | 4 +- src/main/java/pulse/Response.java | 20 ++- src/main/java/pulse/baseline/Baseline.java | 27 +-- .../java/pulse/baseline/FlatBaseline.java | 16 +- .../java/pulse/baseline/LinearBaseline.java | 24 +-- .../pulse/baseline/SinusoidalBaseline.java | 32 ++-- .../java/pulse/input/ExperimentalData.java | 21 ++- src/main/java/pulse/input/IndexRange.java | 6 +- .../pulse/input/InterpolationDataset.java | 107 ++++-------- src/main/java/pulse/input/Metadata.java | 10 +- src/main/java/pulse/input/Range.java | 122 +++++++------- .../pulse/input/listeners/CurveEvent.java | 19 +-- .../pulse/input/listeners/CurveEventType.java | 6 +- .../java/pulse/input/listeners/DataEvent.java | 3 +- .../pulse/input/listeners/DataEventType.java | 2 - .../pulse/input/listeners/DataListener.java | 4 +- .../listeners/ExternalDatasetListener.java | 13 +- .../java/pulse/io/export/ExportManager.java | 2 +- .../pulse/io/export/TextLogPaneExporter.java | 2 +- .../java/pulse/io/readers/AbstractReader.java | 1 + .../io/readers/NetzschPulseCSVReader.java | 4 +- .../java/pulse/io/readers/ReaderManager.java | 19 ++- .../java/pulse/math/AbstractIntegrator.java | 3 +- src/main/java/pulse/math/FFTTransformer.java | 14 +- .../pulse/math/FixedIntervalIntegrator.java | 1 + .../pulse/math/FunctionWithInterpolation.java | 33 +++- src/main/java/pulse/math/Harmonic.java | 10 +- src/main/java/pulse/math/LegendrePoly.java | 4 +- .../java/pulse/math/MidpointIntegrator.java | 2 + src/main/java/pulse/math/Parameter.java | 12 +- .../java/pulse/math/ParameterIdentifier.java | 36 ++-- src/main/java/pulse/math/ParameterVector.java | 14 +- src/main/java/pulse/math/Segment.java | 21 ++- .../java/pulse/math/SimpsonIntegrator.java | 2 + src/main/java/pulse/math/Window.java | 68 ++++---- src/main/java/pulse/math/ZScore.java | 44 +++-- .../math/filters/AssignmentListener.java | 8 +- src/main/java/pulse/math/filters/Filter.java | 12 +- .../math/filters/HalfTimeCalculator.java | 63 +++---- .../math/filters/OptimisablePolyline.java | 15 +- .../math/filters/OptimisedRunningAverage.java | 6 +- .../pulse/math/filters/PolylineOptimiser.java | 3 +- .../java/pulse/math/filters/Randomiser.java | 17 +- .../pulse/math/filters/RunningAverage.java | 79 +++++---- src/main/java/pulse/math/linear/Matrix2.java | 2 + src/main/java/pulse/math/linear/Matrix3.java | 2 + src/main/java/pulse/math/linear/Matrix4.java | 2 + .../pulse/math/linear/RectangularMatrix.java | 4 +- src/main/java/pulse/math/linear/Vector.java | 9 +- .../pulse/math/transforms/AtanhTransform.java | 2 + .../math/transforms/InvDiamTransform.java | 1 + .../math/transforms/PeriodicTransform.java | 6 +- .../pulse/math/transforms/StickTransform.java | 10 +- .../pulse/math/transforms/Transformable.java | 4 +- .../pulse/problem/laser/DiscretePulse.java | 4 +- .../pulse/problem/laser/DiscretePulse2D.java | 38 ++--- .../laser/ExponentiallyModifiedGaussian.java | 5 +- .../pulse/problem/laser/NumericPulse.java | 68 +++++--- .../pulse/problem/laser/NumericPulseData.java | 7 +- .../problem/laser/PulseTemporalShape.java | 7 +- .../pulse/problem/laser/RectangularPulse.java | 9 +- .../pulse/problem/laser/TrapezoidalPulse.java | 9 +- .../java/pulse/problem/schemes/ADIScheme.java | 14 +- .../problem/schemes/BlockMatrixAlgorithm.java | 5 +- .../schemes/CoupledImplicitScheme.java | 17 +- .../problem/schemes/DifferenceScheme.java | 18 +- .../problem/schemes/DistributedDetection.java | 7 +- .../pulse/problem/schemes/ExplicitScheme.java | 5 + .../problem/schemes/FixedPointIterations.java | 10 +- src/main/java/pulse/problem/schemes/Grid.java | 16 +- .../java/pulse/problem/schemes/Grid2D.java | 13 +- .../pulse/problem/schemes/ImplicitScheme.java | 19 ++- .../pulse/problem/schemes/MixedScheme.java | 5 + .../problem/schemes/OneDimensionalScheme.java | 5 +- .../schemes/RadiativeTransferCoupling.java | 9 +- .../schemes/TridiagonalMatrixAlgorithm.java | 25 +-- .../schemes/rte/BlackbodySpectrum.java | 39 ++++- .../schemes/rte/DerivativeCalculator.java | 4 +- .../pulse/problem/schemes/rte/Fluxes.java | 8 +- .../rte/FluxesAndExplicitDerivatives.java | 6 +- .../rte/FluxesAndImplicitDerivatives.java | 2 + .../schemes/rte/RTECalculationListener.java | 4 +- .../schemes/rte/RTECalculationStatus.java | 4 +- .../schemes/rte/dom/ButcherTableau.java | 4 +- .../rte/dom/CompositeGaussianQuadrature.java | 5 +- .../schemes/rte/dom/CornetteSchanksPF.java | 16 +- .../rte/dom/DiscreteOrdinatesMethod.java | 15 +- .../schemes/rte/dom/DiscreteQuantities.java | 5 +- .../schemes/rte/dom/Discretisation.java | 1 + .../schemes/rte/dom/ExplicitRungeKutta.java | 1 + .../schemes/rte/dom/FixedIterations.java | 2 + .../schemes/rte/dom/HenyeyGreensteinPF.java | 2 +- .../schemes/rte/dom/HermiteInterpolator.java | 5 +- .../schemes/rte/dom/LinearAnisotropicPF.java | 3 +- .../schemes/rte/dom/ODEIntegrator.java | 4 +- .../problem/schemes/rte/dom/OrdinateSet.java | 4 +- .../schemes/rte/dom/PhaseFunction.java | 16 +- .../schemes/rte/dom/StretchedGrid.java | 6 +- .../rte/dom/SuccessiveOverrelaxation.java | 4 +- .../pulse/problem/schemes/rte/dom/TRBDF2.java | 1 + .../rte/exact/ChandrasekharsQuadrature.java | 14 +- .../rte/exact/ExponentialIntegral.java | 1 + .../rte/exact/NewtonCotesQuadrature.java | 5 +- .../NonscatteringAnalyticalDerivatives.java | 6 +- .../NonscatteringDiscreteDerivatives.java | 2 + .../exact/NonscatteringRadiativeTransfer.java | 9 +- .../schemes/solvers/ADILinearisedSolver.java | 11 +- .../solvers/ExplicitCoupledSolver.java | 16 +- .../solvers/ExplicitCoupledSolverNL.java | 22 +-- .../solvers/ExplicitLinearisedSolver.java | 5 +- .../solvers/ExplicitNonlinearSolver.java | 1 + .../solvers/ExplicitTranslucentSolver.java | 3 +- .../solvers/ImplicitCoupledSolver.java | 12 +- .../solvers/ImplicitCoupledSolverNL.java | 11 +- .../solvers/ImplicitDiathermicSolver.java | 8 +- .../solvers/ImplicitLinearisedSolver.java | 16 +- .../solvers/ImplicitNonlinearSolver.java | 3 +- .../solvers/ImplicitTranslucentSolver.java | 15 +- .../solvers/ImplicitTwoTemperatureSolver.java | 1 + .../schemes/solvers/MixedCoupledSolver.java | 22 +-- .../schemes/solvers/MixedCoupledSolverNL.java | 11 +- .../solvers/MixedLinearisedSolver.java | 15 +- .../pulse/problem/schemes/solvers/Solver.java | 3 +- .../schemes/solvers/SolverException.java | 10 +- .../problem/statements/AdiabaticSolution.java | 4 +- .../problem/statements/ClassicalProblem.java | 4 + .../statements/ClassicalProblem2D.java | 13 +- .../problem/statements/DiathermicMedium.java | 24 +-- .../problem/statements/NonlinearProblem.java | 35 ++-- .../statements/ParticipatingMedium.java | 7 +- .../statements/PenetrationProblem.java | 5 +- .../pulse/problem/statements/Problem.java | 14 +- .../java/pulse/problem/statements/Pulse.java | 63 +++---- .../pulse/problem/statements/Pulse2D.java | 1 + .../statements/TwoTemperatureModel.java | 10 +- .../statements/model/AbsorptionModel.java | 13 +- .../model/BeerLambertAbsorption.java | 6 +- .../model/DiathermicProperties.java | 5 +- .../model/ExtendedThermalProperties.java | 1 + .../pulse/problem/statements/model/Gas.java | 42 ++--- .../problem/statements/model/Helium.java | 8 +- .../problem/statements/model/Insulator.java | 5 +- .../problem/statements/model/Nitrogen.java | 10 +- .../statements/model/ThermalProperties.java | 81 +++++---- .../model/ThermoOpticalProperties.java | 45 ++--- .../model/TwoTemperatureProperties.java | 10 +- src/main/java/pulse/properties/Flag.java | 7 +- .../pulse/properties/NumericProperties.java | 9 +- .../pulse/properties/NumericProperty.java | 35 ++-- .../properties/NumericPropertyFormatter.java | 9 +- .../properties/NumericPropertyKeyword.java | 26 +-- src/main/java/pulse/properties/Property.java | 4 +- .../java/pulse/properties/SampleName.java | 3 +- .../pulse/properties/ScientificFormat.java | 1 + src/main/java/pulse/search/GeneralTask.java | 91 +++++----- src/main/java/pulse/search/Optimisable.java | 1 - .../pulse/search/SimpleOptimisationTask.java | 14 +- .../pulse/search/direction/ActiveFlags.java | 42 ++--- .../pulse/search/direction/BFGSOptimiser.java | 8 +- .../pulse/search/direction/ComplexPath.java | 1 + .../direction/CompositePathOptimiser.java | 45 +++-- .../search/direction/DirectionSolver.java | 3 +- .../direction/GradientBasedOptimiser.java | 2 +- .../search/direction/GradientGuidedPath.java | 10 +- .../search/direction/IterativeState.java | 26 +-- .../pulse/search/direction/LMOptimiser.java | 48 +++--- .../java/pulse/search/direction/LMPath.java | 1 + .../pulse/search/direction/PathOptimiser.java | 4 +- .../pulse/search/direction/SR1Optimiser.java | 2 + .../direction/SteepestDescentOptimiser.java | 6 +- .../direction/pso/ConstrictionMover.java | 28 ++-- .../pulse/search/direction/pso/FIPSMover.java | 10 +- .../pulse/search/direction/pso/Mover.java | 2 +- .../search/direction/pso/ParticleState.java | 4 +- .../direction/pso/ParticleSwarmOptimiser.java | 8 +- .../direction/pso/StaticTopologies.java | 2 +- .../search/direction/pso/SwarmState.java | 12 +- .../search/linear/GoldenSectionOptimiser.java | 5 + .../pulse/search/linear/WolfeOptimiser.java | 47 +++--- .../pulse/search/statistics/AICStatistic.java | 2 + .../search/statistics/AbsoluteDeviations.java | 4 +- .../statistics/AndersonDarlingTest.java | 7 +- .../pulse/search/statistics/BICStatistic.java | 2 + .../search/statistics/CorrelationTest.java | 6 +- .../statistics/EmptyCorrelationTest.java | 2 + .../pulse/search/statistics/EmptyTest.java | 2 + .../java/pulse/search/statistics/FTest.java | 2 +- .../java/pulse/search/statistics/KSTest.java | 8 +- .../search/statistics/NormalityTest.java | 5 +- .../search/statistics/PearsonCorrelation.java | 2 + .../pulse/search/statistics/RSquaredTest.java | 5 +- .../RangePenalisedLeastSquares.java | 7 +- .../statistics/RegularisedLeastSquares.java | 3 +- .../search/statistics/ResidualStatistic.java | 22 +-- .../statistics/SpearmansCorrelationTest.java | 2 + .../pulse/search/statistics/Statistic.java | 4 +- .../pulse/search/statistics/SumOfSquares.java | 3 +- src/main/java/pulse/tasks/Calculation.java | 5 +- src/main/java/pulse/tasks/Identifier.java | 5 +- src/main/java/pulse/tasks/SearchTask.java | 52 +++--- src/main/java/pulse/tasks/TaskManager.java | 156 ++++++++++++++---- .../listeners/DataCollectionListener.java | 4 +- .../tasks/listeners/LogEntryListener.java | 3 +- .../tasks/listeners/ResultFormatEvent.java | 3 +- .../tasks/listeners/ResultFormatListener.java | 4 +- .../tasks/listeners/SessionListener.java | 7 + .../tasks/listeners/StatusChangeListener.java | 3 +- .../tasks/listeners/TaskRepositoryEvent.java | 7 +- .../listeners/TaskRepositoryListener.java | 7 +- .../tasks/listeners/TaskSelectionEvent.java | 5 - .../listeners/TaskSelectionListener.java | 6 +- .../java/pulse/tasks/logs/AbstractLogger.java | 7 +- .../pulse/tasks/logs/CorrelationLogEntry.java | 6 +- .../java/pulse/tasks/logs/DataLogEntry.java | 3 +- src/main/java/pulse/tasks/logs/Details.java | 15 +- src/main/java/pulse/tasks/logs/Log.java | 64 +++++-- src/main/java/pulse/tasks/logs/LogEntry.java | 16 +- .../java/pulse/tasks/logs/StateEntry.java | 3 +- src/main/java/pulse/tasks/logs/Status.java | 18 +- .../pulse/tasks/processing/AverageResult.java | 25 +-- .../java/pulse/tasks/processing/Buffer.java | 4 + .../tasks/processing/CorrelationBuffer.java | 67 ++++---- .../java/pulse/tasks/processing/Result.java | 18 +- .../pulse/tasks/processing/ResultFormat.java | 24 ++- .../tasks/processing/ResultStatistics.java | 35 ++-- src/main/java/pulse/ui/ColorGenerator.java | 21 ++- src/main/java/pulse/ui/Launcher.java | 7 +- .../java/pulse/ui/components/AuxPlotter.java | 42 ++--- src/main/java/pulse/ui/components/Chart.java | 9 +- .../java/pulse/ui/components/DataLoader.java | 26 ++- .../pulse/ui/components/GraphicalLogPane.java | 8 +- .../java/pulse/ui/components/ProblemTree.java | 2 +- .../ui/components/PropertyHolderTable.java | 7 +- .../java/pulse/ui/components/PulseChart.java | 14 +- .../pulse/ui/components/PulseMainMenu.java | 102 ++++++++---- .../pulse/ui/components/RangeTextFields.java | 47 +++--- .../pulse/ui/components/ResidualsChart.java | 2 +- .../java/pulse/ui/components/ResultTable.java | 22 ++- .../pulse/ui/components/TaskPopupMenu.java | 44 +++-- .../java/pulse/ui/components/TaskTable.java | 58 ++++--- .../java/pulse/ui/components/TextLogPane.java | 9 +- .../components/buttons/ExecutionButton.java | 11 +- .../ui/components/buttons/LoaderButton.java | 51 +++--- .../controllers/AccessibleTableRenderer.java | 30 ++-- .../controllers/InstanceCellEditor.java | 4 +- .../components/controllers/NumberEditor.java | 8 - .../controllers/NumericPropertyRenderer.java | 1 - .../controllers/ProblemCellRenderer.java | 3 +- .../controllers/SearchListRenderer.java | 2 +- .../controllers/TaskTableRenderer.java | 4 +- .../FrameVisibilityRequestListener.java | 3 +- .../ui/components/listeners/LogListener.java | 3 +- .../listeners/MouseOnMarkerListener.java | 30 ++-- .../components/listeners/ResultListener.java | 2 +- .../models/ParameterTableModel.java | 24 ++- .../components/models/ResultTableModel.java | 40 +++-- .../components/models/SelectedKeysModel.java | 31 ++-- .../ui/components/models/TaskBoxModel.java | 4 - .../ui/components/models/TaskTableModel.java | 20 ++- .../ui/components/panels/ChartToolbar.java | 2 +- .../components/panels/DoubleTablePanel.java | 12 +- .../ui/components/panels/LogToolbar.java | 2 +- .../ui/components/panels/ProblemToolbar.java | 14 +- .../ui/components/panels/SettingsToolBar.java | 2 - .../ui/components/panels/TaskToolbar.java | 10 ++ src/main/java/pulse/ui/frames/DataFrame.java | 1 - src/main/java/pulse/ui/frames/LogFrame.java | 5 +- .../java/pulse/ui/frames/MainGraphFrame.java | 3 - .../pulse/ui/frames/ModelSelectionFrame.java | 6 +- .../java/pulse/ui/frames/PreviewFrame.java | 23 ++- .../ui/frames/ProblemStatementFrame.java | 16 +- .../java/pulse/ui/frames/ResultFrame.java | 14 +- .../pulse/ui/frames/SearchOptionsFrame.java | 32 ++-- .../pulse/ui/frames/TaskControlFrame.java | 34 ++-- .../pulse/ui/frames/TaskManagerFrame.java | 28 ++-- .../pulse/ui/frames/dialogs/ExportDialog.java | 6 +- .../ui/frames/dialogs/ProgressDialog.java | 29 +++- .../ui/frames/dialogs/ResultChangeDialog.java | 30 ++-- .../pulse/util/DescriptorChangeListener.java | 4 +- .../java/pulse/util/FunctionSerializer.java | 31 ++++ src/main/java/pulse/util/Group.java | 4 +- .../java/pulse/util/HierarchyListener.java | 4 +- .../java/pulse/util/ImmutableDataEntry.java | 4 +- src/main/java/pulse/util/ImmutablePair.java | 4 +- .../java/pulse/util/InstanceDescriptor.java | 10 +- src/main/java/pulse/util/PropertyEvent.java | 3 +- src/main/java/pulse/util/PropertyHolder.java | 23 ++- .../pulse/util/PropertyHolderListener.java | 4 +- src/main/java/pulse/util/Serializer.java | 118 +++++++++++++ .../java/pulse/util/UpwardsNavigable.java | 13 +- 293 files changed, 2655 insertions(+), 1882 deletions(-) create mode 100644 src/main/java/pulse/tasks/listeners/SessionListener.java create mode 100644 src/main/java/pulse/util/FunctionSerializer.java create mode 100644 src/main/java/pulse/util/Serializer.java diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index a1d276c..58fbceb 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -258,7 +258,7 @@ public void remove(int i) { public boolean ignoreSiblings() { return true; } - + public boolean isFull() { return actualNumPoints() >= count; } @@ -306,4 +306,4 @@ public boolean equals(Object o) { } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/DiscreteInput.java b/src/main/java/pulse/DiscreteInput.java index c6bb8d8..3b29c1f 100644 --- a/src/main/java/pulse/DiscreteInput.java +++ b/src/main/java/pulse/DiscreteInput.java @@ -1,45 +1,48 @@ package pulse; import java.awt.geom.Point2D; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; import pulse.input.IndexRange; import pulse.math.Segment; -public interface DiscreteInput { - +public interface DiscreteInput extends Serializable { + public List getX(); + public List getY(); + public IndexRange getIndexRange(); - + public static List convert(double[] x, double[] y) { - + var ps = new ArrayList(); - - for(int i = 0, size = x.length; i < size; i++) { + + for (int i = 0, size = x.length; i < size; i++) { ps.add(new Point2D.Double(x[i], y[i])); } - + return ps; - + } - + public static List convert(List x, List y) { - + var ps = new ArrayList(); - - for(int i = 0, size = x.size(); i < size; i++) { + + for (int i = 0, size = x.size(); i < size; i++) { ps.add(new Point2D.Double(x.get(i), y.get(i))); } - + return ps; - + } - + public default Segment bounds() { var ir = getIndexRange(); var x = getX(); return new Segment(x.get(ir.getLowerBound()), x.get(ir.getUpperBound())); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/HeatingCurve.java b/src/main/java/pulse/HeatingCurve.java index 86e1462..f682306 100644 --- a/src/main/java/pulse/HeatingCurve.java +++ b/src/main/java/pulse/HeatingCurve.java @@ -1,6 +1,8 @@ package pulse; -import static java.util.Collections.max; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import static java.util.stream.Collectors.toList; import static pulse.input.listeners.CurveEventType.RESCALED; import static pulse.input.listeners.CurveEventType.TIME_ORIGIN_CHANGED; @@ -17,12 +19,14 @@ import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; import org.apache.commons.math3.analysis.interpolation.UnivariateInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; import pulse.baseline.Baseline; import pulse.input.ExperimentalData; import pulse.input.listeners.CurveEvent; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.util.FunctionSerializer; /** * The {@code HeatingCurve} represents a time-temperature profile (a @@ -42,20 +46,30 @@ */ public class HeatingCurve extends AbstractData { - private final List adjustedSignal; + /** + * + */ + private static final long serialVersionUID = 7071147065094996971L; + private List adjustedSignal; private List lastCalculation; private double startTime; - private final List listeners - = new ArrayList<>(); + private List listeners; - private UnivariateInterpolator interpolator; - private UnivariateFunction interpolation; + private transient UnivariateInterpolator interpolator; + private transient UnivariateFunction interpolation; protected HeatingCurve(List time, List signal, final double startTime, String name) { super(time, name); this.adjustedSignal = signal; this.startTime = startTime; + initListeners(); + } + + @Override + public void initListeners() { + super.initListeners(); + listeners = new ArrayList<>(); } /** @@ -167,7 +181,7 @@ public void scale(double scale) { for (int i = 0, max = Math.min(count, signal.size()); i < max; i++) { signal.set(i, signal.get(i) * scale); } - var dataEvent = new CurveEvent(RESCALED, this); + var dataEvent = new CurveEvent(RESCALED); fireCurveEvent(dataEvent); } @@ -195,14 +209,13 @@ private void refreshInterpolation() { } final double alpha = -1.0; - adjustedSignalExtended[0] = alpha * adjustedSignalExtended[2] + adjustedSignalExtended[0] = alpha * adjustedSignalExtended[2] - (1.0 - alpha) * adjustedSignalExtended[1]; // extrapolate // linearly /* * Submit to spline interpolation */ - interpolation = interpolator.interpolate(timeExtended, adjustedSignalExtended); } @@ -343,7 +356,7 @@ public NumericProperty getTimeShift() { public void setTimeShift(NumericProperty startTime) { requireType(startTime, TIME_SHIFT); this.startTime = (double) startTime.getValue(); - var dataEvent = new CurveEvent(TIME_ORIGIN_CHANGED, this); + var dataEvent = new CurveEvent(TIME_ORIGIN_CHANGED); fireCurveEvent(dataEvent); firePropertyChanged(this, startTime); } @@ -357,11 +370,14 @@ public List getBaselineCorrectedData() { } public void addHeatingCurveListener(HeatingCurveListener l) { + if (listeners == null) { + listeners = new ArrayList<>(); + } this.listeners.add(l); } @Override - public void removeHeatingCurveListeners() { + public void removeListeners() { listeners.clear(); } @@ -379,7 +395,7 @@ public boolean equals(Object o) { return super.equals(o) && adjustedSignal.containsAll(((HeatingCurve) o).adjustedSignal); } - + public double interpolateSignalAt(double x) { double min = this.timeAt(0); double max = timeLimit(); @@ -387,4 +403,23 @@ public double interpolateSignalAt(double x) { : (x < min ? signalAt(0) : signalAt(actualNumPoints() - 1)); } -} \ No newline at end of file + /* + * Serialization + */ + private void writeObject(ObjectOutputStream oos) + throws IOException { + // default serialization + oos.defaultWriteObject(); + // write the object + FunctionSerializer.writeSplineFunction((PolynomialSplineFunction) interpolation, oos); + } + + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + this.interpolation = FunctionSerializer.readSplineFunction(ois); + this.interpolator = new SplineInterpolator(); + } + +} diff --git a/src/main/java/pulse/HeatingCurveListener.java b/src/main/java/pulse/HeatingCurveListener.java index 7dd972c..12d8064 100644 --- a/src/main/java/pulse/HeatingCurveListener.java +++ b/src/main/java/pulse/HeatingCurveListener.java @@ -1,15 +1,17 @@ package pulse; +import java.io.Serializable; import pulse.input.listeners.CurveEvent; /** * An interface used to listen to data events related to {@code HeatingCurve}. * */ -public interface HeatingCurveListener { +public interface HeatingCurveListener extends Serializable { /** * Signals that a {@code CurveEvent} has occurred. + * * @param event */ public void onCurveEvent(CurveEvent event); diff --git a/src/main/java/pulse/Response.java b/src/main/java/pulse/Response.java index cc4f23c..e138950 100644 --- a/src/main/java/pulse/Response.java +++ b/src/main/java/pulse/Response.java @@ -1,25 +1,27 @@ package pulse; +import java.io.Serializable; import pulse.math.Segment; import pulse.problem.schemes.solvers.SolverException; import pulse.search.GeneralTask; import pulse.search.statistics.OptimiserStatistic; -public interface Response { - +public interface Response extends Serializable { + public double evaluate(double t); + public Segment accessibleRange(); - + /** - * Calculates the value of the objective function used to identify - * the current state of the optimiser. + * Calculates the value of the objective function used to identify the + * current state of the optimiser. + * * @param task * @return the value of the objective function in the current state * @throws pulse.problem.schemes.solvers.SolverException */ - public double objectiveFunction(GeneralTask task) throws SolverException; - + public OptimiserStatistic getOptimiserStatistic(); - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/baseline/Baseline.java b/src/main/java/pulse/baseline/Baseline.java index 1efbda2..29a8886 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -1,5 +1,6 @@ package pulse.baseline; +import java.util.ArrayList; import java.util.List; import pulse.DiscreteInput; @@ -24,7 +25,7 @@ public abstract class Baseline extends PropertyHolder implements Reflexive, Optimisable { public final static int MIN_BASELINE_POINTS = 15; - + public abstract Baseline copy(); /** @@ -60,24 +61,24 @@ public abstract class Baseline extends PropertyHolder implements Reflexive, Opti * @see fitTo(ExperimentalData,double,double) */ public void fitTo(DiscreteInput data) { - var filtered = Range.NEGATIVE.filter(data); - if(filtered[0].size() > MIN_BASELINE_POINTS) { + var filtered = Range.NEGATIVE.filter(data); + if (filtered[0].size() > MIN_BASELINE_POINTS) { doFit(filtered[0], filtered[1]); - } + } } - + public void fitTo(List x, List y) { - int index = IndexRange.closestLeft(0, x); - var xx = x.subList(0, index + 1); - var yy = y.subList(0, index + 1); - if(xx.size() > MIN_BASELINE_POINTS) { + int index = IndexRange.closestLeft(0, x); + var xx = new ArrayList<>(x.subList(0, index + 1)); + var yy = new ArrayList<>(y.subList(0, index + 1)); + if (xx.size() > MIN_BASELINE_POINTS) { doFit(xx, yy); - } + } } - + @Override public String getDescriptor() { return "Baseline"; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/baseline/FlatBaseline.java b/src/main/java/pulse/baseline/FlatBaseline.java index 9500c8f..1ca0743 100644 --- a/src/main/java/pulse/baseline/FlatBaseline.java +++ b/src/main/java/pulse/baseline/FlatBaseline.java @@ -8,10 +8,11 @@ /** * A flat baseline. */ - public class FlatBaseline extends AdjustableBaseline { - - /** + + private static final long serialVersionUID = -4867631788950622739L; + + /** * A primitive constructor, which initialises a {@code CONSTANT} baseline * with zero intercept and slope. */ @@ -27,8 +28,7 @@ public FlatBaseline() { public FlatBaseline(double intercept) { super(intercept, 0.0); } - - + @Override protected void doFit(List x, List y) { double intercept = mean(y); @@ -37,12 +37,12 @@ protected void doFit(List x, List y) { @Override public Baseline copy() { - return new FlatBaseline((double)getIntercept().getValue()); + return new FlatBaseline((double) getIntercept().getValue()); } @Override public String toString() { return getClass().getSimpleName() + " = " + format("%3.2f", getIntercept().getValue()); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/baseline/LinearBaseline.java b/src/main/java/pulse/baseline/LinearBaseline.java index c4afb45..a04354d 100644 --- a/src/main/java/pulse/baseline/LinearBaseline.java +++ b/src/main/java/pulse/baseline/LinearBaseline.java @@ -29,6 +29,8 @@ */ public class LinearBaseline extends AdjustableBaseline { + private static final long serialVersionUID = -7334390731462268504L; + /** * A primitive constructor, which initialises a {@code CONSTANT} baseline * with zero intercept and slope. @@ -36,19 +38,19 @@ public class LinearBaseline extends AdjustableBaseline { public LinearBaseline() { super(0.0, 0.0); } - + public LinearBaseline(double intercept, double slope) { super(intercept, slope); } - + public LinearBaseline(AdjustableBaseline baseline) { - super( (double) baseline.getIntercept().getValue(), - (double) baseline.getSlope().getValue() - ); + super((double) baseline.getIntercept().getValue(), + (double) baseline.getSlope().getValue() + ); } @Override - protected void doFit(List x, List y) { + protected void doFit(List x, List y) { double meanx = mean(x); double meany = mean(y); @@ -63,7 +65,7 @@ protected void doFit(List x, List y) { xxbar += (x1 - meanx) * (x1 - meanx); xybar += (x1 - meanx) * (y1 - meany); } - + double slope = xybar / xxbar; double intercept = meany - slope * meanx; @@ -74,8 +76,8 @@ protected void doFit(List x, List y) { @Override public String toString() { var slope = getSlope().getValue(); - return getClass().getSimpleName() + " = " + - format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); + return getClass().getSimpleName() + " = " + + format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); } @Override @@ -121,9 +123,9 @@ public void assign(ParameterVector params) { for (Parameter p : params.getParameters()) { var key = p.getIdentifier().getKeyword(); - + if (key == BASELINE_SLOPE) { - setSlope( derive(BASELINE_SLOPE, p.inverseTransform() )); + setSlope(derive(BASELINE_SLOPE, p.inverseTransform())); } } diff --git a/src/main/java/pulse/baseline/SinusoidalBaseline.java b/src/main/java/pulse/baseline/SinusoidalBaseline.java index 9074cdc..94b92c9 100644 --- a/src/main/java/pulse/baseline/SinusoidalBaseline.java +++ b/src/main/java/pulse/baseline/SinusoidalBaseline.java @@ -46,6 +46,7 @@ */ public class SinusoidalBaseline extends LinearBaseline { + private static final long serialVersionUID = -6858521208790195992L; private List hiFreq; private List loFreq; private List active; @@ -87,10 +88,10 @@ public Baseline copy() { newH.setParent(baseline); } for (Harmonic h : hiFreq) { - baseline.hiFreq.add(new Harmonic(h)); + baseline.hiFreq.add(new Harmonic(h)); } for (Harmonic h : loFreq) { - baseline.loFreq.add(new Harmonic(h)); + baseline.loFreq.add(new Harmonic(h)); } return baseline; } @@ -98,7 +99,7 @@ public Baseline copy() { @Override public void optimisationVector(ParameterVector output) { super.optimisationVector(output); - active.forEach(h -> h.optimisationVector(output) ); + active.forEach(h -> h.optimisationVector(output)); } @Override @@ -124,7 +125,7 @@ private void guessHarmonics(double[] x, double[] y) { double maxAmp = 0; hiFreq = new ArrayList<>(); - + double span = x[x.length - 1] - x[0]; double lowerFrequency = 4.0 / span; @@ -146,7 +147,7 @@ private List sort(List hs, int limit) { tmp.sort(null); Collections.reverse(tmp); //leave out a maximum of n harmonics - return tmp.subList(0, Math.min(tmp.size(), limit)); + return new ArrayList<>(tmp.subList(0, Math.min(tmp.size(), limit))); } private void labelActive() { @@ -237,25 +238,25 @@ public NumericProperty getHiFreqMax() { public void setHiFreqMax(NumericProperty maxHarmonics) { NumericProperty.requireType(maxHarmonics, MAX_HIGH_FREQ_WAVES); int oldValue = this.maxHighFreqHarmonics; - + if ((int) maxHarmonics.getValue() != oldValue) { - + var lowFreq = new ArrayList(); int size = active.size(); - - if(maxHighFreqHarmonics < size) { + + if (maxHighFreqHarmonics < size) { lowFreq = new ArrayList<>(active.subList(maxHighFreqHarmonics, size)); } - + this.maxHighFreqHarmonics = (int) maxHarmonics.getValue(); active.clear(); active.addAll(sort(hiFreq, maxHighFreqHarmonics)); active.addAll(lowFreq); this.labelActive(); this.firePropertyChanged(this, maxHarmonics); - + } - + } public NumericProperty getLowFreqMax() { @@ -267,7 +268,7 @@ public void setLowFreqMax(NumericProperty maxHarmonics) { int oldValue = this.maxLowFreqHarmonics; if ((int) maxHarmonics.getValue() != oldValue) { this.maxLowFreqHarmonics = (int) maxHarmonics.getValue(); - active = active.subList(0, maxHighFreqHarmonics); + active = new ArrayList<>(active.subList(0, maxHighFreqHarmonics)); active.addAll(this.sort(loFreq, maxLowFreqHarmonics)); this.labelActive(); this.firePropertyChanged(this, maxHarmonics); @@ -279,7 +280,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { super.set(type, property); switch (type) { - + case MAX_HIGH_FREQ_WAVES: setHiFreqMax(property); break; @@ -346,12 +347,11 @@ private void addLowFreq(DiscreteInput input) { /* These harmonics are inaccessible by FFT */ - for (double f = freq; f > 1.0 / (2.0 * span); f /= 2.0) { loFreq.add(new Harmonic(amp, f, 0.0)); } - active.addAll(loFreq.subList(0, Math.min(loFreq.size(), maxLowFreqHarmonics))); + active.addAll(loFreq.subList(0, Math.min(loFreq.size(), maxLowFreqHarmonics))); } @Override diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index f38a8e0..4be76e7 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -27,11 +27,15 @@ */ public class ExperimentalData extends AbstractData implements DiscreteInput { + /** + * + */ + private static final long serialVersionUID = 7950893319753173094L; private HalfTimeCalculator calculator; private Metadata metadata; private IndexRange indexRange; private Range range; - private List dataListeners; + private transient List dataListeners; /** * This is the cutoff factor which is used as a criterion for data @@ -49,10 +53,16 @@ public class ExperimentalData extends AbstractData implements DiscreteInput { */ public ExperimentalData() { super(); - dataListeners = new ArrayList<>(); setPrefix("RawData"); setNumPoints(derive(NUMPOINTS, 0)); - indexRange = new IndexRange(0,0); + indexRange = new IndexRange(0, 0); + initListeners(); + } + + @Override + public void initListeners() { + super.initListeners(); + dataListeners = new ArrayList<>(); this.addDataListener((DataEvent e) -> { if (e.getType() == DataEventType.DATA_LOADED) { preprocess(); @@ -61,6 +71,9 @@ public ExperimentalData() { } public final void addDataListener(DataListener listener) { + if(dataListeners == null) { + dataListeners = new ArrayList<>(); + } dataListeners.add(listener); } @@ -298,4 +311,4 @@ public List getY() { return this.getSignalData(); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/input/IndexRange.java b/src/main/java/pulse/input/IndexRange.java index 93a9280..cb19490 100644 --- a/src/main/java/pulse/input/IndexRange.java +++ b/src/main/java/pulse/input/IndexRange.java @@ -1,5 +1,6 @@ package pulse.input; +import java.io.Serializable; import static java.util.Objects.requireNonNull; import java.util.List; @@ -15,8 +16,9 @@ * @see pulse.input.Range * */ -public class IndexRange { +public class IndexRange implements Serializable { + private static final long serialVersionUID = 7983756487957427969L; private int iStart; private int iEnd; @@ -24,7 +26,7 @@ public IndexRange(IndexRange other) { iStart = other.iStart; iEnd = other.iEnd; } - + public IndexRange(int start, int end) { this.iStart = start; this.iEnd = end; diff --git a/src/main/java/pulse/input/InterpolationDataset.java b/src/main/java/pulse/input/InterpolationDataset.java index 45f244e..4e0ec64 100644 --- a/src/main/java/pulse/input/InterpolationDataset.java +++ b/src/main/java/pulse/input/InterpolationDataset.java @@ -1,20 +1,18 @@ package pulse.input; -import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; -import static pulse.properties.NumericPropertyKeyword.DENSITY; -import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import java.util.ArrayList; -import java.util.EnumMap; import java.util.List; -import java.util.Map; import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.AkimaSplineInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; -import pulse.input.listeners.ExternalDatasetListener; -import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.EMISSIVITY; +import pulse.util.FunctionSerializer; import pulse.util.ImmutableDataEntry; /** @@ -27,19 +25,19 @@ * * @see pulse.input.listeners.ExternalDatasetListener */ -public class InterpolationDataset { +public class InterpolationDataset implements Serializable { - private UnivariateFunction interpolation; + /** + * + */ + private static final long serialVersionUID = 7439474910490135034L; + private transient UnivariateFunction interpolation; private final List> dataset; - private static final Map standartDatasets - = new EnumMap(StandartType.class); - private static final List listeners = new ArrayList<>(); - /** - * Creates an empty {@code InterpolationDataset}. - */ - - public InterpolationDataset() { + /** + * Creates an empty {@code InterpolationDataset}. + */ + public InterpolationDataset() { dataset = new ArrayList<>(); } @@ -66,7 +64,7 @@ public void add(ImmutableDataEntry entry) { } /** - * Constructs a new spline interpolator and uses the available dataset to + * Constructs a new Akima spline interpolator and uses the available dataset to * produce a {@code SplineInterpolation}. */ public void doInterpolation() { @@ -84,65 +82,22 @@ public List> getData() { return dataset; } - /** - * Retrieves a standard dataset previously loaded by the respective reader. - * - * @param type the standard dataset type - * @return an {@code InterpolationDataset} corresponding to {@code type} - */ - public static InterpolationDataset getDataset(StandartType type) { - return standartDatasets.get(type); - } - - /** - * Puts a datset specified by {@code type} into the static hash map of this - * class, using {@code type} as key. Triggers {@code onDensityDataLoaded} - * - * @param dataset a dataset to be appended to the static hash map - * @param type the dataset type + /* + * Serialization */ - public static void setDataset(InterpolationDataset dataset, StandartType type) { - standartDatasets.put(type, dataset); - listeners.stream().forEach(l -> l.onDataLoaded(type)); + private void writeObject(ObjectOutputStream oos) + throws IOException { + // default serialization + oos.defaultWriteObject(); + // write the object + FunctionSerializer.writeSplineFunction((PolynomialSplineFunction) interpolation, oos); } - /** - * Creates a list of property keywords that can be derived with help of the - * loaded data. For example, if heat capacity and density data is available, - * the returned list will contain {@code CONDUCTIVITY}. - * - * @return - */ - public static List derivableProperties() { - var list = new ArrayList(); - if (standartDatasets.containsKey(StandartType.HEAT_CAPACITY)) { - list.add(SPECIFIC_HEAT); - } - if (standartDatasets.containsKey(StandartType.DENSITY)) { - list.add(DENSITY); - } - if (list.contains(SPECIFIC_HEAT) && list.contains(DENSITY)) { - list.add(CONDUCTIVITY); - list.add(EMISSIVITY); - } - return list; - } - - public static void addListener(ExternalDatasetListener l) { - listeners.add(l); - } - - public enum StandartType { - - /** - * A keyword for the heat capacity dataset (in J/kg/K). - */ - HEAT_CAPACITY, - /** - * A keyword for the density dataset (in kg/m3). - */ - DENSITY; - + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + this.interpolation = FunctionSerializer.readSplineFunction(ois); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/input/Metadata.java b/src/main/java/pulse/input/Metadata.java index ae00b92..6840ff5 100644 --- a/src/main/java/pulse/input/Metadata.java +++ b/src/main/java/pulse/input/Metadata.java @@ -42,11 +42,12 @@ */ public class Metadata extends PropertyHolder implements Reflexive { + private static final long serialVersionUID = -7954252611294551707L; private Set data; private SampleName sampleName; private int externalID; - private InstanceDescriptor pulseDescriptor + private InstanceDescriptor pulseDescriptor = new InstanceDescriptor<>("Pulse Shape Selector", PulseTemporalShape.class); private NumericPulseData pulseData; @@ -117,13 +118,14 @@ public void setSampleName(SampleName sampleName) { public final void setPulseData(NumericPulseData pulseData) { this.pulseData = pulseData; - this.set(PULSE_WIDTH, derive(PULSE_WIDTH, pulseData.pulseWidth()) ); + this.set(PULSE_WIDTH, derive(PULSE_WIDTH, pulseData.pulseWidth())); } /** * If a Numerical Pulse has been loaded (for example, when importing from * Proteus), this will return an object describing this data. - * @return + * + * @return */ public final NumericPulseData getPulseData() { return pulseData; @@ -272,4 +274,4 @@ public boolean equals(Object o) { } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index fd6e4e9..09b247d 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -18,7 +18,6 @@ import pulse.math.Segment; import pulse.math.transforms.StickTransform; import pulse.problem.schemes.solvers.SolverException; -import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.search.Optimisable; @@ -32,12 +31,14 @@ */ public class Range extends PropertyHolder implements Optimisable { - private Segment segment; - - public final static Range UNLIMITED = new Range (Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + private static final long serialVersionUID = 5326569416384623525L; + + private final Segment segment; + + public final static Range UNLIMITED = new Range(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); public final static Range NEGATIVE = new Range(Double.NEGATIVE_INFINITY, -1E-16); public final static Range POSITIVE = new Range(1e-16, Double.POSITIVE_INFINITY); - + /** * Constructs a {@code Range} from the minimum and maximum values of * {@code data}. @@ -60,44 +61,43 @@ public Range(List data) { public Range(double a, double b) { this.segment = new Segment(a, b); } - + /** - * Contains a data double array ([0] - x, [1] - y), - * where the data points have been filtered so that - * each x fits into this range. + * Contains a data double array ([0] - x, [1] - y), where the data points + * have been filtered so that each x fits into this range. + * * @param input * @return a [2][...] array containing filtered x and y values */ - public List[] filter(DiscreteInput input) { - var x = input.getX(); - var y = input.getY(); - - if(x.size() != y.size()) { - throw new IllegalArgumentException("x.length != y.length"); - } - - var xf = new ArrayList(); - var yf = new ArrayList(); - - double min = segment.getMinimum(); - double max = segment.getMaximum(); - - final double eps = 1E-10; - - for(int i = 0, size = x.size(); i < size; i++) { - - if(x.get(i) > min && x.get(i) < max + eps) { - - xf.add(x.get(i)); - yf.add(y.get(i)); - - } - - } - - return new List[]{xf, yf}; - + var x = input.getX(); + var y = input.getY(); + + if (x.size() != y.size()) { + throw new IllegalArgumentException("x.length != y.length"); + } + + var xf = new ArrayList(); + var yf = new ArrayList(); + + double min = segment.getMinimum(); + double max = segment.getMaximum(); + + final double eps = 1E-10; + + for (int i = 0, size = x.size(); i < size; i++) { + + if (x.get(i) > min && x.get(i) < max + eps) { + + xf.add(x.get(i)); + yf.add(y.get(i)); + + } + + } + + return new List[]{xf, yf}; + } /** @@ -139,12 +139,12 @@ public NumericProperty getUpperBound() { */ public void setLowerBound(NumericProperty p) { requireType(p, LOWER_BOUND); - - if( boundLimits(false).contains( ((Number)p.getValue()).doubleValue())) { + + if (boundLimits(false).contains(((Number) p.getValue()).doubleValue())) { segment.setMinimum((double) p.getValue()); firePropertyChanged(this, p); } - + } /** @@ -154,12 +154,12 @@ public void setLowerBound(NumericProperty p) { */ public void setUpperBound(NumericProperty p) { requireType(p, UPPER_BOUND); - - if( boundLimits(true).contains( ((Number)p.getValue()).doubleValue()) ) { + + if (boundLimits(true).contains(((Number) p.getValue()).doubleValue())) { segment.setMaximum((double) p.getValue()); firePropertyChanged(this, p); } - + } /** @@ -215,26 +215,28 @@ public Set listedKeywords() { set.add(LOWER_BOUND); set.add(UPPER_BOUND); return set; - } - + } + /** * Calculates the allowed range for either the upper or lower bound. - * @param isUpperBound if {@code true}, will calculate the range for the upper bound, otherwise -- for the lower one., + * + * @param isUpperBound if {@code true}, will calculate the range for the + * upper bound, otherwise -- for the lower one., * @return a Segment range of limits for the specific bound */ - public Segment boundLimits(boolean isUpperBound) { - + var curve = (ExperimentalData) this.getParent(); var seq = curve.getTimeSequence(); double tHalf = curve.getHalfTimeCalculator().getHalfTime(); - + Segment result = null; - if(isUpperBound) + if (isUpperBound) { result = new Segment(2.5 * tHalf, seq.get(seq.size() - 1)); - else - result = new Segment( Math.max(-0.15 * tHalf, seq.get(0)), 0.75 * tHalf); - + } else { + result = new Segment(Math.max(-0.15 * tHalf, seq.get(0)), 0.75 * tHalf); + } + return result; } @@ -250,14 +252,14 @@ public Segment boundLimits(boolean isUpperBound) { */ @Override public void optimisationVector(ParameterVector output) { - + Segment bounds; - + for (Parameter p : output.getParameters()) { var key = p.getIdentifier().getKeyword(); double value; - + switch (key) { case UPPER_BOUND: bounds = boundLimits(true); @@ -270,7 +272,7 @@ public void optimisationVector(ParameterVector output) { default: continue; } - + var transform = new StickTransform(bounds); p.setBounds(bounds); @@ -292,7 +294,7 @@ public void assign(ParameterVector params) throws SolverException { for (Parameter p : params.getParameters()) { var key = p.getIdentifier().getKeyword(); - var np = derive( key, p.inverseTransform() ); + var np = derive(key, p.inverseTransform()); switch (key) { case UPPER_BOUND: @@ -313,4 +315,4 @@ public String toString() { return "Range given by: " + segment.toString(); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/input/listeners/CurveEvent.java b/src/main/java/pulse/input/listeners/CurveEvent.java index a0b69f9..61413bf 100644 --- a/src/main/java/pulse/input/listeners/CurveEvent.java +++ b/src/main/java/pulse/input/listeners/CurveEvent.java @@ -1,6 +1,6 @@ package pulse.input.listeners; -import pulse.HeatingCurve; +import java.io.Serializable; /** * A {@code CurveEvent} is associated with an {@code HeatingCurve} object. @@ -8,21 +8,18 @@ * @see pulse.HeatingCurve * */ -public class CurveEvent { +public class CurveEvent implements Serializable { private CurveEventType type; - private HeatingCurve data; /** * Constructs a {@code CurveEvent} object, combining the {@code type} and * associated {@code data} * * @param type the type of this event - * @param data the source of the event */ - public CurveEvent(CurveEventType type, HeatingCurve data) { + public CurveEvent(CurveEventType type) { this.type = type; - this.data = data; } /** @@ -34,14 +31,4 @@ public CurveEventType getType() { return type; } - /** - * Used to get the {@code HeatingCurve} object that has undergone certain - * changes specified by this event type. - * - * @return the associated data - */ - public HeatingCurve getData() { - return data; - } - } diff --git a/src/main/java/pulse/input/listeners/CurveEventType.java b/src/main/java/pulse/input/listeners/CurveEventType.java index 3dcb5d0..40a55d3 100644 --- a/src/main/java/pulse/input/listeners/CurveEventType.java +++ b/src/main/java/pulse/input/listeners/CurveEventType.java @@ -19,12 +19,10 @@ public enum CurveEventType { * procedure. */ TIME_ORIGIN_CHANGED, - /** - * A calculation associated with this curve has finished and - * the required arrays have been filled. + * A calculation associated with this curve has finished and the required + * arrays have been filled. */ - CALCULATION_FINISHED; } diff --git a/src/main/java/pulse/input/listeners/DataEvent.java b/src/main/java/pulse/input/listeners/DataEvent.java index d42c1a2..7fefe1c 100644 --- a/src/main/java/pulse/input/listeners/DataEvent.java +++ b/src/main/java/pulse/input/listeners/DataEvent.java @@ -1,5 +1,6 @@ package pulse.input.listeners; +import java.io.Serializable; import pulse.AbstractData; /** @@ -7,7 +8,7 @@ * {@code ExperimentalData}. * */ -public class DataEvent { +public class DataEvent implements Serializable { private DataEventType type; private AbstractData data; diff --git a/src/main/java/pulse/input/listeners/DataEventType.java b/src/main/java/pulse/input/listeners/DataEventType.java index da2160c..52c8085 100644 --- a/src/main/java/pulse/input/listeners/DataEventType.java +++ b/src/main/java/pulse/input/listeners/DataEventType.java @@ -15,11 +15,9 @@ public enum DataEventType { */ RANGE_CHANGED, - /** * All data points loaded and are ready for processing. */ - DATA_LOADED; } diff --git a/src/main/java/pulse/input/listeners/DataListener.java b/src/main/java/pulse/input/listeners/DataListener.java index cb1f3d4..28170c5 100644 --- a/src/main/java/pulse/input/listeners/DataListener.java +++ b/src/main/java/pulse/input/listeners/DataListener.java @@ -1,11 +1,13 @@ package pulse.input.listeners; +import java.io.Serializable; + /** * A listener interface, which is used to listen to {@code DataEvent}s occurring * with an {@code ExperimentalData} object. * */ -public interface DataListener { +public interface DataListener extends Serializable { /** * Triggered when a certain {@code DataEvent} specified by its diff --git a/src/main/java/pulse/input/listeners/ExternalDatasetListener.java b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java index 1bb8539..a73d3d1 100644 --- a/src/main/java/pulse/input/listeners/ExternalDatasetListener.java +++ b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java @@ -1,7 +1,6 @@ package pulse.input.listeners; -import pulse.input.InterpolationDataset.StandartType; - +import java.io.Serializable; /** * A listener associated with the {@code InterpolationDataset} static repository * of interpolations. @@ -9,11 +8,7 @@ */ public interface ExternalDatasetListener { - /** - * Triggered when a data {@code type} has been loaded. - * - * @param type a type of the dataset, for which an interpolation is created. - */ - public void onDataLoaded(StandartType type); + public void onSpecificHeatDataLoaded(); + public void onDensityDataLoaded(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/io/export/ExportManager.java b/src/main/java/pulse/io/export/ExportManager.java index 2064c50..892402c 100644 --- a/src/main/java/pulse/io/export/ExportManager.java +++ b/src/main/java/pulse/io/export/ExportManager.java @@ -21,7 +21,7 @@ * */ public class ExportManager { - + //current working dir private static File cwd = null; diff --git a/src/main/java/pulse/io/export/TextLogPaneExporter.java b/src/main/java/pulse/io/export/TextLogPaneExporter.java index e6ea39e..317af50 100644 --- a/src/main/java/pulse/io/export/TextLogPaneExporter.java +++ b/src/main/java/pulse/io/export/TextLogPaneExporter.java @@ -72,4 +72,4 @@ public Extension[] getSupportedExtensions() { return new Extension[]{HTML}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/AbstractReader.java b/src/main/java/pulse/io/readers/AbstractReader.java index d98036e..29bbc62 100644 --- a/src/main/java/pulse/io/readers/AbstractReader.java +++ b/src/main/java/pulse/io/readers/AbstractReader.java @@ -13,6 +13,7 @@ * lists, arrays and containers may (and usually will) change as a result of * using the reader. *

+ * * @param */ public interface AbstractReader extends AbstractHandler { diff --git a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java index 7d759e6..bfc07fe 100644 --- a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java @@ -55,8 +55,8 @@ public NumericPulseData read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); NumericPulseData data = null; - - ( (NetzschCSVReader) NetzschCSVReader.getInstance() ) + + ((NetzschCSVReader) NetzschCSVReader.getInstance()) .setDefaultLocale(); //always start with a default locale try (BufferedReader reader = new BufferedReader(new FileReader(file))) { diff --git a/src/main/java/pulse/io/readers/ReaderManager.java b/src/main/java/pulse/io/readers/ReaderManager.java index 0f8201b..5d168a9 100644 --- a/src/main/java/pulse/io/readers/ReaderManager.java +++ b/src/main/java/pulse/io/readers/ReaderManager.java @@ -214,27 +214,28 @@ public static Set readDirectory(List> readers, File dir } var es = Executors.newSingleThreadExecutor(); - + var callableList = new ArrayList>(); - + for (File f : directory.listFiles()) { Callable callable = () -> read(readers, f); callableList.add(callable); } - + Set result = new HashSet<>(); - + try { List> futures = es.invokeAll(callableList); - - for(Future f : futures) + + for (Future f : futures) { result.add(f.get()); - + } + } catch (InterruptedException ex) { - Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, + Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, "Reading interrupted when loading files from " + directory.toString(), ex); } catch (ExecutionException ex) { - Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, + Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, "Error executing read operation using concurrency", ex); } diff --git a/src/main/java/pulse/math/AbstractIntegrator.java b/src/main/java/pulse/math/AbstractIntegrator.java index 1fadb3a..b56c00e 100644 --- a/src/main/java/pulse/math/AbstractIntegrator.java +++ b/src/main/java/pulse/math/AbstractIntegrator.java @@ -1,5 +1,6 @@ package pulse.math; +import java.io.Serializable; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -10,7 +11,7 @@ * or more variables and the other to actually do the integration. * */ -public abstract class AbstractIntegrator extends PropertyHolder implements Reflexive { +public abstract class AbstractIntegrator extends PropertyHolder implements Reflexive, Serializable { private Segment integrationBounds; diff --git a/src/main/java/pulse/math/FFTTransformer.java b/src/main/java/pulse/math/FFTTransformer.java index 7e5390d..dea0d65 100644 --- a/src/main/java/pulse/math/FFTTransformer.java +++ b/src/main/java/pulse/math/FFTTransformer.java @@ -1,15 +1,17 @@ package pulse.math; +import java.io.Serializable; import org.apache.commons.math3.complex.Complex; import org.apache.commons.math3.transform.DftNormalization; import org.apache.commons.math3.transform.FastFourierTransformer; import org.apache.commons.math3.transform.TransformType; -public class FFTTransformer { +public class FFTTransformer implements Serializable { + private static final long serialVersionUID = -5424502578926616928L; private double[] amplitudeSpec; private double[] phaseSpec; - + private int n; //number of input points private Complex[] buffer; @@ -18,7 +20,7 @@ public class FFTTransformer { public FFTTransformer(double[] realInput) { this(Window.HANN, realInput, new double[realInput.length]); } - + public FFTTransformer(Window window, double[] realInput) { this(window, realInput, new double[realInput.length]); } @@ -68,7 +70,7 @@ public FFTTransformer(Window window, double[] realInput, double[] imagInput) { public double[] sampling(double[] x) { final double totalTime = x[n - 2] - x[0]; double[] sample = new double[buffer.length / 2]; - double fs = n/totalTime; //sampling rate + double fs = n / totalTime; //sampling rate for (int i = 0; i < sample.length; i++) { sample[i] = i * fs / buffer.length; } @@ -132,5 +134,5 @@ public double[] getAmpltiudeSpectrum() { public double[] getPhaseSpectrum() { return phaseSpec; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/FixedIntervalIntegrator.java b/src/main/java/pulse/math/FixedIntervalIntegrator.java index ac77ccb..8631efb 100644 --- a/src/main/java/pulse/math/FixedIntervalIntegrator.java +++ b/src/main/java/pulse/math/FixedIntervalIntegrator.java @@ -20,6 +20,7 @@ */ public abstract class FixedIntervalIntegrator extends AbstractIntegrator { + private static final long serialVersionUID = -5304597610450009326L; private int integrationSegments; /** diff --git a/src/main/java/pulse/math/FunctionWithInterpolation.java b/src/main/java/pulse/math/FunctionWithInterpolation.java index 9cbf3d4..41971b7 100644 --- a/src/main/java/pulse/math/FunctionWithInterpolation.java +++ b/src/main/java/pulse/math/FunctionWithInterpolation.java @@ -1,18 +1,25 @@ package pulse.math; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; +import pulse.util.FunctionSerializer; /** * An abstract class for univariate functions with the capacity of spline * interpolation. * */ -public abstract class FunctionWithInterpolation { +public abstract class FunctionWithInterpolation implements Serializable { + private static final long serialVersionUID = -303222542756574714L; private Segment tBounds; private int lookupTableSize; - private UnivariateFunction interpolation; + private transient UnivariateFunction interpolation; public final static int NUM_PARTITIONS = 8192; @@ -114,4 +121,26 @@ private void interpolate(double[] lookupTable) { interpolation = splineInterpolation.interpolate(tArray, lookupTable); } + /* + * Serialization + */ + private void writeObject(ObjectOutputStream oos) + throws IOException { + // default serialization + oos.defaultWriteObject(); + // write the object + oos.writeObject(tBounds); + oos.writeInt(lookupTableSize); + FunctionSerializer.writeSplineFunction((PolynomialSplineFunction) interpolation, oos); + } + + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + this.tBounds = (Segment) ois.readObject(); + this.lookupTableSize = ois.readInt(); + this.interpolation = FunctionSerializer.readSplineFunction(ois); + } + } diff --git a/src/main/java/pulse/math/Harmonic.java b/src/main/java/pulse/math/Harmonic.java index 06e3604..b5207f3 100644 --- a/src/main/java/pulse/math/Harmonic.java +++ b/src/main/java/pulse/math/Harmonic.java @@ -30,6 +30,8 @@ */ public class Harmonic extends PropertyHolder implements Optimisable, Comparable { + private static final long serialVersionUID = 3732379391172485157L; + private int rank = -1; private double amplitude; @@ -135,7 +137,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public void optimisationVector(ParameterVector output) { var params = output.getParameters(); - + for (int i = 0, size = params.size(); i < size; i++) { var p = params.get(i); @@ -182,13 +184,13 @@ public void optimisationVector(ParameterVector output) { var newParam = new Parameter(newId, transform, bounds); newParam.setValue(value); params.add(newParam); - + } } } - + } @Override @@ -259,7 +261,7 @@ public int compareTo(Harmonic o) { @Override public String toString() { - return String.format("[%1d]: f = %3.2f, A = %3.2f, phi = %3.2f", + return String.format("[%1d]: f = %3.2f, A = %3.2f, phi = %3.2f", rank, frequency, amplitude, phaseShift); } diff --git a/src/main/java/pulse/math/LegendrePoly.java b/src/main/java/pulse/math/LegendrePoly.java index b809507..29a82bf 100644 --- a/src/main/java/pulse/math/LegendrePoly.java +++ b/src/main/java/pulse/math/LegendrePoly.java @@ -1,5 +1,6 @@ package pulse.math; +import java.io.Serializable; import static pulse.math.MathUtils.fastPowInt; import static pulse.math.MathUtils.fastPowLoop; import static pulse.properties.NumericProperties.def; @@ -21,8 +22,9 @@ * @see Wiki * page */ -public class LegendrePoly { +public class LegendrePoly implements Serializable { + private static final long serialVersionUID = -6859690814783610846L; private double[] c; private int n; diff --git a/src/main/java/pulse/math/MidpointIntegrator.java b/src/main/java/pulse/math/MidpointIntegrator.java index cc66763..6109de5 100644 --- a/src/main/java/pulse/math/MidpointIntegrator.java +++ b/src/main/java/pulse/math/MidpointIntegrator.java @@ -11,6 +11,8 @@ */ public abstract class MidpointIntegrator extends FixedIntervalIntegrator { + private static final long serialVersionUID = -5434607461290096748L; + public MidpointIntegrator(Segment bounds, NumericProperty segments) { super(bounds, segments); } diff --git a/src/main/java/pulse/math/Parameter.java b/src/main/java/pulse/math/Parameter.java index 5556cd1..b46e02e 100644 --- a/src/main/java/pulse/math/Parameter.java +++ b/src/main/java/pulse/math/Parameter.java @@ -1,12 +1,14 @@ package pulse.math; +import java.io.Serializable; import pulse.math.transforms.Transformable; /** * Parameter class */ -public class Parameter { +public class Parameter implements Serializable { + private static final long serialVersionUID = 3222166682943107207L; private ParameterIdentifier index; private Transformable transform; private Segment bound; @@ -17,9 +19,9 @@ public Parameter(ParameterIdentifier index, Transformable transform, Segment bou this.transform = transform; this.bound = bound; } - + public Parameter(ParameterIdentifier index) { - if(index.getKeyword() != null) { + if (index.getKeyword() != null) { bound = Segment.boundsFrom(index.getKeyword()); } this.index = index; @@ -80,8 +82,8 @@ public double getApparentValue() { public void setValue(double value, boolean ignoreTransform) { this.value = transform == null || ignoreTransform - ? value - : transform.transform(value); + ? value + : transform.transform(value); } public void setValue(double value) { diff --git a/src/main/java/pulse/math/ParameterIdentifier.java b/src/main/java/pulse/math/ParameterIdentifier.java index 3fb7bc4..31a66f7 100644 --- a/src/main/java/pulse/math/ParameterIdentifier.java +++ b/src/main/java/pulse/math/ParameterIdentifier.java @@ -1,18 +1,20 @@ package pulse.math; +import java.io.Serializable; import java.util.Objects; import pulse.properties.NumericPropertyKeyword; -public class ParameterIdentifier { - +public class ParameterIdentifier implements Serializable { + + private static final long serialVersionUID = 5288875329862605319L; private NumericPropertyKeyword keyword; private int index; - + public ParameterIdentifier(NumericPropertyKeyword keyword, int index) { this.keyword = keyword; this.index = index; } - + public ParameterIdentifier(NumericPropertyKeyword keyword) { this(keyword, 0); } @@ -24,43 +26,43 @@ public int hashCode() { hash = 29 * hash + this.index; return hash; } - + public ParameterIdentifier(int index) { this.index = index; } - + public NumericPropertyKeyword getKeyword() { return keyword; } - + public int getIndex() { return index; } - + @Override public boolean equals(Object id) { - if(id.getClass() == null) { + if (id.getClass() == null) { return false; } - + var classA = id.getClass(); var classB = this.getClass(); - - if(classA != classB) { + + if (classA != classB) { return false; } - + var pid = (ParameterIdentifier) id; return keyword == pid.keyword && Math.abs(index - pid.index) < 1; } - + @Override public String toString() { StringBuilder sb = new StringBuilder("").append(keyword); - if(index > 0) { + if (index > 0) { sb.append(" # ").append(index); } return sb.toString(); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index 6bd1737..f9d27fb 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -1,9 +1,8 @@ package pulse.math; +import java.io.Serializable; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; import pulse.math.linear.Vector; @@ -15,9 +14,10 @@ * A wrapper subclass that assigns {@code ParameterIdentifier}s to specific * components of the vector. Used when constructing the optimisation vector. */ -public class ParameterVector { +public class ParameterVector implements Serializable { - private List params; + private static final long serialVersionUID = -4678286597080149891L; + private final List params; /** * Constructs an {@code IndexedVector} with the specified list of keywords. @@ -109,11 +109,11 @@ public void setValues(Vector v) { throw new IllegalArgumentException("Illegal vector dimension: " + dim + " != " + this.dimension()); } - - for(int i = 0; i < dim; i++) { + + for (int i = 0; i < dim; i++) { params.get(i).setValue(v.get(i)); } - + } public int dimension() { diff --git a/src/main/java/pulse/math/Segment.java b/src/main/java/pulse/math/Segment.java index c1e0c76..95e6c23 100644 --- a/src/main/java/pulse/math/Segment.java +++ b/src/main/java/pulse/math/Segment.java @@ -1,5 +1,6 @@ package pulse.math; +import java.io.Serializable; import java.util.Random; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericProperties.def; @@ -9,11 +10,15 @@ * that {@code a < b}. * */ -public class Segment { +public class Segment implements Serializable { + /** + * + */ + private static final long serialVersionUID = -1373763811823628708L; private double a; private double b; - + public final static Segment UNBOUNDED = new Segment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); /** @@ -36,17 +41,17 @@ public Segment(Segment segment) { this.a = segment.a; this.b = segment.b; } - + /** - * Creates a segment representing the bounds of {@code p}, i.e. the range - * in which the property value is allowed to change + * Creates a segment representing the bounds of {@code p}, i.e. the range in + * which the property value is allowed to change + * * @param p a property keyword to extract default bounds * @return a {@code Segment} with the bounds */ - public static Segment boundsFrom(NumericPropertyKeyword p) { - return new Segment(def(p).getMinimum().doubleValue(), - def(p).getMaximum().doubleValue()); + return new Segment(def(p).getMinimum().doubleValue(), + def(p).getMaximum().doubleValue()); } /** diff --git a/src/main/java/pulse/math/SimpsonIntegrator.java b/src/main/java/pulse/math/SimpsonIntegrator.java index 55ddda7..8a367d6 100644 --- a/src/main/java/pulse/math/SimpsonIntegrator.java +++ b/src/main/java/pulse/math/SimpsonIntegrator.java @@ -11,6 +11,8 @@ */ public abstract class SimpsonIntegrator extends FixedIntervalIntegrator { + private static final long serialVersionUID = -7800272372472765906L; + public SimpsonIntegrator(Segment bounds) { super(bounds); } diff --git a/src/main/java/pulse/math/Window.java b/src/main/java/pulse/math/Window.java index 48fbd4f..a230ccb 100644 --- a/src/main/java/pulse/math/Window.java +++ b/src/main/java/pulse/math/Window.java @@ -1,60 +1,58 @@ package pulse.math; -public interface Window { +import java.io.Serializable; + +public interface Window extends Serializable { public final static Window NONE = (n, N) -> 1.0; - public final static Window HANN = (n, N) -> Math.pow( Math.sin(Math.PI * n / ((double) N)), 2); - public final static Window HAMMING = (n, N) -> 0.54 + 0.46*Math.cos(2.0 * Math.PI * n / ((double) N)); + public final static Window HANN = (n, N) -> Math.pow(Math.sin(Math.PI * n / ((double) N)), 2); + public final static Window HAMMING = (n, N) -> 0.54 + 0.46 * Math.cos(2.0 * Math.PI * n / ((double) N)); public final static Window BLACKMANN_HARRIS = (n, N) -> { - final double x = 2.0*Math.PI*n/ ((double)N); - return 0.35875 - 0.48829*Math.cos(x) + 0.14128*Math.cos(2.0*x) - 0.01168*Math.cos(3.0*x); - }; - public final static Window FLAT_TOP = (n, N) -> { - final double x = 2.0*Math.PI*n/ ((double)N); - return 0.21557895 - 0.41663158*Math.cos(x) + 0.277263158*Math.cos(2.0*x) - - 0.083578947*Math.cos(3.0*x) + 0.006947368 * Math.cos(4.0 * x); - }; + final double x = 2.0 * Math.PI * n / ((double) N); + return 0.35875 - 0.48829 * Math.cos(x) + 0.14128 * Math.cos(2.0 * x) - 0.01168 * Math.cos(3.0 * x); + }; + public final static Window FLAT_TOP = (n, N) -> { + final double x = 2.0 * Math.PI * n / ((double) N); + return 0.21557895 - 0.41663158 * Math.cos(x) + 0.277263158 * Math.cos(2.0 * x) + - 0.083578947 * Math.cos(3.0 * x) + 0.006947368 * Math.cos(4.0 * x); + }; public final static Window TUKEY = new Window() { - + private final static double alpha = 0.6; - + @Override public double evaluate(int n, int N) { - + double result = 0; - - if(n < 0.5*alpha*N) { - result = 0.5 * ( 1 - Math.cos(2.0*Math.PI*n/(alpha*N))); - } - - else if(n <= N/2) { + + if (n < 0.5 * alpha * N) { + result = 0.5 * (1 - Math.cos(2.0 * Math.PI * n / (alpha * N))); + } else if (n <= N / 2) { result = 1.0; + } else { + result = TUKEY.evaluate(N - n, N); } - - else { - result = TUKEY.evaluate(N - n,N); - } - + return result; - + } }; - + public final static Window HANN_POISSON = (n, N) -> { - + final double alpha = 2.0; - return HANN.evaluate(n, N) * Math.exp( - alpha * (N - 2 * n) / N); - + return HANN.evaluate(n, N) * Math.exp(-alpha * (N - 2 * n) / N); + }; - + public default double[] apply(double[] input) { double[] output = new double[input.length]; - for(int i = 0; i < output.length; i++) { + for (int i = 0; i < output.length; i++) { output[i] = input[i] * evaluate(i, input.length); } return output; } - + public abstract double evaluate(int n, int N); - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/ZScore.java b/src/main/java/pulse/math/ZScore.java index fa13fad..83be83a 100644 --- a/src/main/java/pulse/math/ZScore.java +++ b/src/main/java/pulse/math/ZScore.java @@ -17,23 +17,22 @@ * calculated as the difference of the current value and population mean divided * by the population standard deviation. */ - public class ZScore { - + private double[] avgFilter; private double[] stdFilter; private int[] signals; - + private int lag; private double threshold; private double influence; - + public ZScore(int lag, double threshold, double influence) { this.lag = lag; this.threshold = threshold; this.influence = influence; } - + public ZScore() { this(40, 3.5, 0.3); } @@ -42,32 +41,32 @@ public void process(double[] input) { signals = new int[input.length]; List filteredY = DoubleStream.of(input).boxed().collect(Collectors.toList()); - - var initialWindow = filteredY.subList(input.length - lag, input.length - 1); + + var initialWindow = new ArrayList<>(filteredY.subList(input.length - lag, input.length - 1)); avgFilter = new double[input.length]; stdFilter = new double[input.length]; - + avgFilter[input.length - lag + 1] = mean(initialWindow); stdFilter[input.length - lag + 1] = stdev(initialWindow); for (int i = input.length - lag; i > 0; i--) { - + if (Math.abs(input[i] - avgFilter[i + 1]) > threshold * stdFilter[i + 1]) { - + signals[i] = (input[i] > avgFilter[i + 1]) ? 1 : -1; - filteredY.set(i, influence * input[i] - + (1 - influence) * filteredY.get(i + 1)); - + filteredY.set(i, influence * input[i] + + (1 - influence) * filteredY.get(i + 1)); + } else { - + signals[i] = 0; filteredY.set(i, input[i]); - + } // Update rolling average and deviation - var slidingWindow = filteredY.subList(i, i + lag - 1); + var slidingWindow = new ArrayList<>(filteredY.subList(i, i + lag - 1)); avgFilter[i] = mean(slidingWindow); stdFilter[i] = stdev(slidingWindow); @@ -89,19 +88,19 @@ private static double stdev(List values) { } return ret; } - + public int[] getSignals() { return signals; } - + public double[] getFilteredAverage() { return avgFilter; } - + public double[] getFilteredStdev() { return stdFilter; } - + /* public static void main(String[] args) { Scanner sc = null; @@ -129,6 +128,5 @@ public static void main(String[] args) { } } - */ - -} \ No newline at end of file + */ +} diff --git a/src/main/java/pulse/math/filters/AssignmentListener.java b/src/main/java/pulse/math/filters/AssignmentListener.java index 251884a..6020aa6 100644 --- a/src/main/java/pulse/math/filters/AssignmentListener.java +++ b/src/main/java/pulse/math/filters/AssignmentListener.java @@ -1,7 +1,9 @@ package pulse.math.filters; -public interface AssignmentListener { - +import java.io.Serializable; + +public interface AssignmentListener extends Serializable { + public void onValueAssigned(); - + } \ No newline at end of file diff --git a/src/main/java/pulse/math/filters/Filter.java b/src/main/java/pulse/math/filters/Filter.java index 067ab3e..0bba2ae 100644 --- a/src/main/java/pulse/math/filters/Filter.java +++ b/src/main/java/pulse/math/filters/Filter.java @@ -1,14 +1,16 @@ package pulse.math.filters; import java.awt.geom.Point2D; +import java.io.Serializable; import java.util.List; import pulse.DiscreteInput; -public interface Filter { +public interface Filter extends Serializable { + + public List process(List input); - public List process(List input); public default List process(DiscreteInput input) { return process(DiscreteInput.convert(input.getX(), input.getY())); - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/pulse/math/filters/HalfTimeCalculator.java b/src/main/java/pulse/math/filters/HalfTimeCalculator.java index e2ca9ce..7f98f3e 100644 --- a/src/main/java/pulse/math/filters/HalfTimeCalculator.java +++ b/src/main/java/pulse/math/filters/HalfTimeCalculator.java @@ -1,6 +1,7 @@ package pulse.math.filters; import java.awt.geom.Point2D; +import java.io.Serializable; import static java.lang.Double.valueOf; import static java.util.Collections.max; import java.util.Comparator; @@ -9,20 +10,21 @@ import pulse.baseline.FlatBaseline; import pulse.input.IndexRange; -public class HalfTimeCalculator { - +public class HalfTimeCalculator implements Serializable { + + private static final long serialVersionUID = 8302980290467110065L; private final Filter filter; private final DiscreteInput data; private Point2D max; private double halfTime; - + /** * A fail-safe factor. */ public final static double FAIL_SAFE_FACTOR = 10.0; - private static final Comparator pointComparator = - (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); + private static final Comparator pointComparator + = (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); public HalfTimeCalculator(DiscreteInput input) { this.data = input; @@ -40,56 +42,55 @@ public HalfTimeCalculator(DiscreteInput input) { * The index corresponding to the closest temperature value available for * that curve is used to retrieve the half-rise time (which also has the * same index). If this fails, i.e. the associated index is less than 1, - * this will print out a warning message and still assign a value to the - * half-time variable equal to the acquisition time divided by a fail-safe factor - * {@value FAIL_SAFE_FACTOR}. - *

+ * this will print out a warning message and still assign a value to the + * half-time variable equal to the acquisition time divided by a fail-safe + * factor {@value FAIL_SAFE_FACTOR}. + *

+ * * @see getHalfTime() */ public void calculate() { var baseline = new FlatBaseline(); baseline.fitTo(data); - + var filtered = filter.process(data); - + max = max(filtered, pointComparator); - double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; - + double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; + int indexLeft = IndexRange.closestLeft(halfMax, - filtered.stream().map(point -> point.getY()) - .collect(Collectors.toList())); - + filtered.stream().map(point -> point.getY()) + .collect(Collectors.toList())); + if (indexLeft < 1 || indexLeft > filtered.size() - 2) { halfTime = filtered.get(filtered.size() - 1).getX() / FAIL_SAFE_FACTOR; - } - else { + } else { //extrapolate Point2D p1 = filtered.get(indexLeft); Point2D p2 = filtered.get(indexLeft + 1); - - halfTime = (halfMax - p1.getY())/(p2.getY() - p1.getY()) - *(p2.getX() - p1.getX()) + p1.getX(); + + halfTime = (halfMax - p1.getY()) / (p2.getY() - p1.getY()) + * (p2.getX() - p1.getX()) + p1.getX(); } - + } - - + /** - * Retrieves the half-time value of this dataset, which is equal to the - * time needed to reach half of the signal maximum. + * Retrieves the half-time value of this dataset, which is equal to the time + * needed to reach half of the signal maximum. + * * @return the half-time value. */ - public final double getHalfTime() { return halfTime; } - + public final Point2D getFilteredMaximum() { return max; } - + public DiscreteInput getData() { return data; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/filters/OptimisablePolyline.java b/src/main/java/pulse/math/filters/OptimisablePolyline.java index 4d7a8d6..4356b4f 100644 --- a/src/main/java/pulse/math/filters/OptimisablePolyline.java +++ b/src/main/java/pulse/math/filters/OptimisablePolyline.java @@ -14,9 +14,10 @@ public class OptimisablePolyline extends PropertyHolder implements Optimisable { + private static final long serialVersionUID = 418264754603533971L; private final double[] x; private final double[] y; - private final List listeners; + private List listeners; public OptimisablePolyline(List data) { x = data.stream().mapToDouble(d -> d.getX()).toArray(); @@ -27,7 +28,7 @@ public OptimisablePolyline(List data) { @Override public void assign(ParameterVector input) throws SolverException { var ps = input.getParameters(); - for(int i = 0, size = ps.size(); i < size; i++) { + for (int i = 0, size = ps.size(); i < size; i++) { y[i] = ps.get(i).getApparentValue(); } listeners.stream().forEach(l -> l.onValueAssigned()); @@ -37,7 +38,7 @@ public void assign(ParameterVector input) throws SolverException { public void optimisationVector(ParameterVector output) { output.setValues(new Vector(y)); } - + public List points() { return DiscreteInput.convert(x, y); } @@ -46,17 +47,17 @@ public List points() { public void set(NumericPropertyKeyword type, NumericProperty property) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - + public double[] getX() { return x; } - + public double[] getY() { return y; } - + public void addAssignmentListener(AssignmentListener listener) { listeners.add(listener); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/filters/OptimisedRunningAverage.java b/src/main/java/pulse/math/filters/OptimisedRunningAverage.java index 46c004b..0f9ce5a 100644 --- a/src/main/java/pulse/math/filters/OptimisedRunningAverage.java +++ b/src/main/java/pulse/math/filters/OptimisedRunningAverage.java @@ -6,6 +6,8 @@ public class OptimisedRunningAverage extends RunningAverage { + private static final long serialVersionUID = 1272276960302188392L; + public OptimisedRunningAverage() { super(); } @@ -17,10 +19,10 @@ public OptimisedRunningAverage(int reductionFactor) { @Override public List process(DiscreteInput input) { var p = super.process(input); - var optimisableCurve = new OptimisablePolyline(p); + var optimisableCurve = new OptimisablePolyline(p); var task = new PolylineOptimiser(input, optimisableCurve); task.run(); return optimisableCurve.points(); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/filters/PolylineOptimiser.java b/src/main/java/pulse/math/filters/PolylineOptimiser.java index dde0142..c2f7ffb 100644 --- a/src/main/java/pulse/math/filters/PolylineOptimiser.java +++ b/src/main/java/pulse/math/filters/PolylineOptimiser.java @@ -20,6 +20,7 @@ public class PolylineOptimiser extends SimpleOptimisationTask { + private static final long serialVersionUID = -9056678836812293655L; private final OptimiserStatistic sos; private final PolylineResponse response; private final OptimisablePolyline optimisableCurve; @@ -87,4 +88,4 @@ public double evaluate(double t) { } } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/filters/Randomiser.java b/src/main/java/pulse/math/filters/Randomiser.java index 3d15453..cfd6d71 100644 --- a/src/main/java/pulse/math/filters/Randomiser.java +++ b/src/main/java/pulse/math/filters/Randomiser.java @@ -4,23 +4,24 @@ import java.util.List; public class Randomiser implements Filter { - + + private static final long serialVersionUID = 3390706390237573886L; private final double amplitude; - + public Randomiser(double amplitude) { this.amplitude = amplitude; } @Override - public List process(List input) { - input.forEach(p -> - ((Point2D.Double)p).y += (Math.random() - 0.5) * amplitude + public List process(List input) { + input.forEach(p + -> ((Point2D.Double) p).y += (Math.random() - 0.5) * amplitude ); return input; } - + public double getAmplitude() { return amplitude; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/filters/RunningAverage.java b/src/main/java/pulse/math/filters/RunningAverage.java index 37a08a2..2a2b82b 100644 --- a/src/main/java/pulse/math/filters/RunningAverage.java +++ b/src/main/java/pulse/math/filters/RunningAverage.java @@ -3,30 +3,29 @@ import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.List; -import pulse.DiscreteInput; public class RunningAverage implements Filter { - + + private static final long serialVersionUID = -6134297308468858848L; + private int bins; - - /** + + /** * The binning factor used to build a crude approximation of the heating * curve. Described in Lunev, A., & Heymer, R. (2020). Review of * Scientific Instruments, 91(6), 064902. */ - public static final int DEFAULT_BINS = 16; public final static int MIN_BINS = 4; - + /** * @param reductionFactor the factor, by which the number of points * {@code count} will be reduced for this {@code ExperimentalData}. */ - public RunningAverage(int reductionFactor) { this.bins = reductionFactor; } - + public RunningAverage() { this.bins = DEFAULT_BINS; } @@ -53,7 +52,6 @@ public RunningAverage() { * @see halfRiseTime() * @see pulse.AbstractData.maxTemperature() */ - @Override public List process(List points) { var x = points.stream().mapToDouble(p -> p.getX()).toArray(); @@ -62,14 +60,14 @@ public List process(List points) { int size = x.length; int step = size / bins; List movingAverage = new ArrayList<>(bins); - + for (int i = 0; i < bins; i++) { - int i1 = step*i; - int i2 = step*(i+1); + int i1 = step * i; + int i2 = step * (i + 1); double av = 0; int j; - + for (j = i1; j < i2 && j < size; j++) { av += y[j]; } @@ -77,55 +75,54 @@ public List process(List points) { av /= j - i1; i2 = j - 1; - movingAverage.add(new Point2D.Double( - (x[i1] + x[i2])/ 2.0, av)); + movingAverage.add(new Point2D.Double( + (x[i1] + x[i2]) / 2.0, av)); } - + addBoundaryPoints(movingAverage, x[0], x[size - 1]); - + /* for(int i = 0; i < movingAverage.size(); i++) { System.err.println(movingAverage.get(i)); } - */ - + */ return movingAverage; - } - + } + private static void addBoundaryPoints(List d, double minTime, double maxTime) { int max = d.size(); - + d.add( - extrapolate(d.get(max - 1), - d.get(max - 2), - maxTime) - ); - - d.add( 0, - extrapolate(d.get(0), - d.get(1), - minTime) - ); - + extrapolate(d.get(max - 1), + d.get(max - 2), + maxTime) + ); + + d.add(0, + extrapolate(d.get(0), + d.get(1), + minTime) + ); + } - - private static Point2D extrapolate(Point2D a, Point2D b, double x) { + + private static Point2D extrapolate(Point2D a, Point2D b, double x) { double y1 = a.getY(); double y2 = b.getY(); double x1 = a.getX(); double x2 = b.getX(); - - return new Point2D.Double(x, y1 + (x - x1)/(x2 - x1)*(y2 - y1)); + + return new Point2D.Double(x, y1 + (x - x1) / (x2 - x1) * (y2 - y1)); } - + public final int getNumberOfBins() { return bins; } - + public final void setNumberOfBins(int no) { this.bins = no > MIN_BINS - 1 ? no : MIN_BINS; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/math/linear/Matrix2.java b/src/main/java/pulse/math/linear/Matrix2.java index aa57984..00cf409 100644 --- a/src/main/java/pulse/math/linear/Matrix2.java +++ b/src/main/java/pulse/math/linear/Matrix2.java @@ -6,6 +6,8 @@ */ class Matrix2 extends SquareMatrix { + private static final long serialVersionUID = 6015187791989387058L; + protected Matrix2(double[][] args) { super(args); } diff --git a/src/main/java/pulse/math/linear/Matrix3.java b/src/main/java/pulse/math/linear/Matrix3.java index 09e799f..3835e32 100644 --- a/src/main/java/pulse/math/linear/Matrix3.java +++ b/src/main/java/pulse/math/linear/Matrix3.java @@ -6,6 +6,8 @@ */ class Matrix3 extends SquareMatrix { + private static final long serialVersionUID = -2671066600560428989L; + protected Matrix3(double[][] args) { super(args); } diff --git a/src/main/java/pulse/math/linear/Matrix4.java b/src/main/java/pulse/math/linear/Matrix4.java index 970a7a9..cc34b17 100644 --- a/src/main/java/pulse/math/linear/Matrix4.java +++ b/src/main/java/pulse/math/linear/Matrix4.java @@ -6,6 +6,8 @@ */ class Matrix4 extends SquareMatrix { + private static final long serialVersionUID = -1355372261335732541L; + protected Matrix4(double[][] args) { super(args); } diff --git a/src/main/java/pulse/math/linear/RectangularMatrix.java b/src/main/java/pulse/math/linear/RectangularMatrix.java index 8f325dc..8ac8470 100644 --- a/src/main/java/pulse/math/linear/RectangularMatrix.java +++ b/src/main/java/pulse/math/linear/RectangularMatrix.java @@ -1,5 +1,6 @@ package pulse.math.linear; +import java.io.Serializable; import static pulse.math.MathUtils.approximatelyEquals; import static pulse.math.linear.ArithmeticOperations.DIFFERENCE; import static pulse.math.linear.ArithmeticOperations.SUM; @@ -7,8 +8,9 @@ import pulse.ui.Messages; -public class RectangularMatrix { +public class RectangularMatrix implements Serializable { + private static final long serialVersionUID = -8184303238440935851L; protected final double[][] x; protected RectangularMatrix(double[][] args) { diff --git a/src/main/java/pulse/math/linear/Vector.java b/src/main/java/pulse/math/linear/Vector.java index 39d0653..483cf0c 100644 --- a/src/main/java/pulse/math/linear/Vector.java +++ b/src/main/java/pulse/math/linear/Vector.java @@ -1,5 +1,6 @@ package pulse.math.linear; +import java.io.Serializable; import static java.lang.Math.abs; import static java.lang.Math.sqrt; import java.util.List; @@ -16,8 +17,12 @@ * and ODE solvers. *

*/ -public class Vector { +public class Vector implements Serializable { + /** + * + */ + private static final long serialVersionUID = 5560069982536341831L; private double[] x; /** @@ -122,7 +127,7 @@ public static Vector random(int n, double min, double max) { } return v; } - + /** * Component-wise vector multiplication */ diff --git a/src/main/java/pulse/math/transforms/AtanhTransform.java b/src/main/java/pulse/math/transforms/AtanhTransform.java index b65577e..253fd0e 100644 --- a/src/main/java/pulse/math/transforms/AtanhTransform.java +++ b/src/main/java/pulse/math/transforms/AtanhTransform.java @@ -11,6 +11,8 @@ */ public class AtanhTransform extends BoundedParameterTransform { + private static final long serialVersionUID = -6322775329000050307L; + /** * Only the upper bound of the argument is used. * diff --git a/src/main/java/pulse/math/transforms/InvDiamTransform.java b/src/main/java/pulse/math/transforms/InvDiamTransform.java index c881081..22345df 100644 --- a/src/main/java/pulse/math/transforms/InvDiamTransform.java +++ b/src/main/java/pulse/math/transforms/InvDiamTransform.java @@ -8,6 +8,7 @@ */ public class InvDiamTransform implements Transformable { + private static final long serialVersionUID = 1809584085307619279L; private double d; public InvDiamTransform(ExtendedThermalProperties etp) { diff --git a/src/main/java/pulse/math/transforms/PeriodicTransform.java b/src/main/java/pulse/math/transforms/PeriodicTransform.java index 31cee06..c788a9b 100644 --- a/src/main/java/pulse/math/transforms/PeriodicTransform.java +++ b/src/main/java/pulse/math/transforms/PeriodicTransform.java @@ -4,6 +4,8 @@ public class PeriodicTransform extends BoundedParameterTransform { + private static final long serialVersionUID = 4564881912462997982L; + /** * Only the upper bound of the argument is used. * @@ -23,7 +25,7 @@ public double transform(double a) { double max = getBounds().getMaximum(); double min = getBounds().getMinimum(); double len = max - min; - + return a > max ? transform(a - len) : (a < min ? transform(a + len) : a); } @@ -35,4 +37,4 @@ public double transform(double a) { public double inverse(double t) { return t; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/transforms/StickTransform.java b/src/main/java/pulse/math/transforms/StickTransform.java index f548761..ce5fd4a 100644 --- a/src/main/java/pulse/math/transforms/StickTransform.java +++ b/src/main/java/pulse/math/transforms/StickTransform.java @@ -18,16 +18,18 @@ import pulse.math.Segment; /** - * A simple bounded transform which makes the parameter stick to the - * boundaries upon reaching them. For insatnce, when a parameter x + * A simple bounded transform which makes the parameter stick to the boundaries + * upon reaching them. For insatnce, when a parameter x * attempts to escape its bounds due to a larger increment then allowed, this * transform will return it directly to the respective boundary, where it will * "stick". + * * @author Artem Lunev */ - public class StickTransform extends BoundedParameterTransform { + private static final long serialVersionUID = -8709273330809657074L; + /** * Only the upper bound of the argument is used. * @@ -57,5 +59,5 @@ public double transform(double a) { public double inverse(double t) { return transform(t); } - + } diff --git a/src/main/java/pulse/math/transforms/Transformable.java b/src/main/java/pulse/math/transforms/Transformable.java index 88e9d13..150f6c3 100644 --- a/src/main/java/pulse/math/transforms/Transformable.java +++ b/src/main/java/pulse/math/transforms/Transformable.java @@ -1,11 +1,13 @@ package pulse.math.transforms; +import java.io.Serializable; + /** * An interface for performing reversible one-to-one mapping of the model * parameters. * */ -public interface Transformable { +public interface Transformable extends Serializable { /** * Performs the selected transform with {@code value} diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index 8891324..5ec9be1 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -1,5 +1,6 @@ package pulse.problem.laser; +import java.io.Serializable; import java.util.Objects; import pulse.input.ExperimentalData; import pulse.math.MidpointIntegrator; @@ -19,8 +20,9 @@ * * @see pulse.problem.statements.Pulse */ -public class DiscretePulse { +public class DiscretePulse implements Serializable { + private static final long serialVersionUID = 5826506918603729615L; private final Grid grid; private final Pulse pulse; private final ExperimentalData data; diff --git a/src/main/java/pulse/problem/laser/DiscretePulse2D.java b/src/main/java/pulse/problem/laser/DiscretePulse2D.java index 02a60f0..21be2b8 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse2D.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse2D.java @@ -18,14 +18,14 @@ */ public class DiscretePulse2D extends DiscretePulse { + private static final long serialVersionUID = 6203222036852037146L; private double discretePulseSpot; private double sampleRadius; private double normFactor; - + /** * This had to be decreased for the 2d pulses. */ - private final static int WIDTH_TOLERANCE_FACTOR = 1000; /** @@ -43,9 +43,9 @@ public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { super(problem, grid); var properties = (ExtendedThermalProperties) problem.getProperties(); calcPulseSpot(properties); - properties.addListener(e -> calcPulseSpot(properties) ); + properties.addListener(e -> calcPulseSpot(properties)); } - + /** * This calculates the dimensionless, discretised pulse function at a * dimensionless radial coordinate {@code coord}. @@ -61,58 +61,58 @@ public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { * {@code coord > spotDiameter}. * @see pulse.problem.laser.PulseTemporalShape.laserPowerAt(double) */ - public double evaluateAt(double time, double radialCoord) { - return laserPowerAt(time) + return laserPowerAt(time) * (0.5 + 0.5 * signum(discretePulseSpot - radialCoord)); } - + /** - * Calculates the laser power at a give moment in time. The total laser - * energy is normalised over a beam partially illuminating the sample surface. + * Calculates the laser power at a give moment in time. The total laser + * energy is normalised over a beam partially illuminating the sample + * surface. + * * @param time a moment in time (in dimensionless units) * @return the laser power in arbitrary units */ - @Override public double laserPowerAt(double time) { return normFactor * super.laserPowerAt(time); } - + private void calcPulseSpot(ExtendedThermalProperties properties) { - sampleRadius = (double) properties.getSampleDiameter().getValue() / 2.0; + sampleRadius = (double) properties.getSampleDiameter().getValue() / 2.0; evalPulseSpot(); } /** - * Calculates the {@code discretePulseSpot} using the {@code gridRadialDistance} method. + * Calculates the {@code discretePulseSpot} using the + * {@code gridRadialDistance} method. * * @see pulse.problem.schemes.Grid2D.gridRadialDistance(double,double) */ public final void evalPulseSpot() { var pulse = (Pulse2D) getPhysicalPulse(); var grid2d = (Grid2D) getGrid(); - final double spotRadius = (double) pulse.getSpotDiameter().getValue() / 2.0; + final double spotRadius = (double) pulse.getSpotDiameter().getValue() / 2.0; discretePulseSpot = grid2d.gridRadialDistance(spotRadius, sampleRadius); grid2d.adjustStepSize(this); - normFactor = sampleRadius * sampleRadius / spotRadius / spotRadius; + normFactor = sampleRadius * sampleRadius / spotRadius / spotRadius; } public final double getDiscretePulseSpot() { return discretePulseSpot; } - + public final double getRadialConversionFactor() { return sampleRadius; } - + /** * A smaller tolerance factor is set for 2D calculations */ - @Override public int getWidthToleranceFactor() { return WIDTH_TOLERANCE_FACTOR; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java index 379ab97..d4490d2 100644 --- a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java +++ b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java @@ -24,10 +24,11 @@ */ public class ExponentiallyModifiedGaussian extends PulseTemporalShape { + private static final long serialVersionUID = -4437794069527301235L; private double mu; private double sigma; private double lambda; - + private final static int MIN_POINTS = 10; /** @@ -159,4 +160,4 @@ public int getRequiredDiscretisation() { return MIN_POINTS; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java index 9dbaadb..a71d65f 100644 --- a/src/main/java/pulse/problem/laser/NumericPulse.java +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -1,10 +1,14 @@ package pulse.problem.laser; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; import org.apache.commons.math3.analysis.UnivariateFunction; import org.apache.commons.math3.analysis.interpolation.AkimaSplineInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; import pulse.input.ExperimentalData; import pulse.problem.statements.Problem; @@ -14,6 +18,7 @@ import pulse.baseline.FlatBaseline; import pulse.tasks.Calculation; +import pulse.util.FunctionSerializer; /** * A numeric pulse is given by a set of discrete {@code NumericPulseData} @@ -24,9 +29,10 @@ */ public class NumericPulse extends PulseTemporalShape { + private static final long serialVersionUID = 6088261629992349844L; private NumericPulseData pulseData; - private UnivariateFunction interpolation; - + private transient UnivariateFunction interpolation; + private final static int MIN_POINTS = 20; public NumericPulse() { @@ -57,40 +63,42 @@ public NumericPulse(NumericPulse pulse) { public void init(ExperimentalData data, DiscretePulse pulse) { //generate baseline-subtracted numeric data from ExperimentalData baselineSubtractedFrom(data); - + //notify host pulse object of a new pulse width - var problem = ( (Calculation) ((SearchTask) data.getParent()) - .getResponse() ).getProblem(); + var problem = ((Calculation) ((SearchTask) data.getParent()) + .getResponse()).getProblem(); setPulseWidthOf(problem); //convert to dimensionless time and interpolate double timeFactor = problem.getProperties().characteristicTime(); doInterpolation(timeFactor); } - + /** - * Copies the numeric pulse from metadata and subtracts a horizontal baseline - * from the data points assigned to {@code pulseData}. - * @param data the experimental data containing the metadata with numeric pulse data. + * Copies the numeric pulse from metadata and subtracts a horizontal + * baseline from the data points assigned to {@code pulseData}. + * + * @param data the experimental data containing the metadata with numeric + * pulse data. */ - private void baselineSubtractedFrom(ExperimentalData data) { pulseData = new NumericPulseData(data.getMetadata().getPulseData()); - + //subtracts a horizontal baseline from the pulse data var baseline = new FlatBaseline(); baseline.fitTo(pulseData); - - for(int i = 0, size = pulseData.getTimeSequence().size(); i < size; i++) - pulseData.setSignalAt(i, + + for (int i = 0, size = pulseData.getTimeSequence().size(); i < size; i++) { + pulseData.setSignalAt(i, pulseData.signalAt(i) - baseline.valueAt(pulseData.timeAt(i))); + } } private void setPulseWidthOf(Problem problem) { - var timeSequence = pulseData.getTimeSequence(); - double pulseWidth = timeSequence.get(timeSequence.size() - 1); + var timeSequence = pulseData.getTimeSequence(); + double pulseWidth = timeSequence.get(timeSequence.size() - 1); - var pulseObject = problem.getPulse(); + var pulseObject = problem.getPulse(); pulseObject.setPulseWidth(derive(PULSE_WIDTH, pulseWidth)); } @@ -98,11 +106,11 @@ private void setPulseWidthOf(Problem problem) { private void doInterpolation(double timeFactor) { var interpolator = new AkimaSplineInterpolator(); - var timeList = pulseData.getTimeSequence().stream().mapToDouble(d -> d / timeFactor).toArray(); - var powerList = pulseData.getSignalData(); + var timeList = pulseData.getTimeSequence().stream().mapToDouble(d -> d / timeFactor).toArray(); + var powerList = pulseData.getSignalData(); this.setPulseWidth(timeList[timeList.length - 1]); - + interpolation = interpolator.interpolate(timeList, powerList.stream().mapToDouble(d -> d).toArray()); } @@ -135,7 +143,7 @@ public NumericPulseData getData() { public void setData(NumericPulseData pulseData) { this.pulseData = pulseData; - + } public UnivariateFunction getInterpolation() { @@ -147,4 +155,22 @@ public int getRequiredDiscretisation() { return MIN_POINTS; } + /* + Serialization + */ + private void writeObject(ObjectOutputStream oos) + throws IOException { + // default serialization + oos.defaultWriteObject(); + // write the object + FunctionSerializer.writeSplineFunction((PolynomialSplineFunction) interpolation, oos); + } + + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + this.interpolation = FunctionSerializer.readSplineFunction(ois); + } + } diff --git a/src/main/java/pulse/problem/laser/NumericPulseData.java b/src/main/java/pulse/problem/laser/NumericPulseData.java index 5a53fd0..7bbfeee 100644 --- a/src/main/java/pulse/problem/laser/NumericPulseData.java +++ b/src/main/java/pulse/problem/laser/NumericPulseData.java @@ -14,6 +14,7 @@ */ public class NumericPulseData extends AbstractData implements DiscreteInput { + private static final long serialVersionUID = 8142129124831241206L; private final int externalID; /** @@ -54,7 +55,7 @@ public void addPoint(double time, double power) { public int getExternalID() { return externalID; } - + public double pulseWidth() { return super.timeLimit(); } @@ -73,5 +74,5 @@ public List getY() { public IndexRange getIndexRange() { return new IndexRange(this.getTimeSequence(), Range.UNLIMITED); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/laser/PulseTemporalShape.java b/src/main/java/pulse/problem/laser/PulseTemporalShape.java index 6c3814e..9a74c09 100644 --- a/src/main/java/pulse/problem/laser/PulseTemporalShape.java +++ b/src/main/java/pulse/problem/laser/PulseTemporalShape.java @@ -1,6 +1,5 @@ package pulse.problem.laser; - import pulse.input.ExperimentalData; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -23,7 +22,7 @@ public PulseTemporalShape() { public PulseTemporalShape(PulseTemporalShape another) { this.width = another.width; } - + /** * This evaluates the dimensionless, discretised pulse function on a * {@code grid} needed to evaluate the heat source in the difference scheme. @@ -64,7 +63,7 @@ public double getPulseWidth() { public void setPulseWidth(double width) { this.width = width; } - + public abstract int getRequiredDiscretisation(); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/RectangularPulse.java b/src/main/java/pulse/problem/laser/RectangularPulse.java index 583a92e..7bc5dfb 100644 --- a/src/main/java/pulse/problem/laser/RectangularPulse.java +++ b/src/main/java/pulse/problem/laser/RectangularPulse.java @@ -14,11 +14,12 @@ */ public class RectangularPulse extends PulseTemporalShape { + private static final long serialVersionUID = 8207478409316696745L; private final static int MIN_POINTS = 2; - + /** * @param time the time measured from the start of the laser pulse. - * @return + * @return */ @Override public double evaluateAt(double time) { @@ -35,10 +36,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public PulseTemporalShape copy() { return new RectangularPulse(); } - + @Override public int getRequiredDiscretisation() { return MIN_POINTS; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java index a061a87..908e2cf 100644 --- a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java +++ b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java @@ -18,12 +18,13 @@ */ public class TrapezoidalPulse extends PulseTemporalShape { + private static final long serialVersionUID = 2089809680713225034L; private double rise; private double fall; private double h; private final static int MIN_POINTS = 8; - + /** * Constructs a trapezoidal pulse using a default segmentation principle. * The reader is referred to the {@code .xml} file containing the default @@ -42,7 +43,7 @@ public TrapezoidalPulse(TrapezoidalPulse another) { this.fall = another.fall; this.h = another.h; } - + /** * Calculates the height of the trapezium which under current segmentation * will yield an area of unity. @@ -125,10 +126,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public PulseTemporalShape copy() { return new TrapezoidalPulse(this); } - + @Override public int getRequiredDiscretisation() { return MIN_POINTS; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/ADIScheme.java b/src/main/java/pulse/problem/schemes/ADIScheme.java index db0c7e4..b20b34d 100644 --- a/src/main/java/pulse/problem/schemes/ADIScheme.java +++ b/src/main/java/pulse/problem/schemes/ADIScheme.java @@ -14,6 +14,11 @@ */ public abstract class ADIScheme extends DifferenceScheme { + /** + * + */ + private static final long serialVersionUID = 4772650159522354367L; + /** * Creates a new {@code ADIScheme} with default values of grid density and * time factor. @@ -56,13 +61,14 @@ public ADIScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty public String toString() { return getString("ADIScheme.4"); } - + /** - * Contains only an empty statement, as the pulse needs to be calculated not only - * for the time step {@code m} but also accounting for the radial coordinate + * Contains only an empty statement, as the pulse needs to be calculated not + * only for the time step {@code m} but also accounting for the radial + * coordinate + * * @param m thte time step */ - @Override public void prepareStep(int m) { //do nothing diff --git a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java index acaa5cd..450db9d 100644 --- a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java @@ -11,6 +11,7 @@ */ public class BlockMatrixAlgorithm extends TridiagonalMatrixAlgorithm { + private static final long serialVersionUID = -6553638438386098008L; private final double[] gamma; private final double[] p; private final double[] q; @@ -36,7 +37,7 @@ public void evaluateBeta(final double[] U) { super.evaluateBeta(U); var alpha = getAlpha(); var beta = getBeta(); - + final int N = getGridPoints(); p[N - 1] = beta[N]; @@ -51,7 +52,7 @@ public void evaluateBeta(final double[] U) { @Override public void evaluateBeta(final double[] U, final int start, final int endExclusive) { var alpha = getAlpha(); - + final double h = this.getGridStep(); final double HX2_TAU = h * h / this.getTimeStep(); diff --git a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java index f4577d9..17976a2 100644 --- a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java @@ -14,6 +14,7 @@ public abstract class CoupledImplicitScheme extends ImplicitScheme { + private static final long serialVersionUID = 1974655675470727643L; private RadiativeTransferCoupling coupling; private RTECalculationStatus calculationStatus; private boolean autoUpdateFluxes = true; //should be false for nonlinear solvers @@ -29,11 +30,11 @@ public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor, Nume this(N, timeFactor); setTimeLimit(timeLimit); } - + @Override public void finaliseStep() throws SolverException { super.finaliseStep(); - if(autoUpdateFluxes) { + if (autoUpdateFluxes) { var rte = this.getCoupling().getRadiativeTransferEquation(); setCalculationStatus(rte.compute(getCurrentSolution())); } @@ -59,11 +60,11 @@ public final RTECalculationStatus getCalculationStatus() { public final void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.calculationStatus = calculationStatus; if (calculationStatus != RTECalculationStatus.NORMAL) { - throw new SolverException(calculationStatus.toString(), + throw new SolverException(calculationStatus.toString(), RTE_SOLVER_ERROR); } } - + public final RadiativeTransferCoupling getCoupling() { return coupling; } @@ -72,18 +73,18 @@ public final void setCoupling(RadiativeTransferCoupling coupling) { this.coupling = coupling; this.coupling.setParent(this); } - + public final boolean isAutoUpdateFluxes() { return this.autoUpdateFluxes; } - + public final void setAutoUpdateFluxes(boolean auto) { this.autoUpdateFluxes = auto; } - + @Override public Class[] domain() { return new Class[]{ParticipatingMedium.class}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 8ad8dfd..64de3f8 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -29,7 +29,7 @@ */ public abstract class DifferenceScheme extends PropertyHolder implements Reflexive { - private DiscretePulse discretePulse; + private transient DiscretePulse discretePulse; private Grid grid; private double timeLimit; @@ -92,7 +92,7 @@ public void copyFrom(DifferenceScheme df) { protected void prepare(Problem problem) throws SolverException { if (discretePulse == null) { discretePulse = problem.discretePulseOn(grid); - } + } discretePulse.init(); clearArrays(); } @@ -114,12 +114,12 @@ public void runTimeSequence(Problem problem, final double offset, final double e int numPoints = (int) curve.getNumPoints().getValue(); - final double startTime = (double) curve.getTimeShift().getValue(); + final double startTime = (double) curve.getTimeShift().getValue(); final double timeSegment = (endTime - startTime - offset) / problem.getProperties().characteristicTime(); double tau = grid.getTimeStep(); final double dt = timeSegment / (numPoints - 1); - timeInterval = Math.max( (int) (dt / tau), 1); + timeInterval = Math.max((int) (dt / tau), 1); double wFactor = timeInterval * tau * problem.getProperties().characteristicTime(); @@ -297,16 +297,16 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { setTimeLimit(property); } } - + public abstract double signal(); - + public abstract void clearArrays(); public abstract void timeStep(int m) throws SolverException; public abstract void finaliseStep() throws SolverException; - /** + /** * Retrieves all problem statements that can be solved with this * implementation of the difference scheme. * @@ -322,5 +322,5 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { * @return an exact copy of this {@code DifferenceScheme}. */ public abstract DifferenceScheme copy(); - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/schemes/DistributedDetection.java b/src/main/java/pulse/problem/schemes/DistributedDetection.java index 88831e4..3d17946 100644 --- a/src/main/java/pulse/problem/schemes/DistributedDetection.java +++ b/src/main/java/pulse/problem/schemes/DistributedDetection.java @@ -1,5 +1,6 @@ package pulse.problem.schemes; +import java.io.Serializable; import java.util.stream.IntStream; import pulse.problem.statements.model.AbsorptionModel; @@ -11,7 +12,9 @@ * {@code AbsorptionModel}. * */ -public class DistributedDetection { +public class DistributedDetection implements Serializable { + + private static final long serialVersionUID = 3587781877001360511L; /** * Calculates the effective signal registered by the detector, which takes @@ -35,4 +38,4 @@ public static double evaluateSignal(final AbsorptionModel absorption, final Grid return signal * 0.5 * hx; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/ExplicitScheme.java b/src/main/java/pulse/problem/schemes/ExplicitScheme.java index 70a47ba..e4b858c 100644 --- a/src/main/java/pulse/problem/schemes/ExplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ExplicitScheme.java @@ -19,6 +19,11 @@ */ public abstract class ExplicitScheme extends OneDimensionalScheme { + /** + * + */ + private static final long serialVersionUID = -3025913683505686334L; + /** * Constructs a default explicit scheme using the default values of * {@code GRID_DENSITY} and {@code TAU_FACTOR}. diff --git a/src/main/java/pulse/problem/schemes/FixedPointIterations.java b/src/main/java/pulse/problem/schemes/FixedPointIterations.java index 1c359a4..dbf5992 100644 --- a/src/main/java/pulse/problem/schemes/FixedPointIterations.java +++ b/src/main/java/pulse/problem/schemes/FixedPointIterations.java @@ -1,6 +1,8 @@ package pulse.problem.schemes; import static java.lang.Math.abs; + +import java.io.Serializable; import java.util.Arrays; import pulse.problem.schemes.solvers.SolverException; import static pulse.problem.schemes.solvers.SolverException.SolverExceptionType.FINITE_DIFFERENCE_ERROR; @@ -10,7 +12,7 @@ * page * */ -public interface FixedPointIterations { +public interface FixedPointIterations extends Serializable { /** * Performs iterations until the convergence criterion is satisfied.The @@ -30,9 +32,9 @@ public default void doIterations(double[] V, final double error, final int m) th final int N = V.length - 1; - for (double V_0 = error + 1, V_N = error + 1; - abs(V[0] - V_0)/abs(V[0] + V_0 + 1e-16) > error - || abs(V[N] - V_N)/abs(V[N] + V_N + 1e-16) > error; finaliseIteration(V)) { + for (double V_0 = error + 1, V_N = error + 1; + abs(V[0] - V_0) / abs(V[0] + V_0 + 1e-16) > error + || abs(V[N] - V_N) / abs(V[N] + V_N + 1e-16) > error; finaliseIteration(V)) { V_N = V[N]; V_0 = V[0]; diff --git a/src/main/java/pulse/problem/schemes/Grid.java b/src/main/java/pulse/problem/schemes/Grid.java index 7bbfc9a..d8a0b33 100644 --- a/src/main/java/pulse/problem/schemes/Grid.java +++ b/src/main/java/pulse/problem/schemes/Grid.java @@ -2,7 +2,6 @@ import static java.lang.Math.pow; import static java.lang.Math.rint; -import static java.lang.String.format; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; @@ -10,8 +9,6 @@ import java.util.Set; -import pulse.problem.laser.DiscretePulse; -import pulse.problem.statements.Pulse; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.util.PropertyHolder; @@ -28,6 +25,7 @@ */ public class Grid extends PropertyHolder { + private static final long serialVersionUID = -4293010190416468656L; private double hx; private double tau; private double tauFactor; @@ -47,7 +45,7 @@ public Grid(NumericProperty gridDensity, NumericProperty timeFactor) { this.N = (int) gridDensity.getValue(); this.tauFactor = (double) timeFactor.getValue(); hx = 1. / N; - setTimeStep(tauFactor * pow(hx, 2)); + setTimeStep(tauFactor * pow(hx, 2)); } protected Grid() { @@ -121,7 +119,7 @@ public final double getTimeStep() { protected final void setTimeStep(double tau) { this.tau = tau; - + } /** @@ -166,7 +164,7 @@ public void setGridDensity(NumericProperty gridDensity) { requireType(gridDensity, GRID_DENSITY); this.N = (int) gridDensity.getValue(); hx = 1. / N; - setTimeStep(tauFactor * pow(hx, 2)); + setTimeStep(tauFactor * pow(hx, 2)); firePropertyChanged(this, gridDensity); } @@ -179,7 +177,7 @@ public void setGridDensity(NumericProperty gridDensity) { public void setTimeFactor(NumericProperty timeFactor) { requireType(timeFactor, TAU_FACTOR); this.tauFactor = (double) timeFactor.getValue(); - setTimeStep(tauFactor * pow(hx, 2)); + setTimeStep(tauFactor * pow(hx, 2)); firePropertyChanged(this, timeFactor); } @@ -193,7 +191,7 @@ public void setTimeFactor(NumericProperty timeFactor) { * @return a double representing the time on the finite grid */ public final double gridTime(double time, double dimensionFactor) { - return ( (int) (time / dimensionFactor / tau) ) * tau; + return ((int) (time / dimensionFactor / tau)) * tau; } /** @@ -213,7 +211,7 @@ public final double gridAxialDistance(double distance, double lengthFactor) { public String toString() { var sb = new StringBuilder("Grid"); sb.append(String.format("%n %-25s", this.getGridDensity())); - sb.append(String.format("%n %-25s", this.getTimeFactor())); + sb.append(String.format("%n %-25s", this.getTimeFactor())); return sb.toString(); } diff --git a/src/main/java/pulse/problem/schemes/Grid2D.java b/src/main/java/pulse/problem/schemes/Grid2D.java index e35a6e3..35dac53 100644 --- a/src/main/java/pulse/problem/schemes/Grid2D.java +++ b/src/main/java/pulse/problem/schemes/Grid2D.java @@ -2,7 +2,6 @@ import static java.lang.Math.pow; import static java.lang.Math.rint; -import static java.lang.String.format; import pulse.problem.laser.DiscretePulse; import pulse.problem.laser.DiscretePulse2D; @@ -19,6 +18,7 @@ */ public class Grid2D extends Grid { + private static final long serialVersionUID = 564113358979595637L; private double hy; protected Grid2D() { @@ -46,7 +46,7 @@ public Grid2D copy() { @Override public void setTimeFactor(NumericProperty timeFactor) { super.setTimeFactor(timeFactor); - setTimeStep((double) timeFactor.getValue() * (pow(getXStep(), 2) + pow(hy, 2)) ); + setTimeStep((double) timeFactor.getValue() * (pow(getXStep(), 2) + pow(hy, 2))); } /** @@ -56,18 +56,17 @@ public void setTimeFactor(NumericProperty timeFactor) { * * @param pulse the discrete puls representation */ - public void adjustStepSize(DiscretePulse pulse) { - var pulse2d = (DiscretePulse2D)pulse; + var pulse2d = (DiscretePulse2D) pulse; double pulseSpotSize = pulse2d.getDiscretePulseSpot(); - if(hy > pulseSpotSize) { + if (hy > pulseSpotSize) { final int INCREMENT = 5; final int newN = getGridDensityValue() + INCREMENT; setGridDensityValue(newN); adjustStepSize(pulse); } - + } @Override @@ -103,4 +102,4 @@ public double getYStep() { return hy; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/ImplicitScheme.java b/src/main/java/pulse/problem/schemes/ImplicitScheme.java index 217cb28..0d72003 100644 --- a/src/main/java/pulse/problem/schemes/ImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ImplicitScheme.java @@ -18,6 +18,10 @@ */ public abstract class ImplicitScheme extends OneDimensionalScheme { + /** + * + */ + private static final long serialVersionUID = 2785615380656900783L; private TridiagonalMatrixAlgorithm tridiagonal; /** @@ -67,14 +71,15 @@ protected void prepare(Problem problem) throws SolverException { } /** - * Calculates the solution at the boundaries using the boundary conditions - * specific to the problem statement and runs the tridiagonal matrix algorithm - * to evaluate solution at the intermediate grid points. + * Calculates the solution at the boundaries using the boundary conditions + * specific to the problem statement and runs the tridiagonal matrix + * algorithm to evaluate solution at the intermediate grid points. + * * @param m the time step - * @throws SolverException if the calculation failed - * @see leftBoundary(), evalRightBoundary(), pulse.problem.schemes.TridiagonalMatrixAlgorithm.sweep() + * @throws SolverException if the calculation failed + * @see leftBoundary(), evalRightBoundary(), + * pulse.problem.schemes.TridiagonalMatrixAlgorithm.sweep() */ - @Override public void timeStep(final int m) throws SolverException { leftBoundary(m); @@ -111,4 +116,4 @@ public void setTridiagonalMatrixAlgorithm(TridiagonalMatrixAlgorithm tridiagonal this.tridiagonal = tridiagonal; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/MixedScheme.java b/src/main/java/pulse/problem/schemes/MixedScheme.java index a02bfe3..fa53e69 100644 --- a/src/main/java/pulse/problem/schemes/MixedScheme.java +++ b/src/main/java/pulse/problem/schemes/MixedScheme.java @@ -17,6 +17,11 @@ */ public abstract class MixedScheme extends ImplicitScheme { + /** + * + */ + private static final long serialVersionUID = -770528855578192638L; + /** * Constructs a default semi-implicit scheme using the default values of * {@code GRID_DENSITY} and {@code TAU_FACTOR}. diff --git a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java index 5483b5a..b58bf0f 100644 --- a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java +++ b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java @@ -15,8 +15,7 @@ protected OneDimensionalScheme() { protected OneDimensionalScheme(NumericProperty timeLimit) { super(timeLimit); } - - + @Override public void clearArrays() { final int N = (int) getGrid().getGridDensity().getValue(); @@ -32,9 +31,9 @@ public double signal() { /** * Overwrites previously calculated temperature values with the calculations * made at the current time step + * * @throws SolverException if the calculation failed */ - @Override public void finaliseStep() throws SolverException { System.arraycopy(V, 0, U, 0, V.length); diff --git a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java index 189548a..e507e00 100644 --- a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java +++ b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java @@ -4,7 +4,6 @@ import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.schemes.rte.dom.DiscreteOrdinatesMethod; -import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.problem.statements.model.ThermoOpticalProperties; @@ -16,9 +15,11 @@ public class RadiativeTransferCoupling extends PropertyHolder { + private static final long serialVersionUID = -8969606772435213260L; private RadiativeTransferSolver rte; - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "RTE Solver Selector", RadiativeTransferSolver.class); + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor( + "RTE Solver Selector", RadiativeTransferSolver.class); public RadiativeTransferCoupling() { instanceDescriptor.setSelectedDescriptor(DiscreteOrdinatesMethod.class.getSimpleName()); @@ -34,7 +35,7 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { public void init(ParticipatingMedium problem, Grid grid) { if (rte == null) { - + if (!(problem.getProperties() instanceof ThermoOpticalProperties)) { throw new IllegalArgumentException("Illegal problem type: " + problem); } diff --git a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java index 9d98c30..0b76d96 100644 --- a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java @@ -1,13 +1,17 @@ package pulse.problem.schemes; +import java.io.Serializable; + /** * Implements the tridiagonal matrix algorithm (Thomas algorithms) for solving * systems of linear equations. Applicable to such systems where the forming - * matrix has a tridiagonal form: Ai*xi-1 - Bi xi + Ci xi+1 = -Fi. + * matrix has a tridiagonal form: Ai*xi-1 - Bi + * xi + Ci xi+1 = -Fi. * */ -public class TridiagonalMatrixAlgorithm { +public class TridiagonalMatrixAlgorithm implements Serializable { + private static final long serialVersionUID = 8201903787985856087L; private final double tau; private final double h; @@ -21,10 +25,10 @@ public class TridiagonalMatrixAlgorithm { public TridiagonalMatrixAlgorithm(Grid grid) { tau = grid.getTimeStep(); - N = grid.getGridDensityValue(); - h = grid.getXStep(); - alpha = new double[N + 2]; - beta = new double[N + 2]; + N = grid.getGridDensityValue(); + h = grid.getXStep(); + alpha = new double[N + 2]; + beta = new double[N + 2]; } /** @@ -59,6 +63,7 @@ public void evaluateBeta(final double[] U) { /** * Calculates the {@code beta} coefficients as part of the tridiagonal * matrix algorithm. + * * @param U * @param start * @param endExclusive @@ -116,17 +121,17 @@ protected double getCoefB() { protected double getCoefC() { return c; } - + public final double getTimeStep() { return tau; } - + public final int getGridPoints() { return N; } - + public final double getGridStep() { return h; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java b/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java index f395d00..5f65f01 100644 --- a/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java +++ b/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java @@ -1,11 +1,15 @@ package pulse.problem.schemes.rte; - +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; import static pulse.math.MathUtils.fastPowLoop; import pulse.problem.statements.NonlinearProblem; import pulse.problem.statements.Pulse2D; - +import pulse.util.FunctionSerializer; /** * Contains methods for calculating the integral spectral characteristics of a @@ -14,10 +18,11 @@ * {@code SplineInterpolator}. * */ -public class BlackbodySpectrum { +public class BlackbodySpectrum implements Serializable { - private UnivariateFunction interpolation; - private final double reductionFactor; + private static final long serialVersionUID = 4628793608666198231L; + private transient UnivariateFunction interpolation; + private double reductionFactor; /** * Creates a {@code BlackbodySpectrum}. Calculates the reduction factor @@ -36,16 +41,16 @@ public BlackbodySpectrum(NonlinearProblem p) { public String toString() { return "[" + getClass().getSimpleName() + ": Rel. heating = " + reductionFactor + "]"; } - + /** * Calculates the emissive power. This is equal to * 0.25 T0Tm [1 * +δTm /T0 θ (x) * ]4, where θ is the reduced temperature. + * * @param reducedTemperature the dimensionless reduced temperature * @return the amount of emissive power */ - public double emissivePower(double reducedTemperature) { return 0.25 / reductionFactor * fastPowLoop(1.0 + reducedTemperature * reductionFactor, 4); } @@ -63,7 +68,7 @@ public double radianceAt(double x) { } /** - * Calculates the emissive power at the given coordinate. + * Calculates the emissive power at the given coordinate. * * @param x the geometric coordinate inside the sample * @return the local emissive power value @@ -90,4 +95,22 @@ public final double radiance(double reducedTemperature) { return emissivePower(reducedTemperature) / Math.PI; } + /* + * Serialization + */ + private void writeObject(ObjectOutputStream oos) + throws IOException { + // default serialization + oos.defaultWriteObject(); + // write the object + FunctionSerializer.writeSplineFunction((PolynomialSplineFunction) interpolation, oos); + } + + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + this.interpolation = FunctionSerializer.readSplineFunction(ois); + } + } \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java b/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java index 8ad2b58..23a128f 100644 --- a/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java +++ b/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java @@ -1,11 +1,13 @@ package pulse.problem.schemes.rte; +import java.io.Serializable; + /** * This is basically a coupling interface between a {@code Solver} and a * {@code RadiativeTransferSolver}. * */ -public interface DerivativeCalculator { +public interface DerivativeCalculator extends Serializable { /** * Calculates the average value of the flux derivatives at the diff --git a/src/main/java/pulse/problem/schemes/rte/Fluxes.java b/src/main/java/pulse/problem/schemes/rte/Fluxes.java index 7c3bc78..be74c53 100644 --- a/src/main/java/pulse/problem/schemes/rte/Fluxes.java +++ b/src/main/java/pulse/problem/schemes/rte/Fluxes.java @@ -23,13 +23,13 @@ public Fluxes(NumericProperty gridDensity, NumericProperty opticalThickness) { public void store() { System.arraycopy(fluxes, 0, storedFluxes, 0, N + 1); // store previous results } - + /** - * Checks whether all stored values are finite. This is equivalent to summing - * all elements and checking whether the sum if finite. + * Checks whether all stored values are finite. This is equivalent to + * summing all elements and checking whether the sum if finite. + * * @return {@code true} if the elements are finite. */ - public RTECalculationStatus checkArrays() { double sum = Arrays.stream(fluxes).sum() + Arrays.stream(storedFluxes).sum(); return Double.isFinite(sum) ? NORMAL : INVALID_FLUXES; diff --git a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java index 2b4ac0f..f569717 100644 --- a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java @@ -1,19 +1,17 @@ package pulse.problem.schemes.rte; -import java.util.Arrays; -import static pulse.problem.schemes.rte.RTECalculationStatus.INVALID_FLUXES; -import static pulse.problem.schemes.rte.RTECalculationStatus.NORMAL; import pulse.properties.NumericProperty; public class FluxesAndExplicitDerivatives extends Fluxes { + private static final long serialVersionUID = -6308711091434946173L; private double fd[]; private double fdStored[]; public FluxesAndExplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { super(gridDensity, opticalThickness); } - + @Override public void init() { super.init(); diff --git a/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java b/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java index b2fd4e3..038bc2e 100644 --- a/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java @@ -4,6 +4,8 @@ public class FluxesAndImplicitDerivatives extends Fluxes { + private static final long serialVersionUID = -4161296401342482405L; + public FluxesAndImplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { super(gridDensity, opticalThickness); } diff --git a/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java b/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java index 71da5f9..f761f7c 100644 --- a/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java +++ b/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java @@ -1,11 +1,13 @@ package pulse.problem.schemes.rte; +import java.io.Serializable; + /** * Used to listed to status updates in {@code RadiativeTransferSolver} * subclasses. * */ -public interface RTECalculationListener { +public interface RTECalculationListener extends Serializable { /** * Invoked when a sub-step of the RTE solution has finished. diff --git a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java index f579004..83054ca 100644 --- a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java +++ b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java @@ -22,11 +22,9 @@ public enum RTECalculationStatus { * The grid density required to reach the error threshold was too large. */ GRID_TOO_LARGE, - /** * The radiative fluxes contain illegal values. */ - INVALID_FLUXES; - + } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java b/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java index 9c557e3..5d86e8f 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java @@ -1,5 +1,6 @@ package pulse.problem.schemes.rte.dom; +import java.io.Serializable; import pulse.math.linear.Matrices; import pulse.math.linear.SquareMatrix; import pulse.math.linear.Vector; @@ -10,8 +11,9 @@ * Variable names correspond to the standard notations. * */ -public class ButcherTableau implements Descriptive { +public class ButcherTableau implements Descriptive, Serializable { + private static final long serialVersionUID = -8856270519744473886L; private Vector b; private Vector bHat; private Vector c; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java b/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java index a242ef4..4e3de93 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java @@ -1,5 +1,6 @@ package pulse.problem.schemes.rte.dom; +import java.io.Serializable; import pulse.math.LegendrePoly; import pulse.math.MathUtils; @@ -10,7 +11,9 @@ * @author Teymur Aliev, Vadim Zborovskii, Artem Lunev * */ -public class CompositeGaussianQuadrature { +public class CompositeGaussianQuadrature implements Serializable { + + private static final long serialVersionUID = 780827333372523309L; private LegendrePoly poly; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java b/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java index f2a695e..5dfccb4 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java @@ -6,14 +6,16 @@ import pulse.problem.statements.model.ThermoOpticalProperties; /** - * The single-parameter Cornette-Schanks scattering phase function. - * It converges to the Rayleigh phase function as 〈μ〉 → 0 and approaches - * the Henyey–Greenstein phase function as |〈μ〉| → 1 + * The single-parameter Cornette-Schanks scattering phase function. It converges + * to the Rayleigh phase function as 〈μ〉 → 0 and approaches the + * Henyey–Greenstein phase function as |〈μ〉| → 1 + * * @see https://doi.org/10.1364/ao.31.003152 * */ public class CornetteSchanksPF extends PhaseFunction { + private static final long serialVersionUID = -4371291780762389604L; private double anisoFactor; private double onePlusGSq; private double g2; @@ -29,14 +31,14 @@ public void init(ThermoOpticalProperties top) { g2 = 2.0 * anisotropy; final double aSq = anisotropy * anisotropy; onePlusGSq = 1.0 + aSq; - anisoFactor = 1.5*(1.0 - aSq)/(2.0 + aSq); + anisoFactor = 1.5 * (1.0 - aSq) / (2.0 + aSq); } @Override public double function(final int i, final int k) { - double cosine = cosineTheta(i,k); + double cosine = cosineTheta(i, k); final double f = onePlusGSq - g2 * cosine; - return anisoFactor * (1.0 + cosine*cosine) / (f * sqrt(f)); + return anisoFactor * (1.0 + cosine * cosine) / (f * sqrt(f)); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java index af59449..0e3e5d4 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java @@ -23,6 +23,7 @@ */ public class DiscreteOrdinatesMethod extends RadiativeTransferSolver { + private static final long serialVersionUID = 2881363894773388976L; private InstanceDescriptor integratorDescriptor = new InstanceDescriptor( "Integrator selector", AdaptiveIntegrator.class); private InstanceDescriptor iterativeSolverSelector = new InstanceDescriptor( @@ -55,7 +56,6 @@ public DiscreteOrdinatesMethod(ParticipatingMedium problem, Grid grid) { setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class)); phaseFunctionSelector.setSelectedDescriptor(HenyeyGreensteinPF.class.getSimpleName()); - phaseFunctionSelector.addListener(() -> initPhaseFunction(properties, discrete)); initPhaseFunction(properties, discrete); init(problem, grid); @@ -66,7 +66,8 @@ public DiscreteOrdinatesMethod(ParticipatingMedium problem, Grid grid) { iterativeSolverSelector .addListener(() -> setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class))); - + + phaseFunctionSelector.addListener(() -> initPhaseFunction(properties, discrete)); } @Override @@ -78,7 +79,7 @@ public RTECalculationStatus compute(double[] tempArray) { if (status == RTECalculationStatus.NORMAL) { fluxesAndDerivatives(tempArray.length); } - + fireStatusUpdate(status); return status; } @@ -94,10 +95,10 @@ private void fluxesAndDerivatives(final int nExclusive) { for (int i = 0; i < nExclusive; i++) { double flux = DOUBLE_PI * discrete.firstMoment(interpolation[0], i); fluxes.setFlux(i, flux); - fluxes.setFluxDerivative(i, + fluxes.setFluxDerivative(i, -DOUBLE_PI * discrete.firstMoment(interpolation[1], i)); } - + } @Override @@ -108,8 +109,8 @@ public String getDescriptor() { @Override public void init(ParticipatingMedium problem, Grid grid) { super.init(problem, grid); - var top = (ThermoOpticalProperties)problem.getProperties(); - initPhaseFunction(top, + var top = (ThermoOpticalProperties) problem.getProperties(); + initPhaseFunction(top, integrator.getDiscretisation()); integrator.init(problem); integrator.getPhaseFunction().init(top); diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java index 846dc1b..e81389a 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java @@ -1,12 +1,15 @@ package pulse.problem.schemes.rte.dom; +import java.io.Serializable; + /** * Defines the main quantities calculated within the discrete ordinates method. * This includes the various intensity and flux arrays used internally by the * integrators. */ -public class DiscreteQuantities { +public class DiscreteQuantities implements Serializable { + private static final long serialVersionUID = -3997479317699236996L; private double[][] I; private double[][] Ik; private double[][] f; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java index 932d7d6..91be63d 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java @@ -23,6 +23,7 @@ */ public class Discretisation extends PropertyHolder { + private static final long serialVersionUID = 7069987686586911101L; private DiscreteQuantities quantities; private StretchedGrid grid; private OrdinateSet ordinates; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java b/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java index 648096e..42b98b9 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java @@ -16,6 +16,7 @@ */ public class ExplicitRungeKutta extends AdaptiveIntegrator { + private static final long serialVersionUID = -2478658861611086402L; private ButcherTableau tableau; private DiscreteSelector tableauSelector; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java b/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java index 08cea6d..2a9ea83 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java @@ -6,6 +6,8 @@ public class FixedIterations extends IterativeSolver { + private static final long serialVersionUID = -7308041206602757928L; + @Override public RTECalculationStatus doIterations(AdaptiveIntegrator integrator) { diff --git a/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java b/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java index f1e0d61..12af970 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java @@ -2,7 +2,6 @@ import static java.lang.Math.sqrt; -import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.model.ThermoOpticalProperties; /** @@ -11,6 +10,7 @@ */ public class HenyeyGreensteinPF extends PhaseFunction { + private static final long serialVersionUID = -2196189314681832809L; private double a1; private double a2; private double b1; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java b/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java index 1157850..93c4131 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java @@ -1,5 +1,7 @@ package pulse.problem.schemes.rte.dom; +import java.io.Serializable; + /** * A globally C1 Hermite interpolator used to interpolate intensities * and derivatives in discrete ordinates method when solving the radiative @@ -8,8 +10,9 @@ * @author Vadim Zborovskii, Artem Lunev * */ -public class HermiteInterpolator { +public class HermiteInterpolator implements Serializable { + private static final long serialVersionUID = -1973954803574711053L; protected double y1; protected double y0; protected double d1; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java b/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java index 7c382c6..4693eeb 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java @@ -9,6 +9,7 @@ */ public class LinearAnisotropicPF extends PhaseFunction { + private static final long serialVersionUID = 7074989018933263351L; private double g; public LinearAnisotropicPF(ThermoOpticalProperties top, Discretisation intensities) { @@ -29,7 +30,7 @@ public double partialSum(final int i, final int j, final int n1, final int n2Exc @Override public double function(final int i, final int k) { - return 1.0 + g * cosineTheta(i,k); + return 1.0 + g * cosineTheta(i, k); } } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java b/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java index 1785a2a..3dc388f 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java @@ -21,9 +21,9 @@ public ODEIntegrator(Discretisation intensities) { protected void init(NonlinearProblem problem) { extract((ThermoOpticalProperties) problem.getProperties()); - setEmissionFunction( new BlackbodySpectrum(problem) ); + setEmissionFunction(new BlackbodySpectrum(problem)); } - + protected void extract(ThermoOpticalProperties properties) { discretisation.setEmissivity((double) properties.getEmissivity().getValue()); discretisation.setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); diff --git a/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java b/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java index 68662f6..fc09890 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/OrdinateSet.java @@ -1,5 +1,6 @@ package pulse.problem.schemes.rte.dom; +import java.io.Serializable; import static pulse.math.MathUtils.approximatelyEquals; import java.util.Arrays; @@ -11,8 +12,9 @@ * discretisation of a radiative transfer equation. * */ -public class OrdinateSet implements Descriptive { +public class OrdinateSet implements Descriptive, Serializable { + private static final long serialVersionUID = 4850346144315192409L; private double[] mu; private double[] w; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java index e8123f8..21374fb 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java @@ -1,10 +1,10 @@ package pulse.problem.schemes.rte.dom; -import pulse.problem.statements.ParticipatingMedium; +import java.io.Serializable; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.util.Reflexive; -public abstract class PhaseFunction implements Reflexive { +public abstract class PhaseFunction implements Reflexive, Serializable { private final Discretisation intensities; private double anisotropy; @@ -14,15 +14,15 @@ public PhaseFunction(ThermoOpticalProperties top, Discretisation intensities) { this.intensities = intensities; init(top); } - - /** - * Calculates the cosine of the scattering angle as the product - * of the two discrete cosine nodes. + + /** + * Calculates the cosine of the scattering angle as the product of the two + * discrete cosine nodes. + * * @param i * @param k - * @return + * @return */ - public final double cosineTheta(int i, int k) { final var ordinates = getDiscreteIntensities().getOrdinates(); return ordinates.getNode(k) * ordinates.getNode(i); diff --git a/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java b/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java index 335d1ca..012eb5d 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java @@ -5,18 +5,16 @@ import static pulse.properties.NumericPropertyKeyword.DOM_GRID_DENSITY; import static pulse.properties.NumericPropertyKeyword.GRID_STRETCHING_FACTOR; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; import pulse.util.PropertyHolder; public class StretchedGrid extends PropertyHolder { + private static final long serialVersionUID = -7987714138817824037L; + private double[] nodes; private double stretchingFactor; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java b/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java index a051ce9..1c44c06 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java @@ -5,17 +5,15 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.RELAXATION_PARAMETER; -import java.util.List; import java.util.Set; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; public class SuccessiveOverrelaxation extends IterativeSolver { + private static final long serialVersionUID = 1135563981945852881L; private double W; public SuccessiveOverrelaxation() { diff --git a/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java b/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java index fa1bc4f..5ce2240 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java @@ -14,6 +14,7 @@ */ public class TRBDF2 extends AdaptiveIntegrator { + private static final long serialVersionUID = 5488454845395333565L; /* * Coefficients of the Butcher tableau as originally defined in M.E. Hosea, L.E * Shampine/Applied Numerical Mathematics 20 (1996) 21-37 diff --git a/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java b/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java index 208ab29..535a603 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/ChandrasekharsQuadrature.java @@ -11,9 +11,7 @@ import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.QUADRATURE_POINTS; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Set; import java.util.stream.IntStream; @@ -25,8 +23,6 @@ import pulse.math.linear.Vector; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; /** * This quadrature methods of evaluating the composition product of the @@ -39,10 +35,11 @@ */ public class ChandrasekharsQuadrature extends CompositionProduct { + private static final long serialVersionUID = 3282258803373408111L; private int m; private double expLower; private double expUpper; - private LaguerreSolver solver; + private transient LaguerreSolver solver; private double[] moments; /** @@ -127,8 +124,8 @@ private SquareMatrix xMatrix(final double[] roots) { /** * Calculates \int_{r_{min}}^{r_{max}}{x^{l+1}exp(-x)dx}. * - * @param l an integer such that 0 <= l <= 2*m - 1. - * @return the value of this definite integral. + * @param l an integer such that 0 <= l <= 2*m - 1. @re + * turn the value of this definite integral. */ private static double auxilliaryIntegral(final double x, final int lPlusN, final double exp) { @@ -261,6 +258,9 @@ private double[] roots() { break; default: // use LaguerreSolver + if (solver == null) { + solver = new LaguerreSolver(); + } roots = Arrays.stream(solver.solveAllComplex(c, 1.0)).mapToDouble(complex -> complex.getReal()).toArray(); } diff --git a/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java b/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java index 340b5f2..b3833ac 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java @@ -16,6 +16,7 @@ */ class ExponentialIntegral extends MidpointIntegrator { + private static final long serialVersionUID = -5818633555456309668L; private double t; private int order; diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java b/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java index f2f2328..546bb2c 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java @@ -2,13 +2,11 @@ import static java.lang.Math.max; import static java.lang.Math.min; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.INTEGRATION_CUTOFF; import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; -import java.util.List; import java.util.Set; import pulse.math.FixedIntervalIntegrator; @@ -16,8 +14,6 @@ import pulse.math.Segment; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; /** * A class for evaluating the composition product using a simple Newton-Cotes @@ -26,6 +22,7 @@ */ public class NewtonCotesQuadrature extends CompositionProduct { + private static final long serialVersionUID = -177127670003926420L; private final static int DEFAULT_SEGMENTS = 64; private final static double DEFAULT_CUTOFF = 16.0; private FixedIntervalIntegrator integrator; diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java index bbcf0d3..dbc23a8 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java @@ -20,6 +20,7 @@ */ public class NonscatteringAnalyticalDerivatives extends NonscatteringRadiativeTransfer { + private static final long serialVersionUID = -7549047672012708753L; private static FunctionWithInterpolation ei2 = ExponentialIntegrals.get(2); public NonscatteringAnalyticalDerivatives(ParticipatingMedium problem, Grid grid) { @@ -31,8 +32,9 @@ public NonscatteringAnalyticalDerivatives(ParticipatingMedium problem, Grid grid /** * Evaluates fluxes and their derivatives using analytical formulae and the * selected numerical quadrature.Usually works best with the - {@code ChandrasekharsQuadrature} - * @return + * {@code ChandrasekharsQuadrature} + * + * @return */ @Override public RTECalculationStatus compute(double U[]) { diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java index b620d91..c781488 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java @@ -14,6 +14,8 @@ */ public class NonscatteringDiscreteDerivatives extends NonscatteringRadiativeTransfer { + private static final long serialVersionUID = -6919734351838124553L; + public NonscatteringDiscreteDerivatives(ParticipatingMedium problem, Grid grid) { super(problem, grid); var properties = (ThermoOpticalProperties) problem.getProperties(); diff --git a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java index a4583b5..3afd892 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java @@ -17,6 +17,8 @@ public abstract class NonscatteringRadiativeTransfer extends RadiativeTransferSolver { + private static final long serialVersionUID = 4934841542530728191L; + private static final FunctionWithInterpolation ei3 = ExponentialIntegrals.get(3); private double emissivity; @@ -27,9 +29,9 @@ public abstract class NonscatteringRadiativeTransfer extends RadiativeTransferSo private double radiosityFront; private double radiosityRear; - private InstanceDescriptor instanceDescriptor + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Quadrature Selector", CompositionProduct.class); + "Quadrature Selector", CompositionProduct.class); protected NonscatteringRadiativeTransfer(ParticipatingMedium problem, Grid grid) { super(); @@ -50,6 +52,7 @@ public void init(ParticipatingMedium p, Grid grid) { * The superclass method will update the interpolation that the blackbody * spectrum uses to evaluate the temperature profile and calculate the * radiosities.A {@code NORMAL}status is always returned. + * * @param array */ @Override @@ -182,7 +185,7 @@ public double getRadiosityRear() { */ private void radiosities() { final double doubleReflectivity = 2.0 * (1.0 - emissivity); - + final double b = b(doubleReflectivity); final double sq = 1.0 - b * b; final double a1 = a1(doubleReflectivity); diff --git a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java index d2472df..27196d5 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java @@ -17,6 +17,8 @@ */ public class ADILinearisedSolver extends ADIScheme implements Solver { + private static final long serialVersionUID = 5354981341912770336L; + private TridiagonalMatrixAlgorithm tridiagonal; private int N; @@ -77,12 +79,12 @@ public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor) { public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { super(N, timeFactor, timeLimit); } - + @Override public void clearArrays() { - N = (int) getGrid().getGridDensity().getValue(); - U1 = new double[N + 1][N + 1]; - U2 = new double[N + 1][N + 1]; + N = (int) getGrid().getGridDensity().getValue(); + U1 = new double[N + 1][N + 1]; + U2 = new double[N + 1][N + 1]; U1_E = new double[N + 3][N + 3]; U2_E = new double[N + 3][N + 3]; @@ -117,7 +119,6 @@ public void prepare(Problem problem) throws SolverException { l = (double) properties.getSampleThickness().getValue(); // end - // a[i]*u[i-1] - b[i]*u[i] + c[i]*u[i+1] = F[i] lastIndex = (int) (fovOuter / d / hx); lastIndex = lastIndex > N ? N : lastIndex; diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index 39e504c..5b706a4 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -32,7 +32,7 @@ public abstract class ExplicitCoupledSolver extends ExplicitScheme private double zeta; private boolean autoUpdateFluxes = true; //should be false for nonlinear solvers - + public ExplicitCoupledSolver() { this(derive(GRID_DENSITY, 80), derive(TAU_FACTOR, 0.5)); } @@ -49,7 +49,7 @@ public void prepare(Problem problem) throws SolverException { var grid = getGrid(); - coupling.init((ParticipatingMedium)problem, grid); + coupling.init((ParticipatingMedium) problem, grid); fluxes = coupling.getRadiativeTransferEquation().getFluxes(); setCalculationStatus(fluxes.checkArrays()); @@ -69,7 +69,7 @@ public void prepare(Problem problem) throws SolverException { HX_NP = hx / Np; prefactor = tau * opticalThickness / Np; - zeta = (double) ((ParticipatingMedium)problem).getGeometricFactor().getValue(); + zeta = (double) ((ParticipatingMedium) problem).getGeometricFactor().getValue(); } @Override @@ -139,7 +139,7 @@ public final void setCoupling(RadiativeTransferCoupling coupling) { public String toString() { return getString("ExplicitScheme.4"); } - + @Override public Class[] domain() { return new Class[]{ParticipatingMedium.class}; @@ -148,17 +148,17 @@ public Class[] domain() { public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.status = calculationStatus; if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString(), + throw new SolverException(status.toString(), RTE_SOLVER_ERROR); } } - + public final boolean isAutoUpdateFluxes() { return this.autoUpdateFluxes; } - + public final void setAutoUpdateFluxes(boolean auto) { this.autoUpdateFluxes = auto; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java index ab30eeb..6d40c3b 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java @@ -28,24 +28,24 @@ * * @author Artem Lunev */ -public class ExplicitCoupledSolverNL extends ExplicitCoupledSolver - implements FixedPointIterations -{ +public class ExplicitCoupledSolverNL extends ExplicitCoupledSolver + implements FixedPointIterations { + private static final long serialVersionUID = 4214528397984492532L; private double nonlinearPrecision; - + public ExplicitCoupledSolverNL() { super(); nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); setAutoUpdateFluxes(false); } - + public ExplicitCoupledSolverNL(NumericProperty N, NumericProperty timeFactor) { super(N, timeFactor); nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); setAutoUpdateFluxes(false); } - + @Override public void timeStep(int m) throws SolverException { doIterations(getCurrentSolution(), nonlinearPrecision, m); @@ -61,13 +61,13 @@ public void finaliseIteration(double[] V) throws SolverException { FixedPointIterations.super.finaliseIteration(V); setCalculationStatus(this.getCoupling().getRadiativeTransferEquation().compute(V)); } - + @Override public DifferenceScheme copy() { var grid = getGrid(); return new ExplicitCoupledSolverNL(grid.getGridDensity(), grid.getTimeFactor()); } - + public final NumericProperty getNonlinearPrecision() { return derive(NONLINEAR_PRECISION, nonlinearPrecision); } @@ -84,10 +84,10 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { super.set(type, property); } } - + @Override public String toString() { return Messages.getString("ExplicitScheme.5"); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java index 7b7f0ae..cd667be 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java @@ -46,6 +46,7 @@ */ public class ExplicitLinearisedSolver extends ExplicitScheme implements Solver { + private static final long serialVersionUID = 3084350485569519036L; private int N; private double hx; private double a; @@ -66,8 +67,8 @@ public ExplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, N @Override public void prepare(Problem problem) throws SolverException { super.prepare(problem); - - zeta = (double) ( (ClassicalProblem) problem).getGeometricFactor().getValue(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); N = (int) getGrid().getGridDensity().getValue(); hx = getGrid().getXStep(); diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java index ed8e7fa..546abc0 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java @@ -19,6 +19,7 @@ public class ExplicitNonlinearSolver extends ExplicitScheme implements Solver, FixedPointIterations { + private static final long serialVersionUID = -5392648684733420360L; private int N; private double hx; diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java index 56e747c..ac48b36 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java @@ -13,6 +13,7 @@ public class ExplicitTranslucentSolver extends ExplicitScheme implements Solver { + private static final long serialVersionUID = -3693226611473383024L; private int N; private double hx; private double tau; @@ -33,7 +34,7 @@ public void prepare(Problem problem) throws SolverException { super.prepare(problem); var grid = getGrid(); - model = ((PenetrationProblem)problem).getAbsorptionModel(); + model = ((PenetrationProblem) problem).getAbsorptionModel(); N = (int) grid.getGridDensity().getValue(); hx = grid.getXStep(); diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java index e9e4f60..a42bac2 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java @@ -17,7 +17,7 @@ import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; -public abstract class ImplicitCoupledSolver extends CoupledImplicitScheme +public abstract class ImplicitCoupledSolver extends CoupledImplicitScheme implements Solver { private RadiativeTransferSolver rte; @@ -50,7 +50,7 @@ public void prepare(Problem problem) throws SolverException { final var grid = getGrid(); var coupling = getCoupling(); - coupling.init( (ParticipatingMedium) problem, grid); + coupling.init((ParticipatingMedium) problem, grid); rte = coupling.getRadiativeTransferEquation(); N = (int) getGrid().getGridDensity().getValue(); @@ -72,7 +72,7 @@ public void prepare(Problem problem) throws SolverException { v1 = 1.0 + HX2_2TAU + hx * Bi1; fluxes = rte.getFluxes(); - + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @Override @@ -91,8 +91,8 @@ public double phi(int i) { tridiagonal.evaluateAlpha(); setTridiagonalMatrixAlgorithm(tridiagonal); - - zeta = (double) ((ClassicalProblem)problem).getGeometricFactor().getValue(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); } @Override @@ -104,7 +104,7 @@ public void solve(ParticipatingMedium problem) throws SolverException { var status = getCalculationStatus(); if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString(), + throw new SolverException(status.toString(), RTE_SOLVER_ERROR); } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java index 389d148..af0d6e1 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java @@ -30,6 +30,7 @@ */ public class ImplicitCoupledSolverNL extends ImplicitCoupledSolver implements FixedPointIterations { + private static final long serialVersionUID = -3993380888844448942L; private double nonlinearPrecision; public ImplicitCoupledSolverNL() { @@ -43,7 +44,7 @@ public ImplicitCoupledSolverNL(NumericProperty N, NumericProperty timeFactor, Nu nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); setAutoUpdateFluxes(false); } - + @Override public void timeStep(final int m) throws SolverException { doIterations(getCurrentSolution(), nonlinearPrecision, m); @@ -77,16 +78,16 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { super.set(type, property); } } - + @Override public DifferenceScheme copy() { var grid = getGrid(); return new ImplicitCoupledSolverNL(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); } - + @Override public String toString() { return Messages.getString("ImplicitScheme.5"); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java index 5343958..587271b 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java @@ -18,7 +18,7 @@ public class ImplicitDiathermicSolver extends ImplicitScheme implements Solver[] domain() { return new Class[]{DiathermicMedium.class}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java index 16a80d1..62b5709 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java @@ -45,16 +45,18 @@ * @param a subclass of ClassicalProblem * @see super.solve(Problem) */ -public class ImplicitLinearisedSolver extends ImplicitScheme +public class ImplicitLinearisedSolver extends ImplicitScheme implements Solver { + private static final long serialVersionUID = -5182202341972279175L; + private int N; - + protected double Bi1HTAU; protected double tau; protected double HH; protected double _2HTAU; - + private double zeta; public ImplicitLinearisedSolver() { @@ -78,8 +80,8 @@ public void prepare(Problem problem) throws SolverException { N = (int) grid.getGridDensity().getValue(); final double hx = grid.getXStep(); tau = grid.getTimeStep(); - - zeta = (double) ((ClassicalProblem)problem).getGeometricFactor().getValue(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); @@ -114,7 +116,7 @@ public double firstBeta() { @Override public double evalRightBoundary(final double alphaN, final double betaN) { - return (HH * getPreviousSolution()[N] + 2. * tau * betaN + return (HH * getPreviousSolution()[N] + 2. * tau * betaN + _2HTAU * (1.0 - zeta) * getCurrentPulseValue() //additional term due to stray light ) / (2 * Bi1HTAU + HH - 2. * tau * (alphaN - 1)); } @@ -130,4 +132,4 @@ public Class[] domain() { return new Class[]{ClassicalProblem.class}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java index 0cc048a..20224b8 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java @@ -17,6 +17,7 @@ public class ImplicitNonlinearSolver extends ImplicitScheme implements Solver, FixedPointIterations { + private static final long serialVersionUID = -6263519219698662707L; private int N; private double HH; private double tau; @@ -139,7 +140,7 @@ public void iteration(int m) throws SolverException { @Override public double evalRightBoundary(double alphaN, double betaN) { - return c2 * (2. * betaN * tau + HH * getPreviousSolution()[N] + return c2 * (2. * betaN * tau + HH * getPreviousSolution()[N] + c1 * (fastPowLoop(getCurrentSolution()[N] * dT_T + 1, 4) - 1)); } diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java index d5005ba..7ace1f8 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java @@ -14,6 +14,7 @@ public class ImplicitTranslucentSolver extends ImplicitScheme implements Solver { + private static final long serialVersionUID = -2207434474904484692L; private AbsorptionModel absorption; private int N; @@ -40,12 +41,12 @@ public void prepare(Problem problem) throws SolverException { final double Bi1H = (double) problem.getProperties().getHeatLoss().getValue() * grid.getXStep(); final double hx = grid.getXStep(); - absorption = ((PenetrationProblem)problem).getAbsorptionModel(); - + absorption = ((PenetrationProblem) problem).getAbsorptionModel(); + HH = hx * hx; _2Bi1HTAU = 2.0 * Bi1H * tau; b11 = 1.0 / (1.0 + 2.0 * tau / HH * (1 + Bi1H)); - + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @Override @@ -54,7 +55,7 @@ public double phi(final int i) { } }; - + // coefficients for difference equation tridiagonal.setCoefA(1. / HH); tridiagonal.setCoefB(1. / tau + 2. / HH); @@ -81,7 +82,7 @@ public double evalRightBoundary(final double alphaN, final double betaN) { final double tau = getGrid().getTimeStep(); var tridiagonal = this.getTridiagonalMatrixAlgorithm(); - return (HH * getPreviousSolution()[N] + HH*tau*tridiagonal.phi(N) + return (HH * getPreviousSolution()[N] + HH * tau * tridiagonal.phi(N) + 2. * tau * betaN) / (_2Bi1HTAU + HH + 2. * tau * (1 - alphaN)); } @@ -89,7 +90,7 @@ public double evalRightBoundary(final double alphaN, final double betaN) { public double firstBeta() { var tridiagonal = this.getTridiagonalMatrixAlgorithm(); double tau = getGrid().getTimeStep(); - return (getPreviousSolution()[0] + tau*tridiagonal.phi(0))* b11; + return (getPreviousSolution()[0] + tau * tridiagonal.phi(0)) * b11; } @Override @@ -113,4 +114,4 @@ public Class[] domain() { return new Class[]{PenetrationProblem.class}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java index 1a09949..b31f7fd 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java @@ -22,6 +22,7 @@ public class ImplicitTwoTemperatureSolver extends ImplicitScheme implements Solver, FixedPointIterations { + private static final long serialVersionUID = 7955478815933535623L; private AbsorptionModel absorption; private TridiagonalMatrixAlgorithm gasSolver; diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java index fc3e516..4ba9ea2 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java @@ -19,7 +19,7 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -public abstract class MixedCoupledSolver extends CoupledImplicitScheme +public abstract class MixedCoupledSolver extends CoupledImplicitScheme implements Solver { private RadiativeTransferSolver rte; @@ -74,13 +74,13 @@ public void prepare(Problem problem) throws SolverException { hx = grid.getXStep(); tau = grid.getTimeStep(); - var properties = (ThermoOpticalProperties)problem.getProperties(); + var properties = (ThermoOpticalProperties) problem.getProperties(); //combined biot - Bi1 = (double) properties.getHeatLoss().getValue() + - (double) properties.getConvectiveLosses().getValue(); + Bi1 = (double) properties.getHeatLoss().getValue() + + (double) properties.getConvectiveLosses().getValue(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); - zeta = (double) ( (ClassicalProblem)problem ).getGeometricFactor().getValue(); - var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @Override @@ -95,7 +95,7 @@ public double beta(final double f, final double phi, final int i) { var U = getPreviousSolution(); return super.beta(f + ONE_MINUS_SIGMA * (U[i] - 2.0 * U[i - 1] + U[i - 2]) / HX2, TAU0_NP * phi, i); } - + @Override public void evaluateBeta(final double[] U) { var fluxes = rte.getFluxes(); @@ -168,10 +168,10 @@ public double firstBeta() { var U = getPreviousSolution(); final double phi = TAU0_NP * fluxes.fluxDerivativeFront(); return (_2TAUHX - * (getCurrentPulseValue() * zeta - SIGMA_NP * fluxes.getFlux(0) + * (getCurrentPulseValue() * zeta - SIGMA_NP * fluxes.getFlux(0) - ONE_MINUS_SIGMA_NP * fluxes.getStoredFlux(0)) - + HX2 * (U[0] + phi * tau) + _2TAU_ONE_MINUS_SIGMA * - (U[1] - U[0] * ONE_PLUS_Bi1_HX)) * BETA1_FACTOR; + + HX2 * (U[0] + phi * tau) + _2TAU_ONE_MINUS_SIGMA + * (U[1] - U[0] * ONE_PLUS_Bi1_HX)) * BETA1_FACTOR; } @Override @@ -215,4 +215,4 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java index 77d7b19..94ad40d 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java @@ -30,6 +30,7 @@ */ public class MixedCoupledSolverNL extends MixedCoupledSolver implements FixedPointIterations { + private static final long serialVersionUID = -8344384560376683594L; private double nonlinearPrecision; public MixedCoupledSolverNL() { @@ -43,7 +44,7 @@ public MixedCoupledSolverNL(NumericProperty N, NumericProperty timeFactor, Numer nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); setAutoUpdateFluxes(false); } - + @Override public void timeStep(final int m) throws SolverException { doIterations(getCurrentSolution(), nonlinearPrecision, m); @@ -77,16 +78,16 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { super.set(type, property); } } - + @Override public DifferenceScheme copy() { var grid = getGrid(); return new MixedCoupledSolverNL(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); } - + @Override public String toString() { return Messages.getString("MixedScheme2.5"); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java index 56a74f0..e2fc25c 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java @@ -50,12 +50,13 @@ */ public class MixedLinearisedSolver extends MixedScheme implements Solver { + private static final long serialVersionUID = 2233988060956648641L; private double b1; private double b2; private double b3; private double c1; private double c2; - + private double zeta; private final static double EPS = 1e-7; // a small value ensuring numeric stability @@ -93,8 +94,8 @@ public void prepare(Problem problem) throws SolverException { b3 = hx * tau; c1 = b2; c2 = Bi1HTAU + HH; - - zeta = (double) ((ClassicalProblem)problem).getGeometricFactor().getValue(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @@ -128,15 +129,15 @@ public double evalRightBoundary(final double alphaN, final double betaN) { final double tau = grid.getTimeStep(); final int N = (int) grid.getGridDensity().getValue(); - return ( c1 * U[N] + tau * betaN + b3 * (1.0 - zeta) * getCurrentPulseValue() - - tau * (U[N] - U[N - 1]) ) / (c2 - tau * (alphaN - 1)); + return (c1 * U[N] + tau * betaN + b3 * (1.0 - zeta) * getCurrentPulseValue() + - tau * (U[N] - U[N - 1])) / (c2 - tau * (alphaN - 1)); } - + @Override public double pulse(int m) { final double tau = getGrid().getTimeStep(); var pulse = getDiscretePulse(); - return pulse.laserPowerAt((m - 1 + EPS) * tau) + pulse.laserPowerAt((m - EPS) * tau); + return pulse.laserPowerAt((m - 1 + EPS) * tau) + pulse.laserPowerAt((m - EPS) * tau); } @Override diff --git a/src/main/java/pulse/problem/schemes/solvers/Solver.java b/src/main/java/pulse/problem/schemes/solvers/Solver.java index f371b18..654f897 100644 --- a/src/main/java/pulse/problem/schemes/solvers/Solver.java +++ b/src/main/java/pulse/problem/schemes/solvers/Solver.java @@ -1,5 +1,6 @@ package pulse.problem.schemes.solvers; +import java.io.Serializable; import pulse.problem.statements.Problem; /** @@ -9,7 +10,7 @@ * * @param an instance of Problem */ -public interface Solver { +public interface Solver extends Serializable { /** * Calculates the solution of the {@code t} and stores it in the respective diff --git a/src/main/java/pulse/problem/schemes/solvers/SolverException.java b/src/main/java/pulse/problem/schemes/solvers/SolverException.java index 8aacbe0..ef97e32 100644 --- a/src/main/java/pulse/problem/schemes/solvers/SolverException.java +++ b/src/main/java/pulse/problem/schemes/solvers/SolverException.java @@ -2,22 +2,22 @@ @SuppressWarnings("serial") public class SolverException extends Exception { - + private final SolverExceptionType type; public SolverException(String status, SolverExceptionType type) { super(status); this.type = type; } - + public SolverException(SolverExceptionType type) { this(type.toString(), type); } - + public SolverExceptionType getType() { return type; } - + public enum SolverExceptionType { RTE_SOLVER_ERROR, OPTIMISATION_ERROR, @@ -26,4 +26,4 @@ public enum SolverExceptionType { ILLEGAL_PARAMETERS, } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/AdiabaticSolution.java b/src/main/java/pulse/problem/statements/AdiabaticSolution.java index 5fbb5a8..8429374 100644 --- a/src/main/java/pulse/problem/statements/AdiabaticSolution.java +++ b/src/main/java/pulse/problem/statements/AdiabaticSolution.java @@ -1,5 +1,6 @@ package pulse.problem.statements; +import java.io.Serializable; import static java.lang.Math.PI; import static java.lang.Math.exp; import static java.lang.Math.pow; @@ -9,8 +10,9 @@ import pulse.HeatingCurve; import pulse.problem.statements.model.ThermalProperties; -public class AdiabaticSolution { +public class AdiabaticSolution implements Serializable { + private static final long serialVersionUID = 4240406501288696621L; public final static int DEFAULT_CLASSIC_PRECISION = 200; public final static int DEFAULT_POINTS = 100; diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem.java b/src/main/java/pulse/problem/statements/ClassicalProblem.java index 7243985..2f5fd36 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem.java @@ -24,6 +24,10 @@ */ public class ClassicalProblem extends Problem { + /** + * + */ + private static final long serialVersionUID = -1915004757733565502L; private double bias; public ClassicalProblem() { diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java index 9960a8b..3ea99b1 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -30,6 +30,11 @@ */ public class ClassicalProblem2D extends ClassicalProblem { + /** + * + */ + private static final long serialVersionUID = 8974995052071820422L; + public ClassicalProblem2D() { super(); setPulse(new Pulse2D()); @@ -66,7 +71,7 @@ public String toString() { public DiscretePulse discretePulseOn(Grid grid) { return grid instanceof Grid2D ? new DiscretePulse2D(this, (Grid2D) grid) : super.discretePulseOn(grid); } - + @Override public void optimisationVector(ParameterVector output) { super.optimisationVector(output); @@ -78,7 +83,7 @@ public void optimisationVector(ParameterVector output) { var key = p.getIdentifier().getKeyword(); Transformable transform = new InvDiamTransform(properties); var bounds = Segment.boundsFrom(key); - + switch (key) { case FOV_OUTER: value = (double) properties.getFOVOuter().getValue(); @@ -127,7 +132,7 @@ public void assign(ParameterVector params) throws SolverException { properties.set(type, derive(type, p.inverseTransform())); break; case SPOT_DIAMETER: - ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, + ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, p.inverseTransform())); break; default: @@ -145,4 +150,4 @@ public Problem copy() { return new ClassicalProblem2D(this); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index dc4bf9b..008a54f 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -33,6 +33,8 @@ */ public class DiathermicMedium extends ClassicalProblem { + private static final long serialVersionUID = -98674255799114512L; + public DiathermicMedium() { super(); } @@ -69,18 +71,18 @@ public void optimisationVector(ParameterVector output) { break; case HEAT_LOSS_CONVECTIVE: bounds = Segment.boundsFrom(HEAT_LOSS_CONVECTIVE); - value = (double) properties.getConvectiveLosses().getValue(); - break; + value = (double) properties.getConvectiveLosses().getValue(); + break; case HEAT_LOSS: - if(properties.areThermalPropertiesLoaded()) { - value = (double) properties.getHeatLoss().getValue(); - bounds = new Segment(0.0, properties.maxRadiationBiot() ); - break; - } + if (properties.areThermalPropertiesLoaded()) { + value = (double) properties.getHeatLoss().getValue(); + bounds = new Segment(0.0, properties.maxRadiationBiot()); + break; + } default: continue; } - + p.setTransform(new StickTransform(bounds)); p.setValue(value); p.setBounds(bounds); @@ -101,11 +103,11 @@ public void assign(ParameterVector params) throws SolverException { switch (key) { case DIATHERMIC_COEFFICIENT: - properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, + properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, p.inverseTransform())); break; case HEAT_LOSS_CONVECTIVE: - properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, + properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, p.inverseTransform())); break; default: @@ -130,4 +132,4 @@ public DiathermicMedium copy() { return new DiathermicMedium(this); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index f21b3b8..d3e0187 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -25,6 +25,11 @@ public class NonlinearProblem extends ClassicalProblem { + /** + * + */ + private static final long serialVersionUID = -5266939533182313886L; + public NonlinearProblem() { super(); setPulse(new Pulse2D()); @@ -68,14 +73,15 @@ public NumericProperty getThermalConductivity() { } /** - * - * Does the same as super-class method plus updates the laser energy, if needed. + * + * Does the same as super-class method plus updates the laser energy, if + * needed. + * * @param params * @throws pulse.problem.schemes.solvers.SolverException * @see pulse.problem.statements.Problem.getPulse() - * - */ - + * + */ @Override public void assign(ParameterVector params) throws SolverException { super.assign(params); @@ -92,29 +98,30 @@ public void assign(ParameterVector params) throws SolverException { } } - + /** - * - * Does the same as super-class method plus extracts the laser energy and stores it in the {@code output}, if needed. + * + * Does the same as super-class method plus extracts the laser energy and + * stores it in the {@code output}, if needed. + * * @param output * @param flags * @see pulse.problem.statements.Problem.getPulse() - * + * */ - @Override public void optimisationVector(ParameterVector output) { super.optimisationVector(output); - + for (Parameter p : output.getParameters()) { var key = p.getIdentifier().getKeyword(); - if(key == LASER_ENERGY) { + if (key == LASER_ENERGY) { var bounds = Segment.boundsFrom(LASER_ENERGY); p.setBounds(bounds); p.setTransform(new StickTransform(bounds)); - p.setValue( (double) getPulse().getLaserEnergy().getValue()); + p.setValue((double) getPulse().getLaserEnergy().getValue()); } } @@ -131,4 +138,4 @@ public Problem copy() { return new NonlinearProblem(this); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index e8497e2..844c904 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -1,7 +1,5 @@ package pulse.problem.statements; - -import java.util.List; import java.util.Set; import pulse.math.ParameterVector; @@ -10,13 +8,14 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; import pulse.problem.statements.model.ThermoOpticalProperties; -import pulse.properties.Flag; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.ui.Messages; public class ParticipatingMedium extends NonlinearProblem { + private static final long serialVersionUID = -8227061869299826343L; + public ParticipatingMedium() { super(); setComplexity(ProblemComplexity.HIGH); @@ -73,4 +72,4 @@ public Problem copy() { return new ParticipatingMedium(this); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index d08d17c..588e277 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -17,6 +17,7 @@ public class PenetrationProblem extends ClassicalProblem { + private static final long serialVersionUID = -6760177658036060627L; private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( "Absorption Model Selector", AbsorptionModel.class); @@ -58,7 +59,7 @@ public List listedTypes() { list.add(instanceDescriptor); return list; } - + @Override public Set listedKeywords() { var set = super.listedKeywords(); @@ -97,4 +98,4 @@ public Problem copy() { return new PenetrationProblem(this); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index a9752e2..7392e27 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -1,5 +1,6 @@ package pulse.problem.statements; +import java.io.Serializable; import java.util.Arrays; import static pulse.input.listeners.CurveEventType.RESCALED; import static pulse.properties.NumericProperties.derive; @@ -52,6 +53,10 @@ */ public abstract class Problem extends PropertyHolder implements Reflexive, Optimisable { + /** + * + */ + private static final long serialVersionUID = 7275327427201737684L; private ThermalProperties properties; private HeatingCurve curve; private Baseline baseline; @@ -174,7 +179,7 @@ public final void setPulse(Pulse pulse) { * @param c the {@code ExperimentalData} object */ public void retrieveData(ExperimentalData c) { - baseline.fitTo(c); + baseline.fitTo(c); estimateSignalRange(c); updateProperties(this, c.getMetadata()); properties.useTheoreticalEstimates(c); @@ -423,9 +428,8 @@ private void initBaseline() { var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); if (searchTask != null) { var experimentalData = (ExperimentalData) searchTask.getInput(); - Executors.newSingleThreadExecutor().submit(() - -> baseline.fitTo(experimentalData) - ); + Runnable baselineFitter = (Runnable & Serializable) () -> baseline.fitTo(experimentalData); + Executors.newSingleThreadExecutor().submit(baselineFitter); } parameterListChanged(); } @@ -447,4 +451,4 @@ public final void setProperties(ThermalProperties properties) { public abstract boolean isReady(); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/Pulse.java b/src/main/java/pulse/problem/statements/Pulse.java index 2b5b988..f177b20 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -30,6 +30,10 @@ */ public class Pulse extends PropertyHolder { + /** + * + */ + private static final long serialVersionUID = 3564455042006771282L; private double pulseWidth; private double laserEnergy; @@ -50,11 +54,8 @@ public Pulse() { laserEnergy = (double) def(LASER_ENERGY).getValue(); instanceDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); initShape(); - instanceDescriptor.addListener(() -> { - initShape(); - this.firePropertyChanged(instanceDescriptor, instanceDescriptor); - }); - addListeners(); + addInstanceListener(); + initListeners(); } /** @@ -67,14 +68,20 @@ public Pulse(Pulse p) { this.pulseShape = p.getPulseShape(); this.pulseWidth = p.pulseWidth; this.laserEnergy = p.laserEnergy; - addListeners(); + addInstanceListener(); + initListeners(); } - private void addListeners() { + private void addInstanceListener() { instanceDescriptor.addListener(() -> { initShape(); this.firePropertyChanged(instanceDescriptor, instanceDescriptor); }); + } + + @Override + public void initListeners() { + super.initListeners(); addListener((PropertyEvent event) -> { //when a property of the pulse is changed @@ -91,16 +98,16 @@ private void addListeners() { NumericProperty pw = NumericProperties .derive(NumericPropertyKeyword.LOWER_BOUND, (Number) np.getValue()); - - var range = ( (ExperimentalData) corrTask.getInput() ).getRange(); - - if( range.getLowerBound().compareTo(pw) < 0 ) { - - //update lower bound of the range for that SearchTask - range.setLowerBound(pw); - + + var range = ((ExperimentalData) corrTask.getInput()).getRange(); + + if (range.getLowerBound().compareTo(pw) < 0) { + + //update lower bound of the range for that SearchTask + range.setLowerBound(pw); + } - + } } @@ -131,24 +138,24 @@ public void setPulseWidth(NumericProperty pulseWidth) { requireType(pulseWidth, PULSE_WIDTH); double newValue = (double) pulseWidth.getValue(); - + double relChange = Math.abs((newValue - this.pulseWidth) / (this.pulseWidth + newValue)); final double EPS = 1E-3; - + //do not update -- if new value is the same as the previous one if (relChange > EPS && newValue > 0) { - + //validate -- do not update if the new pulse width is greater than 2 half-times - SearchTask task = (SearchTask) this.specificAncestor(SearchTask.class); - ExperimentalData data = (ExperimentalData) task.getInput(); - - if(newValue < 2.0 * data.getHalfTimeCalculator().getHalfTime()) { + SearchTask task = (SearchTask) this.specificAncestor(SearchTask.class); + ExperimentalData data = (ExperimentalData) task.getInput(); + + if (newValue < 2.0 * data.getHalfTimeCalculator().getHalfTime()) { this.pulseWidth = (double) pulseWidth.getValue(); firePropertyChanged(this, pulseWidth); } - + } - + } public NumericProperty getLaserEnergy() { @@ -214,8 +221,8 @@ public PulseTemporalShape getPulseShape() { public void setPulseShape(PulseTemporalShape pulseShape) { this.pulseShape = pulseShape; - pulseShape.setParent(this); - + pulseShape.setParent(this); + } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/Pulse2D.java b/src/main/java/pulse/problem/statements/Pulse2D.java index 11d4bef..7dd4c37 100644 --- a/src/main/java/pulse/problem/statements/Pulse2D.java +++ b/src/main/java/pulse/problem/statements/Pulse2D.java @@ -12,6 +12,7 @@ public class Pulse2D extends Pulse { + private static final long serialVersionUID = 8753396877032808678L; private double spotDiameter; /** diff --git a/src/main/java/pulse/problem/statements/TwoTemperatureModel.java b/src/main/java/pulse/problem/statements/TwoTemperatureModel.java index 72298d7..980814f 100644 --- a/src/main/java/pulse/problem/statements/TwoTemperatureModel.java +++ b/src/main/java/pulse/problem/statements/TwoTemperatureModel.java @@ -23,11 +23,13 @@ public class TwoTemperatureModel extends PenetrationProblem { + private static final long serialVersionUID = 2567125396986165234L; + private Gas gas; private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>("Gas Selector", Gas.class); - + public TwoTemperatureModel() { super(); setComplexity(ProblemComplexity.MODERATE); @@ -92,7 +94,7 @@ public void optimisationVector(ParameterVector output) { continue; } - p.setTransform(new StickTransform(bounds)); + p.setTransform(new StickTransform(bounds)); p.setValue(value); p.setBounds(bounds); @@ -109,7 +111,7 @@ public void assign(ParameterVector params) throws SolverException { var key = p.getIdentifier().getKeyword(); var np = derive(key, p.inverseTransform()); - + switch (key) { case SOLID_EXCHANGE_COEFFICIENT: ttp.setSolidExchangeCoefficient(np); @@ -124,7 +126,7 @@ public void assign(ParameterVector params) throws SolverException { } } - + } @Override diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index aacbd12..9e895e4 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -25,15 +25,16 @@ public abstract class AbsorptionModel extends PropertyHolder implements Reflexive, Optimisable { + private static final long serialVersionUID = -8937860285061335131L; private Map absorptionMap; - + protected AbsorptionModel() { setPrefix("Absorption model"); absorptionMap = new HashMap<>(); absorptionMap.put(LASER, def(LASER_ABSORPTIVITY)); absorptionMap.put(THERMAL, def(THERMAL_ABSORPTIVITY)); } - + protected AbsorptionModel(AbsorptionModel c) { this.absorptionMap = new HashMap<>(); this.absorptionMap.putAll(c.absorptionMap); @@ -109,7 +110,7 @@ public Set listedKeywords() { set.add(COMBINED_ABSORPTIVITY); return set; } - + @Override public void optimisationVector(ParameterVector output) { for (Parameter p : output.getParameters()) { @@ -117,7 +118,7 @@ public void optimisationVector(ParameterVector output) { double value = 0; Transformable transform = ABS; - + switch (key) { case LASER_ABSORPTIVITY: value = (double) (getLaserAbsorptivity()).getValue(); @@ -161,7 +162,7 @@ public void assign(ParameterVector params) throws SolverException { } } - + public abstract AbsorptionModel copy(); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java index 3aebab0..6e3a956 100644 --- a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java +++ b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java @@ -2,14 +2,16 @@ public class BeerLambertAbsorption extends AbsorptionModel { + private static final long serialVersionUID = -7996852815508481089L; + public BeerLambertAbsorption() { super(); } - + public BeerLambertAbsorption(AbsorptionModel m) { super(m); } - + @Override public double absorption(SpectralRange range, double y) { double a = (double) (this.getAbsorptivity(range).getValue()); diff --git a/src/main/java/pulse/problem/statements/model/DiathermicProperties.java b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java index bc0a9a5..09f6d17 100644 --- a/src/main/java/pulse/problem/statements/model/DiathermicProperties.java +++ b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java @@ -11,6 +11,7 @@ public class DiathermicProperties extends ThermalProperties { + private static final long serialVersionUID = 1294930368429607512L; private double diathermicCoefficient; private double convectiveLosses; @@ -42,7 +43,7 @@ public void setDiathermicCoefficient(NumericProperty diathermicCoefficient) { requireType(diathermicCoefficient, DIATHERMIC_COEFFICIENT); this.diathermicCoefficient = (double) diathermicCoefficient.getValue(); } - + public NumericProperty getConvectiveLosses() { return derive(HEAT_LOSS_CONVECTIVE, convectiveLosses); } @@ -76,4 +77,4 @@ public Set listedKeywords() { return set; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java index 4ff6ff7..fc720e0 100644 --- a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java @@ -19,6 +19,7 @@ public class ExtendedThermalProperties extends ThermalProperties { + private static final long serialVersionUID = 452882822074681811L; private double d; private double Bi3; private double fovOuter; diff --git a/src/main/java/pulse/problem/statements/model/Gas.java b/src/main/java/pulse/problem/statements/model/Gas.java index 7442ebd..ad050e3 100644 --- a/src/main/java/pulse/problem/statements/model/Gas.java +++ b/src/main/java/pulse/problem/statements/model/Gas.java @@ -1,69 +1,69 @@ package pulse.problem.statements.model; +import java.io.Serializable; import pulse.util.Descriptive; import pulse.util.Reflexive; -public abstract class Gas implements Reflexive, Descriptive { - +public abstract class Gas implements Reflexive, Descriptive, Serializable { + private double conductivity; private double thermalMass; private final int atoms; private final double mass; - + /** * Universal gas constant. */ - public final static double R = 8.314; //J/K/mol - + private final static double ROOM_TEMPERATURE = 300; private final static double NORMAL_PRESSURE = 1E5; - + public Gas(int atoms, double atomicWeight) { evaluate(ROOM_TEMPERATURE, NORMAL_PRESSURE); this.atoms = atoms; - this.mass = atoms * atomicWeight/1e3; + this.mass = atoms * atomicWeight / 1e3; } - + public final void evaluate(double temperature, double pressure) { this.conductivity = thermalConductivity(temperature); this.thermalMass = cp() * density(temperature, pressure); } - + public final void evaluate(double temperature) { evaluate(temperature, NORMAL_PRESSURE); } public final double thermalDiffusivity() { - return conductivity/thermalMass; + return conductivity / thermalMass; } - + public abstract double thermalConductivity(double t); - + public double cp() { return (1.5 + atoms) * R / mass; } - + public double density(double temperature, double pressure) { return pressure * mass / (R * temperature); } - + public double getThermalMass() { return thermalMass; } - + public double getConductivity() { return conductivity; - } - + } + public double getNumberOfAtoms() { return atoms; } - + public double getMolarMass() { return mass; } - + @Override public String toString() { StringBuilder sb = new StringBuilder(getClass().getSimpleName()); @@ -71,5 +71,5 @@ public String toString() { sb.append(String.format("atoms per molecule = %d; atomic weight = %1.4f", atoms, mass)); return sb.toString(); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/statements/model/Helium.java b/src/main/java/pulse/problem/statements/model/Helium.java index 96b71f0..206a2bf 100644 --- a/src/main/java/pulse/problem/statements/model/Helium.java +++ b/src/main/java/pulse/problem/statements/model/Helium.java @@ -1,7 +1,9 @@ package pulse.problem.statements.model; public class Helium extends Gas { - + + private static final long serialVersionUID = -4697854426333597653L; + public Helium() { super(1, 4); } @@ -10,5 +12,5 @@ public Helium() { public double thermalConductivity(double t) { return 0.415 + 0.283E-3 * (t - 1200); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/statements/model/Insulator.java b/src/main/java/pulse/problem/statements/model/Insulator.java index 88c5972..25137f7 100644 --- a/src/main/java/pulse/problem/statements/model/Insulator.java +++ b/src/main/java/pulse/problem/statements/model/Insulator.java @@ -11,16 +11,17 @@ public class Insulator extends AbsorptionModel { + private static final long serialVersionUID = -3519941060924868530L; private double R; public Insulator() { super(); R = (double) def(REFLECTANCE).getValue(); } - + public Insulator(AbsorptionModel m) { super(m); - if(m instanceof Insulator) { + if (m instanceof Insulator) { R = (double) ((Insulator) m).getReflectance().getValue(); } else { R = (double) def(REFLECTANCE).getValue(); diff --git a/src/main/java/pulse/problem/statements/model/Nitrogen.java b/src/main/java/pulse/problem/statements/model/Nitrogen.java index acaef03..2b00b8e 100644 --- a/src/main/java/pulse/problem/statements/model/Nitrogen.java +++ b/src/main/java/pulse/problem/statements/model/Nitrogen.java @@ -1,14 +1,16 @@ package pulse.problem.statements.model; public class Nitrogen extends Gas { - + + private static final long serialVersionUID = -8593450360265855427L; + public Nitrogen() { super(2, 14); } @Override public double thermalConductivity(double t) { - return Math.sqrt(t) * (-92.39/t + 1.647 + 5.255E-4*t) * 1E-3; + return Math.sqrt(t) * (-92.39 / t + 1.647 + 5.255E-4 * t) * 1E-3; } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 295f6b8..5e2f135 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -2,8 +2,6 @@ import static java.lang.Math.PI; import java.util.List; -import static pulse.input.InterpolationDataset.getDataset; -import static pulse.input.InterpolationDataset.StandartType.HEAT_CAPACITY; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; @@ -13,17 +11,18 @@ import java.util.stream.Collectors; import pulse.input.ExperimentalData; -import pulse.input.InterpolationDataset; -import pulse.input.InterpolationDataset.StandartType; +import pulse.input.listeners.ExternalDatasetListener; import pulse.math.Segment; import pulse.math.transforms.StickTransform; import pulse.problem.statements.Pulse2D; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.tasks.TaskManager; import pulse.util.PropertyHolder; public class ThermalProperties extends PropertyHolder { + private static final long serialVersionUID = 1868313258119863995L; private double a; private double l; private double Bi; @@ -56,7 +55,6 @@ public ThermalProperties() { signalHeight = (double) def(MAXTEMP).getValue(); T = (double) def(TEST_TEMPERATURE).getValue(); emissivity = (double) def(EMISSIVITY).getValue(); - initListeners(); fill(); } @@ -68,7 +66,6 @@ public ThermalProperties(ThermalProperties p) { this.T = p.T; this.signalHeight = p.signalHeight; this.emissivity = p.emissivity; - initListeners(); fill(); } @@ -78,17 +75,6 @@ public List findMalformedProperties() { return list; } - private void fill() { - var rhoCurve = getDataset(StandartType.DENSITY); - var cpCurve = getDataset(StandartType.HEAT_CAPACITY); - if (rhoCurve != null) { - rho = rhoCurve.interpolateAt(T); - } - if (cpCurve != null) { - cP = cpCurve.interpolateAt(T); - } - } - /** * Calculates some or all of the following properties: * Cp, ρ, &labmda;, @@ -99,21 +85,31 @@ private void fill() { * {@code TaskManager}. *

*/ - private void initListeners() { + + private void fill() { + var i = TaskManager.getManagerInstance(); + var rhoCurve = i.getDensityDataset(); + var cpCurve = i.getSpecificHeatDataset(); + if (rhoCurve != null) { + rho = rhoCurve.interpolateAt(T); + } + if (cpCurve != null) { + cP = cpCurve.interpolateAt(T); + } - InterpolationDataset.addListener(e -> { - if (getParent() == null) { - return; + i.addExternalDatasetListener(new ExternalDatasetListener() { + @Override + public void onSpecificHeatDataLoaded() { + cP = i.getSpecificHeatDataset().interpolateAt(T); } - if (e == StandartType.DENSITY) { - rho = getDataset(StandartType.DENSITY).interpolateAt(T); - } else if (e == StandartType.HEAT_CAPACITY) { - cP = getDataset(StandartType.HEAT_CAPACITY).interpolateAt(T); + @Override + public void onDensityDataLoaded() { + rho = i.getDensityDataset().interpolateAt(T); } - + }); - + } public ThermalProperties copy() { @@ -170,7 +166,7 @@ public void set(NumericPropertyKeyword type, NumericProperty value) { public void setHeatLoss(NumericProperty Bi) { requireType(Bi, HEAT_LOSS); this.Bi = (double) Bi.getValue(); - if(areThermalPropertiesLoaded()) { + if (areThermalPropertiesLoaded()) { calculateEmissivity(); } firePropertyChanged(this, Bi); @@ -248,13 +244,14 @@ public void setTestTemperature(NumericProperty T) { requireType(T, TEST_TEMPERATURE); this.T = (double) T.getValue(); - var heatCapacity = getDataset(HEAT_CAPACITY); + var i = TaskManager.getManagerInstance(); + var heatCapacity = i.getSpecificHeatDataset(); if (heatCapacity != null) { cP = heatCapacity.interpolateAt(this.T); } - var density = getDataset(StandartType.DENSITY); + var density = i.getDensityDataset(); if (density != null) { rho = density.interpolateAt(this.T); @@ -291,31 +288,31 @@ public NumericProperty getThermalConductivity() { public void calculateEmissivity() { double newEmissivity = Bi * thermalConductivity() / (4. * Math.pow(T, 3) * l * STEFAN_BOTLZMAN); var transform = new StickTransform(Segment.boundsFrom(EMISSIVITY)); - setEmissivity(derive(EMISSIVITY, + setEmissivity(derive(EMISSIVITY, transform.transform(newEmissivity)) ); } - + /** * Calculates the radiative Biot number. + * * @return the radiative Biot number. */ - public double radiationBiot() { double lambda = thermalConductivity(); return 4.0 * emissivity * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; } - + /** - * Calculates the maximum Biot number at these conditions, which - * corresponds to an emissivity of unity. If emissivity is non-positive, - * returns the maximum Biot number defined in the XML file. + * Calculates the maximum Biot number at these conditions, which corresponds + * to an emissivity of unity. If emissivity is non-positive, returns the + * maximum Biot number defined in the XML file. + * * @return the maximum Biot number */ - public double maxRadiationBiot() { double absMax = Segment.boundsFrom(HEAT_LOSS).getMaximum(); - return emissivity > 0 ? radiationBiot() / emissivity : absMax; + return emissivity > 0 ? radiationBiot() / emissivity : absMax; } /** @@ -328,7 +325,7 @@ public double maxRadiationBiot() { public double characteristicTime() { return l * l / a; } - + public double getThermalMass() { return cP * rho; } @@ -357,7 +354,7 @@ public final boolean areThermalPropertiesLoaded() { public double maximumHeating(Pulse2D pulse) { final double Q = (double) pulse.getLaserEnergy().getValue(); final double dLas = (double) pulse.getSpotDiameter().getValue(); - return 4.0 * emissivity * Q / (PI * dLas * dLas * l * getThermalMass() ); + return 4.0 * emissivity * Q / (PI * dLas * dLas * l * getThermalMass()); } public NumericProperty getEmissivity() { @@ -385,4 +382,4 @@ public String toString() { return sb.toString(); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java index 371fadc..ff844eb 100644 --- a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -25,6 +25,7 @@ public class ThermoOpticalProperties extends ThermalProperties implements Optimisable { + private static final long serialVersionUID = 3573682830421468534L; private double opticalThickness; private double planckNumber; private double scatteringAlbedo; @@ -33,29 +34,29 @@ public class ThermoOpticalProperties extends ThermalProperties implements Optimi public ThermoOpticalProperties() { super(); - this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); - this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); - scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); - scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); - convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); + this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); + this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); + scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); + scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); + convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); } public ThermoOpticalProperties(ThermalProperties p) { super(p); - opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); - planckNumber = (double) def(PLANCK_NUMBER).getValue(); - scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); - scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); - convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); + opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); + planckNumber = (double) def(PLANCK_NUMBER).getValue(); + scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); + scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); + convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); } public ThermoOpticalProperties(ThermoOpticalProperties p) { super(p); - this.opticalThickness = p.opticalThickness; - this.planckNumber = p.planckNumber; - this.scatteringAlbedo = p.scatteringAlbedo; - this.scatteringAnisotropy = p.scatteringAnisotropy; - this.convectiveLosses = p.convectiveLosses; + this.opticalThickness = p.opticalThickness; + this.planckNumber = p.planckNumber; + this.scatteringAlbedo = p.scatteringAlbedo; + this.scatteringAnisotropy = p.scatteringAnisotropy; + this.convectiveLosses = p.convectiveLosses; } @Override @@ -135,7 +136,7 @@ public void setScatteringAnisotropy(NumericProperty A1) { this.scatteringAnisotropy = (double) A1.getValue(); firePropertyChanged(this, A1); } - + public void setConvectiveLosses(NumericProperty losses) { requireType(losses, HEAT_LOSS_CONVECTIVE); this.convectiveLosses = (double) losses.getValue(); @@ -145,7 +146,7 @@ public void setConvectiveLosses(NumericProperty losses) { public NumericProperty getConvectiveLosses() { return derive(HEAT_LOSS_CONVECTIVE, convectiveLosses); } - + public NumericProperty getScatteringAlbedo() { return derive(SCATTERING_ALBEDO, scatteringAlbedo); } @@ -173,7 +174,7 @@ public void useTheoreticalEstimates(ExperimentalData c) { public String getDescriptor() { return "Thermo-Physical & Optical Properties"; } - + @Override public String toString() { StringBuilder sb = new StringBuilder(super.toString()); @@ -186,7 +187,7 @@ public String toString() { sb.append(String.format("%n %-25s", this.getDensity())); return sb.toString(); } - + @Override public void optimisationVector(ParameterVector output) { Segment bounds = null; @@ -220,9 +221,9 @@ public void optimisationVector(ParameterVector output) { bounds = Segment.boundsFrom(HEAT_LOSS_CONVECTIVE); break; case HEAT_LOSS: - value = (double) getHeatLoss().getValue(); - bounds = new Segment(0.0, maxRadiationBiot() ); - break; + value = (double) getHeatLoss().getValue(); + bounds = new Segment(0.0, maxRadiationBiot()); + break; default: continue; diff --git a/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java b/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java index c879327..099fde4 100644 --- a/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java +++ b/src/main/java/pulse/problem/statements/model/TwoTemperatureProperties.java @@ -11,10 +11,11 @@ public class TwoTemperatureProperties extends ThermalProperties { + private static final long serialVersionUID = 4157382023954200467L; private double exchangeSolid; private double exchangeGas; private double gasHeatLoss; - + public TwoTemperatureProperties() { super(); exchangeSolid = (double) def(SOLID_EXCHANGE_COEFFICIENT).getValue(); @@ -29,8 +30,7 @@ public TwoTemperatureProperties(ThermalProperties p) { this.exchangeSolid = np.exchangeSolid; this.exchangeGas = np.exchangeGas; this.gasHeatLoss = np.gasHeatLoss; - } - else { + } else { exchangeSolid = (double) def(SOLID_EXCHANGE_COEFFICIENT).getValue(); exchangeGas = (double) def(GAS_EXCHANGE_COEFFICIENT).getValue(); gasHeatLoss = (double) def(HEAT_LOSS_GAS).getValue(); @@ -80,7 +80,7 @@ public Set listedKeywords() { public NumericProperty getSolidExchangeCoefficient() { return derive(SOLID_EXCHANGE_COEFFICIENT, exchangeSolid); } - + public NumericProperty getGasExchangeCoefficient() { return derive(GAS_EXCHANGE_COEFFICIENT, exchangeGas); } @@ -90,7 +90,7 @@ public void setSolidExchangeCoefficient(NumericProperty p) { this.exchangeSolid = (double) p.getValue(); firePropertyChanged(this, p); } - + public void setGasExchangeCoefficient(NumericProperty p) { NumericProperty.requireType(p, GAS_EXCHANGE_COEFFICIENT); this.exchangeGas = (double) p.getValue(); diff --git a/src/main/java/pulse/properties/Flag.java b/src/main/java/pulse/properties/Flag.java index 94b6ce3..d6c6f10 100644 --- a/src/main/java/pulse/properties/Flag.java +++ b/src/main/java/pulse/properties/Flag.java @@ -12,6 +12,7 @@ */ public class Flag implements Property { + private static final long serialVersionUID = 4927536419752406797L; private NumericPropertyKeyword index; private boolean value; private String descriptor; @@ -26,15 +27,15 @@ public class Flag implements Property { public Flag(NumericPropertyKeyword type) { this(type, false); } - + public Flag(Flag f) { this(f.index, f.value); } - + public Flag(NumericPropertyKeyword type, boolean flag) { this.index = type; this.value = flag; - } + } /** * Creates a {@code Flag} with the following pre-specified parameters: type diff --git a/src/main/java/pulse/properties/NumericProperties.java b/src/main/java/pulse/properties/NumericProperties.java index 738885a..3f0f5ff 100644 --- a/src/main/java/pulse/properties/NumericProperties.java +++ b/src/main/java/pulse/properties/NumericProperties.java @@ -41,8 +41,8 @@ public static boolean isValueSensible(NumericProperty property, Number val) { double v = val.doubleValue(); final double EPS = 1E-12; boolean ok = true; - - if( !Double.isFinite(v) + + if (!Double.isFinite(v) || v > property.getMaximum().doubleValue() + EPS || v < property.getMinimum().doubleValue() - EPS) { ok = false; @@ -91,7 +91,7 @@ public static int compare(NumericProperty a, NumericProperty b) { Double d1 = ((Number) a.getValue()).doubleValue(); Double d2 = ((Number) b.getValue()).doubleValue(); - final double eps = 1E-8 * (d1 + d2) / 2.0; + final double eps = 1E-8 * Math.abs(d1 + d2) / 2.0; return Math.abs(d1 - d2) < eps ? 0 : d1.compareTo(d2); } @@ -99,8 +99,7 @@ public static int compare(NumericProperty a, NumericProperty b) { /** * Searches for the default {@code NumericProperty} corresponding to * {@code keyword} in the list of pre-defined properties loaded from the - * respective {@code .xml} file, and if found creates a new - * { + * respective {@code .xml} file, and if found creates a new { * * @NumericProperty} which will replicate all field of the latter, but will * set its value to {@code value}. diff --git a/src/main/java/pulse/properties/NumericProperty.java b/src/main/java/pulse/properties/NumericProperty.java index 734a70f..495d1a1 100644 --- a/src/main/java/pulse/properties/NumericProperty.java +++ b/src/main/java/pulse/properties/NumericProperty.java @@ -22,6 +22,11 @@ */ public class NumericProperty implements Property, Comparable { + /** + * + */ + private static final long serialVersionUID = -7132274623596750984L; + private Number value; private Number minimum; @@ -263,6 +268,7 @@ public boolean equals(Object o) { @Override public int compareTo(NumericProperty arg0) { final int result = this.getType().compareTo(arg0.getType()); + int res = compare(this, arg0); return result != 0 ? result : compare(this, arg0); } @@ -310,34 +316,35 @@ public void setDefaultSearchVariable(boolean defaultSearchVariable) { public void setOptimisable(boolean optimisable) { this.optimisable = optimisable; } - + public Number getDimensionDelta() { - if(type == NumericPropertyKeyword.TEST_TEMPERATURE) + if (type == NumericPropertyKeyword.TEST_TEMPERATURE) { return -273.15; - else + } else { return 0.0; + } } - + /** - * Represents the bounds specified for this numeric property - * as a {@code Segment} object. The bound numbers are taken by - * their double values and assigned to the segment. + * Represents the bounds specified for this numeric property as a + * {@code Segment} object. The bound numbers are taken by their double + * values and assigned to the segment. + * * @return the bounds in which this property is allowed to change */ - public Segment getBounds() { return new Segment(minimum.doubleValue(), maximum.doubleValue()); } - + /** * Uses a {@code NumericPropertyFormatter} to generate a formatted output - * @return a formatted string output with the value (and error -- if available) - * of this numeric property + * + * @return a formatted string output with the value (and error -- if + * available) of this numeric property */ - public String formattedOutput() { var num = new NumericPropertyFormatter(this, true, true); return num.numberFormat(this).format(value); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/properties/NumericPropertyFormatter.java b/src/main/java/pulse/properties/NumericPropertyFormatter.java index f3c83cf..065f060 100644 --- a/src/main/java/pulse/properties/NumericPropertyFormatter.java +++ b/src/main/java/pulse/properties/NumericPropertyFormatter.java @@ -30,6 +30,7 @@ */ public class NumericPropertyFormatter extends AbstractFormatter { + private static final long serialVersionUID = -7733589481239097566L; private NumericPropertyKeyword key; private Segment bounds; private boolean convertDimension = true; @@ -74,9 +75,9 @@ public NumberFormat numberFormat(NumericProperty p) { : (double) value; double absAdjustedValue = Math.abs(adjustedValue); - if (addHtmlTags && - ( (absAdjustedValue > UPPER_LIMIT) - || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO)) ) { + if (addHtmlTags + && ((absAdjustedValue > UPPER_LIMIT) + || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO))) { //format with scientific notations f = new ScientificFormat(p.getDimensionFactor(), p.getDimensionDelta()); } else { @@ -85,7 +86,7 @@ public NumberFormat numberFormat(NumericProperty p) { } } - + return f; } diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 36b1666..344bdbe 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -157,13 +157,10 @@ public enum NumericPropertyKeyword { * sample (1D and 2D problems). */ HEAT_LOSS, - /** * The convective heat loss in diathermic and participating medium problems. */ - HEAT_LOSS_CONVECTIVE, - /** * A directive for the optimiser to maintain equal heat losses on all * surfaces of the sample. Note that the dimensionless heat losses, i.e. @@ -224,7 +221,6 @@ public enum NumericPropertyKeyword { * Statistical significance for calculating the critical value. */ SIGNIFICANCE, - /** * Optimiser statistic (usually, RSS). */ @@ -354,50 +350,36 @@ public enum NumericPropertyKeyword { * damping. A value of 1 gives pure Marquardt damping. */ DAMPING_RATIO, - /** * Determines how much weight is attributed to the front-face light source - * compared to rear face. Can be a number between zero and unity. + * compared to rear face. Can be a number between zero and unity. */ - SOURCE_GEOMETRIC_FACTOR, - /** * Max. no. of high-frequency waves in the sinusoidal baseline. */ - MAX_HIGH_FREQ_WAVES, - /** - * Max. no. of low-frequency waves in the sinusoidal baseline. - */ - + * Max. no. of low-frequency waves in the sinusoidal baseline. + */ MAX_LOW_FREQ_WAVES, - /** * Energy exchange coefficient in the two-temperature model (g). */ - SOLID_EXCHANGE_COEFFICIENT, - /** * Energy exchange coefficient in the two-temperature model (g'). */ - GAS_EXCHANGE_COEFFICIENT, - /** * Heat loss for the gas in the 2T-model. */ - HEAT_LOSS_GAS, - /** * Value of objective function. */ - OBJECTIVE_FUNCTION; - + public static Optional findAny(String key) { return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); } diff --git a/src/main/java/pulse/properties/Property.java b/src/main/java/pulse/properties/Property.java index a1215d7..0ff7804 100644 --- a/src/main/java/pulse/properties/Property.java +++ b/src/main/java/pulse/properties/Property.java @@ -1,10 +1,12 @@ package pulse.properties; +import java.io.Serializable; + /** * The basic interface for properties. The only declared functionality consists * in the ability to report the associated value and deliver text description. */ -public interface Property { +public interface Property extends Serializable { /** * Retrieves the value of this {@code Property}. diff --git a/src/main/java/pulse/properties/SampleName.java b/src/main/java/pulse/properties/SampleName.java index 2f9f6cc..0e355f2 100644 --- a/src/main/java/pulse/properties/SampleName.java +++ b/src/main/java/pulse/properties/SampleName.java @@ -4,6 +4,7 @@ public class SampleName implements Property { + private static final long serialVersionUID = -965821128124753850L; private String name; public SampleName(String name) { @@ -65,4 +66,4 @@ public boolean equals(Object obj) { return true; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/properties/ScientificFormat.java b/src/main/java/pulse/properties/ScientificFormat.java index cd9baca..a61108c 100644 --- a/src/main/java/pulse/properties/ScientificFormat.java +++ b/src/main/java/pulse/properties/ScientificFormat.java @@ -30,6 +30,7 @@ */ public class ScientificFormat extends NumberFormat { + private static final long serialVersionUID = -6509402151736747913L; private final int dimensionFactor; private final double dimensionDelta; diff --git a/src/main/java/pulse/search/GeneralTask.java b/src/main/java/pulse/search/GeneralTask.java index 3bd42d0..426b067 100644 --- a/src/main/java/pulse/search/GeneralTask.java +++ b/src/main/java/pulse/search/GeneralTask.java @@ -15,41 +15,41 @@ import static pulse.tasks.processing.Buffer.getSize; import pulse.util.Accessible; -public abstract class GeneralTask +public abstract class GeneralTask extends Accessible implements Runnable { private IterativeState path; //current sate private IterativeState best; //best state - + private final Buffer buffer; private PathOptimiser optimiser; - + public GeneralTask() { buffer = new Buffer(); buffer.setParent(this); } - + public abstract List activeParameters(); /** - * Creates a search vector populated by parameters that - * are included in the optimisation routine. + * Creates a search vector populated by parameters that are included in the + * optimisation routine. + * * @return the parameter vector with optimisation parameters */ - public abstract ParameterVector searchVector(); - + /** - * Tries to assign a selected set of parameters to the search vector - * used in optimisation. - * @param pv a parameter vector containing all of the optimisation parameters - * whose values will be assigned to this task - * @throws SolverException + * Tries to assign a selected set of parameters to the search vector used in + * optimisation. + * + * @param pv a parameter vector containing all of the optimisation + * parameters whose values will be assigned to this task + * @throws SolverException */ - public abstract void assign(ParameterVector pv) throws SolverException; - - /** + + /** *

* Runs this task if is either {@code READY} or {@code QUEUED}. Otherwise, * will do nothing. After making some preparatory steps, will initiate a @@ -65,9 +65,9 @@ public GeneralTask() { public void run() { setDefaultOptimiser(); best = null; - setIterativeState( optimiser.initState(this) ); + setIterativeState(optimiser.initState(this)); - var errorTolerance = (double) optimiser.getErrorTolerance().getValue(); + double errorTolerance = (double) optimiser.getErrorTolerance().getValue(); int bufferSize = (Integer) getSize().getValue(); buffer.init(); //correlationBuffer.clear(); @@ -78,7 +78,7 @@ public void run() { var singleThreadExecutor = Executors.newSingleThreadExecutor(); var response = getResponse(); - + try { response.objectiveFunction(this); } catch (SolverException e1) { @@ -100,7 +100,7 @@ public void run() { onSolverException(e); break outer; } - + //if global best is better than the converged value if (best != null && best.getCost() < path.getCost()) { try { @@ -114,7 +114,7 @@ public void run() { } final var j = i; - + bufferFutures.add(CompletableFuture.runAsync(() -> { buffer.fill(this, j); intermediateProcessing(); @@ -123,52 +123,50 @@ public void run() { } bufferFutures.forEach(future -> future.join()); - - } while (buffer.isErrorTooHigh(errorTolerance) + + } while (buffer.isErrorTooHigh(errorTolerance) && isInProgress()); singleThreadExecutor.shutdown(); if (isInProgress()) { postProcessing(); - } + } - } + } + + public abstract boolean isInProgress(); - public abstract boolean isInProgress(); - /** - * Override this to add intermediate processing of results e.g. - * with a correlation test. + * Override this to add intermediate processing of results e.g. with a + * correlation test. */ - public void intermediateProcessing() { //empty } - + /** * Specifies what should be done when a solver exception is encountered. * Empty by default + * * @param e1 a solver exception */ - public void onSolverException(SolverException e1) { //empty } - + /** - * Override this to add post-processing checks - * e.g. normality tests or range checking. + * Override this to add post-processing checks e.g. normality tests or range + * checking. */ - public void postProcessing() { //empty } - + public final Buffer getBuffer() { return buffer; } - + public void setIterativeState(IterativeState state) { this.path = state; } @@ -196,23 +194,24 @@ public void storeState() { best = new IterativeState(path); } } - + public final void setOptimiser(PathOptimiser optimiser) { this.optimiser = optimiser; } - + public void setDefaultOptimiser() { var instance = PathOptimiser.getInstance(); - if(optimiser == null || optimiser != instance) { + if (optimiser == null || optimiser != instance) { setOptimiser(PathOptimiser.getInstance()); } } - + public double objectiveFunction() throws SolverException { return getResponse().objectiveFunction(this); } - - public abstract I getInput(); + + public abstract I getInput(); + public abstract R getResponse(); - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/search/Optimisable.java b/src/main/java/pulse/search/Optimisable.java index 44666f5..53c29d4 100644 --- a/src/main/java/pulse/search/Optimisable.java +++ b/src/main/java/pulse/search/Optimisable.java @@ -1,6 +1,5 @@ package pulse.search; - import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; diff --git a/src/main/java/pulse/search/SimpleOptimisationTask.java b/src/main/java/pulse/search/SimpleOptimisationTask.java index 4fbce66..ec0fd4e 100644 --- a/src/main/java/pulse/search/SimpleOptimisationTask.java +++ b/src/main/java/pulse/search/SimpleOptimisationTask.java @@ -30,11 +30,11 @@ public SimpleOptimisationTask(T optimisable, DiscreteInput input) { this.input = input; this.optimisable = optimisable; } - + @Override public void run() { var optimiser = PathOptimiser.getInstance(); - if(optimiser == null) { + if (optimiser == null) { PathOptimiser.setInstance(LMOptimiser.getInstance()); } super.run(); @@ -74,20 +74,20 @@ public boolean isInProgress() { public void set(NumericPropertyKeyword type, NumericProperty property) { optimisable.set(type, property); } - + @Override - public List activeParameters() { + public List activeParameters() { return selectActiveAndListed(ActiveFlags.getAllFlags(), optimisable); } - + @Override public void setDefaultOptimiser() { setOptimiser(LMOptimiser.getInstance()); } - + @Override public DiscreteInput getInput() { return input; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/ActiveFlags.java b/src/main/java/pulse/search/direction/ActiveFlags.java index 81103d1..1ad72f3 100644 --- a/src/main/java/pulse/search/direction/ActiveFlags.java +++ b/src/main/java/pulse/search/direction/ActiveFlags.java @@ -1,5 +1,6 @@ package pulse.search.direction; +import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -13,8 +14,9 @@ import pulse.tasks.TaskManager; import pulse.util.PropertyHolder; -public class ActiveFlags { +public class ActiveFlags implements Serializable { + private static final long serialVersionUID = -8711073682010113698L; private static List flags; static { @@ -42,12 +44,12 @@ public static Set availableProperties() { return set; } - var p = ( (Calculation) t.getResponse() ).getProblem(); + var p = ((Calculation) t.getResponse()).getProblem(); if (p != null) { var fullList = p.listedKeywords(); - fullList.addAll( ( (ExperimentalData) t.getInput() ).listedKeywords()); + fullList.addAll(((ExperimentalData) t.getInput()).listedKeywords()); NumericPropertyKeyword key; for (Flag property : flags) { @@ -62,36 +64,36 @@ public static Set availableProperties() { return set; } - + public static Flag get(NumericPropertyKeyword key) { var flag = flags.stream().filter(f -> f.getType() == key).findAny(); return flag.isPresent() ? flag.get() : null; - } - + } + /** * Creates a deep copy of the flags collection. + * * @return a deep copy of the flags */ - public static List storeState() { var copy = new ArrayList(); - for(Flag f : flags) { + for (Flag f : flags) { copy.add(new Flag(f)); } return copy; } - + /** - * Loads the argument into the current list of flags. - * This will update any matching flags and assign values correpon - * @param flags + * Loads the argument into the current list of flags. This will update any + * matching flags and assign values correpon + * + * @param flags */ - public static void loadState(List flags) { - for(Flag f : ActiveFlags.flags) { - Optional existingFlag = flags.stream().filter(fl -> - fl.getType() == f.getType()).findFirst(); - if(existingFlag.isPresent()) { + for (Flag f : ActiveFlags.flags) { + Optional existingFlag = flags.stream().filter(fl + -> fl.getType() == f.getType()).findFirst(); + if (existingFlag.isPresent()) { f.setValue((boolean) existingFlag.get().getValue()); } } @@ -99,15 +101,15 @@ public static void loadState(List flags) { public static List selectActiveAndListed(List flags, PropertyHolder listed) { //return empty list - if(listed == null) { + if (listed == null) { return new ArrayList<>(); } - + return selectActiveTypes(flags).stream() .filter(type -> listed.isListedNumericType(type)) .collect(Collectors.toList()); } - + public static List selectActiveTypes(List flags) { return Flag.selectActive(flags).stream().map(flag -> flag.getType()).collect(Collectors.toList()); } diff --git a/src/main/java/pulse/search/direction/BFGSOptimiser.java b/src/main/java/pulse/search/direction/BFGSOptimiser.java index 273b936..8911926 100644 --- a/src/main/java/pulse/search/direction/BFGSOptimiser.java +++ b/src/main/java/pulse/search/direction/BFGSOptimiser.java @@ -30,6 +30,10 @@ */ public class BFGSOptimiser extends CompositePathOptimiser { + /** + * + */ + private static final long serialVersionUID = -8542438015176648987L; private static BFGSOptimiser instance = new BFGSOptimiser(); private BFGSOptimiser() { @@ -68,9 +72,9 @@ public void prepare(GeneralTask task) throws SolverException { p.setHessian(hessian); // g_k, g_k+1, p_k+1, B_k, alpha_k+1 p.setGradient(g1); // set g1 as the new gradient for next step } - + /** - * Uses the BFGS formula to calculate the Hessian. + * Uses the BFGS formula to calculate the Hessian. * * @param g1 gradient at step k * @param g2 gradient at step k+1 diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index bfe1ef8..8340c81 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -15,6 +15,7 @@ */ public class ComplexPath extends GradientGuidedPath { + private static final long serialVersionUID = -1520823504831702183L; private SquareMatrix hessian; private SquareMatrix inverseHessian; diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 48766c5..1e91de6 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -16,23 +16,22 @@ public abstract class CompositePathOptimiser extends GradientBasedOptimiser { - private InstanceDescriptor instanceDescriptor + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( - "Linear Optimiser Selector", LinearOptimiser.class); + "Linear Optimiser Selector", LinearOptimiser.class); private LinearOptimiser linearSolver; - - /** - * Maximum number of consequent failed iterations that can be rejected. - * Up to {@value MAX_FAILED_ATTEMPTS} failed attempts are allowed. + + /** + * Maximum number of consequent failed iterations that can be rejected. Up + * to {@value MAX_FAILED_ATTEMPTS} failed attempts are allowed. */ - public final static int MAX_FAILED_ATTEMPTS = 2; - + /** * For numerical comparison. */ - public final static double EPS = 1e-10; + public final static double EPS = 1e-10; public CompositePathOptimiser() { instanceDescriptor.setSelectedDescriptor(WolfeOptimiser.class.getSimpleName()); @@ -48,7 +47,7 @@ private void initLinearOptimiser() { @Override public boolean iteration(GeneralTask task) throws SolverException { var p = (GradientGuidedPath) task.getIterativeState(); // the previous state of the task - + boolean accept = true; /* @@ -62,7 +61,7 @@ public boolean iteration(GeneralTask task) throws SolverException { double initialCost = task.getResponse().objectiveFunction(task); p.setCost(initialCost); - var parameters = task.searchVector(); + var parameters = task.searchVector(); p.setParameters(parameters); // store current parameters @@ -71,24 +70,24 @@ public boolean iteration(GeneralTask task) throws SolverException { p.setLinearStep(step); // new set of parameters determined through search - var candidateParams = parameters.toVector().sum(dir.multiply(step)); + var candidateParams = parameters.toVector().sum(dir.multiply(step)); var candidateVector = new ParameterVector(parameters, candidateParams); - - if(candidateVector.findMalformedElements().isEmpty()) { + + if (candidateVector.findMalformedElements().isEmpty()) { task.assign(candidateVector); // assign new parameters } - - double newCost = task.getResponse().objectiveFunction(task); + + double newCost = task.getResponse().objectiveFunction(task); // calculate the sum of squared residuals - if (newCost > initialCost - EPS - && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS - && p instanceof ComplexPath) { - var complexPath = (ComplexPath)p; + if (newCost > initialCost - EPS + && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS + && p instanceof ComplexPath) { + var complexPath = (ComplexPath) p; task.assign(parameters); // roll back if cost increased // attempt to reset -> in case of Hessian-based methods, // this will change the Hessian) { - complexPath.setHessian( createIdentityMatrix(parameters.dimension()) ); + complexPath.setHessian(createIdentityMatrix(parameters.dimension())); p.incrementFailedAttempts(); accept = false; } else { @@ -97,7 +96,7 @@ public boolean iteration(GeneralTask task) throws SolverException { this.prepare(task); // update gradients, Hessians, etc. -> for the next step, [k + 1] p.setCost(newCost); p.incrementStep(); // increment the counter of successful steps - } + } } @@ -144,4 +143,4 @@ public GradientGuidedPath initState(GeneralTask t) { return new ComplexPath(t); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/DirectionSolver.java b/src/main/java/pulse/search/direction/DirectionSolver.java index c8e55a1..f96c191 100644 --- a/src/main/java/pulse/search/direction/DirectionSolver.java +++ b/src/main/java/pulse/search/direction/DirectionSolver.java @@ -1,9 +1,10 @@ package pulse.search.direction; +import java.io.Serializable; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -public interface DirectionSolver { +public interface DirectionSolver extends Serializable { /** * Finds the direction of the minimum using the previously calculated values diff --git a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java index abee584..c55405e 100644 --- a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java +++ b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java @@ -82,7 +82,7 @@ public Vector gradient(GeneralTask task) throws SolverException { final var pVector = params.toVector(); var grad = new Vector(params.dimension()); final var ps = params.getParameters(); - + for (int i = 0, size = params.dimension(); i < size; i++) { var key = ps.get(i).getIdentifier().getKeyword(); var defProp = key != null ? NumericProperties.def(key) : null; diff --git a/src/main/java/pulse/search/direction/GradientGuidedPath.java b/src/main/java/pulse/search/direction/GradientGuidedPath.java index 7b9a06c..f4dd4f8 100644 --- a/src/main/java/pulse/search/direction/GradientGuidedPath.java +++ b/src/main/java/pulse/search/direction/GradientGuidedPath.java @@ -30,6 +30,10 @@ */ public class GradientGuidedPath extends IterativeState { + /** + * + */ + private static final long serialVersionUID = -6450999613326096767L; private Vector direction; private Vector gradient; private double minimumPoint; @@ -51,7 +55,7 @@ public void configure(GeneralTask t) { try { this.gradient = ((GradientBasedOptimiser) PathOptimiser.getInstance()).gradient(t); } catch (SolverException ex) { - t.onSolverException( new SolverException("Gradient calculation error", OPTIMISATION_ERROR)); + t.onSolverException(new SolverException("Gradient calculation error", OPTIMISATION_ERROR)); ex.printStackTrace(); } minimumPoint = 0.0; @@ -79,6 +83,6 @@ public double getMinimumPoint() { public void setLinearStep(double min) { minimumPoint = min; - } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/IterativeState.java b/src/main/java/pulse/search/direction/IterativeState.java index 9045d46..6d25807 100644 --- a/src/main/java/pulse/search/direction/IterativeState.java +++ b/src/main/java/pulse/search/direction/IterativeState.java @@ -1,5 +1,6 @@ package pulse.search.direction; +import java.io.Serializable; import pulse.math.ParameterVector; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.ITERATION; @@ -7,38 +8,41 @@ import pulse.properties.NumericProperty; import pulse.search.GeneralTask; -public class IterativeState { +public class IterativeState implements Serializable { + private static final long serialVersionUID = -3924087865736298552L; private ParameterVector parameters; private double cost = Double.POSITIVE_INFINITY; private int iteration; private int failedAttempts; /** - * Stores the parameter vector and cost function value associated with the specified state. + * Stores the parameter vector and cost function value associated with the + * specified state. + * * @param other another state of the optimiser */ - public IterativeState(IterativeState other) { this.parameters = new ParameterVector(other.parameters); this.cost = other.cost; } - + public IterativeState(GeneralTask t) { this.parameters = t.searchVector(); } - + //default constructor - public IterativeState() {} - + public IterativeState() { + } + public double getCost() { return cost; } - + public void setCost(double cost) { this.cost = cost; } - + public void reset() { iteration = 0; setCost(Double.POSITIVE_INFINITY); @@ -63,7 +67,7 @@ public void resetFailedAttempts() { public void incrementFailedAttempts() { failedAttempts++; } - + public ParameterVector getParameters() { return parameters; } @@ -72,4 +76,4 @@ public void setParameters(ParameterVector parameters) { this.parameters = parameters; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index d3b22c7..9d4c1e5 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -36,13 +36,13 @@ */ public class LMOptimiser extends GradientBasedOptimiser { + private static final long serialVersionUID = -7954867240278082038L; private static final LMOptimiser instance = new LMOptimiser(); private double dampingRatio; - + /** * Up to {@value MAX_FAILED_ATTEMPTS} failed attempts are allowed. */ - public final static int MAX_FAILED_ATTEMPTS = 5; private LMOptimiser() { @@ -79,17 +79,17 @@ public boolean iteration(GeneralTask task) throws SolverException { var lmDirection = getSolver().direction(p); var candidate = parameters.toVector().sum(lmDirection); - - if( Arrays.stream( candidate.getData() ).anyMatch(el -> !Double.isFinite(el) ) ) { + + if (Arrays.stream(candidate.getData()).anyMatch(el -> !Double.isFinite(el))) { throw new SolverException("Illegal candidate parameters: not finite! " + p.getIteration(), ILLEGAL_PARAMETERS); } - + task.assign(new ParameterVector( parameters, candidate)); // assign new parameters - + double newCost = task.objectiveFunction(); // calculate the sum of squared residuals - + /* * Delayed gratification */ @@ -135,12 +135,12 @@ public void prepare(GeneralTask task) throws SolverException { // the Jacobian is then used to calculate the 'gradient' Vector g1 = halfGradient(p); // g1 p.setGradient(g1); - - if(Arrays.stream(g1.getData()).anyMatch(v -> !Double.isFinite(v))) { + + if (Arrays.stream(g1.getData()).anyMatch(v -> !Double.isFinite(v))) { throw new SolverException("Could not calculate objective function gradient", - OPTIMISATION_ERROR); + OPTIMISATION_ERROR); } - + // the Hessian is then regularised by adding labmda*I var hessian = p.getNonregularisedHessian(); var damping = (levenbergDamping(hessian).multiply(dampingRatio) @@ -175,7 +175,7 @@ public void prepare(GeneralTask task) throws SolverException { public RectangularMatrix jacobian(GeneralTask task) throws SolverException { var residualCalculator = task.getResponse().getOptimiserStatistic(); - + var p = ((LMPath) task.getIterativeState()); final var params = p.getParameters(); @@ -186,12 +186,12 @@ public RectangularMatrix jacobian(GeneralTask task) throws SolverException { var jacobian = new double[numPoints][numParams]; var ps = params.getParameters(); - + for (int i = 0; i < numParams; i++) { var key = ps.get(i).getIdentifier().getKeyword(); - double dx = dx( - key != null ? NumericProperties.def(key) : null, + double dx = dx( + key != null ? NumericProperties.def(key) : null, ps.get(i).inverseTransform()); final var shift = new Vector(numParams); @@ -201,19 +201,19 @@ public RectangularMatrix jacobian(GeneralTask task) throws SolverException { task.assign(new ParameterVector(params, pVector.sum(shift))); task.objectiveFunction(); var r = residualCalculator.getResiduals(); - - for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); - j < realNumPoints; j++) { + + for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); + j < realNumPoints; j++) { jacobian[j][i] = r.get(j) / dx; } - + // - shift task.assign(new ParameterVector(params, pVector.subtract(shift))); task.objectiveFunction(); - for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); + for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); j < realNumPoints; j++) { jacobian[j][i] -= r.get(j) / dx; @@ -221,14 +221,14 @@ public RectangularMatrix jacobian(GeneralTask task) throws SolverException { } } - + // revert to original params task.assign(params); return Matrices.createMatrix(jacobian); } - + @Override public GradientGuidedPath initState(GeneralTask t) { return new LMPath(t); @@ -260,8 +260,8 @@ private SquareMatrix levenbergDamping(SquareMatrix hessian) { private SquareMatrix marquardtDamping(SquareMatrix hessian) { return hessian.blockDiagonal(); } - - @Override + + @Override public Set listedKeywords() { var set = super.listedKeywords(); set.add(DAMPING_RATIO); diff --git a/src/main/java/pulse/search/direction/LMPath.java b/src/main/java/pulse/search/direction/LMPath.java index 2528968..39e9985 100644 --- a/src/main/java/pulse/search/direction/LMPath.java +++ b/src/main/java/pulse/search/direction/LMPath.java @@ -7,6 +7,7 @@ class LMPath extends ComplexPath { + private static final long serialVersionUID = -7154616034580697035L; private Vector residualVector; private RectangularMatrix jacobian; private SquareMatrix nonregularisedHessian; diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index cb5d4fa..21618a8 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -94,7 +94,7 @@ public void reset() { * @see pulse.search.linear.LinearOptimiser */ public abstract boolean iteration(GeneralTask task) throws SolverException; - + /** * Defines a set of procedures to be run at the end of the search iteration. * @@ -216,7 +216,7 @@ protected final void setSolver(DirectionSolver solver) { /** * Checks if this optimiser is compatible with the statistic passed to the * method as its argument.By default, this will accept any - {@code OptimiserStatistic} + * {@code OptimiserStatistic} * * @param os a selected optimiser metric * @return {@code true}, if not specified otherwise by its subclass diff --git a/src/main/java/pulse/search/direction/SR1Optimiser.java b/src/main/java/pulse/search/direction/SR1Optimiser.java index 8b2f708..716753f 100644 --- a/src/main/java/pulse/search/direction/SR1Optimiser.java +++ b/src/main/java/pulse/search/direction/SR1Optimiser.java @@ -13,6 +13,8 @@ public class SR1Optimiser extends CompositePathOptimiser { + private static final long serialVersionUID = -3041166132227281210L; + private static SR1Optimiser instance = new SR1Optimiser(); private final static double r = 1E-8; diff --git a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java index 8fcbc31..ee9b68e 100644 --- a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java +++ b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java @@ -16,6 +16,10 @@ */ public class SteepestDescentOptimiser extends CompositePathOptimiser { + /** + * + */ + private static final long serialVersionUID = -6868259511333467862L; private static SteepestDescentOptimiser instance = new SteepestDescentOptimiser(); private SteepestDescentOptimiser() { @@ -67,4 +71,4 @@ public GradientGuidedPath initState(GeneralTask t) { return new GradientGuidedPath(t); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/pso/ConstrictionMover.java b/src/main/java/pulse/search/direction/pso/ConstrictionMover.java index f19d824..db1d89b 100644 --- a/src/main/java/pulse/search/direction/pso/ConstrictionMover.java +++ b/src/main/java/pulse/search/direction/pso/ConstrictionMover.java @@ -4,16 +4,16 @@ import pulse.math.linear.Vector; public class ConstrictionMover implements Mover { - + private double c1; //social private double c2; //cognitive - private double chi; + private double chi; public final static double DEFAULT_CHI = 0.7298; public final static double DEFAULT_C = 1.49618; - + public ConstrictionMover() { - chi = DEFAULT_CHI; - c1 = c2 = DEFAULT_C; + chi = DEFAULT_CHI; + c1 = c2 = DEFAULT_C; } @Override @@ -23,24 +23,24 @@ public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleStat var curPosV = curPos.toVector(); final int n = curPos.dimension(); - Vector nsum = new Vector(n); + Vector nsum = new Vector(n); - var localBest = p.getBestState().getPosition(); //best position by local particle + var localBest = p.getBestState().getPosition(); //best position by local particle var localBestV = localBest.toVector(); var globalBest = gBest.getPosition(); //best position by any particle var globalBestV = globalBest.toVector(); - + nsum = nsum.sum(Vector.random(n, 0.0, c1) - .multComponents(localBestV.subtract(curPosV)) - ); - + .multComponents(localBestV.subtract(curPosV)) + ); + nsum = nsum.sum(Vector.random(n, 0.0, c2) - .multComponents(globalBestV.subtract(curPosV)) - ); + .multComponents(globalBestV.subtract(curPosV)) + ); var newVelocity = (current.getVelocity().toVector().sum(nsum)).multiply(chi); var newPosition = curPosV.sum(newVelocity); - + return new ParticleState( new ParameterVector(curPos, newPosition), new ParameterVector(curPos, newVelocity)); diff --git a/src/main/java/pulse/search/direction/pso/FIPSMover.java b/src/main/java/pulse/search/direction/pso/FIPSMover.java index b6869ec..09fe7a6 100644 --- a/src/main/java/pulse/search/direction/pso/FIPSMover.java +++ b/src/main/java/pulse/search/direction/pso/FIPSMover.java @@ -20,26 +20,26 @@ public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleStat var current = p.getCurrentState(); var curPos = current.getPosition(); var curPosV = curPos.toVector(); - + final int n = curPos.dimension(); final double nLength = (double) neighbours.length; - Vector nsum = new Vector(n); + Vector nsum = new Vector(n); for (var neighbour : neighbours) { var nBestPos = neighbour.getBestState().getPosition(); //best position ever achieved so far by the neighbour - nsum = nsum.sum(Vector.random(n, 0.0, phi/nLength) + nsum = nsum.sum(Vector.random(n, 0.0, phi / nLength) .multComponents(nBestPos.toVector().subtract(curPosV)) ); } var newVelocity = (current.getVelocity().toVector().sum(nsum)).multiply(chi); var newPosition = curPosV.sum(newVelocity); - + return new ParticleState( new ParameterVector(curPos, newPosition), new ParameterVector(curPos, newVelocity)); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/pso/Mover.java b/src/main/java/pulse/search/direction/pso/Mover.java index d1db1d0..1d7ecc2 100644 --- a/src/main/java/pulse/search/direction/pso/Mover.java +++ b/src/main/java/pulse/search/direction/pso/Mover.java @@ -4,4 +4,4 @@ public interface Mover { public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/pso/ParticleState.java b/src/main/java/pulse/search/direction/pso/ParticleState.java index 68f68c1..01985e4 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleState.java +++ b/src/main/java/pulse/search/direction/pso/ParticleState.java @@ -15,7 +15,7 @@ public ParticleState(ParameterVector cur) { //set initial velocity to zero velocity.setValues(new Vector(cur.dimension())); - + this.fitness = Double.MAX_VALUE; } @@ -41,7 +41,7 @@ public final void randomise(ParameterVector pos) { double max = p.getBounds().getMaximum(); return min + Math.random() * (max - min); }).toArray(); - + Vector randomVector = new Vector(randomValues); position.setValues(randomVector); } diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java index 323dfab..f8cbf29 100644 --- a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -19,12 +19,12 @@ public ParticleSwarmOptimiser() { protected void moveParticles() { var topology = swarmState.getNeighborhoodTopology(); for (var p : swarmState.getParticles()) { - p.adopt(mover.attemptMove(p, - topology.neighbours(p, swarmState), + p.adopt(mover.attemptMove(p, + topology.neighbours(p, swarmState), swarmState.getBestSoFar())); var data = p.getCurrentState().getPosition().toVector().getData(); StringBuilder sb = new StringBuilder().append(p.getId()).append(" "); - for(var d : data) { + for (var d : data) { sb.append(d).append(" "); } System.err.println(sb.toString()); @@ -63,7 +63,7 @@ public IterativeState initState(GeneralTask t) { swarmState.create(); return swarmState; } - + //TODO @Override public boolean compatibleWith(OptimiserStatistic os) { diff --git a/src/main/java/pulse/search/direction/pso/StaticTopologies.java b/src/main/java/pulse/search/direction/pso/StaticTopologies.java index 8fa5c24..c466e8f 100644 --- a/src/main/java/pulse/search/direction/pso/StaticTopologies.java +++ b/src/main/java/pulse/search/direction/pso/StaticTopologies.java @@ -32,7 +32,7 @@ public class StaticTopologies { final int latticeParameter = (int) Math.sqrt(ps.length); - final int row = i / latticeParameter; + final int row = i / latticeParameter; final int column = i - row * latticeParameter; final int above = column + (row > 0 diff --git a/src/main/java/pulse/search/direction/pso/SwarmState.java b/src/main/java/pulse/search/direction/pso/SwarmState.java index 7baa955..df5b704 100644 --- a/src/main/java/pulse/search/direction/pso/SwarmState.java +++ b/src/main/java/pulse/search/direction/pso/SwarmState.java @@ -64,16 +64,16 @@ public void bestSoFar() { } } - + //determine the current best - ParticleState curBest = particles[bestIndex].getCurrentState(); - + ParticleState curBest = particles[bestIndex].getCurrentState(); + //is curBest the best so far? - if(bestSoFar == null || curBest.isBetterThan(bestSoFar) ) { + if (bestSoFar == null || curBest.isBetterThan(bestSoFar)) { this.bestSoFar = curBest; this.bestSoFarIndex = bestIndex; } - + } public NeighbourhoodTopology getNeighborhoodTopology() { @@ -113,4 +113,4 @@ public void setBestSoFarIndex(int bestSoFarIndex) { this.bestSoFarIndex = bestSoFarIndex; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java index 4c9f1be..85eab13 100644 --- a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java +++ b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java @@ -18,6 +18,11 @@ */ public class GoldenSectionOptimiser extends LinearOptimiser { + /** + * + */ + private static final long serialVersionUID = -369106060533186038L; + /** * The golden section φ, which is approximately equal to 0.618033989. */ diff --git a/src/main/java/pulse/search/linear/WolfeOptimiser.java b/src/main/java/pulse/search/linear/WolfeOptimiser.java index af3a2cd..d540ba3 100644 --- a/src/main/java/pulse/search/linear/WolfeOptimiser.java +++ b/src/main/java/pulse/search/linear/WolfeOptimiser.java @@ -25,7 +25,12 @@ * page */ public class WolfeOptimiser extends LinearOptimiser { - + + /** + * + */ + private static final long serialVersionUID = 5200832276052099700L; + private static WolfeOptimiser instance = new WolfeOptimiser(); /** @@ -38,7 +43,7 @@ public class WolfeOptimiser extends LinearOptimiser { * gradient projection, equal to {@value C2}. */ public final static double C2 = 0.8; - + private WolfeOptimiser() { super(); } @@ -66,34 +71,34 @@ private WolfeOptimiser() { */ @Override public double linearStep(GeneralTask task) throws SolverException { - + GradientGuidedPath p = (GradientGuidedPath) task.getIterativeState(); - + final Vector direction = p.getDirection(); final Vector g1 = p.getGradient(); - + final double G1P = g1.dot(direction); final double G1P_ABS = abs(G1P); - + var params = task.searchVector(); var vParams = params.toVector(); Segment segment = domain(params, direction); - + double cost1 = task.objectiveFunction(); - + double randomConfinedValue = 0; double g2p; - + var optimiser = (GradientBasedOptimiser) PathOptimiser.getInstance(); - + for (double initialLength = segment.length(); segment.length() / initialLength > searchResolution;) { - + randomConfinedValue = segment.randomValue(); - + final var newParams = vParams.sum(direction.multiply(randomConfinedValue)); - + task.assign(new ParameterVector(params, newParams)); - + final double cost2 = task.objectiveFunction(); /** @@ -105,7 +110,7 @@ public double linearStep(GeneralTask task) throws SolverException { segment.setMaximum(randomConfinedValue); continue; } - + final var g2 = optimiser.gradient(task); g2p = g2.dot(direction); @@ -121,16 +126,16 @@ public double linearStep(GeneralTask task) throws SolverException { * if( g2p >= C2*G1P ) break; */ segment.setMinimum(randomConfinedValue); - + } - + task.assign(params); p.setGradient(g1); - + return randomConfinedValue; - + } - + @Override public String toString() { return Messages.getString("WolfeSolver.Descriptor"); //$NON-NLS-1$ @@ -145,5 +150,5 @@ public String toString() { public static WolfeOptimiser getInstance() { return instance; } - + } diff --git a/src/main/java/pulse/search/statistics/AICStatistic.java b/src/main/java/pulse/search/statistics/AICStatistic.java index 3279461..5e04b38 100644 --- a/src/main/java/pulse/search/statistics/AICStatistic.java +++ b/src/main/java/pulse/search/statistics/AICStatistic.java @@ -7,6 +7,8 @@ */ public class AICStatistic extends ModelSelectionCriterion { + private static final long serialVersionUID = 8549601688520099629L; + public AICStatistic(OptimiserStatistic os) { super(os); } diff --git a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java index 7113c14..37b2561 100644 --- a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java +++ b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java @@ -4,7 +4,6 @@ import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; import pulse.search.GeneralTask; - /** * A statistical optimality criterion relying on absolute deviations or the L1 * norm condition. Similar to the least squares technique, it attempts to find a @@ -14,6 +13,8 @@ */ public class AbsoluteDeviations extends OptimiserStatistic { + private static final long serialVersionUID = 3385019714627583467L; + public AbsoluteDeviations() { super(); } @@ -25,6 +26,7 @@ public AbsoluteDeviations(AbsoluteDeviations another) { /** * Calculates the L1 norm statistic, which simply sums up the absolute * values of residuals. + * * @param t */ @Override diff --git a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java index be69593..84d5104 100644 --- a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java +++ b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java @@ -16,13 +16,16 @@ */ public class AndersonDarlingTest extends NormalityTest { + private static final long serialVersionUID = -7471878404063688512L; + /** * This uses the SSJ statistical library to calculate the Anderson-Darling * test with the input parameters formed by the {@code task} residuals and a * normal distribution with zero mean and variance equal to the residuals * variance. + * * @param task - * @return + * @return */ @Override public boolean test(GeneralTask task) { @@ -33,7 +36,7 @@ public boolean test(GeneralTask task) { var testResult = GofStat.andersonDarling(residuals, nd); this.setStatistic(derive(TEST_STATISTIC, testResult[0])); - + //compare the p-value and the significance return testResult[1] > significance; } diff --git a/src/main/java/pulse/search/statistics/BICStatistic.java b/src/main/java/pulse/search/statistics/BICStatistic.java index 72409f0..c0d09aa 100644 --- a/src/main/java/pulse/search/statistics/BICStatistic.java +++ b/src/main/java/pulse/search/statistics/BICStatistic.java @@ -11,6 +11,8 @@ */ public class BICStatistic extends ModelSelectionCriterion { + private static final long serialVersionUID = 737642724262758403L; + public BICStatistic(ModelSelectionCriterion another) { super(another); } diff --git a/src/main/java/pulse/search/statistics/CorrelationTest.java b/src/main/java/pulse/search/statistics/CorrelationTest.java index 670d7e5..ca7c942 100644 --- a/src/main/java/pulse/search/statistics/CorrelationTest.java +++ b/src/main/java/pulse/search/statistics/CorrelationTest.java @@ -22,15 +22,15 @@ public abstract class CorrelationTest extends PropertyHolder implements Reflexiv static { instanceDescriptor.setSelectedDescriptor(EmptyCorrelationTest.class.getSimpleName()); } - + public CorrelationTest() { //intentionally blank } public static CorrelationTest init() { - return instanceDescriptor.newInstance(CorrelationTest.class); + return instanceDescriptor.newInstance(CorrelationTest.class); } - + public final static InstanceDescriptor getTestDescriptor() { return instanceDescriptor; } diff --git a/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java b/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java index ba6195f..c69dbb3 100644 --- a/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java +++ b/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java @@ -2,6 +2,8 @@ public class EmptyCorrelationTest extends CorrelationTest { + private static final long serialVersionUID = -2462666081516562018L; + @Override public double evaluate(double[] x, double[] y) { return 0; diff --git a/src/main/java/pulse/search/statistics/EmptyTest.java b/src/main/java/pulse/search/statistics/EmptyTest.java index 573280c..4434f83 100644 --- a/src/main/java/pulse/search/statistics/EmptyTest.java +++ b/src/main/java/pulse/search/statistics/EmptyTest.java @@ -4,6 +4,8 @@ public class EmptyTest extends NormalityTest { + private static final long serialVersionUID = 5919796302195242667L; + /** * Always returns true */ diff --git a/src/main/java/pulse/search/statistics/FTest.java b/src/main/java/pulse/search/statistics/FTest.java index 4723f5f..4d9da69 100644 --- a/src/main/java/pulse/search/statistics/FTest.java +++ b/src/main/java/pulse/search/statistics/FTest.java @@ -137,4 +137,4 @@ public static Calculation findNested(Calculation a, Calculation b) { return aParams > bParams ? b : a; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/KSTest.java b/src/main/java/pulse/search/statistics/KSTest.java index 7d5a854..70350d3 100644 --- a/src/main/java/pulse/search/statistics/KSTest.java +++ b/src/main/java/pulse/search/statistics/KSTest.java @@ -21,10 +21,10 @@ public class KSTest extends NormalityTest { @Override public boolean test(GeneralTask task) { evaluate(task); - - this.setStatistic(derive(TEST_STATISTIC, - TestUtils.kolmogorovSmirnovStatistic(nd, residuals))); - return !TestUtils.kolmogorovSmirnovTest(nd, residuals, this.significance); + + this.setStatistic(derive(TEST_STATISTIC, + TestUtils.kolmogorovSmirnovStatistic(nd, residuals))); + return !TestUtils.kolmogorovSmirnovTest(nd, residuals, this.significance); } @Override diff --git a/src/main/java/pulse/search/statistics/NormalityTest.java b/src/main/java/pulse/search/statistics/NormalityTest.java index f383a83..498a7f0 100644 --- a/src/main/java/pulse/search/statistics/NormalityTest.java +++ b/src/main/java/pulse/search/statistics/NormalityTest.java @@ -20,9 +20,10 @@ * residuals. As this is the pre-requisite for optimisers based on the ordinary * least-square statistic, the normality test can also be used to estimate if a * fit 'failed' or 'succeeded' in describing the data. - * + * * The test consists in testing the relation statistic < critValue, - * where the critical value is determined based on a given level of significance. + * where the critical value is determined based on a given level of + * significance. * */ public abstract class NormalityTest extends ResidualStatistic { diff --git a/src/main/java/pulse/search/statistics/PearsonCorrelation.java b/src/main/java/pulse/search/statistics/PearsonCorrelation.java index 401413c..50ffe90 100644 --- a/src/main/java/pulse/search/statistics/PearsonCorrelation.java +++ b/src/main/java/pulse/search/statistics/PearsonCorrelation.java @@ -9,6 +9,8 @@ */ public class PearsonCorrelation extends CorrelationTest { + private static final long serialVersionUID = 4819197257434836120L; + @Override public double evaluate(double[] x, double[] y) { return (new PearsonsCorrelation()).correlation(x, y); diff --git a/src/main/java/pulse/search/statistics/RSquaredTest.java b/src/main/java/pulse/search/statistics/RSquaredTest.java index 0feceed..807c39d 100644 --- a/src/main/java/pulse/search/statistics/RSquaredTest.java +++ b/src/main/java/pulse/search/statistics/RSquaredTest.java @@ -16,6 +16,7 @@ */ public class RSquaredTest extends NormalityTest { + private static final long serialVersionUID = -2022982190434832373L; private SumOfSquares sos; private static NumericProperty signifiance = derive(SIGNIFICANCE, 0.2); @@ -55,13 +56,13 @@ public void evaluate(GeneralTask t) { final double mean = mean(yr); double TSS = 0; int size = yr.size(); - + for (int i = 0; i < size; i++) { TSS += pow(yr.get(i) - mean, 2); } TSS /= size; - + setStatistic(derive(TEST_STATISTIC, (1. - (double) sos.getStatistic().getValue() / TSS))); } diff --git a/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java b/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java index af8710f..68943a7 100644 --- a/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java +++ b/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java @@ -11,8 +11,9 @@ */ public class RangePenalisedLeastSquares extends SumOfSquares { + private static final long serialVersionUID = 4068238957339821770L; private double lambda = 0.1; - + public RangePenalisedLeastSquares() { super(); } @@ -43,7 +44,7 @@ public void evaluate(GeneralTask t) { var x = t.getInput().getX(); double partialRange = t.getInput().bounds().length(); double fullRange = x.get(x.size() - 1) - x.get(IndexRange.closestLeft(0.0, x)); - final double statistic = ssr + lambda * (fullRange - partialRange)/fullRange; + final double statistic = ssr + lambda * (fullRange - partialRange) / fullRange; setStatistic(derive(OPTIMISER_STATISTIC, statistic)); } @@ -51,7 +52,7 @@ public void evaluate(GeneralTask t) { public String getDescriptor() { return "Range-Penalised Least Squares"; } - + @Override public OptimiserStatistic copy() { return new RangePenalisedLeastSquares(this); diff --git a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java index 6fcd893..06bf1ff 100644 --- a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java +++ b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java @@ -13,8 +13,9 @@ */ public class RegularisedLeastSquares extends SumOfSquares { + private static final long serialVersionUID = -7398979361944447180L; private double lambda = 1e-4; - + public RegularisedLeastSquares() { super(); } diff --git a/src/main/java/pulse/search/statistics/ResidualStatistic.java b/src/main/java/pulse/search/statistics/ResidualStatistic.java index 5b9d3e5..7185d5a 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -102,9 +102,8 @@ public ResidualStatistic(ResidualStatistic another) { public final void calculateResiduals(DiscreteInput reference, Response estimate, int min, int max) { var y = reference.getY(); var x = reference.getX(); - + //if size has not changed, use the old list - if (ry.size() == max - min + 1) { for (int i = min; i < max; i++) { @@ -113,13 +112,10 @@ public final void calculateResiduals(DiscreteInput reference, Response estimate, } - } - - //else create a new list - + } //else create a new list else { - rx = x.subList(min, max); + rx = new ArrayList<>(x.subList(min, max)); ry.clear(); for (int i = min; i < max; i++) { @@ -131,18 +127,18 @@ public final void calculateResiduals(DiscreteInput reference, Response estimate, } } - + public void calculateResiduals(DiscreteInput reference, Response estimate) { var y = reference.getY(); var x = reference.getX(); - + var estimateRange = estimate.accessibleRange(); int min = (int) Math.max(reference.getIndexRange().getLowerBound(), - IndexRange.closestLeft(estimateRange.getMinimum(), x) ); + IndexRange.closestLeft(estimateRange.getMinimum(), x)); int max = (int) Math.min(reference.getIndexRange().getUpperBound(), - IndexRange.closestRight(estimateRange.getMaximum(), x) ); - + IndexRange.closestRight(estimateRange.getMaximum(), x)); + calculateResiduals(reference, estimate, min, max); } @@ -178,4 +174,4 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java b/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java index 8bb4507..c073c64 100644 --- a/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java +++ b/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java @@ -9,6 +9,8 @@ */ public class SpearmansCorrelationTest extends CorrelationTest { + private static final long serialVersionUID = -8027167403407629716L; + @Override public double evaluate(double[] x, double[] y) { return (new SpearmansCorrelation()).correlation(x, y); diff --git a/src/main/java/pulse/search/statistics/Statistic.java b/src/main/java/pulse/search/statistics/Statistic.java index 46a7f21..30d056e 100644 --- a/src/main/java/pulse/search/statistics/Statistic.java +++ b/src/main/java/pulse/search/statistics/Statistic.java @@ -13,5 +13,5 @@ public abstract class Statistic extends PropertyHolder implements Reflexive { public abstract void evaluate(GeneralTask t); - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/search/statistics/SumOfSquares.java b/src/main/java/pulse/search/statistics/SumOfSquares.java index f264ed8..e72e41f 100644 --- a/src/main/java/pulse/search/statistics/SumOfSquares.java +++ b/src/main/java/pulse/search/statistics/SumOfSquares.java @@ -11,6 +11,8 @@ */ public class SumOfSquares extends OptimiserStatistic { + private static final long serialVersionUID = 3959714755977689591L; + public SumOfSquares() { super(); } @@ -38,7 +40,6 @@ public SumOfSquares(SumOfSquares sos) { * @param t The task containing the reference and calculated curves * @see calculateResiduals() */ - @Override public void evaluate(GeneralTask t) { calculateResiduals(t); diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 40d857d..db6b25d 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -39,6 +39,7 @@ public class Calculation extends PropertyHolder implements Comparable, Response { + private static final long serialVersionUID = 8098141563821512602L; private Status status; public final static double RELATIVE_TIME_MARGIN = 1.01; @@ -111,7 +112,7 @@ public void clear() { public void setProblem(Problem problem, ExperimentalData curve) { this.problem = problem; problem.setParent(this); - problem.removeHeatingCurveListeners(); + problem.removeListeners(); addProblemListeners(problem, curve); } @@ -206,7 +207,7 @@ public boolean setStatus(Status status) { if (this.getStatus() != status) { changeStatus = true; - + //current status is given by ** this.status ** //new status is the ** argument ** of this method switch (this.status) { diff --git a/src/main/java/pulse/tasks/Identifier.java b/src/main/java/pulse/tasks/Identifier.java index f640fbd..2a14adc 100644 --- a/src/main/java/pulse/tasks/Identifier.java +++ b/src/main/java/pulse/tasks/Identifier.java @@ -15,6 +15,7 @@ */ public class Identifier extends NumericProperty { + private static final long serialVersionUID = 3751417739136256453L; private static int lastId = -1; private Identifier(int value, boolean addToList) { @@ -57,5 +58,5 @@ public static Identifier externalIdentifier(int id) { public String toString() { return Messages.getString("Identifier.Tag") + " " + getValue(); } - -} + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 9cf3e22..ed814ae 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -29,6 +29,7 @@ import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; +import pulse.input.listeners.ExternalDatasetListener; import pulse.math.ParameterIdentifier; import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; @@ -62,23 +63,27 @@ */ public class SearchTask extends GeneralTask { + /** + * + */ + private static final long serialVersionUID = -6763815749875446528L; private Calculation current; private List stored; private ExperimentalData curve; private Log log; - private final CorrelationBuffer correlationBuffer; + private CorrelationBuffer correlationBuffer; private CorrelationTest correlationTest; private NormalityTest normalityTest; - private final Identifier identifier; + private Identifier identifier; /** * If {@code SearchTask} finishes, and its R2 value is * lower than this constant, the result will be considered * {@code AMBIGUOUS}. */ - private final List listeners; - private final List statusChangeListeners; + private transient List listeners; + private transient List statusChangeListeners; /** *

@@ -92,27 +97,29 @@ public class SearchTask extends GeneralTask { * @param curve the {@code ExperimentalData} */ public SearchTask(ExperimentalData curve) { - super(); - this.statusChangeListeners = new CopyOnWriteArrayList<>(); - this.listeners = new CopyOnWriteArrayList<>(); current = new Calculation(this); this.identifier = new Identifier(); this.curve = curve; curve.setParent(this); correlationBuffer = new CorrelationBuffer(); + initListeners(); clear(); - addListeners(); } - private void addListeners() { - InterpolationDataset.addListener(e -> { - if (current.getProblem() != null) { - var p = current.getProblem().getProperties(); - if (p.areThermalPropertiesLoaded()) { - p.useTheoreticalEstimates(curve); - } + private void updateThermalProperties() { + if (current.getProblem() != null) { + var p = current.getProblem().getProperties(); + if (p.areThermalPropertiesLoaded()) { + p.useTheoreticalEstimates(curve); } - }); + } + } + + @Override + public void initListeners() { + super.initListeners(); + this.statusChangeListeners = new CopyOnWriteArrayList<>(); + this.listeners = new CopyOnWriteArrayList<>(); /** * Sets the difference scheme's time limit to the upper bound of the @@ -237,7 +244,7 @@ public void checkProblems() { } setStatus(s); - + } } @@ -278,7 +285,7 @@ public void run() { } current.getProblem().parameterListChanged(); // get updated list of parameters - + super.run(); } @@ -353,9 +360,12 @@ public Calculation findBestCalculation() { } public void switchToBestModel() { - this.switchTo(findBestCalculation()); - var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.BEST_MODEL_SELECTED, this.getIdentifier()); - fireRepositoryEvent(e); + var best = findBestCalculation(); + if (current != best && best != null) { + this.switchTo(best); + var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.BEST_MODEL_SELECTED, this.getIdentifier()); + fireRepositoryEvent(e); + } } private void fireRepositoryEvent(TaskRepositoryEvent e) { diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index 397971d..f3394fb 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -3,7 +3,6 @@ import static java.time.LocalDateTime.now; import static java.time.format.DateTimeFormatter.ISO_WEEK_DATE; import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toList; import static pulse.io.readers.ReaderManager.curveReaders; import static pulse.io.readers.ReaderManager.read; import static pulse.tasks.listeners.TaskRepositoryEvent.State.SHUTDOWN; @@ -18,6 +17,8 @@ import static pulse.util.Group.contents; import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -28,11 +29,19 @@ import java.util.logging.Level; import java.util.logging.Logger; import pulse.input.ExperimentalData; +import pulse.input.InterpolationDataset; import pulse.input.listeners.DataEvent; import pulse.input.listeners.DataEventType; +import pulse.input.listeners.ExternalDatasetListener; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; +import static pulse.properties.NumericPropertyKeyword.DENSITY; +import static pulse.properties.NumericPropertyKeyword.EMISSIVITY; +import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; import pulse.properties.SampleName; import pulse.search.direction.PathOptimiser; +import pulse.tasks.listeners.SessionListener; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.listeners.TaskRepositoryListener; import pulse.tasks.listeners.TaskSelectionEvent; @@ -44,7 +53,6 @@ import pulse.util.Group; import pulse.util.HierarchyListener; import pulse.util.PropertyHolder; -import pulse.util.ResourceMonitor; import pulse.util.UpwardsNavigable; /** @@ -56,40 +64,76 @@ *

* */ -public class TaskManager extends UpwardsNavigable { - - private static final TaskManager instance = new TaskManager(); +public final class TaskManager extends UpwardsNavigable { + /** + * + */ + private static final long serialVersionUID = -4255751786167667650L; private List tasks; private SearchTask selectedTask; - private boolean singleStatement = true; + private HierarchyListener statementListener; - private final List selectionListeners; - private final List taskRepositoryListeners; + private transient List selectionListeners; + private transient List taskRepositoryListeners; + private transient List externalListeners; - private final static String DEFAULT_NAME = "Measurement_" + now().format(ISO_WEEK_DATE); + private static TaskManager instance = new TaskManager(); + + private static List globalListeners = new ArrayList<>(); + + private InterpolationDataset cpDataset; + private InterpolationDataset rhoDataset; - private final HierarchyListener statementListener = e -> { + private TaskManager() { + tasks = new ArrayList<>(); + initListeners(); + } + + /** + * Creates a list of property keywords that can be derived with help of the + * loaded data. For example, if heat capacity and density data is available, + * the returned list will contain {@code CONDUCTIVITY}. + * + * @return + */ + public List derivableProperties() { + var list = new ArrayList(); + if (cpDataset != null) { + list.add(SPECIFIC_HEAT); + } + if (rhoDataset != null) { + list.add(DENSITY); + } + if (rhoDataset != null && cpDataset != null) { + list.add(CONDUCTIVITY); + list.add(EMISSIVITY); + } + return list; + } + + @Override + public void initListeners() { + super.initListeners(); + selectionListeners = new CopyOnWriteArrayList<>(); + taskRepositoryListeners = new CopyOnWriteArrayList<>(); + externalListeners = new CopyOnWriteArrayList<>(); + statementListener = e -> { - if (!(e.getSource() instanceof PropertyHolder)) { + if (!(e.getSource() instanceof PropertyHolder)) { - var task = (SearchTask) e.getPropertyHolder().specificAncestor(SearchTask.class); - for (SearchTask t : tasks) { - if (t == task) { - continue; + var task = (SearchTask) e.getPropertyHolder().specificAncestor(SearchTask.class); + for (SearchTask t : tasks) { + if (t == task) { + continue; + } + t.update(e.getProperty()); } - t.update(e.getProperty()); - } - - } - }; + } - private TaskManager() { - tasks = new ArrayList<>(); - selectionListeners = new CopyOnWriteArrayList<>(); - taskRepositoryListeners = new CopyOnWriteArrayList<>(); + }; addHierarchyListener(statementListener); } @@ -211,7 +255,7 @@ public void cancelAllTasks() { } - private void fireTaskSelected(Object source) { + public void fireTaskSelected(Object source) { var e = new TaskSelectionEvent(source); for (var l : selectionListeners) { l.onSelectionChanged(e); @@ -349,7 +393,7 @@ public void generateTask(File file) { */ public void generateTasks(List files) { requireNonNull(files, "Null list of files passed to generatesTasks(...)"); - + //this is the loader runnable submitted to the executor service Runnable loader = () -> { var pool = Executors.newSingleThreadExecutor(); @@ -463,8 +507,8 @@ public final void addTaskRepositoryListener(TaskRepositoryListener listener) { taskRepositoryListeners.add(listener); } - public TaskSelectionListener[] getSelectionListeners() { - return (TaskSelectionListener[]) selectionListeners.toArray(); + public List getSelectionListeners() { + return selectionListeners; } public void removeSelectionListeners() { @@ -500,7 +544,9 @@ public List getTaskRepositoryListeners() { @Override public String describe() { var name = getSampleName(); - return name == null || name.getValue() == null ? DEFAULT_NAME : name.toString(); + return name == null || name.getValue() == null + ? "Measurement_" + now().format(ISO_WEEK_DATE) + : name.toString(); } public void evaluate() { @@ -530,6 +576,24 @@ public Set allGrouppedContents() { public boolean isSingleStatement() { return singleStatement; } + + public static void assumeNewState(TaskManager loaded) { + TaskManager.instance = null; + TaskManager.instance = loaded; + globalListeners.stream().forEach(l -> l.onNewSessionLoaded()); + } + + public void addExternalDatasetListener(ExternalDatasetListener edl) { + this.externalListeners.add(edl); + } + + public static void addSessionListener(SessionListener sl) { + globalListeners.add(sl); + } + + public static void removeSessionListeners() { + globalListeners.clear(); + } /** * Sets the flag to isolate or inter-connects changes in all instances of @@ -547,5 +611,35 @@ public void setSingleStatement(boolean singleStatement) { this.addHierarchyListener(statementListener); } } - -} + + /* + Serialization + */ + + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + // default deserialization + ois.defaultReadObject(); + } + + public InterpolationDataset getDensityDataset() { + return rhoDataset; + } + + public InterpolationDataset getSpecificHeatDataset() { + return cpDataset; + } + + public void setDensityDataset(InterpolationDataset dataset) { + this.rhoDataset = dataset; + this.externalListeners.stream().forEach(l -> l.onDensityDataLoaded()); + evaluate(); + } + + public void setSpecificHeatDataset(InterpolationDataset dataset) { + this.cpDataset = dataset; + this.externalListeners.stream().forEach(l -> l.onSpecificHeatDataLoaded()); + evaluate(); + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/listeners/DataCollectionListener.java b/src/main/java/pulse/tasks/listeners/DataCollectionListener.java index 5bbdafa..b1f2e4a 100644 --- a/src/main/java/pulse/tasks/listeners/DataCollectionListener.java +++ b/src/main/java/pulse/tasks/listeners/DataCollectionListener.java @@ -1,8 +1,10 @@ package pulse.tasks.listeners; +import java.io.Serializable; import pulse.tasks.logs.LogEntry; -public interface DataCollectionListener { +public interface DataCollectionListener extends Serializable { public void onDataCollected(LogEntry e); + } diff --git a/src/main/java/pulse/tasks/listeners/LogEntryListener.java b/src/main/java/pulse/tasks/listeners/LogEntryListener.java index 660ce18..e2d0392 100644 --- a/src/main/java/pulse/tasks/listeners/LogEntryListener.java +++ b/src/main/java/pulse/tasks/listeners/LogEntryListener.java @@ -1,9 +1,10 @@ package pulse.tasks.listeners; +import java.io.Serializable; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; -public interface LogEntryListener { +public interface LogEntryListener extends Serializable { public void onNewEntry(LogEntry e); diff --git a/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java b/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java index 5407757..edd94e9 100644 --- a/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java +++ b/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java @@ -1,8 +1,9 @@ package pulse.tasks.listeners; +import java.io.Serializable; import pulse.tasks.processing.ResultFormat; -public class ResultFormatEvent { +public class ResultFormatEvent implements Serializable { private ResultFormat rf; diff --git a/src/main/java/pulse/tasks/listeners/ResultFormatListener.java b/src/main/java/pulse/tasks/listeners/ResultFormatListener.java index bfa0c1c..732563c 100644 --- a/src/main/java/pulse/tasks/listeners/ResultFormatListener.java +++ b/src/main/java/pulse/tasks/listeners/ResultFormatListener.java @@ -1,6 +1,8 @@ package pulse.tasks.listeners; -public interface ResultFormatListener { +import java.io.Serializable; + +public interface ResultFormatListener extends Serializable { public void resultFormatChanged(ResultFormatEvent rfe); diff --git a/src/main/java/pulse/tasks/listeners/SessionListener.java b/src/main/java/pulse/tasks/listeners/SessionListener.java new file mode 100644 index 0000000..30d1300 --- /dev/null +++ b/src/main/java/pulse/tasks/listeners/SessionListener.java @@ -0,0 +1,7 @@ +package pulse.tasks.listeners; + +public interface SessionListener { + + public void onNewSessionLoaded(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/listeners/StatusChangeListener.java b/src/main/java/pulse/tasks/listeners/StatusChangeListener.java index 61b767c..3d9ba94 100644 --- a/src/main/java/pulse/tasks/listeners/StatusChangeListener.java +++ b/src/main/java/pulse/tasks/listeners/StatusChangeListener.java @@ -1,8 +1,9 @@ package pulse.tasks.listeners; +import java.io.Serializable; import pulse.tasks.logs.StateEntry; -public interface StatusChangeListener { +public interface StatusChangeListener extends Serializable { public void onStatusChange(StateEntry e); } diff --git a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java index 0c232ab..f234b6a 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java @@ -21,7 +21,6 @@ public Identifier getId() { } public enum State { - /** * Indicates a task has been added to the repository. */ @@ -59,6 +58,10 @@ public enum State { * Indicates the task has discarded superfluous calculations. */ BEST_MODEL_SELECTED, + /** + * A new state has been loaded. + */ + NEW_STATE, /** * The repository has been shut down/ */ @@ -66,4 +69,4 @@ public enum State { } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java b/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java index d30cf03..639d989 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java @@ -1,6 +1,9 @@ package pulse.tasks.listeners; -public interface TaskRepositoryListener { +import java.io.Serializable; + +public interface TaskRepositoryListener extends Serializable { public void onTaskListChanged(TaskRepositoryEvent e); -} + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java b/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java index 89ed065..c2ce45f 100644 --- a/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java @@ -4,11 +4,6 @@ public class TaskSelectionEvent extends EventObject { - /** - * - */ - private static final long serialVersionUID = 4278832926994139917L; - public TaskSelectionEvent(Object source) { super(source); // TODO Auto-generated constructor stub diff --git a/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java b/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java index fb422b4..a4c3f1a 100644 --- a/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java +++ b/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java @@ -1,7 +1,9 @@ package pulse.tasks.listeners; -public interface TaskSelectionListener { +import java.io.Serializable; + +public interface TaskSelectionListener extends Serializable { public void onSelectionChanged(TaskSelectionEvent e); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/logs/AbstractLogger.java b/src/main/java/pulse/tasks/logs/AbstractLogger.java index f128837..966351a 100644 --- a/src/main/java/pulse/tasks/logs/AbstractLogger.java +++ b/src/main/java/pulse/tasks/logs/AbstractLogger.java @@ -1,5 +1,6 @@ package pulse.tasks.logs; +import java.io.Serializable; import java.util.concurrent.ExecutorService; import static java.util.concurrent.Executors.newSingleThreadExecutor; import javax.swing.JComponent; @@ -7,9 +8,9 @@ import static pulse.tasks.logs.Status.DONE; import pulse.util.Descriptive; -public abstract class AbstractLogger implements Descriptive { +public abstract class AbstractLogger implements Descriptive, Serializable { - private final ExecutorService updateExecutor; + private ExecutorService updateExecutor; public AbstractLogger() { updateExecutor = newSingleThreadExecutor(); @@ -27,7 +28,7 @@ public synchronized void update() { if (log.isStarted()) { post(log.lastEntry()); } - + } public ExecutorService getUpdateExecutor() { diff --git a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java index f6785bd..f87f078 100644 --- a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java +++ b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java @@ -35,12 +35,14 @@ public String toString() { for (ImmutablePair key : map.keySet()) { sb.append("
"); sb.append(def(key.getFirst().getKeyword()).getAbbreviation(false)); - if(key.getFirst().getIndex() > 0) + if (key.getFirst().getIndex() > 0) { sb.append(" - ").append(key.getFirst().getIndex()); + } sb.append(""); sb.append(def(key.getSecond().getKeyword()).getAbbreviation(false)); - if(key.getSecond().getIndex() > 0) + if (key.getSecond().getIndex() > 0) { sb.append(" - ").append(key.getSecond().getIndex()); + } sb.append(""); if (test.compareToThreshold(map.get(key))) { sb.append(""); diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 94b8774..4553272 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -22,6 +22,7 @@ */ public class DataLogEntry extends LogEntry { + private static final long serialVersionUID = -8995240410369870205L; private List entry; /** @@ -127,4 +128,4 @@ public String toString() { } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/logs/Details.java b/src/main/java/pulse/tasks/logs/Details.java index acaf6df..af2854e 100644 --- a/src/main/java/pulse/tasks/logs/Details.java +++ b/src/main/java/pulse/tasks/logs/Details.java @@ -42,22 +42,17 @@ public enum Details { PARAMETER_VALUES_NOT_SENSIBLE, MAX_ITERATIONS_REACHED, ABNORMAL_DISTRIBUTION_OF_RESIDUALS, - /** - * Indicates that the result table had not been updated, as the selected - * model produced results worse than expected by the model selection criterion. + * Indicates that the result table had not been updated, as the selected + * model produced results worse than expected by the model selection + * criterion. */ - CALCULATION_RESULTS_WORSE_THAN_PREVIOUSLY_OBTAINED, - - /** - * Indicates that the result table had been updated, as the current - * model selection criterion showed better result than already present. + * Indicates that the result table had been updated, as the current model + * selection criterion showed better result than already present. */ - BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED, - SOLVER_ERROR; @Override diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index b4c7436..acdcde6 100644 --- a/src/main/java/pulse/tasks/logs/Log.java +++ b/src/main/java/pulse/tasks/logs/Log.java @@ -11,6 +11,8 @@ import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.LogEntryListener; +import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.tasks.listeners.TaskRepositoryEvent.State; import pulse.ui.Messages; import pulse.util.Group; @@ -21,13 +23,15 @@ */ public class Log extends Group { + private static final long serialVersionUID = 420096365502122145L; private List logEntries; private LocalTime start; private LocalTime end; - private final Identifier id; - private final List listeners; - private static boolean graphical = true; + private Identifier id; private boolean finished; + private transient List listeners; + + private static boolean graphical = true; /** * Creates a {@code Log} for this {@code task} that will automatically store @@ -44,8 +48,37 @@ public Log(SearchTask task) { id = task.getIdentifier(); this.logEntries = new CopyOnWriteArrayList<>(); + initListeners(); + } + + @Override + public void initListeners() { + super.initListeners(); listeners = new CopyOnWriteArrayList<>(); + var instance = TaskManager.getManagerInstance(); + var existingTask = instance.getTask(id); + + if (existingTask != null) { + //task already exists - add listeners nwo + doAddListeners(existingTask); + } else { + //wait until task is added into repository + instance.addTaskRepositoryListener(event -> { + + if (event.getState() == State.TASK_ADDED && event.getId().equals(id)) { + + var task = TaskManager.getManagerInstance().getTask(id); + doAddListeners(task); + } + } + ); + + } + + } + + private void doAddListeners(SearchTask task) { task.addTaskListener(le -> { /** @@ -60,24 +93,23 @@ public Log(SearchTask task) { task.addStatusChangeListener((StateEntry e) -> { logEntries.add(e); - + if (e.getStatus() == Status.IN_PROGRESS) { start = e.getTime(); end = null; } else { end = e.getTime(); } - + notifyListeners(e); - + if (e.getState() == Status.DONE) { logFinished(); } } /** * Do these actions every time the task status has changed. - */ + */ ); - } private void logFinished() { @@ -112,7 +144,7 @@ public final Identifier getIdentifier() { public boolean isStarted() { return logEntries.size() > 0; } - + public boolean isFinished() { return finished; } @@ -195,16 +227,18 @@ public static boolean isGraphicalLog() { public static void setGraphicalLog(boolean verbose) { Log.graphical = verbose; } - + /** - * Time taken where the first array element contains seconds [0] and the second contains milliseconds [1]. - * @return an array of long values that sum um to the time taken to process a task + * Time taken where the first array element contains seconds [0] and the + * second contains milliseconds [1]. + * + * @return an array of long values that sum um to the time taken to process + * a task */ - public long[] timeTaken() { var seconds = SECONDS.between(getStart(), getEnd()); var ms = MILLIS.between(getStart(), getEnd()) - 1000L * seconds; - return new long[] {seconds, ms}; + return new long[]{seconds, ms}; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/logs/LogEntry.java b/src/main/java/pulse/tasks/logs/LogEntry.java index f311fd1..6a6d644 100644 --- a/src/main/java/pulse/tasks/logs/LogEntry.java +++ b/src/main/java/pulse/tasks/logs/LogEntry.java @@ -1,9 +1,9 @@ package pulse.tasks.logs; +import java.io.Serializable; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Objects; -import pulse.Response; import pulse.tasks.Identifier; import pulse.tasks.SearchTask; @@ -18,12 +18,13 @@ *

* */ -public class LogEntry { +public class LogEntry implements Serializable { + private static final long serialVersionUID = -6797821686964650045L; private final Identifier identifier; private final LocalTime time; private final LogEntry previous; - + /** *

* Creates a {@code LogEntry} from this {@code SearchTask}. The data of the @@ -37,14 +38,13 @@ public LogEntry(SearchTask t) { time = LocalDateTime.now().toLocalTime(); identifier = t.getIdentifier(); var list = t.getLog().getLogEntries(); - if(list != null && !list.isEmpty()) { + if (list != null && !list.isEmpty()) { previous = list.get(list.size() - 1); - } - else { + } else { previous = null; } } - + public LogEntry getPreviousEntry() { return previous; } @@ -57,4 +57,4 @@ public LocalTime getTime() { return time; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/logs/StateEntry.java b/src/main/java/pulse/tasks/logs/StateEntry.java index 17333a6..e2ea9a5 100644 --- a/src/main/java/pulse/tasks/logs/StateEntry.java +++ b/src/main/java/pulse/tasks/logs/StateEntry.java @@ -8,6 +8,7 @@ public class StateEntry extends LogEntry { + private static final long serialVersionUID = 8380229394939453079L; private Status status; public StateEntry(SearchTask task, Status status) { @@ -41,7 +42,7 @@ public String toString() { if (status.getDetails() != NONE) { sb.append(" due to " + status.getDetails() + ""); } - if(status.getDetailedMessage().length() > 0) { + if (status.getDetailedMessage().length() > 0) { sb.append(" Details: "); sb.append(status.getDetailedMessage()); } diff --git a/src/main/java/pulse/tasks/logs/Status.java b/src/main/java/pulse/tasks/logs/Status.java index e2d7017..d479bc2 100644 --- a/src/main/java/pulse/tasks/logs/Status.java +++ b/src/main/java/pulse/tasks/logs/Status.java @@ -37,13 +37,10 @@ public enum Status { * Termination requested. */ AWAITING_TERMINATION(Color.DARK_GRAY), - /** * Task terminated */ - TERMINATED(Color.DARK_GRAY), - /** * Task has been queued and is waiting to be executed. */ @@ -82,11 +79,11 @@ public Details getDetails() { public void setDetails(Details details) { this.details = details; } - + public String getDetailedMessage() { return message; } - + public void setDetailedMessage(String str) { this.message = str; } @@ -133,20 +130,19 @@ public String getMessage() { } return sb.toString(); } - + public static Status troubleshoot(SolverException e1) { Objects.requireNonNull(e1, "Solver exception cannot be null when calling troubleshoot!"); Status status = null; - if(e1.getType() != SolverExceptionType.OPTIMISATION_TIMEOUT) { + if (e1.getType() != SolverExceptionType.OPTIMISATION_TIMEOUT) { status = Status.FAILED; status.setDetails(Details.SOLVER_ERROR); - status.setDetailedMessage(e1.getMessage()); - } - else { + status.setDetailedMessage(e1.getMessage()); + } else { status = Status.TIMEOUT; status.setDetails(Details.MAX_ITERATIONS_REACHED); } return status; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/processing/AverageResult.java b/src/main/java/pulse/tasks/processing/AverageResult.java index f5cdf85..f6186b0 100644 --- a/src/main/java/pulse/tasks/processing/AverageResult.java +++ b/src/main/java/pulse/tasks/processing/AverageResult.java @@ -18,6 +18,8 @@ */ public class AverageResult extends AbstractResult { + private static final long serialVersionUID = 5279249996318155238L; + private final List results; public final static int SIGNIFICANT_FIGURES = 2; @@ -28,11 +30,10 @@ public class AverageResult extends AbstractResult { *

* It will also use the {@code resultFormat}. A method will be invoked to: * (a) calculate the mean values of the list of {@code NumericProperty} - * according to the {@code resultFormat}; (b) calculate the statistical error; - * (c) create a {@code BigDecimal} representation - * of the values and the errors, so that only {@value SIGNIFICANT_FIGURES} - * significant figures are left for consistency between the {@code value} - * and the {@code error}. + * according to the {@code resultFormat}; (b) calculate the statistical + * error; (c) create a {@code BigDecimal} representation of the values and + * the errors, so that only {@value SIGNIFICANT_FIGURES} significant figures + * are left for consistency between the {@code value} and the {@code error}. *

* * @param res a list of {@code AbstractResult}s that are going to be @@ -72,16 +73,16 @@ private void calculate() { if (!Double.isFinite(err[j])) { p = derive(key, av[j]); // ignore error as the value is not finite - } else if(NumericProperties.def(key).getValue() instanceof Double) { - var stdBig = new BigDecimal(err[j]); - var avBig = new BigDecimal(av[j]); + } else if (NumericProperties.def(key).getValue() instanceof Double) { + var stdBig = new BigDecimal(err[j]); + var avBig = new BigDecimal(av[j]); var error = stdBig.setScale( SIGNIFICANT_FIGURES - stdBig.precision() + stdBig.scale(), RoundingMode.HALF_UP); - var mean = stdBig.precision() > 1 ? - avBig.setScale(error.scale(), RoundingMode.CEILING) + var mean = stdBig.precision() > 1 + ? avBig.setScale(error.scale(), RoundingMode.CEILING) : avBig; p = derive(key, mean.doubleValue()); @@ -89,8 +90,8 @@ private void calculate() { } else { //if integer - p = derive(key, (int)Math.round( av[j] ) ); - p.setError((int)Math.round( err[j] ) ); + p = derive(key, (int) Math.round(av[j])); + p.setError((int) Math.round(err[j])); } addProperty(p); diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index 90ef5bd..b257e59 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -29,6 +29,10 @@ */ public class Buffer extends PropertyHolder { + /** + * + */ + private static final long serialVersionUID = 3613745885879508057L; private ParameterVector[] data; private double[] statistic; private static int size = (int) def(BUFFER_SIZE).getValue(); diff --git a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java index fc0c0f7..9a79788 100644 --- a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java +++ b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java @@ -1,5 +1,6 @@ package pulse.tasks.processing; +import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -18,14 +19,15 @@ import pulse.util.ImmutableDataEntry; import pulse.util.ImmutablePair; -public class CorrelationBuffer { +public class CorrelationBuffer implements Serializable { - private final List params; + private static final long serialVersionUID = 1672281370094463238L; + private List params; private static final Set> excludePairList; private static final Set excludeSingleList; private final static double DEFAULT_THRESHOLD = 1E-3; - + static { excludePairList = new HashSet<>(); excludeSingleList = new HashSet<>(); @@ -48,27 +50,28 @@ public void inflate(SearchTask t) { public void clear() { params.clear(); } - + /** * Truncates the buffer by excluding nearly-converged results. */ - private void truncate(double threshold) { int i = 0; int size = params.size(); - final double thresholdSq = threshold*threshold; - - for(i = 0; i < size - 1; i = i + 2) { - + final double thresholdSq = threshold * threshold; + + for (i = 0; i < size - 1; i = i + 2) { + Vector vParams = params.get(i).toVector(); Vector vPlusOneParams = params.get(i + 1).toVector(); Vector vDiff = vPlusOneParams.subtract(vParams); - if(vDiff.lengthSq()/vParams.lengthSq() < thresholdSq) - break; + if (vDiff.lengthSq() / vParams.lengthSq() < thresholdSq) { + break; + } + } + + for (int j = size - 1; j > i; j--) { + params.remove(j); } - - for(int j = size - 1; j > i; j--) - params.remove(j); } public Map, Double> evaluate(CorrelationTest t) { @@ -81,12 +84,12 @@ public Map, Double> evaluate(CorrelationTest } truncate(DEFAULT_THRESHOLD); - + List indices = params.get(0).getParameters().stream() .map(ps -> ps.getIdentifier()).collect(Collectors.toList()); Map map = indices.stream() .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble( - v -> v.getParameterValue(index.getKeyword(), index.getIndex())).toArray())) + v -> v.getParameterValue(index.getKeyword(), index.getIndex())).toArray())) .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); int indicesSize = indices.size(); @@ -96,26 +99,26 @@ public Map, Double> evaluate(CorrelationTest for (int i = 0; i < indicesSize; i++) { var iKey = indices.get(i).getKeyword(); - - if (!excludeSingleList.contains(iKey)) { - + + if (!excludeSingleList.contains(iKey)) { + for (int j = i + 1; j < indicesSize; j++) { - + var jKey = indices.get(j).getKeyword(); - + pair = new ImmutablePair<>(iKey, jKey); - - if (!excludeSingleList.contains(jKey) - && !excludePairList.contains(pair)) { - - correlationMap.put( - new ImmutablePair<>(indices.get(i), indices.get(j)), - t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); - + + if (!excludeSingleList.contains(jKey) + && !excludePairList.contains(pair)) { + + correlationMap.put( + new ImmutablePair<>(indices.get(i), indices.get(j)), + t.evaluate(map.get(indices.get(i)), map.get(indices.get(j)))); + } - + } - + } } @@ -132,7 +135,7 @@ public boolean test(CorrelationTest t) { } var values = map.values(); - + return map.values().stream().anyMatch(d -> t.compareToThreshold(d)); } diff --git a/src/main/java/pulse/tasks/processing/Result.java b/src/main/java/pulse/tasks/processing/Result.java index 626e292..083b82f 100644 --- a/src/main/java/pulse/tasks/processing/Result.java +++ b/src/main/java/pulse/tasks/processing/Result.java @@ -1,6 +1,7 @@ package pulse.tasks.processing; import pulse.tasks.Calculation; +import pulse.tasks.Identifier; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -13,6 +14,12 @@ */ public class Result extends AbstractResult { + /** + * + */ + private static final long serialVersionUID = 471531411060979791L; + private Identifier id; + /** * Creates an individual {@code Result} related to the current state of * {@code task} using the specified {@code format}. @@ -29,14 +36,19 @@ public Result(SearchTask task, ResultFormat format) throws IllegalArgumentExcept throw new IllegalArgumentException(Messages.getString("Result.NullTaskError")); } - setParent((Calculation)task.getResponse()); + id = task.getIdentifier(); + setParent((Calculation) task.getResponse()); format.getKeywords().stream().forEach(key -> addProperty(task.numericProperty(key))); - } public Result(Result r) { super(r); + id = r.getTaskIdentifier(); + } + + public Identifier getTaskIdentifier() { + return id; } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/ResultFormat.java b/src/main/java/pulse/tasks/processing/ResultFormat.java index 6611a84..3c0cd56 100644 --- a/src/main/java/pulse/tasks/processing/ResultFormat.java +++ b/src/main/java/pulse/tasks/processing/ResultFormat.java @@ -1,5 +1,6 @@ package pulse.tasks.processing; +import java.io.Serializable; import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; import static pulse.properties.NumericProperties.def; @@ -11,6 +12,7 @@ import java.util.List; import pulse.properties.NumericPropertyKeyword; +import pulse.tasks.TaskManager; import pulse.tasks.listeners.ResultFormatEvent; import pulse.tasks.listeners.ResultFormatListener; @@ -22,7 +24,12 @@ * characters. *

*/ -public class ResultFormat { +public class ResultFormat implements Serializable { + + /** + * + */ + private static final long serialVersionUID = -3155104011585735097L; private List nameMap; @@ -45,15 +52,22 @@ private ResultFormat() { private ResultFormat(List keys) { nameMap = new ArrayList<>(); - keys.forEach(key -> - nameMap.add(key) + keys.forEach(key + -> nameMap.add(key) ); + TaskManager.addSessionListener(() -> format = this); } - + public static void addResultFormatListener(ResultFormatListener rfl) { listeners.add(rfl); } + public static void removeListeners() { + if (listeners != null) { + listeners.clear(); + } + } + public static ResultFormat generateFormat(List keys) { format = new ResultFormat(keys); @@ -161,4 +175,4 @@ public boolean equals(Object o) { } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/ResultStatistics.java b/src/main/java/pulse/tasks/processing/ResultStatistics.java index 807b2f5..d6d0911 100644 --- a/src/main/java/pulse/tasks/processing/ResultStatistics.java +++ b/src/main/java/pulse/tasks/processing/ResultStatistics.java @@ -1,5 +1,6 @@ package pulse.tasks.processing; +import java.io.Serializable; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -16,14 +17,15 @@ * * @author Artem Lunev */ -class ResultStatistics { +class ResultStatistics implements Serializable { + private static final long serialVersionUID = 4617029204359661289L; private double[] av; private double[] err; /** * Confidence level of {@value CONFIDENCE_LEVEL} for error calculation using - * t-distribution quantiles. + * t-distribution quantiles. */ public final static double CONFIDENCE_LEVEL = 0.95; @@ -41,8 +43,9 @@ public ResultStatistics() { * assuming a {@value CONFIDENCE_LEVEL} confidence level, by calculating a * standard deviation for each {@code NumericPropertyKeyword} and * multiplying the result by the quantile value of the - * t-distribution. The inverse cumulative distribution function of - * Student distribution is calculated using {@code ApacheCommonsMath} library. + * t-distribution. The inverse cumulative distribution function of + * Student distribution is calculated using {@code ApacheCommonsMath} + * library. * * @param results a list of individual (or average) results to be processed */ @@ -60,23 +63,21 @@ public void process(List results) { The number of elements in the parameter list. This ASSUMES that the input list contains results with the same number of output parameters! */ - StandardDeviation sd = new StandardDeviation(true); //bias-corrected sd double sqrtn = Math.sqrt(results.size()); //calculate average values - var stats = ResultFormat.getInstance().getKeywords().stream() - .map(key -> map.get(key)) //preserve the original order of keywods - .map(c -> { - double mean = openStream(c).average().orElse(0.0); //fail-safe, in case if avg is undefined - return new ImmutablePair( - mean, //the mean value - sd.evaluate(openStream(c).toArray(), mean) //that would be the sample standard deviation - / sqrtn //however, since we are calculating the std of the MEAN, - //we need to divide the result by sqrtn - ); - }).collect(Collectors.toList()); + .map(key -> map.get(key)) //preserve the original order of keywods + .map(c -> { + double mean = openStream(c).average().orElse(0.0); //fail-safe, in case if avg is undefined + return new ImmutablePair( + mean, //the mean value + sd.evaluate(openStream(c).toArray(), mean) //that would be the sample standard deviation + / sqrtn //however, since we are calculating the std of the MEAN, + //we need to divide the result by sqrtn + ); + }).collect(Collectors.toList()); av = stats.stream().mapToDouble(pair -> pair.getFirst()).toArray(); //store mean values @@ -90,7 +91,7 @@ public void process(List results) { ).toArray(); //store errors } - + private DoubleStream openStream(List input) { return input.stream().mapToDouble(d -> d); } diff --git a/src/main/java/pulse/ui/ColorGenerator.java b/src/main/java/pulse/ui/ColorGenerator.java index 2e2388d..ac56b59 100644 --- a/src/main/java/pulse/ui/ColorGenerator.java +++ b/src/main/java/pulse/ui/ColorGenerator.java @@ -5,10 +5,9 @@ import static java.awt.Color.GREEN; import static java.awt.Color.RED; import java.util.ArrayList; -import java.util.Collections; public class ColorGenerator { - + private Color a, b, c; public ColorGenerator() { @@ -16,30 +15,30 @@ public ColorGenerator() { b = GREEN; c = BLUE; } - + public Color[] random(int number) { var list = new ArrayList(); - for(int i = 0; i < number; i++) { - list.add(sample(i/(double)(number - 1))); + for (int i = 0; i < number; i++) { + list.add(sample(i / (double) (number - 1))); } //Collections.shuffle(list); return list.toArray(new Color[list.size()]); } public Color sample(double seed) { - return seed < 0.5 ? - mix(a, b, (float) (seed*2)) - : mix(b, c,(float)((seed-0.5)*2)); + return seed < 0.5 + ? mix(a, b, (float) (seed * 2)) + : mix(b, c, (float) ((seed - 0.5) * 2)); } - + private static Color mix(Color a, Color b, float ratio) { float[] aRgb = a.getRGBComponents(null); float[] bRgb = b.getRGBComponents(null); float[] cRgb = new float[3]; - for(int i = 0; i < cRgb.length; i++) { + for (int i = 0; i < cRgb.length; i++) { cRgb[i] = aRgb[i] * (1.0f - ratio) + bRgb[i] * ratio; } return new Color(cRgb[0], cRgb[1], cRgb[2]); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index ee238ac..3744e9e 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; +import pulse.util.Serializer; /** *

@@ -51,7 +52,7 @@ private Launcher() { */ public static void main(String[] args) { new Launcher(); - + if (!LOCK.exists()) { try { @@ -59,7 +60,7 @@ public static void main(String[] args) { } catch (IOException ex) { Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, "Unable to create lock file", ex); } - + LOCK.deleteOnExit(); splashScreen(); @@ -148,7 +149,7 @@ private void createShutdownHook() { errorLog.delete(); } //delete lock explicitly on abnormal termination - if(LOCK.exists()) { + if (LOCK.exists()) { LOCK.delete(); } }; diff --git a/src/main/java/pulse/ui/components/AuxPlotter.java b/src/main/java/pulse/ui/components/AuxPlotter.java index 602a3d6..f35d34d 100644 --- a/src/main/java/pulse/ui/components/AuxPlotter.java +++ b/src/main/java/pulse/ui/components/AuxPlotter.java @@ -14,66 +14,66 @@ import org.jfree.chart.plot.XYPlot; public abstract class AuxPlotter { - + private ChartPanel chartPanel; private JFreeChart chart; private XYPlot plot; - + public AuxPlotter() { //empty } - + public AuxPlotter(String xLabel, String yLabel) { - setChart( ChartFactory.createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false) ); - - setPlot( chart.getXYPlot() ); + setChart(ChartFactory.createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false)); + + setPlot(chart.getXYPlot()); chart.removeLegend(); setFonts(); } - + public final void setFonts() { var jlabel = new JLabel(); var label = jlabel.getFont().deriveFont(20f); var ticks = jlabel.getFont().deriveFont(16f); chart.getTitle().setFont(jlabel.getFont().deriveFont(20f)); - + if (plot instanceof CombinedDomainXYPlot) { var combinedPlot = (CombinedDomainXYPlot) plot; - combinedPlot.getSubplots().stream().forEach(sp -> setFontsForPlot((XYPlot)sp, label, ticks)); + combinedPlot.getSubplots().stream().forEach(sp -> setFontsForPlot((XYPlot) sp, label, ticks)); } else { - setFontsForPlot(plot, label, ticks); + setFontsForPlot(plot, label, ticks); } - + } - + private void setFontsForPlot(XYPlot p, Font label, Font ticks) { var foreColor = UIManager.getColor("Label.foreground"); var domainAxis = p.getDomainAxis(); - Chart.setAxisFontColor(domainAxis, foreColor); + Chart.setAxisFontColor(domainAxis, foreColor); var rangeAxis = p.getRangeAxis(); Chart.setAxisFontColor(rangeAxis, foreColor); } - + public abstract void plot(T t); - + public final ChartPanel getChartPanel() { return chartPanel; } - + public final JFreeChart getChart() { return chart; } - + public final XYPlot getPlot() { return plot; } - + public final void setPlot(XYPlot plot) { this.plot = plot; plot.setBackgroundPaint(chart.getBackgroundPaint()); } - + public final void setChart(JFreeChart chart) { this.chart = chart; var color = UIManager.getLookAndFeelDefaults().getColor("TextPane.background"); @@ -81,5 +81,5 @@ public final void setChart(JFreeChart chart) { chartPanel = new ChartPanel(chart); this.plot = chart.getXYPlot(); } - -} \ No newline at end of file + +} diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index 7fe372e..0801ac8 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -19,6 +19,7 @@ import java.awt.Color; import java.awt.Font; import java.awt.event.MouseEvent; +import java.io.Serializable; import javax.swing.SwingUtilities; import javax.swing.UIManager; @@ -52,10 +53,10 @@ import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.ui.components.listeners.MouseOnMarkerListener; -public class Chart { +public class Chart implements Serializable { - private ChartPanel chartPanel; - private JFreeChart chart; + private final ChartPanel chartPanel; + private final JFreeChart chart; private XYPlot plot; private float opacity = 0.15f; @@ -151,7 +152,7 @@ private void setFonts() { setAxisFontColor(plot.getDomainAxis(), foreColor); setAxisFontColor(plot.getRangeAxis(), foreColor); } - + public static void setAxisFontColor(Axis axis, Color color) { axis.setLabelPaint(color); axis.setTickLabelPaint(color); diff --git a/src/main/java/pulse/ui/components/DataLoader.java b/src/main/java/pulse/ui/components/DataLoader.java index 34f9f97..2baa814 100644 --- a/src/main/java/pulse/ui/components/DataLoader.java +++ b/src/main/java/pulse/ui/components/DataLoader.java @@ -9,7 +9,6 @@ import java.net.URISyntaxException; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.concurrent.Executors; import javax.swing.JFileChooser; @@ -17,8 +16,6 @@ import javax.swing.filechooser.FileNameExtensionFilter; import pulse.input.ExperimentalData; -import pulse.input.InterpolationDataset; -import pulse.input.InterpolationDataset.StandartType; import pulse.io.readers.MetaFilePopulator; import pulse.io.readers.ReaderManager; import pulse.problem.laser.NumericPulse; @@ -111,7 +108,7 @@ public static void loadMetadataDialog() { e.printStackTrace(); } - var p = ( (Calculation) task.getResponse() ).getProblem(); + var p = ((Calculation) task.getResponse()).getProblem(); if (p != null) { p.retrieveData(data); } @@ -175,14 +172,25 @@ public static void loadPulseDialog() { * * @param f a {@code File} containing a property specified by the * {@code type} - * @param type the type of the loaded data * @throws IOException if file cannot be read * @see pulse.tasks.TaskManager.evaluate() */ - public static void load(StandartType type, File f) throws IOException { - Objects.requireNonNull(f); - InterpolationDataset.setDataset(read(datasetReaders(), f), type); - TaskManager.getManagerInstance().evaluate(); + public static void loadDensity(File f) throws IOException { + TaskManager.getManagerInstance().setDensityDataset(read(datasetReaders(), f)); + } + + /** + * Uses the {@code ReaderManager} to create an {@code InterpolationDataset} + * from data stored in {@code f} and updates the associated properties of + * each task. + * + * @param f a {@code File} containing a property specified by the + * {@code type} + * @throws IOException if file cannot be read + * @see pulse.tasks.TaskManager.evaluate() + */ + public static void loadSpecificHeat(File f) throws IOException { + TaskManager.getManagerInstance().setSpecificHeatDataset(read(datasetReaders(), f)); } private static List userInput(String descriptor, List extensions) { diff --git a/src/main/java/pulse/ui/components/GraphicalLogPane.java b/src/main/java/pulse/ui/components/GraphicalLogPane.java index bc69d23..1b643f5 100644 --- a/src/main/java/pulse/ui/components/GraphicalLogPane.java +++ b/src/main/java/pulse/ui/components/GraphicalLogPane.java @@ -49,10 +49,10 @@ public void post(LogEntry logEntry) { double iteration = dle.getData().stream() .filter(p -> p.getIdentifier().getKeyword() == ITERATION) .findAny().get().getApparentValue(); - + chart.changeAxis(true); chart.plot((DataLogEntry) logEntry, iteration); - + } } @@ -71,13 +71,13 @@ public void postAll() { chart.clear(); chart.changeAxis(false); chart.plot(log); - + if (task.getStatus() == DONE) { printTimeTaken(log); } } - + } } diff --git a/src/main/java/pulse/ui/components/ProblemTree.java b/src/main/java/pulse/ui/components/ProblemTree.java index e4c2e0d..0ac58e5 100644 --- a/src/main/java/pulse/ui/components/ProblemTree.java +++ b/src/main/java/pulse/ui/components/ProblemTree.java @@ -67,7 +67,7 @@ private void addListeners() { }); instance.addSelectionListener(e -> { - var current = ( (Calculation) instance.getSelectedTask().getResponse() ).getProblem(); + var current = ((Calculation) instance.getSelectedTask().getResponse()).getProblem(); // select appropriate problem type from list setSelectedProblem(current); diff --git a/src/main/java/pulse/ui/components/PropertyHolderTable.java b/src/main/java/pulse/ui/components/PropertyHolderTable.java index 05413b6..2157789 100644 --- a/src/main/java/pulse/ui/components/PropertyHolderTable.java +++ b/src/main/java/pulse/ui/components/PropertyHolderTable.java @@ -155,11 +155,6 @@ public TableCellEditor getCellEditor(int row, int column) { return new DefaultCellEditor((JComboBox) value); } - if (value instanceof Enum) { - return new DefaultCellEditor( - new JComboBox(((Enum) value).getDeclaringClass().getEnumConstants())); - } - if (value instanceof InstanceDescriptor) { return new InstanceCellEditor((InstanceDescriptor) value); } @@ -187,7 +182,7 @@ public TableCellEditor getCellEditor(int row, int column) { value, false, false, row, column), ((Flag) value).getType()); } - return getDefaultEditor(value.getClass()); + return super.getCellEditor(row, column); } diff --git a/src/main/java/pulse/ui/components/PulseChart.java b/src/main/java/pulse/ui/components/PulseChart.java index 5b4151a..b3c7d06 100644 --- a/src/main/java/pulse/ui/components/PulseChart.java +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -60,8 +60,8 @@ public void plot(Calculation c) { double startTime = (double) problem.getHeatingCurve().getTimeShift().getValue(); var pulseDataset = new XYSeriesCollection(); - - pulseDataset.addSeries(series(problem.getPulse(), c.getScheme().getGrid().getTimeStep()/20.0, + + pulseDataset.addSeries(series(problem.getPulse(), c.getScheme().getGrid().getTimeStep() / 20.0, problem.getProperties().characteristicTime(), startTime)); getPlot().setDataset(0, pulseDataset); @@ -70,15 +70,15 @@ public void plot(Calculation c) { private static XYSeries series(Pulse pulse, double dx, double timeFactor, double startTime) { var series = new XYSeries(pulse.getPulseShape().toString()); var pulseShape = pulse.getPulseShape(); - + double timeLimit = pulseShape.getPulseWidth(); - double x = startTime/timeFactor; + double x = startTime / timeFactor; series.add(TO_MILLIS * (startTime - dx * timeFactor / 100.), 0.0); - series.add(TO_MILLIS * (startTime + timeFactor*(timeLimit + dx / 100.)), 0.0); + series.add(TO_MILLIS * (startTime + timeFactor * (timeLimit + dx / 100.)), 0.0); - for (int i = 0, numPoints = (int) (timeLimit/dx); i < numPoints; i++) { - series.add(x * timeFactor * TO_MILLIS, pulseShape.evaluateAt(x - startTime/timeFactor)); + for (int i = 0, numPoints = (int) (timeLimit / dx); i < numPoints; i++) { + series.add(x * timeFactor * TO_MILLIS, pulseShape.evaluateAt(x - startTime / timeFactor)); x += dx; } diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index 31d0544..caec94a 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -30,6 +30,8 @@ import java.net.URL; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.AbstractButton; import javax.swing.ButtonGroup; @@ -39,6 +41,7 @@ import javax.swing.JMenuItem; import javax.swing.JRadioButtonMenuItem; import javax.swing.JSeparator; +import pulse.util.Serializer; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; @@ -80,25 +83,39 @@ public class PulseMainMenu extends JMenuBar { private List exitListeners; public PulseMainMenu() { - bufferDialog.setConfirmAction(() -> - Buffer.setSize(derive(BUFFER_SIZE, bufferDialog.value()))); + bufferDialog.setConfirmAction(() + -> Buffer.setSize(derive(BUFFER_SIZE, bufferDialog.value()))); initComponents(); initListeners(); assignMenuFunctions(); - addListeners(); + reset(); listeners = new ArrayList<>(); exitListeners = new ArrayList<>(); } - private void addListeners() { - getManagerInstance().addTaskRepositoryListener(event -> { + private void enableIfNeeded() { + var instance = getManagerInstance(); + boolean enabled = instance.getTaskList().size() > 0; + loadMetadataItem.setEnabled(enabled); + loadPulseItem.setEnabled(enabled); + modelSettingsItem.setEnabled(enabled); + searchSettingsItem.setEnabled(enabled); + } + + public final void reset() { + var instance = getManagerInstance(); + + instance.addTaskRepositoryListener(event -> { if (event.getState() == TASK_ADDED) { exportCurrentItem.setEnabled(true); exportAllItem.setEnabled(true); } }); + + enableIfNeeded(); + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> enableIfNeeded()); } private void initListeners() { @@ -171,7 +188,35 @@ private void initComponents() { fileMenu.add(exportCurrentItem); fileMenu.add(exportAllItem); fileMenu.add(new JSeparator()); + + var serializeItem = new JMenuItem("Save Session..."); + fileMenu.add(serializeItem); + serializeItem.addActionListener(e -> { + try { + Serializer.serialize(); + } catch (IOException ex) { + Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); + } catch (ClassNotFoundException ex) { + Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); + } + }); + var deserializeItem = new JMenuItem("Load Session..."); + + fileMenu.add(deserializeItem); + deserializeItem.addActionListener(e -> { + + try { + Serializer.deserialize(); + } catch (IOException ex) { + Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); + } catch (ClassNotFoundException ex) { + Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); + } + + }); + fileMenu.add(exitItem); + add(fileMenu); settingsMenu.add(modelSettingsItem); @@ -246,8 +291,8 @@ private JMenu initAnalysisSubmenu() { if (((AbstractButton) e.getItem()).isSelected()) { var text = ((AbstractButton) e.getItem()).getText(); setSelectedOptimiserDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> - ( (Calculation) t.getResponse() ).initOptimiser()); + getManagerInstance().getTaskList().stream().forEach(t + -> ((Calculation) t.getResponse()).initOptimiser()); } }); @@ -275,29 +320,30 @@ private JMenu initAnalysisSubmenu() { JRadioButtonMenuItem corrItem = null; var ct = CorrelationTest.init(); - + for (var corrName : allDescriptors(CorrelationTest.class)) { corrItem = new JRadioButtonMenuItem(corrName); corrItems.add(corrItem); correlationsSubMenu.add(corrItem); - - if(ct.getDescriptor().equalsIgnoreCase(corrName)) + + if (ct.getDescriptor().equalsIgnoreCase(corrName)) { corrItem.setSelected(true); - + } + corrItem.addItemListener(e -> { if (((AbstractButton) e.getItem()).isSelected()) { var text = ((AbstractButton) e.getItem()).getText(); var allTests = Reflexive.instancesOf(CorrelationTest.class); - var optionalTest = allTests.stream().filter(test -> - test.getDescriptor().equalsIgnoreCase(corrName)).findAny(); - - if(optionalTest.isPresent()) { + var optionalTest = allTests.stream().filter(test + -> test.getDescriptor().equalsIgnoreCase(corrName)).findAny(); + + if (optionalTest.isPresent()) { CorrelationTest.getTestDescriptor() .setSelectedDescriptor(optionalTest.get().getClass().getSimpleName()); getManagerInstance().getTaskList().stream().forEach(t -> t.initCorrelationTest()); } - + } }); @@ -337,20 +383,6 @@ private void assignMenuFunctions() { changeDialog.setVisible(true); }); - getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if (getManagerInstance().getTaskList().size() > 0) { - loadMetadataItem.setEnabled(true); - loadPulseItem.setEnabled(true);; - modelSettingsItem.setEnabled(true); - searchSettingsItem.setEnabled(true); - } else { - loadMetadataItem.setEnabled(false); - loadPulseItem.setEnabled(false); - modelSettingsItem.setEnabled(false); - searchSettingsItem.setEnabled(false); - } - }); - exportAllItem.setEnabled(true); exportAllItem.addActionListener(e -> { exportDialog.setLocationRelativeTo(null); @@ -370,6 +402,16 @@ private void assignMenuFunctions() { } + public void removeAllListeners() { + if (listeners != null) { + listeners.clear(); + } + if (exitListeners != null) { + exitListeners.clear(); + } + + } + public void addFrameVisibilityRequestListener(FrameVisibilityRequestListener l) { listeners.add(l); } diff --git a/src/main/java/pulse/ui/components/RangeTextFields.java b/src/main/java/pulse/ui/components/RangeTextFields.java index a146e55..dbfcd7c 100644 --- a/src/main/java/pulse/ui/components/RangeTextFields.java +++ b/src/main/java/pulse/ui/components/RangeTextFields.java @@ -3,6 +3,7 @@ import java.awt.Color; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; +import java.io.Serializable; import java.text.DecimalFormat; import java.text.ParseException; import java.util.logging.Level; @@ -19,20 +20,19 @@ import pulse.ui.components.panels.ChartToolbar; /** - * Two JFormattedTextFields used to display the range of the currently - * selected task. + * Two JFormattedTextFields used to display the range of the currently selected + * task. */ -public final class RangeTextFields { +public final class RangeTextFields implements Serializable { private JFormattedTextField lowerLimitField; private JFormattedTextField upperLimitField; /** - * Creates textfield objects, which may be accessed with getters from this instance. - * Additionally, binds listeners to all current and future tasks in order to observe - * and reflect upon the changes with the textfield. + * Creates textfield objects, which may be accessed with getters from this + * instance. Additionally, binds listeners to all current and future tasks + * in order to observe and reflect upon the changes with the textfield. */ - public RangeTextFields() { initTextFields(); @@ -53,18 +53,17 @@ public RangeTextFields() { //when a new task is selected instance.addSelectionListener((TaskSelectionEvent e) -> { var task = instance.getSelectedTask(); - var segment = ( (ExperimentalData) task.getInput() ).getRange().getSegment(); + var segment = ((ExperimentalData) task.getInput()).getRange().getSegment(); //update the textfield values lowerLimitField.setValue(segment.getMinimum()); upperLimitField.setValue(segment.getMaximum()); }); } - + /* Creates a formatter for the textfields - */ - + */ private NumberFormatter initFormatter() { var format = new DecimalFormat(); format.setMinimumFractionDigits(1); @@ -83,18 +82,19 @@ private NumberFormatter initFormatter() { formatter.setOverwriteMode(true); return formatter; } - + /** * Checks if the candidate value produced by the formatter is sensible, i.e. - * if it lies within the bounds defined in the Range class. + * if it lies within the bounds defined in the Range class. + * * @param jtf the textfield containing the candidate value as text - * @param upperBound whether the upper bound is checked ({@code false} if the lower bound is checked) + * @param upperBound whether the upper bound is checked ({@code false} if + * the lower bound is checked) * @return {@code true} if the edit may proceed */ - private static boolean isEditValid(JFormattedTextField jtf, boolean upperBound) { - Range range = ( (ExperimentalData) TaskManager.getManagerInstance().getSelectedTask() - .getInput() ).getRange(); + Range range = ((ExperimentalData) TaskManager.getManagerInstance().getSelectedTask() + .getInput()).getRange(); double candidateValue = 0.0; try { @@ -107,12 +107,11 @@ private static boolean isEditValid(JFormattedTextField jtf, boolean upperBound) return range.boundLimits(upperBound).contains(candidateValue); } - + /** - * Creates a formatter and initialised the textfields, setting up rules - * for edit validation. + * Creates a formatter and initialised the textfields, setting up rules for + * edit validation. */ - private void initTextFields() { var instance = TaskManager.getManagerInstance(); @@ -153,7 +152,7 @@ public void commitEdit() throws ParseException { } }; - + var fl = new FocusListener() { @Override public void focusGained(FocusEvent arg0) { @@ -189,11 +188,11 @@ private void updateTextfieldsFromTask(SearchTask newTask) { } }); } - + public JFormattedTextField getLowerLimitField() { return lowerLimitField; } - + public JFormattedTextField getUpperLimitField() { return upperLimitField; } diff --git a/src/main/java/pulse/ui/components/ResidualsChart.java b/src/main/java/pulse/ui/components/ResidualsChart.java index 7543707..2f31108 100644 --- a/src/main/java/pulse/ui/components/ResidualsChart.java +++ b/src/main/java/pulse/ui/components/ResidualsChart.java @@ -19,7 +19,7 @@ public ResidualsChart(String xLabel, String yLabel) { setFonts(); binCount = 32; } - + @Override public void plot(ResidualStatistic stat) { requireNonNull(stat); diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index d718926..e2b2b93 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -44,7 +44,6 @@ public ResultTable(ResultFormat fmt) { var model = new ResultTableModel(fmt); setModel(model); - setRowSorter(sorter()); model.addListener(event -> setRowSorter(sorter())); @@ -60,6 +59,12 @@ public ResultTable(ResultFormat fmt) { headersSize.height = RESULTS_HEADER_HEIGHT; getTableHeader().setPreferredSize(headersSize); + resetSession(); + } + + public void resetSession() { + ((ResultTableModel)getModel()).resetSession(); + /* * Listen to TaskTable and select appropriate results when task selection * changes @@ -76,10 +81,10 @@ public ResultTable(ResultFormat fmt) { * Automatically add finished tasks to this result table Automatically remove * results if corresponding task is removed */ - TaskManager.getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { var t = instance.getTask(e.getId()); - - if(t != null) { + + if (t != null) { var cc = (Calculation) t.getResponse(); @@ -112,10 +117,12 @@ public ResultTable(ResultFormat fmt) { default: break; } - + } }); - + + setRowSorter(sorter()); + } public void clear() { @@ -124,7 +131,8 @@ public void clear() { } private TableRowSorter sorter() { - var sorter = new TableRowSorter((ResultTableModel) getModel()); + var model = (ResultTableModel) getModel(); + var sorter = new TableRowSorter(model); var list = new ArrayList(); Comparator numericComparator = (i1, i2) -> i1.compareTo(i2); diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index c29818f..621210c 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -27,6 +27,8 @@ import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JSeparator; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; import pulse.input.ExperimentalData; import pulse.problem.schemes.solvers.Solver; @@ -61,10 +63,9 @@ public TaskPopupMenu() { ICON_GRAPH); itemExtendedChart.addActionListener(e -> plot(true)); - var instance = TaskManager.getManagerInstance(); - var itemShowMeta = new JMenuItem("Show metadata", ICON_METADATA); itemShowMeta.addActionListener((ActionEvent e) -> { + var instance = TaskManager.getManagerInstance(); var t = instance.getSelectedTask(); if (t == null) { showMessageDialog(getWindowAncestor((Component) e.getSource()), @@ -79,13 +80,8 @@ public TaskPopupMenu() { var itemShowStatus = new JMenuItem("What is missing?", ICON_MISSING); - instance.addSelectionListener(event -> { - instance.getSelectedTask().checkProblems(); - var details = instance.getSelectedTask().getStatus().getDetails(); - itemShowStatus.setEnabled((details != null) & (details != NONE)); - }); - itemShowStatus.addActionListener((ActionEvent e) -> { + var instance = TaskManager.getManagerInstance(); var t = instance.getSelectedTask(); if (t != null) { var d = t.getStatus().getDetails(); @@ -96,6 +92,7 @@ public TaskPopupMenu() { var itemExecute = new JMenuItem(getString("TaskTablePopupMenu.Execute"), ICON_RUN); //$NON-NLS-1$ itemExecute.addActionListener((ActionEvent e) -> { + var instance = TaskManager.getManagerInstance(); var t = instance.getSelectedTask(); if (t == null) { showMessageDialog(getWindowAncestor((Component) e.getSource()), @@ -130,11 +127,12 @@ public TaskPopupMenu() { var itemReset = new JMenuItem(getString("TaskTablePopupMenu.Reset"), ICON_RESET); - itemReset.addActionListener((ActionEvent arg0) -> instance.getSelectedTask().clear()); + itemReset.addActionListener((ActionEvent arg0) -> TaskManager.getManagerInstance().getSelectedTask().clear()); var itemGenerateResult = new JMenuItem(getString("TaskTablePopupMenu.GenerateResult"), ICON_RESULT); itemGenerateResult.addActionListener((ActionEvent arg0) -> { + var instance = TaskManager.getManagerInstance(); var t = instance.getSelectedTask(); if (t == null) { return; @@ -152,8 +150,32 @@ public TaskPopupMenu() { itemViewStored.setEnabled(false); - itemViewStored.addActionListener(arg0 -> instance.notifyListeners( - new TaskRepositoryEvent(TASK_BROWSING_REQUEST, instance.getSelectedTask().getIdentifier()))); + itemViewStored.addActionListener(arg0 -> { + var instance = TaskManager.getManagerInstance(); + instance.notifyListeners( + new TaskRepositoryEvent(TASK_BROWSING_REQUEST, instance.getSelectedTask().getIdentifier())); + }); + + this.addPopupMenuListener(new PopupMenuListener() { + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + var instance = TaskManager.getManagerInstance(); + instance.getSelectedTask().checkProblems(); + var details = instance.getSelectedTask().getStatus().getDetails(); + itemShowStatus.setEnabled((details != null) & (details != NONE)); + } + + @Override + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + // + } + + @Override + public void popupMenuCanceled(PopupMenuEvent e) { + //. + } + + }); add(itemShowMeta); add(itemShowStatus); diff --git a/src/main/java/pulse/ui/components/TaskTable.java b/src/main/java/pulse/ui/components/TaskTable.java index c7cff7b..e69ba89 100644 --- a/src/main/java/pulse/ui/components/TaskTable.java +++ b/src/main/java/pulse/ui/components/TaskTable.java @@ -56,32 +56,11 @@ public TaskTable() { setSelectionMode(SINGLE_INTERVAL_SELECTION); setShowHorizontalLines(false); - var model = new TaskTableModel(); - setModel(model); - var th = new TableHeader(getColumnModel()); setTableHeader(th); - getTableHeader().setPreferredSize(new Dimension(50, HEADER_HEIGHT)); - setAutoCreateRowSorter(true); - var sorter = new TableRowSorter(); - sorter.setModel(model); - var list = new ArrayList(); - - for (var i = 0; i < this.getModel().getColumnCount(); i++) { - list.add(new RowSorter.SortKey(i, ASCENDING)); - if (i == TaskTableModel.STATUS_COLUMN) { - sorter.setComparator(i, statusComparator); - } else { - sorter.setComparator(i, numericComparator); - } - } - - sorter.setSortKeys(list); - setRowSorter(sorter); - initListeners(); menu = new TaskPopupMenu(); @@ -89,8 +68,6 @@ public TaskTable() { public void initListeners() { - var instance = TaskManager.getManagerInstance(); - /* * mouse listener */ @@ -99,6 +76,8 @@ public void initListeners() { @Override public void mouseClicked(MouseEvent e) { + var instance = TaskManager.getManagerInstance(); + if (rowAtPoint(e.getPoint()) >= 0 && rowAtPoint(e.getPoint()) == getSelectedRow() && isRightMouseButton(e)) { var task = instance.getSelectedTask(); menu.getItemViewStored().setEnabled(task.getStoredCalculations().size() > 0); @@ -116,21 +95,50 @@ public void mouseClicked(MouseEvent e) { var reference = this; lsm.addListSelectionListener((ListSelectionEvent e) -> { + var instance = TaskManager.getManagerInstance(); if (!lsm.getValueIsAdjusting() && !lsm.isSelectionEmpty()) { var id = (Identifier) getValueAt(lsm.getMinSelectionIndex(), 0); instance.selectTask(id, reference); } }); + resetSession(); + + } + + public void resetSession() { + var model = new TaskTableModel(); + setModel(model); + + var sorter = new TableRowSorter(); + sorter.setModel(model); + var list = new ArrayList(); + + for (var i = 0; i < this.getModel().getColumnCount(); i++) { + list.add(new RowSorter.SortKey(i, ASCENDING)); + if (i == TaskTableModel.STATUS_COLUMN) { + sorter.setComparator(i, statusComparator); + } else { + sorter.setComparator(i, numericComparator); + } + } + + sorter.setSortKeys(list); + setRowSorter(sorter); + + var instance = TaskManager.getManagerInstance(); instance.addSelectionListener((TaskSelectionEvent e) -> { + // simply ignore call if event is triggered by taskTable - if (e.getSource() instanceof TaskTable) { + if (e.getSource() == this) { return; } var id = instance.getSelectedTask().getIdentifier(); Identifier idFromTable = null; int i = 0; + + clearSelection(); for (i = 0; i < getRowCount() && !id.equals(idFromTable); i++) { idFromTable = (Identifier) getValueAt(i, 0); @@ -139,9 +147,7 @@ public void mouseClicked(MouseEvent e) { if (i < getRowCount()) { setRowSelectionInterval(i, i); } - clearSelection(); }); - } @Override diff --git a/src/main/java/pulse/ui/components/TextLogPane.java b/src/main/java/pulse/ui/components/TextLogPane.java index f1b7dfc..de79129 100644 --- a/src/main/java/pulse/ui/components/TextLogPane.java +++ b/src/main/java/pulse/ui/components/TextLogPane.java @@ -23,12 +23,12 @@ public class TextLogPane extends AbstractLogger { private final JEditorPane editor; private final JScrollPane pane; - + public TextLogPane() { editor = new JEditorPane(); editor.setContentType("text/html"); editor.setEditable(false); - ( (DefaultCaret) editor.getCaret() ).setUpdatePolicy(ALWAYS_UPDATE); + ((DefaultCaret) editor.getCaret()).setUpdatePolicy(ALWAYS_UPDATE); pane = new JScrollPane(); pane.setViewportView(editor); } @@ -52,7 +52,6 @@ public void post(String text) { } } - public void printTimeTaken(Log log) { var time = log.timeTaken(); @@ -71,7 +70,7 @@ public void clear() { e.printStackTrace(); } } - + @Override public JComponent getGUIComponent() { return pane; @@ -82,4 +81,4 @@ public boolean isEmpty() { return editor.getDocument().getLength() < 1; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 5a06530..8ef13f4 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -28,9 +28,9 @@ public ExecutionButton() { setIcon(state.getIcon()); setToolTipText(state.getMessage()); - var instance = TaskManager.getManagerInstance(); - this.addActionListener((ActionEvent e) -> { + var instance = TaskManager.getManagerInstance(); + /* * STOP PRESSED? */ @@ -62,6 +62,12 @@ public ExecutionButton() { } }); + resetSession(); + + } + + public void resetSession() { + var instance = TaskManager.getManagerInstance(); instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { switch (e.getState()) { case TASK_SUBMITTED: @@ -81,7 +87,6 @@ public ExecutionButton() { return; } }); - } public void setExecutionState(ExecutionState state) { diff --git a/src/main/java/pulse/ui/components/buttons/LoaderButton.java b/src/main/java/pulse/ui/components/buttons/LoaderButton.java index cad1066..09733d9 100644 --- a/src/main/java/pulse/ui/components/buttons/LoaderButton.java +++ b/src/main/java/pulse/ui/components/buttons/LoaderButton.java @@ -6,12 +6,8 @@ import static javax.swing.JOptionPane.INFORMATION_MESSAGE; import static javax.swing.JOptionPane.showMessageDialog; import static javax.swing.SwingUtilities.getWindowAncestor; -import static pulse.input.InterpolationDataset.getDataset; -import static pulse.input.InterpolationDataset.StandartType.DENSITY; -import static pulse.input.InterpolationDataset.StandartType.HEAT_CAPACITY; import static pulse.io.readers.ReaderManager.getDatasetExtensions; import static pulse.ui.Messages.getString; -import static pulse.ui.components.DataLoader.load; import java.awt.Color; import java.awt.Component; @@ -26,34 +22,34 @@ import org.apache.commons.math3.exception.OutOfRangeException; import pulse.input.InterpolationDataset; +import pulse.tasks.TaskManager; +import static pulse.ui.components.DataLoader.loadDensity; +import static pulse.ui.components.DataLoader.loadSpecificHeat; import pulse.util.ImageUtils; @SuppressWarnings("serial") public class LoaderButton extends JButton { - private InterpolationDataset.StandartType dataType; + private final DataType dataType; private static File dir; - private final static Color NOT_HIGHLIGHTED = UIManager.getColor("Button.background"); - private final static Color HIGHLIGHTED = ImageUtils.blend(NOT_HIGHLIGHTED, Color.red, 0.35f); + private final static Color NOT_HIGHLIGHTED = UIManager.getColor("Button.background"); + private final static Color HIGHLIGHTED = ImageUtils.blend(NOT_HIGHLIGHTED, Color.red, 0.35f); - public LoaderButton() { + public LoaderButton(DataType type) { super(); + this.dataType = type; init(); } - public LoaderButton(String str) { + public LoaderButton(DataType type, String str) { super(str); + this.dataType = type; init(); } - + public final void init() { - - InterpolationDataset.addListener(e -> { - if (dataType == e) { - highlight(false); - } - }); + highlight(false); addActionListener((ActionEvent arg0) -> { var fileChooser = new JFileChooser(); @@ -71,10 +67,10 @@ public final void init() { try { switch (dataType) { case HEAT_CAPACITY: - load(HEAT_CAPACITY, fileChooser.getSelectedFile()); + loadSpecificHeat(fileChooser.getSelectedFile()); break; case DENSITY: - load(DENSITY, fileChooser.getSelectedFile()); + loadDensity(fileChooser.getSelectedFile()); break; default: throw new IllegalStateException("Unrecognised type: " + dataType); @@ -96,7 +92,7 @@ public final void init() { getString("LoaderButton.OFRError"), //$NON-NLS-1$ ERROR_MESSAGE); } - var size = getDataset(dataType).getData().size(); + int size = getDataset().getData().size(); var label = ""; switch (dataType) { case HEAT_CAPACITY: @@ -115,19 +111,26 @@ public final void init() { showMessageDialog(getWindowAncestor((Component) arg0.getSource()), sb.toString(), "Data loaded", INFORMATION_MESSAGE); + highlight(false); }); } - - public void setDataType(InterpolationDataset.StandartType dataType) { - this.dataType = dataType; + + public InterpolationDataset getDataset() { + var i = TaskManager.getManagerInstance(); + return dataType == DataType.HEAT_CAPACITY ? + i.getSpecificHeatDataset() : i.getDensityDataset(); } - + public void highlight(boolean highlighted) { setBackground(highlighted ? HIGHLIGHTED : NOT_HIGHLIGHTED); } public void highlightIfNeeded() { - highlight(getDataset(dataType) == null); + highlight(getDataset() == null); + } + + public enum DataType { + HEAT_CAPACITY, DENSITY; } } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java index 6f98ec3..4c363b5 100644 --- a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java @@ -1,6 +1,5 @@ package pulse.ui.components.controllers; - import java.awt.Component; import java.awt.Font; @@ -27,28 +26,23 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole int row, int column) { Component result = null; - + if (value instanceof Flag) { result = new IconCheckBox((boolean) ((Property) value).getValue()); ((IconCheckBox) result).setHorizontalAlignment(CENTER); - } - - else if (value instanceof PropertyHolder) { - var sb = new StringBuilder("Click to Edit/View "); - sb.append(((PropertyHolder) value).getSimpleName()); - sb.append("..."); - result = new JButton(sb.toString()); - ((JButton)result).setToolTipText(value.toString()); - ((JButton)result).setHorizontalAlignment(LEFT); - } - - else { + } else if (value instanceof PropertyHolder) { + var sb = new StringBuilder("Click to Edit/View "); + sb.append(((PropertyHolder) value).getSimpleName()); + sb.append("..."); + result = new JButton(sb.toString()); + ((JButton) result).setToolTipText(value.toString()); + ((JButton) result).setHorizontalAlignment(LEFT); + } else { result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); } - + return result; - + } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java index e9611d7..9f01e29 100644 --- a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java +++ b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java @@ -29,7 +29,7 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean if (e.getStateChange() == ItemEvent.SELECTED) { try { descriptor.attemptUpdate(e.getItem()); - } catch(NullPointerException npe) { + } catch (NullPointerException npe) { String text = "Error updating " + descriptor.getDescriptor(false) + ". Cannot be set to " + e.getItem(); System.out.println(text); @@ -37,7 +37,7 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean } } }); - + return combobox; } diff --git a/src/main/java/pulse/ui/components/controllers/NumberEditor.java b/src/main/java/pulse/ui/components/controllers/NumberEditor.java index c51120f..3aa4149 100644 --- a/src/main/java/pulse/ui/components/controllers/NumberEditor.java +++ b/src/main/java/pulse/ui/components/controllers/NumberEditor.java @@ -61,10 +61,6 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.*/ public class NumberEditor extends DefaultCellEditor { - /** - * - */ - private static final long serialVersionUID = 1L; JFormattedTextField ftf; NumberFormat numberFormat; private boolean DEBUG = false; @@ -104,10 +100,6 @@ public NumberEditor(NumericProperty property) { // JFormattedTextField's focusLostBehavior property.) ftf.getInputMap().put(getKeyStroke(VK_ENTER, 0), "check"); ftf.getActionMap().put("check", new AbstractAction() { - /** - * - */ - private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { diff --git a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java index 5f05afc..2d07657 100644 --- a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java @@ -41,7 +41,6 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole result = superRenderer; } - result.setForeground(UIManager.getColor("List.foreground")); return result; diff --git a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java index 4827624..dd621b9 100644 --- a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java @@ -8,7 +8,6 @@ import javax.swing.tree.DefaultTreeCellRenderer; //import com.alee.managers.icon.LazyIcon; - import pulse.problem.statements.Problem; import pulse.util.ImageUtils; import static pulse.util.ImageUtils.loadIcon; @@ -17,7 +16,7 @@ public class ProblemCellRenderer extends DefaultTreeCellRenderer { private static ImageIcon defaultIcon = loadIcon("leaf.png", 16); - + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { diff --git a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java index cfc884c..402bc41 100644 --- a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java @@ -17,7 +17,7 @@ public Component getListCellRendererComponent(JList list, Object value, int i var renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); ((JComponent) renderer).setBorder(createEmptyBorder(10, 10, 10, 10)); - + return renderer; } diff --git a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java index 33ccdcb..8bc8369 100644 --- a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java @@ -34,11 +34,11 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole superRenderer.setForeground(((Status) value).getColor()); superRenderer.setFont(superRenderer.getFont().deriveFont(BOLD)); - ((JLabel)superRenderer).setHorizontalAlignment(JLabel.CENTER); + ((JLabel) superRenderer).setHorizontalAlignment(JLabel.CENTER); return superRenderer; - } + } return superRenderer; diff --git a/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java b/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java index fe79169..121f70b 100644 --- a/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java +++ b/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java @@ -3,7 +3,6 @@ public interface FrameVisibilityRequestListener { public void onProblemStatementShowRequest(); - public void onSearchSettingsShowRequest(); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/listeners/LogListener.java b/src/main/java/pulse/ui/components/listeners/LogListener.java index 5a07f5d..a498817 100644 --- a/src/main/java/pulse/ui/components/listeners/LogListener.java +++ b/src/main/java/pulse/ui/components/listeners/LogListener.java @@ -3,6 +3,7 @@ public interface LogListener { public void onLogExportRequest(); + public void onLogModeChanged(boolean graphical); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java index 834dc1c..b619caf 100644 --- a/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java +++ b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java @@ -29,12 +29,12 @@ public class MouseOnMarkerListener implements ChartMouseListener { private final MovableValueMarker lower; private final MovableValueMarker upper; - + private final Chart chart; private final double margin; - + private final static Cursor CROSSHAIR = new Cursor(Cursor.CROSSHAIR_CURSOR); - private final static Cursor RESIZE = new Cursor(Cursor.E_RESIZE_CURSOR); + private final static Cursor RESIZE = new Cursor(Cursor.E_RESIZE_CURSOR); public MouseOnMarkerListener(Chart chart, MovableValueMarker lower, MovableValueMarker upper, double margin) { this.chart = chart; @@ -51,33 +51,31 @@ public void chartMouseClicked(ChartMouseEvent arg0) { @Override public void chartMouseMoved(ChartMouseEvent arg0) { double xCoord = chart.xCoord(arg0.getTrigger()); - highlightMarker(xCoord); + highlightMarker(xCoord); } private void highlightMarker(double xCoord) { if (xCoord > (lower.getValue() - margin) & xCoord < (lower.getValue() + margin)) { - - lower.setState(MovableValueMarker.State.SELECTED); + + lower.setState(MovableValueMarker.State.SELECTED); chart.getChartPanel().setCursor(RESIZE); - - } - else if (xCoord > (upper.getValue() - margin) + + } else if (xCoord > (upper.getValue() - margin) & xCoord < (upper.getValue() + margin)) { - + upper.setState(MovableValueMarker.State.SELECTED); chart.getChartPanel().setCursor(RESIZE); - - } - else { - + + } else { + lower.setState(MovableValueMarker.State.IDLE); upper.setState(MovableValueMarker.State.IDLE); chart.getChartPanel().setCursor(CROSSHAIR); - + } } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/listeners/ResultListener.java b/src/main/java/pulse/ui/components/listeners/ResultListener.java index 17ba3bf..4ba96d2 100644 --- a/src/main/java/pulse/ui/components/listeners/ResultListener.java +++ b/src/main/java/pulse/ui/components/listeners/ResultListener.java @@ -6,4 +6,4 @@ public interface ResultListener { public void onFormatChanged(ResultFormatEvent fme); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/models/ParameterTableModel.java b/src/main/java/pulse/ui/components/models/ParameterTableModel.java index 66aa0cb..ec1bf9c 100644 --- a/src/main/java/pulse/ui/components/models/ParameterTableModel.java +++ b/src/main/java/pulse/ui/components/models/ParameterTableModel.java @@ -9,19 +9,15 @@ import javax.swing.table.AbstractTableModel; -import pulse.input.InterpolationDataset; import pulse.properties.Flag; import pulse.properties.NumericProperties; import pulse.properties.NumericPropertyKeyword; import pulse.search.direction.ActiveFlags; +import pulse.tasks.TaskManager; import pulse.ui.Messages; public class ParameterTableModel extends AbstractTableModel { - - /** - * - */ - private static final long serialVersionUID = 1L; + protected List elements; private final boolean extendedList; @@ -39,7 +35,7 @@ public final void populateWithAllProperties() { elements.add(OPTIMISER_STATISTIC); elements.add(TEST_STATISTIC); elements.add(IDENTIFIER); - elements.addAll(InterpolationDataset.derivableProperties()); + elements.addAll(TaskManager.getManagerInstance().derivableProperties()); } } @@ -55,19 +51,19 @@ public int getColumnCount() { @Override public Object getValueAt(int i, int i1) { - if(i > -1 && i < getRowCount() && i1 > -1 && i1 < getColumnCount()) { + if (i > -1 && i < getRowCount() && i1 > -1 && i1 < getColumnCount()) { var p = NumericProperties.def(elements.get(i)); - return i1 == 0 ? p.getAbbreviation(true) : Messages.getString("TextWrap.2") + - p.getDescriptor(false) + Messages.getString("TextWrap.1"); - } - else + return i1 == 0 ? p.getAbbreviation(true) : Messages.getString("TextWrap.2") + + p.getDescriptor(false) + Messages.getString("TextWrap.1"); + } else { return null; + } } - + public boolean contains(NumericPropertyKeyword key) { return elements.contains(key); } - + public NumericPropertyKeyword getElementAt(int index) { return elements.get(index); } diff --git a/src/main/java/pulse/ui/components/models/ResultTableModel.java b/src/main/java/pulse/ui/components/models/ResultTableModel.java index 8592285..c131f9c 100644 --- a/src/main/java/pulse/ui/components/models/ResultTableModel.java +++ b/src/main/java/pulse/ui/components/models/ResultTableModel.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import static javax.swing.SwingUtilities.invokeLater; import javax.swing.table.DefaultTableModel; @@ -17,6 +18,7 @@ import pulse.tasks.Identifier; import pulse.tasks.SearchTask; +import pulse.tasks.TaskManager; import pulse.tasks.listeners.ResultFormatEvent; import pulse.tasks.logs.Details; import pulse.tasks.logs.Status; @@ -36,8 +38,8 @@ public class ResultTableModel extends DefaultTableModel { public ResultTableModel(ResultFormat fmt, int rowCount) { super(fmt.abbreviations().toArray(), rowCount); - this.fmt = fmt; results = new ArrayList<>(); + this.fmt = fmt; tooltips = tooltips(); listeners = new ArrayList<>(); } @@ -46,6 +48,14 @@ public ResultTableModel(ResultFormat fmt) { this(fmt, 0); } + public void resetSession() { + clear(); + changeFormat(ResultFormat.getInstance()); + var repo = TaskManager.getManagerInstance(); + repo.getTaskList().stream() + .forEach(t -> addRow(t.findBestCalculation().getResult())); + } + public void addListener(ResultListener listener) { listeners.add(listener); } @@ -219,24 +229,28 @@ private List tooltips() { public void addRow(AbstractResult result) { Objects.requireNonNull(result, "Entry added to the results table must not be null"); + var instance = TaskManager.getManagerInstance(); + //ignore average results if (result instanceof Result) { //result must have a valid ancestor! - var ancestor = Objects.requireNonNull(result.specificAncestor(SearchTask.class), - "Result " + result.toString() + " does not belong a SearchTask!"); - - //the ancestor then has the SearchTask type - SearchTask parentTask = (SearchTask) ancestor; + var id = ((Result) result).getTaskIdentifier(); + SearchTask parentTask = instance.getTask(id); //any old result asssociated withis this task - var oldResult = results.stream().filter(r - -> r.specificAncestor(SearchTask.class) == parentTask).findAny(); - - //check the following only if the old result is present - if (oldResult.isPresent()) { - - AbstractResult oldResultExisting = oldResult.get(); + var linkedResults = results.stream() + .filter(r -> r instanceof Result) + .filter(rr -> ((Result) rr).getTaskIdentifier().equals(parentTask.getIdentifier())) + .collect(Collectors.toList()); + + if (linkedResults.size() > 1) { + //table can't contain more than one result associated with a task + throw new IllegalStateException("More than one result found associated with " + parentTask.getIdentifier()); + } else if (linkedResults.size() == 1) { + //check the following only if the old result is present + AbstractResult oldResultExisting = linkedResults.get(0); + //find specific calculation for this result Optional oldCalculation = parentTask.getStoredCalculations().stream() .filter(c -> c.getResult().equals(oldResultExisting)).findAny(); diff --git a/src/main/java/pulse/ui/components/models/SelectedKeysModel.java b/src/main/java/pulse/ui/components/models/SelectedKeysModel.java index 98491bf..f9098f2 100644 --- a/src/main/java/pulse/ui/components/models/SelectedKeysModel.java +++ b/src/main/java/pulse/ui/components/models/SelectedKeysModel.java @@ -1,6 +1,5 @@ package pulse.ui.components.models; - import java.util.ArrayList; import java.util.List; @@ -12,10 +11,6 @@ public class SelectedKeysModel extends DefaultTableModel { - /** - * - */ - private static final long serialVersionUID = 1L; private final List elements; private final List referenceList; private final NumericPropertyKeyword[] mandatorySelection; @@ -36,7 +31,7 @@ public void update(List keys) { elements.clear(); elements.addAll(keys); } - + @Override public int getRowCount() { return elements != null ? elements.size() : 0; @@ -46,29 +41,29 @@ public int getRowCount() { public int getColumnCount() { return 2; } - + @Override public Object getValueAt(int i, int i1) { - if(i > -1 && i < getRowCount() && i1 > -1 && i1 < getColumnCount()) { + if (i > -1 && i < getRowCount() && i1 > -1 && i1 < getColumnCount()) { var p = NumericProperties.def(elements.get(i)); - return i1 == 0 ? p.getAbbreviation(true) : Messages.getString("TextWrap.2") + - p.getDescriptor(false) + Messages.getString("TextWrap.1"); - } - else + return i1 == 0 ? p.getAbbreviation(true) : Messages.getString("TextWrap.2") + + p.getDescriptor(false) + Messages.getString("TextWrap.1"); + } else { return null; + } } - + public void addElement(NumericPropertyKeyword key) { elements.add(key); var e = NumericProperties.def(key); int index = elements.size() - 1; super.fireTableRowsInserted(index, index); } - + public boolean contains(NumericPropertyKeyword key) { return elements.contains(key); } - + public List getData() { return elements; } @@ -76,7 +71,7 @@ public List getData() { public NumericPropertyKeyword getElementAt(int index) { return elements.get(index); } - + public boolean removeElement(NumericPropertyKeyword key) { if (!elements.contains(key)) { @@ -88,11 +83,11 @@ public boolean removeElement(NumericPropertyKeyword key) { return false; } } - + var index = elements.indexOf(key); super.fireTableRowsDeleted(index, index); elements.remove(key); return true; } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/models/TaskBoxModel.java b/src/main/java/pulse/ui/components/models/TaskBoxModel.java index 5230f28..317d367 100644 --- a/src/main/java/pulse/ui/components/models/TaskBoxModel.java +++ b/src/main/java/pulse/ui/components/models/TaskBoxModel.java @@ -17,10 +17,6 @@ */ public class TaskBoxModel extends AbstractListModel implements ComboBoxModel { - /** - * - */ - private static final long serialVersionUID = 5394433933807306979L; protected SearchTask selectedTask; public TaskBoxModel() { diff --git a/src/main/java/pulse/ui/components/models/TaskTableModel.java b/src/main/java/pulse/ui/components/models/TaskTableModel.java index 86eef58..2145d57 100644 --- a/src/main/java/pulse/ui/components/models/TaskTableModel.java +++ b/src/main/java/pulse/ui/components/models/TaskTableModel.java @@ -31,14 +31,21 @@ public class TaskTableModel extends DefaultTableModel { public TaskTableModel() { super(new Object[][]{}, - new String[]{def(IDENTIFIER).getAbbreviation(true), + new String[]{def(IDENTIFIER).getAbbreviation(true), def(TEST_TEMPERATURE).getAbbreviation(true), - def(OPTIMISER_STATISTIC).getAbbreviation(true), + def(OPTIMISER_STATISTIC).getAbbreviation(true), def(TEST_STATISTIC).getAbbreviation(true), getString("TaskTable.Status")}); + resetSession(); + } + + public void resetSession() { + //clear all rows + this.setRowCount(0); var instance = TaskManager.getManagerInstance(); - + instance.getTaskList().stream().forEach(t -> addTask(t)); + /* * task removed/added listener */ @@ -49,14 +56,13 @@ public TaskTableModel() { addTask(instance.getTask(e.getId())); } }); - } public void addTask(SearchTask t) { - var temperature = ( (ExperimentalData) t.getInput() ) + var temperature = ((ExperimentalData) t.getInput()) .getMetadata().numericProperty(TEST_TEMPERATURE); var calc = (Calculation) t.getResponse(); - var data = new Object[]{t.getIdentifier(), temperature, + var data = new Object[]{t.getIdentifier(), temperature, calc.getOptimiserStatistic().getStatistic(), t.getNormalityTest().getStatistic(), t.getStatus()}; @@ -65,7 +71,7 @@ public void addTask(SearchTask t) { t.addStatusChangeListener((StateEntry e) -> { setValueAt(e.getState(), searchRow(t.getIdentifier()), STATUS_COLUMN); if (t.getNormalityTest() != null) { - setValueAt(t.getNormalityTest().getStatistic(), + setValueAt(t.getNormalityTest().getStatistic(), searchRow(t.getIdentifier()), TEST_STATISTIC_COLUMN); } }); diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index dca6e62..bb54a90 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -171,7 +171,7 @@ private void validateRange(double a, double b) { // set range for all available experimental datasets TaskManager.getManagerInstance().getTaskList() .stream().forEach((aTask) - -> setRange( (ExperimentalData) aTask.getInput(), a, b) + -> setRange((ExperimentalData) aTask.getInput(), a, b) ); } diff --git a/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java index ebb6bf6..d685da9 100644 --- a/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java +++ b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java @@ -20,13 +20,13 @@ public DoubleTablePanel(JTable leftTable, String titleLeft, JTable rightTable, S super(); initComponents(leftTable, titleLeft, rightTable, titleRight); - + moveRightBtn.addActionListener(e -> { var model = (SelectedKeysModel) rightTable.getModel(); - NumericPropertyKeyword key = ( (ParameterTableModel) leftTable.getModel() ) - .getElementAt(leftTable - .convertRowIndexToModel(leftTable.getSelectedRow())); + NumericPropertyKeyword key = ((ParameterTableModel) leftTable.getModel()) + .getElementAt(leftTable + .convertRowIndexToModel(leftTable.getSelectedRow())); if (key != null) { if (!model.contains(key)) { @@ -57,7 +57,7 @@ public DoubleTablePanel(JTable leftTable, String titleLeft, JTable rightTable, S } }); - + } public void initComponents(JTable leftTable, String titleLeft, JTable rightTable, String titleRight) { @@ -72,7 +72,7 @@ public void initComponents(JTable leftTable, String titleLeft, JTable rightTable var borderLeft = createTitledBorder(titleLeft); leftScroller.setBorder(borderLeft); - + leftTable.setRowHeight(80); leftScroller.setViewportView(leftTable); diff --git a/src/main/java/pulse/ui/components/panels/LogToolbar.java b/src/main/java/pulse/ui/components/panels/LogToolbar.java index 5db207b..84879f1 100644 --- a/src/main/java/pulse/ui/components/panels/LogToolbar.java +++ b/src/main/java/pulse/ui/components/panels/LogToolbar.java @@ -56,4 +56,4 @@ public void addLogListener(LogListener l) { listeners.add(l); } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java index ca36f67..0ddf940 100644 --- a/src/main/java/pulse/ui/components/panels/ProblemToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -5,8 +5,6 @@ import static javax.swing.JOptionPane.ERROR_MESSAGE; import static javax.swing.JOptionPane.showMessageDialog; import static javax.swing.SwingUtilities.getWindowAncestor; -import static pulse.input.InterpolationDataset.StandartType.DENSITY; -import static pulse.input.InterpolationDataset.StandartType.HEAT_CAPACITY; import static pulse.tasks.logs.Status.INCOMPLETE; import static pulse.ui.Messages.getString; @@ -22,6 +20,8 @@ import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.buttons.LoaderButton; +import static pulse.ui.components.buttons.LoaderButton.DataType.DENSITY; +import static pulse.ui.components.buttons.LoaderButton.DataType.HEAT_CAPACITY; import pulse.ui.frames.MainGraphFrame; import pulse.ui.frames.TaskControlFrame; @@ -40,12 +40,12 @@ public ProblemToolbar() { btnSimulate = new JButton(getString("ProblemStatementFrame.SimulateButton")); //$NON-NLS-1$ add(btnSimulate); - btnLoadCv = new LoaderButton(getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ - btnLoadCv.setDataType(HEAT_CAPACITY); + btnLoadCv = new LoaderButton(HEAT_CAPACITY, + getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ add(btnLoadCv); - btnLoadDensity = new LoaderButton(getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ - btnLoadDensity.setDataType(DENSITY); + btnLoadDensity = new LoaderButton(DENSITY, + getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ add(btnLoadDensity); btnSimulate.addActionListener((ActionEvent e) @@ -69,7 +69,7 @@ public static void plot(ActionEvent e) { if (status == INCOMPLETE && !status.checkProblemStatementSet()) { getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), + showMessageDialog(getWindowAncestor((Component) e.getSource()), calc.getStatus().getMessage(), getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ ERROR_MESSAGE); diff --git a/src/main/java/pulse/ui/components/panels/SettingsToolBar.java b/src/main/java/pulse/ui/components/panels/SettingsToolBar.java index 405e545..a86cee5 100644 --- a/src/main/java/pulse/ui/components/panels/SettingsToolBar.java +++ b/src/main/java/pulse/ui/components/panels/SettingsToolBar.java @@ -19,8 +19,6 @@ public class SettingsToolBar extends JToolBar { - private static final long serialVersionUID = -1171612225785102673L; - private JCheckBox cbSingleStatement, cbHideDetails; public SettingsToolBar(PropertyHolderTable... tables) { diff --git a/src/main/java/pulse/ui/components/panels/TaskToolbar.java b/src/main/java/pulse/ui/components/panels/TaskToolbar.java index 786178e..522a6f3 100644 --- a/src/main/java/pulse/ui/components/panels/TaskToolbar.java +++ b/src/main/java/pulse/ui/components/panels/TaskToolbar.java @@ -68,6 +68,10 @@ private void initComponents() { execBtn.setToolTipText("Execute All Tasks"); add(execBtn); } + + public void resetSession() { + ((ExecutionButton)execBtn).resetSession(); + } public void setRemoveEnabled(boolean b) { removeBtn.setEnabled(b); @@ -118,5 +122,11 @@ public void notifyGraph() { public void addTaskActionListener(TaskActionListener l) { listeners.add(l); } + + public void removeListeners() { + if(listeners != null) { + listeners.clear(); + } + } } diff --git a/src/main/java/pulse/ui/frames/DataFrame.java b/src/main/java/pulse/ui/frames/DataFrame.java index 492df29..152b1a7 100644 --- a/src/main/java/pulse/ui/frames/DataFrame.java +++ b/src/main/java/pulse/ui/frames/DataFrame.java @@ -16,7 +16,6 @@ public class DataFrame extends JFrame { - private static final long serialVersionUID = 1L; private JPanel contentPane; private PropertyHolderTable dataTable; private Component ancestorFrame; diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index 096bac2..0db89c6 100644 --- a/src/main/java/pulse/ui/frames/LogFrame.java +++ b/src/main/java/pulse/ui/frames/LogFrame.java @@ -27,7 +27,6 @@ import pulse.ui.components.panels.LogToolbar; import pulse.ui.components.panels.SystemPanel; -@SuppressWarnings("serial") public class LogFrame extends JInternalFrame { private AbstractLogger logger; @@ -76,7 +75,7 @@ public void onLogModeChanged(boolean graphical) { } - private void scheduleLogEvents() { + public void scheduleLogEvents() { var instance = TaskManager.getManagerInstance(); instance.addSelectionListener( e -> SwingUtilities.invokeLater(() -> logger.postAll())); @@ -134,4 +133,4 @@ private void setGraphicalLogger(boolean graphicalLog) { } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/frames/MainGraphFrame.java b/src/main/java/pulse/ui/frames/MainGraphFrame.java index e4400d4..d31f570 100644 --- a/src/main/java/pulse/ui/frames/MainGraphFrame.java +++ b/src/main/java/pulse/ui/frames/MainGraphFrame.java @@ -4,11 +4,8 @@ import static java.awt.BorderLayout.LINE_END; import static java.awt.BorderLayout.PAGE_END; import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.swing.JInternalFrame; -import pulse.problem.schemes.solvers.SolverException; import pulse.tasks.TaskManager; import pulse.tasks.logs.Status; diff --git a/src/main/java/pulse/ui/frames/ModelSelectionFrame.java b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java index 68856ab..1d24702 100644 --- a/src/main/java/pulse/ui/frames/ModelSelectionFrame.java +++ b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java @@ -24,13 +24,17 @@ public ModelSelectionFrame() { setSize(new Dimension(400, 400)); setTitle("Stored Calculations"); getContentPane().add(new ModelToolbar(), BorderLayout.SOUTH); + resetSession(); + this.setDefaultCloseOperation(HIDE_ON_CLOSE); + } + + public void resetSession() { var instance = TaskManager.getManagerInstance(); instance.addTaskRepositoryListener(e -> { if (e.getState() == TASK_BROWSING_REQUEST) { table.update(instance.getTask(e.getId())); } }); - this.setDefaultCloseOperation(HIDE_ON_CLOSE); } } diff --git a/src/main/java/pulse/ui/frames/PreviewFrame.java b/src/main/java/pulse/ui/frames/PreviewFrame.java index 508d5a7..87a156b 100644 --- a/src/main/java/pulse/ui/frames/PreviewFrame.java +++ b/src/main/java/pulse/ui/frames/PreviewFrame.java @@ -85,7 +85,7 @@ private void init() { getContentPane().add(createEmptyPanel(), CENTER); - var toolbar = new JToolBar(); + var toolbar = new JToolBar(); toolbar.setFloatable(false); toolbar.setLayout(new GridLayout()); @@ -118,7 +118,7 @@ private void init() { selectXBox.addItemListener(e -> replot(chart)); selectYBox.addItemListener(e -> replot(chart)); - this.setDefaultCloseOperation(EXIT_ON_CLOSE); + this.setDefaultCloseOperation(EXIT_ON_CLOSE); } private void replot(JFreeChart chart) { @@ -146,7 +146,7 @@ private void replot(JFreeChart chart) { if (drawSmooth) { drawSmooth(plot, selectedX, selectedY); } - + } private void drawSmooth(XYPlot plot, int selectedX, int selectedY) { @@ -161,7 +161,7 @@ private void drawSmooth(XYPlot plot, int selectedX, int selectedY) { //Akima spline for small number of points var interpolator = new AkimaSplineInterpolator(); interpolation = interpolator.interpolate(data[selectedX][0], data[selectedY][0]); - } catch( NonMonotonicSequenceException e) { + } catch (NonMonotonicSequenceException e) { //do not draw if points not strictly increasing return; } @@ -175,7 +175,7 @@ private void drawSmooth(XYPlot plot, int selectedX, int selectedY) { x[i] = data[selectedX][0][0] + dx * i; try { y[i] = interpolation.value(x[i]); - } catch(OutOfRangeException e) { + } catch (OutOfRangeException e) { y[i] = Double.NaN; } } @@ -192,14 +192,14 @@ public void update(ResultFormat fmt, double[][][] data) { propertyNames = new ArrayList<>(size); String tmp; - + selectXBox.removeAllItems(); selectYBox.removeAllItems(); for (var s : descriptors) { selectXBox.addItem(s); selectYBox.addItem(s); - } + } selectXBox.setSelectedIndex(fmt.indexOf(TEST_TEMPERATURE)); selectYBox.setSelectedIndex(fmt.indexOf(DIFFUSIVITY)); @@ -228,14 +228,13 @@ private static ChartPanel createEmptyPanel() { //plot.setRangeGridlinesVisible(false); //plot.setDomainGridlinesVisible(false); - var fore = UIManager.getColor("Label.foreground"); plot.setDomainGridlinePaint(fore); - + plot.getRenderer(1).setSeriesPaint(1, SMOOTH_COLOR); plot.getRenderer(0).setSeriesPaint(0, RESULT_COLOR); - plot.getRenderer(0).setSeriesShape(0, - new Rectangle(-MARKER_SIZE/2, -MARKER_SIZE/2, MARKER_SIZE, MARKER_SIZE)); + plot.getRenderer(0).setSeriesShape(0, + new Rectangle(-MARKER_SIZE / 2, -MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE)); chart.removeLegend(); @@ -250,7 +249,7 @@ private static ChartPanel createEmptyPanel() { plot.setBackgroundPaint(chart.getBackgroundPaint()); Chart.setAxisFontColor(plot.getDomainAxis(), fore); Chart.setAxisFontColor(plot.getRangeAxis(), fore); - + return cp; } diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 525ac3e..9c0bd4b 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -37,7 +37,6 @@ import pulse.tasks.Calculation; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; -import pulse.tasks.listeners.TaskSelectionEvent; import pulse.ui.components.ProblemTree; import pulse.ui.components.PropertyHolderTable; import pulse.ui.components.listeners.ProblemSelectionEvent; @@ -91,8 +90,6 @@ public ProblemStatementFrame() { problemTree = new ProblemTree(knownProblems); contentPane.add(new JScrollPane(problemTree)); - var instance = getManagerInstance(); - problemListExecutor = Executors.newCachedThreadPool(); schemeListExecutor = Executors.newCachedThreadPool(); propertyExecutor = Executors.newCachedThreadPool(); @@ -188,12 +185,12 @@ public void setSelectionPath(TreePath path) { getContentPane().add(contentPane, CENTER); getContentPane().add(toolbar, SOUTH); - /* - * listeners - */ - instance.addSelectionListener((TaskSelectionEvent e) -> update(instance.getSelectedTask())); - - getManagerInstance().addHierarchyListener(event -> { + resetSession(); + } + + public void resetSession() { + var instance = getManagerInstance(); + instance.addHierarchyListener(event -> { if ((event.getSource() instanceof PropertyHolderTable) && instance.isSingleStatement()) { //for all tasks @@ -211,7 +208,6 @@ public void setSelectionPath(TreePath path) { } }); - } public void update() { diff --git a/src/main/java/pulse/ui/frames/ResultFrame.java b/src/main/java/pulse/ui/frames/ResultFrame.java index 0703127..93aa7e8 100644 --- a/src/main/java/pulse/ui/frames/ResultFrame.java +++ b/src/main/java/pulse/ui/frames/ResultFrame.java @@ -122,13 +122,19 @@ public void addFrameCreationListener(PreviewFrameCreationListener l) { private void showInputDialog() { averageWindowDialog.setLocationRelativeTo(null); averageWindowDialog.setVisible(true); - averageWindowDialog.setConfirmAction(() -> - ((ResultTableModel)resultTable.getModel()) - .merge(averageWindowDialog.value().doubleValue())); + averageWindowDialog.setConfirmAction(() + -> ((ResultTableModel) resultTable.getModel()) + .merge(averageWindowDialog.value().doubleValue())); } public ResultTable getResultTable() { return resultTable; } -} + public void removeAllListeners() { + if (listeners != null) { + listeners.clear(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index 7d2c9db..94306df 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -88,11 +88,11 @@ public SearchOptionsFrame() { leftTable.setModel(new ParameterTableModel(false)); leftTable.setTableHeader(null); leftTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - + rightTable = new javax.swing.JTable(); rightTable.setTableHeader(null); rightTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - + var mainContainer = new DoubleTablePanel(leftTable, "All Parameters", rightTable, "Optimised Parameters"); getContentPane().add(pathListScroller, gbc); @@ -109,7 +109,7 @@ public SearchOptionsFrame() { tableScroller.setBorder( createTitledBorder("Select search variables and settings")); getContentPane().add(tableScroller, gbc); - + } public void update() { @@ -126,18 +126,18 @@ public void update() { leftTable.setAutoCreateRowSorter(true); leftTable.getRowSorter().toggleSortOrder(0); - + var rightTblModel = rightTable.getModel(); var activeTask = TaskManager.getManagerInstance().getSelectedTask(); //model for the flags list already created if (rightTblModel instanceof SelectedKeysModel) { var searchKeys = activeTask.activeParameters(); - ((ParameterTableModel)leftTable.getModel()).populateWithAllProperties(); + ((ParameterTableModel) leftTable.getModel()).populateWithAllProperties(); ((SelectedKeysModel) rightTblModel).update(searchKeys); } //Create a new model for the flags list else { - var c = (Calculation)activeTask.getResponse(); + var c = (Calculation) activeTask.getResponse(); if (c != null && c.getProblem() != null) { var searchKeys = activeTask.activeParameters(); rightTable.setModel(new SelectedKeysModel(searchKeys, mandatorySelection)); @@ -148,28 +148,28 @@ public void update() { rightTable.getModel().addTableModelListener(new TableModelListener() { private void updateFlag(TableModelEvent arg0, boolean value) { - var source = (NumericPropertyKeyword) - ( (SelectedKeysModel)rightTable.getModel() ) - .getElementAt(arg0.getFirstRow()); + var source = (NumericPropertyKeyword) ((SelectedKeysModel) rightTable.getModel()) + .getElementAt(arg0.getFirstRow()); var flag = new Flag(source); flag.setValue(value); PathOptimiser.getInstance().update(flag); } - + @Override public void tableChanged(TableModelEvent tme) { - if(tme.getType() == TableModelEvent.INSERT) + if (tme.getType() == TableModelEvent.INSERT) { updateFlag(tme, true); - else if(tme.getType() == TableModelEvent.DELETE) + } else if (tme.getType() == TableModelEvent.DELETE) { updateFlag(tme, false); + } } - + }); } } pathTable.updateTable(); - + } class PathSolversList extends JList { @@ -179,10 +179,6 @@ public PathSolversList() { super(); setModel(new AbstractListModel() { - /** - * - */ - private static final long serialVersionUID = -7683200230096704268L; @Override public int getSize() { diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index f56d6ed..2c17b04 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -18,6 +18,7 @@ import java.awt.event.ComponentEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.util.Objects; import javax.swing.JDesktopPane; import javax.swing.JFrame; @@ -25,7 +26,6 @@ import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; -import pulse.problem.statements.Pulse; import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.Version; @@ -55,7 +55,7 @@ public class TaskControlFrame extends JFrame { private InternalGraphFrame pulseFrame; private PulseMainMenu mainMenu; - + private final static int ICON_SIZE = 16; public static TaskControlFrame getInstance() { @@ -75,6 +75,27 @@ private TaskControlFrame() { setIconImage(loadIcon("logo.png", 32).getImage()); addListeners(); setDefaultCloseOperation(EXIT_ON_CLOSE); + TaskManager.addSessionListener(() -> resetSession()); + } + + public void resetSession() { + Objects.requireNonNull(mainMenu, "Menu not created"); + var s = TaskManager.getManagerInstance(); + s.addSelectionListener(e -> graphFrame.plot()); + problemStatementFrame.resetSession(); + taskManagerFrame.resetSession(); + modelFrame.resetSession(); + resultsFrame.getResultTable().resetSession(); + logFrame.getLogger().clear(); + logFrame.scheduleLogEvents(); + mainMenu.reset(); + s.addTaskRepositoryListener(e + -> { + if (e.getState() == TASK_BROWSING_REQUEST) { + setModelSelectionFrameVisible(true); + } + } + ); } private void addListeners() { @@ -136,15 +157,6 @@ public void onSearchSettingsShowRequest() { } }); - var manager = TaskManager.getManagerInstance(); - manager.addTaskRepositoryListener(e - -> { - if (e.getState() == TASK_BROWSING_REQUEST) { - setModelSelectionFrameVisible(true); - } - } - ); - addResultFormatListener(rfe -> ((ResultTableModel) resultsFrame.getResultTable().getModel()) .changeFormat(rfe.getResultFormat())); diff --git a/src/main/java/pulse/ui/frames/TaskManagerFrame.java b/src/main/java/pulse/ui/frames/TaskManagerFrame.java index 033edf9..a24f9c0 100644 --- a/src/main/java/pulse/ui/frames/TaskManagerFrame.java +++ b/src/main/java/pulse/ui/frames/TaskManagerFrame.java @@ -28,6 +28,12 @@ public TaskManagerFrame() { setVisible(true); } + public void resetSession() { + taskTable.resetSession(); + taskToolbar.resetSession(); + adjustEnabledControls(); + } + private void manageListeners() { taskToolbar.addTaskActionListener(new TaskActionListener() { @@ -62,21 +68,21 @@ private void initComponents() { taskToolbar = new TaskToolbar(); getContentPane().add(taskToolbar, PAGE_START); } + + private void enableIfNeeded() { + var ttm = (TaskTableModel) taskTable.getModel(); + + boolean enabled = ttm.getRowCount() > 0; + taskToolbar.setClearEnabled(enabled); + taskToolbar.setResetEnabled(enabled); + taskToolbar.setExecEnabled(enabled); + } private void adjustEnabledControls() { var ttm = (TaskTableModel) taskTable.getModel(); - ttm.addTableModelListener((TableModelEvent arg0) -> { - if (ttm.getRowCount() < 1) { - taskToolbar.setClearEnabled(false); - taskToolbar.setResetEnabled(false); - taskToolbar.setExecEnabled(false); - } else { - taskToolbar.setClearEnabled(true); - taskToolbar.setResetEnabled(true); - taskToolbar.setExecEnabled(true); - } - }); + enableIfNeeded(); + ttm.addTableModelListener((TableModelEvent arg0) -> enableIfNeeded() ); taskTable.getSelectionModel().addListSelectionListener((ListSelectionEvent arg0) -> { var selection = taskTable.getSelectedRows(); diff --git a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java index ed0f7ee..bdfe610 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java @@ -85,11 +85,11 @@ private File directoryQuery() { var returnVal = fileChooser.showOpenDialog(this); File f = null; - + if (returnVal == APPROVE_OPTION) { dir = f = fileChooser.getSelectedFile(); } - + return f; } @@ -250,7 +250,7 @@ public void removeUpdate(DocumentEvent e) { var browseBtn = new JButton("Browse..."); browseBtn.addActionListener(e -> directoryField.setText(directoryQuery() - .getPath() + separator + projectName + separator) ); + .getPath() + separator + projectName + separator)); var exportBtn = new JButton("Export"); diff --git a/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java b/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java index 179ab1f..82f2cf9 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java @@ -6,10 +6,16 @@ import java.awt.Dimension; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.JDialog; import javax.swing.JProgressBar; import javax.swing.SwingWorker; +import pulse.util.Serializer; +import static pulse.util.Serializer.deserialize; @SuppressWarnings("serial") public class ProgressDialog extends JDialog implements PropertyChangeListener { @@ -84,4 +90,25 @@ protected void done() { progressWorker.execute(); } -} + public interface ProgressWorker { + + public default void work() { + var dialog = new ProgressDialog(); + dialog.setLocationRelativeTo(null); + dialog.trackProgress(1); + CompletableFuture.runAsync(() -> { + try { + action(); + } catch (Exception ex) { + Logger.getLogger(Serializer.class.getName()).log(Level.SEVERE, "Failed to load session", ex); + System.err.println("Failed to load session."); + } + }) + .thenRun(() -> dialog.incrementProgress()); + } + + public void action(); + + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java index b0554c3..b196587 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java @@ -14,10 +14,6 @@ public class ResultChangeDialog extends JDialog { - /** - * - */ - private static final long serialVersionUID = 1L; private final static int WIDTH = 1000; private final static int HEIGHT = 600; @@ -51,34 +47,36 @@ private void initComponents() { setDefaultCloseOperation(HIDE_ON_CLOSE); leftTbl = new javax.swing.JTable() { - + @Override - public boolean isCellEditable(int row, int column) { - return false; - }; - + public boolean isCellEditable(int row, int column) { + return false; + } + ; + }; leftTbl.setModel(new ParameterTableModel(true)); leftTbl.setTableHeader(null); rightTbl = new javax.swing.JTable() { - + @Override - public boolean isCellEditable(int row, int column) { - return false; - }; - + public boolean isCellEditable(int row, int column) { + return false; + } + ; + }; rightTbl.setModel(new SelectedKeysModel( ResultFormat.getInstance().getKeywords(), ResultFormat.getMinimalArray())); rightTbl.setTableHeader(null); - + MainContainer = new DoubleTablePanel(leftTbl, "All Parameters", rightTbl, "Output"); - + getContentPane().add(MainContainer, BorderLayout.CENTER); MainToolbar.setFloatable(false); diff --git a/src/main/java/pulse/util/DescriptorChangeListener.java b/src/main/java/pulse/util/DescriptorChangeListener.java index 4c522b7..4edbe51 100644 --- a/src/main/java/pulse/util/DescriptorChangeListener.java +++ b/src/main/java/pulse/util/DescriptorChangeListener.java @@ -1,6 +1,8 @@ package pulse.util; -public interface DescriptorChangeListener { +import java.io.Serializable; + +public interface DescriptorChangeListener extends Serializable { public void onDescriptorChanged(); diff --git a/src/main/java/pulse/util/FunctionSerializer.java b/src/main/java/pulse/util/FunctionSerializer.java new file mode 100644 index 0000000..65911a8 --- /dev/null +++ b/src/main/java/pulse/util/FunctionSerializer.java @@ -0,0 +1,31 @@ +package pulse.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import org.apache.commons.math3.analysis.polynomials.PolynomialFunction; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; + +public class FunctionSerializer { + + private FunctionSerializer() { + //empty + } + + public static void writeSplineFunction(PolynomialSplineFunction f, ObjectOutputStream oos) + throws IOException { + // write the object + double[] knots = f != null ? f.getKnots() : null; + PolynomialFunction[] funcs = f != null ? f.getPolynomials() : null; + oos.writeObject(knots); + oos.writeObject(funcs); + } + + public static PolynomialSplineFunction readSplineFunction(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + var knots = (double[]) ois.readObject(); // knots + var funcs = (PolynomialFunction[]) ois.readObject(); + return knots != null & funcs != null ? new PolynomialSplineFunction(knots, funcs) : null; + } + +} diff --git a/src/main/java/pulse/util/Group.java b/src/main/java/pulse/util/Group.java index 2b1f888..cbdfc35 100644 --- a/src/main/java/pulse/util/Group.java +++ b/src/main/java/pulse/util/Group.java @@ -25,8 +25,8 @@ public List subgroups() { var methods = this.getClass().getMethods(); for (var m : methods) { - - if (m.getParameterCount() > 0 + + if (m.getParameterCount() > 0 || !Group.class.isAssignableFrom(m.getReturnType()) || m.getReturnType().isAssignableFrom(getClass())) { continue; diff --git a/src/main/java/pulse/util/HierarchyListener.java b/src/main/java/pulse/util/HierarchyListener.java index c6c3ef8..c5a87c6 100644 --- a/src/main/java/pulse/util/HierarchyListener.java +++ b/src/main/java/pulse/util/HierarchyListener.java @@ -1,5 +1,7 @@ package pulse.util; +import java.io.Serializable; + /** * An hierarchy listener, which listens to any changes happening with the * children of an {@code UpwardsNavigable}. @@ -7,7 +9,7 @@ * @see pulse.util.UpwardsNavigable * */ -public interface HierarchyListener { +public interface HierarchyListener extends Serializable { /** * This is invoked by the {@code UpwardsNavigable} when an event resulting diff --git a/src/main/java/pulse/util/ImmutableDataEntry.java b/src/main/java/pulse/util/ImmutableDataEntry.java index 7a55587..b764829 100644 --- a/src/main/java/pulse/util/ImmutableDataEntry.java +++ b/src/main/java/pulse/util/ImmutableDataEntry.java @@ -1,5 +1,7 @@ package pulse.util; +import java.io.Serializable; + /** * A {@code DataEntry} is an immutable ordered pair of an instance of {@code T}, * which is considered to be the 'key', and an instance of {@code R}, which is @@ -8,7 +10,7 @@ * @param the key * @param the value */ -public class ImmutableDataEntry { +public class ImmutableDataEntry implements Serializable { private T key; private R value; diff --git a/src/main/java/pulse/util/ImmutablePair.java b/src/main/java/pulse/util/ImmutablePair.java index f264614..f01f9a4 100644 --- a/src/main/java/pulse/util/ImmutablePair.java +++ b/src/main/java/pulse/util/ImmutablePair.java @@ -1,6 +1,8 @@ package pulse.util; -public class ImmutablePair { +import java.io.Serializable; + +public class ImmutablePair implements Serializable { private T anElement; private T anotherElement; diff --git a/src/main/java/pulse/util/InstanceDescriptor.java b/src/main/java/pulse/util/InstanceDescriptor.java index 644ad02..a8769a5 100644 --- a/src/main/java/pulse/util/InstanceDescriptor.java +++ b/src/main/java/pulse/util/InstanceDescriptor.java @@ -30,7 +30,7 @@ public InstanceDescriptor(String generalDescriptor, Class c, Object... argume allDescriptors = nameMap.get(c); selectedDescriptor = allDescriptors.iterator().next(); this.generalDescriptor = generalDescriptor; - listeners = new ArrayList(); + listeners = new ArrayList<>(); } public InstanceDescriptor(Class c, Object... arguments) { @@ -50,15 +50,15 @@ public Object getValue() { @Override public boolean attemptUpdate(Object object) { var string = object.toString(); - + if (selectedDescriptor.equals(string)) { return false; } - - if(!allDescriptors.contains(string)) { + + if (!allDescriptors.contains(string)) { throw new IllegalArgumentException("Unknown descriptor: " + selectedDescriptor); } - + this.selectedDescriptor = string; listeners.stream().forEach(l -> l.onDescriptorChanged()); return true; diff --git a/src/main/java/pulse/util/PropertyEvent.java b/src/main/java/pulse/util/PropertyEvent.java index 70c486a..0c74fc7 100644 --- a/src/main/java/pulse/util/PropertyEvent.java +++ b/src/main/java/pulse/util/PropertyEvent.java @@ -1,5 +1,6 @@ package pulse.util; +import java.io.Serializable; import pulse.properties.Property; /** @@ -7,7 +8,7 @@ * {@code PropertyHolder}. * */ -public class PropertyEvent { +public class PropertyEvent implements Serializable { private Object source; private PropertyHolder propertyHolder; diff --git a/src/main/java/pulse/util/PropertyHolder.java b/src/main/java/pulse/util/PropertyHolder.java index 05cef73..4eff7a0 100644 --- a/src/main/java/pulse/util/PropertyHolder.java +++ b/src/main/java/pulse/util/PropertyHolder.java @@ -22,7 +22,7 @@ public abstract class PropertyHolder extends Accessible { private List parameters = listedTypes(); - private List listeners; + private transient List listeners; private String prefix; /** @@ -63,8 +63,9 @@ public List listedTypes() { return listedKeywords().stream().map(key -> def(key)).collect(Collectors.toList()); } - public PropertyHolder() { - this.listeners = new ArrayList<>(); + public void initListeners() { + super.initListeners(); + listeners = new ArrayList<>(); } /** @@ -195,7 +196,9 @@ public boolean updateProperty(Object sourceComponent, Property updatedProperty) public void firePropertyChanged(Object source, Property property) { var event = new PropertyEvent(source, this, property); - listeners.forEach(l -> l.onPropertyChanged(event)); + if (listeners != null) { + listeners.forEach(l -> l.onPropertyChanged(event)); + } /* * If the changes are triggered by an external GUI component (such as @@ -220,11 +223,19 @@ public void updateProperties(Object sourceComponent, PropertyHolder propertyHold propertyHolder.data().stream().forEach(entry -> this.updateProperty(sourceComponent, entry)); } - public void removeHeatingCurveListeners() { - this.listeners.clear(); + public void removeListeners() { + if(listeners == null) { + listeners = new ArrayList<>(); + } + else { + listeners.clear(); + } } public void addListener(PropertyHolderListener l) { + if (listeners == null) { + this.listeners = new ArrayList<>(); + } this.listeners.add(l); } diff --git a/src/main/java/pulse/util/PropertyHolderListener.java b/src/main/java/pulse/util/PropertyHolderListener.java index de5017b..aa3e2b7 100644 --- a/src/main/java/pulse/util/PropertyHolderListener.java +++ b/src/main/java/pulse/util/PropertyHolderListener.java @@ -1,10 +1,12 @@ package pulse.util; +import java.io.Serializable; + /** * A listener used by {@code PropertyHolder}s to track changes with the * associated {@code Propert}ies. */ -public interface PropertyHolderListener { +public interface PropertyHolderListener extends Serializable { /** * This event is triggered by any {@code PropertyHolder}, the properties of diff --git a/src/main/java/pulse/util/Serializer.java b/src/main/java/pulse/util/Serializer.java new file mode 100644 index 0000000..ccd7833 --- /dev/null +++ b/src/main/java/pulse/util/Serializer.java @@ -0,0 +1,118 @@ +package pulse.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JFileChooser; +import static javax.swing.JFileChooser.APPROVE_OPTION; +import static javax.swing.JFileChooser.FILES_ONLY; +import javax.swing.filechooser.FileNameExtensionFilter; +import pulse.tasks.TaskManager; +import pulse.ui.frames.dialogs.ProgressDialog.ProgressWorker; + +public class Serializer { + + private static final FileNameExtensionFilter filter = new FileNameExtensionFilter( + "Saved sessions (.pulse)", "pulse"); + + private Serializer() { + // + } + + public static void serialize() throws IOException, FileNotFoundException, ClassNotFoundException { + var fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileSelectionMode(FILES_ONLY); + fileChooser.setFileFilter(filter); + File f = new File("./Saved/"); + if (!f.exists()) { + f.mkdir(); + } + fileChooser.setCurrentDirectory(f); + + int returnVal = fileChooser.showSaveDialog(null); + + if (returnVal == APPROVE_OPTION) { + String ext = filter.getExtensions()[0]; + File fileToBeSaved; + if (!fileChooser.getSelectedFile().getAbsolutePath().endsWith(ext)) { + fileToBeSaved = new File(fileChooser.getSelectedFile() + ext); + } else { + fileToBeSaved = fileChooser.getSelectedFile(); + } + + ProgressWorker worker = () -> { + try { + serialize(fileToBeSaved); + } catch (IOException | ClassNotFoundException ex) { + Logger.getLogger(Serializer.class.getName()).log(Level.SEVERE, "Failed to save session", ex); + System.err.println("Failed to save session."); + } + }; + + worker.work(); + } + + } + + public static void deserialize() throws FileNotFoundException { + var fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileSelectionMode(FILES_ONLY); + fileChooser.setFileFilter(filter); + File f = new File("./Saved/"); + if (f.exists()) { + fileChooser.setCurrentDirectory(f); + } + + int returnVal = fileChooser.showOpenDialog(null); + + if (returnVal == APPROVE_OPTION) { + + ProgressWorker worker = () -> { + try { + deserialize(fileChooser.getSelectedFile()); + } catch (IOException | ClassNotFoundException ex) { + Logger.getLogger(Serializer.class.getName()).log(Level.SEVERE, "Failed to load session", ex); + System.err.println("Failed to load session."); + } + }; + + worker.work(); + + } + + } + + public static void serialize(File fname) throws FileNotFoundException, IOException, ClassNotFoundException { + FileOutputStream fileOutputStream = new FileOutputStream(fname); + try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) { + var instance = TaskManager.getManagerInstance(); + objectOutputStream.writeObject(instance); + } + } + + public static void deserialize(File fname) throws FileNotFoundException, IOException, ClassNotFoundException { + FileInputStream fis = new FileInputStream(fname); + TaskManager state; + try (ObjectInputStream ois = new ObjectInputStream(fis)) { + state = (TaskManager) ois.readObject(); + } + //close stream + state.initListeners(); + state.getTaskList().stream().forEach(t -> { + t.initListeners(); + t.children().stream().forEach(c -> c.initListeners()); + } + ); + TaskManager.assumeNewState(state); + state.fireTaskSelected(state); + } + +} diff --git a/src/main/java/pulse/util/UpwardsNavigable.java b/src/main/java/pulse/util/UpwardsNavigable.java index ab8e744..1d4992c 100644 --- a/src/main/java/pulse/util/UpwardsNavigable.java +++ b/src/main/java/pulse/util/UpwardsNavigable.java @@ -1,5 +1,6 @@ package pulse.util; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -16,10 +17,14 @@ *

* */ -public abstract class UpwardsNavigable implements Descriptive { +public abstract class UpwardsNavigable implements Descriptive, Serializable { private UpwardsNavigable parent; - private final List listeners = new ArrayList<>(); + private transient List listeners; + + public void initListeners() { + listeners = new ArrayList<>(); + } public final void removeHierarchyListeners() { this.listeners.clear(); @@ -48,7 +53,9 @@ public final List getHierarchyListeners() { */ public void tellParent(PropertyEvent e) { if (parent != null) { - parent.listeners.forEach(l -> l.onChildPropertyChanged(e)); + if (parent.listeners != null) { + parent.listeners.forEach(l -> l.onChildPropertyChanged(e)); + } parent.tellParent(e); } } From 7b9b47285d89bf7430929761d93ddd72e14b3ad2 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 13 Feb 2023 12:41:27 +0300 Subject: [PATCH 13/14] PULsE v1.98 --- src/main/java/pulse/ui/components/PulseMainMenu.java | 8 ++------ src/main/resources/Version.txt | 2 +- src/main/resources/messages.properties | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index caec94a..ccf0f89 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -194,11 +194,9 @@ private void initComponents() { serializeItem.addActionListener(e -> { try { Serializer.serialize(); - } catch (IOException ex) { - Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); - } catch (ClassNotFoundException ex) { + } catch (IOException | ClassNotFoundException ex) { Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); - } + } }); var deserializeItem = new JMenuItem("Load Session..."); @@ -209,8 +207,6 @@ private void initComponents() { Serializer.deserialize(); } catch (IOException ex) { Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); - } catch (ClassNotFoundException ex) { - Logger.getLogger(PulseMainMenu.class.getName()).log(Level.SEVERE, null, ex); } }); diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt index 9688281..9288825 100644 --- a/src/main/resources/Version.txt +++ b/src/main/resources/Version.txt @@ -1 +1 @@ -1.97c \ No newline at end of file +1.98 \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index b9f7d32..1a4346a 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -291,4 +291,4 @@ MixedScheme2.5=Increased Accuracy Semi-implicit Scheme (NL)

    TextWrap.1=

    TextWrap.2=

    -msg.running=An instance of PULsE appears to be running. Please switch back to the running version or delete the pulse.lock file found in the PULsE directory. \ No newline at end of file +msg.running=An instance of PULsE appears to be running. Please switch back to the running version or delete the pulse.lock file in the user home directory. \ No newline at end of file From 63fc9861b7d4c3f9188bcdf30482306f3bf73b1b Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Mon, 13 Feb 2023 12:42:47 +0300 Subject: [PATCH 14/14] POM for PULsE v1.98 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f7140a2..557f9f5 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.97c + 1.98 PULsE Processing Unit for Laser flash Experiments