001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.experiment;
031
032import java.lang.reflect.Array;
033import java.lang.reflect.Field;
034import java.text.SimpleDateFormat;
035import java.util.ArrayList;
036import java.util.Date;
037import java.util.HashMap;
038import java.util.List;
039import java.util.Map;
040import java.util.Map.Entry;
041import java.util.Set;
042
043import org.apache.commons.lang.SystemUtils;
044import org.apache.commons.lang.WordUtils;
045import org.apache.commons.math.stat.descriptive.SummaryStatistics;
046import org.apache.logging.log4j.Logger;
047import org.apache.logging.log4j.LogManager;
048
049import org.openimaj.citation.ReferenceListener;
050import org.openimaj.citation.annotation.Reference;
051import org.openimaj.citation.annotation.output.StandardFormatters;
052import org.openimaj.experiment.agent.TimeTracker;
053import org.openimaj.experiment.annotations.DatasetDescription;
054import org.openimaj.experiment.annotations.DependentVariable;
055import org.openimaj.experiment.annotations.Experiment;
056import org.openimaj.experiment.annotations.IndependentVariable;
057import org.openimaj.experiment.evaluation.AnalysisResult;
058import org.openimaj.util.array.ArrayUtils;
059
060import com.bethecoder.ascii_table.ASCIITable;
061import com.bethecoder.ascii_table.ASCIITableHeader;
062
063/**
064 * The recorded context of an experiment.
065 * 
066 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
067 */
068public class ExperimentContext {
069        /**
070         * Representation of a recorded variable in an experiment (i.e. a field
071         * annotated with {@link IndependentVariable} or {@link DependentVariable}).
072         * 
073         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
074         */
075        public static class Variable {
076                /**
077                 * The variables identifier taken from the annotation, or the variables
078                 * name if the annotation has the default identifier.
079                 */
080                public String identifier;
081
082                Variable(String identifier) {
083                        this.identifier = identifier;
084                }
085        }
086
087        private static final Logger logger = LogManager.getLogger(ExperimentContext.class);
088
089        private boolean isLocked;
090
091        private RunnableExperiment experiment;
092        private Date dateCompleted;
093        private Class<?> exptClass;
094        private Experiment experimentDetails;
095        private Set<Reference> bibliography;
096        private Map<String, SummaryStatistics> timingInfo;
097        private Map<Variable, Field> independentVariables = new HashMap<Variable, Field>();
098        private Map<Variable, Field> dependentVariables = new HashMap<Variable, Field>();
099
100        protected ExperimentContext(RunnableExperiment experiment) {
101                this.experiment = experiment;
102                experimentDetails = getExperiment(experiment);
103                exptClass = experiment.getClass();
104
105                readVariables(experiment);
106        }
107
108        private Experiment getExperiment(RunnableExperiment experiment) {
109                Class<?> exptClass = experiment.getClass();
110
111                while (exptClass != null) {
112                        final Experiment ann = exptClass.getAnnotation(Experiment.class);
113
114                        if (ann != null)
115                                return ann;
116
117                        exptClass = exptClass.getSuperclass();
118                }
119
120                return null;
121        }
122
123        private void readVariables(RunnableExperiment expt) {
124                Class<?> exptClass = expt.getClass();
125
126                while (exptClass != null) {
127
128                        for (final Field field : exptClass.getDeclaredFields()) {
129                                final IndependentVariable iv = field.getAnnotation(IndependentVariable.class);
130                                final DependentVariable dv = field.getAnnotation(DependentVariable.class);
131
132                                if (iv != null && dv != null)
133                                        throw new RuntimeException("Invalid experiment! The field " + field
134                                                        + " cannot be both a dependent and independent variable.");
135
136                                if (iv != null) {
137                                        String id = iv.identifier();
138                                        if (id == null || id.length() == 0)
139                                                id = field.getName();
140
141                                        this.independentVariables.put(new Variable(id), field);
142                                }
143
144                                if (dv != null) {
145                                        String id = dv.identifier();
146                                        if (id == null || id.length() == 0)
147                                                id = field.getName();
148
149                                        this.dependentVariables.put(new Variable(id), field);
150                                }
151                        }
152
153                        exptClass = exptClass.getSuperclass();
154                }
155        }
156
157        protected void lock() {
158                isLocked = true;
159                this.bibliography = ReferenceListener.getReferences();
160                this.timingInfo = TimeTracker.getTimes();
161                this.dateCompleted = new Date();
162        }
163
164        /**
165         * Get the bibliography for the experiment.
166         * 
167         * @return the bibliography
168         */
169        public Set<Reference> getBibliography() {
170                if (!isLocked)
171                        this.bibliography = ReferenceListener.getReferences();
172
173                return bibliography;
174        }
175
176        /**
177         * Get the timing information for the experiment.
178         * 
179         * @return the timing information
180         */
181        public Map<String, SummaryStatistics> getTimingInfo() {
182                if (!isLocked)
183                        this.timingInfo = TimeTracker.getTimes();
184
185                return timingInfo;
186        }
187
188        /**
189         * Get the independent variables of the experiment and their values at the
190         * time this method is called.
191         * 
192         * @return the independent variables
193         */
194        public Map<Variable, Object> getIndependentVariables() {
195                final Map<Variable, Object> vars = new HashMap<Variable, Object>();
196
197                for (final Entry<Variable, Field> e : this.independentVariables.entrySet()) {
198                        final Field field = e.getValue();
199                        field.setAccessible(true);
200                        Object value;
201                        try {
202                                value = field.get(this.experiment);
203                                vars.put(e.getKey(), value);
204                        } catch (final Exception ex) {
205                                logger.warn(ex);
206                                vars.put(e.getKey(), null);
207                        }
208                }
209
210                return vars;
211        }
212
213        /**
214         * Get the dependent variables of the experiment and their values at the
215         * time this method is called.
216         * 
217         * @return the dependent variables
218         */
219        public Map<Variable, Object> getDependentVariables() {
220                final Map<Variable, Object> vars = new HashMap<Variable, Object>();
221
222                for (final Entry<Variable, Field> e : this.dependentVariables.entrySet()) {
223                        final Field field = e.getValue();
224                        field.setAccessible(true);
225                        Object value;
226                        try {
227                                value = field.get(this.experiment);
228                                vars.put(e.getKey(), value);
229                        } catch (final Exception ex) {
230                                logger.warn(ex);
231                                vars.put(e.getKey(), null);
232                        }
233                }
234
235                return vars;
236        }
237
238        private String getExptInfoTable() {
239                final Date dc = dateCompleted == null ? new Date() : dateCompleted;
240
241                final List<String[]> data = new ArrayList<String[]>();
242                data.add(new String[] { "Class", exptClass.getName() });
243                data.add(new String[] { "Report compiled", new SimpleDateFormat().format(dc) });
244
245                if (experimentDetails != null) {
246                        data.add(new String[] { "Author", WordUtils.wrap(experimentDetails.author(), exptClass.getName().length()) });
247                        data.add(new String[] { "Created on", experimentDetails.dateCreated() });
248                        data.add(new String[] { "Description", WordUtils.wrap(experimentDetails.description(), exptClass.getName()
249                                        .length()) });
250                }
251
252                final ASCIITableHeader[] header = {
253                                new ASCIITableHeader("", ASCIITable.ALIGN_RIGHT),
254                                new ASCIITableHeader("", ASCIITable.ALIGN_LEFT)
255                };
256
257                String table = ASCIITable.getInstance().getTable(header, data.toArray(new String[data.size()][]));
258
259                final int width = table.indexOf("\n") + 1;
260                table = table.substring(2 * width);
261
262                return table;
263        }
264
265        private String getTimingTable() {
266                final ASCIITableHeader[] header = { new ASCIITableHeader("Experimental Timing", ASCIITable.ALIGN_CENTER) };
267                final String[][] data = formatAsTable(TimeTracker.format(timingInfo));
268                return ASCIITable.getInstance().getTable(header, data);
269        }
270
271        private String getBibliographyTable() {
272                final ASCIITableHeader[] header = { new ASCIITableHeader("Bibliography", ASCIITable.ALIGN_LEFT) };
273                String refs = StandardFormatters.STRING.format(bibliography);
274
275                refs = WordUtils.wrap(refs, Math.max(exptClass.getName().length() + 10, 72), SystemUtils.LINE_SEPARATOR + "  ",
276                                true);
277
278                final String[][] data = formatAsTable(refs);
279                return ASCIITable.getInstance().getTable(header, data);
280        }
281
282        private String[][] formatAsTable(String data) {
283                final String[] splits = data.trim().split("\\r?\\n");
284
285                final String[][] out = new String[splits.length][];
286                for (int i = 0; i < splits.length; i++) {
287                        out[i] = new String[] { splits[i] };
288                }
289
290                return out;
291        }
292
293        private String getIndependentVariablesTable() {
294                final ASCIITableHeader[] header = { new ASCIITableHeader("Independent Variables", ASCIITable.ALIGN_CENTER) };
295                final String[][] data = formatAsTable(formatVariables(getIndependentVariables()));
296                return ASCIITable.getInstance().getTable(header, data);
297        }
298
299        private String formatVariables(Map<Variable, Object> vars) {
300                final List<String[]> data = new ArrayList<String[]>();
301
302                for (final Entry<Variable, Object> e : vars.entrySet()) {
303                        final String id = e.getKey().identifier;
304                        final String[] val = formatValue(e.getValue());
305
306                        data.add(new String[] { id, val[0] });
307                        for (int i = 1; i < val.length; i++)
308                                data.add(new String[] { "", val[i] });
309                }
310
311                final ASCIITableHeader[] header = {
312                                new ASCIITableHeader("", ASCIITable.ALIGN_RIGHT),
313                                new ASCIITableHeader("", ASCIITable.ALIGN_LEFT)
314                };
315
316                String table = ASCIITable.getInstance().getTable(header, data.toArray(new String[data.size()][]));
317
318                final int width = table.indexOf("\n") + 1;
319                table = table.substring(2 * width);
320
321                return table;
322        }
323
324        private String getDependentVariablesTable() {
325                final ASCIITableHeader[] header = { new ASCIITableHeader("Dependent Variables", ASCIITable.ALIGN_CENTER) };
326                final String[][] data = formatAsTable(formatVariables(getDependentVariables()));
327                return ASCIITable.getInstance().getTable(header, data);
328        }
329
330        private String[] formatValue(Object value) {
331                // is it a dataset?
332                if (value.getClass().getAnnotation(DatasetDescription.class) != null) {
333                        final DatasetDescription d = value.getClass().getAnnotation(DatasetDescription.class);
334
335                        final List<String[]> data = new ArrayList<String[]>();
336                        data.add(new String[] { "Name", d.name() });
337
338                        final String[] description = WordUtils.wrap(d.description(), exptClass.getName().length() - 20).split(
339                                        "\\r?\\n");
340                        data.add(new String[] { "Description", description[0] });
341                        for (int i = 1; i < description.length; i++)
342                                data.add(new String[] { "", description[i] });
343
344                        final ASCIITableHeader[] header = {
345                                        new ASCIITableHeader("", ASCIITable.ALIGN_RIGHT),
346                                        new ASCIITableHeader("", ASCIITable.ALIGN_LEFT)
347                        };
348
349                        String table = ASCIITable.getInstance().getTable(header, data.toArray(new String[data.size()][]));
350
351                        final int width = table.indexOf("\n") + 1;
352                        table = table.substring(2 * width);
353
354                        return table.split("\\r?\\n");
355                }
356
357                // is it an analysis result?
358                if (value instanceof AnalysisResult) {
359                        // return
360                        // ((AnalysisResult)value).getDetailReport().split("\\r?\\n");
361                        return ((AnalysisResult) value).getSummaryReport().split("\\r?\\n");
362                }
363
364                if (value.getClass().isArray()) {
365                        final int length = Array.getLength(value);
366                        if (length == 0)
367                                return new String[] { "" };
368
369                        String str = Array.get(value, 0).toString();
370                        for (int i = 1; i < length; i++) {
371                                str += ", " + Array.get(value, i);
372                        }
373                        return WordUtils.wrap(str, exptClass.getName().length() - 20).split("\\r?\\n");
374                }
375
376                // otherwise use toString
377                return value.toString().split("\\r?\\n");
378        }
379
380        @Override
381        public String toString() {
382                final String[][] exptinfo = formatAsTable(getExptInfoTable());
383                final String[][] timeInfo = formatAsTable(getTimingTable());
384                final String[][] ivInfo = formatAsTable(getIndependentVariablesTable());
385                final String[][] dvInfo = formatAsTable(getDependentVariablesTable());
386                final String[][] biblInfo = formatAsTable(getBibliographyTable());
387
388                final String[][] data = ArrayUtils.concatenate(exptinfo, timeInfo, ivInfo, dvInfo, biblInfo);
389                final ASCIITableHeader[] header = { new ASCIITableHeader("Experiment Context", ASCIITable.ALIGN_LEFT) };
390
391                return ASCIITable.getInstance().getTable(header, data);
392        }
393}