Skip to content

Factory

Matt King edited this page Sep 30, 2019 · 13 revisions

CMakeLists

Firstly, create your source, header, and test files. Then you can add them to the CMakeLists.txt in CATAP/Factories/CMakeLists.txt.

# Inside EPICSInterface/CMakeLists.txt
message( "Processing EPICS Interface source files...")
set( SOURCE_FILES
    source/HardwareFactory.cpp
    source/MagnetFactory.cpp
    source/NewHardwareFactory.cpp
)

set( INCLUDE_FILES
    include/HardwareFactory.h
    include/MagnetFactory.h 
    include/NewHardwareFactory.h
)

set( TEST_FILES
    HardwareFactoryTest.cpp
    MagnetFactoryTest.cpp
    NewHardwareFactoryTest.cpp
)
...

Factory Class Files

A Factory class will be responsible for creating, connecting (to EPICS), and storing the hardware objects you want to create. It will also be able to access all of the hardware objects that are created. This makes the Factory an ideal class for performing functions on multiple hardware objects at once!

Header

Member Variables

There are a few member variables worth noting in the Factory class: isVirtual, configReader, and the map to store your hardware objects hardwareMap.

isVirtual

This is a boolean variable that is given to the constructor of the Factory class. It allows the configReader to determine which name to use from the Master Lattice YAML file. The below snippet demonstrates what happens when the configReader parses the YAML file:

/*Passing isVirtual when constructing the configReader member of Factory*/

/*In Factory Constructor*/
void MagnetFactory::MagnetFactory(/*isVirtual=*/true)
{
  /*...*/
  isVirtual = isVirtual;
  // call to configReader constructor
  reader = ConfigReader("Magnet", isVirtual);
}
/*Inside configReader constructor*/
ConfigReader::ConfigReader(const std::string &hardwareType, const bool &isVirtual) : 
/*initialise configReader.isVirtual to isVirtual value from magnetFactory*/ isVirtual(isVirtual),
hardwareFolder(hardwareType)
{
	yamlFileDestination = MASTER_LATTICE_FILE_LOCATION + SEPARATOR + hardwareFolder;
	initialiseFilenameAndParsedStatusMap();

}

/*Inside ConfigReader.cpp extractControlsInformationIntoPair function*/
if (controlsInformation["PV"].as<bool>())
	{
		std::string controlRecords = controlsInformation["records"].as<std::string>();
		boost::trim_left(controlRecords);
		std::pair<std::string, std::string> pvAndRecordPair;
                /*decide which name to use when constructing pvStructs*/
		if (this->isVirtual)
		{
			pvAndRecordPair = std::make_pair(configInformationNode["properties"]["virtual_name"].as<std::string>(), controlRecords);
		}
		else
		{
			pvAndRecordPair = std::make_pair(configInformationNode["properties"]["name"].as<std::string>(), controlRecords);
		}
		return pvAndRecordPair;
	}

By creating PVs that use the virtual_name of the hardware, we are only able to communicate with the Virtual Accelerator. If isVirtual is set to False, then we will only be able to access PVs that are on the real VELA/CLARA Accelerator.

configReader

In the context of a hardwareFactory, the configReader member reader will set up all hardware objects that are defined by Master Lattice YAML files. It will fill the hardwareMap (passed by reference) with constructed objects, ready to be used throughout the Factory class.

hardwareMap

The hardwareMap is the holder of all the hardware objects created from the Master Lattice YAML files within your hardware directory. Once you have defined your Hardware class you will be able to create the hardwareMap std::map<std::string, Hardware> hardwareMap.

It contains key,value pairs where the key is the PV name (CLA-C2V-MAG-HCOR-01) and the value being the hardware object associated with that name (Magnet).

The hardwareMap will be used to access individual/multiple hardware objects to perform tasks on them via the factory member functions. See the snippet of MagnetFactory::getCurrents(std::vector<std::string> magNames) below:

std::map<std::string, double> MagnetFactory::getCurrents(const std::vector<std::string>& names)
{
	std::map<std::string, double> currents;
	for (auto name : names)
	{
                // searching the keys of magnetMap for the names in the vector.
		double current = magnetMap.find(name)->second.getCurrent();
		currents[name] = current;
	}
	return currents;
}

Source

Member Functions

We will define 2 types of functions in the Factory class: setup functions, and Hardware related functions. The example of a MagnetFactory header file can be found here.

Populate Hardware Map

All hardware objects created by the factory will be stored in the hardware map. These will be setup by the ConfigReader which parses the Master Lattice YAML configuration files. The populate hardware map function will look like this:

void MagnetFactory::populateMagnetMap()
{
	if (!reader.hasMoreFilesToParse())
	{
		throw std::runtime_error("Did not receive configuration parameters from ConfigReader, please contact support");
	}
        // while we have more files to parse from the hardware directory
	while (reader.hasMoreFilesToParse())
	{
                //pass in the member variable magnetMap by reference,
                // so magnets created in ConfigReader stay alive.
		reader.parseNextYamlFile(magnetMap);
	}
}

This will be the first function called from within the hardware factory setup function.

Setup Function

Once our hardware map has been populated with hardware objects, the only thing left to do is setup their PVStructs with the appropriate values from EPICS. This will involve retrieiving PV related parameters from the epics interface, and setting up any subscriptions for the monitoring PVs.

The setup function for the magnetFactory is as follows:

	populateMagnetMap();
        // loop through all the magnet objects
	for (auto &magnet : magnetMap)
	{
                //get the pvStruct vector for each magnet
		std::map<std::string, pvStruct>& magPVStructs = magnet.second.getPVStructs();
		for (auto &pv : magPVStructs)
		{
                        // setting up PVStruct information for each PV
			std::string pvAndRecordName = pv.second.fullPVName + ":" + pv.first;
			retrieveMonitorStatus(pv.second);
			magnet.second.epicsInterface->retrieveCHID(pv.second);
			magnet.second.epicsInterface->retrieveCHTYPE(pv.second);
			magnet.second.epicsInterface->retrieveCOUNT(pv.second);
			magnet.second.epicsInterface->retrieveUpdateFunctionForRecord(pv.second);
			// not sure how to set the mask from EPICS yet.
			pv.second.MASK = DBE_VALUE;
                        // when the PV is a monitor PV, we will set up a subscription to the PV.
			if (pv.second.monitor)
			{
				magnet.second.epicsInterface->createSubscription(magnet.second, pv.second);
			}
		}
	}

Hardware related functions

The hardware factory will be the main way of interacting with the hardware objects via Python. So we will want to define functions that act on single, multiple, and all hardware objects present in the factory's hardware map. An example of the 3 implementations for the getCurrent function are shown below:

double MagnetFactory::getCurrent(const std::string& name)
{
	if (!hasBeenSetup)
	{
	    messenger.printDebugMessage("Please call MagnetFactory.setup(VERSION)");
	}
	else
	{
	    return magnetMap.find(name)->second.getCurrent();
	}
	return std::numeric_limits<double>::min();
}
std::map<std::string, double> MagnetFactory::getCurrents(const std::vector<std::string>& names)
{
	std::map<std::string, double> currents;
	for (auto name : names)
	{
           double current = magnetMap.find(name)->second.getCurrent();
	   currents[name] = current;
	}
	return currents;
}
std::map<std::string, double> MagnetFactory::getAllCurrents()
{
      std::map<std::string, double> currents;
      for (auto magnet : magnetMap)
      {
          double current = magnet.second.getCurrent();
          currents[magnet.first] = current;
      }
      return currents;
}

Python-typed hardware functions

For the above 3 functions to work in Python, we must take python-type arguments and return python-type results.

We can use the Boost.Python type classes for our getCurrents_Py function. All functions that relate to Python exposure benefit from having the suffix _Py.

Generally, any function that uses a std::vector will need to use boost::python::list and any function that uses a std::map will need to use a boost::python::dict. See the example of getCurrents below.

boost::python::dict MagnetFactory::getCurrents_Py(boost::python::list magNames)
{
	std::map<std::string, double> currents;
	std::vector<std::string> magNamesVector = to_std_vector<std::string>(magNames);
	currents = getCurrents(magNamesVector);
	boost::python::dict newPyDict = to_py_dict(currents);
	return newPyDict;
}

Test

The unit test cases for a hardware factory should involve 3 things:

  • create/setup a hardware factory
  • use the setter function to set a property
  • use the getter function to check that property has changed

Below is a test example for magnetFactory, in this test case we are changing the magnet PSU state and checking the result is as expected:

BOOST_AUTO_TEST_CASE(magnet_factory_turn_on_magnet_test)
{
	std::string testMagnetName = "VM-CLA-C2V-MAG-HCOR-01";
        // create/setup magnetFactory
	MagnetFactory magFac = MagnetFactory(true);
	magFac.setup("nominal");
        // use the setter function to set PSU State to ON
	magFac.turnOn(testMagnetName);
	std::this_thread::sleep_for(std::chrono::seconds(1));
        // use the getter function to check that PSU state has changed
	BOOST_CHECK_EQUAL(magFac.getPSUState(testMagnetName), 1);
}

For the full unit test suite of the magnetFactory, see here

Clone this wiki locally