diff --git a/README.md b/README.md index 306ecaf5..29b5b111 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # PULsE -[![Build Status](https://travis-ci.com/kotik-coder/PULsE.svg?branch=releases)](https://travis-ci.com/kotik-coder/PULsE) -[![Code Quality](https://www.code-inspector.com/project/4377/score/svg)](https://frontend.code-inspector.com/public/project/4377/PULsE/dashboard) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1c354f5fe4d0435d8935f11cf2368d8e)](https://app.codacy.com/manual/kotik-coder/PULsE?utm_source=github.com&utm_medium=referral&utm_content=kotik-coder/PULsE&utm_campaign=Badge_Grade_Dashboard) [![Maintainability](https://api.codeclimate.com/v1/badges/bbbb695c6ffa3fbcb7e9/maintainability)](https://codeclimate.com/github/kotik-coder/PULsE/maintainability) [![codebeat badge](https://codebeat.co/badges/de6c9956-9737-4cba-ad1e-455140160792)](https://codebeat.co/projects/github-com-kotik-coder-pulse-development) @@ -45,6 +43,14 @@ Click [here](https://www.draw.io/?lightbox=1&highlight=0000ff&edit=_blank ## More info +This software is citable with the following DOI: + +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.6622739.svg)](https://doi.org/10.5281/zenodo.6622739) + +For a Journal reference, please use: + +[Lunev, A. et al. Software Impacts 6 (2020) 100044](https://doi.org/10.1016/j.simpa.2020.100044) + Please refer to the software [GitHub Page](https://kotik-coder.github.io/) for a [quickstart guide](https://kotik-coder.github.io/PULsE_Quickstart_Guide.pdf), input file examples and download links. ## Buy me a coffee diff --git a/pom.xml b/pom.xml index a73b8f46..557f9f5c 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,72 @@ 4.0.0 + kotik-coder PULsE - 1.85 + 1.98 PULsE Processing Unit for Laser flash Experiments + + + al + Artem Lunev + alounev@list.ru + + + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + org.jfree + jfreechart + 1.5.4 + + + com.formdev + flatlaf + 3.0 + + + org.apache.commons + commons-math3 + 3.6.1 + + + ca.umontreal.iro.simul + ssj + 3.3.1 + + + commons-io + commons-io + 2.7 + + + colt + colt + 1.2.0 + + + org.ejml + ejml-all + 0.39 + + + org.junit.jupiter + junit-jupiter + 5.7.0-M1 + test + + + + + src/main/java + src/test maven-compiler-plugin @@ -49,72 +110,21 @@ - - org.apache.maven.plugins - maven-surefire-plugin - 2.22.0 - - - org.junit.platform - junit-platform-surefire-provider - 1.2.0 - - - - - src/test/java/ - - - + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.22.0 + - - - org.jfree - jfreechart - 1.5.0 - - - org.apache.commons - commons-math3 - 3.6.1 - - - ca.umontreal.iro.simul - ssj - 3.3.1 - - - commons-io - commons-io - 2.6 - - - colt - colt - 1.2.0 - - - org.ejml - ejml-all - 0.39 - - - org.junit.jupiter - junit-jupiter-engine - 5.2.0 - test - - - org.junit.platform - junit-platform-runner - 1.2.0 - test - - - + UTF-8 UTF-8 - \ No newline at end of file + diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index ebe20b10..58fbceb3 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -8,16 +8,16 @@ import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; +import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; import pulse.util.PropertyHolder; /** - * A named collection of time and temperature values, with user-adjustable number of entries. + * A named collection of time and temperature values, with user-adjustable + * number of entries. *

* The notion of temperature is loosely used here, and this can represent just * the detector signal in mV. Unless explicitly specified otherwise, the unit of @@ -26,251 +26,284 @@ *

* */ - public abstract class AbstractData extends PropertyHolder { - private int count; - - private List time; - private List signal; - - private String name; - - protected AbstractData(List time, String name) { - this.time = time; - this.count = time.size(); - this.name = name; - } - - /** - * Creates an {@code AbstractData} with the default number of points (set in the - * corresponding XML file). - */ - - public AbstractData() { - this(def(NUMPOINTS)); - } - - /** - * Creates a {@code AbstractData}, where the number of elements in the - * {@code time} and {@code temperature} collections are set to - * {@code count.getValue()}. - *

- * - * @param count The {@code NumericProperty} that is derived from the - * {@code NumericPropertyKeyword.NUMPOINTS}. - */ - - public AbstractData(NumericProperty count) { - setNumPoints(count); - time = new ArrayList<>(this.count); - signal = new ArrayList<>(this.count); - } - - /** - * The actual number of points, explicitly calculated as the size of the internal lists. - * @return an integer size equal to the real number of elements (pairs) - */ - - public int actualNumPoints() { - return time.size(); - } - - /** - * Clears all elements from the three {@code List} objects, thus releasing - * memory. - */ - - public void clear() { - this.time.clear(); - this.signal.clear(); - } - - /** - * Getter method providing accessibility to the {@code count NumericProperty}. - * - * @return a {@code NumericProperty} derived from - * {@code NumericPropertyKeyword.NUMPOINTS} with the value of - * {@code count} - */ - - public NumericProperty getNumPoints() { - return derive(NUMPOINTS, count); - } - - /** - * Sets the number of points for this baseline. - *

- * The {@code List} data objects, containing time, temperature, and - * baseline-subtracted temperature are filled with zeroes. - * - * @param c - */ - - public void setNumPoints(NumericProperty c) { - requireType(c, NUMPOINTS); - this.count = (int) c.getValue(); - firePropertyChanged(this, c); - } - - /** - * Retrieves an element from the {@code time List} specified by {@code index} - * - * @param index the index of the element to be returned - * @return a time value corresponding to {@code index} - */ - - public double timeAt(int index) { - return time.get(index); - } - - /** - * Retrieves the last element of the {@code time List}. This is used e.g. by the - * {@code DifferenceScheme} to set the calculation limit for the - * finite-difference scheme. - * - * @see pulse.problem.schemes.DifferenceScheme - * @return a double, equal to the last element of the {@code time List}. - */ - - public double timeLimit() { - return timeAt(time.size() - 1); - } - - /** - * Retrieves the baseline-subtracted temperature corresponding to - * {@code index} in the respective {@code List}. - * - * @param index the index of the element - * @return a double, respresenting the baseline-subtracted temperature at - * {@code index} - */ - - public double signalAt(int index) { - return signal.get(index); - } - - public void addPoint(double time, double temperature) { - this.time.add(time); - this.signal.add(temperature); - } - - protected void incrementCount() { - count++; - } - - /** - * Sets the time {@code t} at the position {@code index} of the - * {@code time List}. - * - * @param index the index - * @param t the new time value at this index - */ - - public void setTimeAt(int index, double t) { - time.set(index, t); - } - - /** - * Sets the temperature {@code t} at the position {@code index} of the - * {@code temperature List}. - * - * @param index the index - * @param t the new temperature value at this index - */ - - public void setSignalAt(int index, double t) { - signal.set(index, t); - } - - public double apparentMaximum() { - return max(signal); - } - - public boolean isIncomplete() { - return time.size() < count; - } - - @Override - public String toString() { - return name != null ? name : getClass().getSimpleName() + " (" + getNumPoints() + ")"; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - /** - * Provides general setter accessibility for the number of points of this - * {@code AbstractData}. - * - * @param type must be equal to {@code NumericPropertyKeyword.NUMPOINTS} - * @param property the property of the type - * {@code NumericPropertyKeyword.NUMPOINTS} - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == NUMPOINTS) - setNumPoints(property); - } - - @Override - public List listedTypes() { - return new ArrayList<>(Arrays.asList(getNumPoints())); - } - - /** - * Removes an element with the index {@code i} from the time-temperature lists. - * - * @param i the element to be removed - */ - - public void remove(int i) { - this.time.remove(i); - this.signal.remove(i); - } - - @Override - public boolean ignoreSiblings() { - return true; - } - - public List getTimeSequence() { - return time; - } - - public List getSignalData() { - return signal; - } - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (!(o instanceof AbstractData)) - return false; - - var other = (AbstractData) o; - - final double EPS = 1e-8; - - if (abs(count - (Integer) other.getNumPoints().getValue()) > EPS) - return false; - - if (signal.hashCode() != other.signal.hashCode()) - return false; - - if (time.hashCode() != other.time.hashCode()) - return false; - - return time.containsAll(other.time) && signal.containsAll(other.signal); - - } - -} \ No newline at end of file + private int count; + + protected List time; + protected List signal; + + private String name; + + protected AbstractData(List time, String name) { + this.time = time; + this.count = time.size(); + this.name = name; + } + + /** + * Copy constructor. Copies all data and assigns the same name to + * {@code this}. + * + * @param d another instance of this class + */ + public AbstractData(AbstractData d) { + this.time = new ArrayList<>(d.time); + this.signal = new ArrayList<>(d.signal); + this.count = d.count; + this.name = d.name; + } + + /** + * Creates an {@code AbstractData} with the default number of points (set in + * the corresponding XML file). + */ + public AbstractData() { + this(def(NUMPOINTS)); + } + + /** + * Creates a {@code AbstractData}, where the number of elements in the + * {@code time} and {@code temperature} collections are set to + * {@code count.getValue()}. + *

+ * + * @param count The {@code NumericProperty} that is derived from the + * {@code NumericPropertyKeyword.NUMPOINTS}. + */ + public AbstractData(NumericProperty count) { + setNumPoints(count); + time = new ArrayList<>(this.count); + signal = new ArrayList<>(this.count); + } + + /** + * The actual number of points, explicitly calculated as the size of the + * internal lists. + * + * @return an integer size equal to the real number of elements (pairs) + */ + public int actualNumPoints() { + return time.size(); + } + + /** + * Clears all elements from the three {@code List} objects, thus releasing + * memory. + */ + public void clear() { + this.time.clear(); + this.signal.clear(); + } + + /** + * Getter method providing accessibility to the + * {@code count NumericProperty}. + * + * @return a {@code NumericProperty} derived from + * {@code NumericPropertyKeyword.NUMPOINTS} with the value of {@code count} + */ + public final NumericProperty getNumPoints() { + return derive(NUMPOINTS, count); + } + + /** + * Sets the number of points for this baseline. + *

+ * The {@code List} data objects, containing time, temperature, and + * baseline-subtracted temperature are filled with zeroes. + * + * @param c + */ + public final void setNumPoints(NumericProperty c) { + requireType(c, NUMPOINTS); + this.count = (int) c.getValue(); + firePropertyChanged(this, c); + } + + /** + * Retrieves an element from the {@code time List} specified by + * {@code index} + * + * @param index the index of the element to be returned + * @return a time value corresponding to {@code index} + */ + public double timeAt(int index) { + return time.get(index); + } + + /** + * Retrieves the last element of the {@code time List}. This is used e.g. by + * the {@code DifferenceScheme} to set the calculation limit for the + * finite-difference scheme. + * + * @see pulse.problem.schemes.DifferenceScheme + * @return a double, equal to the last element of the {@code time List}. + */ + public double timeLimit() { + return timeAt(time.size() - 1); + } + + /** + * Retrieves the signal value corresponding to the index {@code index}. Is + * overriden by subclasses. + * + * @param index the index of the element + * @return a double, representing the signal at {@code index} + */ + public double signalAt(int index) { + return signal.get(index); + } + + /** + * Adds a time-signal pair to the lists. + * + * @param time the time value + * @param sgn the signal value at {@code time} + */ + public void addPoint(double time, double sgn) { + this.time.add(time); + this.signal.add(sgn); + } + + protected final void incrementCount() { + count++; + } + + /** + * Sets the time {@code t} at the position {@code index} of the + * {@code time List}. + * + * @param index the index + * @param t the new time value at this index + */ + public final void setTimeAt(int index, double t) { + time.set(index, t); + } + + /** + * Sets the signal {@code t} at the position {@code index} of the + * {@code signal List}. + * + * @param index the index + * @param t the new signal value at this index + */ + public final void setSignalAt(int index, double t) { + signal.set(index, t); + } + + /** + * Calculates the simple maximum signal. + * + * @return the maximum signal value + * @see java.util.Collections.max + */ + public final double apparentMaximum() { + return max(signal); + } + + @Override + public String toString() { + return name != null ? name : getClass().getSimpleName() + " (" + getNumPoints() + ")"; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * Provides general setter accessibility for the number of points of this + * {@code AbstractData}. + * + * @param type must be equal to {@code NumericPropertyKeyword.NUMPOINTS} + * @param property the property of the type + * {@code NumericPropertyKeyword.NUMPOINTS} + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NUMPOINTS) { + setNumPoints(property); + } + } + + /** + * Lists {@code NUM_POINTS} as an accessible property of this + * {@code PropertyHolder}. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(NUMPOINTS); + return set; + } + + /** + * Removes a time-value pair that is present under the index {@code i}. + * + * @param i the element to be removed + */ + public void remove(int i) { + this.time.remove(i); + this.signal.remove(i); + } + + /** + * @return true + */ + @Override + public boolean ignoreSiblings() { + return true; + } + + public boolean isFull() { + return actualNumPoints() >= count; + } + + public List getTimeSequence() { + return time; + } + + public List getSignalData() { + return signal; + } + + /** + * @return {@code true} only if {@code o} is an {@code AbstractData} + * containing all the elements of the time and signal lists of {@code this} + * object. + */ + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof AbstractData)) { + return false; + } + + var other = (AbstractData) o; + + final double EPS = 1e-8; + + if (abs(count - (Integer) other.getNumPoints().getValue()) > EPS) { + return false; + } + + if (signal.hashCode() != other.signal.hashCode()) { + return false; + } + + if (time.hashCode() != other.time.hashCode()) { + return false; + } + + return time.containsAll(other.time) && signal.containsAll(other.signal); + + } + +} diff --git a/src/main/java/pulse/DiscreteInput.java b/src/main/java/pulse/DiscreteInput.java new file mode 100644 index 00000000..3b29c1f7 --- /dev/null +++ b/src/main/java/pulse/DiscreteInput.java @@ -0,0 +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 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++) { + 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())); + } + +} diff --git a/src/main/java/pulse/HeatingCurve.java b/src/main/java/pulse/HeatingCurve.java index 4fa1f019..f6823062 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; @@ -10,308 +12,414 @@ 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; 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.properties.Property; +import pulse.util.FunctionSerializer; /** - * The {@code HeatingCurve} represents a time-temperature profile (a {@code AbstractData} instance) - * generated using a finite-difference calculation algorithm. + * The {@code HeatingCurve} represents a time-temperature profile (a + * {@code AbstractData} instance) generated using a calculation algorithm + * implemented by a {@code Problem}'s {@code Solver}. In addition to the time + * and signal lists defined in the super-class, it features baseline-corrected + * signal values stored in a separate list.The {@code HeatingCurve} may have + * {@code HeatingCurveListener}s to process simple events. To enable comparison + * with {@code ExperimentalData}, a {@code HeatingCurve} builds a spline + * interpolation of its time - baseline-adjusted signal values, thus + * representing a continuous curve, rather than just a collection of discrete + * data. + * + * @see pulse.HeatingCurveListener + * @see org.apache.commons.math3.analysis.interpolation.UnivariateInterpolation * */ - public class HeatingCurve extends AbstractData { - private List adjustedSignal; - private double startTime; - - private List listeners = new ArrayList(); - - private UnivariateInterpolator splineInterpolator; - private UnivariateFunction splineInterpolation; - - protected HeatingCurve(List time, List signal, final double startTime, String name) { - super(time, name); - this.adjustedSignal = signal; - this.startTime = startTime; - } - - public HeatingCurve() { - super(); - adjustedSignal = new ArrayList((int)this.getNumPoints().getValue()); - splineInterpolator = new SplineInterpolator(); - } - - /** - * Creates a {@code HeatingCurve}, where the number of elements in the - * {@code time} and {@code temperature} collections are set to - * {@code count.getValue()}. - * - * @param count The {@code NumericProperty} that is derived from the - * {@code NumericPropertyKeyword.NUMPOINTS}. - */ - - public HeatingCurve(NumericProperty count) { - super(count); - setPrefix("Solution"); - - adjustedSignal = new ArrayList<>((int)count.getValue()); - startTime = (double) def(TIME_SHIFT).getValue(); - - splineInterpolator = new SplineInterpolator(); - } - - @Override - public void clear() { - super.clear(); - this.adjustedSignal.clear(); - } - - /** - * Retrieves the time from the stored list of values, adding the value of {@code startTime} to the result - * - */ - - @Override - public double timeAt(int index) { - return super.timeAt(index) + startTime; - } - - /** - * Retrieves the baseline-subtracted temperature corresponding to - * {@code index} in the respective {@code List}. - * - * @param index the index of the element - * @return a double, respresenting the baseline-subtracted temperature at - * {@code index} - */ - - public double signalAt(int index) { - return adjustedSignal.get(index); - } - - /** - * Scales the temperature values by a factor of {@code scale}. - *

- * This is done by manually setting each temperature value to {@code T*scale}, - * where T is the current temperature value at this index. Finally. applies the - * baseline to the scaled temperature values. - *

- * This method is used in the DifferenceScheme classes when a dimensionless - * solution needs to be re-scaled to the given maximum temperature (usually - * matching the {@code ExperimentalData}, but also used as a search variable by - * the {@code SearchTask}. - * - * @param scale the scale - * @see pulse.problem.schemes.DifferenceScheme - * @see pulse.problem.statements.Problem - * @see pulse.tasks.SearchTask - */ - - public void scale(double scale) { - var signal = getSignalData(); - final int count = this.actualNumPoints(); - for (int i = 0; i < count; i++) - signal.set(i, signal.get(i) * scale); - var dataEvent = new CurveEvent(RESCALED, this); - fireCurveEvent(dataEvent); - } - - private void refreshInterpolation() { - - /* - * Prepare extended time array - */ - - var time = this.getTimeSequence(); - var timeExtended = new double[time.size() + 1]; - - for (int i = 1; i < timeExtended.length; i++) - timeExtended[i] = timeAt(i - 1); - - final double dt = timeExtended[2] - timeExtended[1]; - timeExtended[0] = timeExtended[1] - dt; // extrapolate linearly - - /* + /** + * + */ + private static final long serialVersionUID = 7071147065094996971L; + private List adjustedSignal; + private List lastCalculation; + private double startTime; + + private List listeners; + + 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<>(); + } + + /** + * Calls the super-constructor and initialises the baseline-corrected signal + * list. Creates a {@code SplineInterpolator} object. + */ + public HeatingCurve() { + super(); + adjustedSignal = new ArrayList<>((int) this.getNumPoints().getValue()); + interpolator = new SplineInterpolator(); + } + + /** + * Copy constructor. In addition to copying the data, also re-builds the + * splines. + * + * @param c another instance of this class + * @see refreshInterpolation() + */ + public HeatingCurve(HeatingCurve c) { + super(c); + this.adjustedSignal = new ArrayList<>(c.adjustedSignal); + this.startTime = c.startTime; + interpolator = new SplineInterpolator(); + if (c.interpolation != null) { + this.refreshInterpolation(); + } + } + + /** + * Creates a {@code HeatingCurve}, where the number of elements in the + * {@code time}, {@code signal}, and {@code adjustedSignal} collections are + * set to {@code count.getValue()}. The time shift is initialized with a + * default value. + * + * @param count The {@code NumericProperty} that is derived from the + * {@code NumericPropertyKeyword.NUMPOINTS}. + */ + public HeatingCurve(NumericProperty count) { + super(count); + setPrefix("Solution"); + + adjustedSignal = new ArrayList<>((int) count.getValue()); + startTime = (double) def(TIME_SHIFT).getValue(); + + interpolator = new SplineInterpolator(); + } + + //TODO + public void copyToLastCalculation() { + lastCalculation = new ArrayList<>(0); + lastCalculation = new ArrayList<>(adjustedSignal); + } + + @Override + public void clear() { + super.clear(); + adjustedSignal.clear(); + } + + /** + * Retrieves the time from the stored list of values, adding the value of + * {@code startTime} to the result. + * + * @return time at {@code index} + startTime + * + */ + @Override + public double timeAt(int index) { + return super.timeAt(index) + startTime; + } + + /** + * Retrieves the baseline-corrected temperature corresponding to + * {@code index} in the respective {@code List}. + * + * @param index the index of the element + * @return a double, representing the baseline-corrected temperature at + * {@code index} + */ + @Override + public double signalAt(int index) { + return adjustedSignal.get(index); + } + + /** + * Scales the temperature values by a factor of {@code scale}. + *

+ * This is done by manually setting each temperature value to + * {@code T*scale}, where T is the current temperature value at this index. + * Finally. applies the baseline to the scaled temperature values. + *

+ * This method is used in the DifferenceScheme subclasses when a + * dimensionless solution needs to be re-scaled to the given maximum + * temperature (usually matching the {@code ExperimentalData}, but also used + * as a search variable by the {@code SearchTask}. + *

+ * Triggers a {@code RESCALED} {@code CurveEvent}. + *

+ * + * @param scale the scale + * @see pulse.problem.schemes.DifferenceScheme + * @see pulse.problem.statements.Problem + * @see pulse.tasks.SearchTask + * @see pulse.input.listeners.CurveEvent + */ + public void scale(double scale) { + final int count = this.actualNumPoints(); + 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); + fireCurveEvent(dataEvent); + } + + private void refreshInterpolation() { + + /* + * Prepare extended time array + */ + var timeExtended = new double[time.size() + 1]; + + for (int i = 1; i < timeExtended.length; i++) { + timeExtended[i] = timeAt(i - 1); + } + + final double dt = timeExtended[2] - timeExtended[1]; + timeExtended[0] = timeExtended[1] - dt; // extrapolate linearly + + /* * Prepare extended signal array - */ - - var adjustedSignalExtended = new double[adjustedSignal.size() + 1]; - - for (int i = 1; i < timeExtended.length; i++) - adjustedSignalExtended[i] = signalAt(i - 1); - - final double alpha = -1.0; - adjustedSignalExtended[0] = alpha * adjustedSignalExtended[2] - (1.0 - alpha) * adjustedSignalExtended[1]; // extrapolate - // linearly - - /* - * Submit to spline interpolation - */ - - splineInterpolation = splineInterpolator.interpolate(timeExtended, adjustedSignalExtended); - } - - /** - * Retrieves the absolute maximum (in arbitrary untis) of the - * baseline-subtracted temperature list. - * - * @return the absolute maximum of the baseline-adjusted temperature. - */ - - public double maxAdjustedSignal() { - return max(adjustedSignal); - } - - /** - * Subtracts the baseline values from each element of the {@code temperature} - * list. - *

- * The baseline.valueAt(...) is explicitly invoked for all {@code time} values, - * and the result of subtracting the baseline value from the corresponding - * {@code temperature} is assigned to a position in the - * {@code baselineAdjustedTemperature} list. - *

- * - * @param baseline the baseline. Note it may not specifically belong to this - * heating curve. - */ - - public void apply(Baseline baseline) { - var time = this.getTimeSequence(); - var signal = this.getSignalData(); - adjustedSignal.clear(); - for (int i = 0, size = time.size(); i < size; i++) - adjustedSignal.add(signal.get(i) + baseline.valueAt(timeAt(i))); - - if (time.get(0) > -startTime) { - time.add(0, -startTime); - adjustedSignal.add(0, baseline.valueAt(-startTime)); - } - - refreshInterpolation(); - } - - /** - * This creates a new {@code HeatingCurve} to match the time boundaries of the - * {@code data}. - *

- * Curves derived in this way are called extended and are used primarily - * to visually inspect how the calculated baseline correlates with the - * {@code data} at times {@code t < 0}. This method is not used in any - * calculation and is introduced primarily because the search for the reverse - * solution of the heat problems only regards time value at - * t0, whereas in reality it may - * not be consistent with the experimental baseline value at {@code t < 0}. - *

- * - * @param data the experimental data, with a time range broader than the time - * range of this {@code HeatingCurve}. - * @return a new {@code HeatingCurve}, extended to match the time limits of - * {@code data} - */ - - public final HeatingCurve extendedTo(ExperimentalData data, Baseline baseline) { - - int dataStartIndex = data.getIndexRange().getLowerBound(); - - if (dataStartIndex < 1) // no extension required - return this; - - var baselineTime = data.getTimeSequence().stream().filter(t -> t < 0).collect(toList()); - var baselineSignal = baselineTime.stream().map(bTime -> baseline.valueAt(bTime)).collect(toList()); - - var time = this.getTimeSequence(); - - baselineTime.addAll(time); - baselineSignal.addAll(adjustedSignal); - - return new HeatingCurve(baselineTime, baselineSignal, startTime, getName()); - } - - /** - * Provides general setter accessibility for the number of points of this - * {@code HeatingCurve}. - * - * @param type must be equal to {@code NumericPropertyKeyword.NUMPOINTS} - * @param property the property of the type - * {@code NumericPropertyKeyword.NUMPOINTS} - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - if (type == TIME_SHIFT) - setTimeShift(property); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(TIME_SHIFT)); - return list; - } - - /** - * Removes an element with the index {@code i} from all three {@code List}s - * (time, temperature, and baseline-subtracted temperature). - * - * @param i the element to be removed - */ - - public void remove(int i) { - super.remove(i); - this.adjustedSignal.remove(i); - } - - public NumericProperty getTimeShift() { - return derive(TIME_SHIFT, startTime); - } - - public void setTimeShift(NumericProperty startTime) { - requireType(startTime, TIME_SHIFT); - this.startTime = (double) startTime.getValue(); - var dataEvent = new CurveEvent(TIME_ORIGIN_CHANGED, this); - fireCurveEvent(dataEvent); - firePropertyChanged(this, startTime); - } - - public UnivariateFunction getSplineInterpolation() { - return splineInterpolation; - } - - public List getAlteredSignalData() { - return adjustedSignal; - } - - public void addHeatingCurveListener(HeatingCurveListener l) { - this.listeners.add(l); - } - - public void removeHeatingCurveListeners() { - listeners.clear(); - } - - private void fireCurveEvent(CurveEvent event) { - for (HeatingCurveListener l : listeners) - l.onCurveEvent(event); - } - - @Override - public boolean equals(Object o) { - if(! (o instanceof HeatingCurve )) - return false; - - return super.equals(o) && adjustedSignal.containsAll( ((HeatingCurve)o).adjustedSignal); - } - -} \ No newline at end of file + */ + var adjustedSignalExtended = new double[adjustedSignal.size() + 1]; + + for (int i = 1; i < timeExtended.length; i++) { + adjustedSignalExtended[i] = signalAt(i - 1); + } + + final double alpha = -1.0; + adjustedSignalExtended[0] = alpha * adjustedSignalExtended[2] + - (1.0 - alpha) * adjustedSignalExtended[1]; // extrapolate + // linearly + + /* + * Submit to spline interpolation + */ + interpolation = interpolator.interpolate(timeExtended, adjustedSignalExtended); + } + + /** + * Retrieves the simple maximum (in arbitrary units) of the + * baseline-corrected temperature list. + * + * @return the simple maximum of the baseline-adjusted temperature. + */ + public double maxAdjustedSignal() { + return max(adjustedSignal); + } + + /** + * Adds the baseline value to each element of the {@code signal} list. + *

+ * The {@code baseline.valueAt} method is explicitly invoked for all + * {@code time} values, and the result of adding the baseline value to the + * corresponding {@code signal} is assigned to a position in the + * {@code adjustedSignal} list. + *

+ * + * @param baseline the baseline. Note it may not specifically belong to this + * heating curve. + */ + public void apply(Baseline baseline) { + adjustedSignal.clear(); + int size = time.size(); + + if (size > 0) { + + for (int i = 0; i < size; i++) { + adjustedSignal.add(signal.get(i) + baseline.valueAt(timeAt(i))); + } + + if (time.get(0) > -startTime) { + time.add(0, -startTime); + adjustedSignal.add(0, baseline.valueAt(-startTime)); + } + + refreshInterpolation(); + + } + + } + + /** + * This creates a new {@code HeatingCurve} to match the time boundaries of + * the {@code data}. + *

+ * Curves derived in this way are called extended and are used + * primarily to visually inspect how the calculated baseline correlates with + * the {@code data} at times {@code t < 0}. This method is not used in any + * calculation and is introduced primarily because the search for the + * reverse solution of the heat problems only regards time value at + * t0, whereas in reality it + * may not be consistent with the experimental baseline value at + * {@code t < 0}. + *

+ * + * @param data the experimental data, with a time range broader than the + * time range of this {@code HeatingCurve}. + * @param baseline + * @return a new {@code HeatingCurve}, extended to match the time limits of + * {@code data} + */ + public final HeatingCurve extendedTo(ExperimentalData data, Baseline baseline) { + + int dataStartIndex = data.getIndexRange().getLowerBound(); + + if (dataStartIndex < 1) // no extension required + { + return this; + } + + var baselineTime = data.getTimeSequence().stream().filter(t -> t < 0).collect(toList()); + var baselineSignal = baselineTime.stream().map(bTime -> baseline.valueAt(bTime)).collect(toList()); + + baselineTime.addAll(time); + this.copyToLastCalculation(); + baselineSignal.addAll(lastCalculation); + + return new HeatingCurve(baselineTime, baselineSignal, startTime, getName()); + } + + /** + * Calls {@code super.set} and provides write access to the + * {@code TIME_SHIFT} property. + * + * @param property the property of the type + * {@code NumericPropertyKeyword.NUMPOINTS} + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if (type == TIME_SHIFT) { + setTimeShift(property); + } + } + + /** + * @return {@code TIME_SHIFT} and {@code NUM_POINTS}. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(TIME_SHIFT); + return set; + } + + /** + * Removes an element with the index {@code i} from all three {@code List}s + * (time, signal, and baseline-corrected signal). + * + * @param i the element to be removed + */ + @Override + public void remove(int i) { + super.remove(i); + this.adjustedSignal.remove(i); + } + + /** + * The time shift is the position of the 'zero-time'. + * + * @return a {@code TIME_SHIFT} property + */ + public NumericProperty getTimeShift() { + return derive(TIME_SHIFT, startTime); + } + + /** + * Sets the time shift and triggers {@code TIME_ORIGIN_CHANGED} in + * {@code CurveEvent}. Triggers the {@code firePropertyChanged}. + * + * @param startTime the new start time value + */ + public void setTimeShift(NumericProperty startTime) { + requireType(startTime, TIME_SHIFT); + this.startTime = (double) startTime.getValue(); + var dataEvent = new CurveEvent(TIME_ORIGIN_CHANGED); + fireCurveEvent(dataEvent); + firePropertyChanged(this, startTime); + } + + public UnivariateFunction getInterpolation() { + return interpolation; + } + + public List getBaselineCorrectedData() { + return adjustedSignal; + } + + public void addHeatingCurveListener(HeatingCurveListener l) { + if (listeners == null) { + listeners = new ArrayList<>(); + } + this.listeners.add(l); + } + + @Override + public void removeListeners() { + listeners.clear(); + } + + private void fireCurveEvent(CurveEvent event) { + for (HeatingCurveListener l : listeners) { + l.onCurveEvent(event); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof HeatingCurve)) { + return false; + } + + 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)); + } + + /* + * 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 3c0d6a18..12d80648 100644 --- a/src/main/java/pulse/HeatingCurveListener.java +++ b/src/main/java/pulse/HeatingCurveListener.java @@ -1,18 +1,19 @@ 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 extends Serializable { -public interface HeatingCurveListener { + /** + * Signals that a {@code CurveEvent} has occurred. + * + * @param event + */ + public void onCurveEvent(CurveEvent event); - /** - * Signals that the {@code HeatingCurve} has been rescaled. - */ - - public void onCurveEvent(CurveEvent event); - -} \ 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 00000000..e138950d --- /dev/null +++ b/src/main/java/pulse/Response.java @@ -0,0 +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 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. + * + * @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(); + +} diff --git a/src/main/java/pulse/baseline/AdjustableBaseline.java b/src/main/java/pulse/baseline/AdjustableBaseline.java new file mode 100644 index 00000000..12464c76 --- /dev/null +++ b/src/main/java/pulse/baseline/AdjustableBaseline.java @@ -0,0 +1,152 @@ +package pulse.baseline; + +import java.util.List; +import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; +import java.util.Set; +import pulse.math.Parameter; + +import pulse.math.ParameterVector; +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; + +/** + * A baseline that can shift in the vertical direction. + * + * @author Artem Lunev + */ +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, double slope) { + this.intercept = intercept; + this.slope = 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) { + return intercept + x * slope; + } + + protected double mean(List x) { + return x.stream().mapToDouble(d -> d).average().getAsDouble(); + } + + /** + * Provides getter accessibility to the intercept as a NumericProperty + * + * @return a NumericProperty derived from + * NumericPropertyKeyword.BASELINE_INTERCEPT where the value is set to that + * of {@code slope} + */ + public NumericProperty getIntercept() { + return derive(BASELINE_INTERCEPT, intercept); + } + + /** + * Checks whether {@code intercept} is a baseline intercept property and + * updates the respective value of this baseline. + * + * @param intercept a {@code NumericProperty} of the + * {@code BASELINE_INTERCEPT} type + * @see set + */ + public void setIntercept(NumericProperty intercept) { + requireType(intercept, BASELINE_INTERCEPT); + this.intercept = (double) intercept.getValue(); + 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}. + * + * @see PropertyHolder + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(BASELINE_INTERCEPT); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == BASELINE_INTERCEPT) { + setIntercept(property); + this.firePropertyChanged(this, property); + } + } + + @Override + public void optimisationVector(ParameterVector output) { + for (Parameter p : output.getParameters()) { + + if (p != null) { + + var key = p.getIdentifier().getKeyword(); + + if (key == BASELINE_INTERCEPT) { + p.setValue(intercept); + } + + } + + } + + } + + @Override + public void assign(ParameterVector params) { + for (Parameter p : params.getParameters()) { + + 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 37bb2796..29a88865 100644 --- a/src/main/java/pulse/baseline/Baseline.java +++ b/src/main/java/pulse/baseline/Baseline.java @@ -1,14 +1,12 @@ 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.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; @@ -19,89 +17,68 @@ * in time (either before or after the laser pulse). The baseline parameters can * be modified within an optimisation loop, hence there are two abstract methods * to implement that functionality. - * + * * @see pulse.HeatingCurve * @see pulse.tasks.SearchTask - * @see pulse.math.IndexedVector + * @see pulse.math.ParameterVector */ - public abstract class Baseline extends PropertyHolder implements Reflexive, Optimisable { - /** - * Calculates the baseline at the given position. - * - * @param x the position on the profile (e.g., the time value) - * @return the baseline value - */ - - public abstract double valueAt(double x); - - /** - * Calculates the baseline parameters based on input arguments. - *

- * This will run a simple least-squares estimation of the parameters of this - * baseline using the specified {@code data} within the time range - * {@code rangeMin < t < rangeMax}. If no data is available, the method will NOT - * change the {@code intercept} and {@code slope} values. Upon completion, the - * method will use the respective {@code set} methods of this class to update - * the parameter 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 - */ - - 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); - - } - - /** - * Calls {@code fitTo} using the default time range for the data: - * {@code -Infinity < t < ZERO_LEFT}, where the upper bound is - * a small negative constant. - * - * @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); - } - -} \ No newline at end of file + public final static int MIN_BASELINE_POINTS = 15; + + public abstract Baseline copy(); + + /** + * Calculates the baseline at the given position. + * + * @param x the position on the profile (e.g., the time value) + * @return the baseline value + */ + public abstract double valueAt(double x); + + /** + * Calculates the baseline parameters based on input arguments. + *

+ * This usually runs a simple least-squares estimation of the parameters of + * this baseline using the specified {@code data} within the time range + * {@code rangeMin < t < rangeMax}. If no data is available, the method will + * NOT change the baseline parameters. Upon completion, the method will use + * the respective {@code set} methods of this class to update the parameter + * values, triggering whatever events are associated with them. + *

+ * + * @param x + * @param y + */ + protected abstract void doFit(List x, List y); + + /** + * Calls {@code fitTo} using the default time range for the data: + * {@code -Infinity < t < ZERO_LEFT}, where the upper bound is a small + * negative constant. + * + * @param data the experimental data stretching to negative time values + * @see fitTo(ExperimentalData,double,double) + */ + 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 = 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"; + } + +} diff --git a/src/main/java/pulse/baseline/FlatBaseline.java b/src/main/java/pulse/baseline/FlatBaseline.java index a556280f..1ca0743b 100644 --- a/src/main/java/pulse/baseline/FlatBaseline.java +++ b/src/main/java/pulse/baseline/FlatBaseline.java @@ -1,148 +1,48 @@ package pulse.baseline; import static java.lang.String.format; +import java.util.List; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import pulse.math.IndexedVector; -import pulse.properties.Flag; -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; -import pulse.util.PropertyHolder; - /** - * A simple constant baseline with no slope. The intercept value can be used as - * an optimisation variable. - * + * A flat baseline. */ - -public class FlatBaseline extends Baseline { - - private double intercept; - - /** - * A primitive constructor, which initialises a {@code CONSTANT} baseline with - * zero intercept and slope. - */ - - public FlatBaseline() { - // intentionally blank - } - - /** - * Creates a flat baseline equal to the argument. - * - * @param intercept the constant baseline value. - */ - - public FlatBaseline(double intercept) { - this.intercept = intercept; - } - - /** - * @return the constant value of this {@code FlatBaseline} - */ - - @Override - public double valueAt(double x) { - return intercept; - } - - @Override - protected void doFit(List x, List y, int size) { - intercept = mean(y); - set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); - } - - protected double mean(List x) { - double sum = 0.0; - final double len = x.size(); - for (int i = 0; i < len; i++) { - sum += x.get(i); - } - return sum / len; - } - - /** - * Provides getter accessibility to the intercept as a NumericProperty - * - * @return a NumericProperty derived from - * NumericPropertyKeyword.BASELINE_INTERCEPT where the value is set to - * that of {@code slope} - */ - - public NumericProperty getIntercept() { - return derive(BASELINE_INTERCEPT, intercept); - } - - /** - * Checks whether {@code intercept} is a baseline intercept property and updates - * the respective value of this baseline. - * - * @param intercept a {@code NumericProperty} of the {@code BASELINE_INTERCEPT} - * type - * @see set - */ - - public void setIntercept(NumericProperty intercept) { - requireType(intercept, BASELINE_INTERCEPT); - this.intercept = (double) intercept.getValue(); - firePropertyChanged(this, intercept); - } - - /** - * Lists the {@code intercept} as accessible property for this - * {@code FlatBaseline}. - * - * @see PropertyHolder - */ - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(getIntercept())); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " = " + format("%3.2f", intercept); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == BASELINE_INTERCEPT) { - setIntercept(property); - this.firePropertyChanged(this, property); - } - } - - @Override - public void optimisationVector(IndexedVector[] output, List flags) { - for (int i = 0, size = output[0].dimension(); i < size; i++) { - - if (output[0].getIndex(i) == BASELINE_INTERCEPT) { - output[0].set(i, intercept); - output[1].set(i, 5); - } - - } - - } - - @Override - public void assign(IndexedVector params) { - for (int i = 0, size = params.dimension(); i < size; i++) { - - if (params.getIndex(i) == BASELINE_INTERCEPT) - setIntercept(derive(BASELINE_INTERCEPT, params.get(i))); - - } - - } - -} \ No newline at end of file +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. + */ + public FlatBaseline() { + this(0.0); + } + + /** + * Creates a flat baseline equal to the argument. + * + * @param intercept the constant baseline value. + */ + public FlatBaseline(double intercept) { + super(intercept, 0.0); + } + + @Override + protected void doFit(List x, List y) { + double intercept = mean(y); + set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); + } + + @Override + public Baseline copy() { + return new FlatBaseline((double) getIntercept().getValue()); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " = " + format("%3.2f", getIntercept().getValue()); + } + +} diff --git a/src/main/java/pulse/baseline/LinearBaseline.java b/src/main/java/pulse/baseline/LinearBaseline.java index 57f817f7..a04354d8 100644 --- a/src/main/java/pulse/baseline/LinearBaseline.java +++ b/src/main/java/pulse/baseline/LinearBaseline.java @@ -2,17 +2,16 @@ import static java.lang.String.format; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; import static pulse.properties.NumericPropertyKeyword.BASELINE_SLOPE; import java.util.List; +import java.util.Set; +import pulse.math.Parameter; -import pulse.math.IndexedVector; -import pulse.properties.Flag; +import pulse.math.ParameterVector; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; +import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; /** * A linear {@code Baseline} which specifies the {@code intercept} and @@ -23,164 +22,130 @@ * associated with the {@code intercept} and {@code slope} parameters can be * used as fitting variables. *

- * + * * @see pulse.HeatingCurve * @see pulse.tasks.SearchTask - * @see pulse.math.IndexedVector + * @see pulse.math.ParameterVector */ - -public class LinearBaseline extends FlatBaseline { - - private double slope; - - /** - * A primitive constructor, which initialises a {@code CONSTANT} baseline with - * zero intercept and slope. - */ - - public LinearBaseline() { - super(); - } - - /** - * 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; - } - - /** - * 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; - } - - @Override - protected void doFit(List x, List y, int size) { - double meanx = mean(x); - double meany = mean(y); - - double x1; - double y1; - double xxbar = 0.0; - double xybar = 0.0; - - for (int i = 0; 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 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); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - if (type == BASELINE_SLOPE) { - setSlope(property); - this.firePropertyChanged(this, property); - } - - } - - @Override - public void optimisationVector(IndexedVector[] output, List flags) { - super.optimisationVector(output, flags); - - for (int i = 0, size = output[0].dimension(); i < size; i++) { - - if (output[0].getIndex(i) == BASELINE_SLOPE) { - output[0].set(i, slope); - output[1].set(i, 1000); - } - - } - - } - - /** - * Assigns parameter values of this {@code Problem} using the optimisation - * vector {@code params}. Only those parameters will be updated, the types of - * which are listed as indices in the {@code params} vector. - * - * @param params the optimisation vector, containing a similar set of parameters - * to this {@code Problem} - * @see listedTypes() - */ - - @Override - public void assign(IndexedVector params) { - super.assign(params); - - for (int i = 0, size = params.dimension(); i < size; i++) { - - if (params.getIndex(i) == BASELINE_SLOPE) - setSlope(derive(BASELINE_SLOPE, params.get(i))); - - } - - } - - /** - * @return a list containing {@code BASELINE_INTERCEPT} and - * {@code BASELINE_SLOPE} properties - */ - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(getSlope()); - return list; - } +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. + */ + 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() + ); + } + + @Override + protected void doFit(List x, List y) { + double meanx = mean(x); + double meany = mean(y); + + double x1; + double y1; + double xxbar = 0.0; + double xybar = 0.0; + + 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); + } + + double slope = xybar / xxbar; + double intercept = meany - slope * meanx; + + set(BASELINE_INTERCEPT, derive(BASELINE_INTERCEPT, intercept)); + set(BASELINE_SLOPE, derive(BASELINE_SLOPE, slope)); + } + + @Override + public String toString() { + var slope = getSlope().getValue(); + return getClass().getSimpleName() + " = " + + format("%3.2f + t * ( %3.2f )", getIntercept().getValue(), slope); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if (type == BASELINE_SLOPE) { + setSlope(property); + this.firePropertyChanged(this, property); + } + + } + + @Override + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + + for (Parameter p : output.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + + if (key == BASELINE_SLOPE) { + double slope = (double) getSlope().getValue(); + p.setValue(slope); + } + + } + + } + + /** + * Assigns parameter values of this {@code Problem} using the optimisation + * vector {@code params}. Only those parameters will be updated, the types + * of which are listed as indices in the {@code params} vector. + * + * @param params the optimisation vector, containing a similar set of + * parameters to this {@code Problem} + * @see listedTypes() + */ + @Override + public void assign(ParameterVector params) { + super.assign(params); + + for (Parameter p : params.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + + if (key == BASELINE_SLOPE) { + setSlope(derive(BASELINE_SLOPE, p.inverseTransform())); + } + + } + + } + + /** + * @return a set containing {@code BASELINE_INTERCEPT} and + * {@code BASELINE_SLOPE} keywords + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(BASELINE_SLOPE); + return set; + } + + @Override + public Baseline copy() { + 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 a9ab943e..94b92c9f 100644 --- a/src/main/java/pulse/baseline/SinusoidalBaseline.java +++ b/src/main/java/pulse/baseline/SinusoidalBaseline.java @@ -1,172 +1,400 @@ package pulse.baseline; -import static java.lang.Math.sin; -import static java.lang.Math.sqrt; -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 pulse.math.IndexedVector; +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.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 pulse.properties.Property; +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 LinearBaseline { + + private static final long serialVersionUID = -6858521208790195992L; + private List hiFreq; + private List loFreq; + private List active; + + 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, 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) { + return super.valueAt(x) + + active.stream().mapToDouble(h -> h.valueAt(x)).sum(); + } + + @Override + 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 optimisationVector(ParameterVector output) { + super.optimisationVector(output); + active.forEach(h -> h.optimisationVector(output)); + } + + @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)); + } + + 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 new ArrayList<>(tmp.subList(0, Math.min(tmp.size(), limit))); + } + + private void labelActive() { + for (int i = 0, size = active.size(); i < size; i++) { + active.get(i).setRank(i); + active.get(i).setParent(this); + } + } + + private void fitHarmonics(DiscreteInput input) { + + var sos = new SumOfSquares() { + + @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); + } + ); + + } + + /** + * @return a set containing {@code BASELINE_INTERCEPT} and + * {@code BASELINE_SLOPE} keywords + */ + @Override + 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)); + } + + 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 = new ArrayList<>(active.subList(0, maxHighFreqHarmonics)); + active.addAll(this.sort(loFreq, maxLowFreqHarmonics)); + this.labelActive(); + this.firePropertyChanged(this, maxHarmonics); + } + } + + @Override + 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 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(); + } + + 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); + } + } -public class SinusoidalBaseline extends FlatBaseline { - - private double frequency; - private double phaseShift; - private double amplitude; - private final static double _2PI = 2.0 * Math.PI; - - /** - * Creates a sinusoidal baseline with default properties. - */ - - public SinusoidalBaseline() { - setFrequency(def(BASELINE_FREQUENCY)); - setAmplitude(def(BASELINE_AMPLITUDE)); - setPhaseShift(def(BASELINE_PHASE_SHIFT)); - } - - @Override - public double valueAt(double x) { - var intercept = (double) getIntercept().getValue(); - return intercept + amplitude * sin(_2PI * (x * frequency + phaseShift)); - } - - /** - * Listed properties include the frequency, amplitude, phase shift, and - * intercept. - */ - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(def(BASELINE_FREQUENCY)); - list.add(def(BASELINE_AMPLITUDE)); - list.add(def(BASELINE_PHASE_SHIFT)); - return list; - } - - @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; - default: - break; - } - - } - - 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 void setFrequency(NumericProperty frequency) { - requireType(frequency, BASELINE_FREQUENCY); - this.frequency = (double) frequency.getValue(); - firePropertyChanged(this, frequency); - } - - public void setAmplitude(NumericProperty amplitude) { - requireType(amplitude, BASELINE_AMPLITUDE); - this.amplitude = (double) amplitude.getValue(); - firePropertyChanged(this, amplitude); - } - - public void setPhaseShift(NumericProperty phaseShift) { - requireType(phaseShift, BASELINE_PHASE_SHIFT); - this.phaseShift = (double) phaseShift.getValue(); - firePropertyChanged(this, phaseShift); - } - - @Override - public void optimisationVector(IndexedVector[] output, List flags) { - super.optimisationVector(output, flags); - - for (int i = 0, size = output[0].dimension(); i < size; i++) { - - switch (output[0].getIndex(i)) { - case BASELINE_FREQUENCY: - output[0].set(i, frequency); - output[1].set(i, 30); - break; - case BASELINE_PHASE_SHIFT: - output[0].set(i, phaseShift); - output[1].set(i, 1.0); - break; - case BASELINE_AMPLITUDE: - output[0].set(i, sqrt(amplitude)); - output[1].set(i, 1.0); - break; - default: - break; - } - - } - - } - - @Override - public void assign(IndexedVector 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.get(i))); - break; - case BASELINE_PHASE_SHIFT: - setPhaseShift(derive(BASELINE_PHASE_SHIFT, params.get(i))); - break; - case BASELINE_AMPLITUDE: - var p = params.get(i); - setAmplitude(derive(BASELINE_AMPLITUDE, p*p)); - break; - default: - break; - } - - } - - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/baseline/package-info.java b/src/main/java/pulse/baseline/package-info.java index 20542d43..103a7c31 100644 --- a/src/main/java/pulse/baseline/package-info.java +++ b/src/main/java/pulse/baseline/package-info.java @@ -2,5 +2,4 @@ * Contains classes for describing and evaluating the baseline signal of a * {@code HeatingCurve} or its subclasses. */ - -package pulse.baseline; \ No newline at end of file +package pulse.baseline; diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 63c486ef..4be76e71 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -1,26 +1,20 @@ package pulse.input; -import static java.lang.Double.valueOf; -import static java.util.Collections.max; -import static pulse.input.listeners.DataEventType.TRUNCATED; 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.properties.NumericProperty; -import pulse.ui.Messages; +import pulse.math.filters.HalfTimeCalculator; import pulse.util.PropertyHolderListener; /** @@ -31,389 +25,290 @@ * {@code CurveReader}s. Any manipulation (e.g. truncation) of the data triggers * an event associated with this {@code ExperimentalData}. */ - -public class ExperimentalData extends AbstractData { - - private Metadata metadata; - private IndexRange indexRange; - private Range range; - private List dataListeners; - - /** - * This is the cutoff factor which is used as a criterion for data truncation. - * Described in Lunev, A., & Heymer, R. (2020). Review of Scientific - * Instruments, 91(6), 064902. - */ - - 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; - - /** - * A fail-safe factor. - */ - - public final static double FAIL_SAFE_FACTOR = 3.0; - - private static Comparator pointComparator = (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); - - /** - * Constructs an {@code ExperimentalData} object using the superclass - * constructor and rejecting the responsibility for the {@code baseline}, making - * its parent {@code null}. The number of points is set to zero by default. - * - */ - - public ExperimentalData() { - super(); - dataListeners = new ArrayList<>(); - setPrefix("RawData"); - setNumPoints(derive(NUMPOINTS, 0)); - indexRange = new IndexRange(); - } - - public void addDataListener(DataListener listener) { - dataListeners.add(listener); - } - - public void clearDataListener() { - dataListeners.clear(); - } - - public void fireDataChanged(DataEvent dataEvent) { - dataListeners.stream().forEach(l -> l.onDataChanged(dataEvent)); - } - - /** - * Calls reset for both the {@code IndexRange} and {@code Range} objects using - * the current time sequence. - * - * @see pulse.input.Range.reset() - * @see pulse.input.IndexRange.reset() - */ - - public void resetRanges() { - indexRange.reset(getTimeSequence()); - range.reset(indexRange, getTimeSequence()); - } - - @Override - public String toString() { - var sb = new StringBuilder(); - sb.append("Experimental data "); - if (metadata.getSampleName() != null) - sb.append("for " + metadata.getSampleName() + " "); - sb.append("(" + metadata.numericProperty(TEST_TEMPERATURE).formattedOutput() + ")"); - return sb.toString(); - } - - /** - * Adds {@code time} and {@code temperature} to the respective {@code List}s. - *

- * Note that the {@code baselineAdjustedTemperature} will be the same as the - * corresponding {@code temperature}, i.e. no baseline subtraction is performed. - * Upon completion, the {@code count} variable will be incremented. - *

- * - * @param time the next time value - * @param signal the next signal value - */ - - @Override - public void addPoint(double time, double signal) { - super.addPoint(time, 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 absolute maximum (which can be an outlier!) of the - * temperature, this overriden method calculates the (absolute) maximum of the - * {@code runningAverage} using the default reduction factor - * {@value REDUCTION_FACTOR}. - * - * @see pulse.problem.statements.Problem.estimateSignalRange(ExperimentalData) - */ - - public double maxAdjustedSignal() { - var degraded = runningAverage(REDUCTION_FACTOR); - return (max(degraded, pointComparator)).getY(); - } - - /** - * 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 return a value equal to the acquistion time divided by a fail-safe - * factor {@value FAIL_SAFE_FACTOR}. - *

- * - * @return A double, representing the half-rise time (in seconds). - */ - - public double halfRiseTime() { - var degraded = runningAverage(REDUCTION_FACTOR); - double max = (max(degraded, pointComparator)).getY(); - var baseline = new FlatBaseline(); - baseline.fitTo(this); - - double halfMax = (max + baseline.valueAt(0)) / 2.0; - - int index = IndexRange.closestLeft(halfMax, - degraded.stream().map(point -> point.getY()).collect(Collectors.toList())); - - if (index < 1) { - System.err.println(Messages.getString("ExperimentalData.HalfRiseError")); - return max(getTimeSequence()) / FAIL_SAFE_FACTOR; - } - - return degraded.get(index).getX(); - - } - - /** - * Retrieves the {@code Metadata} object for this {@code ExperimentalData}. - * - * @return the linked {@code Metadata} - */ - - public Metadata getMetadata() { - return metadata; - } - - @Override - public boolean equals(Object o) { - if(!super.equals(o)) - return false; - - if (!(o instanceof ExperimentalData)) - return false; - - var other = (ExperimentalData) o; - return this.metadata.equals(other.getMetadata()); - } - - /** - * Checks if the acquisition time used to collect this {@code ExperimentalData} - * is sensible. - *

- * The acquisition time is essentially the last element in the - * {@code time List}. By default, it is deemed sensible if that last element is - * less than {@value CUTOFF_FACTOR}*{@code halfRiseTime}. - *

- * - * @return {@code true} if the acquisition time is below the truncation - * threshold, {@code false} otherwise. - */ - - public boolean isAcquisitionTimeSensible() { - final double halfMaximum = halfRiseTime(); - final double cutoff = CUTOFF_FACTOR * halfMaximum; - final int count = (int) getNumPoints().getValue(); - return getTimeSequence().get(count - 1) < cutoff; - } - - /** - * Truncates the {@code range} and {@code indexRange} of this - * {@code ExperimentalData} above a certain threshold, NOT removing any data - * elements. - *

- * The threshold is calculated based on the {@code halfRiseTime} value and is - * set by default to {@value CUTOFF_FACTOR}*{@code halfRiseTime}. A - * {@code DataEvent} will be created and passed to the {@code dataListeners} (if - * any) with the {@code DataEventType.TRUNCATED} as argument. - *

- * - * @see halfRiseTime - * @see DataEvent - * @see fireDataChanged - */ - - public void truncate() { - final double halfMaximum = halfRiseTime(); - final double cutoff = CUTOFF_FACTOR * halfMaximum; - - this.range.setUpperBound(derive(UPPER_BOUND, cutoff)); - this.indexRange.set(getTimeSequence(), range); - - fireDataChanged(new DataEvent(TRUNCATED, this)); - } - - /** - * Sets a new {@code Metadata} object for this {@code ExperimentalData}. - *

- * The {@code pulseWidth} property recorded in {@code Metadata} will be used to - * set the time range for the reverse problem solution. Whenever this property - * is changed in the {@code metadata}, a listener will ensure an updated range - * is used. - *

- * - * @param metadata the new Metadata object - * @see PropertyHolderListener - */ - - public void setMetadata(Metadata metadata) { - this.metadata = metadata; - metadata.setParent(this); - doSetMetadata(); - } - - private void doSetMetadata() { - - if (range != null) - range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); - - metadata.addListener(event -> { - - if (event.getProperty() instanceof NumericProperty) { - var p = (NumericProperty) event.getProperty(); - - if (p.getType() == PULSE_WIDTH) - range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); - - } - - }); - - } - - /** - * Gets the time sequence element corresponding to the lower bound of the index - * range - * - * @return the time (in seconds) associated with - * {@code indexRange.getLowerBound()} - */ - - public double getEffectiveStartTime() { - return getTimeSequence().get(indexRange.getLowerBound()); - } - - /** - * Gets the time sequence element corresponding to the upper bound of the index - * range - * - * @return the time (in seconds) associated with - * {@code indexRange.getUpperBound()} - */ - - public double getEffectiveEndTime() { - return getTimeSequence().get(indexRange.getUpperBound()); - } - - /** - * Gets the dimensional time {@code Range} of this data. - * - * @return the range - */ - - public Range getRange() { - return range; - } - - /** - * Gets the index range of this data. - * - * @return the index range - */ - - public IndexRange getIndexRange() { - return indexRange; - } - - /** - * Sets the range, assigning {@code this} to its parent, and forcing changes to - * the {@code indexRange}. - * - * @param range the range - */ - - public void setRange(Range range) { - this.range = range; - range.setParent(this); - doSetRange(); - } - - private void doSetRange() { - var time = getTimeSequence(); - indexRange.set(time, range); - - addHierarchyListener(l -> { - if (l.getSource() == range) - indexRange.set(time, range); - }); - - if (metadata != null) - range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); - } - -} \ No newline at end of file +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 transient List dataListeners; + + /** + * This is the cutoff factor which is used as a criterion for data + * truncation. Described in Lunev, A., & Heymer, R. (2020). Review of + * Scientific Instruments, 91(6), 064902. + */ + public final static double CUTOFF_FACTOR = 7.2; + + /** + * Constructs an {@code ExperimentalData} object using the superclass + * constructor and creating a new list of data listeners. The number of + * points is set to zero by default, and a new {@code IndexRange} is + * initialized. + * + */ + public ExperimentalData() { + super(); + setPrefix("RawData"); + setNumPoints(derive(NUMPOINTS, 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(); + } + }); + } + + public final void addDataListener(DataListener listener) { + if(dataListeners == null) { + dataListeners = new ArrayList<>(); + } + dataListeners.add(listener); + } + + public final void clearDataListener() { + dataListeners.clear(); + } + + public final void fireDataChanged(DataEvent dataEvent) { + dataListeners.stream().forEach(l -> l.onDataChanged(dataEvent)); + } + + /** + * Calls reset for both the {@code IndexRange} and {@code Range} objects + * using the current time sequence. + * + * @see pulse.input.Range.reset() + * @see pulse.input.IndexRange.reset() + */ + public final void resetRanges() { + indexRange.reset(getTimeSequence()); + range.reset(indexRange, getTimeSequence()); + } + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append("Experimental data "); + if (metadata.getSampleName() != null) { + sb.append("for " + metadata.getSampleName() + " "); + } + sb.append("(").append(metadata.numericProperty(TEST_TEMPERATURE).formattedOutput()).append(")"); + return sb.toString(); + } + + /** + * Adds {@code time} and {@code temperature} to the respective + * {@code List}s. Increments the counter of points. Note that no baseline + * correction is performed. + * + * @param time the next time value + * @param signal the next signal value + */ + @Override + public void addPoint(double time, double signal) { + super.addPoint(time, signal); + incrementCount(); + } + + /** + * Retrieves the {@code Metadata} object for this {@code ExperimentalData}. + * + * @return the linked {@code Metadata} + */ + public Metadata getMetadata() { + return metadata; + } + + @Override + public boolean equals(Object o) { + if (!super.equals(o)) { + return false; + } + + if (!(o instanceof ExperimentalData)) { + return false; + } + + var other = (ExperimentalData) o; + return this.metadata.equals(other.getMetadata()); + } + + /** + * Checks if the acquisition time used to collect this + * {@code ExperimentalData} is sensible. + *

+ * The acquisition time is essentially the last element in the + * {@code time List}. By default, it is deemed sensible if that last element + * is less than {@value CUTOFF_FACTOR}*{@code halfRiseTime}. + *

+ * + * @return {@code true} if the acquisition time is below the truncation + * threshold, {@code false} otherwise. + */ + public boolean isAcquisitionTimeSensible() { + 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; + } + + /** + * Truncates the {@code range} and {@code indexRange} of this + * {@code ExperimentalData} above a certain threshold, NOT removing any data + * elements. + *

+ * The threshold is calculated based on the {@code halfRiseTime} value and + * is set by default to {@value CUTOFF_FACTOR}*{@code halfRiseTime}. A + * {@code DataEvent} will be created and passed to the {@code dataListeners} + * (if any) with the {@code DataEventType.TRUNCATED} as argument. + *

+ * + * @see halfRiseTime + * @see DataEvent + * @see fireDataChanged + */ + public void truncate() { + final double cutoff = CUTOFF_FACTOR * calculator.getHalfTime(); + this.range.setUpperBound(derive(UPPER_BOUND, cutoff)); + } + + /** + * Sets a new {@code Metadata} object for this {@code ExperimentalData}. + *

+ * The {@code pulseWidth} property recorded in {@code Metadata} will be used + * to set the time range for the reverse problem solution. Whenever this + * property is changed in the {@code metadata}, a listener will ensure an + * updated range is used. + *

+ * + * @param metadata the new Metadata object + * @see PropertyHolderListener + */ + public void setMetadata(Metadata metadata) { + this.metadata = metadata; + metadata.setParent(this); + doSetMetadata(); + } + + private void doSetMetadata() { + + if (range != null) { + range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); + } + + } + + /** + * Gets the time sequence element corresponding to the lower bound of the + * index range + * + * @return the time (in seconds) associated with + * {@code indexRange.getLowerBound()} + */ + public double getEffectiveStartTime() { + return getTimeSequence().get(indexRange.getLowerBound()); + } + + /** + * Gets the time sequence element corresponding to the upper bound of the + * index range + * + * @return the time (in seconds) associated with + * {@code indexRange.getUpperBound()} + */ + public double getEffectiveEndTime() { + return getTimeSequence().get(indexRange.getUpperBound()); + } + + /** + * Gets the dimensional time {@code Range} of this data. + * + * @return the range + */ + public Range getRange() { + return range; + } + + /** + * Gets the index range of this data. + * + * @return the index range + */ + @Override + public IndexRange getIndexRange() { + return indexRange; + } + + /** + * Sets the range, assigning {@code this} to its parent, and forcing changes + * to the {@code indexRange}. + * + * @param range the range + */ + public void setRange(Range range) { + this.range = range; + range.setParent(this); + doSetRange(); + } + + private void doSetRange() { + indexRange.set(time, range); + + addHierarchyListener(l -> { + if (l.getSource() == range) { + indexRange.set(time, range); + this.fireDataChanged(new DataEvent(DataEventType.RANGE_CHANGED, this)); + } + }); + + if (metadata != null) { + range.updateMinimum(metadata.numericProperty(PULSE_WIDTH)); + } + } + + /** + * Retrieves the time limit. + * + * @see pulse.problem.schemes.DifferenceScheme + * @return a double, equal to the last element of the {@code time List}. + */ + @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(); + } + +} diff --git a/src/main/java/pulse/input/IndexRange.java b/src/main/java/pulse/input/IndexRange.java index 5ebe0b26..cb19490c 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; @@ -11,218 +12,218 @@ * Essentially, an object of this class contains an ordered pair representing * the associated indices. Works in conjunction with the {@code Range} class. *

- * + * * @see pulse.input.Range * */ - -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; - } - - /** - * Constructs a new index range for {@code data} based on the dimensional - * {@code range}. - * - * @param data the list to be analysed - * @param range the range object used to define the index range - * - * @see set - */ - - public IndexRange(List data, Range range) { - set(data, range); - } - - /** - * Resets the index range by effectively treating the {@code data} list as - * bounded by its first and last elements, assuming that {@code data} is sorted - * in ascending order. Because of this last assumption, the visibility of this - * method has been set to protected. - * - * @param data a list sorted in ascending order - */ - - protected void reset(List data) { - requireNonNull(data); - int size = data.size(); - - if (size > 0) { - setLowerBound(data, data.get(0)); - setUpperBound(data, data.get(size - 1)); - } - - } - - /** - * Sets the start index by conducting a primitive binary search using - * {@code closest(...)} to find an element in {@code data} either matching or - * being as close as possible to {@code a} (if {@code a} is non-negative) or - * zero. - * - * @param data the list to process - * @param a an element representing the lower bound (not necessarily - * contained in {@code data}). - * @see closestLeft - * @see closestRight - */ - - public void setLowerBound(List data, double a) { - iStart = a > 0 ? closestLeft(a, data) : closestRight(0, data); - } - - /** - * Sets the end index by conducting a primitive binary search using - * {@code closest(...)} to find an element in {@code data} either matching or - * being as close as possible to {@code b}. For the above operation, the list is - * searched through from its last to first element (i.e., in reverse order). - * - * @param data the list to process - * @param b an element representing the upper bound (not necessarily - * contained in {@code data}). - * @see closestLeft - * @see closestRight - */ - - public void setUpperBound(List data, double b) { - iEnd = closestRight(b, data); - } - - /** - * Sets the bounds of this index range using the minimum and maximum values of - * the segment specified in the {@code range} object. If the minimum bound is - * negative, it will be ignored and replaced by 0.0. - * - * @param data the data list to be processed - * @param range a range with minimum and maximum values - * @see setLowerBound - * @see setUpperBound - */ - - public void set(List data, Range range) { - var segment = range.getSegment(); - setLowerBound(data, Math.max(0.0, segment.getMinimum())); - setUpperBound(data, segment.getMaximum()); - } - - /** - * Gets the integer value representing the index of the lower bound previously - * set by looking at a certain unspecified data list. - * - * @return the start index - */ - - public int getLowerBound() { - return iStart; - } - - /** - * Gets the integer value representing the index of the upper bound previously - * set by looking at a certain unspecified data list. - * - * @return the end index - */ - - public int getUpperBound() { - return iEnd; - } - - /** - * Checks if this index range is viable. - * - * @return {@code true} if the upper bound is positive and greater than the - * lower bound, {@code false} otherwise. - */ - - public boolean isValid() { - return (iStart < iEnd && iEnd > 0); - } - - /** - * Searches through the elements contained in the the second argument of this - * method to find an element belonging to {@code in} most closely resembling the - * first argument. The search is completed once {@code of} lies between any two - * adjacent elements of {@code in}. The result is then the index of the - * preceding element. - * - * @param of an element which will be compared against - * @param in a list of data presumably containing an element similar to - * {@code of} - * @return - *

- * any integer greater than 0 and lesser than {@code in.size} that - * matches the above criterion. If {@code of} is greater than the last - * elemennt of {@code in}, this will return the latter. Otherwise, if no - * element matching the criterion is found, returns 0. - *

- */ - - public static int closestLeft(double of, List in) { - return closest(of, in, false); - } - - /** - * Searches through the elements contained in the the second argument of this - * method to find an element belonging to {@code in} most closely resembling the - * first argument. The search utilises a reverse order, i.e. it starts from the - * last element and goes to the first. The search is completed once {@code of} - * lies between any two adjacent elements of {@code in}. The result is then the - * index of the preceding element. - * - * @param of an element which will be compared against - * @param in a list of data presumably containing an element similar to - * {@code of} - * @return - *

- * any integer greater than 0 and lesser than {@code in.size} that - * matches the above criterion. If {@code of} is greater than the last - * elemennt of {@code in}, this will return the latter. Otherwise, if no - * element matching the criterion is found, returns 0. - *

- */ - - public static int closestRight(double of, List in) { - return closest(of, in, true); - } - - private static int closest(double of, List in, boolean reverseOrder) { - int sizeMinusOne = in.size() - 1; - - if (of > in.get(sizeMinusOne)) - return sizeMinusOne; - - int start = reverseOrder ? sizeMinusOne - 1 : 0; - int increment = reverseOrder ? -1 : 1; - - for (int i = start; reverseOrder ? (i > -1) : (i < sizeMinusOne); i += increment) { - - if (between(of, in.get(i), in.get(i + 1))) - return i; - - } - - return 0; - - } - - private static boolean between(double x, double minValueInclusive, double maxValueInclusive) { - return (x >= minValueInclusive && x <= maxValueInclusive); - } - - @Override - public String toString() { - return "Index range: from " + iStart + " to " + iEnd; - } - -} \ No newline at end of file +public class IndexRange implements Serializable { + + private static final long serialVersionUID = 7983756487957427969L; + private int iStart; + private int iEnd; + + public IndexRange(IndexRange other) { + iStart = other.iStart; + iEnd = other.iEnd; + } + + public IndexRange(int start, int end) { + this.iStart = start; + this.iEnd = end; + } + + /** + * Constructs a new index range for {@code data} based on the dimensional + * {@code range}. + * + * @param data the list to be analysed + * @param range the range object used to define the index range + * + * @see set + */ + public IndexRange(List data, Range range) { + set(data, range); + } + + /** + * Resets the index range by effectively treating the {@code data} list as + * bounded by its first and last elements, assuming that {@code data} is + * sorted in ascending order. Because of this last assumption, the + * visibility of this method has been set to protected. + * + * @param data a list sorted in ascending order + */ + protected void reset(List data) { + requireNonNull(data); + int size = data.size(); + + if (size > 0) { + setLowerBound(data, data.get(0)); + setUpperBound(data, data.get(size - 1)); + } + + } + + /** + * Sets the start index by conducting a primitive binary search using + * {@code closest(...)} to find an element in {@code data} either matching + * or being as close as possible to {@code a} (if {@code a} is non-negative) + * or zero. + * + * @param data the list to process + * @param a an element representing the lower bound (not necessarily + * contained in {@code data}). + * @see closestLeft + * @see closestRight + */ + public final void setLowerBound(List data, double a) { + iStart = a > 0 ? closestLeft(a, data) : closestRight(0, data); + } + + /** + * Sets the end index by conducting a primitive binary search using + * {@code closest(...)} to find an element in {@code data} either matching + * or being as close as possible to {@code b}. For the above operation, the + * list is searched through from its last to first element (i.e., in reverse + * order). + * + * @param data the list to process + * @param b an element representing the upper bound (not necessarily + * contained in {@code data}). + * @see closestLeft + * @see closestRight + */ + public final void setUpperBound(List data, double b) { + iEnd = closestRight(b, data); + } + + /** + * Sets the bounds of this index range using the minimum and maximum values + * of the segment specified in the {@code range} object. If the minimum + * bound is negative, it will be ignored and replaced by 0.0. + * + * @param data the data list to be processed + * @param range a range with minimum and maximum values + * @see setLowerBound + * @see setUpperBound + */ + public final void set(List data, Range range) { + var segment = range.getSegment(); + setLowerBound(data, segment.getMinimum()); + setUpperBound(data, segment.getMaximum()); + } + + /** + * Gets the integer value representing the index of the lower bound + * previously set by looking at a certain unspecified data list. + * + * @return the start index + */ + public final int getLowerBound() { + return iStart; + } + + /** + * Gets the integer value representing the index of the upper bound + * previously set by looking at a certain unspecified data list. + * + * @return the end index + */ + public final int getUpperBound() { + return iEnd; + } + + /** + * Checks if this index range is viable. + * + * @return {@code true} if the upper bound is positive and greater than the + * lower bound, {@code false} otherwise. + */ + public boolean isValid() { + return (iStart < iEnd && iEnd > 0); + } + + /** + * Searches through the elements contained in the the second argument of + * this method to find an element belonging to {@code in} most closely + * resembling the first argument. The search is completed once {@code of} + * lies between any two adjacent elements of {@code in}. The result is then + * the index of the preceding element. + * + * @param of an element which will be compared against + * @param in a list of data presumably containing an element similar to + * {@code of} + * @return + *

+ * any integer greater than 0 and lesser than {@code in.size} that matches + * the above criterion. If {@code of} is greater than the last element of + * {@code in}, this will return the latter. Otherwise, if no element + * matching the criterion is found, returns 0. + *

+ */ + public static int closestLeft(double of, List in) { + return closest(of, in, false); + } + + /** + * Searches through the elements contained in the the second argument of + * this method to find an element belonging to {@code in} most closely + * resembling the first argument. The search utilises a reverse order, i.e. + * it starts from the last element and goes to the first. The search is + * completed once {@code of} lies between any two adjacent elements of + * {@code in}. The result is then the index of the preceding element. + * + * @param of an element which will be compared against + * @param in a list of data presumably containing an element similar to + * {@code of} + * @return + *

+ * any integer greater than 0 and lesser than {@code in.size} that matches + * the above criterion. If {@code of} is greater than the last element of + * {@code in}, this will return the latter. Otherwise, if no element + * matching the criterion is found, returns 0. + *

+ */ + public static int closestRight(double of, List in) { + return closest(of, in, true); + } + + private static int closest(double of, List in, boolean reverseOrder) { + int sizeMinusOne = in.size() - 1; //has to be non-negative + + int result = 0; + + if (sizeMinusOne < 1) { + result = 0; + } else if (of > in.get(sizeMinusOne)) { + result = sizeMinusOne; + } else { + + int start = reverseOrder ? sizeMinusOne - 1 : 0; + int increment = reverseOrder ? -1 : 1; + + for (int i = start; reverseOrder ? (i > -1) : (i < sizeMinusOne); i += increment) { + + if (between(of, in.get(i), in.get(i + 1))) { + result = i; + break; + } + + } + + } + + return result; + + } + + private static boolean between(double x, double minValueInclusive, double maxValueInclusive) { + return (x >= minValueInclusive && x <= maxValueInclusive); + } + + @Override + public String toString() { + return "Index range: from " + iStart + " to " + iEnd; + } + +} diff --git a/src/main/java/pulse/input/InterpolationDataset.java b/src/main/java/pulse/input/InterpolationDataset.java index 1a47d7d1..4e0ec64a 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.NumericProperties.derive; -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.HashMap; import java.util.List; -import java.util.Map; import org.apache.commons.math3.analysis.UnivariateFunction; -import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.interpolation.AkimaSplineInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; -import pulse.problem.statements.ThermalProperties; -import pulse.properties.NumericPropertyKeyword; +import pulse.util.FunctionSerializer; import pulse.util.ImmutableDataEntry; /** @@ -23,146 +21,83 @@ * 'value') and provides means to interpolate between the 'values' using the * 'keys'. This is used mainly to interpolate between available data for thermal * properties loaded in tabular representation, e.g. the density and specific - * heat tables. + * heat tables. Features a static list of {@code ExternalDatasetListener}s. + * + * @see pulse.input.listeners.ExternalDatasetListener */ - -public class InterpolationDataset { - - private List> dataset; - private static Map standartDatasets = new HashMap(); - private UnivariateFunction interpolation; - - /** - * Creates an empty {@code InterpolationDataset}. - */ - - public InterpolationDataset() { - dataset = new ArrayList<>(); - } - - /** - * Provides an interpolated value at {@code key} based on the available data in - * the {@code DataEntry List}. The interpolation is done using natural cubic - * splines, hence it is important that the input noise is minimal. - * - * @param key the argument, at which interpolation needs to be done (e.g. - * temperature) - * @return a double, representing the interpolated value - */ - - public double interpolateAt(double key) { - return interpolation.value(key); - - } - - /** - * Adds {@code entry} to this {@code InterpolationDataset}. - * - * @param entry the entry to be added - */ - - public void add(ImmutableDataEntry entry) { - dataset.add(entry); - } - - /** - * Constructs a new spline interpolator and uses the available dataset to - * produce a {@code SplineInterpolation}. - */ - - public void doInterpolation() { - var interpolator = new SplineInterpolator(); - interpolation = interpolator.interpolate(dataset.stream().map(a -> a.getKey()).mapToDouble(d -> d).toArray(), - dataset.stream().map(a -> a.getValue()).mapToDouble(d -> d).toArray()); - } - - /** - * Extracts all data available in this {@code InterpolationDataset}. - * - * @return the {@code List} of data. - */ - - 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 - * - * @param dataset a dataset to be appended to the static hash map - * @param type the dataset type - */ - - public static void setDataset(InterpolationDataset dataset, StandartType type) { - standartDatasets.put(type, dataset); - } - - /** - * Calculates some or all of the following properties: - * Cp, ρ, &labmda;, - * ε. - *

- * These properties will be calculated only if the necessary - * {@code InterpolationDataset}s were previously loaded by the - * {@code TaskManager}. - *

- */ - - public static void fill(ThermalProperties properties) { - final double testTemperature = (double)properties.getTestTemperature().getValue(); - var cpCurve = getDataset(StandartType.HEAT_CAPACITY); - - if (cpCurve != null) { - final double cp = cpCurve.interpolateAt(testTemperature); - properties.set(NumericPropertyKeyword.SPECIFIC_HEAT, derive(NumericPropertyKeyword.SPECIFIC_HEAT, cp)); - } - - var rhoCurve = getDataset(StandartType.DENSITY); - - if (rhoCurve != null) { - final double rho = rhoCurve.interpolateAt(testTemperature); - properties.set(NumericPropertyKeyword.DENSITY, derive(NumericPropertyKeyword.DENSITY, rho)); - } - - } - - 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); - return list; - } - - 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; - - } +public class InterpolationDataset implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 7439474910490135034L; + private transient UnivariateFunction interpolation; + private final List> dataset; + + /** + * Creates an empty {@code InterpolationDataset}. + */ + public InterpolationDataset() { + dataset = new ArrayList<>(); + } + + /** + * Provides an interpolated value at {@code key} based on the available data + * in the {@code DataEntry List}. The interpolation is done using natural + * cubic splines, hence it is important that the input noise is minimal. + * + * @param key the argument, at which interpolation needs to be done (e.g. + * temperature) + * @return a double, representing the interpolated value + */ + public double interpolateAt(double key) { + return interpolation.value(key); + } + + /** + * Adds {@code entry} to this {@code InterpolationDataset}. + * + * @param entry the entry to be added + */ + public void add(ImmutableDataEntry entry) { + dataset.add(entry); + } + + /** + * Constructs a new Akima spline interpolator and uses the available dataset to + * produce a {@code SplineInterpolation}. + */ + public void doInterpolation() { + var interpolator = new AkimaSplineInterpolator(); + interpolation = interpolator.interpolate(dataset.stream().map(a -> a.getKey()).mapToDouble(d -> d).toArray(), + dataset.stream().map(a -> a.getValue()).mapToDouble(d -> d).toArray()); + } + + /** + * Extracts all data available in this {@code InterpolationDataset}. + * + * @return the {@code List} of data. + */ + public List> getData() { + return dataset; + } + + /* + * 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/input/Metadata.java b/src/main/java/pulse/input/Metadata.java index f2a03c69..6840ff58 100644 --- a/src/main/java/pulse/input/Metadata.java +++ b/src/main/java/pulse/input/Metadata.java @@ -1,7 +1,6 @@ package pulse.input; import static java.lang.System.lineSeparator; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericPropertyKeyword.DETECTOR_GAIN; import static pulse.properties.NumericPropertyKeyword.DETECTOR_IRIS; import static pulse.properties.NumericPropertyKeyword.DIAMETER; @@ -17,10 +16,13 @@ import java.util.Set; import java.util.TreeSet; +import pulse.problem.laser.NumericPulseData; import pulse.problem.laser.PulseTemporalShape; import pulse.problem.laser.RectangularPulse; +import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.FOV_OUTER; import pulse.properties.Property; import pulse.properties.SampleName; import pulse.tasks.Identifier; @@ -38,214 +40,238 @@ *

* */ - public class Metadata extends PropertyHolder implements Reflexive { - private Set data; - private SampleName sampleName; - private int externalID; - - private InstanceDescriptor pulseDescriptor = new InstanceDescriptor( - "Pulse Shape Selector", PulseTemporalShape.class); - - /** - * Creates a {@code Metadata} with the specified parameters and a default - * rectangular pulse shape. Properties are stored in a {@code TreeSet}. - * - * @param temperature the NumericProperty of the type - * {@code NumericPropertyKeyword.TEST_TEMPERATURE} - * @param externalId an integer, specifying the external ID recorded by the - * experimental setup. - */ - - public Metadata(NumericProperty temperature, int externalId) { - sampleName = new SampleName(); - setExternalID(externalId); - pulseDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); - data = new TreeSet(); - set(TEST_TEMPERATURE, temperature); - } - - /** - * Gets the external ID usually specified in the experimental files. Note this - * is not a {@code NumericProperty} - * - * @return an integer, representing the external ID - */ - - public int getExternalID() { - return externalID; - } - - /** - * Sets the external ID in this {@code Metadata} to {@code externalId} - * - * @param externalId the value of the external ID - */ - - private void setExternalID(int externalId) { - this.externalID = externalId; - } - - /** - * Retrieves the pulse shape recorded in this {@code Metadata} - * - * @return a {@code PulseShape} object - */ - - public InstanceDescriptor getPulseDescriptor() { - return pulseDescriptor; - } - - /** - * Retrieves the sample name. This name is used to create directories when - * exporting the data and also to fill the legend when plotting. - * - * @return the sample name - */ - - public SampleName getSampleName() { - return sampleName; - } - - /** - * Sets the sample name property. - * - * @param sampleName the sample name - */ - - public void setSampleName(SampleName sampleName) { - this.sampleName = sampleName; - } - - /** - * Searches the internal list of this class for a property with the {@code key} - * type. - * - * @return if present, returns a property belonging to this {@code Metadata} - * with the specified type, otherwise return null. - */ - - @Override - public NumericProperty numericProperty(NumericPropertyKeyword key) { - var optional = data.stream().filter(p -> p.getType() == key).findFirst(); - return optional.isPresent() ? optional.get() : null; - } - - /** - * If {@code type} is listed by this {@code Metadata}, will attempt to either - * set a value to the property belonging to this {@code Metadata} and identified - * by {@code type} or add {@code property} to the internal repository of this - * {@code Metadata}. Triggers {@code firePropertyChanged} upon successful - * completion. - * - * @param type the type to be searched for - * @param property a property with the type specified by its first argument. The - * value of this property will be used to update its counterpart - * in this {@code Metadata}. The signature of this method is - * dictated by the use of Reflection API. - * @throws IllegalArgumentException if the types of the arguments do not match - * or if {@code} property is not a listed - * parameter - * @see PropertyHolder.isListedParameter() - * @see PropertyHolder.firePropertyChanged() - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - - if (type != property.getType() || !isListedParameter(property)) - return; //ingore unrecognised properties - - var optional = numericProperty(type); - - if (optional != null) - optional.setValue((Number) property.getValue()); - else - data.add(property); - - firePropertyChanged(this, property); - - } - - @Override - public List listedTypes() { - List list = new ArrayList<>(9); - list.add(def(TEST_TEMPERATURE)); - list.add(def(THICKNESS)); - list.add(def(DIAMETER)); - list.add(def(PULSE_WIDTH)); - list.add(def(SPOT_DIAMETER)); - list.add(def(LASER_ENERGY)); - list.add(def(DETECTOR_GAIN)); - list.add(def(DETECTOR_IRIS)); - list.add(new SampleName()); - list.add(pulseDescriptor); - return list; - } - - @Override - public String toString() { - var sb = new StringBuilder(); - sb.append(sampleName + " [" + externalID + "]"); - sb.append(lineSeparator()); - sb.append(lineSeparator()); - - data.forEach(entry -> { - sb.append(entry.toString()); - sb.append(lineSeparator()); - }); - - sb.append(pulseDescriptor.toString()); - - return sb.toString(); - - } - - /** - * Creates a list of data that contain all {@code NumericProperty} objects - * belonging to this {@code Metadata} and an {@code InstanceDescriptor} relating - * to the pulse shape. - */ - - @Override - public List data() { - var list = new ArrayList(); - list.addAll(data); - list.add(pulseDescriptor); - return list; - } - - /** - * @return If this {@code Metadata} is NOT assigned to a {@code SearchTask}, - * returns a new {@code Identifier} based on the {@code externalID}. - * Otherwise, calls {@code super.identify()}. - * @see Identifier.externalIdentifier() - */ - - @Override - public Identifier identify() { - return getParent() == null ? externalIdentifier(externalID) : super.identify(); - } - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (!(o instanceof Metadata)) - return false; - - var other = (Metadata) o; - - if (other.getExternalID() != this.getExternalID()) - return false; - - if (!sampleName.equals(other.getSampleName())) - return false; - - return this.data().containsAll(other.data()); - - } - -} \ No newline at end of file + private static final long serialVersionUID = -7954252611294551707L; + private Set data; + private SampleName sampleName; + private int externalID; + + private InstanceDescriptor pulseDescriptor + = new InstanceDescriptor<>("Pulse Shape Selector", PulseTemporalShape.class); + + private NumericPulseData pulseData; + + /** + * Creates a {@code Metadata} with the specified parameters and a default + * rectangular pulse shape. Properties are stored in a {@code TreeSet}. + * + * @param temperature the NumericProperty of the type + * {@code NumericPropertyKeyword.TEST_TEMPERATURE} + * @param externalId an integer, specifying the external ID recorded by the + * experimental setup. + */ + public Metadata(NumericProperty temperature, int externalId) { + sampleName = new SampleName(null); + setExternalID(externalId); + pulseDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); + data = new TreeSet<>(); + set(TEST_TEMPERATURE, temperature); + } + + /** + * Gets the external ID usually specified in the experimental files. Note + * this is not a {@code NumericProperty} + * + * @return an integer, representing the external ID + */ + public int getExternalID() { + return externalID; + } + + /** + * Sets the external ID in this {@code Metadata} to {@code externalId} + * + * @param externalId the value of the external ID + */ + private void setExternalID(int externalId) { + this.externalID = externalId; + } + + /** + * Retrieves the pulse shape recorded in this {@code Metadata} + * + * @return a {@code PulseShape} object + */ + public InstanceDescriptor getPulseDescriptor() { + return pulseDescriptor; + } + + /** + * Retrieves the sample name. This name is used to create directories when + * exporting the data and also to fill the legend when plotting. + * + * @return the sample name + */ + public SampleName getSampleName() { + return sampleName; + } + + /** + * Sets the sample name property. + * + * @param sampleName the sample name + */ + public void setSampleName(SampleName sampleName) { + this.sampleName = sampleName; + } + + public final void setPulseData(NumericPulseData pulseData) { + this.pulseData = pulseData; + 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 + */ + public final NumericPulseData getPulseData() { + return pulseData; + } + + /** + * Searches the internal list of this class for a property with the + * {@code key} type. + * + * @return if present, returns a property belonging to this {@code Metadata} + * with the specified type, otherwise return null. + */ + @Override + public NumericProperty numericProperty(NumericPropertyKeyword key) { + var optional = data.stream().filter(p -> p.getType() == key).findFirst(); + return optional.isPresent() ? optional.get() : null; + } + + /** + * If {@code type} is listed by this {@code Metadata}, will attempt to + * either set a value to the property belonging to this {@code Metadata} and + * identified by {@code type} or add {@code property} to the internal + * repository of this {@code Metadata}. Triggers {@code firePropertyChanged} + * upon successful completion. + * + * @param type the type to be searched for + * @param property a property with the type specified by its first argument. + * The value of this property will be used to update its counterpart in this + * {@code Metadata}. The signature of this method is dictated by the use of + * Reflection API. + * @throws IllegalArgumentException if the types of the arguments do not + * match or if {@code} property is not a listed parameter + * @see PropertyHolder.isListedParameter() + * @see PropertyHolder.firePropertyChanged() + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + + if (type != property.getType() || !isListedParameter(property)) { + return; //ingore unrecognised properties + } + var optional = numericProperty(type); + + if (optional != null) { + optional.setValue((Number) property.getValue()); + } else { + data.add(property); + } + + firePropertyChanged(this, property); + + } + + /** + * The listed types include {@code TEST_TEMPERATURE}, {@code THICKNESS}, + * {@code DIAMETER}, {@code PULSE_WIDTH}, {@code SPOT_DIAMETER}, + * {@code LASER_ENERGY}, {@code DETECTOR_GAIN}, {@code DETECTOR_IRIS}, + * sample name and the types listed by the pulse descriptor. + */ + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(new SampleName("")); + list.add(pulseDescriptor); + return list; + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(TEST_TEMPERATURE); + set.add(THICKNESS); + set.add(DIAMETER); + set.add(PULSE_WIDTH); + set.add(SPOT_DIAMETER); + set.add(LASER_ENERGY); + set.add(DETECTOR_GAIN); + set.add(DETECTOR_IRIS); + set.add(FOV_OUTER); + return set; + } + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append(sampleName + " [" + externalID + "]"); + sb.append(lineSeparator()); + sb.append(lineSeparator()); + + data.forEach(entry -> { + sb.append(entry.toString()); + sb.append(lineSeparator()); + }); + + sb.append(pulseDescriptor.toString()); + + return sb.toString(); + + } + + /** + * Creates a list of data that contain all {@code NumericProperty} objects + * belonging to this {@code Metadata} and an {@code InstanceDescriptor} + * relating to the pulse shape. + */ + @Override + public List data() { + var list = new ArrayList(); + list.addAll(data); + list.add(pulseDescriptor); + return list; + } + + /** + * @return If this {@code Metadata} is NOT assigned to a {@code SearchTask}, + * returns a new {@code Identifier} based on the {@code externalID}. + * Otherwise, calls {@code super.identify()}. + * @see Identifier.externalIdentifier() + */ + @Override + public Identifier identify() { + return getParent() == null ? externalIdentifier(externalID) : super.identify(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof Metadata)) { + return false; + } + + var other = (Metadata) o; + + if (other.getExternalID() != this.getExternalID()) { + return false; + } + + if (!sampleName.equals(other.getSampleName())) { + return false; + } + + return this.data().containsAll(other.data()); + + } + +} diff --git a/src/main/java/pulse/input/Range.java b/src/main/java/pulse/input/Range.java index 45ce372a..09b247d5 100644 --- a/src/main/java/pulse/input/Range.java +++ b/src/main/java/pulse/input/Range.java @@ -1,6 +1,8 @@ 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; import static pulse.properties.NumericPropertyKeyword.LOWER_BOUND; @@ -8,10 +10,14 @@ import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; import java.util.List; +import java.util.Set; +import pulse.DiscreteInput; +import pulse.math.Parameter; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.math.Segment; -import pulse.properties.Flag; +import pulse.math.transforms.StickTransform; +import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.search.Optimisable; @@ -23,207 +29,290 @@ * by the {@code ExperimentalData}. * */ - public class Range extends PropertyHolder implements Optimisable { - private Segment segment; - - /** - * Constructs a {@code Range} from the minimum and maximum values of - * {@code data}. - * - * @param data a list of double values - */ - - public Range(List data) { - double min = data.stream().reduce((a, b) -> a < b ? a : b).get(); - double max = data.stream().reduce((a, b) -> b > a ? b : a).get(); - segment = new Segment(min, max); - } - - /** - * Constructs a new {@code Range} based on the segment specified by {@code a} - * and {@code b} - * - * @param a a double value - * @param b another double value - */ - - public Range(double a, double b) { - this.segment = new Segment(a, b); - } - - /** - * Resets the minimum and maximum values of this range to those specified by the - * elements of {@code data}, the indices of which correspond to the lower and - * upper bound of the {@code IndexRange}. - * - * @param range an object specifying the start/end indices in regard to the - * {@code data} list - * @param data a list of double values (usually, a time sequence) - */ - - public void reset(IndexRange range, List data) { - segment.setMaximum(data.get(range.getUpperBound())); - segment.setMinimum(data.get(range.getLowerBound())); - } - - /** - * Gets the numeric property defining the lower bound of this range. - * - * @return the lower bound (usually referring to a time sequence). - */ - - public NumericProperty getLowerBound() { - return derive(LOWER_BOUND, segment.getMinimum()); - } - - /** - * Gets the numeric property defining the upper bound of this range. - * - * @return the upper bound (usually referring to a time sequence). - */ - - public NumericProperty getUpperBound() { - return derive(UPPER_BOUND, segment.getMaximum()); - } - - /** - * Sets the lower bound and triggers {@code firePropertyChanged}. - * - * @param p a numeric property with the required {@code LOWER_BOUND} type. - */ - - public void setLowerBound(NumericProperty p) { - requireType(p, LOWER_BOUND); - segment.setMinimum((double) p.getValue()); - firePropertyChanged(this, p); - } - - /** - * Sets the upper bound and triggers {@code firePropertyChanged}. - * - * @param p a numeric property with the required {@code UPPER_BOUND} type. - */ - - public void setUpperBound(NumericProperty p) { - requireType(p, UPPER_BOUND); - segment.setMaximum((double) p.getValue()); - firePropertyChanged(this, p); - } - - /** - * Gets the segment representing this range - * - * @return a segment - */ - - public Segment getSegment() { - return segment; - } - - /** - * Updates the lower bound of this range using the information contained in - * {@code p}. Since this is not fail-safe, the method has been made protected. - * - * @param p a {@code NumericProperty} representing the laser pulse width. - */ - - protected void updateMinimum(NumericProperty p) { - if (p != null) { - - requireType(p, PULSE_WIDTH); - - double pulseWidth = (double) p.getValue(); - segment.setMinimum(max(segment.getMinimum(), pulseWidth)); - - } - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case LOWER_BOUND: - setLowerBound(property); - break; - case UPPER_BOUND: - setUpperBound(property); - break; - default: - // do nothing - break; - } - } - - /* + 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}. + * + * @param data a list of double values + */ + public Range(List data) { + double min = data.stream().reduce((a, b) -> a < b ? a : b).get(); + double max = data.stream().reduce((a, b) -> b > a ? b : a).get(); + segment = new Segment(min, max); + } + + /** + * Constructs a new {@code Range} based on the segment specified by + * {@code a} and {@code b} + * + * @param a a double value + * @param b another double value + */ + 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 + * the elements of {@code data}, the indices of which correspond to the + * lower and upper bound of the {@code IndexRange}. + * + * @param range an object specifying the start/end indices in regard to the + * {@code data} list + * @param data a list of double values (usually, a time sequence) + */ + public void reset(IndexRange range, List data) { + segment.setMaximum(data.get(range.getUpperBound())); + segment.setMinimum(data.get(range.getLowerBound())); + } + + /** + * Gets the numeric property defining the lower bound of this range. + * + * @return the lower bound (usually referring to a time sequence). + */ + public NumericProperty getLowerBound() { + return derive(LOWER_BOUND, segment.getMinimum()); + } + + /** + * Gets the numeric property defining the upper bound of this range. + * + * @return the upper bound (usually referring to a time sequence). + */ + public NumericProperty getUpperBound() { + return derive(UPPER_BOUND, segment.getMaximum()); + } + + /** + * Sets the lower bound and triggers {@code firePropertyChanged}. + * + * @param p a numeric property with the required {@code LOWER_BOUND} type. + */ + public void setLowerBound(NumericProperty p) { + requireType(p, LOWER_BOUND); + + if (boundLimits(false).contains(((Number) p.getValue()).doubleValue())) { + segment.setMinimum((double) p.getValue()); + firePropertyChanged(this, p); + } + + } + + /** + * Sets the upper bound and triggers {@code firePropertyChanged}. + * + * @param p a numeric property with the required {@code UPPER_BOUND} type. + */ + public void setUpperBound(NumericProperty p) { + requireType(p, UPPER_BOUND); + + if (boundLimits(true).contains(((Number) p.getValue()).doubleValue())) { + segment.setMaximum((double) p.getValue()); + firePropertyChanged(this, p); + } + + } + + /** + * Gets the segment representing this range + * + * @return a segment + */ + public Segment getSegment() { + return segment; + } + + /** + * Updates the lower bound of this range using the information contained in + * {@code p}. Since this is not fail-safe, the method has been made + * protected. + * + * @param p a {@code NumericProperty} representing the laser pulse width. + */ + protected void updateMinimum(NumericProperty p) { + if (p == null) { + return; + } + + requireType(p, PULSE_WIDTH); + double pulseWidth = (double) p.getValue(); + segment.setMinimum(max(segment.getMinimum(), pulseWidth)); + + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case LOWER_BOUND: + setLowerBound(property); + break; + case UPPER_BOUND: + setUpperBound(property); + break; + default: + // do nothing + break; + } + } + + /** + * Lists lower and upper bounds as properties. + * + * @see PropertyHolder + */ + @Override + public Set listedKeywords() { + var set = super.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., + * @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) { + 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); + } + + return result; + } + + /* * TODO put relative bounds in a constant field Consider creating a Bounds * class, or putting them in the XML file - */ - - /** - * The optimisation vector contain both the lower and upper bounds with the - * 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(IndexedVector[] output, List flags) { - int size = output[0].dimension(); - - for (int i = 0; i < size; i++) { - - switch (output[0].getIndex(i)) { - case UPPER_BOUND: - output[0].set(i, segment.getMaximum()); - output[1].set(i, 0.25 * segment.getMaximum()); - break; - case LOWER_BOUND: - output[0].set(i, segment.getMinimum()); - output[1].set(i, 0.25 * segment.getMaximum()); - break; - default: - continue; - } - - } - - } - - /** - * Tries to assign the upper and lower bound based on {@code params}. - * - * @param params an {@code IndexedVector} which may contain the bounds. - */ - - @Override - public void assign(IndexedVector params) { - - NumericProperty p = null; - - for (int i = 0, size = params.dimension(); i < size; i++) { - - p = derive(params.getIndex(i), params.get(i)); - - switch (params.getIndex(i)) { - case UPPER_BOUND: - setUpperBound(p); - break; - case LOWER_BOUND: - setLowerBound(p); - break; - default: - continue; - } - - } - - } - - @Override - public String toString() { - return "Range given by: " + segment.toString(); - } - -} \ No newline at end of file + */ + /** + * The optimisation vector contain both the lower and upper bounds with the + * absolute constraints equal to a fourth of their values. + * + * @param output the vector to be updated + */ + @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); + value = segment.getMaximum(); + break; + case LOWER_BOUND: + bounds = boundLimits(false); + value = segment.getMinimum(); + break; + default: + continue; + } + + var transform = new StickTransform(bounds); + + p.setBounds(bounds); + p.setTransform(transform); + p.setValue(value); + + } + + } + + /** + * Tries to assign the upper and lower bound based on {@code params}. + * + * @param params an {@code IndexedVector} which may contain the bounds. + * @throws SolverException + */ + @Override + public void assign(ParameterVector params) throws SolverException { + for (Parameter p : params.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + var np = derive(key, p.inverseTransform()); + + switch (key) { + case UPPER_BOUND: + setUpperBound(np); + break; + case LOWER_BOUND: + setLowerBound(np); + break; + default: + } + + } + + } + + @Override + public String toString() { + return "Range given by: " + segment.toString(); + } + +} diff --git a/src/main/java/pulse/input/listeners/CurveEvent.java b/src/main/java/pulse/input/listeners/CurveEvent.java index feeadad0..61413bf0 100644 --- a/src/main/java/pulse/input/listeners/CurveEvent.java +++ b/src/main/java/pulse/input/listeners/CurveEvent.java @@ -1,44 +1,34 @@ package pulse.input.listeners; -import pulse.AbstractData; - -public class CurveEvent { - - private CurveEventType type; - private AbstractData 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, AbstractData data) { - this.type = type; - this.data = data; - } - - /** - * Used to get the type of this event. - * - * @return the type of this event - */ - - 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 AbstractData getData() { - return data; - } - -} \ No newline at end of file +import java.io.Serializable; + +/** + * A {@code CurveEvent} is associated with an {@code HeatingCurve} object. + * + * @see pulse.HeatingCurve + * + */ +public class CurveEvent implements Serializable { + + private CurveEventType type; + + /** + * Constructs a {@code CurveEvent} object, combining the {@code type} and + * associated {@code data} + * + * @param type the type of this event + */ + public CurveEvent(CurveEventType type) { + this.type = type; + } + + /** + * Used to get the type of this event. + * + * @return the type of this event + */ + public CurveEventType getType() { + return type; + } + +} diff --git a/src/main/java/pulse/input/listeners/CurveEventType.java b/src/main/java/pulse/input/listeners/CurveEventType.java index 3d4f7de2..40a55d3d 100644 --- a/src/main/java/pulse/input/listeners/CurveEventType.java +++ b/src/main/java/pulse/input/listeners/CurveEventType.java @@ -1,17 +1,28 @@ package pulse.input.listeners; +/** + * An event type associated with an {@code HeatingCurve} object. + * + */ public enum CurveEventType { - RESCALED, - - /** - *

- * Signal a time shift between the time sequences of a {@code HeatingCurve} and - * its linked {@code ExperimentalData}. Triggered either when manually changing - * the time origin of the solution (i.e., shifting it relative to the - * experimental data points) or by the search procedure. - */ - - TIME_ORIGIN_CHANGED; - -} \ No newline at end of file + /** + * Indicates the curve signal values have been re-scaled. This means that + * each signal value has been multiplied by a single number. + */ + RESCALED, + /** + * Indicates a new time shift is introduced between the time sequences of a + * {@code HeatingCurve} and its linked {@code ExperimentalData}. Triggered + * either when manually changing the time origin of the solution (i.e., + * shifting it relative to the experimental data points) or by the search + * procedure. + */ + TIME_ORIGIN_CHANGED, + /** + * 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 90fca6da..7fefe1c0 100644 --- a/src/main/java/pulse/input/listeners/DataEvent.java +++ b/src/main/java/pulse/input/listeners/DataEvent.java @@ -1,50 +1,47 @@ package pulse.input.listeners; +import java.io.Serializable; import pulse.AbstractData; /** * A {@code DataEvent} is used to track changes happening with a - * {@code HeatingCurve}. + * {@code ExperimentalData}. * */ - -public class DataEvent { - - private DataEventType type; - private AbstractData data; - - /** - * Constructs a {@code DataEvent} object, combining the {@code type} and - * associated {@code data} - * - * @param type the type of this event - * @param data the source of the event - */ - - public DataEvent(DataEventType type, AbstractData data) { - this.type = type; - this.data = data; - } - - /** - * Used to get the type of this event. - * - * @return the type of this event - */ - - public DataEventType 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 AbstractData getData() { - return data; - } +public class DataEvent implements Serializable { + + private DataEventType type; + private AbstractData data; + + /** + * Constructs a {@code DataEvent} object, combining the {@code type} and + * associated {@code data} + * + * @param type the type of this event + * @param data the source of the event + */ + public DataEvent(DataEventType type, AbstractData data) { + this.type = type; + this.data = data; + } + + /** + * Used to get the type of this event. + * + * @return the type of this event + */ + public DataEventType getType() { + return type; + } + + /** + * Used to get the {@code ExperimentalData} object that has undergone + * certain changes specified by this event type. + * + * @return the associated data + */ + public AbstractData getData() { + return data; + } } diff --git a/src/main/java/pulse/input/listeners/DataEventType.java b/src/main/java/pulse/input/listeners/DataEventType.java index 5106b4bf..52c80851 100644 --- a/src/main/java/pulse/input/listeners/DataEventType.java +++ b/src/main/java/pulse/input/listeners/DataEventType.java @@ -1,24 +1,23 @@ package pulse.input.listeners; /** - *

- * This is an enum type that is used to store some information about the type of - * the {@code DataEvent}s occurring with a {@code HeatingCurve} object. This is - * currently limited to two types of events. - *

+ * An event type that is associated with an {@code ExperimentalData} object. * */ - public enum DataEventType { - /** - *

- * The {@code TRUNCATED} {@code DataEventType} indicates the range of the - * {@code ExperimentalData} has been truncated. Note this means that only the - * range is affected and not the data itself. - * - * @see pulse.input.ExperimentalData.truncate() - */ + /** + *

+ * The {@code RANGE_CHANGED} {@code DataEventType} indicates the range of + * the {@code ExperimentalData} has either been truncated or extended. Note + * this means that only the range is affected and not the data itself. + * + * @see pulse.input.ExperimentalData.truncate() + */ - TRUNCATED + RANGE_CHANGED, + /** + * All data points loaded and are ready for processing. + */ + DATA_LOADED; -} \ No newline at end of file +} diff --git a/src/main/java/pulse/input/listeners/DataListener.java b/src/main/java/pulse/input/listeners/DataListener.java index 44839288..28170c53 100644 --- a/src/main/java/pulse/input/listeners/DataListener.java +++ b/src/main/java/pulse/input/listeners/DataListener.java @@ -1,21 +1,21 @@ package pulse.input.listeners; +import java.io.Serializable; + /** * A listener interface, which is used to listen to {@code DataEvent}s occurring - * with an {@code HeatingCurve object}. + * with an {@code ExperimentalData} object. * */ +public interface DataListener extends Serializable { -public interface DataListener { - - /** - * Triggered when a certain {@code DataEvent} specified by its - * {@code DataEventType} is initiated from within the {@code HeatingCurve} - * object. - * - * @param e the event object. - */ - - public void onDataChanged(DataEvent e); + /** + * Triggered when a certain {@code DataEvent} specified by its + * {@code DataEventType} is initiated from within the + * {@code ExperimentalData} object. + * + * @param e the event object. + */ + public void onDataChanged(DataEvent e); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/input/listeners/ExternalDatasetListener.java b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java new file mode 100644 index 00000000..a73d3d13 --- /dev/null +++ b/src/main/java/pulse/input/listeners/ExternalDatasetListener.java @@ -0,0 +1,14 @@ +package pulse.input.listeners; + +import java.io.Serializable; +/** + * A listener associated with the {@code InterpolationDataset} static repository + * of interpolations. + * + */ +public interface ExternalDatasetListener { + + public void onSpecificHeatDataLoaded(); + public void onDensityDataLoaded(); + +} \ No newline at end of file diff --git a/src/main/java/pulse/input/listeners/package-info.java b/src/main/java/pulse/input/listeners/package-info.java index 64db8c6d..af353d56 100644 --- a/src/main/java/pulse/input/listeners/package-info.java +++ b/src/main/java/pulse/input/listeners/package-info.java @@ -1,8 +1,9 @@ /** - * Package contains listeners and associated types which are used to track any + * Package contains listeners and event types which are used to track any * runtime changes with the internal data structures defined in - * {@code pulse.input}. This currently only includes a listener for - * {@code ExperimentalData}, since the latter can be truncated during runtime. + * {@code pulse} and {@code pulse.input}. + * + * @see pulse + * @see pulse.input */ - -package pulse.input.listeners; \ No newline at end of file +package pulse.input.listeners; diff --git a/src/main/java/pulse/input/package-info.java b/src/main/java/pulse/input/package-info.java index 31eb4608..6590dcad 100644 --- a/src/main/java/pulse/input/package-info.java +++ b/src/main/java/pulse/input/package-info.java @@ -4,5 +4,4 @@ * metadata, and property curves (e.g. specific heat and density); (b) are used * by the {@code TaskManager} or any affiliated class. */ - -package pulse.input; \ No newline at end of file +package pulse.input; diff --git a/src/main/java/pulse/io/export/CurveExporter.java b/src/main/java/pulse/io/export/CurveExporter.java index 01e07968..81636051 100644 --- a/src/main/java/pulse/io/export/CurveExporter.java +++ b/src/main/java/pulse/io/export/CurveExporter.java @@ -10,123 +10,120 @@ import pulse.AbstractData; /** - * A singleton exporter allows writing the data contained in a heating curve in - * a two-column format to create files conforming to either csv or html - * extension. The first column always represents the time sequence, which may be - * shifted if the associated property of the heating curve is non-zero. The - * second column represents the baseline-adjusted signal. + * A singleton exporter allows writing the data contained in a + * {@code AbstractData} object in a two-column format to create files conforming + * to either csv or html extension. The first column always represents the time + * sequence, which may be shifted if the associated property of the heating + * curve is non-zero. The second column represents the baseline-adjusted signal. * */ - public class CurveExporter implements Exporter { - private static CurveExporter instance = new CurveExporter(); - - private CurveExporter() { - // Intentionally blank - } - - @Override - public void printToStream(AbstractData hc, FileOutputStream fos, Extension extension) { - if (hc.actualNumPoints() < 1) - return; - - switch (extension) { - case HTML: - printHTML(hc, fos); - break; - case CSV: - printCSV(hc, fos); - break; - default: - throw new IllegalArgumentException("Format not recognised: " + extension); - } - } - - /** - * Currently {@code html} and {@code csv} extensions are supported. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML, CSV }; - } - - private void printHTML(AbstractData hc, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - stream.print(getString("ResultTableExporter.style")); - stream.print("Time-temperature profile"); - stream.print(""); - - final String TIME_LABEL = getString("HeatingCurve.6"); - final String TEMPERATURE_LABEL = getString("HeatingCurve.7"); - - stream.print("" + TIME_LABEL + "\t"); - stream.print("" + TEMPERATURE_LABEL + "\t"); - - stream.print(""); - - double t; - double T; - - final int size = hc.actualNumPoints(); - - for (int i = 0; i < size; i++) { - stream.print(""); - - stream.print(""); - t = hc.timeAt(i); - stream.printf("%.6f %n", t); - stream.print("\t"); - T = hc.signalAt(i); - stream.printf("%.6f %n", T); - - stream.println(""); - } - - stream.print(""); - } - - } - - private void printCSV(AbstractData hc, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - final String TIME_LABEL = getString("HeatingCurve.6"); - final String TEMPERATURE_LABEL = hc.getPrefix(); - stream.print(TIME_LABEL + "\t" + TEMPERATURE_LABEL + "\t"); - - double t; - double T; - - final int size = hc.actualNumPoints(); - - for (int i = 0; i < size; i++) { - t = hc.timeAt(i); - stream.printf("%n%3.4f", t); - T = hc.signalAt(i); - stream.printf("\t%3.4f", T); - } - } - - } - - /** - * Returns the single instance of this subclass. - * - * @return an instance of {@code HeatingCurveExporter}. - */ - - public static CurveExporter getInstance() { - return instance; - } - - /** - * @return the {@code HeatingCurve} class. - */ - - @Override - public Class target() { - return AbstractData.class; - } - -} \ No newline at end of file + private static CurveExporter instance = new CurveExporter(); + + private CurveExporter() { + // Intentionally blank + } + + @Override + public void printToStream(AbstractData hc, FileOutputStream fos, Extension extension) { + if (hc.actualNumPoints() < 1) { + return; + } + + switch (extension) { + case HTML: + printHTML(hc, fos); + break; + case CSV: + printCSV(hc, fos); + break; + default: + throw new IllegalArgumentException("Format not recognised: " + extension); + } + } + + /** + * Currently {@code html} and {@code csv} extensions are supported. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML, CSV}; + } + + private void printHTML(AbstractData hc, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + stream.print(getString("ResultTableExporter.style")); + stream.print("Time-temperature profile"); + stream.print(""); + + final String TIME_LABEL = getString("HeatingCurve.6"); + final String TEMPERATURE_LABEL = getString("HeatingCurve.7"); + + stream.print("" + TIME_LABEL + "\t"); + stream.print("" + TEMPERATURE_LABEL + "\t"); + + stream.print(""); + + double t; + double T; + + final int size = hc.actualNumPoints(); + + for (int i = 0; i < size; i++) { + stream.print(""); + + stream.print(""); + t = hc.timeAt(i); + stream.printf("%.8f %n", t); + stream.print("\t"); + T = hc.signalAt(i); + stream.printf("%.8f %n", T); + + stream.println(""); + } + + stream.print(""); + } + + } + + private void printCSV(AbstractData hc, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + final String TIME_LABEL = getString("HeatingCurve.6"); + final String TEMPERATURE_LABEL = hc.getPrefix(); + stream.print(TIME_LABEL + "\t" + TEMPERATURE_LABEL + "\t"); + + double t; + double T; + + final int size = hc.actualNumPoints(); + + for (int i = 0; i < size; i++) { + t = hc.timeAt(i); + stream.printf("%n%3.8f", t); + T = hc.signalAt(i); + stream.printf("\t%3.8f", T); + } + } + + } + + /** + * Returns the single instance of this subclass. + * + * @return an instance of {@code HeatingCurveExporter}. + */ + public static CurveExporter getInstance() { + return instance; + } + + /** + * @return the {@code AbstractData} class. + */ + @Override + public Class target() { + return AbstractData.class; + } + +} diff --git a/src/main/java/pulse/io/export/ExportManager.java b/src/main/java/pulse/io/export/ExportManager.java index 7562b475..892402c1 100644 --- a/src/main/java/pulse/io/export/ExportManager.java +++ b/src/main/java/pulse/io/export/ExportManager.java @@ -20,199 +20,194 @@ * suitable for a given target and shortcuts for export operations. * */ - public class ExportManager { - private ExportManager() { - // intentionally blank - } - - /** - * Finds a suitable exporter for a non-null {@code target} by calling - * {@code findExporter(target.getClass())}. - * - * @param an instance of {@code Descriptive} - * @param target the exported target - * @return an exporter that works for {@code target} - * @see findExporter - */ - - @SuppressWarnings("unchecked") - public static Exporter findExporter(T target) { - Objects.requireNonNull(target); - return (Exporter) findExporter(target.getClass()); - } - - /** - * Finds an exporter that can work with {@code target}. - *

- * Searches through available instances of the Exporter class contained in this - * package and checks if any of those have their target set to the argument of - * this method, return the first occurrence. If nothing matches exactly the same - * class as specified, searches for exporters of any classes assignable from - * {@code target}. - *

- * - * @param an instance of {@code Descriptive} - * @param target the target glass - * @return an intancce of the Exporter class that can work worth the type T, - * null if nothing has been found - */ - - @SuppressWarnings({ "unchecked" }) - public static Exporter findExporter(Class target) { - var allExporters = instancesOf(Exporter.class); - var exporter = allExporters.stream().filter(e -> e.target() == target).findFirst(); - - if (exporter.isPresent()) - return exporter.get(); - else { - exporter = allExporters.stream().filter(e -> e.target().isAssignableFrom(target)).findFirst(); - return exporter.isPresent() ? exporter.get() : null; - } - } - - /** - * Finds an exporter matching to {@code target} and allows the user to select - * the location of export. - * - * @param a {@code Descriptive} type - * @param target the target to be exported - * @param parentWindow a frame to which the file chooser dialog will be - * attached - * @param fileTypeLabel a brief description of the exported file types - * @see findExporter - * @see pulse.io.export.Exporter.askToExport() - * @throws IllegalArgumentException if no exporter can be found - */ - - public static void askToExport(T target, JFrame parentWindow, String fileTypeLabel) { - var exporter = findExporter(target); - if (exporter != null) - exporter.askToExport(target, parentWindow, fileTypeLabel); - else - throw new IllegalArgumentException("No exporter for " + target.getClass().getSimpleName()); - } - - /** - * Attempts to export the given {@code target} to the {@code directory} by - * saving the contents in a file with the given {@code Extension}. - *

- * The file is formatted according to the inherent format, i.e. if it is an - * {@code Extension.HTML} file, it will contain HTML tags, etc. If - * {@code extension} is not present in the list of supported extension of an - * exporter matching {@code target}, this will revert to the first supported - * extension. This method will not have any result if no exporter has been found - * fot {@code target}. - *

- * - * @param the target type - * @param target the exported target - * @param directory a pre-selected directory - * @param extension the desired extension - */ - - public static void export(T target, File directory, Extension extension) { - var exporter = findExporter(target); - - if (exporter != null) { - var supportedExtensions = exporter.getSupportedExtensions(); - - if (supportedExtensions.length > 0) { - var confirmedExtension = asList(supportedExtensions).contains(extension) ? extension - : supportedExtensions[0]; - exporter.export(target, directory, confirmedExtension); - } - - } - } - - /** - * This will invoke {@code exportGroup} on each task listed by the - * {@code TaskManager}. - * - * @param directory a pre-selected directory - * @param extension the desired extension - * @see exportGroup - * @see pulse.tasks.TaskManager - */ - - public static void exportAllTasks(File directory, Extension extension) { - TaskManager.getManagerInstance().getTaskList().stream().forEach(t -> exportGroup(t, directory, extension)); - } - - /** - * Exports the currently selected task as a group of objects. - * - * @param directory a pre-selected directory - * @param extension the desired extension - * @see exportGroup - * @see pulse.tasks.TaskManager.getSelectedTask() - */ - - public static void exportCurrentTask(File directory, Extension extension) { - exportGroup(TaskManager.getManagerInstance().getSelectedTask(), directory, extension); - } - - /** - * Exports the currently selected task as a group of objects using the default - * export extension. - * - * @param directory a pre-selected directory - * @see exportGroup - * @see pulse.tasks.TaskManager.getSelectedTask() - */ - - public static void exportCurrentTask(File directory) { - exportCurrentTask(directory, getDefaultExportExtension()); - } - - /** - * Exports all results generated previously during task execution for all tasks - * listed by the TaskManager, provided those tasks had the respective result - * assigned to them. - * - * @param directory a pre-selected directory - * @param extension the desired extension - */ - - public static void exportAllResults(File directory, Extension extension) { - - var instance = TaskManager.getManagerInstance(); - instance.getTaskList().stream().map(t -> instance.getResult(t)).filter(Objects::nonNull) - .forEach(r -> export(r, directory, extension)); - - } - - /** - * Fully exports {@code group} and all its contents to the root - * {@code directory} requesting the files to be saved with the - * {@code extension}. - *

- * If an {@code Exporter} exists that accepts the {@code group} as its argument, - * this will create files in the root {@code directory} in accordance to the - * root {@code Exporter} rules. All contents of the {@code group} will then be - * processed in a similar manner and the output will be stored in an internal - * directory, the name of which conforms to the respective description. Note - * this method is NOT recursive and it calls the {@code export} method of the - * {@code ExportManager}. - *

- * - * @param group a group - * @param directory a pre-selected root directory - * @param extension the desired extension - * @throws IllegalArgumentException if {@code directory} is not a directory - */ - - public static void exportGroup(Group group, File directory, Extension extension) { - if (!directory.isDirectory()) - throw new IllegalArgumentException("Not a directory: " + directory); - - var internalDirectory = new File(directory + separator + group.describe() + separator); - internalDirectory.mkdirs(); - - export(group, directory, extension); - contents(group).stream().forEach(internalHolder -> export(internalHolder, internalDirectory, extension)); - } - -} \ No newline at end of file + //current working dir + private static File cwd = null; + + private ExportManager() { + // intentionally blank + } + + /** + * Finds a suitable exporter for a non-null {@code target} by calling + * {@code findExporter(target.getClass())}. + * + * @param an instance of {@code Descriptive} + * @param target the exported target + * @return an exporter that works for {@code target} + * @see findExporter + */ + @SuppressWarnings("unchecked") + public static Exporter findExporter(T target) { + Objects.requireNonNull(target); + return (Exporter) findExporter(target.getClass()); + } + + /** + * Finds an exporter that can work with {@code target}. + *

+ * Searches through available instances of the {@code Exporter} class + * contained in this package and checks if any of those have their target + * set to the argument of this method, then returns the first occurrence. If + * nothing matches exactly the same class as specified, searches for + * exporters of any classes assignable from {@code target}. + *

+ * + * @param an instance of {@code Descriptive} + * @param target the target glass + * @return an instance of the Exporter class that can work worth the type T, + * null if nothing has been found + */ + @SuppressWarnings({"unchecked"}) + public static Exporter findExporter(Class target) { + var allExporters = instancesOf(Exporter.class); + var exporter = allExporters.stream().filter(e -> e.target() == target).findFirst(); + + if (exporter.isPresent()) { + return exporter.get(); + } else { + exporter = allExporters.stream().filter(e -> e.target().isAssignableFrom(target)).findFirst(); + return exporter.isPresent() ? exporter.get() : null; + } + } + + /** + * Finds an exporter matching to {@code target} and allows the user to + * select the location of export. + * + * @param a {@code Descriptive} type + * @param target the target to be exported + * @param parentWindow a frame to which the file chooser dialog will be + * attached + * @param fileTypeLabel a brief description of the exported file types + * @see findExporter + * @see pulse.io.export.Exporter.askToExport() + * @throws IllegalArgumentException if no exporter can be found + */ + public static void askToExport(T target, JFrame parentWindow, String fileTypeLabel) { + var exporter = findExporter(target); + if (exporter != null) { + cwd = exporter.askToExport(target, parentWindow, fileTypeLabel, cwd); + } else { + throw new IllegalArgumentException("No exporter for " + target.getClass().getSimpleName()); + } + } + + /** + * Attempts to export the given {@code target} to the {@code directory} by + * saving the contents in a file with the given {@code Extension}. + *

+ * The file is formatted according to the inherent format, i.e. if it is an + * {@code Extension.HTML} file, it will contain HTML tags, etc. If + * {@code extension} is not present in the list of supported extension of an + * exporter matching {@code target}, this will revert to the first supported + * extension. This method will not have any result if no exporter has been + * found fot {@code target}. + *

+ * + * @param the target type + * @param target the exported target + * @param directory a pre-selected directory + * @param extension the desired extension + */ + public static void export(T target, File directory, Extension extension) { + var exporter = findExporter(target); + + if (exporter != null) { + var supportedExtensions = exporter.getSupportedExtensions(); + + if (supportedExtensions.length > 0) { + var confirmedExtension = asList(supportedExtensions).contains(extension) ? extension + : supportedExtensions[0]; + exporter.export(target, directory, confirmedExtension); + } + + } + } + + /** + * This will invoke {@code exportGroup} on each task listed by the + * {@code TaskManager}. + * + * @param directory a pre-selected directory + * @param extension the desired extension + * @see exportGroup + * @see pulse.tasks.TaskManager + */ + public static void exportAllTasks(File directory, Extension extension) { + TaskManager.getManagerInstance().getTaskList().stream().forEach(t -> exportGroup(t, directory, extension)); + } + + /** + * Exports the currently selected task as a group of objects. + * + * @param directory a pre-selected directory + * @param extension the desired extension + * @see exportGroup + * @see pulse.tasks.TaskManager.getSelectedTask() + */ + public static void exportCurrentTask(File directory, Extension extension) { + exportGroup(TaskManager.getManagerInstance().getSelectedTask(), directory, extension); + } + + /** + * Exports the currently selected task as a group of objects using the + * default export extension. + * + * @param directory a pre-selected directory + * @see exportGroup + * @see pulse.tasks.TaskManager.getSelectedTask() + */ + public static void exportCurrentTask(File directory) { + exportCurrentTask(directory, getDefaultExportExtension()); + } + + /** + * Exports all results generated previously during task execution for all + * tasks listed by the TaskManager, provided those tasks had the respective + * result assigned to them. + * + * @param directory a pre-selected directory + * @param extension the desired extension + */ + public static void exportAllResults(File directory, Extension extension) { + + var instance = TaskManager.getManagerInstance(); + instance.getTaskList().stream().map(t -> t.getStoredCalculations()).flatMap(x -> x.stream()).filter(Objects::nonNull) + .forEach(r -> export(r, directory, extension)); + + } + + /** + * Fully exports {@code group} and all its contents to the root + * {@code directory} requesting the files to be saved with the + * {@code extension}. + *

+ * If an {@code Exporter} exists that accepts the {@code group} as its + * argument, this will create files in the root {@code directory} in + * accordance to the root {@code Exporter} rules. All contents of the + * {@code group} will then be processed in a similar manner and the output + * will be stored in an internal directory, the name of which conforms to + * the respective description. Note this method is NOT recursive and it + * calls the {@code export} method of the {@code ExportManager}. + *

+ * + * @param group a group + * @param directory a pre-selected root directory + * @param extension the desired extension + * @throws IllegalArgumentException if {@code directory} is not a directory + */ + public static void exportGroup(Group group, File directory, Extension extension) { + if (!directory.isDirectory()) { + throw new IllegalArgumentException("Not a directory: " + directory); + } + + var internalDirectory = new File(directory + separator + group.describe() + separator); + internalDirectory.mkdirs(); + + export(group, directory, extension); + contents(group).stream().forEach(internalHolder -> export(internalHolder, internalDirectory, extension)); + } + +} diff --git a/src/main/java/pulse/io/export/Exporter.java b/src/main/java/pulse/io/export/Exporter.java index ff5bdccd..d2d35c0a 100644 --- a/src/main/java/pulse/io/export/Exporter.java +++ b/src/main/java/pulse/io/export/Exporter.java @@ -17,144 +17,154 @@ * type of PULsE objects (typically, instances of the PropertyHolder class). * */ - public interface Exporter extends Reflexive { - /** - * Gets the default export extension. If not overriden, will return - * {@code Extension.CSV}. - * - * @return the default export extension - */ - - public static Extension getDefaultExportExtension() { - return Extension.CSV; - } - - /** - * Returns an array of supported extensions, which by default contains only the - * default extension. - * - * @return an array with {@code Extension} type objects. - */ - - public default Extension[] getSupportedExtensions() { - return new Extension[] { getDefaultExportExtension() }; - } - - /** - * Exports the available contents to {@code directory} without asking a - * confirmation from the user. - *

- * A file is created with the name specified by the {@code describe()} method of - * {@code target} with the extension equal to the third argument of this method. - * A {@code FileOutputStream} writes the contents to the file by invoking - * {@code printToStream} and is closed upon completion. - *

- * - * @param directory the directory where the contents will be exported to. - * @param target a {@code Descriptive} target - * @param extension the file extension. If it is not supported, the exporter - * will revert to its default extension - * @throws IllegalArgumentException if {@code directory} is not really a - * directory - * @see printToStream - */ - - public default void export(T target, File directory, Extension extension) { - - if (!directory.isDirectory()) - throw new IllegalArgumentException("Not a directory: " + directory); - - var supportedExtension = extension; - - if (!Arrays.stream(getSupportedExtensions()).anyMatch(extension::equals)) - supportedExtension = getDefaultExportExtension(); // revert to default extension - - try { - var newFile = new File(directory, target.describe() + "." + supportedExtension); - newFile.createNewFile(); - var fos = new FileOutputStream(newFile); - printToStream(target, fos, supportedExtension); - fos.close(); - } catch (IOException e) { - System.err.println("An exception has been encountered while writing the contents of " - + target.getClass().getSimpleName() + " to " + directory); - e.printStackTrace(); - } - - } - - /** - * Provides a {@code JFileChooser} for the user to select the export destination - * for {@code target}. The name of the file and its extension come from the - * selection the user makes by interacting with the dialog. - * - * @param target the exported target - * @param parentWindow the parent frame. - * @param fileTypeLabel the label describing the specific type of files that - * will be saved. - */ - - public default void askToExport(T target, JFrame parentWindow, String fileTypeLabel) { - var fileChooser = new JFileChooser(); - var workingDirectory = new File(System.getProperty("user.home")); - fileChooser.setCurrentDirectory(workingDirectory); - fileChooser.setMultiSelectionEnabled(true); - fileChooser.setSelectedFile(new File(target.describe())); - - for (var s : getSupportedExtensions()) { - fileChooser.addChoosableFileFilter( - new FileNameExtensionFilter(fileTypeLabel + " (." + s + ")", s.toString().toLowerCase())); - } - - fileChooser.setAcceptAllFileFilterUsed(false); - - int returnVal = fileChooser.showSaveDialog(parentWindow); - if (returnVal == JFileChooser.APPROVE_OPTION) { - var file = fileChooser.getSelectedFile(); - var path = file.getPath(); - var currentFilter = (FileNameExtensionFilter) fileChooser.getFileFilter(); - var ext = currentFilter.getExtensions()[0]; - - if (!path.contains(".")) - file = new File(path + "." + ext); - - try { - var fos = new FileOutputStream(file); - printToStream(target, fos, Extension.valueOf(ext.toUpperCase())); - fos.close(); - } catch (IOException e) { - System.err.println("An exception has been encountered while writing the contents of " - + target.getClass().getSimpleName() + " to " + file); - e.printStackTrace(); - } - } - } - - /** - * Defines the class, instances of which can be fed into the exporter to produce - * a result. - * - * @return a class implementing the {@code Descriptive} interface. - */ - - public Class target(); - - /** - * The interface method is implemented by the subclasses to define the - * exportable content in detail. Depending on the supported extensions, this - * will typically involve a switch statement that will invoke private methods - * defined in the subclass handling the different choices. The stream - * {@code fos} is usually closed implicitly in a try-with-resource clause. - * - * @param target the exported target - * @param fos a FileOutputStream created by the {@code export} method - * @param extension an extension of the file saved on disk - * @see export - * @see askToExport - */ - - public void printToStream(T target, FileOutputStream fos, Extension extension); - -} \ No newline at end of file + /** + * Gets the default export extension. + * + * @return {@code Extension.CSV} by default + */ + public static Extension getDefaultExportExtension() { + return Extension.CSV; + } + + /** + * Returns an array of supported extensions, which by default contains only + * the default extension. + * + * @return an array with {@code Extension} type objects. + */ + public default Extension[] getSupportedExtensions() { + return new Extension[]{getDefaultExportExtension()}; + } + + /** + * Exports the available contents to {@code directory} without asking a + * confirmation from the user. + *

+ * A file is created with the name specified by the {@code describe()} + * method of {@code target} with the extension equal to the third argument + * of this method. A {@code FileOutputStream} writes the contents to the + * file by invoking {@code printToStream} and is closed upon completion. + *

+ * + * @param directory the directory where the contents will be exported to. + * @param target a {@code Descriptive} target + * @param extension the file extension. If it is not supported, the exporter + * will revert to its default extension + * @throws IllegalArgumentException if {@code directory} is not really a + * directory + * @see printToStream + */ + public default void export(T target, File directory, Extension extension) { + + if (!directory.isDirectory()) { + throw new IllegalArgumentException("Not a directory: " + directory); + } + + var supportedExtension = extension; + + if (!Arrays.stream(getSupportedExtensions()).anyMatch(extension::equals)) { + supportedExtension = getDefaultExportExtension(); // revert to default extension + } + try { + var newFile = new File(directory, target.describe() + "." + supportedExtension); + newFile.createNewFile(); + var fos = new FileOutputStream(newFile); + printToStream(target, fos, supportedExtension); + fos.close(); + } catch (IOException e) { + System.err.println("An exception has been encountered while writing the contents of " + + target.getClass().getSimpleName() + " to " + directory); + e.printStackTrace(); + } + + } + + /** + * Provides a {@code JFileChooser} for the user to select the export + * destination for {@code target}. The name of the file and its extension + * come from the selection the user makes by interacting with the dialog. + * + * @param target the exported target + * @param parentWindow the parent frame. + * @param fileTypeLabel the label describing the specific type of files that + * @param directory the default directory of the file will be saved. + * @return the directory where files were exported + */ + public default File askToExport(T target, JFrame parentWindow, String fileTypeLabel, File directory) { + var fileChooser = new JFileChooser(directory); + fileChooser.setMultiSelectionEnabled(true); + + FileNameExtensionFilter choosable = null; + + for (var s : getSupportedExtensions()) { + choosable = new FileNameExtensionFilter(fileTypeLabel + " (." + s + ")", s.toString().toLowerCase()); + fileChooser.addChoosableFileFilter(choosable); + } + + fileChooser.setAcceptAllFileFilterUsed(false); + fileChooser.setFileFilter(choosable); + fileChooser.setSelectedFile(new File(target.describe() + "." + choosable.getExtensions()[0])); + + int returnVal = fileChooser.showSaveDialog(parentWindow); + if (returnVal == JFileChooser.APPROVE_OPTION) { + var file = fileChooser.getSelectedFile(); + var path = file.getPath(); + + directory = file.isDirectory() ? file : file.getParentFile(); + + if ((fileChooser.getFileFilter() instanceof FileNameExtensionFilter)) { + + var currentFilter = (FileNameExtensionFilter) fileChooser.getFileFilter(); + var ext = currentFilter.getExtensions()[0]; + + if (!path.contains(".")) { + file = new File(path + "." + ext); + } else { + file = new File(path.substring(0, path.indexOf(".") + 1) + ext); + } + + try { + var fos = new FileOutputStream(file); + printToStream(target, fos, Extension.valueOf(ext.toUpperCase())); + fos.close(); + } catch (IOException e) { + System.err.println("An exception has been encountered while writing the contents of " + + target.getClass().getSimpleName() + " to " + file); + e.printStackTrace(); + } + + } + + } + + return directory; + + } + + /** + * Defines the class, instances of which can be fed into the exporter to + * produce a result. + * + * @return a class implementing the {@code Descriptive} interface. + */ + public Class target(); + + /** + * The interface method is implemented by the subclasses to define the + * exportable content in detail. Depending on the supported extensions, this + * will typically involve a switch statement that will invoke private + * methods defined in the subclass handling the different choices. The + * stream {@code fos} is usually closed implicitly in a try-with-resource + * clause. + * + * @param target the exported target + * @param fos a FileOutputStream created by the {@code export} method + * @param extension an extension of the file saved on disk + * @see export + * @see askToExport + */ + public void printToStream(T target, FileOutputStream fos, Extension extension); + +} diff --git a/src/main/java/pulse/io/export/Extension.java b/src/main/java/pulse/io/export/Extension.java index 33c86bfb..c0cff319 100644 --- a/src/main/java/pulse/io/export/Extension.java +++ b/src/main/java/pulse/io/export/Extension.java @@ -6,32 +6,28 @@ * class are responsible for observing adherence to that format. * */ - public enum Extension { - /** - * The result will be a document with html tags that can be viewed in any web - * browser. Useful for complex formatting, but not for data manipulation, as the - * tags and special symbols do not allow simple parsing through the file. - */ - - HTML, - - /** - * The result will be a tab-delimited CSV document. Useful for data - * manipulations and plotting with external tools, e.g. gnuplot or LaTeX. - */ - - CSV; - - /** - * This will return the lower-case string with the name of the extension (e.g., - * html or csv). - */ - - @Override - public String toString() { - return super.toString().toLowerCase(); - } - -} \ No newline at end of file + /** + * The result will be a document with html tags that can be viewed in any + * web browser. Useful for complex formatting, but not for data + * manipulation, as the tags and special symbols do not allow simple parsing + * through the file. + */ + HTML, + /** + * The result will be a tab-delimited CSV document. Useful for data + * manipulations and plotting with external tools, e.g. gnuplot or LaTeX. + */ + CSV; + + /** + * This will return the lower-case string with the name of the extension + * (e.g., html or csv). + */ + @Override + public String toString() { + return super.toString().toLowerCase(); + } + +} diff --git a/src/main/java/pulse/io/export/LogExporter.java b/src/main/java/pulse/io/export/LogExporter.java index 9dfb1349..1f37b628 100644 --- a/src/main/java/pulse/io/export/LogExporter.java +++ b/src/main/java/pulse/io/export/LogExporter.java @@ -14,65 +14,60 @@ * supported. * */ - public class LogExporter implements Exporter { - private static LogExporter instance = new LogExporter(); - - private LogExporter() { - // intentionally blank - } - - /** - * Gets the only static instance of this subclass. - * - * @return an instance of{@code LogExporter}. - */ - - public static LogExporter getInstance() { - return instance; - } - - /** - * Prints all the data contained in this {@code Log} using {@code fos}. By - * default, this will output all data in an {@code html} format. Note this - * implementation ignores the {@code extension} parameter. After execution, the - * stream is explicitly closed. - * - * @param log a log to be exported - * @param fos an output stream - * @param extension the desired extension - * @see pulse.tasks.Log.toString() - */ - - @Override - public void printToStream(Log log, FileOutputStream fos, Extension extension) { - var stream = new PrintStream(fos); - stream.print(log.toString()); - try { - fos.close(); - } catch (IOException e) { - System.err.println("Unable to close stream"); - e.printStackTrace(); - } - } - - /** - * @return {@code Log.class}. - */ - - @Override - public Class target() { - return Log.class; - } - - /** - * Only html is currently supported by this exporter. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML }; - } - -} \ No newline at end of file + private static LogExporter instance = new LogExporter(); + + private LogExporter() { + // intentionally blank + } + + /** + * Gets the only static instance of this subclass. + * + * @return an instance of{@code LogExporter}. + */ + public static LogExporter getInstance() { + return instance; + } + + /** + * Prints all the data contained in this {@code Log} using {@code fos}. By + * default, this will output all data in an {@code html} format. Note this + * implementation ignores the {@code extension} parameter. After execution, + * the stream is explicitly closed. + * + * @param log a log to be exported + * @param fos an output stream + * @param extension the desired extension + * @see pulse.tasks.Log.toString() + */ + @Override + public void printToStream(Log log, FileOutputStream fos, Extension extension) { + var stream = new PrintStream(fos); + stream.print(log.toString()); + try { + fos.close(); + } catch (IOException e) { + System.err.println("Unable to close stream"); + e.printStackTrace(); + } + } + + /** + * @return {@code Log.class}. + */ + @Override + public Class target() { + return Log.class; + } + + /** + * Only html is currently supported by this exporter. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML}; + } + +} diff --git a/src/main/java/pulse/io/export/LogPaneExporter.java b/src/main/java/pulse/io/export/LogPaneExporter.java deleted file mode 100644 index f801c2e7..00000000 --- a/src/main/java/pulse/io/export/LogPaneExporter.java +++ /dev/null @@ -1,78 +0,0 @@ -package pulse.io.export; - -import static pulse.io.export.Extension.HTML; - -import java.io.FileOutputStream; -import java.io.IOException; - -import javax.swing.text.BadLocationException; -import javax.swing.text.html.HTMLEditorKit; - -import pulse.ui.components.LogPane; - -/** - * 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 { - - private static LogPaneExporter instance = new LogPaneExporter(); - - private LogPaneExporter() { - // intentionally blank - } - - /** - * This will write all contents of {@code pane}, which are accessed using an - * {@code HTMLEditorKit} directly to {@code fos}. The {@code extension} 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(); - try { - kit.write(fos, pane.getDocument(), 0, pane.getDocument().getLength()); - } catch (IOException | BadLocationException e) { - System.err.println("Could not export the log pane!"); - e.printStackTrace(); - } - try { - fos.close(); - } catch (IOException e) { - System.err.println("Unable to close stream"); - e.printStackTrace(); - } - } - - /** - * Gets the only static instance of this subclass. - * - * @return an instance of{@code LogPaneExporter}. - */ - - public static LogPaneExporter getInstance() { - return instance; - } - - /** - * @return {@code LogPane.class}. - */ - - @Override - public Class target() { - return LogPane.class; - } - - /** - * Only html is currently supported by this exporter. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML }; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/io/export/MetadataExporter.java b/src/main/java/pulse/io/export/MetadataExporter.java index 4af47a23..9851c854 100644 --- a/src/main/java/pulse/io/export/MetadataExporter.java +++ b/src/main/java/pulse/io/export/MetadataExporter.java @@ -13,102 +13,97 @@ * A singleton class used to export {@code Metadata} objects in a html format. * */ - public class MetadataExporter implements Exporter { - private static MetadataExporter instance = new MetadataExporter(); - - private MetadataExporter() { - // intentionally left blank - } - - /** - * Retrieves the single instance of this class. - * - * @return a single instance of {@code MetadataExporter}. - */ - - public static MetadataExporter getInstance() { - return instance; - } - - /** - * Prints the metadata content in html format in two columns, where the first - * column forms the description of the entry and the second column gives its - * value. Extension is ignored, as only html is supported. - */ - - @Override - public void printToStream(Metadata metadata, FileOutputStream fos, Extension extension) { - printHTML(metadata, fos); - } - - private void printHTML(Metadata meta, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - stream.print(Messages.getString("ResultTableExporter.style")); - stream.print("Metadata table"); - stream.print(""); - - final String METADATA_LABEL = "Metadata"; - final String VALUE_LABEL = "Value"; - - stream.print(""); - stream.print(""); - stream.print(METADATA_LABEL + "\t"); - stream.print(""); - stream.print(""); - stream.print(VALUE_LABEL + "\t"); - stream.print(""); - - stream.print(""); - - var data = meta.data(); - - data.forEach(entry -> { - stream.print(""); - - stream.print(""); - stream.print(entry.getDescriptor(false)); - stream.print(""); - stream.print(entry.formattedOutput()); - // possible error typecast property -> object - stream.print(""); - - stream.println(""); - }); - - stream.print(""); - stream.print(""); - } - } - - /** - * Ignores metadata whose external IDs are negative, otherwise calls the - * superclass method. - */ - - @Override - public void export(Metadata metadata, File file, Extension extension) { - if (metadata.getExternalID() > -1) - Exporter.super.export(metadata, file, extension); - } - - /** - * @return {@code Metadata.class} - */ - - @Override - public Class target() { - return Metadata.class; - } - - /** - * @return a single-element array containing {@code Extension.HTML} - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML }; - } - -} \ No newline at end of file + private static MetadataExporter instance = new MetadataExporter(); + + private MetadataExporter() { + // intentionally left blank + } + + /** + * Retrieves the single instance of this class. + * + * @return a single instance of {@code MetadataExporter}. + */ + public static MetadataExporter getInstance() { + return instance; + } + + /** + * Prints the metadata content in html format in two columns, where the + * first column forms the description of the entry and the second column + * gives its value. Extension is ignored, as only html is supported. + */ + @Override + public void printToStream(Metadata metadata, FileOutputStream fos, Extension extension) { + printHTML(metadata, fos); + } + + private void printHTML(Metadata meta, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + stream.print(Messages.getString("ResultTableExporter.style")); + stream.print("Metadata table"); + stream.print(""); + + final String METADATA_LABEL = "Metadata"; + final String VALUE_LABEL = "Value"; + + stream.print(""); + stream.print(""); + stream.print(METADATA_LABEL + "\t"); + stream.print(""); + stream.print(""); + stream.print(VALUE_LABEL + "\t"); + stream.print(""); + + stream.print(""); + + var data = meta.data(); + + data.forEach(entry -> { + stream.print(""); + + stream.print(""); + stream.print(entry.getDescriptor(false)); + stream.print(""); + stream.print(entry.formattedOutput()); + // possible error typecast property -> object + stream.print(""); + + stream.println(""); + }); + + stream.print(""); + stream.print(""); + } + } + + /** + * Ignores metadata whose external IDs are negative, otherwise calls the + * superclass method. + */ + @Override + public void export(Metadata metadata, File file, Extension extension) { + if (metadata.getExternalID() > -1) { + Exporter.super.export(metadata, file, extension); + } + } + + /** + * @return {@code Metadata.class} + */ + @Override + public Class target() { + return Metadata.class; + } + + /** + * @return a single-element array containing {@code Extension.HTML} + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML}; + } + +} diff --git a/src/main/java/pulse/io/export/RawDataExporter.java b/src/main/java/pulse/io/export/RawDataExporter.java index 11a8452e..1ebeb2c4 100644 --- a/src/main/java/pulse/io/export/RawDataExporter.java +++ b/src/main/java/pulse/io/export/RawDataExporter.java @@ -7,58 +7,53 @@ /** * A wrapper singleton class that is made specifically to handle export requests * of {@code ExperimentalData}. Does exactly the same as the - * {@code HeatingCurveExporter}, except that its target is specifically set to + * {@code CurveExporter}, except that its target is specifically set to * {@code ExperimentalData}. - * + * * @see pulse.ui.frames.dialogs.ExportDialog * */ - public class RawDataExporter implements Exporter { - private static RawDataExporter instance = new RawDataExporter(); - private static CurveExporter hcExporter = CurveExporter.getInstance(); - - private RawDataExporter() { - // intentionally left blank - } - - /** - * Retrieves the single static instance of this class - * - * @return an instance of {@code RawDataExporter}. - */ - - public static RawDataExporter getInstance() { - return instance; - } - - /** - * @return {@code ExperimentalData.class} - */ - - @Override - public Class target() { - return ExperimentalData.class; - } - - /** - * Invokes the {@code printToStream(...)} method of the - * {@code HeatingCurveExporter} instance. - */ - - @Override - public void printToStream(ExperimentalData target, FileOutputStream fos, Extension extension) { - hcExporter.printToStream(target, fos, extension); - } - - /** - * Currently {@code html} and {@code csv} extensions are supported. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { Extension.HTML, Extension.CSV }; - } - -} \ No newline at end of file + private static RawDataExporter instance = new RawDataExporter(); + private static CurveExporter hcExporter = CurveExporter.getInstance(); + + private RawDataExporter() { + // intentionally left blank + } + + /** + * Retrieves the single static instance of this class + * + * @return an instance of {@code RawDataExporter}. + */ + public static RawDataExporter getInstance() { + return instance; + } + + /** + * @return {@code ExperimentalData.class} + */ + @Override + public Class target() { + return ExperimentalData.class; + } + + /** + * Invokes the {@code printToStream(...)} method of the + * {@code HeatingCurveExporter} instance. + */ + @Override + public void printToStream(ExperimentalData target, FileOutputStream fos, Extension extension) { + hcExporter.printToStream(target, fos, extension); + } + + /** + * Currently {@code html} and {@code csv} extensions are supported. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{Extension.HTML, Extension.CSV}; + } + +} diff --git a/src/main/java/pulse/io/export/ResidualStatisticExporter.java b/src/main/java/pulse/io/export/ResidualStatisticExporter.java index 9770c5da..4430ec50 100644 --- a/src/main/java/pulse/io/export/ResidualStatisticExporter.java +++ b/src/main/java/pulse/io/export/ResidualStatisticExporter.java @@ -14,103 +14,100 @@ * in time. Implements both the csv and html formats. * */ - public class ResidualStatisticExporter implements Exporter { - private static ResidualStatisticExporter instance = new ResidualStatisticExporter(); - - private ResidualStatisticExporter() { - // intentionally left blank - } - - /** - * @return {@code ResidualStatistic.class} - */ - - @Override - public Class target() { - return ResidualStatistic.class; - } - - /** - * Prints the residuals in a two-column format in a {@code html} or {@code csv} - * file (accepts both extensions). - */ - - @Override - public void printToStream(ResidualStatistic rs, FileOutputStream fos, Extension extension) { - switch (extension) { - case HTML: - printHTML(rs, fos); - break; - case CSV: - printCSV(rs, fos); - break; - default: - throw new IllegalArgumentException("Format not recognised: " + extension); - } - } - - /** - * The supported extensions for exporting the data contained in this object. - * Currently include {@code .html} and {@code .csv}. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML, CSV }; - } - - private void printHTML(ResidualStatistic hc, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - var residuals = hc.getResiduals(); - int residualsLength = residuals == null ? 0 : residuals.size(); - stream.print(getString("ResultTableExporter.style")); - stream.print("Time profile of residuals"); - stream.print(""); - final String TIME_LABEL = getString("HeatingCurve.6"); - final String RESIDUAL_LABEL = "Residual"; - stream.print("" + TIME_LABEL + "\t"); - stream.print("" + RESIDUAL_LABEL + "\t"); - stream.print(""); - - for (int i = 0; i < residualsLength; i++) { - double tr = residuals.get(i)[0]; - double Tr = residuals.get(i)[1]; - stream.printf("%n%.6f%.6f", tr, Tr); - } - - stream.print(""); - } - - } - - private void printCSV(ResidualStatistic hc, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - var residuals = hc.getResiduals(); - int residualsLength = residuals == null ? 0 : residuals.size(); - final String TIME_LABEL = getString("HeatingCurve.6"); - final String RESIDUAL_LABEL = "Residual"; - stream.print(TIME_LABEL + "\t" + RESIDUAL_LABEL + "\t"); - double tr, Tr; - for (int i = 0; i < residualsLength; i++) { - tr = residuals.get(i)[0]; - stream.printf("%n%3.4f", tr); - Tr = residuals.get(i)[1]; - stream.printf("\t%3.4f", Tr); - } - } - - } - - /** - * Retrieves the single instance of this class. - * - * @return a single instance of {@code ResidualStatisticExporter}. - */ - - public static ResidualStatisticExporter getInstance() { - return instance; - } + private static ResidualStatisticExporter instance = new ResidualStatisticExporter(); + + private ResidualStatisticExporter() { + // intentionally left blank + } + + /** + * @return {@code ResidualStatistic.class} + */ + @Override + public Class target() { + return ResidualStatistic.class; + } + + /** + * Prints the residuals in a two-column format in a {@code html} or + * {@code csv} file (accepts both extensions). + */ + @Override + public void printToStream(ResidualStatistic rs, FileOutputStream fos, Extension extension) { + switch (extension) { + case HTML: + printHTML(rs, fos); + break; + case CSV: + printCSV(rs, fos); + break; + default: + throw new IllegalArgumentException("Format not recognised: " + extension); + } + } + + /** + * The supported extensions for exporting the data contained in this object. + * Currently include {@code .html} and {@code .csv}. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML, CSV}; + } + + 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")); + stream.print("Time profile of residuals"); + stream.print(""); + final String TIME_LABEL = getString("HeatingCurve.6"); + final String RESIDUAL_LABEL = "Residual"; + stream.print("" + TIME_LABEL + "\t"); + stream.print("" + RESIDUAL_LABEL + "\t"); + stream.print(""); + + for (int i = 0; i < residualsLength; i++) { + double tr = time.get(i); + double Tr = residuals.get(i); + stream.printf("%n%.8f%.8f", tr, Tr); + } + + stream.print(""); + } + + } + + 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"); + final String RESIDUAL_LABEL = "Residual"; + stream.print(TIME_LABEL + "\t" + RESIDUAL_LABEL + "\t"); + double tr, Tr; + for (int i = 0; i < residualsLength; i++) { + tr = time.get(i); + stream.printf("%n%3.8f", tr); + Tr = residuals.get(i); + stream.printf("\t%3.8f", Tr); + } + } + + } + + /** + * Retrieves the single instance of this class. + * + * @return a single instance of {@code ResidualStatisticExporter}. + */ + public static ResidualStatisticExporter getInstance() { + return instance; + } } diff --git a/src/main/java/pulse/io/export/ResultExporter.java b/src/main/java/pulse/io/export/ResultExporter.java index 4c2f9ce0..4babbbb5 100644 --- a/src/main/java/pulse/io/export/ResultExporter.java +++ b/src/main/java/pulse/io/export/ResultExporter.java @@ -10,96 +10,92 @@ import pulse.ui.Messages; /** - * Provides export capabilities for instances of the {@code Result} class both + * Provides export capabilities, for instances, of the {@code Result} class both * in the {@code csv} and {@code html} formats. * */ - public class ResultExporter implements Exporter { - private static ResultExporter instance = new ResultExporter(); - - private ResultExporter() { - // intentionally blank - } - - /** - * Prints the data of this {@code Result} with {@code fos} either in a - * {@code html} or a {@code csv} file format. - */ - - @Override - public void printToStream(Result result, FileOutputStream fos, Extension extension) { - switch (extension) { - case HTML: - printHTML(result, fos); - break; - case CSV: - printCSV(result, fos); - break; - default: - throw new IllegalArgumentException("Format not recognised: " + extension); - } - } - - private void printHTML(Result result, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - stream.print(Messages.getString("ResultTableExporter.style")); - stream.print("Calculated parameters"); - - for (var p : result.getProperties()) { - stream.print(""); - stream.print(""); - - stream.print(p.getDescriptor(true)); - stream.print(""); - stream.print(p.formattedOutput()); - - stream.print(""); - stream.println(""); - } - - stream.print(""); - } - } - - /** - * Currently the supported extensions include {@code .html} and {@code .csv}. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { HTML, CSV }; - } - - private void printCSV(Result result, FileOutputStream fos) { - try (var stream = new PrintStream(fos)) { - stream.print("(Results)"); - - for (var p : result.getProperties()) { - stream.printf("%n%-24.12s", p.getType()); - stream.printf("\t%-24.12s", p.formattedOutput()); - } - } - } - - /** - * @return {@code Result.class} - */ - - @Override - public Class target() { - return Result.class; - } - - /** - * Returns the single static instance of this class. - * - * @return instance an instance of this class. - */ - - public static ResultExporter getInstance() { - return instance; - } - -} \ No newline at end of file + private static ResultExporter instance = new ResultExporter(); + + private ResultExporter() { + // intentionally blank + } + + /** + * Prints the data of this {@code Result} with {@code fos} either in a + * {@code html} or a {@code csv} file format. + */ + @Override + public void printToStream(Result result, FileOutputStream fos, Extension extension) { + switch (extension) { + case HTML: + printHTML(result, fos); + break; + case CSV: + printCSV(result, fos); + break; + default: + throw new IllegalArgumentException("Format not recognised: " + extension); + } + } + + private void printHTML(Result result, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + stream.print(Messages.getString("ResultTableExporter.style")); + stream.print("Calculated parameters"); + + for (var p : result.getProperties()) { + stream.print(""); + stream.print(""); + + stream.print(p.getDescriptor(true)); + stream.print(""); + stream.print(p.formattedOutput()); + + stream.print(""); + stream.println(""); + } + + stream.print(""); + } + } + + /** + * Currently the supported extensions include {@code .html} and + * {@code .csv}. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML, CSV}; + } + + private void printCSV(Result result, FileOutputStream fos) { + try (var stream = new PrintStream(fos)) { + stream.print("(Results)"); + + for (var p : result.getProperties()) { + stream.printf("%n%-24.12s", p.getType()); + stream.printf("\t%-24.12s", p.formattedOutput()); + } + } + } + + /** + * @return {@code Result.class} + */ + @Override + public Class target() { + return Result.class; + } + + /** + * Returns the single static instance of this class. + * + * @return instance an instance of this class. + */ + public static ResultExporter getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/io/export/ResultTableExporter.java b/src/main/java/pulse/io/export/ResultTableExporter.java index 6cc85848..ea6bd54c 100644 --- a/src/main/java/pulse/io/export/ResultTableExporter.java +++ b/src/main/java/pulse/io/export/ResultTableExporter.java @@ -2,13 +2,16 @@ import java.io.FileOutputStream; import java.io.PrintStream; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; +import pulse.tasks.TaskManager; import pulse.tasks.processing.AbstractResult; import pulse.tasks.processing.AverageResult; import pulse.ui.Messages; +import pulse.ui.Version; import pulse.ui.components.ResultTable; import pulse.ui.components.models.ResultTableModel; @@ -18,179 +21,194 @@ * summary file in either {@code csv} or {@code html} format. * */ - public class ResultTableExporter implements Exporter { - private static ResultTableExporter instance = new ResultTableExporter(); - - private ResultTableExporter() { - // intentionally blank - } - - /** - * Both {@code html} and {@code csv} are suported. - */ - - @Override - public Extension[] getSupportedExtensions() { - return new Extension[] { Extension.HTML, Extension.CSV }; - } - - /** - * This will create a single file with the output. Depending on whether the - * results table contain average results (with the respective error margins) or - * only individual results, the file might consist of one or two tables, first - * listing the average results and then finding what individual results have - * been used to calculate the latter. In the {@code html} format, the errors are - * given in the same table cells as the values and are delimited by a plus-minus - * sign. The {@code html} table gives a pretty representation of the results - * whereas the {@code csv}, while difficult to read by a human, can be - * interpreted by most external tools such as LaTeX pgfplots, gnuplot, or excel. - */ - - @Override - public void printToStream(ResultTable table, FileOutputStream fos, Extension extension) { - switch (extension) { - case HTML: - printHTML(table, fos); - break; - case CSV: - printCSV(table, fos); - break; - default: - System.err.println("Extension not supported: " + extension); - } - } - - private void printHeaderCSV(ResultTable table, PrintStream stream) { - NumericPropertyKeyword p = null; - for (int col = 0; col < table.getColumnCount(); col++) { - p = ((ResultTableModel) table.getModel()).getFormat().fromAbbreviation(table.getColumnName(col)); - stream.print(p + "\t"); - stream.print("STDEV" + "\t"); - } - stream.println(""); - } - - private void printIndividualCSV(NumericProperty p, PrintStream stream) { - stream.print(p.valueOutput() + "\t" + p.errorOutput() + "\t"); - } - - private void printCSV(ResultTable table, FileOutputStream fos) { - try (PrintStream stream = new PrintStream(fos)) { - - printHeaderCSV(table, stream); - - for (int i = 0; i < table.getRowCount(); i++) { - for (int j = 0; j < table.getColumnCount(); j++) - printIndividualCSV((NumericProperty) table.getValueAt(i, j), stream); - stream.println(); - } - - List results = ((ResultTableModel) table.getModel()).getResults(); - - if (results.stream().anyMatch(r -> r instanceof AverageResult)) { - - stream.println(""); - stream.print(Messages.getString("ResultTable.SeparatorCSV")); - stream.println(""); - - printHeaderCSV(table, stream); - - results.stream().filter(r -> r instanceof AverageResult) - .forEach(ar -> ((AverageResult) ar).getIndividualResults().stream().forEach(ir -> { - var props = AbstractResult.filterProperties(ir); - - for (int j = 0; j < table.getColumnCount(); j++) - printIndividualCSV(props.get(j), stream); - - stream.println(); - })); - - } - - } - - } - - private void printHeaderHTML(ResultTable table, PrintStream stream, String caption) { - stream.print(Messages.getString("ResultTableExporter.style")); - stream.print("" + caption + ""); - stream.print(""); - - for (int col = 0; col < table.getColumnCount(); col++) { - stream.print(""); - stream.print(table.getColumnName(col) + "\t"); - stream.print(""); - } - - stream.print(""); - } - - private void printIndividualHTML(NumericProperty p, PrintStream stream) { - stream.print(""); - stream.print(p.formattedOutput()); - stream.print(""); - } - - private void printHTML(ResultTable table, FileOutputStream fos) { - try (PrintStream stream = new PrintStream(fos)) { - printHeaderHTML(table, stream, "Exported table (contains either averaged or individual results)"); - - stream.println(""); - - for (int i = 0; i < table.getRowCount(); i++) { - stream.print(""); - for (int j = 0; j < table.getColumnCount(); j++) - printIndividualHTML((NumericProperty) table.getValueAt(i, j), stream); - stream.println(""); - } - - stream.print(""); - - List results = ((ResultTableModel) table.getModel()).getResults(); - - if (results.stream().anyMatch(r -> r instanceof AverageResult)) { - - stream.print(Messages.getString("ResultTable.IndividualResults")); - printHeaderHTML(table, stream, "Exported individual results for each processed task"); - - stream.println(""); - - results.stream().filter(r -> r instanceof AverageResult) - .forEach(ar -> ((AverageResult) ar).getIndividualResults().stream().forEach(ir -> { - var props = AbstractResult.filterProperties(ir); - - stream.print(""); - for (int j = 0; j < table.getColumnCount(); j++) - printIndividualHTML(props.get(j), stream); - stream.print(""); + private static final ResultTableExporter instance = new ResultTableExporter(); + + private ResultTableExporter() { + // intentionally blank + } + + /** + * Both {@code html} and {@code csv} are suported. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{Extension.HTML, Extension.CSV}; + } + + /** + * This will create a single file with the output. Depending on whether the + * results table contain average results (with the respective error margins) + * or only individual results, the file might consist of one or two tables, + * first listing the average results and then finding what individual + * results have been used to calculate the latter. In the {@code html} + * format, the errors are given in the same table cells as the values and + * are delimited by a plus-minus sign. The {@code html} table gives a pretty + * representation of the results whereas the {@code csv}, while difficult to + * read by a human, can be interpreted by most external tools such as LaTeX + * pgfplots, gnuplot, or excel. + */ + @Override + public void printToStream(ResultTable table, FileOutputStream fos, Extension extension) { + switch (extension) { + case HTML: + printHTML(table, fos); + break; + case CSV: + printCSV(table, fos); + break; + default: + System.err.println("Extension not supported: " + extension); + } + } + + private void printHeaderCSV(ResultTable table, PrintStream stream) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + stream.println("Summary report on " + + LocalDateTime.now().format(formatter)); + stream.println("PULsE Version: " + Version.getCurrentVersion().toString()); + stream.println("Sample: " + TaskManager.getManagerInstance().getSampleName()); + stream.println("Ouput format sequence below: "); + + var fmt = ((ResultTableModel) table.getModel()).getFormat(); + + for (int col = 0; col < table.getColumnCount(); col++) { + var colName = fmt.fromAbbreviation(table.getColumnName(col)); + stream.println("Col. no.: " + col + " - " + colName); + } + + stream.println("Note: average results are formatted as ; in the list below."); + stream.println(); + } + + private void printIndividualCSV(NumericProperty p, PrintStream stream) { + String fmt = p.getValue() instanceof Double ? "%-2.5e" : "%-6d"; + String s1 = String.format(fmt, p.getValue()).trim(); + String s2 = ""; + if (p.getError() != null) { + s2 = String.format(fmt, p.getError()).trim(); + stream.print(s1 + " ; " + s2 + " "); + } else { + stream.print(s1 + " "); + } + } + + private void printCSV(ResultTable table, FileOutputStream fos) { + try (PrintStream stream = new PrintStream(fos)) { + + printHeaderCSV(table, stream); + + for (int i = 0; i < table.getRowCount(); i++) { + for (int j = 0; j < table.getColumnCount(); j++) { + printIndividualCSV((NumericProperty) table.getValueAt(i, j), stream); + } + stream.println(); + } + + List results = ((ResultTableModel) table.getModel()).getResults(); + + if (results.stream().anyMatch(r -> r instanceof AverageResult)) { + + stream.println(""); + stream.print(Messages.getString("ResultTable.SeparatorCSV")); + stream.println(""); + + results.stream().filter(r -> r instanceof AverageResult) + .forEach(ar -> ((AverageResult) ar).getIndividualResults().stream().forEach(ir -> { + var props = AbstractResult.filterProperties(ir); + + for (int j = 0; j < table.getColumnCount(); j++) { + printIndividualCSV(props.get(j), stream); + } + + stream.println(); + })); + + } + + } + + } + + private void printHeaderHTML(ResultTable table, PrintStream stream, String caption) { + stream.print(Messages.getString("ResultTableExporter.style")); + stream.print("" + caption + ""); + stream.print(""); + + for (int col = 0; col < table.getColumnCount(); col++) { + stream.print(""); + stream.print(table.getColumnName(col) + "\t"); + stream.print(""); + } + + stream.print(""); + } + + private void printIndividualHTML(NumericProperty p, PrintStream stream) { + stream.print(""); + stream.print(p.formattedOutput()); + stream.print(""); + } + + private void printHTML(ResultTable table, FileOutputStream fos) { + try (PrintStream stream = new PrintStream(fos)) { + printHeaderHTML(table, stream, "Exported table (contains either averaged or individual results)"); + + stream.println(""); + + for (int i = 0; i < table.getRowCount(); i++) { + stream.print(""); + for (int j = 0; j < table.getColumnCount(); j++) { + printIndividualHTML((NumericProperty) table.getValueAt(i, j), stream); + } + stream.println(""); + } + + stream.print(""); + + List results = ((ResultTableModel) table.getModel()).getResults(); - stream.println(); - })); + if (results.stream().anyMatch(r -> r instanceof AverageResult)) { - stream.print(""); - } + stream.print(Messages.getString("ResultTable.IndividualResults")); + printHeaderHTML(table, stream, "Exported individual results for each processed task"); + + stream.println(""); + + results.stream().filter(r -> r instanceof AverageResult) + .forEach(ar -> ((AverageResult) ar).getIndividualResults().stream().forEach(ir -> { + var props = AbstractResult.filterProperties(ir); + + stream.print(""); + for (int j = 0; j < table.getColumnCount(); j++) { + printIndividualHTML(props.get(j), stream); + } + stream.print(""); - } - } + stream.println(); + })); - /** - * Gets the single instance of this class. - * - * @return the single instance of {@code ResultTableExporter}. - */ + stream.print(""); + } - public static ResultTableExporter getInstance() { - return instance; - } + } + } - /** - * @return {@code ResultTable.class}. - */ + /** + * Gets the single instance of this class. + * + * @return the single instance of {@code ResultTableExporter}. + */ + public static ResultTableExporter getInstance() { + return instance; + } - @Override - public Class target() { - return ResultTable.class; - } + /** + * @return {@code ResultTable.class}. + */ + @Override + public Class target() { + return ResultTable.class; + } } diff --git a/src/main/java/pulse/io/export/TextLogPaneExporter.java b/src/main/java/pulse/io/export/TextLogPaneExporter.java new file mode 100644 index 00000000..317af504 --- /dev/null +++ b/src/main/java/pulse/io/export/TextLogPaneExporter.java @@ -0,0 +1,75 @@ +package pulse.io.export; + +import static pulse.io.export.Extension.HTML; + +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.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 TextLogPaneExporter implements Exporter { + + private static TextLogPaneExporter instance = new TextLogPaneExporter(); + + private TextLogPaneExporter() { + // intentionally blank + } + + /** + * This will write all contents of {@code pane}, which are accessed using an + * {@code HTMLEditorKit} directly to {@code fos}. The {@code extension} + * argument is ignored. After exporting, the stream is explicitly closed. + */ + @Override + public void printToStream(TextLogPane pane, FileOutputStream fos, Extension extension) { + var editorPane = (JEditorPane) pane.getGUIComponent(); + var kit = (HTMLEditorKit) editorPane.getEditorKit(); + try { + kit.write(fos, editorPane.getDocument(), 0, editorPane.getDocument().getLength()); + } catch (IOException | BadLocationException e) { + System.err.println("Could not export the log pane!"); + e.printStackTrace(); + } + try { + fos.close(); + } catch (IOException e) { + System.err.println("Unable to close stream"); + e.printStackTrace(); + } + } + + /** + * Gets the only static instance of this subclass. + * + * @return an instance of{@code LogPaneExporter}. + */ + public static TextLogPaneExporter getInstance() { + return instance; + } + + /** + * @return {@code LogPane.class}. + */ + @Override + public Class target() { + return TextLogPane.class; + } + + /** + * Only html is currently supported by this exporter. + */ + @Override + public Extension[] getSupportedExtensions() { + return new Extension[]{HTML}; + } + +} diff --git a/src/main/java/pulse/io/export/XMLConverter.java b/src/main/java/pulse/io/export/XMLConverter.java index 0cc40769..62ecd25d 100644 --- a/src/main/java/pulse/io/export/XMLConverter.java +++ b/src/main/java/pulse/io/export/XMLConverter.java @@ -35,196 +35,220 @@ * information XML file in the resource folder. * */ - public class XMLConverter { - private XMLConverter() { - } - - private static void toXML(NumericProperty np, Document doc, Element rootElement) { - Element property = doc.createElement(np.getClass().getSimpleName()); - rootElement.appendChild(property); - - Attr keyword = doc.createAttribute("keyword"); - keyword.setValue(np.getType().toString()); - property.setAttributeNode(keyword); - - Attr descriptor = doc.createAttribute("descriptor"); - descriptor.setValue(np.getDescriptor(false)); - property.setAttributeNode(descriptor); - - Attr abbreviation = doc.createAttribute("abbreviation"); - abbreviation.setValue(np.getAbbreviation(false)); - property.setAttributeNode(abbreviation); - - Attr value = doc.createAttribute("value"); - value.setValue(np.getValue().toString()); - property.setAttributeNode(value); - - Attr minimum = doc.createAttribute("minimum"); - minimum.setValue(np.getMinimum().toString()); - property.setAttributeNode(minimum); - - Attr maximum = doc.createAttribute("maximum"); - maximum.setValue(np.getMaximum().toString()); - property.setAttributeNode(maximum); - - Attr dim = doc.createAttribute("dimensionfactor"); - dim.setValue(np.getDimensionFactor().toString()); - property.setAttributeNode(dim); - - Attr autoAdj = doc.createAttribute("auto-adjustable"); - autoAdj.setValue(np.isAutoAdjustable() + ""); - property.setAttributeNode(autoAdj); - - Attr primitiveType = doc.createAttribute("primitive-type"); - primitiveType.setValue(np.getValue() instanceof Double ? "double" : "int"); - property.setAttributeNode(primitiveType); - - Attr defSearch = doc.createAttribute("default-search-variable"); - primitiveType.setValue(np.isDefaultSearchVariable() + ""); - property.setAttributeNode(defSearch); - - } - - /** - * Utility method that creates an {@code .xml} file listing all public final - * static instances of {@code NumericProperty} found in the - * {@code NumericProperty} class. - */ - - public static void writeXML() throws ParserConfigurationException, TransformerException { - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); - Document doc = dBuilder.newDocument(); - - Element rootElement = doc.createElement("NumericProperties"); - doc.appendChild(rootElement); - - List properties = new ArrayList<>(); - - int modifiers; - - /** - * Reads all final static {@code NumericProperty} constants in the - * {@code NumericProperty} class - */ - - for (Field field : NumericProperty.class.getDeclaredFields()) { - - modifiers = field.getModifiers(); - - // filter only public final static NumericProperties - if ((Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) - && field.getType().equals(NumericProperty.class)) { - - NumericProperty value = null; - try { - value = (NumericProperty) field.get(null); - } catch (IllegalArgumentException | IllegalAccessException e) { - System.out.println("Unable to access field: " + field); - e.printStackTrace(); - } - if (value != null) - properties.add(value); - - } - - } - - properties.stream().forEach(p -> XMLConverter.toXML(p, doc, rootElement)); - - // write the content into xml file - TransformerFactory transformerFactory = TransformerFactory.newInstance(); - Transformer transformer = transformerFactory.newTransformer(); - transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - DOMSource source = new DOMSource(doc); - StreamResult result = new StreamResult(new File(NumericProperty.class.getSimpleName() + ".xml")); - transformer.transform(source, result); - - // Output to console for testing - StreamResult consoleResult = new StreamResult(System.out); - transformer.transform(source, consoleResult); - - } - - /** - * Utility method used to read {@code NumericProperty} constants from - * {@code xml} files. - * - * @param inputStream the input stream used to read data from. - * @return a list of {@code NumericProperty} objects with their attributes - * specified in the {@code xml} file. - */ - - public static List readXML(InputStream inputStream) - throws ParserConfigurationException, SAXException, IOException { - - List properties = new ArrayList<>(); - - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); - Document doc = dBuilder.parse(inputStream); - - doc.getDocumentElement().normalize(); - NodeList nList = doc.getElementsByTagName(NumericProperty.class.getSimpleName()); - - for (int temp = 0; temp < nList.getLength(); temp++) { - Node nNode = nList.item(temp); - - if (nNode.getNodeType() == Node.ELEMENT_NODE) { - Element eElement = (Element) nNode; - NumericPropertyKeyword keyword = NumericPropertyKeyword.valueOf(eElement.getAttribute("keyword")); - boolean autoAdjustable = Boolean.valueOf(eElement.getAttribute("auto-adjustable")); - boolean discreet = Boolean.valueOf(eElement.getAttribute("discreet")); - String descriptor = eElement.getAttribute("descriptor"); - String abbreviation = eElement.getAttribute("abbreviation"); - boolean defSearch = Boolean.valueOf(eElement.getAttribute("default-search-variable")); - - Number value, minimum, maximum, dimensionFactor; - - if (eElement.getAttribute("primitive-type").equalsIgnoreCase("double")) { - value = Double.valueOf(eElement.getAttribute("value")); - minimum = Double.valueOf(eElement.getAttribute("minimum")); - maximum = Double.valueOf(eElement.getAttribute("maximum")); - dimensionFactor = Double.valueOf(eElement.getAttribute("dimensionfactor")); - } else { - value = Integer.valueOf(eElement.getAttribute("value")); - minimum = Integer.valueOf(eElement.getAttribute("minimum")); - maximum = Integer.valueOf(eElement.getAttribute("maximum")); - dimensionFactor = Integer.valueOf(eElement.getAttribute("dimensionfactor")); - } - - var np = new NumericProperty(keyword, value, minimum, maximum, dimensionFactor); - np.setDescriptor(descriptor); - np.setAbbreviation(abbreviation); - np.setAutoAdjustable(autoAdjustable); - np.setDiscreet(discreet); - np.setDefaultSearchVariable(defSearch); - properties.add(np); - } - } - - return properties; - - } - - /** - * The default XML file is specific in the 'messages.properties' text file in - * the {@code pulse.ui} package - * - * @return a list of default instances of {@code NumericProperty}. - */ - - public static List readDefaultXML() { - try { - return readXML(NumericProperty.class.getResourceAsStream(Messages.getString("NumericProperty.XMLFile"))); - } catch (ParserConfigurationException | SAXException | IOException e) { - System.err.println("Unable to read list of default numeric properties"); - e.printStackTrace(); - } - return null; - } - -} \ No newline at end of file + private XMLConverter() { + } + + private static void toXML(NumericProperty np, Document doc, Element rootElement) { + Element property = doc.createElement(np.getClass().getSimpleName()); + rootElement.appendChild(property); + + Attr keyword = doc.createAttribute("keyword"); + keyword.setValue(np.getType().toString()); + property.setAttributeNode(keyword); + + Attr descriptor = doc.createAttribute("descriptor"); + descriptor.setValue(np.getDescriptor(false)); + property.setAttributeNode(descriptor); + + Attr abbreviation = doc.createAttribute("abbreviation"); + abbreviation.setValue(np.getAbbreviation(false)); + property.setAttributeNode(abbreviation); + + Attr value = doc.createAttribute("value"); + value.setValue(np.getValue().toString()); + property.setAttributeNode(value); + + Attr minimum = doc.createAttribute("minimum"); + minimum.setValue(np.getMinimum().toString()); + property.setAttributeNode(minimum); + + Attr maximum = doc.createAttribute("maximum"); + maximum.setValue(np.getMaximum().toString()); + property.setAttributeNode(maximum); + + Attr dim = doc.createAttribute("dimensionfactor"); + dim.setValue(np.getDimensionFactor().toString()); + property.setAttributeNode(dim); + + Attr autoAdj = doc.createAttribute("visible"); + autoAdj.setValue(np.isVisibleByDefault() + ""); + property.setAttributeNode(autoAdj); + + Attr primitiveType = doc.createAttribute("primitive-type"); + primitiveType.setValue(np.getValue() instanceof Double ? "double" : "int"); + property.setAttributeNode(primitiveType); + + if (np.isOptimisable()) { + Attr defSearch = doc.createAttribute("default-search-variable"); + primitiveType.setValue(np.isDefaultSearchVariable() + ""); + property.setAttributeNode(defSearch); + } + + } + + /** + * Utility method that creates an {@code .xml} file listing all public final + * static instances of {@code NumericProperty} found in the + * {@code NumericProperty} class. + * + * @throws javax.xml.parsers.ParserConfigurationException + * @throws javax.xml.transform.TransformerException + */ + public static void writeXML() throws ParserConfigurationException, TransformerException { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.newDocument(); + + Element rootElement = doc.createElement("NumericProperties"); + doc.appendChild(rootElement); + + List properties = new ArrayList<>(); + + int modifiers; + + /** + * Reads all final static {@code NumericProperty} constants in the + * {@code NumericProperty} class + */ + for (Field field : NumericProperty.class.getDeclaredFields()) { + + modifiers = field.getModifiers(); + + // filter only public final static NumericProperties + if ((Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) + && field.getType().equals(NumericProperty.class)) { + + NumericProperty value = null; + try { + value = (NumericProperty) field.get(null); + } catch (IllegalArgumentException | IllegalAccessException e) { + System.out.println("Unable to access field: " + field); + e.printStackTrace(); + } + if (value != null) { + properties.add(value); + } + + } + + } + + properties.stream().forEach(p -> XMLConverter.toXML(p, doc, rootElement)); + + // write the content into xml file + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(new File(NumericProperty.class.getSimpleName() + ".xml")); + transformer.transform(source, result); + + // Output to console for testing + StreamResult consoleResult = new StreamResult(System.out); + transformer.transform(source, consoleResult); + + } + + /** + * Utility method used to read {@code NumericProperty} constants from + * {@code xml} files. + * + * @param inputStream the input stream used to read data from. + * @return a list of {@code NumericProperty} objects with their attributes + * specified in the {@code xml} file. + * @throws javax.xml.parsers.ParserConfigurationException + * @throws org.xml.sax.SAXException + * @throws java.io.IOException + */ + public static List readXML(InputStream inputStream) + throws ParserConfigurationException, SAXException, IOException { + + List properties = new ArrayList<>(); + + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.parse(inputStream); + + doc.getDocumentElement().normalize(); + NodeList nList = doc.getElementsByTagName(NumericProperty.class.getSimpleName()); + + for (int temp = 0; temp < nList.getLength(); temp++) { + Node nNode = nList.item(temp); + + if (nNode.getNodeType() == Node.ELEMENT_NODE) { + Element eElement = (Element) nNode; + NumericPropertyKeyword keyword = NumericPropertyKeyword.valueOf(eElement.getAttribute("keyword")); + boolean visible = Boolean.valueOf(eElement.getAttribute("visible")); + boolean discrete = Boolean.valueOf(eElement.getAttribute("discreet")); + String descriptor = eElement.getAttribute("descriptor"); + String abbreviation = eElement.getAttribute("abbreviation"); + + String search = eElement.getAttribute("default-search-variable"); + + Number value, minimum, maximum, dimensionFactor; + + if (eElement.getAttribute("primitive-type").equalsIgnoreCase("double")) { + value = Double.valueOf(eElement.getAttribute("value")); + minimum = Double.valueOf(eElement.getAttribute("minimum")); + maximum = Double.valueOf(eElement.getAttribute("maximum")); + dimensionFactor = Double.valueOf(eElement.getAttribute("dimensionfactor")); + } else { + value = Integer.valueOf(eElement.getAttribute("value")); + minimum = Integer.valueOf(eElement.getAttribute("minimum")); + maximum = Integer.valueOf(eElement.getAttribute("maximum")); + dimensionFactor = Integer.valueOf(eElement.getAttribute("dimensionfactor")); + } + + NodeList excludeList = eElement.getElementsByTagName("excludes"); + + var np = new NumericProperty(keyword, value, minimum, maximum, dimensionFactor); + + if (excludeList.getLength() > 0) { + var excludeKeywords = ((Element) excludeList.item(0)).getElementsByTagName("keyword"); + NumericPropertyKeyword[] array = new NumericPropertyKeyword[excludeKeywords.getLength()]; + + for (int i = 0; i < excludeKeywords.getLength(); i++) { + String textValue = excludeKeywords.item(i).getChildNodes().item(0).getNodeValue(); + array[i] = NumericPropertyKeyword.valueOf(textValue); + } + + np.setExcludeKeywords(array); + + } + + np.setDescriptor(descriptor); + np.setAbbreviation(abbreviation); + np.setVisibleByDefault(visible); + np.setDiscrete(discrete); + if (!search.isEmpty()) { + np.setDefaultSearchVariable(Boolean.valueOf(search)); + np.setOptimisable(true); + } + properties.add(np); + } + } + + return properties; + + } + + /** + * The default XML file is specific in the 'messages.properties' text file + * in the {@code pulse.ui} package + * + * @return a list of default instances of {@code NumericProperty}. + */ + public static List readDefaultXML() { + try { + return readXML(NumericProperty.class.getResourceAsStream(Messages.getString("NumericProperty.XMLFile"))); + } catch (ParserConfigurationException | SAXException | IOException e) { + System.err.println("Unable to read list of default numeric properties"); + e.printStackTrace(); + } + return null; + } + +} diff --git a/src/main/java/pulse/io/export/package-info.java b/src/main/java/pulse/io/export/package-info.java index f3e455dc..f241cb52 100644 --- a/src/main/java/pulse/io/export/package-info.java +++ b/src/main/java/pulse/io/export/package-info.java @@ -2,7 +2,6 @@ * Package contains the PULsE export API, which currently consists of different * exporter classes, an export manager, an XML converter and a MassExporter * class. - * + * */ - -package pulse.io.export; \ No newline at end of file +package pulse.io.export; diff --git a/src/main/java/pulse/io/readers/AbstractHandler.java b/src/main/java/pulse/io/readers/AbstractHandler.java index 1e96ec45..c5419967 100644 --- a/src/main/java/pulse/io/readers/AbstractHandler.java +++ b/src/main/java/pulse/io/readers/AbstractHandler.java @@ -11,56 +11,52 @@ * pre-set extension. * */ - public interface AbstractHandler extends Reflexive { - /** - * Retrieves the supported extension of files, which this - * {@code AbstractHandler} is able to process. - * - * @return a {@code String} (usually, lower-case) containing the supported - * extension. - */ - - public String getSupportedExtension(); - - /** - * Checks if the file suffix for {@code file} matches the {@code extension}. - * - * @param file the {@code File} to process - * @param extension a String, which needs to be checked against the suffix of - * {@code File} - * @return {@code false} if {@code file} is a directory or if it has a suffix - * different from {@code extension}. True otherwise. - */ - - public static boolean extensionsMatch(File file, String extension) { - if (file.isDirectory()) - return false; - - String name = file.getName(); - - /* + /** + * Retrieves the supported extension of files, which this + * {@code AbstractHandler} is able to process. + * + * @return a {@code String} (usually, lower-case) containing the supported + * extension. + */ + public String getSupportedExtension(); + + /** + * Checks if the file suffix for {@code file} matches the {@code extension}. + * + * @param file the {@code File} to process + * @param extension a String, which needs to be checked against the suffix + * of {@code File} + * @return {@code false} if {@code file} is a directory or if it has a + * suffix different from {@code extension}. True otherwise. + */ + public static boolean extensionsMatch(File file, String extension) { + if (file.isDirectory()) { + return false; + } + + String name = file.getName(); + + /* * The below code is based on string helper function by Gili Tzabari - */ - - int suffixLength = extension.length(); - return name.regionMatches(true, name.length() - suffixLength, extension, 0, suffixLength); - - } - - /** - * Invokes {@code extensionMatch} with the second argument set as - * {@code getSupportedExtension()}. - * - * @param file the file to be checked - * @return {@code true} if extensions match, false otherwise. - * @see extensionsMatch - * @see getSupportedExtension - */ - - public default boolean isExtensionSupported(File file) { - return extensionsMatch(file, getSupportedExtension()); - } - -} \ No newline at end of file + */ + int suffixLength = extension.length(); + return name.regionMatches(true, name.length() - suffixLength, extension, 0, suffixLength); + + } + + /** + * Invokes {@code extensionMatch} with the second argument set as + * {@code getSupportedExtension()}. + * + * @param file the file to be checked + * @return {@code true} if extensions match, false otherwise. + * @see extensionsMatch + * @see getSupportedExtension + */ + public default boolean isExtensionSupported(File file) { + return extensionsMatch(file, getSupportedExtension()); + } + +} diff --git a/src/main/java/pulse/io/readers/AbstractPopulator.java b/src/main/java/pulse/io/readers/AbstractPopulator.java index e4b138b5..484eed25 100644 --- a/src/main/java/pulse/io/readers/AbstractPopulator.java +++ b/src/main/java/pulse/io/readers/AbstractPopulator.java @@ -10,18 +10,17 @@ * latter does not change the internal structure of an object. * */ - public interface AbstractPopulator extends AbstractHandler { - /** - * Tries to populate {@code t} from data contained in {@code f}. - * - * @param f a file presumably containing data that can be converted to the - * internal format of {@code t}. - * @param t a {@code T} object which can potentially be populated by {@code f}. - * @throws IOException if an exception occurs during processing {@code f}. - */ - - public void populate(File f, T t) throws IOException; + /** + * Tries to populate {@code t} from data contained in {@code f}. + * + * @param f a file presumably containing data that can be converted to the + * internal format of {@code t}. + * @param t a {@code T} object which can potentially be populated by + * {@code f}. + * @throws IOException if an exception occurs during processing {@code f}. + */ + public void populate(File f, T t) throws IOException; -} \ 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 f3e1bd71..29bbc625 100644 --- a/src/main/java/pulse/io/readers/AbstractReader.java +++ b/src/main/java/pulse/io/readers/AbstractReader.java @@ -13,21 +13,22 @@ * lists, arrays and containers may (and usually will) change as a result of * using the reader. *

+ * + * @param */ - public interface AbstractReader extends AbstractHandler { - /** - * Reads {@code f} to translate its contents to one of the immutable structures - * of {@code T}. Usually this involves reading arrays and collections and - * pasting their data into existing structural elements. This does not change - * the internal structure of the object. - * - * @param f a file which has readable content - * @return a {@code T} object created by reading all information from {@code f}. - * @throws IOException - */ - - public T read(File f) throws IOException; + /** + * Reads {@code f} to translate its contents to one of the immutable + * structures of {@code T}. Usually this involves reading arrays and + * collections and pasting their data into existing structural elements. + * This does not change the internal structure of the object. + * + * @param f a file which has readable content + * @return a {@code T} object created by reading all information from + * {@code f}. + * @throws IOException + */ + public T read(File f) throws IOException; -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/ButcherTableauReader.java b/src/main/java/pulse/io/readers/ButcherTableauReader.java index 7e6e14d2..bea5d870 100644 --- a/src/main/java/pulse/io/readers/ButcherTableauReader.java +++ b/src/main/java/pulse/io/readers/ButcherTableauReader.java @@ -18,111 +18,115 @@ * {@code jar}). The coefficients are used by an explicit Runge-Kutta solver. * */ - public class ButcherTableauReader implements AbstractReader { - private final static String SUPPORTED_EXTENSION = "rk"; - private static ButcherTableauReader instance = new ButcherTableauReader(); - - private ButcherTableauReader() { - // intentionally blank - } - - /** - * Reads the Butcher tableau stored in {@code file}. The file contents should be - * arranged as follows: first row contains specific keywords (e.g. FSAL), second - * and subsequent rows contain the matrix coefficients (the matrix is assumed to - * be quadratic), so the number of columns should be equal to the number of - * rows; the three final rows correspond to {@code c}, {@code b} and {@code b^} - * vectors. Consistency should be maintained between the corresponding - * dimensions. - */ + private final static String SUPPORTED_EXTENSION = "rk"; + private static ButcherTableauReader instance = new ButcherTableauReader(); - @Override - public ButcherTableau read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("TBLReader.1")); + private ButcherTableauReader() { + // intentionally blank + } - // ignore extension! + /** + * Reads the Butcher tableau stored in {@code file}. The file contents + * should be arranged as follows: first row contains specific keywords (e.g. + * FSAL), second and subsequent rows contain the matrix coefficients (the + * matrix is assumed to be quadratic), so the number of columns should be + * equal to the number of rows; the three final rows correspond to + * {@code c}, {@code b} and {@code b^} vectors. Consistency should be + * maintained between the corresponding dimensions. + */ + @Override + public ButcherTableau read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("TBLReader.1")); - String name = file.getName().split("\\.")[0]; - ButcherTableau bt = null; + // ignore extension! + String name = file.getName().split("\\.")[0]; + ButcherTableau bt = null; - String delims = Messages.getString("}{,\t "); + String delims = Messages.getString("}{,\t "); - try (var fr = new FileReader(file); BufferedReader reader = new BufferedReader(fr)) { - // first line with declarations (e.g. FSAL, etc.) - var tokenizer = new StringTokenizer(reader.readLine()); + try (var fr = new FileReader(file); BufferedReader reader = new BufferedReader(fr)) { + // first line with declarations (e.g. FSAL, etc.) + var tokenizer = new StringTokenizer(reader.readLine()); - boolean fsal = false; + boolean fsal = false; - while (tokenizer.hasMoreTokens()) - if (tokenizer.nextToken(delims).equalsIgnoreCase("FSAL")) - fsal = true; + while (tokenizer.hasMoreTokens()) { + if (tokenizer.nextToken(delims).equalsIgnoreCase("FSAL")) { + fsal = true; + } + } - var aMatrix = readMatrix(reader, delims); - var v = readVectors(reader, delims, aMatrix.length); + var aMatrix = readMatrix(reader, delims); + var v = readVectors(reader, delims, aMatrix.length); - bt = new ButcherTableau(name, aMatrix, v[0], v[1], v[2], fsal); + bt = new ButcherTableau(name, aMatrix, v[0], v[1], v[2], fsal); - reader.close(); - } + reader.close(); + } - return bt; + return bt; - } + } - private double[][] readMatrix(BufferedReader reader, String delims) throws IOException { - List lineDouble = new ArrayList<>(); - int lineno = 0; - int dimension = 1; + private double[][] readMatrix(BufferedReader reader, String delims) throws IOException { + List lineDouble = new ArrayList<>(); + int lineno = 0; + int dimension = 1; - StringTokenizer tokenizer; + StringTokenizer tokenizer; - for (String line = ""; lineno < dimension; lineno++) { - line = reader.readLine(); - tokenizer = new StringTokenizer(line); + for (String line = ""; lineno < dimension; lineno++) { + line = reader.readLine(); + tokenizer = new StringTokenizer(line); - while (tokenizer.hasMoreTokens()) - lineDouble.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); - if (lineno == 0) - dimension = lineDouble.size(); - } + while (tokenizer.hasMoreTokens()) { + lineDouble.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); + } + if (lineno == 0) { + dimension = lineDouble.size(); + } + } - double[][] aMatrix = new double[dimension][dimension]; + double[][] aMatrix = new double[dimension][dimension]; - for (int i = 0; i < dimension; i++) - for (int j = 0; j < dimension; j++) - aMatrix[i][j] = lineDouble.get(i * dimension + j); + for (int i = 0; i < dimension; i++) { + for (int j = 0; j < dimension; j++) { + aMatrix[i][j] = lineDouble.get(i * dimension + j); + } + } - return aMatrix; - } + return aMatrix; + } - private double[][] readVectors(BufferedReader reader, String delims, int dimension) throws IOException { - var v = new double[3][dimension]; + private double[][] readVectors(BufferedReader reader, String delims, int dimension) throws IOException { + var v = new double[3][dimension]; - int lineno = 0; - StringTokenizer tokenizer; + int lineno = 0; + StringTokenizer tokenizer; - for (String line = ""; lineno < 3 && line != null; lineno++) { - line = reader.readLine(); - tokenizer = new StringTokenizer(line); + for (String line = ""; lineno < 3 && line != null; lineno++) { + line = reader.readLine(); + tokenizer = new StringTokenizer(line); - for (int i = 0; i < dimension && tokenizer.hasMoreTokens(); i++) - v[lineno][i] = (ExpressionParser.evaluate(tokenizer.nextToken(delims))); + for (int i = 0; i < dimension && tokenizer.hasMoreTokens(); i++) { + v[lineno][i] = (ExpressionParser.evaluate(tokenizer.nextToken(delims))); + } - } + } - return v; + return v; - } + } - @Override - public String getSupportedExtension() { - return SUPPORTED_EXTENSION; - } + @Override + public String getSupportedExtension() { + return SUPPORTED_EXTENSION; + } - public static ButcherTableauReader getInstance() { - return instance; - } + public static ButcherTableauReader getInstance() { + return instance; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/CurveReader.java b/src/main/java/pulse/io/readers/CurveReader.java index a928414c..41586f07 100644 --- a/src/main/java/pulse/io/readers/CurveReader.java +++ b/src/main/java/pulse/io/readers/CurveReader.java @@ -17,45 +17,42 @@ * {@code ExperimentalData} objects. *

*/ - public interface CurveReader extends AbstractReader> { - /** - * Basic operation for reading the {@code file} and converting this to a - * {@code List} of {@code ExperimentalData} objects. - *

- * - * @param file a {@code File} which has either all information encoded in - * its contents or provides {@code URI} links to other files, - * each containing the necessary information. - * @return - *

- * a {@code List} of {@code ExperimentalData} objects associated with - * this {@code file}. In case if {@code file} contains only one - * {@code ExperimentalData}, i.e. if the data is only presented for one - * heating curve taken at a specific temperature after a single laser - * shot, the size of the {@code List} will be equal to unity. - *

- * @throws IOException if something goes wrong with reading the file - */ - - @Override - public abstract List read(File file) throws IOException; - - /** - * Sorts the {@code List} of {@code ExperimentalData} according to their - * external IDs (if any). - * - * @param array an unsorted list of {@code ExperimentalData} - * @return the same list after sorting - * @see pulse.input.Metadata.getExternalID() - */ - - public static List sort(List array) { - Comparator externalIdComparator = (ExperimentalData e1, ExperimentalData e2) -> Integer - .valueOf(e1.getMetadata().getExternalID()).compareTo(Integer.valueOf(e2.getMetadata().getExternalID())); - - return array.stream().sorted(externalIdComparator).collect(Collectors.toList()); - } - -} \ No newline at end of file + /** + * Basic operation for reading the {@code file} and converting this to a + * {@code List} of {@code ExperimentalData} objects. + *

+ * + * @param file a {@code File} which has either all information + * encoded in its contents or provides {@code URI} links to other + * files, each containing the necessary information. + * @return + *

+ * a {@code List} of {@code ExperimentalData} objects associated with this + * {@code file}. In case if {@code file} contains only one + * {@code ExperimentalData}, i.e. if the data is only presented for one + * heating curve taken at a specific temperature after a single laser shot, + * the size of the {@code List} will be equal to unity. + *

+ * @throws IOException if something goes wrong with reading the file + */ + @Override + public abstract List read(File file) throws IOException; + + /** + * Sorts the {@code List} of {@code ExperimentalData} according to their + * external IDs (if any). + * + * @param array an unsorted list of {@code ExperimentalData} + * @return the same list after sorting + * @see pulse.input.Metadata.getExternalID() + */ + public static List sort(List array) { + Comparator externalIdComparator = (ExperimentalData e1, ExperimentalData e2) -> Integer + .valueOf(e1.getMetadata().getExternalID()).compareTo(Integer.valueOf(e2.getMetadata().getExternalID())); + + return array.stream().sorted(externalIdComparator).collect(Collectors.toList()); + } + +} diff --git a/src/main/java/pulse/io/readers/DATReader.java b/src/main/java/pulse/io/readers/DATReader.java index 4fd25c5a..31a515ae 100644 --- a/src/main/java/pulse/io/readers/DATReader.java +++ b/src/main/java/pulse/io/readers/DATReader.java @@ -33,74 +33,71 @@ * an absolute scale, according to NIST recommendations. *

*/ - public class DATReader implements CurveReader { - private static CurveReader instance = new DATReader(); - private final static double CONVERSION_TO_KELVIN = 273.15; - - private DATReader() { - // intentionally blank - } - - /** - * @return a {@code String} equal to {.dat} - */ - - @Override - public String getSupportedExtension() { - return Messages.getString("DATReader.0"); //$NON-NLS-1$ - } - - /** - *

- * This will return a single {@code ExperimentalData}, which stores all the - * information available in the {@code file}, wrapped in a {@code List} object - * with the size of unity. In addition to the time-temperature data loaded - * directly into the {@code ExperimentalData} lists, a {@code Metadata} object - * will be created for the {@code ExperimentalData} and will store the test - * temperature declared in {@code file}. - * - * @param file a '{@code .dat}' file, which conforms to the respective format. - * @return a single {@code ExperimentalData} wrapped in a {@code List} with the - * size of unity. - */ - - @Override - public List read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("DATReader.1")); - - ExperimentalData curve = new ExperimentalData(); - - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - double T = Double.parseDouble(reader.readLine()) + CONVERSION_TO_KELVIN; - Metadata met = new Metadata(derive(TEST_TEMPERATURE, T), -1); - curve.setMetadata(met); - double time, temp; - String delims = Messages.getString("DATReader.2"); //$NON-NLS-1$ - StringTokenizer tokenizer; - for (String line = reader.readLine(); line != null; line = reader.readLine()) { - tokenizer = new StringTokenizer(line, delims); - time = Double.parseDouble(tokenizer.nextToken()); - temp = Double.parseDouble(tokenizer.nextToken()); - curve.addPoint(time, temp); - } - curve.setRange(new Range(curve.getTimeSequence())); - } - - return new ArrayList<>(Arrays.asList(curve)); - - } - - /** - * As this class uses the singleton pattern, only one instance is created using - * an empty no-argument constructor. - * - * @return the single instance of this class. - */ - - public static CurveReader getInstance() { - return instance; - } - -} \ No newline at end of file + private static CurveReader instance = new DATReader(); + private final static double CONVERSION_TO_KELVIN = 273.15; + + private DATReader() { + // intentionally blank + } + + /** + * @return a {@code String} equal to {.dat} + */ + @Override + public String getSupportedExtension() { + return Messages.getString("DATReader.0"); //$NON-NLS-1$ + } + + /** + *

+ * This will return a single {@code ExperimentalData}, which stores all the + * information available in the {@code file}, wrapped in a {@code List} + * object with the size of unity. In addition to the time-temperature data + * loaded directly into the {@code ExperimentalData} lists, a + * {@code Metadata} object will be created for the {@code ExperimentalData} + * and will store the test temperature declared in {@code file}. + * + * @param file a '{@code .dat}' file, which conforms to the respective + * format. + * @return a single {@code ExperimentalData} wrapped in a {@code List} with + * the size of unity. + */ + @Override + public List read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("DATReader.1")); + + ExperimentalData curve = new ExperimentalData(); + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + double T = Double.parseDouble(reader.readLine()) + CONVERSION_TO_KELVIN; + Metadata met = new Metadata(derive(TEST_TEMPERATURE, T), -1); + curve.setMetadata(met); + double time, temp; + String delims = Messages.getString("DATReader.2"); //$NON-NLS-1$ + StringTokenizer tokenizer; + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + tokenizer = new StringTokenizer(line, delims); + time = Double.parseDouble(tokenizer.nextToken()); + temp = Double.parseDouble(tokenizer.nextToken()); + curve.addPoint(time, temp); + } + curve.setRange(new Range(curve.getTimeSequence())); + } + + return new ArrayList<>(Arrays.asList(curve)); + + } + + /** + * As this class uses the singleton pattern, only one instance is created + * using an empty no-argument constructor. + * + * @return the single instance of this class. + */ + public static CurveReader getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/io/readers/DatasetReader.java b/src/main/java/pulse/io/readers/DatasetReader.java index 7d320f66..59c4a3bb 100644 --- a/src/main/java/pulse/io/readers/DatasetReader.java +++ b/src/main/java/pulse/io/readers/DatasetReader.java @@ -10,22 +10,20 @@ * with an interpolation algorithm. * */ - public interface DatasetReader extends AbstractReader { - /** - * Creates an {@code InterpolationDataset} using the dataset stored in the - * {@code file}. - * - * @param file a file with a supported extension containing the information - * needed to create an {@code InterpolationDataset}. - * @return an {@code InterpolationDataset}, which not only stores the - * information contained in {@code file}, but also provides means of - * interpolation. - * @throws IOException if something goes wrong with reading the {@code file} - */ - - @Override - public abstract InterpolationDataset read(File file) throws IOException; + /** + * Creates an {@code InterpolationDataset} using the dataset stored in the + * {@code file}. + * + * @param file a file with a supported extension containing the information + * needed to create an {@code InterpolationDataset}. + * @return an {@code InterpolationDataset}, which not only stores the + * information contained in {@code file}, but also provides means of + * interpolation. + * @throws IOException if something goes wrong with reading the {@code file} + */ + @Override + public abstract InterpolationDataset read(File file) throws IOException; } diff --git a/src/main/java/pulse/io/readers/ExpressionParser.java b/src/main/java/pulse/io/readers/ExpressionParser.java index 6cc0114b..4179bb51 100644 --- a/src/main/java/pulse/io/readers/ExpressionParser.java +++ b/src/main/java/pulse/io/readers/ExpressionParser.java @@ -3,114 +3,119 @@ /** * Original source: * https://stackoverflow.com/questions/3422673/how-to-evaluate-a-math-expression-given-in-string-form#3423360 - * + * * @author Romeo Sierra * */ - public class ExpressionParser { - private ExpressionParser() { - } - - public static double evaluate(final String str) { - return new Object() { - int pos = -1, ch; - - void nextChar() { - ch = (++pos < str.length()) ? str.charAt(pos) : -1; - } - - boolean eat(int charToEat) { - while (ch == ' ') - nextChar(); - if (ch == charToEat) { - nextChar(); - return true; - } - return false; - } + private ExpressionParser() { + } - double parse() { - nextChar(); - double x = parseExpression(); - if (pos < str.length()) - throw new RuntimeException("Unexpected: " + (char) ch); - return x; - } + public static double evaluate(final String str) { + return new Object() { + int pos = -1, ch; - // Grammar: - // expression = term | expression `+` term | expression `-` term - // term = factor | term `*` factor | term `/` factor - // factor = `+` factor | `-` factor | `(` expression `)` - // | number | functionName factor | factor `^` factor + void nextChar() { + ch = (++pos < str.length()) ? str.charAt(pos) : -1; + } - double parseExpression() { - double x = parseTerm(); - for (;;) { - if (eat('+')) - x += parseTerm(); // addition - else if (eat('-')) - x -= parseTerm(); // subtraction - else - return x; - } - } + boolean eat(int charToEat) { + while (ch == ' ') { + nextChar(); + } + if (ch == charToEat) { + nextChar(); + return true; + } + return false; + } - double parseTerm() { - double x = parseFactor(); - for (;;) { - if (eat('*')) - x *= parseFactor(); // multiplication - else if (eat('/')) - x /= parseFactor(); // division - else if (eat('^')) - x = Math.pow(x, parseFactor()); // exponentiation -> Moved in to here. So the problem is fixed - else - return x; - } - } + double parse() { + nextChar(); + double x = parseExpression(); + if (pos < str.length()) { + throw new RuntimeException("Unexpected: " + (char) ch); + } + return x; + } - double parseFactor() { - if (eat('+')) - return parseFactor(); // unary plus - if (eat('-')) - return -parseFactor(); // unary minus + // Grammar: + // expression = term | expression `+` term | expression `-` term + // term = factor | term `*` factor | term `/` factor + // factor = `+` factor | `-` factor | `(` expression `)` + // | number | functionName factor | factor `^` factor + double parseExpression() { + double x = parseTerm(); + for (;;) { + if (eat('+')) { + x += parseTerm(); // addition + } else if (eat('-')) { + x -= parseTerm(); // subtraction + } else { + return x; + } + } + } - double x; - int startPos = this.pos; - if (eat('(')) { // parentheses - x = parseExpression(); - eat(')'); - } else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers - while ((ch >= '0' && ch <= '9') || ch == '.') - nextChar(); - x = Double.parseDouble(str.substring(startPos, this.pos)); - } else if (ch >= 'a' && ch <= 'z') { // functions - while (ch >= 'a' && ch <= 'z') - nextChar(); - String func = str.substring(startPos, this.pos); - x = parseFactor(); - if (func.equals("sqrt")) - x = Math.sqrt(x); - else if (func.equals("sin")) - x = Math.sin(Math.toRadians(x)); - else if (func.equals("cos")) - x = Math.cos(Math.toRadians(x)); - else if (func.equals("tan")) - x = Math.tan(Math.toRadians(x)); - else - throw new RuntimeException("Unknown function: " + func); - } else { - throw new RuntimeException("Unexpected: " + (char) ch); - } + double parseTerm() { + double x = parseFactor(); + for (;;) { + if (eat('*')) { + x *= parseFactor(); // multiplication + } else if (eat('/')) { + x /= parseFactor(); // division + } else if (eat('^')) { + x = Math.pow(x, parseFactor()); // exponentiation -> Moved in to here. So the problem is fixed + } else { + return x; + } + } + } - // if (eat('^')) x = Math.pow(x, parseFactor()); // exponentiation -> This is - // causing a bit of problem + double parseFactor() { + if (eat('+')) { + return parseFactor(); // unary plus + } + if (eat('-')) { + return -parseFactor(); // unary minus + } + double x; + int startPos = this.pos; + if (eat('(')) { // parentheses + x = parseExpression(); + eat(')'); + } else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers + while ((ch >= '0' && ch <= '9') || ch == '.') { + nextChar(); + } + x = Double.parseDouble(str.substring(startPos, this.pos)); + } else if (ch >= 'a' && ch <= 'z') { // functions + while (ch >= 'a' && ch <= 'z') { + nextChar(); + } + String func = str.substring(startPos, this.pos); + x = parseFactor(); + if (func.equals("sqrt")) { + x = Math.sqrt(x); + } else if (func.equals("sin")) { + x = Math.sin(Math.toRadians(x)); + } else if (func.equals("cos")) { + x = Math.cos(Math.toRadians(x)); + } else if (func.equals("tan")) { + x = Math.tan(Math.toRadians(x)); + } else { + throw new RuntimeException("Unknown function: " + func); + } + } else { + throw new RuntimeException("Unexpected: " + (char) ch); + } - return x; - } - }.parse(); - } + // if (eat('^')) x = Math.pow(x, parseFactor()); // exponentiation -> This is + // causing a bit of problem + return x; + } + }.parse(); + } } diff --git a/src/main/java/pulse/io/readers/LFRReader.java b/src/main/java/pulse/io/readers/LFRReader.java index 52cf6116..c193ab1f 100644 --- a/src/main/java/pulse/io/readers/LFRReader.java +++ b/src/main/java/pulse/io/readers/LFRReader.java @@ -30,7 +30,7 @@ * Linseis software), test temperatures, and other variables. The individual * ASCII files encoded in ASCII represent tab-delimited time-temperature data. *

- * + * *

* {@code PULsE} currently accepts the formats of only those files output by * Linseis LFA systems that are in ASCII formats, so results from other systems @@ -42,182 +42,178 @@ * format). This should be done for any shot or curve you wish to analyse in * {@code PULsE}. *

- * + * *

* After all shots have been recorded, click “Severals†in the Linseis analysis * window and select all exported heating curve {@code .txt} files for the * experiment. Clicking “Ok†and “Save†on the following windows will create a * {@code .lfr} file with file locations and data for all the heating curves. * Save this in the same folder as the {@code .txt} files. - * + * */ - public class LFRReader implements CurveReader { - private static CurveReader instance = new LFRReader(); - private final static double TO_KELVIN = 273; - private final static double TO_SECONDS = 1E-3; - - private LFRReader() { - // intentionally blank - } - - /** - * @return The supported extension ({@code .lfr}). - */ - - @Override - public String getSupportedExtension() { - return Messages.getString("LFRReader.0"); - } - - /** - * Reads through the {@code file}, identifies the names of other files with - * individual heating curves, theirs external IDs and test temperatures (in - * degrees Celsius, later converted to Kelvin). - *

- * Creates a {@code List} of {@code ExperimentalData} objects with the size - * equal to the number of individual entries in the master-file. Searches for - * the individual files listed in the namelist and stored in the same directory - * where the master-file has been found previously. Upon finding the individual - * files, invokes {@code readSingleCurve} on each of them sequentially and - * stores the {@code ExperimentalData} in a list. Finally, invokes the - * {@code sort} method on that list to sort it. - *

- * - * @param file the master-file with {@code .lfr} suffix - * @return a {@code List} of @code ExperimentalData}, containing all information - * stored in both the master file and linked individual files. - * @see sort - * @see readSingleCurve - */ - - @Override - public List read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("LFRReader.1")); - - String stringSplitter = Messages.getString("LFRReader.3"); - - final String directory = file.getAbsoluteFile().getParent(); - final Map fileMap; - - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - fileMap = fileMap(reader, stringSplitter); - } - - return sort(convertToData(directory, stringSplitter, fileMap)); - - } - - private Map fileMap(BufferedReader reader, String stringSplitter) throws IOException { - - String delims = Messages.getString("LFRReader.2"); - StringTokenizer tokenizer; - - // skip two first lines - reader.readLine(); - reader.readLine(); - - var fileTempMap = new HashMap(); - - String tmp; - for (String line = reader.readLine(); line != null; line = reader.readLine()) { - tokenizer = new StringTokenizer(line); - int id = Integer.parseInt(tokenizer.nextToken(delims)); // id - - tmp = tokenizer.nextToken(delims).split(stringSplitter)[0]; // write file names without extensions - - tokenizer.nextToken(delims); // sample id - var temperature = derive(TEST_TEMPERATURE, parseDouble(tokenizer.nextToken()) + TO_KELVIN); // test - // temperature - - fileTempMap.put(tmp, new Metadata(temperature, id)); // assign metadata object with external id and - // temperature - - } - - return fileTempMap; - - } - - private List convertToData(String directory, String stringSplitter, Map map) - throws IOException { - List curves = new ArrayList<>(); - var filenames = map.keySet(); - - for (File f : new File(directory).listFiles()) { - - var name = f.getName().split(stringSplitter)[0]; - - if (filenames.contains(name)) - curves.add(readSingleCurve(f, map.get(name))); - - } - - return curves; - - } - - /** - * Creates a single {@code ExperimentalData} object with the time-temperature - * information retrieved from {@code file} and using the previously generated - * {@code Metadata} object, containing the external ID and the test temperature - * of this heating curve. - *

- * The time in Linseis files is usually stored in [ms], hence the time values - * are multiplied by {@code 1E-3} to adhere to the {@code PULsE} format. The - * signal rise is recorded in [mV], hence it represents a relative scale, which - * however is functionally linked to the temperature rise. {@code PULsE} does - * not establish this functional relation. Instead, it uses the signal values in - * the dimensionless problem formulation. - *

- * - * @param file the file with a data just enough for a single - * {@code ExperimentalData} object - * @param metadata the previously loaded {@code Metadata} which includes the - * external ID and the test temperature - * @return an {@code ExperimentalData} object - * @throws IOException - */ + private static CurveReader instance = new LFRReader(); + private final static double TO_KELVIN = 273; + private final static double TO_SECONDS = 1E-3; - public ExperimentalData readSingleCurve(File file, Metadata metadata) throws IOException { - Objects.requireNonNull(file, Messages.getString("LFRReader.9")); + private LFRReader() { + // intentionally blank + } - var curve = new ExperimentalData(); - curve.setMetadata(metadata); - curve.clear(); + /** + * @return The supported extension ({@code .lfr}). + */ + @Override + public String getSupportedExtension() { + return Messages.getString("LFRReader.0"); + } + + /** + * Reads through the {@code file}, identifies the names of other files with + * individual heating curves, theirs external IDs and test temperatures (in + * degrees Celsius, later converted to Kelvin). + *

+ * Creates a {@code List} of {@code ExperimentalData} objects with the size + * equal to the number of individual entries in the master-file. Searches + * for the individual files listed in the namelist and stored in the same + * directory where the master-file has been found previously. Upon finding + * the individual files, invokes {@code readSingleCurve} on each of them + * sequentially and stores the {@code ExperimentalData} in a list. Finally, + * invokes the {@code sort} method on that list to sort it. + *

+ * + * @param file the master-file with {@code .lfr} suffix + * @return a {@code List} of @code ExperimentalData}, containing all + * information stored in both the master file and linked individual files. + * @see sort + * @see readSingleCurve + */ + @Override + public List read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("LFRReader.1")); + + String stringSplitter = Messages.getString("LFRReader.3"); + + final String directory = file.getAbsoluteFile().getParent(); + final Map fileMap; + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + fileMap = fileMap(reader, stringSplitter); + } + + return sort(convertToData(directory, stringSplitter, fileMap)); + + } + + private Map fileMap(BufferedReader reader, String stringSplitter) throws IOException { + + String delims = Messages.getString("LFRReader.2"); + StringTokenizer tokenizer; + + // skip two first lines + reader.readLine(); + reader.readLine(); + + var fileTempMap = new HashMap(); + + String tmp; + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + tokenizer = new StringTokenizer(line); + int id = Integer.parseInt(tokenizer.nextToken(delims)); // id + + tmp = tokenizer.nextToken(delims).split(stringSplitter)[0]; // write file names without extensions + + tokenizer.nextToken(delims); // sample id + var temperature = derive(TEST_TEMPERATURE, parseDouble(tokenizer.nextToken()) + TO_KELVIN); // test + // temperature + + fileTempMap.put(tmp, new Metadata(temperature, id)); // assign metadata object with external id and + // temperature + + } + + return fileTempMap; + + } + + private List convertToData(String directory, String stringSplitter, Map map) + throws IOException { + List curves = new ArrayList<>(); + var filenames = map.keySet(); + + for (File f : new File(directory).listFiles()) { + + var name = f.getName().split(stringSplitter)[0]; + + if (filenames.contains(name)) { + curves.add(readSingleCurve(f, map.get(name))); + } + + } + + return curves; + + } + + /** + * Creates a single {@code ExperimentalData} object with the + * time-temperature information retrieved from {@code file} and using the + * previously generated {@code Metadata} object, containing the external ID + * and the test temperature of this heating curve. + *

+ * The time in Linseis files is usually stored in [ms], hence the time + * values are multiplied by {@code 1E-3} to adhere to the {@code PULsE} + * format. The signal rise is recorded in [mV], hence it represents a + * relative scale, which however is functionally linked to the temperature + * rise. {@code PULsE} does not establish this functional relation. Instead, + * it uses the signal values in the dimensionless problem formulation. + *

+ * + * @param file the file with a data just enough for a single + * {@code ExperimentalData} object + * @param metadata the previously loaded {@code Metadata} which includes the + * external ID and the test temperature + * @return an {@code ExperimentalData} object + * @throws IOException + */ + public ExperimentalData readSingleCurve(File file, Metadata metadata) throws IOException { + Objects.requireNonNull(file, Messages.getString("LFRReader.9")); - String delims = Messages.getString("LFRReader.10"); - StringTokenizer tokenizer; + var curve = new ExperimentalData(); + curve.setMetadata(metadata); + curve.clear(); - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - reader.readLine(); // skip first line - double time, temp; - for (String line = reader.readLine(); line != null; line = reader.readLine()) { - tokenizer = new StringTokenizer(line); + String delims = Messages.getString("LFRReader.10"); + StringTokenizer tokenizer; - time = parseDouble(tokenizer.nextToken(delims)) * TO_SECONDS; - temp = parseDouble(tokenizer.nextToken(delims)); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + reader.readLine(); // skip first line + double time, temp; + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + tokenizer = new StringTokenizer(line); - curve.addPoint(time, temp); + time = parseDouble(tokenizer.nextToken(delims)) * TO_SECONDS; + temp = parseDouble(tokenizer.nextToken(delims)); - } - curve.setRange(new Range(curve.getTimeSequence())); - } + curve.addPoint(time, temp); - return curve; + } + curve.setRange(new Range(curve.getTimeSequence())); + } - } + return curve; - /** - * Retrieves the single instance of this class. As this class uses a singleton - * pattern, there is only one such instance. - * - * @return the single instance of this class. - */ + } - public static CurveReader getInstance() { - return instance; - } + /** + * Retrieves the single instance of this class. As this class uses a + * singleton pattern, there is only one such instance. + * + * @return the single instance of this class. + */ + public static CurveReader getInstance() { + return instance; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/MetaFilePopulator.java b/src/main/java/pulse/io/readers/MetaFilePopulator.java index 59a9c8a0..e1f53a71 100644 --- a/src/main/java/pulse/io/readers/MetaFilePopulator.java +++ b/src/main/java/pulse/io/readers/MetaFilePopulator.java @@ -51,17 +51,17 @@ *

* The full list of keywords for the {@code .met} files are listed in the * {@code NumericPropertyKeyword} enum. - * + * *

* An example content of a valid {@code .met} file is provided below. *

- * + * *
  * 
- * Thickness	2.034 						
- * Diameter	9.88 			
- * Spot_Diameter	10.0 						
- *							
+ * Thickness	2.034
+ * Diameter	9.88
+ * Spot_Diameter	10.0
+ *
  * Test_Temperature	Pulse_Width	Spot_Diameter	Laser_Energy	Detector_Gain	TemporalShape	Detector_Iris
  * 200	200	5	2	31.81	50	TrapezoidalPulse	1
  * 201	196	5	2	31.81	100	TrapezoidalPulse	1
@@ -76,154 +76,151 @@
  * 210	400	5	2	31.81	10	TrapezoidalPulse	1
  * 
  * 
- * + * * @see pulse.properties.NumericPropertyKeyword * @see pulse.problem.laser.PulseTemporalShape */ - public class MetaFilePopulator implements AbstractPopulator { - private static MetaFilePopulator instance = new MetaFilePopulator(); - private final static double TO_KELVIN = 273; - - private MetaFilePopulator() { - // intentionally blank - } - - /** - * Gets the single instance of this class. - * - * @return a static instance of {@code MetaFilePopulator}. - */ - - public static MetaFilePopulator getInstance() { - return instance; - } - - @Override - public void populate(File file, Metadata met) throws IOException { - Objects.requireNonNull(file, Messages.getString("MetaFileReader.1")); //$NON-NLS-1$ - Map metaFormat = new HashMap<>(); - metaFormat.put(0, "ID"); // id must always be the first entry in the current row - - List tokens = new LinkedList<>(); + private static MetaFilePopulator instance = new MetaFilePopulator(); + private final static double TO_KELVIN = 273; - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + private MetaFilePopulator() { + // intentionally blank + } - for (String line = reader.readLine(); line != null; line = reader.readLine()) { + /** + * Gets the single instance of this class. + * + * @return a static instance of {@code MetaFilePopulator}. + */ + public static MetaFilePopulator getInstance() { + return instance; + } - tokens.clear(); - for (StringTokenizer st = new StringTokenizer(line); st.hasMoreTokens();) - tokens.add(st.nextToken()); + @Override + public void populate(File file, Metadata met) throws IOException { + Objects.requireNonNull(file, Messages.getString("MetaFileReader.1")); //$NON-NLS-1$ + Map metaFormat = new HashMap<>(); + metaFormat.put(0, "ID"); // id must always be the first entry in the current row - int size = tokens.size(); + List tokens = new LinkedList<>(); - if (size == 2) - processPair(tokens, met); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - else if (size > 2) { + for (String line = reader.readLine(); line != null; line = reader.readLine()) { - if (tokens.get(0).equalsIgnoreCase(metaFormat.get(0))) { + tokens.clear(); + for (StringTokenizer st = new StringTokenizer(line); st.hasMoreTokens();) { + tokens.add(st.nextToken()); + } - for (int i = 1; i < size; i++) - metaFormat.put(i, tokens.get(i)); + int size = tokens.size(); - } + if (size == 2) { + processPair(tokens, met); + } else if (size > 2) { - else if (Integer.compare(Integer.valueOf(tokens.get(0)), met.getExternalID()) == 0) { + if (tokens.get(0).equalsIgnoreCase(metaFormat.get(0))) { - processList(tokens, met, metaFormat); + for (int i = 1; i < size; i++) { + metaFormat.put(i, tokens.get(i)); + } - } + } else if (Integer.compare(Integer.valueOf(tokens.get(0)), met.getExternalID()) == 0) { - } + processList(tokens, met, metaFormat); - } + } - } - } + } - private void processPair(List tokens, Metadata met) { - List> val = new ArrayList<>(); - var entry = new ImmutableDataEntry<>(tokens.get(0), tokens.get(1)); - val.add(entry); + } - try { - translate(val, met); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - System.err.println("Error changing property in Metadata object. Details below."); - e.printStackTrace(); - } - } + } + } - private void processList(List tokens, Metadata met, Map metaFormat) { - int size = tokens.size(); - List> values = new ArrayList<>(size); + private void processPair(List tokens, Metadata met) { + List> val = new ArrayList<>(); + var entry = new ImmutableDataEntry<>(tokens.get(0), tokens.get(1)); + val.add(entry); - for (int i = 1; i < size; i++) - values.add(new ImmutableDataEntry<>(metaFormat.get(i), tokens.get(i))); + try { + translate(val, met); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + System.err.println("Error changing property in Metadata object. Details below."); + e.printStackTrace(); + } + } - try { - translate(values, met); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - System.err.println("Error changing property in Metadata object. Details below."); - e.printStackTrace(); - } - } + private void processList(List tokens, Metadata met, Map metaFormat) { + int size = tokens.size(); + List> values = new ArrayList<>(size); - private void translate(List> data, Metadata met) - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + for (int i = 1; i < size; i++) { + values.add(new ImmutableDataEntry<>(metaFormat.get(i), tokens.get(i))); + } - for (var dataEntry : data) { + try { + translate(values, met); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + System.err.println("Error changing property in Metadata object. Details below."); + e.printStackTrace(); + } + } - var optional = findAny(dataEntry.getKey()); + private void translate(List> data, Metadata met) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - // numeric properties - if (optional.isPresent()) { - var key = optional.get(); + for (var dataEntry : data) { - double value = Double.valueOf(dataEntry.getValue()); - if (key == TEST_TEMPERATURE) - value += TO_KELVIN; + var optional = findAny(dataEntry.getKey()); - var proto = def(key); - value /= proto.getDimensionFactor().doubleValue(); + // numeric properties + if (optional.isPresent()) { + var key = optional.get(); - if (isValueSensible(proto, value)) { - proto.setValue(value); - met.set(key, proto); - } + double value = Double.valueOf(dataEntry.getValue()); + if (key == TEST_TEMPERATURE) { + value += TO_KELVIN; + } - } + var proto = def(key); + value /= proto.getDimensionFactor().doubleValue(); - // generic properties - else { + if (isValueSensible(proto, value)) { + proto.setValue(value); + met.set(key, proto); + } - for (Property genericEntry : met.genericProperties()) { + } // generic properties + else { - if (genericEntry instanceof InstanceDescriptor - || dataEntry.getKey().equalsIgnoreCase(genericEntry.getClass().getSimpleName())) { + for (Property genericEntry : met.genericProperties()) { - if (genericEntry.attemptUpdate(dataEntry.getValue())) - met.updateProperty(instance, genericEntry); + if (genericEntry instanceof InstanceDescriptor + || dataEntry.getKey().equalsIgnoreCase(genericEntry.getClass().getSimpleName())) { - } + if (genericEntry.attemptUpdate(dataEntry.getValue())) { + met.updateProperty(instance, genericEntry); + } - } + } - } + } - } + } - } + } - /** - * @return {@code .met}, an internal PULsE meta-file format. - */ + } - @Override - public String getSupportedExtension() { - return Messages.getString("MetaFileReader.0"); //$NON-NLS-1$ - } + /** + * @return {@code .met}, an internal PULsE meta-file format. + */ + @Override + public String getSupportedExtension() { + return Messages.getString("MetaFileReader.0"); //$NON-NLS-1$ + } } diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java new file mode 100644 index 00000000..d9837f13 --- /dev/null +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -0,0 +1,310 @@ +package pulse.io.readers; + +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import pulse.AbstractData; +import pulse.input.ExperimentalData; +import pulse.input.Metadata; +import pulse.input.Range; +import pulse.properties.NumericPropertyKeyword; +import pulse.properties.SampleName; +import pulse.ui.Messages; + +/** + * Reads the .CSV files exported from Proteus LFA Analysis software. To load + * Proteus measurements in PULsE, the detector signal needs to be imported + * first, followed by the pulse data. + *

+ * Note that by default the decimal separator is assumed to be a point ("."). + *

+ */ +public class NetzschCSVReader implements CurveReader { + + private static NetzschCSVReader instance = new NetzschCSVReader(); + + private final static double TO_KELVIN = 273; + protected final static double TO_SECONDS = 1E-3; + private final static double TO_METRES = 1E-3; + + private final static String SAMPLE_TEMPERATURE = "Sample_temperature"; + private final static String SHOT_DATA = "Shot_data"; + private final static String DETECTOR = "DETECTOR"; + 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"; + private final static String MATERIAL = "Material"; + + /** + * 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; + } + + /** + * @return The supported extension ({@code .csv}). + */ + @Override + public String getSupportedExtension() { + return Messages.getString("NetzschCSVReader.0"); + } + + /** + * Reads {@code file}, assuming that it contains data generated by Proteus + * with the detector signal. + *

+ * This will throw an {@code IllegalArgumentException} if the first entry in + * this file does not contain the {@value SHOT_DATA} string. If this is + * found, then an ID is extracted from the file, which will then be used to + * associate a pulse with the newly create {@code ExperimentalData} (this + * requires another reader. When the ID is identified, the file is searched + * for the keywords {@value THICKNESS} and {@value SAMPLE_TEMPERATURE} to + * determine the sample thickness and baseline temperature of the shot. Then + * the method proceeds to search for the {@code DETECTOR} keyword, marking + * the beginning of the experimental time-signal sequence. If, for example, + * the file only contains the pulse data, the method will return an empty + * list and print an error message in the log, saying that the file was + * skipped. Otherwise, the time-signal sequence will be read, taking care to + * convert the time (in milliseconds by default) to second (used by default + * in PULsE). + *

+ * + * @return a list containing either zero elements, if the procedure failed, + * or one element, corresponding to the stored shot data. + * + */ + @Override + 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 + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + + 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; + 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); + final double diameter = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; + + 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. + */ + var detectorLabel = findLineByLabel(reader, DETECTOR, true); + + if (detectorLabel == null) { + System.err.println("Skipping " + file.getName()); + return new ArrayList<>(); + } + + reader.readLine(); + 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.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)); + met.set(NumericPropertyKeyword.SPOT_DIAMETER, derive(NumericPropertyKeyword.SPOT_DIAMETER, 0.94 * diameter)); + + curve.setMetadata(met); + curve.setRange(new Range(curve.getTimeSequence())); + return new ArrayList<>(Arrays.asList(curve)); + + } catch (ParseException ex) { + Logger.getLogger(NetzschCSVReader.class.getName()).log(Level.SEVERE, null, ex); + } + + return null; + + } + + /** + * Note: the {@code line} must contain a decimal-separated number. + * + * @param line a line containing number with a decimal separator + */ + private static void guessLocaleAndFormat(String line) { + + if (line.contains(".")) { + delims = ENGLISH_DELIMS; + locale = Locale.ENGLISH; + } else { + delims = GERMAN_DELIMS; + locale = Locale.GERMAN; + } + + format = DecimalFormat.getInstance(locale); + format.setGroupingUsed(false); + } + + protected static void populate(AbstractData data, BufferedReader reader) throws IOException, ParseException { + double time; + double power; + String[] tokens; + + for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { + tokens = line.split(delims); + + 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); + } + + } + + protected static int determineShotID(BufferedReader reader, File file) throws IOException { + String shotIDLine = reader.readLine(); + 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() + + " is not a recognised Netzch CSV file. First entry is: " + shotID[shotID.length - 2]); + } else { + id = Integer.parseInt(shotID[shotID.length - 1]); + } + + return id; + + } + + 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()) { + continue; + } + + if (!ignoreLocale) { + guessLocaleAndFormat(line); + } + tokens = line.split(delims); + + for (String token : tokens) { + + if (token.equalsIgnoreCase(label)) { + break outer; + } + + if (token.equalsIgnoreCase(stopLabel)) { + line = null; + reader.reset(); + break outer; + } + + } + + } + + return line; + + } + + /** + * As this class uses the singleton pattern, only one instance is created + * using an empty no-argument constructor. + * + * @return the single instance of this class. + */ + public static CurveReader getInstance() { + return instance; + } + + /** + * Get the standard delimiter chars. + * + * @return delims + */ + public static String getDelims() { + return delims; + } + +} diff --git a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java new file mode 100644 index 00000000..bfc07fe1 --- /dev/null +++ b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java @@ -0,0 +1,95 @@ +package pulse.io.readers; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.text.ParseException; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import pulse.problem.laser.NumericPulseData; +import pulse.ui.Messages; + +/** + * Reads numeric pulse data generated by the Proteus LFA Analysis export tool. + * The data must have a decimal point separator and should follow, in general, + * the same rules set by the NetzschPulseCSVReader + * + * @see pulse.io.reader.NetzschCSVReader + * + */ +public class NetzschPulseCSVReader implements PulseDataReader { + + private static PulseDataReader instance = new NetzschPulseCSVReader(); + + private final static String PULSE = "Laser_pulse_data"; + + private NetzschPulseCSVReader() { + // intentionally blank + } + + /** + * @return The supported extension ({@code .csv}). + */ + @Override + public String getSupportedExtension() { + return Messages.getString("NetzschCSVReader.0"); + } + + /** + * This performs a basic check, finding the shot ID, which is then passed to + * a new {@code NumericPulseData} object.The latter is populated using the + * time-power sequence stored in this file.If the {@value PULSE} keyword is + * not found, the method will display an error. + * + * @param file + * @throws java.io.IOException + * @see pulse.io.readers.NetzschCSVReader.read() + * @return a new {@code NumericPulseData} object encapsulating the contents + * of {@code file} + */ + @Override + public NumericPulseData read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("DATReader.1")); + + NumericPulseData data = null; + + ((NetzschCSVReader) NetzschCSVReader.getInstance()) + .setDefaultLocale(); //always start with a default locale + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + + int shotId = NetzschCSVReader.determineShotID(reader, file); + data = new NumericPulseData(shotId); + + var pulseLabel = NetzschCSVReader.findLineByLabel(reader, PULSE, false); + + if (pulseLabel == null) { + System.err.println("Skipping " + file.getName()); + return null; + } + + reader.readLine(); + NetzschCSVReader.populate(data, reader); + + } catch (ParseException ex) { + Logger.getLogger(NetzschPulseCSVReader.class.getName()).log(Level.SEVERE, null, ex); + } + + return data; + + } + + /** + * As this class uses the singleton pattern, only one instance is created + * using an empty no-argument constructor. + * + * @return the single instance of this class. + */ + public static PulseDataReader getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/io/readers/PulseDataReader.java b/src/main/java/pulse/io/readers/PulseDataReader.java new file mode 100644 index 00000000..b478cf62 --- /dev/null +++ b/src/main/java/pulse/io/readers/PulseDataReader.java @@ -0,0 +1,20 @@ +package pulse.io.readers; + +import java.io.File; +import java.io.IOException; + +import pulse.problem.laser.NumericPulseData; + +/** + * A reader for importing numeric pulse data -- if available. + * + */ +public interface PulseDataReader extends AbstractReader { + + /** + * Converts the ASCII file to a {@code NumericPulseData} object. + */ + @Override + public abstract NumericPulseData read(File file) throws IOException; + +} diff --git a/src/main/java/pulse/io/readers/QuadratureReader.java b/src/main/java/pulse/io/readers/QuadratureReader.java index caba2913..fbcd7520 100644 --- a/src/main/java/pulse/io/readers/QuadratureReader.java +++ b/src/main/java/pulse/io/readers/QuadratureReader.java @@ -18,84 +18,81 @@ * associated resource folder. * */ - public class QuadratureReader implements AbstractReader { - private final static String SUPPORTED_EXTENSION = "quad"; - - private static QuadratureReader instance = new QuadratureReader(); - - private QuadratureReader() { - // intentionally blank - } - - /** - * Reads an ordinate set. Scans the first line for any keywords and then treats - * any subsequent lines as consisting of two tokens, which correspond to the - * quadrature node and weight. Ignores all other information. - */ - - @Override - public OrdinateSet read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("TBLReader.1")); + private final static String SUPPORTED_EXTENSION = "quad"; - // ignore extension! + private static QuadratureReader instance = new QuadratureReader(); - String name = file.getName().split("\\.")[0]; + private QuadratureReader() { + // intentionally blank + } - OrdinateSet set = null; + /** + * Reads an ordinate set. Scans the first line for any keywords and then + * treats any subsequent lines as consisting of two tokens, which correspond + * to the quadrature node and weight. Ignores all other information. + */ + @Override + public OrdinateSet read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("TBLReader.1")); - String delims = Messages.getString("}{,\t "); - StringTokenizer tokenizer; + // ignore extension! + String name = file.getName().split("\\.")[0]; - List nodes = new ArrayList<>(); - List weights = new ArrayList<>(); + OrdinateSet set = null; - String line = ""; + String delims = Messages.getString("}{,\t "); + StringTokenizer tokenizer; - try (var fr = new FileReader(file); var reader = new BufferedReader(fr)) { + List nodes = new ArrayList<>(); + List weights = new ArrayList<>(); - // first line with declarations (e.g. IGNORE, etc.) - tokenizer = new StringTokenizer(reader.readLine()); + String line = ""; - while (tokenizer.hasMoreTokens()) - if (tokenizer.nextToken(delims).equalsIgnoreCase("IGNORE")) - return null; + try (var fr = new FileReader(file); var reader = new BufferedReader(fr)) { - for (line = reader.readLine(); line != null; line = reader.readLine()) { - tokenizer = new StringTokenizer(line); - nodes.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); - weights.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); - } + // first line with declarations (e.g. IGNORE, etc.) + tokenizer = new StringTokenizer(reader.readLine()); - set = new OrdinateSet(name, nodes.stream().mapToDouble(d -> d).toArray(), - weights.stream().mapToDouble(d -> d).toArray()); + while (tokenizer.hasMoreTokens()) { + if (tokenizer.nextToken(delims).equalsIgnoreCase("IGNORE")) { + return null; + } + } - reader.close(); + for (line = reader.readLine(); line != null; line = reader.readLine()) { + tokenizer = new StringTokenizer(line); + nodes.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); + weights.add((ExpressionParser.evaluate(tokenizer.nextToken(delims)))); + } - } + set = new OrdinateSet(name, nodes.stream().mapToDouble(d -> d).toArray(), + weights.stream().mapToDouble(d -> d).toArray()); - return set; + reader.close(); - } + } - /** - * @return {@code quad} - */ + return set; - @Override - public String getSupportedExtension() { - return SUPPORTED_EXTENSION; - } + } - /** - * Returns the single instance of this class. - * - * @return an instance of {@code QuadratureReader}. - */ + /** + * @return {@code quad} + */ + @Override + public String getSupportedExtension() { + return SUPPORTED_EXTENSION; + } - public static QuadratureReader getInstance() { - return instance; - } + /** + * Returns the single instance of this class. + * + * @return an instance of {@code QuadratureReader}. + */ + public static QuadratureReader getInstance() { + return instance; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/io/readers/ReaderManager.java b/src/main/java/pulse/io/readers/ReaderManager.java index 44c87642..5d168a93 100644 --- a/src/main/java/pulse/io/readers/ReaderManager.java +++ b/src/main/java/pulse/io/readers/ReaderManager.java @@ -8,11 +8,19 @@ import java.util.Objects; import java.util.Scanner; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import pulse.ui.Messages; +import pulse.ui.Version; import pulse.util.ReflexiveFinder; /** @@ -27,225 +35,274 @@ *

* This class heavily relies on the stream API from the Java SDK. *

- * + * * @see pulse.util.Reflexive * @see pulse.io.readers.CurveReader * @see pulse.io.readers.DatasetReader */ - public class ReaderManager { - @SuppressWarnings("rawtypes") - private static List allReaders = allReaders(); - - private static List allDataExtensions = supportedExtensions(ReaderManager.curveReaders()); - private static List allDatasetExtensions = supportedExtensions(ReaderManager.datasetReaders()); - - private ReaderManager() { - // intentionally blank - } - - private static List supportedExtensions(List> readers) { - return readers.stream().map(reader -> reader.getSupportedExtension()).collect(Collectors.toList()); - } - - /** - * Returns a list of extensions recognised by the available - * {@code CurveReader}s. - * - * @return a {@code List} of {@String} objects representing file extensions - */ - - public static List getCurveExtensions() { - return allDataExtensions; - } - - /** - * Returns a list of extensions recognised by the available - * {@code DatasetReader}s. - * - * @return a {@code List} of {@String} objects representing file extensions - */ - - public static List getDatasetExtensions() { - return allDatasetExtensions; - } - - /** - * Finds all classes assignable from {@code AbstractReader} within the - * {@code pckgname} package. - * - * @param pckgname the name of the package for the classes to be searched in - * @return a list of {@code AbstractReader}s in {@code pckgnamge} - */ - - @SuppressWarnings("rawtypes") - private static List allReaders(String pckgname) { - return ReflexiveFinder.simpleInstances(pckgname).stream().filter(ref -> ref instanceof AbstractReader) - .map(reflexive -> (AbstractReader) reflexive).collect(Collectors.toList()); - } - - /** - * Finds all classes assignable from {@code AbstractReader} within this - * package. - * - * @return a list of {@code AbstractReader}s in this package - */ - - @SuppressWarnings("rawtypes") - private static List allReaders() { - return allReaders(ReaderManager.class.getPackage().getName()); - } - - /** - * Finds all classes assignable from {@code CurveReader} within the - * {@code pckgname} package. - * - * @param pckgname the name of the package to conduct search in. - * @return a list of {@code CurveReader}s in {@code pckgname} - */ - - public static List findCurveReaders(String pckgname) { - return allReaders.stream().filter(reader -> reader instanceof CurveReader).map(r -> (CurveReader) r) - .collect(Collectors.toList()); - } - - /** - * Finds all classes assignable from {@code CurveReader} within this - * package. - * - * @return a list of {@code CurveReader}s in this package - */ - - public static List curveReaders() { - return findCurveReaders(ReaderManager.class.getPackage().getName()); - } - - /** - * Finds all classes assignable from {@code DatasetReader} within the - * {@code pckgname} package. - * - * @param pckgname the name of the package to conduct search in. - * @return a list of {@code DatasetReader}s in {@code pckgname} - */ - - public static List findDatasetReaders(String pckgname) { - return allReaders.stream().filter(reader -> reader instanceof DatasetReader).map(r -> (DatasetReader) r) - .collect(Collectors.toList()); - } - - /** - * Finds all classes assignable from {@code DatasetReader} within this - * package. - * - * @return a list of {@code DatasetReader}s in this package - */ - - public static List datasetReaders() { - return findDatasetReaders(ReaderManager.class.getPackage().getName()); - } - - /** - * Attempts to find a {@code DatasetReader} for processing {@code file}. - * - * @param file the target file supposedly containing data for an - * {@code InterpolationDataset}. - * @return an {@code InterpolationDataset} extracted from {@file} using the - * first available {@code DatasetReader} from the list - * @throws IOException if the reader has been found, but an error - * occurred when reading the file - * @throws IllegalArgumentException if the file has an unsupported extension - */ - - public static T read(List> readers, File file) { - Objects.requireNonNull(readers); - - var optional = readers.stream() - .filter(reader -> AbstractHandler.extensionsMatch(file, reader.getSupportedExtension())).findFirst(); - - if (!optional.isPresent()) - throw new IllegalArgumentException(Messages.getString("ReaderManager.1") + file.getName()); - - T result = null; - - try { - result = optional.get().read(file); - } catch (IOException e) { - System.err.println("Error reading " + file + " with reader: " + optional.get()); - e.printStackTrace(); - } - - return result; - - } - - /** - * Obtains a set of files in {@code directory} and attemps to convert each file - * to {@code T} using {@code readers}. - * - * @param a type recognised by {@code readers} - * @param readers a list of {@code AbstractReader}s capable of processing - * {@code T} - * @param directory a directory - * @return the set of converted {@code T} objects - * @throws IllegalArgumentException if second argument is not a directory - */ - - public static Set readDirectory(List> readers, File directory) - throws IllegalArgumentException { - if (!directory.isDirectory()) - throw new IllegalArgumentException("Not a directory: " + directory); - - var list = new HashSet(); - - for(File f : directory.listFiles()) - list.add( read(readers, f) ); - - return list; - } - - /** - * This method is specifically introduced to handle multiple files in a - * resources folder enclosed within the {@code jar} archive. A list of files is - * required to be included in the same location, which is scanned and each entry - * is added to a temporary list of names. A combination of these names with the - * relative {@code location} allows reading separate files and collating the - * result in a unique {@code Set}. - * - * @param a type recognised by the {@code reader} - * @param reader the reader specifically targetted at {@code T} - * @param location the relative location of files - * @param listName the name of the list-file - * @return a unique {@code Set} of {@code T} - */ - - public static Set load(AbstractReader reader, String location, String listName) { - - var stream = ReaderManager.class.getResourceAsStream(location + listName); - var names = new ArrayList(); - - try (Scanner s = new Scanner(stream)) { - while (s.hasNext()) - names.add(s.next()); - } - - return names.stream().map(name -> readSpecific(reader, location, name)).map(obj -> (T) obj) - .collect(Collectors.toSet()); - - } - - private static T readSpecific(AbstractReader reader, String location, String name) { - T result = null; - try { - var f = File.createTempFile(name, ".tmp"); - f.deleteOnExit(); - FileUtils.copyInputStreamToFile(ReaderManager.class.getResourceAsStream(location + name), f); - result = reader.read(f); - } catch (IOException e) { - System.err.println("Unable to read: " + name); - e.printStackTrace(); - } - return result; - } - -} \ No newline at end of file + @SuppressWarnings("rawtypes") + private static List allReaders = allReaders(); + + private static List allDataExtensions = supportedExtensions(ReaderManager.curveReaders()); + private static List allPulseExtensions = supportedExtensions(ReaderManager.pulseReaders()); + private static List allDatasetExtensions = supportedExtensions(ReaderManager.datasetReaders()); + + private ReaderManager() { + // intentionally blank + } + + private static List supportedExtensions(List> readers) { + return readers.stream().map(reader -> reader.getSupportedExtension()).collect(Collectors.toList()); + } + + /** + * Returns a list of extensions recognised by the available + * {@code CurveReader}s. + * + * @return a {@code List} of { + * @String} objects representing file extensions + */ + public static List getCurveExtensions() { + return allDataExtensions; + } + + public static List getPulseExtensions() { + return allPulseExtensions; + } + + /** + * Returns a list of extensions recognised by the available + * {@code DatasetReader}s. + * + * @return a {@code List} of { + * @String} objects representing file extensions + */ + public static List getDatasetExtensions() { + return allDatasetExtensions; + } + + /** + * Finds all classes assignable from {@code AbstractReader} within the + * {@code pckgname} package. + * + * @param pckgname the name of the package for the classes to be searched in + * @return a list of {@code AbstractReader}s in {@code pckgnamge} + */ + @SuppressWarnings("rawtypes") + private static List allReaders(String pckgname) { + return ReflexiveFinder.simpleInstances(pckgname).stream().filter(ref -> ref instanceof AbstractReader) + .map(reflexive -> (AbstractReader) reflexive).collect(Collectors.toList()); + } + + /** + * Finds all classes assignable from {@code AbstractReader} within + * this + * package. + * + * @return a list of {@code AbstractReader}s in this package + */ + @SuppressWarnings("rawtypes") + private static List allReaders() { + return allReaders(ReaderManager.class.getPackage().getName()); + } + + /** + * Finds all classes assignable from {@code CurveReader} within the + * {@code pckgname} package. + * + * @param pckgname the name of the package to conduct search in. + * @return a list of {@code CurveReader}s in {@code pckgname} + */ + public static List findCurveReaders(String pckgname) { + return allReaders.stream().filter(reader -> reader instanceof CurveReader).map(r -> (CurveReader) r) + .collect(Collectors.toList()); + } + + public static List findPulseReaders(String pckgname) { + return allReaders.stream().filter(reader -> reader instanceof PulseDataReader).map(r -> (PulseDataReader) r) + .collect(Collectors.toList()); + } + + /** + * Finds all classes assignable from {@code CurveReader} within this + * package. + * + * @return a list of {@code CurveReader}s in this package + */ + public static List curveReaders() { + return findCurveReaders(ReaderManager.class.getPackage().getName()); + } + + public static List pulseReaders() { + return findPulseReaders(ReaderManager.class.getPackage().getName()); + } + + /** + * Finds all classes assignable from {@code DatasetReader} within the + * {@code pckgname} package. + * + * @param pckgname the name of the package to conduct search in. + * @return a list of {@code DatasetReader}s in {@code pckgname} + */ + public static List findDatasetReaders(String pckgname) { + return allReaders.stream().filter(reader -> reader instanceof DatasetReader).map(r -> (DatasetReader) r) + .collect(Collectors.toList()); + } + + /** + * Finds all classes assignable from {@code DatasetReader} within + * this + * package. + * + * @return a list of {@code DatasetReader}s in this package + */ + public static List datasetReaders() { + return findDatasetReaders(ReaderManager.class.getPackage().getName()); + } + + /** + * Attempts to find a {@code DatasetReader} for processing {@code file}. + * + * @param + * @param readers + * @param file the target file supposedly containing data for an + * {@code InterpolationDataset}. + * @return an {@code InterpolationDataset} extracted from { + * @file} using the first available {@code DatasetReader} from the list + * @throws IllegalArgumentException if the file has an unsupported extension + */ + public static T read(List> readers, File file) { + Objects.requireNonNull(readers); + + var optional = readers.stream() + .filter(reader -> AbstractHandler.extensionsMatch(file, reader.getSupportedExtension())).findFirst(); + + if (!optional.isPresent()) { + throw new IllegalArgumentException(Messages.getString("ReaderManager.1") + file.getName()); + } + + T result = null; + + try { + result = optional.get().read(file); + } catch (IOException e) { + System.err.println("Error reading " + file + " with reader: " + optional.get()); + e.printStackTrace(); + } + + return result; + + } + + /** + * Obtains a set of files in {@code directory} and attemps to convert each + * file to {@code T} using {@code readers}. + * + * @param a type recognised by {@code readers} + * @param readers a list of {@code AbstractReader}s capable of processing + * {@code T} + * @param directory a directory + * @return the set of converted {@code T} objects + * @throws IllegalArgumentException if second argument is not a directory + */ + public static Set readDirectory(List> readers, File directory) + throws IllegalArgumentException { + if (!directory.isDirectory()) { + throw new IllegalArgumentException("Not a directory: " + directory); + } + + 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) { + result.add(f.get()); + } + + } catch (InterruptedException ex) { + 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, + "Error executing read operation using concurrency", ex); + } + + return result; + } + + /** + * This method is specifically introduced to handle multiple files in a + * resources folder enclosed within the {@code jar} archive. A list of files + * is required to be included in the same location, which is scanned and + * each entry is added to a temporary list of names. A combination of these + * names with the relative {@code location} allows reading separate files + * and collating the result in a unique {@code Set}. + * + * @param a type recognised by the {@code reader} + * @param reader the reader specifically targetted at {@code T} + * @param location the relative location of files + * @param listName the name of the list-file + * @return a unique {@code Set} of {@code T} + */ + public static Set load(AbstractReader reader, String location, String listName) { + + var stream = ReaderManager.class.getResourceAsStream(location + listName); + var names = new ArrayList(); + + try (Scanner s = new Scanner(stream)) { + while (s.hasNext()) { + names.add(s.next()); + } + } + + return names.stream().map(name -> readSpecific(reader, location, name)).map(obj -> (T) obj) + .collect(Collectors.toSet()); + + } + + private static T readSpecific(AbstractReader reader, String location, String name) { + T result = null; + try { + var f = File.createTempFile(name, ".tmp"); + f.deleteOnExit(); + FileUtils.copyInputStreamToFile(ReaderManager.class.getResourceAsStream(location + name), f); + result = reader.read(f); + } catch (IOException e) { + System.err.println("Unable to read: " + name); + e.printStackTrace(); + } + return result; + } + + public static Version readVersion() { + var versionInfoFile = Version.class.getResource("/Version.txt"); + String versionLabel = ""; + long date = 0; + try { + date = versionInfoFile.openConnection().getLastModified(); + } catch (IOException e1) { + System.err.println("Could not connect to local version file!"); + e1.printStackTrace(); + } + try { + versionLabel = IOUtils.toString(versionInfoFile, "UTF-8"); + } catch (IOException e) { + System.err.println("Could not read current version!"); + e.printStackTrace(); + } + return new Version(versionLabel, date); + } + +} diff --git a/src/main/java/pulse/io/readers/TBLReader.java b/src/main/java/pulse/io/readers/TBLReader.java index c067397f..65cae937 100644 --- a/src/main/java/pulse/io/readers/TBLReader.java +++ b/src/main/java/pulse/io/readers/TBLReader.java @@ -27,7 +27,7 @@ *

* Below is an example of a valid {@code .tbl} file: *

- * + * *
  * 
  * -273	11000.00
@@ -43,73 +43,72 @@
  * 450	10814.93
  * 500	10798.58
  * 550	10782.14
- 
+ * 
  * 
*/ - public class TBLReader implements DatasetReader { - private static DatasetReader instance = new TBLReader(); - - private TBLReader() { - // intentionally blank - } - - /** - * @return a String equal to '{@code tbl}' - */ - - @Override - public String getSupportedExtension() { - return Messages.getString("TBLReader.0"); - } - - /** - * As this class is built using a singleton pattern, only one instance exists. - * - * @return the static instance of this class - */ - - public static DatasetReader getInstance() { - return instance; - } - - /** - * Reads through a {@code file} with {@code .tbl extension}, converting each row - * into an {@code ImmutableDataEntry}, which is then added to a - * newly created {@code InterpolationDataset}. Upon completion, the - * {@code doInterpolation()} method of {@code InterpolationDataset} is invoked. - * - * @see pulse.input.InterpolationDataset.doInterpolation() - * @param file a {@code File} with {@code tbl} extension - */ - - @Override - public InterpolationDataset read(File file) throws IOException { - Objects.requireNonNull(file, Messages.getString("TBLReader.1")); - - if (!isExtensionSupported(file)) - throw new IllegalArgumentException("Extension not supported: " + file.getName()); - - var curve = new InterpolationDataset(); - - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - String delims = Messages.getString("TBLReader.2"); - StringTokenizer tokenizer; - - for (String line = reader.readLine(); line != null; line = reader.readLine()) { - tokenizer = new StringTokenizer(line); - curve.add(new ImmutableDataEntry<>(parse(tokenizer, delims), parse(tokenizer, delims))); - } - } - - curve.doInterpolation(); - return curve; - - } - - private static Double parse(StringTokenizer tokenizer, String delims) { - return Double.parseDouble(tokenizer.nextToken(delims)); - } - -} \ No newline at end of file + private static DatasetReader instance = new TBLReader(); + + private TBLReader() { + // intentionally blank + } + + /** + * @return a String equal to '{@code tbl}' + */ + @Override + public String getSupportedExtension() { + return Messages.getString("TBLReader.0"); + } + + /** + * As this class is built using a singleton pattern, only one instance + * exists. + * + * @return the static instance of this class + */ + public static DatasetReader getInstance() { + return instance; + } + + /** + * Reads through a {@code file} with {@code .tbl extension}, converting each + * row into an {@code ImmutableDataEntry}, which is then + * added to a newly created {@code InterpolationDataset}. Upon completion, + * the {@code doInterpolation()} method of {@code InterpolationDataset} is + * invoked. + * + * @see pulse.input.InterpolationDataset.doInterpolation() + * @param file a {@code File} with {@code tbl} extension + */ + @Override + public InterpolationDataset read(File file) throws IOException { + Objects.requireNonNull(file, Messages.getString("TBLReader.1")); + + if (!isExtensionSupported(file)) { + throw new IllegalArgumentException("Extension not supported: " + file.getName()); + } + + var curve = new InterpolationDataset(); + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String delims = Messages.getString("TBLReader.2"); + StringTokenizer tokenizer; + + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + tokenizer = new StringTokenizer(line); + curve.add(new ImmutableDataEntry<>(parse(tokenizer, delims), parse(tokenizer, delims))); + } + } + + curve.doInterpolation(); + return curve; + + } + + private static Double parse(StringTokenizer tokenizer, String delims) { + return Double.parseDouble(tokenizer.nextToken(delims)); + } + +} diff --git a/src/main/java/pulse/io/readers/package-info.java b/src/main/java/pulse/io/readers/package-info.java index f3566804..b94086f9 100644 --- a/src/main/java/pulse/io/readers/package-info.java +++ b/src/main/java/pulse/io/readers/package-info.java @@ -8,5 +8,4 @@ * {@code AbstractReader} and place it into this package (otherwise, the * {@code ReaderManager} won't know where the class is). */ - -package pulse.io.readers; \ No newline at end of file +package pulse.io.readers; diff --git a/src/main/java/pulse/math/AbstractIntegrator.java b/src/main/java/pulse/math/AbstractIntegrator.java index 1992f3a6..b56c00ea 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,61 +11,56 @@ * or more variables and the other to actually do the integration. * */ - -public abstract class AbstractIntegrator extends PropertyHolder implements Reflexive { - - private Segment integrationBounds; - - /** - * Creates an {@code AbstractIntegrator} with the specified integration bounds. - * - * @param bounds the integration bounds. - */ - - public AbstractIntegrator(Segment bounds) { - setBounds(bounds); - } - - /** - * Calculates the definite integral within the specified integration bounds. - * - * @return the value of the integral - */ - - public abstract double integrate(); - - /** - * Calculates the integrand function. - * - * @param vars one or more variables - * @return the value of the integrand at the specified variable values. - */ - - public abstract double integrand(double... vars); - - /** - * Retrieves the integration bounds - * - * @return the integration bounds. - */ - - public Segment getBounds() { - return integrationBounds; - } - - /** - * Simply sets the integration bounds to {@code bounds} - * - * @param bounds the new integration bounds. - */ - - public void setBounds(Segment bounds) { - this.integrationBounds = bounds; - } - - @Override - public String getPrefix() { - return "Integrator"; - } - -} \ No newline at end of file +public abstract class AbstractIntegrator extends PropertyHolder implements Reflexive, Serializable { + + private Segment integrationBounds; + + /** + * Creates an {@code AbstractIntegrator} with the specified integration + * bounds. + * + * @param bounds the integration bounds. + */ + public AbstractIntegrator(Segment bounds) { + setBounds(bounds); + } + + /** + * Calculates the definite integral within the specified integration bounds. + * + * @return the value of the integral + */ + public abstract double integrate(); + + /** + * Calculates the integrand function. + * + * @param vars one or more variables + * @return the value of the integrand at the specified variable values. + */ + public abstract double integrand(double... vars); + + /** + * Retrieves the integration bounds + * + * @return the integration bounds. + */ + public Segment getBounds() { + return integrationBounds; + } + + /** + * Simply sets the integration bounds to {@code bounds} + * + * @param bounds the new integration bounds. + */ + public void setBounds(Segment bounds) { + this.integrationBounds = bounds; + } + + @Override + public String getPrefix() { + return "Integrator"; + } + +} diff --git a/src/main/java/pulse/math/FFTTransformer.java b/src/main/java/pulse/math/FFTTransformer.java new file mode 100644 index 00000000..dea0d65b --- /dev/null +++ b/src/main/java/pulse/math/FFTTransformer.java @@ -0,0 +1,138 @@ +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 implements Serializable { + + private static final long serialVersionUID = -5424502578926616928L; + 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; + } + +} diff --git a/src/main/java/pulse/math/FixedIntervalIntegrator.java b/src/main/java/pulse/math/FixedIntervalIntegrator.java index 8a54bf29..8631efb2 100644 --- a/src/main/java/pulse/math/FixedIntervalIntegrator.java +++ b/src/main/java/pulse/math/FixedIntervalIntegrator.java @@ -5,13 +5,10 @@ import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; /** * A fixed-interval integrator implements a numerical scheme in which the domain @@ -21,105 +18,101 @@ * such partitioning times the integration weights. * */ - public abstract class FixedIntervalIntegrator extends AbstractIntegrator { - private int integrationSegments; - - /** - * Creates a {@code FixedIntervalIntegrator} with the specified integration - * bounds and a default number of integration segments. - * - * @param bounds the integration bounds - */ - - public FixedIntervalIntegrator(Segment bounds) { - this(bounds, def(INTEGRATION_SEGMENTS)); - } - - /** - * Creates a {@code FixedIntervalIntegrator} with the specified integration - * bounds number of integration segments. - * - * @param bounds the integration bounds - * @param segments number of integration segments - */ - - public FixedIntervalIntegrator(Segment bounds, NumericProperty segments) { - super(bounds); - setIntegrationSegments(segments); - } - - /** - * Retrieves the number of integration segments. - * - * @return the number of integration segments. - */ - - public NumericProperty getIntegrationSegments() { - return derive(INTEGRATION_SEGMENTS, integrationSegments); - } - - /** - * Sets the number of integration segments and re-evaluates the integration step - * size. - * - * @param integrationSegments a property of the {@code INTEGRATION_SEGMENTS} - * type - */ - - public void setIntegrationSegments(NumericProperty integrationSegments) { - requireType(integrationSegments, INTEGRATION_SEGMENTS); - this.integrationSegments = (int) integrationSegments.getValue(); - } - - /** - * Sets the bounds to the argument and re-evaluates the integration step size. - * - * @param bounds the integration bounds - */ - - @Override - public void setBounds(Segment bounds) { - super.setBounds(bounds); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == INTEGRATION_SEGMENTS) { - setIntegrationSegments(property); - firePropertyChanged(this, property); - } - } - - /** - * The listed property is {@code INTEGRATION_SEGMENTS}. - */ - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(def(INTEGRATION_SEGMENTS))); - } - - /** - * Retrieves the step size equal to the integration range length divided by the - * number of integration segments. - * - * @return the integration step size. - */ - - public double stepSize() { - return getBounds().length() / (double) this.integrationSegments; - } - - @Override - public String toString() { - return getClass().getSimpleName() + " ; " + getIntegrationSegments(); - } - - @Override - public String getPrefix() { - return "Fixed step integrator"; - } - -} \ No newline at end of file + private static final long serialVersionUID = -5304597610450009326L; + private int integrationSegments; + + /** + * Creates a {@code FixedIntervalIntegrator} with the specified integration + * bounds and a default number of integration segments. + * + * @param bounds the integration bounds + */ + public FixedIntervalIntegrator(Segment bounds) { + this(bounds, def(INTEGRATION_SEGMENTS)); + } + + /** + * Creates a {@code FixedIntervalIntegrator} with the specified integration + * bounds number of integration segments. + * + * @param bounds the integration bounds + * @param segments number of integration segments + */ + public FixedIntervalIntegrator(Segment bounds, NumericProperty segments) { + super(bounds); + setIntegrationSegments(segments); + } + + /** + * Retrieves the number of integration segments. + * + * @return the number of integration segments. + */ + public NumericProperty getIntegrationSegments() { + return derive(INTEGRATION_SEGMENTS, integrationSegments); + } + + /** + * Sets the number of integration segments and re-evaluates the integration + * step size. + * + * @param integrationSegments a property of the {@code INTEGRATION_SEGMENTS} + * type + */ + public final void setIntegrationSegments(NumericProperty integrationSegments) { + requireType(integrationSegments, INTEGRATION_SEGMENTS); + this.integrationSegments = (int) integrationSegments.getValue(); + } + + /** + * Sets the bounds to the argument and re-evaluates the integration step + * size. + * + * @param bounds the integration bounds + */ + @Override + public final void setBounds(Segment bounds) { + super.setBounds(bounds); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == INTEGRATION_SEGMENTS) { + setIntegrationSegments(property); + firePropertyChanged(this, property); + } + } + + /** + * The listed property is {@code INTEGRATION_SEGMENTS}. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(INTEGRATION_SEGMENTS); + return set; + } + + /** + * Retrieves the step size equal to the integration range length divided by + * the number of integration segments. + * + * @return the integration step size. + */ + public final double stepSize() { + return getBounds().length() / (double) this.integrationSegments; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " ; " + getIntegrationSegments(); + } + + @Override + public String getPrefix() { + return "Fixed step integrator"; + } + +} diff --git a/src/main/java/pulse/math/FunctionWithInterpolation.java b/src/main/java/pulse/math/FunctionWithInterpolation.java index f720e7a1..41971b76 100644 --- a/src/main/java/pulse/math/FunctionWithInterpolation.java +++ b/src/main/java/pulse/math/FunctionWithInterpolation.java @@ -1,122 +1,146 @@ 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 { - - private Segment tBounds; - private int lookupTableSize; - private UnivariateFunction interpolation; - - public final static int NUM_PARTITIONS = 8192; - - /** - * Constructs a {@code FunctionWithInterpolation} by tabulating the function - * values within the {@code parameterBounds} at discrete nodes, the number of - * which is given by the second argument. After having done this, creates a - * {@code SplineInterpolation}, which can be invoked in future to calculate - * intermediate function values without loss of accuracy. - * - * @param parameterBounds the calculation bounds. - * @param lookupTableSize a size of the table for discrete function - * calculations. - */ - - public FunctionWithInterpolation(Segment parameterBounds, int lookupTableSize) { - this.tBounds = parameterBounds; - this.lookupTableSize = lookupTableSize; - init(); - } - - /** - * Creates a {@code FunctionWithInterpolation} using the {@code parameterBounds} - * and a default number of points {@value NUM_PARTITIONS}. - * - * @param parameterBounds the calculation bounds. - */ - - public FunctionWithInterpolation(Segment parameterBounds) { - this(parameterBounds, NUM_PARTITIONS); - } - - /** - * Performs the calculation at {@code t}. - * - * @param t the value of the independent variable - * @return the function value at {@code t} - */ - - public abstract double evaluate(double t); - - /** - * Uses the stored interpolation function to calculate values at any {@code t} - * within the parameter bounds. Note: If {@code t} is not contained - * within the parameter bounds, the method will return 0.0. - * - * @param t the value of the independent variable - * @return will return the interpolated value or 0.0 - */ - - public double valueAt(double t) { - return tBounds.contains(t) ? interpolation.value(t) : 0.0; - } - - /** - * Retrieves the parameter bounds. - * - * @return the parameter bounds. - */ - - public Segment getParameterBounds() { - return tBounds; - } - - /** - * Sets the parameter bounds to {@code parameterBounds}. The interpolation will - * then be re-calculated. - * - * @param parameterBounds the new parameter bounds. - */ - - public void setParameterBounds(Segment parameterBounds) { - this.tBounds = parameterBounds; - init(); - } - - private void init() { - var lookupTable = generateTable(); - interpolate(lookupTable); - } - - private double[] generateTable() { - var lookupTable = new double[lookupTableSize]; - final double delta = tBounds.length() / (lookupTableSize - 1); - - double t = tBounds.getMinimum(); - for (int i = 0; i < lookupTableSize; i++) { - lookupTable[i] = evaluate(t); - t += delta; - } - - return lookupTable; - } - - private void interpolate(double[] lookupTable) { - var tArray = new double[lookupTableSize]; - final double delta = tBounds.length() / (lookupTableSize - 1); - - for (int i = 0; i < tArray.length; i++) - tArray[i] = tBounds.getMinimum() + i * delta; - - var splineInterpolation = new SplineInterpolator(); - interpolation = splineInterpolation.interpolate(tArray, lookupTable); - } - -} \ No newline at end of file +public abstract class FunctionWithInterpolation implements Serializable { + + private static final long serialVersionUID = -303222542756574714L; + private Segment tBounds; + private int lookupTableSize; + private transient UnivariateFunction interpolation; + + public final static int NUM_PARTITIONS = 8192; + + /** + * Constructs a {@code FunctionWithInterpolation} by tabulating the function + * values within the {@code parameterBounds} at discrete nodes, the number + * of which is given by the second argument. After having done this, creates + * a {@code SplineInterpolation}, which can be invoked in future to + * calculate intermediate function values without loss of accuracy. + * + * @param parameterBounds the calculation bounds. + * @param lookupTableSize a size of the table for discrete function + * calculations. + */ + public FunctionWithInterpolation(Segment parameterBounds, int lookupTableSize) { + this.tBounds = parameterBounds; + this.lookupTableSize = lookupTableSize; + init(); + } + + /** + * Creates a {@code FunctionWithInterpolation} using the + * {@code parameterBounds} and a default number of points + * {@value NUM_PARTITIONS}. + * + * @param parameterBounds the calculation bounds. + */ + public FunctionWithInterpolation(Segment parameterBounds) { + this(parameterBounds, NUM_PARTITIONS); + } + + /** + * Performs the calculation at {@code t}. + * + * @param t the value of the independent variable + * @return the function value at {@code t} + */ + public abstract double evaluate(double t); + + /** + * Uses the stored interpolation function to calculate values at any + * {@code t} within the parameter bounds. Note: If {@code t} is not + * contained within the parameter bounds, the method will return 0.0. + * + * @param t the value of the independent variable + * @return will return the interpolated value or 0.0 + */ + public double valueAt(double t) { + return tBounds.contains(t) ? interpolation.value(t) : 0.0; + } + + /** + * Retrieves the parameter bounds. + * + * @return the parameter bounds. + */ + public Segment getParameterBounds() { + return tBounds; + } + + /** + * Sets the parameter bounds to {@code parameterBounds}. The interpolation + * will then be re-calculated. + * + * @param parameterBounds the new parameter bounds. + */ + public void setParameterBounds(Segment parameterBounds) { + this.tBounds = parameterBounds; + init(); + } + + private void init() { + var lookupTable = generateTable(); + interpolate(lookupTable); + } + + private double[] generateTable() { + var lookupTable = new double[lookupTableSize]; + final double delta = tBounds.length() / (lookupTableSize - 1); + + double t = tBounds.getMinimum(); + for (int i = 0; i < lookupTableSize; i++) { + lookupTable[i] = evaluate(t); + t += delta; + } + + return lookupTable; + } + + private void interpolate(double[] lookupTable) { + var tArray = new double[lookupTableSize]; + final double delta = tBounds.length() / (lookupTableSize - 1); + + for (int i = 0; i < tArray.length; i++) { + tArray[i] = tBounds.getMinimum() + i * delta; + } + + var splineInterpolation = new SplineInterpolator(); + 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 new file mode 100644 index 00000000..b5207f32 --- /dev/null +++ b/src/main/java/pulse/math/Harmonic.java @@ -0,0 +1,268 @@ +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 static final long serialVersionUID = 3732379391172485157L; + + 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/IndexedVector.java b/src/main/java/pulse/math/IndexedVector.java deleted file mode 100644 index d77675e7..00000000 --- a/src/main/java/pulse/math/IndexedVector.java +++ /dev/null @@ -1,108 +0,0 @@ -package pulse.math; - -import java.util.ArrayList; -import java.util.List; - -import pulse.math.linear.Vector; -import pulse.properties.NumericPropertyKeyword; - -/** - * A wrapper subclass that assigns {@code NumericPropertyKeyword}s to specific - * components of the vector. Used when constructing the optimisation vector. - */ - -public class IndexedVector extends Vector { - - private List indices; - - /** - * Constructs an {@code IndexedVector} with the specified list of keywords. - * - * @param indices a list of keywords - */ - - public IndexedVector(List indices) { - this(indices.size()); - assign(indices); - } - - /** - * Constructs an {@code IndexedVector} based on {@code v} and a list of keyword - * {@code indices} - * - * @param v the vector to be copied - * @param indices a list of keyword - */ - - public IndexedVector(Vector v, List indices) { - super(v); - this.indices = indices; - } - - private IndexedVector(final int n) { - super(n); - indices = new ArrayList<>(n); - } - - /** - * Finds the component of this vector that corresponds to {@code index} and sets - * its value to {@code x} - * - * @param index the keyword associated with a component of this - * {@code IndexedVector} - * @param x the new value of this component - */ - - public void set(NumericPropertyKeyword index, final double x) { - final int i = indexOf(index); - super.set(i, x); - } - - /** - * 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.get(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 indices.indexOf(index); - } - - /** - * Gets the component at this {@code index} - * - * @param index a keyword-index of a component - * @return the respective component - */ - - public double get(NumericPropertyKeyword index) { - return super.get(indexOf(index)); - } - - /** - * Gets the full list of indices recognised by this {@code IndexedVector}. - * - * @return the full list of {@code NumericPropertyKeyword} indices. - */ - - public List getIndices() { - return indices; - } - - private void assign(List indices) { - this.indices.addAll(indices); - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/math/LegendrePoly.java b/src/main/java/pulse/math/LegendrePoly.java index dc611532..29a82bf1 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; @@ -16,157 +17,151 @@ * point, and the roots of the polynomial (with the help of a * {@code LaguerreSolver}. *

- * + * * @see org.apache.commons.math3.analysis.solvers.LaguerreSolver - * @see Wiki page + * @see Wiki + * page */ - -public class LegendrePoly { - - private double[] c; - private int n; - - private LaguerreSolver solver; - - /** - * Creates a Legendre polynomial of the order {@code n}. The coefficients of the - * polynomial are immediately calculated. - * - * @param n the order of the polynomial. - */ - - public LegendrePoly(final int n) { - this.n = n; - c = new double[n + 1]; - var solverError = (double) def(LAGUERRE_SOLVER_ERROR).getValue(); - solver = new LaguerreSolver(solverError); - coefficients(); - } - - /** - * Fast calculation of binomial coefficient Cmk = - * m!/(k!(m-k)!) - * - * @param m integer. - * @param k integer, k ≤ m. - * @return binomial coefficient Cmk. - */ - - public static long binomial(int m, int k) { - int k1 = m - k; - k = k > k1 ? k1 : k; - - long c = 1; - - // Calculate value of Binomial Coefficient in bottom up manner - for (int i = 1; i < k + 1; i++, m--) { - c = c / i * m + c % i * m / i; // split c * n / i into (c / i * i + c % i) * n / i - } - - return c; - - } - - /** - * Calculates the generalised binomial coefficient. - * - * @param k integer. - * @param alpha a double value - * @return the generalised binomial coefficient Cαk. - */ - - public static double generalisedBinomial(final double alpha, final int k) { - - double c = 1; - - for (int i = 0; i < k; i++) { - c *= (alpha - i) / (k - i); - } - - return c; - - } - - /** - * This will generate the coefficients for the Legendre polynomial, arranged in - * order of significance (from x0 to xn). The coeffients - * will then be stored in a double array for further use. - */ - - public void coefficients() { - - long intFactor = fastPowInt(2, n); - - for (int i = 0; i < c.length; i++) { - c[i] = intFactor * binomial(n, i) * generalisedBinomial((n + i - 1) * 0.5, n); - } - - } - - /** - * Calculates the derivative of this Legendre polynomial. The coefficients are - * assumed to be known. - * - * @param x a real value. - * @return the derivative at {@code x}. - */ - - public double derivative(final double x) { - double d = 0; - - for (int i = 1; i < c.length; i++) { - d += i * c[i] * fastPowLoop(x, i - 1); - } - - return d; - - } - - /** - * @return the order of this polynomial. - */ - - public int getOrder() { - return n; - } - - /** - * Calculates the value of this Legendre polynomial at {@code x} - * - * @param x a real value. - * @return the value of the Legendre polynomial at {@code x}. - */ - - public double poly(final double x) { - double poly = 0; - - for (int i = 0; i < c.length; i++) { - poly += c[i] * fastPowLoop(x, i); - } - - return poly; - - } - - /** - * Uses a {@code LaguerreSolver} to calculate the roots of this polynomial. All - * coefficients are assumed to be known. - * - * @return the real roots of the Legendre polynomial. - */ - - public double[] roots() { - var complexRoots = solver.solveAllComplex(c, 1.0); - var roots = new double[n]; - - // the last roots is always zero, so we have n non-zero roots in total - // in case of even n, the first n/2 roots are positive and the rest are negative - for (int i = 0; i < n; i++) { - roots[i] = complexRoots[i].getReal(); - } - - return roots; - - } - -} \ No newline at end of file +public class LegendrePoly implements Serializable { + + private static final long serialVersionUID = -6859690814783610846L; + private double[] c; + private int n; + + private LaguerreSolver solver; + + /** + * Creates a Legendre polynomial of the order {@code n}. The coefficients of + * the polynomial are immediately calculated. + * + * @param n the order of the polynomial. + */ + public LegendrePoly(final int n) { + this.n = n; + c = new double[n + 1]; + var solverError = (double) def(LAGUERRE_SOLVER_ERROR).getValue(); + solver = new LaguerreSolver(solverError); + coefficients(); + } + + /** + * Fast calculation of binomial coefficient Cmk = + * m!/(k!(m-k)!) + * + * @param m integer. + * @param k integer, k ≤ m. + * @return binomial coefficient Cmk. + */ + public static long binomial(int m, int k) { + int k1 = m - k; + k = k > k1 ? k1 : k; + + long c = 1; + + // Calculate value of Binomial Coefficient in bottom up manner + for (int i = 1; i < k + 1; i++, m--) { + c = c / i * m + c % i * m / i; // split c * n / i into (c / i * i + c % i) * n / i + } + + return c; + + } + + /** + * Calculates the generalised binomial coefficient. + * + * @param k integer. + * @param alpha a double value + * @return the generalised binomial coefficient + * Cαk. + */ + public static double generalisedBinomial(final double alpha, final int k) { + + double c = 1; + + for (int i = 0; i < k; i++) { + c *= (alpha - i) / (k - i); + } + + return c; + + } + + /** + * This will generate the coefficients for the Legendre polynomial, arranged + * in order of significance (from x0 to xn). The + * coeffients will then be stored in a double array for further use. + */ + public void coefficients() { + + long intFactor = fastPowInt(2, n); + + for (int i = 0; i < c.length; i++) { + c[i] = intFactor * binomial(n, i) * generalisedBinomial((n + i - 1) * 0.5, n); + } + + } + + /** + * Calculates the derivative of this Legendre polynomial. The coefficients + * are assumed to be known. + * + * @param x a real value. + * @return the derivative at {@code x}. + */ + public double derivative(final double x) { + double d = 0; + + for (int i = 1; i < c.length; i++) { + d += i * c[i] * fastPowLoop(x, i - 1); + } + + return d; + + } + + /** + * @return the order of this polynomial. + */ + public int getOrder() { + return n; + } + + /** + * Calculates the value of this Legendre polynomial at {@code x} + * + * @param x a real value. + * @return the value of the Legendre polynomial at {@code x}. + */ + public double poly(final double x) { + double poly = 0; + + for (int i = 0; i < c.length; i++) { + poly += c[i] * fastPowLoop(x, i); + } + + return poly; + + } + + /** + * Uses a {@code LaguerreSolver} to calculate the roots of this polynomial. + * All coefficients are assumed to be known. + * + * @return the real roots of the Legendre polynomial. + */ + public double[] roots() { + var complexRoots = solver.solveAllComplex(c, 1.0); + var roots = new double[n]; + + // the last roots is always zero, so we have n non-zero roots in total + // in case of even n, the first n/2 roots are positive and the rest are negative + for (int i = 0; i < n; i++) { + roots[i] = complexRoots[i].getReal(); + } + + return roots; + + } + +} diff --git a/src/main/java/pulse/math/MathUtils.java b/src/main/java/pulse/math/MathUtils.java index 65eec0cc..248f39d3 100644 --- a/src/main/java/pulse/math/MathUtils.java +++ b/src/main/java/pulse/math/MathUtils.java @@ -10,170 +10,161 @@ * https://martin.ankerl.com * */ - public class MathUtils { - public final static double EQUALS_TOLERANCE = 1E-5; - - private MathUtils() { - // intentionally blank - } - - /** - * Checks if two numbers are approximately equal by comparing the modulus of - * their difference to {@value EQUALS_TOLERANCE}. - * - * @param a a number - * @param b another number - * @return {@code true} if numbers are approximately equal, {@code false} - * otherwise - */ - - public static boolean approximatelyEquals(final double a, final double b) { - return Math.abs(a - b) < EQUALS_TOLERANCE; - } - - /** - * A method for the approximate calculation of the hyperbolic tangent. - * - * @param a the argument of {@code tanh(a)}. - * @return the approximate {@code tanh(a)} value. - * @see MathUtils.fastExp(double) - */ - - public static double fastTanh(final double a) { - final double e2x = fastExp(2.0 * a); - return (e2x - 1.0) / (e2x + 1.0); - } - - /** - * Calculate the hyperbolic arctangent (exact). - * - * @param a the argument of {@code atanh(a)} - * @return the exact value of the {@code atanh(a)}. - */ - - public static double atanh(final double a) { - return 0.5 * log((1 + a) / (1 - a)); - } - - /** - * Rapid calculation of {@code b}-th power of {@code a}, which is simply a - * repeated multiplication, in case of positive {@code b}, or a repeated - * division. - * - * @param a the base . - * @param b the exponent. - * @return the exponentiation result. - */ - - public static double fastPowLoop(final double a, final int b) { - double re = 1; - // if positive (i < b is never satisfied for negative b's) - for (int i = 0; i < b; i++) { - re *= a; - } - // if negative - for (int i = 0; i < -b; i++) { - re /= a; - } - return re; - } - - /** - * Rapid calculation of (-1)n using a ternary conditional operator. - * - * @param n a positive integer number - * @return the result of exponentiation. - */ - - public static int fastPowSgn(int n) { - return n % 2 == 0 ? 1 : -1; - } - - /** - * Rapid exponentiation where the base and the exponent are integer value. Uses - * bitwise shiftiing. - * - * @param x the base - * @param y the exponent - * @return result of the exponentiation - */ - - public static long fastPowInt(long x, int y) { - long result = 1; - while (y > 0) { - if ((y & 1) == 0) { - x *= x; - y >>>= 1; - } else { - result *= x; - y--; - } - } - return result; - } - - /** - * Approximate calculation of {@code exp(val)}. - * - * @param val the argument of the exponent. - * @return the result. - */ - - public static double fastExp(double val) { - final long tmp = (long) (1512775 * val + 1072632447); - return longBitsToDouble(tmp << 32); - } - - /** - * A highly-approximate calculation of {@code ln(val)}. - * - * @param val the argument of the natural logarithm. - * @return the result. - */ - - public static double fastLog(double val) { - final double x = (doubleToLongBits(val) >> 32); - return (x - 1072632447) / 1512775; - } - - /** - * Approximate calculation of ab. - * - * @param a the base (real) - * @param b the exponent (real) - * @return the result of calculation - */ - - public static double fastPowGeneral(final double a, final double b) { - final long tmp = doubleToLongBits(a); - final long tmp2 = (long) (b * (tmp - 4606921280493453312L)) + 4606921280493453312L; - return longBitsToDouble(tmp2); - } - - /** - * A fast bitwise calculation of the absolute value. Arguably faster than - * {@code Math.abs}. - * - * @param a the argument - * @return the calculation result - */ - - public static double fastAbs(final double a) { - return longBitsToDouble((doubleToLongBits(a) << 1) >>> 1); - } - - /** - * Performs linear extrapolation according to the rule y(xStar) = y1 + (xStar - x1)/(x2 - x1) * (y2 - y1) - * @param p1 point 1 (x1, y1) - * @param p2 point 2 (x2, y2) - * @param xStar interpolation x coordinate - * @return - */ - - public static double linearExtrapolation(final double[] p1, final double[] p2, final double xStar) { - return p1[1] + (xStar - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]); - } - -} \ No newline at end of file + public final static double EQUALS_TOLERANCE = 1E-5; + + private MathUtils() { + // intentionally blank + } + + /** + * Checks if two numbers are approximately equal by comparing the modulus of + * their difference to {@value EQUALS_TOLERANCE}. + * + * @param a a number + * @param b another number + * @return {@code true} if numbers are approximately equal, {@code false} + * otherwise + */ + public static boolean approximatelyEquals(final double a, final double b) { + return Math.abs(a - b) < EQUALS_TOLERANCE; + } + + /** + * A method for the approximate calculation of the hyperbolic tangent. + * + * @param a the argument of {@code tanh(a)}. + * @return the approximate {@code tanh(a)} value. + * @see MathUtils.fastExp(double) + */ + public static double fastTanh(final double a) { + final double e2x = fastExp(2.0 * a); + return (e2x - 1.0) / (e2x + 1.0); + } + + /** + * Calculate the hyperbolic arctangent (exact). + * + * @param a the argument of {@code atanh(a)} + * @return the exact value of the {@code atanh(a)}. + */ + public static double atanh(final double a) { + return 0.5 * log((1 + a) / (1 - a)); + } + + /** + * Rapid calculation of {@code b}-th power of {@code a}, which is simply a + * repeated multiplication, in case of positive {@code b}, or a repeated + * division. + * + * @param a the base . + * @param b the exponent. + * @return the exponentiation result. + */ + public static double fastPowLoop(final double a, final int b) { + double re = 1; + // if positive (i < b is never satisfied for negative b's) + for (int i = 0; i < b; i++) { + re *= a; + } + // if negative + for (int i = 0; i < -b; i++) { + re /= a; + } + return re; + } + + /** + * Rapid calculation of (-1)n using a ternary conditional + * operator. + * + * @param n a positive integer number + * @return the result of exponentiation. + */ + public static int fastPowSgn(int n) { + return n % 2 == 0 ? 1 : -1; + } + + /** + * Rapid exponentiation where the base and the exponent are integer value. + * Uses bitwise shiftiing. + * + * @param x the base + * @param y the exponent + * @return result of the exponentiation + */ + public static long fastPowInt(long x, int y) { + long result = 1; + while (y > 0) { + if ((y & 1) == 0) { + x *= x; + y >>>= 1; + } else { + result *= x; + y--; + } + } + return result; + } + + /** + * Approximate calculation of {@code exp(val)}. + * + * @param val the argument of the exponent. + * @return the result. + */ + public static double fastExp(double val) { + final long tmp = (long) (1512775 * val + 1072632447); + return longBitsToDouble(tmp << 32); + } + + /** + * A highly-approximate calculation of {@code ln(val)}. + * + * @param val the argument of the natural logarithm. + * @return the result. + */ + public static double fastLog(double val) { + final double x = (doubleToLongBits(val) >> 32); + return (x - 1072632447) / 1512775; + } + + /** + * Approximate calculation of ab. + * + * @param a the base (real) + * @param b the exponent (real) + * @return the result of calculation + */ + public static double fastPowGeneral(final double a, final double b) { + final long tmp = doubleToLongBits(a); + final long tmp2 = (long) (b * (tmp - 4606921280493453312L)) + 4606921280493453312L; + return longBitsToDouble(tmp2); + } + + /** + * A fast bitwise calculation of the absolute value. Arguably faster than + * {@code Math.abs}. + * + * @param a the argument + * @return the calculation result + */ + public static double fastAbs(final double a) { + return longBitsToDouble((doubleToLongBits(a) << 1) >>> 1); + } + + /** + * Performs linear extrapolation according to the rule y(xStar) = y1 + + * (xStar - x1)/(x2 - x1) * (y2 - y1) + * + * @param p1 point 1 (x1, y1) + * @param p2 point 2 (x2, y2) + * @param xStar interpolation x coordinate + * @return + */ + public static double linearExtrapolation(final double[] p1, final double[] p2, final double xStar) { + return p1[1] + (xStar - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]); + } + +} diff --git a/src/main/java/pulse/math/MidpointIntegrator.java b/src/main/java/pulse/math/MidpointIntegrator.java index 1a4f0579..6109de57 100644 --- a/src/main/java/pulse/math/MidpointIntegrator.java +++ b/src/main/java/pulse/math/MidpointIntegrator.java @@ -5,39 +5,39 @@ /** * Implements the midpoint integration scheme for the evaluation of definite * integrals. - * + * * @see Wiki page * */ - public abstract class MidpointIntegrator extends FixedIntervalIntegrator { - public MidpointIntegrator(Segment bounds, NumericProperty segments) { - super(bounds, segments); - } + private static final long serialVersionUID = -5434607461290096748L; - public MidpointIntegrator(Segment bounds) { - super(bounds); - } + public MidpointIntegrator(Segment bounds, NumericProperty segments) { + super(bounds, segments); + } - /** - * Performs the integration according to the midpoint scheme. This scheme should - * be used when the function is not well-defined at either of the integration - * bounds. - */ + public MidpointIntegrator(Segment bounds) { + super(bounds); + } - @Override - public double integrate() { - final double a = getBounds().getMinimum(); + /** + * Performs the integration according to the midpoint scheme. This scheme + * should be used when the function is not well-defined at either of the + * integration bounds. + */ + @Override + public double integrate() { + final double a = getBounds().getMinimum(); - final int points = (int) getIntegrationSegments().getValue(); + final int points = (int) getIntegrationSegments().getValue(); - double sum = 0; - double h = stepSize(); - for (int i = 0; i < points; i++) { - sum += integrand(a + (i + 0.5) * h) * h; - } - return sum; - } + double sum = 0; + double h = stepSize(); + for (int i = 0; i < points; i++) { + sum += integrand(a + (i + 0.5) * h) * h; + } + return sum; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/Parameter.java b/src/main/java/pulse/math/Parameter.java new file mode 100644 index 00000000..b46e02ee --- /dev/null +++ b/src/main/java/pulse/math/Parameter.java @@ -0,0 +1,93 @@ +package pulse.math; + +import java.io.Serializable; +import pulse.math.transforms.Transformable; + +/** + * Parameter class + */ +public class Parameter implements Serializable { + + private static final long serialVersionUID = 3222166682943107207L; + 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 00000000..31a66f7f --- /dev/null +++ b/src/main/java/pulse/math/ParameterIdentifier.java @@ -0,0 +1,68 @@ +package pulse.math; + +import java.io.Serializable; +import java.util.Objects; +import pulse.properties.NumericPropertyKeyword; + +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); + } + + @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; + } + + public NumericPropertyKeyword getKeyword() { + return keyword; + } + + public int getIndex() { + return index; + } + + @Override + public boolean equals(Object id) { + if (id.getClass() == null) { + return false; + } + + var classA = id.getClass(); + var classB = this.getClass(); + + 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) { + sb.append(" # ").append(index); + } + return sb.toString(); + } + +} diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java new file mode 100644 index 00000000..f9d27fba --- /dev/null +++ b/src/main/java/pulse/math/ParameterVector.java @@ -0,0 +1,131 @@ +package pulse.math; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import pulse.math.linear.Vector; +import pulse.properties.NumericProperties; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; + +/** + * A wrapper subclass that assigns {@code ParameterIdentifier}s to specific + * components of the vector. Used when constructing the optimisation vector. + */ +public class ParameterVector implements Serializable { + + private static final long serialVersionUID = -4678286597080149891L; + private final List params; + + /** + * Constructs an {@code IndexedVector} with the specified list of keywords. + * + * @param indices a list of keywords + */ + 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 + */ + public ParameterVector(ParameterVector proto, Vector v) { + 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 + } + } + + /** + * Copy constructor + * + * @param v another vector + */ + public ParameterVector(ParameterVector v) { + params = new ArrayList<>(v.params); + for (Parameter p : params) { + p.setValue(p.getApparentValue(), true); + } + } + + public void add(Parameter p) { + params.add(p); + } + + 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 : 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(); + + 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()); + } + + 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 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 8a89042b..95e6c232 100644 --- a/src/main/java/pulse/math/Segment.java +++ b/src/main/java/pulse/math/Segment.java @@ -1,144 +1,154 @@ package pulse.math; +import java.io.Serializable; import java.util.Random; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericProperties.def; /** * A {@code Segment} is simply a pair of values {@code a} and {@code b} such * that {@code a < b}. * */ - -public class Segment { - private double a; - private double b; - - /** - * Creates a {@code Segment} bounded by {@code a} and {@code b}. - * - * @param a any value - * @param b either {@code b > a} or {@code b < a} - */ - - public Segment(double a, double b) { - this.a = a < b ? a : b; - this.b = b > a ? b : a; - } - - /** - * Copies {@code segment} - * - * @param segment a {@code Segment} - */ - - public Segment(Segment segment) { - this.a = segment.a; - this.b = segment.b; - } - - /** - * Gets the {@code a} value for this {@code Segment} - * - * @return the lower end of this {@code Segment} - */ - - public double getMinimum() { - return a; - } - - /** - * Gets the {@code b} value for this {@code Segment} - * - * @return the upper end of this {@code Segment} - */ - - public double getMaximum() { - return b; - } - - /** - * Calculates the length {@code (b - a)} - * - * @return the length value - */ - - public double length() { - return (b - a); - } - - /** - * Calculates the squared length - * - * @return the squared length value - * @see length() - */ - - public double lengthSq() { - return Math.pow(b - a, 2); - } - - /** - * Sets the minimum value to {@code a}. Note it does not prevent against: - * {@code a >= max}. - * - * @param a a value, which should satisfy {@code a < max} - */ - - public void setMinimum(double a) { - this.a = a; - } - - /** - * Sets the maximum value to {@code b}. Note it does not prevent against: - * {@code b <= min}. - * - * @param b a value, which should satisfy {@code b > min} - */ - - public void setMaximum(double b) { - this.b = b; - } - - /** - * Calculates the middle point of this {@code Segment}. - * - * @return the mean - */ - - public double mean() { - return (a + b) * 0.5; - } - - /** - * Calculates a random value confined in the interval between the two ends of - * this {@code Segment}. - * - * @return a confined random value. - */ - - public double randomValue() { - return (new Random()).nextDouble() * length() + getMinimum(); - } - - /** - * Checks whether {@code x} is contained inside this {@code Segment}. - * - * @param x a value. - * @return {@code true} if min ≤ x ≤ max. - */ - - public boolean contains(double x) { - return x >= a ? (x <= b ? true : false) : false; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("["); - sb.append(a); - sb.append(" ; "); - sb.append(b); - sb.append("]"); - return sb.toString(); - } - -} \ No newline at end of file +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); + + /** + * Creates a {@code Segment} bounded by {@code a} and {@code b}. + * + * @param a any value + * @param b either {@code b > a} or {@code b < a} + */ + public Segment(double a, double b) { + this.a = a < b ? a : b; + this.b = b > a ? b : a; + } + + /** + * Copies {@code segment} + * + * @param segment a {@code Segment} + */ + 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 + * + * @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()); + } + + /** + * Gets the {@code a} value for this {@code Segment} + * + * @return the lower end of this {@code Segment} + */ + public double getMinimum() { + return a; + } + + /** + * Gets the {@code b} value for this {@code Segment} + * + * @return the upper end of this {@code Segment} + */ + public double getMaximum() { + return b; + } + + /** + * Calculates the length {@code (b - a)} + * + * @return the length value + */ + public double length() { + return (b - a); + } + + /** + * Calculates the squared length + * + * @return the squared length value + * @see length() + */ + public double lengthSq() { + return Math.pow(b - a, 2); + } + + /** + * Sets the minimum value to {@code a}. Note it does not prevent against: + * {@code a >= max}. + * + * @param a a value, which should satisfy {@code a < max} + */ + public void setMinimum(double a) { + this.a = a; + } + + /** + * Sets the maximum value to {@code b}. Note it does not prevent against: + * {@code b <= min}. + * + * @param b a value, which should satisfy {@code b > min} + */ + public void setMaximum(double b) { + this.b = b; + } + + /** + * Calculates the middle point of this {@code Segment}. + * + * @return the mean + */ + public double mean() { + return (a + b) * 0.5; + } + + /** + * Calculates a random value confined in the interval between the two ends + * of this {@code Segment}. + * + * @return a confined random value. + */ + public double randomValue() { + return (new Random()).nextDouble() * length() + getMinimum(); + } + + /** + * Checks whether {@code x} is contained inside this {@code Segment}. + * + * @param x a value. + * @return {@code true} if min ≤ x ≤ max. + */ + public boolean contains(double x) { + return x >= a ? (x <= b ? true : false) : false; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + sb.append(a); + sb.append(" ; "); + sb.append(b); + sb.append("]"); + return sb.toString(); + } + +} diff --git a/src/main/java/pulse/math/SimpsonIntegrator.java b/src/main/java/pulse/math/SimpsonIntegrator.java index 04db7241..8a367d6c 100644 --- a/src/main/java/pulse/math/SimpsonIntegrator.java +++ b/src/main/java/pulse/math/SimpsonIntegrator.java @@ -5,53 +5,53 @@ /** * Implements the Simpson's integration rule for the evaluation of definite * integrals. - * + * * @see Wiki page * */ - public abstract class SimpsonIntegrator extends FixedIntervalIntegrator { - public SimpsonIntegrator(Segment bounds) { - super(bounds); - } + private static final long serialVersionUID = -7800272372472765906L; - public SimpsonIntegrator(Segment bounds, NumericProperty segments) { - super(bounds, segments); - } + public SimpsonIntegrator(Segment bounds) { + super(bounds); + } - /** - * Performs the integration with the Simpson's rule. Based on: - * https://introcs.cs.princeton.edu/java/93integration/SimpsonsRule.java.html - */ + public SimpsonIntegrator(Segment bounds, NumericProperty segments) { + super(bounds, segments); + } - @Override - public double integrate() { - double rmin = getBounds().getMinimum(); - double rmax = getBounds().getMaximum(); + /** + * Performs the integration with the Simpson's rule. Based on: + * https://introcs.cs.princeton.edu/java/93integration/SimpsonsRule.java.html + */ + @Override + public double integrate() { + double rmin = getBounds().getMinimum(); + double rmax = getBounds().getMaximum(); - // 1/3 terms - double sum = integrand(rmin) + integrand(rmax); + // 1/3 terms + double sum = integrand(rmin) + integrand(rmax); - double x = 0; - double y = 0; + double x = 0; + double y = 0; - int integrationSegments = (int) getIntegrationSegments().getValue(); - double h = stepSize(); + int integrationSegments = (int) getIntegrationSegments().getValue(); + double h = stepSize(); - // 4/3 terms - for (int i = 1; i < integrationSegments; i += 2) { - x += integrand(rmin + h * i); - } + // 4/3 terms + for (int i = 1; i < integrationSegments; i += 2) { + x += integrand(rmin + h * i); + } - // 2/3 terms - for (int i = 2; i < integrationSegments; i += 2) { - y += integrand(rmin + h * i); - } + // 2/3 terms + for (int i = 2; i < integrationSegments; i += 2) { + y += integrand(rmin + h * i); + } - sum += x * 4.0 + y * 2.0; + sum += x * 4.0 + y * 2.0; - return sum * h / 3.0; - } + return sum * h / 3.0; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/Window.java b/src/main/java/pulse/math/Window.java new file mode 100644 index 00000000..a230ccb8 --- /dev/null +++ b/src/main/java/pulse/math/Window.java @@ -0,0 +1,58 @@ +package pulse.math; + +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 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); + +} diff --git a/src/main/java/pulse/math/ZScore.java b/src/main/java/pulse/math/ZScore.java new file mode 100644 index 00000000..83be83a8 --- /dev/null +++ b/src/main/java/pulse/math/ZScore.java @@ -0,0 +1,132 @@ +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 = 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)); + + } else { + + signals[i] = 0; + filteredY.set(i, input[i]); + + } + + // Update rolling average and deviation + var slidingWindow = new ArrayList<>(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]); + } + + } + */ +} 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 00000000..6020aa6f --- /dev/null +++ b/src/main/java/pulse/math/filters/AssignmentListener.java @@ -0,0 +1,9 @@ +package pulse.math.filters; + +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 new file mode 100644 index 00000000..0bba2aeb --- /dev/null +++ b/src/main/java/pulse/math/filters/Filter.java @@ -0,0 +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 extends Serializable { + + public List process(List input); + + public default List process(DiscreteInput input) { + return process(DiscreteInput.convert(input.getX(), input.getY())); + } + +} 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 00000000..7f98f3e4 --- /dev/null +++ b/src/main/java/pulse/math/filters/HalfTimeCalculator.java @@ -0,0 +1,96 @@ +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; +import java.util.stream.Collectors; +import pulse.DiscreteInput; +import pulse.baseline.FlatBaseline; +import pulse.input.IndexRange; + +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())); + + 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; + } + +} 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 00000000..4356b4f5 --- /dev/null +++ b/src/main/java/pulse/math/filters/OptimisablePolyline.java @@ -0,0 +1,63 @@ +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 static final long serialVersionUID = 418264754603533971L; + private final double[] x; + private final double[] y; + private 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); + } + +} 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 00000000..0f9ce5ad --- /dev/null +++ b/src/main/java/pulse/math/filters/OptimisedRunningAverage.java @@ -0,0 +1,28 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.List; +import pulse.DiscreteInput; + +public class OptimisedRunningAverage extends RunningAverage { + + private static final long serialVersionUID = 1272276960302188392L; + + 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(); + } + +} 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 00000000..c2f7ffb5 --- /dev/null +++ b/src/main/java/pulse/math/filters/PolylineOptimiser.java @@ -0,0 +1,91 @@ +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 static final long serialVersionUID = -9056678836812293655L; + 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 00000000..cfd6d714 --- /dev/null +++ b/src/main/java/pulse/math/filters/Randomiser.java @@ -0,0 +1,27 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +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 + ); + return input; + } + + public double getAmplitude() { + return amplitude; + } + +} 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 00000000..2a2b82b9 --- /dev/null +++ b/src/main/java/pulse/math/filters/RunningAverage.java @@ -0,0 +1,128 @@ +package pulse.math.filters; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; + +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; + } + + /** + * 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; + } + +} diff --git a/src/main/java/pulse/math/linear/ArithmeticOperation.java b/src/main/java/pulse/math/linear/ArithmeticOperation.java index bc7112f7..10b60c15 100644 --- a/src/main/java/pulse/math/linear/ArithmeticOperation.java +++ b/src/main/java/pulse/math/linear/ArithmeticOperation.java @@ -4,16 +4,15 @@ * A basic wrapper interface for binary arithmetic operations. * */ - interface ArithmeticOperation { - /** - * Calculates the result of the binary operation. - * @param x a number - * @param y another number - * @return the result of arithmetic operation - */ - - double evaluate(double x, double y); - -} \ No newline at end of file + /** + * Calculates the result of the binary operation. + * + * @param x a number + * @param y another number + * @return the result of arithmetic operation + */ + double evaluate(double x, double y); + +} diff --git a/src/main/java/pulse/math/linear/ArithmeticOperations.java b/src/main/java/pulse/math/linear/ArithmeticOperations.java index a5d4d0d1..bc11a0aa 100644 --- a/src/main/java/pulse/math/linear/ArithmeticOperations.java +++ b/src/main/java/pulse/math/linear/ArithmeticOperations.java @@ -6,41 +6,35 @@ * Manages available arithmetic operations for use within the package. * */ - class ArithmeticOperations { - /** - * Sum of two numbers. - */ - - public final static ArithmeticOperation SUM = (x, y) -> x + y; - - /** - * Difference of two numbers. - */ - - public final static ArithmeticOperation DIFFERENCE = (x, y) -> x - y; - - /** - * Product of two numbers. - */ - - public final static ArithmeticOperation PRODUCT = (x, y) -> x * y; - - /** - * The result of division of one number by another number. - */ - - public final static ArithmeticOperation DIVISION = (x, y) -> x - y; - - /** - * The squared difference of two numbers. - */ - - public final static ArithmeticOperation DIFF_SQUARED = (x, y) -> fastPowLoop(x*x - y*y, 2); - - private ArithmeticOperations() { - //intentionally blank - } - -} \ No newline at end of file + /** + * Sum of two numbers. + */ + public final static ArithmeticOperation SUM = (x, y) -> x + y; + + /** + * Difference of two numbers. + */ + public final static ArithmeticOperation DIFFERENCE = (x, y) -> x - y; + + /** + * Product of two numbers. + */ + public final static ArithmeticOperation PRODUCT = (x, y) -> x * y; + + /** + * The result of division of one number by another number. + */ + public final static ArithmeticOperation DIVISION = (x, y) -> x - y; + + /** + * The squared difference of two numbers. + */ + public final static ArithmeticOperation DIFF_SQUARED = (x, y) -> fastPowLoop(x * x - y * y, 2); + + private ArithmeticOperations() { + //intentionally blank + } + +} diff --git a/src/main/java/pulse/math/linear/Matrices.java b/src/main/java/pulse/math/linear/Matrices.java index 13be56ae..30b69c53 100644 --- a/src/main/java/pulse/math/linear/Matrices.java +++ b/src/main/java/pulse/math/linear/Matrices.java @@ -1,61 +1,67 @@ package pulse.math.linear; /** - * A static factory class used to create matrices. The factory methods - * are invoked by classes outside this package instead of the constructors, which are package-private. + * A static factory class used to create matrices. The factory methods are + * invoked by classes outside this package instead of the constructors, which + * are package-private. * */ - public class Matrices { - private Matrices() { - // intentionally blank - } - - /** - * Creates a square matrix out of {@code data}. Depending on the data dimensions, - * this will either create a general-form {@code SquareMatrix} or one of the subclasses: - * {@code Matrix2}, {@code Matrix3} or {@code Matrix4}. - * @param data the input data - * @return a {@code} SquareMatrix instance or one of its subclasses - */ - - public static SquareMatrix createMatrix(double[][] data) { - int m = data.length; - - SquareMatrix result; - - switch (m) { - case 2: - result = new Matrix2(data); - break; - case 3: - result = new Matrix3(data); - break; - case 4: - result = new Matrix4(data); - break; - default: - result = new SquareMatrix(data); - } - - return result; - - } - - /** - * Creates an identity matrix with its dimension equal to the argument - * @param dimension the dimension - * @return an identity matrix of the given dimension - */ - - public static SquareMatrix createIdentityMatrix(int dimension) { - var data = new double[dimension][dimension]; - - for(int i = 0; i < dimension; i++) - data[i][i] = 1.0; - - return createMatrix(data); - } - -} \ No newline at end of file + private Matrices() { + // intentionally blank + } + + /** + * Creates a square matrix out of {@code data}. Depending on the data + * dimensions, this will either create a general-form {@code SquareMatrix} + * or one of the subclasses: {@code Matrix2}, {@code Matrix3} or + * {@code Matrix4}. + * + * @param data the input data + * @return a {@code} SquareMatrix instance or one of its subclasses + */ + public static RectangularMatrix createMatrix(double[][] data) { + return data.length == data[0].length ? createSquareMatrix(data) : new RectangularMatrix(data); + } + + public static SquareMatrix createSquareMatrix(double[][] data) { + int m = data.length; + + SquareMatrix result; + + switch (m) { + case 2: + result = new Matrix2(data); + break; + case 3: + result = new Matrix3(data); + break; + case 4: + result = new Matrix4(data); + break; + default: + result = new SquareMatrix(data); + } + + return result; + + } + + /** + * Creates an identity matrix with its dimension equal to the argument + * + * @param dimension the dimension + * @return an identity matrix of the given dimension + */ + public static SquareMatrix createIdentityMatrix(int dimension) { + var data = new double[dimension][dimension]; + + for (int i = 0; i < dimension; i++) { + data[i][i] = 1.0; + } + + return createSquareMatrix(data); + } + +} diff --git a/src/main/java/pulse/math/linear/Matrix2.java b/src/main/java/pulse/math/linear/Matrix2.java index 4794f842..00cf4095 100644 --- a/src/main/java/pulse/math/linear/Matrix2.java +++ b/src/main/java/pulse/math/linear/Matrix2.java @@ -4,40 +4,41 @@ * A square 2-by-2 matrix. * */ - class Matrix2 extends SquareMatrix { - protected Matrix2(double[][] args) { - super(args); - } - - /** - * Fast (in terms of required floating point operations) calculation of the matrix inverse. - */ - - @Override - public SquareMatrix inverse() { - double[][] mx = new double[2][2]; - final var x = this.getData(); - - final double det = det(); - - mx[0][0] = x[1][1] / det; - mx[0][1] = -x[0][1] / det; - mx[1][0] = -x[1][0] / det; - mx[1][1] = x[0][0] / det; - - return new SquareMatrix(mx); - } - - /** - * Fast (in terms of required floating point operations) calculation of the determinant. - */ - - @Override - public double det() { - var x = getData(); - return x[0][0] * x[1][1] - x[1][0] * x[0][1]; - } - -} \ No newline at end of file + private static final long serialVersionUID = 6015187791989387058L; + + protected Matrix2(double[][] args) { + super(args); + } + + /** + * Fast (in terms of required floating point operations) calculation of the + * matrix inverse. + */ + @Override + public SquareMatrix inverse() { + double[][] mx = new double[2][2]; + final var x = this.getData(); + + final double det = det(); + + mx[0][0] = x[1][1] / det; + mx[0][1] = -x[0][1] / det; + mx[1][0] = -x[1][0] / det; + mx[1][1] = x[0][0] / det; + + return new SquareMatrix(mx); + } + + /** + * Fast (in terms of required floating point operations) calculation of the + * determinant. + */ + @Override + public double det() { + var x = getData(); + return x[0][0] * x[1][1] - x[1][0] * x[0][1]; + } + +} diff --git a/src/main/java/pulse/math/linear/Matrix3.java b/src/main/java/pulse/math/linear/Matrix3.java index e6e41a8e..3835e329 100644 --- a/src/main/java/pulse/math/linear/Matrix3.java +++ b/src/main/java/pulse/math/linear/Matrix3.java @@ -4,45 +4,45 @@ * A 3-by-3 matrix class. * */ - class Matrix3 extends SquareMatrix { - protected Matrix3(double[][] args) { - super(args); - } - - /** - * Fast (in terms of required floating point operations) calculation of the - * matrix inverse. - */ - - @Override - public SquareMatrix inverse() { - - double[][] minv = new double[3][3]; - final var x = getData(); - - // computes the inverse of a matrix m - final double invdet = 1.0 / det(); - - minv[0][0] = (x[1][1] * x[2][2] - x[2][1] * x[1][2]) * invdet; - minv[0][1] = (x[0][2] * x[2][1] - x[0][1] * x[2][2]) * invdet; - minv[0][2] = (x[0][1] * x[1][2] - x[0][2] * x[1][1]) * invdet; - minv[1][0] = (x[1][2] * x[2][0] - x[1][0] * x[2][2]) * invdet; - minv[1][1] = (x[0][0] * x[2][2] - x[0][2] * x[2][0]) * invdet; - minv[1][2] = (x[1][0] * x[0][2] - x[0][0] * x[1][2]) * invdet; - minv[2][0] = (x[1][0] * x[2][1] - x[2][0] * x[1][1]) * invdet; - minv[2][1] = (x[2][0] * x[0][1] - x[0][0] * x[2][1]) * invdet; - minv[2][2] = (x[0][0] * x[1][1] - x[1][0] * x[0][1]) * invdet; - - return new SquareMatrix(minv); - } - - @Override - public double det() { - var x = getData(); - return x[0][0] * (x[1][1] * x[2][2] - x[2][1] * x[1][2]) - x[0][1] * (x[1][0] * x[2][2] - x[1][2] * x[2][0]) - + x[0][2] * (x[1][0] * x[2][1] - x[1][1] * x[2][0]); - } + private static final long serialVersionUID = -2671066600560428989L; + + protected Matrix3(double[][] args) { + super(args); + } + + /** + * Fast (in terms of required floating point operations) calculation of the + * matrix inverse. + */ + @Override + public SquareMatrix inverse() { + + double[][] minv = new double[3][3]; + final var x = getData(); + + // computes the inverse of a matrix m + final double invdet = 1.0 / det(); + + minv[0][0] = (x[1][1] * x[2][2] - x[2][1] * x[1][2]) * invdet; + minv[0][1] = (x[0][2] * x[2][1] - x[0][1] * x[2][2]) * invdet; + minv[0][2] = (x[0][1] * x[1][2] - x[0][2] * x[1][1]) * invdet; + minv[1][0] = (x[1][2] * x[2][0] - x[1][0] * x[2][2]) * invdet; + minv[1][1] = (x[0][0] * x[2][2] - x[0][2] * x[2][0]) * invdet; + minv[1][2] = (x[1][0] * x[0][2] - x[0][0] * x[1][2]) * invdet; + minv[2][0] = (x[1][0] * x[2][1] - x[2][0] * x[1][1]) * invdet; + minv[2][1] = (x[2][0] * x[0][1] - x[0][0] * x[2][1]) * invdet; + minv[2][2] = (x[0][0] * x[1][1] - x[1][0] * x[0][1]) * invdet; + + return new SquareMatrix(minv); + } + + @Override + public double det() { + var x = getData(); + return x[0][0] * (x[1][1] * x[2][2] - x[2][1] * x[1][2]) - x[0][1] * (x[1][0] * x[2][2] - x[1][2] * x[2][0]) + + x[0][2] * (x[1][0] * x[2][1] - x[1][1] * x[2][0]); + } } diff --git a/src/main/java/pulse/math/linear/Matrix4.java b/src/main/java/pulse/math/linear/Matrix4.java index cb3fc392..cc34b17d 100644 --- a/src/main/java/pulse/math/linear/Matrix4.java +++ b/src/main/java/pulse/math/linear/Matrix4.java @@ -4,64 +4,63 @@ * A 4-by-4 matrix. * */ - class Matrix4 extends SquareMatrix { - protected Matrix4(double[][] args) { - super(args); - } - - /** - * Fast inverse procedure for 4x4 matrix. Credit to Robin Hilliard. - * - * @return inverse of a 4x4 matrix - */ - - @Override - public SquareMatrix inverse() { - final var x = getData(); - var mx = new double[4][4]; + private static final long serialVersionUID = -1355372261335732541L; + + protected Matrix4(double[][] args) { + super(args); + } + + /** + * Fast inverse procedure for 4x4 matrix. Credit to Robin Hilliard. + * + * @return inverse of a 4x4 matrix + */ + @Override + public SquareMatrix inverse() { + final var x = getData(); + var mx = new double[4][4]; - final double s0 = x[0][0] * x[1][1] - x[1][0] * x[0][1]; - final double s1 = x[0][0] * x[1][2] - x[1][0] * x[0][2]; - final double s2 = x[0][0] * x[1][3] - x[1][0] * x[0][3]; - final double s3 = x[0][1] * x[1][2] - x[1][1] * x[0][2]; - final double s4 = x[0][1] * x[1][3] - x[1][1] * x[0][3]; - final double s5 = x[0][2] * x[1][3] - x[1][2] * x[0][3]; + final double s0 = x[0][0] * x[1][1] - x[1][0] * x[0][1]; + final double s1 = x[0][0] * x[1][2] - x[1][0] * x[0][2]; + final double s2 = x[0][0] * x[1][3] - x[1][0] * x[0][3]; + final double s3 = x[0][1] * x[1][2] - x[1][1] * x[0][2]; + final double s4 = x[0][1] * x[1][3] - x[1][1] * x[0][3]; + final double s5 = x[0][2] * x[1][3] - x[1][2] * x[0][3]; - final double c5 = x[2][2] * x[3][3] - x[3][2] * x[2][3]; - final double c4 = x[2][1] * x[3][3] - x[3][1] * x[2][3]; - final double c3 = x[2][1] * x[3][2] - x[3][1] * x[2][2]; - final double c2 = x[2][0] * x[3][3] - x[3][0] * x[2][3]; - final double c1 = x[2][0] * x[3][2] - x[3][0] * x[2][2]; - final double c0 = x[2][0] * x[3][1] - x[3][0] * x[2][1]; + final double c5 = x[2][2] * x[3][3] - x[3][2] * x[2][3]; + final double c4 = x[2][1] * x[3][3] - x[3][1] * x[2][3]; + final double c3 = x[2][1] * x[3][2] - x[3][1] * x[2][2]; + final double c2 = x[2][0] * x[3][3] - x[3][0] * x[2][3]; + final double c1 = x[2][0] * x[3][2] - x[3][0] * x[2][2]; + final double c0 = x[2][0] * x[3][1] - x[3][0] * x[2][1]; - // Should check for 0 determinant + // Should check for 0 determinant + final double invdet = 1.0 / (s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0); - final double invdet = 1.0 / (s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0); + mx[0][0] = (x[1][1] * c5 - x[1][2] * c4 + x[1][3] * c3) * invdet; + mx[0][1] = (-x[0][1] * c5 + x[0][2] * c4 - x[0][3] * c3) * invdet; + mx[0][2] = (x[3][1] * s5 - x[3][2] * s4 + x[3][3] * s3) * invdet; + mx[0][3] = (-x[2][1] * s5 + x[2][2] * s4 - x[2][3] * s3) * invdet; - mx[0][0] = (x[1][1] * c5 - x[1][2] * c4 + x[1][3] * c3) * invdet; - mx[0][1] = (-x[0][1] * c5 + x[0][2] * c4 - x[0][3] * c3) * invdet; - mx[0][2] = (x[3][1] * s5 - x[3][2] * s4 + x[3][3] * s3) * invdet; - mx[0][3] = (-x[2][1] * s5 + x[2][2] * s4 - x[2][3] * s3) * invdet; + mx[1][0] = (-x[1][0] * c5 + x[1][2] * c2 - x[1][3] * c1) * invdet; + mx[1][1] = (x[0][0] * c5 - x[0][2] * c2 + x[0][3] * c1) * invdet; + mx[1][2] = (-x[3][0] * s5 + x[3][2] * s2 - x[3][3] * s1) * invdet; + mx[1][3] = (x[2][0] * s5 - x[2][2] * s2 + x[2][3] * s1) * invdet; - mx[1][0] = (-x[1][0] * c5 + x[1][2] * c2 - x[1][3] * c1) * invdet; - mx[1][1] = (x[0][0] * c5 - x[0][2] * c2 + x[0][3] * c1) * invdet; - mx[1][2] = (-x[3][0] * s5 + x[3][2] * s2 - x[3][3] * s1) * invdet; - mx[1][3] = (x[2][0] * s5 - x[2][2] * s2 + x[2][3] * s1) * invdet; + mx[2][0] = (x[1][0] * c4 - x[1][1] * c2 + x[1][3] * c0) * invdet; + mx[2][1] = (-x[0][0] * c4 + x[0][1] * c2 - x[0][3] * c0) * invdet; + mx[2][2] = (x[3][0] * s4 - x[3][1] * s2 + x[3][3] * s0) * invdet; + mx[2][3] = (-x[2][0] * s4 + x[2][1] * s2 - x[2][3] * s0) * invdet; - mx[2][0] = (x[1][0] * c4 - x[1][1] * c2 + x[1][3] * c0) * invdet; - mx[2][1] = (-x[0][0] * c4 + x[0][1] * c2 - x[0][3] * c0) * invdet; - mx[2][2] = (x[3][0] * s4 - x[3][1] * s2 + x[3][3] * s0) * invdet; - mx[2][3] = (-x[2][0] * s4 + x[2][1] * s2 - x[2][3] * s0) * invdet; + mx[3][0] = (-x[1][0] * c3 + x[1][1] * c1 - x[1][2] * c0) * invdet; + mx[3][1] = (x[0][0] * c3 - x[0][1] * c1 + x[0][2] * c0) * invdet; + mx[3][2] = (-x[3][0] * s3 + x[3][1] * s1 - x[3][2] * s0) * invdet; + mx[3][3] = (x[2][0] * s3 - x[2][1] * s1 + x[2][2] * s0) * invdet; - mx[3][0] = (-x[1][0] * c3 + x[1][1] * c1 - x[1][2] * c0) * invdet; - mx[3][1] = (x[0][0] * c3 - x[0][1] * c1 + x[0][2] * c0) * invdet; - mx[3][2] = (-x[3][0] * s3 + x[3][1] * s1 - x[3][2] * s0) * invdet; - mx[3][3] = (x[2][0] * s3 - x[2][1] * s1 + x[2][2] * s0) * invdet; + return new SquareMatrix(mx); - return new SquareMatrix(mx); + } - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/math/linear/RectangularMatrix.java b/src/main/java/pulse/math/linear/RectangularMatrix.java new file mode 100644 index 00000000..8ac8470c --- /dev/null +++ b/src/main/java/pulse/math/linear/RectangularMatrix.java @@ -0,0 +1,254 @@ +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; +import static pulse.math.linear.Matrices.createMatrix; + +import pulse.ui.Messages; + +public class RectangularMatrix implements Serializable { + + private static final long serialVersionUID = -8184303238440935851L; + protected final double[][] x; + + protected RectangularMatrix(double[][] args) { + int m = args.length; + int n = args[0].length; + + x = new double[m][n]; + + for (int i = 0; i < m; i++) { + System.arraycopy(args[i], 0, x[i], 0, n); + } + + } + + protected RectangularMatrix(double[] data, int n) { + final int m = data.length / n; + x = new double[m][n]; + + for (int i = 0; i < m; i++) { + System.arraycopy(data, i * n, x[i], 0, n); + } + + } + + /** + * Performs an element-wise summation if {@code this} and {@code m} have + * matching dimensions. + * + * @param m another {@code Matrix} of the same size as {@code this} one + * @return the result of summation + */ + public RectangularMatrix sum(RectangularMatrix m) { + return performOperation(this, m, SUM); + } + + /** + * Performs an element-wise subtraction of {@code m} from {@code this} if + * these matrices have matching dimensions. + * + * @param m another {@code Matrix} of the same size as {@code this} one + * @return the result of subtraction + */ + public RectangularMatrix subtract(RectangularMatrix m) { + return performOperation(this, m, DIFFERENCE); + } + + /** + *

+ * Performs {@code Matrix} multiplication. Checks whether the dimensions of + * each matrix are appropriate (number of columns in {@code this} matrix + * should be equal to the number of rows in {@code m}. + *

+ * + * @param m another {@code Matrix} suitable for multiplication + * @return a {@code Matrix}, which is the result of multiplying {@code this} + * by {@code m} + */ + public RectangularMatrix multiply(RectangularMatrix m) { + if (this.x[0].length != m.x.length) { + throw new IllegalArgumentException(Messages.getString("Matrix.MultiplicationError") + this + " and " + m); + } + + final int mm = this.x.length; + final int nn = m.x[0].length; + + var y = new double[mm][nn]; + + for (int i = 0; i < mm; i++) { + for (int j = 0; j < nn; j++) { + for (int k = 0; k < this.x[0].length; k++) { + y[i][j] += this.x[i][k] * m.x[k][j]; + } + } + } + + return createMatrix(y); + + } + + /** + * Scales this {@code Matrix} by {@code f}, which results in element-wise + * multiplication by {@code f}. + * + * @param f a numeric value + * @return the scaled {@code Matrix} + */ + public RectangularMatrix multiply(double f) { + double[][] y = new double[x.length][x[0].length]; + + for (int i = 0; i < x.length; i++) { + for (int j = 0; j < x[0].length; j++) { + y[i][j] = this.x[i][j] * f; + } + } + + return createMatrix(y); + + } + + /** + * Transposes this {@code Matrix}, i.e. reflects it over the main diagonal. + * + * @return a transposed {@code Matrix} + */ + public RectangularMatrix transpose() { + int m = x.length; + int n = x[0].length; + double[][] y = new double[n][m]; + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + y[j][i] = x[i][j]; + } + } + + return createMatrix(y); + + } + + public double get(int m, int k) { + return x[m][k]; + } + + public double[][] getData() { + return x; + } + + private static RectangularMatrix performOperation(RectangularMatrix m1, RectangularMatrix m2, + ArithmeticOperation op) { + if (!m1.dimensionsMatch(m2)) { + throw new IllegalArgumentException(Messages.getString("Matrix.DimensionError") + m1 + " != " + m2); + } + + double[][] y = new double[m1.x.length][m1.x[0].length]; + + for (int i = 0; i < y.length; i++) { + for (int j = 0; j < y[0].length; j++) { + y[i][j] = op.evaluate(m1.x[i][j], m2.x[i][j]); + } + } + + return createMatrix(y); + } + + /** + *

+ * Multiplies this {@code Matrix} by the vector {@code v}, which is + * represented by a n × 1 {@code Matrix}, where + * {@code n} is the dimension of {@code v}. Note {@code n} should be equal + * to the number of rows in this {@code Matrix}. + *

+ * + * @param v a {@code Vector}. + * @return the result of multiplication, which is a {@code Vector}. + */ + public Vector multiply(Vector v) { + double[] r = new double[x.length]; + + if (x[0].length != v.dimension()) { + throw new IllegalArgumentException( + "Cannot multiply a " + x.length + "x" + x[0].length + " matrix by a " + v.dimension() + " vector"); + } + + for (int i = 0; i < x.length; i++) { + for (int k = 0; k < x[0].length; k++) { + r[i] += x[i][k] * v.get(k); + } + } + + return new Vector(r); + } + + /** + * Prints out matrix dimensions and all the elements contained in it. + */ + @Override + public String toString() { + int m = x.length; + int n = x[0].length; + final String f = Messages.getString("Math.DecimalFormat"); + + StringBuilder sb = new StringBuilder(m + "x" + n + " matrix: "); + for (int i = 0; i < m; i++) { + sb.append(System.lineSeparator()); + for (int j = 0; j < n; j++) { + sb.append(" "); + sb.append(String.format(f, x[i][j])); + } + } + + return sb.toString(); + } + + /** + * Checks if the dimension of {@code this Matrix} and {@code m} match, i.e. + * if the number of rows is the same and the number of columns is the same + * + * @param m another {@code Matrix} + * @return {@code true} if the dimensions match, {@code false} otherwise. + */ + public boolean dimensionsMatch(RectangularMatrix m) { + return (x.length == m.x.length) && (x[0].length == m.x[0].length); + } + + /** + * Checks whether {@code o} is a {@code SquareMatrix} with matching + * dimensions and all elements of which are (approximately) equal to the + * respective elements of {@code this} matrix}. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof SquareMatrix)) { + return false; + } + + if (o == this) { + return true; + } + + var m = (SquareMatrix) o; + + if (!this.dimensionsMatch(m)) { + return false; + } + + boolean result = true; + + for (int i = 0; i < x.length; i++) { + for (int j = 0; j < x[0].length; j++) { + if (!approximatelyEquals(this.x[i][j], m.x[i][j])) { + result = false; + break; + } + } + } + + return result; + + } + +} diff --git a/src/main/java/pulse/math/linear/SquareMatrix.java b/src/main/java/pulse/math/linear/SquareMatrix.java index 3551598c..cb037b50 100644 --- a/src/main/java/pulse/math/linear/SquareMatrix.java +++ b/src/main/java/pulse/math/linear/SquareMatrix.java @@ -1,15 +1,11 @@ package pulse.math.linear; import static org.ejml.dense.row.CommonOps_DDRM.invert; -import static pulse.math.MathUtils.approximatelyEquals; -import static pulse.math.linear.ArithmeticOperations.DIFFERENCE; -import static pulse.math.linear.ArithmeticOperations.SUM; -import static pulse.math.linear.Matrices.createMatrix; +import static pulse.math.linear.Matrices.createSquareMatrix; import org.ejml.data.DMatrixRMaj; import org.ejml.dense.row.CommonOps_DDRM; - -import pulse.ui.Messages; +import org.ejml.dense.row.MatrixFeatures_DDRM; /** * The matrix class. @@ -24,297 +20,97 @@ * {@code pulse.math} package, the user needs to invoke the factory class * methods {@code Matrices} instead. *

- * + * * @see pulse.math.linear.Matrices */ - -public class SquareMatrix { - - private final double[][] x; - - /** - * Constructs a {@code Matrix} with the elements copied from {@code args}. The - * elements are copied by invoking System.arraycopy(...). - * - * @param args a two-dimensional double array - */ - - protected SquareMatrix(double[][] args) { - int m = args.length; - int n = args[0].length; - - x = new double[m][n]; - - for (int i = 0; i < m; i++) - System.arraycopy(args[i], 0, x[i], 0, n); - - } - - private SquareMatrix(double[] data, int n) { - this.x = new double[n][n]; - - for (int i = 0; i < n; i++) - System.arraycopy(data, i * n, x[i], 0, n); - - } - - /** - * Performs an element-wise summation if {@code this} and {@code m} have - * matching dimensions. - * - * @param m another {@code Matrix} of the same size as {@code this} one - * @return the result of summation - */ - - public SquareMatrix sum(SquareMatrix m) { - return performOperation(this, m, SUM); - } - - /** - * Performs an element-wise subtraction of {@code m} from {@code this} if these - * matrices have matching dimensions. - * - * @param m another {@code Matrix} of the same size as {@code this} one - * @return the result of subtraction - */ - - public SquareMatrix subtract(SquareMatrix m) { - return performOperation(this, m, DIFFERENCE); - } - - /** - *

- * Performs {@code Matrix} multiplication. Checks whether the dimensions of each - * matrix are appropriate (number of columns in {@code this} matrix should be - * equal to the number of rows in {@code m}. - *

- * - * @param m another {@code Matrix} suitable for multiplication - * @return a {@code Matrix}, which is the result of multiplying {@code this} by - * {@code m} - */ - - public SquareMatrix multiply(SquareMatrix m) { - if (this.x[0].length != m.x.length) - throw new IllegalArgumentException(Messages.getString("Matrix.MultiplicationError") + this + " and " + m); - - final int mm = this.x.length; - final int nn = m.x[0].length; - - var y = new double[mm][nn]; - - for (int i = 0; i < mm; i++) { - for (int j = 0; j < nn; j++) { - for (int k = 0; k < this.x[0].length; k++) { - y[i][j] += this.x[i][k] * m.x[k][j]; - } - } - } - - return createMatrix(y); - - } - - /** - * Scales this {@code Matrix} by {@code f}, which results in element-wise - * multiplication by {@code f}. - * - * @param f a numeric value - * @return the scaled {@code Matrix} - */ - - public SquareMatrix multiply(double f) { - double[][] y = new double[x.length][x[0].length]; - - for (int i = 0; i < x.length; i++) { - for (int j = 0; j < x[0].length; j++) { - y[i][j] = this.x[i][j] * f; - } - } - - return createMatrix(y); - - } - - /** - *

- * Multiplies this {@code Matrix} by the vector {@code v}, which is represented - * by a n × 1 {@code Matrix}, where {@code n} is the - * dimension of {@code v}. Note {@code n} should be equal to the number of rows - * in this {@code Matrix}. - *

- * - * @param v a {@code Vector}. - * @return the result of multiplication, which is a {@code Vector}. - */ - - public Vector multiply(Vector v) { - double[] r = new double[v.dimension()]; - - for (int i = 0; i < r.length; i++) { - for (int k = 0; k < r.length; k++) { - r[i] += x[i][k] * v.get(k); - } - } - - return new Vector(r); - } - - /** - * Transposes this {@code Matrix}, i.e. reflects it over the main diagonal. - * - * @return a transposed {@code Matrix} - */ - - public SquareMatrix transpose() { - int m = x.length; - int n = x[0].length; - double[][] y = new double[n][m]; - - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - y[j][i] = x[i][j]; - } - } - - return createMatrix(y); - - } - - /** - * Calculates the determinant for an n-by-n square matrix. The - * determinant is calculated using the EJML library. - * - * @return a double, representing the determinant - */ - - public double det() { - var mx = new DMatrixRMaj(x); - return CommonOps_DDRM.det(mx); - } - - /** - * Conducts matrix inversion with the procedural EJML approach. Can be overriden - * by subclasses to boost performance. - * - * @return the inverted {@Code Matrix}. - */ - - public SquareMatrix inverse() { - var mx = new DMatrixRMaj(x); - invert(mx); - return new SquareMatrix(mx.getData(), x.length); - } - - /** - * Calculates the outer product of two vectors. - * - * @param a a Vector - * @param b a Vector - * @return the outer product of {@code a} and {@code b} - */ - - public static SquareMatrix outerProduct(Vector a, Vector b) { - double[][] x = new double[a.dimension()][b.dimension()]; - - for (int i = 0; i < x.length; i++) { - for (int j = 0; j < x[0].length; j++) { - x[i][j] = a.get(i) * b.get(j); - } - } - - return createMatrix(x); - } - - /** - * Checks whether {@code o} is a {@code SquareMatrix} with matching dimensions - * and all elements of which are (approximately) equal to the respective elements - * of {@code this} matrix}. - */ - - @Override - public boolean equals(Object o) { - if (!(o instanceof SquareMatrix)) - return false; - - if (o == this) - return true; - - var m = (SquareMatrix) o; - - if(! this.hasSameDimensions(m) ) - return false; - - boolean result = true; - - for (int i = 0; i < x.length; i++) { - for (int j = 0; j < x.length; j++) { - if (!approximatelyEquals(this.x[i][j], m.x[i][j])) { - result = false; - break; - } - } - } - - return result; - - } - - /** - * Prints out matrix dimensions and all the elements contained in it. - */ - - @Override - public String toString() { - int m = x.length; - int n = x[0].length; - final String f = Messages.getString("Math.DecimalFormat"); - - StringBuilder sb = new StringBuilder(m + "x" + n + " matrix: "); - for (int i = 0; i < m; i++) { - sb.append(System.lineSeparator()); - for (int j = 0; j < n; j++) { - sb.append(" "); - sb.append(String.format(f, x[i][j])); - } - } - - return sb.toString(); - } - - /** - * Checks if the dimension of {@code this Matrix} and {@code m} match, i.e. if - * the number of rows is the same and the number of columns is the same - * - * @param m another {@code Matrix} - * @return {@code true} if the dimensions match, {@code false} otherwise. - */ - - public boolean hasSameDimensions(SquareMatrix m) { - return (x.length == m.x.length) && (x[0].length == m.x[0].length); - } - - public double get(int m, int k) { - return x[m][k]; - } - - public double[][] getData() { - return x; - } - - private static SquareMatrix performOperation(SquareMatrix m1, SquareMatrix m2, ArithmeticOperation op) { - if (!m1.hasSameDimensions(m2)) - throw new IllegalArgumentException(Messages.getString("Matrix.DimensionError") + m1 + " != " + m2); - - double[][] y = new double[m1.x.length][m1.x[0].length]; - - for (int i = 0; i < y.length; i++) { - for (int j = 0; j < y[0].length; j++) { - y[i][j] = op.evaluate(m1.x[i][j], m2.x[i][j]); - } - } - - return createMatrix(y); - } - -} \ No newline at end of file +public class SquareMatrix extends RectangularMatrix { + + /** + * Constructs a {@code Matrix} with the elements copied from {@code args}. + * The elements are copied by invoking System.arraycopy(...). + * + * @param args a two-dimensional double array + */ + protected SquareMatrix(double[][] args) { + super(args); + } + + private SquareMatrix(double[] data, int n) { + super(data, n); + } + + /** + * Calculates the determinant for an n-by-n square matrix. The + * determinant is calculated using the EJML library. + * + * @return a double, representing the determinant + */ + public double det() { + var mx = new DMatrixRMaj(x); + return CommonOps_DDRM.det(mx); + } + + /** + * Conducts matrix inversion with the procedural EJML approach. Can be + * overriden by subclasses to boost performance. + * + * @return the inverted {@Code Matrix}. + */ + public SquareMatrix inverse() { + var mx = new DMatrixRMaj(x); + invert(mx); + return new SquareMatrix(mx.getData(), x.length); + } + + /** + * Checks if a matrix is positive definite. Uses EJML implementation. + * + * @return {@code true} is positive-definite + */ + public boolean isPositiveDefinite() { + return MatrixFeatures_DDRM.isPositiveDefinite(new DMatrixRMaj(x)); + } + + /** + * Calculates the outer product of two vectors. + * + * @param a a Vector + * @param b a Vector + * @return the outer product of {@code a} and {@code b} + */ + public static SquareMatrix outerProduct(Vector a, Vector b) { + double[][] x = new double[a.dimension()][b.dimension()]; + + for (int i = 0; i < x.length; i++) { + for (int j = 0; j < x[0].length; j++) { + x[i][j] = a.get(i) * b.get(j); + } + } + + return createSquareMatrix(x); + } + + public static SquareMatrix asSquareMatrix(RectangularMatrix m) { + return m.x.length == m.x[0].length ? new SquareMatrix(m.getData()) : null; + } + + public int dimension() { + return getData().length; + } + + /** + * Creates a block-diagonal matrix from the diagonal of this matrix. + * + * @return diag(this) + */ + public SquareMatrix blockDiagonal() { + final int dim = dimension(); + var data = getData(); + var diag = new double[dim][dim]; + for (int i = 0; i < dim; i++) { + diag[i][i] = data[i][i]; + } + return new SquareMatrix(diag); + } + +} diff --git a/src/main/java/pulse/math/linear/Vector.java b/src/main/java/pulse/math/linear/Vector.java index 909bf322..483cf0c5 100644 --- a/src/main/java/pulse/math/linear/Vector.java +++ b/src/main/java/pulse/math/linear/Vector.java @@ -1,7 +1,9 @@ package pulse.math.linear; +import java.io.Serializable; 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; @@ -11,284 +13,308 @@ /** *

- * This is a general class for {@code Vector} operations useful for - * optimisers and ODE solvers. + * This is a general class for {@code Vector} operations useful for optimisers + * and ODE solvers. *

*/ - -public class Vector { - - private double[] x; - - /** - * Constructs a new vector specified by the argument array - * @param x an array of double - */ - - public Vector(double[] x) { - this.x = new double[x.length]; - System.arraycopy(x, 0, this.x, 0, x.length); - } - - /** - * Creates a zero {@code Vector}. - * - * @param n the dimension of the {@code Vector}. - */ - - public Vector(int n) { - x = new double[n]; - } - - /** - * Copy constructor. - * - * @param v The vector to be copied - */ - - public Vector(Vector v) { - this(v.x); - } - - /** - * Creates a new {@code Vector} based on {@code this} one, all elements of which - * are inverted, i.e. bi = -ai. - * - * @return a generalised inversion of {@code this Vector}. - */ - - public Vector inverted() { - return performOperation(new Vector(dimension()), this, DIFFERENCE); - } - - /** - * The dimension is simply the number of elements in a {@code Vector} - * - * @return the integer dimension - */ - - public int dimension() { - return x.length; - } - - /** - * Performs an element-wise summation of {@code this} and {@code v}. - * - * @param v another {@code Vector} with the same number of elements. - * @return the result of the summation. - */ - - public Vector sum(Vector v) { - return performOperation(this, v, SUM); - } - - /** - * Performs an element-wise subtraction of {@code v} from {@code this}. - * - * @param v another {@code Vector} with the same number of elements. - * @return the result of subtracting {@code v} from {@code this}. - * @throws IllegalArgumentException f the dimension of {@code this} and - * {@code v} are different. - */ - - public Vector subtract(Vector v) { - return performOperation(this, v, DIFFERENCE); - } - - /** - * Performs an element-wise multiplication by {@code f}. - * - * @param f a double value. - * @return a new {@code Vector}, all elements of which will be multiplied by - * {@code f}. - */ - - public Vector multiply(double f) { - Vector factor = new Vector(this); - - for (int i = 0; i < x.length; i++) { - factor.x[i] *= f; - } - - return factor; - } - - /** - * Calculates the scalar product of {@code this} and {@code v}. - * - * @param v another {@code Vector} with the same dimension. - * @return the dot product of {@code this} and {@code v}. - */ - - public double dot(Vector v) { - return reduce(this, v, PRODUCT); - } - - /** - * Calculates the length, which is represented by the square-root of the squared - * length. - * - * @return the calculated length. - * @see lengthSq() - */ - - public double length() { - return sqrt(lengthSq()); - } - - /** - * The squared length of this vector is the dot product of this vector by - * itself. - * - * @return the squared length. - */ - - public double lengthSq() { - return this.dot(this); - } - - /** - * Performs normalisation, e.g. scalar multiplication of {@code this} by the - * multiplicative inverse of {@code this Vector}'s length. - * - * @return a normalised {@code Vector} obtained from {@code this}. - */ - - public Vector normalise() { - return this.multiply(1.0 / length() ); - } - - /** - * Calculates the squared distance from {@code this Vector} to {@code v} (which - * is the squared length of the connecting {@code Vector}). - * - * @param v another {@code Vector}. - * @return the squared length of the connecting {@code Vector}. - * @throws IllegalArgumentException f the dimension of {@code this} and - * {@code v} are different. - */ - - public double distanceToSq(Vector v) throws IllegalArgumentException { - return reduce(this, v, DIFF_SQUARED); - } - - /** - * Gets the component of {@code this Vector} specified by {@code index} - * - * @param index the index of the component - * @return a double value, representing the value of the component - */ - - public double get(int index) { - return x[index]; - } - - /** - * Sets the component of {@code this Vector} specified by {@code index} to - * {@code value}. - * - * @param index the index of the component. - * @param value a new value that will replace the old one. - */ - - public void set(int index, double value) { - x[index] = value; - } - - /** - * Defines the string representation of the current instance of the class. - * - * @return the string-equivalent of this object containing all it's field - * values. - */ - - @Override - public String toString() { - final String f = Messages.getString("Math.DecimalFormat"); //$NON-NLS-1$ - StringBuilder sb = new StringBuilder().append("("); //$NON-NLS-1$ - for (double c : x) { - sb.append(String.format(f, c) + " "); //$NON-NLS-1$ - } - sb.append(")"); //$NON-NLS-1$ - return sb.toString(); - } - - /** - * Checks if o is logically equivalent to an instance of this - * class. - * - * @param o An object to compare with this vector. - * @return true if o equals this. - */ - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (!(o instanceof Vector)) - return false; - - Vector v = (Vector) o; - - if (v.x.length != x.length) - return false; - - for (int i = 0; i < x.length; i++) { - if (Double.doubleToLongBits(this.x[i]) != Double.doubleToLongBits(v.x[i])) - return false; - } - - return true; - - } - - /** - * Determines the maximum absolute value of the vector components. - * - * @return a component having the maximum absolute value. - */ - - public double maxAbsComponent() { - double max = abs(x[0]); - double abs = 0; - for (int i = 1; i < x.length; i++) { - abs = abs(x[i]); - max = abs > max ? abs : max; - } - return max; - } - - public double[] getData() { - return x; - } - - private static void checkup(Vector v1, Vector v2) { - if (v1.x.length != v2.x.length) - throw new IllegalArgumentException( - Messages.getString("Vector.DimensionError1") + v1.x.length + " != " + v2.x.length); - } - - private static Vector performOperation(Vector v1, Vector v2, ArithmeticOperation op) { - checkup(v1, v2); - - Vector result = new Vector(v2.x.length); - - for (int i = 0; i < v2.x.length; i++) - result.x[i] = op.evaluate(v1.get(i), v2.get(i)); - - return result; - } - - private static double reduce(Vector v1, Vector v2, ArithmeticOperation op) { - checkup(v1, v2); - - double result = 0; - - for (int i = 0; i < v2.x.length; i++) - result += op.evaluate(v1.get(i), v2.get(i)); - - return result; - } - -} \ No newline at end of file +public class Vector implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 5560069982536341831L; + private double[] x; + + /** + * Constructs a new vector specified by the argument array + * + * @param x an array of double + */ + public Vector(double[] x) { + this.x = new double[x.length]; + System.arraycopy(x, 0, this.x, 0, x.length); + } + + /** + * Creates a zero {@code Vector}. + * + * @param n the dimension of the {@code Vector}. + */ + public Vector(int n) { + x = new double[n]; + } + + /** + * Copy constructor. + * + * @param v The vector to be copied + */ + public Vector(Vector v) { + this(v.x); + } + + /** + * Creates a new {@code Vector} based on {@code this} one, all elements of + * which are inverted, i.e. bi = + * -ai. + * + * @return a generalised inversion of {@code this Vector}. + */ + public Vector inverted() { + return performOperation(new Vector(dimension()), this, DIFFERENCE); + } + + /** + * The dimension is simply the number of elements in a {@code Vector} + * + * @return the integer dimension + */ + public final int dimension() { + return x.length; + } + + /** + * Performs an element-wise summation of {@code this} and {@code v}. + * + * @param v another {@code Vector} with the same number of elements. + * @return the result of the summation. + */ + public Vector sum(Vector v) { + return performOperation(this, v, SUM); + } + + /** + * Performs an element-wise subtraction of {@code v} from {@code this}. + * + * @param v another {@code Vector} with the same number of elements. + * @return the result of subtracting {@code v} from {@code this}. + * @throws IllegalArgumentException f the dimension of {@code this} and + * {@code v} are different. + */ + public Vector subtract(Vector v) { + return performOperation(this, v, DIFFERENCE); + } + + /** + * Performs an element-wise multiplication by {@code f}. + * + * @param f a double value. + * @return a new {@code Vector}, all elements of which will be multiplied by + * {@code f}. + */ + public Vector multiply(double f) { + Vector factor = new Vector(this); + + for (int i = 0; i < x.length; i++) { + factor.x[i] *= f; + } + + return factor; + } + + /** + * Creates a vector with random coordinates confined within [min;max] + * + * @param n the vector dimension + * @param min upper bound for the random number generator + * @param max lower bound for the random generator generator + * @return the randomised vector + */ + public static Vector random(int n, double min, double max) { + var v = new Vector(n); + for (int i = 0; i < n; i++) { + v.x[i] = min + Math.random() * (max - min); + } + return v; + } + + /** + * Component-wise vector multiplication + */ + public Vector multComponents(Vector v) { + Vector nv = new Vector(this); + + for (int i = 0; i < x.length; i++) { + nv.x[i] *= v.x[i]; + } + + return nv; + + } + + /** + * Calculates the scalar product of {@code this} and {@code v}. + * + * @param v another {@code Vector} with the same dimension. + * @return the dot product of {@code this} and {@code v}. + */ + public double dot(Vector v) { + return reduce(this, v, PRODUCT); + } + + /** + * Calculates the length, which is represented by the square-root of the + * squared length. + * + * @return the calculated length. + * @see lengthSq() + */ + public double length() { + return sqrt(lengthSq()); + } + + /** + * The squared length of this vector is the dot product of this vector by + * itself. + * + * @return the squared length. + */ + public double lengthSq() { + return this.dot(this); + } + + /** + * Performs normalisation, e.g. scalar multiplication of {@code this} by the + * multiplicative inverse of {@code this Vector}'s length. + * + * @return a normalised {@code Vector} obtained from {@code this}. + */ + public Vector normalise() { + return this.multiply(1.0 / length()); + } + + /** + * Calculates the squared distance from {@code this Vector} to {@code v} + * (which is the squared length of the connecting {@code Vector}). + * + * @param v another {@code Vector}. + * @return the squared length of the connecting {@code Vector}. + * @throws IllegalArgumentException f the dimension of {@code this} and + * {@code v} are different. + */ + public double distanceToSq(Vector v) throws IllegalArgumentException { + return reduce(this, v, DIFF_SQUARED); + } + + /** + * Gets the component of {@code this Vector} specified by {@code index} + * + * @param index the index of the component + * @return a double value, representing the value of the component + */ + public double get(int index) { + return x[index]; + } + + /** + * Sets the component of {@code this Vector} specified by {@code index} to + * {@code value}. + * + * @param index the index of the component. + * @param value a new value that will replace the old one. + */ + public void set(int index, double value) { + x[index] = value; + } + + /** + * Defines the string representation of the current instance of the class. + * + * @return the string-equivalent of this object containing all it's field + * values. + */ + @Override + public String toString() { + final String f = Messages.getString("Math.DecimalFormat"); //$NON-NLS-1$ + StringBuilder sb = new StringBuilder().append("("); //$NON-NLS-1$ + for (double c : x) { + sb.append(String.format(f, c) + " "); //$NON-NLS-1$ + } + sb.append(")"); //$NON-NLS-1$ + return sb.toString(); + } + + /** + * Checks if o is logically equivalent to an instance of this + * class. + * + * @param o An object to compare with this vector. + * @return true if o equals this. + */ + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof Vector)) { + return false; + } + + Vector v = (Vector) o; + + if (v.x.length != x.length) { + return false; + } + + for (int i = 0; i < x.length; i++) { + if (Double.doubleToLongBits(this.x[i]) != Double.doubleToLongBits(v.x[i])) { + return false; + } + } + + return true; + + } + + /** + * Determines the maximum absolute value of the vector components. + * + * @return a component having the maximum absolute value. + */ + public double maxAbsComponent() { + double max = abs(x[0]); + double abs = 0; + for (int i = 1; i < x.length; i++) { + abs = abs(x[i]); + max = abs > max ? abs : max; + } + return max; + } + + public double[] getData() { + return x; + } + + private static void checkup(Vector v1, Vector v2) { + if (v1.x.length != v2.x.length) { + throw new IllegalArgumentException( + Messages.getString("Vector.DimensionError1") + v1.x.length + " != " + v2.x.length); + } + } + + private static Vector performOperation(Vector v1, Vector v2, ArithmeticOperation op) { + checkup(v1, v2); + + Vector result = new Vector(v2.x.length); + + for (int i = 0; i < v2.x.length; i++) { + result.x[i] = op.evaluate(v1.get(i), v2.get(i)); + } + + return result; + } + + private static double reduce(Vector v1, Vector v2, ArithmeticOperation op) { + checkup(v1, v2); + + double result = 0; + + for (int i = 0; i < v2.x.length; i++) { + result += op.evaluate(v1.get(i), v2.get(i)); + } + + return result; + } + +} diff --git a/src/main/java/pulse/math/linear/package-info.java b/src/main/java/pulse/math/linear/package-info.java index 690dcd93..f875da47 100644 --- a/src/main/java/pulse/math/linear/package-info.java +++ b/src/main/java/pulse/math/linear/package-info.java @@ -1,5 +1,4 @@ /** * A linear algebra package based mostly on the EJML library. */ - -package pulse.math.linear; \ No newline at end of file +package pulse.math.linear; diff --git a/src/main/java/pulse/math/package-info.java b/src/main/java/pulse/math/package-info.java index 525d4ae4..28763b06 100644 --- a/src/main/java/pulse/math/package-info.java +++ b/src/main/java/pulse/math/package-info.java @@ -3,5 +3,4 @@ * (a {@code Vector}) of the minimum, including operations with vector and * matrices. */ - -package pulse.math; \ No newline at end of file +package pulse.math; diff --git a/src/main/java/pulse/math/transforms/AtanhTransform.java b/src/main/java/pulse/math/transforms/AtanhTransform.java new file mode 100644 index 00000000..253fd0e1 --- /dev/null +++ b/src/main/java/pulse/math/transforms/AtanhTransform.java @@ -0,0 +1,43 @@ +package pulse.math.transforms; + +import static java.lang.Math.tanh; +import static pulse.math.MathUtils.atanh; + +import pulse.math.Segment; + +/** + * Hyper-tangent parameter transform allowing to set an upper bound for a + * parameter. + */ +public class AtanhTransform extends BoundedParameterTransform { + + private static final long serialVersionUID = -6322775329000050307L; + + /** + * Only the upper bound of the argument is used. + * + * @param bounds the {@code bounda.getMaximum()} is used in the transforms + */ + public AtanhTransform(Segment bounds) { + super(bounds); + } + + /** + * @see pulse.math.MathUtils.atanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double transform(double a) { + return atanh(2.0 * a / getBounds().getMaximum() - 1.0); + } + + /** + * @see pulse.math.MathUtils.tanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + public double inverse(double t) { + return 0.5 * getBounds().getMaximum() * (tanh(t) + 1.0); + } + +} diff --git a/src/main/java/pulse/math/transforms/BoundedParameterTransform.java b/src/main/java/pulse/math/transforms/BoundedParameterTransform.java new file mode 100644 index 00000000..be12e6ea --- /dev/null +++ b/src/main/java/pulse/math/transforms/BoundedParameterTransform.java @@ -0,0 +1,26 @@ +package pulse.math.transforms; + +import pulse.math.Segment; + +/** + * An abstract {@code Transformable} where the bounds of the parameter is + * manually set. Subclasses can be bounded from either on or both sides. + * + */ +public abstract class BoundedParameterTransform implements Transformable { + + private Segment bounds; + + public BoundedParameterTransform(Segment bounds) { + setBounds(bounds); + } + + public Segment getBounds() { + return bounds; + } + + public void setBounds(Segment bounds) { + this.bounds = bounds; + } + +} diff --git a/src/main/java/pulse/math/transforms/InvDiamTransform.java b/src/main/java/pulse/math/transforms/InvDiamTransform.java new file mode 100644 index 00000000..22345dfb --- /dev/null +++ b/src/main/java/pulse/math/transforms/InvDiamTransform.java @@ -0,0 +1,28 @@ +package pulse.math.transforms; + +import pulse.problem.statements.model.ExtendedThermalProperties; + +/** + * A transform that simply divides the value by the squared length of the + * sample. + */ +public class InvDiamTransform implements Transformable { + + private static final long serialVersionUID = 1809584085307619279L; + private double d; + + public InvDiamTransform(ExtendedThermalProperties etp) { + d = (double) etp.getSampleDiameter().getValue(); + } + + @Override + public double transform(double value) { + return value / d; + } + + @Override + public double inverse(double t) { + return t * d; + } + +} 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 00000000..c788a9b6 --- /dev/null +++ b/src/main/java/pulse/math/transforms/PeriodicTransform.java @@ -0,0 +1,40 @@ +package pulse.math.transforms; + +import pulse.math.Segment; + +public class PeriodicTransform extends BoundedParameterTransform { + + private static final long serialVersionUID = 4564881912462997982L; + + /** + * 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; + } +} diff --git a/src/main/java/pulse/math/transforms/StandardTransformations.java b/src/main/java/pulse/math/transforms/StandardTransformations.java new file mode 100644 index 00000000..a8206fdb --- /dev/null +++ b/src/main/java/pulse/math/transforms/StandardTransformations.java @@ -0,0 +1,64 @@ +package pulse.math.transforms; + +import static java.lang.Math.exp; +import static java.lang.Math.log; +import static java.lang.Math.sqrt; + +/** + * A utility class containing standard mathematical transforms and their + * inverses for non-bounded parameters. + * + */ +public class StandardTransformations { + + /** + * Logarithmic parameter transform. The parameter space is only bounded by + * positive numbers, so no bounding segment required. + */ + public final static Transformable LOG = new Transformable() { + + @Override + public double transform(double a) { + return log(a); + } + + @Override + public double inverse(double t) { + return exp(t); + } + + }; + + public final static Transformable SQRT = new Transformable() { + + @Override + public double transform(double a) { + return sqrt(a); + } + + @Override + public double inverse(double t) { + return t * t; + } + + }; + + public final static Transformable ABS = new Transformable() { + + @Override + public double transform(double a) { + return Math.abs(a); + } + + @Override + public double inverse(double t) { + return transform(t); + } + + }; + + private StandardTransformations() { + //empty + } + +} diff --git a/src/main/java/pulse/math/transforms/StickTransform.java b/src/main/java/pulse/math/transforms/StickTransform.java new file mode 100644 index 00000000..ce5fd4a5 --- /dev/null +++ b/src/main/java/pulse/math/transforms/StickTransform.java @@ -0,0 +1,63 @@ +/* + * 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.math.transforms; + +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 + * 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. + * + * @param bounds the {@code bounda.getMaximum()} is used in the transforms + */ + public StickTransform(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(); + return a > max ? max : (a < min ? min : a); + } + + /** + * @see pulse.math.MathUtils.tanh() + * @see pulse.math.Segment.getBounds() + */ + @Override + 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 new file mode 100644 index 00000000..150f6c30 --- /dev/null +++ b/src/main/java/pulse/math/transforms/Transformable.java @@ -0,0 +1,26 @@ +package pulse.math.transforms; + +import java.io.Serializable; + +/** + * An interface for performing reversible one-to-one mapping of the model + * parameters. + * + */ +public interface Transformable extends Serializable { + + /** + * Performs the selected transform with {@code value} + * + * @param value a double representing the parameter value + * @return the results, such that + * {@code inverse( transform(value) ) = value} + */ + public double transform(double value); + + /** + * Inverses the transform. + */ + public double inverse(double t); + +} diff --git a/src/main/java/pulse/package-info.java b/src/main/java/pulse/package-info.java index 139f9d4e..5017a1ad 100644 --- a/src/main/java/pulse/package-info.java +++ b/src/main/java/pulse/package-info.java @@ -1,7 +1,6 @@ /** * Contains some of the most frequently used classes, which did not seem to fit - * in any other packages. Currently consists of {@code HeatingCurve} extensively + * in any other packages. Currently consists of three classes extensively * used in other packages. */ - -package pulse; \ No newline at end of file +package pulse; diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index 9b57faa5..5ec9be1e 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -1,103 +1,247 @@ package pulse.problem.laser; +import java.io.Serializable; +import java.util.Objects; +import pulse.input.ExperimentalData; +import pulse.math.MidpointIntegrator; +import pulse.math.Segment; 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 * physical {@code Pulse} and the respective {@code DifferenceScheme} used to * process the solution of a {@code Problem}. - * + * * @see pulse.problem.statements.Pulse */ - -public class DiscretePulse { - - private Grid grid; - private Pulse pulse; - private double discretePulseWidth; - private double timeFactor; - - /** - * This creates a one-dimensional discrete pulse on a {@code grid}. - *

- * The dimensional factor is taken from the {@code problem}, while the discrete - * pulse width (a multiplier of the {@code grid} parameter {@code tau} is - * calculated using the {@code gridTime} method. - *

- * - * @param problem the problem, used to extract the dimensional time factor - * @param grid a grid used to discretise the {@code pulse} - */ - - public DiscretePulse(Problem problem, Grid grid) { - timeFactor = problem.getProperties().timeFactor(); - this.grid = grid; - this.pulse = problem.getPulse(); - - recalculate(); - - pulse.getPulseShape().init(this); - pulse.addListener(e -> { - recalculate(); - pulse.getPulseShape().init(this); - - }); - } - - /** - * Uses the {@code PulseTemporalShape} of the {@code Pulse} object to calculate - * the laser power at the specified moment of {@code time}. - * - * @param time the time argument - * @return the laser power at the specified moment of {@code time} - */ - - public double laserPowerAt(double time) { - return pulse.evaluateAt(time); - } - - /** - * Recalculates the {@code discretePulseWidth} by calling {@code gridTime} on - * the physical pulse width and {@code timeFactor}. - * - * @see pulse.problem.schemes.Grid.gridTime(double,double) - */ - - public void recalculate() { - final double width = ((Number) pulse.getPulseWidth().getValue()).doubleValue(); - discretePulseWidth = grid.gridTime(width, timeFactor); - } - - /** - * Gets the discrete pulse width defined by {@code DiscretePulse}. - * - * @return a double, representing the discrete pulse width. - */ - - public double getDiscreteWidth() { - return discretePulseWidth; - } - - /** - * Gets the physical {@code Pulse} - * - * @return the {@code Pulse} object - */ - - public Pulse getPulse() { - return pulse; - } - - /** - * Gets the {@code Grid} object used to construct this {@code DiscretePulse} - * - * @return the {@code Grid} object. - */ - - public Grid getGrid() { - return grid; - } - -} \ No newline at end of file +public class DiscretePulse implements Serializable { + + private static final long serialVersionUID = 5826506918603729615L; + private final Grid grid; + private final Pulse pulse; + private final ExperimentalData data; + + private double widthOnGrid; + private double characteristicTime; + private double invTotalEnergy; //normalisation factor + + /** + * This number shows how small the actual pulse may be compared to the + * half-time. If the pulse is shorter than + * tc/{@value WIDTH_TOLERANCE_FACTOR}, it will be replaced + * by a rectangular pulse with the width equal to + * tc/{@value WIDTH_TOLERANCE_FACTOR}. Here + * tc + * is the time factor defined in the {@code Problem} class. + */ + private final static int WIDTH_TOLERANCE_FACTOR = 10000; + + /** + * This creates a one-dimensional discrete pulse on a {@code grid}. + *

+ * The dimensional factor is taken from the {@code problem}, while the + * discrete pulse width (a multiplier of the {@code grid} parameter + * {@code tau} is calculated using the {@code gridTime} method. + *

+ * + * @param problem the problem, used to extract the dimensional time factor + * @param grid a grid used to discretise the {@code pulse} + */ + public DiscretePulse(Problem problem, Grid grid) { + this.grid = grid; + characteristicTime = problem.getProperties().characteristicTime(); + this.pulse = problem.getPulse(); + + Object ancestor + = Objects.requireNonNull(problem.specificAncestor(SearchTask.class), + "Problem has not been assigned to a SearchTask"); + + data = (ExperimentalData) (((SearchTask) ancestor).getInput()); + init(); + + PropertyHolderListener phl = e -> { + characteristicTime = problem.getProperties().characteristicTime(); + widthOnGrid = 0; + init(); + }; + + pulse.addListener(e -> { + widthOnGrid = 0; + init(); + }); + problem.addListener(phl); + + } + + /** + * Uses the {@code PulseTemporalShape} of the {@code Pulse} object to + * calculate the laser power at the specified moment of {@code time}. + * + * @param time the time argument + * @return the laser power at the specified moment of {@code time} + */ + public double laserPowerAt(double time) { + return invTotalEnergy * pulse.getPulseShape().evaluateAt(time); + } + + /** + * Recalculates the {@code discretePulseWidth} by calling {@code gridTime} + * on the physical pulse width and {@code timeFactor}. + * + * @see pulse.problem.schemes.Grid.gridTime(double,double) + */ + 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 && oldValue < EPS) { + //change shape to rectangular + var shape = new RectangularPulse(); + pulse.setPulseShape(shape); + shape.init(null, this); + } else { + pulse.getPulseShape().init(data, this); + } + + invTotalEnergy = 1.0 / totalEnergy(); + } + + /** + * 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(); + + var integrator = new MidpointIntegrator(new Segment(0, widthOnGrid)) { + + @Override + public double integrand(double... vars) { + return pulseShape.evaluateAt(vars[0]); + } + + }; + + return integrator.integrate(); + } + + /** + * 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; + } + + /** + * Gets the physical {@code Pulse} + * + * @return the {@code Pulse} object + */ + public Pulse getPhysicalPulse() { + return pulse; + } + + /** + * Gets the {@code Grid} object used to construct this {@code DiscretePulse} + * + * @return the {@code Grid} object. + */ + 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 + * + * @return the conversion factor + */ + 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 + */ + 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. + */ + public int getWidthToleranceFactor() { + return WIDTH_TOLERANCE_FACTOR; + } + +} diff --git a/src/main/java/pulse/problem/laser/DiscretePulse2D.java b/src/main/java/pulse/problem/laser/DiscretePulse2D.java index 41815356..21be2b89 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse2D.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse2D.java @@ -4,8 +4,8 @@ import pulse.problem.schemes.Grid2D; import pulse.problem.statements.ClassicalProblem2D; -import pulse.problem.statements.ExtendedThermalProperties; import pulse.problem.statements.Pulse2D; +import pulse.problem.statements.model.ExtendedThermalProperties; /** * The discrete pulse on a {@code Grid2D}. @@ -16,69 +16,103 @@ *

* */ - public class DiscretePulse2D extends DiscretePulse { - private double discretePulseSpot; - private double coordFactor; + 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; - /** - * The constructor for {@code DiscretePulse2D}. - *

- * Calls the constructor of the superclass, after which calculates the - * {@code discretePulseSpot} using the {@code gridRadialDistance} method of this - * class. The dimension factor is defined as the sample diameter. - *

- * - * @param problem a two-dimensional problem - * @param grid the two-dimensional grid - */ + /** + * The constructor for {@code DiscretePulse2D}. + *

+ * Calls the constructor of the superclass, after which calculates the + * {@code discretePulseSpot} using the {@code gridRadialDistance} method of + * this class. The dimension factor is defined as the sample diameter. + *

+ * + * @param problem a two-dimensional problem + * @param grid the two-dimensional grid + */ + public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { + super(problem, grid); + var properties = (ExtendedThermalProperties) problem.getProperties(); + calcPulseSpot(properties); + properties.addListener(e -> calcPulseSpot(properties)); + } - public DiscretePulse2D(ClassicalProblem2D problem, Grid2D grid) { - super(problem, grid); - var properties = (ExtendedThermalProperties)problem.getProperties(); - coordFactor = (double) properties.getSampleDiameter().getValue() / 2.0; - var pulse = (Pulse2D)problem.getPulse(); - discretePulseSpot = grid.gridRadialDistance((double) pulse.getSpotDiameter().getValue() / 2.0, coordFactor); + /** + * This calculates the dimensionless, discretised pulse function at a + * dimensionless radial coordinate {@code coord}. + *

+ * It uses a Heaviside function to determine whether the {@code radialCoord} + * lies within the {@code 0 <= radialCoord <= discretePulseSpot} interval. + * It uses the {@code time} parameter to determine the discrete pulse + * function using {@code evaluateAt(time)}.

+ * + * @param time the time for calculation + * @param radialCoord - the radial coordinate [length dimension] + * @return the pulse function at {@code time} and {@code coord}, or 0 if + * {@code coord > spotDiameter}. + * @see pulse.problem.laser.PulseTemporalShape.laserPowerAt(double) + */ + public double evaluateAt(double time, double radialCoord) { + 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. + * + * @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); + } - /** - * This calculates the dimensionless, discretised pulse function at a - * dimensionless radial coordinate {@code coord}. - *

- * It uses a Heaviside function to determine whether the {@code radialCoord} - * lies within the {@code 0 <= radialCoord <= discretePulseSpot} interval. It - * uses the {@code time} parameter to determine the discrete pulse function - * using {@code evaluateAt(time)}. - * - * @param time the time for calculation - * @param radialCoord - the radial coordinate [length dimension] - * @return the pulse function at {@code time} and {@code coord}, or 0 if - * {@code coord > spotDiameter}. - * @see pulse.problem.laser.PulseTemporalShape.laserPowerAt(double) - */ + private void calcPulseSpot(ExtendedThermalProperties properties) { + sampleRadius = (double) properties.getSampleDiameter().getValue() / 2.0; + evalPulseSpot(); + } - public double evaluateAt(double time, double radialCoord) { - return laserPowerAt(time) * (0.5 + 0.5 * signum(discretePulseSpot - radialCoord)); - } + /** + * 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; + discretePulseSpot = grid2d.gridRadialDistance(spotRadius, sampleRadius); + grid2d.adjustStepSize(this); + normFactor = sampleRadius * sampleRadius / spotRadius / spotRadius; + } - /** - * Calls the superclass method, then calculates the {@code discretePulseSpot} - * using the {@code gridRadialDistance} method. - * - * @see pulse.problem.schemes.Grid2D.gridRadialDistance(double,double) - */ + public final double getDiscretePulseSpot() { + return discretePulseSpot; + } - @Override - public void recalculate() { - super.recalculate(); - final double radius = (double) ((Pulse2D) getPulse()).getSpotDiameter().getValue() / 2.0; - discretePulseSpot = ((Grid2D) getGrid()).gridRadialDistance(radius, coordFactor); - } + public final double getRadialConversionFactor() { + return sampleRadius; + } - public double getDiscretePulseSpot() { - return discretePulseSpot; - } + /** + * 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 77f803b0..d4490d2b 100644 --- a/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java +++ b/src/main/java/pulse/problem/laser/ExponentiallyModifiedGaussian.java @@ -6,185 +6,158 @@ import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; import static pulse.properties.NumericPropertyKeyword.SKEW_LAMBDA; import static pulse.properties.NumericPropertyKeyword.SKEW_MU; import static pulse.properties.NumericPropertyKeyword.SKEW_SIGMA; -import java.util.List; +import java.util.Set; -import pulse.math.FixedIntervalIntegrator; -import pulse.math.MidpointIntegrator; -import pulse.math.Segment; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; /** * Represents the exponentially modified Gaussian function, which is given by * three independent parameters (μ, σ and λ). - * + * * @see Wikipedia page * */ - public class ExponentiallyModifiedGaussian extends PulseTemporalShape { - private double mu; - private double sigma; - private double lambda; - private double norm; - - private final static int DEFAULT_POINTS = 100; - private FixedIntervalIntegrator integrator; - - /** - * Creates an exponentially modified Gaussian with the default parameter values. - */ - - public ExponentiallyModifiedGaussian() { - mu = (double) def(SKEW_MU).getValue(); - lambda = (double) def(SKEW_LAMBDA).getValue(); - sigma = (double) def(SKEW_SIGMA).getValue(); - norm = 1.0; - integrator = new MidpointIntegrator(new Segment(0.0, getPulseWidth()), - derive(INTEGRATION_SEGMENTS, DEFAULT_POINTS)) { - - @Override - public double integrand(double... vars) { - return evaluateAt(vars[0]); - } - - }; - } - - /** - * This calls the superclass {@code init method} and sets the normalisation - * factor to 1/∫Φ(Fo)dFo. - */ - - @Override - public void init(DiscretePulse pulse) { - super.init(pulse); - norm = 1.0; // resets the normalisation factor to unity - norm = 1.0 / area(); // calculates the area. The normalisation factor is then set to the inverse of - // the area. - } - - /** - * Uses numeric integration (midpoint rule) to calculate the area of the pulse - * shape corresponding to the selected parameters. - * - * @return the area - */ - - private double area() { - integrator.setBounds(new Segment(0.0, getPulseWidth())); - return integrator.integrate(); - } - - /** - * Evaluates the laser power function. The error function is calculated using - * the ApacheCommonsMath library tools. - * - * @see https://tinyurl.com/ExpModifiedGaussian - * @param time is measured from the 'start' of laser pulse - */ - - @Override - public double evaluateAt(double time) { - final var reducedTime = time / getPulseWidth(); - - final double lambdaHalf = 0.5 * lambda; - final double sigmaSq = sigma * sigma; - - return norm * lambdaHalf * exp(lambdaHalf * (2.0 * mu + lambda * sigmaSq - 2.0 * reducedTime)) - * erfc((mu + lambda * sigmaSq - reducedTime) / (sqrt(2) * sigma)); - - } - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(def(SKEW_MU)); - list.add(def(SKEW_LAMBDA)); - list.add(def(SKEW_SIGMA)); - return list; - } - - /** - * @return the μ parameter - */ - - public NumericProperty getMu() { - return derive(SKEW_MU, mu); - } - - /** - * @return the σ parameter - */ - - public NumericProperty getSigma() { - return derive(SKEW_SIGMA, sigma); - } - - /** - * @return the λ parameter - */ - - public NumericProperty getLambda() { - return derive(SKEW_LAMBDA, lambda); - } - - /** - * Sets the {@code SKEW_LAMBDA} parameter - * - * @param p the λ parameter - */ - - public void setLambda(NumericProperty p) { - requireType(p, SKEW_LAMBDA); - this.lambda = (double) p.getValue(); - } - - /** - * Sets the {@code SKEW_MU} parameter - * - * @param p the μ parameter - */ - - public void setMu(NumericProperty p) { - requireType(p, SKEW_MU); - this.mu = (double) p.getValue(); - } - - /** - * Sets the {@code SKEW_SIGMA} parameter - * - * @param p the σ parameter - */ - - public void setSigma(NumericProperty p) { - requireType(p, SKEW_SIGMA); - this.sigma = (double) p.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case SKEW_MU: - setMu(property); - break; - case SKEW_LAMBDA: - setLambda(property); - break; - case SKEW_SIGMA: - setSigma(property); - break; - default: - break; - } - firePropertyChanged(this, property); - } - -} \ No newline at end of file + private static final long serialVersionUID = -4437794069527301235L; + private double mu; + private double sigma; + private double lambda; + + private final static int MIN_POINTS = 10; + + /** + * Creates an exponentially modified Gaussian with the default parameter + * values. + */ + public ExponentiallyModifiedGaussian() { + mu = (double) def(SKEW_MU).getValue(); + lambda = (double) def(SKEW_LAMBDA).getValue(); + sigma = (double) def(SKEW_SIGMA).getValue(); + } + + public ExponentiallyModifiedGaussian(ExponentiallyModifiedGaussian another) { + super(another); + this.mu = another.mu; + this.sigma = another.sigma; + this.lambda = another.lambda; + } + + /** + * Evaluates the laser power function. The error function is calculated + * using the ApacheCommonsMath library tools. + * + * @see https://tinyurl.com/ExpModifiedGaussian + * @param time is measured from the 'start' of laser pulse + */ + @Override + public double evaluateAt(double time) { + final var reducedTime = time / getPulseWidth(); + + final double lambdaHalf = 0.5 * lambda; + final double sigmaSq = sigma * sigma; + + return lambdaHalf * exp(lambdaHalf * (2.0 * mu + lambda * sigmaSq - 2.0 * reducedTime)) + * erfc((mu + lambda * sigmaSq - reducedTime) / (sqrt(2) * sigma)); + + } + + /** + * @see pulse.properties.NumericPropertyKeyword.SKEW_MU + * @see pulse.properties.NumericPropertyKeyword.SKEW_LAMBDA + * @see pulse.properties.NumericPropertyKeyword.SKEW_SIGMA + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SKEW_MU); + set.add(SKEW_LAMBDA); + set.add(SKEW_SIGMA); + return set; + } + + /** + * @return the μ parameter + */ + public NumericProperty getMu() { + return derive(SKEW_MU, mu); + } + + /** + * @return the σ parameter + */ + public NumericProperty getSigma() { + return derive(SKEW_SIGMA, sigma); + } + + /** + * @return the λ parameter + */ + public NumericProperty getLambda() { + return derive(SKEW_LAMBDA, lambda); + } + + /** + * Sets the {@code SKEW_LAMBDA} parameter + * + * @param p the λ parameter + */ + public void setLambda(NumericProperty p) { + requireType(p, SKEW_LAMBDA); + this.lambda = (double) p.getValue(); + } + + /** + * Sets the {@code SKEW_MU} parameter + * + * @param p the μ parameter + */ + public void setMu(NumericProperty p) { + requireType(p, SKEW_MU); + this.mu = (double) p.getValue(); + } + + /** + * Sets the {@code SKEW_SIGMA} parameter + * + * @param p the σ parameter + */ + public void setSigma(NumericProperty p) { + requireType(p, SKEW_SIGMA); + this.sigma = (double) p.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case SKEW_MU: + setMu(property); + break; + case SKEW_LAMBDA: + setLambda(property); + break; + case SKEW_SIGMA: + setSigma(property); + break; + default: + break; + } + firePropertyChanged(this, property); + } + + @Override + public PulseTemporalShape copy() { + return new ExponentiallyModifiedGaussian(this); + } + + @Override + public int getRequiredDiscretisation() { + return MIN_POINTS; + } + +} diff --git a/src/main/java/pulse/problem/laser/NumericPulse.java b/src/main/java/pulse/problem/laser/NumericPulse.java new file mode 100644 index 00000000..a71d65fd --- /dev/null +++ b/src/main/java/pulse/problem/laser/NumericPulse.java @@ -0,0 +1,176 @@ +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; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.tasks.SearchTask; + +import pulse.baseline.FlatBaseline; +import pulse.tasks.Calculation; +import pulse.util.FunctionSerializer; + +/** + * A numeric pulse is given by a set of discrete {@code NumericPulseData} + * measured independently using a pulse diode. + * + * @see pulse.problem.laser.NumericPulseData + * + */ +public class NumericPulse extends PulseTemporalShape { + + private static final long serialVersionUID = 6088261629992349844L; + private NumericPulseData pulseData; + private transient UnivariateFunction interpolation; + + private final static int MIN_POINTS = 20; + + public NumericPulse() { + //intentionally blank + } + + /** + * Copy constructor + * + * @param pulse another numeric pulse, the data of which will be copied + */ + public NumericPulse(NumericPulse pulse) { + super(pulse); + this.pulseData = new NumericPulseData(pulseData); + } + + /** + * Defines the pulse width as the last element of the time sequence + * contained in {@code NumericPulseData}. Calls {@code super.init}, then + * interpolates the input pulse using spline functions and normalises the + * output. + * + * @param data + * @see normalise() + * + */ + @Override + 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(); + 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. + */ + 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, + 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 pulseObject = problem.getPulse(); + pulseObject.setPulseWidth(derive(PULSE_WIDTH, pulseWidth)); + + } + + private void doInterpolation(double timeFactor) { + var interpolator = new AkimaSplineInterpolator(); + + 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()); + } + + /** + * If the argument is less than the pulse width, uses a spline to + * interpolate the pulse data at {@code time}. Otherwise returns zero. + */ + @Override + public double evaluateAt(double time) { + return time > getPulseWidth() ? 0.0 : interpolation.value(time); + } + + @Override + public PulseTemporalShape copy() { + return new NumericPulse(); + } + + /** + * Does not define any property. + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // TODO Auto-generated method stub + } + + public NumericPulseData getData() { + return pulseData; + } + + public void setData(NumericPulseData pulseData) { + this.pulseData = pulseData; + + } + + public UnivariateFunction getInterpolation() { + return interpolation; + } + + @Override + 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 new file mode 100644 index 00000000..7bbfeeed --- /dev/null +++ b/src/main/java/pulse/problem/laser/NumericPulseData.java @@ -0,0 +1,78 @@ +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 + * {@code externalID}. Use to store numeric data of the pulse for each + * measurement imported from an external source. + * + */ +public class NumericPulseData extends AbstractData implements DiscreteInput { + + private static final long serialVersionUID = 8142129124831241206L; + private final int externalID; + + /** + * Stores {@code id} and calls super-constructor + * + * @param id an external ID defined in the imported file + */ + public NumericPulseData(int id) { + super(); + this.externalID = id; + } + + /** + * Copies everything, including the id number. + * + * @param data another object + */ + public NumericPulseData(NumericPulseData data) { + super(data); + this.externalID = data.externalID; + } + + /** + * Adds a data point to the internal storage and increments counter. + */ + @Override + public void addPoint(double time, double power) { + super.addPoint(time, power); + super.incrementCount(); + } + + /** + * Gets the external ID usually specified in the experimental files. Note + * this is not a {@code NumericProperty} + * + * @return an integer, representing the external ID + */ + public int getExternalID() { + return externalID; + } + + 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); + } + +} diff --git a/src/main/java/pulse/problem/laser/PulseTemporalShape.java b/src/main/java/pulse/problem/laser/PulseTemporalShape.java index ed24ec38..9a74c09c 100644 --- a/src/main/java/pulse/problem/laser/PulseTemporalShape.java +++ b/src/main/java/pulse/problem/laser/PulseTemporalShape.java @@ -1,50 +1,69 @@ package pulse.problem.laser; +import pulse.input.ExperimentalData; import pulse.util.PropertyHolder; import pulse.util.Reflexive; /** * An abstract time-dependent pulse shape. Declares the abstract method to * calculate the laser power function at a given moment of time. This generally - * utilises a discrete pulse width. + * utilises a discrete pulse width. By default, uses a midpoint-rule numeric + * integrator to calculate the pulse integral. * */ - public abstract class PulseTemporalShape extends PropertyHolder implements Reflexive { - private double width; + private double width; + + public PulseTemporalShape() { + //intentionlly blank + } + + 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. + * + * @param time the dimensionless time (a multiplier of {@code tau}), at + * which calculation should be performed + * @return a double value, representing the pulse function at {@code time} + */ + public abstract double evaluateAt(double time); - /** - * This evaluates the dimensionless, discretised pulse function on a - * {@code grid} needed to evaluate the heat source in the difference scheme. - * - * @param time the dimensionless time (a multiplier of {@code tau}), at which - * calculation should be performed - * @return a double value, representing the pulse function at {@code time} - */ + /** + * Stores the pulse width from {@code pulse} and initialises area + * integration. + * + * @param data + * @param pulse the discrete pulse containing the pulse width + */ + public void init(ExperimentalData data, DiscretePulse pulse) { + width = pulse.getDiscreteWidth(); + } - public abstract double evaluateAt(double time); + public abstract PulseTemporalShape copy(); - public void init(DiscretePulse pulse) { - width = pulse.getDiscreteWidth(); - } + @Override + public String getPrefix() { + return "Pulse temporal shape"; + } - @Override - public String getPrefix() { - return "Pulse temporal shape"; - } + @Override + public String toString() { + return getClass().getSimpleName(); + } - @Override - public String toString() { - return getClass().getSimpleName(); - } + public double getPulseWidth() { + return width; + } - public double getPulseWidth() { - return width; - } + public void setPulseWidth(double width) { + this.width = width; + } - 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 fa336720..7bc5dfbd 100644 --- a/src/main/java/pulse/problem/laser/RectangularPulse.java +++ b/src/main/java/pulse/problem/laser/RectangularPulse.java @@ -9,25 +9,37 @@ * The simplest pulse shape defined as 0.5*(1 + * sgn(tpulse - t)), where sgn(...) * is the signum function, pulse is the pulse width. - * + * * @see java.lang.Math.signum(double) */ - public class RectangularPulse extends PulseTemporalShape { - /** - * @param time the time measured from the start of the laser pulse. - */ - - @Override - public double evaluateAt(double time) { - var width = getPulseWidth(); - return 0.5 / width * (1 + signum(width - time)); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally blak - } - -} \ No newline at end of file + 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 + */ + @Override + public double evaluateAt(double time) { + var width = getPulseWidth(); + return 0.5 / width * (1 + signum(width - time)); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally blak + } + + @Override + public PulseTemporalShape copy() { + return new RectangularPulse(); + } + + @Override + public int getRequiredDiscretisation() { + return MIN_POINTS; + } + +} diff --git a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java index 570af2f9..908e2cfc 100644 --- a/src/main/java/pulse/problem/laser/TrapezoidalPulse.java +++ b/src/main/java/pulse/problem/laser/TrapezoidalPulse.java @@ -6,122 +6,130 @@ import static pulse.properties.NumericPropertyKeyword.TRAPEZOIDAL_FALL_PERCENTAGE; import static pulse.properties.NumericPropertyKeyword.TRAPEZOIDAL_RISE_PERCENTAGE; -import java.util.List; +import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; /** * A trapezoidal pulse shape, which combines a rise segment, a constant-power * segment, and a fall segment. The rise and fall ratios can be changed. * */ - public class TrapezoidalPulse extends PulseTemporalShape { - private double rise; - private double fall; - private double h; - - /** - * Constructs a trapezoidal pulse using a default segmentation principle. The - * reader is referred to the {@code .xml} file containing the default values of - * {@code TRAPEZOIDAL_RISE_PERCENTAGE} and {@code TRAPEZOIDAL_FALL_PERCENTAGE}. - * The maximum laser power is adjusted to ensure the area of the shape is equal - * to unity. - */ - - public TrapezoidalPulse() { - rise = (int) def(TRAPEZOIDAL_RISE_PERCENTAGE).getValue() / 100.0; - fall = (int) def(TRAPEZOIDAL_FALL_PERCENTAGE).getValue() / 100.0; - h = height(); - } - - @Override - public void init(DiscretePulse pulse) { - super.init(pulse); - h = height(); - } - - /** - * Calculates the height of the trapezium which under current segmentation will - * yield an area of unity. - * - * @return the calculated height of the constant segmment - */ - - private double height() { - return 2.0 / (getPulseWidth() * (2.0 - rise - fall)); - } - - /** - * Calculates power using a piecewise function, which corresponds to either a - * linearly changing, a constant laser power or zero. - * - * @param time the time measured from the start of the laser pulse. - */ - - @Override - public double evaluateAt(double time) { - final var reducedTime = time / getPulseWidth(); - - double result = 0; - - if (reducedTime < rise) { // triangular - result = reducedTime * h / rise; - } else if (reducedTime < 1.0 - fall) { // rectangular - result = h; - } else if (reducedTime < 1.0) { // triangular - final var t2 = (reducedTime - (1.0 - fall)); - result = (fall - t2) * h / fall; - } - - return result; - - } - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(def(TRAPEZOIDAL_RISE_PERCENTAGE)); - list.add(def(TRAPEZOIDAL_FALL_PERCENTAGE)); - return list; - } - - public NumericProperty getTrapezoidalRise() { - return derive(TRAPEZOIDAL_RISE_PERCENTAGE, (int) (rise * 100)); - } - - public NumericProperty getTrapezoidalFall() { - return derive(TRAPEZOIDAL_FALL_PERCENTAGE, (int) (fall * 100)); - } - - public void setTrapezoidalRise(NumericProperty p) { - requireType(p, TRAPEZOIDAL_RISE_PERCENTAGE); - this.rise = (int) p.getValue() / 100.0; - h = height(); - } - - public void setTrapezoidalFall(NumericProperty p) { - requireType(p, TRAPEZOIDAL_FALL_PERCENTAGE); - this.fall = (int) p.getValue() / 100.0; - h = height(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case TRAPEZOIDAL_RISE_PERCENTAGE: - setTrapezoidalRise(property); - break; - case TRAPEZOIDAL_FALL_PERCENTAGE: - setTrapezoidalFall(property); - break; - default: - break; - } - firePropertyChanged(this, property); - } - -} \ No newline at end of file + 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 + * values of {@code TRAPEZOIDAL_RISE_PERCENTAGE} and + * {@code TRAPEZOIDAL_FALL_PERCENTAGE}. The maximum laser power is adjusted + * to ensure the area of the shape is equal to unity. + */ + public TrapezoidalPulse() { + rise = (int) def(TRAPEZOIDAL_RISE_PERCENTAGE).getValue() / 100.0; + fall = (int) def(TRAPEZOIDAL_FALL_PERCENTAGE).getValue() / 100.0; + h = 1.0; + } + + public TrapezoidalPulse(TrapezoidalPulse another) { + this.rise = another.rise; + this.fall = another.fall; + this.h = another.h; + } + + /** + * Calculates the height of the trapezium which under current segmentation + * will yield an area of unity. + * + * @return the calculated height of the constant segmment + */ + private double height() { + return 2.0 / (getPulseWidth() * (2.0 - rise - fall)); + } + + /** + * Calculates power using a piecewise function, which corresponds to either + * a linearly changing, a constant laser power or zero. + * + * @param time the time measured from the start of the laser pulse. + */ + @Override + public double evaluateAt(double time) { + final var reducedTime = time / getPulseWidth(); + + double result = 0; + + if (reducedTime < rise) { // triangular + result = reducedTime * h / rise; + } else if (reducedTime < 1.0 - fall) { // rectangular + result = h; + } else if (reducedTime < 1.0) { // triangular + final var t2 = (reducedTime - (1.0 - fall)); + result = (fall - t2) * h / fall; + } + + return result; + + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(TRAPEZOIDAL_RISE_PERCENTAGE); + set.add(TRAPEZOIDAL_FALL_PERCENTAGE); + return set; + } + + public NumericProperty getTrapezoidalRise() { + return derive(TRAPEZOIDAL_RISE_PERCENTAGE, (int) (rise * 100)); + } + + public NumericProperty getTrapezoidalFall() { + return derive(TRAPEZOIDAL_FALL_PERCENTAGE, (int) (fall * 100)); + } + + public void setTrapezoidalRise(NumericProperty p) { + requireType(p, TRAPEZOIDAL_RISE_PERCENTAGE); + this.rise = (int) p.getValue() / 100.0; + h = height(); + } + + public void setTrapezoidalFall(NumericProperty p) { + requireType(p, TRAPEZOIDAL_FALL_PERCENTAGE); + this.fall = (int) p.getValue() / 100.0; + h = height(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case TRAPEZOIDAL_RISE_PERCENTAGE: + setTrapezoidalRise(property); + break; + case TRAPEZOIDAL_FALL_PERCENTAGE: + setTrapezoidalFall(property); + break; + default: + break; + } + firePropertyChanged(this, property); + } + + @Override + public PulseTemporalShape copy() { + return new TrapezoidalPulse(this); + } + + @Override + public int getRequiredDiscretisation() { + return MIN_POINTS; + } + +} diff --git a/src/main/java/pulse/problem/laser/package-info.java b/src/main/java/pulse/problem/laser/package-info.java index 21573e60..3e4cf687 100644 --- a/src/main/java/pulse/problem/laser/package-info.java +++ b/src/main/java/pulse/problem/laser/package-info.java @@ -1,5 +1,4 @@ /** * This package deals with discrete laser pulse representation and their various temporal shapes. */ - -package pulse.problem.laser; \ No newline at end of file +package pulse.problem.laser; diff --git a/src/main/java/pulse/problem/schemes/ADIScheme.java b/src/main/java/pulse/problem/schemes/ADIScheme.java index 75e989c1..b20b34db 100644 --- a/src/main/java/pulse/problem/schemes/ADIScheme.java +++ b/src/main/java/pulse/problem/schemes/ADIScheme.java @@ -12,54 +12,66 @@ * needed to solve a {@code Problem}. * */ - public abstract class ADIScheme extends DifferenceScheme { - /** - * Creates a new {@code ADIScheme} with default values of grid density and time - * factor. - */ - - public ADIScheme() { - this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 1.0)); - } - - /** - * Creates an {@code ADIScheme} with the specified arguments. This creates an - * associated {@code Grid2D} object. - * - * @param N the grid density - * @param timeFactor the time factor (τF) - */ + /** + * + */ + private static final long serialVersionUID = 4772650159522354367L; - public ADIScheme(NumericProperty N, NumericProperty timeFactor) { - super(); - setGrid(new Grid2D(N, timeFactor)); - } + /** + * Creates a new {@code ADIScheme} with default values of grid density and + * time factor. + */ + public ADIScheme() { + this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 0.5)); + } - /** - * Creates an {@code ADIScheme} with the specified arguments. This creates an - * associated {@code Grid2D} object. - * - * @param N the grid density - * @param timeFactor the time factor (τF) - * @param timeLimit a custom time limit (tlim) - */ + /** + * Creates an {@code ADIScheme} with the specified arguments. This creates + * an associated {@code Grid2D} object. + * + * @param N the grid density + * @param timeFactor the time factor (τF) + */ + public ADIScheme(NumericProperty N, NumericProperty timeFactor) { + super(); + setGrid(new Grid2D(N, timeFactor)); + } - public ADIScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - setTimeLimit(timeLimit); - setGrid(new Grid2D(N, timeFactor)); - } + /** + * Creates an {@code ADIScheme} with the specified arguments. This creates + * an associated {@code Grid2D} object. + * + * @param N the grid density + * @param timeFactor the time factor (τF) + * @param timeLimit a custom time limit (tlim) + */ + public ADIScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + setTimeLimit(timeLimit); + setGrid(new Grid2D(N, timeFactor)); + } - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ADIScheme.4"); + } - @Override - 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 + * + * @param m thte time step + */ + @Override + public void prepareStep(int m) { + //do nothing + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java index a1530d34..450db9db 100644 --- a/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/BlockMatrixAlgorithm.java @@ -1,79 +1,85 @@ package pulse.problem.schemes; /** - * A modification of the algorithm for solving a system of linear equations, where the first and last equation contains references - * to the last and first elements of the solution, respectively. The corresponding matrix is composed of an inner tridiagonal block - * and a border formed by an extra row and column. This block system is solved using the Sherman-Morrison-Woodbury identity and the - * Thomas algorithm for the main block. + * A modification of the algorithm for solving a system of linear equations, + * where the first and last equation contains references to the last and first + * elements of the solution, respectively. The corresponding matrix is composed + * of an inner tridiagonal block and a border formed by an extra row and column. + * This block system is solved using the Sherman-Morrison-Woodbury identity and + * the Thomas algorithm for the main block. * */ - public class BlockMatrixAlgorithm extends TridiagonalMatrixAlgorithm { - private double[] gamma; - private double[] p; - private double[] q; - - public BlockMatrixAlgorithm(Grid grid) { - super(grid); - gamma = new double[getAlpha().length]; - p = new double[gamma.length - 1]; - q = new double[gamma.length - 1]; - } - - @Override - public void sweep(double[] V) { - final int N = V.length - 1; - for (int j = N - 1; j >= 0; j--) - V[j] = p[j] + V[N] * q[j]; - } - - @Override - public void evaluateBeta(final double[] U) { - super.evaluateBeta(U); - final int N = getGrid().getGridDensityValue(); - var alpha = getAlpha(); - var beta = getBeta(); - - p[N - 1] = beta[N]; - q[N - 1] = alpha[N] + gamma[N]; - - for (int i = N - 2; i >= 0; i--) { - p[i] = alpha[i + 1] * p[i + 1] + beta[i + 1]; - q[i] = alpha[i + 1] * q[i + 1] + gamma[i + 1]; - } - } - - @Override - public void evaluateBeta(final double[] U, final int start, final int endExclusive) { - var alpha = getAlpha(); - var grid = getGrid(); - final double HX2_TAU = grid.getXStep() * grid.getXStep() / getGrid().getTimeStep(); - - final double a = getCoefA(); - final double b = getCoefB(); - - for (int i = start; i < endExclusive; i++) { - setBeta(i, beta(U[i - 1] * HX2_TAU, phi(i - 1), i)); - setGamma(i, a * gamma[i - 1] / (b - a * alpha[i - 1])); - } - - } - - public double[] getP() { - return p; - } - - public double[] getQ() { - return q; - } - - public void setGamma(final int i, final double g) { - this.gamma[i] = g; - } - - public double[] getGamma() { - return gamma; - } + private static final long serialVersionUID = -6553638438386098008L; + private final double[] gamma; + private final double[] p; + private final double[] q; + + public BlockMatrixAlgorithm(Grid grid) { + super(grid); + final int N = this.getGridPoints(); + gamma = new double[N + 2]; + p = new double[N]; + q = new double[N]; + } + + @Override + public void sweep(double[] V) { + final int N = V.length - 1; + for (int j = N - 1; j >= 0; j--) { + V[j] = p[j] + V[N] * q[j]; + } + } + + @Override + public void evaluateBeta(final double[] U) { + super.evaluateBeta(U); + var alpha = getAlpha(); + var beta = getBeta(); + + final int N = getGridPoints(); + + p[N - 1] = beta[N]; + q[N - 1] = alpha[N] + gamma[N]; + + for (int i = N - 2; i >= 0; i--) { + p[i] = alpha[i + 1] * p[i + 1] + beta[i + 1]; + q[i] = alpha[i + 1] * q[i + 1] + gamma[i + 1]; + } + } + + @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(); + + final double a = getCoefA(); + final double b = getCoefB(); + + for (int i = start; i < endExclusive; i++) { + setBeta(i, beta(U[i - 1] * HX2_TAU, phi(i - 1), i)); + setGamma(i, a * gamma[i - 1] / (b - a * alpha[i - 1])); + } + + } + + public double[] getP() { + return p; + } + + public double[] getQ() { + return q; + } + + public void setGamma(final int i, final double g) { + this.gamma[i] = g; + } + + public double[] getGamma() { + return gamma; + } } diff --git a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java index 06ff68bd..17976a27 100644 --- a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java @@ -1,113 +1,90 @@ package pulse.problem.schemes; -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import java.util.List; +import java.util.Set; 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; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; - -public abstract class CoupledImplicitScheme extends ImplicitScheme implements FixedPointIterations { - - private RadiativeTransferCoupling coupling; - private RTECalculationStatus calculationStatus; - private double nonlinearPrecision; - - private double pls; - - public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor) { - super(); - setGrid(new Grid(N, timeFactor)); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - setCoupling(new RadiativeTransferCoupling()); - calculationStatus = RTECalculationStatus.NORMAL; - } - - public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - this(N, timeFactor); - setTimeLimit(timeLimit); - } - - @Override - public void timeStep(final int m) { - pls = pulse(m); - doIterations(getCurrentSolution(), nonlinearPrecision, m); - } - - @Override - public void iteration(final int m) { - super.timeStep(m); - } - - @Override - public void finaliseIteration(double[] V) { - setCalculationStatus( coupling.getRadiativeTransferEquation().compute(V) ); - } - - public RadiativeTransferCoupling getCoupling() { - return coupling; - } - - public void setCoupling(RadiativeTransferCoupling coupling) { - this.coupling = coupling; - this.coupling.setParent(this); - } - - @Override - public void finaliseStep() { - super.finaliseStep(); - coupling.getRadiativeTransferEquation().getFluxes().store(); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(NONLINEAR_PRECISION)); - return list; - } - - public NumericProperty getNonlinearPrecision() { - return derive(NONLINEAR_PRECISION, nonlinearPrecision); - } - - public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { - this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); - } - - @Override - public Class domain() { - return ParticipatingMedium.class; - } - - @Override - public boolean normalOperation() { - return super.normalOperation() && (getCalculationStatus() == RTECalculationStatus.NORMAL); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == NONLINEAR_PRECISION) { - setNonlinearPrecision(property); - } else - super.set(type, property); - } - - public RTECalculationStatus getCalculationStatus() { - return calculationStatus; - } - - public void setCalculationStatus(RTECalculationStatus calculationStatus) { - this.calculationStatus = calculationStatus; - } - - public double getCurrentPulseValue() { - return pls; - } - -} \ No newline at end of file + +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 + + public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor) { + super(); + setGrid(new Grid(N, timeFactor)); + setCoupling(new RadiativeTransferCoupling()); + calculationStatus = RTECalculationStatus.NORMAL; + } + + public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + this(N, timeFactor); + setTimeLimit(timeLimit); + } + + @Override + public void finaliseStep() throws SolverException { + super.finaliseStep(); + if (autoUpdateFluxes) { + var rte = this.getCoupling().getRadiativeTransferEquation(); + setCalculationStatus(rte.compute(getCurrentSolution())); + } + coupling.getRadiativeTransferEquation().getFluxes().store(); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(NONLINEAR_PRECISION); + return set; + } + + @Override + public boolean normalOperation() { + return super.normalOperation() && (getCalculationStatus() == RTECalculationStatus.NORMAL); + } + + public final RTECalculationStatus getCalculationStatus() { + return calculationStatus; + } + + public final void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { + this.calculationStatus = calculationStatus; + if (calculationStatus != RTECalculationStatus.NORMAL) { + throw new SolverException(calculationStatus.toString(), + RTE_SOLVER_ERROR); + } + } + + public final RadiativeTransferCoupling getCoupling() { + return coupling; + } + + 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}; + } + +} diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 5bb773df..64de3f8c 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -1,18 +1,18 @@ 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; import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; -import java.util.ArrayList; -import java.util.List; +import java.util.Set; import pulse.problem.laser.DiscretePulse; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -23,288 +23,304 @@ * partitioning, adjusted to ensure a stable or conditionally-stable behaviour * of the solution. The {@code Grid} is also used to define a * {@code DiscretePulse} function. - * + * * @see pulse.problem.schemes.Grid * @see pulse.problem.laser.DiscretePulse */ - public abstract class DifferenceScheme extends PropertyHolder implements Reflexive { - private DiscretePulse discretePulse; - private Grid grid; - - private double timeLimit; - private int timeInterval; - - private static boolean hideDetailedAdjustment = true; - - private final static double EPS = 1e-7; // a small value ensuring numeric stability - - /** - * A constructor which merely sets the time limit to its default value. - */ - - protected DifferenceScheme() { - setTimeLimit(def(TIME_LIMIT)); - } - - /** - * A constructor for setting the time limit to a pre-set value. - * - * @param timeLimit the calculation time limit - */ - - protected DifferenceScheme(NumericProperty timeLimit) { - setTimeLimit(timeLimit); - } - - /** - * Used to get a class of problems on which this difference scheme is - * applicable. - * - * @return a subclass of the {@code Problem} class which can be used as input - * for this difference scheme. - */ - - public abstract Class domain(); - - /** - * Creates a {@code DifferenceScheme}, which is an exact copy of this object. - * - * @return an exact copy of this {@code DifferenceScheme}. - */ - - public abstract DifferenceScheme copy(); - - /** - * Copies the {@code Grid} and {@code timeLimit} from {@code df}. - * - * @param df the DifferenceScheme to copy from - */ - - public void copyFrom(DifferenceScheme df) { - this.grid = df.getGrid().copy(); - discretePulse = null; - timeLimit = df.timeLimit; - } - - /** - *

- * Contains preparatory steps to ensure smooth running of the solver. This - * includes creating a {@code DiscretePulse} object and calculating the - * {@code timeInterval}. The latter determines the real-time calculation of a - * {@code HeatingCurve} based on the numerical solution of {@code problem}; it - * thus takes into account the difference between the scheme timestep and the - * {@code HeatingCurve} point spacing. All subclasses of - * {@code DifferenceScheme} should override and explicitly call this superclass - * method where appropriate. - *

- * - * @param problem the heat problem to be solved - */ - - protected void prepare(Problem problem) { - discretePulse = problem.discretePulseOn(grid); - grid.adjustTo(discretePulse); - - var hc = problem.getHeatingCurve(); - hc.clear(); - } - - public void runTimeSequence(Problem problem) { - runTimeSequence(problem, 0, timeLimit); - var curve = problem.getHeatingCurve(); - final double maxTemp = (double) problem.getProperties().getMaximumTemperature().getValue(); - curve.scale(maxTemp / curve.apparentMaximum() ); - } - - public void runTimeSequence(Problem problem, final double offset, final double endTime) { - final var grid = getGrid(); - - var curve = problem.getHeatingCurve(); - - int adjustedNumPoints = (int)curve.getNumPoints().getValue(); - - final double startTime = (double)curve.getTimeShift().getValue(); - final double timeSegment = (endTime - startTime - offset) / problem.getProperties().timeFactor(); - final double tau = grid.getTimeStep(); - - for (double dt = 0, factor = 1.0; dt < tau; adjustedNumPoints *= factor) { - dt = timeSegment / (adjustedNumPoints - 1); - factor = dt / tau; - timeInterval = (int) factor; - } - - final double wFactor = timeInterval * tau * problem.getProperties().timeFactor(); - - // First point (index = 0) is always (0.0, 0.0) - - /* - * The outer cycle iterates over the number of points of the HeatingCurve - */ - - double nextTime = offset + wFactor; - curve.addPoint(0.0, 0.0); - - for (int w = 1; nextTime < 1.01*endTime; nextTime = offset + (++w)*wFactor) { - - /* - * Two adjacent points of the heating curves are separated by timeInterval on - * the time grid. Thus, to calculate the next point on the heating curve, - * timeInterval/tau time steps have to be made first. - */ - - timeSegment((w - 1) * timeInterval + 1, w * timeInterval + 1); - curve.addPoint(nextTime, signal()); - - } - - } - - private void timeSegment(final int m1, final int m2) { - for (int m = m1; m < m2 && normalOperation(); m++) { - timeStep(m); - finaliseStep(); - } - } - - public double pulse(final int m) { - return getDiscretePulse().laserPowerAt((m - EPS) * getGrid().getTimeStep()); - } - - public abstract double signal(); - - public abstract void timeStep(final int m); - - public abstract void finaliseStep(); - - public boolean normalOperation() { - return true; - } - - /** - * The superclass only lists the {@code TIME_LIMIT} property. - */ - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.add(def(TIME_LIMIT)); - return list; - } - - @Override - public String toString() { - return this.getClass().getSimpleName(); - } - - /** - * Gets the discrete representation of {@code Pulse} on the {@code Grid}. - * - * @return the discrete pulse - * @see pulse.problem.statements.Pulse - */ - - public DiscretePulse getDiscretePulse() { - return discretePulse; - } - - /** - * Gets the {@code Grid} object defining partioning used in this - * {@code DifferenceScheme} - * - * @return the grid - */ - - public Grid getGrid() { - return grid; - } - - /** - * Sets the grid and adopts it as its child. - * - * @param grid the grid - */ - - public void setGrid(Grid grid) { - this.grid = grid; - this.grid.setParent(this); - } - - /** - * The time interval is the number of discrete timesteps that will be discarded - * when storing the resulting solution into a {@code HeatingCurve} object, thus - * ensuring that only a limited set of points is stored. - * - * @return the time interval - */ - - public int getTimeInterval() { - return timeInterval; - } - - /** - * Sets the time interval to the argument of this method. - * - * @param timeInterval a positive integer. - */ - - public void setTimeInterval(int timeInterval) { - this.timeInterval = timeInterval; - } - - /** - * If true, Lets the UI know that the user only wants to have the most important - * properties displayed. Otherwise this will signal all properties need to be - * displayed. - */ - - @Override - public boolean areDetailsHidden() { - return hideDetailedAdjustment; - } - - /** - * Changes the policy of displaying a detailed information about this scheme. - * - * @param b a boolean. - */ - - public static void setDetailsHidden(boolean b) { - hideDetailedAdjustment = b; - } - - /** - * The time limit (in whatever units this {@code DifferenceScheme} uses to - * process the solution), which serves as the ultimate breakpoint for the - * calculations. - * - * @return the {@code NumericProperty} with the type {@code TIME_LIMIT} - * @see pulse.properties.NumericPropertyKeyword - */ - - public NumericProperty getTimeLimit() { - return derive(TIME_LIMIT, timeLimit); - } - - /** - * Sets the time limit (in units defined by the corresponding - * {@code NumericProperty}), which serves as the breakpoint for the - * calculations. - * - * @param timeLimit the {@code NumericProperty} with the type {@code TIME_LIMIT} - * @see pulse.properties.NumericPropertyKeyword - */ - - public void setTimeLimit(NumericProperty timeLimit) { - requireType(timeLimit, TIME_LIMIT); - this.timeLimit = (double) timeLimit.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == TIME_LIMIT) - setTimeLimit(property); - } - -} \ No newline at end of file + private transient DiscretePulse discretePulse; + private Grid grid; + + private double timeLimit; + private double pls; + private int timeInterval; + + private static boolean hideDetailedAdjustment = true; + + private final static double EPS = 1e-7; // a small value ensuring numeric stability + + /** + * A constructor which merely sets the time limit to its default value. + */ + protected DifferenceScheme() { + setTimeLimit(def(TIME_LIMIT)); + } + + /** + * A constructor for setting the time limit to a pre-set value. + * + * @param timeLimit the calculation time limit + */ + protected DifferenceScheme(NumericProperty timeLimit) { + setTimeLimit(timeLimit); + } + + public void initFrom(DifferenceScheme another) { + this.grid = grid.copy(); + this.timeLimit = another.timeLimit; + this.timeInterval = another.timeInterval; + } + + /** + * Copies the {@code Grid} and {@code timeLimit} from {@code df}. + * + * @param df the DifferenceScheme to copy from + */ + public void copyFrom(DifferenceScheme df) { + this.grid = df.getGrid().copy(); + discretePulse = null; + timeLimit = df.timeLimit; + } + + /** + *

+ * Contains preparatory steps to ensure smooth running of the solver.This + * includes creating a {@code DiscretePulse}object and adjusting the grid of + * this scheme to match the {@code DiscretePulse}created for this + * {@code problem} Finally, a heating curve is cleared from the previously + * calculated values.

+ *

+ * All subclasses of {@code DifferenceScheme} should override and explicitly + * call this superclass method where appropriate. + *

+ * + * @param problem the heat problem to be solved + * @throws pulse.problem.schemes.solvers.SolverException + * @see pulse.problem.schemes.Grid.adjustTo() + */ + protected void prepare(Problem problem) throws SolverException { + if (discretePulse == null) { + discretePulse = problem.discretePulseOn(grid); + } + discretePulse.init(); + clearArrays(); + } + + public void runTimeSequence(Problem problem) throws SolverException { + runTimeSequence(problem, 0, timeLimit); + } + + public void scaleSolution(Problem problem) { + var curve = problem.getHeatingCurve(); + final double maxTemp = (double) problem.getProperties().getMaximumTemperature().getValue(); + //curve.scale(maxTemp / curve.apparentMaximum()); + curve.scale(maxTemp); + } + + public void runTimeSequence(Problem problem, final double offset, final double endTime) throws SolverException { + var curve = problem.getHeatingCurve(); + curve.clear(); + + int numPoints = (int) curve.getNumPoints().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); + + double wFactor = timeInterval * tau * problem.getProperties().characteristicTime(); + + // First point (index = 0) is always (0.0, 0.0) + curve.addPoint(0.0, 0.0); + + double nextTime; + int previous; + + /* + * The outer cycle iterates over the number of points of the HeatingCurve + */ + for (previous = 1, nextTime = offset; nextTime < endTime || !curve.isFull(); + previous += timeInterval) { + + /* + * Two adjacent points of the heating curves are separated by timeInterval on + * the time grid. Thus, to calculate the next point on the heating curve, + * timeInterval/tau time steps have to be made first. + */ + timeSegment(previous, previous + timeInterval); + nextTime += wFactor; + curve.addPoint(nextTime, signal()); + } + + curve.copyToLastCalculation(); + scaleSolution(problem); + } + + private void timeSegment(final int m1, final int m2) throws SolverException { + for (int m = m1; m < m2 && normalOperation(); m++) { + prepareStep(m); //prepare + timeStep(m); //calculate + finaliseStep(); //finalise + } + } + + public double pulse(final int m) { + return getDiscretePulse().laserPowerAt((m - EPS) * getGrid().getTimeStep()); + } + + /** + * Do preparatory calculations that depend only on the time variable, e.g., + * calculate the pulse power. + * + * @param m the time step number + */ + public void prepareStep(int m) { + pls = pulse(m); + } + + public boolean normalOperation() { + return true; + } + + /** + * The superclass only lists the {@code TIME_LIMIT} property. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(TIME_LIMIT); + return set; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + /** + * Gets the discrete representation of {@code Pulse} on the {@code Grid}. + * + * @return the discrete pulse + * @see pulse.problem.statements.Pulse + */ + public final DiscretePulse getDiscretePulse() { + return discretePulse; + } + + /** + * Gets the {@code Grid} object defining partioning used in this + * {@code DifferenceScheme} + * + * @return the grid + */ + public final Grid getGrid() { + return grid; + } + + /** + * Sets the grid and adopts it as its child. + * + * @param grid the grid + */ + public final void setGrid(Grid grid) { + this.grid = grid; + this.grid.setParent(this); + } + + /** + * The time interval is the number of discrete timesteps that will be + * discarded when storing the resulting solution into a {@code HeatingCurve} + * object, thus ensuring that only a limited set of points is stored. + * + * @return the time interval + */ + public final int getTimeInterval() { + return timeInterval; + } + + /** + * Sets the time interval to the argument of this method. + * + * @param timeInterval a positive integer. + */ + public final void setTimeInterval(int timeInterval) { + this.timeInterval = timeInterval; + } + + /** + * If true, Lets the UI know that the user only wants to have the most + * important properties displayed. Otherwise this will signal all properties + * need to be displayed. + */ + @Override + public final boolean areDetailsHidden() { + return hideDetailedAdjustment; + } + + /** + * Changes the policy of displaying a detailed information about this + * scheme. + * + * @param b a boolean. + */ + public final static void setDetailsHidden(boolean b) { + hideDetailedAdjustment = b; + } + + /** + * The time limit (in whatever units this {@code DifferenceScheme} uses to + * process the solution), which serves as the ultimate breakpoint for the + * calculations. + * + * @return the {@code NumericProperty} with the type {@code TIME_LIMIT} + * @see pulse.properties.NumericPropertyKeyword + */ + public final NumericProperty getTimeLimit() { + return derive(TIME_LIMIT, timeLimit); + } + + public double getCurrentPulseValue() { + return pls; + } + + /** + * Sets the time limit (in units defined by the corresponding + * {@code NumericProperty}), which serves as the breakpoint for the + * calculations. + * + * @param timeLimit the {@code NumericProperty} with the type + * {@code TIME_LIMIT} + * @see pulse.properties.NumericPropertyKeyword + */ + public final void setTimeLimit(NumericProperty timeLimit) { + requireType(timeLimit, TIME_LIMIT); + this.timeLimit = (double) timeLimit.getValue(); + firePropertyChanged(this, timeLimit); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == TIME_LIMIT) { + 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. + * + * @return an array containing subclasses of the {@code Problem} class which + * can be used as input for this difference scheme. + */ + public abstract Class[] domain(); + + /** + * Creates a {@code DifferenceScheme}, which is an exact copy of this + * object. + * + * @return an exact copy of this {@code DifferenceScheme}. + */ + public abstract DifferenceScheme copy(); + +} diff --git a/src/main/java/pulse/problem/schemes/DistributedDetection.java b/src/main/java/pulse/problem/schemes/DistributedDetection.java index a32f06d5..3d179465 100644 --- a/src/main/java/pulse/problem/schemes/DistributedDetection.java +++ b/src/main/java/pulse/problem/schemes/DistributedDetection.java @@ -1,38 +1,41 @@ package pulse.problem.schemes; +import java.io.Serializable; import java.util.stream.IntStream; -import pulse.problem.statements.penetration.AbsorptionModel; -import pulse.problem.statements.penetration.SpectralRange; +import pulse.problem.statements.model.AbsorptionModel; +import pulse.problem.statements.model.SpectralRange; /** - * An interface providing the ability to calculate the integral signal - * out from a finite-depth material layer. The depth is governed by - * the current {@code AbsorptionModel}. + * An interface providing the ability to calculate the integral signal out from + * a finite-depth material layer. The depth is governed by the current + * {@code AbsorptionModel}. * */ - -public class DistributedDetection { - - /** - * Calculates the effective signal registered by the detector, which takes into account - * a distributed emission pattern. The emissivity is assumed equal to the average absorptivity - * in the thermal region of the spectrum, as per the Kirchhoff's law. - * @param absorption the absorption model - * @param V the current time-temperature profile - * @return the effective detector signal (arbitrary units) - */ - - public static double evaluateSignal(final AbsorptionModel absorption, final Grid grid, final double[] V) { - final double hx = grid.getXStep(); - final int N = grid.getGridDensityValue(); - - double signal = IntStream.range(0, N) - .mapToDouble(i -> V[N - i] * absorption.absorption(SpectralRange.THERMAL, i * hx) - + V[N - 1 - i] * absorption.absorption(SpectralRange.THERMAL, (i + 1) * hx)) - .reduce((a, b) -> a + b).getAsDouble(); - - return signal * 0.5 * hx; - } - -} \ No newline at end of file +public class DistributedDetection implements Serializable { + + private static final long serialVersionUID = 3587781877001360511L; + + /** + * Calculates the effective signal registered by the detector, which takes + * into account a distributed emission pattern. The emissivity is assumed + * equal to the average absorptivity in the thermal region of the spectrum, + * as per the Kirchhoff's law. + * + * @param absorption the absorption model + * @param V the current time-temperature profile + * @return the effective detector signal (arbitrary units) + */ + public static double evaluateSignal(final AbsorptionModel absorption, final Grid grid, final double[] V) { + final double hx = grid.getXStep(); + final int N = grid.getGridDensityValue(); + + double signal = IntStream.range(0, N) + .mapToDouble(i -> V[N - i] * absorption.absorption(SpectralRange.THERMAL, i * hx) + + V[N - 1 - i] * absorption.absorption(SpectralRange.THERMAL, (i + 1) * hx)) + .reduce((a, b) -> a + b).getAsDouble(); + + return signal * 0.5 * hx; + } + +} diff --git a/src/main/java/pulse/problem/schemes/ExplicitScheme.java b/src/main/java/pulse/problem/schemes/ExplicitScheme.java index e5fb12a4..e4b858cd 100644 --- a/src/main/java/pulse/problem/schemes/ExplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ExplicitScheme.java @@ -12,85 +12,85 @@ * This class provides the necessary framework to enable a simple explicit * finite-difference scheme (also called the forward-time centred space scheme) * for solving the one-dimensional heat conduction problem. - * + * * @see pulse.problem.statements.ClassicalProblem * @see pulse.problem.statements.NonlinearProblem * */ - public abstract class ExplicitScheme extends OneDimensionalScheme { - /** - * Constructs a default explicit scheme using the default values of - * {@code GRID_DENSITY} and {@code TAU_FACTOR}. - */ - - public ExplicitScheme() { - this(derive(GRID_DENSITY, 80), derive(TAU_FACTOR, 0.5)); - } + /** + * + */ + private static final long serialVersionUID = -3025913683505686334L; - /** - * Constructs an explicit scheme on a one-dimensional grid that is specified by - * the values {@code N} and {@code timeFactor}. - * - * @see pulse.problem.schemes.DifferenceScheme - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - */ + /** + * Constructs a default explicit scheme using the default values of + * {@code GRID_DENSITY} and {@code TAU_FACTOR}. + */ + public ExplicitScheme() { + this(derive(GRID_DENSITY, 80), derive(TAU_FACTOR, 0.5)); + } - public ExplicitScheme(NumericProperty N, NumericProperty timeFactor) { - super(); - setGrid(new Grid(N, timeFactor)); - } + /** + * Constructs an explicit scheme on a one-dimensional grid that is specified + * by the values {@code N} and {@code timeFactor}. + * + * @see pulse.problem.schemes.DifferenceScheme + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + */ + public ExplicitScheme(NumericProperty N, NumericProperty timeFactor) { + super(); + setGrid(new Grid(N, timeFactor)); + } - /** - *

- * Constructs an explicit scheme on a one-dimensional grid that is specified by - * the values {@code N} and {@code timeFactor}. Sets the time limit of this - * scheme to {@code timeLimit} - * - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - * @param timeLimit the {@code NumericProperty} with the type - * {@code TIME_LIMIT} - * @see pulse.problem.schemes.DifferenceScheme - */ + /** + *

+ * Constructs an explicit scheme on a one-dimensional grid that is specified + * by the values {@code N} and {@code timeFactor}. Sets the time limit of + * this scheme to {@code timeLimit} + * + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + * @param timeLimit the {@code NumericProperty} with the type + * {@code TIME_LIMIT} + * @see pulse.problem.schemes.DifferenceScheme + */ + public ExplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(timeLimit); + setGrid(new Grid(N, timeFactor)); + } - public ExplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(timeLimit); - setGrid(new Grid(N, timeFactor)); - } - - /** - * Uses the explicit finite-difference representation of the heat equation to calculate the grid-function everywhere - * except for the boundaries. This will update the current solution using the solution from previous time step. - */ - - public void explicitSolution() { - var grid = getGrid(); - var U = getPreviousSolution(); - final double TAU_HH = grid.getTimeStep()/(fastPowLoop(grid.getXStep(), 2)); - for (int i = 1, N = grid.getGridDensityValue(); i < N; i++) - setSolutionAt(i, U[i] + TAU_HH * (U[i + 1] - 2. * U[i] + U[i - 1]) + phi(i) ); - } - - public double phi(final int i) { - return 0; - } + /** + * Uses the explicit finite-difference representation of the heat equation + * to calculate the grid-function everywhere except for the boundaries. This + * will update the current solution using the solution from previous time + * step. + */ + public void explicitSolution() { + var grid = getGrid(); + var U = getPreviousSolution(); + final double TAU_HH = grid.getTimeStep() / (fastPowLoop(grid.getXStep(), 2)); + for (int i = 1, N = grid.getGridDensityValue(); i < N; i++) { + setSolutionAt(i, U[i] + TAU_HH * (U[i + 1] - 2. * U[i] + U[i - 1]) + phi(i)); + } + } - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ + public double phi(final int i) { + return 0; + } - @Override - public String toString() { - return getString("ExplicitScheme.4"); - } + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ExplicitScheme.4"); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/FixedPointIterations.java b/src/main/java/pulse/problem/schemes/FixedPointIterations.java index a093e67f..dbf5992f 100644 --- a/src/main/java/pulse/problem/schemes/FixedPointIterations.java +++ b/src/main/java/pulse/problem/schemes/FixedPointIterations.java @@ -2,26 +2,70 @@ import static java.lang.Math.abs; -public interface FixedPointIterations { +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; - public default void doIterations(double[] V, final double error, final int m) { +/** + * @see Wiki + * page + * + */ +public interface FixedPointIterations extends Serializable { - final int N = V.length - 1; + /** + * 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()}. + * + * @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 + * @see finaliseIteration() + * @see iteration() + */ + public default void doIterations(double[] V, final double error, final int m) throws SolverException { - for (double V_0 = error + 1, V_N = error + 1; abs(V[0] - V_0) > error - || abs(V[N] - V_N) > error; finaliseIteration(V)) { + final int N = V.length - 1; - V_N = V[N]; - V_0 = V[0]; - iteration(m); + 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]; + iteration(m); - public void iteration(final int m); + } + } - public default void finaliseIteration(double[] V) { - // do nothing - } + /** + * Performs an iteration at time {@code m} + * + * @param m time step + * @throws pulse.problem.schemes.solvers.SolverException if the calculation + * failed + */ + public void iteration(final int m) throws SolverException; -} \ No newline at end of file + /** + * Finalises the current iteration.By default, does nothing. + * + * @param V the current iteration + * @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", + FINITE_DIFFERENCE_ERROR); + } + } + +} diff --git a/src/main/java/pulse/problem/schemes/Grid.java b/src/main/java/pulse/problem/schemes/Grid.java index 20537fb8..d8a0b331 100644 --- a/src/main/java/pulse/problem/schemes/Grid.java +++ b/src/main/java/pulse/problem/schemes/Grid.java @@ -2,20 +2,15 @@ import static java.lang.Math.pow; import static java.lang.Math.rint; -import static java.lang.String.format; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; -import java.util.ArrayList; -import java.util.List; +import java.util.Set; -import pulse.problem.laser.DiscretePulse; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; import pulse.util.PropertyHolder; /** @@ -28,218 +23,196 @@ *

* */ - public class Grid extends PropertyHolder { - private double hx; - private double tau; - private double tauFactor; - private int N; - - /** - * Creates a {@code Grid} object with the specified {@code gridDensity} and - * {@code timeFactor}. - * - * @param gridDensity a {@code NumericProperty} of the type {@code GRID_DENSITY} - * @param timeFactor a {@code NumericProperty} of the type {@code TIME_FACTOR} - * @see pulse.properties.NumericPropertyKeyword - */ - - public Grid(NumericProperty gridDensity, NumericProperty timeFactor) { - setGridDensity(gridDensity); - setTimeFactor(timeFactor); - } - - protected Grid() { - // intentionally blank - } - - /** - * Creates a new {@code Grid} object with exactly the same parameters as this - * one. - * - * @return a new {@code Grid} object replicating this {@code Grid} - */ - - public Grid copy() { - return new Grid(getGridDensity(), getTimeFactor()); - } - - /** - * Optimises the {@code Grid} parameters. - *

- * This can change the {@code tauFactor} and {@code tau} variables in the - * {@code Grid} object if {@code discretePulseWidth < grid.tau}. - *

- * - * @param pulse the discrete pulse representation - */ - - public void adjustTo(DiscretePulse pulse) { - final double ADJUSTMENT_FACTOR = 0.75; - for (final double factor = 0.95; factor * tau > pulse.getDiscreteWidth(); pulse.recalculate()) { - tauFactor *= ADJUSTMENT_FACTOR; - tau = tauFactor * pow(hx, 2); - } - } - - /** - * The listed properties include {@code GRID_DENSITY} and {@code TAU_FACTOR}. - */ - - @Override - public List listedTypes() { - List list = new ArrayList<>(2); - list.add(def(GRID_DENSITY)); - list.add(def(TAU_FACTOR)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case TAU_FACTOR: - setTimeFactor(property); - break; - case GRID_DENSITY: - setGridDensity(property); - break; - default: - break; - } - } - - /** - * Retrieves the value of the hx coordinate step - * used in finite-difference calculation. - * - * @return a double, representing the {@code hx} value. - */ - - public double getXStep() { - return hx; - } - - /** - * Sets the value of the hx coordinate step. - * - * @param hx a double, representing the new {@code hx} value. - */ - - public void setXStep(double hx) { - this.hx = hx; - } - - /** - * Retrieves the value of the τ time step used in finite-difference - * calculation. - * - * @return a double, representing the {@code tau} value. - */ - - public double getTimeStep() { - return tau; - } - - protected void setTimeStep(double tau) { - this.tau = tau; - } - - /** - * Retrieves the value of the τ-factor, or the time factor, used in - * finite-difference calculation. This factor determines the proportionally - * coefficient between τ and hx. - * - * @return a NumericProperty of the {@code TAU_FACTOR} type, representing the - * {@code tauFactor} value. - */ - - public NumericProperty getTimeFactor() { - return derive(TAU_FACTOR, tauFactor); - } - - /** - * Retrieves the value of the {@code gridDensity} used to calculate the - * {@code hx} and {@code tau}. - * - * @return a NumericProperty of the {@code GRID_DENSITY} type, representing the - * {@code gridDensity} value. - */ - - public NumericProperty getGridDensity() { - return derive(GRID_DENSITY, N); - } - - protected int getGridDensityValue() { - return N; - } - - protected void setGridDensityValue(int N) { - this.N = N; - } - - /** - * Sets the value of the {@code gridDensity}. Automatically recalculates the - * {@code hx} value. - * - * @param gridDensity a NumericProperty of the {@code GRID_DENSITY} type - */ - - public void setGridDensity(NumericProperty gridDensity) { - requireType(gridDensity, GRID_DENSITY); - this.N = (int) gridDensity.getValue(); - hx = 1. / N; - setTimeFactor(derive(TAU_FACTOR, 1.0)); - } - - /** - * Sets the value of the {@code tauFactor}. Automatically recalculates the - * {@code tau} value. - * - * @param timeFactor a NumericProperty of the {@code TAU_FACTOR} type - */ - - public void setTimeFactor(NumericProperty timeFactor) { - requireType(timeFactor, TAU_FACTOR); - this.tauFactor = (double) timeFactor.getValue(); - setTimeStep(tauFactor * pow(hx, 2)); - } - - /** - * The dimensionless time on this {@code Grid}, which is the - * {@code time/dimensionFactor} rounded up to a factor of the time step - * {@code tau}. - * - * @param time the time - * @param dimensionFactor a conversion factor with the dimension of time - * @return a double representing the time on the finite grid - */ - - public double gridTime(double time, double dimensionFactor) { - return rint((time / dimensionFactor) / tau) * tau; - } - - /** - * The dimensionless axial distance on this {@code Grid}, which is the - * {@code distance/lengthFactor} rounded up to a factor of the coordinate step - * {@code hx}. - * - * @param distance the distance along the axial direction - * @param lengthFactor a conversion factor with the dimension of length - * @return a double representing the axial distance on the finite grid - */ - - public double gridAxialDistance(double distance, double lengthFactor) { - return rint((distance / lengthFactor) / hx) * hx; - } - - @Override - public String toString() { - var sb = new StringBuilder(); - sb.append(""); - sb.append(getClass().getSimpleName() + ": hx=" + format("%3.2e", hx) + "; "); - sb.append("τ=" + format("%3.2e", tau) + "; "); - return sb.toString(); - } - -} \ No newline at end of file + private static final long serialVersionUID = -4293010190416468656L; + private double hx; + private double tau; + private double tauFactor; + private int N; + + /** + * Creates a {@code Grid} object with the specified {@code gridDensity} and + * {@code timeFactor}. + * + * @param gridDensity a {@code NumericProperty} of the type + * {@code GRID_DENSITY} + * @param timeFactor a {@code NumericProperty} of the type + * {@code TIME_FACTOR} + * @see pulse.properties.NumericPropertyKeyword + */ + public Grid(NumericProperty gridDensity, NumericProperty timeFactor) { + this.N = (int) gridDensity.getValue(); + this.tauFactor = (double) timeFactor.getValue(); + hx = 1. / N; + setTimeStep(tauFactor * pow(hx, 2)); + } + + protected Grid() { + // intentionally blank + } + + /** + * Creates a new {@code Grid} object with exactly the same parameters as + * this one. + * + * @return a new {@code Grid} object replicating this {@code Grid} + */ + public Grid copy() { + return new Grid(getGridDensity(), getTimeFactor()); + } + + /** + * The listed properties include {@code GRID_DENSITY} and + * {@code TAU_FACTOR}. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(GRID_DENSITY); + set.add(TAU_FACTOR); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case TAU_FACTOR: + setTimeFactor(property); + break; + case GRID_DENSITY: + setGridDensity(property); + break; + default: + break; + } + } + + /** + * Retrieves the value of the hx coordinate + * step used in finite-difference calculation. + * + * @return a double, representing the {@code hx} value. + */ + public final double getXStep() { + return hx; + } + + /** + * Sets the value of the hx coordinate step. + * + * @param hx a double, representing the new {@code hx} value. + */ + public final void setXStep(double hx) { + this.hx = hx; + } + + /** + * Retrieves the value of the τ time step used in finite-difference + * calculation. + * + * @return a double, representing the {@code tau} value. + */ + public final double getTimeStep() { + return tau; + } + + protected final void setTimeStep(double tau) { + this.tau = tau; + + } + + /** + * Retrieves the value of the τ-factor, or the time factor, used in + * finite-difference calculation. This factor determines the proportionally + * coefficient between τ and hx. + * + * @return a NumericProperty of the {@code TAU_FACTOR} type, representing + * the {@code tauFactor} value. + */ + public final NumericProperty getTimeFactor() { + return derive(TAU_FACTOR, tauFactor); + } + + /** + * Retrieves the value of the {@code gridDensity} used to calculate the + * {@code hx} and {@code tau}. + * + * @return a NumericProperty of the {@code GRID_DENSITY} type, representing + * the {@code gridDensity} value. + */ + public final NumericProperty getGridDensity() { + return derive(GRID_DENSITY, N); + } + + protected final int getGridDensityValue() { + return N; + } + + protected void setGridDensityValue(int N) { + this.N = N; + hx = 1. / N; + } + + /** + * Sets the value of the {@code gridDensity}. Automatically recalculates the + * {@code hx} value. + * + * @param gridDensity a NumericProperty of the {@code GRID_DENSITY} type + */ + public void setGridDensity(NumericProperty gridDensity) { + requireType(gridDensity, GRID_DENSITY); + this.N = (int) gridDensity.getValue(); + hx = 1. / N; + setTimeStep(tauFactor * pow(hx, 2)); + firePropertyChanged(this, gridDensity); + } + + /** + * Sets the value of the {@code tauFactor}. Automatically recalculates the + * {@code tau} value. + * + * @param timeFactor a NumericProperty of the {@code TAU_FACTOR} type + */ + public void setTimeFactor(NumericProperty timeFactor) { + requireType(timeFactor, TAU_FACTOR); + this.tauFactor = (double) timeFactor.getValue(); + setTimeStep(tauFactor * pow(hx, 2)); + firePropertyChanged(this, timeFactor); + } + + /** + * The dimensionless time on this {@code Grid}, which is the + * {@code time/dimensionFactor} rounded up to a factor of the time step + * {@code tau}. + * + * @param time the time + * @param dimensionFactor a conversion factor with the dimension of time + * @return a double representing the time on the finite grid + */ + public final double gridTime(double time, double dimensionFactor) { + return ((int) (time / dimensionFactor / tau)) * tau; + } + + /** + * The dimensionless axial distance on this {@code Grid}, which is the + * {@code distance/lengthFactor} rounded up to a factor of the coordinate + * step {@code hx}. + * + * @param distance the distance along the axial direction + * @param lengthFactor a conversion factor with the dimension of length + * @return a double representing the axial distance on the finite grid + */ + public final double gridAxialDistance(double distance, double lengthFactor) { + return rint((distance / lengthFactor) / hx) * hx; + } + + @Override + public String toString() { + 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 0f1fc76c..35dac537 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; @@ -17,101 +16,90 @@ * dimensions for interpreting the laser flash experiments. *

*/ - public class Grid2D extends Grid { - private double hy; - - protected Grid2D() { - super(); - } - - /** - * Creates a {@code Grid2D} where the radial and axial spatial steps are equal - * to the inverse {@code gridDensity}. Otherwise, calls the superclass - * constructor. - * - * @param gridDensity the grid density - * @param timeFactor the {@code τF} factor - */ - - public Grid2D(NumericProperty gridDensity, NumericProperty timeFactor) { - super(gridDensity, timeFactor); - hy = 1.0 / getGridDensityValue(); - } - - @Override - public Grid2D copy() { - return new Grid2D(getGridDensity(), getTimeFactor()); - } - - @Override - public void setTimeFactor(NumericProperty timeFactor) { - super.setTimeFactor(timeFactor); - setTimeStep((double) timeFactor.getValue() * (pow(getXStep(), 2) + pow(hy, 2))); - } - - /** - * Calls the {@code adjustTo} method from superclass, then adjusts the - * {@code gridDensity} of the {@code grid} if - * {@code discretePulseSpot < (Grid2D)grid.hy}. - * - * @param pulse the discrete puls representation - */ - - @Override - public void adjustTo(DiscretePulse pulse) { - super.adjustTo(pulse); - if (pulse instanceof DiscretePulse2D) - adjustTo((DiscretePulse2D) pulse); - } - - private void adjustTo(DiscretePulse2D pulse) { - final int GRID_DENSITY_INCREMENT = 5; - - for (final var factor = 1.05; factor * hy > pulse.getDiscretePulseSpot(); pulse.recalculate()) { - int N = getGridDensityValue(); - setGridDensityValue(N + GRID_DENSITY_INCREMENT); - hy = 1. / N; - setXStep(1. / N); - } - - } - - /** - * Sets the value of the {@code gridDensity}. Automatically recalculates the - * {@code hx} an {@code hy} values. - */ - - @Override - public void setGridDensity(NumericProperty gridDensity) { - super.setGridDensity(gridDensity); - hy = getXStep(); - } - - /** - * The dimensionless radial distance on this {@code Grid2D}, which is the - * {@code radial/lengthFactor} rounded up to a factor of the coordinate step - * {@code hy}. - * - * @param radial the distance along the radial direction - * @param lengthFactor a factor which has the dimension of length - * @return a double representing the radial distance on the finite grid - */ - - public double gridRadialDistance(double radial, double lengthFactor) { - return rint((radial / lengthFactor) / hy) * hy; - } - - @Override - public String toString() { - var sb = new StringBuilder(super.toString()); - sb.append("hy=" + format("%3.3f", hy)); - return sb.toString(); - } - - public double getYStep() { - return hy; - } - -} \ No newline at end of file + private static final long serialVersionUID = 564113358979595637L; + private double hy; + + protected Grid2D() { + super(); + } + + /** + * Creates a {@code Grid2D} where the radial and axial spatial steps are + * equal to the inverse {@code gridDensity}. Otherwise, calls the superclass + * constructor. + * + * @param gridDensity the grid density + * @param timeFactor the {@code τF} factor + */ + public Grid2D(NumericProperty gridDensity, NumericProperty timeFactor) { + super(gridDensity, timeFactor); + hy = 1.0 / getGridDensityValue(); + } + + @Override + public Grid2D copy() { + return new Grid2D(getGridDensity(), getTimeFactor()); + } + + @Override + public void setTimeFactor(NumericProperty timeFactor) { + super.setTimeFactor(timeFactor); + setTimeStep((double) timeFactor.getValue() * (pow(getXStep(), 2) + pow(hy, 2))); + } + + /** + * Calls the {@code adjustTo} method from superclass, then adjusts the + * {@code gridDensity} of the {@code grid} if + * {@code discretePulseSpot < (Grid2D)grid.hy}. + * + * @param pulse the discrete puls representation + */ + public void adjustStepSize(DiscretePulse pulse) { + var pulse2d = (DiscretePulse2D) pulse; + double pulseSpotSize = pulse2d.getDiscretePulseSpot(); + + if (hy > pulseSpotSize) { + final int INCREMENT = 5; + final int newN = getGridDensityValue() + INCREMENT; + setGridDensityValue(newN); + adjustStepSize(pulse); + } + + } + + @Override + protected void setGridDensityValue(int N) { + super.setGridDensityValue(N); + hy = 1. / N; + } + + /** + * Sets the value of the {@code gridDensity}. Automatically recalculates the + * {@code hx} an {@code hy} values. + */ + @Override + public void setGridDensity(NumericProperty gridDensity) { + super.setGridDensity(gridDensity); + hy = getXStep(); + } + + /** + * The dimensionless radial distance on this {@code Grid2D}, which is the + * {@code radial/lengthFactor} rounded up to a factor of the coordinate step + * {@code hy}. + * + * @param radial the distance along the radial direction + * @param lengthFactor a factor which has the dimension of length + * @return a double representing the radial distance on the finite grid + */ + public double gridRadialDistance(double radial, double lengthFactor) { + return rint((radial / lengthFactor) / hy) * hy; + } + + public double getYStep() { + return hy; + } + +} diff --git a/src/main/java/pulse/problem/schemes/ImplicitScheme.java b/src/main/java/pulse/problem/schemes/ImplicitScheme.java index af2324c6..0d72003c 100644 --- a/src/main/java/pulse/problem/schemes/ImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ImplicitScheme.java @@ -1,5 +1,6 @@ package pulse.problem.schemes; +import pulse.problem.schemes.solvers.SolverException; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; @@ -11,100 +12,108 @@ /** * An abstract implicit finite-difference scheme for solving one-dimensional * heat conduction problems. - * + * * @see pulse.problem.statements.ClassicalProblem * @see pulse.problem.statements.NonlinearProblem */ - public abstract class ImplicitScheme extends OneDimensionalScheme { - - private TridiagonalMatrixAlgorithm tridiagonal; - - /** - * Constructs a default fully-implicit scheme using the default values of - * {@code GRID_DENSITY} and {@code TAU_FACTOR}. - */ - - public ImplicitScheme() { - this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 0.25)); - } - - /** - * Constructs a fully-implicit scheme on a one-dimensional grid that is - * specified by the values {@code N} and {@code timeFactor}. - * - * @see pulse.problem.schemes.DifferenceScheme - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - */ - - public ImplicitScheme(NumericProperty N, NumericProperty timeFactor) { - super(); - setGrid(new Grid(N, timeFactor)); - } - - /** - *

- * Constructs a fully-implicit scheme on a one-dimensional grid that is - * specified by the values {@code N} and {@code timeFactor}. Sets the time limit - * of this scheme to {@code timeLimit} - * - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - * @param timeLimit the {@code NumericProperty} with the type - * {@code TIME_LIMIT} - * @see pulse.problem.schemes.DifferenceScheme - */ - - public ImplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(timeLimit); - setGrid(new Grid(N, timeFactor)); - } - - @Override - protected void prepare(Problem problem) { - super.prepare(problem); - tridiagonal = new TridiagonalMatrixAlgorithm(getGrid()); - } - - @Override - public void timeStep(final int m) { - leftBoundary(m); - final var V = getCurrentSolution(); - final int N = V.length - 1; - setSolutionAt(N, evalRightBoundary(m, tridiagonal.getAlpha()[N], tridiagonal.getBeta()[N]) ); - tridiagonal.sweep(V); - } - - public void leftBoundary(final int m) { - tridiagonal.setBeta( 1, firstBeta(m) ); - tridiagonal.evaluateBeta(getPreviousSolution()); - } - - public abstract double evalRightBoundary(final int m, final double alphaN, final double betaN); - public abstract double firstBeta(final int m); - - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ - - @Override - public String toString() { - return getString("ImplicitScheme.4"); - } - - public TridiagonalMatrixAlgorithm getTridiagonalMatrixAlgorithm() { - return tridiagonal; - } - - public void setTridiagonalMatrixAlgorithm(TridiagonalMatrixAlgorithm tridiagonal) { - this.tridiagonal = tridiagonal; - } - -} \ No newline at end of file + + /** + * + */ + private static final long serialVersionUID = 2785615380656900783L; + private TridiagonalMatrixAlgorithm tridiagonal; + + /** + * Constructs a default fully-implicit scheme using the default values of + * {@code GRID_DENSITY} and {@code TAU_FACTOR}. + */ + public ImplicitScheme() { + this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 0.25)); + } + + /** + * Constructs a fully-implicit scheme on a one-dimensional grid that is + * specified by the values {@code N} and {@code timeFactor}. + * + * @see pulse.problem.schemes.DifferenceScheme + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + */ + public ImplicitScheme(NumericProperty N, NumericProperty timeFactor) { + super(); + setGrid(new Grid(N, timeFactor)); + } + + /** + *

+ * Constructs a fully-implicit scheme on a one-dimensional grid that is + * specified by the values {@code N} and {@code timeFactor}. Sets the time + * limit of this scheme to {@code timeLimit} + * + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + * @param timeLimit the {@code NumericProperty} with the type + * {@code TIME_LIMIT} + * @see pulse.problem.schemes.DifferenceScheme + */ + public ImplicitScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(timeLimit); + setGrid(new Grid(N, timeFactor)); + } + + @Override + protected void prepare(Problem problem) throws SolverException { + super.prepare(problem); + tridiagonal = new TridiagonalMatrixAlgorithm(getGrid()); + } + + /** + * 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() + */ + @Override + public void timeStep(final int m) throws SolverException { + 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(int m) { + tridiagonal.setBeta(1, firstBeta()); + tridiagonal.evaluateBeta(getPreviousSolution()); + } + + public abstract double evalRightBoundary(final double alphaN, final double betaN); + + public abstract double firstBeta(); + + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ImplicitScheme.4"); + } + + public TridiagonalMatrixAlgorithm getTridiagonalMatrixAlgorithm() { + return tridiagonal; + } + + public void setTridiagonalMatrixAlgorithm(TridiagonalMatrixAlgorithm tridiagonal) { + this.tridiagonal = tridiagonal; + } + +} diff --git a/src/main/java/pulse/problem/schemes/LayeredGrid2D.java b/src/main/java/pulse/problem/schemes/LayeredGrid2D.java deleted file mode 100644 index 68f2c8e3..00000000 --- a/src/main/java/pulse/problem/schemes/LayeredGrid2D.java +++ /dev/null @@ -1,83 +0,0 @@ -package pulse.problem.schemes; - -import static pulse.problem.schemes.Partition.Location.CORE_X; -import static pulse.problem.schemes.Partition.Location.CORE_Y; -import static pulse.problem.schemes.Partition.Location.FRONT_Y; -import static pulse.problem.schemes.Partition.Location.REAR_Y; -import static pulse.problem.schemes.Partition.Location.SIDE_X; -import static pulse.problem.schemes.Partition.Location.SIDE_Y; -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.SHELL_GRID_DENSITY; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import pulse.problem.schemes.Partition.Location; -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; - -public class LayeredGrid2D extends Grid2D { - - private Map h; - - public LayeredGrid2D(Map partitions, NumericProperty timeFactor) { - h = new HashMap<>(partitions); - setGridDensity(derive(CORE_Y.densityKeyword(), partitions.get(CORE_Y).getDensity())); - setTimeFactor(timeFactor); - } - - public Partition getPartition(Location location) { - return h.get(location); - } - - @Override - public Grid2D copy() { - return new LayeredGrid2D(h, getTimeFactor()); - } - - private void setDensity(Location location, NumericProperty density) { - h.get(location).setDensity((int) density.getValue()); - } - - @Override - public void setGridDensity(NumericProperty gridDensity) { - super.setGridDensity(gridDensity); - setDensity(CORE_X, gridDensity); - setDensity(CORE_Y, gridDensity); - } - - public NumericProperty getGridDensity(Location location) { - return derive(location.densityKeyword(), h.get(location).getDensity()); - } - - @Override - public NumericProperty getGridDensity() { - return getGridDensity(CORE_X); - } - - @Override - public List listedTypes() { - List list = new ArrayList<>(2); - list.add(def(SHELL_GRID_DENSITY)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case SHELL_GRID_DENSITY: - setDensity(FRONT_Y, property); - setDensity(REAR_Y, property); - setDensity(SIDE_X, property); - setDensity(SIDE_Y, property); - break; - default: - super.set(type, property); - } - } - -} \ 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 976469b2..fa53e697 100644 --- a/src/main/java/pulse/problem/schemes/MixedScheme.java +++ b/src/main/java/pulse/problem/schemes/MixedScheme.java @@ -10,66 +10,64 @@ /** * An abstraction describing a weighted semi-implicit finite-difference scheme * for solving the one-dimensional heat conduction problem. - * + * * @see pulse.problem.statements.ClassicalProblem * @see pulse.problem.statements.NonlinearProblem * */ - public abstract class MixedScheme extends ImplicitScheme { - /** - * Constructs a default semi-implicit scheme using the default values of - * {@code GRID_DENSITY} and {@code TAU_FACTOR}. - */ - - public MixedScheme() { - this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 1.0)); - } - - /** - * Constructs a semi-implicit scheme on a one-dimensional grid that is specified - * by the values {@code N} and {@code timeFactor}. - * - * @see pulse.problem.schemes.DifferenceScheme - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - */ - - public MixedScheme(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } + /** + * + */ + private static final long serialVersionUID = -770528855578192638L; - /** - *

- * Constructs a semi-implicit scheme on a one-dimensional grid that is specified - * by the values {@code N} and {@code timeFactor}. Sets the time limit of this - * scheme to {@code timeLimit} - * - * @param N the {@code NumericProperty} with the type - * {@code GRID_DENSITY} - * @param timeFactor the {@code NumericProperty} with the type - * {@code TAU_FACTOR} - * @param timeLimit the {@code NumericProperty} with the type - * {@code TIME_LIMIT} - * @see pulse.problem.schemes.DifferenceScheme - */ + /** + * Constructs a default semi-implicit scheme using the default values of + * {@code GRID_DENSITY} and {@code TAU_FACTOR}. + */ + public MixedScheme() { + this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 1.0)); + } - public MixedScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } + /** + * Constructs a semi-implicit scheme on a one-dimensional grid that is + * specified by the values {@code N} and {@code timeFactor}. + * + * @see pulse.problem.schemes.DifferenceScheme + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + */ + public MixedScheme(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + } - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ + /** + *

+ * Constructs a semi-implicit scheme on a one-dimensional grid that is + * specified by the values {@code N} and {@code timeFactor}. Sets the time + * limit of this scheme to {@code timeLimit} + * + * @param N the {@code NumericProperty} with the type {@code GRID_DENSITY} + * @param timeFactor the {@code NumericProperty} with the type + * {@code TAU_FACTOR} + * @param timeLimit the {@code NumericProperty} with the type + * {@code TIME_LIMIT} + * @see pulse.problem.schemes.DifferenceScheme + */ + public MixedScheme(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } - @Override - public String toString() { - return getString("MixedScheme.4"); - } + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("MixedScheme.4"); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java index 12a7f756..b58bf0fe 100644 --- a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java +++ b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java @@ -1,49 +1,54 @@ package pulse.problem.schemes; -import pulse.problem.statements.Problem; +import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; public abstract class OneDimensionalScheme extends DifferenceScheme { - private double[] U; - private double[] V; - - protected OneDimensionalScheme() { - super(); - } - - protected OneDimensionalScheme(NumericProperty timeLimit) { - super(timeLimit); - } - - @Override - protected void prepare(Problem problem) { - super.prepare(problem); - final int N = (int) getGrid().getGridDensity().getValue(); - U = new double[N + 1]; - V = new double[N + 1]; - } - - @Override - public double signal() { - return V[ V.length - 1 ]; - } - - @Override - public void finaliseStep() { - System.arraycopy(V, 0, U, 0, V.length); - } - - public double[] getPreviousSolution() { - return U; - } - - public double[] getCurrentSolution() { - return V; - } - - public void setSolutionAt(final int i, final double v) { - this.V[i] = v; - } - -} \ No newline at end of file + private double[] U; + private double[] V; + + protected OneDimensionalScheme() { + super(); + } + + protected OneDimensionalScheme(NumericProperty timeLimit) { + super(timeLimit); + } + + @Override + public void clearArrays() { + final int N = (int) getGrid().getGridDensity().getValue(); + U = new double[N + 1]; + V = new double[N + 1]; + } + + @Override + public double signal() { + return V[V.length - 1]; + } + + /** + * 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); + } + + public double[] getPreviousSolution() { + return U; + } + + public double[] getCurrentSolution() { + return V; + } + + public void setSolutionAt(final int i, final double v) { + this.V[i] = v; + } + +} diff --git a/src/main/java/pulse/problem/schemes/Partition.java b/src/main/java/pulse/problem/schemes/Partition.java deleted file mode 100644 index 8fe29b6d..00000000 --- a/src/main/java/pulse/problem/schemes/Partition.java +++ /dev/null @@ -1,66 +0,0 @@ -package pulse.problem.schemes; - -import pulse.properties.NumericPropertyKeyword; - -public class Partition { - - private int density; - private double multiplier; - private double shift; - - public Partition(int value, double multiplier, double shift) { - this.setDensity(value); - this.setShift(shift); - this.setGridMultiplier(multiplier); - } - - public double evaluate() { - return multiplier / (density * (1.0 + shift)); - } - - public int getDensity() { - return density; - } - - public void setDensity(int density) { - this.density = density; - } - - public double getGridMultiplier() { - return multiplier; - } - - public void setGridMultiplier(double multiplier) { - this.multiplier = multiplier; - } - - public double getShift() { - return shift; - } - - public void setShift(double shift) { - this.shift = shift; - } - - public enum Location { - - FRONT_Y, REAR_Y, SIDE_Y, SIDE_X, CORE_X, CORE_Y; - - public NumericPropertyKeyword densityKeyword() { - switch (this) { - case FRONT_Y: - case REAR_Y: - case SIDE_Y: - case SIDE_X: - return NumericPropertyKeyword.SHELL_GRID_DENSITY; - case CORE_X: - case CORE_Y: - return NumericPropertyKeyword.GRID_DENSITY; - default: - throw new IllegalArgumentException("Type not recognized: " + this); - } - } - - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java index 8208c939..e507e00d 100644 --- a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java +++ b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java @@ -1,12 +1,12 @@ package pulse.problem.schemes; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.schemes.rte.dom.DiscreteOrdinatesMethod; import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.Problem; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; @@ -14,67 +14,76 @@ import pulse.util.PropertyHolder; public class RadiativeTransferCoupling extends PropertyHolder { - - private RadiativeTransferSolver rte; - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "RTE Solver Selector", RadiativeTransferSolver.class); - - public RadiativeTransferCoupling() { - instanceDescriptor.setSelectedDescriptor(DiscreteOrdinatesMethod.class.getSimpleName()); - instanceDescriptor.addListener(() -> firePropertyChanged(this, instanceDescriptor)); - super.parameterListChanged(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - //intentionally blank - } - - public void init(ParticipatingMedium problem, Grid grid) { - - if (rte == null) { - newRTE(problem, grid); - instanceDescriptor.addListener(() -> { - newRTE(problem, grid); - rte.init(problem, grid); - }); - - } - else - rte.init(problem, grid); - - } - - private void newRTE(ParticipatingMedium problem, Grid grid) { - rte = instanceDescriptor.newInstance(RadiativeTransferSolver.class, problem, grid); - rte.setParent(this); - } - - public InstanceDescriptor getInstanceDescriptor() { - return instanceDescriptor; - } - - public RadiativeTransferSolver getRadiativeTransferEquation() { - return rte; - } - - public void setRadiativeTransferEquation(RadiativeTransferSolver solver) { - this.rte = solver; - } - - @Override - public String toString() { - return instanceDescriptor.toString(); - } - - @Override - public String getPrefix() { - return "RTE Coupling"; - } - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(instanceDescriptor)); - } - -} \ No newline at end of file + + private static final long serialVersionUID = -8969606772435213260L; + private RadiativeTransferSolver rte; + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor( + "RTE Solver Selector", RadiativeTransferSolver.class); + + public RadiativeTransferCoupling() { + instanceDescriptor.setSelectedDescriptor(DiscreteOrdinatesMethod.class.getSimpleName()); + instanceDescriptor.addListener(() -> firePropertyChanged(this, instanceDescriptor)); + super.parameterListChanged(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + //intentionally blank + } + + public void init(ParticipatingMedium problem, Grid grid) { + + if (rte == null) { + + if (!(problem.getProperties() instanceof ThermoOpticalProperties)) { + throw new IllegalArgumentException("Illegal problem type: " + problem); + } + + newRTE(problem, grid); + instanceDescriptor.addListener(() -> { + newRTE(problem, grid); + rte.init(problem, grid); + }); + + } else { + rte.init(problem, grid); + } + + } + + private void newRTE(Problem problem, Grid grid) { + rte = instanceDescriptor.newInstance(RadiativeTransferSolver.class, problem, grid); + rte.setParent(this); + } + + public InstanceDescriptor getInstanceDescriptor() { + return instanceDescriptor; + } + + public RadiativeTransferSolver getRadiativeTransferEquation() { + return rte; + } + + public void setRadiativeTransferEquation(RadiativeTransferSolver solver) { + this.rte = solver; + } + + @Override + public String toString() { + return instanceDescriptor.toString(); + } + + @Override + public String getPrefix() { + return "RTE Coupling"; + } + + @Override + public List listedTypes() { + var list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + +} diff --git a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java index 01c3c8cb..0b76d966 100644 --- a/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java +++ b/src/main/java/pulse/problem/schemes/TridiagonalMatrixAlgorithm.java @@ -1,116 +1,137 @@ 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. + * 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. * */ +public class TridiagonalMatrixAlgorithm implements Serializable { + + private static final long serialVersionUID = 8201903787985856087L; + private final double tau; + private final double h; + + private double a; + private double b; + private double c; + + private final int N; + private final double[] alpha; + private final double[] beta; + + public TridiagonalMatrixAlgorithm(Grid grid) { + tau = grid.getTimeStep(); + N = grid.getGridDensityValue(); + h = grid.getXStep(); + alpha = new double[N + 2]; + beta = new double[N + 2]; + } + + /** + * Calculates the solution {@code V} using the tridiagonal matrix algorithm. + * This performs a backwards sweep from {@code N - 1} to {@code 0} where + * {@code N} is the grid density value. The coefficients {@code alpha} and + * {@code beta} should have been precalculated + * + * @param V the array containing the {@code N}th value previously calculated + * from the respective boundary condition + */ + public void sweep(double[] V) { + for (int j = N - 1; j >= 0; j--) { + V[j] = alpha[j + 1] * V[j + 1] + beta[j + 1]; + } + } + + /** + * Calculates the {@code alpha} coefficients as part of the tridiagonal + * matrix algorithm. + */ + public void evaluateAlpha() { + for (int i = 1; i < N; i++) { + alpha[i + 1] = c / (b - a * alpha[i]); + } + } + + public void evaluateBeta(final double[] U) { + evaluateBeta(U, 2, N + 1); + } + + /** + * Calculates the {@code beta} coefficients as part of the tridiagonal + * matrix algorithm. + * + * @param U + * @param start + * @param endExclusive + */ + public void evaluateBeta(final double[] U, final int start, final int endExclusive) { + for (int i = start; i < endExclusive; i++) { + beta[i] = beta(U[i - 1] / tau, phi(i - 1), i); + } + } + + public double beta(final double f, final double phi, final int i) { + return (f + phi + a * beta[i - 1]) / (b - a * alpha[i - 1]); + } + + public double phi(int i) { + return 0; + } + + public void setAlpha(final int i, final double alpha) { + this.alpha[i] = alpha; + } + + public void setBeta(final int i, final double beta) { + this.beta[i] = beta; + } + + public double[] getAlpha() { + return alpha; + } + + public double[] getBeta() { + return beta; + } + + public void setCoefA(double a) { + this.a = a; + } + + public void setCoefB(double b) { + this.b = b; + } + + public void setCoefC(double c) { + this.c = c; + } + + protected double getCoefA() { + return a; + } + + protected double getCoefB() { + return b; + } + + protected double getCoefC() { + return c; + } + + public final double getTimeStep() { + return tau; + } + + public final int getGridPoints() { + return N; + } + + public final double getGridStep() { + return h; + } -public class TridiagonalMatrixAlgorithm { - - private Grid grid; - - private double a; - private double b; - private double c; - - private double[] alpha; - private double[] beta; - - public TridiagonalMatrixAlgorithm(Grid grid) { - this.grid = grid; - final int N = grid.getGridDensityValue(); - alpha = new double[N + 2]; - beta = new double[N + 2]; - } - - /** - * Calculates the solution {@code V} using the tridiagonal matrix algorithm. - * This performs a backwards sweep from {@code N - 1} to {@code 0} where {@code N} - * is the grid density value. The coefficients {@code alpha} and {@code beta} - * should have been precalculated - * @param V the array containing the {@code N}th value previously calculated from the respective boundary condition - */ - - public void sweep(double[] V) { - for (int j = grid.getGridDensityValue() - 1; j >= 0; j--) - V[j] = alpha[j + 1] * V[j + 1] + beta[j + 1]; - } - - /** - * Calculates the {@code alpha} coefficients as part of the tridiagonal matrix algorithm. - */ - - public void evaluateAlpha() { - for (int i = 1, N = grid.getGridDensityValue(); i < N; i++) { - alpha[i + 1] = c / (b - a * alpha[i]); - } - } - - public void evaluateBeta(final double[] U) { - evaluateBeta(U, 2, grid.getGridDensityValue() + 1); - } - - /** - * Calculates the {@code beta} coefficients as part of the tridiagonal matrix algorithm. - */ - - public void evaluateBeta(final double[] U, final int start, final int endExclusive) { - final double tau = grid.getTimeStep(); - for (int i = start; i < endExclusive; i++) - beta[i] = beta(U[i - 1] / tau, phi(i - 1), i); - } - - public double beta(final double f, final double phi, final int i) { - return (f + phi + a * beta[i - 1]) / (b - a * alpha[i - 1]); - } - - public double phi(int i) { - return 0; - } - - public void setAlpha(final int i, final double alpha) { - this.alpha[i] = alpha; - } - - public void setBeta(final int i, final double beta) { - this.beta[i] = beta; - } - - public double[] getAlpha() { - return alpha; - } - - public double[] getBeta() { - return beta; - } - - public void setCoefA(double a) { - this.a = a; - } - - public void setCoefB(double b) { - this.b = b; - } - - public void setCoefC(double c) { - this.c = c; - } - - protected double getCoefA() { - return a; - } - - protected double getCoefB() { - return b; - } - - protected double getCoefC() { - return c; - } - - public Grid getGrid() { - return grid; - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/package-info.java b/src/main/java/pulse/problem/schemes/package-info.java index 5ab514ab..2b28afa7 100644 --- a/src/main/java/pulse/problem/schemes/package-info.java +++ b/src/main/java/pulse/problem/schemes/package-info.java @@ -3,8 +3,7 @@ * PULsE, including the definition of {@code Grid}s, which determine the * partitioning rules for space and time variables. Specific implementation of * the difference schemes may be found separately in a different package. - * + * * @see pulse.problem.schemes.solvers.Solver */ - -package pulse.problem.schemes; \ No newline at end of file +package pulse.problem.schemes; diff --git a/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java b/src/main/java/pulse/problem/schemes/rte/BlackbodySpectrum.java index 7aaa1782..5f65f01e 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 static pulse.math.MathUtils.fastPowLoop; - +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,77 +18,99 @@ * {@code SplineInterpolator}. * */ - -public class BlackbodySpectrum { - - private double reductionFactor; - private UnivariateFunction interpolation; - - /** - * Creates a {@code BlackbodySpectrum}. Calculates the reduction factor - * δTm/T0, which - * is needed for calculations of the maximum heating. Note the interpolation - * needs to be set - * @param p a problem statement - */ - - public BlackbodySpectrum(NonlinearProblem p) { - final double maxHeating = p.getProperties().maximumHeating((Pulse2D)p.getPulse()); - reductionFactor = maxHeating / ((double) p.getProperties().getTestTemperature().getValue()); - } - - /** - * Calculates the spectral radiance, which is equal to the spectral power - * divided by π, at the given coordinate. - * - * @param x the geometric coordinate at which calculation should be performed - * @return the spectral radiance at {@code x} - */ - - public double radianceAt(double x) { - return radiance(interpolation.value(x)); - } - - /** - * Calculates the emissive power at the given coordinate. This is equal to - * 0.25 T0Tm [1 - * +δTm /T0 θ (x) - * ]4, where θ is the reduced temperature. - * - * @param x the geometric coordinate inside the sample - * @return the local emissive power value - */ - - public double powerAt(double x) { - return emissivePower(interpolation.value(x)); - } - - /** - * Sets a new function for the spatial temperature profile. The function is - * generally constructed using a {@code SplineInterpolator} - * - * @param interpolation - */ - - public void setInterpolation(UnivariateFunction interpolation) { - this.interpolation = interpolation; - } - - public UnivariateFunction getInterpolation() { - return interpolation; - } - - @Override - public String toString() { - return "[" + getClass().getSimpleName() + ": Rel. heating = " + reductionFactor + "]"; - } - - private double emissivePower(double reducedTemperature) { - return 0.25 / reductionFactor * fastPowLoop(1.0 + reducedTemperature * reductionFactor, 4); - } - - private double radiance(double reducedTemperature) { - return emissivePower(reducedTemperature) / Math.PI; - } +public class BlackbodySpectrum implements Serializable { + + private static final long serialVersionUID = 4628793608666198231L; + private transient UnivariateFunction interpolation; + private double reductionFactor; + + /** + * Creates a {@code BlackbodySpectrum}. Calculates the reduction factor + * δTm/T0, which is + * needed for calculations of the maximum heating. Note the interpolation + * needs to be set + * + * @param p a problem statement + */ + public BlackbodySpectrum(NonlinearProblem p) { + final double maxHeating = p.getProperties().maximumHeating((Pulse2D) p.getPulse()); + reductionFactor = maxHeating / ((double) p.getProperties().getTestTemperature().getValue()); + } + + @Override + 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); + } + + /** + * Calculates the spectral radiance, which is equal to the spectral power + * divided by π, at the given coordinate. + * + * @param x the geometric coordinate at which calculation should be + * performed + * @return the spectral radiance at {@code x} + */ + public double radianceAt(double x) { + return radiance(interpolation.value(x)); + } + + /** + * Calculates the emissive power at the given coordinate. + * + * @param x the geometric coordinate inside the sample + * @return the local emissive power value + */ + public double powerAt(double x) { + return emissivePower(interpolation.value(x)); + } + + /** + * Sets a new function for the spatial temperature profile. The function is + * generally constructed using a {@code SplineInterpolator} + * + * @param interpolation + */ + public void setInterpolation(UnivariateFunction interpolation) { + this.interpolation = interpolation; + } + + public UnivariateFunction getInterpolation() { + return interpolation; + } + + 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 69b32644..23a128fa 100644 --- a/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java +++ b/src/main/java/pulse/problem/schemes/rte/DerivativeCalculator.java @@ -1,63 +1,61 @@ package pulse.problem.schemes.rte; +import java.io.Serializable; + /** - * This is basically a coupling interface between a {@code Solver} and a {@code RadiativeTransferSolver}. + * This is basically a coupling interface between a {@code Solver} and a + * {@code RadiativeTransferSolver}. * */ - -public interface DerivativeCalculator { - - /** - * Calculates the average value of the flux derivatives at the {@code uIndex} - * grid point on the current and previous timesteps. - * - * @param uIndex the grid point index - * @return the time-averaged value of the flux derivative at {@code uIndex} - */ - - public double meanFluxDerivative(int uIndex); - - /** - * Calculates the average value of the flux derivatives at the first grid point - * on the current and previous timesteps. - * - * @return the time-averaged value of the flux derivative at the front surface - */ - - public double meanFluxDerivativeFront(); - - /** - * Calculates the average value of the flux derivatives at the last grid point - * on the current and previous timesteps. - * - * @return the time-averaged value of the flux derivative at the rear surface - */ - - public double meanFluxDerivativeRear(); - - /** - * Calculates the flux derivative at the {@code uIndex} grid point. - * - * @param uIndex the grid point index - * @return the value of the flux derivative at {@code uIndex} - */ - - public double fluxDerivative(int uIndex); - - /** - * Calculates the flux derivative at the front surface. - * - * @return the value of the flux derivative at the front surface - */ - - public double fluxDerivativeFront(); - - /** - * Calculates the flux derivative at the rear surface. - * - * @return the value of the flux derivative at the rear surface - */ - - public double fluxDerivativeRear(); - -} \ No newline at end of file +public interface DerivativeCalculator extends Serializable { + + /** + * Calculates the average value of the flux derivatives at the + * {@code uIndex} grid point on the current and previous timesteps. + * + * @param uIndex the grid point index + * @return the time-averaged value of the flux derivative at {@code uIndex} + */ + public double meanFluxDerivative(int uIndex); + + /** + * Calculates the average value of the flux derivatives at the first grid + * point on the current and previous timesteps. + * + * @return the time-averaged value of the flux derivative at the front + * surface + */ + public double meanFluxDerivativeFront(); + + /** + * Calculates the average value of the flux derivatives at the last grid + * point on the current and previous timesteps. + * + * @return the time-averaged value of the flux derivative at the rear + * surface + */ + public double meanFluxDerivativeRear(); + + /** + * Calculates the flux derivative at the {@code uIndex} grid point. + * + * @param uIndex the grid point index + * @return the value of the flux derivative at {@code uIndex} + */ + public double fluxDerivative(int uIndex); + + /** + * Calculates the flux derivative at the front surface. + * + * @return the value of the flux derivative at the front surface + */ + public double fluxDerivativeFront(); + + /** + * Calculates the flux derivative at the rear surface. + * + * @return the value of the flux derivative at the rear surface + */ + public double fluxDerivativeRear(); + +} diff --git a/src/main/java/pulse/problem/schemes/rte/Fluxes.java b/src/main/java/pulse/problem/schemes/rte/Fluxes.java index a086a2b2..be74c53d 100644 --- a/src/main/java/pulse/problem/schemes/rte/Fluxes.java +++ b/src/main/java/pulse/problem/schemes/rte/Fluxes.java @@ -1,80 +1,94 @@ 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 abstract class Fluxes implements DerivativeCalculator { - private int N; - private double opticalThickness; - private double[] fluxes; - private double[] storedFluxes; - - public Fluxes(NumericProperty gridDensity, NumericProperty opticalThickness) { - setOpticalThickness(opticalThickness); - setDensity(gridDensity); - } - - /** - * Stores all currently calculated fluxes in a separate array. - */ - - public void store() { - System.arraycopy(fluxes, 0, storedFluxes, 0, N + 1); // store previous results - } - - /** - * Retrieves the currently calculated flux at the {@code i} grid point - * - * @param i the index of the grid point - * @return the flux value at the specified grid point - */ - - public double getFlux(int i) { - return fluxes[i]; - } - - /** - * Sets the flux at the {@code i} grid point - * - * @param i the index of the grid point - */ - - public void setFlux(int i, double value) { - this.fluxes[i] = value; - } - - /** - * Retrieves the previously calculated flux at the {@code i} grid point. - * - * @param i the index of the grid point - * @return the previous flux value at the specified grid point - * @see store() - */ - - public double getStoredFlux(int i) { - return storedFluxes[i]; - } - - public double getOpticalGridStep() { - return opticalThickness/((double)N); - } - - public int getDensity() { - return N; - } - - public double getOpticalThickness() { - return opticalThickness; - } - - public void setDensity(NumericProperty gridDensity) { - this.N = (int)gridDensity.getValue(); - fluxes = new double[N + 1]; - storedFluxes = new double[N + 1]; - } - - public void setOpticalThickness(NumericProperty opticalThickness) { - this.opticalThickness = (double)opticalThickness.getValue(); - } - -} \ No newline at end of file + private int N; + private double opticalThickness; + private double[] fluxes; + private double[] storedFluxes; + + public Fluxes(NumericProperty gridDensity, NumericProperty opticalThickness) { + setOpticalThickness(opticalThickness); + setDensity(gridDensity); + } + + /** + * Stores all currently calculated fluxes in a separate array. + */ + 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. + * + * @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; + } + + /** + * Retrieves the currently calculated flux at the {@code i} grid point + * + * @param i the index of the grid point + * @return the flux value at the specified grid point + */ + public double getFlux(int i) { + return fluxes[i]; + } + + /** + * Sets the flux at the {@code i} grid point + * + * @param i the index of the grid point + */ + public void setFlux(int i, double value) { + this.fluxes[i] = value; + } + + /** + * Retrieves the previously calculated flux at the {@code i} grid point. + * + * @param i the index of the grid point + * @return the previous flux value at the specified grid point + * @see store() + */ + public double getStoredFlux(int i) { + return storedFluxes[i]; + } + + public double getOpticalGridStep() { + return opticalThickness / ((double) N); + } + + public int getDensity() { + return N; + } + + public double getOpticalThickness() { + return opticalThickness; + } + + public final void setDensity(NumericProperty gridDensity) { + this.N = (int) gridDensity.getValue(); + init(); + } + + public void init() { + fluxes = new double[N + 1]; + storedFluxes = new double[N + 1]; + } + + public final void setOpticalThickness(NumericProperty opticalThickness) { + this.opticalThickness = (double) opticalThickness.getValue(); + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java index 9351579a..f569717d 100644 --- a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java @@ -4,66 +4,67 @@ public class FluxesAndExplicitDerivatives extends Fluxes { - private double fd[]; - private double fdStored[]; - - public FluxesAndExplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { - super(gridDensity, opticalThickness); - } - - @Override - public void setDensity(NumericProperty gridDensity) { - super.setDensity(gridDensity); - fd = new double[getDensity() + 1]; - fdStored = new double[getDensity() + 1]; - } - - @Override - public double fluxDerivative(int index) { - return fd[index]; - } - - @Override - public double fluxDerivativeRear() { - return fd[getDensity()]; - } - - @Override - public double fluxDerivativeFront() { - return fd[0]; - } - - @Override - public void store() { - super.store(); - System.arraycopy(fd, 0, fdStored, 0, fd.length); // store previous results - } - - @Override - public double meanFluxDerivative(int uIndex) { - return 0.5 * (fd[uIndex] + fdStored[uIndex]); - } - - @Override - public double meanFluxDerivativeFront() { - return 0.5 * (fd[0] + fdStored[0]); - } - - @Override - public double meanFluxDerivativeRear() { - return 0.5 * (fd[getDensity()] + fdStored[getDensity()]); - } - - public double getStoredFluxDerivative(int index) { - return fdStored[index]; - } - - public double getFluxDerivative(int i) { - return fd[i]; - } - - public void setFluxDerivative(int i, double f) { - fd[i] = f; - } - -} \ No newline at end of file + 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(); + fd = new double[getDensity() + 1]; + fdStored = new double[getDensity() + 1]; + } + + @Override + public double fluxDerivative(int index) { + return fd[index]; + } + + @Override + public double fluxDerivativeRear() { + return fd[getDensity()]; + } + + @Override + public double fluxDerivativeFront() { + return fd[0]; + } + + @Override + public void store() { + super.store(); + System.arraycopy(fd, 0, fdStored, 0, fd.length); // store previous results + } + + @Override + public double meanFluxDerivative(int uIndex) { + return 0.5 * (fd[uIndex] + fdStored[uIndex]); + } + + @Override + public double meanFluxDerivativeFront() { + return 0.5 * (fd[0] + fdStored[0]); + } + + @Override + public double meanFluxDerivativeRear() { + return 0.5 * (fd[getDensity()] + fdStored[getDensity()]); + } + + public double getStoredFluxDerivative(int index) { + return fdStored[index]; + } + + public double getFluxDerivative(int i) { + return fd[i]; + } + + public void setFluxDerivative(int i, double f) { + fd[i] = f; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java b/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java index dc9e6f05..038bc2e2 100644 --- a/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/FluxesAndImplicitDerivatives.java @@ -3,45 +3,47 @@ import pulse.properties.NumericProperty; public class FluxesAndImplicitDerivatives extends Fluxes { - - public FluxesAndImplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { - super(gridDensity, opticalThickness); - } - - @Override - public double meanFluxDerivative(int uIndex) { - double f = (getFlux(uIndex - 1) - getFlux(uIndex + 1)) - + (getStoredFlux(uIndex - 1) - getStoredFlux(uIndex + 1)); - return f * 0.25 / getOpticalGridStep(); - } - - @Override - public double meanFluxDerivativeFront() { - double f = (getFlux(0) - getFlux(1)) + (getStoredFlux(0) - getStoredFlux(1)); - return f * 0.5 / getOpticalGridStep(); - } - - @Override - public double meanFluxDerivativeRear() { - final int N = this.getDensity(); - double f = (getFlux(N - 1) - getFlux(N)) + (getStoredFlux(N - 1) - getStoredFlux(N)); - return f * 0.5 / getOpticalGridStep(); - } - - @Override - public double fluxDerivative(int uIndex) { - return (getFlux(uIndex - 1) - getFlux(uIndex + 1)) * 0.5 / getOpticalGridStep(); - } - - @Override - public double fluxDerivativeFront() { - return (getFlux(0) - getFlux(1)) / getOpticalGridStep(); - } - - @Override - public double fluxDerivativeRear() { - final int N = this.getDensity(); - return (getFlux(N - 1) - getFlux(N)) / getOpticalGridStep(); - } - -} \ No newline at end of file + + private static final long serialVersionUID = -4161296401342482405L; + + public FluxesAndImplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { + super(gridDensity, opticalThickness); + } + + @Override + public double meanFluxDerivative(int uIndex) { + double f = (getFlux(uIndex - 1) - getFlux(uIndex + 1)) + + (getStoredFlux(uIndex - 1) - getStoredFlux(uIndex + 1)); + return f * 0.25 / getOpticalGridStep(); + } + + @Override + public double meanFluxDerivativeFront() { + double f = (getFlux(0) - getFlux(1)) + (getStoredFlux(0) - getStoredFlux(1)); + return f * 0.5 / getOpticalGridStep(); + } + + @Override + public double meanFluxDerivativeRear() { + final int N = this.getDensity(); + double f = (getFlux(N - 1) - getFlux(N)) + (getStoredFlux(N - 1) - getStoredFlux(N)); + return f * 0.5 / getOpticalGridStep(); + } + + @Override + public double fluxDerivative(int uIndex) { + return (getFlux(uIndex - 1) - getFlux(uIndex + 1)) * 0.5 / getOpticalGridStep(); + } + + @Override + public double fluxDerivativeFront() { + return (getFlux(0) - getFlux(1)) / getOpticalGridStep(); + } + + @Override + public double fluxDerivativeRear() { + final int N = this.getDensity(); + return (getFlux(N - 1) - getFlux(N)) / getOpticalGridStep(); + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java b/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java index 607b5334..f761f7c4 100644 --- a/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java +++ b/src/main/java/pulse/problem/schemes/rte/RTECalculationListener.java @@ -1,19 +1,19 @@ package pulse.problem.schemes.rte; +import java.io.Serializable; + /** * Used to listed to status updates in {@code RadiativeTransferSolver} * subclasses. * */ +public interface RTECalculationListener extends Serializable { -public interface RTECalculationListener { - - /** - * Invoked when a sub-step of the RTE solution has finished. - * - * @param status the status of the completed step - */ - - public void onStatusUpdate(RTECalculationStatus status); + /** + * Invoked when a sub-step of the RTE solution has finished. + * + * @param status the status of the completed step + */ + public void onStatusUpdate(RTECalculationStatus status); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java index b656dcb4..83054ca1 100644 --- a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java +++ b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java @@ -4,30 +4,27 @@ * A measure of health for radiative transfer calculations. * */ - public enum RTECalculationStatus { - /** - * The current calculation step finished normally. - */ - - NORMAL, - - /** - * The integrator took too long to finish. - */ - - INTEGRATOR_TIMEOUT, - - /** - * The iterative solver took too long to finish. - */ - - ITERATION_LIMIT_REACHED, - - /** - * The grid density required to reach the error threshold was too large. - */ - - GRID_TOO_LARGE; -} \ No newline at end of file + /** + * The current calculation step finished normally. + */ + NORMAL, + /** + * The integrator took too long to finish. + */ + INTEGRATOR_TIMEOUT, + /** + * The iterative solver took too long to finish. + */ + ITERATION_LIMIT_REACHED, + /** + * 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/RadiativeTransferSolver.java b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java index 3ae192c0..8013c33c 100644 --- a/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java +++ b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java @@ -11,7 +11,7 @@ import pulse.problem.schemes.Grid; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.util.Descriptive; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -24,118 +24,116 @@ * steps with listeners. * */ - public abstract class RadiativeTransferSolver extends PropertyHolder implements Reflexive, Descriptive { - private Fluxes fluxes; - private List rteListeners; - - /** - * Dummy constructor. - * - */ - - public RadiativeTransferSolver() { - rteListeners = new ArrayList<>(); - } - - /** - * Launches a calculation of the radiative transfer equation. - * - * @param temperatureArray the input temperature profile - * @return the status of calculation - */ - - public abstract RTECalculationStatus compute(double[] temperatureArray); - - /** - * Retrieves the parameters from {@code p} and {@code grid} needed to run the - * calculations. Resets the flux arrays. - * - * @param p the problem statement - * @param grid the grid - */ - - public void init(ParticipatingMedium p, Grid grid) { - if (fluxes != null) { - fluxes.setDensity(grid.getGridDensity()); - var properties = (ThermoOpticalProperties)p.getProperties(); - fluxes.setOpticalThickness(properties.getOpticalThickness()); - } - } - - /** - * Performs interpolation with natural cubic splines using the input arguments. - * - * @param tempArray an array of data defined on a previously initialised grid. - * @return a {@code UnivariateFunction} generated with a - * {@code SplineInterpolator} - */ - - public UnivariateFunction interpolateTemperatureProfile(final double[] tempArray) { - var xArray = new double[tempArray.length + 2]; - IntStream.range(0, xArray.length).forEach(i -> xArray[i] = opticalCoordinateAt(i - 1)); - - var tarray = new double[tempArray.length + 2]; - System.arraycopy(tempArray, 0, tarray, 1, tempArray.length - 1); - - final double[] p1 = new double[] { xArray[1], tempArray[0] }; - final double[] p2 = new double[] { xArray[2], tempArray[1] }; - tarray[0] = linearExtrapolation(p1, p2, xArray[0]); - - final double[] p3 = new double[] { xArray[xArray.length - 2], tempArray[tempArray.length - 1] }; - final double[] p4 = new double[] { xArray[xArray.length - 3], tempArray[tempArray.length - 2] }; - tarray[tarray.length - 1] = linearExtrapolation(p3, p4, xArray[xArray.length - 1]); - - return (new SplineInterpolator()).interpolate(xArray, tarray); - } - - /** - * Retrieves the optical coordinate corresponding to the grid index {@code i} - * - * @param i the external grid index - * @return τ0/N i - */ - - public double opticalCoordinateAt(final int i) { - return fluxes.getOpticalGridStep() * i; - } - - @Override - public boolean ignoreSiblings() { - return true; - } - - @Override - public String getPrefix() { - return "Radiative Transfer Solver"; - } - - public List getRTEListeners() { - return rteListeners; - } - - /** - * Adds a listener that can listen to status updates. - * - * @param listener a listener to track the calculation progress - */ - - public void addRTEListener(RTECalculationListener listener) { - rteListeners.add(listener); - } - - public void fireStatusUpdate(RTECalculationStatus status) { - for (RTECalculationListener l : getRTEListeners()) - l.onStatusUpdate(status); - } - - public Fluxes getFluxes() { - return fluxes; - } - - public void setFluxes(Fluxes fluxes) { - this.fluxes = fluxes; - } - -} \ No newline at end of file + private Fluxes fluxes; + private final List rteListeners; + + /** + * Dummy constructor. + * + */ + public RadiativeTransferSolver() { + rteListeners = new ArrayList<>(); + } + + /** + * Launches a calculation of the radiative transfer equation. + * + * @param temperatureArray the input temperature profile + * @return the status of calculation + */ + public abstract RTECalculationStatus compute(double[] temperatureArray); + + /** + * Retrieves the parameters from {@code p} and {@code grid} needed to run + * the calculations.Resets the flux arrays. + * + * @param p + * @param grid the grid + */ + public void init(ParticipatingMedium p, Grid grid) { + if (fluxes != null) { + fluxes.setDensity(grid.getGridDensity()); + fluxes.init(); + ThermoOpticalProperties top = (ThermoOpticalProperties) p.getProperties(); + fluxes.setOpticalThickness(top.getOpticalThickness()); + } + } + + /** + * Performs interpolation with natural cubic splines using the input + * arguments. + * + * @param tempArray an array of data defined on a previously initialised + * grid. + * @return a {@code UnivariateFunction} generated with a + * {@code SplineInterpolator} + */ + public UnivariateFunction interpolateTemperatureProfile(final double[] tempArray) { + var xArray = new double[tempArray.length + 2]; + IntStream.range(0, xArray.length).forEach(i -> xArray[i] = opticalCoordinateAt(i - 1)); + + var tarray = new double[tempArray.length + 2]; + System.arraycopy(tempArray, 0, tarray, 1, tempArray.length - 1); + + final double[] p1 = new double[]{xArray[1], tempArray[0]}; + final double[] p2 = new double[]{xArray[2], tempArray[1]}; + tarray[0] = linearExtrapolation(p1, p2, xArray[0]); + + final double[] p3 = new double[]{xArray[xArray.length - 2], tempArray[tempArray.length - 1]}; + final double[] p4 = new double[]{xArray[xArray.length - 3], tempArray[tempArray.length - 2]}; + tarray[tarray.length - 1] = linearExtrapolation(p3, p4, xArray[xArray.length - 1]); + + return (new SplineInterpolator()).interpolate(xArray, tarray); + } + + /** + * Retrieves the optical coordinate corresponding to the grid index + * {@code i} + * + * @param i the external grid index + * @return τ0/N i + */ + public double opticalCoordinateAt(final int i) { + return fluxes.getOpticalGridStep() * i; + } + + @Override + public boolean ignoreSiblings() { + return true; + } + + @Override + public String getPrefix() { + return "Radiative Transfer Solver"; + } + + public List getRTEListeners() { + return rteListeners; + } + + /** + * Adds a listener that can listen to status updates. + * + * @param listener a listener to track the calculation progress + */ + public void addRTEListener(RTECalculationListener listener) { + rteListeners.add(listener); + } + + public void fireStatusUpdate(RTECalculationStatus status) { + for (RTECalculationListener l : getRTEListeners()) { + l.onStatusUpdate(status); + } + } + + public final Fluxes getFluxes() { + return fluxes; + } + + public final void setFluxes(Fluxes fluxes) { + this.fluxes = fluxes; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/AdaptiveIntegrator.java b/src/main/java/pulse/problem/schemes/rte/dom/AdaptiveIntegrator.java index 614f1bd1..efbba180 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/AdaptiveIntegrator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/AdaptiveIntegrator.java @@ -11,253 +11,253 @@ import java.time.Duration; import java.time.Instant; -import java.util.List; +import java.util.Set; import pulse.math.linear.Vector; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; /** * An ODE integrator with an adaptive step size. * */ - public abstract class AdaptiveIntegrator extends ODEIntegrator { - private HermiteInterpolator hermite; - - private double atol; - private double rtol; - private double scalingFactor; - - private boolean firstRun; - private boolean rescaled; - - private Instant start; - private double timeThreshold; - - public AdaptiveIntegrator(Discretisation intensities) { - super(intensities); - atol = (double) def(ATOL).getValue(); - rtol = (double) def(RTOL).getValue(); - scalingFactor = (double) def(GRID_SCALING_FACTOR).getValue(); - timeThreshold = (double) def(RTE_INTEGRATION_TIMEOUT).getValue(); - hermite = new HermiteInterpolator(); - } - - @Override - public RTECalculationStatus integrate() { - Vector[] v; - final var intensities = getDiscretisation(); - final var quantities = intensities.getQuantities(); - - int N = intensities.getGrid().getDensity(); - final int total = intensities.getOrdinates().getTotalNodes(); - rescaled = false; - - final int nPositiveStart = intensities.getOrdinates().getFirstPositiveNode(); - final int nNegativeStart = intensities.getOrdinates().getFirstNegativeNode(); - final int halfLength = nNegativeStart - nPositiveStart; - - RTECalculationStatus status = RTECalculationStatus.NORMAL; - - for (double error = 1.0, relFactor = 0.0, i0Max = 0, i1Max = 0; (error > atol + relFactor * rtol) - && status == RTECalculationStatus.NORMAL; N = intensities.getGrid() - .getDensity(), status = sanityCheck()) { - - start = Instant.now(); - error = 0; - - treatZeroIndex(); - - /* + private HermiteInterpolator hermite; + + private double atol; + private double rtol; + private double scalingFactor; + + private boolean firstRun; + private boolean rescaled; + + private Instant start; + private double timeThreshold; + + public AdaptiveIntegrator(Discretisation intensities) { + super(intensities); + atol = (double) def(ATOL).getValue(); + rtol = (double) def(RTOL).getValue(); + scalingFactor = (double) def(GRID_SCALING_FACTOR).getValue(); + timeThreshold = (double) def(RTE_INTEGRATION_TIMEOUT).getValue(); + hermite = new HermiteInterpolator(); + } + + @Override + public RTECalculationStatus integrate() { + Vector[] v; + final var intensities = getDiscretisation(); + final var quantities = intensities.getQuantities(); + + int N = intensities.getGrid().getDensity(); + final int total = intensities.getOrdinates().getTotalNodes(); + rescaled = false; + + final int nPositiveStart = intensities.getOrdinates().getFirstPositiveNode(); + final int nNegativeStart = intensities.getOrdinates().getFirstNegativeNode(); + final int halfLength = nNegativeStart - nPositiveStart; + + RTECalculationStatus status = RTECalculationStatus.NORMAL; + + for (double error = 1.0, relFactor = 0.0, i0Max = 0, i1Max = 0; (error > atol + relFactor * rtol) + && status == RTECalculationStatus.NORMAL; N = intensities.getGrid() + .getDensity(), status = sanityCheck()) { + + start = Instant.now(); + error = 0; + + treatZeroIndex(); + + /* * First set of ODE's. Initial condition corresponds to I(0) /t ----> tau0 The * streams propagate in the positive hemisphere - */ - - intensities.intensitiesLeftBoundary(getEmissionFunction()); // initial value for tau = 0 - i0Max = (new Vector(quantities.getIntensities()[0])).maxAbsComponent(); + */ + intensities.intensitiesLeftBoundary(getEmissionFunction()); // initial value for tau = 0 + i0Max = (new Vector(quantities.getIntensities()[0])).maxAbsComponent(); - firstRun = true; + firstRun = true; - for (int j = 0; j < N && error < atol + relFactor * rtol; j++) { + for (int j = 0; j < N && error < atol + relFactor * rtol; j++) { - v = step(j, 1.0); - System.arraycopy(v[0].getData(), 0, quantities.getIntensities()[j + 1], nPositiveStart, halfLength); + v = step(j, 1.0); + System.arraycopy(v[0].getData(), 0, quantities.getIntensities()[j + 1], nPositiveStart, halfLength); - i1Max = (new Vector(quantities.getIntensities()[j + 1])).maxAbsComponent(); - relFactor = Math.max(i0Max, i1Max); - i0Max = i1Max; + i1Max = (new Vector(quantities.getIntensities()[j + 1])).maxAbsComponent(); + relFactor = Math.max(i0Max, i1Max); + i0Max = i1Max; - error = v[1].maxAbsComponent(); - } + error = v[1].maxAbsComponent(); + } - /* + /* * Second set of ODE. Initial condition corresponds to I(tau0) /0 <---- t The * streams propagate in the negative hemisphere - */ - - intensities.intensitiesRightBoundary(getEmissionFunction()); // initial value for tau = tau_0 - i0Max = (new Vector(quantities.getIntensities()[N])).maxAbsComponent(); - - firstRun = true; - - for (int j = N; j > 0 && error < atol + relFactor * rtol; j--) { - - v = step(j, -1.0); - System.arraycopy(v[0].getData(), 0, quantities.getIntensities()[j - 1], nNegativeStart, halfLength); - - i1Max = (new Vector(quantities.getIntensities()[j - 1])).maxAbsComponent(); - relFactor = Math.max(i0Max, i1Max); - i0Max = i1Max; - - error = v[1].maxAbsComponent(); - } - - // store derivatives for Hermite interpolation - for (int i = 0; i < total; i++) { - quantities.setDerivative(N, i, quantities.getDerivative(N - 1, i)); - quantities.setDerivative(0, i, quantities.getDerivative(1, i)); - } - - if (error > atol + relFactor * rtol) { - reduceStepSize(); - hermite.clear(); - } - - } - - return status; - - } - - private RTECalculationStatus sanityCheck() { - if (!isValueSensible(def(DOM_GRID_DENSITY), - getDiscretisation().getGrid().getDensity())) - return RTECalculationStatus.GRID_TOO_LARGE; - else if (Duration.between(Instant.now(), start).toSeconds() > timeThreshold) - return RTECalculationStatus.INTEGRATOR_TIMEOUT; - return RTECalculationStatus.NORMAL; - } - - public abstract Vector[] step(final int j, final double sign); - - public void reduceStepSize() { - var intensities = getDiscretisation(); - final int nNew = (roundEven(scalingFactor * intensities.getGrid().getDensity())); - generateGrid(nNew); - intensities.getQuantities().init(intensities.getGrid().getDensity(), intensities.getOrdinates().getTotalNodes()); - rescaled = true; - } - - public boolean wasRescaled() { - return rescaled; - } - - /** - * Generates a uniform grid using the argument as the density. - * - * @param nNew new grid density - */ - - public void generateGrid(int nNew) { - getDiscretisation().getGrid().generateUniformBase(nNew, true); - } - - private int roundEven(double a) { - return (int) (a / 2 * 2); - } - - public NumericProperty getRelativeTolerance() { - return derive(RTOL, rtol); - } - - public NumericProperty getAbsoluteTolerance() { - return derive(ATOL, atol); - } - - public NumericProperty getGridScalingFactor() { - return derive(GRID_SCALING_FACTOR, scalingFactor); - } - - public void setRelativeTolerance(NumericProperty p) { - if (p.getType() != RTOL) - throw new IllegalArgumentException("Illegal type: " + p.getType()); - this.rtol = (double) p.getValue(); - } - - public void setAbsoluteTolerance(NumericProperty p) { - if (p.getType() != ATOL) - throw new IllegalArgumentException("Illegal type: " + p.getType()); - this.atol = (double) p.getValue(); - } - - public void setGridScalingFactor(NumericProperty p) { - if (p.getType() != GRID_SCALING_FACTOR) - throw new IllegalArgumentException("Illegal type: " + p.getType()); - this.scalingFactor = (double) p.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case RTOL: - setRelativeTolerance(property); - break; - case ATOL: - setAbsoluteTolerance(property); - break; - case GRID_SCALING_FACTOR: - setGridScalingFactor(property); - break; - case RTE_INTEGRATION_TIMEOUT: - setTimeThreshold(property); - break; - default: - return; - } - - firePropertyChanged(this, property); - - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(RTOL)); - list.add(def(ATOL)); - list.add(def(GRID_SCALING_FACTOR)); - list.add(def(RTE_INTEGRATION_TIMEOUT)); - return list; - } - - @Override - public String toString() { - return super.toString() + " : " + this.getRelativeTolerance() + " ; " + this.getAbsoluteTolerance() + " ; " - + this.getGridScalingFactor(); - } - - public NumericProperty getTimeThreshold() { - return derive(RTE_INTEGRATION_TIMEOUT, (double) timeThreshold); - } - - public void setTimeThreshold(NumericProperty timeThreshold) { - if (timeThreshold.getType() == RTE_INTEGRATION_TIMEOUT) - this.timeThreshold = ((Number) timeThreshold.getValue()).longValue(); - } - - public boolean isFirstRun() { - return firstRun; - } - - public void setFirstRun(boolean firstRun) { - this.firstRun = firstRun; - } - - public HermiteInterpolator getHermiteInterpolator() { - return hermite; - } - -} \ No newline at end of file + */ + intensities.intensitiesRightBoundary(getEmissionFunction()); // initial value for tau = tau_0 + i0Max = (new Vector(quantities.getIntensities()[N])).maxAbsComponent(); + + firstRun = true; + + for (int j = N; j > 0 && error < atol + relFactor * rtol; j--) { + + v = step(j, -1.0); + System.arraycopy(v[0].getData(), 0, quantities.getIntensities()[j - 1], nNegativeStart, halfLength); + + i1Max = (new Vector(quantities.getIntensities()[j - 1])).maxAbsComponent(); + relFactor = Math.max(i0Max, i1Max); + i0Max = i1Max; + + error = v[1].maxAbsComponent(); + } + + // store derivatives for Hermite interpolation + for (int i = 0; i < total; i++) { + quantities.setDerivative(N, i, quantities.getDerivative(N - 1, i)); + quantities.setDerivative(0, i, quantities.getDerivative(1, i)); + } + + if (error > atol + relFactor * rtol) { + reduceStepSize(); + hermite.clear(); + } + + } + + return status; + + } + + private RTECalculationStatus sanityCheck() { + if (!isValueSensible(def(DOM_GRID_DENSITY), + getDiscretisation().getGrid().getDensity())) { + return RTECalculationStatus.GRID_TOO_LARGE; + } else if (Duration.between(Instant.now(), start).toSeconds() > timeThreshold) { + return RTECalculationStatus.INTEGRATOR_TIMEOUT; + } + return RTECalculationStatus.NORMAL; + } + + public abstract Vector[] step(final int j, final double sign); + + public void reduceStepSize() { + var intensities = getDiscretisation(); + final int nNew = (roundEven(scalingFactor * intensities.getGrid().getDensity())); + generateGrid(nNew); + intensities.getQuantities().init(intensities.getGrid().getDensity(), intensities.getOrdinates().getTotalNodes()); + rescaled = true; + } + + public boolean wasRescaled() { + return rescaled; + } + + /** + * Generates a uniform grid using the argument as the density. + * + * @param nNew new grid density + */ + public void generateGrid(int nNew) { + getDiscretisation().getGrid().generateUniformBase(nNew, true); + } + + private int roundEven(double a) { + return (int) (a / 2 * 2); + } + + public NumericProperty getRelativeTolerance() { + return derive(RTOL, rtol); + } + + public NumericProperty getAbsoluteTolerance() { + return derive(ATOL, atol); + } + + public NumericProperty getGridScalingFactor() { + return derive(GRID_SCALING_FACTOR, scalingFactor); + } + + public void setRelativeTolerance(NumericProperty p) { + if (p.getType() != RTOL) { + throw new IllegalArgumentException("Illegal type: " + p.getType()); + } + this.rtol = (double) p.getValue(); + } + + public void setAbsoluteTolerance(NumericProperty p) { + if (p.getType() != ATOL) { + throw new IllegalArgumentException("Illegal type: " + p.getType()); + } + this.atol = (double) p.getValue(); + } + + public void setGridScalingFactor(NumericProperty p) { + if (p.getType() != GRID_SCALING_FACTOR) { + throw new IllegalArgumentException("Illegal type: " + p.getType()); + } + this.scalingFactor = (double) p.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case RTOL: + setRelativeTolerance(property); + break; + case ATOL: + setAbsoluteTolerance(property); + break; + case GRID_SCALING_FACTOR: + setGridScalingFactor(property); + break; + case RTE_INTEGRATION_TIMEOUT: + setTimeThreshold(property); + break; + default: + return; + } + + firePropertyChanged(this, property); + + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(RTOL); + set.add(ATOL); + set.add(GRID_SCALING_FACTOR); + set.add(RTE_INTEGRATION_TIMEOUT); + return set; + } + + @Override + public String toString() { + return super.toString() + " : " + this.getRelativeTolerance() + " ; " + this.getAbsoluteTolerance() + " ; " + + this.getGridScalingFactor(); + } + + public NumericProperty getTimeThreshold() { + return derive(RTE_INTEGRATION_TIMEOUT, (double) timeThreshold); + } + + public void setTimeThreshold(NumericProperty timeThreshold) { + if (timeThreshold.getType() == RTE_INTEGRATION_TIMEOUT) { + this.timeThreshold = ((Number) timeThreshold.getValue()).longValue(); + } + } + + public boolean isFirstRun() { + return firstRun; + } + + public void setFirstRun(boolean firstRun) { + this.firstRun = firstRun; + } + + public HermiteInterpolator getHermiteInterpolator() { + return hermite; + } + +} 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 a32c75c8..5d86e8f0 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ButcherTableau.java @@ -1,129 +1,132 @@ 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; import pulse.util.Descriptive; /** - * The Butcher tableau coefficients used by the explicit Runge-Kutta solvers. Variable - * names correspond to the standard notations. + * The Butcher tableau coefficients used by the explicit Runge-Kutta solvers. + * Variable names correspond to the standard notations. * */ +public class ButcherTableau implements Descriptive, Serializable { -public class ButcherTableau implements Descriptive { + private static final long serialVersionUID = -8856270519744473886L; + private Vector b; + private Vector bHat; + private Vector c; + private SquareMatrix coefs; - private Vector b; - private Vector bHat; - private Vector c; - private SquareMatrix coefs; + private boolean fsal; + private String name; - private boolean fsal; - private String name; + public final static String DEFAULT_TABLEAU = "BS23"; - public final static String DEFAULT_TABLEAU = "BS23"; + public ButcherTableau(String name, double[][] coefs, double[] c, double[] b, double[] bHat, boolean fsal) { - public ButcherTableau(String name, double[][] coefs, double[] c, double[] b, double[] bHat, boolean fsal) { + if (c.length != b.length || c.length != bHat.length) { + throw new IllegalArgumentException("Check dimensions of the input vectors"); + } - if (c.length != b.length || c.length != bHat.length) - throw new IllegalArgumentException("Check dimensions of the input vectors"); + if (coefs.length != coefs[0].length || coefs.length != c.length) { + throw new IllegalArgumentException("Check dimensions of the input matrix array"); + } - if (coefs.length != coefs[0].length || coefs.length != c.length) - throw new IllegalArgumentException("Check dimensions of the input matrix array"); + this.name = name; + this.fsal = fsal; - this.name = name; - this.fsal = fsal; + this.coefs = Matrices.createSquareMatrix(coefs); + this.c = new Vector(c); + this.b = new Vector(b); + this.bHat = new Vector(bHat); + } - this.coefs = Matrices.createMatrix(coefs); - this.c = new Vector(c); - this.b = new Vector(b); - this.bHat = new Vector(bHat); - } + public int numberOfStages() { + return b.dimension(); + } - public int numberOfStages() { - return b.dimension(); - } + public SquareMatrix getMatrix() { + return coefs; + } - public SquareMatrix getMatrix() { - return coefs; - } + public void setMatrix(SquareMatrix coefs) { + this.coefs = coefs; + } - public void setMatrix(SquareMatrix coefs) { - this.coefs = coefs; - } + public Vector getEstimator() { + return bHat; + } - public Vector getEstimator() { - return bHat; - } + public void setEstimator(Vector bHat) { + this.bHat = bHat; + } - public void setEstimator(Vector bHat) { - this.bHat = bHat; - } + public Vector getInterpolator() { + return b; + } - public Vector getInterpolator() { - return b; - } + public void setInterpolator(Vector b) { + this.b = b; + } - public void setInterpolator(Vector b) { - this.b = b; - } + public Vector getC() { + return c; + } - public Vector getC() { - return c; - } + public void setC(Vector c) { + this.c = c; + } - public void setC(Vector c) { - this.c = c; - } + public boolean isFSAL() { + return fsal; + } - public boolean isFSAL() { - return fsal; - } + @Override + public String toString() { + return name; + } - @Override - public String toString() { - return name; - } - - public String printTableau() { + public String printTableau() { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); - for (int i = 0; i < b.dimension(); i++) { + for (int i = 0; i < b.dimension(); i++) { - sb.append(String.format("%n%3.8f | ", c.get(i))); + sb.append(String.format("%n%3.8f | ", c.get(i))); - for (int j = 0; j < b.dimension(); j++) { - sb.append(String.format("%3.8f ", coefs.get(i, j))); - } + for (int j = 0; j < b.dimension(); j++) { + sb.append(String.format("%3.8f ", coefs.get(i, j))); + } - } + } - sb.append(System.lineSeparator()); + sb.append(System.lineSeparator()); - for (int i = 0; i < b.dimension() + 1; i++) { - sb.append(String.format("%-12s", "-")); - } + for (int i = 0; i < b.dimension() + 1; i++) { + sb.append(String.format("%-12s", "-")); + } - sb.append(System.lineSeparator() + String.format("%-10s | ", "-")); + sb.append(System.lineSeparator() + String.format("%-10s | ", "-")); - for (int i = 0; i < b.dimension(); i++) { - sb.append(String.format("%3.8f ", b.get(i))); - } + for (int i = 0; i < b.dimension(); i++) { + sb.append(String.format("%3.8f ", b.get(i))); + } - sb.append(System.lineSeparator() + String.format("%-10s | ", "-")); + sb.append(System.lineSeparator() + String.format("%-10s | ", "-")); - for (int i = 0; i < b.dimension(); i++) { - sb.append(String.format("%3.8f ", bHat.get(i))); - } + for (int i = 0; i < b.dimension(); i++) { + sb.append(String.format("%3.8f ", bHat.get(i))); + } - return sb.toString(); + return sb.toString(); - } + } - @Override - public String describe() { - return "Butcher tableau"; - } + @Override + public String describe() { + return "Butcher tableau"; + } -} \ No newline at end of file +} 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 1d49c2bf..4e3de939 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/CompositeGaussianQuadrature.java @@ -1,87 +1,90 @@ package pulse.problem.schemes.rte.dom; +import java.io.Serializable; import pulse.math.LegendrePoly; import pulse.math.MathUtils; /** - * A composite Gaussian quadrature for numerical evaluation of the scattering integral in - * one-dimensional heat transfer. + * A composite Gaussian quadrature for numerical evaluation of the scattering + * integral in one-dimensional heat transfer. + * * @author Teymur Aliev, Vadim Zborovskii, Artem Lunev * */ - -public class CompositeGaussianQuadrature { - - private LegendrePoly poly; - - private double[] roots; - - private double[] nodes; - private double[] weights; - - private int n; - - /** - * Constructs a composite Gaussian quadrature for an even {@code n} - * @param n an even integer - */ - - public CompositeGaussianQuadrature(final int n) { - if(n % 2 != 0) - throw new IllegalArgumentException(n + " is odd. Even number expected."); - this.n = n; - poly = new LegendrePoly(n / 2); - roots = poly.roots(); - nodes(); - weights(); - } - - private void nodes() { - - nodes = new double[n]; - weights = new double[n]; - - for (int i = 0; i < n / 2; i++) { - - nodes[i] = 0.5 * (1.0 + roots[i]); - nodes[i + n / 2] = -0.5 * (1.0 + roots[i]); - - } - - } - - /** - * Calculates the Gaussian weights. Uses the formula by Abramowitz & Stegun - * (Abramowitz & Stegun 1972, p. 887)) - */ - - private void weights() { - double denominator = 1; - - for (int i = 0; i < n / 2; i++) { - denominator = (1 - roots[i] * roots[i]) * MathUtils.fastPowLoop(poly.derivative(roots[i]), 2); - weights[i] = 1.0 / denominator; - weights[i + n / 2] = weights[i]; - } - - } - - /** - * The weights of the composite quadrature. - * @return the weights - */ - - public double[] getWeights() { - return weights; - } - - /** - * The cosine nodes of the composite quadrature. - * @return the cosine nodes - */ - - public double[] getNodes() { - return nodes; - } - -} \ No newline at end of file +public class CompositeGaussianQuadrature implements Serializable { + + private static final long serialVersionUID = 780827333372523309L; + + private LegendrePoly poly; + + private double[] roots; + + private double[] nodes; + private double[] weights; + + private int n; + + /** + * Constructs a composite Gaussian quadrature for an even {@code n} + * + * @param n an even integer + */ + public CompositeGaussianQuadrature(final int n) { + if (n % 2 != 0) { + throw new IllegalArgumentException(n + " is odd. Even number expected."); + } + this.n = n; + poly = new LegendrePoly(n / 2); + roots = poly.roots(); + nodes(); + weights(); + } + + private void nodes() { + + nodes = new double[n]; + weights = new double[n]; + + for (int i = 0; i < n / 2; i++) { + + nodes[i] = 0.5 * (1.0 + roots[i]); + nodes[i + n / 2] = -0.5 * (1.0 + roots[i]); + + } + + } + + /** + * Calculates the Gaussian weights. Uses the formula by Abramowitz & Stegun + * (Abramowitz & Stegun 1972, p. 887)) + */ + private void weights() { + double denominator = 1; + + for (int i = 0; i < n / 2; i++) { + denominator = (1 - roots[i] * roots[i]) * MathUtils.fastPowLoop(poly.derivative(roots[i]), 2); + weights[i] = 1.0 / denominator; + weights[i + n / 2] = weights[i]; + } + + } + + /** + * The weights of the composite quadrature. + * + * @return the weights + */ + public double[] getWeights() { + return weights; + } + + /** + * The cosine nodes of the composite quadrature. + * + * @return the cosine nodes + */ + public double[] getNodes() { + return nodes; + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java b/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java new file mode 100644 index 00000000..5dfccb4d --- /dev/null +++ b/src/main/java/pulse/problem/schemes/rte/dom/CornetteSchanksPF.java @@ -0,0 +1,44 @@ +package pulse.problem.schemes.rte.dom; + +import static java.lang.Math.sqrt; + +import pulse.problem.statements.ParticipatingMedium; +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 + * + * @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; + + public CornetteSchanksPF(ThermoOpticalProperties top, Discretisation intensities) { + super(top, intensities); + } + + @Override + public void init(ThermoOpticalProperties top) { + super.init(top); + final double anisotropy = getAnisotropyFactor(); + g2 = 2.0 * anisotropy; + final double aSq = anisotropy * anisotropy; + onePlusGSq = 1.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); + final double f = onePlusGSq - g2 * cosine; + return anisoFactor * (1.0 + cosine * cosine) / (f * sqrt(f)); + } + +} 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 d16237a0..0e3e5d47 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java @@ -6,8 +6,9 @@ import pulse.problem.schemes.rte.FluxesAndExplicitDerivatives; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.rte.RadiativeTransferSolver; +import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; @@ -20,147 +21,157 @@ * solve to RTE. * */ - public class DiscreteOrdinatesMethod extends RadiativeTransferSolver { - private InstanceDescriptor integratorDescriptor = new InstanceDescriptor( - "Integrator selector", AdaptiveIntegrator.class); - private InstanceDescriptor iterativeSolverSelector = new InstanceDescriptor( - "Iterative solver selector", IterativeSolver.class); - private InstanceDescriptor phaseFunctionSelector = new InstanceDescriptor( - "Phase function selector", PhaseFunction.class); - - private AdaptiveIntegrator integrator; - private IterativeSolver iterativeSolver; - - /** - * Constructs a discrete ordinates solver using the parameters (emissivity, - * scattering albedo and optical thickness) declared by the {@code problem} - * object. - * - * @param problem the coupled problem statement - * @param grid the heat problem grid - */ - - public DiscreteOrdinatesMethod(ParticipatingMedium problem, Grid grid) { - super(); - var properties = (ThermoOpticalProperties)problem.getProperties(); - setFluxes(new FluxesAndExplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); - - var discrete = new Discretisation(problem); - - integratorDescriptor.setSelectedDescriptor(TRBDF2.class.getSimpleName()); - setIntegrator(integratorDescriptor.newInstance(AdaptiveIntegrator.class, discrete)); - - iterativeSolverSelector.setSelectedDescriptor(FixedIterations.class.getSimpleName()); - setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class)); - - phaseFunctionSelector.setSelectedDescriptor(HenyeyGreensteinPF.class.getSimpleName()); - phaseFunctionSelector.addListener(() -> initPhaseFunction(problem, discrete)); - initPhaseFunction(problem, discrete); - - init(problem, grid); - - integratorDescriptor.addListener(() -> setIntegrator( - integratorDescriptor.newInstance(AdaptiveIntegrator.class, discrete))); - - iterativeSolverSelector - .addListener(() -> setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class))); - - } - - @Override - public RTECalculationStatus compute(double[] tempArray) { - integrator.getEmissionFunction().setInterpolation(interpolateTemperatureProfile(tempArray)); - - var status = iterativeSolver.doIterations(integrator); - - if (status == RTECalculationStatus.NORMAL) - fluxesAndDerivatives(tempArray.length); - - fireStatusUpdate(status); - return status; - } - - private void fluxesAndDerivatives(final int nExclusive) { - final var interpolation = integrator.getHermiteInterpolator().interpolateOnExternalGrid(nExclusive, integrator); - - final double DOUBLE_PI = 2.0 * Math.PI; - final var discrete = integrator.getDiscretisation(); - var fluxes = (FluxesAndExplicitDerivatives) getFluxes(); - - for (int i = 0; i < nExclusive; i++) { - fluxes.setFlux(i, DOUBLE_PI * discrete.firstMoment(interpolation[0], i)); - fluxes.setFluxDerivative(i, -DOUBLE_PI * discrete.firstMoment(interpolation[1], i)); - } - } - - @Override - public String getDescriptor() { - return "Discrete Ordinates Method (DOM)"; - } - - @Override - public void init(ParticipatingMedium problem, Grid grid) { - super.init(problem, grid); - initPhaseFunction(problem, integrator.getDiscretisation()); - integrator.init(problem); - integrator.getPhaseFunction().init(problem); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(integratorDescriptor); - list.add(iterativeSolverSelector); - list.add(phaseFunctionSelector); - return list; - } - - public AdaptiveIntegrator getIntegrator() { - return integrator; - } - - public InstanceDescriptor getIntegratorDescriptor() { - return integratorDescriptor; - } - - public void setIntegrator(AdaptiveIntegrator integrator) { - this.integrator = integrator; - integrator.setParent(this); - } - - public IterativeSolver getIterativeSolver() { - return iterativeSolver; - } - - public InstanceDescriptor getIterativeSolverSelector() { - return iterativeSolverSelector; - } - - public void setIterativeSolver(IterativeSolver solver) { - this.iterativeSolver = solver; - solver.setParent(this); - } - - public InstanceDescriptor getPhaseFunctionSelector() { - return phaseFunctionSelector; - } - - @Override - public String toString() { - return getClass().getSimpleName() + " : " + integrator.toString() + " ; " + iterativeSolver.toString(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally left blank - } - - private void initPhaseFunction(ParticipatingMedium problem, Discretisation discrete) { - var pf = phaseFunctionSelector.newInstance(PhaseFunction.class, problem, discrete); - integrator.setPhaseFunction(pf); - pf.init(problem); - } - -} \ No newline at end of file + private static final long serialVersionUID = 2881363894773388976L; + private InstanceDescriptor integratorDescriptor = new InstanceDescriptor( + "Integrator selector", AdaptiveIntegrator.class); + private InstanceDescriptor iterativeSolverSelector = new InstanceDescriptor( + "Iterative solver selector", IterativeSolver.class); + private InstanceDescriptor phaseFunctionSelector = new InstanceDescriptor( + "Phase function selector", PhaseFunction.class); + + private AdaptiveIntegrator integrator; + private IterativeSolver iterativeSolver; + + /** + * Constructs a discrete ordinates solver using the parameters (emissivity, + * scattering albedo and optical thickness) declared by the {@code problem} + * object. + * + * @param problem the coupled problem statement + * @param grid the heat problem grid + */ + public DiscreteOrdinatesMethod(ParticipatingMedium problem, Grid grid) { + super(); + var properties = (ThermoOpticalProperties) problem.getProperties(); + setFluxes(new FluxesAndExplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); + + var discrete = new Discretisation(properties); + + integratorDescriptor.setSelectedDescriptor(TRBDF2.class.getSimpleName()); + setIntegrator(integratorDescriptor.newInstance(AdaptiveIntegrator.class, discrete)); + + iterativeSolverSelector.setSelectedDescriptor(FixedIterations.class.getSimpleName()); + setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class)); + + phaseFunctionSelector.setSelectedDescriptor(HenyeyGreensteinPF.class.getSimpleName()); + initPhaseFunction(properties, discrete); + + init(problem, grid); + + integratorDescriptor.addListener(() -> setIntegrator( + integratorDescriptor.newInstance(AdaptiveIntegrator.class, discrete)) + ); + + iterativeSolverSelector + .addListener(() -> setIterativeSolver(iterativeSolverSelector.newInstance(IterativeSolver.class))); + + phaseFunctionSelector.addListener(() -> initPhaseFunction(properties, discrete)); + } + + @Override + public RTECalculationStatus compute(double[] tempArray) { + integrator.getEmissionFunction().setInterpolation(interpolateTemperatureProfile(tempArray)); + + var status = iterativeSolver.doIterations(integrator); + + if (status == RTECalculationStatus.NORMAL) { + fluxesAndDerivatives(tempArray.length); + } + + fireStatusUpdate(status); + return status; + } + + private void fluxesAndDerivatives(final int nExclusive) { + final var interpolation = integrator.getHermiteInterpolator() + .interpolateOnExternalGrid(nExclusive, integrator); + + final double DOUBLE_PI = 2.0 * Math.PI; + final var discrete = integrator.getDiscretisation(); + var fluxes = (FluxesAndExplicitDerivatives) getFluxes(); + + for (int i = 0; i < nExclusive; i++) { + double flux = DOUBLE_PI * discrete.firstMoment(interpolation[0], i); + fluxes.setFlux(i, flux); + fluxes.setFluxDerivative(i, + -DOUBLE_PI * discrete.firstMoment(interpolation[1], i)); + } + + } + + @Override + public String getDescriptor() { + return "Discrete Ordinates Method (DOM)"; + } + + @Override + public void init(ParticipatingMedium problem, Grid grid) { + super.init(problem, grid); + var top = (ThermoOpticalProperties) problem.getProperties(); + initPhaseFunction(top, + integrator.getDiscretisation()); + integrator.init(problem); + integrator.getPhaseFunction().init(top); + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(integratorDescriptor); + list.add(iterativeSolverSelector); + list.add(phaseFunctionSelector); + return list; + } + + public final AdaptiveIntegrator getIntegrator() { + return integrator; + } + + public final InstanceDescriptor getIntegratorDescriptor() { + return integratorDescriptor; + } + + public final void setIntegrator(AdaptiveIntegrator integrator) { + this.integrator = integrator; + integrator.setParent(this); + firePropertyChanged(this, integratorDescriptor); + } + + public final IterativeSolver getIterativeSolver() { + return iterativeSolver; + } + + public final InstanceDescriptor getIterativeSolverSelector() { + return iterativeSolverSelector; + } + + public final void setIterativeSolver(IterativeSolver solver) { + this.iterativeSolver = solver; + solver.setParent(this); + firePropertyChanged(this, iterativeSolverSelector); + } + + public final InstanceDescriptor getPhaseFunctionSelector() { + return phaseFunctionSelector; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " : " + integrator.toString() + " ; " + iterativeSolver.toString(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + private void initPhaseFunction(ThermoOpticalProperties top, Discretisation discrete) { + var pf = phaseFunctionSelector.newInstance(PhaseFunction.class, top, discrete); + integrator.setPhaseFunction(pf); + pf.init(top); + firePropertyChanged(this, phaseFunctionSelector); + } + +} 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 cb965541..e81389a9 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java @@ -1,96 +1,99 @@ 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. + * 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 implements Serializable { + + private static final long serialVersionUID = -3997479317699236996L; + private double[][] I; + private double[][] Ik; + private double[][] f; + private double[][] fk; + private double[] qLast; + + /** + * Constructs a set of quantities based on the specified spatial and angular + * discretisation. + * + * @param gridDensity the DOM grid density + * @param ordinates the number of angular nodes + */ + public DiscreteQuantities(int gridDensity, int ordinates) { + init(gridDensity, ordinates); + } + + protected final void init(int gridDensity, int ordinates) { + I = new double[gridDensity + 1][ordinates]; + f = new double[gridDensity + 1][ordinates]; + Ik = new double[gridDensity + 1][ordinates]; + fk = new double[gridDensity + 1][ordinates]; + qLast = new double[ordinates]; + } + + protected void store() { + final int n = I.length; + final int m = I[0].length; -class DiscreteQuantities { - - private double[][] I; - private double[][] Ik; - private double[][] f; - private double[][] fk; - private double[] qLast; - - /** - * Constructs a set of quantities based on the specified - * spatial and angular discretisation. - * @param gridDensity the DOM grid density - * @param ordinates the number of angular nodes - */ - - public DiscreteQuantities(int gridDensity, int ordinates) { - init(gridDensity, ordinates); - } - - public void init(int gridDensity, int ordinates) { - I = new double[gridDensity + 1][ordinates]; - f = new double[gridDensity + 1][ordinates]; - Ik = new double[gridDensity + 1][ordinates]; - fk = new double[gridDensity + 1][ordinates]; - qLast = new double[ordinates]; - } - - public void store() { - final int n = I.length; - final int m = I[0].length; - - Ik = new double[n][m]; - fk = new double[n][m]; - - /* + Ik = new double[n][m]; + fk = new double[n][m]; + + /* * store k-th components - */ - for (int j = 0; j < Ik.length; j++) { - System.arraycopy(I[j], 0, Ik[j], 0, Ik[0].length); - System.arraycopy(f[j], 0, fk[j], 0, fk[0].length); - } - - } - - public double[][] getIntensities() { - return I; - } - - public double[][] getDerivatives() { - return f; - } - - public double getQLast(int i) { - return qLast[i]; - } - - public void setQLast(int i, double q) { - this.qLast[i] = q; - } - - public double getDerivative(int i, int j) { - return f[i][j]; - } - - public void setDerivative(int i, int j, double f) { - this.f[i][j] = f; - } - - public double getStoredIntensity(final int i, final int j) { - return Ik[i][j]; - } - - public double getStoredDerivative(final int i, final int j) { - return fk[i][j]; - } - - public void setStoredDerivative(final int i, final int j, final double f) { - this.f[i][j] = f; - } - - public double getIntensity(int i, int j) { - return I[i][j]; - } - - public void setIntensity(int i, int j, double value) { - I[i][j] = value; - } - -} \ No newline at end of file + */ + for (int j = 0; j < Ik.length; j++) { + System.arraycopy(I[j], 0, Ik[j], 0, Ik[0].length); + System.arraycopy(f[j], 0, fk[j], 0, fk[0].length); + } + + } + + public final double[][] getIntensities() { + return I; + } + + public final double[][] getDerivatives() { + return f; + } + + protected final double getQLast(int i) { + return qLast[i]; + } + + protected final void setQLast(int i, double q) { + this.qLast[i] = q; + } + + public final double getDerivative(int i, int j) { + return f[i][j]; + } + + public final void setDerivative(int i, int j, double f) { + this.f[i][j] = f; + } + + public final double getStoredIntensity(final int i, final int j) { + return Ik[i][j]; + } + + public final double getStoredDerivative(final int i, final int j) { + return fk[i][j]; + } + + public final void setStoredDerivative(final int i, final int j, final double f) { + this.f[i][j] = f; + } + + public final double getIntensity(int i, int j) { + return I[i][j]; + } + + public final void setIntensity(int i, int j, double value) { + I[i][j] = value; + } + +} 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 2521c5dc..91be63d3 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/Discretisation.java @@ -2,14 +2,11 @@ import static java.lang.Math.PI; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import pulse.io.readers.QuadratureReader; import pulse.problem.schemes.rte.BlackbodySpectrum; -import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; @@ -24,265 +21,260 @@ * and intensities based on the discrete ordinates method. * */ - public class Discretisation extends PropertyHolder { - private DiscreteQuantities quantities; - private StretchedGrid grid; - private OrdinateSet ordinates; - private DiscreteSelector quadSelector; - - private double emissivity; - private double boundaryFluxFactor; - - /** - * Constructs a {@code DiscreteIntensities} with the default {@code OrdinateSet} - * and a new uniform grid. - * - * @param problem the problem statement - */ - - public Discretisation(ParticipatingMedium problem) { - - quadSelector = new DiscreteSelector<>(QuadratureReader.getInstance(), "/quadratures/", - "Quadratures.list"); - quadSelector.setDefaultSelection(OrdinateSet.DEFAULT_SET); - ordinates = quadSelector.getDefaultSelection(); - quadSelector.addListener(() -> { - ordinates = (OrdinateSet)quadSelector.getValue(); - quantities.init(grid.getDensity(), ordinates.getTotalNodes()); - }); - - var properties = (ThermoOpticalProperties) problem.getProperties(); - setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); - quantities = new DiscreteQuantities(grid.getDensity(), ordinates.getTotalNodes()); - setEmissivity((double) problem.getProperties().getEmissivity().getValue()); - } - - /** - * Calculates the incident radiation iwi - * Iij, which is the zeroth moment of the intensities. The - * calculation uses the symmetry of the quadrature weights for positive and - * negativ nodes. - * - * @param j spatial index - * @return the incident radiation at {@code j} - */ - - public double incidentRadation(final int j) { - double integral = 0; - - final int nHalf = ordinates.getFirstNegativeNode(); - final int nStart = ordinates.getFirstPositiveNode(); - - // uses symmetry - for (int i = nStart; i < nHalf; i++) { - integral += ordinates.getWeight(i) - * (quantities.getIntensity(j, i) + quantities.getIntensity(j, i + nHalf)); - } - - if (ordinates.hasZeroNode()) - integral += ordinates.getWeight(0) * quantities.getIntensity(j, 0); - - return integral; - } - - /** - * Calculates the incident radiation iwi - * Iij, by performing simple summation for node points - * between {@code startInclusive} and {@code endExclusive}. - * - * @param j spatial index - * @param startInclusive lower bound for summation - * @param endExclusive upper bound (exclusive) for summation - * @return the partial incident radiation at {@code j} - * @see pulse.problem.schemes.rte.dom.LinearAnisotropicPF - */ - - public double incidentRadiation(final int j, final int startInclusive, final int endExclusive) { - double integral = 0; - - for (int i = startInclusive; i < endExclusive; i++) { - integral += ordinates.getWeight(i) * quantities.getIntensity(j, i); - } - - return integral; - - } - - /** - * Calculates the first moment - * iwiμiextij, - * which can be applied e.g. for flux or flux derivative calculation. The - * calculation uses the symmetry of the quadrature weights for positive and - * negativ nodes. - * - * @param j spatial index - * @return the first moment at {@code j} - */ - - public double firstMoment(final double[][] ext, final int j) { - double integral = 0; - - final int nHalf = ordinates.getFirstNegativeNode(); - final int nStart = ordinates.getFirstPositiveNode(); - - // uses symmetry - for (int i = nStart; i < nHalf; i++) { - integral += ordinates.getWeight(i) * (ext[j][i] - ext[j][i + nHalf]) * ordinates.getNode(i); - } - - return integral; - } - - /** - * Calculates the net flux at {@code j}. - * - * @param j the spatial coordinate - * @return the flux - * @see firstMoment(double[][],int) - */ - - public double flux(final int j) { - return firstMoment(quantities.getIntensities(), j); - } - - /** - * Calculates the partial flux by performing a simple summation bounded by the - * arguments. - * - * @param j the spatial index - * @param startInclusive node index lower bound - * @param endExclusive node index upper bound (exclusive) - * @return the partial flux - */ - - public double flux(final int j, final int startInclusive, final int endExclusive) { - double integral = 0; - - for (int i = startInclusive; i < endExclusive; i++) { - integral += ordinates.getWeight(i) * quantities.getIntensity(j, i) * ordinates.getNode(i); - } - - return integral; - } - - /** - * Calculates the flux at the left boundary using an alternative formula. - * - * @param emissionFunction the emission function - * @return the net flux at the left boundary - */ - - public double fluxLeft(final BlackbodySpectrum emissionFunction) { - final int nHalf = ordinates.getFirstNegativeNode(); - return emissivity * PI * (emissionFunction.radianceAt(0.0) + 2.0 * flux(0, nHalf, ordinates.getTotalNodes())); - } - - /** - * Calculates the flux at the right boundary using an alternative formula. - * - * @param emissionFunction the emission function - * @return the net flux at the right boundary - */ - - public double fluxRight(final BlackbodySpectrum emissionFunction) { - final int nHalf = ordinates.getFirstNegativeNode(); - final int nStart = ordinates.getFirstPositiveNode(); - return -emissivity * PI - * (emissionFunction.radianceAt(grid.getDimension()) - 2.0 * flux(grid.getDensity(), nStart, nHalf)); - } - - /** - * Calculates the reflected intensity (positive angles, first half of indices) - * at the left boundary (τ = 0). - * - * @param ef the emission function - */ - - public void intensitiesLeftBoundary(final BlackbodySpectrum ef) { - final int nHalf = ordinates.getFirstNegativeNode(); - final int nStart = ordinates.getFirstPositiveNode(); - - for (int i = nStart; i < nHalf; i++) { - // for positive streams - quantities.setIntensity(0, i, ef.radianceAt(0.0) - boundaryFluxFactor * fluxLeft(ef)); - } - - } - - /** - * Calculates the reflected intensity (negative angles, second half of indices) - * at the right boundary (τ = τ0). - * - * @param ef the emission function - */ - - public void intensitiesRightBoundary(final BlackbodySpectrum ef) { - - final int N = grid.getDensity(); - final int nHalf = ordinates.getFirstNegativeNode(); - final double tau0 = grid.getDimension(); - - for (int i = nHalf; i < ordinates.getTotalNodes(); i++) { - // for negative streams - quantities.setIntensity(N, i, ef.radianceAt(tau0) + boundaryFluxFactor * fluxRight(ef)); - } - - } - - protected void setEmissivity(double emissivity) { - this.emissivity = emissivity; - boundaryFluxFactor = (1.0 - emissivity) / (emissivity * PI); - } - - public OrdinateSet getOrdinates() { - return ordinates; - } - - public void setOrdinateSet(OrdinateSet set) { - this.ordinates = set; - quantities.init(grid.getDensity(), ordinates.getTotalNodes()); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally left blank - } - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(quadSelector)); - } - - @Override - public String getDescriptor() { - return "Discretisation"; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder("( "); - sb.append("Quadrature: " + ordinates.getName() + " ; "); - sb.append("Grid: " + grid.toString()); - return sb.toString(); - } - - public StretchedGrid getGrid() { - return grid; - } - - public void setGrid(StretchedGrid grid) { - this.grid = grid; - this.grid.setParent(this); - } - - public DiscreteQuantities getQuantities() { - return quantities; - } - - public DiscreteSelector getQuadratureSelector() { - return quadSelector; - } - -} \ No newline at end of file + private static final long serialVersionUID = 7069987686586911101L; + private DiscreteQuantities quantities; + private StretchedGrid grid; + private OrdinateSet ordinates; + private DiscreteSelector quadSelector; + + private double emissivity; + private double boundaryFluxFactor; + + /** + * Constructs a {@code DiscreteIntensities} with the default + * {@code OrdinateSet} and a new uniform grid. + * + * @param properties + */ + public Discretisation(ThermoOpticalProperties properties) { + + quadSelector = new DiscreteSelector<>(QuadratureReader.getInstance(), "/quadratures/", + "Quadratures.list"); + quadSelector.setDefaultSelection(OrdinateSet.DEFAULT_SET); + ordinates = quadSelector.getDefaultSelection(); + quadSelector.addListener(() -> { + ordinates = (OrdinateSet) quadSelector.getValue(); + quantities.init(grid.getDensity(), ordinates.getTotalNodes()); + this.firePropertyChanged(this, quadSelector); + }); + + setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); + quantities = new DiscreteQuantities(grid.getDensity(), ordinates.getTotalNodes()); + setEmissivity((double) properties.getEmissivity().getValue()); + } + + /** + * Calculates the incident radiation + * iwi + * Iij, which is the zeroth moment of the intensities. + * The calculation uses the symmetry of the quadrature weights for positive + * and negativ nodes. + * + * @param j spatial index + * @return the incident radiation at {@code j} + */ + public double incidentRadation(final int j) { + double integral = 0; + + final int nHalf = ordinates.getFirstNegativeNode(); + final int nStart = ordinates.getFirstPositiveNode(); + + // uses symmetry + for (int i = nStart; i < nHalf; i++) { + integral += ordinates.getWeight(i) + * (quantities.getIntensity(j, i) + quantities.getIntensity(j, i + nHalf)); + } + + if (ordinates.hasZeroNode()) { + integral += ordinates.getWeight(0) * quantities.getIntensity(j, 0); + } + + return integral; + } + + /** + * Calculates the incident radiation + * iwi + * Iij, by performing simple summation for node points + * between {@code startInclusive} and {@code endExclusive}. + * + * @param j spatial index + * @param startInclusive lower bound for summation + * @param endExclusive upper bound (exclusive) for summation + * @return the partial incident radiation at {@code j} + * @see pulse.problem.schemes.rte.dom.LinearAnisotropicPF + */ + public double incidentRadiation(final int j, final int startInclusive, final int endExclusive) { + double integral = 0; + + for (int i = startInclusive; i < endExclusive; i++) { + integral += ordinates.getWeight(i) * quantities.getIntensity(j, i); + } + + return integral; + + } + + /** + * Calculates the first moment + * iwiμiextij, + * which can be applied e.g. for flux or flux derivative calculation. The + * calculation uses the symmetry of the quadrature weights for positive and + * negativ nodes. + * + * @param j spatial index + * @return the first moment at {@code j} + */ + public double firstMoment(final double[][] ext, final int j) { + double integral = 0; + + final int nHalf = ordinates.getFirstNegativeNode(); + final int nStart = ordinates.getFirstPositiveNode(); + + // uses symmetry + for (int i = nStart; i < nHalf; i++) { + integral += ordinates.getWeight(i) * (ext[j][i] - ext[j][i + nHalf]) * ordinates.getNode(i); + } + + return integral; + } + + /** + * Calculates the net flux at {@code j}. + * + * @param j the spatial coordinate + * @return the flux + * @see firstMoment(double[][],int) + */ + public double flux(final int j) { + return firstMoment(quantities.getIntensities(), j); + } + + /** + * Calculates the partial flux by performing a simple summation bounded by + * the arguments. + * + * @param j the spatial index + * @param startInclusive node index lower bound + * @param endExclusive node index upper bound (exclusive) + * @return the partial flux + */ + public double flux(final int j, final int startInclusive, final int endExclusive) { + double integral = 0; + + for (int i = startInclusive; i < endExclusive; i++) { + integral += ordinates.getWeight(i) * quantities.getIntensity(j, i) * ordinates.getNode(i); + } + + return integral; + } + + /** + * Calculates the flux at the left boundary using an alternative formula. + * + * @param emissionFunction the emission function + * @return the net flux at the left boundary + */ + public double fluxLeft(final BlackbodySpectrum emissionFunction) { + final int nHalf = ordinates.getFirstNegativeNode(); + return emissivity * PI * (emissionFunction.radianceAt(0.0) + 2.0 * flux(0, nHalf, ordinates.getTotalNodes())); + } + + /** + * Calculates the flux at the right boundary using an alternative formula. + * + * @param emissionFunction the emission function + * @return the net flux at the right boundary + */ + public double fluxRight(final BlackbodySpectrum emissionFunction) { + final int nHalf = ordinates.getFirstNegativeNode(); + final int nStart = ordinates.getFirstPositiveNode(); + return -emissivity * PI + * (emissionFunction.radianceAt(grid.getDimension()) - 2.0 * flux(grid.getDensity(), nStart, nHalf)); + } + + /** + * Calculates the reflected intensity (positive angles, first half of + * indices) at the left boundary (τ = 0). + * + * @param ef the emission function + */ + public void intensitiesLeftBoundary(final BlackbodySpectrum ef) { + final int nHalf = ordinates.getFirstNegativeNode(); + final int nStart = ordinates.getFirstPositiveNode(); + + for (int i = nStart; i < nHalf; i++) { + // for positive streams + quantities.setIntensity(0, i, ef.radianceAt(0.0) - boundaryFluxFactor * fluxLeft(ef)); + } + + } + + /** + * Calculates the reflected intensity (negative angles, second half of + * indices) at the right boundary (τ = τ0). + * + * @param ef the emission function + */ + public void intensitiesRightBoundary(final BlackbodySpectrum ef) { + + final int N = grid.getDensity(); + final int nHalf = ordinates.getFirstNegativeNode(); + final double tau0 = grid.getDimension(); + + for (int i = nHalf; i < ordinates.getTotalNodes(); i++) { + // for negative streams + quantities.setIntensity(N, i, ef.radianceAt(tau0) + boundaryFluxFactor * fluxRight(ef)); + } + + } + + protected final void setEmissivity(double emissivity) { + this.emissivity = emissivity; + boundaryFluxFactor = (1.0 - emissivity) / (emissivity * PI); + } + + public OrdinateSet getOrdinates() { + return ordinates; + } + + public void setOrdinateSet(OrdinateSet set) { + this.ordinates = set; + quantities.init(grid.getDensity(), ordinates.getTotalNodes()); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + @Override + public List listedTypes() { + var list = super.listedTypes(); + list.add(quadSelector); + return list; + } + + @Override + public String getDescriptor() { + return "Discretisation"; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("( "); + sb.append("Quadrature: " + ordinates.getName() + " ; "); + sb.append("Grid: " + grid.toString()); + return sb.toString(); + } + + public StretchedGrid getGrid() { + return grid; + } + + public final void setGrid(StretchedGrid grid) { + this.grid = grid; + this.grid.setParent(this); + } + + public DiscreteQuantities getQuantities() { + return quantities; + } + + public DiscreteSelector getQuadratureSelector() { + return quadSelector; + } + +} 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 f8ad0f10..42b98b9a 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ExplicitRungeKutta.java @@ -8,193 +8,187 @@ import pulse.util.DiscreteSelector; /** - * Explicit Runge-Kutta integrator with Hermite interpolation for the solution of one-dimensional radiative transfer problems. - * + * Explicit Runge-Kutta integrator with Hermite interpolation for the solution + * of one-dimensional radiative transfer problems. + * * @author Artem Lunev, Vadim Zborovskii * */ - public class ExplicitRungeKutta extends AdaptiveIntegrator { - private ButcherTableau tableau; - private DiscreteSelector tableauSelector; + private static final long serialVersionUID = -2478658861611086402L; + private ButcherTableau tableau; + private DiscreteSelector tableauSelector; - public ExplicitRungeKutta(Discretisation intensities) { - super(intensities); - - tableauSelector = new DiscreteSelector<>(ButcherTableauReader.getInstance(), "/solvers/", - "Solvers.list"); - tableauSelector.setDefaultSelection(ButcherTableau.DEFAULT_TABLEAU); - tableau = tableauSelector.getDefaultSelection(); - tableauSelector.addListener(() -> tableau = (ButcherTableau)tableauSelector.getValue()); - } + public ExplicitRungeKutta(Discretisation intensities) { + super(intensities); - @Override - public Vector[] step(final int j, final double sign) { + tableauSelector = new DiscreteSelector<>(ButcherTableauReader.getInstance(), "/solvers/", + "Solvers.list"); + tableauSelector.setDefaultSelection(ButcherTableau.DEFAULT_TABLEAU); + tableau = tableauSelector.getDefaultSelection(); + tableauSelector.addListener(() -> setButcherTableau((ButcherTableau) tableauSelector.getValue())); + } - var quantities = getDiscretisation().getQuantities(); - - final var grid = getDiscretisation().getGrid(); - final var ordinates = getDiscretisation().getOrdinates(); + @Override + public Vector[] step(final int j, final double sign) { - final double h = grid.step(j, sign); - final double hSigned = h * sign; - final double t = grid.getNode(j); + var quantities = getDiscretisation().getQuantities(); - final int nPositiveStart = ordinates.getFirstPositiveNode(); - final int nNegativeStart = ordinates.getFirstNegativeNode(); + final var grid = getDiscretisation().getGrid(); + final var ordinates = getDiscretisation().getOrdinates(); - var hermite = getHermiteInterpolator(); - - hermite.a = t; - hermite.bMinusA = hSigned; + final double h = grid.step(j, sign); + final double hSigned = h * sign; + final double t = grid.getNode(j); - /* - * Indices of outward (n1 to n2) and inward (> n3) intensities - */ + final int nPositiveStart = ordinates.getFirstPositiveNode(); + final int nNegativeStart = ordinates.getFirstNegativeNode(); - final int n1 = sign > 0 ? nPositiveStart : nNegativeStart; // either first positive index (e.g. 0) or first - // negative (n/2) - final int n2 = sign > 0 ? nNegativeStart : ordinates.getTotalNodes(); // either first negative index (n/2) or - // n - final int n3 = ordinates.getTotalNodes() - n2; // either nNegativeStart or 0 - final int nH = n2 - n1; + var hermite = getHermiteInterpolator(); - var error = new double[nH]; - var iOutward = new double[nH]; - var iInward = new double[nH]; + hermite.a = t; + hermite.bMinusA = hSigned; - int stages = tableau.numberOfStages(); + /* + * Indices of outward (n1 to n2) and inward (> n3) intensities + */ + final int n1 = sign > 0 ? nPositiveStart : nNegativeStart; // either first positive index (e.g. 0) or first + // negative (n/2) + final int n2 = sign > 0 ? nNegativeStart : ordinates.getTotalNodes(); // either first negative index (n/2) or + // n + final int n3 = ordinates.getTotalNodes() - n2; // either nNegativeStart or 0 + final int nH = n2 - n1; - var q = new double[nH][stages]; // first index - cosine node, second index - stage + var error = new double[nH]; + var iOutward = new double[nH]; + var iInward = new double[nH]; - double bDotQ; - double sum; + int stages = tableau.numberOfStages(); - int increment = (int) (1 * sign); + var q = new double[nH][stages]; // first index - cosine node, second index - stage - /* - * RK Explicit (Embedded) - */ + double bDotQ; + double sum; - /* - * First stage - */ + int increment = (int) (1 * sign); - if (tableau.isFSAL() && ! isFirstRun() ) { // if FSAL + /* + * RK Explicit (Embedded) + */ - for (int l = n1; l < n2; l++) { - q[l - n1][0] = quantities.getQLast(l - n1); // assume first stage is the last stage of last step - } + /* + * First stage + */ + if (tableau.isFSAL() && !isFirstRun()) { // if FSAL - } else { // if not FSAL or on first run + for (int l = n1; l < n2; l++) { + q[l - n1][0] = quantities.getQLast(l - n1); // assume first stage is the last stage of last step + } - for (int l = n1; l < n2; l++) { - q[l - n1][0] = derivative(l, j, t, quantities.getIntensity(j, l)); - } + } else { // if not FSAL or on first run - setFirstRun(false); + for (int l = n1; l < n2; l++) { + q[l - n1][0] = derivative(l, j, t, quantities.getIntensity(j, l)); + } - } + setFirstRun(false); - // in any case + } - for (int l = n1; l < n2; l++) { - quantities.setDerivative(j, l, q[l - n1][0]); // store derivative for inward intensities - error[l - n1] = (tableau.getInterpolator().get(0) - tableau.getEstimator().get(0)) * q[l - n1][0] * hSigned; - } + // in any case + for (int l = n1; l < n2; l++) { + quantities.setDerivative(j, l, q[l - n1][0]); // store derivative for inward intensities + error[l - n1] = (tableau.getInterpolator().get(0) - tableau.getEstimator().get(0)) * q[l - n1][0] * hSigned; + } - /* + /* * Next stages - */ - - for (int m = 1; m < stages; m++) { // <------- STAGES (1...s) + */ + for (int m = 1; m < stages; m++) { // <------- STAGES (1...s) - /* + /* * Calculate interpolated (OUTWARD and INWARD) intensities at each stage from m * = 1 onwards - */ - - double tm = t + hSigned * tableau.getC().get(m); // interpolation point for stage m + */ + double tm = t + hSigned * tableau.getC().get(m); // interpolation point for stage m - for (int l = n1; l < n2; l++) { // find unknown intensities (sum over the outward intensities) + for (int l = n1; l < n2; l++) { // find unknown intensities (sum over the outward intensities) - /* - * OUTWARD - */ + /* + * OUTWARD + */ + sum = tableau.getMatrix().get(m, 0) * q[l - n1][0]; + for (int k = 1; k < m; k++) { + sum += tableau.getMatrix().get(m, k) * q[l - n1][k]; + } - sum = tableau.getMatrix().get(m, 0) * q[l - n1][0]; - for (int k = 1; k < m; k++) - sum += tableau.getMatrix().get(m, k) * q[l - n1][k]; + iOutward[l - n1] = quantities.getIntensity(j, l) + hSigned * sum; // outward intensities are simply + // found from the + // RK explicit expressions - iOutward[l - n1] = quantities.getIntensity(j, l) + hSigned * sum; // outward intensities are simply - // found from the - // RK explicit expressions - - /* + /* * INWARD - */ - - hermite.y0 = quantities.getIntensity(j, l + n3); - hermite.y1 = quantities.getIntensity(j + increment, l + n3); - hermite.d0 = quantities.getDerivative(j, l + n3); - hermite.d1 = quantities.getDerivative(j + increment, l + n3); + */ + hermite.y0 = quantities.getIntensity(j, l + n3); + hermite.y1 = quantities.getIntensity(j + increment, l + n3); + hermite.d0 = quantities.getDerivative(j, l + n3); + hermite.d1 = quantities.getDerivative(j + increment, l + n3); - iInward[l - n1] = hermite.interpolate(tm); // inward intensities are interpolated with - // Hermite polynomials + iInward[l - n1] = hermite.interpolate(tm); // inward intensities are interpolated with + // Hermite polynomials - } + } - /* + /* * Derivatives and associated errors at stage m - */ - - for (int l = n1; l < n2; l++) { - q[l - n1][m] = derivative(l, tm, iOutward, iInward, n1, n2); - quantities.setQLast(l - n1, q[l - n1][m]); - error[l - n1] += (tableau.getInterpolator().get(m) - tableau.getEstimator().get(m)) * q[l - n1][m] - * hSigned; - } + */ + for (int l = n1; l < n2; l++) { + q[l - n1][m] = derivative(l, tm, iOutward, iInward, n1, n2); + quantities.setQLast(l - n1, q[l - n1][m]); + error[l - n1] += (tableau.getInterpolator().get(m) - tableau.getEstimator().get(m)) * q[l - n1][m] + * hSigned; + } - } + } - double[] Is = new double[nH]; + double[] Is = new double[nH]; - /* + /* * Value at next step - */ - - for (int l = 0; l < nH; l++) { - bDotQ = tableau.getInterpolator().dot(new Vector(q[l])); - Is[l] = quantities.getIntensity(j, l + n1) + bDotQ * hSigned; - } - - return new Vector[] { new Vector(Is), new Vector(error) }; - - } - - public ButcherTableau getButcherTableau() { - return tableau; - } - - public void setButcherTableau(ButcherTableau coef) { - this.tableau = coef; - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(tableauSelector); - return list; - } - - @Override - public String toString() { - return super.toString() + " ; " + tableau; - } - - public DiscreteSelector getTableauSelector() { - return tableauSelector; - } - -} \ No newline at end of file + */ + for (int l = 0; l < nH; l++) { + bDotQ = tableau.getInterpolator().dot(new Vector(q[l])); + Is[l] = quantities.getIntensity(j, l + n1) + bDotQ * hSigned; + } + + return new Vector[]{new Vector(Is), new Vector(error)}; + + } + + public ButcherTableau getButcherTableau() { + return tableau; + } + + public void setButcherTableau(ButcherTableau coef) { + this.tableau = coef; + this.firePropertyChanged(this, tableauSelector); + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(tableauSelector); + return list; + } + + @Override + public String toString() { + return super.toString() + " ; " + tableau; + } + + public DiscreteSelector getTableauSelector() { + return 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 3ec1b198..2a9ea834 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/FixedIterations.java @@ -6,41 +6,43 @@ public class FixedIterations extends IterativeSolver { - @Override - public RTECalculationStatus doIterations(AdaptiveIntegrator integrator) { + private static final long serialVersionUID = -7308041206602757928L; - var discrete = integrator.getDiscretisation(); - double relativeError = 100; + @Override + public RTECalculationStatus doIterations(AdaptiveIntegrator integrator) { - double qld = 0; - double qrd = 0; - double qsum; + var discrete = integrator.getDiscretisation(); + double relativeError = 100; - int iterations = 0; + double qld = 0; + double qrd = 0; + double qsum; - RTECalculationStatus status = RTECalculationStatus.NORMAL; - final var ef = integrator.getEmissionFunction(); + int iterations = 0; - for (double ql = 1e8, qr = ql; relativeError > getIterationError() - && status == RTECalculationStatus.NORMAL; status = sanityCheck(status, ++iterations)) { - // do the integration - status = integrator.integrate(); + RTECalculationStatus status = RTECalculationStatus.NORMAL; + final var ef = integrator.getEmissionFunction(); - // get the difference in boundary heat fluxes - qld = discrete.fluxLeft(ef); - qrd = discrete.fluxRight(ef); - qsum = abs(qld - ql) + abs(qrd - qr); - - // if the integrator attempted rescaling, last iteration is not valid anymore - relativeError = integrator.wasRescaled() ? Double.POSITIVE_INFINITY : qsum / (abs(ql) + abs(qr)); - - //use previous iteration - ql = qld; - qr = qrd; - } + for (double ql = 1e8, qr = ql; relativeError > getIterationError() + && status == RTECalculationStatus.NORMAL; status = sanityCheck(status, ++iterations)) { + // do the integration + status = integrator.integrate(); - return status; + // get the difference in boundary heat fluxes + qld = discrete.fluxLeft(ef); + qrd = discrete.fluxRight(ef); + qsum = abs(qld - ql) + abs(qrd - qr); - } + // if the integrator attempted rescaling, last iteration is not valid anymore + relativeError = integrator.wasRescaled() ? Double.POSITIVE_INFINITY : qsum / (abs(ql) + abs(qr)); -} \ No newline at end of file + //use previous iteration + ql = qld; + qr = qrd; + } + + return status; + + } + +} 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 053e9452..12af9709 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/HenyeyGreensteinPF.java @@ -2,39 +2,37 @@ import static java.lang.Math.sqrt; -import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.model.ThermoOpticalProperties; /** * The single-parameter Henyey-Greenstein scattering phase function. * */ - public class HenyeyGreensteinPF extends PhaseFunction { - private double a1; - private double a2; - private double b1; - - public HenyeyGreensteinPF(ParticipatingMedium medium, Discretisation intensities) { - super(medium, intensities); - } - - @Override - public void init(ParticipatingMedium problem) { - super.init(problem); - final double anisotropy = getAnisotropyFactor(); - b1 = 2.0 * anisotropy; - final double aSq = anisotropy * anisotropy; - a1 = 1.0 - aSq; - a2 = 1.0 + aSq; - } - - @Override - public double function(final int i, final int k) { - final var ordinates = getDiscreteIntensities().getOrdinates(); - final double theta = ordinates.getNode(k) * ordinates.getNode(i); - final double f = a2 - b1 * theta; - return a1 / (f * sqrt(f)); - } - -} \ No newline at end of file + private static final long serialVersionUID = -2196189314681832809L; + private double a1; + private double a2; + private double b1; + + public HenyeyGreensteinPF(ThermoOpticalProperties properties, Discretisation intensities) { + super(properties, intensities); + } + + @Override + public void init(ThermoOpticalProperties properties) { + super.init(properties); + final double anisotropy = getAnisotropyFactor(); + b1 = 2.0 * anisotropy; + final double aSq = anisotropy * anisotropy; + a1 = 1.0 - aSq; + a2 = 1.0 + aSq; + } + + @Override + public double function(final int i, final int k) { + final double f = a2 - b1 * cosineTheta(i, k); + return a1 / (f * sqrt(f)); + } + +} 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 18d8f0a7..93c4131d 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/HermiteInterpolator.java @@ -1,130 +1,134 @@ 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 transfer equation with a Runge-Kutta - * solver (either implicit or explicit). + * A globally C1 Hermite interpolator used to interpolate intensities + * and derivatives in discrete ordinates method when solving the radiative + * transfer equation with a Runge-Kutta solver (either implicit or explicit). + * * @author Vadim Zborovskii, Artem Lunev * */ - -public class HermiteInterpolator { - - protected double y1; - protected double y0; - protected double d1; - protected double d0; - - protected double a; - protected double bMinusA; - - public void clear() { - y1 = 0; - y0 = 0; - d1 = 0; - d0 = 0; - a = 0; - bMinusA = 0; - } - - /** - * Interpolates the function at {@code x} - * @param x the value within the specified bounds - * @return the interpolated value - */ - - public double interpolate(final double x) { - final double t = (x - a) / bMinusA; - final double tMinusOne = (t - 1.0); - return t * t * (3.0 - 2.0 * t) * y1 + tMinusOne * tMinusOne * (1.0 + 2.0 * t) * y0 - + (t * t * tMinusOne * d1 + tMinusOne * tMinusOne * t * d0) * bMinusA; - } - - /** - * Calculates the derivative of the interpolant at {@code x} - * @param x the value within the specified bounds - * @return the derivative of the interpolant - */ - - public double derivative(final double x) { - final double t = (x - a) / bMinusA; - final double tt1 = t * (t - 1.0); - - return 6.0 * tt1 * (y0 - y1) / bMinusA + (t * (3.0 * t - 2.0) * d1 + (3.0 * t * t - 4.0 * t + 1.0) * d0); - } - - @Override - public String toString() { - return String.format("%n (%3.6f ; %3.6f), (%3.6f ; %3.6f), (%3.6f, %3.6f)", y0, y1, d0, d1, a, (bMinusA - a)); - } - - /** - * Interpolates intensities and their derivatives w.r.t. tau on EXTERNAL grid - * points of the heat problem solver based on the derivatives on INTERNAL grid - * points of DOM solver. - * @param externalGridSize the number of points in the external grid - * @param integrator the adaptive integrator - * @return a three-dimensional array containing the interpolated intensities and derivatives - */ - - public double[][][] interpolateOnExternalGrid(final int externalGridSize, AdaptiveIntegrator integrator) { - final var discrete = integrator.getDiscretisation(); - final var intensities = discrete.getQuantities().getIntensities(); - final var internalGrid = discrete.getGrid(); - final var derivatives = discrete.getQuantities().getDerivatives(); - final int total = discrete.getOrdinates().getTotalNodes(); - - var iExt = new double[2][externalGridSize][total]; - double t; - - final double hx = internalGrid.getDimension() / (externalGridSize - 1.0); - final int n = internalGrid.getDensity() + 1; - - /* +public class HermiteInterpolator implements Serializable { + + private static final long serialVersionUID = -1973954803574711053L; + protected double y1; + protected double y0; + protected double d1; + protected double d0; + + protected double a; + protected double bMinusA; + + public void clear() { + y1 = 0; + y0 = 0; + d1 = 0; + d0 = 0; + a = 0; + bMinusA = 0; + } + + /** + * Interpolates the function at {@code x} + * + * @param x the value within the specified bounds + * @return the interpolated value + */ + public double interpolate(final double x) { + final double t = (x - a) / bMinusA; + final double tMinusOne = (t - 1.0); + return t * t * (3.0 - 2.0 * t) * y1 + tMinusOne * tMinusOne * (1.0 + 2.0 * t) * y0 + + (t * t * tMinusOne * d1 + tMinusOne * tMinusOne * t * d0) * bMinusA; + } + + /** + * Calculates the derivative of the interpolant at {@code x} + * + * @param x the value within the specified bounds + * @return the derivative of the interpolant + */ + public double derivative(final double x) { + final double t = (x - a) / bMinusA; + final double tt1 = t * (t - 1.0); + + return 6.0 * tt1 * (y0 - y1) / bMinusA + (t * (3.0 * t - 2.0) * d1 + (3.0 * t * t - 4.0 * t + 1.0) * d0); + } + + @Override + public String toString() { + return String.format("%n (%3.6f ; %3.6f), (%3.6f ; %3.6f), (%3.6f, %3.6f)", y0, y1, d0, d1, a, (bMinusA - a)); + } + + /** + * Interpolates intensities and their derivatives w.r.t. tau on EXTERNAL + * grid points of the heat problem solver based on the derivatives on + * INTERNAL grid points of DOM solver. + * + * @param externalGridSize the number of points in the external grid + * @param integrator the adaptive integrator + * @return a three-dimensional array containing the interpolated intensities + * and derivatives + */ + public double[][][] interpolateOnExternalGrid(final int externalGridSize, AdaptiveIntegrator integrator) { + final var discrete = integrator.getDiscretisation(); + final var intensities = discrete.getQuantities().getIntensities(); + final var internalGrid = discrete.getGrid(); + final var derivatives = discrete.getQuantities().getDerivatives(); + final int total = discrete.getOrdinates().getTotalNodes(); + + var iExt = new double[2][externalGridSize][total]; + double t; + + final double hx = internalGrid.getDimension() / (externalGridSize - 1.0); + final int n = internalGrid.getDensity() + 1; + + /* * Loop through the external grid points - */ - outer: for (int i = 0; i < iExt[0].length; i++) { - t = i * hx; + */ + outer: + for (int i = 0; i < iExt[0].length; i++) { + t = i * hx; - // loops through nodes sorted in ascending order - for (int j = 0; j < n; j++) { + // loops through nodes sorted in ascending order + for (int j = 0; j < n; j++) { - /* + /* * if node is greater than t, then the associated function can be interpolated * between points f_i and f_i-1, since t lies between nodes n_i and n_i-1 - */ - if (internalGrid.getNode(j) > t) { // nearest points on internal grid have been found -> j - 1 and j + */ + if (internalGrid.getNode(j) > t) { // nearest points on internal grid have been found -> j - 1 and j - a = internalGrid.getNode(j - 1); - bMinusA = internalGrid.stepLeft(j); + a = internalGrid.getNode(j - 1); + bMinusA = internalGrid.stepLeft(j); - /* + /* * Loops through ordinate set - */ - - for (int k = 0; k < total; k++) { - y0 = intensities[j - 1][k]; - y1 = intensities[j][k]; - d0 = derivatives[j - 1][k]; - d1 = derivatives[j][k]; - iExt[0][i][k] = interpolate(t); // intensity - iExt[1][i][k] = derivative(t); // derivative - } + */ + for (int k = 0; k < total; k++) { + y0 = intensities[j - 1][k]; + y1 = intensities[j][k]; + d0 = derivatives[j - 1][k]; + d1 = derivatives[j][k]; + iExt[0][i][k] = interpolate(t); // intensity + iExt[1][i][k] = derivative(t); // derivative + } - continue outer; // move to next point t + continue outer; // move to next point t - } + } - } + } - for (int k = 0; k < total; k++) { - iExt[0][i][k] = intensities[internalGrid.getDensity()][k]; - iExt[1][i][k] = derivatives[internalGrid.getDensity()][k]; - } + for (int k = 0; k < total; k++) { + iExt[0][i][k] = intensities[internalGrid.getDensity()][k]; + iExt[1][i][k] = derivatives[internalGrid.getDensity()][k]; + } - } + } - return iExt; - } + return iExt; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java b/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java index 86a1f672..6bfb45c1 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java @@ -6,104 +6,106 @@ import static pulse.properties.NumericPropertyKeyword.DOM_ITERATION_ERROR; import static pulse.properties.NumericPropertyKeyword.RTE_MAX_ITERATIONS; -import java.util.ArrayList; -import java.util.List; +import java.util.Set; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; import pulse.util.PropertyHolder; import pulse.util.Reflexive; /** - * Used to iteratively solve the radiative transfer problem.

This is necessary since the latter can - * only be solved separately for rays travelling in the positive or negative hemispheres. The problem - * lies in the fact that the intensities of incoming and outward rays are coupled. The only way to - * solve the coupled set of two Cauchy problems is iterative in nature.

- *

This abstract class only defines the basic functionaly, its implementation is defined by the - * subclasses.

+ * Used to iteratively solve the radiative transfer problem. + *

+ * This is necessary since the latter can only be solved separately for rays + * travelling in the positive or negative hemispheres. The problem lies in the + * fact that the intensities of incoming and outward rays are coupled. The only + * way to solve the coupled set of two Cauchy problems is iterative in + * nature.

+ *

+ * This abstract class only defines the basic functionaly, its implementation is + * defined by the subclasses.

* */ - public abstract class IterativeSolver extends PropertyHolder implements Reflexive { - private double iterationError; - private int maxIterations; - - /** - * Constructs an {@code IterativeSolver} with the default thresholds for - * iteration error and number of iterations. - */ - - public IterativeSolver() { - iterationError = (double) def(DOM_ITERATION_ERROR).getValue(); - maxIterations = (int) def(RTE_MAX_ITERATIONS).getValue(); - } - - /** - * De-facto solves the radiative transfer problem iteratively. - * @param integrator the integerator embedded in the iterative approach - * @return a calculation status - */ - - public abstract RTECalculationStatus doIterations(AdaptiveIntegrator integrator); - - protected RTECalculationStatus sanityCheck(RTECalculationStatus status, int iterations) { - return iterations < maxIterations ? status : ITERATION_LIMIT_REACHED; - } - - public NumericProperty getIterationErrorTolerance() { - return derive(DOM_ITERATION_ERROR, this.iterationError); - } - - public void setIterationErrorTolerance(NumericProperty e) { - if (e.getType() != DOM_ITERATION_ERROR) - throw new IllegalArgumentException("Illegal type: " + e.getType()); - this.iterationError = (double) e.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case DOM_ITERATION_ERROR: - setIterationErrorTolerance(property); - break; - case RTE_MAX_ITERATIONS: - setMaxIterations(property); - break; - default: - return; - } - - firePropertyChanged(this, property); - - } - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.add(def(DOM_ITERATION_ERROR)); - list.add(def(RTE_MAX_ITERATIONS)); - return list; - } - - public NumericProperty getMaxIterations() { - return derive(RTE_MAX_ITERATIONS, maxIterations); - } - - public void setMaxIterations(NumericProperty iterations) { - if (iterations.getType() == RTE_MAX_ITERATIONS) - this.maxIterations = (int) iterations.getValue(); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " : " + this.getIterationErrorTolerance(); - } - - public double getIterationError() { - return iterationError; - } - -} \ No newline at end of file + private double iterationError; + private int maxIterations; + + /** + * Constructs an {@code IterativeSolver} with the default thresholds for + * iteration error and number of iterations. + */ + public IterativeSolver() { + iterationError = (double) def(DOM_ITERATION_ERROR).getValue(); + maxIterations = (int) def(RTE_MAX_ITERATIONS).getValue(); + } + + /** + * De-facto solves the radiative transfer problem iteratively. + * + * @param integrator the integerator embedded in the iterative approach + * @return a calculation status + */ + public abstract RTECalculationStatus doIterations(AdaptiveIntegrator integrator); + + protected RTECalculationStatus sanityCheck(RTECalculationStatus status, int iterations) { + return iterations < maxIterations ? status : ITERATION_LIMIT_REACHED; + } + + public NumericProperty getIterationErrorTolerance() { + return derive(DOM_ITERATION_ERROR, this.iterationError); + } + + public void setIterationErrorTolerance(NumericProperty e) { + if (e.getType() != DOM_ITERATION_ERROR) { + throw new IllegalArgumentException("Illegal type: " + e.getType()); + } + this.iterationError = (double) e.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case DOM_ITERATION_ERROR: + setIterationErrorTolerance(property); + break; + case RTE_MAX_ITERATIONS: + setMaxIterations(property); + break; + default: + return; + } + + firePropertyChanged(this, property); + + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(DOM_ITERATION_ERROR); + set.add(RTE_MAX_ITERATIONS); + return set; + } + + public NumericProperty getMaxIterations() { + return derive(RTE_MAX_ITERATIONS, maxIterations); + } + + public void setMaxIterations(NumericProperty iterations) { + if (iterations.getType() == RTE_MAX_ITERATIONS) { + this.maxIterations = (int) iterations.getValue(); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + " : " + this.getIterationErrorTolerance(); + } + + public double getIterationError() { + return iterationError; + } + +} 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 dbecad8d..4693eeb4 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/LinearAnisotropicPF.java @@ -1,36 +1,36 @@ package pulse.problem.schemes.rte.dom; import pulse.problem.statements.ParticipatingMedium; +import pulse.problem.statements.model.ThermoOpticalProperties; /** * The linear-anisotropic scattering phase function. * */ - public class LinearAnisotropicPF extends PhaseFunction { - private double g; - - public LinearAnisotropicPF(ParticipatingMedium medium, Discretisation intensities) { - super(medium, intensities); - } - - @Override - public void init(ParticipatingMedium medium) { - super.init(medium); - g = 3.0 * getAnisotropyFactor(); - } - - @Override - public double partialSum(final int i, final int j, final int n1, final int n2Exclusive) { - final var intensities = getDiscreteIntensities(); - return intensities.incidentRadiation(j, n1, n2Exclusive) + g * intensities.getOrdinates().getNode(i) * intensities.flux(j, n1, n2Exclusive); - } - - @Override - public double function(final int i, final int k) { - final var ordinates = getDiscreteIntensities().getOrdinates(); - return 1.0 + g * ordinates.getNode(i) * ordinates.getNode(k); - } - -} \ No newline at end of file + private static final long serialVersionUID = 7074989018933263351L; + private double g; + + public LinearAnisotropicPF(ThermoOpticalProperties top, Discretisation intensities) { + super(top, intensities); + } + + @Override + public void init(ThermoOpticalProperties top) { + super.init(top); + g = 3.0 * getAnisotropyFactor(); + } + + @Override + public double partialSum(final int i, final int j, final int n1, final int n2Exclusive) { + final var intensities = getDiscreteIntensities(); + return intensities.incidentRadiation(j, n1, n2Exclusive) + g * intensities.getOrdinates().getNode(i) * intensities.flux(j, n1, n2Exclusive); + } + + @Override + public double function(final int i, final int 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 21eda5c0..3dc388f9 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/ODEIntegrator.java @@ -2,137 +2,140 @@ import pulse.problem.schemes.rte.BlackbodySpectrum; import pulse.problem.schemes.rte.RTECalculationStatus; -import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.NonlinearProblem; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.util.PropertyHolder; import pulse.util.Reflexive; public abstract class ODEIntegrator extends PropertyHolder implements Reflexive { - private Discretisation discretisation; - private PhaseFunction pf; - private BlackbodySpectrum spectrum; + private Discretisation discretisation; + private PhaseFunction pf; + private BlackbodySpectrum spectrum; - public ODEIntegrator(Discretisation intensities) { - setDiscretisation(intensities); - } + public ODEIntegrator(Discretisation intensities) { + setDiscretisation(intensities); + } - public abstract RTECalculationStatus integrate(); + public abstract RTECalculationStatus integrate(); - protected void init(ParticipatingMedium problem) { - discretisation.setEmissivity((double)problem.getProperties().getEmissivity().getValue()); - var properties = (ThermoOpticalProperties)problem.getProperties(); - discretisation.setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); - setEmissionFunction(new BlackbodySpectrum(problem)); - } + protected void init(NonlinearProblem problem) { + extract((ThermoOpticalProperties) problem.getProperties()); + setEmissionFunction(new BlackbodySpectrum(problem)); + } - protected void treatZeroIndex() { - var ordinates = discretisation.getOrdinates(); + protected void extract(ThermoOpticalProperties properties) { + discretisation.setEmissivity((double) properties.getEmissivity().getValue()); + discretisation.setGrid(new StretchedGrid((double) properties.getOpticalThickness().getValue())); + } - if (ordinates.hasZeroNode()) { + protected void treatZeroIndex() { + var ordinates = discretisation.getOrdinates(); - var grid = discretisation.getGrid(); - var quantities = discretisation.getQuantities(); - double denominator = 0; - final double halfAlbedo = pf.getHalfAlbedo(); + if (ordinates.hasZeroNode()) { - // loop through the spatial indices - for (int j = 0; j < grid.getDensity() + 1; j++) { + var grid = discretisation.getGrid(); + var quantities = discretisation.getQuantities(); + double denominator = 0; + final double halfAlbedo = pf.getHalfAlbedo(); - // solve I_k = S_k for mu[k] = 0 - denominator = 1.0 - halfAlbedo * ordinates.getWeight(0) * pf.function(0, 0); - quantities.setIntensity(j, 0, - (emission(grid.getNode(j)) + halfAlbedo * pf.sumExcludingIndex(0, j, 0)) / denominator); + // loop through the spatial indices + for (int j = 0; j < grid.getDensity() + 1; j++) { - } - } + // solve I_k = S_k for mu[k] = 0 + denominator = 1.0 - halfAlbedo * ordinates.getWeight(0) * pf.function(0, 0); + quantities.setIntensity(j, 0, + (emission(grid.getNode(j)) + halfAlbedo * pf.sumExcludingIndex(0, j, 0)) / denominator); - } + } + } - public double derivative(int i, int j, double t, double I) { - return 1.0 / discretisation.getOrdinates().getNode(i) * (source(i, j, t, I) - I); - } + } - public double derivative(int i, double t, double[] out, double[] in, int l1, int l2) { - return 1.0 / discretisation.getOrdinates().getNode(i) * (source(i, out, in, t, l1, l2) - out[i - l1]); - } + public double derivative(int i, int j, double t, double I) { + return 1.0 / discretisation.getOrdinates().getNode(i) * (source(i, j, t, I) - I); + } - public double partial(int i, double t, double[] inward, int l1, int l2) { - return (emission(t) + pf.getHalfAlbedo() * pf.inwardPartialSum(i, inward, l1, l2)) - / discretisation.getOrdinates().getNode(i); - } + public double derivative(int i, double t, double[] out, double[] in, int l1, int l2) { + return 1.0 / discretisation.getOrdinates().getNode(i) * (source(i, out, in, t, l1, l2) - out[i - l1]); + } - public double partial(int i, int j, double t, int l1, int l2) { - return (emission(t) + pf.getHalfAlbedo() * pf.partialSum(i, j, l1, l2)) - / discretisation.getOrdinates().getNode(i); - } + public double partial(int i, double t, double[] inward, int l1, int l2) { + return (emission(t) + pf.getHalfAlbedo() * pf.inwardPartialSum(i, inward, l1, l2)) + / discretisation.getOrdinates().getNode(i); + } - public double source(int i, int j, double t, double I) { - return emission(t) + pf.getHalfAlbedo() - * (pf.sumExcludingIndex(i, j, i) + pf.function(i, i) * discretisation.getOrdinates().getWeight(i) * I); - } + public double partial(int i, int j, double t, int l1, int l2) { + return (emission(t) + pf.getHalfAlbedo() * pf.partialSum(i, j, l1, l2)) + / discretisation.getOrdinates().getNode(i); + } - public double source(final int i, final double[] iOut, final double[] iIn, final double t, final int l1, - final int l2) { + public double source(int i, int j, double t, double I) { + return emission(t) + pf.getHalfAlbedo() + * (pf.sumExcludingIndex(i, j, i) + pf.function(i, i) * discretisation.getOrdinates().getWeight(i) * I); + } - double sumOut = 0; - final var ordinates = discretisation.getOrdinates(); + public double source(final int i, final double[] iOut, final double[] iIn, final double t, final int l1, + final int l2) { - for (int l = l1; l < l2; l++) { - // sum over the OUTWARD intensities iOut - sumOut += iOut[l - l1] * ordinates.getWeight(l) * pf.function(i, l); - } + double sumOut = 0; + final var ordinates = discretisation.getOrdinates(); - double sumIn = 0; + for (int l = l1; l < l2; l++) { + // sum over the OUTWARD intensities iOut + sumOut += iOut[l - l1] * ordinates.getWeight(l) * pf.function(i, l); + } - for (int start = ordinates.getTotalNodes() - l2, l = start, end = ordinates.getTotalNodes() - - l1; l < end; l++) { - // sum over the INWARD - // intensities iIn - sumIn += iIn[l - start] * ordinates.getWeight(l) * pf.function(i, l); - } + double sumIn = 0; - return emission(t) + pf.getHalfAlbedo() * (sumIn + sumOut); // contains sum over the incoming rays + for (int start = ordinates.getTotalNodes() - l2, l = start, end = ordinates.getTotalNodes() + - l1; l < end; l++) { + // sum over the INWARD + // intensities iIn + sumIn += iIn[l - start] * ordinates.getWeight(l) * pf.function(i, l); + } - } + return emission(t) + pf.getHalfAlbedo() * (sumIn + sumOut); // contains sum over the incoming rays - public double emission(double t) { - return (1.0 - 2.0 * pf.getHalfAlbedo()) * spectrum.radianceAt(t); - } + } - public PhaseFunction getPhaseFunction() { - return pf; - } + public double emission(double t) { + return (1.0 - 2.0 * pf.getHalfAlbedo()) * spectrum.radianceAt(t); + } - protected void setPhaseFunction(PhaseFunction pf) { - this.pf = pf; - } + public final PhaseFunction getPhaseFunction() { + return pf; + } - @Override - public String getDescriptor() { - return "Numeric integrator"; - } + protected final void setPhaseFunction(PhaseFunction pf) { + this.pf = pf; + } - @Override - public String toString() { - return getClass().getSimpleName(); - } + @Override + public String getDescriptor() { + return "Numeric integrator"; + } - public Discretisation getDiscretisation() { - return discretisation; - } + @Override + public String toString() { + return getClass().getSimpleName(); + } - public void setDiscretisation(Discretisation discretisation) { - this.discretisation = discretisation; - discretisation.setParent(this); - } + public final Discretisation getDiscretisation() { + return discretisation; + } - public BlackbodySpectrum getEmissionFunction() { - return spectrum; - } + public final void setDiscretisation(Discretisation discretisation) { + this.discretisation = discretisation; + discretisation.setParent(this); + } - public void setEmissionFunction(BlackbodySpectrum emissionFunction) { - this.spectrum = emissionFunction; - } + public final BlackbodySpectrum getEmissionFunction() { + return spectrum; + } -} \ No newline at end of file + public final void setEmissionFunction(BlackbodySpectrum emissionFunction) { + this.spectrum = emissionFunction; + } + +} 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 60734ee6..fc09890d 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; @@ -7,107 +8,109 @@ import pulse.util.Descriptive; /** - * A fixed set of discrete cosine nodes and weights for the angular discretisation of a radiative - * transfer equation. + * A fixed set of discrete cosine nodes and weights for the angular + * discretisation of a radiative transfer equation. * */ +public class OrdinateSet implements Descriptive, Serializable { -public class OrdinateSet implements Descriptive { + private static final long serialVersionUID = 4850346144315192409L; + private double[] mu; + private double[] w; - private double[] mu; - private double[] w; + private int firstPositiveNode; + private int firstNegativeNode; + private int totalNodes; - private int firstPositiveNode; - private int firstNegativeNode; - private int totalNodes; + public final static String DEFAULT_SET = "G8M"; + private String name; - public final static String DEFAULT_SET = "G8M"; - private String name; + public OrdinateSet(String name, double[] mu, double[] w) { + if (mu.length != w.length) { + throw new IllegalArgumentException("Arrays sizes do not match: " + mu.length + " != " + w.length); + } - public OrdinateSet(String name, double[] mu, double[] w) { - if (mu.length != w.length) - throw new IllegalArgumentException("Arrays sizes do not match: " + mu.length + " != " + w.length); + setName(name); + this.mu = mu; + this.w = w; + totalNodes = mu.length; - setName(name); - this.mu = mu; - this.w = w; - totalNodes = mu.length; + checkWeights(); - checkWeights(); + firstPositiveNode = hasZeroNode() ? 1 : 0; //zero node should always be the first one + firstNegativeNode = totalNodes / 2 + (hasZeroNode() ? 1 : 0); - firstPositiveNode = hasZeroNode() ? 1 : 0; //zero node should always be the first one - firstNegativeNode = totalNodes / 2 + (hasZeroNode() ? 1 : 0); - - } + } - @Override - public String toString() { - return this.getName(); - } + @Override + public String toString() { + return this.getName(); + } - public String printOrdinateSet() { - var sb = new StringBuilder(); + public String printOrdinateSet() { + var sb = new StringBuilder(); - sb.append("Quadrature set: " + this.getName()); - sb.append(System.lineSeparator()); + sb.append("Quadrature set: " + this.getName()); + sb.append(System.lineSeparator()); - for (int i = 0; i < mu.length; i++) { - sb.append(String.format("%nmu[%1d] = %3.8f; w[%1d] = %3.8f", i, mu[i], i, w[i])); - } + for (int i = 0; i < mu.length; i++) { + sb.append(String.format("%nmu[%1d] = %3.8f; w[%1d] = %3.8f", i, mu[i], i, w[i])); + } - return sb.toString(); + return sb.toString(); - } + } - public boolean hasZeroNode() { - return Arrays.stream(mu).anyMatch(Double.valueOf(0.0)::equals); - } + public final boolean hasZeroNode() { + return Arrays.stream(mu).anyMatch(Double.valueOf(0.0)::equals); + } - public int getFirstPositiveNode() { - return firstPositiveNode; - } + public int getFirstPositiveNode() { + return firstPositiveNode; + } - public int getFirstNegativeNode() { - return firstNegativeNode; - } + public int getFirstNegativeNode() { + return firstNegativeNode; + } - public String getName() { - return name; - } + public String getName() { + return name; + } - public void setName(String name) { - this.name = name; - } + public final void setName(String name) { + this.name = name; + } - @Override - public String describe() { - return "Ordinate set"; - } + @Override + public String describe() { + return "Ordinate set"; + } - public int getNumberOfNodes() { - return totalNodes; - } + public int getNumberOfNodes() { + return totalNodes; + } - public int getTotalNodes() { - return totalNodes; - } + public int getTotalNodes() { + return totalNodes; + } - public double getNode(int i) { - return mu[i]; - } + public double getNode(int i) { + return mu[i]; + } - public double getWeight(int i) { - return w[i]; - } + public double getWeight(int i) { + return w[i]; + } - public int getHalfLength() { - return firstNegativeNode - firstPositiveNode; - } + public int getHalfLength() { + return firstNegativeNode - firstPositiveNode; + } - private void checkWeights() { - final double sum = Arrays.stream(w).sum(); - if (!approximatelyEquals(sum, 2.0)) - throw new IllegalStateException("Summed quadrature weights != 2.0"); - } - -} \ No newline at end of file + private void checkWeights() { + final double sum = Arrays.stream(w).sum(); + if (!approximatelyEquals(sum, 2.0)) { + throw new IllegalStateException("Summed quadrature weights != 2.0"); + } + } + +} 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 addb751b..21374fb6 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java @@ -1,73 +1,85 @@ package pulse.problem.schemes.rte.dom; -import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import java.io.Serializable; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.util.Reflexive; -public abstract class PhaseFunction implements Reflexive { - - private Discretisation intensities; - private double anisotropy; - private double halfAlbedo; - - public PhaseFunction(ParticipatingMedium medium, Discretisation intensities) { - this.intensities = intensities; - init(medium); - } - - public double fullSum(int i, int j) { - return partialSum(i, j, 0, intensities.getOrdinates().getTotalNodes()); - } - - public double sumExcludingIndex(int i, int j, int index) { - return partialSum(i, j, 0, index) + partialSum(i, j, index + 1, intensities.getOrdinates().getTotalNodes()); - } - - public double partialSum(int i, int j, int startInclusive, int endExclusive) { - double result = 0; - final var ordinates = intensities.getOrdinates(); - final var quantities = intensities.getQuantities(); - - for (int k = startInclusive; k < endExclusive; k++) { - result += ordinates.getWeight(k) * quantities.getIntensity(j, k) * function(i, k); - } - return result; - } - - public double inwardPartialSum(int i, double[] inward, int kStart, int kEndExclusive) { - double result = 0; - final var ordinates = intensities.getOrdinates(); - - for (int k = kStart; k < kEndExclusive; k++) { - result += ordinates.getWeight(k) * inward[k - kStart] * function(i, k); - } - - return result; - } - - public abstract double function(int i, int k); - - public double getAnisotropyFactor() { - return anisotropy; - } - - protected Discretisation getDiscreteIntensities() { - return intensities; - } - - public void init(ParticipatingMedium problem) { - var properties = (ThermoOpticalProperties)problem.getProperties(); - this.anisotropy = (double) properties.getScatteringAnisostropy().getValue(); - this.halfAlbedo = 0.5 * (double) properties.getScatteringAlbedo().getValue(); - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } - - public double getHalfAlbedo() { - return halfAlbedo; - } - -} \ No newline at end of file +public abstract class PhaseFunction implements Reflexive, Serializable { + + private final Discretisation intensities; + private double anisotropy; + private double halfAlbedo; + + 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. + * + * @param i + * @param k + * @return + */ + public final double cosineTheta(int i, int k) { + final var ordinates = getDiscreteIntensities().getOrdinates(); + return ordinates.getNode(k) * ordinates.getNode(i); + } + + public double fullSum(int i, int j) { + return partialSum(i, j, 0, intensities.getOrdinates().getTotalNodes()); + } + + public double sumExcludingIndex(int i, int j, int index) { + return partialSum(i, j, 0, index) + partialSum(i, j, index + 1, intensities.getOrdinates().getTotalNodes()); + } + + public double partialSum(int i, int j, int startInclusive, int endExclusive) { + double result = 0; + final var ordinates = intensities.getOrdinates(); + final var quantities = intensities.getQuantities(); + + for (int k = startInclusive; k < endExclusive; k++) { + result += ordinates.getWeight(k) * quantities.getIntensity(j, k) * function(i, k); + } + return result; + } + + public double inwardPartialSum(int i, double[] inward, int kStart, int kEndExclusive) { + double result = 0; + final var ordinates = intensities.getOrdinates(); + + for (int k = kStart; k < kEndExclusive; k++) { + result += ordinates.getWeight(k) * inward[k - kStart] * function(i, k); + } + + return result; + } + + public abstract double function(int i, int k); + + public double getAnisotropyFactor() { + return anisotropy; + } + + protected Discretisation getDiscreteIntensities() { + return intensities; + } + + public void init(ThermoOpticalProperties properties) { + this.anisotropy = (double) properties.getScatteringAnisostropy().getValue(); + this.halfAlbedo = 0.5 * (double) properties.getScatteringAlbedo().getValue(); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + public double getHalfAlbedo() { + return halfAlbedo; + } + +} 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 7a26d5f5..012eb5d3 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/StretchedGrid.java @@ -5,147 +5,150 @@ 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 pulse.properties.Property; import pulse.util.PropertyHolder; public class StretchedGrid extends PropertyHolder { - private double[] nodes; - - private double stretchingFactor; - private double dimension; - - /** - * Constructs a uniform grid where the dimension is set to the argument. The stretching factor - * and grid density are set to a default value. - * @param dimension the dimension of the grid - */ - - public StretchedGrid(double dimension) { - this(def(DOM_GRID_DENSITY), dimension, def(GRID_STRETCHING_FACTOR), true); - } - - /** - * Constructs a non-uniform grid where the dimension and the grid density are specified by the arguments. The stretching factor - * and grid density are set to a default value. - * @param gridDensity the grid density, which is a property of the {@code DOM_GRID_DENSITY} type - * @param dimension the dimension of the grid - */ - - public StretchedGrid(NumericProperty gridDensity, double dimension) { - this(gridDensity, dimension, def(GRID_STRETCHING_FACTOR), false); - } - - protected StretchedGrid(NumericProperty gridDensity, double dimension, NumericProperty stretchingFactor, boolean uniform) { - this.stretchingFactor = (double) stretchingFactor.getValue(); - this.dimension = dimension; - int n = (int) gridDensity.getValue(); - if (uniform) - generateUniformBase(n, true); - else - generate(n); - } - - public void generate(int n) { - generateUniformBase(n, false); - - // apply stretching function - - for (int i = 0; i < nodes.length; i++) { - nodes[i] = 0.5 * dimension * tanh(nodes[i], stretchingFactor); - } - - } - - public void generateUniform(boolean scaled) { - int n1 = (int) def(DOM_GRID_DENSITY).getValue(); - generateUniformBase(n1, scaled); - } - - public void generateUniformBase(int n, boolean scaled) { - nodes = new double[n + 1]; - double h = (scaled ? dimension : 1.0) / n; - - for (int i = 0; i < nodes.length; i++) { - nodes[i] = i * h; - } - - } - - public int getDensity() { - return nodes.length - 1; - } - - public NumericProperty getStretchingFactor() { - return derive(GRID_STRETCHING_FACTOR, stretchingFactor); - } - - public void setStretchingFactor(NumericProperty p) { - if (p.getType() != GRID_STRETCHING_FACTOR) - throw new IllegalArgumentException("Illegal type: " + p.getType()); - this.stretchingFactor = (double) p.getValue(); - } - - public double getDimension() { - return dimension; - } - - public double getNode(int i) { - return nodes[i]; - } - - public double[] getNodes() { - return nodes; - } - - public double step(int i, double sign) { - return nodes[i + (int) ((1. + sign) * 0.5)] - nodes[i - (int) ((1. - sign) * 0.5)]; - } - - public double stepLeft(int i) { - return nodes[i] - nodes[i - 1]; - } - - public double stepRight(int i) { - return nodes[i + 1] - nodes[i]; - } - - public double tanh(final double x, final double stretchingFactor) { - return 1.0 - Math.tanh(stretchingFactor * (1.0 - 2.0 * x)) / Math.tanh(stretchingFactor); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case GRID_STRETCHING_FACTOR: - setStretchingFactor(property); - break; - default: - throw new IllegalArgumentException("Unknown type: " + type); - } - } - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.add(def(GRID_STRETCHING_FACTOR)); - list.add(def(DOM_GRID_DENSITY)); - return list; - } - - @Override - public String toString() { - return "{ " + derive(DOM_GRID_DENSITY, getDensity()) + " ; " + getStretchingFactor() + " }"; - } - - @Override - public String getDescriptor() { - return "Adaptive grid"; - } - -} \ No newline at end of file + private static final long serialVersionUID = -7987714138817824037L; + + private double[] nodes; + + private double stretchingFactor; + private double dimension; + + /** + * Constructs a uniform grid where the dimension is set to the argument. The + * stretching factor and grid density are set to a default value. + * + * @param dimension the dimension of the grid + */ + public StretchedGrid(double dimension) { + this(def(DOM_GRID_DENSITY), dimension, def(GRID_STRETCHING_FACTOR), true); + } + + /** + * Constructs a non-uniform grid where the dimension and the grid density + * are specified by the arguments. The stretching factor and grid density + * are set to a default value. + * + * @param gridDensity the grid density, which is a property of the + * {@code DOM_GRID_DENSITY} type + * @param dimension the dimension of the grid + */ + public StretchedGrid(NumericProperty gridDensity, double dimension) { + this(gridDensity, dimension, def(GRID_STRETCHING_FACTOR), false); + } + + protected StretchedGrid(NumericProperty gridDensity, double dimension, NumericProperty stretchingFactor, boolean uniform) { + this.stretchingFactor = (double) stretchingFactor.getValue(); + this.dimension = dimension; + int n = (int) gridDensity.getValue(); + if (uniform) { + generateUniformBase(n, true); + } else { + generate(n); + } + } + + public void generate(int n) { + generateUniformBase(n, false); + + // apply stretching function + for (int i = 0; i < nodes.length; i++) { + nodes[i] = 0.5 * dimension * tanh(nodes[i], stretchingFactor); + } + + } + + public void generateUniform(boolean scaled) { + int n1 = (int) def(DOM_GRID_DENSITY).getValue(); + generateUniformBase(n1, scaled); + } + + public void generateUniformBase(int n, boolean scaled) { + nodes = new double[n + 1]; + double h = (scaled ? dimension : 1.0) / n; + + for (int i = 0; i < nodes.length; i++) { + nodes[i] = i * h; + } + + } + + public int getDensity() { + return nodes.length - 1; + } + + public NumericProperty getStretchingFactor() { + return derive(GRID_STRETCHING_FACTOR, stretchingFactor); + } + + public void setStretchingFactor(NumericProperty p) { + if (p.getType() != GRID_STRETCHING_FACTOR) { + throw new IllegalArgumentException("Illegal type: " + p.getType()); + } + this.stretchingFactor = (double) p.getValue(); + } + + public double getDimension() { + return dimension; + } + + public double getNode(int i) { + return nodes[i]; + } + + public double[] getNodes() { + return nodes; + } + + public double step(int i, double sign) { + return nodes[i + (int) ((1. + sign) * 0.5)] - nodes[i - (int) ((1. - sign) * 0.5)]; + } + + public double stepLeft(int i) { + return nodes[i] - nodes[i - 1]; + } + + public double stepRight(int i) { + return nodes[i + 1] - nodes[i]; + } + + public double tanh(final double x, final double stretchingFactor) { + return 1.0 - Math.tanh(stretchingFactor * (1.0 - 2.0 * x)) / Math.tanh(stretchingFactor); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + switch (type) { + case GRID_STRETCHING_FACTOR: + setStretchingFactor(property); + break; + default: + throw new IllegalArgumentException("Unknown type: " + type); + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(GRID_STRETCHING_FACTOR); + set.add(DOM_GRID_DENSITY); + return set; + } + + @Override + public String toString() { + return "{ " + derive(DOM_GRID_DENSITY, getDensity()) + " ; " + getStretchingFactor() + " }"; + } + + @Override + public String getDescriptor() { + return "Adaptive grid"; + } + +} 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 691ea493..1c44c065 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/SuccessiveOverrelaxation.java @@ -5,118 +5,120 @@ 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 pulse.properties.Property; public class SuccessiveOverrelaxation extends IterativeSolver { - private double W; + private static final long serialVersionUID = 1135563981945852881L; + private double W; - public SuccessiveOverrelaxation() { - super(); - this.W = (double) def(RELAXATION_PARAMETER).getValue(); - } + public SuccessiveOverrelaxation() { + super(); + this.W = (double) def(RELAXATION_PARAMETER).getValue(); + } - private void successiveOverrelaxation(AdaptiveIntegrator integrator) { + private void successiveOverrelaxation(AdaptiveIntegrator integrator) { - final var intensities = integrator.getDiscretisation(); - final var quantities = intensities.getQuantities(); - final int density = intensities.getGrid().getDensity(); - final int total = intensities.getOrdinates().getTotalNodes(); + final var intensities = integrator.getDiscretisation(); + final var quantities = intensities.getQuantities(); + final int density = intensities.getGrid().getDensity(); + final int total = intensities.getOrdinates().getTotalNodes(); - final double ONE_MINUS_W = 1.0 - W; + final double ONE_MINUS_W = 1.0 - W; - for (int i = 0; i < density + 1; i++) { - for (int j = 0; j < total; j++) { - quantities.setIntensity(i, j, - ONE_MINUS_W * quantities.getStoredIntensity(i, j) + W * quantities.getIntensity(i, j)); - quantities.setStoredDerivative(i, j, - ONE_MINUS_W * quantities.getStoredDerivative(i, j) + W * quantities.getDerivative(i, j)); - } - } + for (int i = 0; i < density + 1; i++) { + for (int j = 0; j < total; j++) { + quantities.setIntensity(i, j, + ONE_MINUS_W * quantities.getStoredIntensity(i, j) + W * quantities.getIntensity(i, j)); + quantities.setStoredDerivative(i, j, + ONE_MINUS_W * quantities.getStoredDerivative(i, j) + W * quantities.getDerivative(i, j)); + } + } - } + } - @Override - public RTECalculationStatus doIterations(AdaptiveIntegrator integrator) { + @Override + public RTECalculationStatus doIterations(AdaptiveIntegrator integrator) { - var discrete = integrator.getDiscretisation(); - var quantities = discrete.getQuantities(); - double relativeError = 100; + var discrete = integrator.getDiscretisation(); + var quantities = discrete.getQuantities(); + double relativeError = 100; - double qld = 0; - double qrd = 0; - double qsum; + double qld = 0; + double qrd = 0; + double qsum; - int iterations = 0; - final var ef = integrator.getEmissionFunction(); - RTECalculationStatus status = RTECalculationStatus.NORMAL; + int iterations = 0; + final var ef = integrator.getEmissionFunction(); + RTECalculationStatus status = RTECalculationStatus.NORMAL; - for (double ql = 1e8, qr = ql; relativeError > getIterationError(); status = sanityCheck(status, - ++iterations)) { - quantities.store(); - ql = qld; - qr = qrd; - - status = integrator.integrate(); - - // if the integrator attempted rescaling, last iteration is not valid anymore - if (integrator.wasRescaled()) { - relativeError = Double.POSITIVE_INFINITY; - } else { // calculate the (k+1) iteration as: I_k+1 = I_k * (1 - W) + I* + for (double ql = 1e8, qr = ql; relativeError > getIterationError(); status = sanityCheck(status, + ++iterations)) { + quantities.store(); + ql = qld; + qr = qrd; - // get the difference in boundary heat fluxes - qld = discrete.fluxLeft(ef); - qrd = discrete.fluxRight(ef); - qsum = abs(qld - ql) + abs(qrd - qr); + status = integrator.integrate(); - successiveOverrelaxation(integrator); - relativeError = qsum / ( abs(qld) + abs(qrd) ); - - } + // if the integrator attempted rescaling, last iteration is not valid anymore + if (integrator.wasRescaled()) { + relativeError = Double.POSITIVE_INFINITY; + } else { // calculate the (k+1) iteration as: I_k+1 = I_k * (1 - W) + I* - } + // get the difference in boundary heat fluxes + qld = discrete.fluxLeft(ef); + qrd = discrete.fluxRight(ef); + qsum = abs(qld - ql) + abs(qrd - qr); - return status; + successiveOverrelaxation(integrator); + relativeError = qsum / (abs(qld) + abs(qrd)); - } + } - public NumericProperty getRelaxationParameter() { - return derive(RELAXATION_PARAMETER, W); - } + } - public void setRelaxationParameter(NumericProperty p) { - if (p.getType() != RELAXATION_PARAMETER) - throw new IllegalArgumentException("Unknown type: " + p.getType()); - W = (double) p.getValue(); - } + return status; - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { + } - super.set(type, property); + public NumericProperty getRelaxationParameter() { + return derive(RELAXATION_PARAMETER, W); + } - if (type == RELAXATION_PARAMETER) - setRelaxationParameter(property); - else - throw new IllegalArgumentException("Unknown type: " + type); + public void setRelaxationParameter(NumericProperty p) { + if (p.getType() != RELAXATION_PARAMETER) { + throw new IllegalArgumentException("Unknown type: " + p.getType()); + } + W = (double) p.getValue(); + } - } + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(RELAXATION_PARAMETER)); - return list; - } + super.set(type, property); - @Override - public String toString() { - return super.toString() + " ; " + getRelaxationParameter(); - } - -} \ No newline at end of file + if (type == RELAXATION_PARAMETER) { + setRelaxationParameter(property); + } else { + throw new IllegalArgumentException("Unknown type: " + type); + } + + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(RELAXATION_PARAMETER); + return set; + } + + @Override + public String toString() { + return super.toString() + " ; " + getRelaxationParameter(); + } + +} 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 b84c5bfd..5ce22402 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/TRBDF2.java @@ -6,24 +6,24 @@ import pulse.problem.schemes.rte.RTECalculationStatus; /** - * TRBDF2 (Trapezoidal Backward Differencing Second Order) Scheme for the solution of one-dimensional radiative transfer problems. - * + * TRBDF2 (Trapezoidal Backward Differencing Second Order) Scheme for the + * solution of one-dimensional radiative transfer problems. + * * @author Artem Lunev, Vadim Zborovskii * */ - 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 - */ + */ + private final static double gamma = 2.0 - Math.sqrt(2.0); + private final static double w = Math.sqrt(2.0) / 4.0; + private final static double d = gamma / 2.0; - private final static double gamma = 2.0 - Math.sqrt(2.0); - private final static double w = Math.sqrt(2.0) / 4.0; - private final static double d = gamma / 2.0; - - /* + /* * Uncomment this for coefficients for the error estimator from: Christopher A. * Kennedy, Mark H. Carpenter. Diagonally Implicit Runge-Kutta Methods for * Ordinary Differential Equations. A Review. NASA/TM–2016–219173, p. 72 @@ -33,209 +33,198 @@ public class TRBDF2 extends AdaptiveIntegrator { * g) / (2.0 * g - 1.0); bHat[0] = 1.0 - bHat[1] - bHat[2]; * * - */ - - private static double[] bHat = new double[3]; + */ + private static double[] bHat = new double[3]; - /* + /* * These are the original error estimator coefficients. - */ + */ + static { + bHat[0] = (1.0 - w) / 3.0; + bHat[1] = (3.0 * w + 1.0) / 3.0; + bHat[2] = d / 3.0; + } - static { - bHat[0] = (1.0 - w) / 3.0; - bHat[1] = (3.0 * w + 1.0) / 3.0; - bHat[2] = d / 3.0; - } + private final static double[] bbHat = new double[]{w - bHat[0], w - bHat[1], d - bHat[2]}; - private final static double[] bbHat = new double[] { w - bHat[0], w - bHat[1], d - bHat[2] }; + private double k[][]; - private double k[][]; + private double[] inward; + private double[] bVector; // right-hand side of linear set A * x = B + private double[] est; // error estimator + private double[][] aMatrix; // matrix of linear set A * x = B - private double[] inward; - private double[] bVector; // right-hand side of linear set A * x = B - private double[] est; // error estimator - private double[][] aMatrix; // matrix of linear set A * x = B + private SquareMatrix invA; // inverse matrix + private Vector i2; // second stage (trapezoidal) + private Vector i3; // third stage (backward-difference second order) - private SquareMatrix invA; // inverse matrix - private Vector i2; // second stage (trapezoidal) - private Vector i3; // third stage (backward-difference second order) - - /* + /* * Constants for third-stage calculation - */ - - private double w_d = w / d; - private double _1w_d = (1.0 - w_d); - - public TRBDF2(Discretisation intensities) { - super(intensities); - } - - @Override - public RTECalculationStatus integrate() { - final int nH = getDiscretisation().getOrdinates().getHalfLength(); - - bVector = new double[nH]; - est = new double[nH]; - aMatrix = new double[nH][nH]; - inward = new double[nH]; - - k = new double[3][nH]; - return super.integrate(); - } - - /** - * Generates a non-uniform (stretched at boundaries) grid using the - * argument as the density. - * @param nNew new grid density - */ - - @Override - public void generateGrid(int nNew) { - getDiscretisation().getGrid().generate(nNew); - } - - /** - * Performs a TRBDF2 step. - */ - - @Override - public Vector[] step(final int j, final double sign) { - var intensities = getDiscretisation(); - var quantities = intensities.getQuantities(); - var hermite = getHermiteInterpolator(); - - final double h = sign * intensities.getGrid().step(j, sign); - hermite.bMinusA = h; // <---- for Hermite interpolation - - final int total = getDiscretisation().getOrdinates().getTotalNodes(); - final int increment = (int) (1 * sign); - final double t = intensities.getGrid().getNode(j); - hermite.a = t; // <---- for Hermite interpolation - - /* + */ + private double w_d = w / d; + private double _1w_d = (1.0 - w_d); + + public TRBDF2(Discretisation intensities) { + super(intensities); + } + + @Override + public RTECalculationStatus integrate() { + final int nH = getDiscretisation().getOrdinates().getHalfLength(); + + bVector = new double[nH]; + est = new double[nH]; + aMatrix = new double[nH][nH]; + inward = new double[nH]; + + k = new double[3][nH]; + return super.integrate(); + } + + /** + * Generates a non-uniform (stretched at boundaries) grid using the argument + * as the density. + * + * @param nNew new grid density + */ + @Override + public void generateGrid(int nNew) { + getDiscretisation().getGrid().generate(nNew); + } + + /** + * Performs a TRBDF2 step. + */ + @Override + public Vector[] step(final int j, final double sign) { + var intensities = getDiscretisation(); + var quantities = intensities.getQuantities(); + var hermite = getHermiteInterpolator(); + + final double h = sign * intensities.getGrid().step(j, sign); + hermite.bMinusA = h; // <---- for Hermite interpolation + + final int total = getDiscretisation().getOrdinates().getTotalNodes(); + final int increment = (int) (1 * sign); + final double t = intensities.getGrid().getNode(j); + hermite.a = t; // <---- for Hermite interpolation + + /* * Indices of OUTWARD intensities (n1 <= i < n2) - */ - - final int nPositiveStart = intensities.getOrdinates().getFirstPositiveNode(); - final int nNegativeStart = intensities.getOrdinates().getFirstNegativeNode(); - final int halfLength = nNegativeStart - nPositiveStart; - final int n1 = sign > 0 ? nPositiveStart : nNegativeStart; // either first positive index or first negative - final int n2 = sign > 0 ? nNegativeStart : total; // either first negative index or n - - /* + */ + final int nPositiveStart = intensities.getOrdinates().getFirstPositiveNode(); + final int nNegativeStart = intensities.getOrdinates().getFirstNegativeNode(); + final int halfLength = nNegativeStart - nPositiveStart; + final int n1 = sign > 0 ? nPositiveStart : nNegativeStart; // either first positive index or first negative + final int n2 = sign > 0 ? nNegativeStart : total; // either first negative index or n + + /* * Indices of INWARD intensities (n3 <= i < n4) - */ + */ + final int n3 = total - n2; // either first negative index or 0 (for INWARD intensities) + final int n4 = total - n1; // either n or first negative index (for INWARD intensities) + final int n5 = nNegativeStart - n3; // either 0 or first negative index - final int n3 = total - n2; // either first negative index or 0 (for INWARD intensities) - final int n4 = total - n1; // either n or first negative index (for INWARD intensities) - final int n5 = nNegativeStart - n3; // either 0 or first negative index - - /* + /* * Try to use FSAL - */ - - if (!isFirstRun()) { // if this is not the first step + */ + if (!isFirstRun()) { // if this is not the first step - for (int l = n1; l < n2; l++) { - k[0][l - n1] = quantities.getQLast(l - n1); - } + for (int l = n1; l < n2; l++) { + k[0][l - n1] = quantities.getQLast(l - n1); + } - } else { + } else { - for (int l = n1; l < n2; l++) { - k[0][l - n1] = super.derivative(l, j, t, quantities.getIntensity(j, l)); // first-stage right-hand - // side: f( t, In) - // ) - } // ) + for (int l = n1; l < n2; l++) { + k[0][l - n1] = super.derivative(l, j, t, quantities.getIntensity(j, l)); // first-stage right-hand + // side: f( t, In) + // ) + } // ) - setFirstRun(false); + setFirstRun(false); - } + } - /* + /* * ============================= 1st and 2nd stages begin here * ============================= - */ - - final double hd = h * d; - final double tPlusGamma = t + gamma * h; + */ + final double hd = h * d; + final double tPlusGamma = t + gamma * h; - /* + /* * Interpolate INWARD intensities at t + gamma*h (second stage) - */ - - for (int i = 0; i < inward.length; i++) { - hermite.y0 = quantities.getIntensity(j, i + n3); - hermite.y1 = quantities.getIntensity(j + increment, i + n3); - hermite.d0 = quantities.getDerivative(j, i + n3); - hermite.d1 = quantities.getDerivative(j + increment, i + n3); - inward[i] = hermite.interpolate(tPlusGamma); - } - - /* + */ + for (int i = 0; i < inward.length; i++) { + hermite.y0 = quantities.getIntensity(j, i + n3); + hermite.y1 = quantities.getIntensity(j + increment, i + n3); + hermite.d0 = quantities.getDerivative(j, i + n3); + hermite.d1 = quantities.getDerivative(j + increment, i + n3); + inward[i] = hermite.interpolate(tPlusGamma); + } + + /* * Trapezoidal step - */ + */ + final double prefactorNumerator = -hd * getPhaseFunction().getHalfAlbedo(); - final double prefactorNumerator = -hd * getPhaseFunction().getHalfAlbedo(); - - double matrixPrefactor; - final var ordinates = intensities.getOrdinates(); + double matrixPrefactor; + final var ordinates = intensities.getOrdinates(); - for (int i = 0; i < halfLength; i++) { + for (int i = 0; i < halfLength; i++) { - quantities.setDerivative(j, i + n1, k[0][i]); // store derivatives for Hermite interpolation + quantities.setDerivative(j, i + n1, k[0][i]); // store derivatives for Hermite interpolation - bVector[i] = quantities.getIntensity(j, i + n1) - + hd * (k[0][i] + partial(i + n1, tPlusGamma, inward, n3, n4)); // only - // INWARD - // intensities + bVector[i] = quantities.getIntensity(j, i + n1) + + hd * (k[0][i] + partial(i + n1, tPlusGamma, inward, n3, n4)); // only + // INWARD + // intensities - matrixPrefactor = prefactorNumerator / ordinates.getNode(i + n1); + matrixPrefactor = prefactorNumerator / ordinates.getNode(i + n1); - // all elements - for (int k = 0; k < aMatrix[0].length; k++) { - aMatrix[i][k] = matrixPrefactor * ordinates.getWeight(k + n5) - * getPhaseFunction().function(i + n1, k + n5); // only - // OUTWARD - // (and zero) - // intensities - } + // all elements + for (int k = 0; k < aMatrix[0].length; k++) { + aMatrix[i][k] = matrixPrefactor * ordinates.getWeight(k + n5) + * getPhaseFunction().function(i + n1, k + n5); // only + // OUTWARD + // (and zero) + // intensities + } - // additionally for the diagonal elements - aMatrix[i][i] += 1.0 + hd / ordinates.getNode(i + n1); + // additionally for the diagonal elements + aMatrix[i][i] += 1.0 + hd / ordinates.getNode(i + n1); - } + } - invA = (Matrices.createMatrix(aMatrix)).inverse(); // this matrix is re-used for subsequent stages - i2 = invA.multiply(new Vector(bVector)); // intensity vector at 2nd stage + invA = (Matrices.createSquareMatrix(aMatrix)).inverse(); // this matrix is re-used for subsequent stages + i2 = invA.multiply(new Vector(bVector)); // intensity vector at 2nd stage - /* + /* * ================== Third stage (BDF2) ================== - */ - - final double th = t + h; + */ + final double th = t + h; - for (int i = 0; i < aMatrix.length; i++) { + for (int i = 0; i < aMatrix.length; i++) { - bVector[i] = quantities.getIntensity(j, i + n1) * _1w_d + w_d * i2.get(i) - + hd * partial(i + n1, j + increment, th, n3, n4); // only INWARD intensities at node j + 1 (i.e. no - // interpolation) - k[1][i] = (i2.get(i) - quantities.getIntensity(j, i + n1)) / hd - k[0][i]; + bVector[i] = quantities.getIntensity(j, i + n1) * _1w_d + w_d * i2.get(i) + + hd * partial(i + n1, j + increment, th, n3, n4); // only INWARD intensities at node j + 1 (i.e. no + // interpolation) + k[1][i] = (i2.get(i) - quantities.getIntensity(j, i + n1)) / hd - k[0][i]; - } + } - i3 = invA.multiply(new Vector(bVector)); + i3 = invA.multiply(new Vector(bVector)); - for (int i = 0; i < aMatrix.length; i++) { - k[2][i] = (i3.get(i) - quantities.getIntensity(j, i + n1) - - w_d * (i2.get(i) - quantities.getIntensity(j, i + n1))) / hd; - quantities.setQLast(i, k[2][i]); - est[i] = (bbHat[0] * k[0][i] + bbHat[1] * k[1][i] + bbHat[2] * k[2][i]) * h; - } + for (int i = 0; i < aMatrix.length; i++) { + k[2][i] = (i3.get(i) - quantities.getIntensity(j, i + n1) + - w_d * (i2.get(i) - quantities.getIntensity(j, i + n1))) / hd; + quantities.setQLast(i, k[2][i]); + est[i] = (bbHat[0] * k[0][i] + bbHat[1] * k[1][i] + bbHat[2] * k[2][i]) * h; + } - return new Vector[] { i3, invA.multiply(new Vector(est)) }; + return new Vector[]{i3, invA.multiply(new Vector(est))}; - } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/dom/package-info.java b/src/main/java/pulse/problem/schemes/rte/dom/package-info.java index 42f5716b..4c5177ea 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/package-info.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/package-info.java @@ -4,5 +4,4 @@ * includes ODE solvers, ordinate set handlers, non-uniform grid, iterative * solvers, and scattering phase functions. */ - -package pulse.problem.schemes.rte.dom; \ No newline at end of file +package pulse.problem.schemes.rte.dom; 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 2fb41182..535a603e 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,8 @@ 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; import org.apache.commons.math3.analysis.solvers.LaguerreSolver; @@ -24,252 +23,253 @@ import pulse.math.linear.Vector; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; /** * This quadrature methods of evaluating the composition product of the * exponential integral and blackbody spectral power spectrum has been given by * Chandrasekhar and is based on constructing a moment matrix. - * + * * @see Chandrasekhar, - * S. Radiative transfer - * + * S. Radiative transfer + * */ - public class ChandrasekharsQuadrature extends CompositionProduct { - private int m; - private double expLower; - private double expUpper; - private LaguerreSolver solver; - private double[] moments; - - /** - * Constructs a {@code ChandrasekharsQuadrature} object with a default number of - * nodes, a {@code LaguerreSolver} with default precision and integration bounds - * set to [0,1]. - */ - - public ChandrasekharsQuadrature() { - super(new Segment(0, 1)); - m = (int) def(QUADRATURE_POINTS).getValue(); - solver = new LaguerreSolver(); - } - - @Override - public double integrate() { - var bounds = this.transformedBounds(); - expLower = -exp(-bounds[0]); - expUpper = -exp(-bounds[1]); - - double[] roots = roots(); - - Vector weights = weights(roots); - - return f(roots).dot(weights) / getBeta(); - } - - public NumericProperty getQuadraturePoints() { - return derive(QUADRATURE_POINTS, m); - } - - public void setQuadraturePoints(NumericProperty m) { - requireType(m, QUADRATURE_POINTS); - this.m = (int) m.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == QUADRATURE_POINTS) { - setQuadraturePoints(property); - firePropertyChanged(this, property); - } - } - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(def(QUADRATURE_POINTS))); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " : " + getQuadraturePoints(); - } - - /* + private static final long serialVersionUID = 3282258803373408111L; + private int m; + private double expLower; + private double expUpper; + private transient LaguerreSolver solver; + private double[] moments; + + /** + * Constructs a {@code ChandrasekharsQuadrature} object with a default + * number of nodes, a {@code LaguerreSolver} with default precision and + * integration bounds set to [0,1]. + */ + public ChandrasekharsQuadrature() { + super(new Segment(0, 1)); + m = (int) def(QUADRATURE_POINTS).getValue(); + solver = new LaguerreSolver(); + } + + @Override + public double integrate() { + var bounds = this.transformedBounds(); + expLower = -exp(-bounds[0]); + expUpper = -exp(-bounds[1]); + + double[] roots = roots(); + + Vector weights = weights(roots); + + return f(roots).dot(weights) / getBeta(); + } + + public NumericProperty getQuadraturePoints() { + return derive(QUADRATURE_POINTS, m); + } + + public void setQuadraturePoints(NumericProperty m) { + requireType(m, QUADRATURE_POINTS); + this.m = (int) m.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == QUADRATURE_POINTS) { + setQuadraturePoints(property); + firePropertyChanged(this, property); + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(QUADRATURE_POINTS); + return set; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " : " + getQuadraturePoints(); + } + + /* * Private methods - */ - - private Vector f(final double[] roots) { - final var ef = getEmissionFunction(); - return new Vector(Arrays.stream(roots).map(root -> ef.powerAt(root)).toArray()); - } - - private double[] transformedBounds() { - final double min = getBounds().getMinimum(); - final double max = getBounds().getMaximum(); - return new double[] { getAlpha() + getBeta() * min, getAlpha() + getBeta() * max }; - } - - private SquareMatrix xMatrix(final double[] roots) { - double[][] x = new double[m][m]; - - for (int l = 0; l < m; l++) { - for (int j = 0; j < m; j++) { - x[l][j] = fastPowLoop(roots[j] * getBeta() + getAlpha(), l); - } - } - - return Matrices.createMatrix(x); - } - - /** - * 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. - */ - - private static double auxilliaryIntegral(final double x, final int lPlusN, final double exp) { - - double f = 0; - long m = 0; - - final int k = lPlusN - 1; - - for (int i = 0; i < lPlusN; i++) { - m = 1; - for (int j = 0; j < i; j++) { - m *= (k - j); - } - f += m * fastPowLoop(x, k - i); - } - - return f * exp; - - } - - private static double[] solveCubic(final double a, final double b, final double c) { - final double p = b / 3.0 - a * a / 9.0; - final double q = a * a * a / 27.0 - a * b / 6.0 + c / 2.0; - - final double ang = acos(-q / sqrt(-p * p * p)); - final double r = 2.0 * sqrt(-p); - var result = new double[3]; - double theta; - for (int k = -1; k < 2; k++) { - theta = (ang - 2.0 * PI * k) / 3.0; - result[k + 1] = r * cos(theta); - } - - for (int i = 0; i < result.length; i++) { - result[i] -= a / 3.0; - } - - return result; - } - - private double moment(int l) { - var bounds = this.transformedBounds(); - return momentIntegral(bounds[1], l, expUpper) - momentIntegral(bounds[0], l, expLower); - } - - private double momentIntegral(final double x, final int l, final double exp) { - - double e = 0; - int m = 0; - - final int n = getOrder(); - final int lPlusOne = l + 1; - - for (int i = 0; i < n; i++) { - m = lPlusOne; - for (int j = 1; j < i + 1; j++) { - m *= (lPlusOne + j); - } - e += ExponentialIntegrals.get(n - i).valueAt(x) * fastPowLoop(x, lPlusOne + i) / ((double) m); - } - - return e + auxilliaryIntegral(x, l + n, exp) / ((double) m); - - } - - private Vector coefficients() { - return momentMatrix().inverse().multiply(momentVector(m, 2 * m)); - } - - private SquareMatrix momentMatrix() { - - double[][] data = new double[m][m]; - moments = new double[2 * m]; - - // diagonal elements - IntStream.range(0, m).forEach(i -> data[i][i] = moment(i * 2)); - - // find (symmetric) non-diagonal elements - for (int i = 1, j = 0; i < m; i++) { - for (j = 0; j < i; j++) { - data[i][j] = moment(i + j); - data[j][i] = data[i][j]; - } - } - - for (int i = 0; i < m; i++) { - moments[i] = data[0][i]; - moments[i + m - 1] = data[i][m - 1]; - } - - moments[2 * m - 1] = moment(2 * m - 1); - - return Matrices.createMatrix(data); - - } - - private Vector momentVector(final int lowerInclusive, final int upperExclusive) { - var array = IntStream.range(lowerInclusive, upperExclusive).mapToDouble(i -> -moments[i]).toArray(); - return new Vector(array); - } + */ + private Vector f(final double[] roots) { + final var ef = getEmissionFunction(); + return new Vector(Arrays.stream(roots).map(root -> ef.powerAt(root)).toArray()); + } - private Vector weights(final double[] roots) { - final var x = xMatrix(roots); - final var a = momentVector(0, m).inverted(); - - return x.inverse().multiply(a); - } - - private double[] roots() { - double[] roots; - double[] c = new double[m + 1]; - - // coefficients of the monic polynomial x_j^m + sum_{l=0}^{m-1}{c_lx_j^l} - System.arraycopy(coefficients().getData(), 0, c, 0, m); - c[m] = 1.0; + private double[] transformedBounds() { + final double min = getBounds().getMinimum(); + final double max = getBounds().getMaximum(); + return new double[]{getAlpha() + getBeta() * min, getAlpha() + getBeta() * max}; + } + + private SquareMatrix xMatrix(final double[] roots) { + double[][] x = new double[m][m]; + + for (int l = 0; l < m; l++) { + for (int j = 0; j < m; j++) { + x[l][j] = fastPowLoop(roots[j] * getBeta() + getAlpha(), l); + } + } + + return Matrices.createSquareMatrix(x); + } + + /** + * Calculates \int_{r_{min}}^{r_{max}}{x^{l+1}exp(-x)dx}. + * + * @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) { + + double f = 0; + long m = 0; - switch (m) { - // m = 1 never used - case 2: - roots = new double[2]; - // solve quadratic equation, all roots of which are real - final double det = sqrt(c[1] * c[1] - 4.0 * c[0]); - roots[0] = (-c[1] + det) * 0.5; - roots[1] = (-c[1] - det) * 0.5; - break; - case 3: - roots = new double[3]; - // solve cubic equation, all roots of which are real - roots = solveCubic(c[2], c[1], c[0]); - break; - default: - // use LaguerreSolver - roots = Arrays.stream(solver.solveAllComplex(c, 1.0)).mapToDouble(complex -> complex.getReal()).toArray(); - } + final int k = lPlusN - 1; - for (int i = 0; i < roots.length; i++) { - roots[i] = (roots[i] - getAlpha()) / getBeta(); - } + for (int i = 0; i < lPlusN; i++) { + m = 1; + for (int j = 0; j < i; j++) { + m *= (k - j); + } + f += m * fastPowLoop(x, k - i); + } - return roots; - - } + return f * exp; -} \ No newline at end of file + } + + private static double[] solveCubic(final double a, final double b, final double c) { + final double p = b / 3.0 - a * a / 9.0; + final double q = a * a * a / 27.0 - a * b / 6.0 + c / 2.0; + + final double ang = acos(-q / sqrt(-p * p * p)); + final double r = 2.0 * sqrt(-p); + var result = new double[3]; + double theta; + for (int k = -1; k < 2; k++) { + theta = (ang - 2.0 * PI * k) / 3.0; + result[k + 1] = r * cos(theta); + } + + for (int i = 0; i < result.length; i++) { + result[i] -= a / 3.0; + } + + return result; + } + + private double moment(int l) { + var bounds = this.transformedBounds(); + return momentIntegral(bounds[1], l, expUpper) - momentIntegral(bounds[0], l, expLower); + } + + private double momentIntegral(final double x, final int l, final double exp) { + + double e = 0; + int m = 0; + + final int n = getOrder(); + final int lPlusOne = l + 1; + + for (int i = 0; i < n; i++) { + m = lPlusOne; + for (int j = 1; j < i + 1; j++) { + m *= (lPlusOne + j); + } + e += ExponentialIntegrals.get(n - i).valueAt(x) * fastPowLoop(x, lPlusOne + i) / ((double) m); + } + + return e + auxilliaryIntegral(x, l + n, exp) / ((double) m); + + } + + private Vector coefficients() { + return momentMatrix().inverse().multiply(momentVector(m, 2 * m)); + } + + private SquareMatrix momentMatrix() { + + double[][] data = new double[m][m]; + moments = new double[2 * m]; + + // diagonal elements + IntStream.range(0, m).forEach(i -> data[i][i] = moment(i * 2)); + + // find (symmetric) non-diagonal elements + for (int i = 1, j = 0; i < m; i++) { + for (j = 0; j < i; j++) { + data[i][j] = moment(i + j); + data[j][i] = data[i][j]; + } + } + + for (int i = 0; i < m; i++) { + moments[i] = data[0][i]; + moments[i + m - 1] = data[i][m - 1]; + } + + moments[2 * m - 1] = moment(2 * m - 1); + + return Matrices.createSquareMatrix(data); + + } + + private Vector momentVector(final int lowerInclusive, final int upperExclusive) { + var array = IntStream.range(lowerInclusive, upperExclusive).mapToDouble(i -> -moments[i]).toArray(); + return new Vector(array); + } + + private Vector weights(final double[] roots) { + final var x = xMatrix(roots); + final var a = momentVector(0, m).inverted(); + + return x.inverse().multiply(a); + } + + private double[] roots() { + double[] roots; + double[] c = new double[m + 1]; + + // coefficients of the monic polynomial x_j^m + sum_{l=0}^{m-1}{c_lx_j^l} + System.arraycopy(coefficients().getData(), 0, c, 0, m); + c[m] = 1.0; + + switch (m) { + // m = 1 never used + case 2: + roots = new double[2]; + // solve quadratic equation, all roots of which are real + final double det = sqrt(c[1] * c[1] - 4.0 * c[0]); + roots[0] = (-c[1] + det) * 0.5; + roots[1] = (-c[1] - det) * 0.5; + break; + case 3: + roots = new double[3]; + // solve cubic equation, all roots of which are real + roots = solveCubic(c[2], c[1], c[0]); + break; + default: + // use LaguerreSolver + if (solver == null) { + solver = new LaguerreSolver(); + } + roots = Arrays.stream(solver.solveAllComplex(c, 1.0)).mapToDouble(complex -> complex.getReal()).toArray(); + } + + for (int i = 0; i < roots.length; i++) { + roots[i] = (roots[i] - getAlpha()) / getBeta(); + } + + return roots; + + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/CompositionProduct.java b/src/main/java/pulse/problem/schemes/rte/exact/CompositionProduct.java index f008189b..26b5d0bb 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/CompositionProduct.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/CompositionProduct.java @@ -6,79 +6,80 @@ import pulse.problem.schemes.rte.BlackbodySpectrum; /** - * A class for evaluating the definite integral ab f(x) En (α + - * β x) dx}. This integral appears as a result of analytically integrating the radiative transfer equation for an - * absorbing-emitting medium. The number n is the order of this integral, and α and β are the coefficients. + * A class for evaluating the definite integral + * ab f(x) En + * (α + β x) dx}. This integral appears as a + * result of analytically integrating the radiative transfer equation for an + * absorbing-emitting medium. The number n is the order of this integral, + * and α and β are the coefficients. * */ - public abstract class CompositionProduct extends AbstractIntegrator { - private double alpha; - private double beta; - private int order; + private double alpha; + private double beta; + private int order; + + private FunctionWithInterpolation expIntegral; + private BlackbodySpectrum blackbody; + + /** + * Constructs the composition product with the specified integration bounds. + * + * @param bounds integration bounds + */ + public CompositionProduct(Segment bounds) { + super(bounds); + } - private FunctionWithInterpolation expIntegral; - private BlackbodySpectrum blackbody; + /** + * Evaluates the integrand f(x) En + * (α + β x) dx}. + */ + @Override + public double integrand(double... vars) { + return blackbody.powerAt(vars[0]) * expIntegral.valueAt(alpha + beta * vars[0]); + } - /** - * Constructs the composition product with the specified integration bounds. - * @param bounds integration bounds - */ - - public CompositionProduct(Segment bounds) { - super(bounds); - } - - /** - * Evaluates the integrand f(x) En (α + - * β x) dx}. - */ - - @Override - public double integrand(double... vars) { - return blackbody.powerAt(vars[0]) * expIntegral.valueAt(alpha + beta * vars[0]); - } + public BlackbodySpectrum getEmissionFunction() { + return blackbody; + } - public BlackbodySpectrum getEmissionFunction() { - return blackbody; - } + public void setEmissionFunction(BlackbodySpectrum emissionFunction) { + this.blackbody = emissionFunction; + } - public void setEmissionFunction(BlackbodySpectrum emissionFunction) { - this.blackbody = emissionFunction; - } + public double getBeta() { + return beta; + } - public double getBeta() { - return beta; - } + public double getAlpha() { + return alpha; + } - public double getAlpha() { - return alpha; - } + public void setCoefficients(double alpha, double beta) { + this.alpha = alpha; + this.beta = beta; + } - public void setCoefficients(double alpha, double beta) { - this.alpha = alpha; - this.beta = beta; - } - - public int getOrder() { - return order; - } + public int getOrder() { + return order; + } - /** - * Sets the integration order n. Updates the exponential integral associated with this - * {@code CompositionProduct} upon completion. - * @param order an integer in the range from 1 to 4 inclusively - */ - - public void setOrder(int order) { - this.order = order; - expIntegral = ExponentialIntegrals.get(order); - } + /** + * Sets the integration order n. Updates the exponential integral + * associated with this {@code CompositionProduct} upon completion. + * + * @param order an integer in the range from 1 to 4 inclusively + */ + public void setOrder(int order) { + this.order = order; + expIntegral = ExponentialIntegrals.get(order); + } - @Override - public String getPrefix() { - return "Composition Product Integrator"; - } + @Override + public String getPrefix() { + return "Composition Product Integrator"; + } -} \ No newline at end of file +} 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 0c06593a..b3833ac3 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegral.java @@ -10,72 +10,71 @@ import pulse.properties.NumericProperty; /** - * A {@code MidpointIntegrator} for calculating the exponential integrals - * of order from 1 through 4. + * A {@code MidpointIntegrator} for calculating the exponential integrals of + * order from 1 through 4. * */ - class ExponentialIntegral extends MidpointIntegrator { - private double t; - private int order; + private static final long serialVersionUID = -5818633555456309668L; + private double t; + private int order; - private final static double EPS = 1E-10; - private final static int DEFAULT_INTEGRATION_SEGMENTS = 2048; - - /** - * Constructs an {@code ExponentialIntegral} with the specified - * {@code order} and the required number of {@code segments}. - * @param order the order of exponential integral - * @param segments the number of integration segments - */ + private final static double EPS = 1E-10; + private final static int DEFAULT_INTEGRATION_SEGMENTS = 2048; - public ExponentialIntegral(int order, NumericProperty segments) { - super(new Segment(0, 1), segments); // [0, 1] - cosine domain - setOrder(order); - } - - /** - * Constructs an {@code ExponentialIntegral} of the specified order - * with the default number of integration segments. - * @param order the order of exponential integral - */ + /** + * Constructs an {@code ExponentialIntegral} with the specified + * {@code order} and the required number of {@code segments}. + * + * @param order the order of exponential integral + * @param segments the number of integration segments + */ + public ExponentialIntegral(int order, NumericProperty segments) { + super(new Segment(0, 1), segments); // [0, 1] - cosine domain + setOrder(order); + } - public ExponentialIntegral(int order) { - this(order, derive(INTEGRATION_SEGMENTS, DEFAULT_INTEGRATION_SEGMENTS)); - setOrder(order); - } + /** + * Constructs an {@code ExponentialIntegral} of the specified order with the + * default number of integration segments. + * + * @param order the order of exponential integral + */ + public ExponentialIntegral(int order) { + this(order, derive(INTEGRATION_SEGMENTS, DEFAULT_INTEGRATION_SEGMENTS)); + setOrder(order); + } - /** - * Either calls the superclass method or, if the parameter is zero, - * uses the shortcut formula En(0) = 1/(n - 1). - */ - - @Override - public double integrate() { - return t < EPS ? 1.0 / (order - 1.0) : super.integrate(); - } + /** + * Either calls the superclass method or, if the parameter is zero, uses the + * shortcut formula En(0) = 1/(n - 1). + */ + @Override + public double integrate() { + return t < EPS ? 1.0 / (order - 1.0) : super.integrate(); + } - @Override - public double integrand(double... vars) { - final double mu = vars[0]; - return fastPowLoop(mu, order - 2) * exp(-t / mu); - } + @Override + public double integrand(double... vars) { + final double mu = vars[0]; + return fastPowLoop(mu, order - 2) * exp(-t / mu); + } - protected double getParameter() { - return t; - } + protected double getParameter() { + return t; + } - protected void setParameter(double t) { - this.t = t; - } + protected void setParameter(double t) { + this.t = t; + } - public int getOrder() { - return order; - } + public int getOrder() { + return order; + } - public void setOrder(int order) { - this.order = order; - } + public void setOrder(int order) { + this.order = order; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegrals.java b/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegrals.java index 549358e6..461d7738 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegrals.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/ExponentialIntegrals.java @@ -4,51 +4,50 @@ import pulse.math.Segment; /** - * A factory class for creating and evaluating {@code ExponentialIntegral}s of - * orders from 1 to 4. + * A factory class for creating and evaluating {@code ExponentialIntegral}s of + * orders from 1 to 4. * */ - public class ExponentialIntegrals { - public final static double CUTOFF = 20.0; // corresponds to a precision of 1E-5 - public final static int HIGHEST_ORDER = 4; - - private FunctionWithInterpolation[] exponentialIntegrals = new FunctionWithInterpolation[HIGHEST_ORDER + 1]; - private static ExponentialIntegrals instance = new ExponentialIntegrals(); - - private ExponentialIntegrals() { - final double LOWER_BOUND_E1 = 1E-8; - for (int i = 1; i < HIGHEST_ORDER + 1; i++) { - var ei = new ExponentialIntegral(i); - var min = i == 1 ? LOWER_BOUND_E1 : 0.0; // First-order exponential integral is discontinuous at 0.0, so - // discard this point - - exponentialIntegrals[i] = new FunctionWithInterpolation(new Segment(min, CUTOFF)) { - - @Override - public double evaluate(double t) { - ei.setParameter(t); - return ei.integrate(); - } - - }; - } - } - - /** - * Retrieves the pre-calculated interpolation functions for the exponential integrals. - * @param order the order (1 to 4) of the exponential integral - * @return a pre-calculated interpolation with the default bounds - */ - - public static FunctionWithInterpolation get(int order) { - return instance.exponentialIntegrals[order]; - } + public final static double CUTOFF = 20.0; // corresponds to a precision of 1E-5 + public final static int HIGHEST_ORDER = 4; + + private FunctionWithInterpolation[] exponentialIntegrals = new FunctionWithInterpolation[HIGHEST_ORDER + 1]; + private static ExponentialIntegrals instance = new ExponentialIntegrals(); + + private ExponentialIntegrals() { + final double LOWER_BOUND_E1 = 1E-8; + for (int i = 1; i < HIGHEST_ORDER + 1; i++) { + var ei = new ExponentialIntegral(i); + var min = i == 1 ? LOWER_BOUND_E1 : 0.0; // First-order exponential integral is discontinuous at 0.0, so + // discard this point + + exponentialIntegrals[i] = new FunctionWithInterpolation(new Segment(min, CUTOFF)) { + + @Override + public double evaluate(double t) { + ei.setParameter(t); + return ei.integrate(); + } + + }; + } + } + + /** + * Retrieves the pre-calculated interpolation functions for the exponential + * integrals. + * + * @param order the order (1 to 4) of the exponential integral + * @return a pre-calculated interpolation with the default bounds + */ + public static FunctionWithInterpolation get(int order) { + return instance.exponentialIntegrals[order]; + } // public static void main(String[] args) { // var ei = get(2); // System.out.println(ei.valueAt(0.01)); // } - -} \ No newline at end of file +} 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 c602745b..546bb2cf 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NewtonCotesQuadrature.java @@ -2,148 +2,150 @@ 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; import pulse.math.MidpointIntegrator; import pulse.math.Segment; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; /** - * A class for evaluating the composition product using a simple Newton-Cotes quadrature - * with a cutoff. + * A class for evaluating the composition product using a simple Newton-Cotes + * quadrature with a cutoff. * */ - public class NewtonCotesQuadrature extends CompositionProduct { - private final static int DEFAULT_SEGMENTS = 64; - private final static double DEFAULT_CUTOFF = 16.0; - private FixedIntervalIntegrator integrator; - private double cutoff; - - /** - * Constructs a default {@code NewtonCotesQuadrature} with integration bounds spanning from 0 to 1. - */ - - public NewtonCotesQuadrature() { - this(new Segment(0, 1)); - } - - /** - * Constructs a default {@code NewtonCotesQuadrature} whose integration bounds are specified by the argument. - * @param bounds the integration bounds - */ - - public NewtonCotesQuadrature(Segment bounds) { - this(bounds, derive(INTEGRATION_SEGMENTS, DEFAULT_SEGMENTS)); - } - - /** - * Constructs a custom {@code NewtonCotesQuadrature} with specified integration bounds and number of integration segments. - * The underlying integration scheme by default is a {@code SimpsonIntegrator}. - * @param bounds the integration bounds - * @param segments the number of integration segments. The higher this number, the higher is the accuracy. - * @see pulse.math.SimpsonIntegrator - */ - - public NewtonCotesQuadrature(Segment bounds, NumericProperty segments) { - super(bounds); - setCutoff(derive(INTEGRATION_CUTOFF, DEFAULT_CUTOFF)); - CompositionProduct reference = this; - integrator = new MidpointIntegrator(new Segment(0.0, 1.0), segments) { - - @Override - public double integrand(double... vars) { - return reference.integrand(vars); - } - - @Override - public String toString() { - return getDescriptor() + " ; " + getIntegrationSegments(); - } - - @Override - public String getDescriptor() { - return "Midpoint Integrator"; - } - - }; - integrator.setParent(this); - } - - /** - * Uses the Newton-Cotes integrator (by default, the Simpson's rule) to evaluate the composition product. - */ - - @Override - public double integrate() { - integrator.setBounds(truncatedBounds()); - return integrator.integrate(); - } - - /** - * This will retrieve the Newton-Cotes integrator, which by default is the Simpson integrator. - * @return the integrator - */ - - public FixedIntervalIntegrator getIntegrator() { - return integrator; - } - - @Override - public String toString() { - return getClass().getSimpleName() + " : " + cutoff + " ; " + integrator.getIntegrationSegments(); - } - - public NumericProperty getCutoff() { - return derive(INTEGRATION_CUTOFF, cutoff); - } - - public void setCutoff(NumericProperty cutoff) { - requireType(cutoff, INTEGRATION_CUTOFF); - this.cutoff = (double) cutoff.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == INTEGRATION_CUTOFF) { - setCutoff(property); - firePropertyChanged(this, property); - } - } - - @Override - public List listedTypes() { - var list = super.listedTypes(); - list.add(def(INTEGRATION_CUTOFF)); - list.add(def(INTEGRATION_SEGMENTS)); - return list; - } - - @Override - public boolean ignoreSiblings() { - return true; - } - - private Segment truncatedBounds() { - final double min = getBounds().getMinimum(); - final double max = getBounds().getMaximum(); - - double bound = (cutoff - getAlpha()) / getBeta(); - - double a = 0.5 - getBeta() / 2; // beta usually takes values of 1 or -1, so a is either 0 or 1 - double b = 1. - a; // either 1 or 0 - - return new Segment(max(bound, min) * a + min * b, max * a + min(bound, max) * b); - } - -} \ No newline at end of file + private static final long serialVersionUID = -177127670003926420L; + private final static int DEFAULT_SEGMENTS = 64; + private final static double DEFAULT_CUTOFF = 16.0; + private FixedIntervalIntegrator integrator; + private double cutoff; + + /** + * Constructs a default {@code NewtonCotesQuadrature} with integration + * bounds spanning from 0 to 1. + */ + public NewtonCotesQuadrature() { + this(new Segment(0, 1)); + } + + /** + * Constructs a default {@code NewtonCotesQuadrature} whose integration + * bounds are specified by the argument. + * + * @param bounds the integration bounds + */ + public NewtonCotesQuadrature(Segment bounds) { + this(bounds, derive(INTEGRATION_SEGMENTS, DEFAULT_SEGMENTS)); + } + + /** + * Constructs a custom {@code NewtonCotesQuadrature} with specified + * integration bounds and number of integration segments. The underlying + * integration scheme by default is a {@code SimpsonIntegrator}. + * + * @param bounds the integration bounds + * @param segments the number of integration segments. The higher this + * number, the higher is the accuracy. + * @see pulse.math.SimpsonIntegrator + */ + public NewtonCotesQuadrature(Segment bounds, NumericProperty segments) { + super(bounds); + setCutoff(derive(INTEGRATION_CUTOFF, DEFAULT_CUTOFF)); + CompositionProduct reference = this; + integrator = new MidpointIntegrator(new Segment(0.0, 1.0), segments) { + + @Override + public double integrand(double... vars) { + return reference.integrand(vars); + } + + @Override + public String toString() { + return getDescriptor() + " ; " + getIntegrationSegments(); + } + + @Override + public String getDescriptor() { + return "Midpoint Integrator"; + } + + }; + integrator.setParent(this); + } + + /** + * Uses the Newton-Cotes integrator (by default, the Simpson's rule) to + * evaluate the composition product. + */ + @Override + public double integrate() { + integrator.setBounds(truncatedBounds()); + return integrator.integrate(); + } + + /** + * This will retrieve the Newton-Cotes integrator, which by default is the + * Simpson integrator. + * + * @return the integrator + */ + public FixedIntervalIntegrator getIntegrator() { + return integrator; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " : " + cutoff + " ; " + integrator.getIntegrationSegments(); + } + + public NumericProperty getCutoff() { + return derive(INTEGRATION_CUTOFF, cutoff); + } + + public void setCutoff(NumericProperty cutoff) { + requireType(cutoff, INTEGRATION_CUTOFF); + this.cutoff = (double) cutoff.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == INTEGRATION_CUTOFF) { + setCutoff(property); + firePropertyChanged(this, property); + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(INTEGRATION_CUTOFF); + set.add(INTEGRATION_SEGMENTS); + return set; + } + + @Override + public boolean ignoreSiblings() { + return true; + } + + private Segment truncatedBounds() { + final double min = getBounds().getMinimum(); + final double max = getBounds().getMaximum(); + + double bound = (cutoff - getAlpha()) / getBeta(); + + double a = 0.5 - getBeta() / 2; // beta usually takes values of 1 or -1, so a is either 0 or 1 + double b = 1. - a; // either 1 or 0 + + return new Segment(max(bound, min) * a + min * b, max * a + min(bound, max) * b); + } + +} 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 b4aa263a..dbc23a8c 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringAnalyticalDerivatives.java @@ -10,7 +10,7 @@ import pulse.problem.schemes.rte.FluxesAndExplicitDerivatives; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; /** * A solver of the radiative transfer equation for an absorbing-emitting medium @@ -18,80 +18,79 @@ * formulae with the selected numerical quadrature. * */ - public class NonscatteringAnalyticalDerivatives extends NonscatteringRadiativeTransfer { - private static FunctionWithInterpolation ei2 = ExponentialIntegrals.get(2); - - public NonscatteringAnalyticalDerivatives(ParticipatingMedium problem, Grid grid) { - super(problem, grid); - var properties = (ThermoOpticalProperties)problem.getProperties(); - setFluxes(new FluxesAndExplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); - } - - /** - * Evaluates fluxes and their derivatives using analytical formulae and the - * selected numerical quadrature. Usually works best with the - * {@code ChandrasekharsQuadrature}. - */ - - @Override - public RTECalculationStatus compute(double U[]) { - super.compute(U); - fluxes(); - var fluxContainer = (FluxesAndExplicitDerivatives) getFluxes(); - IntStream.range(0, fluxContainer.getDensity() + 1) - .forEach(i -> fluxContainer.setFluxDerivative(i, evalFluxDerivative(i))); - - return RTECalculationStatus.NORMAL; - } - - /* + private static final long serialVersionUID = -7549047672012708753L; + private static FunctionWithInterpolation ei2 = ExponentialIntegrals.get(2); + + public NonscatteringAnalyticalDerivatives(ParticipatingMedium problem, Grid grid) { + super(problem, grid); + var properties = (ThermoOpticalProperties) problem.getProperties(); + setFluxes(new FluxesAndExplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); + } + + /** + * Evaluates fluxes and their derivatives using analytical formulae and the + * selected numerical quadrature.Usually works best with the + * {@code ChandrasekharsQuadrature} + * + * @return + */ + @Override + public RTECalculationStatus compute(double U[]) { + super.compute(U); + fluxes(); + var fluxContainer = (FluxesAndExplicitDerivatives) getFluxes(); + IntStream.range(0, fluxContainer.getDensity() + 1) + .forEach(i -> fluxContainer.setFluxDerivative(i, evalFluxDerivative(i))); + + return RTECalculationStatus.NORMAL; + } + + /* * -dF/d\tau * * = 2 R_1 E_2(y \tau_0) + 2 R_2 E_2( (1 - y) \tau_0 ) - \pi J*(y 'tau_0) * - */ - - private double evalFluxDerivative(final int uIndex) { - double t = opticalCoordinateAt(uIndex); - - double value = getRadiosityFront() * ei2.valueAt(t) - + getRadiosityRear() * ei2.valueAt(getFluxes().getOpticalThickness() - t) - - 2.0 * getEmissionFunction().powerAt(t) + integrateFirstOrder(t); - return 2.0 * value; - } - - private double integrateFirstOrder(final double y) { - double integral = 0; - final double tau0 = getFluxes().getOpticalThickness(); - var quadrature = getQuadrature(); - - setForIntegration(0, y); - quadrature.setCoefficients(y, -1); - integral += compare(y, 0) == 0 ? 0 : quadrature.integrate(); - - setForIntegration(y, tau0); - quadrature.setCoefficients(-y, 1); - integral += compare(y, tau0) == 0 ? 0 : quadrature.integrate(); - - return integral; - } - - /** - * This will set integration bounds by creating a segment using {@code x} and - * {@code y} values. Note this ignores the order of arguments, as the lower and - * upper bound will be equal to {@code min(x,y)} and {@code max(x,y)} - * respectively. The order of integration is set to unity. - * - * @param x lower bound - * @param y upper bound - */ - - private void setForIntegration(final double x, final double y) { - final var quadrature = getQuadrature(); - quadrature.setBounds(new Segment(x, y)); - quadrature.setOrder(1); - } - -} \ No newline at end of file + */ + private double evalFluxDerivative(final int uIndex) { + double t = opticalCoordinateAt(uIndex); + + double value = getRadiosityFront() * ei2.valueAt(t) + + getRadiosityRear() * ei2.valueAt(getFluxes().getOpticalThickness() - t) + - 2.0 * getEmissionFunction().powerAt(t) + integrateFirstOrder(t); + return 2.0 * value; + } + + private double integrateFirstOrder(final double y) { + double integral = 0; + final double tau0 = getFluxes().getOpticalThickness(); + var quadrature = getQuadrature(); + + setForIntegration(0, y); + quadrature.setCoefficients(y, -1); + integral += compare(y, 0) == 0 ? 0 : quadrature.integrate(); + + setForIntegration(y, tau0); + quadrature.setCoefficients(-y, 1); + integral += compare(y, tau0) == 0 ? 0 : quadrature.integrate(); + + return integral; + } + + /** + * This will set integration bounds by creating a segment using {@code x} + * and {@code y} values. Note this ignores the order of arguments, as the + * lower and upper bound will be equal to {@code min(x,y)} and + * {@code max(x,y)} respectively. The order of integration is set to unity. + * + * @param x lower bound + * @param y upper bound + */ + private void setForIntegration(final double x, final double y) { + final var quadrature = getQuadrature(); + quadrature.setBounds(new Segment(x, y)); + quadrature.setOrder(1); + } + +} 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 419c908e..c781488e 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringDiscreteDerivatives.java @@ -4,7 +4,7 @@ import pulse.problem.schemes.rte.FluxesAndImplicitDerivatives; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; /** * A solver of the radiative transfer equation for an absorbing-emitting medium @@ -12,20 +12,21 @@ * derivatives are calculated using the central-difference approximation. * */ - public class NonscatteringDiscreteDerivatives extends NonscatteringRadiativeTransfer { - public NonscatteringDiscreteDerivatives(ParticipatingMedium problem, Grid grid) { - super(problem, grid); - var properties = (ThermoOpticalProperties)problem.getProperties(); - setFluxes(new FluxesAndImplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); - } + private static final long serialVersionUID = -6919734351838124553L; + + public NonscatteringDiscreteDerivatives(ParticipatingMedium problem, Grid grid) { + super(problem, grid); + var properties = (ThermoOpticalProperties) problem.getProperties(); + setFluxes(new FluxesAndImplicitDerivatives(grid.getGridDensity(), properties.getOpticalThickness())); + } - @Override - public RTECalculationStatus compute(double U[]) { - super.compute(U); - fluxes(); - return RTECalculationStatus.NORMAL; - } + @Override + public RTECalculationStatus compute(double U[]) { + super.compute(U); + fluxes(); + return RTECalculationStatus.NORMAL; + } -} \ No newline at end of file +} 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 b95aa910..3afd8920 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/NonscatteringRadiativeTransfer.java @@ -17,228 +17,224 @@ public abstract class NonscatteringRadiativeTransfer extends RadiativeTransferSolver { - private static FunctionWithInterpolation ei3 = ExponentialIntegrals.get(3); - - private double emissivity; - - private BlackbodySpectrum emissionFunction; - private CompositionProduct quadrature; - - private double radiosityFront; - private double radiosityRear; - - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Quadrature Selector", CompositionProduct.class); - - protected NonscatteringRadiativeTransfer(ParticipatingMedium problem, Grid grid) { - super(); - instanceDescriptor.setSelectedDescriptor(ChandrasekharsQuadrature.class.getSimpleName()); - init(problem, grid); - emissionFunction = new BlackbodySpectrum(problem); - initQuadrature(); - instanceDescriptor.addListener(() -> initQuadrature()); - } - - @Override - public void init(ParticipatingMedium p, Grid grid) { - super.init(p, grid); - emissivity = (double)p.getProperties().getEmissivity().getValue(); - } - - /** - * 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. - */ - - @Override - public RTECalculationStatus compute(double[] array) { - emissionFunction.setInterpolation(interpolateTemperatureProfile(array)); - radiosities(); - return RTECalculationStatus.NORMAL; - } - - /** - * Calculates the radiative fluxes on the grid specified in the constructor - * arguments. This uses the values of radiosities and involves calculating the - * composition product using the selected quadratures. - * - * @see pulse.problem.schemes.rte.exact.CompositionProduct - */ - - public void fluxes() { - fluxFront(); - IntStream.range(1, getFluxes().getDensity()).forEach(i -> flux(i)); - fluxRear(); - } - - private void fluxFront() { - final double tau0 = getFluxes().getOpticalThickness(); - double flux = radiosityFront - 2.0 * radiosityRear * ei3.valueAt(tau0) - 2.0 * integrateSecondOrder(0.0, 1.0); - getFluxes().setFlux(0, flux); - } - - /* + private static final long serialVersionUID = 4934841542530728191L; + + private static final FunctionWithInterpolation ei3 = ExponentialIntegrals.get(3); + + private double emissivity; + + private BlackbodySpectrum emissionFunction; + private CompositionProduct quadrature; + + private double radiosityFront; + private double radiosityRear; + + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor( + "Quadrature Selector", CompositionProduct.class); + + protected NonscatteringRadiativeTransfer(ParticipatingMedium problem, Grid grid) { + super(); + instanceDescriptor.setSelectedDescriptor(ChandrasekharsQuadrature.class.getSimpleName()); + init(problem, grid); + emissionFunction = new BlackbodySpectrum(problem); + initQuadrature(); + instanceDescriptor.addListener(() -> initQuadrature()); + } + + @Override + public void init(ParticipatingMedium p, Grid grid) { + super.init(p, grid); + emissivity = (double) p.getProperties().getEmissivity().getValue(); + } + + /** + * 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 + public RTECalculationStatus compute(double[] array) { + emissionFunction.setInterpolation(interpolateTemperatureProfile(array)); + radiosities(); + return RTECalculationStatus.NORMAL; + } + + /** + * Calculates the radiative fluxes on the grid specified in the constructor + * arguments. This uses the values of radiosities and involves calculating + * the composition product using the selected quadratures. + * + * @see pulse.problem.schemes.rte.exact.CompositionProduct + */ + public void fluxes() { + fluxFront(); + IntStream.range(1, getFluxes().getDensity()).forEach(i -> flux(i)); + fluxRear(); + } + + private void fluxFront() { + final double tau0 = getFluxes().getOpticalThickness(); + double flux = radiosityFront - 2.0 * radiosityRear * ei3.valueAt(tau0) - 2.0 * integrateSecondOrder(0.0, 1.0); + getFluxes().setFlux(0, flux); + } + + /* * Assumes radiosities have already been calculated using radiosities() F*(1) = * -R_2 + 2R_1 E_3(\tau_0) + 2 int - */ - - private void fluxRear() { - var fluxes = getFluxes(); - final int N = fluxes.getDensity(); - final double tau0 = fluxes.getOpticalThickness(); - double flux = -radiosityRear + 2.0 * radiosityFront * ei3.valueAt(tau0) - + 2.0 * integrateSecondOrder(tau0, -1.0); - fluxes.setFlux(N, flux); - } - - protected void flux(int uIndex) { - final double t = opticalCoordinateAt(uIndex); - final double tau0 = getFluxes().getOpticalThickness(); - - quadrature.setOrder(2); - quadrature.setBounds(new Segment(0, t)); - quadrature.setCoefficients(t, -1.0); - final double I_1 = quadrature.integrate(); - - quadrature.setBounds(new Segment(t, tau0)); - quadrature.setCoefficients(-t, 1.0); - final double I_2 = quadrature.integrate(); - - double result = radiosityFront * ei3.valueAt(t) - radiosityRear * ei3.valueAt(tau0 - t) + I_1 - I_2; - - getFluxes().setFlux(uIndex, result * 2.0); - - } - - /** - * Retrieves the quadrature that is used to evaluate the composition product - * invoked when calculating the radiative fluxes. - * - * @return the quadrature - */ - - public CompositionProduct getQuadrature() { - return quadrature; - } - - /** - * Sets the quadrature and updates its spectral function to that specified by - * this object. - * - * @param specialIntegrator the quadrature used to evaluate the composition - * product - */ - - public void setQuadrature(CompositionProduct specialIntegrator) { - this.quadrature = specialIntegrator; - quadrature.setParent(this); - quadrature.setEmissionFunction(emissionFunction); - } - - public BlackbodySpectrum getEmissionFunction() { - return emissionFunction; - } - - public void setEmissionFunction(BlackbodySpectrum emissionFunction) { - this.emissionFunction = emissionFunction; - quadrature.setEmissionFunction(emissionFunction); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - // intentionally left blank - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(instanceDescriptor); - return list; - } - - @Override - public String toString() { - return "( " + this.getSimpleName() + " )"; - } - - public InstanceDescriptor getInstanceDescriptor() { - return instanceDescriptor; - } - - @Override - public String getDescriptor() { - return "Non-scattering Radiative Transfer"; - } - - public double getRadiosityFront() { - return radiosityFront; - } - - public double getRadiosityRear() { - return radiosityRear; - } - - /* + */ + private void fluxRear() { + var fluxes = getFluxes(); + final int N = fluxes.getDensity(); + final double tau0 = fluxes.getOpticalThickness(); + double flux = -radiosityRear + 2.0 * radiosityFront * ei3.valueAt(tau0) + + 2.0 * integrateSecondOrder(tau0, -1.0); + fluxes.setFlux(N, flux); + } + + protected void flux(int uIndex) { + final double t = opticalCoordinateAt(uIndex); + final double tau0 = getFluxes().getOpticalThickness(); + + quadrature.setOrder(2); + quadrature.setBounds(new Segment(0, t)); + quadrature.setCoefficients(t, -1.0); + final double I_1 = quadrature.integrate(); + + quadrature.setBounds(new Segment(t, tau0)); + quadrature.setCoefficients(-t, 1.0); + final double I_2 = quadrature.integrate(); + + double result = radiosityFront * ei3.valueAt(t) - radiosityRear * ei3.valueAt(tau0 - t) + I_1 - I_2; + + getFluxes().setFlux(uIndex, result * 2.0); + + } + + /** + * Retrieves the quadrature that is used to evaluate the composition product + * invoked when calculating the radiative fluxes. + * + * @return the quadrature + */ + public CompositionProduct getQuadrature() { + return quadrature; + } + + /** + * Sets the quadrature and updates its spectral function to that specified + * by this object. + * + * @param specialIntegrator the quadrature used to evaluate the composition + * product + */ + public void setQuadrature(CompositionProduct specialIntegrator) { + this.quadrature = specialIntegrator; + quadrature.setParent(this); + quadrature.setEmissionFunction(emissionFunction); + } + + public BlackbodySpectrum getEmissionFunction() { + return emissionFunction; + } + + public void setEmissionFunction(BlackbodySpectrum emissionFunction) { + this.emissionFunction = emissionFunction; + quadrature.setEmissionFunction(emissionFunction); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + @Override + public String toString() { + return "( " + this.getSimpleName() + " )"; + } + + public InstanceDescriptor getInstanceDescriptor() { + return instanceDescriptor; + } + + @Override + public String getDescriptor() { + return "Non-scattering Radiative Transfer"; + } + + public double getRadiosityFront() { + return radiosityFront; + } + + public double getRadiosityRear() { + return radiosityRear; + } + + /* * Radiosities of front and rear surfaces respectively in the assumption of * diffuse and opaque boundaries - */ + */ + private void radiosities() { + final double doubleReflectivity = 2.0 * (1.0 - emissivity); - 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); - final double a2 = a2(doubleReflectivity); + final double b = b(doubleReflectivity); + final double sq = 1.0 - b * b; + final double a1 = a1(doubleReflectivity); + final double a2 = a2(doubleReflectivity); - radiosityFront = (a1 + b * a2) / sq; - radiosityRear = (a2 + b * a1) / sq; - } + radiosityFront = (a1 + b * a2) / sq; + radiosityRear = (a2 + b * a1) / sq; + } - /* + /* * Coefficient b - */ + */ + private double b(final double doubleReflectivity) { + return doubleReflectivity * ei3.valueAt(getFluxes().getOpticalThickness()); + } - private double b(final double doubleReflectivity) { - return doubleReflectivity * ei3.valueAt(getFluxes().getOpticalThickness()); - } - - /* + /* * Coefficient a1 * * a1 = \varepsilon*J*(0) + integral = int_0^1 { J*(t) E_2(\tau_0 t) dt } - */ - - private double a1(final double doubleReflectivity) { - return emissivity * emissionFunction.powerAt(0.0) + doubleReflectivity * integrateSecondOrder(0.0, 1.0); - } + */ + private double a1(final double doubleReflectivity) { + return emissivity * emissionFunction.powerAt(0.0) + doubleReflectivity * integrateSecondOrder(0.0, 1.0); + } - /* + /* * Coefficient a2 * * a2 = \varepsilon*J*(0) + ... integral = int_0^1 { J*(t) E_2(\tau_0 t) dt } - */ - - private double a2(final double doubleReflectivity) { - final double tau0 = getFluxes().getOpticalThickness(); - return emissivity * emissionFunction.powerAt(tau0) + doubleReflectivity * integrateSecondOrder(tau0, -1.0); - } + */ + private double a2(final double doubleReflectivity) { + final double tau0 = getFluxes().getOpticalThickness(); + return emissivity * emissionFunction.powerAt(tau0) + doubleReflectivity * integrateSecondOrder(tau0, -1.0); + } - /* + /* * Source function J*(t) = (1 + @U[i]*tFactor)^4, where i = t/hx tFactor = * (tMax/t0) - */ - - private double integrateSecondOrder(double a, double b) { - quadrature.setOrder(2); - quadrature.setBounds(new Segment(0, getFluxes().getOpticalThickness())); - quadrature.setCoefficients(a, b); - return quadrature.integrate(); - } - - private void initQuadrature() { - setQuadrature(instanceDescriptor.newInstance(CompositionProduct.class)); - } - -} \ No newline at end of file + */ + private double integrateSecondOrder(double a, double b) { + quadrature.setOrder(2); + quadrature.setBounds(new Segment(0, getFluxes().getOpticalThickness())); + quadrature.setCoefficients(a, b); + return quadrature.integrate(); + } + + private void initQuadrature() { + setQuadrature(instanceDescriptor.newInstance(CompositionProduct.class)); + firePropertyChanged(this, instanceDescriptor); + } + +} diff --git a/src/main/java/pulse/problem/schemes/rte/exact/package-info.java b/src/main/java/pulse/problem/schemes/rte/exact/package-info.java index 1937a0ae..86abaeb4 100644 --- a/src/main/java/pulse/problem/schemes/rte/exact/package-info.java +++ b/src/main/java/pulse/problem/schemes/rte/exact/package-info.java @@ -1,6 +1,5 @@ /** - * Contains classes for solving the radiative transfer equation in an absorbing-emitting medium (no scattering) + * Contains classes for solving the radiative transfer equation in an absorbing-emitting medium (no scattering) * using a semi-analytical approach. */ - -package pulse.problem.schemes.rte.exact; \ No newline at end of file +package pulse.problem.schemes.rte.exact; diff --git a/src/main/java/pulse/problem/schemes/rte/package-info.java b/src/main/java/pulse/problem/schemes/rte/package-info.java index 1a0c0508..e13033d3 100644 --- a/src/main/java/pulse/problem/schemes/rte/package-info.java +++ b/src/main/java/pulse/problem/schemes/rte/package-info.java @@ -2,5 +2,4 @@ * Contains generic classes that act as a bridge between the specific * implementation of radiative heat transfer and the heat problem solvers. */ - -package pulse.problem.schemes.rte; \ No newline at end of file +package pulse.problem.schemes.rte; diff --git a/src/main/java/pulse/problem/schemes/solvers/ADILayeredSolver.java b/src/main/java/pulse/problem/schemes/solvers/ADILayeredSolver.java deleted file mode 100644 index fa2f20f7..00000000 --- a/src/main/java/pulse/problem/schemes/solvers/ADILayeredSolver.java +++ /dev/null @@ -1,90 +0,0 @@ -package pulse.problem.schemes.solvers; - -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericPropertyKeyword.SHELL_GRID_DENSITY; - -import java.util.HashMap; - -import pulse.problem.schemes.ADIScheme; -import pulse.problem.schemes.DifferenceScheme; -import pulse.problem.schemes.LayeredGrid2D; -import pulse.problem.schemes.Partition; -import pulse.problem.schemes.Partition.Location; -import pulse.problem.statements.CoreShellProblem; -import pulse.problem.statements.Problem; -import pulse.properties.NumericProperty; - -public class ADILayeredSolver extends ADIScheme implements Solver { - - public ADILayeredSolver() { - super(); - initGrid(getGrid().getGridDensity(), def(SHELL_GRID_DENSITY), getGrid().getTimeFactor()); - } - - public ADILayeredSolver(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor) { - initGrid(nCore, nShell, timeFactor); - } - - public ADILayeredSolver(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor, - NumericProperty timeLimit) { - setTimeLimit(timeLimit); - initGrid(nCore, nShell, timeFactor); - } - - public void initGrid(NumericProperty nCore, NumericProperty nShell, NumericProperty timeFactor) { - var map = new HashMap(); - map.put(Location.CORE_X, new Partition((int) nCore.getValue(), 1.0, 0.5)); - map.put(Location.CORE_Y, new Partition((int) nCore.getValue(), 1.0, 0.0)); - map.put(Location.FRONT_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); - map.put(Location.REAR_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); - map.put(Location.SIDE_X, new Partition((int) nShell.getValue(), 1.0, 0.0)); - map.put(Location.SIDE_Y, new Partition((int) nShell.getValue(), 1.0, 0.0)); - setGrid(new LayeredGrid2D(map, timeFactor)); - getGrid().setTimeFactor(timeFactor); - } - - private void prepareGrid(CoreShellProblem problem) { - var layeredGrid = (LayeredGrid2D) getGrid(); // TODO - layeredGrid.getPartition(Location.FRONT_Y).setGridMultiplier(problem.axialFactor()); - layeredGrid.getPartition(Location.REAR_Y).setGridMultiplier(problem.axialFactor()); - layeredGrid.getPartition(Location.SIDE_X).setGridMultiplier(problem.radialFactor()); - } - - @Override - public void solve(CoreShellProblem problem) { - prepareGrid(problem); - - // TODO - - } - - @Override - public DifferenceScheme copy() { - // TODO Auto-generated method stub - return null; - } - - @Override - public Class domain() { - return CoreShellProblem.class; - } - - @Override - public double signal() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public void timeStep(int m) { - // TODO Auto-generated method stub - - } - - @Override - public void finaliseStep() { - // TODO Auto-generated method stub - - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java index 90828d35..27196d50 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java @@ -6,8 +6,8 @@ import pulse.problem.schemes.Grid2D; import pulse.problem.schemes.TridiagonalMatrixAlgorithm; import pulse.problem.statements.ClassicalProblem2D; -import pulse.problem.statements.ExtendedThermalProperties; import pulse.problem.statements.Problem; +import pulse.problem.statements.model.ExtendedThermalProperties; import pulse.properties.NumericProperty; /** @@ -15,295 +15,293 @@ * two-dimensional linearised problem. * */ - public class ADILinearisedSolver extends ADIScheme implements Solver { - private TridiagonalMatrixAlgorithm tridiagonal; - - private int N; - private double hx; - private double hy; - private double tau; - private int firstIndex; - private int lastIndex; + private static final long serialVersionUID = 5354981341912770336L; + + private TridiagonalMatrixAlgorithm tridiagonal; + + private int N; + private double hx; + private double hy; + private double tau; + private int firstIndex; + private int lastIndex; + + private double d; + private double l; + private double Bi1; + private double Bi3; + + private double[][] U1; + private double[][] U2; + private double[][] U1_E; + private double[][] U2_E; + + private double[] a1; + private double[] b1; + private double[] c1; + + private double a2; + private double b2; + private double c2; + + private double a11; + private double _a11; + private double b11; + private double _b11; + private double _b12; + private double _c11; + private double HX2; + private double HY2; + + private double C1_U2; + private double C2_U2; + private double C3_U2; + + private double C1_U1; + + private double TAU_HY; + private double OMEGA_SQ_HX2; + private double E_C_U2; + private double E_C_U1; + + private final static double EPS = 1e-8; + + public ADILinearisedSolver() { + super(); + } + + public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, 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]; + + U1_E = new double[N + 3][N + 3]; + U2_E = new double[N + 3][N + 3]; + + a1 = new double[N + 1]; + b1 = new double[N + 1]; + c1 = new double[N + 1]; + } + + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); + + var grid = getGrid(); + tridiagonal = new TridiagonalMatrixAlgorithm(grid); - private double d; - private double l; - private double Bi1; - private double Bi3; + hx = grid.getXStep(); + hy = ((Grid2D) getGrid()).getYStep(); + HX2 = hx * hx; + HY2 = hy * hy; - private double[][] U1; - private double[][] U2; - private double[][] U1_E; - private double[][] U2_E; - - private double[] a1; - private double[] b1; - private double[] c1; - - private double a2; - private double b2; - private double c2; - - private double a11; - private double _a11; - private double b11; - private double _b11; - private double _b12; - private double _c11; - private double HX2; - private double HY2; - - private double C1_U2; - private double C2_U2; - private double C3_U2; - - private double C1_U1; - - private double TAU_HY; - private double OMEGA_SQ_HX2; - private double E_C_U2; - private double E_C_U1; - - private final static double EPS = 1e-8; - - public ADILinearisedSolver() { - super(); - } - - public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } - - public ADILinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } - - private void prepare(ClassicalProblem2D problem) { - super.prepare(problem); - - var grid = getGrid(); - tridiagonal = new TridiagonalMatrixAlgorithm(grid); - - N = (int) grid.getGridDensity().getValue(); - - hx = grid.getXStep(); - hy = ((Grid2D) getGrid()).getYStep(); - HX2 = hx * hx; - HY2 = hy * hy; - - tau = grid.getTimeStep(); - - var properties = (ExtendedThermalProperties)problem.getProperties(); - - Bi1 = (double) properties.getHeatLoss().getValue(); - Bi3 = (double) properties.getSideLosses().getValue(); - - d = (double) properties.getSampleDiameter().getValue(); - final double fovOuter = (double) properties.getFOVOuter().getValue(); - final double fovInner = (double) properties.getFOVInner().getValue(); - l = (double) properties.getSampleThickness().getValue(); - - // end - - 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]; - - a1 = new double[N + 1]; - b1 = new double[N + 1]; - c1 = new double[N + 1]; - - // 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; - - firstIndex = (int) (fovInner / d / hx); - firstIndex = firstIndex < 0 ? 0 : firstIndex; - - initConst(); - } - - // precalculated FD constants - private void initConst() { - final double OMEGA = 2.0 * l / d; - final double OMEGA_SQ = OMEGA * OMEGA; - - for (int i = 1; i < N + 1; i++) { - a1[i] = OMEGA_SQ * (i - 0.5) / (HX2 * i); - b1[i] = 2. / tau + 2. * OMEGA_SQ / HX2; - c1[i] = OMEGA_SQ * (i + 0.5) / (HX2 * i); - } + tau = grid.getTimeStep(); - a2 = 1. / HY2; - b2 = 2. / HY2 + 2. / tau; - c2 = 1. / HY2; + var properties = (ExtendedThermalProperties) problem.getProperties(); - // precalc coefs + Bi1 = (double) properties.getHeatLoss().getValue(); + Bi3 = (double) properties.getSideLosses().getValue(); - a11 = 1.0 / (1.0 + HX2 / (OMEGA_SQ * tau)); - b11 = 0.5 * tau / (1.0 + OMEGA_SQ * tau / HX2); + d = (double) properties.getSampleDiameter().getValue(); + final double fovOuter = (double) properties.getFOVOuter().getValue(); + final double fovInner = (double) properties.getFOVInner().getValue(); + 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; + + firstIndex = (int) (fovInner / d / hx); + firstIndex = firstIndex < 0 ? 0 : firstIndex; + + initConst(); + } + + // precalculated FD constants + private void initConst() { + final double OMEGA = 2.0 * l / d; + final double OMEGA_SQ = OMEGA * OMEGA; + + for (int i = 1; i < N + 1; i++) { + a1[i] = OMEGA_SQ * (i - 0.5) / (HX2 * i); + b1[i] = 2. / tau + 2. * OMEGA_SQ / HX2; + c1[i] = OMEGA_SQ * (i + 0.5) / (HX2 * i); + } - _a11 = 1.0 / (1.0 + Bi1 * hy + HY2 / tau); - _b11 = 1.0 / ((1 + hy * Bi1) * tau + HY2); - _c11 = 0.5 * HY2 * tau * OMEGA_SQ / HX2; - _b12 = _c11 * _b11; + a2 = 1. / HY2; + b2 = 2. / HY2 + 2. / tau; + c2 = 1. / HY2; - C1_U2 = 1.0 + hx * OMEGA * Bi3; - C2_U2 = OMEGA_SQ * tau; - C3_U2 = HX2 * tau / (2.0 * HY2); + // precalc coefs + a11 = 1.0 / (1.0 + HX2 / (OMEGA_SQ * tau)); + b11 = 0.5 * tau / (1.0 + OMEGA_SQ * tau / HX2); - C1_U1 = 1.0 + hy * Bi1; - - TAU_HY = tau * hy; - OMEGA_SQ_HX2 = OMEGA_SQ / HX2; - - E_C_U2 = 2.0 * hx * OMEGA * Bi3; - E_C_U1 = 2.0 * hy * Bi1; - } + _a11 = 1.0 / (1.0 + Bi1 * hy + HY2 / tau); + _b11 = 1.0 / ((1 + hy * Bi1) * tau + HY2); + _c11 = 0.5 * HY2 * tau * OMEGA_SQ / HX2; + _b12 = _c11 * _b11; - @Override - public void solve(ClassicalProblem2D problem) { - prepare(problem); - runTimeSequence(problem); - } + C1_U2 = 1.0 + hx * OMEGA * Bi3; + C2_U2 = OMEGA_SQ * tau; + C3_U2 = HX2 * tau / (2.0 * HY2); - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ADILinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } + C1_U1 = 1.0 + hy * Bi1; - @Override - public Class domain() { - return ClassicalProblem2D.class; - } + TAU_HY = tau * hy; + OMEGA_SQ_HX2 = OMEGA_SQ / HX2; - @Override - public double signal() { - double sum = 0; + E_C_U2 = 2.0 * hx * OMEGA * Bi3; + E_C_U1 = 2.0 * hy * Bi1; + } - for (int i = firstIndex; i <= lastIndex; i++) - sum += U1[i][N]; + @Override + public void solve(ClassicalProblem2D problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); + } - return sum / (lastIndex - firstIndex + 1); - } + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ADILinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } - public double pulse(final int m, final int i) { - return ((DiscretePulse2D) getDiscretePulse()).evaluateAt((m - EPS) * tau, i * hx); - } + @Override + public Class[] domain() { + return new Class[]{ClassicalProblem2D.class}; + } - private void extendedU1(final int m) { - for (int i = 0; i <= N; i++) { + @Override + public double signal() { + double sum = 0; - System.arraycopy(U1[i], 0, U1_E[i + 1], 1, N + 1); + for (int i = firstIndex; i <= lastIndex; i++) { + sum += U1[i][N]; + } - U1_E[i + 1][0] = U1[i][1] + 2.0 * hy * pulse(m, i) - E_C_U1 * U1[i][0]; - U1_E[i + 1][N + 2] = U1[i][N - 1] - E_C_U1 * U1[i][N]; - } + return sum / (lastIndex - firstIndex + 1); + } - } + public double pulse(final int m, final int i) { + return ((DiscretePulse2D) getDiscretePulse()).evaluateAt((m - EPS) * tau, i * hx); + } - private void extendedU2() { - for (int j = 0; j <= N; j++) { + private void extendedU1(final int m) { + for (int i = 0; i <= N; i++) { - for (int i = 0; i <= N; i++) { - U2_E[i + 1][j + 1] = U2[i][j]; - } + System.arraycopy(U1[i], 0, U1_E[i + 1], 1, N + 1); + U1_E[i + 1][0] = U1[i][1] + 2.0 * hy * pulse(m, i) - E_C_U1 * U1[i][0]; + U1_E[i + 1][N + 2] = U1[i][N - 1] - E_C_U1 * U1[i][N]; + } - U2_E[N + 2][j + 1] = U2[N - 1][j] - E_C_U2 * U2[N][j]; - } - } + } - private double diff2(double[][] U, final int i, final int j) { - return (U[i][j + 1] - 2. * U1_E[i][j] + U[i][j - 1]); - } + private void extendedU2() { + for (int j = 0; j <= N; j++) { - private double diff2r(double[][] U, final int i, final int j) { - final double C = 1.0 / (2.0 * (i - 1.0)); - return U[i + 1][j] * (1.0 + C) - 2. * U[i][j] + (1.0 - C) * U[i - 1][j]; - } + for (int i = 0; i <= N; i++) { + U2_E[i + 1][j + 1] = U2[i][j]; + } - @Override - public void timeStep(int m) { - var alpha = tridiagonal.getAlpha(); - var beta = tridiagonal.getBeta(); + U2_E[N + 2][j + 1] = U2[N - 1][j] - E_C_U2 * U2[N][j]; + } + } - /* create extended U1 array to accommodate edge values */ + private double diff2(double[][] U, final int i, final int j) { + return (U[i][j + 1] - 2. * U1_E[i][j] + U[i][j - 1]); + } - extendedU1(m); + private double diff2r(double[][] U, final int i, final int j) { + final double C = 1.0 / (2.0 * (i - 1.0)); + return U[i + 1][j] * (1.0 + C) - 2. * U[i][j] + (1.0 - C) * U[i - 1][j]; + } - // first equation, i -> x (radius), j -> y (thickness) + @Override + public void timeStep(int m) { + var alpha = tridiagonal.getAlpha(); + var beta = tridiagonal.getBeta(); - tridiagonal.setAlpha(1, a11); + /* create extended U1 array to accommodate edge values */ + extendedU1(m); - for (int j = 0; j <= N; j++) { + // first equation, i -> x (radius), j -> y (thickness) + tridiagonal.setAlpha(1, a11); - tridiagonal.setBeta(1, b11 * (2. * U1_E[1][j + 1] / tau + diff2(U1_E, 1, j + 1) / HY2)); + for (int j = 0; j <= N; j++) { - for (int i = 1; i < N; i++) { - final double F = -2. * U1_E[i + 1][j + 1] / tau - diff2(U1_E, i + 1, j + 1) / HY2; - final double denominator = b1[i] - a1[i] * alpha[i]; - tridiagonal.setAlpha(i + 1, c1[i] / denominator); - tridiagonal.setBeta(i + 1, (a1[i] * beta[i] - F) / denominator); - } + tridiagonal.setBeta(1, b11 * (2. * U1_E[1][j + 1] / tau + diff2(U1_E, 1, j + 1) / HY2)); - U2[N][j] = (C2_U2 * beta[N] + HX2 * U1_E[N + 1][j + 1] + C3_U2 * diff2(U1_E, N + 1, j + 1)) - / ((C1_U2 - alpha[N]) * C2_U2 + HX2); + for (int i = 1; i < N; i++) { + final double F = -2. * U1_E[i + 1][j + 1] / tau - diff2(U1_E, i + 1, j + 1) / HY2; + final double denominator = b1[i] - a1[i] * alpha[i]; + tridiagonal.setAlpha(i + 1, c1[i] / denominator); + tridiagonal.setBeta(i + 1, (a1[i] * beta[i] - F) / denominator); + } - for (int i = N - 1; i >= 0; i--) { - U2[i][j] = alpha[i + 1] * U2[i + 1][j] + beta[i + 1]; - } + U2[N][j] = (C2_U2 * beta[N] + HX2 * U1_E[N + 1][j + 1] + C3_U2 * diff2(U1_E, N + 1, j + 1)) + / ((C1_U2 - alpha[N]) * C2_U2 + HX2); - } + for (int i = N - 1; i >= 0; i--) { + U2[i][j] = alpha[i + 1] * U2[i + 1][j] + beta[i + 1]; + } - // second equation + } - /* create extended U2 array to accommodate edge values */ + // second equation - extendedU2(); - tridiagonal.setAlpha(1, _a11); + /* create extended U2 array to accommodate edge values */ + extendedU2(); + tridiagonal.setAlpha(1, _a11); - for (int i = 1; i <= N; i++) { + for (int i = 1; i <= N; i++) { - tridiagonal.setBeta(1, - (TAU_HY * pulse(m + 1, i) + HY2 * U2_E[i + 1][1]) * _b11 + _b12 * diff2r(U2_E, i + 1, 1)); + tridiagonal.setBeta(1, + (TAU_HY * pulse(m + 1, i) + HY2 * U2_E[i + 1][1]) * _b11 + _b12 * diff2r(U2_E, i + 1, 1)); - for (int j = 1; j < N; j++) { - final double F = -2. / tau * U2_E[i + 1][j + 1] - OMEGA_SQ_HX2 * diff2r(U2_E, i + 1, j + 1); - final double denominator = b2 - a2 * alpha[j]; - tridiagonal.setAlpha(j + 1, c2 / denominator); - tridiagonal.setBeta(j + 1, (a2 * beta[j] - F) / denominator); - } + for (int j = 1; j < N; j++) { + final double F = -2. / tau * U2_E[i + 1][j + 1] - OMEGA_SQ_HX2 * diff2r(U2_E, i + 1, j + 1); + final double denominator = b2 - a2 * alpha[j]; + tridiagonal.setAlpha(j + 1, c2 / denominator); + tridiagonal.setBeta(j + 1, (a2 * beta[j] - F) / denominator); + } - U1[i][N] = (tau * beta[N] + HY2 * U2_E[i + 1][N + 1] + _c11 * diff2r(U2_E, i + 1, N +1) ) - / ((C1_U1 - alpha[N]) * tau + HY2); + U1[i][N] = (tau * beta[N] + HY2 * U2_E[i + 1][N + 1] + _c11 * diff2r(U2_E, i + 1, N + 1)) + / ((C1_U1 - alpha[N]) * tau + HY2); - tridiagonal.sweep(U1[i]); + tridiagonal.sweep(U1[i]); - } + } - // i = 0 boundary - tridiagonal.setBeta(1, - (TAU_HY * pulse(m + 1) + HY2 * U2_E[1][1]) * _b11 + 2.0 * _b12 * (U2_E[2][1] - U2_E[1][1])); + // i = 0 boundary + tridiagonal.setBeta(1, + (TAU_HY * pulse(m + 1) + HY2 * U2_E[1][1]) * _b11 + 2.0 * _b12 * (U2_E[2][1] - U2_E[1][1])); - for (int j = 1; j < N; j++) { - final double F = -2. / tau * U2_E[1][j + 1] - 2.0 * OMEGA_SQ_HX2 * (U2_E[2][j + 1] - U2_E[1][j + 1]); - tridiagonal.setBeta(j + 1, (F - a2 * beta[j]) / (a2 * alpha[j] - b2)); - } + for (int j = 1; j < N; j++) { + final double F = -2. / tau * U2_E[1][j + 1] - 2.0 * OMEGA_SQ_HX2 * (U2_E[2][j + 1] - U2_E[1][j + 1]); + tridiagonal.setBeta(j + 1, (F - a2 * beta[j]) / (a2 * alpha[j] - b2)); + } - U1[0][N] = (tau * beta[N] + HY2 * U2_E[1][N + 1] + 2.0 * _c11 * (U2_E[2][N + 1] - U2_E[1][N + 1])) - / ((C1_U1 - alpha[N]) * tau + HY2); + U1[0][N] = (tau * beta[N] + HY2 * U2_E[1][N + 1] + 2.0 * _c11 * (U2_E[2][N + 1] - U2_E[1][N + 1])) + / ((C1_U1 - alpha[N]) * tau + HY2); - tridiagonal.sweep(U1[0]); - } + tridiagonal.sweep(U1[0]); + } - @Override - public void finaliseStep() { - // do nothing - } + @Override + public void finaliseStep() { + // do nothing + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index 53ba210e..5b706a42 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -1,159 +1,164 @@ package pulse.problem.schemes.solvers; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; import static pulse.ui.Messages.getString; -import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ExplicitScheme; -import pulse.problem.schemes.FixedPointIterations; import pulse.problem.schemes.RadiativeTransferCoupling; 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.ParticipatingMedium; import pulse.problem.statements.Problem; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; -public class ExplicitCoupledSolver extends ExplicitScheme implements Solver, FixedPointIterations { - - private RadiativeTransferCoupling coupling; - private RadiativeTransferSolver rte; - private RTECalculationStatus status; - private Fluxes fluxes; - - private double hx; - private double a; - private double nonlinearPrecision; - private double pls; - - private int N; - - private double HX_NP; - private double prefactor; - - public ExplicitCoupledSolver() { - this( derive(GRID_DENSITY, 80), derive(TAU_FACTOR, 0.5) ); - } - - public ExplicitCoupledSolver(NumericProperty N, NumericProperty timeFactor) { - super( N, timeFactor ); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - setCoupling(new RadiativeTransferCoupling()); - status = RTECalculationStatus.NORMAL; - } - - private void prepare(ParticipatingMedium problem) { - super.prepare(problem); - - var grid = getGrid(); - - coupling.init(problem, grid); - rte = coupling.getRadiativeTransferEquation(); - fluxes = coupling.getRadiativeTransferEquation().getFluxes(); - - N = (int) grid.getGridDensity().getValue(); - hx = grid.getXStep(); - - var p = (ThermoOpticalProperties) problem.getProperties(); - double Bi = (double) p.getHeatLoss().getValue(); - - a = 1. / (1. + Bi * hx); - - final double opticalThickness = (double) p.getOpticalThickness().getValue(); - final double Np = (double) p.getPlanckNumber().getValue(); - final double tau = getGrid().getTimeStep(); - - HX_NP = hx / Np; - prefactor = tau * opticalThickness / Np; - } - - @Override - public void solve(ParticipatingMedium problem) throws SolverException { - this.prepare(problem); - status = coupling.getRadiativeTransferEquation().compute(getPreviousSolution()); - runTimeSequence(problem); - - if (status != RTECalculationStatus.NORMAL) - throw new SolverException(status.toString()); - } - - @Override - public boolean normalOperation() { - return super.normalOperation() && (status == RTECalculationStatus.NORMAL); - } - - @Override - public void timeStep(int m) { - pls = pulse(m); - doIterations(getCurrentSolution(), nonlinearPrecision, m); - } - - @Override - public void iteration(final int m) { - /* +public abstract class ExplicitCoupledSolver extends ExplicitScheme + implements Solver { + + private RadiativeTransferCoupling coupling; + private RTECalculationStatus status; + private Fluxes fluxes; + + private double hx; + private double a; + + private int N; + + private double HX_NP; + private double prefactor; + 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)); + } + + public ExplicitCoupledSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + setCoupling(new RadiativeTransferCoupling()); + status = RTECalculationStatus.NORMAL; + } + + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); + + var grid = getGrid(); + + coupling.init((ParticipatingMedium) problem, grid); + fluxes = coupling.getRadiativeTransferEquation().getFluxes(); + setCalculationStatus(fluxes.checkArrays()); + + N = (int) grid.getGridDensity().getValue(); + hx = grid.getXStep(); + + var p = (ThermoOpticalProperties) problem.getProperties(); + //combined Biot + double Bi = (double) p.getHeatLoss().getValue() + (double) p.getConvectiveLosses().getValue(); + + a = 1. / (1. + Bi * hx); + + final double opticalThickness = (double) p.getOpticalThickness().getValue(); + final double Np = (double) p.getPlanckNumber().getValue(); + final double tau = getGrid().getTimeStep(); + + HX_NP = hx / Np; + prefactor = tau * opticalThickness / Np; + + zeta = (double) ((ParticipatingMedium) problem).getGeometricFactor().getValue(); + } + + @Override + public void timeStep(int m) throws SolverException { + explicitSolution(); + } + + @Override + public void finaliseStep() throws SolverException { + super.finaliseStep(); + if (autoUpdateFluxes) { + var rte = this.getCoupling().getRadiativeTransferEquation(); + setCalculationStatus(rte.compute(getCurrentSolution())); + } + coupling.getRadiativeTransferEquation().getFluxes().store(); + } + + @Override + public void solve(ParticipatingMedium problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public void explicitSolution() { + /* * Uses the heat equation explicitly to calculate the grid-function everywhere * except the boundaries - */ - explicitSolution(); - - var V = getCurrentSolution(); - - // Front face - V[0] = (V[1] + hx * pls - HX_NP * fluxes.getFlux(0)) * a; - // Rear face - V[N] = (V[N - 1] + HX_NP * fluxes.getFlux(N)) * a; - } - - @Override - public void finaliseIteration(double[] V) { - status = rte.compute(V); - } - - @Override - public double phi(final int i) { - return prefactor * fluxes.fluxDerivative(i); - } - - @Override - public void finaliseStep() { - super.finaliseStep(); - coupling.getRadiativeTransferEquation().getFluxes().store(); - } - - public RadiativeTransferCoupling getCoupling() { - return coupling; - } - - public void setCoupling(RadiativeTransferCoupling coupling) { - this.coupling = coupling; - this.coupling.setParent(this); - } - - /** - * Prints out the description of this problem type. - * - * @return a verbose description of the problem. - */ - - @Override - public String toString() { - return getString("ExplicitScheme.4"); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ExplicitCoupledSolver(grid.getGridDensity(), grid.getTimeFactor()); - } - - @Override - public Class domain() { - return ParticipatingMedium.class; - } - -} \ No newline at end of file + */ + super.explicitSolution(); + + var V = getCurrentSolution(); + + double pls = getCurrentPulseValue(); + + // Front face + V[0] = (V[1] + hx * zeta * pls - HX_NP * fluxes.getFlux(0)) * a; + // Rear face + V[N] = (V[N - 1] + hx * (1.0 - zeta) * pls + HX_NP * fluxes.getFlux(N)) * a; + } + + @Override + public boolean normalOperation() { + return super.normalOperation() && (status == RTECalculationStatus.NORMAL); + } + + @Override + public double phi(final int i) { + return prefactor * fluxes.fluxDerivative(i); + } + + public RadiativeTransferCoupling getCoupling() { + return coupling; + } + + public final void setCoupling(RadiativeTransferCoupling coupling) { + this.coupling = coupling; + this.coupling.setParent(this); + } + + /** + * Prints out the description of this problem type. + * + * @return a verbose description of the problem. + */ + @Override + public String toString() { + return getString("ExplicitScheme.4"); + } + + @Override + public Class[] domain() { + return new Class[]{ParticipatingMedium.class}; + } + + public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { + this.status = calculationStatus; + if (status != RTECalculationStatus.NORMAL) { + throw new SolverException(status.toString(), + RTE_SOLVER_ERROR); + } + } + + public final boolean isAutoUpdateFluxes() { + return this.autoUpdateFluxes; + } + + public final void setAutoUpdateFluxes(boolean auto) { + this.autoUpdateFluxes = auto; + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java new file mode 100644 index 00000000..6d40c3b8 --- /dev/null +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolverNL.java @@ -0,0 +1,93 @@ +/* + * Copyright 2022 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.problem.schemes.solvers; + +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.FixedPointIterations; +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; +import pulse.ui.Messages; + +/** + * + * @author Artem Lunev + */ +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); + } + + @Override + public void iteration(final int m) { + explicitSolution(); + } + + @Override + 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); + } + + public final void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } else { + super.set(type, property); + } + } + + @Override + public String toString() { + return Messages.getString("ExplicitScheme.5"); + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java index db3a7c99..cd667beb 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java @@ -9,10 +9,9 @@ /** * Performs a fully-dimensionless calculation for the {@code LinearisedProblem}. *

- * Relies on using the heat equation to - * calculate the value of the grid-function at the next timestep. Fills the - * {@code grid} completely at each specified spatial point. The heating curve is - * updated with the rear-side temperature + * Relies on using the heat equation to calculate the value of the grid-function + * at the next timestep. Fills the {@code grid} completely at each specified + * spatial point. The heating curve is updated with the rear-side temperature * Θ(xN,ti) (here * N is the grid density) at the end of {@code timeLimit} * intervals, which comprise of {@code timeLimit/tau} time steps. The @@ -42,62 +41,66 @@ * ensures that the error is not too high (typically a {@code 1.5E-2} relative * error). *

- * + * * @see super.solve(Problem) */ - public class ExplicitLinearisedSolver extends ExplicitScheme implements Solver { - private int N; - private double hx; - private double a; + private static final long serialVersionUID = 3084350485569519036L; + private int N; + private double hx; + private double a; + private double zeta; + + public ExplicitLinearisedSolver() { + super(); + } + + public ExplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + } - public ExplicitLinearisedSolver() { - super(); - } + public ExplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } - public ExplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); - public ExplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); - @Override - public void prepare(Problem problem) { - super.prepare(problem); + N = (int) getGrid().getGridDensity().getValue(); + hx = getGrid().getXStep(); - N = (int) getGrid().getGridDensity().getValue(); - hx = getGrid().getXStep(); + final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); + a = 1. / (1. + Bi1 * hx); + } - final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - a = 1. / (1. + Bi1 * hx); - } + @Override + public void solve(ClassicalProblem problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); + } - @Override - public void solve(ClassicalProblem problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public void timeStep(int m) { - explicitSolution(); - var V = getCurrentSolution(); - setSolutionAt(0, (V[1] + hx * pulse(m)) * a); - setSolutionAt(N, V[N - 1] * a); - } + @Override + public void timeStep(int m) { + explicitSolution(); + var V = getCurrentSolution(); + double pulse = getCurrentPulseValue(); + setSolutionAt(0, (V[1] + hx * zeta * pulse) * a); + setSolutionAt(N, (V[N - 1] + hx * (1.0 - zeta) * pulse) * a); + } - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ExplicitLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ExplicitLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } - @Override - public Class domain() { - return ClassicalProblem.class; - } + @Override + public Class[] domain() { + return new Class[]{ClassicalProblem.class}; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java index 5e8a9a66..546abc03 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitNonlinearSolver.java @@ -19,123 +19,122 @@ public class ExplicitNonlinearSolver extends ExplicitScheme implements Solver, FixedPointIterations { - private int N; - private double hx; - private double pls; - - private double dT_T; - - private double a00; - private double a11; - private double f01; - private double fN1; - - private double nonlinearPrecision; - - public ExplicitNonlinearSolver() { - super(); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - public ExplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - public ExplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - private void prepare(NonlinearProblem problem) { - super.prepare(problem); - - var grid = getGrid(); - - N = (int) grid.getGridDensity().getValue(); - hx = grid.getXStep(); - final double tau = grid.getTimeStep(); - - var p = problem.getProperties(); - final double T = (double) p.getTestTemperature().getValue(); - final double dT = p.maximumHeating((Pulse2D)problem.getPulse()); - - a00 = 2 * tau / (hx * hx + 2 * tau); - a11 = hx * hx / (2.0 * tau); - final double Bi1 = (double) p.getHeatLoss().getValue(); - f01 = 0.25 * Bi1 * T / dT; - fN1 = 0.25 * Bi1 * T / dT; - - dT_T = dT/T; - } - - @Override - public void timeStep(int m) { - explicitSolution(); - pls = pulse(m); - doIterations(getCurrentSolution(), nonlinearPrecision, m); - } - - @Override - public void iteration(int m) { - var V = getCurrentSolution(); - var U = getPreviousSolution(); - - /** - * y = 0 - */ - - final double f0 = f01 * (fastPowLoop(V[0] * dT_T + 1, 4) - 1); - V[0] = a00 * (V[1] + a11 * U[0] + hx * (pls - f0)); - - /** - * y = 1 - */ - - final double fN = fN1 * (fastPowLoop(V[N] * dT_T + 1, 4) - 1); - V[N] = a00 * (V[N - 1] + a11 * U[N] - hx * fN); - } - - @Override - public void solve(NonlinearProblem problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ExplicitNonlinearSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public Class domain() { - return NonlinearProblem.class; - } - - public NumericProperty getNonlinearPrecision() { - return derive(NONLINEAR_PRECISION, nonlinearPrecision); - } - - public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { - this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(NONLINEAR_PRECISION)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - - if(type == NONLINEAR_PRECISION) - setNonlinearPrecision(property); - else - throw new IllegalArgumentException("Property not recognised: " + property); - } - -} \ No newline at end of file + private static final long serialVersionUID = -5392648684733420360L; + private int N; + private double hx; + + private double dT_T; + + private double a00; + private double a11; + private double f01; + private double fN1; + + private double nonlinearPrecision; + + public ExplicitNonlinearSolver() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + public ExplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + public ExplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); + + var grid = getGrid(); + + N = (int) grid.getGridDensity().getValue(); + hx = grid.getXStep(); + final double tau = grid.getTimeStep(); + + var p = problem.getProperties(); + final double T = (double) p.getTestTemperature().getValue(); + final double dT = p.maximumHeating((Pulse2D) problem.getPulse()); + + a00 = 2 * tau / (hx * hx + 2 * tau); + a11 = hx * hx / (2.0 * tau); + final double Bi1 = (double) p.getHeatLoss().getValue(); + f01 = 0.25 * Bi1 * T / dT; + fN1 = 0.25 * Bi1 * T / dT; + + dT_T = dT / T; + } + + @Override + public void timeStep(int m) throws SolverException { + explicitSolution(); + doIterations(getCurrentSolution(), nonlinearPrecision, m); + } + + @Override + public void iteration(int m) { + var V = getCurrentSolution(); + var U = getPreviousSolution(); + + /** + * y = 0 + */ + final double f0 = f01 * (fastPowLoop(V[0] * dT_T + 1, 4) - 1); + V[0] = a00 * (V[1] + a11 * U[0] + hx * (getCurrentPulseValue() - f0)); + + /** + * y = 1 + */ + final double fN = fN1 * (fastPowLoop(V[N] * dT_T + 1, 4) - 1); + V[N] = a00 * (V[N - 1] + a11 * U[N] - hx * fN); + } + + @Override + public void solve(NonlinearProblem problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ExplicitNonlinearSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public Class[] domain() { + return new Class[]{NonlinearProblem.class}; + } + + public NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(def(NONLINEAR_PRECISION)); + return list; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } else { + throw new IllegalArgumentException("Property not recognised: " + property); + } + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java index 8a087e54..ac48b368 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitTranslucentSolver.java @@ -1,107 +1,101 @@ package pulse.problem.schemes.solvers; import static pulse.problem.schemes.DistributedDetection.evaluateSignal; -import static pulse.problem.statements.penetration.SpectralRange.LASER; +import static pulse.problem.statements.model.SpectralRange.LASER; import static pulse.ui.Messages.getString; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ExplicitScheme; import pulse.problem.statements.PenetrationProblem; import pulse.problem.statements.Problem; -import pulse.problem.statements.penetration.AbsorptionModel; +import pulse.problem.statements.model.AbsorptionModel; import pulse.properties.NumericProperty; public class ExplicitTranslucentSolver extends ExplicitScheme implements Solver { - private int N; - private double hx; - private double tau; - private double a; + private static final long serialVersionUID = -3693226611473383024L; + private int N; + private double hx; + private double tau; + private double a; - private double pls; + private AbsorptionModel model; - private final static double EPS = 1e-7; // a small value ensuring numeric stability + public ExplicitTranslucentSolver() { + super(); + } - private AbsorptionModel model; + public ExplicitTranslucentSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } - public ExplicitTranslucentSolver() { - super(); - } + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); - public ExplicitTranslucentSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } + var grid = getGrid(); + model = ((PenetrationProblem) problem).getAbsorptionModel(); - private void prepare(PenetrationProblem problem) { - super.prepare(problem); + N = (int) grid.getGridDensity().getValue(); + hx = grid.getXStep(); + tau = grid.getTimeStep(); - var grid = getGrid(); - model = problem.getAbsorptionModel(); + final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); + a = 1. / (1. + Bi1 * hx); + } - N = (int) grid.getGridDensity().getValue(); - hx = grid.getXStep(); - tau = grid.getTimeStep(); + @Override + public void solve(PenetrationProblem problem) throws SolverException { + this.prepare(problem); + runTimeSequence(problem); + } - final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - a = 1. / (1. + Bi1 * hx); - } - - @Override - public void solve(PenetrationProblem problem) throws SolverException { - this.prepare(problem); - runTimeSequence(problem); - } - - @Override - public void timeStep(final int m) { - pls = this.pulse(m); - - /* + @Override + public void timeStep(final int m) { + /* * Uses the heat equation explicitly to calculate the grid-function everywhere * except the boundaries - */ - explicitSolution(); + */ + explicitSolution(); - /* + /* * Calculates boundary values - */ - - var V = getCurrentSolution(); - setSolutionAt(0, V[1] * a); - setSolutionAt(N, V[N - 1] * a); - - } - - @Override - public double phi(final int i) { - return tau * pls * model.absorption(LASER, (i - EPS) * hx); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ExplicitTranslucentSolver(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("ExplicitScheme.4"); - } - - @Override - public Class domain() { - return PenetrationProblem.class; - } - - @Override - public double signal() { - return evaluateSignal(model, getGrid(), getCurrentSolution()); - } - -} \ No newline at end of file + */ + var V = getCurrentSolution(); + setSolutionAt(0, V[1] * a); + setSolutionAt(N, V[N - 1] * a); + + } + + @Override + public double phi(final int i) { + return tau * getCurrentPulseValue() * model.absorption(LASER, i * hx); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ExplicitTranslucentSolver(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("ExplicitScheme.4"); + } + + @Override + public Class[] domain() { + return new Class[]{PenetrationProblem.class}; + } + + @Override + public double signal() { + return evaluateSignal(model, getGrid(), getCurrentSolution()); + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java index 0de6f0f9..a42bac2c 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolver.java @@ -6,132 +6,136 @@ import static pulse.ui.Messages.getString; import pulse.problem.schemes.CoupledImplicitScheme; -import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.TridiagonalMatrixAlgorithm; 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.ThermoOpticalProperties; +import pulse.problem.statements.Problem; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; -public class ImplicitCoupledSolver extends CoupledImplicitScheme implements Solver { +public abstract class ImplicitCoupledSolver extends CoupledImplicitScheme + implements Solver { - private RadiativeTransferSolver rte; - private Fluxes fluxes; + private RadiativeTransferSolver rte; + private Fluxes fluxes; - private double alpha1; - private int N; + private double alpha1; + private int N; - private double hx; + private double hx; - private double HX2_2TAU; - private double HX2TAU0_2NP; - private double HX_NP; + private double HX2_2TAU; + private double HX2TAU0_2NP; + private double HX_NP; - private double v1; + private double v1; + private double zeta; - public ImplicitCoupledSolver() { - super(derive(GRID_DENSITY, 20), derive(TAU_FACTOR, 0.66667)); - } + public ImplicitCoupledSolver() { + super(derive(GRID_DENSITY, 20), derive(TAU_FACTOR, 0.66667)); + } - public ImplicitCoupledSolver(NumericProperty gridDensity, NumericProperty timeFactor, NumericProperty timeLimit) { - super(gridDensity, timeFactor, timeLimit); - } + public ImplicitCoupledSolver(NumericProperty gridDensity, NumericProperty timeFactor, NumericProperty timeLimit) { + super(gridDensity, timeFactor, timeLimit); + } - private void prepare(ParticipatingMedium problem) { - super.prepare(problem); + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); - final var grid = getGrid(); + final var grid = getGrid(); - var coupling = getCoupling(); - coupling.init(problem, grid); - rte = coupling.getRadiativeTransferEquation(); + var coupling = getCoupling(); + coupling.init((ParticipatingMedium) problem, grid); + rte = coupling.getRadiativeTransferEquation(); - N = (int) getGrid().getGridDensity().getValue(); - hx = grid.getXStep(); - final double HH = hx * hx; - final double tau = grid.getTimeStep(); + N = (int) getGrid().getGridDensity().getValue(); + hx = grid.getXStep(); + final double HH = hx * hx; + final double tau = grid.getTimeStep(); - var p = (ThermoOpticalProperties)problem.getProperties(); - final double Bi1 = (double) p.getHeatLoss().getValue(); - final double Np = (double) p.getPlanckNumber().getValue(); - final double tau0 = (double) p.getOpticalThickness().getValue(); + var p = (ThermoOpticalProperties) problem.getProperties(); + //combined Biot + final double Bi1 = (double) p.getHeatLoss().getValue() + (double) p.getConvectiveLosses().getValue(); + final double Np = (double) p.getPlanckNumber().getValue(); + final double tau0 = (double) p.getOpticalThickness().getValue(); - final double TAU0_NP = tau0 / Np; - HX2_2TAU = HH / (2.0 * tau); - HX_NP = hx / Np; - HX2TAU0_2NP = 0.5 * HH * tau0 / Np; + final double TAU0_NP = tau0 / Np; + HX2_2TAU = HH / (2.0 * tau); + HX_NP = hx / Np; + HX2TAU0_2NP = 0.5 * HH * tau0 / Np; - v1 = 1.0 + HX2_2TAU + hx * Bi1; + v1 = 1.0 + HX2_2TAU + hx * Bi1; - fluxes = rte.getFluxes(); + fluxes = rte.getFluxes(); - var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { - @Override - public double phi(int i) { - return TAU0_NP * fluxes.fluxDerivative(i); - } + @Override + public double phi(int i) { + return TAU0_NP * fluxes.fluxDerivative(i); + } - }; + }; - alpha1 = 1.0 / (1.0 + Bi1 * hx + HX2_2TAU); - tridiagonal.setAlpha(1, alpha1); + alpha1 = 1.0 / (1.0 + Bi1 * hx + HX2_2TAU); + tridiagonal.setAlpha(1, alpha1); - tridiagonal.setCoefA(1. / HH); - tridiagonal.setCoefB(1. / tau + 2. / HH); - tridiagonal.setCoefC(1. / HH); + tridiagonal.setCoefA(1. / HH); + tridiagonal.setCoefB(1. / tau + 2. / HH); + tridiagonal.setCoefC(1. / HH); - tridiagonal.evaluateAlpha(); - setTridiagonalMatrixAlgorithm(tridiagonal); + tridiagonal.evaluateAlpha(); + setTridiagonalMatrixAlgorithm(tridiagonal); - } + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); + } - @Override - public void solve(ParticipatingMedium problem) throws SolverException { - this.prepare(problem); - setCalculationStatus(rte.compute(getPreviousSolution())); + @Override + public void solve(ParticipatingMedium problem) throws SolverException { + this.prepare(problem); + setCalculationStatus(rte.compute(getPreviousSolution())); - runTimeSequence(problem); + runTimeSequence(problem); - var status = getCalculationStatus(); - if (status != RTECalculationStatus.NORMAL) - throw new SolverException(status.toString()); + var status = getCalculationStatus(); + if (status != RTECalculationStatus.NORMAL) { + throw new SolverException(status.toString(), + RTE_SOLVER_ERROR); + } - } + } - @Override - public String toString() { - return getString("ImplicitScheme.4"); - } + @Override + public String toString() { + return getString("ImplicitScheme.4"); + } - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ImplicitCoupledSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - /* + @Override + public double evalRightBoundary(final double alphaN, final double betaN) { + /* * UNCOMMENT FOR A SIMPLIFIED CALCULATION * return (betaN + HX2_2TAU * getPreviousSolution()[N] + HX_2NP * (fluxes.getFlux(N - 1) + * fluxes.getFlux(N))) / (v1 - alphaN); - */ - return (betaN + HX2_2TAU * getPreviousSolution()[N] + HX_NP * fluxes.getFlux(N) - + HX2TAU0_2NP * fluxes.fluxDerivativeRear()) / (v1 - alphaN); - } - - @Override - public double firstBeta(final int m) { - /* + */ + return (betaN + HX2_2TAU * getPreviousSolution()[N] + hx * (1.0 - zeta) * getCurrentPulseValue() + + HX_NP * fluxes.getFlux(N) + + HX2TAU0_2NP * fluxes.fluxDerivativeRear()) / (v1 - alphaN); + } + + @Override + public double firstBeta() { + /* * UNCOMMENT FOR A SIMPLIFIED CALCULATION * return (HX2_2TAU * getPreviousSolution()[0] + hx * getCurrentPulseValue() - HX_2NP * * (fluxes.getFlux(0) + fluxes.getFlux(1))) * alpha1; - */ - return (HX2_2TAU * getPreviousSolution()[0] + hx * getCurrentPulseValue() - + HX2TAU0_2NP * fluxes.fluxDerivativeFront() - HX_NP * fluxes.getFlux(0)) * alpha1; - } + */ + return (HX2_2TAU * getPreviousSolution()[0] + hx * zeta * getCurrentPulseValue() + + HX2TAU0_2NP * fluxes.fluxDerivativeFront() - HX_NP * fluxes.getFlux(0)) * alpha1; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java new file mode 100644 index 00000000..af0d6e11 --- /dev/null +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitCoupledSolverNL.java @@ -0,0 +1,93 @@ +/* + * Copyright 2022 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.problem.schemes.solvers; + +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.FixedPointIterations; +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; +import pulse.ui.Messages; + +/** + * + * @author Artem Lunev + */ +public class ImplicitCoupledSolverNL extends ImplicitCoupledSolver implements FixedPointIterations { + + private static final long serialVersionUID = -3993380888844448942L; + private double nonlinearPrecision; + + public ImplicitCoupledSolverNL() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setAutoUpdateFluxes(false); + } + + public ImplicitCoupledSolverNL(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setAutoUpdateFluxes(false); + } + + @Override + public void timeStep(final int m) throws SolverException { + doIterations(getCurrentSolution(), nonlinearPrecision, m); + } + + @Override + public void iteration(final int m) throws SolverException { + super.timeStep(m); + } + + @Override + public void finaliseIteration(double[] V) throws SolverException { + FixedPointIterations.super.finaliseIteration(V); + var rte = this.getCoupling().getRadiativeTransferEquation(); + setCalculationStatus(rte.compute(V)); + } + + public final NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public final void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } else { + 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"); + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java index 729fd14a..587271b8 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitDiathermicSolver.java @@ -3,103 +3,108 @@ import pulse.problem.schemes.BlockMatrixAlgorithm; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ImplicitScheme; +import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.DiathermicMedium; import pulse.problem.statements.Problem; +import pulse.problem.statements.model.DiathermicProperties; import pulse.properties.NumericProperty; public class ImplicitDiathermicSolver extends ImplicitScheme implements Solver { - private double hx; - private int N; - - private double HX2_2TAU; - private double z0; - private double zN_1; - private double fN1; - - public ImplicitDiathermicSolver() { - super(); - } - - public ImplicitDiathermicSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } - - public ImplicitDiathermicSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } - - private void prepare(DiathermicMedium problem) { - super.prepare(problem); - - var grid = getGrid(); - - N = (int) grid.getGridDensity().getValue(); - hx = grid.getXStep(); - - final double HX2 = hx * hx; - final double tau = grid.getTimeStep(); - HX2_2TAU = HX2 / (2.0 * grid.getTimeStep()); - - /* Constants */ - - final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - final double eta = (double) problem.getDiathermicCoefficient().getValue(); - - z0 = 1.0 + HX2_2TAU + hx * Bi1 * (1.0 + eta); - zN_1 = -hx * eta * Bi1; - final double f01 = HX2_2TAU; - fN1 = f01; - - /* End of constants */ - - var tridiagonal = new BlockMatrixAlgorithm(grid); - - tridiagonal.setCoefA(1.0); - tridiagonal.setCoefB(2.0 + HX2 / tau); - tridiagonal.setCoefC(1.0); - - tridiagonal.setAlpha(1, 1.0 / z0); - tridiagonal.evaluateAlpha(); - setTridiagonalMatrixAlgorithm(tridiagonal); - - } - - @Override - public void leftBoundary(final int m) { - var tridiagonal = (BlockMatrixAlgorithm) getTridiagonalMatrixAlgorithm(); - tridiagonal.setGamma(1, -zN_1 / z0); - super.leftBoundary(m); - } - - @Override - public double firstBeta(final int m) { - return (HX2_2TAU * getPreviousSolution()[0] + hx * pulse(m)) / z0; - } - - @Override - public double evalRightBoundary(int m, double alphaN, double betaN) { - var tri = (BlockMatrixAlgorithm) getTridiagonalMatrixAlgorithm(); - var p = tri.getP(); - var q = tri.getQ(); - return (fN1 * getPreviousSolution()[N] - zN_1 * p[0] + p[N - 1]) / (z0 + zN_1 * q[0] - q[N - 1]); - } - - @Override - public void solve(DiathermicMedium problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ImplicitDiathermicSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public Class domain() { - return DiathermicMedium.class; - } - -} \ No newline at end of file + private double hx; + private int N; + + private double HX2_2TAU; + private double z0; + private double zN_1; + private double zeta; + + public ImplicitDiathermicSolver() { + super(); + } + + public ImplicitDiathermicSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + } + + public ImplicitDiathermicSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } + + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); + + var grid = getGrid(); + + N = (int) grid.getGridDensity().getValue(); + hx = grid.getXStep(); + + final double HX2 = hx * hx; + final double tau = grid.getTimeStep(); + HX2_2TAU = HX2 / (2.0 * grid.getTimeStep()); + + /* Constants */ + var properties = (DiathermicProperties) problem.getProperties(); + + final double BiR = (double) properties.getHeatLoss().getValue(); + final double BiC = (double) properties.getConvectiveLosses().getValue(); + final double eta = (double) properties.getDiathermicCoefficient().getValue(); + + z0 = 1.0 + HX2_2TAU + hx * BiR * (1.0 + eta) + hx * BiC; + zN_1 = -hx * eta * BiR; + + /* End of constants */ + var tridiagonal = new BlockMatrixAlgorithm(grid); + + tridiagonal.setCoefA(1.0); + tridiagonal.setCoefB(2.0 + HX2 / tau); + tridiagonal.setCoefC(1.0); + + tridiagonal.setAlpha(1, 1.0 / z0); + tridiagonal.evaluateAlpha(); + setTridiagonalMatrixAlgorithm(tridiagonal); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); + } + + @Override + public void leftBoundary(int m) { + var tridiagonal = (BlockMatrixAlgorithm) getTridiagonalMatrixAlgorithm(); + tridiagonal.setGamma(1, -zN_1 / z0); + super.leftBoundary(m); + } + + @Override + public double firstBeta() { + return (HX2_2TAU * getPreviousSolution()[0] + hx * zeta * getCurrentPulseValue()) / z0; + } + + @Override + public double evalRightBoundary(double alphaN, double betaN) { + var tri = (BlockMatrixAlgorithm) getTridiagonalMatrixAlgorithm(); + var p = tri.getP(); + var q = tri.getQ(); + return (HX2_2TAU * getPreviousSolution()[N] + (1.0 - zeta) * hx * getCurrentPulseValue() + - zN_1 * p[0] + p[N - 1]) + / (z0 + zN_1 * q[0] - q[N - 1]); + } + + @Override + public void solve(DiathermicMedium problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitDiathermicSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public Class[] domain() { + return new Class[]{DiathermicMedium.class}; + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java index 5db66ff8..62b5709a 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitLinearisedSolver.java @@ -11,10 +11,10 @@ /** * Performs a fully-dimensionless calculation for the {@code LinearisedProblem}. *

- * Initiates constants for calculations - * and uses a sweep method to evaluate the solution for each subsequent - * timestep, filling the {@code grid} completely at each specified spatial - * point. The heating curve is updated with the rear-side temperature + * Initiates constants for calculations and uses a sweep method to evaluate the + * solution for each subsequent timestep, filling the {@code grid} completely at + * each specified spatial point. The heating curve is updated with the rear-side + * temperature * Θ(xN,ti) (here * N is the grid density) at the end of {@code timeLimit} * intervals, which comprise of {@code timeLimit/tau} time steps. The @@ -23,7 +23,7 @@ * calculated solution (with respect to time), and {@code maxTemp} is the * {@code maximumTemperature} {@code NumericProperty} of {@code problem}. *

- * + * *

* The fully implicit scheme uses a standard 4-point template on a * one-dimensional grid that utilises the following grid-function values on each @@ -41,92 +41,95 @@ * approximation of at least O(τ + h2) for * both the heat equation and the boundary conditions. *

- * + * + * @param a subclass of ClassicalProblem * @see super.solve(Problem) */ +public class ImplicitLinearisedSolver extends ImplicitScheme + implements Solver { -public class ImplicitLinearisedSolver extends ImplicitScheme implements Solver { - - private double Bi1HTAU; - - private int N; - private double tau; - - private double HH; - private double _2HTAU; - - private final static double EPS = 1e-7; // a small value ensuring numeric stability - - public ImplicitLinearisedSolver() { - super(); - } - - public ImplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } - - public ImplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } - - @Override - public void prepare(Problem problem) { - super.prepare(problem); - - var grid = getGrid(); - - N = (int) grid.getGridDensity().getValue(); - final double hx = grid.getXStep(); - tau = grid.getTimeStep(); - - final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - - Bi1HTAU = Bi1 * hx * tau; - - // precalculated constants - - HH = hx*hx; - _2HTAU = 2. * hx * tau; - - final double alpha0 = 2. * tau / (2. * Bi1HTAU + 2. * tau + hx*hx); - final var tridiagonal = getTridiagonalMatrixAlgorithm(); - tridiagonal.setAlpha(1, alpha0); - - // coefficients for difference equation - - tridiagonal.setCoefA( 1. / pow(hx, 2) ); - tridiagonal.setCoefB( 1. / tau + 2. / pow(hx, 2) ); - tridiagonal.setCoefC( 1. / pow(hx, 2) ); - - tridiagonal.evaluateAlpha(); - } - - @Override - public void solve(ClassicalProblem problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public double firstBeta(final int m) { - final double pls = getDiscretePulse().laserPowerAt((m - EPS) * tau); // NOTE: EPS is very important here and ensures numeric stability! - return (HH * getPreviousSolution()[0] + _2HTAU * pls) / (2. * Bi1HTAU + 2. * tau + HH); - } - - @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - return (HH * getPreviousSolution()[N] + 2. * tau * betaN) / (2 * Bi1HTAU + HH - 2. * tau * (alphaN - 1)); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ImplicitLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public Class domain() { - return ClassicalProblem.class; - } - -} \ No newline at end of file + 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() { + super(); + } + + public ImplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + } + + public ImplicitLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } + + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); + + var grid = getGrid(); + + N = (int) grid.getGridDensity().getValue(); + final double hx = grid.getXStep(); + tau = grid.getTimeStep(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); + + final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); + + Bi1HTAU = Bi1 * hx * tau; + + // precalculated constants + HH = hx * hx; + _2HTAU = 2. * hx * tau; + + final double alpha0 = 2. * tau / (2. * Bi1HTAU + 2. * tau + hx * hx); + final var tridiagonal = getTridiagonalMatrixAlgorithm(); + tridiagonal.setAlpha(1, alpha0); + + // coefficients for difference equation + tridiagonal.setCoefA(1. / pow(hx, 2)); + tridiagonal.setCoefB(1. / tau + 2. / pow(hx, 2)); + tridiagonal.setCoefC(1. / pow(hx, 2)); + + tridiagonal.evaluateAlpha(); + } + + @Override + public void solve(ClassicalProblem problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public double firstBeta() { + return (HH * getPreviousSolution()[0] + _2HTAU * getCurrentPulseValue() * zeta) / (2. * Bi1HTAU + 2. * tau + HH); + } + + @Override + public double evalRightBoundary(final double alphaN, final double 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)); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public Class[] domain() { + return new Class[]{ClassicalProblem.class}; + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java index 30f71b21..20224b8a 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitNonlinearSolver.java @@ -3,9 +3,7 @@ import static pulse.math.MathUtils.fastPowLoop; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; - -import java.util.List; +import java.util.Set; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.FixedPointIterations; @@ -15,140 +13,141 @@ import pulse.problem.statements.Pulse2D; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; +import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; public class ImplicitNonlinearSolver extends ImplicitScheme implements Solver, FixedPointIterations { - private int N; - private double HH; - private double tau; - private double pls; - - private double dT_T; - - private double b1; - private double c1; - private double c2; - private double b2; - private double b3; - - private double nonlinearPrecision; - - public ImplicitNonlinearSolver() { - super(); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - public ImplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - public ImplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); - } - - private void prepare(NonlinearProblem problem) { - super.prepare(problem); - - var grid = getGrid(); - - N = (int) grid.getGridDensity().getValue(); - final double hx = grid.getXStep(); - tau = grid.getTimeStep(); - - HH = hx*hx; - - var p = problem.getProperties(); - - final double Bi1 = (double) p.getHeatLoss().getValue(); - - final double T = (double) p.getTestTemperature().getValue(); - final double dT = p.maximumHeating((Pulse2D)problem.getPulse()); - dT_T = dT/T; - - // constant for bc calc - - final double a1 = 2. * tau / (HH + 2. * tau); - b1 = HH / (2. * tau + HH); - b2 = a1 * hx; - b3 = Bi1 * T / (4.0 * dT); - c1 = -0.5 * hx * tau * Bi1 * T / dT; - - var tridiagonal = getTridiagonalMatrixAlgorithm(); - - tridiagonal.setCoefA( 1.0/HH); - tridiagonal.setCoefB( 1.0/tau + 2.0/HH); - tridiagonal.setCoefC( 1.0/HH); - - tridiagonal.setAlpha(1, a1); - tridiagonal.evaluateAlpha(); - c2 = 1. / (HH + 2. * tau - 2 * tridiagonal.getAlpha()[N] * tau); - } - - @Override - public void solve(NonlinearProblem problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ImplicitNonlinearSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public Class domain() { - return NonlinearProblem.class; - } - - public NumericProperty getNonlinearPrecision() { - return derive(NONLINEAR_PRECISION, nonlinearPrecision); - } - - public void setNonlinearPrecision(NumericProperty nonlinearPrecision) { - this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(NONLINEAR_PRECISION)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case NONLINEAR_PRECISION: - setNonlinearPrecision(property); - break; - default: - throw new IllegalArgumentException("Property not recognised: " + property); - } - } - - @Override - public void timeStep(final int m) { - pls = pulse(m); - doIterations(getCurrentSolution(), nonlinearPrecision, m); - } - - @Override - public void iteration(int m) { - super.timeStep(m); - } - - @Override - public double evalRightBoundary(int m, double alphaN, double betaN) { - return c2 * (2. * betaN * tau + HH * getPreviousSolution()[N] + c1 * (fastPowLoop(getCurrentSolution()[N] * dT_T + 1, 4) - 1)); - } - - @Override - public double firstBeta(int m) { - return b1 * getPreviousSolution()[0] + b2 * (pls - b3 * (fastPowLoop(getCurrentSolution()[0] * dT_T + 1, 4) - 1)); - } - -} \ No newline at end of file + private static final long serialVersionUID = -6263519219698662707L; + private int N; + private double HH; + private double tau; + + private double dT_T; + + private double b1; + private double c1; + private double c2; + private double b2; + private double b3; + + private double nonlinearPrecision; + + public ImplicitNonlinearSolver() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + public ImplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + public ImplicitNonlinearSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + } + + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); + + var grid = getGrid(); + + N = (int) grid.getGridDensity().getValue(); + final double hx = grid.getXStep(); + tau = grid.getTimeStep(); + + HH = hx * hx; + + var p = problem.getProperties(); + + final double Bi1 = (double) p.getHeatLoss().getValue(); + + final double T = (double) p.getTestTemperature().getValue(); + final double dT = p.maximumHeating((Pulse2D) problem.getPulse()); + dT_T = dT / T; + + // constant for bc calc + final double a1 = 2. * tau / (HH + 2. * tau); + b1 = HH / (2. * tau + HH); + b2 = a1 * hx; + b3 = Bi1 * T / (4.0 * dT); + c1 = -0.5 * hx * tau * Bi1 * T / dT; + + var tridiagonal = getTridiagonalMatrixAlgorithm(); + + tridiagonal.setCoefA(1.0 / HH); + tridiagonal.setCoefB(1.0 / tau + 2.0 / HH); + tridiagonal.setCoefC(1.0 / HH); + + tridiagonal.setAlpha(1, a1); + tridiagonal.evaluateAlpha(); + c2 = 1. / (HH + 2. * tau - 2 * tridiagonal.getAlpha()[N] * tau); + } + + @Override + public void solve(NonlinearProblem problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitNonlinearSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public Class[] domain() { + return new Class[]{NonlinearProblem.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) { + switch (type) { + case NONLINEAR_PRECISION: + setNonlinearPrecision(property); + break; + default: + throw new IllegalArgumentException("Property not recognised: " + property); + } + } + + @Override + public void timeStep(final int m) throws SolverException { + doIterations(getCurrentSolution(), nonlinearPrecision, m); + } + + @Override + public void iteration(int m) throws SolverException { + super.timeStep(m); + } + + @Override + public double evalRightBoundary(double alphaN, double betaN) { + return c2 * (2. * betaN * tau + HH * getPreviousSolution()[N] + + c1 * (fastPowLoop(getCurrentSolution()[N] * dT_T + 1, 4) - 1)); + } + + @Override + public double firstBeta() { + return b1 * getPreviousSolution()[0] + b2 * (getCurrentPulseValue() + - b3 * (fastPowLoop(getCurrentSolution()[0] * 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 068defcf..7ace1f8d 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java @@ -1,128 +1,117 @@ package pulse.problem.schemes.solvers; import static pulse.problem.schemes.DistributedDetection.evaluateSignal; -import static pulse.problem.statements.penetration.SpectralRange.LASER; +import static pulse.problem.statements.model.SpectralRange.LASER; import static pulse.ui.Messages.getString; import pulse.problem.schemes.DifferenceScheme; -import pulse.problem.schemes.Grid; import pulse.problem.schemes.ImplicitScheme; import pulse.problem.schemes.TridiagonalMatrixAlgorithm; import pulse.problem.statements.PenetrationProblem; import pulse.problem.statements.Problem; -import pulse.problem.statements.penetration.AbsorptionModel; +import pulse.problem.statements.model.AbsorptionModel; import pulse.properties.NumericProperty; public class ImplicitTranslucentSolver extends ImplicitScheme implements Solver { - private AbsorptionModel absorption; - private Grid grid; - private double pls; - private int N; - - private double HH; - private double _2Bi1HTAU; - private double b11; - - private double frontAbsorption; - private double rearAbsorption; - - public ImplicitTranslucentSolver() { - super(); - } - - public ImplicitTranslucentSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } - - private void prepare(PenetrationProblem problem) { - super.prepare(problem); - - grid = getGrid(); - final double tau = grid.getTimeStep(); - N = (int) grid.getGridDensity().getValue(); - - final double Bi1H = (double) problem.getProperties().getHeatLoss().getValue() * grid.getXStep(); - final double hx = grid.getXStep(); - HH = hx * hx; - _2Bi1HTAU = 2.0 * Bi1H * tau; - b11 = 1.0 / (1.0 + 2.0 * tau / HH * (1 + Bi1H)); - - absorption = problem.getAbsorptionModel(); - final double EPS = 1E-7; - rearAbsorption = tau * absorption.absorption(LASER, (N - EPS) * hx); - frontAbsorption = tau * absorption.absorption(LASER, 0.0); - - var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { - - @Override - public double phi(final int i) { - return pls * absorption.absorption(LASER, (i - EPS) * hx); - } - - }; - - // coefficients for difference equation - - tridiagonal.setCoefA(1. / HH); - tridiagonal.setCoefB(1. / tau + 2. / HH); - tridiagonal.setCoefC(1. / HH); - - tridiagonal.setAlpha(1, 1.0 / (1.0 + HH / (2.0 * tau) + Bi1H)); - tridiagonal.evaluateAlpha(); - setTridiagonalMatrixAlgorithm(tridiagonal); - } - - @Override - public void solve(PenetrationProblem problem) { - prepare(problem); - runTimeSequence(problem); - } - - @Override - public void timeStep(final int m) { - pls = pulse(m); - super.timeStep(m); - } - - @Override - public double signal() { - return evaluateSignal(absorption, grid, getCurrentSolution()); - } - - @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - final double tau = grid.getTimeStep(); - - return (HH * (getPreviousSolution()[N] + pls * rearAbsorption) - + 2. * tau * betaN) / (_2Bi1HTAU + HH + 2. * tau * (1 - alphaN)); - } - - @Override - public double firstBeta(int m) { - return (getPreviousSolution()[0] + pls * frontAbsorption) * b11; - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new ImplicitTranslucentSolver(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 PenetrationProblem.class; - } - -} \ No newline at end of file + private static final long serialVersionUID = -2207434474904484692L; + private AbsorptionModel absorption; + private int N; + + private double HH; + private double _2Bi1HTAU; + private double b11; + + public ImplicitTranslucentSolver() { + super(); + } + + public ImplicitTranslucentSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } + + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); + + var grid = getGrid(); + final double tau = grid.getTimeStep(); + N = (int) grid.getGridDensity().getValue(); + + final double Bi1H = (double) problem.getProperties().getHeatLoss().getValue() * grid.getXStep(); + final double hx = grid.getXStep(); + + 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 + public double phi(final int i) { + return getCurrentPulseValue() * absorption.absorption(LASER, i * hx); + } + + }; + + // coefficients for difference equation + tridiagonal.setCoefA(1. / HH); + tridiagonal.setCoefB(1. / tau + 2. / HH); + tridiagonal.setCoefC(1. / HH); + + tridiagonal.setAlpha(1, 1.0 / (1.0 + HH / (2.0 * tau) + Bi1H)); + tridiagonal.evaluateAlpha(); + setTridiagonalMatrixAlgorithm(tridiagonal); + } + + @Override + public void solve(PenetrationProblem problem) throws SolverException { + prepare(problem); + runTimeSequence(problem); + } + + @Override + public double signal() { + return evaluateSignal(absorption, getGrid(), getCurrentSolution()); + } + + @Override + 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) + + 2. * tau * betaN) / (_2Bi1HTAU + HH + 2. * tau * (1 - alphaN)); + } + + @Override + public double firstBeta() { + var tridiagonal = this.getTridiagonalMatrixAlgorithm(); + double tau = getGrid().getTimeStep(); + return (getPreviousSolution()[0] + tau * tridiagonal.phi(0)) * b11; + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new ImplicitTranslucentSolver(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[]{PenetrationProblem.class}; + } + +} 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 00000000..b31f7fd2 --- /dev/null +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTwoTemperatureSolver.java @@ -0,0 +1,227 @@ +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 static final long serialVersionUID = 7955478815933535623L; + 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/MixedCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java index d495ff4d..4ba9ea26 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolver.java @@ -7,221 +7,212 @@ import static pulse.properties.NumericPropertyKeyword.SCHEME_WEIGHT; import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; -import java.util.List; +import java.util.Set; import pulse.problem.schemes.CoupledImplicitScheme; -import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.TridiagonalMatrixAlgorithm; -import pulse.problem.schemes.rte.Fluxes; -import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.problem.schemes.rte.RadiativeTransferSolver; +import pulse.problem.statements.ClassicalProblem; import pulse.problem.statements.ParticipatingMedium; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.Problem; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; -import pulse.ui.Messages; -public class MixedCoupledSolver extends CoupledImplicitScheme implements Solver { +public abstract class MixedCoupledSolver extends CoupledImplicitScheme + implements Solver { - private RadiativeTransferSolver rte; - private Fluxes fluxes; + private RadiativeTransferSolver rte; - private int N; - private double hx; - private double tau; - private double sigma; + private int N; + private double hx; + private double tau; + private double sigma; - private final static double A = 5.0 / 6.0; - private final static double B = 1.0 / 12.0; + private final static double A = 5.0 / 6.0; + private final static double B = 1.0 / 12.0; - private final static double EPS = 1e-7; // a small value ensuring numeric stability + private final static double EPS = 1e-7; // a small value ensuring numeric stability - private double Bi1; + private double Bi1; - private double HX2; - private double HX_NP; - private double TAU0_NP; - private double ONE_PLUS_Bi1_HX; - private double SIGMA_NP; - - private double _2TAUHX; - private double HX2_2TAU; - private double ONE_MINUS_SIGMA_NP; - private double _2TAU_ONE_MINUS_SIGMA; - private double BETA1_FACTOR; - private double ONE_MINUS_SIGMA; - - public MixedCoupledSolver() { - super(derive(GRID_DENSITY, 16), derive(TAU_FACTOR, 0.25)); - sigma = (double) def(SCHEME_WEIGHT).getValue(); - } - - public MixedCoupledSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - sigma = (double) def(SCHEME_WEIGHT).getValue(); - } - - private void prepare(ParticipatingMedium problem) { - super.prepare(problem); - - var grid = getGrid(); - - var coupling = getCoupling(); - coupling.init(problem, grid); - rte = coupling.getRadiativeTransferEquation(); - - var U = getPreviousSolution(); - - N = (int) grid.getGridDensity().getValue(); - hx = grid.getXStep(); - tau = grid.getTimeStep(); - - Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - - fluxes = rte.getFluxes(); - - var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { - - @Override - public double phi(int i) { - return A * fluxes.meanFluxDerivative(i) - + B * (fluxes.meanFluxDerivative(i - 1) + fluxes.meanFluxDerivative(i + 1)); - } - - @Override - public double beta(final double f, final double phi, final int i) { - 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) { - final double phiSecond = A * fluxes.meanFluxDerivative(1) - + B * (fluxes.meanFluxDerivativeFront() + fluxes.meanFluxDerivative(2)); - setBeta(2, beta(U[1] / tau, phiSecond, 2)); - - super.evaluateBeta(U, 3, N); - - final double phiLast = A * fluxes.meanFluxDerivative(N - 1) - + B * (fluxes.meanFluxDerivative(N - 2) + fluxes.meanFluxDerivativeRear()); - setBeta(N, beta(U[N - 1] / tau, phiLast, N)); - - } - - }; - setTridiagonalMatrixAlgorithm(tridiagonal); - } - - private void initConst(ParticipatingMedium problem) { - var p = (ThermoOpticalProperties)problem.getProperties(); - final double Np = (double) p.getPlanckNumber().getValue(); - final double opticalThickness = (double) p.getOpticalThickness().getValue(); - - HX2 = hx * hx; - adjustSchemeWeight(); - - ONE_MINUS_SIGMA = 1.0 - sigma; - TAU0_NP = opticalThickness / Np; - - final double Bi2HX = Bi1 * hx; - ONE_PLUS_Bi1_HX = 1. + Bi2HX; - - _2TAUHX = 2.0 * tau * hx; - HX2_2TAU = HX2 / (2.0 * tau); - ONE_MINUS_SIGMA_NP = ONE_MINUS_SIGMA / Np; - _2TAU_ONE_MINUS_SIGMA = 2.0 * tau * ONE_MINUS_SIGMA; - BETA1_FACTOR = 1.0 / (HX2 + 2.0 * tau * sigma * ONE_PLUS_Bi1_HX); - SIGMA_NP = sigma / Np; - HX_NP = hx / Np; - - final double sigma_HX2 = sigma / HX2; - var tridiagonal = getTridiagonalMatrixAlgorithm(); - tridiagonal.setCoefA(sigma_HX2); - tridiagonal.setCoefB(1. / tau + 2. * sigma_HX2); - tridiagonal.setCoefC(sigma_HX2); - final double alpha0 = 1.0 / (HX2_2TAU / sigma + ONE_PLUS_Bi1_HX); - tridiagonal.setAlpha(1, alpha0); - tridiagonal.evaluateAlpha(); - } - - @Override - public void solve(ParticipatingMedium problem) throws SolverException { - this.prepare(problem); - initConst(problem); - - setCalculationStatus(rte.compute(getPreviousSolution())); - this.runTimeSequence(problem); - - var status = getCalculationStatus(); - if (status != RTECalculationStatus.NORMAL) - throw new SolverException(status.toString()); - - } - - @Override - public double pulse(final int m) { - var pulse = getDiscretePulse(); - return (pulse.laserPowerAt((m - 1 + EPS) * tau) * ONE_MINUS_SIGMA - + pulse.laserPowerAt((m - EPS) * tau) * sigma); - } - - @Override - public double firstBeta(final int m) { - var U = getPreviousSolution(); - final double phi = TAU0_NP * fluxes.fluxDerivativeFront(); - return (_2TAUHX - * (getCurrentPulseValue() - 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; - } - - @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - final double phi = TAU0_NP * fluxes.fluxDerivativeRear(); - final var U = getPreviousSolution(); - return (sigma * betaN + HX2_2TAU * U[N] + 0.5 * HX2 * phi - + ONE_MINUS_SIGMA * (U[N - 1] - U[N] * ONE_PLUS_Bi1_HX) - + HX_NP * (sigma * fluxes.getFlux(N) + ONE_MINUS_SIGMA * fluxes.getStoredFlux(N))) - / (HX2_2TAU + sigma * (ONE_PLUS_Bi1_HX - alphaN)); - } - - private void adjustSchemeWeight() { - final double newSigma = 0.5 - HX2 / (12.0 * tau); - setWeight(derive(SCHEME_WEIGHT, newSigma > 0 ? newSigma : 0.5)); - } - - public void setWeight(NumericProperty weight) { - requireType(weight, SCHEME_WEIGHT); - this.sigma = (double) weight.getValue(); - } - - public NumericProperty getWeight() { - return derive(SCHEME_WEIGHT, sigma); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(SCHEME_WEIGHT)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == SCHEME_WEIGHT) - setWeight(property); - else - super.set(type, property); - } - - @Override - public String toString() { - return Messages.getString("MixedScheme2.4"); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new MixedCoupledSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - -} \ No newline at end of file + private double HX2; + private double HX_NP; + private double TAU0_NP; + private double ONE_PLUS_Bi1_HX; + private double SIGMA_NP; + + private double _2TAUHX; + private double HX2_2TAU; + private double ONE_MINUS_SIGMA_NP; + private double _2TAU_ONE_MINUS_SIGMA; + private double BETA1_FACTOR; + private double ONE_MINUS_SIGMA; + private double zeta; + + public MixedCoupledSolver() { + super(derive(GRID_DENSITY, 16), derive(TAU_FACTOR, 0.25)); + sigma = (double) def(SCHEME_WEIGHT).getValue(); + } + + public MixedCoupledSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + sigma = (double) def(SCHEME_WEIGHT).getValue(); + } + + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); + + var grid = getGrid(); + + var coupling = getCoupling(); + coupling.init((ParticipatingMedium) problem, grid); + rte = coupling.getRadiativeTransferEquation(); + + N = (int) grid.getGridDensity().getValue(); + hx = grid.getXStep(); + tau = grid.getTimeStep(); + + var properties = (ThermoOpticalProperties) problem.getProperties(); + //combined biot + Bi1 = (double) properties.getHeatLoss().getValue() + + (double) properties.getConvectiveLosses().getValue(); + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); + + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { + + @Override + public double phi(int i) { + var fluxes = rte.getFluxes(); + return A * fluxes.meanFluxDerivative(i) + + B * (fluxes.meanFluxDerivative(i - 1) + fluxes.meanFluxDerivative(i + 1)); + } + + @Override + 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(); + final double phiSecond = A * fluxes.meanFluxDerivative(1) + + B * (fluxes.meanFluxDerivativeFront() + fluxes.meanFluxDerivative(2)); + setBeta(2, beta(U[1] / tau, phiSecond, 2)); + + super.evaluateBeta(U, 3, N); + + final double phiLast = A * fluxes.meanFluxDerivative(N - 1) + + B * (fluxes.meanFluxDerivative(N - 2) + fluxes.meanFluxDerivativeRear()); + setBeta(N, beta(U[N - 1] / tau, phiLast, N)); + + } + + }; + setTridiagonalMatrixAlgorithm(tridiagonal); + } + + private void initConst(ClassicalProblem problem) { + var p = (ThermoOpticalProperties) problem.getProperties(); + final double Np = (double) p.getPlanckNumber().getValue(); + final double opticalThickness = (double) p.getOpticalThickness().getValue(); + + HX2 = hx * hx; + adjustSchemeWeight(); + + ONE_MINUS_SIGMA = 1.0 - sigma; + TAU0_NP = opticalThickness / Np; + + final double Bi2HX = Bi1 * hx; + ONE_PLUS_Bi1_HX = 1. + Bi2HX; + + _2TAUHX = 2.0 * tau * hx; + HX2_2TAU = HX2 / (2.0 * tau); + ONE_MINUS_SIGMA_NP = ONE_MINUS_SIGMA / Np; + _2TAU_ONE_MINUS_SIGMA = 2.0 * tau * ONE_MINUS_SIGMA; + BETA1_FACTOR = 1.0 / (HX2 + 2.0 * tau * sigma * ONE_PLUS_Bi1_HX); + SIGMA_NP = sigma / Np; + HX_NP = hx / Np; + + final double sigma_HX2 = sigma / HX2; + var tridiagonal = getTridiagonalMatrixAlgorithm(); + tridiagonal.setCoefA(sigma_HX2); + tridiagonal.setCoefB(1. / tau + 2. * sigma_HX2); + tridiagonal.setCoefC(sigma_HX2); + final double alpha0 = 1.0 / (HX2_2TAU / sigma + ONE_PLUS_Bi1_HX); + tridiagonal.setAlpha(1, alpha0); + tridiagonal.evaluateAlpha(); + } + + @Override + public void solve(ParticipatingMedium problem) throws SolverException { + this.prepare(problem); + initConst(problem); + this.runTimeSequence(problem); + } + + @Override + public double pulse(final int m) { + //todo + var pulse = getDiscretePulse(); + return (pulse.laserPowerAt((m - 1 + EPS) * tau) * ONE_MINUS_SIGMA + + pulse.laserPowerAt((m - EPS) * tau) * sigma); + } + + @Override + public double firstBeta() { + var fluxes = rte.getFluxes(); + var U = getPreviousSolution(); + final double phi = TAU0_NP * fluxes.fluxDerivativeFront(); + return (_2TAUHX + * (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; + } + + @Override + public double evalRightBoundary(final double alphaN, final double betaN) { + var fluxes = rte.getFluxes(); + final double phi = TAU0_NP * fluxes.fluxDerivativeRear(); + final var U = getPreviousSolution(); + return (sigma * betaN + hx * getCurrentPulseValue() * (1.0 - zeta) + HX2_2TAU * U[N] + 0.5 * HX2 * phi + + ONE_MINUS_SIGMA * (U[N - 1] - U[N] * ONE_PLUS_Bi1_HX) + + HX_NP * (sigma * fluxes.getFlux(N) + ONE_MINUS_SIGMA * fluxes.getStoredFlux(N))) + / (HX2_2TAU + sigma * (ONE_PLUS_Bi1_HX - alphaN)); + } + + private void adjustSchemeWeight() { + final double newSigma = 0.5 - HX2 / (12.0 * tau); + setWeight(derive(SCHEME_WEIGHT, newSigma > 0 ? newSigma : 0.5)); + } + + public void setWeight(NumericProperty weight) { + requireType(weight, SCHEME_WEIGHT); + this.sigma = (double) weight.getValue(); + } + + public NumericProperty getWeight() { + return derive(SCHEME_WEIGHT, sigma); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SCHEME_WEIGHT); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == SCHEME_WEIGHT) { + setWeight(property); + } else { + super.set(type, property); + } + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java new file mode 100644 index 00000000..94ad40d9 --- /dev/null +++ b/src/main/java/pulse/problem/schemes/solvers/MixedCoupledSolverNL.java @@ -0,0 +1,93 @@ +/* + * Copyright 2022 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.problem.schemes.solvers; + +import pulse.problem.schemes.DifferenceScheme; +import pulse.problem.schemes.FixedPointIterations; +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; +import pulse.ui.Messages; + +/** + * + * @author Artem Lunev + */ +public class MixedCoupledSolverNL extends MixedCoupledSolver implements FixedPointIterations { + + private static final long serialVersionUID = -8344384560376683594L; + private double nonlinearPrecision; + + public MixedCoupledSolverNL() { + super(); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setAutoUpdateFluxes(false); + } + + public MixedCoupledSolverNL(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + nonlinearPrecision = (double) def(NONLINEAR_PRECISION).getValue(); + setAutoUpdateFluxes(false); + } + + @Override + public void timeStep(final int m) throws SolverException { + doIterations(getCurrentSolution(), nonlinearPrecision, m); + } + + @Override + public void iteration(final int m) throws SolverException { + super.timeStep(m); + } + + @Override + public void finaliseIteration(double[] V) throws SolverException { + FixedPointIterations.super.finaliseIteration(V); + var rte = this.getCoupling().getRadiativeTransferEquation(); + setCalculationStatus(rte.compute(V)); + } + + public final NumericProperty getNonlinearPrecision() { + return derive(NONLINEAR_PRECISION, nonlinearPrecision); + } + + public final void setNonlinearPrecision(NumericProperty nonlinearPrecision) { + this.nonlinearPrecision = (double) nonlinearPrecision.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NONLINEAR_PRECISION) { + setNonlinearPrecision(property); + } else { + 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"); + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java index c8136160..e2fc25c2 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java @@ -24,7 +24,7 @@ * calculated solution (with respect to time), and {@code maxTemp} is the * {@code maximumTemperature} {@code NumericProperty} of {@code problem}. *

- * + * *

* The semi-implicit scheme uses a 6-point template on a one-dimensional grid * that utilises the following grid-function values on each step: @@ -45,117 +45,123 @@ * pulse term in the boundary condition, a higher error is introduced into the * calculation than for the implicit scheme. *

- * + * * @see super.solve(Problem) */ - public class MixedLinearisedSolver extends MixedScheme implements Solver { - private double b1; - private double b2; - private double b3; - private double c1; - private double c2; - - private final static double EPS = 1e-7; // a small value ensuring numeric stability - - public MixedLinearisedSolver() { - super(); - } - - public MixedLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { - super(N, timeFactor); - } - - public MixedLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { - super(N, timeFactor, timeLimit); - } - - @Override - public void prepare(Problem problem) { - super.prepare(problem); - - var grid = getGrid(); - - final double hx = grid.getXStep(); - final double tau = grid.getTimeStep(); - - final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - - // precalculated constants - - final double HH = pow(hx, 2); - final double Bi1HTAU = Bi1 * hx * tau; - - // constant for boundary-conditions calculation - - b1 = 1. / (Bi1HTAU + HH + tau); - b2 = -hx * (Bi1 * tau - hx); - b3 = hx * tau; - c1 = b2; - c2 = Bi1HTAU + HH; - - var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { - - @Override - public double phi(int i) { - final var U = getPreviousSolution(); - return U[i] / tau + (U[i + 1] - 2. * U[i] + U[i - 1]) / HH; - } - - }; - - setTridiagonalMatrixAlgorithm(tridiagonal); - - final double a1 = tau / (Bi1HTAU + HH + tau); - tridiagonal.setAlpha(1, a1); - - // coefficients for the finite-difference heat equation - - tridiagonal.setCoefA( 1. / pow(hx, 2) ); - tridiagonal.setCoefB( 2. / tau + 2. / pow(hx, 2) ); - tridiagonal.setCoefC( 1. / pow(hx, 2) ); - - tridiagonal.evaluateAlpha(); - - } - - @Override - public double evalRightBoundary(final int m, final double alphaN, final double betaN) { - final var U = getPreviousSolution(); - - final var grid = getGrid(); - final double tau = grid.getTimeStep(); - final int N = (int)grid.getGridDensity().getValue(); - - return (c1 * U[N] + tau * betaN - tau * (U[N] - U[N - 1])) / (c2 - tau * (alphaN - 1)); - } - - @Override - public double firstBeta(final int m) { - final double tau = getGrid().getTimeStep(); - final var pulse = getDiscretePulse(); - final double pls = pulse.laserPowerAt((m - 1 + EPS) * tau) + pulse.laserPowerAt((m - EPS) * tau); - - final var U = getPreviousSolution(); - return b1 * (b2 * U[0] + b3 * pls - tau * (U[0] - U[1])); - } - - @Override - public void solve(ClassicalProblem problem) { - this.prepare(problem); - runTimeSequence(problem); - } - - @Override - public DifferenceScheme copy() { - var grid = getGrid(); - return new MixedLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); - } - - @Override - public Class domain() { - return ClassicalProblem.class; - } - -} \ No newline at end of file + 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 + + public MixedLinearisedSolver() { + super(); + } + + public MixedLinearisedSolver(NumericProperty N, NumericProperty timeFactor) { + super(N, timeFactor); + } + + public MixedLinearisedSolver(NumericProperty N, NumericProperty timeFactor, NumericProperty timeLimit) { + super(N, timeFactor, timeLimit); + } + + @Override + public void prepare(Problem problem) throws SolverException { + super.prepare(problem); + + var grid = getGrid(); + + final double hx = grid.getXStep(); + final double tau = grid.getTimeStep(); + + final double Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); + + // precalculated constants + final double HH = pow(hx, 2); + final double Bi1HTAU = Bi1 * hx * tau; + + // constant for boundary-conditions calculation + b1 = 1. / (Bi1HTAU + HH + tau); + b2 = -hx * (Bi1 * tau - hx); + b3 = hx * tau; + c1 = b2; + c2 = Bi1HTAU + HH; + + zeta = (double) ((ClassicalProblem) problem).getGeometricFactor().getValue(); + + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { + + @Override + public double phi(int i) { + final var U = getPreviousSolution(); + return U[i] / tau + (U[i + 1] - 2. * U[i] + U[i - 1]) / HH; + } + + }; + + setTridiagonalMatrixAlgorithm(tridiagonal); + + final double a1 = tau / (Bi1HTAU + HH + tau); + tridiagonal.setAlpha(1, a1); + + // coefficients for the finite-difference heat equation + tridiagonal.setCoefA(1. / pow(hx, 2)); + tridiagonal.setCoefB(2. / tau + 2. / pow(hx, 2)); + tridiagonal.setCoefC(1. / pow(hx, 2)); + + tridiagonal.evaluateAlpha(); + + } + + @Override + public double evalRightBoundary(final double alphaN, final double betaN) { + final var U = getPreviousSolution(); + + final var grid = getGrid(); + 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)); + } + + @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); + } + + @Override + public double firstBeta() { + final double tau = getGrid().getTimeStep(); + final var U = getPreviousSolution(); + return b1 * (b2 * U[0] + b3 * zeta * getCurrentPulseValue() - tau * (U[0] - U[1])); + } + + @Override + public void solve(ClassicalProblem problem) throws SolverException { + this.prepare(problem); + runTimeSequence(problem); + } + + @Override + public DifferenceScheme copy() { + var grid = getGrid(); + return new MixedLinearisedSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); + } + + @Override + public Class[] domain() { + return new Class[]{ClassicalProblem.class}; + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/Solver.java b/src/main/java/pulse/problem/schemes/solvers/Solver.java index 535710c0..654f8970 100644 --- a/src/main/java/pulse/problem/schemes/solvers/Solver.java +++ b/src/main/java/pulse/problem/schemes/solvers/Solver.java @@ -1,25 +1,24 @@ package pulse.problem.schemes.solvers; +import java.io.Serializable; import pulse.problem.statements.Problem; /** * A solver interface which provides the capability to use the {@code solve} * method on a {@code Problem}. This interface is implemented by the subclasses * of {@code DifferenceSCheme}. - * + * * @param an instance of Problem */ +public interface Solver extends Serializable { -public interface Solver { + /** + * Calculates the solution of the {@code t} and stores it in the respective + * {@code HeatingCurve}. + * + * @param problem - an accepted instance of {@code T} + * @throws SolverException + */ + public void solve(T problem) throws SolverException; - /** - * Calculates the solution of the {@code t} and stores it in the respective - * {@code HeatingCurve}. - * - * @param problem - an accepted instance of {@code T} - * @throws SolverException - */ - - public void solve(T problem) throws SolverException; - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/schemes/solvers/SolverException.java b/src/main/java/pulse/problem/schemes/solvers/SolverException.java index 745dfd1d..ef97e328 100644 --- a/src/main/java/pulse/problem/schemes/solvers/SolverException.java +++ b/src/main/java/pulse/problem/schemes/solvers/SolverException.java @@ -3,8 +3,27 @@ @SuppressWarnings("serial") public class SolverException extends Exception { - public SolverException(String status) { - super(status); - } + private final SolverExceptionType type; -} \ No newline at end of file + 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, + } + +} diff --git a/src/main/java/pulse/problem/schemes/solvers/package-info.java b/src/main/java/pulse/problem/schemes/solvers/package-info.java index 5e2ab6bc..965635b9 100644 --- a/src/main/java/pulse/problem/schemes/solvers/package-info.java +++ b/src/main/java/pulse/problem/schemes/solvers/package-info.java @@ -1,7 +1,6 @@ /** * Contains various finite-difference solvers for the different problem statements available. - * Each solver is tailored to a specific problem statement and the solution is highly + * Each solver is tailored to a specific problem statement and the solution is highly * hard-coded. */ - -package pulse.problem.schemes.solvers; \ No newline at end of file +package pulse.problem.schemes.solvers; diff --git a/src/main/java/pulse/problem/statements/AdiabaticSolution.java b/src/main/java/pulse/problem/statements/AdiabaticSolution.java index 7e21603c..84293749 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; @@ -7,103 +8,106 @@ import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import pulse.HeatingCurve; - -public class AdiabaticSolution { - - public final static int DEFAULT_CLASSIC_PRECISION = 200; - public final static int DEFAULT_POINTS = 100; - - private AdiabaticSolution() { - //do nothing - } - - /** - * A static factory method for calculating a heating curve based on the - * analytical solution of Parker et al. - *

- * The math itself is done separately in the {@code Problem} class. This method - * creates a {@code HeatingCurve} with the number of points equal to that of the - * {@code p.getHeatingCurve()}, and with the same baseline. The solution is - * calculated for the time range {@code 0 <= t <= timeLimit}. - *

- * - * @param p The problem statement, providing access to the - * {@code classicSolutionAt} method and to the - * {@code HeatingCurve} object it owns. - * @param timeLimit The upper time limit (in seconds) - * @param precision The second argument passed to the {@code classicSolutionAt} - * @return a {@code HeatingCurve} representing the analytical solution. - * @see Parker et al. Journal - * of Applied Physics 32 (1961) 1679 - * @see Problem.classicSolutionAt(double,int) - */ - - public static HeatingCurve classicSolution(Problem p, double timeLimit, int precision) { - final int points = DEFAULT_POINTS; - var classicCurve = new HeatingCurve(derive(NUMPOINTS, points)); - - final double step = timeLimit / (points - 1.0); - var prop = p.getProperties(); - - for (int i = 1; i < points; i++) - classicCurve.addPoint(i * step, solutionAt(prop, i * step, precision)); - - classicCurve.apply(p.getBaseline()); - classicCurve.setName("Adiabatic Solution"); - - return classicCurve; - } - - /** - *

- * Calculates the classic analytical solution - * T(x=l,time) of Parker et al. at the - * specified {@code time} using the first {@code n = precision} terms of the - * solution series. The results is then scaled by a factor of - * {@code signalHeight} and returned. - *

- * - * @param time The calculation time - * @param precision The number of terms in the approximated solution - * @return a double, representing T(x=l,time) - * @see Parker et al. Journal - * of Applied Physics 32 (1961) 1679 - */ - - private final static double solutionAt(ThermalProperties p, double time, int precision) { - - final double EPS = 1E-8; - final double Fo = time / p.timeFactor(); - - if (time < EPS) - return 0; - - double sum = 0; - - for (int i = 1; i <= precision; i++) { - sum += pow(-1, i) * exp(-pow(i * PI, 2) * Fo); - } - - return (1. + 2. * sum) * (double) p.getMaximumTemperature().getValue(); - - } - - /** - * Calculates the classic solution, using the default value of the - * {@code precision} and the time limit specified by the {@code HeatingCurve} of - * {@code p}. - * - * @param p the problem statement - * @return a {@code HeatinCurve}, representing the classic solution. - * @see classicSolution - */ - - public static HeatingCurve classicSolution(Problem p) { - return classicSolution(p, p.getHeatingCurve().timeLimit(), DEFAULT_CLASSIC_PRECISION); - } - - public static HeatingCurve classicSolution(Problem p, double timeLimit) { - return classicSolution(p, timeLimit, DEFAULT_CLASSIC_PRECISION); - } - -} \ No newline at end of file +import pulse.problem.statements.model.ThermalProperties; + +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; + + private AdiabaticSolution() { + //do nothing + } + + /** + * A static factory method for calculating a heating curve based on the + * analytical solution of Parker et al. + *

+ * The math itself is done separately in the {@code Problem} class. This + * method creates a {@code HeatingCurve} with the number of points equal to + * that of the {@code p.getHeatingCurve()}, and with the same baseline. The + * solution is calculated for the time range {@code 0 <= t <= timeLimit}. + *

+ * + * @param p The problem statement, providing access to the + * {@code classicSolutionAt} method and to the {@code HeatingCurve} object + * it owns. + * @param timeLimit The upper time limit (in seconds) + * @param precision The second argument passed to the + * {@code classicSolutionAt} + * @return a {@code HeatingCurve} representing the analytical solution. + * @see Parker et al. + * Journal of Applied Physics 32 (1961) 1679 + * @see Problem.classicSolutionAt(double,int) + */ + public static HeatingCurve classicSolution(Problem p, double timeLimit, int precision) { + final int points = DEFAULT_POINTS; + var classicCurve = new HeatingCurve(derive(NUMPOINTS, points)); + + final double step = timeLimit / (points - 1.0); + var prop = p.getProperties(); + + for (int i = 1; i < points; i++) { + classicCurve.addPoint(i * step, solutionAt(prop, i * step, precision)); + } + + classicCurve.apply(p.getBaseline()); + classicCurve.setName("Adiabatic Solution"); + + return classicCurve; + } + + /** + *

+ * Calculates the classic analytical solution + * T(x=l,time) of Parker et al. at the + * specified {@code time} using the first {@code n = precision} terms of the + * solution series. The results is then scaled by a factor of + * {@code signalHeight} and returned. + *

+ * + * @param time The calculation time + * @param precision The number of terms in the approximated solution + * @return a double, representing + * T(x=l,time) + * @see Parker et al. + * Journal of Applied Physics 32 (1961) 1679 + */ + private final static double solutionAt(ThermalProperties p, double time, int precision) { + + final double EPS = 1E-8; + final double Fo = time / p.characteristicTime(); + + if (time < EPS) { + return 0; + } + + double sum = 0; + + for (int i = 1; i <= precision; i++) { + sum += pow(-1, i) * exp(-pow(i * PI, 2) * Fo); + } + + return (1. + 2. * sum) * (double) p.getMaximumTemperature().getValue(); + + } + + /** + * Calculates the classic solution, using the default value of the + * {@code precision} and the time limit specified by the + * {@code HeatingCurve} of {@code p}. + * + * @param p the problem statement + * @return a {@code HeatinCurve}, representing the classic solution. + * @see classicSolution + */ + public static HeatingCurve classicSolution(Problem p) { + return classicSolution(p, p.getHeatingCurve().timeLimit(), DEFAULT_CLASSIC_PRECISION); + } + + public static HeatingCurve classicSolution(Problem p, double timeLimit) { + return classicSolution(p, timeLimit, DEFAULT_CLASSIC_PRECISION); + } + +} diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem.java b/src/main/java/pulse/problem/statements/ClassicalProblem.java index d079fb6f..2f5fd36a 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem.java @@ -1,7 +1,20 @@ package pulse.problem.statements; +import java.util.Set; +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.solvers.ImplicitLinearisedSolver; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.statements.model.ThermalProperties; +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.SOURCE_GEOMETRIC_FACTOR; import pulse.ui.Messages; /** @@ -9,42 +22,114 @@ * formulated in the dimensionless form and with linearised boundary conditions. * */ - public class ClassicalProblem extends Problem { - public ClassicalProblem() { - super(); - setPulse(new Pulse()); - } - - public ClassicalProblem(Problem lp) { - super(lp); - setPulse(new Pulse(lp.getPulse())); - } - - @Override - public Class defaultScheme() { - return ImplicitLinearisedSolver.class; - } - - @Override - public void initProperties() { - setProperties(new ThermalProperties()); - } - - @Override - public void initProperties(ThermalProperties properties) { - setProperties(new ThermalProperties(properties)); - } - - @Override - public String toString() { - return Messages.getString("LinearizedProblem.Descriptor"); - } - - @Override - public boolean isReady() { - return true; - } - -} \ No newline at end of file + /** + * + */ + private static final long serialVersionUID = -1915004757733565502L; + private double bias; + + public ClassicalProblem() { + super(); + bias = (double) def(SOURCE_GEOMETRIC_FACTOR).getValue(); + setPulse(new Pulse()); + } + + public ClassicalProblem(Problem p) { + super(p); + bias = (double) def(SOURCE_GEOMETRIC_FACTOR).getValue(); + setPulse(new Pulse(p.getPulse())); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SOURCE_GEOMETRIC_FACTOR); + return set; + } + + @Override + public Class defaultScheme() { + return ImplicitLinearisedSolver.class; + } + + @Override + public void initProperties() { + setProperties(new ThermalProperties()); + } + + @Override + public void initProperties(ThermalProperties properties) { + setProperties(new ThermalProperties(properties)); + } + + @Override + public String toString() { + return Messages.getString("LinearizedProblem.Descriptor"); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public Problem copy() { + return new ClassicalProblem(this); + } + + public NumericProperty getGeometricFactor() { + return derive(SOURCE_GEOMETRIC_FACTOR, bias); + } + + public void setGeometricFactor(NumericProperty bias) { + requireType(bias, SOURCE_GEOMETRIC_FACTOR); + this.bias = (double) bias.getValue(); + firePropertyChanged(this, bias); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + super.set(type, value); + if (type == SOURCE_GEOMETRIC_FACTOR) { + setGeometricFactor(value); + } + } + + @Override + public void optimisationVector(ParameterVector output) { + + super.optimisationVector(output); + + for (Parameter p : output.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + + if (key == SOURCE_GEOMETRIC_FACTOR) { + var bounds = Segment.boundsFrom(SOURCE_GEOMETRIC_FACTOR); + p.setTransform(new StickTransform(bounds)); + p.setValue(bias); + } + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + for (Parameter p : params.getParameters()) { + + 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 49cd8563..3ea99b1d 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem2D.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem2D.java @@ -1,24 +1,25 @@ package pulse.problem.statements; -import static java.lang.Math.exp; -import static java.lang.Math.log; -import static java.lang.Math.tanh; -import static pulse.math.MathUtils.atanh; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_SIDE; import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; -import java.util.List; +import pulse.math.Parameter; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.InvDiamTransform; +import pulse.math.transforms.StickTransform; +import pulse.math.transforms.Transformable; import pulse.problem.laser.DiscretePulse; import pulse.problem.laser.DiscretePulse2D; import pulse.problem.schemes.ADIScheme; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; import pulse.problem.schemes.Grid2D; -import pulse.properties.Flag; -import pulse.properties.NumericPropertyKeyword; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.statements.model.ExtendedThermalProperties; +import pulse.problem.statements.model.ThermalProperties; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_SIDE; import pulse.ui.Messages; /** @@ -27,117 +28,126 @@ * pulse-to-diameter ratio. * */ - -public class ClassicalProblem2D extends Problem { - - public ClassicalProblem2D() { - super(); - setPulse( new Pulse2D() ); - setComplexity(ProblemComplexity.MODERATE); - } - - public ClassicalProblem2D(Problem lp2) { - super(lp2); - setPulse( new Pulse2D(lp2.getPulse()) ); - setComplexity(ProblemComplexity.MODERATE); - } - - @Override - public void initProperties() { - setProperties( new ExtendedThermalProperties() ); - } - - @Override - public void initProperties(ThermalProperties properties) { - setProperties(new ExtendedThermalProperties(properties)); - } - - @Override - public Class defaultScheme() { - return ADIScheme.class; - } - - @Override - public String toString() { - return Messages.getString("LinearizedProblem2D.Descriptor"); //$NON-NLS-1$ - } - - @Override - public DiscretePulse discretePulseOn(Grid grid) { - return grid instanceof Grid2D ? new DiscretePulse2D(this, (Grid2D) grid) : super.discretePulseOn(grid); - } - - @Override - public void optimisationVector(IndexedVector[] output, List flags) { - super.optimisationVector(output, flags); - var properties = (ExtendedThermalProperties) getProperties(); - - double value; - final double d = (double)properties.getSampleDiameter().getValue(); - - for (int i = 0, size = output[0].dimension(); i < size; i++) { - switch (output[0].getIndex(i)) { - case FOV_OUTER: - value = (double)properties.getFOVOuter().getValue(); - output[0].set(i, value / d); - output[1].set(i, 0.25); - break; - case FOV_INNER: - value = (double)properties.getFOVInner().getValue(); - output[0].set(i, value / d); - output[1].set(i, 0.25); - break; - case SPOT_DIAMETER : - value = (double) ((Pulse2D) getPulse()).getSpotDiameter().getValue(); - output[0].set(i, value / d); - output[1].set(i, 0.25); - break; - case HEAT_LOSS_SIDE: - final double Bi3 = (double)properties.getSideLosses().getValue(); - output[0].set(i, - properties.areThermalPropertiesLoaded() ? atanh(2.0 * Bi3 / properties.maxBiot() - 1.0) : log(Bi3)); - output[1].set(i, 2.0); - break; - default: - continue; - } - } - - } - - @Override - public void assign(IndexedVector params) { - super.assign(params); - var properties = (ExtendedThermalProperties) getProperties(); - NumericPropertyKeyword type; - - final double d = (double)properties.getSampleDiameter().getValue(); - - // TODO one-to-one mapping for FOV and SPOT_DIAMETER - for (int i = 0, size = params.dimension(); i < size; i++) { - type = params.getIndex(i); - switch (type) { - case FOV_OUTER: - case FOV_INNER: - properties.set(type, derive(type, params.get(i) * d)); - break; - case SPOT_DIAMETER: - var spotDiameter = derive(SPOT_DIAMETER, params.get(i) * d); - ((Pulse2D) getPulse()).setSpotDiameter(spotDiameter); - break; - case HEAT_LOSS_SIDE: - final double bi = properties.areThermalPropertiesLoaded() ? 0.5 * properties.maxBiot() * (tanh(params.get(i)) + 1.0) : exp(params.get(i)); - properties.setSideLosses( derive(HEAT_LOSS_SIDE, bi) ); - break; - default: - continue; - } - } - } - - @Override - public boolean isReady() { - return true; - } - -} \ No newline at end of file +public class ClassicalProblem2D extends ClassicalProblem { + + /** + * + */ + private static final long serialVersionUID = 8974995052071820422L; + + public ClassicalProblem2D() { + super(); + setPulse(new Pulse2D()); + setComplexity(ProblemComplexity.MODERATE); + } + + public ClassicalProblem2D(Problem p) { + super(p); + setPulse(new Pulse2D(p.getPulse())); + setComplexity(ProblemComplexity.MODERATE); + } + + @Override + public void initProperties() { + setProperties(new ExtendedThermalProperties()); + } + + @Override + public void initProperties(ThermalProperties properties) { + setProperties(new ExtendedThermalProperties(properties)); + } + + @Override + public Class defaultScheme() { + return ADIScheme.class; + } + + @Override + public String toString() { + return Messages.getString("LinearizedProblem2D.Descriptor"); //$NON-NLS-1$ + } + + @Override + 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); + var properties = (ExtendedThermalProperties) getProperties(); + double value; + + for (Parameter p : output.getParameters()) { + + 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(); + transform = new StickTransform(bounds); + break; + case FOV_INNER: + value = (double) properties.getFOVInner().getValue(); + break; + case SPOT_DIAMETER: + value = (double) ((Pulse2D) getPulse()).getSpotDiameter().getValue(); + transform = new StickTransform(bounds); + break; + case HEAT_LOSS_SIDE: + value = (double) properties.getSideLosses().getValue(); + transform = new StickTransform(bounds); + break; + case HEAT_LOSS_COMBINED: + value = (double) properties.getHeatLoss().getValue(); + transform = new StickTransform(bounds); + break; + default: + continue; + } + + p.setTransform(transform); + p.setBounds(bounds); + p.setValue(value); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + var properties = (ExtendedThermalProperties) getProperties(); + + // TODO one-to-one mapping for FOV and SPOT_DIAMETER + 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, p.inverseTransform())); + break; + case SPOT_DIAMETER: + ((Pulse2D) getPulse()).setSpotDiameter(derive(SPOT_DIAMETER, + p.inverseTransform())); + break; + default: + } + } + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public Problem copy() { + return new ClassicalProblem2D(this); + } + +} diff --git a/src/main/java/pulse/problem/statements/CoreShellProblem.java b/src/main/java/pulse/problem/statements/CoreShellProblem.java deleted file mode 100644 index bde9c340..00000000 --- a/src/main/java/pulse/problem/statements/CoreShellProblem.java +++ /dev/null @@ -1,171 +0,0 @@ -package pulse.problem.statements; - -import static java.lang.Math.pow; -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.AXIAL_COATING_THICKNESS; -import static pulse.properties.NumericPropertyKeyword.COATING_DIFFUSIVITY; -import static pulse.properties.NumericPropertyKeyword.RADIAL_COATING_THICKNESS; - -import java.util.ArrayList; -import java.util.List; - -import pulse.math.IndexedVector; -import pulse.properties.Flag; -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; -import pulse.ui.Messages; - -public class CoreShellProblem extends ClassicalProblem2D { - - private double tA; - private double tR; - private double coatingDiffusivity; - private final static boolean DEBUG = true; - - public CoreShellProblem() { - super(); - tA = (double) def(AXIAL_COATING_THICKNESS).getValue(); - tR = (double) def(RADIAL_COATING_THICKNESS).getValue(); - coatingDiffusivity = (double) def(COATING_DIFFUSIVITY).getValue(); - setComplexity(ProblemComplexity.HIGH); - } - - public CoreShellProblem(Problem sdd) { - super(sdd); - tA = (double) def(AXIAL_COATING_THICKNESS).getValue(); - tR = (double) def(RADIAL_COATING_THICKNESS).getValue(); - coatingDiffusivity = (double) def(COATING_DIFFUSIVITY).getValue(); - setComplexity(ProblemComplexity.HIGH); - } - - public CoreShellProblem(CoreShellProblem csp) { - super(csp); - tA = (double) csp.getCoatingAxialThickness().getValue(); - tR = (double) csp.getCoatingRadialThickness().getValue(); - coatingDiffusivity = (double) csp.getProperties().getDiffusivity().getValue(); - setComplexity(ProblemComplexity.HIGH); - } - - @Override - public String toString() { - return Messages.getString("UniformlyCoatedSample.Descriptor"); - } - - public NumericProperty getCoatingAxialThickness() { - return derive(AXIAL_COATING_THICKNESS, tA); - } - - public NumericProperty getCoatingRadialThickness() { - return derive(RADIAL_COATING_THICKNESS, tR); - } - - public double axialFactor() { - return tA / (double)getProperties().getSampleThickness().getValue(); - } - - public double radialFactor() { - return tR / (double)getProperties().getSampleThickness().getValue(); - } - - public void setCoatingAxialThickness(NumericProperty t) { - this.tA = (double) t.getValue(); - } - - public void setCoatingRadialThickness(NumericProperty t) { - this.tR = (double) t.getValue(); - } - - public NumericProperty getCoatingDiffusivity() { - return derive(COATING_DIFFUSIVITY, coatingDiffusivity); - } - - public void setCoatingDiffusivity(NumericProperty a) { - this.coatingDiffusivity = (double) a.getValue(); - } - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.addAll(super.listedTypes()); - list.add(def(AXIAL_COATING_THICKNESS)); - list.add(def(RADIAL_COATING_THICKNESS)); - list.add(def(COATING_DIFFUSIVITY)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case COATING_DIFFUSIVITY: - setCoatingDiffusivity(property); - break; - case AXIAL_COATING_THICKNESS: - setCoatingAxialThickness(property); - break; - case RADIAL_COATING_THICKNESS: - setCoatingRadialThickness(property); - break; - default: - super.set(type, property); - break; - } - } - - @Override - public boolean isEnabled() { - return !DEBUG; - } - - @Override - public void optimisationVector(IndexedVector[] output, List flags) { - super.optimisationVector(output, flags); - - for (int i = 0, size = output[0].dimension(); i < size; i++) { - switch (output[0].getIndex(i)) { - case AXIAL_COATING_THICKNESS: - output[0].set(i, tA / (double)getProperties().getSampleThickness().getValue()); - output[1].set(i, 0.01); - break; - case RADIAL_COATING_THICKNESS: - final double d = (double)((ExtendedThermalProperties)getProperties()).getSampleDiameter().getValue(); - output[0].set(i, 2.0 * tR / d); - output[1].set(i, 0.01); - break; - case COATING_DIFFUSIVITY: - double value = coatingDiffusivity / pow(tA + 2.0 * tR, -2); - output[0].set(i, value); - output[1].set(i, 0.75 * value); - break; - default: - continue; - } - } - - } - - @Override - public void assign(IndexedVector params) { - super.assign(params); - - for (int i = 0, size = params.dimension(); i < size; i++) { - switch (params.getIndex(i)) { - case AXIAL_COATING_THICKNESS: - final double l = (double)((ExtendedThermalProperties)getProperties()).getSampleThickness().getValue(); - tA = params.get(i) * l; - break; - case RADIAL_COATING_THICKNESS: - final double d = (double)((ExtendedThermalProperties)getProperties()).getSampleDiameter().getValue(); - tR = params.get(i) / (d / 2.0); - break; - case COATING_DIFFUSIVITY: - coatingDiffusivity = params.get(i) * pow(tA + 2.0 * tR, 2); - break; - default: - continue; - } - } - } - -} \ 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 26e98e37..008a54ff 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -1,22 +1,19 @@ package pulse.problem.statements; -import static java.lang.Math.tanh; -import static pulse.math.MathUtils.atanh; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; -import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; -import java.util.List; +import pulse.math.Parameter; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.StickTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitDiathermicSolver; -import pulse.properties.Flag; -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.statements.model.DiathermicProperties; +import pulse.problem.statements.model.ThermalProperties; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_CONVECTIVE; import pulse.ui.Messages; /** @@ -34,98 +31,105 @@ *

* */ - public class DiathermicMedium extends ClassicalProblem { - private double diathermicCoefficient; - private final static int DEFAULT_CURVE_POINTS = 300; - - public DiathermicMedium() { - this(def(DIATHERMIC_COEFFICIENT)); - } - - public DiathermicMedium(NumericProperty diathermicCoefficient) { - super(); - this.diathermicCoefficient = (double) (diathermicCoefficient.getValue()); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - } - - public DiathermicMedium(Problem sdd) { - super(sdd); - this.diathermicCoefficient = sdd instanceof DiathermicMedium ? ((DiathermicMedium) sdd).diathermicCoefficient - : (double) def(DIATHERMIC_COEFFICIENT).getValue(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - } - - public NumericProperty getDiathermicCoefficient() { - return derive(DIATHERMIC_COEFFICIENT, diathermicCoefficient); - } - - public void setDiathermicCoefficient(NumericProperty diathermicCoefficient) { - requireType(diathermicCoefficient, DIATHERMIC_COEFFICIENT); - this.diathermicCoefficient = (double) diathermicCoefficient.getValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == DIATHERMIC_COEFFICIENT) { - diathermicCoefficient = ((Number) property.getValue()).doubleValue(); - } else { - super.set(type, property); - } - } - - @Override - public void optimisationVector(IndexedVector[] output, List flags) { - super.optimisationVector(output, flags); - - for (int i = 0, size = output[0].dimension(); i < size; i++) { - if (output[0].getIndex(i) == DIATHERMIC_COEFFICIENT) { - output[0].set(i, atanh(2.0 * diathermicCoefficient - 1.0)); - output[1].set(i, 10.0); - } - } - - } - - @Override - public void assign(IndexedVector params) { - super.assign(params); - var properties = this.getProperties(); - - for (int i = 0, size = params.dimension(); i < size; i++) { - switch (params.getIndex(i)) { - case DIATHERMIC_COEFFICIENT: - diathermicCoefficient = 0.5 * (tanh(params.get(i)) + 1.0); - break; - case HEAT_LOSS: - if (properties.areThermalPropertiesLoaded()) { - properties.emissivity(); - final double emissivity = (double)properties.getEmissivity().getValue(); - diathermicCoefficient = emissivity / (2.0 - emissivity); - } - break; - default: - continue; - } - } - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(DIATHERMIC_COEFFICIENT)); - return list; - } - - @Override - public String toString() { - return Messages.getString("DiathermicProblem.Descriptor"); - } - - @Override - public Class defaultScheme() { - return ImplicitDiathermicSolver.class; - } - -} \ No newline at end of file + private static final long serialVersionUID = -98674255799114512L; + + public DiathermicMedium() { + super(); + } + + public DiathermicMedium(Problem p) { + super(p); + } + + @Override + public void initProperties() { + setProperties(new DiathermicProperties()); + } + + @Override + public void initProperties(ThermalProperties properties) { + setProperties(new DiathermicProperties(properties)); + } + + @Override + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + var properties = (DiathermicProperties) this.getProperties(); + + for (Parameter p : output.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + Segment bounds; + double value; + + switch (key) { + case DIATHERMIC_COEFFICIENT: + bounds = Segment.boundsFrom(DIATHERMIC_COEFFICIENT); + value = (double) properties.getDiathermicCoefficient().getValue(); + break; + case HEAT_LOSS_CONVECTIVE: + bounds = Segment.boundsFrom(HEAT_LOSS_CONVECTIVE); + 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; + } + 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 properties = (DiathermicProperties) this.getProperties(); + + for (Parameter p : params.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + + switch (key) { + + case DIATHERMIC_COEFFICIENT: + properties.setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, + p.inverseTransform())); + break; + case HEAT_LOSS_CONVECTIVE: + properties.setConvectiveLosses(derive(HEAT_LOSS_CONVECTIVE, + p.inverseTransform())); + break; + default: + } + + } + + } + + @Override + public String toString() { + return Messages.getString("DiathermicProblem.Descriptor"); + } + + @Override + public Class defaultScheme() { + return ImplicitDiathermicSolver.class; + } + + @Override + public DiathermicMedium copy() { + return new DiathermicMedium(this); + } + +} diff --git a/src/main/java/pulse/problem/statements/ExtendedThermalProperties.java b/src/main/java/pulse/problem/statements/ExtendedThermalProperties.java deleted file mode 100644 index 85eda9c8..00000000 --- a/src/main/java/pulse/problem/statements/ExtendedThermalProperties.java +++ /dev/null @@ -1,144 +0,0 @@ -package pulse.problem.statements; - -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.DIAMETER; -import static pulse.properties.NumericPropertyKeyword.FOV_INNER; -import static pulse.properties.NumericPropertyKeyword.FOV_OUTER; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_SIDE; - -import java.util.ArrayList; -import java.util.List; - -import pulse.input.ExperimentalData; -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; - -public class ExtendedThermalProperties extends ThermalProperties { - - private double d; - private double Bi3; - private double fovOuter; - private double fovInner; - - public ExtendedThermalProperties() { - super(); - Bi3 = (double) def(HEAT_LOSS_SIDE).getValue(); - d = (double) def(DIAMETER).getValue(); - fovOuter = (double) def(FOV_OUTER).getValue(); - fovInner = (double) def(FOV_INNER).getValue(); - defaultValues(); - } - - public ExtendedThermalProperties(ThermalProperties sdd) { - super(sdd); - defaultValues(); - } - - private void defaultValues() { - Bi3 = (double) def(HEAT_LOSS_SIDE).getValue(); - d = (double) def(DIAMETER).getValue(); - fovOuter = (double) def(FOV_OUTER).getValue(); - fovInner = (double) def(FOV_INNER).getValue(); - } - - public ExtendedThermalProperties(ExtendedThermalProperties sdd) { - super(sdd); - this.d = sdd.d; - this.Bi3 = sdd.Bi3; - this.fovOuter = sdd.fovOuter; - this.fovInner = sdd.fovInner; - } - - @Override - public ThermalProperties copy() { - return new ExtendedThermalProperties(this); - } - - @Override - public void useTheoreticalEstimates(ExperimentalData c) { - super.useTheoreticalEstimates(c); - if (areThermalPropertiesLoaded()) - Bi3 = biot(); - } - - public NumericProperty getSampleDiameter() { - return derive(DIAMETER, d); - } - - public void setSampleDiameter(NumericProperty d) { - requireType(d, DIAMETER); - this.d = (double) d.getValue(); - firePropertyChanged(this, d); - } - - public NumericProperty getSideLosses() { - return derive(HEAT_LOSS_SIDE, Bi3); - } - - public void setSideLosses(NumericProperty bi3) { - requireType(bi3, HEAT_LOSS_SIDE); - this.Bi3 = (double) bi3.getValue(); - firePropertyChanged(this, bi3); - } - - public NumericProperty getFOVOuter() { - return derive(FOV_OUTER, fovOuter); - } - - public void setFOVOuter(NumericProperty fovOuter) { - requireType(fovOuter, FOV_OUTER); - this.fovOuter = (double) fovOuter.getValue(); - firePropertyChanged(this, fovOuter); - } - - public NumericProperty getFOVInner() { - return derive(FOV_INNER, fovInner); - } - - public void setFOVInner(NumericProperty fovInner) { - requireType(fovInner, FOV_INNER); - this.fovInner = (double) fovInner.getValue(); - firePropertyChanged(this, fovInner); - } - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.addAll(super.listedTypes()); - list.add(def(HEAT_LOSS_SIDE)); - list.add(def(DIAMETER)); - list.add(def(FOV_OUTER)); - list.add(def(FOV_INNER)); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - switch (type) { - case FOV_OUTER: - setFOVOuter(property); - break; - case FOV_INNER: - setFOVInner(property); - break; - case DIAMETER: - setSampleDiameter(property); - break; - case HEAT_LOSS_SIDE: - setSideLosses(property); - break; - default: - break; - } - } - - @Override - public String getDescriptor() { - return "Sample Thermo-Physical Properties (2D)"; - } - -} \ 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 acb7534b..d3e01870 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -1,118 +1,141 @@ package pulse.problem.statements; -import static java.lang.Math.tanh; -import static pulse.math.MathUtils.atanh; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; import static pulse.properties.NumericPropertyKeyword.DENSITY; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; -import java.util.List; +import java.util.Set; import pulse.input.ExperimentalData; -import pulse.math.IndexedVector; +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.properties.Flag; +import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; -import pulse.properties.Property; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.LASER_ENERGY; +import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.ui.Messages; public class NonlinearProblem extends ClassicalProblem { - public NonlinearProblem() { - super(); - setPulse( new Pulse2D() ); - setComplexity(ProblemComplexity.MODERATE); - } - - public NonlinearProblem(Problem p) { - super(p); - setPulse( new Pulse2D(p.getPulse())); - setComplexity(ProblemComplexity.MODERATE); - } - - @Override - public boolean isReady() { - return getProperties().areThermalPropertiesLoaded(); - } - - @Override - public void retrieveData(ExperimentalData c) { - super.retrieveData(c); - getProperties().setTestTemperature(c.getMetadata().numericProperty(TEST_TEMPERATURE)); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(TEST_TEMPERATURE)); - list.add(def(SPECIFIC_HEAT)); - list.add(def(DENSITY)); - list.remove(def(SPOT_DIAMETER)); - return list; - } - - @Override - public String toString() { - return Messages.getString("NonlinearProblem.Descriptor"); - } - - public NumericProperty getThermalConductivity() { - return derive(CONDUCTIVITY, getProperties().thermalConductivity()); - } - - @Override - public void optimisationVector(IndexedVector[] output, List flags) { - super.optimisationVector(output, flags); - int size = output[0].dimension(); - - for (int i = 0; i < size; i++) { - - if (output[0].getIndex(i) == HEAT_LOSS) { - var properties = getProperties(); - final double Bi1 = (double)properties.getHeatLoss().getValue(); - output[0].set(i, atanh(2.0 * Bi1 / properties.maxBiot() - 1.0)); - output[1].set(i, 10.0); - } - } - - } - - /** - * Assigns parameter values of this {@code Problem} using the optimisation - * vector {@code params}. Only those parameters will be updated, the types of - * which are listed as indices in the {@code params} vector. - * - * @param params the optimisation vector, containing a similar set of parameters - * to this {@code Problem} - * @see listedTypes() - */ - - @Override - public void assign(IndexedVector params) { - super.assign(params); - var p = getProperties(); - - for (int i = 0, size = params.dimension(); i < size; i++) { - - if (params.getIndex(i) == HEAT_LOSS) { - final double heatLoss = 0.5 * p.maxBiot() * (tanh(params.get(i)) + 1.0); - p.setHeatLoss(derive(HEAT_LOSS, heatLoss)); - p.emissivity(); - } - - } - - } - - @Override - public Class defaultScheme() { - return ImplicitScheme.class; - } - -} \ No newline at end of file + /** + * + */ + private static final long serialVersionUID = -5266939533182313886L; + + public NonlinearProblem() { + super(); + setPulse(new Pulse2D()); + setComplexity(ProblemComplexity.MODERATE); + } + + public NonlinearProblem(NonlinearProblem p) { + super(p); + setPulse(new Pulse2D((Pulse2D) p.getPulse())); + } + + @Override + public boolean isReady() { + return getProperties().areThermalPropertiesLoaded(); + } + + @Override + public void retrieveData(ExperimentalData c) { + super.retrieveData(c); + getProperties().setTestTemperature(c.getMetadata().numericProperty(TEST_TEMPERATURE)); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(TEST_TEMPERATURE); + set.add(SPECIFIC_HEAT); + set.add(DENSITY); + set.remove(SPOT_DIAMETER); + set.remove(SOURCE_GEOMETRIC_FACTOR); + return set; + } + + @Override + public String toString() { + return Messages.getString("NonlinearProblem.Descriptor"); + } + + public NumericProperty getThermalConductivity() { + return derive(CONDUCTIVITY, getProperties().thermalConductivity()); + } + + /** + * + * 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); + getProperties().calculateEmissivity(); + + for (Parameter p : params.getParameters()) { + + double value = p.inverseTransform(); + NumericPropertyKeyword key = p.getIdentifier().getKeyword(); + + if (key == LASER_ENERGY) { + this.getPulse().setLaserEnergy(derive(key, value)); + } + + } + } + + /** + * + * 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) { + var bounds = Segment.boundsFrom(LASER_ENERGY); + p.setBounds(bounds); + p.setTransform(new StickTransform(bounds)); + p.setValue((double) getPulse().getLaserEnergy().getValue()); + } + + } + + } + + @Override + public Class defaultScheme() { + return ImplicitScheme.class; + } + + @Override + public Problem copy() { + return new NonlinearProblem(this); + } + +} diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index 1e6b0ef9..844c904d 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -1,127 +1,75 @@ package pulse.problem.statements; -import static java.lang.Math.exp; -import static java.lang.Math.log; -import static java.lang.Math.tanh; -import static pulse.math.MathUtils.atanh; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; +import java.util.Set; -import java.util.List; - -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.MixedCoupledSolver; -import pulse.properties.Flag; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.statements.model.ThermalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.ui.Messages; public class ParticipatingMedium extends NonlinearProblem { - private final static int DEFAULT_CURVE_POINTS = 300; - - public ParticipatingMedium() { - super(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - setComplexity(ProblemComplexity.HIGH); - } - - public ParticipatingMedium(Problem p) { - super(p); - setComplexity(ProblemComplexity.HIGH); - } - - @Override - public String toString() { - return Messages.getString("ParticipatingMedium.Descriptor"); - } - - @Override - public void optimisationVector(IndexedVector[] output, List flags) { - super.optimisationVector(output, flags); - var properties = (ThermoOpticalProperties)getProperties(); - - for (int i = 0, size = output[0].dimension(); i < size; i++) { - switch (output[0].getIndex(i)) { - case PLANCK_NUMBER: - final double planckNumber = (double)properties.getPlanckNumber().getValue(); - output[0].set(i, atanh(2.0 * planckNumber / properties.maxNp() - 1.0)); - output[1].set(i, 1.0); - break; - case OPTICAL_THICKNESS: - final double opticalThickness = (double)properties.getOpticalThickness().getValue(); - output[0].set(i, log(opticalThickness)); - output[1].set(i, 1.0); - break; - case SCATTERING_ALBEDO: - final double scatteringAlbedo = (double)properties.getScatteringAlbedo().getValue(); - output[0].set(i, atanh(2.0 * scatteringAlbedo - 1.0)); - output[1].set(i, 1.0); - break; - case SCATTERING_ANISOTROPY: - final double scatteringAnisotropy = (double)properties.getScatteringAnisostropy().getValue(); - output[0].set(i, atanh(scatteringAnisotropy)); - output[1].set(i, 1.0); - break; - default: - continue; - } - } - - } - - @Override - public void assign(IndexedVector params) { - super.assign(params); - var properties = (ThermoOpticalProperties)getProperties(); - - NumericPropertyKeyword type; - - for (int i = 0, size = params.dimension(); i < size; i++) { - type = params.getIndex(i); - switch (type) { - - case PLANCK_NUMBER: - var nP = derive(type, 0.5 * properties.maxNp() * (tanh(params.get(i)) + 1.0)); - properties.setPlanckNumber(nP); - break; - case OPTICAL_THICKNESS: - var tau0 = derive(type, exp(params.get(i))); - properties.setOpticalThickness(tau0); - break; - case SCATTERING_ALBEDO: - var omega0 = derive(type, 0.5 * (tanh(params.get(i)) + 1.0)); - properties.setScatteringAlbedo(omega0); - break; - case SCATTERING_ANISOTROPY: - var anisotropy = derive(type, tanh(params.get(i))); - properties.setScatteringAnisotropy(anisotropy); - break; - case HEAT_LOSS: - case DIFFUSIVITY: - getProperties().emissivity(); - break; - default: - break; - } - - } - - } - - @Override - public Class defaultScheme() { - return MixedCoupledSolver.class; - } - - @Override - public void initProperties(ThermalProperties properties) { - setProperties(new ThermoOpticalProperties(properties)); - } - - @Override - public void initProperties() { - setProperties( new ThermoOpticalProperties() ); - } - -} \ No newline at end of file + private static final long serialVersionUID = -8227061869299826343L; + + public ParticipatingMedium() { + super(); + setComplexity(ProblemComplexity.HIGH); + } + + public ParticipatingMedium(ParticipatingMedium p) { + super(p); + setComplexity(ProblemComplexity.HIGH); + } + + @Override + public String toString() { + return Messages.getString("ParticipatingMedium.Descriptor"); + } + + @Override + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + var properties = (ThermoOpticalProperties) getProperties(); + properties.optimisationVector(output); + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + var properties = (ThermoOpticalProperties) getProperties(); + properties.assign(params); + } + + @Override + public Class defaultScheme() { + return MixedCoupledSolver.class; + } + + @Override + public void initProperties(ThermalProperties properties) { + setProperties(new ThermoOpticalProperties(properties)); + } + + @Override + public void initProperties() { + setProperties(new ThermoOpticalProperties()); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SOURCE_GEOMETRIC_FACTOR); + return set; + } + + @Override + public Problem copy() { + return new ParticipatingMedium(this); + } + +} diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index 0a28c71b..588e2773 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -1,134 +1,101 @@ package pulse.problem.statements; -import static java.lang.Math.exp; -import static java.lang.Math.log; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.LASER_ABSORPTIVITY; -import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; -import static pulse.properties.NumericPropertyKeyword.THERMAL_ABSORPTIVITY; - import java.util.List; +import java.util.Set; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitTranslucentSolver; -import pulse.problem.statements.penetration.AbsorptionModel; -import pulse.problem.statements.penetration.BeerLambertAbsorption; -import pulse.properties.Flag; +import pulse.problem.schemes.solvers.SolverException; +import pulse.problem.statements.model.AbsorptionModel; +import pulse.problem.statements.model.BeerLambertAbsorption; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.properties.Property; import pulse.ui.Messages; import pulse.util.InstanceDescriptor; public class PenetrationProblem extends ClassicalProblem { - private final static int DEFAULT_CURVE_POINTS = 300; - - private static InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Absorption model selector", AbsorptionModel.class); - - static { - instanceDescriptor.setSelectedDescriptor(BeerLambertAbsorption.class.getSimpleName()); - } - - private AbsorptionModel absorption = instanceDescriptor.newInstance(AbsorptionModel.class); - - public PenetrationProblem() { - super(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - instanceDescriptor.addListener(() -> initAbsorption()); - absorption.setParent(this); - } - - public PenetrationProblem(Problem sdd) { - super(sdd); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - if (sdd instanceof PenetrationProblem) { - PenetrationProblem tp = (PenetrationProblem) sdd; - setAbsorptionModel(tp.absorption); - } else { - initAbsorption(); - instanceDescriptor.addListener(() -> initAbsorption()); - } - absorption.setParent(this); - } - - public PenetrationProblem(PenetrationProblem tp) { - super(tp); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); - setAbsorptionModel(tp.absorption); - } - - private void initAbsorption() { - setAbsorptionModel(instanceDescriptor.newInstance(AbsorptionModel.class)); - } - - public AbsorptionModel getAbsorptionModel() { - return absorption; - } - - public void setAbsorptionModel(AbsorptionModel model) { - this.absorption = model; - this.absorption.setParent(this); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(instanceDescriptor); - return list; - } - - public static InstanceDescriptor getAbsorptionSelector() { - return instanceDescriptor; - } - - @Override - public void optimisationVector(IndexedVector[] output, List flags) { - super.optimisationVector(output, flags); - - for (int i = 0, size = output[0].dimension(); i < size; i++) { - switch (output[0].getIndex(i)) { - case LASER_ABSORPTIVITY: - output[0].set(i, log((double) (absorption.getLaserAbsorptivity()).getValue())); - output[1].set(i, 2.0); - break; - case THERMAL_ABSORPTIVITY: - output[0].set(i, log((double) (absorption.getThermalAbsorptivity()).getValue())); - output[0].set(i, 2.0); - break; - default: - continue; - } - } - - } - - @Override - public void assign(IndexedVector params) { - super.assign(params); - - for (int i = 0, size = params.dimension(); i < size; i++) { - switch (params.getIndex(i)) { - case LASER_ABSORPTIVITY: - absorption.setLaserAbsorptivity(derive(LASER_ABSORPTIVITY, exp(params.get(i)))); - break; - case THERMAL_ABSORPTIVITY: - absorption.setThermalAbsorptivity( - derive(THERMAL_ABSORPTIVITY, exp(params.get(i)))); - break; - default: - continue; - } - } - } - - @Override - public Class defaultScheme() { - return ImplicitTranslucentSolver.class; - } - - @Override - public String toString() { - return Messages.getString("DistributedProblem.Descriptor"); - } - -} \ No newline at end of file + + private static final long serialVersionUID = -6760177658036060627L; + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor<>( + "Absorption Model Selector", 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(BeerLambertAbsorption.class.getSimpleName()); + instanceDescriptor.addListener(() -> initAbsorption()); + this.absorption = p.getAbsorptionModel().copy(); + this.absorption.setParent(this); + } + + private void initAbsorption() { + setAbsorptionModel(instanceDescriptor.newInstance(AbsorptionModel.class)); + firePropertyChanged(this, instanceDescriptor); + } + + public AbsorptionModel getAbsorptionModel() { + return absorption; + } + + public void setAbsorptionModel(AbsorptionModel model) { + this.absorption = model; + this.absorption.setParent(this); + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.remove(SOURCE_GEOMETRIC_FACTOR); + return set; + } + + public InstanceDescriptor getAbsorptionSelector() { + return instanceDescriptor; + } + + @Override + public void optimisationVector(ParameterVector output) { + super.optimisationVector(output); + absorption.optimisationVector(output); + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + absorption.assign(params); + } + + @Override + public Class defaultScheme() { + return ImplicitTranslucentSolver.class; + } + + @Override + public String toString() { + return Messages.getString("DistributedProblem.Descriptor"); + } + + @Override + public Problem copy() { + return new PenetrationProblem(this); + } + +} diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index b79b05eb..7392e27f 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -1,33 +1,39 @@ package pulse.problem.statements; -import static java.lang.Math.exp; -import static java.lang.Math.log; -import static java.lang.Math.tanh; +import java.io.Serializable; +import java.util.Arrays; import static pulse.input.listeners.CurveEventType.RESCALED; -import static pulse.math.MathUtils.atanh; -import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; -import static pulse.properties.NumericPropertyKeyword.MAXTEMP; -import static pulse.properties.NumericPropertyKeyword.THICKNESS; 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; import pulse.HeatingCurve; import pulse.baseline.Baseline; import pulse.baseline.FlatBaseline; +import pulse.baseline.LinearBaseline; import pulse.input.ExperimentalData; -import pulse.math.IndexedVector; +import pulse.math.Parameter; +import pulse.math.ParameterVector; +import pulse.math.Segment; +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.properties.Flag; +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.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; +import static pulse.properties.NumericPropertyKeyword.MAXTEMP; +import static pulse.properties.NumericPropertyKeyword.THICKNESS; import pulse.properties.Property; import pulse.search.Optimisable; import pulse.tasks.SearchTask; @@ -42,396 +48,407 @@ * Most importantly, this class sets out the procedures for reading and writing * the vector argument of the objective function for solving the optimisation * problem. - * + * * @see pulse.problem.schemes.DifferenceScheme */ - public abstract class Problem extends PropertyHolder implements Reflexive, Optimisable { - private ThermalProperties properties; - private HeatingCurve curve; - private Baseline baseline; - private Pulse pulse; - - private static boolean hideDetailedAdjustment = true; - private ProblemComplexity complexity = ProblemComplexity.LOW; - - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Baseline Selector", Baseline.class); - - /** - * Creates a {@code Problem} with default parameters (as found in the .XML - * file). - *

- * First, invokes the {@code super()} constructor of {@code PropertyHolder} to - * initialise {@code PropertyHolderListener}s, then initialises the variables - * and creates default {@code Pulse} and {@code HeatingCurve}, setting this - * object as their parent. - *

- */ - - protected Problem() { - super(); - initProperties(); - - curve = new HeatingCurve(); - curve.setParent(this); - - instanceDescriptor.attemptUpdate(FlatBaseline.class.getSimpleName()); - addListeners(); - initBaseline(); - } - - /** - * Copies all essential parameters from {@code p}, excluding the heating curve, - * which is created anew. - * - * @param p the {@code Problem} to replicate - */ - - public Problem(Problem p) { - super(); - initProperties(p.getProperties().copy()); - - this.curve = new HeatingCurve(); - this.curve.setParent(this); - this.curve.setNumPoints(p.getHeatingCurve().getNumPoints()); - - instanceDescriptor.attemptUpdate(p.getBaseline().getClass().getSimpleName()); - addListeners(); - initBaseline(); - } - - private void addListeners() { - instanceDescriptor.addListener(() -> { - initBaseline(); - this.firePropertyChanged(instanceDescriptor, instanceDescriptor); - }); - curve.addHeatingCurveListener(e -> { - if (e.getType() == RESCALED) { - var c = e.getData(); - if (!c.isIncomplete()) - curve.apply(getBaseline()); - } - }); - } - - /** - * Lists the available {@code DifferenceScheme}s for this {@code Problem}. - *

- * This is done utilising the {@code Reflexive} interface implemented by the - * class {@code DifferenceSheme}. This method dynamically locates any subclasses - * of the {@code DifferenceScheme} in the associated package (note this can be - * extended to include plugins) and checks whether any of the instances of those - * schemes return a non-{@code null} result when calling the - * {@code solver(Problem)} method. - *

- * - * @return a {@code List} of available {@code DifferenceScheme}s for solving - * this {@code Problem}. - */ - - public List availableSolutions() { - var allSchemes = Reflexive.instancesOf(DifferenceScheme.class); - return allSchemes.stream().filter(scheme -> scheme instanceof Solver).filter(s -> s.domain() == this.getClass()) - .collect(Collectors.toList()); - } - - /** - * 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()}. - * - * @see listedTypes() - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty value) { - properties.set(type, value); - } - - public HeatingCurve getHeatingCurve() { - return curve; - } - - public Pulse getPulse() { - return pulse; - } - - /** - * Sets the {@code pulse} of this {@code Problem} and assigns this - * {@code Problem} as its parent. - * - * @param pulse a {@code Pulse} object - */ - - public void setPulse(Pulse pulse) { - this.pulse = pulse; - pulse.setParent(this); - } - - /** - * This will use the data contained in {@code c} to estimate the detector signal - * span and the thermal diffusivity for this {@code Problem}. Note these - * estimates may be very rough. - * - * @param c the {@code ExperimentalData} object - */ - - public void retrieveData(ExperimentalData c) { - baseline.fitTo(c); // used to estimate the floor of the signal range - estimateSignalRange(c); - updateProperties(this, c.getMetadata()); - properties.useTheoreticalEstimates(c); - } - - /** - * The signal range is defined as max{ T(t) } - min{ T(t) - * }, where max{...} and min{...} are robust to - * outliers. This calls the {@code maxTemperature} method of {@code c} and uses - * the baseline value at {@code 0} as the min{...} value. - * - * @param c the {@code ExperimentalData} object - * @see pulse.input.ExperimentalData.maxTemperature() - */ - - public void estimateSignalRange(ExperimentalData c) { - final double signalHeight = c.maxAdjustedSignal() - baseline.valueAt(0); - properties.setMaximumTemperature(derive(MAXTEMP, signalHeight)); - } - - /** - * Calculates the vector argument defined on Rn - * to the scalar objective function for this {@code Problem}. To fill the vector - * with data, only those parameters from this {@code Problem} will be used which - * are defined by the {@code flags}, e.g. if the flag associated with the - * {@code HEAT_LOSS} keyword is set to false, its value will be skipped when - * creating the vector. - *

- * - * @see listedTypes() - */ - - /* + /** + * + */ + private static final long serialVersionUID = 7275327427201737684L; + private ThermalProperties properties; + private HeatingCurve curve; + private Baseline baseline; + private Pulse pulse; + + private static boolean hideDetailedAdjustment = true; + private ProblemComplexity complexity = ProblemComplexity.LOW; + + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor<>( + "Baseline Selector", Baseline.class); + + /** + * Creates a {@code Problem} with default parameters (as found in the .XML + * file). + *

+ * First, invokes the {@code super()} constructor of {@code PropertyHolder} + * to initialise {@code PropertyHolderListener}s, then initialises the + * variables and creates default {@code Pulse} and {@code HeatingCurve}, + * setting this object as their parent. + *

+ */ + protected Problem() { + initProperties(); + setHeatingCurve(new HeatingCurve()); + addListeners(); + instanceDescriptor.attemptUpdate(LinearBaseline.class.getSimpleName()); + } + + /** + * Copies all essential parameters from {@code p}, excluding the heating + * curve, which is created anew. + * + * @param p the {@code Problem} to replicate + */ + public Problem(Problem p) { + initProperties(p.getProperties().copy()); + setHeatingCurve(new HeatingCurve(p.getHeatingCurve())); + curve.setNumPoints(p.getHeatingCurve().getNumPoints()); + setBaseline(p.getBaseline()); + addListeners(); + } + + public abstract Problem copy(); + + public final void setHeatingCurve(HeatingCurve curve) { + this.curve = curve; + curve.setParent(this); + } + + private void addListeners() { + instanceDescriptor.addListener(() -> { + initBaseline(); + this.firePropertyChanged(instanceDescriptor, instanceDescriptor); + }); + curve.addHeatingCurveListener(e -> { + if (e.getType() == RESCALED) { + curve.apply(getBaseline()); + } + }); + } + + /** + * Lists the available {@code DifferenceScheme}s for this {@code Problem}. + *

+ * This is done utilising the {@code Reflexive} interface implemented by the + * class {@code DifferenceSheme}. This method dynamically locates any + * subclasses of the {@code DifferenceScheme} in the associated package + * (note this can be extended to include plugins) and checks whether any of + * the instances of those schemes return a non-{@code null} result when + * calling the {@code solver(Problem)} method. + *

+ * + * @return a {@code List} of available {@code DifferenceScheme}s for solving + * this {@code Problem}. + */ + 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())) + .collect(Collectors.toList()); + } + + /** + * 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()}. + * + * @see listedTypes() + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + properties.set(type, value); + } + + public final HeatingCurve getHeatingCurve() { + return curve; + } + + public final Pulse getPulse() { + return pulse; + } + + /** + * Sets the {@code pulse} of this {@code Problem} and assigns this + * {@code Problem} as its parent. + * + * @param pulse a {@code Pulse} object + */ + public final void setPulse(Pulse pulse) { + this.pulse = pulse; + this.pulse.setParent(this); + } + + /** + * This will use the data contained in {@code c} to estimate the detector + * signal span and the thermal diffusivity for this {@code Problem}. Note + * these estimates may be very rough. + * + * @param c the {@code ExperimentalData} object + */ + public void retrieveData(ExperimentalData c) { + baseline.fitTo(c); + estimateSignalRange(c); + updateProperties(this, c.getMetadata()); + properties.useTheoreticalEstimates(c); + } + + /** + * The signal range is defined as max{ T(t) } - min{ + * T(t) + * }, where max{...} and min{...} are + * robust to outliers. This calls the {@code maxTemperature} method of + * {@code c} and uses the baseline value at {@code 0} as the + * min{...} value. + * + * @param c the {@code ExperimentalData} object + * @see pulse.input.ExperimentalData.maxTemperature() + */ + public void estimateSignalRange(ExperimentalData c) { + 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)); + } + + /** + * Calculates the vector argument defined on + * Rn + * to the scalar objective function for this {@code Problem}. To fill the + * vector with data, only those parameters from this {@code Problem} will be + * used which are defined by the {@code flags}, e.g. if the flag associated + * with the {@code HEAT_LOSS} keyword is set to false, its value will be + * skipped when creating the vector. + *

+ * + * @see listedTypes() + */ + + /* * TODO put relative bounds in a constant field Consider creating a Bounds * class, or putting them in the XML file - */ - - @Override - public void optimisationVector(IndexedVector[] output, List flags) { - - baseline.optimisationVector(output, flags); - - for (int i = 0, size = output[0].dimension(); i < size; i++) { - - switch (output[0].getIndex(i)) { - case DIFFUSIVITY: - final double l = (double) properties.getSampleThickness().getValue(); - final double a = (double) properties.getDiffusivity().getValue(); - final double prefactor = 1.0 / (l * l); - output[0].set(i, a * prefactor); - output[1].set(i, 0.45 * a * prefactor); - break; - case MAXTEMP: - final double signalHeight = (double) properties.getMaximumTemperature().getValue(); - output[0].set(i, signalHeight); - output[1].set(i, 0.5 * signalHeight); - break; - case HEAT_LOSS: - final double Bi = (double) properties.getHeatLoss().getValue(); - output[0].set(i, properties.areThermalPropertiesLoaded() ? atanh(2.0 * Bi / properties.maxBiot() - 1.0) - : log(Bi)); - output[1].set(i, 2.0); - break; - case TIME_SHIFT: - output[0].set(i, (double) curve.getTimeShift().getValue()); - output[1].set(i, 0.025 * properties.timeFactor()); - break; - default: - continue; - } - } - - } - - /** - * Assigns parameter values of this {@code Problem} using the optimisation - * vector {@code params}. Only those parameters will be updated, the types of - * which are listed as indices in the {@code params} vector. - * - * @see listedTypes() - */ - - @Override - public void assign(IndexedVector params) { - baseline.assign(params); - for (int i = 0, size = params.dimension(); i < size; i++) { - - switch (params.getIndex(i)) { - case DIFFUSIVITY: - final double l = (double) properties.getSampleThickness().getValue(); - properties.setDiffusivity(derive(DIFFUSIVITY, params.get(i) * (l * l))); - break; - case MAXTEMP: - properties.setMaximumTemperature(derive(MAXTEMP, params.get(i))); - break; - case HEAT_LOSS: - final double bi = properties.areThermalPropertiesLoaded() - ? 0.5 * properties.maxBiot() * (tanh(params.get(i)) + 1.0) - : exp(params.get(i)); - properties.setHeatLoss(derive(HEAT_LOSS, bi)); - break; - case TIME_SHIFT: - curve.set(TIME_SHIFT, derive(TIME_SHIFT, params.get(i))); - break; - default: - continue; - } - } - - } - - /** - * Checks whether some 'advanced' details should stay hidden by the GUI when - * customising the {@code Problem} statement. - * - * @return {@code true} if the user does not want to see the details (by - * default), {@code false} otherwise. - */ - - @Override - public boolean areDetailsHidden() { - return Problem.hideDetailedAdjustment; - } - - /** - * Allows to either hide or display all 'advanced' settings for this - * {@code Problem}. - * - * @param b {@code true} if the user does not want to see the details, - * {@code false} otherwise. - */ - - public static void setDetailsHidden(boolean b) { - Problem.hideDetailedAdjustment = b; - } - - public String shortName() { - return getClass().getSimpleName(); - } - - /** - * Used for debugging. Initially, the nonlinear and two-dimensional problem - * statements are disabled, since they have not yet been thoroughly tested - * - * @return {@code true} if this problem statement has been enabled, - * {@code false} otherwise - */ - - public boolean isEnabled() { - return true; - } - - /** - * Constructs a {@code DiscretePulse} on the specified {@code grid} using the - * {@code Pulse} corresponding to this {@code Problem}. - * - * @param grid the grid - * @return a {@code DiscretePulse} objects constructed for this {@code Problem} - * and the {@code grid} - */ - - public DiscretePulse discretePulseOn(Grid grid) { - return new DiscretePulse(this, grid); - } - - /** - * Listed parameters include: - * MAXTEMP, DIFFUSIVITY, THICKNESS, HEAT_LOSS_FRONT, HEAT_LOSS_REAR. - */ - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(MAXTEMP)); - list.add(def(DIFFUSIVITY)); - list.add(def(THICKNESS)); - list.add(def(HEAT_LOSS)); - list.add(instanceDescriptor); - return list; - } - - @Override - public String toString() { - return this.getClass().getSimpleName(); - } - - public ProblemComplexity getComplexity() { - return complexity; - } - - public void setComplexity(ProblemComplexity complexity) { - this.complexity = complexity; - } - - /** - * Return the {@code Baseline} of this {@code Problem}. - * - * @return the baseline - */ - - public Baseline getBaseline() { - return baseline; - } - - /** - * Sets a new baseline. Calls {@code apply(baseline)} on the - * {@code HeatingCurve} when done and sets the {@code parent} of the baseline to - * this object. - * - * @param baseline the new baseline. - * @see pulse.baseline.Baseline.apply(Baseline) - */ - - public void setBaseline(Baseline baseline) { - this.baseline = baseline; - if (!curve.isIncomplete()) - curve.apply(baseline); - baseline.setParent(this); - - var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); - if (searchTask != null) { - var experimentalData = searchTask.getExperimentalCurve(); - baseline.fitTo(experimentalData); - } - } - - public InstanceDescriptor getBaselineDescriptor() { - return instanceDescriptor; - } - - private void initBaseline() { - // TODO - var baseline = instanceDescriptor.newInstance(Baseline.class); - setBaseline(baseline); - parameterListChanged(); - } - - public ThermalProperties getProperties() { - return properties; - } - - public final void setProperties(ThermalProperties properties) { - this.properties = properties; - this.properties.setParent(this); - } - - public abstract void initProperties(); - - public abstract void initProperties(ThermalProperties properties); - - public abstract Class defaultScheme(); - - public abstract boolean isReady(); - -} \ No newline at end of file + */ + @Override + public void optimisationVector(ParameterVector output) { + + baseline.optimisationVector(output); + + for (Parameter p : output.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + + Segment bounds = Segment.boundsFrom(key); + double value; + + switch (key) { + case THICKNESS: + value = (double) properties.getSampleThickness().getValue(); + break; + case DIFFUSIVITY: + 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); + value = signalHeight; + break; + case HEAT_LOSS: + value = (double) properties.getHeatLoss().getValue(); + p.setTransform(new StickTransform(bounds)); + break; + case TIME_SHIFT: + double magnitude = 0.25 * properties.characteristicTime(); + bounds = new Segment(-magnitude, magnitude); + value = (double) curve.getTimeShift().getValue(); + break; + default: + continue; + } + + p.setTransform(new StickTransform(bounds)); + p.setBounds(bounds); + p.setValue(value); + + } + + } + + /** + * Assigns parameter values of this {@code Problem} using the optimisation + * vector {@code params}. Only those parameters will be updated, the types + * of which are listed as indices in the {@code params} vector. + * + * @see listedTypes() + */ + @Override + public void assign(ParameterVector params) throws SolverException { + baseline.assign(params); + + List malformedList = params.findMalformedElements(); + + if (!malformedList.isEmpty()) { + StringBuilder sb = new StringBuilder("Cannot assign values: "); + malformedList.forEach(p + -> sb.append(String.format("%n %-25s", p.toString())) + ); + throw new SolverException(sb.toString(), ILLEGAL_PARAMETERS); + } + + for (Parameter p : params.getParameters()) { + + double value = p.inverseTransform(); + var key = p.getIdentifier().getKeyword(); + + switch (key) { + case THICKNESS: + properties.setSampleThickness(derive(THICKNESS, value)); + break; + case DIFFUSIVITY: + properties.setDiffusivity(derive(DIFFUSIVITY, value)); + break; + case MAXTEMP: + properties.setMaximumTemperature(derive(MAXTEMP, value)); + break; + case HEAT_LOSS: + properties.setHeatLoss(derive(HEAT_LOSS, value)); + break; + case TIME_SHIFT: + curve.set(TIME_SHIFT, derive(TIME_SHIFT, value)); + break; + default: + } + } + + } + + /** + * Checks whether some 'advanced' details should stay hidden by the GUI when + * customising the {@code Problem} statement. + * + * @return {@code true} if the user does not want to see the details (by + * default), {@code false} otherwise. + */ + @Override + public boolean areDetailsHidden() { + return Problem.hideDetailedAdjustment; + } + + /** + * Allows to either hide or display all 'advanced' settings for this + * {@code Problem}. + * + * @param b {@code true} if the user does not want to see the details, + * {@code false} otherwise. + */ + public final static void setDetailsHidden(boolean b) { + Problem.hideDetailedAdjustment = b; + } + + /** + * Used for debugging. Initially, the nonlinear and two-dimensional problem + * statements are disabled, since they have not yet been thoroughly tested + * + * @return {@code true} if this problem statement has been enabled, + * {@code false} otherwise + */ + public boolean isEnabled() { + return true; + } + + /** + * Constructs a {@code DiscretePulse} on the specified {@code grid} using + * the {@code Pulse} corresponding to this {@code Problem}. + * + * @param grid the grid + * @return a {@code DiscretePulse} objects constructed for this + * {@code Problem} and the {@code grid} + */ + public DiscretePulse discretePulseOn(Grid grid) { + return new DiscretePulse(this, grid); + } + + /** + * Listed parameters include: + * MAXTEMP, DIFFUSIVITY, THICKNESS, HEAT_LOSS_FRONT, HEAT_LOSS_REAR. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(MAXTEMP); + set.add(DIFFUSIVITY); + set.add(THICKNESS); + set.add(HEAT_LOSS); + return set; + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + public final ProblemComplexity getComplexity() { + return complexity; + } + + public final void setComplexity(ProblemComplexity complexity) { + this.complexity = complexity; + } + + /** + * Return the {@code Baseline} of this {@code Problem}. + * + * @return the baseline + */ + public Baseline getBaseline() { + return baseline; + } + + /** + * Sets a new baseline. Calls {@code apply(baseline)} on the + * {@code HeatingCurve} when done and sets the {@code parent} of the + * baseline to this object. + * + * @param baseline the new baseline. + * @see pulse.baseline.Baseline.apply(Baseline) + */ + public final void setBaseline(Baseline baseline) { + instanceDescriptor.setSelectedDescriptor(baseline.getClass().getSimpleName()); + this.baseline = baseline.copy(); + this.baseline.setParent(this); + curve.apply(this.baseline); + } + + public final InstanceDescriptor getBaselineDescriptor() { + return instanceDescriptor; + } + + private void initBaseline() { + setBaseline(instanceDescriptor.newInstance(Baseline.class)); + var searchTask = (SearchTask) this.specificAncestor(SearchTask.class); + if (searchTask != null) { + var experimentalData = (ExperimentalData) searchTask.getInput(); + Runnable baselineFitter = (Runnable & Serializable) () -> baseline.fitTo(experimentalData); + Executors.newSingleThreadExecutor().submit(baselineFitter); + } + parameterListChanged(); + } + + public final ThermalProperties getProperties() { + return properties; + } + + public final void setProperties(ThermalProperties properties) { + this.properties = properties; + this.properties.setParent(this); + } + + public abstract void initProperties(); + + public abstract void initProperties(ThermalProperties properties); + + public abstract Class defaultScheme(); + + public abstract boolean isReady(); + +} diff --git a/src/main/java/pulse/problem/statements/ProblemComplexity.java b/src/main/java/pulse/problem/statements/ProblemComplexity.java index d63c4d3f..693b4f74 100644 --- a/src/main/java/pulse/problem/statements/ProblemComplexity.java +++ b/src/main/java/pulse/problem/statements/ProblemComplexity.java @@ -4,16 +4,16 @@ public enum ProblemComplexity { - LOW(Color.green), MODERATE(Color.yellow), HIGH(Color.red); + LOW(Color.green), MODERATE(Color.yellow), HIGH(Color.red); - private Color clr; + private Color clr; - private ProblemComplexity(Color clr) { - this.clr = clr; - } + private ProblemComplexity(Color clr) { + this.clr = clr; + } - public Color getColor() { - return clr; - } + public Color getColor() { + return clr; + } -} \ 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 9b533b47..f177b20f 100644 --- a/src/main/java/pulse/problem/statements/Pulse.java +++ b/src/main/java/pulse/problem/statements/Pulse.java @@ -6,149 +6,223 @@ import static pulse.properties.NumericPropertyKeyword.LASER_ENERGY; import static pulse.properties.NumericPropertyKeyword.PULSE_WIDTH; -import java.util.ArrayList; import java.util.List; +import java.util.Set; +import pulse.input.ExperimentalData; import pulse.problem.laser.PulseTemporalShape; import pulse.problem.laser.RectangularPulse; +import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; +import pulse.tasks.SearchTask; import pulse.util.InstanceDescriptor; +import pulse.util.PropertyEvent; import pulse.util.PropertyHolder; /** * A {@code Pulse} stores the parameters of the laser pulse, but does not * provide the calculation facilities. - * + * * @see pulse.problem.laser.DiscretePulse - * + * */ - public class Pulse extends PropertyHolder { - private double pulseWidth; - private double laserEnergy; - - private PulseTemporalShape pulseShape; - - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( - "Pulse Shape Selector", PulseTemporalShape.class); - - /** - * Creates a {@code Pulse} with default values of pulse width and laser spot - * diameter (as per XML specification) and with a default pulse temporal shape (rectangular). - * - */ - - public Pulse() { - super(); - pulseWidth = (double) def(PULSE_WIDTH).getValue(); - laserEnergy = (double) def(LASER_ENERGY).getValue(); - instanceDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); - initShape(); - instanceDescriptor.addListener(() -> { - initShape(); - this.firePropertyChanged(instanceDescriptor, instanceDescriptor); - }); - } - - /** - * Copy constructor - * - * @param p the pulse, parameters of which will be copied. - */ - - public Pulse(Pulse p) { - super(); - this.pulseShape = p.getPulseShape(); - this.pulseWidth = p.pulseWidth; - this.laserEnergy = p.laserEnergy; - instanceDescriptor.addListener(() -> { - initShape(); - this.firePropertyChanged(instanceDescriptor, instanceDescriptor); - }); - } - - public double evaluateAt(final double time) { - return pulseShape.evaluateAt(time); - } - - private void initShape() { - setPulseShape(instanceDescriptor.newInstance(PulseTemporalShape.class)); - parameterListChanged(); - } - - public NumericProperty getPulseWidth() { - return derive(PULSE_WIDTH, pulseWidth); - } - - public void setPulseWidth(NumericProperty pulseWidth) { - requireType(pulseWidth, PULSE_WIDTH); - this.pulseWidth = (double) pulseWidth.getValue(); - firePropertyChanged(this, pulseWidth); - } - - public NumericProperty getLaserEnergy() { - return derive(LASER_ENERGY, laserEnergy); - } - - public void setLaserEnergy(NumericProperty laserEnergy) { - requireType(laserEnergy, LASER_ENERGY); - this.laserEnergy = (double) laserEnergy.getValue(); - firePropertyChanged(this, laserEnergy); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(getPulseShape()); - sb.append(" ; "); - sb.append(getPulseWidth()); - sb.append(" ; "); - sb.append(getLaserEnergy()); - return sb.toString(); - } - - /** - * The listed parameters for {@code Pulse} are: - * PulseShape, PULSE_WIDTH, SPOT_DIAMETER. - */ - - @Override - public List listedTypes() { - List list = new ArrayList<>(); - list.add(def(PULSE_WIDTH)); - list.add(def(LASER_ENERGY)); - list.add(instanceDescriptor); - return list; - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if(type == PULSE_WIDTH) - setPulseWidth(property); - else if(type == LASER_ENERGY) - setLaserEnergy(property); - } - - public InstanceDescriptor getPulseDescriptor() { - return instanceDescriptor; - } - - public void setPulseDescriptor(InstanceDescriptor shapeDescriptor) { - this.instanceDescriptor = shapeDescriptor; - //TODO - initShape(); - } - - public PulseTemporalShape getPulseShape() { - return pulseShape; - } - - public void setPulseShape(PulseTemporalShape pulseShape) { - this.pulseShape = pulseShape; - pulseShape.setParent(this); - } + /** + * + */ + private static final long serialVersionUID = 3564455042006771282L; + private double pulseWidth; + private double laserEnergy; + + private PulseTemporalShape pulseShape; + + private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( + "Pulse Shape Selector", PulseTemporalShape.class); + + /** + * Creates a {@code Pulse} with default values of pulse width and laser spot + * diameter (as per XML specification) and with a default pulse temporal + * shape (rectangular). + * + */ + public Pulse() { + super(); + pulseWidth = (double) def(PULSE_WIDTH).getValue(); + laserEnergy = (double) def(LASER_ENERGY).getValue(); + instanceDescriptor.setSelectedDescriptor(RectangularPulse.class.getSimpleName()); + initShape(); + addInstanceListener(); + initListeners(); + } + + /** + * Copy constructor + * + * @param p the pulse, parameters of which will be copied. + */ + public Pulse(Pulse p) { + super(); + this.pulseShape = p.getPulseShape(); + this.pulseWidth = p.pulseWidth; + this.laserEnergy = p.laserEnergy; + addInstanceListener(); + initListeners(); + } + + 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 + if (event.getProperty() instanceof NumericProperty) { + + var np = (NumericProperty) event.getProperty(); + var type = np.getType(); + + //when this property is a pulse width + if (type == NumericPropertyKeyword.PULSE_WIDTH) { + //find the specific SearchTask ancestor + var corrTask = (SearchTask) specificAncestor(SearchTask.class); + //new lower bound + 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); + + } + + } + + } + + }); + } + + public Pulse copy() { + return new Pulse(this); + } + + public void initFrom(Pulse pulse) { + this.pulseWidth = pulse.pulseWidth; + this.laserEnergy = pulse.laserEnergy; + this.pulseShape = pulse.pulseShape; + } + + private void initShape() { + setPulseShape(instanceDescriptor.newInstance(PulseTemporalShape.class)); + parameterListChanged(); + } + + public NumericProperty getPulseWidth() { + return derive(PULSE_WIDTH, pulseWidth); + } + + 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()) { + this.pulseWidth = (double) pulseWidth.getValue(); + firePropertyChanged(this, pulseWidth); + } + + } + + } + + public NumericProperty getLaserEnergy() { + return derive(LASER_ENERGY, laserEnergy); + } + + public void setLaserEnergy(NumericProperty laserEnergy) { + requireType(laserEnergy, LASER_ENERGY); + this.laserEnergy = (double) laserEnergy.getValue(); + firePropertyChanged(this, laserEnergy); + } + + @Override + public String toString() { + 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(); + } + + /** + * The listed parameters for {@code Pulse} are: + * PulseShape, PULSE_WIDTH, SPOT_DIAMETER. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(PULSE_WIDTH); + set.add(LASER_ENERGY); + return set; + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == PULSE_WIDTH) { + setPulseWidth(property); + } else if (type == LASER_ENERGY) { + setLaserEnergy(property); + } + } + + public InstanceDescriptor getPulseDescriptor() { + return instanceDescriptor; + } + + public void setPulseDescriptor(InstanceDescriptor shapeDescriptor) { + this.instanceDescriptor = shapeDescriptor; + //TODO + initShape(); + } + + public PulseTemporalShape getPulseShape() { + return pulseShape; + } + + public void setPulseShape(PulseTemporalShape pulseShape) { + this.pulseShape = pulseShape; + 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 ab08e385..7dd4c37c 100644 --- a/src/main/java/pulse/problem/statements/Pulse2D.java +++ b/src/main/java/pulse/problem/statements/Pulse2D.java @@ -5,69 +5,80 @@ import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; -import java.util.List; +import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; public class Pulse2D extends Pulse { - private double spotDiameter; + private static final long serialVersionUID = 8753396877032808678L; + private double spotDiameter; - /** - * Creates a {@code Pulse} with default values of pulse width and laser spot - * diameter (as per XML specification). - * - */ + /** + * Creates a {@code Pulse} with default values of pulse width and laser spot + * diameter (as per XML specification). + * + */ + public Pulse2D() { + super(); + spotDiameter = (double) def(SPOT_DIAMETER).getValue(); + } - public Pulse2D() { - super(); - spotDiameter = (double) def(SPOT_DIAMETER).getValue(); - } + /** + * Copy constructor + * + * @param p the pulse, parameters of which will be copied. + */ + public Pulse2D(Pulse p) { + super(p); + this.spotDiameter = p instanceof Pulse2D ? ((Pulse2D) p).spotDiameter : (double) def(SPOT_DIAMETER).getValue(); + } - /** - * Copy constructor - * - * @param p the pulse, parameters of which will be copied. - */ + @Override + public void initFrom(Pulse pulse) { + super.initFrom(pulse); + if (pulse instanceof Pulse2D) { + this.spotDiameter = ((Pulse2D) pulse).spotDiameter; + } + } - public Pulse2D(Pulse p) { - super(p); - this.spotDiameter = p instanceof Pulse2D ? ((Pulse2D) p).spotDiameter : (double) def(SPOT_DIAMETER).getValue(); - } + @Override + public Pulse copy() { + return new Pulse2D(this); + } - public NumericProperty getSpotDiameter() { - return derive(SPOT_DIAMETER, spotDiameter); - } + public NumericProperty getSpotDiameter() { + return derive(SPOT_DIAMETER, spotDiameter); + } - public void setSpotDiameter(NumericProperty spotDiameter) { - requireType(spotDiameter, SPOT_DIAMETER); - this.spotDiameter = (double) spotDiameter.getValue(); - firePropertyChanged(this, spotDiameter); - } + public void setSpotDiameter(NumericProperty spotDiameter) { + requireType(spotDiameter, SPOT_DIAMETER); + this.spotDiameter = (double) spotDiameter.getValue(); + firePropertyChanged(this, spotDiameter); + } - @Override - public String toString() { - StringBuilder sb = new StringBuilder(super.toString()); - sb.append(" ; "); - sb.append(getSpotDiameter()); - return sb.toString(); - } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(super.toString()); + sb.append(String.format("%n %-25s", getSpotDiameter())); + return sb.toString(); + } - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(SPOT_DIAMETER)); - return list; - } + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SPOT_DIAMETER); + return set; + } - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == SPOT_DIAMETER) - setSpotDiameter(property); - else - super.set(type, property); - } + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == SPOT_DIAMETER) { + setSpotDiameter(property); + } else { + super.set(type, property); + } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/problem/statements/ThermalProperties.java b/src/main/java/pulse/problem/statements/ThermalProperties.java deleted file mode 100644 index 3b523a5d..00000000 --- a/src/main/java/pulse/problem/statements/ThermalProperties.java +++ /dev/null @@ -1,303 +0,0 @@ -package pulse.problem.statements; - -import static java.lang.Math.PI; -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; -import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; -import static pulse.properties.NumericPropertyKeyword.DENSITY; -import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; -import static pulse.properties.NumericPropertyKeyword.EMISSIVITY; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; -import static pulse.properties.NumericPropertyKeyword.MAXTEMP; -import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; -import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; -import static pulse.properties.NumericPropertyKeyword.THICKNESS; - -import java.util.ArrayList; -import java.util.List; - -import pulse.input.ExperimentalData; -import pulse.input.InterpolationDataset.StandartType; -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; -import pulse.util.PropertyHolder; - -public class ThermalProperties extends PropertyHolder { - - private double a; - private double l; - private double Bi; - private double signalHeight; - private double cP; - private double rho; - private double T; - private double emissivity; - - public final static double STEFAN_BOTLZMAN = 5.6703E-08; // Stephan-Boltzmann constant - - /** - * The corrected proportionality factor setting out the relation between - * the thermal diffusivity and the half-rise time of an {@code ExperimentalData} - * curve. - * - * @see Parker et al. Journal - * of Applied Physics 32 (1961) 1679 - * @see Parker et al. - * Chem. Eng. Sci. 199 (2019) 546-551 - */ - - public final double PARKERS_COEFFICIENT = 0.1370; // in mm - - public ThermalProperties() { - super(); - a = (double) def(DIFFUSIVITY).getValue(); - l = (double) def(THICKNESS).getValue(); - Bi = (double) def(HEAT_LOSS).getValue(); - signalHeight = (double) def(MAXTEMP).getValue(); - T = (double) def(TEST_TEMPERATURE).getValue(); - emissivity = (double) def(EMISSIVITY).getValue(); - } - - public ThermalProperties(ThermalProperties p) { - super(); - this.l = p.l; - this.a = p.a; - this.Bi = p.Bi; - this.T = p.T; - this.emissivity = p.emissivity; - } - - public ThermalProperties copy() { - return new ThermalProperties(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()}. - * - * @see listedTypes() - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty value) { - switch (type) { - case DIFFUSIVITY: - setDiffusivity(value); - break; - case MAXTEMP: - setMaximumTemperature(value); - break; - case THICKNESS: - setSampleThickness(value); - break; - case HEAT_LOSS: - setHeatLoss(value); - break; - case SPECIFIC_HEAT: - setSpecificHeat(value); - break; - case DENSITY: - setDensity(value); - break; - case TEST_TEMPERATURE: - setTestTemperature(value); - break; - default: - break; - } - } - - public void setHeatLoss(NumericProperty Bi) { - requireType(Bi, HEAT_LOSS); - this.Bi = (double) Bi.getValue(); - firePropertyChanged(this, Bi); - } - - public NumericProperty getDiffusivity() { - return derive(DIFFUSIVITY, a); - } - - public void setDiffusivity(NumericProperty a) { - requireType(a, DIFFUSIVITY); - this.a = (double) a.getValue(); - firePropertyChanged(this, a); - } - - public NumericProperty getMaximumTemperature() { - return derive(MAXTEMP, signalHeight); - } - - public void setMaximumTemperature(NumericProperty maxTemp) { - requireType(maxTemp, MAXTEMP); - this.signalHeight = (double) maxTemp.getValue(); - firePropertyChanged(this, maxTemp); - } - - public NumericProperty getSampleThickness() { - return derive(THICKNESS, l); - } - - public void setSampleThickness(NumericProperty l) { - requireType(l, THICKNESS); - this.l = (double) l.getValue(); - firePropertyChanged(this, l); - } - - /** - *

- * Assuming that Bi1 = Bi2, returns the value - * of Bi1. If - * Bi1 = Bi2, this will print a warning - * message (but will not throw an exception) - *

- * - * @return Bi1 as a {@code NumericProperty} - */ - - public NumericProperty getHeatLoss() { - return derive(HEAT_LOSS, Bi); - } - - public NumericProperty getSpecificHeat() { - return derive(SPECIFIC_HEAT, cP); - } - - public void setSpecificHeat(NumericProperty cP) { - requireType(cP, SPECIFIC_HEAT); - this.cP = (double) cP.getValue(); - } - - public NumericProperty getDensity() { - return derive(DENSITY, rho); - } - - public void setDensity(NumericProperty p) { - requireType(p, DENSITY); - this.rho = (double) (p.getValue()); - } - - public NumericProperty getTestTemperature() { - return derive(TEST_TEMPERATURE, T); - } - - public void setTestTemperature(NumericProperty T) { - requireType(T, TEST_TEMPERATURE); - this.T = (double) T.getValue(); - - var heatCapacity = getDataset(HEAT_CAPACITY); - - if (heatCapacity != null) - cP = heatCapacity.interpolateAt(this.T); - - var density = getDataset(StandartType.DENSITY); - - if (density != null) - rho = density.interpolateAt(this.T); - - firePropertyChanged(this, T); - } - - /** - * Listed parameters include: - * MAXTEMP, DIFFUSIVITY, THICKNESS, HEAT_LOSS_FRONT, HEAT_LOSS_REAR. - */ - - @Override - public List listedTypes() { - List list = new ArrayList(); - list.add(def(MAXTEMP)); - list.add(def(DIFFUSIVITY)); - list.add(def(THICKNESS)); - list.add(def(HEAT_LOSS)); - list.add(def(DENSITY)); - list.add(def(SPECIFIC_HEAT)); - return list; - } - - public final double thermalConductivity() { - return a * cP * rho; - } - - public NumericProperty getThermalConductivity() { - return derive(CONDUCTIVITY, thermalConductivity()); - } - - public void emissivity() { - setEmissivity(derive(EMISSIVITY, Bi * thermalConductivity() / (4. * Math.pow(T, 3) * l * STEFAN_BOTLZMAN))); - } - - protected double maxBiot() { - double lambda = thermalConductivity(); - return 4.0 * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; - } - - protected double biot() { - double lambda = thermalConductivity(); - return 4.0 * emissivity * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; - } - - /** - * Performs simple calculation of the l2/a factor - * that is commonly used to evaluate the dimensionless time - * {@code t/timeFactor}. - * - * @return the time factor - */ - - public double timeFactor() { - return l * l / a; - } - - /** - * Calculates the half-rise time t1/2 of {@code c} and uses it - * to estimate the thermal diffusivity of this problem: - * a={@value PARKERS_COEFFICIENT}*l2/t1/2. - * - * @param c the {@code ExperimentalData} used to estimate the thermal - * diffusivity value - * @see pulse.input.ExperimentalData.halfRiseTime() - */ - - public void useTheoreticalEstimates(ExperimentalData c) { - final double t0 = c.halfRiseTime(); - this.a = PARKERS_COEFFICIENT * l * l / t0; - if (areThermalPropertiesLoaded()) - Bi = biot(); - } - - public final boolean areThermalPropertiesLoaded() { - return (Double.compare(cP, 0.0) > 0 && Double.compare(rho, 0.0) > 0); - } - - 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); - } - - public NumericProperty getEmissivity() { - return derive(EMISSIVITY, emissivity); - } - - public void setEmissivity(NumericProperty e) { - requireType(e, EMISSIVITY); - this.emissivity = (double) e.getValue(); - setHeatLoss(derive(HEAT_LOSS, biot())); - } - - @Override - public String getDescriptor() { - return "Sample Thermo-Physical Properties"; - } - - @Override - public String toString() { - return "Show Details..."; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/ThermoOpticalProperties.java deleted file mode 100644 index 9637cdf8..00000000 --- a/src/main/java/pulse/problem/statements/ThermoOpticalProperties.java +++ /dev/null @@ -1,152 +0,0 @@ -package pulse.problem.statements; - -import static pulse.math.MathUtils.fastPowLoop; -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; -import static pulse.properties.NumericPropertyKeyword.PLANCK_NUMBER; -import static pulse.properties.NumericPropertyKeyword.SCATTERING_ALBEDO; -import static pulse.properties.NumericPropertyKeyword.SCATTERING_ANISOTROPY; - -import java.util.List; - -import pulse.input.ExperimentalData; -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; - -public class ThermoOpticalProperties extends ThermalProperties { - - private double opticalThickness; - private double planckNumber; - private double scatteringAlbedo; - private double scatteringAnisotropy; - - 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(); - } - - public ThermoOpticalProperties(ThermalProperties p) { - super(p); - this.opticalThickness = (double) def(OPTICAL_THICKNESS).getValue(); - this.planckNumber = (double) def(PLANCK_NUMBER).getValue(); - scatteringAlbedo = (double) def(SCATTERING_ALBEDO).getValue(); - scatteringAnisotropy = (double) def(SCATTERING_ANISOTROPY).getValue(); - } - - public ThermoOpticalProperties(ThermoOpticalProperties p) { - super(p); - this.opticalThickness = p.opticalThickness; - this.planckNumber = p.planckNumber; - this.scatteringAlbedo = p.scatteringAlbedo; - this.scatteringAnisotropy = p.scatteringAnisotropy; - } - - @Override - public ThermalProperties copy() { - return new ThermoOpticalProperties(this); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty value) { - super.set(type, value); - - switch (type) { - case PLANCK_NUMBER: - setPlanckNumber(value); - break; - case OPTICAL_THICKNESS: - setOpticalThickness(value); - break; - case SCATTERING_ALBEDO: - setScatteringAlbedo(value); - break; - case SCATTERING_ANISOTROPY: - setScatteringAnisotropy(value); - break; - default: - break; - } - - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(PLANCK_NUMBER)); - list.add(def(OPTICAL_THICKNESS)); - list.add(def(SCATTERING_ALBEDO)); - list.add(def(SCATTERING_ANISOTROPY)); - return list; - } - - public double maxNp() { - final double l = (double) getSampleThickness().getValue(); - final double T = (double) getTestTemperature().getValue(); - return thermalConductivity() / (4.0 * STEFAN_BOTLZMAN * fastPowLoop(T, 3) * l); - } - - public NumericProperty getOpticalThickness() { - return derive(OPTICAL_THICKNESS, opticalThickness); - } - - public void setOpticalThickness(NumericProperty tau0) { - requireType(tau0, OPTICAL_THICKNESS); - this.opticalThickness = (double) tau0.getValue(); - firePropertyChanged(this, tau0); - } - - public NumericProperty getPlanckNumber() { - return derive(PLANCK_NUMBER, planckNumber); - } - - public void setPlanckNumber(NumericProperty planckNumber) { - requireType(planckNumber, PLANCK_NUMBER); - this.planckNumber = (double) planckNumber.getValue(); - firePropertyChanged(this, planckNumber); - } - - public NumericProperty getScatteringAnisostropy() { - return derive(SCATTERING_ANISOTROPY, scatteringAnisotropy); - } - - public void setScatteringAnisotropy(NumericProperty A1) { - requireType(A1, SCATTERING_ANISOTROPY); - this.scatteringAnisotropy = (double) A1.getValue(); - firePropertyChanged(this, A1); - } - - public NumericProperty getScatteringAlbedo() { - return derive(SCATTERING_ALBEDO, scatteringAlbedo); - } - - public void setScatteringAlbedo(NumericProperty omega0) { - requireType(omega0, SCATTERING_ALBEDO); - this.scatteringAlbedo = (double) omega0.getValue(); - firePropertyChanged(this, omega0); - } - - @Override - public void useTheoreticalEstimates(ExperimentalData c) { - super.useTheoreticalEstimates(c); - if ( areThermalPropertiesLoaded() ) { - final double nSq = 4; - final double lambda = thermalConductivity(); - final double l = (double) getSampleThickness().getValue(); - final double T = (double) getTestTemperature().getValue(); - final double nP = lambda / (4.0 * nSq * STEFAN_BOTLZMAN * fastPowLoop(T, 3) * l); - setPlanckNumber(derive(PLANCK_NUMBER, nP)); - } - } - - @Override - public String getDescriptor() { - return "Thermo-Physical & Optical Properties"; - } - -} \ No newline at end of file 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 00000000..980814fd --- /dev/null +++ b/src/main/java/pulse/problem/statements/TwoTemperatureModel.java @@ -0,0 +1,178 @@ +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 static final long serialVersionUID = 2567125396986165234L; + + 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 new file mode 100644 index 00000000..9e895e49 --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -0,0 +1,168 @@ +package pulse.problem.statements.model; + +import static pulse.problem.statements.model.SpectralRange.LASER; +import static pulse.problem.statements.model.SpectralRange.THERMAL; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericPropertyKeyword.LASER_ABSORPTIVITY; +import static pulse.properties.NumericPropertyKeyword.THERMAL_ABSORPTIVITY; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import pulse.math.Parameter; +import pulse.math.ParameterVector; +import static pulse.math.transforms.StandardTransformations.ABS; +import pulse.math.transforms.Transformable; +import pulse.problem.schemes.solvers.SolverException; +import static pulse.properties.NumericProperties.derive; + +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.COMBINED_ABSORPTIVITY; +import pulse.util.PropertyHolder; +import pulse.util.Reflexive; +import pulse.search.Optimisable; + +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); + } + + public abstract double absorption(SpectralRange range, double x); + + public NumericProperty getLaserAbsorptivity() { + return absorptionMap.get(LASER); + } + + public NumericProperty getThermalAbsorptivity() { + return absorptionMap.get(THERMAL); + } + + public NumericProperty getCombinedAbsorptivity() { + return getThermalAbsorptivity(); + } + + public NumericProperty getAbsorptivity(SpectralRange spectrum) { + return absorptionMap.get(spectrum); + } + + public void setAbsorptivity(SpectralRange range, NumericProperty a) { + absorptionMap.put(range, a); + } + + public void setLaserAbsorptivity(NumericProperty a) { + absorptionMap.put(LASER, a); + } + + public void setThermalAbsorptivity(NumericProperty a) { + absorptionMap.put(THERMAL, a); + } + + public void setCombinedAbsorptivity(NumericProperty a) { + setThermalAbsorptivity(a); + setLaserAbsorptivity(a); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + + switch (type) { + case LASER_ABSORPTIVITY: + absorptionMap.put(LASER, property); + break; + case THERMAL_ABSORPTIVITY: + absorptionMap.put(THERMAL, property); + break; + case COMBINED_ABSORPTIVITY: + setCombinedAbsorptivity(property); + break; + default: + break; + } + + } + + @Override + public String toString() { + 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 + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(LASER_ABSORPTIVITY); + set.add(THERMAL_ABSORPTIVITY); + set.add(COMBINED_ABSORPTIVITY); + return set; + } + + @Override + public void optimisationVector(ParameterVector output) { + for (Parameter p : output.getParameters()) { + var key = p.getIdentifier().getKeyword(); + double value = 0; + + Transformable transform = ABS; + + switch (key) { + case LASER_ABSORPTIVITY: + value = (double) (getLaserAbsorptivity()).getValue(); + break; + case THERMAL_ABSORPTIVITY: + value = (double) (getThermalAbsorptivity()).getValue(); + break; + case COMBINED_ABSORPTIVITY: + value = (double) (getCombinedAbsorptivity()).getValue(); + break; + default: + continue; + } + + //do this for the listed key values + p.setTransform(transform); + p.setValue(value); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + double value; + + for (Parameter p : params.getParameters()) { + var key = p.getIdentifier().getKeyword(); + + switch (key) { + case LASER_ABSORPTIVITY: + case THERMAL_ABSORPTIVITY: + case COMBINED_ABSORPTIVITY: + value = p.inverseTransform(); + break; + default: + continue; + } + + set(key, derive(key, value)); + + } + } + + public abstract AbsorptionModel copy(); + +} diff --git a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java new file mode 100644 index 00000000..6e3a956d --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java @@ -0,0 +1,26 @@ +package pulse.problem.statements.model; + +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()); + return a * Math.exp(-a * y); + } + + @Override + public AbsorptionModel copy() { + return new BeerLambertAbsorption(this); + } + +} diff --git a/src/main/java/pulse/problem/statements/model/DiathermicProperties.java b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java new file mode 100644 index 00000000..09f6d17d --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/DiathermicProperties.java @@ -0,0 +1,80 @@ +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 static pulse.properties.NumericProperty.requireType; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.DIATHERMIC_COEFFICIENT; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_CONVECTIVE; + +public class DiathermicProperties extends ThermalProperties { + + private static final long serialVersionUID = 1294930368429607512L; + private double diathermicCoefficient; + private double convectiveLosses; + + public DiathermicProperties() { + super(); + this.diathermicCoefficient = (double) def(DIATHERMIC_COEFFICIENT).getValue(); + this.convectiveLosses = (double) def(HEAT_LOSS_CONVECTIVE).getValue(); + } + + public DiathermicProperties(ThermalProperties p) { + super(p); + var property = p instanceof DiathermicProperties + ? ((DiathermicProperties) p).getDiathermicCoefficient() + : def(DIATHERMIC_COEFFICIENT); + this.diathermicCoefficient = (double) property.getValue(); + this.convectiveLosses = (double) property.getValue(); + } + + @Override + public ThermalProperties copy() { + return new ThermalProperties(this); + } + + public NumericProperty getDiathermicCoefficient() { + return derive(DIATHERMIC_COEFFICIENT, diathermicCoefficient); + } + + public void setDiathermicCoefficient(NumericProperty diathermicCoefficient) { + requireType(diathermicCoefficient, DIATHERMIC_COEFFICIENT); + this.diathermicCoefficient = (double) diathermicCoefficient.getValue(); + } + + public NumericProperty getConvectiveLosses() { + return derive(HEAT_LOSS_CONVECTIVE, convectiveLosses); + } + + public void setConvectiveLosses(NumericProperty convectiveLosses) { + requireType(convectiveLosses, HEAT_LOSS_CONVECTIVE); + this.convectiveLosses = (double) convectiveLosses.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + double value = ((Number) property.getValue()).doubleValue(); + switch (type) { + case DIATHERMIC_COEFFICIENT: + diathermicCoefficient = value; + break; + case HEAT_LOSS_CONVECTIVE: + convectiveLosses = value; + break; + default: + super.set(type, property); + break; + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(DIATHERMIC_COEFFICIENT); + set.add(HEAT_LOSS_CONVECTIVE); + return set; + } + +} diff --git a/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java new file mode 100644 index 00000000..fc720e0a --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/ExtendedThermalProperties.java @@ -0,0 +1,162 @@ +package pulse.problem.statements.model; + +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericProperty.requireType; +import static pulse.properties.NumericPropertyKeyword.DIAMETER; +import static pulse.properties.NumericPropertyKeyword.FOV_INNER; +import static pulse.properties.NumericPropertyKeyword.FOV_OUTER; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_SIDE; + +import java.util.Set; + +import pulse.input.ExperimentalData; +import pulse.properties.NumericProperties; +import static pulse.properties.NumericProperties.def; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_COMBINED; + +public class ExtendedThermalProperties extends ThermalProperties { + + private static final long serialVersionUID = 452882822074681811L; + private double d; + private double Bi3; + private double fovOuter; + private double fovInner; + + public ExtendedThermalProperties() { + super(); + Bi3 = (double) def(HEAT_LOSS_SIDE).getValue(); + d = (double) def(DIAMETER).getValue(); + fovOuter = (double) def(FOV_OUTER).getValue(); + fovInner = (double) def(FOV_INNER).getValue(); + defaultValues(); + } + + public ExtendedThermalProperties(ThermalProperties sdd) { + super(sdd); + defaultValues(); + } + + private void defaultValues() { + Bi3 = (double) def(HEAT_LOSS_SIDE).getValue(); + d = (double) def(DIAMETER).getValue(); + fovOuter = (double) def(FOV_OUTER).getValue(); + fovInner = (double) def(FOV_INNER).getValue(); + } + + public ExtendedThermalProperties(ExtendedThermalProperties sdd) { + super(sdd); + this.d = sdd.d; + this.Bi3 = sdd.Bi3; + this.fovOuter = sdd.fovOuter; + this.fovInner = sdd.fovInner; + } + + @Override + public ThermalProperties copy() { + return new ExtendedThermalProperties(this); + } + + @Override + public void useTheoreticalEstimates(ExperimentalData c) { + super.useTheoreticalEstimates(c); + if (areThermalPropertiesLoaded()) { + Bi3 = radiationBiot(); + } + } + + public NumericProperty getSampleDiameter() { + return derive(DIAMETER, d); + } + + public void setSampleDiameter(NumericProperty d) { + requireType(d, DIAMETER); + this.d = (double) d.getValue(); + firePropertyChanged(this, d); + } + + public NumericProperty getSideLosses() { + return derive(HEAT_LOSS_SIDE, Bi3); + } + + public void setSideLosses(NumericProperty bi3) { + requireType(bi3, HEAT_LOSS_SIDE); + this.Bi3 = (double) bi3.getValue(); + firePropertyChanged(this, bi3); + } + + public NumericProperty getCombinedLosses() { + return derive(HEAT_LOSS_COMBINED, (double) this.getHeatLoss().getValue()); + } + + public void setCombinedLosses(NumericProperty bic) { + requireType(bic, HEAT_LOSS_COMBINED); + double value = (double) bic.getValue(); + setSideLosses(NumericProperties.derive(HEAT_LOSS_SIDE, value)); + setHeatLoss(NumericProperties.derive(HEAT_LOSS, value)); + } + + public NumericProperty getFOVOuter() { + return derive(FOV_OUTER, fovOuter); + } + + public void setFOVOuter(NumericProperty fovOuter) { + requireType(fovOuter, FOV_OUTER); + this.fovOuter = (double) fovOuter.getValue(); + firePropertyChanged(this, fovOuter); + } + + public NumericProperty getFOVInner() { + return derive(FOV_INNER, fovInner); + } + + public void setFOVInner(NumericProperty fovInner) { + requireType(fovInner, FOV_INNER); + this.fovInner = (double) fovInner.getValue(); + firePropertyChanged(this, fovInner); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(HEAT_LOSS_SIDE); + set.add(HEAT_LOSS_COMBINED); + set.add(DIAMETER); + set.add(FOV_OUTER); + set.add(FOV_INNER); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + switch (type) { + case FOV_OUTER: + setFOVOuter(property); + break; + case FOV_INNER: + setFOVInner(property); + break; + case DIAMETER: + setSampleDiameter(property); + break; + case HEAT_LOSS_SIDE: + setSideLosses(property); + break; + //extracts the value from the combined heat loss and sets the facial and side heat losses + case HEAT_LOSS_COMBINED: + setCombinedLosses(property); + break; + default: + break; + } + } + + @Override + public String getDescriptor() { + return "Sample Thermo-Physical Properties (2D)"; + } + +} 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 00000000..ad050e36 --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Gas.java @@ -0,0 +1,75 @@ +package pulse.problem.statements.model; + +import java.io.Serializable; +import pulse.util.Descriptive; +import pulse.util.Reflexive; + +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; + } + + 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(); + } + +} 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 00000000..206a2bf7 --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Helium.java @@ -0,0 +1,16 @@ +package pulse.problem.statements.model; + +public class Helium extends Gas { + + private static final long serialVersionUID = -4697854426333597653L; + + public Helium() { + super(1, 4); + } + + @Override + public double thermalConductivity(double t) { + return 0.415 + 0.283E-3 * (t - 1200); + } + +} diff --git a/src/main/java/pulse/problem/statements/model/Insulator.java b/src/main/java/pulse/problem/statements/model/Insulator.java new file mode 100644 index 00000000..25137f72 --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Insulator.java @@ -0,0 +1,66 @@ +package pulse.problem.statements.model; + +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.REFLECTANCE; + +import java.util.Set; + +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; + +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) { + R = (double) ((Insulator) m).getReflectance().getValue(); + } else { + R = (double) def(REFLECTANCE).getValue(); + } + } + + @Override + public double absorption(SpectralRange spectrum, double x) { + double a = (double) (this.getAbsorptivity(spectrum).getValue()); + return a * (Math.exp(-a * x) - R * Math.exp(-a * (2.0 - x))) / (1.0 - R * R * Math.exp(-2.0 * a)); + } + + public NumericProperty getReflectance() { + return derive(REFLECTANCE, R); + } + + public void setReflectance(NumericProperty a) { + NumericProperty.requireType(a, REFLECTANCE); + this.R = (double) a.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if (type == REFLECTANCE) { + setReflectance(property); + } + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(REFLECTANCE); + 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 00000000..2b00b8ed --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/Nitrogen.java @@ -0,0 +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; + } + +} diff --git a/src/main/java/pulse/problem/statements/model/SpectralRange.java b/src/main/java/pulse/problem/statements/model/SpectralRange.java new file mode 100644 index 00000000..bbb074ed --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/SpectralRange.java @@ -0,0 +1,24 @@ +package pulse.problem.statements.model; + +import pulse.properties.NumericPropertyKeyword; + +public enum SpectralRange { + LASER("Laser Absorption"), THERMAL("Thermal Radiation Absorption"); + + String name; + + SpectralRange(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public NumericPropertyKeyword typeOfAbsorption() { + return this == SpectralRange.LASER ? NumericPropertyKeyword.LASER_ABSORPTIVITY + : NumericPropertyKeyword.THERMAL_ABSORPTIVITY; + } + +} diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java new file mode 100644 index 00000000..5e2f135a --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -0,0 +1,385 @@ +package pulse.problem.statements.model; + +import static java.lang.Math.PI; +import java.util.List; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericProperty.requireType; +import static pulse.properties.NumericPropertyKeyword.*; + +import java.util.Set; +import java.util.stream.Collectors; + +import pulse.input.ExperimentalData; +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; + private double signalHeight; + private double cP; + private double rho; + private double T; + private double emissivity; + + public final static double STEFAN_BOTLZMAN = 5.6703E-08; // Stephan-Boltzmann constant + + /** + * The corrected proportionality factor setting out the relation + * between the thermal diffusivity and the half-rise time of an + * {@code ExperimentalData} curve. + * + * @see Parker et al. + * Journal of Applied Physics 32 (1961) 1679 + * @see Parker et + * al. + * Chem. Eng. Sci. 199 (2019) 546-551 + */ + public final double PARKERS_COEFFICIENT = 0.1388; // in mm + + public ThermalProperties() { + super(); + a = (double) def(DIFFUSIVITY).getValue(); + l = (double) def(THICKNESS).getValue(); + Bi = (double) def(HEAT_LOSS).getValue(); + signalHeight = (double) def(MAXTEMP).getValue(); + T = (double) def(TEST_TEMPERATURE).getValue(); + emissivity = (double) def(EMISSIVITY).getValue(); + fill(); + } + + public ThermalProperties(ThermalProperties p) { + super(); + this.l = p.l; + this.a = p.a; + this.Bi = p.Bi; + this.T = p.T; + this.signalHeight = p.signalHeight; + this.emissivity = p.emissivity; + fill(); + } + + public List findMalformedProperties() { + var list = this.numericData().stream() + .filter(np -> !np.validate()).collect(Collectors.toList()); + return list; + } + + /** + * Calculates some or all of the following properties: + * Cp, ρ, &labmda;, + * ε. + *

+ * These properties will be calculated only if the necessary + * {@code InterpolationDataset}s were previously loaded by the + * {@code TaskManager}. + *

+ */ + + 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); + } + + i.addExternalDatasetListener(new ExternalDatasetListener() { + @Override + public void onSpecificHeatDataLoaded() { + cP = i.getSpecificHeatDataset().interpolateAt(T); + } + + @Override + public void onDensityDataLoaded() { + rho = i.getDensityDataset().interpolateAt(T); + } + + }); + + } + + public ThermalProperties copy() { + return new ThermalProperties(this); + } + + /** + * Hides optimiser directives + * + * @return true + */ + @Override + public boolean areDetailsHidden() { + return true; + } + + /** + * 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 value + * @see listedTypes() + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + switch (type) { + case DIFFUSIVITY: + setDiffusivity(value); + break; + case MAXTEMP: + setMaximumTemperature(value); + break; + case THICKNESS: + setSampleThickness(value); + break; + case HEAT_LOSS: + setHeatLoss(value); + break; + case SPECIFIC_HEAT: + setSpecificHeat(value); + break; + case DENSITY: + setDensity(value); + break; + case TEST_TEMPERATURE: + setTestTemperature(value); + break; + default: + break; + } + } + + public void setHeatLoss(NumericProperty Bi) { + requireType(Bi, HEAT_LOSS); + this.Bi = (double) Bi.getValue(); + if (areThermalPropertiesLoaded()) { + calculateEmissivity(); + } + firePropertyChanged(this, Bi); + } + + public NumericProperty getDiffusivity() { + return derive(DIFFUSIVITY, a); + } + + public void setDiffusivity(NumericProperty a) { + requireType(a, DIFFUSIVITY); + this.a = (double) a.getValue(); + firePropertyChanged(this, a); + } + + public NumericProperty getMaximumTemperature() { + return derive(MAXTEMP, signalHeight); + } + + public void setMaximumTemperature(NumericProperty maxTemp) { + requireType(maxTemp, MAXTEMP); + this.signalHeight = (double) maxTemp.getValue(); + firePropertyChanged(this, maxTemp); + } + + public NumericProperty getSampleThickness() { + return derive(THICKNESS, l); + } + + public void setSampleThickness(NumericProperty l) { + requireType(l, THICKNESS); + this.l = (double) l.getValue(); + firePropertyChanged(this, l); + } + + /** + *

+ * Assuming that Bi1 = Bi2, returns the + * value of Bi1. If Bi1 = + * Bi2, this will print a warning message (but will not + * throw an exception) + *

+ * + * @return Bi1 as a {@code NumericProperty} + */ + public NumericProperty getHeatLoss() { + return derive(HEAT_LOSS, Bi); + } + + public NumericProperty getSpecificHeat() { + return derive(SPECIFIC_HEAT, cP); + } + + public void setSpecificHeat(NumericProperty cP) { + requireType(cP, SPECIFIC_HEAT); + this.cP = (double) cP.getValue(); + firePropertyChanged(this, cP); + } + + public NumericProperty getDensity() { + return derive(DENSITY, rho); + } + + public void setDensity(NumericProperty p) { + requireType(p, DENSITY); + this.rho = (double) (p.getValue()); + firePropertyChanged(this, p); + } + + public NumericProperty getTestTemperature() { + return derive(TEST_TEMPERATURE, T); + } + + public void setTestTemperature(NumericProperty T) { + requireType(T, TEST_TEMPERATURE); + this.T = (double) T.getValue(); + + var i = TaskManager.getManagerInstance(); + var heatCapacity = i.getSpecificHeatDataset(); + + if (heatCapacity != null) { + cP = heatCapacity.interpolateAt(this.T); + } + + var density = i.getDensityDataset(); + + if (density != null) { + rho = density.interpolateAt(this.T); + } + + firePropertyChanged(this, T); + } + + /** + * Listed parameters include: + * MAXTEMP, DIFFUSIVITY, THICKNESS, HEAT_LOSS_FRONT, HEAT_LOSS_REAR. + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(MAXTEMP); + set.add(DIFFUSIVITY); + set.add(THICKNESS); + set.add(HEAT_LOSS); + set.add(DENSITY); + set.add(SPECIFIC_HEAT); + set.add(EMISSIVITY); + return set; + } + + public final double thermalConductivity() { + return a * getThermalMass(); + } + + public NumericProperty getThermalConductivity() { + return derive(CONDUCTIVITY, thermalConductivity()); + } + + 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, + 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. + * + * @return the maximum Biot number + */ + public double maxRadiationBiot() { + double absMax = Segment.boundsFrom(HEAT_LOSS).getMaximum(); + return emissivity > 0 ? radiationBiot() / emissivity : absMax; + } + + /** + * Performs simple calculation of the l2/a + * factor that is commonly used to evaluate the dimensionless time + * {@code t/timeFactor}. + * + * @return the time factor + */ + public double characteristicTime() { + return l * l / a; + } + + public double getThermalMass() { + return cP * rho; + } + + /** + * Calculates the half-rise time t1/2 of {@code c} and + * uses it to estimate the thermal diffusivity of this problem: + * a={@value PARKERS_COEFFICIENT}*l2/t1/2. + * + * @param c the {@code ExperimentalData} used to estimate the thermal + * diffusivity value + * @see pulse.input.ExperimentalData.halfRiseTime() + */ + public void useTheoreticalEstimates(ExperimentalData c) { + final double t0 = c.getHalfTimeCalculator().getHalfTime(); + this.a = PARKERS_COEFFICIENT * l * l / t0; + if (areThermalPropertiesLoaded()) { + Bi = radiationBiot(); + } + } + + public final boolean areThermalPropertiesLoaded() { + return (Double.compare(cP, 0.0) > 0 && Double.compare(rho, 0.0) > 0); + } + + 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()); + } + + public NumericProperty getEmissivity() { + return derive(EMISSIVITY, emissivity); + } + + public void setEmissivity(NumericProperty e) { + requireType(e, EMISSIVITY); + this.emissivity = (double) e.getValue(); + firePropertyChanged(this, e); + } + + @Override + public String getDescriptor() { + return "Sample Thermo-Physical Properties"; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getDescriptor()); + sb.append(":"); + sb.append(String.format("%n %-25s", this.getDiffusivity())); + sb.append(String.format("%n %-25s", this.getMaximumTemperature())); + sb.append(String.format("%n %-25s", this.getHeatLoss())); + return sb.toString(); + } + +} diff --git a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java new file mode 100644 index 00000000..ff844eb6 --- /dev/null +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -0,0 +1,266 @@ +package pulse.problem.statements.model; + +import static pulse.math.MathUtils.fastPowLoop; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperty.requireType; +import static pulse.properties.NumericPropertyKeyword.SCATTERING_ALBEDO; +import static pulse.properties.NumericPropertyKeyword.SCATTERING_ANISOTROPY; + +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 static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS_CONVECTIVE; +import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; +import static pulse.properties.NumericPropertyKeyword.PLANCK_NUMBER; +import pulse.search.Optimisable; + +public class ThermoOpticalProperties extends ThermalProperties implements Optimisable { + + private static final long serialVersionUID = 3573682830421468534L; + private double opticalThickness; + private double planckNumber; + private double scatteringAlbedo; + private double scatteringAnisotropy; + private double convectiveLosses; + + 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(); + } + + 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(); + } + + 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; + } + + @Override + public ThermalProperties copy() { + return new ThermoOpticalProperties(this); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + super.set(type, value); + + switch (type) { + case PLANCK_NUMBER: + setPlanckNumber(value); + break; + case OPTICAL_THICKNESS: + setOpticalThickness(value); + break; + case SCATTERING_ALBEDO: + setScatteringAlbedo(value); + break; + case SCATTERING_ANISOTROPY: + setScatteringAnisotropy(value); + break; + case HEAT_LOSS_CONVECTIVE: + setConvectiveLosses(value); + break; + default: + break; + } + + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(PLANCK_NUMBER); + set.add(OPTICAL_THICKNESS); + set.add(SCATTERING_ALBEDO); + set.add(SCATTERING_ANISOTROPY); + set.add(HEAT_LOSS_CONVECTIVE); + return set; + } + + public double maxNp() { + final double l = (double) getSampleThickness().getValue(); + final double T = (double) getTestTemperature().getValue(); + return thermalConductivity() / (4.0 * STEFAN_BOTLZMAN * fastPowLoop(T, 3) * l); + } + + public NumericProperty getOpticalThickness() { + return derive(OPTICAL_THICKNESS, opticalThickness); + } + + public void setOpticalThickness(NumericProperty tau0) { + requireType(tau0, OPTICAL_THICKNESS); + this.opticalThickness = (double) tau0.getValue(); + firePropertyChanged(this, tau0); + } + + public NumericProperty getPlanckNumber() { + return derive(PLANCK_NUMBER, planckNumber); + } + + public void setPlanckNumber(NumericProperty planckNumber) { + requireType(planckNumber, PLANCK_NUMBER); + this.planckNumber = (double) planckNumber.getValue(); + firePropertyChanged(this, planckNumber); + } + + public NumericProperty getScatteringAnisostropy() { + return derive(SCATTERING_ANISOTROPY, scatteringAnisotropy); + } + + public void setScatteringAnisotropy(NumericProperty A1) { + requireType(A1, SCATTERING_ANISOTROPY); + this.scatteringAnisotropy = (double) A1.getValue(); + firePropertyChanged(this, A1); + } + + public void setConvectiveLosses(NumericProperty losses) { + requireType(losses, HEAT_LOSS_CONVECTIVE); + this.convectiveLosses = (double) losses.getValue(); + firePropertyChanged(this, losses); + } + + public NumericProperty getConvectiveLosses() { + return derive(HEAT_LOSS_CONVECTIVE, convectiveLosses); + } + + public NumericProperty getScatteringAlbedo() { + return derive(SCATTERING_ALBEDO, scatteringAlbedo); + } + + public void setScatteringAlbedo(NumericProperty omega0) { + requireType(omega0, SCATTERING_ALBEDO); + this.scatteringAlbedo = (double) omega0.getValue(); + firePropertyChanged(this, omega0); + } + + @Override + public void useTheoreticalEstimates(ExperimentalData c) { + super.useTheoreticalEstimates(c); + if (areThermalPropertiesLoaded()) { + final double nSq = 4; + final double lambda = thermalConductivity(); + final double l = (double) getSampleThickness().getValue(); + final double T = (double) getTestTemperature().getValue(); + final double nP = lambda / (4.0 * nSq * STEFAN_BOTLZMAN * fastPowLoop(T, 3) * l); + setPlanckNumber(derive(PLANCK_NUMBER, nP)); + } + } + + @Override + public String getDescriptor() { + return "Thermo-Physical & Optical Properties"; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(super.toString()); + sb.append(String.format("%n %-25s", this.getConvectiveLosses())); + sb.append(String.format("%n %-25s", this.getOpticalThickness())); + sb.append(String.format("%n %-25s", this.getPlanckNumber())); + sb.append(String.format("%n %-25s", this.getScatteringAlbedo())); + sb.append(String.format("%n %-25s", this.getScatteringAnisostropy())); + sb.append(String.format("%n %-25s", this.getSpecificHeat())); + sb.append(String.format("%n %-25s", this.getDensity())); + return sb.toString(); + } + + @Override + public void optimisationVector(ParameterVector output) { + Segment bounds = null; + double value; + Transformable transform; + + for (Parameter p : output.getParameters()) { + + var key = p.getIdentifier().getKeyword(); + + switch (key) { + case PLANCK_NUMBER: + final double lowerBound = Segment.boundsFrom(PLANCK_NUMBER).getMinimum(); + bounds = new Segment(lowerBound, maxNp()); + value = planckNumber; + break; + case OPTICAL_THICKNESS: + value = opticalThickness; + bounds = Segment.boundsFrom(OPTICAL_THICKNESS); + break; + case SCATTERING_ALBEDO: + value = scatteringAlbedo; + bounds = Segment.boundsFrom(SCATTERING_ALBEDO); + break; + case SCATTERING_ANISOTROPY: + value = scatteringAnisotropy; + bounds = Segment.boundsFrom(SCATTERING_ANISOTROPY); + break; + case HEAT_LOSS_CONVECTIVE: + value = convectiveLosses; + bounds = Segment.boundsFrom(HEAT_LOSS_CONVECTIVE); + break; + case HEAT_LOSS: + value = (double) getHeatLoss().getValue(); + bounds = new Segment(0.0, maxRadiationBiot()); + break; + default: + continue; + + } + + transform = new StickTransform(bounds); + p.setTransform(transform); + p.setValue(value); + p.setBounds(bounds); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + + for (Parameter p : params.getParameters()) { + + var type = p.getIdentifier().getKeyword(); + + switch (type) { + + case PLANCK_NUMBER: + case SCATTERING_ALBEDO: + case SCATTERING_ANISOTROPY: + case OPTICAL_THICKNESS: + case HEAT_LOSS_CONVECTIVE: + 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 00000000..099fde4b --- /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 static final long serialVersionUID = 4157382023954200467L; + 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/problem/statements/package-info.java b/src/main/java/pulse/problem/statements/package-info.java index c6685603..1144423a 100644 --- a/src/main/java/pulse/problem/statements/package-info.java +++ b/src/main/java/pulse/problem/statements/package-info.java @@ -1,7 +1,5 @@ /** * Introduces various problem statements for the heat conduction problem in the - * laser flash experiment. Note that currently only one problem statement is - * enabled, which is the {@code LinearisedProblem}. + * laser flash experiment. */ - -package pulse.problem.statements; \ No newline at end of file +package pulse.problem.statements; diff --git a/src/main/java/pulse/problem/statements/penetration/AbsorptionModel.java b/src/main/java/pulse/problem/statements/penetration/AbsorptionModel.java deleted file mode 100644 index 09614636..00000000 --- a/src/main/java/pulse/problem/statements/penetration/AbsorptionModel.java +++ /dev/null @@ -1,85 +0,0 @@ -package pulse.problem.statements.penetration; - -import static pulse.problem.statements.penetration.SpectralRange.LASER; -import static pulse.problem.statements.penetration.SpectralRange.THERMAL; -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericPropertyKeyword.LASER_ABSORPTIVITY; -import static pulse.properties.NumericPropertyKeyword.THERMAL_ABSORPTIVITY; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; -import pulse.util.PropertyHolder; -import pulse.util.Reflexive; - -public abstract class AbsorptionModel extends PropertyHolder implements Reflexive { - - private Map absorptionMap; - - protected AbsorptionModel() { - setPrefix("Absorption model"); - absorptionMap = new HashMap<>(); - absorptionMap.put(LASER, def(LASER_ABSORPTIVITY)); - absorptionMap.put(THERMAL, def(THERMAL_ABSORPTIVITY)); - } - - public abstract double absorption(SpectralRange range, double x); - - public NumericProperty getLaserAbsorptivity() { - return absorptionMap.get(LASER); - } - - public NumericProperty getThermalAbsorptivity() { - return absorptionMap.get(THERMAL); - } - - public NumericProperty getAbsorptivity(SpectralRange spectrum) { - return absorptionMap.get(spectrum); - } - - public void setAbsorptivity(SpectralRange range, NumericProperty a) { - absorptionMap.put(range, a); - } - - public void setLaserAbsorptivity(NumericProperty a) { - absorptionMap.put(LASER, a); - } - - public void setThermalAbsorptivity(NumericProperty a) { - absorptionMap.put(THERMAL, a); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - - switch (type) { - case LASER_ABSORPTIVITY: - absorptionMap.put(LASER, property); - break; - case THERMAL_ABSORPTIVITY: - absorptionMap.put(THERMAL, property); - break; - default: - break; - } - - } - - @Override - public String toString() { - return getClass().getSimpleName() + " : " + absorptionMap.get(LASER) + " ; " + absorptionMap.get(THERMAL); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(LASER_ABSORPTIVITY)); - list.add(def(THERMAL_ABSORPTIVITY)); - return list; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/penetration/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/penetration/BeerLambertAbsorption.java deleted file mode 100644 index 623e9303..00000000 --- a/src/main/java/pulse/problem/statements/penetration/BeerLambertAbsorption.java +++ /dev/null @@ -1,15 +0,0 @@ -package pulse.problem.statements.penetration; - -public class BeerLambertAbsorption extends AbsorptionModel { - - public BeerLambertAbsorption() { - super(); - } - - @Override - public double absorption(SpectralRange range, double y) { - double a = (double) (this.getAbsorptivity(range).getValue()); - return a * Math.exp(-a * y); - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/penetration/Insulator.java b/src/main/java/pulse/problem/statements/penetration/Insulator.java deleted file mode 100644 index 5a7dd547..00000000 --- a/src/main/java/pulse/problem/statements/penetration/Insulator.java +++ /dev/null @@ -1,50 +0,0 @@ -package pulse.problem.statements.penetration; - -import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.REFLECTANCE; - -import java.util.List; - -import pulse.properties.NumericProperty; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; - -public class Insulator extends AbsorptionModel { - - protected double R; - - public Insulator() { - R = (double) def(REFLECTANCE).getValue(); - } - - @Override - public double absorption(SpectralRange spectrum, double x) { - double a = (double) (this.getAbsorptivity(spectrum).getValue()); - return a * (Math.exp(-a * x) - R * Math.exp(-a * (2.0 - x))) / (1.0 - R * R * Math.exp(-2.0 * a)); - } - - public NumericProperty getReflectance() { - return derive(REFLECTANCE, R); - } - - public void setReflectance(NumericProperty a) { - this.R = (double) a.getValue(); - - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - super.set(type, property); - if (type == REFLECTANCE) - R = ((Number) property.getValue()).doubleValue(); - } - - @Override - public List listedTypes() { - List list = super.listedTypes(); - list.add(def(REFLECTANCE)); - return list; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/penetration/SpectralRange.java b/src/main/java/pulse/problem/statements/penetration/SpectralRange.java deleted file mode 100644 index 0d3f8aaf..00000000 --- a/src/main/java/pulse/problem/statements/penetration/SpectralRange.java +++ /dev/null @@ -1,24 +0,0 @@ -package pulse.problem.statements.penetration; - -import pulse.properties.NumericPropertyKeyword; - -public enum SpectralRange { - LASER("Laser Absorption"), THERMAL("Thermal Radiation Absorption"); - - String name; - - SpectralRange(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } - - public NumericPropertyKeyword typeOfAbsorption() { - return this == SpectralRange.LASER ? NumericPropertyKeyword.LASER_ABSORPTIVITY - : NumericPropertyKeyword.THERMAL_ABSORPTIVITY; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/properties/Flag.java b/src/main/java/pulse/properties/Flag.java index dcfd6d3f..d6c6f10a 100644 --- a/src/main/java/pulse/properties/Flag.java +++ b/src/main/java/pulse/properties/Flag.java @@ -1,12 +1,6 @@ package pulse.properties; import static pulse.properties.NumericProperties.def; -import static pulse.properties.NumericProperties.defaultList; -import static pulse.properties.NumericPropertyKeyword.LOWER_BOUND; -import static pulse.properties.NumericPropertyKeyword.TIME_SHIFT; -import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; - -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -16,172 +10,168 @@ * with a short abbreviation describing the type of this flag (this is usually * defined by the corresponding {@code NumericProperty}). */ - public class Flag implements Property { - private NumericPropertyKeyword index; - private boolean value; - private String descriptor; - - /** - * Creates a {@code Flag} with the type {@code type}. The default {@code value} - * is set to {@code false}. - * - * @param type the {@code NumericPropertyKeyword} associated with this - * {@code Flag} - */ - - public Flag(NumericPropertyKeyword type) { - this.index = type; - value = false; - } - - /** - * Creates a {@code Flag} with the following pre-specified parameters: type - * {@code type}, short description {@code abbreviations}, and {@code value}. - * - * @param property the {@code NumericProperty} parameter containing the {@code NumericPropertyKeyword} identifier - * @param value the {@code boolean} value of this {@code flag} - */ - - public Flag(NumericProperty property, boolean value) { - this.index = property.getType(); - this.descriptor = property.getDescriptor(true); - this.value = value; - } - - /** - * A static method for converting enabled flags to a {@code List} of - * {@code NumericPropertyKeyword}s. Each keyword in this list corresponds to an - * enabled flag in the {@code flags} {@code List}. - * - * @param flags the list of flags that needs to be analysed - * @return a list of {@code NumericPropertyKeyword}s corresponding to enabled - * {@code flag}s. - */ - - public static List convert(List flags) { - var filtered = flags.stream().filter(flag -> (boolean) flag.getValue()); - return filtered.map(flag -> flag.getType()).collect(Collectors.toList()); - } - - /** - * The default list of {@code Flag}s used in finding the reverse solution of the - * heat conduction problem contains: - * DIFFUSIVITY (true), HEAT_LOSS (true), MAXTEMP (true), BASELINE_INTERCEPT (false), BASELINE_SLOPE (false) . - * - * @return a {@code List} of default {@code Flag}s - */ - - public static List allProblemDependentFlags() { - return defaultList().stream().filter(p -> p.isAutoAdjustable()).map(p -> new Flag(p, p.isDefaultSearchVariable())) - .collect(Collectors.toList()); - } - - public static List allProblemIndependentFlags() { - List flags = new ArrayList<>(); - flags.add(new Flag(def(TIME_SHIFT), false)); - flags.add(new Flag(def(LOWER_BOUND), false)); - flags.add(new Flag(def(UPPER_BOUND), false)); - return flags; - } - - /** - * Returns the type of this {@code Flag}. - * - * @return a {@code NumericPropertyKeyword} representing the type of this - * {@code Flag}. - */ - - public NumericPropertyKeyword getType() { - return index; - } - - /** - * Creates a new {@code Flag} object based on this {@code Flag}, but with a - * different {@code value}. - * - * @param value either {@code true} or {@code false} - * @return a {@code Flag} that replicates the {@code type} and - * {@code abbreviation} of this {@code Flag}, but sets a new - * {@code value} - */ - - public Flag derive(boolean value) { - return new Flag(def(index), value); - } - - /** - * Creates a short description for the GUI. - */ - - @Override - public String getDescriptor(boolean addHtmlTags) { - return addHtmlTags ? "Search for " + descriptor + "" : "Search for " + descriptor; - } - - /** - * The value for this {@code Property} is a {@code boolean}. - */ - - @Override - public Object getValue() { - return value; - } - - public void setValue(boolean value) { - this.value = (boolean) value; - } - - @Override - public String toString() { - return getClass().getSimpleName() + ": " + index.name(); - } - - public String abbreviation(boolean addHtmlTags) { - return addHtmlTags ? "" + descriptor + "" : descriptor; - } - - public void setAbbreviation(String abbreviation) { - this.descriptor = abbreviation; - } - - @Override - public Object identifier() { - return index; - } - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (o == null) - return false; - - if (!(o instanceof Flag)) - return false; - - Flag f = (Flag) o; - - if (f.getType() == this.getType()) { - if (f.getValue().equals(this.getValue())) { - return true; - } - } - - return false; - - } - - @Override - public boolean attemptUpdate(Object value) { - // TODO Auto-generated method stub - return false; - } - - public static List selectActive(List flags) { - return flags.stream().filter(flag -> (boolean) flag.getValue()).collect(Collectors.toList()); - } - -} \ No newline at end of file + private static final long serialVersionUID = 4927536419752406797L; + private NumericPropertyKeyword index; + private boolean value; + private String descriptor; + + /** + * Creates a {@code Flag} with the type {@code type}. The default + * {@code value} is set to {@code false}. + * + * @param type the {@code NumericPropertyKeyword} associated with this + * {@code Flag} + */ + 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 + * {@code type}, short description {@code abbreviations}, and {@code value}. + * + * @param property the {@code NumericProperty} parameter containing the + * {@code NumericPropertyKeyword} identifier + * @param value the {@code boolean} value of this {@code flag} + */ + public Flag(NumericProperty property, boolean value) { + this.index = property.getType(); + this.descriptor = property.getDescriptor(true); + this.value = value; + } + + /** + * A static method for converting enabled flags to a {@code List} of + * {@code NumericPropertyKeyword}s. Each keyword in this list corresponds to + * an enabled flag in the {@code flags} {@code List}. + * + * @param flags the list of flags that needs to be analysed + * @return a list of {@code NumericPropertyKeyword}s corresponding to + * enabled {@code flag}s. + */ + public static List convert(List flags) { + var filtered = flags.stream().filter(flag -> (boolean) flag.getValue()); + return filtered.map(flag -> flag.getType()).collect(Collectors.toList()); + } + + /** + * List of all possible {@code Flag}s that can be used in finding the + * reverse solution of the heat conduction problems. Includes all flags that + * correspond to {@code NumericPropert}ies satisfying + * {@code p.isOptimisable() = true}. The default value of the flag is set to + * {@code p.isDefaultSearchVariable()} -- based on the information contained + * in the {@code NumericProperty.xml} file. + * + * @return a {@code List} of all possible {@code Flag}s + * @see + */ + public static List allFlags() { + return NumericProperties.defaultList().stream() + .filter(p -> p.isOptimisable()) + .map(p -> new Flag(p, p.isDefaultSearchVariable())) + .collect(Collectors.toList()); + } + + /** + * Returns the type of this {@code Flag}. + * + * @return a {@code NumericPropertyKeyword} representing the type of this + * {@code Flag}. + */ + public NumericPropertyKeyword getType() { + return index; + } + + /** + * Creates a new {@code Flag} object based on this {@code Flag}, but with a + * different {@code value}. + * + * @param value either {@code true} or {@code false} + * @return a {@code Flag} that replicates the {@code type} and + * {@code abbreviation} of this {@code Flag}, but sets a new {@code value} + */ + public Flag derive(boolean value) { + return new Flag(def(index), value); + } + + /** + * Creates a short description for the GUI. + */ + @Override + public String getDescriptor(boolean addHtmlTags) { + return addHtmlTags ? "Search for " + descriptor + "" : "Search for " + descriptor; + } + + /** + * The value for this {@code Property} is a {@code boolean}. + */ + @Override + public Object getValue() { + return value; + } + + public void setValue(boolean value) { + this.value = (boolean) value; + } + + @Override + public String toString() { + return getClass().getSimpleName() + ": " + index.name(); + } + + public String abbreviation(boolean addHtmlTags) { + return addHtmlTags ? "" + descriptor + "" : descriptor; + } + + public void setAbbreviation(String abbreviation) { + this.descriptor = abbreviation; + } + + @Override + public Object identifier() { + return index; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (o == null) { + return false; + } + + if (!(o instanceof Flag)) { + return false; + } + + Flag f = (Flag) o; + + return (f.getType() == this.getType()) + && (f.getValue().equals(this.getValue())); + + } + + @Override + public boolean attemptUpdate(Object value) { + // TODO Auto-generated method stub + return false; + } + + public static List selectActive(List flags) { + return flags.stream().filter(flag -> (boolean) flag.getValue()).collect(Collectors.toList()); + } + +} diff --git a/src/main/java/pulse/properties/NumericProperties.java b/src/main/java/pulse/properties/NumericProperties.java index 9be3c48f..3f0f5ff1 100644 --- a/src/main/java/pulse/properties/NumericProperties.java +++ b/src/main/java/pulse/properties/NumericProperties.java @@ -1,198 +1,122 @@ package pulse.properties; -import java.text.DecimalFormat; -import java.text.NumberFormat; import java.util.List; import pulse.io.export.XMLConverter; -import pulse.ui.Messages; /** * Default operations with NumericProperties * */ - public class NumericProperties { - /** - * The list of default properties read that is created by reading the default - * {@code .xml} file. - */ - - private final static List DEFAULT = XMLConverter.readDefaultXML(); - - private NumericProperties() { - //empty constructor - } - - /** - * Checks whether the {@code val} that is going to be passed to the - * {@code property} (a) has the same type as the {@code property.getValue()} - * object; (b) is confined within the definition domain: - * {@code minimum <= value <= maximum}. - * - * @param property the {@code property} containing the definition domain - * @param val a numeric value, the conformity of which to the definition - * domain needs to be checked - * @return {@code true} if {@code minimum <= val <= maximum} and if both - * {@code val} and {@code value} are instances of the same - * {@code class}; {@code false} otherwise - */ - - public static boolean isValueSensible(NumericProperty property, Number val) { - if (!property.getValue().getClass().equals(val.getClass())) - return false; - - double v = val.doubleValue(); - - final double EPS = 1E-12; - - if (v > property.getMaximum().doubleValue() + EPS) - return false; - - return v >= property.getMinimum().doubleValue() - EPS; - - } - - public static String printRangeAndNumber(NumericProperty p, Number value) { - StringBuilder msg = new StringBuilder(); - msg.append("Acceptable region for "); - msg.append("parameter : "); - msg.append(p.getValue().getClass().getSimpleName()); - msg.append(" [ " + p.getMinimum()); - msg.append(" : " + p.getMaximum() + " ]. "); - msg.append("Value received: " + value); - return msg.toString(); - } - - public static NumberFormat numberFormat(NumericProperty p, boolean convertDimension) { - var value = p.getValue(); - - if (value instanceof Integer) - return NumberFormat.getIntegerInstance(); - - double adjustedValue = convertDimension ? (double) value * p.getDimensionFactor().doubleValue() - : (double) value; - double absAdjustedValue = Math.abs(adjustedValue); - - final double UPPER_LIMIT = 1e4; // the upper limit, used for formatting - final double LOWER_LIMIT = 1e-2; // the lower limit, used for formatting - final double ZERO = 1e-30; - - if ((absAdjustedValue > UPPER_LIMIT) || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO)) - return new DecimalFormat(Messages.getString("NumericProperty.BigNumberFormat")); - else - return new DecimalFormat(Messages.getString("NumericProperty.NumberFormat")); - } - - public static List defaultList() { - return DEFAULT; - } - - /** - * Searches for the default {@code NumericProperty} corresponding to - * {@code keyword} in the list of pre-defined properties loaded from the - * respective {@code .xml} file. - * - * @param keyword one of the constant {@code NumericPropertyKeyword}s - * @return a {@code NumericProperty} in the default list of properties - * @see pulse.properties.NumericPropertyKeyword - */ - - public static NumericProperty def(NumericPropertyKeyword keyword) { - return new NumericProperty(DEFAULT.stream().filter(p -> p.getType() == keyword).findFirst().get()); - } - - /** - * Compares the numeric values of this {@code NumericProperty} and {@code arg0} - * - * @param a a {@code NumericProperty} - * @param b another {@code NumericProperty} - * @return {@code true} if the values are equals - */ - - 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; - - return Math.abs(d1 - d2) < eps ? 0 : d1.compareTo(d2); - } - - /** - * 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 - * {@NumericProperty} which will replicate all field of the latter, but will set - * its value to {@code value}. - * - * @param keyword one of the constant {@code NumericPropertyKeyword}s - * @param value the new value for the created {@code NumericProperty} - * @return a new {@code NumericProperty} that is built according to the default - * pattern specified by the {@code keyword}, but with a different - * {@code value} - * @see pulse.properties.NumericPropertyKeyword - */ - - public static NumericProperty derive(NumericPropertyKeyword keyword, Number value) { - return new NumericProperty(value, DEFAULT.stream().filter(p -> p.getType() == keyword).findFirst().get()); - } - - /** - * Used to print out a nice {@code value} for GUI applications and for - * exporting. - *

- * Will use a {@code DecimalFormat} to reduce the number of digits, if - * neccessary. Automatically detects whether it is dealing with {@code int} or - * {@code double} values, and adjust formatting accordingly. If - * {@code error != null}, will use the latter as the error value, which is - * separated from the main value by a plus-minus sign. - *

- * - * @param convertDimension if {@code true}, the output will be the - * {@code value * dimensionFactor} - * @return a nice {@code String} representing the {@code value} of this - * {@code NumericProperty} and its {@code error} - */ - - public static String formattedValueAndError(NumericProperty p, boolean convertDimension) { - - if (p.getValue() instanceof Integer) { - Number val = convertDimension ? ((Number) p.getValue()).intValue() * p.getDimensionFactor().intValue() - : ((Number) p.getValue()).intValue(); - return (NumberFormat.getIntegerInstance()).format(val); - } - - final String PLUS_MINUS = Messages.getString("NumericProperty.PlusMinus"); - - final double UPPER_LIMIT = 1e4; // the upper limit, used for formatting - final double LOWER_LIMIT = 1e-2; // the lower limit, used for formatting - final double ZERO = 1e-30; - - double adjustedValue = convertDimension ? p.valueInCurrentUnits().doubleValue() : (double) p.getValue(); - - double absAdjustedValue = Math.abs(adjustedValue); - - DecimalFormat selectedFormat = null; - - if ((absAdjustedValue > UPPER_LIMIT) || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO)) - selectedFormat = new DecimalFormat(Messages.getString("NumericProperty.BigNumberFormat")); - else - selectedFormat = new DecimalFormat(Messages.getString("NumericProperty.NumberFormat")); - - if (p.getError() != null) - return selectedFormat.format(adjustedValue) + PLUS_MINUS - + selectedFormat - .format(convertDimension ? (double) p.getError() * p.getDimensionFactor().doubleValue() - : (double) p.getError()); - else - return selectedFormat.format(adjustedValue); - - } - - public static boolean isDiscrete(NumericPropertyKeyword key) { - return def(key).isDiscrete(); - } - -} \ No newline at end of file + /** + * The list of default properties read that is created by reading the + * default {@code .xml} file. + */ + private final static List DEFAULT = XMLConverter.readDefaultXML(); + + private NumericProperties() { + //empty constructor + } + + /** + * Checks whether the {@code val} that is going to be passed to the + * {@code property} (a) has the same type as the {@code property.getValue()} + * object; (b) is confined within the definition domain: + * {@code minimum <= value <= maximum}. + * + * @param property the {@code property} containing the definition domain + * @param val a numeric value, the conformity of which to the definition + * domain needs to be checked + * @return {@code true} if {@code minimum <= val <= maximum} and if both + * {@code val} and {@code value} are instances of the same {@code class}; + * {@code false} otherwise + */ + public static boolean isValueSensible(NumericProperty property, Number val) { + if (!property.getValue().getClass().equals(val.getClass())) { + return false; + } + + double v = val.doubleValue(); + final double EPS = 1E-12; + boolean ok = true; + + if (!Double.isFinite(v) + || v > property.getMaximum().doubleValue() + EPS + || v < property.getMinimum().doubleValue() - EPS) { + ok = false; + } + + return ok; + } + + public static String printRangeAndNumber(NumericProperty p, Number value) { + StringBuilder msg = new StringBuilder(); + msg.append("Acceptable region for "); + msg.append("parameter : "); + msg.append(p.getValue().getClass().getSimpleName()); + msg.append(" [ ").append(p.getMinimum()); + msg.append(" : ").append(p.getMaximum()).append(" ]. "); + msg.append("Value received: ").append(value); + return msg.toString(); + } + + public static List defaultList() { + return DEFAULT; + } + + /** + * Searches for the default {@code NumericProperty} corresponding to + * {@code keyword} in the list of pre-defined properties loaded from the + * respective {@code .xml} file. + * + * @param keyword one of the constant {@code NumericPropertyKeyword}s + * @return a {@code NumericProperty} in the default list of properties + * @see pulse.properties.NumericPropertyKeyword + */ + public static NumericProperty def(NumericPropertyKeyword keyword) { + return new NumericProperty(DEFAULT.stream().filter(p -> p.getType() == keyword).findFirst().get()); + } + + /** + * Compares the numeric values of this {@code NumericProperty} and + * {@code arg0} + * + * @param a a {@code NumericProperty} + * @param b another {@code NumericProperty} + * @return {@code true} if the values are equals + */ + 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 * Math.abs(d1 + d2) / 2.0; + + return Math.abs(d1 - d2) < eps ? 0 : d1.compareTo(d2); + } + + /** + * 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 { + * + * @NumericProperty} which will replicate all field of the latter, but will + * set its value to {@code value}. + * + * @param keyword one of the constant {@code NumericPropertyKeyword}s + * @param value the new value for the created {@code NumericProperty} + * @return a new {@code NumericProperty} that is built according to the + * default pattern specified by the {@code keyword}, but with a different + * {@code value} + * @see pulse.properties.NumericPropertyKeyword + */ + public static NumericProperty derive(NumericPropertyKeyword keyword, Number value) { + return new NumericProperty(value, DEFAULT.stream().filter(p -> p.getType() == keyword).findFirst().get()); + } + + public static boolean isDiscrete(NumericPropertyKeyword key) { + return def(key).isDiscrete(); + } + +} diff --git a/src/main/java/pulse/properties/NumericProperty.java b/src/main/java/pulse/properties/NumericProperty.java index 27939415..495d1a18 100644 --- a/src/main/java/pulse/properties/NumericProperty.java +++ b/src/main/java/pulse/properties/NumericProperty.java @@ -1,10 +1,9 @@ package pulse.properties; +import pulse.math.Segment; import static pulse.properties.NumericProperties.compare; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericProperties.formattedValueAndError; import static pulse.properties.NumericProperties.isValueSensible; -import static pulse.properties.NumericProperties.numberFormat; import static pulse.properties.NumericProperties.printRangeAndNumber; /** @@ -17,287 +16,335 @@ * {@code NuemricPropertyKeyword}. The latter is used to link with a repository * of default {@code NumericPropert}ies loaded from an {@code .xml} file. *

- * + * * @see pulse.properties.NumericPropertyKeyword * @see pulse.io.export.XMLConverter */ - public class NumericProperty implements Property, Comparable { - private Number value; - - private Number minimum; - private Number maximum; - - private String descriptor; - private String abbreviation; - - private Number dimensionFactor; - private Number error; - - private NumericPropertyKeyword type; - - private boolean autoAdjustable; - private boolean discreet; - private boolean defaultSearchVariable; - - /** - * Creates a {@code NumericProperty} based on {@pattern} and assigns - * {@code value} as its value. - * - * @param value the {@code} for the {@NumericProperty} that is otherwise equal - * to {@code pattern} - * @param pattern a valid {@code NumericProperty} - */ - - protected NumericProperty(Number value, NumericProperty pattern) { - this(pattern); - this.value = value; - } - - /** - * Constructor used by {@code XMLConverter} to create a {@code NumericProperty} - * with fully specified set of parameters - * - * @param type the type of this {@code NumericProperty}, set by one - * of the {@code NumericPropertyKeyword} constants - * @param params the numeric parameters in the following order: value, minimum, maximum, dimension factor. - * @see pulse.io.export.XMLConverter - */ - - public NumericProperty(NumericPropertyKeyword type, Number... params) { - if(params.length != 4) - throw new IllegalArgumentException("Input array must be of length 4. Received: " + params.length); - - this.type = type; - this.value = params[0]; - this.dimensionFactor = params[3]; - setDomain(params[1], params[2]); - } - - /** - * A copy constructor for {@code NumericProperty} - * - * @param num another {@code NumericProperty} that is going to be replicated - */ - - public NumericProperty(NumericProperty num) { - this.value = num.value; - this.descriptor = num.descriptor; - this.abbreviation = num.abbreviation; - this.minimum = num.minimum; - this.maximum = num.maximum; - this.type = num.type; - this.dimensionFactor = num.dimensionFactor; - this.autoAdjustable = num.autoAdjustable; - } - - public NumericPropertyKeyword getType() { - return type; - } - - @Override - public Object getValue() { - return value; - } - - public boolean validate() { - return isValueSensible(this, value); - } - - /** - * Sets the {@code value} of this {@code NumericProperty} -- if and only if the - * new {@code value} is confined within the definition domain for this - * {@code NumericProperty}. Checks whether - * - * @param value the value to be set to {@code this property} - * @see NumericProperties.isValueSensible(NumericProperty,Number) - */ - - public void setValue(Number value) { - - if (!validate()) - throw new IllegalArgumentException(printRangeAndNumber(this, value)); - - this.value = value; - - } - - /** - * Sets the definition domain for this {@code NumericProperty}. - * - * @param minimum the minimum value - * @param maximum the maximum value - * @throws IllegalArgumentException if any two of - * {@code minimum, maximum, or this.value} have - * different primitive types (e.g. a - * {@code double} and an {@code int}). - */ - - public void setDomain(Number minimum, Number maximum) throws IllegalArgumentException { - var minClass = minimum.getClass(); - var maxClass = maximum.getClass(); - if (!minClass.equals(maxClass)) - throw new IllegalArgumentException( - "Types of minimum and maximum do not match: " + minClass + " and " + maxClass); - if (!minClass.equals(value.getClass())) - throw new IllegalArgumentException("Interrupted attempt of setting " + minClass.getSimpleName() - + " boundaries to a " + value.getClass().getSimpleName() + " property"); - this.minimum = minimum; - this.maximum = maximum; - } - - public Number getMinimum() { - return minimum; - } - - public Number getMaximum() { - return maximum; - } - - /** - * Prints out the {@code type} and {@code value} of this - * {@code NumericProperty}. - */ - - @Override - public String toString() { - return (type + " = " + formattedValueAndError(this, false)); - } - - /** - * Calls {@code formattedValue(true)}. - * - * @see NumericProperties.formattedValueAndError(boolean) - */ - - @Override - public String formattedOutput() { - return formattedValueAndError(this, true); - } - - public String valueOutput() { - return numberFormat(this, true).format(valueInCurrentUnits()); - } - - public String errorOutput() { - return numberFormat(this, true).format(errorInCurrentUnits()); - } - - public Number valueInCurrentUnits() { - return value instanceof Double ? (double) value * dimensionFactor.doubleValue() : (int) value; - } - - public double errorInCurrentUnits() { - return error == null ? 0.0 : (double) error * dimensionFactor.doubleValue(); - } - - public Number getDimensionFactor() { - return dimensionFactor; - } - - public void setDimensionFactor(Number dimensionFactor) { - this.dimensionFactor = dimensionFactor; - } - - public void setAutoAdjustable(boolean autoAdjustable) { - this.autoAdjustable = autoAdjustable; - } - - public boolean isAutoAdjustable() { - return autoAdjustable; - } - - public Number getError() { - return error; - } - - public void setError(Number error) { - this.error = error; - } - - @Override - public String getDescriptor(boolean addHtmlTag) { - return addHtmlTag ? "" + descriptor + "" : descriptor; - } - - public void setDescriptor(String descriptor) { - this.descriptor = descriptor; - } - - public String getAbbreviation(boolean addHtmlTags) { - return addHtmlTags ? "" + abbreviation + "" : abbreviation; - } - - public void setAbbreviation(String abbreviation) { - this.abbreviation = abbreviation; - } - - /** - * The {@code Object} o is considered to be equal to this - * {@code NumericProperty} if (a) it is of the same class; (b) its value is the - * same as for this {@code NumericProperty}, and (c) if it is specified by the - * same {@code NumericPropertyKeyword}. - */ - - @Override - public boolean equals(Object o) { - if (o == null) - return false; - - if (o == this) - return true; - - if (!(o instanceof NumericProperty)) - return false; - - NumericProperty onp = (NumericProperty) o; - - if (onp.getType() != this.getType()) - return false; - - return compare(this, onp) == 0; - - } - - @Override - public int compareTo(NumericProperty arg0) { - final int result = this.getType().compareTo(arg0.getType()); - return result != 0 ? result : compare(this, arg0); - } - - public boolean isDiscrete() { - return discreet; - } - - public void setDiscreet(boolean discreet) { - this.discreet = discreet; - } - - @Override - public boolean attemptUpdate(Object value) { - if (!(value instanceof Number)) - return false; - - if (!(derive(this.getType(), (Number) value).validate())) - return false; - - this.value = (Number) value; - return true; - - } - - public static void requireType(NumericProperty property, NumericPropertyKeyword type) { - if (property.getType() != type) - throw new IllegalArgumentException("Illegal type: " + property.getType()); - } - - public boolean isDefaultSearchVariable() { - return defaultSearchVariable; - } - - public void setDefaultSearchVariable(boolean defaultSearchVariable) { - this.defaultSearchVariable = defaultSearchVariable; - } - -} \ No newline at end of file + /** + * + */ + private static final long serialVersionUID = -7132274623596750984L; + + private Number value; + + private Number minimum; + private Number maximum; + + private String descriptor; + private String abbreviation; + + private Number dimensionFactor; + private Number error; + + private NumericPropertyKeyword type; + private NumericPropertyKeyword[] excludes; + + private boolean autoAdjustable; + private boolean discrete; + private boolean defaultSearchVariable; + private boolean optimisable; + + /** + * Creates a {@code NumericProperty} based on { + * + * @pattern} and assigns {@code value} as its value. + * + * @param value the {@code} for the { + * @NumericProperty} that is otherwise equal to {@code pattern} + * @param pattern a valid {@code NumericProperty} + */ + protected NumericProperty(Number value, NumericProperty pattern) { + this(pattern); + this.value = value; + } + + /** + * Constructor used by {@code XMLConverter} to create a + * {@code NumericProperty} with fully specified set of parameters + * + * @param type the type of this {@code NumericProperty}, set by one of the + * {@code NumericPropertyKeyword} constants + * @param params the numeric parameters in the following order: value, + * minimum, maximum, dimension factor. + * @see pulse.io.export.XMLConverter + */ + public NumericProperty(NumericPropertyKeyword type, Number... params) { + if (params.length != 4) { + throw new IllegalArgumentException("Input array must be of length 4. Received: " + params.length); + } + + this.type = type; + this.value = params[0]; + this.dimensionFactor = params[3]; + this.excludes = new NumericPropertyKeyword[0]; + setDomain(params[1], params[2]); + } + + /** + * A copy constructor for {@code NumericProperty} + * + * @param num another {@code NumericProperty} that is going to be replicated + */ + public NumericProperty(NumericProperty num) { + this.value = num.value; + this.descriptor = num.descriptor; + this.abbreviation = num.abbreviation; + this.minimum = num.minimum; + this.maximum = num.maximum; + this.type = num.type; + this.discrete = num.discrete; + this.dimensionFactor = num.dimensionFactor; + this.autoAdjustable = num.autoAdjustable; + this.error = num.error; + this.defaultSearchVariable = num.defaultSearchVariable; + this.optimisable = num.optimisable; + this.excludes = num.excludes; + } + + public NumericPropertyKeyword[] getExcludeKeywords() { + return excludes; + } + + public void setExcludeKeywords(NumericPropertyKeyword[] keys) { + this.excludes = keys; + } + + public NumericPropertyKeyword getType() { + return type; + } + + @Override + public Object getValue() { + return value; + } + + public boolean validate() { + return isValueSensible(this, value); + } + + /** + * Sets the {@code value} of this {@code NumericProperty} -- if and only if + * the new {@code value} is confined within the definition domain for this + * {@code NumericProperty}. Checks whether + * + * @param value the value to be set to {@code this property} + * @see NumericProperties.isValueSensible(NumericProperty,Number) + */ + public void setValue(Number value) { + + Number oldValue = this.value; + this.value = value; + + if (!validate()) { + this.value = oldValue; + throw new IllegalArgumentException(printRangeAndNumber(this, value)); + } + + } + + /** + * Sets the definition domain for this {@code NumericProperty}. + * + * @param minimum the minimum value + * @param maximum the maximum value + * @throws IllegalArgumentException if any two of + * {@code minimum, maximum, or this.value} have different primitive types + * (e.g. a {@code double} and an {@code int}). + */ + public void setDomain(Number minimum, Number maximum) throws IllegalArgumentException { + var minClass = minimum.getClass(); + var maxClass = maximum.getClass(); + if (!minClass.equals(maxClass)) { + throw new IllegalArgumentException( + "Types of minimum and maximum do not match: " + minClass + " and " + maxClass); + } + if (!minClass.equals(value.getClass())) { + throw new IllegalArgumentException("Interrupted attempt of setting " + minClass.getSimpleName() + + " boundaries to a " + value.getClass().getSimpleName() + " property"); + } + this.minimum = minimum; + this.maximum = maximum; + } + + public Number getMinimum() { + return minimum; + } + + public Number getMaximum() { + return maximum; + } + + /** + * Prints out the {@code type} and {@code value} of this + * {@code NumericProperty}. + */ + @Override + public String toString() { + return type + " = " + formattedOutput(); + } + + public Number valueInCurrentUnits() { + return value instanceof Double ? (double) value * dimensionFactor.doubleValue() + + getDimensionDelta().doubleValue() : (int) value; + } + + public Number errorInCurrentUnits() { + return error == null ? 0.0 : error.doubleValue() * dimensionFactor.doubleValue(); + } + + public Number getDimensionFactor() { + return dimensionFactor; + } + + public void setDimensionFactor(Number dimensionFactor) { + this.dimensionFactor = dimensionFactor; + } + + public void setVisibleByDefault(boolean autoAdjustable) { + this.autoAdjustable = autoAdjustable; + } + + public boolean isVisibleByDefault() { + return autoAdjustable; + } + + public Number getError() { + return error; + } + + public void setError(Number error) { + this.error = error; + } + + @Override + public String getDescriptor(boolean addHtmlTag) { + return addHtmlTag ? "" + descriptor + "" : descriptor; + } + + public void setDescriptor(String descriptor) { + this.descriptor = descriptor; + } + + public String getAbbreviation(boolean addHtmlTags) { + return addHtmlTags ? "" + abbreviation + "" : abbreviation; + } + + public void setAbbreviation(String abbreviation) { + this.abbreviation = abbreviation; + } + + /** + * The {@code Object} o is considered to be equal to this + * {@code NumericProperty} if (a) it is of the same class; (b) its value is + * the same as for this {@code NumericProperty}, and (c) if it is specified + * by the same {@code NumericPropertyKeyword}. + */ + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + + if (o == this) { + return true; + } + + if (!(o instanceof NumericProperty)) { + return false; + } + + NumericProperty onp = (NumericProperty) o; + + if (onp.getType() != this.getType()) { + return false; + } + + return compare(this, onp) == 0; + + } + + @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); + } + + public boolean isDiscrete() { + return discrete; + } + + public void setDiscrete(boolean discrete) { + this.discrete = discrete; + } + + @Override + public boolean attemptUpdate(Object value) { + if (!(value instanceof Number)) { + return false; + } + + if (!(derive(this.getType(), (Number) value).validate())) { + return false; + } + + this.value = (Number) value; + return true; + + } + + public static void requireType(NumericProperty property, NumericPropertyKeyword type) { + if (property.getType() != type) { + throw new IllegalArgumentException("Illegal type: " + property.getType()); + } + } + + public boolean isDefaultSearchVariable() { + return defaultSearchVariable; + } + + public boolean isOptimisable() { + return optimisable; + } + + public void setDefaultSearchVariable(boolean defaultSearchVariable) { + this.defaultSearchVariable = defaultSearchVariable; + } + + public void setOptimisable(boolean optimisable) { + this.optimisable = optimisable; + } + + public Number getDimensionDelta() { + if (type == NumericPropertyKeyword.TEST_TEMPERATURE) { + return -273.15; + } 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. + * + * @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 + */ + public String formattedOutput() { + var num = new NumericPropertyFormatter(this, true, true); + return num.numberFormat(this).format(value); + } + +} diff --git a/src/main/java/pulse/properties/NumericPropertyFormatter.java b/src/main/java/pulse/properties/NumericPropertyFormatter.java new file mode 100644 index 00000000..065f0602 --- /dev/null +++ b/src/main/java/pulse/properties/NumericPropertyFormatter.java @@ -0,0 +1,217 @@ +/* + * 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.properties; + +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParseException; +import javax.swing.JFormattedTextField.AbstractFormatter; +import javax.swing.text.NumberFormatter; +import pulse.math.Segment; +import pulse.ui.Messages; + +/** + * + * @author Artem Lunev + */ +public class NumericPropertyFormatter extends AbstractFormatter { + + private static final long serialVersionUID = -7733589481239097566L; + private NumericPropertyKeyword key; + private Segment bounds; + private boolean convertDimension = true; + private boolean addHtmlTags = true; + + private final static String PLUS_MINUS = Messages.getString("NumericProperty.PlusMinus"); + + /** + * Start using scientific notations for number whose absolute values are + * larger than {@value UPPER_LIMIT}. + */ + public final static double UPPER_LIMIT = 1e4; + + /** + * Start using scientific notations for number whose absolute values are + * lower than {@value LOWER_LIMIT}. + */ + public final static double LOWER_LIMIT = 1e-2; // the lower limit, used for formatting + + private final static double ZERO = 1e-30; + + /** + * @param convertDimension if {@code true}, the output will be the + * {@code value * dimensionFactor} + */ + public NumericPropertyFormatter(NumericProperty p, boolean convertDimension, boolean addHtmlTags) { + this.key = p.getType(); + this.convertDimension = convertDimension; + this.bounds = p.getBounds(); + this.addHtmlTags = addHtmlTags; + } + + public NumberFormat numberFormat(NumericProperty p) { + Number value = (Number) p.getValue(); + NumberFormat f; + + if (value instanceof Integer) { + f = NumberFormat.getIntegerInstance(); + } else { + + double adjustedValue = convertDimension ? value.doubleValue() * p.getDimensionFactor().doubleValue() + : (double) value; + double absAdjustedValue = Math.abs(adjustedValue); + + if (addHtmlTags + && ((absAdjustedValue > UPPER_LIMIT) + || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO))) { + //format with scientific notations + f = new ScientificFormat(p.getDimensionFactor(), p.getDimensionDelta()); + } else { + //format "standard" numbers + f = new DecimalFormatImpl(p); + } + + } + + return f; + + } + + @Override + public Object stringToValue(String arg0) throws ParseException { + var nf = new NumberFormatter(); + Number n = (Number) nf.stringToValue(arg0); + this.setEditValid( + bounds.contains(n.doubleValue())); + return NumericProperties.derive(key, n); + } + + /** + * Used to print out a nice {@code value} for GUI applications and for + * exporting. + *

+ * Will use a {@code DecimalFormat} to reduce the number of digits, if + * necessary. Automatically detects whether it is dealing with {@code int} + * or {@code double} values, and adjust formatting accordingly. If + * {@code error != null}, will use the latter as the error value, which is + * separated from the main value by a plus-minus sign. + *

+ * + * @return a nice {@code String} representing the {@code value} of this + * {@code NumericProperty} and its {@code error} + */ + @Override + public String valueToString(Object o) throws ParseException { + if (o == null) { + return ""; + } + + if (!(o instanceof NumericProperty)) { + throw new IllegalArgumentException("Cannot format. Not a property: " + + o.getClass()); + } + + var p = (NumericProperty) o; + String result; + + if (Double.isInfinite( + ((Number) p.getValue()).doubleValue())) { + result = "∞"; + } else if (Double.isNaN( + ((Number) p.getValue()).doubleValue())) { + result = "unknown"; + } else { + + if (p.getError() != null) { + result = formatValueAndError(p); + } else { + result = formatValueOnly(p); + } + + } + + return addHtmlTags ? encloseInHtmlTags(result) : result; + + } + + private String encloseInHtmlTags(String s) { + return new StringBuffer("").append(s).append("").toString(); + } + + private String formatValueOnly(NumericProperty p) { + return numberFormat(p).format(p.getValue()); + } + + private String formatValueAndError(NumericProperty p) { + Number adjustedValue = ((Number) p.getValue()); + var selectedFormat = numberFormat(p); + String value = selectedFormat.format(adjustedValue); + String errorString = selectedFormat.format( + adjustedValue instanceof Double + ? (p.getError().doubleValue() - p.getDimensionDelta().doubleValue()) + : p.getError().intValue() - p.getDimensionDelta().intValue()); + return selectedFormat.format(adjustedValue) + PLUS_MINUS + errorString; + } + + public boolean isDimensionConverted() { + return convertDimension; + } + + public boolean areHtmlTagsAdded() { + return addHtmlTags; + } + + public Segment getBounds() { + return bounds; + } + + private class DecimalFormatImpl extends DecimalFormat { + + private final long dimensionDelta; + + public DecimalFormatImpl(NumericProperty p) { + super(); + dimensionDelta = p.getDimensionDelta().longValue(); + final int digits = p.getType() == NumericPropertyKeyword.TEST_TEMPERATURE ? 1 : 4; + setMinimumFractionDigits(digits); + setMaximumFractionDigits(digits); + + if (convertDimension) { + setMultiplier(p.getDimensionFactor().intValue()); + } + } + + @Override + public StringBuffer format(long arg0, StringBuffer arg1, FieldPosition arg2) { + return super.format( + //add delta (e.g. -273.15) + arg0 + dimensionDelta, + arg1, arg2); + } + + @Override + public StringBuffer format(double arg0, StringBuffer arg1, FieldPosition arg2) { + return super.format( + //add delta (e.g. -237.15) + arg0 + dimensionDelta, + arg1, arg2); + } + + //parse not needed for temperature since this is not changed + } + +} diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 0433e62c..344bdbea 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -4,483 +4,384 @@ import java.util.Optional; /** - * Contains a list of NumericProperty types recognised by the constituent modules of PULsE. + * Contains a list of NumericProperty types recognised by the constituent + * modules of PULsE. * */ - public enum NumericPropertyKeyword { - /** - * The thermal diffusivity of the sample. - */ - - DIFFUSIVITY, - - /** - * Not implemented yet. - */ - - COATING_DIFFUSIVITY, - - /** - * Sample thickness. - */ - - THICKNESS, - - /** - * Sample diameter. - */ - - DIAMETER, - - /** - * The maximum temperature, or the signal height -- if a relative temperature - * scale is used. - */ - - MAXTEMP, - - /** - * The number of points in the {@code HeatingCurve}. - */ - - NUMPOINTS, - - /** - * The precision parameter used to solve nonlinear problems. - */ - - NONLINEAR_PRECISION, - - /** - * Pulse width (time). - */ - - PULSE_WIDTH, - - /** - * Laser spot diameter. - */ - - SPOT_DIAMETER, - - /** - * Calculation time limit. - */ - - TIME_LIMIT, - - /** - * Grid (space partitioning) density. - */ - - GRID_DENSITY, - - /** - * Not implemented yet. - */ - - - SHELL_GRID_DENSITY, - - /** - * Not implemented yet. - */ - - - AXIAL_COATING_THICKNESS, - - /** - * Not implemented yet. - */ - - RADIAL_COATING_THICKNESS, - - /** - * Specific heat (Cp). - */ - - SPECIFIC_HEAT, - - /** - * Thermal conductivity. - */ - - CONDUCTIVITY, - - /** - * Emissivity of the sample (nonlinear problems). - */ - - EMISSIVITY, - - /** - * Density of the sample. - */ - - DENSITY, - - /** - * Absorbed energy (nonlinear problems). - */ - - LASER_ENERGY, - - /** - * Test temperature, at which the laser was fired at the sample. - */ - - TEST_TEMPERATURE, - - /** - * The resolution of linear search. - */ - - LINEAR_RESOLUTION, - - /** - * The accuracy of gradient calculation. - */ - - GRADIENT_RESOLUTION, - - /** - * The buffer size that is used to establish whether results are converging. - */ - - BUFFER_SIZE, - - /** - * The total error tolerance. - */ - - ERROR_TOLERANCE, - - /** - * The outer field of view diameter for the detector sighting. - */ - - FOV_OUTER, - - /** - * The inner field of view diameter for the detector sighting. - */ - - FOV_INNER, - - /** - * The baseline slope (for linear baselines). - */ - - BASELINE_SLOPE, - - /** - * Frequency of the sinusoidal baseline. - */ - - BASELINE_FREQUENCY, - - /** - * Phase shift of the sinusoidal baseline. - */ - - BASELINE_PHASE_SHIFT, - - /** - * Amplitude of the sinusoidal baseline. - */ - - BASELINE_AMPLITUDE, - - /** - * The baseline intercept value. - */ - - BASELINE_INTERCEPT, - - /** - * The factor used to convert squared grid spacing to time step. - */ - - TAU_FACTOR, - - /** - * The detector gain (amplification). - */ - - DETECTOR_GAIN, - - /** - * The detector iris (aperture). - */ - - DETECTOR_IRIS, - - /** - * The coefficient of heat losses from the side surface of the sample - * (two-dimensional problems). - */ - - HEAT_LOSS_SIDE, - - /** - * A general keyword for the coefficient of heat losses. Indicates primarily - * those on the front and rear faces. - */ - - HEAT_LOSS, - - /** - * Search iteration. - */ - - ITERATION, - - /** - * Task identifier. - */ - - IDENTIFIER, - - /** - * Iteration limit for reverse problem solution. - */ - - ITERATION_LIMIT, - - /** - * Dimensionless coefficient of laser energy absorption (γ0). - */ - - LASER_ABSORPTIVITY, - - /** - * Dimensionless coefficient of thermal radiation absorption. - */ - - THERMAL_ABSORPTIVITY, - - /** - * Reflectance of the sample (0 < R ≤ 1). - */ - - REFLECTANCE, - - /** - * A dimensionless coefficient in the radiation flux expression for the - * radiative heat transfer between the front and the rear (coated) surfaces. - * Used by the DiathermicMaterialProblem. - */ - - DIATHERMIC_COEFFICIENT, - - /** - * The Planck number. - */ - - PLANCK_NUMBER, - - /** - * The optical thickness of a material, equal to a product of its geometric thickness and the absorptivity. - */ - - OPTICAL_THICKNESS, - - /** - * Time shift (pulse sync) - */ - - TIME_SHIFT, - - /** - * Statistical significance. - */ - - SIGNIFICANCE, - - /** - * Statistical probability. - */ - - PROBABILITY, - - /** - * Optimiser statistic (usually, RSS). - */ - - OPTIMISER_STATISTIC, - - /** - * Test statistic (e.g. normality test criterion). - */ - - TEST_STATISTIC, - - /** - * Lower calculation bound for optimiser. - */ - - LOWER_BOUND, - - /** - * Upper calculation bound for optimiser. - */ - - UPPER_BOUND, - - /** - * Averaging window. - */ - - WINDOW, - - /** - * Intensity of incident radiation. - */ - - INCIDENT_INTENSITY, - - /** - * Threshold above which properties are thought to be strongly correlated. - */ - - CORRELATION_THRESHOLD, - - /** - * Number of subdivisions for numeric integration. - */ - - INTEGRATION_SEGMENTS, - - /** - * Cutoff for numeric integration. - */ - - INTEGRATION_CUTOFF, - - /** - * Weight of the semi-implicit finite-difference scheme. - */ - - SCHEME_WEIGHT, - - /** - * Number of quadrature points (RTE). - */ - - QUADRATURE_POINTS, - - /** - * Albedo of single scattering. - */ - - SCATTERING_ALBEDO, - - /** - * Anisotropy coefficient for the phase function of scattering. - */ - - SCATTERING_ANISOTROPY, - - /** - * Iteration error tolerance in DOM calculations. - */ - - DOM_ITERATION_ERROR, - - /** - * Number of independent directions (DOM). - */ - - DOM_DIRECTIONS, - - /** - * Error tolerance for the Laguerre solver (RTE). - */ - - LAGUERRE_SOLVER_ERROR, - - /** - * Absolute tolerance (atol) for RK calculations (RTE). - */ - - ATOL, - - /** - * Relative tolerance (atol) for RK calculations (RTE). - */ - - RTOL, - - /** - * Grid scaling factor. - */ - - GRID_SCALING_FACTOR, - - /** - * Internal DOM grid density (RTE). - */ - - DOM_GRID_DENSITY, - - /** - * Grid stretching factor (RTE). - */ - - GRID_STRETCHING_FACTOR, - - /** - * Relaxation parameter of iterative solver (RTE). - */ - - RELAXATION_PARAMETER, - - /** - * Iteration threshold for RTE calculations. - */ - - RTE_MAX_ITERATIONS, - - /** - * Maximum allowed time spent on integration (RTE). - */ - - RTE_INTEGRATION_TIMEOUT, - - /** - * Percentage of initial (rise) segment of the pulse trapezoid. - */ - - TRAPEZOIDAL_RISE_PERCENTAGE, - - /** - * Percentage of final (fall) segment of the pulse trapezoid. - */ - - TRAPEZOIDAL_FALL_PERCENTAGE, - - /** - * μ parameter for skewed normal distribution. - */ - - SKEW_MU, - - /** - * σ parameter for skewed normal distribution. - */ - - SKEW_SIGMA, - - /** - * λ parameter for skewed normal distribution. - */ - - SKEW_LAMBDA; - - public static Optional findAny(String key) { - return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); - } - -} \ No newline at end of file + /** + * The thermal diffusivity of the sample. + */ + DIFFUSIVITY, + /** + * Not implemented yet. + */ + COATING_DIFFUSIVITY, + /** + * Sample thickness. + */ + THICKNESS, + /** + * Sample diameter. + */ + DIAMETER, + /** + * The maximum temperature, or the signal height -- if a relative + * temperature scale is used. + */ + MAXTEMP, + /** + * The number of points in the {@code HeatingCurve}. + */ + NUMPOINTS, + /** + * The precision parameter used to solve nonlinear problems. + */ + NONLINEAR_PRECISION, + /** + * Pulse width (time). + */ + PULSE_WIDTH, + /** + * Laser spot diameter. + */ + SPOT_DIAMETER, + /** + * Calculation time limit. + */ + TIME_LIMIT, + /** + * Grid (space partitioning) density. + */ + GRID_DENSITY, + /** + * Not implemented yet. + */ + SHELL_GRID_DENSITY, + /** + * Not implemented yet. + */ + AXIAL_COATING_THICKNESS, + /** + * Not implemented yet. + */ + RADIAL_COATING_THICKNESS, + /** + * Specific heat (Cp). + */ + SPECIFIC_HEAT, + /** + * Thermal conductivity. + */ + CONDUCTIVITY, + /** + * Emissivity of the sample (nonlinear problems). + */ + EMISSIVITY, + /** + * Density of the sample. + */ + DENSITY, + /** + * Absorbed energy (nonlinear problems). + */ + LASER_ENERGY, + /** + * Test temperature, at which the laser was fired at the sample. + */ + TEST_TEMPERATURE, + /** + * The resolution of linear search. + */ + LINEAR_RESOLUTION, + /** + * The accuracy of gradient calculation. + */ + GRADIENT_RESOLUTION, + /** + * The buffer size that is used to establish whether results are converging. + */ + BUFFER_SIZE, + /** + * The total error tolerance. + */ + ERROR_TOLERANCE, + /** + * The outer field of view diameter for the detector sighting. + */ + FOV_OUTER, + /** + * The inner field of view diameter for the detector sighting. + */ + FOV_INNER, + /** + * The baseline slope (for linear baselines). + */ + BASELINE_SLOPE, + /** + * Frequency of the sinusoidal baseline. + */ + BASELINE_FREQUENCY, + /** + * Phase shift of the sinusoidal baseline. + */ + BASELINE_PHASE_SHIFT, + /** + * Amplitude of the sinusoidal baseline. + */ + BASELINE_AMPLITUDE, + /** + * The baseline intercept value. + */ + BASELINE_INTERCEPT, + /** + * The factor used to convert squared grid spacing to time step. + */ + TAU_FACTOR, + /** + * The detector gain (amplification). + */ + DETECTOR_GAIN, + /** + * The detector iris (aperture). + */ + DETECTOR_IRIS, + /** + * The coefficient of heat losses from the side surface of the sample + * (two-dimensional problems). + */ + HEAT_LOSS_SIDE, + /** + * The coefficient of heat losses from the front and rear surfaces of the + * 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. + * Biot numbers, will differ due to different areas of the side and + * front/rear surfaces. + */ + HEAT_LOSS_COMBINED, + /** + * Search iteration. + */ + ITERATION, + /** + * Task identifier. + */ + IDENTIFIER, + /** + * Iteration limit for reverse problem solution. + */ + ITERATION_LIMIT, + /** + * Dimensionless coefficient of laser energy absorption + * (γ0). + */ + LASER_ABSORPTIVITY, + /** + * Dimensionless coefficient of thermal radiation absorption. + */ + THERMAL_ABSORPTIVITY, + /** + * A directive to the optimiser informing both front and rear side + * absorptivities must be equal. + */ + COMBINED_ABSORPTIVITY, + /** + * Reflectance of the sample (0 < R ≤ 1). + */ + REFLECTANCE, + /** + * A dimensionless coefficient in the radiation flux expression for the + * radiative heat transfer between the front and the rear (coated) surfaces. + * Used by the DiathermicMaterialProblem. + */ + DIATHERMIC_COEFFICIENT, + /** + * The Planck number. + */ + PLANCK_NUMBER, + /** + * The optical thickness of a material, equal to a product of its geometric + * thickness and the absorptivity. + */ + OPTICAL_THICKNESS, + /** + * Time shift (pulse sync). + */ + TIME_SHIFT, + /** + * Statistical significance for calculating the critical value. + */ + SIGNIFICANCE, + /** + * Optimiser statistic (usually, RSS). + */ + OPTIMISER_STATISTIC, + /** + * Model selection criterion (AIC, BIC, etc.). + */ + MODEL_CRITERION, + /** + * Test statistic (e.g. normality test criterion). + */ + TEST_STATISTIC, + /** + * Lower calculation bound for optimiser. + */ + LOWER_BOUND, + /** + * Upper calculation bound for optimiser. + */ + UPPER_BOUND, + /** + * Averaging window. + */ + WINDOW, + /** + * Intensity of incident radiation. + */ + INCIDENT_INTENSITY, + /** + * Threshold above which properties are thought to be strongly correlated. + */ + CORRELATION_THRESHOLD, + /** + * Number of subdivisions for numeric integration. + */ + INTEGRATION_SEGMENTS, + /** + * Cutoff for numeric integration. + */ + INTEGRATION_CUTOFF, + /** + * Weight of the semi-implicit finite-difference scheme. + */ + SCHEME_WEIGHT, + /** + * Number of quadrature points (RTE). + */ + QUADRATURE_POINTS, + /** + * Albedo of single scattering. + */ + SCATTERING_ALBEDO, + /** + * Anisotropy coefficient for the phase function of scattering. + */ + SCATTERING_ANISOTROPY, + /** + * Iteration error tolerance in DOM calculations. + */ + DOM_ITERATION_ERROR, + /** + * Number of independent directions (DOM). + */ + DOM_DIRECTIONS, + /** + * Error tolerance for the Laguerre solver (RTE). + */ + LAGUERRE_SOLVER_ERROR, + /** + * Absolute tolerance (atol) for RK calculations (RTE). + */ + ATOL, + /** + * Relative tolerance (atol) for RK calculations (RTE). + */ + RTOL, + /** + * Grid scaling factor. + */ + GRID_SCALING_FACTOR, + /** + * Internal DOM grid density (RTE). + */ + DOM_GRID_DENSITY, + /** + * Grid stretching factor (RTE). + */ + GRID_STRETCHING_FACTOR, + /** + * Relaxation parameter of iterative solver (RTE). + */ + RELAXATION_PARAMETER, + /** + * Iteration threshold for RTE calculations. + */ + RTE_MAX_ITERATIONS, + /** + * Maximum allowed time spent on integration (RTE). + */ + RTE_INTEGRATION_TIMEOUT, + /** + * Percentage of initial (rise) segment of the pulse trapezoid. + */ + TRAPEZOIDAL_RISE_PERCENTAGE, + /** + * Percentage of final (fall) segment of the pulse trapezoid. + */ + TRAPEZOIDAL_FALL_PERCENTAGE, + /** + * μ parameter for skewed normal distribution. + */ + SKEW_MU, + /** + * σ parameter for skewed normal distribution. + */ + SKEW_SIGMA, + /** + * λ parameter for skewed normal distribution. + */ + SKEW_LAMBDA, + /** + * A weight indicating how good a calculation model is. + */ + MODEL_WEIGHT, + /** + * Levenberg-Marquardt damping ratio. A zero value presents pure Levenberg + * 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. + */ + 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, + /** + * 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 c371e635..0ff78043 100644 --- a/src/main/java/pulse/properties/Property.java +++ b/src/main/java/pulse/properties/Property.java @@ -1,46 +1,45 @@ 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 { - - /** - * Retrieves the value of this {@code Property}. - * - * @return an object representing the value of this {@code Property} - */ - - public Object getValue(); - - /** - * Formats the value so that it is suitable for output using the GUI or console. - * - * @return a formatted {@code String} representing the {@code value} - */ - - public default String formattedOutput() { - return getValue().toString(); - }; - - /** - * Creates a {@code String} to describe this property (often used in GUI - * applications). - * - * @param addHtmlTags if {@code true}, adds the 'html' tags at both ends of the - * descriptor {@code String}. - * @return a {@code String}, with or without 'html' tags, describing this - * {@code Property} - */ - - public String getDescriptor(boolean addHtmlTags); - - public boolean attemptUpdate(Object value); - - public default Object identifier() { - return getClass(); - } - -} \ No newline at end of file +public interface Property extends Serializable { + + /** + * Retrieves the value of this {@code Property}. + * + * @return an object representing the value of this {@code Property} + */ + public Object getValue(); + + /** + * Formats the value so that it is suitable for output using the GUI or + * console. + * + * @return a formatted {@code String} representing the {@code value} + */ + public default String formattedOutput() { + return getValue().toString(); + } + + /** + * Creates a {@code String} to describe this property (often used in GUI + * applications). + * + * @param addHtmlTags if {@code true}, adds the 'html' tags at both ends of + * the descriptor {@code String}. + * @return a {@code String}, with or without 'html' tags, describing this + * {@code Property} + */ + public String getDescriptor(boolean addHtmlTags); + + public boolean attemptUpdate(Object value); + + public default Object identifier() { + return getClass(); + } + +} diff --git a/src/main/java/pulse/properties/SampleName.java b/src/main/java/pulse/properties/SampleName.java index 55d2c625..0e355f2b 100644 --- a/src/main/java/pulse/properties/SampleName.java +++ b/src/main/java/pulse/properties/SampleName.java @@ -4,55 +4,66 @@ public class SampleName implements Property { - private String name; - - public SampleName() { - name = "Nameless"; - } - - @Override - public Object getValue() { - return name; - } - - @Override - public String getDescriptor(boolean addHtmlTags) { - return "Sample name"; - } - - @Override - public boolean attemptUpdate(Object value) { - Objects.requireNonNull(value); - - if (!(value instanceof String)) - throw new IllegalArgumentException( - "Illegal type: " + value.getClass().getSimpleName() + ". String expected."); - - final boolean result = !name.equals(value); - this.name = (String) value; - return result; - - } - - @Override - public String toString() { - return name; - } - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (o == null) - return false; - - boolean result = false; - - if (o instanceof SampleName) - result = name.equals(((SampleName) o).getValue()); - - return result; - } - -} \ No newline at end of file + private static final long serialVersionUID = -965821128124753850L; + private String name; + + public SampleName(String name) { + this.name = name; + } + + @Override + public Object getValue() { + return name; + } + + @Override + public String getDescriptor(boolean addHtmlTags) { + return "Sample name"; + } + + @Override + public boolean attemptUpdate(Object value) { + Objects.requireNonNull(value); + + if (!(value instanceof String)) { + throw new IllegalArgumentException( + "Illegal type: " + value.getClass().getSimpleName() + ". String expected."); + } + + final boolean result = !name.equals(value); + this.name = (String) value; + return result; + + } + + @Override + public String toString() { + return name; + } + + @Override + 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 (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SampleName other = (SampleName) obj; + if (!Objects.equals(this.name, other.name)) { + return false; + } + return true; + } + +} diff --git a/src/main/java/pulse/properties/ScientificFormat.java b/src/main/java/pulse/properties/ScientificFormat.java new file mode 100644 index 00000000..a61108c6 --- /dev/null +++ b/src/main/java/pulse/properties/ScientificFormat.java @@ -0,0 +1,113 @@ +/* + * 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.properties; + +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author Artem Lunev + */ +public class ScientificFormat extends NumberFormat { + + private static final long serialVersionUID = -6509402151736747913L; + private final int dimensionFactor; + private final double dimensionDelta; + + public ScientificFormat(Number dimensionFactor, Number dimensionDelta) { + super(); + this.dimensionFactor = dimensionFactor.intValue(); + this.dimensionDelta = dimensionDelta.doubleValue(); + } + + private static int getExponentForNumber(double number) { + var nf = new DecimalFormat("0.000E000"); + String numberAsString = nf.format(number); + try { + var substring = numberAsString.substring(numberAsString.indexOf('E') + 1, numberAsString.length()); + return NumberFormat.getIntegerInstance().parse(substring).intValue(); + } catch (ParseException ex) { + //no "E" found + return 0; + } + } + + @Override + public StringBuffer format(double arg0, StringBuffer arg1, FieldPosition arg2) { + double adjusted = arg0 * dimensionFactor + dimensionDelta; + + int exponent = getExponentForNumber(adjusted); + double mantissa = adjusted / Math.pow(10, exponent); + + return format(mantissa, exponent, true); + } + + private static StringBuffer format(Number a, Number b, boolean decimal) { + StringBuffer sb = new StringBuffer(); + var nf = new DecimalFormat(); + nf.setMaximumFractionDigits(2); + nf.setMinimumFractionDigits(decimal ? 2 : 0); + + sb.append(nf.format(a)) + .append(" × 10") + .append(b) + .append(""); + + return sb; + } + + @Override + public StringBuffer format(long arg0, StringBuffer arg1, FieldPosition arg2) { + long adjusted = arg0 * dimensionFactor + Double.doubleToLongBits(dimensionDelta); + + int exponent = Math.getExponent(adjusted); + long mantissa = Double.doubleToLongBits(adjusted / Math.pow(2, exponent)); + + return format(mantissa, exponent, false); + } + + @Override + public Number parse(String arg0, ParsePosition arg1) { + var tokenizer = new StringTokenizer(arg0); + Number a = null; + Number b = null; + try { + a = NumberFormat.getInstance().parse(tokenizer.nextToken(" ")); + tokenizer.nextToken(); //ignore × + b = NumberFormat.getInstance().parse(tokenizer.nextToken(" ")); + } catch (ParseException ex) { + Logger.getLogger(ScientificFormat.class.getName()).log(Level.SEVERE, null, ex); + } + + Number result; + + if (a instanceof Double) { + result = a.doubleValue() * Math.pow(10, b.doubleValue()); + } else { + result = a.longValue() * Math.pow(10, b.intValue()); + } + + return result; + } + +} diff --git a/src/main/java/pulse/properties/package-info.java b/src/main/java/pulse/properties/package-info.java index ee1f8ba4..1aed5413 100644 --- a/src/main/java/pulse/properties/package-info.java +++ b/src/main/java/pulse/properties/package-info.java @@ -4,5 +4,4 @@ * {@code Problem}s, etc. This package contains all property types used by all * other classes. */ - -package pulse.properties; \ No newline at end of file +package pulse.properties; diff --git a/src/main/java/pulse/search/GeneralTask.java b/src/main/java/pulse/search/GeneralTask.java new file mode 100644 index 00000000..426b067f --- /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(); + best = null; + setIterativeState(optimiser.initState(this)); + + double 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(); + +} diff --git a/src/main/java/pulse/search/Optimisable.java b/src/main/java/pulse/search/Optimisable.java index 946afe49..53c29d43 100644 --- a/src/main/java/pulse/search/Optimisable.java +++ b/src/main/java/pulse/search/Optimisable.java @@ -1,39 +1,36 @@ package pulse.search; -import java.util.List; - -import pulse.math.IndexedVector; -import pulse.properties.Flag; +import pulse.math.ParameterVector; +import pulse.problem.schemes.solvers.SolverException; /** * An interface for dealing with optimisation variables. The variables are * collected in {@code IndexedVector}s according to the pattern set up by a list * of {@code Flag}s. */ - public interface Optimisable { - /** - * Assigns parameter values of this {@code Optimisable} using the optimisation - * vector {@code params}. Only those parameters will be updated, the types of - * which are listed as indices in the {@code params} vector. - * - * @param params the optimisation vector, containing a similar set of parameters - * to this {@code Problem} - * @see pulse.util.PropertyHolder.listedTypes() - */ - - public void assign(IndexedVector params); - - /** - * Calculates the vector argument defined on Rn - * 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(IndexedVector[] output, List flags); - -} \ No newline at end of file + /** + * Assigns parameter values of this {@code Optimisable} using the + * optimisation vector {@code params}. Only those parameters will be + * updated, the types of which are listed as indices in the {@code params} + * vector. + * + * @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 input) throws SolverException; + + /** + * Calculates the vector argument defined on + * Rn + * to the scalar objective function for this {@code Optimisable}. + * + * @param output the output vector where the result will be stored + */ + 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 00000000..ec0fd4ed --- /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; + } + +} diff --git a/src/main/java/pulse/search/SimpleResponse.java b/src/main/java/pulse/search/SimpleResponse.java new file mode 100644 index 00000000..9b658713 --- /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 b3b9edfa..1ad72f30 100644 --- a/src/main/java/pulse/search/direction/ActiveFlags.java +++ b/src/main/java/pulse/search/direction/ActiveFlags.java @@ -1,103 +1,117 @@ package pulse.search.direction; -import static pulse.properties.Flag.allProblemDependentFlags; -import static pulse.properties.Flag.allProblemIndependentFlags; -import static pulse.properties.Flag.selectActive; - +import java.io.Serializable; 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.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; -import pulse.tasks.SearchTask; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; - -public class ActiveFlags { - - private static List problemIndependentFlags = allProblemIndependentFlags(); - private static List problemDependentFlags = allProblemDependentFlags(); - - private ActiveFlags() { - //empty constructor - } - - public static void reset() { - problemDependentFlags = allProblemDependentFlags(); - problemIndependentFlags = allProblemIndependentFlags(); - } - - public static List getAllFlags() { - var newList = new ArrayList(); - newList.addAll(problemDependentFlags); - newList.addAll(problemIndependentFlags); - return newList; - } - - public static void listAvailableProperties(List list) { - list.addAll(problemIndependentFlags); - - var t = TaskManager.getManagerInstance().getSelectedTask(); - - if (t != null) { - var p = t.getProblem(); - - if (p != null) { - - var params = p.listedTypes().stream().filter(pp -> pp instanceof NumericProperty) - .map(pMap -> ((NumericProperty) pMap).getType()).collect(Collectors.toList()); - - NumericPropertyKeyword key; - - for (Flag property : problemDependentFlags) { - key = property.getType(); - if (params.contains(key)) - list.add(property); - - } - - } - } else { - for (Flag property : problemDependentFlags) { - list.add(property); - } - } - } - - /** - * Finds what properties are being altered in the search - * - * @return a {@code List} of property types represented by - * {@code NumericPropertyKeyword}s - */ - - public static List activeParameters(SearchTask t) { - Problem p = t.getProblem(); - - var list = new ArrayList(); - list.addAll(selectActiveAndListed(problemDependentFlags, p)); - list.addAll(selectActiveTypes(problemIndependentFlags)); - return list; - } - - public static List selectActiveAndListed(List flags, Problem listed) { - return selectActiveTypes(flags).stream().filter(type -> listed.isListedNumericType(type)) - .collect(Collectors.toList()); - } - - public static List selectActiveTypes(List flags) { - return selectActive(flags).stream().map(flag -> flag.getType()).collect(Collectors.toList()); - } - - public static List getProblemIndependentFlags() { - return problemIndependentFlags; - } - - public static List getProblemDependentFlags() { - return problemDependentFlags; - } - -} \ No newline at end of file +import pulse.util.PropertyHolder; + +public class ActiveFlags implements Serializable { + + private static final long serialVersionUID = -8711073682010113698L; + private static List flags; + + static { + reset(); + } + + private ActiveFlags() { + //empty constructor + } + + public static void reset() { + flags = Flag.allFlags(); + } + + public static List getAllFlags() { + return flags; + } + + public static Set availableProperties() { + var set = new HashSet(); + + var t = TaskManager.getManagerInstance().getSelectedTask(); + + if (t == null) { + return set; + } + + var p = ((Calculation) t.getResponse()).getProblem(); + + if (p != null) { + + var fullList = p.listedKeywords(); + fullList.addAll(((ExperimentalData) t.getInput()).listedKeywords()); + NumericPropertyKeyword key; + + for (Flag property : flags) { + key = property.getType(); + if (fullList.contains(key)) { + set.add(property); + } + + } + + } + + 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) { + 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 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/ApproximatedHessianOptimiser.java b/src/main/java/pulse/search/direction/ApproximatedHessianOptimiser.java deleted file mode 100644 index a0345a37..00000000 --- a/src/main/java/pulse/search/direction/ApproximatedHessianOptimiser.java +++ /dev/null @@ -1,128 +0,0 @@ -package pulse.search.direction; - -import static pulse.math.linear.SquareMatrix.outerProduct; - -import pulse.math.linear.SquareMatrix; -import pulse.math.linear.Vector; -import pulse.problem.schemes.solvers.SolverException; -import pulse.tasks.SearchTask; -import pulse.ui.Messages; - -/** - * The 'advanced' {@code PathSolver} implementing the variable-metric - * (quasi-Newton) search method. - *

- * The latter does not only rely on the gradient (first derivatives) of the - * target function, as commonly used in simpler optimisation methods, such as - * the steepest descent method, but also accounts for the second-order - * derivatives. This leads to an additional term in the equation defining the - * minimum direction. This term is called the 'Hessian' matrix, which is - * calculated approximately using the BFGS formula. Note that the initial value - * for the 'Hessian' matrix is an identity matrix. It is recommended to use this - * {@code PathSolver} in combination with the {@code WolfeSolver}. - *

- * - * @see Wikipedia - * page - * @see pulse.search.linear.WolfeOptimiser - */ - -public class ApproximatedHessianOptimiser extends PathOptimiser { - - private static ApproximatedHessianOptimiser instance = new ApproximatedHessianOptimiser(); - - private ApproximatedHessianOptimiser() { - super(); - } - - /** - * Uses an approximation of the Hessian matrix, containing the information on - * second derivatives, calculated with the BFGS formula in combination with the - * local value of the gradient to evaluate the direction of the minimum on - * {@code p}. Invokes {@code p.setDirection()}. - */ - - @Override - public Vector direction(Path p) { - Vector dir = (((ComplexPath) p).getHessian().inverse()).multiply(p.getGradient()).inverted(); - p.setDirection(dir); - return dir; - } - - /** - *

- * Calculated the gradient at the end of this step. Invokes {@code hessian(...)} - * to calculate the Hessian matrix at the {@code k+1} step using the - * gk and - * gk+1 gradient values, the previously - * calculated Hessian matrix on step k, and the result of the linear - * search αk+1. - *

- * - * @throws SolverException - */ - - @Override - public void endOfStep(SearchTask task) throws SolverException { - var p = (ComplexPath) task.getPath(); - Vector dir = p.getDirection(); - - final double minimumPoint = p.getMinimumPoint(); - final SquareMatrix prevHessian = p.getHessian(); - final Vector g0 = p.getGradient(); // g0 - Vector g1 = gradient(task); // g1 - - p.setHessian(hessian(g0, g1, dir, prevHessian, minimumPoint)); // 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. - * - * @param g1 gradient at step k - * @param g2 gradient at step k+1 - * @param dir direction pointing to the minimum at step k+1 - * @param prevHessian the Hessian matrix at step k - * @param alpha the results of the linear search at step k+1 - * @return a Hessian {@code Matrix} - */ - - private SquareMatrix hessian(Vector g1, Vector g2, Vector dir, SquareMatrix prevHessian, double alpha) { - Vector y = g2.subtract(g1); // g[k+1] - g[k] - return prevHessian.sum((outerProduct(g1, g1)).multiply(1. / g1.dot(dir))) - .sum((outerProduct(y, y)).multiply(1. / (alpha * y.dot(dir)))); // BFGS for Ge[k+1] - } - - @Override - public String toString() { - return Messages.getString("ApproximatedHessianSolver.Descriptor"); - } - - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ - - public static ApproximatedHessianOptimiser getInstance() { - return instance; - } - - /** - * Creates a new {@code Path} instance for storing the gradient, direction, and - * minimum point for this {@code PathSolver}. - * - * @param t the search task - * @return a {@code Path} instance - */ - - @Override - public Path createPath(SearchTask t) { - return new ComplexPath(t); - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/BFGSOptimiser.java b/src/main/java/pulse/search/direction/BFGSOptimiser.java new file mode 100644 index 00000000..89119266 --- /dev/null +++ b/src/main/java/pulse/search/direction/BFGSOptimiser.java @@ -0,0 +1,110 @@ +package pulse.search.direction; + +import static pulse.math.linear.SquareMatrix.asSquareMatrix; +import static pulse.math.linear.SquareMatrix.outerProduct; + +import pulse.math.linear.SquareMatrix; +import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; +import pulse.ui.Messages; + +/** + * The 'advanced' {@code PathSolver} implementing the variable-metric + * (quasi-Newton) search method. + *

+ * The latter does not only rely on the gradient (first derivatives) of the + * target function, as commonly used in simpler optimisation methods, such as + * the steepest descent method, but also accounts for the second-order + * derivatives. This leads to an additional term in the equation defining the + * minimum direction. This term is called the 'Hessian' matrix, which is + * calculated approximately using the BFGS formula. Note that the initial value + * for the 'Hessian' matrix is an identity matrix. It is recommended to use this + * {@code PathSolver} in combination with the {@code WolfeSolver}. + *

+ * + * @see Wikipedia + * page + * @see pulse.search.linear.WolfeOptimiser + */ +public class BFGSOptimiser extends CompositePathOptimiser { + + /** + * + */ + private static final long serialVersionUID = -8542438015176648987L; + private static BFGSOptimiser instance = new BFGSOptimiser(); + + private BFGSOptimiser() { + super(); + this.setSolver(new HessianDirectionSolver() { + //empty statement + }); + } + + /** + *

+ * Calculated the gradient at the end of this step. Invokes + * {@code hessian(...)} to calculate the Hessian matrix at the + * {@code k+1} step using the + * gk and + * gk+1 gradient values, the + * previously calculated Hessian matrix on step k, and the result of + * the linear search αk+1. + *

+ * + * @throws SolverException + */ + @Override + public void prepare(GeneralTask task) throws SolverException { + var p = (ComplexPath) task.getIterativeState(); + Vector dir = p.getDirection(); //p[k] + + final double minimumPoint = p.getMinimumPoint(); // alpha[k] + final SquareMatrix prevHessian = p.getHessian(); // B[k] + + final Vector g0 = p.getGradient(); // g[k] + final Vector g1 = gradient(task); // g[k+1] + + var hessian = hessian(g0, g1, dir, prevHessian, minimumPoint); //B[k+1] + + 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. + * + * @param g1 gradient at step k + * @param g2 gradient at step k+1 + * @param dir direction pointing to the minimum at step k+1 + * @param prevHessian the Hessian matrix at step k + * @param alpha the results of the linear search at step k+1 + * @return a Hessian {@code Matrix} + */ + private SquareMatrix hessian(Vector g1, Vector g2, Vector dir, SquareMatrix prevHessian, double alpha) { + Vector y = g2.subtract(g1); // g[k+1] - g[k] + + var m = prevHessian.sum((outerProduct(g1, g1)).multiply(1. / g1.dot(dir))) + .sum((outerProduct(y, y)).multiply(1. / (alpha * y.dot(dir)))); // BFGS formula + + return asSquareMatrix(m); + } + + @Override + public String toString() { + return Messages.getString("ApproximatedHessianSolver.Descriptor"); + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static BFGSOptimiser getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/search/direction/ComplexPath.java b/src/main/java/pulse/search/direction/ComplexPath.java index 59e2042b..8340c81c 100644 --- a/src/main/java/pulse/search/direction/ComplexPath.java +++ b/src/main/java/pulse/search/direction/ComplexPath.java @@ -1,12 +1,9 @@ package pulse.search.direction; import static pulse.math.linear.Matrices.createIdentityMatrix; -import static pulse.search.direction.PathOptimiser.getInstance; -import static pulse.search.direction.PathOptimiser.gradient; import pulse.math.linear.SquareMatrix; -import pulse.problem.schemes.solvers.SolverException; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; /** *

@@ -16,35 +13,43 @@ *

* */ - -public class ComplexPath extends Path { - - private SquareMatrix hessian; - - protected ComplexPath(SearchTask task) { - super(task); - } - - /** - * In addition to the superclass method, resets the Hessian to an Identity - * matrix. - * - * @throws SolverException - */ - - @Override - public void reset(SearchTask task) { - setGradient(gradient(task)); - hessian = createIdentityMatrix(ActiveFlags.activeParameters(task).size()); - setDirection(getInstance().direction(this)); - } - - public SquareMatrix getHessian() { - return hessian; - } - - public void setHessian(SquareMatrix hes) { - this.hessian = hes; - } - -} \ No newline at end of file +public class ComplexPath extends GradientGuidedPath { + + private static final long serialVersionUID = -1520823504831702183L; + private SquareMatrix hessian; + private SquareMatrix inverseHessian; + + protected ComplexPath(GeneralTask task) { + super(task); + } + + /** + * In addition to the superclass method, resets the Hessian to an Identity + * matrix. + * + * @param task + */ + @Override + public void configure(GeneralTask task) { + hessian = createIdentityMatrix(this.getParameters().dimension()); + inverseHessian = createIdentityMatrix(hessian.getData().length); + super.configure(task); + } + + public SquareMatrix getHessian() { + return hessian; + } + + public void setHessian(SquareMatrix hes) { + this.hessian = hes; + } + + public SquareMatrix getInverseHessian() { + return inverseHessian; + } + + public void setInverseHessian(SquareMatrix inverseHessian) { + this.inverseHessian = inverseHessian; + } + +} diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java new file mode 100644 index 00000000..1e91de69 --- /dev/null +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -0,0 +1,146 @@ +package pulse.search.direction; + +import static pulse.properties.NumericProperties.compare; + +import java.util.List; + +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.util.InstanceDescriptor; + +public abstract class CompositePathOptimiser extends GradientBasedOptimiser { + + private InstanceDescriptor instanceDescriptor + = new InstanceDescriptor<>( + "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. + */ + public final static int MAX_FAILED_ATTEMPTS = 2; + + /** + * For numerical comparison. + */ + public final static double EPS = 1e-10; + + public CompositePathOptimiser() { + instanceDescriptor.setSelectedDescriptor(WolfeOptimiser.class.getSimpleName()); + linearSolver = instanceDescriptor.newInstance(LinearOptimiser.class); + linearSolver.setParent(this); + instanceDescriptor.addListener(() -> initLinearOptimiser()); + } + + private void initLinearOptimiser() { + setLinearSolver(instanceDescriptor.newInstance(LinearOptimiser.class)); + } + + @Override + public boolean iteration(GeneralTask task) throws SolverException { + var p = (GradientGuidedPath) task.getIterativeState(); // the previous state of the task + + boolean accept = true; + + /* + * Checks whether an iteration limit has been already reached + */ + if (compare(p.getIteration(), getMaxIterations()) > 0) { + + throw new SolverException(OPTIMISATION_TIMEOUT); + + } else { + + double initialCost = task.getResponse().objectiveFunction(task); + p.setCost(initialCost); + var parameters = task.searchVector(); + + p.setParameters(parameters); // store current parameters + + var dir = getSolver().direction(p); // find p[k] + double step = linearSolver.linearStep(task); // find magnitude of step + p.setLinearStep(step); + + // new set of parameters determined through search + var candidateParams = parameters.toVector().sum(dir.multiply(step)); + var candidateVector = new ParameterVector(parameters, candidateParams); + + 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 + && 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())); + p.incrementFailedAttempts(); + accept = false; + } else { + 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 + } + + } + + return accept; + + } + + public LinearOptimiser getLinearSolver() { + return linearSolver; + } + + /** + * Assigns a {@code LinearSolver} to this {@code PathSolver} and sets this + * object as its parent. + * + * @param linearSearch a {@code LinearSolver} + */ + public void setLinearSolver(LinearOptimiser linearSearch) { + this.linearSolver = linearSearch; + linearSolver.setParent(this); + super.parameterListChanged(); + } + + public InstanceDescriptor getLinearOptimiserDescriptor() { + return instanceDescriptor; + } + + @Override + public List listedTypes() { + List list = super.listedTypes(); + list.add(instanceDescriptor); + return list; + } + + /** + * Creates a new {@code Path} instance for storing the gradient, direction, + * and minimum point for this {@code PathSolver}. + * + * @param t the search task + * @return a {@code Path} instance + */ + @Override + public GradientGuidedPath initState(GeneralTask t) { + return new ComplexPath(t); + } + +} diff --git a/src/main/java/pulse/search/direction/DirectionSolver.java b/src/main/java/pulse/search/direction/DirectionSolver.java new file mode 100644 index 00000000..f96c1910 --- /dev/null +++ b/src/main/java/pulse/search/direction/DirectionSolver.java @@ -0,0 +1,21 @@ +package pulse.search.direction; + +import java.io.Serializable; +import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; + +public interface DirectionSolver extends Serializable { + + /** + * Finds the direction of the minimum using the previously calculated values + * stored in {@code p}. + * + * @param p a {@code Path} object + * @return a {@code Vector} pointing to the minimum direction for this + * {@code Path} + * @throws SolverException + * @see pulse.problem.statements.Problem.optimisationVector(List) + */ + public Vector direction(GradientGuidedPath p) throws SolverException; + +} diff --git a/src/main/java/pulse/search/direction/GradientBasedOptimiser.java b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java new file mode 100644 index 00000000..c55405ec --- /dev/null +++ b/src/main/java/pulse/search/direction/GradientBasedOptimiser.java @@ -0,0 +1,176 @@ +package pulse.search.direction; + +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericProperty.requireType; +import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; + +import java.util.Set; + +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.NumericProperties; +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; + +public abstract class GradientBasedOptimiser extends PathOptimiser { + + private double gradientResolution; + private double gradientStep; + + 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 + * {@code ITERATION_LIMIT, ERROR_TOLERANCE} and {@code GRADIENT_RESOLUTION} + * for this {@code PathSolver}. In addition, sets up a list of search flags + * defined by the {@code Flag.defaultList} method. + * + * @see pulse.properties.Flag.defaultList() + */ + protected GradientBasedOptimiser() { + super(); + this.gradientResolution = gradientStep = RESOLUTION_HIGH; + } + + /** + * Resets the default {@code ITERATION_LIMIT, ERROR_TOLERANCE} and + * {@code GRADIENT_RESOLUTION} values for this {@code PathSolver}. In + * addition, sets up a list of search flags defined by the + * {@code Flag.defaultList} method. + * + * @see pulse.properties.Flag.defaultList() + */ + @Override + public void reset() { + super.reset(); + gradientResolution = RESOLUTION_HIGH; + gradientStep = gradientResolution; + } + + /** + * Calculates the {@code Vector} gradient of the target function (the sum of + * squared residuals, SSR, for this {@code task}. + *

+ * If Δf(Δxi) is the change in + * the target function associated with the change of the parameter + * xi, the i-th component of the + * gradient is equal to gi = + * (Δf(Δxi)/Δxi). The + * accuracy of this calculation depends on the + * Δxi value, which is roughly the + * {@code GRADIENT_RESOLUTION}. Note however that instead of using a + * forward-difference scheme to calculate the gradient, this method utilises + * the central-difference calculation of the gradient, which significantly + * increases the overall accuracy of calculation. This means that to + * evaluate each component of this vector, the {@code Problem} associated + * with this {@code task} is solved twice (for xi ± + * Δxi). + *

+ * + * @param task a {@code SearchTask} that is being driven to the minimum of + * SSR + * @return the gradient of the target function + * @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()); + 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); + + var shiftVector = new ParameterVector(params, pVector.sum(shift)); + task.assign(shiftVector); + final double ss2 = task.objectiveFunction(); + + task.assign(new ParameterVector(params, pVector.subtract(shift))); + final double ss1 = task.objectiveFunction(); + + grad.set(i, (ss2 - ss1) / dx); + } + + task.assign(params); + + 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 + * @param value the value of the parameter under the optimisation vector + * @return the gradient step + */ + protected double dx(NumericProperty defProp, double 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); + } + + return result; + } + + public void setGradientResolution(NumericProperty resolution) { + requireType(resolution, GRADIENT_RESOLUTION); + this.gradientResolution = (double) resolution.getValue(); + firePropertyChanged(this, resolution); + } + + public NumericProperty getGradientResolution() { + return derive(GRADIENT_RESOLUTION, gradientResolution); + } + + /** + *

+ * The types of the listed parameters for this class include: GRADIENT_RESOLUTION, + * ERROR_TOLERANCE, ITERATION_LIMIT. Also, all the flags in this + * class are treated as separate listed parameters. + *

+ * + * @see pulse.properties.NumericPropertyKeyword + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(GRADIENT_RESOLUTION); + return set; + } + + /** + * The accepted types are: + * GRADIENT_RESOLUTION, ERROR_TOLERANCE, ITERATION_LIMIT. + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if (type == GRADIENT_RESOLUTION) { + setGradientResolution(property); + } + } + + public double getGradientStep() { + return gradientStep; + } + +} diff --git a/src/main/java/pulse/search/direction/GradientGuidedPath.java b/src/main/java/pulse/search/direction/GradientGuidedPath.java new file mode 100644 index 00000000..f4dd4f8f --- /dev/null +++ b/src/main/java/pulse/search/direction/GradientGuidedPath.java @@ -0,0 +1,88 @@ +package pulse.search.direction; + +import java.util.logging.Level; +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; + +/** + *

+ * A {@code Path} stores information relevant to the selected + * {@code PathSolver}, which is related to a specific {@code SearchTask}. This + * information is used by the {@code PathSolver} to perform the next search + * step. + *

+ * + *

+ * This is the most basic implementation, which stores only the gradient, the + * direction (equal to the inverse gradient), minimum point achieved by the + * linear solver, and the number of current iteration. It is used in combination + * with the {@code SteepestDescentSolver}. Note the constructors for + * {@code Path} are protected, as they should not be invoked directly. Instead, + * they are invoked from within the {@code PathSolver}. + *

+ * + * + */ +public class GradientGuidedPath extends IterativeState { + + /** + * + */ + private static final long serialVersionUID = -6450999613326096767L; + private Vector direction; + private Vector gradient; + private double minimumPoint; + + protected GradientGuidedPath(GeneralTask t) { + super(t); + configure(t); + } + + /** + * Resets the {@code Path}: calculates the current gradient and the + * direction of search.Sets the minimum point to 0.0. + * + * @param t the {@code SearchTask}, for which this {@code Path} is created. + * @see pulse.search.direction.PathSolver.direction(Path) + */ + public void configure(GeneralTask t) { + super.reset(); + try { + this.gradient = ((GradientBasedOptimiser) PathOptimiser.getInstance()).gradient(t); + } catch (SolverException ex) { + t.onSolverException(new SolverException("Gradient calculation error", OPTIMISATION_ERROR)); + ex.printStackTrace(); + } + minimumPoint = 0.0; + } + + public Vector getDirection() { + return direction; + } + + public void setDirection(Vector currentDirection) { + this.direction = currentDirection; + } + + public Vector getGradient() { + return gradient; + } + + public void setGradient(Vector currentGradient) { + this.gradient = currentGradient; + } + + public double getMinimumPoint() { + return minimumPoint; + } + + public void setLinearStep(double min) { + minimumPoint = min; + } + +} diff --git a/src/main/java/pulse/search/direction/HessianDirectionSolver.java b/src/main/java/pulse/search/direction/HessianDirectionSolver.java new file mode 100644 index 00000000..7aee8422 --- /dev/null +++ b/src/main/java/pulse/search/direction/HessianDirectionSolver.java @@ -0,0 +1,56 @@ +package pulse.search.direction; + +import org.ejml.data.DMatrixRMaj; +import org.ejml.dense.row.CommonOps_DDRM; + +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 { + + /** + * Uses an approximation of the Hessian matrix, containing the information + * on second derivatives, calculated with the BFGS formula in combination + * with the local value of the gradient to evaluate the direction of the + * minimum on {@code p}. Invokes {@code p.setDirection()}. + * + * @throws SolverException + */ + @Override + public default Vector direction(GradientGuidedPath p) throws SolverException { + var cp = (ComplexPath) p; + + Vector invGrad = p.getGradient().inverted(); + var result = solve(cp, invGrad); + + p.setDirection(result); + return result; + } + + public static Vector solve(ComplexPath cp, Vector rhs) throws SolverException { + final int dimg = cp.getGradient().dimension(); + Vector result; + // use linear solver for big matrices + if (dimg > 4) { + + var hess = new DMatrixRMaj(cp.getHessian().getData()); + var antigrad = new DMatrixRMaj(rhs.getData()); + var dirv = new DMatrixRMaj(dimg, 1); + + if (!CommonOps_DDRM.solve(hess, antigrad, dirv)) { + throw new SolverException("Singular matrix!", OPTIMISATION_ERROR); + } + + result = new Vector(dirv.getData()); + + } else // use fast inverse + { + result = cp.getHessian().inverse().multiply(rhs); + } + + return result; + + } + +} diff --git a/src/main/java/pulse/search/direction/IterativeState.java b/src/main/java/pulse/search/direction/IterativeState.java new file mode 100644 index 00000000..6d258076 --- /dev/null +++ b/src/main/java/pulse/search/direction/IterativeState.java @@ -0,0 +1,79 @@ +package pulse.search.direction; + +import java.io.Serializable; +import pulse.math.ParameterVector; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.ITERATION; + +import pulse.properties.NumericProperty; +import pulse.search.GeneralTask; + +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. + * + * @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 double getCost() { + return cost; + } + + public void setCost(double cost) { + this.cost = cost; + } + + public void reset() { + iteration = 0; + setCost(Double.POSITIVE_INFINITY); + } + + public NumericProperty getIteration() { + return derive(ITERATION, iteration); + } + + public void incrementStep() { + iteration++; + } + + public int getFailedAttempts() { + return failedAttempts; + } + + public void resetFailedAttempts() { + failedAttempts = 0; + } + + public void incrementFailedAttempts() { + failedAttempts++; + } + + public ParameterVector getParameters() { + return parameters; + } + + public void setParameters(ParameterVector parameters) { + this.parameters = parameters; + } + +} diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java new file mode 100644 index 00000000..9d4c1e55 --- /dev/null +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -0,0 +1,317 @@ +package pulse.search.direction; + +import java.util.Arrays; +import static pulse.math.linear.SquareMatrix.asSquareMatrix; +import static pulse.properties.NumericProperties.compare; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericProperty.requireType; +import static pulse.properties.NumericPropertyKeyword.DAMPING_RATIO; + +import java.util.Set; + +import pulse.math.ParameterVector; +import pulse.math.linear.Matrices; +import pulse.math.linear.RectangularMatrix; +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.SumOfSquares; +import pulse.ui.Messages; + +/** + * Given an objective function equal to the sum of squared residuals, + * iteratively approaches the minimum of this function by applying the + * Levenberg-Marquardt formulas. + * + */ +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() { + super(); + dampingRatio = (double) def(DAMPING_RATIO).getValue(); + this.setSolver(new HessianDirectionSolver() { + // see default implementation + }); + } + + @Override + 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 + + /* + * Checks whether an iteration limit has been already reached + */ + if (compare(p.getIteration(), getMaxIterations()) > 0) { + + throw new SolverException(OPTIMISATION_TIMEOUT); + + } else { + + double initialCost = task.objectiveFunction(); + p.setCost(initialCost); + var parameters = task.searchVector(); + + p.setParameters(parameters); // store current parameters + + prepare(task); // do the preparatory step + + var lmDirection = getSolver().direction(p); + + 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(), ILLEGAL_PARAMETERS); + } + + task.assign(new ParameterVector( + parameters, candidate)); // assign new parameters + + double newCost = task.objectiveFunction(); // calculate the sum of squared residuals + + /* + * Delayed gratification + */ + if (newCost > initialCost - EPS && p.getFailedAttempts() < MAX_FAILED_ATTEMPTS) { + p.setLambda(p.getLambda() * 2.0); + task.assign(parameters); // roll back if cost increased + p.setComputeJacobian(true); + p.incrementFailedAttempts(); + accept = false; + } else { + task.storeState(); + p.resetFailedAttempts(); + p.setLambda(p.getLambda() / 3.0); + p.setComputeJacobian(false); + p.setCost(newCost); + p.incrementStep(); // increment the counter of successful steps + } + + } + + return accept; //either accept or reject this step + + } + + /** + * Calculates the Jacobian, if needed, evaluates the gradient and the + * Hessian matrix. + */ + @Override + 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(rs.residualsArray())); + + // Calculate the Jacobian -- if needed + if (p.isComputeJacobian()) { + p.setJacobian(jacobian(task)); // J + p.setNonregularisedHessian(halfHessian(p)); // this is just J'J + } + + // 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))) { + throw new SolverException("Could not calculate objective function gradient", + OPTIMISATION_ERROR); + } + + // the Hessian is then regularised by adding labmda*I + var hessian = p.getNonregularisedHessian(); + var damping = (levenbergDamping(hessian).multiply(dampingRatio) + .sum(marquardtDamping(hessian).multiply(1.0 - dampingRatio))) + .multiply(p.getLambda()); + var regularisedHessian = asSquareMatrix(hessian.sum(damping)); // J'J + lambda I + + p.setHessian(regularisedHessian); // so this is the new Hessian + + } + + /** + *

+ * Calculates the Jacobian of the model function given as a discrete set of + * time-signal values. The elements of the Jacobian are calculated using + * central differences from two residual vectors evaluated by shifting the + * search vector slightly to the right or left of each search parameter. + *

+ *

+ * This is also equivalent to calculating the difference of the model values + * when performing the shift, when taking the model values at the time + * points of the reference dataset. Because of a different discretisation of + * the model, it is easier to substitute these with the residuals, which had + * already been interpolated at the reference time values. + *

+ * + * @param task the task being optimised + * @return the jacobian matrix + * @throws SolverException + * @see pulse.search.statistics.ResidualStatistic.calculateResiduals() + */ + public RectangularMatrix jacobian(GeneralTask task) throws SolverException { + + 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++) { + + 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, 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, pVector.subtract(shift))); + task.objectiveFunction(); + + for (int j = 0, realNumPoints = Math.min(numPoints, r.size()); + j < realNumPoints; j++) { + + jacobian[j][i] -= r.get(j) / dx; + + } + + } + + // revert to original params + task.assign(params); + + return Matrices.createMatrix(jacobian); + + } + + @Override + public GradientGuidedPath initState(GeneralTask t) { + return new LMPath(t); + } + + private Vector halfGradient(LMPath path) { + var jacobian = path.getJacobian(); + var residuals = path.getResidualVector(); + return jacobian.transpose().multiply(new Vector(residuals)); + } + + private SquareMatrix halfHessian(LMPath path) { + var jacobian = path.getJacobian(); + return asSquareMatrix(jacobian.transpose().multiply(jacobian)); + } + + /* + * Additive damping strategy, where the scaling matrix is simply the identity matrix. + */ + private SquareMatrix levenbergDamping(SquareMatrix hessian) { + return Matrices.createIdentityMatrix(hessian.getData().length); + } + + /* + * Multiplicative damping strategy, where the scaling matrix is equal to the 'hessian' block-diagonal matrix. + * Works best for badly scaled problems. However, this is also scale-invariant, + * which mean it increases the susceptibility to parameter evaporation. + */ + private SquareMatrix marquardtDamping(SquareMatrix hessian) { + return hessian.blockDiagonal(); + } + + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(DAMPING_RATIO); + return set; + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static LMOptimiser getInstance() { + return instance; + } + + @Override + public String toString() { + return Messages.getString("LMOptimiser.Descriptor"); + } + + /** + * The Levenberg-Marquardt optimiser will only accept ordinary least-squares + * as its objective function. Therefore, {@code os} should be an instance of + * {@code SumOfSquares}. + * + * @return {@code true} if {@code.getClass()} returns + * {@code SumOfSquares.class}, {@code false} otherwise + */ + @Override + public boolean compatibleWith(OptimiserStatistic os) { + return os.getClass().equals(SumOfSquares.class); + } + + public NumericProperty getDampingRatio() { + return derive(DAMPING_RATIO, dampingRatio); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + super.set(type, property); + if (type == DAMPING_RATIO) { + setDampingRatio(property); + } + } + + public void setDampingRatio(NumericProperty dampingRatio) { + requireType(dampingRatio, DAMPING_RATIO); + this.dampingRatio = (double) dampingRatio.getValue(); + firePropertyChanged(this, dampingRatio); + } + +} diff --git a/src/main/java/pulse/search/direction/LMPath.java b/src/main/java/pulse/search/direction/LMPath.java new file mode 100644 index 00000000..39e99855 --- /dev/null +++ b/src/main/java/pulse/search/direction/LMPath.java @@ -0,0 +1,68 @@ +package pulse.search.direction; + +import pulse.math.linear.RectangularMatrix; +import pulse.math.linear.SquareMatrix; +import pulse.math.linear.Vector; +import pulse.search.GeneralTask; + +class LMPath extends ComplexPath { + + private static final long serialVersionUID = -7154616034580697035L; + private Vector residualVector; + private RectangularMatrix jacobian; + private SquareMatrix nonregularisedHessian; + private double lambda; + private boolean computeJacobian; + + public LMPath(GeneralTask t) { + super(t); + } + + @Override + public void configure(GeneralTask t) { + super.configure(t); + this.lambda = 1.0; + computeJacobian = true; + } + + public RectangularMatrix getJacobian() { + return jacobian; + } + + public void setJacobian(RectangularMatrix jacobian) { + this.jacobian = jacobian; + } + + public double getLambda() { + return lambda; + } + + public void setLambda(double lambda) { + this.lambda = lambda; + } + + public SquareMatrix getNonregularisedHessian() { + return nonregularisedHessian; + } + + public void setNonregularisedHessian(SquareMatrix nonregularisedHessian) { + this.nonregularisedHessian = nonregularisedHessian; + } + + public Vector getResidualVector() { + return residualVector; + } + + public void setResidualVector(Vector residualVector) { + this.residualVector = residualVector; + } + + public boolean isComputeJacobian() { + return computeJacobian; + } + + public void setComputeJacobian(boolean computeJacobian) { + this.computeJacobian = computeJacobian; + } + +} diff --git a/src/main/java/pulse/search/direction/Path.java b/src/main/java/pulse/search/direction/Path.java deleted file mode 100644 index 3ed91f32..00000000 --- a/src/main/java/pulse/search/direction/Path.java +++ /dev/null @@ -1,87 +0,0 @@ -package pulse.search.direction; - -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.ITERATION; - -import pulse.math.linear.Vector; -import pulse.properties.NumericProperty; -import pulse.tasks.SearchTask; - -/** - *

- * A {@code Path} stores information relevant to the selected - * {@code PathSolver}, which is related to a specific {@code SearchTask}. This - * information is used by the {@code PathSolver} to perform the next search - * step. - *

- * - *

- * This is the most basic implementation, which stores only the gradient, the - * direction (equal to the inverse gradient), minimum point achieved by the - * linear solver, and the number of current iteration. It is used in combination - * with the {@code SteepestDescentSolver}. Note the constructors for - * {@code Path} are protected, as they should not be invoked directly. Instead, - * they are invoked from within the {@code PathSolver}. - *

- * - * - */ - -public class Path { - - private Vector direction; - private Vector gradient; - private double minimumPoint; - private int iteration; - - protected Path(SearchTask t) { - reset(t); - } - - /** - * Resets the {@code Path}: calculates the current gradient and the direction of - * search. Sets the minimum point to 0.0. - * - * @param t the {@code SearchTask}, for which this {@code Path} is created. - * @see pulse.search.direction.PathSolver.direction(Path) - */ - - public void reset(SearchTask t) { - this.gradient = PathOptimiser.gradient(t); - this.direction = PathOptimiser.getInstance().direction(this); - minimumPoint = 0.0; - } - - public Vector getDirection() { - return direction; - } - - public void setDirection(Vector currentDirection) { - this.direction = currentDirection; - } - - public Vector getGradient() { - return gradient; - } - - public void setGradient(Vector currentGradient) { - this.gradient = currentGradient; - } - - public double getMinimumPoint() { - return minimumPoint; - } - - public void setLinearStep(double min) { - minimumPoint = min; - } - - public NumericProperty getIteration() { - return derive(ITERATION, iteration); - } - - public void incrementStep() { - iteration++; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index 25423419..21618a8d 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -1,28 +1,22 @@ package pulse.search.direction; -import static pulse.properties.NumericProperties.compare; 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.ERROR_TOLERANCE; -import static pulse.properties.NumericPropertyKeyword.GRADIENT_RESOLUTION; import static pulse.properties.NumericPropertyKeyword.ITERATION_LIMIT; -import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; +import java.util.Set; -import pulse.math.IndexedVector; -import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import pulse.properties.Property; -import pulse.search.linear.LinearOptimiser; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; +import pulse.search.statistics.OptimiserStatistic; import pulse.tasks.TaskManager; -import pulse.tasks.logs.Status; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -37,329 +31,207 @@ * class is closely linked with another abstract search class, the * {@code LinearSolver}. *

- * + * * @see pulse.search.tasks.SearchTask.run() * @see pulse.search.linear.LinearOptimiser */ - public abstract class PathOptimiser extends PropertyHolder implements Reflexive { - private static int maxIterations; - private static double errorTolerance; - private static double gradientResolution; - - private static LinearOptimiser linearSolver; - - private static PathOptimiser instance; - - /** - * Abstract constructor that sets up the default - * {@code ITERATION_LIMIT, ERROR_TOLERANCE} and {@code GRADIENT_RESOLUTION} for - * this {@code PathSolver}. In addition, sets up a list of search flags defined - * by the {@code Flag.defaultList} method. - * - * @see pulse.properties.Flag.defaultList() - */ - - protected PathOptimiser() { - super(); - reset(); - } - - /** - * Resets the default {@code ITERATION_LIMIT, ERROR_TOLERANCE} and - * {@code GRADIENT_RESOLUTION} values for this {@code PathSolver}. In addition, - * sets up a list of search flags defined by the {@code Flag.defaultList} - * method. - * - * @see pulse.properties.Flag.defaultList() - */ - - public static void reset() { - maxIterations = (int) def(ITERATION_LIMIT).getValue(); - errorTolerance = (double) def(ERROR_TOLERANCE).getValue(); - gradientResolution = (double) def(GRADIENT_RESOLUTION).getValue(); - ActiveFlags.reset(); - } - - /** - *

- * This method sets out the basic algorithm for estimating the minimum of the - * target function, which is defined as the sum of squared residuals (SSR), or - * the deviations of the model solution (a {@code DifferenceScheme} used to - * solve the {@code Problem} for this {@code task}) from the empirical values - * (the {@code ExperimentalData}). The algorithm will go through the following - * steps: (1) find the direction, which points to the minimum, using the - * concrete {@code direction} method; (2) estimate the magnitude of the step to - * reach the minimum using the {@code LinearSolver}; (3) assign a new set of - * parameters to the {@code SearchTask}; (4) calculate the new SSR value. - *

- *

- * - * @param task a {@code SearchTask} that needs to be driven to a minimum of SSR. - * @return the SSR value with the newly found parameters. - * @throws SolverException - * @see direction(Path) - * @see pulse.search.linear.LinearOptimiser - */ - - public double iteration(SearchTask task) throws SolverException { - var p = task.getPath(); // the previous path of the task - - /* - * Checks whether an iteration limit has been already reached - */ - - if (compare(p.getIteration(), getMaxIterations()) > 0) - task.setStatus(Status.TIMEOUT); - - var parameters = task.searchVector()[0]; // get current search vector - - var dir = direction(p); // find the direction to the global minimum - - double step = linearSolver.linearStep(task); // find how big the step needs to be to reach the minimum - p.setLinearStep(step); - - var newParams = parameters.sum(dir.multiply(step)); // this set of parameters supposedly corresponds to the - // minimum - task.assign(new IndexedVector(newParams, parameters.getIndices())); // assign new parameters to this task - - endOfStep(task); // compute gradients, Hessians, etc. with new parameters - - p.incrementStep(); // increment the counter of successful steps - - return task.solveProblemAndCalculateDeviation(); // calculate the sum of squared residuals - } - - /** - * Finds the direction of the minimum using the previously calculated values - * stored in {@code p}. - * - * @param p a {@code Path} object - * @return a {@code Vector} pointing to the minimum direction for this - * {@code Path} - * @see pulse.problem.statements.Problem.optimisationVector(List) - */ - - public abstract Vector direction(Path p); - - /** - * 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 endOfStep(SearchTask task) throws SolverException; - - /** - * Calculates the {@code Vector} gradient of the target function (the sum of - * squared residuals, SSR, for this {@code task}. - *

- * If Δf(Δxi) is the change in the - * target function associated with the change of the parameter - * xi, the i-th component of the gradient - * is equal to gi = - * (Δf(Δxi)/Δxi). The - * accuracy of this calculation depends on the - * Δxi value, which is roughly the - * {@code GRADIENT_RESOLUTION}. Note however that instead of using a - * forward-difference scheme to calculate the gradient, this method utilises the - * central-difference calculation of the gradient, which significantly increases - * the overall accuracy of calculation. This means that to evaluate each - * component of this vector, the {@code Problem} associated with this - * {@code task} is solved twice (for xi ± - * Δxi). - *

- * - * @param task a {@code SearchTask} that is being driven to the minimum of SSR - * @return the gradient of the target function - * @throws SolverException - */ - - public static Vector gradient(SearchTask task) { - - final var params = task.searchVector()[0]; - var grad = new Vector(params.dimension()); - - boolean discreteGradient = params.getIndices().stream().anyMatch(index -> isDiscrete(index)); - final double dx = discreteGradient ? 2.0 * task.getScheme().getGrid().getXStep() : 2.0 * gradientResolution; - - for (int i = 0; i < params.dimension(); i++) { - final var shift = new Vector(params.dimension()); - shift.set(i, 0.5 * dx); - - task.assign(new IndexedVector( params.sum(shift) , params.getIndices())); - final double ss2 = task.solveProblemAndCalculateDeviation(); - - task.assign(new IndexedVector( params.subtract(shift), params.getIndices())); - final double ss1 = task.solveProblemAndCalculateDeviation(); - - grad.set(i, (ss2 - ss1) / dx); - - } - - task.assign(params); - - return grad; - - } - - public static LinearOptimiser getLinearSolver() { - return linearSolver; - } - - /** - * Assigns a {@code LinearSolver} to this {@code PathSolver} and sets this - * object as its parent. - * - * @param linearSearch a {@code LinearSolver} - */ - - public void setLinearSolver(LinearOptimiser linearSearch) { - PathOptimiser.linearSolver = linearSearch; - linearSolver.setParent(this); - super.parameterListChanged(); - } - - public static NumericProperty getErrorTolerance() { - return derive(ERROR_TOLERANCE, errorTolerance); - } - - public static void setErrorTolerance(NumericProperty errorTolerance) { - PathOptimiser.errorTolerance = (double) errorTolerance.getValue(); - } - - public static void setGradientResolution(NumericProperty resolution) { - PathOptimiser.gradientResolution = (double) resolution.getValue(); - } - - public static NumericProperty getGradientResolution() { - return derive(GRADIENT_RESOLUTION, gradientResolution); - } - - public static NumericProperty getMaxIterations() { - return derive(ITERATION_LIMIT, maxIterations); - } - - public static void setMaxIterations(NumericProperty maxIterations) { - PathOptimiser.maxIterations = (int) maxIterations.getValue(); - } - - @Override - public String toString() { - return this.getClass().getSimpleName(); - } - - /** - * This method has been overriden to account for each individual flag in the - * {@code List} set out by this class. - */ - - @Override - public List genericProperties() { - var original = super.genericProperties(); - original.addAll(ActiveFlags.getProblemDependentFlags()); - original.addAll(ActiveFlags.getProblemIndependentFlags()); - return original; - } - - /** - *

- * The types of the listed parameters for this class include: - * GRADIENT_RESOLUTION, - * ERROR_TOLERANCE, ITERATION_LIMIT. Also, all the flags in this class - * are treated as separate listed parameters. - *

- * - * @see pulse.properties.NumericPropertyKeyword - */ - - @Override - public List listedTypes() { - List list = new ArrayList(); - list.add(def(GRADIENT_RESOLUTION)); - list.add(def(ERROR_TOLERANCE)); - list.add(def(ITERATION_LIMIT)); - - ActiveFlags.listAvailableProperties(list); - - return list; - } - - @Override - public List data() { - var list = listedTypes(); - return super.data().stream().filter(p -> list.contains(p)).collect(Collectors.toList()); - } - - /** - * The accepted types are: - * GRADIENT_RESOLUTION, ERROR_TOLERANCE, ITERATION_LIMIT. - */ - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - switch (type) { - case GRADIENT_RESOLUTION: - setGradientResolution(property); - break; - case ERROR_TOLERANCE: - setErrorTolerance(property); - break; - case ITERATION_LIMIT: - setMaxIterations(property); - break; - default: - break; - } - } - - /** - * @return {@code false} for {@code PathSolver} - */ - - @Override - public boolean ignoreSiblings() { - return true; - } - - - /** - * Finds a {@code Flag} equivalent to {@code flag} in the {@code originalList} - * and substitutes its value with {@code flag.getValue}. - */ - - @Override - public void update(Property property) { - if(! (property instanceof Flag) ) - super.update(property); - else { - var flag = (Flag) property; - var optional = ActiveFlags.getAllFlags().stream().filter(f -> f.getType() == flag.getType()).findFirst(); - - if (optional.isPresent()) - optional.get().setValue((boolean) flag.getValue()); - - } - } - - /** - * Creates a new {@code Path} suitable for this {@code PathSolver} - * - * @param t the task, the optimisation path of which will be tracked - * @return a {@code Path} instance - */ - - public abstract Path createPath(SearchTask t); - - public static PathOptimiser getInstance() { - return instance; - } - - public static void setInstance(PathOptimiser selectedPathOptimiser) { - PathOptimiser.instance = selectedPathOptimiser; - selectedPathOptimiser.setParent(TaskManager.getManagerInstance()); - } - -} \ No newline at end of file + private DirectionSolver solver; + + private int maxIterations; + private double errorTolerance; + + private static PathOptimiser instance; + + /** + * Abstract constructor that sets up the default + * {@code ITERATION_LIMIT, ERROR_TOLERANCE} and {@code GRADIENT_RESOLUTION} + * for this {@code PathSolver}. In addition, sets up a list of search flags + * defined by the {@code Flag.defaultList} method. + * + * @see pulse.properties.Flag.defaultList() + */ + protected PathOptimiser() { + super(); + reset(); + } + + /** + * Resets the default {@code ITERATION_LIMIT, ERROR_TOLERANCE} and + * {@code GRADIENT_RESOLUTION} values for this {@code PathSolver}. In + * addition, sets up a list of search flags defined by the + * {@code Flag.defaultList} method. + * + * @see pulse.properties.Flag.defaultList() + */ + public void reset() { + maxIterations = (int) def(ITERATION_LIMIT).getValue(); + errorTolerance = (double) def(ERROR_TOLERANCE).getValue(); + ActiveFlags.reset(); + } + + /** + *

+ * This method sets out the basic algorithm for estimating the minimum of + * the target function, which is defined as the sum of squared residuals + * (SSR), or the deviations of the model solution (a + * {@code DifferenceScheme} used to solve the {@code Problem} for this + * {@code task}) from the empirical values (the {@code ExperimentalData}). + * The algorithm will go through the following steps: (1) find the + * direction, which points to the minimum, using the concrete + * {@code direction} method; (2) estimate the magnitude of the step to reach + * the minimum using the {@code LinearSolver}; (3) assign a new set of + * parameters to the {@code SearchTask}; (4) calculate the new SSR value. + *

+ *

+ * + * @param task a {@code SearchTask} that needs to be driven to a minimum of + * SSR. + * @return the SSR value with the newly found parameters. + * @throws SolverException + * @see direction(Path) + * @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. + * + * @param task the {@code SearchTask} undergoing optimisation + * @throws SolverException + */ + public abstract void prepare(GeneralTask task) throws SolverException; + + public NumericProperty getErrorTolerance() { + return derive(ERROR_TOLERANCE, errorTolerance); + } + + public void setErrorTolerance(NumericProperty errorTolerance) { + requireType(errorTolerance, ERROR_TOLERANCE); + this.errorTolerance = (double) errorTolerance.getValue(); + firePropertyChanged(this, errorTolerance); + } + + public NumericProperty getMaxIterations() { + return derive(ITERATION_LIMIT, maxIterations); + } + + public void setMaxIterations(NumericProperty maxIterations) { + requireType(maxIterations, ITERATION_LIMIT); + this.maxIterations = (int) maxIterations.getValue(); + firePropertyChanged(this, maxIterations); + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + /** + * This method has been overriden to account for each individual flag in the + * {@code List} set out by this class. + */ + @Override + public List genericProperties() { + var original = super.genericProperties(); + original.addAll(ActiveFlags.getAllFlags()); + return original; + } + + /** + *

+ * The types of the listed parameters for this class include: + * ERROR_TOLERANCE, ITERATION_LIMIT. Also, all the flags in + * this class are treated as separate listed parameters. + *

+ * + * @see pulse.properties.NumericPropertyKeyword + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(ERROR_TOLERANCE); + set.add(ITERATION_LIMIT); + return set; + } + + /** + * The accepted types are: ERROR_TOLERANCE, ITERATION_LIMIT. + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == ERROR_TOLERANCE) { + setErrorTolerance(property); + } else if (type == ITERATION_LIMIT) { + setMaxIterations(property); + } + } + + /** + * @return {@code false} for {@code PathSolver} + */ + @Override + public boolean ignoreSiblings() { + return true; + } + + /** + * Finds a {@code Flag} equivalent to {@code flag} in the + * {@code originalList} and substitutes its value with + * {@code flag.getValue}. + */ + @Override + public void update(Property property) { + if (!(property instanceof Flag)) { + super.update(property); + } else { + var flag = (Flag) property; + var optional = ActiveFlags.getAllFlags().stream().filter(f -> f.getType() == flag.getType()).findFirst(); + + if (optional.isPresent()) { + optional.get().setValue((boolean) flag.getValue()); + } + + } + } + + public static PathOptimiser getInstance() { + return instance; + } + + public static void setInstance(PathOptimiser selectedPathOptimiser) { + PathOptimiser.instance = selectedPathOptimiser; + selectedPathOptimiser.setParent(TaskManager.getManagerInstance()); + } + + protected final DirectionSolver getSolver() { + return solver; + } + + protected final void setSolver(DirectionSolver solver) { + this.solver = 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} + * + * @param os a selected optimiser metric + * @return {@code true}, if not specified otherwise by its subclass + * implementation. + */ + public boolean compatibleWith(OptimiserStatistic os) { + return true; + } + + /** + * Creates a new {@code Path} suitable for this {@code PathSolver} + * + * @param t the task, the optimisation path of which will be tracked + * @return a {@code Path} instance + */ + 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 new file mode 100644 index 00000000..716753f2 --- /dev/null +++ b/src/main/java/pulse/search/direction/SR1Optimiser.java @@ -0,0 +1,94 @@ +package pulse.search.direction; + +import static java.lang.Math.abs; +import static pulse.math.linear.SquareMatrix.asSquareMatrix; +import static pulse.math.linear.SquareMatrix.outerProduct; + +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; + +public class SR1Optimiser extends CompositePathOptimiser { + + private static final long serialVersionUID = -3041166132227281210L; + + private static SR1Optimiser instance = new SR1Optimiser(); + + private final static double r = 1E-8; + + private SR1Optimiser() { + super(); + this.setSolver(path -> ((ComplexPath) path).getInverseHessian().multiply(path.getGradient().inverted())); + } + + /** + *

+ * Calculated the gradient at the end of this step. Invokes + * {@code hessian(...)} to calculate the Hessian matrix at the + * {@code k+1} step using the + * gk and + * gk+1 gradient values, the + * previously calculated Hessian matrix on step k, and the result of + * the linear search αk+1. + *

+ * + * @throws SolverException + */ + @Override + public void prepare(GeneralTask task) throws SolverException { + var p = (ComplexPath) task.getIterativeState(); + Vector dir = p.getDirection(); + + final double minimumPoint = p.getMinimumPoint(); + final Vector g0 = p.getGradient(); // g0 + final Vector g1 = gradient(task); // g1 + + /* + * Evaluate condition and update if needed + */ + Vector y = g1.subtract(g0); // g[k+1] - g[k] + + final var dx = dir.multiply(minimumPoint); + final var m1 = y.subtract(p.getHessian().multiply(dx)); + + if (abs(dx.dot(m1)) > r * dx.length() * m1.length()) { + + var m = p.getHessian().sum((outerProduct(m1, m1)).multiply(1. / m1.dot(dx))); + p.setHessian(asSquareMatrix(m)); + p.setInverseHessian(inverseHessian(g0, g1, dir, p.getInverseHessian(), minimumPoint)); + + } + + p.setGradient(g1); // set g1 as the new gradient for next step + + } + + private SquareMatrix inverseHessian(Vector g1, Vector g2, Vector dir, SquareMatrix prevInvHessian, double alpha) { + Vector y = g2.subtract(g1); // g[k+1] - g[k] + + final var dx = dir.multiply(alpha); + final var m1 = dx.subtract(prevInvHessian.multiply(y)); + + var m = prevInvHessian.sum((outerProduct(m1, m1)).multiply(1. / m1.dot(y))); //SR1 formula + return asSquareMatrix(m); + } + + @Override + public String toString() { + return Messages.getString("SR1.Descriptor"); + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static SR1Optimiser getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java index 23e663d5..ee9b68e2 100644 --- a/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java +++ b/src/main/java/pulse/search/direction/SteepestDescentOptimiser.java @@ -2,81 +2,73 @@ import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.ui.Messages; /** * The simplest possible {@code PathSolver}, which assumes that the minimum * direction coincides with the inverted gradient. Used in combination with the * {@code GoldenSectionSolver} for increased accuracy. - * + * * @see pulse.search.linear.GoldenSectionOptimiser * @see Wikipedia - * page + * page */ +public class SteepestDescentOptimiser extends CompositePathOptimiser { -public class SteepestDescentOptimiser extends PathOptimiser { + /** + * + */ + private static final long serialVersionUID = -6868259511333467862L; + private static SteepestDescentOptimiser instance = new SteepestDescentOptimiser(); - private static SteepestDescentOptimiser instance = new SteepestDescentOptimiser(); + private SteepestDescentOptimiser() { + super(); + //init gradient solver + this.setSolver(p -> { - private SteepestDescentOptimiser() { - super(); - } + Vector dir = p.getGradient().inverted(); // p_k = -g + p.setDirection(dir); + return dir; - /** - *

- * The direction of the minimum at the iteration k is - * calculated simply as the the inverted gradient vector: - * pk = -gk. Invokes - * {@code p.setDirection()}. - *

- */ + }); + } - @Override - public Vector direction(Path p) { - Vector dir = p.getGradient().inverted(); // p_k = -g - p.setDirection(dir); - return dir; - } + /** + * Calculates the gradient value at the end of each step. + * + * @throws SolverException + */ + @Override + public void prepare(GeneralTask task) throws SolverException { + ((GradientGuidedPath) task.getIterativeState()).setGradient(gradient(task)); + } - /** - * Calculates the gradient value at the end of each step. - * - * @throws SolverException - */ + @Override + public String toString() { + return Messages.getString("SteepestDescentSolver.Descriptor"); + } - @Override - public void endOfStep(SearchTask task) throws SolverException { - task.getPath().setGradient(gradient(task)); - } + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static SteepestDescentOptimiser getInstance() { + return instance; + } - @Override - public String toString() { - return Messages.getString("SteepestDescentSolver.Descriptor"); - } + /** + * Creates a new {@code Path} instance for storing the gradient, direction, + * and minimum point for this {@code PathSolver}. + * + * @param t the search task + * @return a {@code Path} instance + */ + @Override + public GradientGuidedPath initState(GeneralTask t) { + return new GradientGuidedPath(t); + } - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ - - public static SteepestDescentOptimiser getInstance() { - return instance; - } - - /** - * Creates a new {@code Path} instance for storing the gradient, direction, and - * minimum point for this {@code PathSolver}. - * - * @param t the search task - * @return a {@code Path} instance - */ - - @Override - public Path createPath(SearchTask t) { - return new Path(t); - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/direction/package-info.java b/src/main/java/pulse/search/direction/package-info.java index bd4394f5..a2a9b048 100644 --- a/src/main/java/pulse/search/direction/package-info.java +++ b/src/main/java/pulse/search/direction/package-info.java @@ -3,5 +3,4 @@ * to determine the direction of the minimum of a specific {@code SearchTask} * using an iterative approach. */ - -package pulse.search.direction; \ No newline at end of file +package pulse.search.direction; 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 00000000..db1d89b2 --- /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 new file mode 100644 index 00000000..09fe7a69 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/FIPSMover.java @@ -0,0 +1,45 @@ +package pulse.search.direction.pso; + +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; + +public class FIPSMover implements Mover { + + private double chi; + private double phi; + public final static double DEFAULT_CHI = 0.7298; + public final static double DEFAULT_PHI = 4.1; + + public FIPSMover() { + chi = DEFAULT_CHI; + phi = DEFAULT_PHI; + } + + @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(); + final double nLength = (double) neighbours.length; + + 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) + .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)); + + } + +} diff --git a/src/main/java/pulse/search/direction/pso/Mover.java b/src/main/java/pulse/search/direction/pso/Mover.java new file mode 100644 index 00000000..1d7ecc21 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/Mover.java @@ -0,0 +1,7 @@ +package pulse.search.direction.pso; + +public interface Mover { + + public ParticleState attemptMove(Particle p, Particle[] neighbours, ParticleState gBest); + +} diff --git a/src/main/java/pulse/search/direction/pso/NeighbourhoodTopology.java b/src/main/java/pulse/search/direction/pso/NeighbourhoodTopology.java new file mode 100644 index 00000000..64fa9519 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/NeighbourhoodTopology.java @@ -0,0 +1,7 @@ +package pulse.search.direction.pso; + +public interface NeighbourhoodTopology { + + public Particle[] neighbours(Particle p, SwarmState ss); + +} diff --git a/src/main/java/pulse/search/direction/pso/Particle.java b/src/main/java/pulse/search/direction/pso/Particle.java new file mode 100644 index 00000000..d5031dbb --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/Particle.java @@ -0,0 +1,84 @@ +/* %% + * + * JPSO + * + * Copyright 2006 Jeff Ridder + * + * 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.direction.pso; + +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; +import pulse.tasks.SearchTask; + +/** + * Class defining a particle - the basic unit of a swarm. + */ +public class Particle { + + private int id; + + private ParticleState current; + private ParticleState pbest; + + private Particle[] neighbours; + + public Particle(ParticleState cur, int id) { + this.id = id; + current = cur; + pbest = new ParticleState(current); + } + + public void adopt(ParticleState state) { + this.current = state; + } + + public void evaluate(GeneralTask t) throws SolverException { + var params = t.searchVector(); + t.assign(current.getPosition()); + current.setFitness(t.objectiveFunction()); + t.assign(params); + + if (current.isBetterThan(pbest)) { + pbest = new ParticleState(current); + } + } + + /** + * Returns the current state (position, velocity, fitness) of the particle. + * + * @return current state. + */ + public ParticleState getCurrentState() { + return current; + } + + /** + * Returns the personal best state ever achieved by the particle. + * + * @return personal best state. + */ + public ParticleState getBestState() { + return pbest; + } + + public int getId() { + return id; + } + + public Particle[] getNeighbours() { + return neighbours; + } + +} diff --git a/src/main/java/pulse/search/direction/pso/ParticleState.java b/src/main/java/pulse/search/direction/pso/ParticleState.java new file mode 100644 index 00000000..01985e48 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/ParticleState.java @@ -0,0 +1,65 @@ +package pulse.search.direction.pso; + +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; + +public class ParticleState { + + private ParameterVector position; + private ParameterVector velocity; + private double fitness; + + public ParticleState(ParameterVector cur) { + randomise(cur); + this.velocity = new ParameterVector(cur); + + //set initial velocity to zero + velocity.setValues(new Vector(cur.dimension())); + + this.fitness = Double.MAX_VALUE; + } + + public ParticleState(ParticleState another) { + this.position = new ParameterVector(another.position); + this.velocity = new ParameterVector(another.velocity); + this.fitness = another.fitness; + } + + public ParticleState(ParameterVector p, ParameterVector v) { + this.position = p; + this.velocity = v; + } + + public boolean isBetterThan(ParticleState s) { + return this.fitness < s.fitness; + } + + 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() { + return this.position; + } + + public ParameterVector getVelocity() { + return this.velocity; + } + + public double getFitness() { + return this.fitness; + } + + protected void setFitness(double fitness) { + this.fitness = fitness; + } + +} diff --git a/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java new file mode 100644 index 00000000..f8cbf29e --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/ParticleSwarmOptimiser.java @@ -0,0 +1,73 @@ +package pulse.search.direction.pso; + +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 ConstrictionMover(); + } + + protected void moveParticles() { + var topology = swarmState.getNeighborhoodTopology(); + for (var p : swarmState.getParticles()) { + 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. + * + */ + @Override + public boolean iteration(GeneralTask task) throws SolverException { + this.prepare(task); + + swarmState.evaluate(task); + swarmState.bestSoFar(); + moveParticles(); + + swarmState.incrementStep(); + + task.assign(swarmState.getBestSoFar().getPosition()); + double cost = task.objectiveFunction(); + swarmState.setCost(cost); + + 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; + } + +} diff --git a/src/main/java/pulse/search/direction/pso/StaticTopologies.java b/src/main/java/pulse/search/direction/pso/StaticTopologies.java new file mode 100644 index 00000000..c466e8f2 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/StaticTopologies.java @@ -0,0 +1,54 @@ +package pulse.search.direction.pso; + +import java.util.Arrays; + +public class StaticTopologies { + + /** + * Global best + * + */ + public final static NeighbourhoodTopology GLOBAL = (p, state) -> state.getParticles(); + + /** + * Ring topology (1D - lattice) + */ + public final static NeighbourhoodTopology RING = (p, state) -> { + var ps = state.getParticles(); + final int i = Arrays.asList(ps).indexOf(p); + return new Particle[]{ps[i > 0 ? i - 1 : ps.length - 1], + ps[i + 1 < ps.length ? i + 1 : 0] + }; + }; + + /** + * Von Neumann topology (square lattice) Condition: if( ( ps.length & + * (ps.length - 1) ) != 0) throw new IllegalArgumentException("Number of + * particles: " + ps.length + " is not power of 2"); + */ + public final static NeighbourhoodTopology SQUARE = (p, state) -> { + var ps = state.getParticles(); + final int i = Arrays.asList(ps).indexOf(p); + + final int latticeParameter = (int) Math.sqrt(ps.length); + + final int row = i / latticeParameter; + final int column = i - row * latticeParameter; + + final int above = column + (row > 0 + ? (row - 1) * latticeParameter : (latticeParameter - 1) * latticeParameter); + + final int below = column + (row + 1 < ps.length + ? latticeParameter * (row + 1) : 0); + + final int left = row * latticeParameter + (column > 0 ? column - 1 : ps.length - 1); + final int right = row * latticeParameter + (column + 1 < ps.length ? column + 1 : 0); + + return new Particle[]{ps[left], ps[right], ps[above], ps[below]}; + }; + + private StaticTopologies() { + //empty + } + +} diff --git a/src/main/java/pulse/search/direction/pso/SwarmState.java b/src/main/java/pulse/search/direction/pso/SwarmState.java new file mode 100644 index 00000000..df5b7041 --- /dev/null +++ b/src/main/java/pulse/search/direction/pso/SwarmState.java @@ -0,0 +1,116 @@ +package pulse.search.direction.pso; + +import pulse.math.ParameterVector; +import pulse.problem.schemes.solvers.SolverException; +import pulse.search.GeneralTask; +import pulse.search.direction.IterativeState; + +public class SwarmState extends IterativeState { + + private ParameterVector seed; + + private Particle[] particles; + private NeighbourhoodTopology neighborhoodTopology; + + private ParticleState bestSoFar; + private int bestSoFarIndex; + + private final static int DEFAULT_PARTICLES = 16; + + public SwarmState() { + this(DEFAULT_PARTICLES, StaticTopologies.RING); + } + + public SwarmState(int numberOfParticles, NeighbourhoodTopology neighborhoodTopology) { + this.neighborhoodTopology = neighborhoodTopology; + this.particles = new Particle[numberOfParticles]; + this.bestSoFar = null; + this.bestSoFarIndex = -1; + } + + public void evaluate(GeneralTask t) throws SolverException { + for (Particle p : particles) { + p.evaluate(t); + } + } + + public void prepare(GeneralTask t) { + seed = t.searchVector(); + } + + public void create() { + for (int i = 0; i < particles.length; i++) { + particles[i] = new Particle(new ParticleState(seed), i); + } + } + + /** + * Returns the best state achieved by any particle so far. + * + */ + public void bestSoFar() { + int bestIndex = 0; + + double fitness = 0; + double bestFitness = Double.MAX_VALUE; + + for (int i = 0; i < particles.length; i++) { + + fitness = particles[i].getBestState().getFitness(); + + if (fitness < bestFitness) { + bestIndex = i; + bestFitness = fitness; + } + + } + + //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() { + return neighborhoodTopology; + } + + public void setNeighborhoodTopology(NeighbourhoodTopology neighborhoodTopology) { + this.neighborhoodTopology = neighborhoodTopology; + } + + /** + * Returns the particles of the swarm. + * + * @return array of Particles. + */ + public Particle[] getParticles() { + return particles; + } + + public void setParticles(Particle[] particles) { + this.particles = particles; + } + + public ParticleState getBestSoFar() { + return bestSoFar; + } + + public void setBestSoFar(ParticleState bestSoFar) { + this.bestSoFar = bestSoFar; + } + + public int getBestSoFarIndex() { + return bestSoFarIndex; + } + + public void setBestSoFarIndex(int bestSoFarIndex) { + this.bestSoFarIndex = bestSoFarIndex; + } + +} diff --git a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java index 6d5c1897..85eab130 100644 --- a/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java +++ b/src/main/java/pulse/search/linear/GoldenSectionOptimiser.java @@ -1,8 +1,10 @@ package pulse.search.linear; -import pulse.math.IndexedVector; +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; @@ -10,92 +12,97 @@ * The golden-section search is a simple dichotomy search for finding the * minimum of strictly unimodal functions by successively narrowing the domain * of the search using the golden ratio partitioning. - * + * * @see Wikipedia - * page + * page */ - public class GoldenSectionOptimiser extends LinearOptimiser { - /** - * The golden section φ, which is approximately equal to 0.618033989. - */ - - public final static double PHI = 1.0 - (3.0 - Math.sqrt(5.0)) / 2.0; - - private static GoldenSectionOptimiser instance = new GoldenSectionOptimiser(); - - private GoldenSectionOptimiser() { - super(); - } - - /** - *

- * Let {@code a} and {@code b} be the start and end point of a {@code Segment}, - * initially defined by the {@code super.domain(IndexedVector,Vector)} method. - * This method will start a loop, which at each step i will compare the - * values of the target function at the end points of a {@code Segment} - * constructed from one of the end points ai or - * bi and substituting the second end point with either - * ai + φ*(b-a) or bi - - * φ*(b-a). This theoretically ensures the least number of steps - * to reach the minimum (as compared to the standard dichotomy methods). - *

- * - * @throws SolverException - */ - - @Override - public double linearStep(SearchTask task) throws SolverException { - - final double EPS = 1e-14; - - final var params = task.searchVector(); - final Vector direction = task.getPath().getDirection(); - - var segment = domain(params[0], params[1], direction); - - final double absError = searchResolution * PHI * segment.length(); - - for (double t = PHI * segment.length(); Math.abs(t) > absError; t = PHI * segment.length()) { - final double alpha = segment.getMinimum() + t; - final double one_minus_alpha = segment.getMaximum() - t; - - final var newParams1 = params[0].sum(direction.multiply(alpha)); // alpha - task.assign(new IndexedVector(newParams1, params[0].getIndices())); - final double ss2 = task.solveProblemAndCalculateDeviation(); // f(alpha) - - final var newParams2 = params[0].sum(direction.multiply(one_minus_alpha)); // 1 - alpha - task.assign(new IndexedVector(newParams2, params[0].getIndices())); - final double ss1 = task.solveProblemAndCalculateDeviation(); // f(1-alpha) - - task.assign(new IndexedVector(newParams2, params[0].getIndices())); // return to old position - - if (ss2 - ss1 > EPS) - segment.setMaximum(alpha); - else - segment.setMinimum(one_minus_alpha); - - } - - return segment.mean(); - - } - - @Override - public String toString() { - return Messages.getString("GoldenSectionSolver.Descriptor"); - } - - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ - - public static GoldenSectionOptimiser getInstance() { - return instance; - } - -} \ No newline at end of file + /** + * + */ + private static final long serialVersionUID = -369106060533186038L; + + /** + * The golden section φ, which is approximately equal to 0.618033989. + */ + public final static double PHI = 1.0 - (3.0 - Math.sqrt(5.0)) / 2.0; + + private static GoldenSectionOptimiser instance = new GoldenSectionOptimiser(); + + private GoldenSectionOptimiser() { + super(); + } + + /** + *

+ * Let {@code a} and {@code b} be the start and end point of a + * {@code Segment}, initially defined by the + * {@code super.domain(IndexedVector,Vector)} method. This method will start + * a loop, which at each step i will compare the values of the target + * function at the end points of a {@code Segment} constructed from one of + * the end points ai or + * bi and substituting the second end point with either + * ai + φ*(b-a) or bi + * - φ*(b-a). This theoretically ensures the least number of + * steps to reach the minimum (as compared to the standard dichotomy + * methods). + *

+ * + * @throws SolverException + */ + @Override + 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); + + final double absError = searchResolution * PHI * segment.length(); + + for (double t = PHI * segment.length(); Math.abs(t) > absError; t = PHI * segment.length()) { + final double alpha = segment.getMinimum() + t; + final double one_minus_alpha = segment.getMaximum() - t; + + final var newParams1 = vParams.sum(direction.multiply(alpha)); // alpha + task.assign(new ParameterVector(params, newParams1)); + final double ss2 = task.objectiveFunction(); // f(alpha) + + final var newParams2 = vParams.sum(direction.multiply(one_minus_alpha)); // 1 - alpha + task.assign(new ParameterVector(params, newParams2)); + final double ss1 = task.objectiveFunction(); // f(1-alpha) + + task.assign(new ParameterVector(params, newParams2)); // return to old position + + if (ss2 - ss1 > EPS) { + segment.setMaximum(alpha); + } else { + segment.setMinimum(one_minus_alpha); + } + + } + + return segment.mean(); + + } + + @Override + public String toString() { + return Messages.getString("GoldenSectionSolver.Descriptor"); + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static GoldenSectionOptimiser getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/search/linear/LinearOptimiser.java b/src/main/java/pulse/search/linear/LinearOptimiser.java index e31ce2cc..a82b9188 100644 --- a/src/main/java/pulse/search/linear/LinearOptimiser.java +++ b/src/main/java/pulse/search/linear/LinearOptimiser.java @@ -1,22 +1,20 @@ package pulse.search.linear; import static java.lang.Math.abs; -import static java.lang.Math.min; import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.LINEAR_RESOLUTION; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.Set; +import pulse.math.Parameter; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -28,112 +26,126 @@ * algorithm to initialise the calculation domain. * */ - public abstract class LinearOptimiser extends PropertyHolder implements Reflexive { - protected static double searchResolution = (double) def(LINEAR_RESOLUTION).getValue(); - - protected LinearOptimiser() { - super(); - } - - /** - * Finds the minimum of the target function on the {@code domain} - * {@code Segment}. - * - * @param task the target function is the sum of squared residuals (SSR) for - * this {@code task} - * @return a double, representing the step magnitude that needs to be multiplied - * by the direction of the search determined previously using the - * {@code PathSolver} to arrive at the next set of parameters - * corresponding to a lower SSR value of this {@code task} - * @throws SolverException - */ - - public abstract double linearStep(SearchTask task) throws SolverException; - - /** - * Sets the domain for this linear search on {@code p}. - *

- * The domain is defined as a {@code Segment} {@code [0; max]}, where - * {@code max} determines the maximum magnitude of the {@code linearStep}. This - * value is calculated initially as - * max = 0.5*xi/pi, where i is the - * index of the {@code DIFFUSIVITY NumericProperty}. Later it is corrected to - * ensure that the change in the {@code HEAT_LOSS} {@code NumericProperty} is - * less than unity. - *

- * - * @param x the current set of parameters - * @param bounds the bounds for x - * @param p the result of the direction search with the {@code PathSolver} - * @return a {@code Segment} defining the domain of this search - * @see pulse.search.direction.PathSolver.direction(SearchTask) - */ - - public static Segment domain(IndexedVector x, IndexedVector bounds, Vector p) { - double alpha = Double.POSITIVE_INFINITY; - - final double EPS = 1E-15; - - for (int i = 0; i < x.dimension(); i++) { - - final double component = p.get(i); - - //check if zero - if (component < EPS && component > -EPS) - continue; - - alpha = min(alpha, abs(bounds.get(i) / component)); - - } - - return new Segment(0, alpha); - } - - /** - *

- * The linear resolution determines the minimum distance between any two points - * belonging to the {@code domain} of this search while they still are - * considered separate. In case of a partitioning method, e.g. the - * golden-section search, this determines the partitioning limit. Note different - * {@code PathSolver}s can have different sensitivities to the linear search and - * may require different linear resolutions to work effectively. - *

- * - * @return a {@code NumericProperty} with the current value of the linear - * resolution - * @see domain(IndexedVector,IndexedVector,Vector) - */ - - public static NumericProperty getLinearResolution() { - return derive(LINEAR_RESOLUTION, searchResolution); - } - - public static void setLinearResolution(NumericProperty searchError) { - LinearOptimiser.searchResolution = (double) searchError.getValue(); - } - - @Override - public String toString() { - return this.getClass().getSimpleName(); - } - - /** - * The {@code LINEAR_RESOLUTION} is the single listed parameter for this class. - * - * @see pulse.properties.NumericPropertyKeyword - */ - - @Override - public List listedTypes() { - return new ArrayList<>(Arrays.asList(def(LINEAR_RESOLUTION))); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == LINEAR_RESOLUTION) - setLinearResolution(property); - } - -} \ No newline at end of file + protected static double searchResolution = (double) def(LINEAR_RESOLUTION).getValue(); + private final static double EPS = 1E-15; + + protected LinearOptimiser() { + super(); + } + + /** + * Finds the minimum of the target function on the {@code domain} + * {@code Segment}. + * + * @param task the target function is the sum of squared residuals (SSR) for + * this {@code task} + * @return a double, representing the step magnitude that needs to be + * multiplied by the direction of the search determined previously using the + * {@code PathSolver} to arrive at the next set of parameters corresponding + * to a lower SSR value of this {@code task} + * @throws SolverException + */ + public abstract double linearStep(GeneralTask task) throws SolverException; + + /** + * Sets the domain for this linear search on {@code p}. + *

+ * The domain is defined as a {@code Segment} {@code [0; max]}, where + * {@code max} determines the maximum magnitude of the {@code linearStep}. + * This value is calculated initially as + * max = 0.5*xi/pi, where i is the + * index of the {@code DIFFUSIVITY NumericProperty}. Later it is corrected + * to ensure that the change in the {@code HEAT_LOSS} + * {@code NumericProperty} is less than unity. + *

+ * + * @param x the current set of parameter + * @param p the result of the direction search with the {@code PathSolver} + * @return a {@code Segment} defining the domain of this search + * @see pulse.search.direction.PathSolver.direction(SearchTask) + */ + public static Segment domain(ParameterVector x, Vector p) { + double alphaMax = Double.POSITIVE_INFINITY; + double alpha; + + var params = x.getParameters(); + + for (Parameter xp : params) { + + final double component = p.get(params.indexOf(xp)); + + //check if zero + if (Math.abs(component) > EPS) { + + var bound = xp.getTransformedBounds(); + + alpha = abs( + ((component > 0 ? bound.getMaximum() + : bound.getMinimum()) - xp.inverseTransform()) + / component); + + if (Double.isFinite(alpha) && alpha < alphaMax) { + alphaMax = alpha; + } + + } + + } + + //check that alphaMax is not zero! otherwise the optimise will crash + return new Segment(0.0, + Math.max(alphaMax, 1E-10)); + + } + + /** + *

+ * The linear resolution determines the minimum distance between any two + * points belonging to the {@code domain} of this search while they still + * are considered separate. In case of a partitioning method, e.g. the + * golden-section search, this determines the partitioning limit. Note + * different {@code PathSolver}s can have different sensitivities to the + * linear search and may require different linear resolutions to work + * effectively. + *

+ * + * @return a {@code NumericProperty} with the current value of the linear + * resolution + * @see domain(IndexedVector,IndexedVector,Vector) + */ + public static NumericProperty getLinearResolution() { + return derive(LINEAR_RESOLUTION, searchResolution); + } + + public static void setLinearResolution(NumericProperty searchError) { + LinearOptimiser.searchResolution = (double) searchError.getValue(); + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + /** + * The {@code LINEAR_RESOLUTION} is the single listed parameter for this + * class. + * + * @see pulse.properties.NumericPropertyKeyword + */ + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(LINEAR_RESOLUTION); + return set; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == LINEAR_RESOLUTION) { + setLinearResolution(property); + } + } + +} diff --git a/src/main/java/pulse/search/linear/WolfeOptimiser.java b/src/main/java/pulse/search/linear/WolfeOptimiser.java index 5bd11959..d540ba3b 100644 --- a/src/main/java/pulse/search/linear/WolfeOptimiser.java +++ b/src/main/java/pulse/search/linear/WolfeOptimiser.java @@ -2,11 +2,13 @@ import static java.lang.Math.abs; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; import pulse.math.Segment; import pulse.math.linear.Vector; import pulse.problem.schemes.solvers.SolverException; -import pulse.search.direction.Path; +import pulse.search.GeneralTask; +import pulse.search.direction.GradientBasedOptimiser; +import pulse.search.direction.GradientGuidedPath; import pulse.search.direction.PathOptimiser; import pulse.tasks.SearchTask; import pulse.ui.Messages; @@ -17,131 +19,136 @@ * inexact linear search. This type of linear search works best with the * {@code ApproximatedHessianSolver}. *

- * - * @see pulse.search.direction.ApproximatedHessianOptimiser + * + * @see pulse.search.direction.BFGSOptimiser * @see Wikipedia - * page + * page */ - public class WolfeOptimiser extends LinearOptimiser { - private static WolfeOptimiser instance = new WolfeOptimiser(); - - /** - * The constant used in the Armijo inequality, equal to {@value C1}. - */ - - public final static double C1 = 0.05; - - /** - * The constant used in the strong Wolfe inequality for the modulus of the - * gradient projection, equal to {@value C2}. - */ - - public final static double C2 = 0.8; - - private WolfeOptimiser() { - super(); - } - - /** - *

- * This uses a combination of the Wolfe conditions for conducting an inexact - * line search with the domain partitioning using a random number generator. The - * partitioning is done in such a way that: (a) whenever the Armijo inequality - * is not satisfied, the original domain {@code [a; b]} is reduced to - * [ai; α], where α is the random number - * confined inside [ai; bi]; (b) when the - * Armijo inequality is satisfied and the second (strong) Wolfe condition for - * the modulus of the gradient projection is not satisfied, the α value is - * used to substitute the lower end point for the search domain: [α; - * bi]. As this is done iteratively, the length of the - * associated {@code Segment} will decrease. The method will return a value if - * either the strong Wolfe conditions are strictly satisfied, or if the linear - * precision has been reached. - *

- * - * @throws SolverException - */ - - @Override - public double linearStep(SearchTask task) throws SolverException { - - Path p = task.getPath(); - - 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(); - Segment segment = domain(params[0], params[1], direction); - - double ss1 = task.solveProblemAndCalculateDeviation(); - - double randomConfinedValue = 0; - double g2p; - - for (double initialLength = segment.length(); segment.length() / initialLength > searchResolution;) { - - randomConfinedValue = segment.randomValue(); - - final var newParams = params[0].sum(direction.multiply(randomConfinedValue)); - task.assign(new IndexedVector(newParams, params[0].getIndices())); - - final double ss2 = task.solveProblemAndCalculateDeviation(); - - /** - * Checks if the first Armijo inequality is not satisfied. In this case, it will - * set the maximum of the search domain to the {@code randomConfinedValue}. - */ - - if (ss2 - ss1 > C1 * randomConfinedValue * G1P) { - segment.setMaximum(randomConfinedValue); - continue; - } - - final var g2 = PathOptimiser.gradient(task); - g2p = g2.dot(direction); - - /** - * This is the strong Wolfe condition that ensures that the absolute value of - * the projection of the gradient decreases. - */ - - if (abs(g2p) <= C2 * G1P_ABS) - break; - - /* + /** + * + */ + private static final long serialVersionUID = 5200832276052099700L; + + private static WolfeOptimiser instance = new WolfeOptimiser(); + + /** + * The constant used in the Armijo inequality, equal to {@value C1}. + */ + public final static double C1 = 0.05; + + /** + * The constant used in the strong Wolfe inequality for the modulus of the + * gradient projection, equal to {@value C2}. + */ + public final static double C2 = 0.8; + + private WolfeOptimiser() { + super(); + } + + /** + *

+ * This uses a combination of the Wolfe conditions for conducting an inexact + * line search with the domain partitioning using a random number generator. + * The partitioning is done in such a way that: (a) whenever the Armijo + * inequality is not satisfied, the original domain {@code [a; b]} is + * reduced to + * [ai; α], where α is the random + * number confined inside [ai; bi]; (b) + * when the Armijo inequality is satisfied and the second (strong) Wolfe + * condition for the modulus of the gradient projection is not satisfied, + * the α value is used to substitute the lower end point for the + * search domain: [α; + * bi]. As this is done iteratively, the length of the + * associated {@code Segment} will decrease. The method will return a value + * if either the strong Wolfe conditions are strictly satisfied, or if the + * linear precision has been reached. + *

+ * + * @throws SolverException + */ + @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(); + + /** + * Checks if the first Armijo inequality is not satisfied. In this + * case, it will set the maximum of the search domain to the + * {@code randomConfinedValue}. + */ + if (cost2 - cost1 > C1 * randomConfinedValue * G1P) { + segment.setMaximum(randomConfinedValue); + continue; + } + + final var g2 = optimiser.gradient(task); + g2p = g2.dot(direction); + + /** + * This is the strong Wolfe condition that ensures that the absolute + * value of the projection of the gradient decreases. + */ + if (abs(g2p) <= C2 * G1P_ABS) { + break; + } + + /* * if( g2p >= C2*G1P ) break; - */ - - segment.setMinimum(randomConfinedValue); - - } + */ + segment.setMinimum(randomConfinedValue); - task.assign(params[0]); - p.setGradient(g1); + } - return randomConfinedValue; + task.assign(params); + p.setGradient(g1); - } + return randomConfinedValue; - @Override - public String toString() { - return Messages.getString("WolfeSolver.Descriptor"); //$NON-NLS-1$ - } + } - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ + @Override + public String toString() { + return Messages.getString("WolfeSolver.Descriptor"); //$NON-NLS-1$ + } - public static WolfeOptimiser getInstance() { - return instance; - } + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static WolfeOptimiser getInstance() { + return instance; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/linear/package-info.java b/src/main/java/pulse/search/linear/package-info.java index 2355e067..7acb4733 100644 --- a/src/main/java/pulse/search/linear/package-info.java +++ b/src/main/java/pulse/search/linear/package-info.java @@ -3,5 +3,4 @@ * of a vector variable that is unimodal on a specific {@code Segment}. These * should be subclasses of {@code LinearSolver}. */ - -package pulse.search.linear; \ No newline at end of file +package pulse.search.linear; diff --git a/src/main/java/pulse/search/statistics/AICStatistic.java b/src/main/java/pulse/search/statistics/AICStatistic.java index 9e6fe0bb..5e04b383 100644 --- a/src/main/java/pulse/search/statistics/AICStatistic.java +++ b/src/main/java/pulse/search/statistics/AICStatistic.java @@ -1,37 +1,42 @@ package pulse.search.statistics; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; - -import pulse.tasks.SearchTask; - /** * AIC algorithm: Banks, H. T., & Joyner, M. L. (2017). Applied Mathematics * Letters, 74, 33–45. doi:10.1016/j.aml.2017.05.005 - * + * */ - -public class AICStatistic extends SumOfSquares { - - private int kq; - private final static double PENALISATION_FACTOR = Math.log(2.0 * Math.PI) + 1.0; - - @Override - public void evaluate(SearchTask t) { - kq = t.alteredParameters().size(); - super.evaluate(t); - double n = getResiduals().size(); - final double stat = n * Math.log((double)getStatistic().getValue()) + 2.0 * (kq + 1) + n * PENALISATION_FACTOR; - this.setStatistic(derive(OPTIMISER_STATISTIC, stat)); - } - - @Override - public String getDescriptor() { - return "Akaike Information Criterion (AIC)"; - } - - public int getNumVariables() { - return kq; - } +public class AICStatistic extends ModelSelectionCriterion { + + private static final long serialVersionUID = 8549601688520099629L; + + public AICStatistic(OptimiserStatistic os) { + super(os); + } + + public AICStatistic(ModelSelectionCriterion another) { + super(another); + } + + public AICStatistic() { + super(new SumOfSquares()); + } + + @Override + public ModelSelectionCriterion copy() { + return new AICStatistic(this); + } + + /** + * @return the AIC penalising term. + */ + @Override + public double penalisingTerm(final int kq, final int n) { + return 2.0 * (kq + 1); + } + + @Override + public String getDescriptor() { + return "Akaike Information Criterion (AIC)"; + } } diff --git a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java index 472c47d9..37b2561f 100644 --- a/src/main/java/pulse/search/statistics/AbsoluteDeviations.java +++ b/src/main/java/pulse/search/statistics/AbsoluteDeviations.java @@ -1,23 +1,56 @@ package pulse.search.statistics; -import static java.lang.Math.*; 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 + * norm condition. Similar to the least squares technique, it attempts to find a + * function which closely approximates a set of data. However, unlike the L2 + * norm, it is much more robust to data outliers. + * + */ +public class AbsoluteDeviations extends OptimiserStatistic { -public class AbsoluteDeviations extends ResidualStatistic { + private static final long serialVersionUID = 3385019714627583467L; - @Override - public void evaluate(SearchTask t) { - calculateResiduals(t); - final double statistic = getResiduals().stream().map(r -> abs(r[1]) ).reduce(Double::sum).get() / getResiduals().size(); - setStatistic(derive(OPTIMISER_STATISTIC, statistic)); - } + public AbsoluteDeviations() { + super(); + } - @Override - public String getDescriptor() { - return "Absolute Deviations"; - } + public AbsoluteDeviations(AbsoluteDeviations another) { + super(another); + } + + /** + * Calculates the L1 norm statistic, which simply sums up the absolute + * values of residuals. + * + * @param t + */ + @Override + public void evaluate(GeneralTask t) { + calculateResiduals(t); + final double statistic = getResiduals().stream() + .mapToDouble(a -> Math.abs(a)).average().getAsDouble(); + setStatistic(derive(OPTIMISER_STATISTIC, statistic)); + } + + @Override + public double variance() { + final double stat = (double) this.getStatistic().getValue(); + return stat * stat; + } + + @Override + public String getDescriptor() { + return "Absolute Deviations"; + } + + @Override + public OptimiserStatistic copy() { + return new AbsoluteDeviations(this); + } } diff --git a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java index bcfd2aba..84d51043 100644 --- a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java +++ b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java @@ -1,39 +1,54 @@ package pulse.search.statistics; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.PROBABILITY; 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; +/** + * The Anderson-Darling normality test. In this variant of the test, the mean + * and the variance are assumed to be known. + * + */ public class AndersonDarlingTest extends NormalityTest { - @Override - public boolean test(SearchTask task) { - calculateResiduals(task); - - double[] residuals = super.transformResiduals(task); - var nd = new NormalDist(0.0, (new StandardDeviation()).evaluate(residuals)); - var testResult = GofStat.andersonDarling(residuals, nd); - - this.setStatistic(derive(TEST_STATISTIC, testResult[0])); - setProbability(derive(PROBABILITY, testResult[1])); - - return significanceTest(); - } - - @Override - public String getDescriptor() { - return "Anderson-Darling test"; - } - - @Override - public void evaluate(SearchTask t) { - test(t); - } - -} \ No newline at end of file + 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 + */ + @Override + public boolean test(GeneralTask task) { + calculateResiduals(task); + + double[] residuals = residualsArray(); + var nd = new NormalDist(0.0, (new StandardDeviation()).evaluate(residuals)); + var testResult = GofStat.andersonDarling(residuals, nd); + + this.setStatistic(derive(TEST_STATISTIC, testResult[0])); + + //compare the p-value and the significance + return testResult[1] > significance; + } + + @Override + public String getDescriptor() { + return "Anderson-Darling test"; + } + + @Override + public void evaluate(GeneralTask t) { + test(t); + } + +} diff --git a/src/main/java/pulse/search/statistics/BICStatistic.java b/src/main/java/pulse/search/statistics/BICStatistic.java new file mode 100644 index 00000000..c0d09aac --- /dev/null +++ b/src/main/java/pulse/search/statistics/BICStatistic.java @@ -0,0 +1,46 @@ +package pulse.search.statistics; + +import static java.lang.Math.log; + +/** + * Bayesian Information Criterion (BIC) algorithm formulated for the Gaussian + * distribution of residuals. This is used in model selection. BIC values are + * always negative. The absolute BIC value is meaningless, it is only used as a + * comparative statistic. + * + */ +public class BICStatistic extends ModelSelectionCriterion { + + private static final long serialVersionUID = 737642724262758403L; + + public BICStatistic(ModelSelectionCriterion another) { + super(another); + } + + public BICStatistic(OptimiserStatistic os) { + super(os); + } + + public BICStatistic() { + super(new SumOfSquares()); + } + + @Override + public ModelSelectionCriterion copy() { + return new BICStatistic(this); + } + + /** + * @return the BIC penalising term + */ + @Override + public double penalisingTerm(final int kq, final int n) { + return (kq + 1) * log(n); + } + + @Override + public String getDescriptor() { + return "Bayesian Information Criterion (BIC)"; + } + +} diff --git a/src/main/java/pulse/search/statistics/CorrelationTest.java b/src/main/java/pulse/search/statistics/CorrelationTest.java index b001d411..ca7c9423 100644 --- a/src/main/java/pulse/search/statistics/CorrelationTest.java +++ b/src/main/java/pulse/search/statistics/CorrelationTest.java @@ -7,45 +7,54 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.util.InstanceDescriptor; import pulse.util.PropertyHolder; import pulse.util.Reflexive; public abstract class CorrelationTest extends PropertyHolder implements Reflexive { - private static double threshold = (double) def(CORRELATION_THRESHOLD).getValue(); - private static String selectedTestDescriptor; + private static double threshold = (double) def(CORRELATION_THRESHOLD).getValue(); - public CorrelationTest() { - //intentionall blank - } + private static final InstanceDescriptor instanceDescriptor + = new InstanceDescriptor<>( + "Correlation Test Selector", CorrelationTest.class); - public abstract double evaluate(double[] x, double[] y); + static { + instanceDescriptor.setSelectedDescriptor(EmptyCorrelationTest.class.getSimpleName()); + } - public boolean compareToThreshold(double value) { - return Math.abs(value) > threshold; - } + public CorrelationTest() { + //intentionally blank + } - public static NumericProperty getThreshold() { - return derive(CORRELATION_THRESHOLD, threshold); - } + public static CorrelationTest init() { + return instanceDescriptor.newInstance(CorrelationTest.class); + } - public static void setThreshold(NumericProperty p) { - requireType(p, CORRELATION_THRESHOLD); - threshold = (double) p.getValue(); - } + public final static InstanceDescriptor getTestDescriptor() { + return instanceDescriptor; + } - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == NumericPropertyKeyword.CORRELATION_THRESHOLD) - threshold = (double) property.getValue(); - } + public abstract double evaluate(double[] x, double[] y); - public static String getSelectedTestDescriptor() { - return selectedTestDescriptor; - } + public boolean compareToThreshold(double value) { + return Math.abs(value) > threshold; + } - public static void setSelectedTestDescriptor(String selectedTestDescriptor) { - CorrelationTest.selectedTestDescriptor = selectedTestDescriptor; - } + public static NumericProperty getThreshold() { + return derive(CORRELATION_THRESHOLD, threshold); + } -} \ No newline at end of file + public static void setThreshold(NumericProperty p) { + requireType(p, CORRELATION_THRESHOLD); + threshold = (double) p.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == NumericPropertyKeyword.CORRELATION_THRESHOLD) { + threshold = (double) property.getValue(); + } + } + +} diff --git a/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java b/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java index a4b2d9de..c69dbb3b 100644 --- a/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java +++ b/src/main/java/pulse/search/statistics/EmptyCorrelationTest.java @@ -2,14 +2,16 @@ public class EmptyCorrelationTest extends CorrelationTest { - @Override - public double evaluate(double[] x, double[] y) { - return 0; - } - - @Override - public String getDescriptor() { - return "Don't test please"; - } + private static final long serialVersionUID = -2462666081516562018L; + + @Override + public double evaluate(double[] x, double[] y) { + return 0; + } + + @Override + public String getDescriptor() { + return "Don't test please"; + } } diff --git a/src/main/java/pulse/search/statistics/EmptyTest.java b/src/main/java/pulse/search/statistics/EmptyTest.java index 652de5a2..4434f83f 100644 --- a/src/main/java/pulse/search/statistics/EmptyTest.java +++ b/src/main/java/pulse/search/statistics/EmptyTest.java @@ -1,26 +1,27 @@ package pulse.search.statistics; -import pulse.tasks.SearchTask; +import pulse.search.GeneralTask; public class EmptyTest extends NormalityTest { - /** - * Always returns true - */ + private static final long serialVersionUID = 5919796302195242667L; - @Override - public boolean test(SearchTask task) { - return true; - } + /** + * Always returns true + */ + @Override + public boolean test(GeneralTask task) { + return true; + } - @Override - public String getDescriptor() { - return "Don't test please"; - } + @Override + public String getDescriptor() { + return "Don't test please"; + } - @Override - public void evaluate(SearchTask t) { - // deliberately empty - } + @Override + public void evaluate(GeneralTask t) { + // deliberately empty + } -} \ 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 new file mode 100644 index 00000000..4d9da690 --- /dev/null +++ b/src/main/java/pulse/search/statistics/FTest.java @@ -0,0 +1,140 @@ +package pulse.search.statistics; + +import org.apache.commons.math3.distribution.FDistribution; +import pulse.tasks.Calculation; + +/** + * A static class for testing two calculations based on the Fischer test + * (F-Test) implemented in Apache Commons Math. + */ +public class FTest { + + /** + * False-rejection probability for the F-test, equal to + * {@value FALSE_REJECTION_PROBABILITY} + */ + public final static double FALSE_REJECTION_PROBABILITY = 0.05; + + private FTest() { + //intentionall blank + } + + /** + * Tests two models to see which one is better according to the F-test + * + * @param a a calculation + * @param b another calculation + * @return {@code null} if the result is inconclusive, otherwise the best of + * two calculations. + * @see FTest.evaluate() + */ + public static Calculation test(Calculation a, Calculation b) { + + double[] data = evaluate(a, b); + + Calculation best = null; + + if (data != null) { + + //Under the null hypothesis the general model does not provide + //a significantly better fit than the nested model + Calculation nested = findNested(a, b); + + //if the F-statistic is greater than the F-critical, reject the null hypothesis. + if (nested == a) { + best = data[0] > data[1] ? b : a; + } else { + best = data[0] > data[1] ? a : b; + } + + } + + return best; + + } + + /** + * Evaluates the F-statistic for two calculations. + * + * @param a a calculation + * @param b another calculation + * @return {@code null} if the test is inconclusive, i.e., if models are not + * nested, or if the model selection criteria are based on a statistic + * different from least-squares, or if the calculations refer to different + * data ranges. Otherwise returns an double array, consisting of two + * elements {@code [fStatistic, fCritical] } + */ + public static double[] evaluate(Calculation a, Calculation b) { + + Calculation nested = findNested(a, b); + + double[] result = null; + + //if one of the models is nested into the other + if (nested != null) { + Calculation general = nested == a ? b : a; + + ResidualStatistic nestedResiduals = nested.getModelSelectionCriterion().getOptimiserStatistic(); + ResidualStatistic generalResiduals = general.getModelSelectionCriterion().getOptimiserStatistic(); + + final int nNested = nestedResiduals.getResiduals().size(); //sample size + final int nGeneral = generalResiduals.getResiduals().size(); //sample size + + //if both models use a sum-of-square statistic for the model selection criteria + //and if both calculations refer to the same calculation range + if (nestedResiduals.getClass() == generalResiduals.getClass() + && nestedResiduals.getClass() == SumOfSquares.class + && nNested == nGeneral) { + + double rssNested = ((Number) ((SumOfSquares) nestedResiduals).getStatistic().getValue()).doubleValue(); + double rssGeneral = ((Number) ((SumOfSquares) generalResiduals).getStatistic().getValue()).doubleValue(); + + int kGeneral = general.getModelSelectionCriterion().getNumVariables(); + int kNested = nested.getModelSelectionCriterion().getNumVariables(); + + if (kGeneral > kNested && nGeneral > kGeneral) { + + double fStatistic = (rssNested - rssGeneral) + / (kGeneral - kNested) + / (rssGeneral / (nGeneral - kGeneral)); + + var fDistribution = new FDistribution(kGeneral - kNested, nGeneral - kGeneral); + + double fCritical = fDistribution.inverseCumulativeProbability(1.0 - FALSE_REJECTION_PROBABILITY); + + result = new double[]{fStatistic, fCritical}; + + } + + } + + } + + return result; + + } + + /** + * Tests two models to see which one is nested in the other. A model is + * considered nested if it refers to the same class of problems but has + * fewer parameters. + * + * @param a a calculation + * @param b another calculation + * @return {@code null} if the models refer to different problem classes. + * Otherwise returns the model that is nested in the second model. + */ + public static Calculation findNested(Calculation a, Calculation b) { + var classA = a.getProblem().getClass(); + var classB = b.getProblem().getClass(); + if (!classA.isAssignableFrom(classB) && !classB.isAssignableFrom(classA)) { + return null; + } + + int aParams = a.getModelSelectionCriterion().getNumVariables(); + int bParams = b.getModelSelectionCriterion().getNumVariables(); + + return aParams > bParams ? b : a; + } + +} diff --git a/src/main/java/pulse/search/statistics/KSTest.java b/src/main/java/pulse/search/statistics/KSTest.java index 9496cbb8..70350d30 100644 --- a/src/main/java/pulse/search/statistics/KSTest.java +++ b/src/main/java/pulse/search/statistics/KSTest.java @@ -1,42 +1,47 @@ package pulse.search.statistics; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.PROBABILITY; import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; 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.search.GeneralTask; -import pulse.tasks.SearchTask; - +/** + * The Kolmogorov-Smirnov normality test as implemented in + * {@code ApacheCommonsMath}. + * + */ public class KSTest extends NormalityTest { - private double[] residuals; - private NormalDistribution nd; - - @Override - public boolean test(SearchTask task) { - evaluate(task); - setProbability(derive(PROBABILITY, TestUtils.kolmogorovSmirnovTest(nd, residuals))); - return significanceTest(); - } - - @Override - public void evaluate(SearchTask t) { - calculateResiduals(t); - residuals = transformResiduals(t); - - final double sd = (new StandardDeviation()).evaluate(residuals); - nd = new NormalDistribution(0.0, sd); // null hypothesis: normal distribution with zero mean and empirical - // standard dev - final double statistic = TestUtils.kolmogorovSmirnovStatistic(nd, residuals); - this.setStatistic(derive(TEST_STATISTIC, statistic)); - } - - @Override - public String getDescriptor() { - return "Kolmogorov-Smirnov test"; - } - -} \ No newline at end of file + private double[] residuals; + private NormalDistribution nd; + + @Override + public boolean test(GeneralTask task) { + evaluate(task); + + this.setStatistic(derive(TEST_STATISTIC, + TestUtils.kolmogorovSmirnovStatistic(nd, residuals))); + return !TestUtils.kolmogorovSmirnovTest(nd, residuals, this.significance); + } + + @Override + public void evaluate(GeneralTask t) { + calculateResiduals(t); + residuals = residualsArray(); + + final double sd = (new StandardDeviation()).evaluate(residuals); + nd = new NormalDistribution(0.0, sd); // null hypothesis: normal distribution with zero mean and empirical + // standard dev + final double statistic = TestUtils.kolmogorovSmirnovStatistic(nd, residuals); + this.setStatistic(derive(TEST_STATISTIC, statistic)); + } + + @Override + public String getDescriptor() { + return "Kolmogorov-Smirnov test"; + } + +} diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java new file mode 100644 index 00000000..69518450 --- /dev/null +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -0,0 +1,156 @@ +package pulse.search.statistics; + +import static java.lang.Math.PI; +import static java.lang.Math.exp; +import static java.lang.Math.log; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericProperty.requireType; +import static pulse.properties.NumericPropertyKeyword.MODEL_CRITERION; +import static pulse.properties.NumericPropertyKeyword.MODEL_WEIGHT; + +import java.util.List; + +import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; +import pulse.util.PropertyEvent; + +/** + * An abstract superclass for the BIC and AIC statistics. + * + */ +public abstract class ModelSelectionCriterion extends Statistic { + + private OptimiserStatistic os; + private int kq; //the number of parameters (dimensionality of the search vector) + private final static double PENALISATION_FACTOR = 1.0 + log(2 * PI); + private double criterion; + + public ModelSelectionCriterion(OptimiserStatistic os) { + super(); + setOptimiserStatistic(os); + } + + public ModelSelectionCriterion(ModelSelectionCriterion another) { + this.os = another.os.copy(); + this.kq = another.kq; + 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 + calcCriterion(); + } + + /** + * This calculates either the AIC or BIC statistic, which only differ by the + * penalising term. + * + * @see penalisingTerm() + */ + public void calcCriterion() { + final int n = os.getResiduals().size(); //sample size + criterion = n * log(os.variance()) + penalisingTerm(kq, n) + n * PENALISATION_FACTOR; + this.tellParent(new PropertyEvent(null, this, getStatistic())); + } + + /** + * The penalising term, which is different depending on implementation. + * + * @param k the number of model variables + * @param n the sample size + * @return the penalising term + */ + public abstract double penalisingTerm(int k, int n); + + public abstract ModelSelectionCriterion copy(); + + /** + * Calculates the weight (in the Akaike sense) when comparing the model + * associated with this statistic with other models represented by + * statistics of the same type. + * + * @param the selection statistics of the same type as this one + * @return a {@code NumericProperty} of the {@code MODEL_WEIGHT} type, which + * is the probability this model is the best one. + */ + public NumericProperty weight(List all) { + if (all.stream().anyMatch(s -> s.getClass() != this.getClass())) { + throw new IllegalArgumentException("Cannot mix different model selection criteria!"); + } + final double sum = all.stream().map(criterion -> criterion.probability(all)).reduce((a, b) -> a + b).get(); + return derive(MODEL_WEIGHT, probability(all) / sum); + } + + /** + * Calculates the probability that this model is the best among {@code all} + * others. + * + * @param all statistics from models that will be compared with this one + * @return the probability, which is a decimal value within the [0,1] range. + */ + public double probability(List all) { + final double min = all.stream().map(criterion -> (double) criterion.getStatistic().getValue()).reduce((a, b) -> a < b ? a : b).get(); + final double di = (double) this.getStatistic().getValue() - min; + return exp(-0.5 * di); + } + + public int getNumVariables() { + return kq; + } + + public OptimiserStatistic getOptimiserStatistic() { + return os; + } + + public final void setOptimiserStatistic(OptimiserStatistic os) { + this.os = os; + } + + public final void setStatistic(NumericProperty p) { + requireType(p, MODEL_CRITERION); + this.criterion = (double) p.getValue(); + } + + public NumericProperty getStatistic() { + return derive(MODEL_CRITERION, criterion); + } + + @Override + public void set(NumericPropertyKeyword key, NumericProperty p) { + if (key == MODEL_CRITERION) { + setStatistic(p); + } + } + +} diff --git a/src/main/java/pulse/search/statistics/NormalityTest.java b/src/main/java/pulse/search/statistics/NormalityTest.java index fa311532..498a7f08 100644 --- a/src/main/java/pulse/search/statistics/NormalityTest.java +++ b/src/main/java/pulse/search/statistics/NormalityTest.java @@ -3,74 +3,75 @@ import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.PROBABILITY; import static pulse.properties.NumericPropertyKeyword.SIGNIFICANCE; import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; +/** + * A normality test is invoked after a task finishes, to validate its result. It + * may be used as an acceptance criterion for tasks. + * + * For the test to pass, the model residuals need be distributed according to a + * (0, σ) normal distribution, where σ is the variance of the model + * 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. + * + */ public abstract class NormalityTest extends ResidualStatistic { - private double statistic; - private double probability; - private static double significance = (double) def(SIGNIFICANCE).getValue(); + private double statistic; + protected static double significance = (double) def(SIGNIFICANCE).getValue(); - private static String selectedTestDescriptor; + private static String selectedTestDescriptor; - protected NormalityTest() { - probability = (double) def(PROBABILITY).getValue(); - statistic = (double) def(TEST_STATISTIC).getValue(); - } - - public boolean significanceTest() { - return probability > significance; - } + protected NormalityTest() { + statistic = (double) def(TEST_STATISTIC).getValue(); + } - public static NumericProperty getStatisticalSignifiance() { - return derive(SIGNIFICANCE, significance); - } + public static NumericProperty getStatisticalSignifiance() { + return derive(SIGNIFICANCE, significance); + } - public static void setStatisticalSignificance(NumericProperty alpha) { - requireType(alpha, SIGNIFICANCE); - NormalityTest.significance = (double) alpha.getValue(); - } + public static void setStatisticalSignificance(NumericProperty alpha) { + requireType(alpha, SIGNIFICANCE); + NormalityTest.significance = (double) alpha.getValue(); + } - public NumericProperty getProbability() { - return derive(PROBABILITY, probability); - } + public abstract boolean test(GeneralTask task); - public abstract boolean test(SearchTask task); + @Override + public NumericProperty getStatistic() { + return derive(TEST_STATISTIC, statistic); + } - @Override - public NumericProperty getStatistic() { - return derive(TEST_STATISTIC, statistic); - } + @Override + public void setStatistic(NumericProperty statistic) { + requireType(statistic, TEST_STATISTIC); + this.statistic = (double) statistic.getValue(); + } - @Override - public void setStatistic(NumericProperty statistic) { - requireType(statistic, TEST_STATISTIC); - this.statistic = (double) statistic.getValue(); - } - - public void setProbability(NumericProperty probability) { - requireType(probability, PROBABILITY); - this.probability = (double) probability.getValue(); - } + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == TEST_STATISTIC) { + statistic = (double) property.getValue(); + } + } - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == TEST_STATISTIC) - statistic = (double) property.getValue(); - } + public static String getSelectedTestDescriptor() { + return selectedTestDescriptor; + } - public static String getSelectedTestDescriptor() { - return selectedTestDescriptor; - } + public static void setSelectedTestDescriptor(String selectedTestDescriptor) { + NormalityTest.selectedTestDescriptor = selectedTestDescriptor; + } - public static void setSelectedTestDescriptor(String selectedTestDescriptor) { - NormalityTest.selectedTestDescriptor = selectedTestDescriptor; - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/OptimiserStatistic.java b/src/main/java/pulse/search/statistics/OptimiserStatistic.java new file mode 100644 index 00000000..7cb5c992 --- /dev/null +++ b/src/main/java/pulse/search/statistics/OptimiserStatistic.java @@ -0,0 +1,32 @@ +package pulse.search.statistics; + +/** + * An Optimiser statistic is simply the objective function that is calculated by + * the Optimiser. + * + */ +public abstract class OptimiserStatistic extends ResidualStatistic { + + private static String selectedOptimiserDescriptor; + + public OptimiserStatistic(OptimiserStatistic stat) { + super(stat); + } + + protected OptimiserStatistic() { + super(); + } + + public static String getSelectedOptimiserDescriptor() { + return selectedOptimiserDescriptor; + } + + public static void setSelectedOptimiserDescriptor(String selectedTestDescriptor) { + OptimiserStatistic.selectedOptimiserDescriptor = selectedTestDescriptor; + } + + public abstract OptimiserStatistic copy(); + + public abstract double variance(); + +} diff --git a/src/main/java/pulse/search/statistics/PearsonCorrelation.java b/src/main/java/pulse/search/statistics/PearsonCorrelation.java index 1de6b0db..50ffe90b 100644 --- a/src/main/java/pulse/search/statistics/PearsonCorrelation.java +++ b/src/main/java/pulse/search/statistics/PearsonCorrelation.java @@ -2,16 +2,23 @@ import org.apache.commons.math3.stat.correlation.PearsonsCorrelation; +/** + * Wrapper {@code CorrelationTest} class for ApacheCommonsMath Pearson + * Correlation. + * + */ public class PearsonCorrelation extends CorrelationTest { - @Override - public double evaluate(double[] x, double[] y) { - return (new PearsonsCorrelation()).correlation(x, y); - } + private static final long serialVersionUID = 4819197257434836120L; - @Override - public String getDescriptor() { - return "Pearson's Product-Moment Correlation"; - } + @Override + public double evaluate(double[] x, double[] y) { + return (new PearsonsCorrelation()).correlation(x, y); + } -} \ No newline at end of file + @Override + public String getDescriptor() { + return "Pearson's Product-Moment Correlation"; + } + +} diff --git a/src/main/java/pulse/search/statistics/RSquaredTest.java b/src/main/java/pulse/search/statistics/RSquaredTest.java index 48822b5b..807c39d9 100644 --- a/src/main/java/pulse/search/statistics/RSquaredTest.java +++ b/src/main/java/pulse/search/statistics/RSquaredTest.java @@ -1,98 +1,86 @@ 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 {@code HeatingCurve} - *provides for the {@code ExperimentalData} + * The coefficient of determination represents the goodness of fit that a + * {@code HeatingCurve} provides for the {@code ExperimentalData} * */ - public class RSquaredTest extends NormalityTest { - private SumOfSquares sos; - private static NumericProperty signifiance = derive(SIGNIFICANCE, 0.2); - - public RSquaredTest() { - super(); - } - - public RSquaredTest(SumOfSquares sos) { - this(); - this.sos = sos; - } - - @Override - public boolean test(SearchTask task) { - evaluate(task); - return getStatistic().compareTo(signifiance) > 0; - } - - /** - * Calculates the coefficient of determination, or simply the - * R2 value. - *

- * First, the mean temperature of the {@code data} is calculated. Then, the - * {@code TSS} (total sum of squares) is calculated as proportional to the - * variance of data. The residual sum of squares ({@code RSS}) is calculated by - * calling {@code this.deviationSquares(curve)}. Finally, these values are - * combined together as: {@code 1 - RSS/TSS}. - *

- * - * @param t the task containing the reference data - * @see Wikipedia - * page - */ - - @Override - public void evaluate(SearchTask t) { - var reference = t.getExperimentalCurve(); - - final int start = reference.getIndexRange().getLowerBound(); - final int end = reference.getIndexRange().getUpperBound(); - - final double mean = mean(reference, start, end); - double TSS = 0; - - for (int i = start; i < end; i++) { - TSS += pow(reference.signalAt(i) - mean, 2); - } - - TSS /= (end - start); - - 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; - } - - public SumOfSquares getSumOfSquares() { - return sos; - } - - public void setSumOfSquares(SumOfSquares sos) { - this.sos = sos; - } - - @Override - public String getDescriptor() { - return "R2 test"; - } - -} \ No newline at end of file + private static final long serialVersionUID = -2022982190434832373L; + private SumOfSquares sos; + private static NumericProperty signifiance = derive(SIGNIFICANCE, 0.2); + + public RSquaredTest() { + super(); + sos = new SumOfSquares(); + } + + @Override + public boolean test(GeneralTask task) { + evaluate(task); + sos = new SumOfSquares(); + return getStatistic().compareTo(signifiance) > 0; + } + + /** + * Calculates the coefficient of determination, or simply the + * R2 value. + *

+ * First, the mean temperature of the {@code data} is calculated. Then, the + * {@code TSS} (total sum of squares) is calculated as proportional to the + * variance of data. The residual sum of squares ({@code RSS}) is calculated + * by calling {@code this.deviationSquares(curve)}. Finally, these values + * are combined together as: {@code 1 - RSS/TSS}. + *

+ * + * @param t the task containing the reference data + * @see Wikipedia + * page + */ + @Override + public void evaluate(GeneralTask t) { + var yr = t.getInput().getY(); + sos.evaluate(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))); + } + + private double mean(List input) { + return input.stream().mapToDouble(d -> d).average().getAsDouble(); + } + + public SumOfSquares getSumOfSquares() { + return sos; + } + + public void setSumOfSquares(SumOfSquares sos) { + this.sos = sos; + } + + @Override + public String getDescriptor() { + return "R2 test"; + } + +} 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 00000000..68943a74 --- /dev/null +++ b/src/main/java/pulse/search/statistics/RangePenalisedLeastSquares.java @@ -0,0 +1,61 @@ +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 static final long serialVersionUID = 4068238957339821770L; + 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/RangePenalisedSSR.java b/src/main/java/pulse/search/statistics/RangePenalisedSSR.java deleted file mode 100644 index 2c62048b..00000000 --- a/src/main/java/pulse/search/statistics/RangePenalisedSSR.java +++ /dev/null @@ -1,27 +0,0 @@ -package pulse.search.statistics; - -import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; - -import pulse.tasks.SearchTask; - -public class RangePenalisedSSR extends SumOfSquares { - - private final static double PENALISATION_FACTOR = 0.5; - - @Override - public void evaluate(SearchTask t) { - super.evaluate(t); - - final double n = getResiduals().size(); - final double n0 = t.getExperimentalCurve().actualNumPoints(); - - incrementStatistic( - (n0 - n) / n0 * (new StandardDeviation().evaluate(transformResiduals(t))) * PENALISATION_FACTOR); - } - - @Override - public String getDescriptor() { - return "Range Penalised SSR"; - } - -} diff --git a/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java new file mode 100644 index 00000000..06bf1ff4 --- /dev/null +++ b/src/main/java/pulse/search/statistics/RegularisedLeastSquares.java @@ -0,0 +1,65 @@ +package pulse.search.statistics; + +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; +import pulse.search.GeneralTask; + +/** + * This is an experimental feature. The objective function here is equal to the + * ordinary least-square (OLS) plus a penalising term proportional to the + * squared length of a search vector. This way, search vectors of lower + * dimensionality are favoured. + * + */ +public class RegularisedLeastSquares extends SumOfSquares { + + private static final long serialVersionUID = -7398979361944447180L; + private double lambda = 1e-4; + + public RegularisedLeastSquares() { + super(); + } + + public RegularisedLeastSquares(RegularisedLeastSquares 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; + } + + /* + * OLS with L2 regularisation. The penalisation term is equal to {@code lambda} times the + * L2 norm of the search vector. + * @see pulse.search.statistics.SumOfSquares + */ + @Override + 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)); + } + + @Override + public String getDescriptor() { + return "L2 Regularised Least Squares"; + } + + @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 ebb49011..7185d5ae 100644 --- a/src/main/java/pulse/search/statistics/ResidualStatistic.java +++ b/src/main/java/pulse/search/statistics/ResidualStatistic.java @@ -1,104 +1,177 @@ 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 java.util.Objects; +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 + * procedure) that operates with model residuals. The list of residuals is + * stored in a field value for objects of this class. Each {@code SearchTask} + * will have at least two {@code ResidualStatistic}s associated with its + * {@code Calculation}s. + * + * @see pulse.tasks.SearchTask + * @see pulse.tasks.Calculation + * + */ public abstract class ResidualStatistic extends Statistic { - private double statistic; - private List residuals; - private static String selectedOptimiserDescriptor; - - public ResidualStatistic() { - super(); - residuals = new ArrayList<>(); - setPrefix("Residuals"); - } - - public double[] transformResiduals(SearchTask task) { - return task.getResidualStatistic().getResiduals().stream().map(doubleArray -> doubleArray[1]) - .mapToDouble(Double::doubleValue).toArray(); - } - - public void calculateResiduals(SearchTask task) { - var estimate = task.getProblem().getHeatingCurve(); - var reference = task.getExperimentalCurve(); - - residuals.clear(); - var indexRange = reference.getIndexRange(); - var time = reference.getTimeSequence(); - - var s = estimate.getSplineInterpolation(); - - int startIndex = max(closestLeft(estimate.timeAt(0), time), indexRange.getLowerBound()); - int endIndex = min(closestRight(estimate.timeLimit(), time), indexRange.getUpperBound()); - - double interpolated; - - 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 - */ - - interpolated = s.value(reference.timeAt(i)); - - residuals.add(new double[] { reference.timeAt(i), - reference.signalAt(i) - interpolated }); // y_exp - y* - - } - - } - - public List getResiduals() { - return residuals; - } - - public double residualUpperBound() { - return residuals.stream().map(array -> array[1]).reduce((a, b) -> b > a ? b : a).get(); - } - - public double residualLowerBound() { - return residuals.stream().map(array -> array[1]).reduce((a, b) -> a < b ? a : b).get(); - } - - public static void setSelectedOptimiserDescriptor(String selectedTestDescriptor) { - ResidualStatistic.selectedOptimiserDescriptor = selectedTestDescriptor; - } - - public static String getSelectedOptimiserDescriptor() { - return selectedOptimiserDescriptor; - } - - public NumericProperty getStatistic() { - return derive(OPTIMISER_STATISTIC, statistic); - } - - public void setStatistic(NumericProperty statistic) { - requireType(statistic, OPTIMISER_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) - statistic = (double) property.getValue(); - } - -} \ No newline at end of file + private double 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<>(); + rx = new ArrayList<>(); + setPrefix("Residuals"); + } + + public ResidualStatistic(ResidualStatistic another) { + this.statistic = another.statistic; + ry = new ArrayList<>(another.rx); + rx = new ArrayList<>(another.ry); + } + + /** + * 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 + * 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 + * solution is generated. Therefore, calling this method does not involve + * expensive calculation of the spline coefficents. The residuals are + * calculated only for the range that is specified by the + * {@code ExperimentalData} reference. The output of this method is stored + * in the field of the {@code residuals} object. + * + * @param reference + * @param estimate + * @see pulse.input.ExperimentalData + * @see pulse.HeatingCurve + */ + 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++) { + + ry.set(i - min, y.get(i) - estimate.evaluate(x.get(i))); + + } + + } //else create a new list + else { + + rx = new ArrayList<>(x.subList(min, max)); + ry.clear(); + + for (int i = min; i < max; i++) { + + ry.add(y.get(i) - estimate.evaluate(x.get(i))); + + } + + } + + } + + 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 final void calculateResiduals(GeneralTask task) { + calculateResiduals(task.getInput(), task.getResponse()); + } + + public List getResiduals() { + return ry; + } + + public List getTimeSequence() { + return rx; + } + + public NumericProperty getStatistic() { + return derive(OPTIMISER_STATISTIC, statistic); + } + + public void setStatistic(NumericProperty statistic) { + requireType(statistic, OPTIMISER_STATISTIC); + this.statistic = (double) statistic.getValue(); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == OPTIMISER_STATISTIC) { + statistic = (double) property.getValue(); + } + } + +} diff --git a/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java b/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java index f4b8adb4..c073c64a 100644 --- a/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java +++ b/src/main/java/pulse/search/statistics/SpearmansCorrelationTest.java @@ -3,20 +3,22 @@ import org.apache.commons.math3.stat.correlation.SpearmansCorrelation; /** - * Wrapper CorrelationTest class for ApacheCommonsMath Spearmans Correlation. + * Wrapper {@code CorrelationTest} class for ApacheCommonsMath Spearmans + * Correlation. * */ - public class SpearmansCorrelationTest extends CorrelationTest { - - @Override - public double evaluate(double[] x, double[] y) { - return (new SpearmansCorrelation()).correlation(x, y); - } - @Override - public String getDescriptor() { - return "Spearman's Rank Correlation"; - } + private static final long serialVersionUID = -8027167403407629716L; + + @Override + public double evaluate(double[] x, double[] y) { + return (new SpearmansCorrelation()).correlation(x, y); + } + + @Override + public String getDescriptor() { + return "Spearman's Rank Correlation"; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/search/statistics/Statistic.java b/src/main/java/pulse/search/statistics/Statistic.java index 0c170752..30d056ee 100644 --- a/src/main/java/pulse/search/statistics/Statistic.java +++ b/src/main/java/pulse/search/statistics/Statistic.java @@ -1,14 +1,17 @@ package pulse.search.statistics; -import pulse.properties.NumericProperty; +import pulse.search.GeneralTask; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; import pulse.util.Reflexive; +/** + * A statistic is an abstract class that hosts the {@code evaluate} method to + * validate the results of a {@code SearchTask}. + * + */ public abstract class Statistic extends PropertyHolder implements Reflexive { - public abstract void evaluate(SearchTask t); - public abstract NumericProperty getStatistic(); - public abstract void setStatistic(NumericProperty statistic); + 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 3c9bdd25..e72e41f1 100644 --- a/src/main/java/pulse/search/statistics/SumOfSquares.java +++ b/src/main/java/pulse/search/statistics/SumOfSquares.java @@ -2,46 +2,65 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; +import pulse.search.GeneralTask; -import pulse.tasks.SearchTask; - -public class SumOfSquares extends ResidualStatistic { - - /** - * Calculates the sum of squared deviations using {@code curve} as reference. - *

- * This calculates - * i=i1i2(T(ti)-T(ti)ref)2, - * where - * Tiref - * is the temperature value corresponding to the {@code time} at index {@code i} - * for the reference {@code curve}. Note that the time - * ti corresponds to the - * reference's time list, which generally does not match to that of this - * heating curve. The - * T(ti) - * is the interpolated value for this heating curve at the reference time. The - * temperature value is interpolated using two nearest elements of the - * baseline-subtracted temperature list. The value is interpolated using - * the experimental time ti and the nearest - * solution points to that time. The accuracy of this interpolation depends on - * the number of points. The boundaries of the summation are set by the - * {@code curve.getFittingStartIndex()} and {@code curve.getFittingEndIndex()} - * methods. - * - * @param t The task containing the reference and calculated curves - */ - - @Override - public void evaluate(SearchTask t) { - calculateResiduals(t); - final double statistic = getResiduals().stream().map(r -> r[1] * r[1]).reduce(Double::sum).get() / getResiduals().size(); - setStatistic(derive(OPTIMISER_STATISTIC, statistic)); - } - - @Override - public String getDescriptor() { - return "Ordinary least squares"; - } - -} \ No newline at end of file +/** + * The standard optimality criterion of the L2 norm condition, or simply + * ordinary least squares. + * + */ +public class SumOfSquares extends OptimiserStatistic { + + private static final long serialVersionUID = 3959714755977689591L; + + public SumOfSquares() { + super(); + } + + public SumOfSquares(SumOfSquares sos) { + super(sos); + } + + /** + * Calculates the sum of squared deviations using {@code curve} as + * reference. + *

+ * This calculates + * i=i1i2(T(ti)-T(ti)ref)2, + * where + * Tiref + * is the temperature value corresponding to the {@code time} at index + * {@code i} for the reference {@code curve}. Note that the time + * ti corresponds to the + * reference's time list, which generally does not match to that of + * this heating curve. The + * T(ti) + * is the interpolated value. + * + * @param t The task containing the reference and calculated curves + * @see calculateResiduals() + */ + @Override + public void evaluate(GeneralTask t) { + calculateResiduals(t); + final double statistic = getResiduals().stream().mapToDouble(r -> r * r) + .average().getAsDouble(); + setStatistic(derive(OPTIMISER_STATISTIC, statistic)); + } + + @Override + public String getDescriptor() { + return "Ordinary Least Squares"; + } + + @Override + public double variance() { + return (double) getStatistic().getValue(); + } + + @Override + public OptimiserStatistic copy() { + return new SumOfSquares(this); + } + +} diff --git a/src/main/java/pulse/search/statistics/package-info.java b/src/main/java/pulse/search/statistics/package-info.java index d0209dc0..4a9e7b35 100644 --- a/src/main/java/pulse/search/statistics/package-info.java +++ b/src/main/java/pulse/search/statistics/package-info.java @@ -1,5 +1,4 @@ /** * PULsE Statistical Kit. */ - -package pulse.search.statistics; \ No newline at end of file +package pulse.search.statistics; diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java new file mode 100644 index 00000000..db6b25df --- /dev/null +++ b/src/main/java/pulse/tasks/Calculation.java @@ -0,0 +1,420 @@ +package pulse.tasks; + +import static pulse.input.listeners.CurveEventType.TIME_ORIGIN_CHANGED; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.MODEL_WEIGHT; +import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; +import static pulse.tasks.logs.Status.INCOMPLETE; +import static pulse.util.Reflexive.instantiate; + +import java.util.List; +import java.util.Objects; +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; +import pulse.search.statistics.OptimiserStatistic; +import pulse.search.statistics.Statistic; +import pulse.tasks.logs.Status; +import pulse.tasks.processing.Result; +import pulse.ui.components.PropertyHolderTable; +import pulse.util.InstanceDescriptor; +import pulse.util.PropertyEvent; +import pulse.util.PropertyHolder; +import pulse.util.UpwardsNavigable; + +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; + + private Problem problem; + private DifferenceScheme scheme; + private ModelSelectionCriterion rs; + private OptimiserStatistic os; + private Result result; + + private static InstanceDescriptor instanceDescriptor = new InstanceDescriptor<>( + "Model Selection Criterion", ModelSelectionCriterion.class); + + //BIC as default + static { + instanceDescriptor.setSelectedDescriptor(BICStatistic.class.getSimpleName()); + } + + public Calculation(SearchTask t) { + status = INCOMPLETE; + this.initOptimiser(); + setParent(t); + instanceDescriptor.addListener(() -> initModelCriterion(rs)); + } + + /** + * Creates an orphan Calculation, retaining some properties of the argument + * + * @param c another calculation to be archived. + */ + public Calculation(Calculation c) { + problem = c.problem.copy(); + scheme = c.scheme.copy(); + rs = c.rs.copy(); + os = c.os.copy(); + status = c.status; + if (c.getResult() != null) { + result = new Result(c.getResult()); + } + instanceDescriptor.addListener(() -> initModelCriterion(rs)); + } + + 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; + this.scheme = null; + } + + /** + *

+ * After setting and adopting the {@code problem} by this + * {@code SearchTask}, this will attempt to change the parameters of that + * {@code problem} in accordance with the loaded {@code ExperimentalData} + * for this {@code SearchTask} (if not null).Later, if any changes to the + * properties of that {@code Problem} occur and if the source of that event + * is either the {@code Metadata} or the {@code PropertyHolderTable}, they + * will be accounted for by altering the parameters of the {@code problem} + * accordingly -- immediately after the former take place. + * + * @param problem a {@code Problem} + * @param curve + */ + public void setProblem(Problem problem, ExperimentalData curve) { + this.problem = problem; + problem.setParent(this); + problem.removeListeners(); + addProblemListeners(problem, curve); + } + + private void addProblemListeners(Problem problem, ExperimentalData curve) { + problem.getProperties().addListener((PropertyEvent event) -> { + var source = event.getSource(); + + if (source instanceof Metadata || source instanceof PropertyHolderTable) { + + var property = event.getProperty(); + if (property instanceof NumericProperty && ((NumericProperty) property).isOptimisable()) { + return; + } + + problem.estimateSignalRange(curve); + problem.getProperties().useTheoreticalEstimates(curve); + } + }); + + problem.getHeatingCurve().addHeatingCurveListener(dataEvent -> { + + var event = dataEvent.getType(); + + if (event == TIME_ORIGIN_CHANGED) { + var upperLimitUpdated = RELATIVE_TIME_MARGIN * curve.timeLimit() + - (double) problem.getHeatingCurve().getTimeShift().getValue(); + scheme.setTimeLimit(derive(TIME_LIMIT, upperLimitUpdated)); + } + + }); + } + + /** + * Adopts the {@code scheme} by this {@code SearchTask} and updates the time + * limit of {@code scheme} to match {@code ExperimentalData}. + * + * @param scheme the {@code DiffenceScheme}. + * @param curve + */ + public void setScheme(DifferenceScheme scheme, ExperimentalData curve) { + this.scheme = scheme; + + if (problem != null && scheme != null) { + scheme.setParent(this); + + var upperLimit = RELATIVE_TIME_MARGIN * curve.timeLimit() + - (double) problem.getHeatingCurve().getTimeShift().getValue(); + + scheme.setTimeLimit(derive(TIME_LIMIT, upperLimit)); + + } + + } + + /** + * This will use the current {@code DifferenceScheme} to solve the + * {@code Problem} for this {@code Calculation}. + * + * @throws SolverException + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public void process() throws SolverException { + var list = problem.getProperties().findMalformedProperties(); + if (!list.isEmpty()) { + StringBuilder sb = new StringBuilder("Illegal values:"); + list.forEach(np + -> sb.append(String.format("%n %-25s", np)) + ); + throw new SolverException(sb.toString(), ILLEGAL_PARAMETERS); + } + ((Solver) scheme).solve(problem); + } + + public Status getStatus() { + return status; + } + + /** + * Attempts to set the status of this calculation to {@code status}. + * + * @param status a status + * @return {@code true} if this attempt is successful, including the case + * when the status being set is equal to the current status. {@code false} + * if the current status is one of the following: {@code DONE}, + * {@code EXECUTION_ERROR}, {@code INCOMPLETE}, {@code IN_PROGRES}, AND the + * {@code status} being set is {@code QUEUED}. + */ + public boolean setStatus(Status status) { + + 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; + } + + } + + return changeStatus; + + } + + public NumericProperty weight(List all) { + var result = def(MODEL_WEIGHT); + + boolean condition = all.stream() + .allMatch(c -> c.getModelSelectionCriterion().getClass().equals(rs.getClass())); + + if (condition) { + var list = all.stream().map(a -> (ModelSelectionCriterion) a.getModelSelectionCriterion()) + .collect(Collectors.toList()); + result = rs.weight(list); + } + + return result; + } + + public void setModelSelectionCriterion(ModelSelectionCriterion rs) { + this.rs = rs; + rs.setParent(this); + firePropertyChanged(this, instanceDescriptor); + } + + public ModelSelectionCriterion getModelSelectionCriterion() { + return rs; + } + + public void setOptimiserStatistic(OptimiserStatistic os) { + this.os = os; + os.setParent(this); + initModelCriterion(os); + } + + @Override + public OptimiserStatistic getOptimiserStatistic() { + return os; + } + + public Problem getProblem() { + return problem; + } + + public void initOptimiser() { + this.setOptimiserStatistic( + instantiate(OptimiserStatistic.class, OptimiserStatistic.getSelectedOptimiserDescriptor())); + initModelCriterion(os); + } + + protected void initModelCriterion(Statistic res) { + setModelSelectionCriterion(instanceDescriptor.newInstance(ModelSelectionCriterion.class, res)); + } + + public DifferenceScheme getScheme() { + return scheme; + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + // intentionally left blank + } + + /** + * Checks if this {@code Calculation} is better than {@code a}. + * + * @param a another completed calculation + * @return {@code true} if another calculation hasn't been completed or if + * this calculation's statistic is lower than statistic of {@code a}. + */ + public boolean isBetterThan(Calculation a) { + boolean result = true; + + if (a.getStatus() == Status.DONE) { + result = compareTo(a) < 0; //compare statistic + + //do F-test + Calculation fBest = FTest.test(this, a); + //if the models are nested and calculations can be compared + if (fBest != null) { + //use the F-test result instead + result = fBest == this; + } + + } + + return result; + } + + /** + * Compares two calculations based on their model selection criteria. + * + * @param arg0 another calculation + * @return the result of comparing the model selection statistics of + * {@code this} and {@code arg0}. + */ + @Override + public int compareTo(Calculation arg0) { + var sAnother = arg0.getModelSelectionCriterion().getStatistic(); + var sThis = getModelSelectionCriterion().getStatistic();; + return sThis.compareTo(sAnother); + } + + public static InstanceDescriptor getModelSelectionDescriptor() { + return instanceDescriptor; + } + + public Result getResult() { + return result; + } + + public void setResult(Result result) { + this.result = result; + if (result != null) { + result.setParent(this); + } + } + + @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(); + } + + @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; + } + +} diff --git a/src/main/java/pulse/tasks/Identifier.java b/src/main/java/pulse/tasks/Identifier.java index 4b19c0f6..2a14adca 100644 --- a/src/main/java/pulse/tasks/Identifier.java +++ b/src/main/java/pulse/tasks/Identifier.java @@ -13,50 +13,50 @@ *

* */ - public class Identifier extends NumericProperty { - private static int lastId = -1; - - private Identifier(int value, boolean addToList) { - super(def(IDENTIFIER)); - setValue(value); - if (addToList) - setLastId(value); - } - - private static void setLastId(int value) { - Identifier.lastId = value; - } - - /** - * Creates an {@code Identifier} by incrementing the previously recorded ID. - */ - - public Identifier() { - this(Identifier.lastId + 1, true); - } - - /** - * Seeks an {@code Identifier} from the list of available tasks in - * {@code TaskManager} that matches this {@code string}. - * - * @param string the string describing the identifier. - * @return a matching {@code Identifier}. - */ - - public static Identifier parse(String string) { - var i = TaskManager.getManagerInstance().getTaskList().stream().map(t -> t.getIdentifier()) - .filter(id -> id.toString().equals(string)).findFirst(); - return i.isPresent() ? i.get() : null; - } - - public static Identifier externalIdentifier(int id) { - return id > -1 ? new Identifier(id, false) : null; - } - - @Override - public String toString() { - return Messages.getString("Identifier.Tag") + " " + getValue(); - } + private static final long serialVersionUID = 3751417739136256453L; + private static int lastId = -1; + + private Identifier(int value, boolean addToList) { + super(def(IDENTIFIER)); + setValue(value); + if (addToList) { + setLastId(value); + } + } + + private static void setLastId(int value) { + Identifier.lastId = value; + } + + /** + * Creates an {@code Identifier} by incrementing the previously recorded ID. + */ + public Identifier() { + this(Identifier.lastId + 1, true); + } + + /** + * Seeks an {@code Identifier} from the list of available tasks in + * {@code TaskManager} that matches this {@code string}. + * + * @param string the string describing the identifier. + * @return a matching {@code Identifier}. + */ + public static Identifier parse(String string) { + var i = TaskManager.getManagerInstance().getTaskList().stream().map(t -> t.getIdentifier()) + .filter(id -> id.toString().equals(string)).findFirst(); + return i.isPresent() ? i.get() : null; + } + + public static Identifier externalIdentifier(int id) { + return id > -1 ? new Identifier(id, false) : null; + } + + @Override + 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 a8afd60a..ed814aee 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -1,21 +1,15 @@ package pulse.tasks; -import static pulse.input.listeners.CurveEventType.TIME_ORIGIN_CHANGED; 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.ActiveFlags.getAllFlags; -import static pulse.search.direction.PathOptimiser.getErrorTolerance; import static pulse.search.direction.PathOptimiser.getInstance; -import static pulse.search.direction.PathOptimiser.getLinearSolver; -import static pulse.search.statistics.ResidualStatistic.getSelectedOptimiserDescriptor; 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.MISSING_BUFFER; import static pulse.tasks.logs.Details.MISSING_DIFFERENCE_SCHEME; import static pulse.tasks.logs.Details.MISSING_HEATING_CURVE; -import static pulse.tasks.logs.Details.MISSING_LINEAR_SOLVER; -import static pulse.tasks.logs.Details.MISSING_PATH_SOLVER; +import static pulse.tasks.logs.Details.MISSING_OPTIMISER; import static pulse.tasks.logs.Details.MISSING_PROBLEM_STATEMENT; import static pulse.tasks.logs.Details.PARAMETER_VALUES_NOT_SENSIBLE; import static pulse.tasks.logs.Details.SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS; @@ -25,46 +19,37 @@ 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.logs.Status.TERMINATED; -import static pulse.tasks.processing.Buffer.getSize; import static pulse.util.Reflexive.instantiate; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CompletableFuture; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; import java.util.stream.Collectors; import pulse.input.ExperimentalData; -import pulse.input.Metadata; -import pulse.math.IndexedVector; -import pulse.problem.schemes.DifferenceScheme; -import pulse.problem.schemes.solvers.Solver; +import pulse.input.InterpolationDataset; +import pulse.input.listeners.ExternalDatasetListener; +import pulse.math.ParameterIdentifier; +import pulse.math.ParameterVector; import pulse.problem.schemes.solvers.SolverException; -import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.search.direction.Path; +import pulse.search.GeneralTask; +import pulse.search.direction.ActiveFlags; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.NormalityTest; -import pulse.search.statistics.RSquaredTest; -import pulse.search.statistics.ResidualStatistic; -import pulse.search.statistics.SumOfSquares; import pulse.tasks.listeners.DataCollectionListener; import pulse.tasks.listeners.StatusChangeListener; +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.ui.components.PropertyHolderTable; -import pulse.util.Accessible; -import pulse.util.PropertyEvent; +import static pulse.tasks.logs.Status.AWAITING_TERMINATION; /** * A {@code SearchTask} is the most important class in {@code PULsE}. It @@ -73,582 +58,502 @@ * heat conduction, which is done using the {@code PathSolver}. A * {@code SearchTask} has an associated {@code ExperimentalData} object linked * to it. - * + * * @see pulse.tasks.TaskManager */ - -public class SearchTask extends Accessible implements Runnable { - - private Problem problem; - private DifferenceScheme scheme; - private ExperimentalData curve; - private ResidualStatistic rs; - - private Path path; - private Buffer buffer; - private CorrelationBuffer correlationBuffer; - private Log log; - private CorrelationTest correlationTest; - - private Identifier identifier; - private Status status = INCOMPLETE; - - private NormalityTest normalityTest; - - private final static double RELATIVE_TIME_MARGIN = 1.01; - - /** - * If {@code SearchTask} finishes, and its R2 value is lower - * than this constant, the result will be considered {@code AMBIGUOUS}. - */ - - private List listeners = new CopyOnWriteArrayList(); - private List statusChangeListeners = new CopyOnWriteArrayList(); - - /** - *

- * Creates a new {@code SearchTask} from {@code curve}. Generates a new - * {@code Identifier}, sets the parent of {@code curve} to {@code this}, and - * invokes clear(). If any changes to the {@code ExperimentalData} occur, a - * listener will ensure the {@code DifferenceScheme} is modified accordingly. - *

- * - * @param curve the {@code ExperimentalData} - */ - - public SearchTask(ExperimentalData curve) { - this.identifier = new Identifier(); - this.curve = curve; - curve.setParent(this); - correlationBuffer = new CorrelationBuffer(); - clear(); - } - - /** - *

- * Resets everything to default values (for a list of default values please see - * the {@code .xml} document. Sets the status of this task to - * {@code INCOMPLETE}. - *

- */ - - public void clear() { - curve.resetRanges(); - buffer = new Buffer(); - correlationBuffer.clear(); - buffer.setParent(this); - log = new Log(this); - - initOptimiser(); - initCorrelationTest(); - initNormalityTest(); - - this.path = null; - this.problem = null; - this.scheme = null; - - setStatus(INCOMPLETE); - - curve.addDataListener(dataEvent -> { - if (scheme != null) { - var startTime = (double) problem.getHeatingCurve().getTimeShift().getValue(); - scheme.setTimeLimit(derive(TIME_LIMIT, RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); - } - }); - - } - - 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 IndexedVector[] searchVector() { - var flags = getAllFlags(); - var keywords = activeParameters(this); - var optimisationVector = new IndexedVector(keywords); - var upperBound = new IndexedVector(optimisationVector.getIndices()); - - var array = new IndexedVector[] { optimisationVector, upperBound }; - - problem.optimisationVector(array, flags); - curve.getRange().optimisationVector(array, flags); - - return array; - - } - - /** - * Assigns the values of the parameters of this {@code SearchTask} to - * {@code searchParameters}. - * - * @param searchParameters an {@code IndexedVector} with relevant search - * parameters - * @see pulse.problem.statements.Problem.assign(IndexedVector) - */ - - public void assign(IndexedVector searchParameters) { - problem.assign(searchParameters); - curve.getRange().assign(searchParameters); - } - - /** - * 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 - */ - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public double solveProblemAndCalculateDeviation() { - try { - ((Solver) scheme).solve(problem); - } catch (SolverException e) { - status = FAILED; - System.err.println("Solver of " + this + " has encountered an error. Details: "); - e.printStackTrace(); - } - rs.evaluate(this); - return (double) rs.getStatistic().getValue(); - } - - /** - *

- * 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() { - - /* check of status */ - - switch (status) { - case READY: - case QUEUED: - setStatus(IN_PROGRESS); - break; - default: - return; - } - - /* preparatory steps */ - - getProblem().parameterListChanged(); // get updated list of parameters - solveProblemAndCalculateDeviation(); - - var pathSolver = getInstance(); - - path = pathSolver.createPath(this); - - var errorTolerance = (double) 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(); - - outer: do { - - bufferFutures.clear(); - - for (var i = 0; i < bufferSize; i++) { - - if (status != IN_PROGRESS) - break outer; - - try { - pathSolver.iteration(this); - } catch (SolverException e) { - status = FAILED; - System.err.println(this + " failed during execution. Details: "); - e.printStackTrace(); - } - - 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)); - - singleThreadExecutor.shutdown(); - - if (status == IN_PROGRESS) - runChecks(); - - } - - private void runChecks() { - - if (!normalityTest.test(this)) // first, check if the residuals are normally-distributed - setStatus(FAILED, ABNORMAL_DISTRIBUTION_OF_RESIDUALS); - - else { - - var test = correlationBuffer.test(correlationTest); // second, check there are no unexpected - // correlations - notifyDataListeners(new CorrelationLogEntry(this)); - - if (test) - setStatus(AMBIGUOUS, SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS); - else { - // lastly, check if the parameter values estimated in this procedure are - // reasonable - - var properties = alteredParameters(); - - if (properties.stream().anyMatch(np -> !np.validate())) - setStatus(FAILED, PARAMETER_VALUES_NOT_SENSIBLE); - else - setStatus(DONE); - - } - - } - - } - - public void addTaskListener(DataCollectionListener toAdd) { - listeners.add(toAdd); - } - - public void addStatusChangeListener(StatusChangeListener toAdd) { - statusChangeListeners.add(toAdd); - } - - public void removeTaskListeners() { - listeners.clear(); - } - - public void removeStatusChangeListeners() { - statusChangeListeners.clear(); - } - - @Override - public String toString() { - return getIdentifier().toString(); - } - - public Problem getProblem() { - return problem; - } - - public DifferenceScheme getScheme() { - return scheme; - } - - public ExperimentalData getExperimentalCurve() { - return curve; - } - - public Path getPath() { - return path; - } - - /** - *

- * After setting and adopting the {@code problem} by this {@code SearchTask}, - * this will attempt to change the parameters of that {@code problem} in - * accordance with the loaded {@code ExperimentalData} for this - * {@code SearchTask} (if not null). Later, if any changes to the properties of - * that {@code Problem} occur and if the source of that event is either the - * {@code Metadata} or the {@code PropertyHolderTable}, they will be accounted - * for by altering the parameters of the {@code problem} accordingly -- - * immediately after the former take place. - *

- * - * @param problem a {@code Problem} - */ - - public void setProblem(Problem problem) { - this.problem = problem; - problem.setParent(this); - problem.removeHeatingCurveListeners(); - problem.retrieveData(curve); - - problem.getProperties().addListener((PropertyEvent event) -> { - var source = event.getSource(); - - if (source instanceof Metadata || source instanceof PropertyHolderTable ) { - - var property = event.getProperty(); - if(property instanceof NumericProperty && ((NumericProperty)property).isAutoAdjustable() ) - return; - - problem.estimateSignalRange(curve); - problem.getProperties().useTheoreticalEstimates(curve); - } - }); - - problem.getHeatingCurve().addHeatingCurveListener(dataEvent -> { - - var event = dataEvent.getType(); - - if (event == TIME_ORIGIN_CHANGED) { - var upperLimitUpdated = RELATIVE_TIME_MARGIN * curve.timeLimit() - - (double) problem.getHeatingCurve().getTimeShift().getValue(); - scheme.setTimeLimit(derive(TIME_LIMIT, upperLimitUpdated)); - } - - }); - - } - - /** - * Adopts the {@code scheme} by this {@code SearchTask} and updates the time - * limit of {@scheme} to match {@code ExperimentalData}. - * - * @param scheme the {@code DiffenceScheme}. - */ - - public void setScheme(DifferenceScheme scheme) { - this.scheme = scheme; - - if (problem != null && scheme != null) { - scheme.setParent(this); - - var upperLimit = RELATIVE_TIME_MARGIN * curve.timeLimit() - - (double) problem.getHeatingCurve().getTimeShift().getValue(); - - scheme.setTimeLimit(derive(TIME_LIMIT, upperLimit)); - - } - - } - - /** - * Adopts the {@code curve} by this {@code SearchTask}. - * - * @param curve the {@code ExperimentalData}. - */ - - public void setExperimentalCurve(ExperimentalData curve) { - this.curve = curve; - - if (curve != null) - curve.setParent(this); - - } - - public Status getStatus() { - return status; - } - - public void setStatus(Status status, Details details) { - if (this.status != status) { - this.status = status; - status.setDetails(details); - notifyStatusListeners(new StateEntry(this, status)); - } - } - - /** - * Sets a new {@code status} to this {@code SearchTask} and informs the - * listeners. - * - * @param status the new status - */ - - public void setStatus(Status status) { - setStatus(status, Details.NONE); - } - - public Status checkProblems() { - return checkProblems(true); - } - - /** - *

- * Checks if this {@code SearchTask} is ready to be run. Performs basic check to - * see whether the user has uploaded all necessary data. If not, will create a - * status update with information about the missing data. - *

- * - * @return {@code READY} if the task is ready to be run, {@code DONE} if has - * already been done previously, {@code INCOMPLETE} if some problems - * exist. For the latter, additional details will be available using the - * {@code status.getDetails()} method. - *

- * @return the current status - */ - - public Status checkProblems(boolean updateStatus) { - if (status == DONE) - return status; - - var pathSolver = getInstance(); - var s = INCOMPLETE; - - if (problem == null) - s.setDetails(MISSING_PROBLEM_STATEMENT); - else if (!problem.isReady()) - s.setDetails(INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT); - else if (scheme == null) - s.setDetails(MISSING_DIFFERENCE_SCHEME); - else if (curve == null) - s.setDetails(MISSING_HEATING_CURVE); - else if (pathSolver == null) - s.setDetails(MISSING_PATH_SOLVER); - else if (getLinearSolver() == null) - s.setDetails(MISSING_LINEAR_SOLVER); - else if (buffer == null) - s.setDetails(MISSING_BUFFER); - else - s = READY; - - if (updateStatus) - setStatus(s); - - return status; - } - - public Identifier getIdentifier() { - return identifier; - } - - public Log getLog() { - return log; - } - - private void notifyDataListeners(LogEntry e) { - for (var l : listeners) { - l.onDataCollected(e); - } - } - - private void notifyStatusListeners(StateEntry e) { - for (var l : statusChangeListeners) { - 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_" + identifier.getValue()); - else - sb.append("ExtID_" + extId); - - return sb.toString(); - - } - - /** - * If the current task is either {@code IN_PROGRESS}, {@code QUEUED}, or - * {@code READY}, terminates it by setting its status to {@code TERMINATED}. - * This change of status will then force the {@code run()} loop to stop (if - * running). - */ - - public void terminate() { - switch (status) { - case IN_PROGRESS: - case QUEUED: - case READY: - setStatus(TERMINATED); - break; - default: - return; - } - } - - @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; - } - - public ResidualStatistic getResidualStatistic() { - return rs; - } - - public void setResidualStatistic(ResidualStatistic rs) { - this.rs = rs; - rs.setParent(this); - } - - public void initNormalityTest() { - normalityTest = instantiate(NormalityTest.class, NormalityTest.getSelectedTestDescriptor()); - - if (normalityTest instanceof RSquaredTest && rs instanceof SumOfSquares) - ((RSquaredTest) normalityTest).setSumOfSquares((SumOfSquares) rs); - - normalityTest.setParent(this); - } - - public void initOptimiser() { - rs = instantiate(ResidualStatistic.class, getSelectedOptimiserDescriptor()); - rs.setParent(this); - } - - public void initCorrelationTest() { - correlationTest = instantiate(CorrelationTest.class, CorrelationTest.getSelectedTestDescriptor()); - correlationTest.setParent(this); - } - - public CorrelationBuffer getCorrelationBuffer() { - return correlationBuffer; - } - - public CorrelationTest getCorrelationTest() { - return correlationTest; - } - -} \ No newline at end of file +public class SearchTask extends GeneralTask { + + /** + * + */ + private static final long serialVersionUID = -6763815749875446528L; + private Calculation current; + private List stored; + private ExperimentalData curve; + private Log log; + + private CorrelationBuffer correlationBuffer; + private CorrelationTest correlationTest; + private NormalityTest normalityTest; + + private Identifier identifier; + /** + * If {@code SearchTask} finishes, and its R2 value is + * lower than this constant, the result will be considered + * {@code AMBIGUOUS}. + */ + private transient List listeners; + private transient List statusChangeListeners; + + /** + *

+ * Creates a new {@code SearchTask} from {@code curve}. Generates a new + * {@code Identifier}, sets the parent of {@code curve} to {@code this}, and + * invokes clear(). If any changes to the {@code ExperimentalData} occur, a + * listener will ensure the {@code DifferenceScheme} is modified + * accordingly. + *

+ * + * @param curve the {@code ExperimentalData} + */ + public SearchTask(ExperimentalData curve) { + current = new Calculation(this); + this.identifier = new Identifier(); + this.curve = curve; + curve.setParent(this); + correlationBuffer = new CorrelationBuffer(); + initListeners(); + clear(); + } + + 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 + * range of {@code ExperimentalData} multiplied by a safety margin + * {@value Calculation.RELATIVE_TIME_MARGIN}. + */ + curve.addDataListener(dataEvent -> { + var scheme = current.getScheme(); + if (scheme != null) { + var hcurve = current.getProblem().getHeatingCurve(); + var startTime = (double) hcurve.getTimeShift().getValue(); + scheme.setTimeLimit( + derive(TIME_LIMIT, Calculation.RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); + } + }); + } + + /** + *

+ * Resets everything to default values (for a list of default values please + * see the {@code .xml} document. Sets the status of this task to + * {@code INCOMPLETE}. curve.addDataListener(dataEvent -> { var scheme = + * current.getScheme(); if (scheme != null) { var curve = + * current.getProblem().getHeatingCurve(); var startTime = (double) + * curve.getTimeShift().getValue(); scheme.setTimeLimit(derive(TIME_LIMIT, + * RELATIVE_TIME_MARGIN * curve.timeLimit() - startTime)); } }); + *

+ */ + public void clear() { + stored = new ArrayList<>(); + curve.resetRanges(); + correlationBuffer.clear(); + log = new Log(this); + + initCorrelationTest(); + initNormalityTest(); + + //this.path = null; + current.clear(); + + this.checkProblems(); + } + + public List alteredParameters() { + return activeParameters().stream().map(key + -> this.numericProperty(key)).collect(Collectors.toList()); + } + + public void addTaskListener(DataCollectionListener toAdd) { + listeners.add(toAdd); + } + + public void addStatusChangeListener(StatusChangeListener toAdd) { + statusChangeListeners.add(toAdd); + } + + public void removeTaskListeners() { + listeners.clear(); + } + + public void removeStatusChangeListeners() { + statusChangeListeners.clear(); + } + + @Override + public String toString() { + return getIdentifier().toString(); + } + + /** + * Adopts the {@code curve} by this {@code SearchTask}. + * + * @param curve the {@code ExperimentalData}. + */ + public void setExperimentalCurve(ExperimentalData curve) { + this.curve = curve; + + if (curve != null) { + curve.setParent(this); + } + + } + + /** + *

+ * Checks if this {@code SearchTask} is ready to be run.Performs basic check + * to see whether the user has uploaded all necessary data. If not, will + * create a status update with information about the missing data. + *

+ * + * Status will be set to {@code READY} if the task is ready to be run, + * {@code DONE} if has already been done previously, {@code INCOMPLETE} if + * some problems exist. For the latter, additional details will be available + * using the {@code status.getDetails()} method. + *

+ * + */ + public void checkProblems() { + var status = getStatus(); + + 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; + } + + setStatus(s); + + } + + } + + public Identifier getIdentifier() { + return identifier; + } + + public Log getLog() { + return log; + } + + private void notifyDataListeners(LogEntry e) { + for (var l : listeners) { + l.onDataCollected(e); + } + } + + private void notifyStatusListeners(StateEntry e) { + for (var l : statusChangeListeners) { + l.onStatusChange(e); + } + } + + @Override + public void run() { + correlationBuffer.clear(); + current.setResult(null); + + /* check of status */ + switch (getStatus()) { + case READY: + case QUEUED: + setStatus(IN_PROGRESS); + break; + 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}. + * This change of status will then force the {@code run()} loop to stop (if + * running). + */ + public void terminate() { + switch (getStatus()) { + case IN_PROGRESS: + case QUEUED: + setStatus(AWAITING_TERMINATION); + break; + default: + } + } + + public NormalityTest getNormalityTest() { + return normalityTest; + } + + public void initNormalityTest() { + normalityTest = instantiate(NormalityTest.class, NormalityTest.getSelectedTestDescriptor()); + normalityTest.setParent(this); + } + + public void initCorrelationTest() { + correlationTest = CorrelationTest.init(); + correlationTest.setParent(this); + } + + public CorrelationBuffer getCorrelationBuffer() { + return correlationBuffer; + } + + 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); + current.conformTo(null); + current = new Calculation(calc); + current.setParent(this); + calc.conformTo(calc); + current.setStatus(Status.READY); + var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); + fireRepositoryEvent(e); + } + + /** + * Finds the best calculation by comparing those already stored by their + * model selection statistics. + * + * @return the calculation showing the optimal value of the model selection + * statistic. + */ + public Calculation findBestCalculation() { + var c = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); + return c.isPresent() ? c.get() : null; + } + + public void switchToBestModel() { + 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) { + var instance = TaskManager.getManagerInstance(); + for (var l : instance.getTaskRepositoryListeners()) { + 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 != status; + 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; + } + +} diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index fe431cc0..f3394fb9 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -1,10 +1,8 @@ package pulse.tasks; -import static java.lang.System.gc; 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; @@ -16,30 +14,40 @@ import static pulse.tasks.logs.Status.DONE; import static pulse.tasks.logs.Status.IN_PROGRESS; import static pulse.tasks.logs.Status.QUEUED; -import static pulse.tasks.logs.Status.READY; -import static pulse.ui.Launcher.threadsAvailable; 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.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutionException; 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; +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; import pulse.tasks.listeners.TaskSelectionListener; +import pulse.tasks.logs.Status; +import static pulse.tasks.logs.Status.AWAITING_TERMINATION; import pulse.tasks.processing.Result; import pulse.tasks.processing.ResultFormat; import pulse.util.Group; @@ -56,527 +64,582 @@ *

* */ - -public class TaskManager extends UpwardsNavigable { - - private static TaskManager instance = new TaskManager(); - - private List tasks; - private SearchTask selectedTask; - private Map results; - - private boolean singleStatement = true; - - private final int THREADS_AVAILABLE = threadsAvailable(); - private ForkJoinPool taskPool; - - private List selectionListeners; - private List taskRepositoryListeners; - - private final static String DEFAULT_NAME = "Project 1 - " + now().format(ISO_WEEK_DATE); - - private HierarchyListener statementListener = e -> { - - if (!(e.getSource() instanceof PropertyHolder)) { - - var task = (SearchTask) e.getPropertyHolder().specificAncestor(SearchTask.class); - for (SearchTask t : tasks) { - if (t == task) - continue; - t.update(e.getProperty()); - } - - } - - }; - - private TaskManager() { - tasks = new ArrayList(); - results = new HashMap(); - taskPool = new ForkJoinPool(THREADS_AVAILABLE - 1); - selectionListeners = new CopyOnWriteArrayList(); - taskRepositoryListeners = new CopyOnWriteArrayList(); - this.addHierarchyListener(statementListener); - } - - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ - - public static TaskManager getManagerInstance() { - return instance; - } - - /** - * Executes {@code t} asynchronously using a {@code CompletableFuture}. When - * done, creates a {@code Result} and puts it into the - * {@code Map(SearchTask,Result)} in this {@code TaskManager}. - * - * @param t a {@code SearchTask} that will be executed - */ - - public void execute(SearchTask t) { - removeResult(t); // remove old result - t.setStatus(QUEUED); // notify listeners computation is about to start - - // notify listeners - notifyListeners(new TaskRepositoryEvent(TASK_SUBMITTED, t.getIdentifier())); - - // run task t -- after task completed, write result and trigger listeners - - CompletableFuture.runAsync(t).thenRun(() -> { - if (t.getStatus() == DONE) { - results.put(t, new Result(t, ResultFormat.getInstance())); - } - var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); - notifyListeners(e); - }); - } - - /** - * Notifies the {@code TaskRepositoryListener}s of the {@code e} - * - * @param e an event - */ - - public void notifyListeners(TaskRepositoryEvent e) { - taskRepositoryListeners.stream().forEach(l -> l.onTaskListChanged(e)); - } - - /** - *

- * Creates a queue of {@code SearchTask}s based on their readiness and feeds - * that queue to a {@code ForkJoinPool} using a parallel stream. The size of the - * pool is usually limited by hardware, e.g. for a 4 core system with 2 - * independent threads on each core, the limitation will be 4*2 - 1 = - * 7, etc. - */ - - public void executeAll() { - - var queue = tasks.stream().filter(t -> { - switch (t.getStatus()) { - case DONE: - case IN_PROGRESS: - case EXECUTION_ERROR: - return false; - default: - return true; - } - }).collect(toList()); - - try { - taskPool.submit(() -> queue.parallelStream().forEach(t -> execute(t))).get(); - } catch (InterruptedException | ExecutionException e) { - System.err.println("Execution exception while running multiple tasks"); - e.printStackTrace(); - } - - gc(); - - } - - /** - * Checks if any of the tasks that this {@code TaskManager} manages is either - * {@code QUEUED} or {@code IN_PROGRESS}. - * - * @return {@code false} if the status of the {@code SearchTask} is any of the - * above; {@code false} otherwise. - */ - - public boolean isTaskQueueEmpty() { - return !tasks.stream().anyMatch(t -> t.getStatus() == QUEUED || t.getStatus() == IN_PROGRESS); - } - - /** - * This will terminate all tasks in this {@code TaskManager} and trigger a - * {@code SHUTDOWN} {@code TaskRepositoryEvent}. - * - * @see pulse.tasks.Task.terminate() - */ - - public void cancelAllTasks() { - - tasks.stream().forEach(t -> t.terminate()); - - var e = new TaskRepositoryEvent(SHUTDOWN, null); - - notifyListeners(e); - - } - - /** - * Checks whether the acquisition time recorded by the experimental setup has - * been chosen appropriately. - * - * @return {@code false} if the acquisition time seems sensible for the - * {@code ExperimentalData} in each of the tasks; {@code true} - * otherwise. - * @see pulse.input.ExperimentalData.isAcquisitionTimeSensible() - */ - - public boolean dataNeedsTruncation() { - - return tasks.stream().anyMatch(t -> - - !t.getExperimentalCurve().isAcquisitionTimeSensible() - - ); - - } - - /** - * Calls {@code truncate()} on {@code ExperimentalData} for each - * {@code SearchTask}. - * - * @see pulse.input.ExperimentalData.truncate() - */ - - public void truncateData() { - tasks.stream().forEach(t -> t.getExperimentalCurve().truncate()); - } - - private void fireTaskSelected(Object source) { - var e = new TaskSelectionEvent(source); - for (var l : selectionListeners) { - l.onSelectionChanged(e); - } - } - - /** - *

- * Purges all tasks from this {@code TaskManager}. Generates a - * {@code TASK_REMOVED} {@code TaskRepositoryEvent} for each of the removed - * tasks. Clears task selection. - *

- */ - - public void clear() { - tasks.stream().sorted((t1, t2) -> -t1.getIdentifier().compareTo(t2.getIdentifier())).forEach(task -> { - var e = new TaskRepositoryEvent(TASK_REMOVED, task.getIdentifier()); - notifyListeners(e); - }); - - tasks.clear(); - selectTask(null, null); - } - - /** - * Uses the first non-{@code null} {@code SearchTask} to retrieve the sample - * name from the {@code Metadata} associated with its {@code ExperimentalData}. - * - * @return a {@code String} with the sample name, or {@code null} if no suitable - * task can be found. - */ - - public SampleName getSampleName() { - if (tasks.size() < 1) - return null; - - var optional = tasks.stream().filter(t -> t != null).findFirst(); - - if (!optional.isPresent()) - return null; - - return optional.get().getExperimentalCurve().getMetadata().getSampleName(); - } - - /** - *

- * Clears any progress for all the tasks and resets everything. Triggers a - * {@code TASK_RESET} event. - *

- */ - - public void reset() { - if (tasks.isEmpty()) - return; - - for (var task : tasks) { - var e = new TaskRepositoryEvent(TASK_RESET, task.getIdentifier()); - - task.clear(); - - notifyListeners(e); - } - - PathOptimiser.reset(); - - } - - /** - * Finds a {@code SearchTask} whose {@code Identifier} matches {@code id}. - * - * @param id the {@code Identifier} of the task. - * @return the {@code SearchTask} associated with this {@code Identifier}. - */ - - public SearchTask getTask(Identifier id) { - return tasks.stream().filter(t -> t.getIdentifier().equals(id)).findFirst().get(); - } - - /** - *

- * Generates a {@code SearchTask} assuming that the {@code ExperimentalData} is - * stored in the {@code file}. This will make the {@code ReaderManager} attempt - * to read that {@code file}. If successful, invokes {@code addTask(...)} on the - * created {@code SearchTask}. - *

- * - * @param file - * @see addTask(SearchTask) - * @see pulse.io.readers.ReaderManager.extract(File) - */ - - public void generateTask(File file) { - read(curveReaders(), file).stream().forEach(curve -> addTask(new SearchTask(curve))); - } - - /** - * Generates multiple tasks from multiple {@code files}. - * - * @param files a list of {@code File}s that can be parsed down to - * {@code ExperimentalData}. - */ - - public void generateTasks(List files) { - requireNonNull(files, "Null list of files passed to generatesTasks(...)"); - - var pool = Executors.newSingleThreadExecutor(); - files.stream().forEach(f -> pool.submit(() -> generateTask(f))); - pool.shutdown(); - try { - pool.awaitTermination(2, TimeUnit.MINUTES); - } catch (InterruptedException e) { - System.err.println("Failed to load all tasks within 2 minutes. Details:"); - e.printStackTrace(); - } - - selectFirstTask(); - - } - - /** - *

- * If a task {@code equal} to {@code t} has already been previously loaded, does - * nothing. Otherwise, adds this {@code t} to the task repository and triggers a - * {@code TASK_ADDED} event. - *

- * - * @param t the {@code SearchTask} that needs to be added to the internal - * repository - * @return {@code null} if a task like {@code t} has already been added - * previously, {@code t} otherwise. - * @see pulse.tasks.SearchTask.equals(SearchTask) - */ - - public SearchTask addTask(SearchTask t) { - - if (tasks.stream().filter(task -> task.equals(t)).count() > 0) - return null; - - tasks.add(t); - - var e = new TaskRepositoryEvent(TASK_ADDED, t.getIdentifier()); - t.setParent(getManagerInstance()); - notifyListeners(e); - - return t; - } - - /** - * If {@code t} is found in the local repository, removes it and triggers a - * {@code TASK_REMOVED} event. - * - * @param t a {@code SearchTask} that has been previously loaded to this - * repository. - * @return {@code true} if the operation is successful, {@code false} otherwise. - */ - - public boolean removeTask(SearchTask t) { - if (tasks.stream().filter(task -> task.equals(t)).count() < 1) - return false; - - tasks.remove(t); - - var e = new TaskRepositoryEvent(TASK_REMOVED, t.getIdentifier()); - - notifyListeners(e); - selectedTask = null; - - return true; - } - - /** - * Gets the current number of tasks in the repository. - * - * @return the number of available tasks. - */ - - public int numberOfTasks() { - return tasks.size(); - } - - /** - *

- * Selects a {@code SearchTask} within this repository with the specified - * {@code id} (if present). Informs the listeners this selection has been - * triggered by {@code src}. - *

- * - * @param id the {@code Identifier} of a task within this repository. - * @param src the source of the selection. - */ - - public void selectTask(Identifier id, Object src) { - tasks.stream().filter(t -> t.getIdentifier().equals(id)).filter(t -> t != selectedTask).findAny() - .ifPresent(t -> { - selectedTask = t; - fireTaskSelected(src); - }); - } - - public void selectFirstTask() { - if (!tasks.isEmpty()) - selectTask(tasks.get(0).getIdentifier(), this); - } - - public void addSelectionListener(TaskSelectionListener listener) { - selectionListeners.add(listener); - } - - public void addTaskRepositoryListener(TaskRepositoryListener listener) { - taskRepositoryListeners.add(listener); - } - - public TaskSelectionListener[] getSelectionListeners() { - return (TaskSelectionListener[]) selectionListeners.toArray(); - } - - public void removeSelectionListeners() { - selectionListeners.clear(); - } - - public void removeTaskRepositoryListener(TaskRepositoryListener trl) { - taskRepositoryListeners.remove(trl); - } - - public int indexOfTask(SearchTask t) { - return tasks.indexOf(t); - } - - public List getTaskList() { - return tasks; - } - - public SearchTask getSelectedTask() { - return selectedTask; - } - - public List getTaskRepositoryListeners() { - return taskRepositoryListeners; - } - - /** - * This {@code TaskManager} will be described by the sample name for the - * experiment. - */ - - @Override - public String describe() { - return tasks.size() > 0 ? getSampleName().toString() : DEFAULT_NAME; - } - - public Result getResult(SearchTask t) { - return results.get(t); - } - - /** - * Assigns {@code r} as the {@code Result} for {@code t}. - * - * @param t the {@code Result} - * @param r the {@code SearchTask}. - */ - - public void useResult(SearchTask t, Result r) { - results.put(t, r); - } - - /** - * Searches for a {@code Result} for a {@code SearchTask} with a specific - * {@code id}. - * - * @param id the {@code Identifier} of a {@code SearchTask} - * @return {@code null} if such {@code Result} cannot be found. Otherwise, - * returns the found {@code Result}. - */ - - public Result getResult(Identifier id) { - var optional = tasks.stream().filter(t -> t.getIdentifier().equals(id)).findFirst(); - return optional.isPresent() ? results.get(optional.get()) : null; - } - - /** - * Removes the results of the task {@code t} and sets its status to - * {@code READY}. - * - * @param t a {@code SearchTask} contained in the repository - */ - - public void removeResult(SearchTask t) { - if (!results.containsKey(t)) - return; - results.remove(t); - t.setStatus(READY); - } - - public void evaluate() { - tasks.stream().forEach(t -> { - var properties = t.getProblem().getProperties(); - InterpolationDataset.fill(properties); - properties.useTheoreticalEstimates(t.getExperimentalCurve()); - }); - } - - public Set allGrouppedContents() { - - return getTaskList().stream().map(t -> contents(t)).reduce((a, b) -> { - a.addAll(b); - return a; - }).get(); - - } - - /** - * Checks whether changes in this {@code PropertyHolder} should automatically be - * accounted for by other instances of this class. - * - * @return {@code true} if the user has specified so (set by default), - * {@code false} otherwise - */ - - public boolean isSingleStatement() { - return singleStatement; - } - - /** - * Sets the flag to isolate or inter-connects changes in all instances of - * {@code PropertyHolder} - * - * @param singleStatement {@code false} if other {@code PropertyHoder}s should - * disregard changes, which happened to this instances. - * {@code true} otherwise. - */ - - public void setSingleStatement(boolean singleStatement) { - this.singleStatement = singleStatement; - if (!singleStatement) - this.removeHierarchyListener(statementListener); - else - this.addHierarchyListener(statementListener); - } +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 transient List selectionListeners; + private transient List taskRepositoryListeners; + private transient List externalListeners; + + private static TaskManager instance = new TaskManager(); + + private static List globalListeners = new ArrayList<>(); + + private InterpolationDataset cpDataset; + private InterpolationDataset rhoDataset; + + 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)) { + + var task = (SearchTask) e.getPropertyHolder().specificAncestor(SearchTask.class); + for (SearchTask t : tasks) { + if (t == task) { + continue; + } + t.update(e.getProperty()); + } + + } + + }; + addHierarchyListener(statementListener); + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static TaskManager getManagerInstance() { + return instance; + } + + /** + * Executes {@code t} asynchronously using a {@code CompletableFuture}. + * When done, creates a {@code Result} and puts it into the + * {@code Map(SearchTask,Result)} in this {@code TaskManager}. + * + * @param t a {@code SearchTask} that will be executed + */ + public void execute(SearchTask t) { + t.checkProblems(); + + // try to start computation + // notify listeners computation is about to start + 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(); + 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; + } + } + }); + + } + + /** + * Notifies the {@code TaskRepositoryListener}s of the {@code e} + * + * @param e an event + */ + public void notifyListeners(TaskRepositoryEvent e) { + taskRepositoryListeners.stream().forEach(l -> l.onTaskListChanged(e)); + } + + /** + *

+ * Creates a queue of {@code SearchTask}s based on their readiness and feeds + * that queue to a {@code ForkJoinPool} using a parallel stream. The size of + * the pool is usually limited by hardware, e.g. for a 4 core system with 2 + * independent threads on each core, the limitation will be 4*2 - 1 = + * 7, etc. + */ + public void executeAll() { + + tasks.stream().filter(t -> { + switch (t.getStatus()) { + case IN_PROGRESS: + case EXECUTION_ERROR: + return false; + default: + return true; + } + }).forEach(t -> { + execute(t); + }); + + } + + /** + * Checks if any of the tasks that this {@code TaskManager} manages is + * either {@code QUEUED} or {@code IN_PROGRESS}. + * + * @return {@code false} if the status of the {@code SearchTask} is any of + * the above; {@code false} otherwise. + */ + public boolean isTaskQueueEmpty() { + return !tasks.stream().anyMatch(t -> { + var status = t.getStatus(); + return status == QUEUED || status == IN_PROGRESS; + }); + } + + /** + * This will terminate all tasks in this {@code TaskManager} and trigger a + * {@code SHUTDOWN} {@code TaskRepositoryEvent}. + * + * @see pulse.tasks.Task.terminate() + */ + public void cancelAllTasks() { + + tasks.stream().forEach(t -> t.terminate()); + + var e = new TaskRepositoryEvent(SHUTDOWN, null); + + notifyListeners(e); + + } + + public void fireTaskSelected(Object source) { + var e = new TaskSelectionEvent(source); + for (var l : selectionListeners) { + l.onSelectionChanged(e); + } + } + + /** + *

+ * Purges all tasks from this {@code TaskManager}. Generates a + * {@code TASK_REMOVED} {@code TaskRepositoryEvent} for each of the removed + * tasks. Clears task selection. + *

+ */ + public void clear() { + tasks.stream().sorted((t1, t2) -> -t1.getIdentifier().compareTo(t2.getIdentifier())).forEach(task -> { + var e = new TaskRepositoryEvent(TASK_REMOVED, task.getIdentifier()); + notifyListeners(e); + }); + + tasks.clear(); + selectTask(null, null); + } + + /** + * Uses the first non-{@code null} {@code SearchTask} to retrieve the sample + * name from the {@code Metadata} associated with its + * {@code ExperimentalData}. + * + * @return a {@code String} with the sample name, or {@code null} if no + * suitable task can be found. + */ + public SampleName getSampleName() { + if (tasks.size() < 1) { + return null; + } + + var optional = tasks.stream().filter(t -> t != null).findFirst(); + + if (!optional.isPresent()) { + return null; + } + + return ((ExperimentalData) optional.get().getInput()) + .getMetadata().getSampleName(); + } + + /** + *

+ * Clears any progress for all the tasks and resets everything. Triggers a + * {@code TASK_RESET} event. + *

+ */ + public void reset() { + if (tasks.isEmpty()) { + return; + } + + for (var task : tasks) { + var e = new TaskRepositoryEvent(TASK_RESET, task.getIdentifier()); + + task.clear(); + + notifyListeners(e); + } + + PathOptimiser.getInstance().reset(); + + } + + /** + * Finds a {@code SearchTask} whose {@code Identifier} matches {@code id}. + * + * @param id the {@code Identifier} of the task. + * @return the {@code SearchTask} associated with this {@code Identifier}. + */ + public SearchTask getTask(Identifier id) { + var o = tasks.stream().filter(t -> t.getIdentifier().equals(id)).findFirst(); + return o.isPresent() ? o.get() : null; + } + + /** + * Finds a {@code SearchTask} using the external identifier specified in its + * metadata. + * + * @param externalId the external ID of the data. + * @return the {@code SearchTask} associated with this {@code Identifier}. + */ + public SearchTask getTask(int externalId) { + var o = tasks.stream().filter(t + -> Integer.compare(((ExperimentalData) t.getInput()) + .getMetadata().getExternalID(), + externalId) == 0).findFirst(); + return o.isPresent() ? o.get() : null; + } + + /** + *

+ * Generates a {@code SearchTask} assuming that the {@code ExperimentalData} + * is stored in the {@code file}. This will make the {@code ReaderManager} + * attempt to read that {@code file}. If successful, invokes + * {@code addTask(...)} on the created {@code SearchTask}. After the task is + * generated, checks whether the acquisition time recorded by the + * experimental setup has been chosen appropriately. + * + * @see pulse.input.ExperimentalData.isAcquisitionTimeSensible() + * + *

+ * + * @param file the file to load the experimental data from + * @see addTask(SearchTask) + * @see pulse.io.readers.ReaderManager.extract(File) + */ + public void generateTask(File file) { + var curves = read(curveReaders(), file); + //notify curves have been loaded + curves.stream().forEach(c -> c.fireDataChanged(new DataEvent( + DataEventType.DATA_LOADED, c + ))); + //create tasks + curves.stream().forEach((ExperimentalData curve) -> { + var task = new SearchTask(curve); + addTask(task); + var data = (ExperimentalData) task.getInput(); + if (!data.isAcquisitionTimeSensible()) { + data.truncate(); + } + }); + } + + /** + * Generates multiple tasks from multiple {@code files}. + * + * @param files a list of {@code File}s that can be parsed down to + * {@code ExperimentalData}. + */ + 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(); + files.stream().forEach(f -> pool.submit(() -> generateTask(f))); + pool.shutdown(); + + try { + pool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } catch (InterruptedException ex) { + Logger.getLogger(TaskManager.class.getName()).log(Level.SEVERE, null, ex); + } + + //when pool has been shutdown + selectFirstTask(); + + }; + + Executors.newSingleThreadExecutor().submit(loader); + } + + /** + *

+ * If a task {@code equal} to {@code t} has already been previously loaded, + * does nothing. Otherwise, adds this {@code t} to the task repository and + * triggers a {@code TASK_ADDED} event. + *

+ * + * @param t the {@code SearchTask} that needs to be added to the internal + * repository + * @return {@code null} if a task like {@code t} has already been added + * previously, {@code t} otherwise. + * @see pulse.tasks.SearchTask.equals(SearchTask) + */ + public SearchTask addTask(SearchTask t) { + + if (tasks.stream().filter(task -> task.equals(t)).count() > 0) { + return null; + } + + tasks.add(t); + + var e = new TaskRepositoryEvent(TASK_ADDED, t.getIdentifier()); + t.setParent(getManagerInstance()); + notifyListeners(e); + + return t; + } + + /** + * If {@code t} is found in the local repository, removes it and triggers a + * {@code TASK_REMOVED} event. + * + * @param t a {@code SearchTask} that has been previously loaded to this + * repository. + * @return {@code true} if the operation is successful, {@code false} + * otherwise. + */ + public boolean removeTask(SearchTask t) { + if (tasks.stream().filter(task -> task.equals(t)).count() < 1) { + return false; + } + + tasks.remove(t); + + var e = new TaskRepositoryEvent(TASK_REMOVED, t.getIdentifier()); + + notifyListeners(e); + selectedTask = null; + + return true; + } + + /** + * Gets the current number of tasks in the repository. + * + * @return the number of available tasks. + */ + public int numberOfTasks() { + return tasks.size(); + } + + /** + *

+ * Selects a {@code SearchTask} within this repository with the specified + * {@code id} (if present). Informs the listeners this selection has been + * triggered by {@code src}. + *

+ * + * @param id the {@code Identifier} of a task within this repository. + * @param src the source of the selection. + */ + public void selectTask(Identifier id, Object src) { + tasks.stream().filter(t -> t.getIdentifier().equals(id)).filter(t -> t != selectedTask).findAny() + .ifPresent(t -> { + selectedTask = t; + fireTaskSelected(src); + }); + } + + public void selectFirstTask() { + if (!tasks.isEmpty() && selectedTask != tasks.get(0)) { + selectTask(tasks.get(0).getIdentifier(), this); + } + } + + public final void addSelectionListener(TaskSelectionListener listener) { + selectionListeners.add(listener); + } + + public final void addTaskRepositoryListener(TaskRepositoryListener listener) { + taskRepositoryListeners.add(listener); + } + + public List getSelectionListeners() { + return selectionListeners; + } + + public void removeSelectionListeners() { + selectionListeners.clear(); + } + + public void removeTaskRepositoryListener(TaskRepositoryListener trl) { + taskRepositoryListeners.remove(trl); + } + + public int indexOfTask(SearchTask t) { + return tasks.indexOf(t); + } + + public List getTaskList() { + return tasks; + } + + public SearchTask getSelectedTask() { + return selectedTask; + } + + public List getTaskRepositoryListeners() { + return taskRepositoryListeners; + } + + /** + * This {@code TaskManager} will be described by the sample name for the + * experiment. + * + * @return the string descriptor + */ + @Override + public String describe() { + var name = getSampleName(); + return name == null || name.getValue() == null + ? "Measurement_" + now().format(ISO_WEEK_DATE) + : name.toString(); + } + + public void evaluate() { + tasks.stream().forEach(t -> { + var properties = ((Calculation) t.getResponse()).getProblem().getProperties(); + var c = (ExperimentalData) t.getInput(); + properties.useTheoreticalEstimates(c); + }); + } + + public Set allGrouppedContents() { + + return getTaskList().stream().map(t -> contents(t)).reduce((a, b) -> { + a.addAll(b); + return a; + }).get(); + + } + + /** + * Checks whether changes in this {@code PropertyHolder} should + * automatically be accounted for by other instances of this class. + * + * @return {@code true} if the user has specified so (set by default), + * {@code false} otherwise + */ + 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 + * {@code PropertyHolder} + * + * @param singleStatement {@code false} if other {@code PropertyHoder}s + * should disregard changes, which happened to this instances. {@code true} + * otherwise. + */ + public void setSingleStatement(boolean singleStatement) { + this.singleStatement = singleStatement; + if (!singleStatement) { + this.removeHierarchyListener(statementListener); + } else { + 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 0f8cdc4c..b1f2e4a1 100644 --- a/src/main/java/pulse/tasks/listeners/DataCollectionListener.java +++ b/src/main/java/pulse/tasks/listeners/DataCollectionListener.java @@ -1,7 +1,10 @@ package pulse.tasks.listeners; +import java.io.Serializable; import pulse.tasks.logs.LogEntry; -public interface DataCollectionListener { - public void onDataCollected(LogEntry e); -} \ No newline at end of file +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 f528b4ee..e2d03925 100644 --- a/src/main/java/pulse/tasks/listeners/LogEntryListener.java +++ b/src/main/java/pulse/tasks/listeners/LogEntryListener.java @@ -1,12 +1,13 @@ 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); + public void onNewEntry(LogEntry e); - public void onLogFinished(Log log); + public void onLogFinished(Log log); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java b/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java index f71d255e..edd94e99 100644 --- a/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java +++ b/src/main/java/pulse/tasks/listeners/ResultFormatEvent.java @@ -1,17 +1,18 @@ package pulse.tasks.listeners; +import java.io.Serializable; import pulse.tasks.processing.ResultFormat; -public class ResultFormatEvent { +public class ResultFormatEvent implements Serializable { - private ResultFormat rf; + private ResultFormat rf; - public ResultFormatEvent(ResultFormat rf) { - this.rf = rf; - } + public ResultFormatEvent(ResultFormat rf) { + this.rf = rf; + } - public ResultFormat getResultFormat() { - return rf; - } + public ResultFormat getResultFormat() { + return rf; + } } diff --git a/src/main/java/pulse/tasks/listeners/ResultFormatListener.java b/src/main/java/pulse/tasks/listeners/ResultFormatListener.java index 9a8e1896..732563c2 100644 --- a/src/main/java/pulse/tasks/listeners/ResultFormatListener.java +++ b/src/main/java/pulse/tasks/listeners/ResultFormatListener.java @@ -1,7 +1,9 @@ package pulse.tasks.listeners; -public interface ResultFormatListener { +import java.io.Serializable; - public void resultFormatChanged(ResultFormatEvent rfe); +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 00000000..30d13009 --- /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 877eb73d..3d9ba946 100644 --- a/src/main/java/pulse/tasks/listeners/StatusChangeListener.java +++ b/src/main/java/pulse/tasks/listeners/StatusChangeListener.java @@ -1,7 +1,9 @@ package pulse.tasks.listeners; +import java.io.Serializable; import pulse.tasks.logs.StateEntry; -public interface StatusChangeListener { - public void onStatusChange(StateEntry e); +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 6af6b77d..f234b6a4 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryEvent.java @@ -4,60 +4,69 @@ public class TaskRepositoryEvent { - private State state; - private Identifier id; - - public TaskRepositoryEvent(State state, Identifier id) { - this.state = state; - this.id = id; - } - - public State getState() { - return state; - } - - public Identifier getId() { - return id; - } - - public enum State { - - /** - * Indicates a task has been added to the repository. - */ - - TASK_ADDED, - - /** - * A task has been removed from the repository. - */ - - TASK_REMOVED, - - /** - * A task has been submitted for execution. - */ - - TASK_SUBMITTED, - - /** - * A task has finished executing. - */ - - TASK_FINISHED, - - /** - * A task has been reset. - */ - - TASK_RESET, - - /** - * The repository has been shut down/ - */ - - SHUTDOWN; - - } + private State state; + private Identifier id; + + public TaskRepositoryEvent(State state, Identifier id) { + this.state = state; + this.id = id; + } + + public State getState() { + return state; + } + + public Identifier getId() { + return id; + } + + public enum State { + /** + * Indicates a task has been added to the repository. + */ + TASK_ADDED, + /** + * A task has been removed from the repository. + */ + TASK_REMOVED, + /** + * A task has been submitted for execution. + */ + TASK_SUBMITTED, + /** + * A task has finished executing. + */ + TASK_FINISHED, + /** + * A task has been reset. + */ + TASK_RESET, + /** + * An external request has been received to browse previous + * calculations. + */ + TASK_BROWSING_REQUEST, + /** + * The task has switched to a new model. + */ + TASK_MODEL_SWITCH, + /** + * The task changed its selection criterion. + */ + TASK_CRITERION_SWITCH, + /** + * Indicates the task has discarded superfluous calculations. + */ + BEST_MODEL_SELECTED, + /** + * A new state has been loaded. + */ + NEW_STATE, + /** + * The repository has been shut down/ + */ + SHUTDOWN; + + } } \ 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 cc3e173b..639d9892 100644 --- a/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java +++ b/src/main/java/pulse/tasks/listeners/TaskRepositoryListener.java @@ -1,5 +1,9 @@ package pulse.tasks.listeners; -public interface TaskRepositoryListener { - public void onTaskListChanged(TaskRepositoryEvent e); -} +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 3dd33ab7..c2ce45f9 100644 --- a/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java +++ b/src/main/java/pulse/tasks/listeners/TaskSelectionEvent.java @@ -4,18 +4,13 @@ public class TaskSelectionEvent extends EventObject { - /** - * - */ - private static final long serialVersionUID = 4278832926994139917L; + public TaskSelectionEvent(Object source) { + super(source); + // TODO Auto-generated constructor stub + } - public TaskSelectionEvent(Object source) { - super(source); - // TODO Auto-generated constructor stub - } - - public void setSource(Object source) { - this.source = source; - } + public void setSource(Object source) { + this.source = source; + } } diff --git a/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java b/src/main/java/pulse/tasks/listeners/TaskSelectionListener.java index fbcda125..a4c3f1a5 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 void onSelectionChanged(TaskSelectionEvent e); +public interface TaskSelectionListener extends Serializable { -} + public void onSelectionChanged(TaskSelectionEvent e); + +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/listeners/package-info.java b/src/main/java/pulse/tasks/listeners/package-info.java index 44d859eb..034b020d 100644 --- a/src/main/java/pulse/tasks/listeners/package-info.java +++ b/src/main/java/pulse/tasks/listeners/package-info.java @@ -4,5 +4,4 @@ * generation, as well as with the task repository events generated by a * {@code TaskManager}. */ - -package pulse.tasks.listeners; \ No newline at end of file +package pulse.tasks.listeners; diff --git a/src/main/java/pulse/tasks/logs/AbstractLogger.java b/src/main/java/pulse/tasks/logs/AbstractLogger.java new file mode 100644 index 00000000..966351a9 --- /dev/null +++ b/src/main/java/pulse/tasks/logs/AbstractLogger.java @@ -0,0 +1,83 @@ +package pulse.tasks.logs; + +import java.io.Serializable; +import java.util.concurrent.ExecutorService; +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, Serializable { + + private ExecutorService updateExecutor; + + public AbstractLogger() { + updateExecutor = newSingleThreadExecutor(); + } + + public synchronized void update() { + var task = TaskManager.getManagerInstance().getSelectedTask(); + + if (task == null) { + return; + } + + var log = task.getLog(); + + if (log.isStarted()) { + 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 clear(); + + public abstract boolean isEmpty(); + +} diff --git a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java index 0f804d8c..f87f078b 100644 --- a/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java +++ b/src/main/java/pulse/tasks/logs/CorrelationLogEntry.java @@ -1,53 +1,63 @@ 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; public class CorrelationLogEntry extends LogEntry { - public CorrelationLogEntry(SearchTask t) { - super(t); - } - - @Override - public String toString() { - var t = TaskManager.getManagerInstance().getTask(getIdentifier()); - var buffer = t.getCorrelationBuffer(); - var test = t.getCorrelationTest(); - var map = buffer.evaluate(test); - - if (map == null) - return ""; - - if (map.isEmpty()) - return ""; - - StringBuilder sb = new StringBuilder(); - sb.append("

"); - sb.append(""); - - for (ImmutablePair key : map.keySet()) { - sb.append(""); - } - - sb.append("
Correlation table
x y Correlation
"); - sb.append(def(key.getFirst()).getAbbreviation(false)); - sb.append(""); - sb.append(def(key.getSecond()).getAbbreviation(false)); - sb.append(""); - if (test.compareToThreshold(map.get(key))) - sb.append(""); - sb.append("" + String.format("%3.2f", map.get(key)) + ""); - if (test.compareToThreshold(map.get(key))) - sb.append(""); - sb.append("

"); - - return sb.toString(); - - } + public CorrelationLogEntry(SearchTask t) { + super(t); + } + + @Override + public String toString() { + var t = TaskManager.getManagerInstance().getTask(getIdentifier()); + var buffer = t.getCorrelationBuffer(); + var test = t.getCorrelationTest(); + var map = buffer.evaluate(test); + + if (map == null) { + return ""; + } + + if (map.isEmpty()) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + sb.append("

"); + sb.append(""); + + for (ImmutablePair key : map.keySet()) { + sb.append(""); + } + + sb.append("
Correlation table
x y Correlation
"); + 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().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("").append(String.format("%3.2f", map.get(key))).append(""); + if (test.compareToThreshold(map.get(key))) { + sb.append(""); + } + sb.append("

"); + + return sb.toString(); + + } } diff --git a/src/main/java/pulse/tasks/logs/DataLogEntry.java b/src/main/java/pulse/tasks/logs/DataLogEntry.java index 5b7d9ca3..45532729 100644 --- a/src/main/java/pulse/tasks/logs/DataLogEntry.java +++ b/src/main/java/pulse/tasks/logs/DataLogEntry.java @@ -1,10 +1,13 @@ 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 static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericPropertyKeyword.OBJECTIVE_FUNCTION; -import pulse.properties.NumericProperty; import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.ui.Messages; @@ -15,67 +18,75 @@ * from a {@code SearchTask}. The output is accessible via the * {@code toString()} method. *

- * + * */ - public class DataLogEntry extends LogEntry { - private List entry; - - /** - * Creates a new {@code DataLogEntry} based on the current values of the - * properties from {@code task} which match the currently selected - * {@code LogFormat}. - * - * @param task a task, which will be used to build the {@code DataLogEntry} - */ - - public DataLogEntry(SearchTask task) { - super(task); - try { - fill(); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - System.err.println("Failed to fill this log entry with data. Details below."); - e.printStackTrace(); - } - } - - /** - * Fills this {@code DataLogEtnry} with properties from the {@code SearchTask}, - * which have types matching to those listed in the {@code LogFormat}. - * - * @throws IllegalAccessException if the call to - * {@code task.numericProperties() fails} - * @throws IllegalArgumentException if the call to - * {@code task.numericProperties() fails} - * @throws InvocationTargetException if the call to - * {@code task.numericProperties() fails} - */ - - 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.getPath().getIteration()); - } - - public List getData() { - return entry; - } - - /** - * This {@code String} will be displayed by the {@code LogPane} if the verbose - * log option is enabled. - * - * @see pulse.ui.components.LogPane - */ - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - /* + private static final long serialVersionUID = -8995240410369870205L; + private List entry; + + /** + * Creates a new {@code DataLogEntry} based on the current values of the + * properties from {@code task} which match the currently selected + * {@code LogFormat}. + * + * @param task a task, which will be used to build the {@code DataLogEntry} + */ + public DataLogEntry(SearchTask task) { + super(task); + try { + fill(); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + System.err.println("Failed to fill this log entry with data. Details below."); + e.printStackTrace(); + } + } + + /** + * Fills this {@code DataLogEtnry} with properties from the + * {@code SearchTask}, which have types matching to those listed in the + * {@code LogFormat}. + * + * @throws IllegalAccessException if the call to + * {@code task.numericProperties() fails} + * @throws IllegalArgumentException if the call to + * {@code task.numericProperties() fails} + * @throws InvocationTargetException if the call to + * {@code task.numericProperties() fails} + */ + 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()); + //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() { + return entry; + } + + /** + * This {@code String} will be displayed by the {@code LogPane} if the + * verbose log option is enabled. + * + * @see pulse.ui.components.LogPane + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + /* * // UNCOMMENT THIS TO PRODUCE EASY-TO-READ DATA ENTRIES sb.append("\n"); * * for(NumericProperty p : entry) { sb.append((p.getValue() instanceof Double ? @@ -83,26 +94,38 @@ public String toString() { * sb.append("\n"); } * * return sb.toString(); - */ - - sb.append(""); - - for (NumericProperty p : entry) { - sb.append("<
"); - } - - sb.append("
"); - sb.append(p.getAbbreviation(false)); - sb.append(""); - sb.append(Messages.getString("DataLogEntry.FontTagNumber")); //$NON-NLS-1$ - sb.append(""); - sb.append(p.formattedOutput()); - sb.append(""); - sb.append(Messages.getString("DataLogEntry.FontTagClose")); //$NON-NLS-1$ - sb.append("


"); - - return sb.toString(); - - } - -} \ No newline at end of file + */ + sb.append(""); + + for (Parameter p : entry) { + sb.append("<
"); + } + + sb.append("
"); + 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(def.formattedOutput()); + sb.append(""); + sb.append(Messages.getString("DataLogEntry.FontTagClose")); //$NON-NLS-1$ + sb.append("


"); + + return sb.toString(); + + } + +} diff --git a/src/main/java/pulse/tasks/logs/Details.java b/src/main/java/pulse/tasks/logs/Details.java index 2c4f1434..af2854e1 100644 --- a/src/main/java/pulse/tasks/logs/Details.java +++ b/src/main/java/pulse/tasks/logs/Details.java @@ -4,65 +4,60 @@ * An enum which lists different possible problems wit the {@code SearchTask}. * */ - public enum Details { - NONE, - - /** - * The {@code Problem} has not been specified by the user. - */ - - MISSING_PROBLEM_STATEMENT, - - /** - * The {@code DifferenceScheme} for solving the {@code Problem} has not been - * specified by the user. - */ - - MISSING_DIFFERENCE_SCHEME, - - /** - * A heating curve has not been set up for the {@code DifferenceScheme}. - */ - - MISSING_HEATING_CURVE, - - /** - * No information can be found about the selected path solver. - */ - - MISSING_LINEAR_SOLVER, - - /** - * There is no information about the selected path solver. - */ - - MISSING_PATH_SOLVER, - - /** - * The buffer has not been created. - */ - - MISSING_BUFFER, - - /** - * Some data is missing in the problem statement. Probably, the interpolation - * datasets have been set up incorrectly or the specific heat and density data - * have not been loaded. - */ - - INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT, - - SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS, - - PARAMETER_VALUES_NOT_SENSIBLE, - - ABNORMAL_DISTRIBUTION_OF_RESIDUALS; - - @Override - public String toString() { - return Status.parse(super.toString()); - } - -} \ No newline at end of file + NONE, + /** + * The {@code Problem} has not been specified by the user. + */ + MISSING_PROBLEM_STATEMENT, + /** + * The {@code DifferenceScheme} for solving the {@code Problem} has not been + * specified by the user. + */ + MISSING_DIFFERENCE_SCHEME, + /** + * A heating curve has not been set up for the {@code DifferenceScheme}. + */ + MISSING_HEATING_CURVE, + /** + * There is no information about the selected optimiser. + */ + MISSING_OPTIMISER, + /** + * The buffer has not been created. + */ + MISSING_BUFFER, + /** + * The optimisation statistic is not suported by the selected optimiser. + */ + INCOMPATIBLE_OPTIMISER, + /** + * Some data is missing in the problem statement. Probably, the + * interpolation datasets have been set up incorrectly or the specific heat + * and density data have not been loaded. + */ + INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT, + SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS, + 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. + */ + 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. + */ + BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED, + SOLVER_ERROR; + + @Override + public String toString() { + return Status.parse(super.toString()); + } + +} diff --git a/src/main/java/pulse/tasks/logs/Log.java b/src/main/java/pulse/tasks/logs/Log.java index 09819e94..acdcde6c 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; @@ -9,7 +11,8 @@ import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.LogEntryListener; -import pulse.tasks.listeners.StatusChangeListener; +import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.tasks.listeners.TaskRepositoryEvent.State; import pulse.ui.Messages; import pulse.util.Group; @@ -18,190 +21,224 @@ * such as changes of status and/or data collection events. * */ - public class Log extends Group { - private List logEntries; - private LocalTime start; - private LocalTime end; - private Identifier id; - private List listeners; - private static boolean verbose = false; - - /** - * Creates a {@code Log} for this {@code task} that will automatically store - * {@code TaskStatEvent}s and a list of {@code DataLogEntr}ies in thread-safe - * collections. This is done by adding a {@code TaskListener} and a - * {@code StatusChangeListener} to the {@code task} object. - * - * @param task the task to be logged. - */ - - public Log(SearchTask task) { - Objects.requireNonNull(task, Messages.getString("Log.NullTaskError")); - - setParent(task); - id = task.getIdentifier(); - - this.logEntries = new CopyOnWriteArrayList<>(); - listeners = new CopyOnWriteArrayList<>(); - - task.addTaskListener(le -> { - - /** - * Do these actions each time data has been collected for this task. - */ - - 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(); - - } - - }); - - } - - private void logFinished() { - listeners.stream().forEach(l -> l.onLogFinished(this)); - } - - private void notifyListeners(LogEntry logEntry) { - listeners.stream().forEach(l -> l.onNewEntry(logEntry)); - } - - public List getListeners() { - return listeners; - } - - public void addListener(LogEntryListener l) { - listeners.add(l); - } - - public Identifier getIdentifier() { - return id; - } - - /** - * Checks whether this {@code Log} has observed a {@code TaskStateEvent} - * triggered by a change of status of its respective {@code SearchTask} to - * {@code IN_PROGRESS}. - * - * @return {@code true} if the start time is not {@code null} - */ - - public boolean isStarted() { - return start != null; - } - - /** - * Outputs all log entries consecutively. - */ - - @Override - public String toString() { - - StringBuilder sb = new StringBuilder(); - String newLine = System.lineSeparator(); - - sb.append(TaskManager.getManagerInstance().getTask(id)); - sb.append(newLine); - sb.append(newLine); - - for (LogEntry le : logEntries) { - sb.append(le); - sb.append(newLine); - } - - return sb.toString(); - - } - - public List getLogEntries() { - return logEntries; - } - - /** - * This is the time after the creation of the {@code Log} when a change of - * status to {@code IN_PROGRESS} happened. - * - * @return the start time - */ - - public LocalTime getStart() { - return start; - } - - /** - * This is the time after the creation of the {@code Log} when a change of - * status to {@code DONE} happened. - * - * @return the start time - */ - - public LocalTime getEnd() { - return end; - } - - /** - * Finds the last recorded entry in this {@code Log}. - * - * @return last recorded entry. - */ - - public LogEntry lastEntry() { - return logEntries.stream().reduce((first, second) -> second).get(); - } - - /** - * Checks whether this {@code Log} is verbose. Verbose logs stores all data - * entries and outputs them on request. This is useful to get an idea of how the - * search method works, how many iterations are taken to reach a converged - * value, etc. - * - * @return {@code true} if the verbose flag is on - */ - - public static boolean isVerbose() { - return verbose; - } - - /** - * Sets the verbose flag to {@code verbose} - * - * @param verbose the new value of the flag - * @see isVerbose() - */ - - public static void setVerbose(boolean verbose) { - Log.verbose = verbose; - } - -} \ No newline at end of file + private static final long serialVersionUID = 420096365502122145L; + private List logEntries; + private LocalTime start; + private LocalTime end; + 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 + * {@code TaskStatEvent}s and a list of {@code DataLogEntr}ies in + * thread-safe collections. This is done by adding a {@code TaskListener} + * and a {@code StatusChangeListener} to the {@code task} object. + * + * @param task the task to be logged. + */ + public Log(SearchTask task) { + Objects.requireNonNull(task, Messages.getString("Log.NullTaskError")); + + setParent(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 -> { + + /** + * Do these actions each time data has been collected for this task. + */ + if (task.getStatus() != Status.INCOMPLETE) { + logEntries.add(le); + notifyListeners(le); + } + + }); + + 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() { + finished = true; + listeners.stream().forEach(l -> l.onLogFinished(this)); + } + + private void notifyListeners(LogEntry logEntry) { + finished = false; + listeners.stream().forEach(l -> l.onNewEntry(logEntry)); + } + + public final List getListeners() { + return listeners; + } + + public final void addListener(LogEntryListener l) { + listeners.add(l); + } + + public final Identifier getIdentifier() { + return id; + } + + /** + * Checks whether this {@code Log} has observed a {@code TaskStateEvent} + * triggered by a change of status of its respective {@code SearchTask} to + * {@code IN_PROGRESS}. + * + * @return {@code true} if the start time is not {@code null} + */ + public boolean isStarted() { + return logEntries.size() > 0; + } + + public boolean isFinished() { + return finished; + } + + /** + * Outputs all log entries consecutively. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(); + String newLine = System.lineSeparator(); + + sb.append(TaskManager.getManagerInstance().getTask(id)); + sb.append(newLine); + sb.append(newLine); + + logEntries.stream().map(le -> { + sb.append(le); + return le; + }).forEachOrdered(_item -> { + sb.append(newLine); + }); + + return sb.toString(); + + } + + public List getLogEntries() { + return logEntries; + } + + /** + * This is the time after the creation of the {@code Log} when a change of + * status to {@code IN_PROGRESS} happened. + * + * @return the start time + */ + public LocalTime getStart() { + return start; + } + + /** + * This is the time after the creation of the {@code Log} when a change of + * status to {@code DONE} happened. + * + * @return the start time + */ + public LocalTime getEnd() { + return end; + } + + /** + * Finds the last recorded entry in this {@code Log}. + * + * @return last recorded entry. + */ + public LogEntry lastEntry() { + return logEntries.stream().reduce((first, second) -> second).get(); + } + + /** + * Checks whether this {@code Log} is verbose. Verbose logs stores all data + * entries and outputs them on request. This is useful to get an idea of how + * the search method works, how many iterations are taken to reach a + * converged value, etc. + * + * @return {@code true} if the verbose flag is on + */ + public static boolean isGraphicalLog() { + return graphical; + } + + /** + * Sets the verbose flag to {@code verbose} + * + * @param verbose the new value of the flag + * @see #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 + */ + 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 d0d93e48..6a6d6442 100644 --- a/src/main/java/pulse/tasks/logs/LogEntry.java +++ b/src/main/java/pulse/tasks/logs/LogEntry.java @@ -1,5 +1,6 @@ package pulse.tasks.logs; +import java.io.Serializable; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Objects; @@ -17,33 +18,43 @@ *

* */ - -public class LogEntry { - - private Identifier identifier; - private LocalTime time; - - /** - *

- * Creates a {@code LogEntry} from this {@code SearchTask}. The data of the - * creation of this {@code LogEntry} will be stored. - *

- * - * @param t a {@code SearchTask} - */ - - public LogEntry(SearchTask t) { - Objects.requireNonNull(t, Messages.getString("LogEntry.NullTaskError")); - time = LocalDateTime.now().toLocalTime(); - identifier = t.getIdentifier(); - } - - public Identifier getIdentifier() { - return identifier; - } - - public LocalTime getTime() { - return time; - } - -} \ No newline at end of file +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 + * creation of this {@code LogEntry} will be stored. + *

+ * + * @param t a {@code SearchTask} + */ + public LogEntry(SearchTask t) { + Objects.requireNonNull(t, Messages.getString("LogEntry.NullTaskError")); + time = LocalDateTime.now().toLocalTime(); + identifier = t.getIdentifier(); + var list = t.getLog().getLogEntries(); + if (list != null && !list.isEmpty()) { + previous = list.get(list.size() - 1); + } else { + previous = null; + } + } + + public LogEntry getPreviousEntry() { + return previous; + } + + public Identifier getIdentifier() { + return identifier; + } + + public LocalTime getTime() { + return time; + } + +} diff --git a/src/main/java/pulse/tasks/logs/StateEntry.java b/src/main/java/pulse/tasks/logs/StateEntry.java index c70128c4..e2ea9a55 100644 --- a/src/main/java/pulse/tasks/logs/StateEntry.java +++ b/src/main/java/pulse/tasks/logs/StateEntry.java @@ -8,41 +8,47 @@ public class StateEntry extends LogEntry { - private Status status; - - public StateEntry(SearchTask task, Status status) { - super(task); - this.setStatus(status); - } - - public SearchTask getTask() { - return TaskManager.getManagerInstance().getTask(getIdentifier()); - } - - public Status getStatus() { - return status; - } - - public void setStatus(Status status) { - this.status = status; - } - - public Status getState() { - return status; - } - - @Override - public String toString() { - var sb = new StringBuilder(); - sb.append("
"); - sb.append(getIdentifier().toString() + " changed status to "); - var hex = "#" + toHexString(status.getColor().getRGB()).substring(2); - sb.append("" + status.toString() + ""); - if (status.getDetails() != NONE) - sb.append(" due to " + status.getDetails() + ""); - sb.append(" at "); - sb.append(getTime()); - return sb.toString(); - } + private static final long serialVersionUID = 8380229394939453079L; + private Status status; + + public StateEntry(SearchTask task, Status status) { + super(task); + this.setStatus(status); + } + + public SearchTask getTask() { + return TaskManager.getManagerInstance().getTask(getIdentifier()); + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public Status getState() { + return status; + } + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append("
"); + sb.append(getIdentifier().toString() + " changed status to "); + var hex = "#" + toHexString(status.getColor().getRGB()).substring(2); + sb.append("" + status.toString() + ""); + if (status.getDetails() != NONE) { + sb.append(" due to " + status.getDetails() + ""); + } + if (status.getDetailedMessage().length() > 0) { + sb.append(" Details: "); + sb.append(status.getDetailedMessage()); + } + sb.append(" at "); + sb.append(getTime()); + return sb.toString(); + } } diff --git a/src/main/java/pulse/tasks/logs/Status.java b/src/main/java/pulse/tasks/logs/Status.java index e7670a0b..d479bc23 100644 --- a/src/main/java/pulse/tasks/logs/Status.java +++ b/src/main/java/pulse/tasks/logs/Status.java @@ -1,120 +1,148 @@ 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} * can be. * */ - public enum Status { - /** - * Not all necessary details have been uploaded to a {@code SearchTask} and that - * it cannot be executed yet. - */ - - INCOMPLETE(Color.RED), - - /** - * Everything seems to be in order and the task can now be executed. - */ - - READY(Color.MAGENTA), - - /** - * The task is being executed. - */ - - IN_PROGRESS(Color.DARK_GRAY), - - /** - * Task successfully finished. - */ - - DONE(Color.BLUE), - - /** - * An error has occurred during execution. - */ - - EXECUTION_ERROR(Color.red), - - /** - * The task has been terminated by the user. - */ - - TERMINATED(Color.DARK_GRAY), - - /** - * Task has been queued and is waiting to be executed. - */ - - QUEUED(Color.GREEN), - - /** - * Task has finished, but the results cannot be considered reliable (perhaps, - * due to large scatter of data points). - */ - - AMBIGUOUS(Color.GRAY), - - /** - * The iteration limit has been reached and the task aborted. - */ - - TIMEOUT(Color.RED), - - /** - * Task has finished without errors, however failing to meet a statistical - * criterion. - */ - - FAILED(Color.RED); - - private final Color clr; - private Details details = Details.NONE; - - Status(Color clr) { - this.clr = clr; - } - - public final Color getColor() { - return clr; - } - - public Details getDetails() { - return details; - } - - public void setDetails(Details details) { - this.details = details; - } - - static String parse(String str) { - var tokens = str.split("_"); - var sb = new StringBuilder(); - final var BLANK_SPACE = ' '; - for (var t : tokens) { - sb.append(t.toLowerCase()); - sb.append(BLANK_SPACE); - } - - return sb.toString(); - } - - @Override - public String toString() { - return parse(super.toString()); - } - - public String getMessage() { - var sb = new StringBuilder(); - sb.append(toString()); - if (details != null) - sb.append(" : ").append(details.toString()); - return sb.toString(); - } - -} \ No newline at end of file + /** + * Not all necessary details have been uploaded to a {@code SearchTask} and + * that it cannot be executed yet. + */ + INCOMPLETE(Color.RED), + /** + * Everything seems to be in order and the task can now be executed. + */ + READY(Color.MAGENTA), + /** + * The task is being executed. + */ + IN_PROGRESS(Color.DARK_GRAY), + /** + * Task successfully finished. + */ + DONE(Color.BLUE), + /** + * An error has occurred during execution. + */ + EXECUTION_ERROR(Color.red), + /** + * Termination requested. + */ + AWAITING_TERMINATION(Color.DARK_GRAY), + /** + * Task terminated + */ + TERMINATED(Color.DARK_GRAY), + /** + * Task has been queued and is waiting to be executed. + */ + QUEUED(Color.GREEN), + /** + * Task has finished, but the results cannot be considered reliable + * (perhaps, due to large scatter of data points). + */ + AMBIGUOUS(Color.GRAY), + /** + * The iteration limit has been reached and the task aborted. + */ + TIMEOUT(Color.RED), + /** + * Task has finished without errors, however failing to meet a statistical + * criterion. + */ + FAILED(Color.RED); + + private final Color clr; + private Details details = Details.NONE; + private String message = ""; + + Status(Color clr) { + this.clr = clr; + } + + public final Color getColor() { + return clr; + } + + public Details getDetails() { + return details; + } + + public void setDetails(Details details) { + this.details = details; + } + + public String getDetailedMessage() { + return message; + } + + public void setDetailedMessage(String str) { + this.message = str; + } + + static String parse(String str) { + var tokens = str.split("_"); + var sb = new StringBuilder(); + final var BLANK_SPACE = ' '; + for (var t : tokens) { + sb.append(t.toLowerCase()); + sb.append(BLANK_SPACE); + } + + return sb.toString(); + } + + public boolean checkProblemStatementSet() { + if (details == null) { + return true; + } + + switch (details) { + case MISSING_DIFFERENCE_SCHEME: + case MISSING_HEATING_CURVE: + case MISSING_PROBLEM_STATEMENT: + case INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT: + return false; + default: + return true; + } + + } + + @Override + public String toString() { + return parse(super.toString()); + } + + public String getMessage() { + var sb = new StringBuilder(); + sb.append(toString()); + if (details != null) { + sb.append(" : ").append(details.toString()); + } + 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; + } + +} diff --git a/src/main/java/pulse/tasks/logs/package-info.java b/src/main/java/pulse/tasks/logs/package-info.java index f41cde04..7a00e289 100644 --- a/src/main/java/pulse/tasks/logs/package-info.java +++ b/src/main/java/pulse/tasks/logs/package-info.java @@ -1,5 +1,4 @@ /** * Lists classes for logging, storing runtime information including statuses. */ - -package pulse.tasks.logs; \ No newline at end of file +package pulse.tasks.logs; diff --git a/src/main/java/pulse/tasks/package-info.java b/src/main/java/pulse/tasks/package-info.java index 8abb2174..45aa320c 100644 --- a/src/main/java/pulse/tasks/package-info.java +++ b/src/main/java/pulse/tasks/package-info.java @@ -6,5 +6,4 @@ * ordering of final execution results, storing intermediate results of * execution to check convergence. */ - -package pulse.tasks; \ No newline at end of file +package pulse.tasks; diff --git a/src/main/java/pulse/tasks/processing/AbstractResult.java b/src/main/java/pulse/tasks/processing/AbstractResult.java index 86d2ac05..cc727edb 100644 --- a/src/main/java/pulse/tasks/processing/AbstractResult.java +++ b/src/main/java/pulse/tasks/processing/AbstractResult.java @@ -17,94 +17,118 @@ * {@code NumericPropert}ies. * */ - public abstract class AbstractResult extends UpwardsNavigable { - private List properties; - private ResultFormat format; - - /** - * Constructs an {@code AbstractResult} with the list of properties specified by - * {@code format}. - * - * @param format a {@code ResultFormat} - */ - - public AbstractResult(ResultFormat format) { - this.format = format; - properties = new ArrayList<>(format.size()); - } - - public ResultFormat getFormat() { - return format; - } - - /** - * This will print out all the properties according to the {@code ResultFormat}. - */ - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - for (NumericProperty p : properties) { - if (p != null) { - sb.append(p.toString()); - sb.append(System.lineSeparator()); - } - } - return sb.toString(); - } - - /** - * Returns a list of {@code NumericPropert}ies, which conform to the chosen - * {@code ResultFormat} - * - * @return a list of relevant {@code NumericProperty} objects - */ - - public List getProperties() { - return properties; - } - - protected void addProperty(NumericProperty p) { - properties.add(p); - } - - protected NumericProperty getProperty(int i) { - return filterProperties(this, format).get(i); - } - - public void setFormat(ResultFormat format) { - this.format = format; - } - - /** - * A static method for filtering the properties contained in the {@code result} - * to choose only those that conform to the {@code format}. - * - * @param result an {@code AbstractResult} with a list of properties - * @param format the format used for filtering - * @return the filtered list of properties - */ - - public static List filterProperties(AbstractResult result, ResultFormat format) { - return format.getKeywords().stream().map(keyword -> { - var p = result.properties.stream().filter(property -> property.getType().equals(keyword)).findFirst(); - return p.isPresent() ? p.get() : def(keyword); - }).collect(Collectors.toList()); - } - - /** - * A static method for filtering the properties contained in the {@code result} - * to choose only those that conform to its {@code format}. - * - * @param result an {@code AbstractResult} with a list of properties and a - * specified format - * @return the filtered list of properties - */ - - public static List filterProperties(AbstractResult result) { - return filterProperties(result, result.format); - } - -} \ No newline at end of file + private List properties; + private ResultFormat format; + + /** + * Constructs an {@code AbstractResult} with the list of properties + * specified by {@code format}. + * + * @param format a {@code ResultFormat} + */ + public AbstractResult(ResultFormat format) { + this.format = format; + properties = new ArrayList<>(format.size()); + } + + public AbstractResult(AbstractResult r) { + this.properties = new ArrayList<>(r.getProperties()); + this.format = r.format; + } + + public ResultFormat getFormat() { + return format; + } + + /** + * This will print out all the properties according to the + * {@code ResultFormat}. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (NumericProperty p : properties) { + if (p != null) { + sb.append(p.toString()); + sb.append(System.lineSeparator()); + } + } + return sb.toString(); + } + + /** + * Returns a list of {@code NumericPropert}ies, which conform to the chosen + * {@code ResultFormat} + * + * @return a list of relevant {@code NumericProperty} objects + */ + public List getProperties() { + return properties; + } + + protected void addProperty(NumericProperty p) { + properties.add(p); + } + + protected NumericProperty getProperty(int i) { + return filterProperties(this, format).get(i); + } + + public void setFormat(ResultFormat format) { + this.format = format; + } + + /** + * A static method for filtering the properties contained in the + * {@code result} to choose only those that conform to the {@code format}. + * + * @param result an {@code AbstractResult} with a list of properties + * @param format the format used for filtering + * @return the filtered list of properties + */ + public static List filterProperties(AbstractResult result, ResultFormat format) { + return format.getKeywords().stream().map(keyword -> { + var p = result.properties.stream().filter(property -> property.getType().equals(keyword)).findFirst(); + return p.isPresent() ? p.get() : def(keyword); + }).collect(Collectors.toList()); + } + + /** + * A static method for filtering the properties contained in the + * {@code result} to choose only those that conform to its {@code format}. + * + * @param result an {@code AbstractResult} with a list of properties and a + * specified format + * @return the filtered list of properties + */ + public static List filterProperties(AbstractResult result) { + return filterProperties(result, result.format); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (o == null) { + return false; + } + + if (!(o.getClass().equals(o.getClass()))) { + return false; + } + + var another = (AbstractResult) o; + + if (!another.properties.containsAll(this.properties) || !this.properties.containsAll(another.properties)) { + return false; + } + + return another.format.equals(this.format); + + } + +} diff --git a/src/main/java/pulse/tasks/processing/AverageResult.java b/src/main/java/pulse/tasks/processing/AverageResult.java index a94995d7..f6186b07 100644 --- a/src/main/java/pulse/tasks/processing/AverageResult.java +++ b/src/main/java/pulse/tasks/processing/AverageResult.java @@ -3,10 +3,10 @@ import static pulse.properties.NumericProperties.derive; import java.math.BigDecimal; -import java.math.MathContext; import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; +import pulse.properties.NumericProperties; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -16,107 +16,115 @@ * {@code AbstractResult}s and calculating the associated errors of averaging. * */ - public class AverageResult extends AbstractResult { - private List results; + private static final long serialVersionUID = 5279249996318155238L; - public final static int SIGNIFICANT_FIGURES = 2; + private final List results; - /** - * This will create an {@code AverageResult} based on the {@code AbstractResult} - * in {@code res}. - *

- * It will also use the {@code resultFormat}. A method will be invoked to: (a) - * calculate the average values of the list of {@code NumericProperty} according - * to the {@code resultFormat}; (b) calculate the standard error associated with - * averaging; (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 - * averaged (not necessarily instances of {@code Result}). - * @param resultFormat the {@code ResultFormat}, which will be used for this - * {@code AveragedResult}. - */ + public final static int SIGNIFICANT_FIGURES = 2; - public AverageResult(List res, ResultFormat resultFormat) { - super(resultFormat); + /** + * This will create an {@code AverageResult} based on the + * {@code AbstractResult} in {@code res}. + *

+ * 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}. + *

+ * + * @param res a list of {@code AbstractResult}s that are going to be + * averaged (not necessarily instances of {@code Result}). + * @param resultFormat the {@code ResultFormat}, which will be used for this + * {@code AveragedResult}. + */ + public AverageResult(List res, ResultFormat resultFormat) { + super(resultFormat); - results = new ArrayList<>(); - results.addAll(res); + results = new ArrayList<>(); + results.addAll(res); - calculate(); - } + calculate(); + } - private void calculate() { + private void calculate() { - ResultOperations.process(results, ResultFormat.getInstance().size()); + var rs = new ResultStatistics(); - /* - * Calculate average and standard error for each column - */ + rs.process(results); - double[] av = ResultOperations.getAverages(); - double[] std = ResultOperations.getDeviations(); + /* + * Calculate average and statistical error for each column + */ + double[] av = rs.getAverages(); + double[] err = rs.getErrors(); - /* + /* * Round up - */ - - NumericProperty p; - NumericPropertyKeyword key; - - for (int j = 0; j < av.length; j++) { - key = getFormat().getKeywords().get(j); - - if (!Double.isFinite(std[j])) - p = derive(key, av[j]); // ignore error as the value is not finite - else { - var stdBig = (new BigDecimal(std[j])).sqrt(MathContext.DECIMAL64); - var avBig = new BigDecimal(av[j]); - - var resultStd = stdBig.setScale(SIGNIFICANT_FIGURES - stdBig.precision() + stdBig.scale(), - RoundingMode.HALF_UP); - - var resultAv = stdBig.precision() > 1 ? avBig.setScale(resultStd.scale(), RoundingMode.CEILING) : avBig; - - p = derive(key, resultAv.doubleValue()); - p.setError(resultStd.doubleValue()); - - } - - addProperty(p); - - } - - } - - /** - * This will analyse the list of {@code AbstractResult}s used for calculation of - * the average and find all associated individual results. - *

- * If it is established that some instances of {@code AverageResult} were used - * in the calculation, this will invoke this method recursively to get a full - * list of {@code AbstractResult}s that are not {@code AverageResult}s - * - * @return a list of {@code AbstractResult}s that are guaranteed not to be - * {@code AveragedResult}s. - */ - - public List getIndividualResults() { - List indResults = new ArrayList<>(); - - for (AbstractResult r : results) { - if (r instanceof AverageResult) { - var ar = (AverageResult) r; - indResults.addAll(ar.getIndividualResults()); - } else - indResults.add(r); - } - - return indResults; - } - -} \ No newline at end of file + */ + NumericProperty p; + NumericPropertyKeyword key; + + for (int j = 0; j < av.length; j++) { + key = getFormat().getKeywords().get(j); + + 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]); + + var error = stdBig.setScale( + SIGNIFICANT_FIGURES - stdBig.precision() + stdBig.scale(), + RoundingMode.HALF_UP); + + var mean = stdBig.precision() > 1 + ? avBig.setScale(error.scale(), RoundingMode.CEILING) + : avBig; + + p = derive(key, mean.doubleValue()); + p.setError(error.doubleValue()); + + } else { + //if integer + p = derive(key, (int) Math.round(av[j])); + p.setError((int) Math.round(err[j])); + } + + addProperty(p); + + } + + } + + /** + * This will analyse the list of {@code AbstractResult}s used for + * calculation of the mean and find all associated individual results. + *

+ * If it is established that some instances of {@code AverageResult} were + * used in the calculation, this will invoke this method recursively to get + * a full list of {@code AbstractResult}s that are not + * {@code AverageResult}s + * + * @return a list of {@code AbstractResult}s that are guaranteed not to be + * {@code AveragedResult}s. + */ + public List getIndividualResults() { + List indResults = new ArrayList<>(); + + for (AbstractResult r : results) { + if (r instanceof AverageResult) { + var ar = (AverageResult) r; + indResults.addAll(ar.getIndividualResults()); + } else { + indResults.add(r); + } + } + + return indResults; + } + +} diff --git a/src/main/java/pulse/tasks/processing/Buffer.java b/src/main/java/pulse/tasks/processing/Buffer.java index c7782d23..b257e59d 100644 --- a/src/main/java/pulse/tasks/processing/Buffer.java +++ b/src/main/java/pulse/tasks/processing/Buffer.java @@ -8,190 +8,202 @@ 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.IndexedVector; +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.tasks.SearchTask; +import pulse.search.GeneralTask; import pulse.util.PropertyHolder; /** * A {@code Buffer} is used to estimate the convergence of the reverse problem * solution, by comparing the variance of the properties to a pre-specified * error tolerance. - * + * * @see pulse.tasks.SearchTask.run() */ - public class Buffer extends PropertyHolder { - private IndexedVector[] data; - private double[] statistic; - private static int size = (int) def(BUFFER_SIZE).getValue(); - - /** - * Creates a {@code Buffer} with a default size. - */ - - public Buffer() { - init(); - } - - /** - * Retrieves the contents of this {@code Buffer}. - * - * @return the data - */ - - public IndexedVector[] getData() { - return data; - } - - public void init() { - this.data = new IndexedVector[size]; - statistic = new double[size]; - } - - /** - * (Over)writes a buffer cell corresponding to the {@code bufferElement} with - * the current set of parameters of {@code SearchTask}. - * - * @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.getResidualStatistic().getStatistic().getValue(); - data[bufferElement] = t.searchVector()[0]; - } - - /** - * Determines whether the relative error (variance divided by mean) for any of - * the properties in this buffer is higher than the expect - * {@code errorTolerance}. - * - * @param errorTolerance the maximum tolerated relative error. - * @return {@code true} if convergence has not been reached. - */ - - public boolean isErrorTooHigh(double errorTolerance) { - double[] e = new double[data[0].dimension()]; - - boolean result = false; - - for (int i = 0; i < e.length && (!result); i++) { - var index = data[0].getIndex(i); - final double av = average(index); - e[i] = variance(index) / (av*av); - - result = e[i] > errorTolerance; - } - - return result; - - } - - /** - * Calculates the average for the {@code index} -- if the respective - * {@code NumericProperty} is contained in the {@code IndexedVector} data of - * this {@code Buffer}. - * - * @param index a symbolic index (keyword) - * @return the mean of the data sample for the specific type of - * {@code NumericPropert}ies - */ - - public double average(NumericPropertyKeyword index) { - - double av = 0; - - for (IndexedVector v : data) { - av += v.get(index); - } - - return av / data.length; - - } - - /** - * Calculated the average statistic value - * - * @return the mean statistic value. - */ - - public double averageStatistic() { - - double av = 0; - - for (double ss : statistic) { - av += ss; - } - - return av / data.length; - - } - - /** - * Calculates the variance for the {@code index} -- if the respective - * {@code NumericProperty} is contained in the {@code IndexedVector} data of - * this {@code Buffer}. - * - * @param index a symbolic index (keyword). - * @return the variance of the data sample for the specific type of - * {@code NumericPropert}ies. - */ - - public double variance(NumericPropertyKeyword index) { - double sd = 0; - double av = average(index); - - for (IndexedVector v : data) { - final double s = v.get(index) - av; - sd += s*s; - } - - return sd / data.length; - - } - - /** - * Gets the buffer size (a NumericProperty derived from {@code BUFFER_SIZE}. - * - * @return the buffer size property - * @see pulse.properties.NumericPropertyKeyword - */ - - public static NumericProperty getSize() { - return derive(BUFFER_SIZE, size); - } - - /** - * Sets a new size for this {@code Buffer}. - * - * @param newSize a {@code NumericProperty} of the type {@code BUFFER_SIZE}. - */ - - public static void setSize(NumericProperty newSize) { - requireType(newSize, BUFFER_SIZE); - Buffer.size = ((Number) newSize.getValue()).intValue(); - } - - @Override - public void set(NumericPropertyKeyword type, NumericProperty property) { - if (type == BUFFER_SIZE) - setSize(property); - } - - /** - * The {@code BUFFER_SIZE} is the single listed parameter for this class. - * - * @see pulse.properties.NumericPropertyKeyword - */ - - @Override - public List listedTypes() { - return new ArrayList(Arrays.asList(def(BUFFER_SIZE))); - } - -} \ No newline at end of file + /** + * + */ + private static final long serialVersionUID = 3613745885879508057L; + private ParameterVector[] data; + private double[] statistic; + private static int size = (int) def(BUFFER_SIZE).getValue(); + + /** + * Creates a {@code Buffer} with a default size. + */ + public Buffer() { + init(); + } + + /** + * Retrieves the contents of this {@code Buffer}. + * + * @return the data + */ + public ParameterVector[] getData() { + return data; + } + + /* + * Re-inits the storage. + */ + public final void init() { + this.data = new ParameterVector[size]; + statistic = new double[size]; + } + + /** + * (Over)writes a buffer cell corresponding to the {@code bufferElement} + * with the current set of parameters of {@code SearchTask} and the search + * statistic. + * + * @param t the {@code SearchTask} + * @param bufferElement the {@code bufferElement} which will be written over + */ + public final void fill(GeneralTask t, int bufferElement) { + statistic[bufferElement] = (double) t.getResponse() + .getOptimiserStatistic().getStatistic().getValue(); + data[bufferElement] = t.searchVector(); + } + + /** + * Determines whether the relative error (variance divided by mean) for any + * of the properties in this buffer is higher than the expect + * {@code errorTolerance}. + * + * @param errorTolerance the maximum tolerated relative error. + * @return {@code true} if convergence has not been reached. + */ + public boolean isErrorTooHigh(double errorTolerance) { + double[] e = new double[data[0].dimension()]; + + boolean result = false; + + for (int i = 0; i < e.length && (!result); i++) { + var index = data[0].getParameters().get(i).getIdentifier().getKeyword(); + final double av = average(index); + e[i] = variance(index) / (av * av); + + result = e[i] > errorTolerance; + } + + return result; + + } + + /** + * Calculates the average for the {@code index} -- if the respective + * {@code NumericProperty} is contained in the {@code IndexedVector} data of + * this {@code Buffer}. + * + * @param index a symbolic index (keyword) + * @return the mean of the data sample for the specific type of + * {@code NumericPropert}ies + */ + public double average(NumericPropertyKeyword index) { + + double av = 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; + + } + + /** + * Calculates the average statistic value + * + * @return the mean statistic value. + */ + public double averageStatistic() { + double av = 0; + + for (double ss : statistic) { + av += ss; + } + + return av / data.length; + + } + + /** + * Calculates the variance for the {@code index} -- if the respective + * {@code NumericProperty} is contained in the {@code IndexedVector} data of + * this {@code Buffer}. + * + * @param index a symbolic index (keyword). + * @return the variance of the data sample for the specific type of + * {@code NumericPropert}ies. + */ + public double variance(NumericPropertyKeyword index) { + double sd = 0; + double av = average(index); + + for (ParameterVector v : data) { + final double s = v.getParameterValue(index, 0) - av; + sd += s * s; + } + + return sd / data.length; + + } + + /** + * Gets the buffer size (a NumericProperty derived from {@code BUFFER_SIZE}. + * + * @return the buffer size property + * @see pulse.properties.NumericPropertyKeyword + */ + public static NumericProperty getSize() { + return derive(BUFFER_SIZE, size); + } + + /** + * Sets a new size for this {@code Buffer}. + * + * @param newSize a {@code NumericProperty} of the type {@code BUFFER_SIZE}. + */ + public static void setSize(NumericProperty newSize) { + requireType(newSize, BUFFER_SIZE); + Buffer.size = ((Number) newSize.getValue()).intValue(); + } + + /* + * Sets the buffer size + * @param type @code{BUFFER_SIZE} + */ + @Override + public void set(NumericPropertyKeyword type, NumericProperty property) { + if (type == BUFFER_SIZE) { + setSize(property); + } + } + + /** + * The {@code BUFFER_SIZE} is the single listed parameter for this class. + * + * @see pulse.properties.NumericPropertyKeyword + */ + @Override + public List listedTypes() { + 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 88d9049b..9a79788f 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; @@ -7,8 +8,10 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import pulse.math.ParameterIdentifier; -import pulse.math.IndexedVector; +import pulse.math.ParameterVector; +import pulse.math.linear.Vector; import pulse.properties.NumericPropertyKeyword; import pulse.search.statistics.CorrelationTest; import pulse.search.statistics.EmptyCorrelationTest; @@ -16,88 +19,136 @@ import pulse.util.ImmutableDataEntry; import pulse.util.ImmutablePair; -public class CorrelationBuffer { +public class CorrelationBuffer implements Serializable { - private List params; - private static Set> excludePairList; - private static Set excludeSingleList; + private static final long serialVersionUID = 1672281370094463238L; + private List params; + private static final Set> excludePairList; + private static final Set excludeSingleList; - static { - excludePairList = new HashSet<>(); - excludeSingleList = new HashSet<>(); - excludeSingle(NumericPropertyKeyword.DIFFUSIVITY); - excludePair(NumericPropertyKeyword.HEAT_LOSS_SIDE, NumericPropertyKeyword.MAXTEMP); - excludePair(NumericPropertyKeyword.HEAT_LOSS, NumericPropertyKeyword.MAXTEMP); - excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.BASELINE_INTERCEPT); - excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.BASELINE_SLOPE); - } + private final static double DEFAULT_THRESHOLD = 1E-3; - public CorrelationBuffer() { - params = new ArrayList<>(); - } + static { + excludePairList = new HashSet<>(); + excludeSingleList = new HashSet<>(); + excludeSingle(NumericPropertyKeyword.DIFFUSIVITY); + excludePair(NumericPropertyKeyword.HEAT_LOSS_SIDE, NumericPropertyKeyword.MAXTEMP); + excludePair(NumericPropertyKeyword.HEAT_LOSS, NumericPropertyKeyword.MAXTEMP); + excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.BASELINE_INTERCEPT); + excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.BASELINE_SLOPE); + excludePair(NumericPropertyKeyword.MAXTEMP, NumericPropertyKeyword.HEAT_LOSS_COMBINED); + } - public void inflate(SearchTask t) { - params.add(t.searchVector()[0]); - } + public CorrelationBuffer() { + params = new ArrayList<>(); + } - public void clear() { - params.clear(); - } + public void inflate(SearchTask t) { + params.add(t.searchVector()); + } - public Map, Double> evaluate(CorrelationTest t) { - if(params.isEmpty()) - throw new IllegalStateException("Zero number of entries in parameter list"); + public void clear() { + params.clear(); + } - if (t instanceof EmptyCorrelationTest) - return null; + /** + * 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; - var indices = params.get(0).getIndices(); - var map = indices.stream() - .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble(v -> v.get(index)).toArray())) - .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); + for (i = 0; i < size - 1; i = i + 2) { - int indicesSize = indices.size(); - var correlationMap = new HashMap, Double>(); - ImmutablePair pair = null; + 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 i = 0; i < indicesSize; i++) { + for (int j = size - 1; j > i; j--) { + params.remove(j); + } + } - if (!excludeSingleList.contains(indices.get(i))) - 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)))); - } + public Map, Double> evaluate(CorrelationTest t) { + if (params.isEmpty()) { + throw new IllegalStateException("Zero number of entries in parameter list"); + } - } + if (t instanceof EmptyCorrelationTest) { + return null; + } - return correlationMap; + 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())) + .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue())); - public boolean test(CorrelationTest t) { - var map = evaluate(t); + int indicesSize = indices.size(); + var correlationMap = new HashMap, Double>(); + ImmutablePair pair; - if (map == null) - return false; + for (int i = 0; i < indicesSize; i++) { - for (Double d : map.values()) { - if (t.compareToThreshold(d)) - return true; - } - return false; - } + var iKey = indices.get(i).getKeyword(); - public static void excludePair(ImmutablePair pair) { - excludePairList.add(pair); - } + if (!excludeSingleList.contains(iKey)) { - public static void excludePair(NumericPropertyKeyword first, NumericPropertyKeyword second) { - excludePair(new ImmutablePair<>(first, second)); - } + for (int j = i + 1; j < indicesSize; j++) { - public static void excludeSingle(NumericPropertyKeyword key) { - excludeSingleList.add(key); - } + var jKey = indices.get(j).getKeyword(); -} \ No newline at end of file + 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)))); + + } + + } + + } + + } + + return correlationMap; + + } + + public boolean test(CorrelationTest t) { + var map = evaluate(t); + + if (map == null) { + return false; + } + + var values = map.values(); + + return map.values().stream().anyMatch(d -> t.compareToThreshold(d)); + } + + public static void excludePair(ImmutablePair pair) { + excludePairList.add(pair); + } + + public static void excludePair(NumericPropertyKeyword first, NumericPropertyKeyword second) { + excludePair(new ImmutablePair<>(first, second)); + } + + public static void excludeSingle(NumericPropertyKeyword key) { + excludeSingleList.add(key); + } + +} diff --git a/src/main/java/pulse/tasks/processing/Result.java b/src/main/java/pulse/tasks/processing/Result.java index dac042ed..083b82f2 100644 --- a/src/main/java/pulse/tasks/processing/Result.java +++ b/src/main/java/pulse/tasks/processing/Result.java @@ -1,38 +1,54 @@ package pulse.tasks.processing; +import pulse.tasks.Calculation; +import pulse.tasks.Identifier; import pulse.tasks.SearchTask; import pulse.ui.Messages; /** * The individual {@code Result} that is associated with a {@code SearchTask}. * The {@code Identifier} of the task is stored as a field value. - * + * * @see pulse.tasks.SearchTask * @see pulse.tasks.Identifier */ - public class Result extends AbstractResult { - /** - * Creates an individual {@code Result} related to the current state of - * {@code task} using the specified {@code format}. - * - * @param task a {@code SearchTask}, the properties of which that conform to - * {@code ResultFormat} will form this {@code Result} - * @param format a {@code ResultFormat} - * @throws IllegalArgumentException if {@code task} is null - */ - - public Result(SearchTask task, ResultFormat format) throws IllegalArgumentException { - super(format); - - if (task == null) - throw new IllegalArgumentException(Messages.getString("Result.NullTaskError")); - - setParent(task); - - format.getKeywords().stream().forEach(key -> addProperty(task.numericProperty(key))); - - } + /** + * + */ + 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}. + * + * @param task a {@code SearchTask}, the properties of which that conform to + * {@code ResultFormat} will form this {@code Result} + * @param format a {@code ResultFormat} + * @throws IllegalArgumentException if {@code task} is null + */ + public Result(SearchTask task, ResultFormat format) throws IllegalArgumentException { + super(format); + + if (task == null) { + throw new IllegalArgumentException(Messages.getString("Result.NullTaskError")); + } + + 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 40e5ab15..3c0cd56f 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,135 +24,155 @@ * characters. *

*/ - -public class ResultFormat { - - private List nameMap; - - private final static NumericPropertyKeyword[] minimalArray = new NumericPropertyKeyword[] { IDENTIFIER, - TEST_TEMPERATURE, DIFFUSIVITY }; - - /** - *

- * The default format specified by the - * {@code Messages.getString("ResultFormat.DefaultFormat")}. See file - * messages.properties in {@code pulse.ui}. - *

- */ - - private static ResultFormat format = new ResultFormat(); - private static List listeners = new ArrayList(); - - private ResultFormat() { - this(asList(minimalArray)); - } - - private ResultFormat(List keys) { - nameMap = new ArrayList<>(); - for (var key : keys) { - nameMap.add(key); - } - } - - private ResultFormat(ResultFormat fmt) { - nameMap = new ArrayList<>(fmt.nameMap.size()); - nameMap.addAll(fmt.nameMap); - } - - public static void addResultFormatListener(ResultFormatListener rfl) { - listeners.add(rfl); - } - - public static ResultFormat generateFormat(List keys) { - format = new ResultFormat(keys); - - var rfe = new ResultFormatEvent(format); - for (var rfl : listeners) { - rfl.resultFormatChanged(rfe); - } - - return format; - } - - /** - * This class uses a singleton pattern, meaning there is only instance of this - * class. - * - * @return the single (static) instance of this class - */ - - public static ResultFormat getInstance() { - return format; - } - - /** - * Retrieves the list of keyword associated with this {@code ResultFormat} - * - * @return a list of keywords that can be used to access {@code NumericProperty} - * objects - */ - - public List getKeywords() { - return nameMap; - } - - /** - * Creates a {@code List} of default abbreviations corresponding to the - * list of keywords specific to {@code NumericProperty} objects. - * - * @return a list of abbreviations (typically, for filling the result table - * headers) - */ - - public List abbreviations() { - return nameMap.stream().map(keyword -> def(keyword).getAbbreviation(true)).collect(toList()); - } - - /** - * Creates a {@code List} of default descriptions corresponding to the - * list of keywords specific to {@code NumericProperty} objects. - * - * @return a list of abbreviations (typically, for filling the result table - * tooltips) - */ - - public List descriptors() { - return nameMap.stream().map(keyword -> def(keyword).getDescriptor(false)).collect(toList()); - } - - /** - * Finds a {@code NumericPropertyKeyword} contained in the {@code nameMap}, the - * description of which matches {@code descriptor}. - * - * @param descriptor a {@code String} describing the - * {@code NumericPropertyKeyword} - * @return the {@code NumericPropertyKeyword} object - */ - - public NumericPropertyKeyword fromAbbreviation(String descriptor) { - return nameMap.stream().filter(keyword -> def(keyword).getAbbreviation(true).equals(descriptor)) - .findFirst().get(); - } - - /** - * Calculates the length of the format string, which is the same as the size of - * the keyword list. - * - * @return an integer, representing the size of the format string. - */ - - public int size() { - return nameMap.size(); - } - - public int indexOf(NumericPropertyKeyword key) { - if (nameMap.contains(key)) - return nameMap.indexOf(key); - return -1; - } - - public static NumericPropertyKeyword[] getMinimalArray() { - return minimalArray; - } +public class ResultFormat implements Serializable { + + /** + * + */ + private static final long serialVersionUID = -3155104011585735097L; + + private List nameMap; + + private final static NumericPropertyKeyword[] minimalArray = new NumericPropertyKeyword[]{IDENTIFIER, + TEST_TEMPERATURE, DIFFUSIVITY}; + + /** + *

+ * The default format specified by the + * {@code Messages.getString("ResultFormat.DefaultFormat")}. See file + * messages.properties in {@code pulse.ui}. + *

+ */ + private static ResultFormat format = new ResultFormat(); + private static List listeners = new ArrayList(); + + private ResultFormat() { + this(asList(minimalArray)); + } + + private ResultFormat(List keys) { + nameMap = new ArrayList<>(); + 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); + + var rfe = new ResultFormatEvent(format); + for (var rfl : listeners) { + rfl.resultFormatChanged(rfe); + } + + return format; + } + + /** + * This class uses a singleton pattern, meaning there is only instance of + * this class. + * + * @return the single (static) instance of this class + */ + public static ResultFormat getInstance() { + return format; + } + + /** + * Retrieves the list of keyword associated with this {@code ResultFormat} + * + * @return a list of keywords that can be used to access + * {@code NumericProperty} objects + */ + public List getKeywords() { + return nameMap; + } + + /** + * Creates a {@code List} of default abbreviations corresponding to + * the list of keywords specific to {@code NumericProperty} objects. + * + * @return a list of abbreviations (typically, for filling the result table + * headers) + */ + public List abbreviations() { + return nameMap.stream().map(keyword -> def(keyword).getAbbreviation(true)).collect(toList()); + } + + /** + * Creates a {@code List} of default descriptions corresponding to + * the list of keywords specific to {@code NumericProperty} objects. + * + * @return a list of abbreviations (typically, for filling the result table + * tooltips) + */ + public List descriptors() { + return nameMap.stream().map(keyword -> def(keyword).getDescriptor(true)).collect(toList()); + } + + /** + * Finds a {@code NumericPropertyKeyword} contained in the {@code nameMap}, + * the description of which matches {@code descriptor}. + * + * @param descriptor a {@code String} describing the + * {@code NumericPropertyKeyword} + * @return the {@code NumericPropertyKeyword} object + */ + public NumericPropertyKeyword fromAbbreviation(String descriptor) { + return nameMap.stream().filter(keyword -> def(keyword).getAbbreviation(true).equals(descriptor)) + .findFirst().get(); + } + + /** + * Calculates the length of the format string, which is the same as the size + * of the keyword list. + * + * @return an integer, representing the size of the format string. + */ + public int size() { + return nameMap.size(); + } + + public int indexOf(NumericPropertyKeyword key) { + if (nameMap.contains(key)) { + return nameMap.indexOf(key); + } + return -1; + } + + public static NumericPropertyKeyword[] getMinimalArray() { + return minimalArray; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (o == null) { + return false; + } + + if (!(o.getClass().equals(o.getClass()))) { + return false; + } + + var another = (ResultFormat) o; + + return (another.nameMap.containsAll(this.nameMap)) && (this.nameMap.containsAll(another.nameMap)); + + } } \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/ResultOperations.java b/src/main/java/pulse/tasks/processing/ResultOperations.java deleted file mode 100644 index 0391133c..00000000 --- a/src/main/java/pulse/tasks/processing/ResultOperations.java +++ /dev/null @@ -1,34 +0,0 @@ -package pulse.tasks.processing; - -import java.util.List; - -class ResultOperations { - - private static double[] av; - private static double[] dev; - - public static ResultProcessing average = ( (p, i) -> ((Number) p.getValue()).doubleValue()); - - public static ResultProcessing stdev = (p, i) -> { - double x = ( (Number) p.getValue() ).doubleValue() - av[i]; - return x*x; - }; - - private ResultOperations() { - //intentionaly blank - } - - public static void process(List results, final int properties) { - av = average.process(results, properties); - dev = stdev.process(results, properties); - } - - public static double[] getAverages() { - return av; - } - - public static double[] getDeviations() { - return dev; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/processing/ResultProcessing.java b/src/main/java/pulse/tasks/processing/ResultProcessing.java deleted file mode 100644 index cd2cd14f..00000000 --- a/src/main/java/pulse/tasks/processing/ResultProcessing.java +++ /dev/null @@ -1,24 +0,0 @@ -package pulse.tasks.processing; - -import java.util.List; - -import pulse.properties.NumericProperty; - -interface ResultProcessing { - - public default double[] process(List results, final int properties) { - final int size = results.size(); - double[] av = new double[properties]; - - for (int i = 0; i < av.length; i++) { - for (AbstractResult r : results) { - av[i] += value(r.getProperty(i), i); - } - av[i] /= size; - } - return av; - } - - public abstract double value(NumericProperty p, int i); - -} \ 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 new file mode 100644 index 00000000..d6d09118 --- /dev/null +++ b/src/main/java/pulse/tasks/processing/ResultStatistics.java @@ -0,0 +1,119 @@ +package pulse.tasks.processing; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.DoubleStream; +import org.apache.commons.math3.distribution.TDistribution; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; +import pulse.properties.NumericPropertyKeyword; + +import pulse.util.ImmutablePair; + +/** + * A simple collection of mean values and errors associated with an + * {@code AverageResult}, and a method for calculating those statistics. + * + * @author Artem Lunev + */ +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. + */ + public final static double CONFIDENCE_LEVEL = 0.95; + + public ResultStatistics() { + //intentionaly blank + } + + /** + * This will calculate the statistics for each type of + * {@code NumericProperty} stored in the {@code results}. It is assumed that + * the order in which these properties appear in each element of + * {@code results} is consistent, and it will be maintained as the order in + * which these statistic are stored. The calculated statistics include the + * arithmetic mean and the statistical error. The latter is calculated + * 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. + * + * @param results a list of individual (or average) results to be processed + */ + public void process(List results) { + + //group all properties contained in the list of results by their keyword + Map> map = results.stream() + .flatMap(r -> r.getProperties().stream()) + .collect(Collectors.groupingBy(t -> t.getType(), + Collectors.mapping(p + -> ((Number) p.getValue()).doubleValue(), + Collectors.toList()))); + + /* + 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()); + + av = stats.stream().mapToDouble(pair -> pair.getFirst()).toArray(); //store mean values + + //Student t-distribution with degrees of freedom equal to number of individual results + TDistribution student = new TDistribution(av.length); + //CDF value + double t = student.inverseCumulativeProbability(CONFIDENCE_LEVEL); //right tail + + err = stats.stream().mapToDouble(pair + -> t * pair.getSecond() //the error is equal to half of the span of the confidence interval + ).toArray(); //store errors + + } + + private DoubleStream openStream(List input) { + return input.stream().mapToDouble(d -> d); + } + + /** + * Retrieves the mean values of properties. + * + * @return the mean values + * @see process() + */ + public double[] getAverages() { + return av; + } + + /** + * Retrieves the statistical errors associated with the property values. + * + * @return the values of the statistical error + * @see process() + */ + public double[] getErrors() { + return err; + } + +} diff --git a/src/main/java/pulse/tasks/processing/package-info.java b/src/main/java/pulse/tasks/processing/package-info.java index 7542dcc6..5db104f1 100644 --- a/src/main/java/pulse/tasks/processing/package-info.java +++ b/src/main/java/pulse/tasks/processing/package-info.java @@ -3,5 +3,4 @@ * ordering of final execution results, storing intermediate results of * execution to check convergence. */ - -package pulse.tasks.processing; \ No newline at end of file +package pulse.tasks.processing; diff --git a/src/main/java/pulse/ui/ColorGenerator.java b/src/main/java/pulse/ui/ColorGenerator.java new file mode 100644 index 00000000..ac56b59f --- /dev/null +++ b/src/main/java/pulse/ui/ColorGenerator.java @@ -0,0 +1,44 @@ +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; + +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]); + } + +} diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index f6da21f6..f766933a 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -1,165 +1,164 @@ package pulse.ui; import static java.awt.EventQueue.invokeLater; -import static java.awt.Image.SCALE_SMOOTH; import static java.awt.SplashScreen.getSplashScreen; -import static java.lang.Integer.valueOf; -import static java.lang.Runtime.getRuntime; import static java.lang.System.err; -import static java.lang.management.ManagementFactory.getPlatformMBeanServer; +import static java.lang.System.setErr; +import static java.time.LocalDateTime.now; import static java.util.Objects.requireNonNull; -import static java.util.logging.Level.SEVERE; -import static java.util.logging.Logger.getLogger; -import static javax.management.ObjectName.getInstance; -import static javax.swing.UIManager.getInstalledLookAndFeels; -import static javax.swing.UIManager.setLookAndFeel; import static pulse.ui.frames.TaskControlFrame.getInstance; -import javax.management.Attribute; -import javax.management.AttributeList; -import javax.management.InstanceNotFoundException; -import javax.management.MalformedObjectNameException; -import javax.management.ObjectName; -import javax.management.ReflectionException; -import javax.swing.ImageIcon; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import javax.swing.JOptionPane; +import javax.swing.UIManager; + +import com.formdev.flatlaf.FlatDarkLaf; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; /** *

* This is the main class used to launch {@code PULsE} and start the GUI. In - * addition to providing the launcher methods, it also provides some - * functionality for accessing the System CPU and memory usage, as well as the - * number of available threads that can be used in calculation. + * addition to providing the launcher methods, it also redirects the System.err + * stream to an external file. An empty log file is deleted upon program exit + * via a shutdown hook. *

* */ - public class Launcher { - private Launcher() { - // intentionally blank - } - - /** - * Launches the application and creates a GUI. - */ - - public static void main(String[] args) { - splashScreen(); - - /* Set the Nimbus Look and feel setting code. - /* - * If Nimbus (introduced in Java SE 6) is not available, stay with the default - * look and feel. For details see - * http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html - */ - try { - for (var info : getInstalledLookAndFeels()) { - if ("Nimbus".equals(info.getName())) { - setLookAndFeel(info.getClassName()); - break; - } - } - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException - | javax.swing.UnsupportedLookAndFeelException ex) { - getLogger(Launcher.class.getName()).log(SEVERE, null, ex); - } - // - - /* Create and display the form */ - invokeLater(() -> { - getInstance().setLocationRelativeTo(null); - getInstance().setVisible(true); - }); - } - - private static void splashScreen() { - var splash = getSplashScreen(); - if (splash == null) - err.println("SplashScreen.getSplashScreen() returned null"); - else { - var g = splash.createGraphics(); - requireNonNull(g, "splash.createGraphics() returned null"); - } - } - - /** - *

- * This will calculate the ratio {@code totalMemory/maxMemory} using the - * standard {@code Runtime}. Note this memory usage depends on heap allocation - * for the JVM. - *

- * - * @return a value depicting the memory usage - */ - - public static double getMemoryUsage() { - double totalMemory = getRuntime().totalMemory(); - double maxMemory = getRuntime().maxMemory(); - return (totalMemory / maxMemory * 100); - } - - /** - *

- * This will calculate the CPU load for the machine running {@code PULsE}. Note - * this is rather code-intensive, so it is recommende to use only at certain - * time intervals. - *

- * - * @return a value depicting the CPU usage - */ - - public static double cpuUsage() { - - var mbs = getPlatformMBeanServer(); - ObjectName name = null; - try { - name = getInstance("java.lang:type=OperatingSystem"); - } catch (MalformedObjectNameException | NullPointerException e1) { - err.println("Error while calculating CPU usage:"); - e1.printStackTrace(); - } - - AttributeList list = null; - try { - list = mbs.getAttributes(name, new String[] { "ProcessCpuLoad" }); - } catch (InstanceNotFoundException | ReflectionException e) { - err.println("Error while calculating CPU usage:"); - e.printStackTrace(); - } - - if (list.isEmpty()) - return valueOf(null); - - var att = (Attribute) list.get(0); - var value = (double) att.getValue(); - - if (value < 0) - return valueOf(null); - - return (value * 100); - } - - /** - * Finds the number of threads available for calculation. This will be used by - * the {@code TaskManager} when allocating the {@code ForkJoinPool} for running - * several tasks in parallel. - * - * @return the number of threads, which is greater or equal to the number of - * cores - * @see pulse.tasks.TaskManager - */ - - public static int threadsAvailable() { - var number = getRuntime().availableProcessors(); - return number > 1 ? (number - 1) : 1; - } - - public static ImageIcon loadIcon(String path, int iconSize) { - var imageIcon = new ImageIcon(Launcher.class.getResource("/images/" + path)); // load the image to a - // imageIcon - var image = imageIcon.getImage(); // transform it - var newimg = image.getScaledInstance(iconSize, iconSize, SCALE_SMOOTH); // scale it the smooth way - return new ImageIcon(newimg); // transform it back - } - -} \ No newline at end of file + private PrintStream errStream; + private File errorLog; + private final static boolean DEBUG = false; + + private static final File LOCK = new File("pulse.lock"); + + private Launcher() { + if (!DEBUG) { + arrangeErrorOutput(); + } + arrangeMessages(); + } + + /** + * Launches the application and creates a GUI. + */ + public static void main(String[] args) { + new Launcher(); + + if (!LOCK.exists()) { + + try { + LOCK.createNewFile(); + } catch (IOException ex) { + Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, "Unable to create lock file", ex); + } + + LOCK.deleteOnExit(); + + splashScreen(); + + //WebLookAndFeel.install(WebDarkSkin.class); + FlatDarkLaf.setup(); + try { + UIManager.setLookAndFeel(new FlatDarkLaf()); + } catch (Exception ex) { + System.err.println("Failed to initialize LaF"); + } + + var newVersion = Version.getCurrentVersion().checkNewVersion(); + + /* Create and display the form */ + invokeLater(() -> { + getInstance().setLocationRelativeTo(null); + getInstance().setVisible(true); + + if (newVersion != null) { + JOptionPane.showMessageDialog(null, "A new version of this software is available: " + + newVersion.toString() + "
Please visit the PULsE website for more details."); + } + + }); + + } else { + System.out.println(Messages.getString("msg.running")); + } + + } + + private static void splashScreen() { + var splash = getSplashScreen(); + if (splash == null) { + err.println("SplashScreen.getSplashScreen() returned null"); + } else { + var g = splash.createGraphics(); + requireNonNull(g, "splash.createGraphics() returned null"); + } + } + + private void arrangeErrorOutput() { + String path = Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + String decodedPath = ""; + // + try { + decodedPath = URLDecoder.decode(path, "UTF-8"); + } catch (UnsupportedEncodingException e1) { + System.err.println("Unsupported UTF-8 encoding. Details below."); + e1.printStackTrace(); + } + // + try { + var dir = new File(decodedPath).getParent(); + errorLog = new File(dir + File.separator + "ErrorLog_" + now() + ".log"); + setErr(new PrintStream(errorLog)); + } catch (FileNotFoundException e) { + System.err.println("Unable to set up error stream"); + e.printStackTrace(); + } + + createShutdownHook(); + + } + + private void arrangeMessages() { + System.setOut(new PrintStream(System.out) { + + @Override + public void println(String str) { + JOptionPane.showMessageDialog(null, Messages.getString("TextWrap.0") + + str + Messages.getString("TextWrap.1")); + } + + }); + } + + private void createShutdownHook() { + + /* + * Delete log file on program exit if empty + */ + Runnable r = () -> { + if (errorLog != null && errorLog.exists() && errorLog.length() < 1) { + errorLog.delete(); + } + //delete lock explicitly on abnormal termination + if (LOCK.exists()) { + LOCK.delete(); + } + }; + Runtime.getRuntime().addShutdownHook(new Thread(r)); + + } + + @Override + public void finalize() { + errStream.close(); + } + +} diff --git a/src/main/java/pulse/ui/Messages.java b/src/main/java/pulse/ui/Messages.java index 9de6ff73..169c9f34 100644 --- a/src/main/java/pulse/ui/Messages.java +++ b/src/main/java/pulse/ui/Messages.java @@ -6,18 +6,19 @@ import java.util.ResourceBundle; public class Messages { - private static final String BUNDLE_NAME = "messages"; - private static final ResourceBundle RESOURCE_BUNDLE = getBundle(BUNDLE_NAME); + private static final String BUNDLE_NAME = "messages"; - private Messages() { - } + private static final ResourceBundle RESOURCE_BUNDLE = getBundle(BUNDLE_NAME); - public static String getString(String key) { - try { - return RESOURCE_BUNDLE.getString(key); - } catch (MissingResourceException e) { - return '!' + key + '!'; - } - } -} \ No newline at end of file + private Messages() { + } + + public static String getString(String key) { + try { + return RESOURCE_BUNDLE.getString(key); + } catch (MissingResourceException e) { + return '!' + key + '!'; + } + } +} diff --git a/src/main/java/pulse/ui/Version.java b/src/main/java/pulse/ui/Version.java new file mode 100644 index 00000000..c1a17903 --- /dev/null +++ b/src/main/java/pulse/ui/Version.java @@ -0,0 +1,67 @@ +package pulse.ui; + +import static pulse.ui.Messages.getString; + +import java.io.IOException; +import java.net.URL; +import java.text.DateFormat; +import java.util.Date; + +import org.apache.commons.io.IOUtils; + +import pulse.io.readers.ReaderManager; + +public class Version { + + private long versionDate; + private String versionLabel; + private static Version currentVersion = ReaderManager.readVersion(); + + public Version(String label, long versionDate) { + this.versionLabel = label; + this.versionDate = versionDate; + } + + public Version checkNewVersion() { + + try { + var website = new URL("https://kotik-coder.github.io/Version.txt"); + var conn = website.openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + long date = conn.getLastModified(); + + if (date == 0) { + System.out.println("No remote version info found"); + } + + var label = IOUtils.toString(website, "UTF-8"); + + return Long.compare(date, versionDate) > 0 ? new Version(label, date) : null; + + } catch (IOException e) { + System.err.println("Could not check for new version"); + e.printStackTrace(); + return null; + } + } + + public long getVersionDate() { + return versionDate; + } + + public String getVersionLabel() { + return versionLabel; + } + + public String toString() { + var fmt = DateFormat.getDateInstance(DateFormat.SHORT); + return getString("TaskControlFrame.SoftwareTitle") + " - " + versionLabel + " (" + fmt.format(new Date(versionDate)) + ")"; + } + + public static Version getCurrentVersion() { + return currentVersion; + } + +} diff --git a/src/main/java/pulse/ui/components/AuxChart.java b/src/main/java/pulse/ui/components/AuxChart.java deleted file mode 100644 index 1ca5eb25..00000000 --- a/src/main/java/pulse/ui/components/AuxChart.java +++ /dev/null @@ -1,118 +0,0 @@ -package pulse.ui.components; - -import static java.awt.Color.RED; -import static java.awt.Color.black; -import static java.awt.Color.white; -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.ChartPanel; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.annotations.XYTitleAnnotation; -import org.jfree.chart.block.BlockBorder; -import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.renderer.xy.XYDifferenceRenderer; -import org.jfree.chart.title.LegendTitle; -import org.jfree.chart.ui.RectangleAnchor; -import org.jfree.chart.ui.RectangleEdge; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; - -import pulse.problem.statements.Problem; - -public class AuxChart { - - private ChartPanel chartPanel; - private JFreeChart chart; - private XYPlot plot; - - private final static int NUM_PULSE_POINTS = 100; - - private final static double TO_MILLIS = 1E3; - - public AuxChart() { - chart = ChartFactory.createScatterPlot("", "Time (ms)", "Laser Power (a. u.)", null, VERTICAL, true, true, - false); - - plot = chart.getXYPlot(); - plot.setBackgroundPaint(white); - setFonts(); - setRenderer(); - setLegendTitle(); - - chart.removeLegend(); - chartPanel = new ChartPanel(chart); - } - - private void setRenderer() { - var rendererPulse = new XYDifferenceRenderer(new Color(0.0f, 0.2f, 0.8f, 0.1f), Color.red, false); - rendererPulse.setSeriesPaint(0, RED); - rendererPulse.setSeriesStroke(0, new BasicStroke(3.0f)); - plot.setRenderer(rendererPulse); - } - - private void setFonts() { - var fontLabel = new Font("Arial", Font.PLAIN, 20); - var fontTicks = new Font("Arial", Font.PLAIN, 16); - plot.getDomainAxis().setLabelFont(fontLabel); - plot.getDomainAxis().setTickLabelFont(fontTicks); - plot.getRangeAxis().setLabelFont(fontLabel); - plot.getRangeAxis().setTickLabelFont(fontTicks); - } - - private void setLegendTitle() { - var lt = new LegendTitle(plot); - lt.setItemFont(new Font("Dialog", PLAIN, 16)); - 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); - ta.setMaxWidth(0.58); - plot.addAnnotation(ta); - } - - public void plot(Problem problem) { - requireNonNull(problem); - - double startTime = (double) problem.getHeatingCurve().getTimeShift().getValue(); - - var pulseDataset = new XYSeriesCollection(); - - pulseDataset.addSeries(series(problem, startTime)); - - plot.setDataset(0, pulseDataset); - } - - private static XYSeries series(Problem problem, double startTime) { - var pulse = problem.getPulse(); - - var series = new XYSeries(pulse.getPulseShape().toString()); - - double timeLimit = (double)pulse.getPulseWidth().getValue(); - final double timeFactor = problem.getProperties().timeFactor(); - - double dx = timeLimit / (NUM_PULSE_POINTS - 1); - double x = startTime; - - series.add(TO_MILLIS*(startTime - dx / 10.), 0.0); - series.add(TO_MILLIS*(startTime + timeLimit + dx / 10.), 0.0); - - for (var i = 0; i < NUM_PULSE_POINTS; i++) { - series.add(x*TO_MILLIS, pulse.evaluateAt((x-startTime)/timeFactor)); - x += dx; - } - - return series; - } - - public ChartPanel getChartPanel() { - return chartPanel; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/AuxPlotter.java b/src/main/java/pulse/ui/components/AuxPlotter.java new file mode 100644 index 00000000..f35d34db --- /dev/null +++ b/src/main/java/pulse/ui/components/AuxPlotter.java @@ -0,0 +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) { + 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)); + } else { + 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); + 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"); + chart.setBackgroundPaint(color); + chartPanel = new ChartPanel(chart); + this.plot = chart.getXYPlot(); + } + +} diff --git a/src/main/java/pulse/ui/components/CalculationTable.java b/src/main/java/pulse/ui/components/CalculationTable.java new file mode 100644 index 00000000..4ebf483f --- /dev/null +++ b/src/main/java/pulse/ui/components/CalculationTable.java @@ -0,0 +1,105 @@ +package pulse.ui.components; + +import static javax.swing.ListSelectionModel.SINGLE_SELECTION; +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.ListSelectionModel; +import javax.swing.SwingUtilities; +import javax.swing.event.ListSelectionEvent; +import javax.swing.table.TableCellRenderer; + +import pulse.tasks.SearchTask; +import pulse.tasks.TaskManager; +import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.ui.components.controllers.TaskTableRenderer; +import pulse.ui.components.models.StoredCalculationTableModel; + +@SuppressWarnings("serial") +public class CalculationTable extends JTable { + + private final static int ROW_HEIGHT = 70; + 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); + setRowHeight(ROW_HEIGHT); + + setFillsViewportHeight(true); + setSelectionMode(SINGLE_SELECTION); + setShowHorizontalLines(false); + + var model = new StoredCalculationTableModel(); + setModel(model); + + getTableHeader().setPreferredSize(new Dimension(50, HEADER_HEIGHT)); + + setAutoCreateRowSorter(false); + initListeners(); + + var instance = TaskManager.getManagerInstance(); + instance.addTaskRepositoryListener(e -> { + + if (e.getState() == TaskRepositoryEvent.State.TASK_CRITERION_SWITCH) { + update(TaskManager.getManagerInstance().getSelectedTask()); + } + + }); + + } + + public void update(SearchTask t) { + if (t != null) { + SwingUtilities.invokeLater(() -> { + ((StoredCalculationTableModel) getModel()).update(t); + identifySelection(t); + }); + } + } + + public void identifySelection(SearchTask t) { + int modelIndex = t.getStoredCalculations().indexOf(t.getResponse()); + if (modelIndex > -1) { + this.getSelectionModel().setSelectionInterval(modelIndex, modelIndex); + } + } + + public void initListeners() { + + var lsm = getSelectionModel(); + + lsm.addListSelectionListener((ListSelectionEvent e) -> { + var task = TaskManager.getManagerInstance().getSelectedTask(); + var id = convertRowIndexToModel(this.getSelectedRow()); + if (!lsm.getValueIsAdjusting() + && id > -1 + && id < task.getStoredCalculations().size()) { + + plotExecutor.submit(() -> { + task.switchTo(task.getStoredCalculations().get(id)); + getChart().plot(task, true); + }); + + } + + }); + + } + + @Override + public TableCellRenderer getCellRenderer(int row, int column) { + return taskTableRenderer; + } + +} diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index f7da5ca4..0801ac8f 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -7,7 +7,6 @@ import static java.awt.Color.GRAY; import static java.awt.Color.GREEN; import static java.awt.Color.black; -import static java.awt.Color.white; import static java.awt.Font.PLAIN; import static java.lang.String.format; import static java.util.Objects.requireNonNull; @@ -19,13 +18,17 @@ import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; -import java.awt.Stroke; +import java.awt.event.MouseEvent; +import java.io.Serializable; +import javax.swing.SwingUtilities; + +import javax.swing.UIManager; 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.ValueMarker; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.title.LegendTitle; @@ -36,260 +39,366 @@ import pulse.AbstractData; import pulse.HeatingCurve; +import pulse.input.ExperimentalData; 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; +import pulse.tasks.Calculation; import pulse.tasks.SearchTask; +import pulse.tasks.TaskManager; +import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.ui.components.listeners.MouseOnMarkerListener; + +public class Chart implements Serializable { + + private final ChartPanel chartPanel; + private final JFreeChart chart; + private XYPlot plot; + + private float opacity = 0.15f; + private boolean residualsShown = true; + private boolean zeroApproximationShown = false; + + private final static double TO_MILLIS = 1E3; + private final static double RANGE_THRESHOLD = 1E-1; + private double factor; + + private MovableValueMarker lowerMarker; + private MovableValueMarker upperMarker; + + public Chart() { + chart = createScatterPlot("", getString("Charting.TimeAxisLabel"), (getString("Charting.TemperatureAxisLabel")), + null, VERTICAL, true, true, false); + + plot = chart.getXYPlot(); + setRenderers(); + setBackgroundAndGrid(); + setLegendTitle(); + setFonts(); + + final TaskManager instance = TaskManager.getManagerInstance(); + + chart.removeLegend(); + chart.setBackgroundPaint(UIManager.getColor("TextPane.background")); + chartPanel = new ChartPanel(chart) { + + @Override + public void mouseDragged(MouseEvent e) { + if (lowerMarker == null || upperMarker == null) { + super.mouseDragged(e); + } + + SwingUtilities.invokeLater(() -> { + + //process dragged events + 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) { + if (range.boundLimits(false).contains(value)) { + range.setLowerBound(NumericProperties.derive(LOWER_BOUND, value)); + } + } else if (upperMarker.getState() != MovableValueMarker.State.IDLE) { + if (range.boundLimits(true).contains(value)) { + range.setUpperBound(NumericProperties.derive(UPPER_BOUND, value)); + } + } else { + super.mouseDragged(e); + } + + }); + + } + + }; + + instance.addTaskRepositoryListener((TaskRepositoryEvent 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 + data.addDataListener((DataEvent e1) -> { + //that will be triggered only when this task is selected + if (instance.getSelectedTask() == eventTask) { + //update marker values + 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 + } + }); + } //tasks that have been finihed + else if (e.getState() == TaskRepositoryEvent.State.TASK_FINISHED + && instance.getSelectedTask() == eventTask) { + //add passive data listener + plot(eventTask, false); + } + }); + + } + + public double xCoord(MouseEvent e) { + double xVirtual = e.getX(); + return plot.getDomainAxis().java2DToValue(xVirtual, + chartPanel.getScreenDataArea(), plot.getDomainAxisEdge()); + } + + private void setFonts() { + 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("TextPane.background")); + plot.setRangeGridlinesVisible(true); + plot.setRangeGridlinePaint(GRAY); + + plot.setDomainGridlinesVisible(true); + plot.setDomainGridlinePaint(GRAY); + } + + private void setLegendTitle() { + var lt = new LegendTitle(plot); + lt.setItemFont(new Font("Dialog", PLAIN, 14)); + lt.setBackgroundPaint(new Color(200, 200, 255, 100)); + lt.setFrame(new BlockBorder(black)); + lt.setPosition(RectangleEdge.RIGHT); + var ta = new XYTitleAnnotation(0.98, 0.2, lt, RectangleAnchor.RIGHT); + ta.setMaxWidth(0.58); + plot.addAnnotation(ta); + } + + private void setRenderers() { + var renderer = (XYLineAndShapeRenderer) chart.getXYPlot().getRenderer(); + renderer.setDefaultShapesFilled(false); + renderer.setUseFillPaint(false); + renderer.setSeriesPaint(0, new Color(1.0f, 0.0f, 0.0f, opacity)); + + var rendererLines = new XYLineAndShapeRenderer(); + rendererLines.setSeriesPaint(0, BLUE); + rendererLines.setSeriesStroke(0, new BasicStroke(2.0f)); + rendererLines.setSeriesShapesVisible(0, false); + + var rendererResiduals = new XYLineAndShapeRenderer(); + rendererResiduals.setSeriesPaint(0, GREEN); + rendererResiduals.setSeriesShapesVisible(0, false); + + var rendererClassic = new XYLineAndShapeRenderer(); + rendererClassic.setSeriesPaint(0, BLACK); + rendererClassic.setSeriesShapesVisible(0, false); + + var rendererOld = new XYLineAndShapeRenderer(); + rendererOld.setSeriesPaint(0, BLUE); + rendererOld.setSeriesStroke(0, new BasicStroke(2.0f, CAP_BUTT, JOIN_MITER, 2.0f, new float[]{10f}, 0)); + rendererOld.setSeriesShapesVisible(0, false); + + plot.setRenderer(0, rendererLines); + plot.setRenderer(1, rendererResiduals); + plot.setRenderer(2, rendererClassic); + plot.setRenderer(3, renderer); + + } + + private void adjustAxisLabel(double maximum) { + if (maximum < RANGE_THRESHOLD) { + factor = TO_MILLIS; + plot.getDomainAxis().setLabel("Time (ms)"); + } else { + factor = 1.0; + plot.getDomainAxis().setLabel("Time (s)"); + } + } + + public void plot(SearchTask task, boolean extendedCurve) { + requireNonNull(task); + + for (int i = 0; i < 6; i++) { + plot.setDataset(i, null); + } + + var rawData = (ExperimentalData) task.getInput(); + var segment = rawData.getRange().getSegment(); + + adjustAxisLabel(segment.getMaximum()); + + factor = segment.getMaximum() < RANGE_THRESHOLD ? TO_MILLIS : 1.0; + + var rawDataset = new XYSeriesCollection(); + + rawDataset.addSeries(series(rawData, "Raw data (" + task.getIdentifier() + ")", extendedCurve)); + plot.setDataset(3, rawDataset); + plot.getRenderer(3).setSeriesPaint(0, new Color(1.0f, 0.0f, 0.0f, opacity)); -public class Chart { - - private ChartPanel chartPanel; - private JFreeChart chart; - private XYPlot plot; - - private float opacity = 0.15f; - private boolean residualsShown = true; - private boolean zeroApproximationShown = false; - - private final static double TO_MILLIS = 1E3; - private final static double RANGE_THRESHOLD = 1E-1; - private double factor; - - public Chart() { - chart = createScatterPlot("", getString("Charting.TimeAxisLabel"), (getString("Charting.TemperatureAxisLabel")), - null, VERTICAL, true, true, false); - - plot = chart.getXYPlot(); - setRenderers(); - setBackgroundAndGrid(); - setLegendTitle(); - setFonts(); - - chart.removeLegend(); - chartPanel = new ChartPanel(chart); - } - - 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); - } - - private void setBackgroundAndGrid() { - plot.setBackgroundPaint(white); - plot.setRangeGridlinesVisible(true); - plot.setRangeGridlinePaint(GRAY); - - plot.setDomainGridlinesVisible(true); - plot.setDomainGridlinePaint(GRAY); - } - - private void setLegendTitle() { - var lt = new LegendTitle(plot); - lt.setItemFont(new Font("Dialog", PLAIN, 14)); - lt.setBackgroundPaint(new Color(200, 200, 255, 100)); - lt.setFrame(new BlockBorder(black)); - lt.setPosition(RectangleEdge.RIGHT); - var ta = new XYTitleAnnotation(0.98, 0.2, lt, RectangleAnchor.RIGHT); - ta.setMaxWidth(0.58); - plot.addAnnotation(ta); - } - - private void setRenderers() { - var renderer = (XYLineAndShapeRenderer) chart.getXYPlot().getRenderer(); - renderer.setDefaultShapesFilled(false); - renderer.setUseFillPaint(false); - renderer.setSeriesPaint(0, new Color(1.0f, 0.0f, 0.0f, opacity)); - - var rendererLines = new XYLineAndShapeRenderer(); - rendererLines.setSeriesPaint(0, BLUE); - rendererLines.setSeriesStroke(0, new BasicStroke(2.0f)); - rendererLines.setSeriesShapesVisible(0, false); - - var rendererResiduals = new XYLineAndShapeRenderer(); - rendererResiduals.setSeriesPaint(0, GREEN); - rendererResiduals.setSeriesShapesVisible(0, false); - - var rendererClassic = new XYLineAndShapeRenderer(); - rendererClassic.setSeriesPaint(0, BLACK); - rendererClassic.setSeriesShapesVisible(0, false); - - var rendererOld = new XYLineAndShapeRenderer(); - rendererOld.setSeriesPaint(0, BLUE); - rendererOld.setSeriesStroke(0, new BasicStroke(2.0f, CAP_BUTT, JOIN_MITER, 2.0f, new float[] { 10f }, 0)); - rendererOld.setSeriesShapesVisible(0, false); - - plot.setRenderer(0, renderer); - plot.setRenderer(1, rendererLines); - plot.setRenderer(2, rendererOld); - plot.setRenderer(3, rendererResiduals); - plot.setRenderer(4, rendererClassic); - } + plot.clearDomainMarkers(); - private void adjustAxisLabel(double maximum) { - if (maximum < RANGE_THRESHOLD) { - factor = TO_MILLIS; - plot.getDomainAxis().setLabel("Time (ms)"); - } else { - factor = 1.0; - plot.getDomainAxis().setLabel("Time (s)"); - } - } + lowerMarker = new MovableValueMarker(segment.getMinimum() * factor); + upperMarker = new MovableValueMarker(segment.getMaximum() * factor); + + final double margin = (lowerMarker.getValue() + upperMarker.getValue()) / 20.0; - public void plot(SearchTask task, boolean extendedCurve) { - requireNonNull(task); + //add listener to handle range adjustment + var lowerMarkerListener = new MouseOnMarkerListener(this, lowerMarker, upperMarker, margin); + var upperMarkerListener = new MouseOnMarkerListener(this, upperMarker, upperMarker, margin); - var plot = chart.getXYPlot(); + chartPanel.addChartMouseListener(lowerMarkerListener); + chartPanel.addChartMouseListener(upperMarkerListener); - for (int i = 0; i < 6; i++) - plot.setDataset(i, null); + plot.addDomainMarker(upperMarker); + plot.addDomainMarker(lowerMarker); - var rawData = task.getExperimentalCurve(); - var segment = rawData.getRange().getSegment(); + var calc = (Calculation) task.getResponse(); + var problem = calc.getProblem(); - adjustAxisLabel(segment.getMaximum()); + if (problem != null) { - factor = segment.getMaximum() < RANGE_THRESHOLD ? TO_MILLIS : 1.0; + var solution = problem.getHeatingCurve(); + var scheme = calc.getScheme(); - var rawDataset = new XYSeriesCollection(); + if (solution != null && !solution.isFull()) { + try { + calc.process(); + } catch (SolverException ex) { + System.out.println("Could not plot solution! See details in debug."); + ex.printStackTrace(); + } + } - rawDataset.addSeries(series(rawData, "Raw data (" + task.getIdentifier() + ")", extendedCurve)); - plot.setDataset(0, rawDataset); + if (solution != null && scheme != null) { - plot.clearDomainMarkers(); + var solutionDataset = new XYSeriesCollection(); + var displayedCurve = extendedCurve ? solution.extendedTo(rawData, problem.getBaseline()) : solution; - var lowerMarker = new ValueMarker(segment.getMinimum() * factor); + solutionDataset + .addSeries(series(displayedCurve, "Solution with " + scheme.getSimpleName(), extendedCurve)); + plot.setDataset(0, solutionDataset); - Stroke dashed = new BasicStroke(1.5f, CAP_BUTT, JOIN_MITER, 5.0f, new float[] { 10f }, 0.0f); - - lowerMarker.setPaint(black); - lowerMarker.setStroke(dashed); - - var upperMarker = new ValueMarker(segment.getMaximum() * factor); - upperMarker.setPaint(black); - upperMarker.setStroke(dashed); - - plot.addDomainMarker(upperMarker); - plot.addDomainMarker(lowerMarker); - - var problem = task.getProblem(); - - if (problem != null) { - - var solution = problem.getHeatingCurve(); - - if (solution != null && task.getScheme() != null && solution.actualNumPoints() > 0) { + /* + * plot residuals + */ + if (residualsShown) { + var residuals = calc.getOptimiserStatistic().getResiduals(); + if (residuals != null && residuals.size() > 0) { + var residualsDataset = new XYSeriesCollection(); + residualsDataset.addSeries(residuals(calc)); + plot.setDataset(1, residualsDataset); + } + } - var solutionDataset = new XYSeriesCollection(); - var displayedCurve = extendedCurve ? solution.extendedTo(rawData, problem.getBaseline()) : solution; + } - solutionDataset.addSeries( - series(displayedCurve, "Solution with " + task.getScheme().getSimpleName(), extendedCurve)); - plot.setDataset(1, solutionDataset); + } - /* - * plot residuals - */ + if (zeroApproximationShown) { + var p = calc.getProblem(); + var s = calc.getScheme(); - if (residualsShown) - if (task.getResidualStatistic().getResiduals() != null) { - var residualsDataset = new XYSeriesCollection(); - residualsDataset.addSeries(residuals(task)); - plot.setDataset(3, residualsDataset); - } + if (p != null && s != null) { + plotSingle(classicSolution(p, (double) (s.getTimeLimit().getValue()))); + } + } - } + } - plot.getRenderer().setSeriesPaint(0, new Color(1.0f, 0.0f, 0.0f, opacity)); + public void plotSingle(HeatingCurve curve) { + requireNonNull(curve); - } + plot = chart.getXYPlot(); - if (zeroApproximationShown) { - var p = task.getProblem(); - var s = task.getScheme(); + var classicDataset = new XYSeriesCollection(); - if (p != null && s != null) - plotSingle(classicSolution(p, (double) (s.getTimeLimit().getValue()))); - } + classicDataset.addSeries(series(curve, curve.getName(), false)); - } + plot.setDataset(2, classicDataset); + plot.getRenderer(2).setSeriesPaint(0, black); + } - public void plotSingle(AbstractData curve) { - requireNonNull(curve); + public XYSeries series(HeatingCurve curve, String title, boolean extendedCurve) { + final int realCount = curve.getBaselineCorrectedData().size(); + final double startTime = (double) ((HeatingCurve) curve).getTimeShift().getValue(); + return series(curve, title, startTime, realCount, extendedCurve); + } - var plot = chart.getXYPlot(); + public XYSeries series(ExperimentalData curve, String title, boolean extendedCurve) { + return series(curve, title, 0, curve.actualNumPoints(), extendedCurve); + } - var classicDataset = new XYSeriesCollection(); + private XYSeries series(AbstractData curve, String title, final double startTime, final int realCount, + boolean extendedCurve) { + var series = new XYSeries(title); - classicDataset.addSeries(series(curve, curve.getName(), false)); + int iStart = IndexRange.closestLeft(startTime < 0 ? startTime : 0, curve.getTimeSequence()); - plot.setDataset(4, classicDataset); - plot.getRenderer(4).setSeriesPaint(0, black); - } + for (var i = 0; i < iStart && extendedCurve; i++) { + series.add(factor * curve.timeAt(i), curve.signalAt(i)); + } - public XYSeries series(AbstractData curve, String title, boolean extendedCurve) { - var series = new XYSeries(title); + for (var i = iStart; i < realCount; i++) { + series.add(factor * curve.timeAt(i), curve.signalAt(i)); + } - final int realCount = curve.actualNumPoints(); - double startTime = 0; - if (curve instanceof HeatingCurve) - startTime = (double) ((HeatingCurve) curve).getTimeShift().getValue(); - int iStart = IndexRange.closestLeft(startTime < 0 ? startTime : 0, curve.getTimeSequence()); - - for (var i = 0; i < iStart && extendedCurve; i++) - series.add(factor * curve.timeAt(i), curve.signalAt(i)); + return series; + } - for (var i = iStart; i < realCount; i++) - series.add(factor * curve.timeAt(i), curve.signalAt(i)); + public XYSeries residuals(Calculation calc) { + var problem = calc.getProblem(); + var baseline = problem.getBaseline(); - return series; - } + var time = calc.getOptimiserStatistic().getTimeSequence(); + var residuals = calc.getOptimiserStatistic().getResiduals(); + var size = residuals.size(); - public XYSeries residuals(SearchTask task) { - var problem = task.getProblem(); - var baseline = problem.getBaseline(); - final var span = problem.getHeatingCurve().maxAdjustedSignal() - baseline.valueAt(0); - final var offset = baseline.valueAt(0) - span / 2.0; + final var span = problem.getHeatingCurve().maxAdjustedSignal() - baseline.valueAt(0); + final var offset = baseline.valueAt(0) - span / 2.0; - var series = new XYSeries(format("Residuals (offset %3.2f)", offset)); + var series = new XYSeries(format("Residuals (offset %3.2f)", offset)); - var residuals = task.getResidualStatistic().getResiduals(); - var size = residuals.size(); + for (var i = 0; i < size; i++) { + series.add(factor * time.get(i), (Number) (residuals.get(i) + offset)); + } - for (var i = 0; i < size; i++) { - series.add(factor * residuals.get(i)[0], (Number) (residuals.get(i)[1] + offset)); - } + return series; + } - return series; - } + public void setOpacity(float opacity) { + this.opacity = opacity; + } - public void setOpacity(float opacity) { - this.opacity = opacity; - } + public double getOpacity() { + return opacity; + } - public double getOpacity() { - return opacity; - } + public boolean isResidualsShown() { + return residualsShown; + } - public boolean isResidualsShown() { - return residualsShown; - } + public void setResidualsShown(boolean residualsShown) { + this.residualsShown = residualsShown; + } - public void setResidualsShown(boolean residualsShown) { - this.residualsShown = residualsShown; - } + public boolean isZeroApproximationShown() { + return zeroApproximationShown; + } - public boolean isZeroApproximationShown() { - return zeroApproximationShown; - } + public void setZeroApproximationShown(boolean zeroApproximationShown) { + this.zeroApproximationShown = zeroApproximationShown; + } - public void setZeroApproximationShown(boolean zeroApproximationShown) { - this.zeroApproximationShown = zeroApproximationShown; - } + public ChartPanel getChartPanel() { + return chartPanel; + } - public ChartPanel getChartPanel() { - return chartPanel; - } + public XYPlot getChartPlot() { + return plot; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/DataLoader.java b/src/main/java/pulse/ui/components/DataLoader.java index 3f1b7079..2baa8148 100644 --- a/src/main/java/pulse/ui/components/DataLoader.java +++ b/src/main/java/pulse/ui/components/DataLoader.java @@ -1,24 +1,25 @@ package pulse.ui.components; import static pulse.io.readers.ReaderManager.datasetReaders; +import static pulse.io.readers.ReaderManager.pulseReaders; import static pulse.io.readers.ReaderManager.read; -import java.awt.Window; import java.io.File; import java.io.IOException; 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; 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; @@ -31,171 +32,212 @@ * {@code ProgressDialog}. * */ - public class DataLoader { - private static File dir; - private static ProgressDialog progressFrame = new ProgressDialog(); - - static { - TaskManager.getManagerInstance().addTaskRepositoryListener(e -> { - if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) - progressFrame.incrementProgress(); - }); - progressFrame.setLocationRelativeTo(null); - progressFrame.setAlwaysOnTop(true); - } - - private DataLoader() { - // intentionally blank - } - - /** - * Initiates a user dialog to load experimental time-temperature profiles. - * Multiple selection is possible. When the user finalises selection, the - * {@code TaskManager} will start generating tasks using the files selected by - * the user as input. The tracker progress bar is reset and made visible. - */ - - public static void loadDataDialog() { - var files = userInput(Messages.getString("TaskControlFrame.ExtensionDescriptor"), - ReaderManager.getCurveExtensions()); - - if (files != null) { - progressFrame.trackProgress(files.size()); - TaskManager.getManagerInstance().generateTasks(files); - } - - } - - /** - * Asks the user to select a single file containing the metadata, with the - * extension given by the {@code MetaFilePopulator} class. If a valid selection - * is made and the task list is not empty, proceeds to populating each task's - * metadata object using the information contained in the selected file. If the - * task has a problem assigned to it, sets the parameters of that problem to - * match the loaded {@code Metadata}. Throughout the process, progress is - * monitored in a separate dialog with a {@code JProgressBar}. Upon finishing, - * the data range will be checked to determine if truncation is needed. - * - * @see truncateDataDialog - */ - - public static void loadMetadataDialog() { - var handler = MetaFilePopulator.getInstance(); - var file = userInputSingle(Messages.getString("TaskControlFrame.ExtensionDescriptor"), - handler.getSupportedExtension()); - - var instance = TaskManager.getManagerInstance(); - - if (instance.numberOfTasks() < 1 || file == null) - return; // invalid input received, do nothing - - progressFrame.trackProgress(instance.numberOfTasks() + 1); - - // attempt to fill metadata and problem - - for (SearchTask task : instance.getTaskList()) { - var data = task.getExperimentalCurve(); + private static File dir; + private static ProgressDialog progressFrame = new ProgressDialog(); + + static { + TaskManager.getManagerInstance().addTaskRepositoryListener(e -> { + if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { + progressFrame.incrementProgress(); + } + }); + progressFrame.setLocationRelativeTo(null); + progressFrame.setAlwaysOnTop(true); + } + + private DataLoader() { + // intentionally blank + } + + /** + * Initiates a user dialog to load experimental time-temperature profiles. + * Multiple selection is possible. When the user finalises selection, the + * {@code TaskManager} will start generating tasks using the files selected + * by the user as input. The tracker progress bar is reset and made visible. + */ + public static void loadDataDialog() { + var files = userInput(Messages.getString("TaskControlFrame.ExtensionDescriptor"), + ReaderManager.getCurveExtensions()); + + var instance = TaskManager.getManagerInstance(); + if (files != null) { + + progressFrame.trackProgress(files.size()); + instance.generateTasks(files); + } + + } + + /** + * Asks the user to select a single file containing the metadata, with the + * extension given by the {@code MetaFilePopulator} class. If a valid + * selection is made and the task list is not empty, proceeds to populating + * each task's metadata object using the information contained in the + * selected file. If the task has a problem assigned to it, sets the + * parameters of that problem to match the loaded {@code Metadata}. + * Throughout the process, progress is monitored in a separate dialog with a + * {@code JProgressBar}. Upon finishing, the data range will be checked to + * determine if truncation is needed. + * + * @see truncateDataDialog + */ + public static void loadMetadataDialog() { + var handler = MetaFilePopulator.getInstance(); + var file = userInputSingle(Messages.getString("TaskControlFrame.ExtensionDescriptor"), + handler.getSupportedExtension()); + + var instance = TaskManager.getManagerInstance(); + + if (instance.numberOfTasks() < 1 || file == null) { + return; // invalid input received, do nothing + } + progressFrame.trackProgress(instance.numberOfTasks() + 1); + + // attempt to fill metadata and problem + for (SearchTask task : instance.getTaskList()) { + var data = (ExperimentalData) task.getInput(); + + try { + handler.populate(file, data.getMetadata()); + } catch (IOException e) { + JOptionPane.showMessageDialog(progressFrame, Messages.getString("TaskControlFrame.LoadError"), + Messages.getString("TaskControlFrame.IOError"), JOptionPane.ERROR_MESSAGE); + e.printStackTrace(); + } + + var p = ((Calculation) task.getResponse()).getProblem(); + if (p != null) { + p.retrieveData(data); + } + progressFrame.incrementProgress(); + + } + + progressFrame.incrementProgress(); + + // select first of the generated task + instance.selectFirstTask(); + + } + + public static void loadPulseDialog() { + var files = userInput(Messages.getString("TaskControlFrame.ExtensionDescriptor"), + ReaderManager.getPulseExtensions()); + + if (files != null) { + + var manager = TaskManager.getManagerInstance(); + + progressFrame.trackProgress(files.size()); + + Runnable loader = () -> { + + final var pool = Executors.newCachedThreadPool(); + + files.stream().map(f -> read(pulseReaders(), f)) + .filter(pulseData -> (pulseData != null)) + .forEach(pulseData -> { + + var task = manager.getTask(pulseData.getExternalID()); + + if (task != null) { + pool.submit(() -> { + var metadata = ((ExperimentalData) task.getInput()).getMetadata(); + metadata.setPulseData(pulseData); + metadata.getPulseDescriptor() + .setSelectedDescriptor( + NumericPulse.class.getSimpleName()); + progressFrame.incrementProgress(); + } + ); + } + + }); + + }; + + Executors.newSingleThreadExecutor().submit(loader); + + } + + } + + /** + * 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 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) { + JFileChooser fileChooser = new JFileChooser(); + + fileChooser.setCurrentDirectory(directory()); + fileChooser.setMultiSelectionEnabled(true); + + String[] extArray = extensions.toArray(new String[extensions.size()]); + fileChooser.setFileFilter(new FileNameExtensionFilter(descriptor, extArray)); + + boolean approve = fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION; + dir = fileChooser.getCurrentDirectory(); + + return approve ? Arrays.asList(fileChooser.getSelectedFiles()) : null; + } + + private static File userInputSingle(String descriptor, List extensions) { + JFileChooser fileChooser = new JFileChooser(); + + fileChooser.setCurrentDirectory(directory()); + fileChooser.setMultiSelectionEnabled(false); + + String[] extArray = extensions.toArray(new String[extensions.size()]); + fileChooser.setFileFilter(new FileNameExtensionFilter(descriptor, extArray)); + + boolean approve = fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION; + dir = fileChooser.getCurrentDirectory(); + + return approve ? fileChooser.getSelectedFile() : null; + } + + private static File userInputSingle(String descriptor, String... extensions) { + return userInputSingle(descriptor, Arrays.asList(extensions)); + } + + private static File directory() { + if (dir != null) { + return dir; + } else try { - handler.populate(file, data.getMetadata()); - } catch (IOException e) { - JOptionPane.showMessageDialog(progressFrame, Messages.getString("TaskControlFrame.LoadError"), - Messages.getString("TaskControlFrame.IOError"), JOptionPane.ERROR_MESSAGE); - e.printStackTrace(); - } - - var p = task.getProblem(); - if (p != null) - p.retrieveData(data); - progressFrame.incrementProgress(); - - } - - // check if the data loaded needs truncation - if (instance.dataNeedsTruncation()) - truncateDataDialog(progressFrame); - - progressFrame.incrementProgress(); - - // select first of the generated task - instance.selectFirstTask(); - - } - - /** - * 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} - * @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(); - } - - private static void truncateDataDialog(Window frame) { - Object[] options = { "Truncate", "Do not change" }; - int answer = JOptionPane.showOptionDialog(frame, - ("The acquisition time for some experiments appears to be too long.\nIf time resolution is low, the model estimates will be biased.\n\nIt is recommended to allow PULSE to truncate this data.\n\nWould you like to proceed? "), //$NON-NLS-1$ - "Potential Problem with Data", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, - options[0]); - if (answer == 0) - TaskManager.getManagerInstance().truncateData(); - } - - private static List userInput(String descriptor, List extensions) { - JFileChooser fileChooser = new JFileChooser(); - - fileChooser.setCurrentDirectory(directory()); - fileChooser.setMultiSelectionEnabled(true); - - String[] extArray = extensions.toArray(new String[extensions.size()]); - fileChooser.setFileFilter(new FileNameExtensionFilter(descriptor, extArray)); - - boolean approve = fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION; - dir = fileChooser.getCurrentDirectory(); - - return approve ? Arrays.asList(fileChooser.getSelectedFiles()) : null; - } - - private static File userInputSingle(String descriptor, List extensions) { - JFileChooser fileChooser = new JFileChooser(); - - fileChooser.setCurrentDirectory(directory()); - fileChooser.setMultiSelectionEnabled(false); - - String[] extArray = extensions.toArray(new String[extensions.size()]); - fileChooser.setFileFilter(new FileNameExtensionFilter(descriptor, extArray)); - - boolean approve = fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION; - dir = fileChooser.getCurrentDirectory(); - - return approve ? fileChooser.getSelectedFile() : null; - } - - private static File userInputSingle(String descriptor, String... extensions) { - return userInputSingle(descriptor, Arrays.asList(extensions)); - } - - private static File directory() { - if (dir != null) - return dir; - else - try { - return new File(DataLoader.class.getProtectionDomain().getCodeSource().getLocation().toURI()); - } catch (URISyntaxException e) { - System.err.println("Cannot determine current working directory."); - e.printStackTrace(); - } - return null; - } - -} \ No newline at end of file + return new File(DataLoader.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + } catch (URISyntaxException e) { + System.err.println("Cannot determine current working directory."); + e.printStackTrace(); + } + return null; + } + +} 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 00000000..1b643f53 --- /dev/null +++ b/src/main/java/pulse/ui/components/GraphicalLogPane.java @@ -0,0 +1,100 @@ +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; +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; + 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(); + } + }); + } + + @Override + public JComponent getGUIComponent() { + return pane; + } + + @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() && log.isFinished()) { + + 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; + } + +} 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 00000000..f1c896cf --- /dev/null +++ b/src/main/java/pulse/ui/components/LogChart.java @@ -0,0 +1,222 @@ +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.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; +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.StateEntry; +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(); + public final static int HEIGHT_FACTOR = 75; + public final static int MARGIN = 10; + + 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(); + 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]; + } + + 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); + + 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()); + + 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); + + 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) { + int lower = (iteration / bufSize) * bufSize; + + var domainAxis = pl.getDomainAxis(); + 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); + } + } + } + + 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); + } + + } + + } + +} 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 6a77c151..00000000 --- a/src/main/java/pulse/ui/components/LogPane.java +++ /dev/null @@ -1,171 +0,0 @@ -package pulse.ui.components; - -import static java.lang.String.valueOf; -import static java.lang.System.err; -import static java.lang.System.setErr; -import static java.lang.System.setOut; -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.io.OutputStream; -import java.io.PrintStream; -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(); - - private final static boolean DEBUG = false; - - private PrintStream outStream, errStream; - - public LogPane() { - super(); - setContentType("text/html"); - setEditable(false); - var c = (DefaultCaret) getCaret(); - c.setUpdatePolicy(ALWAYS_UPDATE); - - OutputStream out = new OutputStream() { - @Override - public void write(final int b) throws IOException { - postError(valueOf((char) b)); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - postError(new String(b, off, len)); - } - - @Override - public void write(byte[] b) throws IOException { - write(b, 0, b.length); - } - }; - - if (!DEBUG) { - setOut(outStream = new PrintStream(out, true)); - setErr(errStream = new PrintStream(out, true)); - } - - } - - 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(); - } - } - - @Override - public void finalize() { - outStream.close(); - errStream.close(); - } - - public ExecutorService getUpdateExecutor() { - return updateExecutor; - } - - @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/MovableValueMarker.java b/src/main/java/pulse/ui/components/MovableValueMarker.java new file mode 100644 index 00000000..5dfb7408 --- /dev/null +++ b/src/main/java/pulse/ui/components/MovableValueMarker.java @@ -0,0 +1,58 @@ +/* + * 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.BasicStroke; +import static java.awt.BasicStroke.CAP_BUTT; +import static java.awt.BasicStroke.JOIN_MITER; +import static java.awt.Color.black; +import java.awt.Stroke; +import org.jfree.chart.plot.ValueMarker; + +/** + * + * @author Artem Lunev + */ +public class MovableValueMarker extends ValueMarker { + + private State state = State.IDLE; + + public final static Stroke IDLE_STROKE = new BasicStroke(1.5f, CAP_BUTT, JOIN_MITER, 5.0f, new float[]{10f}, 0.0f); + public final static Stroke SELECTED_STROKE = new BasicStroke(3.0f, CAP_BUTT, JOIN_MITER, 5.0f, new float[]{10f}, 0.0f); + + public MovableValueMarker(double value) { + super(value); + setPaint(black); + setStroke(IDLE_STROKE); + } + + public State getState() { + return state; + } + + public void setState(State state) { + if (this.state != state) { + //do only if state has changed + this.state = state; + super.setStroke(state == State.IDLE ? IDLE_STROKE : SELECTED_STROKE); + } + } + + public enum State { + IDLE, SELECTED, MOVING; + } + +} diff --git a/src/main/java/pulse/ui/components/ProblemTree.java b/src/main/java/pulse/ui/components/ProblemTree.java new file mode 100644 index 00000000..0ac58e52 --- /dev/null +++ b/src/main/java/pulse/ui/components/ProblemTree.java @@ -0,0 +1,122 @@ +package pulse.ui.components; + +import static pulse.tasks.TaskManager.getManagerInstance; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JTree; +import javax.swing.SwingUtilities; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +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; + +@SuppressWarnings("serial") +public class ProblemTree extends JTree { + + private final List selectionListeners; + + public ProblemTree(List allProblems) { + super(); + this.setCellRenderer(new ProblemCellRenderer()); + var root = new DefaultMutableTreeNode("Problem Statements"); + + for (var c : ProblemComplexity.values()) { + var currentComplexity = new DefaultMutableTreeNode(c.toString() + " Complexity"); + + allProblems.stream().filter(p -> p.getComplexity() == c).forEach(pFiltered -> { + var node = new DefaultMutableTreeNode(pFiltered); + currentComplexity.add(node); + }); + + root.add(currentComplexity); + + } + + var model = (DefaultTreeModel) this.getModel(); + model.setRoot(root); + + for (int i = 0; i < getRowCount(); i++) { + expandRow(i); + } + + this.setRootVisible(false); + + selectionListeners = new ArrayList<>(); + this.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + + addListeners(); + } + + private void addListeners() { + var instance = getManagerInstance(); + + addTreeSelectionListener(e -> { + var object = ((DefaultMutableTreeNode) e.getPath().getLastPathComponent()).getUserObject(); + if (object instanceof Problem) { + fireProblemSelection(new ProblemSelectionEvent((Problem) object, this)); + } + }); + + instance.addSelectionListener(e -> { + var current = ((Calculation) instance.getSelectedTask().getResponse()).getProblem(); + // select appropriate problem type from list + + setSelectedProblem(current); + fireProblemSelection(new ProblemSelectionEvent(current, instance)); + + }); + + } + + public void setSelectedProblem(Problem p) { + if (p == null) { + return; + } + + var model = this.getModel(); + var root = model.getRoot(); + + SwingUtilities.invokeLater(() -> { + + TreePath path = null; + + outer: + for (int i = 0, size = model.getChildCount(model.getRoot()); i < size; i++) { + var child = model.getChild(model.getRoot(), i); + + for (int j = 0, cSize = model.getChildCount(child); j < cSize; j++) { + var node = (DefaultMutableTreeNode) model.getChild(child, j); + var problem = (Problem) node.getUserObject(); + if (p.getClass().equals(problem.getClass())) { + path = new TreePath(new Object[]{root, child, node}); + break outer; + } + } + + } + + this.setSelectionPath(path); + + }); + } + + public final void addProblemSelectionListener(ProblemSelectionListener l) { + selectionListeners.add(l); + } + + private void fireProblemSelection(ProblemSelectionEvent e) { + for (var l : selectionListeners) { + l.onProblemSelected(e); + } + } + +} diff --git a/src/main/java/pulse/ui/components/PropertyHolderTable.java b/src/main/java/pulse/ui/components/PropertyHolderTable.java index de05925a..21577895 100644 --- a/src/main/java/pulse/ui/components/PropertyHolderTable.java +++ b/src/main/java/pulse/ui/components/PropertyHolderTable.java @@ -1,11 +1,9 @@ package pulse.ui.components; -import static java.awt.Font.BOLD; import static java.lang.Boolean.TRUE; import static javax.swing.SortOrder.ASCENDING; import static pulse.ui.Messages.getString; -import java.awt.Font; import java.awt.event.ItemEvent; import java.util.ArrayList; import java.util.List; @@ -37,162 +35,165 @@ @SuppressWarnings("serial") public class PropertyHolderTable extends JTable { - private PropertyHolder propertyHolder; + private PropertyHolder propertyHolder; - private final static Font font = new Font(getString("PropertyHolderTable.FontName"), BOLD, 12); - private final static int ROW_HEIGHT = 40; + private final static int ROW_HEIGHT = 40; - public PropertyHolderTable(PropertyHolder p) { - super(); - putClientProperty("terminateEditOnFocusLost", TRUE); + public PropertyHolderTable(PropertyHolder p) { + super(); + putClientProperty("terminateEditOnFocusLost", TRUE); - var model = new DefaultTableModel(dataArray(p), new String[] { getString("PropertyHolderTable.ParameterColumn"), //$NON-NLS-1$ - getString("PropertyHolderTable.ValueColumn") } //$NON-NLS-1$ - ); + var model = new DefaultTableModel(dataArray(p), new String[]{getString("PropertyHolderTable.ParameterColumn"), //$NON-NLS-1$ + getString("PropertyHolderTable.ValueColumn")} //$NON-NLS-1$ + ); - setModel(model); + setModel(model); - setAutoResizeMode(AUTO_RESIZE_ALL_COLUMNS); + setAutoResizeMode(AUTO_RESIZE_ALL_COLUMNS); - setShowGrid(false); - setFont(font); - setRowHeight(ROW_HEIGHT); + setShowGrid(false); + setRowHeight(ROW_HEIGHT); - var list = new ArrayList(); - list.add(new SortKey(0, ASCENDING)); + var list = new ArrayList(); + list.add(new SortKey(0, ASCENDING)); - setPropertyHolder(p); + setPropertyHolder(p); - addListeners(); + addListeners(); - } + } - private void addListeners() { - /* + private void addListeners() { + /* * Update properties of the PropertyHolder when table is changed by the user - */ - - getModel().addTableModelListener((TableModelEvent e) -> { - - final int row = e.getFirstRow(); - final int column = e.getColumn(); - - if ((row < 0) || (column < 0)) - return; - - var changedObject = ((TableModel) e.getSource()).getValueAt(row, column); - - if (changedObject instanceof Property) { - var changedProperty = (Property) changedObject; - propertyHolder.updateProperty(this, changedProperty); - } - - }); - - } - - private Object[][] dataArray(PropertyHolder p) { - if (p == null) - return null; - - List dataList = new ArrayList<>(); - var data = p.data().stream().map(property -> new Object[] { property.getDescriptor(true), property }) - .collect(Collectors.toList()); - dataList.addAll(data); - - if (p.ignoreSiblings()) - return dataList.toArray(new Object[data.size()][2]); - - p.subgroups().stream().filter(group -> group instanceof PropertyHolder).forEach(holder -> dataList.add( - new Object[] { ((PropertyHolder) holder).getPrefix() != null ? ((PropertyHolder) holder).getPrefix() - : holder.getDescriptor(), holder }) - - ); - - return dataList.toArray(new Object[dataList.size()][2]); - - } - - public void setPropertyHolder(PropertyHolder propertyHolder) { - this.propertyHolder = propertyHolder; - if (propertyHolder != null) { - updateTable(); - propertyHolder.addListener(event -> { - if (!(event.getSource() instanceof PropertyHolderTable)) - updateTable(); - }); - } - } - - public void updateTable() { - this.editCellAt(-1, -1); - this.clearSelection(); - - var model = ((DefaultTableModel) getModel()); - model.setDataVector(dataArray(propertyHolder), new String[] { model.getColumnName(0), model.getColumnName(1) }); - } - - @Override - public TableCellEditor getCellEditor(int row, int column) { - - var value = super.getValueAt(row, column); - - if (value == null) - super.getCellEditor(row, column); - - // do not edit labels - - if (value instanceof String) - return null; - - if (value instanceof NumericProperty) - return new NumberEditor((NumericProperty) value); - - if (value instanceof JComboBox) - return new DefaultCellEditor((JComboBox) value); - - if (value instanceof Enum) - return new DefaultCellEditor( - new JComboBox(((Enum) value).getDeclaringClass().getEnumConstants())); - - if (value instanceof InstanceDescriptor) { - var inst = new InstanceCellEditor((InstanceDescriptor) value); - return inst; - } - - if (value instanceof DiscreteSelector) { - var selector = (DiscreteSelector) value; - var combo = new JComboBox<>(selector.getAllOptions().toArray()); - combo.setSelectedItem(selector.getValue()); - combo.addItemListener(e -> { - if (e.getStateChange() == ItemEvent.SELECTED) { - selector.attemptUpdate(e.getItem()); - updateTable(); - } - }); - return new DefaultCellEditor(combo); - } - - if ((value instanceof PropertyHolder)) - return new ButtonEditor((AbstractButton) getCellRenderer(row, column).getTableCellRendererComponent(this, - value, false, false, row, column), (PropertyHolder) value); - - if (value instanceof Flag) - return new ButtonEditor((IconCheckBox) getCellRenderer(row, column).getTableCellRendererComponent(this, - value, false, false, row, column), ((Flag) value).getType()); - - return getDefaultEditor(value.getClass()); - - } - - @Override - public TableCellRenderer getCellRenderer(int row, int column) { - var value = super.getValueAt(row, column); - return value != null ? new AccessibleTableRenderer() : super.getCellRenderer(row, column); - } - - public PropertyHolder getPropertyHolder() { - return propertyHolder; - } - -} \ No newline at end of file + */ + + getModel().addTableModelListener((TableModelEvent e) -> { + + final int row = e.getFirstRow(); + final int column = e.getColumn(); + + if ((row < 0) || (column < 0)) { + return; + } + + var changedObject = ((TableModel) e.getSource()).getValueAt(row, column); + + if (changedObject instanceof Property) { + var changedProperty = (Property) changedObject; + propertyHolder.updateProperty(this, changedProperty); + } + + }); + + } + + private Object[][] dataArray(PropertyHolder p) { + if (p == null) { + return null; + } + + List dataList = new ArrayList<>(); + //ignore flags + var data = p.data().stream().filter(property -> !(property instanceof Flag)) + .map(property -> new Object[]{property.getDescriptor(true), property}) + .collect(Collectors.toList()); + dataList.addAll(data); + + if (p.ignoreSiblings()) { + return dataList.toArray(new Object[data.size()][2]); + } + + p.subgroups().stream().filter(group -> group instanceof PropertyHolder).forEach(holder -> dataList.add( + new Object[]{((PropertyHolder) holder).getPrefix() != null ? ((PropertyHolder) holder).getPrefix() + : holder.getDescriptor(), holder}) + ); + + return dataList.toArray(new Object[dataList.size()][2]); + + } + + public void setPropertyHolder(PropertyHolder propertyHolder) { + this.propertyHolder = propertyHolder; + if (propertyHolder != null) { + updateTable(); + propertyHolder.addListener(event -> { + if (!(event.getSource() instanceof PropertyHolderTable)) { + updateTable(); + } + }); + } + } + + public void updateTable() { + this.editCellAt(-1, -1); + this.clearSelection(); + + var model = ((DefaultTableModel) getModel()); + model.setDataVector(dataArray(propertyHolder), new String[]{model.getColumnName(0), model.getColumnName(1)}); + } + + @Override + public TableCellEditor getCellEditor(int row, int column) { + + var value = super.getValueAt(row, column); + + if (value == null) { + super.getCellEditor(row, column); + } + + // do not edit labels + if (value instanceof String) { + return null; + } + + if (value instanceof NumericProperty) { + return new NumberEditor((NumericProperty) value); + } + + if (value instanceof JComboBox) { + return new DefaultCellEditor((JComboBox) value); + } + + if (value instanceof InstanceDescriptor) { + return new InstanceCellEditor((InstanceDescriptor) value); + } + + if (value instanceof DiscreteSelector) { + var selector = (DiscreteSelector) value; + var combo = new JComboBox<>(selector.getAllOptions().toArray()); + combo.setSelectedItem(selector.getValue()); + combo.addItemListener(e -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + selector.attemptUpdate(e.getItem()); + updateTable(); + } + }); + return new DefaultCellEditor(combo); + } + + if ((value instanceof PropertyHolder)) { + return new ButtonEditor((AbstractButton) getCellRenderer(row, column).getTableCellRendererComponent(this, + value, false, false, row, column), (PropertyHolder) value); + } + + if (value instanceof Flag) { + return new ButtonEditor((IconCheckBox) getCellRenderer(row, column).getTableCellRendererComponent(this, + value, false, false, row, column), ((Flag) value).getType()); + } + + return super.getCellEditor(row, column); + + } + + @Override + public TableCellRenderer getCellRenderer(int row, int column) { + var value = super.getValueAt(row, column); + return value != null ? new AccessibleTableRenderer() : super.getCellRenderer(row, column); + } + + public PropertyHolder getPropertyHolder() { + return propertyHolder; + } + +} diff --git a/src/main/java/pulse/ui/components/PulseChart.java b/src/main/java/pulse/ui/components/PulseChart.java new file mode 100644 index 00000000..b3c7d06f --- /dev/null +++ b/src/main/java/pulse/ui/components/PulseChart.java @@ -0,0 +1,88 @@ +package pulse.ui.components; + +import static java.awt.Color.RED; +import static java.awt.Color.black; +import static java.awt.Font.PLAIN; +import static java.util.Objects.requireNonNull; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; + +import org.jfree.chart.annotations.XYTitleAnnotation; +import org.jfree.chart.block.BlockBorder; +import org.jfree.chart.renderer.xy.XYDifferenceRenderer; +import org.jfree.chart.title.LegendTitle; +import org.jfree.chart.ui.RectangleAnchor; +import org.jfree.chart.ui.RectangleEdge; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + +import pulse.problem.statements.Problem; +import pulse.problem.statements.Pulse; +import pulse.tasks.Calculation; + +public class PulseChart extends AuxPlotter { + + private final static double TO_MILLIS = 1E3; + + public PulseChart(String xLabel, String yLabel) { + super(xLabel, yLabel); + setRenderer(); + setLegendTitle(); + } + + private void setRenderer() { + var rendererPulse = new XYDifferenceRenderer(new Color(0.0f, 0.2f, 0.8f, 0.1f), Color.red, false); + rendererPulse.setSeriesPaint(0, RED); + rendererPulse.setSeriesStroke(0, new BasicStroke(3.0f)); + getPlot().setRenderer(rendererPulse); + } + + 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.setFrame(new BlockBorder(black)); + lt.setPosition(RectangleEdge.RIGHT); + var ta = new XYTitleAnnotation(0.5, 0.2, lt, RectangleAnchor.CENTER); + ta.setMaxWidth(0.58); + plot.addAnnotation(ta); + } + + @Override + public void plot(Calculation c) { + requireNonNull(c); + + Problem problem = c.getProblem(); + + double startTime = (double) problem.getHeatingCurve().getTimeShift().getValue(); + + var pulseDataset = new XYSeriesCollection(); + + pulseDataset.addSeries(series(problem.getPulse(), c.getScheme().getGrid().getTimeStep() / 20.0, + problem.getProperties().characteristicTime(), startTime)); + + getPlot().setDataset(0, pulseDataset); + } + + 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; + + 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)); + x += dx; + } + + return series; + } + +} diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index abd92ec7..ccf0f89d 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -1,6 +1,5 @@ package pulse.ui.components; -import static java.awt.Font.PLAIN; import static java.io.File.separator; import static javax.swing.JFileChooser.APPROVE_OPTION; import static javax.swing.JFileChooser.DIRECTORIES_ONLY; @@ -15,18 +14,24 @@ import static pulse.properties.NumericPropertyKeyword.SIGNIFICANCE; import static pulse.search.statistics.CorrelationTest.setThreshold; import static pulse.search.statistics.NormalityTest.setStatisticalSignificance; -import static pulse.search.statistics.ResidualStatistic.setSelectedOptimiserDescriptor; +import static pulse.search.statistics.OptimiserStatistic.setSelectedOptimiserDescriptor; import static pulse.tasks.TaskManager.getManagerInstance; import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_ADDED; -import static pulse.ui.Launcher.loadIcon; import static pulse.ui.components.DataLoader.loadDataDialog; import static pulse.ui.components.DataLoader.loadMetadataDialog; +import static pulse.ui.components.DataLoader.loadPulseDialog; +import static pulse.util.ImageUtils.loadIcon; import static pulse.util.Reflexive.allDescriptors; -import java.awt.Font; +import java.awt.Desktop; import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +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; @@ -36,322 +41,391 @@ 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; -import pulse.search.statistics.ResidualStatistic; +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; import pulse.ui.components.listeners.FrameVisibilityRequestListener; -import pulse.ui.frames.dialogs.AboutDialog; import pulse.ui.frames.dialogs.ExportDialog; import pulse.ui.frames.dialogs.FormattedInputDialog; import pulse.ui.frames.dialogs.ResultChangeDialog; +import pulse.util.Reflexive; @SuppressWarnings("serial") public class PulseMainMenu extends JMenuBar { - private final static int ICON_SIZE = 24; - - private static JMenuItem aboutItem; - private static JMenu fileMenu; - private static JMenuItem exitItem; - private static JMenuItem exportAllItem; - private static JMenuItem exportCurrentItem; - private static JMenuItem loadDataItem; - private static JMenuItem resultFormatItem; - private static JMenuItem searchSettingsItem; - private static JMenuItem loadMetadataItem; - private static JMenuItem modelSettingsItem; - - private static ExportDialog exportDialog = new ExportDialog(); - private static FormattedInputDialog bufferDialog = new FormattedInputDialog(def(BUFFER_SIZE)); - - private static File dir; - - private List listeners; - private List exitListeners; - - public PulseMainMenu() { - bufferDialog.setConfirmAction(() -> Buffer.setSize(derive(BUFFER_SIZE, bufferDialog.value()))); - - initComponents(); - initListeners(); - assignMenuFunctions(); - addListeners(); - - listeners = new ArrayList<>(); - exitListeners = new ArrayList<>(); - } - - private void addListeners() { - getManagerInstance().addTaskRepositoryListener(event -> { - if (event.getState() == TASK_ADDED) { - exportCurrentItem.setEnabled(true); - exportAllItem.setEnabled(true); - } - }); - } - - private void initListeners() { - exportCurrentItem.addActionListener(e -> { - var selectedTask = getManagerInstance().getSelectedTask(); - - if (selectedTask == null) { - showMessageDialog(getWindowAncestor(this), "No data to export!", "No Data to Export", WARNING_MESSAGE); - return; - } - - var fileChooser = new JFileChooser(); - fileChooser.setMultiSelectionEnabled(false); - fileChooser.setFileSelectionMode(DIRECTORIES_ONLY); - - var returnVal = fileChooser.showSaveDialog(this); - - if (returnVal == APPROVE_OPTION) { - dir = new File(fileChooser.getSelectedFile() + separator + getManagerInstance().describe()); - dir.mkdirs(); - exportCurrentTask(dir); - } - - }); - - exitItem.addActionListener(e -> notifyExit()); - - } - - private void initComponents() { - fileMenu = new JMenu("File"); - loadDataItem = new JMenuItem("Load Heating Curve(s)...", loadIcon("load.png", ICON_SIZE)); - loadMetadataItem = new JMenuItem("Load Metadata...", loadIcon("metadata.png", ICON_SIZE)); - exportCurrentItem = new JMenuItem("Export Current", loadIcon("save.png", ICON_SIZE)); - exportAllItem = new JMenuItem("Export...", loadIcon("save.png", ICON_SIZE)); - exitItem = new JMenuItem("Exit"); - var settingsMenu = new JMenu("Calculation Settings"); - modelSettingsItem = new JMenuItem("Heat Problem: Statement & Solution", - loadIcon("heat_problem.png", ICON_SIZE)); - searchSettingsItem = new JMenuItem("Parameter Estimation: Method & Settings", - loadIcon("inverse_problem.png", ICON_SIZE)); - resultFormatItem = new JMenuItem("Change Result Format...", loadIcon("result_format.png", ICON_SIZE)); - var infoMenu = new JMenu("Info"); - aboutItem = new JMenuItem("About..."); - var selectBuffer = new JMenuItem("Buffer size...", loadIcon("buffer.png", ICON_SIZE)); - selectBuffer.addActionListener(e -> bufferDialog.setVisible(true)); - - fileMenu.setMnemonic('f'); - loadDataItem.setMnemonic('h'); - loadMetadataItem.setMnemonic('m'); - exportCurrentItem.setMnemonic('c'); - exportAllItem.setMnemonic('e'); - exitItem.setMnemonic('x'); - aboutItem.setMnemonic('a'); - settingsMenu.setMnemonic('s'); - - loadMetadataItem.setEnabled(false); - exportCurrentItem.setEnabled(false); - exportAllItem.setEnabled(false); - modelSettingsItem.setEnabled(false); - searchSettingsItem.setEnabled(false); - - var menuFont = new Font("Arial", PLAIN, 18); - fileMenu.setFont(menuFont); - settingsMenu.setFont(menuFont); - infoMenu.setFont(menuFont); - - fileMenu.add(loadDataItem); - fileMenu.add(loadMetadataItem); - fileMenu.add(new JSeparator()); - fileMenu.add(exportCurrentItem); - fileMenu.add(exportAllItem); - fileMenu.add(new JSeparator()); - fileMenu.add(exitItem); - add(fileMenu); - - settingsMenu.add(modelSettingsItem); - settingsMenu.add(searchSettingsItem); - settingsMenu.add(initAnalysisSubmenu()); - settingsMenu.add(new JSeparator()); - settingsMenu.add(resultFormatItem); - settingsMenu.add(selectBuffer); - - add(settingsMenu); - - infoMenu.add(aboutItem); - add(infoMenu); - } - - private JMenu initAnalysisSubmenu() { - var analysisSubMenu = new JMenu("Statistical Analysis"); - var statisticsSubMenu = new JMenu("Normality tests"); - statisticsSubMenu.setIcon(loadIcon("normality_test.png", ICON_SIZE)); - - var statisticItems = new ButtonGroup(); - - JRadioButtonMenuItem item = null; - - for (var statisticName : allDescriptors(NormalityTest.class)) { - item = new JRadioButtonMenuItem(statisticName); - statisticItems.add(item); - statisticsSubMenu.add(item); - item.addItemListener(e -> { + private final static int ICON_SIZE = 24; + + private static JMenuItem aboutItem; + private static JMenu fileMenu; + private static JMenuItem exitItem; + private static JMenuItem exportAllItem; + private static JMenuItem exportCurrentItem; + private static JMenuItem loadDataItem; + private static JMenuItem resultFormatItem; + private static JMenuItem searchSettingsItem; + private static JMenuItem loadMetadataItem; + private static JMenuItem loadPulseItem; + private static JMenuItem modelSettingsItem; + + private static final ExportDialog exportDialog = new ExportDialog(); + private static final FormattedInputDialog bufferDialog = new FormattedInputDialog(def(BUFFER_SIZE)); + + private static File dir; + + private List listeners; + private List exitListeners; + + public PulseMainMenu() { + bufferDialog.setConfirmAction(() + -> Buffer.setSize(derive(BUFFER_SIZE, bufferDialog.value()))); + + initComponents(); + initListeners(); + assignMenuFunctions(); + reset(); + + listeners = new ArrayList<>(); + exitListeners = new ArrayList<>(); + } + + 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() { + exportCurrentItem.addActionListener(e -> { + var selectedTask = getManagerInstance().getSelectedTask(); + + if (selectedTask == null) { + showMessageDialog(getWindowAncestor(this), "No data to export!", "No Data to Export", WARNING_MESSAGE); + return; + } + + var fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileSelectionMode(DIRECTORIES_ONLY); + + var returnVal = fileChooser.showSaveDialog(this); + + if (returnVal == APPROVE_OPTION) { + dir = new File(fileChooser.getSelectedFile() + separator + getManagerInstance().describe()); + dir.mkdirs(); + exportCurrentTask(dir); + } + + }); + + exitItem.addActionListener(e -> notifyExit()); + + } + + private void initComponents() { + fileMenu = new JMenu("File"); + loadDataItem = new JMenuItem("Load Heating Curve(s)...", loadIcon("load.png", ICON_SIZE)); + loadMetadataItem = new JMenuItem("Load Metadata...", loadIcon("metadata.png", ICON_SIZE)); + loadPulseItem = new JMenuItem("Load Pulse Measurement(s)...", loadIcon("pulse.png", ICON_SIZE)); + exportCurrentItem = new JMenuItem("Export Current", loadIcon("save.png", ICON_SIZE)); + exportAllItem = new JMenuItem("Export...", loadIcon("save.png", ICON_SIZE)); + exitItem = new JMenuItem("Exit"); + var settingsMenu = new JMenu("Calculation Settings"); + modelSettingsItem = new JMenuItem("Heat Problem: Statement & Solution", + loadIcon("heat_problem.png", ICON_SIZE)); + searchSettingsItem = new JMenuItem("Parameter Estimation: Method & Settings", + loadIcon("inverse_problem.png", ICON_SIZE)); + resultFormatItem = new JMenuItem("Change Result Format...", loadIcon("result_format.png", ICON_SIZE)); + var infoMenu = new JMenu("Info"); + aboutItem = new JMenuItem("PULsE Web-site"); + var selectBuffer = new JMenuItem("Buffer size...", loadIcon("buffer.png", ICON_SIZE)); + selectBuffer.addActionListener(e -> bufferDialog.setVisible(true)); + + fileMenu.setMnemonic('f'); + loadDataItem.setMnemonic('h'); + loadMetadataItem.setMnemonic('M'); + loadPulseItem.setMnemonic('P'); + exportCurrentItem.setMnemonic('c'); + exportAllItem.setMnemonic('e'); + exitItem.setMnemonic('x'); + aboutItem.setMnemonic('a'); + settingsMenu.setMnemonic('s'); + + loadMetadataItem.setEnabled(false); + loadPulseItem.setEnabled(false); + exportCurrentItem.setEnabled(false); + exportAllItem.setEnabled(false); + modelSettingsItem.setEnabled(false); + searchSettingsItem.setEnabled(false); + + fileMenu.add(loadDataItem); + fileMenu.add(loadMetadataItem); + fileMenu.add(loadPulseItem); + fileMenu.add(new JSeparator()); + 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 | 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); + } + + }); + + fileMenu.add(exitItem); + + add(fileMenu); + + settingsMenu.add(modelSettingsItem); + settingsMenu.add(searchSettingsItem); + settingsMenu.add(initAnalysisSubmenu()); + settingsMenu.add(new JSeparator()); + settingsMenu.add(resultFormatItem); + settingsMenu.add(selectBuffer); + + add(settingsMenu); + + infoMenu.add(aboutItem); + add(infoMenu); + } + + private JMenu initAnalysisSubmenu() { + var analysisSubMenu = new JMenu("Statistical Analysis"); + var statisticsSubMenu = new JMenu("Normality tests"); + statisticsSubMenu.setIcon(loadIcon("normality_test.png", ICON_SIZE)); + + var statisticItems = new ButtonGroup(); + + for (var statisticName : allDescriptors(NormalityTest.class)) { + var item = new JRadioButtonMenuItem(statisticName); + statisticItems.add(item); + statisticsSubMenu.add(item); + item.addItemListener(e -> { + + if (((AbstractButton) e.getItem()).isSelected()) { + var text = ((AbstractButton) e.getItem()).getText(); + NormalityTest.setSelectedTestDescriptor(text); + + getManagerInstance().getTaskList().stream().forEach(t -> t.initNormalityTest()); + + } + + }); + } + + var significanceDialog = new FormattedInputDialog(def(SIGNIFICANCE)); - if (((AbstractButton) e.getItem()).isSelected()) { - var text = ((AbstractButton) e.getItem()).getText(); - NormalityTest.setSelectedTestDescriptor(text); + significanceDialog + .setConfirmAction(() -> setStatisticalSignificance(derive(SIGNIFICANCE, significanceDialog.value()))); - getManagerInstance().getTaskList().stream().forEach(t -> t.initNormalityTest()); + var sigItem = new JMenuItem("Change significance..."); + statisticsSubMenu.add(new JSeparator()); + statisticsSubMenu.add(sigItem); + sigItem.addActionListener(e -> significanceDialog.setVisible(true)); - } + statisticsSubMenu.getItem(0).setSelected(true); + analysisSubMenu.add(statisticsSubMenu); - }); - } + var optimisersSubMenu = new JMenu("Optimiser statistics"); + optimisersSubMenu.setIcon(loadIcon("optimiser.png", ICON_SIZE)); - var significanceDialog = new FormattedInputDialog(def(SIGNIFICANCE)); + var optimisersItems = new ButtonGroup(); - significanceDialog - .setConfirmAction(() -> setStatisticalSignificance(derive(SIGNIFICANCE, significanceDialog.value()))); - - var sigItem = new JMenuItem("Change significance..."); - statisticsSubMenu.add(new JSeparator()); - statisticsSubMenu.add(sigItem); - sigItem.addActionListener(e -> significanceDialog.setVisible(true)); - - statisticsSubMenu.getItem(0).setSelected(true); - analysisSubMenu.add(statisticsSubMenu); - - var optimisersSubMenu = new JMenu("Optimiser statistics"); - optimisersSubMenu.setIcon(loadIcon("optimiser.png", ICON_SIZE)); - - var optimisersItems = new ButtonGroup(); - - item = null; - - var set = allDescriptors(ResidualStatistic.class); - set.removeAll(allDescriptors(NormalityTest.class)); - - for (var statisticName : set) { - item = new JRadioButtonMenuItem(statisticName); - optimisersItems.add(item); - optimisersSubMenu.add(item); - item.addItemListener(e -> { - - if (((AbstractButton) e.getItem()).isSelected()) { - var text = ((AbstractButton) e.getItem()).getText(); - setSelectedOptimiserDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> t.initOptimiser()); - } + var set = allDescriptors(OptimiserStatistic.class); + var defaultOptimiser = new SumOfSquares(); - }); - } + for (var statisticName : set) { + var item = new JRadioButtonMenuItem(statisticName); + optimisersItems.add(item); + optimisersSubMenu.add(item); - optimisersSubMenu.getItem(0).setSelected(true); - analysisSubMenu.add(optimisersSubMenu); + if (statisticName.equalsIgnoreCase(defaultOptimiser.getDescriptor())) { + item.setSelected(true); + } - // + item.addItemListener(e -> { - var correlationsSubMenu = new JMenu("Correlation tests"); - correlationsSubMenu.setIcon(loadIcon("correlation.png", ICON_SIZE)); + if (((AbstractButton) e.getItem()).isSelected()) { + var text = ((AbstractButton) e.getItem()).getText(); + setSelectedOptimiserDescriptor(text); + getManagerInstance().getTaskList().stream().forEach(t + -> ((Calculation) t.getResponse()).initOptimiser()); + } - var corrItems = new ButtonGroup(); + }); - JRadioButtonMenuItem corrItem = null; + } - for (var corrName : allDescriptors(CorrelationTest.class)) { - corrItem = new JRadioButtonMenuItem(corrName); - corrItems.add(corrItem); - correlationsSubMenu.add(corrItem); - corrItem.addItemListener(e -> { + //for some reason it does not work without this line! + optimisersSubMenu.getItem(0).setSelected(true); - if (((AbstractButton) e.getItem()).isSelected()) { - var text = ((AbstractButton) e.getItem()).getText(); - CorrelationTest.setSelectedTestDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> t.initCorrelationTest()); - } + for (int i = 0, size = set.size(); i < size; i++) { + var item = optimisersSubMenu.getItem(i); + if (item.getText().equalsIgnoreCase(defaultOptimiser.getDescriptor())) { + item.setSelected(true); + } + } - }); - } + analysisSubMenu.add(optimisersSubMenu); - var thresholdDialog = new FormattedInputDialog(def(CORRELATION_THRESHOLD)); + // + var correlationsSubMenu = new JMenu("Correlation tests"); + correlationsSubMenu.setIcon(loadIcon("correlation.png", ICON_SIZE)); - thresholdDialog.setConfirmAction(() -> setThreshold(derive(CORRELATION_THRESHOLD, thresholdDialog.value()))); + var corrItems = new ButtonGroup(); - var thrItem = new JMenuItem("Change threshold..."); - correlationsSubMenu.add(new JSeparator()); - correlationsSubMenu.add(thrItem); - thrItem.addActionListener(e -> thresholdDialog.setVisible(true)); + JRadioButtonMenuItem corrItem = null; - correlationsSubMenu.getItem(0).setSelected(true); + var ct = CorrelationTest.init(); - analysisSubMenu.add(correlationsSubMenu); - return analysisSubMenu; - } + for (var corrName : allDescriptors(CorrelationTest.class)) { + corrItem = new JRadioButtonMenuItem(corrName); + corrItems.add(corrItem); + correlationsSubMenu.add(corrItem); - private void assignMenuFunctions() { - loadDataItem.addActionListener(e -> loadDataDialog()); - loadMetadataItem.setEnabled(false); - loadMetadataItem.addActionListener(e -> loadMetadataDialog()); + if (ct.getDescriptor().equalsIgnoreCase(corrName)) { + corrItem.setSelected(true); + } - modelSettingsItem.setEnabled(false); + corrItem.addItemListener(e -> { - modelSettingsItem.addActionListener(e -> notifyProblem()); - searchSettingsItem.addActionListener(e -> notifySearch()); + 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(); - searchSettingsItem.setEnabled(false); + if (optionalTest.isPresent()) { + CorrelationTest.getTestDescriptor() + .setSelectedDescriptor(optionalTest.get().getClass().getSimpleName()); + getManagerInstance().getTaskList().stream().forEach(t -> t.initCorrelationTest()); + } - resultFormatItem.addActionListener(e -> { - var changeDialog = new ResultChangeDialog(); - changeDialog.setLocationRelativeTo(getWindowAncestor(this)); - changeDialog.setAlwaysOnTop(true); - changeDialog.setVisible(true); - }); + } - getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if (getManagerInstance().getTaskList().size() > 0) { - loadMetadataItem.setEnabled(true); - modelSettingsItem.setEnabled(true); - searchSettingsItem.setEnabled(true); - } else { - loadMetadataItem.setEnabled(false); - modelSettingsItem.setEnabled(false); - searchSettingsItem.setEnabled(false); - } - }); + }); + } - exportAllItem.setEnabled(true); - exportAllItem.addActionListener(e -> { - exportDialog.setLocationRelativeTo(null); - exportDialog.setAlwaysOnTop(true); - exportDialog.setVisible(true); - }); + var thresholdDialog = new FormattedInputDialog(def(CORRELATION_THRESHOLD)); - aboutItem.addActionListener(e -> { - var aboutDialog = new AboutDialog(); - aboutDialog.setLocationRelativeTo(getWindowAncestor(this)); - aboutDialog.setAlwaysOnTop(true); - aboutDialog.setVisible(true); - }); - - } + thresholdDialog.setConfirmAction(() -> setThreshold(derive(CORRELATION_THRESHOLD, thresholdDialog.value()))); - public void addFrameVisibilityRequestListener(FrameVisibilityRequestListener l) { - listeners.add(l); - } + var thrItem = new JMenuItem("Change threshold..."); + correlationsSubMenu.add(new JSeparator()); + correlationsSubMenu.add(thrItem); + thrItem.addActionListener(e -> thresholdDialog.setVisible(true)); - public void addExitRequestListener(ExitRequestListener el) { - exitListeners.add(el); - } + analysisSubMenu.add(correlationsSubMenu); + return analysisSubMenu; + } - public void notifyProblem() { - listeners.stream().forEach(l -> l.onProblemStatementShowRequest()); - } + private void assignMenuFunctions() { + loadDataItem.addActionListener(e -> loadDataDialog()); + loadMetadataItem.setEnabled(false); + loadPulseItem.setEnabled(false); + loadMetadataItem.addActionListener(e -> loadMetadataDialog()); + loadPulseItem.addActionListener(e -> loadPulseDialog()); - public void notifySearch() { - listeners.stream().forEach(l -> l.onSearchSettingsShowRequest()); - } + modelSettingsItem.setEnabled(false); - public void notifyExit() { - exitListeners.stream().forEach(el -> el.onExitRequested()); - } - -} \ No newline at end of file + modelSettingsItem.addActionListener(e -> notifyProblem()); + searchSettingsItem.addActionListener(e -> notifySearch()); + + searchSettingsItem.setEnabled(false); + + resultFormatItem.addActionListener(e -> { + var changeDialog = new ResultChangeDialog(); + changeDialog.setLocationRelativeTo(getWindowAncestor(this)); + changeDialog.setAlwaysOnTop(true); + changeDialog.setVisible(true); + }); + + exportAllItem.setEnabled(true); + exportAllItem.addActionListener(e -> { + exportDialog.setLocationRelativeTo(null); + exportDialog.setAlwaysOnTop(true); + exportDialog.setVisible(true); + }); + + aboutItem.addActionListener(e -> { + try { + Desktop.getDesktop().browse(new URL("https://kotik-coder.github.io/").toURI()); + } catch (IOException | URISyntaxException e1) { + System.err.println("Unable to open URL. Details: "); + e1.printStackTrace(); + } + + }); + + } + + public void removeAllListeners() { + if (listeners != null) { + listeners.clear(); + } + if (exitListeners != null) { + exitListeners.clear(); + } + + } + + public void addFrameVisibilityRequestListener(FrameVisibilityRequestListener l) { + listeners.add(l); + } + + public void addExitRequestListener(ExitRequestListener el) { + exitListeners.add(el); + } + + public void notifyProblem() { + listeners.stream().forEach(l -> l.onProblemStatementShowRequest()); + } + + public void notifySearch() { + listeners.stream().forEach(l -> l.onSearchSettingsShowRequest()); + } + + public void notifyExit() { + exitListeners.stream().forEach(el -> el.onExitRequested()); + } + +} diff --git a/src/main/java/pulse/ui/components/RangeTextFields.java b/src/main/java/pulse/ui/components/RangeTextFields.java new file mode 100644 index 00000000..dbfcd7c7 --- /dev/null +++ b/src/main/java/pulse/ui/components/RangeTextFields.java @@ -0,0 +1,200 @@ +package pulse.ui.components; + +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; +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; +import pulse.tasks.TaskManager; +import pulse.tasks.listeners.TaskRepositoryEvent; +import pulse.tasks.listeners.TaskSelectionEvent; +import pulse.ui.components.panels.ChartToolbar; + +/** + * Two JFormattedTextFields used to display the range of the currently selected + * task. + */ +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. + */ + public RangeTextFields() { + initTextFields(); + + var instance = TaskManager.getManagerInstance(); + + //for each new task created in the repo + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { + if (e.getState() == TaskRepositoryEvent.State.TASK_ADDED) { + + var newTask = instance.getTask(e.getId()); + //when the range of the selected data is changed and the task is the selected one, + //update the textfields values + updateTextfieldsFromTask(newTask); + + } + }); + + //when a new task is selected + instance.addSelectionListener((TaskSelectionEvent e) -> { + var task = instance.getSelectedTask(); + 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); + format.setMaximumFractionDigits(1); + format.setMinimumIntegerDigits(1); + format.setMaximumIntegerDigits(6); + format.setMultiplier(1000); //ms to seconds + format.setPositiveSuffix(" ms"); + format.setGroupingUsed(false); + + /* + * A custom formatter for the time range + */ + var formatter = new NumberFormatter(format); + formatter.setAllowsInvalid(false); + 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. + * + * @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) + * @return {@code true} if the edit may proceed + */ + private static boolean isEditValid(JFormattedTextField jtf, boolean upperBound) { + Range range = ((ExperimentalData) TaskManager.getManagerInstance().getSelectedTask() + .getInput()).getRange(); + + double candidateValue = 0.0; + try { + candidateValue = ((Number) jtf.getFormatter().stringToValue(jtf.getText())).doubleValue(); + } catch (ParseException ex) { + Logger.getLogger(ChartToolbar.class.getName()).log(Level.SEVERE, null, ex); + } + + boolean b = range.boundLimits(upperBound).contains(candidateValue); + + return range.boundLimits(upperBound).contains(candidateValue); + } + + /** + * Creates a formatter and initialised the textfields, setting up rules for + * edit validation. + */ + private void initTextFields() { + var instance = TaskManager.getManagerInstance(); + + var formatter = initFormatter(); + + lowerLimitField = new JFormattedTextField(formatter) { + + @Override + public boolean isEditValid() { + return super.isEditValid() + ? RangeTextFields.isEditValid(this, false) + : false; + } + + @Override + public void commitEdit() throws ParseException { + if (isEditValid()) { + super.commitEdit(); + } + } + + }; + + upperLimitField = new JFormattedTextField(formatter) { + + @Override + public boolean isEditValid() { + return super.isEditValid() + ? RangeTextFields.isEditValid(this, true) + : false; + } + + @Override + public void commitEdit() throws ParseException { + if (isEditValid()) { + super.commitEdit(); + } + } + + }; + + var fl = new FocusListener() { + @Override + public void focusGained(FocusEvent arg0) { + arg0.getComponent().setForeground(Color.WHITE); + } + + @Override + public void focusLost(FocusEvent arg0) { + arg0.getComponent().setForeground(Color.lightGray); + } + + }; + + lowerLimitField.addFocusListener(fl); + upperLimitField.addFocusListener(fl); + + lowerLimitField.setColumns(10); + upperLimitField.setColumns(10); + + lowerLimitField.setForeground(Color.lightGray); + upperLimitField.setForeground(Color.lightGray); + + } + + private void updateTextfieldsFromTask(SearchTask newTask) { + var data = (ExperimentalData) newTask.getInput(); + //add data listeners in case when the range of the selected task is changed + data.addDataListener((DataEvent e1) -> { + if (TaskManager.getManagerInstance().getSelectedTask() == newTask) { + var segment = data.getRange().getSegment(); + lowerLimitField.setValue(segment.getMinimum()); + upperLimitField.setValue(segment.getMaximum()); + } + }); + } + + 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 new file mode 100644 index 00000000..2f311088 --- /dev/null +++ b/src/main/java/pulse/ui/components/ResidualsChart.java @@ -0,0 +1,47 @@ +package pulse.ui.components; + +import static java.util.Objects.requireNonNull; +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; + +import pulse.search.statistics.ResidualStatistic; + +public class ResidualsChart extends AuxPlotter { + + private int binCount; + + public ResidualsChart(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); + + var pulseDataset = new HistogramDataset(); + pulseDataset.setType(HistogramType.RELATIVE_FREQUENCY); + + var residuals = stat.residualsArray(); + + if (residuals.length > 0) { + pulseDataset.addSeries("H1", residuals, binCount); + } + + getPlot().setDataset(0, pulseDataset); + } + + public int getBinCount() { + return binCount; + } + + public void setBinCount(int binCount) { + this.binCount = binCount; + } + +} diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index 3a9cd4de..e2b2b93e 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -1,21 +1,14 @@ package pulse.ui.components; -import static java.awt.Font.PLAIN; -import static java.lang.Math.abs; -import static java.util.stream.Collectors.toList; -import static javax.swing.ListSelectionModel.SINGLE_INTERVAL_SELECTION; +import static javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION; import static javax.swing.SortOrder.ASCENDING; import static javax.swing.SwingConstants.TOP; import static javax.swing.SwingUtilities.invokeLater; -import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; -import static pulse.ui.Messages.getString; -import java.awt.Font; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; +import java.util.Objects; import javax.swing.JTable; import javax.swing.RowSorter; @@ -24,12 +17,11 @@ import javax.swing.table.TableRowSorter; import pulse.properties.NumericProperty; -import pulse.properties.Property; +import pulse.tasks.Calculation; +import pulse.tasks.SearchTask; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.listeners.TaskSelectionEvent; -import pulse.tasks.processing.AbstractResult; -import pulse.tasks.processing.AverageResult; import pulse.tasks.processing.Result; import pulse.tasks.processing.ResultFormat; import pulse.ui.components.controllers.NumericPropertyRenderer; @@ -39,247 +31,225 @@ @SuppressWarnings("serial") public class ResultTable extends JTable implements Descriptive { - private final static Font font = new Font(getString("ResultTable.FontName"), PLAIN, 12); + private final static int ROW_HEIGHT = 25; + private final static int RESULTS_HEADER_HEIGHT = 50; - private final static int ROW_HEIGHT = 25; - private final static int RESULTS_HEADER_HEIGHT = 30; + private NumericPropertyRenderer renderer; - private NumericPropertyRenderer renderer; + public ResultTable(ResultFormat fmt) { + super(); - public ResultTable(ResultFormat fmt) { - super(); + renderer = new NumericPropertyRenderer(); + renderer.setVerticalAlignment(TOP); - renderer = new NumericPropertyRenderer(); - renderer.setVerticalAlignment(TOP); + var model = new ResultTableModel(fmt); + setModel(model); - var model = new ResultTableModel(fmt); - setModel(model); - setRowSorter(sorter()); + model.addListener(event -> setRowSorter(sorter())); - model.addListener(event -> setRowSorter(sorter())); + this.setRowHeight(ROW_HEIGHT); + setShowHorizontalLines(false); + setFillsViewportHeight(true); - this.setRowHeight(ROW_HEIGHT); - setShowHorizontalLines(false); - setFillsViewportHeight(true); + setSelectionMode(MULTIPLE_INTERVAL_SELECTION); + setRowSelectionAllowed(true); + setColumnSelectionAllowed(false); - getTableHeader().setFont(font); + var headersSize = getPreferredSize(); + headersSize.height = RESULTS_HEADER_HEIGHT; + getTableHeader().setPreferredSize(headersSize); - setSelectionMode(SINGLE_INTERVAL_SELECTION); - setRowSelectionAllowed(false); - setColumnSelectionAllowed(true); + resetSession(); + } - var headersSize = getPreferredSize(); - headersSize.height = RESULTS_HEADER_HEIGHT; - getTableHeader().setPreferredSize(headersSize); - - /* + public void resetSession() { + ((ResultTableModel)getModel()).resetSession(); + + /* * Listen to TaskTable and select appropriate results when task selection * changes - */ - - var instance = TaskManager.getManagerInstance(); - - instance.addSelectionListener((TaskSelectionEvent e) -> { - var id = instance.getSelectedTask().getIdentifier(); - getSelectionModel().clearSelection(); - var results = ((ResultTableModel) getModel()).getResults(); - var jj = 0; - for (var r : results) { - if (!(r instanceof Result)) - continue; - var rid = r.identify(); - if (!rid.equals(id)) - continue; - jj = convertRowIndexToView(results.indexOf(r)); - - if (jj > -1) { - getSelectionModel().addSelectionInterval(jj, jj); - scrollToSelection(jj); - } - - } - }); - - /* - * Automatically add finished tasks to this result table Automatically remove - * results if corresponding task is removed - */ - - TaskManager.getManagerInstance().addTaskRepositoryListener((TaskRepositoryEvent e) -> { - switch (e.getState()) { - case TASK_FINISHED: - var t = instance.getTask(e.getId()); - var r = instance.getResult(t); - invokeLater(() -> ((ResultTableModel) getModel()).addRow(r)); - break; - case TASK_REMOVED: - case TASK_RESET: - ((ResultTableModel) getModel()).removeAll(e.getId()); - getSelectionModel().clearSelection(); - break; - default: - break; - } - }); - - } - - public void clear() { - var model = (ResultTableModel) getModel(); - model.clear(); - } - - private TableRowSorter sorter() { - var sorter = new TableRowSorter((ResultTableModel) getModel()); - var list = new ArrayList(); - Comparator numericComparator = (i1, i2) -> i1.compareTo(i2); - - for (var i = 0; i < getColumnCount(); i++) { - list.add(new RowSorter.SortKey(i, ASCENDING)); - sorter.setComparator(i, numericComparator); - } - - sorter.setSortKeys(list); - sorter.sort(); - return sorter; - } - - public double[][][] data() { - var data = new double[getColumnCount()][2][getRowCount()]; - NumericProperty property = null; - - for (var i = 0; i < data.length; i++) { - for (var j = 0; j < data[0][0].length; j++) { - property = (NumericProperty) getValueAt(j, i); - data[i][0][j] = ((Number) property.getValue()).doubleValue() - * property.getDimensionFactor().doubleValue(); - data[i][1][j] = property.getError() == null ? 0 - : property.getError().doubleValue() * property.getDimensionFactor().doubleValue(); - } - } - return data; - } - - private void scrollToSelection(int rowIndex) { - scrollRectToVisible(getCellRect(rowIndex, rowIndex, true)); - } - - @Override - public TableCellRenderer getCellRenderer(int row, int column) { - var value = getValueAt(row, column); - - if (value instanceof NumericProperty) - return renderer; - - return super.getCellRenderer(row, column); - - } - - /* - * Merges data withing a temperature interval - */ - - public void merge(double temperatureDelta) { - var model = (ResultTableModel) this.getModel(); - var temperatureIndex = model.getFormat().indexOf(TEST_TEMPERATURE); - - if (temperatureIndex < 0) - return; - - List newRows = new LinkedList<>(); - List skipList = new ArrayList<>(); - - for (var i = 0; i < this.getRowCount(); i++) { - if (skipList.contains(convertRowIndexToModel(i))) - continue; // check if value is independent (does not belong to a group) - - var val = ((Number) ((Property) this.getValueAt(i, temperatureIndex)).getValue()); - - var indices = group(val.doubleValue(), temperatureIndex, temperatureDelta); // get indices of results in table - skipList.addAll(indices); // skip those indices if they refer to the same group - - if (indices.size() < 2) - newRows.add(model.getResults().get(indices.get(0))); - else - newRows.add(new AverageResult(indices.stream().map(model.getResults()::get).collect(toList()), - model.getFormat())); - - } - - invokeLater(() -> { - model.setRowCount(0); - model.getResults().clear(); - newRows.stream().forEach(r -> model.addRow(r)); - }); - - } - - public List group(double val, int index, double precision) { + */ + var instance = TaskManager.getManagerInstance(); - List selection = new ArrayList<>(); + instance.addSelectionListener((TaskSelectionEvent e) -> { + var t = instance.getSelectedTask(); + getSelectionModel().clearSelection(); + select(t); + }); - for (var i = 0; i < getRowCount(); i++) { - - var valNumber = (Number) ((Property) getValueAt(i, index)).getValue(); - - if (abs(valNumber.doubleValue() - val) < precision) - selection.add(convertRowIndexToModel(i)); - - } - - return selection; - - } - - // Implement table header tool tips. - @Override - protected JTableHeader createDefaultTableHeader() { - return new JTableHeader(columnModel) { - @Override - public String getToolTipText(MouseEvent e) { - var index = columnModel.getColumnIndexAtX(e.getPoint().x); - var realIndex = columnModel.getColumn(index).getModelIndex(); - return ((ResultTableModel) getModel()).getTooltips().get(realIndex); - } - }; - } - - @Override - public String describe() { - return "SummaryTable"; - } - - public boolean isSelectionEmpty() { - return getSelectedRows().length < 1; - } - - public boolean hasEnoughElements(int elements) { - return getRowCount() >= elements; - } - - public void deleteSelected() { - - var rtm = (ResultTableModel) getModel(); - var selection = getSelectedRows(); - - if (selection.length < 0) - return; - - for (var i = selection.length - 1; i >= 0; i--) { - rtm.remove(rtm.getResults().get(convertRowIndexToModel(selection[i]))); - } - - } - - public void undo() { - var dtm = (ResultTableModel) getModel(); - - for (var i = dtm.getRowCount() - 1; i >= 0; i--) { - dtm.remove(dtm.getResults().get(convertRowIndexToModel(i))); - } - - var instance = TaskManager.getManagerInstance(); - instance.getTaskList().stream().map(t -> instance.getResult(t)).forEach(r -> dtm.addRow(r)); - } - -} \ No newline at end of file + /* + * Automatically add finished tasks to this result table Automatically remove + * results if corresponding task is removed + */ + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { + var t = instance.getTask(e.getId()); + + 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(cc.getResult()); + break; + case TASK_MODEL_SWITCH: + this.getSelectionModel().clearSelection(); + if (cc != null && cc.getResult() != null) { + select(cc.getResult()); + } + break; + default: + break; + } + + } + }); + + setRowSorter(sorter()); + + } + + public void clear() { + var model = (ResultTableModel) getModel(); + model.clear(); + } + + private TableRowSorter sorter() { + var model = (ResultTableModel) getModel(); + var sorter = new TableRowSorter(model); + var list = new ArrayList(); + Comparator numericComparator = (i1, i2) -> i1.compareTo(i2); + + for (var i = 0; i < getColumnCount(); i++) { + list.add(new RowSorter.SortKey(i, ASCENDING)); + sorter.setComparator(i, numericComparator); + } + + sorter.setSortKeys(list); + sorter.sort(); + return sorter; + } + + public double[][][] data() { + var data = new double[getColumnCount()][2][getRowCount()]; + NumericProperty property = null; + + for (var i = 0; i < data.length; i++) { + for (var j = 0; j < data[0][0].length; j++) { + property = (NumericProperty) getValueAt(j, i); + data[i][0][j] = ((Number) property.getValue()).doubleValue() + * property.getDimensionFactor().doubleValue() + property.getDimensionDelta().doubleValue(); + data[i][1][j] = property.getError() == null ? 0 + : property.getError().doubleValue() * property.getDimensionFactor().doubleValue(); + } + } + return data; + } + + private void scrollToSelection(int rowIndex) { + scrollRectToVisible(getCellRect(rowIndex, rowIndex, true)); + } + + @Override + public TableCellRenderer getCellRenderer(int row, int column) { + var value = getValueAt(row, column); + + if (value instanceof NumericProperty) { + return renderer; + } + + return super.getCellRenderer(row, column); + + } + + // Implement table header tool tips. + @Override + protected JTableHeader createDefaultTableHeader() { + return new JTableHeader(columnModel) { + @Override + public String getToolTipText(MouseEvent e) { + var index = columnModel.getColumnIndexAtX(e.getPoint().x); + var realIndex = columnModel.getColumn(index).getModelIndex(); + return ((ResultTableModel) getModel()).getTooltips().get(realIndex); + } + }; + } + + @Override + public String describe() { + return "Summary_" + TaskManager.getManagerInstance().describe(); + } + + public boolean isSelectionEmpty() { + return getSelectedRows().length < 1; + } + + public boolean hasEnoughElements(int elements) { + return getRowCount() >= elements; + } + + public void deleteSelected() { + + invokeLater(() -> { + var rtm = (ResultTableModel) getModel(); + var selection = getSelectedRows(); + + if (selection.length < 0) { + return; + } + + for (var i = selection.length - 1; i >= 0; i--) { + rtm.remove(rtm.getResults().get(convertRowIndexToModel(selection[i]))); + } + }); + + } + + public void select(Result r) { + var results = ((ResultTableModel) getModel()).getResults(); + int modelIndex = results.indexOf(r); + if (modelIndex > -1) { + int jj = convertRowIndexToView(modelIndex); + getSelectionModel().addSelectionInterval(jj, jj); + scrollToSelection(jj); + } + } + + public void select(SearchTask t) { + t.getStoredCalculations().stream().forEach(c -> { + if (c.getResult() != null) { + select(c.getResult()); + } + }); + } + + public void undo() { + var dtm = (ResultTableModel) getModel(); + + for (var i = dtm.getRowCount() - 1; i >= 0; i--) { + dtm.remove(dtm.getResults().get(convertRowIndexToModel(i))); + } + + var instance = TaskManager.getManagerInstance(); + instance.getTaskList().stream().map(t -> t.getStoredCalculations()).flatMap(list -> list.stream()) + .filter(Objects::nonNull) + .forEach(c -> dtm.addRow(c.getResult())); + } + +} diff --git a/src/main/java/pulse/ui/components/TaskBox.java b/src/main/java/pulse/ui/components/TaskBox.java index 253844a7..13b84459 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; @@ -17,42 +16,39 @@ @SuppressWarnings("serial") public class TaskBox extends JComboBox { - private final static int FONT_SIZE = 12; + public TaskBox() { + super(); - public TaskBox() { - super(); + init(); + this.setModel(new TaskBoxModel()); - init(); - this.setModel(new TaskBoxModel()); + var instance = TaskManager.getManagerInstance(); - var instance = TaskManager.getManagerInstance(); - - addItemListener((ItemEvent event) -> { - if (event.getStateChange() == SELECTED) { - var id = ((SearchTask) this.getModel().getSelectedItem()).getIdentifier(); - /* + addItemListener((ItemEvent event) -> { + if (event.getStateChange() == SELECTED) { + var id = ((SearchTask) this.getModel().getSelectedItem()).getIdentifier(); + /* * if task already selected, just ignore this event and return - */ - if (instance.getSelectedTask() != instance.getTask(id)) { - instance.selectTask(id, this); - } - - } - }); - - instance.addSelectionListener((TaskSelectionEvent e) -> { - // simply ignore if source of event is taskBox - if (e.getSource() != this) - getModel().setSelectedItem(instance.getSelectedTask()); - }); - } - - public void init() { - setMaximumSize(new Dimension(32767, 24)); - setFont(getFont().deriveFont(FONT_SIZE)); - setMinimumSize(new Dimension(250, 20)); - setToolTipText(getString("TaskBox.DefaultText")); //$NON-NLS-1$ - setBackground(WHITE); - } - -} \ No newline at end of file + */ + if (instance.getSelectedTask() != instance.getTask(id)) { + instance.selectTask(id, this); + } + + } + }); + + instance.addSelectionListener((TaskSelectionEvent e) -> { + // simply ignore if source of event is taskBox + if (e.getSource() != this) { + getModel().setSelectedItem(instance.getSelectedTask()); + } + }); + } + + public final void init() { + setMaximumSize(new Dimension(32767, 24)); + setMinimumSize(new Dimension(250, 20)); + setToolTipText(getString("TaskBox.DefaultText")); //$NON-NLS-1$ + } + +} diff --git a/src/main/java/pulse/ui/components/TaskPopupMenu.java b/src/main/java/pulse/ui/components/TaskPopupMenu.java index 83a4095b..621210cb 100644 --- a/src/main/java/pulse/ui/components/TaskPopupMenu.java +++ b/src/main/java/pulse/ui/components/TaskPopupMenu.java @@ -1,6 +1,5 @@ package pulse.ui.components; -import static java.awt.Font.PLAIN; import static java.lang.System.err; import static java.lang.System.lineSeparator; import static javax.swing.JOptionPane.ERROR_MESSAGE; @@ -10,27 +9,31 @@ import static javax.swing.JOptionPane.showConfirmDialog; import static javax.swing.JOptionPane.showMessageDialog; import static javax.swing.SwingUtilities.getWindowAncestor; +import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_BROWSING_REQUEST; import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_FINISHED; import static pulse.tasks.logs.Details.MISSING_HEATING_CURVE; import static pulse.tasks.logs.Details.NONE; import static pulse.tasks.logs.Status.DONE; import static pulse.tasks.logs.Status.READY; import static pulse.tasks.processing.ResultFormat.getInstance; -import static pulse.ui.Launcher.loadIcon; import static pulse.ui.Messages.getString; import static pulse.ui.frames.MainGraphFrame.getChart; +import static pulse.util.ImageUtils.loadIcon; import java.awt.Component; -import java.awt.Font; import java.awt.event.ActionEvent; import javax.swing.ImageIcon; 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; import pulse.problem.schemes.solvers.SolverException; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; import pulse.tasks.processing.Result; @@ -38,163 +41,196 @@ @SuppressWarnings("serial") public class TaskPopupMenu extends JPopupMenu { - private final static Font f = new Font(getString("TaskTable.FontName"), PLAIN, 16); //$NON-NLS-1$ - - private final static int ICON_SIZE = 24; - - private static ImageIcon ICON_GRAPH = loadIcon("graph.png", ICON_SIZE); - private static ImageIcon ICON_METADATA = loadIcon("metadata.png", ICON_SIZE); - private static ImageIcon ICON_MISSING = loadIcon("missing.png", ICON_SIZE); - private static ImageIcon ICON_RUN = loadIcon("execute_single.png", ICON_SIZE); - private static ImageIcon ICON_RESET = loadIcon("reset.png", ICON_SIZE); - private static ImageIcon ICON_RESULT = loadIcon("result.png", ICON_SIZE); - - public TaskPopupMenu() { - var referenceWindow = getWindowAncestor(this); - - var itemChart = new JMenuItem(getString("TaskTablePopupMenu.ShowHeatingCurve"), ICON_GRAPH); //$NON-NLS-1$ - itemChart.addActionListener(e -> plot(false)); - itemChart.setFont(f); - - var itemExtendedChart = new JMenuItem(getString("TaskTablePopupMenu.ShowExtendedHeatingCurve"), //$NON-NLS-1$ - ICON_GRAPH); - itemExtendedChart.addActionListener(e -> plot(true)); - itemExtendedChart.setFont(f); - - var instance = TaskManager.getManagerInstance(); - - var itemShowMeta = new JMenuItem("Show metadata", ICON_METADATA); - itemShowMeta.addActionListener((ActionEvent e) -> { - var t = instance.getSelectedTask(); - if (t == null) { - showMessageDialog(getWindowAncestor((Component) e.getSource()), - getString("TaskTablePopupMenu.EmptySelection2"), //$NON-NLS-1$ - getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ - } else - showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.getExperimentalCurve().getMetadata().toString(), "Metadata", PLAIN_MESSAGE); - }); - - itemShowMeta.setFont(f); - - var itemShowStatus = new JMenuItem("What is missing?", ICON_MISSING); - - instance.addSelectionListener(event -> { - var details = instance.getSelectedTask().checkProblems(false).getDetails(); - if ((details == null) || (details == NONE)) - itemShowStatus.setEnabled(false); - else - itemShowStatus.setEnabled(true); - }); - - itemShowStatus.addActionListener((ActionEvent e) -> { - var t = instance.getSelectedTask(); - if (t != null) { - var d = t.getStatus().getDetails(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), - "This is due to " + d.toString() + "", "Problems with " + t, INFORMATION_MESSAGE); - } - }); - - itemShowStatus.setFont(f); - - var itemExecute = new JMenuItem(getString("TaskTablePopupMenu.Execute"), ICON_RUN); //$NON-NLS-1$ - itemExecute.addActionListener((ActionEvent e) -> { - var t = instance.getSelectedTask(); - if (t == null) { - showMessageDialog(getWindowAncestor((Component) e.getSource()), - getString("TaskTablePopupMenu.EmptySelection"), //$NON-NLS-1$ - getString("TaskTablePopupMenu.ErrorTitle"), ERROR_MESSAGE); //$NON-NLS-1$ - } else if (t.checkProblems() == DONE) { - var dialogButton = YES_NO_OPTION; - var dialogResult = showConfirmDialog(referenceWindow, - getString("TaskTablePopupMenu.TaskCompletedWarning") + lineSeparator() - + getString("TaskTablePopupMenu.AskToDelete"), - getString("TaskTablePopupMenu.DeleteTitle"), dialogButton); - if (dialogResult == 0) { - instance.removeResult(t); - // t.storeCurrentSolution(); - instance.execute(instance.getSelectedTask()); - } - } else if (t.checkProblems() != READY) { - showMessageDialog(getWindowAncestor((Component) e.getSource()), - t.toString() + " is " + t.getStatus().getMessage(), //$NON-NLS-1$ - getString("TaskTablePopupMenu.TaskNotReady"), //$NON-NLS-1$ - ERROR_MESSAGE); - } else - instance.execute(instance.getSelectedTask()); - }); - - itemExecute.setFont(f); - - var itemReset = new JMenuItem(getString("TaskTablePopupMenu.Reset"), ICON_RESET); - itemReset.setFont(f); - - itemReset.addActionListener((ActionEvent arg0) -> instance.getSelectedTask().clear()); - - var itemGenerateResult = new JMenuItem(getString("TaskTablePopupMenu.GenerateResult"), ICON_RESULT); - itemGenerateResult.setFont(f); - - itemGenerateResult.addActionListener((ActionEvent arg0) -> { - var t = instance.getSelectedTask(); - if (t == null) - return; - if (t.getProblem() != null) { - var r = new Result(t, getInstance()); - instance.useResult(t, r); - var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); - instance.notifyListeners(e); - } - }); - - add(itemShowMeta); - add(itemShowStatus); - add(new JSeparator()); - add(itemChart); - add(itemExtendedChart); - add(new JSeparator()); - add(itemReset); - add(itemGenerateResult); - add(new JSeparator()); - add(itemExecute); - - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void plot(boolean extended) { - var t = TaskManager.getManagerInstance().getSelectedTask(); - - if (t == null) { - showMessageDialog(getWindowAncestor(this), getString("TaskTablePopupMenu.EmptySelection2"), //$NON-NLS-1$ - getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ - } else { - - var statusDetails = t.getStatus().getDetails(); - - if (statusDetails == MISSING_HEATING_CURVE) { - - showMessageDialog(getWindowAncestor(this), getString("TaskTablePopupMenu.12"), //$NON-NLS-1$ - getString("TaskTablePopupMenu.13"), //$NON-NLS-1$ - ERROR_MESSAGE); - - } else { - - var scheme = (Solver) t.getScheme(); - if (scheme != null) { - try { - scheme.solve(t.getProblem()); - } catch (SolverException e) { - err.println("Solver error for " + t + "Details: "); - e.printStackTrace(); - } - } - - getChart().plot(t, extended); - - } - - } - - } - -} \ No newline at end of file + private JMenuItem itemViewStored; + + private final static int ICON_SIZE = 24; + + private static ImageIcon ICON_GRAPH = loadIcon("graph.png", ICON_SIZE); + private static ImageIcon ICON_METADATA = loadIcon("metadata.png", ICON_SIZE); + private static ImageIcon ICON_MISSING = loadIcon("missing.png", ICON_SIZE); + private static ImageIcon ICON_RUN = loadIcon("execute_single.png", ICON_SIZE); + private static ImageIcon ICON_RESET = loadIcon("reset.png", ICON_SIZE); + private static ImageIcon ICON_RESULT = loadIcon("result.png", ICON_SIZE); + private static ImageIcon ICON_STORED = loadIcon("stored.png", ICON_SIZE); + + public TaskPopupMenu() { + var referenceWindow = getWindowAncestor(this); + + var itemChart = new JMenuItem(getString("TaskTablePopupMenu.ShowHeatingCurve"), ICON_GRAPH); //$NON-NLS-1$ + itemChart.addActionListener(e -> plot(false)); + + var itemExtendedChart = new JMenuItem(getString("TaskTablePopupMenu.ShowExtendedHeatingCurve"), //$NON-NLS-1$ + ICON_GRAPH); + itemExtendedChart.addActionListener(e -> plot(true)); + + 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()), + 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()), + input.getMetadata().toString(), "Metadata", PLAIN_MESSAGE); + } + }); + + var itemShowStatus = new JMenuItem("What is missing?", ICON_MISSING); + + itemShowStatus.addActionListener((ActionEvent e) -> { + var instance = TaskManager.getManagerInstance(); + var t = instance.getSelectedTask(); + if (t != null) { + var d = t.getStatus().getDetails(); + showMessageDialog(getWindowAncestor((Component) e.getSource()), + "This is due to " + d.toString() + "", "Problems with " + t, INFORMATION_MESSAGE); + } + }); + + 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()), + getString("TaskTablePopupMenu.EmptySelection"), //$NON-NLS-1$ + getString("TaskTablePopupMenu.ErrorTitle"), ERROR_MESSAGE); //$NON-NLS-1$ + } else { + t.checkProblems(); + var status = t.getStatus(); + + if (status == DONE) { + var dialogButton = YES_NO_OPTION; + var dialogResult = showConfirmDialog(referenceWindow, + getString("TaskTablePopupMenu.TaskCompletedWarning") + lineSeparator() + + getString("TaskTablePopupMenu.AskToDelete"), + getString("TaskTablePopupMenu.DeleteTitle"), dialogButton); + if (dialogResult == 0) { + // instance.removeResult(t); + instance.getSelectedTask().setStatus(READY); + instance.execute(instance.getSelectedTask()); + } + } else if (status != READY) { + showMessageDialog(getWindowAncestor((Component) e.getSource()), + t.toString() + " is " + t.getStatus().getMessage(), //$NON-NLS-1$ + getString("TaskTablePopupMenu.TaskNotReady"), //$NON-NLS-1$ + ERROR_MESSAGE); + } else { + instance.execute(instance.getSelectedTask()); + } + } + + }); + + var itemReset = new JMenuItem(getString("TaskTablePopupMenu.Reset"), ICON_RESET); + + 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; + } + var current = (Calculation) t.getResponse(); + if (current != null) { + var r = new Result(t, getInstance()); + current.setResult(r); + var e = new TaskRepositoryEvent(TASK_FINISHED, t.getIdentifier()); + instance.notifyListeners(e); + } + }); + + itemViewStored = new JMenuItem(getString("TaskTablePopupMenu.ViewStored"), ICON_STORED); + + itemViewStored.setEnabled(false); + + 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); + add(new JSeparator()); + add(itemChart); + add(itemExtendedChart); + add(new JSeparator()); + add(itemReset); + add(itemGenerateResult); + add(itemViewStored); + add(new JSeparator()); + add(itemExecute); + + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public void plot(boolean extended) { + var t = TaskManager.getManagerInstance().getSelectedTask(); + + if (t == null) { + showMessageDialog(getWindowAncestor(this), getString("TaskTablePopupMenu.EmptySelection2"), //$NON-NLS-1$ + getString("TaskTablePopupMenu.11"), ERROR_MESSAGE); //$NON-NLS-1$ + } else { + + var calc = (Calculation) t.getResponse(); + var statusDetails = t.getStatus().getDetails(); + + if (statusDetails == MISSING_HEATING_CURVE) { + + showMessageDialog(getWindowAncestor(this), getString("TaskTablePopupMenu.12"), //$NON-NLS-1$ + getString("TaskTablePopupMenu.13"), //$NON-NLS-1$ + ERROR_MESSAGE); + + } else { + + var scheme = (Solver) calc.getScheme(); + if (scheme != null) { + try { + scheme.solve(calc.getProblem()); + } catch (SolverException e) { + err.println("Solver error for " + t + "Details: "); + e.printStackTrace(); + } + } + + getChart().plot(t, extended); + + } + + } + + } + + public JMenuItem getItemViewStored() { + return itemViewStored; + } + +} diff --git a/src/main/java/pulse/ui/components/TaskTable.java b/src/main/java/pulse/ui/components/TaskTable.java index a48e319d..e69ba896 100644 --- a/src/main/java/pulse/ui/components/TaskTable.java +++ b/src/main/java/pulse/ui/components/TaskTable.java @@ -17,6 +17,7 @@ import javax.swing.JTable; import javax.swing.RowSorter; +import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.table.DefaultTableModel; import javax.swing.table.JTableHeader; @@ -35,150 +36,160 @@ @SuppressWarnings("serial") public class TaskTable extends JTable { - private final static int ROW_HEIGHT = 35; - private final static int HEADER_HEIGHT = 30; + private final static int ROW_HEIGHT = 35; + private final static int HEADER_HEIGHT = 30; - private TaskTableRenderer taskTableRenderer; - private TaskPopupMenu menu; + private TaskTableRenderer taskTableRenderer; + private TaskPopupMenu menu; - private Comparator numericComparator = (i1, i2) -> i1.compareTo(i2); - private Comparator statusComparator = (s1, s2) -> s1.compareTo(s2); + private Comparator numericComparator = (i1, i2) -> i1.compareTo(i2); + private Comparator statusComparator = (s1, s2) -> s1.compareTo(s2); - private final static int FONT_SIZE = 14; + public TaskTable() { + super(); + setDefaultEditor(Object.class, null); + taskTableRenderer = new TaskTableRenderer(); + this.setRowSelectionAllowed(true); + setRowHeight(ROW_HEIGHT); - public TaskTable() { - super(); - setDefaultEditor(Object.class, null); - taskTableRenderer = new TaskTableRenderer(); - this.setRowSelectionAllowed(true); - setRowHeight(ROW_HEIGHT); + setFillsViewportHeight(true); + setSelectionMode(SINGLE_INTERVAL_SELECTION); + setShowHorizontalLines(false); - setFillsViewportHeight(true); - setSelectionMode(SINGLE_INTERVAL_SELECTION); - setShowHorizontalLines(false); + var th = new TableHeader(getColumnModel()); - var model = new TaskTableModel(); - setModel(model); + setTableHeader(th); + getTableHeader().setPreferredSize(new Dimension(50, HEADER_HEIGHT)); + setAutoCreateRowSorter(true); + initListeners(); + menu = new TaskPopupMenu(); - var th = new TableHeader(getColumnModel()); + } - setTableHeader(th); + public void initListeners() { - var font = getTableHeader().getFont().deriveFont(FONT_SIZE); - getTableHeader().setFont(font); - 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(); - - } - - public void initListeners() { - - var instance = TaskManager.getManagerInstance(); - - /* + /* * mouse listener - */ + */ + addMouseListener(new MouseAdapter() { - addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { - @Override - public void mouseClicked(MouseEvent e) { + var instance = TaskManager.getManagerInstance(); - if (rowAtPoint(e.getPoint()) >= 0 && rowAtPoint(e.getPoint()) == getSelectedRow() && isRightMouseButton(e)) - menu.show(e.getComponent(), e.getX(), e.getY()); + if (rowAtPoint(e.getPoint()) >= 0 && rowAtPoint(e.getPoint()) == getSelectedRow() && isRightMouseButton(e)) { + var task = instance.getSelectedTask(); + menu.getItemViewStored().setEnabled(task.getStoredCalculations().size() > 0); + menu.show(e.getComponent(), e.getX(), e.getY()); + } - } + } - }); + }); - /* + /* * selection listener - */ - - var lsm = getSelectionModel(); - var reference = this; - - lsm.addListSelectionListener((ListSelectionEvent e) -> { - if (!lsm.getValueIsAdjusting() && !lsm.isSelectionEmpty()) { - var id = (Identifier) getValueAt(lsm.getMinSelectionIndex(), 0); - instance.selectTask(id, reference); - } - }); - - instance.addSelectionListener((TaskSelectionEvent e) -> { - // simply ignore call if event is triggered by taskTable - if (e.getSource() instanceof TaskTable) - return; - var id = instance.getSelectedTask().getIdentifier(); - Identifier idFromTable = null; - int i = 0; - - for (i = 0; i < getRowCount() && !id.equals(idFromTable); i++) - idFromTable = (Identifier) getValueAt(i, 0); - - if(i < getRowCount()) - setRowSelectionInterval(i, i); - clearSelection(); - }); - - } - - @Override - public TableCellRenderer getCellRenderer(int row, int column) { - return taskTableRenderer; - } - - public void removeSelectedRows() { - var rows = getSelectedRows(); - Identifier id; - - var instance = TaskManager.getManagerInstance(); - - for (var i = rows.length - 1; i >= 0; i--) { - id = (Identifier) getValueAt(rows[i], 0); - instance.removeTask(instance.getTask(id)); - } - - clearSelection(); - } - - private class TableHeader extends JTableHeader { - - private String[] tooltips; - - public TableHeader(TableColumnModel columnModel) { - super(columnModel);// do everything a normal JTableHeader does - tooltips = new String[] { def(IDENTIFIER).getDescriptor(true), - def(TEST_TEMPERATURE).getDescriptor(true), def(OPTIMISER_STATISTIC).getDescriptor(true), - def(TEST_STATISTIC).getDescriptor(true), ("Task status")}; - } - - @Override - public String getToolTipText(MouseEvent e) { - var p = e.getPoint(); - var index = columnModel.getColumnIndexAtX(p.x); - var realIndex = columnModel.getColumn(index).getModelIndex(); - return this.tooltips[realIndex]; - } - - } - -} \ No newline at end of file + */ + var lsm = getSelectionModel(); + 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() == 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); + } + + if (i < getRowCount()) { + setRowSelectionInterval(i, i); + } + }); + } + + @Override + public TableCellRenderer getCellRenderer(int row, int column) { + return taskTableRenderer; + } + + public void removeSelectedRows() { + SwingUtilities.invokeLater(() -> { + var rows = getSelectedRows(); + Identifier id; + + var instance = TaskManager.getManagerInstance(); + + for (var i = rows.length - 1; i >= 0; i--) { + id = (Identifier) getValueAt(rows[i], 0); + instance.removeTask(instance.getTask(id)); + } + + clearSelection(); + }); + } + + private class TableHeader extends JTableHeader { + + private String[] tooltips; + + public TableHeader(TableColumnModel columnModel) { + super(columnModel);// do everything a normal JTableHeader does + tooltips = new String[]{def(IDENTIFIER).getDescriptor(true), + def(TEST_TEMPERATURE).getDescriptor(true), def(OPTIMISER_STATISTIC).getDescriptor(true), + def(TEST_STATISTIC).getDescriptor(true), ("Task status")}; + } + + @Override + public String getToolTipText(MouseEvent e) { + var p = e.getPoint(); + var index = columnModel.getColumnIndexAtX(p.x); + var realIndex = columnModel.getColumn(index).getModelIndex(); + return this.tooltips[realIndex]; + } + + } + +} 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 00000000..de791299 --- /dev/null +++ b/src/main/java/pulse/ui/components/TextLogPane.java @@ -0,0 +1,84 @@ +package pulse.ui.components; + +import pulse.tasks.logs.AbstractLogger; +import static java.lang.System.err; +import static javax.swing.text.DefaultCaret.ALWAYS_UPDATE; +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.logs.Log; +import pulse.tasks.logs.LogEntry; + +@SuppressWarnings("serial") +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 + 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 clear() { + try { + editor.getDocument().remove(0, editor.getDocument().getLength()); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + + @Override + public JComponent getGUIComponent() { + return pane; + } + + @Override + public boolean isEmpty() { + return editor.getDocument().getLength() < 1; + } + +} diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 4883968e..8ef13f4b 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -7,6 +7,7 @@ import static pulse.ui.Messages.getString; import static pulse.ui.components.buttons.ExecutionButton.ExecutionState.EXECUTE; import static pulse.ui.components.buttons.ExecutionButton.ExecutionState.STOP; +import static pulse.util.ImageUtils.loadIcon; import java.awt.Component; import java.awt.event.ActionEvent; @@ -16,101 +17,108 @@ import pulse.tasks.TaskManager; import pulse.tasks.listeners.TaskRepositoryEvent; -import pulse.ui.Launcher; @SuppressWarnings("serial") public class ExecutionButton extends JButton { - private ExecutionState state = EXECUTE; - - public ExecutionButton() { - super(); - setIcon(state.getIcon()); - setToolTipText(state.getMessage()); - - var instance = TaskManager.getManagerInstance(); - - this.addActionListener((ActionEvent e) -> { - /* - * STOP PRESSED? - */ - if (state == STOP) { - instance.cancelAllTasks(); - return; - } - /* - * EXECUTE PRESSED? - */ - if (instance.getTaskList().isEmpty()) { - showMessageDialog(getWindowAncestor((Component) e.getSource()), - getString("TaskControlFrame.PleaseLoadData"), //$NON-NLS-1$ - "No Tasks", //$NON-NLS-1$ - ERROR_MESSAGE); - return; - } - var problematicTask = instance.getTaskList().stream() - .filter((t) -> t.checkProblems() == INCOMPLETE).findFirst(); - if (problematicTask.isPresent()) { - var t = problematicTask.get(); - showMessageDialog(getWindowAncestor((Component) e.getSource()), t + " is " + t.getStatus().getMessage(), - "Problems found", ERROR_MESSAGE); - } else { - instance.executeAll(); - } - }); - - instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { - switch (e.getState()) { - case TASK_SUBMITTED: - setExecutionState(STOP); - break; - case TASK_FINISHED: - if (instance.isTaskQueueEmpty()) { - setExecutionState(EXECUTE); - } else { - setExecutionState(STOP); - } - break; - case SHUTDOWN: - setExecutionState(EXECUTE); - break; - default: - return; - } - }); - - } - - public void setExecutionState(ExecutionState state) { - this.state = state; - this.setToolTipText(state.getMessage()); - this.setIcon(state.getIcon()); - } - - public ExecutionState getExecutionState() { - return state; - } - - public enum ExecutionState { - EXECUTE("Execute All Tasks", Launcher.loadIcon("execute.png", 24)), - STOP("Terminate All Running Tasks", Launcher.loadIcon("stop.png", 24)); - - private String message; - private ImageIcon icon; - - private ExecutionState(String message, ImageIcon icon) { - this.icon = icon; - this.message = message; - } - - public ImageIcon getIcon() { - return icon; - } - - public String getMessage() { - return message; - } - - } - -} \ No newline at end of file + private ExecutionState state = EXECUTE; + + public ExecutionButton() { + super(); + setIcon(state.getIcon()); + setToolTipText(state.getMessage()); + + this.addActionListener((ActionEvent e) -> { + var instance = TaskManager.getManagerInstance(); + + /* + * STOP PRESSED? + */ + if (state == STOP) { + instance.cancelAllTasks(); + return; + } + /* + * EXECUTE PRESSED? + */ + if (instance.getTaskList().isEmpty()) { + showMessageDialog(getWindowAncestor((Component) e.getSource()), + getString("TaskControlFrame.PleaseLoadData"), //$NON-NLS-1$ + "No Tasks", //$NON-NLS-1$ + ERROR_MESSAGE); + return; + } + var problematicTask = instance.getTaskList().stream().filter(t -> { + t.checkProblems(); + return t.getStatus() == INCOMPLETE; + }).findFirst(); + if (problematicTask.isPresent()) { + var t = problematicTask.get(); + showMessageDialog(getWindowAncestor((Component) e.getSource()), + t + " is " + t.getStatus().getMessage(), "Problems found", + ERROR_MESSAGE); + } else { + instance.executeAll(); + } + }); + + resetSession(); + + } + + public void resetSession() { + var instance = TaskManager.getManagerInstance(); + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { + switch (e.getState()) { + case TASK_SUBMITTED: + setExecutionState(STOP); + break; + case TASK_FINISHED: + if (instance.isTaskQueueEmpty()) { + setExecutionState(EXECUTE); + } else { + setExecutionState(STOP); + } + break; + case SHUTDOWN: + setExecutionState(EXECUTE); + break; + default: + return; + } + }); + } + + public void setExecutionState(ExecutionState state) { + this.state = state; + this.setToolTipText(state.getMessage()); + this.setIcon(state.getIcon()); + } + + public ExecutionState getExecutionState() { + return state; + } + + public enum ExecutionState { + EXECUTE("Execute All Tasks", loadIcon("execute.png", 24)), + STOP("Terminate All Running Tasks", loadIcon("stop.png", 24)); + + private String message; + private ImageIcon icon; + + private ExecutionState(String message, ImageIcon icon) { + this.icon = icon; + this.message = message; + } + + public ImageIcon getIcon() { + return icon; + } + + public String getMessage() { + return message; + } + + } + +} diff --git a/src/main/java/pulse/ui/components/buttons/IconCheckBox.java b/src/main/java/pulse/ui/components/buttons/IconCheckBox.java index 6470e26e..845754d9 100644 --- a/src/main/java/pulse/ui/components/buttons/IconCheckBox.java +++ b/src/main/java/pulse/ui/components/buttons/IconCheckBox.java @@ -1,6 +1,6 @@ package pulse.ui.components.buttons; -import static pulse.ui.Launcher.loadIcon; +import static pulse.util.ImageUtils.loadIcon; import javax.swing.ImageIcon; import javax.swing.JCheckBox; @@ -8,31 +8,31 @@ @SuppressWarnings("serial") public class IconCheckBox extends JCheckBox { - /* + /* * Checkbox icons for the inner button editor class - */ - - private final static int ICON_SIZE = 20; - private final static ImageIcon ICON_ENABLED = loadIcon("checked.png", ICON_SIZE); - private final static ImageIcon ICON_DISABLED = loadIcon("unchecked.png", ICON_SIZE); - - public IconCheckBox() { - super(); - setHorizontalAlignment(CENTER); - } - - public IconCheckBox(boolean b) { - super("", b); - setSelected(b); - } - - @Override - public void setSelected(boolean selected) { - super.setSelected(selected); - if (selected) - this.setIcon(ICON_ENABLED); - else - this.setIcon(ICON_DISABLED); - } - -} \ No newline at end of file + */ + private final static int ICON_SIZE = 20; + private final static ImageIcon ICON_ENABLED = loadIcon("checked.png", ICON_SIZE); + private final static ImageIcon ICON_DISABLED = loadIcon("unchecked.png", ICON_SIZE); + + public IconCheckBox() { + super(); + setHorizontalAlignment(CENTER); + } + + public IconCheckBox(boolean b) { + super("", b); + setSelected(b); + } + + @Override + public void setSelected(boolean selected) { + super.setSelected(selected); + if (selected) { + this.setIcon(ICON_ENABLED); + } else { + this.setIcon(ICON_DISABLED); + } + } + +} diff --git a/src/main/java/pulse/ui/components/buttons/LoaderButton.java b/src/main/java/pulse/ui/components/buttons/LoaderButton.java index 84ea003d..09733d9b 100644 --- a/src/main/java/pulse/ui/components/buttons/LoaderButton.java +++ b/src/main/java/pulse/ui/components/buttons/LoaderButton.java @@ -1,19 +1,15 @@ package pulse.ui.components.buttons; -import static java.awt.Font.BOLD; import static java.awt.Toolkit.getDefaultToolkit; import static javax.swing.JFileChooser.APPROVE_OPTION; import static javax.swing.JOptionPane.ERROR_MESSAGE; 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; import java.awt.event.ActionEvent; import java.io.File; @@ -21,82 +17,120 @@ import javax.swing.JButton; import javax.swing.JFileChooser; +import javax.swing.UIManager; import javax.swing.filechooser.FileNameExtensionFilter; +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 static File dir; + private final DataType dataType; + private static File dir; - public LoaderButton() { - super(); - init(); - } + 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(String str) { - super(str); - init(); - } + public LoaderButton(DataType type) { + super(); + this.dataType = type; + init(); + } - public void init() { + public LoaderButton(DataType type, String str) { + super(str); + this.dataType = type; + init(); + } + + public final void init() { + highlight(false); - setFont(getFont().deriveFont(BOLD, 14f)); + addActionListener((ActionEvent arg0) -> { + var fileChooser = new JFileChooser(); + fileChooser.setCurrentDirectory(dir); + var extensions = getDatasetExtensions(); + var extArray = extensions.toArray(new String[extensions.size()]); + fileChooser.setFileFilter( + new FileNameExtensionFilter(getString("LoaderButton.SupportedExtensionsDescriptor"), extArray)); //$NON-NLS-1$ + // $NON-NLS-1$ + var approve = fileChooser.showOpenDialog(getWindowAncestor((Component) arg0.getSource())) == APPROVE_OPTION; + dir = fileChooser.getCurrentDirectory(); + if (!approve) { + return; + } + try { + switch (dataType) { + case HEAT_CAPACITY: + loadSpecificHeat(fileChooser.getSelectedFile()); + break; + case DENSITY: + loadDensity(fileChooser.getSelectedFile()); + break; + default: + throw new IllegalStateException("Unrecognised type: " + dataType); + } + } catch (IOException e) { + getDefaultToolkit().beep(); + showMessageDialog(getWindowAncestor((Component) arg0.getSource()), getString("LoaderButton.ReadError"), //$NON-NLS-1$ + getString("LoaderButton.IOError"), //$NON-NLS-1$ + ERROR_MESSAGE); + } catch (OutOfRangeException ofre) { + getDefaultToolkit().beep(); + StringBuilder sb = new StringBuilder(getString("TextWrap.0")); + 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(), + getString("LoaderButton.OFRError"), //$NON-NLS-1$ + ERROR_MESSAGE); + } + int size = getDataset().getData().size(); + var label = ""; + switch (dataType) { + case HEAT_CAPACITY: + label = getString("LoaderButton.5"); //$NON-NLS-1$ + // $NON-NLS-1$ + break; + case DENSITY: + label = getString("LoaderButton.6"); //$NON-NLS-1$ + // $NON-NLS-1$ + break; + 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()), + sb.toString(), + "Data loaded", INFORMATION_MESSAGE); + highlight(false); + }); + } + + 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); + } - addActionListener((ActionEvent arg0) -> { - var fileChooser = new JFileChooser(); - fileChooser.setCurrentDirectory(dir); - var extensions = getDatasetExtensions(); - var extArray = extensions.toArray(new String[extensions.size()]); - fileChooser.setFileFilter( - new FileNameExtensionFilter(getString("LoaderButton.SupportedExtensionsDescriptor"), extArray)); //$NON-NLS-1$ - // $NON-NLS-1$ - var approve = fileChooser.showOpenDialog(getWindowAncestor((Component) arg0.getSource())) == APPROVE_OPTION; - dir = fileChooser.getCurrentDirectory(); - if (!approve) - return; - try { - switch (dataType) { - case HEAT_CAPACITY: - load(HEAT_CAPACITY, fileChooser.getSelectedFile()); - break; - case DENSITY: - load(DENSITY, fileChooser.getSelectedFile()); - break; - default: - throw new IllegalStateException("Unrecognised type: " + dataType); - } - } catch (IOException e) { - getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor((Component) arg0.getSource()), getString("LoaderButton.ReadError"), //$NON-NLS-1$ - getString("LoaderButton.IOError"), //$NON-NLS-1$ - ERROR_MESSAGE); - e.printStackTrace(); - } - var size = getDataset(dataType).getData().size(); - var label = ""; - switch (dataType) { - case HEAT_CAPACITY: - label = getString("LoaderButton.5"); //$NON-NLS-1$ - // $NON-NLS-1$ - break; - case DENSITY: - label = getString("LoaderButton.6"); //$NON-NLS-1$ - // $NON-NLS-1$ - break; - default: - throw new IllegalStateException("Unknown data type: " + dataType); - } - showMessageDialog(getWindowAncestor((Component) arg0.getSource()), - "" + label + " data loaded! A total of " + size + " data points loaded.", - "Data loaded", INFORMATION_MESSAGE); - }); - } - - public void setDataType(InterpolationDataset.StandartType dataType) { - this.dataType = dataType; - } + public void highlightIfNeeded() { + highlight(getDataset() == null); + } + + public enum DataType { + HEAT_CAPACITY, DENSITY; + } } \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/buttons/package-info.java b/src/main/java/pulse/ui/components/buttons/package-info.java index da58c342..a6cf9c9a 100644 --- a/src/main/java/pulse/ui/components/buttons/package-info.java +++ b/src/main/java/pulse/ui/components/buttons/package-info.java @@ -1 +1 @@ -package pulse.ui.components.buttons; \ No newline at end of file +package pulse.ui.components.buttons; diff --git a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java index 134dca97..4c363b56 100644 --- a/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/AccessibleTableRenderer.java @@ -1,15 +1,10 @@ package pulse.ui.components.controllers; -import static java.awt.Color.BLUE; -import static java.awt.Color.LIGHT_GRAY; -import static java.awt.Color.RED; -import static java.awt.Color.WHITE; -import static java.awt.Font.BOLD; -import static java.awt.Font.ITALIC; - import java.awt.Component; +import java.awt.Font; import javax.swing.JButton; +import javax.swing.JLabel; import javax.swing.JTable; import pulse.properties.Flag; @@ -18,61 +13,36 @@ import pulse.ui.components.buttons.IconCheckBox; import pulse.util.PropertyHolder; +@SuppressWarnings("serial") public class AccessibleTableRenderer extends NumericPropertyRenderer { - /** - * - */ - private static final long serialVersionUID = 2269077862064919825L; - - public AccessibleTableRenderer() { - super(); - } - - @Override - - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, - int row, int column) { - - if (value instanceof NumericProperty) { - var renderer = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - renderer.setForeground(BLUE); - return renderer; - } + public AccessibleTableRenderer() { + super(); + } - if (value instanceof Flag) { - var btn = new IconCheckBox((boolean) ((Property) value).getValue()); - btn.setHorizontalAlignment(CENTER); - if (isSelected) - btn.setBackground(LIGHT_BLUE); - else - btn.setBackground(WHITE); - return btn; - } + @Override - if (value instanceof PropertyHolder) { - var button = initButton(value.toString()); - return button; - } + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { - if (value instanceof Property) { - var jtf = initTextField(value.toString(), table.isRowSelected(row)); - jtf.setForeground(RED); - jtf.setFont(jtf.getFont().deriveFont(BOLD)); - return jtf; - } + Component result = null; - return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + 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 { + result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + } - } + return result; - private JButton initButton(String str) { - var button = new JButton(str); - button.setContentAreaFilled(true); - button.setBackground(LIGHT_GRAY); - button.setFont(button.getFont().deriveFont(12.0f).deriveFont(ITALIC)); - button.setToolTipText(str); - return button; - } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/ButtonEditor.java b/src/main/java/pulse/ui/components/controllers/ButtonEditor.java index 71b88e0f..89b0dd0f 100644 --- a/src/main/java/pulse/ui/components/controllers/ButtonEditor.java +++ b/src/main/java/pulse/ui/components/controllers/ButtonEditor.java @@ -22,53 +22,54 @@ @SuppressWarnings("serial") public class ButtonEditor extends AbstractCellEditor implements TableCellEditor { - private AbstractButton btn; - private PropertyHolder dat; - private NumericPropertyKeyword type; + private AbstractButton btn; + private PropertyHolder dat; + private NumericPropertyKeyword type; - public ButtonEditor(AbstractButton btn, PropertyHolder dat) { - this.btn = btn; - this.dat = dat; + public ButtonEditor(AbstractButton btn, PropertyHolder dat) { + this.btn = btn; + this.dat = dat; - btn.addActionListener((ActionEvent e) -> { - JFrame dataFrame = new DataFrame(dat, btn); - dataFrame.setVisible(true); - btn.setEnabled(false); - dataFrame.addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent we) { - btn.setText(((DataFrame) dataFrame).getDataObject().toString()); - btn.setEnabled(true); - } - }); - }); + btn.addActionListener((ActionEvent e) -> { + JFrame dataFrame = new DataFrame(dat, btn); + dataFrame.setVisible(true); + btn.setEnabled(false); + dataFrame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent we) { + btn.setText(((DataFrame) dataFrame).getDataObject().toString()); + btn.setEnabled(true); + } + }); + }); - } + } - public ButtonEditor(IconCheckBox btn, NumericPropertyKeyword index) { - this.btn = btn; - this.type = index; + public ButtonEditor(IconCheckBox btn, NumericPropertyKeyword index) { + this.btn = btn; + this.type = index; - btn.addActionListener((ActionEvent e) -> { - var source = (IconCheckBox) e.getSource(); - source.setHorizontalAlignment(CENTER); - stopCellEditing(); - }); + btn.addActionListener((ActionEvent e) -> { + var source = (IconCheckBox) e.getSource(); + source.setHorizontalAlignment(CENTER); + stopCellEditing(); + }); - } + } - @Override - public Object getCellEditorValue() { - if (dat != null) - return dat; - var f = new Flag(type); - f.setValue(btn.isSelected()); - return f; - } + @Override + public Object getCellEditorValue() { + if (dat != null) { + return dat; + } + var f = new Flag(type); + f.setValue(btn.isSelected()); + return f; + } - @Override - public Component getTableCellEditorComponent(JTable arg0, Object value, boolean arg2, int arg3, int arg4) { - return btn; - } + @Override + public Component getTableCellEditorComponent(JTable arg0, Object value, boolean arg2, int arg3, int arg4) { + return btn; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/ConfirmAction.java b/src/main/java/pulse/ui/components/controllers/ConfirmAction.java index f63e0053..be3a35ec 100644 --- a/src/main/java/pulse/ui/components/controllers/ConfirmAction.java +++ b/src/main/java/pulse/ui/components/controllers/ConfirmAction.java @@ -2,6 +2,6 @@ public interface ConfirmAction { - public void onConfirm(); + public void onConfirm(); } diff --git a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java index 9b6397a3..9f01e291 100644 --- a/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java +++ b/src/main/java/pulse/ui/components/controllers/InstanceCellEditor.java @@ -6,57 +6,45 @@ import javax.swing.DefaultCellEditor; import javax.swing.JComboBox; import javax.swing.JTable; -import javax.swing.event.PopupMenuEvent; -import javax.swing.event.PopupMenuListener; import pulse.util.InstanceDescriptor; @SuppressWarnings("serial") public class InstanceCellEditor extends DefaultCellEditor { - private InstanceDescriptor descriptor; - private JComboBox combobox; - - public InstanceCellEditor(InstanceDescriptor value) { - super(new JComboBox(((InstanceDescriptor) value).getAllDescriptors().toArray())); - this.descriptor = value; - } - - @Override - public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { - combobox = new JComboBox<>(((InstanceDescriptor) value).getAllDescriptors().toArray()); - combobox.setSelectedItem(descriptor.getValue()); - - combobox.addItemListener(e -> { - if (e.getStateChange() == ItemEvent.SELECTED) - descriptor.attemptUpdate(e.getItem()); - }); - - combobox.addPopupMenuListener(new PopupMenuListener() { - - @Override - public void popupMenuWillBecomeVisible(PopupMenuEvent e) { - // - } - - @Override - public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { - fireEditingCanceled(); - } - - @Override - public void popupMenuCanceled(PopupMenuEvent e) { - // - } - }); - - return combobox; - } - - @Override - public Object getCellEditorValue() { - descriptor.setSelectedDescriptor((String) combobox.getSelectedItem()); - return descriptor; - } - -} \ No newline at end of file + private InstanceDescriptor descriptor; + private JComboBox combobox; + + public InstanceCellEditor(InstanceDescriptor value) { + super(new JComboBox(((InstanceDescriptor) value).getAllDescriptors().toArray())); + this.descriptor = value; + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + combobox = new JComboBox<>(((InstanceDescriptor) value).getAllDescriptors().toArray()); + combobox.setSelectedItem(descriptor.getValue()); + + combobox.addItemListener((ItemEvent e) -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + try { + descriptor.attemptUpdate(e.getItem()); + } catch (NullPointerException npe) { + String text = "Error updating " + descriptor.getDescriptor(false) + + ". Cannot be set to " + e.getItem(); + System.out.println(text); + npe.printStackTrace(); + } + } + }); + + return combobox; + } + + @Override + public Object getCellEditorValue() { + descriptor.setSelectedDescriptor((String) combobox.getSelectedItem()); + return descriptor; + } + +} diff --git a/src/main/java/pulse/ui/components/controllers/KeywordListRenderer.java b/src/main/java/pulse/ui/components/controllers/KeywordListRenderer.java deleted file mode 100644 index 1ab97360..00000000 --- a/src/main/java/pulse/ui/components/controllers/KeywordListRenderer.java +++ /dev/null @@ -1,39 +0,0 @@ -package pulse.ui.components.controllers; - -import static java.awt.Color.black; -import static java.awt.Font.BOLD; -import static pulse.properties.NumericProperties.def; - -import java.awt.Component; - -import javax.swing.DefaultListCellRenderer; -import javax.swing.JList; - -import pulse.properties.NumericPropertyKeyword; - -public class KeywordListRenderer extends DefaultListCellRenderer { - - /** - * - */ - private static final long serialVersionUID = 1L; - - public KeywordListRenderer() { - super(); - } - - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, - boolean cellHasFocus) { - - var renderer = super.getListCellRendererComponent(list, - (def((NumericPropertyKeyword) value).getDescriptor(true)), index, cellHasFocus, cellHasFocus); - - renderer.setForeground(black); - if (isSelected) - renderer.setFont(renderer.getFont().deriveFont(BOLD)); - return renderer; - - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/controllers/NumberEditor.java b/src/main/java/pulse/ui/components/controllers/NumberEditor.java index 1e1a2321..3aa41496 100644 --- a/src/main/java/pulse/ui/components/controllers/NumberEditor.java +++ b/src/main/java/pulse/ui/components/controllers/NumberEditor.java @@ -59,174 +59,168 @@ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * 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; - private NumericProperty property; - - public NumberEditor(NumericProperty property) { - super(new JFormattedTextField()); - - this.property = property; - ftf = (JFormattedTextField) getComponent(); - - // Set up the editor for the integer cells. - - var numFormatter = new NumberFormatter(numberFormat); - numFormatter.setFormat(numberFormat); - - Number value; - - if (property.getValue() instanceof Integer) { - numberFormat = getIntegerInstance(); - value = (int) property.getValue() * (int) property.getDimensionFactor(); - numFormatter.setMinimum(property.getMinimum().intValue() * property.getDimensionFactor().intValue()); - numFormatter.setMaximum(property.getMaximum().intValue() * property.getDimensionFactor().intValue()); - } else { - numberFormat = new DecimalFormat(getString("NumberEditor.NumberFormat")); //$NON-NLS-1$ - value = ((Number) property.getValue()).doubleValue() * property.getDimensionFactor().doubleValue(); - numFormatter.setMinimum(property.getMinimum().doubleValue() * property.getDimensionFactor().doubleValue()); - numFormatter.setMaximum(property.getMaximum().doubleValue() * property.getDimensionFactor().doubleValue()); - } - - ftf.setFormatterFactory(new DefaultFormatterFactory(numFormatter)); - ftf.setValue(value); - ftf.setHorizontalAlignment(CENTER); - ftf.setFocusLostBehavior(PERSIST); - - // React when the user presses Enter while the editor is - // active. (Tab is handled as specified by - // 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) { - if (!ftf.isEditValid()) { // The text is invalid. - if (userSaysRevert()) { // reverted - ftf.postActionEvent(); // inform the editor - } - } else + + JFormattedTextField ftf; + NumberFormat numberFormat; + private boolean DEBUG = false; + private NumericProperty property; + + public NumberEditor(NumericProperty property) { + super(new JFormattedTextField()); + + this.property = property; + ftf = (JFormattedTextField) getComponent(); + + // Set up the editor for the integer cells. + var numFormatter = new NumberFormatter(numberFormat); + numFormatter.setFormat(numberFormat); + + Number value; + + if (property.getValue() instanceof Integer) { + numberFormat = getIntegerInstance(); + value = (int) property.getValue() * (int) property.getDimensionFactor(); + numFormatter.setMinimum(property.getMinimum().intValue() * property.getDimensionFactor().intValue()); + numFormatter.setMaximum(property.getMaximum().intValue() * property.getDimensionFactor().intValue()); + } else { + numberFormat = new DecimalFormat(getString("NumberEditor.NumberFormat")); //$NON-NLS-1$ + value = ((Number) property.getValue()).doubleValue() * property.getDimensionFactor().doubleValue(); + numFormatter.setMinimum(property.getMinimum().doubleValue() * property.getDimensionFactor().doubleValue()); + numFormatter.setMaximum(property.getMaximum().doubleValue() * property.getDimensionFactor().doubleValue()); + } + + ftf.setFormatterFactory(new DefaultFormatterFactory(numFormatter)); + ftf.setValue(value); + ftf.setHorizontalAlignment(CENTER); + ftf.setFocusLostBehavior(PERSIST); + + // React when the user presses Enter while the editor is + // active. (Tab is handled as specified by + // JFormattedTextField's focusLostBehavior property.) + ftf.getInputMap().put(getKeyStroke(VK_ENTER, 0), "check"); + ftf.getActionMap().put("check", new AbstractAction() { + + @Override + public void actionPerformed(ActionEvent e) { + if (!ftf.isEditValid()) { // The text is invalid. + if (userSaysRevert()) { // reverted + ftf.postActionEvent(); // inform the editor + } + } else try { // The text is valid, - ftf.commitEdit(); // so use it. - ftf.postActionEvent(); // stop editing - } catch (java.text.ParseException exc) { - } - } - }); - } - - // Override to invoke setValue on the formatted text field. - @Override - public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { - var ftf = (JFormattedTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column); - - Number num; - var prop = (NumericProperty) value; - - if ((prop.getValue() instanceof Integer)) - num = (int) prop.getValue() * (int) prop.getDimensionFactor(); - else - num = ((Number) prop.getValue()).doubleValue() * prop.getDimensionFactor().doubleValue(); - - ftf.setValue(num); - return ftf; - } - - @Override - public Object getCellEditorValue() { - var ftf = (JFormattedTextField) getComponent(); - var o = ftf.getValue(); - if (o instanceof Number) { - - try { - if (o instanceof Integer) { - if (property.getValue() instanceof Integer) - property.setValue((int) o / (int) property.getDimensionFactor()); - else - property.setValue(((Number) o).doubleValue() / (double) (property.getDimensionFactor())); - } else { - if (property.getValue() instanceof Integer) - property.setValue(((Number) o).intValue() / (int) property.getDimensionFactor()); - else - property.setValue(((Number) o).doubleValue() / (double) (property.getDimensionFactor())); - } - } catch (IllegalArgumentException e) { - getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor(ftf), e.getMessage(), getString("NumberEditor.IllegalTableEntry"), //$NON-NLS-1$ - ERROR_MESSAGE); - property.setValue(property.getMinimum()); - } - return property; - } else { - if (DEBUG) { - out.println(getString("NumberEditor.NotANumberError")); //$NON-NLS-1$ - } - try { - return numberFormat.parseObject(o.toString()); - } catch (ParseException exc) { - err.println(getString("NumberEditor.ParseError") + o); //$NON-NLS-1$ - return null; - } - } - } - - // Override to check whether the edit is valid, - // setting the value if it is and complaining if - // it isn't. If it's OK for the editor to go - // away, we need to invoke the superclass's version - // of this method so that everything gets cleaned up. - @Override - public boolean stopCellEditing() { - var ftf = (JFormattedTextField) getComponent(); - if (ftf.isEditValid()) { - try { - ftf.commitEdit(); - } catch (java.text.ParseException exc) { - } - - } else { // text is invalid - if (!userSaysRevert()) { // user wants to edit - return false; // don't let the editor go away - } - } - - return super.stopCellEditing(); - } - - /** - * Lets the user know that the text they entered is bad. Returns true if the - * user elects to revert to the last good value. Otherwise, returns false, - * indicating that the user wants to continue editing. - */ - protected boolean userSaysRevert() { - getDefaultToolkit().beep(); - ftf.selectAll(); - Object[] options = { getString("NumberEditor.EditText"), getString("NumberEditor.RevertText") }; - var answer = showOptionDialog(getWindowAncestor(ftf), - "The value must be a " + property.getMinimum().getClass().getSimpleName() + " between " - + property.getMinimum().doubleValue() * property.getDimensionFactor().doubleValue() + " and " - + property.getMaximum().doubleValue() * property.getDimensionFactor().doubleValue() + ".\n" - + getString("NumberEditor.MessageLine1") //$NON-NLS-1$ - + getString("NumberEditor.MessageLine2"), //$NON-NLS-1$ - getString("NumberEditor.InvalidText"), //$NON-NLS-1$ - YES_NO_OPTION, ERROR_MESSAGE, null, options, options[1]); - - if (answer == 1) { // Revert! - ftf.setValue(ftf.getValue()); - return true; - } - return false; - } + ftf.commitEdit(); // so use it. + ftf.postActionEvent(); // stop editing + } catch (java.text.ParseException exc) { + } + } + }); + } + + // Override to invoke setValue on the formatted text field. + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + var ftf = (JFormattedTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column); + + Number num; + var prop = (NumericProperty) value; + + if ((prop.getValue() instanceof Integer)) { + num = (int) prop.getValue() * (int) prop.getDimensionFactor(); + } else { + num = ((Number) prop.getValue()).doubleValue() * prop.getDimensionFactor().doubleValue(); + } + + ftf.setValue(num); + return ftf; + } + + @Override + public Object getCellEditorValue() { + var ftf = (JFormattedTextField) getComponent(); + var o = ftf.getValue(); + if (o instanceof Number) { + + try { + if (o instanceof Integer) { + if (property.getValue() instanceof Integer) { + property.setValue((int) o / (int) property.getDimensionFactor()); + } else { + property.setValue(((Number) o).doubleValue() / (double) (property.getDimensionFactor())); + } + } else { + if (property.getValue() instanceof Integer) { + property.setValue(((Number) o).intValue() / (int) property.getDimensionFactor()); + } else { + property.setValue(((Number) o).doubleValue() / (double) (property.getDimensionFactor())); + } + } + } catch (IllegalArgumentException e) { + getDefaultToolkit().beep(); + showMessageDialog(getWindowAncestor(ftf), e.getMessage(), getString("NumberEditor.IllegalTableEntry"), //$NON-NLS-1$ + ERROR_MESSAGE); + property.setValue(property.getMinimum()); + } + return property; + } else { + if (DEBUG) { + out.println(getString("NumberEditor.NotANumberError")); //$NON-NLS-1$ + } + try { + return numberFormat.parseObject(o.toString()); + } catch (ParseException exc) { + err.println(getString("NumberEditor.ParseError") + o); //$NON-NLS-1$ + return null; + } + } + } + + // Override to check whether the edit is valid, + // setting the value if it is and complaining if + // it isn't. If it's OK for the editor to go + // away, we need to invoke the superclass's version + // of this method so that everything gets cleaned up. + @Override + public boolean stopCellEditing() { + var ftf = (JFormattedTextField) getComponent(); + if (ftf.isEditValid()) { + try { + ftf.commitEdit(); + } catch (java.text.ParseException exc) { + } + + } else { // text is invalid + if (!userSaysRevert()) { // user wants to edit + return false; // don't let the editor go away + } + } + + return super.stopCellEditing(); + } + + /** + * Lets the user know that the text they entered is bad. Returns true if the + * user elects to revert to the last good value. Otherwise, returns false, + * indicating that the user wants to continue editing. + */ + protected boolean userSaysRevert() { + getDefaultToolkit().beep(); + ftf.selectAll(); + Object[] options = {getString("NumberEditor.EditText"), getString("NumberEditor.RevertText")}; + var answer = showOptionDialog(getWindowAncestor(ftf), + "The value must be a " + property.getMinimum().getClass().getSimpleName() + " between " + + property.getMinimum().doubleValue() * property.getDimensionFactor().doubleValue() + " and " + + property.getMaximum().doubleValue() * property.getDimensionFactor().doubleValue() + ".\n" + + getString("NumberEditor.MessageLine1") //$NON-NLS-1$ + + getString("NumberEditor.MessageLine2"), //$NON-NLS-1$ + getString("NumberEditor.InvalidText"), //$NON-NLS-1$ + YES_NO_OPTION, ERROR_MESSAGE, null, options, options[1]); + + if (answer == 1) { // Revert! + ftf.setValue(ftf.getValue()); + return true; + } + return false; + } } diff --git a/src/main/java/pulse/ui/components/controllers/NumericPropertyComparator.java b/src/main/java/pulse/ui/components/controllers/NumericPropertyComparator.java index 9c77734f..7bd86cf9 100644 --- a/src/main/java/pulse/ui/components/controllers/NumericPropertyComparator.java +++ b/src/main/java/pulse/ui/components/controllers/NumericPropertyComparator.java @@ -6,16 +6,16 @@ public class NumericPropertyComparator implements Comparator { - protected NumericPropertyComparator() { + protected NumericPropertyComparator() { - } + } - @Override - public int compare(NumericProperty o1, NumericProperty o2) { - var v1 = (Double) o1.getValue(); - var v2 = (Double) o2.getValue(); + @Override + public int compare(NumericProperty o1, NumericProperty o2) { + var v1 = (Double) o1.getValue(); + var v2 = (Double) o2.getValue(); - return v1.compareTo(v2); - } + return v1.compareTo(v2); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java index a615de3c..2d07657f 100644 --- a/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/NumericPropertyRenderer.java @@ -1,55 +1,56 @@ package pulse.ui.components.controllers; -import static java.awt.Color.white; -import static java.awt.Font.PLAIN; -import static pulse.ui.Messages.getString; - -import java.awt.Color; import java.awt.Component; -import java.awt.Font; import javax.swing.JFormattedTextField; +import javax.swing.JLabel; import javax.swing.JTable; +import javax.swing.UIManager; import javax.swing.table.DefaultTableCellRenderer; import pulse.properties.NumericProperty; -import pulse.properties.Property; +import pulse.properties.NumericPropertyFormatter; @SuppressWarnings("serial") public class NumericPropertyRenderer extends DefaultTableCellRenderer { - protected static final Color LIGHT_BLUE = new Color(175, 238, 238); - private final static Font font = new Font(getString("PropertyHolderTable.FontName"), PLAIN, 14); - - public NumericPropertyRenderer() { - super(); - } - - @Override - - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, - int row, int column) { - - if (value instanceof NumericProperty) - return initTextField(((Property) value).formattedOutput(), table.isRowSelected(row)); - - return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - - } - - protected static JFormattedTextField initTextField(String text, boolean rowSelected) { - var jtf = new JFormattedTextField(text); - jtf.setOpaque(true); - jtf.setBorder(null); - jtf.setHorizontalAlignment(CENTER); - jtf.setFont(font); - - if (rowSelected) - jtf.setBackground(LIGHT_BLUE); - else - jtf.setBackground(white); - - return jtf; - } + public NumericPropertyRenderer() { + super(); + } + + @Override + + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { + + Component result = null; + + if (value instanceof NumericProperty) { + var jtf = initTextField((NumericProperty) value, table.isRowSelected(row)); + if (table.getEditorComponent() != null) { + result = jtf; + } else { + 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); + result = superRenderer; + } + + result.setForeground(UIManager.getColor("List.foreground")); + return result; + + } + + private static JFormattedTextField initTextField(NumericProperty np, boolean rowSelected) { + var jtf = new JFormattedTextField(new NumericPropertyFormatter(np, true, true)); + jtf.setValue(np); + jtf.setHorizontalAlignment(RIGHT); + return jtf; + } } diff --git a/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java new file mode 100644 index 00000000..dd621b96 --- /dev/null +++ b/src/main/java/pulse/ui/components/controllers/ProblemCellRenderer.java @@ -0,0 +1,32 @@ +package pulse.ui.components.controllers; + +import java.awt.Component; + +import javax.swing.ImageIcon; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +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; + +@SuppressWarnings("serial") +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) { + + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + var object = ((DefaultMutableTreeNode) value).getUserObject(); + if (leaf && object instanceof Problem) { + var icon = ImageUtils.dye(defaultIcon, ((Problem) object).getComplexity().getColor()); + setIcon(icon); + } + return this; + } + +} diff --git a/src/main/java/pulse/ui/components/controllers/ProblemListCellRenderer.java b/src/main/java/pulse/ui/components/controllers/ProblemListCellRenderer.java deleted file mode 100644 index 06909a2e..00000000 --- a/src/main/java/pulse/ui/components/controllers/ProblemListCellRenderer.java +++ /dev/null @@ -1,57 +0,0 @@ -package pulse.ui.components.controllers; - -import static java.awt.Color.black; -import static java.awt.Font.BOLD; -import static javax.swing.BorderFactory.createTitledBorder; - -import java.awt.Color; -import java.awt.Component; - -import javax.swing.DefaultListCellRenderer; -import javax.swing.JComponent; -import javax.swing.JList; - -import pulse.problem.statements.Problem; - -@SuppressWarnings("serial") -public class ProblemListCellRenderer extends DefaultListCellRenderer { - - public ProblemListCellRenderer() { - super(); - } - - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, - boolean cellHasFocus) { - - var renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - var complexity = ((Problem) value).getComplexity(); - var color = blend(renderer.getBackground(), complexity.getColor(), 15); - - if (isSelected) { - color = color.darker(); - renderer.setFont(renderer.getFont().deriveFont(BOLD)); - } - - renderer.setForeground(black); - renderer.setBackground(color); - - var border = createTitledBorder("Complexity: " + complexity); - border.setTitleColor(complexity.getColor().darker().darker()); - ((JComponent) renderer).setBorder(border); - return renderer; - - } - - private static Color blend(Color c0, Color c1, int alpha) { - double totalAlpha = c0.getAlpha() + c1.getAlpha(); - var weight0 = c0.getAlpha() / totalAlpha; - var weight1 = c1.getAlpha() / totalAlpha; - var r = weight0 * c0.getRed() + weight1 * c1.getRed(); - var g = weight0 * c0.getGreen() + weight1 * c1.getGreen(); - var b = weight0 * c0.getBlue() + weight1 * c1.getBlue(); - - return new Color((int) r, (int) g, (int) b, alpha); - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java index 208b47fe..402bc41a 100644 --- a/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/SearchListRenderer.java @@ -2,34 +2,24 @@ import static javax.swing.BorderFactory.createEmptyBorder; -import java.awt.Color; import java.awt.Component; import javax.swing.DefaultListCellRenderer; import javax.swing.JComponent; import javax.swing.JList; +@SuppressWarnings("serial") public class SearchListRenderer extends DefaultListCellRenderer { - /** - * - */ - private static final long serialVersionUID = 1L; + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, + boolean cellHasFocus) { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, - boolean cellHasFocus) { + var renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + ((JComponent) renderer).setBorder(createEmptyBorder(10, 10, 10, 10)); - var renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + return renderer; - ((JComponent) renderer).setBorder(createEmptyBorder(10, 10, 10, 10)); - ((JComponent) renderer).setOpaque(true); + } - if (isSelected) - renderer.setBackground(new Color(51, 102, 153, 210)); - - return renderer; - - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java index b249e2c6..8bc83691 100644 --- a/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java +++ b/src/main/java/pulse/ui/components/controllers/TaskTableRenderer.java @@ -3,44 +3,45 @@ import static java.awt.Font.BOLD; import java.awt.Component; +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; @SuppressWarnings("serial") public class TaskTableRenderer extends NumericPropertyRenderer { - public TaskTableRenderer() { - super(); - } + public TaskTableRenderer() { + super(); + } - @Override + @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, - int row, int column) { + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { - if (value instanceof NumericProperty) - return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + var superRenderer = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - else if (value instanceof Identifier) - return initTextField("" + ((Property) value).getValue(), table.isRowSelected(row)); + if (value instanceof Identifier) { + var lab = (JLabel) superRenderer; + lab.setHorizontalAlignment(JLabel.CENTER); + } else if (value instanceof NumericProperty) { + return superRenderer; + } else if (value instanceof Status) { - else if (value instanceof Status) { + superRenderer.setForeground(((Status) value).getColor()); + superRenderer.setFont(superRenderer.getFont().deriveFont(BOLD)); + ((JLabel) superRenderer).setHorizontalAlignment(JLabel.CENTER); - var jtf = initTextField(value.toString(), table.isRowSelected(row)); - jtf.setForeground(((Status) value).getColor()); - jtf.setFont(jtf.getFont().deriveFont(BOLD)); + return superRenderer; - return jtf; + } - } + return superRenderer; - return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + } - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/controllers/package-info.java b/src/main/java/pulse/ui/components/controllers/package-info.java index fc6bf5f0..e46aff83 100644 --- a/src/main/java/pulse/ui/components/controllers/package-info.java +++ b/src/main/java/pulse/ui/components/controllers/package-info.java @@ -1 +1 @@ -package pulse.ui.components.controllers; \ No newline at end of file +package pulse.ui.components.controllers; diff --git a/src/main/java/pulse/ui/components/listeners/ExitRequestListener.java b/src/main/java/pulse/ui/components/listeners/ExitRequestListener.java index 0764d6fa..74dd233f 100644 --- a/src/main/java/pulse/ui/components/listeners/ExitRequestListener.java +++ b/src/main/java/pulse/ui/components/listeners/ExitRequestListener.java @@ -2,6 +2,6 @@ public interface ExitRequestListener { - public void onExitRequested(); + public void onExitRequested(); } diff --git a/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java b/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java index 8f14c00b..121f70b6 100644 --- a/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java +++ b/src/main/java/pulse/ui/components/listeners/FrameVisibilityRequestListener.java @@ -2,8 +2,7 @@ public interface FrameVisibilityRequestListener { - public void onProblemStatementShowRequest(); + public void onProblemStatementShowRequest(); + public void onSearchSettingsShowRequest(); - public void onSearchSettingsShowRequest(); - -} +} \ No newline at end of file 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 c64fe350..00000000 --- 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 00000000..a498817a --- /dev/null +++ b/src/main/java/pulse/ui/components/listeners/LogListener.java @@ -0,0 +1,9 @@ +package pulse.ui.components.listeners; + +public interface LogListener { + + public void onLogExportRequest(); + + public void onLogModeChanged(boolean graphical); + +} diff --git a/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java new file mode 100644 index 00000000..b619cafe --- /dev/null +++ b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java @@ -0,0 +1,81 @@ +/* + * 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.listeners; + +import java.awt.Cursor; +import org.jfree.chart.ChartMouseEvent; +import org.jfree.chart.ChartMouseListener; +import pulse.ui.components.Chart; +import pulse.ui.components.MovableValueMarker; + +/** + * + * @author Artem Lunev + */ +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); + + public MouseOnMarkerListener(Chart chart, MovableValueMarker lower, MovableValueMarker upper, double margin) { + this.chart = chart; + this.lower = lower; + this.upper = upper; + this.margin = margin; + } + + @Override + public void chartMouseClicked(ChartMouseEvent arg0) { + //blank + } + + @Override + public void chartMouseMoved(ChartMouseEvent arg0) { + double xCoord = chart.xCoord(arg0.getTrigger()); + highlightMarker(xCoord); + } + + private void highlightMarker(double xCoord) { + + if (xCoord > (lower.getValue() - margin) + & xCoord < (lower.getValue() + margin)) { + + lower.setState(MovableValueMarker.State.SELECTED); + chart.getChartPanel().setCursor(RESIZE); + + } else if (xCoord > (upper.getValue() - margin) + & xCoord < (upper.getValue() + margin)) { + + upper.setState(MovableValueMarker.State.SELECTED); + chart.getChartPanel().setCursor(RESIZE); + + } else { + + lower.setState(MovableValueMarker.State.IDLE); + upper.setState(MovableValueMarker.State.IDLE); + chart.getChartPanel().setCursor(CROSSHAIR); + + } + + } + +} diff --git a/src/main/java/pulse/ui/components/listeners/PlotRequestListener.java b/src/main/java/pulse/ui/components/listeners/PlotRequestListener.java index f030ec61..b6144fc0 100644 --- a/src/main/java/pulse/ui/components/listeners/PlotRequestListener.java +++ b/src/main/java/pulse/ui/components/listeners/PlotRequestListener.java @@ -2,6 +2,6 @@ public interface PlotRequestListener { - public void onPlotRequest(); + public void onPlotRequest(); } diff --git a/src/main/java/pulse/ui/components/listeners/PreviewFrameCreationListener.java b/src/main/java/pulse/ui/components/listeners/PreviewFrameCreationListener.java index ba0037c6..401a236f 100644 --- a/src/main/java/pulse/ui/components/listeners/PreviewFrameCreationListener.java +++ b/src/main/java/pulse/ui/components/listeners/PreviewFrameCreationListener.java @@ -2,6 +2,6 @@ public interface PreviewFrameCreationListener { - public void onPreviewFrameRequest(); + public void onPreviewFrameRequest(); } diff --git a/src/main/java/pulse/ui/components/listeners/ProblemSelectionEvent.java b/src/main/java/pulse/ui/components/listeners/ProblemSelectionEvent.java new file mode 100644 index 00000000..1eb7c6dc --- /dev/null +++ b/src/main/java/pulse/ui/components/listeners/ProblemSelectionEvent.java @@ -0,0 +1,31 @@ +package pulse.ui.components.listeners; + +import pulse.problem.statements.Problem; + +public class ProblemSelectionEvent { + + private Problem problem; + private Object source; + + public ProblemSelectionEvent(Problem problem, Object source) { + this.problem = problem; + this.source = source; + } + + public Problem getProblem() { + return problem; + } + + public void setProblem(Problem problem) { + this.problem = problem; + } + + public Object getSource() { + return source; + } + + public void setSource(Object source) { + this.source = source; + } + +} diff --git a/src/main/java/pulse/ui/components/listeners/ProblemSelectionListener.java b/src/main/java/pulse/ui/components/listeners/ProblemSelectionListener.java new file mode 100644 index 00000000..e217383b --- /dev/null +++ b/src/main/java/pulse/ui/components/listeners/ProblemSelectionListener.java @@ -0,0 +1,7 @@ +package pulse.ui.components.listeners; + +public interface ProblemSelectionListener { + + public void onProblemSelected(ProblemSelectionEvent e); + +} diff --git a/src/main/java/pulse/ui/components/listeners/ResultListener.java b/src/main/java/pulse/ui/components/listeners/ResultListener.java index f90fe5be..4ba96d26 100644 --- a/src/main/java/pulse/ui/components/listeners/ResultListener.java +++ b/src/main/java/pulse/ui/components/listeners/ResultListener.java @@ -4,6 +4,6 @@ public interface ResultListener { - public void onFormatChanged(ResultFormatEvent fme); + public void onFormatChanged(ResultFormatEvent fme); -} +} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/listeners/ResultRequestListener.java b/src/main/java/pulse/ui/components/listeners/ResultRequestListener.java index 88156bc9..ad7961cf 100644 --- a/src/main/java/pulse/ui/components/listeners/ResultRequestListener.java +++ b/src/main/java/pulse/ui/components/listeners/ResultRequestListener.java @@ -2,14 +2,14 @@ public interface ResultRequestListener { - public void onMergeRequest(); + public void onMergeRequest(); - public void onDeleteRequest(); + public void onDeleteRequest(); - public void onPreviewRequest(); + public void onPreviewRequest(); - public void onUndoRequest(); + public void onUndoRequest(); - public void onExportRequest(); + public void onExportRequest(); } diff --git a/src/main/java/pulse/ui/components/listeners/TaskActionListener.java b/src/main/java/pulse/ui/components/listeners/TaskActionListener.java index 747c7598..5262cd2c 100644 --- a/src/main/java/pulse/ui/components/listeners/TaskActionListener.java +++ b/src/main/java/pulse/ui/components/listeners/TaskActionListener.java @@ -2,12 +2,12 @@ public interface TaskActionListener { - public void onRemoveRequest(); + public void onRemoveRequest(); - public void onClearRequest(); + public void onClearRequest(); - public void onResetRequest(); + public void onResetRequest(); - public void onGraphRequest(); + public void onGraphRequest(); } diff --git a/src/main/java/pulse/ui/components/listeners/package-info.java b/src/main/java/pulse/ui/components/listeners/package-info.java index 030cf495..fc163630 100644 --- a/src/main/java/pulse/ui/components/listeners/package-info.java +++ b/src/main/java/pulse/ui/components/listeners/package-info.java @@ -1 +1 @@ -package pulse.ui.components.listeners; \ No newline at end of file +package pulse.ui.components.listeners; diff --git a/src/main/java/pulse/ui/components/models/ParameterListModel.java b/src/main/java/pulse/ui/components/models/ParameterListModel.java deleted file mode 100644 index e4e169fe..00000000 --- a/src/main/java/pulse/ui/components/models/ParameterListModel.java +++ /dev/null @@ -1,52 +0,0 @@ -package pulse.ui.components.models; - -import static pulse.properties.NumericPropertyKeyword.IDENTIFIER; -import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; -import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; -import static pulse.search.direction.ActiveFlags.listAvailableProperties; - -import java.util.ArrayList; -import java.util.List; - -import javax.swing.AbstractListModel; - -import pulse.input.InterpolationDataset; -import pulse.properties.Flag; -import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; - -public class ParameterListModel extends AbstractListModel { - - /** - * - */ - private static final long serialVersionUID = 1L; - private List elements = new ArrayList(); - - public ParameterListModel() { - super(); - update(); - } - - public void update() { - elements.clear(); - var list = new ArrayList(); - listAvailableProperties(list); - list.stream().forEach(property -> elements.add(((Flag) property).getType())); - elements.add(OPTIMISER_STATISTIC); - elements.add(TEST_STATISTIC); - elements.add(IDENTIFIER); - elements.addAll(InterpolationDataset.derivableProperties()); - } - - @Override - public int getSize() { - return elements.size(); - } - - @Override - public NumericPropertyKeyword getElementAt(int i) { - return elements.get(i); - } - -} diff --git a/src/main/java/pulse/ui/components/models/ParameterTableModel.java b/src/main/java/pulse/ui/components/models/ParameterTableModel.java new file mode 100644 index 00000000..ec1bf9c3 --- /dev/null +++ b/src/main/java/pulse/ui/components/models/ParameterTableModel.java @@ -0,0 +1,75 @@ +package pulse.ui.components.models; + +import static pulse.properties.NumericPropertyKeyword.IDENTIFIER; +import static pulse.properties.NumericPropertyKeyword.OPTIMISER_STATISTIC; +import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.table.AbstractTableModel; + +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 { + + protected List elements; + private final boolean extendedList; + + public ParameterTableModel(boolean extendedList) { + super(); + this.elements = new ArrayList<>(); + this.extendedList = extendedList; + } + + public final void populateWithAllProperties() { + elements.clear(); + var set = ActiveFlags.availableProperties(); + set.stream().forEach(property -> elements.add(((Flag) property).getType())); + if (extendedList) { + elements.add(OPTIMISER_STATISTIC); + elements.add(TEST_STATISTIC); + elements.add(IDENTIFIER); + elements.addAll(TaskManager.getManagerInstance().derivableProperties()); + } + } + + @Override + public int getRowCount() { + return elements != null ? elements.size() : 0; + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int i, int i1) { + 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 null; + } + } + + public boolean contains(NumericPropertyKeyword key) { + return elements.contains(key); + } + + public NumericPropertyKeyword getElementAt(int index) { + return elements.get(index); + } + + public List getData() { + return elements; + } + +} diff --git a/src/main/java/pulse/ui/components/models/ResultListModel.java b/src/main/java/pulse/ui/components/models/ResultListModel.java deleted file mode 100644 index 42dc1857..00000000 --- a/src/main/java/pulse/ui/components/models/ResultListModel.java +++ /dev/null @@ -1,69 +0,0 @@ -package pulse.ui.components.models; - -import static pulse.tasks.processing.ResultFormat.getInstance; -import static pulse.tasks.processing.ResultFormat.getMinimalArray; - -import java.util.ArrayList; -import java.util.List; - -import javax.swing.AbstractListModel; - -import pulse.properties.NumericPropertyKeyword; - -public class ResultListModel extends AbstractListModel { - - /** - * - */ - private static final long serialVersionUID = 1L; - private List elements = new ArrayList(); - - public ResultListModel() { - super(); - update(); - } - - public void update() { - elements.clear(); - elements.addAll(getInstance().getKeywords()); - } - - @Override - public int getSize() { - return elements.size(); - } - - @Override - public NumericPropertyKeyword getElementAt(int i) { - return elements.get(i); - } - - public void add(NumericPropertyKeyword key) { - elements.add(key); - var size = this.getSize(); - this.fireContentsChanged(this, size - 1, size); - } - - public void remove(NumericPropertyKeyword key) { - if (!elements.contains(key)) - return; - - for (var keyMin : getMinimalArray()) { - if (key == keyMin) - return; - } - var index = elements.indexOf(key); - elements.remove(key); - this.fireContentsChanged(this, index - 1, index); - - } - - public boolean contains(NumericPropertyKeyword key) { - return elements.contains(key); - } - - public List getData() { - return elements; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/ui/components/models/ResultTableModel.java b/src/main/java/pulse/ui/components/models/ResultTableModel.java index 4a3e0b0f..c131f9cc 100644 --- a/src/main/java/pulse/ui/components/models/ResultTableModel.java +++ b/src/main/java/pulse/ui/components/models/ResultTableModel.java @@ -1,16 +1,29 @@ package pulse.ui.components.models; -import static java.util.stream.Collectors.toList; +import static java.lang.Math.abs; import static pulse.tasks.processing.AbstractResult.filterProperties; import java.util.ArrayList; 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; +import pulse.properties.NumericProperties; +import static pulse.properties.NumericPropertyKeyword.IDENTIFIER; +import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; +import pulse.tasks.Calculation; 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; import pulse.tasks.processing.AbstractResult; +import pulse.tasks.processing.AverageResult; import pulse.tasks.processing.Result; import pulse.tasks.processing.ResultFormat; import pulse.ui.components.listeners.ResultListener; @@ -18,127 +31,309 @@ @SuppressWarnings("serial") public class ResultTableModel extends DefaultTableModel { - private ResultFormat fmt; - private List results; - private List tooltips; - private List listeners; + private ResultFormat fmt; + private List results; + private List tooltips; + private List listeners; + + public ResultTableModel(ResultFormat fmt, int rowCount) { + super(fmt.abbreviations().toArray(), rowCount); + results = new ArrayList<>(); + this.fmt = fmt; + tooltips = tooltips(); + listeners = new ArrayList<>(); + } + + 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); + } + + public void removeListeners() { + listeners.clear(); + } + + public void clear() { + results.clear(); + listeners.clear(); + setRowCount(0); + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; // all cells false + } + + public void changeFormat(ResultFormat fmt) { + this.fmt = fmt; + + for (var r : results) { + r.setFormat(fmt); + } + + if (this.getRowCount() > 0) { + this.setRowCount(0); + + List oldResults = new ArrayList<>(results); + + results.clear(); + this.setColumnIdentifiers(fmt.abbreviations().toArray()); + + oldResults.stream().filter(Objects::nonNull).forEach(r -> addRow(r)); + + } else { + this.setColumnIdentifiers(fmt.abbreviations().toArray()); + } + + tooltips = tooltips(); + + listeners.stream().forEach(l -> l.onFormatChanged(new ResultFormatEvent(fmt))); + + } + + /** + * Transforms the result model by merging individual results which: (a) + * correspond to test temperatures within a specified + * {@code temperatureDelta} (b) form a single sequence of measurements + * + * @param temperatureDelta the maximum difference between the test + * temperature of two results being merged + */ + public void merge(double temperatureDelta) { + List skipList = new ArrayList<>(); + List avgResults = new ArrayList<>(); + List sortedResults = new ArrayList<>(results); + + /*sort results in the order of their ids + * This is essential for the algorithm below which assumes the results + * are listed in the order of ascending ids. + */ + sortedResults.sort((AbstractResult arg0, AbstractResult arg1) -> { + var id1 = arg0.getProperties().get(fmt.indexOf(IDENTIFIER)); + var id2 = arg1.getProperties().get(fmt.indexOf(IDENTIFIER)); + return NumericProperties.compare(id1, id2); + }); + + //iterated over the merged list + for (var r : sortedResults) { + + //ignore results added to the skip list + if (skipList.contains(r)) { + continue; + } + + //form a group of individual results corresponding to the specified criteria + List group = group(sortedResults, r, temperatureDelta); + //remove any previous occurences in that group + group.removeAll(skipList); + + if (group.isEmpty()) { + continue; + } else if (group.size() == 1) { + //just one result is being added - no need to average + avgResults.addAll(group); + } else { + //add and average result + var result = new AverageResult(group, fmt); + avgResults.add(result); + } + + //ignore processed results later on + skipList.addAll(group); + + } + + //populate model + invokeLater(() -> { + setRowCount(0); + results.clear(); + avgResults.stream().filter(Objects::nonNull).forEach(r -> addRow(r)); + }); + + } + + /** + * Takes a list of results, which should be mandatory sorted in the order of + * ascending id values, and searches for those results that can be merged + * with {@code r}, satisfying these criteria: (a) these results correspond + * to test temperatures within a specified {@code temperatureDelta} (b) they + * form a single sequence of measurements + * + * @param listOfResults an orderer list of results, as explained above + * @param r the result of interest + * @param propertyInterval an interval for the temperature merging + * @return a group of results + */ + public List group(List listOfResults, AbstractResult r, double propertyInterval) { + List selection = new ArrayList<>(); + + final int idIndex = fmt.indexOf(IDENTIFIER); + final int temperatureIndex = fmt.indexOf(TEST_TEMPERATURE); + + final double curTemp = ((Number) r.getProperties().get(temperatureIndex) + .getValue()).doubleValue(); + + final int curId = ((Number) r.getProperties().get(idIndex).getValue()) + .intValue(); + + List ids = new ArrayList<>(); + ids.add(curId); + + for (var rr : listOfResults) { + + var props = rr.getProperties(); + //temperature of a different result + double temp = ((Number) props.get(temperatureIndex).getValue()).doubleValue(); + + //if the property at modelIndex and the property value lie withing a specified interval + if (abs(temp - curTemp) < propertyInterval) { + + //what is ID of that property? + int newId = ((Number) props.get(idIndex).getValue()).intValue(); + + //calculate the minimum "ID" distance between that property and + //the group elements, that should be either "one" or "zero" + Optional minDistance = ids.stream().map(id + -> (int) Math.abs(id - newId)) + .reduce((a, b) -> a < b ? a : b); + + //accept only measurements within a single interval + if (minDistance.get() < 2) { + selection.add(rr); + ids.add(newId); + } + + } + + } + + return selection; - public ResultTableModel(ResultFormat fmt, int rowCount) { - super(fmt.abbreviations().toArray(), rowCount); - this.fmt = fmt; - results = new ArrayList<>(); - tooltips = tooltips(); - listeners = new ArrayList<>(); - } + } - public ResultTableModel(ResultFormat fmt) { - this(fmt, 0); - } + private List tooltips() { + return fmt.descriptors(); + } - public void addListener(ResultListener listener) { - listeners.add(listener); - } + public void addRow(AbstractResult result) { + Objects.requireNonNull(result, "Entry added to the results table must not be null"); - public void removeListeners() { - listeners.clear(); - } + var instance = TaskManager.getManagerInstance(); - public void clear() { - results.clear(); - listeners.clear(); - setRowCount(0); - } + //ignore average results + if (result instanceof Result) { - @Override - public boolean isCellEditable(int row, int column) { - return false; // all cells false - } + //result must have a valid ancestor! + var id = ((Result) result).getTaskIdentifier(); + SearchTask parentTask = instance.getTask(id); - public void changeFormat(ResultFormat fmt) { - this.fmt = fmt; + //any old result asssociated withis this task + var linkedResults = results.stream() + .filter(r -> r instanceof Result) + .filter(rr -> ((Result) rr).getTaskIdentifier().equals(parentTask.getIdentifier())) + .collect(Collectors.toList()); - for (var r : results) { - r.setFormat(fmt); - } + 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(); - if (this.getRowCount() > 0) { - this.setRowCount(0); + //old calculation found + if (oldCalculation.isPresent()) { - List oldResults = new ArrayList<>(results); + //since the task has already been completed anyway + Status status = Status.DONE; - results.clear(); - this.setColumnIdentifiers(fmt.abbreviations().toArray()); + //better result than already present -- update table + var c = (Calculation) parentTask.getResponse(); + if (c.isBetterThan(oldCalculation.get())) { + remove(oldResultExisting); + status.setDetails(Details.BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED); + parentTask.setStatus(status); + } else { + //do not remove result and do not add new result + status.setDetails(Details.CALCULATION_RESULTS_WORSE_THAN_PREVIOUSLY_OBTAINED); + parentTask.setStatus(status); + return; + } - for (var r : oldResults) { - addRow(r); - } + } else { + //calculation has been purged -- delete previous result - } else - this.setColumnIdentifiers(fmt.abbreviations().toArray()); + remove(oldResultExisting); - tooltips = tooltips(); + } - listeners.stream().forEach(l -> l.onFormatChanged(new ResultFormatEvent(fmt))); + } - } + } - private List tooltips() { - return fmt.descriptors().stream().map(d -> "" + d + "").collect(toList()); - } + var propertyList = filterProperties(result, fmt); + super.addRow(propertyList.toArray()); + results.add(result); - public void addRow(AbstractResult result) { - if (result == null) - return; + } - var propertyList = filterProperties(result, fmt); - super.addRow(propertyList.toArray()); - results.add(result); + public void removeAll(Identifier id) { + AbstractResult result = null; - } + for (var i = results.size() - 1; i >= 0; i--) { + result = results.get(i); - public void removeAll(Identifier id) { - AbstractResult result = null; + if (!(result instanceof Result)) { + continue; + } - for (var i = results.size() - 1; i >= 0; i--) { - result = results.get(i); + if (id.equals(result.identify())) { + results.remove(result); + super.removeRow(i); + } - if (!(result instanceof Result)) - continue; + } - if (result.identify().equals(id)) { - results.remove(result); - super.removeRow(i); - } + } - } + public void remove(AbstractResult r) { + AbstractResult result = null; - } + for (var i = results.size() - 1; i >= 0; i--) { + result = results.get(i); - public void remove(AbstractResult r) { - AbstractResult result = null; + if (result.equals(r)) { + results.remove(result); + super.removeRow(i); + } - for (var i = results.size() - 1; i >= 0; i--) { - result = results.get(i); + } - if (result.equals(r)) { - results.remove(result); - super.removeRow(i); - } + } - } + public List getResults() { + return results; + } - } + public ResultFormat getFormat() { + return fmt; + } - public List getResults() { - return results; - } + public List getTooltips() { + return tooltips; + } - public ResultFormat getFormat() { - return fmt; - } - - public List getTooltips() { - return tooltips; - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/models/SelectedKeysModel.java b/src/main/java/pulse/ui/components/models/SelectedKeysModel.java new file mode 100644 index 00000000..f9098f20 --- /dev/null +++ b/src/main/java/pulse/ui/components/models/SelectedKeysModel.java @@ -0,0 +1,93 @@ +package pulse.ui.components.models; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.table.DefaultTableModel; +import pulse.properties.NumericProperties; + +import pulse.properties.NumericPropertyKeyword; +import pulse.ui.Messages; + +public class SelectedKeysModel extends DefaultTableModel { + + private final List elements; + private final List referenceList; + private final NumericPropertyKeyword[] mandatorySelection; + + public SelectedKeysModel(List keys, NumericPropertyKeyword[] mandatorySelection) { + super(); + this.elements = new ArrayList<>(); + this.mandatorySelection = mandatorySelection; + referenceList = keys; + update(); + } + + public final void update() { + update(referenceList); + } + + public void update(List keys) { + elements.clear(); + elements.addAll(keys); + } + + @Override + public int getRowCount() { + return elements != null ? elements.size() : 0; + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int i, int i1) { + 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 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; + } + + public NumericPropertyKeyword getElementAt(int index) { + return elements.get(index); + } + + public boolean removeElement(NumericPropertyKeyword key) { + + if (!elements.contains(key)) { + return false; + } + + for (var keyMin : mandatorySelection) { + if (key == keyMin) { + return false; + } + } + + var index = elements.indexOf(key); + super.fireTableRowsDeleted(index, index); + elements.remove(key); + return true; + } + +} diff --git a/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java new file mode 100644 index 00000000..83255e35 --- /dev/null +++ b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java @@ -0,0 +1,52 @@ +package pulse.ui.components.models; + +import static javax.swing.SwingUtilities.invokeLater; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericPropertyKeyword.MODEL_WEIGHT; + +import javax.swing.table.DefaultTableModel; + +import pulse.tasks.Calculation; +import pulse.tasks.SearchTask; + +@SuppressWarnings("serial") +public class StoredCalculationTableModel extends DefaultTableModel { + + public static final int WEIGHT_COLUMN = 5; + public static final int STATUS_COLUMN = 4; + public static final int MODEL_STATISTIC_COLUMN = 3; + public static final int OPTIMISER_STATISTIC_COLUMN = 2; + public static final int BASELINE_COLUMN = 1; + public static final int PROBLEM_COLUMN = 0; + + public StoredCalculationTableModel() { + + super(new Object[][]{}, + new String[]{"Problem Statement", "Baseline", "Parameter count", + "Optimiser Statistic", "Model Selection Statistic", + def(MODEL_WEIGHT).getAbbreviation(true)}); + } + + public void update(SearchTask t) { + super.setRowCount(0); + var list = t.getStoredCalculations(); + + for (Calculation c : list) { + //we assume all problem descriptions contain the word Problem after their titles + String problem = c.getProblem().toString().split("Problem")[0] + ""; + //likewise -- for baselines containing Baseline + String baseline = c.getProblem().getBaseline().getSimpleName().split("Baseline")[0]; + var optimiser = c.getOptimiserStatistic(); + var criterion = c.getModelSelectionCriterion(); + var parameters = c.getModelSelectionCriterion().getNumVariables(); + + var weight = c.weight(list); + + var data = new Object[]{problem, baseline, parameters, optimiser.getStatistic(), criterion.getStatistic(), weight}; + + invokeLater(() -> super.addRow(data)); + } + + } + +} diff --git a/src/main/java/pulse/ui/components/models/TaskBoxModel.java b/src/main/java/pulse/ui/components/models/TaskBoxModel.java index 5042ae4c..317d367d 100644 --- a/src/main/java/pulse/ui/components/models/TaskBoxModel.java +++ b/src/main/java/pulse/ui/components/models/TaskBoxModel.java @@ -15,83 +15,81 @@ /* * BASED ON DefaultComboBoxModel */ - public class TaskBoxModel extends AbstractListModel implements ComboBoxModel { - /** - * - */ - private static final long serialVersionUID = 5394433933807306979L; - protected SearchTask selectedTask; - - public TaskBoxModel() { - var instance = TaskManager.getManagerInstance(); - selectedTask = instance.getSelectedTask(); - - instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if (e.getState() == TASK_ADDED) { - notifyTaskAdded(e.getId()); - } - if (e.getState() == TASK_REMOVED) { - notifyTaskRemoved(e.getId()); - } - }); - - } - - @Override - public int getSize() { - return TaskManager.getManagerInstance().numberOfTasks(); - } - - @Override - public SearchTask getElementAt(int index) { - return TaskManager.getManagerInstance().getTaskList().get(index); - } - - @Override - public void setSelectedItem(Object anItem) { - // No item is selected and object is null, so no change required. - if (selectedTask == null && anItem == null) - return; - - if (!(anItem instanceof SearchTask)) - throw new IllegalArgumentException(getString("TaskBoxModel.WrongClassError")); //$NON-NLS-1$ - - // object is already selected so no change required. - if (selectedTask != null && selectedTask.equals(anItem)) - return; - - // Simply return if object is not in the list. - if (selectedTask != null && !TaskManager.getManagerInstance().getTaskList().contains(anItem)) - return; - - // Here we know that object is either an item in the list or null. - // Handle the three change cases: selectedItem is null, object is - // non-null; selectedItem is non-null, object is null; - // selectedItem is non-null, object is non-null and they're not - // equal. - selectedTask = (SearchTask) anItem; - fireContentsChanged(this, -1, -1); - } - - public int getSelectedIndex() { - return TaskManager.getManagerInstance().getTaskList().indexOf(selectedTask); - } - - @Override - public Object getSelectedItem() { - return selectedTask; - } - - private void notifyTaskAdded(Identifier id) { - var index = (int) id.getValue(); - fireIntervalAdded(this, index, index); - } - - private void notifyTaskRemoved(Identifier id) { - var index = (int) id.getValue(); - fireIntervalRemoved(this, index, index); - } - -} \ No newline at end of file + protected SearchTask selectedTask; + + public TaskBoxModel() { + var instance = TaskManager.getManagerInstance(); + selectedTask = instance.getSelectedTask(); + + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { + if (e.getState() == TASK_ADDED) { + notifyTaskAdded(e.getId()); + } + if (e.getState() == TASK_REMOVED) { + notifyTaskRemoved(e.getId()); + } + }); + + } + + @Override + public int getSize() { + return TaskManager.getManagerInstance().numberOfTasks(); + } + + @Override + public SearchTask getElementAt(int index) { + return TaskManager.getManagerInstance().getTaskList().get(index); + } + + @Override + public void setSelectedItem(Object anItem) { + // No item is selected and object is null, so no change required. + if (selectedTask == null && anItem == null) { + return; + } + + if (!(anItem instanceof SearchTask)) { + throw new IllegalArgumentException(getString("TaskBoxModel.WrongClassError")); //$NON-NLS-1$ + } + // object is already selected so no change required. + if (selectedTask != null && selectedTask.equals(anItem)) { + return; + } + + // Simply return if object is not in the list. + if (selectedTask != null && !TaskManager.getManagerInstance().getTaskList().contains(anItem)) { + return; + } + + // Here we know that object is either an item in the list or null. + // Handle the three change cases: selectedItem is null, object is + // non-null; selectedItem is non-null, object is null; + // selectedItem is non-null, object is non-null and they're not + // equal. + selectedTask = (SearchTask) anItem; + fireContentsChanged(this, -1, -1); + } + + public int getSelectedIndex() { + return TaskManager.getManagerInstance().getTaskList().indexOf(selectedTask); + } + + @Override + public Object getSelectedItem() { + return selectedTask; + } + + private void notifyTaskAdded(Identifier id) { + var index = (int) id.getValue(); + fireIntervalAdded(this, index, index); + } + + private void notifyTaskRemoved(Identifier id) { + var index = (int) id.getValue(); + fireIntervalRemoved(this, index, index); + } + +} diff --git a/src/main/java/pulse/ui/components/models/TaskTableModel.java b/src/main/java/pulse/ui/components/models/TaskTableModel.java index 2826bf58..2145d57f 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; @@ -22,63 +24,78 @@ @SuppressWarnings("serial") public class TaskTableModel extends DefaultTableModel { - public static final int SEARCH_STATISTIC_COLUMN = 2; - public static final int TEST_STATISTIC_COLUMN = 3; - public static final int STATUS_COLUMN = 4; - - public TaskTableModel() { - - super(new Object[][] {}, - new String[] { def(IDENTIFIER).getAbbreviation(true), def(TEST_TEMPERATURE).getAbbreviation(true), - def(OPTIMISER_STATISTIC).getAbbreviation(true), def(TEST_STATISTIC).getAbbreviation(true), - getString("TaskTable.Status") }); - - var instance = TaskManager.getManagerInstance(); - - /* + public static final int SEARCH_STATISTIC_COLUMN = 2; + public static final int TEST_STATISTIC_COLUMN = 3; + public static final int STATUS_COLUMN = 4; + + public TaskTableModel() { + + super(new Object[][]{}, + new String[]{def(IDENTIFIER).getAbbreviation(true), + def(TEST_TEMPERATURE).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 - */ - - instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { - if (e.getState() == TASK_REMOVED) - removeTask(e.getId()); - else if (e.getState() == TASK_ADDED) - addTask(instance.getTask(e.getId())); - }); - - } - - public void addTask(SearchTask t) { - var temperature = t.getExperimentalCurve().getMetadata().numericProperty(TEST_TEMPERATURE); - var data = new Object[] { t.getIdentifier(), temperature, t.getResidualStatistic().getStatistic(), - t.getNormalityTest().getStatistic(), t.getStatus() }; - - invokeLater(() -> super.addRow(data)); - - t.addStatusChangeListener((StateEntry e) -> { - setValueAt(e.getState(), searchRow(t.getIdentifier()), STATUS_COLUMN); - if (t.getNormalityTest() != null) - setValueAt(t.getNormalityTest().getStatistic(), searchRow(t.getIdentifier()), TEST_STATISTIC_COLUMN); - }); - - t.addTaskListener((LogEntry e) -> { - setValueAt(t.getResidualStatistic().getStatistic(), searchRow(t.getIdentifier()), SEARCH_STATISTIC_COLUMN); - }); - - } - - public void removeTask(Identifier id) { - var index = searchRow(id); - - if (index > -1) - invokeLater(() -> super.removeRow(index)); - - } - - public int searchRow(Identifier id) { - var data = this.getDataVector(); - var v = dataVector.stream().filter(row -> ((Identifier) row.get(0)).equals(id)).findFirst(); - return v.isPresent() ? data.indexOf(v.get()) : -1; - } - -} \ No newline at end of file + */ + instance.addTaskRepositoryListener((TaskRepositoryEvent e) -> { + if (e.getState() == TASK_REMOVED) { + removeTask(e.getId()); + } else if (e.getState() == TASK_ADDED) { + addTask(instance.getTask(e.getId())); + } + }); + } + + public void addTask(SearchTask t) { + var temperature = ((ExperimentalData) t.getInput()) + .getMetadata().numericProperty(TEST_TEMPERATURE); + var calc = (Calculation) t.getResponse(); + var data = new Object[]{t.getIdentifier(), temperature, + calc.getOptimiserStatistic().getStatistic(), + t.getNormalityTest().getStatistic(), t.getStatus()}; + + invokeLater(() -> super.addRow(data)); + + t.addStatusChangeListener((StateEntry e) -> { + setValueAt(e.getState(), searchRow(t.getIdentifier()), STATUS_COLUMN); + if (t.getNormalityTest() != null) { + setValueAt(t.getNormalityTest().getStatistic(), + searchRow(t.getIdentifier()), TEST_STATISTIC_COLUMN); + } + }); + + t.addTaskListener((LogEntry e) -> { + setValueAt(calc.getOptimiserStatistic() + .getStatistic(), searchRow(t.getIdentifier()), SEARCH_STATISTIC_COLUMN); + }); + + } + + public void removeTask(Identifier id) { + var index = searchRow(id); + + if (index > -1) { + invokeLater(() -> super.removeRow(index)); + } + + } + + public int searchRow(Identifier id) { + var data = this.getDataVector(); + var v = dataVector.stream().filter(row -> ((Identifier) row.get(0)).equals(id)).findFirst(); + return v.isPresent() ? data.indexOf(v.get()) : -1; + } + +} diff --git a/src/main/java/pulse/ui/components/models/package-info.java b/src/main/java/pulse/ui/components/models/package-info.java index f15d7bc4..ac134d93 100644 --- a/src/main/java/pulse/ui/components/models/package-info.java +++ b/src/main/java/pulse/ui/components/models/package-info.java @@ -1 +1 @@ -package pulse.ui.components.models; \ No newline at end of file +package pulse.ui.components.models; diff --git a/src/main/java/pulse/ui/components/package-info.java b/src/main/java/pulse/ui/components/package-info.java index 3fbd7a3f..c71c6d7c 100644 --- a/src/main/java/pulse/ui/components/package-info.java +++ b/src/main/java/pulse/ui/components/package-info.java @@ -3,5 +3,4 @@ * interface of {@code PULsE} that are used to interact with all other entities, * such as {@code PropertyHolder}s, etc. {@code Propert}ies. */ - -package pulse.ui.components; \ No newline at end of file +package pulse.ui.components; diff --git a/src/main/java/pulse/ui/components/panels/ChartToolbar.java b/src/main/java/pulse/ui/components/panels/ChartToolbar.java index 297ead37..bb54a90e 100644 --- a/src/main/java/pulse/ui/components/panels/ChartToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ChartToolbar.java @@ -1,231 +1,189 @@ package pulse.ui.components.panels; -import static java.awt.Color.GRAY; -import static java.awt.Color.black; -import static java.awt.Color.gray; import static java.awt.GridBagConstraints.BOTH; -import static java.awt.Toolkit.getDefaultToolkit; -import static java.lang.String.format; -import static javax.swing.JOptionPane.ERROR_MESSAGE; -import static javax.swing.JOptionPane.YES_NO_OPTION; -import static javax.swing.JOptionPane.YES_OPTION; -import static javax.swing.JOptionPane.showConfirmDialog; import static javax.swing.JOptionPane.showOptionDialog; import static javax.swing.SwingUtilities.getWindowAncestor; -import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_FINISHED; -import static pulse.ui.Launcher.loadIcon; +import static pulse.properties.NumericProperties.derive; +import static pulse.properties.NumericPropertyKeyword.LOWER_BOUND; +import static pulse.properties.NumericPropertyKeyword.UPPER_BOUND; import static pulse.ui.Messages.getString; import static pulse.ui.frames.MainGraphFrame.getChart; +import static pulse.util.ImageUtils.loadIcon; +import java.awt.Color; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; +import java.text.ParseException; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.JButton; -import javax.swing.JFormattedTextField; -import javax.swing.JPanel; -import javax.swing.JTextField; +import javax.swing.JOptionPane; import javax.swing.JToggleButton; -import javax.swing.text.NumberFormatter; +import javax.swing.JToolBar; +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; +import pulse.ui.components.ResidualsChart; import pulse.ui.components.listeners.PlotRequestListener; +import pulse.ui.frames.HistogramFrame; @SuppressWarnings("serial") -public class ChartToolbar extends JPanel { +public final class ChartToolbar extends JToolBar { - private final static int ICON_SIZE = 16; - private List listeners; + private final static int ICON_SIZE = 16; + private final List listeners; - public ChartToolbar() { - super(); - listeners = new ArrayList<>(); - initComponents(); - } + private RangeTextFields rtf; - public void initComponents() { - setLayout(new GridBagLayout()); + public ChartToolbar() { + super(); + setFloatable(false); + listeners = new ArrayList<>(); + rtf = new RangeTextFields(); + initComponents(); + } - var lowerLimitField = new JFormattedTextField(new NumberFormatter()); - var upperLimitField = new JFormattedTextField(new NumberFormatter()); + public final void initComponents() { + setLayout(new GridBagLayout()); - var limitRangeBtn = new JButton(); - var adiabaticSolutionBtn = new JToggleButton(); - var residualsBtn = new JToggleButton(); + var limitRangeBtn = new JButton(); + var adiabaticSolutionBtn = new JToggleButton(loadIcon("parker.png", ICON_SIZE, Color.white)); + var residualsBtn = new JToggleButton(loadIcon("residuals.png", ICON_SIZE, Color.white)); + var pdfBtn = new JButton(loadIcon("pdf.png", ICON_SIZE, Color.white)); + pdfBtn.setToolTipText("Residuals Histogram"); - var gbc = new GridBagConstraints(); - gbc.fill = BOTH; - gbc.weightx = 0.25; + var residualsChart = new ResidualsChart("Residual value", "Frequency"); + var chFrame = new HistogramFrame(residualsChart, 450, 450); - lowerLimitField.setValue(0.0); + pdfBtn.addActionListener(e -> { - var ghostText1 = "Lower bound"; - lowerLimitField.setText(ghostText1); + var task = TaskManager.getManagerInstance().getSelectedTask(); + var calc = (Calculation) task.getResponse(); - var ghostText2 = "Upper bound"; + if (task != null && calc.getModelSelectionCriterion() != null) { - add(lowerLimitField, gbc); + chFrame.setLocationRelativeTo(null); + chFrame.setVisible(true); + chFrame.plot(calc.getOptimiserStatistic()); + + } + + }); - upperLimitField.setValue(1.0); - upperLimitField.setText(ghostText2); - - add(upperLimitField, gbc); - - limitRangeBtn.setText("Limit Range To"); - - lowerLimitField.setForeground(GRAY); - upperLimitField.setForeground(GRAY); - - var ftfFocusListener = new FocusListener() { - - @Override - public void focusGained(FocusEvent e) { - var src = (JTextField) e.getSource(); - if (src.getText().length() > 0) - src.setForeground(black); - } - - @Override - public void focusLost(FocusEvent e) { - var src = (JFormattedTextField) e.getSource(); - if (src.getValue() == null) { - src.setText(ghostText1); - src.setForeground(gray); - } - } - - }; - - var instance = TaskManager.getManagerInstance(); - - instance.addSelectionListener(event -> { - var t = instance.getSelectedTask(); - var expCurve = t.getExperimentalCurve(); - - lowerLimitField.setValue(expCurve.getRange().getSegment().getMinimum()); - upperLimitField.setValue(expCurve.getRange().getSegment().getMaximum()); - - }); - - instance.addTaskRepositoryListener(e -> { - - if (e.getState() == TASK_FINISHED) { - - var t = instance.getSelectedTask(); - - if (e.getId().equals(t.getIdentifier())) { - lowerLimitField.setValue(t.getExperimentalCurve().getRange().getSegment().getMinimum()); - upperLimitField.setValue(t.getExperimentalCurve().getRange().getSegment().getMaximum()); - notifyPlot(); - } - - } - - }); - - lowerLimitField.addFocusListener(ftfFocusListener); - upperLimitField.addFocusListener(ftfFocusListener); - - limitRangeBtn.addActionListener(e -> { - if ((!lowerLimitField.isEditValid()) || (!upperLimitField.isEditValid())) { // The text is invalid. - if (userSaysRevert(lowerLimitField)) { // reverted - lowerLimitField.postActionEvent(); // inform the editor - } - } - - else { - var lower = ((Number) lowerLimitField.getValue()).doubleValue(); - var upper = ((Number) upperLimitField.getValue()).doubleValue(); - validateRange(lower, upper); - notifyPlot(); - } - }); - - gbc.weightx = 0.25; - add(limitRangeBtn, gbc); - - adiabaticSolutionBtn.setToolTipText("Sanity check (original adiabatic solution)"); - adiabaticSolutionBtn.setIcon(loadIcon("parker.png", ICON_SIZE)); - - adiabaticSolutionBtn.addActionListener(e -> { - getChart().setZeroApproximationShown(adiabaticSolutionBtn.isSelected()); - notifyPlot(); - }); - - gbc.weightx = 0.125; - add(adiabaticSolutionBtn, gbc); - - residualsBtn.setToolTipText("Plot residuals"); - residualsBtn.setIcon(loadIcon("residuals.png", ICON_SIZE)); - residualsBtn.setSelected(true); - - residualsBtn.addActionListener(e -> { - getChart().setResidualsShown(residualsBtn.isSelected()); - notifyPlot(); - }); - - gbc.weightx = 0.125; - add(residualsBtn, gbc); - } - - public void addPlotRequestListener(PlotRequestListener plotRequestListener) { - listeners.add(plotRequestListener); - } - - private void notifyPlot() { - listeners.stream().forEach(l -> l.onPlotRequest()); - } - - private static boolean userSaysRevert(JFormattedTextField ftf) { - getDefaultToolkit().beep(); - ftf.selectAll(); - Object[] options = { getString("NumberEditor.EditText"), getString("NumberEditor.RevertText") }; - var answer = showOptionDialog(getWindowAncestor(ftf), - "Time domain should be consistent with the experimental data range.
" - + getString("NumberEditor.MessageLine1") + getString("NumberEditor.MessageLine2") + "", - getString("NumberEditor.InvalidText"), YES_NO_OPTION, ERROR_MESSAGE, null, options, options[1]); - - if (answer == 1) { // Revert! - ftf.setValue(ftf.getValue()); - return true; - } - return false; - } - - private void validateRange(double a, double b) { - var task = TaskManager.getManagerInstance().getSelectedTask(); - - if (task == null) - return; - - var expCurve = task.getExperimentalCurve(); - - if (expCurve == null) - return; - - var sb = new StringBuilder(); - - sb.append("

"); - sb.append(getString("RangeSelectionFrame.ConfirmationMessage1")); - sb.append("


"); - sb.append(getString("RangeSelectionFrame.ConfirmationMessage2")); - sb.append(expCurve.getEffectiveStartTime()); - sb.append(" to "); - sb.append(expCurve.getEffectiveEndTime()); - sb.append("

"); - sb.append(getString("RangeSelectionFrame.ConfirmationMessage3")); - sb.append(format("%3.4f", a) + " to " + format("%3.4f", b)); - sb.append(""); - - var dialogResult = showConfirmDialog(getWindowAncestor(this), sb.toString(), "Confirm chocie", YES_NO_OPTION); - - if (dialogResult == YES_OPTION) - expCurve.setRange(new Range(a, b)); - - } - -} \ No newline at end of file + var gbc = new GridBagConstraints(); + gbc.fill = BOTH; + gbc.weightx = 0.25; + + add(rtf.getLowerLimitField(), gbc); + add(rtf.getUpperLimitField(), gbc); + + limitRangeBtn.setText("Set Range"); + limitRangeBtn.addActionListener(e -> { + var lower = ((Number) rtf.getLowerLimitField().getValue()).doubleValue(); + var upper = ((Number) rtf.getUpperLimitField().getValue()).doubleValue(); + validateRange(lower, upper); + notifyPlot(); + }); + + gbc.weightx = 0.25; + add(limitRangeBtn, gbc); + + adiabaticSolutionBtn.setToolTipText("Sanity check (original adiabatic solution)"); + + adiabaticSolutionBtn.addActionListener(e -> { + getChart().setZeroApproximationShown(adiabaticSolutionBtn.isSelected()); + notifyPlot(); + }); + + gbc.weightx = 0.08; + add(adiabaticSolutionBtn, gbc); + + residualsBtn.setToolTipText("Plot residuals"); + residualsBtn.setSelected(true); + + residualsBtn.addActionListener(e -> { + getChart().setResidualsShown(residualsBtn.isSelected()); + notifyPlot(); + }); + + add(residualsBtn, gbc); + add(pdfBtn, gbc); + } + + public void addPlotRequestListener(PlotRequestListener plotRequestListener) { + listeners.add(plotRequestListener); + } + + private void notifyPlot() { + listeners.stream().forEach(l -> l.onPlotRequest()); + } + + private void validateRange(double a, double b) { + var task = TaskManager.getManagerInstance().getSelectedTask(); + + if (task == null) { + return; + } + + var expCurve = (ExperimentalData) task.getInput(); + + if (expCurve == null) { + return; + } + + var sb = new StringBuilder(); + + sb.append(Messages.getString("TextWrap.0")) + .append(getString("RangeSelectionFrame.ConfirmationMessage1")) + .append("
") + .append(getString("RangeSelectionFrame.ConfirmationMessage2")); + try { + sb.append(rtf.getLowerLimitField().getFormatter().valueToString(expCurve.getEffectiveStartTime())) + .append(" to ") + .append(rtf.getUpperLimitField().getFormatter().valueToString(expCurve.getEffectiveEndTime()) + ); + } catch (ParseException ex) { + Logger.getLogger(ChartToolbar.class.getName()).log(Level.SEVERE, null, ex); + } + sb.append("
").append(getString("RangeSelectionFrame.ConfirmationMessage3")) + .append(rtf.getLowerLimitField().getText()) + .append(" to ") + .append(rtf.getUpperLimitField().getText()) + .append(Messages.getString("TextWrap.1")); + + String[] options = new String[]{"Apply to all", "Change current", "Cancel"}; + + var dialogResult = showOptionDialog(getWindowAncestor(this), + sb.toString(), "Confirm chocie", JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE, null, options, options[2]); + + if (dialogResult == JOptionPane.NO_OPTION) { + //just set the range for this particular dataset + setRange(expCurve, a, b); + } else if (dialogResult == JOptionPane.YES_OPTION) { + // set range for all available experimental datasets + TaskManager.getManagerInstance().getTaskList() + .stream().forEach((aTask) + -> setRange((ExperimentalData) aTask.getInput(), a, b) + ); + } + + } + + private void setRange(ExperimentalData expCurve, double a, double b) { + if (expCurve.getRange() == null) { + expCurve.setRange(new Range(a, b)); + } else { + expCurve.getRange().setLowerBound(derive(LOWER_BOUND, a)); + expCurve.getRange().setUpperBound(derive(UPPER_BOUND, b)); + } + } + +} diff --git a/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java new file mode 100644 index 00000000..d685da97 --- /dev/null +++ b/src/main/java/pulse/ui/components/panels/DoubleTablePanel.java @@ -0,0 +1,123 @@ +package pulse.ui.components.panels; + +import java.awt.GridBagConstraints; +import static java.awt.GridBagConstraints.BOTH; +import static javax.swing.BorderFactory.createTitledBorder; +import javax.swing.SwingConstants; +import javax.swing.JPanel; +import javax.swing.JTable; +import pulse.properties.NumericProperties; +import pulse.properties.NumericPropertyKeyword; +import pulse.ui.components.models.ParameterTableModel; +import pulse.ui.components.models.SelectedKeysModel; + +public class DoubleTablePanel extends JPanel { + + private javax.swing.JButton moveLeftBtn; + private javax.swing.JButton moveRightBtn; + + public DoubleTablePanel(JTable leftTable, String titleLeft, JTable rightTable, String titleRight) { + + super(); + initComponents(leftTable, titleLeft, rightTable, titleRight); + + moveRightBtn.addActionListener(e -> { + + var model = (SelectedKeysModel) rightTable.getModel(); + NumericPropertyKeyword key = ((ParameterTableModel) leftTable.getModel()) + .getElementAt(leftTable + .convertRowIndexToModel(leftTable.getSelectedRow())); + + if (key != null) { + if (!model.contains(key)) { + model.addElement((NumericPropertyKeyword) key); + var excluded = NumericProperties.def( + (NumericPropertyKeyword) key) + .getExcludeKeywords(); + + for (var aKey : excluded) { + if (model.contains(aKey)) { + model.removeElement(aKey); + } + } + + } + } + + }); + + moveLeftBtn.addActionListener(e -> { + + var model = (SelectedKeysModel) rightTable.getModel(); + NumericPropertyKeyword key = model.getElementAt(rightTable + .convertRowIndexToModel(rightTable.getSelectedRow())); + + if (key != null) { + model.removeElement(key); + } + + }); + + } + + public void initComponents(JTable leftTable, String titleLeft, JTable rightTable, String titleRight) { + var leftScroller = new javax.swing.JScrollPane(); + var rightScroller = new javax.swing.JScrollPane(); + var moveToolbar = new javax.swing.JToolBar(); + moveRightBtn = new javax.swing.JButton(); + moveLeftBtn = new javax.swing.JButton(); + + setPreferredSize(new java.awt.Dimension(650, 400)); + setLayout(new java.awt.GridBagLayout()); + + var borderLeft = createTitledBorder(titleLeft); + leftScroller.setBorder(borderLeft); + + leftTable.setRowHeight(80); + + leftScroller.setViewportView(leftTable); + + var gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(8, 5, 8, 5); + add(leftScroller, gridBagConstraints); + + var borderRight = createTitledBorder(titleRight); + rightScroller.setBorder(borderRight); + + rightTable.setRowHeight(80); + rightScroller.setViewportView(rightTable); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.fill = GridBagConstraints.BOTH; + gridBagConstraints.weightx = 0.5; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 5, 9, 5); + add(rightScroller, gridBagConstraints); + + moveToolbar.setFloatable(false); + moveToolbar.setOrientation(SwingConstants.HORIZONTAL); + moveToolbar.setRollover(true); + + moveRightBtn.setText("\u25BA"); + moveRightBtn.setFocusable(false); + moveRightBtn.setHorizontalTextPosition(SwingConstants.CENTER); + moveRightBtn.setVerticalTextPosition(SwingConstants.BOTTOM); + + moveLeftBtn.setText("\u25C4"); + moveLeftBtn.setFocusable(false); + moveLeftBtn.setHorizontalTextPosition(SwingConstants.CENTER); + moveLeftBtn.setVerticalTextPosition(SwingConstants.BOTTOM); + + moveToolbar.add(moveLeftBtn); + moveToolbar.add(moveRightBtn); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + add(moveToolbar, gridBagConstraints); + } + +} diff --git a/src/main/java/pulse/ui/components/panels/LogToolbar.java b/src/main/java/pulse/ui/components/panels/LogToolbar.java index b9a06c2a..84879f15 100644 --- a/src/main/java/pulse/ui/components/panels/LogToolbar.java +++ b/src/main/java/pulse/ui/components/panels/LogToolbar.java @@ -1,56 +1,59 @@ package pulse.ui.components.panels; -import static javax.swing.SwingConstants.CENTER; -import static pulse.tasks.logs.Log.isVerbose; -import static pulse.tasks.logs.Log.setVerbose; -import static pulse.ui.Launcher.loadIcon; import static pulse.ui.Messages.getString; +import static pulse.util.ImageUtils.loadIcon; +import java.awt.Color; import java.awt.GridLayout; import java.util.ArrayList; import java.util.List; import javax.swing.JButton; import javax.swing.JCheckBox; -import javax.swing.JPanel; +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 JPanel { +public class LogToolbar extends JToolBar { - private final static int ICON_SIZE = 16; - private List listeners; + private final static int ICON_SIZE = 16; + private List listeners; - public LogToolbar() { - initComponents(); - listeners = new ArrayList<>(); - } + public LogToolbar() { + super(); + setFloatable(false); + initComponents(); + listeners = new ArrayList<>(); + } - public void initComponents() { - setLayout(new GridLayout()); + public void initComponents() { + setLayout(new GridLayout()); - var saveLogBtn = new JButton(loadIcon("save.png", ICON_SIZE)); - saveLogBtn.setToolTipText("Save"); + 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); - } + add(saveLogBtn); - public void notifyLog() { - listeners.stream().forEach(l -> l.onLogExportRequest()); - } + add(logmodeCheckbox); + } - public void addLogExportListener(LogExportListener l) { - listeners.add(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 new file mode 100644 index 00000000..770734eb --- /dev/null +++ b/src/main/java/pulse/ui/components/panels/ModelToolbar.java @@ -0,0 +1,63 @@ +package pulse.ui.components.panels; + +import static pulse.util.ImageUtils.loadIcon; + +import java.awt.Color; +import java.awt.Dimension; + +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JToolBar; + +import pulse.tasks.Calculation; +import pulse.tasks.TaskManager; +import pulse.tasks.listeners.TaskRepositoryEvent; + +@SuppressWarnings("serial") +public class ModelToolbar extends JToolBar { + + private final static int ICON_SIZE = 16; + + public ModelToolbar() { + super(); + setOpaque(false); + setFloatable(false); + setRollover(true); + var set = Calculation.getModelSelectionDescriptor().getAllDescriptors(); + var criterionSelection = new JComboBox<>(set.toArray(String[]::new)); + criterionSelection.addActionListener(e + -> Calculation.getModelSelectionDescriptor().setSelectedDescriptor((String) criterionSelection.getSelectedItem()) + ); + criterionSelection.setSelectedItem(Calculation.getModelSelectionDescriptor().getValue()); + + add(new JLabel("Model Selection Criterion: ")); + add(Box.createRigidArea(new Dimension(5, 0))); + add(criterionSelection); + + var doCalc = new JButton(loadIcon("go_estimate.png", ICON_SIZE, Color.WHITE)); + doCalc.setToolTipText("Re-calculate model weights"); + add(Box.createRigidArea(new Dimension(15, 0))); + add(doCalc); + + doCalc.addActionListener(e -> { + var instance = TaskManager.getManagerInstance(); + var t = instance.getSelectedTask(); + t.getStoredCalculations().forEach(c -> c.getModelSelectionCriterion().calcCriterion()); + instance.notifyListeners(new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_CRITERION_SWITCH, t.getIdentifier())); + }); + + var bestSelection = new JButton(loadIcon("best_model.png", ICON_SIZE, Color.RED)); + bestSelection.setToolTipText("Select Best Model"); + add(Box.createRigidArea(new Dimension(15, 0))); + add(bestSelection); + + bestSelection.addActionListener(e -> { + var t = TaskManager.getManagerInstance().getSelectedTask(); + t.switchToBestModel(); + }); + + } + +} diff --git a/src/main/java/pulse/ui/components/panels/OpacitySlider.java b/src/main/java/pulse/ui/components/panels/OpacitySlider.java index 257fdebe..c8ef55ee 100644 --- a/src/main/java/pulse/ui/components/panels/OpacitySlider.java +++ b/src/main/java/pulse/ui/components/panels/OpacitySlider.java @@ -14,33 +14,33 @@ @SuppressWarnings("serial") public class OpacitySlider extends JSlider { - private List listeners; + private List listeners; - private final static float SLIDER_A_COEF = 0.01f; - private final static float SLIDER_B_COEF = 0.04605f; + private final static float SLIDER_A_COEF = 0.01f; + private final static float SLIDER_B_COEF = 0.04605f; - public OpacitySlider() { - initComponents(); - listeners = new ArrayList<>(); - } + public OpacitySlider() { + initComponents(); + listeners = new ArrayList<>(); + } - public void initComponents() { - setBorder(createEmptyBorder(5, 0, 5, 0)); - setOrientation(VERTICAL); - setToolTipText("Slide to change the dataset opacity"); + public void initComponents() { + setBorder(createEmptyBorder(5, 0, 5, 0)); + setOrientation(VERTICAL); + setToolTipText("Slide to change the dataset opacity"); - addChangeListener(e -> { - getChart().setOpacity((float) (SLIDER_A_COEF * exp(SLIDER_B_COEF * getValue()))); - notifyPlot(); - }); - } + addChangeListener(e -> { + getChart().setOpacity((float) (SLIDER_A_COEF * exp(SLIDER_B_COEF * getValue()))); + notifyPlot(); + }); + } - public void addPlotRequestListener(PlotRequestListener plotRequestListener) { - listeners.add(plotRequestListener); - } + public void addPlotRequestListener(PlotRequestListener plotRequestListener) { + listeners.add(plotRequestListener); + } - private void notifyPlot() { - listeners.stream().forEach(l -> l.onPlotRequest()); - } + private void notifyPlot() { + listeners.stream().forEach(l -> l.onPlotRequest()); + } -} \ 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 new file mode 100644 index 00000000..0ddf940b --- /dev/null +++ b/src/main/java/pulse/ui/components/panels/ProblemToolbar.java @@ -0,0 +1,101 @@ +package pulse.ui.components.panels; + +import static java.awt.Toolkit.getDefaultToolkit; +import static java.lang.System.err; +import static javax.swing.JOptionPane.ERROR_MESSAGE; +import static javax.swing.JOptionPane.showMessageDialog; +import static javax.swing.SwingUtilities.getWindowAncestor; +import static pulse.tasks.logs.Status.INCOMPLETE; +import static pulse.ui.Messages.getString; + +import java.awt.Component; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; + +import javax.swing.JButton; +import javax.swing.JToolBar; + +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 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; + +@SuppressWarnings("serial") +public class ProblemToolbar extends JToolBar { + + private JButton btnSimulate; + private LoaderButton btnLoadCv; + private LoaderButton btnLoadDensity; + + public ProblemToolbar() { + super(); + setFloatable(false); + setLayout(new GridLayout()); + + btnSimulate = new JButton(getString("ProblemStatementFrame.SimulateButton")); //$NON-NLS-1$ + add(btnSimulate); + + btnLoadCv = new LoaderButton(HEAT_CAPACITY, + getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ + add(btnLoadCv); + + btnLoadDensity = new LoaderButton(DENSITY, + getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ + add(btnLoadDensity); + + btnSimulate.addActionListener((ActionEvent e) + -> plot(e)); + } + + public static void plot(ActionEvent e) { + var instance = TaskManager.getManagerInstance(); + + if (instance.getSelectedTask() == null) { + instance.selectFirstTask(); + } + + var t = instance.getSelectedTask(); + + var calc = (Calculation) t.getResponse(); + + t.checkProblems(); + var status = t.getStatus(); + + if (status == INCOMPLETE && !status.checkProblemStatementSet()) { + + getDefaultToolkit().beep(); + showMessageDialog(getWindowAncestor((Component) e.getSource()), + calc.getStatus().getMessage(), + getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ + ERROR_MESSAGE); + + } else { + try { + Solver solver = (Solver) calc.getScheme(); + solver.solve(calc.getProblem()); + } catch (SolverException se) { + err.println("Solver of " + t + " has encountered an error. Details: "); + se.printStackTrace(); + } + MainGraphFrame.getInstance().plot(); + TaskControlFrame.getInstance().getPulseFrame().plot(calc); + } + + } + + public void highlightButtons(boolean highlight) { + if (highlight) { + btnLoadDensity.highlightIfNeeded(); + btnLoadCv.highlightIfNeeded(); + } else { + btnLoadDensity.highlight(false); + btnLoadCv.highlight(false); + } + } + +} diff --git a/src/main/java/pulse/ui/components/panels/ResultToolbar.java b/src/main/java/pulse/ui/components/panels/ResultToolbar.java index 877fa725..64a83365 100644 --- a/src/main/java/pulse/ui/components/panels/ResultToolbar.java +++ b/src/main/java/pulse/ui/components/panels/ResultToolbar.java @@ -1,116 +1,119 @@ package pulse.ui.components.panels; -import static pulse.ui.Launcher.loadIcon; +import static pulse.util.ImageUtils.loadIcon; +import java.awt.Color; import java.awt.GridLayout; import java.util.ArrayList; import java.util.List; import javax.swing.JButton; -import javax.swing.JPanel; +import javax.swing.JToolBar; import pulse.ui.components.listeners.ResultRequestListener; @SuppressWarnings("serial") -public class ResultToolbar extends JPanel { +public class ResultToolbar extends JToolBar { - private final static int ICON_SIZE = 16; + private final static int ICON_SIZE = 16; - private JButton deleteEntryBtn; - private JButton mergeBtn; - private JButton undoBtn; - private JButton previewBtn; - private JButton saveResultsBtn; + private JButton deleteEntryBtn; + private JButton mergeBtn; + private JButton undoBtn; + private JButton previewBtn; + private JButton saveResultsBtn; - private List listeners; + private List listeners; - public ResultToolbar() { - initComponents(); - listeners = new ArrayList<>(); - } + public ResultToolbar() { + super(); + this.setFloatable(false); + initComponents(); + listeners = new ArrayList<>(); + } - public void initComponents() { - deleteEntryBtn = new JButton(loadIcon("remove.png", ICON_SIZE)); - mergeBtn = new JButton(loadIcon("merge.png", ICON_SIZE)); - undoBtn = new JButton(loadIcon("reset.png", ICON_SIZE)); - previewBtn = new JButton(loadIcon("preview.png", ICON_SIZE)); - saveResultsBtn = new JButton(loadIcon("save.png", ICON_SIZE)); - setLayout(new GridLayout(5, 0)); + public void initComponents() { + deleteEntryBtn = new JButton(loadIcon("remove.png", ICON_SIZE)); + mergeBtn = new JButton(loadIcon("merge.png", ICON_SIZE)); + undoBtn = new JButton(loadIcon("reset.png", ICON_SIZE)); + previewBtn = new JButton(loadIcon("preview.png", ICON_SIZE)); + saveResultsBtn = new JButton(loadIcon("save.png", ICON_SIZE, Color.white)); + setLayout(new GridLayout(5, 0)); - deleteEntryBtn.setToolTipText("Delete Entry"); - add(deleteEntryBtn); + deleteEntryBtn.setToolTipText("Delete Entry"); + add(deleteEntryBtn); - mergeBtn.setToolTipText("Merge (Auto)"); - add(mergeBtn); + mergeBtn.setToolTipText("Merge (Auto)"); + add(mergeBtn); - undoBtn.setToolTipText("Undo"); - add(undoBtn); + undoBtn.setToolTipText("Undo"); + add(undoBtn); - previewBtn.setToolTipText("Preview"); - add(previewBtn); + previewBtn.setToolTipText("Preview"); + add(previewBtn); - saveResultsBtn.setToolTipText("Save"); - add(saveResultsBtn); + saveResultsBtn.setToolTipText("Save"); + add(saveResultsBtn); - deleteEntryBtn.setEnabled(false); - deleteEntryBtn.addActionListener(e -> notifyDelete()); + deleteEntryBtn.setEnabled(false); + deleteEntryBtn.addActionListener(e -> notifyDelete()); - mergeBtn.setEnabled(false); - mergeBtn.addActionListener(e -> notifyMerge()); + mergeBtn.setEnabled(false); + mergeBtn.addActionListener(e -> notifyMerge()); - undoBtn.setEnabled(false); - undoBtn.addActionListener(e -> notifyUndo()); + undoBtn.setEnabled(false); + undoBtn.addActionListener(e -> notifyUndo()); - previewBtn.setEnabled(false); - previewBtn.addActionListener(e -> notifyPreview()); + previewBtn.setEnabled(false); + previewBtn.addActionListener(e -> notifyPreview()); - saveResultsBtn.setEnabled(false); - saveResultsBtn.addActionListener(e -> notifyExport()); + saveResultsBtn.setEnabled(false); + saveResultsBtn.addActionListener(e -> notifyExport()); - } + } - public void setDeleteEnabled(boolean deleteEnabled) { - deleteEntryBtn.setEnabled(deleteEnabled); - } + public void setDeleteEnabled(boolean deleteEnabled) { + deleteEntryBtn.setEnabled(deleteEnabled); + } - public void setMergeEnabled(boolean mergeEnabled) { - mergeBtn.setEnabled(mergeEnabled); - } + public void setMergeEnabled(boolean mergeEnabled) { + mergeBtn.setEnabled(mergeEnabled); + } - public void setUndoEnabled(boolean undoEnabled) { - undoBtn.setEnabled(undoEnabled); - } + public void setUndoEnabled(boolean undoEnabled) { + undoBtn.setEnabled(undoEnabled); + } - public void setPreviewEnabled(boolean previewEnabled) { - previewBtn.setEnabled(previewEnabled); - } + public void setPreviewEnabled(boolean previewEnabled) { + previewBtn.setEnabled(previewEnabled); + } - public void setExportEnabled(boolean exportEnabled) { - saveResultsBtn.setEnabled(exportEnabled); - } + public void setExportEnabled(boolean exportEnabled) { + saveResultsBtn.setEnabled(exportEnabled); + } - private void notifyDelete() { - listeners.stream().forEach(l -> l.onDeleteRequest()); - } + private void notifyDelete() { + listeners.stream().forEach(l -> l.onDeleteRequest()); + } - private void notifyMerge() { - listeners.stream().forEach(l -> l.onMergeRequest()); - } + private void notifyMerge() { + listeners.stream().forEach(l -> l.onMergeRequest()); + } - private void notifyUndo() { - listeners.stream().forEach(l -> l.onUndoRequest()); - } + private void notifyUndo() { + listeners.stream().forEach(l -> l.onUndoRequest()); + } - private void notifyPreview() { - listeners.stream().forEach(l -> l.onPreviewRequest()); - } + private void notifyPreview() { + listeners.stream().forEach(l -> l.onPreviewRequest()); + } - private void notifyExport() { - listeners.stream().forEach(l -> l.onExportRequest()); - } + private void notifyExport() { + listeners.stream().forEach(l -> l.onExportRequest()); + } - public void addResultRequestListener(ResultRequestListener l) { - listeners.add(l); - } + public void addResultRequestListener(ResultRequestListener l) { + listeners.add(l); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/components/panels/SettingsToolBar.java b/src/main/java/pulse/ui/components/panels/SettingsToolBar.java index 1a64814e..a86cee52 100644 --- a/src/main/java/pulse/ui/components/panels/SettingsToolBar.java +++ b/src/main/java/pulse/ui/components/panels/SettingsToolBar.java @@ -1,11 +1,9 @@ package pulse.ui.components.panels; -import static java.awt.Font.PLAIN; import static java.awt.GridBagConstraints.BOTH; import static javax.swing.Box.createHorizontalStrut; import static pulse.ui.Messages.getString; -import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -21,68 +19,62 @@ public class SettingsToolBar extends JToolBar { - private static final long serialVersionUID = -1171612225785102673L; + private JCheckBox cbSingleStatement, cbHideDetails; - private JCheckBox cbSingleStatement, cbHideDetails; + public SettingsToolBar(PropertyHolderTable... tables) { + super(); + setFloatable(false); - private Font f = new Font(getString("TaskSelectionToolBar.FontName"), PLAIN, 14); //$NON-NLS-1$ + var taskBox = new TaskBox(); - public SettingsToolBar(PropertyHolderTable... tables) { - super(); - setFloatable(false); + cbSingleStatement = new JCheckBox(getString("TaskSelectionToolBar.ApplyToAll")); //$NON-NLS-1$ + cbSingleStatement.setSelected(TaskManager.getManagerInstance().isSingleStatement()); - var taskBox = new TaskBox(); + cbHideDetails = new JCheckBox(getString("TaskSelectionToolBar.Hide")); //$NON-NLS-1$ + cbHideDetails.setSelected(true); - cbSingleStatement = new JCheckBox(getString("TaskSelectionToolBar.ApplyToAll")); //$NON-NLS-1$ - cbSingleStatement.setSelected(TaskManager.getManagerInstance().isSingleStatement()); - cbSingleStatement.setFont(f); + setLayout(new GridBagLayout()); - cbHideDetails = new JCheckBox(getString("TaskSelectionToolBar.Hide")); //$NON-NLS-1$ - cbHideDetails.setSelected(true); - cbHideDetails.setFont(f); + var gbc = new GridBagConstraints(); + gbc.fill = BOTH; + gbc.weightx = 3.0; + gbc.gridx = 0; + gbc.gridy = 0; - setLayout(new GridBagLayout()); + add(taskBox, gbc); - var gbc = new GridBagConstraints(); - gbc.fill = BOTH; - gbc.weightx = 3.0; - gbc.gridx = 0; - gbc.gridy = 0; + gbc.gridx = 1; + gbc.weightx = 1.0; + add(createHorizontalStrut(5), gbc); - add(taskBox, gbc); + gbc.gridx = 2; + gbc.weightx = 1.0; - gbc.gridx = 1; - gbc.weightx = 1.0; - add(createHorizontalStrut(5), gbc); + add(cbSingleStatement, gbc); - gbc.gridx = 2; - gbc.weightx = 1.0; + cbSingleStatement.addChangeListener(e -> TaskManager.getManagerInstance().setSingleStatement(cbSingleStatement.isSelected())); - add(cbSingleStatement, gbc); + gbc.gridx = 3; - cbSingleStatement.addChangeListener(e -> TaskManager.getManagerInstance().setSingleStatement(cbSingleStatement.isSelected())); + add(cbHideDetails, gbc); - gbc.gridx = 3; + cbHideDetails.addChangeListener((ChangeEvent e) -> { + var selected = cbHideDetails.isSelected(); + Problem.setDetailsHidden(selected); + DifferenceScheme.setDetailsHidden(selected); + for (var table : tables) { + table.updateTable(); + } + }); - add(cbHideDetails, gbc); + } - cbHideDetails.addChangeListener((ChangeEvent e) -> { - var selected = cbHideDetails.isSelected(); - Problem.setDetailsHidden(selected); - DifferenceScheme.setDetailsHidden(selected); - for (var table : tables) { - table.updateTable(); - } - }); + public JCheckBox getHideDetailsCheckBox() { + return cbHideDetails; + } - } - - public JCheckBox getHideDetailsCheckBox() { - return cbHideDetails; - } - - public JCheckBox getSingleStatementCheckBox() { - return cbHideDetails; - } + public JCheckBox getSingleStatementCheckBox() { + return cbHideDetails; + } } diff --git a/src/main/java/pulse/ui/components/panels/SystemPanel.java b/src/main/java/pulse/ui/components/panels/SystemPanel.java index 485eecff..0fe6a200 100644 --- a/src/main/java/pulse/ui/components/panels/SystemPanel.java +++ b/src/main/java/pulse/ui/components/panels/SystemPanel.java @@ -1,96 +1,84 @@ package pulse.ui.components.panels; -import static java.awt.Color.black; import static java.awt.Color.red; -import static java.awt.Color.yellow; import static java.lang.String.format; import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; import static java.util.concurrent.TimeUnit.SECONDS; import static javax.swing.SwingConstants.CENTER; import static javax.swing.SwingConstants.LEFT; import static javax.swing.SwingConstants.RIGHT; -import static pulse.ui.Launcher.cpuUsage; -import static pulse.ui.Launcher.getMemoryUsage; -import static pulse.ui.Launcher.threadsAvailable; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.UIManager; + +import pulse.util.ImageUtils; +import pulse.util.ResourceMonitor; @SuppressWarnings("serial") public class SystemPanel extends JPanel { - private JLabel coresLabel; - private JLabel cpuLabel; - private JLabel memoryLabel; - - public SystemPanel() { - initComponents(); - startSystemMonitors(); - } - - private void initComponents() { - coresLabel = new JLabel(); - cpuLabel = new JLabel(); - memoryLabel = new JLabel(); - - setLayout(new GridBagLayout()); - var gridBagConstraints = new GridBagConstraints(); - - cpuLabel.setHorizontalAlignment(LEFT); - cpuLabel.setText("CPU:"); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.weightx = 2.5; - add(cpuLabel, gridBagConstraints); - - memoryLabel.setHorizontalAlignment(CENTER); - memoryLabel.setText("Memory:"); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.weightx = 2.5; - add(memoryLabel, gridBagConstraints); - - coresLabel.setHorizontalAlignment(RIGHT); - coresLabel.setText("{n cores} "); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.weightx = 2.5; - add(coresLabel, gridBagConstraints); - } - - private void startSystemMonitors() { - var coresAvailable = format("{" + (threadsAvailable() + 1) + " cores}"); - coresLabel.setText(coresAvailable); - - var executor = newSingleThreadScheduledExecutor(); - - Runnable periodicTask = () -> { - var cpuUsage = cpuUsage(); - var memoryUsage = getMemoryUsage(); - var cpuString = format("CPU usage: %3.1f%%", cpuUsage); - cpuLabel.setText(cpuString); - var memoryString = format("Memory usage: %3.1f%%", memoryUsage); - memoryLabel.setText(memoryString); - if (cpuUsage > 75) { - cpuLabel.setForeground(red); - } else if (cpuUsage > 50) { - cpuLabel.setForeground(yellow); - } else { - cpuLabel.setForeground(black); - } - /* - * - */ - if (memoryUsage > 75) { - memoryLabel.setForeground(red); - } else if (memoryUsage > 50) { - memoryLabel.setForeground(yellow); - } else { - memoryLabel.setForeground(black); - } - }; - - executor.scheduleAtFixedRate(periodicTask, 0, 2, SECONDS); - } - -} \ No newline at end of file + private JLabel coresLabel; + private JLabel cpuLabel; + private JLabel memoryLabel; + + public SystemPanel() { + initComponents(); + startSystemMonitors(); + } + + private void initComponents() { + coresLabel = new JLabel(); + cpuLabel = new JLabel(); + memoryLabel = new JLabel(); + + setLayout(new GridBagLayout()); + var gridBagConstraints = new GridBagConstraints(); + + cpuLabel.setHorizontalAlignment(LEFT); + cpuLabel.setText("CPU:"); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.weightx = 2.5; + add(cpuLabel, gridBagConstraints); + + memoryLabel.setHorizontalAlignment(CENTER); + memoryLabel.setText("Memory:"); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.weightx = 2.5; + add(memoryLabel, gridBagConstraints); + + coresLabel.setHorizontalAlignment(RIGHT); + coresLabel.setText("{n cores} "); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.weightx = 2.5; + add(coresLabel, gridBagConstraints); + } + + private void startSystemMonitors() { + var monitor = ResourceMonitor.getInstance(); + + var coresAvailable = format("{" + (monitor.getThreadsAvailable() + 1) + " cores}"); + coresLabel.setText(coresAvailable); + + var executor = newSingleThreadScheduledExecutor(); + var defColor = UIManager.getColor("Label.foreground"); + + Runnable periodicTask = () -> { + monitor.update(); + var cpuString = format("CPU usage: %3.1f%%", monitor.getCpuUsage()); + cpuLabel.setText(cpuString); + var memoryString = format("Memory usage: %3.1f%%", monitor.getMemoryUsage()); + memoryLabel.setText(memoryString); + + cpuLabel.setForeground(ImageUtils.blend(defColor, red, (float) monitor.getCpuUsage() / 100)); + memoryLabel.setForeground(ImageUtils.blend(defColor, red, (float) monitor.getMemoryUsage() / 100)); + + }; + + executor.scheduleAtFixedRate(periodicTask, 0, 2, SECONDS); + } + +} diff --git a/src/main/java/pulse/ui/components/panels/TaskToolbar.java b/src/main/java/pulse/ui/components/panels/TaskToolbar.java index b5e99025..522a6f3b 100644 --- a/src/main/java/pulse/ui/components/panels/TaskToolbar.java +++ b/src/main/java/pulse/ui/components/panels/TaskToolbar.java @@ -1,117 +1,132 @@ package pulse.ui.components.panels; -import static pulse.ui.Launcher.loadIcon; +import static java.awt.Color.black; +import static java.awt.Color.red; +import static pulse.util.ImageUtils.blend; +import static pulse.util.ImageUtils.loadIcon; import java.awt.GridLayout; import java.util.ArrayList; import java.util.List; import javax.swing.JButton; -import javax.swing.JPanel; +import javax.swing.JToolBar; import pulse.tasks.TaskManager; import pulse.ui.components.buttons.ExecutionButton; import pulse.ui.components.listeners.TaskActionListener; @SuppressWarnings("serial") -public class TaskToolbar extends JPanel { - - private final static int ICON_SIZE = 16; - - private JButton removeBtn; - private JButton clearBtn; - private JButton graphBtn; - private JButton execBtn; - private JButton resetBtn; - - private List listeners; - - public TaskToolbar() { - initComponents(); - listeners = new ArrayList<>(); - addButtonListeners(); - } - - private void initComponents() { - - removeBtn = new JButton(loadIcon("remove.png", ICON_SIZE)); - clearBtn = new JButton(loadIcon("clear.png", ICON_SIZE)); - resetBtn = new JButton(loadIcon("reset.png", ICON_SIZE)); - graphBtn = new JButton(loadIcon("graph.png", ICON_SIZE)); - execBtn = new ExecutionButton(); - - setLayout(new GridLayout(1, 0)); - - removeBtn.setEnabled(false); - clearBtn.setEnabled(false); - resetBtn.setEnabled(false); - graphBtn.setEnabled(false); - execBtn.setEnabled(false); - - removeBtn.setToolTipText("Remove Task"); - add(removeBtn); - - clearBtn.setToolTipText("Clear All Tasks"); - add(clearBtn); - - resetBtn.setToolTipText("Reset All Tasks"); - add(resetBtn); - - graphBtn.setToolTipText("Show Graph"); - add(graphBtn); - - execBtn.setToolTipText("Execute All Tasks"); - add(execBtn); - } - - public void setRemoveEnabled(boolean b) { - removeBtn.setEnabled(b); - } - - public void setClearEnabled(boolean b) { - clearBtn.setEnabled(b); - } - - public void setGraphEnabled(boolean b) { - graphBtn.setEnabled(b); - } - - public void setExecEnabled(boolean b) { - execBtn.setEnabled(b); - } - - public void setResetEnabled(boolean b) { - resetBtn.setEnabled(b); - } - - private void addButtonListeners() { - removeBtn.addActionListener(e -> notifyRemove()); - clearBtn.addActionListener(e -> notifyClear()); - resetBtn.addActionListener(e -> { - TaskManager.getManagerInstance().reset(); - notifyReset(); - }); - graphBtn.addActionListener(e -> notifyGraph()); - } - - public void notifyRemove() { - listeners.stream().forEach(l -> l.onRemoveRequest()); - } - - public void notifyClear() { - listeners.stream().forEach(l -> l.onClearRequest()); - } - - public void notifyReset() { - listeners.stream().forEach(l -> l.onResetRequest()); - } - - public void notifyGraph() { - listeners.stream().forEach(l -> l.onGraphRequest()); - } - - public void addTaskActionListener(TaskActionListener l) { - listeners.add(l); - } - -} \ No newline at end of file +public class TaskToolbar extends JToolBar { + + private final static int ICON_SIZE = 16; + + private JButton removeBtn; + private JButton clearBtn; + private JButton graphBtn; + private JButton execBtn; + private JButton resetBtn; + + private List listeners; + + public TaskToolbar() { + super(); + setFloatable(false); + initComponents(); + listeners = new ArrayList<>(); + addButtonListeners(); + } + + private void initComponents() { + + removeBtn = new JButton(loadIcon("remove.png", ICON_SIZE)); + clearBtn = new JButton(loadIcon("clear.png", ICON_SIZE, blend(red, black, 0.5f))); + resetBtn = new JButton(loadIcon("reset.png", ICON_SIZE)); + graphBtn = new JButton(loadIcon("graph.png", ICON_SIZE)); + execBtn = new ExecutionButton(); + + setLayout(new GridLayout(1, 0)); + + removeBtn.setEnabled(false); + clearBtn.setEnabled(false); + resetBtn.setEnabled(false); + graphBtn.setEnabled(false); + execBtn.setEnabled(false); + + removeBtn.setToolTipText("Remove Task"); + add(removeBtn); + + clearBtn.setToolTipText("Clear All Tasks"); + add(clearBtn); + + resetBtn.setToolTipText("Reset All Tasks"); + add(resetBtn); + + graphBtn.setToolTipText("Show Graph"); + add(graphBtn); + + execBtn.setToolTipText("Execute All Tasks"); + add(execBtn); + } + + public void resetSession() { + ((ExecutionButton)execBtn).resetSession(); + } + + public void setRemoveEnabled(boolean b) { + removeBtn.setEnabled(b); + } + + public void setClearEnabled(boolean b) { + clearBtn.setEnabled(b); + } + + public void setGraphEnabled(boolean b) { + graphBtn.setEnabled(b); + } + + public void setExecEnabled(boolean b) { + execBtn.setEnabled(b); + } + + public void setResetEnabled(boolean b) { + resetBtn.setEnabled(b); + } + + private void addButtonListeners() { + removeBtn.addActionListener(e -> notifyRemove()); + clearBtn.addActionListener(e -> notifyClear()); + resetBtn.addActionListener(e -> { + TaskManager.getManagerInstance().reset(); + notifyReset(); + }); + graphBtn.addActionListener(e -> notifyGraph()); + } + + public void notifyRemove() { + listeners.stream().forEach(l -> l.onRemoveRequest()); + } + + public void notifyClear() { + listeners.stream().forEach(l -> l.onClearRequest()); + } + + public void notifyReset() { + listeners.stream().forEach(l -> l.onResetRequest()); + } + + public void notifyGraph() { + listeners.stream().forEach(l -> l.onGraphRequest()); + } + + public void addTaskActionListener(TaskActionListener l) { + listeners.add(l); + } + + public void removeListeners() { + if(listeners != null) { + listeners.clear(); + } + } + +} diff --git a/src/main/java/pulse/ui/components/panels/package-info.java b/src/main/java/pulse/ui/components/panels/package-info.java index 458922b9..6a17ceb3 100644 --- a/src/main/java/pulse/ui/components/panels/package-info.java +++ b/src/main/java/pulse/ui/components/panels/package-info.java @@ -1 +1 @@ -package pulse.ui.components.panels; \ No newline at end of file +package pulse.ui.components.panels; diff --git a/src/main/java/pulse/ui/frames/AuxGraphFrame.java b/src/main/java/pulse/ui/frames/AuxGraphFrame.java deleted file mode 100644 index e0f8b82a..00000000 --- a/src/main/java/pulse/ui/frames/AuxGraphFrame.java +++ /dev/null @@ -1,47 +0,0 @@ -package pulse.ui.frames; - -import static java.awt.BorderLayout.CENTER; - -import javax.swing.JInternalFrame; - -import pulse.tasks.TaskManager; -import pulse.ui.components.AuxChart; - -@SuppressWarnings("serial") -public class AuxGraphFrame extends JInternalFrame { - - private static AuxChart chart; - private static AuxGraphFrame instance = new AuxGraphFrame(); - - private AuxGraphFrame() { - super("Laser Pulse", true, false, true, true); - initComponents(); - setVisible(true); - } - - private void initComponents() { - chart = new AuxChart(); - var chartPanel = chart.getChartPanel(); - getContentPane().add(chartPanel, CENTER); - - chartPanel.setMaximumDrawHeight(2000); - chartPanel.setMaximumDrawWidth(2000); - chartPanel.setMinimumDrawWidth(10); - chartPanel.setMinimumDrawHeight(10); - } - - public void plot() { - var task = TaskManager.getManagerInstance().getSelectedTask(); - if (task != null) - chart.plot(task.getProblem()); - } - - public static AuxChart getChart() { - return chart; - } - - public static AuxGraphFrame getInstance() { - return instance; - } - -} \ No newline at end of file diff --git a/src/main/java/pulse/ui/frames/DataFrame.java b/src/main/java/pulse/ui/frames/DataFrame.java index 34803b1d..152b1a70 100644 --- a/src/main/java/pulse/ui/frames/DataFrame.java +++ b/src/main/java/pulse/ui/frames/DataFrame.java @@ -2,13 +2,10 @@ import static java.awt.BorderLayout.CENTER; import static java.awt.Color.BLACK; -import static java.awt.Font.PLAIN; import static java.awt.Window.Type.UTILITY; -import static pulse.ui.Messages.getString; import java.awt.BorderLayout; import java.awt.Component; -import java.awt.Font; import javax.swing.JFrame; import javax.swing.JPanel; @@ -19,61 +16,60 @@ public class DataFrame extends JFrame { - private static final long serialVersionUID = 1L; - private JPanel contentPane; - private PropertyHolderTable dataTable; - private Component ancestorFrame; - private PropertyHolder dataObject; - private final static Font TABLE_FONT = new Font(getString("DataFrame.FontName"), PLAIN, 16); - private final static int ROW_HEIGHT = 70; + private JPanel contentPane; + private PropertyHolderTable dataTable; + private Component ancestorFrame; + private PropertyHolder dataObject; + private final static int ROW_HEIGHT = 70; - @Override - public void dispose() { - if (ancestorFrame != null) { - ancestorFrame.setEnabled(true); - if (ancestorFrame.getParent() != null) - ancestorFrame.getParent().setEnabled(true); - } - super.dispose(); - } + @Override + public void dispose() { + if (ancestorFrame != null) { + ancestorFrame.setEnabled(true); + if (ancestorFrame.getParent() != null) { + ancestorFrame.getParent().setEnabled(true); + } + } + super.dispose(); + } - /** - * Create the frame. - */ - public DataFrame(PropertyHolder dataObject, Component ancestor) { - setDefaultCloseOperation(DISPOSE_ON_CLOSE); - setLocationRelativeTo(ancestor); - this.ancestorFrame = ancestor.getParent(); - this.dataObject = dataObject; - if (ancestor != null) { - ancestor.setEnabled(false); - if (ancestorFrame != null) - ancestorFrame.setEnabled(false); - } - setType(UTILITY); - setResizable(false); - setAlwaysOnTop(true); - setTitle(dataObject.getClass().getSimpleName() + " properties"); + /** + * Create the frame. + */ + public DataFrame(PropertyHolder dataObject, Component ancestor) { + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setLocationRelativeTo(ancestor); + this.ancestorFrame = ancestor.getParent(); + this.dataObject = dataObject; + if (ancestor != null) { + ancestor.setEnabled(false); + if (ancestorFrame != null) { + ancestorFrame.setEnabled(false); + } + } + setType(UTILITY); + setResizable(false); + setAlwaysOnTop(true); + setTitle(dataObject.getClass().getSimpleName() + " properties"); - contentPane = new JPanel(); - contentPane.setForeground(BLACK); - contentPane.setBorder(null); - contentPane.setLayout(new BorderLayout(0, 0)); - setContentPane(contentPane); - var scrollPane = new JScrollPane(); - contentPane.add(scrollPane, CENTER); + contentPane = new JPanel(); + contentPane.setForeground(BLACK); + contentPane.setBorder(null); + contentPane.setLayout(new BorderLayout(0, 0)); + setContentPane(contentPane); + var scrollPane = new JScrollPane(); + contentPane.add(scrollPane, CENTER); - dataTable = new PropertyHolderTable(dataObject); - dataTable.setFont(TABLE_FONT); - dataTable.setRowHeight(ROW_HEIGHT); - - setBounds(100, 100, 600, 450); + dataTable = new PropertyHolderTable(dataObject); + dataTable.setRowHeight(ROW_HEIGHT); - scrollPane.setViewportView(dataTable); - } + setBounds(100, 100, 600, 450); - public PropertyHolder getDataObject() { - return dataObject; - } + scrollPane.setViewportView(dataTable); + } + + public PropertyHolder getDataObject() { + return dataObject; + } } diff --git a/src/main/java/pulse/ui/frames/ExternalGraphFrame.java b/src/main/java/pulse/ui/frames/ExternalGraphFrame.java new file mode 100644 index 00000000..358c373f --- /dev/null +++ b/src/main/java/pulse/ui/frames/ExternalGraphFrame.java @@ -0,0 +1,38 @@ +package pulse.ui.frames; + +import static java.awt.BorderLayout.CENTER; + +import java.awt.Dimension; + +import javax.swing.JFrame; +import javax.swing.WindowConstants; + +import pulse.ui.components.AuxPlotter; + +@SuppressWarnings("serial") +public class ExternalGraphFrame extends JFrame { + + private AuxPlotter chart; + + public ExternalGraphFrame(String name, AuxPlotter chart, final int width, final int height) { + super(name); + this.chart = chart; + initComponents(chart); + this.setSize(new Dimension(width, height)); + setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); + } + + private void initComponents(AuxPlotter chart) { + var chartPanel = chart.getChartPanel(); + getContentPane().add(chartPanel, CENTER); + } + + public void plot(T t) { + chart.plot(t); + } + + public AuxPlotter getChart() { + return chart; + } + +} diff --git a/src/main/java/pulse/ui/frames/HistogramFrame.java b/src/main/java/pulse/ui/frames/HistogramFrame.java new file mode 100644 index 00000000..9debcdc8 --- /dev/null +++ b/src/main/java/pulse/ui/frames/HistogramFrame.java @@ -0,0 +1,40 @@ +package pulse.ui.frames; + +import static java.awt.BorderLayout.SOUTH; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSeparator; +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; + +@SuppressWarnings("serial") +public class HistogramFrame extends ExternalGraphFrame { + + public HistogramFrame(AuxPlotter chart, int width, int height) { + super("Residuals PDF", chart, width, height); + this.getChart().getChartPanel().setBorder(BorderFactory.createRaisedSoftBevelBorder()); + var slider = new JSlider(8, 100, 20); + var panel = new JPanel(); + var info = new JLabel("Number of bins: " + ((ResidualsChart) chart).getBinCount()); + panel.add(info); + panel.add(new JSeparator()); + panel.add(slider); + panel.setBorder(BorderFactory.createRaisedSoftBevelBorder()); + getContentPane().add(panel, SOUTH); + slider.addChangeListener(e -> { + ((ResidualsChart) chart).setBinCount(slider.getValue()); + 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/InternalGraphFrame.java b/src/main/java/pulse/ui/frames/InternalGraphFrame.java new file mode 100644 index 00000000..44239f81 --- /dev/null +++ b/src/main/java/pulse/ui/frames/InternalGraphFrame.java @@ -0,0 +1,39 @@ +package pulse.ui.frames; + +import static java.awt.BorderLayout.CENTER; + +import javax.swing.JInternalFrame; + +import pulse.ui.components.AuxPlotter; + +@SuppressWarnings("serial") +public class InternalGraphFrame extends JInternalFrame { + + private AuxPlotter chart; + + public InternalGraphFrame(String name, AuxPlotter chart) { + super(name, true, false, true, true); + this.chart = chart; + initComponents(chart); + setVisible(true); + } + + private void initComponents(AuxPlotter chart) { + var chartPanel = chart.getChartPanel(); + getContentPane().add(chartPanel, CENTER); + + chartPanel.setMaximumDrawHeight(2000); + chartPanel.setMaximumDrawWidth(2000); + chartPanel.setMinimumDrawWidth(10); + chartPanel.setMinimumDrawHeight(10); + } + + public void plot(T t) { + chart.plot(t); + } + + public AuxPlotter getChart() { + return chart; + } + +} diff --git a/src/main/java/pulse/ui/frames/LogFrame.java b/src/main/java/pulse/ui/frames/LogFrame.java index d68ffaa2..0db89c6b 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,97 +13,124 @@ 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.ui.components.LogPane; +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; -@SuppressWarnings("serial") public class LogFrame extends JInternalFrame { - private LogPane logTextPane; + 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); - initComponents(); - scheduleLogEvents(); - setVisible(true); - } + public LogFrame() { + super("Log", true, false, true, true); + initComponents(); + scheduleLogEvents(); + setVisible(true); + } - private void initComponents() { - logTextPane = new LogPane(); - var logScroller = new JScrollPane(); - logScroller.setViewportView(logTextPane); + private void initComponents() { + logger = Log.isGraphicalLog() ? graphical : text; - getContentPane().setLayout(new BorderLayout()); - getContentPane().add(logScroller, CENTER); + getContentPane().setLayout(new BorderLayout()); + getContentPane().add(logger.getGUIComponent(), CENTER); - var gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.anchor = WEST; - gridBagConstraints.weightx = 0.5; + var gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.anchor = WEST; + gridBagConstraints.weightx = 0.5; - getContentPane().add(new SystemPanel(), PAGE_END); + getContentPane().add(new SystemPanel(), PAGE_END); - var logToolbar = new LogToolbar(); - logToolbar.addLogExportListener(() -> { - if (logTextPane.getDocument().getLength() > 0) - askToExport(logTextPane, (JFrame) getWindowAncestor(this), - getString("LogToolBar.FileFormatDescriptor")); - }); - getContentPane().add(logToolbar, NORTH); + var logToolbar = new LogToolbar(); - } + 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!"); + } + } - private void scheduleLogEvents() { - var instance = TaskManager.getManagerInstance(); - instance.addSelectionListener(e -> logTextPane.printAll()); + @Override + public void onLogModeChanged(boolean graphical) { + SwingUtilities.invokeLater(() -> setGraphicalLogger(graphical)); + } + }; - instance.addTaskRepositoryListener(event -> { - if (event.getState() != TASK_ADDED) - return; + logToolbar.addLogListener(lel); + getContentPane().add(logToolbar, NORTH); - var task = instance.getTask(event.getId()); + } - task.getLog().addListener(new LogEntryListener() { + public void scheduleLogEvents() { + var instance = TaskManager.getManagerInstance(); + instance.addSelectionListener( + e -> SwingUtilities.invokeLater(() -> logger.postAll())); - @Override - public void onLogFinished(Log log) { - if (instance.getSelectedTask() == task) { + instance.addTaskRepositoryListener(event -> { + if (event.getState() != TASK_ADDED) { + return; + } - try { - logTextPane.getUpdateExecutor().awaitTermination(10, MILLISECONDS); - } catch (InterruptedException e) { - err.println("Log not finished in time"); - e.printStackTrace(); - } + var task = instance.getTask(event.getId()); - logTextPane.printTimeTaken(log); + task.getLog().addListener(new LogEntryListener() { - } - } + @Override + public void onLogFinished(Log log) { + if (instance.getSelectedTask() == task) { - @Override - public void onNewEntry(LogEntry e) { - if (instance.getSelectedTask() == task) - logTextPane.callUpdate(); - } + try { + logger.getUpdateExecutor().awaitTermination(10, MILLISECONDS); + } catch (InterruptedException e) { + err.println("Log not finished in time"); + } - } + logger.printTimeTaken(log); - ); + } + } - }); - } + @Override + public void onNewEntry(LogEntry e) { + if (instance.getSelectedTask() == task) { + logger.callUpdate(); + } + } - public LogPane getLogTextPane() { - return logTextPane; - } + } + ); -} \ No newline at end of file + }); + } + + 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); + SwingUtilities.invokeLater(() -> logger.postAll()); + } + + } + +} diff --git a/src/main/java/pulse/ui/frames/MainGraphFrame.java b/src/main/java/pulse/ui/frames/MainGraphFrame.java index 9e8ebac6..d31f5700 100644 --- a/src/main/java/pulse/ui/frames/MainGraphFrame.java +++ b/src/main/java/pulse/ui/frames/MainGraphFrame.java @@ -3,10 +3,12 @@ import static java.awt.BorderLayout.CENTER; import static java.awt.BorderLayout.LINE_END; import static java.awt.BorderLayout.PAGE_END; +import java.util.concurrent.Executors; import javax.swing.JInternalFrame; import pulse.tasks.TaskManager; +import pulse.tasks.logs.Status; import pulse.ui.components.Chart; import pulse.ui.components.panels.ChartToolbar; import pulse.ui.components.panels.OpacitySlider; @@ -14,45 +16,47 @@ @SuppressWarnings("serial") public class MainGraphFrame extends JInternalFrame { - private static Chart chart; - private static MainGraphFrame instance = new MainGraphFrame(); - - private MainGraphFrame() { - super("Time-temperature profile(s)", true, false, true, true); - initComponents(); - setVisible(true); - } - - private void initComponents() { - chart = new Chart(); - var chartPanel = chart.getChartPanel(); - getContentPane().add(chartPanel, CENTER); - - chartPanel.setMaximumDrawHeight(2000); - chartPanel.setMaximumDrawWidth(2000); - chartPanel.setMinimumDrawWidth(10); - chartPanel.setMinimumDrawHeight(10); - - var opacitySlider = new OpacitySlider(); - opacitySlider.addPlotRequestListener(() -> plot()); - getContentPane().add(opacitySlider, LINE_END); - var chartToolbar = new ChartToolbar(); - chartToolbar.addPlotRequestListener(() -> plot()); - getContentPane().add(chartToolbar, PAGE_END); - } - - public void plot() { - var task = TaskManager.getManagerInstance().getSelectedTask(); - if (task != null) - chart.plot(task, false); - } - - public static Chart getChart() { - return chart; - } - - public static MainGraphFrame getInstance() { - return instance; - } - -} \ No newline at end of file + private static Chart chart; + private static MainGraphFrame instance = new MainGraphFrame(); + + private MainGraphFrame() { + super("Time-temperature profile(s)", true, false, true, true); + initComponents(); + setVisible(true); + } + + private void initComponents() { + chart = new Chart(); + var chartPanel = chart.getChartPanel(); + getContentPane().add(chartPanel, CENTER); + + chartPanel.setMaximumDrawHeight(2000); + chartPanel.setMaximumDrawWidth(2000); + chartPanel.setMinimumDrawWidth(10); + chartPanel.setMinimumDrawHeight(10); + + var opacitySlider = new OpacitySlider(); + opacitySlider.addPlotRequestListener(() -> plot()); + getContentPane().add(opacitySlider, LINE_END); + var chartToolbar = new ChartToolbar(); + chartToolbar.addPlotRequestListener(() -> plot()); + getContentPane().add(chartToolbar, PAGE_END); + } + + public void plot() { + var task = TaskManager.getManagerInstance().getSelectedTask(); + //do not plot tasks that are not finished + if (task != null && task.getStatus() != Status.IN_PROGRESS) { + Executors.newSingleThreadExecutor().submit(() -> chart.plot(task, false)); + } + } + + public static Chart getChart() { + return chart; + } + + public static MainGraphFrame getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/ui/frames/ModelSelectionFrame.java b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java new file mode 100644 index 00000000..1d247024 --- /dev/null +++ b/src/main/java/pulse/ui/frames/ModelSelectionFrame.java @@ -0,0 +1,40 @@ +package pulse.ui.frames; + +import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_BROWSING_REQUEST; + +import java.awt.BorderLayout; +import java.awt.Dimension; + +import javax.swing.JInternalFrame; +import javax.swing.JScrollPane; + +import pulse.tasks.TaskManager; +import pulse.ui.components.CalculationTable; +import pulse.ui.components.panels.ModelToolbar; + +@SuppressWarnings("serial") +public class ModelSelectionFrame extends JInternalFrame { + + private CalculationTable table; + + public ModelSelectionFrame() { + super("Model Comparison", true, true, true, true); + table = new CalculationTable(); + getContentPane().add(new JScrollPane(table)); + 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())); + } + }); + } + +} diff --git a/src/main/java/pulse/ui/frames/PreviewFrame.java b/src/main/java/pulse/ui/frames/PreviewFrame.java index e842d0b4..87a156bd 100644 --- a/src/main/java/pulse/ui/frames/PreviewFrame.java +++ b/src/main/java/pulse/ui/frames/PreviewFrame.java @@ -5,21 +5,19 @@ import static java.awt.BorderLayout.CENTER; import static java.awt.BorderLayout.SOUTH; import static java.awt.Color.BLUE; -import static java.awt.Color.GRAY; import static java.awt.Color.RED; -import static java.awt.Color.white; import static org.jfree.chart.ChartFactory.createScatterPlot; import static org.jfree.chart.plot.PlotOrientation.VERTICAL; import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; -import static pulse.ui.Launcher.loadIcon; +import static pulse.util.ImageUtils.loadIcon; 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.Shape; -import java.awt.geom.Rectangle2D; +import java.awt.Rectangle; import java.util.ArrayList; import java.util.List; @@ -29,235 +27,264 @@ import javax.swing.JSeparator; import javax.swing.JToggleButton; import javax.swing.JToolBar; +import javax.swing.UIManager; +import org.apache.commons.math3.analysis.interpolation.AkimaSplineInterpolator; +import org.apache.commons.math3.analysis.interpolation.LoessInterpolator; +import org.apache.commons.math3.exception.NonMonotonicSequenceException; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.renderer.xy.XYErrorRenderer; -import org.jfree.chart.renderer.xy.XYSplineRenderer; import org.jfree.data.xy.XYIntervalSeries; import org.jfree.data.xy.XYIntervalSeriesCollection; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; +import org.apache.commons.math3.exception.DimensionMismatchException; +import org.apache.commons.math3.exception.NumberIsTooSmallException; +import org.apache.commons.math3.exception.OutOfRangeException; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import pulse.tasks.processing.ResultFormat; +import pulse.ui.components.Chart; @SuppressWarnings("serial") public class PreviewFrame extends JInternalFrame { - private final static int FRAME_WIDTH = 640; - private final static int FRAME_HEIGHT = 480; + private final static int FRAME_WIDTH = 640; + private final static int FRAME_HEIGHT = 480; - private List propertyNames; - private JComboBox selectXBox, selectYBox; + private List propertyNames; + private JComboBox selectXBox, selectYBox; - private static String xLabel, yLabel; + private static String xLabel, yLabel; - private double[][][] data; - private static JFreeChart chart; + private double[][][] data; + private static JFreeChart chart; - private final static Color RESULT_COLOR = BLUE; - private final static Color SMOOTH_COLOR = RED; + private final static Color RESULT_COLOR = BLUE; + private final static Color SMOOTH_COLOR = RED; - private static boolean drawSmooth = true; + private static boolean drawSmooth = true; - private final static int ICON_SIZE = 24; + private final static int ICON_SIZE = 24; + private final static int MARKER_SIZE = 6; + private final static int SPLINE_SAMPLES = 100; - public PreviewFrame() { - super("Preview Plotting", true, true, true, true); - init(); - } + public PreviewFrame() { + super("Preview Plotting", true, true, true, true); + init(); + } - private void init() { - setSize(FRAME_WIDTH, FRAME_HEIGHT); - setDefaultCloseOperation(HIDE_ON_CLOSE); + private void init() { + setSize(FRAME_WIDTH, FRAME_HEIGHT); + setDefaultCloseOperation(HIDE_ON_CLOSE); - getContentPane().setLayout(new BorderLayout()); + getContentPane().setLayout(new BorderLayout()); - getContentPane().add(createEmptyPanel(), CENTER); + getContentPane().add(createEmptyPanel(), CENTER); - var toolbar = new JToolBar(); - toolbar.setFloatable(false); - toolbar.setLayout(new GridLayout()); + var toolbar = new JToolBar(); + toolbar.setFloatable(false); + toolbar.setLayout(new GridLayout()); - getContentPane().add(toolbar, SOUTH); + getContentPane().add(toolbar, SOUTH); - var selectX = new JLabel("Bottom axis: "); - toolbar.add(selectX); + var selectX = new JLabel("Bottom axis: "); + toolbar.add(selectX); - selectXBox = new JComboBox<>(); + selectXBox = new JComboBox<>(); - toolbar.add(selectXBox); - toolbar.add(new JSeparator()); + toolbar.add(selectXBox); + toolbar.add(new JSeparator()); - var selectY = new JLabel("Vertical axis:"); - toolbar.add(selectY); + var selectY = new JLabel("Vertical axis:"); + toolbar.add(selectY); - selectYBox = new JComboBox<>(); - toolbar.add(selectYBox); + selectYBox = new JComboBox<>(); + toolbar.add(selectYBox); - var drawSmoothBtn = new JToggleButton(); - drawSmoothBtn.setToolTipText("Smooth with cubic normal splines"); - drawSmoothBtn.setIcon(loadIcon("spline.png", ICON_SIZE)); - drawSmoothBtn.setSelected(true); - toolbar.add(drawSmoothBtn); + var drawSmoothBtn = new JToggleButton(); + drawSmoothBtn.setToolTipText("Smooth with cubic normal splines"); + drawSmoothBtn.setIcon(loadIcon("spline.png", ICON_SIZE)); + drawSmoothBtn.setSelected(true); + toolbar.add(drawSmoothBtn); - drawSmoothBtn.addActionListener(e -> { - drawSmooth = drawSmoothBtn.isSelected(); - replot(chart); - }); + drawSmoothBtn.addActionListener(e -> { + drawSmooth = drawSmoothBtn.isSelected(); + replot(chart); + }); - selectXBox.addItemListener(e -> replot(chart)); - selectYBox.addItemListener(e -> replot(chart)); - this.setDefaultCloseOperation(EXIT_ON_CLOSE); - } + selectXBox.addItemListener(e -> replot(chart)); + selectYBox.addItemListener(e -> replot(chart)); + this.setDefaultCloseOperation(EXIT_ON_CLOSE); + } - private void replot(JFreeChart chart) { - var selectedX = selectXBox.getSelectedIndex(); - var selectedY = selectYBox.getSelectedIndex(); + private void replot(JFreeChart chart) { + int selectedX = selectXBox.getSelectedIndex(); + int selectedY = selectYBox.getSelectedIndex(); - if (selectedX < 0 || selectedY < 0) - return; + if (selectedX < 0 || selectedY < 0) { + return; + } - xLabel = propertyNames.get(selectedX); - yLabel = propertyNames.get(selectedY); + var plot = chart.getXYPlot(); - var plot = chart.getXYPlot(); + plot.setDataset(0, null); + plot.setDataset(1, null); - plot.setDataset(0, null); - plot.setDataset(1, null); + var dataset = new XYIntervalSeriesCollection(); - plot.getDomainAxis().setLabel(xLabel); - plot.getRangeAxis().setLabel(yLabel); + if (data == null) { + return; + } - var dataset = new XYIntervalSeriesCollection(); - var datasetSmooth = new XYSeriesCollection(); + dataset.addSeries(series(data[selectedX][0], data[selectedX][1], data[selectedY][0], data[selectedY][1])); + plot.setDataset(0, dataset); - if (data == null) - return; + if (drawSmooth) { + drawSmooth(plot, selectedX, selectedY); + } - dataset.addSeries(series(data[selectedX][0], data[selectedX][1], data[selectedY][0], data[selectedY][1])); - plot.setDataset(0, dataset); + } - if (drawSmooth) { - datasetSmooth.addSeries(series(data[selectedX][0], data[selectedY][0])); - plot.setDataset(1, datasetSmooth); - } + private void drawSmooth(XYPlot plot, int selectedX, int selectedY) { + PolynomialSplineFunction interpolation = null; - } + try { + //LOESS interpolator for monotonic x sequence (average results) + //usually works when number of points is large + var interpolator = new LoessInterpolator(); + interpolation = interpolator.interpolate(data[selectedX][0], data[selectedY][0]); + } catch (DimensionMismatchException | NumberIsTooSmallException e) { + //Akima spline for small number of points + var interpolator = new AkimaSplineInterpolator(); + interpolation = interpolator.interpolate(data[selectedX][0], data[selectedY][0]); + } catch (NonMonotonicSequenceException e) { + //do not draw if points not strictly increasing + return; + } - public void update(ResultFormat fmt, double[][][] data) { - this.data = data; - var descriptors = fmt.descriptors(); - List htmlDescriptors = new ArrayList<>(); - var size = descriptors.size(); + double[] x = new double[SPLINE_SAMPLES]; + double[] y = new double[SPLINE_SAMPLES]; - propertyNames = new ArrayList<>(size); - String tmp; + double dx = (data[selectedX][0][data[selectedX][0].length - 1] - data[selectedX][0][0]) / (SPLINE_SAMPLES - 1); - for (var i = 0; i < size; i++) { - tmp = descriptors.get(i).replaceAll("<.*?>", " ").replaceAll("&.*?;", ""); - htmlDescriptors.add("" + descriptors.get(i) + ""); - propertyNames.add(tmp); - } + for (int i = 0; i < SPLINE_SAMPLES; i++) { + x[i] = data[selectedX][0][0] + dx * i; + try { + y[i] = interpolation.value(x[i]); + } catch (OutOfRangeException e) { + y[i] = Double.NaN; + } + } - selectXBox.removeAllItems(); + var datasetSmooth = new XYSeriesCollection(); + datasetSmooth.addSeries(series(x, y)); + plot.setDataset(1, datasetSmooth); + } - for (var s : htmlDescriptors) { - selectXBox.addItem(s); - } + public void update(ResultFormat fmt, double[][][] data) { + this.data = data; + var descriptors = fmt.descriptors(); + var size = descriptors.size(); - selectXBox.setSelectedIndex(fmt.indexOf(TEST_TEMPERATURE)); + propertyNames = new ArrayList<>(size); + String tmp; - selectYBox.removeAllItems(); + selectXBox.removeAllItems(); + selectYBox.removeAllItems(); - for (var s : htmlDescriptors) { - selectYBox.addItem(s); - } - - selectYBox.setSelectedIndex(fmt.indexOf(DIFFUSIVITY)); - } - - /* + for (var s : descriptors) { + selectXBox.addItem(s); + selectYBox.addItem(s); + } + + selectXBox.setSelectedIndex(fmt.indexOf(TEST_TEMPERATURE)); + selectYBox.setSelectedIndex(fmt.indexOf(DIFFUSIVITY)); + } + + /* * - */ - - private static ChartPanel createEmptyPanel() { - chart = createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false); + */ + private static ChartPanel createEmptyPanel() { + chart = createScatterPlot("", xLabel, yLabel, null, VERTICAL, true, true, false); - var renderer = new XYErrorRenderer(); - renderer.setSeriesPaint(0, RESULT_COLOR); + var renderer = new XYErrorRenderer(); + renderer.setSeriesPaint(0, RESULT_COLOR); + renderer.setDefaultShapesFilled(false); - var rendererSpline = new XYSplineRenderer(); - rendererSpline.setSeriesPaint(0, SMOOTH_COLOR); - rendererSpline.setSeriesStroke(0, - new BasicStroke(2.0f, CAP_ROUND, JOIN_ROUND, 1.0f, new float[] { 6.0f, 6.0f }, 0.0f)); + var rendererLine = new XYLineAndShapeRenderer(); + rendererLine.setDefaultShapesVisible(false); + rendererLine.setSeriesPaint(0, SMOOTH_COLOR); + rendererLine.setSeriesStroke(0, + new BasicStroke(2.0f, CAP_ROUND, JOIN_ROUND, 1.0f, new float[]{6.0f, 6.0f}, 0.0f)); - var size = 6.0; - var delta = size / 2.0; - Shape shape1 = new Rectangle2D.Double(-delta, -delta, size, size); - renderer.setSeriesShape(0, shape1); + var plot = chart.getXYPlot(); - var plot = chart.getXYPlot(); + plot.setRenderer(0, renderer); + plot.setRenderer(1, rendererLine); - plot.setRenderer(0, renderer); - plot.setRenderer(1, rendererSpline); - plot.setBackgroundPaint(white); + //plot.setRangeGridlinesVisible(false); + //plot.setDomainGridlinesVisible(false); + var fore = UIManager.getColor("Label.foreground"); + plot.setDomainGridlinePaint(fore); - plot.setRangeGridlinesVisible(true); - plot.setRangeGridlinePaint(GRAY); + 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.setDomainGridlinesVisible(true); - plot.setDomainGridlinePaint(GRAY); + chart.removeLegend(); - plot.getRenderer(1).setSeriesPaint(1, SMOOTH_COLOR); - plot.getRenderer(0).setSeriesPaint(0, RESULT_COLOR); + var cp = new ChartPanel(chart); - chart.removeLegend(); + cp.setMaximumDrawHeight(2000); + cp.setMaximumDrawWidth(2000); + cp.setMinimumDrawWidth(10); + cp.setMinimumDrawHeight(10); - var cp = new ChartPanel(chart); + chart.setBackgroundPaint(UIManager.getColor("Panel.background")); + plot.setBackgroundPaint(chart.getBackgroundPaint()); + Chart.setAxisFontColor(plot.getDomainAxis(), fore); + Chart.setAxisFontColor(plot.getRangeAxis(), fore); - cp.setMaximumDrawHeight(2000); - cp.setMaximumDrawWidth(2000); - cp.setMinimumDrawWidth(10); - cp.setMinimumDrawHeight(10); + return cp; + } - return cp; - } - - /* + /* * - */ - - private static XYIntervalSeries series(double[] x, double[] xerr, double[] y, double[] yerr) { - var series = new XYIntervalSeries("Preview"); + */ + private static XYIntervalSeries series(double[] x, double[] xerr, double[] y, double[] yerr) { + var series = new XYIntervalSeries("Preview"); - for (var i = 0; i < x.length; i++) { - series.add(x[i], x[i] - xerr[i], x[i] + xerr[i], y[i], y[i] - yerr[i], y[i] + yerr[i]); - } + for (var i = 0; i < x.length; i++) { + series.add(x[i], x[i] - xerr[i], x[i] + xerr[i], y[i], y[i] - yerr[i], y[i] + yerr[i]); + } - return series; - } + return series; + } - /* + /* * - */ - - private static XYSeries series(double[] x, double[] y) { - var series = new XYSeries("Preview"); + */ + private static XYSeries series(double[] x, double[] y) { + var series = new XYSeries("Preview"); - for (var i = 0; i < x.length; i++) { - series.add(x[i], y[i]); - } + for (var i = 0; i < x.length; i++) { + series.add(x[i], y[i]); + } - return series; - } + return series; + } - public boolean isDrawSmooth() { - return drawSmooth; - } + public boolean isDrawSmooth() { + return drawSmooth; + } - public void setDrawSmooth(boolean drawSmooth) { - PreviewFrame.drawSmooth = drawSmooth; - } + public void setDrawSmooth(boolean drawSmooth) { + PreviewFrame.drawSmooth = drawSmooth; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java index 5d56a91a..9c0bd4ba 100644 --- a/src/main/java/pulse/ui/frames/ProblemStatementFrame.java +++ b/src/main/java/pulse/ui/frames/ProblemStatementFrame.java @@ -3,508 +3,418 @@ import static java.awt.BorderLayout.CENTER; import static java.awt.BorderLayout.NORTH; import static java.awt.BorderLayout.SOUTH; -import static java.awt.Font.BOLD; -import static java.awt.Toolkit.getDefaultToolkit; -import static java.lang.System.err; -import static javax.swing.BorderFactory.createLineBorder; -import static javax.swing.JOptionPane.ERROR_MESSAGE; import static javax.swing.JOptionPane.INFORMATION_MESSAGE; import static javax.swing.JOptionPane.WARNING_MESSAGE; import static javax.swing.JOptionPane.showMessageDialog; import static javax.swing.ListSelectionModel.SINGLE_SELECTION; -import static javax.swing.SwingUtilities.getWindowAncestor; -import static pulse.input.InterpolationDataset.StandartType.DENSITY; -import static pulse.input.InterpolationDataset.StandartType.HEAT_CAPACITY; import static pulse.problem.statements.ProblemComplexity.HIGH; import static pulse.tasks.TaskManager.getManagerInstance; -import static pulse.tasks.logs.Details.INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT; -import static pulse.tasks.logs.Details.MISSING_DIFFERENCE_SCHEME; -import static pulse.tasks.logs.Details.MISSING_PROBLEM_STATEMENT; -import static pulse.tasks.logs.Status.INCOMPLETE; import static pulse.ui.Messages.getString; import static pulse.util.Reflexive.instancesOf; import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; import java.awt.GridLayout; -import java.awt.event.ActionEvent; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; import javax.swing.DefaultListModel; -import javax.swing.DefaultListSelectionModel; -import javax.swing.JButton; import javax.swing.JInternalFrame; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; -import javax.swing.JToolBar; import javax.swing.event.ListSelectionEvent; import javax.swing.table.DefaultTableModel; +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.schemes.solvers.Solver; -import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.Problem; +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.buttons.LoaderButton; -import pulse.ui.components.controllers.ProblemListCellRenderer; +import pulse.ui.components.listeners.ProblemSelectionEvent; +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 { - private PropertyHolderTable problemTable, schemeTable; - private SchemeSelectionList schemeSelectionList; - private ProblemList problemList; + private PropertyHolderTable problemTable; + private PropertyHolderTable schemeTable; + private JList schemeSelectionList; + private ProblemTree problemTree; + private ProblemToolbar toolbar; - private final static int LIST_FONT_SIZE = 12; + private final static List knownProblems = instancesOf(Problem.class); - private final static List knownProblems = instancesOf(Problem.class); + private ExecutorService problemListExecutor; + private ExecutorService schemeListExecutor; + private ExecutorService propertyExecutor; - /** - * Create the frame. - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - public ProblemStatementFrame() { - setResizable(true); - setClosable(true); - setMaximizable(true); - setDefaultCloseOperation(HIDE_ON_CLOSE); + /** + * Create the frame. + */ + public ProblemStatementFrame() { + setResizable(true); + setClosable(true); + setMaximizable(true); + setDefaultCloseOperation(HIDE_ON_CLOSE); - setTitle(getString("ProblemStatementFrame.Title")); //$NON-NLS-1$ + setTitle(getString("ProblemStatementFrame.Title")); //$NON-NLS-1$ - setBounds(100, 100, WIDTH, HEIGHT); + setBounds(100, 100, WIDTH, HEIGHT); - getContentPane().setLayout(new BorderLayout()); + getContentPane().setLayout(new BorderLayout()); - /* + /* * Create a 2x2 grid for lists and tables - */ + */ + var contentPane = new JPanel(); + var layout = new GridLayout(2, 2); + layout.setHgap(5); + layout.setVgap(5); + contentPane.setLayout(layout); + + /* + * Problem selection list and scroller + */ + problemTree = new ProblemTree(knownProblems); + contentPane.add(new JScrollPane(problemTree)); + + problemListExecutor = Executors.newCachedThreadPool(); + schemeListExecutor = Executors.newCachedThreadPool(); + propertyExecutor = Executors.newCachedThreadPool(); + + problemTree.addProblemSelectionListener((ProblemSelectionEvent e) + -> { + if (e.getProblem() == null) { + ((DefaultTableModel) problemTable.getModel()).setRowCount(0); + } else { + changeProblems(e.getProblem(), e.getSource()); + } + }); + + /* + * Scheme list and scroller + */ + schemeSelectionList = new JList<>(); + schemeSelectionList.setSelectionMode(SINGLE_SELECTION); + schemeSelectionList.setModel(new DefaultListModel<>()); - var contentPane = new JPanel(); - var layout = new GridLayout(2, 2); - layout.setHgap(5); - layout.setVgap(5); - contentPane.setLayout(layout); + schemeSelectionList.addListSelectionListener((ListSelectionEvent arg0) -> { + if (TaskControlFrame.getInstance().getMode() == Mode.PROBLEM) { - LoaderButton btnLoadCv, btnLoadDensity; + var selectedValue = schemeSelectionList.getSelectedValue(); - /* - * Problem selection list and scroller - */ + if (selectedValue != null) { - problemList = new ProblemList(); - contentPane.add(new JScrollPane(problemList)); + if (arg0.getValueIsAdjusting() || !(selectedValue instanceof DifferenceScheme)) { + ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); + } else { + changeSchemes(selectedValue); + } - /* - * Scheme list and scroller - */ + } - schemeSelectionList = new SchemeSelectionList(); - schemeSelectionList.setToolTipText(getString("ProblemStatementFrame.PleaseSelect")); //$NON-NLS-1$ + } - var schemeScroller = new JScrollPane(schemeSelectionList); - contentPane.add(schemeScroller); + }); - /* - * Problem details scroller - */ + schemeSelectionList.setToolTipText(getString("ProblemStatementFrame.PleaseSelect")); //$NON-NLS-1$ + + var schemeScroller = new JScrollPane(schemeSelectionList); + contentPane.add(schemeScroller); - problemTable = new PropertyHolderTable(null); - var problemDetailsScroller = new JScrollPane(problemTable); - contentPane.add(problemDetailsScroller); + /* + * Problem details scroller + */ + problemTable = new PropertyHolderTable(null); + var problemDetailsScroller = new JScrollPane(problemTable); + contentPane.add(problemDetailsScroller); - /* + /* * Scheme details table and scroller - */ - - schemeTable = new PropertyHolderTable(null); - var schemeDetailsScroller = new JScrollPane(schemeTable); - contentPane.add(schemeDetailsScroller); - - /* - * Toolbar - */ - - var toolBar = new JToolBar(); - toolBar.setFloatable(false); - toolBar.setLayout(new GridLayout()); - - var btnSimulate = new JButton(getString("ProblemStatementFrame.SimulateButton")); //$NON-NLS-1$ - btnSimulate.setFont(btnSimulate.getFont().deriveFont(BOLD, 14f)); - - var instance = getManagerInstance(); - - // simulate btn listener - - btnSimulate.addActionListener((ActionEvent arg0) -> { - var t = instance.getSelectedTask(); - if (t == null) - return; - if (t.checkProblems() == INCOMPLETE) { - var d = t.getStatus().getDetails(); - if (d == MISSING_PROBLEM_STATEMENT || d == MISSING_DIFFERENCE_SCHEME - || d == INSUFFICIENT_DATA_IN_PROBLEM_STATEMENT) { - getDefaultToolkit().beep(); - showMessageDialog(getWindowAncestor((Component) arg0.getSource()), t.getStatus().getMessage(), - getString("ProblemStatementFrame.ErrorTitle"), //$NON-NLS-1$ - ERROR_MESSAGE); - return; - } - } - try { - ((Solver) t.getScheme()).solve(t.getProblem()); - } catch (SolverException e) { - err.println("Solver of " + t + " has encountered an error. Details: "); - e.printStackTrace(); - } - MainGraphFrame.getInstance().plot(); - AuxGraphFrame.getInstance().plot(); - problemTable.updateTable(); - schemeTable.updateTable(); - }); - - toolBar.add(btnSimulate); - - btnLoadCv = new LoaderButton(getString("ProblemStatementFrame.LoadSpecificHeatButton")); //$NON-NLS-1$ - btnLoadCv.setDataType(HEAT_CAPACITY); - toolBar.add(btnLoadCv); - - btnLoadDensity = new LoaderButton(getString("ProblemStatementFrame.LoadDensityButton")); //$NON-NLS-1$ - btnLoadDensity.setDataType(DENSITY); - toolBar.add(btnLoadDensity); - - problemList.setSelectionModel(new DefaultListSelectionModel() { - - @Override - public void setSelectionInterval(int index0, int index1) { - if (index0 != index1) - return; - - var problem = knownProblems.get(index0); - var enabledFlag = problem.isEnabled(); - - if (enabledFlag) { - super.setSelectionInterval(index0, index0); - problemList.ensureIndexIsVisible(index0); - - if (!problem.isReady()) { - var bred = new Color(1.0f, 0.0f, 0.0f, 0.35f); - btnLoadDensity.setBorder(createLineBorder(bred, 3)); - btnLoadCv.setBorder(createLineBorder(bred, 3)); - } else { - btnLoadDensity.setBorder(null); - btnLoadCv.setBorder(null); - } - - } else - showMessageDialog(null, "This problem statement is not currently supported. Please select another.", - "Feature not supported", WARNING_MESSAGE); - - } - - }); - - /* - * - */ + */ + schemeTable = new PropertyHolderTable(null); + var schemeDetailsScroller = new JScrollPane(schemeTable); + contentPane.add(schemeDetailsScroller); - getContentPane().add(new SettingsToolBar(problemTable, schemeTable), NORTH); - getContentPane().add(contentPane, CENTER); - getContentPane().add(toolBar, SOUTH); + toolbar = new ProblemToolbar(); - /* - * listeners - */ + problemTree.setSelectionModel(new DefaultTreeSelectionModel() { - instance.addSelectionListener((TaskSelectionEvent e) -> - update(instance.getSelectedTask()) - ); - // TODO + @Override + public void setSelectionPath(TreePath path) { + var object = (DefaultMutableTreeNode) path.getLastPathComponent(); - getManagerInstance().addHierarchyListener(event -> { - if (!(event.getSource() instanceof PropertyHolderTable)) - return; + if (!(object.getUserObject() instanceof Problem)) { + super.setSelectionPath(path); + } else { + + var problem = (Problem) object.getUserObject(); + var enabledFlag = problem.isEnabled(); + + if (enabledFlag) { + super.setSelectionPath(path); + toolbar.highlightButtons(!problem.isReady()); + } else { + showMessageDialog(null, getString("problem.notsupportedmessage"), + getString("problem.notsupportedtitle"), WARNING_MESSAGE); + path = null; + } + + } + + } + + }); + + /* + * + */ + getContentPane().add(new SettingsToolBar(problemTable, schemeTable), NORTH); + getContentPane().add(contentPane, CENTER); + getContentPane().add(toolbar, SOUTH); + + resetSession(); + } + + public void resetSession() { + var instance = getManagerInstance(); + instance.addHierarchyListener(event -> { + if ((event.getSource() instanceof PropertyHolderTable) && instance.isSingleStatement()) { + + //for all tasks + instance.getTaskList().stream(). + //select the problem statement of the current calculation + map(t -> ((Calculation) t.getResponse()).getProblem()) + //that is non-null + .filter(problem -> problem != null) + //for each problem, update its properties in a separete thread + .forEach(p -> propertyExecutor.submit(() + -> p.updateProperty(event, event.getProperty()) + ) + ); + + } + + }); + } + + public void update() { + update(getManagerInstance().getSelectedTask()); + } + + private void update(SearchTask selectedTask) { + var calc = (Calculation) selectedTask.getResponse(); + var selectedProblem = calc.getProblem(); + var selectedScheme = calc.getScheme(); + + // problem + if (selectedProblem == null) { + problemTree.clearSelection(); + } else { + problemTree.setSelectedProblem(selectedProblem); + } + + // scheme + if (selectedScheme == null) { + schemeSelectionList.clearSelection(); + } else { + setSelectedElement(schemeSelectionList, selectedScheme); + schemeTable.setPropertyHolder(selectedScheme); + } + } + + private void changeSchemes(DifferenceScheme newScheme) { + var instance = TaskManager.getManagerInstance(); + var selectedTask = instance.getSelectedTask(); + + var tracker = new ProgressDialog(); + tracker.setTitle("Initialising solution schemes..."); + tracker.setLocationRelativeTo(null); + tracker.setAlwaysOnTop(true); + + 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()) { + + var runnables = instance.getTaskList().stream().map(t -> new Runnable() { + @Override + public void run() { + changeScheme(t, newScheme); + tracker.incrementProgress(); + } + + }).collect(Collectors.toList()); + + CompletableFuture.runAsync(() + -> runnables.parallelStream().forEach(c -> c.run())) + .thenRun(finishingTouch); + + } else { + CompletableFuture.runAsync(() -> changeScheme(selectedTask, newScheme)).thenRun(finishingTouch); + } + + } + + private void changeProblems(Problem newlySelectedProblem, Object source) { + + var instance = TaskManager.getManagerInstance(); + var selectedProblem = instance.getSelectedTask(); + + var tracker = new ProgressDialog(); + tracker.setTitle("Changing problem statements..."); + tracker.setLocationRelativeTo(null); + tracker.setAlwaysOnTop(true); + + if (source != instance) { + + tracker.trackProgress(instance.isSingleStatement() ? instance.getTaskList().size() : 1); + + 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()); + defaultModel.clear(); + var schemes = newlySelectedProblem.availableSolutions(); + 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); + } - if (instance.isSingleStatement()) - return; + } + + } - Problem p; + private void changeProblem(SearchTask task, Problem newProblem) { + var data = (ExperimentalData) task.getInput(); + var calc = (Calculation) task.getResponse(); + var oldProblem = calc.getProblem(); // stores previous information + var np = newProblem.copy(); - for (var task : instance.getTaskList()) { - p = task.getProblem(); - if (p != null) - p.updateProperty(event, event.getProperty()); - } + 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); + } - public void update() { - update(getManagerInstance().getSelectedTask()); - } + task.checkProblems(); + toolbar.highlightButtons(!np.isReady()); + } - private void update(SearchTask selectedTask) { + private static void selectDefaultScheme(JList list, Problem p) { + var defaultSchemeClass = p.defaultScheme(); - var selectedProblem = selectedTask == null ? null : selectedTask.getProblem(); - var selectedScheme = selectedTask == null ? null : selectedTask.getScheme(); + var model = list.getModel(); + DifferenceScheme element = null; - // problem + for (int i = 0, size = model.getSize(); i < size; i++) { + element = model.getElementAt(i); - if (selectedProblem == null) - problemList.clearSelection(); - else { - setSelectedElement(problemList, selectedProblem); - problemTable.setPropertyHolder(selectedProblem); + if (defaultSchemeClass.isAssignableFrom(element.getClass())) { + list.setSelectedValue(element, true); + break; + } + } - } + } - // scheme + private void changeScheme(SearchTask task, DifferenceScheme newScheme) { - if (selectedScheme == null) - schemeSelectionList.clearSelection(); - else { - setSelectedElement(schemeSelectionList, selectedScheme); - schemeTable.setPropertyHolder(selectedScheme); - } + // TODO + var calc = (Calculation) task.getResponse(); + var data = (ExperimentalData) task.getInput(); - } + if (calc.getScheme() == null) { + calc.setScheme(newScheme.copy(), data); + } else { - private void changeProblem(SearchTask task, Problem newProblem) { - var oldProblem = task.getProblem(); // stores previous information + var oldScheme = calc.getScheme().copy(); // stores previous information + calc.setScheme(newScheme.copy(), data); // assigns new problem type - var problemClass = newProblem.getClass(); - Constructor problemCopyConstructor = null; + if (newScheme.getClass().getSimpleName().equals(oldScheme.getClass().getSimpleName())) { + calc.getScheme().copyFrom(oldScheme); // copies information from old problem to new problem type + } + oldScheme = null; // deletes reference to old problem - try { - if (newProblem != null) { - problemCopyConstructor = newProblem.getClass().getConstructor(Problem.class); - } - } catch (NoSuchMethodException | SecurityException e) { - err.println(getString("ProblemStatementFrame.ConstructorAccessError") + problemClass); //$NON-NLS-1$ - e.printStackTrace(); - } + } - Problem np = null; + task.checkProblems(); - try { - if (problemCopyConstructor != null && oldProblem != null) - np = problemCopyConstructor.newInstance(oldProblem); - else - np = newProblem.getClass().getDeclaredConstructor().newInstance(); - } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - err.println(getString("ProblemStatementFrame.InvocationError") + problemCopyConstructor); //$NON-NLS-1$ - e.printStackTrace(); - } + } - task.setProblem(np); // copies information from old problem to new problem type - - oldProblem = null; - problemCopyConstructor = null; - problemClass = null; + private void setSelectedElement(JList list, Object o) { + if (o == null) { + list.clearSelection(); + return; + } - task.checkProblems(); + var size = list.getModel().getSize(); + Object fromList = null; + var found = false; - } + for (var i = 0; i < size; i++) { + fromList = list.getModel().getElementAt(i); + if (fromList.toString().equals(o.toString())) { + list.setSelectedIndex(i); + found = true; + } + } - private static void selectDefaultScheme(JList list, Problem p) { - var defaultSchemeClass = p.defaultScheme(); + if (!found) { + list.clearSelection(); + } - var model = list.getModel(); - DifferenceScheme element = null; + } - for (int i = 0, size = model.getSize(); i < size; i++) { - element = model.getElementAt(i); - - if (defaultSchemeClass.isAssignableFrom(element.getClass())) { - list.setSelectedValue(element, true); - break; - } - } - - } - - private void changeScheme(SearchTask task, DifferenceScheme newScheme) { - - // TODO - - if (task.getScheme() == null) { - task.setScheme(newScheme.copy()); - // task.getScheme().setTimeLimit( task.getTimeLimit() ); - } - - else { - - var oldScheme = task.getScheme().copy(); // stores previous information - task.setScheme(null); - task.setScheme(newScheme.copy()); // assigns new problem type - - if (newScheme.getClass().getSimpleName().equals(oldScheme.getClass().getSimpleName())) - task.getScheme().copyFrom(oldScheme); // copies information from old problem to new problem type - // else - // task.getScheme().setTimeLimit( task.getTimeLimit() ); - - oldScheme = null; // deletes reference to old problem - - } - - task.checkProblems(); - - } - - private void setSelectedElement(JList list, Object o) { - if (o == null) { - list.clearSelection(); - return; - } - - var size = list.getModel().getSize(); - Object fromList = null; - var found = false; - - for (var i = 0; i < size; i++) { - fromList = list.getModel().getElementAt(i); - if (fromList.toString().equals(o.toString())) { - list.setSelectedIndex(i); - found = true; - } - } - - if (!found) - list.clearSelection(); - - } - - /* - * ################## Problem List Class ################## - */ - - class ProblemList extends JList { - - public ProblemList() { - super(); - setFont(getFont().deriveFont(LIST_FONT_SIZE)); - this.setCellRenderer(new ProblemListCellRenderer()); - - var listModel = new DefaultListModel(); - for (var p : knownProblems) { - listModel.addElement(p); - } - - setModel(listModel); - setSelectionMode(SINGLE_SELECTION); - - var instance = getManagerInstance(); - - addListSelectionListener((ListSelectionEvent arg0) -> { - if (arg0.getValueIsAdjusting()) - return; - var newlySelectedProblem = getSelectedValue(); - if (newlySelectedProblem == null) { - ((DefaultTableModel) problemTable.getModel()).setRowCount(0); - return; - } - - if (instance.getSelectedTask() == null) { - instance.selectFirstTask(); - } - var selectedTask = instance.getSelectedTask(); - if (TaskManager.getManagerInstance().isSingleStatement()) { - instance.getTaskList().stream().forEach(t -> changeProblem(t, newlySelectedProblem)); - } else { - changeProblem(selectedTask, newlySelectedProblem); - } - listModel.set(listModel.indexOf(newlySelectedProblem), selectedTask.getProblem()); - problemTable.setPropertyHolder(instance.getSelectedTask().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.getProblem()); - schemeSelectionList.setToolTipText(null); - }); - - instance.addSelectionListener((TaskSelectionEvent e) -> { - // select appropriate problem type from list - if (instance.getSelectedTask().getProblem() != null) { - for (var i = 0; i < getModel().getSize(); i++) { - var p = getModel().getElementAt(i); - if (instance.getSelectedTask().getProblem().getClass().equals(p.getClass())) { - setSelectedIndex(i); - break; - } - } - } - // then, select appropriate scheme type - if (instance.getSelectedTask().getScheme() != null) { - for (var i = 0; i < schemeSelectionList.getModel().getSize(); i++) { - if (instance.getSelectedTask().getScheme().getClass() - .equals(schemeSelectionList.getModel().getElementAt(i).getClass())) { - schemeSelectionList.setSelectedIndex(i); - break; - } - } - } - }); - - } - - } - - /* - * ########################### Scheme selection list class - * ########################### - */ - - class SchemeSelectionList extends JList { - - public SchemeSelectionList() { - - super(); - setFont(getFont().deriveFont(LIST_FONT_SIZE)); - setSelectionMode(SINGLE_SELECTION); - var m = new DefaultListModel(); - setModel(m); - // scheme list listener - - addListSelectionListener((ListSelectionEvent arg0) -> { - if (arg0.getValueIsAdjusting()) - return; - if (!(getSelectedValue() instanceof DifferenceScheme)) { - ((DefaultTableModel) schemeTable.getModel()).setRowCount(0); - return; - } - var instance = getManagerInstance(); - var selectedTask = instance.getSelectedTask(); - var newScheme = getSelectedValue(); - if (newScheme == null) - return; - if (instance.isSingleStatement()) { - instance.getTaskList().stream().forEach(t -> changeScheme(t, newScheme)); - } else { - changeScheme(selectedTask, newScheme); - } - schemeTable.setPropertyHolder(selectedTask.getScheme()); - if (selectedTask.getProblem().getComplexity() == HIGH) { - showMessageDialog(null, "

" + "You have selected a " - + "high-complexity problem statement. Calculations will take longer than usual. " - + "You may track the progress of your task with the verbose logging option. Watch out for " - + "timeouts as they typically may occur for multi-variate optimisation when the problem is ill-posed." - + "

", "High complexity", INFORMATION_MESSAGE); - } - }); - - } - - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/frames/ResultFrame.java b/src/main/java/pulse/ui/frames/ResultFrame.java index 3200c7cb..93aa7e8d 100644 --- a/src/main/java/pulse/ui/frames/ResultFrame.java +++ b/src/main/java/pulse/ui/frames/ResultFrame.java @@ -23,107 +23,118 @@ import pulse.ui.components.ResultTable; import pulse.ui.components.listeners.PreviewFrameCreationListener; import pulse.ui.components.listeners.ResultRequestListener; +import pulse.ui.components.models.ResultTableModel; import pulse.ui.components.panels.ResultToolbar; import pulse.ui.frames.dialogs.FormattedInputDialog; @SuppressWarnings("serial") public class ResultFrame extends JInternalFrame { - private ResultToolbar resultToolbar; - private ResultTable resultTable; - private List listeners; - private FormattedInputDialog averageWindowDialog; - - public ResultFrame() { - super("Results", true, false, true, true); - initComponents(); - listeners = new ArrayList<>(); - addListeners(); - setVisible(true); - } - - private void initComponents() { - var resultsScroller = new JScrollPane(); - - resultTable = new ResultTable(getInstance()); - resultsScroller.setViewportView(resultTable); - getContentPane().add(resultsScroller, CENTER); - - resultToolbar = new ResultToolbar(); - getContentPane().add(resultToolbar, EAST); - - averageWindowDialog = new FormattedInputDialog(def(WINDOW)); - } - - private void addListeners() { - resultToolbar.addResultRequestListener(new ResultRequestListener() { - - @Override - public void onDeleteRequest() { - resultTable.deleteSelected(); - } - - @Override - public void onPreviewRequest() { - if (!resultTable.hasEnoughElements(1)) { - showMessageDialog(getWindowAncestor(resultTable), getString("ResultsToolBar.NoDataError"), - getString("ResultsToolBar.NoResultsError"), ERROR_MESSAGE); - } else - notifyPreview(); - } - - @Override - public void onMergeRequest() { - if (resultTable.hasEnoughElements(1)) - showInputDialog(); - } - - @Override - public void onUndoRequest() { - resultTable.undo(); - } - - @Override - public void onExportRequest() { - if (!resultTable.hasEnoughElements(1)) { - showMessageDialog(getWindowAncestor(resultTable), getString("ResultsToolBar.7"), - getString("ResultsToolBar.8"), ERROR_MESSAGE); - return; - } - - askToExport(resultTable, (JFrame) getWindowAncestor(resultTable), "Calculation results"); - } - - }); - - resultTable.getSelectionModel().addListSelectionListener((ListSelectionEvent arg0) -> { - resultToolbar.setDeleteEnabled(!resultTable.isSelectionEmpty()); - }); - - resultTable.getModel().addTableModelListener((TableModelEvent arg0) -> { - resultToolbar.setPreviewEnabled(resultTable.hasEnoughElements(3)); - resultToolbar.setMergeEnabled(resultTable.hasEnoughElements(2)); - resultToolbar.setExportEnabled(resultTable.hasEnoughElements(1)); - resultToolbar.setUndoEnabled(resultTable.hasEnoughElements(1)); - }); - } - - public void notifyPreview() { - listeners.stream().forEach(l -> l.onPreviewFrameRequest()); - } - - public void addFrameCreationListener(PreviewFrameCreationListener l) { - listeners.add(l); - } - - private void showInputDialog() { - averageWindowDialog.setLocationRelativeTo(null); - averageWindowDialog.setVisible(true); - averageWindowDialog.setConfirmAction(() -> resultTable.merge(averageWindowDialog.value().doubleValue())); - } - - public ResultTable getResultTable() { - return resultTable; - } + private ResultToolbar resultToolbar; + private ResultTable resultTable; + private List listeners; + private FormattedInputDialog averageWindowDialog; + + public ResultFrame() { + super("Results", true, false, true, true); + initComponents(); + listeners = new ArrayList<>(); + addListeners(); + setVisible(true); + } + + private void initComponents() { + var resultsScroller = new JScrollPane(); + + resultTable = new ResultTable(getInstance()); + resultsScroller.setViewportView(resultTable); + getContentPane().add(resultsScroller, CENTER); + + resultToolbar = new ResultToolbar(); + getContentPane().add(resultToolbar, EAST); + + averageWindowDialog = new FormattedInputDialog(def(WINDOW)); + } + + private void addListeners() { + resultToolbar.addResultRequestListener(new ResultRequestListener() { + + @Override + public void onDeleteRequest() { + resultTable.deleteSelected(); + } + + @Override + public void onPreviewRequest() { + if (!resultTable.hasEnoughElements(1)) { + showMessageDialog(getWindowAncestor(resultTable), getString("ResultsToolBar.NoDataError"), + getString("ResultsToolBar.NoResultsError"), ERROR_MESSAGE); + } else { + notifyPreview(); + } + } + + @Override + public void onMergeRequest() { + if (resultTable.hasEnoughElements(1)) { + showInputDialog(); + } + } + + @Override + public void onUndoRequest() { + resultTable.undo(); + } + + @Override + public void onExportRequest() { + if (!resultTable.hasEnoughElements(1)) { + showMessageDialog(getWindowAncestor(resultTable), getString("ResultsToolBar.7"), + getString("ResultsToolBar.8"), ERROR_MESSAGE); + return; + } + + askToExport(resultTable, (JFrame) getWindowAncestor(resultTable), "Calculation results"); + } + + }); + + resultTable.getSelectionModel().addListSelectionListener((ListSelectionEvent arg0) -> { + resultToolbar.setDeleteEnabled(!resultTable.isSelectionEmpty()); + }); + + resultTable.getModel().addTableModelListener((TableModelEvent arg0) -> { + resultToolbar.setPreviewEnabled(resultTable.hasEnoughElements(3)); + resultToolbar.setMergeEnabled(resultTable.hasEnoughElements(2)); + resultToolbar.setExportEnabled(resultTable.hasEnoughElements(1)); + resultToolbar.setUndoEnabled(resultTable.hasEnoughElements(1)); + }); + } + + public void notifyPreview() { + listeners.stream().forEach(l -> l.onPreviewFrameRequest()); + } + + public void addFrameCreationListener(PreviewFrameCreationListener l) { + listeners.add(l); + } + + private void showInputDialog() { + averageWindowDialog.setLocationRelativeTo(null); + averageWindowDialog.setVisible(true); + 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 bdf2a690..94306df5 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -4,8 +4,6 @@ import static java.awt.GridBagConstraints.BOTH; import static javax.swing.BorderFactory.createTitledBorder; import static javax.swing.ListSelectionModel.SINGLE_SELECTION; -import static pulse.search.direction.PathOptimiser.getInstance; -import static pulse.search.direction.PathOptimiser.getLinearSolver; import static pulse.search.direction.PathOptimiser.setInstance; import static pulse.ui.Messages.getString; import static pulse.util.Reflexive.instancesOf; @@ -20,197 +18,198 @@ import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; import javax.swing.border.EmptyBorder; import javax.swing.event.ListSelectionEvent; -import javax.swing.table.DefaultTableModel; +import javax.swing.event.TableModelEvent; +import javax.swing.event.TableModelListener; +import pulse.properties.Flag; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; +import static pulse.properties.NumericPropertyKeyword.MAXTEMP; +import pulse.search.direction.ActiveFlags; +import pulse.search.direction.LMOptimiser; import pulse.search.direction.PathOptimiser; -import pulse.search.linear.LinearOptimiser; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; import pulse.ui.components.PropertyHolderTable; import pulse.ui.components.controllers.SearchListRenderer; +import pulse.ui.components.models.ParameterTableModel; +import pulse.ui.components.models.SelectedKeysModel; +import pulse.ui.components.panels.DoubleTablePanel; @SuppressWarnings("serial") public class SearchOptionsFrame extends JInternalFrame { - private PropertyHolderTable pathTable; - private JList linearList; - private PathSolversList pathList; + private final PropertyHolderTable pathTable; + private final JTable leftTable; + 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 static List pathSolvers = instancesOf(PathOptimiser.class); - private final static List linearSolvers = instancesOf(LinearOptimiser.class); + private final NumericPropertyKeyword[] mandatorySelection = new NumericPropertyKeyword[]{MAXTEMP}; - /** - * Create the frame. - */ - public SearchOptionsFrame() { - setClosable(true); - setTitle(getString("SearchOptionsFrame.SelectSearch")); //$NON-NLS-1$ - setDefaultCloseOperation(HIDE_ON_CLOSE); - setBounds(100, 100, WIDTH, HEIGHT); + /** + * Create the frame. + */ + public SearchOptionsFrame() { + setClosable(true); + setTitle(getString("SearchOptionsFrame.SelectSearch")); //$NON-NLS-1$ + setDefaultCloseOperation(HIDE_ON_CLOSE); + setBounds(100, 100, WIDTH, HEIGHT); - /* + /* * Path solver list and scroller - */ - - var panel = new JPanel(); - panel.setBorder(new EmptyBorder(5, 5, 5, 5)); - setContentPane(panel); - - pathList = new PathSolversList(); - var pathListScroller = new JScrollPane(pathList); - pathListScroller.setBorder(createTitledBorder("Select a Direction Search Method")); - - linearList = new LinearSearchList(); - linearList.setEnabled(false); - var linearListScroller = new JScrollPane(linearList); - linearListScroller.setBorder(createTitledBorder("Select a Line Search Method")); - - pathTable = new PropertyHolderTable(null); - - getContentPane().setLayout(new GridBagLayout()); + */ + var panel = new JPanel(); + panel.setBorder(new EmptyBorder(5, 5, 5, 5)); + setContentPane(panel); + + pathList = new PathSolversList(); + var pathListScroller = new JScrollPane(pathList); + pathListScroller.setBorder(createTitledBorder("Select an Optimiser")); + + pathTable = new PropertyHolderTable(null); - var gbc = new GridBagConstraints(); - - gbc.fill = BOTH; - gbc.gridy = 0; - gbc.gridx = 0; - gbc.weightx = 1.0; - gbc.weighty = 0.2; - - getContentPane().add(pathListScroller, gbc); - - gbc.gridy = 1; - - getContentPane().add(linearListScroller, gbc); - - gbc.gridy = 2; - gbc.weighty = 0.6; - - var tableScroller = new JScrollPane(pathTable); - tableScroller.setBorder(createTitledBorder("Select search variables and settings")); - getContentPane().add(tableScroller, gbc); - - } - - public void update() { - var selected = getInstance(); - if (selected != null) { - pathList.setSelectedIndex(pathSolvers.indexOf(selected)); - linearList.setSelectedIndex(linearSolvers.indexOf(getLinearSolver())); - pathTable.updateTable(); - } else { - pathList.clearSelection(); - linearList.clearSelection(); - linearList.setEnabled(false); - } - } - - class PathSolversList extends JList { - - /** - * - */ - private static final long serialVersionUID = 3662972578473909850L; - - public PathSolversList() { - - super(); - - setModel(new AbstractListModel() { - /** - * - */ - private static final long serialVersionUID = -7683200230096704268L; - - @Override - public int getSize() { - return pathSolvers.size(); - } - - @Override - public PathOptimiser getElementAt(int index) { - return pathSolvers.get(index); - } - }); - - setFont(font); - setSelectionMode(SINGLE_SELECTION); - setCellRenderer(new SearchListRenderer()); - - addListSelectionListener((ListSelectionEvent arg0) -> { - if (arg0.getValueIsAdjusting()) - return; - if (!(getSelectedValue() instanceof PathOptimiser)) { - ((DefaultTableModel) pathTable.getModel()).setRowCount(0); - return; - } - var searchScheme = getSelectedValue(); - if (searchScheme == null) - return; - setInstance(searchScheme); - linearList.setEnabled(true); - for (var t : TaskManager.getManagerInstance().getTaskList()) { - t.checkProblems(); - } - }); - - } - } - - class LinearSearchList extends JList { - - /** - * - */ - private static final long serialVersionUID = 5478023007473400159L; - - public LinearSearchList() { - - super(); - - setFont(font); - setSelectionMode(SINGLE_SELECTION); - setModel(new AbstractListModel() { - /** - * - */ - private static final long serialVersionUID = -3560305247730025830L; - - @Override - public int getSize() { - return linearSolvers.size(); - } - - @Override - public LinearOptimiser getElementAt(int index) { - return linearSolvers.get(index); - } - }); - - this.setCellRenderer(new SearchListRenderer()); - - addListSelectionListener((ListSelectionEvent arg0) -> { - if (arg0.getValueIsAdjusting()) - return; - if (!(getSelectedValue() instanceof LinearOptimiser)) { - pathTable.setEnabled(false); - return; - } - var linearSolver = getSelectedValue(); - var pathSolver = getInstance(); - pathSolver.setLinearSolver(linearSolver); - pathTable.setPropertyHolder(pathSolver); - pathTable.setEnabled(true); - for (var t : TaskManager.getManagerInstance().getTaskList()) { - t.checkProblems(); - } - }); - - } - - } - -} \ No newline at end of file + getContentPane().setLayout(new GridBagLayout()); + + var gbc = new GridBagConstraints(); + + gbc.fill = BOTH; + gbc.gridy = 0; + gbc.gridx = 0; + gbc.weightx = 1.0; + gbc.weighty = 0.3; + + leftTable = new javax.swing.JTable(); + 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); + + gbc.gridy = 1; + gbc.weighty = 0.45; + + getContentPane().add(mainContainer, gbc); + + gbc.gridy = 2; + gbc.weighty = 0.25; + + var tableScroller = new JScrollPane(pathTable); + tableScroller.setBorder( + createTitledBorder("Select search variables and settings")); + getContentPane().add(tableScroller, gbc); + + } + + public void update() { + var selected = PathOptimiser.getInstance(); + /* + Select Levenberg-Marquardt as default optimiser + */ + if (selected == null) { + pathList.setSelectedValue(LMOptimiser.getInstance(), closable); + } + + pathList.setSelectedIndex(pathSolvers.indexOf(selected)); + ((ParameterTableModel) leftTable.getModel()).populateWithAllProperties(); + + 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(); + ((SelectedKeysModel) rightTblModel).update(searchKeys); + } //Create a new model for the flags list + else { + var c = (Calculation) activeTask.getResponse(); + if (c != null && c.getProblem() != null) { + var searchKeys = activeTask.activeParameters(); + rightTable.setModel(new SelectedKeysModel(searchKeys, mandatorySelection)); + + /* + Add listener to this + */ + rightTable.getModel().addTableModelListener(new TableModelListener() { + + private void updateFlag(TableModelEvent arg0, boolean value) { + 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) { + updateFlag(tme, true); + } else if (tme.getType() == TableModelEvent.DELETE) { + updateFlag(tme, false); + } + } + + }); + + } + } + pathTable.updateTable(); + + } + + class PathSolversList extends JList { + + public PathSolversList() { + + super(); + + setModel(new AbstractListModel() { + + @Override + public int getSize() { + return pathSolvers.size(); + } + + @Override + public PathOptimiser getElementAt(int index) { + return pathSolvers.get(index); + } + }); + + setSelectionMode(SINGLE_SELECTION); + setCellRenderer(new SearchListRenderer()); + + addListSelectionListener((ListSelectionEvent arg0) -> { + if (arg0.getValueIsAdjusting()) { + return; + } + + var optimiser = getSelectedValue(); + + setInstance(optimiser); + pathTable.setPropertyHolder(optimiser); + + for (var t : TaskManager.getManagerInstance().getTaskList()) { + t.checkProblems(); + } + }); + + } + } + +} diff --git a/src/main/java/pulse/ui/frames/TaskControlFrame.java b/src/main/java/pulse/ui/frames/TaskControlFrame.java index 7c434da6..c8e77fc0 100644 --- a/src/main/java/pulse/ui/frames/TaskControlFrame.java +++ b/src/main/java/pulse/ui/frames/TaskControlFrame.java @@ -5,10 +5,12 @@ import static javax.swing.JOptionPane.YES_NO_OPTION; import static javax.swing.JOptionPane.YES_OPTION; import static javax.swing.JOptionPane.showOptionDialog; +import static pulse.tasks.listeners.TaskRepositoryEvent.State.TASK_BROWSING_REQUEST; import static pulse.tasks.processing.ResultFormat.addResultFormatListener; -import static pulse.ui.Launcher.loadIcon; import static pulse.ui.Messages.getString; +import static pulse.util.ImageUtils.loadIcon; +import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; @@ -16,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; @@ -23,7 +26,10 @@ import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; +import pulse.tasks.Calculation; import pulse.tasks.TaskManager; +import pulse.ui.Version; +import pulse.ui.components.PulseChart; import pulse.ui.components.PulseMainMenu; import pulse.ui.components.listeners.FrameVisibilityRequestListener; import pulse.ui.components.listeners.TaskActionListener; @@ -32,363 +38,445 @@ @SuppressWarnings("serial") public class TaskControlFrame extends JFrame { - private static Mode mode = Mode.TASK; - - private final static int HEIGHT = 730; - private final static int WIDTH = 1035; - - private static TaskControlFrame instance = new TaskControlFrame(); - - private static ProblemStatementFrame problemStatementFrame; - private static SearchOptionsFrame searchOptionsFrame; - private static TaskManagerFrame taskManagerFrame; - private static PreviewFrame previewFrame; - private static ResultFrame resultsFrame; - private static MainGraphFrame graphFrame; - private static AuxGraphFrame auxGraphFrame; - private static LogFrame logFrame; - - private static PulseMainMenu mainMenu; - - public static TaskControlFrame getInstance() { - return instance; - } - - /** - * Create the frame. - */ - - private TaskControlFrame() { - setTitle(getString("TaskControlFrame.SoftwareTitle")); - setPreferredSize(new Dimension(WIDTH, HEIGHT)); - setExtendedState(getExtendedState() | MAXIMIZED_BOTH); - initComponents(); - initListeners(); - TaskManager.getManagerInstance().addSelectionListener(e -> graphFrame.plot()); - setIconImage(loadIcon("logo.png", 32).getImage()); - addListeners(); - setDefaultCloseOperation(EXIT_ON_CLOSE); - } - - private void addListeners() { - addWindowListener(new WindowAdapter() { - - @Override - public void windowClosing(WindowEvent evt) { - var closingWindow = (JFrame) evt.getSource(); - if (!exitConfirmed(closingWindow)) { - closingWindow.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); - } else - closingWindow.setDefaultCloseOperation(EXIT_ON_CLOSE); - } - - }); - - addComponentListener(new ComponentAdapter() { - - @Override - public void componentResized(ComponentEvent e) { - doResize(); - } - - }); - - } - - private boolean exitConfirmed(Component closingComponent) { - Object[] options = { "Yes", "No" }; - return showOptionDialog(closingComponent, getString("TaskControlFrame.ExitMessage"), - getString("TaskControlFrame.ExitTitle"), YES_NO_OPTION, WARNING_MESSAGE, null, options, - options[1]) == YES_OPTION; - } - - private void initListeners() { - mainMenu.addFrameVisibilityRequestListener(new FrameVisibilityRequestListener() { - - @Override - public void onProblemStatementShowRequest() { - setProblemStatementFrameVisible(true); - } - - @Override - public void onSearchSettingsShowRequest() { - setSearchOptionsFrameVisible(true); - } - - }); - - mainMenu.addExitRequestListener(() -> { - if (exitConfirmed(this)) - exit(0); - }); - - addResultFormatListener(rfe -> ((ResultTableModel) resultsFrame.getResultTable().getModel()) - .changeFormat(rfe.getResultFormat())); - - resultsFrame.addFrameCreationListener(() -> setPreviewFrameVisible(true)); - - taskManagerFrame.getTaskToolbar().addTaskActionListener(new TaskActionListener() { - - @Override - public void onRemoveRequest() { - // no new actions - } - - @Override - public void onClearRequest() { - logFrame.getLogTextPane().clear(); - resultsFrame.getResultTable().clear(); - } - - @Override - public void onResetRequest() { - logFrame.getLogTextPane().clear(); - resultsFrame.getResultTable().removeAll(); - } - - @Override - public void onGraphRequest() { - graphFrame.plot(); - } - - }); - } - - /** - * This method is called from within the constructor to initialize the form. - */ - - private void initComponents() { - - var desktopPane = new JDesktopPane(); - setContentPane(desktopPane); - - mainMenu = new PulseMainMenu(); - setJMenuBar(mainMenu); - - logFrame = new LogFrame(); - resultsFrame = new ResultFrame(); - previewFrame = new PreviewFrame(); - taskManagerFrame = new TaskManagerFrame(); - graphFrame = MainGraphFrame.getInstance(); - auxGraphFrame = AuxGraphFrame.getInstance(); - - problemStatementFrame = new ProblemStatementFrame(); - - searchOptionsFrame = new SearchOptionsFrame(); - - /* + private final static int HEIGHT = 730; + private final static int WIDTH = 1035; + + private static TaskControlFrame instance = new TaskControlFrame(); + + private Mode mode = Mode.TASK; + private ProblemStatementFrame problemStatementFrame; + private SearchOptionsFrame searchOptionsFrame; + private TaskManagerFrame taskManagerFrame; + private ModelSelectionFrame modelFrame; + private PreviewFrame previewFrame; + private ResultFrame resultsFrame; + private MainGraphFrame graphFrame; + private LogFrame logFrame; + private InternalGraphFrame pulseFrame; + + private PulseMainMenu mainMenu; + + private final static int ICON_SIZE = 16; + + public static TaskControlFrame getInstance() { + return instance; + } + + /** + * Create the frame. + */ + private TaskControlFrame() { + setTitle(Version.getCurrentVersion().toString()); + setPreferredSize(new Dimension(WIDTH, HEIGHT)); + setExtendedState(getExtendedState() | MAXIMIZED_BOTH); + initComponents(); + initListeners(); + setIconImage(loadIcon("logo.png", 32).getImage()); + addListeners(); + setDefaultCloseOperation(EXIT_ON_CLOSE); + TaskManager.addSessionListener(() -> resetSession()); + addRepositoryListeners(TaskManager.getManagerInstance()); + } + + private void addRepositoryListeners(TaskManager s) { + s.addSelectionListener(e -> graphFrame.plot()); + s.addTaskRepositoryListener(e + -> { + if (e.getState() == TASK_BROWSING_REQUEST) { + setModelSelectionFrameVisible(true); + } + } + ); + } + + public void resetSession() { + Objects.requireNonNull(mainMenu, "Menu not created"); + problemStatementFrame.resetSession(); + taskManagerFrame.resetSession(); + modelFrame.resetSession(); + resultsFrame.getResultTable().resetSession(); + logFrame.getLogger().clear(); + logFrame.scheduleLogEvents(); + mainMenu.reset(); + addRepositoryListeners(TaskManager.getManagerInstance()); + } + + private void addListeners() { + addWindowListener(new WindowAdapter() { + + @Override + public void windowClosing(WindowEvent evt) { + var closingWindow = (JFrame) evt.getSource(); + if (!exitConfirmed(closingWindow)) { + closingWindow.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + } else { + closingWindow.setDefaultCloseOperation(EXIT_ON_CLOSE); + } + } + + }); + + addComponentListener(new ComponentAdapter() { + + @Override + public void componentResized(ComponentEvent e) { + doResize(); + } + + }); + + } + + private boolean exitConfirmed(Component closingComponent) { + Object[] options = {"Yes", "No"}; + return showOptionDialog(closingComponent, getString("TaskControlFrame.ExitMessage"), + getString("TaskControlFrame.ExitTitle"), YES_NO_OPTION, WARNING_MESSAGE, null, options, + options[1]) == YES_OPTION; + } + + private void initListeners() { + mainMenu.addFrameVisibilityRequestListener(new FrameVisibilityRequestListener() { + + @Override + public void onProblemStatementShowRequest() { + if (TaskManager.getManagerInstance().getSelectedTask() != null) { + problemStatementFrame.update(); + setProblemStatementFrameVisible(true); + } else { + System.out.println("Please select a task"); + } + } + + @Override + public void onSearchSettingsShowRequest() { + setSearchOptionsFrameVisible(true); + } + + }); + + mainMenu.addExitRequestListener(() -> { + if (exitConfirmed(this)) { + exit(0); + } + }); + + addResultFormatListener(rfe -> ((ResultTableModel) resultsFrame.getResultTable().getModel()) + .changeFormat(rfe.getResultFormat())); + + resultsFrame.addFrameCreationListener(() -> setPreviewFrameVisible(true)); + + taskManagerFrame.getTaskToolbar().addTaskActionListener(new TaskActionListener() { + + @Override + public void onRemoveRequest() { + // no new actions + } + + @Override + public void onClearRequest() { + logFrame.getLogger().clear(); + resultsFrame.getResultTable().clear(); + } + + @Override + public void onResetRequest() { + logFrame.getLogger().clear(); + resultsFrame.getResultTable().removeAll(); + } + + @Override + public void onGraphRequest() { + graphFrame.plot(); + } + + }); + + } + + /** + * This method is called from within the constructor to initialize the form. + */ + private void initComponents() { + + var desktopPane = new JDesktopPane(); + setContentPane(desktopPane); + + mainMenu = new PulseMainMenu(); + setJMenuBar(mainMenu); + + logFrame = new LogFrame(); + logFrame.setFrameIcon(loadIcon("log.png", ICON_SIZE, Color.white)); + resultsFrame = new ResultFrame(); + resultsFrame.setFrameIcon(loadIcon("result.png", ICON_SIZE, Color.white)); + previewFrame = new PreviewFrame(); + previewFrame.setFrameIcon(loadIcon("preview.png", ICON_SIZE, Color.white)); + taskManagerFrame = new TaskManagerFrame(); + taskManagerFrame.setFrameIcon(loadIcon("task_manager.png", ICON_SIZE, Color.white)); + graphFrame = MainGraphFrame.getInstance(); + graphFrame.setFrameIcon(loadIcon("curves.png", ICON_SIZE, Color.white)); + + problemStatementFrame = new ProblemStatementFrame(); + problemStatementFrame.setFrameIcon(loadIcon("heat_problem.png", ICON_SIZE, Color.white)); + modelFrame = new ModelSelectionFrame(); + modelFrame.setFrameIcon(loadIcon("stored.png", ICON_SIZE, Color.white)); + + searchOptionsFrame = new SearchOptionsFrame(); + 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", ICON_SIZE, Color.white)); + pulseFrame.setVisible(false); + + /* * CONSTRAINT ADJUSTMENT - */ - - resizeQuadrants(); - desktopPane.add(taskManagerFrame); - desktopPane.add(auxGraphFrame); - desktopPane.add(graphFrame); - desktopPane.add(previewFrame); - desktopPane.add(logFrame); - desktopPane.add(resultsFrame); - desktopPane.add(problemStatementFrame); - desktopPane.add(searchOptionsFrame); - - setDefaultResizeBehaviour(); - - pack(); - - } - - private void setDefaultResizeBehaviour() { - var ifa = new InternalFrameAdapter() { - - @Override - public void internalFrameDeiconified(InternalFrameEvent e) { - resizeQuadrants(); - } - - }; - - taskManagerFrame.addInternalFrameListener(ifa); - graphFrame.addInternalFrameListener(ifa); - logFrame.addInternalFrameListener(ifa); - resultsFrame.addInternalFrameListener(ifa); - - previewFrame.addInternalFrameListener(new InternalFrameAdapter() { - - @Override - public void internalFrameClosing(InternalFrameEvent e) { - setPreviewFrameVisible(false); - } - - }); - - problemStatementFrame.addInternalFrameListener(new InternalFrameAdapter() { - - @Override - public void internalFrameClosing(InternalFrameEvent e) { - setProblemStatementFrameVisible(false); - } - - }); - - searchOptionsFrame.addInternalFrameListener(new InternalFrameAdapter() { - - @Override - public void internalFrameClosing(InternalFrameEvent e) { - setSearchOptionsFrameVisible(false); - } - - }); - - } + */ + resizeQuadrants(); + desktopPane.add(taskManagerFrame); + desktopPane.add(pulseFrame); + desktopPane.add(graphFrame); + desktopPane.add(previewFrame); + desktopPane.add(logFrame); + desktopPane.add(resultsFrame); + desktopPane.add(problemStatementFrame); + desktopPane.add(searchOptionsFrame); + desktopPane.add(modelFrame); + + setDefaultResizeBehaviour(); + + pack(); + + } + + private void setDefaultResizeBehaviour() { + var ifa = new InternalFrameAdapter() { + + @Override + public void internalFrameDeiconified(InternalFrameEvent e) { + resizeQuadrants(); + } + + }; + + taskManagerFrame.addInternalFrameListener(ifa); + graphFrame.addInternalFrameListener(ifa); + logFrame.addInternalFrameListener(ifa); + resultsFrame.addInternalFrameListener(ifa); + + previewFrame.addInternalFrameListener(new InternalFrameAdapter() { - private void doResize() { - switch (mode) { - case TASK: - resizeQuadrants(); - break; - case PROBLEM: - resizeTriplet(problemStatementFrame, auxGraphFrame, graphFrame); - break; - case SEARCH: - resizeFull(searchOptionsFrame); - break; - case PREVIEW: - resizeHalves(previewFrame, resultsFrame); - break; - } - } + @Override + public void internalFrameClosing(InternalFrameEvent e) { + setPreviewFrameVisible(false); + } + + }); - private void resizeFull(JInternalFrame f1) { - final var gap = 10; - final var h = this.getContentPane().getHeight() - 2 * gap; - final var w = this.getContentPane().getWidth() - 2 * gap; + problemStatementFrame.addInternalFrameListener(new InternalFrameAdapter() { - var p1 = new Point(gap, gap); - var s1 = new Dimension(w, h); - f1.setLocation(p1); - f1.setSize(s1); - } + @Override + public void internalFrameClosing(InternalFrameEvent e) { + setProblemStatementFrameVisible(false); + } - private void resizeHalves(JInternalFrame f1, JInternalFrame f2) { - final var gap = 10; - final var h = this.getContentPane().getHeight() - 3 * gap; - final var w = this.getContentPane().getWidth() - 2 * gap; + }); - var p1 = new Point(gap, gap); - var s1 = new Dimension(w, 6 * h / 10); + searchOptionsFrame.addInternalFrameListener(new InternalFrameAdapter() { - var p2 = new Point(gap, 2 * gap + 6 * h / 10); - var s2 = new Dimension(w, 4 * h / 10); + @Override + public void internalFrameClosing(InternalFrameEvent e) { + setSearchOptionsFrameVisible(false); + } - f1.setLocation(p1); - f1.setSize(s1); - f2.setLocation(p2); - f2.setSize(s2); - } + }); - private void resizeTriplet(JInternalFrame f1, JInternalFrame f2, JInternalFrame f3) { - final var gap = 10; + modelFrame.addInternalFrameListener(new InternalFrameAdapter() { - final var h = this.getContentPane().getHeight() - 3 * gap; - var w = this.getContentPane().getWidth() - 2 * gap; + @Override + public void internalFrameClosing(InternalFrameEvent e) { + setModelSelectionFrameVisible(false); + } - var p1 = new Point(gap, gap); - var s1 = new Dimension(w, 6 * h / 10); + }); - f1.setLocation(p1); - f1.setSize(s1); + } - w = this.getContentPane().getWidth() - 3 * gap; + private void doResize() { + switch (mode) { + case TASK: + resizeQuadrants(); + break; + case PROBLEM: + resizeTriplet(problemStatementFrame, pulseFrame, graphFrame); + break; + case SEARCH: + resizeFull(searchOptionsFrame); + break; + case PREVIEW: + resizeHalves(previewFrame, resultsFrame); + break; + case MODEL_COMPARISON: + resizeTriplet(graphFrame, resultsFrame, modelFrame); + break; + default: + break; + } + } - var p2 = new Point(gap, 2 * gap + 6 * h / 10); - var s2 = new Dimension(w / 4, 4 * h / 10); + private void resizeFull(JInternalFrame f1) { + final var gap = 10; + final var h = this.getContentPane().getHeight() - 2 * gap; + final var w = this.getContentPane().getWidth() - 2 * gap; - f2.setLocation(p2); - f2.setSize(s2); + var p1 = new Point(gap, gap); + var s1 = new Dimension(w, h); + f1.setLocation(p1); + f1.setSize(s1); + } - var p3 = new Point(2 * gap + w / 4, 2 * gap + 6 * h / 10); - var s3 = new Dimension(3 * w / 4, 4 * h / 10); + private void resizeHalves(JInternalFrame f1, JInternalFrame f2) { + final var gap = 10; + final var h = this.getContentPane().getHeight() - 3 * gap; + final var w = this.getContentPane().getWidth() - 2 * gap; - f3.setLocation(p3); - f3.setSize(s3); - } + var p1 = new Point(gap, gap); + var s1 = new Dimension(w, 6 * h / 10); - private void resizeQuadrants() { - final var gap = 10; - final var h = this.getContentPane().getHeight() - 3 * gap; - final var w = this.getContentPane().getWidth() - 3 * gap; + var p2 = new Point(gap, 2 * gap + 6 * h / 10); + var s2 = new Dimension(w, 4 * h / 10); - var p1 = new Point(gap, gap); - var s1 = new Dimension(45 * w / 100, 55 * h / 100); + f1.setLocation(p1); + f1.setSize(s1); + f2.setLocation(p2); + f2.setSize(s2); + } + + private void resizeTriplet(JInternalFrame f1, JInternalFrame f2, JInternalFrame f3) { + final var gap = 10; + + final var h = this.getContentPane().getHeight() - 3 * gap; + var w = this.getContentPane().getWidth() - 2 * gap; + + var p1 = new Point(gap, gap); + var s1 = new Dimension(w, 6 * h / 10); + + f1.setLocation(p1); + f1.setSize(s1); + + w = this.getContentPane().getWidth() - 3 * gap; + + var p2 = new Point(gap, 2 * gap + 6 * h / 10); + var s2 = new Dimension(w / 4, 4 * h / 10); + + f2.setLocation(p2); + f2.setSize(s2); + + var p3 = new Point(2 * gap + w / 4, 2 * gap + 6 * h / 10); + var s3 = new Dimension(3 * w / 4, 4 * h / 10); + + f3.setLocation(p3); + f3.setSize(s3); + } + + private void resizeQuadrants() { + final var gap = 10; + final var h = this.getContentPane().getHeight() - 3 * gap; + final var w = this.getContentPane().getWidth() - 3 * gap; + + var p1 = new Point(gap, gap); + var s1 = new Dimension(45 * w / 100, 55 * h / 100); + + var p2 = new Point(2 * gap + 45 * w / 100, gap); + var s2 = new Dimension(55 * w / 100, 55 * h / 100); + + var p3 = new Point(gap, 2 * gap + 55 * h / 100); + var s3 = new Dimension(45 * w / 100, 45 * h / 100); + + var p4 = new Point(2 * gap + 45 * w / 100, 2 * gap + 55 * h / 100); + var s4 = new Dimension(55 * w / 100, 45 * h / 100); + + taskManagerFrame.setLocation(p1); + taskManagerFrame.setSize(s1); + graphFrame.setLocation(p2); + graphFrame.setSize(s2); + logFrame.setLocation(p3); + logFrame.setSize(s3); + resultsFrame.setLocation(p4); + resultsFrame.setSize(s4); + } + + private void setPreviewFrameVisible(boolean show) { + previewFrame.update(((ResultTableModel) resultsFrame.getResultTable().getModel()).getFormat(), + resultsFrame.getResultTable().data()); + + previewFrame.setVisible(show); + + resultsFrame.setVisible(true); + taskManagerFrame.setVisible(!show); + graphFrame.setVisible(!show); + logFrame.setVisible(!show); - var p2 = new Point(2 * gap + 45 * w / 100, gap); - var s2 = new Dimension(55 * w / 100, 55 * h / 100); + mode = show ? Mode.PREVIEW : Mode.TASK; + doResize(); - var p3 = new Point(gap, 2 * gap + 55 * h / 100); - var s3 = new Dimension(45 * w / 100, 45 * h / 100); + } - var p4 = new Point(2 * gap + 45 * w / 100, 2 * gap + 55 * h / 100); - var s4 = new Dimension(55 * w / 100, 45 * h / 100); + private void setProblemStatementFrameVisible(boolean show) { + problemStatementFrame.setVisible(show); + pulseFrame.setVisible(show); + graphFrame.setVisible(true); - taskManagerFrame.setLocation(p1); - taskManagerFrame.setSize(s1); - graphFrame.setLocation(p2); - graphFrame.setSize(s2); - logFrame.setLocation(p3); - logFrame.setSize(s3); - resultsFrame.setLocation(p4); - resultsFrame.setSize(s4); - } - - private void setPreviewFrameVisible(boolean show) { - previewFrame.update(((ResultTableModel) resultsFrame.getResultTable().getModel()).getFormat(), - resultsFrame.getResultTable().data()); - - previewFrame.setVisible(show); + previewFrame.setVisible(false); + resultsFrame.setVisible(!show); + taskManagerFrame.setVisible(!show); + logFrame.setVisible(!show); - resultsFrame.setVisible(true); - taskManagerFrame.setVisible(!show); - graphFrame.setVisible(!show); - logFrame.setVisible(!show); + mode = show ? Mode.PROBLEM : Mode.TASK; + doResize(); + } - mode = show ? Mode.PREVIEW : Mode.TASK; - doResize(); + private void setSearchOptionsFrameVisible(boolean show) { + if (show) { + searchOptionsFrame.update(); + } + searchOptionsFrame.setVisible(show); - } + problemStatementFrame.setVisible(false); + previewFrame.setVisible(false); + resultsFrame.setVisible(!show); + taskManagerFrame.setVisible(!show); + graphFrame.setVisible(!show); + logFrame.setVisible(!show); - private void setProblemStatementFrameVisible(boolean show) { - problemStatementFrame.setVisible(show); - graphFrame.setVisible(true); + mode = show ? Mode.SEARCH : Mode.TASK; + doResize(); + } - previewFrame.setVisible(false); - resultsFrame.setVisible(!show); - taskManagerFrame.setVisible(!show); - logFrame.setVisible(!show); + private void setModelSelectionFrameVisible(boolean show) { + modelFrame.setVisible(show); + resultsFrame.setVisible(true); + graphFrame.setVisible(true); - mode = show ? Mode.PROBLEM : Mode.TASK; - doResize(); - } + problemStatementFrame.setVisible(false); + previewFrame.setVisible(false); + taskManagerFrame.setVisible(!show); + logFrame.setVisible(!show); - private void setSearchOptionsFrameVisible(boolean show) { - if (show) - searchOptionsFrame.update(); - searchOptionsFrame.setVisible(show); + mode = show ? Mode.MODEL_COMPARISON : Mode.TASK; + doResize(); + } - problemStatementFrame.setVisible(false); - previewFrame.setVisible(false); - resultsFrame.setVisible(!show); - taskManagerFrame.setVisible(!show); - graphFrame.setVisible(!show); - logFrame.setVisible(!show); + public enum Mode { - mode = show ? Mode.SEARCH : Mode.TASK; - doResize(); - } + TASK, PROBLEM, PREVIEW, SEARCH, MODEL_COMPARISON; - private enum Mode { + } - TASK, PROBLEM, PREVIEW, SEARCH; + public Mode getMode() { + return mode; + } - } + public InternalGraphFrame getPulseFrame() { + return pulseFrame; + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/ui/frames/TaskManagerFrame.java b/src/main/java/pulse/ui/frames/TaskManagerFrame.java index ddeec09f..a24f9c0f 100644 --- a/src/main/java/pulse/ui/frames/TaskManagerFrame.java +++ b/src/main/java/pulse/ui/frames/TaskManagerFrame.java @@ -17,86 +17,92 @@ @SuppressWarnings("serial") public class TaskManagerFrame extends JInternalFrame { - private TaskTable taskTable; - private TaskToolbar taskToolbar; - - public TaskManagerFrame() { - super("Task Manager", true, false, true, true); - initComponents(); - adjustEnabledControls(); - manageListeners(); - setVisible(true); - } - - private void manageListeners() { - taskToolbar.addTaskActionListener(new TaskActionListener() { - - @Override - public void onRemoveRequest() { - taskTable.removeSelectedRows(); - } - - @Override - public void onClearRequest() { - TaskManager.getManagerInstance().clear(); - } - - @Override - public void onResetRequest() { - // no new actions - } - - @Override - public void onGraphRequest() { - // no new actions - } - - }); - } - - private void initComponents() { - var taskScrollPane = new JScrollPane(); - taskTable = new TaskTable(); - taskScrollPane.setViewportView(taskTable); - getContentPane().add(taskScrollPane, CENTER); - taskToolbar = new TaskToolbar(); - getContentPane().add(taskToolbar, PAGE_START); - } - - 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); - } - }); - - taskTable.getSelectionModel().addListSelectionListener((ListSelectionEvent arg0) -> { - var selection = taskTable.getSelectedRows(); - if (taskTable.getSelectedRow() < 0) { - taskToolbar.setRemoveEnabled(false); - taskToolbar.setGraphEnabled(false); - } else { - if (selection.length > 1) { - taskToolbar.setRemoveEnabled(false); - taskToolbar.setGraphEnabled(false); - } else if (selection.length > 0) { - taskToolbar.setRemoveEnabled(true); - taskToolbar.setGraphEnabled(true); - } - } - }); - } - - public TaskToolbar getTaskToolbar() { - return taskToolbar; - } - -} \ No newline at end of file + private TaskTable taskTable; + private TaskToolbar taskToolbar; + + public TaskManagerFrame() { + super("Task Manager", true, false, true, true); + initComponents(); + adjustEnabledControls(); + manageListeners(); + setVisible(true); + } + + public void resetSession() { + taskTable.resetSession(); + taskToolbar.resetSession(); + adjustEnabledControls(); + } + + private void manageListeners() { + taskToolbar.addTaskActionListener(new TaskActionListener() { + + @Override + public void onRemoveRequest() { + taskTable.removeSelectedRows(); + } + + @Override + public void onClearRequest() { + TaskManager.getManagerInstance().clear(); + } + + @Override + public void onResetRequest() { + // no new actions + } + + @Override + public void onGraphRequest() { + // no new actions + } + + }); + } + + private void initComponents() { + var taskScrollPane = new JScrollPane(); + taskTable = new TaskTable(); + taskScrollPane.setViewportView(taskTable); + getContentPane().add(taskScrollPane, CENTER); + 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(); + + enableIfNeeded(); + ttm.addTableModelListener((TableModelEvent arg0) -> enableIfNeeded() ); + + taskTable.getSelectionModel().addListSelectionListener((ListSelectionEvent arg0) -> { + var selection = taskTable.getSelectedRows(); + if (taskTable.getSelectedRow() < 0) { + taskToolbar.setRemoveEnabled(false); + taskToolbar.setGraphEnabled(false); + } else { + if (selection.length > 1) { + taskToolbar.setRemoveEnabled(false); + taskToolbar.setGraphEnabled(false); + } else if (selection.length > 0) { + taskToolbar.setRemoveEnabled(true); + taskToolbar.setGraphEnabled(true); + } + } + }); + } + + public TaskToolbar getTaskToolbar() { + return taskToolbar; + } + +} diff --git a/src/main/java/pulse/ui/frames/dialogs/AboutDialog.java b/src/main/java/pulse/ui/frames/dialogs/AboutDialog.java deleted file mode 100644 index 5ebbefe8..00000000 --- a/src/main/java/pulse/ui/frames/dialogs/AboutDialog.java +++ /dev/null @@ -1,64 +0,0 @@ -package pulse.ui.frames.dialogs; - -import static java.awt.BorderLayout.CENTER; -import static java.lang.System.err; -import static javax.swing.UIManager.getColor; -import static pulse.ui.Messages.getString; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -import javax.swing.JDialog; -import javax.swing.JScrollPane; -import javax.swing.JTextPane; -import javax.swing.text.BadLocationException; -import javax.swing.text.html.HTMLDocument; -import javax.swing.text.html.HTMLEditorKit; - -@SuppressWarnings("serial") -public class AboutDialog extends JDialog { - - private final static int WIDTH = 570; - private final static int HEIGHT = 370; - - public AboutDialog() { - - setTitle(getString("TaskControlFrame.AboutDialog")); - setAlwaysOnTop(true); - setSize(WIDTH, HEIGHT); - - var reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/About.html"))); - var sb = new StringBuilder(); - String str; - - try { - while ((str = reader.readLine()) != null) - sb.append(str); - } catch (IOException e1) { - // TODO Auto-generated catch block - e1.printStackTrace(); - } - - var textPane = new JTextPane(); - textPane.setBackground(getColor("Panel.background")); - textPane.setEditable(false); - textPane.setContentType("text/html"); - - final var doc = (HTMLDocument) textPane.getDocument(); - final var kit = (HTMLEditorKit) textPane.getEditorKit(); - try { - kit.insertHTML(doc, doc.getLength(), sb.toString(), 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(); - } - - getContentPane().add(new JScrollPane(textPane), CENTER); - - } - -} diff --git a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java index e0f15268..bdfe6109 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java @@ -11,10 +11,10 @@ import static pulse.io.export.ExportManager.exportAllResults; import static pulse.io.export.ExportManager.exportGroup; import static pulse.io.export.Extension.valueOf; -import static pulse.ui.Launcher.threadsAvailable; import java.awt.Dimension; import java.io.File; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; @@ -43,260 +43,264 @@ import pulse.tasks.TaskManager; import pulse.tasks.logs.Log; import pulse.tasks.processing.Result; +import pulse.util.ResourceMonitor; @SuppressWarnings("serial") public class ExportDialog extends JDialog { - private static Map, Boolean> exportSettings = new HashMap, Boolean>(); - private final static int HEIGHT = 160; - private final static int WIDTH = 650; + private static Map, Boolean> exportSettings = new HashMap, Boolean>(); + private final static int HEIGHT = 180; + private final static int WIDTH = 750; - private static ProgressDialog progressFrame = new ProgressDialog(); + private static ProgressDialog progressFrame = new ProgressDialog(); - static { - progressFrame.setLocationRelativeTo(null); - progressFrame.setAlwaysOnTop(true); - } + static { + progressFrame.setLocationRelativeTo(null); + progressFrame.setAlwaysOnTop(true); + } - static { - exportSettings.put(MetadataExporter.getInstance().target(), false); - exportSettings.put(CurveExporter.getInstance().target(), true); - exportSettings.put(ResidualStatisticExporter.getInstance().target(), true); - exportSettings.put(RawDataExporter.getInstance().target(), true); - exportSettings.put(ResultExporter.getInstance().target(), true); - exportSettings.put(LogExporter.getInstance().target(), false); - } - private boolean createSubdirectories = false; + static { + exportSettings.put(MetadataExporter.getInstance().target(), false); + exportSettings.put(CurveExporter.getInstance().target(), true); + exportSettings.put(ResidualStatisticExporter.getInstance().target(), true); + exportSettings.put(RawDataExporter.getInstance().target(), true); + exportSettings.put(ResultExporter.getInstance().target(), true); + exportSettings.put(LogExporter.getInstance().target(), false); + } + private boolean createSubdirectories = false; - private File dir; + private File dir; - private JFileChooser fileChooser; + private JFileChooser fileChooser; - private String projectName; + private String projectName; - public ExportDialog() { - initComponents(); - setTitle("Export Dialog"); - setSize(new Dimension(WIDTH, HEIGHT)); - } + public ExportDialog() { + initComponents(); + setTitle("Export Dialog"); + setSize(new Dimension(WIDTH, HEIGHT)); + } - private File directoryQuery() { - var returnVal = fileChooser.showSaveDialog(this); + private File directoryQuery() { + var returnVal = fileChooser.showOpenDialog(this); - if (returnVal == APPROVE_OPTION) { - dir = fileChooser.getSelectedFile(); - return dir; - } + File f = null; - return null; + if (returnVal == APPROVE_OPTION) { + dir = f = fileChooser.getSelectedFile(); + } - } + return f; - private void export(Extension extension) { - var instance = TaskManager.getManagerInstance(); - - if (instance.numberOfTasks() < 1) - return; // nothing to export + } - var destination = new File(dir + separator + projectName); - var subdirs = instance.getTaskList(); + private void export(Extension extension) { + var instance = TaskManager.getManagerInstance(); - if (subdirs.size() > 0 && !destination.exists()) - destination.mkdirs(); + if (instance.numberOfTasks() < 1) { + return; // nothing to export + } + var destination = new File(dir + separator + projectName); + var subdirs = instance.getTaskList(); - final var threads = threadsAvailable(); + if (subdirs.size() > 0 && !destination.exists()) { + destination.mkdirs(); + } - if (createSubdirectories) { - progressFrame.trackProgress(subdirs.size()); - var pool = newFixedThreadPool(threads - 1); - subdirs.stream().forEach(s -> { - pool.submit(() -> { - exportGroup(s, destination, extension); - progressFrame.incrementProgress(); - }); - }); - } else { - var groupped = instance.allGrouppedContents(); - var pool = newFixedThreadPool(threads - 1); - progressFrame.trackProgress(groupped.size()); + var monitor = ResourceMonitor.getInstance(); - groupped.stream().forEach(individual -> pool.submit(() -> { - Class individualClass = individual.getClass(); + if (createSubdirectories) { + progressFrame.trackProgress(subdirs.size()); + var pool = newFixedThreadPool(monitor.getThreadsAvailable()); + subdirs.stream().forEach(s -> { + pool.submit(() -> { + exportGroup(s, destination, extension); + progressFrame.incrementProgress(); + }); + }); + } else { + var groupped = instance.allGrouppedContents(); + var pool = newFixedThreadPool(monitor.getThreadsAvailable()); + progressFrame.trackProgress(groupped.size()); + + groupped.stream().forEach(individual -> pool.submit(() -> { + Class individualClass = individual.getClass(); + + if (!exportSettings.containsKey(individualClass)) { + + var key = exportSettings.keySet().stream() + .filter(aClass -> aClass.isAssignableFrom(individual.getClass())).findFirst(); + + if (key.isPresent()) { + individualClass = key.get(); + } + + } + + if (individualClass != null) { + if (exportSettings.containsKey(individualClass)) { + if (exportSettings.get(individualClass)) { + ExportManager.export(individual, destination, extension); + } + } + } + + progressFrame.incrementProgress(); + + }) + ); + } + + if (exportSettings.get(Result.class)) { + exportAllResults(destination, extension); + } + + } + + private void initComponents() { + + var layout = new GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setAutoCreateGaps(true); + layout.setAutoCreateContainerGaps(true); - if (!exportSettings.containsKey(individualClass)) { + final var defaultProjectName = TaskManager.getManagerInstance().describe(); + projectName = defaultProjectName; - var key = exportSettings.keySet().stream() - .filter(aClass -> aClass.isAssignableFrom(individual.getClass())).findFirst(); + var directoryLabel = new JLabel("Export to:"); - if (key.isPresent()) - individualClass = key.get(); - - } - - if (individualClass != null) { - if (exportSettings.containsKey(individualClass)) - if (exportSettings.get(individualClass)) - ExportManager.export(individual, destination, extension); - } - - progressFrame.incrementProgress(); + fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileSelectionMode(DIRECTORIES_ONLY); - }) - - ); - } - - if (exportSettings.get(Result.class)) - exportAllResults(destination, extension); - - } + //get cwd + dir = new File("").getAbsoluteFile(); + + var directoryField = new JTextField(dir.getPath() + separator + projectName + separator); + directoryField.setEditable(false); + + var formatLabel = new JLabel("Export format:"); + var formats = new JComboBox(Extension.values()); + + var projectLabel = new JLabel("Project name:"); + var projectText = new JTextField(projectName); + + projectText.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + // + } + + @Override + public void insertUpdate(DocumentEvent e) { + if (projectText.getText().trim().isEmpty()) { + return; + } + projectName = projectText.getText(); + directoryField.setText(dir.getPath() + separator + projectName + separator); + } + + @Override + public void removeUpdate(DocumentEvent e) { + if (projectText.getText().trim().isEmpty()) { + projectName = TaskManager.getManagerInstance().describe(); + directoryField.setText(dir.getPath() + separator + projectName + separator); + } else { + projectName = projectText.getText(); + directoryField.setText(dir.getPath() + separator + projectName + separator); + } + } + }); + + var solutionCheckbox = new JCheckBox("Export Solution(s)"); + solutionCheckbox.setSelected(exportSettings.get(AbstractData.class)); + solutionCheckbox.addActionListener(e -> { + exportSettings.put(AbstractData.class, solutionCheckbox.isSelected()); + exportSettings.put(ResidualStatisticExporter.class, solutionCheckbox.isSelected()); + }); + + var rawDataCheckbox = new JCheckBox("Export Raw Data"); + rawDataCheckbox.setSelected(exportSettings.get(ExperimentalData.class)); + rawDataCheckbox + .addActionListener(e -> exportSettings.put(ExperimentalData.class, rawDataCheckbox.isSelected())); - private void initComponents() { - - var layout = new GroupLayout(getContentPane()); - getContentPane().setLayout(layout); - layout.setAutoCreateGaps(true); - layout.setAutoCreateContainerGaps(true); + var metadataCheckbox = new JCheckBox("Export Metadata"); + metadataCheckbox.setSelected(exportSettings.get(Metadata.class)); + metadataCheckbox.addActionListener(e -> exportSettings.put(Metadata.class, metadataCheckbox.isSelected())); - final var defaultProjectName = TaskManager.getManagerInstance().describe(); - projectName = defaultProjectName; - - var directoryLabel = new JLabel("Export to:"); - - fileChooser = new JFileChooser(); - fileChooser.setMultiSelectionEnabled(false); - fileChooser.setFileSelectionMode(DIRECTORIES_ONLY); - // Checkboxex - dir = fileChooser.getCurrentDirectory(); - - var directoryField = new JTextField(dir.getPath() + separator + projectName + separator); - directoryField.setEditable(false); - - var formatLabel = new JLabel("Export format:"); - var formats = new JComboBox(Extension.values()); - - var projectLabel = new JLabel("Project name:"); - var projectText = new JTextField(projectName); - - projectText.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void changedUpdate(DocumentEvent e) { - // - } - - @Override - public void insertUpdate(DocumentEvent e) { - if (projectText.getText().trim().isEmpty()) - return; - projectName = projectText.getText(); - directoryField.setText(dir.getPath() + separator + projectName + separator); - } - - @Override - public void removeUpdate(DocumentEvent e) { - if (projectText.getText().trim().isEmpty()) { - projectName = TaskManager.getManagerInstance().describe(); - directoryField.setText(dir.getPath() + separator + projectName + separator); - } else { - projectName = projectText.getText(); - directoryField.setText(dir.getPath() + separator + projectName + separator); - } - } - }); - - var solutionCheckbox = new JCheckBox("Export Solution(s)"); - solutionCheckbox.setSelected(exportSettings.get(AbstractData.class)); - solutionCheckbox.addActionListener(e -> { - exportSettings.put(AbstractData.class, solutionCheckbox.isSelected()); - exportSettings.put(ResidualStatisticExporter.class, solutionCheckbox.isSelected()); - }); - - var rawDataCheckbox = new JCheckBox("Export Raw Data"); - rawDataCheckbox.setSelected(exportSettings.get(ExperimentalData.class)); - rawDataCheckbox - .addActionListener(e -> exportSettings.put(ExperimentalData.class, rawDataCheckbox.isSelected())); - - var metadataCheckbox = new JCheckBox("Export Metadata"); - metadataCheckbox.setSelected(exportSettings.get(Metadata.class)); - metadataCheckbox.addActionListener(e -> exportSettings.put(Metadata.class, metadataCheckbox.isSelected())); - - var createDirCheckbox = new JCheckBox("Create Sub-Directories"); - createDirCheckbox.setSelected(createSubdirectories); - - var logCheckbox = new JCheckBox("Export log(s)"); - logCheckbox.setSelected(exportSettings.get(Log.class)); - logCheckbox.addActionListener(e -> exportSettings.put(Log.class, logCheckbox.isSelected())); - - createDirCheckbox.addActionListener(e -> { - metadataCheckbox.setEnabled(!createDirCheckbox.isSelected()); - rawDataCheckbox.setEnabled(!createDirCheckbox.isSelected()); - solutionCheckbox.setEnabled(!createDirCheckbox.isSelected()); - logCheckbox.setEnabled(!createDirCheckbox.isSelected()); - createSubdirectories = createDirCheckbox.isSelected(); - }); - - var resultsCheckbox = new JCheckBox("Export Results"); - resultsCheckbox.setSelected(exportSettings.get(Result.class)); - resultsCheckbox.addActionListener(e -> exportSettings.put(Result.class, resultsCheckbox.isSelected())); - - var browseBtn = new JButton("Browse..."); - - browseBtn.addActionListener(e -> { - if (directoryQuery() != null) - directoryField.setText(dir.getPath() + separator + projectName + separator); - }); - - var exportBtn = new JButton("Export"); - - exportBtn.addActionListener( - e -> invokeLater(() -> export(valueOf(formats.getSelectedItem().toString().toUpperCase())))); - - /* + var createDirCheckbox = new JCheckBox("Create Sub-Directories"); + createDirCheckbox.setSelected(createSubdirectories); + + var logCheckbox = new JCheckBox("Export log(s)"); + logCheckbox.setSelected(exportSettings.get(Log.class)); + logCheckbox.addActionListener(e -> exportSettings.put(Log.class, logCheckbox.isSelected())); + + createDirCheckbox.addActionListener(e -> { + metadataCheckbox.setEnabled(!createDirCheckbox.isSelected()); + rawDataCheckbox.setEnabled(!createDirCheckbox.isSelected()); + solutionCheckbox.setEnabled(!createDirCheckbox.isSelected()); + logCheckbox.setEnabled(!createDirCheckbox.isSelected()); + createSubdirectories = createDirCheckbox.isSelected(); + }); + + var resultsCheckbox = new JCheckBox("Export Results"); + resultsCheckbox.setSelected(exportSettings.get(Result.class)); + resultsCheckbox.addActionListener(e -> exportSettings.put(Result.class, resultsCheckbox.isSelected())); + + var browseBtn = new JButton("Browse..."); + + browseBtn.addActionListener(e -> directoryField.setText(directoryQuery() + .getPath() + separator + projectName + separator)); + + var exportBtn = new JButton("Export"); + + exportBtn.addActionListener( + e -> invokeLater(() -> export(valueOf(formats.getSelectedItem().toString().toUpperCase())))); + + /* * layout - */ - - layout.setHorizontalGroup(layout.createSequentialGroup() - // #1 - .addComponent(directoryLabel) - // #2 - .addGroup(layout.createParallelGroup(LEADING).addComponent(directoryField) - // #2a - .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(LEADING).addComponent(solutionCheckbox) - .addComponent(rawDataCheckbox)) - .addGroup(layout.createParallelGroup(LEADING).addComponent(metadataCheckbox) - .addComponent(createDirCheckbox)) - .addGroup(layout.createParallelGroup(LEADING).addComponent(logCheckbox) - .addComponent(resultsCheckbox))) - // #2b - // .addGroup(layout.createSequentialGroup() - .addGroup(layout.createSequentialGroup().addComponent(formatLabel).addComponent(formats) - .addComponent(projectLabel).addComponent(projectText)) - // ) - ) - // #3 - .addGroup(layout.createParallelGroup(LEADING).addComponent(browseBtn).addComponent(exportBtn))); - layout.linkSize(HORIZONTAL, browseBtn, exportBtn); - - layout.setVerticalGroup(layout.createSequentialGroup() - // #1 - .addGroup(layout.createParallelGroup(BASELINE).addComponent(directoryLabel) - - .addComponent(directoryField).addComponent(browseBtn)) - // #2 - .addGroup(layout.createParallelGroup(LEADING) - // #2a - .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(BASELINE).addComponent(solutionCheckbox) - .addComponent(metadataCheckbox).addComponent(logCheckbox)) - .addGroup(layout.createParallelGroup(BASELINE).addComponent(rawDataCheckbox) - .addComponent(createDirCheckbox).addComponent(resultsCheckbox))) - // #2b - .addComponent(exportBtn)) - // 2b - .addGroup(layout.createParallelGroup(BASELINE).addComponent(formats).addComponent(formatLabel) - .addComponent(projectLabel).addComponent(projectText))); - - } - -} \ No newline at end of file + */ + layout.setHorizontalGroup(layout.createSequentialGroup() + // #1 + .addComponent(directoryLabel) + // #2 + .addGroup(layout.createParallelGroup(LEADING).addComponent(directoryField) + // #2a + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(LEADING).addComponent(solutionCheckbox) + .addComponent(rawDataCheckbox)) + .addGroup(layout.createParallelGroup(LEADING).addComponent(metadataCheckbox) + .addComponent(createDirCheckbox)) + .addGroup(layout.createParallelGroup(LEADING).addComponent(logCheckbox) + .addComponent(resultsCheckbox))) + // #2b + // .addGroup(layout.createSequentialGroup() + .addGroup(layout.createSequentialGroup().addComponent(formatLabel).addComponent(formats) + .addComponent(projectLabel).addComponent(projectText)) + // ) + ) + // #3 + .addGroup(layout.createParallelGroup(LEADING).addComponent(browseBtn).addComponent(exportBtn))); + layout.linkSize(HORIZONTAL, browseBtn, exportBtn); + + layout.setVerticalGroup(layout.createSequentialGroup() + // #1 + .addGroup(layout.createParallelGroup(BASELINE).addComponent(directoryLabel) + .addComponent(directoryField).addComponent(browseBtn)) + // #2 + .addGroup(layout.createParallelGroup(LEADING) + // #2a + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(BASELINE).addComponent(solutionCheckbox) + .addComponent(metadataCheckbox).addComponent(logCheckbox)) + .addGroup(layout.createParallelGroup(BASELINE).addComponent(rawDataCheckbox) + .addComponent(createDirCheckbox).addComponent(resultsCheckbox))) + // #2b + .addComponent(exportBtn)) + // 2b + .addGroup(layout.createParallelGroup(BASELINE).addComponent(formats).addComponent(formatLabel) + .addComponent(projectLabel).addComponent(projectText))); + + } + +} diff --git a/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java b/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java index 65961e39..84628d7f 100644 --- a/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/FormattedInputDialog.java @@ -2,13 +2,11 @@ import static java.awt.BorderLayout.SOUTH; import static java.awt.Toolkit.getDefaultToolkit; -import static java.lang.System.out; import static javax.swing.BorderFactory.createEmptyBorder; import static javax.swing.JOptionPane.ERROR_MESSAGE; import static javax.swing.JOptionPane.YES_NO_OPTION; import static javax.swing.JOptionPane.showOptionDialog; import static javax.swing.SwingUtilities.getWindowAncestor; -import static pulse.properties.NumericProperties.numberFormat; import static pulse.ui.Messages.getString; import java.awt.BorderLayout; @@ -28,144 +26,125 @@ import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.text.DefaultFormatterFactory; -import javax.swing.text.NumberFormatter; +import pulse.math.Segment; import pulse.properties.NumericProperty; +import pulse.properties.NumericPropertyFormatter; import pulse.ui.components.controllers.ConfirmAction; @SuppressWarnings("serial") public class FormattedInputDialog extends JDialog { - private final static int FONT_SIZE = 14; - private final static int WIDTH = 550; - private final static int HEIGHT = 130; - private JFormattedTextField ftf; - private ConfirmAction confirmAction; - private NumberFormatter numFormatter; - - public FormattedInputDialog(NumericProperty p) { - this.setDefaultCloseOperation(HIDE_ON_CLOSE); - this.setMinimumSize(new Dimension(WIDTH, HEIGHT)); - setLocationRelativeTo(null); - - setTitle("Choose " + p.getAbbreviation(false)); - - var northPanel = new JPanel(); - - northPanel.setBorder(createEmptyBorder(5, 5, 5, 5)); - - northPanel.setLayout(new GridLayout()); - - northPanel.add(new JLabel(p.getDescriptor(true))); - northPanel.add(new JSeparator()); - northPanel.add(ftf = initFormattedTextField(p)); - add(northPanel, BorderLayout.CENTER); - // - var btnPanel = new JPanel(); - var okBtn = new JButton("OK"); - var cancelBtn = new JButton("Cancel"); - btnPanel.add(okBtn); - btnPanel.add(cancelBtn); - add(btnPanel, SOUTH); - // - cancelBtn.addActionListener(event -> { - close(); - }); - okBtn.addActionListener(event -> { - if (!ftf.isEditValid()) { // The text is invalid. - if (userSaysRevert(ftf, numFormatter, p)) // reverted - ftf.postActionEvent(); // inform the editor - } else { - try { - ftf.commitEdit(); - } catch (ParseException e) { - out.println("Could not parse merge value"); - e.printStackTrace(); - } - confirmAction.onConfirm(); - close(); - } - }); - } - - private void close() { - this.setVisible(false); - } - - public Number value() { - return (Number) ftf.getValue(); - } - - private JFormattedTextField initFormattedTextField(NumericProperty p) { - var inputTextField = new JFormattedTextField(p.getValue()); - // Set up the editor for the integer cells. - - var numberFormat = numberFormat(p, true); - numFormatter = new NumberFormatter(numberFormat); - - numFormatter.setMinimum(p.getMinimum().doubleValue()); - numFormatter.setMaximum(p.getMaximum().doubleValue()); - - numFormatter.setFormat(numberFormat); - - inputTextField.setFormatterFactory(new DefaultFormatterFactory(numFormatter)); - inputTextField.setHorizontalAlignment(SwingConstants.CENTER); - inputTextField.setFocusLostBehavior(JFormattedTextField.PERSIST); - - // React when the user presses Enter while the editor is - // active. (Tab is handled as specified by - // JFormattedTextField's focusLostBehavior property.) - inputTextField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "check"); //$NON-NLS-1$ - inputTextField.getActionMap().put("check", new AbstractAction() { //$NON-NLS-1$ - - @Override - public void actionPerformed(ActionEvent e) { - if (!inputTextField.isEditValid()) { // The text is invalid. - if (userSaysRevert(inputTextField, numFormatter, p)) { // reverted - inputTextField.postActionEvent(); // inform the editor - } - } else - try { // The text is valid, - inputTextField.commitEdit(); // so use it. - inputTextField.postActionEvent(); // stop editing - } catch (ParseException exc) { - } - } - }); - - inputTextField.setFont(inputTextField.getFont().deriveFont(FONT_SIZE)); - inputTextField.setColumns(10); - return inputTextField; - } - - public void setConfirmAction(ConfirmAction confirmAction) { - this.confirmAction = confirmAction; - } - - public ConfirmAction getConfirmAction() { - return confirmAction; - } - - private static boolean userSaysRevert(JFormattedTextField inputTextField, NumberFormatter numFormatter, - NumericProperty p) { - getDefaultToolkit().beep(); - inputTextField.selectAll(); - Object[] options = { getString("SimpleInputFrame.Edit"), //$NON-NLS-1$ - getString("SimpleInputFrame.Revert") }; //$NON-NLS-1$ - var answer = showOptionDialog(getWindowAncestor(inputTextField), - "The value must be a " + p.getValue().getClass().getSimpleName() + " between " //$NON-NLS-1$ - + numFormatter.getMinimum() + " and " //$NON-NLS-1$ - + numFormatter.getMaximum() + ".\n" //$NON-NLS-1$ - + getString("SimpleInputFrame.MessageLine1") //$NON-NLS-1$ - + getString("SimpleInputFrame.MessageLine2"), //$NON-NLS-1$ - "Invalid Text Entered", //$NON-NLS-1$ - YES_NO_OPTION, ERROR_MESSAGE, null, options, options[1]); - - if (answer == 1) { // Revert! - inputTextField.setValue(inputTextField.getValue()); - return true; - } - return false; - } - -} \ No newline at end of file + private final static int WIDTH = 550; + private final static int HEIGHT = 130; + private JFormattedTextField ftf; + private ConfirmAction confirmAction; + + public FormattedInputDialog(NumericProperty p) { + this.setDefaultCloseOperation(HIDE_ON_CLOSE); + this.setMinimumSize(new Dimension(WIDTH, HEIGHT)); + setLocationRelativeTo(null); + + setTitle("Choose " + p.getAbbreviation(false)); + + var northPanel = new JPanel(); + + northPanel.setBorder(createEmptyBorder(5, 5, 5, 5)); + + northPanel.setLayout(new GridLayout()); + + northPanel.add(new JLabel(p.getDescriptor(true))); + northPanel.add(new JSeparator()); + northPanel.add(ftf = initFormattedTextField(p)); + + add(northPanel, BorderLayout.CENTER); + // + var btnPanel = new JPanel(); + var okBtn = new JButton("OK"); + var cancelBtn = new JButton("Cancel"); + btnPanel.add(okBtn); + btnPanel.add(cancelBtn); + add(btnPanel, SOUTH); + // + cancelBtn.addActionListener(event -> { + close(); + }); + okBtn.addActionListener(event -> { + confirmAction.onConfirm(); + close(); + }); + + } + + private void close() { + this.setVisible(false); + } + + private JFormattedTextField initFormattedTextField(NumericProperty p) { + var numFormatter = new NumericPropertyFormatter(p, true, false); + + var inputTextField = new JFormattedTextField(numFormatter); + + inputTextField.setValue(p); + inputTextField.setHorizontalAlignment(SwingConstants.CENTER); + + // React when the user presses Enter while the editor is + // active. (Tab is handled as specified by + // JFormattedTextField's focusLostBehavior property.) + inputTextField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "check"); //$NON-NLS-1$ + inputTextField.getActionMap().put("check", new AbstractAction() { //$NON-NLS-1$ + + @Override + public void actionPerformed(ActionEvent e) { + if (!inputTextField.isEditValid()) { // The text is invalid. + if (userSaysRevert(inputTextField, numFormatter, p)) { // reverted + inputTextField.postActionEvent(); // inform the editor + } + } else + try { // The text is valid, + inputTextField.commitEdit(); // so use it. + inputTextField.postActionEvent(); // stop editing + } catch (ParseException exc) { + } + } + }); + + inputTextField.setColumns(10); + return inputTextField; + } + + public void setConfirmAction(ConfirmAction confirmAction) { + this.confirmAction = confirmAction; + } + + public ConfirmAction getConfirmAction() { + return confirmAction; + } + + public Number value() { + return (Number) ((NumericProperty) ftf.getValue()).getValue(); + } + + private static boolean userSaysRevert(JFormattedTextField inputTextField, NumericPropertyFormatter numFormatter, + NumericProperty p) { + getDefaultToolkit().beep(); + inputTextField.selectAll(); + Object[] options = {getString("SimpleInputFrame.Edit"), //$NON-NLS-1$ + getString("SimpleInputFrame.Revert")}; //$NON-NLS-1$ + var answer = showOptionDialog(getWindowAncestor(inputTextField), + "The value must be a " + p.getValue().getClass().getSimpleName() + " between " //$NON-NLS-1$ + + numFormatter.getBounds().getMinimum() + " and " //$NON-NLS-1$ + + numFormatter.getBounds().getMaximum() + ".\n" //$NON-NLS-1$ + + getString("SimpleInputFrame.MessageLine1") //$NON-NLS-1$ + + getString("SimpleInputFrame.MessageLine2"), //$NON-NLS-1$ + "Invalid Text Entered", //$NON-NLS-1$ + YES_NO_OPTION, ERROR_MESSAGE, null, options, options[1]); + + if (answer == 1) { // Revert! + inputTextField.setValue(inputTextField.getValue()); + return true; + } + return false; + } + +} diff --git a/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java b/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java index 609b56d2..82f2cf94 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ProgressDialog.java @@ -6,82 +6,109 @@ 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 { - private JProgressBar progressBar; - private int progress; - - public ProgressDialog() { - super(); - initComponents(); - setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); - setTitle("Please wait..."); - setPreferredSize(new Dimension(400, 75)); - pack(); - } - - /** - * Invoked when task's progress property changes. - */ - @Override - public void propertyChange(PropertyChangeEvent evt) { - if (evt.getPropertyName().equals("progress")) { - int progress = (Integer) evt.getNewValue(); - progressBar.setValue(progress); - } - } - - private void initComponents() { - progressBar = new JProgressBar(HORIZONTAL); - progressBar.setMinimum(0); - progressBar.setStringPainted(true); - getContentPane().add(progressBar); - } - - public void incrementProgress() { - progress++; - } - - private boolean reachedCapacity() { - return progress >= progressBar.getMaximum(); - } - - public void trackProgress(int maximum) { - progressBar.setMaximum(maximum); - setVisible(true); - progress = 0; - - var progressWorker = new SwingWorker() { - - @Override - protected Void doInBackground() { - setProgress(0); - while (!reachedCapacity()) { - try { - sleep(50); - } catch (InterruptedException ignore) { - } - setProgress(progress); - } - return null; - - } - - @Override - protected void done() { - setVisible(false); - } - - }; - - progressWorker.addPropertyChangeListener(this); - progressWorker.execute(); - } + private JProgressBar progressBar; + private int progress; + + public ProgressDialog() { + super(); + initComponents(); + setDefaultCloseOperation(HIDE_ON_CLOSE); + setTitle("Please wait..."); + setPreferredSize(new Dimension(400, 75)); + pack(); + } + + /** + * Invoked when task's progress property changes. + */ + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals("progress")) { + int progress = (Integer) evt.getNewValue(); + progressBar.setValue(progress); + } + } + + private void initComponents() { + progressBar = new JProgressBar(HORIZONTAL); + progressBar.setMinimum(0); + progressBar.setStringPainted(true); + getContentPane().add(progressBar); + } + + public void incrementProgress() { + progress++; + } + + private boolean reachedCapacity() { + return progress >= progressBar.getMaximum(); + } + + public void trackProgress(int maximum) { + progressBar.setMaximum(maximum); + setVisible(true); + progress = 0; + + var progressWorker = new SwingWorker() { + + @Override + protected Void doInBackground() { + setProgress(0); + while (!reachedCapacity()) { + try { + sleep(50); + } catch (InterruptedException ignore) { + } + setProgress(progress); + } + return null; + + } + + @Override + protected void done() { + setVisible(false); + } + + }; + + progressWorker.addPropertyChangeListener(this); + 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 c31af837..b1965877 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ResultChangeDialog.java @@ -1,185 +1,115 @@ package pulse.ui.frames.dialogs; -import static java.awt.GridBagConstraints.BOTH; -import static javax.swing.BorderFactory.createTitledBorder; import static javax.swing.SwingConstants.BOTTOM; -import static javax.swing.SwingConstants.VERTICAL; -import static pulse.tasks.processing.ResultFormat.generateFormat; import java.awt.BorderLayout; -import java.awt.GridBagConstraints; import javax.swing.JDialog; import javax.swing.SwingConstants; -import pulse.properties.NumericPropertyKeyword; -import pulse.ui.components.controllers.KeywordListRenderer; -import pulse.ui.components.models.ParameterListModel; -import pulse.ui.components.models.ResultListModel; +import pulse.tasks.processing.ResultFormat; +import pulse.ui.components.models.ParameterTableModel; +import pulse.ui.components.models.SelectedKeysModel; +import pulse.ui.components.panels.DoubleTablePanel; public class ResultChangeDialog extends JDialog { - /** - * - */ - private static final long serialVersionUID = 1L; - private final static int WIDTH = 500; - private final static int HEIGHT = 500; - public ResultChangeDialog() { - - setTitle("Result output formatting"); - - setSize(WIDTH, HEIGHT); - - initComponents(); - - var model = (ResultListModel) rightList.getModel(); - - moveRightBtn.addActionListener(e -> { - - var key = leftList.getSelectedValue(); - - if (key != null) - if (!model.contains(key)) - model.add(key); - - }); - - moveLeftBtn.addActionListener(e -> { - - var key = rightList.getSelectedValue(); - - if (key != null) - model.remove(key); - - }); - - commitBtn.addActionListener(e -> generateFormat(model.getData())); - cancelBtn.addActionListener(e -> this.setVisible(false)); - - } - - @Override - public void setVisible(boolean value) { - super.setVisible(value); - ((ResultListModel) rightList.getModel()).update(); - ((ParameterListModel) leftList.getModel()).update(); - } - - private void initComponents() { - java.awt.GridBagConstraints gridBagConstraints; - - MainContainer = new javax.swing.JPanel(); - leftScroller = new javax.swing.JScrollPane(); - leftList = new javax.swing.JList<>(); - rightScroller = new javax.swing.JScrollPane(); - rightList = new javax.swing.JList<>(); - moveToolbar = new javax.swing.JToolBar(); - moveRightBtn = new javax.swing.JButton(); - moveLeftBtn = new javax.swing.JButton(); - MainToolbar = new javax.swing.JToolBar(); - filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), - new java.awt.Dimension(32767, 0)); - cancelBtn = new javax.swing.JButton(); - filler3 = new javax.swing.Box.Filler(new java.awt.Dimension(25, 0), new java.awt.Dimension(25, 0), - new java.awt.Dimension(25, 32767)); - commitBtn = new javax.swing.JButton(); - filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), - new java.awt.Dimension(32767, 0)); - - setDefaultCloseOperation(HIDE_ON_CLOSE); - - MainContainer.setPreferredSize(new java.awt.Dimension(650, 400)); - MainContainer.setLayout(new java.awt.GridBagLayout()); - - leftScroller.setBorder(createTitledBorder("Available properties")); - leftList.setModel(new ParameterListModel()); - leftList.setCellRenderer(new KeywordListRenderer()); - leftList.setFixedCellHeight(50); - leftScroller.setViewportView(leftList); - - gridBagConstraints = new java.awt.GridBagConstraints(); - gridBagConstraints.fill = BOTH; - gridBagConstraints.weightx = 0.5; - gridBagConstraints.weighty = 1.0; - gridBagConstraints.insets = new java.awt.Insets(8, 5, 8, 5); - MainContainer.add(leftScroller, gridBagConstraints); - - rightScroller.setBorder(createTitledBorder("Printed output")); - - rightList.setModel(new ResultListModel()); - rightList.setCellRenderer(new KeywordListRenderer()); - rightList.setFixedCellHeight(50); - rightScroller.setViewportView(rightList); - - gridBagConstraints = new java.awt.GridBagConstraints(); - gridBagConstraints.gridx = 2; - gridBagConstraints.fill = GridBagConstraints.BOTH; - gridBagConstraints.weightx = 0.5; - gridBagConstraints.weighty = 1.0; - gridBagConstraints.insets = new java.awt.Insets(9, 5, 9, 5); - MainContainer.add(rightScroller, gridBagConstraints); - - moveToolbar.setFloatable(false); - moveToolbar.setOrientation(VERTICAL); - moveToolbar.setRollover(true); - - moveRightBtn.setText(">>"); - moveRightBtn.setFocusable(false); - moveRightBtn.setHorizontalTextPosition(SwingConstants.CENTER); - moveRightBtn.setVerticalTextPosition(SwingConstants.BOTTOM); - moveToolbar.add(moveRightBtn); - - moveLeftBtn.setText("<<"); - moveLeftBtn.setFocusable(false); - moveLeftBtn.setHorizontalTextPosition(SwingConstants.CENTER); - moveLeftBtn.setVerticalTextPosition(SwingConstants.BOTTOM); - moveToolbar.add(moveLeftBtn); - - gridBagConstraints = new java.awt.GridBagConstraints(); - gridBagConstraints.gridx = 1; - MainContainer.add(moveToolbar, gridBagConstraints); - - getContentPane().add(MainContainer, BorderLayout.CENTER); - - MainToolbar.setFloatable(false); - MainToolbar.setRollover(true); - MainToolbar.add(filler1); - - cancelBtn.setFont(new java.awt.Font("Dialog", 1, 16)); // NOI18N - cancelBtn.setText("Cancel"); - cancelBtn.setFocusable(false); - cancelBtn.setHorizontalTextPosition(SwingConstants.CENTER); - cancelBtn.setVerticalTextPosition(BOTTOM); - MainToolbar.add(cancelBtn); - MainToolbar.add(filler3); - - commitBtn.setFont(new java.awt.Font("Dialog", 1, 16)); // NOI18N - commitBtn.setText("Commit"); - commitBtn.setFocusable(false); - commitBtn.setHorizontalTextPosition(SwingConstants.CENTER); - commitBtn.setVerticalTextPosition(SwingConstants.BOTTOM); - MainToolbar.add(commitBtn); - MainToolbar.add(filler2); - - getContentPane().add(MainToolbar, BorderLayout.SOUTH); - - pack(); - } - - private javax.swing.JPanel MainContainer; - private javax.swing.JToolBar MainToolbar; - private javax.swing.JButton cancelBtn; - private javax.swing.JButton commitBtn; - private javax.swing.Box.Filler filler1; - private javax.swing.Box.Filler filler2; - private javax.swing.Box.Filler filler3; - private javax.swing.JButton moveRightBtn; - private javax.swing.JButton moveLeftBtn; - private javax.swing.JToolBar moveToolbar; - private javax.swing.JList leftList; - private javax.swing.JScrollPane leftScroller; - private javax.swing.JList rightList; - private javax.swing.JScrollPane rightScroller; - -} \ No newline at end of file + private final static int WIDTH = 1000; + private final static int HEIGHT = 600; + + public ResultChangeDialog() { + setTitle("Result output formatting"); + initComponents(); + setSize(WIDTH, HEIGHT); + var model = (SelectedKeysModel) rightTbl.getModel(); + commitBtn.addActionListener(e -> ResultFormat.generateFormat(model.getData())); + cancelBtn.addActionListener(e -> this.setVisible(false)); + } + + @Override + public void setVisible(boolean value) { + super.setVisible(value); + ((SelectedKeysModel) rightTbl.getModel()).update(); + ((ParameterTableModel) leftTbl.getModel()).populateWithAllProperties(); + } + + private void initComponents() { + MainToolbar = new javax.swing.JToolBar(); + filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), + new java.awt.Dimension(32767, 0)); + cancelBtn = new javax.swing.JButton(); + filler3 = new javax.swing.Box.Filler(new java.awt.Dimension(25, 0), new java.awt.Dimension(25, 0), + new java.awt.Dimension(25, 32767)); + commitBtn = new javax.swing.JButton(); + filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), + new java.awt.Dimension(32767, 0)); + + setDefaultCloseOperation(HIDE_ON_CLOSE); + + leftTbl = new javax.swing.JTable() { + + @Override + 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; + } + ; + + }; + + 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); + MainToolbar.setRollover(true); + MainToolbar.add(filler1); + + cancelBtn.setText("Cancel"); + cancelBtn.setFocusable(false); + cancelBtn.setHorizontalTextPosition(SwingConstants.CENTER); + cancelBtn.setVerticalTextPosition(BOTTOM); + MainToolbar.add(cancelBtn); + MainToolbar.add(filler3); + + commitBtn.setText("Commit"); + commitBtn.setFocusable(false); + commitBtn.setHorizontalTextPosition(SwingConstants.CENTER); + commitBtn.setVerticalTextPosition(SwingConstants.BOTTOM); + MainToolbar.add(commitBtn); + MainToolbar.add(filler2); + + getContentPane().add(MainToolbar, BorderLayout.SOUTH); + + pack(); + } + + private javax.swing.JPanel MainContainer; + private javax.swing.JToolBar MainToolbar; + private javax.swing.JButton cancelBtn; + private javax.swing.JButton commitBtn; + private javax.swing.Box.Filler filler1; + private javax.swing.Box.Filler filler2; + private javax.swing.Box.Filler filler3; + private javax.swing.JTable leftTbl; + private javax.swing.JTable rightTbl; + +} diff --git a/src/main/java/pulse/ui/frames/dialogs/package-info.java b/src/main/java/pulse/ui/frames/dialogs/package-info.java index 78fd6320..ab04ede4 100644 --- a/src/main/java/pulse/ui/frames/dialogs/package-info.java +++ b/src/main/java/pulse/ui/frames/dialogs/package-info.java @@ -1 +1 @@ -package pulse.ui.frames.dialogs; \ No newline at end of file +package pulse.ui.frames.dialogs; diff --git a/src/main/java/pulse/ui/frames/package-info.java b/src/main/java/pulse/ui/frames/package-info.java index d159c12d..7ae2bff5 100644 --- a/src/main/java/pulse/ui/frames/package-info.java +++ b/src/main/java/pulse/ui/frames/package-info.java @@ -2,5 +2,4 @@ * Contains all JFrame classes that are used to create the graphical user * interface of {@code PULsE}. */ - -package pulse.ui.frames; \ No newline at end of file +package pulse.ui.frames; diff --git a/src/main/java/pulse/ui/package-info.java b/src/main/java/pulse/ui/package-info.java index 6ae5a6c4..f215bab1 100644 --- a/src/main/java/pulse/ui/package-info.java +++ b/src/main/java/pulse/ui/package-info.java @@ -4,5 +4,4 @@ * 'message.properties' text file, which is used for storing verbose text * constants used e.g. by the GUI. */ - -package pulse.ui; \ No newline at end of file +package pulse.ui; diff --git a/src/main/java/pulse/util/Accessible.java b/src/main/java/pulse/util/Accessible.java index 0ee0047e..9cc28326 100644 --- a/src/main/java/pulse/util/Accessible.java +++ b/src/main/java/pulse/util/Accessible.java @@ -25,248 +25,253 @@ *

* */ - public abstract class Accessible extends Group { - /** - *

- * Searches for a {@code Property} in this {@code Accessible} that looks - * {@code similar} to the argument. Determines whether the {@code similar} is a - * {@code NumericProperty} or a generic property and calls the suitable method - * in this class. - *

- * - * @param similar a generic or a numeric {@code Property} - * @return the matching property of this {@code Accessible} - */ - - public Property property(Property similar) { - if (similar instanceof NumericProperty) - return numericProperty(((NumericProperty) similar).getType()); - else - return genericProperty(similar); - } - - /** - * Tries to access the property getter methods in this {@code Accessible}, which - * should be declared as no-argument methods with a specific return type. - * - * @return This will return a unique {@code Set} containing all - * instances of {@code NumericProperty} belonging to this - * {@code Accessible}. This set will not contain any duplicate elements - * by definition. - * @see pulse.properties.NumericProperty.equal(Object) - */ - - public Set numericProperties() { - Set fields = new TreeSet<>(); - - var methods = this.getClass().getMethods(); - for (var m : methods) { - - if (m.getParameterCount() > 0) - continue; - - if (NumericProperty.class.isAssignableFrom(m.getReturnType())) + /** + *

+ * Searches for a {@code Property} in this {@code Accessible} that looks + * {@code similar} to the argument. Determines whether the {@code similar} + * is a {@code NumericProperty} or a generic property and calls the suitable + * method in this class. + *

+ * + * @param similar a generic or a numeric {@code Property} + * @return the matching property of this {@code Accessible} + */ + public Property property(Property similar) { + if (similar instanceof NumericProperty) { + return numericProperty(((NumericProperty) similar).getType()); + } else { + return genericProperty(similar); + } + } + + /** + * Tries to access the property getter methods in this {@code Accessible}, + * which should be declared as no-argument methods with a specific return + * type. + * + * @return This will return a unique {@code Set} containing + * all instances of {@code NumericProperty} belonging to this + * {@code Accessible}. This set will not contain any duplicate elements by + * definition. + * @see pulse.properties.NumericProperty.equal(Object) + */ + public Set numericProperties() { + Set fields = new TreeSet<>(); + + var methods = this.getClass().getMethods(); + for (var m : methods) { + + if (m.getParameterCount() > 0) { + continue; + } + + if (NumericProperty.class.isAssignableFrom(m.getReturnType())) try { - var obj = m.invoke(this); - if (obj != null) - fields.add((NumericProperty) m.invoke(this)); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - err.println("Error invoking method " + m); - e.printStackTrace(); - } - - } - - return fields; - - } - - /** - * Tries to access the property getter methods in this {@code Accessible}, which - * should be declared as no-argument methods with a specific return type. - * - * @return This will return a {@code List} containing all properties - * belonging to this {@code Accessible}, which are not assignable from - * the {@code NumericProperty} class. - */ - - public List genericProperties() { - List fields = new ArrayList<>(); - - var methods = this.getClass().getMethods(); - for (var m : methods) { + var obj = m.invoke(this); + if (obj != null) { + fields.add((NumericProperty) m.invoke(this)); + } + } catch (IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { + err.println("Error invoking method " + m); + e.printStackTrace(); + } + + } + + return fields; + + } + + /** + * Tries to access the property getter methods in this {@code Accessible}, + * which should be declared as no-argument methods with a specific return + * type. + * + * @return This will return a {@code List} containing all + * properties belonging to this {@code Accessible}, which are not assignable + * from the {@code NumericProperty} class. + */ + public List genericProperties() { + List fields = new ArrayList<>(); + + var methods = this.getClass().getMethods(); + for (var m : methods) { + + //getters only + if (m.getParameterCount() == 0) { + + 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(); + } - if (m.getParameterCount() > 0) - continue; + } - 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(); - } - - } - /* + } + /* * Get access to the properties of accessibles contained in this accessible - */ + */ // for (var a : accessibleChildren()) { // fields.addAll(a.genericProperties()); // } - - return fields; - - } - - /** - *

- * Recursively searches for a {@code NumericProperty} from the unique set of - * numeric properties in this {@code Accessible} by comparing its - * {@code NumericPropertyKeyword} to {@code type}. This will search for this - * property through the children of this object, through the children of their - * children, etc. - *

- * - * @param type the type of the {@code NumericProperty}. - * @return the respective {@code NumericProperty}, or {@code null} if nothing is - * found. - * @see numericProperties() - */ - - public NumericProperty numericProperty(NumericPropertyKeyword type) { - - var match = numericProperties().stream().filter(p -> p.getType() == type).findFirst(); - - if (match.isPresent()) - return match.get(); - - NumericProperty property = null; - - for (var accessible : accessibleChildren()) { - property = accessible.numericProperty(type); - if (property != null) - break; - } - - return property; - - } - - /** - *

- * Recursively searches for a {@code Property} from the non-unique list of - * generic properties in this {@code Accessible} by comparing its class to - * {@code sameClass.getClass()}. This will search for this property through the - * children of this object, through the children of their children, etc. - *

- * - * @param sameClass the class identifying this {@code Property}. - * @return the respective {@code Property}, or {@code null} if nothing is found. - * @see genericProperties() - */ - - public Property genericProperty(Property sameClass) { - - var match = genericProperties().stream().filter(p -> p.identifier().equals(sameClass.identifier())) - .collect(Collectors.toList()); - - Property result = null; - - switch (match.size()) { - case 0: - - break; - // just one matching element found - case 1: - result = match.get(0); - break; - // several possible matches found; use other criteria - default: - throw new IllegalArgumentException("Too many matches found: " + sameClass + " : " + match.size()); - } - - return result; - - } - - /** - *

- * An abstract method, which must be overriden to gain access over setting the - * values of all relevant (selected by the programmer) {@code NumericPropert}ies - * in subclasses of {@code Accessible}. Typically this involves a {@code switch} - * statement that goes through the different options for the {@code type} and - * invokes different {@code set(...)} methods to update the matching - * {@code NumericProperty} with {@code property}. - *

- * - * @param type the type, which must be equal by definition to - * {@code property.getType()}. - * @param property the property, which contains new information. - */ - - public abstract void set(NumericPropertyKeyword type, NumericProperty property); - - /** - * Runs recursive search for a property in this {@code Accessible} object with - * the same identifier as {@code property} and sets its value to the value of - * the {@code property} parameter.If {@code property} is a - * {@code NumericProperty}, uses its {@code NumericPropertyKeyword} for - * identification. For generic properties, calls {@code attemptUpdate}. - * - * @param property the {@code Property}, which will update a similar property of - * this {@code Accessible}. - * @see Property.attemptUpdate(Property) - */ - - public void update(Property property) { - - if (property instanceof NumericProperty) - update((NumericProperty) property); - else { - var p = genericProperty(property); - - if (p == null) - accessibleChildren().stream().forEach(c -> c.update(property)); - else - p.attemptUpdate(property.getValue()); - } - - } - - /** - * Set a NumericProperty contained in this Accessible or any of its accessible - * childern, using the NumericPropertyKeyword of the argument as identifier and - * its value. - * - * @param p a NumericProperty - * @see Accessible.accessibleChildren() - */ - - public void update(NumericProperty p) { - this.set(p.getType(), p); - for (var a : accessibleChildren()) - a.update(p); - } - - /** - *

- * Selects only those {@code Accessible}s, the parent of which is {@code this}. - * Note that all {@code Accessible}s are required to explicitly adopt children - * by calling the {@code setParent()} method. - *

- * - * @return a {@code List} of children that this {@code Accessible} has adopted. - * @see children - */ - - public List accessibleChildren() { - return children().stream().filter(group -> group instanceof Accessible).map(acGroup -> (Accessible) acGroup) - .collect(toList()); - } - -} \ No newline at end of file + return fields; + + } + + /** + *

+ * Recursively searches for a {@code NumericProperty} from the unique set of + * numeric properties in this {@code Accessible} by comparing its + * {@code NumericPropertyKeyword} to {@code type}. This will search for this + * property through the children of this object, through the children of + * their children, etc. + *

+ * + * @param type the type of the {@code NumericProperty}. + * @return the respective {@code NumericProperty}, or {@code null} if + * nothing is found. + * @see numericProperties() + */ + public NumericProperty numericProperty(NumericPropertyKeyword type) { + + var match = numericProperties().stream().filter(p -> p.getType() == type).findFirst(); + + if (match.isPresent()) { + return match.get(); + } + + NumericProperty property = null; + + for (var accessible : accessibleChildren()) { + property = accessible.numericProperty(type); + if (property != null) { + break; + } + } + + return property; + + } + + /** + *

+ * Recursively searches for a {@code Property} from the non-unique list of + * generic properties in this {@code Accessible} by comparing its class to + * {@code sameClass.getClass()}. This will search for this property through + * the children of this object, through the children of their children, etc. + *

+ * + * @param sameClass the class identifying this {@code Property}. + * @return the respective {@code Property}, or {@code null} if nothing is + * found. + * @see genericProperties() + */ + public Property genericProperty(Property sameClass) { + + var match = genericProperties().stream().filter(p -> p.identifier().equals(sameClass.identifier())) + .collect(Collectors.toList()); + + Property result = null; + + switch (match.size()) { + case 0: + + break; + // just one matching element found + case 1: + result = match.get(0); + break; + // several possible matches found; use other criteria + default: + throw new IllegalArgumentException("Too many matches found: " + sameClass + " : " + match.size()); + } + + return result; + + } + + /** + *

+ * An abstract method, which must be overriden to gain access over setting + * the values of all relevant (selected by the programmer) + * {@code NumericPropert}ies in subclasses of {@code Accessible}. Typically + * this involves a {@code switch} statement that goes through the different + * options for the {@code type} and invokes different {@code set(...)} + * methods to update the matching {@code NumericProperty} with + * {@code property}. + *

+ * + * @param type the type, which must be equal by definition to + * {@code property.getType()}. + * @param property the property, which contains new information. + */ + public abstract void set(NumericPropertyKeyword type, NumericProperty property); + + /** + * Runs recursive search for a property in this {@code Accessible} object + * with the same identifier as {@code property} and sets its value to the + * value of the {@code property} parameter.If {@code property} is a + * {@code NumericProperty}, uses its {@code NumericPropertyKeyword} for + * identification. For generic properties, calls {@code attemptUpdate}. + * + * @param property the {@code Property}, which will update a similar + * property of this {@code Accessible}. + * @see Property.attemptUpdate(Property) + */ + public void update(Property property) { + + if (property instanceof NumericProperty) { + update((NumericProperty) property); + } else { + var p = genericProperty(property); + + if (p == null) { + accessibleChildren().stream().forEach(c -> c.update(property)); + } else { + p.attemptUpdate(property.getValue()); + } + } + + } + + /** + * Set a NumericProperty contained in this Accessible or any of its + * accessible childern, using the NumericPropertyKeyword of the argument as + * identifier and its value. + * + * @param p a NumericProperty + * @see Accessible.accessibleChildren() + */ + public void update(NumericProperty p) { + this.set(p.getType(), p); + for (var a : accessibleChildren()) { + a.update(p); + } + } + + /** + *

+ * Selects only those {@code Accessible}s, the parent of which is + * {@code this}. Note that all {@code Accessible}s are required to + * explicitly adopt children by calling the {@code setParent()} method. + *

+ * + * @return a {@code List} of children that this {@code Accessible} has + * adopted. + * @see children + */ + public List accessibleChildren() { + return children().stream().filter(group -> group instanceof Accessible).map(acGroup -> (Accessible) acGroup) + .collect(toList()); + } + +} diff --git a/src/main/java/pulse/util/Descriptive.java b/src/main/java/pulse/util/Descriptive.java index 91ddc32f..23909ad3 100644 --- a/src/main/java/pulse/util/Descriptive.java +++ b/src/main/java/pulse/util/Descriptive.java @@ -2,24 +2,22 @@ /** * Provides the {@code describe()} functionality. - * + * * @see pulse.io.export.Exporter */ - public interface Descriptive { - /** - * Creates a {@code String} 'describing' this object, usually for exporting - * purposes. - * - * @return by default, this will return the name of the implementing class and - * the date of the calculation. - */ - - public default String describe() { + /** + * Creates a {@code String} 'describing' this object, usually for exporting + * purposes. + * + * @return by default, this will return the name of the implementing class + * and the date of the calculation. + */ + public default String describe() { - return getClass().getSimpleName(); + return getClass().getSimpleName(); - } + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/util/DescriptorChangeListener.java b/src/main/java/pulse/util/DescriptorChangeListener.java index 513c0130..4edbe51d 100644 --- a/src/main/java/pulse/util/DescriptorChangeListener.java +++ b/src/main/java/pulse/util/DescriptorChangeListener.java @@ -1,7 +1,9 @@ package pulse.util; -public interface DescriptorChangeListener { +import java.io.Serializable; - public void onDescriptorChanged(); +public interface DescriptorChangeListener extends Serializable { -} \ No newline at end of file + public void onDescriptorChanged(); + +} diff --git a/src/main/java/pulse/util/DiscreteSelector.java b/src/main/java/pulse/util/DiscreteSelector.java index fc2552eb..beb52254 100644 --- a/src/main/java/pulse/util/DiscreteSelector.java +++ b/src/main/java/pulse/util/DiscreteSelector.java @@ -10,72 +10,74 @@ public class DiscreteSelector implements Property { - private Set allOptions; - private T defaultSelection; - private T selection; - - private List listeners; - - public DiscreteSelector(AbstractReader reader, String directory, String listLocation) { - allOptions = ReaderManager.load(reader, directory, listLocation); - listeners = new ArrayList<>(); - } - - public void addListener(DescriptorChangeListener l) { - listeners.add(l); - } - - public List getListeners() { - return listeners; - } - - public void fireDescriptorChange() { - for(var l : listeners) - l.onDescriptorChanged(); - } - - @Override - public String toString() { - return selection.toString(); - } - - @Override - public Object getValue() { - return selection; - } - - @Override - public String getDescriptor(boolean addHtmlTags) { - return selection.describe(); - } - - @Override - public boolean attemptUpdate(Object value) { - selection = find(value.toString()); - - if(selection == null) - return false; - - fireDescriptorChange(); - return true; - } - - public T find(String name) { - var optional = allOptions.stream().filter(t -> t.toString().equalsIgnoreCase(name)).findAny(); - return optional.get(); - } - - public T getDefaultSelection() { - return defaultSelection; - } - - public void setDefaultSelection(String name) { - defaultSelection = allOptions.stream().filter(d -> d.toString().equals(name)).findAny().get(); - selection = defaultSelection; - } - - public Set getAllOptions() { - return allOptions; - } - -} \ No newline at end of file + private Set allOptions; + private T defaultSelection; + private T selection; + + private List listeners; + + public DiscreteSelector(AbstractReader reader, String directory, String listLocation) { + allOptions = ReaderManager.load(reader, directory, listLocation); + listeners = new ArrayList<>(); + } + + public void addListener(DescriptorChangeListener l) { + listeners.add(l); + } + + public List getListeners() { + return listeners; + } + + public void fireDescriptorChange() { + for (var l : listeners) { + l.onDescriptorChanged(); + } + } + + @Override + public String toString() { + return selection.toString(); + } + + @Override + public Object getValue() { + return selection; + } + + @Override + public String getDescriptor(boolean addHtmlTags) { + return selection.describe(); + } + + @Override + public boolean attemptUpdate(Object value) { + selection = find(value.toString()); + + if (selection == null) { + return false; + } + + fireDescriptorChange(); + return true; + } + + public T find(String name) { + var optional = allOptions.stream().filter(t -> t.toString().equalsIgnoreCase(name)).findAny(); + return optional.get(); + } + + public T getDefaultSelection() { + return defaultSelection; + } + + public void setDefaultSelection(String name) { + defaultSelection = allOptions.stream().filter(d -> d.toString().equals(name)).findAny().get(); + selection = defaultSelection; + } + + public Set getAllOptions() { + return allOptions; + } + +} diff --git a/src/main/java/pulse/util/FunctionSerializer.java b/src/main/java/pulse/util/FunctionSerializer.java new file mode 100644 index 00000000..65911a8b --- /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 95da82d6..cbdfc35c 100644 --- a/src/main/java/pulse/util/Group.java +++ b/src/main/java/pulse/util/Group.java @@ -10,117 +10,116 @@ public class Group extends UpwardsNavigable { - /** - *

- * Tries to access getter methods to retrieve all {@code Accessible} instances - * belonging to this object. Ignores any methods that return instances of the - * same class as {@code this} one. - *

- * - * @return a {@code List} containing {@code Accessible} objects which could be - * accessed by the declared getter methods. - */ - - public List subgroups() { - var fields = new ArrayList(); - - var methods = this.getClass().getMethods(); - for (var m : methods) { - if (m.getParameterCount() > 0) - continue; - - if (!Group.class.isAssignableFrom(m.getReturnType())) - continue; - - Group a = null; - - try { - a = (Group) m.invoke(this); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - System.err.println("Failed to invoke " + m + " Details: "); - e.printStackTrace(); - } - - /* Ignore null, factor/instance methods returning same accessibles */ - if (a == null || a.getDescriptor().equals(getDescriptor())) - continue; - - fields.add(a); - fields.addAll(a.subgroups()); - - } - - return fields; - - } - - /** - *

- * Recursively analyses all {@code Group} objects that are identified as - * subgroups to {@code root} (explicitly checks that subgroups exclude parents - * of {@code root}) and chooses those for which an {@code Exporter} exists. - *

- * - * @param root the root group. - * @return a set of unique {@code Group}s objects. - * @see pulse.util.Group.subgroups() - */ - - public static Set contents(Group root) { - var contents = root.subgroups().stream().filter(ph -> root.getParent() != ph).collect(Collectors.toSet()); - - for (var it = contents.iterator(); it.hasNext();) - contents(it.next()).stream().forEach(a -> contents.add(a)); - - return contents; - } - - /** - * Searches for a specific {@code Accessible} with a {@code simpleName}. - * - * @see subgroups - * @param simpleName the name of the {@code Accessible}, - * @return the {@code Accessible} object. - */ - - public Group access(String simpleName) { - return subgroups().stream().filter(a -> a.getSimpleName().equals(simpleName)).findFirst().get(); - } - - /** - *

- * Selects only those {@code Accessible}s, the parent of which is {@code this}. - * Note that all {@code Accessible}s are required to explicitly adopt children - * by calling the {@code setParent()} method. - *

- * - * @return a {@code List} of children that this {@code Accessible} has adopted. - * @see subgroups - */ - - public List children() { - return subgroups().stream().filter(a -> a.getParent() == this).collect(toList()); - } - - /** - * The same as {@code getSimpleName} in this implementation. - * - * @return the simple name of the declaring class. - * @see getSimpleName() - */ - - public String getDescriptor() { - return getClass().getSimpleName(); - } - - /** - * This will generate a simple name for identifying this {@code Accessible}. - * - * @return the simple name of the declaring class. - */ - - public String getSimpleName() { - return getClass().getSimpleName(); - } - -} \ No newline at end of file + /** + *

+ * Tries to access getter methods to retrieve all {@code Accessible} + * instances belonging to this object. Ignores any methods that return + * instances of the same class as {@code this} one. + *

+ * + * @return a {@code List} containing {@code Accessible} objects which could + * be accessed by the declared getter methods. + */ + public List subgroups() { + var fields = new ArrayList(); + + var methods = this.getClass().getMethods(); + for (var m : methods) { + + if (m.getParameterCount() > 0 + || !Group.class.isAssignableFrom(m.getReturnType()) + || m.getReturnType().isAssignableFrom(getClass())) { + continue; + } + + Group a = null; + + try { + a = (Group) m.invoke(this); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + System.err.println("Failed to invoke " + m + " Details: "); + e.printStackTrace(); + } + + /* Ignore null, factory/instance methods returning same accessibles */ + if (a == null || a.getDescriptor().equals(getDescriptor())) { + continue; + } + + fields.add(a); + fields.addAll(a.subgroups()); + + } + + return fields; + + } + + /** + *

+ * Recursively analyses all {@code Group} objects that are identified as + * subgroups to {@code root} (explicitly checks that subgroups exclude + * parents of {@code root}) and chooses those for which an {@code Exporter} + * exists. + *

+ * + * @param root the root group. + * @return a set of unique {@code Group}s objects. + * @see pulse.util.Group.subgroups() + */ + public static Set contents(Group root) { + var contents = root.subgroups().stream().filter(ph -> root.getParent() != ph).collect(Collectors.toSet()); + + for (var it = contents.iterator(); it.hasNext();) { + contents(it.next()).stream().forEach(a -> contents.add(a)); + } + + return contents; + } + + /** + * Searches for a specific {@code Accessible} with a {@code simpleName}. + * + * @see subgroups + * @param simpleName the name of the {@code Accessible}, + * @return the {@code Accessible} object. + */ + public Group access(String simpleName) { + return subgroups().stream().filter(a -> a.getSimpleName().equals(simpleName)).findFirst().get(); + } + + /** + *

+ * Selects only those {@code Accessible}s, the parent of which is + * {@code this}. Note that all {@code Accessible}s are required to + * explicitly adopt children by calling the {@code setParent()} method. + *

+ * + * @return a {@code List} of children that this {@code Accessible} has + * adopted. + * @see subgroups + */ + public List children() { + return subgroups().stream().filter(a -> a.getParent() == this).collect(toList()); + } + + /** + * The same as {@code getSimpleName} in this implementation. + * + * @return the simple name of the declaring class. + * @see getSimpleName() + */ + public String getDescriptor() { + return getClass().getSimpleName(); + } + + /** + * This will generate a simple name for identifying this {@code Accessible}. + * + * @return the simple name of the declaring class. + */ + public String getSimpleName() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/pulse/util/HierarchyListener.java b/src/main/java/pulse/util/HierarchyListener.java index affd18ec..c5a87c60 100644 --- a/src/main/java/pulse/util/HierarchyListener.java +++ b/src/main/java/pulse/util/HierarchyListener.java @@ -1,22 +1,22 @@ package pulse.util; +import java.io.Serializable; + /** * An hierarchy listener, which listens to any changes happening with the * children of an {@code UpwardsNavigable}. - * + * * @see pulse.util.UpwardsNavigable * */ +public interface HierarchyListener extends Serializable { -public interface HierarchyListener { - - /** - * This is invoked by the {@code UpwardsNavigable} when an event resulting in a - * change of the child's property has occurred. - * - * @param property the event data. - */ - - public void onChildPropertyChanged(PropertyEvent property); + /** + * This is invoked by the {@code UpwardsNavigable} when an event resulting + * in a change of the child's property has occurred. + * + * @param property the event data. + */ + public void onChildPropertyChanged(PropertyEvent property); -} \ No newline at end of file +} diff --git a/src/main/java/pulse/util/ImageUtils.java b/src/main/java/pulse/util/ImageUtils.java new file mode 100644 index 00000000..b059792a --- /dev/null +++ b/src/main/java/pulse/util/ImageUtils.java @@ -0,0 +1,98 @@ +package pulse.util; + +import static java.awt.Image.SCALE_SMOOTH; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +import javax.swing.ImageIcon; + +import pulse.ui.Launcher; + +public class ImageUtils { + + private ImageUtils() { + // intentionally blank + } + + public static ImageIcon loadIcon(String path, int iconSize) { + var imageIcon = new ImageIcon(Launcher.class.getResource("/images/" + path)); // load the image to a + // imageIcon + var image = imageIcon.getImage(); // transform it + var newimg = image.getScaledInstance(iconSize, iconSize, SCALE_SMOOTH); // scale it the smooth way + return new ImageIcon(newimg); // transform it back + } + + public static ImageIcon loadIcon(String path, int iconSize, Color clr) { + var icon = loadIcon(path, iconSize); + return dye(icon, clr); + } + + /** + * Credit to Marco13 + * (https://stackoverflow.com/questions/21382966/colorize-a-picture-in-java) + */ + public static BufferedImage dye(BufferedImage image, Color color) { + int w = image.getWidth(); + int h = image.getHeight(); + BufferedImage dyed = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = dyed.createGraphics(); + g.drawImage(image, 0, 0, null); + g.setComposite(AlphaComposite.SrcAtop); + g.setColor(color); + g.fillRect(0, 0, w, h); + g.dispose(); + return dyed; + } + + /** + * Credit to Werner Kvalem Vesterås + * (https://stackoverflow.com/questions/15053214/converting-an-imageicon-to-a-bufferedimage) + */ + public static ImageIcon dye(ImageIcon icon, Color color) { + var bi = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics g = bi.createGraphics(); + // paint the Icon to the BufferedImage. + icon.paintIcon(null, g, 0, 0); + g.dispose(); + var dyedImage = dye(bi, color); + return new ImageIcon(dyedImage); + } + + /** + * Credit to bmauter + * (https://stackoverflow.com/questions/19398238/how-to-mix-two-int-colors-correctly) + */ + public static Color blend(final Color c1, final Color c2, float ratio) { + if (ratio > 1f) { + ratio = 1f; + } else if (ratio < 0f) { + ratio = 0f; + } + float iRatio = 1.0f - ratio; + + int i1 = c1.getRGB(); + int i2 = c2.getRGB(); + + int a1 = (i1 >> 24 & 0xff); + int r1 = ((i1 & 0xff0000) >> 16); + int g1 = ((i1 & 0xff00) >> 8); + int b1 = (i1 & 0xff); + + int a2 = (i2 >> 24 & 0xff); + int r2 = ((i2 & 0xff0000) >> 16); + int g2 = ((i2 & 0xff00) >> 8); + int b2 = (i2 & 0xff); + + int a = (int) ((a1 * iRatio) + (a2 * ratio)); + int r = (int) ((r1 * iRatio) + (r2 * ratio)); + int g = (int) ((g1 * iRatio) + (g2 * ratio)); + int b = (int) ((b1 * iRatio) + (b2 * ratio)); + + return new Color(a << 24 | r << 16 | g << 8 | b); + } + +} diff --git a/src/main/java/pulse/util/ImmutableDataEntry.java b/src/main/java/pulse/util/ImmutableDataEntry.java index df8db6f0..b7648294 100644 --- a/src/main/java/pulse/util/ImmutableDataEntry.java +++ b/src/main/java/pulse/util/ImmutableDataEntry.java @@ -1,53 +1,52 @@ 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 * considered to be the 'value'. - * + * * @param the key * @param the value */ - -public class ImmutableDataEntry { - private T key; - private R value; - - /** - * Constructs a new {@code DataEntry} from {@code key} and {@code value}. - * - * @param key the key. - * @param value the value associated with this {@code key}. - */ - - public ImmutableDataEntry(T key, R value) { - this.key = key; - this.value = value; - } - - /** - * Gets the key object - * - * @return the key - */ - - public T getKey() { - return key; - } - - /** - * Gets the value object - * - * @return the value - */ - - public R getValue() { - return value; - } - - @Override - public String toString() { - return "<" + key + " : " + value + ">"; - } - -} \ No newline at end of file +public class ImmutableDataEntry implements Serializable { + + private T key; + private R value; + + /** + * Constructs a new {@code DataEntry} from {@code key} and {@code value}. + * + * @param key the key. + * @param value the value associated with this {@code key}. + */ + public ImmutableDataEntry(T key, R value) { + this.key = key; + this.value = value; + } + + /** + * Gets the key object + * + * @return the key + */ + public T getKey() { + return key; + } + + /** + * Gets the value object + * + * @return the value + */ + public R getValue() { + return value; + } + + @Override + public String toString() { + return "<" + key + " : " + value + ">"; + } + +} diff --git a/src/main/java/pulse/util/ImmutablePair.java b/src/main/java/pulse/util/ImmutablePair.java index cd02f293..f01f9a49 100644 --- a/src/main/java/pulse/util/ImmutablePair.java +++ b/src/main/java/pulse/util/ImmutablePair.java @@ -1,45 +1,50 @@ package pulse.util; -public class ImmutablePair { +import java.io.Serializable; - private T anElement; - private T anotherElement; +public class ImmutablePair implements Serializable { - public ImmutablePair(T anElement, T anotherElement) { - this.anElement = anElement; - this.anotherElement = anotherElement; - } + private T anElement; + private T anotherElement; - public T getFirst() { - return anElement; - } + public ImmutablePair(T anElement, T anotherElement) { + this.anElement = anElement; + this.anotherElement = anotherElement; + } - public T getSecond() { - return anotherElement; - } + public T getFirst() { + return anElement; + } - @Override - public boolean equals(Object o) { - if (!(o instanceof ImmutablePair)) - return false; + public T getSecond() { + return anotherElement; + } - if (this == o) - return true; + @Override + public boolean equals(Object o) { + if (!(o instanceof ImmutablePair)) { + return false; + } - var ip = (ImmutablePair) o; + if (this == o) { + return true; + } - // direct order - if (this.getFirst().equals(ip.getFirst()) && this.getSecond().equals(ip.getSecond())) - return true; + var ip = (ImmutablePair) o; - // reverse order - return this.getFirst().equals(ip.getSecond()) && this.getSecond().equals(ip.getFirst()); + // direct order + if (this.getFirst().equals(ip.getFirst()) && this.getSecond().equals(ip.getSecond())) { + return true; + } - } + // reverse order + return this.getFirst().equals(ip.getSecond()) && this.getSecond().equals(ip.getFirst()); - @Override - public int hashCode() { - return anElement.hashCode() + anotherElement.hashCode(); - } + } -} \ No newline at end of file + @Override + public int hashCode() { + return anElement.hashCode() + anotherElement.hashCode(); + } + +} diff --git a/src/main/java/pulse/util/InstanceDescriptor.java b/src/main/java/pulse/util/InstanceDescriptor.java index 79652b4f..a8769a57 100644 --- a/src/main/java/pulse/util/InstanceDescriptor.java +++ b/src/main/java/pulse/util/InstanceDescriptor.java @@ -13,105 +13,111 @@ public class InstanceDescriptor implements Property { - private String selectedDescriptor = ""; - private Set allDescriptors; - private String generalDescriptor; - private int hashCode; + private String selectedDescriptor = ""; + private Set allDescriptors; + private String generalDescriptor; + private int hashCode; + + private List listeners; - private List listeners; + private static Map, Set> nameMap = new HashMap<>(); + + public InstanceDescriptor(String generalDescriptor, Class c, Object... arguments) { + if (nameMap.get(c) == null) { + nameMap.put(c, allSubclassesNames(c)); + } + this.hashCode = c.hashCode(); + allDescriptors = nameMap.get(c); + selectedDescriptor = allDescriptors.iterator().next(); + this.generalDescriptor = generalDescriptor; + listeners = new ArrayList<>(); + } + + public InstanceDescriptor(Class c, Object... arguments) { + this(c.getSimpleName(), c, arguments); + } + + public K newInstance(Class c, Object... arguments) { + return instancesOf(c, arguments).stream().filter(r -> getValue().equals(r.getClass().getSimpleName())).findAny() + .get(); + } + + @Override + public Object getValue() { + return selectedDescriptor; + } + + @Override + public boolean attemptUpdate(Object object) { + var string = object.toString(); + + if (selectedDescriptor.equals(string)) { + return false; + } + + if (!allDescriptors.contains(string)) { + throw new IllegalArgumentException("Unknown descriptor: " + selectedDescriptor); + } + + this.selectedDescriptor = string; + listeners.stream().forEach(l -> l.onDescriptorChanged()); + return true; + } + + public void setSelectedDescriptor(String selectedDescriptor) { + attemptUpdate(selectedDescriptor); + } + + @Override + public Object identifier() { + return hashCode; + } + + @Override + public String getDescriptor(boolean addHtmlTags) { + return generalDescriptor; + } + + public Set getAllDescriptors() { + return allDescriptors; + } + + @Override + public String toString() { + return selectedDescriptor; + } + + public void addListener(DescriptorChangeListener l) { + this.listeners.add(l); + } + + public List getListeners() { + return listeners; + } + + @Override + public boolean equals(Object o) { + + if (o == null) { + return false; + } + + if (o == this) { + return true; + } + + if (!(o instanceof InstanceDescriptor)) { + return false; + } + + var descriptor = (InstanceDescriptor) o; + + if (!allDescriptors.containsAll(descriptor.allDescriptors)) { + return false; + } - private static Map, Set> nameMap = new HashMap<>(); + return selectedDescriptor.equals(descriptor.selectedDescriptor); - public InstanceDescriptor(String generalDescriptor, Class c, Object... arguments) { - if (nameMap.get(c) == null) - nameMap.put(c, allSubclassesNames(c)); - this.hashCode = c.hashCode(); - allDescriptors = nameMap.get(c); - selectedDescriptor = allDescriptors.iterator().next(); - this.generalDescriptor = generalDescriptor; - listeners = new ArrayList(); - } + } - public InstanceDescriptor(Class c, Object... arguments) { - this(c.getSimpleName(), c, arguments); - } - - public K newInstance(Class c, Object... arguments) { - return instancesOf(c, arguments).stream().filter(r -> getValue().equals(r.getClass().getSimpleName())).findAny() - .get(); - } - - @Override - public Object getValue() { - return selectedDescriptor; - } - - @Override - public boolean attemptUpdate(Object object) { - if (!(object instanceof String)) - return false; - - if (selectedDescriptor.equals(object)) - return false; - - if (!allDescriptors.contains(object)) - return false; - - this.selectedDescriptor = (String) object; - listeners.stream().forEach(l -> l.onDescriptorChanged()); - return true; - } - - public void setSelectedDescriptor(String selectedDescriptor) { - attemptUpdate(selectedDescriptor); - } - - @Override - public Object identifier() { - return hashCode; - } - - @Override - public String getDescriptor(boolean addHtmlTags) { - return generalDescriptor; - } - - public Set getAllDescriptors() { - return allDescriptors; - } - - @Override - public String toString() { - return selectedDescriptor; - } - - public void addListener(DescriptorChangeListener l) { - this.listeners.add(l); - } - - public List getListeners() { - return listeners; - } - - @Override - public boolean equals(Object o) { - - if (o == null) - return false; - - if (o == this) - return true; - - if (!(o instanceof InstanceDescriptor)) - return false; - - var descriptor = (InstanceDescriptor) o; - - if (!allDescriptors.containsAll(descriptor.allDescriptors)) - return false; - - return selectedDescriptor.equals(descriptor.selectedDescriptor); - - } - -} \ No newline at end of file +} diff --git a/src/main/java/pulse/util/PropertyEvent.java b/src/main/java/pulse/util/PropertyEvent.java index cd94c241..0c74fc75 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,55 +8,52 @@ * {@code PropertyHolder}. * */ - -public class PropertyEvent { - - private Object source; - private PropertyHolder propertyHolder; - private Property property; - - /** - * Constructs an event that has happened because of {@code source}, resulting in - * an action taken on the {@code property}. - * - * @param source the originator of the event - * @param property the object of the event - */ - - public PropertyEvent(Object source, PropertyHolder propertyHolder, Property property) { - this.source = source; - this.property = property; - this.propertyHolder = propertyHolder; - } - - /** - * Gets the 'source', which is an Object that is the originator of this event. - * - * @return - */ - - public Object getSource() { - return source; - } - - /** - * Gets the property, which is related to this event. - * - * @return the related property. - */ - - public Property getProperty() { - return property; - } - - public PropertyHolder getPropertyHolder() { - return propertyHolder; - } - - @Override - public String toString() { - return "Source: " + source.getClass().getSimpleName() + " ; Holder: " - + propertyHolder.getClass().getSimpleName() + " ; Property: " + property; - } - -} \ No newline at end of file +public class PropertyEvent implements Serializable { + + private Object source; + private PropertyHolder propertyHolder; + private Property property; + + /** + * Constructs an event that has happened because of {@code source}, + * resulting in an action taken on the {@code property}. + * + * @param source the originator of the event + * @param property the object of the event + */ + public PropertyEvent(Object source, PropertyHolder propertyHolder, Property property) { + this.source = source; + this.property = property; + this.propertyHolder = propertyHolder; + } + + /** + * Gets the 'source', which is an Object that is the originator of this + * event. + * + * @return + */ + public Object getSource() { + return source; + } + + /** + * Gets the property, which is related to this event. + * + * @return the related property. + */ + public Property getProperty() { + return property; + } + + public PropertyHolder getPropertyHolder() { + return propertyHolder; + } + + @Override + public String toString() { + return "Source: " + source.getClass().getSimpleName() + " ; Holder: " + + propertyHolder.getClass().getSimpleName() + " ; Property: " + property; + } + +} diff --git a/src/main/java/pulse/util/PropertyHolder.java b/src/main/java/pulse/util/PropertyHolder.java index efe780e2..4eff7a05 100644 --- a/src/main/java/pulse/util/PropertyHolder.java +++ b/src/main/java/pulse/util/PropertyHolder.java @@ -1,9 +1,13 @@ package pulse.util; import static java.util.stream.Collectors.toList; +import static pulse.properties.NumericProperties.def; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -15,270 +19,294 @@ * properties of the {@code Accessible}. * */ - public abstract class PropertyHolder extends Accessible { - private List parameters = listedTypes(); - private List listeners; - private String prefix; - - /** - *

- * By default, this will search the children of this {@code PropertyHolder} to - * collect the types of their listed parameters recursively. Note this method is - * used only to retrieve the type and not the data! - *

- * - * @return a list of {@code Property} instances, which have been explicitly - * marked as a listed parameter for this {@code PropertyHolder}. - */ - - public List listedTypes() { - - List properties = new ArrayList<>(); - - for (var accessible : accessibleChildren()) { - if (accessible instanceof PropertyHolder) { - properties.addAll(((PropertyHolder) accessible).listedTypes()); - } - } - - return properties; - } - - public PropertyHolder() { - this.listeners = new ArrayList<>(); - } - - /** - * Checks whether {@code p} belongs to the list of parameters for this - * {@code PropertyHolder}, i.e. if the associated - * {@code NumericPropertyKeyword}s match. - * - * @param p the {@code NumericProperty} of a certain type. - * @return {@code true} if {@code p} is listed. - * @see listedParameters() - */ - - private boolean isListedNumericType(NumericProperty p) { - return isListedNumericType(p.getType()); - } - - public boolean isListedNumericType(NumericPropertyKeyword p) { - if (p == null) - return false; - - return parameters.stream().filter(pr -> pr instanceof NumericProperty) - .anyMatch(param -> ((NumericProperty) param).getType() == p); - - } - - /** - * Checks whether {@code p} belongs to the list of parameters for this - * {@code PropertyHolder}, i.e. if the properties are of the same class. - * - * @param p the {@code Property}. - * @return {@code true} if {@code p} is listed. - * @see listedParameters() - */ - - private boolean isListedGenericType(Property p) { - if (p == null) - return false; - - if (parameters.contains(null)) - parameters = listedTypes(); - - return parameters.stream().anyMatch(param -> param.getClass().equals(p.getClass())); - - } - - /** - * Checks whether {@code p}, which is either a generic or a numeric property, is - * listed as as parameter for this {@code PropertyHolder}. - * - * @param p the {@code Property} - * @return {@code true} if {@code p} is listed, {@code false} otherwise. - */ - - public boolean isListedParameter(Property p) { - return p instanceof NumericProperty ? isListedNumericType((NumericProperty) p) : isListedGenericType(p); - } - - /** - * Lists all data contained in this {@code PropertyHolder}. The data objects - * must satisfy the following conditions: (a) they must be explicitly listed; - * (b) the corresponding property must not be auto-adjustable if the details - * need to remain hidden. - * - * @return a list of data, which combines generic and numeric properties. - */ - - public List data() { - var numeric = numericData(); - var all = genericProperties().stream().filter(p -> isListedGenericType(p)).collect(toList()); - - all.addAll(numeric); - return all; - } - - /** - * Lists all numeric data contained in this {@code PropertyHolder}. The data - * objects must satisfy the following conditions: (a) they must be explicitly - * listed; (b) the corresponding property must not be auto-adjustable if the - * details need to remain hidden. - * - * @return a list of {@code Property} data. - * @see areDetailsHidden() - * @see pulse.properties.NumericProperty.isAutoAdjustable() - * @see isListedNumericType(NumericProperty) - */ - - public List numericData() { - return numericProperties().stream() - .filter(p -> (isListedNumericType(p) && (areDetailsHidden() ? !p.isAutoAdjustable() : true))) - .collect(toList()); - } - - /** - *

- * Attempts to update an {@code updatedProperty} similar to one found in this - * {@code PropertyHolder}. The call originator is declared to be the - * {@code sourceComponent}. If the originator is not the parent of this - * {@code UpwardsNavigable}, this object will tell their parent about this - * behavior. The update is done by calling the superclass method - * {@code update(Property} -- if and only if a property similar to - * {@code updatedProperty} exists and its value is not equal to the - * {@code updatedProperty}. When the update happens, this will pass the - * corresponding {@code PropertyEvent} to the available listeners. - *

- * - * @param sourceComponent the originator of the change. - * @param updatedProperty the updated property that will be assigned to this - * {@code PropertyHolder}. - * @see pulse.util.Accessible.update(Property) - */ - - public boolean updateProperty(Object sourceComponent, Property updatedProperty) { - var existing = property(updatedProperty); - - if (existing == null) { - - return accessibleChildren().stream().filter(a -> a instanceof PropertyHolder) - .anyMatch(c -> ((PropertyHolder) c).updateProperty(sourceComponent, updatedProperty)); - } - - if (existing.equals(updatedProperty)) - return false; - - update(updatedProperty); - firePropertyChanged(sourceComponent, updatedProperty); - - return true; - } - - public void firePropertyChanged(Object source, Property property) { - var event = new PropertyEvent(source, this, property); - listeners.forEach(l -> l.onPropertyChanged(event)); - - /* + private List parameters = listedTypes(); + private transient List listeners; + private String prefix; + + /** + *

+ * By default, this will search the children of this {@code PropertyHolder} + * to collect the types of their listed numeric parameters recursively. + *

+ * + * @return a set of {@code NumericPropertyKeyword} instances, which have + * been explicitly marked as a listed parameter for this + * {@code PropertyHolder}. + */ + public Set listedKeywords() { + + Set keys = new HashSet<>(); + + accessibleChildren().stream() + .filter(accessible -> (accessible instanceof PropertyHolder)) + .forEachOrdered(accessible -> { + keys.addAll(((PropertyHolder) accessible).listedKeywords()); + }); + + return keys; + + } + + /** + *

+ * By default, collects a list of default properties corresponding to types + * defined by listedKeywords(). However, this method is overridable to + * include non-numeric properties. + *

+ * + * @return a list of {@code Property} instances, which have been explicitly + * marked as a listed parameter for this {@code PropertyHolder}. + */ + public List listedTypes() { + return listedKeywords().stream().map(key -> def(key)).collect(Collectors.toList()); + } + + public void initListeners() { + super.initListeners(); + listeners = new ArrayList<>(); + } + + /** + * Checks whether {@code p} belongs to the list of parameters for this + * {@code PropertyHolder}, i.e. if the associated + * {@code NumericPropertyKeyword}s match. + * + * @param p the {@code NumericProperty} of a certain type. + * @return {@code true} if {@code p} is listed. + * @see listedParameters() + */ + private boolean isListedNumericType(NumericProperty p) { + return isListedNumericType(p.getType()); + } + + public boolean isListedNumericType(NumericPropertyKeyword p) { + if (p == null) { + return false; + } + + return listedTypes().stream().filter(pr -> pr instanceof NumericProperty) + .anyMatch(param -> ((NumericProperty) param).getType() == p); + + } + + /** + * Checks whether {@code p} belongs to the list of parameters for this + * {@code PropertyHolder}, i.e. if the properties are of the same class. + * + * @param p the {@code Property}. + * @return {@code true} if {@code p} is listed. + * @see listedParameters() + */ + private boolean isListedGenericType(Property p) { + if (p == null) { + return false; + } + + if (parameters.contains(null)) { + parameters = listedTypes(); + } + + return parameters.stream().anyMatch(param -> param.getClass().equals(p.getClass())); + + } + + /** + * Checks whether {@code p}, which is either a generic or a numeric + * property, is listed as as parameter for this {@code PropertyHolder}. + * + * @param p the {@code Property} + * @return {@code true} if {@code p} is listed, {@code false} otherwise. + */ + public boolean isListedParameter(Property p) { + return p instanceof NumericProperty ? isListedNumericType((NumericProperty) p) : isListedGenericType(p); + } + + /** + * Lists all data contained in this {@code PropertyHolder}. The data objects + * must satisfy the following conditions: (a) they must be explicitly + * listed; (b) the corresponding property must not be auto-adjustable if the + * details need to remain hidden. + * + * @return a list of data, which combines generic and numeric properties. + */ + public List data() { + var numeric = numericData(); + var all = genericProperties().stream().filter(p -> isListedGenericType(p)).collect(toList()); + + all.addAll(numeric); + return all; + } + + /** + * Lists all numeric data contained in this {@code PropertyHolder}. The data + * objects must satisfy the following conditions: (a) they must be + * explicitly listed; (b) the corresponding property must not be + * auto-adjustable if the details need to remain hidden. + * + * @return a list of {@code Property} data. + * @see areDetailsHidden() + * @see pulse.properties.NumericProperty.isAutoAdjustable() + * @see isListedNumericType(NumericProperty) + */ + public List numericData() { + return numericProperties().stream() + .filter(p -> (isListedNumericType(p) + && (areDetailsHidden() ? p.isVisibleByDefault() : true))) + .collect(toList()); + } + + /** + *

+ * Attempts to update an {@code updatedProperty} similar to one found in + * this {@code PropertyHolder}. The call originator is declared to be the + * {@code sourceComponent}. If the originator is not the parent of this + * {@code UpwardsNavigable}, this object will tell their parent about this + * behaviour. The update is done by calling the superclass method + * {@code update(Property} -- if and only if a property similar to + * {@code updatedProperty} exists and its value is not equal to the + * {@code updatedProperty}. When the update happens, this will pass the + * corresponding {@code PropertyEvent} to the available listeners. + *

+ * + * @param sourceComponent the originator of the change. + * @param updatedProperty the updated property that will be assigned to this + * {@code PropertyHolder}. + * @see pulse.util.Accessible.update(Property) + */ + public boolean updateProperty(Object sourceComponent, Property updatedProperty) { + var existing = property(updatedProperty); + + if (existing == null) { + + return accessibleChildren().stream().filter(a -> a instanceof PropertyHolder) + .anyMatch(c -> ((PropertyHolder) c).updateProperty(sourceComponent, updatedProperty)); + } + + if (existing.equals(updatedProperty)) { + return false; + } + + update(updatedProperty); + firePropertyChanged(sourceComponent, updatedProperty); + + return true; + } + + public void firePropertyChanged(Object source, Property property) { + var event = new PropertyEvent(source, this, property); + if (listeners != null) { + listeners.forEach(l -> l.onPropertyChanged(event)); + } + + /* * If the changes are triggered by an external GUI component (such as * PropertyHolderTable), inform parents about this - */ - - if (source != getParent()) - tellParent(event); - - } - - /** - * This method will update this {@code PropertyHolder} with all properties that - * are contained in a different {@code propertyHolder}, if they also are present - * in the former. - * - * @param sourceComponent the source of the change - * @param propertyHolder another {@code PropertyHolder} - * @see updateProperty(Object, Property) - */ - - public void updateProperties(Object sourceComponent, PropertyHolder propertyHolder) { - propertyHolder.data().stream().forEach(entry -> this.updateProperty(sourceComponent, entry)); - } - - public void removeHeatingCurveListeners() { - this.listeners.clear(); - } - - public void addListener(PropertyHolderListener l) { - this.listeners.add(l); - } - - public List getListeners() { - return listeners; - } - - /** - * By default, this is set to {@code false}. If the overriding subclass sets - * this to {@code true}, only those {@code NumericPropert}ies that have the - * {@code autoAdjustable} flag set {@code false} will be shown. - * - * @return {@code true} if the auto-adjustable numeric properties need to stay - * hidden, {@code false} otherwise. - * @see pulse.properties.NumericProperty.isAutoAdjustable() - */ - - public boolean areDetailsHidden() { - return false; - } - - public void parameterListChanged() { - this.parameters = listedTypes(); - } - - /** - * Should {@code Accessible}s that belong to this {@code PropertyHolder} be - * ignored when this {@code PropertyHolder} is displayed in a table? - * - * @return {@code false} by default - * @see pulse.ui.components.PropertyHolderTable - */ - - public boolean ignoreSiblings() { - return false; - } - - @Override - public String describe() { - if (prefix == null) - return super.describe(); - - var id = identify(); - - if (id == null) - return super.describe(); - - if (!prefix.trim().isEmpty()) - return prefix + "_" + id.getValue(); - else - return describe() + "_" + id.getValue(); - } - - public String getPrefix() { - return prefix; - } - - /** - * If not null, will return the prefix, otherwise calls the superclass method. - * - * @return the descriptor - */ - - public String getDescriptor() { - return prefix != null ? getPrefix() : super.getDescriptor(); - } - - public void setPrefix(String prefix) { - this.prefix = prefix; - } - -} \ No newline at end of file + */ + if (source != getParent()) { + tellParent(event); + } + + } + + /** + * This method will update this {@code PropertyHolder} with all properties + * that are contained in a different {@code propertyHolder}, if they also + * are present in the former. + * + * @param sourceComponent the source of the change + * @param propertyHolder another {@code PropertyHolder} + * @see updateProperty(Object, Property) + */ + public void updateProperties(Object sourceComponent, PropertyHolder propertyHolder) { + propertyHolder.data().stream().forEach(entry -> this.updateProperty(sourceComponent, entry)); + } + + 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); + } + + public List getListeners() { + return listeners; + } + + /** + * By default, this is set to {@code false}. If the overriding subclass sets + * this to {@code true}, only those {@code NumericPropert}ies that have the + * {@code autoAdjustable} flag set {@code false} will be shown. + * + * @return {@code true} if the auto-adjustable numeric properties need to + * stay hidden, {@code false} otherwise. + * @see pulse.properties.NumericProperty.isAutoAdjustable() + */ + public boolean areDetailsHidden() { + return false; + } + + public void parameterListChanged() { + this.parameters = listedTypes(); + } + + /** + * Should {@code Accessible}s that belong to this {@code PropertyHolder} be + * ignored when this {@code PropertyHolder} is displayed in a table? + * + * @return {@code false} by default + * @see pulse.ui.components.PropertyHolderTable + */ + public boolean ignoreSiblings() { + return false; + } + + @Override + public String describe() { + if (prefix == null) { + return super.describe(); + } + + var id = identify(); + + if (id == null) { + return super.describe(); + } + + if (!prefix.trim().isEmpty()) { + return prefix + "_" + id.getValue(); + } else { + return describe() + "_" + id.getValue(); + } + } + + public String getPrefix() { + return prefix; + } + + /** + * If not null, will return the prefix, otherwise calls the superclass + * method. + * + * @return the descriptor + */ + @Override + public String getDescriptor() { + return prefix != null ? getPrefix() : super.getDescriptor(); + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + +} diff --git a/src/main/java/pulse/util/PropertyHolderListener.java b/src/main/java/pulse/util/PropertyHolderListener.java index fbcff235..aa3e2b72 100644 --- a/src/main/java/pulse/util/PropertyHolderListener.java +++ b/src/main/java/pulse/util/PropertyHolderListener.java @@ -1,19 +1,20 @@ 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 extends Serializable { -public interface PropertyHolderListener { - - /** - * This event is triggered by any {@code PropertyHolder}, the properties of - * which have been changed. - * - * @param event the event associated with actions taken on a {@code Property}. - */ - - public void onPropertyChanged(PropertyEvent event); + /** + * This event is triggered by any {@code PropertyHolder}, the properties of + * which have been changed. + * + * @param event the event associated with actions taken on a + * {@code Property}. + */ + public void onPropertyChanged(PropertyEvent event); } diff --git a/src/main/java/pulse/util/Reflexive.java b/src/main/java/pulse/util/Reflexive.java index 7dcc41b0..07b1d4b4 100644 --- a/src/main/java/pulse/util/Reflexive.java +++ b/src/main/java/pulse/util/Reflexive.java @@ -10,63 +10,60 @@ * its available subclasses. * */ - public interface Reflexive { - public static List instancesOf(Class reflexiveType, Object... params) { - return instancesOf(reflexiveType, reflexiveType.getPackage().getName(), params); - } - - public static List instancesOf(Class reflexiveType, String pckgname) { - return instancesOf(reflexiveType, pckgname, new Object[0]); - } - - /** - * Uses the {@code ReflexiveFinder} to create a list of simple instance of - * {@code reflexiveType} generated by any classes listed in the package - * {@code pckgname}. - * - * @see ReflexiveFinder.simpleInstances(String) - * @param a class implementing {@code Reflexive} - * @param reflexiveType a class that extends {@code T} - * @param pckgname the String with the package name - * @return a list of {@code Reflexive} conforming with the conditions above. - */ + public static List instancesOf(Class reflexiveType, Object... params) { + return instancesOf(reflexiveType, reflexiveType.getPackage().getName(), params); + } - @SuppressWarnings("unchecked") - public static List instancesOf(Class reflexiveType, String pckgname, Object... params) { - return (List) ReflexiveFinder.simpleInstances(pckgname, params).stream() - .filter(r -> reflexiveType.isAssignableFrom(r.getClass())).collect(Collectors.toList()); - } + public static List instancesOf(Class reflexiveType, String pckgname) { + return instancesOf(reflexiveType, pckgname, new Object[0]); + } - /** - * Uses the {@code ReflexiveFinder} to create a list of simple instance of - * {@code reflexiveType} generated by any classes listed in the same package - * where the {@code reflexiveType} is found. - * - * @see ReflexiveFinder.simpleInstances(String) - * @param a class implementing {@code Reflexive} - * @param reflexiveType a class that extends {@code T} - * @return a list of {@code Reflexive} conforming with the conditions above. - */ + /** + * Uses the {@code ReflexiveFinder} to create a list of simple instance of + * {@code reflexiveType} generated by any classes listed in the package + * {@code pckgname}. + * + * @see ReflexiveFinder.simpleInstances(String) + * @param a class implementing {@code Reflexive} + * @param reflexiveType a class that extends {@code T} + * @param pckgname the String with the package name + * @return a list of {@code Reflexive} conforming with the conditions above. + */ + @SuppressWarnings("unchecked") + public static List instancesOf(Class reflexiveType, String pckgname, Object... params) { + return (List) ReflexiveFinder.simpleInstances(pckgname, params).stream() + .filter(r -> reflexiveType.isAssignableFrom(r.getClass())).collect(Collectors.toList()); + } - public static List instancesOf(Class reflexiveType) { - return Reflexive.instancesOf(reflexiveType, reflexiveType.getPackage().getName()); - } + /** + * Uses the {@code ReflexiveFinder} to create a list of simple instance of + * {@code reflexiveType} generated by any classes listed in the same package + * where the {@code reflexiveType} is found. + * + * @see ReflexiveFinder.simpleInstances(String) + * @param a class implementing {@code Reflexive} + * @param reflexiveType a class that extends {@code T} + * @return a list of {@code Reflexive} conforming with the conditions above. + */ + public static List instancesOf(Class reflexiveType) { + return Reflexive.instancesOf(reflexiveType, reflexiveType.getPackage().getName()); + } - public static T instantiate(Class c, String descriptor) { - var opt = Reflexive.instancesOf(c).stream().filter(test -> test.getDescriptor().equals(descriptor)).findFirst(); - return opt.get(); - } + public static T instantiate(Class c, String descriptor) { + var opt = Reflexive.instancesOf(c).stream().filter(test -> test.getDescriptor().equals(descriptor)).findFirst(); + return opt.get(); + } - public static Set allDescriptors(Class c) { - return Reflexive.instancesOf(c).stream().map(t -> t.getDescriptor()).collect(Collectors.toSet()); - } + public static Set allDescriptors(Class c) { + return Reflexive.instancesOf(c).stream().map(t -> t.getDescriptor()).collect(Collectors.toSet()); + } - public static Set allSubclassesNames(Class c) { - var classes = ReflexiveFinder.classesIn(c.getPackageName()); - return classes.stream().filter(cl -> c.isAssignableFrom(cl) && !Modifier.isAbstract(cl.getModifiers())) - .map(aClass -> aClass.getSimpleName()).collect(Collectors.toSet()); - } + public static Set allSubclassesNames(Class c) { + var classes = ReflexiveFinder.classesIn(c.getPackageName()); + return classes.stream().filter(cl -> c.isAssignableFrom(cl) && !Modifier.isAbstract(cl.getModifiers())) + .map(aClass -> aClass.getSimpleName()).collect(Collectors.toSet()); + } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/util/ReflexiveFinder.java b/src/main/java/pulse/util/ReflexiveFinder.java index d052d393..056a7e62 100644 --- a/src/main/java/pulse/util/ReflexiveFinder.java +++ b/src/main/java/pulse/util/ReflexiveFinder.java @@ -23,228 +23,236 @@ * {@code Reflexive} in a {@code PULsE} package. * */ - public class ReflexiveFinder { - - private static Map>> classMap = new HashMap<>(); - private ReflexiveFinder() { - // intentionall blank - } + private static Map>> classMap = new HashMap<>(); - private static List listf(File directory) { + private ReflexiveFinder() { + // intentionall blank + } - var files = new ArrayList(); + private static List listf(File directory) { - // Get all files from a directory. - var fList = directory.listFiles(); + var files = new ArrayList(); - if (fList != null) { + // Get all files from a directory. + var fList = directory.listFiles(); - for (var file : fList) { + if (fList != null) { - if (file.isFile()) - files.add(file); - else if (file.isDirectory()) - files.addAll(listf(file)); + for (var file : fList) { - } + if (file.isFile()) { + files.add(file); + } else if (file.isDirectory()) { + files.addAll(listf(file)); + } - } + } - return files; + } - } + return files; - private static String adjustClassName(String name) { - var result = ""; - if (!name.startsWith(separator)) - result = separatorChar + name; - return result.replace('.', separatorChar); - } + } - private static String initialiseLocationPath() { - String result = null; - try { - result = ReflexiveFinder.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath(); - } catch (URISyntaxException e) { - System.err.println("Failed to initialise the general path to ReflxeiveFinder"); - e.printStackTrace(); - } - return result; - } + private static String adjustClassName(String name) { + var result = ""; + if (!name.startsWith(separator)) { + result = separatorChar + name; + } + return result.replace('.', separatorChar); + } - private static List> listClassesInDirectory(File root, String pckgname) { - List> classes = new ArrayList<>(); - var files = listf(root); + private static String initialiseLocationPath() { + String result = null; + try { + result = ReflexiveFinder.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath(); + } catch (URISyntaxException e) { + System.err.println("Failed to initialise the general path to ReflxeiveFinder"); + e.printStackTrace(); + } + return result; + } - files.stream().map(f -> { + private static List> listClassesInDirectory(File root, String pckgname) { + List> classes = new ArrayList<>(); + var files = listf(root); - var pathName = f.getName(); + files.stream().map(f -> { - for (var parent = f.getParentFile(); !parent.equals(root); parent = parent.getParentFile()) { - pathName = parent.getName() + "." + pathName; - } + var pathName = f.getName(); - return pathName; + for (var parent = f.getParentFile(); !parent.equals(root); parent = parent.getParentFile()) { + pathName = parent.getName() + "." + pathName; + } - }).forEach(path -> { - if (path.endsWith(".class")) - try { - classes.add(forName(pckgname + "." + path.substring(0, path.length() - 6))); - } catch (ClassNotFoundException e) { - System.err.println("Failed to find the .class file"); - e.printStackTrace(); - } - }); - - return classes; - } - - private static List> listClassesInJar(String locationPath, String pckgname) { - ZipInputStream zip = null; - List> classes = new ArrayList<>(); - try { - zip = new ZipInputStream(new FileInputStream(locationPath)); - } catch (FileNotFoundException e1) { - System.err.println("Cannt find the main jar file at " + locationPath); - e1.printStackTrace(); - } - - try { - for (var entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { - if (!entry.isDirectory() && entry.getName().endsWith(".class")) { - // This ZipEntry represents a class. Now, what class does it represent? - var className = entry.getName().replace('/', '.'); // including ".class" - if (!className.contains(pckgname)) - continue; - classes.add(forName(className.substring(0, className.length() - ".class".length()))); - } - } - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - } - return classes; - } - - /** - * Uses Java Reflection API to find all classes within the package named - * {@code pckgname}. Works well with .jar files. - * - * @param pckgname the name of the package. - * @return a list of {@code Class} objects. - */ - - public static List> classesIn(String pckgname) { - var name = adjustClassName(pckgname); - String locationPath = initialiseLocationPath(); - - var root = new File(locationPath + name); - - return root.isDirectory() ? listClassesInDirectory(root, pckgname) : listClassesInJar(locationPath, pckgname); - } - - @SuppressWarnings("unchecked") - private static V instanceMethod(Class aClass) { - // if the class has a getInstance() method - var methods = aClass.getMethods(); - - for (var method : methods) { - if (method.getName().equals("getInstance")) { - Object o = null; + return pathName; + + }).forEach(path -> { + if (path.endsWith(".class")) try { - o = method.invoke(null, new Object[0]); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - e.printStackTrace(); - } - if (o instanceof Reflexive) - return (V) o; - } - } - return null; - } - - @SuppressWarnings("unchecked") - private static V instanceConstructor(Class aClass, Object... params) { - var ctrs = aClass.getDeclaredConstructors(); - - outer: for (var ctr : ctrs) { - - if (isPublic(ctr.getModifiers())) { - - var types = ctr.getParameterTypes(); - - if (Integer.compare(types.length, params.length) == 0) { - - for (int i = 0; i < types.length; i++) { - if (!types[i].equals(params[i].getClass())) - if (!types[i].isAssignableFrom(params[i].getClass())) - continue outer; - } - - try { - var o = ctr.newInstance(params); - if (o instanceof Reflexive) - return (V) o; - } catch (InstantiationException | IllegalAccessException | IllegalArgumentException - | InvocationTargetException e) { - e.printStackTrace(); - } - - } - - } - - } - - return null; - - } - - /** - *

- * Finds simple instances of {@code Reflexive} subclasses within - * {@code pckgname}. A simple instance is either one that results from invoking - * a no-argument constructor or a {@code getInstance()} method. - *

- * - * @param a class implementing {@code Reflexive} - * @param pckgname the name of the package for the search - * @return a list of classes implementing {@code Reflexive} that are found in - * {@code pckgname}. - */ - - public static List simpleInstances(String pckgname, Object... params) { - List instances = new ArrayList<>(); - - //generate a class list only once - if(classMap.get(pckgname) == null) - classMap.put( pckgname, classesIn(pckgname) ); - - for (var aClass : classMap.get(pckgname)) { - - if (isAbstract(aClass.getModifiers())) - continue; - - // Try to create an instance of the object - V instance = instanceConstructor(aClass, params); - if (instance != null) - instances.add(instance); - else { - // if the class has a getInstance() method - instance = instanceMethod(aClass); - if (instance != null) - instances.add(instance); - } - - } - - return instances; - - } - - public static List simpleInstances(String pckgname) { - return simpleInstances(pckgname, new Object[0]); - } - -} \ No newline at end of file + classes.add(forName(pckgname + "." + path.substring(0, path.length() - 6))); + } catch (ClassNotFoundException e) { + System.err.println("Failed to find the .class file"); + e.printStackTrace(); + } + }); + + return classes; + } + + private static List> listClassesInJar(String locationPath, String pckgname) { + ZipInputStream zip = null; + List> classes = new ArrayList<>(); + try { + zip = new ZipInputStream(new FileInputStream(locationPath)); + } catch (FileNotFoundException e1) { + System.err.println("Cannt find the main jar file at " + locationPath); + e1.printStackTrace(); + } + + try { + for (var entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { + if (!entry.isDirectory() && entry.getName().endsWith(".class")) { + // This ZipEntry represents a class. Now, what class does it represent? + var className = entry.getName().replace('/', '.'); // including ".class" + if (!className.contains(pckgname)) { + continue; + } + classes.add(forName(className.substring(0, className.length() - ".class".length()))); + } + } + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + } + return classes; + } + + /** + * Uses Java Reflection API to find all classes within the package named + * {@code pckgname}. Works well with .jar files. + * + * @param pckgname the name of the package. + * @return a list of {@code Class} objects. + */ + public static List> classesIn(String pckgname) { + var name = adjustClassName(pckgname); + String locationPath = initialiseLocationPath(); + + var root = new File(locationPath + name); + + return root.isDirectory() ? listClassesInDirectory(root, pckgname) : listClassesInJar(locationPath, pckgname); + } + + @SuppressWarnings("unchecked") + private static V instanceMethod(Class aClass) { + // if the class has a getInstance() method + var methods = aClass.getMethods(); + + for (var method : methods) { + if (method.getName().equals("getInstance")) { + Object o = null; + try { + o = method.invoke(null, new Object[0]); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + e.printStackTrace(); + } + if (o instanceof Reflexive) { + return (V) o; + } + } + } + return null; + } + + @SuppressWarnings("unchecked") + private static V instanceConstructor(Class aClass, Object... params) { + var ctrs = aClass.getDeclaredConstructors(); + + outer: + for (var ctr : ctrs) { + + if (isPublic(ctr.getModifiers())) { + + var types = ctr.getParameterTypes(); + + if (Integer.compare(types.length, params.length) == 0) { + + for (int i = 0; i < types.length; i++) { + if (!types[i].equals(params[i].getClass())) { + if (!types[i].isAssignableFrom(params[i].getClass())) { + continue outer; + } + } + } + + try { + var o = ctr.newInstance(params); + if (o instanceof Reflexive) { + return (V) o; + } + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + e.printStackTrace(); + } + + } + + } + + } + + return null; + + } + + /** + *

+ * Finds simple instances of {@code Reflexive} subclasses within + * {@code pckgname}. A simple instance is either one that results from + * invoking a no-argument constructor or a {@code getInstance()} method. + *

+ * + * @param a class implementing {@code Reflexive} + * @param pckgname the name of the package for the search + * @return a list of classes implementing {@code Reflexive} that are found + * in {@code pckgname}. + */ + public static List simpleInstances(String pckgname, Object... params) { + List instances = new ArrayList<>(); + + //generate a class list only once + if (classMap.get(pckgname) == null) { + classMap.put(pckgname, classesIn(pckgname)); + } + + for (var aClass : classMap.get(pckgname)) { + + if (isAbstract(aClass.getModifiers())) { + continue; + } + + // Try to create an instance of the object + V instance = instanceConstructor(aClass, params); + if (instance != null) { + instances.add(instance); + } else { + // if the class has a getInstance() method + instance = instanceMethod(aClass); + if (instance != null) { + instances.add(instance); + } + } + + } + + return instances; + + } + + public static List simpleInstances(String pckgname) { + return simpleInstances(pckgname, new Object[0]); + } + +} diff --git a/src/main/java/pulse/util/ResourceMonitor.java b/src/main/java/pulse/util/ResourceMonitor.java new file mode 100644 index 00000000..b7d1ab12 --- /dev/null +++ b/src/main/java/pulse/util/ResourceMonitor.java @@ -0,0 +1,117 @@ +package pulse.util; + +import static java.lang.Runtime.getRuntime; +import static java.lang.System.err; +import static java.lang.management.ManagementFactory.getPlatformMBeanServer; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.InstanceNotFoundException; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.ReflectionException; + +/** + * Provides unified means of storage and methods of access to runtime system + * information, such as CPU usage, memory usage, an number of available threads. + * + */ +public class ResourceMonitor { + + private double memoryUsage; + private double cpuUsage; + private int threadsAvailable; + + private static ResourceMonitor instance = new ResourceMonitor(); + + private ResourceMonitor() { + threadsAvailable(); + } + + public void update() { + cpuUsage(); + memoryUsage(); + } + + /** + *

+ * This will calculate the ratio {@code totalMemory/maxMemory} using the + * standard {@code Runtime}. Note this memory usage depends on heap + * allocation for the JVM. + *

+ * + */ + public void memoryUsage() { + final double totalMemory = getRuntime().totalMemory(); + final double maxMemory = getRuntime().maxMemory(); + memoryUsage = (totalMemory / maxMemory * 100); + } + + /** + *

+ * This will calculate the CPU load for the machine running {@code PULsE}. + * Note this is rather code-intensive, so it is recommended for use only at + * certain time intervals. + *

+ * + */ + public void cpuUsage() { + + var mbs = getPlatformMBeanServer(); + ObjectName name = null; + try { + name = ObjectName.getInstance("java.lang:type=OperatingSystem"); + } catch (MalformedObjectNameException | NullPointerException e1) { + err.println("Error while calculating CPU usage:"); + e1.printStackTrace(); + } + + AttributeList list = null; + try { + list = mbs.getAttributes(name, new String[]{"ProcessCpuLoad"}); + } catch (InstanceNotFoundException | ReflectionException e) { + err.println("Error while calculating CPU usage:"); + e.printStackTrace(); + } + + if (!list.isEmpty()) { + + var att = (Attribute) list.get(0); + var value = (double) att.getValue(); + + cpuUsage = value < 0 ? 0 : (value * 100); + + } + + } + + /** + * Finds the number of threads available for calculation. This will be used + * by the {@code TaskManager} when allocating the {@code ForkJoinPool} for + * running several tasks in parallel. The number of threads is greater or + * equal to the number of cores + * + * @see pulse.tasks.TaskManager + */ + public void threadsAvailable() { + final int number = getRuntime().availableProcessors(); + threadsAvailable = number > 1 ? (number - 1) : 1; + } + + public double getCpuUsage() { + return cpuUsage; + } + + public int getThreadsAvailable() { + return threadsAvailable; + } + + public double getMemoryUsage() { + return memoryUsage; + } + + public static ResourceMonitor getInstance() { + return instance; + } + +} diff --git a/src/main/java/pulse/util/Serializer.java b/src/main/java/pulse/util/Serializer.java new file mode 100644 index 00000000..f7e8d099 --- /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 f8590805..1d4992c7 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,103 +17,108 @@ *

* */ - -public abstract class UpwardsNavigable implements Descriptive { - - private UpwardsNavigable parent; - private List listeners = new ArrayList(); - - public void removeHierarchyListeners() { - this.listeners.clear(); - } - - public void removeHierarchyListener(HierarchyListener l) { - this.listeners.remove(l); - } - - public void addHierarchyListener(HierarchyListener l) { - this.listeners.add(l); - } - - public List getHierarchyListeners() { - return listeners; - } - - /** - * Recursively informs the parent, the parent of its parent, etc. of this - * {@code UpwardsNavigable} that an action has been taken on its child's - * properties specified by {@e}. - * - * @param e the property event - */ - - public void tellParent(PropertyEvent e) { - if (parent != null) { - parent.listeners.forEach(l -> l.onChildPropertyChanged(e)); - parent.tellParent(e); - } - } - - /** - * Return the parent of this {@code UpwardsNavigable} -- if is has been - * previously explicitly set. - * - * @return the parent (which is also an {@code UpwardsNavigable}). - */ - - public UpwardsNavigable getParent() { - return parent; - } - - /** - * Finds an ancestor that looks similar to {@code aClass} by recursively calling - * {@code getParent()}. - * - * @param aClass a class which should be similar to an ancestor of this - * {@code UpwardsNavigable} - * @return the ancestor, which is a parent, or grand-parent, or - * grand-grand-parent, etc. of this {@code UpwardsNavigable}. - */ - - public UpwardsNavigable specificAncestor(Class aClass) { - 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); - return result; - } - - /** - * Explicitly sets the parent of this {@code UpwardsNavigable}. - * - * @param parent the new parent that will adopt this {@code UpwardsNavigable}. - */ - - public void setParent(UpwardsNavigable parent) { - this.parent = parent; - } - - /** - * Retrieves the Identifier of the SearchTaks this UpwardsNavigable belongs to. - * @return the identifier of the SearchTask - */ - - public Identifier identify() { - var un = specificAncestor(SearchTask.class); - return un == null ? null : ((SearchTask) un).getIdentifier(); - } - - /** - * Uses the SearchTask id (if present) to describe this UpwardsNavigable. - */ - - @Override - public String describe() { - var id = identify(); - String name = getClass().getSimpleName(); - return id == null ? name : name + "_" + id.getValue(); - } - -} \ No newline at end of file +public abstract class UpwardsNavigable implements Descriptive, Serializable { + + private UpwardsNavigable parent; + private transient List listeners; + + public void initListeners() { + listeners = new ArrayList<>(); + } + + public final void removeHierarchyListeners() { + this.listeners.clear(); + } + + public final void removeHierarchyListener(HierarchyListener l) { + this.listeners.remove(l); + } + + public final void addHierarchyListener(HierarchyListener l) { + this.listeners.add(l); + } + + public final List getHierarchyListeners() { + return listeners; + } + + /** + * Recursively informs the parent, the parent of its parent, etc. of this + * {@code UpwardsNavigable} that an action has been taken on its child's + * properties specified by { + * + * @e}. + * + * @param e the property event + */ + public void tellParent(PropertyEvent e) { + if (parent != null) { + if (parent.listeners != null) { + parent.listeners.forEach(l -> l.onChildPropertyChanged(e)); + } + parent.tellParent(e); + } + } + + /** + * Return the parent of this {@code UpwardsNavigable} -- if is has been + * previously explicitly set. + * + * @return the parent (which is also an {@code UpwardsNavigable}). + */ + public UpwardsNavigable getParent() { + return parent; + } + + /** + * Finds an ancestor that looks similar to {@code aClass} by recursively + * calling {@code getParent()}. + * + * @param aClass a class which should be similar to an ancestor of this + * {@code UpwardsNavigable} + * @return the ancestor, which is a parent, or grand-parent, or + * grand-grand-parent, etc. of this {@code UpwardsNavigable}. + */ + public UpwardsNavigable specificAncestor(Class aClass) { + if (aClass.equals(this.getClass())) { + return this; + } + UpwardsNavigable result = null; + if (parent != null) { + result = parent.getClass().equals(aClass) ? parent : parent.specificAncestor(aClass); + } + return result; + } + + /** + * Explicitly sets the parent of this {@code UpwardsNavigable}. + * + * @param parent the new parent that will adopt this + * {@code UpwardsNavigable}. + */ + public final void setParent(UpwardsNavigable parent) { + this.parent = parent; + } + + /** + * Retrieves the Identifier of the SearchTaks this UpwardsNavigable belongs + * to. + * + * @return the identifier of the SearchTask + */ + public Identifier identify() { + var un = specificAncestor(SearchTask.class); + return un == null ? null : ((SearchTask) un).getIdentifier(); + } + + /** + * Uses the SearchTask id (if present) to describe this UpwardsNavigable. + */ + @Override + public String describe() { + var id = identify(); + String name = getClass().getSimpleName(); + return id == null ? name : name + "_" + id.getValue(); + } + +} diff --git a/src/main/java/pulse/util/package-info.java b/src/main/java/pulse/util/package-info.java index f5616328..c1429066 100644 --- a/src/main/java/pulse/util/package-info.java +++ b/src/main/java/pulse/util/package-info.java @@ -2,5 +2,4 @@ * Contains abstract data and hierarchical structures that implement much of the * Java Reflection API. */ - -package pulse.util; \ No newline at end of file +package pulse.util; diff --git a/src/main/resources/About.html b/src/main/resources/About.html deleted file mode 100644 index 5c08a188..00000000 --- a/src/main/resources/About.html +++ /dev/null @@ -1,5 +0,0 @@ -
Processing Unit for Laser flash Experiments
-
(PULsE), v. 1.85
-

Date of release: 21/09/2020
Lead developer: Dr. Artem Lunev <artem.lunev@ukaea.uk>
Beta testing and validation: Rob Heymer & Olga Vilkhivskaya
Heat transfer models: Artem Lunev, Teymur Aliev, Vadim Zborovskii

-

PULsE is an advanced software toolkit for processing data from laser flash experiments, allowing effective treatment of difficult cases where conditions may not be ideal for simpler analysis. PULsE analyses the heating curves from laser flash experiments and outputs the thermal properties of the sample.

-

This software is specifically tailored for use in the Materials Research Facility at UKAEA, and reads ASCII files generated by the Linseis LFA; it was initially designed to read custom file formats from the 'Kvant' laser flash analyser. For full specification regarding the file formats, please refer to manual.

\ No newline at end of file diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 90884661..cfc99845 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -1,310 +1,354 @@ + + + + + + + + + + + + + + + maximum="5" minimum="0" primitive-type="double" value="0.1" default-search-variable="false"> - + discreet="false" keyword="BASELINE_PHASE_SHIFT" maximum="6.28318" + minimum="0.0" value="0.1" primitive-type="double" + default-search-variable="true"> - + discreet="false" keyword="BASELINE_FREQUENCY" maximum="20000" + minimum="1E-4" primitive-type="double" value="50" + default-search-variable="true"> - + primitive-type="double" value="0.14"> - + primitive-type="double" value="15.0"> - + primitive-type="double" value="0.425"> + primitive-type="int" value="10"> + primitive-type="int" value="10"> + maximum="10000" minimum="1" primitive-type="int" value="200"> + maximum="10" minimum="1E-3" primitive-type="double" value="0.5"> + maximum="2" minimum="1" value="1.7" primitive-type="double"> + maximum="4096" minimum="8" primitive-type="int" value="8"> + maximum="5" minimum="1.0" primitive-type="double" value="3.0"> + maximum="3.0" minimum="1.1" value="1.5" primitive-type="double"> - + value="1E-3" primitive-type="double"> - + value="1E-2" primitive-type="double"> + primitive-type="double" value="1e-12"> + maximum="64" minimum="2" primitive-type="int" value="8"> + maximum="1E-3" minimum="1E-10" primitive-type="double" value="1E-4"> + maximum="32" minimum="2" value="2" primitive-type="int"> - + primitive-type="double"> + maximum="100" minimum="2" primitive-type="double" value="3.5"> + maximum="1024" minimum="8" primitive-type="int" value="64"> - - - +
- - + primitive-type="double" value="1"> + visible="false" discreet="false" keyword="SIGNIFICANCE" + maximum="0.5" minimum="0.00001" primitive-type="double" value="0.001"> - + + + THERMAL_ABSORPTIVITY + LASER_ABSORPTIVITY + + + default-search-variable="false"> + + COMBINED_ABSORPTIVITY + + discreet="false" default-search-variable="false"> + + COMBINED_ABSORPTIVITY + + minimum="1" primitive-type="int" value="256" discreet="false"> + + + discreet="false" default-search-variable="true"> + + THICKNESS + + + + DIFFUSIVITY + + minimum="1.0E-6" value="0.01" primitive-type="double" discreet="true"/> + minimum="1.0E-12" value="1e-4" primitive-type="double" + discreet="false"> + value="300" primitive-type="int" discreet="false" + /> + /> + /> + dimensionfactor="1000.0" keyword="SPOT_DIAMETER" maximum="0.2" + minimum="1.0E-4" value="0.01" primitive-type="double" discreet="true" default-search-variable="false"> + dimensionfactor="1.0" keyword="TIME_LIMIT" maximum="100.0" + minimum="1.0E-6" value="1.0" primitive-type="double" discreet="true"/> + minimum="1.0" value="0.0" primitive-type="double" discreet="false"> + minimum="1.0" value="0.0" primitive-type="double" discreet="false"/> + + + value="0.8" primitive-type="double" discreet="false"/> + minimum="10.0" value="0.0" primitive-type="double" discreet="false"/> + default-search-variable="false"/> + minimum="0.0" value="298.0" primitive-type="double" discreet="false"/> + discreet="false"> - + - + + minimum="1.0E-7" value="0.005" primitive-type="double" + discreet="false"/> + default-search-variable="false" /> + + + + + HEAT_LOSS + HEAT_LOSS_SIDE + + - + + HEAT_LOSS_COMBINED + + + + + HEAT_LOSS_COMBINED + + + - - + + discreet="false" /> + discreet="false"> + maximum="200" minimum="1" primitive-type="int" value="10"> + maximum="1000" discreet="false"> + minimum="1E-4" value="0.25" discreet="false"> - + minimum="-1e10" primitive-type="double" value="Infinity"> + value="Infinity"> - + primitive-type="double" value="1.0"> diff --git a/src/main/resources/ResultFormatDescription.html b/src/main/resources/ResultFormatDescription.html deleted file mode 100644 index c739093f..00000000 --- a/src/main/resources/ResultFormatDescription.html +++ /dev/null @@ -1,17 +0,0 @@ -

Please add or remove the following characters in the textfield below to include or remove specific numeric properties from the result table.

- - - - - - - - - - - - - - - -
D
 thermal diffusivity
S
 specific heat (constant pressure)
T
initial temperature
B
heat losses
M
maximum heating
N
diathermic coefficient
R
density
C
thermal conductivity
E
integral emissivity
U
baseline intercept
V
baseline slope
I
task identifier
K
Test statistic
\ No newline at end of file diff --git a/src/main/resources/Version.txt b/src/main/resources/Version.txt new file mode 100644 index 00000000..92888258 --- /dev/null +++ b/src/main/resources/Version.txt @@ -0,0 +1 @@ +1.98 \ No newline at end of file diff --git a/src/main/resources/images/best_model.png b/src/main/resources/images/best_model.png new file mode 100644 index 00000000..ea0b463c Binary files /dev/null and b/src/main/resources/images/best_model.png differ diff --git a/src/main/resources/images/clear.png b/src/main/resources/images/clear.png index 57773041..eef77168 100644 Binary files a/src/main/resources/images/clear.png and b/src/main/resources/images/clear.png differ diff --git a/src/main/resources/images/curves.png b/src/main/resources/images/curves.png new file mode 100644 index 00000000..5b08ec35 Binary files /dev/null and b/src/main/resources/images/curves.png differ diff --git a/src/main/resources/images/go_estimate.png b/src/main/resources/images/go_estimate.png new file mode 100644 index 00000000..6d07c6b5 Binary files /dev/null and b/src/main/resources/images/go_estimate.png differ diff --git a/src/main/resources/images/graph.png b/src/main/resources/images/graph.png index 8ec2543e..05c481b9 100644 Binary files a/src/main/resources/images/graph.png and b/src/main/resources/images/graph.png differ diff --git a/src/main/resources/images/leaf.png b/src/main/resources/images/leaf.png new file mode 100644 index 00000000..58facdb9 Binary files /dev/null and b/src/main/resources/images/leaf.png differ diff --git a/src/main/resources/images/log.png b/src/main/resources/images/log.png new file mode 100644 index 00000000..5f7463e2 Binary files /dev/null and b/src/main/resources/images/log.png differ diff --git a/src/main/resources/images/pdf.png b/src/main/resources/images/pdf.png new file mode 100644 index 00000000..05328f67 Binary files /dev/null and b/src/main/resources/images/pdf.png differ diff --git a/src/main/resources/images/pulse.png b/src/main/resources/images/pulse.png new file mode 100644 index 00000000..dc70f959 Binary files /dev/null and b/src/main/resources/images/pulse.png differ diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png index 8250c476..77b7064f 100644 Binary files a/src/main/resources/images/splash.png and b/src/main/resources/images/splash.png differ diff --git a/src/main/resources/images/stored.png b/src/main/resources/images/stored.png new file mode 100644 index 00000000..5f46620c Binary files /dev/null and b/src/main/resources/images/stored.png differ diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 985a86f7..1a4346a2 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1,14 +1,14 @@ -TaskControlFrame.SoftwareTitle=PULsE / v. 1.85 +TaskControlFrame.SoftwareTitle=PULsE TaskControlFrame.AboutDialog=About PULsE NumericProperty.XMLFile=/NumericProperty.xml -NumericProperty.PlusMinus=\ ± +NumericProperty.PlusMinus=\ \u00b1 NumericProperty.BigNumberFormat=0.0\#\#\#E00 NumericProperty.NumberFormat=\#\#\#0.0\#\#\#\# Identifier.Tag=Task HeatingCurve.3=Heating curve: Number of data points -HeatingCurve.4=Baseline (mV or °) +HeatingCurve.4=Baseline (mV or \u00b0) HeatingCurve.6=Time (s) -HeatingCurve.7=Temperature (mV or °) +HeatingCurve.7=Temperature (mV or \u00b0) ExperimentalData.HalfRiseError=Failed to calculate half-rise time from data. Check data / metadata for possible problems. Switching to rough approximation now. DataLogEntry.AccessError=Problems when accessing value of the parameter DataLogEntry.BigNumberFormat=0.0\#\#E00 @@ -33,7 +33,9 @@ Vector.DimensionError1=Vectors length do not match: Vector.DimensionError2=Vectors have different dimensions: GoldenSectionSolver.Descriptor=Golden Section
(low accuracy, high speed) WolfeSolver.Descriptor=Wolfe Conditions
(high accuracy, low speed) -ApproximatedHessianSolver.Descriptor=Approximated Hessian Method
(high accuracy, low speed) +ApproximatedHessianSolver.Descriptor=BFGS (Quasi-Newton)
(high accuracy, low speed) +LMOptimiser.Descriptor=Levenberg-Marquardt
Damped least-squares solver. Does not require a linear search routine.
+SR1.Descriptor=SR1 (Quasi-Newton)
(Best for sparse problems) SteepestDescentSolver.Descriptor=Steepest Descent
(low accuracy, high speed) BufferSize.Label=BufferSize BufferSize.Descriptor=Buffer size @@ -41,16 +43,20 @@ SearchFlags.Label=SearchFlags SearchFlags.Descriptor=Search flags LinearizedProblem.Descriptor=Classical 1D Problem Statement
  • Linearized heat losses (front and rear)
  • One-dimensional heat flow
  • Dimensionless formulation
  • Cp and ρ assumed constant
DistributedProblem.Descriptor=Penetration (1D) Problem Statement
  • Based on 1D formulation, except:
  • Distributed radiation absorption (finite penetration depth) is considered
  • Detector measures an integral signal from the bulk of the sample
  • Laser and thermal radiation absorption are considered separately
-ParticipatingMedium.Descriptor=Participating Medium (1D) Problem Statement
  • Based on a coupled radiative-conductive heat transfer model with distributed absorption, emission, and scattering;
  • Describes a sample with opaque coatings on front and rear faces and a semi-transparent bulk;
  • Sample material acts a continuous medium in terms of absorption, emission, and scattering.
+ParticipatingMedium.Descriptor=Participating Medium (1D) Problem Statement
  • Based on a coupled radiative-conductive heat transfer model with distributed absorption, emission, and scattering;
  • Describes a sample with opaque coatings on front and rear faces and a semi-transparent bulk;
  • Sample material acts a continuous medium in terms of absorption, emission, and scattering.
  • Allows a nonlinear emission function -- hence requires Cp and ρ data
+LinearisedParticipatingMedium.Descriptor=Linearised Participating Medium (1D) Problem Statement
  • Based on a coupled radiative-conductive heat transfer model with distributed absorption, emission, and scattering;
  • Describes a sample with opaque coatings on front and rear faces and a semi-transparent bulk;
  • Sample material acts a continuous medium in terms of absorption, emission, and scattering.
  • Assumes heating is small to suppress nonlinear emission
DiathermicProblem.Descriptor=Diathermic Sample with Grey Walls (1D) Problem Statement
  • Based on 1D formulation, except:
  • Laser radiation is absorbed at the front surface and then re-radiated to the rear surface in the form of thermal radiation
  • Sample material considered fully transparent to any kind of radiation
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 DATReader.1=No file selected when trying to load data\! DATReader.2=\ \t ; +NetzschCSVReader.0=csv +NetzschCSVReader.1=\ \t # ; LFRReader.0=lfr LFRReader.1=No file selected when trying to load data\! LFRReader.10=\ \t; @@ -87,6 +93,9 @@ LoaderButton.5=Specific heat, (CP) LoaderButton.6=Density, (ρ) LoaderButton.IOError=I/O Error LoaderButton.ReadError=Unable to read data from file\! +LoaderButton.OFRError=Out of Range +LoaderButton.OFRErrorDescriptor=Data file does not cover the test temperature of current measurement: +LoaderButton.OFRErrorDescriptor2=. Please try expanding the temperature range in the input file! LoaderButton.SupportedExtensionsDescriptor=Supported thermal properties files LogPane.Init=Initializing... LogPane.InsertError=Could not insert log entry to log panel @@ -96,7 +105,7 @@ LogPane.Seconds=\ s LogPane.TimeTaken=
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 @@ -135,12 +144,12 @@ TaskTable.R2=R2 TaskTable.SS2=&\#8721i(Tiexp - Ti)2 TaskTable.Status=Status TaskTable.TaskID=Task ID -TaskTable.Temperature=Temperature,
T0 (K) +TaskTable.Temperature=Temperature,
T0 (°C) TaskTablePopupMenu.11=Error TaskTablePopupMenu.12=Missing heat curve\! TaskTablePopupMenu.13=Error -TaskTablePopupMenu.AskToDelete=Do you want to delete the old result and execute it a second time? -TaskTablePopupMenu.DeleteTitle=Delete old result? +TaskTablePopupMenu.AskToDelete=Do you want to start a new calculation sequence? +TaskTablePopupMenu.DeleteTitle=Run again? TaskTablePopupMenu.EmptySelection=Selection empty\! TaskTablePopupMenu.EmptySelection2=Selection empty\! TaskTablePopupMenu.ErrorTitle=Error @@ -191,7 +200,7 @@ RangeSelectionFrame.RangeSelectorTitle=Range Selector RangeSelectionFrame.TextFieldFont=Arial RangeSelectionFrame.TimeLimitMax=Time (MAX) RangeSelectionFrame.TimeLimitMin=Time (MIN) -RangeSelectionFrame.ConfirmationMessage1=This will change the time domain for the reverse solution of the heat problem. +RangeSelectionFrame.ConfirmationMessage1=This will change the time domain for the solution of the inverse heat problem. RangeSelectionFrame.ConfirmationMessage2=Current range: RangeSelectionFrame.ConfirmationMessage3=New range: SearchOptionsFrame.ListFont=Tahoma @@ -246,6 +255,7 @@ TaskControlFrame.SaveAllButton=Export All... TaskControlFrame.ShowError=Cannot show chart TaskControlFrame.ShowParkerSolution=Show Parker's solution TaskControlFrame.TaskManagerTitle=Task Manager +TaskTablePopupMenu.ViewStored=Stored Calculations... PropertyHolder.0=Unknown Property PropertyHolder.FieldMapError=Error when trying to access fieldMap() method from data() ReflexiveFinder.ConstructorAccessError=Error accessing constructor @@ -262,13 +272,23 @@ DifferenceScheme.2=Access locked to DifferenceScheme.3=Illegal argument to constructor method DifferenceScheme.4=Could not invoke constructor for Class XStep.Label=XStep +problem.notsupportedmessage=This problem statement is not currently supported. Please amend your choice. +problem.notsupportedtitle=Problem Not Supported +complexity.warning=

You have selected a high-complexity problem statement. Calculations will take longer than usual and are more likely to fail.

ExplicitScheme.2=Time interval too small: ExplicitScheme.3=Problem not supported or unknown: ExplicitScheme.4=Forward Time, Centred Space (FTCS) Scheme
  • Order of approximation O(h2 + τ)
  • Conditionally stable
  • Faster than other schemes
+ExplicitScheme.5=Forward Time, Centred Space (FTCS) Scheme (NL)
  • Order of approximation O(h2 + τ)
  • Conditionally stable
  • Faster than other schemes
ImplicitScheme.2=Time interval too small: ImplicitScheme.3=Problem not supported or unknown: ImplicitScheme.4=Fully Implicit Scheme
  • Order of approximation O(h2 + &tau)
  • Unconditionally stable
  • Intermediate computational cost
  • Finite-difference representation of boundary conditions uses a Taylor expansion with three terms
+ImplicitScheme.5=Fully Implicit Scheme (NL)
  • Order of approximation O(h2 + &tau)
  • Unconditionally stable
  • Intermediate computational cost
  • Heat equation and BC are linear while RTE has a nonlinear emission term processed with a fixed iteration algorithm
MixedScheme.2=Time interval too small: MixedScheme.3=Problem not supported or unknown: MixedScheme.4=Symmetric Semi-Implicit Scheme
  • Order of approximation O(h2 + &tau2)
  • Unconditionally stable
  • Steps are computationally more expensive but their number is fewer compared to other schemes
  • Finite-difference representation of boundary conditions uses a Taylor expansion with three terms
  • Weight is set to 0.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
  • Finite-difference representation of boundary conditions uses a Taylor expansion with three terms
  • Auto-adjusts its weight and discrete representation of the flux derivative based on accuracy.
\ No newline at end of file +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
  • Finite-difference representation of boundary conditions uses a Taylor expansion with three terms
  • Auto-adjusts its weight and discrete representation of the flux derivative based on accuracy.
+MixedScheme2.5=Increased Accuracy Semi-implicit Scheme (NL)
  • 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=

+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 diff --git a/src/main/resources/test/fft.txt b/src/main/resources/test/fft.txt new file mode 100644 index 00000000..194936e8 --- /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 diff --git a/src/test/java/repository/AnalyticalNonscatteringTransferValidation.java b/src/test/java/test/AnalyticalNonscatteringTestCase.java similarity index 85% rename from src/test/java/repository/AnalyticalNonscatteringTransferValidation.java rename to src/test/java/test/AnalyticalNonscatteringTestCase.java index ad9af903..ec790dd4 100644 --- a/src/test/java/repository/AnalyticalNonscatteringTransferValidation.java +++ b/src/test/java/test/AnalyticalNonscatteringTestCase.java @@ -1,10 +1,10 @@ -package repository; +package test; import static org.junit.jupiter.api.Assertions.assertTrue; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; import static pulse.properties.NumericPropertyKeyword.QUADRATURE_POINTS; -import static repository.TestProfileLoader.loadTestProfileDense; +import static test.ProfileLoader.loadTestProfileDense; import java.util.List; @@ -17,12 +17,12 @@ import pulse.problem.schemes.rte.exact.ChandrasekharsQuadrature; import pulse.problem.schemes.rte.exact.NonscatteringAnalyticalDerivatives; import pulse.problem.schemes.rte.exact.NonscatteringRadiativeTransfer; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; -class AnalyticalNonscatteringTransferValidation { +class AnalyticalNonscatteringTestCase { private static List testProfile; - private static NonscatteringTestCase testCase; + private static NonscatteringSetup testCase; private NonscatteringRadiativeTransfer chand; private RadiativeTransferSolver dom; @@ -31,7 +31,7 @@ class AnalyticalNonscatteringTransferValidation { @BeforeAll static void setUpBeforeClass() throws Exception { testProfile = loadTestProfileDense(); - testCase = new NonscatteringTestCase(testProfile.size(), 10.0); + testCase = new NonscatteringSetup(testProfile.size(), 10.0); } @BeforeEach @@ -47,7 +47,7 @@ void testFluxesLowThickness() { quadrature.setQuadraturePoints(derive(QUADRATURE_POINTS, 2)); var properties = (ThermoOpticalProperties)testCase.getTestProblem().getProperties(); properties.setOpticalThickness(derive(OPTICAL_THICKNESS, 0.1)); - assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-2, 3e-1)); + assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-3, 1e-1)); } @Test @@ -55,7 +55,7 @@ void testFluxesMediumThickness() { quadrature.setQuadraturePoints(derive(QUADRATURE_POINTS, 3)); var properties = (ThermoOpticalProperties)testCase.getTestProblem().getProperties(); properties.setOpticalThickness(derive(OPTICAL_THICKNESS, 1.5)); - assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-2, 1)); + assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-3, 1e-2)); } @Test @@ -63,7 +63,7 @@ void testFluxesHighThickness() { quadrature.setQuadraturePoints(derive(QUADRATURE_POINTS, 6)); var properties = (ThermoOpticalProperties)testCase.getTestProblem().getProperties(); properties.setOpticalThickness(derive(OPTICAL_THICKNESS, 100.0)); - assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-1, Double.POSITIVE_INFINITY)); + assertTrue(testCase.testFluxesAndDerivatives(testProfile, chand, dom, 1e-3, 1e-2)); } } \ No newline at end of file diff --git a/src/test/java/repository/DiscreteNonscatteringTransferValidation.java b/src/test/java/test/DiscreteNonscatteringTestCase.java similarity index 84% rename from src/test/java/repository/DiscreteNonscatteringTransferValidation.java rename to src/test/java/test/DiscreteNonscatteringTestCase.java index cf6ce7ac..550edce5 100644 --- a/src/test/java/repository/DiscreteNonscatteringTransferValidation.java +++ b/src/test/java/test/DiscreteNonscatteringTestCase.java @@ -1,10 +1,10 @@ -package repository; +package test; import static org.junit.jupiter.api.Assertions.assertTrue; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; -import static repository.TestProfileLoader.loadTestProfileDense; +import static test.ProfileLoader.loadTestProfileDense; import java.util.List; @@ -17,12 +17,12 @@ import pulse.problem.schemes.rte.exact.NewtonCotesQuadrature; import pulse.problem.schemes.rte.exact.NonscatteringDiscreteDerivatives; import pulse.problem.schemes.rte.exact.NonscatteringRadiativeTransfer; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; -class DiscreteNonscatteringTransferValidation { +class DiscreteNonscatteringTestCase { private static List testProfile; - private static NonscatteringTestCase testCase; + private static NonscatteringSetup testCase; private NonscatteringRadiativeTransfer newton; private RadiativeTransferSolver dom; @@ -30,7 +30,7 @@ class DiscreteNonscatteringTransferValidation { @BeforeAll private static void setUpBeforeClass() { testProfile = loadTestProfileDense(); - testCase = new NonscatteringTestCase(testProfile.size(), 10.0); + testCase = new NonscatteringSetup(testProfile.size(), 10.0); } @BeforeEach @@ -48,14 +48,14 @@ void setUp() throws Exception { void testFluxesLowThickness() { var properties = (ThermoOpticalProperties)testCase.getTestProblem().getProperties(); properties.setOpticalThickness(derive(OPTICAL_THICKNESS, 0.1)); - assertTrue(testCase.testFluxesAndDerivatives(testProfile, newton, dom, 1e-2, 3e-1)); + assertTrue(testCase.testFluxesAndDerivatives(testProfile, newton, dom, 1e-3, 1e-1)); } @Test void testFluxesMediumThickness() { var properties = (ThermoOpticalProperties)testCase.getTestProblem().getProperties(); properties.setOpticalThickness(derive(OPTICAL_THICKNESS, 1.5)); - assertTrue(testCase.testFluxesAndDerivatives(testProfile, newton, dom, 1e-2, 3e-1)); + assertTrue(testCase.testFluxesAndDerivatives(testProfile, newton, dom, 1e-3, 1e-1)); } } \ No newline at end of file diff --git a/src/test/java/repository/NonscatteringTestCase.java b/src/test/java/test/NonscatteringSetup.java similarity index 68% rename from src/test/java/repository/NonscatteringTestCase.java rename to src/test/java/test/NonscatteringSetup.java index e81f3e15..5de96229 100644 --- a/src/test/java/repository/NonscatteringTestCase.java +++ b/src/test/java/test/NonscatteringSetup.java @@ -1,4 +1,4 @@ -package repository; +package test; import static java.lang.Math.abs; import static pulse.properties.NumericProperties.derive; @@ -11,19 +11,22 @@ import java.util.List; +import org.apache.commons.math3.stat.regression.SimpleRegression; + import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.rte.RadiativeTransferSolver; import pulse.problem.schemes.solvers.ImplicitCoupledSolver; +import pulse.problem.schemes.solvers.ImplicitCoupledSolverNL; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Pulse2D; -import pulse.problem.statements.ThermoOpticalProperties; +import pulse.problem.statements.model.ThermoOpticalProperties; -public class NonscatteringTestCase { +public class NonscatteringSetup { private ParticipatingMedium testProblem; private DifferenceScheme testScheme; - public NonscatteringTestCase(final int testProfileSize, final double maxHeating) { + public NonscatteringSetup(final int testProfileSize, final double maxHeating) { testProblem = new ParticipatingMedium(); var properties = (ThermoOpticalProperties)testProblem.getProperties(); properties.setSpecificHeat(derive(SPECIFIC_HEAT, 300.0)); @@ -36,7 +39,7 @@ public NonscatteringTestCase(final int testProfileSize, final double maxHeating) properties.setTestTemperature(derive(TEST_TEMPERATURE, 800.0)); properties.setScatteringAlbedo(derive(SCATTERING_ALBEDO, 0.0)); - testScheme = new ImplicitCoupledSolver(); + testScheme = new ImplicitCoupledSolverNL(); var grid = testScheme.getGrid(); grid.setGridDensity(derive(GRID_DENSITY, testProfileSize - 1)); @@ -76,12 +79,35 @@ public boolean testFluxesAndDerivatives(List testProfile, RadiativeTrans var fluxes1 = solver1.getFluxes(); var fluxes2 = solver2.getFluxes(); + final int n = testProfile.size(); + double[][] test1 = new double[n][2]; + double[][] test2 = new double[n][2]; + + System.out.printf("%12s ; %12s ; %12s ; %12s %n", "Flux (Solver1)", "Flux (Solver2)", "Derivative (Solver1)", "Derivative (Solver2)"); for (int i = 1, size = testProfile.size(); i < size - 1 && pass; i++) { - pass &= approximatelyEquals(fluxes1.getFlux(i), fluxes2.getFlux(i), margin1); - pass &= approximatelyEquals(fluxes1.fluxDerivative(i), fluxes2.fluxDerivative(i), margin2); - System.out.printf("%1.4e ; % 1.4e %1.4e ; % 1.4e %n", fluxes1.getFlux(i), fluxes2.getFlux(i), - fluxes1.fluxDerivative(i), fluxes2.fluxDerivative(i)); + test1[i][0] = fluxes1.getFlux(i); + test1[i][1] = fluxes2.getFlux(i); + + test2[i][0] = fluxes1.fluxDerivative(i); + test2[i][1] = fluxes2.fluxDerivative(i); + + System.out.printf("%1.4e ; %1.4e %1.4e ; %1.4e %n", test1[i][0], test1[i][1], test2[i][0], test2[i][1]); } + + + var regression1 = new SimpleRegression(); + regression1.addData(test1); + double rsq1 = regression1.getRSquare(); + System.out.println("R-Squared of linear regression for fluxes: " + rsq1); + + var regression2 = new SimpleRegression(); + regression2.addData(test2); + double rsq2 = regression2.getRSquare(); + System.out.println("R-Squared of linear regression for derivatives: " + rsq2); + + pass &= approximatelyEquals(rsq1, 1.0, margin1); + pass &= approximatelyEquals(rsq2, 1.0, margin2); + System.out.println(pass ? "SUCCESS" : "FAILED"); return pass; } diff --git a/src/test/java/repository/TestProfileLoader.java b/src/test/java/test/ProfileLoader.java similarity index 93% rename from src/test/java/repository/TestProfileLoader.java rename to src/test/java/test/ProfileLoader.java index f1d80aa4..63488cbd 100644 --- a/src/test/java/repository/TestProfileLoader.java +++ b/src/test/java/test/ProfileLoader.java @@ -1,4 +1,4 @@ -package repository; +package test; import java.io.File; import java.io.FileNotFoundException; @@ -9,9 +9,9 @@ import pulse.problem.schemes.rte.exact.NonscatteringRadiativeTransfer; -public class TestProfileLoader { +public class ProfileLoader { - private TestProfileLoader() { + private ProfileLoader() { //intentionally blank } diff --git a/src/test/java/repository/QuadratureValidation.java b/src/test/java/test/QuadratureTest.java similarity index 88% rename from src/test/java/repository/QuadratureValidation.java rename to src/test/java/test/QuadratureTest.java index 5a933e77..baa71ea3 100644 --- a/src/test/java/repository/QuadratureValidation.java +++ b/src/test/java/test/QuadratureTest.java @@ -1,12 +1,12 @@ -package repository; +package test; import static java.lang.Math.pow; import static org.junit.jupiter.api.Assertions.assertTrue; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.INTEGRATION_SEGMENTS; import static pulse.properties.NumericPropertyKeyword.QUADRATURE_POINTS; -import static repository.NonscatteringTestCase.approximatelyEquals; -import static repository.TestProfileLoader.loadTestProfileDense; +import static test.NonscatteringSetup.approximatelyEquals; +import static test.ProfileLoader.loadTestProfileDense; import java.util.ArrayList; import java.util.List; @@ -25,7 +25,7 @@ import pulse.problem.schemes.rte.exact.NewtonCotesQuadrature; import pulse.problem.statements.ParticipatingMedium; -class QuadratureValidation { +class QuadratureTest { private static ParticipatingMedium problem; private static List testProfile; @@ -41,7 +41,7 @@ class QuadratureValidation { @BeforeAll static void setUpBeforeClass() throws Exception { testProfile = loadTestProfileDense(); - problem = new NonscatteringTestCase(testProfile.size(), 0.1).getTestProblem(); + problem = new NonscatteringSetup(testProfile.size(), 0.1).getTestProblem(); var tempArray = testProfile.stream().mapToDouble(d -> d).toArray(); @@ -99,19 +99,21 @@ private boolean test(final double margin) { System.out.printf("%nSegments: %6d. Result = %3.4f", quad2.getIntegrator().getIntegrationSegments().getValue(), value); } + System.out.println(); + return approximatelyEquals(list.get(list.size() - 1), list2.get(list2.size() - 1), margin); } @Test void testFirstOrderConvergence() { - System.out.printf("%n%nFirst-order test"); + System.out.printf("%n%nQuadratures: First-order test"); prepareFirstOrder(quad1, quad2); - assertTrue( test(1E-3) ); + assertTrue( test(1E-2) ); } @Test void testSecondOrderConvergence() { - System.out.printf("%n%nSecond-order test"); + System.out.printf("%n%nQuadratures: Second-order test"); prepareSecondOrder(quad1, quad2); assertTrue( test(1E-3) ); } @@ -121,7 +123,7 @@ void testChandrasekhars() { prepareFirstOrder(quad1); quad1.setQuadraturePoints(derive(QUADRATURE_POINTS, 8)); final double result = quad1.integrate(); - assertTrue(approximatelyEquals(result, 1961.617, 1E-6)); + assertTrue(approximatelyEquals(result, 1961.617, 1E-4)); } @Test @@ -129,7 +131,7 @@ void testNewtonCotes() { prepareFirstOrder(quad2); quad2.getIntegrator().setIntegrationSegments(derive(INTEGRATION_SEGMENTS, 4096)); final double result = quad2.integrate(); - assertTrue(approximatelyEquals(result, 1962.45, 1E-6)); + assertTrue(approximatelyEquals(result, 1965.20, 1E-4)); } } \ No newline at end of file