jeplus.data.ParameterItem.java Source code

Java tutorial

Introduction

Here is the source code for jeplus.data.ParameterItem.java

Source

/***************************************************************************
 *   jEPlus - EnergyPlus shell for parametric studies                      *
 *   Copyright (C) 2010  Yi Zhang <yi@jeplus.org>                          *
 *                                                                         *
 *   This program is free software: you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation, either version 3 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
 *                                                                         *
 ***************************************************************************/
package jeplus.data;

import com.fasterxml.jackson.annotation.JsonIgnore;
import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.tree.DefaultMutableTreeNode;
import jeplus.JEPlusProject;
import jeplus.util.CsvUtil;
import jeplus.util.RelativeDirUtil;
import org.apache.commons.math3.distribution.ExponentialDistribution;
import org.apache.commons.math3.distribution.LogNormalDistribution;
import org.slf4j.LoggerFactory;

/**
 * Encapsulate a parameter for parametric analysis. This includes a variable name,
 * description, short name, lookup string, and alternative values string.
 * @author Yi Zhang
 * @version 0.5b
 * @since 0.1
 */
public class ParameterItem implements Serializable, Cloneable {

    /** Logger */
    final static org.slf4j.Logger logger = LoggerFactory.getLogger(ParameterItem.class);

    // Serialization version code; to maintain backwards compatibility, do not change.
    static final long serialVersionUID = 7439317083898309376L;

    private final static int COMBINE = 1;
    private final static int EXCLUDE = 2;

    // Parameter value types
    public final static int INTEGER = 0;
    public final static int DOUBLE = 1;
    public final static int DISCRETE = 2;
    public final static int VALUE_SETS = 3;
    // Parameter types
    public final static int PARAMETRICS = 0;
    public final static int OPTIMISATION = 1;
    public final static int UNCERTAINTY = 2;
    // Simulation platform types
    public final static int ANY = 0;
    public final static int WINDOWS = 1;
    public final static int LINUX = 2;

    /** Parameter id as a unique identifier */
    @JsonIgnore
    public String ID = null;
    /** Parameter name - for information only */
    @JsonIgnore
    public String Name = null;
    /** Parameter type: PARAMETRICS, OPTIMISATION or UNCERTAINTY */
    @JsonIgnore
    public int ParamType = PARAMETRICS;
    /** Parameter value type: INTEGER, DOUBLE, or DISCRETE */
    @JsonIgnore
    public int Type = 0;
    /** Description of the parameter - for information only */
    @JsonIgnore
    public String Description = null;
    @JsonIgnore
    public String SearchString = null;
    protected String ValuesString = null;

    transient private boolean ValueStringChanged = true;

    transient protected String[] AltValues = null;
    transient protected int NAltValues = 0;
    protected int SelectedAltValue = 0; // 0-use all; 1-the 1st value; 2-the 2nd value ...

    /** Reference to project in order to get access to its base dir - for loading parameter from file */
    protected JEPlusProject Project;

    /**
     * Construct an empty entry
     */
    public ParameterItem() {
        Name = "Parameter1";
        Type = INTEGER;
        Description = "new parameter item";
        ID = "P1";
        SearchString = "@@parameter1@@";
        ValuesString = "[ : : ] & { , , , } ^ { , , }";
        Project = null;
    }

    /**
     * Construct an empty entry
     * @param project Reference to the parent project
     */
    public ParameterItem(JEPlusProject project) {
        int pnumber = project.getNumberOfParams() + 1;
        Name = "Parameter " + pnumber;
        Type = INTEGER;
        Description = "new parameter item";
        ID = "P" + pnumber;
        SearchString = "@@tag" + pnumber + "@@";
        ValuesString = "[ : : ] & { , , , } ^ { , , }";
        Project = project;
    }

    /**
     * Construct a new parameter item with supplied information
     * @param name Name of the item
     * @param type Type of the parameter
     * @param desc Description of the item
     * @param id ID of the item to be used for naming the work directory
     * @param sstr SearchString encoded in the E+ IDF template file
     * @param vstr ValuesString encodes the alternative values
     */
    public ParameterItem(String name, int type, String desc, String id, String sstr, String vstr) {
        Name = name;
        Type = type;
        Description = desc;
        ID = id;
        SearchString = sstr;
        ValuesString = vstr;
        ValueStringChanged = true;
    }

    /**
     * Construct a new parameter item with supplied information in a string array.
     * This constructor is used for importing a parameter from a parameter table,
     * see <code>JEPlusProject.importParameterTableFile()</code> for details.
     * @param project Reference to the parent project
     * @param vals values in a string array
     */
    public ParameterItem(JEPlusProject project, String[] vals) {
        // Input sequence: ID, Name, Parameter Type, Description, Search String, Value Type, Value String, Selected Value Index
        try {
            ID = CsvUtil.stripEnclosingQuotes(vals[0]);
            Name = CsvUtil.stripEnclosingQuotes(vals[1]);
            ParamType = Integer.parseInt(vals[2]);
            Description = CsvUtil.stripEnclosingQuotes(vals[3]);
            SearchString = CsvUtil.stripEnclosingQuotes(vals[4]);
            Type = Integer.parseInt(vals[5]);
            ValuesString = CsvUtil.stripEnclosingQuotes(vals[6]);
            SelectedAltValue = Integer.parseInt(vals[7]);
            ValueStringChanged = true;
            Project = project;
        } catch (NumberFormatException ex) {
            logger.error("", ex);
        }
    }

    /**
     * Construct a new instance by copying from an existing one
     * @param item The item from which to copy
     */
    public ParameterItem(ParameterItem item) {
        Name = item.Name;
        Type = item.Type;
        Description = item.Description;
        ID = item.ID;
        SearchString = item.SearchString;
        ValuesString = item.ValuesString;
        ValueStringChanged = true;
        SelectedAltValue = item.SelectedAltValue;
        Project = item.Project;
    }

    @JsonIgnore
    public boolean isAltValueFixed() {
        return this.SelectedAltValue > 0;
    }

    public int getSelectedAltValue() {
        return SelectedAltValue;
    }

    public void setSelectedAltValue(int SelectedAltValue) {
        this.SelectedAltValue = SelectedAltValue;
    }

    public String getDescription() {
        return Description;
    }

    public void setDescription(String Description) {
        this.Description = Description;
    }

    public String getID() {
        return ID;
    }

    public void setID(String ID) {
        this.ID = ID;
    }

    public String getName() {
        return Name;
    }

    public void setName(String Name) {
        this.Name = Name;
    }

    public String getSearchString() {
        return SearchString;
    }

    public void setSearchString(String SearchString) {
        this.SearchString = SearchString;
    }

    public int getParamType() {
        return ParamType;
    }

    public void setParamType(int ParamType) {
        this.ParamType = ParamType;
    }

    public int getType() {
        return Type;
    }

    public void setType(int Type) {
        this.Type = Type;
    }

    public String getValuesString() {
        return ValuesString;
    }

    public void setValuesString(String ValuesString) {
        this.ValuesString = ValuesString;
        ValueStringChanged = true;
    }

    /**
     * clone the object
     * @return the clone
     * @throws java.lang.CloneNotSupportedException
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        return new ParameterItem(this);
    }

    /**
     * Return the number of alternative values identified by the 
     * <code>getAlternativeValues ()</code> function.
     * @return Number of alternative values of this parameter item
     */
    @JsonIgnore
    public int getNAltValues() {
        if (NAltValues <= 0)
            getAlternativeValues();
        return NAltValues;
    }

    /**
     * Retrieve alternative values list. Par
     * @return
     */
    @JsonIgnore
    public String[] getAlternativeValues() {
        if (ValueStringChanged || AltValues == null) {
            parseAlternativeValues();
            ValueStringChanged = false;
        }
        return AltValues;
    }

    @JsonIgnore
    public JEPlusProject getProject() {
        return Project;
    }

    @JsonIgnore
    public void setProject(JEPlusProject Project) {
        this.Project = Project;
    }

    /**
     * Get the minimum of the alternative values. This function is used for 
     * optimisation or sampling purposes. In the current implementation, it
     * takes the left most alt value. This is subject to future changes.
     * @return 
     */
    @JsonIgnore
    public double getMinAltValue() {
        double min = Double.NaN;
        String[] vals = getAlternativeValues();
        if (this.Type == DOUBLE) {
            min = Double.parseDouble(vals[0]);
        }
        return min;
    }

    /**
     * Get the maximum of the alternative values. This function is used for 
     * optimisation or sampling purposes. In the current implementation, it
     * takes the right most alt value. This is subject to future changes.
     * @return 
     */
    @JsonIgnore
    public double getMaxAltValue() {
        double max = Double.NaN;
        String[] vals = getAlternativeValues();
        if (this.Type == DOUBLE) {
            max = Double.parseDouble(vals[vals.length - 1]);
        }
        return max;
    }

    /**
     * This function extracts alternative values from a string. An example
     * of the string is shown here: [ : : ]&[ : : ]&{ , , , }^{ , , }
     * The square brackets define a range, for which lower boundary, interval,
     * and upper boundary are given, e.g. [LB:INT:UB]. The curvy brackets define
     * a list of options, e.g. {v1, v2, v3 ...}. Different ranges and lists can
     * be combined by using a "&" sign. Lists can be excluded, too, by using a
     * "^" prefixes. "^" cannot be used with ranges.
     */
    protected void parseAlternativeValues() {
        //ValuesString = ValuesString.trim();
        if (ValuesString.length() > 0) {
            try {
                //String [] sections = ValuesString.split("\\s*[\\&|\\^]\\s*");
                String[] sections = ValuesString.split("\\s*[\\&\\^]\\s*");
                int[] signs = new int[sections.length];
                // handle first character
                char[] chars = ValuesString.toCharArray();
                char c0 = chars[0];
                if (c0 == '^') {
                    signs[0] = EXCLUDE;
                } else if (c0 == '&' | c0 == '[' | c0 == '{' | c0 == '@') {
                    signs[0] = COMBINE;
                } else {
                    // something's wrong
                    throw new Exception("Illegal character at the beginning of the line.");
                }
                // scan for "+" and "-"
                int idx = 1;
                for (int i = 1; i < chars.length; i++) {
                    if (chars[i] == '&') {
                        signs[idx] = COMBINE;
                        idx++;
                    } else if (chars[i] == '^') {
                        signs[idx] = EXCLUDE;
                        idx++;
                    }
                }
                // parse those sections to be combined
                String[] vals = new String[0];
                for (int i = 0; i < signs.length; i++) {
                    if (signs[i] == COMBINE) {
                        String tmp = sections[i].substring(1, sections[i].length() - 1).trim();
                        if (sections[i].startsWith("[")) {
                            vals = mergeLists(vals, parseRange(tmp), COMBINE);
                        } else if (sections[i].startsWith("{")) {
                            vals = mergeLists(vals, parseList(tmp), COMBINE);
                        } else if (sections[i].toLowerCase().startsWith("@file")) {
                            int start = sections[i].indexOf("(") + 1;
                            int end = sections[i].indexOf(")");
                            int end_selected = Math.max(0, sections[i].indexOf(","))
                                    + Math.max(0, sections[i].indexOf(";"));
                            if (end_selected == 0) {
                                vals = mergeLists(vals,
                                        loadValuesFromFile(sections[i].substring(start, end).trim()), COMBINE,
                                        true);
                            } else {
                                int pos = Integer.parseInt(sections[i].substring(end_selected + 1, end).trim());
                                String[] selected = new String[1];
                                String[] FileVals = loadValuesFromFile(
                                        sections[i].substring(start, end_selected).trim());
                                selected[0] = FileVals[Math.min(Math.max(0, pos - 1), FileVals.length)];
                                vals = mergeLists(vals, selected, COMBINE, true);
                            }
                        } else if (sections[i].toLowerCase().startsWith("@sample")) {
                            int start = sections[i].indexOf("(") + 1;
                            int end = sections[i].indexOf(")");
                            vals = mergeLists(vals, sampleDistribution(sections[i].substring(start, end).trim()),
                                    COMBINE, true);
                        } else if (sections[i].toLowerCase().startsWith("@calc")) {
                            int start = sections[i].indexOf("(") + 1;
                            int end = sections[i].lastIndexOf(")");
                            vals = mergeLists(vals, createFormula(sections[i].substring(start, end).trim()),
                                    COMBINE, true);
                        } else if (sections[i].toLowerCase().startsWith("@jython")) {
                            // Exclusive type. If present, other vals are ignored
                            int start = sections[i].indexOf("(") + 1;
                            int end = sections[i].lastIndexOf(")");
                            vals = createScript("jython", sections[i].substring(start, end).trim());
                        } else if (sections[i].toLowerCase().startsWith("@python2")) {
                            // Exclusive type. If present, other vals are ignored
                            int start = sections[i].indexOf("(") + 1;
                            int end = sections[i].lastIndexOf(")");
                            vals = createScript("python2", sections[i].substring(start, end).trim());
                        } else if (sections[i].toLowerCase().startsWith("@python3")) {
                            // Exclusive type. If present, other vals are ignored
                            int start = sections[i].indexOf("(") + 1;
                            int end = sections[i].lastIndexOf(")");
                            vals = createScript("python3", sections[i].substring(start, end).trim());
                        }
                    }
                }

                // parse those sections to be excluded
                for (int i = 0; i < signs.length; i++) {
                    if (signs[i] == EXCLUDE) {
                        String tmp = sections[i].substring(1, sections[i].length() - 1).trim();
                        if (sections[i].startsWith("[")) {
                            vals = mergeLists(vals, parseRange(tmp), EXCLUDE);
                        } else if (sections[i].startsWith("{")) {
                            vals = mergeLists(vals, parseList(tmp), EXCLUDE);
                        } else if (sections[i].toLowerCase().startsWith("@file")) {
                            int start = sections[i].indexOf("(") + 1;
                            int end = sections[i].indexOf(")");
                            int end_selected = Math.max(0, sections[i].indexOf(","))
                                    + Math.max(0, sections[i].indexOf(";"));
                            if (end_selected == 0) {
                                vals = mergeLists(vals,
                                        loadValuesFromFile(sections[i].substring(start, end).trim()), EXCLUDE);
                            } else {
                                int pos = Integer.parseInt(sections[i].substring(end_selected + 1, end).trim());
                                String[] selected = new String[1];
                                String[] FileVals = loadValuesFromFile(
                                        sections[i].substring(start, end_selected).trim());
                                selected[0] = FileVals[Math.min(Math.max(0, pos - 1), FileVals.length)];
                                vals = mergeLists(vals, selected, EXCLUDE);
                            }
                        } else if (sections[i].toLowerCase().startsWith("@sample")) {
                            int start = sections[i].indexOf("(") + 1;
                            int end = sections[i].indexOf(")");
                            vals = mergeLists(vals, sampleDistribution(sections[i].substring(start, end).trim()),
                                    EXCLUDE);
                        } else if (sections[i].toLowerCase().startsWith("@calc")) {
                            // @calc is not supported in the exclusion mode
                        }
                    }
                }

                // return
                this.NAltValues = vals.length;
                this.AltValues = vals;
                return;

            } catch (MalformedValuesException mve) {
                // does nothing
            } catch (Exception ex) {
                // logger.error("Something's wrong with parameter " + this.getID(), ex);
            }
        } else {
            //System.err.println("Empty string.");
        }
        this.NAltValues = 0;
        this.AltValues = null;
    }

    /**
     * To parse the given string as a range delimited with colon ':', e.g. -20:1:20
     * @param rstr The string to be parse
     * @return A parsed list of strings
     * @throws java.lang.Exception
     */
    private String[] parseRange(String rstr) throws Exception {
        String[] s = rstr.split("\\s*:\\s*");
        if (s.length != 3)
            throw new MalformedValuesException("Range format: [LB:Int:UB]");
        if (Type == INTEGER) {
            int lb = Integer.parseInt(s[0]);
            int intv = Integer.parseInt(s[1]);
            if (intv <= 0)
                throw new MalformedValuesException(
                        "Range format: [LB:Int:UB]; incremental value must be greater than 0.");
            int ub = Integer.parseInt(s[2]);
            ArrayList<String> list = new ArrayList();
            while (lb <= ub) {
                list.add(Integer.toString(lb));
                lb += intv;
            }
            return list.toArray(new String[0]);
        } else if (Type == DOUBLE) {
            double lb = Double.parseDouble(s[0]);
            double intv = Double.parseDouble(s[1]);
            if (intv <= 0)
                throw new MalformedValuesException(
                        "Range format: [LB:Int:UB]; incremental value must be greater than 0.");
            double ub = Double.parseDouble(s[2]);
            ArrayList<String> list = new ArrayList();
            while (lb <= ub) {
                // list.add(Double.toString(lb));
                String val = new Formatter().format("%g", lb).toString();
                if (val.contains(".")) { // only when a decimal point is present
                    // split at 'e'
                    String[] parts = val.split("e");
                    val = parts[0];
                    while (val.length() > 3 && val.endsWith("0"))
                        val = val.substring(0, val.length() - 1);
                    if (parts.length > 1 && parts[1] != null)
                        val = val.concat("e").concat(parts[1]);
                }
                list.add(val);
                lb += intv;
            }
            return list.toArray(new String[0]);
        }
        throw new MalformedValuesException("parseRange failed: unsupported parameter type.");
    }

    /**
     * Parse the given string as a list delimited by comma ','
     * @param lstr The string to be parsed
     * @return A parsed list of strings
     */
    private String[] parseList(String lstr) {
        ArrayList<String> list = new ArrayList<>();
        Matcher m = Pattern.compile("([^\"][\\S&&[^,;]]*|\".+?\")(\\s|,|;)*").matcher(lstr);
        while (m.find()) {
            if (m.group(1) != null) {
                String part = m.group(1).replace("\"", "");
                list.add(part);
            }
        }
        return list.toArray(new String[0]);
        //return lstr.split("\\s*[,;]\\s*");
    }

    /**
     * Combining the two lists or excluding the second list from the first.
     * @param vals1 The first list
     * @param vals2 The second list
     * @param op Operation can be either COMBINE or EXCLUDE
     * @return a new list as the combination of the two
     */
    private String[] mergeLists(String[] vals1, String[] vals2, int op) {
        return mergeLists(vals1, vals2, op, false);
    }

    /**
     * Combining the two lists or excluding the second list from the first.
     * @param vals1 The first list
     * @param vals2 The second list
     * @param op Operation can be either COMBINE or EXCLUDE
     * @return a new list as the combination of the two
     */
    private String[] mergeLists(String[] vals1, String[] vals2, int op, boolean allow_duplicates) {
        ArrayList<String> set = new ArrayList(Arrays.asList(vals1));
        if (op == COMBINE) {
            for (int i = 0; i < vals2.length; i++) {
                if (allow_duplicates || !set.contains(vals2[i]))
                    set.add(vals2[i]);
            }
        } else {
            for (int i = 0; i < vals2.length; i++) {
                set.remove(vals2[i]);
            }
        }
        return set.toArray(new String[0]);
    }

    /**
     * Load parameter item's alternative values from a text file
     * @param file Name of file containing alt. values
     * @return String array contains all parsed alternative values
     */
    protected String[] loadValuesFromFile(String file) {
        String path = (Project == null) ? file : RelativeDirUtil.checkAbsolutePath(file, Project.getBaseDir());
        ArrayList<String> list;
        try (BufferedReader fr = new BufferedReader(new FileReader(path))) {
            list = new ArrayList<>();
            String line = fr.readLine();
            while (line != null) {
                // Filter comments
                if (line.contains("!"))
                    line = line.substring(0, line.indexOf("!")).trim();
                // split values in the same line
                if (line.length() > 0) {
                    Matcher m = Pattern.compile("([^\"][\\S&&[^,;]]*|\".+?\")(\\s|,|;)*").matcher(line);
                    while (m.find()) {
                        if (m.group(1) != null) {
                            String part = m.group(1).replace("\"", "");
                            list.add(part);
                        }
                    }
                    //String []  vals = line.trim().split("(\\s|,|;)+");
                    //for (int i=0; i<vals.length; i++) list.add (vals[i]);
                }
                line = fr.readLine();
            }
            return list.toArray(new String[0]);
        } catch (Exception ex) {
            logger.error("", ex);
        }
        return null;
    }

    /**
     * Sampling function for probabilistic distributions
     * @param funcstr Function string
     * @return List of sampled values in a string array
     */
    private String[] sampleDistribution(String funcstr) {
        ArrayList<String> list = new ArrayList<>();
        String[] params = funcstr.split("\\s*[,;]\\s*");
        // For integer/double types, returns randomized N samples conforming
        // a specified distribution, currently 'gaussian'/'normal'/'n', 
        // 'uniform'/'u', 'triangular'/'tr', or 'discrete'/'d'
        // for examples: @sample(gaussian, 0, 1.5, 20), with mean, sd and N
        //           or  @sample(uniform, -10, 10, 20), with lb, ub and N
        //           of  @sample(triangular, -1.0, 0.3, 1.0, 20), with lb, mode, ub and N
        //           of  @sample(discrete, option_A, 0.3, option_B, 0.5, option_C, 0.2, 20), with op1, weight1, op2, weight2..., and N
        String distribution = params[0].toLowerCase();
        switch (distribution) {
        case "uniform":
        case "u": {
            // requires lb, ub, n
            double lb = Double.parseDouble(params[1]);
            double ub = Double.parseDouble(params[2]);
            int n = Integer.parseInt(params[3]);
            if (this.Type == DISCRETE) {
                // list.add(params[1]);
                // list.add(params[2]);
                list = null;
            } else {
                for (int i = 0; i < n; i++) {
                    if (this.Type == DOUBLE) {
                        list.add(Double.toString(RandomSource.getRandomGenerator().nextDouble() * (ub - lb) + lb));
                    } else if (this.Type == INTEGER) {
                        list.add(Long.toString(
                                Math.round(RandomSource.getRandomGenerator().nextDouble() * (ub - lb) + lb)));
                    }
                }
            }
            break;
        }
        case "gaussian":
        case "normal":
        case "n": {
            // requires mean, sd, n
            double mean = Double.parseDouble(params[1]);
            double sd = Double.parseDouble(params[2]);
            int n = Integer.parseInt(params[3]);
            if (this.Type == DISCRETE) {
                // list.add(params[1]);
                list = null;
            } else {
                for (int i = 0; i < n; i++) {
                    if (this.Type == DOUBLE) {
                        list.add(Double.toString(RandomSource.getRandomGenerator().nextGaussian() * sd + mean));
                    } else if (this.Type == INTEGER) {
                        list.add(Long.toString(
                                Math.round(RandomSource.getRandomGenerator().nextGaussian() * sd + mean)));
                    }
                }
            }
            break;
        }
        case "lognormal":
        case "ln": {
            // requires mean, sd, n
            double mean = Double.parseDouble(params[1]);
            double sd = Double.parseDouble(params[2]);
            int n = Integer.parseInt(params[3]);
            if (this.Type == DISCRETE) {
                // list.add(params[1]);
                list = null;
            } else {
                LogNormalDistribution lndist = new LogNormalDistribution(mean, sd);
                // Which RNG is used??
                double[] sample = lndist.sample(n);
                for (int i = 0; i < n; i++) {
                    if (this.Type == DOUBLE) {
                        list.add(Double.toString(sample[i]));
                    } else if (this.Type == INTEGER) {
                        list.add(Long.toString(Math.round(sample[i])));
                    }
                }
            }
            break;
        }
        case "exponential":
        case "e": {
            // requires mean, n
            double mean = Double.parseDouble(params[1]);
            int n = Integer.parseInt(params[2]);
            if (this.Type == DISCRETE) {
                // list.add(params[1]);
                list = null;
            } else {
                ExponentialDistribution lndist = new ExponentialDistribution(mean);
                // Which RNG is used??
                double[] sample = lndist.sample(n);
                for (int i = 0; i < n; i++) {
                    if (this.Type == DOUBLE) {
                        list.add(Double.toString(sample[i]));
                    } else if (this.Type == INTEGER) {
                        list.add(Long.toString(Math.round(sample[i])));
                    }
                }
            }
            break;
        }
        case "triangular":
        case "tr": {
            // requires a(lb), c(tip), b(ub), n
            double a = Double.parseDouble(params[1]);
            double c = Double.parseDouble(params[2]);
            double b = Double.parseDouble(params[3]);
            // sort a, b, c, so a < c < b
            // ...
            int n = Integer.parseInt(params[4]);
            if (this.Type == DISCRETE || !(a < c && c < b)) {
                // list.add(params[1]);
                // list.add(params[2]);
                // list.add(params[3]);
                list = null;
            } else {
                for (int i = 0; i < n; i++) {
                    double x = RandomSource.getRandomGenerator().nextDouble();
                    double y = 0;
                    if (x <= (c - a) / (b - a)) {
                        y = Math.sqrt((c - a) * (b - a) * x) + a;
                    } else {
                        y = b - Math.sqrt((1 - x) * (b - a) * (b - c));
                    }
                    if (this.Type == DOUBLE) {
                        list.add(Double.toString(y));
                    } else if (this.Type == INTEGER) {
                        list.add(Long.toString(Math.round(y)));
                    }
                }
            }
            break;
        }
        case "discrete":
        case "d": {
            // requires op1, prob1, op2, prob2, ..., n
            int nOptions = params.length / 2 - 1;
            String[] options = new String[nOptions];
            double[] probabilities = new double[nOptions];
            double[] accProb = new double[nOptions];
            for (int i = 0; i < nOptions; i++) {
                options[i] = params[2 * i + 1];
                try {
                    probabilities[i] = Double.parseDouble(params[2 * i + 2]);
                } catch (NumberFormatException nfe) {
                    probabilities[i] = 0.1;
                }
                if (i == 0)
                    accProb[i] = probabilities[i];
                else
                    accProb[i] = accProb[i - 1] + probabilities[i];
            }
            int n = Integer.parseInt(params[params.length - 1]);
            for (int i = 0; i < n; i++) {
                double x = RandomSource.getRandomGenerator().nextDouble() * accProb[nOptions - 1];
                int sel = 0;
                for (int j = 0; j < nOptions; j++) {
                    if (x < accProb[j]) {
                        sel = j;
                        break;
                    }
                }
                list.add(options[sel]);
            }
            break;
        }
        case "custom":
            // to be implemented
            list = null;
            break;
        }
        return (list == null) ? null : list.toArray(new String[0]);
    }

    private String[] createFormula(String funcstr) {
        // scan for parameter ids in the given string, and replace them with the corresponding search tag
        // 1. locate this parameter in the tree
        Enumeration nodes = Project.getParamTree().depthFirstEnumeration();
        DefaultMutableTreeNode thisnode = Project.getParamTree();
        while (nodes.hasMoreElements()) {
            thisnode = (DefaultMutableTreeNode) nodes.nextElement();
            if (thisnode.getUserObject() == this) {
                break;
            }
        }
        Object[] items = thisnode.getUserObjectPath();
        String newstr = "";
        String bufstr = funcstr;
        for (int i = 0; i < items.length - 1; i++) {
            ParameterItem item = (ParameterItem) items[i];
            newstr = bufstr.replace(item.getID(), item.getSearchString());
            bufstr = newstr;
        }
        return new String[] { "?=" + newstr };
    }

    private String[] createScript(String type, String funcstr) {
        // scan for parameter ids in the given string, and replace them with the corresponding search tag
        // 1. locate this parameter in the tree
        Enumeration nodes = Project.getParamTree().depthFirstEnumeration();
        DefaultMutableTreeNode thisnode = Project.getParamTree();
        while (nodes.hasMoreElements()) {
            thisnode = (DefaultMutableTreeNode) nodes.nextElement();
            if (thisnode.getUserObject() == this) {
                break;
            }
        }
        Object[] items = thisnode.getUserObjectPath();
        // 2. split function string into func name and args
        String[] args = funcstr.split("\\s*,\\s*");
        // 3. get a map from parameter id to search tags
        HashMap<String, String> tagmap = new HashMap<>();
        for (int i = 0; i < items.length - 1; i++) {
            ParameterItem item = (ParameterItem) items[i];
            tagmap.put(item.getID(), item.getSearchString());
        }
        // 4. create script def string
        StringBuilder buf = new StringBuilder("call(").append(type);
        buf.append(", ").append(args[0]);
        for (int i = 1; i < args.length; i++) {
            buf.append(", ");
            if (tagmap.containsKey(args[i])) {
                buf.append(tagmap.get(args[i]));
            } else {
                buf.append(args[i]);
            }
        }
        buf.append(")");
        return new String[] { buf.toString() };
    }

    /**
     * ToString function
     * @return String presentation of the contents of this item
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder("[");
        buf.append(ID).append("]").append(Name);
        buf.append("::").append(SearchString).append("=");
        buf.append(ValuesString);
        return buf.toString();
    }

    /**
     * Write parameter item definition to a text line following CSV format
     * @return CSV row text
     */
    public String toCSVrow() {
        StringBuilder buf = new StringBuilder(ID);
        buf.append(",").append(Name);
        buf.append(",").append(ParamType);
        buf.append(",\"").append(Description).append("\"");
        buf.append(",").append(SearchString);
        buf.append(",").append(Type);
        buf.append(",").append(ValuesString);
        buf.append(",").append(SelectedAltValue);
        return buf.toString();
    }

    public String toText() {
        Properties prop = new Properties();
        prop.setProperty("ID", ID);
        prop.setProperty("Name", Name);
        prop.setProperty("Type", Integer.toString(Type));
        prop.setProperty("Description", Description);
        prop.setProperty("SearchString", SearchString);
        prop.setProperty("ValuesString", ValuesString);
        StringWriter sw = new StringWriter();
        try {
            prop.store(sw, "Parameter item details");
        } catch (Exception ex) {
            logger.error("", ex);
            return null;
        }
        return sw.getBuffer().toString();
    }

    public ParameterItem fromText(String text) {
        StringReader sr = new StringReader(text);
        Properties prop = new Properties();
        try {
            prop.load(sr);
            ID = prop.getProperty("ID");
            Name = prop.getProperty("Name");
            Type = Integer.parseInt(prop.getProperty("Type"));
            Description = prop.getProperty("Description", "");
            SearchString = prop.getProperty("SearchString");
            ValuesString = prop.getProperty("ValuesString");
            ValueStringChanged = true;
        } catch (IOException | NumberFormatException ex) {
            logger.error("", ex);
            return null;
        }
        return this;
    }

    public boolean exportCSV(String fn) {
        try (PrintWriter fw = new PrintWriter(new FileWriter(fn))) {
            fw.println("Index, ID, NAME, Type, DISCRIPTION, SEARCHSTRING, VALUE");
            String[] vals = getAlternativeValues();
            for (int i = 0; i < vals.length; i++) {
                fw.println("" + i + ", " + ID + ", " + Name + ", " + Type + ", " + Description + ", " + SearchString
                        + ", " + vals[i]);
            }
            return true;
        } catch (Exception ex) {
            logger.error("", ex);
        }
        return false;
    }

    public boolean exportSQL(String fn, String tablename) {
        try (PrintWriter fw = new PrintWriter(new FileWriter(fn, true))) {
            fw.println("CREATE TABLE `" + tablename + "` (");
            fw.println("`ID` smallint NOT NULL,");
            fw.println("`Name` varchar(25) NOT NULL,");
            fw.println("`LongName` varchar(255) NOT NULL,");
            fw.println("`Type` smallint NOT NULL,");
            fw.println("`Description` varchar(255) NOT NULL,");
            fw.println("`SearchString` varchar(50) NOT NULL,");
            fw.println("`Value` varchar(255) NOT NULL,");
            fw.println("PRIMARY KEY  (`ID`),");
            fw.println("KEY `Name` (`Name`)");
            fw.println(") ENGINE=InnoDB DEFAULT CHARSET=latin1;");
            fw.println();

            // Insert command
            fw.println("INSERT INTO `" + tablename
                    + "` (`ID`, `Name`, `LongName`, `Type`, `Description`, `SearchString`, `Value`) VALUES");
            String[] vals = getAlternativeValues();
            for (int i = 0; i < vals.length; i++) {
                if (i > 0)
                    fw.println(",");
                fw.print("(" + i + "," + "'" + ID + "', " + "'" + Name + "', " + Type + ", " + "'" + Description
                        + "', " + "'" + SearchString + "', " + "'" + vals[i] + "')");
            }
            fw.println(";");
            fw.println();
            return true;
        } catch (Exception ex) {
            logger.error("", ex);
        }
        return false;
    }

}