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.log4j.Logger;
047import org.openimaj.citation.ReferenceListener;
048import org.openimaj.citation.annotation.Reference;
049import org.openimaj.citation.annotation.output.StandardFormatters;
050import org.openimaj.experiment.agent.TimeTracker;
051import org.openimaj.experiment.annotations.DatasetDescription;
052import org.openimaj.experiment.annotations.DependentVariable;
053import org.openimaj.experiment.annotations.Experiment;
054import org.openimaj.experiment.annotations.IndependentVariable;
055import org.openimaj.experiment.evaluation.AnalysisResult;
056import org.openimaj.util.array.ArrayUtils;
057
058import com.bethecoder.ascii_table.ASCIITable;
059import com.bethecoder.ascii_table.ASCIITableHeader;
060
061/**
062 * The recorded context of an experiment.
063 * 
064 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
065 */
066public class ExperimentContext {
067        /**
068         * Representation of a recorded variable in an experiment (i.e. a field
069         * annotated with {@link IndependentVariable} or {@link DependentVariable}).
070         * 
071         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
072         */
073        public static class Variable {
074                /**
075                 * The variables identifier taken from the annotation, or the variables
076                 * name if the annotation has the default identifier.
077                 */
078                public String identifier;
079
080                Variable(String identifier) {
081                        this.identifier = identifier;
082                }
083        }
084
085        private static final Logger logger = Logger.getLogger(ExperimentContext.class);
086
087        private boolean isLocked;
088
089        private RunnableExperiment experiment;
090        private Date dateCompleted;
091        private Class<?> exptClass;
092        private Experiment experimentDetails;
093        private Set<Reference> bibliography;
094        private Map<String, SummaryStatistics> timingInfo;
095        private Map<Variable, Field> independentVariables = new HashMap<Variable, Field>();
096        private Map<Variable, Field> dependentVariables = new HashMap<Variable, Field>();
097
098        protected ExperimentContext(RunnableExperiment experiment) {
099                this.experiment = experiment;
100                experimentDetails = getExperiment(experiment);
101                exptClass = experiment.getClass();
102
103                readVariables(experiment);
104        }
105
106        private Experiment getExperiment(RunnableExperiment experiment) {
107                Class<?> exptClass = experiment.getClass();
108
109                while (exptClass != null) {
110                        final Experiment ann = exptClass.getAnnotation(Experiment.class);
111
112                        if (ann != null)
113                                return ann;
114
115                        exptClass = exptClass.getSuperclass();
116                }
117
118                return null;
119        }
120
121        private void readVariables(RunnableExperiment expt) {
122                Class<?> exptClass = expt.getClass();
123
124                while (exptClass != null) {
125
126                        for (final Field field : exptClass.getDeclaredFields()) {
127                                final IndependentVariable iv = field.getAnnotation(IndependentVariable.class);
128                                final DependentVariable dv = field.getAnnotation(DependentVariable.class);
129
130                                if (iv != null && dv != null)
131                                        throw new RuntimeException("Invalid experiment! The field " + field
132                                                        + " cannot be both a dependent and independent variable.");
133
134                                if (iv != null) {
135                                        String id = iv.identifier();
136                                        if (id == null || id.length() == 0)
137                                                id = field.getName();
138
139                                        this.independentVariables.put(new Variable(id), field);
140                                }
141
142                                if (dv != null) {
143                                        String id = dv.identifier();
144                                        if (id == null || id.length() == 0)
145                                                id = field.getName();
146
147                                        this.dependentVariables.put(new Variable(id), field);
148                                }
149                        }
150
151                        exptClass = exptClass.getSuperclass();
152                }
153        }
154
155        protected void lock() {
156                isLocked = true;
157                this.bibliography = ReferenceListener.getReferences();
158                this.timingInfo = TimeTracker.getTimes();
159                this.dateCompleted = new Date();
160        }
161
162        /**
163         * Get the bibliography for the experiment.
164         * 
165         * @return the bibliography
166         */
167        public Set<Reference> getBibliography() {
168                if (!isLocked)
169                        this.bibliography = ReferenceListener.getReferences();
170
171                return bibliography;
172        }
173
174        /**
175         * Get the timing information for the experiment.
176         * 
177         * @return the timing information
178         */
179        public Map<String, SummaryStatistics> getTimingInfo() {
180                if (!isLocked)
181                        this.timingInfo = TimeTracker.getTimes();
182
183                return timingInfo;
184        }
185
186        /**
187         * Get the independent variables of the experiment and their values at the
188         * time this method is called.
189         * 
190         * @return the independent variables
191         */
192        public Map<Variable, Object> getIndependentVariables() {
193                final Map<Variable, Object> vars = new HashMap<Variable, Object>();
194
195                for (final Entry<Variable, Field> e : this.independentVariables.entrySet()) {
196                        final Field field = e.getValue();
197                        field.setAccessible(true);
198                        Object value;
199                        try {
200                                value = field.get(this.experiment);
201                                vars.put(e.getKey(), value);
202                        } catch (final Exception ex) {
203                                logger.warn(ex);
204                                vars.put(e.getKey(), null);
205                        }
206                }
207
208                return vars;
209        }
210
211        /**
212         * Get the dependent variables of the experiment and their values at the
213         * time this method is called.
214         * 
215         * @return the dependent variables
216         */
217        public Map<Variable, Object> getDependentVariables() {
218                final Map<Variable, Object> vars = new HashMap<Variable, Object>();
219
220                for (final Entry<Variable, Field> e : this.dependentVariables.entrySet()) {
221                        final Field field = e.getValue();
222                        field.setAccessible(true);
223                        Object value;
224                        try {
225                                value = field.get(this.experiment);
226                                vars.put(e.getKey(), value);
227                        } catch (final Exception ex) {
228                                logger.warn(ex);
229                                vars.put(e.getKey(), null);
230                        }
231                }
232
233                return vars;
234        }
235
236        private String getExptInfoTable() {
237                final Date dc = dateCompleted == null ? new Date() : dateCompleted;
238
239                final List<String[]> data = new ArrayList<String[]>();
240                data.add(new String[] { "Class", exptClass.getName() });
241                data.add(new String[] { "Report compiled", new SimpleDateFormat().format(dc) });
242
243                if (experimentDetails != null) {
244                        data.add(new String[] { "Author", WordUtils.wrap(experimentDetails.author(), exptClass.getName().length()) });
245                        data.add(new String[] { "Created on", experimentDetails.dateCreated() });
246                        data.add(new String[] { "Description", WordUtils.wrap(experimentDetails.description(), exptClass.getName()
247                                        .length()) });
248                }
249
250                final ASCIITableHeader[] header = {
251                                new ASCIITableHeader("", ASCIITable.ALIGN_RIGHT),
252                                new ASCIITableHeader("", ASCIITable.ALIGN_LEFT)
253                };
254
255                String table = ASCIITable.getInstance().getTable(header, data.toArray(new String[data.size()][]));
256
257                final int width = table.indexOf("\n") + 1;
258                table = table.substring(2 * width);
259
260                return table;
261        }
262
263        private String getTimingTable() {
264                final ASCIITableHeader[] header = { new ASCIITableHeader("Experimental Timing", ASCIITable.ALIGN_CENTER) };
265                final String[][] data = formatAsTable(TimeTracker.format(timingInfo));
266                return ASCIITable.getInstance().getTable(header, data);
267        }
268
269        private String getBibliographyTable() {
270                final ASCIITableHeader[] header = { new ASCIITableHeader("Bibliography", ASCIITable.ALIGN_LEFT) };
271                String refs = StandardFormatters.STRING.format(bibliography);
272
273                refs = WordUtils.wrap(refs, Math.max(exptClass.getName().length() + 10, 72), SystemUtils.LINE_SEPARATOR + "  ",
274                                true);
275
276                final String[][] data = formatAsTable(refs);
277                return ASCIITable.getInstance().getTable(header, data);
278        }
279
280        private String[][] formatAsTable(String data) {
281                final String[] splits = data.trim().split("\\r?\\n");
282
283                final String[][] out = new String[splits.length][];
284                for (int i = 0; i < splits.length; i++) {
285                        out[i] = new String[] { splits[i] };
286                }
287
288                return out;
289        }
290
291        private String getIndependentVariablesTable() {
292                final ASCIITableHeader[] header = { new ASCIITableHeader("Independent Variables", ASCIITable.ALIGN_CENTER) };
293                final String[][] data = formatAsTable(formatVariables(getIndependentVariables()));
294                return ASCIITable.getInstance().getTable(header, data);
295        }
296
297        private String formatVariables(Map<Variable, Object> vars) {
298                final List<String[]> data = new ArrayList<String[]>();
299
300                for (final Entry<Variable, Object> e : vars.entrySet()) {
301                        final String id = e.getKey().identifier;
302                        final String[] val = formatValue(e.getValue());
303
304                        data.add(new String[] { id, val[0] });
305                        for (int i = 1; i < val.length; i++)
306                                data.add(new String[] { "", val[i] });
307                }
308
309                final ASCIITableHeader[] header = {
310                                new ASCIITableHeader("", ASCIITable.ALIGN_RIGHT),
311                                new ASCIITableHeader("", ASCIITable.ALIGN_LEFT)
312                };
313
314                String table = ASCIITable.getInstance().getTable(header, data.toArray(new String[data.size()][]));
315
316                final int width = table.indexOf("\n") + 1;
317                table = table.substring(2 * width);
318
319                return table;
320        }
321
322        private String getDependentVariablesTable() {
323                final ASCIITableHeader[] header = { new ASCIITableHeader("Dependent Variables", ASCIITable.ALIGN_CENTER) };
324                final String[][] data = formatAsTable(formatVariables(getDependentVariables()));
325                return ASCIITable.getInstance().getTable(header, data);
326        }
327
328        private String[] formatValue(Object value) {
329                // is it a dataset?
330                if (value.getClass().getAnnotation(DatasetDescription.class) != null) {
331                        final DatasetDescription d = value.getClass().getAnnotation(DatasetDescription.class);
332
333                        final List<String[]> data = new ArrayList<String[]>();
334                        data.add(new String[] { "Name", d.name() });
335
336                        final String[] description = WordUtils.wrap(d.description(), exptClass.getName().length() - 20).split(
337                                        "\\r?\\n");
338                        data.add(new String[] { "Description", description[0] });
339                        for (int i = 1; i < description.length; i++)
340                                data.add(new String[] { "", description[i] });
341
342                        final ASCIITableHeader[] header = {
343                                        new ASCIITableHeader("", ASCIITable.ALIGN_RIGHT),
344                                        new ASCIITableHeader("", ASCIITable.ALIGN_LEFT)
345                        };
346
347                        String table = ASCIITable.getInstance().getTable(header, data.toArray(new String[data.size()][]));
348
349                        final int width = table.indexOf("\n") + 1;
350                        table = table.substring(2 * width);
351
352                        return table.split("\\r?\\n");
353                }
354
355                // is it an analysis result?
356                if (value instanceof AnalysisResult) {
357                        // return
358                        // ((AnalysisResult)value).getDetailReport().split("\\r?\\n");
359                        return ((AnalysisResult) value).getSummaryReport().split("\\r?\\n");
360                }
361
362                if (value.getClass().isArray()) {
363                        final int length = Array.getLength(value);
364                        if (length == 0)
365                                return new String[] { "" };
366
367                        String str = Array.get(value, 0).toString();
368                        for (int i = 1; i < length; i++) {
369                                str += ", " + Array.get(value, i);
370                        }
371                        return WordUtils.wrap(str, exptClass.getName().length() - 20).split("\\r?\\n");
372                }
373
374                // otherwise use toString
375                return value.toString().split("\\r?\\n");
376        }
377
378        @Override
379        public String toString() {
380                final String[][] exptinfo = formatAsTable(getExptInfoTable());
381                final String[][] timeInfo = formatAsTable(getTimingTable());
382                final String[][] ivInfo = formatAsTable(getIndependentVariablesTable());
383                final String[][] dvInfo = formatAsTable(getDependentVariablesTable());
384                final String[][] biblInfo = formatAsTable(getBibliographyTable());
385
386                final String[][] data = ArrayUtils.concatenate(exptinfo, timeInfo, ivInfo, dvInfo, biblInfo);
387                final ASCIITableHeader[] header = { new ASCIITableHeader("Experiment Context", ASCIITable.ALIGN_LEFT) };
388
389                return ASCIITable.getInstance().getTable(header, data);
390        }
391}