From 0ff868ae1e461dcad711e2b70e611137029e802b Mon Sep 17 00:00:00 2001
From: jmfisher <j.fisher@jz-juelich.de>
Date: Wed, 23 Mar 2016 16:17:01 +0100
Subject: [PATCH] fixed python3 import problem, added custom FF to performance
 test

---
 Tests/PerformanceTests/README              |  10 +
 Tests/PerformanceTests/customformfactor.py | 136 -----------
 Tests/PerformanceTests/test_performance.py | 251 +++++++++++++++------
 cmake/scripts/__init__.py.in               |   4 +
 4 files changed, 202 insertions(+), 199 deletions(-)
 create mode 100644 Tests/PerformanceTests/README
 delete mode 100644 Tests/PerformanceTests/customformfactor.py

diff --git a/Tests/PerformanceTests/README b/Tests/PerformanceTests/README
new file mode 100644
index 00000000000..7e059dc797b
--- /dev/null
+++ b/Tests/PerformanceTests/README
@@ -0,0 +1,10 @@
+This script replaces the performance test functionality of App.
+
+Example usage:
+
+python3 test_performance.py ../../dev-tools/log/perf_history.txt
+
+This will perform the tests and append the summary to the file
+"perf_history.txt". If the filename argument is left blank, the
+script will simply write the results to stdout.
+
diff --git a/Tests/PerformanceTests/customformfactor.py b/Tests/PerformanceTests/customformfactor.py
deleted file mode 100644
index fb60d879b22..00000000000
--- a/Tests/PerformanceTests/customformfactor.py
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/usr/bin/python
- # -*- coding: utf-8 -*-
-
-
-## ************************************************************************** ##
-##
-##  BornAgain: simulate and fit scattering at grazing incidence
-##
-##! @file      customformfactor.py
-##! @brief     Test performance of customformfactor
-##!
-##! @homepage  http:##www.bornagainproject.org
-##! @license   GNU General Public License v3 or higher (see COPYING)
-##! @copyright Forschungszentrum Jülich GmbH 2016
-##! @authors   Scientific Computing Group at MLZ Garching
-##! @authors   C. Durniak, M. Ganeva, G. Pospelov, W. Van Herck, J. Wuttke
-##
-## ************************************************************************** ##
-
-
-from __future__ import print_function
-import sys
-import os
-import numpy
-import math
-import timeit
-import resource
-
-from libBornAgainCore import *
-
-nrepetitions = 50
-
-
-
-
-
-
-phi_min, phi_max = -1.0, 1.0
-alpha_min, alpha_max = 0.0, 2.0
-
-
-class CustomFormFactor(IFormFactorBorn):
-    """
-    A custom defined form factor
-    The form factor is V sech(q L) with
-    V volume of particle
-    L length scale which defines mean radius
-    """
-    def __init__(self, V, L):
-        IFormFactorBorn.__init__(self)
-        # parameters describing the form factor
-        self.V = V
-        self.L = L
-
-    def clone(self):
-        """
-        IMPORTANT NOTE:
-        The clone method needs to call transferToCPP() on the cloned object
-        to transfer the ownership of the clone to the cpp code
-        """
-        cloned_ff = CustomFormFactor(self.V, self.L)
-        cloned_ff.transferToCPP()
-        return cloned_ff
-
-    def evaluate_for_q(self, q):
-        return self.V*1.0/math.cosh(q.mag()*self.L)
-
-
-def get_sample():
-    """
-    Build and return the sample to calculate custom form factor in Distorted Wave Born Approximation.
-    """
-    # defining materials
-    m_ambience = HomogeneousMaterial("Air", 0.0, 0.0)
-    m_substrate = HomogeneousMaterial("Substrate", 6e-6, 2e-8)
-    m_particle = HomogeneousMaterial("Particle", 6e-4, 2e-8)
-
-    # collection of particles
-    ff = CustomFormFactor(343.0*nanometer, 7.0*nanometer)
-    particle = Particle(m_particle, ff)
-    particle_layout = ParticleLayout()
-    particle_layout.addParticle(particle, 1.0)
-    air_layer = Layer(m_ambience)
-    air_layer.addLayout(particle_layout)
-    substrate_layer = Layer(m_substrate)
-
-    # assemble multilayer
-    multi_layer = MultiLayer()
-    multi_layer.addLayer(air_layer)
-    multi_layer.addLayer(substrate_layer)
-    return multi_layer
-
-
-def get_simulation():
-    """
-    Create and return GISAXS simulation with beam and detector defined
-    IMPORTANT NOTE:
-    Multithreading should be deactivated by putting ThreadInfo.n_threads to -1
-    """
-    simulation = GISASSimulation()
-    thread_info = ThreadInfo()
-    thread_info.n_threads = -1
-    simulation.setThreadInfo(thread_info)
-    simulation.setDetectorParameters(100, phi_min*degree, phi_max*degree, 100, alpha_min*degree, alpha_max*degree)
-    simulation.setBeamParameters(1.0*angstrom, 0.2*degree, 0.0*degree)
-    return simulation
-
-
-def run_test():
-    """
-    Run simulation and plot results
-    """
-    sample = get_sample()
-    simulation = get_simulation()
-
-    start_time_real = timeit.default_timer()
-    start_time_user = resource.getrusage(resource.RUSAGE_SELF)[0]
-    
-    for i in range(nrepetitions):
-        simulation.setSample(sample)
-        simulation.runSimulation()
-
-    end_time_real = timeit.default_timer()
-    end_time_user = resource.getrusage(resource.RUSAGE_SELF)[0]
-        
-    real_time = (end_time_real - start_time_real)
-    user_time = (end_time_real - start_time_real)
-
-    print("\ntime after %d repetitions:" % nrepetitions)
-    print("real time: %.4f" % real_time)
-    print("user time: %.4f\n" % user_time)
-
-
-
-if __name__ == '__main__':
-    run_test()
diff --git a/Tests/PerformanceTests/test_performance.py b/Tests/PerformanceTests/test_performance.py
index fa50aa18ba8..c15776611b5 100644
--- a/Tests/PerformanceTests/test_performance.py
+++ b/Tests/PerformanceTests/test_performance.py
@@ -1,7 +1,6 @@
 #!/usr/bin/python
  # -*- coding: utf-8 -*-
 
-
 ## ************************************************************************** ##
 ##
 ##  BornAgain: simulate and fit scattering at grazing incidence
@@ -23,26 +22,180 @@ import sys
 import platform
 import timeit
 from collections import OrderedDict
-import resource
+import math
 from libBornAgainCore import *
 
+# used for logging intermediate output
 logfile = sys.stdout
 
-class PerfTest:
+# for code readability
+get_wall_time = timeit.default_timer
+
+# for code readability and portability, since resource is not guaranteed to be available
+try:
+    import resource
+    record_cpu_time = True
+    get_cpu_time = lambda: resource.getrusage(resource.RUSAGE_SELF)[0]
+except:
+    record_cpu_time = False
+    get_cpu_time = lambda: None
+      
+
+# globals used in custom form factor
+phi_min, phi_max = -1.0, 1.0
+alpha_min, alpha_max = 0.0, 2.0
+
+# user-defined custom form factor
+class CustomFormFactor(IFormFactorBorn):
+    """
+    A custom defined form factor
+    The form factor is V sech(q L) with
+    V volume of particle
+    L length scale which defines mean radius
+    """
+    def __init__(self, V, L):
+        IFormFactorBorn.__init__(self)
+        # parameters describing the form factor
+        self.V = V
+        self.L = L
+
+    def clone(self):
+        """
+        IMPORTANT NOTE:
+        The clone method needs to call transferToCPP() on the cloned object
+        to transfer the ownership of the clone to the cpp code
+        """
+        cloned_ff = CustomFormFactor(self.V, self.L)
+        cloned_ff.transferToCPP()
+        return cloned_ff
+
+    def evaluate_for_q(self, q):
+        return self.V*1.0/math.cosh(q.mag()*self.L)
+
+# class for performance test, constructed using sample factories
+class FactoryTest:
     def __init__(self, name, simulation_name, sample_builder, nrepetitions):
         self.m_test_name = name
         self.m_simulation_name = simulation_name
         self.m_sample_builder_name = sample_builder
         self.m_nrepetitions = nrepetitions
         self.m_cpu_time = 0.0
-        self.m_real_time = 0.0
+        self.m_wall_time = 0.0
+
+        self.m_sample = None
+
+        if simulation_name != None and sample_builder != None:
+            self.m_sample_factory = SampleBuilderFactory()
+            self.m_simulation_registry = SimulationRegistry()
+            self.m_simulation = self.m_simulation_registry.createSimulation(self.m_simulation_name)
+            self.m_sample_builder = self.m_sample_factory.createBuilder(self.m_sample_builder_name)
+        else:
+            self.m_sample_factory = None
+            self.m_simulation_registry = None
+            self.m_simulation = None
+            self.m_sample_builder = None
+
+    def prepare(self):
+        pass
+
+    # do the actual work
+    def run_loop(self):
+        # actual work is done here
+        for i in range(self.m_nrepetitions):
+            self.m_simulation.setSampleBuilder(self.m_sample_builder)
+            self.m_simulation.runSimulation()
+
+    def run(self):
+
+        self.prepare()
+        
+        logfile.write("Running test: %-30s " % self.m_test_name)
+        logfile.flush()
+
+        # resource.getrusage is less accurate but returns user (cpu) time
+        start_wall_time = get_wall_time()
+        start_cpu_time = get_cpu_time()
+
+        self.run_loop()
 
-class TestPerformance:
+        end_wall_time = get_wall_time()
+        end_cpu_time = get_cpu_time()
+
+        # note that on multi-core system, cpu time can be higher than wall time
+        self.m_wall_time = end_wall_time - start_wall_time
+        
+        if record_cpu_time:
+            self.m_cpu_time = end_cpu_time - start_cpu_time
+            logfile.write("OK: %-6.3f (wall sec), %-6.3f (cpu sec) \n" % (self.m_wall_time, self.m_cpu_time))
+        else:
+            logfile.write("OK: %-6.3f (wall sec)\n" % (self.m_wall_time))
+
+
+            
+# special performance test case: custom form factor
+class CustomTest(FactoryTest):
+    def __init__(self, name, nrepetitions):
+        FactoryTest.__init__(self, name, None, None, nrepetitions)
+        self.m_sample = None
+        self.m_simulation = None
+
+    def prepare(self):
+        self.m_sample = self.get_sample()
+        self.m_simulation = self.get_simulation()
+
+    # do the actual work
+    def run_loop(self):
+        for i in range(self.m_nrepetitions):
+            self.m_simulation.setSample(self.m_sample)              
+            self.m_simulation.runSimulation()
+            
+    def get_sample(self):
+        """
+        Build and return the sample to calculate custom form factor in Distorted Wave Born Approximation.
+        """
+        # defining materials
+        m_ambience = HomogeneousMaterial("Air", 0.0, 0.0)
+        m_substrate = HomogeneousMaterial("Substrate", 6e-6, 2e-8)
+        m_particle = HomogeneousMaterial("Particle", 6e-4, 2e-8)
+        
+        # collection of particles
+        ff = CustomFormFactor(343.0*nanometer, 7.0*nanometer)
+        particle = Particle(m_particle, ff)
+        particle_layout = ParticleLayout()
+        particle_layout.addParticle(particle, 1.0)
+        air_layer = Layer(m_ambience)
+        air_layer.addLayout(particle_layout)
+        substrate_layer = Layer(m_substrate)
+
+        # assemble multilayer
+        multi_layer = MultiLayer()
+        multi_layer.addLayer(air_layer)
+        multi_layer.addLayer(substrate_layer)
+        return multi_layer
+
+    def get_simulation(self):
+        """
+        Create and return GISAXS simulation with beam and detector defined
+        IMPORTANT NOTE:
+        Multithreading should be deactivated by putting ThreadInfo.n_threads to -1
+        """
+        simulation = GISASSimulation()
+        thread_info = ThreadInfo()
+        thread_info.n_threads = -1
+        simulation.setThreadInfo(thread_info)
+        simulation.setDetectorParameters(100, phi_min*degree, phi_max*degree, 100, alpha_min*degree, alpha_max*degree)
+        simulation.setBeamParameters(1.0*angstrom, 0.2*degree, 0.0*degree)
+        return simulation
+
+
+# class for running all of the tests and collecting results
+class PerformanceTests:
     def __init__(self, filename=None):
         self.m_tests = []
         self.m_datime = ""
         self.m_hostname = ""
         self.m_sysinfo = ""
+        self.m_pyversion = ""
         self.m_filename = filename
 
         self.add("MultiLayer",         "MaxiGISAS",    "MultiLayerWithRoughnessBuilder", 1);
@@ -55,104 +208,76 @@ class TestPerformance:
         self.add("SSCA",               "MaxiGISAS",    "SizeDistributionSSCAModelBuilder", 10);
         self.add("Mesocrystal",        "MaxiGISAS",    "MesoCrystalBuilder", 2);
         self.add("PolMagCyl",          "MaxiGISAS00",  "MagneticCylindersBuilder", 10);
+
+        # custom form factor is a special case since it's not in the registry
+        self.m_tests.append(CustomTest("Custom FF", 10))     
+        
         logfile.write("\nPreparing to run %d performance tests.\n\n" % len(self.m_tests))
 
     def add(self, name, simulation_name, sample_builder, nrepetitions):
-        self.m_tests.append(PerfTest(name, simulation_name, sample_builder, nrepetitions))
+        self.m_tests.append(FactoryTest(name, simulation_name, sample_builder, nrepetitions))
 
+    # execute all performance tests
     def execute(self):
         self.init_sysinfo()
-
-        for test in self.m_tests:
-            self.runTest(test)
-        
+        for test in self.m_tests: test.run()
         self.write_results()
-        
-    def runTest(self, test):
-        simulationRegistry = SimulationRegistry()
-        sampleFactory = SampleBuilderFactory()
-                
-        # std::cout << "Running test: " << std::setw(20) << std::left << test->m_test_name << " ... ";
-        logfile.write("Running test: %-30s " % test.m_test_name)
-        logfile.flush()
-        
-        # std::cout.flush();
-
-        # boost::scoped_ptr<Simulation> simulation(simulationRegistry.createSimulation(test->m_simulation_name));
-        simulation = simulationRegistry.createSimulation(test.m_simulation_name)
-        
-        # SampleBuilder_t sample = sampleFactory.createBuilder(test->m_sample_builder_name);
-        sample = sampleFactory.createBuilder(test.m_sample_builder_name)
-
-        # timeit.default_timer picks the most accurate timer available
-        start_real_time = timeit.default_timer()
-
-        # resource.getrusage is less accurate but returns user time
-        start_cpu_time = resource.getrusage(resource.RUSAGE_SELF)[0]
-
-        for i in range(test.m_nrepetitions):
-            simulation.setSampleBuilder(sample)
-            simulation.runSimulation()
-
-        end_real_time = timeit.default_timer()
-        end_cpu_time = resource.getrusage(resource.RUSAGE_SELF)[0]
-
-        # note that on multi-core system, cpu time can be higher than wall time
-        test.m_real_time = end_real_time - start_real_time
-        test.m_cpu_time = end_cpu_time - start_cpu_time
-
-        logfile.write("OK: %-6.3f (real sec), %-6.3f (cpu sec) \n" % (test.m_real_time, test.m_cpu_time))
 
+    
+    # write out system information and test results to file
     def write_results(self):
 
         if ( self.m_filename != None ):
             try:
                 write_file = open(self.m_filename, "a")
             except:
-                sys.stderr.write("Could not open filed %s for writing.\n" % self.m_filename)
-                exit(1)
+                sys.stderr.write("Could not open filed '%' for writing. Writing to stdout instead.\n" % self.m_filename)
+                write_file = sys.stdout
+                self.m_filename = None
+
         else:
             write_file = sys.stdout
 
         logfile.write("\nWriting output to %s...\n\n" % write_file.name)
-                
+
+        # record results into an ordered dict, which is used by pretty_write() below
         dictionary = OrderedDict()
 
         dictionary["date"] = self.m_datime
         dictionary["hostname"] = self.m_hostname
         dictionary["sysinfo"] = self.m_sysinfo
+        dictionary["python"] = self.m_pyversion
 
-        sum_real = 0.0
+        sum_wall = 0.0
         sum_cpu = 0.0
         
         for test in self.m_tests:
-            sum_real += test.m_real_time;
+            sum_wall += test.m_wall_time;
             sum_cpu += test.m_cpu_time;
 
-        dictionary["total cpu"] = "%-.4f" % sum_cpu
-        dictionary["total real"] = "%-.4f" % sum_real
+        if record_cpu_time: dictionary["total cpu"] = "%-.4f" % sum_cpu
+        dictionary["total wall"] = "%-.4f" % sum_wall
 
         for test in self.m_tests:
-            dictionary[test.m_test_name] = "%-.4f" % test.m_real_time
-            
+            dictionary[test.m_test_name] = "%-.4f" % test.m_wall_time
+
+        write_file.write("\n");
         pretty_write(dictionary, write_file)
         write_file.write("\n");
         write_file.flush()
 
         if ( self.m_filename != None ):
             write_file.close()  
-            
+
+    # determine platform, architecture, python version, etc.
     def init_sysinfo(self):
         system, node, release, version, machine, processor = platform.uname()
-        
-        if system == 'Linux':        
-            system, version, id = platform.linux_distribution()
-
-        self.m_sysinfo = system + " " + version + " " + machine
         self.m_datime = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d %H:%M:%S')
         self.m_hostname = node        
+        self.m_sysinfo = "%s %s" % (system, machine)
+        self.m_pyversion = "%d.%d" % (sys.version_info[:2])
 
-
+# used for writing the summary with proper formatting
 def pretty_write(dictionary, file):
     header = "|"
     footer = "|"
@@ -178,5 +303,5 @@ if __name__ == '__main__':
     else:
         filename = None
         
-    testPerformance = TestPerformance(filename)
-    testPerformance.execute()
+    tests = PerformanceTests(filename)
+    tests.execute()
diff --git a/cmake/scripts/__init__.py.in b/cmake/scripts/__init__.py.in
index 79fb2939c01..667e10d0b41 100644
--- a/cmake/scripts/__init__.py.in
+++ b/cmake/scripts/__init__.py.in
@@ -6,5 +6,9 @@ sys.path.append(os.path.abspath(
 
 from libBornAgainCore import *
 from libBornAgainFit import *
+
+# this line needed to fix python3 import problem
+sys.path.append(os.path.abspath(os.path.dirname(__file__)))
+
 from plot_utils import *
 
-- 
GitLab