test-measure-process-lib


Nametest-measure-process-lib JSON
Version 1.0.9 PyPI version JSON
download
home_page
SummaryTest, Measure and Process library. Framework for lab experiments.
upload_time2023-07-03 21:42:13
maintainer
docs_urlNone
author
requires_python>=3.7
licenseApache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
keywords lab test measurement scientific engineering
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Test, Measure, Process library (TMPL)

TMPL is a library for writing lab test measurement code in a modular and reusable way.

Lab tests are broken up into lightweight classes that represent setup conditions and measurements. These conditions and measurements can be combined together to make test sequences. TMPL provides infrastructure to execute the conditions and measurements in a specific order and log the data into a convenient format.

Although written for use in lab measurements TMPL makes no assumptions about test equipment setup and is very generic. It could be used to make test sequences based on models or data analysis instead of actual measurements.

TMPL is built around storing data in the [xarray](http://xarray.pydata.org/en/stable/) _Dataset_ class. This provides a convenient data structure for storing multi-dimensional data that can be easily visualised using libraries like [Holoviews](http://holoviews.org/index.html) or its offshoot [hvplot](https://hvplot.holoviz.org/index.html).

## Dependencies and installation
TMPL depends on these libraries. 

* [xarray](http://xarray.pydata.org/en/stable/)
* [pandas](https://pandas.pydata.org/pandas-docs/stable/)
* [xlsxwriter](https://xlsxwriter.readthedocs.io/)

TMPL can be installed via *pip*

```bash
pip install test-measure-process-lib
```

Note that this does not install the dependencies. This is in case another package manager, e.g. Anaconda, is being used. So they have to be manually installed.

## Documentation
This file gives basic descriptions of how to use TMPL, for more details consult [the full documentation](https://redlegjed.github.io/test_measure_process_lib/)


## Core classes

TMPL is built on a set of core classes. These are built by inheriting from the Abstract classes defined in *tmpl_core.py*. The classes are:

* _TestManager_ classes: Based on _AbstractTestManager_ class. These classes run sequences of measurements over multiple setup conditions e.g. temperature, humidity etc. The _TestManagers_ are ultimately responsible for gathering up all data recorded during the sequence and packaging it into one xarray Dataset object.
* _Measurement_ classes: Based on _AbstractMeasurement_ class. These are the classes that actually perform the measurements. They are classes that can be run independently or in a sequence from a _TestManager_. The data that they collect and process is stored in an internal xarray Dataset object. When run from a _TestManager_ this dataset will be scooped up at the end of the sequence and added into the overall Dataset maintained by the _TestManager_.
* _SetupCondition_ classes: Based on _AbstractSetupCondition_. These classes are responsible for setting up experimental conditions e.g. temperature, pressure, supply voltage etc. They are small classes that have only one purpose, to set a specific condition. _TestManagers_ use _SetupCondition_ classes to set conditions during a sequence before running measurements. 


## Example usage

Before explaining the inner workings of TMPL this section runs through a hypothetical example to show how the classes are used at the top level.

Suppose a _TestManager_ has been defined that has objects for setting *temperature and pressure*. It also has three measurement objects defined called *calibrate_scales, MeasureVolume and MeasureMass*.

The _TestManager_ is initialised with any _resources_ that the measurement classes require, e.g. instruments.

```python
# Define test equipment objects for measurements to use
# - can be anything that Measurement classes require
resources = {'chamber':chamber_object,
            'test_sample':test_sample_object,
            'instrument':instr_object}

# Create test manager object
test = TestManager_mymeas(resources)
```

Setup conditions under which test is to be run can be defined by accessing the _SetupConditions_ objects directly through the _conditions_ property.

```python
test.conditions.temperature.values = [25,40,75]
test.conditions.pressure.values = [12,15,65]
```

The measurements to run during the sequence can be enabled/disabled by direct access to the _Measurement_ objects through the _meas_ property.

```python
test.meas.calibrate_scales.enable = False
test.meas.MeasureVolume.enable = True
test.meas.MeasureMass.enable = True
```

Now the sequence has been configured we can run the test over all setup conditions

```python
test.run()
```

Once the test sequence is finished we can get the results as an *xarray Dataset*.

```python
test.ds_results
```

Can also get individual results from a _Measurement_ object directly.

```python
test.meas.MeasureVolume.ds_results
```

Individual measurements can be run with specific conditions independent of the _TestManager_.

```python
conditions = dict(temperature_degC=34,pressure_nm=15)
test.meas.MeasureVolume.run(conditions)
```

Or measurements can be run without specifiying conditions

```python
test.meas.MeasureVolume.run()
```

Note in the last two cases where the measurement is run individually, specifiying the conditions merely includes the conditions as coordinates in the results Dataset. _Measurement_ classes _do not_ set their own conditions, that is only done by _SetupConditions_ classes.

## Creating a measurement sequence

A measurement sequence consists of a _TestManager_ class to run the overall sequence, any number of _SetupConditions_ classes and any number of _Measurement_ classes.

Let's take a simple example, the measurement of resistance of a resistor.

```
+--------------+
|   voltage    |
|   source     +-----+
|              |     |
+--------------+     |
                     |
                     |
                   +-+-+
                   |   |
                   |   |
                   | R |    Resistor to measure
                   |   |
                   |   |
                   |   |
                   +-+-+
                     |
                     |
                +----+-----+
                |          |
                | Ammeter  |
                |          |
                +----+-----+
                     |
                     |
                 --------- Ground
                   -----
                    ---

```
We have two pieces of test equipment in this measurement: the voltage source and the ammeter. The measurement is simply to set a voltage, measure the current, and calculate the resistance from Ohm's law :

Voltage = Resistance x Current

In this measurement we have one setup condition, voltage, one measurement, current and one processing step, resistance.

Let's assume that the voltage source and ammeter are controlled through the objects *voltage_source* and *ammeter*. These two objects will be supplied as _resources_ to the _TestManager_, _SetupConditions_ and _Measurement_ classes e.g.

```python
resources = {'voltage_source':voltage_source, 'ammeter':ammeter}
```
All classes will automatically have the *voltage_source* and *ammeter* objects available as properties.

### Setup conditions

First we'll setup the voltage source. This is our only setup condition and it will be done using a _SetupConditions_ class. _SetupConditions_ classes inherit from the abstract class _AbstractSetupConditions_. They require one method and two properties to be defined:

* _initialise_ : Perform any initialisation, usually setting defaults for the property _values_.
* _setpoint_ : Property that is used to set/get the condition set point value
* _actual_ : Property that returns the actual value of the condition, e.g. the actual voltage rather than the setpoint

The complete class definition is shown here:

```python
class Voltage(tmpl.AbstractSetupConditions):

    def initialise(self):
        """
        Initialise default values and any other setup
        """

        # Set default values
        self.values = [3.0]

        
    @property
    def actual(self):
        """
        Return actual measured voltage
        """
        return self.voltage_source.actual_voltage_V

    @property
    def setpoint(self):
        """
        Get/Set the output voltage of the voltage source
        """
        return self.voltage_source.voltage_set_V

    @setpoint.setter
    def setpoint(self,value):
        self.log(f'Set Voltage source to {value}V') # printout
        self.voltage_source.voltage_set_V = value
        
```

### Measurements

Next the central measurement class is defined. Measurement classes inherit from the _AbstractMeasurement_ class. The only method that _needs_ to be defined is *meas_sequence()*. This is generally the top level function of a specific measurement procedure. Any number of extra methods can be added to the class to support *meas_sequence()*, but when a measurement is executed it basically calls the *meas_sequence()* method.

In this case the measurement is simply to read the ammeter and store the reading, which can be done in the *meas_sequence()*. The resistance, however, is derived from the ammeter reading and the setpoint of the voltage source. Since this is "processing" rather than measurement it is good practice to do this in another method. This ensures that the real measurement, the ammeter reading, is done even if the processing step crashes. In this case the processing function could be re-run later to debug it, without re-running the measurement.

```python
class CurrentMeasure(tmpl.AbstractMeasurement):
           

    def meas_sequence(self):
        """
        Mandatory method for Measurement classes

        Performs the actual measurement and stores data.
        """
        #  Measure current with ammeter
        current = self.ammeter.current_A

        # Store the data
        self.store_data_var('current_A',current)



    @tmpl.with_results(data_vars=['current_A'])
    def process(self):
        """
        Calculate resistance using measured current and voltage source
        setting.
        """

        # Get voltage from current conditions
        Voltage = self.current_conditions['Voltage']

        # Get current measured at the last conditions
        current_A = self.current_results.current_A
        resistance_ohms = Voltage/current_A

        self.store_data_var('resistance_ohms',[resistance_ohms])
```
The *process()* method is called automatically after the *meas_setup()* method if it is present.

The *process()* method uses the *tmpl.with_results* [decorator](https://wiki.python.org/moin/PythonDecorators#What_is_a_Decorator) to ensure that there is always an entry stored called *current_A*. If *process_results()* were to be executed before *meas_sequence()* then an error would be thrown because *current_A* had not been created. The *tmpl.with_results* decorator is not mandatory it is a convenience that avoids having to add boilerplate code such as :

```python
assert 'current_A' in self.ds_results
# use decorator instead: @tmpl.with_results(data_vars=['current_A'])
```
Note also that *tmpl.with_results()* can have a list of names passed to it if more than one value has been measured and stored.


### Test manager

Now that the setup conditions and measurement have been defined, all that remains is to assemble the top level test sequence class. Again this is inherited from an abstract class: *AbstractTestManager*

```python
class SimpleResistanceMeasurement(tmpl.AbstractTestManager):

    def define_setup_conditions(self):
        """
        Add the setup conditions here in the order that they should be set
        """

        # Add setup conditions using class name
        self.add_setup_condition(Voltage)
        

    def define_measurements(self):
        """
        Add measurements here in the order of execution
        """

        # Setup links to all the measurements using class name
        self.add_measurement(CurrentMeasure)
        
```

The test manager requires two methods to be defined:

* *define_setup_conditions()* : This method is a list of calls to *self.add_setup_condition(\<class name>)*. This method takes the name of the SetupConditions class defined previously. In this case there is only one setup condition, Voltage, but if there are multiple SetupConditions classes they are all added here *in the order that they should be set*.
* *define_measurements()* : Similarly measurement classes are added using their class names using the *self.add_measurement(\<class name>)*. Again the order here dictates the order in which the measurements will be executed.

### Running the test

With the classes defined the test can be run by supplying the required resources to the test manager class:

```python
import tmpl

# Make the instrument objects
R = tmpl.examples.ResistorModel(10e3)
vs = tmpl.examples.VoltageSupply(R)
am = tmpl.examples.Ammeter(R)

# Make resources
resources = {'voltage_source':vs, 'ammeter':am}

# Create test manager
test = SimpleResistanceMeasurement(resources)

# Run the test
test.run()
```
the output should look like this:

```
@ SimpleResistanceMeasurement | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
@ SimpleResistanceMeasurement | Running SimpleResistanceMeasurement
@ SimpleResistanceMeasurement | Generating the sequence running order
@ SimpleResistanceMeasurement | 	Running order done
------------------------------------------------------------
@ Voltage                   | Set Voltage source to 3.0V
@ CurrentMeasure            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
@ CurrentMeasure            | Running CurrentMeasure
@ CurrentMeasure            | CurrentMeasure	Time taken: 0.003s 
@ CurrentMeasure            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
@ SimpleResistanceMeasurement | ========================================
@ SimpleResistanceMeasurement | SimpleResistanceMeasurement	Time taken: 0.006s 
@ SimpleResistanceMeasurement | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
```

### Running order

Internally TMPL generates a list of functions to call when the *run()* method is called. It can be useful to view this running order before actually running the test sequence. The property *df_running_order* displays this in a [pandas](https://pandas.pydata.org/pandas-docs/stable/) DataFrame for a convenient tabular printout.

Here's the running order of the resistance measurement example:

```python
>>> test.df_running_order

     Operation           Label  Voltage
0    CONDITION         Voltage      3.0
1  MEASUREMENT  CurrentMeasure      3.0
```
It shows that the test sequence consists of two steps, the first step is a *CONDITION* operation, i.e. setting the voltage. The second step is a *MEASUREMENT*, i.e. reading the Ammeter.

### Results data

The whole point of the TMPL library is to get experimental data into [xarray](http://xarray.pydata.org/en/stable/) Dataset format. Once a test sequence has been run, all the data collected will be available from the test manager object in the property *ds_results*. *ds_results* is an [xarray Dataset](http://xarray.pydata.org/en/stable/user-guide/data-structures.html#dataset) object. Here's the result of the simple resistance measurement:

```python
>>> test.ds_results # Display results from test sequence

<xarray.Dataset>
Dimensions:          (Voltage: 1)
Coordinates:
  * Voltage          (Voltage) float64 3.0
Data variables:
    current_A        (Voltage) float64 0.0002963
    resistance_ohms  (Voltage) float64 1.013e+04

```

The data can be stored and re-loaded in JSON format

```python
# Save to JSON
test.save('my_data.json')

# Load from JSON
test.load('my_data.json')

```
This stores the *ds_results* Dataset into JSON format, which can be loaded back in later. Loading previously measured data can be useful for testing new processing functions.

#### Individual Measurement data

The *ds_results* property of a test manager class, e.g. *test*, scoops up all the data measured in individual measurement class object and puts it into one Dataset. However the individual measurement data can be accessed in the same way. All TMPL class objects have a *ds_results* property and all can be saved and loaded in the same way.

So for the resistor measurement example we can access the data from the measurement class, *CurrentMeasure* like this:

```python
>>> test.meas.CurrentMeasure.ds_results

<xarray.Dataset>
Dimensions:          (Voltage: 1)
Coordinates:
  * Voltage          (Voltage) float64 3.0
Data variables:
    current_A        (Voltage) float64 0.0002963
    resistance_ohms  (Voltage) float64 1.013e+04
```
It looks exactly the same as *test.ds_results*, because *CurrentMeasure* is the only measurement class in this test sequence. It can also be saved and loaded in the same manner.

```python
# Save to JSON
test.meas.CurrentMeasure.save('my_data.json')

# Load from JSON
test.meas.CurrentMeasure.load('my_data.json')

```

### Dataset extra features

TMPL adds some extra features to Datasets for easy storing of the data. It registers a [dataset_accessor](http://xarray.pydata.org/en/stable/internals/extending-xarray.html), which adds the *save* property to the Dataset. The *save* property has several functions for saving the Dataset into different formats as shown here:

```python
# Save Dataset to JSON
test.ds_results.save.to_json(filename)

# Save Dataset to JSON string
jstr = test.ds_results.save.to_json_str()

# Save Dataset to Excel spreadsheet
test.ds_results.save.to_excel(filename)

```

## More advanced example

The simple resistance measurement was good for demonstrating the basic operation of TMPL. Now we will look at a more advanced example. It is still based on measuring the resistance of a resistor but this time we will make the measurement more sophisticated in the following ways.

* Instead of using the setting of the voltage source for the voltage value, we will use a dedicated voltmeter across the resistor.
* Rather than calculating resistance from single values of voltage and resistance we will sweep the voltage and measure the current. We can then fit a line to these measurements and obtain resistance from the slope of the line.
* We also want to measure the resistance variation against environmental conditions so we will put it in a chamber that can vary the temperature and humidity.

Here's a diagram of the new setup:
```
+--------------+
|   voltage    |
|   source     +-----+
|              |     |
+--------------+     |
                     +----------------------+
                     |                      |
           +---------------+         +------+------+
           |         |     |         |             |
 Chamber   |       +-+-+   |         |             |
           |       |   |   |         |             |
    +------+       | R |   |         | Voltmeter   |
    | Temp |       |   |   |         |             |
    +------+       |   |   |         |             |
    | Hum  |       +-+-+   |         +-------+-----+
    +------+         |     |                 |
           +---------------+                 |
                     |                       |
                     +-----------------------+
                     |
                +----+-----+
                | Ammeter  |
                |          |
                +----+-----+
                     |
                     |
                 +-------+ Ground
                   +---+
                    +-+

```

Now our representation in TMPL will be:

* Setup condition:
  - Temperature
  - Humidity
  - Voltage source setpoint
* Measurements:
  - Current (from Ammeter)
  - Voltage across resistor (from Voltmeter)

First we'll need more resources for the new equipment: an environmental chamber for setting temperature and humidity, plus a voltmeter.


```python
resources = {'chamber':chamber_object,
            'voltage_source':voltage_source_object, 
            'ammeter':ammeter_object,
            'voltmeter':voltmeter_object}
```
These will be given to the test manager class.

### Setup conditions

We now need some new setup condition classes for temperature and humidity. These will make use of the chamber *temperature_degC* and *humidity_pc* properties like this:

```python
class Temperature(tmpl.AbstractSetupConditions):

    def initialise(self):
        """
        Initialise default values and any other setup
        """

        # Set default values
        self.values = [25,35,45]

    @property
    def actual(self):
        return self.chamber.temperature_degC

    @property
    def setpoint(self):
        return self.chamber.temperature_setpoint_degC

    @setpoint.setter
    def setpoint(self,value):
        self.chamber.temperature_setpoint_degC = value


class Humidity(tmpl.AbstractSetupConditions):

    def initialise(self):
        """
        Initialise default values and any other setup
        """

        # Set default values
        self.values = [55,85]

    @property
    def actual(self):
        return self.chamber.humidity_degC

    @property
    def setpoint(self):
        return self.chamber.humidity_setpoint_degC

    @setpoint.setter
    def setpoint(self,value):
        self.chamber.humidity_setpoint_degC = value


```

This time we are not going to use the voltage source as a setup condition because we want to sweep the voltage.

### Measurements

The main measurement in this example will be a sweep of the voltage source setpoint. During this sweep the current from the ammeter and the voltage from the voltmeter will be measured. This requires a new measurement class to be created.

```python

class VoltageSweeper(tmpl.AbstractMeasurement):

    def initialise(self):

        # Set up the voltage values to sweep over
        self.config.voltage_sweep = np.linspace(0,1,10)
        

    def meas_sequence(self):
        
        #  Do the measurement
        
        current = np.zeros(self.config.voltage_sweep.shape)
        voltage = np.zeros(self.config.voltage_sweep.shape)

        for index,V in enumerate(self.config.voltage_sweep):
            # Set voltage
            self.voltage_source.voltage_set_V=V

            # Measure current
            current[index] = self.ammeter.current_A

            # Measure voltage across resistor
            voltage[index] = self.voltmeter.voltage_V

        
        # Store the data
        self.store_coords('swp_voltage',self.config.voltage_sweep)
        self.store_data_var('current_A',current,coords=['swp_voltage'])
        self.store_data_var('voltage_diff_V',voltage,coords=['swp_voltage'])


    @tmpl.with_results(data_vars=['current_A','voltage_diff_V'])
    def process(self):

        volts = self.current_results.voltage_diff_V.values
        amps = self.current_results.current_A.values

        # Fit line to amps vs volts, get resistance from slope
        fit_coefficients=np.polyfit(amps,volts,1)
        resistance_ohms = fit_coefficients[0] # slope

        self.store_data_var('resistance_ohms',[resistance_ohms])

```
This measurement is more detailed than the previous example. Measurement classes can have an _initialise()_ method where configuration parameters can be defined. In this case we are defining the voltage values that going to be swept over in the line:

```python
self.config.voltage_sweep = np.linspace(0,1,10)
```
Every TMPL class has a _config_ dictionary that can be used to store any kind of data. It is a special dictionary defined in TMPL called an _ObjDict_, where elements can be added by using the dot notation to assign values. The standard _dict_ way can also be used. We could equally have used:

```python
self.config['voltage_sweep'] = np.linspace(0,1,10)
```
Measurements are just normal classes so you can define your own properties as well. Using the _config_ dict just organises the data under one roof.

The mandatory *meas_setup()* is now a loop that follows the sequence:
* Set voltage
* Measure current
* Measure voltage across resistor

Where the two measurements are stored in 1 dimensional arrays.

The last part of the *meas_setup()* method stores the data. The current and measured voltage are functions of the *voltage_sweep* values. To capture this relationship we need to make *voltage_sweep* into a coordinate in addition to the setup conditions of temperature and humidity. This is done by explicitly storing *voltage_sweep* as a coordinate and indicating in *store_data_vars()* that it is a coordinate.

```python
# Store the data
self.store_coords('swp_voltage',self.config.voltage_sweep)
self.store_data_var('current_A',current,coords=['swp_voltage'])
self.store_data_var('voltage_diff_V',voltage,coords=['swp_voltage'])
```
In order to do this the data being stored must be the same shaped array as the coordinate. In this case everything is a 1D array.

The *process()* method operates just as before. It requires that the current and voltage have been measured. If they have then it will fit a line to the data and get the resistance from the slope of that line.

### Test manager

Now we can assemble the setup condition and measurement into a *TestManager* class. This follows the same pattern as before:

```python
class AdvancedResistanceMeasurement(tmpl.AbstractTestManager):

    def define_setup_conditions(self):
        """
        Add the setup conditions here in the order that they should be set
        """

        # Add setup conditions using class name
        self.add_setup_condition(Temperature)
        self.add_setup_condition(Humidity)

    def define_measurements(self):
        """
        Add measurements here in the order of execution
        """

        # Setup links to all the measurements using class name
        self.add_measurement(VoltageSweeper)
```

Set up the *TestManager* object:

```python
import tmpl

# Setup resources
R = tmpl.examples.ResistorModel(10e3)
vs = tmpl.examples.VoltageSupply(R)
am = tmpl.examples.Ammeter(R)
vm = tmpl.examples.Voltmeter(R)
chamber = tmpl.examples.EnvironmentalChamber(R)
resources = {'voltage_source':vs, 'ammeter':am,'voltmeter':vm,'chamber':chamber}

# Create test manager 
test = AdvancedResistanceMeasurement(resources)
```


We can see the running order again:
```python
>>> test.df_running_order
@ AdvancedResistanceMeasurement | Generating the sequence running order
@ AdvancedResistanceMeasurement | 	Running order done
      Operation           Label  Temperature  Humidity
0     CONDITION     Temperature         25.0       NaN
1     CONDITION        Humidity          NaN      55.0
2   MEASUREMENT  VoltageSweeper         25.0      55.0
3     CONDITION        Humidity          NaN      85.0
4   MEASUREMENT  VoltageSweeper         25.0      85.0
5     CONDITION     Temperature         35.0       NaN
6     CONDITION        Humidity          NaN      55.0
7   MEASUREMENT  VoltageSweeper         35.0      55.0
8     CONDITION        Humidity          NaN      85.0
9   MEASUREMENT  VoltageSweeper         35.0      85.0
10    CONDITION     Temperature         45.0       NaN
11    CONDITION        Humidity          NaN      55.0
12  MEASUREMENT  VoltageSweeper         45.0      55.0
13    CONDITION        Humidity          NaN      85.0
14  MEASUREMENT  VoltageSweeper         45.0      85.0
```

and run the test:

```python
>>> test.run()
Run
@ AdvancedResistanceMeasurement | Generating the sequence running order
@ AdvancedResistanceMeasurement | 	Running order done
@ AdvancedResistanceMeasurement | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
@ AdvancedResistanceMeasurement | Running AdvancedResistanceMeasurement
@ AdvancedResistanceMeasurement | Generating the sequence running order
@ AdvancedResistanceMeasurement | 	Running order done
------------------------------------------------------------
------------------------------------------------------------
@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
@ VoltageSweeper            | Running VoltageSweeper
@ VoltageSweeper            | finished sweep
@ VoltageSweeper            | VoltageSweeper	Time taken: 0.008 s 
@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
------------------------------------------------------------
@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
@ VoltageSweeper            | Running VoltageSweeper
@ VoltageSweeper            | finished sweep
@ VoltageSweeper            | VoltageSweeper	Time taken: 0.008 s 
@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
------------------------------------------------------------
------------------------------------------------------------
@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
@ VoltageSweeper            | Running VoltageSweeper
@ VoltageSweeper            | finished sweep
@ VoltageSweeper            | VoltageSweeper	Time taken: 0.007 s 
@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
------------------------------------------------------------
@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
@ VoltageSweeper            | Running VoltageSweeper
@ VoltageSweeper            | finished sweep
@ VoltageSweeper            | VoltageSweeper	Time taken: 0.008 s 
@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
------------------------------------------------------------
------------------------------------------------------------
@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
@ VoltageSweeper            | Running VoltageSweeper
@ VoltageSweeper            | finished sweep
@ VoltageSweeper            | VoltageSweeper	Time taken: 0.008 s 
@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
------------------------------------------------------------
@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
@ VoltageSweeper            | Running VoltageSweeper
@ VoltageSweeper            | finished sweep
@ VoltageSweeper            | VoltageSweeper	Time taken: 0.007 s 
@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
@ AdvancedResistanceMeasurement | ========================================
@ AdvancedResistanceMeasurement | AdvancedResistanceMeasurement	Time taken: 0.049 s 
@ AdvancedResistanceMeasurement | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

```

## Installing for development

TMPL can be installed locally by cloning the repository to a local folder and then installing with pip

```bash
cd <local_path>
git clone https://github.com/redlegjed/test_measure_process_lib.git
pip install -e <local_path>/test_measure_process_lib

```

The code can then be edited from *<local_path>/test_measure_process_lib*. Changes will be included when importing *tmpl* into a new python instance.

            

Raw data

            {
    "_id": null,
    "home_page": "",
    "name": "test-measure-process-lib",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": "",
    "keywords": "lab,test,measurement,scientific,engineering",
    "author": "",
    "author_email": "RedLegJed <rlj_pypi@nym.hush.com>",
    "download_url": "https://files.pythonhosted.org/packages/4c/cb/7a37720832bfa748e5df6aff7edb3ec81abd8c3135c4b4d79271e95a1765/test_measure_process_lib-1.0.9.tar.gz",
    "platform": null,
    "description": "# Test, Measure, Process library (TMPL)\n\nTMPL is a library for writing lab test measurement code in a modular and reusable way.\n\nLab tests are broken up into lightweight classes that represent setup conditions and measurements. These conditions and measurements can be combined together to make test sequences. TMPL provides infrastructure to execute the conditions and measurements in a specific order and log the data into a convenient format.\n\nAlthough written for use in lab measurements TMPL makes no assumptions about test equipment setup and is very generic. It could be used to make test sequences based on models or data analysis instead of actual measurements.\n\nTMPL is built around storing data in the [xarray](http://xarray.pydata.org/en/stable/) _Dataset_ class. This provides a convenient data structure for storing multi-dimensional data that can be easily visualised using libraries like [Holoviews](http://holoviews.org/index.html) or its offshoot [hvplot](https://hvplot.holoviz.org/index.html).\n\n## Dependencies and installation\nTMPL depends on these libraries. \n\n* [xarray](http://xarray.pydata.org/en/stable/)\n* [pandas](https://pandas.pydata.org/pandas-docs/stable/)\n* [xlsxwriter](https://xlsxwriter.readthedocs.io/)\n\nTMPL can be installed via *pip*\n\n```bash\npip install test-measure-process-lib\n```\n\nNote that this does not install the dependencies. This is in case another package manager, e.g. Anaconda, is being used. So they have to be manually installed.\n\n## Documentation\nThis file gives basic descriptions of how to use TMPL, for more details consult [the full documentation](https://redlegjed.github.io/test_measure_process_lib/)\n\n\n## Core classes\n\nTMPL is built on a set of core classes. These are built by inheriting from the Abstract classes defined in *tmpl_core.py*. The classes are:\n\n* _TestManager_ classes: Based on _AbstractTestManager_ class. These classes run sequences of measurements over multiple setup conditions e.g. temperature, humidity etc. The _TestManagers_ are ultimately responsible for gathering up all data recorded during the sequence and packaging it into one xarray Dataset object.\n* _Measurement_ classes: Based on _AbstractMeasurement_ class. These are the classes that actually perform the measurements. They are classes that can be run independently or in a sequence from a _TestManager_. The data that they collect and process is stored in an internal xarray Dataset object. When run from a _TestManager_ this dataset will be scooped up at the end of the sequence and added into the overall Dataset maintained by the _TestManager_.\n* _SetupCondition_ classes: Based on _AbstractSetupCondition_. These classes are responsible for setting up experimental conditions e.g. temperature, pressure, supply voltage etc. They are small classes that have only one purpose, to set a specific condition. _TestManagers_ use _SetupCondition_ classes to set conditions during a sequence before running measurements. \n\n\n## Example usage\n\nBefore explaining the inner workings of TMPL this section runs through a hypothetical example to show how the classes are used at the top level.\n\nSuppose a _TestManager_ has been defined that has objects for setting *temperature and pressure*. It also has three measurement objects defined called *calibrate_scales, MeasureVolume and MeasureMass*.\n\nThe _TestManager_ is initialised with any _resources_ that the measurement classes require, e.g. instruments.\n\n```python\n# Define test equipment objects for measurements to use\n# - can be anything that Measurement classes require\nresources = {'chamber':chamber_object,\n            'test_sample':test_sample_object,\n            'instrument':instr_object}\n\n# Create test manager object\ntest = TestManager_mymeas(resources)\n```\n\nSetup conditions under which test is to be run can be defined by accessing the _SetupConditions_ objects directly through the _conditions_ property.\n\n```python\ntest.conditions.temperature.values = [25,40,75]\ntest.conditions.pressure.values = [12,15,65]\n```\n\nThe measurements to run during the sequence can be enabled/disabled by direct access to the _Measurement_ objects through the _meas_ property.\n\n```python\ntest.meas.calibrate_scales.enable = False\ntest.meas.MeasureVolume.enable = True\ntest.meas.MeasureMass.enable = True\n```\n\nNow the sequence has been configured we can run the test over all setup conditions\n\n```python\ntest.run()\n```\n\nOnce the test sequence is finished we can get the results as an *xarray Dataset*.\n\n```python\ntest.ds_results\n```\n\nCan also get individual results from a _Measurement_ object directly.\n\n```python\ntest.meas.MeasureVolume.ds_results\n```\n\nIndividual measurements can be run with specific conditions independent of the _TestManager_.\n\n```python\nconditions = dict(temperature_degC=34,pressure_nm=15)\ntest.meas.MeasureVolume.run(conditions)\n```\n\nOr measurements can be run without specifiying conditions\n\n```python\ntest.meas.MeasureVolume.run()\n```\n\nNote in the last two cases where the measurement is run individually, specifiying the conditions merely includes the conditions as coordinates in the results Dataset. _Measurement_ classes _do not_ set their own conditions, that is only done by _SetupConditions_ classes.\n\n## Creating a measurement sequence\n\nA measurement sequence consists of a _TestManager_ class to run the overall sequence, any number of _SetupConditions_ classes and any number of _Measurement_ classes.\n\nLet's take a simple example, the measurement of resistance of a resistor.\n\n```\n+--------------+\n|   voltage    |\n|   source     +-----+\n|              |     |\n+--------------+     |\n                     |\n                     |\n                   +-+-+\n                   |   |\n                   |   |\n                   | R |    Resistor to measure\n                   |   |\n                   |   |\n                   |   |\n                   +-+-+\n                     |\n                     |\n                +----+-----+\n                |          |\n                | Ammeter  |\n                |          |\n                +----+-----+\n                     |\n                     |\n                 --------- Ground\n                   -----\n                    ---\n\n```\nWe have two pieces of test equipment in this measurement: the voltage source and the ammeter. The measurement is simply to set a voltage, measure the current, and calculate the resistance from Ohm's law :\n\nVoltage = Resistance x Current\n\nIn this measurement we have one setup condition, voltage, one measurement, current and one processing step, resistance.\n\nLet's assume that the voltage source and ammeter are controlled through the objects *voltage_source* and *ammeter*. These two objects will be supplied as _resources_ to the _TestManager_, _SetupConditions_ and _Measurement_ classes e.g.\n\n```python\nresources = {'voltage_source':voltage_source, 'ammeter':ammeter}\n```\nAll classes will automatically have the *voltage_source* and *ammeter* objects available as properties.\n\n### Setup conditions\n\nFirst we'll setup the voltage source. This is our only setup condition and it will be done using a _SetupConditions_ class. _SetupConditions_ classes inherit from the abstract class _AbstractSetupConditions_. They require one method and two properties to be defined:\n\n* _initialise_ : Perform any initialisation, usually setting defaults for the property _values_.\n* _setpoint_ : Property that is used to set/get the condition set point value\n* _actual_ : Property that returns the actual value of the condition, e.g. the actual voltage rather than the setpoint\n\nThe complete class definition is shown here:\n\n```python\nclass Voltage(tmpl.AbstractSetupConditions):\n\n    def initialise(self):\n        \"\"\"\n        Initialise default values and any other setup\n        \"\"\"\n\n        # Set default values\n        self.values = [3.0]\n\n        \n    @property\n    def actual(self):\n        \"\"\"\n        Return actual measured voltage\n        \"\"\"\n        return self.voltage_source.actual_voltage_V\n\n    @property\n    def setpoint(self):\n        \"\"\"\n        Get/Set the output voltage of the voltage source\n        \"\"\"\n        return self.voltage_source.voltage_set_V\n\n    @setpoint.setter\n    def setpoint(self,value):\n        self.log(f'Set Voltage source to {value}V') # printout\n        self.voltage_source.voltage_set_V = value\n        \n```\n\n### Measurements\n\nNext the central measurement class is defined. Measurement classes inherit from the _AbstractMeasurement_ class. The only method that _needs_ to be defined is *meas_sequence()*. This is generally the top level function of a specific measurement procedure. Any number of extra methods can be added to the class to support *meas_sequence()*, but when a measurement is executed it basically calls the *meas_sequence()* method.\n\nIn this case the measurement is simply to read the ammeter and store the reading, which can be done in the *meas_sequence()*. The resistance, however, is derived from the ammeter reading and the setpoint of the voltage source. Since this is \"processing\" rather than measurement it is good practice to do this in another method. This ensures that the real measurement, the ammeter reading, is done even if the processing step crashes. In this case the processing function could be re-run later to debug it, without re-running the measurement.\n\n```python\nclass CurrentMeasure(tmpl.AbstractMeasurement):\n           \n\n    def meas_sequence(self):\n        \"\"\"\n        Mandatory method for Measurement classes\n\n        Performs the actual measurement and stores data.\n        \"\"\"\n        #  Measure current with ammeter\n        current = self.ammeter.current_A\n\n        # Store the data\n        self.store_data_var('current_A',current)\n\n\n\n    @tmpl.with_results(data_vars=['current_A'])\n    def process(self):\n        \"\"\"\n        Calculate resistance using measured current and voltage source\n        setting.\n        \"\"\"\n\n        # Get voltage from current conditions\n        Voltage = self.current_conditions['Voltage']\n\n        # Get current measured at the last conditions\n        current_A = self.current_results.current_A\n        resistance_ohms = Voltage/current_A\n\n        self.store_data_var('resistance_ohms',[resistance_ohms])\n```\nThe *process()* method is called automatically after the *meas_setup()* method if it is present.\n\nThe *process()* method uses the *tmpl.with_results* [decorator](https://wiki.python.org/moin/PythonDecorators#What_is_a_Decorator) to ensure that there is always an entry stored called *current_A*. If *process_results()* were to be executed before *meas_sequence()* then an error would be thrown because *current_A* had not been created. The *tmpl.with_results* decorator is not mandatory it is a convenience that avoids having to add boilerplate code such as :\n\n```python\nassert 'current_A' in self.ds_results\n# use decorator instead: @tmpl.with_results(data_vars=['current_A'])\n```\nNote also that *tmpl.with_results()* can have a list of names passed to it if more than one value has been measured and stored.\n\n\n### Test manager\n\nNow that the setup conditions and measurement have been defined, all that remains is to assemble the top level test sequence class. Again this is inherited from an abstract class: *AbstractTestManager*\n\n```python\nclass SimpleResistanceMeasurement(tmpl.AbstractTestManager):\n\n    def define_setup_conditions(self):\n        \"\"\"\n        Add the setup conditions here in the order that they should be set\n        \"\"\"\n\n        # Add setup conditions using class name\n        self.add_setup_condition(Voltage)\n        \n\n    def define_measurements(self):\n        \"\"\"\n        Add measurements here in the order of execution\n        \"\"\"\n\n        # Setup links to all the measurements using class name\n        self.add_measurement(CurrentMeasure)\n        \n```\n\nThe test manager requires two methods to be defined:\n\n* *define_setup_conditions()* : This method is a list of calls to *self.add_setup_condition(\\<class name>)*. This method takes the name of the SetupConditions class defined previously. In this case there is only one setup condition, Voltage, but if there are multiple SetupConditions classes they are all added here *in the order that they should be set*.\n* *define_measurements()* : Similarly measurement classes are added using their class names using the *self.add_measurement(\\<class name>)*. Again the order here dictates the order in which the measurements will be executed.\n\n### Running the test\n\nWith the classes defined the test can be run by supplying the required resources to the test manager class:\n\n```python\nimport tmpl\n\n# Make the instrument objects\nR = tmpl.examples.ResistorModel(10e3)\nvs = tmpl.examples.VoltageSupply(R)\nam = tmpl.examples.Ammeter(R)\n\n# Make resources\nresources = {'voltage_source':vs, 'ammeter':am}\n\n# Create test manager\ntest = SimpleResistanceMeasurement(resources)\n\n# Run the test\ntest.run()\n```\nthe output should look like this:\n\n```\n@ SimpleResistanceMeasurement | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n@ SimpleResistanceMeasurement | Running SimpleResistanceMeasurement\n@ SimpleResistanceMeasurement | Generating the sequence running order\n@ SimpleResistanceMeasurement | \tRunning order done\n------------------------------------------------------------\n@ Voltage                   | Set Voltage source to 3.0V\n@ CurrentMeasure            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n@ CurrentMeasure            | Running CurrentMeasure\n@ CurrentMeasure            | CurrentMeasure\tTime taken: 0.003s \n@ CurrentMeasure            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n@ SimpleResistanceMeasurement | ========================================\n@ SimpleResistanceMeasurement | SimpleResistanceMeasurement\tTime taken: 0.006s \n@ SimpleResistanceMeasurement | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n```\n\n### Running order\n\nInternally TMPL generates a list of functions to call when the *run()* method is called. It can be useful to view this running order before actually running the test sequence. The property *df_running_order* displays this in a [pandas](https://pandas.pydata.org/pandas-docs/stable/) DataFrame for a convenient tabular printout.\n\nHere's the running order of the resistance measurement example:\n\n```python\n>>> test.df_running_order\n\n     Operation           Label  Voltage\n0    CONDITION         Voltage      3.0\n1  MEASUREMENT  CurrentMeasure      3.0\n```\nIt shows that the test sequence consists of two steps, the first step is a *CONDITION* operation, i.e. setting the voltage. The second step is a *MEASUREMENT*, i.e. reading the Ammeter.\n\n### Results data\n\nThe whole point of the TMPL library is to get experimental data into [xarray](http://xarray.pydata.org/en/stable/) Dataset format. Once a test sequence has been run, all the data collected will be available from the test manager object in the property *ds_results*. *ds_results* is an [xarray Dataset](http://xarray.pydata.org/en/stable/user-guide/data-structures.html#dataset) object. Here's the result of the simple resistance measurement:\n\n```python\n>>> test.ds_results # Display results from test sequence\n\n<xarray.Dataset>\nDimensions:          (Voltage: 1)\nCoordinates:\n  * Voltage          (Voltage) float64 3.0\nData variables:\n    current_A        (Voltage) float64 0.0002963\n    resistance_ohms  (Voltage) float64 1.013e+04\n\n```\n\nThe data can be stored and re-loaded in JSON format\n\n```python\n# Save to JSON\ntest.save('my_data.json')\n\n# Load from JSON\ntest.load('my_data.json')\n\n```\nThis stores the *ds_results* Dataset into JSON format, which can be loaded back in later. Loading previously measured data can be useful for testing new processing functions.\n\n#### Individual Measurement data\n\nThe *ds_results* property of a test manager class, e.g. *test*, scoops up all the data measured in individual measurement class object and puts it into one Dataset. However the individual measurement data can be accessed in the same way. All TMPL class objects have a *ds_results* property and all can be saved and loaded in the same way.\n\nSo for the resistor measurement example we can access the data from the measurement class, *CurrentMeasure* like this:\n\n```python\n>>> test.meas.CurrentMeasure.ds_results\n\n<xarray.Dataset>\nDimensions:          (Voltage: 1)\nCoordinates:\n  * Voltage          (Voltage) float64 3.0\nData variables:\n    current_A        (Voltage) float64 0.0002963\n    resistance_ohms  (Voltage) float64 1.013e+04\n```\nIt looks exactly the same as *test.ds_results*, because *CurrentMeasure* is the only measurement class in this test sequence. It can also be saved and loaded in the same manner.\n\n```python\n# Save to JSON\ntest.meas.CurrentMeasure.save('my_data.json')\n\n# Load from JSON\ntest.meas.CurrentMeasure.load('my_data.json')\n\n```\n\n### Dataset extra features\n\nTMPL adds some extra features to Datasets for easy storing of the data. It registers a [dataset_accessor](http://xarray.pydata.org/en/stable/internals/extending-xarray.html), which adds the *save* property to the Dataset. The *save* property has several functions for saving the Dataset into different formats as shown here:\n\n```python\n# Save Dataset to JSON\ntest.ds_results.save.to_json(filename)\n\n# Save Dataset to JSON string\njstr = test.ds_results.save.to_json_str()\n\n# Save Dataset to Excel spreadsheet\ntest.ds_results.save.to_excel(filename)\n\n```\n\n## More advanced example\n\nThe simple resistance measurement was good for demonstrating the basic operation of TMPL. Now we will look at a more advanced example. It is still based on measuring the resistance of a resistor but this time we will make the measurement more sophisticated in the following ways.\n\n* Instead of using the setting of the voltage source for the voltage value, we will use a dedicated voltmeter across the resistor.\n* Rather than calculating resistance from single values of voltage and resistance we will sweep the voltage and measure the current. We can then fit a line to these measurements and obtain resistance from the slope of the line.\n* We also want to measure the resistance variation against environmental conditions so we will put it in a chamber that can vary the temperature and humidity.\n\nHere's a diagram of the new setup:\n```\n+--------------+\n|   voltage    |\n|   source     +-----+\n|              |     |\n+--------------+     |\n                     +----------------------+\n                     |                      |\n           +---------------+         +------+------+\n           |         |     |         |             |\n Chamber   |       +-+-+   |         |             |\n           |       |   |   |         |             |\n    +------+       | R |   |         | Voltmeter   |\n    | Temp |       |   |   |         |             |\n    +------+       |   |   |         |             |\n    | Hum  |       +-+-+   |         +-------+-----+\n    +------+         |     |                 |\n           +---------------+                 |\n                     |                       |\n                     +-----------------------+\n                     |\n                +----+-----+\n                | Ammeter  |\n                |          |\n                +----+-----+\n                     |\n                     |\n                 +-------+ Ground\n                   +---+\n                    +-+\n\n```\n\nNow our representation in TMPL will be:\n\n* Setup condition:\n  - Temperature\n  - Humidity\n  - Voltage source setpoint\n* Measurements:\n  - Current (from Ammeter)\n  - Voltage across resistor (from Voltmeter)\n\nFirst we'll need more resources for the new equipment: an environmental chamber for setting temperature and humidity, plus a voltmeter.\n\n\n```python\nresources = {'chamber':chamber_object,\n            'voltage_source':voltage_source_object, \n            'ammeter':ammeter_object,\n            'voltmeter':voltmeter_object}\n```\nThese will be given to the test manager class.\n\n### Setup conditions\n\nWe now need some new setup condition classes for temperature and humidity. These will make use of the chamber *temperature_degC* and *humidity_pc* properties like this:\n\n```python\nclass Temperature(tmpl.AbstractSetupConditions):\n\n    def initialise(self):\n        \"\"\"\n        Initialise default values and any other setup\n        \"\"\"\n\n        # Set default values\n        self.values = [25,35,45]\n\n    @property\n    def actual(self):\n        return self.chamber.temperature_degC\n\n    @property\n    def setpoint(self):\n        return self.chamber.temperature_setpoint_degC\n\n    @setpoint.setter\n    def setpoint(self,value):\n        self.chamber.temperature_setpoint_degC = value\n\n\nclass Humidity(tmpl.AbstractSetupConditions):\n\n    def initialise(self):\n        \"\"\"\n        Initialise default values and any other setup\n        \"\"\"\n\n        # Set default values\n        self.values = [55,85]\n\n    @property\n    def actual(self):\n        return self.chamber.humidity_degC\n\n    @property\n    def setpoint(self):\n        return self.chamber.humidity_setpoint_degC\n\n    @setpoint.setter\n    def setpoint(self,value):\n        self.chamber.humidity_setpoint_degC = value\n\n\n```\n\nThis time we are not going to use the voltage source as a setup condition because we want to sweep the voltage.\n\n### Measurements\n\nThe main measurement in this example will be a sweep of the voltage source setpoint. During this sweep the current from the ammeter and the voltage from the voltmeter will be measured. This requires a new measurement class to be created.\n\n```python\n\nclass VoltageSweeper(tmpl.AbstractMeasurement):\n\n    def initialise(self):\n\n        # Set up the voltage values to sweep over\n        self.config.voltage_sweep = np.linspace(0,1,10)\n        \n\n    def meas_sequence(self):\n        \n        #  Do the measurement\n        \n        current = np.zeros(self.config.voltage_sweep.shape)\n        voltage = np.zeros(self.config.voltage_sweep.shape)\n\n        for index,V in enumerate(self.config.voltage_sweep):\n            # Set voltage\n            self.voltage_source.voltage_set_V=V\n\n            # Measure current\n            current[index] = self.ammeter.current_A\n\n            # Measure voltage across resistor\n            voltage[index] = self.voltmeter.voltage_V\n\n        \n        # Store the data\n        self.store_coords('swp_voltage',self.config.voltage_sweep)\n        self.store_data_var('current_A',current,coords=['swp_voltage'])\n        self.store_data_var('voltage_diff_V',voltage,coords=['swp_voltage'])\n\n\n    @tmpl.with_results(data_vars=['current_A','voltage_diff_V'])\n    def process(self):\n\n        volts = self.current_results.voltage_diff_V.values\n        amps = self.current_results.current_A.values\n\n        # Fit line to amps vs volts, get resistance from slope\n        fit_coefficients=np.polyfit(amps,volts,1)\n        resistance_ohms = fit_coefficients[0] # slope\n\n        self.store_data_var('resistance_ohms',[resistance_ohms])\n\n```\nThis measurement is more detailed than the previous example. Measurement classes can have an _initialise()_ method where configuration parameters can be defined. In this case we are defining the voltage values that going to be swept over in the line:\n\n```python\nself.config.voltage_sweep = np.linspace(0,1,10)\n```\nEvery TMPL class has a _config_ dictionary that can be used to store any kind of data. It is a special dictionary defined in TMPL called an _ObjDict_, where elements can be added by using the dot notation to assign values. The standard _dict_ way can also be used. We could equally have used:\n\n```python\nself.config['voltage_sweep'] = np.linspace(0,1,10)\n```\nMeasurements are just normal classes so you can define your own properties as well. Using the _config_ dict just organises the data under one roof.\n\nThe mandatory *meas_setup()* is now a loop that follows the sequence:\n* Set voltage\n* Measure current\n* Measure voltage across resistor\n\nWhere the two measurements are stored in 1 dimensional arrays.\n\nThe last part of the *meas_setup()* method stores the data. The current and measured voltage are functions of the *voltage_sweep* values. To capture this relationship we need to make *voltage_sweep* into a coordinate in addition to the setup conditions of temperature and humidity. This is done by explicitly storing *voltage_sweep* as a coordinate and indicating in *store_data_vars()* that it is a coordinate.\n\n```python\n# Store the data\nself.store_coords('swp_voltage',self.config.voltage_sweep)\nself.store_data_var('current_A',current,coords=['swp_voltage'])\nself.store_data_var('voltage_diff_V',voltage,coords=['swp_voltage'])\n```\nIn order to do this the data being stored must be the same shaped array as the coordinate. In this case everything is a 1D array.\n\nThe *process()* method operates just as before. It requires that the current and voltage have been measured. If they have then it will fit a line to the data and get the resistance from the slope of that line.\n\n### Test manager\n\nNow we can assemble the setup condition and measurement into a *TestManager* class. This follows the same pattern as before:\n\n```python\nclass AdvancedResistanceMeasurement(tmpl.AbstractTestManager):\n\n    def define_setup_conditions(self):\n        \"\"\"\n        Add the setup conditions here in the order that they should be set\n        \"\"\"\n\n        # Add setup conditions using class name\n        self.add_setup_condition(Temperature)\n        self.add_setup_condition(Humidity)\n\n    def define_measurements(self):\n        \"\"\"\n        Add measurements here in the order of execution\n        \"\"\"\n\n        # Setup links to all the measurements using class name\n        self.add_measurement(VoltageSweeper)\n```\n\nSet up the *TestManager* object:\n\n```python\nimport tmpl\n\n# Setup resources\nR = tmpl.examples.ResistorModel(10e3)\nvs = tmpl.examples.VoltageSupply(R)\nam = tmpl.examples.Ammeter(R)\nvm = tmpl.examples.Voltmeter(R)\nchamber = tmpl.examples.EnvironmentalChamber(R)\nresources = {'voltage_source':vs, 'ammeter':am,'voltmeter':vm,'chamber':chamber}\n\n# Create test manager \ntest = AdvancedResistanceMeasurement(resources)\n```\n\n\nWe can see the running order again:\n```python\n>>> test.df_running_order\n@ AdvancedResistanceMeasurement | Generating the sequence running order\n@ AdvancedResistanceMeasurement | \tRunning order done\n      Operation           Label  Temperature  Humidity\n0     CONDITION     Temperature         25.0       NaN\n1     CONDITION        Humidity          NaN      55.0\n2   MEASUREMENT  VoltageSweeper         25.0      55.0\n3     CONDITION        Humidity          NaN      85.0\n4   MEASUREMENT  VoltageSweeper         25.0      85.0\n5     CONDITION     Temperature         35.0       NaN\n6     CONDITION        Humidity          NaN      55.0\n7   MEASUREMENT  VoltageSweeper         35.0      55.0\n8     CONDITION        Humidity          NaN      85.0\n9   MEASUREMENT  VoltageSweeper         35.0      85.0\n10    CONDITION     Temperature         45.0       NaN\n11    CONDITION        Humidity          NaN      55.0\n12  MEASUREMENT  VoltageSweeper         45.0      55.0\n13    CONDITION        Humidity          NaN      85.0\n14  MEASUREMENT  VoltageSweeper         45.0      85.0\n```\n\nand run the test:\n\n```python\n>>> test.run()\nRun\n@ AdvancedResistanceMeasurement | Generating the sequence running order\n@ AdvancedResistanceMeasurement | \tRunning order done\n@ AdvancedResistanceMeasurement | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n@ AdvancedResistanceMeasurement | Running AdvancedResistanceMeasurement\n@ AdvancedResistanceMeasurement | Generating the sequence running order\n@ AdvancedResistanceMeasurement | \tRunning order done\n------------------------------------------------------------\n------------------------------------------------------------\n@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n@ VoltageSweeper            | Running VoltageSweeper\n@ VoltageSweeper            | finished sweep\n@ VoltageSweeper            | VoltageSweeper\tTime taken: 0.008 s \n@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n------------------------------------------------------------\n@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n@ VoltageSweeper            | Running VoltageSweeper\n@ VoltageSweeper            | finished sweep\n@ VoltageSweeper            | VoltageSweeper\tTime taken: 0.008 s \n@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n------------------------------------------------------------\n------------------------------------------------------------\n@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n@ VoltageSweeper            | Running VoltageSweeper\n@ VoltageSweeper            | finished sweep\n@ VoltageSweeper            | VoltageSweeper\tTime taken: 0.007 s \n@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n------------------------------------------------------------\n@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n@ VoltageSweeper            | Running VoltageSweeper\n@ VoltageSweeper            | finished sweep\n@ VoltageSweeper            | VoltageSweeper\tTime taken: 0.008 s \n@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n------------------------------------------------------------\n------------------------------------------------------------\n@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n@ VoltageSweeper            | Running VoltageSweeper\n@ VoltageSweeper            | finished sweep\n@ VoltageSweeper            | VoltageSweeper\tTime taken: 0.008 s \n@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n------------------------------------------------------------\n@ VoltageSweeper            | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n@ VoltageSweeper            | Running VoltageSweeper\n@ VoltageSweeper            | finished sweep\n@ VoltageSweeper            | VoltageSweeper\tTime taken: 0.007 s \n@ VoltageSweeper            | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n@ AdvancedResistanceMeasurement | ========================================\n@ AdvancedResistanceMeasurement | AdvancedResistanceMeasurement\tTime taken: 0.049 s \n@ AdvancedResistanceMeasurement | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n```\n\n## Installing for development\n\nTMPL can be installed locally by cloning the repository to a local folder and then installing with pip\n\n```bash\ncd <local_path>\ngit clone https://github.com/redlegjed/test_measure_process_lib.git\npip install -e <local_path>/test_measure_process_lib\n\n```\n\nThe code can then be edited from *<local_path>/test_measure_process_lib*. Changes will be included when importing *tmpl* into a new python instance.\n",
    "bugtrack_url": null,
    "license": "Apache License Version 2.0, January 2004 http://www.apache.org/licenses/  TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION  1. Definitions.  \"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.  \"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.  \"Legal Entity\" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.  \"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.  \"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.  \"Object\" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.  \"Work\" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).  \"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.  \"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"  \"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.  2. Grant of Copyright License.  Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.  3. Grant of Patent License.  Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.  4. Redistribution.  You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:  You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a \"NOTICE\" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.  5. Submission of Contributions.  Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.  6. Trademarks.  This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.  7. Disclaimer of Warranty.  Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.  8. Limitation of Liability.  In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.  9. Accepting Warranty or Additional Liability.  While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.  END OF TERMS AND CONDITIONS  APPENDIX: How to apply the Apache License to your work  To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets \"[]\" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \"printed page\" as the copyright notice for easier identification within third-party archives.  Copyright [yyyy] [name of copyright owner]  Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at  http://www.apache.org/licenses/LICENSE-2.0  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ",
    "summary": "Test, Measure and Process library. Framework for lab experiments.",
    "version": "1.0.9",
    "project_urls": {
        "Github": "https://github.com/redlegjed/test_measure_process_lib",
        "Homepage": "https://redlegjed.github.io/test_measure_process_lib/"
    },
    "split_keywords": [
        "lab",
        "test",
        "measurement",
        "scientific",
        "engineering"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "319292bdd5f9b9fad0574f3f4d165ff4489659f3fa152e64dad8d492d9c01da0",
                "md5": "02260be6db21a7ae6bd807ad3cc30d41",
                "sha256": "b9e0b2ddcf4e6ab0ae2c3b8eb556f67717e10e60ef229d216fb321695d9fc390"
            },
            "downloads": -1,
            "filename": "test_measure_process_lib-1.0.9-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "02260be6db21a7ae6bd807ad3cc30d41",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7",
            "size": 51929,
            "upload_time": "2023-07-03T21:42:09",
            "upload_time_iso_8601": "2023-07-03T21:42:09.730561Z",
            "url": "https://files.pythonhosted.org/packages/31/92/92bdd5f9b9fad0574f3f4d165ff4489659f3fa152e64dad8d492d9c01da0/test_measure_process_lib-1.0.9-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "4ccb7a37720832bfa748e5df6aff7edb3ec81abd8c3135c4b4d79271e95a1765",
                "md5": "6d2a52785ccbeece44d363f6c2c9ba25",
                "sha256": "2156d9412dfd62d2a13a14887089a045c48a22cc125bd2e32246a7d71d60a3eb"
            },
            "downloads": -1,
            "filename": "test_measure_process_lib-1.0.9.tar.gz",
            "has_sig": false,
            "md5_digest": "6d2a52785ccbeece44d363f6c2c9ba25",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 56332,
            "upload_time": "2023-07-03T21:42:13",
            "upload_time_iso_8601": "2023-07-03T21:42:13.854537Z",
            "url": "https://files.pythonhosted.org/packages/4c/cb/7a37720832bfa748e5df6aff7edb3ec81abd8c3135c4b4d79271e95a1765/test_measure_process_lib-1.0.9.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-07-03 21:42:13",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "redlegjed",
    "github_project": "test_measure_process_lib",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "test-measure-process-lib"
}
        
Elapsed time: 0.09023s