PyMeasure-0.9.0/0000775000175000017500000000000014010046235013661 5ustar colincolin00000000000000PyMeasure-0.9.0/tests/0000775000175000017500000000000014010046235015023 5ustar colincolin00000000000000PyMeasure-0.9.0/tests/test_process.py0000664000175000017500000000274614010037617020127 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.process import StoppableProcess def test_process_stopping(): process = StoppableProcess() process.start() process.stop() assert process.should_stop() is True process.join() def test__process_joining(): process = StoppableProcess() process.start() process.join() assert process.should_stop() is True PyMeasure-0.9.0/tests/instruments/0000775000175000017500000000000014010046235017416 5ustar colincolin00000000000000PyMeasure-0.9.0/tests/instruments/keithley/0000775000175000017500000000000014010046235021234 5ustar colincolin00000000000000PyMeasure-0.9.0/tests/instruments/keithley/test_keithley2750.py0000664000175000017500000000367614010037617025021 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest from pymeasure.instruments.keithley.keithley2750 import clean_closed_channels def test_clean_closed_channels(): # Example outputs from `self.ask(":ROUTe:CLOSe?")` example_outputs = ["(@)", # if no channels are open. Note that it's a string and not a list "(@101)", # if only 1 channel is open. Note that it's a string and not a list ["(@101", "105)"], # if only 2 channels are open ["(@101", 102.0, 103.0, 104.0, "105)"]] # if more than 2 channels are open assert clean_closed_channels(example_outputs[0]) == [] assert clean_closed_channels(example_outputs[1]) == [101] assert clean_closed_channels(example_outputs[2]) == [101, 105] assert clean_closed_channels(example_outputs[3]) == [101, 102, 103, 104, 105] PyMeasure-0.9.0/tests/instruments/keysight/0000775000175000017500000000000014010046235021245 5ustar colincolin00000000000000PyMeasure-0.9.0/tests/instruments/keysight/test_keysightDSOX1102G.py0000664000175000017500000003337314010037617025633 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from time import sleep import logging import pytest import numpy as np from pymeasure.instruments.keysight.keysightDSOX1102G import KeysightDSOX1102G from pyvisa.errors import VisaIOError pytest.skip('Only work with connected hardware', allow_module_level=True) class TestKeysightDSOX1102G: """ Unit tests for KeysightDSOX1102G class. This test suite, needs the following setup to work properly: - A KeysightDSOX1102G device should be connected to the computer; - The device's address must be set in the RESOURCE constant; - A probe on Channel 1 must be connected to the Demo output of the oscilloscope. """ ################################################## # KeysightDSOX1102G device address goes here: RESOURCE = "USB0::10893::6039::CN57266430::INSTR" ################################################## ######################### # PARAMETRIZATION CASES # ######################### BOOLEANS = [False, True] CHANNEL_COUPLINGS = ["ac", "dc"] CHANNEL_LABELS = [["label", "LABEL"], ["quite long label", "QUITE LONG"], [12345, "12345"]] TIMEBASE_MODES = ["main", "window", "xy", "roll"] ACQUISITION_TYPES = ["normal", "average", "hresolution", "peak"] ACQUISITION_MODES = ["realtime", "segmented"] DIGITIZE_SOURCES = ["channel1", "channel2", "function", "math", "fft", "abus", "ext"] WAVEFORM_POINTS_MODES = ["normal", "maximum", "raw"] WAVEFORM_POINTS = [100, 250, 500, 1000, 2000, 5000, 10000, 20000, 50000, 62500] WAVEFORM_SOURCES = ["channel1", "channel2", "function", "fft", "wmemory1", "wmemory2", "ext"] WAVEFORM_FORMATS = ["ascii", "word", "byte"] DOWNLOAD_SOURCES = ["channel1", "channel2", "function", "fft", "ext"] CHANNELS = [1, 2] SCOPE = KeysightDSOX1102G(RESOURCE) ############ # FIXTURES # ############ @pytest.fixture def make_reseted_cleared_scope(self): self.SCOPE.reset() self.SCOPE.clear_status() return self.SCOPE ######### # TESTS # ######### def test_scope_connection(self, make_reseted_cleared_scope): bad_resource = "USB0::10893::45848::MY12345678::0::INSTR" # The pure python VISA library (pyvisa-py) raises a ValueError while the PyVISA library raises a VisaIOError. with pytest.raises((ValueError, VisaIOError)): scope = KeysightDSOX1102G(bad_resource) def test_autoscale(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope scope.write(":timebase:position 1") # Autoscale should turn off the zoomed (delayed) time mode assert scope.ask(":timebase:position?") == "+1.000000000000E+00\n" scope.autoscale() assert scope.ask(":timebase:position?") != "+1.000000000000E+00\n" # Channel def test_ch_current_configuration(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope expected = {"OFFS": 0.0, "COUP": "DC", "IMP": "ONEM", "DISP": True, "BWL": False, "INV": False, "UNIT": "VOLT", "PROB": 10.0, "PROB:SKEW": 0.0, "STYP": "SING", "CHAN": 1, "RANG": 40.0} actual = scope.ch(1).current_configuration assert actual == expected @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_ch_bwlimit(self, make_reseted_cleared_scope, ch_number, case): scope = make_reseted_cleared_scope scope.ch(ch_number).bwlimit = case assert scope.ch(ch_number).bwlimit == case @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case", CHANNEL_COUPLINGS) def test_ch_coupling(self, make_reseted_cleared_scope, ch_number, case): scope = make_reseted_cleared_scope scope.ch(ch_number).coupling = case assert scope.ch(ch_number).coupling == case @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_ch_display(self, make_reseted_cleared_scope, ch_number, case): scope = make_reseted_cleared_scope scope.ch(ch_number).display = case assert scope.ch(ch_number).display == case @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_ch_invert(self, make_reseted_cleared_scope, ch_number, case): scope = make_reseted_cleared_scope scope.ch(ch_number).invert = case assert scope.ch(ch_number).invert == case @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case, expected", CHANNEL_LABELS) def test_ch_label(self, make_reseted_cleared_scope, ch_number, case, expected): scope = make_reseted_cleared_scope scope.ch(ch_number).label = case assert scope.ch(ch_number).label == expected @pytest.mark.parametrize("ch_number", CHANNELS) def test_ch_offset(self, make_reseted_cleared_scope, ch_number): scope = make_reseted_cleared_scope scope.ch(ch_number).offset = 1 assert scope.ch(ch_number).offset == 1 @pytest.mark.parametrize("ch_number", CHANNELS) def test_ch_probe_attenuation(self, make_reseted_cleared_scope, ch_number): scope = make_reseted_cleared_scope scope.ch(ch_number).probe_attenuation = 10 assert scope.ch(ch_number).probe_attenuation == 10 @pytest.mark.parametrize("ch_number", CHANNELS) def test_ch_range(self, make_reseted_cleared_scope, ch_number): scope = make_reseted_cleared_scope scope.ch(ch_number).range = 10 assert scope.ch(ch_number).range == 10 @pytest.mark.parametrize("ch_number", CHANNELS) def test_ch_scale(self, make_reseted_cleared_scope, ch_number): scope = make_reseted_cleared_scope scope.ch(ch_number).scale = 0.1 assert scope.ch(ch_number).scale == 0.1 # Timebase def test_timebase(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope expected = {"REF": "CENT", "MAIN:RANG": +1.000E-03, "POS": 0.0, "MODE": "MAIN"} actual = scope.timebase assert actual == expected @pytest.mark.parametrize("case", TIMEBASE_MODES) def test_timebase_mode(self, make_reseted_cleared_scope, case): scope = make_reseted_cleared_scope scope.timebase_mode = case assert scope.timebase_mode == case def test_timebase_offset(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope scope.timebase_offset = 1 assert scope.timebase_offset == 1 def test_timebase_range(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope scope.timebase_range = 10 assert scope.timebase_range == 10 def test_timebase_scale(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope scope.timebase_scale = 0.1 assert scope.timebase_scale == 0.1 # Acquisition @pytest.mark.parametrize("case", ACQUISITION_TYPES) def test_acquisition_type(self, make_reseted_cleared_scope, case): scope = make_reseted_cleared_scope scope.acquisition_type = case assert scope.acquisition_type == case @pytest.mark.parametrize("case", ACQUISITION_MODES) def test_acquisition_mode(self, make_reseted_cleared_scope, case): scope = make_reseted_cleared_scope scope.acquisition_mode = case assert scope.acquisition_mode == case @pytest.mark.parametrize("case", WAVEFORM_POINTS_MODES) def test_waveform_points_mode(self, make_reseted_cleared_scope, case): scope = make_reseted_cleared_scope scope.waveform_points_mode = case assert scope.waveform_points_mode == case @pytest.mark.parametrize("case", WAVEFORM_POINTS) def test_waveform_points(self, make_reseted_cleared_scope, case): scope = make_reseted_cleared_scope scope.waveform_points_mode = "raw" scope.waveform_points = case assert scope.waveform_points == case @pytest.mark.parametrize("case", WAVEFORM_SOURCES) def test_waveform_source(self, make_reseted_cleared_scope, case): scope = make_reseted_cleared_scope scope.waveform_source = case assert scope.waveform_source == case @pytest.mark.parametrize("case", WAVEFORM_FORMATS) def test_waveform_format(self, make_reseted_cleared_scope, case): scope = make_reseted_cleared_scope scope.waveform_format = case assert scope.waveform_format == case def test_waveform_preamble(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope scope.waveform_format = "ascii" scope.waveform_source = "channel1" expected_preamble = {"count": 1, "format": "ASCII", "points": 62500, "type": "NORMAL", "xincrement": 1.6e-08, "xorigin": -0.0005, "xreference": 0, "yincrement": 0.0007851759, "yorigin": 0, "yreference": 32768.0} preamble = scope.waveform_preamble assert preamble == expected_preamble @pytest.mark.parametrize("case", DIGITIZE_SOURCES) def test_digitize(self, make_reseted_cleared_scope, case): scope = make_reseted_cleared_scope # Here, we only assert that no error arrises when using the expected parameters. # Success of digitize operation is evaluated through test_waveform_data scope.digitize(case) sleep(2) # Account for Digitize operation duration def test_waveform_data(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope scope.digitize("channel1") sleep(2) # Account for Digitize operation duration scope.waveform_format = "ascii" value = scope.waveform_data assert type(value) is list assert len(value) > 0 assert all(isinstance(n, float) for n in value) def test_system_setup(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope initial_setup = scope.system_setup scope.ch(1).display = not scope.ch(1).display scope.ch(2).display = not scope.ch(2).display # Assert that setup block is different assert scope.system_setup != initial_setup # Assert that the setup was successful scope.system_setup = initial_setup assert scope.system_setup == initial_setup # Setup methods @pytest.mark.parametrize("ch_number", CHANNELS) def test_channel_setup(self, make_reseted_cleared_scope, ch_number, caplog): # Using caplog to check content of log. caplog.set_level(logging.WARNING) scope = make_reseted_cleared_scope # Not testing the actual values assignment since different combinations of # parameters can play off each other. expected = scope.ch(ch_number).current_configuration scope.ch(ch_number).setup() assert scope.ch(ch_number).current_configuration == expected with pytest.raises(ValueError): scope.ch(3) scope.ch(ch_number).setup(1, vertical_range=1, scale=1) assert 'Both "vertical_range" and "scale" are specified. Specified "scale" has priority.' in caplog.text def test_timebase_setup(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope expected = scope.timebase scope.timebase_setup() assert scope.timebase == expected # Download methods def test_download_image_default_arguments(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope img = scope.download_image() assert type(img) is bytearray @pytest.mark.parametrize("format", ["bmp", "bmp8bit", "png"]) @pytest.mark.parametrize("color_palette", ["color", "grayscale"]) def test_download_image(self, make_reseted_cleared_scope, format, color_palette): scope = make_reseted_cleared_scope img = scope.download_image(format_=format, color_palette=color_palette) assert type(img) is bytearray def test_download_data_missingArgument(self, make_reseted_cleared_scope): scope = make_reseted_cleared_scope with pytest.raises(TypeError): data = scope.download_data() @pytest.mark.parametrize("case1", DOWNLOAD_SOURCES) @pytest.mark.parametrize("case2", WAVEFORM_POINTS) def test_download_data(self, make_reseted_cleared_scope, case1, case2): scope = make_reseted_cleared_scope data, preamble = scope.download_data(source=case1, points=case2) assert type(data) is np.ndarray # Returned length is not always as specified. Problem seems to be from scope itself. # assert len(data) == case2 assert type(preamble) is dict PyMeasure-0.9.0/tests/instruments/test_validators.py0000664000175000017500000001040014010037617023176 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest from pymeasure.instruments.validators import ( strict_range, strict_discrete_range, strict_discrete_set, truncated_range, truncated_discrete_set, modular_range, modular_range_bidirectional, joined_validators ) def test_strict_range(): assert strict_range(5, range(10)) == 5 assert strict_range(5.1, range(10)) == 5.1 with pytest.raises(ValueError) as e_info: strict_range(20, range(10)) def test_strict_discrete_range(): assert strict_discrete_range(0.1, [0, 0.2], 0.001) == 0.1 assert strict_discrete_range(5, range(10), 0.1) == 5 assert strict_discrete_range(5.1, range(10), 0.1) == 5.1 assert strict_discrete_range(5.1, range(10), 0.001) == 5.1 assert strict_discrete_range(-5.1, [-20, 20], 0.001) == -5.1 with pytest.raises(ValueError) as e_info: strict_discrete_range(5.1, range(5), 0.001) with pytest.raises(ValueError) as e_info: strict_discrete_range(5.01, range(5), 0.1) with pytest.raises(ValueError) as e_info: strict_discrete_range(0.003, [0, 0.2], 0.002) def test_strict_discrete_set(): assert strict_discrete_set(5, range(10)) == 5 with pytest.raises(ValueError) as e_info: strict_discrete_set(5.1, range(10)) with pytest.raises(ValueError) as e_info: strict_discrete_set(20, range(10)) def test_truncated_range(): assert truncated_range(5, range(10)) == 5 assert truncated_range(5.1, range(10)) == 5.1 assert truncated_range(-10, range(10)) == 0 assert truncated_range(20, range(10)) == 9 def test_truncated_discrete_set(): assert truncated_discrete_set(5, range(10)) == 5 assert truncated_discrete_set(5.1, range(10)) == 6 assert truncated_discrete_set(11, range(10)) == 9 assert truncated_discrete_set(-10, range(10)) == 0 def test_modular_range(): assert modular_range(5, range(10)) == 5 assert abs(modular_range(5.1, range(10)) - 5.1) < 1e-6 assert modular_range(11, range(10)) == 2 assert abs(modular_range(11.3, range(10)) - 2.3) < 1e-6 assert abs(modular_range(-7.1, range(10)) - 1.9) < 1e-6 assert abs(modular_range(-13.2, range(10)) - 4.8) < 1e-6 def test_modular_range_bidirectional(): assert modular_range_bidirectional(5, range(10)) == 5 assert abs(modular_range_bidirectional(5.1, range(10)) - 5.1) < 1e-6 assert modular_range_bidirectional(11, range(10)) == 2 assert abs(modular_range_bidirectional(11.3, range(10)) - 2.3) < 1e-6 assert modular_range_bidirectional(-7, range(10)) == -7 assert abs(modular_range_bidirectional(-7.1, range(10)) - (-7.1)) < 1e-6 assert abs(modular_range_bidirectional(-13.2, range(10)) - (-4.2)) < 1e-6 def test_joined_validators(): tst_validator = joined_validators(strict_discrete_set, strict_range) assert tst_validator(5, [["ON", "OFF"], range(10)]) == 5 assert tst_validator(5.1, [["ON", "OFF"], range(10)]) == 5.1 assert tst_validator("ON", [["ON", "OFF"], range(10)]) == "ON" with pytest.raises(ValueError) as e_info: tst_validator("OUT", [["ON", "OFF"], range(10)]) with pytest.raises(ValueError) as e_info: tst_validator(20, [["ON", "OFF"], range(10)]) PyMeasure-0.9.0/tests/instruments/agilent/0000775000175000017500000000000014010046235021041 5ustar colincolin00000000000000PyMeasure-0.9.0/tests/instruments/agilent/test_agilent34450A.py0000664000175000017500000004700414010037617024607 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest from pymeasure.instruments.agilent.agilent34450A import Agilent34450A from pyvisa.errors import VisaIOError pytest.skip('Only work with connected hardware', allow_module_level=True) class TestAgilent34450A: """ Unit tests for Agilent34450A class. This test suite, needs the following setup to work properly: - A Agilent34450A device should be connected to the computer; - The device's address must be set in the RESOURCE constant. """ ############################################################### # Agilent34450A device goes here: RESOURCE = "USB0::10893::45848::MY56511723::0::INSTR" ############################################################### ######################### # PARAMETRIZATION CASES # ######################### BOOLEANS = [False, True] RESOLUTIONS = [[3.00E-5, 3.00E-5], [2.00E-5, 2.00E-5], [1.50E-6, 1.50E-6], ["MIN", 1.50E-6], ["MAX", 3.00E-5], ["DEF", 1.50E-6]] MODES = ["current", "ac current", "voltage", "ac voltage", "resistance", "4w resistance", "current frequency", "voltage frequency", "continuity", "diode", "temperature", "capacitance"] CURRENT_RANGES = [[100E-6, 100E-6], [1E-3, 1E-3], [10E-3, 10E-3], [100E-3, 100E-3], [1, 1], ["MIN", 100E-6], ["MAX", 10], ["DEF", 100E-3]] CURRENT_AC_RANGES = [[10E-3, 10E-3], [100E-3, 100E-3], [1, 1], ["MIN", 10E-3], ["MAX", 10], ["DEF", 100E-3]] VOLTAGE_RANGES = [[100E-3, 100E-3], [1, 1], [10, 10], [100, 100], [1000, 1000], ["MIN", 100E-3], ["MAX", 1000], ["DEF", 10]] VOLTAGE_AC_RANGES = [[100E-3, 100E-3], [1, 1], [10, 10], [100, 100], [750, 750], ["MIN", 100E-3], ["MAX", 750], ["DEF", 10]] RESISTANCE_RANGES = [[1E2, 1E2], [1E3, 1E3], [1E4, 1E4], [1E5, 1E5], [1E6, 1E6], [1E7, 1E7], [1E8, 1E8], ["MIN", 1E2], ["MAX", 1E8], ["DEF", 1E3]] RESISTANCE_4W_RANGES = [[1E2, 1E2], [1E3, 1E3], [1E4, 1E4], [1E5, 1E5], [1E6, 1E6], [1E7, 1E7], [1E8, 1E8], ["MIN", 1E2], ["MAX", 1E8], ["DEF", 1E3]] FREQUENCY_APERTURES = [[100E-3, 100E-3], [1, 1], ["MIN", 100E-3], ["MAX", 1], ["DEF", 1]] CAPACITANCE_RANGES = [[1E-9, 1E-9], [1E-8, 1E-8], [1E-7, 1E-7], [1E-6, 1E-6], [1E-5, 1E-5], [1E-4, 1E-4], [1E-3, 1E-3], [1E-2, 1E-2], ["MIN", 1E-9], ["MAX", 1E-2], ["DEF", 1E-6]] DMM = Agilent34450A(RESOURCE) ############ # FIXTURES # ############ @pytest.fixture def make_reseted_dmm(self): self.DMM.reset() return self.DMM ######### # TESTS # ######### def test_dmm_initialization_bad(self): bad_resource = "USB0::10893::45848::MY12345678::0::INSTR" # The pure python VISA library (pyvisa-py) raises a ValueError while the PyVISA library raises a VisaIOError. with pytest.raises((ValueError, VisaIOError)): dmm = Agilent34450A(bad_resource) def test_reset(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.write(":configure:current") # Instrument should return to DCV once reseted dmm.reset() assert dmm.ask(":configure?") == '"VOLT +1.000000E+01,+1.500000E-06"\n' def test_beep(self, make_reseted_dmm): dmm = make_reseted_dmm # Assert that a beep is audible dmm.beep() @pytest.mark.parametrize("case", MODES) def test_modes(self, make_reseted_dmm, case): dmm = make_reseted_dmm dmm.mode = case assert dmm.mode == case # Current @pytest.mark.parametrize("case, expected", CURRENT_RANGES) def test_current_range(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.current_range = case assert dmm.current_range == expected @pytest.mark.parametrize("case", BOOLEANS) def test_current_auto_range(self, make_reseted_dmm, case): dmm = make_reseted_dmm dmm.current_auto_range = case assert dmm.current_auto_range == case @pytest.mark.parametrize("case, expected", RESOLUTIONS) def test_current_resolution(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.current_resolution = case assert dmm.current_resolution == expected @pytest.mark.parametrize("case, expected", CURRENT_AC_RANGES) def test_current_ac_range(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.current_ac_range = case assert dmm.current_ac_range == expected @pytest.mark.parametrize("case", BOOLEANS) def test_current_ac_auto_range(self, make_reseted_dmm, case): dmm = make_reseted_dmm dmm.current_ac_auto_range = case assert dmm.current_ac_auto_range == case @pytest.mark.parametrize("case, expected", RESOLUTIONS) def test_current_ac_resolution(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.current_ac_resolution = case assert dmm.current_ac_resolution == expected def test_configure_current(self, make_reseted_dmm): dmm = make_reseted_dmm # No parameters specified dmm.configure_current() assert dmm.mode == "current" assert dmm.current_auto_range == 1 assert dmm.current_resolution == 1.50E-6 # Four possible paths dmm.configure_current(current_range=1, ac=True, resolution="MAX") assert dmm.mode == "ac current" assert dmm.current_ac_range == 1 assert dmm.current_ac_auto_range == 0 assert dmm.current_ac_resolution == 3.00E-5 dmm.configure_current(current_range="AUTO", ac=True, resolution="MIN") assert dmm.mode == "ac current" assert dmm.current_ac_auto_range == 1 assert dmm.current_ac_resolution == 1.50E-6 dmm.configure_current(current_range=1, ac=False, resolution="MAX") assert dmm.mode == "current" assert dmm.current_range == 1 assert dmm.current_auto_range == 0 assert dmm.current_resolution == 3.00E-5 dmm.configure_current(current_range="AUTO", ac=False, resolution="MIN") assert dmm.mode == "current" assert dmm.current_auto_range == 1 assert dmm.current_resolution == 1.50E-6 def test_current_reading(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.mode = "current" value = dmm.current assert type(value) is float def test_current_ac_reading(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.mode = "ac current" value = dmm.current_ac assert type(value) is float # Voltage @pytest.mark.parametrize("case, expected", VOLTAGE_RANGES) def test_voltage_range(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.voltage_range = case assert dmm.voltage_range == expected @pytest.mark.parametrize("case", BOOLEANS) def test_voltage_auto_range(self, make_reseted_dmm, case): dmm = make_reseted_dmm dmm.voltage_auto_range = case assert dmm.voltage_auto_range == case @pytest.mark.parametrize("case, expected", RESOLUTIONS) def test_voltage_resolution(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.voltage_resolution = case assert dmm.voltage_resolution == expected @pytest.mark.parametrize("case, expected", VOLTAGE_AC_RANGES) def test_voltage_ac_range(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.voltage_ac_range = case assert dmm.voltage_ac_range == expected @pytest.mark.parametrize("case", BOOLEANS) def test_voltage_ac_auto_range(self, make_reseted_dmm, case): dmm = make_reseted_dmm dmm.voltage_ac_auto_range = case assert dmm.voltage_ac_auto_range == case @pytest.mark.parametrize("case, expected", RESOLUTIONS) def test_voltage_ac_resolution(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.voltage_ac_resolution = case assert dmm.voltage_ac_resolution == expected def test_configure_voltage(self, make_reseted_dmm): dmm = make_reseted_dmm # No parameters specified dmm.configure_voltage() assert dmm.mode == "voltage" assert dmm.voltage_auto_range == 1 assert dmm.voltage_resolution == 1.50E-6 # Four possible paths dmm.configure_voltage(voltage_range=100, ac=True, resolution="MAX") assert dmm.mode == "ac voltage" assert dmm.voltage_ac_range == 100 assert dmm.voltage_ac_auto_range == 0 assert dmm.voltage_ac_resolution == 3.00E-5 dmm.configure_voltage(voltage_range="AUTO", ac=True, resolution="MIN") assert dmm.mode == "ac voltage" assert dmm.voltage_ac_auto_range == 1 assert dmm.voltage_ac_resolution == 1.50E-6 dmm.configure_voltage(voltage_range=100, ac=False, resolution="MAX") assert dmm.mode == "voltage" assert dmm.voltage_range == 100 assert dmm.voltage_auto_range == 0 assert dmm.voltage_resolution == 3.00E-5 dmm.configure_voltage(voltage_range="AUTO", ac=False, resolution="MIN") assert dmm.mode == "voltage" assert dmm.voltage_auto_range == 1 assert dmm.voltage_resolution == 1.50E-6 def test_voltage_reading(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.mode = "voltage" value = dmm.voltage assert type(value) is float def test_voltage_ac_reading(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.mode = "ac voltage" value = dmm.voltage_ac assert type(value) is float # Resistance @pytest.mark.parametrize("case, expected", RESISTANCE_RANGES) def test_resistance_range(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.resistance_range = case assert dmm.resistance_range == expected @pytest.mark.parametrize("case", BOOLEANS) def test_resistance_auto_range(self, make_reseted_dmm, case): dmm = make_reseted_dmm dmm.resistance_auto_range = case assert dmm.resistance_auto_range == case @pytest.mark.parametrize("case, expected", RESOLUTIONS) def test_resistance_resolution(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.resistance_resolution = case assert dmm.resistance_resolution == expected @pytest.mark.parametrize("case, expected", RESISTANCE_4W_RANGES) def test_resistance_4w_range(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.resistance_4w_range = case assert dmm.resistance_4w_range == expected @pytest.mark.parametrize("case", BOOLEANS) def test_resistance_4w_auto_range(self, make_reseted_dmm, case): dmm = make_reseted_dmm dmm.resistance_4w_auto_range = case assert dmm.resistance_4w_auto_range == case @pytest.mark.parametrize("case, expected", RESOLUTIONS) def test_resistance_4w_resolution(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.resistance_4w_resolution = case assert dmm.resistance_4w_resolution == expected def test_configure_resistance(self, make_reseted_dmm): dmm = make_reseted_dmm # No parameters specified dmm.configure_resistance() assert dmm.mode == "resistance" assert dmm.resistance_auto_range == 1 assert dmm.resistance_resolution == 1.50E-6 # Four possible paths dmm.configure_resistance(resistance_range=10E3, wires=2, resolution="MAX") assert dmm.mode == "resistance" assert dmm.resistance_range == 10E3 assert dmm.resistance_auto_range == 0 assert dmm.resistance_resolution == 3.00E-5 dmm.configure_resistance(resistance_range="AUTO", wires=2, resolution="MIN") assert dmm.mode == "resistance" assert dmm.resistance_auto_range == 1 assert dmm.resistance_resolution == 1.50E-6 dmm.configure_resistance(resistance_range=10E3, wires=4, resolution="MAX") assert dmm.mode == "4w resistance" assert dmm.resistance_4w_range == 10E3 assert dmm.resistance_4w_auto_range == 0 assert dmm.resistance_4w_resolution == 3.00E-5 dmm.configure_resistance(resistance_range="AUTO", wires=4, resolution="MIN") assert dmm.mode == "4w resistance" assert dmm.resistance_4w_auto_range == 1 assert dmm.resistance_4w_resolution == 1.50E-6 # Should raise ValueError with pytest.raises(ValueError): dmm.configure_resistance(wires=3) def test_resistance_reading(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.mode = "resistance" value = dmm.resistance assert type(value) is float def test_resistance_4w_reading(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.mode = "4w resistance" value = dmm.resistance_4w assert type(value) is float # Frequency @pytest.mark.parametrize("case, expected", CURRENT_AC_RANGES) def test_frequency_current_range(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.frequency_current_range = case assert dmm.frequency_current_range == expected @pytest.mark.parametrize("case", BOOLEANS) def test_frequency_current_auto_range(self, make_reseted_dmm, case): dmm = make_reseted_dmm dmm.frequency_current_auto_range = case assert dmm.frequency_current_auto_range == case @pytest.mark.parametrize("case, expected", VOLTAGE_AC_RANGES) def test_frequency_voltage_range(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.frequency_voltage_range = case assert dmm.frequency_voltage_range == expected @pytest.mark.parametrize("case", BOOLEANS) def test_frequency_voltage_auto_range(self, make_reseted_dmm, case): dmm = make_reseted_dmm dmm.frequency_voltage_auto_range = case assert dmm.frequency_voltage_auto_range == case @pytest.mark.parametrize("case, expected", FREQUENCY_APERTURES) def test_frequency_aperture(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.frequency_aperture = case assert dmm.frequency_aperture == expected def test_configure_frequency(self, make_reseted_dmm): dmm = make_reseted_dmm # No parameters specified dmm.configure_frequency() assert dmm.mode == "voltage frequency" assert dmm.frequency_voltage_auto_range == 1 assert dmm.frequency_aperture == 1 # Four possible paths dmm.configure_frequency(measured_from="voltage_ac", measured_from_range=1, aperture=1E-1) assert dmm.mode == "voltage frequency" assert dmm.frequency_voltage_range == 1 assert dmm.frequency_voltage_auto_range == 0 assert dmm.frequency_aperture == 1E-1 dmm.configure_frequency(measured_from="voltage_ac", measured_from_range="AUTO", aperture=1) assert dmm.mode == "voltage frequency" assert dmm.frequency_voltage_auto_range == 1 assert dmm.frequency_aperture == 1 dmm.configure_frequency(measured_from="current_ac", measured_from_range=1E-1, aperture=1E-1) assert dmm.mode == "current frequency" assert dmm.frequency_current_range == 1E-1 assert dmm.frequency_current_auto_range == 0 assert dmm.frequency_aperture == 1E-1 dmm.configure_frequency(measured_from="current_ac", measured_from_range="AUTO", aperture=1) assert dmm.mode == "current frequency" assert dmm.frequency_current_auto_range == 1 assert dmm.frequency_aperture == 1 # Should raise ValueError with pytest.raises(ValueError): dmm.configure_frequency(measured_from="") def test_frequency_reading(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.mode = "voltage frequency" value = dmm.frequency assert type(value) is float # Temperature def test_configure_temperature(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.configure_temperature() assert dmm.mode == "temperature" def test_temperature_reading(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.mode = "temperature" value = dmm.temperature assert type(value) is float # Diode def test_configure_diode(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.configure_diode() assert dmm.mode == "diode" def test_diode_reading(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.mode = "diode" value = dmm.diode assert type(value) is float # Capacitance @pytest.mark.parametrize("case, expected", CAPACITANCE_RANGES) def test_capacitance_range(self, make_reseted_dmm, case, expected): dmm = make_reseted_dmm dmm.capacitance_range = case assert dmm.capacitance_range == expected @pytest.mark.parametrize("case", BOOLEANS) def test_capacitance_auto_range(self, make_reseted_dmm, case): dmm = make_reseted_dmm dmm.capacitance_auto_range = case assert dmm.capacitance_auto_range == case def test_configure_capacitance(self, make_reseted_dmm): dmm = make_reseted_dmm # No parameters specified dmm.configure_capacitance() assert dmm.mode == "capacitance" assert dmm.capacitance_auto_range == 1 # Two possible paths dmm.configure_capacitance(capacitance_range=1E-2) assert dmm.mode == "capacitance" assert dmm.capacitance_range == 1E-2 assert dmm.capacitance_auto_range == 0 dmm.configure_capacitance(capacitance_range="AUTO") assert dmm.mode == "capacitance" assert dmm.capacitance_auto_range == 1 def test_capacitance_reading(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.mode = "capacitance" value = dmm.capacitance assert type(value) is float # Continuity def test_configure_continuity(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.configure_continuity() assert dmm.mode == "continuity" def test_continuity_reading(self, make_reseted_dmm): dmm = make_reseted_dmm dmm.mode = "continuity" value = dmm.continuity assert type(value) is float PyMeasure-0.9.0/tests/instruments/test_instrument.py0000664000175000017500000001336114010037617023247 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest from pymeasure.adapters import FakeAdapter from pymeasure.instruments.instrument import Instrument, FakeInstrument from pymeasure.instruments.validators import strict_discrete_set, strict_range def test_fake_instrument(): fake = FakeInstrument() fake.write("Testing") assert fake.read() == "Testing" assert fake.read() == "" assert fake.values("5") == [5] def test_control_doc(): doc = """ X property """ class Fake(Instrument): x = Instrument.control( "", "%d", doc ) assert Fake.x.__doc__ == doc def test_control_validator(): class Fake(FakeInstrument): x = Instrument.control( "", "%d", "", validator=strict_discrete_set, values=range(10), ) fake = Fake() fake.x = 5 assert fake.read() == '5' fake.x = 5 assert fake.x == 5 with pytest.raises(ValueError) as e_info: fake.x = 20 def test_control_validator_map(): class Fake(FakeInstrument): x = Instrument.control( "", "%d", "", validator=strict_discrete_set, values=[4, 5, 6, 7], map_values=True, ) fake = Fake() fake.x = 5 assert fake.read() == '1' fake.x = 5 assert fake.x == 5 with pytest.raises(ValueError) as e_info: fake.x = 20 def test_control_dict_map(): class Fake(FakeInstrument): x = Instrument.control( "", "%d", "", validator=strict_discrete_set, values={5: 1, 10: 2, 20: 3}, map_values=True, ) fake = Fake() fake.x = 5 assert fake.read() == '1' fake.x = 5 assert fake.x == 5 fake.x = 20 assert fake.read() == '3' def test_control_dict_str_map(): class Fake(FakeInstrument): x = Instrument.control( "", "%d", "", validator=strict_discrete_set, values={'X': 1, 'Y': 2, 'Z': 3}, map_values=True, ) fake = Fake() fake.x = 'X' assert fake.read() == '1' fake.x = 'Y' assert fake.x == 'Y' fake.x = 'Z' assert fake.read() == '3' def test_control_process(): class Fake(FakeInstrument): x = Instrument.control( "", "%d", "", validator=strict_range, values=[5e-3, 120e-3], get_process=lambda v: v * 1e-3, set_process=lambda v: v * 1e3, ) fake = Fake() fake.x = 10e-3 assert fake.read() == '10' fake.x = 30e-3 assert fake.x == 30e-3 def test_control_get_process(): class Fake(FakeInstrument): x = Instrument.control( "", "JUNK%d", "", validator=strict_range, values=[0, 10], get_process=lambda v: int(v.replace('JUNK', '')), ) fake = Fake() fake.x = 5 assert fake.read() == 'JUNK5' fake.x = 5 assert fake.x == 5 def test_control_preprocess_reply_property(): # test setting preprocess_reply at property-level class Fake(FakeInstrument): x = Instrument.control( "", "JUNK%d", "", preprocess_reply=lambda v: v.replace('JUNK', ''), cast=int ) fake = Fake() fake.x = 5 assert fake.read() == 'JUNK5' # notice that read returns the full reply since preprocess_reply is only # called inside Adapter.values() fake.x = 5 assert fake.x == 5 fake.x = 5 assert type(fake.x) == int def test_control_preprocess_reply_adapter(): # test setting preprocess_reply at Adapter-level class Fake(FakeInstrument): def __init__(self): super().__init__(preprocess_reply=lambda v: v.replace('JUNK', '')) x = Instrument.control( "", "JUNK%d", "", cast=int ) fake = Fake() fake.x = 5 assert fake.read() == 'JUNK5' # notice that read returns the full reply since preprocess_reply is only # called inside Adapter.values() fake.x = 5 assert fake.x == 5 def test_measurement_dict_str_map(): class Fake(FakeInstrument): x = Instrument.measurement( "", "", values={'X': 1, 'Y': 2, 'Z': 3}, map_values=True, ) fake = Fake() fake.write('1') assert fake.x == 'X' fake.write('2') assert fake.x == 'Y' fake.write('3') assert fake.x == 'Z' def test_setting_process(): class Fake(FakeInstrument): x = Instrument.setting( "OUT %d", "", set_process=lambda v: int(bool(v)), ) fake = Fake() fake.x = False assert fake.read() == 'OUT 0' fake.x = 2 assert fake.read() == 'OUT 1' PyMeasure-0.9.0/tests/adapters/0000775000175000017500000000000014010046235016626 5ustar colincolin00000000000000PyMeasure-0.9.0/tests/adapters/test_adapter.py0000664000175000017500000000375414010037617021674 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from pymeasure.adapters import FakeAdapter log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def test_adapter_values(): a = FakeAdapter() assert a.values("5,6,7") == [5, 6, 7] assert a.values("5,6,7", cast=str) == ['5', '6', '7'] assert a.values("X,Y,Z") == ['X', 'Y', 'Z'] assert a.values("X,Y,Z", cast=str) == ['X', 'Y', 'Z'] assert a.values("X.Y.Z", separator='.') == ['X', 'Y', 'Z'] def test_adapter_preprocess_reply(): a = FakeAdapter(preprocess_reply=lambda v: v[1:]) assert a.values("R42.1") == [42.1] assert a.values("A4,3,2") == [4, 3, 2] assert a.values("TV 1", preprocess_reply=lambda v: v.split()[0]) == ['TV'] assert a.values("15", preprocess_reply=lambda v: v) == [15] a = FakeAdapter() assert a.values("V 3.4", preprocess_reply=lambda v: v.split()[1]) == [3.4] PyMeasure-0.9.0/tests/adapters/test_visa.py0000664000175000017500000000510714010037617021210 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import importlib.util import pytest from pytest import approx from pymeasure.adapters import VISAAdapter from pymeasure.instruments import Instrument # This uses a pyvisa-sim default instrument, we could also define our own. SIM_RESOURCE = 'ASRL2::INSTR' is_pyvisa_sim_installed = bool(importlib.util.find_spec('pyvisa_sim')) if not is_pyvisa_sim_installed: pytest.skip('PyVISA tests require the pyvisa-sim library', allow_module_level=True) def make_visa_adapter(**kwargs): return VISAAdapter(SIM_RESOURCE, visa_library='@sim', **kwargs) def test_visa_version(): assert VISAAdapter.has_supported_version() def test_correct_visa_kwarg(): """Confirm that the query_delay kwargs gets passed through to the VISA connection.""" instr = Instrument(adapter=SIM_RESOURCE, name='delayed', query_delay=0.5, visa_library='@sim') assert instr.adapter.connection.query_delay == approx(0.5) def test_visa_adapter(): adapter = make_visa_adapter() assert repr(adapter) == f"" assert adapter.ask("*IDN?") == "SCPI,MOCK,VERSION_1.0\n" adapter.write("*IDN?") assert adapter.read() == "SCPI,MOCK,VERSION_1.0\n" def test_visa_adapter_ask_values(): adapter = make_visa_adapter() assert adapter.ask_values(":VOLT:IMM:AMPL?", separator=",") == [1.0] def test_visa_adapter_write_binary_values(): adapter = make_visa_adapter() adapter.write_binary_values("OUTP", [1], datatype='B') PyMeasure-0.9.0/tests/conftest.py0000664000175000017500000000222314010037617017225 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest PyMeasure-0.9.0/tests/test_thread.py0000664000175000017500000000265114010037617017713 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.thread import StoppableThread def test_thread_stopping(): t = StoppableThread() t.start() t.stop() assert t.should_stop() is True t.join() def test_thread_joining(): t = StoppableThread() t.start() t.join() assert t.should_stop() is True PyMeasure-0.9.0/tests/display/0000775000175000017500000000000014010046235016470 5ustar colincolin00000000000000PyMeasure-0.9.0/tests/display/test_inputs.py0000664000175000017500000002255514010037617021440 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest from unittest import mock from pymeasure.display.Qt import QtGui, QtCore from pymeasure.display.inputs import ScientificInput, BooleanInput, ListInput from pymeasure.experiment.parameters import BooleanParameter, ListParameter, FloatParameter @pytest.mark.parametrize("default_value", [True, False]) class TestBooleanInput: @pytest.mark.parametrize("value_remains_default", [True, False]) def test_init_from_param(self, qtbot, default_value, value_remains_default): # set up BooleanInput bool_param = BooleanParameter('potato', default=default_value) if (value_remains_default): # Enable check that the value is initialized to default_value check_value = default_value else: # Set to a non default value bool_param.value = not default_value # Enable check that the value is changed after initialization to a non default value check_value = not default_value bool_input = BooleanInput(bool_param) qtbot.addWidget(bool_input) # test assert bool_input.text() == bool_param.name assert bool_input.value() == check_value def test_setValue_should_update_value(self, qtbot, default_value): # set up BooleanInput bool_param = BooleanParameter('potato', default=default_value) bool_input = BooleanInput(bool_param) qtbot.addWidget(bool_input) bool_input.setValue(not default_value) assert bool_input.value() == (not default_value) def test_leftclick_should_update_parameter(self, qtbot, default_value): # set up BooleanInput bool_param = BooleanParameter('potato', default=default_value) with mock.patch('test_inputs.BooleanParameter.value', new_callable=mock.PropertyMock, return_value=default_value) as p: bool_input = BooleanInput(bool_param) # Clear any call to property 'value' during initialization p.reset_mock() qtbot.addWidget(bool_input) bool_input.show() # TODO: fix: fails to toggle on Windows #qtbot.mouseClick(bool_input, QtCore.Qt.LeftButton) bool_input.setValue(not default_value) assert bool_input.value() == (not default_value) bool_input.parameter # lazy update p.assert_called_once_with(not default_value) class TestListInput: @pytest.mark.parametrize("choices,default_value", [ (["abc", "def", "ghi"], "abc"), # strings ([123, 456, 789], 123), # numbers (["abc", "def", "ghi"], "def") # default not first value ]) @pytest.mark.parametrize("value_remains_default", [True, False]) def test_init_from_param(self, qtbot, choices, default_value, value_remains_default): list_param = ListParameter('potato', choices=choices, default=default_value, units='m') if (value_remains_default): # Enable check that the value is initialized to default_value check_value = default_value else: # Set to a non default value list_param.value = choices[2] # Enable check that the value is changed after initialization to a non default_value check_value = choices[2] list_input = ListInput(list_param) qtbot.addWidget(list_input) assert list_input.isEditable() == False assert list_input.value() == check_value def test_setValue_should_update_value(self, qtbot): # Test write-read loop: verify value -> index -> value conversion choices = [123, 'abc', 0] list_param = ListParameter('potato', choices=choices, default=123) list_input = ListInput(list_param) qtbot.addWidget(list_input) for choice in choices: list_input.setValue(choice) assert list_input.currentText() == str(choice) assert list_input.value() == choice def test_setValue_should_update_parameter(self, qtbot): choices = [123, 'abc', 0] list_param = ListParameter('potato', choices=choices, default=123) list_input = ListInput(list_param) qtbot.addWidget(list_input) with mock.patch('test_inputs.ListParameter.value', new_callable=mock.PropertyMock, return_value=123) as p: for choice in choices: list_input.setValue(choice) list_input.parameter # lazy update p.assert_has_calls((mock.call(123), mock.call('abc'), mock.call(0))) def test_unit_should_append_to_strings(self, qtbot): list_param = ListParameter('potato', choices=[123, 456], default=123, units='m') list_input = ListInput(list_param) qtbot.addWidget(list_input) assert list_input.currentText() == '123 m' def test_set_invalid_value_should_raise(self, qtbot): list_param = ListParameter('potato', choices=[123, 456], default=123, units='m') list_input = ListInput(list_param) qtbot.addWidget(list_input) with pytest.raises(ValueError): list_input.setValue(789) class TestScientificInput: @pytest.mark.parametrize("min_,max_,default_value", [ [0, 20, 12], [0, 1000, 200], # regression #118: default above default max 99.99 [-1000, 1000, -10], # regression #118: default below default min 0 [0.004, 5.5, 3.3], # minimum #225: 0 < minimum < 0.005 [0, 0.01, 0.002] # default #233: default <0.01 changes to 0 ]) @pytest.mark.parametrize("value_remains_default", [True, False]) def test_init_from_param(self, qtbot, min_, max_, default_value, value_remains_default): float_param = FloatParameter('potato', minimum=min_, maximum=max_, default=default_value, units='m') if (value_remains_default): # Enable check that the value is initialized to default_value check_value = default_value else: # Set to a non default value float_param.value = min_ # Enable check that the value is changed after initialization to a non default value check_value = min_ sci_input = ScientificInput(float_param) qtbot.addWidget(sci_input) assert sci_input.minimum() == min_ assert sci_input.maximum() == max_ assert sci_input.value() == check_value assert sci_input.suffix() == ' m' def test_setValue_within_range_should_set(self, qtbot): float_param = FloatParameter('potato', minimum=-10, maximum=10, default=0) sci_input = ScientificInput(float_param) qtbot.addWidget(sci_input) # test sci_input.setValue(5) assert sci_input.value() == 5 def test_setValue_within_range_should_set_regression_118(self, qtbot): float_param = FloatParameter('potato', minimum=-1000, maximum=1000, default=0) sci_input = ScientificInput(float_param) qtbot.addWidget(sci_input) # test - validate min/max beyond QDoubleSpinBox defaults # QDoubleSpinBox defaults are 0 to 99.9 - so test value >= 100 sci_input.setValue(999) assert sci_input.value() == 999 sci_input.setValue(-999) assert sci_input.value() == -999 def test_setValue_out_of_range_should_constrain(self, qtbot): float_param = FloatParameter('potato', minimum=-1000, maximum=1000, default=0) sci_input = ScientificInput(float_param) qtbot.addWidget(sci_input) # test sci_input.setValue(1024) assert sci_input.value() == 1000 sci_input.setValue(-1024) assert sci_input.value() == -1000 def test_setValue_should_update_param(self, qtbot): float_param = FloatParameter('potato', minimum=-1000, maximum=1000, default=10.0) sci_input = ScientificInput(float_param) qtbot.addWidget(sci_input) with mock.patch('test_inputs.FloatParameter.value', new_callable=mock.PropertyMock, return_value=10.0) as p: # test sci_input.setValue(5.0) sci_input.parameter # lazy update p.assert_called_once_with(5.0) PyMeasure-0.9.0/tests/display/test_plotter.py0000664000175000017500000000373614010037617021607 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest from unittest import mock from pymeasure.display.Qt import QtGui, QtCore from pymeasure.display.plotter import Plotter from pymeasure.experiment.results import Results # TODO: Repair this unit test # class TestPlotter: # # TODO: More thorough unit (or integration?) tests. # # @mock.patch('pymeasure.display.plotter.PlotterWindow') # @mock.patch('pymeasure.display.plotter.QtGui') # @mock.patch.object(Plotter, 'setup_plot') # def test_setup_plot_called_on_init(self, mock_sp, MockQtGui, MockPlotterWindow): # r = mock.MagicMock(spec=Results) # mockplot = mock.MagicMock() # MockPlotterWindow.return_value = mock.MagicMock(plot=mockplot) # p = Plotter(r) # p.run() # we don't care about starting the process, just check the run # mock_sp.assert_called_once_with(mockplot) PyMeasure-0.9.0/tests/display/test_windows.py0000664000175000017500000000451114010037617021600 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest from unittest import mock from pymeasure.display.Qt import QtGui, QtCore from pymeasure.display.windows import ManagedWindow from pymeasure.experiment.procedure import Procedure # TODO: Repair this unit test # class TestManagedWindow: # # TODO: More thorough unit (or integration?) tests. # # # TODO: Could we make this more testable? These patches are a bit ridiculous. # @mock.patch('pymeasure.display.windows.Manager') # @mock.patch('pymeasure.display.windows.InputsWidget') # @mock.patch('pymeasure.display.windows.BrowserWidget') # @mock.patch('pymeasure.display.windows.PlotWidget') # @mock.patch('pymeasure.display.windows.QtGui') # @mock.patch.object(ManagedWindow, 'setCentralWidget') # @mock.patch.object(ManagedWindow, 'addDockWidget') # @mock.patch.object(ManagedWindow, 'setup_plot') # def test_setup_plot_called_on_init(self, mock_sp, mock_a, mock_b, # MockQtGui, MockPlotWidget, MockBrowserWidget, MockInputsWidget, # MockManager, qtbot): # mock_procedure = mock.MagicMock(spec=Procedure) # w = ManagedWindow(mock_procedure) # qtbot.addWidget(w) # mock_sp.assert_called_once_with(w.plot) PyMeasure-0.9.0/tests/test_log.py0000664000175000017500000000404314010037617017222 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import time import sys from unittest import mock import pytest from pymeasure.process import context from pymeasure.log import Scribe, setup_logging # TODO: Add tests for logging convenience functions and TopicQueueHandler def test_scribe_stop(): q = context.Queue() s = Scribe(q) s.start() assert s.is_alive() is True s.stop() assert s.is_alive() is False def test_scribe_finish(): q = context.Queue() s = Scribe(q) s.start() assert s.is_alive() is True q.put(None) time.sleep(0.1) assert s.is_alive() is False @pytest.mark.skipif(sys.version_info < (3, 6), reason='Mock.assert_called_once requires python 3.6') def test_setup_file_logging(): with mock.patch('pymeasure.log.file_log') as mocked_file_log: setup_logging() mocked_file_log.assert_not_called() setup_logging(filename='log.txt') mocked_file_log.assert_called_once() PyMeasure-0.9.0/tests/experiment/0000775000175000017500000000000014010046235017203 5ustar colincolin00000000000000PyMeasure-0.9.0/tests/experiment/test_workers.py0000664000175000017500000000556414010037617022326 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest import os import tempfile from time import sleep from importlib.machinery import SourceFileLoader from pymeasure.experiment.workers import Worker from pymeasure.experiment.results import Results # Load the procedure, without it being in a module data_path = os.path.join(os.path.dirname(__file__), 'data/procedure_for_testing.py') RandomProcedure = SourceFileLoader('procedure', data_path).load_module().RandomProcedure #from data.procedure_for_testing import RandomProcedure def test_procedure(): """ Ensure that the loaded test procedure is properly functioning """ procedure = RandomProcedure() assert procedure.iterations == 100 assert procedure.delay == 0.001 assert hasattr(procedure, 'execute') def test_worker_stop(): procedure = RandomProcedure() file = tempfile.mktemp() results = Results(procedure, file) worker = Worker(results) worker.start() worker.stop() assert worker.should_stop() worker.join() def test_worker_finish(): procedure = RandomProcedure() procedure.iterations = 100 procedure.delay = 0.001 file = tempfile.mktemp() results = Results(procedure, file) worker = Worker(results) worker.start() worker.join(timeout=5) assert not worker.is_alive() new_results = Results.load(file, procedure_class=RandomProcedure) assert new_results.data.shape == (100, 2) def test_worker_closes_file_after_finishing(): procedure = RandomProcedure() procedure.iterations = 100 procedure.delay = 0.001 file = tempfile.mktemp() results = Results(procedure, file) worker = Worker(results) worker.start() worker.join(timeout=5) # Test if the file has been properly closed by removing the file os.remove(file) PyMeasure-0.9.0/tests/experiment/data/0000775000175000017500000000000014010046235020114 5ustar colincolin00000000000000PyMeasure-0.9.0/tests/experiment/data/results_for_testing_parameters.csv0000664000175000017500000001125014010042511027150 0ustar colincolin00000000000000#Procedure: .DummyProcedure> #Parameters: # Directory string: /test directory with space/test_filename.csv # checkbox False: False # checkbox True: True # Delay Time: 0.0005 s # Loop Iterations: 101 # Random Seed: 54321 #Data: Iteration,Random Number 0,0.003436653809785084 1,0.0011213975570397716 2,0.9462703288511235 3,0.7104581682479906 4,0.6979967793672998 5,0.7350625767874299 6,0.254069079417218 7,0.0850342956655038 8,0.3513142790695486 9,0.4129347095549948 10,0.9433873118020634 11,0.42632981178504537 12,0.9845394478308614 13,0.4853153744632873 14,0.9322288013684165 15,0.11920215231413334 16,0.36595600877496237 17,0.6880705216020775 18,0.007085791587457813 19,0.04771678417125724 20,0.6204968634936813 21,0.03670164824808697 22,0.16560124399616993 23,0.5910456143539181 24,0.4701602225292316 25,0.38757156171702156 26,0.07066488455792508 27,0.43203406053799454 28,0.45606357237132034 29,0.2386638439542721 30,0.8774550969433488 31,0.5113905252519452 32,0.5649692772307565 33,0.9155700152165492 34,0.2508324769755844 35,0.42404305999302716 36,0.05342908448137029 37,0.6922629049967902 38,0.19162485967625909 39,0.9272775836766017 40,0.10246983780746877 41,0.7567060330812814 42,0.2712753618078092 43,0.9986513830610451 44,0.9581134432254521 45,0.3616759452814807 46,0.6147710755859486 47,0.5163387330879938 48,0.5673654852380584 49,0.1333241618798513 50,0.2052885881887433 51,0.5478649038853718 52,0.024982701054910517 53,0.7587916771531213 54,0.3734970192374186 55,0.2379926539973377 56,0.9552315131807496 57,0.16531174118626124 58,0.29292556844061857 59,0.5513973795966665 60,0.6943835510717676 61,0.14981542970077155 62,0.5862982982401417 63,0.6015254275108515 64,0.5636205820020208 65,0.8066879565904457 66,0.3783539400653987 67,0.5695373634177995 68,0.8265786355540176 69,0.2153466559021835 70,0.4244235048959558 71,0.949708983209304 72,0.2798541310660513 73,0.6830726954191861 74,0.9890450320061706 75,0.7013908727500409 76,0.6895165149856857 77,0.550831154795627 78,0.7622870294471245 79,0.6422449422563139 80,0.47675381181086607 81,0.8535406112193479 82,0.03376294477430797 83,0.2348291609836196 84,0.8965416562370098 85,0.8290787741760914 86,0.04870418373777741 87,0.2704738906523143 88,0.2871755565652039 89,0.1404860662711659 90,0.6208787621956987 91,0.7880681671146791 92,0.8248763722577053 93,0.016491816178995977 94,0.5507881078348331 95,0.8665449320365218 96,0.300059458673104 97,0.7732105026481019 98,0.8123246052159471 99,0.6384258741463413 100,0.7022065356996623 0,0.003436653809785084 1,0.0011213975570397716 2,0.9462703288511235 3,0.7104581682479906 4,0.6979967793672998 5,0.7350625767874299 6,0.254069079417218 7,0.0850342956655038 8,0.3513142790695486 9,0.4129347095549948 10,0.9433873118020634 11,0.42632981178504537 12,0.9845394478308614 13,0.4853153744632873 14,0.9322288013684165 15,0.11920215231413334 16,0.36595600877496237 17,0.6880705216020775 18,0.007085791587457813 19,0.04771678417125724 20,0.6204968634936813 21,0.03670164824808697 22,0.16560124399616993 23,0.5910456143539181 24,0.4701602225292316 25,0.38757156171702156 26,0.07066488455792508 27,0.43203406053799454 28,0.45606357237132034 29,0.2386638439542721 30,0.8774550969433488 31,0.5113905252519452 32,0.5649692772307565 33,0.9155700152165492 34,0.2508324769755844 35,0.42404305999302716 36,0.05342908448137029 37,0.6922629049967902 38,0.19162485967625909 39,0.9272775836766017 40,0.10246983780746877 41,0.7567060330812814 42,0.2712753618078092 43,0.9986513830610451 44,0.9581134432254521 45,0.3616759452814807 46,0.6147710755859486 47,0.5163387330879938 48,0.5673654852380584 49,0.1333241618798513 50,0.2052885881887433 51,0.5478649038853718 52,0.024982701054910517 53,0.7587916771531213 54,0.3734970192374186 55,0.2379926539973377 56,0.9552315131807496 57,0.16531174118626124 58,0.29292556844061857 59,0.5513973795966665 60,0.6943835510717676 61,0.14981542970077155 62,0.5862982982401417 63,0.6015254275108515 64,0.5636205820020208 65,0.8066879565904457 66,0.3783539400653987 67,0.5695373634177995 68,0.8265786355540176 69,0.2153466559021835 70,0.4244235048959558 71,0.949708983209304 72,0.2798541310660513 73,0.6830726954191861 74,0.9890450320061706 75,0.7013908727500409 76,0.6895165149856857 77,0.550831154795627 78,0.7622870294471245 79,0.6422449422563139 80,0.47675381181086607 81,0.8535406112193479 82,0.03376294477430797 83,0.2348291609836196 84,0.8965416562370098 85,0.8290787741760914 86,0.04870418373777741 87,0.2704738906523143 88,0.2871755565652039 89,0.1404860662711659 90,0.6208787621956987 91,0.7880681671146791 92,0.8248763722577053 93,0.016491816178995977 94,0.5507881078348331 95,0.8665449320365218 96,0.300059458673104 97,0.7732105026481019 98,0.8123246052159471 99,0.6384258741463413 100,0.7022065356996623 PyMeasure-0.9.0/tests/experiment/data/results_for_testing.csv0000644000175000017500000000000013640137321024726 0ustar colincolin00000000000000PyMeasure-0.9.0/tests/experiment/data/__init__.py0000644000175000017500000000000014010032216022203 0ustar colincolin00000000000000PyMeasure-0.9.0/tests/experiment/data/procedure_for_testing.py0000664000175000017500000000370514010037617025072 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.experiment import ( Procedure, IntegerParameter, Parameter, FloatParameter ) import random from time import sleep class RandomProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=100) delay = FloatParameter('Delay Time', units='s', default=0.001) seed = Parameter('Random Seed', default='12345') DATA_COLUMNS = ['Iteration', 'Random Number'] def startup(self): random.seed(self.seed) def execute(self): for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } self.emit('results', data) self.emit('progress', 100.*i/self.iterations) sleep(self.delay) if self.should_stop(): break PyMeasure-0.9.0/tests/experiment/test_procedure.py0000664000175000017500000000406014010037617022610 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest import pickle from pymeasure.experiment.procedure import Procedure, ProcedureWrapper from pymeasure.experiment.parameters import Parameter from data.procedure_for_testing import RandomProcedure def test_parameters(): class TestProcedure(Procedure): x = Parameter('X', default=5) p = TestProcedure() assert p.x == 5 p.x = 10 assert p.x == 10 assert p.parameters_are_set() objs = p.parameter_objects() assert 'x' in objs assert objs['x'].value == p.x # TODO: Add tests for measureables def test_procedure_wrapper(): assert RandomProcedure.iterations.value == 100 procedure = RandomProcedure() procedure.iterations = 101 wrapper = ProcedureWrapper(procedure) new_wrapper = pickle.loads(pickle.dumps(wrapper)) assert hasattr(new_wrapper, 'procedure') assert new_wrapper.procedure.iterations == 101 assert RandomProcedure.iterations.value == 100 PyMeasure-0.9.0/tests/experiment/test_parameters.py0000664000175000017500000001235414010042511022755 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest from pymeasure.experiment.parameters import Parameter from pymeasure.experiment.parameters import IntegerParameter from pymeasure.experiment.parameters import BooleanParameter from pymeasure.experiment.parameters import FloatParameter from pymeasure.experiment.parameters import ListParameter from pymeasure.experiment.parameters import VectorParameter def test_parameter_default(): p = Parameter('Test', default=5) assert p.value == 5 def test_integer_units(): p = IntegerParameter('Test', units='V') assert p.units == 'V' def test_integer_value(): p = IntegerParameter('Test', units='tests') with pytest.raises(ValueError): v = p.value # not set with pytest.raises(ValueError): p.value = 'a' # not an integer p.value = 0.5 # a float assert p.value == 0 p.value = False # a boolean assert p.value == 0 p.value = 10 assert p.value == 10 p.value = '5' assert p.value == 5 p.value = '11 tests' assert p.value == 11 assert p.units == 'tests' with pytest.raises(ValueError): p.value = '31 incorrect units' # not the correct units def test_integer_bounds(): p = IntegerParameter('Test', minimum=0, maximum=10) p.value = 10 assert p.value == 10 with pytest.raises(ValueError): p.value = 100 # above maximum with pytest.raises(ValueError): p.value = -100 # below minimum def test_boolean_value(): p = BooleanParameter('Test') with pytest.raises(ValueError): v = p.value # not set with pytest.raises(ValueError): p.value = 'a' # a string with pytest.raises(ValueError): p.value = 10 # a number other than 0 or 1 p.value = "True" assert p.value == True p.value = "False" assert p.value == False p.value = "true" assert p.value == True p.value = "false" assert p.value == False p.value = 1 # a number assert p.value == True p.value = 0 # zero assert p.value == False p.value = True assert p.value == True def test_float_value(): p = FloatParameter('Test', units='tests') with pytest.raises(ValueError): v = p.value # not set with pytest.raises(ValueError): p.value = 'a' # not a float p.value = False # boolean assert p.value == 0.0 p.value = 100 assert p.value == 100.0 p.value = '1.06' assert p.value == 1.06 p.value = '11.3 tests' assert p.value == 11.3 assert p.units == 'tests' with pytest.raises(ValueError): p.value = '31.3 incorrect units' # not the correct units def test_float_bounds(): p = FloatParameter('Test', minimum=0.1, maximum=0.5) p.value = 0.3 assert p.value == 0.3 with pytest.raises(ValueError): p.value = 10 # above maximum with pytest.raises(ValueError): p.value = -10 # below minimum def test_list_value(): # TODO: check against setting the string version of the numeric choices p = ListParameter('Test', choices=[1, 2.2, 'three', 'and four']) p.value = 1 assert p.value == 1 p.value = 2.2 assert p.value == 2.2 p.value = 'three' assert p.value == 'three' p.value = 'and four' assert p.value == 'and four' with pytest.raises(ValueError): p.value = 5 def test_list_value_with_units(): # TODO: check against setting the string version (with units) of the numeric choices p = ListParameter('Test', choices=[1, 2.2, 'three', 'and four'], units='tests') p.value = 'three tests' assert p.value == 'three' p.value = 'and four tests' assert p.value == 'and four' def test_vector(): p = VectorParameter('test', length=3, units='tests') p.value = [1, 2, 3] assert p.value == [1, 2, 3] p.value = '[4, 5, 6]' assert p.value == [4, 5, 6] p.value = '[7, 8, 9] tests' assert p.value == [7, 8, 9] with pytest.raises(ValueError): p.value = '[0, 1, 2] wrong unit' with pytest.raises(ValueError): p.value = [1, 2] with pytest.raises(ValueError): p.value = ['a', 'b'] with pytest.raises(ValueError): p.value = '0, 1, 2' # TODO: Add tests for Measurable PyMeasure-0.9.0/tests/experiment/test_listeners.py0000664000175000017500000000274614010037617022641 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import time from queue import Queue from pymeasure.experiment.listeners import Listener, Recorder from pymeasure.experiment.results import Results # TODO: Make results_for_testing.csv # TODO: Make procedure_for_testing.py """ def test_recorder_stop(): q = Queue() d = Results.load('results_for_testing.csv') r = Recorder(d, q) r. """ PyMeasure-0.9.0/tests/experiment/test_results.py0000664000175000017500000001261614010042511022314 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pytest from unittest import mock import os import tempfile import pickle from importlib.machinery import SourceFileLoader import pandas as pd import numpy as np from pymeasure.experiment.results import Results, CSVFormatter from pymeasure.experiment.procedure import Procedure, Parameter from pymeasure.experiment import BooleanParameter # Load the procedure, without it being in a module #data_path = os.path.join(os.path.dirname(__file__), 'data/procedure_for_testing.py') #RandomProcedure = SourceFileLoader('procedure', data_path).load_module().RandomProcedure from data.procedure_for_testing import RandomProcedure def test_procedure(): """ Ensure that the loaded test procedure is properly functioning """ p = RandomProcedure() assert p.iterations == 100 assert hasattr(p, 'execute') def test_csv_formatter_format_header(): """Tests CSVFormatter.format_header() method.""" columns = ['t', 'x', 'y', 'z', 'V'] formatter = CSVFormatter(columns=columns) assert formatter.format_header() == 't,x,y,z,V' def test_csv_formatter_format(): """Tests CSVFormatter.format() method.""" columns = ['t', 'x', 'y', 'z', 'V'] formatter = CSVFormatter(columns=columns) data = {'t': 1, 'y': 2, 'z': 3.0, 'x': -1, 'V': 'abc'} assert formatter.format(data) == '1,-1,2,3.0,abc' def test_procedure_wrapper(): assert RandomProcedure.iterations.value == 100 procedure = RandomProcedure() procedure.iterations = 101 file = tempfile.mktemp() results = Results(procedure, file) new_results = pickle.loads(pickle.dumps(results)) assert hasattr(new_results, 'procedure') assert new_results.procedure.iterations == 101 assert RandomProcedure.iterations.value == 100 class TestResults: # TODO: add a full set of Results tests @mock.patch('pymeasure.experiment.results.open', mock.mock_open(), create=True) @mock.patch('os.path.exists', return_value=True) @mock.patch('pymeasure.experiment.results.pd.read_csv') def test_regression_attr_data_when_up_to_date_should_retain_dtype(self, read_csv_mock, path_exists_mock): procedure_mock = mock.MagicMock(spec=Procedure) result = Results(procedure_mock, 'test.csv') read_csv_mock.return_value = [pd.DataFrame(data={ 'A': [1,2,3,4,5,6,7], 'B': [2,3,4,5,6,7,8] })] first_data = result.data # if no updates, read_csv returns a zero-row dataframe read_csv_mock.return_value = [pd.DataFrame(data={ 'A': [], 'B': [] }, dtype=object)] second_data = result.data assert second_data.iloc[:,0].dtype is not object assert first_data.iloc[:,0].dtype is second_data.iloc[:,0].dtype def test_regression_param_str_should_not_include_newlines(self, tmpdir): class DummyProcedure(Procedure): par = Parameter('Generic Parameter with newline chars') DATA_COLUMNS = ['Foo', 'Bar', 'Baz'] procedure = DummyProcedure() procedure.par = np.linspace(1,100,17) filename = os.path.join(str(tmpdir), 'header_linebreak_test.csv') result = Results(procedure, filename) result.reload() # assert no error pd.read_csv(filename, comment="#") # assert no error assert (result.parameters['par'].value == np.linspace(1,100,17)).all() def test_parameter_reading(): data_path = os.path.join(os.path.dirname(__file__), "data/results_for_testing_parameters.csv") test_string = "/test directory with space/test_filename.csv" iterations = 101 delay = 0.0005 seed = '54321' class DummyProcedure(RandomProcedure): check_false = BooleanParameter('checkbox False') check_true = BooleanParameter('checkbox True') check_dir = Parameter('Directory string') results = Results.load(data_path, procedure_class=DummyProcedure) # Check if all parameters are correctly read from file assert results.parameters["iterations"].value == iterations assert results.parameters["delay"].value == delay assert results.parameters["seed"].value == seed assert results.parameters["check_true"].value == True assert results.parameters["check_false"].value == False assert results.parameters["check_dir"].value == test_string PyMeasure-0.9.0/setup.py0000664000175000017500000000554014010046171015376 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from setuptools import setup, find_packages setup( name='PyMeasure', version='0.9.0', author='PyMeasure Developers', packages=find_packages(), scripts=[], url='https://github.com/pymeasure/pymeasure', download_url='https://github.com/pymeasure/pymeasure/tarball/v0.9.0', license='MIT License', description='Scientific measurement library for instruments, experiments, and live-plotting', long_description=open('README.rst').read() + "\n\n" + open('CHANGES.txt').read(), install_requires=[ "numpy >= 1.6.1", "pandas >= 0.14", "pyvisa >= 1.8", "pyserial >= 2.7", "pyqtgraph >= 0.9.10" ], extras_require={ 'matplotlib': ['matplotlib >= 2.0.2'], 'tcp': [ 'zmq >= 16.0.2', 'cloudpickle >= 0.3.1' ], 'python-vxi11': ['python-vxi11 >= 0.9'] }, setup_requires=[ 'pytest-runner' ], tests_require=[ 'pytest >= 2.9.1', 'pytest-qt >= 2.4.0', 'pyvisa-sim >= 0.4.0', ], classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering", ], keywords="measure instrument experiment control automate graph plot" ) PyMeasure-0.9.0/setup.cfg0000644000175000017500000000007714010046235015504 0ustar colincolin00000000000000[aliases] test = pytest [egg_info] tag_build = tag_date = 0 PyMeasure-0.9.0/CHANGES.txt0000664000175000017500000002220214010046171015467 0ustar colincolin00000000000000Version 0.9 -- released 2/7/21 ============================== - PyMeasure is now officially at github.com/pymeasure/pymeasure - Python 3.9 is now supported, Python 3.5 removed due to EOL - Move to GitHub Actions from TravisCI and Appveyor for CI (@bilderbuchi) - New additions to Oxford Instruments ITC 503 (@CasperSchippers) - New Agilent 34450A and Keysight DSOX1102G instruments (@theMashUp, @jlarochelle) - Improvements to NI VirtualBench (@moritzj29) - New Agilent B1500 instrument (@moritzj29) - New Keithley 6517B instrument (@wehlgrundspitze) - Major improvements to PyVISA compatbility (@bilderbuchi, @msmttchr, @CasperSchippers, @cjermain) - New Anapico APSIN12G instrument (@StePhanino) - Improvements to Thorelabs Pro 8000 and SR830 (@Mike-HubGit) - New SR860 instrument (@StevenSiegl, @bklebel) - Fix to escape sequences (@tirkarthi) - New directory input for ManagedWindow (@paulgoulain) - New TelnetAdapter and Attocube ANC300 Piezo controller (@dkriegner) - New Agilent 34450A (@theMashUp) - New Razorbill RP100 strain cell controller (@pheowl) - Fixes to precision and default value of ScientificInput and FloatParameter (@moritzj29) - Fixes for Keithly 2400 and 2450 controls (@pyMatJ) - Improvments to Inputs and open_file_externally (@msmttchr) - Fixes to Agilent 8722ES (@alexmcnabb) - Fixes to QThread cleanup (@neal-kepler, @msmttchr) - Fixes to Keyboard interrupt, and parameters (@CasperSchippers) Version 0.8 -- released 3/29/19 =============================== - Python 3.8 is now supported - New Measurement Sequencer allows for running over a large parameter space (@CasperSchippers) - New image plotting feature for live image measurements (@jmittelstaedt) - Improvements to VISA adapter (@moritzj29) - Added Tektronix AFG 3000, Keithley 2750 (@StePhanino, @dennisfeng2) - Documentation improvements (@mivade) - Fix to ScientificInput for float strings (@moritzj29) - New validator: strict_discrete_range (@moritzj29) - Improvements to Recorder thread joining - Migrating the ReadtheDocs configuration to version 2 - National Instruments Virtual Bench initial support (@moritzj29) Version 0.7 -- released 8/4/19 ============================== - Dropped support for Python 3.4, adding support for Python 3.7 - Significant improvements to CI, dependencies, and conda environment (@bilderbuchi, @cjermain) - Fix for PyQT issue in ResultsDialog (@CasperSchippers) - Fix for wire validator in Keithley 2400 (@Fattotora) - Addition of source_enabled control for Keithley 2400 (@dennisfeng2) - Time constant fix and input controls for SR830 (@dennisfeng2) - Added Keithley 2450 and Agilent 33521A (@hlgirard, @Endever42) - Proper escaping support in CSV headers (@feph) - Minor updates (@dvase) Version 0.6.1 -- released 4/21/19 ================================= - Added Elektronica SM70-45D, Agilent 33220A, and Keysight N5767A instruments (@CasperSchippers, @sumatrae) - Fixes for Prologix adapter and Keithley 2400 (@hlgirard, @ronan-sensome) - Improved support for SRS SR830 (@CasperSchippers) Version 0.6 -- released 1/14/19 =============================== - New VXI11 Adapter for ethernet instruments (@chweiser) - PyQt updates to 5.6.0 - Added SRS SG380, Ametek 7270, Agilent 4156, HP 34401A, Advantest R3767CG, and Oxford ITC503 instrustruments (@sylkar, @jmittelstaedt, @vik-s, @troylf, @CasperSchippers) - Updates to Keithley 2000, Agilent 8257D, ESP 300, and Keithley 2400 instruments (@watersjason, @jmittelstaedt, @nup002) - Various minor bug fixes (@thosou) Version 0.5.1 -- released 4/14/18 ================================= - Minor versions of PyVISA are now properly handled - Documentation improvements (@Laogeodritt and @ederag) - Instruments now have `set_process` capability (@bilderbuchi) - Plotter now uses threads (@dvspirito) - Display inputs and PlotItem improvements (@Laogeodritt) Version 0.5 -- released 10/18/17 ================================ - Threads are used by default, eliminating multiprocessing issues with spawn - Enhanced unit tests for threading - Sphinx Doctests are added to the documentation (@bilderbuchi) - Improvements to documentation (@JuMaD) Version 0.4.6 -- released 8/12/17 ================================= - Reverted multiprocessing start method keyword arguments to fix Unix spawn issues (@ndr37) - Fixes to regressions in Results writing (@feinsteinben) - Fixes to TCP support using cloudpickle (@feinsteinben) - Restructing of unit test framework Version 0.4.5 -- released 7/4/17 ================================ - Recorder and Scribe now leverage QueueListener (@feinsteinben) - PrologixAdapter and SerialAdapter now handle Serial objects as adapters (@feinsteinben) - Optional TCP support now uses cloudpickle for serialization (@feinsteinben) - Significant PEP8 review and bug fixes (@feinsteinben) - Includes docs in the code distribution (@ghisvail) - Continuous integration support for Python 3.6 (@feinsteinben) Version 0.4.4 -- released 6/4/17 ================================ - Fix pip install for non-wheel builds - Update to Agilent E4980 (@dvspirito) - Minor fixes for docs, tests, and formatting (@ghisvail, @feinsteinben) Version 0.4.3 -- released 3/30/17 ================================= - Added Agilent E4980, AMI 430, Agilent 34410A, Thorlabs PM100, and Anritsu MS9710C instruments (@TvBMcMaster, @dvspirito, and @mhdg) - Updates to PyVISA support (@minhhaiphys) - Initial work on resource manager (@dvspirito) - Fixes for Prologix adapter that allow read-write delays (@TvBMcMaster) - Fixes for conda environment on continuous integration Version 0.4.2 -- released 8/23/16 ================================= - New instructions for installing with Anaconda and conda-forge package (thanks @melund!) - Bug-fixes to the Keithley 2000, SR830, and Agilent E4408B - Re-introduced the Newport ESP300 motion controller - Major update to the Keithely 2400, 2000 and Yokogawa 7651 to achieve a common interface - New command-string processing hooks for Instrument property functions - Updated LakeShore 331 temperature controller with new features - Updates to the Agilent 8257D signal generator for better feature exposure Version 0.4.1 -- released 7/31/16 ================================= - Critical fix in setup.py for importing instruments (also added to documentation) Version 0.4 -- released 7/29/16 =============================== - Replaced Instrument add_measurement and add_control with measurement and control functions - Added validators to allow Instrument.control to match restricted ranges - Added mapping to Instrument.control to allow more flexible inputs - Conda is now used to set up the Python environment - macOS testing in continuous integration - Major updates to the documentation Version 0.3 -- released 4/8/16 ============================== - Added IPython (Jupyter) notebook support with significant features - Updated set of example scripts and notebooks - New PyMeasure logo released - Removed support for Python <3.4 - Changed multiprocessing to use spawn for compatibility - Significant work on the documentation - Added initial tests for non-instrument code - Continuous integration setup for Linux and Windows Version 0.2 -- released 12/16/15 ================================ - Python 3 compatibility, removed support for Python 2 - Considerable renaming for better PEP8 compliance - Added MIT License - Major restructuring of the package to break it into smaller modules - Major rewrite of display functionality, introducing new Qt objects for easy extensions - Major rewrite of procedure execution, now using a Worker process which takes advantage of multi-core CPUs - Addition of a number of examples - New methods for listening to Procedures, introducing ZMQ for TCP connectivity - Updates to Keithley2400 and VISAAdapter Version 0.1.6 -- released 4/19/15 ================================= - Renamed the package to PyMeasure from Automate to be more descriptive about its purpose - Addition of VectorParameter to allow vectors to be input for Procedures - Minor fixes for the Results and Danfysik8500 Version 0.1.5 -- release 10/22/14 ================================= - New Manager class for handling Procedures in a queue fashion - New Browser that works in tandem with the Manager to display the queue - Bug fixes for Results loading Version 0.1.4 -- released 8/2/14 ================================ - Integrated Results class into display and file writing - Bug fixes for Listener classes - Bug fixes for SR830 Version 0.1.3 -- released 7/20/14 ================================= - Replaced logging system with Python logging package - Added data management (Results) and bug fixes for Procedures and Parameters - Added pandas v0.14 to requirements for data management - Added data listeners, Qt4 and PyQtGraph helpers Version 0.1.2 -- released 7/18/14 ================================= - Bug fixes to LakeShore 425 - Added new Procedure and Parameter classes for generic experiments - Added version number in package Version 0.1.1 -- released 7/16/14 ================================= - Bug fixes to PrologixAdapter, VISAAdapter, Agilent 8722ES, Agilent 8257D, Stanford SR830, Danfysik8500 - Added Tektronix TDS 2000 with basic functionality - Fixed Danfysik communication to handle errors properly Version 0.1.0 -- released 7/15/14 ================================= - Initial release PyMeasure-0.9.0/PyMeasure.egg-info/0000775000175000017500000000000014010046235017265 5ustar colincolin00000000000000PyMeasure-0.9.0/PyMeasure.egg-info/dependency_links.txt0000644000175000017500000000000114010046235023331 0ustar colincolin00000000000000 PyMeasure-0.9.0/PyMeasure.egg-info/top_level.txt0000644000175000017500000000001214010046235022006 0ustar colincolin00000000000000pymeasure PyMeasure-0.9.0/PyMeasure.egg-info/SOURCES.txt0000644000175000017500000002442014010046235021151 0ustar colincolin00000000000000AUTHORS.txt CHANGES.txt LICENSE.txt MANIFEST.in README.rst setup.cfg setup.py PyMeasure.egg-info/PKG-INFO PyMeasure.egg-info/SOURCES.txt PyMeasure.egg-info/dependency_links.txt PyMeasure.egg-info/requires.txt PyMeasure.egg-info/top_level.txt docs/Makefile docs/conf.py docs/index.rst docs/introduction.rst docs/make.bat docs/quick_start.rst docs/about/authors.rst docs/about/license.rst docs/api/adapters.rst docs/api/display/Qt.rst docs/api/display/browser.rst docs/api/display/curves.rst docs/api/display/index.rst docs/api/display/inputs.rst docs/api/display/listeners.rst docs/api/display/log.rst docs/api/display/manager.rst docs/api/display/plotter.rst docs/api/display/thread.rst docs/api/display/widgets.rst docs/api/display/windows.rst docs/api/experiment/experiment.rst docs/api/experiment/index.rst docs/api/experiment/listeners.rst docs/api/experiment/parameters.rst docs/api/experiment/procedure.rst docs/api/experiment/results.rst docs/api/experiment/workers.rst docs/api/instruments/comedi.rst docs/api/instruments/index.rst docs/api/instruments/instruments.rst docs/api/instruments/resources.rst docs/api/instruments/validators.rst docs/api/instruments/advantest/advantestR3767CG.rst docs/api/instruments/advantest/index.rst docs/api/instruments/agilent/agilent33220A.rst docs/api/instruments/agilent/agilent33500.rst docs/api/instruments/agilent/agilent33521A.rst docs/api/instruments/agilent/agilent34410A.rst docs/api/instruments/agilent/agilent34450A.rst docs/api/instruments/agilent/agilent4156.rst docs/api/instruments/agilent/agilent8257D.rst docs/api/instruments/agilent/agilent8722ES.rst docs/api/instruments/agilent/agilentB1500.rst docs/api/instruments/agilent/agilentE4408B.rst docs/api/instruments/agilent/agilentE4980.rst docs/api/instruments/agilent/index.rst docs/api/instruments/ametek/ametek7270.rst docs/api/instruments/ametek/index.rst docs/api/instruments/ami/ami430.rst docs/api/instruments/ami/index.rst docs/api/instruments/anapico/apsin12G.rst docs/api/instruments/anapico/index.rst docs/api/instruments/anritsu/anritsuMG3692C.rst docs/api/instruments/anritsu/anritsuMS9710C.rst docs/api/instruments/anritsu/index.rst docs/api/instruments/attocube/adapters.rst docs/api/instruments/attocube/anc300.rst docs/api/instruments/attocube/index.rst docs/api/instruments/danfysik/adapters.rst docs/api/instruments/danfysik/danfysik8500.rst docs/api/instruments/danfysik/index.rst docs/api/instruments/deltaelektronica/index.rst docs/api/instruments/deltaelektronica/sm7045d.rst docs/api/instruments/fwbell/fwbell5080.rst docs/api/instruments/fwbell/index.rst docs/api/instruments/hp/hp33120A.rst docs/api/instruments/hp/hp34401A.rst docs/api/instruments/hp/index.rst docs/api/instruments/keithley/index.rst docs/api/instruments/keithley/keithley2000.rst docs/api/instruments/keithley/keithley2400.rst docs/api/instruments/keithley/keithley2450.rst docs/api/instruments/keithley/keithley2700.rst docs/api/instruments/keithley/keithley2750.rst docs/api/instruments/keithley/keithley6221.rst docs/api/instruments/keithley/keithley6517b.rst docs/api/instruments/keysight/index.rst docs/api/instruments/keysight/keysightDSOX1102G.rst docs/api/instruments/keysight/keysightN5767A.rst docs/api/instruments/lakeshore/adapters.rst docs/api/instruments/lakeshore/index.rst docs/api/instruments/lakeshore/lakeshore331.rst docs/api/instruments/lakeshore/lakeshore425.rst docs/api/instruments/newport/esp300.rst docs/api/instruments/newport/index.rst docs/api/instruments/ni/index.rst docs/api/instruments/ni/virtualbench.rst docs/api/instruments/oxfordinstruments/ITC503.rst docs/api/instruments/oxfordinstruments/index.rst docs/api/instruments/parker/index.rst docs/api/instruments/parker/parkerGV6.rst docs/api/instruments/razorbill/index.rst docs/api/instruments/razorbill/razorbillRP100.rst docs/api/instruments/signalrecovery/dsp7265.rst docs/api/instruments/signalrecovery/index.rst docs/api/instruments/srs/index.rst docs/api/instruments/srs/sr830.rst docs/api/instruments/srs/sr860.rst docs/api/instruments/tektronix/afg3152c.rst docs/api/instruments/tektronix/index.rst docs/api/instruments/tektronix/tds2000.rst docs/api/instruments/thorlabs/index.rst docs/api/instruments/thorlabs/thorlabspm100usb.rst docs/api/instruments/thorlabs/thorlabspro8000.rst docs/api/instruments/yokogawa/index.rst docs/api/instruments/yokogawa/yokogawa7651.rst docs/dev/adding_instruments.rst docs/dev/coding_standards.rst docs/dev/contribute.rst docs/dev/reporting_errors.rst docs/images/PyMeasure logo.png docs/images/PyMeasure logo.svg docs/images/PyMeasure preview.png docs/images/PyMeasure preview.svg docs/images/PyMeasure.png docs/images/PyMeasure.svg docs/tutorial/connecting.rst docs/tutorial/graphical.rst docs/tutorial/gui_sequencer_example_sequence.txt docs/tutorial/index.rst docs/tutorial/procedure.rst docs/tutorial/pymeasure-directoryinput.png docs/tutorial/pymeasure-managedwindow-queued.png docs/tutorial/pymeasure-managedwindow-resume.png docs/tutorial/pymeasure-managedwindow-running.png docs/tutorial/pymeasure-managedwindow.png docs/tutorial/pymeasure-plotter.png docs/tutorial/pymeasure-sequencer.png pymeasure/__init__.py pymeasure/console.py pymeasure/errors.py pymeasure/log.py pymeasure/process.py pymeasure/thread.py pymeasure/adapters/__init__.py pymeasure/adapters/adapter.py pymeasure/adapters/prologix.py pymeasure/adapters/serial.py pymeasure/adapters/telnet.py pymeasure/adapters/visa.py pymeasure/adapters/vxi11.py pymeasure/display/Qt.py pymeasure/display/__init__.py pymeasure/display/browser.py pymeasure/display/curves.py pymeasure/display/inputs.py pymeasure/display/listeners.py pymeasure/display/log.py pymeasure/display/manager.py pymeasure/display/plotter.py pymeasure/display/thread.py pymeasure/display/widgets.py pymeasure/display/windows.py pymeasure/experiment/__init__.py pymeasure/experiment/config.py pymeasure/experiment/experiment.py pymeasure/experiment/listeners.py pymeasure/experiment/parameters.py pymeasure/experiment/procedure.py pymeasure/experiment/results.py pymeasure/experiment/workers.py pymeasure/instruments/__init__.py pymeasure/instruments/comedi.py pymeasure/instruments/instrument.py pymeasure/instruments/mock.py pymeasure/instruments/resources.py pymeasure/instruments/validators.py pymeasure/instruments/advantest/__init__.py pymeasure/instruments/advantest/advantestR3767CG.py pymeasure/instruments/agilent/__init__.py pymeasure/instruments/agilent/agilent33220A.py pymeasure/instruments/agilent/agilent33500.py pymeasure/instruments/agilent/agilent33521A.py pymeasure/instruments/agilent/agilent34410A.py pymeasure/instruments/agilent/agilent34450A.py pymeasure/instruments/agilent/agilent4156.py pymeasure/instruments/agilent/agilent8257D.py pymeasure/instruments/agilent/agilent8722ES.py pymeasure/instruments/agilent/agilentB1500.py pymeasure/instruments/agilent/agilentE4408B.py pymeasure/instruments/agilent/agilentE4980.py pymeasure/instruments/ametek/__init__.py pymeasure/instruments/ametek/ametek7270.py pymeasure/instruments/ami/__init__.py pymeasure/instruments/ami/ami430.py pymeasure/instruments/anapico/__init__.py pymeasure/instruments/anapico/apsin12G.py pymeasure/instruments/anritsu/__init__.py pymeasure/instruments/anritsu/anritsuMG3692C.py pymeasure/instruments/anritsu/anritsuMS9710C.py pymeasure/instruments/attocube/__init__.py pymeasure/instruments/attocube/adapters.py pymeasure/instruments/attocube/anc300.py pymeasure/instruments/danfysik/__init__.py pymeasure/instruments/danfysik/adapters.py pymeasure/instruments/danfysik/danfysik8500.py pymeasure/instruments/deltaelektronika/__init__.py pymeasure/instruments/deltaelektronika/sm7045d.py pymeasure/instruments/fwbell/__init__.py pymeasure/instruments/fwbell/fwbell5080.py pymeasure/instruments/hp/__init__.py pymeasure/instruments/hp/hp33120A.py pymeasure/instruments/hp/hp34401A.py pymeasure/instruments/keithley/__init__.py pymeasure/instruments/keithley/buffer.py pymeasure/instruments/keithley/keithley2000.py pymeasure/instruments/keithley/keithley2400.py pymeasure/instruments/keithley/keithley2450.py pymeasure/instruments/keithley/keithley2700.py pymeasure/instruments/keithley/keithley2750.py pymeasure/instruments/keithley/keithley6221.py pymeasure/instruments/keithley/keithley6517b.py pymeasure/instruments/keysight/__init__.py pymeasure/instruments/keysight/keysightDSOX1102G.py pymeasure/instruments/keysight/keysightN5767A.py pymeasure/instruments/lakeshore/__init__.py pymeasure/instruments/lakeshore/adapters.py pymeasure/instruments/lakeshore/lakeshore331.py pymeasure/instruments/lakeshore/lakeshore425.py pymeasure/instruments/newport/__init__.py pymeasure/instruments/newport/esp300.py pymeasure/instruments/ni/__init__.py pymeasure/instruments/ni/daqmx.py pymeasure/instruments/ni/nidaq.py pymeasure/instruments/ni/virtualbench.py pymeasure/instruments/oxfordinstruments/__init__.py pymeasure/instruments/oxfordinstruments/itc503.py pymeasure/instruments/parker/__init__.py pymeasure/instruments/parker/parkerGV6.py pymeasure/instruments/razorbill/__init__.py pymeasure/instruments/razorbill/razorbillRP100.py pymeasure/instruments/signalrecovery/__init__.py pymeasure/instruments/signalrecovery/dsp7265.py pymeasure/instruments/srs/__init__.py pymeasure/instruments/srs/sg380.py pymeasure/instruments/srs/sr830.py pymeasure/instruments/srs/sr860.py pymeasure/instruments/tektronix/__init__.py pymeasure/instruments/tektronix/afg3152c.py pymeasure/instruments/tektronix/tds2000.py pymeasure/instruments/thorlabs/__init__.py pymeasure/instruments/thorlabs/thorlabspm100usb.py pymeasure/instruments/thorlabs/thorlabspro8000.py pymeasure/instruments/yokogawa/__init__.py pymeasure/instruments/yokogawa/yokogawa7651.py tests/conftest.py tests/test_log.py tests/test_process.py tests/test_thread.py tests/adapters/test_adapter.py tests/adapters/test_visa.py tests/display/test_inputs.py tests/display/test_plotter.py tests/display/test_windows.py tests/experiment/test_listeners.py tests/experiment/test_parameters.py tests/experiment/test_procedure.py tests/experiment/test_results.py tests/experiment/test_workers.py tests/experiment/data/__init__.py tests/experiment/data/procedure_for_testing.py tests/experiment/data/results_for_testing.csv tests/experiment/data/results_for_testing_parameters.csv tests/instruments/test_instrument.py tests/instruments/test_validators.py tests/instruments/agilent/test_agilent34450A.py tests/instruments/keithley/test_keithley2750.py tests/instruments/keysight/test_keysightDSOX1102G.pyPyMeasure-0.9.0/PyMeasure.egg-info/requires.txt0000644000175000017500000000025614010046235021666 0ustar colincolin00000000000000numpy>=1.6.1 pandas>=0.14 pyvisa>=1.8 pyserial>=2.7 pyqtgraph>=0.9.10 [matplotlib] matplotlib>=2.0.2 [python-vxi11] python-vxi11>=0.9 [tcp] zmq>=16.0.2 cloudpickle>=0.3.1 PyMeasure-0.9.0/PyMeasure.egg-info/PKG-INFO0000644000175000017500000003421114010046235020361 0ustar colincolin00000000000000Metadata-Version: 2.1 Name: PyMeasure Version: 0.9.0 Summary: Scientific measurement library for instruments, experiments, and live-plotting Home-page: https://github.com/pymeasure/pymeasure Author: PyMeasure Developers License: MIT License Download-URL: https://github.com/pymeasure/pymeasure/tarball/v0.9.0 Description: .. image:: https://raw.githubusercontent.com/pymeasure/pymeasure/master/docs/images/PyMeasure.png :alt: PyMeasure Scientific package PyMeasure scientific package ############################ PyMeasure makes scientific measurements easy to set up and run. The package contains a repository of instrument classes and a system for running experiment procedures, which provides graphical interfaces for graphing live data and managing queues of experiments. Both parts of the package are independent, and when combined provide all the necessary requirements for advanced measurements with only limited coding. PyMeasure is currently under active development, so please report any issues you experience to our `Issues page`_. .. _Issues page: https://github.com/pymeasure/pymeasure/issues PyMeasure runs on Python 3.6, 3.7, 3.8 and 3.9, and is tested with continous-integration on Linux, macOS, and Windows. .. image:: https://github.com/pymeasure/pymeasure/workflows/Pymeasure%20CI/badge.svg :target: https://github.com/pymeasure/pymeasure/actions .. image:: http://readthedocs.org/projects/pymeasure/badge/?version=latest :target: http://pymeasure.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3732545.svg :target: https://doi.org/10.5281/zenodo.3732545 .. image:: https://anaconda.org/conda-forge/pymeasure/badges/version.svg :target: https://anaconda.org/conda-forge/pymeasure .. image:: https://anaconda.org/conda-forge/pymeasure/badges/downloads.svg :target: https://anaconda.org/conda-forge/pymeasure Quick start =========== Check out `the documentation`_ for the `quick start guide`_, that covers the installation of Python and PyMeasure. There are a number of examples in the `examples`_ directory that can help you get up and running. .. _the documentation: http://pymeasure.readthedocs.org/en/latest/ .. _quick start guide: http://pymeasure.readthedocs.io/en/latest/quick_start.html .. _examples: https://github.com/pymeasure/pymeasure/tree/master/examples Version 0.9 -- released 2/7/21 ============================== - PyMeasure is now officially at github.com/pymeasure/pymeasure - Python 3.9 is now supported, Python 3.5 removed due to EOL - Move to GitHub Actions from TravisCI and Appveyor for CI (@bilderbuchi) - New additions to Oxford Instruments ITC 503 (@CasperSchippers) - New Agilent 34450A and Keysight DSOX1102G instruments (@theMashUp, @jlarochelle) - Improvements to NI VirtualBench (@moritzj29) - New Agilent B1500 instrument (@moritzj29) - New Keithley 6517B instrument (@wehlgrundspitze) - Major improvements to PyVISA compatbility (@bilderbuchi, @msmttchr, @CasperSchippers, @cjermain) - New Anapico APSIN12G instrument (@StePhanino) - Improvements to Thorelabs Pro 8000 and SR830 (@Mike-HubGit) - New SR860 instrument (@StevenSiegl, @bklebel) - Fix to escape sequences (@tirkarthi) - New directory input for ManagedWindow (@paulgoulain) - New TelnetAdapter and Attocube ANC300 Piezo controller (@dkriegner) - New Agilent 34450A (@theMashUp) - New Razorbill RP100 strain cell controller (@pheowl) - Fixes to precision and default value of ScientificInput and FloatParameter (@moritzj29) - Fixes for Keithly 2400 and 2450 controls (@pyMatJ) - Improvments to Inputs and open_file_externally (@msmttchr) - Fixes to Agilent 8722ES (@alexmcnabb) - Fixes to QThread cleanup (@neal-kepler, @msmttchr) - Fixes to Keyboard interrupt, and parameters (@CasperSchippers) Version 0.8 -- released 3/29/19 =============================== - Python 3.8 is now supported - New Measurement Sequencer allows for running over a large parameter space (@CasperSchippers) - New image plotting feature for live image measurements (@jmittelstaedt) - Improvements to VISA adapter (@moritzj29) - Added Tektronix AFG 3000, Keithley 2750 (@StePhanino, @dennisfeng2) - Documentation improvements (@mivade) - Fix to ScientificInput for float strings (@moritzj29) - New validator: strict_discrete_range (@moritzj29) - Improvements to Recorder thread joining - Migrating the ReadtheDocs configuration to version 2 - National Instruments Virtual Bench initial support (@moritzj29) Version 0.7 -- released 8/4/19 ============================== - Dropped support for Python 3.4, adding support for Python 3.7 - Significant improvements to CI, dependencies, and conda environment (@bilderbuchi, @cjermain) - Fix for PyQT issue in ResultsDialog (@CasperSchippers) - Fix for wire validator in Keithley 2400 (@Fattotora) - Addition of source_enabled control for Keithley 2400 (@dennisfeng2) - Time constant fix and input controls for SR830 (@dennisfeng2) - Added Keithley 2450 and Agilent 33521A (@hlgirard, @Endever42) - Proper escaping support in CSV headers (@feph) - Minor updates (@dvase) Version 0.6.1 -- released 4/21/19 ================================= - Added Elektronica SM70-45D, Agilent 33220A, and Keysight N5767A instruments (@CasperSchippers, @sumatrae) - Fixes for Prologix adapter and Keithley 2400 (@hlgirard, @ronan-sensome) - Improved support for SRS SR830 (@CasperSchippers) Version 0.6 -- released 1/14/19 =============================== - New VXI11 Adapter for ethernet instruments (@chweiser) - PyQt updates to 5.6.0 - Added SRS SG380, Ametek 7270, Agilent 4156, HP 34401A, Advantest R3767CG, and Oxford ITC503 instrustruments (@sylkar, @jmittelstaedt, @vik-s, @troylf, @CasperSchippers) - Updates to Keithley 2000, Agilent 8257D, ESP 300, and Keithley 2400 instruments (@watersjason, @jmittelstaedt, @nup002) - Various minor bug fixes (@thosou) Version 0.5.1 -- released 4/14/18 ================================= - Minor versions of PyVISA are now properly handled - Documentation improvements (@Laogeodritt and @ederag) - Instruments now have `set_process` capability (@bilderbuchi) - Plotter now uses threads (@dvspirito) - Display inputs and PlotItem improvements (@Laogeodritt) Version 0.5 -- released 10/18/17 ================================ - Threads are used by default, eliminating multiprocessing issues with spawn - Enhanced unit tests for threading - Sphinx Doctests are added to the documentation (@bilderbuchi) - Improvements to documentation (@JuMaD) Version 0.4.6 -- released 8/12/17 ================================= - Reverted multiprocessing start method keyword arguments to fix Unix spawn issues (@ndr37) - Fixes to regressions in Results writing (@feinsteinben) - Fixes to TCP support using cloudpickle (@feinsteinben) - Restructing of unit test framework Version 0.4.5 -- released 7/4/17 ================================ - Recorder and Scribe now leverage QueueListener (@feinsteinben) - PrologixAdapter and SerialAdapter now handle Serial objects as adapters (@feinsteinben) - Optional TCP support now uses cloudpickle for serialization (@feinsteinben) - Significant PEP8 review and bug fixes (@feinsteinben) - Includes docs in the code distribution (@ghisvail) - Continuous integration support for Python 3.6 (@feinsteinben) Version 0.4.4 -- released 6/4/17 ================================ - Fix pip install for non-wheel builds - Update to Agilent E4980 (@dvspirito) - Minor fixes for docs, tests, and formatting (@ghisvail, @feinsteinben) Version 0.4.3 -- released 3/30/17 ================================= - Added Agilent E4980, AMI 430, Agilent 34410A, Thorlabs PM100, and Anritsu MS9710C instruments (@TvBMcMaster, @dvspirito, and @mhdg) - Updates to PyVISA support (@minhhaiphys) - Initial work on resource manager (@dvspirito) - Fixes for Prologix adapter that allow read-write delays (@TvBMcMaster) - Fixes for conda environment on continuous integration Version 0.4.2 -- released 8/23/16 ================================= - New instructions for installing with Anaconda and conda-forge package (thanks @melund!) - Bug-fixes to the Keithley 2000, SR830, and Agilent E4408B - Re-introduced the Newport ESP300 motion controller - Major update to the Keithely 2400, 2000 and Yokogawa 7651 to achieve a common interface - New command-string processing hooks for Instrument property functions - Updated LakeShore 331 temperature controller with new features - Updates to the Agilent 8257D signal generator for better feature exposure Version 0.4.1 -- released 7/31/16 ================================= - Critical fix in setup.py for importing instruments (also added to documentation) Version 0.4 -- released 7/29/16 =============================== - Replaced Instrument add_measurement and add_control with measurement and control functions - Added validators to allow Instrument.control to match restricted ranges - Added mapping to Instrument.control to allow more flexible inputs - Conda is now used to set up the Python environment - macOS testing in continuous integration - Major updates to the documentation Version 0.3 -- released 4/8/16 ============================== - Added IPython (Jupyter) notebook support with significant features - Updated set of example scripts and notebooks - New PyMeasure logo released - Removed support for Python <3.4 - Changed multiprocessing to use spawn for compatibility - Significant work on the documentation - Added initial tests for non-instrument code - Continuous integration setup for Linux and Windows Version 0.2 -- released 12/16/15 ================================ - Python 3 compatibility, removed support for Python 2 - Considerable renaming for better PEP8 compliance - Added MIT License - Major restructuring of the package to break it into smaller modules - Major rewrite of display functionality, introducing new Qt objects for easy extensions - Major rewrite of procedure execution, now using a Worker process which takes advantage of multi-core CPUs - Addition of a number of examples - New methods for listening to Procedures, introducing ZMQ for TCP connectivity - Updates to Keithley2400 and VISAAdapter Version 0.1.6 -- released 4/19/15 ================================= - Renamed the package to PyMeasure from Automate to be more descriptive about its purpose - Addition of VectorParameter to allow vectors to be input for Procedures - Minor fixes for the Results and Danfysik8500 Version 0.1.5 -- release 10/22/14 ================================= - New Manager class for handling Procedures in a queue fashion - New Browser that works in tandem with the Manager to display the queue - Bug fixes for Results loading Version 0.1.4 -- released 8/2/14 ================================ - Integrated Results class into display and file writing - Bug fixes for Listener classes - Bug fixes for SR830 Version 0.1.3 -- released 7/20/14 ================================= - Replaced logging system with Python logging package - Added data management (Results) and bug fixes for Procedures and Parameters - Added pandas v0.14 to requirements for data management - Added data listeners, Qt4 and PyQtGraph helpers Version 0.1.2 -- released 7/18/14 ================================= - Bug fixes to LakeShore 425 - Added new Procedure and Parameter classes for generic experiments - Added version number in package Version 0.1.1 -- released 7/16/14 ================================= - Bug fixes to PrologixAdapter, VISAAdapter, Agilent 8722ES, Agilent 8257D, Stanford SR830, Danfysik8500 - Added Tektronix TDS 2000 with basic functionality - Fixed Danfysik communication to handle errors properly Version 0.1.0 -- released 7/15/14 ================================= - Initial release Keywords: measure instrument experiment control automate graph plot Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Scientific/Engineering Provides-Extra: matplotlib Provides-Extra: tcp Provides-Extra: python-vxi11 PyMeasure-0.9.0/AUTHORS.txt0000664000175000017500000000103014010032254015535 0ustar colincolin00000000000000Colin Jermain Graham Rowlands Minh-Hai Nguyen Guen Prawiro-Atmodjo Tim van Boxtel Davide Spirito Marcos Guimaraes Ghislain Antony Vaillant Ben Feinstein Neal Reynolds Christoph Buchner Julian Dlugosch Sylvain Karlen Joseph Mittelstaedt Troy Fox Vikram Sekar Casper Schippers Sumatran Tiger Michael Schneider Dennis Feng Stefano Pirotta Moritz Jung Richard Schlitz Manuel Zahn Mikhaël Myara Paul Goulain John McMaster Dominik Kriegner Jonathan Larochelle Dominic Caron Mathieu Plante Michele Sardo Steven Siegl Benjamin Klebel-Knobloch PyMeasure-0.9.0/README.rst0000664000175000017500000000403614010032244015346 0ustar colincolin00000000000000.. image:: https://raw.githubusercontent.com/pymeasure/pymeasure/master/docs/images/PyMeasure.png :alt: PyMeasure Scientific package PyMeasure scientific package ############################ PyMeasure makes scientific measurements easy to set up and run. The package contains a repository of instrument classes and a system for running experiment procedures, which provides graphical interfaces for graphing live data and managing queues of experiments. Both parts of the package are independent, and when combined provide all the necessary requirements for advanced measurements with only limited coding. PyMeasure is currently under active development, so please report any issues you experience to our `Issues page`_. .. _Issues page: https://github.com/pymeasure/pymeasure/issues PyMeasure runs on Python 3.6, 3.7, 3.8 and 3.9, and is tested with continous-integration on Linux, macOS, and Windows. .. image:: https://github.com/pymeasure/pymeasure/workflows/Pymeasure%20CI/badge.svg :target: https://github.com/pymeasure/pymeasure/actions .. image:: http://readthedocs.org/projects/pymeasure/badge/?version=latest :target: http://pymeasure.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3732545.svg :target: https://doi.org/10.5281/zenodo.3732545 .. image:: https://anaconda.org/conda-forge/pymeasure/badges/version.svg :target: https://anaconda.org/conda-forge/pymeasure .. image:: https://anaconda.org/conda-forge/pymeasure/badges/downloads.svg :target: https://anaconda.org/conda-forge/pymeasure Quick start =========== Check out `the documentation`_ for the `quick start guide`_, that covers the installation of Python and PyMeasure. There are a number of examples in the `examples`_ directory that can help you get up and running. .. _the documentation: http://pymeasure.readthedocs.org/en/latest/ .. _quick start guide: http://pymeasure.readthedocs.io/en/latest/quick_start.html .. _examples: https://github.com/pymeasure/pymeasure/tree/master/examples PyMeasure-0.9.0/MANIFEST.in0000644000175000017500000000037413640137324015431 0ustar colincolin00000000000000include setup.py include README.rst include LICENSE.txt include CHANGES.txt include AUTHORS.txt recursive-include pymeasure * recursive-include tests * recursive-include docs * prune docs/_build recursive-exclude * __pycache__ recursive-exclude * *.pycPyMeasure-0.9.0/LICENSE.txt0000664000175000017500000000205514010037617015512 0ustar colincolin00000000000000Copyright (c) 2013-2021 PyMeasure Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PyMeasure-0.9.0/docs/0000775000175000017500000000000014010046235014611 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/make.bat0000644000175000017500000001506313640137324016231 0ustar colincolin00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyMeasure.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyMeasure.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end PyMeasure-0.9.0/docs/index.rst0000664000175000017500000000514214010032244016447 0ustar colincolin00000000000000.. PyMeasure documentation master file, created by sphinx-quickstart on Mon Apr 6 13:06:00 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. ############################ PyMeasure scientific package ############################ .. image:: images/PyMeasure.png :alt: PyMeasure Scientific package PyMeasure makes scientific measurements easy to set up and run. The package contains a repository of instrument classes and a system for running experiment procedures, which provides graphical interfaces for graphing live data and managing queues of experiments. Both parts of the package are independent, and when combined provide all the necessary requirements for advanced measurements with only limited coding. Installing Python and PyMeasure are demonstrated in the :doc:`Quick Start guide `. From there, checkout the existing :doc:`instruments that are available for use `. PyMeasure is currently under active development, so please report any issues you experience on our `Issues page`_. .. image:: https://github.com/pymeasure/pymeasure/workflows/Pymeasure%20CI/badge.svg :target: https://github.com/pymeasure/pymeasure/actions .. image:: http://readthedocs.org/projects/pymeasure/badge/?version=latest :target: http://pymeasure.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3732545.svg :target: https://doi.org/10.5281/zenodo.3732545 .. image:: https://anaconda.org/conda-forge/pymeasure/badges/version.svg :target: https://anaconda.org/conda-forge/pymeasure .. image:: https://anaconda.org/conda-forge/pymeasure/badges/downloads.svg :target: https://anaconda.org/conda-forge/pymeasure .. _Issues page: https://github.com/pymeasure/pymeasure/issues The main documentation for the site is organized into a couple sections: * :ref:`learning-docs` * :ref:`api-docs` * :ref:`about-docs` Information about development is also available: * :ref:`dev-docs` .. _learning-docs: .. toctree:: :maxdepth: 2 :caption: Learning PyMeasure introduction quick_start tutorial/index .. _api-docs: .. toctree:: :maxdepth: 1 :caption: API References api/adapters api/experiment/index api/display/index api/instruments/index .. _dev-docs: .. toctree:: :maxdepth: 2 :caption: Getting involved dev/contribute dev/reporting_errors dev/adding_instruments dev/coding_standards .. _about-docs: .. toctree:: :maxdepth: 2 :caption: About PyMeasure about/authors about/license PyMeasure-0.9.0/docs/introduction.rst0000644000175000017500000000416714010032244020065 0ustar colincolin00000000000000############ Introduction ############ PyMeasure uses an object-oriented approach for communicating with scientific instruments, which provides an intuitive interface where the low-level SCPI and GPIB commands are hidden from normal use. Users can focus on solving the measurement problems at hand, instead of re-inventing how to communicate with instruments. Instruments with VISA (GPIB, Serial, etc) are supported through the `PyVISA package`_ under the hood. `Prologix GPIB`_ adapters are also supported. Communication protocols can be swapped, so that instrument classes can be used with all supported protocols interchangeably. .. _PyVISA package: http://pyvisa.readthedocs.org/en/master/ .. _Prologix GPIB: http://prologix.biz/ Before using PyMeasure, you may find it helpful to be acquainted with `basic Python programming for the sciences`_ and understand the concept of objects. .. _basic Python programming for the sciences: https://scipy-lectures.github.io/ Instrument ready ================ The package includes a number of :doc:`instruments already defined`. Their definitions are organized based on the manufacturer name of the instrument. For example the class that defines the :doc:`Keithley 2400 SourceMeter` can be imported by calling: .. code-block:: python from pymeasure.instruments.keithley import Keithley2400 The :doc:`Tutorials ` section will go into more detail on :doc:`connecting to an instrument `. If you don't find the instrument you are looking for, but are interested in contributing, see the documentation on :doc:`adding an instrument `. Graphical displays ================== Graphical user interfaces (GUIs) can be easily generated to manage execution of measurement procedures with PyMeasure. This includes live plotting for data, and a queue system for managing large numbers of experiments. These features are explored in the :doc:`Using a graphical interface ` tutorial. .. image:: tutorial/pymeasure-managedwindow-running.png :alt: ManagedWindow Running ExamplePyMeasure-0.9.0/docs/dev/0000775000175000017500000000000014010046235015367 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/dev/reporting_errors.rst0000664000175000017500000000046514010032244021526 0ustar colincolin00000000000000################## Reporting an error ################## Please report all errors to the `Issues section`_ of the PyMeasure GitHub repository. Use the search function to determine if there is an existing or resolved issued before posting. .. _`Issues section`: https://github.com/pymeasure/pymeasure/issues PyMeasure-0.9.0/docs/dev/contribute.rst0000664000175000017500000001165414010032244020301 0ustar colincolin00000000000000############ Contributing ############ Contributions to the instrument repository and the main code base are highly encouraged. This section outlines the basic work-flow for new contributors. Using the development version ============================= New features are added to the development version of PyMeasure, hosted on `GitHub`_. We use `Git version control`_ to track and manage changes to the source code. On Windows, we recommend using `GitHub Desktop`_. Make sure you have an appropriate version of Git (or GitHub Desktop) installed and that you have a GitHub account. .. _GitHub: https://github.com/ .. _Git version control: https://git-scm.com/ .. _GitHub Desktop: https://git-scm.com/downloads In order to add your feature, you need to first `fork`_ PyMeasure. This will create a copy of the repository under your GitHub account. .. _fork: https://help.github.com/articles/fork-a-repo/ The instructions below assume that you have set up Anaconda, as described in the :doc:`Quick Start guide <../quick_start>` and describe the terminal commands necessary. If you are using GitHub Desktop, take a look through `their documentation`_ to understand the corresponding steps. .. _their documentation: https://help.github.com/desktop/ Clone your fork of PyMeasure :code:`your-github-username/pymeasure`. In the following terminal commands replace your desired path and GitHub username. .. code-block:: bash cd /path/for/code git clone https://github.com/your-github-username/pymeasure.git If you had already installed PyMeasure using :code:`pip`, make sure to uninstall it before continuing. .. code-block:: bash pip uninstall pymeasure Install PyMeasure in the editable mode. .. code-block:: bash cd /path/for/code/pymeasure pip install -e . This will allow you to edit the files of PyMeasure and see the changes reflected. Make sure to reset your notebook kernel or Python console when doing so. Now you have your own copy of the development version of PyMeasure installed! Working on a new feature ======================== We use branches in Git to allow multiple features to be worked on simultaneously, without causing conflicts. The master branch contains the stable development version. Instead of working on the master branch, you will create your own branch off the master and merge it back into the master when you are finished. Create a new branch for your feature before editing the code. For example, if you want to add the new instrument "Extreme 5000" you will make a new branch "dev/extreme-5000". .. code-block:: bash git branch dev/extreme-5000 You can also `make a new branch`_ on GitHub. If you do so, you will have to fetch these changes before the branch will show up on your local computer. .. code-block:: bash git fetch .. _make a new branch: https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/ Once you have created the branch, change your current branch to match the new one. .. code-block:: bash git checkout dev/extreme-5000 Now you are ready to write your new feature and make changes to the code. To ensure consistency, please follow the :doc:`coding standards for PyMeasure `. Use :code:`git status` to check on the files that have been changed. As you go, commit your changes and push them to your fork. .. code-block:: bash git add file-that-changed.py git commit -m "A short description about what changed" git push Making a pull-request ===================== While you are working, its helpful to start a pull-request (PR) on the :code:`master` branch of :code:`pymeasure/pymeasure`. This will allow you to discuss your feature with other contributors. We encourage you to start this pull-request after your first commit. `Start a pull-request`_ on the `PyMeasure GitHub page`_. .. _`Start a pull-request`: https://help.github.com/articles/using-pull-requests/ .. _PyMeasure GitHub page: https://github.com/pymeasure/pymeasure Your pull-request will be merged by the PyMeasure maintainers once it meets the coding standards and passes unit tests. You will notice that your pull-request is automatically checked with the unit tests. Unit testing ============ Unit tests are run each time a new commit is made to a branch. The purpose is to catch changes that break the current functionality, by testing each feature unit. PyMeasure relies on `pytest`_ to preform these tests, which are run on TravisCI and Appveyor for Linux/macOS and Windows respectively. Running the unit tests while you develop is highly encouraged. This will ensure that you have a working contribution when you create a pull request. .. code-block:: bash python setup.py test If your feature can be tested, unit tests are required. This will ensure that your features keep working as new features are added. .. _`pytest`: http://pytest.org/latest/ Now you are familiar with all the pieces of the PyMeasure development work-flow. We look forward to seeing your pull-request! PyMeasure-0.9.0/docs/dev/adding_instruments.rst0000664000175000017500000004775714010037617022052 0ustar colincolin00000000000000################## Adding instruments ################## You can make a significant contribution to PyMeasure by adding a new instrument to the :code:`pymeasure.instruments` package. Even adding an instrument with a few features can help get the ball rolling, since its likely that others are interested in the same instrument. Before getting started, become familiar with the :doc:`contributing work-flow ` for PyMeasure, which steps through the process of adding a new feature (like an instrument) to the development version of the source code. This section will describe how to lay out your instrument code. File structure ============== Your new instrument should be placed in the directory corresponding to the manufacturer of the instrument. For example, if you are going to add an "Extreme 5000" instrument you should add the following files assuming "Extreme" is the manufacturer. Use lowercase for all filenames to distinguish packages from CamelCase Python classes. .. code-block:: none pymeasure/pymeasure/instruments/extreme/ |--> __init__.py |--> extreme5000.py Updating the init file ********************** The :code:`__init__.py` file in the manufacturer directory should import all of the instruments that correspond to the manufacturer, to allow the files to be easily imported. For a new manufacturer, the manufacturer should also be added to :code:`pymeasure/pymeasure/instruments/__init__.py`. Adding documentation ******************** Documentation for each instrument is required, and helps others understand the features you have implemented. Add a new reStructuredText file to the documentation. .. code-block:: none pymeasure/docs/api/instruments/extreme/ |--> index.rst |--> extreme5000.rst Copy an existing instrument documentation file, which will automatically generate the documentation for the instrument. The :code:`index.rst` file should link to the :code:`extreme5000` file. For a new manufacturer, the manufacturer should be also linked in :code:`pymeasure/docs/api/instruments/index.rst`. Instrument file =============== All standard instruments should be child class of :class:`Instrument `. This provides the basic functionality for working with :class:`Adapters `, which perform the actual communication. The most basic instrument, for our "Extreme 5000" example starts like this: .. testcode:: # # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # from pymeasure.instruments import Instrument .. testcode:: :hide: # Behind the scene, replace Instrument with FakeInstrument to enable # doctesting all this from pymeasure.instruments.instrument import FakeInstrument as Instrument This is a minimal instrument definition: .. testcode:: class Extreme5000(Instrument): """ Represents the imaginary Extreme 5000 instrument. """ def __init__(self, resourceName, **kwargs): super().__init__( resourceName, "Extreme 5000", **kwargs ) Make sure to include the PyMeasure license to each file, and add yourself as an author to the :code:`AUTHORS.txt` file. In principle you are free to write any functions that are necessary for interacting with the instrument. When doing so, make sure to use the :code:`self.ask(command)`, :code:`self.write(command)`, and :code:`self.read()` methods to issue command instead of calling the adapter directly. In practice, we have developed a number of convenience functions for making instruments easy to write and maintain. The following sections detail these conveniences and are highly encouraged. Writing properties ================== In PyMeasure, `Python properties`_ are the preferred method for dealing with variables that are read or set. PyMeasure comes with two convenience functions for making properties for classes. The :func:`Instrument.measurement ` function returns a property that issues a GPIB/SCPI requests when the value is used. For example, if our "Extreme 5000" has the :code:`*IDN?` command we can write the following property to be added above the :code:`def __init__` line in our above example class, or added to the class after the fact as in the code here: .. _Python properties: https://docs.python.org/3/howto/descriptor.html#properties .. testcode:: Extreme5000.id = Instrument.measurement( "*IDN?", """ Reads the instrument identification """ ) .. testcode:: :hide: # We are not mocking this in FakeInstrument, let's override silently Extreme5000.id = 'Extreme 5000 identification from instrument' You will notice that a documentation string is required, and should be descriptive and specific. When we use this property we will get the identification information. .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.id # Reads "*IDN?" 'Extreme 5000 identification from instrument' The :func:`Instrument.control ` function extends this behavior by creating a property that you can read and set. For example, if our "Extreme 5000" has the :code:`:VOLT?` and :code:`:VOLT ` commands that are in Volts, we can write the following property. .. testcode:: Extreme5000.voltage = Instrument.control( ":VOLT?", ":VOLT %g", """ A floating point property that controls the voltage in Volts. This property can be set. """ ) You will notice that we use the `Python string format`_ :code:`%g` to pass through the floating point. .. _Python string format: https://docs.python.org/3/library/string.html#format-specification-mini-language We can use this property to set the voltage to 100 mV, which will execute the command and then request the current voltage. .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.voltage = 0.1 # Executes ":VOLT 0.1" >>> extreme.voltage # Reads ":VOLT?" 0.1 Using both of these functions, you can create a number of properties for basic measurements and controls. The next section details additional features of :func:`Instrument.control ` that allow you to write properties that cover specific ranges, or have to map between a real value to one used in the command. Furthermore it is shown how to perform more complex processing of return values from your device. .. _advanced-properties: Advanced properties =================== Many GPIB/SCIP commands are more restrictive than our basic examples above. The :func:`Instrument.control ` function has the ability to encode these restrictions using :mod:`validators `. A validator is a function that takes a value and a set of values, and returns a valid value or raises an exception. There are a number of pre-defined validators in :mod:`pymeasure.instruments.validators` that should cover most situations. We will cover the four basic types here. In the examples below we assume you have imported the validators. .. testcode:: :hide: from pymeasure.instruments.validators import strict_discrete_set, strict_range, truncated_range, truncated_discrete_set In many situations you will also need to process the return string in order to extract the wanted quantity or process a value before sending it to the device. The :func:`Instrument.control `, :func:`Instrument.measurement ` and :func:`Instrument.setting ` function also provide means to achieve this. In a restricted range ********************* If you have a property with a restricted range, you can use the :func:`strict_range ` and :func:`truncated_range ` functions. For example, if our "Extreme 5000" can only support voltages from -1 V to 1 V, we can modify our previous example to use a strict validator over this range. .. testcode:: Extreme5000.voltage = Instrument.control( ":VOLT?", ":VOLT %g", """ A floating point property that controls the voltage in Volts, from -1 to 1 V. This property can be set. """, validator=strict_range, values=[-1, 1] ) Now our voltage will raise a ValueError if the value is out of the range. .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.voltage = 100 Traceback (most recent call last): ... ValueError: Value of 100 is not in range [-1,1] This is useful if you want to alert the programmer that they are using an invalid value. However, sometimes it can be nicer to truncate the value to be within the range. .. testcode:: Extreme5000.voltage = Instrument.control( ":VOLT?", ":VOLT %g", """ A floating point property that controls the voltage in Volts, from -1 to 1 V. Invalid voltages are truncated. This property can be set. """, validator=truncated_range, values=[-1, 1] ) Now our voltage will not raise an error, and will truncate the value to the range bounds. .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.voltage = 100 # Executes ":VOLT 1" >>> extreme.voltage 1.0 In a discrete set ***************** Often a control property should only take a few discrete values. You can use the :func:`strict_discrete_set ` and :func:`truncated_discrete_set ` functions to handle these situations. The strict version raises an error if the value is not in the set, as in the range examples above. For example, if our "Extreme 5000" has a :code:`:RANG ` command that sets the voltage range that can take values of 10 mV, 100 mV, and 1 V in Volts, then we can write a control as follows. .. testcode:: Extreme5000.voltage = Instrument.control( ":RANG?", ":RANG %g", """ A floating point property that controls the voltage range in Volts. This property can be set. """, validator=truncated_discrete_set, values=[10e-3, 100e-3, 1] ) Now we can set the voltage range, which will automatically truncate to an appropriate value. .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.voltage = 0.08 >>> extreme.voltage 0.1 Using maps ********** Now that you are familiar with the validators, you can additionally use maps to satisfy instruments which require non-physical values. The :code:`map_values` argument of :func:`Instrument.control ` enables this feature. If your set of values is a list, then the command will use the index of the list. For example, if our "Extreme 5000" instead has a :code:`:RANG `, where 0, 1, and 2 correspond to 10 mV, 100 mV, and 1 V, then we can use the following control. .. testcode:: Extreme5000.voltage = Instrument.control( ":RANG?", ":RANG %d", """ A floating point property that controls the voltage range in Volts, which takes values of 10 mV, 100 mV and 1 V. This property can be set. """, validator=truncated_discrete_set, values=[10e-3, 100e-3, 1], map_values=True ) Now the actual GPIB/SCIP command is ":RANG 1" for a value of 100 mV, since the index of 100 mV in the values list is 1. .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.voltage = 100e-3 >>> extreme.read() '1' >>> extreme.voltage = 1 >>> extreme.voltage 1 Dictionaries provide a more flexible method for mapping between real-values and those required by the instrument. If instead the :code:`:RANG ` took 1, 2, and 3 to correspond to 10 mV, 100 mV, and 1 V, then we can replace our previous control with the following. .. testcode:: Extreme5000.voltage = Instrument.control( ":RANG?", ":RANG %d", """ A floating point property that controls the voltage range in Volts, which takes values of 10 mV, 100 mV and 1 V. This property can be set. """, validator=truncated_discrete_set, values={10e-3:1, 100e-3:2, 1:3}, map_values=True ) .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.voltage = 10e-3 >>> extreme.read() '1' >>> extreme.voltage = 100e-3 >>> extreme.voltage 0.1 The dictionary now maps the keys to specific values. The values and keys can be any type, so this can support properties that use strings: .. testcode:: Extreme5000.channel = Instrument.control( ":CHAN?", ":CHAN %d", """ A string property that controls the measurement channel, which can take the values X, Y, or Z. """, validator=strict_discrete_set, values={'X':1, 'Y':2, 'Z':3}, map_values=True ) .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.channel = 'X' >>> extreme.read() '1' >>> extreme.channel = 'Y' >>> extreme.channel 'Y' As you have seen, the :func:`Instrument.control ` function can be significantly extended by using validators and maps. Processing of set values ************************ The :func:`Instrument.control `, and :func:`Instrument.setting ` allow a keyword argument `set_process` which must be a function that takes a value after validation and performs processing before value mapping. This function must return the processed value. This can be typically used for unit conversions as in the following example: .. testcode:: Extreme5000.current = Instrument.setting( ":CURR %g", """ A floating point property that takes the measurement current in A """, validator=strict_range, values=[0, 10], set_process=lambda v: 1e3*v, # convert current to mA ) .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.current = 1 # set current to 1000 mA Processing of return values *************************** Similar to `set_process` the :func:`Instrument.control `, and :func:`Instrument.measurement ` functions allow a `get_process` argument which if specified must be a function that takes a value and performs processing before value mapping. The function must return the processed value. In analogy to the example above this can be used for example for unit conversion: .. testcode:: Extreme5000.current = Instrument.control( ":CURR?", ":CURR %g", """ A floating point property representing the measurement current in A """, validator=strict_range, values=[0, 10], set_process=lambda v: 1e3*v, # convert to mA get_process=lambda v: 1e-3*v, # convert to A ) .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.current = 3.1 >>> extreme.current 3.1 `get_process` can also be used to perform string processing. Let's say your instrument returns a value with its unit which has to be removed. This could be achieved by the following code: .. testcode:: Extreme5000.capacity = Instrument.measurement( ":CAP?", """ A measurement returning a capacity in nF in the format ' nF' """, get_process=lambda v: float(v.replace('nF', '')) ) The same can be also achieved by the `preprocess_reply` keyword argument to :func:`Instrument.control ` or :func:`Instrument.measurement `. This function is forwarded to :func:`Adapter.values ` and runs directly after receiving the reply from the device. One can therefore take advantage of the built in casting abilities and simplify the code accordingly: .. testcode:: Extreme5000.capacity = Instrument.measurement( ":CAP?", """ A measurement returning a capacity in nF in the format ' nF' """, preprocess_reply=lambda v: v.replace('nF', '') # notice how we don't need to cast to float anymore ) The real purpose of `preprocess_reply` is, however, for instruments where many/all properties need similar reply processing. `preprocess_reply` can be applied to all :func:`Instrument.control ` or :func:`Instrument.measurement ` properties, for example if all quantities are returned with a unit as in the example above. To avoid running into troubles for other properties this `preprocess_reply` should be clever enough to skip the processing in case it is not appropriate, for example if some identification string is returned. Typically this can be achieved by regular expression matching. In case of no match the reply is returned unchanged: .. testcode:: import re _reg_value = re.compile(r"([-+]?[0-9]*\.?[0-9]+)\s+\w+") def extract_value(reply): """ extract numerical value from reply. If none can be found the reply is returned unchanged. :param reply: reply string :returns: string with only the numerical value """ r = _reg_value.search(reply) if r: return r.groups()[0] else: return reply class Extreme5001(Instrument): """ Represents the imaginary Extreme 5001 instrument. This instrument sends numerical values including their units in an format " ". """ capacity = Instrument.measurement( ":CAP?", """ A measurement returning a capacity in nF in the format ' nF' """ ) voltage = Instrument.measurement( ":VOLT?", """ A measurement returning a voltage in V in the format ' V' """ ) id = Instrument.measurement( "*idn?", """ The identification of the instrument. """ ) def __init__(self, resourceName, **kwargs): super().__init__( resourceName, "Extreme 5000", preprocess_reply=extract_value, **kwargs, ) In cases where the general `preprocess_reply` function should not run it can be also overwritten in the property definition: .. testcode:: Extreme5001.channel = Instrument.control( ":CHAN?", ":CHAN %d", """ A string property that controls the measurement channel, which can take the values X, Y, or Z. """, validator=strict_discrete_set, values=[1,2,3], preprocess_reply=lambda v: v, ) Using a combination of the decribed abilities also complex communication schemes can be achieved. PyMeasure-0.9.0/docs/dev/coding_standards.rst0000664000175000017500000000311314010032244021420 0ustar colincolin00000000000000################ Coding Standards ################ In order to maintain consistency across the different instruments in the PyMeasure repository, we enforce the following standards. Python style guides =================== The `PEP8 style guide`_ and `PEP257 docstring conventions`_ should be followed. .. _PEP8 style guide: https://www.python.org/dev/peps/pep-0008/ .. _PEP257 docstring conventions: https://www.python.org/dev/peps/pep-0257/ Function and variable names should be lower case with underscores as needed to seperate words. CamelCase should only be used for class names, unless working with Qt, where its use is common. There are no plans to support type hinting in PyMeasure code. This adds a lot of additional code to manage, without a clear advantage for this project. Type documentation should be placed in the docstring where not clear from the variable name. Documentation ============= PyMeasure documents code using reStructuredText and the `Sphinx documentation generator`_. All functions, classes, and methods should be documented in the code using a `docstring`_. .. _Sphinx documentation generator: http://www.sphinx-doc.org/en/stable/ .. _docstring: http://www.sphinx-doc.org/en/stable/ext/example_numpy.html?highlight=docstring Usage of getter and setter functions ==================================== Getter and setter functions are discouraged, since properties provide a more fluid experience. Given the extensive tools avalible for defining properties, detailed in the :ref:`Advanced properties ` section, these types of properties are prefered. PyMeasure-0.9.0/docs/Makefile0000644000175000017500000001516613640137324016270 0ustar colincolin00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PyMeasure.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyMeasure.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PyMeasure" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyMeasure" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." PyMeasure-0.9.0/docs/conf.py0000664000175000017500000002111514010046171016107 0ustar colincolin00000000000000# -*- coding: utf-8 -*- # # PyMeasure documentation build configuration file, created by # sphinx-quickstart on Mon Apr 6 13:06:00 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os sys.path.insert(0, os.path.abspath('..')) # Allow modules to be found # Include Read the Docs formatting on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.doctest' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'PyMeasure' copyright = u'2013-2021, PyMeasure Developers' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.9.0' # The full version, including alpha/beta/rc tags. release = '0.9.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'PyMeasuredoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'PyMeasure.tex', u'PyMeasure Documentation', u'PyMeasure Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pymeasure', u'PyMeasure Documentation', [u'PyMeasure Developers'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'PyMeasure', u'PyMeasure Documentation', u'PyMeasure Developers', 'PyMeasure', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # Automatically mock optional packages autodoc_mock_imports = ['pyqtgraph', 'zmq', 'cloudpickle', 'vxi11', 'pyvirtualbench'] PyMeasure-0.9.0/docs/api/0000775000175000017500000000000014010046235015362 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/0000775000175000017500000000000014010046235017755 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/danfysik/0000775000175000017500000000000014010046235021565 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/danfysik/index.rst0000644000175000017500000000053614010032244023423 0ustar colincolin00000000000000.. module:: pymeasure.instruments.danfysik ######## Danfysik ######## This section contains specific documentation on the Danfysik instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 adapters danfysik8500PyMeasure-0.9.0/docs/api/instruments/danfysik/danfysik8500.rst0000644000175000017500000000026114010032244024434 0ustar colincolin00000000000000########################## Danfysik 8500 Power Supply ########################## .. autoclass:: pymeasure.instruments.danfysik.Danfysik8500 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/danfysik/adapters.rst0000644000175000017500000000025314010032244024113 0ustar colincolin00000000000000####################### Danfysik Serial Adapter ####################### .. autoclass:: pymeasure.instruments.danfysik.DanfysikAdapter :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/index.rst0000664000175000017500000000134014010032244021607 0ustar colincolin00000000000000.. module:: pymeasure.instruments ##################### pymeasure.instruments ##################### This section contains documentation on the instrument classes. .. toctree:: :maxdepth: 2 instruments validators comedi resources Instruments by manufacturer: .. toctree:: :maxdepth: 2 advantest/index agilent/index ametek/index ami/index anapico/index anritsu/index attocube/index danfysik/index deltaelektronica/index fwbell/index hp/index keithley/index keysight/index lakeshore/index newport/index ni/index oxfordinstruments/index parker/index razorbill/index signalrecovery/index srs/index tektronix/index thorlabs/index yokogawa/index PyMeasure-0.9.0/docs/api/instruments/keithley/0000775000175000017500000000000014010046235021573 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/keithley/index.rst0000664000175000017500000000066314010032244023434 0ustar colincolin00000000000000.. module:: pymeasure.instruments.keithley ######## Keithley ######## This section contains specific documentation on the Keithley instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 keithley2000 keithley2400 keithley2450 keithley2700 keithley6221 keithley6517b keithley2750PyMeasure-0.9.0/docs/api/instruments/keithley/keithley2750.rst0000664000175000017500000000050314010032244024452 0ustar colincolin00000000000000###################################### Keithley 2750 Multimeter/Switch System ###################################### .. autoclass:: pymeasure.instruments.keithley.Keithley2750 :members: :show-inheritance: :inherited-members: :exclude-members: ask, control, clear, measurement, read, setting, values, write PyMeasure-0.9.0/docs/api/instruments/keithley/keithley2700.rst0000664000175000017500000000050214010032244024444 0ustar colincolin00000000000000###################################### Keithley 2700 MultiMeter/Switch System ###################################### .. autoclass:: pymeasure.instruments.keithley.Keithley2700 :members: :show-inheritance: :inherited-members: :exclude-members: ask, control, clear, measurement, read, setting, values, writePyMeasure-0.9.0/docs/api/instruments/keithley/keithley2000.rst0000644000175000017500000000043014010032244024433 0ustar colincolin00000000000000######################## Keithley 2000 Multimeter ######################## .. autoclass:: pymeasure.instruments.keithley.Keithley2000 :members: :show-inheritance: :inherited-members: :exclude-members: ask, control, clear, measurement, read, setting, values, writePyMeasure-0.9.0/docs/api/instruments/keithley/keithley6221.rst0000664000175000017500000000050214010032244024446 0ustar colincolin00000000000000###################################### Keithley 6221 AC and DC Current Source ###################################### .. autoclass:: pymeasure.instruments.keithley.Keithley6221 :members: :show-inheritance: :inherited-members: :exclude-members: ask, control, clear, measurement, read, setting, values, writePyMeasure-0.9.0/docs/api/instruments/keithley/keithley6517b.rst0000664000175000017500000000046214010032244024625 0ustar colincolin00000000000000########################### Keithley 6517B Electrometer ########################### .. autoclass:: pymeasure.instruments.keithley.Keithley6517B :members: :show-inheritance: :inherited-members: :exclude-members: ask, control, clear, extract_value, measurement, read, setting, values, write PyMeasure-0.9.0/docs/api/instruments/keithley/keithley2400.rst0000644000175000017500000000043314010032244024442 0ustar colincolin00000000000000######################### Keithley 2400 SourceMeter ######################### .. autoclass:: pymeasure.instruments.keithley.Keithley2400 :members: :show-inheritance: :inherited-members: :exclude-members: ask, control, clear, measurement, read, setting, values, writePyMeasure-0.9.0/docs/api/instruments/keithley/keithley2450.rst0000664000175000017500000000043314010032244024451 0ustar colincolin00000000000000######################### Keithley 2450 SourceMeter ######################### .. autoclass:: pymeasure.instruments.keithley.Keithley2450 :members: :show-inheritance: :inherited-members: :exclude-members: ask, control, clear, measurement, read, setting, values, writePyMeasure-0.9.0/docs/api/instruments/keysight/0000775000175000017500000000000014010046235021604 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/keysight/index.rst0000664000175000017500000000055114010032244023441 0ustar colincolin00000000000000.. module:: pymeasure.instruments.keysight ######## Keysight ######## This section contains specific documentation on the keysight instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 keysightDSOX1102G keysightN5767APyMeasure-0.9.0/docs/api/instruments/keysight/keysightDSOX1102G.rst0000664000175000017500000000036414010032244025274 0ustar colincolin00000000000000############################### Keysight DSOX1102G Oscilloscope ############################### .. autoclass:: pymeasure.instruments.keysight.KeysightDSOX1102G :members: :show-inheritance: :inherited-members: :exclude-members: PyMeasure-0.9.0/docs/api/instruments/keysight/keysightN5767A.rst0000664000175000017500000000044714010032244024735 0ustar colincolin00000000000000############################ Keysight N5767A Power Supply ############################ .. autoclass:: pymeasure.instruments.keysight.KeysightN5767A :members: :show-inheritance: :inherited-members: :exclude-members: ask, control, clear, measurement, read, setting, values, write PyMeasure-0.9.0/docs/api/instruments/signalrecovery/0000775000175000017500000000000014010046235023011 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/signalrecovery/index.rst0000644000175000017500000000055714010032244024652 0ustar colincolin00000000000000.. module:: pymeasure.instruments.signalrecovery ############### Signal Recovery ############### This section contains specific documentation on the Signal Recovery instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 dsp7265PyMeasure-0.9.0/docs/api/instruments/signalrecovery/dsp7265.rst0000644000175000017500000000026214010032244024646 0ustar colincolin00000000000000########################## DSP 7265 Lock-in Amplifier ########################## .. autoclass:: pymeasure.instruments.signalrecovery.DSP7265 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/validators.rst0000644000175000017500000000072714010032244022656 0ustar colincolin00000000000000.. module:: pymeasure.instruments.validators ################### Validator functions ################### Validators are used in conjunction with the :func:`Instrument.control ` function to allow properties with complex restrictions for valid values. They are described in more detail in the :ref:`Advanced properties ` section. .. automodule:: pymeasure.instruments.validators :members: :noindex: PyMeasure-0.9.0/docs/api/instruments/advantest/0000775000175000017500000000000014010046235021746 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/advantest/index.rst0000664000175000017500000000053414010032244023604 0ustar colincolin00000000000000.. module:: pymeasure.instruments.advantest ######### Advantest ######### This section contains specific documentation on the Advantest instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 advantestR3767CG PyMeasure-0.9.0/docs/api/instruments/advantest/advantestR3767CG.rst0000664000175000017500000000034514010032244025351 0ustar colincolin00000000000000######################################### Advantest R3767CG Vector Network Analyzer ######################################### .. automodule:: pymeasure.instruments.advantest.advantestR3767CG :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/attocube/0000775000175000017500000000000014010046235021563 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/attocube/index.rst0000664000175000017500000000053114010032244023416 0ustar colincolin00000000000000.. module:: pymeasure.instruments.attocube ######## Attocube ######## This section contains specific documentation on the Attocube instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 adapters anc300 PyMeasure-0.9.0/docs/api/instruments/attocube/anc300.rst0000664000175000017500000000046214010032244023276 0ustar colincolin00000000000000################################# Attocube ANC300 Motion Controller ################################# .. autoclass:: pymeasure.instruments.attocube.anc300.ANC300Controller :members: :show-inheritance: .. autoclass:: pymeasure.instruments.attocube.anc300.Axis :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/attocube/adapters.rst0000664000175000017500000000025214010032244024112 0ustar colincolin00000000000000################# Attocube Adapters ################# .. autoclass:: pymeasure.instruments.attocube.adapters.AttocubeConsoleAdapter :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/agilent/0000775000175000017500000000000014010046235021400 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/agilent/agilentE4980.rst0000644000175000017500000000026514010032244024203 0ustar colincolin00000000000000############################## Agilent E4980 LCR Meter ############################## .. autoclass:: pymeasure.instruments.agilent.AgilentE4980 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/agilent/agilent8722ES.rst0000644000175000017500000000032514010032244024321 0ustar colincolin00000000000000###################################### Agilent 8722ES Vector Network Analyzer ###################################### .. autoclass:: pymeasure.instruments.agilent.Agilent8722ES :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/agilent/index.rst0000664000175000017500000000076314010032244023242 0ustar colincolin00000000000000.. module:: pymeasure.instruments.agilent ####### Agilent ####### This section contains specific documentation on the Agilent instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 agilent8257D agilent8722ES agilentE4408B agilentE4980 agilent34410A agilent34450A agilent4156 agilent33220A agilent33500 agilent33521A agilentB1500 PyMeasure-0.9.0/docs/api/instruments/agilent/agilentE4408B.rst0000644000175000017500000000030314010032244024271 0ustar colincolin00000000000000################################ Agilent E4408B Spectrum Analyzer ################################ .. autoclass:: pymeasure.instruments.agilent.AgilentE4408B :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/agilent/agilent33500.rst0000664000175000017500000000042114010032244024140 0ustar colincolin00000000000000########################################################## Agilent 33500 Function/Arbitrary Waveform Generator Family ########################################################## .. autoclass:: pymeasure.instruments.agilent.Agilent33500 :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/agilent/agilent8257D.rst0000644000175000017500000000027414010032244024203 0ustar colincolin00000000000000############################## Agilent 8257D Signal Generator ############################## .. autoclass:: pymeasure.instruments.agilent.Agilent8257D :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/agilent/agilent34450A.rst0000664000175000017500000000035314010032244024252 0ustar colincolin00000000000000############################################# HP/Agilent/Keysight 34450A Digital Multimeter ############################################# .. autoclass:: pymeasure.instruments.agilent.Agilent34450A :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/agilent/agilent34410A.rst0000644000175000017500000000027414010032244024246 0ustar colincolin00000000000000################################ Agilent 34410A Multimeter ################################ .. autoclass:: pymeasure.instruments.agilent.Agilent34410A :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/agilent/agilentB1500.rst0000664000175000017500000002561714010032244024173 0ustar colincolin00000000000000############################################## Agilent B1500 Semiconductor Parameter Analyzer ############################################## .. currentmodule:: pymeasure.instruments.agilent.agilentB1500 .. contents:: ********************************************** General Information ********************************************** This instrument driver does not support all configuration options of the B1500 mainframe yet. So far, it is possible to interface multiple SMU modules and source/measure currents and voltages, perform sampling and staircase sweep measurements. The implementation of further measurement functionalities is highly encouraged. Meanwhile the model is managed by Keysight, see the corresponding "Programming Guide" for details on the control methods and their parameters Command Translation =================== Alphabetical list of implemented B1500 commands and their corresponding method/attribute names in this instrument driver. .. |br| raw:: html
=========== ============================================= Command Property/Method =========== ============================================= ``AAD`` :meth:`SMU.adc_type` ``AB`` :meth:`~AgilentB1500.abort` ``AIT`` :meth:`~AgilentB1500.adc_setup` ``AV`` :meth:`~AgilentB1500.adc_averaging` ``AZ`` :attr:`~AgilentB1500.adc_auto_zero` ``BC`` :meth:`~AgilentB1500.clear_buffer` ``CL`` :meth:`SMU.disable` ``CM`` :attr:`~AgilentB1500.auto_calibration` ``CMM`` :meth:`SMU.meas_op_mode` ``CN`` :meth:`SMU.enable` ``DI`` :meth:`SMU.force` mode: ``'CURRENT'`` ``DV`` :meth:`SMU.force` mode: ``'VOLTAGE'`` ``DZ`` :meth:`~AgilentB1500.force_gnd`, :meth:`SMU.force_gnd` ``ERRX?`` :meth:`~AgilentB1500.check_errors` ``FL`` :attr:`SMU.filter` ``FMT`` :meth:`~AgilentB1500.data_format` ``*IDN?`` :meth:`~AgilentB1500.id` ``*LRN?`` :meth:`~AgilentB1500.query_learn`, |br| multiple methods to read/format settings directly ``MI`` :meth:`SMU.sampling_source` mode: ``'CURRENT'`` ``ML`` :attr:`~AgilentB1500.sampling_mode` ``MM`` :meth:`~AgilentB1500.meas_mode` ``MSC`` :meth:`~AgilentB1500.sampling_auto_abort` ``MT`` :meth:`~AgilentB1500.sampling_timing` ``MV`` :meth:`SMU.sampling_source` mode: ``'VOLTAGE'`` ``*OPC?`` :meth:`~AgilentB1500.check_idle` ``PA`` :meth:`~AgilentB1500.pause` ``PAD`` :attr:`~AgilentB1500.parallel_meas` ``RI`` :attr:`~SMU.meas_range_current` ``RM`` :meth:`SMU.meas_range_current_auto` ``*RST`` :meth:`~AgilentB1500.reset` ``RV`` :attr:`~SMU.meas_range_voltage` ``SSR`` :attr:`~SMU.series_resistor` ``TSC`` :attr:`~AgilentB1500.time_stamp` ``TSR`` :meth:`~AgilentB1500.clear_timer` ``UNT?`` :meth:`~AgilentB1500.query_modules` ``WAT`` :meth:`~AgilentB1500.wait_time` ``WI`` :meth:`SMU.staircase_sweep_source` mode: ``'CURRENT'`` ``WM`` :meth:`~AgilentB1500.sweep_auto_abort` ``WSI`` :meth:`SMU.synchronous_sweep_source` mode: ``'CURRENT'`` ``WSV`` :meth:`SMU.synchronous_sweep_source` mode: ``'VOLTAGE'`` ``WT`` :meth:`~AgilentB1500.sweep_timing` ``WV`` :meth:`SMU.staircase_sweep_source` mode: ``'VOLTAGE'`` ``XE`` :meth:`~AgilentB1500.send_trigger` =========== ============================================= ********************************************** Examples ********************************************** Initialization of the Instrument ==================================== .. code-block:: python from pymeasure.instruments.agilent import AgilentB1500 # explicitly define r/w terminations; set sufficiently large timeout in milliseconds or None. b1500=AgilentB1500("GPIB0::17::INSTR", read_termination='\r\n', write_termination='\r\n', timeout=600000) # query SMU config from instrument and initialize all SMU instances b1500.initialize_all_smus() b1500.data_format(21, mode=1) #call after SMUs are initialized to get names for the channels IV measurement with 4 SMUs ================================================= .. code-block:: python # choose measurement mode b1500.meas_mode('STAIRCASE_SWEEP', *b1500.smu_references) #order in smu_references determines order of measurement # settings for individual SMUs for smu in b1500.smu_references: smu.enable() #enable SMU smu.adc_type = 'HRADC' #set ADC to high-resoultion ADC smu.meas_range_current = '1 nA' smu.meas_op_mode = 'COMPLIANCE_SIDE' # other choices: Current, Voltage, FORCE_SIDE, COMPLIANCE_AND_FORCE_SIDE # General Instrument Settings # b1500.adc_averaging = 1 # b1500.adc_auto_zero = True b1500.adc_setup('HRADC','AUTO',6) #b1500.adc_setup('HRADC','PLC',1) #Sweep Settings b1500.sweep_timing(0,5,step_delay=0.1) #hold,delay b1500.sweep_auto_abort(False,post='STOP') #disable auto abort, set post measurement output condition to stop value of sweep # Sweep Source nop = 11 b1500.smu1.staircase_sweep_source('VOLTAGE','LINEAR_DOUBLE','Auto Ranging',0,1,nop,0.001) #type, mode, range, start, stop, steps, compliance # Synchronous Sweep Source b1500.smu2.synchronous_sweep_source('VOLTAGE','Auto Ranging',0,1,0.001) #type, range, start, stop, comp # Constant Output (could also be done using synchronous sweep source with start=stop, but then the output is not ramped up) b1500.smu3.ramp_source('VOLTAGE','Auto Ranging',-1,stepsize=0.1,pause=20e-3) #output starts immediately! (compared to sweeps) b1500.smu4.ramp_source('VOLTAGE','Auto Ranging',0,stepsize=0.1,pause=20e-3) #Start Measurement b1500.check_errors() b1500.clear_buffer() b1500.clear_timer() b1500.send_trigger() # read measurement data all at once b1500.check_idle() #wait until measurement is finished data = b1500.read_data(2*nop) #Factor 2 beacuse of double sweep #alternatively: read measurement data live meas = [] for i in range(nop*2): read_data = b1500.read_channels(4+1) # 4 measurement channels, 1 sweep source (returned due to mode=1 of data_format) # process live data for plotting etc. # data format for every channel (status code, channel name e.g. 'SMU1', data name e.g 'Current Measurement (A)', value) meas.append(read_data) #sweep constant sources back to 0V b1500.smu3.ramp_source('VOLTAGE','Auto Ranging',0,stepsize=0.1,pause=20e-3) b1500.smu4.ramp_source('VOLTAGE','Auto Ranging',0,stepsize=0.1,pause=20e-3) Sampling measurement with 4 SMUs ===================================== .. code-block:: python # choose measurement mode b1500.meas_mode('SAMPLING', *b1500.smu_references) #order in smu_references determines order of measurement number_of_channels = len(b1500.smu_references) # settings for individual SMUs for smu in b1500.smu_references: smu.enable() #enable SMU smu.adc_type = 'HSADC' #set ADC to high-speed ADC smu.meas_range_current = '1 nA' smu.meas_op_mode = 'COMPLIANCE_SIDE' # other choices: Current, Voltage, FORCE_SIDE, COMPLIANCE_AND_FORCE_SIDE b1500.sampling_mode = 'LINEAR' # b1500.adc_averaging = 1 # b1500.adc_auto_zero = True b1500.adc_setup('HSADC','AUTO',1) #b1500.adc_setup('HSADC','PLC',1) nop=11 b1500.sampling_timing(2,0.005,nop) #MT: bias hold time, sampling interval, number of points b1500.sampling_auto_abort(False,post='BIAS') #MSC: BASE/BIAS b1500.time_stamp = True # Sources b1500.smu1.sampling_source('VOLTAGE','Auto Ranging',0,1,0.001) #MV/MI: type, range, base, bias, compliance b1500.smu2.sampling_source('VOLTAGE','Auto Ranging',0,1,0.001) b1500.smu3.ramp_source('VOLTAGE','Auto Ranging',-1,stepsize=0.1,pause=20e-3) #output starts immediately! (compared to sweeps) b1500.smu4.ramp_source('VOLTAGE','Auto Ranging',-1,stepsize=0.1,pause=20e-3) meas=[] for i in range(nop): read_data = b1500.read_channels(1+2*number_of_channels) #Sampling Index + (time stamp + measurement value) * number of channels # process live data for plotting etc. # data format for every channel (status code, channel name e.g. 'SMU1', data name e.g 'Current Measurement (A)', value) meas.append(read_data) #sweep constant sources back to 0V b1500.smu3.ramp_source('VOLTAGE','Auto Ranging',0,stepsize=0.1,pause=20e-3) b1500.smu4.ramp_source('VOLTAGE','Auto Ranging',0,stepsize=0.1,pause=20e-3) ********************************************** Main Classes ********************************************** Classes to communicate with the instrument: * :class:`AgilentB1500`: Main instrument class * :class:`SMU`: Instantiated by main instrument class for every SMU All `query` commands return a human readable dict of settings. These are intended for debugging/logging/file headers, not for passing to the accompanying setting commands. .. autoclass:: AgilentB1500 :members: :show-inheritance: :member-order: bysource .. autoclass:: SMU :members: :show-inheritance: :member-order: bysource .. .. automodule:: pymeasure.instruments.agilent.agilentB1500 .. :members: AgilentB1500, SMU .. :show-inheritance: ********************************************** Supporting Classes ********************************************** Classes that provide additional functionalities: * :class:`QueryLearn`: Process read out of instrument settings * :class:`SMUCurrentRanging`, :class:`SMUVoltageRanging`: Allowed ranges for different SMU types and transformation of range names to indices (base: :class:`Ranging`) .. autoclass:: QueryLearn :members: :show-inheritance: .. autoclass:: Ranging :members: :show-inheritance: .. autoclass:: SMUCurrentRanging :members: :show-inheritance: .. autoclass:: SMUVoltageRanging :members: :show-inheritance: .. .. automodule:: pymeasure.instruments.agilent.agilentB1500 .. :members: QueryLearn, Ranging, SMUCurrentRanging, SMUVoltageRanging .. :show-inheritance: Enumerations ========================= Enumerations are used for easy selection of the available parameters (where it is applicable). Methods accept member name or number as input, but name is recommended for readability reasons. The member number is passed to the instrument. Converting an enumeration member into a string gives a title case, whitespace separated string (:meth:`~.CustomIntEnum.__str__`) which cannot be used to select an enumeration member again. It's purpose is only logging or documentation. .. call automodule with full module path only once to avoid duplicate index warnings .. autodoc other classes via currentmodule:: and autoclass:: .. automodule:: pymeasure.instruments.agilent.agilentB1500 :members: :exclude-members: AgilentB1500, SMU, QueryLearn, Ranging, SMUCurrentRanging, SMUVoltageRanging :show-inheritance: :member-order: bysource PyMeasure-0.9.0/docs/api/instruments/agilent/agilent4156.rst0000664000175000017500000000037114010032244024071 0ustar colincolin00000000000000################################################## Agilent 4155/4156 Semiconductor Parameter Analyzer ################################################## .. automodule:: pymeasure.instruments.agilent.agilent4156 :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/agilent/agilent33521A.rst0000664000175000017500000000040014010032244024241 0ustar colincolin00000000000000#################################################### Agilent 33521A Function/Arbitrary Waveform Generator #################################################### .. autoclass:: pymeasure.instruments.agilent.Agilent33521A :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/agilent/agilent33220A.rst0000664000175000017500000000034514010032244024245 0ustar colincolin00000000000000########################################### Agilent 33220A Arbitrary Waveform Generator ########################################### .. autoclass:: pymeasure.instruments.agilent.Agilent33220A :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/yokogawa/0000775000175000017500000000000014010046235021576 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/yokogawa/index.rst0000644000175000017500000000052214010032244023427 0ustar colincolin00000000000000.. module:: pymeasure.instruments.yokogawa ######## Yokogawa ######## This section contains specific documentation on the Yokogawa instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 yokogawa7651PyMeasure-0.9.0/docs/api/instruments/yokogawa/yokogawa7651.rst0000644000175000017500000000030614010032244024464 0ustar colincolin00000000000000################################# Yokogawa 7651 Programmable Supply ################################# .. autoclass:: pymeasure.instruments.yokogawa.Yokogawa7651 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/deltaelektronica/0000775000175000017500000000000014010046235023267 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/deltaelektronica/index.rst0000664000175000017500000000057214010032244025127 0ustar colincolin00000000000000.. module:: pymeasure.instruments.deltaelektronika ################# Delta Elektronika ################# This section contains specific documentation on the Delta Elektronika instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 sm7045d PyMeasure-0.9.0/docs/api/instruments/deltaelektronica/sm7045d.rst0000664000175000017500000000033114010032244025114 0ustar colincolin00000000000000###################################### Delta Elektronica SM7045D Power source ###################################### .. autoclass:: pymeasure.instruments.deltaelektronika.SM7045D :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/lakeshore/0000775000175000017500000000000014010046235021732 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/lakeshore/lakeshore425.rst0000644000175000017500000000025714010032244024671 0ustar colincolin00000000000000######################### Lake Shore 425 Gaussmeter ######################### .. autoclass:: pymeasure.instruments.lakeshore.LakeShore425 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/lakeshore/index.rst0000644000175000017500000000064314010032244023567 0ustar colincolin00000000000000.. module:: pymeasure.instruments.lakeshore ##################### Lake Shore Cryogenics ##################### This section contains specific documentation on the Lake Shore Cryogenics instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 adapters lakeshore331 lakeshore425PyMeasure-0.9.0/docs/api/instruments/lakeshore/lakeshore331.rst0000644000175000017500000000032314010032244024657 0ustar colincolin00000000000000##################################### Lake Shore 331 Temperature Controller ##################################### .. autoclass:: pymeasure.instruments.lakeshore.LakeShore331 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/lakeshore/adapters.rst0000644000175000017500000000032114010032244024254 0ustar colincolin00000000000000################### Lake Shore Adapters ################### .. autoclass:: pymeasure.instruments.lakeshore.LakeShoreUSBAdapter :members: :undoc-members: :inherited-members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/comedi.rst0000644000175000017500000000043414010032244021741 0ustar colincolin00000000000000####################### Comedi data acquisition ####################### The Comedi libraries provide a convenient method for interacting with data acquisition cards, but are restricted to Linux compatible operating systems. .. automodule:: pymeasure.instruments.comedi :members: PyMeasure-0.9.0/docs/api/instruments/oxfordinstruments/0000775000175000017500000000000014010046235023572 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/oxfordinstruments/index.rst0000664000175000017500000000060414010032244025426 0ustar colincolin00000000000000.. module:: pymeasure.instruments.oxfordinstruments ##################### Oxford Instruments ##################### This section contains specific documentation on the Oxford Instruments instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 ITC503 PyMeasure-0.9.0/docs/api/instruments/oxfordinstruments/ITC503.rst0000664000175000017500000000041714010032244025170 0ustar colincolin00000000000000######################################################## Oxford Instrument Intelligent Temperature Controller 503 ######################################################## .. autoclass:: pymeasure.instruments.oxfordinstruments.ITC503 :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/fwbell/0000775000175000017500000000000014010046235021230 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/fwbell/index.rst0000644000175000017500000000052214010032244023061 0ustar colincolin00000000000000.. module:: pymeasure.instruments.fwbell ######### F.W. Bell ######### This section contains specific documentation on the F.W. Bell instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 fwbell5080PyMeasure-0.9.0/docs/api/instruments/fwbell/fwbell5080.rst0000644000175000017500000000030514010032244023541 0ustar colincolin00000000000000################################## F.W. Bell 5080 Handheld Gaussmeter ################################## .. autoclass:: pymeasure.instruments.fwbell.FWBell5080 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/tektronix/0000775000175000017500000000000014010046235022004 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/tektronix/index.rst0000664000175000017500000000053614010032244023644 0ustar colincolin00000000000000.. module:: pymeasure.instruments.tektronix ######### Tektronix ######### This section contains specific documentation on the Tektronix instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 tds2000 afg3152cPyMeasure-0.9.0/docs/api/instruments/tektronix/tds2000.rst0000644000175000017500000000023314010032244023621 0ustar colincolin00000000000000#################### TDS2000 Oscilloscope #################### .. autoclass:: pymeasure.instruments.tektronix.TDS2000 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/tektronix/afg3152c.rst0000664000175000017500000000032014010032244023737 0ustar colincolin00000000000000##################################### AFG3152C Arbitrary function generator ##################################### .. autoclass:: pymeasure.instruments.tektronix.AFG3152C :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/anapico/0000775000175000017500000000000014010046235021367 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/anapico/apsin12G.rst0000664000175000017500000000030114010032244023472 0ustar colincolin00000000000000################################# Anapico APSIN12G Signal Generator ################################# .. autoclass:: pymeasure.instruments.anapico.APSIN12G :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/anapico/index.rst0000664000175000017500000000051114010032244023220 0ustar colincolin00000000000000.. module:: pymeasure.instruments.anapico ####### Anapico ####### This section contains specific documentation on the Anapico instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 apsin12GPyMeasure-0.9.0/docs/api/instruments/ni/0000775000175000017500000000000014010046235020363 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/ni/index.rst0000664000175000017500000000057414010032244022225 0ustar colincolin00000000000000.. module:: pymeasure.instruments.ni #################### National Instruments #################### This section contains specific documentation on the National Instruments instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 virtualbenchPyMeasure-0.9.0/docs/api/instruments/ni/virtualbench.rst0000664000175000017500000000234714010032244023604 0ustar colincolin00000000000000################ NI Virtual Bench ################ ******************* General Information ******************* The `armstrap/pyvirtualbench `_ Python wrapper for the VirtualBench C-API is required. This Instrument driver only interfaces the pyvirtualbench Python wrapper. ******** Examples ******** To be documented. Check the examples in the pyvirtualbench repository to get an idea. Simple Example to switch digital lines of the DIO module. .. code-block:: python from pymeasure.instruments.ni import VirtualBench vb = VirtualBench(device_name='VB8012-3057E1C') line = 'dig/2' # may be list of lines # initialize DIO module -> available via vb.dio vb.acquire_digital_input_output(line, reset=False) vb.dio.write(self.line, {True}) sleep(1000) vb.dio.write(self.line, {False}) vb.shutdown() **************** Instrument Class **************** .. autoclass:: pymeasure.instruments.ni.virtualbench.VirtualBench :members: :show-inheritance: :inherited-members: :exclude-members: .. autoclass:: pymeasure.instruments.ni.virtualbench.VirtualBench_Direct :members: :show-inheritance: :inherited-members: :exclude-members: PyMeasure-0.9.0/docs/api/instruments/srs/0000775000175000017500000000000014010046235020564 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/srs/sr830.rst0000644000175000017500000000023414010032244022165 0ustar colincolin00000000000000####################### SR830 Lock-in Amplifier ####################### .. autoclass:: pymeasure.instruments.srs.SR830 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/srs/index.rst0000664000175000017500000000063114010032244022420 0ustar colincolin00000000000000.. module:: pymeasure.instruments.srs ######################### Stanford Research Systems ######################### This section contains specific documentation on the Stanford Research Systems (SRS) instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 sr830 sr860PyMeasure-0.9.0/docs/api/instruments/srs/sr860.rst0000664000175000017500000000023414010032244022172 0ustar colincolin00000000000000####################### SR860 Lock-in Amplifier ####################### .. autoclass:: pymeasure.instruments.srs.SR860 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/anritsu/0000775000175000017500000000000014010046235021442 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/anritsu/anritsuMG3692C.rst0000644000175000017500000000030414010032244024522 0ustar colincolin00000000000000################################ Anritsu MG3692C Signal Generator ################################ .. autoclass:: pymeasure.instruments.anritsu.AnritsuMG3692C :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/anritsu/index.rst0000644000175000017500000000054214010032244023275 0ustar colincolin00000000000000.. module:: pymeasure.instruments.anritsu ####### Anritsu ####### This section contains specific documentation on the Anritsu instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 anritsuMG3692C anritsuMS9710C PyMeasure-0.9.0/docs/api/instruments/anritsu/anritsuMS9710C.rst0000644000175000017500000000034214010032244024535 0ustar colincolin00000000000000########################################## Anritsu MS9710C Optical Spectrum Analyzer ########################################## .. autoclass:: pymeasure.instruments.anritsu.AnritsuMS9710C :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/parker/0000775000175000017500000000000014010046235021241 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/parker/index.rst0000644000175000017500000000050514010032244023073 0ustar colincolin00000000000000.. module:: pymeasure.instruments.parker ###### Parker ###### This section contains specific documentation on the Parker instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 parkerGV6PyMeasure-0.9.0/docs/api/instruments/parker/parkerGV6.rst0000644000175000017500000000030114010032244023565 0ustar colincolin00000000000000################################# Parker GV6 Servo Motor Controller ################################# .. autoclass:: pymeasure.instruments.parker.ParkerGV6 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/newport/0000775000175000017500000000000014010046235021453 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/newport/index.rst0000644000175000017500000000050714010032244023307 0ustar colincolin00000000000000.. module:: pymeasure.instruments.newport ####### Newport ####### This section contains specific documentation on the Newport instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 esp300PyMeasure-0.9.0/docs/api/instruments/newport/esp300.rst0000644000175000017500000000072114010032244023210 0ustar colincolin00000000000000######################### ESP 300 Motion Controller ######################### .. autoclass:: pymeasure.instruments.newport.ESP300 :members: :show-inheritance: .. autoclass:: pymeasure.instruments.newport.esp300.Axis :members: :show-inheritance: .. autoclass:: pymeasure.instruments.newport.esp300.AxisError :members: :show-inheritance: .. autoclass:: pymeasure.instruments.newport.esp300.GeneralError :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/hp/0000775000175000017500000000000014010046235020364 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/hp/index.rst0000664000175000017500000000056114010032244022222 0ustar colincolin00000000000000.. module:: pymeasure.instruments.hp ############### Hewlett Packard ############### This section contains specific documentation on the Hewlett Packard instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 hp33120A hp34401A PyMeasure-0.9.0/docs/api/instruments/hp/hp33120A.rst0000644000175000017500000000031314010032244022205 0ustar colincolin00000000000000###################################### HP 33120A Arbitrary Waveform Generator ###################################### .. autoclass:: pymeasure.instruments.hp.HP33120A :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/hp/hp34401A.rst0000664000175000017500000000022614010032244022215 0ustar colincolin00000000000000#################### HP 34401A Multimeter #################### .. autoclass:: pymeasure.instruments.hp.HP34401A :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/instruments.rst0000644000175000017500000000031014010032244023065 0ustar colincolin00000000000000################## Instrument classes ################## .. autoclass:: pymeasure.instruments.Instrument :members: .. autoclass:: pymeasure.instruments.Mock :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/razorbill/0000775000175000017500000000000014010046235021755 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/razorbill/index.rst0000664000175000017500000000053214010032244023611 0ustar colincolin00000000000000.. module:: pymeasure.instruments.razorbill ######### Razorbill ######### This section contains specific documentation on the Razorbill instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 razorbillRP100 PyMeasure-0.9.0/docs/api/instruments/razorbill/razorbillRP100.rst0000664000175000017500000000053214010032244025165 0ustar colincolin00000000000000################################################################################# Razorbill RP100 custrom power supply for Razorbill Instrums stress & strain cells ################################################################################# .. autoclass:: pymeasure.instruments.razorbill.razorbillRP100 :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/thorlabs/0000775000175000017500000000000014010046235021573 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/thorlabs/index.rst0000664000175000017500000000055214010032244023431 0ustar colincolin00000000000000.. module:: pymeasure.instruments.thorlabs ######## Thorlabs ######## This section contains specific documentation on the Thorlabs instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 thorlabspm100usb thorlabspro8000 PyMeasure-0.9.0/docs/api/instruments/thorlabs/thorlabspm100usb.rst0000644000175000017500000000027314010032244025426 0ustar colincolin00000000000000############################ Thorlabs PM100USB Powermeter ############################ .. autoclass:: pymeasure.instruments.thorlabs.ThorlabsPM100USB :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/thorlabs/thorlabspro8000.rst0000664000175000017500000000033114010032244025164 0ustar colincolin00000000000000###################################### Thorlabs Pro 8000 modular laser driver ###################################### .. autoclass:: pymeasure.instruments.thorlabs.ThorlabsPro8000 :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/ami/0000775000175000017500000000000014010046235020523 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/ami/index.rst0000644000175000017500000000046314010032244022360 0ustar colincolin00000000000000.. module:: pymeasure.instruments.ami ### AMI ### This section contains specific documentation on the AMI instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 ami430PyMeasure-0.9.0/docs/api/instruments/ami/ami430.rst0000644000175000017500000000022414010032244022241 0ustar colincolin00000000000000#################### AMI 430 Power Supply #################### .. autoclass:: pymeasure.instruments.ami.AMI430 :members: :show-inheritance:PyMeasure-0.9.0/docs/api/instruments/ametek/0000775000175000017500000000000014010046235021223 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/instruments/ametek/ametek7270.rst0000775000175000017500000000030714010032244023541 0ustar colincolin00000000000000################################ Ametek 7270 DSP Lockin Amplifier ################################ .. autoclass:: pymeasure.instruments.ametek.Ametek7270 :members: :show-inheritance: PyMeasure-0.9.0/docs/api/instruments/ametek/index.rst0000775000175000017500000000050714010032244023064 0ustar colincolin00000000000000.. module:: pymeasure.instruments.ametek ###### Ametek ###### This section contains specific documentation on the Ametek instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 ametek7270 PyMeasure-0.9.0/docs/api/instruments/resources.rst0000644000175000017500000000033214010032244022510 0ustar colincolin00000000000000####################### Resource Manager ####################### The list_resources function provides an interface to check connected instruments interactively. .. autofunction:: pymeasure.instruments.list_resources PyMeasure-0.9.0/docs/api/display/0000775000175000017500000000000014010046235017027 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/display/plotter.rst0000644000175000017500000000017214010032244021243 0ustar colincolin00000000000000############# Plotter class ############# .. automodule:: pymeasure.display.plotter :members: :show-inheritance: PyMeasure-0.9.0/docs/api/display/curves.rst0000644000175000017500000000017414010032244021063 0ustar colincolin00000000000000############## Curves classes ############## .. automodule:: pymeasure.display.curves :members: :show-inheritance: PyMeasure-0.9.0/docs/api/display/index.rst0000644000175000017500000000043414010032244020662 0ustar colincolin00000000000000################# pymeasure.display ################# This section contains specific documentation on the classes and methods of the package. .. toctree:: :maxdepth: 2 browser curves inputs listeners log manager plotter Qt thread widgets windowsPyMeasure-0.9.0/docs/api/display/Qt.rst0000644000175000017500000000032014010032244020131 0ustar colincolin00000000000000########## Qt classes ########## All Qt imports should reference :code:`pymeasure.display.Qt`, for consistant importing from either PySide or PyQt4. .. automethod:: pymeasure.display.Qt.fromUi :noindex:PyMeasure-0.9.0/docs/api/display/browser.rst0000644000175000017500000000020014010032244021225 0ustar colincolin00000000000000############### Browser classes ############### .. automodule:: pymeasure.display.browser :members: :show-inheritance: PyMeasure-0.9.0/docs/api/display/windows.rst0000644000175000017500000000020014010032244021234 0ustar colincolin00000000000000############### Windows classes ############### .. automodule:: pymeasure.display.windows :members: :show-inheritance: PyMeasure-0.9.0/docs/api/display/log.rst0000644000175000017500000000016014010032244020330 0ustar colincolin00000000000000########### Log classes ########### .. automodule:: pymeasure.display.log :members: :show-inheritance: PyMeasure-0.9.0/docs/api/display/widgets.rst0000644000175000017500000000017514010032244021223 0ustar colincolin00000000000000############## Widget classes ############## .. automodule:: pymeasure.display.widgets :members: :show-inheritance: PyMeasure-0.9.0/docs/api/display/inputs.rst0000644000175000017500000000017414010032244021076 0ustar colincolin00000000000000############## Inputs classes ############## .. automodule:: pymeasure.display.inputs :members: :show-inheritance: PyMeasure-0.9.0/docs/api/display/listeners.rst0000644000175000017500000000021014010032244021553 0ustar colincolin00000000000000################# Listeners classes ################# .. automodule:: pymeasure.display.listeners :members: :show-inheritance: PyMeasure-0.9.0/docs/api/display/thread.rst0000644000175000017500000000017414010032244021023 0ustar colincolin00000000000000############## Thread classes ############## .. automodule:: pymeasure.display.thread :members: :show-inheritance: PyMeasure-0.9.0/docs/api/display/manager.rst0000644000175000017500000000020014010032244021154 0ustar colincolin00000000000000############### Manager classes ############### .. automodule:: pymeasure.display.manager :members: :show-inheritance: PyMeasure-0.9.0/docs/api/experiment/0000775000175000017500000000000014010046235017542 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/api/experiment/index.rst0000644000175000017500000000040214010032244021370 0ustar colincolin00000000000000#################### pymeasure.experiment #################### This section contains specific documentation on the classes and methods of the package. .. toctree:: :maxdepth: 2 experiment listeners procedure parameters workers resultsPyMeasure-0.9.0/docs/api/experiment/procedure.rst0000644000175000017500000000015614010032244022257 0ustar colincolin00000000000000############### Procedure class ############### .. automodule:: pymeasure.experiment.procedure :members: PyMeasure-0.9.0/docs/api/experiment/results.rst0000644000175000017500000000014514010032244021766 0ustar colincolin00000000000000############# Results class ############# .. automodule:: pymeasure.experiment.results :members:PyMeasure-0.9.0/docs/api/experiment/parameters.rst0000644000175000017500000000040414010032244022426 0ustar colincolin00000000000000################# Parameter classes ################# The parameter classes are used to define input variables for a :class:`.Procedure`. They each inherit from the :class:`.Parameter` base class. .. automodule:: pymeasure.experiment.parameters :members:PyMeasure-0.9.0/docs/api/experiment/workers.rst0000644000175000017500000000021614010032244021760 0ustar colincolin00000000000000############ Worker class ############ .. automodule:: pymeasure.experiment.workers :members: :undoc-members: :show-inheritance: PyMeasure-0.9.0/docs/api/experiment/listeners.rst0000644000175000017500000000022614010032244022275 0ustar colincolin00000000000000############## Listener class ############## .. automodule:: pymeasure.experiment.listeners :members: :undoc-members: :show-inheritance: PyMeasure-0.9.0/docs/api/experiment/experiment.rst0000644000175000017500000000040414010032244022443 0ustar colincolin00000000000000################ Experiment class ################ The Experiment class is intended for use in the Jupyter notebook environment. .. automodule:: pymeasure.experiment.experiment :members: :undoc-members: :inherited-members: :show-inheritance: PyMeasure-0.9.0/docs/api/adapters.rst0000664000175000017500000000332514010032244017715 0ustar colincolin00000000000000################## pymeasure.adapters ################## The adapter classes allow the instruments to be independent of the communication method used. Adapters for specific instruments should be grouped in an :code:`adapters.py` file in the corresponding manufacturer's folder of :mod:`pymeasure.instruments
`. For example, the adapter for communicating with LakeShore instruments over USB, :class:`LakeShoreUSBAdapter `, is found in :mod:`pymeasure.instruments.lakeshore.adapters`. ================== Adapter base class ================== .. autoclass:: pymeasure.adapters.Adapter :members: :undoc-members: ============ Fake adapter ============ .. autoclass:: pymeasure.adapters.FakeAdapter :members: :undoc-members: :inherited-members: :show-inheritance: ============== Serial adapter ============== .. autoclass:: pymeasure.adapters.SerialAdapter :members: :undoc-members: :inherited-members: :show-inheritance: ================ Prologix adapter ================ .. autoclass:: pymeasure.adapters.PrologixAdapter :members: :undoc-members: :inherited-members: :show-inheritance: ============ VISA adapter ============ .. autoclass:: pymeasure.adapters.VISAAdapter :members: :undoc-members: :inherited-members: :show-inheritance: ============== VXI-11 adapter ============== .. autoclass:: pymeasure.adapters.VXI11Adapter :members: :undoc-members: :inherited-members: :show-inheritance: ============== Telnet adapter ============== .. autoclass:: pymeasure.adapters.TelnetAdapter :members: :undoc-members: :inherited-members: :show-inheritance: PyMeasure-0.9.0/docs/tutorial/0000775000175000017500000000000014010046235016454 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/tutorial/index.rst0000644000175000017500000000026614010032244020312 0ustar colincolin00000000000000######### Tutorials ######### The following sections provide instructions for getting started with PyMeasure. .. toctree:: :maxdepth: 2 connecting procedure graphical PyMeasure-0.9.0/docs/tutorial/pymeasure-managedwindow.png0000644000175000017500000015223013640137324024027 0ustar colincolin00000000000000‰PNG  IHDRW:šŠ¦ pHYs  šœtIMEà ë– IDATxÚìÝuX[ðwf{—¥AZD°±P,ÄÆÀîŽk‹^»»»ÅVÄÀƼög+6"J7ËÆÌùþØ/*WWxÏ}¼°Ìœ={ÞsÞs†r !„B!”§!„B!„¡B!„BC„B!„† !„B! B!„B* „B!„0T@!„Ba¨€B!„ÂP!„B!„¡B!„BC„B!„PqÃýá=+Ú¥V¥äf\J„áB!„B: ]ÉDg(®|IýH¹þžPg¯ì×y¹9W®\ß„B!„tŠïÇgîüoIxœ©¤â¯úò3úµj!‘HX–Åw!„B!R©R%'''êÀÑíªœï<3¸:‹ÅbŒB!„ÒA„‘HÔ´ZÅkñ·^g8üp9ß=ÏÀȪT©BÁ÷!„B!U±bÅšüŸ)á»GŒ€QG*xöB!„ÒeÖ’_*hŠeY B!„ÒqBÎO­Uú#Óš1T@!„B¨ØûÁPOB¡ïÂÄ]Y³ò\5>ÒÛœ‹'!„Šg¨€C !ôß`e‘.‡Ý~òösª¥J»T­]§v+~Ü¥5k.ÅץDŽnÎB&þ꺕çbvê6©§Õ³‹}Ê)ƨÁÐ-²+øÌ[çí}Ÿû™ÄÕúikËÿ…¯EP4üA¡BŠˆˆˆŒŒôðð (JÝ¿qㆭ­­Ž† ,˪!„PÑa’ÙxèIZÎ#ò¤ÈG—ÅØõ¯ ´ºÚ¥(u;›h?ÀÑ3·2NŽŠOYXò´{uH>U6Gý⎟ì'#C„PɲsçNèÙ³ç÷î¦Îèñðð „\¿~ýíÛ·oß¾mÖ¬™©©©Ž† 4M㻎BEˆM}|ìIè¹ùvkëá`Èa3â^ß {ÇÏժׄ Úm‘‹ßˆ2/6ÍØùÀ¢QÿឆŒBõukܲõ¤‘žFêê›(âîîžèe&\:ŽèìÆýxjݶ;)Àuh5ÔÏèöGQ‰J”PjWÞ³y“jB6îêú5â@âV×5ãÑý÷)c·&~^¥^œ=sãU‘–oîßÖÝ”Çh6—­í"{òðceX¦Ÿ_=«Ü/I¿zúÂWÑé ¤NÕ½}¹aZB¨û^SSS‡7oÞ¼zõЦi•Jõöí[ptt411ùÝ.* „N|ƒ¤½¹ûZ’ê]üjYs”™2%ÐúµÚ9{0 …BöÍ…\Îh¾1X…\¡âä·ÙçàÁêõ«÷Õºm»§+ö=—¿àñžž>U®\ÏŠ"Ífáÿ‹´3—òÒâ’__ÜÁ~Xm­–½9¹iïý4Jê\µÿË£§onØÃ2Ø ç0 „Pn 6‰DOžYYZèÕ0®½ÍçC 旅¸jŸñâC¯¹–ÌÄJVVjžBê5hdS øtrÉškÉiÏE»eG6IO.ÜO0oìײ’•f¿1ôKÜÃDZuÍ,0V@{÷îÕ^ h×®]êºwï^øBäry5"##“’’@*•º»»Ëåò_óp±T„Ò(M3ZŲ„e•ʬ¿(¬vÛ?+T€<‘ÉýH®²5¿i% Uf†œa„®­Û–{sà…B(´«(fX¬üóõc»î¼ŒÎÈyV†%D3œÌó” ‰@:ð%UF&ÏX •ö¡¥L&ãêÙ›R×’IZ|:c”%¼‹ˆ>¿qÅùìÔ%ÉðÛ!¤#öìÙ“ïãݺuûŽš½€*í»ª:–e¯^½ªŽ !!áâÅ‹uêÔù59>* „. õ-ŒR|y•YÆÑkìÂfª7{çl|( €šK0 %C`•rÍ¥IîØáë‘ßì_T²´4žöw Qf*³:¼˜Œ”t9+ä2 ·‚ö„}–oP§œá— Áw“r‡„°${Z5°,Èž8gRa@çÜ('°1ñìÐ${&ŠkhNã· BH·}W-5`Àõ7n€ªMKK+|«ûæÍ›>|WWW†aÂÃÃ?|ø@QTíÚµA´€÷U@!ÄNˆ·LÆÿ5nß¼†C)Jžó…Ä‘˜Iy£$o?޶¯n¢ŒøßãC+Î?)›üøäÉpM¶+‰:ìN™^ú‰ï£€ròiç]–wãZAß—…z†´·?ÚIs¾p8¥ìáùHŽVš6ªdÌ`3">LQÚš ñR@é‚ìf}…oå½ñwí«ÿñãGpvv®U«EQ üyó&""ÂÙÙYGW@¹ !TôÄåÛµy¾þÈó Ù« {^]šC±LvµË³mPÛøÅÕxå»3žÉÚ‡çܰª”fÓ_žØ{îÍø¶kóÛÚ~Ü¥œ¼úϧVÎ=¥ùÙ zïÁžñ'O¿VHë èÈ9x9.êbðçne, àuysxË6Kòåm´VÕùüœëKä ?$\ݹþ¤$¦³\Çú•KQ²v¡ +5­zeçƒô×Á«æ_6–ò ñ©*iƒAUõp)n„.HMM-ò2 ýlnnîííýþýû:uê¨ç'Ô¯_ŸÃáØÛÛ›™™1 £‹¡& !„PÑ#D\¹ë³{ç/Þ|ò&:aŽžii‡²ª¹è!´yãA BÏÞ|ò>A@KÌÊTmèÓÈŲ&3þKbº¦YRLl†*gjA®i Ù’þ8äÌ%€~Íöõ,Í¡½Çƒ7“?] ~àÒ£K‹Øç^$|‰Q¹×©­<ÿO|žï¸\ýEyº²暘R?°À•–kä×¾¢„ÕÚ„çÐfD_³3ço?‹HŒJí*Ö*£ H¡bI=F‘’’ò½;ªT*333kkk™L³žL&óôôT*•*•ê9å:#ô»v˜Z6¾aÆb±ßu„*úJ™Ë ø<.‡ ,£R)r¹ŠUÿQ òyšªòL¹‚!4_O_¤•‡ÄÊÓR3súš(žÄ@œ§gˆ(Ò3i±ˆKQ¤§ÊTÀéIø4“™š®â‰ÅB. „QÈ®O«K$}}¡æg†èé 9Y¿¨€(ÒReŠ„Û[ן©×¨Ñͬø°ªLY†\EhíÝh®@(äs³^ £RdÊä*ŒB¨(:ujv¸ñïþ#£ ' „Є¨2UA7Q *¹,MžÏYEZò7Ö×&Êôääüþ¢µ“J–šœ]´<=5g¾œg”§&˵­_ò;635%™«ÀhíÀªäir|ËBH‡ýà\ܶ2$J$ļ;v…©Õ{jÀà†ì¥¥ NDæ›;ijï³pXYŽÔwîöÃûö®hiQpCÑ(šži•ÖÍ^¸jÁØ:©Go¸›FT‘AëÂ(÷NƒýÜô¹†®ÍûÏZ¶jÃÒ‰¾¼°¥+ÂbYõ¡^ÞÿÊ©÷äq=í^ïš8<༨Õè€áµÒC×ïz&’roEÀ¦ˆ*C×mß¶y|ø}sV?1ëëðL¢ŽÌœ~NÜaæ¦}[ô2ûgñÜUÀfÜY·ã}iŸ^=R¡³WÝ2ï>oó†å3z×Ç$©ðr+X៷µê‹0Nȇ¾ˆç`%]u§†#„B¨p‹²¬R-æÏ\^à.¸~bÖýOòN¦À-ÿל ^¥(€FooL¼t+®•g>{s„¢ú††v¾UZä½CkÏ';v«ia`ã×È"5>ž®YÛ2äÊ«Æ À±èºdY_Gu!6½í¨Òbyîžvûö¿ˆQz—൘=µ§›2×ö?•š5¬®>(­^ºr÷mŠÒòA× ý7v­mÉ0ëØ·Æ©¹#` ÖáÉ_l>S{Ì\ßòbËf=Úrù^|3[ Õ&ήkHìÑ’XÖ´m 7{SÚÞÖÙ½1^lÿ-@á¦77ÂÓõ Áw"ñ$ „BH·BàðxêQ Zd$ ”š©Î­»ZºZP·#¨{}:«³/è¹µ ˜ÑÁ†«ŒÛ¾20øQ Ãð3CK(¾žPóŒLâýÝ+Üþ$§„âL§´z y\ €H´œKPB=Å(ù—gŸdï6÷ë°C=C¥P°•ÒícIyŸ>½ûuõó¥\i—ÎÚÅéñ(¡}ƒÚ†3—|É£†»{ݦõ]¥\¼Þ Tø$}žO׿œ".ž"„BéZ¨ƒ¢ò›æLC(._Ý”&ìw–é8pјš†"#s3C>À| Y8ï$í?cKçjæÂ¤ #{ïûêù’o¬œ·û³÷ÔÀež6ù½YûH5?¸åGT“dÿ…§/a¯k•̰“v³úgϬ¦8"Þsí‚ jŒß°Òó•›÷ï-9¾ÿÖßÛ&Õ.Eá%—?uRö¿ßØÒˆ‹Pý ^;w]ZÞ0ýéá5Wy fW3 ùŽå¥‰çv«ÐÊ*ñö‘]W2ØÚ”ÀØF’xçòçT)•ı‚µè›]ð\3g3v÷±ƒa⪂—öl{¡’VÏÛæ7r²ä>}ôœu£¸Ûû7ßͤ 3c€cV·c f-ŽèXËŠŠyëüM^§i´Ϧƒ¯å ³VCŸf.ú™^8û¶ÖÄ­R2¬˜~ɶU‹ŽziÄr¬›áDÜ‚>ɈÍß;E<ù»ïêÕ©'઩ßM.—¿zûJ©RšHMðl „ÂPá#…„‹W¼€=£»=œ¸gFÁ›²q77ü½í‹Râà5tÁà*ú€c§ ^/سpªÀ¾qÇ.µ>î”í6Àëɪù#Žë»^=ïßB+Ÿ¿>]¸}áßû¤n­Úµqøp5ï&”´þˆ¡÷æl^25D¿LãÎíÝ^/Ôk£<'.µq}Т {åÀ7-W§M/ž@/×áu;Ÿ»aãΙ!),èÛ¸{ùÙ‹¨X­BøVÕìâ÷.·=8R׿&6µààõV L@*ÊP¯¨èVC¡P(x®~„“½ÓÕW½ê{á©@!TÂQ®3B¿k‡©eã›5kVØ­¯ÖõÞçò˜¥ŠÄÞÓaûµûeO—ri¨÷BÛMÇ&Uþ½ÉcòKü=y|K“Ÿð²vïѶ jþÆZE»Oá?)˜üçÇrõæÕ¦šâÇ !„П.44tv¸ñò Pø$¼ F}¿¦Shˆzvµ;Œ èWϬȮðôÛÍ's–Ï©).Ìælúës›6ì9yëu¼ø&ejzµêÞ½mMó_‘…fÄ“–Š"ßl9ù <ÈÿIÉOUØ}PدBa¨€tHá¾…æ×·r¤+7-êNÐò£'ÝØÉæ7\ã$ýñƾƒvÆUï6lþ˜2úŠÏÏož<¸a‘Q}}íA¬@S@€Ü-f´[}–?˵y™Q›f7ʫȧžýêO”Ö2 WëZölÙà™¯[-[ÕÁ𫽾XÞ•rþFr5”ösç $¤\›4hoÙ¹+²n™’Mùfç¨ñ[®_ä“•ã—ïúk@   ÿÙ8B!„¡Â7ð‡î:†g2Z€ÂÝ‚í׳¡mYWW¸º•an]ž~ùEz'C`SŸï_²pÓÙð$bP¶ùÀYÚ•Éß\1}]È“VdR¦N·sÛ&Íl=&iFÈZO=&ê`Ÿv‡íßÝ[}ïhæÓÞÞ#ÃÒ7ª`ÖmÛÎöoÖåÚÝ_««|·{ÎΈê“-omͨT¹f³Ý_pä/–ø zÔ5 Ö­Àíÿs\v°ÇíɳN¼ø’¢EµNc¦¨o‘{ý»©Ü¯vÄÑà»1”uýÁ §wqãQ|¹¶jئ]÷bEeÛN[<¦‰yŸ_–eóiô–Å«Ðs|7§¬ã¥%V`YBeoDåj2S¹öÏ)ŽÊõ;ÉÙkÙbà°”Ò†4ɵ.ùºñžqyâÍ_K°,aó«Ùƒ%„Êýò~@¾þ+ù¡Ñ„BC„þ?5˜_+U1À+%æ0Ñ!SGoåö˜´Ð6óÑŽ©sÆl*ÔîþÄùWÆ®™VUûüzèËDå7[‡K¿5ó͙µgr51‡›t¼_ÞÝ!;TPD\<õ¡”Ïä&ÖÚ/žq[9(Ÿ¬\I5ó2²Š÷Á½Ó”A•mõåoO->c™ûñù Š:ß·÷ô.ܧۗ¬·¹êÑÁ z¶n»eÿÁKüÒίX>{U¹µô ÉÕ‹Oiµé)Ž¡m—²Âì²*êĸ }–Ìô6å°IW—Ž['ó_Ö_¶`ü¹rª|>ñImîÞiܰN" Ø”GÇ6o:qSª¬WŸa=ê™3–[¨ðïc~mϹ˜“ÿ’ìÞð´ÃJwÓÏ[GOÚÈßñÎá ëº}'uÖ¿¸~Óñgi¥*´›0¦mY1TÚ™_w³ûÏ¿I5pi7bt7—¸í×¼PÁ‹€^Á¶=–/miÎ!9a aµ–C&˜¤G7l?ñ¿èLžqyïîûÖ2ç“úâ膇ï}‘óMl #õ{ÎidLçzë) C„BC¤;Š&IƒI}yý–gÆÍ—W”€òý©íÇô¯-¥¼õ?¼þò›Ú1ÑÄ¢‹g¥2ætWÙýoIóõôø4‡£_ª”‘2޾λ»eì«h°®Rº …Jérã·mí I–é>¨<°Šä¸Žg½Ò‡.8À»¬ÀÌ«S‹Sn¦böj§a)½ªAÛ½N»rëŸí3ö︺àÈd½Â÷&Ó¥êÌ̳ûìúRMãx&N¦ðøŸä­MD ®:uçÖ4Uì©©“Îç-†‰ ž6ù0Õwé¡^5­„‰§z·Ýúõs–ÍKÉAÑÿr„„ä™ÔL²c ¸zŽÏž[A‹Ìõ€À±Cÿ×ç],Ý~qSK! !@Sê¦4ÇÄÑœ{"*11âMbêÛEÃns4—BiŸÁZŠ#s5üš!Í¡!|1¦hŠ%„-qY%£Jùü&1õí¢¡¹JKg¬Es¸êÝ(ˆRe*YBg½®¦Ì–=ʬÜÌ2.‡Ë ¼Jó²[ß„°Ä°lcÿ²ý‡Dh¹úò;e;‡LU°4FÁäm-R4°*FÓàf©<»ËëKEš ù¶ ½-·ïÛt¶ßò6Ö<=›²å€1x üº£ZñéÁ¨0¥[m+1›P“Kæû»ï˜Òݬ¿ó¶Ë,ËC°„âê[Ú99µ·f‡Ç€Ì4™’a9Ë@X–e)`‡U±ióq“ÛfÏà¸B=Î+MPCX*+à"$k2˲¬¦ëŸ°,˲ÀŠ`YæëÒ8B=úUÖ^,K²öÓD ꇳ6WG?šçc³C–ímÕû²ŒŠ¥h êYVÐäŒE䉪B!„¡Ò¡h~v$5Ú Ú°•c_û/»ÐaûÔÚ­»W Z4~¶xtÇ&ªO.¿o?¬Í³eç:¶­ã¬Ÿzïv4Ǧ……Īš}tÇÖc’šÜ§6m}§²ËU(ߨQšqôä҆¶D¼öªc®Ýµðä—é=©Ý¹Q {Œù8¼s]gCH‹yr7îëH€gîjÉn ÚuNRSð>tóÚ'J“Zê¿(£¯Ÿ¼T®Ž…2Ö’ iÎk2Ž4+e-H¤žM ‰Ôùš”!žmëñ9»÷æ”Ö¢´€Ä«Mt‘vðìÛv®¾sÝ´s’JýfŒ7Ð9¯ þÒÚE—²^Q鋦µøkxÆ–½kfžPpŒ\¼´·çsÙ.Ã;$_µT¥çàÙ ‚á‡D.•uÆœ7…ÀP!„R-ºÎý®¦–oÖ¬ž8ô»œ½x¶‹_—büU‘{ýýO¶Ú—;TøA‡‚*¹V"…IºÏ¹ñ•'IŸ‰ 2õFýÙÓZšqUP?õ}©ýßí?¬ã4·dËó¬ò[V©-è"Ì}|„ÇÏ7mÔ?n!„þt¡¡¡³ÃxwU@:¡¨J:6ûÆù6Ö³eß³L{aU Ú““YÍ‚@TöæTξy﬜ϽËQ·¿s äs伿Q_ßuùûiÝŽYñîÜéÎeÌ…™oî»MUeÃ#yï(# !„† H·é-ØþðÏd鮯výÉB²Ö¥¾Ñ|Îõ*ß¾öj¢ä«½¨|b¬†6É·´œaò/1ÌWáKÎè•ß䛯SƒQÆ=9|2U}»Z~û: A{$*÷1"„B*à)@º-@Ѭ€„4KæÓä-¨ üuÓÝÄ{ÎZﬢþ}’_›ÿë ‚t…mœ±Pß(\ƒïÔ)`U§\ç‰äM¬yÙ!„ÂPéL(Z„°?Ó9þ3꤈Ë+ì’¢+ !„B* ‚ HE*B€úºéKéÄÑð8¥;ç/!„BC¤[Ñ`Ra‰zÕ¢¯RròÌ^  Õ:¦ ÛÌ'lFåú‰YhP?T†ŽE,!„† aRQ+ø&b„ü@ã¼›åAëyHA›‘ÚåÔ7º  †pÈÔ·^ •÷'B0X@!„0T@:ŠÖëw¯ð$ü¤³ÏâI@!ôGøïî„¡Ò¡h0©(tîÐOB!TBì?¼ÿ¿+C¤¾k0à]²B!„þ{4ž¤ Ô©Gê˜ÏB!„† åŠB!„0T@H& „Ba¨€P>0 !„BC„ ŒB!„0T@H& „B•àP!õúhߎsfþWåËŸ/ó÷õöñõöñõî±1\¡x¹¡góñWS~n]ÍŒs[w\ð@²Çó:úöÞ÷Q…NQÃ$„B¡*ü×.öl\æUØØ» ÿ?x ¾M›‘cÖ‘rðÂùo¢À$„B!ñ»oÁ¦ø|yÛÊÀSOb•BëmGŒòw—r€d¾ \ºóÂË$Vß¾^×Ãü\õ˜¨#ƒŸtówÿt:ôaeY»çÔqmËŠ)í°G –ˆ¸ÍIļ԰‰Ã‚¶o¹DuïžèwqýŠgž'¨DÖu:ßÑMôùÈàÁgklüéÀ¾›’A[†%®Zü8*E\#—ÆÇ ò¢BFN¹™0¾ƒ/€I‡¥œÍ+vÛêa'aîí\±áðÝÏ™<“J>ý'ô­kÁ“ÝŸÛkº¬[?‹°§ÂS ݺL˜Ò·ª!$ýo÷òµî|Îäê[»5<¡¯‡TvmbçÍVnQ^€!ÀwÞ‚ !„Bý¿wT!óùŽ¿ç^·›¶róʱ 3Žý=38RERú¡M÷¹×/Vñã–¿—\ˆgTѧÏ'Tè2vƨ–†÷·ÌØóZ^`É”gÀÂf¥8.#¶íÝsxÛ¨ Š{+6ETºnû¶ÍãëÄú~Udк0ʽÓ`?7}®¡kóþ³–­Ú°t¢//l銰Dsßù“Ý…¢šÓ·î9´¦}Vd¥Š<8}ÖaYƒI+×m˜ÖJ|iá„]¯ä¬ìþ¶“ŠºÃ§Mìë´lÿkEúýõ ö%Ö Xµ>pÞȶ™Ñé P4 4NÉ H!„Bºæ·Ž*È^>›PiÈ’ŽÕ(pê1ºëõ!g>Ôu:|]UwêpŸŠzŽýF½º9îÐØzÕ¸¶ýfMìbͨ]:öÖ€sW"z9;hDq%zšâŠ (664èš¡ÿÆ®µ-9fûÖ85÷â»Ìn‹®K–õuT—bÓÛˆ*=!–çîi·oÿ‹U ¦9z†††"E´ºpÅû3ÇߕK]+.€Í˜wº¿ð @ kMY>¦†€\úìøØç¯“RŸÒ%ÎÕ«–±ƒSàyÎ>WßWÑ`B!„† À$ˆÈ”Ö´—¨³ˆ¸FÎŽ¢ø—ŸcØÏ¬…§µH½ß²¢u6%ª 5'AñåÙ'Ù»Íý:ìP R(ØJé,P|=¡¦wŸI¼¿{eàÛŸä”Ð@œ©â”f ˜-~ rp1RŸ=JÏÞE*û_Dk Íå©‹ãˆJ‰)e&käޠ̺m“z½­åQ½j¯ÆµlD^w_Á$„B! ŠaYày…nt¸åGT“d?ÄÓ§Fim‘|cå¼ÝŸ½§.ó´‘ÈïÍê¼ø»è«G(uÈÀµõ[°Ó9ììõ{÷/l<t¦×ºå=ìyxåå¡N@ÊþOB!„Ðo÷;Óå9†vv„gïÓÔÍlUBø™´¬¥YYKúË“H™úQyÔÃOÄÌÙ4wãZþñakéfñÍ!ЦeXž™‹ûéE¢HšÍHÂÍh(ãž½S8¶lçi#á6{DVÅä.Y`^ÖXööe¢zÑT’öîe‚ÈÆFÎ?¨E•¦=‡¬Ø0ß§Ô‡ëOY¼î ˆB!„tƯU Ê„·OÑBõo<©Mû¦Ò‘×4ì^Ë úBàÞHÇ®ÍíL ýêr§¯]{zt§òÜ7'WŸH¯4¶Ž)G Š½}ẳ»™òMèÚàt÷±uÍs­ZÊÊ3Òe*Â*eé ¡˜kd+eC/^xdíJ¹GÇ:;ÌZ ѱ–ÿòÖù›¼N“kíÍ3r²ä>}ôœu£¸Ûû7ßͤ_jg$;uîÚ3k–k¡9r¾}Ó6ŽÁ;WY¨ož|gÛ¦§ÆM––Á³¯_²*òÐÌ]I¾^n¦Ô§»¯2 ìôiH¿à?=¢ÙÊ­Ãp$5L@!„*á¡‚êѦc²~7˜·ôœÉªå3GnTñ-«·ÐƆKAõ¡‹†mXºsò€"±õì3{¸· ÍDIùßÞiû>Éø–uzÎßPš«_þrm¿ñ§’`¾ÿ Öë¶ t®×¿ûåyëFsK·^¾zÀÄÅ£6®Z4a¯ø¦åê´ée’k¤‚’Ö1ôÞœÍK¦†è—iܹ½ÛËã\›CÛÜ™·|¡KÿųN[i¿é§¯Ø0oTœk\ÑgÜÂ^e… Ëç%sŒÜÊó×íšy AâÒž]U¤–Gr”À$–e#"#ÞxŸ–ž†B:KO¢gogo[Ú–..ËÖaåƒV>…G¹Îý®¦–oÖ¬Ù¯?PUÔ‘ÁƒÏ7ݰª“¯ž’ììų;t./ä}ÄûOQŸ\œ]Œ¥Æø¶"¤³ââ_¾zimemok_<^V>³ÊgÿáýM5-诡¡¡³ÃüÃŽÍn¤J`Òûï+¸U040T(x ¤³ ]œ]ž<{R|B¬|ÂÊCôg) HiéiR#©\.Çw¡_ìæÍ›…ܘeY©‘´8åê`åƒV>Å0TàZµßÜ/²â-@IZ‰¢( „à[Ð/FùÞžú‹•B¨¤U>8ª€tBÉ\‰eYg·#¤ó½â÷9ÅÊ!¬|0T@’˜€DQ!¿­úõ¾÷£G)f£ ÿ]å“ùdy—€¸‰As=$ºøÚUQ‡ 8×|ӚθD Âʧph|ÛîD P H÷¶©î»àŽæÎ~ÀÄœÞÈ{ȉ/L>_iK›zxÖt*æ[Ùƒ)M<Ûo}¯,òok ˆwf”W³êÿF\NN»7»eûy÷2ÈÏ`“. oÙcm¸œBHÚÕq-¼Fžc BèPðïßÖ›»Õk0érBV}BÒÌkU¿ãæ× Ý­|I¹:2«òñjî×ãïí7c•?u¦€d%YüäaÇ^á·/Ì—µÿø¶‡‡¬Üü¤æøJb ¯÷/9F·ÙàïÄ×áÊX¿ÚÐÙýËrÒ¿<<¶q×”Y†Û—´¶þÑÖºŠ!E“ßIJ„­—r~ÇÅÎó|L9À’U¯ýÄáÌ¿BŬòÁQT"†¤R©:føÙkÚ¨îè‘Õ£÷,:¡L´eþeã¾íìx_m'{êJjõa£šˆŸßId@öl…ŸgƒÉa ,¨>èÑlê¥ØØK«çmy– À¦=Û?©[óêž5¼Û÷ž{ú“¯Öùzxö ‰ûïíŽ=à$"Mѱ˜}tä”›éׯwðõöé½þ•œM ?²xd‡–¾Þ>þŸ|“ÁyøÚm‡¹¸õï>ÍÚMÿ'îñú±}Û¶öõöñmÙ+`ÃÍX¥<|Ýð%OUq‡G¶÷öñíäã‡Ð ËTBäQ—6Lòoãëíã×súî;ñ*Bؤ°ñÍ»-=vpQÿ¾Þ­zO>žŽã¨Ø´eñã‡ÓÒÓ²ÉÈÈ8~âè¦-~ cOR©ÏØú—y¯æóéÅ»¼Æö­"¡~¶ò!™ïBôlY¯º‡gÝV½&½U@‘U>„ 8ÖŽÎe\ªÔí8º_yxuãu!LüÕuñkííãëÝaà”½’¢®y†ì?8±gSßÖÃׄÅ( !DsmódÿV¾Þ-»Úò(ES{°)OÌÒÅÛÇ·i×ÑKμË`5% ÞsléðNÞ>íû,>ñåö¦‰=šú´í29èYûõϹ]G›ð]‡Âe¹ÆÒ®MlÛaÅ3õƒò7ÛºµšÀ$…oÞmñÓý}}½»NÚþ8æUÈâ~|½ý†-º¬>VPÅÞÙ=±w[oŸö=g Ï` !ÿ^ͦãÇén僣 ¨¤D P4+ q,št¨ÇúùÞ&s;®ëV†ùD ç.¥”^·–ÉÁ¨{‰Íš‹\ûOn{fØòÜÇY_[¼1Âcâ¼Ò„‡š^nXó¼ÎÔsʰQOÂ.NbÀh¨ ¸)`&»wAÓGXÚ¼ÅÜIû­ Ç¯QQÄ“BçMÛÇñ›´nеüÙÅ«¦írÚÔ“P½Ø¼‰jèݳoy[7¢R«QÝ]­%Šˆ‹K–VÚ:¾×¼¡O‡îsž±¤—Ÿ#â½ '@Ëf<Û>uÞeÛ~S–×4ˆ¹¼yùÔY¢ÀEmôYP%^ÚzÓ§ÏÈÉ’7GVn[y¢ÖòŽÖ¼,Q±”Ý©lnf}òÔ Ÿæ¾b±X.Ï,ÑÄ$*%<}°,Kø–5ºüÕ§œ¹0íÕÉeko¯8¨^í8dÞ­ç4ßô+›6-Û\»êÄò1AÓg†ˆÚ Ÿîa"m÷º×@XVsaîÔ Þ#—·W=?ºxõß«Œ×«@T¯ƒÎ9ö6­ùƒMëV ½cQµu¯©M"®Ù³êBýU¾æZõKǼi·º§æí¼Ü~VÓR,!š£e Ñ hF4/B•¶?¼cÿI £CÖîž4â¤s½®£&’6¯[¿«I¿ÜKTQÁ'?ûû÷%ÏlØž<ðÜqІ6UKQu»ûŸ>»óÆÇÎuÐNƒ—-ñ1U­¶ïæ D‘O»×²8yíU©a äR´@¢o`ÀÈ$’ñòȹ„Šƒt¨fDCב]®=ú¡¹·Tó9³º ªñ¯‡Ì}ð)ÓÏJŒ—7*®Ô½F^MNŸ IJN:z²QÃÆaW/%%%6òòÖ^аð{\k߉þ»Ï~›²´³¹%§*EÜëhbѹN%'sÚÉÁµv‹"­|rúéUiÿÙ¹/ܨÁôr"B¨RÕ;t`åÉñqejT3 Hcí ŽË¨ùSQÀØ|>ú"6UpôT”Kÿ ý›Òμ;».ÅPE_ ~ ç³¢wƒ²|Û£œyäá@73œr#fŒndD1öO\<]kR@{>¤‘óÇ–½ˆ‘·4å}—(I…ެFí:þº~OÑ-’õ“æWB€kÔ|Æän®Èà_;øL6|ú`O}PZ¾9rõîÛd•+àÚôþ{LG+@u«ØÛƒÏ_y×@\ˆjW—E:]ù“P!:6úSÔ§R}³RR¼VŠ ¹Rñ)êXš[þp!E¾Q§´Ê’2 è}õ1S¼½˜èÒ×ݘÊ­E î”àûÉM½hÚÐsøØú§­‰ª:9¨‰9 {6´ØµU zƤNkÕó¬U§yˆ.?•¿GQ”v¾¬V¯«•M ²¨§Q÷ï¼L½J¡›¦©Z áiÖQSF_ݵvóÉ'± G¬ÏÏ` ”*†%Ú…š1 eÒû¹‘»fÎ]ÊÑA”-gY.GSœÀPH’*ìCCžcÏç7mÒ"ôì©ää¤cÁG!úúúÍš´ „Ú×?˲…^„DX®Û_ÍŽŒû§þÈμüº)¾»ò:6©g8~n—®¡õëxxxù6®hƒ"«|Vöw—0Žc‹¿õ© &,Ëf¾?¿uåîó/“'ѧÒ[…Ša ŠÃ¥X–¥"J%ËLþð&ͨJÐÔ5êŠGC[¸Yh:ãùV®¦lð›X…)ŠæP,ËR„'áÓ „eY b«PåžC@4u!eÞ¤‡Ç…{®ùôΞ Áæ­àÔ ¹´:œ/ЙŠeY <ŸbJ–…ìM)Ú¸Œ÷pdL¤òß«Y„t½ò)¡BTT”‰¡‘±‰)—ÇÃË¥ØP)•Ü8nTTÔÏ„ P´·`S¼Þ3ï Êgh›{ëÞ­5¹¦AîšâÃ…ó_QóÚÕ˜§y„ò ¹q## €Ãp2ÒRå¹{8f­Ù^8}îæËSwîï°yïèªâÿS7TÈþd³'î±ÀªXJì1qq_aÖž´À€÷ùzÖ×$Lô¹¥‹NÓ'¯ó«b&H¾ä^²—·AÊ?k_uê3˜8?Ø{]‡ÒZÝ[,פró•›÷ùvkç§®QÕù'‚^ X–ÍI@Ê5žNQ„a!xfNƲ[ï’Ú¼œ÷I™=O_ž~‚²ÃÚT1! C4¥Ð°*&k|€Ðú6¶ÂÄRY{# @ÿę̂¦ |Ò*Okæ ~(Pq­ë´/o¡@ØÄ»ùµW,K–mÜ¥lã.C¢ƒú® {'¯/AÑT>€âê™Û8¸•3éó˜i+æÚ-˜ÛÎNù,FRu„·‹ˆJ=ꙓç£]SÐb Snâ«O¬¥©8®IS6øi”¼¦3€È>>á˜Ùq´JÐ^¸T»lí7*{éUÚ¢I×Z‡ÝⱄP\­H“1êCQ)’“—”Ubv D“ãIr~ @õ4’5oem•øoÕ,Bzåó‡„ E E¢ŒŒŒŸ/[íÿÑôÃoëO¾)E˜€ÄD‡Î|_c¼:¥8tí!cjùÏ\p°õÖžN¼œPâü…»¶Ík”µQX£v•×.þ_rM½M³Ï÷ÛÙ«-¾Üuýü³õVyg}\ãÎNžû¤šŸOM[~ÌGñB»r&Å« ~=wK§ÛZàz‡…êØ#Lfjº‚%lfZZ†RŸkXÚPvîÂgKµlØÆåä¦ùk}ZT2R}yùÏù§Ö†;gAPÀ‘:š²N¹*¬Äÿtõàîp•´2a ÏÈZœtïÊݤ”Jb®él8µò2š°-ðˆ~'wý˜°í£ìýYsØH­õ µÆ7°ZGÅÒ×ke ‚Æ^M €{£YÇÞU>éwæµ÷k[ÇY?õÞhNé|(ªÊr:â‰ØµëÔ¾ïÿÚ:oCéLJ¥_9zêÆ‘}u1ho”Ê‘h²juš'aMªòælÙpihžòüü‘Ë©L%–¥Ljµ¬¼wãÊNC›Øªž_ûÇ# ¼„Ää” I»$Ùc_-²ª5J@¤B‡¶V£w~'²,×¢¼¥<èàËŠ2™ÏÏ8Ï”É}€š! õ %Y㪨bo_¸f_ÍœùpqKHz•aµ,lÿVÍ"ô‡W>J¨ nV–„†þïjµÿ®ø¤H޶¨.-_ý?§A½Mi Íšé¿ß?p~H“Àvš†Š—Î~¶jéi‘}õÓFÕZ¹©æ^ýá„ s`g{ÚOîr´çò•aÕzj^£^™¦ÇvL°8–5ügxÑLËùÁ5 £éK›Õwý80àñ¨Mó<¼ûµ¸¿lÝô0A™ó§·Ÿ0•Ù¾óèÒ©ÛT 2wólÙÀ€ÒêŒÚÂ{pïç+÷¬˜sȨ\s_»ˆ›@á;úõ¬û|ãòñ§ôªÕN³ßÙÊÕúí &ngøæ•}&ö±æ”|ú ±/ awLVD-Bòc•¿tM§Ø­³­OŽIÅ63f·²æ€Фò­NvÀµjúׄwç,]n·`ĨöV-J›VmÞ¤‰Ù7ëþe9s‰A¯Z¿¿üV¬Û½âϼj ¯Êá!,BKë’´vã–‰§3(}G¯þ¨,¥ö@BÎS<ª õ Ç¢i×:‡çß „ÐÞƒ{=[´z1Xº·iÓ rï§o*dÿ @’ìŸ{$VÎ3¯Ñiâ(ÏR-þ¥šEèÏ®|~ªEê:#ô»v˜Z6¾Y³f?öd7oßô¨éñó£ H׈Åbõ›ûÃ%~0áìų;t.'íÂ¥ nåÜ ^?ýbwïÞuww/üö|>ÿÙ‹g½—•BŬòÙxÓFM úkhhèìpã>æ_;ª@hõ@§§§ãEóǸÚôôôò¼¹?IY IDAT¬ÈW@Ò}EißW!ô++½ïúè1 SœÆÃ±òA+ ò¤[ýð‹/9sŠü•æÉñó¹tE¹Ò"'!ôk+ØïÎÀÊ!T"+Ÿßp·æìöeÖ»ˆ*åMð¢ù—«Ì\êWZ=õ”d¼ Ù´ñàí(Ϥ’Oß‘~ i6åÑ‘ 'Ç(„Vîm‡ ô)+)æ1C‘‡ :e•œÁ„lb±8C–!à ð ¡_¬Zµj…ÿÜQ•!Ë‹‹Ï ±òA+] ¨¯CåÛ½“&œøB,ªÒ´:z w/ÛùºâÀ)ClS®o^¿r­Í²Éž†)ÿl\t<¡Ñ© >Ÿ ܲp·ýšÁ‹e°@ù#æç:BL@úN¶6¶"> B¬=ÒY™òÌä”d;[;¬|B%°òùÕ‹¥~õ+ߩ۲Ã]bBÆžR¯™oÏÞ–Uèß½YEC ¬û=º6ãìãä:•Ÿ^xÊ­=¡[=W1”3ï~ûúúóá½*U“ËãO̰úùc.i Hæ„‘£c¢±BDHg‰D"[[ s ¬|B%°òѹ TÖpEQLò‡™†Õ­%4E,]ÌȃðØŒÒáŸÓæBŠ¢€[;+®¿IbªëqñbÒ‰Páç†J`MÓÖVÖ¥­KãŃŽ+f‰:Xù „•ކ …ÄÊ’eÀ—ðÕMOZ / ²$™J–” | _=¿ê ˆ,AÆü²—€w„È÷œUQ%0©X6ABXù „Г߀ôÕ´f¢þƒf®ES4‡ËQo@©×¢i €ærh𦰀¢èœBÐ6”ÀB!„0TÈ¿M©€”“À—Qò ¡( €U¤+@d-æ‰DTT†’P @é K%< þèð [ L@B!„ÂPA«eù¹ P”fZ3×ÈÁN˜ô*J| ”_^ÆÒæÍÌ%¦å,9—^Æ([˜qÈ¢^Çó­ËqYRPɹ™C‘¼¹ß«Ä& !„Ba¨ iNj·¹9°Š Y¦\ÉV%ÏÌ”Ó"1_âÒÒC¥“Â6]•WVÍ„oT¥EEfùö=562útbÇCAÍ¿ËpèbºXêo N¾÷‹p±TÀ$„B¡’*ä7WAñzçÈ‘! ‘“»5j½$phY½*ý'÷[µzãÔñ2®Iå6&x™ri0®;òïˆ+WM9®Z×ì>cpuCÎ3þrÝ­™ÂB!„0T(Jü²·žøUTââ°Æ/Ï£t©êÝgììž7ä(~Ô÷ý.Q .aRPP~°B!TltéÒ¥D„ êtö|¦5£<'ê]\ˆ?w¼˜€„B!¤k~ç¨*Nᮀ„B!„¡ÂŸÝ¢ýSü®E~#\ !„BH§üÒû—‰Åâ´´4ê§•„7æÏz¥iiib±øgJJ¥øiD!„*¹¡‚…™ÅçèÏiiixÞ‹“´´´ÏÑŸ-Ì,~¦uêÆ !„Bºã—& ©[‘Q‘™™™xê‹ ¡Phin)•J†ùÉh0 !„B¨d† ÃXàÌæâ„¢T*U*ÕO†‘$ „B•ÜPT*ÕO¶)Q±„+ !„BéOÒhJP{cWà¡gißy„Bé(.ž¤ p0AÛúõë¿~pÈ!xfB¿ÞÁƒ;vìˆç! úm0 „BéL@B:-®€„B!„¡BÚJÖíX•B.W2„dý€B!„0T@(%êlʨó;¶î8þ$Ä_ß¿mëáGÉ,^!]…*Ép®Ò¡hJF¯tó8!„B:GN(Y H!ôç8xð ž„0TÐ9Ì—à­îŠÀûµ•%* !„Bèð R¯î<ÿ±æiõ­ËÕhÚÞ߯–¥€*ú§’=^ê?ñRÚWùÌöqêÅ_–yK)|÷u.Z\ !„B¨$† @ñ« Ý¿ •ûöÎùã;g »ÓwÙB?{AQ?Щ뢥-”@ötÕÔ=¢Ó¸(¾±aÔØI¬«ãƒ·S@!„Ò5¿6‰â”v*ëâV½®ïàékÖ÷±{¾mEH” €È?^X1¶gs_x–ª½zdÆ“õcû¶kíëíãÛ²W@à?q*ÕÇ=ƒZù¾’«7P¼ÝÔ§íàh&ûyÄ–e]ÝÊ»¹•/g£GsJÙ—+ïæVÞÍÕÙœuvÃòã‘ ÈîÏíÔjÊ¡}óùøø¶¹áÆ—È‹kF·óñmÙoîñ¨¢ol›Ð·½·o‹ÞS6ßN`Ò¯Môõö_ýTŽOQÂ$„ÒM¸B*ü”À±eÏâ×§nÄ(Sî­ØQeèºíÛ6¯¿oÎêûiÙÁži•ÖÍ^¸jÁØ:©Go¸›iÑÀÛ&îú¥ EdXX¢“¯‡)çûŽ€•=º)mЯZlÈ´áã÷ÊêŽü{DsÞ?›î$½Ø>aÙ}Û wïØ0Ï—²péÅx(š§ƒÿ'Ñ`B!„† Bë ûæÿìÝw`SUÿÇñs3Ú$Ý;”QfÙ*KÙS6*‚7âïq=î½Ü(*‚¬( Šð0e´ÈÊꢴ¥¥3͸÷÷Gh Ø*¶¥¹4ï×ZnÓ4='Mï'ç{¾7çDòüõ“šÒ³ExX³žWßÒñeÕKU¦0µ¹ê†qý:5  ižØ3Òzüà)ÉÜghóSW³ aM_³¾ ÕˆËBþíÏ"{>úÊ´—_~å5—kÌ×½ôÀĽ‡\3ª…#ý@ž­dçw+#î»c@+sXL×17 õÙ¿|©"|ú¼òóНÿ¯½7OžúÄb¨O¦†ë*Øsöf–ùôÖ‰óœ;ìV«Ü©´ªšHØrÖÎ}çãŸwthMþ^eŽ»¬è"zŽh>÷»UÇnmª]µþtüÝþuR’¤ÕIB!|½4¦òcÉasØòö+9‘òàøÅÎ%WXíqåŠðc›ÃÅà,@ªú/àÙQ¡"ûÏa¦Mºö÷~üX‚OÕ§ô~>¥K…B8N,{íå_4“ŸýlRB„¡på=7}+„Ú°^ÚüÝêƒWè/n{×ÀºœÁK’äú!„ŠCÑ6»iÆ+ƒC*ëš$O…G5- Tç¾Öã¿}µÖ?ºwTd›p9s1¸J®òäÝ–½'C´0±[„QB–•3›´á— kuzÍœy«K:Žìâ_ßïõëBZGjsöçzU=¨@_/’ÂÅB€gGÅQ”‘v ußöäe_¼vÿ´OŽt¼õža^á}¯îmûíùW¿Y»'íàŸ,™óò“_§UvÒ‡· —S%­Ý±=eñÌggí¯¼&›&¤Ç•­Šwí*ï:²CýWI~ “ù¤¼õÌœUÛ¦íÛºâ‹O½»»\ˆÒ |Ýt@ª_t@u¢àɶI±nûà‘»„’¹]âÏ==±{„—"¨Ï#¯ß;{ÖüS!¼ÂÚö;5Tæk´Q#ï»ãÏ׿¾öä·Áñ£Çm~l]eVJÐB:ÖdTüŸL‚äÓéö™O˜ÞûñC K…60.qè‘z!*Y²ÌSçb¤A€jHíž]ö¯¾à©ÖùÆ SÅcWŠÖ?wÛÛOy#3y‰»ðÝÌËW-Ÿ4qÒßÜ`þüù“'O¾¤GcÖ¬YÓ§OçY@ ’’’XXÜ|Ϋ(óÙ  8´¦Ï.[¶ì…!µþÖ—pñ½|ê…;¼ûoINh(@P›K7*سÿi¯ßå#â Ìb£I ‚$ÕÐ]ºy: ªþ˨€JÐ *4œÜœÌ¬Ì@¿ð@ÞBn<*lÖ̬L!DdDd]î‡$Ï YYY¡A!¡a:½ž¡o4ì6›.O—••U—¨Àb¨SRR Q¡!X¬–s¤C–eYfèUHQ”Ú|™$E˜#3sOÔå[S€àÑQA’$ƒÑXVVV÷ûá¬ý"MP­§µî“B€çFçi¥'œè»ë¬Ý]ù¤î–ŵiÐf©’ $¨=ŸÔNÝ'—K°€:±Qðd »ª 9uþ«´´” PƒZ/&øúúž7¹uL ‚$ÏŒ ç½ñ\ëªÏÙ«Pï?iµ© ^&¢Ž«  €:Ñ *4¨ªóKF#„R‘¹öÓ¿[{´XØjÈ Ó§ö ;ç1ÉE»ÍûvÝÞ#'K¦¨ÄÑ7ß9ª­ŸÇÔ1Õ{TPmÊ¢€gGé/QÁztᛟmŽœüø mí;ç¿óá{æÏ6kÏ~‰£èà![«¡7]ÛÌtzçO³Ìü¬ÙÛ÷wñiÜaAQ”Kbÿ÷9¤z•––æü ..ŽÑ?*œwî+I’5sýï'›\óøÈnZÑlê•¿?þ¿M¹#ÇEž}X^±W?úЙ¯kp<ù‰{ó]}].ᦔúmÖÇ?lH/•Œ¡­z^uÿ­WDè.íY¹+¬êø˜YL8 xXT8¯‚°d.04i¨“$!¼ÂÚFj~NͱIQújOA[I…d 2i]OK­‡çÏü:­ëô×îkªœ<øÇyŲd¦ÓRÃG…úè€D¨ ¢‚»Èå§Ë¯&Þ•I?/û‰b«"LÕœvʧ¶þœ\Öbr÷P­ëa[þÑB]tçέbü¤˜&m.Ú©0W„¨nLêñÞ(@P•†½®‚³%F£‘$¡Ñiµ'EVÄ™OœÏ~bå;ÿÙôº»†GêÏù„©ÕÀŽÒÖ7ï¿ÿ•¾øõ£eBsq.jlàr  NIII à±ÜÙI’´¦@£d-µ*Îc²µÔªóõ7hÎ?ëtä'øÂW§=üêèª%ˆÊûÑ„ yô½˜äµë·íÜôíÌþêÛ·´7rZ ÄW xtTøË^)ºUpùî#ErG£VØòR³åðfïsÏ@í§6}øô¬C=xå¦Îš¿Þ§PôÁñýÆÇ÷óñïþïþ5[ONíÐL¯æÓbÕªõµØD}\Š›$Ï ç5KÕjµÆfFü²`î²Ø«Ûض~±87vrß(o­|hÞÝ÷, ~`ÎK½÷}úø››bo|h`pÁñ£B_tlxÕ²œ¿öÕ÷S;Žìß%Ú;oûþBCLË/æÒ>­¿H×_«÷8TÍRYLðè¨PµWÁùOF# -¦<1ýÔ[_<õP‰Ðzä›ë¥V" EÆv2-W)Ê÷ÔÖÊ{1ôyuÞC]Œ•÷é×¼kȲïÞxô£ áÞyüCÿé¤Õ0± -êl(@u¢@TpçÙ¦!vèƒo}ðœƒ^-oùô·[œ|ûç9L-ÆÜ7cÌ}jVEq^…Í£ž‹ ¨JÃv@é€TÛfDû4ºê”.¢Î{è€êD$À“¹a¯Â¥{úÞˆ¿S]¶5×qr)@ðè¨PµªÀ¸7¾8D$¢Â¥}F{©pW$wa1@mt¯‚Éd*))©—ÊøFïÒúIKJJL&S]îÁYz$Ø´*C$€¨Ð@Ìáæìœì’’ƽ1)))ÉÎÉ6‡›ëx? ¨Jƒ 9ß0ÎÈʰX, }£a0"#"ƒƒƒG]ž„P¡¤¤$¢BCp8!!!f³™Í‰¢(6›Ín·×åNè€àÑQAa·ÛëxN‰ÆŠ$UÑ0Pv3€jÐ Ô‰ QPEZ W,&€:%%%1Qp' ˆ @iAP€@T\±˜@TªA¨¢Â%ÉòçÛÇ=•Rª¾G&çýrϸ¿×·‰WÑñ+—¬Ý=&!Л§HÃp Uý—ð´¨ õ‰kÝÊ ZµiáØ¶ñõ‡ÊÆDûžZÿîóï¯?œW& cÔeWÝýð¤ŽöƒÞúОÑÓ»lûúû]§ -F<ðÌ´~a:aÏÝ0wæû?íÊ•ãÚzÉÑÎ3ýâ½?¾ýÞwk•h[ zïÿ mf´üðÖ‡v ¿©Uò7KÛ› ø¿çoô_6óÝïvw¹ö™'&µó‘Î>0ëñÕ› š\ûÊmCctBˆv{¾ªÄá%„R‘¾jÖÛó~ÛwÊnŒî=éÞ‡®Ž÷“ª?Xz`ñÛo½úh©!ªm“»è!„ÖßßüŸ…÷}ñþÐ`‰§Û?¥A€j¸­ÀÆáp½É¨BHÞQ‰Sþûò‡ïÏ|ýövG¾z}î !„ös“²;Ýôìwô*^úÖ§»J…õÈ‚gž]RqÅ=Ͻùâ]#šz¹«Ü•/=ùÅñNÓÞ™õþë×Çl÷Éw·+Ba?4ÿÊໟýO?ûê™ÓïþøxÇ›Ÿyp|ÄÞ¯ß]uÒáúh´>á>"wwj¾½êˆÁ×KRж¾ýØ'Ç»ÜõáÜÏ?}¨wþ·/¾·­D®ö`aòOÌÞsͳ¯¼üÔ×Â%FHRÂ?a73€Ú¸¥úÇQ’¾qÞׂ<ßÎ$„Ôãš)Bȧór[%v^{àx©ÒB¡k{ÿŒ§‡KÂÞ4û·5¿íÏ-6ü°$«íŸL¦¢×_®Êž³~Ñ6ß+ß»u@/!šÞyÿÎ[žþa×…е»÷ÅIöæ?­\Òë‰'¯kî%J”ÿýøÆ¾“ÖÑƳÃ3zú¸uÏμnÊ„ ‰WôéePò’篘<{JÏH­áWßÒã×—V¥eæUwðä÷Éúa3îŸØÉ(„•·è»B!¼ZLýòש<ÏþH NlTˆ E._óØUk„B7êÁ™·uö“„P,G–òÖ¼eû …ÞÇOSjµËŠóÁy9žÖh”lkñ±C¥A]Ûœ»b?y WÕ!ễ!¦}¸¼(-ß.„ÐêµB¡1øxk4Zç›ûZ£^¶9”sîDpÛ‡_Ýž²yëŽmË>\ôé§½yç^óÞÌò#ŸÞ:qžsUÀnµÊв«=¸?[4™ØÜÈ3ªNiAP€à¡QAòêv÷ËwDšóüG©%Z£NBØŽ&=óÎú&·ÏXpe»Mæ×wݽþ¯_ç<É—²¢ÑJ©æùËå/÷àš.$©úz ­_l÷!±Ý‡L¼ãtÊ‹·½8wÉ„ÿ:„®ý½?–àSu#½>ýãjÿH-uFµÖ˜ìûÖ­LI͵H&s|ŸA}ZúŸWç'—Ù´z㞌"»dmÙ½ÿ¼xîP): ž¬a÷*HZ¿Èfq¯|ô©1>k^fáÑ !Ê3öäøô¸fx|ˆ—$„,+JÕ72B_p ³L© ŠBèÃ[‡ÉY»³¬Îƒ–ã{NjÍ-BþU²ͱT}cÉ'ªi€°YDX›p9s1¸J1¢šƒ†¦AŽûs¬5fü½Æs 6{Þ–_×1v9aÂÐöšÔ•ËwŸ–Ï …;~]¶O×yÌ·Þríá'Öÿ²!›Kp •§â~n~ñÎv‡?æ½? õæ–Á¥[“–$oß¾~þ›/~‘i¯ñ댭Gv×oýè½ùkSVÿüé3¯¬(² !´}Ç'”üúÆœ5{ݹtÖÛ´—Oèìÿ¯Þ¥­ØÿÁÿM{xÖ¢ÕÛöîûsó/Ÿ½½ +bà æ1}¯îmûíùW¿Y»'íàŸ,™óò“_ ¨æà±à^šæ,zcÎoë7¬øú­g?8tæ§°žwÃÈÑwÿïááBÒ‚¸ô ì©‹Cº]Þ)6"¢y×¾}óö¦“l§ÒOiÌí[Gøxy6ëÐÊ¿âdn¹ÌüÕq×E ôMF=òTÚ=O¾üjów|øÚ#¯}ùÒCRD÷Ñ#Gš¿å¹'Ê}›D™¸ÖP©Ý³ËþÕ<Õ:ذa .RZøÇ̰|ÕòI'ýÍ æÏŸ?yòd7ýrÑÎï¿Þnžpý:!„-kÅ?õ½q|“Ë2—\œºÖü<ç>eGIN‘âgöw]1Plå6¥jÙAk ô‘V»YQø›´ .ý$]pëÖþù[7ìN?™sdÛúÝÅÁmã4‘›üÕ¬Y?¦–)º –±†‚íve•—:¼eK¶¾iû/æ¨ïĆ!€4žK°iC†÷+]™²da…dŒˆ4¼S F‡P”3×Ûð޹|L?ÍÚ-‹¿N–%cx›¾cú´0q6@TªUÕû¨d}püÀ«ãž Âzß0½wå BâûOˆïϬ¸p©fÀ“Q€¥Ñ(: €zÓX: @cC$€¨¸Y£é€Ðh4è^Y–srs²²²,V Cßh¼ QQQaM’'Hžrrs2³2}üÂyç¸ñ¨°Y3³2…‘‘µ¾“ÆÓ €¨P YYY¡A!¡a:½ž¡o4ì6›.O—••U—¨Ð˜: @cB$€¨Ð@,VK„9Ò!˲,3ô*¤(µºh°$E˜#3sOÔñ»S€ * º­Y’$ƒÑ(IRÝï§Ñs˳¡vU£ÑÔ}ZÙÍ êD$À“5ô%ØÜx|iqË(ÕrU¡>-H$AHh´ù¤î“K€çFçÉ$« 4gÔé«YLðè¨P_« ž6œµ@—ÐOZÇÉ¥ Ô‰HQ¡aÏ)Ï9ýUìE¾ðÜÊ„W>˜«Bȧw|ÿé«w§å”8L1=Çßqïøx?ã䯝?Z¸ñÏ£y¥Š1¦ûÈ[¦]“¢­º“ò´Ÿz웃^þlZko{æ‚ûïý2óì÷3cöM-¼˜mu£ Àƒ£ÂùH¶´y÷ß»(["²[å~g¹pªµÍÈÛ¯oîS¸}á‡_¿:«Å¬‡t'ÿõø™¶ 8ºþ¡õ»sm Í;ÞñtÇ3·híhå½[²K"P'ùÉï>—¤òØõ+Ÿšwöž5†ðíÚFé˜áK$ÛP€ 6 {]… zçÙåœÓQQR!™‚ŒU•FB±^÷ㆲ¶CzFè„R¼û³çfŸþÄCƒÍç^ÿÙ‘þåÝcÇO۬ɲ*L´J&÷Ò‚  @5Ôü¶»|jË¢õ¥q×'†‰ ©ïßùøÒB©É°‡_­·gþðÊ[;»=øê¸fqúì×io~¤•!ÈO[|`õW¿ó¼wÌ;·µô®ë©0›.&<:*8϶Ï?ç>óÏó¯ÍfÍ\þÆ»›Ý8cd¤îÌqïæS^x}@vê²9oý÷ãÇ޿ɾí@ÑÑ}O_»¨òkR»&û¡¯žïÕ¢ûeέã‚NlpCʉ©­š±­¹&·Ö(@ub£@Tpë9eU€pù„=ã{ÏÌ;5äñ7ÆÆzW• ¡±-CcãšÚS&¿½ôÀÍÿ½ÿ½wËe!„Jñ¦7Ÿü.ú¾×nîêçú t~¾’¥¸‚Eõ£ Às£Â–³ÛóSÞü½Ømºú IDAT—=üÚ-ý«ù ÅrºLÖøj4†°è&•ÇN§4z?st˜Q#ÛeI§‘„B)ÏØ“#†×ýçôœ‹9\ÔÉ­ ‹  Nt@ˆ —Î;ç–­eeåe²"ÛÊKËÊ}Œ&½åÏ™‘Ütê#‚òÎBÒûE›OÏiAy‰qAöìÍßÍ=ÐçÆ6F×{’Μ®J’íè‚Çfç% éÓ>IJï—9ëä„Gz…éXT¸Øq¨nL€GG…¿ìU°¦ÍûÏÝ‹O !Dú£“óæì»ÂNÊUŠrç>±¥òV†>¯~~{§¶úyKfÿz²Ä® j7ðî·wñÓHçÜ÷™ï é‚Ûµ7Í[òñš/+„‹+î|ñÎ!Z‚‚ªcFeZ xfTø ¯Öw|¾ìŽó|gñÀjn{ݣݯ«ù¬T ôƃœ‡ô¸í¹·]âS—Z K‹ œåìl0þ#véŒU÷*P€êÄF€¨Ðp§“œµ«9$Ôi)£Î—$UqÃÕš¥K“'<ê28uï€Äo#¨PRRƒx,Ý¥ø =§o©çkQ€àÑQÁd2•””øúú2î,•””˜L¦º§A€j4h’9Üœ“]RR¸7&%%%Ù9Ùæps]î„$µiÐUçé`FV†Åbaè ƒÁìp8j}' €:Ñ *4‡Ãb6›é—Ú˜(Šb³Ùìv{ï‡$Ï B»Ý^÷sJ4>,&€:%%%±°x, C5p– 6-TJ«ÄPwÑ1POZ UŠ‹‹c€{±ªU`1€¨Tƒ$P'6*D@iAP€@T\±˜ê”””Ä DÀ(@ *5¦AQpÅbQ¨H Nt@ˆ €*Ò‚  €¨¸b1Ô‰HQp3 ˆ @iAP€@T\±˜@TªA¨¢ Š´ (@ *®XLu¢@TÜŒ$¢PcZ W,&¨®ÞïQQ”œÜœìÙeåež0‚&£)Ò!IƒSëÁq Uý—ßLP : D…ú”“›“*¿S‡N!Á!ž0‚ù§ò÷Ø/„0‡›/dpòòsÛ¶nè ƒSxº0íÈá  yTÈ>‘Ý©C§ÿ€ÌÌÌÇÿ«¯U¥ê×ÏûÔßWÅù¶$IUïdW©éŸ5}ê5oÞ<""¢më¶»ö캳áìÙž“„qÍ[ì?pà‡ÅP§¤¤$¢B½)+/  ®¨¨HKKKLLlÜ÷iÓ¦à à ,(*+/óœœP•.dp(@hüQÁù–¼R©q_ÕÏx ¾^ј\àOM€ª\”HU'ÐJc'έ•B­¹­’íØOïÍKÉw0繫 BRE®Ô¸‡Ïù3*Š" éÇŸdòS»­©üÏŸywÔ˜ë{†ðjÕ`£@T¨×óBIrž@;‰ ²,«©Éž»yÅfÑ}hP:FIÕH¦ÎS‡[Þ}oaâ݃چûèÎ5¿B£:˜tëMW¶òQÛ{ò‡ÃÝÕGrÉÁßWí+ñm׿+ß:žÝÚs·,ß"%éÖð!Ãm»™Ëv}³lïÆS“z¼éz´ÿ¢ÂÕcx}: D…ú%)߯Ãi´\<óÎ~÷pãCSãCåœm¿|1óþýù3gÜØÒ Â¨ (Ê…Õ{֑˲3JuFmIFNYœ¯¯:ß ¿€ŸÚmH¾W|¸)õõóV¿4>Ñ~¼8OWÿ§–ν ub9ôͬ5¶+žxë‰ý:·kßµÿ Íx¨sæ×®:a·Ÿøùîá·}}Ī(Š¢”ízùš«žÜRª(²åøŠ·¸qøÈQƒ'N{ú»?‹d¥"íóëÆÜÿkžól¾dý#ã®~¿EQGá¶o^¸ùªQƒG޽濮̪¨ûþfwíUËr2JýZuhf*É8QæÌì§m_·ôç%?ÿ¶~Ï ‹"„ö¢c;_öËÏ?/Yºf[ZMB.M[½d;ôC›W-]²vëïÉ'ì¶ì¿.ùùç:êñ)q!7sO’Æ'º…Ù²iΓ·NqÕéöíÏÞh3GùP~ˆ õ$ɹKÁn·ËµbÉØ°1/hÀ˜Ž~U»£¥à^c»y¥®Ü^àåÊò¦Ê 'E–…[f>þé±Nw¾ÿÙ§?Ð+ïÛ—ÞÛRäEœÝ`-;o*[.|öÙÆñÏ|ôõì—oO~ãåŸ[k÷HÏüŒ‡ÃM{”òœŒRŸè¨°¨HCIzNYå›ãŠ=7í„.¦SbBûPËá­;3-²5g÷Æ]y¾mzöЧcHñ¾äí™Î!—Ü•%…·lߺÕeÝõºˆƒ‡Þ¯m€¶Ÿÿx·u@{_Òçþu~½z賲ʴ&ËÒéƒï^YHS+àñ.JMºóöZïU°:^ ÂZ‡é]¿Ü+¼E¨Ø(×ÚITµ(âÌ y) 6úOúðÚÄH­anê¾ôÕ5G,㡜½é™‡f9ôãâ“—Ý÷üÈv&!ÌC®»è?k·æ‰×Öú'uÛv¹üDz©OL„QgŒŽô>œžSÞ¼…F!é"»^Ö1L'„ÖäýoëñSeÒÑl%²GÇf!z!üã;žØ–].„djÕ·O»­ž«‘$IïååÕà{ÜV€T¶õíY%ÿ]µõ¹f«Gû¡.rôÛ?¿´ñò϶—à[Ë»´ì[·2%5×"™Ìñ}õiéÿ×Dn+8´%yëþôSٯÄk/Ðñb@بêSU$gZ¨Í °|æ²,¹T„b+³ÙeEˆ3kΣB‘+²öf•3mÒ—Îx‡Õ&w,¶;3Å™–­²¬EQì§È/:ôìÔ ZI!›Õ–Wb—Ckó~Õò†[VKNz‰)ÚlÒ_´ÙëHFŽ¥y Ó™oU9Ã>‘Y\RXª˜"}ÎÌ·Ö'ÄGdZ”p!$­—îâVÛ¨º’½0£"zd“³{`ôÁ-ÂÅÊòZ÷î²çmùuÍ‘À^#'D:ÒSV®\î6¡sÀ9#,ŸÞýëØ[ö¸âÊ0£pèü©vžDe Qçµørm`LH;[1 À«ê`yÎá<ábdEÅQ!¡(²]ѵ»ëÝ»˜ªNOõ¾Þ¹©•7Λ E–E:æ©ç¯j¢¯¼©Öä§­u_W÷]>B)ÏÉ(’KŠVÿº¿òPzNyóæ^Õœ¬kÕ~=·u@2¶hÞòê¬ÍîR„B)ûsÞ[[#Æ´2Ö6)¤,é6ºS¬¿F„ö혚´7­¨cB K°KÞRÜöÊI}Ìz^¨¢B}r®*(Š"IRí*stæÄÄà×,Ùuí=Ýü§¸Žü䟶–û$vóÒyëKq¹CQtBÈ6›,EÖ*LÞ™ZàÝ?ºò O‘‡Î['*Š,E‘„â°:!É7&ÖXš%ü;ž)9R¥–T8F÷¬*(å9Ç‹M»õhîÜ‚+ŸNÝ´#ýdy³˜sne+Ê·èüý}¤ãù¥öæ&½ÂQš_"Œ‘†¿<I\„jª ù©ÝV€¤oyûÿ7@bð!RAÙž.‘G3š?²ê¶¸ÚžÅÛ O–êƒB!„Ðú™¤Ù§í"ðl€³žÜŸá04ÝþãçÇó+tM»ôex1*; Õ:*¯æWÝÚ÷÷×_Tšrý v!òÉ¿}ýÕNkô„Ñm B4mT¸æÛ%ñ#Ì[/ØP&÷P4!=Ç]öÍÌWß š6¾{¤têàæ5èÇ?:¦Uœ÷‚_¾[ÝÛ73eÑ7û¾±BñŽ;Ü|ÿœ×>× nå[ž¹gíÊ£Ýï¿··mÎÕœ?£[: )–Üôb¯ÈÎáþg"©YØž”ô“–h!ì§3ÒOhu–“÷äb¯2šZFJ›ví>Ú¥eˆ¦è讣¶ÐÎf£tîrˆÆàçm?–žuJï+4¾Aþ^R}=%.äfî)@R@ï’ÓÇ-þ~ÙöôR¯ÈÎC®Û3Ò»¶÷&ÛÊlŠÖ»òRn’Þ “‹,vET¥\š[hshtщú™lYÛ×$ÿ²ÒÊHõ]4.©—¨ÚÐ\Ûw¨¥ÀÄéo=ýùü%o<ý¥M!DPâíOl¦WÑtü=ã¼½`æ‹Þ±ýÆOè–ñ½ŠpÙ}/Ü9÷³…ï<•T!¼B['Žœ¬5ÆÞ<}è+³ç¾²Þ¿Íñׯ}¹D(Š¢o~ÕSÏèæÌýöÕߊeáÝõò±±†Z>ÔªU…¿®‚bÉI/Ò‡wð;»[ªÝœžSÑF!ݱ±Ø*¼‚š&ôl¨•DxÇÞ÷ìØŸ¼Æ*ô~‘m{uŠ1JJé¹QÁ·i‡9ÛvlÌж»¢w}E… ù©ÝV€$„BÞmÂ]Ý&Ô×o€æì2Š,ŸÿÄ’í»Сk‡æ!Âúõ>6oÕþœŠV-νdÈ–-[ª>îÞ½;/U QD!œÍREm÷*8ÏZƒ;{ ó8¡”l|þÎü|÷¾.¾’óþ|:^ûÔg×VÞpÌ(ç ™WL¿;žéwÇ9'e"(ñæ‰7Wþ{ØDçc’:¿æøsnZëGêl–Úà'›õÕìüÞ#¯BˆfÃÇT7×þͺ\ѬË9÷â7`TœË¿½#:\>¢ƒžˆn+@BØN¬õÂ[Iëÿ<^ä“0òö'Ÿ¼±k@-wkô&½°[íJe.¨ph½ :—°¤Ñzk[¹Íy Içãë%XìÊy‰Šx@%بêõ¶²R=í÷õn‘جbÁ’_7ëÛÈyåM{&F¨h/hÕé¦ë*\TÝI)Þð`ŸA_„ßxßíO´ð³fnýᙃ3ÿý‰NµÛجŒð±fä•ËQzp”ä)~mý]Í4¾áÁºÍ™Ù¥r˜¿F(Ö¢ÓV¯oÊ€'D!9Žº ¹Ò„ ˜~gÚÛŸ½ù¢E úLá*ê@ïܨàp8ÜuµæK#*ˆ º›{ J·|dxzÝê§;8 €¦ÞvcâØ!ïl¹ï³Ë}jõܺµÿέvu3Û¥ì.î ŽÜäo¿ßá3pê¸6¦ÈÎm}mZµÅ·WSmÖÖä“~ñ}#è…@­è€ê™s¯B½]›LÑÿÎWúßyöì\U#XëËGÀ•û d‡ÞÜ:âì>fmPÛ¶>‹k]mhÂð~¥+S–,¬Œñƒ†w Ôág¯¨ì9j€}eò²¶J&s‡!£ÃI À#¢‚Ñh,+/óöònÒ¤ÉöíÛÿíiwU¨v]Âõ³ÕÞÒÙyI¸½H’äúqÕ]ýõãjoö÷bcc…eåeFã•«ø˜|Š‹‹ýüü<çV\\ìcº ÷ç¼ÉQ^RîPâ§MÈ›~÷GÍfÞ|Y¤A.9²âGWv~ð1ŸÚ߯>8~àÕñÏ a½o˜Þ»êAm\Õv¯?ÀâBTdTfVf`@`hhhhhh£AK…¥ðtatTô…ÜØl6OOmÒÄCÒBqqññôt³Ùü·tCÒé%#Ç­9ó»z-¸ëì§Lúä7† àõêUP`,ËÙ'²+**wÓš‚«^u=Ê^¢ÔÁm|û}¼-m¦|îAÉìÇœ¢ në€dËZûý‚Cçôn9éîI-½™ @TÔ‘Dà Ùs·®ø-¹T!„â(Ï;¸5µ"aÊ3£d&pß%ØzÍømË¿­G挱´™™%B‘””ÄÂà±è€Up[¤óy55%ì÷yÛK˜àéXU€ŠÒ‚hø$![-Öªj#ÅzjçWsvëZ𛈠ÔÀmH§ ·æœCæqŸ|ÒÍäÎÑ u^óȹèïzý#Žp„#iø#¼"q„#õˆškü¤vÏ.ûW_ðTëüaÆqj‹‹”þ13,_µ|ÒÄIsƒùóçOž<ùB¿«\šy8³´rYAÒxDÆ„ûhÝ;³fÍš>}:O „Påo>»`á‚¡‡ÖôÙeË–½p ¤ÖßšU¨…{ 4>Ñ-[3øD¨” ,{g?;{¯¥ºOâïxöŽxÓt@ˆ €»¹álriæ¡ÊÎ9æ(ܽ<9CFœ¨ˆ €jÒ‚hÈ$Sç¾ÿÕåßöœÕ¯M´TÛvêìïÞæÏ„OÇu  n¾œBÅñÅôo5ð¥¼Éߥnûü–Ž~Sˆ ÔÀ}—`SÊR¿žØjÌ'ú{—L~çê8#1Îb£@TT‘Dw@’‹¶|}çø)?7{iýÁ/ Ö3 •Ø«UpC$ëY“úßµ(§íï}r[W±?eCÕ§´Añ=⃴L Ð *îæ†Håû¿_œ-„ØÿÅÝW~qî§´Ê_9&€iD@iA4dRÀ˜•v…a¨ {  nÔÉ}‡ €'£ *J ¢; ©XZZšóƒ¸¸8F¸« PÎW‰¡à^III à±XU€*¸¡ÒYÖ¼;öf[åÊ]κÐ.}»„òˈ €JÒ‚hø$¹`Õ=‰ƒ>8$´:½¦ò:Íþ#~:üÓæwsÇb‚BˆÒ?Þ_>óÏ÷Æ›$¦À{  n뀤1GÆ'Ä’ zt@<« PQZ _€äÓãÞ«_þÏC³Ÿ¼µW”I{&0h|c[Çú’£Qp;· )ÖÓyYë?š6ô#×£ý®À´€HJJbaðX¼q Up[RÉæw¿Ñ?¹:½¸ÂæbÅrðx¬*@EiA4|’¤ñï6¤GŒ¯3pV  n»›O÷iýw=õâÂíÇóO—T*w0%€¨À@ ÜV€T¼áõ¹›Ö¼zUBÓÐ@¿J#—œfN@: ž$¨(-ˆ†/@òí÷ñ¶´™òy ÚdöcB€§cUªà¶$ÉãsôûgnÚ³KÇ.=‡ÝøÌÂã¾1f¿ „"))‰Aˆ €;¹­I”ïx~ÐÈÇ»N}ÎWŸÍ˜ÖùøŒC_ÞeaJ€Ç£ *J ¢á ʶ}0ÏñàŠß^ìbBqõµC‚»ŽyÛƒ³{›˜àÑXU€*¸­É^˜amšØÌXuÀдGSkF9D@ÜV€dˆ»¾îˆåú^§Ç͘»½t`ßZÞ¥¤‘ª6)˲¢œ›%ÊNd•­ùföšÊC«æ}sêªk{…i]o·eË–ª»wïÎ3àÑiA4|’wlBàÞ“ó‡÷mÓÖöÎÚtk¢ï‰“§ÊkyµfÞ¤vkå2‚l¯ph½ º³ý´F_ÛêÌnf{þ–%ÿ;ÕyìðvÁÚóîˆxˆ €nì€äÝnÚã ÝF]ÑtÇï7.¼¼[«O¼Žg'|Þѧ–÷§Œð±fä•ËQzp”ä)~mý]Í´ÿ Ùí6ƒF£÷ ô7jy ¢P· ]“ë“_~°8"8ò «º}»átËñ·Œ‹©í¹».¸ukÿ[7ìêf¶KÙ]Ü-.@#¹Éß~¿ÃgàÔq\BÀ¥…HQPEZ _€$„ߦm|…"¦ÿMõJéÑ?s "j·³Yš0¼_éÊ”% +$cDü á5B8„¢¡0Ë€¨ü;n)@R,G—}øÖW)…a—ßòðý#õB8 6½Ǥ§O½s|e­¯Ö¬ŽxuüÀsDXï¦÷þ˯_øåS§1ùT-))‰…ÀcÑ®ªà,= Ù5U)Xqgâˆ~H;}ôçôy8¹èôæ7Gµîyßê¸ûëéËœOǪT”DC •íùæ7qí/»¾\ºñÞŽcpÇÖOž™òÙÝ—±Í€U¨‚.Áf/̬h6¸s $„o‡áíKÖ|^|ß¿œ@T€z¸¡IÈ6Y£sÆÉËä3íƒG/â7ÎÁF€¨¨"-÷t@B¡ ˆ¤Àåüˆ!€¸çlö“›o ”„e×Ñ’S𥠓vé„BÙ÷ʾ‘z¦è€wsÇ%Ø´éȬ;&ͪü÷æû§,p~ä3|qæÒQL *êH ¢! F--ášh5b¯TÁ @T€ú¹£àŸ±Q *ªH Â@T€ ±˜ê”””Ä DÀ(@ *5¦AQpÅbQ¨H Nt@ˆ €*Ò‚  €¨¸b1Ô‰HQp3 ˆ @iAP€@T\±˜@TªA¨¢ Š´ (@ *®XLu¢@TÜŒ$¢PcZ W,&€jP€êD$€¨¨"- ˆ €+@è€7£+¯ IDAT €¨Ô˜Hª¡c ÁÁÁ„WiiiÎâââ @T€çr Uý—!!P : žŒ$¨(- ˆ €+v3€:Ñ *nF$¢PcZ W,&€jP€êD$€¨¨"- ˆ €+@è€7£ €¨Ô˜HDÀ‹ D  €:Ñ *ªH ‚$¢àŠÅP': DÀÍ(@ *5¦AQpÅbQ¨H Nt@ˆ €*Ò‚  €¨¸b1Ô‰HQp3 ˆ @iAP€@T\±˜@TªA¨¢ Š´ (@ *®XLu¢@TÜŒ$¢PcZ W,&€jP€êD$€¨¨"- ˆ €+@è€x2C5p Uý÷’þYìûÖ­LI͵H&s|ŸA}ZúŸ“È•òŒ­ë6¥fäUÈ^AÍ»^Ñ¿k”Aâ)T‡U¨(-ˆFP€dÏÛòëš#Æ®#'LÚ^“ºrùîÓò97Ërrìá®1æÊí ›–®K¯`ö€ ±ªUh‹ g’BAêÁân£;ÅúkDhߎ©I{ÓŠ:&ž åÚnWŽ<ó±Ùxêà÷i™ÅŽXo-O 2¬*@M$[áÉR}P¨Q#„Z?s€Tœ}Ú^Ó­e»Å.¼L^ÔP-: žŒU¨(-ˆK¾I¶•Ù­·î̹¿¤7èä"‹]Õ¥¥ôØŽÃÖðÍ}ÿÙ·lÙRõq÷îÝyz¢í­·A÷—%¹$mÍâäÒv#Ft b— *ÿÄ¥ßIácÍÏ+—…ÂQ’S¤ø™ýÏ[¼“KÿþÓêÜfÃÆõŽ¡M* *§Ñ\‚MܺµþÖ »ÓOæÙ¶~wqpÛ¸pä&5kÖ©eаf­ÿaù‘Àî}ÚKòrssssó‹m O ¾†jÐx.Á¦ MÞ¯teÊ’…’1"~ÐðN!BQ„P„н8¯D±”$/9^ù%ú¸ÑS‡ÆèyP#6*D@iA4‚K° ¡ŽxuüÀsDXï¦÷v~ÜfÂô6L7P?   ¦ ™¤¤$ *îÔh.Á@Tê?-ˆFQ€@Tê ‹ D  €:Ñ *ªH ‚$¢àŠÅP': DÀÍ(@ *5¦AQpÅbQ¨H Nt@ˆ €*Ò‚  €¨¸b1Ô‰HQp3 ˆ @iAP€@T\±˜@TªA¨¢ Š´ (@ *®XLu¢@TÜŒ$¢PcZ W,&€jP€êD$€¨¨"- ˆ €+@è€7£ €¨Ô˜HDÀ‹ D  €:Ñ *ªH ‚$¢àŠÅP': DÀÍ(@PCõ¤AR¥´´4çqqqŒ *Às\‘€ÛQ€U  Ô‰HQPEZ W,&€:Ñ *nFQ¨1- ˆ €+ˆ @5(@u¢@TT‘HDÀ‹  Nt@ˆ €›Q€@TjL ‚$¢àŠÅ¢P @è€U¤AQpÅb¨¢àf €Ó‚  €¨¸b1€¨Tƒ$P': D@iAP€@T\±˜êD$€¨¸HD Æ´ (@ *®XL *Õ  Ô‰HQPEZ W,&€:Ñ *nFQ¨1- ˆ €+ˆ @5(@u¢@TT‘„g )9;—ÍÿlÖ¬Y³¿Z¼)½\©æ6³fÍâ)€'Pó}¢TÁ“¬™—n<ÚsôÄqš—ïX¶êP™Â3à±eËyñ4µî€Ä¼ðû‚F€¨Uð $ëɽ‡­Ñ=ûÄÇ„G¶JìÛJ›±'½qg…´´4žáÌ ˜æÌ Q¨SZP€$—ç²Ã½$!„Ðù›ý”Ó'ŠÌ? *Õñœ$ÅZfSt^zI!„¤7ê…µÔ*óª£c Î¤ªÿ6òŸV’ª2º"+5±³Y6oÞÌ 0/ž¦Ö/GÌ ¿/ *õ–„ I^&½°W8œA±WØ%¯@¯s—÷¦OŸÎó¸HPÏ)@Ò˜Bƒõe¹V!„ö¢Å’„Ÿ–§ *Õñ Húðö-¼2R’÷eæfücýA{Tû&&‰§P  ¢´ <âl^Q½GôZ³jÃÏ©6ot硃ZùV%{Á¾u+SRs-’ÉßgPŸ–þ¤ù†§TäìZ³zËá«Ö/¦s¿Á‰MŒçd9¹$mÓïÛeç—X…!´UWtÕ3lnŸ—*ŽÂ½¿-\›yåM#c½75Ì‹R‘»ÿ”í2O[EH⤉݂XI½Øþù¯‰½ uãšMûN”ÊsÛËôiÈ9¡[Èùûþ÷ÓæQ·ÜDTjä»™+IÞ]†MîRÍK{Þ–_× ì5rB¤#=eåÊåþa:˜óy1WŒ\~pݪe«‚§Œlåºðã(ÊÊÕÄtÐ+H[”¶é÷u¿z‡Oé΋©»çåÌÝÒ´5KRò¹¬¡ŠæÅ–»ùçE{½Û÷˜ì­ÈÞ&^Ô.~PøÇ¿&öœ”%«ŽEºjxsSé¡ß¯úÅzíe!d¸†Ž E»~üvÃIYƒj#¿°P*@ú›×ö‚ÔƒÅ!Ý.ïѼkߎ¾y{ÓŠh£Úàg>ÿx<}Ôåc†÷êÐ"&ºi|¯>-½J³rʘ'÷Ï‹B±d¤,YWÒaDßh=U}*™¹äàúÝJ÷±Wönß<:2*&:Ä›¹qÿ_¹,'«ÄÛ¾EˆQo mÙ±©wIv¡‘køÓpÿN§M¿eh¤ŠÏlj PQZQ€T#[áÉR}P¨Q#„Z?s€Tœ}ÚÎ3£aýËkäÉÖ ‡ÆÛ¨ç¥TóbËݾtEFìÐáC ª™¥°=C3(ÊÀÀ¹{^ä’+ÝØoüeQÞoŽªç÷ÅQ’S$; -z_™èUzlóêuK4SÆx3xîýk¢è5ªwþ÷Íݬ±^>¶S oy€¨Õò¬K°ÕLÒHU¯ärÍWgÃEž† ºFž°Üº<¹0¶ßÕ-haåþyqgf—Y~›·¿êÐ/s¾ï3eB'z¸õ÷E±UØ¥àÝâ›…¡ýJ³u¾-&ŠVîýk"îß°½<~äµmGvnÞr0%å`Ôà6~ü¶€¨5§áÑH½I/ìVû™WtÙ^áÐztœ„6ðß× ¸FžB8 ÷þï—ÚncøãªŽyч]6áÚ.gÞζe¯[´Né;a@œ/“ãæßIç¥Sle6Y5Bh þFÉZnåmwÿ5±ØöG¦ŸA±A&)hp“Hïo¥ìèwy§…øËó‰!€xònæ³ç:>Öü¼rY窽âgöçe»¡_/äyŽÂ}ÿ[”lé8úÊ„pÞUɼèLAUü¼4½o€¿7ãÜ=/Z?s€túøÉ E!ä²S¥ŠOˆ}vÜý×D±•Û”ªu­O A8¬"ˆ P+: !tÁ­[ûçoݰ;ýdΑmëw·£UjÃÿ­þy¶Ìsf}ò[ºU(å‡Wþ¸67¦WϦš¢¼ÜÜÜܼ‚R¶Ÿ»}^ Îy‘LM»´Ðe¬[½óxΉ´Ík¶…wЧ%§›þšœ}hë݉”ûN–XJslÞqÊÔ¢mï|4<Åa«¨¨°©¹â˜·,¡¢´ <»’І& ïWº2eÉ É?h8»ÌÜ¡¦kä)²PdE¥¢ ߢ”\ûÓÁÊ/ñí|ÕäÞa¼œºu^ Òy‘ŒÍú]Ù{íš-¿ü`Õø5é:r`G^ØÜ÷פr^$Ÿ–ƒF•­[·é‡/×*ZߨvƒÆöŠf¯yÓ‹v-ü&¥@ÕQj÷ì²õOµÎ6l“‹úuá»™—¯Z>i⤿¹Áüùó'OžÌ€ÆAùÛe‡  8´¦Ï.[¶ì…!µþÖD{¨HjCT€ŠÒ‚ðð$¢pˆ @5(@ *5¦AQpÅbQ¨HD Æ´ (@ *®XL *Õ  €¨Ô˜HDÀ‹ D  ¸ô)eé©'mj¸ * Ñ¥A€K‹ýØ]õÍÛiÂvèþ§ý^\§ûs¹“âW™oÛPÊ pC5&$¸¤Éåyùårý݉±Óƒó>Õ·50²Ü‡U¨H.m…¿LüÒa˺ɭz<·Ë"*Ž,|pp I’ MúÝ» Í"„œ5'ѧý7v6I†ÎOl+-Úöáͽþ¿½û¢zÛ8~f[’Mo¤)”@(B ¡ƒ ЍôfATªJ‘"M@AzPªtD üÄö‚€4©!”@€„@Bҷ̼$à» ÈB¾Ÿ‹KeÜlvž™=;÷œgf˺¨%IÒøÖî¹46÷Î'I;:ýµ7?ÍÂpñëá­*¸H’¤ñì1{oš,„œüE—*oy1"$ÐÇÕ£B›~»!³ðä¦A€Ç”G›uÿ]αñº3Œ‹|ðL· þãö§²N-¨¹óõÖcä!DÎÉ-Ò˜?¯Æ}ý^…“´{ïð3òsÆÁ1ÎÛ•~ǓܚNÈ=4¾U—µÞ£öÜÈϽ¸ñ¥„±Ïtýâ’I!²O}•þú¶¸Äë—¿ñäø¾KÎp‰¢žDL&xrä^¼"¡Þ¤i¯Tu×:‡¾0þãfW¿X|8G¡ ì<àùŠ~Aå¼\ªÜu,fdsFÒ•›ú@×üë)VÛ—r¯\u±Á”™=#!«0H®ž¼ªxyrWD”L&x샂ÖIgN»œ®­Õ§›×Ž÷FoŠË4›ÒÌëP=¢ÇÆD³åƒÍ¹iÙŠ“—»N2¥ìûtôç—ŒÆ<“bñ$†Â)§ê=_.ûûÈ¡_O7æ'ý2}àÂëѽ[¨)8¢J <îtAÏv¯{elƒˆþÿ'5˜ºsyëSÃjºi´^-¨ßÙ¼é­Ð;æœj_ðŽïÒÆî®Ñ£/¼øQϤƒs-Ÿ$çÖCkOؾ¶SÒ„úž:Çà.[ÊŒù²W0“  )||ÌýÀذÔV­ZQ8—»vtéÐåo°~ýúnݺQ(ðdPþöJ¤ ›7<Óâ[ÿ7&&fbœ÷¿þÕÌ*À.0™`oˆ ° 4 ›iAp$¢`‰É¢` HDÀfZ4 KL&+h@ *6Ó‚  €¨Xb2€¨XAQ°™ HDÀ“ ·ÜSKzÔyê½-‰FjñέíÛ IŸµí½ÔDØ(æ£×øãWeSþwoôÉ;öÉów¼Î®˜MI?Îøj»&ÑÑ‘QO·ºä·k¦Â‡›o\÷Q¯—ZFFEGõû>Y~à/gÆ~=íÝ—žjÕ¨ëŠxƒÐ•ï:éÆçOÙ–h¶ë½BÃö“ H %cï’5W"Í­ë¦Âtùëác·ª_ùpEýñ•ã' _¾v@¸Ã]?£òk7î£gýÔB¡r,¤ÕŠÓ?•_+äÿ7eÔVw)÷â¡Ë¥ZöŸ0¨´.ùÿ–NŸ;BWmËØ:z!„Prb—û}–Z¸¼Œ¬³‡Í:QÔ¼±ÒvL™8j\èÆ¹­¼UBa¾¶kÊ€g!ýúäŸY2tÊOÁæ,­iÜ»p̸1e*-y¹ìݽRÈë³FÖw“„B¥/í«2;|Î)úÕqo‡8gZ=mé°É¶Ílá%nî›Ñ{Оò¯¼ùaÿ2NF¥”Û­ß¿/>7vñ€þ›ÝÚõûZy7³ÉÍG#„Py·Øw]—Ï—}zL-=Qø;^^^„Š‹|cïê=š§f7óS !LI¿n=åÓ~u¯f•u¢êà7¿k¿ü›³o„Wu¼ë`[X©FDåÑd@¥ê…ÿi8½è£øÀv«êÝuã?mZ¸´Šfßæw®æÊB¯†„¯Æ Û2lZÃÏûþ „"çäæŸ³ëŽx§}]/•¶o{¿/÷¥>ýœ¯JÎøcÁ€iW;Mrdðùï“Âù~H*ß{n׆¥Õ"lp·ï{~µ+±ËkAÚ»‚ŽkPµˆ/‹ÜQ{èìÚ…ÿYÕãÔw/ï¾tÓ,\.múd§ÿû_L~Ñ_}ÇÏßï‹oeŽ™µN~}ŬWÊÝù Ô~-zF-½áÈ»5£Ý$;Ý1h@‚]  €b”·ë”®îs• No®MÕtB¡v/_É-íÄù¬{ÐÍñ ºÖ‹ŠnðBßÉ?$äßÙ¢$§þº`ã};VÔýµ,ïúÑ-ëhv¬ë¡æë;'Z¢îýéðÆ>·ÂM7ΜËõ vQ !„C™ˆùÒÑDƒ’ûÅ ±MžúJ˜þŸ^•ì„S×BüÔB¡õR'þy9ÿžÇ™MxªAtd‹Nýü–l²|㸫̪þb³@­1ñ×÷Mèõü[“~¸dâ^|^ÚþïbÕggôxºNTãæ/Ûp:»°T’kg«ÊG~:Ÿg¿;³ °£´ h@ ˜RÎ\2–íZ8m`ÌLÏÓ¸8&ªõŽ";5Û,|þ:LW{7~wJ½·»ææ‰ï~2acðÚ!UnO;äÇmX´Ç³ÃêÆÞ·~${÷{Ͻ³Û¨«òÚ‚ÙOû©r/<%©ýÂÙÏ—Ñš/ÞÙ7²WÇ‚sì*'w'‘šy5fìÐoƒß_üv-WUÎ?®‹œcùBíäî`ºœž/ —¿^½¶ôs#f5sóv×ož>ïýÁëVöÒ !òŽMjßgó Uhûu Ö‰ì+Ç’dC@é¶ï/~Gs1fÞÄ ƒ]B>o¹ë~_|vÞÕc‰FCiïÎèï’úÛ瓦œ¼it”«$„äZË/ûçø,¹¦“ž¾'*À.Ѐ@ñ1g§ä:øøZœ±—TêÛ1›•»¯j*—°†M þ³J%¯Ë{_ÿñçËoW©P0… §ü2oSZÔÈÎa]ÞàTkزUÝþØ0s@_eégíãþ8Ÿ»°[ó…·°¤ýóæâ-$µZ*<ÌW̲"„)ùàá”ÄŒ÷Ûn½ýdƒZu¼ñ‹.jë#©n?‡Mò=¯^íU­qaKT• úÓ{ûÅü‘Ò5(@-„CÅÞ –={ùØ–Ùc_›úɺaÙYf‡šÝz>WÏYˆêåGÄþÔkç§Üï‹æÜŒ\UÅŽ=Û7ò”„¨ôþÕ]—w潨H'!„Ú=ÐC•“’e¾DÀ¶‚¤Ûÿ¤ wú†Y!ŒW]–ªÞùZdãí /ä›qÇS4å¼îx>%'=[VkT’®LùÜîÓwg5\?“"ùV(åuß/>Ð%°FY)aÏ©‚ÄSÏ^S|Ã| /à0ß¼š¡è}\Ôv»g0«»ÀdÅx@èS±¬fí ùÏz; !4M_¬²pÑ´•Õ5t8ºâóØR­GUpæÄµ¯wœnìûÕªWÎ/í7ãZTÛ5KåûrÎN9jR³‚»¦æ\»xŸG»/šÝ¾°!gÿäa\›·®WÎ-÷ÜŽÏV_)ÝõùrÖ_ˆSxÇfÎ}ç/ØâÙ14uû´yµGÖ÷±yf;ïìwkbÒÂ:tkìoqHëPîÙ6>›½)ôõêù{¬»Zî­–¥µÂ;¿[÷5>~÷éóú#“ú/×¶x¾Y¸Ûýk>9ìÒznmwéùÖfG=Ó¸²éòÿ-ŸwÚ³e¿*NZçVÝj/úáôˆqC®}?ck^ýqO¨ïÿÅk¼¿ÒÔaøŒ×:¿U#ï—y«ÃßlW® *(Ùñ‡“+„ºØï¹{¢ì H#}Åá¹ó¶Î­]ÓI¡)ýÒ” —FÏõÚRÙµü3ïOï]ÕI³f!dY¡ñލ©_°iÆöEyÂ=ìé¡ó‡µòQ !äk?Îû*½Þð®áÝXUW¦n%ãªÕ×]Ë’ëtþdTï*Ž6^‡äR{à''}4sÀ¶l­_Ýn“>j]ÊvR8·éÓ¥1uf¿vב»®ÂŸŒ¸6~á€^7UžUÛû¸{°Vƒe¡ÈŠBã_/\^²fòæt£Ú§Ú‹c¼W×U’KEV×-Ü4csR¦Iã]ý¹Q‹×u“„h;eFÊ„©sûnÏ×7ë?gdk[רxñžF.8uÂÒ÷^ÍÒÔï1ûƒÎ!ó/JæÑŽ©jL(çh¿;†>>æ~`lXj«V­xG¡¸ìص£K‡.ó€õë×wëÖBð”ŒßFw•?|Ë´–ÞIºñŠ.ݶD/Ý0´ŠÃãXróÕ­½»~VnÎÆÑ5œþaãÜ{Y¹… ›7<Óâ[ÿ7&&fbœ÷¿~‘\«»À×)Pœ$÷¨>¯”Ù?oÑÁLùñxÅrÚ±ÝWCÚu¬øXæ!§ý¶`Ql•·zUs²ç—IT€]à+Ø(^ÚàŽã^qÞ2r·‰¦ÇáõfÇþrµr—Öw óãÁ¿iÌè_ÊxÿÙµ]¿P®U€¥Á(6Ž•{¯ù£÷ãòj]›Lÿ¶ÉãZj]h—»»</”YØ&ˆ €4 ›iAЀ@T,1™@T¬  €¨ØL ‚$¢`‰É¢` HDÀfZ4 KL&+h@°7JûI ¢(ºvíª( õø˜U€]`2€¨XAQ°™w@ *–˜L *VЀ@Tl¦AQ°ÄdQ°‚$¢`3-ì¦oY[ IDATßÖ »àååuÿ!aÃæ T €¨€¡ éö?ÿæ‘Ï´x†r<4 ÁŽÒ‚¸¿¤{¯g` KX–°„%,a QO¦Ú×z%,a KX–°¤Ä.yx¤ðñ1ôcÃR[µjÅ¡-FZ¸Ÿ$«?Å–°„%,a KXR’—Ø31ΛY<öþ]RÁãY–°„%,a KJòfðäO)P€"Ĭž|€½!*ÀŽÒ‚à+؈ €%&ˆ €4 ›iAЀ@T,1™@T¬  €¨ØL ‚$¢`‰É¢` HDÀfZ4 KL&+h@ *6Ó‚  €¨Xb2€¨XA€½ÑPØOZÿ¹IQ”äëÉIW“rrsJBÑôNúÿ?_?I’(ÅyHÅûÁpMT@IäååU$W)$_ONI½^9,ÌÃÝ£$Ô-=#ý\üy!„)ŠCqRqÀ~$_ON½‘Q-ÂÛË»$¬oêÔØ¸Øb®‰ °  H·ÿù¯Ÿ'éjRÉ9ÚBx¸{”-w?#Å¡8ÿ®8`?’®&ET‹pws7 %a}ÝÝÜ+‡U>zü(Q¤…"h@ÊÉÍ)9G{·ùîs–âPœW°9¹9^ž^ùùù%d}eYöòô*Æáš¨»PT H%³ñú>ךâPÖÀ“1p)ŠÂpMT@ RT Hàɦ(J‰Š Å»²DØQZÿ¹I%òÜðý­5Å¡8¬5€'`àRE–åŠq¸æ{`ŠêëŠl†Îœq|Ƕÿ»ò(.™Rrâþî§Ó™òíÿxHký(§/Í7þÜþݾdã­‰3Nìüþׄ<å!ïE[cÒžï¶½a~ˆ¾ý+þËï¢ Àã$I.yh@BIWœ HŠ)ãâÉ£q—Óòd¡rpõö ©âÆ6±$gœøé×ëåZ6 ÕK…Çòçùéœo£–UõÉ»cþk<Ó¨¬cÉ<ì4g'Å:|3_'O¿²*•÷a—€‡úÑ]t=9JNüÎ/–­ûß‘„,YãZ¿u×ÞÝ–qàL Qö—Än@zð :%/ñðî£i^•j4ðu’òo&'\¼œ’üxEé!ç~¨=ÂjE*žZ©DGξ°ï×£i®!UêT÷tPrR¯œ?òB©†ÞOÖžö4påµ † kG\–T­s¯1ƒ‚2Ïþ¶~ùÇ}âÞþlBë»9FV¥‡k¢ìB‘ÝéßLæŒK×Mžµ*•vB/Ÿ€`“IQ‰L!97ùäžÓçRòÔîÁ5ëU t’„0ݼxâHìåô|EëV£z9×ôƒ;˜"ŸŠò× ó#;÷fUmѰ¬£dN;ú¿½YÕ[Fêþúm¦'öNHÏ5ÊBíäR½V¸œ‹/Öv|9;áØŸÆHÿRz!ç^=uäØùÔ\ÅÁÝSe.…%μ|âÏØ„y²ÆÙ¿bš¼t–/DÉOúsÿ±Ä›y&Ehœý*DÔªè­-ÊÏ”‡W%?éø‰ÇŠ£Â=5Bááá”-T™{ؽ«o¶²ˆìs¿ütÉ¿F™¬3g’µU[6 r’nï£göˆOÍ6ÈBrð,[¥VÕ².êGY°§¤ ]TS~]²êLÐ ?è¤BˆÊUkUtè=dù燎ŽHúôÍaGšw;ºuG\†cpóþ£Þn]F'äŒÃæÍûjߥµWåV}†¾Ñ"À|hrÏñ¹ÝßðûuÕq7Ý«t>ªWM÷¢êòçZ °Iü狼™OÒ9iENJzþíAGÒh ÞJúÉ3ÙÞ•ëÖ­êo¸øç‰£P ÉÇöMq©Õ¬yÃêÞ™§~?|ÅèêçªÜLÉ6 !g^½nÓ¯¤…P 7ÓŒz?÷»zUŽ>¡5¢5m]#ÀèøucTïµã+ЬXº{À½yfÿþ æÀõ£Tr+<–U ×þüýD¦Oµ&O·lwàØ=ë¬vö «Õ¤iã語7cŦeÿÃ,ŽñFBªÙ³|ˆ‡Å9Iëìò׋շ±ÈYgŽ&J¥*T½3MI*gp•z›4m\;DuåÏ#‰¹Ê#-ØQT$³Ù\4W˜ožøñ„¹ò O•ÖÜZ¤8„¶j”}ð§ó9²"„9þ›ßLõ^=¢wSóÏ3§|›`0\Ø<~üÿœÚ[´fÉäWJý>}òÖƒ¬(rî¡ß¢ûÖ3,aÃÌ qyEv¡‚ÙlæZ ˆ˜Ú=¬jÙë‡öÅ\w÷+åëãèïéT$ÏZ ë”u„âšpåbZžìvó\’P·zˆ·V·*5Ò¯î>wÕT«”“!9น®¥¨üKëÓÓ^Y)¹:/¯»æ 4å+{E6äåJ~þúøÄô<¹”úñØ@JæÉ]ß´\âTÊâ/æôóñYžU[T r’„ðT'Ÿ¾”'„’wõÌU]X³JþzI§ á~^¾i*åý×Ð#9”ª&„b6äå ¿RŽWÒ3ŠÃãPÙ•+;ø¸êlà6WÿÞÝÀW%„¤¯Ø¨a¸ûÝ;„Ê5¸²«²)/WåàzæLZ®\Ö•!@IUd³ ¦ŒK)fˆ@½åµj÷]VbrޤM•w& mæ. QÛãüï£~Ú%ìÒ·×êúð¹p½þO÷xqËÛ¿L}º¬ë½?m`! ž'¿~êÜMsEoUQ­o1V›¨»PŒ_Á¦r ¬Õ»ܵääëׯÅ9{Â1¸~Ã!„$© ŸNí –d³,ç¥g+úç·ÚÙÛY\N78yjΧd}³’M>á!G%fnd*îáw÷‰È9‰§þ5W‘4:µIÑÉÛÿÍ*èCëF–.Ì>JÞ•C’,2Cf†ÉÑ×ó® ÁäœÙ¦›'wm‹•„B‘Ͳâc¼s¥M7/žü36!-_¨´:ɨ¸ÈE9&ëí¡l¬¾­Ý@Rë4V>W”üëqGŽŸMÎ6KÆ$K.Ê£-ØÛw@*ŠAZ’ŠrçÓɲ¢!üKHŠ,Ë’:¿J~Ò„+çϤÞ<;þµÝê‚qÝh0ú¦d™J+’J­‘ žÇÁÝI2æÌEuCWî€÷W°©ÜýƒÜýƒÂªå&ìÙuäTbÅF^V¨¬¾SÕ®~nJljFÊ|Šn_éPRºOF¾>ÀõÎ7˜’“pè`¼T±~‹ ¾zµáòÿíŒ{œFgÞÝÓãöÒ4Ò]'=„bm(“•WD³:¥n—BRi-»²ä›çþø3É¥jô3¡^Ž"+îç_“—Ѝt.NR~F¦QñTK6?‡îY}1á÷Ø ÃÕ#â²ËÖnÑ8ÀEc¾öÇŽC J´"‹ ’K —:ýâ•,s…¿:GÍ7Î'C}YQ ƒ„$„l6É’Z'™µÏ c?ìXöÖG™¤Ö»ªc…Røº„(8ߥÈ?W4ë[œŸuìp°Ÿ´ þûW°=pì6çfæÿõ”t..Z¡˜­Ÿ×V9º;K9©Ù¦ÂÍNÍNŽ*›ŸS^RBB¶k »FíèeNº¥õñ¼«ýÈœ“š%¼Ê—/¥×HBÝ|¢=|¯‚¤uvRå¥g™nà–ÌÉS¯d§åk ÿ8hïvLY©9ÚR僽U¢(‹ò(Š£õ,ã¥J;{.ÝdqdoÊɾ=mbuõ•Û ”Ü7e×Ðr./Ê1«à±S0«  ¡¯Ô4\ûíŽíErΙ˜ï/9ÕlT0áû×ò‹Ç’Ô•ƒ‚ƒœÒN' 7Ï‚?n.:•R*,/åS„RT˜UŠïHŠáêá_Î;—/ëëæ(Ò.L0º‡ûëUV¯¬ÕùVö=v¡foÕÍ G/}jø;I*ÙËGs">Ã;ÒS+„äè‘¿ÿªð¯à|WW;yè•Øóg¯h|ÕYWNŸL“}‹dÜ´‡/$Öºû©ö;zF)£7ÜHˆ»dP|…œ+øÇ=k8 ³¢Sâž]¿{†»eŸÚºd¶áèÞZû^6u‘èñTE—Ü+ÇùñBÁ½ Ž)nE†[)šs;Å{$¢ìB‘5 =ð ü+‡Ý<›{àœA’ÎÕ¿Rýˆò.*9Ãê£u¥ªGW?~$ö÷Ÿ BëP¹AD'IµK)u|^`Á}k´e<¥«æR®w¿½$çàUSÆÜ{ÖÑ+¸|9·ÌÄ¢8JqŒ®TõÚ‰=xUÒ— òM?WPà€Z kœè«…W3 ý7Î/8îì+ 'G»ïyNwüÔñ=\ÇÄÄLŒû÷ß Ê¬ìB1Þé `×*Pœ'¸8`WW‘]Öü÷dY…×*ïXɵ @‘5 •ÌÞk»¸Vâ<¹Å»¸Ìfó£øªÂß ï×!Ìf3×*ÅõlàqR”×*ü mHÏyË ÃBq¯o1þv¢ìBQ5 9ë333]]KÐWÙfff:ë)ÅyxÅûáä䔓›ã s(ö“ý†$I9¹9NNNÅõø^Ø…‚Ö£‚Ìð_žÇßß?áÒ¥ÌÌÌ’s´—pé’¿¿?Å¡8¯8`?ÓÒÓòòó¤’!/?/-=-0 °¸ άì(-ˆÿÜ€äéá)Ëòù òòòJBÑýýü==<Íf3Å¡8©8`? †ë¤«Iùùù%a}üŠq¸&*À.U’Ùlöõñ ðP©JÄŒ™,Ë&“Éh4RŠóðŠöƒáš¨€’¨È¾‚M£ÑÈÅ¡8ÀpÿŽk`GiAp$¢`é?^Í ¢žLEu$ð¦AQ°Äd€½áH° Er¤˜˜* žH­Zµ"* D§ñŸºuëVr*¦(Š$Iì9l#jE­@­¨UIØ:ÅÒ¤Mì Hÿß³Ë6¢VÔ ÔŠZ±uˆ xòq¤AQŠÀ6¢VÔ ÔŠZ±uˆ (iAp¤!Ë2E`Q+jjE­Ø:D<á˜Lø7‡Â°Î6¢VÔ ÔŠZ±uˆ xÒÑ€ôoᨵµ¢Vl¢JHZ4 =E¦¯”mD­¨¨µbëð¤{\&L—×vjÜcÕe“=¼˜šŽ¼ùSÿz­§ü™Ç¾f¿ÛˆZQjeoµ2'nìҨ˒x#µúïrÿœÔ¢É ß²ìpͯ}Õ³é KÎx×[Ã÷*À.ÉW°Y‹áù—]=oå·¿¼–'´!ÍÚtéÙ±a“Þ::++ëÛï¿y®uww>Çpó§7ž}ØrIhß%/þ¯÷ßyÛgTg'+·t>Ušw:¤KMwNÙX«•Ó¯›:kÍÿâÒeáX*,²e—·ßm¥^ÿÊËß¿°vm·Ò¶>½²÷l=J=cëGõô%i¿j=Éwî·Sê8=”ß—w죗ú|•.„·óê/û)ó;õЏcQK÷ÿ0”Þ±¥rnûöÉîk7ö Ñ>²÷ Æ-¨ztÛîo¼Ü¨´ÃCøLÈ9<áÙ~1™÷,÷n?ghüÀ‘ù£w|ÞÆGõ˜ŒW’KpT‡!#ßh\Ê^Ž•¼„íŸN[¸õà•\¡v+ݦ÷»/×÷R•”w=Q%3-ˆ"n@2œ_?¸ÇœÓaúNî%§œþã_®œñEÍ:£«;ÚcNÈÎZ·~õ´kׯéÑíe77÷¿¼Y¾ç.Ë*mÓú‡®J¬­ð‘—c˜ûWñ°¶tõ‡Îy·²*ãÒþusæõåüõÜÔ”êîZ™®lü΂”¦ý§ªámL>öË–Í?œ|ýi2¯ýê¡r¬:lóWu†vï0ñ›I }”SEþ+t!FŽËñQ?ª÷à;•ÅÍ«gölÛ¸hXÝo/]ørù"ÿTp {óÓÏÚ!rŽLô¹~àôw«: !9ú†¸_÷‘\ÝUe÷ûUÁx®ÉJücý¬%ƒß÷üzIç²öpä¨d˜9`ôÎÒ/™Ù"D—`Û—;ºÒ¹¾—#ïú"ÇÙ,Ø…‡Ñ€d¾ºmâü£¡}/Ö©yíj5ê6ëÜÿ£u_ÎìRV“¾³oýç'oÙ8©ûSMÚ~&þëÑžkY;*:2ºu—±›Ne+BäìùtÃ~Ÿ/ýJ“¨èÈVoNý9ùVב9íЪÁšEF5o7æ› ùEój³³³×¯_s#í†$I7of¬Û°&33óïÄÚENZ¯òÕkFDü‰¨à#_=~æŽDÓçb.l›ôÖ õ¢¢#›w²êÏ ¹ ±ªë̯>h¹^rê®I½Z·lÙ¢ËÀe‡Òe!òc§·mÒýóõ¾útdTóöã¶]HÚ3·Û:QM[¿³âX–"„rÚþe#Ú·ŒŽŒjüô[Ó¸b(±;¶µm¤q ¯R=ªU¯‡G‰CߺvrzÛ¦¯®Û¾`P»ºÍÞû-[Î8ºvDV‘QÑuž}cÂÖs9EÍ8²tX—è¨èȦí:µkR»ç7Érξ‘O7òõ–™}ŸjØvâÑ×(2*ºáó¯[g/sîwÖJN;´ý¨¦É¨‘/·Œ¬^³þS¯ Ÿ»ñãúgÆõœo<;§S½¨èÈ>?^‹[Ñ¿ÓsQQÑ‘QMžî5廄|Óåµ=ßý%;kWßÑ‘Q/Í8²oôÓÍì.èx0'nzµ~—Œ÷ÔÄøìW÷ž*¹²cæ€gGGFµ|qÈg¿§˜…BÉ=óÍäWžmÝ¬ÇØ/ŽeÊ}•:~´|zïšDF5m;|ÝÉì»Î_ªœ]ôIÒê]œµ7w|eSŠñϱÍDG6³?ÇÖÒmÞ÷«†ui^ÿ­—NÿÓ–:™øó¼IŸŸÌB˜Sö.ܹaTtdãvoÍÚuÅ( äF7l˜úfó¨èÈÖýæþ‘. !§ý±xHç†QÑ‘Z¿8`î¯)òý¿«T©Õ¢ãi«Ö(lá¤Í—MVÇF‹1úÈŒ·Ú7kÝॷgýzÍd¼°´KÃggÅ~ÎÌi×´û¦ÄÛt*çÒUªGÔŒˆ¨Y-ÔM¥ñ,_­fDD͈ê•4—¿1qÃESÁz½³zÙè®QQÑMzÎü%1aû'½šEEGw¹1¾à‰I?/è×¾ydTtÔKïÎÝb~ôãUxÕº-_ón åÔϱÙ÷ÑÐcåÖÙý_¨ÝøÕOv|f“šûö³ £#´é5ÿ`Fážem”»Ÿ—Ûòãc~K é9êݶQ5«G6}¡÷ÔåKTÒÙØˆVÊY'7ìñLdTtÃÎc¾L0> ïz¢ž`áHrÊÞojôn_Þò”ºÚ#´‚‡ZaLùnòÆ´ˆ.ÞŠöÖ¸ThÝ÷Ã¥+W®šþZéý³GoºhB9÷ðƽîÏŽ˜òáÀÚ×7|09æº,„Æ„•‹†¼6~ÆÈ6š]3'ÿ”úßß¾999ë7¬I½‘êååݫ盥J•JOO_·aMVöß5uZýBE6ß&[›¯”Ó÷Nz{N|ak¶|½ñÃf×—œº?SBV~¼ÜëåX¶a¯±óÖ¬Z¶h`õ3ŸûôdÁµ¦ØåßÉϽ?}øÓƘz¼:3>rÀ´ñÝŽ~þñIfaŒ_óÞ{ßë{ÌÜøÃæ…ýü™0rãc ݱÿöKs$I£’Z’$!ŒÇçÌÙïþT¿wÛ‡¦ÿ0jà¢øÈ!Ë׬ZÔ;hÿ”w§ì»©˜“¿óÞâ+µ‡Ï[²ìã^QM²ËÙ{¦/:Ü®_ŸgK +•Ï:½èýy§"†¬Z·zÙ‡¯×I)¿ññooÎ߸néôþMœ¯¦í±V*Oo‡¼sÄß>@Pé\½êŽœß;HòÖç;¶oÛ5£‰»Æ£ú ïÎú|åÚÏ&vÐíœ8igªÇù“895šöÕ¶·Ñ¿‚µf–ü»k’f~’ö«¹Ç?8r‡¾ë´›–Ð*{ý a/åŒß?é3õ@pï¹Ö,yaþÀñÛ †5Sâ×ߥÖè9nú¨ž{ç ýü´ís ’{³I _ðRWyÿ«¶ý¸eTÍ|[CÊÅåÓÿ'5xuðËnÿ¼¥t…Oo¼øÅakržúhùšµÓ:êc>è·øTžBÈ9ûn6´1íÃUϯøpEl~Ö¾éc–§6ÿhåÚõóGv­›”e~àZIŽÛ÷yZûõOI[ccÁó­_Σg-]·ráM3ÖŸù{^é§ž¹þÓösùBa¸°sgjXÇÆþ81"çü±üWï.“&½Sÿê—ƒ_é³4»åÈ)ï¿ ûmæœ=iŠÈ9¾¨ßÄ}!}~ûÍú¹Õ›Ç|øÃu¹8ö+E6™…ÖE¯ÂöGéOW]©Ýoúǃ›fl™8÷`–0œ]9dèæü§FÎ\2wØKå„"„ædk£Ü?¸X~–»ø¹ŠäÃǯß>¦vrÕ©¬~À™­.LûuÂÀYG‚_›>þÔ7ê{©Ÿ€wýÃBì(-ˆ¢l@2\?sMñobã²µG›+FÕu‘„¢ÊëoaιqÕ±f³êúÿL2ˆRB¨œ›}´`x=g!”Zºƒ»GóçÍg* ¡ ê¿`Ú›!Z¡ÔÈûî›i‡®ä·öþ Â_où2%5ÅÓÓ«[—...];w_»~MJÊõ¯·l~¥ÇkpŽAÎÙùî3;oýÍùéù_¶¸û×]þ“G¯ o6.­Âÿ•Ñ_üáLî[Bh‚ú/¼k½"¢_}C9/ýZ²hÔÀ{ÇÉø,¹¢BSmÔœÚx«L/nÜöU“)S߬ ™â»uã_Ío{cíÆ«M>˜×¡†³e^èÓm]·˜})ÝBJd“Íó@²áƹŸÎù]Dެí&ªÊÖ/ë௦Ëëzísí°òíVU„(ÿÞ˜í¯9ø®ßÅ•½^ÿbðKåµBTr9¾jãžÂ#‡ú×Íná) ‘w|Ò½•O ŒKÕ†Ö¯_5ÔC -ÑP䟬øwmQÁOU!4¼ÁsvZ+·zý70å­61ÕD׫װY‹Æa½‹“Z¥Ñ»{xxh„./õ+'cVÊ5mƒå–/?~Õô|J­võððÔ ‘kulH¾«&OÎ~õ×釫¿½^{èâW¢¼U"¬÷˜7~êòå7ç[TZ½ËÔü“í"]…¨øö¨“¿ô^ýsrËBhCß™5±gFˆ&AÉÿ×å»ñý*WÖÙ8ºÖº¸:¨TZgOwINÞjcHQ¾±ø³…í!ýÊÿí–2$nœs[7œ î³±Wó2!‚ÇÚóüÌÇß&„äÔèã¥c£]…È÷>¶ñ­£§Ó2ò²]ÂÔ«â,BÂj6ý—µr ª(~‹»šhulìsk­«¾Ü§ª )×Ô —ùòÇS)¢Aóç˺aÛù~UÂ¥ø»n„¿Þ¨Ôƒž•ô'ÎÒÀEäû^ûÓÑžsÇuPÉ©_mþâÔ5ck+¿3½4wp«J:!JwêÓvcÿïŽg¶iî.=ÒýÊœyáçO—žôn=«º³’·Í†±‹§¶ñV S¹Ë[b¾9žœé´öËKUmü¼ŸJˆªºÝ‹¸.„)i׆{G¹!5þáÃ¥Sà_×iƒ;¾×õÇ÷>jóìšúÑõë×oôTóšeœ„µ¸Ó—®Y[xuõ/Úé©B.smêÌ*`ÏŠøjæ{b¸Ååeúgæo}_•ÖÙY[0ØÊÇ6|¡šxôRÎÙyZ,’„B1åäÚYfam½”ܳßÎùðÓ­ÇÓ„ÎÕ]Ê4…ͅϬSKB•ƒ«ƒZÒ¨ Oé¸heƒÙœqîäõôSƒÛìRKÇÄùFÿkY²D…Â8·ãí–;„BøÔë5wÂs~ê8!ÔŽnŽ*!„0&¼ª*S³lá\˜SpMy]Ü•‹§õáuï=G.iô®º‚J[­¼b#TïIDAT¹ÒóuUãßïÜ%ªIÃúÑ­Ú4 +÷Lc÷¡uéÓ4:ªA‹¶-«ûhí²VºÎÓ¾jzü÷_öþ±ï÷åï-›YéµO?óŽËýÍ©û>ûxÖªÿKÈS9¹;çUÁ÷uáŸ>ü®šTvS=öûÕ]Gvéçãs}–+l†×øTsN9y%IN”K7*<Þr(S»Œôý©dC!„PÖ@Pµ¬ö‹„T£ºûy-¶‡•ƒ«^ý [*/)6Źb•[¹ªÞ9ûã3ä !©´Ú‚רÒ{:«L¹²wƒg¦/èÿÒ™ÆMë×múìs ƒõª€eJ²µ"Œ‰;NžµñÐU³ÚÙÝ!Ûìa”…& éK>]µ=þòêí?¦Uïõà—)K’¦`äU9º8¨Uã©äèâ 2̆kÇÏg&þֻ闪‚3ûùc¥Ô\E<ª¨ð×x¥ ë0~é»uÜ$!þñ£A¨ô^z•)7?ã|l–wýªžwVÅú(wÝð÷.wî2*Ïúï®ÙÖvÿo»÷îß·uÚú¹s›~¸bti+1ýŠÕ…Ç®ˆà—Ë럜w=QO¸‡p$Oy_ñíñ 9J¨»äX¹ïÒ•]Œ™&¼}ωƒÜ£³‡Í;RÔÚù­+{˜Ž}Òé­KV2‡Y–4Ú»Fg•T4Ãõˆa£îZâììü7ó wæ KZÏÐðjUÿšä¸yúžáƬhjŒ\?)Êåö'•ÎÍ忦{×Ëxî‹!“w… ü4¦C„¯”°ôåW¼çCN%Y~âŒÃfEí×mÖÂ×CohHj½‡¶dîØV¶‘J5bþàjn.>þîZIqW«Ç=û”"„b6É·>6mÿ2«•÷ÐOÛüã;ßÿÓ¢1+7tXºvÈ„õ+šoÛùë¾½+ÆoXùÛ”¯&6±‡»†X©•äàW½YçêÍ:¿5ðüÊ×;.Y²§Ó0‹]9ýçÉ£>¿Üfʆϛ»äíÖjüý=±Úï…{jRK/=ÞûÕ=µ»Ÿ¥VŸE‘e¡qÒÞw=þyH¹ß-ekmï]‚ýUúòÂo*ïüî§½û¶ÏþjÅ7}×,ë]^ûÀµÊ»r4I>막³¶"Y_„Ä­ŒÚ,õšñåkõÓ¶õ|iYÁ¾ÔôÅ ³Wn?õ”î7k ¬ïùŸÞIãé­±U1+ê ýÏ{Î÷ö¹­‹§êÑŽWƒb›y"«p·0üóGCá&Rd“¢¨Õ÷~PJÿ¸‘­}¸ÜCíÚ Mhƒ6/Lÿmd§Ÿ~Ùý+{£.~Ö½ µñ3Ì’Z¥z¢Þõ ×*ÀŽÒ‚(Ê$•OýV•{—|}.O•“_…J•ÃÃJ;߳˛oœ>qÓÿÙÏTöÐØ îrFÜ¡kú°0/»ÊÖÿêƒ. Š¿|éø ½·Oá/Õa8çâá$×èW_ªá«“„Í÷7L©ÜCË;§¿$yÞz~/7§’z‡«— º•._±|P€»Õƒ1]@ùòáKùAÉ?|U]ºb@© ÷ìøóòW^%ËŸ­_:aÞÆeo]û}÷eƒäÖ²Û€ ³Wnžµÿçø|;¬•œy9!Ãl™ütŠÁ`*•Mf!„0%=›_¡c·æÁ.j!”Û—æH·!„¤uRËÙ™¹à¨«àĤrOM®˜ÿýêÎC(råô)ÇÎe<ÎtýäélïðÒá¥UWŽ\,¸P^ä]:xIñ/uçäAÞ…ñæ25JÿímÓ$õí"ßÇr?[êÇÀpßì¸)[D¾yöxª>4Äú …eYq,]·mŸá“–­[ÐÞ3þç#V®û§Zâ¿Y²3/¢S³2¥ÿnE W_Õzôˆ Ô«„øë÷jÿF/VNÛ±`ÑYµÚÕ-òk|Â˨]ÕyÝ~U^®ªG<^UªÕ~â'cÆ Ys6OˆÜûþhP»– Ð¥œL¸u™¼R ¬r¾ôùjºq6)÷öoV9—)ç!ŒyÂÏÊFt ´¶Ð§œ·ùÊñDC±‡?ÔOü"Ú 9B…=x HšÀF÷ÚöÚ¢>½¯õíÙ2ÜW›rþ÷DYøÞ5–¹‡–Ó_YûmdÛ2™7ÍÙ’fª[øÆÌŽÛñýn]¸Óõÿ[>óßK««:‰”Ç}àPû·xµéâ1ÃÇøŒxµQYéú‰ß¶ý¢{õck 뎕}2øâË_”Êæ[/N0VºŸßàTååeºÏ>Eô±š[Î…ƒÛ¾=Óxâ„–R ܱ|©Zt­¿xê„ùácÚU0Y=é'ÍS3ëøVðnæ¹eÁÔU]ÂŒç~]¿îŠ1ø¾*ßpPƒóNÔîøl½ ‡ä=GSƒƒ/Mí·¾\§—¢+ºfÜŸ¬.û¬¿ÎkeJÚ:¼Ïž°.Ÿ®ê©\;¸aöUݱu|rðÔ–ez/ÌjŸê/ŽŸøBiµÖJ[¦Mßî×Öoóþ’lEh½«=3xÉ V~j•{ÿ~ ß›ûn¯¥>OÍ\7bس#玴ٽÒs¯v¯qbƒBhC^Úy÷èúmsªúβ…=†¿·ûýy ßêW·Ó+݃gï’k…º>_[Ö¤…çã­‚l<8ç½·nýÍùéù1cçL6}4kèk³M¥£ºÎšÜ5D«Qà 9añ;Ó—Ðff¿ÿ\)•|Y¡¤X:dyBŽC™¦}gŒúΖû¼Ó:|Ûèg»v^ýå Ê-Þ}+fäô·{i‚;-]9膕÷S#†ýþ÷[jñà[[>øåS³>š1êõùßZíÆ/ìWÅIäX;~ñªQÃaÚ’a«RŒÂ9¸ù›“×Ðß_­ û>é×C¡r)]­aï™Ó^n BüÝØ¨)ÛnÔ #cޏܧFÇn]*œÿñöÚÕk]AŠéáò†9É¥ö ¥;üé¬>k²„Ú«R£¶½ËèŠc¼Ò†t˜øÉé׎]qÅ„ûýhÜû¨×è‰ Çþª)Ý KÛúÇ×*6G9ÃD§j}4_¹yõ¸M7 B8Eu›2±oU'•°²¼+Ý»PÜuêÈ‹#fOºÉ£j›ö/]ØóÄ}šÙ^>>æ~`lXj«V­8´…Љ‰éÖ­[=Yξ‘/“·\×Ù^×÷ü…óåBʱÝíÙÃÙF¹‡&vjšøÝ„H=µb¿úÏL—×vëö}Ûu+_-£¡VLIÿiX§IŸlSKO­ð·Î¿;—31Îû_ÿjf`ÖžhÅø5ïxäÛ(/výªž5#‚œ³Ïl›µSj23̉Z±_¡¸k%§ì^ý‡cóù•õÔ OèÖ!*À.<”¤']1NGâ‘o#%/ñ÷e V¤…Ú³r«w¦t‘¨ûйV¦+ÿÛpÔ­Å'"¹³_±uˆ °÷´ ŠòHÿ‘¾þÇ;³óCaX·ûÁ½È¶‘SÍ!Ëw ¡V(úZiÊtßô[wjõïŠW¶Ûª?ºQ+<É[‡›¥Â.xyyQ„88Ä6¢VÔ ÔŠZ±uˆ xâ´‘Ècp#h¶ÛˆZQ+jE­ð8o`GiAüç¤ã'—¨¢•´õeQ+P+jE­@T@‰STW3W«Rb `h@ *6Ó‚°£; ;ÀdQ°‚$¢`3-ˆ €%&ˆ €4 ›iAЀ@T,1™@T¬  €¨ØL ‚$¢`‰É¢` HDÀfZ4 KL&+h@ *6Ó‚  €¨Xb2€¨XAQ°™ HDÀ“ DÀ ˆ €Í´ h@ *–˜L *VЀ@Tl¦AQ°ÄdQ°‚$¢`3-ˆ €%&ˆ €4 ›iAЀ@T,1™@T¬  €¨ØL ‚$¢`‰É¢` HDÀfZ4 KL&+h@ *6Ó‚  €¨Xb2€¨XAQ°™ HDÀ“ DÀ ˆ €Í´ h@ *–˜L *VЀ@Tl¦AQ°ÄdQ°‚$¢`3-ˆ €%&ˆ €4 ›iAЀ@T,1™@T¬  €¨ØL ‚$¢`‰É¢` HDÀfZ4 KL&+h@ *6Ó‚  €¨Xb2€¨XAQ°™ HDÀ“ DÀ ˆ €Í´ h@ *–˜L *VЀ@Tl¦AQ°ÄdQ°‚$¢`3-ˆ €%&ˆ €4 ›iAЀ@T,1™@T¬  €¨ØL ‚$¢`‰É¢` HDÀfZ4 KL&+h@ *6Ó‚  €¨Xb2€¨XAQ°™ HDÀ“ DÀ ˆ €Í´ h@ *–˜L *VЀ@Tl¦AQ°ÄdQ°‚$¢`3-ˆ €%&ˆ €4 ›iAЀ@T,1™@T¬  €¨ØL ‚$¢`‰É¢` HDÀfZ4 KL&+h@ *6Ó‚  €¨Xb2€¨XAQ°™ HDÀ“ DÀ ˆ €Í´ h@ *–˜L *VЀ@Tl¦AQ°ÄdQ°‚$¢`3-ˆ €%&ˆ €4 ›iAЀ@T,1™@T¬  €¨ØL ‚$¢`‰É¢` HDÀfZ4 KL&Ø %€=(h@ºýÏ÷$­ZµbR ¨0«;J ‚$¢`‰$¢`w@ *6Ó‚  €¨Xb2€¨XAQ°™ HDÀ“ DÀ ˆ €Í´ h@ *–˜L *VЀ@Tl¦AQ°ÄdQ°‚$¢`3-ˆ ì™æ_üLLL …ˆ w˜çMÕ€' Hˆ ˆ ˆ ˆ ˆ ˆ ˆ ˆ ˆ ˆ ˆ ˆ ˆ @T@T@TðŸý?}È]à†{›IEND®B`‚PyMeasure-0.9.0/docs/tutorial/procedure.rst0000664000175000017500000004032714010032244021177 0ustar colincolin00000000000000#################### Making a measurement #################### .. role:: python(code) :language: python This tutorial will walk you through using PyMeasure to acquire a current-voltage (IV) characteristic using a Keithley 2400. Even if you don't have access to this instrument, this tutorial will explain the method for making measurements with PyMeasure. First we describe using a simple script to make the measurement. From there, we show how :mod:`Procedure ` objects greatly simplify the workflow, which leads to making the measurement with a graphical interface. Using scripts ============= Scripts are a quick way to get up and running with a measurement in PyMeasure. For our IV characteristic measurement, we perform the following steps: 1) Import the necessary packages 2) Set the input parameters to define the measurement 3) Connect to the Keithley 2400 4) Set up the instrument for the IV characteristic 5) Allocate arrays to store the resulting measurements 6) Loop through the current points, measure the voltage, and record 7) Save the final data to a CSV file 8) Shutdown the instrument These steps are expressed in code as follows. :: # Import necessary packages from pymeasure.instruments.keithley import Keithley2400 import numpy as np import pandas as pd from time import sleep # Set the input parameters data_points = 50 averages = 50 max_current = 0.01 min_current = -max_current # Connect and configure the instrument sourcemeter = Keithley2400("GPIB::4") sourcemeter.reset() sourcemeter.use_front_terminals() sourcemeter.measure_voltage() sourcemeter.config_current_source() sleep(0.1) # wait here to give the instrument time to react sourcemeter.set_buffer(averages) # Allocate arrays to store the measurement results currents = np.linspace(min_current, max_current, num=data_points) voltages = np.zeros_like(currents) voltage_stds = np.zeros_like(currents) # Loop through each current point, measure and record the voltage for i in range(data_points): sourcemeter.current = currents[i] sourcemeter.reset_buffer() sleep(0.1) sourcemeter.start_buffer() sourcemeter.wait_for_buffer() # Record the average and standard deviation voltages[i] = sourcemeter.means voltage_stds[i] = sourcemeter.standard_devs # Save the data columns in a CSV file data = pd.DataFrame({ 'Current (A)': currents, 'Voltage (V)': voltages, 'Voltage Std (V)': voltage_stds, }) data.to_csv('example.csv') sourcemeter.shutdown() Running this example script will execute the measurement and save the data to a CSV file. While this may be sufficient for very basic measurements, this example illustrates a number of issues that PyMeasure solves. The issues with the script example include: * The progress of the measurement is not transparent * Input parameters are not associated with the data that is saved * Data is not plotted during the execution (nor at all in this case) * Data is only saved upon successful completion, which is otherwise lost * Canceling a running measurement causes the system to end in a undetermined state * Exceptions also end the system in an undetermined state The :class:`Procedure ` class allows us to solve all of these issues. The next section introduces the :class:`Procedure ` class and shows how to modify our script example to take advantage of these features. Using Procedures ================ The Procedure object bundles the sequence of steps in an experiment with the parameters required for its successful execution. This simple structure comes with huge benefits, since a number of convenient tools for making the measurement use this common interface. Let's start with a simple example of a procedure which loops over a certain number of iterations. We make the SimpleProcedure object as a sub-class of Procedure, since SimpleProcedure *is a* Procedure. :: from time import sleep from pymeasure.experiment import Procedure from pymeasure.experiment import IntegerParameter class SimpleProcedure(Procedure): # a Parameter that defines the number of loop iterations iterations = IntegerParameter('Loop Iterations') # a list defining the order and appearance of columns in our data file DATA_COLUMNS = ['Iteration'] def execute(self): """ Loops over each iteration and emits the current iteration, before waiting for 0.01 sec, and then checking if the procedure should stop """ for i in range(self.iterations): self.emit('results', {'Iteration': i}) sleep(0.01) if self.should_stop(): break At the top of the SimpleProcedure class we define the required Parameters. In this case, :python:`iterations` is a IntegerParameter that defines the number of loops to perform. Inside our Procedure class we reference the value in the iterations Parameter by the class variable where the Parameter is stored (:python:`self.iterations`). PyMeasure swaps out the Parameters with their values behind the scene, which makes accessing the values of parameters very convenient. We define the data columns that will be recorded in a list stored in :python:`DATA_COLUMNS`. This sets the order by which columns are stored in the file. In this example, we will store the Iteration number for each loop iteration. The :python:`execute` methods defines the main body of the procedure. Our example method consists of a loop over the number of iterations, in which we emit the data to be recorded (the Iteration number). The data is broadcast to any number of listeners by using the :code:`emit` method, which takes a topic as the first argument. Data with the :python:`'results'` topic and the proper data columns will be recorded to a file. The sleep function in our example provides two very useful features. The first is to delay the execution of the next lines of code by the time argument in units of seconds. The seconds is that during this delay time, the CPU is free to perform other code. Successful measurements often require the intelligent use of sleep to deal with instrument delays and ensure that the CPU is not hogged by a single script. After our delay, we check to see if the Procedure should stop by calling :python:`self.should_stop()`. By checking this flag, the Procedure will react to a user canceling the procedure execution. This covers the basic requirements of a Procedure object. Now let's construct our SimpleProcedure object with 100 iterations. :: procedure = SimpleProcedure() procedure.iterations = 100 Next we will show how to run the procedure. Running Procedures ~~~~~~~~~~~~~~~~~~ A Procedure is run by a Worker object. The Worker executes the Procedure in a separate Python thread, which allows other code to execute in parallel to the procedure (e.g. a graphical user interface). In addition to performing the measurement, the Worker spawns a Recorder object, which listens for the :python:`'results'` topic in data emitted by the Procedure, and writes those lines to a data file. The Results object provides a convenient abstraction to keep track of where the data should be stored, the data in an accessible form, and the Procedure that pertains to those results. We first construct a Results object for our Procedure. :: from pymeasure.experiment import Results data_filename = 'example.csv' results = Results(procedure, data_filename) Constructing the Results object for our Procedure creates the file using the :python:`data_filename`, and stores the Parameters for the Procedure. This allows the Procedure and Results objects to be reconstructed later simply by loading the file using :python:`Results.load(data_filename)`. The Parameters in the file are easily readable. We now construct a Worker with the Results object, since it contains our Procedure. :: from pymeasure.experiment import Worker worker = Worker(results) The Worker publishes data and other run-time information through specific queues, but can also publish this information over the local network on a specific TCP port (using the optional :python:`port` argument. Using TCP communication allows great flexibility for sharing information with Listener objects. Queues are used as the standard communication method because they preserve the data order, which is of critical importance to storing data accurately and reacting to the measurement status in order. Now we are ready to start the worker. :: worker.start() This method starts the worker in a separate Python thread, which allows us to perform other tasks while it is running. When writing a script that should block (wait for the Worker to finish), we need to join the Worker back into the main thread. :: worker.join(timeout=3600) # wait at most 1 hr (3600 sec) Let's put all the pieces together. Our SimpleProcedure can be run in a script by the following. :: from time import sleep from pymeasure.experiment import Procedure, Results, Worker from pymeasure.experiment import IntegerParameter class SimpleProcedure(Procedure): # a Parameter that defines the number of loop iterations iterations = IntegerParameter('Loop Iterations') # a list defining the order and appearance of columns in our data file DATA_COLUMNS = ['Iteration'] def execute(self): """ Loops over each iteration and emits the current iteration, before waiting for 0.01 sec, and then checking if the procedure should stop """ for i in range(self.iterations): self.emit('results', {'Iteration': i}) sleep(0.01) if self.should_stop(): break if __name__ == "__main__": procedure = SimpleProcedure() procedure.iterations = 100 data_filename = 'example.csv' results = Results(procedure, data_filename) worker = Worker(results) worker.start() worker.join(timeout=3600) # wait at most 1 hr (3600 sec) Here we have included an if statement to only run the script if the __name__ is __main__. This precaution allows us to import the SimpleProcedure object without running the execution. Using Logs ~~~~~~~~~~ Logs keep track of important details in the execution of a procedure. We describe the use of the Python logging module with PyMeasure, which makes it easy to document the execution of a procedure and provides useful insight when diagnosing issues or bugs. Let's extend our SimpleProcedure with logging. :: import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from time import sleep from pymeasure.log import console_log from pymeasure.experiment import Procedure, Results, Worker from pymeasure.experiment import IntegerParameter class SimpleProcedure(Procedure): iterations = IntegerParameter('Loop Iterations') DATA_COLUMNS = ['Iteration'] def execute(self): log.info("Starting the loop of %d iterations" % self.iterations) for i in range(self.iterations): data = {'Iteration': i} self.emit('results', data) log.debug("Emitting results: %s" % data) sleep(0.01) if self.should_stop(): log.warning("Caught the stop flag in the procedure") break if __name__ == "__main__": console_log(log) log.info("Constructing a SimpleProcedure") procedure = SimpleProcedure() procedure.iterations = 100 data_filename = 'example.csv' log.info("Constructing the Results with a data file: %s" % data_filename) results = Results(procedure, data_filename) log.info("Constructing the Worker") worker = Worker(results) worker.start() log.info("Started the Worker") log.info("Joining with the worker in at most 1 hr") worker.join(timeout=3600) # wait at most 1 hr (3600 sec) log.info("Finished the measurement") First, we have imported the Python logging module and grabbed the logger using the :python:`__name__` argument. This gives us logging information specific to the current file. Conversely, we could use the :python:`''` argument to get all logs, including those of pymeasure. We use the :python:`console_log` function to conveniently output the log to the console. Further details on how to use the logger are addressed in the Python logging documentation. Modifying our script ~~~~~~~~~~~~~~~~~~~~ Now that you have a background on how to use the different features of the Procedure class, and how they are run, we will revisit our IV characteristic measurement using Procedures. Below we present the modified version of our example script, now as a IVProcedure class. :: # Import necessary packages from pymeasure.instruments.keithley import Keithley2400 from pymeasure.experiment import Procedure from pymeasure.experiment import IntegerParameter, FloatParameter from time import sleep class IVProcedure(Procedure): data_points = IntegerParameter('Data points', default=50) averages = IntegerParameter('Averages', default=50) max_current = FloatParameter('Maximum Current', units='A', default=0.01) min_current = FloatParameter('Minimum Current', units='A', default=-0.01) DATA_COLUMNS = ['Current (A)', 'Voltage (V)', 'Voltage Std (V)'] def startup(self): log.info("Connecting and configuring the instrument") self.sourcemeter = Keithley2400("GPIB::4") self.sourcemeter.reset() self.sourcemeter.use_front_terminals() self.sourcemeter.measure_voltage() self.sourcemeter.config_current_source() sleep(0.1) # wait here to give the instrument time to react self.sourcemeter.set_buffer(averages) def execute(self): currents = np.linspace( self.min_current, self.max_current, num=self.data_points ) # Loop through each current point, measure and record the voltage for current in currents: log.info("Setting the current to %g A" % current) self.sourcemeter.current = current self.sourcemeter.reset_buffer() sleep(0.1) self.sourcemeter.start_buffer() log.info("Waiting for the buffer to fill with measurements") self.sourcemeter.wait_for_buffer() self.emit('results', { 'Current (A)': current, 'Voltage (V)': self.sourcemeter.means, 'Voltage Std (V)': self.sourcemeter.standard_devs }) sleep(0.01) if self.should_stop(): log.info("User aborted the procedure") break def shutdown(self): self.sourcemeter.shutdown() log.info("Finished measuring") if __name__ == "__main__": console_log(log) log.info("Constructing an IVProcedure") procedure = IVProcedure() procedure.data_points = 100 procedure.averages = 50 procedure.max_current = -0.01 procedure.min_current = 0.01 data_filename = 'example.csv' log.info("Constructing the Results with a data file: %s" % data_filename) results = Results(procedure, data_filename) log.info("Constructing the Worker") worker = Worker(results) worker.start() log.info("Started the Worker") log.info("Joining with the worker in at most 1 hr") worker.join(timeout=3600) # wait at most 1 hr (3600 sec) log.info("Finished the measurement") At this point, you are familiar with how to construct a Procedure sub-class. The next section shows how to put these procedures to work in a graphical environment, where will have live-plotting of the data and the ability to easily queue up a number of experiments in sequence. All of these features come from using the Procedure object. PyMeasure-0.9.0/docs/tutorial/connecting.rst0000664000175000017500000001003114010032244021323 0ustar colincolin00000000000000########################### Connecting to an instrument ########################### .. role:: python(code) :language: python After following the :doc:`Quick Start <../quick_start>` section, you now have a working installation of PyMeasure. This section describes connecting to an instrument, using a Keithley 2400 SourceMeter as an example. To follow the tutorial, open a command prompt, IPython terminal, or Jupyter notebook. First import the instrument of interest. :: from pymeasure.instruments.keithley import Keithley2400 Then construct an object by passing the GPIB address. For this example we connect to the instrument over GPIB (using VISA) with an address of 4. See the :ref:`adapters ` section below for more details. :: sourcemeter = Keithley2400("GPIB::4") For instruments with standard SCPI commands, an :code:`id` property will return the results of a :code:`*IDN?` SCPI command, identifying the instrument. :: sourcemeter.id This is equivalent to manually calling the SCPI command. :: sourcemeter.ask("*IDN?") Here the :code:`ask` method writes the SCPI command, reads the result, and returns that result. This is further equivalent to calling the methods below. :: sourcemeter.write("*IDN?") sourcemeter.read() This example illustrates that the top-level methods like :code:`id` are really composed of many lower-level methods. Both can be called depending on the operation that is desired. PyMeasure hides the complexity of these lower-level operations, so you can focus on the bigger picture. .. _adapters: Using adapters ============== PyMeasure supports a number of adapters, which are responsible for communicating with the underlying hardware. In the example above, we passed the string "GPIB::4" when constructing the instrument. By default this constructs a VISAAdapter class to connect to the instrument using VISA. Instead of passing a string, we could equally pass an adapter object. :: from pymeasure.adapters import VISAAdapter adapter = VISAAdapter("GPIB::4") sourcemeter = Keithely2400(adapter) To instead use a Prologix GPIB device connected on :code:`/dev/ttyUSB0` (proper permissions are needed in Linux, see :class:`PrologixAdapter `), the adapter is constructed in a similar way. Unlike the VISA adapter which is specific to each instrument, the Prologix adapter can be shared by many instruments. Therefore, they are addressed separately based on the GPIB address number when passing the adapter into the instrument construction. :: from pymeasure.adapters import PrologixAdapter adapter = PrologixAdapter('/dev/ttyUSB0') sourcemeter = Keithley2400(adapter.gpib(4)) For instruments using serial communication that have particular settings that need to be matched, a custom :class:`Adapter ` sub-class can be made. For example, the LakeShore 425 Gaussmeter connects via USB, but uses particular serial communication settings. Therefore, a :class:`LakeShoreUSBAdapter ` class enables these requirements in the background. :: from pymeasure.instruments.lakeshore import LakeShore425 gaussmeter = LakeShore425('/dev/lakeshore425') Behind the scenes the :code:`/dev/lakeshore425` port is passed to the :class:`LakeShoreUSBAdapter `. Some equipment may require the vxi-11 protocol for communication. An example would be a Agilent E5810B ethernet to GPIB bridge. To use this type equipment the python-vxi11 library has to be installed which is part of the extras package requirements. :: from pymeasure.adapters import VXI11Adapter from pymeasure.instruments import Instrument adapter = VXI11Adapter("TCPIP::192.168.0.100::inst0::INSTR") instr = Instrument(adapter, "my_instrument") The above examples illustrate different methods for communicating with instruments, using adapters to keep instrument code independent from the communication protocols. Next we present the methods for setting up measurements. PyMeasure-0.9.0/docs/tutorial/pymeasure-sequencer.png0000664000175000017500000002622513640137324023203 0ustar colincolin00000000000000‰PNG  IHDRo˜ù €sRGB®ÎégAMA± üa pHYsÃÃÇo¨d,*IDATx^í1oäȹ®ûÏŒŽý 'ä (é`àÂÁŠ,L$œ`/Np€;ÀBÉ ,o(`€Å&XX§ØÙD›M`à„Ž؆yYŪâWÅ*v±Evó£žxvšÅb‘ê"Ÿ®.­È]C!D]7!„( ò&„…AÞ„¢0»¿þõ¯ èb÷ÓO?5°n²òþå—_`¥ o… o… o… o… o…ÔÉûóûæÝn×ì,×ÍG¹îµ˜¶ß½o>çÊÃ>Øï1”ŽàÄÔ¼—ט¼eùÇëfwnq"oX ÇÉÛ½~ÝŒ¯?úòëæú]®\H/,nÞ‹ºïÞÎïÏò±¹v£ïÏïß…mv»wÍûÏ}ýî˜Õëñúc öXý¾ÓírÇš«wè8¢Ÿ à8Ž—w+¢ ;?*¶å^v®ÜLn›k+'´¤Ü 0WÏìãúãð˜Fë¹c´Ç'^Ûöû »]ñX õÒã°Ë}½×b€#yÕÈ;ˆTŠK–[Áµr|,ÔO_KL¹­ÒíÓu¹vÕ˽N·±8á¦õsõd‹yD3°ny—ÊåèÞ×Kë×Ô˽–e)¥mKu"8ÌDzòöS¦\H+šIÖÒ¶ å¡­´~M½ìëN²ÙéS'k¡^²_³|æòͼ¹ø@8’:y[iL øå¨^<Ê”¿´{w}mÿÑýâsü–=¾~ÔV¦þÁzc¯ÝvÑnt¬¹z²‹q‹íÂ:€ã¨y×0,òPȼò€“P”7¬›¼ÓX?È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!È@!³Éûòò¾ù’)Ÿ•/÷Íån×ì,·Í“/›sß²½¹Û˜ ]ò6¤B]RÞ+eYy¦#åR¹•æms{éËwÍí“«/‰äú¥¹õ/ï¿$m_6÷_úmîoMY·¿/÷—a»P/mï7¿é÷U×ûvɧTN!z£RÞ_®: y?ïwÍÞ™Ù®w ¥rBѽ#ï¯ÍU·]‹QuXW*'„ÝÙ¦¼Ãr©œBtyBˆÂlSÞL›B6žÈ»Õ7¿°$„¼¡¨”wø¿MÍ(;÷¿–Ê !Doô޼ !ä yBˆÂ oBQäM! £FÞ“óåÌ&o8È@!È@!È@!È@!È@!È@!È@!ç“÷—ûæòò¾ù’[£Ì&ïËpK׎ۧ|½À«åý¥¹¿Œ÷é¹üÍoø`€M3Ÿ¼¥,˜[‰Ž |Α7£xxc,#oƒª“y72¾lî¿Äë¿Ü_Š‘³[ïFÖýÀSsÖ%¤òöËößÛæÖÐoŸúÑzh7wl¡¼°?€3³œ¼­lo›§ð¯+ÄšnÓòtÛìnŸÆ_§¤mÉ}x›íåk[¿plþ5ò€•²¼¼­ýÈÖãÊ#QŠõ¡­^®O·#Ó0²-¹œî#}mþ•ûµ™¬”å§M¤4KëÓé Q¿“v2BNI÷!Ûöå¹×évJXHÞF¶~¤,_ µóß²-³¾ÝþòþK¼½$•p®íìë±ù:L›ÀJ™OÞvÊÁ“HÏŠP¬OÄjF×~Ýåím/Y‹ù%ã‰J1ËeY>ö:=¶P޼`Ì&ïÅ¿\̬x£¬^Þ£¿¨x£¬ä 7€B7€B7€B7€B7€Bf“÷‡ !çË9˜UÞ„Bú oBQäM! ƒ¼ !Da7!„( ò&„…Q(ïçfo— ùúÐ\‰{i_=|u+!DÔÉûy¿köÎØ_®š_Hó¼õ:á_5ø›²•(“·‘°m›ÑõÕCsØÉ_›‡«¡¼­üý輪BYGtË{°\HNòvZ¥ßöy_Ñ!„¬$o@Þ¦NnÊÄ”˜7'„•F·¼N›”Ä-ƒÄ !ú¢LÞ­jK¿°L¦Aºåq·ë÷ae~NœBÖuòîGÊÉh9‘wôËHGü¿ Êvø_ !º¢PÞ„B7!„( ò&„…AÞ„¢0jä 19_ÎÁlò€Ó¼‚¼‚¼‚¼‚¼‚¼‚¼²~y¹o./ï›/¹ukAÃ1À¦˜MÞ—âÖ­»Ýesÿ%_o2s‹Ñ´'ŽõòþK¾Þ7œ˜ùä-åeyÛ<‰õG3«ŸšÛ胥]¾}ëyÀ‰YFÞV’½¼¿Ü_GåNx÷·ý(øöÉmoåß—ï|ÛQ¹kß¶sÛÜ^vå·O_šûðÚµˆ+"j[¾ª¼%:Æ¿ydXFÞO·½ÌRÌ:3Úuò ‚ ÛÁ&"ϔۄЎ“¥iC¾ÎÓÿ°ˆF܉ԣ}–ÊsÇè^#oX˜eæ¼SiZ¡%ë¥ð|©åF¬²¼ô:‡}{,âƒ$ŸÅµ]*Oel_33ÿÈÛ /­¦Ó ¦nI€SÊ_#o‹Y—êÖ–Wí `>–›6ñOÄf§;ÌrQ€F¨½ìCýdª"š6ñí”^{L™œ.‰ö)¦Acå¹cl—M›L›ÀÂ,ô K'4'±0ÏÜry{ÛÕMå*—ý”†¬ïë¸òì‡Céu ÿefGò ”·d÷)ÊGyÀ²Ì&o8È@!È@!È@!È@!È@!È@!È@!³ÉûÇóåÌ*oB!}7!„( ò&„…AÞ„¢0È›ByBˆÂ(–÷s³¿zh¾º¥æëCs%î}õà×´õDùnÿìÊû<ïÛòÐVRwÕ„¦!d%Q)ï¯WX¥¼Ÿ÷Mïe#`'ݨükópËØˆûj¿o®"yïÛÿBÈz£wämFÚRÞQ†’î‹Ù|ØzÔò&„¬?Û”÷`‘¹™Ɉ»[HäÝO›ôÓ/„²žlPÞbÊd¿ÎË].Õ_€µÉÛŽœÝÔF4ÅQ+éR¹_‡¼ÎÊê~ayóؾ6Ò#ä¾®¨¶;¢þ¬KÞݼt4Õá§Nñ†é‘±i“œ¼íü·([ä Pælò6¤ÿ« ”¬\wqsÓ ­¨ûmüÀÔúò8æ`UòÎŽŒÝÔÉñй¬g¥ì×99gÛú“ý%hÍ'ëæy”9«¼'œÞXŠu¼·ò(³y·<ÞøÑñrÿ;ß’ ïùAÞeV#oí ïùAÞe÷L ïùAÞe÷L ïùAÞe&ɯɽoc9Á8¹÷ &ÈÖòPòPòPòPòPòPòPòPòPòPòPòPòPòPòPòPòPòPòPòPÈî§Ÿ~j`ÝdåýË/¿ÀJAÞ AÞ AÞ AÞ AÞ ©—÷ç÷ͻݮÙ9Þ½ÿ<¬'¡RÞ›ëÝ»æýg±|ýQ¬€S2AÞ×ÍǨÌÈÁ‹‘úîÝû泩kþ•Ûúå\;nýûë¾ënÛ¤}[~°ÂÏ ŒJyÿÒ|ôF܉ԃŒ?7ïß Ñúò°>­_hÇüëÅlÊ?^wiû–Ê66@µ¼F ^âNŒ~ôÛa:"R+ßLy®±ú²Ü¯«i`L—·Åɹ(Æ#äkgJýÚ66@¼åtI¢‘tnJbdÚDHýóûwn¤ÐN*Þ°\š6©h`T޼;YöÓâ“VÈbe¡Ü Û•½»¾îÅš«ŸŠW.'õ­´kÚØ•ò~%`V7€BN#o˜•¢¼`Ý äÀúAÞ AÞ AÞ AÞ AÞ AÞ AÞ AÞ AÞ AÞ ©’÷üŸÿ ‘óî!ªåM–Ë_þò÷Šh }Hމ9o·âpáë}HŽ òV.|ý¡É1AÞÊÃ…¯?ô!9&È[y¸ðõ‡>$Çy+¾þÐ‡ä˜ oåáÂ×ú“3Éû¹Ùﮚ‡¯nqö,Ýþz’¿ðÍÏ¿kv·ñ^hMQÞÏ{чû¶W 郼•§,ïþçÿúpÕ쮚õ¼o§j’íïÍ•v»ü€½‰òVžy¯ïý@Þ2Ù>4£îU}à’µe}ò¶#ùUñkópµkörÔ!OìA}[yÈûyï߯–ðÆvuöûv”îÞDZzÏýûnVÙѽ[¾’oü Ìö~¹Å·;Òò˜¶˜r&ï¥Oö½j#ʯº~²›»>+œcïýÃC?uKw]úuáô(™=+“wRÞJÚž0FÖáì0‹þd)Ô/¶¿½ÔÈ»}úÊùío 0ä­<æâÿöí(ÅË›)AÞòÖ ò&Çyo È[7È›ä½ oÝ orL÷‚¼uƒ¼É19«¼ÿõ¯¹WqJåÅDwS{{<”÷cs#ïìÖrq÷’ÔÉa¶»hî^rëêy¼‰÷ÃË]s1Ã>¶„y‡{Š›T\sò1z²Nw»Z¿®òþßvSêÎwl¦î½r6yÿþ÷¿o~÷»ß5ÿøÇ?\I³lÊŸžž\É¡˜{8øN}›÷sÈË[ ÒÉüæQÔÉ1¼{æno›¨·¹ÕkxØCÅ5×ÖïŸv×écX+hsÛy/qlæÖ畳ÈûŸÿü§ô¯~õ«Hà^ܾÜÔ;ó)+ÞmóɹÆOÉ%sXÞ-vÄ{Óòv"¶§¸y]Þ°^¸Ì5Õ­ó«ÌõS|¸t»]·/SÞ‹sê5Wž3Nö7šøJYîØêöÊœUÞ&éTÉq› ï)òveÑÖa…<d_G Wµ8ê6äŽC–¥ëG–‹Çìë®sœ¹rIVÞV¸þg-ÈÕ¤ªžXŒ “u¡-‡½–bqÕ_sÝH6/Ç.ey¦‰¡”åŽ-þö±†œ]Þ&FØøÃìTÊä$3è¼7*yË9où:"•¥¯#Äß.›i 3êõÿÆmH2Ç•¥ëG–‹Ç¬Ÿ¼­D½¨¤4)×Ö“Ëåd"Ϊk®NvFY·’C)‹[åþO˜UÈûU‰N8q"¾¡–·Y–ÓÝòP¼b;3êö£j+NÑžY¾¸iGÝr9ÒãHËÒõcËæuíÿò¨‹¼€jžÜ_[/Z6¯Ç¦Mr£ÑDvÅkN¼N$šMṫbÚ®çRÇf–™ó^ ¦3Ú“Îp¨O¶˜¼¼å×ßT -VÈ¢Î`Ú¤›#·ë2¢¶¿Ì<8m‘Ê8-K×Xγ¯«—¼­,ÝϸÄþÅõ2þ ËwAe§#\þšë÷Ó}(ˆvZºíM¾,lkÚ•£ÙNÈ{¬þÜÇfbÚŒ ΟmÈûg(ïåÿE%La(ï&©Ždá‘gýôI—©õ_—ºi–Syo '—·6ûE%Láœò6ñö±#ÖEŒ™ŒªfjýמG+u› ï ätòöS)éT¼†ÓÊ[LµXN'A2o÷rò‘7ÌÊ9GÞDo÷‚¼uƒ¼É1AÞˆ¹øA7æB˜ òVžœ @¹ àÈ[yÌÅŸû::ðò&dJ÷‚¼uƒ¼É1AÞòÖ ò&Çyo È[7È›ä½ oÝ orL6%ïÙhlbon£ã¯Ï†ò67t’E7÷}HÒH-@t/ñ¹n{‚ã>‚¡¼ý ’’ûiqNÚ?i¯ùówq3§pªPÆ_a®1›‘÷|4v>\ýÀÓó'/o!©è‰4s°°íÝÅñ¶Ëw³|øh’·<÷Ž='݇ÀAy›zþƒB¾6I…¬%›÷¬4ÑsÒ”÷ìÒZX‚ò^â³ry÷߆äê–·Ï´sÒŒº÷wLî¸ö‡.›yÏõ@ã>’w"ÃჅÅ6wýtEôàq/í‹»»¸ýä>ÛñCÚzµ˜í ë¢}Å£óCåƒã^ÿóIrõ ‹ÈÛÙ¸êV®ò–§é>÷Z³y›È‘ö±4î£]ÞBŇHÉ»m|Ýhª¥[ç¥ÜÉ7Þ.ÛÊ2iÓp¸yì åÑéœþaƒ/ßvû®­±òÒq/Oxÿ[rë=óË[ȸö>ܦž;Öøî§È{­Ù”¼MÒ©’ãÄm¢]ÞNRV¦‰ D;¤h¥Ø’6¢iŒñöû5Œ´™]ÎàFζ=÷º?öóa3V^:n¹1Ç’+—Ì-o3]~éX#ï¤Ó&:²9y›aý@ãÈ»]ŽÉV>XxD¬cŒÚë˜UÞ-vÄ$©•ŸYÞ5Ì+oS'ù 3Œ<’½IôÈ/ä½ÖlRÞód;òŽ–Í¨Û ÍJÏ׫y]š~ˆ×Å"k3·ìx¼eÝô‰œ Dñòüq¯‡¹GÞQjFÞFÖÉÈ»—9ò^kw1[’·—•véÁÂé6ɲ˜jÿ…åH—e¹o¯EÎÙGûëJåcǽŽ–w"Ýl¤¼GêÛÿÜ¿wѤ7ò^k÷2”7h¢^Þqâ¹éÙZ¿ ò^k÷‚¼u“—·'aeªT0a¹ê ï yëf(oByo È[7È›ä½ oÝ orL÷b.~馂¼•''ÐEîÂ8òVsñ美ƒ¼¼ ™ä½ oÝ orL÷‚¼uƒ¼É1AÞòÖ ò&Çyo È[7È›ä}‚˜{Jø›þD·Þœ)Cy—nø´4f¿™[³.AtGDÝ,-o{Ó){Só'òâ! £~¿ŽôÇîbÿd¿îÏõ£›mÜÆ='ÔÕ÷»Œï°8\>g÷Òi/’‡pÖˆ‹gƬSÞsù~¦åYTÞÑ-a—9ÿKtìÓÂ<íF\Ïͳ¯k?Øü>ä#âLÒåóyŸ4Ët<ò^ýhoH®þ’òŽ´ KÞñ±ûÔß`ëyìÏšìÃŒöÓÑý§ÂbAÞ'Mý‰7%“äÜ÷:žI'{üòçK¿”¾œ6È{ñ,ßѧ—w7Ý0]Þqûý6¥z\}ìé²¶eîç9 yyuPÞÝ@a6y»øQißnaŸÑ±‹sTÞ¹ŸÕìcJùx¢—ŠÄõÓ}w sy/s±M9kŽHµ¼m¹T$½ÃëÂuTÄr›Tš¹QnRÏŒº_ù€äá±—¶ë1_r>Æ@ÞV¥)# )¤nIž6¦®_.½6I—»ô¿@Ù§9ß³Ó)àó?S®<=vS¢8£ã(äÙ¼«>ÉÏo¶gÎûí%Ì£ †'êë’—w¼ÏX¼¾<‘Xq]'¼î—ŠÝúü(ÕÔë¥nF¿¶¾ŸWŽÚ÷å©LKHNÛK¶;pìÙåhz&÷at†ònc…ã­eÿ_‘üìyeÒ‰f¿ïÏ39Vè…k"¥Tzm"–£)ùQÚg'i[~µoöƒß–Øfð³v+âc÷1myË‹(…}•ê'Ç ßÃÁ,]>S÷2”÷ܤ„9ÉÊ»:B´¹,2J<°Ï¹Rq쯛©Iú  ôàôAÞòÖÍ¢òncG鳎O$ï6ãÇnŽcÊÊÔúìåþÓåsyo È[7KË{þœcŸ$ òÞ@–—7,ÉëäMÞj÷‚¼uƒ¼É1AÞˆ¹øA7æB˜Ê›—÷ŸÿG79€.r&À!wFˆš0îë8èÀË›)AÞmrBÔòÖ ò&Çy·É QÈ[7È›äÝ&'DM oÝ orLw›œ5¼uƒ¼É1AÞmrBüóÿ37äÙ7ÿ™[·2†ò6ÙßdÇ0ïíNOð—‹Ü4j)ªEÞƒ?U·7­JþÜÜ–uýV}¶)í,ؾùÓ÷ê6WäÝ&–á׿ÿ[Û©ÿkßü»jy IYÎy×¼…%ÝÒµ[¾›åÃyè&Qî΃‡Ë{‡Ôü ýÔv–n=7ª òn“âŸÿçy;òž]Z KPÞÓ{VN#o?¢Ë‘«¯AÞÕ·g#óü6¹T¶³tû&ɺ5y·É qSòNdî‹mÏptÛdÔÛ"î—}qw·ŸÜ§»Ÿ¢qmŠ›uþÉ5][¹ ˜í ë¢}Å£óCåƒã^ÿóIrõ 9yÛ»×ùmƒLºQbwíNFcõòn#î]}õðÐÕõ«£ûZ{á•F¤±2­a];K·o³È-t— òn“¢~yû °%:EJÞmãëFS-Ý:/åN¾ñvAØV–I›þƒÃÍc)Nçôe|xù¶Ûwm•—Ž{yÂûß’[ïy;[›˜×¥yÙa½ $#§ ¯n]ï*óA ·’n·ëöeÊ{ùõ‰Ë7!ï¤îšƒ¼Û䄸™‘·•i"H'Ñ)Z)¶¤hc¼ýúçG¦ËÜÈÙ¶ç^K)Ú›±òÒqË},ˆ9–\¹$+o#”ðóäjRUO,F–ɺЖÃJ­$´¸|òÖ3ï¼Û䄸¥i“èùŒ‘l'<à÷\òn±#æ éÌH}¬üÌò®a o+Q/)!Z“Úzrù ¼{Éõ‰å×')Od:d1•í,ݾMég]_w›œ7û K3êöB³ÒóõÆÄj^—¦âu±HÇÚÌ-;ïDY7}"§AÂQ`¼<Üëa o#/X+Ô‚”këEËæõØ´InZ¦nλ;¿œÃX*ÛYº}³Ž9ïä½<‡åíÄe¥]zÀoºM²,¦ZÆa9ÒÆÁeYîÛk‘söѾĺRùØq¯„¼­,ÝÏ=Ä7Vm½dÙHß½'ã¿°lq–Ïr)šˆ¶ÃàV~ÈdSÙŽÉ)Ú ÖäÝ&'DM å šÊû„‰F¡#yňÔü1Kúp¾öKß0ÖäÝ&'DM oÝœSÞö5¬”²b™lĘ́wÖÌ×¾}/”ŒºMw›œ5¼usZy‹©Ë’b%Ky·É QÈ[7çy½AÞmrBÔòÖ ò&Çy·É Qæâݘ `*o^Þ„¢1È›ByBˆÂ oBQäM! ƒ¼ !Da7!„( ò&„…AÞ„¢0È›ByBˆÂ oBQäM! ƒ¼ !Da7!„( ò&„…AÞ„¢0È›ByBˆÂ oBQäM! ƒ¼ !Da7!„( ò&„…AÞ„¢0È›ByBˆÂ oBQäM! ƒ¼ !Da7!„( ò&„…AÞ„¢0È›ByBˆÂ oBQäM! ƒ¼ !Da7!„( ò&„…AÞ„¢0È›ByBˆÂ oBQäM! ƒ¼ !Da7!„( ò&„…AÞ„¢0È›ByBˆÂ oBQäM! ƒ¼ !Da7!„( ò&„…AÞ„¢0È›ByBˆÂ oBQäM! ƒ¼ !Da7!„( ò&„…AÞ„¢0È›ByBˆÂ oBQäM! ƒ¼ !Da7!„( ò&„…AÞ„¢0È›ByBˆÂ oBQäM! ƒ¼ !Da7!„( ò&„…AÞ„¢0È›ByBˆÂ oBQäM! ƒ¼ !Da7!„( ò&„…Y\Þ° 9ï¢JÞ°.7€B7€B7€B7€B7€B7€BªåýéÓ'PF®%¹m`ÝäúQ’ÛÖM®k˜$ïoß¾jN úTôéö¨éÓÈ{£p¡oút{Ôôi ä½Q¸Ð·}º=jú´òÞ(\èÛƒ>Ý5}Zyo.ôíAŸnš>-¡GÞ/wÍÅÅ]óR[þÆÙÜ…Nÿo¯O¡ªOK¬DÞ/ÍÝÅ®Ù]„§¾xkö·bqèºÐgî%}4•³ö©ywm.š»—L½s£¬ïkú´Ä:ämß䛿æbä„(uÄRTÓîŠO޳^èSY²ÿWÜGS9kŸ¦ï£YÞÝ4²ÎÚPÐ÷5}Zbò~¹»h.î^šÇ›ý7¬³'ˆø´÷Q*—Ûµewm{;‚EÛˆ“.[îF‚®|pLv™:Q[ND¶¾SW~óØowóèÚ\€³^èYªÿ×ÞGS9kŸ†÷Ô—=67ÅëhÂûm'¯UѶÜWn?¡ž[¯¨ïkú´Ä ämÞ,÷F>Þˆ ÑtXÒÉv]©Ü-û²èÍ·1²ØÝ<Ž”»6ÒvÓò¨Ž8Áä:{,â瓯síÏÄY/ôI,Ôÿ¾,Z¿®>šÊYû4}Ÿ}uÌû÷¥¼þÌyècS?\¯™ýøå´,Z¿Î¾¯éÓç—÷à vo^T.–Kå~9W–[6Y*Ïm#ëøòôu{"úOúŽd¥× pÖ } Ñû°@ÿ§¯WÔGS9kŸ¦ï|_Ž}_eYhÇ]AØFäâœÈíÇoïÛ(í3·íX}yl QÓ§%Î.oûi›¼©á+Ž|óür©Ü/çÊrËiÇÉòÜ6²Ž//½–L­?g½Ð'°xÿ—^K¦Ö?gíÓô}‘Ò,½O¥÷Ò¿N·3Ë¡]ó̼n?Ðsm”Èí'}-™Zfjú´Ä™åÝÍ/…¯G†è+•ûÄmËíE>Z.Ú¼ñf›Ü׳R¹kCž ž´ƒC¸­býÜë8ë…^Í úÅ}4•³öiú^Ø)†×¾¯ñvÑõç–onnÄïA û‘¤ûYyß×ôi‰óÊ;û™7Ù]œöq£1Ó‰¾n©Ü“k×”¹m¢¯Z¥ò3ïf÷NW_´Õ‰Új1õdýÒë8ë…^Kö=˜¿ÿ×ÚGS9kŸfÞ +[ßWǾ¯Ñvñõ×­Ë•%ûI׋²µ÷}MŸ–XÁ/,a Îz¡Ã"Чۣ¦OK ïÂ…¾=èÓíQÓ§%÷FáBßôéö¨éÓÈ{£p¡oút{Ôôi ä½Q¸Ð·}º=jú´Ä$yƒ.rý(Émë&×’Ü6°nrýXCµ¼`= o… o… o… o… o… o… o…TË;÷?—¬…Ü9+Ém°rçk “äûóN€sSspþÂAÞð¦AÞ ä oä ZAÞð¦AÞ ä oä ZAÞçàD(…à oÐ ò>Ä¢EÞ«a‹òîžÌÞ?éÜ>ýâÂÊ¥'Éwï4®«ãƒµó¾M-ƲÖÀ!õìSêû¤Ggíx™þ™”åzhé"ý_#Ž#"âxbøøLÁÁÁoÞ¼ñòòâ8Îп|ù²‹‹‹³³snA04ä}ÌÝýkÿ¸Ÿúˆ:úͳD˜ìýñ†j—ã n–ö‘©cþ˜ˆx""…¥½ƒ…$m÷ˤʖH¸¹(åÉCÌ?–­[·Q×®]¿tǰ°°ÀÀ@CŽ——cìÒ¥KÏŸ?þüyÆ mllr{ÌÀó<Þ~€$ÄÝó?x/žˆL‹7íÔÒ«…HHüðôf`à iºæ½1fHÛêVxú)ühÝ”­Ï‰ìëö\ÍB¯Ñ}Ü,wh>nh5KCõÍ4®ÿ>ûÇI$ól;¤}qñë£+7]‹%q¡f}-¯î9|'$*Qˈ“[,Q­QýröráÃ…UËO “âÕ‹%Þ¹ù2V”¿x}ß:ù8~ùI$³*Ñȯe‰Þ¸™²HOÕ½Û¯ã9‹Âµ|}k8¦ ú˜  ÇN_{š '™•{yï¦u‹["c ò°¯è.±±±)T¨Ð³gÏžâmŒ¶¤ÜøVuú m`Oo,X~1&þáÐ –)!Nô½Ó7ã‰ìêù6)mÊÅ[G¬ xÿáöÝðê¶ö ÏØ±cGÚ¥ƒ¶mÛfø¡sçΟ_ˆZ­®X±â›7o¢££‰ÈÊʪB… jµú[<ÖZÈ…qÆö´N˜ hµÉÑh„´A@rÌ@B–þ‘teK“›ÄtI‰j½^^¬yË¢Ïö<Ò"†V¥”z‘ ~wéà¶kCSŸU/0f`–*%ÚDÌÚ”(¤&2]b’$¿)Q éui•iU*•ØÔÕ†»Ãâ#ô–)Rä‹DD¡§Ö.9•r˜ªh¾] —ؾ}{¦wêÔé jö,ª´/ªêA¸pá‚!` ¢ÈÈÈ3gÎT­Zõ[gý fÈ…x3{K¢8bï…$v«3rnCݳ3ÖÞVGÄ/扈ô­ž1F$hÕ:""^̳ôAÄÇcÁ)¿èTññ’´_2L›¤MîÓ'Æ&¨¹Xù×®íJÔªZÔâýiÿëÑéãÆ–2›¥Ì²Î0ñ‚ zžˆˆO½«Pj„c]­Mý”µ›8±…oÈݾ¨–êӧᇵk×Qß¾} ¿ÆÇÇ~«ûÊ•+¯^½"¢bÅŠéõú   W¯^qW¥J•o6àþ ¹1fPºWp?×'þýÇü­U,”S§~3‰Ll­$¦e¯¯Þ u-o­ þûn$‘…£¹èë«ó˜»GŽ3bYÈ©ƒ× wó2‹zJDœ»O+ï"¢—/fõÅùYÏÿüö[FDV­R¿Dù\óÓÃ÷ªµ©[:¿˜HH|sûv¬ÖÅFŽKrƒ”ö}ŸßÜÿxã/Ú× ""âõë×DäááQ¹reŽãôzý³gÏ‚ƒƒ=<þLKdV©u ;jíuk하·güoyvéÐ8|ÏÉG‘ïÃtªVÑžú_D†/»tGú‘R~[[Ëâ^ $¶*Z×·u)!<Í&’B-†ô´=~êêƒà¨ˆ0"¹UÁR• ›!7 ò$èEllì—î¨ÓélmmœœT*ãòy*•ªZµjZ­V§Ó}ÓcæŠM ÈÎþ“ŠDÔ®][©TâíÈù:Z,•ˤ±ˆ#"b‚^§ÓjÔj`ø£L.—JD±N7Ó&ÄÄdö‡Ø4;éTq1)E«âR—ñK}Fu\Œ:M\’æ—Ì@HŠ‹§dÒìN$èÔ‰ñj¼å¹RÎÌgÀy€,¾#R¿,ð}€˜ #>_…^ã*àû1¾ò,§3ÀwÆÊÊ '1@–"## a‚Ä Y† )ÿb€t0¼€˜àS›€˜àŸÃBnb€Lax ïÅ ,âÄpoŸ¦†ÿ~ŒÍþmÞX̹aÍ»¯zªùøOªÛs›'?W#¿Á¿n¿þA÷oŸ¸Odr“rq•ÃYÕ½·ÀöÁ£ÿò^4¿ƒ»÷ͼPïÙCËJCœX·aÊxÑÊ•\$x?óVØ@Ÿ—›tãµféù¸«/“pÒ2¨ä*ïãeZÃM†S¹!f N,7UJyŽ—›˜(Ä8¦ýZûþ-õ‡w¾ÔXWê:ñ_¥öÉÊ^£nÕíäyûP@PŒ¼`ÝA7* zïÿsw¯Õ«:ºJˆTwgu›š8b²ãŠ÷uthë}D®}Ö­nåîXy¥£›‡§yó´ ¾5üì_ï}bO]tâIXœŽd6¥šÙ«Š5…ìïßÿD•¡õÞîÙyŤ߆AQËæøß ‰ÕØÒ³^ßQýê;IbǶ_kÛ³Aܽ7ÂÍKv™0²Ú«-³7ž{Ål8xÊðÚ6bbê×gV-Ùrüa¤NáTµý°Ñ-$Û§?¼Š»»[¶ï¯×‰"«b ûê]×Asf·)šÏn8ü¾êœå¾![æm:ù(Zç/T±õ¨ñ­ JqfÁÊÊê3g2œxœtò±ª¢‡uÝÒˆ3ŠSi܉ŒV ÍJ(p6à«}Ãù º¨3ÿgÖôç_ƶ±¹½iéáCþþÅÁóúÊÝ'ï_[8»pΟo2M+’¸ö˜;¨ˆÈªéÌÍûvîXÒÄ>ëà†ãEÝ6vðøSŠfÃÇ®œ°jÛ±ØKƯ .3påæMëGWØ9ã·{¶ÝÓžuÈþ©“O*ÛL]·sÜn¶ÿ›?óÏ×:"¯­Üò²€O·.µ¹€éËþ²ë?7ÉL¢Åéú‡S$Æ)€\3¤â¸ÌæD3¦gœXjhS3á Ëtë;oD% …¥­…”#Ò¿?‹½y…}AWW³”ßµï¢?·)o§ b‚À>^íUûáÁ [›VÕœMD†m>ë8%¶ž¶ÂÍGQ oçäy¤L`Ÿzx¼YÁBÊèG!œE+Qr<ĸôËø07w¯ÑʽF«ná#;n¸¬®’OŽ+ðSa}Fn’¥ âŠdšè˜èÛwo«5jœ/%“ÊÊ”.canSˆr"Ð%Å'j&$%$¨tfYo¨{}ñä%« ÷÷-¿ ©5½œ9/u+aurÛÁ’Í£®îßv>Q¨BÄÉò;›D];wí!—OgâVÒIñÉNy±­‡­ðûÁ½Ê²²×g·oz¤³*Ÿ±ñoéî ÚwìÀI§ª–®î^=‰ÿœY"Ûêm«n3mŽÕ¶•¹ˆÇº"i÷k»´‡çܦ©Ã€ÕÓ~£ =Í’^ß>}âyå±CLÒ”’xkÉä³.ÍWt3¿}+\äXϳv³öù¹I–R´ƒÿéIÔ/^½¨Qµ†L†EW¿˜Z­~òü‰V§µ¶²ÆÙÄ Ù "ÏŒo¿ä mÞéöØíS²ÞTøpeõ/›ÞkM Õ8§3ŽÈ­Ý˜vOçlŸ;IæZ¯m‡Ê¯wÉŠtêSçÞ²ÙC™UþÛ¬Š}~î{îæ¹¿ì´*Þ¬U‹B¯.dÜ„³ª9dàëL:lV¸^ûÖÅú¬×Æ[V;ØÚU»æÙ¡&©MѪ-ºYKd¦é¯ãÌÙâÕk·N=+™s…:¾® .Ù„þLì‹÷Ê8!ó'eÙ8îs÷aÄ¡ƒ1äFŸŸ›ô)¼´ò¨¥C‹‰ãC®íZ¼vø8ËkÛ9ÿ—doVÔÝý{wÃ]©õowt˜@Ô¿nU"ÛN›¶¶~¶2Ýî~iZ²Ú¿ÏØ\~Ü‹›;‰‰ˆJÿT©a›ÎOßÉDêG |ûÝé8¾ò_k6ÿí¶ho—«¦ýùè}¬–döåÚøuHM[z³Ã¯ÃþŸzU >à=ŒsªÙîäÅÅDÄ4ï/.´nÛpE‘–¿ÎQß.˳ ™´þ‘Àˆ“”ì:º“{òñò&Ž&$ŒKÙˆK×væÒíŸZ—îw–º¡Ø¡qßA±,x–n53öq+>óâ2:Æ¿01&d82Á¸‡À—þ%d<þÌ€}üWöUã1ˆ¾±l /dÖtz’äSŠˆô¡‡' ß(î2k×\—¤;[&ͱ®è®V7ÇξPhäò_ËÊÂ^ x¥ýd3Qäà»|ÖÖÓEÓ¶O(§‰£õʸ;¥Ä šà3G_åó™Pß)í§M’¿° ‘šˆ´÷–.å60´Œ»DòªB»‰ý~r1S??ºpò”EÍö""]ÈÃ=»Oî ¾¿yÁo£Ö—=ПˆtVnvèÝoü©%‹§/«å5³²iV1céúõ¹4{NdáRسˆ<¥¥¬ ùsÔ˜½² ¦zÛˆ„è G­Tù-ê­š3údÑ6eÞ:s/’·«ÐnÔ Æî Ž„Ø;ׯûóÆ[•(_‘:=u©a§¿³xÐ\_»‹ÛO†Uœð³Éï«ï·YZÁæÝÆá“ï×õs»¶ïô+½SõžãÚ›YµîЃø|%[Ѳˆ’²*mŽÊ·³íåݧžÅ™{¶2¼“ç‡Íc—?ÒÑ£ñÝü‰\º,^ØÄNÄRã"&¤YM™1ÒGßÙ»zóŸ‡&Iò—ðî<¸ce; éãX½vß÷j©µ³EÔ³îkfÔÍϧ{ë9b 1 fÀ)€\(gr“Œôq/Ï­Úð £Å¥LHûòèæ»#÷úU±â‰¼ûõÞï¿êܳ*a¡Ì¾CµÒ…íøÂ…Šyù©n~ªH^jj*åE"³|ù,•”øüiÆÝÓІ? %§2²Zç”/:zÓÆ6Æ<šÎýJ ‰ù&ªV£À§~ÐyIˆ$…†,žÞÝELTÓ%ôbûÃ'_ô¬O$.ùËš¹Mòó¤s{s0àнPmeSÉ'‚JRºæ1"¦×ët:ãÃ/r¬? eàèÛ¯V\âå®M÷œ:Í©iC¤?ÕºåÀÆâ§7mŸ»·èoÃþœ?ïœc—1‹Ê[ÆÜܳtù"›B3¼ÆT·6í*Q«^ûÚ?ÙˆnñÄé_8çÚ©÷hÉ­Vºe[º±ß¨:o­ûcíùj³Z¾;<î9Ç®c•Ë—\Úto Iwvž¨Ù®×(Ÿ°Ów¬:赸£ß¤žGíw;ͯ ”W˜r,完1"FŒ%GD¤ ñŸ³À_ÖlØÌ*¶q·w­X6UmrC˜7n!q(á,Ù¡%"N,qDD¼ÒJÉëTÚ¬[¶c\º ~.õ°„¤Ë³ú\NÙÔ½ÏÚ©5]|z79?mýïGË>úŸUëéõmyÊHìÔqôà–"¢rönŽ ¼ÜRwêxx¹þ㼋(ˆljû6>2öòíˆZŽDÒRƒç­dÎi_Ý`DŒ11&òè3n@ NïrüÂÉòÃF´rS;wteP˜¦VÔ‘cáåû÷.¢ ²­Ó®ñÑ1—oGÕv$NVnØ´~eLˆ4Myü<޹ÊebŽ—*MLM¥D,M†’  iÇ´¯Î vh· eE;‘cßηm xXÙáà}‹V³ºÔu¹*žøÿ›K~§˜ñ,1Ž¥Ÿà€˜ wÈ™Ü$^Zeìòa.VŽ^t?^¬pDÄôz޴ΜÍ#J¥,–*’Yä3-»ks£'Ïÿõ¿ÍSvo¹0gÿÓÏï_æóUša÷é5­Œ­|’X»ÛÐݿߪ›[+HYvÒÖñºð£“ÆÊXŒ>Äÿ× û¸ž ÿèVÉQu´{Ë?2¾”TÿGÈX†Ð,%˜ N\²ëè”ù¼ÂÎŒÉÜÚô®uiö®3ZÏoà bLÏñœ¡M-²v³ÿü,*îù¼AWEÆL£Í‘(82N$WŠ]þÆcD/âcDR¥„çxN`Œ/Sˆ­^ûîYTÜóyÓ•– wd/vãdæ N—¤Ÿüº2¼IÉã L`)Mý¤ðÑr7sÞ°±I÷|ª{¯^Å„ËÝŠ[‹’&2”Ç%OOž€˜ WÉ¡Ü$±¹“»gÙâÓç½é2pòˆÂÖv),s,n›x.Hm^§€$¥ΘÀ,ŠÔó+RÏo@¨Ÿ&¿{¡m¥ oâ4OL¯Ñgl6r< :½±å-pvW×´R7”ºÔövؼs݉^‹[8IL‹%½ù-ùÇ]ך··^Qɉª8*‰„”\›t’^^¡/ÐÉé oè,ú,‚ qb3‡‚î…äi·ˆ‘H""¢¤x•V/ˆ8A`Œˆ ‚ pD¤×3ID‚NY55¡eÊ\ N,7=1F7Là’#/Æ’g+ ‚ ˜ ‚@ã8"AÐ\šHnÊ?IÞK Xò~ÆPÄðpòæ†0Èø|BJÌ ”v[þ‚^'p<1Â`ŒlRG'2„WˆrcØ@Ù]7É€7/7héȧ~ GÎ-´yR•æKíš7zºrxÛŠÖº·wκé:¨ÅƒE§ µmYÕÃ,îÆÕP‘sc{Çrîü-šT?:ºnã ]Át…Jó»Y%8ræ¶… ÓEø¯¸à–n÷´ëJ w×êä°¹]F¼ܾº‡Ňݻþáã@bWÌAX·kÛI“J²—ëWÜÓZW6üEzéÈÙ¢UíµAþó÷Ä{M®ã úðE§ Ë“ä<œŒ$=ß¿.PѰ]åË{ש6«µ‹a†qr?¼&äÁÁ¶±m>'gy̳÷dZÜB”òLœÚØj7,”d ’'dó`É›§<9gò‰Ò [&OQ`ÄÇ‘ ×§g0ÉšÒú—Z»Z&Ý}­+f'"bñÁÏbäŽN62SÕ‹7±úbÖ|jÁæ. f€ïDN¯›$qm3}ÞãnÃ&Lôغ¤ÃüúÅK¶Lè·XK&Neëwhbëb,¿¹Þ IDATê¾qjß• $².ÕbÊôæN©Í˜‘—Æý6cŒ¿]Ŷ]:\r)C‰-Gµ»4qÆ€£Šç´rß’nwQº Å¢òÈíëÜW¬ÝûÛØ1z’Ú­ÖhÄä¶®z‘öÃèÜjÂÏOZ9iØ&ëŸ|ýÚ~~:¥1}}ÈMÁ‰²µú/œRßš×~iÌ dzo2&0ÆtѯßçŒQÇ+Üí"l8.Ô™Ôª•˃‘7Tߘg¤‹¸uñjÁÒùu¯Îo=©*Õ§¼•BÙ¸žíÄíK7‘o-7Ó¤w/.; ›ÒÚߥ6 Œ ††½a0@à’Ûë2WŸz¶2/‚ãBªŒ‘4Ÿƒ"æïË·Ÿ¹NáâiŸ2pÃ#Aõ>èÑÆ”-^áàR½¾óÉ}ëÙûU¶Ž»½wGP¾¿-¤«`vn÷–æõ êßÜ:¦³§ss 3@Iæu6\®“¦ÝnYãÿ›¿~)ÙqòúŽ“Ón]tÌo Ǥ/@Z°ùÌÍgÒq(ÕYyÕXjþ#6ž‘¼ueŸ1Ÿ:Þ¢Të ¿µžññ¢£þ Lù…Sí2o—”ß; "Ý"‰}“É[ºHói•¥Ý‘ÏßxÝ_?3DœùM ˜îá®Å3Rqë3ö§€£ñ^ڸɈ«Ô¡ÍÉ_÷nºP¾;#bñ-òM’Ú–k=¬_% ^ —æ£Æˆ¶ïÜ·ôt¼@&¥½|œd,ÒØ®7&#×1J^ÆÈ0ãÀ4ºöÙD—æ£ÇŠ~ß‘ZZã2aˆ@ŒaFrü!qmÙ¾JÐÖ•¿ž4)ÝkÊh[ŸúŠˆ"ή˜w6ùè2ïׯ?NܰcùÔ?5"KÏ:}Æ·v•Š…ƒÛD®9´l¡Î´PµZ%-^E‰¹ä?¤¾)bøáqŦdgÿIE"6lˆóÿ•gNtðí‡_ îÍ?¿#Ív¦¾Ä®?v•.Vš}Nb~êÔ¸ ‰üú°€‰“.לþk[ã²Êòç¾,ý?ëû«}Ã*Ïx· Ϫ~´qü2]¿E}=åéãˆ1v÷áÝuàãß»€€€éAù¿bGŒ3@n”Ó¹I?:!åîÈ™¶ÚS¤Ü-íº¬ÄÒÎdŒËq)›s©ûf¼gs&7CcÌÐO-p”ÉÝ—3þÆ}|?ç/—æFÏš'=2÷(l'Oz}eçU®Ì0g Ëx¯jŒ1 f€\*GïéöD tÜ{¡c6 I^N”ûD;:Ýÿ¹L›úi#eíÅeŒ$·¸Y¦¥¥L°f>ŠcRÇC¸Ìö`Ÿ|Fz퇻‡÷‰Ó‘Ȭ`eßÁ=ä”vÙ$.ý1 fÈ…aå̺I`\I4“¶oVáÛðÖÞ3Vx'õÏû°ÌÿG,«ƒøÜVz–¡ ÷‰Â¤îíÆ/k—î<±Œã+”<‰1@.ƒá…œÅ˜îòìt³³.ïsŸå\Q€˜r#ä&åtÌÀ#îã60—+Ž.‹Ç¹Üsþp b€\6r“rˆÀ k}”­“a†÷YÍdîsÛû,‹Í¸t?±k©³¬#î«ÊÈe¡ b€40¼³²¾+c_ÑJÿŒÍ2N·N÷<,«ÍX tî“GU4ò8dîS¯†Ëøcˆ3@n„ܤœõôÅœ„l:qæN|¾Å=…3@î ¹I9¡}›ö8 ?ˆÝûv‹b3@nôEà ·Ýø–xœÈ… YI†àg1@æa!7 1@¦0¼€˜àS›€˜àŸÃBnb€Lax1QÜ¥áMÛθô­ÊW?\ä×ÔÛ§©·OSï.kƒ4šÇ«»6}!6{Ër&ÞšÙ¼íœ[*""RÝÕ¶i÷¯u¸Žrr“3|{2ÏA6N(ÉKÊß¹¦{aé7x ©s‹¡#ûVµá:ú6a!7 Èe÷tÓ¼;·i隣÷µr§Š-‡ ó«`%"–ô<`Í­§G f®5:ä[ÌT²¿ÿ#Åý*¼=pûçP¥ë¤Q-‹(¹´áLi¢s¼Da¢”ÄŽäI4·u“¹¤¨>ÿ÷±Å?œYµdËñ‡‘:…SÕöÃF·-®x·¿ÿU†Ö{»gç“~E-›ã7$VCbKÏz}Gõ«Ã:ñJ"Ñè6M‰¬Û,/Z¿ôv§^Mô‘7¶.Y½ïú»$‰uiŸÞczV·—¨nÎì6YÕ©—}àÖ£A±Å;Œ™Ø³¬Eÿýûâ{®½K›9¯×LO/+Õűí§7\ºyH ®I¢/¼§|S¹jœ!éá–_fžU¶úuéú¥#k'üeªÿ‹½¾|ôo·;Ï\»jþ R¯7ü²àt„@D¤ =v*²d‡‘S†5±¸¹aÊö§ê,KæÌ«ŸÛ0ŸÈsȦÛ÷mVRscÉøuÁe®Ü¼iýèª;güv3ž‘îÍ®•\…vý}‹›‰-Š5ê=mÑ²Õ Ç6•.\e×tö„ rE¥É·ïÛµ¼‡krÄ¥{³wò´}ªZã–®\ýk3åÙ¹c¶=Q ª››ŽhªþulOÏà]‹v?Õ$Ü\5ggTõñËV­™5´e¡¤Ð=q|§y›`âQ¾lag%9»—ô"""IµÙþ§p1~6r“3¤¥yœdUÉÕÄ`$¶ôpSD<~&¼ì«9) IJ9r'‚µ剈xc.’ÄÖÓIüÇÛ(ÑgÍ[мðVõb}¯6[ è4¡t‚@DœÔTnìï×GÝü}éš=Wߪ9¹¹2I'* d1Zú4RQÈÓÒp29SWO+ÕßÁñ‚q¼Xb(N¤È§ä´I‚e…Z…Wn×íye¯òe«Ö©WÙYÁá2ür“3ä0&$’K>»õÍô$.1lÍør&)IÌ”q!i¶ˆ¹¼tÖïï¼'­YTÍÙD}cZûù_t@=Âb±‹ïœ­'.ݸyzíÑ]Ç»­\ÜÅU‚ 1CnRÊ¿8!ÿ¡\”C/²(XPùàe¼¡½­‹ z¦²*â`[ÄïÊð¨:äö[fëa“¾•­~};Xp(nÿÉAŽçIÐ DD[O[áí£(…U KqºˆCûáÁ [“VÕœMDDLHcàIÐéÓ—,³+’_õüq”aÍUÿâq¤ÂÙÙ”Ï<º!™}™]_²z¶O¾W—îE ¸ ³¹I¹À:ÎÀ´‘ÏïÝáå†ß$VέX ]»|¯EçÊæ¡§×ìxãÖ±QAk ßêâÉ+VÞ®„øÙ‘ßþL(=²ªHC¤ ¿zú’G[í³€þ FV·K·è© NLP阠U%$jäJ±¥‹•pæô§bÙyµ­ºuδ9VCÚVvä"ÿuꊤ݄ziö–Xº;ˆö;pÒ©ªå‡«»×_Oâë‘Ôª ¥êèɋ̱½ñÈ¥® Z¸ùo]²Ë©OM»˜k›ÖÝÏ_aQ=øø%ëÞü1u[´WÓ:Åm¸·×Ÿ$Z¸4ã)áÒx¿ÉÁ —n„u“ 0¼€˜Á3è2"ù7e­Y»‡Ï˜ [¼fêе:©Cù–ÓÆ·psT~à¼A«nÐ'–™¸Të1}°·5¯!"û÷Ž_w¾UIªv:º¶UºŽ}õã½F!¢Ù~ýš¯ÜÐ×£FïÎçf­?\\ ùâßúŒ?líª]óÆìP“Ô¦hÕݬÓ]pV5‡ ¼1cý‚I‡Í ×kߺøãCDDbçÆ[\›µxÌi¹gïù}“ÏbßÉ¿$,Y=kØ.µ8)ŸQs»‘“*“—,²,^BºrÛÔ=‘:R¨Öq|¿’ ¢&$`¼!Õ˜›$Bð›à—¯^Æ'ÄãȵLML] ººpáóÊbw¨|Pù|®Ø”€ìì?©HDÆ ÿýãÖ…ìïßÿTƒÕËÚ9ŠqýÈNœ9ѾMû<ðB^¿|òÖÓÃ3¿U~¼­¹VDdÄã'\]\óÆ+BåÇ*ŸÝûv7¨Û «¿Lúš;Üý€¹I/_½,Y¼¤…¹…F£ÁkY˜[xzxÞ{p/ïÄ ¨|Pù f€ïÔ˜›Ÿoei¥V«ñîüË®\¹âååõ™ ‚`ei•—ÒxPù òÉË1ƒØ±õzÿÖ¸ÚòvØ@?ÒºIÇc o=À¿Œ1ö¥=Õü8•Æ 7ú1×MAÀTx€\ÿÑË{ŸST>¨|3ÀwéÌMâ8Ž1†¯m€ß—~ôcylœáÛU>I÷wÿaì®™^&¹ñµëBöõés²Ñºåí±ž  òù'<Þ0ȵaåDnÒ¹-Ê7sÍx«@Ò‡\×{ÀŸïõ™|·-làU­b¿£aŸøüªnM¬_­õÆ—ÚÿÚ&Ž Ž«Ó°±á¿!çbâoLoÒzÖD–BôÙÁMº¬R3Æ‹¿0ªq¡'> ¾Gÿüµ½¾SZãÎE&×',þÖ¬f5Û®ªÉ½•c±†&W>uùvùeó•pm¶Î±äü‹ìS­íZ§aã6Kï&ÐoëÕ¼ïá0}vrîrGåó °†Ü(‡Êöá}j¬m­vð” 1——ÿv£PŸ]ìE7T? ¸eš_ôððÕÈFM­³§¥Ûû5ºµ(çûúRûòÕ™µ×iûàÑy/šßÁÝ„ÝeDŒe/w€ ¥–"ñè0rœPL™æçèêûüí?ç[ÛotË}–®¿Witi%Gš§»ä[¬ös—æâʇFœ´ÜÀ齋ˆÞß>¸vÛÄi›4wúÚÆ‚¡Ša9“ú$Œo{jË™ö³|lD$0–\¯eãðR³ U>g€kÁÊÊÊ|hùÐíóþÖ&ÜÙ0û\þžã[”|´úùÑóqå «¯|è-J "Õƒ%¾ÕjMŒˆH÷zO_¯†“Ά‡ŸýmÖ†IDBüƒÝã:5*ïU­¢wëî3½Õ’æÉʦ^Õºþð_>i»úH$3QHxŽ—)•âÐC'^IL¸8ºMSoŸî«ž¨…ø ýó‡¶iÒÔÛǯïü#ϦZÑ¥åàýg6þÒ£a«ÉÿûpwÕÈž-›7õöiÚ¤ÛøÕWµꠕƒÜ×}Ø7´µ·OÓÞû_¿ X½øÐkcLrvõ8¿M½}|»NþýZ„Ž1!:pt£N î×»MSïfÝ'ì JÀˆüÖmX}ðоø„ø”Gýy`݆Õ_ÑÕgRºÇÈš‰{ì©%ý»có¬3²g.»•KzqxN×&5Ê{U«Þ¬Û˜]Ï5”c•cŒ8‘¹“›GaÏ2ÕÛïU‚ž\~šÈ˜>âÂ’Ÿ;ø6÷öiêݦïÄw¢õÌPó Ø}|ÍØ® |š6¼<0LËcÚ°‹ë'ø5kêݤӰ wbµ‡{ÿ´¼}š6è8|Áñ‰‚±„þÛ.ÜÎÛ§uù§‚ß_]7¶KŸ–&ìz/|<( ñhÕÖ9hÛAªt£ñǶl³äáAõ³Mš ˆÔGŽnÔiþž-“ýš6õî8nóݰ'‡ç÷jÓÔÛwмs†c%"]øµßÇvoéíÓºëôƒA‰c쟫Ù|\ ÷V>g€.l œY7IdßhD¿?º¬š½úy¤¿¸íÊN…¥”IÈpòllÑÁÕ+[ß’ ;|#ªaƒüŠb½'´<>hñÚ;F9]œ¿6Økì¬ZV‘·;<^=~ùê“¶Ì(,„Ü <÷.ZO6ÄóÄ}e Α^¯Oéo0vÎ1·kp¤nízÎFGG™›[Ô­ãv=ÄÏïê;5ë··óŒÁW9û~[9ˆr òÑ|xÊìÛW-ínÇ»*V¥qŽV>©=÷ºø×ÿÛº3ȲÖä¢ Æ¸|åÛ´'Ô1 W,g/¸2F"Ïa³'Ö³äHïüîx`À£ð8Ù£!ž½W÷®gÃyH®m;ÁHzÑÿ–©Ï’H‰\ú »ÝwêþÛ}‹Û22ex]KNïúúÏ3Ç*ßÁUJñìÔÁEÂÔMlß%ΤdÛvŽÃ¶zZ³«‚–KþÉø+cD$¶l4eB§b2J”^Üû@5xrÿjf¤ux¶ÿÂõç1ºbŒHìÜý—mEDåïö?uþE-ågT³Xœruå“×b†Ððз!oó™˜Ùæ³ÂE“g¨µš·!o‰ÈÁÎá« Éñu“˜!íUÄÈô£Ï›æeÀ™(ÏžòóÄo\Q<ÑÿfLoKž·¨6xdÍö¿.);aW};QÊÔie±fù)ãÚµ¯\£Z媚Ôö4ÏV¦ÇqisjÓô“ i2nIr?$ñÎüÞí¶Ói4J›xcÄËL$ÆeØ´¡¶­Xä^¸^¤4“&ê͵:½ÀÒvG1´Ñ/ƒÕ–œóø|n…‘A¡jA`$‹ŒÅÉ,ä,F£C¯äù®>©TÚ ~ã€Gcb¢úïgŒ™™™5¬ßX.“§½þAøì¥KäE;ýÜpÿ¨ÿÕÚ¾$³þŠ/®|änõkXŒžÙ¡c@ͪ^^ušÖ+e-¡«|#AøK‡@""‘[ãŸçõ(©d‚ $½<µqéï§ÇÄÄŒKлhtz1âDbNŽHf®àtª¤˜WÏâ-Ë6'c]c¨xÔ¡Aa¼}q{c÷¼Ô±˜àÿ,\cÈãEœ “˜HyŽ'&q2¥DÐèÒÏ3`ƺ³«ßÅkÏÜí}º§Ì–2Vp†‡ñbÞ1.UÊø$'1‰BÊé5ZA ”M9">a{ñ¾7ao´ÿ\ÍäöÊ'oÅ !!!Ö–ù­mÄ ®›N¾OIdÛlÞ.—ÓÇN^¹vnͤ­»Û¬ß1¼¬òë?ÉeŒR¾ …”Y~ :SzßÓSž¼'/3—¼»”ü}É‘>ôäÂyÇø¶Vú–±•ÅœÕooòˆ}Ú¡{CåC©á¥„i¾}"8"°,äå~¾”Ë[&“Õ¯ßèĉc±q±&&¦õë5”Ëå.~A>¿«S8º[ñ÷í-%™íñ•Ÿ¯ê”›j;yþ¯¿6OݽåÂì}ÓkZe£Ë"]å#Œ“”í?µ‡ã³m³7<‰ç¥<¦}µoúŠ+N=flmäiÅ…ì>ê23&%×<Œ8"ƽV ž#c‚ÀˆQjé©OC†n‘ÔÒ´ô—v{ã‰OžòÌ”%}[;ŒÜæÿ¢Vr]•v.³± K_• ŒŒG’\:XòŽéõ‰¤œ^ýOÕ,ÀwPù䩘!I“dgï Çz¹øRþº>+;{‡·áï³óÔ9𛤠>0wcdùC:”zðôÜÈyZn^B‘vƒ—§N‡ºv_>µŽaÁ’ø ¬:|#¦Ž·yìÿ–Ï¿àÞ£¿xךÙþÞ+ÛHÓá%ˆ­jÔå§F]†>ߨ¾ËÑKo‡”õÈFôË‘ ©¹Ié†Ú9ŽéõŒ1F[÷üª¿^hLªØKRß'mÊø<#Ò¼¿ÿ–Š jQÆFFŒéõÌX Ï‘ Ó'Ý1ÞÌÙEõèUœàjÉé"ž•\¤a—ïDÈ µiž¬öíú»Õă³\.ñ³ºú˜>).A#0!)>>Qk&¶(`¡:yúò&v¨ÝÂóȺÙËe=—¶Ô½ü¿S÷ú öH•àHdåf#ìùsÿyiéÛ {ÒYýÄ&±tRFß8ý˧3±3v¿Éܛձ³iÍ~³vÌÂ7ï qõ­ë$Þ¤Y1͈êwÈ“>^jS&“Õ«SŸ²¸ëjŽuõ}Uå“pmÖèWß–U=Ìân\ hl/¥œª|(µkž)‹uœÔóåÏg­.0§M¡| çýŸÞMxrf׎33$×<† P&È Õ/+™±aõ^}m»Ø‡§öŸ‹Ó—κr“Ÿv¬]ºÕ}`}ÝÃC+þ'ò_„…¥–`ÌÈd)£­ÑšfÜ€#“’mZ:ßúŠÜ™ bûê]{÷œÓNzx|ÏÑ}áôh¤0¼P–<Òʈ‘.üêé‹®åìô¯Îl8œPfPe{gÍ?U³ßyåóÝÅ †öåÐâÿ¯šïÿU ’#G›S¹Igÿö·{¿½Þ6<ñ¶ GôÞí·föáúkZ§#j^Ÿ=ñαI5û”oY®YqÝìCþ¿½úSÖ~M{W‰„ZOèp ëâ¥åº_£iáŠ6·Lê3?‰dý¦¯cÉë?±¯lYs¤×ûÈ"§õ\õŒˆöŒïswغY^Þ½ß\´rr ¬p—Ù“[™¤ß¼õÀÂI›t¤°+^­I-s.M÷ñöÞý»?\º}ÉŒ?,‹6jêS0ø 1Ƥn¾]«?\»xôQÓ²§µ2î õð›8B·j󜱛õR»Ÿ|& ÷q±ØLzÑ»è—Iî‰È¡¥K¾®ò‘¨ä¾qZ¿U $².ÕbÊôfN"ÒPŽT>”¦Û‰ü<æÅØ œ3dXëWKw-œÄÛ”mT¿¾ížg”î†h©É´\¯Ÿ}—¬ü}É5‰]ÙÆu~ :,c¼U‘£W¬Ý0öX"gæV§÷/}~2!mÚ¡…Ô§Îzœ!̓"û«î›}™ˆ1ÆÛ{÷ïö`ɮ߿“C…-j½ÙñöSã )?±˜[»gîWKì*¶;¬Z>Ž£ÆÿPÍ|ß•ÏWVŦdgÿIE"6løuû^¹zÅ«’WöÇ ·Q*•†7÷«Â¼µ IDATKøüá…gN´oÓ>œ´ÓgO/Z\£Ñàúø—]¿~½B… Ÿ¿½T*}ðèA½:õòÆËGåÇ*ŸÝûv7¨Û «¿LÊÿGûŸŽ3pDiú¤põ|w!oZ¦¦¦Þܯ–ãë&å~Ç¥½?ü›•Þ}ôôz}^!Gå€Ê'·Ç R²¾ú,ü8órü•fäÈ‘ý|»œ\7é;‘šÿnûÅé¨|à«|þûû@§44“oÆt±ÏüçÍ>WfêB߆yª,ñÙáuk÷^ QI¬Kûôê[Ê‚'bïì_½æÈÝ0ܱBËA}}Š˜pDºð«;—m=ý(J0-XµÓÀõ È8bê·ëWî |Ççó¨ße@·*6ßã°s-¾—¡†ï1ù*ûÇü£å&ÙÛÙ3Æ^¿yŠš ×R(.Î.övö¨|à‡ª|rå|.y‚ã8"}Ì«×IåLxŽ#’9xÚ²[AቂÞém*ÚÉ9Ž#Néä‘_séYt’8(Œl=ì¤G$6/XÈ$öé›D•äy”ܹP>1ÇImŠ:ðþCµœ#î>ý¯½¹_êÌMâyÞÉÑ©€S\<¹\ËáAå€Ê'·Ç ŸIPŨHj"5´Ay™™Œ©¢U:UtIM¤†9¼ÜLÆT‘*uB¬F¤”ïhÃËÍåL•˜ KdRgYr”ÜLª{§a¤ÌF³w–ÈôœäTQ?`nRžl‹*Èþûܤæ@1ÃŒó8ž#âEb‘aΰ‚ÏsD¼XÄóO>]\‘Ù;^ä&ä¹hœòR\„u“3䑦í÷â¿Z:é?„u“r‰ÿrõ ¥RÏeÛð>}_¯4>>^©Tf§+++|83½­ý»Ðwñññxò’øøøw¡ïìmí³Sˆ!+ Á@nð_æ&šƒoBÞ$%%áÈ3är¹ƒƒ•••^¯ÏfØ@ÈMøÁc½^Ÿ?~{{{LƒÎKcZ­V§Óe3žD´€˜ˆH§Óe³q yÖMÈ=xœȵa!7 1@¦0õ9­U«Vá$äa{÷îÅIÄ _ ë& føç°›€˜ S^@Ìð)ÈM€GÛ¶mq1ÀW† „Ü$Ä ™Âðü8°n fÈ1ú÷þ}›÷ÝŒÀý›{üw÷Ž»4¼ýì»Æ£0s*Z±Ak?ßÊ2.çŸJu÷ÿìÝg|U߯ñ3[Ò{o„jè"Jo‘*Å‚•¢7–[Ñ[{Å‚ŠŠ`¤ˆ¢`‡ª@@H•’„PÓ·Ì;Õá¡Oæõù´nç¿{uö®ìâÔ }p‡¡½2kî‡o>Ñ-ëÇ·?MÊëÙ¯Aæ– ' Ba8³iÓŘ!7k«÷ä¼Ý‹†=óÌýq«žÿÏ´Ey=™>u~ÛÜÏv\VDÞ¡ùO½·+ú®·¾]ðéëC´«ÞzwýyY!i4BíäuµId†k¸E¶ Éégoö7m|×&!ÁºŽ¾¯‹9iý‰üâpáÑbÔ]Ãz¶kâØ8¾k¸áÔÑ RX÷/l]{Ò „áôÆÍ› ¾!°ºMrïú¿7& ¾é¦[n¿)@vÇkOŒìÝ­ÿíCš˜OÉ4fïYºÖ<ø±‰½›…Guz×ÏCkå(³û?¯]øpkW¶¥ÚÄôÀy°nÔO§¾·dJ?˜’wâóûG.°Üe`2äv9Å…F˜¾iþsÞ{άõðqÉ5ûšdEÚupãùKן¼¿¡výæË±wwªvd’¤ÕIB!¹y¹h4:MÑß%³ÑlÌ6²S¨»Š,+…7:hCnØì³¯¿\ ËnûpŸÚv雇kWÊpÝÌW*~S îêŠSM/SÖ.üùhž"˜žjdÓ åŠùÊ™ä#‡ÿÙ¸úë·ŸôÙ‰¶÷?20Ô%¤ÇènÆß_~sѦýÉGüµêË×§/L.Z—HÒ,D>¼bÙ¦¿wo[9ëÅ9‡Š¾äMØå–fY{÷æuLhSûC’wܘ¾žÛÞ{áËõ»&ÿ³sí×3g|¸/Oˆœ-Ï íwÇǬ›T»œjÝ$}d¿{¦LÕÞ‹i+pN¬›õ³é<ƒbØõñÓ !$ϰVñw¿ôüÈΡ.BÿîO¿ýè¼9‹g>µ¨@¸·ìvÛ„ }áïh#›xà­ùoMÿ. öÖá·5>ùgQhðëÝD:Ù`Hl]|Ý‚äÙîÁYÏy|8î´å9Bë?àîp½Š, YfKª‹Ø X7 @¤V/®®ÉïÏh~~àÀªø(Ê•Í/=ð¾ïóß<ÞÆŽµsU¯MZ³~͘‘c*ùÅ‹7NõŸØœ±uÑ©î(ï~†9sæL™2…­Õ²e˘j@5ƼŠRÉÿ.Y¾d@ŸýïêÕ«_9hÅ‹ê¦ùä -ÿÛµÇM €u“ÊÆ†R"ªÄ²†A¥ÇVÞ'ΓLiütÐû¦É1ntªÃÄAm ±>§=¤p„™¡.>HäðÙ«‡Ó¡‚é úPVj9ÕºIB6 ŒfE)ú @½Ž–$M¦}hؘãßÌ Îýˆ½»:t4T„sÔ&S×.XyÂ$„[–|µÅ¿ë¸Û;ú©+Ísö>X)µ³Iƒ=Â1¶ÿÊ?K?©$q4 3À^9Um’>jЃܭNŒu“ ~Ô&Aœ«6 P9æî•ì‰ìŒ 3ecƒ`Ý$;:!Ùêd¦†“(gq™¨L/ Ÿón6uõ°åý ²,§g¤§¦¦æòé ‡áæâªÑXŸHùN7Øß™FpO-+q3È •IÏHOIMñóôñ㢲ã(0RRS„á¡á5yj“aìØ_È "555È×?0(X§×ÓÃd4ê2u©©©5É L/ '€ ìë&ÌP™|C~hX¸Y–eY¦'TH±î,(I¡aá)gkòÒÔ&ÁIGŸ =d†2çGÉÍÝ=77·æÏÃð½Ž:Èên­y§P›€”ìtÁÄÈ V/%ª Ôš‹¬*5·L/õZMÈy;NU}Ƕ$„°íZ«’ -¨=¨X§æËwº€-GH\Ϋ_öt3›‡³²é<ƒT80µü+''‡þP«§¼¼¼Jun cƒ 6 ¨úY\p °Û˱Gád 2C#ƒtí%Y»):Ëý µþI˵Ò5œg 6‰c:€5»<Ép¼ÌPj ©Ñh„P R6}þÉÒMÿfiüšõ¿kÊ„®Á¥Þ¢’›¼ê³yËþJÍÓµK¸ï‘Qm}-O çŸýë«és¯LùøéNŽzH”Tþ„µ…u“Æ5ú]ŽöÒËÎÙSÜþÎ&Jf¨fg”É †—¿ûÅŽðqϾÒÒ´gñŸÌkòÒ­aÚ’‰aß·ï}}¬íÄç¦D_Ùòùœ>nðÞ³Ýý¤¬í/Oýpoš¶’¦&_@¬FŠ¢ØÅÍâ×¼Cj“È µ<ÄB’$CÊæ?Î5¸ýÙ„N¡ZÑhÂ-<ûÛ3†…_}—ùÇ×ü•׿;¶õ•Däý{7¿¸fßåî7øt}qA×ü³x#»Üѵ|eÿ²9_ü´;=_ëÞòæ{¦Žïâg7Á‹¯jøž™^àtœê¢&Õƒ`ÿ%3T£yKßÏ òÓŽ_tkÐØO'IB¸· ×ü|8Ý(EM´ùòÉÓù¾"=5’$„kx‹e÷‘ cÏ@WËŽVË‹r÷~õÁ—ú=ñæM¡©¶ïÉÌ•%-[x½unuQ›Ä!Ò>;Öà`Ã#u#€ód†2ä¼Ë¹ŠK×¢b%7oÓÙ,ƒ"<¤«?‘'\<],ÿÖ¸z»*y—ò®û-ÒæœÔÔ\˜öíGº‹ÈF-»ÔÆÁŠo–(§MjñÙ¨M‚Ú‡JÀÐN¸1ƒÌ`«!fÉû4’$4:­ÖrC‚F‘!I’F£)ÚJ%$„F«+ü ɲšPÑ $„TÎý šÀŽ=¹ðå‡NvŽoß6þæž#ÝÙðÕœ˜^Àh¨Ûíß‘6~vgÇÎ ¥—’¤õðs— 9Åò˜lÈ1è¼|Ü4W‡Ÿ:?w© ·ð'dCŽA¸Gzh%I’$Y¢…¦œ{ ]¢‡¿<¯Ù–õ‰»wÿ±`Íëîxïͱ õt¿ªrBIÔ&q‚.ªÛË8DlÓƒ¥ïgÐxD6 ÈÛwâŠÜÖ]+Œ™‡Óä>a®%†¢:ÿÆ Ý.MÍ]$a<{8C:0Ô¥Ô”»*²p k×w|»¾ãóÿ™=åùm‡.i¬©Ñæç,_ Q+k]lÔ&ŒéÁ0ì˜pòÌPj­U­VëÞ¨wŸÐ_–Ì_=º…qç×+3¢ÇõˆpÕÊÇL}dEÀ_¾ÖׯÅ-7z<ûÍÂuþ Q—6}ögAÛ‡ã‚ôZbÎËÍË+0+й /7Oçîᦽz¨3žþá­o/ß0¸g«`)ewržO“†>:ûYµŽ¾Ð­ÖsQ-®µÊô`å€Ó?ƒrp°ÌPö~áÖdüsS.¼÷õŒiÙ’oó„ÿ>3"ÚE# EŠ,4Ö«ÃÏÞÿáìy3¦åé‚ÚßöÔS½ƒu¡\Þ<ã®÷ !„xëÁ»EËÇ¿ÕÓ¯øPìغµë§‹^_~Ñ$<¢º}úÁ6žÔu™1j˜p¨MÀÀ€õ‡NŽ”Êíd·èO¾?àÉktizßç¿ßWôž-F=óѨk‡ª’_Ï÷Wõªøi}ÚŽ~êãÑöÚIŠ¢X¾ÖÍ©6Mj“»Ï`?õ?z©£WtúbûûJÜÍõ¡Š¶Rûk¢ÞÏÀô ê3(a€‰ú쩲ÝD÷Á©2ƒ°ï[ð%~9‹šÜ]ÃÎ¥6 €Ì`UJ‚éÍE¬›»ØÄÕþÞlr±†/ÍåOp”pæ¶²ìûÈ Ž7´µ¶Z:ÉV˜^†žùP=l¹Þ¨‡‡Gvv¶Tcα%ÛÓ'ÍÎÎöðð¨É3Xª’,á½µ|VàÄÀ)™íà €j²å5¦­¯éè'÷OÛë”»~¿÷‚[“ÁO¼0©g°N˜2¶ÌŸõÑO{3d¿˜–.WäHË?ëàïÏ^ºéd¶Æ¯Ù€ >< ‘»ñè'÷OÛ;èžf‰‹~;njÐûá—ïöY=ëÃ¥{³:Œ}á¹1­šõöƒ­N|ûöü#B!LGæ/KkwÏ‹ÏM¼1ë·÷>ß›# '–¼ð⪂›yéÝWÜÐ¥ð©2Ö½6ýëSí&}0磷ïŒÚýáôwe)Ba:¶øÿ”~S_üOOÓ†YS¦Î=ÕöÞžzpá‡ëÏ™K¾­gˆ§ÈØwø¼©ø7/I¹²óýg>;Õá¡Oæõù´nç¿{uö®l¹Ü/%¾óܼýQ·¿øÆë3Æ—¬iÒh„¤!.\·>pÀÅÑ€3\æpPj¨2gŸÞº`áÿÞ/·òBòïrûx!ä‚Ë™Íâ;l:r*Gi"„е||æóý$aj˜öûÆßed¹ý°*µåÄÏ&õÖÑÂå¯oÖŸ”¾yÅ.¯[fßß»…‹ '?¾ç¾çØ;¹MˆºV¾úD?ÉÔøÌOëVÝøÜô;»ˆlåÿ~|çŸs†[Cݯ¶JÔ­S†ýùâ¬;Æ/ës÷¶nJfââ;ãæï®"dô}]~}m}rJfyžû>Q?pæã#Û¹ !Gd®XºD!„K“ ßü:ÍMâ €]’ƒ @fB!çm|fÔF!„ÐÅ yrÖí½%!”ük>{oÁê. ½§·&Çm’-Ó™:˻պû¹KÆ|CÖÉc9þ[ø^;Wb:w$CÑ&¢pÖÁ-ªuˆ¼"ù¼)D¡Õk…BãæéªÑh-—ûµîžzÙh¾vÆTã÷À' ìÞ¶cçß»V²âóÏo|úƒGæäøüþ‘ ,G,“Á ·»’VÒDƒ‘ÝÙÀjµIŒZÊþ å ª{œá¸Øwf\:M}}bä±/_þôp¶Ö]' !Œÿ.{áƒÍ œ¹ä–Vš”…MÝ\ö÷,£}Ù,+mÙaG™Ê*4×Oʸh½£;÷îÜäÄËÛ^}àÕù«Fü×,t­ûLœgñéõ§ç–óà©Oe¡Ñr1ÄjL/`¤ êaÓû$­wx£˜¶·üoÆPÏo¿°üß!òÎìO÷ìrû Ø@IY®øÚ€Æ+¤œŸíÕ»áüïÞù2쎶º“‰+¾=f .\7iÁýÿùÞïñögݤëÇAmÀVt*š•e!„¾Á§g$?2ýõ7ðäScO¼õÍkÓ¤Ðη&$„}s¤â¬ÿÐÿƽ9ëË·¶éÃ:ß6 îЊBÔç¹W.½;{Î#?çH>Mû?üÚ:yKÆêd·–wÝÛ}É/ßÏ\yÉ(„Gd§áÓŸžÐÂM#º?ýö£óæ,žùÔ¢áܲÛm‚\ýcÊ>è5lÆ#§_ùì£öiÑï–Á‘§“ ŸZ–…"sºj“IêjOç˜Ã§°â³¶zquM~FóódDņ놇5ë׌9¦’X¼xñ¸qãìº)æÌ™3eÊ”ú;‘”:ö•< –:8Vò_Õ:K•û[eŸ¼ì—|ÏÅ?_öµÊþJÙ§­â›±âƒTåLS¶ñ+úÈ×mÛJÞÉŸ©èK¶dE[EE›J%[Eå¿RQoVq모ƒ*úh×íµj½zå;N©W©dû©¨å«ÕøÖíォrÛ³ÜΊÃËuÛ­Š“Jž°*;QE¡Z›îuÇŽ×ݧ*z·í5Ù *:B^w³¯Ê¡µòg®hǼî.\I;W²£•ݶ+ßÙ+ß„*†jíµA©ô…–,_2 Ï€ŠþwõêÕ¯ ´âEÕòý @)Ô&Ô‚›màôÈ P#n}æôL d€Ê°n¢&Yš>"y‚Ì\?6j“8M‚­t42P.¦È @e¨Mr(\†ÀÎéh¨66‡¨M2]üçÏuÛgäKa±ÝûvoêS*©ËÙ'¶oغÿÌ“äÔ´s¯›Û»0ÈT}ùþ z Ö5uñj¤’¤öÞQÿ;´æ FŽ3½`ÊLúuã ÷Ž #F h­9¼n;Ëòµ‰áÒß¿®þG×~èÝ÷ß7öæ³›Ù’fd °ë“(ó*°ÉÐ »™ÎÆaj“LÍ ìtS»èÐÐÆ{´õÊ<˜|åšÐ`¼pú‚&¬uóPOW¿FmšùœËÈ“ÙRÀ˜*bËÚ$Y–Ó3ÒSSSó Þšxl IDATùô„Ãpsq‹ˆˆ Õhj”H£6Éxé\ŽÞ?È]#„Zï0_iOÚe“ðs)þ ½”¿9é¯]§ºD{æeœÍójáA–çTÍ©­IP»*ùîpêHzFzJjŠŸ§wˆ÷¹:Ž£!%5Enõ“8Ä*«²1רh]u…GWI煉¯ä›qõ†Ç„>ËÖÿòÍ~¿@Wƒ[—!]‚¹Íˆ± Ø«ý~Ø#8jfHMM òõ Öéõô„Ã0ºL]jjjM2ƒ¥6©øOûZh®^‘å2gu9çxâ_Ñýnïèš²?iÇÁ¤-$´(µg&%%ÿ½sçÎlfd’ºýì¨'3äòCÃÂͲ,Ë”o«‘bݘI’BÃÂS2ÎÖðÕ¢6I£÷Ð “ÁTز©À¬uuÓ••™/îß~\×zlL Ÿ&°gd¯¥‹ÿú+µÕ h—kžˆœb! œ43H’äæîž››[óçaø^Gdu·Ö°S¥6IèýB= g2óä½F˜³Ó¯(Þ-}JîuŠ1Ϩ7–ÖÝÏS2ÌŒôÀ¨€Ž\z@­·ÆZà»­ Ø0Å9̺Iº€æÍ}Îïܲïô¹ô»6ïË hã«æŒÄoçÌùñp®¢óoívq÷–½©Wòr/OJJÓ7lê¡ €š†4¶<§N*j*6ì\ùN7mPÜ ž9ë¶­Z^ ¹‡ÆöÔÎO#„Y(ŠŠB¸FÝ4´§fSÒÊ…‰²äÒ¢ÇÐîM<Ø3ŠÇ•Â9*‹œòœW£ßv˜Ú$!„> ¶ÏèØ>×&‰ànwMéVô±½FÄöb›ˆ™¡¼QeíÁç~;ú¤5ì\GZ7 UÌQ2kÅð—F[;h"8Cf(oį˜®]þÊKëâÞøx\´^!_þûûϿް/9=ÛìÕuøÄG‡ÇzkÌç¶.ütùÖÿfæ(îQî›t{| ¶øIò’œñÌ¢³}_ÿbRsWSÊ’Çý&åêk 9ïž&TŒ«œƒÔ&œj¼ËEê`¿*•ŒÉ tEš"Dx§¢›påK‡Z$ÍÙ’‡¿{q’PŒ—Žÿùã–Ü–ý»†ê„’µï‹—æôÜ´~a×~³´ùô7So>røÏ}¼1ÕÀ£–νNlÔ&A’¸Ô3èG>€×bgÑïuÉŽ®¼Ë’VlΉ¹3>¸03þhò³¿]’ |êõ!‘zSÊo¼·§Ó“okä&._ý=­_ü½O7só÷ÖfÙðíÜ^vúম5Þ,Ù.ëÓ €õ§L.¤)3X†Ý¥ß…ÿ,ý¥b†”5ï|¼¯ÑÝ3Âu…»6ÿÊÛ½Ó¯þò½ÿÎ}æ£{L»Ž\ù÷ŸçÇ®(úÃÏÜž6íÛ—olÒùËÍcüÏî~r˶³š5âèzè\«Q›dãA§` TºydZçT“îS”š@8þ8mf(pYœ$Jü‡éüÖÙ/,¸ÐÿÙwn‹v-~Tr ŠnÓдmÜû¿¹÷¿Ïþ0O¶l—YÛß¾4ò±·îíè]òtÞ¡^R~VÓêGm@` oìP´™¡ª%ï¦óÛ>zvö‘žzë¾ö>åü†’9WÖxi4nÁ‘ Š»œì¦Ñ{‡E»kd“,é4– /ïÌþt< @W [²³|)DvnE˜^qÈ …ãÊRƒoÙ››—[ +²1/'7ÏÓÝCŸ`îÓ3Nxº·ÿùÇÏ !é½#Ã./~mI^ÜÍñ1þ¦´Kç÷í~w ÷’Ï$Ž[%Éøï’gæeÆõïÞ:0ÿŸ_¾üSŽ{úÆ`'éºÎE5k`j“PnŠuÒ[¢)ç€3g†2÷3’ügêÊ Bqúã~ðú‚ÏËP®dÌ.©è§Üº¿ùÕƒíZ꬚÷ë¹l“οUŸ©3ìà­‘Ê%I’t­Z{,X5wã7§ÉÍ“_Ü;PË!SÕy£(6j“¤8ÀÉ3C.Í'~µzb©û|°²O9?{Çÿ:ßQñðTòëûÎ}-ìòÀK]°ó~Rœì*#Ó »OÌ‚ÌP;;”‹–Vïd7mUÃû¨MBÙÜ\£“7gnì43{ õ<|·IZ¨ÑäFß/µI@•â=¾a"[2CuF•Ì38l.bÝ$¤ 38ÈÐÖ^X.ù;O¾¢6 pÆä 3”åáá‘íååE78X.ÊÎÎöðð¨ylÔ&¨ÏÐÂtT@cÃ× KKOËÎΦIvvvZzZXHXMž$ €–å‡|뮥2¥Y¶œg°Œ ϤžÉÏϧ'†››[xhx@@€Ùl¶úI¨MªÛC­àz*Øaë ì 3T‘Ùl ã6hG¢(ŠÑh4™L5|j“È Ba2™j>¸„ãaz¡^Úë¢I¹FÀaÎL}À¶÷3±T% nlÀˆÍêçár2œ!6j“pò­†ôk™cqîNf€1½8ιêÙØl¸¿Ðø 3µŽÚ$pVpî 3× ‚Ú$ÇÃ-tqÖó{cŠ d8*¦FE [A‚ÌT†Ú$pšp UMÕÆAmR‘äädË_bbbh À–cêëd@%øN·Rˆ À†¨M‚Q›»Ä5Qgnz`Ç!36‰ ‚Ú$8çù’“(Ø@¿ƒÌ\Ó €S ‰1|@f¬Fm™¸~lÔ&©> 36Äôµ¬äµV ™€Ú$¨âœŠª4-dÀ†±AP›@ý2P.¦F p®DWo à0¨MòõÐÑPmlÔ&qÚ£Íí—¢8ï&dÅgw³¯Îåb8ßnË<ÔˆéA> Ô£ÎçEIÏHO;›–›—ë êáî*UaG¥q*b©M*þ“¶9×r)‘Q œm³asåØ[e†ôŒôóηkÓ.0 Ðôü…ó‡ŽB„…„Ñ85ij“0˜¨ö`—Áì=(:sY£“g†´³iíÚ´óõñMII9~üxµ~W):ö)ŠRòï¥þ«’ÇE±\Ò–$©øÚvñ#ý³¢ÿº®Æ‡††¶lÞrïþ½U7ŽÁ`p†­Í×Ç·ŠÃô€k†ÇÎ ¹y¹þÉÉÉñññŽÝšÛ·o ð¨b­Qqã8ÉÖ&Ër‡Ú$€A6À.•n-N9¡§«û¶•DÑDâèM\ü«85QÜ8N¶»U©q¨Mp–ÌPr$í ƒãêF#gˆR¥>oU~ÌfÓ Æ“?}º1tü]µXƒ‹Ö¥š¢ÔaŸöqàŽ¶—Îå6hufIHŠ¢ÈE»5-ŸQQIHÕj§Ê Ui›Õ&å˜û‡C†ÞÙ5ƒCt¿ºÆ.œ6D1j$Cu™A’,#i³Ùì$™A–åª×&Õ}”ÊÛùê]/ˆç–Nï衎&Rum’Gû ƒò?œ½<~jß–!žºÂwªqqsá«L8ÙÐ 2Cݲ\M¯AŽ)s×ó¾^¹ùðùáѾ÷˜ûï¹¥™§Úνf³ÙŠÏX«µI¦S‹¾wÁ™è s?¥BˆkOUó¹rw½:æuíKß<ç^ß-i³Ú¤Ü½‹VÜzaL—wK>ÚkÅ¥ ·ùr¸H\¨Zq  3TÿðQX²oÝxZ!„|1qÖäWþðî}÷´ ±Arú®_¾žõø¡ó³fÞÝÔM…™AQ!U¯qj/2¤mÙêìzzCbêÈQ t¢Äª³Õ~ëÓFå© c³Ú$¯›?Ù~øíR?ÏHoŽŒ6À€4çUç–’ýÉ?¶hÎFãÍϽ÷ÄÈží[µîØë®gfNkŸ²ð“õgM¦³?OôÀÂEQ%wïë·šž”£(rþ©µï?q÷ „!ýFNz~é+²RüÕCÿ5Ó2¬ÏÞüô°ÑÊWÅ|i×¢Wî5¤_Âm·ÿ÷“u©JUë~†Zd<»mmZô¸‰ ¡)ë·ž5/V¥ä&ÿòÞ#ÆôûÄÇ[3MŠ¢ÈyÉ¿}ððø¡ý† {èÍeÿdÉŠbLùá¡“?_»ô¥‰#=üÆCÏ%æælž6rH¿„{æ-¨­7YÅÆ±Mm’Æ3²IXþö/§ß?bð¨wN›vÏ·ÕáIeÀàÈ uy(–,w2˜L&Ù*ùg¶lÍôï=´­wñ­ÔRÀ·ur9¼n÷E³,U>?)²l¾”4ëÙÏO¶›üÑŸÏ}âÆÌï^›tÅ,+Џz7¶lùQÙðïò_\ë>ü…OÎ{ý®Äw^ÿù”ÁºwZøÍfsÕïg°4N-1¦mÛÑ«G»›ºœù¿mg …­"çíþ~«kŸG¦O{°]æ3?Øi¼´ã£§>ÚuÇ+Ÿ~ôÖCmN1ýµ&Y‘әşü!:š8fôËOwrsë2ãó¯—.üàZy‹Ulœ€€íßîßýñ?½oì¢OMÍÕzäÿ6¥ßÔu—¸ðÍ8O=Ÿš.Àñ2ƒ(ZNÔ2¤·æÚù…SEpó`}É]Bš‰ŒcEWÞ×И3·-Ùê3æ¿cã5ŒqOgóÎ'òE(W´ð­åûq幞˜Ð*,0¬Eÿ;n‹8ýÇÎó¦^J¯nãÔÓ¹mëSÃ{v qêÙÕÿÔú¿ÎY>‡Ð¸ßøÄË“†Üß}Ä”‰7H{?ºó‡­¦îS¦ lÝ(ºEŸ{þs«çßË3LŠÚбoÎ|îþÛ‡vkäï©—4.žÞ>>Þž®šZ{›UiKU’ ÂCîÎ÷çdÿwýº/¦ßÞÌ]Ò…ßúþϯ…­þbwÇ à¼êoÝ$«-ü%Y–e©ÄƒŠPŒ¹F“¬Q8{`yT(rAêÁÔ¼¿œ4æ˰Ül0Êm³L–pQ¸L‘,+BQÓ圿rìÅ [´–Bj£Áœ™m’ƒ¬¹’W<áa“u“ÌçþZ{&¤g|°FÖ5èÑÅ÷— ÛÎ%Ü*ËŠ,í'„p k¨l IˆX—'s¬ó6´5dQ´©å/VüºÖ/Ê_$É(èíëRü`^úñLáè&ÉŠй(3(B(BQd“¢kõЇOv(^]TÒ{¹f.úQaùQ¡È²Ù¬hƒ†ÎxyT}Ñj=¼µV“«›j/3˜ÏíØpÜ”rü?·SôÐÚ ÞŠP _GXnÓ–4:IÅKT’ “—Ròaùš”UkCU~Ìfë&¹7ë–ô朽R„BÉ=°à½¡C›¹s¬lÊŽ¾. È V]• ¿æL’¬[GðãÆU{Ç>ÒÉÇrÊ0ŸOüigžg|›`«NÉÏÊ3+ŠNÙh”…¢hƒ›Ë{_tíY4ÔSdŬsÕ‰‚+ùfE‘„b6˜!É+*ÚýâáTáÓÖO[tfR¬|§Âò«;ÏP;Ë™ÏïX:¨ÿÏ× !„!yÉknØ™9 gÉu“”ì“λ5Љj¦Ys0%oP°§$DAê¾T|C N-jE¡IR,KAÕff¨JãØlÝ$}Ó?~xqïø€w¥‹¹û;„ÿ{¦ñÓëˆÑs¬k°Ñ±HåßÁç‹þÕÓ÷@×$3—Æ£îïñÇÛoÿOgßVò¹¿_øíCäˆ[[º Ѱ…ÿ¥ß­ŠvqçÊ%[rå.Š&°ë°Ízó=ÿIÃ;‡KŽîØø—~øÿ†6‹q]òËÒ5‘ݼR¶­XtÈì-טÛ…=þå[ŸŠ;ú5óÊKÙ¿iÝ¿´›dÕ6#Y·nRmD†]kOúÜ8±cÓèÂÙ˜à潺1é|÷¡äÿcí_º¦—vÿðù¾€~ï6½­«öõysÿï¡-t'ÖÌû5§ÍÔø@±(1(ŠBç×À7oÍúļÂ]T‹F^šÚ8Œ¨{Ý$!ùv{%ñô°•߯Þ}:Ç%¼}ÿÑ·u w­É3š.þóçºm‡3ò%°Øî}»7õ)ێƋǒw:}!_ön3bìM¡: nPó˜ŒU˜ÇÎ BÅ—Ò­K~ñSÞ{6ò«Å«Þyþ£BÿøŸÙH¯(¢áðG†ŸxɬW]£{ÑéÌ÷B(’ï ½2yþË?˜±¬@¸5O u¾wÊ€7æÍc³O‹þÃÇÆ|³J(Š¢o±W€$ŸUJfmD¿v¾ûñó]›Þýæ‹ =káb{ÇfµIB!ô!F<ÔiD­<—)3é×'ünLn>½mݺ5>Á#Úû^“äËû~ýá/SÓ.7ßì.Ì:vAÂ~6EØW»*3aYNTX{?ƒBM@ûaO´&”ì­/OþØçÉëàUx§®gÛ±3¾[ôƒC‡!dYq‰ê9ñ…ž¯š ÿø{gÆß[ôï#-ïIòm?üñYÃKl­›Íf+§†û¾ú}ßkß¹{»§æ/Bˆ¾_þPvà.\ö›üN¿ÉW÷;Y–Bnù`á-%žÅ'nÂk 'Ô¸Q®ý¼Uù1›Õ& !Œg7Íyå½e›œºâ—ðàôéwwôµvoºxøhV`§[ÛEûhDP¶‡—L¾Ò6ίÄóåŸLLÊjy˘îa@ ÈÀI3CñÒ@µt³¯k“øFKVýºCßBÎÌkØ5>TE­âi“u“ì‚Ú×MR²¶<Ù½ï×!w?öàsM¼ );x¡w¿”?þx®uwA/ËÑû¹k„Bëæ+íI»l~Woç7œ;tÆìÖp÷_:_ ókØ¡gï¸7ÆI¨õm›«q5g!™ÍæšÕ&•¤ î=eròû_¼ûj¾<à….]BtêiMKUÙl®úý Åã$ªØ86«MÊIúx™Ûónx¾e¹Õ Ü[ÿ’ûâ&Ok"’1רh]u…ŸXÒ»éä+ù&E¸µœ“qÉhÖè"ãvò0¦îÞ˜øË:Ÿñ Í<ëatç÷l¨Í#êNÉos«·Úkò½&_¦«ªA«û5µ¶n’¨bãØ®6I6ëÚ‡^½éYëß²¥çÊš|?ƒ¤¹z’±¬f{Íë™òM’o›Žmûi„îÙíä‚õ‡Ò š5q»æÇ’’’ŠÿÞ¹sgŽ\N:X!ࡾÎd5³¹d†êrwwÏÍËuuqmРÁîÝ»«;þ.O—;SQòËýIËzMBˆâzI’Jþ½Äñ­ôßËý±ÊEGG !rórÝÝÝ«Õ8N$IªzãÔ{m’9/;ϬÄN‘9eê§fÝ{C¸›œ}bí;ÿ[×þÉg<­{NÞC/L“R ÌZW7]‰ J£uÕ*Æ<£å'$§—‹|1ߤˆkgcÈ Àe~ºŽœ"Â#RRSü|ý‚‚‚‚‚‚¾Aó ò/]¾Y­Æqsus†­­êcƒÚ¤Ë«ü†m,üÇC7.yèêyèß4ÄךgÕû…zÎdæÉz0g§_Q¼[ú”Üë4^!º)i9r°F(†+— .¾>®v{0¥Þ `Ì€Ì`?Y–ÓΦ8Cƒººº†‡…ûûùWe §"6¨Mòîÿ݉äÜrk4aÞÖî`Í›ûìÙ¹eŸ§0ÓÉmû²:Åøj„9#ñ»ïÿöì3aX ðö-½Vl_ŸäucCmêÎÄsÞ±=BYA 8Uf0›ÍÁAÁáaáS¬:/˲Éd24N §¾k“4aš!„0dùûà™,ƒ\t±\äæa]iƒâõÌY·mÕòÉ=4¶ï v~!ÌBQ„°<¿>¼ëÞ¦u‰«Ø)y„µék|ˆ³G®_êëËÙ63!ŒFc‡‰NˆÆ)—ÍÖM’/®$¾ïÇÇ„V§×8|ÿtü§Á>V>¥> ¶ÏèØ>×&‰ànwMéVüþ-{jÙ›né R|ã,ÔÈR•d õúÂ9}´"dÖÙd4Ë´>08MÕÆQÿßé¦q‹öàò©*0M UTòhÉB 3ÀIج6ɳˣ£_ÿÏ´yÓï¿1ÂC[xÖxE7öbN õ©dDÈæ3d@Øð;ÝÃåÌÔÍŸNðiÉG{­¸´á6_º¥ÆÃGT’÷8+‘à0±AÔmRöŽé§o8=­[˜ÛÕ‰IËŽ'<¡P„z ¨Q}ßú|uœ¤q éÔ¿K”—‹®-£' ¦¸¤r,@fj—ÍÖMòì<©×Þ¯.ß}êüå¬ì"yfº81J. ÞØ ê¿6)kËÛó·o¼8*îÍ’:Ðý \¨³¢)h4uAf   B6[7É«çÜ]ɳäkÔx„yÓ'Çb«•Í G9å-Ôd¨‘ÍÖM2¦nú~ɱ‚ktm:f꘦®t‹ºßy:\¤€Jcƒ¨ÿÚ$SÆÎµ¿'æXFlæ¼Ì£;ÄaˆL‡2 &¶ûN·gþþg‰N|9|ðo˜dp6–){ðàä˜êDÖM‚ÙlݤÒ\ üÇ‚ÝÙô u¤Y8ÒØ^FäŠÂ¶æ ÞØ ê¿6IȆ|Cq!’b¸°çÛ/÷éšù²£\÷tµ¨²CR§Xu6‘ÃÏ¥°e™ŽÊfµI—Wô¶ñš‡Â†}öY'ú5±ÙºIÞý¾=z8§h¢AÒ¸ú†G…xj騗Ìé> 3¥bƒ¨ÿÚ$gdÓæ4>™jgƒÚ¤üƒó^œw0¿¼ÿr‹øâÄX7º5pÝkÌÜ‚ÚÚÒ¸W¨ë#6™P Ô&É9)ÇŽɽæ1ó¥}kÏ·[=ë|™#&À®a/É;… d ¶bƒ¨ÏÚ$./}ÿk‰›Ò7¼5aÌoÚ–æ-=Їa€8æ(8@ÕóöÆ1Ö~ðý P#-CÁ©•O÷jÖçµÌqKïúê¾¶ÞÒ€hMC̨‹í¾ÓMÉ=üÝ”øfC?Ó?úÛÑÄFǸsˆd@­±AÔóºIò•Ýsïl;þçF¯m>ºö•A‘zºÁ:\K«FJ¥€£ð9Ð9îg€Ù`Ý$Ñ9cz=´"½åݳ?{ £8´mKñiýc»Äúó% Ž=j'ç@f€}±ÁºIy‡¾_™&„8ôõÔ[¾¾ö¿´}~:¿n¨/Ýph\C™öD}Ö&ù]gbò¨c Šcnv8(îg€ÙxÝ$ÀŽÆñÎ\(ÌàŒ½2œ™íÖM@iÔ&A½±AÔóºI*–œœlùKLLŒ}¼cË—³Öfÿ 36X7IÝj?*”;æf,È °6X7é*Cæ‘¿žÉ2ÈE—ÉuAztb_q$"pNe/ŠqF 3Àabƒ¨ÿÚ$ùâúGâû~|LhuzMÑÑÄgðOÇìCŸ áGÕ™öÅfµI9}´"dÖ=ÆzpÖ°`Ý$¨‘ÍÖMÒ¸„ÇÆE®bžê ¢þk“<»<:úõÿL›7ýþ#<´…ÉAãÝ<Ú‹| ûEɬØf¾Æ‰".ú dØ;›Õ&)†Ë™©›?4àÓ’öZqiÃm¾t pR\;…Ù¬6){LJ‹ôÓ7œÎ*0–°v(áúqK©§Wá’¹Ã÷2ª¨ø‹Àéuy¨76ˆú¯M’4®!úw‰òr¡ 1Ï5ªïé…bž'õÚ;ãÕå»O¿œ•]$ÏL—8®Ë`ïlV›”µåíùÛ7¾9*®aŸw‘„U—éH‰Î‹Ú$¨76ˆú¯Mòê9wWò,¹T²öó¦Cêkb VÌ3@lV›¤ñ‹òü÷ûîеCÛ]ÞýÂòS^Qaì(5Ä5g2P»lV›$òþ~¹oÂÌS§¼ýå·_ÌœÔþÔÌÁ^ß›O—À Ìœµ>d˜ØµIPolõ_›”»ëãæ'×þþjw!„£Çöè8ô£]OÎëæAŸÀFã'²¨MñbÇNsˆfžjd³Ú$Ó¥3††ñÜ‹pkØ¥¡á¬ñ.' IDATÌE}Ôû™€ ±‚Ìõ³Ym’[ÌMÁ;?Xt¸¨)ÿðw&uoâæ@çNV¨&j“ ÞØ ê¿6É¥ùäYã¿îß2rNÏ®=sÿݾqàVOnÁW¼Pwá“"L2`…€€€úN …4}ßß•|Û’%kö¤ä¹v1}ì¸>M<8’q"À~úØ6@fªÎR›Tügý¾¸äѸϽÿëC/0L½8èh€2œ'6ˆú¬M*8¶dö’cåý—kÓ1SÇ4u¥O"Õ°Áô‚)cçÚßs„ùÒÁÄýB;ô‰oì]pf×ú§#½4è!ÇmkÖ¨8-®w`×lP›äyãÌßÿ¦Ó_ éþô®ïîlè"„¦”ewõþ$8€{  $´T¸i@}a­U¨76|§Û¾ï÷ÆÜ{k⌠‹0¾É¡•séTŒld þÙì;Ý\ÂZ¹ïžÿëicá¿ó,ùx§o§hnf»&I6˜œa:„Ú$¨‘ÍÖMrkÿß™»ŽjõFŽQn9ÿîØ|$ü±ß¶&3Ô"V¶)Õ`û‡M:‚^È pŒØ ê¿6Ih#FÎ?tü¾~X·'%ߣÿ½3oÖ5Ò pfÔ&AlV›$„’wjÛ†íG/˜t®š‚ô¿šýâ3/Î?”OŸT‰,Ôæ F6«M’Ï­¸³Ãˆõá=º4ôÖs=\³dú¤NlœÕèSÀŠ1@ "3%cƒ¨ÿÚ¤œÝßììºðà/ãÃkm ÎtñŸ?×m;œ‘/y„ÅvïÛ½©OùO-ç$oX¾æˆWŸ Ã[xpDÃ,à¨"Ô&AlV›$¹úDFùÔÞ~aÊLúuã ÷Ž #F h­9¼n;ËåÍY(ùg¶­ÚtÚL×2P%–ª$„ÏN“ú|á•öœ¹p%+»HžÕcyÓÅÃG³;ÝÔ.:4´qÇm½2&_)Œ»[{¦a¿>1ì‘P.ÎÈ Plõ_›tå×>MÜøÖÈ }}¼‹$¬ºlåó/ËÑû¹k„Bëæ+e¥]6]ó#æK×üú_Ï!ñõ¶>cA *¨{€"ÜÏ5ªï[Ÿ‹y÷þ|Orn©‰G˜·uO'sŠÖµènjI煉¯ä›áRøˆ’êÏ_vhâ‡ÝÜØCcʦçÕ§>ïëUÉ=Ä%ß· ‚h€ÌÕ²ÙºIÐ Óo‹Vì:›gV„й +õ„öžÏföð²öt¦¹zB“åRWøåܳ)Wr¯l\4ocÑCë,º0jìÁÚ’?—””Tü÷Î;;ݨ€rcƒ¨ÿÚ$ùÜò;:Þrj÷¥æ7DdîØs¾Ã”yá.VF½‡^˜ ¦ÂA¯l*0k]ÝtW/Ši}ÛÝ:¶Yáí¦óI«þïBûÛµ Жz"§Ë †ë ;Çý P#›­›”³wɶÆoïØ·ù›ûZ¶}òÇ]'Ž}9ÀlŠ Ò[ù|z¿POÃùÌ—~b׿}Y-c|5œ‘øíœ9?ÎåØdÏçN-Ý‚ÌØ<6ˆú¯MrŽó;øcâyáÛ¢¥qÛ¦ÓsÖÙs/äYý=ÐÚ ¸A=fï\µü‡ÕÌÍûjç§):Ös¸½€}à~¨‘ÍÖMrm5éÙ¸NCnnø÷÷Þzé¦NÍ>s9•÷U[OëŸRÛgtlŸk“Dp·»¦t+³7†Ü4aÔÆž :€½d8<›­›$t î\vü¦£Y¡áïlYßé»-—›¿oX”–>@M—@f„­j“„BãÕ°…—BDõºgZ/¡äü{ Ý­M¨ž> :czæ:÷3@l²n’’ÿïïï=rçíw?>{cšQ!„ùâönoÝþÑm¹ô Ôû “kÉ™¨„ ÖMR.®?ø‰’/ÿûó½»?•xåòŽw‡4ïú؆˜ÇžéêEŸÀnG]Tµ¿°WÂ>Q›õÆQŸµI¹ûý.Æþ²wQB@ÎÖGÛ}öÉ¿w~v´ß¬m_L½ÁŸÛ€3cžjdƒÚ$Ó¥”‚FýÚûIBxµÔ:{ãWYý±ïûÇ €Ì@@…lñn²QÖè,ù@rñðŒšôñÿnòg;ä0õ?2Ì\76Û¬›$„BçéGåÀ>±`j}dD@…lón¦sÛW~ïá'‰ü½ÿf_Ðü¶|Ù^BèÃ{ÜÒ#œµVÛâ›Ú€Ì”d‹ïtÓºI'æL3§èß;¿Äò7ÏA+S~âK·2 ²Ø ê³6ÉwÈoÙ”N© (÷3@lònlŒÛ=¦ÕЕlN@f€Ã³ÅºI€€A’!3ÀÞbƒ°áºIIÀaúQå%g*™™È °;L/€¡!ç9X-[¶Œˆ2PmÔ&9ݹ“OÔýЙµI`è@{È @¹˜^ê|ϸµ‹ÙB€ÌÔ3j“À@€óìà£G¦Í@f¬Œ ‚Ú$Õâ‚"dÀ¶˜^8z]7 3ÀaP›T‹'ìŒ@]ÒÑPmlÔ&¡VÎß`Pë»EªN†y¨Ó °ýP‹…ŒËáTÛ0›18è‘`w¨Mã 0"qÞŽóíY¥×MâÀ2PõØ ¨Mì}ð 3u„éŽëjíÔ ®µ‡u“@f¬AmàŒ£d€6ï26‰ °nÔµIE’““-‰‰‰¡5¨Kèd þJ"*¨)U]:Ub`_¨M‚Q›{ýØ;†n€­•^7 3U ‚Ú$¨V–&N×ÝM@f€Ã`z£-8ÖM™°µIdàú±AP›ÔniPsLD“€zÆô€:Ü0¾£a€Ì»Fm‡bÝLN¹ãT¯Žˆu“@f¬ ‚Ú$»Pë#.Ãñ¶jÐ#™¨uL/IÛy°nÔï†Yj“ŠÿtòÖ ²œN,“×%O-5y¤Ü“ÖèŠÏgÅÏ3º‚SÝ芟­Ü÷3º‚-ùZ•}ŠêŸŒGWá“^÷sUtš¯naAÉ>½úÏj¾RÏSIïTÞAUy‡ýʲ¥KG× +yõ²­qͳ•WíSî{¶®wF×øçË}é*¾Ÿâ½ *¯U•½»Š/}µkP*SŪÊѦÜbtõ÷¯ë¾Ã*înÕú®rû” ×!-i°Ì_ÑVWƒÛµíSj««â1¼òÏ5º ÛsMŽÏUùÅrÎ)K—޾ýöJ5×<íÒ¥–ÿ}½í°ÏÔê¬U“Z½¸º&¿?£ùù2Æ…­¬Y¿fÌÈ1•üÀâÅ‹ÇgןqΜ9S¦L©í]ÿÚÓOñÊŠª®K^¬âï*Já#]þ,þߊž°Z¡ì+–}õâ_)ù¢åž+ù¼½É꬗j·²o£òN©¨y«þAʶ|EÏ\¶ÝÊ6`¹=[Q›WÒ&ý|æë´j%¿XùVåÝ^÷ç+Ù´®ûº•ìVm'×ݫ҆×Ýò«ò„×}¡r?EE[T%;rµ¶„ŠÞ@å{PE]\ùg¯ÊáôÚg[¶lYñ(¶J[B%[]Õ»¸òcNå»yåg¹Ç·ª ªrܨè#TrL«äÍWô1Ë>XîQ庭T7”J_kÉò%ú ¨èW¯^ýÊ‘@+^”Ú$¨µI€#£Šì ™jĺI`D˸þ®á({ë&ÌXë&1fØ/h@Ú 3åbzÎ5rbô87ÖM™°µI)¨êöÆŽƒúß$œ¯¿È PolÔ&p’ñ"€êbz›ó¶'M vU°‚ÌTµIç‰è*]7‰!>È °‹Ø ¨M0º…úb™P¦Aå_ †"¬›2` j“8=ƒûÈ Àõcƒ 6 Pó¦*ƒ:@fêÓ ÉAf*CmàD˾1†M°*]7‰ƒÈ °‹Ø ¨MÀX`û§)È @¹˜^à0 ΃u“@f¬Am"1€jïãìæuFG@µ±A8Dm’éâ?®Ûv8#_ò‹íÞ·{SŸk’º’wfçŸÛŸÉ¼R »ø7îxs¯Žný$=@E˜g€9Îô‚)3é×'Ü;&Œ1 µæðº5û.Ë×ü€œ›žn isóà¡·ôiívfûož.`€£ùöì³ 3µÎaj“LÍ ìtS»èÐÐÆ{´õÊ<˜|åšÐ  ìtKÂMí›FGF·èÒ½µ—!=%ËÌ ¶°¬›2`}lö_›d¼t.Gïä®B­w˜¯”•vÙTÑO˦|“pñp¡4 Œk¿Aê®Mhm8ù.2`+ŽR›$sŠÖUW$½›N.È7•ÜVrNþ}ÜÒ²±—óî–œÕÈ!€SbÝ$¨÷@C,µIÅÚõg‘4Rñ¼,W8ì2_:°n㙀®£Zû”’’’ŠÿÞ¹sg¶F´ôÀ¾ǨMÒè=ôÂd(šXMf­«›®L표¼qebN«ÁÃÛùkË{"rB}œ€%ŠÂ`£Íî7?öÒœµIP#‡Y7Iïêi8Ÿ™' !„9;ýŠâæS*©Ë9ÇÿøiCF£ÃºE±Ê*ja|ÀÑØ»ù 3À8̺Iº€æÍ}Îïܲïô¹ô»6ïË hã«æŒÄoçÌùñp®" ©›Xs¯s÷îÙ™糌N9Õì/Τ®ÖMrŒŒ• ih¨66øN7mPÜ ž9ë¶­Z^ ¹‡ÆöÔÎO#„Y(ŠŠB1eef+ùÙ‰«NýŠ>æÖ ¢ôª?ˆS”Ù°_8K_s0™°!¸õ¹˜> ¶ÏèØ>×&‰ànwMéfù{‹SZÐã! ®y!8™eË–ñ P9j“ FS›v–Ê1dÔdûὑ€ZŒ Âj“ÔÊ©Œ®d ¦€ÀNDf*Cm•#jw$Áõ{!ênÝ$öÖZÜ¿œ~Wåh¨76j“N``›¤1ëôUXûUÃ<Ôˆé0:ÓmÏÕݤkñjŸÇ9wº:š ºö9—-[ÆN2PmÔ& Ù¡Z‰N„£6 ê ‚Ú$€q@˜g€1½À Ž¿™±¥½¸È @MP›pW×Mºî¨”¼ 2P*6j“È @¹˜^€ªÉ¥ÁzY¹ÅÛ °¬›2` j“vs9€dK£‘ÆAmPê,kÏ š™¨L/À‘‡w\`SUçÒì­È °SÔ&ÃzçquÝ$€ÌT76j“*$I´È €í1½`߸ŽX¶AªÕ&TË@µ'ê†]®›T§[[&™¨ j“0t2pýØ ¨M€° 2P.¦P·'9û}!–[@r™° 6 y΃u“@f¬ ‚Ú$†SØë6c2P.¦ ô¸„¶ãö]®›'Cf€Q›ÀŽd @ýû»™vµIàÀ‰ßNÇ@fêÓ PkùJýËy¾VYµ}Q[oŒ Rd >Q›pžÀ`Ëu“å£jt4TµIE’““-‰‰‰©×‰ó\übè u„y¨Ó ¥Äaäa÷C=Úœ-(ƒu“@f¬Am”ìMõ 6 ê ‚Ú$•Ÿ˜ùàPO§Ø¤ŽŽ€Ó`žjÄô º“€ÊP›d¯n몯–€ê›7]»ÑÖɺIì¨UÔ&A½±AP›v8àx˜g€1½Àð¢z¯Â8€=cÝ$kP›¤¢A?ÃqÀñöÁzÛ¯«ûBpµ¢6 ê ‚Ú$8À€êú Ã÷o¢î1Ï5bzuurej%½ên3lcœÛéY62 6Ô&Ó© pu²n@f€“ÄAmÀ¶1™ªÅôB½žËžk~5šëÙ°€¾æƒ×ÖM™°µIc>Úl0 yÕƒu“ ÞØ ¨MB]Ÿ„øà| Þ­Smðt`-æ FL/€ƒ$"®£¶@¶12œµI€ŠÎ»µx>æÔƦåaÝ$¨µIPolÔ&áºço€­×ÚÁ!¿’Œ.†cažjÄôP ã†,`{³¬›2` j“ vB=¨M‚zcƒ 6 öxÚìq3`ÓeÃ*Å<Ôˆé€Çß ÁÙØÈ P9j“8ƒÁAÞÛyµÚÊY›”u“8þ¨µIPolÔ&€¦ÐÈPæ FL/œgÀͺI 3Ö 6 €ª‡•\Êàd¨M‚zcƒ 6 Û&d@Á<Ô‰é _€ÌT†Ú$Õ+¢^È‘zÓù°nÔÚ$¨76ç¨MR Ò÷nÜtü¢AëÕ¾g¿øîý¯¾áŸ`“œó P#gš^0¤lýmë¹ ®·ŽÖ»qÞß«×Ëåä T6¸d|IK:\/°nÈ €5œ¨6ÉpîàqCd×î±Q!áÍâ{4ÓžÙšÐjOSª ]$@à̱A8Am’œ—yÁèìç" !„Î'Ì[¹|6ËLÿs€J9Om’bÈ5*:½åIﮆƒÌ&`‡£IF¢ Àqq4ÔÈR›Tü§ƒZI*ÎîŠ\þ‰hΜ9¥™2e Û 8ÖM™°>6'¨M’\<ôÂT`¶$ÅT`’\ü\ÊÌÿ‘€ Q›5ržÚ$GP€>7ã¢A!„éÊÙ,É'Ô[Ë&N„u“@f¬áDë&éCZ7q9³-ñŸ”Œ´£m>jŠhÝÀƒïgjBmÔ„S|§›KD·Á7n\¿åçÃFWdû}›y™¸§¸õ¹ˆäÚaà¸ô:P+j“ FNT›pz¬›2`}lNQ›@fªé€ó`Ý$kP›@f®µId \L/€ÊP›p¬›2`}lÔ&Õ9sæÐ€Ì»ÇôPRRR@¯Á»Œu“ØÑÈ €5¨M‚’““iz tè5à\±AP›@fÊÅô€zèh¨¥6©øO¤vo\æ6h{´cÇ^ƒwGfv´’¦L™BfªµIj=p§BmÔˆÚ$2PÖMPj“ ÞØ ¨MªEJÞ™ë6í9}Ť÷kÜ©Oï¡®’=~9/}÷/?Ž9&>@kùdé{7nH:~Ñ õŽjß³_|wÉÞ>¯áÜž?·<}îRžYçÕ¶{ŸøFž‡øhæ‹ÿlÞ¼÷dú…£äÔ¤C÷›;D¸IB˜.þóçºm‡3ò%°Øî}»7õ©ðóªùÒÁß—oJ ¿åž„háÍpê×ù¿œ4_} |À}Ãb\å¬ä­ë¶HË‘]ƒšwí{sl€¾¢Ï«fƋǒw:}!_ön3bìM¡º ö©ò?¯JŒ—ÿ^¶(ñÚóehŸ Ã[¸çÛûGBÉKÙµéÏ¿O\4ÿÆnêé.9Ä‘ßÞhƒ{ÝU“ßï˜×´iSÚµ+ //¯*?™|"¹Ml›J~`ÿþýmÛ¶%1ä&¯ùqKvLï7µ É;¸%éRx«Æ>övÍÀœ‘¸ø›ßÿNÉ•]#Ú´‰t×!„!åÏ×¥…uØ»c”œœ¸-Å·e³@]ž]}^9÷䞣¦†ºtjÓØûÊ?;wõmÕ<ÈÅèÍpvÏ?9‘íââÚ·ˆÐÿ?{wEúÇü™Ùb—înE@JP ;±[ûôÎîÆ8»NìöìÆnÅöüÙ]ˆ ¢" Ý˲1óûcQQWE]àó~ÝëÎwgçù>³ÏÌwžï̾}pëQ®¥›½6›rãàñhíj ª;ân^ŽæVt6ÓPÙ^žzð™Üç_N”+äÚN^u9D^š¦ÈŒ¾ÿ\è×¼QÕÊ.....®Ž¦ÚB:ëá±Ã÷ˆ{ÃÀÝ´»—Hí\­5•íUßNc2Ûw%ÓÌÓ××Ç­’­‰¾Ž&¨éLUíU×|ˆâé˜Û:89»¸¸¸¸¸8;êe¾HàVôr¼*õMcsžž8p]êÖ´M“šfy/^‰Ów©hÀ–áñçyüä±£½ãOœž?ÿ/Uô«Em¨#Ô&•ô +‰{ü†v¬éëdabéâ`%{ñ(1¿^ãðï2 _׆….iRÄ ©¥_ W+óŠÕkVä¼y+fJY{im×&-ëyW²µ´r¬RÓÇIŽÍ—¦iØ7lYßÇÙÞÊÂÆ¹ª¯Oš‘-cåéO£³ }jyؘšÚW©é®•ñ<‹QÙ^V½¿Wo®½”ãÖ´¦å»óÿ²Ò4B8B³wLõ4“ýòI²¨r-Sk·ÕŒ³£"SåªÛ«¾­’¼ºz+Û¹y«:ŽVæVV&Bú ãêöªq‰ Mßõ—‰fæ‹$~Eo+AYhš<36Ynàâb¡Íçk[¹U6P¤%æ(ÊÄðXê gõMj“JŠ";1“Õ2-¸ÜÂÓ3Õ”§%‰™2Ð2&/%M&4ÖãS„ÂÕ1Óf3ßfKKq{Yy¾œðE|ºl5•ç¾}r?Žcëf©AÉ2’ryúFʉ"޶™.•)UÙ^…ú6I–|÷Ä™76›x}˜1(M#„°²˜£ëV­Z½a×ÉÛñy,!Šì·ÙDÇT‡C!´†¡¡ /)=_ªª½ê{ú)MŠ|£Ðß=°qͪÕëwž¸/a¿0<ÊT¶—- cˆ4þöíCs~™hG×ʈ“tûæó aóSãrÖ:t™ùKÜÏê?ËPÒÇ©XÊr\å™ ÅÓà©XÊ”«¬T,c¹|Þ»– yDš+U”Þöæ'<ˆÈÔ©ädÀaÓÊLÓ Êã9&Þ-ZTÔ¢™,±Œå¼k¡x\&K"ËWÕ^u=Ü39Qg?Ñ«ÓÖ×B@É>,–•þ¦B¸ž ›zŠD½n¨)£][Z Ø”26ò#gø^øM·Ÿ€¢hªàÀ2„-[-{ŸB0,[ŠÛËä>»xæ© Jo#Q”¦ñÌkµoç™ÿðG’6íªiò¡ „0º½êH‘— Î’œÜù~ѱ {k7,ýM#„ÐZ–Z„BLL Hü–³O¥6íx…¡º½êù“Kä”®[7{=šã:¯6Ÿ‹L̯ò…ï”êöªûU‡øÛwR «4²à5\”¦¦±ùq·®¼Ô¯Õ¡¹~rä÷ï_¾oÛʇS–F~ä ?œ6Ô&•ØY5_Ä'™ùråõV–/£ø"]6ZÆ#ò|…òÈÀÊóå_Ï)íe%o®9ŸhÕ°muS~ÙjÅÓ64Õ64ÖÎÞq'2ͧšˆGäR9ûîD._ÁhðªÚ«®-ãû¶ëìU0U K¸tð[³]=GmÞëRß´OÏD".›)•ÓZB.“/gÞ·LNñE|>WE{¹êz)žæ8¬,O¦Ü\Š«©ÅgÒ% OÕwŠ'PÑ^^)˜d¸ÍVhî¬Kyä/]Mcr¢¯EHÛ8p,lônûïjŒK½23<–&%¨#Üú\Â8Ú¦ºtöÛ,eµ4#)—«o¬Y¾þ´ÈÈ€'NN—B‘g½Í¦tLµù¥®½¬$îú‘“Ïê¶©ï¨E—©¦j¤,OÆÒ4Exz¦šÒÔ”<†B9‰Y¬¶™_e{9ê{*­«ÿž6Ÿ¦yZº:º,4†yy–ÉML‘òõuø\íB·a0’Ô”| #_U{Õöb$­ebÀ͉KÈe!„•feJùº:BUß)žÊöªyÎÿææ4ƒ*>‚¢FþRÖ4™¤P%ÐÕæ±r–µá9ÀwÂs“J¥aYÙ’y~ùFtBrü“«Wc¹v•Í¥¯¬BšŸ/•3,«Jó¥ –žIeþ›kWŸÄ%'Dßø_´Ü¢²µˆ.eíU¤Þ:|ô×£¦»ž$599999%3Ÿ- M“½¹pèäÕGÏcãß¼xxé̽,Š• ¸\''ÔÛ—Æ&%ÆÜùßÃlgG]Ze{KÛ£ÕËDÓ$/Ã÷»üàyì›——OßH×uu7áÑÚö®ÆâÇÿ»“˜ûðòÍÍŠÎ_êJµmßÜÓY+åú¹[/_Ý»x5IÛÕÓ”¯ò;¥º½j½ó1מG?=ºÈ‘¿t5Ö±µ×?¹|çUF^^fì½1 SgkMAYùKü>¨)åï3|õWðû ߘ4ðô­™Ø{×nÜ{jUD IDAT'·¨Ö¨n%=n©û¥yò•;NÝ‹“éÛÈ{wŸæÛºÙjò´-ÌyÉ®_»ñ2×À³~C>UÊÚ+O¹-*-;áÙ“ˆwRŒÝõK}Ó(JžþòiÄ£Ÿ<‹Ï7p«èk%¤ -2µÔÌ|zóêíGÏÓEÎuW³Ô  GUW–†“ÉzvÿëèUQ—S6šÆJŸG>yô("úMž{½ÀêVBšPck}ñ‹ÛWo>ˆJâØÖpÐúR{Õ÷œGÛÂF;çùë·>KºÔkâg-¢U§T·WåÇ^:õ€ªÒØÏêÃïc”…¦Ñš¶úÒØû׮ܸóð•ÄÔ§QCcÅ)#ÿÏó“~Ÿr™þ#›5Ù)500'¸P²¾ýÖçSçNµ*â»ví FH l`‹¼s=l_Xãú¿ô·ááá3¢ ¿'CÜA ¡6 @} gõMž›€œ@%L/ g( j“3|=m ¨M@Π¦3µIȾž6Ô& gP Ó ÈŠ‚Ú$ä _Oj“3¨„éä EAm”~¬8öi’LV€œÊnÚ@P›¥‹üÕŠ*<ûûBdÏ–Ôõì÷_ö­¯ÐJ²ÏtÐ7ûór.‚ ¿!5d``€lJ5&/%5)¹•=Fo^ÇsÖ@dàwÀ<¨#Ô&@é–q,¸á¬’KÁ«M{ !ù1ûF7tФ(Júΰ°çB˜ø Õ5+÷ïá)¢4<'ÞÉͺ³òk-EQ\cŸ^ë#ó>^IúƒÐž®Š”"}u`l`-Š¢¸ÆÞ]_Kga·úk¹ÙÚÃÎÂH[¯Bó™—Òt g€r6Ô&@)¥×|癉µvFßœâÁÜú»qp˜Ù”Òœ'+¼NÿÑdò-1!„ˆ#R“î¿:0ªBÄßmGÝmö:ŸUdÞž¤6l̹ŒVòn‚!ïÎÔÀ †®¤åç½ÚÝæõ䯷ÆÊ !$÷ÉþŒ?ŽGÅ'¿9Ö:bjÿµÑ¸ 3@™†é(;Äw×lz]}Öüî•uyšö­¦Î©ûvëš»bB×¢Ó Mm ´ÜBÎ= ñ×Sd&Äe‰,´ó“STV6å=Ú¼å•ÿÜ…½<ôù–õÆ-ltaù‰!„gÕyP ŸPºžÍªé$?I”"ö€œÊ2Ô&@Ù!Myž"½ÜÓ‚CQEQmNäd¾LÊ'„põ,õ”÷RòÄsÿ´wÒâé8Ôì:u_T.˪.-’§Ædj9Øj¾y¦Î&TÚët9!„£i¨ÉQ.æðh–aYÄ3@ÙOj“ £ þËÓ·6Ðny$URd¿Šz²«™žò5ÊI#vìÜme¤X’sçØ¬z†4!lᕼÃ5°ÕÉyñ:§ £¾xËØèã‰&€œÊL/@©ÏxB¾"52êM¯J¿`ƒS£&î‰ÊVÈÓo-kïîÑuw¼¢ð‹y鹬ÐÀH—OÉS®¯š¸.V&“ÈÙB+‘LÝ{u³¾2zë£ Y~ÂÅÐa+“úšsp@Îåj“ ´ãÛ4íR-n²¿ÇÀÿQþóNolòdŒ——gPgȾ=Ù43 ô»bˆñúZºm‹€‰/[Ïl®ŸpûU^ᕈ߽ÔgÚɦùêó5lƒZM ßÛÛÓ ð“Q.SÃäý“RGø]N;Ô>¨ˆìÚµ+88€²-òn¥°}aë7þÒ߆‡‡Ïˆ2üŽÅ<¨#L/¨ä  ŽP›€œàëiÁs“3¨„éä EAmr€¯§ µIÈTÂôr€¢ 6 9À×Ó‚Ú$ä *az9@QP›€œàëiAmr•0½PŠ0)gF7ªÙné½lÁøÒç;úû×î·ã• 9@1 6  ôd ——„^²¼h€—6Maó^žY2º[ýÞ~íæ>”Br¯ñó ðþðÏ€3Y§Ë —7ŒëÕÒÏ/À»Áø+¹Ÿ¬9íÚú‰=Ú7­îà]§}¿…á¯ò !ò„³Ë†õh[; ÀÛ¯Q»Ñk/%É !Lêñ}DÃñ·òŠÚêìÇ[CºÔò ð®ÝnÀê멟f;ò·ç–ëѶV@€w@³.S÷GŠÙ‚¿Q¤ÝÞ9³w›Þ~~Ž%2„Eê £º6÷õ h?bùådù»µ°Òä«K:øõ:”XxýL^ì™i ë ¿”£ü¾cçYÓk¼Z>÷x¼BM;™‹ýÔ6m ¨MP{²×—œ×ì¼¶µ-Bd±{‡õY+iügÈ"gC*_`ÅS¾ŒâyŒ è&$„BkÛkBI§§uŸõÒïÁ ‡™ró){Á'«N¹ÿ„ñê4ªE=ɓá˦O´vßÜQ?þîsa@)ƒí43ïl›¿~Ìì ÇÖ×#„ÐZõÆÎîîÀW~ž¡ƒà‹ÍæÜZ4ÒUÀd]_ÐwøÇîNh%”±&:‘Åî1j4aõ$ãÄÓK§Ñß±º«5W|{ZËAáé„çBŸœqn`›I×%„ÐÕ?,¥ ëë¿3h݆&U!gø&ÈJƒüg‡Å:uª$$„6óò² ñ-† wפ>9ëÔst÷ð*|>œ÷øß¥÷ªÎÜ>½¦žêÒS¿Ð9ö´x{üÜÆ˜t±ð½Ø§`ie½'G»]ŽÍR=B%0qr÷r|}«Åû.äV7¤]5šØ¹~rÀÞ멚Øa•Q˼)e\5îœêw÷qŠÂÕøÕžN›ß:»µç}vsú`„aûm}:ó ©09îr·°¯;öuùL9{-$riP÷;>™Ò«¿êÂñ­¿GdÞ"Žiý^~+'†Ýê C©]7£6 Ôj“JÙÛ+WÒìýL•§Ð¹‡nIõR·õnRË; qÛÑ›ngå°yÿ ªàØaܦ[ !Dò<übº6{bL“Z>uÛõ[v)Q®úCXyvÌaûßÚ¶lá ña©,-êô¶³9î­ëZ('3©a½ëyûÕnØsZXdN÷VÈÓ¢Ÿç¸ØjÑ„"°ò0gbÄK?z EQï?(7[ÊÑÖѲøÿN¿Òм>­c€_€_‹¿fˆ•&7=—ÕÐ*O«y&nŽ‚·ãò‹IJÛ³ieæÞùuìgÌ3€ú¦ µIjŸ3z›¯àˆe,!Å›. 4í«˜æ^ˆÉa¼„jwY9¨#Ô&” ¬4+C®í®SpJÉäeæÑÖA=Ú׳áRiÂÐËÓ>ÈnÔÀħ¾ !„WGönàÔãrVÌ”ÐF5{7tâj;þö…A'®'u²¶à||¶jÕvÑŸ7‘gV„þ9ž·y~  ‡"¨Øwņ¦o\<¹ç¼vޝ®«aPÏŠBˆ‹«eö•öë=“øW~ùÃy7“À*öK ?Y?aùk¿ñ;êÒlFnŽBàÜ«YuMBÜÇEžï}úlì€~c¦·75¤Ó~BˆvWšá¸hñ‹__ÄѵУÅ)9 b¬v9j“@¡6  y_ÅC ´¬8U¬, ¢E&&šLv†¤ð 9¥ah$ds³¥D %`Åé¯%CK*7MüYEÅÓ³uò¨ÑjÀˆÚŠÿöÜLg Vclïì]«Ã¨nI'÷>þø I3=:?3ï‹ÕI´¦&‘dç+_ÀH²ò)MCç³×I_ïŸf}®_ãZÎFò7ÿÛ¸ì©~ƒ®ByüÞQ!wœ[Vµ&Ï.ß›ê:¸•=&óáÁWÕ:µ¯ª_èZ¹Ð¥C]ÍþËWÔï`Ÿzrþ)‰Oˆ¯MÄ·§4tÉgñ‘…ÕóÏMë7ó©ï„¿kð⢞Bh‘­u`°ÏúyÓC=¦t²K:¶à°ÄwJCs+y}ýV¼@$}yióŠ0iÓЮŽ|BQHrĹ9ù V!ÉÉÊÑÒiòhÂÊÅ9yÙb)ËÊÅ9ÙÙ¡–P™±¹1w5+Øk©ã5}ä  Ž”µIïÿ€¨mÎàïoðï‰kImÛšs!"ÏA‹¦äÏXÒw=eèÙ~Úª~.B*ßÖ×!sǺ »r|óêg‡öuÖ „ðz†ÎÊœ¾hjÿ}ríŠ-Ç-ç«C)aÂ2,!´¾‹ÁÙýË&®ÈÒº [ÒȘfiowþÊ= ö%d˹†îÍ&¬QM‡bI¥*&G,™°5—ÙÕì³|B'.!Düpëò£ÚÖíõñ‰8¥å3ìŸa³f.t<—gZ-xÖÌ&&4!„e¢PBÙ1Ï2™·§f<õî=&]7…rn9wAÊ´yKûŸÌÙÖ¸$¤‰1Mñ“í“BnJi]‡:Ìîé¯LOroOk<ô¢„B–tl²Ä~Ю°î6œÌsƒšL½O!$¤M qŸrfm E›ýàÄCÚsZ¡'C©Êejø¼²Sj`` ¾/ð»œ:w*¨}P/صkWpp0ð“È^nêÒýxÝõÛ9ñÕnã$f·ë/xÇŽî6j~¥\ñöpßÎÿ:,Ù=ÑSXô+Y–-âoÃö…5®ßøK>#Êð;6÷3€:­ϥϦíÐú9ÛŽ•©_>óöæ­<÷®Vê^Zä_Z±:Òõ¯ÞnBõÜ@ä  ŽðÜ$€RƒÖ­1d¤ÿ‹ÅÃVßÏfÔëL<íÁµÜj]ê«ù¯4fϤ‰­ojÎQÓMÄý  ¾iÁs“JEÖ`ÔpÁé†j¸]¦­Ö„·RÿøñíƒV\Rï.Æ^jÓ ÈŠ‚Ú$ä _Oj“3¨„éä EAmr€¯§ µIÈTÂôr€¢ 6 9À×Ó‚Ú$ä *az9@QP› >¸¨mÚ@J¢6©sçÎ,Ë"žß ó  Ž0½€œ (¨M@Îðõ´à¹IÈTÂôr€¢ 6 9À×Ó‚Ú$ä *az9@QP›€œàëiAm€Àï@ƒ:200øöl!l_"€œÊemÒûñÊÆõ#\?j“@}ÓòmµIŸßó€%X‚%X‚%X‚%X‚%È Œ+ÖNÿù ÓX‚%X‚%X‚%X‚%åvÉÏ@¹L ÿ‘÷OvJ Ä9.üŒ´á[j“T¾ K°K°K°K°¤3€ú¦ ¿é€œ@%L/ g( j“3|=m ¨M@Π¦3µIȾž6Ô& gP Ó ÈŠ‚Ú$ä _Oj“3¨„éä EAmr€¯§ µIÈTÂôr€¢ 6 @}pPÛ´üpm˲‰É‰ oÄyâò4‘PdnfnjlJQ‚ƒàü¤à¨ ×È \300(‘;“SÓR=Ü< ËCÜRÓR#£" !f&f‚ó“‚ >0\#g€rMY›ôþßß½ž„· nº:ºR©´<ÄMWG×ÙÉùÁ£ß2” 8Î÷@}`¸FÎHJ 6Iœ'6Ð7ÈÏÏ/'AcÆ@ßàçgçû‚ >0\#g€r­¤j“”Õ~,Ë–«è}c#‚ƒà|_pÔmàÂpœÊ©’ªMRŽ#åj()Vcçû‚ VÆkä P®ÓòõI¡X–e¦\ %¡çç@}`¸þeðû  ŽJêg(ŠbJ„äé²àƒÏg(˜ŸN w³Þ›^Jßÿ¡¸køöòæW?\кEÈåì #‰^Ù­E¿ÉßÝ¿%8™ÿnØqñüŸá÷ñ#Ÿ…Ú$(}9Ã/<–©Ô&(ÁÚ$RÜY<67úĺ¥;ÎG¤ÊOÏÎͯeÏÞ­íY–Lþ‚ë¤ðgýäý®•K£×õx»í–å­Í9„Bo÷ þkÏâ-šÝÛiŠtøîE ©ÂŸòîÃØ’ˆÉ¯š™ýŽOaóã/íZ¿õä­r"4q­Ö°c÷ö^?¿'í^ ^JpÈcÅ1§·nØyæÞ놫kïÛ¤sßàV\RAÎjœ6®M"TqË™´ÿBG.‹¬ÜeØ?>&TÚ‹§Ÿ¹›ÚÜî׈©:ù+^þ·ÎXRß]ª|W¡w³ïs*¾SçQãÍpöÇr®BÁ~ÿŠ~zpdñ‡§]úĺeŸ!NzŠÄÇçø÷hµEnägÜþØ^SÌਪD‡WéË!Ã6$¸uê=i¸­0ûÙ¥]çô‹üï´&æjsÊü»†kä  ŽJì¹I„*æP’}æŽØyè˜.uô)Bˆ‹— 8Ðl Kˆ4éÚºñÛ÷91ý:ÈÞ„¯^$íïmÊ—'_Þ¼xšɌ®c%Ac¡|C~ì¹ÕK¶œ|’&Z ÝÁU›ú(kûß²+þ÷"EÌ¡…oûÁc‚ܵ>„‚üHòñ“ƒÃ¦]Y³æž~çE³{;iP„ŠNžãXîËñRÑü<»‰?0`À)¿¡õãvﺪÙoKhcãw•¤ù/¦Ì9ü0>KJ¸úN þÝ·‘%ÿÇöšâ@Rªär&念[¢mú¬ü»³ ŸBœ+W©(è;r㺻5&z$¬ús̽z]œ>•©a[oà„ÁM¬ø„ɼ¶lÙþë±bŽs`¿Ñ}ê›+îÌî55¯KÓÿ¶œˆÊÒu ;¡·—nIÝ€û>PV%‘¾±¢(…¢X¥ï”¦‘&I¸™!/XÀÒ4˰„È£6íŽsïþwÈ_~Y'®»ŸÍ(2n.»ü¾U׫—Ïè»~R虫j6ÌËûñy #y}åN–<úbD†‚‘g<‹Ì5«f/d }˜‚áz´6-tåâÙ#ü³†®¹™¥`å5|¦ÐŠA¡P|{É~1ƒóËÂ*dÈ,!,Ã0 KX² #y¾kÊ´£’Zƒ§ÌŸ> ‰-ŸÂ2Œ"ãÖ¢ ë^yô_¾~ÝšQþ);g-»•õñF°|ój‡ÏX¾dÁÜ>Î1ÛçozšW8,a‹“_EÖãS÷¥Ní[:ð?l š™‹È»ÍWÙ|¹ÊÝ€eXù›]+ÿ#>ú¶sÖ,Ôd–ÖqnÜ{JèâóÇ4ã]\¸øB’ü÷šâ@r†8–©ÆÏ>V8·jhÉ}?â ì[ÚäÞ>ÿB̰„(b]’Wï1q\ß:Š çy-•¾Ü7uêaÛ)«·¯ÝÝäjèìï¥ Ë2yw6“ œ4¦—Óë°…aQ’»™áw טgõMÈ×&»Jƒ_¡ÓŸ ï-ø»ûÝ >U¼=½ýkTw6âS,ËN¥¡s&6ЧˆÂ:áäÅðÈä|û—û¯ÈkLXY‹»^ƒ£¯ßwM2ªŠIæÍç™ »¼[¸~µMž^Šû»Æ=LÔvqÕ§?ÚaÅv]+VššJWõ5;ö¿è4¹_yÖ]pQý{j“~Vp>úæÕÆ>í7^j\¥ÐVç=;p<¾ÒŸ«ÿl`LR‘wsëùT–(R®…]Ñ ZÙ¹º9‡ãv½ªž˜{!&ÏÛMøaø×óiD“Ÿ™šR¡š·þŨ×9Œî'¡øÚ¤ŸyÆëd¹ž›µõéûÞÍ˨n¾ÄÛíóÝÀ›KÇ´óÜz9ð?ÙvžuãîÖ„•‹ÓR¸>6a»Ÿ&JØüðý ¸J©›ggƦ(ô<,D…GDŽ®9?'>QÌØ°„ë:dÚ躺!>z/®N8#Î)öH’ïðéÍ\D„˜5êÚúàà‹·SYJ£úøùêj"Õ82öÉó,EECº¤Úû[⌜ÔQ þ¦[ÁÕÞoFÔ±¬r«{7oÝ»ûÈâÝë ›Lš;؃e ÅáR ÃP„t„”ßEú½+Öí»ŸOièˆ$rÚR®œbßÍ3ÌÄü¬à¨8–©Æ õîhSxtdYBÞ€(Vyâ›V2¥n½Ž{šõljÏËŠBX™Tfœ’#·d)ZyÒ@è )YžTQRσÅs“>(Áç&}ÏPÂÑqðiààÓ S¯”SS-Ýv©Ã¬Jï+>(ònLaò® G9T0„–áYzÛ)v<ˆ¾™Q¡ƒƒµ¦=ÿJ´û« 3_kÁG›¢H<½àŸtÇ +;x™2/Œî·çýiñÇ(^Îðsƒóî˜Ò0²¯PÁ¬ gHŒÐ  ò–Â(ä2†Ð)X¿²d‰aXFÎr].í%z?Üó´4 m„ìÕ¾+®Zþ1sK“JT|؈ÑWØB¡`•§Äߟ3üÄàPZœÌ¯³äŸ^Kb”{†êæ ³NO-r7ø¸¡lÖ•åóv¾­?~Ù\?KQþÝ9=–¼?¾½ßYXæ»rŒ<P•XÎ@iYp2^Åå(*ðÞ½Š´ 2M{c –)t&@…œ¡8|JÁrŒZMžÞÁš÷n%‘6'’°äÝ%$F9ýû.5®q?¨oÚ@~ü7Ý(ŠaöÛ1’¤Ø4éûÿ¥µ¬,4‰<_þá±?…¦@9úŽftbD\žò$ñ㉱£!Odçc’víì™xëG ¾µŸ‹ôúñ3o´Ü+êÑ}˜ôíã8âÔªµ—±€°¬B¡¼@ünƱЊ£¸—ÒÙïCØO·ìý¦BKkšsÓ£ãÄLá·°ãŠÆLÂÓtž¾^Á?ºBNá彉HÒ¬Ò®a%}.aYåUsòQ(¾#(¿&8Dä\Ç…µÿ`t¡V+rrÛ¬²ùÌvƒ/4T–ùJfפ¥¯…fÙ‚yÏfûSƒ >~èXöÙ0^©Ž 'òÈ©×ïOqtø±X¡WMþGgŒøÕÃŽ…³­0ýi<Ñy7ªëëhñéB|?,¶¤`žàƒßöÜ$Eæõ¥c4l]×ËAŸÎˆ>¿ålŽc—ª&´äÃ{ÞÿB§å= IDAT›h{¶öãÌ^»æôÀv•¸1§ÖÏuRݦ宮¢ 'ŸWRA“°”m@ÅÌ9×Hõ¶fÜ·„kèh„Ùÿ?¡'ÿÍ¥ÝÛ¢ä^ïÎúÞo6«¶ÏMb?~nRáçö°„ÕphìÍ›±~õ^¦®iæ“Óû/d+!„oâ Ÿ{ñÀ‰ëŒƒ"úì®ñr‡Ïb¢®ÏM¢ôý{u=9nóßSrƒ[8qÒ¢¯?rÏaì\Weꣲù­zªÚ È—ž™Êѳ7ã:u蜹Ÿ^ê­}[îH¨:ì§·¿°ÏM€r’3”äs“(ÿ^]NŒÛ2iVv÷V¾6‚œçWvo;•åÕ¯—§&Q°,‘Ç_9wUßE'÷ÉáµWx5&zV0ob6büդkÊZyq.ž}YuD_MòÑIùÔg¸FÎê¨Äj“(eâ7KiݪÛÇ>»mÁá†PÚ6þAú¶°à(^úÂ8OÖòì3ýÏËvL’ÍŠ¬|»Mê[×€bYž…—=ÿdzMm²¬Ð¡–çZ^Á'B›5ìß+bñöE3öê;7mÑÜöõUU—ì‹7Ê0 óí?AP¼à|4^}¼iŸŸÔ-ï>#:,Z¹uÑMži•fõ=£Ž°,ËRº¾Ãgôß´~ß’É{ò ßÈ©z³`N¡àÙµÖîåâ¡“hã*M76 {öQ(ÈŒ¼?=8<ë¶“çêïÚºwßÒS¹,šºVìÝÄ’óêÝ(¯¢ùÆ–Ž_Ü Tõüûö¹ºeéÌp-‡:íZ:Gû™ɪŠuJ¾÷X¦zo÷÷½[ön™{PÌÒÚVU‡/êägB³¬œBi7ÖÏØ–$ÙÖê3µ·»&¡ì;LžÂݰiçܓ٠Ѳ¬R«µIùô¤-Á‘ø]Ã5å25üGÞ?Ù)500{,¨§Û÷n»¹¸I¥ÒòÓd>ŸÿèÉ#/ÁùIÁ(zé‹õæ<ë¾rv€öï½ÀòƒÃuxxøŒ(Ãïx#æ@ýÆç&•v¿ä¹IN¹€úøuÃuÁ3>Jî^æÒ6\#guTRµI¡ E¹zð¼B¡øö’}ÁùŽà¨QÎðˆë‚O`û¡áw ×È@}ÓR¿éV’eŽ¥Aq'Šà 8ßµ¾~ÅpͳëµlcAÖP.‡kä  ŽJª6I(Šóľ œœüQ%Î …BÁùyÁP®‘3@¹VRµIæqñqzºzò7I¾$#3ÃÒÂÁAp~^pÔ†kä €´¡j“ôõô†Ix›ŸŸ_‚&ÌÍÌõõô ‚ƒàü¤à¨ ×È \+©Ú$…BaldlnfNÓåâ'φ‘Ëå2™ ÁAp~^pÔ†kä P®•Øoº"“Ép&„à 8®áGШmÚ@Jâ¹I€œÊ 9À)«’< g(*m ¨M@Π¦Ôž›ê¨Dž›ŽH@™ˆœ dj“‚ƒƒËOÄX–¥( {ú±B¬±B¬ÊCïüâúmÔ&:BmÒwÀ/ø¢+Ä +Ä ½ƒœÊ<7é;°,‹  +Ä +Ä ½ƒœÊWÚ@ðܤâ`A@!Vˆ Vˆz9”˜^øž„ÅøŽ>B¬+@¬+ôr(7P›ô=#® ¡+Ä +Ä ½ƒœÊ[Ú@P›T,ƒÚSôb…Xb…X¡w3@¹QZ¦äovt¬Õu˹:lL±f*³Î¬Þdî} ö5õí#Ä A@¬Ô-VŠøÝA5ƒÖÆÈ«—wVýÚÃ/å¨aË“ö÷ªÓjí )¾õÃï3€:*‘ßtS•˜ç¿ùoÛ²ÍG.E$IOÏΣnó ^jØÕôÔ999GŽjÖ¤¹®®^±¯:dïÓxâÝÂKìû¯m}¦ï ãe'¸c'ûŠê#¾‘k½àÑ#ƒ¼tq)GU¬“~wç¼EÛÏDe0DÃÄÉ»AÐ࡜]Ý»kµcG°å—f¹7BšLà,8<³º¨<íWMf/=2·ªð§|žäáÌ6ýögB1î´mïvyÇÞQÃN­n ûCéG=•wgbËÁ]vìîmÇûeßA®Ž{@Ë.}ºÕ´ü„c‚øî´¦³?[nØnÉè˜a!ùO­knD—’ñŠÒ²õk?2¤O-u9d%¯O®š¿òðí¸<ÂѱöhÞwh7_º¼|ë‘3@9OH ×&I_ìÑuÉS§öýg r1`RžÞ<³wó‚­^U'ºk¨c›³s×¶´ô´»¶w £[ôëÌgOk¦yU‡ÍèRÐ:ZdaË«0Ó@ÃI€ýë÷PÕG|ßÑK†:Ó™±7v.YÖ‚業Ì9Õ§±’Çí1dEJó†{Ê^<¸ïDÄü~a¿ú©4*Ù·¿êèS3ͪa,`Ÿ”øGðí:†LÉ´3âüªïàg’õ6úÊñÝ«Çt½cùûþkÖŽéXÏÇͳZÝNgîÜ»0Èš›qº¿o‹ÙwÏêÒ°v»uÑ1&vlÖÀÇ/À; IÐä=OrYBÄ×CÕ°nõÄîµý¼ÿœw!ñ]A’"ýΖíëzûÕk;éÐËü’ÙÚÜÜÜ]»¶§¥§Q•••¹3l{vvvÑoQuGÏÀÑÝËÃCùG#êŶ© OÅË?¾:óòø¬¿ZU÷ ð®×yä–û™Œ²æªóÂýë†n“znVï& jzûx×¶áNCH~dhËÚ]ÖíšÞ£‘·_½vSŽ¿L¸²t`˪~uš Ùô0‡%„&ýƆqíxûÕjôWè‰8i¹Ý±UõWׯÉÅÕÝ/°÷ô±~äÎÑ;I¡-ëôØyrÅð¶Õꎺ”Ëd>Ø1®k ·_@Õ¦}¦~.V5óÞú1A~ÞuÚvl[ۧסDF|=¤Q­‘.ìß°FË$ª"ÏäD„¬­AÛž³ŽÇI%1GçöhVÓÛ/ F‹cvªËtüDZbÒïœ|À­=!¤[ow/߆ÝÇ.Ý=Ç7zJ¯%1²gK:V÷ ðîw6)jÓÀŽÍüü¼ýj7ê=÷èë|ù›½†^ÌÍ9׿~€·_›÷®OlTwÐee1„"~Oß M/eŸÅDVö«Ï¯™ÄZ8¨i­o¿­Gþ{5EA!l^ô¡ÙÝ›Öôö ¨ÛuòÖ‡ÙŒ²ä²f‡™Cû¶ªííW§åعŸ\ѤšZ".EñDZš¼¬ÓúïI‘ÝŸ\Ï?À»Þ¤â/ )ÁËŽmTÏ·ÏáØ§_멈ø Ëf­‹B)×VŽèTÃ/À»VÛ¿‹“å€\sXXؼ?ëùx7°ôfC“~sÍÈN5ü¼k6i=hé)Ì·]]=ýêw9ËŽAŽWÎÚ÷F®rl,4Fß[ðW»ºµ¼ýüÛ ^ô_’\ör}P¦‹" Òè%mëtÙÿþÌŽÖ´tu÷ðòððr³×¡¹úŽn^^îÎæÜ7GÌ{%W¶kȶ ;ûùÔîµðbüë“ÿô®ëÐ>dwŒrŲ„ +´«çíà×fèÒË)Š_?^¹T®Ö ç¤¡žì“ ‘¹Eºn>¼x`«ª~µzüsZyÌ”%ž_:¸ioÿæ½—ßÎ,سTrßrpy/?&üRª]¯ C[úy¹{×iÕwÞÆµƒ*ñ¿Ð‰*29{Bº6öö ¨ÑiÒÞײ²ð­GÎåÁOxn“ríÈ®ßvŽ…/²sôì+èq!²”£³w§{ ú+À«U¡Iÿéë7oÞÚÓòÆâ‰{^É!„É»»ûšnÓqs§óIû{vx2C!²×›×<°ë9uAHsî¹…³Ï§þø÷X,ï Ûžš–j``ػן&&&;öçäUø©ò^XFñ£j*“ɸ6k𒘪c¶<°{zÝä !ónd³„Ùëmë9|Ü. ë½'/Û¾eÃêaîÑÿNY¡¼B¹ñ(Ól|èØF²ð™]{,Œñ4j°ùƒusN$(ˆ,fû¨QÇD]î>±o峋ÓBv¿”•Ó»È_á¡(.M(EQ„È-YrC·á€¡íì3NL¶:Æ{äÆí[V÷µ¹1wèÜëY¬"ñȤQkâ|Æ.[»aNo¿óïLî•ÐÕ/lÛè×Ô’¨ˆ|ÎÓÕã—=ñ¹eç¶ Óÿð! )¯öŽ›sÉüÏå»w®X[ómºLcE ô ’ç7cÞŸ)Ð|mƒj!ËûÚpìþZwêäñs jërõÜ[ ]´nóŽg´çŸž1ëtªY‡å³ý…šó÷?{rëÀ ªê\ò?Iº¢,íWJyV 9%ê<Óžæî>f÷+“yõŸ~ónÙö]¶}ÍXï—ˇM=©Öäñަzöš:¡½þµe£×=ýòÅJ·î¬•­ 8®ã÷Ÿ8~öà¯ü/ )¯6†ž¡ü{Œèæ¡óõžâ¬^öjëÈ1ÛÅ gnܾc~QøßÖ<‘B#¾¾rŸ´þ¸ùÓU~±iú¦Èüœë¡“6¦Ö›¹yÇ®å!+ä%ä(Š+J£b»~D‘Î'H¿46*×Cx¦U;M\´~çæ•×ÉÜ9uáU‰eÃvÉçO>Ï'„éËÓ§S:Ô2+æT #¾¹ñ?àY³†ø¾Ý;¢{¿õ¹ BæŽoÅ¿´pÉ•t–ˆ­0ãº]¿•GíZÚ³oÒôÉÌïØ¯XF® <-‡/ž¬Úç3 tΈ:™g,½C¤Ï6½/¿aȵKÇ´q–B‰ªF¹¯\ ˵LµIâÝGÉ﯈q„Ú|ZåN¡raúÓ†-ºgÛ3tùòy}| 8eà[_òP›ê›6’¬M’&G'±fµì¾pëG¯ùŠMªiQ„âúGBâ´·^uÝEsî'H‰ !´fÝ™+ÆV×$„­Â¿}yâ¡ûY áÚ \1ÿO;a=%GÍ¿—ßÄð‹ˆÜ›’š¢¯oÔUKK«s§.;vmOII>pp_÷®=‹qÕŸÚøô»ÿÓl´|oýO_‘ü߯óz½Ãþ¬eÉ!Ĭû €ý!'¢óþ"„k3på'íòèчF’‘”HjúžŠˆÉa*B¸n–üÝÜ–W|µûøþÚsçýYO²ÉÑS½Ío™¶c÷ÛÚ/kï©IˆU«~Á;ƒÃ¯§Û•Ëú›/^b¤iÏ/¬\r•x‡øèP„v³qC{3‘¿ÙÙûºvû̓+ q5éV»Ûo5}µù¶Á[G´qäRIëÑ–ÝW Ny¾3v.®¯O"y4ëóÈ'XD¥òì}}+ÛëQö5ˆøÎÔDÖ¬s ¦t{ÿfj+êGÔ4÷¯æánþիר[¿–“W¤%äÐ\‘®žž—¢Õf€ae9)I<ÿú7>z+oQA‹Os8Úzzú"BòTŽ ‰ŸÄ¤ììW®C<Þv$Ùgôšî~†4qê;©Ïù ½‡^Ô¯´íœ¼Þ?ãÚzkRqð„ˆ‹}·]HlàOÏ~È¢½l¸„Ô¶Iü_ÐÑÓ1œù_8Íæii h𧩝§§K1‰‡¿0¤p,ú¬ùwPAe¤ÝÇ"{JšPÐ9χ=³í·»w=+.!¶“‡_i±p÷£>c¡„5笟 MH¾áÃÝ=xšž™ÿ:WËÅ¿z%;MbçäUç;c%´ñ² —¢ÞÆ«û½kµfåný*Fš™’Ä©QËjïÙ')Ä¿^ ÇUaÇ_ pu¡bNŸKsù£¦Iq¯ÌR¢Z3–ô×"ù¦wwœÐké”öæ4㑺ßÖ'I²ªI›ÊÛ,X‰OˆeÇ~-w<ú(»y=]ê—îWŠì—V­0l²È]“Êð‹‡†Ékæ57¤‰ÜáÍÁðC³…;öÆV¾gD SšÊüËkN$"O8öù(7ÒËü+—ŽîQâÙvÕù쨙͛n÷ ðõõ­Ù°ž—•¨:À=MRµðí¶‹¼V«'uõÂX%…mÙ„yä P*”ð­ÏŸ%æ…îE5^~xwl}DóýÆ-ᦣedn¦Ë£!ŸT|¶O±„° 9óîøùåSy=Ñü0Û³'N_½q~õ¤Íaí×ï9mצzÇOÿwýÚ¦©a›/ÍÝ?£¶:7l]=[-ɵ1S¿mÅÓVŸÅ¤Šˆ*ÝûÕg±û–¥*×Â2 á y߯)ßÚS_jíçÍPî¯<ûn+9Ÿ>zþÚõ“‹÷o:Ôû†¾Ž¼bÇJ÷ X45åF©jHÎeÖøï û¨Þ öö¬n¡‘~¼W› Ê}©Në ‹7Ÿ|Ò&Ës˜¯þ}“ §ïÆVVÁr* \³¬™ñû‹.<-}ú׎WÃl"WŽYø8§`·~ýÐPÐE,#gYçó%õÕNVupù GÇÞ¿¹½ónÃ2.…t·jo—¿Uìü˜EŸ/äÅ,PPš.Sßú’‡û@}ÓR’µI´‘o ³ìÚÚÏ%„ÐBÓ •œ]œ,5?û(Òž>Î2kÚµ±³÷K©<“u'Iääd V9÷w]uà›»š1±ÒD†FFÿhqUŽÇâWw´z´ñ4æS„0Šo¯h]{GÍÔG±”þ»õèËësTÞS¨céXÑÑÆ\WåYßÜÕŒys7V™H°y1wßr,+š›ØèæÆ¼ÈdŠyša¸FžMºž¶l÷†>6IW/¿‘RºN ‚M[¼y÷—œbòÕ0VLö›×™ŠÂ—Ìù¬Tª 4M¹‚B䉞åWè\ÏV‹Cûþöêý+¡xB“›-e”§_ÊK•ìg1‰“—þýêãs)=QÊÃçÙÊ×É“#žæºXš»XÒq÷^)ïª'’ØÛ±¬™‹ÉÇÓ ’—·bVž–E>lâ¼ò7 )ßÒSïhX¸çF=NQö“õìQªÈÞNõóˆ†Õ°¬Ö²ßØYv®h§sដ»Ë¾+iÌ¡µ§%ëZYÕiÜÝWÄ­kW? Mȇ'åsÌj¶vN?µbõ‰œ*m«•øs“¹F.Vœø‡oùï·Ê@[@ÿâñªR•v3þé¤>eäögBò¾ùÐÀѶ2ç§D¼~wO=«Ì TrÆÅ:¾ÊÓž%ä½ÿdZÓÊAÈ$ÄTE' -T-4r0TÄ=Š—þÆò_pÄÿá禠†~Fm×¢ÕÄÞÇ{®î×7©¯.Æœ¼”WãbüÉ ¦kï z¾ãˆwK«ìÛ{–L—W+ø†æF:v™ï"Lþ߯…wLÛl«,$)¥}á˜ÕïQgͤ±“ŒÆõ¨iM%?¾tü"¿ÇUEíÎFÙ'¶î½È:+^³æµ¬Ò·|‚е[{«.‹ÆÎ%[»éˆ_Þ>~$ºÖŒi ô¨r¸c¿8æõ;û®™7m¹Ë¤¶¤÷¶Í:Ïm¸°ªqúúWÌÛ¢ä${þß®q2ÛoŠ|áþ§–=öéдº ñʃT [ÛØyv9tlPQ;ûöDŽuS3¾ÆJžpxl¿+NAUµ×g“n‡-¾BW›\ÕH?ÒŒóúÜék>~2Ž£gûǬë$ýoÓ²«yT3BßÐÁ@|àØ¹ûº6,ÏÖÒÛ‘>°yÃAÍêÜÈãÿnˆ‘Û&åÔ¸ŽI%#Né߯d©Ñ÷ns”𤸆¶Á-ŒÿX2o›þ_5uãO,ÚðªbïPcýnõ¹£þùçञ¼¨s÷äøü]ÇŒ#%D–xùØyç3YÔáù»sü§Ôûøþ#&?7G,gY™8'W*ÔäÚ)ŸÈQº¾=›i š1jEÞ€†Ž™ÑWŽŸJi=g¢è׎W´Ž÷ %£ž/5Ï~ÓØo?4ˆ\Úúó‡-œ»‘in™qïЦc™òê_åt¨øbl«äñün¡T³à¶5]-4r¢ÏnØkÑqj38¯½Š…³Ú¯Ú8m™åŸ>ܘ‹»þ}*3©QÆŽ&È Œú)¿éF +÷Y¹ÓjÃòí›Cö§ÉÇÀ±z½ÁSº¸ ÈåB/Óò1©ý˜¹ ‡Ô°¯Û¹s݈5¹ï'ù¯Ï47…ѯÜnÖ’¿\4ˆ¼ô ´a½k&, Ý8¥ÿz ˜ºÕéÔßTeå RÏé½¢'¯ éG™ûwlÛÖâ߈oúA¥?—-ç/X¼vô¾ †èØùvsQåsÇþŽ>â˜4™½8mÆÜ½öäPzÎ-Æ.ë§CSîCç Lšüï¸á2]׿mü Ÿ&ñ¹ßù ÆÓnž´ÿ ˜Wë2#¤‘Ýí'7Lë»2—pŒÜ[OÑÊ’£†±âY5ïß%i×á%ã׿²„gèÖxÄÚá¦Zwà€£–í½Þ¨áÂãÆ4 Y:uø>ÝJÍztñ|F!<»6£;]ž8sÀqaå!Vv;êòøe3Ç6­Ö±{ÛÅ— ¥]¡šÑÂ1©¯_úïg`d·—ŒúëÝÿi6Z>yÉlùÌE£{.– ,ý:/šÝÙŽG¿1«G/œ¶fH§ V˾î Åã›™ÐÌB›qkýȯÅ«:ýLmôqY¾äñüöÊßt›Ø´s§m{‡;×úWxHèàÞ\ÛŽë7ÿÊB67æjÑ=µfÄ»ž·í¶`^ÎÌþØ”Ï5®ÒvêÊ®B"Vu:càé)˜¿vÌ–Ñ´­÷ç잢o‹•ôú?ºBh-K·}ÎïæoÎ'„56r­ÛN~oòÊÉÃ6yvªðâìûÖUoRбkï¡õ†9JËgøú9šsV-ê·=‡p *ÕlÙ׊ÿ;Æ+ž]ûÿ<í9lÂÄŠ›¦}ë¡Ò­1ffï‰3VNþkéÔÒ÷Ñö‹£œ´89ƒÐ­ß z›÷m›²'MJˆ¦_ðÜý+ i¢¢†•>_(²í9§š¦º¶÷ÅËvèwuösú(ïÎŒ£å3ŽNó!Vد~˜üÍŽààc-wnîaÅE¬ŠÍ8?¦ã,½Oª"B¬à'÷Î÷]T ŸeøoÄ<¨£Ÿõܤ2í·ü’<ü¦>’éË¥# IDATDîÚrKßËÃF37úø¢ÓTí…NBÄ ûüîX1)—·ÝÔ¨·ÜY„XA™ëä  Ž~JmRY÷[f*á7õ+‰¿ºaŦ áè; í­E!Vدà7ÇJw&ìNý‘e"…Ç~…ÞAÎ¥&m %ùܤ$òsú’š ,ÆwµåK¬„^#7ž‰XAÉÇŠkÕeÏ¥.ˆÕ÷Ï:xËÍ`Ä ÊfïàY« Ž „b ¸&„>B¬+@¬+ôr(?”UIHŠ¥B¬+Ä ±‚ÒÙ;¨MõMÈ×&=ŠxT®‚VÞÚ‹>B¬±B¬+@ÎåWIÝúìæê†` g€2ÏM‚2,W"]~ôá±HEª˜Ö×`j[åT5Íå¡PT‰%æžîž\.OȾ!m jôÜ$€³üèÃÑÜê.¹R*G"»’˜.Í—V5Î@d!çMüBˆOD@}àÒ¨#Üú eرHEu'M¡ *IšœÇÑÕÓ»š¨°() ©TŸP¨Ì3€:*³µIT1w ­(‹RÅ´•‘(6]®ü_-¡à™”CÓ¸‚ó~¯g …0 gø?{çÅÑðww¯½s E¢"Šç)"ŠEŒ±ELŒŒŠ½ƒJS° *±wÅÞ»#XE,ôRë»ß(–`0ùñÉr»;óîìÌÛfø2³Pnâ› $„Œkd4 ²ˆ¦PÚð‚P$¼—rëeVI*>ùŽ–fV];÷ˆ¾\výÙ›ÐámÛj W‰øÒ¾˜@½1@ Í€@ü”67)ùá]±Xì`Ëÿ³WX¡Pä—þsϵ£±‹Š9“‘0Í©õ¾"ªòâÔ~mvžÙžùÁiIzÔw“ŸN?ï©ñÏÅŠ‚ƒ£¿?Òg×îIæôoªÍ£8Ç 8@((ƒ¡¼f(_nR^a¶±gdhÈûäPÓ6Ø–¬æÞA“$˜8Agb²¯ªÑ_øÉÅgMºD9Ä&/º3Ão¨‹@à 0:øhzÕÒªÐ6à .+üãä–ÈðeËÃVnÜw1£‚Àey7÷E…†…m>p¿œ¬ÿ%Tÿ±#zwš#¾Ø'ª¯þÌ8ÔÝû Üz³Dþ/ ¾öêTÃøÓed«2ée•Oât÷‰{-}wЬIÛµp´ _àÐËwê–¤òúú’wã½]ü>¾ów?®©?++¼²nü ^ün~+޾Q”831xÜ€n|ãÀÉ— d­BPMVªq¹)ʽ´ÜÃuæÍÚogXþS»"+’âû órä m¾À/è¿ñ¹)@Ê Š3 ”¥ÍM"›Åa0ŸZ8BùÌ=/‡9ñžåWT*êž¿x±Õ¿'z襤Ӝ~žfÆ(¸³:j¦šÍ‰YÅ'ÿqœª{~vç¹â®£§MhË(½w$þàÍ™ã)ÇoˆzN[ì,¹¶!þüS»qª κ}Sdï×Qþõ"rÅŸLœábÊÐ>N]øžc§ Ò—æV+¥WWÌØN›¼zªÍ‡‚Ã8vùÜ™xäé;N‘DÍó”ã·6Ó! 2x”áã_ÃÏ*HуƒwÕûÏX1£kéeaç˹íù<á³'¥r€ºçžˆ….eˆdE^“3&€¬üüî4ãÑKW…Œo›¿(òZ @ž¾ëL]ÏiQsú‘çV,9’#ÿ =’ k¤„ª&ç¯û†åGU¦“µköÜcÔ å± ›—ý`§ŠµºEhií¿<˜:&̲¤ )¥2ŒP”g•ÑMì Ù4Ç' 0Àpœe]¿!uh§AÿoËÓÄñ—k­R¤\tÎ_…AȺ¤MG¤îó#Wtx½sÅη9n²œÝ¿>i;.xÍ´+kî–“M]U—ºyjðõ‘aÛ·mX4Ø‚ -²åÉß¼x%Ò²5UÁ˜Æ ÉÜÇÂâ'¹ ƒŽF¬z½Í¤£ž$3­L˜ÿ¸x „º…µZEÚëššœg¥ls+-€nÐÉ„(x”×Â)aS•j\nM§ÎÔ&E-ÙQî’°wÌ‘–¢ÂZE+¨?˜y¤W‰1®öf%{“qq÷åZ»!½yt «î¯›Yè»6tñÇ‘EfìH'¾ ûà)ags$hqe§JˆÒæ&Ñé ƒY+a ñíL‰ow“Ûi9ã±ëŽl||É@O¡°3ÅQ“NÉÛi‹dV“†u/rI½Aš5I;_™ô`<>u2µT¡ní1jlocV³ûý°Ï Šš¬k›ãŸj{­³ãˆ?{vÏðø¥U‰vêÁ‰ŸW“V@3ñß9ÁŒ”½øÔ‰È”|‰W»¿¼ªº|ש*Á’~®š8€¹øèÖû-±u‘Â7BŠ©úöEâlu6Ë…2aYÅTaÖŸ%8êl– %5•bš û­r@p4X ,¯©a~p ØêLy^¥„•ìx$ëÞ4Q©ÆåÖ¤þ¯¨ÉΪØvw²6ã‚™Ug×o`Ð&Koí¹Tk3µ—þÛŽLœê;ùÈÜÜ7|ËS†,{×¼e÷!ÛÆX²±Š:>m—éí9Úê´ê´Ó›V/ŸÉ2Ý;«= iAÈf@ ¾¥]7‰Nc0™L¾µ*ër±Kéun¶mÈ¥Ñù«I;f÷•×hVÂã§wSû+KÃéôúÖ‚s4¹¸\$£0‚AÃ0†š:]!’S}U]ñ“WR£ï­ÔZAËÃâT)I5œ¥áoÏ’ g1œh¨°BA½?ÛðZH9Ù:ÜÁMWªq¹ýõǬãÜ×**Ößç…‹«³£kÿ=L9­;©Cš}lIØývþñß½Ó*™í&Ånz|ýÒq«Vï›&ûýIåËGÓÜö¼»&u¢kNè…7«½êO´·ÖÊ»ûÓåkyÓÚ[2„lâo™  |¹I :N§ÓhÄîÙn>AÇúõ$¦Öñ¸ò 7ïX:ØÞ\ûnòke"±¿ÔÌÞŽŒIÇUÚõ4®<–¡}Wá¼Ð¢CeûêM¿§«¤‹Ì4ûÔ DQ$à4þÉýH’ˆzk¤9G—ÎúñSÜr/ÿÙñP¤œ¤Zùê›ÿíþ ”ðñ™«ÅmÇϲ2bƒQ»¦[ƒ=vϽc_=˾“Vö­W‹ÏFfZx7¨=]ˆ÷5Uå6¶Ü‹/Ê)/ƒæ)‰3à þü˜&雿®M«¥±éØû¶G5Ñä=ÿuƒz{•Bª ÞkÕ-œ«ÅqäíbIâj Æ5åй:È©‘€B\-ƸÚ¦ªSV+zëLWHªÅWKE•ÉÉ»€BT#¥«i0[¶"Œs´š¨TãrkÚ‚¦›ÿ°é„ÍÅSWï&[tç‰){¶O²hµ3{å%WVN)¼jûHó÷º>ÆÒ5·Ñ5·¶^ï³üpÚôåA{÷ëIUßXæ¿Ó4hë/ΪÙZj†ª¸¨JDBY¿$„¢lSŸ`2YE€¹îþ%ƒO_¹]–ûâòíäcÁ>]ÛéSÉd*IPÇ€”ÿ£®—¬ÊH)áXYiÑmû®š¹çŽŸ«íänÊäÚxZ•œ9r±ØPÐNõJþ&íY® ÷ãóuY)ù„i'HsŽ/õ__õÝæu~8Øß­¡fÌc”¥eÕ¾›e ­/åõ?žTLŠë䀿ýGGŸr ùáR¬¢ô³×Áŧ£  £Ó ‚ h4( oþ9Ð¥&ÑÔŒ,¬»ø®\ý=÷|Ь=/Å€ÑÙ)¬‘’õ¶nCÐì_³ä4Í´¡ði¡¸¥[¤ÚVœò´l! ÉM-ÄŒ:ð¸úvmˆÂÔ|q½Y™ý´„iÚ^—kÔÉÊ{RŸµ¯¨|™Q£nm®¦jÚ^Wøêù€¬(54´ãµð¥Ï0nS•j\nMûÀI’b9zOžº}_¬¯f浇å­U–—^ ›ú´WضN‚©ºJ!IÐpœ­ojÞÖ¼þ0Ñcãtu#SNÊb7T]ÖƒJ¿Z7Iy{$„¢´¹Iº:…%<}cæhepbùÐw^úyö17P—Ëå…%º:J1²ô̸åwÎÝyZ2õŠ)̸pú6Ö]zkÇÚ}ŸÝØ`·–ï¿\ྶ0¦]?“ìàÇšßÏÖ©w´)Þ<8yþw©9-÷ÊÖ˜,«€0s¼e_>yY«‹¶ðñ¾È‹„ç:¾ZÅ•e“Cž;/ZÖƒžŸñ p¦Ž©™.ûÒ*pÍ ˜k#vÀ ãªG‰ §«ä­m‰Ãÿ87IÃÖÉDvøè±-oVé½Ó7+ôÝ ™ •^K̲òcÄ$@ÏÒ€º\X‡ud•牴l XÍ¿ßÜçç3àjf¿µfö*óK< üXÂöã\'Zú™mÛ3å¦ÿn94u¨‹ŒZgçï®Q–r:á®”¬ôm‰” …uµ"EJ…55BU.—Á¶ý®7wJLìqÍïÌËÏE^w]è¬CÓpÖM¼~s9t²l¼ÇvYÓI…ÆuÒ~Ó–È»™=˜wþš®çµÈ’Å$úäضþùOv’;±ûŠÚNìcÔÒu;fÛF+%M5zΊS›i5&7€’×ÕŠjê¤%¯«­©!Ø*lÚû&+ËÞµpKE¯a^ö}ç©PݪmkÈnûs»¢‹FN\rµ­XÍ’ŒôŒ®n«Ü>?^Èïëb£#Ï»µ#ú¹fŸ©íÙßSò"~êš¾·{g=qêá I~ho}´½%²ˆ¿m6€òå&´).-|þ*­>‚0Ì‘#¬Ì|R †i¨iòt”¢ ,ÛŸgô{±Øï Z÷…K;DI“d'†D”‘š|C7L´ep,ØU.àš]=ÚBºžÀ„ oýÂG»‚äÖº?¬ eB ò‚ËQ36—È9–ýfošã¨Fæd¾¬"‹.„ø_x÷8½1;̶a~i–&l˜úóâ›—Ý¢õíÓýÉo­m’ÿX/'xž³Ä»÷ì¹"–~Ǿ¿ú˜³Þ=’ª~tò:Í}aG5 éè盺5võ]LÆõëÁcÍŸšóWO¤› [¹úù¸‹·ûmýwófß^2/QßqøØÑ¦ëoÿËr3´òÅâ°ˆ¹Gè¼^#z[ý~ñkˆão!y=jì¡2€¬Éž»µGl?<¿½J׫g„†¬ 8#¤ë;Ž ñÒôÝG¾\²rê^ÛÄeʺ9ÎꀑOÄòÜÅk‹'U-ú.ˆšÔ –?¯ž_¼)`|5®ÙÁ7(|´iËw3­”H(’¬q¹QUW¼‚ÀBŸ~`t)®ŸVC» ´í홑qs+“×ÔmBX =§åwQ´«¹úϊȪ¢˜€;ï~Åvß’è`ÇØthÍ‘Â9MÛnÀ¢­ŽŸ›}DÓîÔ™{h͹-bP·òœ3·ŸÊQ^0ÛàóÿÏõK­Êûõë‡äˆøwùW çÏŸ5j”’}pSÛ š'§.iá9v.ܑۚ‡èQèÀ_J—ŸYï¢Ò"›w‡E÷'÷k—[!OÉÒé4m.ñðyîa?.úðÈ/È÷p÷P¶R‘ÅÇ~øþ˜÷¾£xÈŠ@ ¾>ÿLS:þüÊ íp!Š3 ”ô3PÎÜ$Ä¿Uu}þ°e?9Í´™¾=z”ñ·Ø/ý·s [˜ÒÌx—]K8Wmao©‡•ü¾kÛk‹{ê!ƒ@|‹ Q ¡ÔÖsk3(´]Í[PÝuõ¥ëHïiþ9ˆ/BQýô䆨-"¶±Ó°ˆ°¡mа‰@ Í€@( (¼Ð¼pœÃ/Þl9ÅeÛ/¾r£‹[— …":0hXµP¬§B ›¡‘XÄR–õÇ€f4xÍ‘Áè¥ šk‚PFê³’@‰]E þ1Ãì¹)/Ë$‰¶ AÉ¥¥oª¾ëÌAbi0ÊËË (B©@q„òš Ðúr“€Ÿ=-)xqôQA©´ØT3©%«ìU& A#x†<3S3$ @6Ñ(7 ÑŠá0éÓµŸ>I@ -”›„PFPn@ „ò€â å5å&!Z#/Š„Ë¥‹Äï¶Uz9ÈGPÀ3äÙÛÙÓhhxB d3  ÊMB´b6ŸÍ`«jIiŠ;ùRO^ȇ‘W]»tEÒ@ åå&!””›„hż®¦ñ´UD+³NIã …T*-(,@¢@ ¥ÅÊk6@ëËM:ô7w·Žö€kj1Ðqœ „„¢ã8rÜ| EQ€!1 ²ˆ¦@¹IˆoD7F[¹5 †!£@ Í€@4E}nRÕ§`B‘ð^Ê­—Y$©øäO8NXšYuíÜ#úrÙõgoB‡·ul‹2OMuÁê„lâÿ0@ùr“’ދŶü?»‡ E~Yá1÷\;k°¨˜3 ÓœÐ{D| ÃPœáó¢Q"d/ãF¹óCâö!ú(• @|« þ¡Œ(íÔç¼Âlc3ÏÈÐ÷É¡¦m°-Yͽƒ&I0q‚ÎÄd_±œTå…Ÿ\|Ö¤Kþ¯{\œâÀ|tŒØ™ÕhµÈšÔýËðvsà÷òš¼öl®äï]þ9o’÷…Œ÷éãÀð§ž.&ÿ•z)os“š ¨I¿¿6bÙò°ð­§RÊIœ \–ws_ThXPØæ÷ËÉ·?¬þcGôî4F|5°?Oh +Š˜8ÌÃ/ppúãò}÷ß(PWù¹¦%«|ÿ£»OÜkéûo4m×ÂÑ.|C/ß©[’ÊÉú³wã½]ü>¾ów?®©?++¼²nü ^ün~+޾Q”831xÜ€n|ãÀÉ— d­BPMVªq¹)ʽ´ÜÃuæÍÚo¸]‘Iñ‹ý†y9ò£g'<¬"@^x9z†ßÐ^ßÓwNÜÍ’W”¦„é ã\ž«ÓÄ ËŽñÑ0Ñãs)ú‚•g@(#J››D46‹Ã`0>-°P>sÏËaN¼gù•Šºç/^lõïÙÂ_¦ê4?!®æ^&}µwqx¡*£?Yzq¹tA¿ùQ‹Ú oÅ…, d™ïño÷¥—²:iͤ™w,ÆNXáoÌ–Qzj­-ŽYã Šò?öz€÷îoËȹ~äÄÞ›m¦{T¦¿!ê9m±³äÚ†øóOíÆ9¨‚8ëöM‘½_Guú× Èo[AÕ¦lœK’ĸÓ7Ý®äåS%¾›3ÅJåÍÝ„ðÍóÂ­Ž„w§å]¯XtêÖµ-Œœ~À÷à•ËϪ >ÛC@Qpè§¡‡ÝìþÑŒ.+¼·bãᤠàÛè¹ËzhúÉç€ë¾}?[ÐD)‹Ï­]™¸Ï%kžˆZµíBF%¥få5iż¡VŒ¬LŽYµýVžDÕ¬“v‰œÕRÚÃfÚä©{ýFìo8W÷ôÈ5¡ãü_|µp0››tnêá¤ò>üûGïÓúDýâÕ™ öFÓ¯œ_qôq ƒÉÄg:¾»Ç÷¶a@‡À §|wœxùs[üìÙB‹IGö0"À*pÔé^)1®e«w’×MUªQ¹yÐÕpß|íNÝýeý«÷xTü±meäowòD4µ6öf/ŸÖK§¥·µ+†Å„uQo í4_^üébr¡¬»U×9ëßí®ÒAãÙ©nçV+@ EéÕ3¶Ó&¯žzÒ?æï ‡gmßÉé£ÊòÐ ”¥ÍM¢ÓL&ãu©ädrÁx^ÜÅÌÄû…qÉcz[¦—P%U’»÷FOîåÕÍìk–’ÙÎÓ¼ö„ö°èc—ω÷ÑÁAž¾ã9`AÔþ5ül¡€=8xW½ÿüˆ3º–Xv¾”üplÍ8¸å¶Æwþ.†¶ƒÜu2Ï^-”Ô=;qWî8ØZ"¤˜jìúW³ì¨U“ñªšlär-\Q|rÉì­ù]çEÇmϯw=%|=yÙY|PpÂoÛÂÇu¨NËͺq1›ÅMZ>\ÀðM =›+ý¤^à [ÁC#hÍr@UNl/ IDATh›é2i4‚ÆPã³ëòJec€F`8NȲ®ß:´Ó 7SÁ?>NMR”Ü»ø’ÅÛ[¿ÁÀÂ5FõÕɽt#ÿ3)2uO¶L]™d6yÓÉû7~GY²âìGMúÃÛŸZ¸]è¶ÿØ©„ÙöO7ÌÚöL,/8ºpv\^çÀ¨˜­ËFÚ©´ì —üÍ‹W"-[S€iÜÉÌ}\ ,~’«0èhĪ×ÛL:êI2ÓÊ„ù ×Ñ@¨[X«U¤½®©ÉyVÊ6·Ò"èLˆ‚Gy-½ry ŠùžéÔ«UÐl¹I¡ g±èõÏ£³UTf­ŒaÑN[ô ³š4¬{‘Kê Ò¬IÚùʤãñᨓ©¥ ukQc{³š]aþx>ƒ¬üõÐw3þÈÓÏ0°Õ‡ÄçE²NÝ &9á”Ügc`?k€ÑðÉÞýO=©éÒØOe9gv¦¶›}h_ ð˜<áhâæk/½»éMØ5÷{ :€æ³]ï´àfF ß)¦êÛ‰³ÕÙ ,Ê„euS…Y–ਲ਼AX&”ÔTŠi*o]@p4X ,¯©a~p ØêLy^¥„•ìx$ëÞ4Q©ÆåÖ¤þ¯¨ÉΪØvw²6ã‚™Ug×o`Ð&Koí¹Tk3µ×;Ã^œê;ùÈÜÜ7|ËS†,{×¼e÷!ÛÆX²±Šz@m—éí9Úê´ê´Ó›V/ŸÉ2Ý;«= Èf@ þ–ÙÊ—›D§1˜L&ßZ•‹‰u¹Ø¥ô:7Û6 äR‰èüÕ¤³ûÊkž+§7ˆÆ 0À™ªL£áoÇH:)UPïFoÿå´é¨GË«”ƒ@òüÀÖß5‡ííÀ¾úûÎÞ(1´øTtî¬ÊåL[þc~Њ1}W0Ml4ê@[ýnØýèòº¼‡Ûn¼ÝLòÒ§¹rc_õ÷ú‡LX«`võã'.€Åüô«ã/^Ι2¦µµóæ²ðú€Qÿ<£(À0‚®/ã••°qùL»³¯Ÿuñ…Hßß4çpÄ˶“–ÍQ½uèwû@ÝæžÙÐØºI.”T$o4x +yòº¦àæ$×Ã8EJ¤2ërQ£LIþ£\aòRŸ>Ë1J&‘r *ó*Š9¶¼V”YÄ;©R ’j8KÃßž%Îb8Ñð)*Ôû³ ¯…”“­ÃÜt¥—Û_Õ:Î}­¢bý}^¸¸:;ºöÐÔӺ“:¤ÙÇ–„Ýoçÿ]Cº(³Ý¤ØíýóR¯_:nÕê}Ód¿?©|ùhšÛžwפNtÍ ½ãfÕ£Wý‰öÖZywº|-oZ{KÒÍ€@|!J»§ƒN§Óé4±{¶›Oб~½‰©u<®üÂÍû–¶7×¾›üZÙ‡Hì¯5³ú‘£1éï‚ ×bW8/øÞê­gŸaîÕÏ`ïÙ«¯,_$á.¡U@Óiú–S?—½‘«éÓnMö‰67SÅ»œ”ËÉw&ËÇτʂÑ9BVU-£0º†‰&|S׺ü†5Ûþ ,UUº¼N4("°5¸LšŠeßI+ûÖ+‡Åg#3-¼‡Ôž.ÄŒûšªr[îÅå”—AsÇèÚmµàìóB ˜½×á¥Eé% ê¤B@ýÌOÛ0EXúoðÞÜ¡«¨ OPŸª}”B«¸Eìœe×àÜ$˜ø£$F´=çjqA\#y»X’¸Z‚qM9t®rj$$ W‹1®6‡©ªÁ”ÕŠÞ~k Iµãj©¨2¹ ywPˆj¤t5 fËÎÑj¢RË­i šnþæ6O]½›tnýÑ'¦ìÙ>É¢ÕÎì•—\Y9=¦tðªí#ÍßëúK×ÜF×ÜÚBz½ÏòÃiÓ—íÝ#¬$U}c™ÿNÓ ­¿8«~dk©ªâ¢*‰t ¥ýfJH}V(ßÄ&“EQ$Ø™ëî_2øô•Ûe¹/.ßN>ìÓµ>E‘L¦’Uq Hù?êzɪŒ”Ž•• @ülïÖ»ê>®ïgð1L<ñrlùõÍe¨-§¡¿çêµ1`^8˜¦"`Y/‡O/gh›¨ 3_W}\2šŽÏýãEÃФÛ’¯n?Ö뇥/Ê0]KÆÿQ/å£9×ZeêYè@YvIÕå‰TLx*´÷?ÀEég¯ƒ‹OG5F§AÐh8PÞük­~T t=-E¿ÿv­¸Áh$+Sö/¥Y9›²èl‚ÖHÉzý¿>nFÓ±5& R‹ZÚ::õ‡–*“ ³è”¸J\R.#)`òÚëս̨½û¥¶Ž‡£k¦!|õ¢BÑð®Z¶wPÛÊ‚Sž–-¤$¹©…˜QWß® Q˜š/ „ÙOK˜¦íu¹F ©¼'õYûŠÊ—5êÖæjª¦íu…¯ž×¯o++JÍ# íx-æêr¹¼°¤@WÇ@)C–ž·üι;O@KÆø}‡f\8}›aË.½µcmоÏîl K¯F©tZ0ÊæCSˆnÜo ÑÖ-iºÃmØ +þãn¡Bæß;½#¿ûÒˆ.hôr–e¿ÞšÇcWý¦1ÂJöêÆþ}ù2SLÍiŒ;;pÕ²ßhSzê Ó/îÐ-h¨®ñ«VDu úÞ¬äôšD±s‡!Uþ¾^*íìMZ|Ä¿¹r“=‡žfÇOì¿`ù½ýÕÙÓ9ÎãLØï./¼–˜eå;ƈI€ž¥u¹°ëÈ*ÏiÙ°šã¹Oög ›øÌ‘81dʼÿ‘ÎÆDé£Äm±g*µ|Æñ5Y üXÂöã\'Zú™mÛ3妘ºó¸*+gÇŠ¦zX°ª^Ü9s¡lHøœvö:å§¶îï2¼Mù­½q—„ †ùàìö¯ž»’8ÜQGžÿøÊ‰³¹K½ÜµŽÆ†þÊý¾ìÙÅ 2ëÓ¨H©PXW+RP¤TXS#TårlÛïzs§ÄÄ×üμü\äq×…Î:4 ÇaÝäÁë7÷˜3@'ûÀÆ{l—5Th\×!í7m‰L°›Ùƒùxç¯éz^‹,YL¢ÿ@Þmë™ÿd'¹»¯¨íÄ>F-]·c¶m´RÒô˜Q£÷è¬8µyVcrÃ(y]­¨¦NJQòºÚš‚­Â¦½o²²ì] ·TôæeoÙwž Õ­Ú¶†%ÿܮ袇‘—\mëÖ_³$#½£«›ð*·ÏòûºØèÈóníˆ~®Ùgj{vã÷”¼ˆŸº¦„ïíÞYOœzxÃE’úÁRd3 M£´¹IÆmŠK Ÿ¿J«O†  sä+3ŸT†ajš<]#¥((ËöçýF,ö;¨Ö}áR³/QÒ$Ù‰áe¤fßÐ mY zº'ÏonŸ,Hçõìm´å\ŸAo£ Ï;¶2ð\5pÛ8ù†þ:Õ½¾Óoìr¶ÝôUþ%K·ÍŸ)So?Ч»öó puþ¼¸EBâ–øm‘á:íŒÌ1tŽXS¶|ÕÆ)ç$ÓÞþzéâÔkiB´ §¥7õfSÇ ƒÞÓ&–nÞrŠä;›1Ô’Ó°š*UýèäušûÂŽj4@ÓÑÏ7ukì께Œë9ÖƒÇü «®~òDL¥ëŒ¸xÃØèý“ãDÀí6eÕ¬®*¦â1oöíÑ!óõ‡mºþvý3ãùá›×MÞS „–uOïIÆ ¦®ßòqÏ—þºt&Ër€ß=³vÐŒ†GÆ*Ö­OX4y ¸F]ڴ廃VJ $ù6‡ k\nTÕ•¯àG°Ð§Ø]Šë§ÕÐh m{{fdÜÜßÊdÀ5u›hßâ{ªFÛÕ\ý‚gEdUQL@à l÷-‰vŒM‡Ö)¬‘Ó´í,Úèø¹ÝuhÚ:sb­9·E êVžsbæöÓAù/Ê f|þÿ¹~©Uy¿~ýÿ‘ÙðÿçÏŸ5j”rÕêÐßT¾†7OFD]ÒÂ!s ì\¸#÷‹~¯(<:ÅwO·ßöOn÷© ¢”•ßÍ‘¯<µÜó-µmï é],õæ‰ËÊ+â}ÑÙùùîŸk5‹¼gÏ>¶­¿6’ø–ùg ÒùóçWfhÿƒ Qœ¡Ô_‚rF¾udyg=7¸ÈìŸ âôý¿Ý×ìÜÉ„+|qfÝE¬×Z+ö7*ÇæœÝEó9˜æîèKöï½ Ûƒ]üÆÈÝÃ’†@ ÿ9h¸B(#J››ôÿ2¼5¬O(}}òpv›mþQŒü¾=vg¥ M›~¿DÍqháÛeýsšsèV®Ù{Á²¡ ÂMÛ®ë]úX²1$@6â[Di÷tk¥pœÃ/Þüâ_3¬§¹3íŸ>‹ÝyÖŽ+³¾iqÓq¹X¦ (J…l†F‰E¬¿\Œ¦ç:»ë|$)@6r“­¼°¢V"–wá¡„üF †òòr$ @6Ñ(¼€hÅüäfºâØK5Jb¯^÷*í_ôàòÌLÍ(Ù D Ü$D+¦w×T{$@´ Pd¡¼f Ü$@ %ÅÊ / Z1/Š„Ë¥‹Äï¶Uz9ÈGPÀ3äÙÛÙÓhhxB d3  ÊMB´b6ŸÍ`«jIiŠ;ùRO^ȇ‘W]»tEÒ@ åå&!”×l”›„h¼®¦ñ´UD+³NIã …T*-(,@¢@ ¥ÅÊHk /‚¿·ùÔp Pch…j1Ðqœ „„¢ã8rÜ| EQ€viC d3 M‚r“߈nŒötk CF@ ›ø2³”/7I(ÞK¹õ2+ƒ$Ÿü Ç K3«®{D_.»þìMèð¶ŽmQæ ¢©.˜@0@ Í€@ü#”6¼üð®X,v°åÿÙ=¬P(òË Œ¹çÚÑXƒEÅœÉH˜æ„^%ˆ…ü¥tù™õ.*ÿÙ3joûÕ‹>lÏn9‚Á0 Å>/%Bö2nô˜;?$n¢RÉÄ· êÿÊH}VR½ñ TË+Ì660ãñŒ yŸjÚÛ’ÕÜ;h’'èLLöËIU^øÉÅgMºä_×ͯN8Œ?]†6/þ7ÞR}nRs5é—â×F,[¾õTJ9‰Ëònî‹ Û|à~9ùö‡ÕìˆÞ&ˆ¯öç då£C‡y8ð®C\¾ïþjCŸkZ²Ê'ñ?ºûĽ–6ȯ&m×ÂÑ.|C/ß©[’Êë?a²ân\ ·«ÀßÇwþîÇ5õge…WÖÔËßÃÍoÅÑW" €g&Ð/p89âR¬UªÉJ5.7 E¹—–{¸Î¼Yû ·+²")~±ß0/G¾ÀÁcô섇U$È /GÏðÚK pà{úΉ»YòáŠÒ”0=aœ‹ÀsušdÙñ#ü÷GÿÏ¥è VVPœ¡¼f(_nAÐØ,ƒÁø´´BùÌ=/‡9ñžåWT*êž¿x±Õ¿gk|-Ìö?…vªÈÛð/ЬqEùû=À{ ÷·eä\?rbïÍ6Ó= *SŽßõœ¶ØYrmCüù§vãTAœuû¦ÈÞ¯£:ýë…@䊷­ jS6Nž°_êúóìX'#¢,5ñ×M“~Îßœ0ËY µÅO¦ÇŒ³;—0¼àýõs×¥9/Š^jYq!bå¢ óƒûiV\™½³Ä{Ù&oÝœC!«×Y_êÄÎ?6oi"1vÅÎ^œ' Á¡ó¶Ûî °…qs"®šlˆï,»»iIÐcë¸Ú´pBÒT¥—›6VyÅßgI’wú¦Û•¼üqª¤Ãws¦X©¼¹›¾y^¸Õ‘ðî´‚¯Ø¿ ifܪ”Ý‘ñsÃ,Ϭuפ9'ÎÞ_üÑqý¡A!ýõ œ¥gBG±²‚z[„2¢lá…t:“Á"úÊ“EQiw_ i4zI" þñÈ&™T^uç^røØ®NÖú_o L_36ø‘¬dÏn|Á÷»n¬öî5ú×ý+ü<øn¾Ag² ïlô÷îÆwõúegj-P—´Ð³ÇÔ_·,Û‹/pè7aÕµb¹,+~DþëÞ…*¤/6 u}(7ëäš•²…O×yõøép ÒWqÞ‚QñÙ2eïôu·¿P²â«§õï!pè>p|LrTåÅ)ÎWØô½»À¡ÇÐ_v=R’ô¨/(^ϬšàÆ8xMÝøG% ”8ëô*?¯ü^ýgìLoy¾©æŒ3@õóªN¾ÍM­\¼ûð*%—’dEv%×ÜR›Å1hgL”çTST§\|ÉóìaÌ"¥‰3ÈsGì/v\¼%jbÿîöœúŒY±yõÑÑðYRQÊbÏ޷뽊‚C~Î#vfÉ@Vx-vª¯›_À÷™¾ñv™‡üœ¾‹UïL¥,öì3ã®Èšgû‚~tï.pà{ >šQGY™¼mÎ÷ÝùÏq!WJZÎÞ{ ›i’ožcñÍW÷ôÈ5¡ã´_|m;÷4·/ãÞá¤ryå½£÷i}fþâÕ¹CWï™Ó»Õ]=ú¸V^x#ñ™Žï‚ñ½;µw8Áºð܉—bÉë³g -&ÍÙ£C‡Þ~£ô3Ž^iñ¡†¦+Õ¨ÜHÀ4Ü7_»s+ƃû»š¬øcë¬ï{ð=½†l¼Ñ"³µ+†Å„uQóGyºòMœ9Ü 65¹Pì®sÖGLÚÇ© ßsì´AúòÂÜj€¢ôêŠÛi“WOµùPp‡gmß©SçN:wêhcÀF ›ø(mnÎ`2¯K%'“ Æ»ðâ.f&Þ/ ˆKÓÛ2½„*©’ܽ÷0zr/¯nf_³”Ìvþ›æµ'´‡E»|îL¼òô§È ¢æyÊ·Œñ[›é<Êðñ¯ág ¤èÁÁ»êýçG¬˜ÑµôÀ²°ó•?rE@‡×;WìL—P‡1SV^Q¼ySd ›!½e®KK#hÍr@UNh›é2i4‚ÆPã³ëòJec€F`8NȲ®ß:´Ó 7SÁ?>6%÷.¾dñÇöÖoP4p ‡Q}ur/ÝÈÿŒÞZ÷dËÔ•If“7<±ãwÄ‘%+Ζ~FoSŸZ¸]è¶ÿØ©„ÙöO7ÌÚöL,/8ºpv\^çÀ¨˜­ËFÚ©´l5FþæÅ+‘–­© À4îdHæ>.?ÉUt4bÕëm&õ$™ieÂüÇ…ÀëhÈ Ô-¬Õ*Ò^×Ôä<+e›[itƒN&DÁ£Ðq{ëÙÒ·=ŽÖИ³íÙÝ™Wͽ—#§_P<ŒÝ3<~©@@¢zpâãçUU%»ÎÔö àªXÉÇÝo‰í¼Ùr“$BÎbÑëŸGg«2¨ÌZâ¶èAf5iX÷"—Ô¤Y“´ó•IÿÆãÃQ'SKêÖ£Æö6f5»ÂüqœAVþú 軳>òzØêCâó"Y§ÆnP“œpJî³1°Ÿ5Àhødïƒþ§žÔtiì§²œ3;SÛÍ>4Н…xLžp4qóµ—^Œ]ô&ìšû½ÀNóÙ®ƒwZpwJ ß)¦êÛ‰³ÕÙ ,Ê„euS…Y–ਲ਼AX&”ÔTŠi*ì·ÊÁÑ`°¼¦†ùÁ €`«3åy•TZ°ã‘¬{ÓD¥—[“ú¿¢&;G¨bÛÝÉÚŒ fV]¿›,½µçR­ÍÔ^ï {qj¨ïä#opsßð-#L²ì]ó–Ý„lcÉÆ*>èµ]¦G´çh«ÓªÓNoZ½|&Ëtï¬ö,¤!›øR”vÝ$:Ád2ùÖª\L¬ËÅ.¥×¹Ù¶yX —JDç¯&í˜ÝW^ó\9½A4ÎTe ;FªÐI©‚zç0zû/§MG=ê\^%¸¹úXnþí\æ/ĹËv“ù:8¼µpÍŽNº¥)¯jª—ЬFÌrÞ1ãâãj‡êÔj“Á¬ê;éµÚÎ4ÿ¬N`^çh±áµŒú²âaøûë4¹¸\$*-Ì–h§ÚÂÃØÍe3àõ¢þy$FQ€a]_0Æ++aãò3˜vg_?ëâ ‘ ¾¿iÎሗm'-›£z7*êÐïöºÍ=³¡±u“yÓ”T$o4x +yòº¦àæ$×Ã8EJ¤2ërQ£LIþ£\aòRŸ>Ë1J&‘r *ó*Š9¶¼V”YÄ;©R ’j8KÃßž%Îb8Ñðí*Ôû³ ¯…”“­ÃÜt¥—Û_Õ:Î}­¢bý}^¸¸:;ºöÐÔӺ“:¤ÙÇ–„ÝoçÿÑ;­’ÙnRìöþy©Ç×/·jõ¾i²ßŸT¾|4ÍmÏ»kR'ºæ„^ˆq³êÑ«þD{k­¼»?]¾–7­½%©AÈf@ ¾¥ÝÓA§ÓétØ=ÛÍ'èX¿Þ‚ÄÔ:W~áæýKÛ›kßM~­ìC$öךYýȈјtŒÐwb¹>áÜ3Æ¥jûΚ84ÄíéFŽÙ‰<}$~ݦO'sw­='Š´Úkb¤œ¢Þ´_R’¿Q<¬~ðUÈIŠ ð–l2`Ölû3°TUéò:NÐh PˆdÀÖà2i*–}'­ì[/Ñⳑ™Þà jObÆ}MU9„-÷â‹rÊË ¹GŠã tí¶Zpöy¡ÌÞëðÒ¢ôPuR! ~fȧm˜",ý·FxoîÐUÔ„'¨OÕ>J¡ÀUÜ"vβkpnLüÑ#Zž‡sµ¸ ®‘¼],I\-Á¸¦:W‡95€Pˆ«ÅW›ÃTÕ`ÊjEoé Iµãj©¨2¹ ywPˆj¤t5 fËÎÑj¢RË­i šnþæ6O]½›tnýÑ'¦ìÙ>É¢ÕÎì•—\Y9=¦tðªí#ÍßëúK×ÜF×ÜÚBz½ÏòÃiÓ—íÝ#¬$U}c™ÿNÓ ­¿8«~dk©ªâ¢*Z–Oy¿$„Òš  |¹IL&‹¢H°3×Ý¿dðé+·Ër_\¾|,اk;}Š"™L% ªâòÔõ’U)%++-=‡ØT\ˆÝr¶¶ËPGõº –¹ ­øÁñCOôÜì5醂žÜ䃧ƒµ“P56d”=;UΨÿ`ªM«­>ɽSüZ`oÎ9ÐL= (Ë® ‚ °º¢<‘Š O…öþ¸(ýìupñé¨FÃÃèt‚   ÛôÇAB×ÑÓRôûo׊²BÈÊ”}çKiVΦ,:› …5R²^ÿ¯LÑtl‰‚Ô"†–¶ŽNý¡¥Ê$è,:%®×ß…”ËH ˜¼özu/3$jï~©¦ ô IDAT­£Ááèši_½¨P4¼«–íÔ¶²à”§e×’’ÜÔB̨«o׆(LÍPÂì§%LÓöº\£N†TÞ“ú¬}EåËŒuks5UÓöºÂWÏë×·•¥æ‘†vôÄ—~ž}Ì ÔåryaI®ŽR †,=3nùswž€–Œñ%ú)̸pú6Ö]zkÇÚ}ŸÝØ€ë †ÙF'³<ÖuQûØ¥«ÙôÐ+ˆN6Ÿ>_š‰K7*áhA§¥,ÌvhwÆŒµ;ÈF•Oì<]%ÿ·W$Ä4½íêÂW­²™â¦^ôû‘w¥ÄàØÔ›+7‰ÐsèivüÄþ –ßÛÑ_=£á<΄ýþáòÂk‰YV¾cŒ˜èYP— ë°Ž¬ò<‘–«ù7žûdÝ$º‰Ïü‰C¦Ì+ñélL”>JÜ{¦RËg_“%q°À%l?Îu¢¥ŸÙ¶=Sn €©; °rv¬hª‡«êÅ3ʆ„Ïig¯S~jëþ.ÃÛ”ßÚwI¨p`˜þÁnÿê¹+9Ãuäù¯œH1›»ÔË]ëhlè¯ÜïÛÉž]Ü‘P ³n1Š” …uµ"EJ…55BU.—Á¶ý®7wJLìqÍïÌËÏE^w]è¬CÓpÖM¼~s9t²l¼ÇvYÓI…ÆuÒ~Ó–È»™=˜wþš®çµÈ’Å$úäضþùOv’;±ûŠÚNìcÔÒu;fÛF+%M5zΊS›i5&7€’×ÕŠjê¤%¯«­©!Ø*lÚû&+ËÞµpKE¯a^ö}ç©PݪmkXøÏíŠ.z9qÉÕ¶þaý5K2ÒK0ºº ¯rûüx!¿¯‹Ž<ïÖŽèçš}¦¶ÿÌf›’ñS×”ð½Ý;ë‰So¸HòC?Xêl¢i”67ÉØ MqiáóWiõÉ4€aŽaeæ“JÀ0LCM“§k¤eÙþ<£ßÈÅ~Õº/\jö%Jš$;1< ¢ŒÔìàºa¢m}¼×vò²Ä2͆uúÓÂ14={Mº iÀjëæ¤z4ÍÙFõsCÆ/^¹ié šQ÷ÞÎOöþënZ\¿ÿ²ç‹ÃÖ-9Å5÷1ÔæÙÉ–ØÔ›M' zO›Xºy_\È)’cì4nÆPKñîRÕN^§¹/ì¨F#4ý|S·Æ®¾‹É¸žc=xL¢ù3À>y"¦ÒuF\¼alôþˆÉqõ±%n·)«fuUÁ0y³o/ˆ™—¨ï8|ìhÓõ·ë/˜Î ß¼nòžZ ´¬{zO2f0uý–{¾ô×¥3Y–ü~왵€f4<2V±n}¢ÉëdÀ5êâ9r &ÇjÚª€¢¥qAóz]† öÔÙ™ÓRÚ”äYô¨±‡Ê²&{îÖ±ýðüö*]g¬ž²6àŒ®ï8*4ÄKÐv_ùrEÈÊ©{el—)ëæ8«cF>Ës¯]4.žTµè» jR6Xþ¼z~Ið¦€ñÕ¸fß ðѦ-ßÌh´RR É·9lXãr£ª®x?€…>ýÀ.èR\?­†FKhÛÛ3#ãæþV&®©Û„°@{NË–iWsõ ž‘UE1 +°Ý·$:Ø16Zs¤°FNÓ¶°hk £Úgúšv§ÎœØCkÎmƒº•眘¹ýtPþ‹ò‚ÙŸÿ®_jUÞ¯_?$G„rþüùQ£F)U‘ÁßS¾†7SÊM]ÒÂ!s ì\¸#÷“¿P•WçÕX¸¤ 5© ï é],õæ‰ËÊ+â}ÑÙùùîÿM”²È{fñìcÛúk#Ù!ˆo™æT=þüÊ íp!Š3 ”¥ÍMúÖ Ënïþƒåcƒ †ÿ€æœÝEó9˜æîèKöï½ Ûƒ]üÆÈÝÃ’†@ ÿ9h¸B(©é¬œ¹Iÿ'Ã[ØT]yþ¥ÕÜgY!¥ì¿àíh$ˆ¿®Ù{Á²¡ ÂMÛ®ë]úX¢}cÙ ˆoÙlå[7©•Âq¿x³±þ¡Í¨ßþ…äó/CÇåb™‚¢(²A$±þrý1šžëüí®ó‘¤¢Aé eDKK ÑZ±ÑÁK+j%âº.<Ô7b0”—— Q „Râ e¤µæ&!ð“›éŠc/Õ(‰½zÝ«L´ÑG4‚gÈ335C¢@ d3 _d6ÊMB´FÚpwMµGr@ D EÆÊÊMB PPœ¡Œ Ü$D+æE‘pÙ¡t‘XâݶJ#Gù x†<{;{ Olâ Ì@¹IˆÖÈæ³lU-)Mq'_êÉ+Bù‚ ò ò k—®H¡< Ü$„2‚r“­˜×Õ4ž¶ŠˆbeÖi i|‚B¡J¥…H¡T 8Bi­¹Iü½Í§¨¶âËÔb ã8A# EÇqä¸ùS³§(@»´!²ˆ/4å&!Z»nŒötkܺÆÑ€@ Èf@ šBià B‘ð^Ê­—Y$©øäO8NXšYuíÜ#úrÙõgoB‡·ul‹2OMuÁê„lâ¡´¹IÉïŠÅb[þŸÝà …"¿¬ðǘ{®5XTÌ™Œ„iNßæë“çí5ê´÷¾?cZó_ÞrÀ0 Å>/%Bö2nô˜;?$n¢RÉÄ· êÿÊk6€òå&åf˜ñxF††¼O5mƒmÉjî4I‚‰t&&ûŠå¤*/üäâ³&]Ò\”¤­ì<ùršñwÞR}nRs5é—â×F,[¾õTJ9‰Ëònî‹ Û|à~9ùö‡ÕìˆÞ&ˆ¯F#3ÈÊG‡"&ópà \‡þ¸|ßý7 Ô†>×´d•Oât÷‰{-m_MÚ®…£]ø‡^¾S·$•×ï?NVÜ ôv8ðûøÎßý¸¦þ¬¬ðʺñƒz9ð{¸ù­8úêíÝw\÷ûðçrÙao‚ * P b‘!Z±ˆµ•Z±Ã nÜ{€ âŠJ]XœhUÜ»jÝ8p"C@†Œ’äî÷GWÛþ¾4ÐçýÊ«¾zdÝ“ÏÝ}žÏç¹; @Ks2fìñ…ÐÛã«¡ Ç åÍ"P ®”ú¸%É?>'Ðw̹šÿp»¢*.§L‹ìâ!ôv ü>&õfŠ¢+GGöîâíí& Ÿ|®äí+JÓâìÔ>ÞA ïJ@ž›ò·›ðÍ£ûŠu¸k*œg@šHck“H’ÉãòÙlöûŽX1fÛã>žæ÷ŸWT*ká”uôò”òK«§Ïšné˜üCË&Þ…5´RêãfHTžŒ ›~Y ÀðüO·+Eùí,Y»oÆsÐzy)5~ÍÄx‡Ýñ˜…7žð¼#g°T]ߺ(eÂ|ûCK uyû§Ä¤½x禽gÅv7%\+nÄš ÷¶H©ª’@ó.ºÊf±8l.I²æ(L¼{驘Éd•ˆ”Ñ)·ûu¶Ê©  ªè‹W®Åp÷t4ý÷„Ù‹̾%/Ùö£¿›ÐûÛ-g†vù~CÚÜÈ 7¡ø¬CÏŠ.®ˆ ýBè2rsV P{yJPçáÖNÐEè탨ÆñhEÉÅäè0_7¡_¯ÉiÙµ4ȲX‹&§1ç úÁBmÏðÀö¶Ö>¡]Í+o]+¥¨ŠÜJ­½!—oÖÚ’,Ï«¦I¨¾~ì±yPgK.IjÌ<ƒ"_BÚ ikwïäêâÙµÿÜ5 {HöÄï|V'¹>-È/ú‚j°WY˜éõÝægrNîï&ô†Zq¡L ÊÂôHÏoRž¨“%×§u}I ”èþŽY?tòv†ô›½ça- Tåµõã¿í$ôv {²¤éÜ{ÝfÄÎkv·{+竽·û´ØcÄÈp§݆LèÆ¾²ër¹¢òÊž«Ì®cF†thç:fÔµ§öÜ®Q͸o>ùg?—¶žßäXtdÿc©ìéáÃEvCbúun×Î/rl„éÃ='›üTCÃ+¥6nzkN_<¿*P𑼚ªøsݸo; ½Ý¾ ù:zÅÙ2ªé–Õµ+¶Ý ¥‰“"½Ý…=ékV“u­H<÷ñˆõîêÙQ4`DOSEQ~µ@YzjîèÌ¡ ‡·y;pßÜÑÕÅ¥ƒ‹K—ömÌxxÌúËih^m‹ÅæpØOKe®þìcž|,'ãjQtòµþ~öÙ%tI•ìÒ•›+‡v ùÂæßü–œÖQ«'¶% û¬Ü{âÈ¡”0#(²7¤zLNœ$ÏŒí¹$Ç-zÑ숷7Ä.RP’¿]Òí>)aîh÷Ò3çg–Rʇ§Ž^›ã6nÓ¶_×±º’0*árµ²òbÜÈ%7ì¯X¿vÙHÿl týâV÷2 ÛNÞsøÐ‰}S;rU]beÉÉø)Òˆ%ñýZ±Zö!Ãæ¦¤¦þš8Ðâʲi鹟êp(òwî.t4wñ´>ú¬ˆÙð@ ¯EÓÃ$™ò€ª¼ 0´1æ0™$“­cnÉ«-(•“$ƒI2™$IÁ`òggÎÖy|å¬Çj¤/¦þñnÊ ,¹rì1W8ÀÏ´¾£ÁÐs‹èf”üìó4£Ú;k‡Ï»l3tõýi+¾!wOŸ{¸ô#ý6勃3ÆnÍOÛ{05ÆõÞòqëïK…{¦Ä$t›¸jÝÌ~ÎZM»£xùè‰ÄÀÉZ‹À±tiAåß.¿¸“¯4koÁUõÛ¬Ú›Èrî–‰Ÿß.óö-ؤ®£NÅݧ"QÞýRž­ƒ À2s±" oÈšöA†7´RêãÖpéLÍåÄé›ÊýcS·§­šÒÏ^RTÓ\ëèÞ:óH)­’÷Ò(Zþòá±­'jœ¿ö3gUuué¨EEáKâzZ¾;“ ÌIêç)ôîÔkØüÃy2,pÕä†i ­Mb±Øl6§F*!”Ò 9²ðNVîæ rº÷BYS+;~öòÈ£€ŽVÿö×$yÚ|ƒäjëéëq@V Àl?uù̯ ŠÖ¹¿ÚÓ%aÁ {6ˆààŽÙwŠe}õ¿Ø¤‰žº#ûÚ…iûo½t)ÝyY»Oêˆàv»˜éWÃÇnû3²Ûo'=WOîëÎhgV´cËf‚¥¥Ía0X}==]"ñ½c²:Í]ÝA‹`ôÓ/ÊÚ—ÅÜ~Îüø[Eu`ôñ@ÛŽHœ=°%ÀÛêÅùoÏù)°¡µ0ç7Á¦ÞhµI2±œÁå²TŸÇâi³éœ9Û®µ¡äFN5Õ¢öQ>eÒS_tyó«îÑìÛ»d•*u#øYr½Ãüî<ƒ¼üéK0õ·ä¾3êiæd Šå.êÞ@t-õ "lÅØ`G6€Eß¡¡¿E¼#ê¨î©ò¼C›³ZǤG CíÉXsúq{Ë-“A[&|kÇpÖ¿¿å·‹MxJ‰_ŠiŽö«’ÁÓå¸\,—ÕÒ-Žj)É×å¸L,UJ™Z¼W’¯Çq¹HÄyë €äér•2 ´šðÀ#Uû²•R·ûÿJQnžXË©“§£l:øþÚTéùmÇkÚ ïò:±—fÅ…Ýý’a¿ö;k¶¾^¾Ý{t¶æ7ºÜ½Óç_m•òÅë^%§õ¤Ý ²ö-›1pÁÂ#äÜ©||k„ÿ¶×¯Éì›wt•¿Cç.ªm .ýtâtÁˆ¶ölìa΀Ð_J@ój“Ø,‹Åb2É­1þa³öûygdÕš GÏ]Ý9£—«­á¥kO5ýI|ºg¦:2LëÃ?Ò”\AÌÏ8þiu蟷eeÜ!á²P3¦äö² +ozMݾ*¤ž"kaßÁù¯ß³¡£0MQ@rH⯮Eè®D£ÝŸ«­ÍRÔ*$“ J¥D<=‡©eßmȼnªÎá‹Ã‹rìBûšÕü^DXv³Öæ“mœÇ•Ó!f}¤xwžeØÊ?(’Í›>|]qv h{j‘ :3äý6L“öQëVöx“î°´tÄÔ48Z©dhù'lç\?¸Ir·&SÙlúy ¤"Ù«‹%I«e„ÀšÏñ!O$£H¥´ZJ ùm=޼Fòj0])«–-mŽd¯ß”QKGÓ´Äà4°RêãÖpͲýaõþ6ÇžºtùȲ=›÷Û¶qˆ]³=³WQrrÞ¨U¥½lìgû¦¯OpmÛÛ:ÚÕé:g×ÝQsfmß&V’®>;3j³õ¬u#½´ßɵtZh3$U ¦n3¤4íÔçz—¦)p¶5N›Þë÷“Êò¸pmïì0÷Ö¦4Mq82©Ê €Rü­]/Uõðz ßÁÁÔ¢­Up#_UÞKKrn“6¶Ê¢ûÅ‘$(Å›I{–±{ÏQ [^Z8sg®\ùòÁÝj³îý»µÑcE©¾Áâ²êWGZ¡v²@–ã™Ò¢ƒ%§ù5óÆ<šcbge¹I’$Q[\ Ѳ2×b¾yC’}ø ø„µ×a2€ X,’$I&“4Áhüs ßT =‚ì%üzúE}£*¯ïÈ,e:xYsY<’‹êTçÎ+UˆiädIf³ ŒTmÉâ²hi•Tõ.”BNÑÀ1okRûø¡Lçõ3 ôø|c=ñ“GÊúߪi:ØñËïæŠiÕ6•UDX´3˜:·$‹²žKhqu[c…K ºàŽªj_Yùø¡H×ÑVGÛº­±øÉÕõmåÅYT gó&¾Q‚†VJ}ܧ(šká:tbÜÆIáú9§o–7×~°¢ôôüáq÷ºÌ_?ÚSOM’®­S$“Áà™ZÛ¶²U=¬Lx –®…µŸAÉë“xºöÙBÚ´µ^7Is÷$¤4¶6ÉØÈ¬¨¤ÐÜÔ’Édz8˜íŸÓ{ÿÅÇ‘A]mÍt EQI¡±‘™F ¹&6‚ò‹G.Þ9ûsú;”øáÑß/°x¥ç7-¹n¶µV‹–ý¼Ö-˜³Êizoûº›[ãN1—x9šäY%oŸl5È•yßúl¹ÀX†¶FÊŒ#G®Y·gЦ†ª/ÁiýÜÑç#—NMu[îÜŠ_œ¹ý€[¨¥èZúò} àX}a%]·1尼修õ¿•(Ú½ú:òâóO8v6W>:˜&ê43 YÖ,›zcÕ&‘&n_ÚìÛŸvÔþ[g֓ÿçéy ´â½ùpEÑéŒgáý-8$˜Ø›Ñ'Šj‰öÜò‰A3nãßxî½ë&±¬Â&}—18vØÄ’¨~^–dé­ŒõI‡*  õ¹27;ÆÞÔûžÌìCë7æ(¬]¯=´¢çÅ$I†Úq«]؉ À ™¿ì弄Å?¦×zmzN\1Q¨Ã…þ‹&åOZ1wä¾]×p?‹[7Ø$ ã€Qƒ3§$Žø™iÝ7yþëë̲l"æŽ9‘8!uåŠi}f/X2ú×Ö¯_?¿{ëÄÀlÙ{ÚèÓ×Í™V>"{ä¬Î­zùGòØÍER¶E—!‹ç1ä˜3ü³2ó1¸tÍŽäØƒßÒsàèÞöüú‚/ºúÖ3Ì€)íu˜$€¾GdxÖº¤…—¹ h@ ù{…a”4¼û¿ZSZ$­LKš¬º6¯à‹a ƹk„VàĘ “WÆNÌ0õè;à{ëeT/“/ˆ_³tè¶ ¿ bÉæGÎø`Ɔc¸ö="üòÙ&¦EßEIÊ¥ËR§]*EÇ ~_éóF,ˆ.ž‘$f™zDÄņ˜0 ¦-z<7vÞðírž•ϰ¥ã½t ‹°„9ùÓ–L˜BiÛu›œ8¤ìY8©döê蟫úíÂgÅoÝô‡ƒÙjWª(êU ¡>ntÕÉèÙ·`JX08Ï:žlPßhICWWÎ¢ä ¿–ÉA`í?hþXW~ÓßW©iWL ïSUÅ«¢ë¯À X›1ÖÍ™½:}ñî"‘‚ièÜc꺱:Ù‡0 ]:ð“ÒY+]‡ ñ«&aý‹æ"œfgþ“×Ïp(Æ8¢ÿ_ÿ/Ó ™™™šµ½Á_ë|ÑÐ8µ—§|=æ‰÷ü¥×ÉŸ$õr;z÷Ú=¼¢öç ]žÝÑÞäf´¬¼"%jÉÁû½ƒd’æ-Ìm¬m0!„9B ÀÚ$ÔŒµ6lîŠq@!Ô„àÌ8ÒDX›„B!¤9pžinÚX›„š£GÅâ™éÙ©,´U• _y æ-Ì]]™L< }±%4Ãn1° ’I’2šÅ`àÀÍÍž¦ïÒ†B˜3 Ô ¬MBÿ‘¾1ÞÓM}vM`Ò€B˜3 ÔUmRý5狉%â+×Ï?~ö¢”ïý‰Á ímÜ;t^y¢ìÌý—q}[y´ÂÊ3¶/IDATÔÐ.˜Ä0B!Ìúih^mÒµ›—¤R©›“ðÃáa¥Rù¼¬èÇUW|Û[êqéU‡¦ŽðlÒ?¢`{DÄï¡;R#-?gGQ÷,#~âòÌÇbVûÑ[7F´ÄKC‚Ày†‡FƒÈ'ßÿâ¿6ÅR2„Ðîÿ&ÒØSŸ Šr-ÍlÌÍ-Z´0ï¡ch¶þšN@;}Šä0H‡ÿ‹ß“®<ú“OØâlY£}"Uv2nÑqè:röœ™Ã;âžås~%UmRcQöñ”% 3çÌ_wðz9Å I’!/8·#1nþ¬ùkv^-§^=±úÏM+·Þ•俆øð„ªòVzÂà>nBo7ßÞ?ÎÙqõ¥ÛÐÇš–¼òNÊaÉOëêã'º»eÊ÷>Bo·.áÃ×^.WÝœª¸”<6Ô×ÛMØ5|ÒÖÛ"ÕRyÑÉ¥?÷ìâ&ìì9wÏ @Ks2fìñ…ÐÛã«¡ Ç åÍ"P ®”ú¸%É?>'Ðw̹šÿp»¢*.§L‹ìâ!ôv ü>&õfŠ¢+GGöîâíí& Ÿ|®äí+JÓâìÔ>ÞA ïJ@ž›ò·›ðÍ£ûŠu¸k* DšHck“H’ÉãòÙlöû_X¬³íqOóûÏ+*•µ=Zõåê'«+ÊzÆpŸ62ÂOÛïgjÔyeùŸ;Òo0ºôrbçÙ½û¹–£‚Ì*¯ï;+ùrÄ4/Ùéå)™÷œºiƒôÙ…s×Èöº¬o D¡|÷¶tÍõC¥Õùþ“äiA–eelX=ä—çkRÇyé`~úþ¶˜½j@ÿ­ù4€¥÷›^]6aé]¯©+gØWM˜7u–ío+‚õ+NÄÆl. ¹:Ô8/=váØ¥ûfxòžï8#ƒ0wsþÔÙq7:mv‚GÉãNYG/Oé ¿´zú¬é–ŽÉ?4õÙDYC+¥>n†Dåɨ°é—¥ Ïÿt»R”ßÎ’µûfü0­——Rã×LŒwØ߉Yxã Ï;rÖAÕõ­‹R&Ì·?´$À€P—·JLÚ‹wÞ™aÚ{VlwS€Á5±báF¬©po‹47mÍ«Mb³X6—$Yó&Þ½ôTÌd²JDÊè”Ûý:[åTÐUôÅ+×â¸{:šþ{ÂìÅfß’—lûÑßMèýí–³ C»|¿!mnd›Ð?|Ö¡gEWD…~!ô ¹9«†¨½<%¨óð k§ è"ôv ´àô‹×]6eÅõ_Çöñsú÷ž¾ÿ™ @–êómRêìŸ{¸ ½¿ì?w®Œ*Í4xo¹ìʸ o7Ÿ˜³·÷õé·dφÑo¿Pr}Z_ôÕ œ²0=Òë»ÍÏä@Uü¹nÜ·…Þn_†|½âlõáP”ž[ÞÕÛM9-õž˜nm¼1ç úÁBmÏðÀö¶Ö>¡]Í+o]+¥¨ŠÜJ­½!—oÖÚ’,Ï«¦I¨¾~ì±yPgK.IjÌ<ƒ"_BÚ ikwïäêâÙµÿÜ5 {HöÄï|V÷‘v /:4<ÜßMè- µâB™”…鑞ߤN;¡ô‰Z7®›âèÜé»òu»/M ÒauŒÝ}èÄÁ¹ž<yÞÖ wZ½ÿÂÕ\Nœ¾©Ü?6u{Úª)ýì%E5U,‘<Úï²ÍÐÕö§­ø†Ü=}îáÒ´勃3ÆnÍOÛ{05ÆõÞòqëïK…{¦Ä$t›¸jÝÌ~ÎZM»£xùè‰ÄÀÉZ‹À±tiAåß.¿¸“¯4koÁUõÛ¬Ú›Èrî–‰Ÿß.óö-ؤ®£NÅݧ"QÞýRž­ƒ À2s±" oÈšöÆG‹Z)õqk¸tæÃÝZs­£{ëÌ#¥´JJ ßJ£hùˇǶž¨qþÚÏœTÕÕ¥£…/‰ëiùîL‚2'©Ÿ§Ð»S¯aóçÉh@š{À ¤±µI,›ÍæÔH%„Rz!GÞÉêÂݼAN÷^(kjeÇÏ^bÐÑêßþš$O›Ïb\m=}=ÈŠ˜í§.Ÿù•!CÑ:÷·C{º$,dÏÜ1ûN±¬¯Cà›4ÑS@wd_»0mÿ­ênm˜VQI‹Ù°€v•Ü¿èús™?€å:}ùÜ`}ÀÛðÑ™áG.”öë­Íf,m]=}-Pˆ˜VQ«ß}¡¯Úîš(7O¬åÔÉÓÑF6|AY”öÞÉ­Ù»r¢wŽ ¶ Ú´õèÙŒ*V›$Ë\.Kõy,ž6›Î©‘³íZJnäTS-jåS&=õE—7?±ê;½+ñ@V©R×10b€Ÿ%·Ñ;ÌïÎ3ÈËŸ¾SKî;£žfN¦ñ Xî¢î D×R*ÂVŒ vdXôú[ÔÁ;¢Žêž*Ï;´9«uLz„Ѐ8tОŒ5§‡°·Ü2´e·v,gýû[~»Ø„›%~)¦9Ú¯~HO—âr±\\VKs´8ª¥$_—â2±LT)ejñ^uH¾Äå"ç­7’§ËQTÊ(ÐjÂTíËVJ}Üìÿ¸[kþ¨Òóێ״Þåub/ÍŠ ºû%Ã6<~íwÖlyî–‰3¯xÇ®ïoÏ#*ÞÚúŒJhË7ÔeVßý}õÂ9c¸ÖÛǵåb/s„þZÚšW›Äb²9ŽÐQ[@HÄñìZ§–7 u2Iæ©Ë›bº)D4s4ˆÉ& `p´9$Ád¼:Fj±¨:%ýzÀèÕ¿ü–íMè#•Š6Éf[G—¥”(h`Áxõd®¥³9qáÙË{Õ¼Pm—ÙÈ«›CbRTØ#_/ßî=:[¿¿¤çéC‘¡»«ùVs†¿ˆ¡šP UŸG4 A²L½û‡|]qv h{j‘jÛ­¤Iû¨u+{¼IwXZ:âôûÝ>Z©dhù'lç\?¸Ir·&SÙlúy ¤"Ù«‹%I«e„ÀšÏñ!O$£H¥´ZJ ùm=޼Fòj0])«–-mŽd¯ß”QKGÓ´Äà4°RêãÖp­n·f×lÏìU”œœ7jUi¯ûÙ¾éë\cÛ6ƶŽvugºÎÙuwÔœYÛ·‰U¤«ÏÎŒÚl=kÝH¯w.™Aê´ÐfHª$ MÝf0H©ª’@óNlàp¸4M€³­qÚô^¿Ÿ¼P–ÿèÄ…k{g‡¹·6¥iŠÃÑIU”âoíz©ª‡×KøŸÑO¤ÅO¯0­\Ì>cTˆ`ñHJ,ª£Tý´WóEs-n ïí>Ü­•7×~°¢ôôüáq÷ºÌ_?ÚSOM’®­S$“Áà™ZÛ¶²U=¬Lx –®…µŸAÉëwçtí³…´ik#¼n’æîI0HcÓмÚ$c#³¢’BsSK&“éá`¶NïýGuµ5ÓU(E%…ÆFfq0äšØÊ/¹x äìÏéïPâ‡G¿Àv╞ߴäºiØÖv<(ûØ“å¹'~?eÔA_tsëÂãìÀe^ºŸ1À6w³cìMݸOàÉÌ>´~cŽÂ@ž»eÊÚŠ.}B\Í ÷â=±®ƒU嶉ËÞYÒÆý›žf‘+g%éìn¥xrfïÙQózš5{¡5Vmiâö¥Í¾ýiGí¿uf=9ü{žž×@+Þ›WÎxæÞß‚C‚‰½}¢¨–hÏ-/´1ã6þçÞ»nË*lÒwƒc‡M,‰êçeI–ÞÊXŸt¨Ò l PŸ+SÓ®]¯=´¢çÅ$I†Úq«]x|Òx$ ~Ã0pbÌ…É+c'f˜zôð½õ² ¤¡«+gQò„_Ëä °ö4lG‹œìw—¸è¶[±â—ÅÝ,]{¿§h5›ÒF뎓f~#—®Ù‘{â[zÝÛžO¾îåÐÕ·œaLi¯Ã$ô="óÖ%-¼DÈAÍ9dã—†½÷‰„–ûèä”I+Ó†&«Úšà‹a ƹk„–šv„–û˜”xAüš¥C·ÕiàøeèK6Ç8rÎÀ36Ìõïùã—Ï60-ú.JR.]–:uèR9,:õûJŸï0bAtñŒäY“”&¿îd´9¯©´)Ùý•ÒËž ÚjøÝÆ]“Új¹^8:.vIô!1ËÔ#".6Ä„`0mÑã¹±ó†o—ó¬|†-ï¥KX„%ÌÉŸ¶dêÀJÛ®ÛäÄ!íx`ÿËÂI%³WGÿ\ÍÐo>+þ{ë¦?ÌV»Ru@Q¯f3 õq£«NF‡Ì¾S‚ÁyÖñä`ƒúFûánÍ•ßô÷UjÚÕÓÂûÅTUñªèú+ðÖfŒusf¯N_¼»H¤`:÷˜ºn¬‡ÎGö!LC—ü¤ôÅGÖJA×!hüª ÁFXÿ¢¹§Ù™ÿäõ3ʃƒƒ1Žè”6ü“ä!333""B³¶7HÿKϧ¡o£|¯ÚËS¾óÄ{>pd'~3ôÞ¨ý)Azxí¿'tyvG{“›Ò²òŠ”p<@ªñ¼ðy`@ ú¿I®O ó"fïúîx»q„ÐÚßë effÎ{hø7^ˆó H£·Íœm@èÿ%…lÄs ›`h>†càšž¶ý¨qgÞ‹—ö< BýÏáá i"­Mú‡kÞ5æ :ñ×0ôý&Ïì=9~ꈠç»Ì§«=Þ7!„0g@ÿM{O·fŠïìÜg>—Ófü3²‚ÅPHåJ𦵨˜3¨!‘J¸Ÿ¼þÓÄwÒFßI)„œ!¬MBÍU#FQELªèhŽùj†òòr333 Ba΀Ppz5c?ù[ÏÝûX‡–¹êÖ>ÉÁû½ƒd’æ-Ìm¬m0!„9B ÀÚ$ÔŒµ6lîŠq@!Ô„àÌ8ÒÜ´°6 !„Bs„Ô200À „Ba΀ÐG©ª’0y@!„œ¡O¥ €µI!„B˜3 ¤N/ „Ba΀Ч`mB!„æ 5œ6Ö&!„Ba΀Z8½€B!„9BŸ‚µI!„B˜3 ÔpÚX›„B!„9BjáôB!„æ`bRÕ&Õÿ÷ï½Ipp0NS „Býs8Ï€47m¬MB!„œ!µ°6 !„Bs„>¯›„B!„9B § €µI!„B˜3 ¤N/ „Ba΀Ч`mB!„æ 5œ6Ö&!„Ba΀Z8½€B!„9BŸ‚µI!„B˜3 ÔpÚX›„B!„9BjáôB!„æ } Ö&!„Ba΀PÃi`mB!„æ ©…Ó !„B˜3 ô)X›„B!„9B § €µI!„B˜3 „B!„4óŸ¿Eff&Æ!„B!ÌÔ›÷ЃˆB!„P3†µI!„B!ÌB!„B˜3 „B!„0g@!„Ba΀B!„œ!„B!„9B!„Bs„B!„æ !„B!ÌB!„Bs„B!„æ !„B!ÌB!„Bçÿù” mIEND®B`‚PyMeasure-0.9.0/docs/tutorial/graphical.rst0000664000175000017500000004377414010032244021152 0ustar colincolin00000000000000########################### Using a graphical interface ########################### In the previous tutorial we measured the IV characteristic of a sample to show how we can set up a simple experiment in PyMeasure. The real power of PyMeasure comes when we also use the graphical tools that are included to turn our simple example into a full-flegged user interface. .. _tutorial-plotterwindow: Using the Plotter ~~~~~~~~~~~~~~~~~ While it lacks the nice features of the ManagedWindow, the Plotter object is the simplest way of getting live-plotting. The Plotter takes a Results object and plots the data at a regular interval, grabbing the latest data each time from the file. Let's extend our SimpleProcedure with a RandomProcedure, which generates random numbers during our loop. This example does not include instruments to provide a simpler example. :: import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) import random from time import sleep from pymeasure.log import console_log from pymeasure.display import Plotter from pymeasure.experiment import Procedure, Results, Worker from pymeasure.experiment import IntegerParameter, FloatParameter, Parameter class RandomProcedure(Procedure): iterations = IntegerParameter('Loop Iterations') delay = FloatParameter('Delay Time', units='s', default=0.2) seed = Parameter('Random Seed', default='12345') DATA_COLUMNS = ['Iteration', 'Random Number'] def startup(self): log.info("Setting the seed of the random number generator") random.seed(self.seed) def execute(self): log.info("Starting the loop of %d iterations" % self.iterations) for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } self.emit('results', data) log.debug("Emitting results: %s" % data) sleep(self.delay) if self.should_stop(): log.warning("Caught the stop flag in the procedure") break if __name__ == "__main__": console_log(log) log.info("Constructing a RandomProcedure") procedure = RandomProcedure() procedure.iterations = 100 data_filename = 'random.csv' log.info("Constructing the Results with a data file: %s" % data_filename) results = Results(procedure, data_filename) log.info("Constructing the Plotter") plotter = Plotter(results) plotter.start() log.info("Started the Plotter") log.info("Constructing the Worker") worker = Worker(results) worker.start() log.info("Started the Worker") log.info("Joining with the worker in at most 1 hr") worker.join(timeout=3600) # wait at most 1 hr (3600 sec) log.info("Finished the measurement") The important addition is the construction of the Plotter from the Results object. :: plotter = Plotter(results) plotter.start() The Plotter is started in a different process so that it can be run on a separate CPU for higher performance. The Plotter launches a Qt graphical interface using pyqtgraph which allows the Results data to be viewed based on the columns in the data. .. image:: pymeasure-plotter.png :alt: Results Plotter Example .. _tutorial-managedwindow: Using the ManagedWindow ~~~~~~~~~~~~~~~~~~~~~~~ The ManagedWindow is the most convenient tool for running measurements with your Procedure. This has the major advantage of accepting the input parameters graphically. From the parameters, a graphical form is automatically generated that allows the inputs to be typed in. With this feature, measurements can be started dynamically, instead of defined in a script. Another major feature of the ManagedWindow is its support for running measurements in a sequential queue. This allows you to set up a number of measurements with different input parameters, and watch them unfold on the live-plot. This is especially useful for long running measurements. The ManagedWindow achieves this through the Manager object, which coordinates which Procedure the Worker should run and keeps track of its status as the Worker progresses. Below we adapt our previous example to use a ManagedWindow. :: import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) import sys import tempfile import random from time import sleep from pymeasure.log import console_log from pymeasure.display.Qt import QtGui from pymeasure.display.windows import ManagedWindow from pymeasure.experiment import Procedure, Results from pymeasure.experiment import IntegerParameter, FloatParameter, Parameter class RandomProcedure(Procedure): iterations = IntegerParameter('Loop Iterations') delay = FloatParameter('Delay Time', units='s', default=0.2) seed = Parameter('Random Seed', default='12345') DATA_COLUMNS = ['Iteration', 'Random Number'] def startup(self): log.info("Setting the seed of the random number generator") random.seed(self.seed) def execute(self): log.info("Starting the loop of %d iterations" % self.iterations) for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } self.emit('results', data) log.debug("Emitting results: %s" % data) sleep(self.delay) if self.should_stop(): log.warning("Caught the stop flag in the procedure") break class MainWindow(ManagedWindow): def __init__(self): super(MainWindow, self).__init__( procedure_class=RandomProcedure, inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], x_axis='Iteration', y_axis='Random Number' ) self.setWindowTitle('GUI Example') def queue(self): filename = tempfile.mktemp() procedure = self.make_procedure() results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) if __name__ == "__main__": app = QtGui.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_()) This results in the following graphical display. .. image:: pymeasure-managedwindow.png :alt: ManagedWindow Example In the code, the MainWindow class is a sub-class of the ManagedWindow class. We override the constructor to provide information about the procedure class and its options. The :code:`inputs` are a list of Parameters class-variable names, which the display will generate graphical fields for. When the list of inputs is long, a boolean key-word argument :code:`inputs_in_scrollarea` is provided that adds a scrollbar to the input area. The :code:`displays` is a list similar to the :code:`inputs` list, which instead defines the parameters to display in the browser window. This browser keeps track of the experiments being run in the sequential queue. The :code:`queue` method establishes how the Procedure object is constructed. We use the :code:`self.make_procedure` method to create a Procedure based on the graphical input fields. Here we are free to modify the procedure before putting it on the queue. In this context, the Manager uses an Experiment object to keep track of the Procedure, Results, and its associated graphical representations in the browser and live-graph. This is then given to the Manager to queue the experiment. .. image:: pymeasure-managedwindow-queued.png :alt: ManagedWindow Queue Example By default the Manager starts a measurement when its procedure is queued. The abort button can be pressed to stop an experiment. In the Procedure, the :code:`self.should_stop` call will catch the abort event and halt the measurement. It is important to check this value, or the Procedure will not be responsive to the abort event. .. image:: pymeasure-managedwindow-resume.png :alt: ManagedWindow Resume Example If you abort a measurement, the resume button must be pressed to continue the next measurement. This allows you to adjust anything, which is presumably why the abort was needed. .. image:: pymeasure-managedwindow-running.png :alt: ManagedWindow Running Example Now that you have learned about the ManagedWindow, you have all of the basics to get up and running quickly with a measurement and produce an easy to use graphical interface with PyMeasure. Customising the plot options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For both the PlotterWindow and ManagedWindow, plotting is provided by the pyqtgraph_ library. This library allows you to change various plot options, as you might expect: axis ranges (by default auto-ranging), logarithmic and semilogarithmic axes, downsampling, grid display, FFT display, etc. There are two main ways you can do this: 1. You can right click on the plot to manually change any available options. This is also a good way of getting an overview of what options are available in pyqtgraph. Option changes will, of course, not persist across a restart of your program. 2. You can programmatically set these options using pyqtgraph's PlotItem_ API, so that the window will open with these display options already set, as further explained below. For :class:`~pymeasure.display.plotter.Plotter`, you can make a sub-class that overrides the :meth:`~pymeasure.display.plotter.Plotter.setup_plot` method. This method will be called when the Plotter constructs the window. As an example :: class LogPlotter(Plotter): def setup_plot(self, plot): # use logarithmic x-axis (e.g. for frequency sweeps) plot.setLogMode(x=True) For :class:`~pymeasure.display.windows.ManagedWindow`, Similarly to the Plotter, the :meth:`~pymeasure.display.windows.ManagedWindow.setup_plot` method can be overridden by your sub-class in order to do the set-up :: class MainWindow(ManagedWindow): # ... def setup_plot(self, plot): # use logarithmic x-axis (e.g. for frequency sweeps) plot.setLogMode(x=True) # ... It is also possible to access the :attr:`~pymeasure.display.windows.ManagedWindow.plot` attribute while outside of your sub-class, for example we could modify the previous section's example :: if __name__ == "__main__": app = QtGui.QApplication(sys.argv) window = MainWindow() window.plot.setLogMode(x=True) # use logarithmic x-axis (e.g. for frequency sweeps) window.show() sys.exit(app.exec_()) See pyqtgraph's API documentation on PlotItem_ for further details. Using the sequencer ~~~~~~~~~~~~~~~~~~~ As an extension to the way of graphically inputting parameters and executing multiple measurements using the :class:`~pymeasure.display.windows.ManagedWindow`, :class:`~pymeasure.display.widgets.SequencerWidget` is provided which allows users to queue a series of measurements with varying one, or more, of the parameters. This sequencer thereby provides a convenient way to scan through the parameter space of the measurement procedure. To activate the sequencer, two additional keyword arguments are added to :class:`~pymeasure.display.windows.ManagedWindow`, namely :code:`sequencer` and :code:`sequencer_inputs`. :code:`sequencer` accepts a boolean stating whether or not the sequencer has to be included into the window and :code:`sequencer_inputs` accepts either :code:`None` or a list of the parameter names are to be scanned over. If no list of parameters is given, the parameters displayed in the manager queue are used. In order to be able to use the sequencer, the :class:`~pymeasure.display.windows.ManagedWindow` class is required to have a :code:`queue` method which takes a keyword (or better keyword-only for safety reasons) argument :code:`procedure`, where a procedure instance can be passed. The sequencer will use this method to queue the parameter scan. In order to implement the sequencer into the previous example, only the :class:`MainWindow` has to be modified slightly (where modified lines are marked): .. code-block:: python :emphasize-lines: 10,11,12,16,19,20 class MainWindow(ManagedWindow): def __init__(self): super(MainWindow, self).__init__( procedure_class=TestProcedure, inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], x_axis='Iteration', y_axis='Random Number', sequencer=True, # Added line sequencer_inputs=['iterations', 'delay', 'seed'], # Added line sequence_file="gui_sequencer_example_sequence.txt", # Added line, optional ) self.setWindowTitle('GUI Example') def queue(self, *, procedure=None): # Modified line filename = tempfile.mktemp() if procedure is None: # Added line procedure = self.make_procedure() # Indented results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) This adds the sequencer underneath the the input panel. .. image:: pymeasure-sequencer.png :alt: Example of the sequencer widget The widget contains a tree-view where you can build the sequence. It has three columns: :code:`level` (indicated how deep an item is nested), :code:`parameter` (a drop-down menu to select which parameter is being sequenced by that item), and :code:`sequence` (the text-box where you can define the sequence). While the two former columns are rather straightforward, filling in the later requires some explanation. In order to maintain flexibility, the sequence is defined in a text-box, allowing the user to enter any list-generating single-line piece of code. To assist in this, a number of functions is supported, either from the main python library (namely :code:`range`, :code:`sorted`, and :code:`list`) or the numpy library. The supported numpy functions (prepending :code:`numpy.` or any abbreviation is not required) are: :code:`arange`, :code:`linspace`, :code:`arccos`, :code:`arcsin`, :code:`arctan`, :code:`arctan2`, :code:`ceil`, :code:`cos`, :code:`cosh`, :code:`degrees`, :code:`e`, :code:`exp`, :code:`fabs`, :code:`floor`, :code:`fmod`, :code:`frexp`, :code:`hypot`, :code:`ldexp`, :code:`log`, :code:`log10`, :code:`modf`, :code:`pi`, :code:`power`, :code:`radians`, :code:`sin`, :code:`sinh`, :code:`sqrt`, :code:`tan`, and :code:`tanh`. As an example, :code:`arange(0, 10, 1)` generates a list increasing with steps of 1, while using :code:`exp(arange(0, 10, 1))` generates an exponentially increasing list. This way complex sequences can be entered easily. The sequences can be extended and shortened using the buttons :code:`Add root item`, :code:`Add item`, and :code:`Remove item`. The later two either add a item as a child of the currently selected item or remove the selected item, respectively. To queue the entered sequence the button :code:`Queue` sequence can be used. If an error occurs in evaluating the sequence text-boxes, this is mentioned in the logger, and nothing is queued. Finally, it is possible to write a simple text file to quickly load a pre-defined sequence with the :code:`Load sequence` button, such that the user does not need to write the sequence again each time. In the sequence file each line adds one item to the sequence tree, starting with a number of dashes (:code:`-`) to indicate the level of the item (starting with 1 dash for top level), followed by the name of the parameter and the sequence string, both as a python string between parentheses. An example of such a sequence file is given below, resulting in the sequence shown in the figure above. .. literalinclude:: gui_sequencer_example_sequence.txt This file can also be automatically loaded at the start of the program by adding the key-word argument :code:`sequence_file="filename.txt"` to the :code:`super(MainWindow, self).__init__` call, as was done in the example. Using the directory input ~~~~~~~~~~~~~~~~~~~~~~~~~ It is possible to add a directory input in order to choose where the experiment's result will be saved. This option is activated by passing a boolean key-word argument :code:`directory_input` during the :class:`~pymeasure.display.windows.ManagedWindow` init. The value of the directory can be retrieved using the property :code:`directory`. Only the MainWindow needs to be modified in order to use this option (modified lines are marked). .. code-block:: python :emphasize-lines: 10,15,16 class MainWindow(ManagedWindow): def __init__(self): super(MainWindow, self).__init__( procedure_class=TestProcedure, inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], x_axis='Iteration', y_axis='Random Number', directory_input=True, # Added line ) self.setWindowTitle('GUI Example') def queue(self): directory = self.directory filename = unique_filename(directory) # Modified line results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) This adds the input line above the Queue and Abort buttons. .. image:: pymeasure-directoryinput.png :alt: Example of the directory input widget A completer is implemented allowing to quickly select an existing folder, and a button on the right side of the input widget opens a browse dialog. .. _pyqtgraph: http://www.pyqtgraph.org/ .. _PlotItem: http://www.pyqtgraph.org/documentation/graphicsItems/plotitem.html PyMeasure-0.9.0/docs/tutorial/pymeasure-managedwindow-resume.png0000644000175000017500000023155013640137324025330 0ustar colincolin00000000000000‰PNG  IHDR]DOÓß pHYs  šœtIMEà%"W†â IDATxÚìÝu\éðïÌöRÒ%ˆ bŸ­ØÝŠÝg×yv·gwœ}gasÖù³¤cÙšç÷Ç.©( ç!~Þ¯{y°Ì<;;;ûìó™ç™g¸¢S xì@~ä@~ä@~ää@~ä@~ä@~@~ä@~ä@~€¢Ž~}ûìæzÿD¼¡Úå8C㛥}@djïdOD¤°tp´¤=ÕÃ>ReK$Ü7>”òdŒ!?ÀeëÖ­DÔµk×/]1,,,00Ð0öÇÛÛ›1vñâÅgÏž={ö¬Aƒ¶¶¶ßS~ày‡@âîú¼OD¦Åšvjé]ÐB$$¾r#0ð¹4]SߘÒ¶À^¾C =\7eë3"‡:½WµÐkt6Ñ›ZÕÒP}3Íûk¿ÏþãQɼÚi_LüêèÊMWcI\°Ù@_Ë+{߉JÔ2âäVŠWmX¯¬ƒ\x~ÕòÓïɤXµ¢‰·o¼ˆY«ç[;ßÃÇ/=ŽdVÅúµ,o+ÑS®ì¥º{ëU¹²^£Vë_#‚F­Ñ‰>¶Ø[ÿ9ãý ?š•ë9¬yËV÷–ì| ~tôtÜôÔÕX"±Gsßò–ÂWÁ‘Ì:¿›Rñê]ä‹ën¬‡µpá ] ÷/\“HˆHˆ¸tÍ}"‰„ˆ(òÞ±£EŠt-©0.–ô¿×ì­$ñïcžœÙ}ÚmPå4¬zzdÝŽñœ•g™"Òw·ï=½´g»d@ÿÚ¸. ½Zµj)Š»wï>zôÈðHÉ’%+V¬¨Ñh¾ÍäØõèzÈQÚ¸·QDD¼SQ[^v~ÚSoµDD$um2 [™Ô*˜Œý tg÷Ó×ÑÖØzÆéE‘æ-‹>Ýý áæ®mDDâB-Zÿd"ú¢~ãË[›‹ubÌóã+7_‹O|ù"F›ßJL+öÕÚåís×\K e™£Û{Dž¿üBŒ>âMŒ¶„ÜøVµû ­ï@oŽ,X~!&þÁíÐò–)q'úîéñDöu}›”2åâm"Ö¼{ëNx5;È3vìØ‘vÚ¡mÛ¶~èܹsÖ Q«Õ*Txýúutt4YYY•/_^­V³Wù[r'Fœ±m­&Zmò_4!m HΔ!.°ô¤+Ûø[šñKL—”¨ÖëåE›·,òtÏC!=´*©Ô ŒõÛ‹·]}š˜ú¬z1cdzT)Ñ&jd6¦D $5‘é“$Ö¦D1¤×¥ÝT¦U©TbS7[îB ‹HÐ[¦„¥ÈçBO­]r*e3UÑ*|»@.±}ûö>Þ©S§/¨Ù3©Ò¾¨ªáüùó†ð@D‘‘‘gΜ©R¥Ê7 „ü;ñf–DqÄÞ= I*ä^{äܺ§;f¬½¥&Žˆ1^Ìé5Z=cŒHЪuDD¼˜géŇ}Ä)¿èTññ’´_8L›¤M>5¦OŒMP r±>òŸ]ÛßY¯Y¥ˆÅ»Óþ×¢ÓgÆ–rý6 #J¹B;Ã…LÐkôDD|êŒRÓŽMÕ6õRæ€âÄö<¾] wû¢ZªOŸ>†Ö®]KD}ûö5üŸõV÷åË—_¾|IDE‹ÕëõAAA/_¾ä8®råÊß&Bàþ¹4?(=Ê»‹‚Ÿéÿ÷ÇëÖ +ÌÇ©S¿¥D&vV Ó²WW•³ÑÿïN$‘…“¹èë«ó˜;GŽGвS¯êæmõ"”ˆ8Æ­| ‹Þ_ºÙ—h–ž!þÙ­7Œˆ¬ X¥~‰ò¹YÓƒwªµ­SÊZL$$¾¾u+Vëj+Ç¡¹AJ[?ƒ¬7ý?\ø‹Ö5ˆˆˆxõêyzzVªT‰ã8½^ÿôéÓàà`OOÏïiþ%\ÿó”Å[µx°jÿƒDÕãÓÛŸ&^Ä ú”jWâZ³²õÃóÚçÇ×Î=ž¼ŽÄ³V+^Hxô玓OßÑ»ÀmëŸUömSÞJ”±¥ÿöèÒ™G?›—ëÞ¿jÄ‘cO4DVÕû´Xsî}Èÿ«ž 9šÓ“höt߆MŽìݳÐ4U?}äçt_ ,µ£"òüÖU·)6*A »×ø)w;y΢Tý2m½™ðÄÙìsÖVMdDœÎªf¿Òrƒ¸¸¸/ó+ÚÏööö>>>/^¼¨R¥Šáš‡5jˆD"777;;;½^ÿÝäŒ_ÈyŒ)ê8Âîú©3—ï> × z‘©mþ‚…K”õ2%Æxûºýúšœ¸|÷E¤šˆ7±+T¦Vã:E•z^Ÿñ.*ÁXŽ*:,•JUµjU­V«Óé¾ÍÆsE§d³ˆI…#jÕª¥T*q(ä|5-–ÊeR‰XÄ1A¯Ói5jµN0üQ&—K%"ž#"&è4ê$µFψx©©™"Í0&A—”zVŠ“˜˜+3œ@bš„$^©sÄ4 q*‰¦&RžHŸ— “(•r1OL¯QëÅr)o(‘ÉÌÌäÆŸõ¼ÌÔL.JþŰL§ÒD^Ù¸êT8YÕ6¼“”#A—¤JTëŸvu"^,“Ë¥âäW£×i’Tjâ@N:zôèô ëì”3ýÿ¦Ó¨t™ÝìéÔªxõGþ(hâc>18Ó&ÄÄ|ì±iVÒ©âbRŠV'Ä¥N ˜úŒê¸ušŒ’æ—m€#N›jÒ¬N$èÔ‰ñj¼å¹[Ž]ÿ€] ™|G¤~Yàûùù>…ÏW¾×¸òø¾@~@~ø±ðØ€üß7+++ìä€,‰ŒŒ4D ä€,Eˆ”ù Sèv@~È*Œ_@~ø²A¿€üðYèvÈÛùEœîÓ¸©á¿Ÿc³K9snXóh>ü“êÖÜæÉÏÕÐoð¯Û¯½×}ë}÷‰ÍƒìÃø%€\HœsEqVufïÍ¿}ðè|ÍïàaÆýë_°÷ì¡e¤‰¡÷O¬Û0e¼håÊ®¼¥y+BPÖÆ/]¥YúWÜ•IØiTt“÷ñ6­î.î€Ü–ˆËM•Ržãå&& 1ÅŽi¿Ö¡Kýá]/46»NüÅ×S©}¼²×¨›u:yÝ:#/PgÐÄÁ ó‹ÞùÿÜÝß{õªŽn"ÕYݦ&Ž˜ì´bÁ=ÝÚz‘[Ÿu«[9¦Û\^éäîéeF^E½,ƒo?ûϻƱ‡§.:ñ8,NG2Û’ÍŒìUÙ†Bö÷ï¢òкoöì¼lÒoà¨esüï„ÄjHléU·ï¨~õœ%1cÛ¯µëY?îÀÞëáæ%ºLYõå–ÙϽä 4Áÿêkìøò‰$C¯°”±—Zã5Õoì÷;u஼Žúªñ½i훑i±ã§´qkC7/]ã;L/RšKõ:qRS¹ñõQ7~_ºfÏ•7jNn®LÒ‰ò!^"戈x™‰ŒW‹y""Nn*ãô½úÝý7ªçë{µÙb¸¨C§Ñ¥ôi·%öEPDLÐäÎ Ïôj­ÝûÁ•8‘ÂTÂÉÝjV¶˜º¸oÿ³ÞÊ—¯V¿FQ+1ŽÀLe}ü’™D‹Ýõ™]$Æ.€ï"?¤â¸]O͘žqb©¡}Í„/,Ó½ï¼-–övRŽHÿîðÜYGx¿)Ú—µ—GŸÚ}çÏsié¬ßßúLZ³¨ª‹‰úú´öó?½¥ÆŸ™žÄŇ­_Ö$å/3ábš’õ‚Ȧմ¹~)—ps"…¹äAÚ‚Í+Œ^½´êé¿.߸¾kÁ¡Ýÿü²i\å|Â3Œ_Jù÷KZJ1Ög`Àw—>Š%¾¼"rno+“ÈÅ,)NeHL—Üo âHÐë3Y›W8ps3Kù]ûöîk*òs›rö "&ìÃdµïï?׸·iUÕÅDdX&KÛ)±ó²n<ŒRø¸$_ƒÊFñ©›Ç›(¨Œ~ÂY”¶%g#Æ¥Ÿˆ ¼¹GõVÕ[u ÙqÃ¥`uå|r„ŸŠ”…ñK–4Ž?—dšè˜è[wn©5jì/%“ÊJ—*man]Ãùé’â5’T:³ÌÔ½ºpò¢Uq‹„{û–Ÿ—Ôœ^Öœ—º·Š:¹í`‰fNQWöoû+Q¨LÄɬ]L¢®ž»ú€Ë§3q/á¬øäÉz±§ðûÁ½Ê2²Wg·oz¨³*—1Xz8Šö;pÒ¹Šåû+»×_Kâ³r‚È®ZÛ*[çL›c5¤m%'.âÑ?§.KÚýÚ.íæ¹´iê8`õ´ß¨G/³¤W·NŸxViì“4¥$Þ\2ù¬k³FÜMãoÝ 9յÿ™Ëúø%K)ÚÄŸÛEõó—Ï«W©.“a"×/¦V«?{¬Õim¬l°7r0?°È3ãÛ/yLDÛ‡wº5vû”ÌÞ_^ý˦wZ“‚µÎé_ÚŒ#ro7¦Ý“9ÛçN’¹ÕmÛ¡Ò«DD²ÂúÔ¾»löCfå‡ÿ6ësùÁ©ñÏ}ïÍÝ<÷—VÅšµjQðåùŒ‹pV5† ¼>cý‚I‡Í ÕmߺأCYzm¼eÕ±ó‡­]µkÞ˜j’Ú©Ò¢›Dfšnó:Μ-^½vëÔñ™¹”¯íë¦àÂÓ"u*[ bÇ‚Q›IdU´á˜±õD83…ñK9™¤š’Å*h4ûêkx¸yœ¿t¾vÚØ\Ñ)Ù,bRሠduiÍ㕽FõÞº¸¦9FþCŽØq,pp§VßìébÏô™ëºîฟþÛ±gê‡ |ûÝzhC½Ï_D³bÇ–5+þ‡õLÚ ÿJÁì_ß–ó—ÏׯS7øÞL²ÎN ˜r©¬_²‘©3ké÷ª?ñ¦±ÕhZ r›ã{U·Ë±c>áÊø†D ýgTTfeq!áÉÉu«·ùçI„–¤6…*ÖnÖ¹sËŠößb›¥DÍ8Ž}²9Eì‹×ÊÌðñ'eÙØ.«ë0âp²ùr»¬_ú^ZiÔÒ¡EÅñ!Ww-^;|œåµí\þƒ£ž%ÜYÛ³ßÖ÷å: š=¢™æíƒËGö®žgYagO·o xŽ1–¾͈#ÿÏø‹ï§[¼Ð°uÓë¤öj|䌽ðÁŸ¸4ó0¢tMnÕýEý§>i¶hYgqÚÙÍ2Nqú7–.épiŸ;µË!ö¸~; Ï\’|k—Ú§[‡¾ÕdÕ¼ÆÉC?:û1ÊlØ¿Öc€ü𥤞·Ä~‡,FÊÚýã>q„[¸.ZTAE‹Òÿsnò¹‡ í\,Hˆ{°{ÁÜu'‚¢™yá†}§iUX¡~~dÉ䕇ïF ›BU:M™Ù2zjóÑS¯¨jJ¤ÙÛ£ÕuvÿÞÝp7lý›݇&õ¯S…ȮӦ­­Ÿ®L·º_šV­öùï3¶—÷ÇâæÎb"¢R?UlЦó“·2‘úáß~·;ޝôÏšÍÿs_´·Ë• Óþ|ø.VK2‡²íFü:¤†½Þá×aÿO½*ð¿Æ9×è?wr‡bb"bšw– Z·íz¸¢pË_ç¨gŸé'Z„$F$0â$%ºŽî䑼½¼‰“ ãRâÒµ£¹të§Ç¥û¥.(vlÔwPl~ ž¥›}Ø¢ÿxqBñ/L`DŒ ¶L0®!0Æ¥ ·ÿcÀ>ü+ûª~ä€o([Ýk :=Iò)EDúÐÓ†ow™µk®kÒí-“fŒXWdW«cgŸ/8rù¯edá.<ŠÒ~²É(rô]>ëzëé¢iÛ'”UŠÄчze\Ròƒ&øÌÑ—ùO¨çœö'±.äJ¤&"íÝ¥K¹M -í!‘¼,ßnb¿Ÿ\ÍÔÏŽ.œ¶"!úüÂQ+U~‹z«æŒ>Y¤Mé·§ÎÜäíË·5¨‘‡‚#!ööÁõëþ¼þF%ÊW¸vA]ªÛëo/4Wã×ÃþÂö“a&ülòûê{m––·}»qøä{uüܯî;ýRï\­ç¸öfgV­;t?>_‰VcF´,¬¤ÌJ›£òílwi÷©§qæ^­† ïäõ~óØåuôp|7"×.‹6±±ÔìBÄ„4343FúèÛ{Woþó¡Iëâ>w¬d/!}Üë×î»þN-µq±ˆzmÖ}ÍŒ:Ö|º·ž#Æ —Ë™ñKFú¸çVm¸oÝpqIÒ¾8ºùŽçȽ~•­x"Ÿ~½÷û¯:÷´rX(sèPµT!{¾PÁ¢Þ‰T7>U$/55•ò"‘Y¾|–JJ|ö$ãêihÇ’séü™ÍʽicãX›ÎýŠ“ ‰y&ªZ=ÿ§¼×yKˆ$‡,žÞÝULTÃ5ôBûÃ'Ÿ÷¬G$.ñËš¹M¬yÒ¹¿>pèn¨¶’©ä‚RDÊ){FŒˆéõ:Îø0Ç‹œê h8zÇö+±kÓ]çNsjØRéÃOFµn9°‘øÉÁMÛçî-ò[g—°?çÏ;çÔeÌ¢r–17ö,]¾È¶à 1ÕÍM»Š×¬Û¾ÖO¶¢†=1Fúçιuê=Zr{ë†Õ£nÚ•jä7ªö›CëþXûWÕÙ ,ßž?÷œS×1‹ÊæK.mºÀ„¤Û;OÔh×kTã°Ów¬:轸£ß¤žFí÷;ͯ€”W˜r,唌1"FŒ%' "Ò…øÏYà/k6lfe»¸[»V,›*žº¨½Å…eswGUí;¡Ÿ³îÍßû·¼Ö3&$óJà &äø"esü’xbpÝDDâÂm¦lZÞœ£ø7·^%\ŸÔ²îTÃ}ϵj‰ƒ¶@ýê£f´÷ ¨Y¥²wfuK~ÑTÿr÷VÏØŽOn‚ÆŸïë3öÙwÜt¨‰äærÞ5BN®œµxÏwz‘‰…,AŸO›Ü(æKH‹»H¶Gh‰ˆKE¯´Rò:•6óV®À—nÄ?—ºYBÒ¥Y}.¥,êÑgíÔ®{7ùkÚúß–yø·UëéõìxÊHìÜqôà–Ž"¢²ïoŒ ¼ÜRwêxxÙþã| +ˆlkù6:2öÒ­ˆšNDÒ’ƒç­hÎi_^gDŒ11&òì3n@u NïrüüÉrÃF´rS;wteP˜¦fÔ‘cáåú÷)¬ ²«Ý®ÑÑ1—nEÕr"NVvØ´~¥Mˆ4ŽMyô,޹ÉebŽ—*MLM¥D,Í(&A "Òö?h_ž9ìØnAË ö""§¾oÚð ’ãÁ{­fu©ã"&rS<ö?þ?b,ùbƽÄ8–þ‚ä€Ü'gÆ/ñÒÊc—s}¸rô¢{ñb…„#"¦×ó¦µçlQ2eV‘Ì"Ÿi™]›k=ù×?ož²{Ëù9û'˜fý¼3Ÿ¯ÊÔ «O¯aelñ“ÄÆÃ–îüﺹ‚”e&mݯ ?:iÜ©ŒÅèCü°ë¹ðnäQG»·Üøás1A ãKIÅñŸÙBÆ2\=ÍR‚qâ]G§\¯Á+ì͈‘̽Mïšgï:“¿õüúŽ"ÆôŒÏÚ×"w{ñŸ!QQÁO£âžÍtEdLc­uD¢àÄ8‘\)6vÿaŒq¼ˆcŒI•žã91F¼L!´z]ìÛ§QqÏæ LWZ‚Þ‰q¼HlX“™+8]’V`|òëÊð&%÷;0¥4û“ŸGË]ÝÍyÃÂ&ù=ò©î¾|.w/f#Jî°ÈP—|Ùyòe€ü¹V_›;{x•)6}Þë.'(´am—B2§bv‰ç‚ÔæµóKRšäŒ Ì¢p]¿Âuý„ú÷iòÛ¹çÚV ‘ð:N#ñÄô}Æ&$Ç“ Ó[á—auu +…qA©k-ÇÍ;×赸…³ÄÔ¥pқߔxJ[óææK*1±Se'%‘2'¤מëówrþÂI ‚>“`!0NlæXÀ£ <íÒ1IDD”¯Òê'Œ1AŽˆôzF"‰HÐ "«†£&´L¹¶ƒËMEI‡ \r c,ùJgAc'AH`G$úKÉMùÇÉk DK^ÏK '/nˆDÆçRòƒ PÚe ë zÀñÄ  ‚1å¤öZdˆZ€ü¹=BPvç_2àÍËZ:ò‰ß‘s nžT¹y璻枮Þ¶‚îÍí3‡n¸ jqÑ©‚m[Vñ4‹»~%TäÒÈÁÄ©¬`ËÆƒ&Å®Ûø\W ]¡Rkw«ÄGÎܲpeºÿçÝÓ­žvNQi¡îãZ6·ËˆWƒÛWó´ ø°»×Þ$öE…u»¶4©({°~Å]­M%Ã_´¡œ-RÅAä?O¼÷äÚŽ¢÷_´ 2íII«“q¤gû×*´«tiïš#Ugµv5\œ|~^rÿµ`×È.Ÿ³‹<æé;2-f!Jy&NmlÁ&\2F…ä 2ô…°äÅSžœ3ùDi†%“/i`ÄÇ‘ ×§ë0†‹ä MIR7ˤ;O¢uEíED,>øiŒÜÉÙVfªzþ:V_Ô†O-XÀµÈðÊéù—$nm¦Ï{ÔmØ„‰ž[—t˜¿B¿xÉ– ýkÉĹL½Mì\M=Â7Ní»2D6%[L™ÞÜY"µÉ F IDAT3òâ¸ßfŒñ·¯Ð¶KÇK.f(±å¨v'ÎpTQ|àœVá[Ò­.J`,*ܾÎcÅÚ½¿Ý£'©m‘ª GLnë&¡çi?.­&üü¿I+' Ûdó“¯_ûBÏN§4Œ£¯m±)8Q–¿fÿ…SêÙðÚ/ÍÂGïƒÆƘ.úå£{œ1ñp¼ÒÙÃ>òȆãBíI­ê[¹Þ¹qS@åñxFºˆ›®(e­{ù×Ö“ª’}ÊY)”êÚMܾtùÖt7Mzûàâ_ÁetS‚‚¡-.PÚäÁH`L04ò —Üv—¹5®k7áã¥1C1NÎÊIó9*bþwéÖc2×)\½R:tc$¨Þ=|ðÞ0¬‹W8ºV«çrrßúC~•lâníÝ”¯ú/E êʛ۽å°i½ú×7„éèÃñP„ë§ ÷ËñKæµ7\ª¦ oYýÿ¿~)ÑqòúŽ“Ó.]dÌo Ƥ/@Z ùÌÍgÒq(Õ^yÅXªuõÿ‘¼t¥Æc>µ5¼EÉÖ~k=!ããEFý˜ò §,ÒeþÞ.)¿w@Dº×D‡&“·tÍŸæ+K»"oÝhÝ?>™"NøøM˜îÁ®Å3Rqï3ö§€£ñÞڸˈ«Ø¡ÍÉ_÷n:_®;#bñ÷-òM’Ú•m=¬_E ^ ׿£Æˆ¶ïÜ·ôt¼@&Ž¥¼;ËX¤±o°dœ)y:$à Æa8åoq$qm>z¬è÷©¥5Ê/c†4bŒÉYDâÖ²}å ­+=iRª×”Ñv2>õEœ]1ïlò+ÊßeÞ¯~œ¸aÇò©jD–^µûŒoí& ·‰\shÙBiÁª5KX¼ŒsÉ7–H}S8"䀔oÆ¢S²YĤ 4À®„ÿʉ3':øvÈÃ/P÷z‡Ÿß‘f;Óç‡/±ë]¥Š–bYÈŸz×6.ÃÀ}XÀÄI—jLÿµ‰ˆq™]À}Ùå™ßËí_¬õŒ÷“Ëð¬ê‡Ç/Óõ[Ô×Kž~û8ŽcwÜ©_§>>nð½ ˜dÐÿ¹TN_úÑ )weþh >EÊ ×ÒÎõJ,íUЂq:".eq.uÝŒ÷ŠþÈ×34ÊÓ'Ž>r×猿qÞGúË¥¹Á´æùÉcÍ= ÙË“^]Þy…+=ÌEÂ2Þ#}ÈðÈÑûÇ}çŸÒü÷žï˜ÍB’§(å>ѦN÷î£Íþ´œ²Öâ>L’[ß죥¥vX°Ï›2Mj? ÷±5Ø'_§‘^ûþÎá}Gât$2+PÉwpGO9¥~‰K¿€ü¹¿ûXø0i°Ì6"«-öLc ÷‰Â¤íÆ/k—n?±Œý.”|8 ?@.†n‡œÅ˜ÓèÙ9ýÎr¸¼¬>!˹¢ùr;Œ_ÊéüÀ#îÃö0—+¶.“ǹܳÿp ?Àw!ã—rˆÀ s&}0¢'Ã\–šÌ\VÛþ,“Ÿt?±kµ³ÌÓ÷Ueä²€ü t;ä¬Ìï€ÆØW´Ø³°XÆKµÓ=Ël1–IcûäVg–lX&›Ì}êÕpb ùr;Œ_ÊYOž?ÆNȦgN`'Àwá_½gòäêA¿”Ú·iðƒØ½o÷¿Z>òäR_ÔíÀp‹/€o‚Ç.€ÜÉ0rÉ$°7>!ã—> ÝÈY…ñKÈ_!ã—> ÝÈéÅ]Þ´íŒ[IÿVùê‹üšú4nêÓ¸©O—µAÍ£Õ]Ž>›½©>oÎlÞvÎM©îÌjÛ´ûÎW:J9 ã—¾-™×  '”à%åÆï\Ó½ô_x ©K‹¡#ûV±áPúw"aü@n’ûî§y{nÓÒ5Gï†kåÎZæWÞJD,éYÀš…[O?ŠÌܪw2È·¨©>dÿþGŠù•s,àÖ{αr×I£ZVriÓ‘Li¢s¼Da¢”ÄŽäI4·u“¹¤¨6ÿ÷±ÅÞŸYµdËñ‘:…s•öÃF·-¦x»¿ÿ•‡Ö}³gçe“~E-›ã'$VCbK¯º}Gõ«Í:ñr"Ñè6M‰lÚ,/Z¿ôV§ÞLô‘×·.Y½ïÚÛ$‰M©Æ½Çô¬æ QݘÙm²ªS/‡À­Gƒb-Šu3±g Šþßï‹Wì¹ú6Ilæ\¬nÿ1=½­TƶŸÜ`éæ!Åe8,‰¾ðþqðmä¶þ‡¤[~™yVÙêץ뗎¬•xð—©þ¯u,öÚòÑ¿Ýréþ²šÇ¾‹ºj“7.iJäÞkØãË£þ¸^½‘ص״±œÅD•ó‡ÿÓçä_ÁÝ<=3§Ä‰MLe<'VZXX˜sBxÀ® ~k;VvÙµíYáèÌ3Ï“:‰:.XÔÓÝPŠKw7bº„ÈpIùªvî~¦«WÐDÂó"S ‘&ÔP¸æÅñCÏów]Û¡š“˜ÈeDָ߫?ôHÄÉ+M\<¢‚)‘Úêþ¡‘žDÇiÞ$˜x–+SÈEI.%¼‰ˆHRu¶ÿ)DÂø%ä‡Ìèc^'YUt31 B[zº+"½ Þ U†…¤Ž%¸AáÚrDD¼q¼’ÄÎËYüÇ›(Q–®sм»ÿFõ|}¯6[ è4¡T‚@DœÔTnìÐGÝø}éš=WÞ¨9¹¹2I'Ê/dríµ:ôI¤¢ —¥ar¦n^VªÿÇ ÎÄñb‰¡8‘"Ÿ’Ó& –åkZ¹i\·g•¼Ë•©R»n%‡#ñ¿€üð/b‚@"¹$Ë-q¦'qñakÆ—5IyHb¦Œ I³DÌ¥¥³~ë3iÍ¢ª.&êëÓÚÏÿ¢ úàÎ#Ä®¾s¶zž¸xýÆéµGwï¶rq7 ŽÅ ã—RþÅÈ r×€{‘EòÈû/â mo]dÐS•UaG»ÂŽü»»¯U†GÕ!·Þ0;OÛô-nõ«[Á‚c1‡Ov>p»{›—~“X¹´®o5tíò½+™‡ž^³ãµ{džl,|«‰'¯Xqlx»ââ§G~û3¡ÔÈ*¶" ‘.üÊ鋞åí´OVø'”YÍ>ÝDª‚:1A¥c‚V•¨‘+Å–®VBÀ™Ó·‹rdïݶÊÖ9ÓæX i[ɉ‹xôϩ˒vê¦Y[béá(ÚwìÀIç*–ï¯ì^-‰¯KDR«–ª£'/Ü7wÄÆ-—ºÕoáî¿uÉ.ç>5ìc®nZwϺÞÂ" ºÿáKÖ½þcê¶h例‹Ùro®=N´ð(`ÆSÂÅñ~“ƒ,Ý8ó/ ÛùáÃü »½nʈäß”5gí>c‚nñš©C×ꤎåZNßÂEÌQ¹ó­^¸uBŸXfâZµÇôÁ>6¼>„ˆXìÿvüºóJêX¥ëÔѵ¬ÒðW?ZÑkôÑ"šíׯùÊ }=«÷î|nÖªñÃÅù›/þ­ÏØùÃÖ®Ú5oÌ5Im‹TiÑÍ&]ŸgUcÈÀë3Ö/˜tجPÝö­‹=:DD$vi4°ÅÕY‹Çœ–{õžß7yGæ÷üKÂ’Õ³†íR‹­K65·[a9©>ò’E–ÅŠKWn›º'RGÊüU;ŽïWBA”ÀôC¤úÇ/ ‚ü:øÅËñ ñ8r-SS·n®ù]ù¼2i*T>_„+:% ›EL*Ñ Aƒo¿éºýýûŸª¿zY;'1©Ù‰3'Ú·iŸ^È‹àoBÞxyzY[Yãmȵ""#=~äìäìæê–7^*€ìhyC.õŽ_zñòE‰b%,Ì-4 €\ËÂÜÂËÓëîý»y'? ò@åƒüyÀ8~)>!ÞÊÒJ­VãÝøÆ._¾ìííÅ…A°²´ÊKC}Pù òùQòƒØ©õzÿÖ8òòv„ iþ%Žãˆˆ1†·àcŒ}éGÏðEå?`åƒþÈ¥~Ìù—Ap=@®ÿèå½Ï)*T>ÈðÝûÇ/qÇÃW8À·÷¥=ÆXëø÷*Ÿ¤»‹;Œ?v×Lo“ÜøÚu!ûúô9ÙpÝòö˜‹Pùdwrs„ œ¿t}n‹rMç\5Þ–ôaÇ×ñðç;ýG¾çÖ÷®Z¡ßѰO|–U7'Ö«Úzã mŽ…ÇŒ„÷LJÕnÐÈðßs1ñ×§7i=ëz"Ë!úìà&]V©cŒÅŸÕ¨öÐï_£Ï…¯ïT½æ¸s‘Éõ ‹¿9«Y¶ëŸhroåÃXìù¡É•O톾]~Ù|9\›­=E,yŒFö©ƒÖv­Ý Q›¥w hƒ·õjÞ÷p˜>;H9·¹£òù·!mC.•ƒÝeúŽð95aÖ¶V;xÉ…˜KË»^°Ï®†¢Œ ªŸ\ˆ2µ=8|%²aS›LÒµ´@»q¿F´åü9ÀÔóùjÏÚë¼}ðè|ÍïàaÂî0"Ʋ7¾€ ¥–"ñì0rœPT™æÎê8˜õå³ò î7ºå¾K×ß­8º”’#Í“Ý ò-VûyHsqåC#NZvàôÞ…E ïn\»mâ4‹Í š;m{ÁPŰœ%Œo{jË™ö³ÛŠH`,¹^ËÆæ1 ß‚ýúÎ?ò4Q`ê ]ZÞfã/=´šü÷û;«FölÙ¼©Oã¦Mº_}9\«Z9xÁ=Ýû}C[û4nÚ{ÿ«—«z¥cŒ©Cήç×¢©Ocß®“¿¡cLˆݰÓƒ{çõnÓÔ§Y÷ {ƒÐS?€uV<´/>!>å‘ÄÄÄCX·aõWœ4)ÕcdĽ ö¿Ð’þí±ù¿GÖÙ³´ —Ýʇ%=?<§k“ê弫VkÖmÌ®gʱʇ1FœÈÜÙݳWéjm‡÷*N/=IdLq~ÉÏ|›û4nêÓ¦ïÄ·£õÌPó Ø}|ÍØ®õ7m>xy`˜–1Æ´aÖOðkÖÔ§I§anÇk!öÞþi:ø4nZ¿ãðÇŸ' Æúo?¸pp;ŸÆ­{Ì?üîʺ±]ê7nÙa®ûñ‡ÏVm]‚¶ý¤J×{alË6KîT?ÝÔ©ùÈ€H}tàè†æïÙ2Ù¯iSŸŽã6ß {|x~¯6M}|Í;gØV"Ò…_ý}l÷–>[w~0(Q`Œ}¾šMÀÇroåƒþø¡#åÌüK"‡†#úýÑeÕìÕÏ"ýÅmWv*$¥Ä‡“gc‹ ®VÉæ¦lØáëQ ê[+ŠöžÐòø Åko—å|aþÚ`ï±³jZEÞ2®ðhõøåªLÚ2£r7ðÜÛh=ÙÏ÷•¹œ#½^ŸrÂxÒŽ ¼}£™ãnõZÂ^>¤¤B$“GÌúu§ÈwÜʉÎêû{æ/ûu›Çº®‘îáúu\-Ÿ®=‹»ŠÄÁ¥š ë\ÔÙD|fÍ‚kJmÝmÖÀ{wzNYЭ T¤< bĘ $ÞßKÓÝÉtZ=IÌd$“:Vèðs"öòøÇG­˜¿¹ôš!‘îñ–?ì;uýµiÂ_ëÖ-Z_¹ÌØâa»&O=¬h5x²·úÅ…ßW>!&Ú°Ó3'm‰ôºx´›îÁù¿ý²Ìz娌H÷d×I÷ƒ~mxsÝÊ%¯:”iÞmR½×{—o_vºÆ²¦öiê1"‘}ýNÕŽÎÚz®õ´úùÆŒ[+0fìH0öS_„.*pwPÛÞãj…^ñû¸!G<«w6–ý½~åªmõ*ü\Œ Lâä­Ÿßè¦ìÁžÕë§l/¶¾›EàÜÏT³Äè±€\\ùäíü BhxhHHH’& ÇPž!—Êœœìmí³sõ¾lZâÖnœßþžÛY7_Ý£¨ò#KhžŸ:íÕ»‚•©¢qé˜ÃW£}Zñ¦¥ûNlpvÄœµöOß*1lO^Ÿ¼Uš° ‰[åJÅ ZpÝKU%""Ïþþ—ûí78—v·äÿ1F¥RÂñ"3ssé^ÙóÀ½ßêeòqDÕ:û;±õÒ«öUñý-hlkø®mÝÉ“˜&."‚/_ÉáÈ…Ç‘¬‚¹\Ìñ23ss)QcĈ±ÄGûOF–ì?§MYKŽ vÚáâÀ#/ú‰ó5œ1­o19QYéÅÃ3o¾IòuRâð†¼ÊðÑ«S»Þ±ã‡£c¢©S«nàù³ÑÑQææujû¤c1ë§ÅÎMÇúííÝþÚ³]QF$véþˈ¶N"¢rNáWúŸúëyMeªYLx ¹ºòÉËù!4<ôMÈ›|&fvù¬påj­æMÈ"r´wüêBr|þ%f&«ŠŒNbdúÁgOó"àL”WÏòÖr÷zÕ-FÏìÐ1 FoïÚMë–´‘PŽU>Œ‘  ü¥C ‘ȽÑÏóz”P2A’^œÚ¸ô÷SbHbbÆ%è]5:½Àq"1'G$3Wp:URÌ˧ñ–¥ ™“±®1T<êР0Þ¡˜ƒñ´½Ô©¨­àÿ4\cˈãEœ “˜HyŽ'&q2¥DÐèÒ_—ÀŒu!g_¯‹÷ž¹Û/4îžru…±‚3<̈ó†æR¥ŒOq‚ “(¤œ^£JY”#â­ 9ˆ÷½{­ý|5 Û+Ÿ¼›BBBl,,­mlÅ Ž¡Nþ\“Ȯټ]®§¼|õÜšI[w·Y¿cxåת9ʘR¾…”+t§ô;¿§—_•);7Õ:vò¯þÙy6?$i’ìõ˜÷ Ö_w.ËÞÁñMø»ì‰½ñ´œÌ£YmË1›Öì7kWÞ,,póÞ7ß:Î"áuš)Óô„ ®‡<éÃé;e2YÝÚõ(“»½æØ)À¯ª|®ÎàæÛ²Š§YÜõ«¡¢ü¤”S•¥ž²gÊ¢'õ|ñóÆY«óÏéc[0_Â_Žþ­wŸÙµ#DçÎŒÝÉ5a”(dë•‘ÌØ°z¯¾–}ìƒSûÏÅéK gS©ÉO;Ö.Ýê1°ž«îÁ¡‹¼Ç7aa©%Gm²”Þ‚æ}MÓŸÀ‘I‰6-†o}IL±CqGõ®½{Îi %=8¾çh„¾Pú 4v^^(KîeÄH~åô·²öú—g6N(=¨’ƒ‹æsÕ,Àw^ù|×ùÁÐÖüZÿÿUSþ¿ -9²µ95~éìâßþçÑo¯-OD¼]ƒ½wû­™}¸ÞšVÆK5¯ÎžxëÔ¤ªCÊç·,Û¬˜nö!ÿß^þ)k¿¦½›DB­'t8ÐuñÒÀ²]¯Ñ´PÛƒ[&õ™ŸD2Ç ~ӯ׶äõïØW¶²9ÒëçÎ"§õ\õ”ˆöŒïsgغYÞ>½ÝX´rr ¬P—Ù“[™¤ß¼õÀÂI›t¤°/VµIMs.Íi;â|úw°tû’XiØ´qàËÄ“ºûv­ö`íâÑGMË œÖʸ‚ÔÓoâݪÍsÆnÖKíj+‘CS |]å#Í_Ñ#|ã´~«HdS²Å”éÍœE¤¡©|(ÍéxF$vªÿó˜çcg,\\`Îa­_.ݵpo[¦a½zv{žRº›¯¥^´L¦e{ýì»dåïK®JìË4ªýSÐaã­ªœ½b톱Ç93÷Ú½éó“ iÓv9¤>uæýi9ÔïXeßìKDŒ1ÞÁ§·ûKvý6ŸË·hQóõŽ7ŸêHù™ˆˆÅÜÜ=s¸Zb_¡ÝØaUóq5úL5 ð}W>Ùm”Í"&ŽhРÁ×­{ùÊeïŠÞÙï€ÜF©TÞܯ.!ëÝ'Μhߦ}sXTe IDATØi§Ïž.V¤˜F£Áñð]»v­|ùòY_^*•Þx¿níºyãå£òÈc•Ïî}»ëשŸÙ_¦Ygg³ÿëþŽ(͹ꄄIß]NËÔÔ4ÛûÕr|þ¥Ü㸴÷€oYé}ÑGO¯×神sT>¨|¾§üa×Wï‘çú‡¥ 9òFd|^NοôHBß¶‚ýâ!¨|àG­|rÅý§SÉ·cºØ§þófŸ+=u¡o~Ã5®,ñéáuk÷^ QIlJ5î9Ô·¤O$ÄÞÞ¿zÍ‘;a¹Sù–ƒú6.lÂé¯ì\¶õôÃ(Á´@•N{ÔÍ/Ë3ñ"ÇóC®^?N·C ¥R™¨J”IeøøÆÊ–-›õÏÇq‰ªD¥2ïÜN•*Ÿï*?pæí³ãÆüùŽ9”áyC¤`‰w~_´õIɾ¸Æ^\¿jé —EªZÄþ½vÞ¡È:&Õ²|{|͆¹¿»-ï_RzfÑòs¢æCf•“?>¸bíƒóý<¤ß}<ý..4O·…¿ô…\]\_¿´0·Ëä¨Rr­$uRLlLר|àǬ|þûù[?øUêÑiѾa‡Çæå«$‹rÎ&<ÇɽìØÍ ðÄüAoõ¶ìåǧtö´Ö\|$ #;O{)Ç‰Í 4‰}òZEžrÌûß¾¹_ê¿Ä󼳓s~çü8xr¹<6Ε*Ÿï)?d‘ ŠQ‘ÔDjhò23SE«tªè$’šH ×Lðr3SEªÔ ±‘Rn¼{/7—3U”JO–9þRqçŠî“œ*ê¿”'Û%€Êò˜\1~éƒë§‰˜áÆë8ž#âEb‘aÎ0ÏsD¼XÄóïû\‚%kñ[Ó¿ÞçEëžMöèŸ=?~ðà¡—ßõÖo÷-{}ÒˆÁC‡Üôì×{J…B8²ûðGö_qõýcº5ŽkØ-õÆ.Î5ËvWàæW޽¢gÛñ‘1ºvK*ݳý¨”Øc@££¿-Ù]*DéÞå+Ž5taLM?Ôí¡çǺä’!i—D›¯yöÞQ½»÷OÚØ¹wÛa{þ_éKœƒî¹µwÓĸº†²eñ–E„ôxþ›%ŸÞÞ:ÍÉ›vú¦Ñù—,ª|WŽìÍû‹v½wÓ¨\W%8JKå¶ÍHžýóìWßþfã!§98< ÐáKB·Af§/Û}Só²'Z]שÆñAH’Ù" !„d 0™,¦òŸ%§Ýi?¼ewþÁÕ÷øÖõ¦ä’RGʱ"E„qéDmpõ/Uü—@~8CɲEbŸ8s¦°´¾ûí‡;†Tü•5,¤à!„΃‹^xî;ÓÕSÞ¿ªc‚íøÒ»®ÿL!Ìq]ÖèíôŸ¶_jý%¯õ-"=)ë%Iªü!„ŠS17¼~ÚóýbÊÛ¢$KH}Kµ!„qú—œ{_ðCV‡1£Z…Iù¡J÷üøÉÏÅ­nïžœd‰—×m9Ô¯^PÙß)²"¸~´ø{ŸhqϨN AB(²¬”]aŽ¿ð²¦ïÎùà#K~›ÛÛ‡{»³Ä4K2/Ø’Ú4BªxSzµÅPÃöýK>ýf{‘"Ž5TÍßgÏgî¾Ìm[ÿ]¿jÑœ&wW››îº,! þâÔîöŸš:÷ç¿3·ÿóÇž{ìÓÌòù¬ñMãå­ 2~Þ°~õ·Ó§ÌÚR~C9SL—!Mó6n,ê0øï7Ia¯ê²ú•É,[¿=óßµKæL{|æ¦"! V><¼ß5o0ÿ’wjþ%k~×O¸õÊv¡ g`ÉßãJéº7œ(„B[v½îÉ'FuNBDõxðŻߙõù´æ–ˆ€¸Ý/k-ûsòà{nýç…Ù/<öYt«a#.o´û×òÕ±wciw½¡­jãvRHÛ[¦?RV&}V¸P!QN€O0Ý TÊPó/ ÙQZRbw*Jùlީϼ;!•Õèü쪭¿vYì7´¼@˜ ð~„Æè_²g-ùèÛ]!„X9ïÕQÝ®N멊pÏ)Rœ¢„WG9NÛÆ|¿É1nýí Ïñ†Ù«ƒü3Tÿ’µîÀ[˜]‰Bܰ҅ÈX ¢¨å.|y(pwþ%ÿ¢ *e¬þ%è5!dH|€Î ê!˜ÉgÜkôôY¿,¹Áù8†pJx *2B¥«†MK õ=ijØë²âìü|ýƒ,ËÙ9ÙYYYťŬ ݰØ’““âL&÷*÷S×Ä€í°\e¨@ÂeT¼«gþ%wdçdïÏÚÉÉfý(±—îÏÚ/„HJHòäuè_ª}•RvÊÓ_7u¢ÄDå…Oò'[/È5’••g±ZYºá°Û-‡-YYYžä†PHÐ÷>G£ó/ù9?—'$&9eY–e¶@RÜû>HRBbÒþœƒžüÓô/·Nþ1¡¶ÖXk5}Wl{Tç0Z~$ÉTXXèùëPÊ×Ò r{µz¾Rè_‚G%HºiT̾ªö¨›¡†‚2¥CÑχ2@~pÕš×ø«5#¹Z<· ;„PEAE ŸUÏ¢(ççù[%Á±Gí¡Å=ž¯\î§+éÕ8/È”¬0x9©´jFÖt_!ƒ»Ö@-˜ÉÍ!*«.((`KR·‡BCCO[¹FAÿ UA Îí܃WN?_r i ÞÝꌺÇö÷õ§Ö˜n7½çú¯Ò*£‚WV„‡ãô/éD­žçãÜž.ûøÓ©óT¶E‘lAH¨½ïó/y°jÊŠ“É$„R²ÿç÷ÞLÿù¿yeÎŽ6·>:¡~îÊ÷f½úF½Wé)åýþÔ37–aj#™<¹ñ±)Š¢‰ ÍOy‡ô/0fœLî€üà›rSI’J÷¯øåP½´GwJ0‹†ã†üòÈÿýž3øŠ¤“o´xçâ?Š.¸ùÚËÚDH¢ÎMWLY¼éDK£Ã»Mù¨[ñ?¯Ýü|~••¶œûwƬ÷¿^Ÿ]lMjqéõwŽé©™¡Å-ß3Ãü²çB/Ã#’¤áùLk{T˜h8?œ~ýƒ(>°ó˜­^£H‹$ ×"ÉôÍÖl»”\q{jç‰Ý{‹#:Õ 1I’IÍã•õÛrì=c]/XV¹žQ»nüðÕùÇûÝ;õ’„’¬~ÿëp¡,E™Ù|¶rkŠþ%µ×%–žºÕý;0ÍÔõPI°¡Ç ^Ùlhõ©æ_ò ¹èD¡P/°¼¡Éà8˜Wªˆ`éä3ŠD@H€ëϦÀ°@¥èxÑyï^í,ÈÊ* Ni×¶Q Q§a‹.Þø¦pçŠ*–‰_þ%øáàÇ—šú»ú8‡sll†Êœ0ßÿÁu£€r&“É$IÂd1›M.Ьˆ²¿(#™$!LfKÙ3$׬D•þ^ˆSž_ÆÓáâFyËžšøÈ˳æ-^{ ¤ŠçÔá¡V³·}0îü€*Kî âõ/ËFØ{h§BËÈÈÐâ2V×üK’dŽ ’J J×criA©%4Üf:¹!X‚#ƒ¤’²gÈ¥¥"¨N°Y’$!I’IBS×OÔñÔ;MW.[µ~ý/-ž¿ôšW¦Žn`å[¦ªÌPýKŽº<KÕåyà<¨š¿tlá|%­ä‡3®0×i]´iW®Ü&È,쇷ãû$V*K-QØŽoÏ*$a?¸5Ç”pYBÀi78³ŽUdaKlÛwLÛ¾cŠÿ}m«·œ¸ªaœÉ£oŸQn:á••ë^„ô/A”_æ§£¬F±n_—Baá÷ªÔÜùáÜ5fåBÜl65ìÝ'á»y³ÕOmn_;çÛœúW_œh–w|tç] ¢ïýàÙ¾‘͇\üÈÇŸ.\÷øÏïþZÒæöޱV³Iq•8ÅYRTXd ¶™Oî‰í{ç¿ðɉ õl'í_ŸYÞ¸A¸E;“¼ÖÒÍ㼞‘¼8+ÀÊ)®ù6lž@~ðßÑG§ÝÿÁÖxÌ£޾2çñûó¥ˆfƒÿ÷ðÈú&QjRd¡ÈÂd2™CÛßüÈM3_{çñû‹,±í.àÞq“PN¬x|ì+›…B¼pËu¢Å¤Ï_èYQÂÄ´nøÖÜç¾<æÁu»~ð– BLœè©Í¼áaÚ¡‰2z+:3$/ "Ñf]î}ðO0r¢JÌ¿äµmÜVÀ}3ÜwʃMn|ïÇËŸÒüʇ_¿òÔ²UŠì9ca¯³¿lx›ÔÞHÕêæ¥(Šër†úRÑ¿œR^TçðZÏRZ˜<ßÃèf&_£!•aâ­¢–¢íašâïù—Äó/¹;’¾këŠ(i„ðøúæ_¼S f”—ôÖûw¥°"œ¹oÐæüKþ¾@Ë{QWä•“’/c€¿V®«s‰ ¡R´ÕR­m„n,½³í֨齓èš*æ_âF jÎH®\#„  ¨å/¹ô ´Á§Ÿe­¥úüÞÞ|û@~Ðn™«þš‚É_¸lTç|(h®ÖWííP([϶¾|¹²ø^³×Õ2?÷/çççû¹ÍF;ICŸ4???88Ø“W I];Mv¦F+…U¸Æ uå@:Cµ¶`©\¡&Ì¿äŽÄøÄÙ’DRhh(ÛnäççÈ>ŸèáëпTYff¦ë‡””-¥ÏÖF®¢¨ Á6 Íñ}bHÍhùÁujy_Ö¾ââbV†nØl¶¤„¤èèh§ÓéɶAr¨LK±A¹åÜõ“wû\¿ôò×g T ¦Òýt‹z}×å^çú¸ …WWDFF†‡ üœœNgLLLbb"—P뉢(v»Ýápxò"Ü?˜_`H€¶òƒÂápxXhB¯è_`è:˜® O6#ŒHß•1± N\6 ÊMŸ~v5ŒÀ¨HU×p«öf FÍ;÷VÁØ&È€Ï0ÿR-rµ×W¿ò¨@oµŽžf4Ny]嬑:[›ðY°q/ó0u)È@5qÿ8Cz½~¤äÌve4§Þ…=C›S<zÇüKþWüÏŒQW<¾º@}ïL>üÝ]W\÷ñn;_”FAÿÕ@­‰êàjÀWü:þ·rÒUÏo*+Bêv2á®1ƪeHD)Ù÷ÓìY.úë@±0‡%·êÒìÍ£:Frň0ìÀá¼Z¯_û˪ýbãQ[ãA÷Nß3Î"9+gOýë9rdJ‹€\¹Ž«üÏÛüÕŒ×ÒÞoŠl:`ÜÝ·hdßþæM÷ox}ÓUsØé¨×ûö§® _4}fúƼèö£'?zUËJ_¹Ò=?ý~¬ÞèçoP×"„hÙ¦{¿+óB(%{—ÍšñÑÿuÕé~ÕÝ÷§¶ “ª~°`Û·3f|úÓ¶äõŽ9D!„(Ýùá w|yÏœ×Dó?o„ô/Á5J5Ç7@ ¢0uã8Na 2 !¤Àä®cþ÷Ü›¯Oñ––»>yqö¶!„Žm³3´½~Ê£·^”÷Ã+ïm,¥»æMž²°äÒ»ž|ù™‰ƒ”½TÎÒg›³§íøWg½þâµu×Ï|læºÿ?¥ßSîèéøiú„;ßÞÓæ†É÷HØüéÌe‡œ•ß9$>DälÚzÄQñˆ-4@Rr×ÎxøÝ=í'¾9ûÃ÷îï~ä³g^[—/WùàñU/=úÎßuÓ¦<ÿÜãc*÷=™LB2q(<nû`Ð’Pù¶Êæ b­†Vй×kÐ]*¹ØÀ™¿÷·>ÝÕû©–ÁBHQ]ÒÆ!—œ8œÓ´kç蟷í)P !,-&M{¢´$ ü¸üÇ-9y¶ù ³ZÜúîøþq&!šüññ²#B8²W,X:äµ›z7¢Ám“þºñ‰ùo» ^KË»Ÿ¹·_”äh´ïë¥ /zô±kˆ|åÿ¾zéßC¥Ã‚N.˜ºÃ&\ñë”é׌ù²c—Ž;v½´G›d›rxÕç+"®~gL·$³ñ©7vùþÙe™ûWõà¡/VY/›6iTÛ !ääà Òç !„h<îãïDZåýKz8¬Ri+ÎN£ó/ù;?ÈE˾r¹BXR†Þ7ýæva’Jñ®Åï¾òÑ¢ kH˜©ÀQß!»†c-®7lŠ ’ìÅ¥y»wDuhqê0ŠãжSòÉe£¶º­ãå™GñB³Õ,„&[H Édv ˜ƒB¬²Ýyꈯ)¢ãÍo~:`ýê?×nX·èÍï½wу¯Þ¸yÑ®÷nõ‘ë à(-•Ûæ¨òÁ-D½Q‚øjx!ýK”Χ†¦T\ÏU+ŒÁÿ×Owºó¹[ëìøà©·¶æ›ƒ,’Âþ_ÆäWWÔ»eÚ¼!-cLû?xçŠ3ÏUùËNY1™Ï<£sÆgîÔ+Gé,ç„Ìaõ;÷¯ß¹ÿ¨[O¬~æægf/ù?§°´¾ûí‡;†T<ÉjÝûvîyK&3çšÜưàÃ]±Æ÷Uµ1‡c®¥J(Bmï|Œ}ÅŽFç_ò÷õ’9,©aJ›!=>dNlS£¬ä8þ_vqÅ?,…$7ˆöb×<^Þ¿åXPt…¨ „*´Å4ˆrÜ’]zÖƒssu. .„áõ­‚ Ã*¹þA k{Ã3·íºí­É¯5xõöÄ&ÑË2®’›8·.ž3g¿£ÉÙ~/¨ÙàÎÖÇÞzís¹_≿œ·$×ÑAsÂÅ#:Îyí¥šMÔÐþÏ3Vš/y²]¸t°ï¨dË·Ï}G ¾°Yb`ÁΟÍËJv£ºñ©ÝçL}jjô©&KG¶þ¾d•5í±¡U<øH¯Þ fö҉״±ì^µà“ޏ²ù—>ºéŽ/"'}4³?ó/?Bú—Õª²çJÇÍ<Ü©Ãà ^§™Ïv¾œ[%€üàÖzC|<ó®Çž›ÚèÕû½ë…Ÿ½_JèŸöÀÜ×¢ûåãb£RÎ|0¨îßµ÷éw_ŸòMxó~CÕÙ»¦ì¥eY(2;™ó¡ ”’5(h¸|‚,Ýš†B— ŸÝ ù!¬Çô…=NþÑÙmÒœ%“„B\÷Üçו?~ó(!„±?^P±º¢ú¼ôc!„nš:ç¦ò¿?®Zåƒ Þ÷ÞÀûÊÿtMÙÿßðé÷7°??/æ_‚ö!œ}„j¿œ'PnÑèüK&ÖÔ!ýK ¿8Dë9 e䨗MkXF5Úi2$ðä­'hÐPÓ˜ª‰#CFùðæ_òO õN IDAT‰F•ƒ .A S€üƒFAÿQ ÕÁŒ0È€¿0ìÞA²@~€пè¤r­Õâ•A0ƒo!D#hœFç_²°æ æ!tÑ¿ä8öï¯KWoÍ)–‚[õèÛ£IøiÁ]ÎßõûO¿ý½/×!Ùb›tîuéq¨‰+ª¹q1 %>·–ÓÍörŒ?@¥ô3ìà8¼æû廂: 9r@kÓÖ¥‹7OMÇ7|¿è_K»á×ÝtãèKã®ønå;[€zöìÌ5i(\l À‡˜ ð&Ýô/9ŽmÝžÓé’¶õu¸¸MèáÍ™¹§ûѽGM‰­›%„F6¼ ixÉ¡œ"™M€êÕ<üJ¼õ¾zäçþ%Y–³s²³²²ŠK‹Yºa °%'''Ä%˜LT}ô/Ù*°FÅ™„–!ýuà„CDT<ÃU7ʹæu{¢»Ô)Ê9XZ/9˜h}“˜VÕ³lJ? £æ‡ìœìýYû#CÂâ#¹FV?Jì¥û³ö !’’Ü~‘èèh]ÌÜ*Û íŠ9ÐRv6P²Ú,rn±C'/p0EuÜ''cÙwÿXjë2´KœQ®LÊÎSQeÂgØÒ@ù!+++6"*&6Îbµ²2tÃa·[[²²²<É®þ¥ŠÿjzH¦“íó²|ÆetrÁÎUäÔï—Ö!pÿßkþܼfåæzƒÛEŸöå\³fMÅÏ;wf3sweHB®eÔ•¦TQ1ÿ’;ŠK‹“œ²,Ë´{«‘â^™%I ‰Iûszø¯ë¢Éd ¶ G©£lAÊŽ§9Ðf©Ôœì<ö÷ï;-­G§ÄDšbzÖ©šþùdµX?à”"3ÔJ½¢òœÃq½®_ÕÙBµÅ¿7É$y¼•Hà¯ä“ÉäùjÕÍüKÖÈ„Ò#‡]D;ó³s•°ÄðÊÁ]±Ù•Š…eŠ ‘œ¥NNÃÂt¿Kfþ%?–þƈâZ -ž¯ÝÌ¿d‰nÖ,üÈÚ•›öÊÞµnŦ¼è)&áÌYõɬY_m-T,QMêÛŽ­_¹1+·¨ðèÎ5kX´N`§ TYÛø¹&œ S{hñãÊÕÉýã̱ö,Xºzá—%RPB«¾ÛFš„p EBBˆÀº— ïiúyÍ·Ÿ®’¥ øæïÑ8˜o†W¤ ‘ÆR@GùÁUaJt¹é3|xôÛz™I!¬Ñ­ú¤¶êsjªˆë>vB÷ò'Ä´ê5²U/m}ª I?Õ9-=,†®X˜ZCSùÁ[ãFH ®K™5ôI=\¹zš §ï¦ÙGûqásM6Œ)­ª ØQ©ûó/y¥úwÿkþë³\³û„9¡ýð›'^Ó©Ò,–Σ+_èùŸc&}ð\ßH©pýó×<½Ö~òeZ><çé![¾{ïí¿o?êŒi9à†;ÆöJ:ÙH®ämzÿ‘'ˆ«ßš>ª®…VítÒ¿µ·ð¢ù˜¢®þ%ÇžzvièÄçîˆ9¼böŒ^ŒœùìIfWí¿áã§gþeR¥+‰--n~tls›BSh½`åØŠ™”Ýó¶Gn¯_´fîë/?c«?ãÚ”!„PŠ23žeYY„hò¢k ¾c~›ah]€Mð_µ¤­÷›‘‘¡Å!ÿÏßZéOŽ¿.Þ5p☋[7iÙóÚ;G'd.\¾ß.„PŠv|ùü‹;=xW÷èÊoÙÞ y‹V-Z´jÑ¢U³ºa¦ãk¾[oêqóÍýÚ4mÖõª;¯o~hé·™%BaÏúñåç×¹õ‘!®8µÝÌ¿ 'þΧœ£–‹N‰À0›ëMYbš7ÈÙr°TØ÷/~eÊ’:w<1¦Mè)oX)þãñ1£†Œ7aê—›re!ç*¡6WB0…5hY°swžì<ºâ§æšÇ<3¾k´‰•î—•ëf„ô/†ÅuÛUìXY&[§5ZãU>™mú˧²&´ndÝ·xþ‹gQÎέ‡J%EG×¾ûø<ÓØÉ·^uÊÈA@ý¡<2奧>ÿ¿!ñÿÎ}â•_HQÍ›…ýuÁòÝ…²\rt÷¶ý…ÎÒ¼-sŸ~óÐeONê“d•¼ú•G-Þ‘ƒa€RºÚ µE)J/æ’bh‡¿ç_’$q²ÑEŠì6þÞ˦NŸvûBˆMM²¹iPÁ¶²œ9qôÌòßzå†;ö¼üê)m{Ä !„hÚ´øgìŒå[‹z]4nRêÁé¯Þ3öU!’S"ŠDd˜²û·='2?¹óêOÊ_`îø÷N}wR[@­¯\·1ÿ’/¸Îï¦*§ã9Œé¬ž# À_»ßü YÌ¿äBÓéÄ—»_wäP¾U¼ðw4Kj4dÊ;—–ºÊ™ÒÿÒ}yψgXßZ©<•lÑÑ69«Ð.ÌÑn|þÃÑGsŽ;CcÍ>|óìzõ“{=ôz›Y!„óàâ秬î2ùñ‘-lÜvBíè_ ê3œèñpB»ù¡ªyÉ›*Š·ôÃþ؞ݒƒÂ­õÂËþ®´8Üb²Å&'F§C6[\ýWΣ[w„4I*»g¯98:1X8ö/X¸-¤Ó¸†Á!AÁ!e/à4G˜¬‘ÉÉђ盟Qn:ὕ[ ;¾ãšŸ9yN[&,•¯ + }Ie÷ŸVJö¯ß˜Tº÷÷/fkïóØÈF•«téä/ä¯~á‰E‰}úwlphÕÇs÷&¿§y$9oX·×¢Üð݇óvºçá ‚Oy©üŸµž‘<[Âô/ùxM³ t˜Àýå‡S¯BïXðâóJ¥°]ñÄC©"MUç Éd«×¾~Þ‚Ï_ø¦ÀßaØC“¯mb“„pXôê“?å‰àäƒ|y\x‹tfI+‘Ôž=Ê#„  €üpö’3²×Ó½Îþ÷ÍnýlëÇ€†Ãþ÷ê°3JÕ vöÍÃgÿ¼õRßüA“WªxÔJ¤E ;€ÎÑ€tÖrˆ%òÃ9¾ â´ñœw—¢™eåáõô/Ah‚ˆ%@­aþ%7kLJy5'=<~¿ô/~NA’Ä…j»¾ahŽ*î?mØ[¤i"#¹½p<Ÿ‰ï'ø'pð‰ŒŒ -¾m‹>–¾q¦R5N¯ýK ŽaÕ~ªé8#¨8?çç燆†²&t–‘òó󃃃=‚þ%ú+@iØ e~î_JŒO<} ??Ÿ5¡'ùùù²$Æ'zò"ô/ÁR!ªw®=ݧäÓ5øÊšË^_° ÝY¢)9îòóøƒ«FÜ—µ¯¸¸˜•¡6›-)!)::Úétºý"ô/Õî† g«é™o ÌW˜ÉN§3&&&11‘)\õDQ»Ýîp8<|ú—^HÅô”„‡ÃóBúðƒïh.½3~êZ°ÿ„.dddhqÂÄšƒ:¹:—BÀùkªXä€þ%îáÔ>Œ@’ýù8ÃtRäÁs °€øD~΋þ%á²¾itþ%òT!ýK€ÎpƒcB^äÃ`,Œp€[}Fùðú—àeµwÚ›á{µý ÔÄÂ"€š#„ ©\ff¦ë‡””–¼œ¬ ?@ë¸Üi4è=¨\‘³4È*ð™46-À§è_‚JÑ¿Ú@ZÜÅüK€÷#„  €ßÑzä¨Ã UŒHÕü®0ÿàEô/iíÈГjA¤*,*@ßMx×OCÕBпFq:£(”ª€ ãP)†È@uÑ¿À@àοDÿT!ýK8·t!ÒX ¨Þ¦µÄÕÛ¦å (Æ R ;€ŠÒ ¨Ì¿xýK~¨*“¤j])HIáG\Ê ?§EAÿ’V¸ÝM /æ^ÿ£$vßc6^€ümaØðEê£NÃI •¯V øâúiEQ²s²í¯Îñ¸¢(®SÝ’$Uœó®xäl<Û_W£FZ4k±ñïÕ)‘d0NxBÄDÇ´lÞræ ç]8 ;ÐŒ4Nºë‘$qbÞ;‹g—‘‘¡Å!_ä‡Â¢Âè¨è’’’ÌÌÌ®]»ê{;øý÷ßcbb¢£¢«Ù¬UXX-˲q¾*ÑQÑÕiÖ¢ɧÒuúoé{}q®š¯àÇH •pE–Fóƒëä½RNß ´â3VsÈB2d.¯æ§¦ jV$€>òCåªZ1@þs#&)FŠÅÕ ~v°ïþú­å c®ícfMJUª˜N—+Ë" ¡ü IQ¹œ¾¨ë3*Š" ©ú ÇPù¡š ÇoýKEÿ¼=yæÐá×v‹aÿ µ#wzí<™Šðûwê\ƒ4yŒù—αI®ªÚét$?Ȳ\£þ%C]ÿ`2™TÝ¿ÜnÜÀ♯}Ùõξ-âC,eïÔ` àn)êE€Ïj9ŸýK®!Å}öœµéÏÞ=vÐÀÁ}ŽºöÁ׿ݖïÉËÕ§Ó鋿¥£óû–³Å_0ü‰öÛµ²ÍUóóúíþq…ç.ÚüÛËWuiTî²oO°»£¬•*¯¿dFF†—„O®„Ûµu9ùتé·=ýKXïëî×*VÎ^÷ݜ银™>íº&6U-P×gTEHÕ]8îDEQL¶>3¾{¦“5wײ7ïŸ22-nÓOw¤Xµ²3ªFDòWÿRè¥oþ¾õÅÓ„L!uÂØqødü¡¢Åß}Å;æÎZn¿ôÑWîÕ³]ËÖz}xÚýíöú沃ÇÁoîxó§»JEQ”ÂÏ¥]ùØšE‘‹÷,™qïuí7jüéÿäÊJIæ‡× ŸôýaW‰Ÿ¿âÁ+R_ßR¬(Šóøº¹OßpåÐ~ƒ/Oûß›K³J<ˆ¨éõ5#„¬ÑMÛvèØ¥ç¨Þ˜ÚCY»`C®¢(Šóøš×¯¿°NÍfKêzÃÛåÊŠ\ðïG.®b³Ùbu=óŸÇ%Fûþ¸k`ç¿7º†·º¥$wéèøØÁ/M»¦M¤Í–pѤowmwg÷D›-ºUÚ¬Í…Š¢(JÉî¯Ü"Êf³E4ôè÷YöÚ\8þé_2…ÔiœXüûÝ4rЕ/íu¬ûßì‰É!t/8‡4!Ré‡ÞqÁ|—$ÉuåƒÃáÝR¼oåo‡£zoVq¶}Ñå¶.]Ì)ËåÝQ®)!Yv_3ý‘÷v·½íõ÷ß{ûÞ‹öìkkr²¢ˆ“WrË®§Ê¥ÿ}9eÊ’ “ßúôçÆÆ¯zé¹oö”º÷NË>£Óé¬þõîýCBT|§½Ô!ÃM²\ºç“±Ã^ÌM›½nëæ•¯\¸æ´§ÿ<¶mÖµ·/lðè÷®ýå³§†„î>T"+Šrr™ÉePáÌ_6õ»„;ÞÿøùÞ{g¥ví=5wÔŒÏÞ°ðá¾?ä”sW?>äæ%ÍžXôÏÖ óǛ߻îÆÏ÷ÙÝxóÕY8~ë_%›_ìßcÒ¯au±fešƒ‹˜ÐïÎ¥ÇÙgꛯ ð¤€‹=ø¬Ê§y þÊ¢|JS·¯°ÝsLÄ5‹³V~0 ¾q¬ÈÙ‘SªEœ~ çáÕó~ ¿ê£»6Œ‹mÐuäõk—ï*V¡œ|jÙ[+ÞñÕ·‡.¼ýÖÁ-c›÷¿æò使¬=âðpüÁWkÐqbëWÏ<·&᪉]ÃDéŽO^^ÝvêwõiV·AûÔGé|蛯·ìÙ¸O®ßkàE­Ztê;æ±·§õ<{'Ž:d΂o¾bä-÷ŒJ47zlþ»÷¤{Ïø6Žmk³JŽÿúÒûo={U§†u›ö?ùÚˆ?>ýãD-}TWç’‚DáÚ³òÿ·léû¥5 ’,IÃf|ólâ¢÷×°»P…Û˜éß©blÀ½‰†Ê~I–eYªô "{¡Ý!+B”Šw=*¹$ksVÑŒ¿êcWHp–Úå6yWÐ(›EV–¡(ŠãÄÛŽäî˜2n¥Ùu-‚½Ôw8ß!Ǻóm¯8©_ýñwò†"gþƒ¾Bk›[Þ_ü\Ï¡äf®Ü™÷ó--ã'º>JiQqX½’¦iƒb®¼³C‡Œ¡ûyÍÈ …R‘êÊOS”5I«¤(Š‚"‚Ìf‹IQE Š29JìÅûÖl>¾û»>uß1¹jQqiÛì<§e®á"RõüKŽãûJê ®wòºktãx±´Hf/1 ¾º\ÅÜËæÈºQ"s[NIŠ‹²w!16IV„PœåùABŠ";Kˉ3ïk\±ÕYCs¶–?U¸ž*Yv:sìðÇŸº²^ùÕÇ’98Ììöœª5½Í…;·Åea²õž±ðé&ž}ߺãf›E‘eÅépšÂ¯øð—i]*>¶98:6üÂ?òÙ?,]òÊÍoLÿnîÚ×#”Ê‹¬"€U¼!+RyԒʆì°Ë–VS9&©<0H±R ß}5Ÿî·ûÇ5퓸fê¬?{Ot}îÂ>zemÂð¦A†Ù+¤‹*î>u®©š>Ÿ[È ¿nÞ¬ÔFIÍ5 šÞ!edhqÂw㮳Îî5öX»vþjù£ïêîÚó;¬úzmQH× â,¥8¯È©(!d»]ŠbŽk'ÿµõX`¯:åeŸ"+NK E”ä;EŠ³Ô©¡H¡uëÛš%ÂÛD–•ÅŠ¢¸ùNËÆ|0þ $KT£Vmztzç³ÌÞCnsAÊÂ{Z6蘔ÿÍßE‘CžüزC‰j;|bÛá'g}Ô¯õßl)¹1ØêÌ?VäTÂLB¶;\]]'‡%ÄÉö®“^–¤ö-oý±7àîö±eoгæKªšã~›ÉÚä–7nÿ¼w×è—b¤c…·Oúo_£—ݬ™¹­ ¡Ò?MŽû€Ê«sþÉåS ¹D@£+oºø—_|Hsmß–1ò¡ ?~úÉ_¥uFka¢Aó¨ãË?[ØjPâ±µßÎ[Y(wQL1Ý®¸pîô©¯DÑ9I:ºýÏåXG<4¼iJà¼ïÒ×éºõ‚¹[œ¡õ…˜rùÀÄI¼ð–¸¦_ÓТýÿ¼ô¿Î“îî.¹µ«‘j:ÿ’gK×yñ“éÓ6]òÀ•÷·øåÕ¾ãî¸ðÍFO™:¾g’ý¿Õ >^Ñü‰qk™ßâæë´Ž<¾bù>Kã«ë…5èÑÒ4ç•©‡õ1oølÚ´íM«ó^£{Ý{uÄÐñ×NÎddë £›–ÌMϾþÓ—„º±ITçiþé_RD÷§Wí½âÛ/­ß[Ô®êåÝ’=yEDZ]ºzkN±œØªGßMÂϼðÈ~lÇšUk·ì=Z,‡]0rô% vOÜÉ¥ ƒü $Q>1’p÷Âb)²ë„W©óáç _zâc×Ò¢ºÞòȨ†VE FÜ5b׌yÓŸ ¬ßsÄÈNû¾B‘".¼çéÛf¿ÿå«g”ˆ€Øf]_mªÄÏ¿3ûùáÍûòñB¡(ŠµÑ•O¶|0û³©?æÉ"´N‡K.¯osó­VŒ?Tÿþn?(׌ Ðä¦?üë’Ñã®kõóüñs¿u>üðŒq+¡zŒ˜0&)%¼eÖ ·_69OX/¼þ÷ÇÖ·Úâ_yéÿÆ>zט9uzÞzÇÍžXTñ&\?T}(»JB"ìâ翟vßS ›™+,ñm.ûP£wî—W…ã·þ%!„ÖøN#'vé•×r^óýò]‘ ™äÜ»zéÒÅáq#ÛEœ’ 䛾Ÿÿ‡£I—K‡Ä §%œÉbÕ”ãtªåÝ"šÁÔ•„pÍß*ܽþA!„)ºÝ÷¶»B(ù¿=uÛá÷ͼ§}hYë}H›Ñ¿?ºü‰Ã‡ !dY ¨ÛóÖÉ=o=¥LQ]o˜Öõ†ò?_6Êõž¤ˆv#&MqÊSÝ~§®ù[ÝX851컜a'ߥ3àµÍG_sý]ç[ßü¿[߬üìvÏuåó§¾Ã€”kßþíÚ·ËxzüSBÑdöîÝe/jmþÐÚƒ•ý!jÄ÷G”ý^ý¡SÒ‡NñdIUsáø­Ia?øó¬§_ÉXñÏžÜÀºßòØc×uˆp·¤wÛº=/¦Ó°¶õÃM"öâ6[36gæ¶éYéõŠw¯Z“×bÈU=i’R—4!Ò}{N7U©º;_Q¸(ô‡ù—Îç¤So\à¡ÀÆ]–Ì[øýŸÖæòá¢ݺ&¨¨èªÑ-„Û×?h™Úç_RòVÞ×£ïœøëî¹åÑÆa¥û×Οܻßþ_~y´­{WPÛ*°FÅ™„–!ýuà„CDžœ  ôЖ}N[ƒõ_}¸çH‰%²Aûž½;&Û(Ô:¨=?ÉétzÖ¿T™)®÷„Û2g¼ÿò3ÅRÜ€É]ºÄ«¨SÜœNgî?m¨m®š‡ø­©`Ͷ'~ýé‰ \S¸Ž»ùº®—÷uÍ=ï_âN¢´Ús ¥ìKV›EÎ-v(" |È9ÇíN“¥N×Ë:Û³Ö/_õÝÒð1ƒ›† Ð5æ_:g UéÎqÞx× ½n{¾×m'ëQµÕÇ5f1àøCužæ¿þ%ÙiMl–pò‚isT‹!ßzrÿÉtrÀE–Oÿü²£Ø!E\Ðá‚F‘&!âzvßýѲ-Ù%MÛNyÚš5k*~îܹ3û\@ŸðuùÆ©@•ù!((¨°¨00 °^½zëׯ¯i­YQnV9‚Qùo«|fE·LEÏŒ$I•>Yäñs•O;·úõë ! ‹ ƒ‚ªÕ휗—lœm®úŸ×çýK΢ü"§ÒjüÈÃî|«áô.L²Éù»–¼ôÐÒv÷=âÞkš¬ÁVá(u(åa¡Äi´Y*mP&s Y±Ù]Ï,!¡ò±b‡"N¥!3pS±šâ*º…ùs ’“’÷g팈ŒÕý2-.)>~âxä:ÕyrRbRÖÁ¬äÄä#lpY³’“ÎûL?ô/X88òŠåe˜xѼ‰•ržuÕK‡F¸óªÖÈ„Ò}‡‹äd«I8ó³s•°á•¿x¦ÐøhËŸûÈqá&¡”æž( ˆÔãNÓ77Ï¢N@ëù!*2J–å”””a™&%&EEFUg¢!×ÂÙ·_qI±Ž-Ж˜˜X…ã‡þ¥°þŸíÊ,¬²OÉœæîw,ºY³ð¿Ö®ÜÕ)ѱ{õ¦¼èN)&áÌYõÙBúŒ»¢ypR»¡ ~_¶&ô¢欵«…µº8™˜j5]0#'€Ê\SŸeH"•é;áÛMù—ÎÆétÆÅÆ%%&™L†˜Õ^–e‡Ãa·ÛY8._÷/™‚6BQzxÛ†ÍûòJåò‰%6¼nb°{«ÈÛq`Ï‚¥«~Y"%´ê;°m¤I§P”ò›[“º ííXºjÑüµRpâý‡u'>ãæ!„Ýn¯fÉh@,œ*ùmþ%ùز»ºö}c‡0[¬¦ò&¢ðA_ïüzP¸›/inÕ'µUŸSSE\÷±ºW<#ªEï+[ôfµ£*tdAÛ0ƒ~@U˜ ð&¿Í¿TðÇë â§ÿó×Ý­‚¹nK'Å1õ7¿ãš`舉E5GáûûÇ™‚¢“Zu¬¯ßðÀ¡ ¨U§5ÐW'¾qÙ1šÂøTÊoýK!]îN}îŽûßy즋’ƒÍe‡ShýfõC‰Û]Æ'… ÇA~€öù­I)=q8kÅ[ã¼UùÑ^ Žÿty«µ¦ ÒèüKœP…ª#„ð}ÿRþŸ3çZûio^‰½’%à à ŠÚF#ÎQ€sˆŽŽöSíe ŒïÔ¿KÝÐK%f*2|—ô™222Ȁ׸:—ü$B:ïµññg¾\¿çȉ¼ürENV PÛ8ûZÀõPu„¾ï_Ê[ùâìß—»²ãÔÊrýü*ƒ0°I ?çä·ù—B{¾½.sº|ꃦàÄ0Ö‰7p‚{¤3UîÒñË%L\7òtÀoó/Ù³~þbÞŽ’S lrÕW5 dµ$ÌsV`\Þ  &4:ÿùªŽÂ÷ýKŽœµK~\U „BqÞ¾vkIÇ1“‡Ê¬Ô²4!(>ÝçL#à6ÛzG~€JùïþqMûñ×J.ÝõÁˆA?4LdðÁ€Õ<¸W=Õ“‘‘¡Å!æ_‚Jùmþ¥Ó4:&î—Öç³N /µ6kâ @x4‚ñ¨:Bß÷/ ¹´¸´¢YI)=ú×'l²4໢Qô9°4äßú—N|{YäËOy(ñŠwßíÌ:¡&àì 3‰ù*å·ù“6dµ IDAT—Âú}²}kAù„d ŒHªb¦þàÏ/)n€1ÿàý!|ß¿d ©Ó¤ €ü-ñCÿRñæw¦¼³¹¸ª¿²µºuÊ­­l¬œ…Ûc8’Äøª»1èŽFç_"?@¥üп$ìß±m[á)9oZ¼jŸ° øùÈ?ÈPw„¾ì_ îòäßWú³#û§Æ]õƒ¹Å¸wÒ_»,œBý¡,"ÀhÜ»S·xƒ¸ÿTÊÏ·}(Ùó탽šöyöðÕé[×}xc›0ö²ä¨—ÿî§nýlBצÃßµÞýÃöU¯¦¦‘€÷itþ%òT!„ç_’s׿}m»Vc¾iøìŠíKžXÇÊj .9ŽwÑ"¥S\ÿ•òÃüK¥Ûf]Õkâ‚ì×½öîÍÄ–Õ++þÊÕªK«(3«µ€+À¨˜ ð&?Ì¿T´å‹o!¶Ì¹sÈœSÿÊÜçë#K‡G°Zü-‚€jp ²†æÎÐä!|Û¿1|©ƒ À¹pýTÊÏó/AÐö ÈÐÿÍ¿TÅ‹MS4<„š‰þ%¨:BÏ¿¤b™™™®RRRXð4 1²¤Îl©Nl-NÅøTŠa‡Ó¤”cQâ*j*6–'ÈÈÈÐâÛfü*å‡ù—N*=¼mÃæ}y¥rùåÔ–Øö·åë ‚ªŽÂ÷ýKò±ewuíûÆa¶XMå}êჾÞùõ pÖ‰±I§±qþD0Ü@çè_‚Jù­©à×ÄOÿ§@vØK+&<À`¨€„¢pkˆšbü*å·þ%SPtR«ŽõƒU5ENll¬«EÒ5QCåvÉš>âÆD©l‘>Á¢´þwÇýïõÊŽ b#ÃÃÊ ^x‚uЦ+U ­\0}ú— ê!|ß¿Úóíu™ÓåÓ‚vpb˜×D†$Òi8;z½FÂøTÊoýK¦àĺ!ÿ}1yì€níÛ´ïvÙu“¿ÜZ71˜ï U2K€ú1¶ )Ì¿x“ßú—Dц§úž¶§Ã„?øäýiãÛí™6hÀs‹Y%FæùØ Gtì= ô/AÕBø¾©pÝ9ï[òã3탄B¤ŽîÝaøëëî{§{0ëãP)¿õ/9Žï+mеaPŶ]”î;æÐË’åT°/Èпõ/ÙR.‰[ûêÜ­å KÅ[?›¹&¶Gc뢔71ÿàý!|ß¿Ðì¶écæôoQgVÏnB ÿû}ùß1w,º­y+µyôåÆF~ÁµìPsŒ?@¥üÖ¿$LÑ}g¬Ë\úÒ5Ýš6hÒuô Kv¬ŸÙ?†¯ À§…Sûº§Ñù—€J¹ú—*þëÛ\ nÔ熇ú°Ê`œäh!B_ö/•ì˜÷Ú¼%UýU`“«î¼ªI ëðª4:ˆÀÖ/ñðƒ#gí’WçñÍ«þ>šÐ¾O×Fa%ûÖ-ûsoòÀ'Nd@¼U¹WZQЮ:ó MÝP)?Ì¿rÑ´ýõ§¹·Ô‰ùñ{Ö/ýfþ‚Eìü/}t`q\´.¯ŸNcCÛŒo“›û À{4:ÿùªŽÂ÷ÛôÅÆ”†5(Ï –:Æ4ÞòíæBVˆÁpÙ"¡@~€†ømþ¥€Ä–Aëg¿×^öçâmóÞXÑ©¾ž.~ 2æ\2àL>ojÒèüK䨔ÿî×îÓ.ûcLúm{|Ù%­’›OÜ{íë÷´æâiÐ µpý4T!„ïû—„9yÔì-;oœ?é_û‹ƒûß0-íŠnulƽ̊É[ý…Aèu“¦v4Žñ¨”ÿî'”¢=«ú}ûQ‡%ÐT’½áëצ<¸lšÚ\5´ÅòåRÆ R~»œ|hÁµíG.Kº¸Kƒ0Kù©÷àÀ<™uÐ~M!®&‰üUGáûþ¥‚õ¯íöéæïÆ$ymtÎqìß_—®ÞšS,'¶êÑ·G“ðª_Z.ÈüéËÅÛBûŒÑ<˜®!xCîØÀíÕ@ÿTÊoýKR`xtºáÞûj8¯ù~ù® ƒGŽÐÚ´uéâM'ªËPŠ÷­^øó^'«ÞÓr_d@#˜ ð&¿Í¿Òi|ÿÍ“Ÿžÿ×¾£¹yùåŠÜ®ëǶnÏ‹étIÛú :\Ü&ôðæÌÜ3„=gýKö5è×'%˜/%e äÀ½!|ß¿”û˳o­Zþ¨öõb"ÂÃÊ ^xÂÍ׳?T`Š 2 !„9,1BÊ;pÂqÊSœÇ7/þþßÈžC»&Ûh[‚Çhn oÇJ?ãú¨”¯/›®Öû½¿2 O 0'†¹÷r²½Ð®˜˯ˬ6‹œ[ìPD@Ù#Jñž_¿ûÓÔõŠK›ùg}¡5kÖTüܹsg¶ÔJð`º^ùå·ù—LÁ ±Žæ.Xw°È©!gI^Ö.óõïN»8ÔÍ—”L'O•Èòi'‡åƒûs s—Ï}gyùCË>š{ôÊÑÅ™+?Oÿ™!U×ïj—_@M}Ì¿x=Bß÷/ɇ¾¼¦sêÏÉã÷¬?ÞìÂäÃþu¤ý„w’ÜŒ#Ö`«p”:ÊBƒì(qšm–“'yÍm‡nZvy…ãÈš…ÿw´Ýå[F›Ùàión6`(ŠB›ÎZܰ N~›©`ã¼Õ^üsÓŠolÑæ¾¯ÖíÚñÁ§£N¬ÕÍ׳F&„”9\$ !„3?;W K ¯ÜͶð¨ á6“ÉD|P# þ@ÇIF¿—u1ÿàM~›I))0×m•` ¬×1zϯ»Š­ GMHùuöú7_ÏݬYø‘µ+7í=”½kÝŠMyÑ-R"L™³ê“Y³¾ÚZÈ¥®=È€—"„ð}ÿR`ýŽ‘›¿ZuDD4oa_ýóÞRgÞÁCÇŽ¹}ÿislÇ=ä¯]øåüEÿ8›õØ6ÒT¶SìÔt$&@ûèØªë R~›)°åøG:vziƒ ¿Ü0ìø%š¾°ç@ÇÛ„¸ÿ’ÖèV}R[õ95UÄu;¡û_ÈøKÆgåà[\ïQŒ?@¥üÖ¿$,õ®ÍعcþM)Ñ=^Z¹ìõ;nœ<ÍWãêrA‚V0 ûU“¦‹- ꬡ òªó/ÞÂ÷ýKBa mÐ訦zh¨¶a@ôsÿ8slÇ= –®^øe‰”ЪïÀ¶‘&!œBQ„P„Š#ïp¾Rœ¿jážò_±¦ 7 ®•­ð,‰Ñ@Ý4:ÿùªŽB÷ÂݪOj«>§¦Š¸îc'twýÜ|䄿¬nøéإ٫ j5(\R{ÛKÐ<ú— Rzš €z¥ñq:'ýF£ó/‘ Rº¹TQ©ê ¯\„͕ܺ¯¶Ý(æØ$(ÜŸ  ªŽBýK€/¤2É D&vVä 6hþ²irãܧ·†˜Á V3F‹š½´Ã’ÓÙd=¨Úë€7п•¢ +kÜ>¢s8j£V®o–Ö¿°ìmTxô`þ%ÀëBпD9¡ÊÓ®i¢ß}üú®ì…P]ûŠ/;jèÞö1þ•bØÐ0_^ÞíÅ´™ªh)»¦ÖæÖdxœ;ÂK˜ ð&ú—t…Zà›T!ä-£ ªŽ‚þ%-Õ1ŒÈë÷H­KÓÂÛãÂhö ÐÆ R ;š,Y>E¼¦—* –üí¢ è›Fç_"?@ÕBпT‰3dî©ðå•Úº,›[› ?@£v€>‹~ê~@ýüž÷ØWó/ÞDÿ`è P+æ_‚ª#„  8G‘­ï»Pq›-õKóí?¤ÔÚ‹“Xòt ::šäø§VSIx¨ýKP)ú—â~gôÊ­«V4:ÿãPu„ô/Ï2$uî©t%•¯–ÜÛrøhð7Æ R ;Ôz Ç ž€o°·΂ù—o¢ Fxþúwi1¼ƒIÂÐú— ê!è_‚.‹‰ê4®Ôê„3@5Ãjjy \5_çÌ'§©ø“¦ùö‹€ZÅüKÐpé¯õBÄu‰B:唋Ú*kt®»M¨ýKP)ú—¨¢²÷±4o¿“êç´Óº×hf|°›ÑæüKä¨:Bú—Ôæ´ZDsåõÜÞ`|6`Bá^垇+€üœÃz–¦£ùX(õ@¥ À]Ì¿xýK:¯r(°À¦UÓ¤ ¨vk'6 ùªŽ‚þ%ÝÔ.iÌOeãsigyÐgµ8¨¶Ù§‘ŸaØôÃ[g”óÍ6ëI8L«ö{ ”„±1+TÊÕ¿Tñ_H­;Ç$’)njôdEÑÿ²â*ü’2& ³¯µ6ç_"?@ÕBпT.33ÓõCJJ Kƒ2«ÆÉ°¦|6e»Ï‚kª"„¤Še‹êl~|…õ¢ *EÿÒiRÊi¦ÕDÂ3òj2BtôצÈW¨æ_¼‰ù—¨ç|ý¡¨SÕïÜ=ôúX‰*ünçjþ¢k¤‹/  ô/AÕBпäÇBM‘΂@UÛ†R ½@*§’ê ½y€S:ùð .›Ös-¥}wu‚ήJ¯\¼ª¡CÝÍ9tç00ú— Rô/Au¥3§¬Î¦¦wT`a€‚ù—€Úˆ‚þ%ÃíJ•³VœµtÆ7±lãEAÞ-ÀfÌgôãP)†Ø:”vÎátdñ½ªÿe©ýï ó/ÞDÿ’¤*Þ¹G,GtŠþ%¨:Bú—PÓâ¬eß$mµ%mÍËä÷‰¤\[“Yé`oÀJôÆ R ;§&IGäI€ÝÈÀÙпDŶ  ºüx³m°'ñdËeþ%ÀëBпäû±Û»fUíÓ 2¨­›Œ²Ê>¶Àn6ô®þÀøTŠaè¼êb€ÞíåzÙ‡0ÿàMô/AÛljZê¦ÐVM¹oï+šo Ù`è_‚ª#„  ðMÚr­õÛ0¡ÎÜ€ÚÁøTŠa …B•×±xX@pÊ ¾ßn·Ñ¿Š  v1îT±“üõ-ÔæüKä¨:Bú—@p ÙÃb— ùFÀ°ƒ*0¥:tQk4:ÿ×OC¥\ýKÿeÀSi®Ã°»o 6ŠB¨ gLøü­GAÿcŽë  ¥J:ËGMè_‚JÑ¿¤Ã=,!Ö£Ïl„ìÀæo`ü*EÿUÐâH‚'¥’JÊ,×çÊðáí,…»g¨h0Άù—¯GAÿtp4¥WÄ¿e(ÓÕ(òŒ€þ%•Öajxà̲Ê€1ÿàMô/i)Wèþ_€êäX°¢üUGAÿ¡€óbØ”eÚPSqœ®SÕêÐÇW’o+@~Nc¨þ%¥${ãòŸÖì©i׋›š÷ý½—m×9´S÷Ôè#ûk™°­ò-ó7οD~€J¹:—Œ$ä¢ÃGíAq‘’BXÂÔóœlûY¡F¢þ©™|@9®€ª#„0@ÿ’RZhW,V×’5È*J JeÖ-Ô¦>ø£•¼¼1øþ‹ €üTÉXó/IRÅP "W}85kÖiL˜0í„R›ÖH›ÄQò „‘æ_’‚­ÂQâtíõG‰C ˆ 8£µ´€Þ7ó/^ÂýK¦àØhkaαR!„ŽÜƒyRxB˜™õÈ@µhþ%k|ëÆûV¯úwÎí¬ØîHn]/˜û? wÌ¿x“qæ_" ¹û ‹â²W~óÅ‚e;míômJ|êÄõPu„†¸œÚ_vu{V9P=Æ Rê_ ?2Rÿ0"æ_¼!Äÿ³w×qQlmÀÏl²»tHJ‰¨ˆ vb·"öÕkw·¢ØÝuíûÚ`__» T,ºk—­™÷DE%u•ß÷ã‡ë—Ù™çœ=3ÏœgfËGýò€Á´üÙðü%€Ò„ú%äEK!ê—?ü¦?ê—àφç/”~ AP¿T6mÚ„ à‹üþ(˜v€?Ïýû÷D‘DòàùK¥ õKP ×¯_#ˆ<"ˆ<"È N!ê—?ü¦Ô!õ¤ª_Êû‰€”î-Y¸…úW¹wï‚€È#ò€ÈãxýÃ×6 ù@qR‚ú%µD \Aý¨)Ô/ (,< @ ¡~ Ô:… ¨_*EŒäã½Kמ|HWpumÜš4v1æSˆJi’Å?¹~3äC|ªDÉѶ¨^·Ik‹F÷ôê•ûoRdl-‹ ›Õ©( ÐeÓdz?Þ:~ê ©ãëã¦ËBä ZýìÖ§ob3ål³Æ=Û;hRD‘zýÒí— Ù”ÐÄ©nÓºvÚßü @1ûº4þùõk÷_%f3m‹j^Ým4Y‘/Óž÷èÌÉ—–]|êè³s¡ð# ñúæ¥/b²h¾¡½GÓNúÜß:˜5…i‡R?Úˆ__>ÿ0Ó²a‡.íëš$Þ9w#JЍ”îÑEš#Ö©âéݾ] W½ÄGA—"²BdQ7ÏÝŒ7ôh×¥ccÉã Ë¯Ä š£,È»ðR–w\CäÆYìÇÿ;óLj^»y‡Ží[Õ6åS„()¨ÙºsçUY//?K£¿ÑPLÒÿ¾™h֨ǀ}ÛU¥_½H§ù2£L¸u`Ë®cwùBW„†N{|1”vhÞ¹KÛZZﯻŸ ø½#‚üÔê—Jû0Ÿõâ#«R=w{³ 掞^ò7ÏãpÞTºã©–SËv]«X™[TªYÏÍ€Nøª ²ø72sºNL+שW™ýñù1æ(õã{jHðÙPÝÞÕtrlˆüOˆzʳÿ½ÒªßÑ»Žƒ•™©yE3.!Š”—nõ-mjÖ«®™ò:.°9p[ÜÀgDÅJµ+;Yéòy"Ggc’“®DäË Ûȳç°!½êä;m.ÂCg¼ MV­ïfk\¡bµºµ2ÂÃ’~ïù¨u AP¿Tz‡›¸4FÓX[U³ÈÕ5)’ãÅ4SVùšBª GUõJq58D&–ѸˆP6¤1OCÒ´«Øë³™d±œáð¸¹p‰,K¦Ds”"yÜÝÓ×3ª¶mã¨Í¦S Lp6 IDATóuyD¾Œ)Òã2¥––CÃöž¬Ôˆ[W/æéu«*–3ìÜŠ«Á¡Ó³åÒ‚š‰[qiX7jãzüøµÃoîèÐJ˦*‹˜ DþçT =ÂÈ¥9+÷¥„¥z­œ!ä÷½ù¨)|\ (•3Z14ÁüuY¡³^]»ø’_³£«!›(UÏ;äÐ ƒæ(åø4éc²$áöám·sÝ=¸;¥mK!"_Ö]]&£9¦5\«Täb¬ß0ñ͑ׯÓäS„ ¡ó"_ps@±’æ„'ÿ åÔîÐÃ"3âñ½‡!7ïY×Ó#ˆüO?¨v„É·ðhä Ö)AýR© s‹£g$ÂPÚÉCÔSç_6êØ¤’¦*¸,¡¡>Wœ"#„¢HÍ ´µxhŽÒ<ñµtõré9,¶@GWÄ!òe£m¢Í$¿KTÅX™•”Eiê‹4tE²¤D M!Ê̸tFËD›Wào¤QJ¥Ÿî` ¸Z:D)SrùŸ9îalçjå»í„ÎNJ”jêó‘?”><©´/•h˜W5§_߸“zëÖŽuU>âRš”I÷Ož~Îq®W]7;)!!!!!1MÊp+Tµå}¼}+4*!&âîÿ"fU+ Yh޲‡È—ý)„¶} 3EøÕë/>ÆG¿¼u-LQÑÅN‹§oo¯ôàÆ³ñq‘ÿ÷,Cß¡’«ÀæÀ·s<˜Ù)ÞÞ¼ó*I,Ɉyq74S·²‘/ÔM&•Ê4Ã(e2©LÉi„aiÙ8‰_üïad\ü‡g7î%Š*;èÿÞ@l£F}J¸Š†;;;ô-(u‰$ïçw¼Ž|]Í©Úw^ðüùóêÕ«ã€ÃÕ«hDx|ûîãÐ(…Yíæªèrp )MŠÄ'·Ã“3b^…†äJ4ªî`¤gfÊMx~çö÷Yú5š4s®À£Ðeu”ÏŽ}ñ"Ñ Z53 Š­…È—õ°Â7´2&±/îÝ{úQfZÇ»‰“>‡b ÍEi/ïÝzðüuŠÐ¡a‹Úæ,R`s „Ž TÁªûâîÍ;ž¾NשָEm3 D¾ìF÷„›?ŽÊ&²Ø°Ç^J­ªY‰¸…a(¾QE=ñ›·î= gÛxy×µÕ,Û9 ¡/*ÙTúæ‰Óë×ÿ% KÔý‚J¸‰³ì“¼½½Ñ¹ tþ¶éàËÁ>]|¾ó‚€€___„þ‹%ß½ ;ðh`‹&-¾õ¯AAAþá%ywÔ/šBý€BþjB< ùÀaÚù@a¡~ ù@ÑR‚ú%ä?„iä……ú%äEK!ê—?ü¦?ê—?-… ¨_@þðC˜v@þPX¨_€ß#þð2^®+@þå#… ¨_€ß‹â݆š\›iO² ‘¿ZÓ¨Æÿ2J´¾|+ɸØUÏäïY2üR„Ô“¾¾>2ø­Ñ’Ä$ ]z+8Oܽë ÈÀ/…ùPS¨_€ß[êßf Þd_÷­\{îÓl"<:±™­ˆ¢(Š Ç¾Î&„ŽÞQGTuhßBJ£ÆŒ‡Yé7þåYQ“MQÇÈ­ÿö0Éç+Iyº¼ßߛ² ‘½;>ÙÛN“¢(Ž‘k¯Õ·ShB踽žšN#Çwp¶63ÔÒµk3ÿz2fäPÎR‚ú%øMé¶9xq†­Fýƒ÷æ8Ó÷g·ð 4™s7U–ºÁåÂ_-gÝBˆ8ä5óIløñ v!³;MxÔ"ð½”Q¦=˜) 3érêg+Éx<ôóö9`0ýf²TòîPÇ÷³ZôØûAA!Y¡ÇRÿ:ðñL‡¿¡[#pÛ €rÓðç?Ú²ë}ËúTÕáŠlÚû-j»wË#1!„cÖ}DÛÊÆ–¶úšÕ¦]~4ÍSW™•.4Ó’&$Xý$y¾{Ï;ÏÅ+û;ëñ4ÌOY?Òðêús1JB×¢Ço ¡tj´®­'Cìù”¨_€?‡,ñu¢ìF?36EQEéw<—™ö6^JáèšëªnE¤q——v±×äjÛÖëåw4<‹a .?R$E¦iÚZiåÁ¹Æ¨ä÷) B[d b«³¹,†fÄ?@ùJ!ê—à7Fåü—«WQ_«Ý©dFE™ñ.<4 µ®ê5ªÉBVvz¶ÚÆ0qvRäÃ3 °aò¯$GßJ;óÍûÌœìBËè[êá(€üÊ7L;ÀoŸ=pfc‚×`oS6È \Cýüîx–­zÖŽšåé<ü”ç’ ;[†NrÑæpõ›l`:zxÍg3—ÉFm¯¯Ã×2óšñ¶Ãü6z1ÞIò¯DœûR·¹çt‹™ë®ÇÓ°ò9aátd€¦àg¡ý‚J¸ŠYöIÞÞÞ%ü*Á—ƒ}ºø|ç¾¾¾ü˜ïÞÝx4°E“ßú×   ÿpƒ’¼;æ@MaÚ@ !5…ú%äEK!ž¿€üà‡0í€ü °P¿€ü h)Aýò€´ò€ÂBýò€¢¥õKÈ~ÓÈ õKÈŠ–BÔ/ ø!L;¨%:ýÞÒVžígÿ—D#¥FöúÀPÏC¼“#(&Ô/¨#IÈÖ'y}–Lk`À"„Fžú|{ÿ&·¾‘åe/öNëYßÃ˵Aça›ï$Ñ„EìåõcúvªïååêÕº§ß±01“wêscÇ”þí<<¼\›N½™õåÊcÿ[3ª«—‡—«Wkß9‡C2ó§-²w'&Ô÷h8ævÖw7šÉŽ<éׯu-¯Úm†,¾ýÍ“teÂŹí]=J¢ !LöÛSK‡¶¬çåêÑÀ{ÐÒÓï²B褳}=¼\?ýi6õ¾„É Ý?wX§– ]=¼¼:]û¿!Šº}öJ/WA§’d…‚W©Ç‚yuß­_|6Z©î]€ƒO¨s AP¿ Fè¸ Žf7_ÞÛA@!²°õ}zíûÀbá•w®žyõ¤U/ܧ¯›e—¼Øú›Ck½…ÑÃ8uzû´æE_Y¿tùXm‡Ç;ñ‰2þÂÜ> Þzü5råcŽ”²á‘=¼Û;aúqÓ±[O·ª˜ugݘ™£VW:9ÓUD!ÊøË‹Glˆ`õƒ­–Fl¸øŠÕˆ5Û]ä·7Μ3Ó¢ÊÖÞ¿: ¦Óî®ëGž»¾ìÇùýÏÍ÷↠ÁKFϯcpˆ!„¥ÙxòÂ>¶2õ¢ˆ|aØqbmmÕ)6ÏadàƒaÑúúä½FrôjVí)£:×ÖgëIwÎ;r'©yëšÖ¹RªßrÒx<äÑ‹D¥“¹ìÅ?kך¿^=Ý‚«b$‘7#Ù5F´¬f("†õ}Zšý{ýeŠÂUÄ¡Óîm±,¶Û²ñÇÍý~%•ô͹s1•¯íQלMìÇùžéìr´O?Knþ1âÐ=“§ß¯»h¶ØoÂKBQ&?}¯çÕ±I%] ¢Û²»×š‘߉;Bů`_ÝÅ)_®#j¿r]Õ_]Í“.tÙvë½´›Aåêy¡ X¢å½Ê][(,0lÂ6nÒßcãŒÀÇ£]¼´)õí¨_5…ú%uC§>½ôÞ i㊼ï¤É¯%úŽVš,Bá[8›ÒžFËEåž3ò¬ [KOÈ"Ù¯ƒ®¥h1ç&µ¬ïåÖ¨óu×㟯N`åi£¸µeÏ89C§„i9cÕ7íÜõ±õª¹&m x’ª ò¸g/åvõí5)BQ&hìêÑ Y¿¹aª‚ªOçûÊìôl"2±óoCæ£Û"ìôwQ‡‚B(­­ªÒ¯¼ÉVën€ùPë‚ ~ @mÈãBc¹•ÜÌyßË1²’³¾–†ê™%Ь¤¬|5ýЍ ÷Ùž³kêPtò»—‰ SÙgA/SE؉ÅK§M0<¸Ë7_iצïò™/úúêpÌÊN+S÷p₦M;÷Î ÕJdSÓ8ëjd&í"Pß«üÈ@M¡~ @Ý(²’²5Mõ¸?zÅfç^bg”4“ï_qèöéëß{L=ÐØ€E’´l–a½¾¾Íªñ q²šúàêˆswâ»W4Ë»v¯L¸²~K¨—ß¡>ZwîØ||ûòu–8=x”6µÝɼõŽõî9îÐ^3ö·¶ˆ•·E„V|¶I„(Ÿ=ŒJŒòïÚÐ?wÑœ6>ovïêøtͮԎkZÉ®nÙ´fýE×y­Ì4,¼[Bqt2ϸÙeû™WÙž5ªUÅ_Z:ýœv¿}«|J˜Œ‡;·GTàï¢I}+9Ø:fº,qb¦’!("UýRÞO@=ü 0Ÿ%Ò‘ì ©ê–:;]J‰¬„ªózÙûc³ÆïáýµunKS!„Å×ä3âqÎí |sm*,YL’›È"­»"èz°±×nJ-ã>]·l}~tÒ U yºdÈJåø-s›UøFòÀꋈ4w‹ˆR’!ãjëò?Ÿs,»¯9Ú\ªZŸìÕÎa³ßôÞ¸¨“iøêQÌÇήkmÀ²žíá"êÒ¦mÍòíÅÓ5³ÑÍù©D—ËT°4×g}H–0¹³”†‘•‘fÊhZ¡¤8lÕâì÷ßÓFmx„Ði÷7›r¥ÊôMsšæÏd˜ôû;v¼¶ýk¾k^‰SA¡È¡LMc„†šluî¸Ôn›P7\ccéë‡ù¾>–eeddJ” -ËÊÈÈ’Ñ„»6Ý[¿áă°ÇÁ[–g»uv7$É—ç™ÿ²Ö¸áu¹Qá¡aa¡á‘ RF£Jçî•âöÍ]wöqÈã+Ý´ìSG% [ßÅÕsÈé$šoÓ¤®îÛ]«ÝJMz}më¶'‚º5¿µÙ¯Noß°÷zìç7aóm[µ1{ýÏê÷B^\ݽò`¬m‡¦æ\òé] ^™Ð¾ÿÉÆµ'BâRÂÎüsà•a“vœèããûNÛtäê½;WÌ9’äÔ¥½ Wöjçà±G5|Æ÷°J{öòc&M‘8¾ái6¼cÞ=tÁ¡P%Y‘âDv6šj}ŠŽùPS¨_P7,]禖ñG¯~\Yuw°4toŸÃ‰„òvHó}>;ŽLqÒt³tÌ‚ù+GœÍâ×ö]0¿e–ò]ä«4:6xþðàÜ•Uèµ+p‚ƒm¿å Òæ­òzT¡U¹Ý”µSܵ)"#4Mš!„Òª=eÝTÎ’m#º¬—³ô«¶÷ϸ|· |™>¼>¼i{P­Õý¾¸|ϳ¸tJ¼ßÆÒYzU;ÏYÔÓŠKò½Ë7öÖÐ{îªÄe+ÖÿÝi>Í5®ÙaކᵴØUjV8u|Íô½Y´ÐºÞÀõÓ»[rHVbØ;…D¶sJ¿¹¿^ÃÿÊæ&äÞ¶¯mÿšïúéy¬ÊŒo„‚O˜Œ§çž±j̵ÕPën@9ú•p³ì“¼½½ñ‰‚_%ør°OŸï¼ À××(1:îäÈŽ›+®=<µ¶Hí¾¡@þv—ï ¯íø¿cp•±'÷øÇvÍ¡5ß%Ã0ßù×À£-š´øÖ¿ù‡”(ÄçÔê—Ô˸ùˆ®üóKö‡«ßWÐ)ÏnÄZwêZù·Lr}Ãæ0§Aª Ô½àsê ß ŽŽƒ¦·ÉÚ5iÑÉ´zmYVصXŸ––Üß1¬²ÈÃ3g\«8bj+S¶ºo+îµN!ž¿ ^XÚµ'Ÿ»9Yý6L«ÁòS ~רòl|6ÜðùMz> ž0í€ü °P¿€ü h)Aýò€´ò€ÂBýò€¢¥õKÈ~ÓÈ õKÈŠ–BÔ/ ø!L; (,Ô/¨!BêœBÒ¨_êÑ£Ã0ˆ'@ÉaþÔ¦?ê—?-… xþò€´ò€ÂBýò€¢¥õKÈ~ÓÈ õKÈŠ–BÔ/¨|ÿ4¨)}}ýÂgG1äP~©ê—ò~~ç•-š´@¸~Ô/Z§¤põK_ß#%X‚%X‚%X‚%X‚%È )Òàë›­±K°K°K°KÊí’2E9ú•p³ì“¼½½q¾ e‘B¦~©ÀßÂ,Á,Á,Á,)ÏK¾%((È?Üóðg*^ý’êõX‚%X‚%X‚%X‚%åy æ œN> ¥óðÇÂ÷Ǩ!ä Ö)Á÷Ç ø!L; (,Ô/ (Z AP¿€üà‡0í€ü °P¿€ü h)Aýò€´ò€ÂBýò€¢¥õKÈ~ÓÈ õKÈŠ–BÔ/ ø!L; (,Ô/¨!BêœB×/1 —#–ˆËCЄ¡©‰©±‘1EQ‚SFÁP®‘?äÐ××/•;ââ’’“œ«9蔇¸%%'……‡BL*˜ 8N@}`¸FþCU¿”÷³Øë‰‰q®æ¬£­#“ÉÊCÜt´uìž>Z˜aÁApŠõáùÀg))qý’X"Ö×Ó—J¥å$h4MëëérÁApŠõáù@ŽÒª_RU2 S®¢WÈšHÁ)^pÔmàÂpü Ôê—TcJ¹VŠ´³‚S¼à¨Õð…áù@N AJ\¿DŠaš¦ËÕ°B ÁApÊ.8êÃõχï5UZ_û@Q]*²_®óm;òJª’.sòèãZØõV–÷—¢®¡ð%:ôÏ"~¶¢CÛi72J:;bcï¶CÎ%»!~IpÒþ›Ø¬Ûêg’2ŒpÞ[”ä½P¿¿_þðeêõK(Åú%RÔ™>&+âܶµ®„$É W׺šG»~:Ø0 É™"ý ×Hþ÷*ã7-ÖÊeÛÐiÏú¦lB!ÊØ£#v[½ço“»“»Ï‘=´ª¹•ÿ]rߌ)˜ü¬ÙÛb¼ #¾°}ïùûoRDPÁ©v³n}º¸”}KþÜ^ ^JqÈcÄ‘öî8xññûLš£cãÞ²Ç`ߺ|\^Aþ¿I AJ\¿D¨¢–EÒÉÿ-¿.¬jÏ1KÝ*PÉoî^8{ñQRëŸwRVЉ`Ѫö ;«I»fTõ[ù~›É˯xö=&L¥EŸæLÉò¯ÏBÁEeyôI¿ÑkC+¶8m”½®2îÅåÇÿ9]{U5RÖº%ë5E €ú Jux•½=0mÌŽ˜jÝÌk%Èxu=`ç¢!á#ÿ™ÛÒTmΚùpüÔT©=‰PEV$ŠFOêÙP"„8ºxz‹%„ÅD2„Èâoo›ºÿðÓ$ Û–fi`Ä!Lö› ­+÷^z™JkZ×ë5jDˈùýæ‹ÇïZä©E²_,ÿkæÇÛW5Ó§¤/× œ5tÇ’zÚŸ>õâç›g¯ ŠˆÏT¾QõvCÇô0¤òNIq’–¢–ø3øâ$õS!ÿ´y•x¢§«1O‘pc÷ê ÿ>M u*Uá§Ófª_~¸¼yÍžó¡É ¹—Ïè‰]´¨Ï2¸ÿ­óßð¿7‰bšÌÜ»ŒœäS]óS(HI‘2“|sË–Çz=V-`¯AB*Û×ðjÃpÞ~ŠW»/) èãÆ{Œnu(à–hÈžå-Œr N¥oç,:ù,:]F8zöMMÜÜœW²^S´à¨Sú@•^þ@'þ·uO„åÀ³{Xò!Ä¡jÍÊüÁãwn{Tw†ș¿'=nÜÓþéÉàð4 «ÆÃ§liÁ#tÚ£ÀuëŽÝù fë;x™8°‰©òáÂþ~’žÿÛs.<]ÇÉgòô.:¥uÛî(˜ªr‰”øFŠ¢”Ê"•ÊS"C‰y–ªÈYÀ°44X Í¢ßu(ªzŸÙÓy¤Ÿ_¹íI­L½·~òú'½ü7¯_2¼Ú‡í3—_Ì´¨mI¿}-¡éì÷7¦+"®…¤*iEê«°,“Ú6&ß›)iŽs»1s—o\½pœgú‰å[î¥+iZumŸÎ÷—"P*•…/ñ/bp>aB¥ü…’!„¡išfC†¦³_Ì™{:»þÈ9Ëæ kiÅ#„04­L½¿jú¶wÎC×oß¶e‚gâÁëî§¾ Ï´v±þë׬X<Ð!rÿ²]/%ùCÁ¦¨1ùIÁQ¦¿~"³ïÒΖ÷i‰ÀÄTHr7¿ÀÝWØ šQ| Øøqë:¸³ƒ(ß.3,m‡æ,_½aÙ¤ÖÜk+W_W”°×-8j”?”àXVÀ0~é…Ò¡}3sNވ˷ñng™õàÊ1Í¢Œü÷º¢NßS7T^]¹øÔ{™ìíQ?¿‹‚Ns6ïߺ°O…[Ëž|/£†–<ÜuFæ5|æ¤þöïW†g—ÚÍ¿|¸Æü¨u AJ^¿TäJž]÷¿›=^1»Ï#;·š®5\=ëÖq0äQ Ãv•Ñ‹f4Õ£ˆ²bÌùkAa R›·Çn*êNæ]U“ëþ##nO=z;{BÍ i÷^§)­%÷Ÿr<Txy=Bìéõ,NËÑIõÙÆ*wîU™0²Œ¤$V-w“3ÿ‹HV¸ñTgà9Û‹S¿TVÁùìMèw;vÙ™©QÍ|[-yuült•¿7ÿÝÔˆEHeî½½W’¢L¼xSÛgc:¦lBŒ:÷¯unñÕH‰k5Á§C®[BhiZR¢]mW½káï3i/BQ‚ú¥² Ž"õ}‚B·ZEMêËß˯)x÷³]«}Ý \9„°{,^Úß–÷Ŷs+¶èS‘0 qr"ÇÍË2ðÐË8YSËßÿ€&à7UjóŠ´‰J]g3aþ‘­cmÊËŒŽÓ– á8š;±‘Eˆ›î›[ӯܲÿp*Þ}ì¼ÖŽBBLš÷êpbäµIÍ+J£ÎÔecji"Ó 959ôuº²²«´ö÷×ù¨©Rüþ¸œ«À…ÆÒ¯;n]ÕöïÝüäÁ©Õ‡¶´œ¹x¤3Ê͡hš¦ák (…D&M ¥=Lùª7àW5!Óu›Už|ü>³FÔ=IÞ­õ>.¿™nò–¶îkÆý|[äq×÷nØvæy‚’-Ô≕Úr…’æä»Ôž{!¹ðÛ_ŒG cì¢B·>¶žaÎýÓÉWW/»©ºüMaEú»×™z.vÚ„¦iBr/ŠgG‡DKÞîâ³Wµ¥LNWÏPäß&ûíÅkö]|™F¸"-*Ki)S(?…‚aT;IÓŸöR¶ÁQíÆïA IDAT$)à×hF5kòÝ—Æ\ûº°BñD|êëmP¦<>¸aÛÑûÑRJC[˜­`™+T³VLîüCÎ MY@}”àXVÀ0N¨Ü£MþÑ‘aÉ=QŒêÄ3®bLÝõ&")ý•_¿lŠB¹Ln”˜©0g(–ê¤Â×Pr‰LYZϘÅó— VŠÏ_*ΰÂÖ¶ukjëÖ´{ÿÄ`¿k÷]ﺠJ^UErdž&¹E;ªaƒ&„04×ÜÕZyàiÄ“°T»®¶Eάe7#ª¿K5q¯ÈÿlS”qV,=Çê6}cW— ü´«‡Î;Eþü/EËÊ68¹çÔ†¡INþ¢Aåä: !„V*ä4aQ$gýª²&šfhÃq¾v¢‹0oèçjjäÛù»£þn™ÿ5OË*úTtฉ7™|¡`T§ÇÅÏÊ08”¦™>;íÍûtŗטhUÏ(x÷éü¾Û >ßQ&ýæú%c›L]·ØÃ\(}´¨ïš¼c]^gaèbåyàwTjù¥i¦ÏN}•©´ãæ ½Êä71r‘‘Cç; ´RASl¥d؆ígÍëZ‘›»¶P‹F’{9‰VM óÀ¥ŽÃ5îµN!HÉ¿?Ž¢hšf ÎŽÿ,Ëû_–¦…™ˆ(¤ŠOÊ7MÊÖ«dŠ ‰’¨Þ!;úY41ªdÀZ»UH¾}ébtE¯J¼Š޲;g/~Ô¬^Y—õÙ›Éb_Dûö\Œø„a”JÕ…ãÜYÉ|)Š¢^bgЇ0_nYÞ¦BÙqR"¢Ätþ_aØF•蘗)|]=Ýœ?:vþI>†Ä‹jvnVECFu5|Šbåç‡:²ÃˆÈ·×ʬ˜˜LeÎ6¸ûô7ºÁ7vTžöNnݲ»™€Å09ó=_= ·Lƒ >Jt,ûj¯ÒБv*ø}Þ‰-Ž:óAàRÏ’÷Ù™-~÷,†mæ`ie)HyM´sGu=mM+ßC ó†e”Ì?ì—=I™vgí¤“úÍ:4r±Õc¥F\Ùs)³RÏZXÙŸ”÷“hÕèàÁ^¸uË…á«p"ƒ·žÍª6ªŽ‹¥prî8ÿºê(;a(+¯Êi‹n“:L8Ÿo Ç R:ðÔ±ÿ jð>^?´/\¡ï’{˜·ÙŒÚ>‰ùüùKùŸÿÃFö…+×ûæ#t#ã´Ð Ç®f(†eàÑÑýÀªÅ+õ†tªeJ%GÜ»z—ÛijœB¯‚­^ÖµãçîжʈK¢¶_ÅD]Ÿ¿Déyöïu~ÊîÙs²|Û{Ù²“#nž=õØvòb'UTàî·ïWP7 ßz+[ׯ„ýoð¿—M=t“îÝó0›jÈ|y» Cðü%('ùCi>‰Ò÷ìßóÜ”=3dôiïnÉÏ|}óоàt—!ýkˆˆ’aˆ"úæå[zŽÚY¡'·ÞäÖQÃÀδ¥É¸K6“^Í*kJ¢ž_»ô¶Ö¸Á"òÙICIŽ\j8\#5UjõK”ªb±ÐŸX–N­]"O^Ú·âd&M(-KOŸéƒÛš±•oò}ø?3kÖ8ïïëÌ•Á-Ü{ÏÜHŸb®™‹ ï|J=G-Â0ŒÀ¶¾=û¶¤¦%ÿ‹ a™4Ú?dõþUþGôZµmcõþVA—ò‹6âÐ4]ø¯8(Zp>»>ß´¯Oð‰¦ëÀq]WmÜ»ê׸fë&5ÂO1 ÃP:îcý‡îÚ~tͬÃRÂ3´¯ÓÚWŸo#¸ÖÆt~»úàò™,£š-[´¨øê³P’ŒÂenÅN³ëì=rtmpCÆNu¼´4g¿Ëñ Ø}#óJßìu=|´|ÏÚùAš¶ ;·sˆ8ó)2%ɰŠuJ Š{,+xï<{‘î=Gö,>!fXZ5½Ç®êîQÅ0 BQ&ßÝî¿/^.´ª?Ðo@u¡lºÎšÃÙ±ëàâó4Ñ4¯Y¿ƒ¥Iüò¤)Å/©øåÃ5åèTÂU̲OòööFïõôàñƒjŽÕd2YùÙe÷<ô¹›‹‚ƒà”QpÊã^öfû˜9¯úl\è¥õk/¶”p¸ ò7(É`þÔÔ/|þÒïî§< Á)×ÁP?o¸Îy>HéÝýÛ×È@M•VýE(¥RY®l¯T* _âà 8Å€å?m¸Îyæ—~ùpüÔ:… ¥ñýq¥Yù;(ê#J§ÁP«áëg ×\ëþëvædå{¸Fþjª´ê—X"æóøåäD¢(±D,§ì‚ >0\#ÈQZõKf¦fQÑQº:º|ò·livjZª¹™9‚ƒà”]pÔ†k䟥¤ÄõKzºz4MÇÄÆH¥Òò4>Ÿojbª§«§T*§Œ‚ >0\#ÈQZõKJ¥ÒÈÐÈÔÄ”Å*ß¶NÓ´B¡Ëå‚SvÁP®‘?ä(µï#D.—ã¬ÁAp0\C©`! Î))ç/òøÃéëë#È EU¹„Dù@aS‚ú%ä?„i5„ç/š*•ç/!’ðGòööFþðe AJ\¿äëë[~"Æ0 EQè9h#Ä ±Ä ±*­ó«j¼Q¿j õKÅ€oF!Vˆ VˆZù”SxþR10 ƒ  +Ä +Ä ­ƒüÊo Aðü¥¢ iA@!Vˆ VˆZù”G˜v(ÎhÂ`¬G!Vˆ VˆZù”K¨_*Îh‚kEh#Ä ±Ä ±Bë €òœBÔ/C£Vm„X!V€X!VhäP.ý.ÓŠºÕïµç£B6¦H³™éW†×i¹øI6úšú¶b… Vê+eô!Ÿz>[#åˆUÉIž,hÒ`ìõL5Üóøcý¶ßúF†Oý7àû@M•Ê÷Ç”°K?þ·oÝîS×Câ³ W×Ú¹QŸþ]ëZ Ôô×™™™§ÎüÛºeÝ"_H¿2°ÅŒGù—Ø ÝÚáâà Fëί¨ŽNö+|¯x†N}'Ž÷qÑÁ•‚bEè”G—¬Ú1<•&ì]›úŒíÍèÓûLû|Í¿u<˺;­åtöŠ“óëËS¿j¹Àhí©Åµeò~ÙÏæwr,•BˆQ÷}G†1ë» ¼¹©N †ÒÏZJòpF»‘!=`ÍýiŸA޶eu¯v=ö®gÎ/ƒc‚øÑÜVÂ2¾ZnÐyÍÄÈ1Ó¤3‚·µ1dý&ã¥iåÑeü´õ+¨Ë™$“ýþü¦eO>ˆ’¶vEg¯6ƒG÷v×g•—O=ò€¼‚”rý’ìMÀ¸^k^Úwº`„£>øòÞÅ#»Wìu©5£º†:&Y™ö%§$ØßË··¶¶Î÷_¯¤¿z4‹[k̲áŽ9{ÇšYqíæëkØóÑ¿~‚Úˆç>qÍhVÚ‡»׬:]t|m{S6Bõe¬QGÇÚØpø’±5 äqÏ®8z.䝿H„¿Ñ¯Ê”FÕIGÕšØÕïÿF|&´Ôß‚gÝmÚœ4kCöÏú Žr é±7ÏÚ<©×‘Û7ö®TêG û¿7ýÓIÆ"~¼xì6á˜å£«jBiYë|˜3Ÿ®®ÅRû~¥¯9™Ñ÷Vm7UïøÖîÕá\’ɼ¿rÄŒ æ½Ç¯lbÍK‹¼öÈ…+QÝÝõ5ð©/ ¸Êjª,ê—”±gý×?µºeë¤nݪըݨûðù¬ô©ÈI½0Ô½í‡ôlÖ ó¶ˆÈã3ºµnêæáåêÕÒgÖáÐ,†ñiÍëÛ¶yFŸ^®Þ/¹—[´¤Ly¸g\—F®;Íü÷­´t¶6+++ `rJ2EQééi÷gdd|ÿW º›Š«_©º‹³³ê³!õfŸßÊàhÅçWmÞž]0¨}/ׯ=Æïy’F«ê²z¬<¶mLþý¢“./вi=W/×&>cvdy»†}žß0¶SíF®gÑiOLéåíêáU«ÕÀ¹'_‹UAM{¼}’—‡—kÃNÝ:5pëÿo-¾3­yýñÇO¬Ú¬n;ÿ§ÙEžÎ ÌY[ÓNýœ’eGž^Ü·u=W¯ºmûN:¨.SöŸÇŠNyxþ)§Áôi½›ºVwqoÖgòÚC‹Ü#æô_)µ¦[/×!—âÃw ïÖÚÃÃËÕ£Aó‹O¿—*>è?úZVæå¡M¼\=:®x|gFóF#n¨ &”чûºûìz+ÿ*&ò? _}}ý$*xåˆVõ½\=švÿÏ­D%!„0’ˆöiUÏÕëQ¯Y{ŸeЪ²Ìz]çï\>¸}W†í& ÉúâJ'‹/Òr(Š+ÔqÓ/Œés8QþdVcO/ׯ35¤ø®;³g’Oc÷'?¼üQK…D_]·`[H6!D™x{ã¸îu=¼\ëw´êr”œ¨äzc—üÝØÃ˵尵÷RiBè”{[Æw¯ëáåZ¯e‡kÿK¤ ÿtrªáѤëøe{Œ¨ôlã‚£ŽùÆèÇ+unTßËÕÃ˳ãÈUÿÅ+äo·ûÔmµ*,ç ‹XÓ©aÏÃÑygy,‘¹Suggg—j6Ú,Ž^¥j.ÎÎ.ÎÕL9O­ð|§Píר};fôðððjÐåµè÷ç—häáåÕeÚ¡HÕŠå1W7 ëÜØÕÃË£ãèµ7•?¼r¬Z»i¿™£k0¡Wò¾whèµûäêáíkyxÕï»ô‚ê˜)»²vd«º^®žm¬–Ó³ å spÉ# ºždÝúèv.Õ]¶¼dçÖUxßhÄÒ™!‡§õjáêáU·ûÌ#ïå§ù”7eðü%:ñö©§ÏÁ+å¿øÎÖµ±ÓeB䉧Jqö1ÈË€£i×rè¼í»wïYÞÏüîê‡ßÉ !„–<:t[§Õ”ÅóƸ%Î^”@Bˆüýî-O­ûù­˜Ö†syåÂ+I%ÿL‹Åâ€ÀýIÉIúúúÿ]¡B…ÔÔÔƒû3³¾W(Zà·É0´2]Ðt'z{ÁÈ5‘µ&í?qüмF ;¦-¹›ÁBäï÷m{nûù~iT¬;`Öºý{vlS=âŸ9›BT÷O(Âvž¦[O]>¹¹¼(6E®Ž±bñõ øÙ¯ïEæ5°xZúµ§­lɶ´-øüÙË+èpt«·½jÛîÿøwá]ð_p!ɤëú…žA½eÇÎ^:¿w¸]Aµ0Ò/c’¢ü“ú•Šäù¦1Ó‚…=–í:¼s¶wVÀØI‡ÞÉé´[K‡,¹o5xmàþ-“]ß®ãw^5¬)¢ŸNªÑÎòé]ôn¯›¸íå·/ŒP:ll¯ÏvšzìÜÙK'¦»H¿5¤¼Û¹ü"åÙw\ogí·/gõòw{ÇOÚ/n6çþ˺ ƒfÛšM!´øÎÆ£²&S–ÍQõÍ®y»Â¤™w–ÏÜ™Ôxþîë§õ°“Äd*‹+J£rç!Í…aǯÄȾ56ªÖC¸ÆµºÏXµýàô[y+Û¼Y[ë„+ç_K !DööÂ…$û®õMŠ8…B‹ïíüÏÀgÁ‚Qî±GÆõ²=«é´ÅSÛó®¯\s3…!â盇ùß±²ñÔ¿k»²Îœw.þýŠ¡JÂÕ² ùö¡!tÓž(·aËk˜vÂíƒL"{µ{üÄ£ÒfÓVn];©£-Ÿ0„¢Œ+h”ûñÁ%ÿ±\ÓX‹Ä=zžwuŒ-Ðâ± <À) \˜òßÜ1«[õ[¾~ý’îúì?àS_†P¿jBÒ¬_’%DÄ3&õ­¿q«[·Í†]ÓkkR„âô×@B”âäX —FÕ…‹žÄÈHBX¢Fó7L®#"„©É{pcÆ¿OÒ[8±¾aÙßÖ\ÂÔÈ>ýﲇQÒ–%,:>~âHbR¢žž¾¯O/MMÍÝ{ØŸ˜˜püÄÑ>½úáj-¾0ºÅ…Üÿ5_¤É—¯HøoçÝ×7gbÒg„×±iç"$ƒáXßøÅ~9{õHGêy‡DfÒ• !œjÓ×ÌncÀRT~wè챋—ümÇ#äôA¿ç±ÒvÉÅ6˜½®K !í‡øô º“èk].kt¾yň–%¿¾ºqÍ-â:ÍM› !,‡I;wt1aÅǃîhuÙ=Ò»*ŸJfÞï¿uíö½;·vNر²J¿MÛþþì¹ʤ;ÿ,Zµçï³Y‘Dβ*Ô†BÇ/bâ ÍúíûÕ§{©o"%†ums ì9†ö¢Ä¨:š6ol™sÆ·p³ Î„ÆÉ< !„•®iՊܽï“ä„ð ³-ßRX|-!»¨-•–(ªìd :]aiÛV5ßL£- ÅârUÛÈê‰X màÙÂ~ù†á#ê7t¯Ý°UëºVBVñϺ1ßÚyô… Wz«d‹tøYJ]9M8¦ ;ÚmÚs>rT%öùK)Õ‡xý~hŠâ¨F^–†&ŸÍR§”†&Ÿ¥”)eñÏßdD_ÜðK5 •É«$Iò³ò‡OãǾ‹ßöѵ´)B~xh ,¡¾¥HÓÞ„e¸WÕû<*r Óï\>ï2,=÷Ñû϶»{ýÆí»wN. X»¶á¼]3Ì hÄÔ¨>‹"V½+ ÿœO=ò(ÊàùK<ÃJFäÔó·bÆF‡Òpº}·<ãþÜ‘_]b<]=iÝc÷éÖ·tÐU<[ÚmЇ%MqøÜ/†lU:cø”IÓ¿X"‰¾3óðyr”WÏÆ±ZÕOÓ!é/¿ƒ” §Æ´€šy‡/ž¶fúá¯÷Kþzïø…—­Çl êâlD½ß޻說Ž|,*ÿaP58+¶±ïªÙäž}Pl¡.·|vìÚˆÅó˜²~\5mMCS.Eù¢Rä«>ÅÂ(tî±ôÛoV`äu…Ë­.»pëî•Í3wvÙ~`üÜ€]Ï^øïÎí]~»¯/>æß@žYR@¬(¾qõFÝ«7ê>hÌ›Ýuݺõf·IùºrêÕ…Ó·}l³8p[c+ÍìÛ“¼ý ·b¶qû¯bRSHýÞýê«ØfikahšpÜBÇãÇCJa[ê[{ûõn¨ú+צ÷Æ.œ¾rûÎùÕÇvý;tÿŽÁ•¸EŽUvÔÓbÖʘ^ÐŽdWe@Ñ'gO?J Xq¤_3”³ý;îPõ¥†ìVï>ÚŒw1½Æw½}’ò§¹c+£dØv÷¬km”w†«©Çú¹ãÕ˰“V¾ÈÌé²ršˆ¡ Ãf} ¤~ØÈ\¾ÂÖ¶ñlcãÙ¦÷˜ÔëÓºMÙt¤çìz#/rÕ× ¹‘+”›Åú£>õe÷?€Z§¤4ë—X†îÞòÛ[¿Î&„%0¶«âàho.úêC L~ù"ݤU¯ºœo¥øtZøÃx¡½½¾Z¥àźÁ3u2¡?0&Ž>ŸfÈ~{?RiQÃü»m£ØyA.ÄR˜–Ê¥aæh”þ"QÕ"tú«çIB낟qLÓŒ†yívC&/ØqpCg½È« ¸íG±’Eþ»õB¶s·FæßÛYÔ£w¤Z¯^fB!ŸžÄÏ6©×Á!%xÃæs™5;Õ.õg1s -ØÑÏbyúy[¥¯ÅgýäñªJÍÎþK»‹‚æŒßÿ*›I¡ l- S^bÈûÜûñUžPð(gT¤ã«"ùUŒ$ïY" []"Ï&Æ4¢À¬ …†¶ʨçÑ2u89/»#~éõDœ¤‚z*‹ú%ŽYûÎöÛš°Mšôm¸eæä™†SúÖ«H%¼¸~ö¯ï¢‚Šà5Ì 3Îí=rqP¾8¹eË{y•¼ƒÀ©w‹ž«&/&Ã;TÓ¿}pöTD}ÿ¹Mu©rر‹ÞFlÓ&=Ü·,™»Þqf';Ùã} ®pš­¬edgÐHïĆ%{t}ìå¯ÿ 8%·*TäëŽõ ^÷­k«:–ü¸›O“4¬¬>,`Û­£We­ŒwãØ[™ðÔ0VŠ˜““‡Ü´÷éÞ¼–ÿ põMVíYµ õÂLØï/_¸íæ¡!gW²`ï?~àLÅÆúñÿ۵jMáØê‹Ÿ¹üDÇ’áZ™»Vb߽ㄨ'ìì?;"V„N ž²ôyþ˜T1dÿþýJžñø[õðJŠg`åÛÖè¯5Köé ª§}nÕŽw•,·5ÒëÝ„3aéÒ3û9sÃ/>œé6»¡ [Fˆ<îÆ™+^&òð“ËezÎiüùýJ´4+S¬`¹83K&q l •'ÏŸ`UŘ6üÑÂ1¬òÖ2Ëyì&϶OåC[ì°ÓÜ4õƦ5OŒÚþSM@ž~ùû½“·¦4èÒ²† yw3$KÇÞV»põKŠ´÷á¡BVFB䣋‡v^ˆq¿£½)SÐØ¸¸­êW¸ÆŽ¦ô?{/ˆêðßmÛð\nè®:s5òêâ¸ÌïF³U5µK}£tÜûµÖá?aƒdX³Ji7Ï'vX4ÃMøsÇ+–¶ëˆ5^ù®˜°Äf×ä„޹‘­« IDATïešnãfv™´xå˜ó6zôh²%+ç÷‰ôÝÉE#'ÒzU;/X3ÈQƒ(~ÿÑ„eÐØËô5Ëwκ=›ð«5ì>Ô¸Àê"~•~óúGÌÚ2meêÙ­S'³B õü*¯[Ï[±zëÄ£©4Ѷöôî]IH•ÏŽ]Œ6bWh¹pu²ÿâýgRºm'¯ì¡Í¢ª^2<~Ö?SÆÊuœÚtô4xÏã"òvF•S Oìžyli6á›Öîé?­¹õƒÐ;æÞ˜E؆Õ;øù·7g«a¬¸m†öŒ8¹fêÖ,†p ªµ·u¬·1›¥3|XÝ kGØnØlåÁ)“ZM[ë7ö¨N•Ö}{ÖxH!\뎻ߘ1ØYAÕQ;6öš<áÆÔuó'Ÿ4®Ý­OO«Õ7¥eWÛðxþ˜4Ñûýï åÖL”û¢æëƒf­Y¨˜¿jb¿Õ ¾¹GU {XsYÄcÒæ‰+çnÕ=•Ñ´i4bõÔÖXôGB“zûøïÅ|‹†CWø5ÿ¼Œ?ûŲ.ªï›ÑªG÷}GÆ:4=(hÚò‘8Vݶïûƒ!…eÐlʤ[ßo©-ãr[Þª÷Š%™óWLÿk—”cT³“߯aN".èŒF¿F þ²­“ö$ʉȪñß ÇÕ.V²;K‡õ"„°4ͫռrYoOS!ä{c#§b§écÏÚ8kÌNÃ]}}ìÞ\ÊÛ»:-í¨Hë.Κe0ÌQšnc·/-Ú´jÈþLÂÖ¯R¯Ý` Þ¯¯¸Ö]ü—¾ì7fúŒÊ»æöÐ@éÔ4À ÿ³þã˜{ú´s~€ùæ('+Jþ ¨6dDãÝG÷Í9œ,#Ddéá»ØhU‹Ј|ƒ*_/ZõX2íÝ”ÕK&Ö­Ú¦sGË·7ÿ¸£IivDG¿ ®b–}’··7ÎwA ùúú–ÒÊÄw¦u˜Hž_T[¤®ûûæí[´»:+›6’<ôï:Qáz®«±B¿*1ÅǾ¾gÚÜÝׂƒX“zeR·ºKOά)D¬ Œ[§xXƒ‚‚üà Jòî˜5UVÏ_ú£ýÚo³‡ŸÛFÙa{îë¹8[в"ήº@5Xi/@¬Ð¯àWÇŠN¼±ïžFãõBÄ þÜÖAþjªLê—þt¿v6~n1ÙÑ·vlØ•*'l=ïQË'ºjRˆúüâX)¢.>Õn2þHçѯÐ:Èà·L!Hi>©„„î‹.\WóÑ„ÁX¯ö#~©µ‘ÀeüÎËã+(ýXq,z¾Þ±*^ð*úî¹ç‹XÁÞ:x~+¨)}}}¡È£ ®¡+Ä +Ä ­ƒüÊ'Uå‰"ù X6B!Vˆb…XÁoÞ:¨_µN!H‰ë—ž‡±~ø£eDÛf¸7xóNaÎÄG–›;BפܫÒýø& §zzßzSGý÷ÚOR,4سkrÂÐÚŠ¯ %ý{º~å-”Іßïe,¹^^Ì`<ÿÖn³‚LíxlfBÞòðü`¶>Ž ;[Õ7ÁÊó;Ng†°5õZÉCƒºØñ8•êtÄóͱúw¼¥j»†;ú¸ìèÍ[¹69Rÿ_õ+ŒçßÒyt/ ¡×ÝŒÌX©*ã0[‡i-,Øð¥“‘T°õ|^dùkÛÙné`÷$¡ß9µŽ[w´ìõGÜïYc@1ÒÄÿÑæ/ñIR$óùüwÕŽšžr ¥o€}|NI~¤¦¥XYÙ×Ì߆èÞ³4{âzE6ßtÜ÷µµý•Ul—ƒ®¼Ü¨œ=QZ þžSéXóAƒÉíE‚¼ü-‘Eÿqyê“æ/©ÎÞ›;®Ð€·¤h¶â ¶pϰXçm-êKJ.N|´e­tÁ\3ˆ‰ß2#ÇfnÀÌ6nåãŸG ÂÝ-¢cöÂC"»û&>]2éi\ûÆ>&\ùÅg·%n¡,Ã~ê`ÉÀT%P8îñ¥”°—³R¬AŸIk—¨Ù”÷‰Zwê×ô«Eÿ5p¯>;ƒ@NòëçÖ¯³Ç·å«÷=O“X†ö®5· fF eá㺲%ÿüéÄ JaH/—ï»húŸ.×YZ- ‘3‘ic’غÍÝfô·OØžX#“Êx⯸´*Êž±SEÖrXØÇ=7/ápñ› PÕvãÄë'»û“\ùÚ¯²ž#ÿ{ÖÎ|ÚÂÃ.´Gh~ôÂÎÎE¤KÉ_yE§It³_ÖSr°¤bPˆ/³Z2ÐÆúí¢ ¢Ò—<«˜wätåÿEÁôñÏ`´ùK|’ð…A.:õ[¢bYˆk OóÜÝäÝφ¶­•T@e—qq1Oe6NB±ä_ì'qó%À÷›FýÒ®¤>jê^ÿQN’—][,#:ó»ët÷^.ƒ\yŠô¼¹Góbõ„?ß•â‚Ãjó~>"©V}âlÚ¦xXÚì'¿±Ä)Ȧ¹dÅfÍ:S”AŠ­ úª"NÉ”žˆÖw÷;zYoý‚X³1)B`Q·öñ®ôödÓ©8øÜh Ù‹È&r€ØÓÏ~/e1©éÈ^Îý="Æðøaöò+%Jw_Ú p>5ßò‹¬-rEÏOe€°°þy‚õýŸâv¼¨9I#Ã>Ý*aQw¿u™LÒ„i†30@k*3Ø©‹„qßiY‹6>Ïœê‰Ï.ªSkÂ׿rØ/v¾ÛíyÔs»€¸bµ½«-Ȭt)iù:/žêÊÚrŸåõ,ç_ÜüžõÓlqêYÖk0ñ¶ÓE÷m¢’òö:ÓY¿ù>ÜÒÒg¸w=mA¤Z“-;º6ú-úNçÚÐin{™Ó*µ÷ï¦/~À737Ì~ +©Í/\u8ëZ9€ÙÖqø®“•¿9N•)œOK¦À]8Íë(÷s9 ŒÝ¹ØÄ+1-® ú®Û÷Õ=¤wâѹìðtŠí†Z–üM-ãÊýš˜2q)?F«Ô ÚrÙ¢}o«zBe¶—ÜKU8êVi" ñ¹'Ùõ°ÉMÌ©)[kY'sí®ý÷J8ÈÏ:Öл—àÄí7â×*í«(Ò”LY%rs?3ˆ¨:Ö–˜~ÕËy`mˆ¥³3›NæDªªû¸HU~Ek÷L©übÙ´{]ïv®2EóøBòãÊw¨Ëíå?טâPÂabñÝ`;úzêÏ<Ǽ16 )ÕDgéк§ê¤&‘ Æ,!Àøò—H’/ðÓ õ¿FåŽliv9=üaÞ„°¨Ám< ¸‚2ý½O¬lœ¤&æÿf-imØÞÌ–:½?ºûš§ãXÀ<[ʰ§sΓ¾®»F;¹fäÌ=õ"ßÉ>´¾€ÀD.ÖþZÅúcé[Óù}û¸t0Áá€6’ç·ÓgŸUÐ>Î3¼IÜ }Pˆ´± 4Å0Œ¬ãÄ+HUffÞ§¥=ÝH0ž_#©:¡ðÂŤÍ/¸ü{‰ÁkžŽºœ=|CrxEJòèÏ/^úsÜWG 4¾îÛJ˜Ôôoï˜ÙÃ×=íöKÞ‰,< ‘D €ÙÔ–¹•GÕ¨ðÅ@³zê_4TåiËS³À²!¬žb /‰¨¸$»D“÷T/¨-âzŠÕsö+\›œ®£1ŽšbõK±Àq¬¾è|â ÇöM˜ÏPmŠÕWà†xå…¿=EAJec¼˜Óá©Kïµwé-{ý2Of½¤‡yþ¤¡Ûæ\-Õ˜ñH¬N[‡ç9sŽeEŠ­fu2‘ˆ6ô6ͼžôņØé¿sÁ}Ý:™`ǽ-TÓ§ü’²%ÞPMGFy‘;ŸJPTÌþq9Y\&µð},±ül½4 u!)ò6!ìP¢Î£6YËó¶"jd¸ ¶Ë Ú5Àb`ï$|Ðnz%ü»º•–,ù)vø/Ç_¶5w3âתÃMIP«˜7fü0 [‹þu‰ØÇ¥¹ à"“ Cœm&/z¢{;= sm_÷·¯MñšQ_(@ÑOµhR Æ‰Ñæ/‘$ŸÏ¨tZŒÑE¦ëCš9GÆfïè÷‚QiôWnÞŸØE~"Íüß®&§Ó1Çéµt©†€‹û5yÕSŠÅµÎMe½Sçÿ¦55´™ãÄçÿNpš„´©çÊ5ØsÖÏÓ½‡3q5€Õý´7eO 6Oqð£U?÷íºvŠUÖ F¬É›vC££ˆ3Éìj?‹˜â±IOGæÆuÚÀÓ²À˜R MGë*毭å}mt»¶æýVÌh¿0;ÑźÎÍçeDzL¹š.ãà~”ŠßM^WP~Ÿâ7o(Ìy’ž]³ÒPèO½þ€a€ަ9 zR‹ Å4ÍqÀŠq>G—–iI%Gz±lÅ{pV$dÅzÒ[*Í-J+°âÇŠÄLJ¯oP{¯tWîÛ\RŠ‹|gzöí/"?UÍ«žÀˆvC¶{ù?ulÒ¸7ì©. Ý—M¤°­ü=šÈðƒe/ŸY‘ ¦?‘¬N+çÒ ÕžðÄ\Ü™ÄùO)°çVíý¤V„Ú¾¹œ÷(iK´Æ÷ ç|€WG¾º¡Lš”¾èFiäa=Û˜TËÁB)ÆXUaXN«a@@Jx¸L]åÚÖ@ë— p Îè^F[Fa‚ªÆM@`b)ÑŒ®ÒÝ8–%,x ^MTm·j)œp’ªÜ²‡y: èR2Kÿ6&÷´m+TÿœørûR:c’W°žG¥N¾¯£áÀþîMRÒ¾½«ÕqoŸ,y)5Á@•±<ïSƒkëŠb7ç¢ý ~@ þ/Œ6‰äñA —‰ÓYI°+ š¶ÞNOriƒ^qýþîÐN´2ñDšQ¬#Ç*)`™ÊA#žÅ¬b¨–{Ùer]l9t²äñò8ÎPÙ’såZŽàcN¶ê`ï•’>,’iÜÄîëÆv“—ϸ§}\ª(o")yà"¯«-Þþþ\#¡¹X¦×ƿ좕…j…ÀÔU„=}ã=%é…O —ŽDT±Y7™þ×8} ˈ¥XîS'±ìK¡ÂG8f`8†f9Žå(#h†3àÀQ gÀ½m¾–z¢Óƒ“bQ“î7Ÿ?¶±#qàuƒ“ [hs6 KyÚÆ«‘õgÕu1yÇËÀ‚Vëôvo¼Xñ]€cJ(p}#©D_P©ñ˜9¡n‡Ä²É%çãTŠ7Ÿà4j†%p’Ç÷¶&ì¼ê\ô¯\+àaɦ‚:r,ç¾VU|ŽƒWvÅqìÕ þ|ô1ìÕ]†}=®L`PƒáXîUÓGà€}´ÝþôÙ£Æh&·÷:bSz;Uy;Zq·ˆ©Ù1_&_ÐÓ$õjüéW[_PšÝ¿Ä_¶”öèäÖ=uÄe¬©#¯–³ç…f/ÿƱÎEË´ÞûJR’KS !r¨åÝÆ[ðc®­ŸFúøÿ%_þŸ$I’äñˆý¡mƒžêÜ&(Öƒ½Rž1üѱ€‡qiWBŸ¾^rM3ä ¦À²\µO]×SÀ¥< €À"œÓë44«ÐsNB`p’r¬BÏ*Õ,)Ä+%XI)5G;hT <¿\Y›ÓOV[ »½Qo,·ÒX°Ô|âÂ^¢e#6yuqim‹:Nô늩u·¶S>ëERœbfj­BEYë´E¯‰#ÃpŸhLšþG-#qwYî«;U–¢#üÜùLqq~Õõ¦ÞV¨†¹­ìùs¬V'6°´|”¾÷‰ZÔÓeZ^Î- ?ÀßΟÇ3zïÂy„˜OpÀy„‰PéƒA}:žÝØÁ±‡º C*›Rük¹‚¥=,çõqú&)3B%ü¢£©.1%ZÇiŠÚ;Li¡ÚšÌÖkaïY^¼úEÍ̾ÑG”ØŒèdq[Å÷püÂL»'NOîÕ¡î® Ã²u‰TUÙL$ÄMøŽa"!!åXµáI*B8 ŸƒEbá•,'3óÐ)5B€ýѯ(¾tÊÈZ- ²?£¬lÅVÀЙ%äðþvâÔâ;yaa6´£¸4.'á= I[û ]ÈK¢•DÝ&Ží°²Å ÿÁ=—‘~@ þQ Æ—¿ô K 5èÊ•1ãQ<€¾þbuizL)°¼P ¢_ŒA?¥ÞsI±¶‡ûOôý³Ï3?BqOؽ§g¨+Í)\| 7‘ªºàtåëöe0Ýí×Ot$9*þYÖÄ‹%zA÷¢øÖªöã’Ü÷mŸSñ/£;|8UÚËiÑ(;K=}˜>åšZ÷‡l'N§ŽÈâ‚ðÂûª8ïc²¼rÍÅÜÕÓ*6Õ)ØäŽkÝ¿îÅ“ ² ÉÈ¿81ç&ƒ[÷´8U,À¼e–²'Wçí,I³>Û,­ù¯ªÅ•þZ’è*ûÆ—‡c€»Xt©;;"÷.ÙM±ñµùtõÇÿÑ’ Ååé&ö³¿r§È*Zq¢(­z:Aõ<{ü16´Ó¦f°Tr’bW1S•¾ÄÎ}Fg÷®zíµû…‰örãw.¯u ä^¿Q'vƬËa_JÙÖÓuÒ0o1cxt/mI´PÄe̵vÙÛëK‚ÍJÌ™u¡¬J œ,ìâÖ S¯=œ_S„iÍž#ò>ë¾æ±juø©Ô#E¼Ì¿¬Ún˜Øbýt7_X<¹!d§÷ØU\òªX–ŽÉ⦴õ$Å@¯»ù[ê–ÌWáWÊ^f`f渮ö«ç­dò†¬'ÙÜ7þÎ½Í KÅ>͘ñvžÒ›¿€RõLïÒU6Š„²üâû2¯(ÿU0ïE³ˆùžŠÎ;#S"þYþ‘i‡ˆˆˆ-ÉžÿlÅÌD¼æžfuì%¼?,*¤.!W™TV¦}ïôöú£3þÒÇMýrõg±7áßÏw9¤+ÿ;Ò‡´¶ß7Êìè¶„“U.Wà›,œRÛæì³ q·ñ_úÝ·Vb¦ÇÓ5¬?)Õq¾µìâòõèI¯+)/ÿE¹C¦@ ˆ÷¹ ñÿ)EDD,M’ýFó#Åhó—Ê´ôù§ŠóOè7úxíÆrÛüÜÛÌå‘>.77›VBá¢ÍM1q±nK*—>§kä`”€‹ùrš*ñÑÚ<0^~@µ„ãË_ú›|®ù„%"–ö«Ç‹(+úC¼‹Í¦ v©Këo]N?ô‘›`d³¦fšöD[C­ÅÃ…$Š’ßcd@úø«íùq5æ÷cO:ýÍ2ôå‹W?ªò¶¼hô’¢¿VG]ÚÿøR 5·LZAˆæª‚Ïà ƒL Ê‚F ¤ˆÇh󗈿O÷ºÂ I%fææR‘YãÊÕºEiH]ax Z‚@ H? QB@Ë_B @(1éT»üb\AŠã]d"èí#JL~@ ¤ˆM; j0áQŽšXmfn!A𻔨雉¥á ‘)éâ/ðOå/E.hŒŒ‰@T#\åÐÈE:¹“#2@ ý€@üe ( QQë [ÎFŸK`šêºÑL̶°W5±Qÿó[%q`ogßÀ·‡z(@úøhPþ¢³ålô…d^ ·½£\\M¿B^‰6*¥È ÏnbUúÏ–LDvn64n„&éâ£Aû/!j0ç˜o{‘HUBWÛ/Az9YÞOTÿãúaƒÁ›—‹ô@ ý€@üe 5/éö×Þßí_Qhp[saf5fA±ŽÀñ>‹ã8À› ÒÄ_M; j6ÍQtõ‡ÃA|’Sð0 @úø+mþ’Z«~ðèvÊó$–eÞy Ç WÏÆ ›o¾Zô[|ñò~îþîæè§DT‰áô4[½¿ À#P?‚@ H? F#!Àøò—¢žÜÓét~Þve&§(oÄ–­ë9š ¹-ç“ö~ðýñèW7Ì[~<¶s¶kïï—§,«"Çw[n½ùÄ¢:k¾7éÌÎŽæÿÍqfŠáôÕ|þÁÀüËó\éåq7ÕÙ}|ŠÏ§>Èû•ë6¡Æ@üÇÁ‘ Ɖ¥¥¥qV,;/ÃÑÖÕÞÞÁÎÎþËTfûS”i»º,!À R€Qÿb=¹ÒK_µ ^› ÿô…pÚçW6NÒ®y_`Ȫh¨n^|\0vö²¥»Ø“ÈŸ«ÔúS_¬ònïƒ_Ù¿uÍÞ¨ÐqúòG1›[ûÊþàè€+Ç®—8ÍÔñq››ixjÿ§‰ezš3М¾ø×S»ÅäRU~D…~ø`U-€à”wBÛùµ}Kù·¥µw`ë/åÐ5Ó¿8ª4fçˆvÁai†—·Xeì¾ÙƒZùµ ·ã¾¢bö‹-¹6µgë ¿Àö!³ö?SVÜ¥ò®­Ù£•_`ó¶Ã–œLÕÖÜeXœ.=|ÑðnMƒü»Yu%÷­vÕvV›ueq‡ÖSn©þ[m[öpmÿ ¿þ{žS ‹þ¡H`{ IDAT‡_`Ðëkð‘†Î¿¶eò°>-ƒ‚ü‚º Zt2AýϯXUÜÅãútmãÐã›Å'Õ[rçÜa}»øùuº÷I £Í? Œ£Í_"žH(æóùïVXMO9Ò7À>>§¤”Ñ$&'ÿ8¾Å"Î:>ùë0]§Q³×בaz# @&¤¼†NÖÓQ¼_?|†ü%ÎsU‹ée/ƒ?½òÊØèbš)Ë=<üYz«ú“×›(OGÿòÍÛ[þ2ýƒï¢3:7[7“¼|ó—}6sÆI]ÙåÕ%ÞKë[ଭÁà“Í?ÐL½*æìCÚÒ”Ž Q·l&E^ô [†ÞŸÅ8½rÕà 3ÖÇ6³y¾GÉ¥UKç,t;º©³EÉÕe¡{ z.ØÖÓ*óز¦®÷<=?@”sjæüpbè’=­Ä1{-Ÿ¹ËûàoAM4•>9lúªë.6îlHÝÛ6oás/ýÔ^uqã‚Íá1¥„M]/½¬_öD†üÛ›&ü´/ªPä¼`õ´Ž6<  oý¼jý±»ÏÕB+Ïfƒ¦†/›xÊsåéå­-q:ëè¸~?Û¬<¼¤­eµ×%ØO?ª+¬-q}iîò³I Œ|P7B÷8ý±ÒrÀ|ÛÚr jyÅžxxã7U£nšätÌiŠ€äcµš’çî•éÆòôãïš¹Nmʱœ¡ª¯ðéôÃ0”Ñg£ð6Ó§”¯\{6VÕ¬ieÖ=Þ?gÞþÏõV_-üþk?3˜¢{?._wàn¶–´i2qÑ·íHÍýÙ½§ëLJ:Fl9‘íÝ@™NÁÆ~,½¾£•æ·°%›ŽßÏÕóm› š±`Bs9Á鞟¯Òu~oDË=8¬ÿáW÷4q'n¨ýgM ñ·ÄÁuÆý‹ãŽßW´|xò!¯ýš‰]J Ã¤kKN>›êç|3<^²d›:|¨;uÔÙÝgR¾ö®+¬ò!íÂ…¼Z£7 hî@€çÔçFœ¼–Û¸3ùçvëØÍʼÝöw4tžZ^eÉlÉï?-]ýËl-ÏÔ©A·ÐÅß¶’WÿÑCæÉy3N»ÎXÝüç±^߯MœëÕ¯oùFtß(t³_e ¢ðÑ¥1c‹O¶*¿ú®nð굕åïaˆøuIj1žµF­_SY€¯EÊå¯.GåQÍLø(02ÐØ ÂH1Úü%’ä ü´Bý¯Q¹#[Ú‡]N˜7!,jp„® LïÁ“ÍcZuiâúoÖRP{ü¶™>„¬ïæSW/žß,ÇNØ}–íöÝš™©ˆeƒ‡­K÷›°zÑ@»g?¯¼Ç°ÚÇGï™uµjÉ䯅G¬ˆ(¯õV!!¶§æ#ço>ðË®“}“Z¸=Nê¸3 æŠý#»´ô êÔgúž¨RVÒtáOƒl ‰‡Ï¿¶©£àÙ–±K¯™ô]´}Ûê©míHx4ÓqÛödû]³rjë²ÓK7E©À¼gÊ”%Ígn?¸ëû‰AXBŽýȹ}D××…=UÓ…Wø1³ÙŒ)­-kBÃe Y=õ9/Mñµï‹¤Ã\¼-õÅ1J½\"“(VOñy.NPð{™†£8 8Ö@±šã£(µâê:¥ÏL™)þ¾b€Gð>ÅUEú’*.üwèØ¢G]ÃsÑ/“F(EÄþXÇAó¿_6Ò=açœÕ¿«8*cß´4–í>ppõâˆã~Œ×°ê;kv¤¹ô7vÈÌM£ ×o~¾tñüµµ­x±;Æ-½ï:fÛ¯goú‚81oÉ…BFýä}®[ý ‹“Sµ–Þ.R@àXߎÍz–«~“ÅØÖs`bçzÖúôØ"uγ<°¯gÇ Ìjy™–Ħ©jbþ§ÎŒ/¹yZ¤m}g"÷i¶þƒv3|°dÕý5óv+Ú.Û{ðð–Ù<´y*¦Ú[‹)¼¼bJ1zûÌ–ò· ˜èÅšùµë7~ë­4À›ë—8J­4&bÜðç~Ũsï=œbÝéKoÑ[  ]™“È$ Œ4ÿ€0RŒ6‰$ù|¾@¥ÓbŒ.2]ÒÌ926sxG︌J£¿róþÄ.òvœÿíj"1‰Bs sèóxõæl\Ð]†Óµ3Žž?ÙjÕ÷£<ø „³‡Åäëû™à’6˶ΠpøQ‘sÏöðUûSoÞßw—á@»gŸŽ8óB)þ=8î›y¯O]fívå©ïäcíä5c؃f*×|¦@©<"õV¡ù#øÀéÕ…413ÐΈDœî…ÞÀ»Õâ"n–ª›‰w(˾„òBêC¹ý8û¢“}Ó~cÄ~vý69Õ~=Èøi÷oýƒ€PÇŸ»Ï5^ê#µÐw¯§[ÿk¼º™¿Hy¿5+Ç×´ñÔÝé»á\R™Yä‘—1GG¶uä¸ÌŸr§Çº£1_ÏÀM—ÚÐΠ³œ'6377çòÖÞ³t𦩽øýÆô<:þltžäìû\·úÁª‹ÕœÀDXaU\d&µBM©‹4œ@*¨¸KˆÍD .Rë•¥:žTTbs!¨jä5nđռi Df:»TÏ‚ÿS»}P 0ÊŒLµÔ»Y€—«\=¶®þRK³cꪼmz8’LÆ]¢C·YëۘʤPøøÄšÍßM5?´wðë :'b÷C¢Ù‚Ffõ~¿b ÏŽé¼,L¦nÚÌìMGc o¸¢ª3®• ÒH? +!Àøò—H_ z™H0•»’ iëíô$—6èµ×ïïíD+ÒœO`€ LÆÃ+ûK)ɾJ£y9àƒ‰êYs³Ké&oõ)¿n\²=<¦ø&f˜’v£V[¦ÅúëÛÖ™à5gRdç%gŸ)Û¸¼“ ã2h‡þµ«J\}Y%ÀÅ–bœÖêKÓ’”²Æ ÞÛÂ0óæ§µúrÁ–ÜFstª1}Å~–ü¥Jtš›kJ$C}êÈ Ç²ËAEhšŠÅÀg©cÂÄø%û9Ó–zén~©õùAªX÷Hî2ý‘$éëØ£[MBçIÈÏ¢ÞÝI{öÝhn})†K÷òÖ®;¯öoüÖ[q³ZµM5)ù¥¹ E’Ú>²Š7u¯+ÓNÓ¸öÛ_”cçñ«Ô>à8ÃãÜÜÍê•"ùE{Øu« ¸ÄR:¥¾rÓ%]¹“¸ˆI‰\ ™J= @0ºr&‘‰&æJ¥­dgôå:Lb)®‰£¿¸ØRú—VF«4¦æüCvû°-H·!ÛÎÔ¹|öú½û7œÜsfì]£kUçèpYÇ•‡ê*ìÀ䜙5åF‹ ë†ú¾µªž4·‘âñeÚ 2džœ?íþWa‹»Øñ€ü¿"L¼|¼¼Ì’.Ž<~-·ß€.¸¶tÒ–Â^ßïà†V>G  Œ“ŠÌ%0¾…ãXðu³:<¯×¹k‘EYÉW#£N- n\Û†ãXÀHVâ°ÿß?lYÒ£±§§%ïB´óL‚†7°âclÅÐ9iíã.(xø¤°b…2;[-qr2y£aáYºË!/&Gûqnââ*.z[̼V°e÷¶üp»ÖȱM²\q&»¦l·Y±~ú³\åíu*ñ ™—UŇR">¿PýBi`8£Sgdc ÞüuùÍ­zŸP±” Xp’b8GpÀÑ ûêm¯æ>ïʇ„ówô¾Ówürdÿ/GöÿrpãWîÊ»gµo½‰.ŽWZù:[:x[©“b‹*|…-O‰QˆÝ\ÍðwE+,ÍðäÞŽDnt>ßR&—W\–&ùÇ»n5,”yÖ+b3Ô€>+:s¨k/±ñu"ò¢stœ:#®@àâc%q¨oÇeÇTdù3¥)IJ3/7“š.`+ujbE“CåGg³v¾ö‚ÚíÃÁ,ËrBÿžcf.ßuhkˆEú'Šj¾€„0±sqwsswsswsu– p…““\ˆ³¯‡AØò¤˜"ž½»%Àyzþø e_l_?¬®¸âaæدhu©–Å   o¬·<®ÕŠŸ&˜£XÕ›d„1K0¾ü%+¹m^A®½#Çó÷´=³¸Ï™;)Ã:¶w³5£i:¯ ×Jnk£ÐÚU¢¸sñN XRüÉ?`ÕI—ÎEò½E…·w¯{d¼¿®ƒ×…¨q™òò¾ã¿qu˜Øð̤¼@Ú`Xë‘;–í°ÐŠ|òóÖxûài¾bÈy­ ,ü{újV~ÿ}±mÍòïžØsÏ@ôz_Ä>ƒzØÛ¼p«ùÄ®Îtêo§nÚ›i»oÉ%Ù×û†ÆD7m_ÑrS÷šÄD³•ñ÷'Gy±àA±¨ûWÎQÊŒWOZGšwùûbÑPRu¦(Z'ìÑ‚ã^¾ À•…Ä;™ôe‚³óÂÇh ÝIE4-ªOðñWÕ¦?ßúmÂù;Ÿ‰ê{Tæà» ƒÝ÷ì:—8¶)0Ÿkpãe]ûqËsÏ +ܤv=û×>¶|—óäŽv¥‘Û7>µêñS=<{+2s¶%2¯]¾×8PHË ï&°4t«v\‡Z²ä;ç/õ^>êã]×È` jµF¥e8Ö V*Õ& _äýEÉØ-[O[|ᦸ¸ú’®ñì¦rž¹ß&ô¢ Û›Oï&Ï8²é¨åÚúRž¤uoŸm;VïõÒ\ðlÏÏ Ö]æxÔÀÍ—@àÞµ»ý‘Ÿ6sûÊWgë¡|÷oÚ;`HØ2pÐù’³Û{XVe7€£5*­Rcà8Z£R* ‘TÄ{í²TƾÙ;JZõíÒÀ2îÄ©Í<ÝMkf ¬}²|ün²]6Þ¦ÅüðXÚeSc3®øÚâ1Ë›ÎYМÌIŠ\ wqµ«Â¯øŠ«sgýæÚ­}#aitø¶p¥÷ÔÖ¶„úɪoæ]w¿¢«EARBFš9»Ù‰‘@úøsŒ6ÉÑÖéEa^bj,ÇqP_±º4=¦0 37µ°·r0ŠŠ ½¿žÜùɪ¹ÃŽš6›=ßõc6}FøÊ «ŠX‹º!Ë7~ã-€7 ™·yñˆô…?΃Ù5ë×§ýOqâÖ/Ô/Ý8{ôNLÖ ïâíc¼EðæL¸M×Ëç®X?ï¬Ä­cÿ>uâ}DõÆmúVnX6fÌ<ÚŒ˜’¶üWÁ€°þ®$ !sžºaãoÍ–µ«þ[0áöYRc´º{ëµ’Num^'ƒ©´óyø,žS,a'j¶Ù¶Ý¯ªµwv0õ6™™ó0Â}–•ëäÂÝ_`4ߤû&±àuqüžÿð¶|H:{[å5¾ñëŸgÛ¢³ËºƒS€úé¾…G²T„•ßï t&\†¬ý^µl휯öèyVú,Ú6ÎGš·Ë7i:~\óÐM“Fî”wXwbÙ”+%+·¯s@„¥W‹ž£…6õ?Þu }üæC<Óq¿¬ÿ®ã³|¤'ÿ0yù²uΫIÿË—u±Ædíæ®NY²l鸃”ȹåØõÓ›šaÁ«gÍ]7gøNÖ¤V§ïÖŒ®[SÝæ{|ýì‚EÛ&Œ,Ç-ê†,\9È…0ËÇr€Um7®ìÚ„.‹žÀìàÎà»ðJXç×{ ² «ÃfüRDÄ¥í¨Sˆkhià͆Xq¢”"äõzÏßêo‚Ñé)elþ¥eã/½|Ÿõà=GBëüѯp½[€ë©#ÛíVhAâÒ|Ìúï9“œ"7>Ÿ-Ëß2áΫΡݎsËj¨«#˜÷¢ˆ¿YÄ|OEçΑ)ŸHBü!1pà@ãúVÇþbÈØïódMhîÏî=V\\é/Až÷é©;ç¡o-»¸|}µþVR^þ‹‚ãÃ>‰Ëääæth×¹ @ü ÿ_€±4Iö·”#2=ÂÈŸ 㜅@ þ&>.æWï<,ÿS®ŸÆÐQ³a¤ ý€0RŒ6éïÒC?.<\HVï,,‰Ã§Ë_B ÒÄ_ÂhÏ«¡ˆ›®¼| ™ás!VgVçù>3 ÖRâSè­N+‘Ÿ ÒÄ_–€ò—5‘~$'bJÌÌÍ¥"A5ý åj]aqÙP¿~=£V§U(¶¶¶ÈOéâ/€¦5˜¯;zp|üqAжÚ>¡"®««ÁCX”šþ—Lð{;{WWä'ôñ@ùKˆŒX@Nêá3©²@ ªè(„QK@ùK@ Æš@)hÚQƒIÎW/8– Õé{º—Y‹id·àÀÞξoõPôñÑ ü%D fû…$‘‰¥ÇÜÉ1t´ÏGy‚ ²s³ q£ÆÈa„ ü%„QK@ùKˆšHZ9Ï^&ÕrÂt9²Æ;0 c0rór‘)Â8Aó#¥¦N;ƒ¿vªn?@çÍÕÄH'x¡çHGã8ïÂq ã§éâ/ò—ÿ‘8ß\%†@ ý€@üu Æ—¿¤Öª<ºò<‰e™w^ÂqÂÃÕ³qÃæ›¯ý_¼¼Ÿ»¿;ÊNA|¨&P;Œ@ ¤ˆ¿ÑN;D=¹§Óéü¼ÿ8lÌ0LNQÞˆ-Z×s4r[Î'íý6ý”ˆ÷ƒašx¿iŒ*%lÐà;CÂwõ¶Aéfâ?jFJEæR…0ªŠeçe8ÚºÚÛ;ØÙÙ¿s™ÊlŠ2mWׂ%8A 0ê_¬'Wzé«–Ákô»$C^ä®Y#zùµÿîŽúŸ¨[|oçÜa}»ùµî;f]DÆû«É©ã,×§Kk¿À  )›nVlwjȽ±aò€6AA~Oß÷¤Œ­Žn^™¿ô¹e•ëV-X¼båg)Xœ œÊ¾uhÍò Wl?òPÁV¾±ü÷Ý›÷Çj1â_ûã¶ôé±Ußôíàä׺ψŇ3¨©|ŸkQ¥1;G´ K3¼²Ÿ2vßìA-ƒüZ…ŒÛq_QñȰ%÷¦öläØ>dÖþgÊŠ»TÞµõ#{´ò lÞvØ’“©Úš» ‹Ó¥‡/Þ­I`÷1«®äþ±Õ®ÚnÀj³®,îÐzÊ-ÕÄ©Ń]¡ƒ»7 ê;uKdá[;O3…W÷ò üæW ê{3ƒü^_ã®”°Êèˇôlëت˘u²Þj÷Ù²‡kûùõßóœBÏo5Í? ŒZB€ñå/O$óùüwk«¦§Hé`ŸSRÊh““ߢt—]þ<ð«o×M¶áé17Á?Q*Uô4žmøeèØÚæºøð5›—ÌuòÝÛϾÊqx¶<)ªÈ©ç¤¡¾ríÓ#k·Í^ìvrSO“¤“çvµ(,P’vzåªéó­oè,¯n#"ŸuþQü~èØc¼U¿ñÞüÌßNœ9xËiRGÛÒG§oj[|;·©þÆÆq¾ÃýL@÷<ò–¶Á°zfä¿75B3o‹Á©m3ê°¡õס[ˆ¢èðŸ·þ:gûÞiMMÑ@Ø»š?aËÐÁû³8Ç ×|¸aÆúئs6Ï÷(¹´j霅nG7u¶(¹º,tOAÏÛzZe[öÃÔõž§çˆrNÍœN ]²§•8fï¢å3wyœà-¨‰¦Ò'‡M_uÝeÂÆ ©{Ûæ-œçè6ĉ÷¦ãUe7Vzm|ð¼û:ü¿2ÉLe›ºë?gÇ<«—7-þn¶ÅÁƒ+lÅ–=Ø2eé}êMÕ‘õ§­_O¸‰›„-¼¼xüæÜγÖÌqWß[6oªÐíÀø:N“°{úüs*¤èù­ fa¤Û´Ã+ø$)à ‚\úk~‡5±÷ÒÔ<Y d&ì|6 ¹sz —]ÆÝyµrhã/›¯SLX;tÑSªàÀˆ¶~A_î»ùCÏVƒ~>¼dXG¿À¶! Ï?Ï»³i|Ï&­»LÜ­â4÷gwl>îçs‡¶ òë<êû/hÐÆþ´éI“e[— ïÔ¨a@`«ŠÎ¢ôÑOÓ¿läצϗ!­8ó‚ ^\ßüm׿A~zîå×rÊ-WzylÓîKŽì[øe» ¿æ}&î‹WsÏ1kVNëß!ȯI»c‡ÔÂ^¤—Pï©a×kÝæ9#»ù´ÿjÚˆÚTÒÝL½.ùLxŽû˜™CÛÔ«ãßkò¢`ÑÝ÷ ªßÄ眀òÄǹ&!ê¹¹x¶ìÙÞ¾ôiT!Ë–d”Jܚ۱̈́ȊA`&÷ذ¦•C˜TÞ­ãBÚúOÚYÄ“{lXÀ;S+8µævl?ùžXeü¡…#Ú5 ò ì2`ÑÉ$ liT¥Ÿw¾ìZAõ9ç_çÛ#Q‘ǧ×zCÿiâNÜPû;1Äß»a§Ñ3:ñ¿¯ Kœ|Èk?eb—†u÷œ2©‰æúÉg*:ïfx¼<仑mêûôŸ:Ê+ïâ™]ìhôi.äÕ: yݺm†Mh“tòÚÛSUÚ̼Ýöwnoé yÆfK~ÿqڗ̓üZté=aÓÍ"¶š›ŠÊ¾|:NÖwöè¼t7”}‘ ™à4ñ¿Ìœó°ùÊíßÂá™×ò­ß°~ý†õë7¬çf†)wÚ«±O½V£æOòÍ?{8A `È<9oÆi׫û9¡lN¤ˆ¿…Ñæ/‘$_ à§êÊÙÒ>ìrzøÃ¼ aQƒÛx$peú{žlÓªK׳–‚Úã·Íô!d}7ŸºzñüÎ`9tÂî³l·ïÖÌìHE,>zϬë¬UK&7.<²`ED!«Kø­Ä„»0£KË ÆmBÆl¾õ‚`òNÏ ÝY8{sØ®•£‚d >•²/tÆ)®Çâ­{·/âk‚½Ìx ·Ü4í;÷û¥ƒ­n]q<ëuÆÑÊô›GNæ»ôìá.|Oà8’Ñ•ë@"—Œ¦DÍ ÍD-˜Ð¹‘dÇäê«¥«óÞg¹ ,³d®VàñMíEšìBŠ p p‚àñ Ç êùo7 þÝ}ÍÉÏT±ª¯·åSðàrŠ0ph›Wánî7°“<ëÊÍœ÷d;hbvŒ[zßu̶_ÏÞôqbÞ’ …ï‰á˜gçOÝ¥î¸âð©³{CÄmœöS¼ŽÎ=9;4,»áÔ5[~\0ÀWZ½÷ƒ¢‹“Sµ–Þ.R@àXߎÍz–«~“ÅØÖs`bçzÖúôØ"uγ<°¯gÇ Ìjy™–Ħ©ØØÏpêÌøB‘›§%@ÚÖw&rŸfë?h7ÃKVÝ_3o·¢í²½o™=ÀC›§ªî¹v¬úÍF—´®WK£CÆéù“Âç®Óøí™@N{sB» ¿ Î_ÌÚó°”V[¢æ¦¢ŠÉÜÔ£ž¥2ÌÁR'›IDAT)µœe /¯˜FŒÞ>³¥©‡jÔs! ŒYB€ñå/‘$ŸÏ¨tZŒÑE¦ëCš9GÆfïè÷‚QiôWnÞŸØEÞ®‘ó¿]MBd"&qBhbna.}>¯Þœ ºËpºvÆÑó'[­ú~””pöТ˜|}?s\ÒfÙÖ™®?*rî™§eúÄ"ÚÀÕî¿|°pzÕ³Cå‡~jzé—'6£NìåJxI£÷º ‹?t,Ëwʺq¬qü·íç +[Ë>[6…64\?6ãA¦n¸³¨ôÝýþô¬;ÎÛ:ÖG ©ªåÝ:˜Wnœòñ‘3Ùݺ¹ó…EMja~9ô{“ošXêrcŒÁ\_=s´?[þ’^MáB!Yñy¤È„Ï¥«(~­Ú2íãôrÖN“œÅZ÷°PÞß“êÜuÿÙñ5¿F2f^mã(üìÁóÛó”"­lÚ: ß| ßÖÛÂó©úU ŒÚ{–Þ4µ³À¡ß˜žGÇŸQ6ªrd5óüžèÚ¡ÇZâÆŒ:¾ýFJþ¾§Ö£öÍø² àk¿ïèjý«9I剋ÌD V¨)u‘†Hw ±™ÔEj½²TÇ“VÆy@ˆÍ… V¨׸GVó¦U€™ èìR= RüOíöA-À(32ÕRïf^®põlغúÛŠthäÉ?vjß­6¡­í؂Ę|=C¨‹î¬´“¿cf[9¡/xcË}ÀŠ5d–"CîÝ_Ö„M\`{fC+ߺ¦E—ö_žÓËWœ›¡bôê²g;–¬Ê Ù¶¡‡#Éd ¸éâob´û/‘<¾@ ô2‘`:+ v%AÓÖÛéI.mÐk#®ßßÚ‰V&¥E1ŸÀ˜Œ‡Wö—R’50•÷Ëo0±S=kîbv±Š¯Ãå-† ìPOàãò]Ô îgÛGçKê4´!ßê‡ËÒ”²€:æŒ00‚$+îâbK¤Q•ŸÆsì³þ—ÆÙ W¶®õ¹wugiUu(¥Áœ` ®þ0ç‚éðmüø€ÙõX2óYèê)]~Ë:µu 2UÏâÏ¥ðЉ¢âóXŒãÃÒ&hp—ç{7->Ɇ ózqi58Þ%óøª÷Ñ ¦›Ü[³æØÝS;X}îÁªö_ªâæ ZºÊqª &M™{ktëã8Çê ”—¢êuÀúœ§Yê¨ùÁíc¥7HlK³K^ˆ½ýìÉšÓªbñÒªÃr¯îòðʻ쫻N¼z”†«É§Xbø+«K³ÜÇÚíÏŸjyÓNžk¶ŽNnÙº©ë®Ýš»ˆ«¹úÂ-ZÍXÒgÖ¢Ù_žœ%¼Eª˜G9E9K¿h½ôåûv÷Àd¯Æí¬ÀÇ»÷¸ó¢ó1ª.m¾]<"gá’Á–œë˜k@fʦ\M+IØ6°í¶—„…ôxþãéEþ"!ý€@üuŒöü8>I’$ÉãûCÛ/<Õ¹MPx´Æ^B_ºõðÈü^ Üd÷¢ÒŒ½»ÄþÈL±„b C³ˆä8–f¹î¸ùöG“æ.žæ.žµù/Ž;ö{IǶUÕÄØÒß·Ž_ÝlQØ_ ˜Ð=xɉnÓ^hø2ó¼­ý¿Š«kWýÖwböÙΚ˜´†Á FKÈ\"àI=:^Ú©"P|qauz­žýlUçò0ÇN.&b¢Ž·är²‚ëbû¹;‹·çH™»%\HÌÓƒëëxÞŸP&R*V’¼ë?á1þÇÍÝ^KRjªþ€{7ä—¶]µgšï«ù B€?ýŽÅˆ3âŽK,% Sê+7]Ò•ë1‰‹˜”ÈũԳDE~ &‘‰&æJ¥­dgôå:Lb)®‰©%¸ØRú—VF«4¦æüCvû°-H·!ÛÎÔ¹|öú½û7œÜsfì]£kUs-ʳí8kO›ñù帥LstÄe='÷/6žèX9õkHÙ=nAÚm+û¸½ùM1¡L.â²”—LÚqöë¢übÚÔ†w{Lðf77§Î+ùé*ÌË䜙5åF‹ ë†úÖÈÅú5ìáA&@³„ãË_„Ç€¯›Õáy½Î]‹,ÊJ¾ujQpãÚ6Ç B#yº1`éÿ+g™-KzT öô”Y¸{[jbïf8Ý‹ôRž­»µ•«¹:%ñÕÖ™„©£=¿(öùË$i>vÄ’Ó——ëàaU×Á’lÙÃãf]÷š³}a{ë7;nœonçhoA=;z¥Ì«k“j˜]ñ9×O ¬kÉ¡(£„%‚À4ùÙZ©³½”÷ú ¸6áÂoÐ2¸ž) #I‚ Ã?ÿúé·%*aåßÑC{÷—/^eް¥Eò<›ºIÁª•¶B ŢñäÞŽDnt>ßR&—W\–&‚’œ®LWQ KS,{kMJ’Þôå;ers±ØÊÕ\š\ò–ŸWãØOæYK¬ˆÍPsú¬è<Ì¡®½ÄÆ×‰È‹ÎÑpꌸ‹•Ä¡¾—S‘åÏ”¦$)ͼÜLjb¸€I\|¬Ô©•­•ÍÚùÚ >h7þ‡Q–:ø÷3sù®C[C,Òosø’Ç—¾dê…s™æM‡;‹^8w#ü¹gÈ`Ö¶ÜÕ< VO¨ÈÖZÖ±~þCîÞÙ‰tžÕ?ü›ecgŒÐÔ‘(|þÓÖó¥–ÁÃ-„z¿Zø©½»NKx çÚ•N»`fM‡w“NXºU;®C-aYòó—Šz¯œ^»\qöÇÃú9)n »¢fZðÝz ñ=üÃŒ¥â©ýüåtγkg¹Î˜ß¥åÉ­Ë–|Y›Š¿¼{o.åUmœŠ5¨Õ•–áXƒZ©T›H$|‘÷m$c·l=mñ…›ââêKºÆ³›Êyæþ}›Ð‹6lo>½›<ãȦ¢–këKy’Ö½}¶íX½×wJsÁ³=?'Xw™ã!„šˆÀ½kwû#?m8æö•¯þÎÖCùîß´w Á°eà ò%g·÷°¬Ên8GkTZ¥ÆÀq´F¥T"©èA*cßì%­úvi` wâÔfžîÕ›aN—yÿa®@lx~kïÖ#†®k×zâÊoÌžtÚ±{¯@wAÞaéNæ×P/~¿÷œ²9ŽoÞÓlþªFbì ý€@ü£mþÒÿÚ¹×ØªÏŽãÏé…KÛÁÚŽk™Eìæ]ƒ„0ÂV©E„Õ ªâ¸ à.¥t×RnA\¹étŒˆJ‚fY½Ìx™qјgÆ6Ýt8X€…â &YðÿÿÃÒ²Ïç/ §=yÒÐóíóëéÓóÆƒ‡þõÒ+»0˜È á‹CrN=ðÂÑJ¥®ï’ß»[QZ<ÑNý§Î©x~Eí”v^S×7Î ¶Ó¯íi˜µâpkþÀÊúõ÷öïBèWµ¦þØ’µ‹füøìu7­~´zX—TjÀ앳߬{lÑüsÝ?UY9¦û–!¤ºŽx ~jí’Mÿ>«hĤñÃ_xü²¿c˜‘ßpÁ¯~²¡vãÑ3]KÊçytmºïùŒ©¼Ás·öÚ¸açŠéÍ!„r?=cå7ç¥Ry£¼ïÙù–=¸§Ç “'¯{öÂænmÈmØ´vúÇCfÁÇo;­O‡ŽÝ¦,®z©nKÝÜN%ŸŸò•Û^ÝBYEVo<·vÝ÷L_Ûr‹•ßsg~ÎͳWÎz³®qaõ¹îƒÆ}¡ü†ïþ½½|M~qÃÄÉ»‡«ÓËPx÷¶UÈñPÍŸÎdtí7êžo-¯žŸqÙ_ëwlÇ–;ŸëÐkè„åk¦ÝÒ)„pöõÝKç=ývȽqheý–™e=¼ßRû•꿨é*?DÝÍG***%i¨©©iâĉiõ”v…d/Ä&„¶YMœ|®fÜýaùÓ Cr“=°eã¤/ýáË{¶ëñ®ï%Í{ëïüÆ¡ÅO­™×Ï¡ý»~ß ’îÏ¿~êð‘·¶Vºª¿„7þùÆè²Ñ—þ·æ¿,;÷à}»7®ÐÙfWöÖ¦¦¦¥/^Íçuÿ@šJÛý¼Úò÷§ÛáÑ\NÇ–ÝšýÐÎ?ï6¢óÁÿ•.ñV-mÈ÷-Ò7©Ós¿t•Úê>váߟvÉdäß>ÿá»æ7,˜ýd¸~Ôº‘Ÿ)éœr*úÒôý—®Q9Ã~ñ»+y`vÉ´]ÏM{ïßv.­}æ·möڟ쌳§ZÎ?>¯ƒ~¸„æSÍþïû˜euU½mTµ“ø ØŽ’¦ ת[nÈ8ôÖñÓ§Nêí?áKÄÑ#Gzöìé(Ò“ûÒÔµº_‚ÂWï(^²{—ó§K»ž|å@«y·Ì¬ÌÞ½z÷-îë(ô$Nˆ`¿Äµè¦ž¹ßŸYêh\“¦ì—ôÄua¹$$ôÄMˆ`¿  ’kýqÙ/èH–Á~ @?@$×úâ²_Ð,!‚ý€~€H®ôÄe¿  YBû%ý‘\;èˆË~ @?@²„öKú"¹vЗý€~€d ì—ôDrí  .û%ýÉ"Ø/èˆäÚ@?@\öKú’%D°_Ðɵ€~€¸ì—ô$Kˆ`¿  ’kýqÙ/èH–Á~ @?@$×úâ²_Ð,!‚ý€~€H®ôÄe¿  YBû%ý‘\;èˆË~ @?@²„öKú"¹vЗý€~€d ì—ôDrí  .û%ýÉ"Ø/èˆäÚ@?@\öKú’%D°_Ðɵ€~€¸ì—ô$Kˆ`¿  ’kýqÙ/èH–Á~ @?@$×úâ²_Ð,!‚ý€~€H®ôÄe¿  YBû%ý‘\;¤¡,G@zº°_ºøç•}ŠŠ ×ï#÷¤uBû%ý‘ì—ôÄåý—ô$Kˆ`¿  ’kýqÙ/èH–Á~ @?@$×úâ²_Ð,!‚ý€~€H®ôÄe¿  YBû%ý‘\;èˆË~ @?@²„öKúh²Þ—ÒÔÔä(@?D[úr¡s€û%@?úЀ~ô ô ýè@?ú@?úÐÀé¿éEeì)‰°jIEND®B`‚PyMeasure-0.9.0/docs/tutorial/gui_sequencer_example_sequence.txt0000664000175000017500000000023614010032254025453 0ustar colincolin00000000000000- "Delay Time", "arange(0.25, 1, 0.25)" -- "Random Seed", "[1, 4, 8]" --- "Loop Iterations", "exp(linspace(1, 5, 3))" -- "Random Seed", "arange(10, 100, 10)" PyMeasure-0.9.0/docs/tutorial/pymeasure-managedwindow-running.png0000644000175000017500000023314213640137324025507 0ustar colincolin00000000000000‰PNG  IHDR`i¬’ pHYs  šœtIMEà$üS IDATxÚìÝu\éðïÌöRÒ%!ˆ(ÆÙÝb!6vwœ]çÙÝžÝgq6gÝ™?[±QQ@^¶æùý±K‡ŠwˆŸ÷ë^,3ÏÎÎÎ>û|æyæ®ÈÄ`ø' 2µw²Ž N$"RX:8ZH2žça¨²%î_>”ödŒ!<ÀeÓ¦MDÔ©S§Ï]1222$$Ä0ê§R¥JŒ±óçÏ?yòäÉ“'õë×·µµýnÂÏó8rp;hÿíD"2õño߬R ‘üöѵ§ÒLí|cxÈØüVx ,xõÄMOˆj÷PÅB¯Ñ½ß>wl2zPKCõÍ4o¯l™ñûƒ’y·ØÆGüâð²õ—ãI\ q¿ËK»Þ —¬eÄÉ­ÜŠViP·´ƒ\x{vù’“oÉħj‘ä›×žÅ‹¬}êÔÊwÿØÑ c˜UÑÍÊÚJôÆÅ”…*z«nßx‘ÈY¬PÍ)óKÐÇ…ž=ròòÈ$=ɬ<Ëøú×ö±Ä`&Èþ༉­­m?~üðáCžçu:Ý“'OˆÈÃÃÃÆÆæ[ŸˆAxȽß)‰¯<Ò‘I™¶œEÚ•–x³š{UÒk4Õ'WÖkÔj½ñ;DШ5:ч{4sLáG³2Ý7iÖüÎÂí÷ÔŸ •›ž¸O$ölPÖR¸ö",†YçwWê¢_¼‰yvõM‚õà¦.œ¡³#éî¹+  Ñw¯¼K$‘ÅÜ9r¸páNÅÆÅ’Cÿ÷ÒÍÞJ’ø6îÑ©'ÝûW̰ÁªÇ‡Vo»–ÈYy•*,}sóÎã »¶Júö©…k!2«Y³¦B¡¸}ûöƒ /^¼|ùòæ[?uN^ó€g€¥MxýŽˆˆw*bË«#Ï®]yâµ–ˆˆ¤®úv.•^“±ç!Ë”é¼~æ:úý[Ï8½ (ܤY‘Ç;ï%]ß±™ˆH\°i‹ŸLA_$pLYks‘ NŽ{ztÙ†+‰ÉÏŸÅió‰iùžÃ[¸¼þ}ÖÊ+I¤,ÕuDÏèƒs–œ‹ÓG¿ŠÓ“ŸÂªVïAõèÕ¡¹KÎÅ%Þ»QÖ2-ëÄÞ>y-‘ȾN@£¦\¢Môªà7ooÜŠªjç€ôyƶmÛ2Î3´yófÃ:tÈ~!jµº\¹r/_¾Œ%"++«²e˪Õêaû1U+@®Åˆ36¬u‚ÀA«Mý‹F#dL©á²d–ù‘LeË0l‰éR’Õz½¼H“f…ﺯ1D‡æÅ•z‘ ~}~ÿæË"’ÓŸU/0fìr–*%ÚdÌÆ”(‰¤&2]rŠÄÚ”(ŽôºŒ›Ê´*•JlênË‹c‰ÑIzË´¤óô-QĉU O¤m¦*V…oÈ%¶nÝúÁÇÛ·oÿ5ûGª´ÏªêA8{ö¬!9QLLÌ©S§*W®ü/ŒBxȵx3K¢boô¨5lV}ÝãmSWÝPGÄ/扈ô­ž1F$hÕ:""^̳ÌiâýÞá´_tªÄDIÆo¦MѦžÓ'Ç'©¹Xó÷Ž­!¯‰¬ŠÖ¨\ØâÍÉ +±™ cK»`›¥]’åâ &è5z"">ý6EéQǦJ˺i3>qb {ß.»}V-Õ³gOëV­"¢^½z~MLLÌ~«ûâŋϟ?'¢"EŠèõúÐÐÐçÏŸsW±bÅopŸ€Ü”že=DaOôÉÿû}Ÿu‹å äãÔé_Q";+ EjÙ‹K·"ÜËØhÃþw+†ˆÈÂÉ\ôåÕyÜ­C‡B£fYø‰ý— v®döîYqž~Í} ‰Þ^8÷±oÐl=C⓯Y¹Y¥ ‰ò¹[Ó½7¡µ­]ÂZL$$¿¼q#^ëj+Ç¡¹AZC?‹ì·ûß_ø³Ö5ˆŽŽ~ñâyyyU¨Pã8½^ÿøñã°°0//¯ïf¶%\óó”E›7½·|ï½dÕÓ[ž$^Ä ú´jWâZ£¢õý³ÑÚ§GWÍ:šºŽÄ«f)+^HzðǶãßÑ›ÍkžT hYÖJ”µ™ÿúð¢i‡?›—éÒ§Jô¡#4DVÕz¶í[yæmø© Ë^í :šÓ£XöxÏÚõŽìÍ“ˆ U?}àçL_ ,½‹"æì¦å7)þ]’@$ö¨þS>îfê*œE‰z¥þÜt=éQÐâg¬­$š˜èUÞ%=L18ä 9^æ´Ÿííí}}}Ÿ={V¹reÃuÕ«W‰Dîîîvvvz½þû¶óSþÔn¨ÝÕ§.Þ~‘¨ô"SÛü +ímJŒñöuz÷2>vñö³5obW°TM¿ÚE”z^Ÿýæ]’±UldT².ý…L—C¤Ñ³¤[>Ö™•oQÍÑžZTº¾êbÜ«SA×½;¶mµëøý˜7‘º²•+jOüå[/Ó¤,'”Ò~ÛØÈž $¶*\; Eq!*Ã"’Mv³;zâÒݰwÑ‘Dr+·â šaØäI†~ŒøøøÏ]Q§ÓÙÙÙ9;;«TÆI÷T*U•*U´Z­N§û֛͙üõ¥Œ/]³fM¥R‰ã çkj±T.“JÄ"Žˆˆ zN«Q«u‚á2¹\*ñ1A§Q§¨5zFÄKMÍF/ êÄ„”ôóQœÄÄ\™åÓ$¥ðJ…˜#¦IJPéH¬05‘òDú”„$D©”‹ybzZ/–KyC‰Lff&7þ¬çe¦frQê/† `šÄ•&æÒºå'¢ÈªÖà!õ¤ ºU²ZÇøŒ«ñb™\.§¾½N“¢RërÒáǧ„ZÙº9Öó€äð0F¥ûØM˜N­JTà‚&1îó}3mR\܇þŸa%*!.­huRBú,€éϨNˆSg(~ùÐ) ñq⌑&ÃêD‚Nœ¨Æ[[åä5Ø›ð‘ïˆô/ |_ < <ÀGñùÊv]ß?»àûfee…€ððÏbbb ù)áàŸóCÚ¿€ððaèp@xÈ [@xøŒü@¶€ððièpÈÃáEâëçoøïçø¯¿o‹;3¸I—å4ïÿIucV“Ôçj8à×­WÞêþíÝ÷‰Íƒ¯‡aK¹Š8GKã¬jÏØë€ûΟÓÖÓŒûæÛ_ ÇŒA¥¤Éw­^;qŒhÙ²¶®¼«y+?Pö†-]}¡YôgÂ¥g)ØiY”w—÷¬dZÍC†]¹*<'–›*¥<ÇËMLbŠ Ùf•CŸfúƒ;BžilÊw÷K€—RûpY÷á×k·÷¾q 84NîV»ÿ¸ ò‹ÞýÜ%¨ÒŠåíÜ%Dª[Ó;OJ:ÁiéÜ;:º3¨Å"÷ž«W4w̴żÒÉÃËÛŒ¼‹x[†]rúï7~ñ'Í?ö02AG2ÛâMúë^цÂ÷öés¬â :¯vm¿hÒ{mÿw‹gÝ ×ØÒ»N¯á½ë:KâBFµYe×­^¾ÝW£Ì‹u;¬Êó3ÖyιÕ0qHM[11õ‹SËnôì2vx'·G›G sBÑxȘ’‚—o¾«"uá˜Õa%û-Û°~͈ÊÑÛ§þvÛ®K¦Í³ ß;iÂqeËI«·¯ÙÙî¯9Óþx¡#"!ùò²ÏòûuîX“ ž²øoûÓ׬X0±KEed¬ÇàÇeØÒœÑ®Vf $‡0SH 8Y->ƒ‹Îàk‰¿ùäk8cZŸ¢r¢²²óL¾öJÝÚ–ˆÄEž:²V>ލœå“ £Nÿý¶q•¬-’›($/33·°øè z¦K|yõ÷¥'â<Ú—w0w èèML“Í—¯èxðχ1ú²b"‘C»¹ó»y qéâNL—%)[ÅmûÎû‘ZßüDbˆSÆwò‘Q²ìÜÎ;ªÁ“ûW5#­Ó£ßÿ¼ò$^ëø¿ç,Wµ«è("²kÕ­Üái§Â¨W†ÍSß_s ²âÐiþE•DŽõ;6Û×÷ÌÕèú®ÄÉJZ9¥ªGª›s£Ûfå|ÜmywW¯²upþs~ ì [šÐÀ»ë‚.¿ÄN€ÜH$‘º7x…¥Œ=×/¢æxc§‡Ü±ˆwéå»/:/ܙܯŸˆˆL}šŽ™ØÒE¬Ù°heÐÍH½Hi.MÖ[èFDÄIMåÆgÔ¿»¶eÑÊ]—^©9¹¹2E'ÊoX„x‰˜#"âe&2^-扈8¹©ŒÓkôê7w_©ž®éÞr£áBF#”HÒgÜ–øg¡Ñq¡:œ7<Óªµvo“WâD S GD$w¯QÑbÒ‚^}NW*W¶lÕzÕ‹X‰q~Tö‡-™I´Ø]ÿ°‹ÄØEûÃC:ŽûÐÔŒé'–×LøÌ2=zÍZÞBaiog!åˆôoΚ~ˆœ¸¶Mi{yìÉA]¶¿÷|qMßòÚwüÊùU\LÔW'·™óé-5þÌô$.:xå˜Ò&i‘˜™ç3”¬D6Í'Ï L»f›)Ì%÷2l^nÄŠEUNþyñÚÕsìüû—õ£+æãp~˜aØRÚ¿ŸXÒRŠ ¯þv|_ááƒXòóá"ç6¶2‰\ÌRT†øÀt©="޽þ#kó 7ww³´ßµ¯o¿¤Â?·,c¯ b‚ÀÞŸ,VûöîSGËæU\LD†e²µ;o;áÚýw _—Ô‹N™À(1}óx3·ÊØûáœEI+Qj0b\æÉ˜À›{VkîY­yç¨àaíÖ^SWÌ'Çqø©ü@Ù¶d)AËøŸÂƒL{ãÖ µF½ñ¹dRYÉ%-Ì-°+á!G£€.%1Y#0!%)I¥3ûø‚º玟·*j‘tgÏ’³’SJ›óR¢VïŽoÞ_¬±Ó»K{7ÿ™,T$âdÖ.&ï.Ÿ¹|˧3ñ(æ¬øäiz±—°eÿîe)Ù‹Ó[×ß×Y•Éš,=E{Žì;î\Ùòí¥k®¤ðÙ¹ò@dWµUåM3'Ï´ت‚ýàï%­mqó\Zú;ö]1ù7êZßÛ,åœǞT5Ð$C)É×N8íÚ¸a9ÓÄ×£DNuìp‰ïÇeØ’¥ âÚEõÓçO«U®&“aÎÖϦV«>y¨Õim¬l°7á!DzCÌ©1m>$¢­CÚßµuâÇÞ^\ñËú7Z“µúÍìSÒŒ#òh=²õ£™[g—¹×iն‹íDD²Bí{Öº½xÆÀfe‡ü6ýŸÂƒ“ßϽîÌÚ0ë—íV>›7-ðülÖE8«êû]ºfîøƒfë´iáóà@¶^oYeÔœÁ«–ï˜=r›š¤¶…+7íl#‘™fÚ¼vÓfˆW¬Ú4é`¼@f.ek¸+¸¨ …HJ»Eo›;|C2‰¬Š49ªžƒáGaØRN†©¦¸O9F£Ñ`_} OwϳÎÖª^ »~d\‘‰Á__ÊøBÑõë×ÏîÒš‡Ëºí±iA sŒö‡±íHÈ€öÍÿµ§‹?ÝÏw–ëêý£úo‡œ©ïÏ è}wеuÿù™¥Ûö5«Qþ?¬j2žeø&³o¾-g/ž­W»>nð½ žjýeëb®Ƚ²?lÉF¦þX3¿{½q×MFS·Š-‡Žé^Í.Çû¤KcŒÍ šZ^™Å…¤GÇW¯ØzèïGÑZ’Ú,_«q‡ÍÊÛÿc×,%j&Äqì“mélbŸ½V†?)ûŠM಻#g: WËþ°¥O᥆/TDœ~yÇ‚UCF[î[ÕÚå?8ðYÒ­UÝzoz[¦}ÿC ši^ß»xh÷ŠÙ–å¶wsÿÒÏ#"Æ2·¡qÄÿÓuÁÝL‹¼zJíôþŒœ«Þû—a‚F”©½­º;¿Ï¤Gç/né,Î8—YÖ9 ÒÿÆ2Å.ãs§w6ÄŸÝ{[¡i Soá’FûxÓà7-Ÿí—:2ðƒs½£mûf}%ŸEêÕoó~ìzÈf~ ìÝ$î¹…k¡"ETħ þï3ÎÜOjíbAB½sg­>ËÌ 5è5ydóB õÓC ',;x;FPجÜ~â´f±“š xpiS"}øî®Í¯½sKÃý®õ¯¶u’DÔ§ve"»öë7µx¼,Óêš´Ú§[¦n +3ú÷MœÅDD%~*_¿e‡G¯e"õý¹½o¶Sáï•þç1wÇKc'ÿqÿM¼–d¥[ýu`u;z¹-°íÞŸºW Ût%’s®ÞgÖ„¶>b"bš7ç÷_½ùj”¢P³_ç ­kÿѵ ˆŒH`ÄIŠuÑÞ3u{y'Æ¥-ÄejDs™ÖO/ŽËô;K_PìØ°Wÿøü<Ë4{¿9ÿáâ²$ã_˜Àˆ ²l™`\C`ŒËü²nÿ‡6€½ÿWöE=4ÿ–¯êpøP»PÐéI’O)"ÒG?d¸ãô³\Snn?uèêÂ;š_5ãlaK~-%‹ºw>øÁ;í'Û‹"Ç€%Ó¯¶˜"š¼uli¥H{ {ÖÕ)-rh³BJúXi3Uì.ì<ñ8ÁÜ»ùÀ!í½ßnµä¾ŽîéDäÚqÁ¼Fö"–\ˆ˜a2fÆH{s÷Š ü/"Eb]ԷÀvì%¤O¸¿oŪ=Wߨ¥6.ï^šuY9µ¶5Ÿé­çˆ1„„ÈÍrfØ’‘>áÙ™åkïZ7XPÜ„´Ïo¸å5lw`E+žÈ·w½AËÏ<®ÁÚV)QО/X H%?"ÕµOÉKMM¥¼Hd–/Ÿ¥’’Ÿ<ʺzÚ¨‡ä\2ÿǦIå X¿®¥qˆM‡ÞEIÐĽU©–ÿ÷“÷Þê*Iˆ$.˜ÒÅULTÝ5â\›ƒÇŸv«K$.öËÊY¬yÒy¼Ü|àv„¶‚©äéÒãCÚÉzFŒˆéõ:Îø0Ç‹œêöm2bÛÖKå}¶cýmçö3«ÛR0é£N„¼kѬ_Cñ£ýë·ÎÚ]ø·.‘Ì™}Æ©ãÈùe,ã®íZ´d¾m©¾cªëëw­Q§MÍŸlE× ­ybŒôO÷qoßc„俦µ+†_·+Ñ0px­WVÿ¾êÏ*3ê[¾>8gÖ§N#ç—ΗZÚ_ )7·«Þºûp¿È“ë¶-ß_iA»ÀñÝî ßë9jr ›”W˜r,í6“Œ1"FŒ¥Æ"Ò…Íœ$kŸ6µEÌ—8u‘l‹Ö'–Š8""^i¥äu*íÇ›¸c\¦Qþ\úf )¦÷¼¶¨gÏU“ª»úõhôçä5[—ºÿ—U‹)uíxÁHìÜnÄ€fŽ"¢Òo¯ ¹ÖLwâhTé>£} )ˆlk4<4êÂèNDÒâæŒ*oÎiŸ_eDŒ11&òê9ºo5 Nï~ôìñ2ƒ‡6wS;sxYh¤¦Æ»CG¢Êôã[HAdW«uÃÃ#/ÜxWÓ‰8YéÁ“{—4!ÒX<82ñÁ“æ.—‰9^ª415•± ƒ—ˆHÈØó }~êH˜cë¹ÍÊÙ‹ˆœzu¸Þsð½ ŽûïX4ŸÞ±¶‹˜È]ñ0èèÿˆ±ÔwŠ÷ãXæ‹ r™œ¶ÄK+ŽZ2Øõþ²óï$ŠŽˆ˜^ϛ֚¹ahñ´¹VE2‹|¦¥vl¨uøøŸÿµaâÎggîkšý3Î|¾Ê“²¬>¥º•±¹OO[ºõ¿Wê&6 R–¿i]¢.êðøÑ'²£úuì®Û¼ß;—w’¿;Ü¥Ùº÷Ÿ‹ _J:Žÿ‡-d,ËåÒ,-U'.ÖiDÚ5¼ÂÞŒÉQšaÉÔË1Æq$èõ™zŒÉ"uCÓb€ÔÆÝ2åÖ£X]{K {'wr¶•™ªž¾Œ×±áÓ p}Â|orz¶%‰{Ë)³tp‡3ÆŽW:{ÚÇZ{T¨5¾y=+×»ÃÖ­®8¦!ÏH}ýÜ%·Öºçn:®*Þ³Œ•BÙ°ŽÝ¸­‹ÖS@ Ó”×÷ÎÿVªog¥!%âeŒŒÆC ßÐ= p© w™»_»±.Ò‡qVÆHšÏQ÷¿ 7’¹NáêíÖ•Ã#Aõ&ôþ½·†Ñ\¼Âѵj]—ã{Öp¬`“pc÷¶Ð|Õ~)\@WÖìÌÎMëºé_^?©s ÷‡A.˜@x€\.†-™×Z{¡V†¼eµ_‚®ýbø¥X» kÚMȸtá‘¿Õ™¹©[“iÛ›L3þ6°Ý ""ªµì’±TëjC×ý94ué ~#?µ5¼Eñck16ëã…‡ÿ’ö §,ÜqÎîŽi¿·ïKDº—D‡F6vÊŸá3+˸"oÝpõß ?"NøðÍ ˜îÞŽSÓñè9ê§àÉ•7òW¾mËã¿î^¶LFÄ"µ+Ýbpïò¼@®M†mݾgÑÉDLKTòs–±cß8NÉ8ûQêäG†«ŒéÁp²ß8ÐHâÚdÄ(Ñ–mé¥5Ì/cц(bÌ©ADâÞ¬MÅÐMË~=nR¢ûÄv2>ýEŸ^:ûtê+Êßqö¯ ¼vÛ’IhD–ÞµzŽiá. m´ŒYy`ñ~öoXño—åYÕ÷×Y¬ë=¿—·<óöq1ÆnÝ»U¯v=|Üà{<%ÔúËÖEÏä^9=léG'¤ÝwùƒÍ÷4iwUË8­+±Œ—= ÆÉ‡¸´Å¹ôu³Þ úwWcÌÐ"Ï8úÀ}³þƽ§èÏ—áÒš§ÇÜ7÷*h/Oyqqû%®ä` Ëzlô: <@n—£7‰ûÎ?¨ùÛí>Ûî+ I”ûDƒ:Óÿ¹¶ù3ÎeÊÞ[‹û@*Imz³––ÞUÁþ!Õ¼hÒ{H¸­Á>ù:ôÚ··î9” #‘™[…€í¼ä”q²%.ó6 <äæü@93Û'"ý@#øc­â÷ó6¾S—ú¦õÏë°¥€÷cûØFd·¹þÑ Ã}¢p#©gë1‹[gÚO,k ¥^ñ €ð[¡Ã!g1&|Í ô¯9ñÎr¸¼ì>!˹¢ár5 [ÊéðÀ#îýÆ0—+¶î#s¹gÿá@x€ÜžÖrˆÀ 3$½7'ËU\¶ÚË\vþì#‹q™~b9ÖdgÜ•‘Ë2 ÂÀ‡ Ã!g}ü6gŒ}As=‹e½6;Óó°-Æ>ÒRç>¹Õ‹5ì#›Ì}êÕpYb ñár5 [ÊYž>ÄNøJÇNÃN€ïÂ7º7Âäöü@¶”Ú´lƒðƒØ¹gç7*ár¯Ïêp`¸À7Æc@®e°dHØÿÖ> Ù‚aKŸ‘Ö> $œâßjê”oU¾úÞü@_?_?ߎ«B5š+:5q6þëfõL¾>­I«™×UDD¤º5½•—í/t8šr†- <ü‹dÞý×®[Œ—”³}e—‚ÒoðR—¦ƒ†õªl%ÂÑômòaØ@î+o§y}fý¢•‡oGiåÎåš XÖJD,åIðÊy›N>ˆÌÜ«µØ? ˆ©>|oŸ>‡|˾:|ã-çX±ÓøáÍ )¹ŒéH¦4Qˆ9^¢0QJBFõŠ!šÕ¢Ñ,RT³e”ÏÛSËnŠ×{ûô9VqPW»¶_4é½¶ÿ»Å3ƒn…ÇkHlé]§×ðÞµ¸ƒƒÆ]L&ÑҟȦå¼1¢5‹n´_WÉÍDsuÓÂ{®¼N‘Ø”ðë1²[U‰êÚ´ÎTí»;„l:oáÓvä¸n¥,(ö[,ÝuùuŠØÌÙ§NŸ‘Ý*Y©Îj31¬þ¢ ‹Êpd}æMâà[Ë…=)÷6þ2í´²ù¯‹Ö,V3yÿ/“‚^êXü•%#~»áÒaÚªåsú±ö—¹'£""]Ä‘1ÅÚ›8¸‘ŵµ·>R´dμʘYõ󉼮߶uÏúÁÅ4WŽYV²ß² ë׌¨½}êo×é^îX•mÝ'ÀÇLlQ¤AÉó¯˜7Ê_2oaÈ;{ÿcËÊå'¬ÛºgÇ’®î©L÷r÷„É{T5F/Z¶âׯÊÓ³Fn~¨&"T×ÖÒTðë¨nÞa;æï|¤Iº¶|æöwUÇ,^¾rú fR"’ôDÄñ<ñ¸ %†-ä*¹¯çAõ`ϱ˜}ç¶*cÉ‘gÇ!íÎ÷:xôyUÏ=çuUÇð+nJäÑ}ðËÿU­ ‘صûäQmÅDóGýÝóøŸa½¼>2<‰›˜ÊxN¬´°°0焨àç,Wµ«è("²kÕ­Üái§ž¦´'9´›;¿›‡¡—.îÄtI1Q’²UܶI«[ÀDÂó"S ‘&ÂP¸æÙÑOówZÕ¶ª“˜Èeh¯ËíWÝìGÄÉ+Œ[0´œ)‘Úêîa÷Å&h^%™x•)UÐEI.žÅ*‘¤ÊŒ 8$ßË„aK¤{–bUÞÝÄ0öHlé塈~ð:Rx-8TqV’:w⎅FiËñÆaJ;ogñï¯Þ鈲umƒæÍÝWª§kº·Üh(@§Ñ%’"⤦rc€þݵ-‹VîºôJÍÉÍ•):Q~á#[«#Å( x[v)gêîm¥ú_X¢àL/–Š)ò)9mŠ`Y¶FÁeëGw~R¡R™R•kÕ©à¢àp0¾Ö¾&$’K²Ý gz¼rLi“´‡$fÊ„ð KÄ]X4}Ëkßñ+çWq1Q_ÜfÎgmÐ{p†!v ˜¹É+äØù«×N®:¼ãhçe :ºKp8fa¶”ö/vÀ+×°Y¸¹Écî>K44¼u1¡UV…í 9òon¿TU‡ßxÅì¼l37·Õ/n„ Ž>Ÿìvàxž½@D$±ó¶^ݧ°Jci"Î=´oï>Õx4j^ÅÅDDÄ„´^ž>sÉ2ûBÖª'Þ¦le‰OÄ(\\LùÇ’9”¬×©ÿ˜…+føå{~þö;ãGòaØ@î z˜6æÉ훼Üð›ÄÊ¥E=«A«–ì¶èPÁ<âäÊm/=Ú5p³±¨*ž°té‘!­‹Šúí¤Ã*ÛŠ4Dº¨K'Ï{•µÓ>^”TvXUûLs¦ êä$•Ž ZUR²F®[ºZ Á§NÞt.‘}¥V•7Íœ<Ój`« N\ôƒ¿O\”´['ÃÚKOGÑž#ûŽ;W¶|{içš+)|"’Z¹Yª?w×ÜY;·\ê^¯©GЦ…;œ{V·»¼~õëºó +èîû/Y÷ò÷I›c+ù×ò±å^]y˜láéfÆSÒù1Âê/Z׳- Ãá!KxÐÝ\=qhêoÊÓw™:V·`å¤A«tRÇ2Í&iê"æ¨L¿ÙýWÌÛ4¶g<3q­ÒuÊ_^ND,þÛ~ÝþJ%u¬ÜiÒˆšV™Nõ«,í>âpÍìÝdÙÚ^^Õzt83}ù˜!âüMüÖsÔœÁ«–ï˜=r›š¤¶…+7íl“©7ƒ³ª>°ßÕ©kæŽ?hV°N›>‰]ökzyú‚‘'åÞ=æôJÝ—ù&ü’´pÅôÁ;Ôbëâ~Ãgu.$'Õ^²ÈÒ§¨tÙæI»bt¤Ì_¥Ý˜ÞÅDILH@DºpØ’ a/Þ=–˜”ˆ ×251uwswÍïÊç•)òPù òÉ&®ÈÄà¯/e|¡èúõëÿû[¯ ßۧωz+·vãxú‘;u¬MË6yà…< {ö*ü•·—·µ•5ÞV€\+:&úÁÃÎNÎî®îyã¡òÈc•ÏÎ=;ëÕ®÷±¿O ýÂ;ÚÜ{ý€Ã–ž=V̧˜…¹…F£ÁkY˜[x{yß¾{;ï„T>¨|à{÷[JLJ´²´R«Õx÷þe/^¬T©R6ÁÊÒ*/ðAå€Êç‡b§k‚Zà°ËÛù~¤Ù–8Ž#"ÆÞz€cìs?z†,*ø¡*ô<@îõcζ$‚€ëærýG/ï}NQù òAx€ïÛ8l‰ã8ƾ¿þ}ŸûÑcŒå±ž‡oWù¤Ü^ÐvÌÛQ;¦U2ɯ]¾§gÏã V/iƒÉW•O6ðxÏ —çʉaKWg5-ã?ó²ñÞƒ¤<2 ¶oß?Þè?ð%7¯^¥*åzŽüÄYu}\Ý*-Ö=Óæø÷7qÌHx{tp­ú ÿ <—xuJ£Ó¯&³¯!ÄžШãÒP5cŒ±Ä³ÃÖtì­Àà pôÏßßkÚW«1úLLj}¯Oo\½ÕšGšÜ[ù0vPjåS«A@Ç_6\ŒÒ~Õž"–:4ãë©CWuªU¿aËE·’ hÃ6woÒë`¤þk6rnrGåóí dC¥z õ=1vúææÛûzË…¸ K~»Z çŽ¢¬ ªŸ{gj-ºwðRL›¤k©[ëѿưåüÙ¿ô3ùjMßí¼uÀˆ¿}çÏiëiÂn1"ƾnXJ/EâÕvØh¡ˆ2ýÓpò/ûËgçë;pD³=}­¹]~D %GšG;çîç›®ô”æâʇFœ´t¿)= ‰’ÞÜØ¿jó¸Éæ6qþÒ&ƒ¡Ša93*J#Þ4þÄÆSm¦ûÙŠH`,µ^ûŠÍcµy¬òAÏü ÝVVV†ñµºeÕ!ƒÊDlý{˜6éæÚg¬»iî&yo9õ“Ã&”é?¸®ò^Ðåw©î. ¨RclHŒ@Dº»zUª?þtTÔéߦ¯½›B$$ÞÝ9º}ƒ2•ª”ómÑeÚ‘WZÒ<\æ_©J§ƒo¿à[(ãÉ?ÉLžãeJ¥8bß q““Îhéïë×eùCµºwΠ–ü}ý{Í9ô8Y`êÐ¥› Ø{jÝ/]ë7Ÿð×Û[ˇukÖÄß×Ï¿Qç1+.FiÕ¡Ë̽£{»gP _?ÿ{_<^±àÀ cL~zÅèÀ¦þ¾~&l¹­cLˆ Ñ ý¼ý»g÷héïÛ¸ËØÝ¡I裀Àêµ+öØ“˜”˜öHrrò?ö­^»â Nþ™”è:¬zòî¹{ŸiIÿúÈœ-1µ†u+iÂ}måÃRžœÙ©Qµ2•ªTmÜyäŽ'ʱʇ1FœÈÜÙë wɪ­†t/J/MŒ%ôÙºÞ€Ö¾~-ºÎ9öæÒêQëù5k;vÇÝDáýn‰WóV.¡›Ueê7H<7ªYË…w ª¯oßdXpŒ>6dDƒösvmœèïïÛnô†[‘ÎéÞÒß7 ÿì3†m%"]Ôå-£º4óõkÑiÊþÐd1öÏÕl>.{+ô<À›(gf[94Úû÷ŽËg¬x$nµ¬}A)} ;?_x@Õ 6×eƒ^}W¿žµ¢H±ÍŽö_°êfÙáÎçæ¬ «4jz «˜Æ¬³ä^åñ§Âo‡œy«'[âyâ¾0—s¤×ëÓÎ@O×1·o8môî ùKWˆdòØàé¿nŒ^6ÎY}wלſnö\݉#ÒÝ_³š«éÛ©[QW‘8¬DãÁŠ8›hÂN­œ;we‰u#:Oïw§ßv¯‰s;Š’Ç¡Œ„仯O?ãÚ}Ü‚òæ‘gÖ,?Y±rvS3tïN¯»è×uÐX“Ç{­_ôG…­œE8,!OJ;ñlogqèð~ ü•J¥ZrèÈÁØØw¶¶vÏêõúl}}óV5õ,¸bþ‰ -.¬¼[´ßî*–üWW>º{FÏL`ºð C¯Gø³{»V¬™¸ÕgMg‹YÿPͺ‰ÑW¹¸òÉÃáA„ˆ¨ˆðððM  u«4‘¡Ñ÷ŠŠ°à x”¨BDD^}‚.öùÒ¯o.ã”m©ÿcŒ$J¥„ãE&fææ Ò½8´ëžGïMKå㈪vš·"˜C›Ê%<íyÏE*6ÌÑÊ'ý\¾.ñÅ_›¶‡ZÖ˜PXÁ—¯LË6D‚:.úmÁr¥-CBÃwÆHä=xƸ:–é]^ ¾• Ûw8ܻNJuly"/Éåͧ£é"Î]7õ[Ø¥F!)‘kÏÁ7zMÚ{£—#Qá‡Ô¶äôî/þ8u¤Âè1mÝ¥”ÈNìŸ?RÝÈN‘õ]âLеjí4xóGÕ;)˜qk±ÔŸŒ¿2FDbËǶ/"£dé¹ÝwU&ô©bFZÇÇ{Ï^y§+ˆÄ.]~ÚÊIDTÆ)êRŸ>­¡ÌF5‹¹m!WW>y6 S[–u1^ûÀçó( ˆ P #‘Xd,Nf!gqγAž?ù'•JëÕm|ìp\\ìþ ½Œ133³úuÊeòŒÇ¿ ÙžðD^¸ýÏõ÷ÿ«ú 6$:qñÙ•Ü£n5‹ÓÚ¶ ®^¹R¥ZþuŠÛH(Ç*ÆHP…üÒ6„ˆHäÑðçÙ]‹)™ )ÏN¬[´åă8’˜˜qIzWN/0FœHÌ ‚ÀÉÌœN•÷üq¢eÉ‚æd¬k :"4’wðq0ž°—:±‚Gilq¼ˆc)ÏñÄA N¦”]æk˜±.äìëv¬´kÖÖs~]Ò®¨²Vp†‡ñbÞ0ª\ª”ñ)"Nb…”Ók´‚@i‹rD¼uAñž—‘/µÿ\ÍäöÊ'†‡ððp Kk[±D‚(ÏÐiµâ·âðð內³7‰Ó<Ú:}·Î¯_Ó«Ëf¯¼RalyóÌ=Íó“'Þh§7/7Ýøˆäàõ¸:µ-9"‘T&"JNLPg>Ë$²k<{‡ëÉ#Ç/^>³rü¦-×lRJùåi޲†‡´ïD!í’@À)+šÓÍ[žº&/3—¼>ŸúÅÉ‘>âø¼ÙGøVc—”´“ÅÞ{wjg~Æ^}C-D鹄Ò2E†¯aH ŽˆL# yùÌ_Úá-“ÉêÖmpìØ‘ø„xÓºuêËåò,¿ Ù?ùÇ)œ<­ø;–’­ñ•Ÿ¯òÄíëk9þçßo˜´sãÙ{¦T·úŠs™*A`œ¤TŸI]ož±öa"/å™ 0íó=S–^tî:uSo+.|çá˜q¬PjÍÈ#bLÐkâ92(Œ¥—žþ4d8?’^B†&¿@ã2.oÜñ©×G3e±€ŽÃ6=­‘ZWe¼ðÙX…e®ÊFÆ-I- ‚@,uEŽˆôzDRN¯þ§jà;¨|òfxHѤØ;8ê1ËA.>¦¿ì,–½ƒã«¨7_óÔ9:lI¶oÖº˜Zs¶-~÷Ñ™a³×6Û<¤¨"ãÏNœŒpï²dR-Ã4'‰Wçõ]~ðj\-_óø¿–Ì9ëÙµxÇÊA¾ËZæÏp LÛüÔ ãO :z²®MÇÃç_ ,åõ1˜#AÒ‡-eê…ç8¦×3ÆIì<­U?Õ˜Tt¤¿OÚ´®{F¤ysçêß´¤­ŒÓë™±ž#A§OíÕ'"b¼™‹«üÝýç ‚»%G¤‹~øXeYÞFÂ^e(/Ã5‰øP@^­ë2Þr™¼®oƒsþ¬T¡²Riòþ‘/B}}Qå#œE¡:m ÕiÛ7"¨—ÿ’§êêV ʙʇqbS{—>‡Ž~=ô×…ÓÜfNkî¦yy7Ò¤Ô@_oK11¡g4}tPÆš‚W:ØŠß=|•,¸™sÆŠƒÛ´‚î„«Ë{I‰˜êÅH‘»¥(C §MÍXvÆ7*mâWÞ¡n» {fïø["ˆ1Ɖe¼&Q¥7lŠN£g飙RKL«S‰G†²ô#Ò„ßy)Ø7vvz÷OÕ,À÷^ù|·áã8¹B‘œœüõå ÿÞ /~[¿òMÉÁaKúˆà+Ÿ•9½r>_±ïÐ “fîn²®“§$=\œ8éÖ¬A¹B.†O…`Ùü§¥3ƒþWÞtõ”ãÖÝ7unÏ+Ï´[>ãXµÅ¾©à·ÇÆN»]:À¯¼«4òÂÍh¹[a‘æáŠ€N[¬Æí_÷ÑÙ³uòéS’4R“µfb‹üªã'/Ü5qdbÇšM½­ž±DÖµa KÝ›¸ãÜs€WZ?G"+[a×{ÏÊKH_ݽ%Tgõ˜ÄÒY{õÏ+÷Y>‰½ñ„œÌ³q-Ë‘ëWî5k]Ö,2dÃîp÷€ÚÎ"áe†Ù3ô ¢‡<éý™:e2YZué#÷sͱ“_Tù$]ž>"Ø= Ye/³„«—#Dù:H)§*J?YÏ”EÚïöìçuÓWäŸÙÓ¶@¾¤?÷þKï!<<µc[¸Îƒ;RkÃàP&È Ô-%™ºvÅn}Mûø{'öžIЗΦB£Ÿ¶­Z´É³_]WݽKÿUSÔ„E¦—`¬ÉÒú Þ›â5COG&ÅZ6s²é9y2AÄEÕ;vï:£)˜rïè®ÃÑú‚™7ÐØmax¡,µï•#]Ô¥“çÜKÛ럟Z{0©dÿ .šªf¾óÊçû ††æÐôÿ¯ÚñÿUbÉ‘­Í©aK§üö?ÏÞ»}my"âíêí±3p匃uW67^»¨yqúØk§FUÒ>¼eéÆ>º‚~{þ‡¬ÍÊ6î µÛv_§‹BJw2¾FÓ‚ål÷oßsN ÉËNS˒׿b_ØÄæH¯7ž5‹ ™Ümùc"Ú5¦ç­Á«§WòíÞðÚüeBd;ΘÐbäxý†Mûæ_¯#…½O•F5̹ 'ìˆwðíÓåÞ¢­ §þnY¸¿Ÿ[ØEbŒI=:U½·jÁˆÃ¦¥úMnn\Aê8n¨nù†™£6è¥ö?ùâç,bñ8Ÿˆóm€4©§$rh“/«|¤ùË{F­›Ü{y‰lŠ78¥±³ˆ4”#•e8ψÄNõ~ùtÔÔy ÜfÜâù¢óÆó¶¥Ô­k·ë1eºÃZúUÊdZºûÏ —mYxYb_ªa­ŸB ÄoUmظإ«ÖŽ:’Ì™yÔêñKÏŸLH›±³!ý©?ÞóáA‘C½v•÷̸@Äã|ût¾»pÇosȱlÓ¦5^n{õ©ž‡´Ÿ‰ˆXÜõÓöF©%öåZ\%ÇQèf¾ïÊçË›£E&})ã Eׯ_ÿËÖ½xéb¥ò•¾¾çr¥Rixs¿¸„ìw8;udªTž IDAT¬MË6y`§<}Ò§°F£Áñð/»råJÙ²e³¿¼T*½{ÿnZuòÆËGåÇ*Ÿ{vÖ«]ïc žjýeœ z8¢ g©“’’p}w!8#SSÓ,oîËñÙ–r?Žã2ÞçþÍJï³>zz½>/õ™£ò@åóÝ„‡,ö¾xwü8×<äø+ý`NÈ‘7âëÇäåälK߉ô‘ðïV°Ÿ=r•üx•On¹ÃtZ‹3õžbLÿ8höŒ3%'Í Èo¸¨•%?>¸zÕîKá*‰M ¿nƒŠ[ðDBüͽ+Vº©‘;•mÖ¿—_!ŽHuiûâM'ï¿LÝ*·ï×µN~YžÉ9rmîúq:Ò(•ÊdU²L*ÃW8À¿¬téÒÙÿÜq—¬JV*óÎ=Qù òù~Â÷~xÐ>Ù6zäo‘C)ž7ä –|kËüMŠ÷××5þüšå‹–ºÌ[Å"þ¯U³ÄÔî;¾¦åë£+×ÎÚ⾤OqyÄ©ùKΈš œ^FþpÿÒUóö{Î ô”~÷Áô»¸²<ÓbØÒgruq}öÜÂÜB.“£>ȵRÔ)qñqn®n¨|àG«|rÅT­ïý*õl?OÛȃc†6ÌÅDD)OŽ]RëÑ¡~q Žœ»ß<7ñØ­¸Ê?Ý9yG\qdûjE”TؾåóËO„v.êxõÌ“|ug·(ï!!¯.!ƒözѲ`AÙw}¸|ã²¾~›´aKöŒ±/_DDF Šȵ …«‹«ƒ½*øÑ*Ÿ\|Í—Ú%Áq‘>îù‹‹2Î&<ÇɽíØõШäü¡¯õ¶åìåǧtö²Öœ›"$;/{)ljÍÝ ˜Ä?z©"/9¦ƒýoßÜÏõ[âyÞÙÉ9¿s~<¹\ރʕÏw²IPÅ©Hj"54Fy™™Œ©bU:Ul IM¤†ë$x¹™Œ©bTê¤xH)7Þ"‡—›Ë™êJO–9þjq‡Š*ê¶”'%€ÊòŒÜ2lé½ ¦‰˜áÆk8ž#âEb‘aÎ0ïÏsD¼XÄó`|‡ÂJŒYúÂÆWFt8 <ü#i¡^ëŽöz¯Ujâ0fI@–Gù|e:LÜÔ!K‘:Vï½°zï¼ôVî^þCMÖvìØO;ämÛ¶ýAÃaXü.˜†,;ê;ÜEå¢Ù–q@Þð_Í­œ[®yøÞôyòéràÐüºíŰ%€\%×õ<@^ H˜m á!¯µq¿ÿÕ„Kÿ!̶{ü÷Ó—*•ÊÄÄDî«ýïÖ÷õJ•Jå×”`ee…(ÂC:;‡×¯ñfä%‰‰‰¯#^;Ø9|M!†KH¹Ä?lÉÐ.|þ2%%ïGž!—Ëí­¬¬ôzýWæ°%„½^ommíàà€k¦óƘV«Õét_,2Ñét_ÙÊ„< ³-ä*#?†- <|:²ÖòªV­Za' <ä|~ [@xø4t8äU˜m ááß Ô«I¯Ía¸£ÜÖr•ÿúÓ ç‡´™q˸-fÎ…ËÕkPÁQÆåüS©nÍ u:ñ½Ç-ý¦ô1~ªæç]ó}­8¹.?†- <qÒÒý¦ô(È%D=¹|âÀ¦Éý/w›?+À]–ÓÏ#÷l7{^C-#RÝY<~«¢ç„žÞ2"Njíb>l´PÄÉ!—±²²BlÈ=rÁ°%Ndžß³·O™ªþ}&,YÞÕíÞú…ÃuDLýâäÂaøùû¶ìýë®» ,ÃZÉ·—ëÖ¼‰¿¯Ÿ£ÎcVþõV§{±µwãÀ•Õ†4OVwmÖ7(BŸö áÇ ¿¤Ô¿ßþ²³bÿÒçï~ss×ñ¯Ny÷­Ûûîúø‘—–VÇMì©]GÞüð¤×_|âÖ¾_<ýÚ’ÒŒþƒšå/œ·©\D¤|ë?îi=âÄTkÃîZ²ü“EIgß}÷ÝófÞýíSKNºq Cí‹_ó÷}š”¬žrÇsËš_üä‡ï½öØëÌ'Ÿ»KÅb «Ïý’„¶%ÂCÝ"šŸ!yëwl_ôÉ‚ø nÓ瘴Ԗ}—÷r/™»±´:eDµ?÷â³ûwn‘–ܪwŸÌòÍëv+ý†´ÚýËìMå"å[æ/ØÓöŒ’ú•È>w=~Í'Ÿ<<ûä$KÆ…Þ:úÔ¾ƒ³GãÞ²6¿¢ðÏé³ÝgÜ|õ©m3R›vyñèÕ³ViÝïñ¯ftÝqáœQ¾Ä„`Vì¶eÓësíX•S²ñ­+F¿çY‰à*/W;U÷ IÅŽ§¼ðúW+vº­QqaÅîx—ªÙÒûœÑjÊô¹›®ha»`_ÇKz48;ˆ¢XmŠˆˆf±Ø,U_+î wEþêM…ÛßvÎמ;¥–•»Zï)Ñ$–åþài[ªþ/@x8XÙ¶¿wHÆÀTëz±wÓëww®þ+{ltÑw""âÞþÓ}c¹`âÛçuOØ;çÆK?±¦žxz«×§Ï[wŠý§‚ã®ê–àMM¯(JÍ?ˆˆˆæÖ¬-/}êñAÉUÝPŠ-:žv%¿æ ¶%wÑ–_g|—ÛmÌèŽ1äQ@x8šòÍßøciÇëúfeÚÒÔe«÷DjYùwšª©Ež/+¶ýµU:Ü<ºGz¤ˆ¦ªZåbkÚ §·}óýwÞ³vº®kœ¯ë/[r»LëŒÕya޶ñJõ¢Êó—šp¨È™ýÑWëJD$•#tJškîý[ׯ]óÏòE?¼ÿä-×¼¹±Ó7žž–v’£oÅ÷=1õǿ֯ûû·™ï<6á£õU»ÙÓÚ¦©kf8ücùâ¯'Mœ¼ºêªq–ä^ÃÛ¬XQÒmØñ¾ï%Rb»ŸwZôâçxgîòuëÿY:ûý§î{qe‰HÑ»Gºðv[ò­ÚmÉÞdÐ¥ã®>·K Yì¶¥ƒ™­|Ù+wŽ%:ãØÞ—ù#?»-èƒrìļ¿•ûÚí:ýôÓuñ€´ý ¼òùøû?¸åøH¯ÁÕ¿miÖÜYç>ï?ðÉ'Ÿ\pÁºÄî¼_¦~žÛãº֡$¡ur#æj‚žHë<“‰ÊÐë.'!êü5ªªîÈÛ‘››[Z^Êñ0ˆ°ˆ¬¬¬ôÔt‹¥ñ•‹Äéë]$[`YVè7º¼4+¡ðRo¾ð°#oGNnNBtlZ3›GYEyNnŽˆd¦gzs;´-ù§pÓ*?ì¬wQåÔDDñj1«n_©/ƒ;ø=Ýj÷ų¡rssSâ“SRmv;ÇÃ4\¶|[nn®7á  @5RÀw:ª½ÛR#nÁ!â}¦’›áÂCiyizF¦[UUUåôÓ!­qOEIÏÈÌÉÛîÍ?MÛ|ö²Î§qþLU`Lj‡P—G­¡÷ŠsÒ!E‰ˆŒ,..öþv¨ãýt€}X½?(´-Á«úcºH¶ÿÿJã-^§Ü4k59Ñ¡âçA™=‡ž¡ÐÃV­ŠôžXÇûƒËEâLez=>d÷Õ Ý– W‹ð9Z½R©âíôBÀ^j8¦0#Ì<(•ªçOEEE=hô„CLLL­ƒëe~Ú–à«’Cÿ„ûá£,>þ‡éOòƒnyúÑb½jðíYz“:XóppÙè^—ÐYóàóGZgNðÉðræ¶%³Ôìþ|nç‰ï¯"Þ˜½ûGÙmi:E^°Ÿ°µŸf ŸðP«â´X,"ZYÎo½:ýÇÿ , m_¥¿tåã…u–Ùêþ¿œ“ßþrùŽRkLf‡S.½aL¯Ã$ #öeyyŸ™pªHY½¨W.³LŒ(Š·.õ÷|0jx¨½æAJ·mØѬU‚MQDÂR;dZ¾Z³£Bɪ¾µ{ߦ-¥ñ=šD[E$<³}š¶|m^EÿäpÏ V–­‡®Å+Þ}áó½ƒn}âäô²Ü¿ý3¿XU­œ;¸ EÛ’Þ‹’êêÊLêÁ6òõN‡ˆˆŸ6Qr2àTÃ"øä´aµá! Ô’}ÅZX³ðª>¦ˆØ0×ö‚rM¢”?Q"aÑaž?[Âcõ’½%G½>µ»(7·8ªu—έšDJ“–zùâiÂ*êÞmKÂ;Ÿ>žÔÎ`lcíÉ Õýþ(ˆùôG8ÙB*p‚ðàZ³æš‹¢ˆÅfµz-X4UEQ,KÕ3Q±("«­ò'ÏDU‹,ED©c̓%¹ÛI­Þùè¡ñ›zöîÒ©÷)ý{6‰¤ð×s`Â!´ÿ U•7|W÷Pôøé‰FÚA(„7^@ÌjU™ŠbJˆTÊ‹Ê5Ï÷Ôò¢r[L\„å@j‹JˆTÊŠ+B-/*—È&QVEQDQOưԱ`:¬ù9½ÑváÜEË—ÿôÞ¬Ïç\øÜç·°sè*0ÔDÛ¥‰ùâMè¶ßñ½ÜIäÒÓŽK4ò” Yó`‰jÒ6©dåÆýj§H«Tä¯Ù¦¦ ̯Q“Ú[µˆØ».·DZ…)R±}Mž%ýôô°Z8´ˆÕT‰Èè|ژΧ)ýç¥q÷/^½ï¼–©¯žz¡rq ŸÜÆå¡m ÓE²}WnÕ¿?ÇП;•ÆL…˜¢ªpŠbÈHB+,7@x8jY³ ·Z­‘-O˜þÍ´)?4w´¯Xúþ×yÍ/8)+ܪþûÞ 7ÎHºõGOKh?üĨ{>øhNâ°¦{|óç²N×uO±[-𻤏¤¤Ì­iî²’â[dT„õÀËpÅ–ÏŸüpß gô?6UÉY¾¾$î˜q6ãìçê§+Äù< ùp«V&Pß2Q6xšä‚ãk¯yˆcÆÜ;n÷sïßw{¡ßnØÿîÕ<Ì"åMM‹Åbézå=W¼øÒ÷Ý^bKérÖwœšj³ˆ¶oÁ}?·JDDž¼êépË'OöO¨®_Ã’;.üµ©}¶Ç%QMûžçUÇG[øˆÇŸaÃ˨CÛRH7”# Kݳճ+—÷ÓGt×À”¯ø'˜39ÂC'xDó!·=?䶃¾Öæò·¾¿¼ê'¢ÛŸ{÷Ëç\³* ýŸŸ9àð7×ÉqÇ+†í¬Õ4Ís¸:Ai[ •jXq8T»¡¶¨çû~p·Í5ͦ½!÷$$’…@8ñU á§BW˜A¿iÇÓ¯T±4–éwM­~€ŠAˆ×k’’’xŠê°nfm«G… ¢ÐOäêÜ©p ^Ö<· 7ñ?çáÍ‚i/.mKº’]F~ NGû¨Þ4)RîCS z_ÏÂV°áÁ¿ÙA‘ÐØ¶(»-!â„C—÷ªÅ4GÒ×Õ3ôœ§5óÔlΡQ„ƒÖ¸F¬ —‚… xûÆl²ÃúºýžšõpÇ+‹ç5¯ºÆü5QQQ………>é°3ÙH´°°0**Ê›[ð4, ‹xÅDPê`ž !µZ`:“~X^„C‡‡Œ´Œm;¶r0̤°°pÛŽmi^ÞmK5­¯bŒRV§°0 ¦"ôþ•Ÿ}(ømKž•·æn---åx˜FDDDfzfRR’ÛíöæÜ 6ÔÔºuk¡o$Óv‡¥ê3Qv;Á§¡§(÷×=7Çå&B;¨?<¸Ýîää䌌 ÖL›‰¦i.—Ë›a·%ðf“ DÄåryYe¬h[‚ê.#l¥ cgZV67.…Â\Ïœ…!€n±N:ÐoµZ啞C«¼Ì¿ã0Âc÷ÑÜKåroêW0ú¹“ÕžÓgRuí2³š <ÁnK~äi©ohÙRoÓu¿+‹÷‡ƒfªmSF—Œ‘f:!ÉZ„ÀËü ´-…ž#}nÍ–¾*û “ŒÊ¹ ÅÆ@·X'@¤Ü=M¼‡@­týaæúEÛLeüðv¨›­]õ¸ºÃЦ3‡ïªOeɱâ'3oòƒÐ¶ô7n6·Ðå3ÇÅ¿%;Ý8@x€±0á ÷P!TW¡|øCf±lmÙ|2Šz$ü†ýUCOB HXóýâ"q€¾ò‚CDÄá4]=gâ€7ýà?ñ!гÍ<”þýüè³ï[\¤¿{¦æsãÙ—|°©‚“®ùAh[ Ì{´Þ¦ø˜Íd'˜æÿåš`…àÁžy(XxËy¯¬¬¢›ö>îÆ1'¤èe>D+Û:oÊäwøs[©Xc³:ö|ñ•£»'Ðë L8˜!9øû½œ+È" b§¢Ÿ5ñÁÿL„à1ç¢ÎÐWx%¬ûø‡¯lg-ÚþçŒ7>˜ðH”gF6ÑC|ЊþxíîÇ~Ì<÷ÚOj¶óŸsfþ¸pÛÈî áœ7AÛ’ißûuùVäY\ç~©žpÐm„œŠ°m‘ÏO<˜ G™¦üõë9É ³‡k\ÓÖíÚFHÛöǸ—ýòô/ÿl³{Á‹½¼`C~±*‘Y'œ{ÃçuŠw­{õŠÛÿ:s\×e}ºbwÄ1gÜúÀ5ýSmâÊ[8eÒË_®ÈSZwÛ¯6ñÔþ«¾xþ¥é?n*´$´2ö¦ë†´Œ¬X÷ê·¯ziÛES¿Ûàjvêu]÷䧯(Hêzþ÷žwltç[ùæy¿îivþãWij‘c;õtn¡;LD+Û2wòóï}ÿÏnWd“¾çÝt»£c¬R÷7‹Ö~ýüóÍû¯("«C³=.é%"R¾áÝË®ÿ,áæ÷_’Äóû¨ùAh[ IÁÜJhúÁ_ûmfÃØK|p|Ç‘G†zÈ\ù€9è«Çív‹=*Ò*"JxVï1ÿ{ìÕ—'=}Õ±?|zÊÚ2q­âÜÖùÒ‰÷^}bÁwϽµ¢HÊ7N{`â̲Sn|ðÙGÆŸÑ"¬ò¦òæ<:áýͯyaòËO_Ôtù‹^\V ‰ˆ¸þýäÿ´A7L¼¾¿kÞ¤q7¼¾¹ÓeÜvNúª^œ»Ó]óÞX£Ó¢%oåš]®êïDÄ„)Úþ¥Ïßýææ®ã_òî[·÷Ýõñ#/-+TëüæÞEÏÜûÆ_M³'>þØ}cj¶;Y,¢Xx<v[‚±ëcQ qу#ä4§(ì©UG¶¤mdZ ##Ž`Ãég·%wá–_Þûhmâ©%¢$öÊ#¢–íËÏkÛ»gÒk7iLjˆ­Ã-OÝ?8IW‹mßÏÿ~u^AÄç3s;\ýæ5ƒS-"íÃ~û`î.׎3–Å éŠSÛ‡‰´¸ö–?/¿ÿóן&b;ö¦Gn”¨¸ZmýrÎÌïpa«0)Ôþï‹gþÙY~fzä±iz渳ž8éÂ1ŸuïÕ½{÷Þ§ô딡å/údAüoŒé“iIs\ÞëÛGç®Ïɯë›;?]d?ý©[FwŽQ³ògLŸ&""aÇŒýàÛ±œ|GEÛàÃ$ÃE¬©5ÀáA-™÷¹óEDl­GÜ6éÊ.±ŠˆVºq֛Ͻ÷Ã?{Åk)r5w©žOÇlažûlLˆT*JË 6ý[”Ø­}üÁs(®kó,YÇgUÎCD4=.M±~—+MD¬v«ˆˆ%":Üb±z&¬‘ÑvµÂ}ðp–øîW¾úÑå‹_úDz^ñÖ['ÞùÂM«rJ6¾uÅè÷<ï®òrµóþmu~sõ6i6ºU$§™WùAh[Bˆ0×çÙ¡5eQ½ÑËšüN ëqÃcW7ù÷‡^[Sh´)"RñŸó4»ê©iÃM¶ä|4þ†‡þž§ìWݪf±ÖqŸÚß8ô½fÞPói5¶yÏÁÍ{}õ¾Å\ùÈ”™£þçÛq7½~w÷èê²Û·¼^Ç77¿¦ŠÅʧLÆ„|S×鬜s:ô¾úkÇ P¾ûòí)ä_¿=£J"‚ë@6t2¬yP¬±™-[w~×}#£ç?ýÀgÿ•‰”lýkGt¯ì¡“ÃU=üYe‰ÉL·ïY›S¬UEMDÄžÖ.UÍ]™[îùféæ¿vZ3ŽInPVríýoGiõ?¬Dgµˆ—ŠRImŸ¦æ¬Þ™T-1:2½ŽoF$·Hto_½£ü°éGæiX?¾K¬a@ˆ–§À¡g'F£èg̓Ûù²G®Ýxík¼Ôâ…ë2Ú$ÍuÎ\¤¶q¯™õþû9®6‡û½ÈvÃzÚ'¼öÒ'ê Œ}}?mö~W7kúIçtÿ¥gÞiwË-+þþôù…Ö“ì§loÀ=*[ýÊu“å´s†Ð.#¼h§妟y{«¦i޾ï?ñÐI78NÈRv­ùuö"{ö„u|óž§¶˜òñ3ïd\ØÉ¶iÑŒÿu¥Vî¶ôÞךpË{/f·¥£ç¡m A©³Cx+$þ´à5Û»òÝ0î“r>Aá!àìÍFÜyßú'<öD«n»ãüO~ðèíJzÏ3‡ Ëø`íáCGïñw]ðĤwž\lÏèyÖî«¿ÐDÄš2ðÞ‡÷>ûÒä¿*RâÚ ¾îÑë{Ä* .¾¬ß´o>}êë½"QMzœ3áαí#,ÒïΧozcò'OÝ1µLÂR;ô=klJxbëCF“‹s IDAT¿ÙôìûnÜòð›/Oü*®ý ág4Ù²¤ò¦UU4•W˜£¡m h@5Sï%NQÄA."H„’é^?¿H\ú"{(†‡Ø~“fö;ðGKBŸ[ÞŸ}‹ˆˆ\òØ'—T}ÿÊÑ""’2þƒÕÇ*qà3ß‘°nW<ñþUqÍØªTqÜ良>èŸ k{à,)Ã_œ1¼ò‘ÝïþU­ûfMè<üêÎï>ô4‰l9ø¦'ßtðwëüf‹¡·½5ô¶ª?]Xùÿc.ûèÛËx1?*v[‚ÁÞ? þ‰@=‡èpNƇ(P>~:’¦€Ð`a óü ´-€ù²íæáð-ÖIX}>ƒt*!òQ¥ÓÁ á \š @C3*)ì¶Ü« à K ÿð›››Ÿ˜œ’j³Û9¦áª¨°åÛrss½ ž¶¥êÿz@Ë•VªzȺ9µhâßòšÊîžó×’ßW-Y¸ªÙ°.IµžŸK–,©þºgÏžÁ,à ýÙ¼çPhµŽB±3J1ê©´¼4=#Ó­ªªJ‹·iÛ BQÒ32sò¶{ù¯›¢mÉb²‹«ÜU9ª«Ìm °Õ(VÝ{þúuƒí¸ó[''X’û7i3ý“ß~Ë=vhó°ƒn(¸€Àaw#ÂÃáÏ %"2²¸¸ØûÛ¡Ž÷Ójôaõò ˜¥mIì éÑå[óKÔ,»EÜ…;ök±âj>÷´Š’ ­z°¬‘ ÑŠ»ÜÍp çE4qðŽ€èb·%ÅB#„Aãœiv[²%µk·ké•[vîØ¸lÁÊ‚¤­ã-âÎ[ôáäÉ_¬)Öl‰mšGìY¾pEîþ’âÝ–,Ùfoq\z/Pí(H~V5Á/ˆ… õžX‚xpMr‘8kJ÷¡ý‹æ,žùY™™Þñ´¡,"nÑ´ÊÖûð¦'ìoùqÉ×-R•È´ö'ìwLÏ Ÿ` ,f žòR¡³ÍœÉëß6MÛ’ˆØ“:ttxp¤Hí{ñ¸¾U?ÜqÀ¨Ž8gBÅâÙKƒO#„_Í<„Κ=R/®™v[Bí×hƒ¼@r_)§CÎÆþ®&"ÿu $d×5iœ—#ÊtWú»÷þùùËS¾_²iŸ5½ëÈ+Ç_أƆ•îÝ _¾ëñ“oyç±Ó”âå_øðÒŠ7sìÝï?Ü/ºhõ7o½>ã×u»ÝÉǹìú‹dh× V¾}σ3ä‚×&njãÐ;“´-8¢ÿÒ´ÐÜJwmK®mß=ôè ˈñ]Ÿœ¿`ÊóO>ðâ£ggZ=…ÿ<ü⟢ÔX:lëpå½·KL³(mÏ‚ï{oGÿkï¹®yÉ’©/?ûHDóç/j&"¢•¬w>þÜÜ"«Dr•µï±W¿Í„C8YtäÓE²…zœ~\Ì›SðCµÄø•E‡¸æ1vmûyֺġãÇœt\›cû_tÃùéëgÎÏ©­äßÏzE;oì›Tó^ÛâZ´ïбC‡Ž:tl×4Ö²wÉ7Ë-ý®¼rP§¶ízŸwÃ¥íwÎùz}™ˆHEî÷Ï>6«ÉÕ÷ ÷dè=í˜f·%ƒê˃>øTKö•Hxl„ç~Ù’Û·Ë[½½\*rf=7qv“ëïÓ)æ û¬•þvߘÑÃG÷Äg+÷«¢–î/ÖÂc"<ñÀÛ¢}BцMª{÷‚WšjóÈ5½“,÷ ÜFæ¡m ¨|_{{V'Ÿ úÂʘ„Ø1mЯó‡9g`²ðp0{úq­ì[g}þÛöRÍ]’·aÍÎrwYÉî¥oÞ7ÍrñWŸ˜xМAXówÜ3ñ™§ŸxüÃÓþ™zÿs?ïRÛ·‹ÙýóŒù›ŠUµl÷¦µ9Åîòâ‚ÕS~uçéÞ20Ó®øôù?^yƒ ˜§æ!€¸jU¬¦5xM0kˆaº¸Â´èoQú\sëéOLzêºïE$ºE[‹jmY´öï{¶¿8þü«~ë¹Ë®ßüì —·îÜ/EDDÚ¶m!_üüü5%N{‹cû¤n¾ø‘°¬Öñ%’«múeó¾õÞpÁ‡U70õšË·<ñæ-#8ü~pÝ–‚Y+ðÀ۪Nx¨]eÚÒNÿlßKví,TKgþ廉v™­†O|ã”rO-Sþßô{ŸÝ|Σw mn¯Q›*IIjnq…X“º]þø»çïÎÛëŽI±þ~÷•Sš5Ïp×ËÊTqoŸõøÄޏoT‡./¡w´-™¤„`]¬¡«ê½a·SŸ êj‹Wl1)Y1Rºî½ïrRú÷ÉŠŒ³7‹«ü»òÒ8›%"%+#!LÜ.Õjót^¹w¯ÙXÝ&³òª¼Ö¨¤Œ(qå̘¹6ºÇØ–QÑ‘QÑ•7à¶&„Yì YYIáŠ÷ç^¨\\Âw·˜pLlI¤ÿÄ4‚á¡ö¦µ²œå+v„G–oùõÓ)_W œ0ªUXÍ]9ð …‹Ÿ¼ÿ‡Œƒ»·۹胩[²FÞÜ>RQ\ù,ÛbÖ¶ÿñͻӶ÷¸ùî㣺¥êˆ~HÞ0mKãwÞ3ëb¤J ÿðpðš)ýwÆÓÿQ®Ä¶8ñìûïrôH°Ô6KD³®Í f|òäWEî°´ngÞõÀEm"÷¶^xp^Deu;ãÎgÇöK³)‡Ö³ áAïÁ£*?mK„‡ÃÔ› v8ü߇·»úãž/ÃZžù¿Î<¤Nìr÷Ç_Ý}ø‡ÜÌñêwÆü,P ±îF&=cR¾8øïpå#ÂÃážRkæG}=1ÌXy¹æ¶%™ <Ô.0©ãõ¼šîðúþÒ¶„Æ” šˆˆÃÉH€1Ê)Æ€ðРò’™Ó$v[‚>£…ƒ×àÈX>@·áA5®Qx&B'hѶŸÝ†‡¨¨¨Â˜˜†ÉRaaaTT”÷ùAh[àYŸm¬=#Ž\}2ßÀ˜,A¿iÛvl+,,ä`˜Iaaá¶Û2Ò2¼¹‘¤¤$F>¨8uÎÁ5bCáD `T˜NÓQhkP.%Ä¢á‚?óà)·æn---åx˜FDDDfzfRR’ÛínôжäßwÊ ÈT† n·;999##ƒ5Óf¢iZEE…Ëåòòvh[x…J“…q¹\ÞW™0&pXÌœ€¢úy9B‰…!€ny–„Å@ÕÅ+|yƒ\Úl§%,ÂÈ´-P'EaÞ„à&˜¤Âƒ÷˜ZÙ‰ðmKAx„¶%ÀdX(`Ù ÂBZèM@£Ñ¶Ã`Ö¯3 6†:ÏBÛR•õë×{¾hݺ5£s8EØ @Àp‘¸Z h9ÀQ±àþÍ©mKÐ/Ú–Àøà <:ÉBÛ€ £ÝÐ9&À¨˜‹@€Ñ¶}ÕB,ÿ qõœ|ph  Çç&‘ü ´-!µæÒ©‰3„+F‡ópãÂ;:ÌE#„èAát„t ¿Ÿ]LàáðÚ–£óLJÌ ™bB×y€ÞóƒÐ¶„#csw¥NáYz½mÌ<@¿˜p¥tÒÂôŒ¶%GÅ <òƒÐ¶d˜"®±}Ïì²wèX:‚󻨯é!ùO‡ 6Þ0&B¡Ò Φð>ò…Ò ƒÃ':?:   ЂiMÓväíØ¶}[qIq( kTdTfFfzjºRó›Á9OÛRõyºšíÕŸP)ÓaÖ§!ÏnÃ%%.a½…‡y;víÞÕùøÎÉIÉ¡0¬»vïZ½vµˆd¤e08Þ mKB…§àÎf D¶mßÖùøÎñqñååå¡0¬ñqñÚuXñ׊úÔÇÕƒ“““³aÆýCZÕGš¦ÕüºÖ_áûš¦yfE©ž ¨þÎáþx¸¿:ªV­Z¥§§×sp˜p`Ù|ÜnFŠÂGò¾Fª¸¤8)1©¬¬,D†UUդĤz¶!UÎúõë{÷îmî‘ùõ×_“““ë98´-™5|Áá4l_O˜#%YÑšeÄðàùdZ ±ÃVÏÏã«§æìYU?ÆzmKа LäྚPР[=8¡0Dõ?‚6áP±éË׿§¹¨O²•×ù txPDÑ4MUÕ Š( s‹ç1Ösp‚Ö¶Tò÷ë¼8bäE}’y‰0ÚÛ6“Ò€±ž³tpþU——]ƒ¿Î.%*ãZUrýÛ–<ƒãv»ý6D%K¹ø¹wú„nQ:õœà´-Eu;´ôÅ—>ë}ÃiÒ¢m•÷ÔÆ…Qt͘UˆSø¨@x¨‹×=9®üe_¼ñþ× Öì*“Ȭ.§žwÅ¥ÃÛF›ã×3ùࣶ%׿O®»ì½­ÍǾþæùMm""56ejàm/{ä¼Ç¬~pw÷HÜ3·Û]ÿ»´¶¥âSXõËîóz=[ó»fìwV·™MjìËÚà ñ±ãðᡞƒ´¶¥˜S^ýuÍÓµ&,ÑMbyÕ¬.L€Y¨ ÃÓÖï•Ò§Nž_qʽÏÝ:º—cë6à⻟º½KÎG¯ÎÝîrmÿꆡW~´±\Ó4M+^ñXö¹–išZºyöó·^2t؈A£¯¹úßûU­lý»޼åÛ|O [¸àγ/¯.Õ4ͽwÙÔ‡/;wÄ ageÿïÕ9¹eš×´æÁ‡*¶/ž½­ùWKÏ™ûËöŠê]œ´âõ߳ŵüõ7~©ÈÈŠ¦i Àd‹8èÿ†Ù±Èð ð (ž†þF+ݺð—üÄSGvŠ­^Z¬$xV°5s–ïq«jUßOU릪î½K&ÝóÖ¦Î×¾üö[¯ßzbþǾ´d¿[Õ49°:Yõü¨Zþßg'ÎŽ<ç×>z㱋Ó=óØW›Ë½¹¿ªÛí®ÿšÏà¸\.Õ*¶-ž—“5à¤Î'÷KÚú‹·—WŽŠZ²üÓ_ÂÞ8áö«:çñÔ óò+öþþò/ÿÙô‡_{ùÉñÇoy{Â3³ó\ª¦j®­Ÿ¼ú“ô8÷êóÝÙ#"¢×}o½?ý£.iaóòÎyc=')))HÏ‹²UOîwËϱ'ö²çæ[£J¿7è†9{yÁ„O_(ñy±BPƒԸŽA#?Mß½y¤¶Kµ×üfXÚ1)’÷o^¹&šÔ¾T‚;ñ´_âÎûßù½[¦¦´è=êÒžî¥ó7–jšh~´ò®•þûÅ×;O¸îêaÇf$g´|áYY[~ZºËåå}nèàx‚·\;ÏÍÍìß+-²iÿ>‰›çþ¶Óó8Äyâ­]3â”ÞýF»úeÅ÷ç.ýüW¿qãN?®eóö/½þÌè?>[”çÒD¬éç?ñÔ½WdìÛ21Ú®X¢cãâb£Ã->›©Ï°x–‚"Š—>?¹ðsç¼=!»m¤bË<óù¯ÍøáíåE¼b(ÁÒ ³ÛR寪ªª*5¾©‰VQ\áR5‘ÊùÏwESËrWå–ü÷Î5ç}à©ÏÝåj§—'eTÞUÕDÓ4×¾ÿÖîÚÿïı ­žkV”W¤æºÔÅ‹;ÜàÝ–|²›­{ço³·¦õïjQmÍNêÿͼÅ;‡•®ªš(žñ‘ðŒ¶ÉÚüÍ[ÖlWÓûd†{¾iK?.Cf¯ÝYÞU%,:\90FÕcë‹ i€Ý–\{·–5ÖìÀZ{Ò1i2§Då2á! ¼ Ö„¦‰²~m^Ù©ñaÕß,Ù±!_¢“#UÑÜUáAÑDÓT—f;vü‹·u­ÞœT±Ç„ç­©úQñü¨hªêvkÖ”‘÷=tn3{ÕZ£b­Þ¥µƒãu…îÞùû¼ ®œ ×gPõ­Ù¿ç «‰VùïˆgÙ²b±)R• <™I­Œ`ZÍo«Å­€ž AÛm)²íÀŒ%OLþýÔñšˆˆVü÷{Ï-MÙ62”^œŠ°î&>½ƒeºH6¾®§Y‡ó…χëÞì×cËèÝ;é‹ù3Wœc8Ï˾{×¢/—–D÷>>5ÌnÓJ JÜšfQ+*TÑ4kjÛTõÏ5{Â4©ªù4UsÛÂmR¶¿Ô­iŠhîr·&¢)1M›GîY“+q*/)¬išâÕþB yÐ4MQo·4rïú}î–”Á·ÞsF¦]D¤|ý´G_œ·4Hÿš»-i…›þÞѲuÓ¶–Y«rJ†¦F+"e¹+s%õ„d›¬«MD4QͳG’oÎÏäC}'h»-ÙÛ\õÊuŸœÚ;é™deOñ_]3ÿÛÚêιW¶¶óŠ¡ƒªOq©ös8}·—ׯŒRšBx¨ÚP¨ñ7ÖêÜ+Núéé§ïRÆ\tÚ±ÉêÎ?¾ÿèÃ?Ë›Œ:³C„H‹ö‰{ç<³ã{–~=ma±ÚK³$÷9û„©“žx.ñšszf*»×ý>ÿ7û9wlÛ:|Ú7Óg5铳xÆÔÕî˜æ¢…·>khÆ-ï<ùš\8¨mLIÎ_?Îù¯ç-7õküKF#v[ò><¸w-›½)îÄ«»µi^9?“:¤ëÌ_²«_–hÅ~šý›­MÔÞ埿µ2iгíRÓÏêc}ì×ÿoü¨ö¶³Þø¶èøz'[*ª¢ƒ¦‰ˆ-¡Y|ɬ¹‹þ‰ÉÔlMÛ·ŒñjŒç1ê}·%Qâû>¼hËÙ_úÃò-Ea™];Îê“îÍ-ºöüóóœÅkòJ•¨ŒŽýNë×&îÐq¬Øóï’EKWoÙ]ªÆ?êü“Óm¼BA¿)މ)}"”0GxE¼¾š’Ð{Üs÷4y÷“™ÏÜÿA…ˆˆ$ö¾êžÑ-íš&-ιñœÏO›ôHxóþçŒê±õSM‰?áæ‡¯òög/Üç,“°”v½‡]dl~Ù¸!¿1åñqíŸs~ëfЦiöVçÞ÷€í)?ñ}*1Mº|Vó¯®j§ªjý¯óP=8^^ c×òycº_ÕÔ^};‘­Oéñäìå{.E©Øôý«7hqmýï^Gë0‘.Wƒ´¶%±§õ5¾Ç(ŸÜ–+É·ó7&œ8lT¦{Ëâ9sfÅ¥ŽêP|P÷­üöóß\mz2<5Rܶ8ö…tS‹Óhè–o§†èÓQxñìFêÝmX’ºœ}k—³E+üå¡k_‰»íÅ›»ÆT.ëîtþ}oŸ_õƒ#GˆˆªjaMû_ý@ÿ«ªÑ$±÷eOõ¾¬êϧöTúJ|—sn™tNíúß›ÇÛˆÁñrˆ’O{äÓÓ¾™ÈÎwLùHDä´w>?4ÞHx‹A×>3èÚêojªª¤ á£á5n%®ûØG?ë‹A©º…zNÐÚ–D¤bû“~ιàïÍûÛvvÕ„ —t‹ol=ïÚ³f]Ar3;7³HÊIÖ8W­ßß©{BÛ+Ý´hIA‡áçõË 7*Ô9´`vä ‡ðàýnK ?¦w˲i3¿ýÝÞ^Í/iѧwºî*®Fì¶äÓ!ҩꇩëÝ–´‚…·õ;íý´Kn¾êÞcbËs–~þÀ©ƒr~úéÞÎ[2]±wg‘=1%Ò""b͈WþܶÏ% Öþ—ï\½ÕÑbùïnÞUfKhѵÿ©Ý³"¨ ø NtDq»ÝšÏf‚,©§Ž»výóo?ûH©’:ä^½Òt×îv»ë¿æ¡zp4³O–yº¡ê98Ak[*ZòŠ3âþŸçݼg·Ö±W^Òû¬Á/,¹ùí“£“˜*Š+4k¸­ò+ö›º¿Ô¥IXÕ¨Ey{+Ü[“Þ§÷ˆªÈ]>Ñ7sâÆ k}ð -Y²¤úëž={÷4p:ÄáäåÂà (o×<Ôºãé®}|ÀµJR½lC·jmĥ匛ê98Ák[RÝöŒvéVH[;tˆþÚ›ë<(–S-žÍpú÷\¥.%þønÇ·J°ˆ¤öï»é½¹«w”µ=&â 3t`8\ŠiÀ–°N:Q ˜ô+Ó¼€þÂCdddqIqxX¸ËPE).)ŽŒŒlÐà4kÖlùòå -Ä«‡´Î¹‹š[çOzvyòÜçê;_ó뚪Ö×uþØ‘5oÞ\Dê?8o[r—–¸µŽ×ŒÊwÃk-']vBf„Z¸qö3wÍérÛÝÑ»M‹=Ê.®r—V•ÊÜÖð[1³XíZEI…ç'[tL˜º§Ô¥‰ð–†@ä7˜_6™+¤<õ [å†nxÈÊÌÊÉÍIˆOˆ…a--+Ý»oo“¬& œ”””””§ZÚ–öÍ–pöüÊ?Œ?qÚøe_ôÌÐñ¹U{BztùÖü5ËnwáŽýZl‡¸šÏ=KLZ’í÷œmEjjœE´òýûÊÃâãÂyŤ2 $ÃCbB¢ªªÛ¶o+++ …a ÏÌÈLLH¬Ï¶B Îá¡m)vðÇ××Ùžd‰ÊˆmìÓ,©]»¸?—.\™Ø#õiñÊ‚¤­ã-âÎ[ôñ§D{vû¨Ì.bfü:wỈ-¬¹KíŒíxR:û.@Àx6:s*â ?ÐAxp»Ý©)©™™KHì^¯ªªË媨¨`p¼œ@·-Y¢2Z#""åùkÿXµµ \­z±¥Ä5͈jÜ!²¦tÚ¿hÎ♟•)‘éOÚ9Á"âMñܾ=³ÏˆS]sýðùR%*ãøÁgöN#;€ "RQQQÏz118u ÚnKêž¹7ö>í•Åj³[ªz‡âÎør×gÄ5ò&íI::<8R¤ö½x\ßêŸHìpê¹N5ÛA¤qÈ'ƒÆJqÞt®µ˜W±…~y–<)" ÿpÑo/ÏH›ôw‘êª(¯–߸䀣WúSŸÍdNöœPoÕ‹€Âà¿ü ¿Hœ%2)³c÷æQ¦]°ìŸÍ+œŽ†Í30)ÓªÕ4_ŸxÌVH&}aÌÇÆ@·‚Ö¶Ýë&Çc×ßþÆ„+NÌŠ²V¾ŸXbš·kCܘ2;1%ÂŒ.h‰ÓÊ÷åç.xíš!¯Õüî€{çÏa@¯„$ðmK…¿¿8Õ>aÞ–ÛûfD˜jP¬<]øýK82®›†`£ úèuÒb‚%<­Çà^McÂl5Xyµ†>5zõËN ,ÑOExê)h»-E÷¼fÀŠûùlùæ]û «”¸9$Ô£=ç |ÛRÁ§§ü:ÿ‰s»·HIˆ‹­2læ>‚‰maqpRg6@ÐÄ ý ÚnK1ý__¶~’Z+hGeÄrLÝ*&»àý,2©Ùœ”U ,¥áF´Ý–*rütÚ¿e3¼Íy7œ×&œÃb6¦)==e´Ž¡W ³ß%ÂÜü o[rå-ýý¢"ÑÜ%ùë–®)ë>æ*„,áç([„Ê0}È ì Ex/x‰;ñ©ï®ñçòïœsÆw-3˜vÔ³tLŠÓЯ í¶T[XËcRzoy!Ç$ä9(ô}|XS’@x@ˆç |Û’¨å¥”ìÏýõÃwVڲ♨H@xô)hû¾>=ò€¨ø&'ÞºêÔïèÅ1¡nàðB`¥¥B¿‚¶ÛRì ×­)ªZ­XÂã3›¦E[9"k—t¾ IDAT‚fºH6£€ð-?HàÛ–,ÑMÚ´cðýÄ<;Š@xô#»-•®zcâ«Jëú«ˆŽWO¼ºc‡~äÔD„Í qL> <G„¶%µ(çßµk‹úž{ïÊY‹¶JÄ™Cï!<ºÀÚÂÂPw~@¶-EõzðÓoküÙµcÞ“cÏûÎÚaìÓ_:=ŽrX´!@Ð4n‘.×qC£°Ûô+È—w(ÛüõÚ|4ÿ‚ék–½{y§X^b`È3B‚w‘8­xÍÇãz·ù¦ý¦ïÖ-zÁÑ:’à@x€îóƒx·%uÿò×/êÒqÌW-]°nöÃC›Ø9 @@xV2èqÊhºÈtŽÐ(tF™k _AØm©|íä󌟱£Ã%/½ye7Y½xaõ_Y;öê˜ÈÅáС ì¶T²úÓ¯·‰Èê÷oþþÁeøå®9#ã9,€*¬96EMc0~@¶-Åœãâ•%ÐXÙ €Q°æúäÝ–@x€Qo·%Ô¶%è=?H€w[Ò±õë×{¾hݺ5£¡g4b…Ó_8™Í¦ AØmIßÈ µK™ljPþá‘ ì¶t@yþÚ?Vm-(W«ÖOÛRºžÔ5…g <ºÍø¶%uÏÜ{ŸöÊ¿bµÙ-U{ðÅñå†/ψ㘄6E¶ãÂQOa¢á¸ µ-ýöòŒ´IÿySÇ(¶ïÀä<×à’õÃnKЯ í¶d‰LÊìØ½9É€ðCå |ÛRt¯›[î»ýÿ[²òïUUVo.TM2ªNMœôÞˆ8œŒ‚az~€3 Þ¡m ú´¶%­|_~î‚×®òZÍwÞYñ¡2øl6 ÅÌô+hmK…¿¿8Õ>aÞ–‚²ŠfŒç˜ ÈNqð‘PK6C3Ð{~À·-)–ð´ƒ{5 ãp„wL…{j˜y€~z¡ZtÏk¬¸ï‘Ï–oÞµ¯ °J‰›C_c@x|#hmK Ÿžòëü'ÎíÞ"%!.¶Ê°™û8& )‹YŽÌ [ûf ‘h[‚Þóƒ¾m)¦ÿëËÖOªµ·’%*#–  ¤1óý ZÛ’%*£iôŸ>pñ>];uísú%|¶9¦iFO—zãÓh@@­mIJþxè´aOmî6îéw>|û©kºl~êŒ!­(å™è mKÐ{~À·-/{å=÷m³¿¤k¤ˆˆ8ΜÔmäËËn{£oÇ„.f _Ak[ríÝZÞ¢wËÈêoD´èÕ¢|ëÇP… ] ZÛRDë“S—¾0uMUŸRéš_\’Ò ÌÉLkTXo£àW´-AïùAß¶ÖîÚIcÞÜ¡Éäþ}ZEÿ÷ëü¿’¯ÿáÚö\30Ÿé ` IIIŽ •,I§=¿lýYÓ¦Íú3§$¼Ï¨ ç_0ð˜(.ç(>Ñá¨?OÛRõû+Q­^v×@Žá†Éȶ¥²§½4íß²ºþ*¼Íy7œ×&œc‘M[@x@( „ƒ+oéìï‰{ïªEíNï:°w«Ø²­Ëæþ¾%kèƒCÇsL Cà=M…~äÆ`·%èWv[Š>ñ©ïþyÞÔ«š$úà¿ÍËç|õùŒ~ÛðßôóÃKS“X0}v¶AEĶêAÈf  ×ü A¸HÜÊOW´¾ìÌUaÁÖdȘcV½ª˜€.í"qaÇF.Ÿòí–ŠÊ?—®öÊÒøÍYðXLk^&ÂŒ+x‰ëò¿§NÿmL˦ûvúɳÚßrÑË7GxÓ ù hLCïùAß¶$Ö¬ÑSVo¸üóÏçü™S5ø²§²ÏîÓ$‚Ï"À+Ó)ÜÃcæú´¶%­dóây¿®Ûí²…[ÊvüñåKïž8eu)ÇÀQ_>¸ª uºèŒåÉ¥Ì<@¿‚v‘8u猋ºŽš›yR¯±¶ªù†¨ðµñ7éÚóÏÏs¯É+U¢2:ö;­_›¸ºƒ»Z´~Þg³ÖÆ {N{.iÁú  Õ3U80q~À·--ÿ`iŸV}3&ÓGSs®ü%ßÎߘpâ°Q™î-‹çÌ™—:ªKü!7®•n]<óÇ-nŽ;Ð'Ú– _Ak[RÂã’š4óٳõgͺ‚ä'wnžžÞªÛIbòW­ßÈ4FEÞòïfom1h`ë¨Ð{^òÁ³Ç“Y+‹"ÂÐPAÛm)ºÇ5ƒW=ððçnݽ¿ °JI£g*öî,²'¦DZDD¬±ñJÁ¶}®ƒ~ĽwÕ¬oÿIè?¢w ³ëÁé` „±¡*á8\~À·-íÿéÑ×Írt×fÉñq±U†ÍÜ׸›S+Š+4kxÕê ÅaSËJ]:NµÒÍ?ó»¥÷ˆSZEñ”„/»¡™©$0+Ö<@¿½NºZì©oý¹¾¸V_‘%*#¶ñ7©X|H¢ªµ*;µx{Îþâýó§¾1¿ê[sß›ºûÜóOLµÖü¹%K–TݳgOÎ@x*m·%KTzŠë»©3–m/qk"¢¹Ë r7Z/}ó©“bssö(»¸Ê«¦TW™Ûa;0ãjï|æùm+»¢\»–Ìü¿Ý]Îzl’µÖ êD „ 2?HàÛ–ÔŸ]ØÓñcV÷´ÍË÷¶;!+ÿ÷?wu÷FfX#oÏž]¾5¿DͲ[Ä]¸c¿Û!®æsÏ—Qùµ«"Âb±G'ÄEZ9þ€àÑ4–àP4XC¿‚¶ÛRÑŠi‹[=ýûÊ\Þ¡Óm_,Ûøï;CÜ®&)öÆfô¤víâv-]¸rËΗ-XYÔ¡u¼EÜy‹>œ<ù‹5Åì· À¦30oŒáÚ„ਂ¶Û’VVdmÚ1ÝÞ¬{ÒæŸ7–Ú[Ž×úç)Ë‹{ƒÖ”îCû·(\:ó³ÏøÛÝî´¡,•¯†Â«!àm zÏø¶¥ðæÝV}±h×ГÚw¨xáÇ-å½c¶ïܳ»Ä‹+LÛ“:ttxp¤Hí{ñ¸¾‡<'ÓN{ G‚„Fàˆ˜y€~­m)üØkîéþåˆSû¯Ãegî½§GÛv.[rü¹¢Í6ž¥Æìª õÄÌô+h»-‰­ÙEÎ '¯+HOÊ|fáÜ/Ü׿œËÏnÊ f@'²ë×ߟ­ïeÙ¬R€. èÀϽ°2›ðø0?HàÛ–DD,1-ÚLjˆ4péíD+úïïǧÛ9&ú+¿ø¨€q+u¡Q CÛô+(mKZéß?wãEÙ—ÜòÒüm""âÞóë ÙÇu¹iq1Ç!N¶Ž—ßòk6ƒkÀ£€` ÂnKÚžÙ×ö>ãÖÏ×ïûï«[OíwÇ¢ýû~vD»>7Ïk}óÝ}b8& ¤Ñ¶½ç dÛRñ_S¿—ó¿Y1uXRÑ/7uyÏm,}sÝ I‹ß¾á„D–<h0ÏìkŘÆÄÌô+mK®½9e-uIPDbŽz\áüw nþiå§7“€ºÐà 6®«@x‚%‰S+T‹Í”°¨è¦×¼r×ɉ ª8b [¿¼ƒˆˆ¸vþúõ§Q Š”®ø¯p·å»Ïœ+l""öÌ“†Ÿ”ÉV­ÔWo <Œ‹ÄY#”“¯>orÕŸ¿eÌ4ÏWÑC¿ÎùnD|ȇ³òBÔA}$ `°ü l[Šñ]aè5Pr5p ×{n`ç¤Îù!D±æú”‹ÄüŽœÈŠœûÂBD0v[`j¬c¦D>êéA°! àˆh[‚Þóƒq·%FŒÔ3¾E1M• ÔÀÌô‹ ÂP/´-׿0™MM0&I@x@ˆç¡m‰Â—ÂÄ0ÂpdL8Ðeœ¥¤@xô‡¶%ÂЀü ´-€Þ8´ÆwÑJ`”/íØ¯!ŒQ@ Aˆ¡m zÏBÛR•õë×{¾hݺ5£áMPq²êÁ@x€É°Nº2.Ú– _´-B =B /áð2?mKh ®ƒæËÁtúàp„Ê¡; v'<AÄ„(ÄPƒ£Cxê…¶%•°¢ ‹†áæËBÛÜ´É„‰—øì€‰°Ûô‹Ý–((ÊIUfôI4©÷hâä¨/f _´-H!‡É:ÂàM~Ú–ôV2*lg„€&œoì³ dжý¢m‰šòœª5Ô’2s:»Þ¹Ž¦,&ÀÌôŒ¶%HÐf ÷ü ´-‰‰r ™è@À˜iõ³ÃÉbnC•eÙÜ1Î_ m‰‚ŒJ0pë Z@ÀóƒÐ¶@xŽŒ xÏø=n¾:ÿ}džíÿÏã³ëúŸ¦ïlæùÓ‘÷)fcÂH´-ŒêÇ…‹ŠáhP~Ú–¢èPHΫ _L8ÀÏ5¸¹^ôëÝÃs¸ÏòÉ$ Œ¶%µÞ­×ïR$yó©õ™7K @x„¶%Ô¬ðÉÎfÂÌŽ Ê2@xê…¶%À÷Ø9¡€©òE•ðè*?mK `„˜ °Môƒ¹j™ª c®€¿Ñ¶d˜¢çíŒød œÙ:?9Eõ}C!ý’ðAx€iòƒÐ¶ðm8@x€ù0áÁljb›ø”!€nyÚ–ªÿkèÇâÚóÏÏs¯É+U¢2:ö;­_›¸ƒ‚»V²uéÏ¿®Ùš¿¿L KlÕí”ݲ"½Üy.²êšHOG‡"NГśéŒ|‘DDQ cæzÏb‚¶%Wþ’oçoŒì6lÔ¨!ÇYÖÌ™µrŸzШÅ;v¸ÒŽ?匑ñõ×ï~ÞRÆÑà¯Â8a”›5f _&˜p¨Ì{Ö¬+HîqfçæqI9©Óçªõû;uO8Ý­É=†«ü:#r÷ºO×縛‡[9 À(E*Ÿy#40óý2ÍnK{wÙS"-""ÖØŒx¥`Û>×á~Zu•º$,*Œw!ÐE*Ç„(?ˆáÛ–ÔŠâ Ín«LŠ=¦–•ºê~ÉÒŠ6ý±¡<­C«žœæpîªzDÊÑ2Ðð‹z‹J!¶%è—iÚ–DD±˜ÏVÕþø÷þ=gþÖ¤>çWGvX²dIõ×={öä  ÙúÖLƒÈGx|È,»-YìQvq•WM5¨®2·5<ÂvȧÂjáúù_/*:öŒs:'Ö¹ÚÁ0!›—cƒÐÁ6Jº+#œ (+qðÑ×4´TÖ0Ðy~ãï¶dOH.ß•_¢Šˆ¸ wì×b3âjwµhÃO_ÎËkyúÙ}›Fð>¯¸,@ã˜æ"q¶¤víâv-]¸rËΗ-XYÔ¡u¼EÜy‹>œ<ù‹5Åš”ç.ø|ÖÆ„žýÚGæçååååí*¨©÷º¨0DUÃ@·Ìs‘8kJ÷¡ý‹æ,žùY™™Þñ´¡,"nÑ4MD4WA~¡VZ¸hææª_±·>sì¦vÎÀ ´!ᡖĉ±'uèè8ðàH‘Ú÷âq}=_·5®=‡Ûßü´¹çfC1‹l¿fO߈‡%[÷'+mÂàfÚm ¥6 ÷4Ôk _¦¹Hjáò¬ðW1Ç©eÐMáÂà«ü ¦h[˨?˜4ƒð†Ôˆ>ë*y7;áM'W‰fvÅx¥LHU3ÆÍ™ÙoKv w`V´-…‡èâýÌá¤î„æÍ™$`úü ´-…h˜Ðh©-›ûÊzSTÕTð <þÀ„ƒ™£ð›ò°Öœ>Ê6fº0VjuøsE5éþ?„(Âà[´-™«®äMà™ ë<@ïùAh[2M‘ÃT#[d:£P×°èÿîqà(ý¡{Ì<@¿˜p€#"ÁßЉÅ¨àý7ª ,áEÛ#…: <AÏBÛªž· äjc­ÃÎfv áÄ„p ø¦Ñ@>å‚ýŒ£y„ ¡h[‚ÿ qjqÝ– ÷ü ´-…v•/""4³‡*M…£¯oÙý‡4¿Ý8Ÿ'„]RR±¡“œõÉH†íDw(âô®8c«_ <Gâi[ªþ/èjZ €ú e*„è=?mK0S$£ 5ëŒÐiÊâÊ}T¨ßfÁ‚iè뤃PÚ:9áàÃiŠ#ßæC¨“„ ì¶)Dú¥^ç"né L †Yж½ç¡mÉ(§xÛƒB-=š¦÷‡éÐDQè3!.Vþ­SiÀíúÃÙ:~¤Ù~â‡jvbÛ4¬“öûÛ(/æ¦{¬<¦|ÄiÄÚ½ÎÝAÕ@_h[‚~ѶdøŠÈŸ×bcñWÖX¶¯ïIý?ðvhýCµþ„è??mKzS«1\mQã3÷ȼg†ü°¶ªö:_yX6èmKÐ/Ú–Ì̯‹ NqIÁÁÃÉ)À„˜y€~Ѷˆ§Aý þ(°?ÕîæÃ\ô|¶352˜y€ÞóƒÐ¶džÙ†`làÃ¥Ù‚VL4v±¯Ïº§ê¼6YvK.Ê)pbpÔ@`ж„Ö•4„ýøÀûêK;â+€—ó3õ¼6E$€yÚ–ªÿË€¬_¿ÞóEëÖ­ýV˜‡p]Ø­è±VÛLKSLÙ¹á8b~Ú–ªø13˜ ¦Òq…ëpŠÓ¡ƒ¢³¡][»¢Å!W9hÐÑt8ëýè~¸6ëüDc‚Ð)LC¿X'Äb·öçâf­ü×{@MiˆÃdþ'sð¶|å)€c·%й@?¨Ã?.‡¸F#.¶p$ÙGl03Ç~/:|!õüEÏ) <Þç¡mIŸUü–D=wm„àçÊ:yÈTØÏ£ bÍô‹uÒf.¤ÌñÂ]ÏAðG«½NÞ}½ëJ÷ÁK{rèÈ’˜y€~Ѷ¸Bˆ*¨žµ2V@hcæzÏBÛRÈE-TþQS=©×MA^ÔA-ð¤ã163Ð/&x)ÄÑ«pÎ9Ds:¡{Ì<«‰—Wcy^õ²ð|93Ð/.¤ÊÎѶä?[©Ì¥Lw&°Û’nÎò!9hLCïùAh[ ü:ȆçFÜaÎ%„˜»-é‚Cã³Iø\ãw[H¹@PѶý¢m >–íynì;7ÐÐàÍyeÖ½%+€êü ´-ñNÌû:@‰ ªsÆGh[‚~Ѷd—Wó¾Âúðjðã{3½A%¼™3Ð/Ú–àM)ÏÃÔõ=7ÖnK:ÿ¾Î»g‚:Éó¸œÜfâ’qá&ÈBÛLPµ;´€Ö@0Péž)€¡Ð¶ý¢mIïëÞ×a!¶}½LµFÃ3 ¾ßm‰žf _´-)T¸ w˜ç²q¤ Àš'h=Þ!h1Û[~è¦a{€ðx›$Ú–´òâ Íf÷|Š®Ø#íR^T®rü)LëWÑq+$ÝV“”¹<1jBk·%E©n"ÔÔºßK'Ož\ë;ãÆã<Ñ[]~Ø_il9Ë6©äŽPߨI%< ”…NÛ’eW™Ûó’§¹Ê\JXBØ!+’ˆ 0OyÉnK`L,˜†Þóƒ„@Û’%*%É^œ·§\DD\û·(qé±VŽ? <õB»-ÙÓŽ;&lëâEÿääm[÷Û‚u®¬ãšEq˜»-áð1OÃRh¤ˆ°¬¾gœ˜ºcáWŸÎ˜»!¢ËÓÚÆ€Þ°æzω%<½ëétåcæúBmK„À¡Ô¶„v[ÂÃÿ³w×qQ¤ÀŸÙdwéîAJA‘P±±[õ<Ï®³»»»õì³°ÏÆŽ³[,º{sæ÷Ç¢"?Pðù¼_¾vò)ñ ôÔf!ò?ûüöÝgQñÙ2¶i£íÕ)"O ¿qéΫ$1%4®á×ÄÏ^ó›øÎc]’zãÚƒ×Éb†£iîâÛ¨®:‹ òz¤'<>}â•eç@/]v~#”¾‡¡³ÞܺtóE\Í×wðnÒ †.·’Æ3 º0áPîCMî›Ëçe[ú·ïÜÎÏ8ùîÙ›1D¥|‡Ir\®VuŸ€vm›{è$?¹™Ã"¹uöV¢¾wÛÎÙä= ¹ü:—AsTYÒã³^I ‡6DþgœÂ~ü÷èéç³:ÍÚwh×²Ž Ÿ"DžüàÌÕhA­V:5wf½ºtþyýæ€ï$ùpýÔ­dÓ†ÝûöëÝÖ™~q>äE&ÈWEÒí}›w½—$/º2ô0tFèù‹á´c³NÛÔÖxíìƒ$ye’P]([*ï1^óâ#Ë®^]SC3'_sYThNšÊ·KըѢm#êVfævµêyêÑIÒåDš%5óö«anhRÍ«^5öÇй4š£Ü÷ô°ógµ¸håmˆüOˆzÚó_kÔïàåhejbfaªÅ%Džö*2Kϳ¾›¥‘‘M­z®êÉao2éb›ç°ßø¬˜x‰fµVÚ|žÈØÉ͈¤Åe*ù Ã6ðé1dPO?½"gÎeèa謷áIBçúž¶F†.~u ²"^¦TÖìɨzþ@P¶T~cMB£n¤©¬Väj‰ä©‰¹4SQÉš\"'›(”/oh†As”ó.Iù˜š—tçÐÖ;‹îíߕ֦…‘¯èC]*¥9&îÕ-¸„éú'G~ó&ÃQ@>E˜º0òÅ7|WÆœôôßpNöÝͳ#ŸÜvë¾µQ=‚ÈÿôAµ´=L‘…•¼<€ªçeKåÖÇñ„<’!‘+/±22‰Œâ ¹¸ÚZþg²âwN^I0oÚÑˈ§ <—È% åPÁÈ%rЧÍc£9ÊñØÚ™äßÇHg†=mݪU-#V$"_ÁX<.¥K„p !l5-5"Í“³õ¸D.-¸±”–Kl¾—_ÜÿNâw§·p3Õá¯Kƒ=çn…»·Ó@äê Zê†Ëph‰œ.l9År+é#¯pì€êÂ}Ò匭a¤ÅÊŠÏTVºJÓs8:"tå9ÄÜ=yî~ÃíÔ•Áe õu¹¹IiRB!òÌø,JÓHƒ‡æ(ϱŒ¯¡­S@KÈa±ZÚ"ž‘¯hMcM&õ]²2ÆŠœ”J]W¤¦m$’¦$çÑ„¢ÈNÈd4Œ5yÅ~؈á÷õ4 ‰äÓ] WCK(¤ "ÿ3û2ôí\"·šÐâ”d‰š¾.É@9ÃÓ–Êû"‰š™³ýææ½È¸¤ØðÛ·?p¬ùˆKyR¤<8q*”ãVÏU[œ’”””””œ!a¸†Î¶¼wn‡Ç$ÅEÞû7Rnêl!d¡9*"_ñgšî¦òˆ«7^|LŒ}uûÚK¹EM{ ž®ƒƒfÊÛÏ?$&D?ú÷y–®£«ØæÀ· |g.0µ7¿½u÷uJn^VÜ‹{áÙÚÕìµøˆ|ækR‰D*§F!•J¤ ¦L= Kæ†Aî‹E'$~x~ó~²¨š£ne-ÿa4üíǷ⯗goo Ê]^^^áß%xýÆ¥†K +„††ºººb´áêXОܹ÷$|ö&SË¥Qó:¦jˆ|ÅõîI·öï;ÿ$FL¤ñ/Ÿ<~%±r±qKßÃP| ܨ‡·ï?‹HdÛøøÙªWììÏ‹ðv6vß§Y!?¾ÓRpdAù*ý}Òç/ŸìX  BHà¹RRâ]×ÁG‚›7nþ­Ÿ†„„ÌÐû¾×EÙ¨.”-¨$ êùÁÓ–<” HJeKHÊ?”- y(&<” Ê–<”! ([@òP2L8 y(”- y(Cþ@P¶€ä d˜p@òP*([€ÊÉýð*Q¦ @òU  ([€ÊEþn}-®Íä§bBd¯W7tt=뇶Wd#Y»è÷¿™ƒ À„ƒ€ÊÒÕÕEÚ•—œ’G—ßFnãvmå:ª!²ðĄ́.”-@å–~:¨éü(ñ juf?Iô‘qMmEE©Yø ~#&„ŽÝî%rÜÛ]H©¹O}”“ùhÃ>êlŠ¢8ž}¶½Ìû|#iÏ–ýÞãK1!ÒwÇ&Ø«SÅ1ðè¹êNMð·zácÚ»Y›êkhÛ·žw#•F3’¨JùAÙTRÚ­÷_œj«Väý™nôƒ̓‚gÞK—f‡¯¯yáÓäBHnØqjÚÓøˆccíÃftû¸yð{ £Èx8M½)·R%yïvx?½y÷¿?È !$'ühúg"b“>žn6kð–HÜ*H jÀ„ü:roÞùÞkþÒßœµ¸"›v³6Œÿ{óã\BÇ´Û°6ÕŒ,muÕ]&_~2ÙG[‘“)4Õ$%[ô”ºk÷;ŸE+ú¸éðÔÌM\7\ÿ꺳q B×¼û°s¡´Ü[ÕÑL O"ö€äª”-À¯Cšü&YzówS6EQEév8›ñ6QBáh›i+ï@¤ä ——tvPçjÚÖë9ëHDÃ_u$O‰ÎP·µÒÈĹFކTêû49!„-Ò±•‹Ù\C3 bH  åeKP‰QùÿåêXèj´=™Ê()²ÞE„h¥­\G¹’4lE×Ág\6¼Ì§D?:=¿‘‹¦èF pt­4³£Þgç§Òø°xF×RA$P…aÂ*}êÀð)/#>¦sk Ò=?vꡈ,…<íÁÚήn=Æ*Š®¬ÈKËaºúZ·¯kÜìº:<5«Àãæ³B÷µÂÄT<ÊiVÈoeºCJ@@¢ ÿ•ó—Ïv,a…!Pðk`J¼£)øHpóÆÍ¿õÓ¹zß÷º˜yÕ… •‚äTÊ–<”! xÚ’€’aÂÉ@© l É@ò‚²%$%Ä’€RAÙ’€2äeKHJ† $¥‚²%$eÈÊ–<” *‰Î¼¿¤¥O»×Sh£ÜHßììÓ`оw2$ßeKª(/lËü¼ßOn G²ÃöÎÒ±eCo_¯6f}•ÃYS‘tqv;ï'•YvgËè¶þ¾ÞM:MÜó,뛩ñ`y ¯Gàη2Bˆøù’6Þ¾Ÿþô ŽQœ;㽋.ôr1³„fÄÑ'fýÞª¶·oÖƒ]Œýò N½³mjïÎ-½¼}=ü;ZòNRø£¼g7õu#»pmYÜå•}Û4ððökÔ{ÎÑ7y aÒ/ þl'½}=w¾•¤ÝÝ6µwçu¼}=šö»ëI­Ü›·'— nQÏ×ûAÀ€%§Þ‰³ë>Žß»u‹ÎÄ*T·ñ98þAÅó‚²%B'\XDÜlY/G!òŒˆ'™vFüVÃŽ>¿qÉ’©º.ûþtà)3€{ëFͽ+#Tþ/¦\š7vgbÛÚ¼?4oÉè•ǧ{iP_æç¾Ü1núél6Qÿ´²þcå亚!„°„f,"#„âºY6ÔE@!„¥a#úö^K"·Œ[tÅjØêm5ew6L›9ͼú–^E΄eÉOÃéšÝÆ®¦-?±l휩®»ºš²Ò/í0í®˜–WáºòÇ&L?ÁþmÎÎÂÐ]³æOØî´o˜ƒ×Ä][² Nû¥oöM]§Á£Sž=—8w7ØA=õή…',t8²Ð‡ûbÃèYÿzÎݵÈ_/éüâ3Æh9ìäÀÓk»©¯Gã®C×ßHç/dò®kìëáÐeâÎé%ÜÁä¼OØ8è² !„kìfÉŽ}úQRìªò¬èëÁGã­Ú¶±U+~kÒ˜gqÄÔÅ„G!l-»êši/¢²‹¼¼$âছÚ]†Ö×eŠ*Ìâ 1%Ò±Ù:.5õSC¶xš.'²„ç¯döõÔ)B¡4Ü[:ÓO®D‰UôĄ̀zþ@P¶ 2d áñ\;O³¢¹tjPÀ¼DÃkôúÑ>Z$ýöòáÛØC7Mh¤Ï–$®•“œËðÕùÊsi¶PK@r’s„p SÐM£ÅuÚ°ª9WñîÓö¹f­&®l¨©§N’Y¶vÒhíý»zZòm»/XÖ]OW ½½{Ù–?gÿ³ª…añÆéÜÔ†¯¡–ÏhñåÓ%4Qÿ|}YôŽÀ ¿ÞÃfÓÖ®!øV ²ÒÅuAþi4[¨­FrRrD_¹1:ùúúƒ©>SºVû,Á¢“þÝ{1ÛqH#6á¸_96òåƒZí¶±eçZNÞØÕ*s”Ȧ–QÎÕèlº¦@¯ò#yÕ…²%U#ÏI«›èp‹.céúÏÚoûææÎEƒÆ°w-÷zþ(&9fnÿ¹kÌlµy,!›Ã*¸BA3ŸošÉЏ•örCP£ ‹¶tjóvóñYu\êû+Ô°¾º3$ä~rwKCÏÆ†Ê…NvÌã€YgB³[4ÖüæžS,vá$-ÿòÅóÏŒÍ;®ÜíùñåÅõËúOâîZ`ÈþæÆ Ïì ¦èÆ$¯‚7ßÖé¼§^Ñsé»cÓ<¨6t[3!²÷'WïLï°ú@KéµàÍ[CV¯»è1§¥)‡BØZ¦Ú¬Üäl1@òPʲ¥Â¿Õðe1>[캫YõêZçú¾œØ¾Ûê#Í$ÊjéëCfDõÚ°°£mÖ6!yŸ%¡ a¢gŠ)‘ž°ÈÉ9K¯ÙÂý5ÅÊòEÌ?G]­·jÅo®ü¢/ÅÕ6Rg…gä)·R°?jzúæC–”þFM>K¨+"’,I~i‘"/KÊÕÔæµ.ÅÕ¶rжr¨Æ{pnÈ¡ûiÍZë³=®†6_–—o´B’)¦Dºùï„N¾ºîpZÝIÝŠì¶<ñòÜë’Ú-ÞÞ݆GÉ{±í¯‡f£føY뱬gx×u´awÏÆ“œùªßöH@Õó‚²%Õ9wé©å&dÊ ùúDWž“žG³Øl¾¶©vþ2Iž6—%0´4Ó¥»Z°O?w1&ç]X"ߪ†÷³ÄĪàöi9[ÏâëXX諱hÍâæ—eF„&sLmu9ŒBưó—Ê“^Df«;š‹¾u©žYÕ0Èyð*UáiÆ&²øçi“6¦ßÝY@¿>µ7$Í¡sPý¢ûÁ·mÙÚ4ø¯U‡lþp•ÜZ¿?Þv@3.‘¾\Ôc¯þœS[ä»,Ò-ÀßÍ”•xÿв;ŸÙ:,Bynv^V®”aä¹ÙYYlº€câ߾ƆMKw¹Žòã?Û¹õ¥a‹)öj„:éÊÚ#é^“‚ nµfrž,0íŠíÐ-u#^&Bqµ,­Z{ò§nXsÜjhcƒ´ëí{­ßx¬½ZþoD?NÙÛ¨«èc<€êBÙ€ªai»5±Lm>Œˆ y€*JY¶Tøw k6oÜá¨h([UÏHéÊ–¾¾/K°K°K°K°K<@UQ¦£ÿ뻫±K°K°K°Kªì’ B9Í ùñ­LwH ÀÉ.TDþPš²¥b K°K°K°K°¤*/ù–¹z˜y€_Ð÷•-)×Ç,Á,Á,Á,©ÊK0óUqÚq(G˜y€_¾$@¥ yUϾ$É@É0á€ä TP¶€ä  ùAÙ’€’aÂÉ@© l É@ò‚²%$%Ä’€RAÙ’€2äeKHJ† $¥‚²%$eÈÊ–<” HJeK*…ƒ€Šçä‡Ë–†IHJˆ‹ËÍË­ A „&Æ&FFE!8N@u »Fò@!ºººår·CBRBJjŠ›‹›ž®^Uˆ[JjÊˈ—„cCc烓œ’äèà ­¥]‚“ž‘þ&:ª”ÁPË<RP¶Tø÷wo'.>ÎÍÅMKSK*•V…¸iij9:8> }Vš>Á)98U's „hkiÛÙØ¾Œˆ@ò• Æ2$ŸòòÃeK¹y¹º:º‰¤Š¦i]ÝRNÝ"8%§êd…ùC™ô€_ Æ2$„”_Ù’²"a˜*½RÖA"8?¾ZÕ €ªu\Ë<@UW^eKÊ¥Jõ)ez³ü};Æ2$åS¶DŠaš¦«TŸB ÁùñàTÁ]Õ|×PÙ;.Œe? ¾çTWy}½EQt¹¿ZÔfø•t]ád±Çú¶ê»ó­´ðeÝBé+sèŸ%÷ùòöm&ßÌúјÐâÈ ½Ú :›ôÝ ñŸ”-Éân:÷,UQ™Â—ø‘×BÙT¾äá'ŽeªeK_*Dz%RÖ >&'òìÖ5û®„¥ÈWÛÚÅ»íï}ÛÛ0 ÉŸý WHÑתàý®K#·ú°ãîuíMØ„BñG†8ä¹jwã{ºÍ”Ž:¸²™UôU ^Œ)˜¨r’"'."üÕû„L C8# ûêvúøHü„¡³\6•}áïíû/>yŸMs´lê¶è>0ÈÏœk+H 2ää‡Ë–UÖRH:õú²1k_:÷¹ÄÓJºwáÌÅÇ)­­Æy|ÑóìÏs‡²ÝÆPÚÉLê»ëD•¿Uä·™ÂäŠçÐ}ì$ÚIôyOÎüXòõY(˜ïßP™‚ó=蜷w¯?KÓ°®QÛU‡Ïä¦ÄDE…½5ô«,Çà•U®÷/wÌÎ…>DübÙÓ>öÛ¶²©.%yµºßÔ˜ÁÛ×Óüô‘Ï Ý4ceHdb¶œð \ÛÓÏ[Ÿ"gÊä{2–²ÞóðÉÃyͧBö1dÓÊÜq>FKßþ];wý¿Qɹ4˜Öí<|| «ú§PÉB*øžFú"Y­Z}o'!„hkë›XæäVÖ§•Y_<}ù>ULsDÆÕÜkÚëò©/î<~Ÿž'£ [ gíZËÉXä¼¹v僱»yvdd×¹I=KAÁ)2"ï=ˆNɑ҄âëXÔ¨ål¡Î.§!ÙTºÜ*¿äN¾¾ew¤e¿ 3º[ò!ÄѹV5þÀ1;¶>ö›ê·±ÿø'z8<;q>"CͪÑÐ)Ã[˜óñ8xíÚ£w?ä²uëרDñhAŸYy=ú]ß}6"S«Fà„)}kj•× ¸ç Ê‚%òÃ7?P¥P”©<žé‹HÜ“—éòü KMÅÐ !òˆc\›1y€wæ¹[ŸfÑŠôûë&¬{jÞsî¦u‹‡º|Ø6mÙÅló:–ôÛ§±y4-~ëQ¦<òZXº‚–§¿~™c\ÇFÀy1ÍÑsk;rö² «ŒöÉ<¾lóýLM+¯êÓEþQ …¢ôeýe Î' C£}"W0„04MÓ aÃдøÍ™³O‰ëŸ¹tÎV‹¥ ííuyEv‡bñt¬jxÕoà_ßÓšóôIl^9]qÃ=Pù’‡˾$2_\z¡pl×ÔŒS8$ñmÚZæ<¼•K3„(¢ÿ¹!÷ê=uâ@ÅÕ‹N¾—Jß™5뢠ãÌM{·,øÍðö²'ÞKi†¡óí<-õ:m|‡÷Á+‚#ÄåvÃCéDzr‡™PõüüxÙR™Ë~xöÝú7}²|Æoí=ky¸{øøy9êó(†a»úˆ…S›èPDawîZÈË$‰ÍÛ£·ä~S†8«bÝgxäIGîˆÇÖ2̸ÿ&Ca÷àǻ᫑¹>5bž'h8ÕÐa}¶3‚jzV#Œ4+%…U»®ñé#Såž<åéwþeöï)[ª¨à|ö"ô»ý:ï(ºÔ V‘½Î{}ìLlõþ›ú71`R{ÿï+) Q$ß ¾¥¸¡»— ›ƒN}jŸ]t5:ÏÃEðiÐöìH-ÉHI¶¯ã¡s-â}6­õE(~ l©âÐÒì<š¯¯Áûf—Έã#ãy « )BöNFÿ‘yí IDATï~Ì”êÙ9j†–Šó(#catlº˜6`B «ÕósÒúrV¥aå¨A-ç± L4"#Óòh tPU•Û̃<ãC²BÛÍTXt¸`kY›ð²criK†pjü9{\C-ŠOí¨ÛS®Ü‹qøp2±î¨9­œ„„7ëÙþøðkSšYJÍkÒÒ‘µÕ ‘ꄜþ&SQMU^ï÷¿ 5’P]åø%qÊ4½ô¿ÂÒõ½Ö¹Ý“ûž<}xrÕÁmz-¦-îÆ0„bs(š¦)BøšJž'•$GÄÓFÞ&|å pŒœÉňTí¦Õ…'ž¼Ïv¹ŸçÞ«•ÎÇe×¢3õÂÞÒÖ½M¹Ÿï‹,áÆßë·žMR°…¼\…¦L® 9E.²L<”á ¶ìO[úŽŽ‹feÔfʨzúù7L§^]µô–rº„Â0òÌwo²ujÚkš¦ )˜9Ç†Åæ½Ý>(ðoåvRíš%/ºŒøíÅí«÷\|•A¸" *Ga)•+>…‚a”o’¦¿ãªËõ´¥Â×ÏMÍ‘g†]>ó’R†QA3ú2†Î ú"*%¡8<¶œáå ›Ç)f¤a$IOB_'ä((#§)õòF0ó•ÎŒe_wÒ ¡ F›¢CÃR0QŒrâU7¢¼‰ŠLÉ|=ë÷›le¿.“Ê ’³åf ÅRž4BøZJ–'U”×ãdñ´%€b”ãÓ–¾§OakÚz6±õlÒ­OòùYÃÖì¹Ñe~õºŠt. M juòÏ ! Í5ó°Vì{ùôeº}[ ‘ké­H×wéÆu-øŸíŠ"áÂò%gY]§lèRÓŸquÜ C…çÇŸÿ£lÉCÅGÙ¿2„RÓ·±·7ÎOÂÔ¨üD‡!„Ð ¹Œ&,Šäo_YÍDÓ -g8NC׌«),ì÷¹êjEvBöîÈÜõ·Íþ˜·»Eu]*6xô¸[L‘P04Ãäwìß—}zœ¸{Úü¬ßÚÕµäg¿¹upÏùÌšƒú¸‹ˆ‚aˆ<öÖåÛ:Nš9á'¶ÜâúMu׳7ia¾¡ lޱµ“@CÛÝ9åáˇw^«éZÙÙjfÅ–¸c|S7×Äû/žÜyËÓ2·³ÓM*ÏA Òeß;–‡kÑiÆBí}»ï^t<—ai˜× µ²›·!‹aä„¢H½·mîžD™Ðª~¿Y}]E„²é2}&gûÎý‹ÎeÑDݬVýö–j$ùË“¦¿Œ¢ cY¹ÛiVÈoeºCJ@@]PMŸ_EN‘)ŠÊÍËÎG$eeeihT¡/nÎÊÊ Eè| rÁX†ä€ò+[251‰ÑÖÒVã«U…¸‰%âôŒt3S3çƒcllüþÃK ‹*’?dee½ÿðÁØØT.Ë<|ÊÈ—-éhëÐ4'‘HªBÐø|¾‰±‰Ž¶ŽB¡@p~<8QoߊÅâª555c#ãR@u`,Cò@Hù•-) }c«J|¥:MÓr¹\&“!8NÅ@u »Fò@H9~I!2™ §D‚€î~ !ÏHy^ß³v×Éa‰bÂÕ¶vkØ:°O?KŠ>Ê:;;ûäéZµh­¥¥]æë™Wú5Ÿú¸è›Á[Ú_¸Þ`í¹å®8Èþ %µO¿F£ qckjáÊNq±"tÚãý‹WN5C&ÃG°üÖët»}û‚̾5¤åÜ›Üb {ù‰y^ªt\µ˜o°æä¢Ú‚ y=ñóyM'„bÐmÏá!̺®}#FžßÔDëºÒÏZ*ïÑÔ¶ÃÃzì;Ø×šûÓ>ƒMKWß¶=úõªgƯ€1!÷ñì–CB²¾Z®×iõ¸è‘“%SÏom­Ïª$ý¥nåÝyÌä~õ Uåd’¿?·qé†cò[ÓÂÍ·õÀ½ê겪ʧÉ©²%iÔÑ=W¿rèƒ:’ÌøÈ[gnßóæðmzÙ•û¨ æÐã_¥ !¹OÚ*¹l„³!”šµÖ‡™óhW –ÊWÊþʉ“{ÿÀÊ-£'éÛÒÍBN'™ì+†M½`ÖkÌŠÆÖ¼Œèg_¸Ó­®®>õå ·@uUDÙ’"þÌÜuÏloÞ2¾k#O÷: » ·ÿðŠ@ Nú…ÁuÛ,8~p~¦ :mŒ>6µk«&žÞ¾¾-§ Ïaɽ;¹™ß­›¦þÖÀÛ×# ÿâ« µJŠ´G»Gwnèáݨã´ÞJÊgosrrØ›š–JQTffÆþà½YYY%ÿJq·Oquí\kº¹)ÿ¸ÙëSQ{f­8+ÿüzÍÛ3ó´óòöõhÔ}Ìî§´²«ûŠ£[G}_tÊåù}[4©çáíëÑ8päöGé4!’—ËÚ6è±õÀœÞÍ<¼ušyæmÜ­5CÛÖööoñçÎçÙ !„Ði÷¶OìÔÄ×û~³ËÎÆH«ì]\q´,œj¸zô3Á›<:õ(1lY[ÿÞûÏ­Õ±Nñ7rèŒgû&ö ððö­Ý²ßìor•AÍx²m| ¯·¯‡Ç®xöù'ν;¹Yý1ÇŽ¯ÜÔ¯íÜgââ"Og‡ço­IÇß矉‘Š£O-êݪž‡·¯_›Þã÷«ÊLýç±¢Ó{Æi0er¯&®5ë6ýmšƒ ëFÎì³:ZözuW/o_A—#víÚÊÛÛ×ûA³¾‹N½—È?îë3âZNöåÁ}=¼;,rwj³†Ãn*ë$±‡z× ÜùVöULd¿ÀqõõÅ“˜ó+†µ¬ïëáݤý˜¿n'+!„É‹ügÁo-ëyxû6ì9ýïçY´²³^—y;– l×ÀÃÛ¿í„ýa9_\ãdñEêBEq…ê"næ…‘¿J–=ÞÈÇףѴ{¹ßêR‚ÖžÞ=>°QÝ~'>¼ú-{uíü­abBˆ"ùΆÑÝü¼}=êw°òrŒŒ(;äz#ƒƒ÷oäíëÑbÈšûé4!tÚýÍcºùyûzÔkÑ~ØšëÉté?ƒ5j¸{7î2féî}Ãìžo˜䣼ؾ±Hýdù€N ëûzxûút¾òz¢\öv[ _Ë•/óiäêŽþ=Åžâ±Df5\Ýjº¹Õt±Ñdqtì\jº¹Õtsu4á|<¹|nð;¹ò}ý¹gûÔîÞÞ¾ ú¬¸ûþÜ’¾ ½}};O>­Ü°,îêú!yxûzw±æf²âç÷WNÎušü>m„;~õeNICCÏ]'V mWÛÛ·~ï%”c¦,áÊšá-ý|=|Z÷]÷0#ÿÈ*®—+ÍàRHr#źϔm½kºzø·¸xÇ–aÕyßhÄbÒÙa‡&÷lîáíë×mÚá÷²_áS䪔 xÚ|çä3ŽÏÀNvE/»³µmìµÙ„Yò©Ó܇ ðÕã¨Û·xG«åÄEsFz&ÏX’DBˆìý®ÍϬŸµ|rkÎå ®¤üø:77÷@ðÞ”Ô]]½¾}ú¦§§ïÞ›SRqh±_ÃЊBtq³œtúùÃWG׿÷ø±ƒs&mŸ¼ø^C‘½ß³5Ôöó÷¥fá×wúÚ½»·oéù×ÌaÊ{&ä/wœ¢[MZ6¡™,d^ÏÞ+¢=†-dòlë³q "‹Þ;vìiaÏÏÙ0ÄøÚìÉßʪè]â×úP‡E(6EQ„ÈBW¯¾§ÕtȈN6ég§ŒÜí1fÇÞÝ›ZÞ[4bÑÝLF‘prÚØÍ1žÖnÙ¾°¯÷§©y:çÖ²MQV‡ jiFЉ|ö«M“Ö†»Ù½Ïö9x’¸äw‡'.¼aÒÝÁýÛ– m ŠO“©b¬X|=¾øÍýèÂSOC·Îäu-ÙÖ¶ž?wæòòZm×v#VnݵﯹyæÎ¿bÜeÝ ÞÒ£g.û{¨}q%0’/c’¦ø•Ž+¥¼Ð#'Ÿv_ºóÐŽ9F?øNFgÜ^2hñ«k‚÷nžàñvÝÈYç”Ýš<öØ©÷>3—Mé¬sgí¸­¯¾}U„Òj8C;]vIGÏž¹t|JMÉ·º”w;–]¤|zîå¦ùÿ[Š—¿yÙ»¿ÇŒß›ÛtÞŽ½û–v†Ì²9\L!tîÝ G¤'.3Ì9jçœ/%Ùw—MÛ‘ÒhÞ®}ÖMînŸ—­(s¬(µj5¾wÍÃl"}½k̸#’¦“WlY3¾ƒ-Ÿ0„¢H(®—ûÿƒKѱ\ÝHƒ$<M*¼4ÆhðXÅpŠb¦]Ÿ=rå«ß—­[·¸_]]ö/ð©¯([UÏHy–-I“"ãúÖ߸½­ÝzýÎ)uÔ)B©ñG?B¹©ñj5º >“CBX¢†óÖOðÂÔâ=¼9õŸ§™Í áX]¿´¿5—0îâSÿ,}#i¡÷ƒ…ÆÇŽNNIÖÑÑ ì©®®Þ½[}ö&'';~ä·ž¿—á:{aDó ÿ'j¶îpã/×Hº¾ãŠvßàþõÍØ„ÿ6Ì÷è䳑yáXÝðÅûróíÝZœž˜@êùè‹Î¦«B8.SVÏh­Ç’W{wðÌÑ‹÷·ç‘,rjÿ¬ÐxIÛÔ}ãÌXÛÙ]Dˆy»AAûƒBî&YWÉÒœo^+¢¥©o®nX}›xLöÔ¤ÂËqüŽíÙDþqß»w pæb7vÚƒN£÷>aôn×CÝ?þÝÁŽKHuõÐÝoåŸûðëÎÝ¿ª±Eˆ8tþב3HáÚÔ­ël£MÙØºù‘ÜG³ãî~nöF,{'ŸV*+M¯¡£ë [4 uˆ‹¯——_ÃÆõ´9Bu›Åjikks!ê†ØF–œÈõil»cGh¼¼½:Åfkhkë É+¶oHø"&¿Îqõé‚Ä‹='“<ÇmþÍ[ENëw%ðð?Q«ï¹,o´dbG Bª Ÿvmàž« M|áÚü¹rnK! ,þ ‹ÅéhkkQt‰ot)lÓ~›ÿ–_4i=ĮĖ’Æå7ΛÁ¯­ìÛÈœCˆÕôQ·Ú¬8ÚoêDbôxß•g}ÖÌìl¢ÝRŽù;EF¯4ûòí.…ÅײËÚR⸗ɢj5ô”g,,M[g½Ü{Ñ´%¡X\®rYBKžGëù4wX¶~h‡Èúþuëø·låg%d}ÿ)—<î[oDI{a•Å+Ø"-~ŽB[Fމû»ÏEÿiÇ>w)ÍuwÙo€¦(޲çe©©óÙ,eJ©©óY ©Bš•{c ÿa–òê¿D*«ž’ÇŸ•<|ê¯8gmQ[“"äÿ „%Ô²äy’Œ¨—Ùzuu>Jñ½\’ܤäÁåóC†¥SwÄÞ3mïݸyçÞÝK¬Yã?gçT³b1=¦Ø…ÏcˆU/;á¯ó©GòUN÷‹þšE•O>qü”/–ˆD¢æ>ÏŒŠâêØ8¹8šÉ|õU¤`8î“Ì÷V/»xšê™‡¾~_²7YpÙzäÆÎnÔûm½z_újØcQEÇ@eϬ`ØFA+7üaSpêA±…Úܪy`ÓF,ž÷Äu£]4ÕõMŒµ¸!ä‹‘¯Ž)†F!§ Òo¿X±‘×. ¶ºtöÂí{W6MÛÜyÛ¾1³ìltæÂõ»wvÎ ÞucÑѹ Tá %ÅÄŠâ¹6ìæÚ°Û€‘Q»þè²eË­®ã‹ÊéWLÙú±õ¢à­¬ÔÅwÆÌ*݆ÙFí¾ŠI-!U¹«¯bWš¥Ån…¡iÂpKÿߥ”¶¥¾õn¿~Êã•kÓkÃ?ŽN]¹s÷ܪ£;ÿ¼wû@;n™c%ŽyGL[q"Š{#ÙÇ”éOì‰SŽP}—þÝËT-íLŸÛ•Ç’{ûU»Î…7å]ÌtYWç‡>IEúÓ‚¾•Q0lû¡›×¶2(¼úÂU×aýÜþj¤åË ãW¼ÈÎ?,¤ÿhÈo"†–3 ›ýõ@IýßF.npù [ÓÆ§µOë^#ÓoLî:qãá3Š9yÑ+¿^È^® Ø,Ö/õ©¯¸çT= åY¶ÄÒ¯à(»³åØ1!,‘}uG'3ÑWŸEê«™Æ-{6wÔæ|+¹§3"% tU*ÿ®ë<“Æô‡ÐT¡ž¾~þ]uN±sî»Çq¾½;¸ð(BhEé:.––(%ô¥S°}}]MAU}šP±7 jšÙU³³4Ñ*öôŒgRØþøøƒ2£`ò¢dzͪ™ZjåDGeÐeŽ<‹¦9úî-~7{íÁíý,oßü(¥´š ›½j×Á)NÙ÷®FKT0VtÖÇ÷Š¢×LxŒTª ,¡å B‘'<{-±ïÔÈJMSx‹U¸!WÀ¦s²¤´òdßI¶æY­>ž&¯“ÿQ͉8ú&ÏIô:ìqäÊÞ•°÷ößlîØõyCšÚ©eDÞ:s>¹ý©žÂŸÛ_±4=†­û:hùØÅ6;'”~h:uôá\±hÝÚ,ýÉ?;OgȽ¾ÕËiR±eØWñ‹¥½–Q­‚:Ö«aª–yiûΦ]gÙ[Óˆ‹;³pAó›;f¯5ëïɉ¾và¯W2C¿_l4Aò¿º ù’8JàÜoÃ~óíëöîš|4UFغv^†ÏìQƒOnYMÝsô´Îã­yNͦa÷î Ã6çäÿ>‘¼;±pØ¢dZǹÓü՜Ԉ¼òw%,½Fs7OY½lÇÌÁÛÄ„oäâßm°Q±EEüê¿Ïé9}óäA”‰O׎Mÿ +Õ+ð«÷_»Ž·|Õ–qGÒi¢iíÐËNHUÍû;ÚˆmØbÁªÔ¹‹–÷9”Mi;¶™°f‚·&‹r±xhâô¿&Ž’iÕhÝÁGïU"SŠÈÛTKÓ?¾kÚÑ%bÂ7©ÓcîäfÖÃoŸ=pCaë»¶Ÿ5·[cÅ5o=¸Gâ«'mÉaWÏ¥ùè-£ŒØ,­¡CüÆ®Ñw›~Óû'Žo9yͬQG´ª·êÝÃýE0!„p­;Œëvsê¼!gÎnßÐsÂØ›“ÖΛp¨N×ßzX­ºI( û:úNJƤ±Nå¿ç–=\=v@Áÿ‰š­ ™¾z|ÞÊq¿¯’óͼ»¯\ÐÝšË"Þã7[1{óŸÝÒu›†ÃVMjeÈ¢?B˜ôÛÆìxŸË7÷¼|V³ÏK÷Å/–vV~IÜÔ–Ý»í9<ʱñˆ!“— ï˱êºmרÿÓ¥°ôšN»ä–Ú<º å­z-_œ=où”?vJ8µ:ÎÚ0¤†€äwR£ëîÎ_ºeüîdY5ê¿`´»°t±’Þ]2¤'!„¥næâ7pÅÒ^>&r‡¾{— @û¨K…ïΫ…=mÝÙM½º9JÝsÔ¶…¢…WÚ›MغÕëµhÎû/ú+®uç¹K^ý>rÊÔj;g—vh ´üÆÏë;uî†é×9f>më†îc¾ÙËIË’<\ k´ëÈž™‡R¥„ˆ,½ƒÍì,`‘b‘¯Wýë…B«î‹'¿›¸jñ¸CÚέ;u°|{ë—MÊçtšòã[™î€“]PA!!!AAA崱ܻ“Û# Î-¬#RÕ÷õ6ÊÖÚöìuxTG‡Ï•õîÆ=@BA !¸×R\KÑâVܥŦ¸»[ð‹;ñdcë»W¾?<øJèf;ïsŸ§åîæîÜsÏ9¿™33è¹2óŒÔOÿ0•Z|~¡¯Ù ùÕ?†Ê8Ø·ï…·ö r$‘­¾¶èæ´^KÍ?;§®Ù QÁOçÿë] Y/ùÿ~< —ŠZmɨù·6«GüÏH{xïc‹:µœEÊ„‹k¯bM×x­_!þm[1ù¡ûñ[lª.D¶BãÓAâa¸THÚ’±óo b"þgÄj²îïܼ»H„Eõ q«¦úŠ1d+äWˆÙVTæµ#/L[N6 -ü =$•O?À·\mé"l°üê¯JXTÑ|uÿÍž‘ Îä]7&#[!¾½­HÇ~ÇîôC¶úÿŒçÔwÈV£}:h©V„ábii‰ŒðÕU ê%BÏÙ Ù l…l…žˆÿ ¥ KHE|•`ajôŒÐ3B¶B¶B¶B¶BTÚ§ƒÒ–†®à§-EFGþ§Œö_»_ôŒ­ÈVÈVÈV$ˆo¶ÚÒÈãZdL@ FFè<$ˆw@«-!Œ n&ê„"®LÒG?D"`ª™ëRä-…Œ@ †C×`@«-!ß 7u’Â$ÀÛÚQ*DÖø€ìBõ“Ä|7“¢ØB²@ ñ€@|hÀaÄ$qý½­^z!ê]ÿާ“å³x2@ ñ€@|)Æš¶´öè´¯úþ¤W"g0>djÜÖœŸ†”Ã'0ñdjÜœ,@ H< _£¥-!Œ=Åê)´ â'ŒC#Ë Ä×`°R×Ð*ÕŸ‹Á‡û³°€gj¬ïåºäÈqµŽ²‘<zË壣Y-…6`ú„qÐ @ ñ€@|›¶ÔØ&ÓÏ…okÙ ˆ>¢iÚ6/ë¯Ô’N5í“eº§I6Òÿúw¸ÐdxO·nBY0~cJ¸îï¿Ímä>ÝOdšƒÛc6gsl­§Y-yø¤²ªèÖO8a\õêW^Œ‰úÊgÀ3[7Å5wIJ4$ü„áé0¼´¥*fÅÎvõ¥+ÿp‘͵~Gˆ²W€Ù£L,_Áýon1 -·NrˆÞµ!ûÛG93ó{¸óø,••½âBnlù÷Š{6ul£Ú}¡à¥J÷’c±åWwòô‹Q/ôå–Iìæ8Óv#õn¡>µÐÈk=Íê*T<0JEø¯7wå×_}È®lÕ"½*|ñÃÃ{ò‹h®SÏš?-ww°tÖ¡§;–§¦€Y}÷Þkû9ã@k^,¾·{WB`¸¬Ñ€"´Å§Û….l3¤)«`ñð±e[¦¹×~÷L^æ¸'–[리J|P®ûqMæOòðº5ð¶Fÿß«?9BQ¿þíbÞR—Þ>Îõîì:Ô‹/¢t¤.ºYRÀ`¤_3·¦v$“›½ôlN¤0[oÇùAVµL°’Ù¶Óigsc}!ùVÒ)ÝÚÙ‘¬Bq:äåÆ(íÞR¾Ýý]¶u%—¯NÕþWý #ýš8¨mâiAZÍíÐÔ¡Šb³õv˜ÜØ¢¶ WŒÑ©ñ¹›/f‡–¼õ ¡íºavÏb{]PjþàQ5FHÞþDîý˜ÞWU:!ñ€@|-›¶Äåp|!—ËýPê(©‰{úÛÇdæCRr¢••½q>‚ß§¯{碌qke\ÓÑÜW©{•—³8F¸:Yá™»ÃÕz ¸»O¥`éô'Ô &µð²s6…æçþzäõš¶¤8ÿ`öè<ÞDO1¥&0y»ïÀ‚¶4®%*¼<îɦÕây³Í 2fÓ´L›ÙþÓý™èåO·à;œu·ˆˆÜwïÚÑ'îù¢ñÏ£[Õó6aK.¿¸+r›ÀÐLEGJ:ºyCŒ,mv–†].t ´idéQé3Îä§rVq]-§ŠNDh;z=­7ÿ@¬Z¢°¨Qíx{jk‚é$ ¼o5ŒÇùœúR€¨Ó/1˜ØtXçÞUyZ÷ôqÆÒk…r÷ª{[ð p>5×ròc­-²EÌM¢€°°Þ>Ö:ìÏèm¯Œ'WDO³ºŠ›,èè»&Ž{ksE³:(ÕÓCÅfƒÚ¶q@Øsrú‚õ/Ó&yàÇ3ò«Wû“¹”û…Î÷;¼ iç] ´·sµ¢x<‰•&19GãI*®­.ñ^ZÓ‚ÐUülæOL˜f r/Òßú€‰—í¬vš_âäz/Ï3AÚaX·&–î¤îÁÍäy¡J%ÎiÒÆµî_÷rY×:N³[I¼E˜Z®»Ÿ²ð1×Ì|ô û¾®uNÞŠÃé7JX̶ºÃ¯m­üÌq}±üÈÅ”à= ¸Km§9m¤ÞB67SÅ#!×Ћ‰»Ùø¯÷ˆ=ߜ㉺V'ž\È8›¢g@½®Šå:?SËèßú¦ttâ %(6]µhÕÕª&_žá)õTä ¿SGALHVÐx»N6Yq™F˜©Å³–´5WïÜŸû …œôcu¼ºxóNÜ}'x-×nQ²|UáÄåá7÷3ýˆòm‘éÐ.Î}«ñ •‘&Ûp23TQÙ;EÊó+J½û`bÙePî5¼Zºò8‰ª§—ž–}CYb/Ý^gŠC! „‰Å¯ýí¨›IÛëzŒ|§c@U¤ŠH× ¹N•CD" \?€á¥-q8\›œ§=ž5¬‰}ðÕ”³³Ç‡÷o^56—Í-Ö>xøÌÊÆIlbþo–’RïI‹eô§÷Gt\õ|ô˜G öîë[ˆè¤”_îëèWƒ×<ï°7ûD:î_W$ÀlªIÜŠ Bò*vÑQŒV_ÁÅ€¾ì‡Ô%Ié`YF«gtqãï; £*^º%ævmËÆî¦º[÷÷Lê{F÷±c‰±É×c§<;Çš¢9ýð•>]]«W.&1ã pV«QQŒLË:ñq€À9ŸedZF®d8|¼L•“‹b<ÂA¥ CòÊÜ ãóqJMiÙÏÙíó/.û24îÇ,Ëö^¦~µ»4‘nß³Ëx‚ M-f t”>MýàÝ|$&?O•Ÿ§J!ÌÏw³ö¾š¼lk”°ÌÔdãžd)ã®–È߯³r4 («ÞAOa¸XZè .Ÿeðq³:<§Ë…¡ùé ×CÃO-èV¯š Ë2®$·²,`äÿõ–ãaS&1G_îô5œK °7ñ[(Ó3$ÁÃtÊó±tÍÆƒõ—#þn¹ M‘*Ÿ'ð—i k±D§IUвROÃåxUûþ¼äð¢WF×kW:aºbØ×?ÄG'VöD¥eu4«•Ǫ4<‰)mQ‹ÐÅ)J(Íê´™Š†ãàD½smÁÙìHËÀZÃ2β £Âp`˜Š+ù7ÞaÃXµêÖƒŒ%cžSŠÜÍ]Ëí:£õ±¬#W§ÔËe‡\£)[G^eOY§êdé%-*‚98qÙBe¶VU¶¯³üER‘¥‰“ÓÙZ̲,³Ÿ ªò©ø<ãì8WÉ”ùMËiàcæÍê9ô— µc"[ËvÕ˜8=§a§:Åy?e2P^v·.¯ð¾J: È:ò’,c2°™XŸ©&þYQ‰¯•{^ælÙߺÜü9ÖúØe\)Èšÿ$έƒ–¶-IÍ{U›™+–$_²5PÌ·‘?¸¼FɨµÀRzU‘–á\<~ä][sÛWR§¹»^kÒMjŧ°nbóݲK{ˆ&u™„•EÕ,ª;Qo ¦ÔÜÙª÷^+ãzÆ™g¦TËzk:_OÖw¤i¶‚z£©oj‘»ËRÍ©ðâD áëÎ¥ rÊ/7õø®L1Èmyë´íQj@PÛÒòIÊžgJAg—ÉÙ™wT\?;?’½`ðÞ…“„KðpÀI„O(´´N§<ìoíØI™›*–L¬‰?=W"côO—Ý~ŽO Qðhcª‰KŒÐ°ªØüØV+6'05Û{”¬|eœI7Ú¼‚B›!m­Sï*¸U0SïŽÖê÷l]cg nÉš¸KŠòìÆ&àã&\ Ç0Ÿ³ŒR÷ÎðÁïÓËÁ".ïZºŽ•˜yò¨D£P_û•ž+ž8¬J“ÜŒ…/ôV¶B+ ©´BÎàÞv¤‚{ÙzÂÂl`aQtfì'òx¶öëÚq>-Œ5ê;¶ÄŠÆþ—WFâøvú /méa:O­Ó”È#K{¢H€ž~BeQJd0,¼Rò"^ñ A<€^¹ûŠlu'÷?ý©°ó/Ó¾@nÉïØÙcŠ+ÊÌ[x +N_~ÁjJÖìK¥;Ú¯çÈaõ1/ÒÇ]., šWÕVÕžf}j±œÒÿҚÇ“Ä]œ ·ã1úçS&ÞPj>Jrb5Êt6Ï Sáp?Ža^a¹äªËY+'—.¡“»ÁW»ÿÔ…”ô³ë‘šsy\æm·îlÝw’‡xIú,fN®ÌÞQ ¢ÚfÝ·XZsß‹-:Wç*ùÙ‡Ä1À],:Óœ’uŸfÍ&ÚøØT\ùñoze]AIЉýÌ¡"`eéùËNäg3å$(^fŒ9ÆLié´¡!Œ>!^¶³€ÎMOYdç>-Ƚ½V}#,/Î^jøÎåÙºÆv€Ôó\ þÄŽÈ5™ôÓ+‰[:»Žä%¤uO$/ŠÐ1²èÔÙÖ®Ó»zþH0éq™3.—@aÞ¼“¼ùí܃›bŠÜ‚Õ‡3cŒµ+˜Rí>’*íî°æ'’Q*ÏžJ:’Ï`€Áë´Ëòí† -ÖNuó€…ê@FJ§…o.ËP‘éìÄUû‰1Ðjnÿ•´)Íâárüj]1ÏÓ ÌÌ×T{ó¾NX—þ,ƒýÙϹ«9A2ú¨ç©ãBÞOOz÷ È/´=ÚK†s 8§`ý¾´kòÿÀF?•ÌkAÈ?¿Ê\YPP²&âÛòMBBB6%x|Û‚™ ÈFfÕíEäG³)šÍR†Æ«?9ª½öè´¯ú¹I?®ü.ö&üzù,…änÇJþ‰îáXÛïnvtKìÉr§(pMæO¬fsþÅØè/[fˆ+þõ—ªV—"§ÆPFÖ˜iXŸ*vÑ9Zô¦—‹•˜Ìy•kÎÇ)âS„Ϋ÷ÿEJ!!!‹ã%ÿߢ‘„áb°iKÅjêâsÙÅç2ôŒ>¯VOj›“u÷ã¼W’ãí"rs³iÊ×ÊÿÒ”ëùâ—”QvCñ¸¸K §)MÉC C‰„¡ë0¼´¥È÷IøWÂaq¯šdTHqþGÁ>.4›Øß¥¥½s5åÐ.9‚q60ƒ¤ägj#µ‰ó9(Dþ„qe‰â«0ØMâŒúѱgmÿá5´% W>)÷¦$Ä¢ü¯»«¿²ÿé#5·DjŽFʃKb:N"e>#Äb°iKÄ?§c þ¥øB3ss±€‡¬ñ%JM®¬¨G þÙH4'@ x@ ¾F?€Ñ¥-!À™´­Vr9:7QŒñ!tõæñE&H< Ä—FÌÙð‚.õ­Ö2·¡zøC •Ôí¸¢³ó)‰âKùViK¡óê!c"•W)ÔuOhëˆL@ H< _§¥-!Œ¥F·é|Ä…XZ¦ª¬Ë I„Lc{E}å·_‰{;ûÚ>µI5RÄñe ´%„³é|Ä¥2ÀËÞQ*¬¤·]¨OÌ×i3ê[}Û+‘‘•õê¢aC@âø2ÐjK#æB,íïe/ðÒ ©J{O'˰8å74Mëtº¬ì,$‰âëô_ÚÒ1ìë¾ß ­uo„ÈT¸­_:Ô IDAT9?­+3¯@Càø·Ï¼bY0ä&ÄñÅ „q£§X=U¹•¡žf€ *d«; Cê@ x@ ¾ƒM[Rª•ŸÜM|Ï0ôá8QÕÕ£^F¯çÿS°´—»Ÿ»9z”ˆrÑѬ–b*÷-P$š@â0 ý†—¶þìF£ñõ ø¸Ã•¦éÌüì!›6«éhÎg7]Œßó‹ÿôáQ¯®¯›³ôxTæûÆÚašú4j1hÑÉ$µñN½b5)g îP? ЯãÈײ>®µË·0êôk [7›xGñߪ»˜âÇ«{úöÞýRšˆß;ù¾=úɤ©œ›& êÞ$0Ð7°C¿'cUì§üŠQDX8º{ûæ¾þ~^x2NÉ0…a;fêÙÎ/ зu¿){ž3€0ÐÈÂp1Ø´%‚ |!—Ëý°ÀJjâÄžþö1™…E´*.!á1ÿApúñ ?kÚŸ¹¶ºÓò9ú¼ØDçÀIƒ:Û >ŠO‹‡ï¶Äz¬h<µøuä§•_Q`BÑÅY‡¿HiZkÂZù鈽??´½ãï'Ñ>ü5"5¨ášéœ»ÝnïÝg3k´ˆÐ_]Y赸–þqiu46ò@ÑåDõŠÈó)KS*ül¤²IC1ò¢O ‹Ý4°ÿþtÀ1ð+(¯›¶6ªÁ¬s«^Y±xÖ|·£‚, ¯/™²;·ó¼-­ÒŽ-ù}ÒZÓsý™§¦Ï=K \´»©0rÏ‚¥ÓwzëÅ3FSi‚§®¸é2výŽ:ú[æÌŸãè<Àéè¨|»I°¢cºÍ Óàÿ­áeV»kêÜ Þyÿ0סkg60Åp¡ƒ®|KúXð‹+7ëæ¦ßWM4­~f²7‘QŽ_U+ŽVR¥ÛøÞÖLÊ•­¿ÿ>Û²æÁq®²Ú?Lå!.x°gùÖéË=N,oh‚&B!ñ€@|^?€á¥-q9—Oœ§ÓÿŠ“-éáÚØÃ<«P3a׋-ªÄçê3ŠÙÑ᫇7ö÷´ù÷ZÄØÕ<×Ãó!-T»¢þÑ9ÏºŽ©~{ÇéxÊ5hÚšQæg/ßû¤Dê7tåòÁ>buØÌ®“‹z´üëàõ$…™wï™K§4,\÷îEÆÿœ²fõø\%B§&f,âkއnÜ™ÕiÍ‘‰>¢×õº"têÛÒhXÙ>p%xϾòg+Ååõó6ž,"ljxju`ýºÒåÜÝ0öÏ}áynóVNncC•wgûеÇî¿Tò­<ö›Ô£`ɸSËO/mf‰SéGG÷Ún³ü𢖕^”èiVGW|.¿šÈõµ¹KÎÇÇÒÒ~ÍÓ”§rË>sm«I1¨âuâñ­¿u;¨R0§‰<«Ò€sáA±f©½sßÌuR–auåÝBʼnš¦?ÖçÃñæS'–,_}>JѰAYôÂ?Ý?kÎþ[/µVþCçÿö“¯tþƒ?–®9p?Cͱ©×cÜ‚_Z:pTa3»NÕŽ™â²éD†WmUhŠÖ÷ò_P{ñÍmMU/Úp<,K˵­ßoÚ¼±¤«yy±\×5p¸Õ9>:ëà Þ‡ßœSEŸ¸¥ô›1®‡Ÿ%®ÓÂ.>&kðøäc²Õªqíꈠ¶Ãø!‹N¾˜äë|ûlŒ´ÇþaÍ«s¡Æ¤áç{ì:“ø“W ¾j‡äK—²«ŒØÐ§‘“ú^ròFVïÁΜ¿·[›Væ-·Þº§ze’å™ðK­Š Ì ”)õÊ|ËóJÏB3(ó•Zy‘† ÊBBhÎ¥LIƒÔèzÕ»VB`Æ£2Š´ ˆñ¿µÛg…-OMSнú{ºŠÀÕ£N³Ê¯³Ô‘Û&­Èî±e]'GúN“èÐaÆÚæ¦1ä==±j㯓Ìíéÿvè†Ê Ùõ˜h8¯®¦ÿ´_1yçG-‰ÿI›'54{×ј¼»®)ªnjƒÄÄé0¼´%Éåñxž&"Lc%®ŪZx9=Ë¢tZuÈͰ]SÚRò8ƒ4'Fr pž ÀH¼¬±s˜7Ù3¯»z0¡SMkörFUÿ½Æ#ñÜúE[ÏF×Ä “SnzšQ«q§Þƒz¶p&Åÿut³É”9"Îw®¶¤Œ:ÿª;»–ÃEõºx©×œQúÕ{﫸Y•j¦ªÄœ¢¬Ø|Q5oIi‡›º×¨¦3΀‘BîÇ®¨ÏL–gÝÑì8À2ZÞ3/óÓ®[9Á⵩Xšaßœ%ñ²³Ì›³N¼ àhš5æ­*1üU€¡öKíö÷½7Òm=VmÓ-¡I³~ÍÚwhä"¬ÜÒ‹•Ç?J.ŒÝÒ·Å–×§‚{tzùÇé~5›”i#ïªÂ¸£Cå÷q¶#XUÌŽY›Ò~=ØB‚Cñ§ý ·l¶àKVRèî#'{6ô{­>t©§æ,{\mÌŽPÈŠÄñ9 v“8.‡ÃápH’Ø?¥E·ù§‚šžPÙ‹¨+w™Û¥¶›äAx²¡·•Øß„hošHŒäqÞýL—´oò²®¶†ô¬e…¥í0è:ÎóX•LU:òŒ ­­EÌË" ûV<° ŰcŸ)SÙ·¡Ü… P)J´ÆÄè(¶4íç»´û—Òîç‹» ÆÑê%¤¦ÖªõZ=@ëåjà¹14£ÅÝL{œ7íÀùÜV:Ò[ó4’¶ÅåqÇæåyy Eš–=--…C…íóðáȃ*îì½b¥|zó†¯Ï¼¼­ªWíýeÀIn9QÚg‡¥Y¢ê˜?6v°z£„8Üü=‡?ﺕ\d)\[¶Ä’¦D‹‰\„‘Tir-@К &’y&æ<½B]Ö½NkK4˜ÈRhŒý¾¸ÐRÚ×VZ-×qLÍyøçìöy[pÜl9Sýêù›Â.¯;¹ų̂;GT©ÌëÎá’6ËÕÑ”ÚÎ<3câ­ÆëÖ ôyo=ÇÜFŒÇ«KýI—vrîä½Ü¡Á ÛÙ‘Àù¿"L<}<=Íâ/;~#«×•{cñøMy]~ÛÙÇ Ív0 _@&@,¥ K`x“x<>Ë2àãfuxN— 7BóÓ®‡†ŸZЭ^5–ex<™VˆcÀüËù0ÅñOr…–ä;Q§>Í6 Ô­¶`J;Í9ÖÞî¼ÜÇÏòJgMÈ32”"''“wêÒÒ] Ù‘™ê/âpWaþ³¨ú­ô`)~°é÷»U†ªŸñDz3Ʋ²fé„éïr¨äw×(„ý$žV¥?ªxs¹yÊWrÍêh25³¨ðîŸ(KnoÖzOŠ =ÃÎÑÓ¬Ž%X`)šyóµ7#Á‡Ú!öâ=­ÏÔm{ìß{dÿރ뇺ËïŸS¿÷%ª *Fnåãléàe¥ŒÊ/õ¦$1R&ts5Ã?T¬80 @J½‰¬ˆ®¥D*-=,MDÒ/wÝJÐY(ñ¨"”E¥*YmzD6æPÃ^dããDdGdjXejt.ÏÅÛJäPˎ͈,Íì§‹ãåfžn&Æ1`"o+eR\i•£Ï‰È`ì|ìyŸµÛç#Y†aù~GN_ºóÐæ)·žÉ*ù¤ÂÄÎÅÝÍÍÝÍÍÝÍÕYÂÃyNNR>μíaJâ#óI{wK@—vzî˜uÅ?l];¨†°ôeæ~Þ¯(e‘šÁ  òn-½4ºé²?'ø›£pÕ *d„ë0¼´%+©mvn–½#I’~¶gv?s/qP›Vn¶fEeçfYIm ¢Uä[»Šd÷.ß‹K=÷KÒeü• ¡\/AÞÝ]kžØtÛ_C€ÁÛ‹(ñªùÕ}Çÿb«ÓQgÿø#Mï âÚƒ:YÛ¶d›íئœgÛ7ÇØw›ì#„Ì·‚À¯³jùo¿UÕÂ,çþ‰ÝtD—OAèݯ“í ó7›kïL%ýuê¶Ýèé¶û]‘ü´opLp«ßÖå!M6t4†Ü%Š) ¾+ùå܇‚ŽC œÕ—Ê2²¦¸º8ûêo‚Å™ü ¿Sc–}ý)[|67ÆÉ|˜M¬'þ4R­ëÈ‘EP‚ZSlêûÍyPÇ^¼§ò×¶VÕ²¼{W~7÷Ý;/ÄjtÁÓs!÷undú?6½ô»ÌMl×¹wµ£ÁKw:OhcWºuýs«NÖÀ‹÷b!3g["íÆÕõø”¤îàⱋ§lVn]…_œpïâ•ü®K‡¹ëŒN©T)Ô4Ëè”r¹ÒD$â ¼~h.µiói‹Üd—W^ÑÔ›Ù@Jšûõ¬O-X·µÑÔÒÔ# š¬®%&EͺzoÙ¶rÏÄF¼»·ÇZ·›UÕ—Zž{ûŽöGþ\wÌm¨öÞæC9î?·rà€.vSß~¤‹ÎoídYžÝp–R)Ôr•Že)•B.'bùÖeõ©ûfn+lÚ³]m[H½­4óp75ÎXýlé˜]œ–š{™<<ðûSq» õÌØ‚ G.‰k0k^#Nf| à<©‹«]9~Å•]Ÿ=ã/×­êºð‹"În9+÷šÔÌ–P>[ñóœ›îc–µ·ÈÍÀ8fÎnvB¤"x@ þƒM[r´uz•——Ųlé[ÔÓO¨,J‰, ÃÌM-ì­ ¢ |¯Ÿ&=[1{ÐQÓ†3çº~I´¦M=»|ìŠ|Æ¢F¥ëöâÀ;™³qá”ù̉Ù5ìÕ½»ýŸÑÂÚc×Î×.^?sÄLR»ç­#½ðî>K¸MûyKâf/[;ç¼È­MïîÕcÎ}º ‚š£7üË×-¹[fU›™˜¼ô¯OpoWzÌî{jàºõ5\Ò²ò/¸„cØwɈQk¬U‹ú8Õ°y›l ¦â mÒ³3dûN1„ áFÛÚvï|ªTßÛF×Ü`fNb„û +× y»~À(®IÇ BÞÛ2ãxîóð¾vˆ?Wá9¦ÞÛÇNÚ6rYsðrR]P>ß7ÿHº‚°òðÛ‚¾Î—«S,Y=kèn-iU·û‚-£½ zÿú& ÆŒn4eÃøa;¤­×œX2qÇrÑò­kGPaéÙ¸óG¾M­/w]ƒB³±ïÀcù/G¶Ù/é½óø oq½ ¿OXºdÍØ‹JŽ_ߥKÚYã’–³W&.Z²xôA½À¹É¨µS˜aÝV,LŸ½fÖàŒI•¶¿®QÃX·ÕæVýé÷¹ ¶ŒV‚[Ôè1y?€X†¬|»±Å7ƶ[ðfv ŸùׂƒÞ.&@Hj׿­ ž¶7_"—×Mª-4Ò@ÒÖß‹ >°ìD‘žÖì:wó?ŒJMI,fr®,såõ÷¬ûï>2¥úÇ~…kÝü]OÙº`—L "—F#×þÚÏ™Ãʲbr˜âœMcï½iZn»°ÄßH­X¹À¼„üó«Ìõ!k"*H?üÒ·o_ú«c_/öú>ɪ°™]§Â²ËËýDÈó*ž³ûT±‹ÎÑVê»°“9¯rª—ÉÌÊlݲ5røþ¿)$$dq¼äÿŒÈèÃ% süø‡ð¸¸[¹Ó¯ÜŠœ0¡ÍdÂà@âa¸lÚÒ?¥‹.x$ÎçTîä+‡ŠK[B Ä—c°›Ä)Â˯ÞAfø^H Öèø•yäKb:ÎZLT„xPkÔ|ù @ ñ€@|~”¶„0FzÕˆ,437 x•ôJ”š¼‚â¾ß~£Z£–Éd¶¶¶ÈO‰âKA#æ§6UYH8þ47Q]ißPÛÞUW•ŸŸ”ò¯L„½½«‹+ò@âøRPÚˆò8ã;yï„,@ ˆÊÚlaèúPÚ@ „a€F† p@1JnÓùˆ ±´LU±8!ÓØ^QßFY™vbÁÞξ¶Om’D@ ñ€@|(m aÄl:q) ð²w”V쎩مêðÄ|6£¾UQe1AYP¯n=ä*ÄñúPÚ¹Kû{Ù ¼ôBª‚Šãéd§¬Dâ¦iN—•…Ä@ ñ€@|)Æ:àp ¾nßÜ^€6•3Bd*ÜÖœŸVáÊÀLÄ+Ð8^™&¹±, hƒi@âørPÚ¸ÑS¬žúÊPO³P ÷Æ0¤‰â+õ^Ú’R­|øänâËx†¡?øljª®õê4Úx=ÿ¯˜‚¥½ÜýÜÍÑsD”‹Žfµó=~ˆ Tá#‰„ñb°áÏh4_¯€»rišÎÌϲéa³šŽæ|vÓÅø=¿ø£G‰(=Íj¿ËȃŽ@#ø }†KiÂR©Š0¨‚ed§:ÚºÚÛ;ØÙÙp˜Jlÿ 7mYÂ!x8Ááaú±œlÑ•¡Mº­ŽÕþóà3;tçŒ!}[ýzOù- Ç<Ø1{PÏöþ¾ÍzŽ\’úéb²Ê˜ Gwo×Ì7 0°ÇÄ wóJ' è²n­›Ð§y` oëþS÷=+f*™‡ëiVGUø¡•? ߨ5q}£y —-\³çLx.…A°%ñWü±|Ѳy¿ýyä^¦#‚ÀiÙÃc[/Z¶pí¡Û™zœ ‚ÀÕq‡Öì~P„ßìƒI%7 ô-=¶í>é;¹ßjƈâæè@ßaòcªAY}QäŽ!-»'ëÞ¼vò¨}3û5 ômÚcô¶0Yéý2…‚'unèЪnjý/ä¥gõÙ7ÖëÔÔ7 Q‹A‹N&©wê«I9»`p‡ú~G®¸–õq­]¾Ý€Q§_[غÙÄ;ŠÿH³LËîœÒ¿cƒ€ÀÀž“6…æ½÷Òy×vñ øùœŒåƒio^Ø€@߀Ñ×JyÄá…:·ð hÚnäšKéïÕûLñãÕ½}{ï~©„!ƒF†®ÀðÒ–‚ð…\.÷ÃÒ*©‰{úÛÇdѪ¸„„?Æ46‚Ö"÷êÂK_ ýeÍR‹¹ñ¾Iàœÿ<†©óã”QÕÌ51gWm\4ÛÉgO/ûrûÆ™’øð|§ÎãúHÕϬÞ2s¡ÛÉ Mâ·M˜sÚaø‚àQòéå+¦Îµ>¼.HZ‰zDôß%m‰)~‘­ð´hÕûçÎDî‹ëgÏŸ0qÛNš{eï‰Ç–-z¬Ê{õøô™ÃÇ-~\[¤Œ¹’íõóÜ–‚Ç;6œ{Vgl#)¡Ï û+Û£Co+ñ}G(ú#m€sL]?Þ‹Td=:¼6xÒ¯§‚túíÏ{èü%Œ‰ñô§éb7 ì¿?p |$+¯›¶6ªÁ¬s«^Y±xÖ|·£‚, ¯/™²;·ó¼-­ÒŽ-ù}ÒZÓsý™§¦Ï=K \´»©0rÏ‚¥ÓwzëÅ#D›ApŸËi½*êA²’$9¹rzìŽ}9§²Å콇áËÖó÷´ù÷ZÄØÕ<×çÒÂ7 ðÇ}·ïÜ´ßö˵ñ hÑcþÅ—Ù÷6Œé\? Y»q»#,€*lf›F£·o›=°i@ oÐðßn½¢@õç†gõ—l^48(°nÿ€ÚV¥-EÑ“?§þØ0 зy÷{4­7äÌ+@ÿêæÆ_Ú7 ô hÛµß&ï(Ø¢«£t\tdßü[ú6ê>n_Œ’àyŒ\µ|rïÖ¾õ[ö5  ö*¥Pÿ‰2v]Ölœ5¬C Ÿ«¡“‡TÓÇßOÓjΜÍt9}`óšÕýºLXÐMpÿðÜJÕs¬§Y]Å”EÿF–Ij6ö¨æÝ¸} WLž«&÷IxMÛîÍ|\<tìíÏI¸— ÄØ¢ÔÒÁÃ^È“¸»‰ŠR YSÅ_y*hÞÊ]DÿòÈiæìáåUïÕà9ãk³1·b•ê'³Û4ZÚ÷KgÔ ÷î—zUØÌ6'9òÛð¾íFoxTÄP{5鳿äö =›û´è>çÌK-PiçV/>’JAù M Y9¨]#߀Æ-{÷oÐaÁsµa;·ú/GÂCO­òŽWEŸ¸¥ôûe\?¯:mGLkË}x”k70ó–[oÝ»»©µèù€Lá£?&ÿØ( зq»®c7Ü®ô[úŒ«§£%=gŽh]׫v»Ñs‡Ûǹ”¦V³wú¬Ç–Ïkõ^ÿ i^ŧVZµêÔªU§¦›&»wôÞzʤ.õ¼k6>w¼OÎùñj]ÚÉ9ÓN»N[ÙˉÄÿ‹Á¦-q8\›œ§=ž5¬‰}ðÕ”³³Ç‡÷o^56—Í-Ö>xølãȦíê»þ›¥äU³eº7!é¹ñÔõËwt“â@Åî:ÏtøuÕô6ú%ý­Iñ»rA_»Û—_ʦõÓ£ÌÚÏX±hB½¼#ó–…ä1š¤¿ MØKÓÚ5 ¬×¼ÇÈw^QtöéYSväÌܼsùð@Iim¯OÜ7eÚ)¶ÓÂÍ{¶Îàc‚½NtÐË.oºmÚsöo‹û[?Þ¼ìxúÛe–’§Ü>r2Ç¥s'wþ'Êï‘´¦D"©ˆ U…J–o&(­ÄøÎu #2K[™<\G1Zýw:×—$Þ¿ŸcV§‰‹׫´À5pH‚$ m È2ŠYI`€á$A–þ³¯Bo¾òjß@Ê# ò;»T+ËP4pÄ¿ 4UØ–º–3V.[#y÷¢Ýe|ú´ýÛ#Ý/X=³#ycͲ›2æs¥ŠØ:zÁ ³>Ëvþ¹~V—*¼Ê¹z2U¤¶ôrã<ÇZvLú‹,å«ÈtÚ¶¦:×´Ö¦Då+3_dƒ}M;.aVÅÓ´0*YÁa;Ã*Óbòn–Ƕ–3‘õ{eEت9»d-–ì9xxÓÌ>UÕÙ º’ÛŠQ¾[ér¬kVáåDdjA—zzîø³Î³×Ž¬gú^TɪomèôÌ݋`Ô…J–g*(ÖÁM«Ö´”Ç'•0tÞÕeƒ‰[§7‘"éP@iKC×`xiK—Ëå)4jŒÖ„¦h{4tJÜÆ+ú­Pi¯Ý×NÚ²®ó¿]LB`"äàßÄܜڲæ¬õó:JpªZêÑ‹'›®ømxU.Èáü¡‘9Ú^渨ù’ÍÓýEl]nxèì3Ï‹ýµqù”Ž­Ö{i;*öôŠßgN‘ú³Á•½Ïl†×Å•à)ŽØ}è€&æÐ±tŸ‰kF·¶Æªâm½˜WVÑXvß´aJm@CÞÍcÓ¦i;‹@Ÿ²«wß?_‚u›9›Gy Tå•¡¤Ckó²0’•?=r&áCw.?¿~lÝÞCêÿ\ßR“+£uæÚJÔQ4«ûNKµÒÔ‹¡Sn˜€À£Û˜nÞ&$kãagoÞMñ ª*Ödd)JÌà<ÛªÖÌ¥¤\ª ?)YeYÏZ}ü‰I›±’¤sÛŽ=ÌÒ‰\šôîß¡šèût;aŸT´üå­­;¢%íÖúˆ>Ù!Ž /ß17Ð@+‰8ú󋏯Hç1[Vwå[[sþÌÊ'™ÚvÕþö¯JdûÎÎY4¨™à¦9ùÇãÊú(Yž ¿Ôª¸ÀLJ™R¯ÌW±<1¯ô,!4€2_©•iHqY„МJ™’©Ñõ82ªw­„ÀŒGeiãk·Ï Zžš¦{5ô÷t«Gf•ßV‡ºÜc§öÝi>¥™“™£¥ eþ½Õãwc¶Mo!%´¹ïô_¹÷Y¶ªÄR Ëº¿wUð¸y¶gÖ5õ©ašeÿ¥n³º¸‘IQ© Z«,~±mÑŠì[ÖuräЩ(îAâø'ìjK’Ëãñ2·Km7ɃðdCo+±¿ ÑÊšHŒäñø"«*T•å+ð$¦XlЦ˜z…Y†bØÏ.®ùþOsÌ]<Ì]<ªq_}ìQa›啃0E6YÑpAðXß½Û¢&¿ÊUq%æÙ›{®aW©&tê(¶4¡¨¢ÑR8Æwt­âàêlšõxÕE-‚lœš YЫ HÃ1ËN/ù=ÕÍŠG’¸}³!¿–ö‘2…a럚w˜êª;¦µnãaÎyÔ°<ö$[C¸ó¿‡J+gäçÌØ4Á9vË´5Q RÀyóö3Á-V~؆ÿ½Ã–ý­£Ù·áuå ýD–"ÐȵeK,iJ´˜ÈEÈI…&×2DiZ &’y&æ<½B]Ö½NkK4˜ÈRhŒ%¸ÐRÚ×VZ-×qLÍyøçìöy[pÜl9Sýêù›Â.¯;¹ų̂;GTáTnk‘¶mfìn>&7§·”¨Ž ¯éäþÃúmÊ}u‰»FÏK°eyw·wïãK¤6]®cp©ÿømçÊÏ) LmÈ»#»mtss Z~ÈWSj^:óÌŒ‰·¯[3ÐÇ(gçÍkƒL€0pý†—¶ÄãñY–7«Ãsº\¸šŸžp=4üÔ‚nõªÙ°,Ããñ äÇ€ùÿ–óaŠãŸä =<$î^–ª¨ûé:Vó*¥ˆ´u·¶r5W&ÆÐo"7 Lí¹ùQ/_'F³_œÎjKJ´@Xùe°$)~¼môŒ›ž³¶ÎoeM¼LšÛ9Ú[è_½VìÙ¾~åJªø>¦u4ûf‡itJKœÒÉÏ$ßÔÚÖÚœyyû‰Ò¹AuKλӕ©´ëWd¾]¤\Œà’A’püûL˜./k‰4u¨âY·Çâß…ÌŸ| QG@0J¹Ž)œß|4‡´p•@vtveŸ/LJ<ªeQ©J@›‘9Ô°Ùø8Ù™V™Ësñ¶9Ô²c3"K3ûé¢Äx¹™§›‰1F ˜ÈÅÛJ™TV›és"2;{ÞgíÆý|%ʰ|¿Î#§/Ýyhs‹”[ÏdF1iã˜Ø89XáÉçO¦Ù´iæ(2·wssswsswsss0çàkgK.Ðo{F¨¼¨…ØÕ±,á‘IœlùÙWŽF‰;T™Ø¹¸—]ÁÕYÂÃyNNR>ŠO ¹2A&@,›¶d%µÍÎͲ·q$IÒÏÃöÌÂîgî%jÓÊÍÖŒ¢¨ìÜ,+©­AÔñ|kW‘ìÞå{‘`ù¿öî48Êú€ãø?»¹6 g#` hFRµEm°R¤ å¨J[+j±‚£¥¢œ "JQ ¢½,"­ ©Ö:U¼ÇŠcD¼ŠÕIp I_Ø:¶#ë.ÝÐÏç…/˜IØyÜç»ÿ_ög'3;¨«zí¯ÍîÛñäÒ¹/¶üË®±Üèï”®\tÃüNמSøÆò™kcæönݱ͙…Þ9mQ£á'Ön|ôž{ÞÛby%gÜÜYKÃÀv•ëV-{¸²ö O0¬}kùÕs^ï^vF÷¢Èöç—Ïy&³ï =›E>û5Ä7Ý=füм §Œ(®Ü´¡2„)hש]ÞÞן}¹2/gïÆ5KîXÝtÄ’sÛ4¬OFkëþu[$ÕX?~ÝæNù¥7mÌÞüäʵ5ÅÃ{¶ÈŽFªÞY¿yoNVõ[Ϭ~èéFgNüjËO?еn׋¾ÔbPyI^4’QÒ.ûéw*ë{ç~ðöîFŠgnîà?óiÜsìmWoyËÕ7u¸gòWŠz–FV.[òP~ïÌ «/ÙR[|x_G³Þ»VÏžsk·õoºóŇ—=¯”ö﮺xUUõÞšõuñª={ªåçgÇ:ŸÿµüË~¾à¡fçwصfö÷\Þ§EfÓ^CO©2ïÎ~×|³ÅÖûo.vÚ-Ý 2óÏ8¯Ë g/ë6¾_ÎË÷ܵ¡å€‰sñ(§äœs‹î_¹}2wkm]5sì¬uͺ™~Û%sC%£çL¯¼ñÖ)—­¨mÔé[nŸÐ§qFF—+nºbÛä_L¹î@ËžC† hy×–2šô»vúÅ“n¼ógOf¶í7jpßWî=èFšu>¹ùŸœ?iÁ‡ñH“Žg»½üìã"¡ú3_CÕÎ [kkâK'Œ^úï¯ï1õÏ Ï¨|añøÛ6Ôevéé⫆wÏk`»’HFFôˆoa¢‘Výbë–íX5Ö;}c­»{üEŠr¢á@õÆßÏ»k}f“§ ž8âÌÿã§ ÷mªXSÙûÒ¾-²¢!t9ÿ‚//ZtÓMÑý™§^pii,z”î]›¬öC§Þ¼qô¸‰“:Ý;ïüŸ\½öºùÓ~²ªU¯aŽ*ž·ö0_Ã6¯Ÿúú¤³®]‘Utúð¯ðô#9Ñ4³}ôêü‘.ßBo^zö/ ‡/y`B—‚“ÇÝiRrƈ[fŒîÛ,rÐÏ®Šû”Tþú®‰¿Ý{ »Mïa3æŒùrn¡ö•S¯Z³;äß{Èô».ïßÊÓ•¢ŒÎS*¾øw™|®²²2W“4TQQ1räÈ´zIËCj·ÃŽÒs!«Ÿ-?ïš0cÍÌ^ù©}áþM‹F}÷© V-9¯Õ§þ"©Y7ýÜï¸aõ¼Ó ŽÂkhxºN|¡[i›õÛŽÆÃe+ÈÜöþö.j`õÝ÷Þ=«ÿYéöªêÞ_yÁwV~ë7KG¹íþ÷íÓÕŠŠŠ©¯Úïèäô•¶³%8,r²#yÙGã4– !dFØøéòé~í¶Ç—­Ù]Ú£cËŒíOß·øÒï}µ¥rþO‰Ò:¦Ós¶ô õþçBÈÉŒäfPNV$„ºß=Tv¯ÿým Ö„k×{è¬ß>Þ_ž€x€ôì‡~O[:Fåõ™ùȇò…YÇ,vÌÿj¬Ç¤ÇþrÔ^CÃS 5ûâ¹Gþä!;3#·,ˆ6¬x¨ÙW“›.O- ™mݲb?#ÄiÍl‰cذ“òW¼òA“¦M bGöy滫öíøGå…=ÒkÖì«ÙµkWëÖ­½OÄ$ëX-Aáâ³;և׸ëöM5G8Âcõç´wÌݹyKƒ¹8ÑÌhQ›¢öÅí½OĤÖÁl‰cQ^NÖ•»\9Е !ñ/ø‘¾š7oî"ˆø|–T€x€¤ú!˜-ˆHÌ€x€¤˜-ˆH¡‚Ù€x€Ä8ˆHŠÙ€x€ú!˜-ˆHÌ€x€¤˜-ˆH¡‚Ù€x€Ä8ˆHŠÙ€x€ú!˜-ˆHÌ€x€¤˜-ˆH¡‚Ù€x€Ä8ˆHŠÙ€x€ú!˜-ˆHÌ€x€¤˜-ˆH¡‚Ù€x€Ä8ˆHŠÙ€x€ú!˜-ˆHÌ€x€¤˜-ˆH¡‚Ù€x€Ä8ˆHŠÙ€x€ú!˜-ˆHÌ€x€¤˜-ˆH¡‚Ù€x€Ä8ˆHŠÙ€x€ú!˜-ˆHÌ€x€¤˜-ˆH¡‚Ù€x€Ä8ˆHŠÙ€x€ú!˜-ˆHÌ€x€¤˜-ˆH¡‚Ù€x€Ä8ˆHŠÙ€x€ú!˜-ˆHÌ€x€¤˜-ˆH¡‚Ù€x€Ä8ˆHŠÙ€x€ú!˜-ˆHÌ€x€¤˜-ˆH¡‚Ù€x€Ä8¤•L—€´õñlé“ÿÚ7)++sppX8y Ýû!˜-ˆHÌl @<@RŽJ€MI*•JBȦ¼ŒsÇÏæXì9xsÓÎ=iVØnŠlÞ¼yóæÍøÃìììÇ'%%;v¬(ÀÅÇÇ'%%>|8;;»2V•~8¶ä^·N N&ÇN,Ö¼\³â­‘„MYÉç’Òr ,ŠPé¼Â#Â|ô’ Ò’2sMV!T:ƒOPÕ*ÊùƒGRÌÂ-¬~T°NÎ9±ÿx¶"yVoPËçBžRpîð‰E!gß»G}HݨPñ²¥^i`Ô-¼^T Ö”q*þL¶UÎN˵z.zƒlÌ8î|zŽÉ*$»w@hD°—%ñ’O ®æ™qê²upWú¦JñX­.°J€%5)[„Ô ÖIì/€óðòò HIIIJJR©T‹%%%EàååUŸH?ÛSY.j&êÔBÅ’s.þdRvÊÃÏÏÛ]1夜<‘T [²Ï;“–k’µOwµ%?3Ëtƒ]bjwOwUñOžžÞ^zÉZ®¥I*—¾(±Y­­¼%ótü锓U­×k%KAVÒ‰£‰¹â²OÔ\öŠJ\ù›–®†)õlbfaqáp&’$EEE……… !‹\xxxTTTÑЪÍÑÀ– 8¯X¬²BèÃ}µ’PLY‰é!ÜB"«øi„ÙÍt,±Ð˜‘Yài͵ !´Uk„{i$Ù”›§HâFޤóÍÌ<‘£¨<ÃjV÷R !ç½ÒÒ®ò÷Šb)HO+Bµ››¶ÌûsFR–UµOzÕ¼Õ¦´£‡Îæ›Ò’ÑÕ/ùD!|B³.zE1¥¼Ò7- *¾Fr 4è5Z7 p€=¹dü´ô¿íÚµ+×rjÔ¨‘™™YPP „0 Õ«W¯¼u&ð)ÙZ|œÚ§zpO$„lÌ1 !DaÒÑCI¥o´˜d­·›*-W6§ߟªÖyz‡©DaÛÀ:+-íJ îM(ù3Cp°‡J˜.d8c®Q!Ü| I­‡—^äc¾Y¹îí7•«|SkIGœÎ+ Пy¯@eƯRå_6¨ÿd9>>¾(À !òóóãããëÔ©C?àQ¿ºêÌ¡3¹Š5ë|J¡W˜»J¡(B¡¬b(É/’Zï®s¯VÓšx>%+×dµšr3s ¥zµ‹“’"Ê7Ó@Òú^aiu‚ôW©;%I£÷ô  ñu»Nß_yÖãÊßÔMùì€=+ |E‰°ùOQ”£G¦¦¦ !BCCeYNNN.Q­¤G†`cZ¿ªU2ŸÉ‘ “O÷®î¡Ò{èEv¡0…!ÜO/ ¡Xò33Ͳє§øFÔ ¬ªXòÎÇM6 cžIñ”TBa)4ÉBg5[¯žÚ$!”¢q[•Š1+ç K»R†s ¯uõl§÷Ô‹ìBQ•gñõQ[rsÌBIoÐJ—|âåë ]å›*<à¨üøu{åää¸àààš5k¥º”””ÔÔÔÐÐPooo2»'éüªVÉ:t:[6¦œJôŠŠðôóK9‘aÉ9{h_’^§’F³¢©£+<{âÔY½»A«2囄*Ow•NÑ«„Q¶fߟ-)V媟â¦ÙF%ïtü‘4½»¸oÖÙã—-­ü-_Iëê“t2Ëšuòà!Úb2+BèB¼4’ùâO¬RÅç’u¨qåoZ××ÝprÞÞÞõêÕKKK«]»vQ¯[Q÷[@@@e8Á¼T•’â´¾Uªx«„æ´Ó 9Vµw•¨a~J6 f•ÞÃÏßS«÷ ðrW[ ssóLBçP¥v¤¯FR{FT)ºfLÒx]-†©ôAU‚=u’PL¹ùF«¤»âÒ*°òŸÈ:‘ž:µl2™›wHõZažêË>ñòu’æŠß” €Ãh×®]…;öüýýË›J’T§NJºÁ¯BªÿÎÊrýÁô»µMš4aØÐž={žÛ`¾ñ÷ÓàxÈpd8á@† À 2d8áÈp À @†d8áÈp ÃàZ4Á­$ËrRJRBBB¡©ÒÀ¸éÜÂÃÃC‚BT*[ö‘án©¤”¤s ç|=¼‚}ý) \Ñl:—pNF†sT >~A­–ÒÀXÌfMª&!! çÀ M…!¡aVY–e™Ò¸EQ\ô›KRHhع”ó¶]*îoDÉÍÝ=??Ÿ¢¸v)QgqP½;ÙÙßæ_Ÿ w6$gqàÙóÉpµ •€C Ã9àd'Àe3îŠ ÿýdÈ›y£ãÞkip° n::oä{Yÿ|±¡»Í>ª2–IC Àͺöh‡?œ:ÂÙx÷¶õsr6¿Û¥èßý†¼úé÷[W?`•¬u/ÞÿØŒ£¦Ì%ñ_=Ö¥tù1±]bú¼³;uÓ˜Ø./­NwœAvéÖäí» ëÞUÓ=gÓ«sÜ,ݼÊX¦$‰Ü}s^è×ó…_ÎY¥JY ’ýïãŸìß7¶÷£C?Y|(·ìFU Žý2꡾~yÄXüŠ9ygÜÇ£Ÿ~°wߨG'ìd¢À•TB?œ¤k>üý§jK9)Ç·¯^òÍ{ÏmâÓOúV×Ûjù!±o½|w ºè³´>‘>úþ¯¼&×÷pˆÆÛ•ÆR-©{¶dÕèå!•þú¦[¢•±L¥àXÜGŸ®ÍS ÛL̸lJÆ–éã¥vyáýÎþ ˦Í|nÍÙ#šmYSŸÆÿž"•~kÚ¦©/O;Ó¬ßà·T›¤H7ð€c4b)¸êÞïNí]¥VT]7Q7ºE»®Ý~~mØÜÉKÛNêã{hÆ[Ÿ®ŠOα}P£û‡½òdóŒYÏOÚoû_è½HˆêOMtzæŒMÇSóeá~G߯>ÜÈ粎B•{XTtt˜ºô…ÂWÍü,otÛ¾Þ$gî^øùÔEÛÎä«ýëß;tÔS¬»ÆWðÈ“¡ë¿ùãH¶OtÿWß|¢™*ߥkÕ&P$ü2tèŠf·Œÿé·}yAmžûBí]Sÿ÷ýÖdCý^yÿɾ*ÅxfíŒÉóWL·¸G´}øÅÑý¢½$ëùßF>:Ó8dæƒ"57\yÉû6'WéV_w|öó“ZÄÁQ Qíñÿ=yè­ò{?ä¹ö‡Í ÚZ=^Søëä/V3‡ÞñÄ#c«ZÿðÌûÙ÷=èûÏo›OåyÕé9|ÔSm57µLmö¦±ƒfûöïdZ»|W‚Ù¿qŸ^ïßÈ[%„9aÅÿ>\ñÌ-¾¹NaMXüÊÐwLý|@u­œ¼âµ§~ª3qÆÓuõJúÚ7žXXïõn»ÇÿyµßF:mPMqÉ…P²ö¬øOÝöÍ!¢ ¢~èco˜ºâà“MZzkÚ–©ïÆ©¾þèš·æoöøæhüÊäWZy_(ÓÂýŸz?û¥¯ßoaš´ìÕg–·>¹O.û8]¦’Ÿy/ékÞ7¸ƒáè'›…6¨éý#ßÿìË©¿Ò6ç׉3w˜j<þÉsQjÿØñóý°`rL°!¼õÀ—?übÚgŸ®⻉óJÍ.꽑­ÈWAµœþåÝqú¼;û‡9 Þ:qüïg,B¹`×Üe¦öÏ¿=扺§ütáQ“P._«¢ñ;K¯¿¥µzfìèžÞ;f½òÌ„#û;¤îÙ_¿XtÒ¬dïœüúìÓM‡1oîW£Û¦ýðÁç»r!„J%„JºV¿ì—Jö ¡íúºUôþ³uÔ~=ÞùrÁü¹{«…µ`ÏÏÛ|cGÒ4eù{/¿±° Ýsc†uÕþ3gîÎ,Ea-Ü¿l·g‡¡c^z¢QÚïŸNß&ßô2…%{ë²=Ÿ{õ•¾UÅMúrO®bMß4ý½ê<ÛÚ¿x—QDG{§ï?‘§%ûÀæ“ÖŒÝÛÍB˜ö××nÕè¿mªº|B˜S'Zƒë†è…Brˆ 4‰O·(9{ç¼;+¹û›£»„–>ÝÂxzöLyý‡ƒêÛèëó·§Z©öÜ çpóRÝ"†Š­ÇR,†&}ÕŠ)'-MÕºMØÒ ñéâw­¤Ò{yûøè„­(„lÌJM©Óº¥ÿú#§ó”zúK¾±|jÎcÌ)ù_õá_Ró’O4ýeIr›—ÇÇ60vï ~¶ngÚ½‘Br»ãÍÏ^nå)„ÑÿÀ’WÍ‘£ê^¶VÖ–!4Uoô#U5J³‚eKfEŒûLcwQèµíç±Î¤Æÿ¸ÉgÀ¬mÂÔB÷{¢Õãמ(lÑ(4vÊêØò’¿î¤_»gýÕBíæá®‘TzOooB¨Ü[¿üÞSÍ=„)pßo[õyçÅÁ*¹^ÆŠå¿M³4BíÞfÔC›¸ ¡4ÐíÝ1iÕÁÜŽí½EÅ—Ù&P·ßx,J'„\˸kè×kïÝ÷ÝÉ÷~ðv§0­|®dÍõáÍk«·ìI0uÔÝt¾f·†Éÿü›:°ªt䘱J¯HßpóÕrzÁû—/PÈBç¡/ u*7o½Rž{æ×Ÿîi1ê㪻‰¬ ÅvöxºÕ,ª÷Õ+ØzlåŒ/'|à7uR7ªšªà*ná “9iý¼)_þö_²UmðÖå[},—t¢)…'VÍþtþʃ™Bëá¥Ê³DZdå²ác)ôþq£:¥ªÜÃÕ'/ yÙ'¤e÷èæ¢, ˜æàÔ<9RH*¶è5µ»¯A2𕫝•J«BIëᦖÔjI!Tz½°š‰ÎœøêÉ>ó‹ÖÍb2Éón¤¨èäTöUp|ÓÏVƒƒµ’$„Tzµ—$„$„$©µjI’„ÊÍS§RiÔ’$I*7O½$[dQôfUñ» áu•uçsdÉÇtË”„’ºx¡jï5½ O&Æ'žÎ:ö݈ß•¬õ‚gŸ8óñ—ƒZG,ÏÈ[NuóÏ›‹÷ev Ø›Ð¢Ž—Ê£ÆU[K:ùÇ•øyg!„ªäÃ%EV„æ¤]G²O|»ÿâ’÷~ý¡ÄW&µ0J~­úôj_W'DT•áû¶]÷oz—È¢†NÑŠ‹™{*œ±ZùΘ¸?I„v ÌXùɇËTÞ™ópó·Ì5/<öÃ¥ïdܸ)›ª>=aá}õTç¾>bÓ‹À-¤Vݺe¯‡»¬kË*«|ï“‘%#o’ÚÝ[{ð¢¥e9ëùë­•(NAÅŠc¡Uh¼øåëÍ=J¡õò¨ÈV4žÚ|P׬g}IÞ¥óŠß%I’$$I%•&1•Jˆ2³J#Š,K½V’L¶Y¦TüMUßö£¦Ýa’‹KlÕGïlm5î­Þõ ^žýæþwüHê¯6EFë«~¹ößcµÎj£ú†è$µoý«ý6,¸ækÓ/_`úwé\¾Y‘$•Š)×(Üýý÷ÿ|jAÑ{•œmÿûSÄÈOoîõïQ˜U¨­§Î/ÔS:–U¨H’Ji†‚çb臫Óéß­/Œ~®­oÒܳ¢ÞÈ>-BÜ…Pd¹øA’Z²Õ*„g÷%y´z¥{t€NKé;ÊMåU­†!óP‚äÓÔ¿8é)Š"^±o0qß•Öêz…R7XÞu(ýKÕ’{¯)²r#9áÒ±pÓ¹¿ÿ³6©/I–’P¬rE¾µ’{|oš{š¾Û-SX3ÍõoQ5 $BWò’ÚW§Òú†‡ûë%Ö´¶fÝêßÏkZ½¨õVß4÷ÕYÙýªê…Ú«ÿVíáqùÕªzáê5Ï›b‚Ü…’.>M_¥v€g§gÉ—Ì:æ¦Òz…FDdmß‚»ÌMkj…bL9“­ŠôÕjô9?פƒ$„Õ,s„ÓT`?\üi¹Žp=œbÍ>{숛*/ýôÞ ¿ý°>©Ñ³“ï Ñ§Ô –¿[·ÞÐLæ¯ïç²ø·BÒTõÈØ¾nûAÉ·@UÝ?oCÜÒ-rmëáUß|sÎR»b+àÕ76lèÌ÷>ß[׫ðÌž5«Žß1fÄ•úÉ´ÁWZ«ëQµï×ö›ßûØD¿;Â¥´ÃÛVoÑ>ôö K_~ô ãc3§=z•y©—Ü[Ä’¼c{~ݧkºw¹VñÈØ±~çaÉ×b¥ÝHeD%Q<**I’óo\»S_[ŸöÏO³÷vŸZ×Ýz“Ë”$kæ5÷XªHç6~ûíÙZ½^MáœzQG¡z«*™S¶©î®“4ÍîôþâÛÓ‘ƒkz©$I\ç·WX Ê·iÆÖOç|Ózh'¿³¿ÏýWßúížeOèRñA IúZ1=«­ü~ò¼C;ú^úÅv·Žãšùº™GJ+-\éÑL}tí‚…§-‚~8 …f'!Îþ3œi×ô1Ã…’GhýÖƒß}»OËá1#ŸÙÿɼOÆþàÝóÁ^5NmB}Ô#OwÜ7õ£K¼ZŽüptÿ3¾?Z iÙ3&&ôÛ#\]­ã?ÒÌœõÍ»K³eáUµeǾÕÝ¥”+¥±+¯Õõ¨üÚ™øâ¬?NxuQè‚êµí5$P+„e!dåz»oÉNlMݽ5½f¿º%/¸E |òž±Ó&ŒüݳåÈOz—Í[—Ýå­èua:·jÚ›ÓÒeŸº=Ƽ;0ÊMN¼¹e !$©0þ·IKNæª÷ûòéÒ°$ùy×i.ÎÔm[M/IBÚ²Uð·9 i‹–sÍß^i’ûÆžúlÊ”×›Ý"îôÎÐæÞMó-³Êºj½ýjÎg_}úúÏ]Ÿ{ï¹æ^*IÜõÜÐí㿞2þÏ &±}¬òÕ?Œ¥œ3ÂJõßYY®?˜~·¶I“&l‰ŠÙùïÎM[X‹å´c^ø³ûç“:ûWh«ìþè‰÷Äk ^oráiZ7»L%kÃë¿®ýÙ¬§¢t4ÔTB3ßµGÓà²ÔjuQ¸Æ{öìÙóÜó/“›ŸÞÚt".ôÃ)9× íÐÈWUÁÈ"•­P\9Þì2…d³Ç:M8àÒÓ¶ ‘ánõ¼ð£ï]Ÿüx—_ Ë8‹ƒí ›põ^X¸×Ëà6ªÂ -ÞX¸ÄÖ+èÛaÂòl( ¾R2äÒµ{b8ü2œ2 ¹¹¹ž¥7<i€ ÈÍÍ5 ¶]¦Šb½•BƒC“sss) \'À%&%†‡Úv±ôÃÝRþþþBˆ³ g ) \››[XH˜¿¿ñÍÅÈpŽÈjµ„††2š€‹PÅl6[,Û.– w«Y,›oEàj¸€ 2Èpd8á@† À 2d8áÈp àòh(‚K(Š’”’”x>1¿ ŸÒ‚ÁÝ"I©ˆÈp—JJIJKOkܰq€¥8„´ô´CG !BƒC©ˆ¸HED†»TâùÄÆ ûxû˜L&Jp>Þ>õ¢êý·ï?§ÉpTD®Üò òýýüF#EÜF[¶l¹óÎ;oðͲ,ûûù;Ó°#@ED†+·¢alEQ( à6R¥¼‡¡Ó\ GEP‘á*H–eY–)ÀCç;f©ˆ*"2\¹›¿Š¢Pu·½ù[®ÃPQ'뇫¼Š¨pßgý_Oóãø;=ìñ»[=ýôŸÝgO{8œ3¨ˆ®åÖÝnÏ´Á]ù|w^qŸ¤5uík}úŽ^™l½ìÆC3Љí:juê5Ê­`ï‡ýbûáŒÅæU§”brêŠ;ÞÛ£è߈uY¹;ß¿¯÷‡;ó•›!gþõü}ƒ¦1*Š¢(¹GõèøÂªTYp“$qýªóûa={}°9£dlDÉÛ;yP¯'¿?i²ßŠHQ²7¾PRuìÞwÐØy[RÌ7URB)#ºyÆ#³w¼·GŸ){óŠ^0ŸþöÉûŸYšl½™¶[AÀþ*"Çë‡kü谻ן×cÖcµõJöö¯¿ÚùèìÎÁêKßh:ñ×¶LuüŸ»2:w ¸JYèªözᕬHµíÛ¿r·oÇã"¾~ô¶.ŸNì_ËCÙ«¡(77¾¡È²¸°mþ¯¼&×7(²Ìu/ÀÍ5o¤æ|ð¹˜ß_ýýÁæÏE»KÂtbñ˥³ãŠHÈŠt͇¿ÿT”:ïüžÅ³¾}ó=Ÿy“î¨hý]TÝ(¶ª•eE*ÏìÕó×>üaLZÈŠRRÇÝÄê) %É+"Ç뇓|[}ªIê¢é¿3çï_0e“ßÀbª\^™N­Ù’Û䉧ïq_±'CBžõDl¯ñ[2!„åÜ’Q1¼)=móWS¾?lBÉ;¼äýaý»ÄÄvíûøˆÉk-Ât|î#1±Ï­J¯@.*Ûüj½‡»V%©ôƒ&é×ÞÜ’Ÿ·itŸØ.1͈7ʹG~™øBŸûb»Ä xfâ²cù²b<2}ÐÏÿ²öë±ßûอ©{g¼òÄ÷Çv‰‰½oÈë3·¤˜G¾x~Ò~Kê¢zw‰‰}ê—3§VÎülÉ‹¢(Æ„¿f¾6 Wl—˜¾ƒÇ}·=Í¢(ræúÑÝùß⸠Oõ‰íÒó±7âŽäÑc3{ÎÌÅKåæå–¾’ŸŸ¿ä÷_gÏ™Y毡Á€áwüöŲ3aMZ;íçÌöÃ42H7[)ÆÓ~þüÀû»ÄÄÆñLÂf‘¢(BR{GÔ¬S»nÓöý^z²ˆÿûh¾¢XÓ6NÙ¿ïý]bb»ôyæÍÿeZ•¢ZhØÂ_ŽÜ-&öþç§­O6+Š¢˜“7}õÆ€ž±]î{äÅ9ÿe×$röþ_ÞÖ¿KLl·/MZq"_.^ÂÐïÿïù‡ºÄô~|âêÓçÿ™=fP·˜ú¿ñã\ùòN3mûU=òíÏG .êEËÝ4æ>“½h<6÷‘û_Y™nÍ\?ºû#š?n@ll—¯ÍÛ›¿tâ“}b»ô}nº¢uBXR¶7決Äôüþâ#ù²¢(ׯró8\à‘ãõà ¡î4tðÒçæM™*c¥æþûÖÐ]þ&ã©u›rj?yG󀽺7Wý—Ññ÷¨GFöXóÚÌoö7¶mú·g[Žx£­OÆâªöèüñsâ[¿üùë5”ó‡þþ;)Ë*„J%$UÅŠQV«µ4w7XYÒcük{žœ¬=mD#wµÞ-så‡oÿ îûÚoFü4qêÛßÖš=XÂrè«ÙÒ=]?Ñ R­9ݸç‹Öð0^ûå¤I_6þzô‡ïþCw& ©¡S»kQ„¢Èrþyo}¸.òÉ7?kí¼î«ÏÞzÏýË ½¼daÉøëë-1¿ð†Ç±_¦ÌòûŸõ‹PÓ6‚Ó+í† IJNZöÇï1Ýc ƒÑX¸lùÒÌÌŒ  à²íc«ÕzC5§äÛö©Gë>;Æúæ÷mŸ¤îcsZûJ7]Y–¾?u[ä°G5Ô§Æÿó×ÑL‹¨¦±UE$+e†‹Ù*´^z!˲¢ kÕäãõBÜrã—}:}⼦_ލ¡a‰ŸÿsÈ#ƒßŽÍÛ0{ö§_µi6¦AòãÞ]êþàóãî 4žÜôÝG…"Ëæä5ãßšŸÞå…ÏFW·üuâçc§|1ª¡"„åèÖ|ü¹·»ïžýÅäáÛC›Ý?ä­®gã¦}?uÍÝScCÊÔA²¢¡éöHû?>üf]ï÷ºùÊŠR¼¶²¢w¨÷× KÆú…Gú=õÚ=IK§÷ÚˆeuîøâeëW_Ìø¶k«‘ÑŠ¬X~[–8`ÀèXåàO3¿zçû识ø¬ÿä:Un5 =wpŠÈ3œÚª½F<¸lä¢~÷Nå~…w˜N¯ÿ;»ö#Íü<Ü:7Õ¾ÿç¿Y:ùJ½ÔqóÛŸ¹qý§¿jﯲf”üEʱ MÕæÍëEzK‘Õ¢[ !„¨9äÛ?†Tž:ÿÙ IDATtC*;—X¹Ð=ª5´’Jíáåíí.,g–ýt°æ³3{5ó•„hÿè€å«¾ùûÌÃm¡ª5ôÓI1AEu\ïGêÅ”“–¦jyGè²MñéJ+o7¤Ò{xy{ë„(T¡EÉ?üËŸé†~ܧ¹Ÿ$j |¡ÿæáËVžêÞWo÷Þ{&ÚMˆæºÍKÇï>WØ7ÜÀqÅBtêØuùŠ¥™Y™ËW.ëtOçõÿÊÌÌðööéÔ±KÙ9ÿ7ÞüÕ„uÑû·áŸ¾¾[4xZ§µ *"sÚ‰9èVÑÕƒTÕ#ë´ìlÓŠèBÏ–%÷ÌÖo~8â×a\=wE‘|[ôyXÙ˜•–Z»Us¿õGNçÊÕE¨ë¾øÑ›ý$a­š¸býÊC)9ú_ÿH¨ûÔ̧:©„¨£Ýþí_iа$múm·gÌäÇ:D鄈|úÅ=ϼûËžg¢ƒ¡®7â—:ùIÖêg~_»üŽ×^ï_]'r•Õ‹?=”l¼/ØýÒ­$y4ì÷Pø‹ß.9z÷`w¥xm!„RòSñE¡ñëþÎÔ׋|ݦ¸ÏÚÎK˜ÃŽý²qÇñ,K}EMÕÇÆ¾Ü/\-D‹ð”†®Þp¢ƒáª\nƇ©ˆ0Ã×BˆÂÌl£"<.¿8óצ¬ZšúIBªÛ¹™ú£ÿeu¼ÛW’¼[?9¬ÍÓŸÌIl8rV‡ µ¥S!Ü£º5UMüàé§[´iݼU§.mk{ÝÔ±$Ie¯Ã(Óv”Ë\¥! ö'äÿ7ñ©‡?-zŸÅd2åZE¨ôÚâéÅæ¤ßNÿjÙ¾«Úà¥Ë·z›-VY)Û¤.îå3gžï~öÌпîlÕ²eûnw×÷¿¹Êõ¢ŠHQ„\°~lÿõB¡®Ùcä„ÇY– O®þzÊw«g ­‡—”g4Y¬²¢I­‘dY–„Ð{»K–‚¬SÇrýšÖöÅõNQ%dL:’¬ -î¾Ò…×’;–b R„¤RK²,KŠÖC§’TB‘eYHzƒV6Y.¾NM)®¥®ƒîüé“ï7ÅÛȽâ%(‰K3\iu$—^©+ Ù"K†;ÇL|¢®[É_ªôÞÚÄÍ%u–$¬IþoÂrU¿7¾èÛ4XŸµnÔ³q%£ e‡Š6º¸Ei´+SÊBÈBBÈÜ÷®Òö-ÝÕõz}×®ÝW­Zž“íááÙµó½nnn—²,ßxóWr­î§:䫽Ò_T "’¼[ž9¥Ýš [víüqÒ’…ÛÆÎ}­ïMÔäUD²¬HÚfCß}<üØ·͉ÏUéTŠ,+æS‹ÞŸ¾%âñ¾é^×_JXøÒ¨¿•âÌ’ZH’Š"[ͲPI¢x²¬E\Xú…EMÖ K(“¼d!+RÙ÷|ÉÅаoï°W¾ýíD‡’z«ìÜ„âêìâjMVDñš”,]‘eY(%( !¬VY¨u’Õx½*p°ŠÈ±2œåÜŸÿÑ~ÜSÔ?|âïqÓÄLöÂá(„0ŸÙ°1¥jÿ_mï¯B(yÿÍ=wÕžìvw{çîøzú¶ê†¨?eåݟĆ—-`ƒNýtê÷ô©O _óOâ“jjo¦î”eùÂXêEÃ’¤X­Š¢(B\+ `Û “G›Pí…51—Ž!(B˜Îï?'¢žëÕ4H/ÅjUŠ—¢’„l±– /!„¢òªé–qèTŽ\ÝOÂ’¬À¯u V9Wfye.渂+Tewu7½[×.Ý7ý½áÎ;Ú —²,Û¨æ¬XE$«¼kÝõ`­»’²ò•sþ>mlãë&lS)BHϪ5¢k¿üZâËoO_íãñV3=ìÑlD—º~¡XŠÆ . Y–­5T†Ð MFü¹|¹š·T\‰(BÑÖ’ÛŸ`l]G'„Rpf²:¸ºŸºÌÊÞç£ì²Ën¨Ò;•¨B»¼cÑ„·ieµ"EÒèU¦ÜkѪXLVåÂkÉKëW¡_º¢\øAQ„0%ì?+‡ôŒϸ^• 8SEdgΚò×äoN7}þVÞjU‹Ç†µxvÂÔßîüPõÒ¸e>·aCj•˜ŽMkO™W|cΙºb_v3Ïïþ·Þà´‡úJn›7u]›ñw—”Wú_L>Ô$¶s³*ú”í2ܪÖöטŽÏòùŸ}_š?µ«¿TîšóBóW±æä™dE.ÌÍÍ7{i|ªøü¹æïaŠ&ìž^u—Íþhšþñý,ço]½?âéçë”öÚIBí_3Hþé÷_6º5ÖÛ÷Ý‹EV´~†ÌvR|-!ÅMR}­žý^ûå/^µôJ^?/.¡zßNjùl™[”é丂ӻüÖz½¾sÇ®â*wB·Yó·BQþîÉïüÙ³G«šž¹{v§¨Ã;k…­*"q¡ëJ1ÔøÖ'G~ýáÌ*?TÃ7oïlµÖ”ã×þ¸ ÁRS)î†+©…”¢.5Y_£k3ísfÆYï É>¸ú—u9ÖÆ²,Þq_“³¦|Skx×HËÁ%Ó·ªï|½‡’|a ÅW“(¥½f—Ý“¤L¿š$<öy ü¥oN‰ZŠ,ËšÐaÆã~Zgª]xpÅO¤Yk_¼‚ÅxE_T)•P„",)ÿ¬ÙT½yˆõÔÚ9Kóš>wGhUÓõª\À‰*"{Ëp›¾üj_ÁsîP !T‡>úë³ßLù³Ã¤˜âKŠÍ ›×%‡tiRºN’oãnu­SV¬úêì*}¯ITÕjÄ}#\þÜ—³·4éWüšÍü—/üø•iF¡iúà˜ÚûJÖôÒ ¡"Í_«µ¸Ý˜¾þ½'fBüôúÓ{_œýá]žì±ëÓ/Æ­××ôѸޯ¾e÷ͯÿ{k®E¸‡D·»¯ƒ·T¦É*T¡]†>vpÊ÷“?øÙ¯^÷ؘj§·EQt5ûnpÖg£ÿðl6ü½‹ÿ@WgÀ›/[fÌûxÌ<«.¤IÌ/ÅD¨•l…~8¸t ¸­DM«XE¤ o^-mÁ¤Qóò…Ú¿~÷WÇt U “°IE$ÊtK)Bh»|õĘþ÷YµG¼ØûÔ”ÿ÷–*¨Y÷®]ƒ:&.ºAî…‰³ù“#ûNþâ»ÉÛµ!Íztlrd©,Eå×+ofNŸ5gÌò|É«fǧÆ>ÝÄC˜Ëv½•틸Z?\™Õ¡Ý¶]ôÑßB(Š¢ í2tÈÉ?~>Q„µìÕ«ÃÙç®ÕWú³B(Y»Žÿ%Ũ iõИÛùJ’èq*pžŠèFë‰úï¬,×L¿[Û¤I'ÞZ»þÝ]/Úd2±ã·ÑŽ;Z¶lyãï×éthÞ´9­ˆöìÙóÜ󿟧Ñ]–j%©ìýáÜ®¶o¹C«ÕêdÏK¥"¨ˆÈpåvaÀí«:Ë=„AEÀ•*"2Ü¥ÜÝÝó òõ:=µ'p5oÞüÆAI’ò òÝÝÝ©ˆ¸NED†»TxXø¹„s¾>¾nz7Jp…ÆÂ̬̈ð*"®S‘á.åçë'ËrâùD£ÑHiA¯×‡…†ùùú9͈*@ED†+7«Õ¦R©( À!Ȳl±XÌf³Ó|#*"€Šˆ Wf³Ù™N¨ˆ8Zxd8á@† Ãàv¸uóRW®\Iq§wï½÷:U†B 0€í œ[zzú-øÆR€ 2Èpd8á@† À çpf̘A!pqqq@†d82Àáõë×BÈp Ã*óR2Èp Ãá΀y©ιXóÎüýý¬Ÿä*” Ã9ó¹Õóf}³tO¶•²à옗 ᜇ6¢ËcÞéÛÄ“Bd8á\ž†"(—3f\òʰaÃ(‡y©ε؀=`,\óR2œ‘-&£ÑlU”’(`ŸK½Àœ°zþï',B±yáÜÍ~m<ÔÌ— » IB¡Ð®á.£­Òýi®vŽ€n&ÀqõƶÀ¼T€ Ü’ìâÊñ…è Ã- Á¼T€ à)šÐ@ŸÜmp@†àöçr¢9Èp@eÕ° @d8]÷‡ƒí0/ À à˜Ùa^*@†d886žõ~9†Sd8pMÌKÈpnºâ"á8—qÓ˜— á@†p#˜ÙpëQÚÈp@9N™Œ‚§’pΤ ”óR2€Û„J€¶ÈpìúÔrí¸ÆéåǼT€ àö¡+Èp8ÆÑd8À.0)\­Ú§9J†\ ]q(?æ¥d8€›C+” ¸(ºâPNÌKÈphËÈpÀÑÏtƒE ÷ “Ro¾ôÀ> € À10pØ— áØºâà²û3÷£Ê"Ã኎• (`§J»â¨qM6/•ý¸Œ†"(—ZµjQp€WtÂã.8ÒpôÃ*ÛIc\Ù9ç(Rv ÃÁ1RHr2@’+Nr¥ÿàœ|^*»·ã6ÝÙv7Œëá®KÿK¯'ˆb¸½[™Z¨üè‡\,É•þãLéÚi^êwϰ'ÕиpžsˆÓge¸úáÀ ÛîKƒ!€ã ÃÁ1P«ÂÎwNÈp`ïœ1±9À¼Ô[ÖOF.œÝÃÈp€ÛËÁž— gÅU‰d82@ƒ µ@†àf8è´j2À¶œðy©tÝ 2P Mm7‡y©°—úœ®S2œÇ9àpgèŠ]f·§v®od8àøxîpmN8/ ÃpÛp;@†"ÅÕ8ê¼TFÙ8²@†À©ŽäÚ'û[8X@†à§F°—f48}³„Þ2áÎÇNç¥ÒtÈpd8pR®úô$ž—zK÷1\±Lèp%Ã@t8Ýâöm…ÛÒ7Ã4ápΈLƒѸÎWµdܸfëá”BÉÝ®s»ÚÞ—X9÷Ķ¿þÞw6Û"¹ÖnyÏÝ ƒt±œ“C>/UQН_¼¤¯Ž‹«à’\¦Î’ºãu'Ü›ÅôîÝ­êðšU{³ä‹\濬<¨irÿà'ŸèwðùMË6'šÙAÀ)û@†s˜—q8>' Å]#CBj4kßÈ3õÀ±ì‹Bœ9ýLº*´ATˆ‡Nï[½aocrJÌØ.W²5æ¥d8Ç`ÎLÎÓúº«„Bíê#å$fYʾCëWÅÏzæŸ]§s­B)H9_àY5ÜÀå‚€ë¸b@¤·¨¤cÃꦹÈõp²9߬¨õšâ*ZÒºiäìB‹".\ð¦òkÓ)%ní²o÷ùèMn­b[]^:;vì(ý¹eË–ì@€“œKèá£la'®vÕ#\5à !„¤ºP‘Èòe»†œw|Ë?)‘]j¦?·oÇö;6¨ÓÄÿ’"·Ñ,À¸È`¡JkÐ ‹ÉRœd‹ÑªÖ»iÊ4­û¶×4hY+ ²q‡^½[ÎýóO‚‰=€s²»y©4ä2Üi}C6Å\`VJ;êÔî¾’Õd¥2±7\Ø€Ke8T”wÚÎÍ{Ï$'صio޽Z>*aMÙòÝŒ¿ÎW4~µ#Ý2voþ/!» ?ýøŽ‰Új Btì!œóR‡Ï6®òEÕÍ»wÈ[³ué"£äݹ{c_•V¡(B(B¡¯r×ýTëwüþýYr®Ûþþv5 t÷.‚˨wK1 2œÓÓúGwêÝéâdÔvа¶%oˆ¾§wô=ìÀîq4ì@Qÿ"@†\í›—ÊT$ rh(‚r){pQ È+6y¥_Iñ^÷¯ú¹Æ¶è÷ÐC¥'¿[ðéö\ªöJXÃ~W9Ư½gö£N¸‘WÊ–ç ”X9Jõâ÷\yï½Þr.ÿ«~6\ëäãr}/Ù7´ž·“[óHõßYY®?˜~·¶I“&ø¤•+W0À¡ÜŒ3† F­œ=ñ†¯Jv‘ë—oñ×´çR½ävÿ¶]ɲË,í(*ûWüP§Ø Kƒé­Ø©n¤Ä*\ Tl]þ†ü“Ëw›¿ŸúÍñ±ôôô üÕž={žÛ`¾ñ÷3– ÀŽG»neUÎÄFÐzt¬-âòôd8à´ € p¦;n;&R€ 8Û9›:BØÛóRË•§Ëæ3‚¸‹·\»J'ÃÁÅ*àæÑÇöá€ÓímÁóRiì €³$xК"À pL5À-ØÁÈp¸ ûš— € 7… qávë–ÎK%d8…ày®\ìpÊyd8ÐþlyNef%2À™°v4/•J Ã9I ¡‡ ¸½¹0ëî-TÔ Ã(_¼l„ç¥V&5$.t°Ñ`TRžváS € ”Ed8æàÒn帉 \jãðÏKår(á(8jŒ³Û$Çu÷ ÃÁá“(UÈv‰y©°÷PУA†@ d8T"NŸh$Ð,Èp ¡ f€Ýmn®±!ÃÁaÔk®ÏÀmR‰óR+p\S €X~Š \Ž;VôC­Zµ( p*ŠRühÛF„JZ,\ýpåS«EÀ¡1/ ס¯ì¢@†lv΀ ”“ü´ÒÉpàXp“þy©·¸N«Ô‘w÷o²ƒÀU‘áÀqöTsÒp5ÌKÈp€ã4U¢K®Q=2ÎC†s€â¥\»d8š„öR×¹H½á@l`[·a^*;@†ƒ4¯]óë3R Ðz„mC?õ*4µ”żT€ €–be–*­ Ãá6£cDC Ãó"d8€ áy©€tɃ]‹ ¨<ÌK…“pá+­Ép¸}Í)PRVàlÎ|jÉçó·¦YÙBÎyæ‡ˆÚŒß Wnû¿7uW.œ”}Æ#zX]m‹“álÏÐdH÷Âï?_´ãTZnAa “Ìs²ƒÀmRÁy©¬Ëˆ5™œý0Ç&»ìøz¸“gä¾¼vÍœ±Õq—4a='ÿ6>tåœÝyl2¸@=EWåÕP2œGA {Ïp–̳ƈfUÝJ_Ðú× ÙLL@J¶.†»Ù$A¢¥¨ÉpWæ^§SèŽglÏ’!„Pò÷ÏÿtgHÇ:îl2”ÿÀæ´ p:ö:/U[ûééÏýرµÿ¤)#_Ó°“gkŒYûT--› ö‡6"óR2\e}Ú¾¿åÌ¿ÿ¼r÷™<]X“®ýzµ Ó³Áœ„¢{pÊ '„Úཇ·èm£¥Y2n\³õpJ¡dn×¹]mïË’ÍGwlÙyèLz¡ìÕ°wÿ»B4ì"•ã@EÙñýÖÌç×O}®×]MjW«Ñ ]ŸÑóvgÝÌ„KêŽ?ÖpoÓ»w·ªÃkVí½lqrÖÞ?~YŸàQÿîûèÕóžºÞÜŒ*W©:Ðfr”N×Þ±×~&%gó¨v¿ <òé7kz™Îíüe\Ç.ç6lx³qÅf5X2Çç´èÙ8Ò[%Û7:wàXv£æ¾ebZá©-;rêÝ÷p»P.ºàô¸Ž„êðÑÍåÙk†ËÛ1=Îíí½Ý°èö"CžܺW×);Fιˣ"Ë3g&çiýÝUB¡ö õ‘ö$fY„¯®ô¦äCg­nÕvÿ:÷tšQã[­i‡ŽÍÃÝØà’6¶"$IH½hÀíe·Ã…²UraƒÚ¯^=ÜŠÞN6盵^SÉ$­›F6ZÊÔ?r^J¦ÙjÕD´º÷ÁÞ=Zø%ÿ³lÍÑ<*(N*..ŽB¸|í­ñ»ÏpÖ‚ÜÜ\%úÙÞ©ãFÌÜšX¨aÍ=±ò“×Ö4y¼…ÇMŒª ½Ã²|é¾ [ -’OT³†5ƒCª5éÐ6ÜzöP’ñÒ…ì(Ãñª‰¢¦35]¥Ö/ŒA€Ðsíª€31`#ö7–šµ4Æ÷uÅÿ~çÂá~eÐn™Ô=Ö§"QUkÐ ‹©¤ãM¶­j½›¦ÌÙV¥Ö«s¹è’ÆÃS'gZ!.:%·lÙòÖF+€'à0Ϋë'Žå_qÌTeõªàRµ¾!¦³©r¸V%¬¹IÙŠW=ï²_^åì¯Ù~.1OòV Å”eÒùxëéSÜŽ¬IЄãe8•!´zM!„¦Ô#ÿ8›c’KöcM w•PC…†5þQQÞ{vnÞë×"ÔrjëÞÿµ|Tš²å‡Ÿÿõè4了°&õ<o[»ÃóÎjê„[’½¢Û‡h©TÎa(nsoP¹ç¥Òƒukp t8p†+"g¬}¡uçéG…Z£U•ìÍÞ=–_ÒûB T6ïÞ!oÍÖ¥‹Œ’{Htçî}UBX…¢QT%iÃÚÄv´¬Ù²ò—’!´aמ­ƒ¹ÉˆÓÖYá*EÞ?Ó¶Ï‹Ñ[µG´þÑúEwº8Ùµ4¬mé;üêuì[¯#{àøŽlw=YRv®^±%¯h7²¤Æïp\¬Ì&ÀyÈÊ7/•~€ w£<bc™ÿ›N|ý`åÕC]ªζçžÒ*˜kqŽoüköÊQz «;0hÃüݹl2À5ؼ±AëåbÌKÝÞ3W6šJNSúžï¾Þ«©ãÃ=~—b“^ºŸò¶vè€$ÃU\Öï÷ú>°î¢—B˜=»…Mê#ÜîPŽ5€ wU^]¾‹?œWÒ'©ô>aU‚=Ôl0ŒntÅ ÃÝ *ˆÚQl6NröæöÍ ày©ÎÖ ÌzgÖÂ+ýÊ-ú™wž‰vc«ÝôiŒ[-ÀY]c8ÏÞvxº8[†“óÎ=r$ÿ¢×¬™{Wm9+Üzvƒ 6GW@†³)C«wþ£Ìÿ-I}2äáåêzCfýôù½Þl2ÀµÑ‘̶ „°÷ûÃOÿ>æž:Ƨøéð®¹O4òbàà$˜ÉkóF…p­;AÚm†Sòÿ0¬uûgk_\¿eJ¿Zîä7œ0Àž3œœ½ûËG›Dü­úøMñ«ßï¡¥6UŽ‹á¨©»d×ÙŽÌxøžá‹“ê þ|öSÍÄ¡­›K¥ö‹níÇMâ@ À 2œÝ­QÁ¡ŸOBúfÄ}ß\ü+u§%ikî÷a«Q-Ú+gºÄ› ÕóR¯,d¸òñ¹…3gSpölݸœ‹Š"°÷ªp[ªßkÔÀ›Ñâpó`lµÂ ×á`w‡% ÄQ°ß2¶¹Žc ÃàÆiìxÝL©Gþ=p6Ç$—Œ k›¶o¨a«ÀÍb^*@†«rÆÚZwž~T¨5ZUI/½w%Ç—ôà‘©€ g§ë•÷Ï´ÅÁŸíßób´ë,à˜·Å†Š8Ó&áŠNåîÝ<ÒîܱcÇŠ~¨U«{õ#@ì#›R Ã]Ì£Õ‹ý>|~ô¬K'SH IDAT±OÞnP'9•gdT¤çm†At8w:‡½ŽRÈp壘²R6Í|¶Û̲¯Þ³8ó¯^Nó¬­[Y3R NPcpƒ“Åuƒk ³Øë½Er·O] û×™£¹ŒÕ<,l"..ŽBš½öÃI*}p‹®­ªxêØF4VØ}eà–³×~8–ÏÞóß[,Ú}:-+'·D•-`¿ýp9›'ÎÛ¶.£oó˾ê\×ÃÀuÑÅÅ–,Ãyvør×±Ïä‹_TB½\o1è p€WÒ`/;PQö:–ª2„Vñ8ùó¸AÝÚ4mԴͽƒÇ-:íY%ÔÀó]Q–n%Õ’Ìöá.Sðï{c&œn6lâ×ßÍ™ðl“Óztûð¿B¶ØóRGg¯c©ù»¦Ï·ŽZ½âƒ¦îB!úõïêßìþi»FÍjk`«á2Œ5@eàz;f¯ýp–̳¦j­«»—¾àV­U5ÓÙ › —Õ/\Ò°'ÃÝ6nµî Ú9eÁá’ÁÓÂÃ?LÝØ®¦;& ao·c©º¨¡Ÿ ü¦k½ˆÚÔðÈ?¹mݾ€çW­Ë-¸ŽÒy3•Щ`Ëy©Üæ×ö%á*ŸÊ¿óä]Çz-\¸jϹ}›ÞcûèTÓ@%aãÆ W9àÄïR”­I†»5ûš¡F§Ç_ëÄFÈ÷°¹¸¸8wÅÙÿ. j02\å2]øù£Æ+ýJ_ûá×ÖÓº€ó«³¿ gIÙ¹zÅ–7+öV]\†ç¥‚ª˜ W9t¡õÝwÏû㌹øÿ…GNßéÓ"RÏŽY&ÀÙÏjÐjç[ppÑ6àέÉËîýg`õ*;t¹÷®èðºÃÏ<:md2à:MÚ•‰ç¥ŽÎnï-¢ï3ïÐñ'~ùeÍžs…†®Oxè6n.Ú½ür™Ê¼ó'°*»]3¥àôÖ¿¶Å§[4z•1éß%Ÿ¿óú;ó²Éè¢pr¹nŒpþ‹.ìµNN^ühÓÞkÃÚ·ªæ¥)Ù}ŽÌ1fÓcŒ qçHd8[ÊÛýíÎ6ßX60LÅFºôç;sjÇ-ǼTÀÑÙkB’ôÞþU¼ pW™þŠBÀ칡*s¸J†óhñl×ãÞÿeÏÙôìœÜV¶XŽy*bÀAóSeb^*»–ó·“½…o¯c©ÙÆÏܲ.»OÓOʾzÏâÌ¿zù¸jtãª5€6(LÀÞ3œWǯöË¿dƒÊêE@yp;*2Ü-¥2„Z–/X¼ë|UB(VcN õc³'´÷d«pº3ÄJM±ÎxpÙï½E=Ò²ßúðæÁ§wgFÝžº}OZÓa³Âtì‰È^6À¼T¸ÐAê¤ìuNCÞ ·Ö˜¸}ï¦oŸ¨×hÔ¯»Nýº›Õ¨e°Û §óÔU¢C´úªÍýOo·‘G…\êŽ?Öð½3¦w˜õÌÖ5kVyõnâsYŠS Ïn]ºþ ÷¡s¢áZ’«ëlk‡>ñ€ãe8¡©úhÜñ»âsBüÃ&m^Ûâ‡ÍYµ|â*êŠF¸ŒÃñ9-z6ŽôV‰ÀöÇ8–ݨ¹ïÅ!Μ²{ùê³Õºt2ýµ<•á*@åY­®§BT¹ç±Ñ÷%ïäþ$·†!šÖ`ÎLÎÓúº«„Bíê#íI̲ß2ó\­™VýqзíÃs7³g°/¶ž^ǼTÀÑÙ㜥ðäŠO_xô¡Á/}¾.Ñ\”¯2¶My¨A“·æWl‘²9߬¨õšâ PÒºidc¡E)ó™§7.Û®j{w i¸¤Ú^ݲ-Â3ú@…f÷ì¯NÉX=´uùº6Ýjny¥ã’ãÿûŽfvÿ˜Q+”NoÿØæ&nð+©.l=Y¾¤f’óÏŸËÎÏ^·`Öº’—ÖÎ_Þ·ÿA ßîØ±£ôç–-[r\À%B'rgGWœ§Õá’ì/Ãåï[°Bô_öß‚ÿ¼¿_ltÿ£þÝ9;¾Ëg[猸é´­°˜J:Þd‹ÑªÖ»i.ìZjŸÆ=û×)žÊ`IÛ±ôÏô&½º×÷¿ôÉmp­–+õ/àLGÖíšÎeí@žÓp+X2Ï«?ÜÄW³a÷¹Sçæ¼µaï;wùÝÔ§Ö7ÄÃt6µ@ת„57)[ñªç]ö˫ݼýÜJVÁì¦Ri=|½ÝÕŽT'ÒcTy-i ¸ÁƒE0[įý’ͲJSž$Á£Ê³Ó_»É'„ÐøGEy§íܼ÷LrÒ‰]›öæø×«å£Ö”-ß͘ñëá|Ž7ÀUZ®v]ÔEMJÀd»_AŸ_[¬¤:°y÷yk¶.]d”ÜC¢;woì«Â*EêJÐÔ¾¯Lªp.\ Gëd¸J`IÞöûÏ_Iþw27]µ|QÜ!„Іµ¿¯}XE™ªõîÔ/ºÓÅÉ.¨í am/+”ໆ<˾”œl¸*pñ¦)׫ánŒÚM:1㙇g”üûKýäÑý÷sËc}ØjÀíˆqTßÎ…y©ÎÖ|b—çržà àöáš<¶&÷³pcA_p«' ÃćnÓÈpp’È64n$þÞH1Þ’¢æb8€ —?'á–ÚÙî2œó¸öÈW³Âæ;œB\\…áàö¡¡Byd8¸&¨VvÙ Ãà<d8@ågÓkôÆÙ´£Žy©p Ñû€ §ÂàPÌKÈpgÁ¤T€ 2na7RJ®ÜQÁ°5á`_geÿoï¾³(>ŽÏ>5½7RIB Eºô¢€"Ær"¯ŠÊÝÙ»žÀééyvi‚Šw*¢‚ˆ@@z DBK$”Ò{{ÚîûG A áy6ßÏ?†Ççy²;3;û›ÙM«ÁºT€ Ø%fË Ù‡úLÀ“á€ü}}±.•£‰c™ à|€ ˆï ø¶ý>ÇÂmd8p@¢µ7¹ß™óh™v‰u©.à08 ÄÅc"÷ñ\9Ö¥d8ø}I ÔhQ Ú3š E‘á€ëÖY0øáà*3wªÙ÷@…u©¸†8üÉpÀ) ­!Rd8üÓ8ϵ¦½c]*®í©d8À!cñï?¯3=@ÅÃ\K:ŠàФ§§×ýMidSê ¸H[ÎÝø´®¥‚; ·F¬K%c]ñ!ÉmÓd8­î4@ÖZÃH›±=p­±.•2p&Zß1þ{^@憽Ç8Wƒ[MAÐrcJ“$Žèë‹g‹@kĺT4ϬiŒc®e1 ™ºòÆ¡y wåœ6€ëxøƒ 4'¦÷ÿ…u©h†qWÓK«D:2`¿ãBný&a¸|’×3©æKŸ¢î¡~™d8á´VLê´€fš/g]*@†­t02€Öq&à´a7X— á@†à ˜]2GÅ‚€1Èp8Áà™:u*ëˆS ×ã¤ÂÓááÀù ™]+v9ÝźT€ û>m„9 ÃÁNŸD·kš‰›7£p7€ €ñÀ‘2àà½ýcË;Š…¿— áÀ°áÒpkÆ=‚5Z¸›d8\’Ž"ÀÕ°ó«0\ úLµcÀõ;gp¿ ¥€ Ðz´¢k©Ö’”›ö¤ÔJ.AŽØÎã‚«Ôdر7-«°Ü$¼#{ Ú#؉Q.°K­fÎZ¸íÖSÎ=ÆO™2:V“¶iÃá2ù‚7ÈÕyyÖ€.ƒÇÝÃj}{M_ÿssñ‰oÒ³+láF-m‚'i& êv§µÌÃYJó«ôÞ~Î!„кyJçʬ—z·l­µ ƒ‹K©€Ëqôõ%ü•<2œÝ“-ÕEkÔÕe’ÞI'›j­o·JÕ™ƒ怘H7V|ûÔŠÖ4Hšó%Y¾ä¸ÃVztÓÖ,Ÿ~·Åz\$Âíß¿¿ñçn¸ü¡ I’Ä4á.E£wÑ «¹aâM¶šlZ£“î7“ßreúÖ«:›ÜÍû¢w‘ۀ]„›V²Ÿz¯@WsQa,„¶Ê¼rÅ=ÈãWV®ÊØþý–‚¶c& uÔ§Š4Ë<>2œÐùtèàQt`×áÌü¼SI;WøÄD{j„­ ñ‹… W¦U+œ³ó» §¼nØÑ¹²°     ¨Â {Øg¶i-;ªõë9vHÕ¦=k¾5IÎGŒíæ¥Â&EE¡X+ +•ÚÊÄ5g>¢žxïèP=ᮽOçáS;¿0Ùù¸çáu?wœòpGõãáa€*ðô HÆ ÃÀïÇd×ÏŠ+(€ F„.{0*d8 ¥1© à ¢üá[ §NJ)d8á×HÃ0ëR2ü<^¸61’ À ‡ÀÔd8€½a]*@†p LgÈp¸²Ü—źT€ 2~h^\t¦q©ž–Šœ®¸äIKŒ(pd8€`]*@†\?\eÈpGòÇ.‰². À ®×ÖÉpÀéÇðà 4=|[K[EÀ.²A¤e#2ëRU;ò!À‘áêŒËPez£f[®¥â²=S# d¨ëRUuÈpÔáÀ‰Ã-8€Æ?!CWO†ŽàÐêq?œzÇå £„4!IÐÕã`ä:J´F¬Ke`2È. ÓÞì è.ߨhrt à µ@MŽu©tptÜweÒÓÓë~ˆŽŽn-=Eݳ¿é2ÇÅäM ô“-VôÆ Ã]ÖÝ8hV»Ì žs?AêµTüŽ!&Ýð;ϲ×ëH¹L>ãàU_‡ !˜‡Àù©y¶¼1*ÙçßÿÍÆ°.ptÌÃáwc4Õ'Îë8=ƒ–¬ë_=4ŸJƒá@Ç¡†Ò#ɵÚ0׆߽.¶1¬Ku°>„~¿ÁµT eO®Ä8?m Íy8ü!>®´ éˆÑ’x@†ÐüIŽ3+€ ‡ ¢5MÅ]Ó9 ®õAú¿0ëR9®A†¸.X4 •µgñ?¦…X— áК4žÔtÉàWô"ÌÈp€ÃÄ8žÎ Z χÃÕž!Ô½wd8 ªöÖzY— 8:®¥ᎀu©d8áÈp5`]*@†pí±. à¥ñwøcøË.¸˜‡ ÃëR2Èp€ku©d8áÈp5`]*àèxÆïyŠ)ïÐÖ-û3JÌZ÷и!#û„9K” °GÌÃ52gï^·;߯ßÄ[' ‹¬9˜°ùd5Þ V¬KÈpª‰pùÇ2Ì!ýv hÓ¾ÏíµYG2 q€ g×äšÂb‹³¿—ABG»R–[a£`€=â~¸zŠ¹Ú¢è úº;à$½³^˜«Ìò¯ß¶páÂ_½òðÃSz€ wýHRã´¤"+½ŽÚŒ‰máÂ…ä?× ëRGǵԆüfpÑ «ÉV—Ü«É*\ ®Äþýû)*T(¨P2\Ë„‹Ÿ¾º Ä,„ÂZž[!yºk)˜fžžN!P¡°7ëR©PRáœ> 6ʵ'1%»à܉Ÿwž°dž¹ð|8`—¸®‘!xÀ¸þ[7ïZfѸ…ÄÑÞì“ÔivÂ}`þ`}\\ÜUü¦„„„øøx‡.¬ß.J­ÐåW%_Åw&''ÏÚnùýïg®Ù*  Åp?××R[œR“µoÓ¶äÌr«Þ+²×ðaÝêZ;¡XÊÒ·­ÜR3ìÞ‰†º—äŠôÝ›v=W%ý:ô1¸³Þá+ñÀ޽iY…å&ÙàÙcðÐÁN’ +×V’²sç¡3yÅUÉÉ/ªûÀÁ݃$!¬%);6íI+¨•\‚:1°‡Z†ƒ¶Òcë¿Ý–ÝfÂôñá6]¹ôÀò¯~.=ÿ‚[Ümwðטòmݲ?£Ä¬u2²O˜³ º%¹&çpâÞC¹•mð°;oŽq“ÔÖt岃+–%^xãUàð{'wt®U݉F©ÉNÚ¶ãੳ0xGv4¤gˆ³$U6]2œÝ¶ÂêôÍë“*;¿¥£kÙÑ-ÛÖíòjTÍþÕ¦ÿðù†,«Rh“^æÈ†ŸR¤£¦´Õç%ý´mÝ~ÿiýýºéÉÕyyÖ€.ƒ{ûj2“vì]·ÃïîQáõU®¥4»Üµ]ŸÚÊ3ûwìY¿;ðžáÁRáþµ[Oyõ?¥-sϦM<ü§Äyª ÅÉUé[×ì)RTÜt…Brë5'vlNØìsçøöþl%Å”µó»ug}âzê멳)nFI«Ú𮯭ÃÈ)A–ú&«˜Ïí]¿O«×õõEJeÚÆµI¶žãïîê/ò’Ö­]·Ùë®ñÑÚlõ5Ý+j¤ª–m†µÙG³4Ñ7öíÒ©ÿ€PKÆ‘<“ŠvÐ)zâßs;ÃùcH®8Rà;¨WT`@X—½ý+ާY{7µ¾½&Œ×.<$¼cï±næ¼ì ›+×)räÄá½b"CƒÃcnèÛVo.­°(Ö’´¾½u  ŒìqcW·Âcéå² ά=kvTvwcHýßMVcÓBIïáT/ÐÏM/ÌùÇ2Ì!ýv hÓ¾ÏíµYG2«ÇÞI[Éá'ÝMÓ'&"¸MHX°§^6]­‹o`Ce¸–eäÚ÷ 5ª°/²–eX}:u v7ÜC»ÄúØŠó*mjlºd8{îW*òÊ·@º‘¼Þ+ÐÕZœ_-«|Ÿs+„G ‡V!4N¾¾Æšü“zŽ2ÙZkƒ¤æÊU¬U¹)ÉÙÚˆ.!N’¥4¿Jïíç¬B­{§Tq®ÌÑ£¥à—u?e…ç§—TÞtå’=_}´pá’ÏWn?^jB®),¶8û{Õ¼tAîJYn…ͱ÷±âÔ‰R>kÓ²% .þ÷wÛŽ—Ù„PeÓmdÎ9p Ð·W¯65öEZÏP?mþ}饡˜Š²+Áa6Ý+õÔ>š«ÍŠÎ¨“ê‡ÃN:a®6˪Ó6SEch8/jôÎza®²(B¨bº[©:s0ÃÐ;ÒM£”«´rÍg×~öã›6 çM7µwÓÈåÕEÛ°ŸBÒ;éäòZ«" [£råñMkS¼†Lîl”,ªnº’sÛcô.NS~Úž›Öè¼îè+U[]ÃŽJuûivì3¾µ<¯B±¹»Ç ¹¹¿¦ôDâÖMk ÞScU×t›4â²Ô=ÇE‡ =4¢ÂÆ-fô˜¼oÖnü*m¯¯«EÓyÌÄP£R¨¾¦K†³ûNTÒHõ=†" ¥•í²PdE=ûl+=ºik–O¿Ûb=4¢ÖÊÕ·t딸ҜÃ;׬“¦ôv¢I… ÙñkÔV‘}®º¼vý¿S_úñÓoÞÚ]}MW2úFDú !„ð÷w.;ûMFFÙ ÑBHRãé]û)›Í²®M\ÏŽaz!}†f|“ž^㬶¦ÛÈ”s ©È·Ç¨`ƒJO4Š){ÿîÓÞƒn›à]šôsrò®äˆ›{iÕ×tÉpvÝ\ ¢Ìd­)“E2¸èÕ}E[ktÖÉ&kýÐH¶š¬’ÁE¯†qoeúÖ«:›ÜÍ[«æÊ•ôî¾î¾þîU'–%¥÷êí¢V³Ui¬Q›Öè¤säÕû÷rG÷ú&j9·cÕåÆ)â=ÊÏ©³é6T¬“»Q²š,Âà¢V“­®F«É*¼ ŽÝr5½d«5Ù„Ð !´NžNÂ\cÕúª­é6tFe©‰'”vbêÖg¨°/’+Oì9fŽžãï£óîµê‹í‰§: S_ӽ†N¬jÙ<ãè©©È-¯»^o.ͯÒyû»ª<ù7¹GA®-*49ùù8ü2w¹*cû÷[ ÚŽ™4 ÔIj•«Xj,ŠF# ½W «¹¨°FB[e^¹âäáØÃA‹§w#wƒF£wóô0êUØtùüD…¥ä\…âæëªsñóÑW”˜…BXËs+$@w­cרG‡R|¦°nŸlUEU’›«“úš®BSÖ¾¤bŸ½‚ê=ÑXj›\#•ŒžîzÅjÎêkºd8ûø†Ä†Èé»~>q® '%11S×66Ȩ¦=”-&“ÉlSê~°ÈBhÜ#;ûWÝ™t*/?óð®}…®íc|¼Û4çìünÃ)¯vt®,,(((((ª°õU®%kë÷ë¤gædeÞñÓÁrö}t:Ÿ<Šì:œ™Ÿw*içá Ÿ˜hO5v%*lºÖÂ}«VnÚŸz:+óäÍ[Ò•ð¸H7> 6ʵ'1%»à܉Ÿwž°dž9øã4â‚­Ç·î8š•Ÿ“–¸-ÕÖ½»A•MW.=¶ç¤ˆî×ÉK£ÞÆ#"Ò£:eWÒ™Òšš²Ìƒ?Ÿ²Æ„¹Õ×t¯pŽÄè=Wô Ú   «øMééé]»v%Äé½Ãüå̃{~>˜’m î=jhG/ŠZ\ÍÉÕK¿Ý}²TV*2ýòK¶w—N¾£˜wuÆÄ}‡Žçk#Œ忨%Å”“´ïTYYÖñ”cõŽW…tkç맲ʕ$kÉé´cGN9™còé2tLßPgи†¸–¥íKššQ¤kÛwô°NÞ:Ih݃Ûè ŽìÝ“tìt•OÜð‘Ýý6Éè(rîÛw0%ËܦϘá}t’›®)sdžCRÑý®¨óD£q Žð6g&ïÙýsÒá3µ½Fìæ¯—ì·éÖÔÔ\ŧòòòÖž¹‚ER§Ù Wô æÖÇÅÅ]Å–%$$ÄÇÇ U+..¾ŠO%''ÏÚn¹‚hKA82d8áÈp À @†d82Èp`”êÌ´|‹=| áà2¬gæ÷ÐG>Ÿ\+„åäûCãfn¯øCß×äK*~ºÍ;èþ]U2€§£´rMaQÜ|_âÜí©¬q¢d´8æá´¥?Æ|-£vG|ûÞsÕ Ó©oŸå*I’SØG—§× !ç|ÚÇ5ö¡?ŹHNq/&U•'-¸¯˜›V’$¯éŸ¤Ö\ø%%‡Þº÷þ…©µB˜Ï¬|fL;7I’tþ=ïzoO‰,„œ÷y·Î~â–nmƒýܽÚMxuG±L5 ÃÀñšðÕO/F9 úêľWºÉûÿ6:~yÐ+?—š+Sæwßxߨ—÷W !Dõ±UÒKɹÇW>ÙîØß&?ùËèågMŠ­ìÀK®Ë}zsé_Ò0W“4{Ì´e¾/ì.6ÕœùzÒÙ—Gßñy¦U!ªR¾+½oíñœ‚¬o96û¡Å'¸®Zõ/}v¶Ïkÿº'ÖSïyóìׇæ~þÑ/ÕB]ðí³njåãÖåù͇žïïe+;—]îìn*(¼è•Øš#ÿþÏ™þo¼3½›·Á)dسóþì·uÞºs6!„>ôŽYcB BòŒßÛ£ %ÏLÙhÜ U2¦šwݬ½·ñ%§‘ù&!„Î+Ä«®k”¬y›ß|äï_&æ9…Çvï(ªýÅ/…Z‹N•¹EE¸×Šõ1Ò×gK¬B­«¯«¶îe­^£ÈŠBÙhÌÃhU¤úÿê½Ã|Ü'þP¬Ô±Uœ9žòßñ^uï©{“ùØ;SZÛeAjumÑ©¤_æ«Biú%£aŸÊŒ³•õ Ïœ{,Wñ ÷f € Í’àôÎ[Qêñ¬R}™ñ>ž|qÅñ ›µdÿ‡·vív××9¶¦o¶Õ”T)Î>~žÉZ¸wá‹gZ,µV¥É—˜ë'Õœ»N¿;,ñù§>?Rj1ÛöÖ£ <8¦–@†¤;{X èIDAT€f`wgïì—ûw{d§ÔÿŸ—ŽMyº»‡Nï3|¾ö/ß®x ò‚™3çîÏÌÿ‹ÿ'ƒ<îÁ^<}Ë«¼Ï8SÓôKªÞÚkÎúeSÏÍéëmpŠ˜¶*tvÂ73"˜†pM¥f'\ÑæÖÇÅÅ]ÅoJHHˆ§Ä€º_ŧ’““gm¿‚¥ëÌÃ82d8áÈp À @†d82ÈpàÚ“Ë÷½9®ÿÍÛ^$SÍÆœ¾ì¡þƒg.;c±ŸmÒQ-¨Gͱů­6ܳäùÁ¾ó4rÙþwüë—âÁリÞV/Ì9[üsѪ}gË]"‡ßûôËwu÷ü픎­øÀ× æ/ßr0·ÆÐãÅïçOÔ!Ìçv}ñÞ’•ÛR‹Ì®ƒç­~c€«B©9½iñ¼Ï¾OÌ(µݾdÙs]u¹›½þÙ¦¤“yUÂ+fôý{frŒ‹téVjOýðÆìEkÒJ%ß®·>þÊ“#ƒõMþ·5kYümóÒ/øH윕§=xÿ²‚¦/ê½÷Íßßñ𖚦¯úNYòý#ÒÊ·?üfOÊ™³Î/vÂŒ§žšÜÑUºhQX/¶ñ†è;^›{ø®WÞX;tÞ-ÁZ2hNrÞÆùßÖŽzëîçÆ|Tºô©—¬Ô 7!„µ©‹}iUÈý³÷sÍXõúO½ðß÷Æø]âäò½o?øØîè{îŸûH¨³E ð„¶üsîyít¿ûþüΣ:“iBKæ7þßâÚÑ÷?ÿnŒ¯d2†ê…0ç$¥êúÜ=ûÏm 9[æ½ùÖc1ß?ÑÙx©­6XüÔ["f½ÿIwËž/½òRhÇÅw‡(Ú€1ÿXÒ¥J©ß¡ªäEO.1º:GÞõÖG#Mõ¯ÊÅ;ßxaµ§Ñ5ö‘yKã-õ¯ZÏ­™3w¯‡Q”9X=é¯÷tOmXøæ›/útYö—ðŒ‹…ëÅ7^ã;üч¾šöñ§‡F½ÔÃ… š5gÃò£~“žêíÑ0ée>ûÝKO¯jûô¿~üÐ:!„¨=ñýê쨙ïß34X+º<:;u×ÿýwGþ¨ÉAMBœùôŠ77=÷ù?n j2ãTstÉoxõ˹7z¯R¶ëÃOsnzgùc]]ÏO´9÷xòÞRÝ¿;;%m˜ùËÑB[çKL_™2Ö­;ýàw ÑŠÇÿ8ý»Í9Óî oœŠ“ ¾í»ú6îãß;æ>æÝ¾žÎ®^]Û4lrÚ¢WOOþ{¬‹›¡S\XýÆUøq[q§û'¶u i÷¯·ëßÚ»9ᇹéÅ–ZÓÅ‹âR¯ >½ß‚—ük÷Òõ¯mî‡@%äÒC›ÎúŽf¨û·­`ã?[¬}pá3ƒüò“­º¤Jqòt® Ná=BDÖ‘SÓ¯±älßxÆÉuú èwÓ¯­Ë4 Q›ž°­Ä]Y÷ôØAz 2óÃyV!ªŽ}¿ßìUôÅŒ±ƒz=ù©Ï”ÊBIjÈ8Š¥ªÂ¬u÷v¹dâPªÎ¦8GvðÑ !„>¨[¸6'9Ët‰7Wþ²ôãífLïÖ$2 ¹hûü¯‹û?t[{Éö‡ùë•1O>?ce«ÊIüú¿'FßÞÉùREqÉ—ÜãÆÅÊ·dÔÚEu“áP K^J®>ºWˆA!”š#‹ãÜ”æÞª?ŸwœÂnˆ–Žýç«}ùÙZ‘}4µÈf®j¸ YÇœ}øœl6‡L|î£Oç½4ºzÎã Sk+ϤZÍJûi¯}´dÞ_{å~ùü“+2k ÓÎT›ÍN=zcñgoNJYü—Ù?4YLaÍNXº_ÛrÏKN\ÉÕÅUŠÑÝ©þ ZgO£µ¬ÔtÑÖìÕ 6ˆÑO nz!Ñt|ù¢ÝÞ·>2È·I¬Q*~Yúññè÷ŹÕ³\°æÞ#n›õµËýo>ÞßSó?‹â7/¹Fö¬J?UiËE¸– €JX«ŠjÝÚxëë3Ìñ}%© â‡-høÿ‹§Ütú£•ÏÍ}æÐ“ÿzlì„>1ík…³§sÓ€¥Xª*mÆîñÓÇ÷q¢kô³©[flÜtz¢w­ÆïÆ?Åìb¢sÄs¶ÎZ·7¯[Y&lÚŸn®¢ã Ý5fîšC£FxJB¥:哿í÷ܲa¾—Ÿ4’4ÚÆÉ/Ù*+—R‘´ô“Ñ3þÞÝ­é$\á¶W”ô{þöMo¸³f­ž·AŒy»IÚÓø ™ýUDNú®ÏÞ˜ù„ößÜvÓeŠâ¢¯õ öÒTVÚ„ÿõŸ#à& Dã;êõ¯º×ÖÍÙ²¿ö±­7¾÷Î=]œœ&ÍývüyùÕ_¯só§Ýw,¶MÓô#é]\´–²r‹"„$„Þ+ÜKª*®Õ…•ê’êú(£oˆ‡”Z\«w3*ÕEõ¯j\\åÓ¥µŠð”„ùìw/?ñÃ}‹çŒms¹¸¡qñq¦Š†‰7[M…YïáeümF²f­š·AõöÍ!M¿®6õ«öxMùbÈ“på–~r2zÆ«=Ü›¤=­{HÇ®!;z_?ã›Í9S§GD]ª(~ïÆ_O\K@%t®¾NÕyåÖ†ÄÒ&"*22*22*²m¸¯Qcô ósÒ!„ÆàÕ&4ØÛrèëŸÊ:Ž»áÂU©†Ð¸6rú®´ªº4Sp¢PòoçïÕɧúhb¦Y!”Ú¼S¥º ¨€ ÎQÆüý lB!WdeU¹†…¹k„ù쪗y¯ì¶…ïþ)Öåò÷ÿK®ý«ÒÓŠmBaÉ=œ%·éü›E¬JùþO?Mš~_Ϧ±L.Øúá·%}gNëxÁ$\æªù•Qßz±üe­*­‘5ZMÝ×\¤(.½ñ¶òÜ2ÅÅÏÍ..Â<*¡Œ 4%&åX´×_ú]rù‰½‡Ê\Œ•ië?]°ÖëŽO'´Ñ Q}à•q³vôzï‡wú¹†‰ïõÉ?ç¾Õí•ÛÛæÿøöêÚ¾¯Œlãê5åöè•‹ç|Øþéq¾+^ßå<ö>Þº?Ý0cÑ«‹‚f Öüx~Jð¤'º:o~yæ«i}_øÛ@}öñ!„ÆèÑÖßh:¹æË„’·Æ j@ŒQã&/_òÞŠÈûºšvÏÿ*7ê!zaNç—~s×,¼ÉW#,™+ço#ßžvÁ$ܱeíõšüùЦ)T)Û÷éÒ“‘Ó_íÙ°zT.Úôò³ÛÚŽÑ#©ôðê«+:=>$HwÑ¢‹7ϹèÆKB(U§~ÉsméfS`d8TBãÕmDxþ·[3le¸t†+Ý¿ä±÷Se½oçá3—<>­[ÝT“¢ÈÂfB¡m3ñ· çüóƒ‡Ö›\"†>òþócý5BDÝûÖkesßýзV÷öŸýàÙ¾’q³Þ}Åô÷÷ŸðÉ7îÖ9 gvr¶9u²LÎÝðê#~iÀ]Ÿ-²múŠ…Ÿ$ÜðÞ½¿šÆ2´û¿7ŸÍŸ½`ÖŒrwì”W^¿3B/„YȲPêîSÊö}¼4=ê¾ó±L!äüM~WÚç™;:95ù6KæÊù?‰‘ÿj’ö4®‘}Ú®\¾pöÒ¢á1pæ»ÏÝ®Âz‘¢°T\|ãcŒB©8´î°&nN”“]T·ÔivÂ}`þ`}\\ÜUü¦„„„øøx0®9oõŸ'- û`Ås½]%{Û8ËéϦůðÉò§.ý¸_{fË]ýàK¢ÞÿúÅ8çË¿³¸¸ø*¾?99yÖö+ø[^Ü€jhGͺ͸þŸ_¯µ»m“KïÊm;ù¶öà„\²cþ¢ÔÎÌèâl/•M{@=œ;=ð„ªÏž~}{±ýÍûªÔm¹1ÓÆ†ë±XͧV¼ôâ¶°YÏk£µ—mâ~8ÔDãÑû™u»Ÿ±¿ süÖƒµT ‘Óæïšfg5Mcp¼´Ná@†€ 2Èpd8á@†h%Zî漏3¦¸¸˜øã˜‡ À 2d8áÈp À @†d82Èp À àÈtWñ™ääd À‘2ܬíJ àúâZ*d8áÈp À @†d82Èp Ãáà8þ Q?ð ¬·IEND®B`‚PyMeasure-0.9.0/docs/tutorial/pymeasure-directoryinput.png0000664000175000017500000001111114001111765024252 0ustar colincolin00000000000000‰PNG  IHDR¾IýˆÞsRGB®ÎégAMA± üa pHYsÃÃÇo¨dtEXtSoftwareGreenshot^UÀIDATx^íûsוÇõ‡ä‡”ÚJ¶v“8ëªMvkpb‚q6[[åe[µúRR¸’²SlAÖ`' ±ŽclBŒÁáé2Ʋ ¯ØYˆ‡1 À<BðgËöì¹}Ï}ôczfN÷Œz¤ï§ºF·Ï=÷ÎôœnHêny€7«@©s¨„@ ê!P«³vfË̵ÜΚÜòÍ…{8 Å„«CóÄåpÁ= ¿Ù@y_ H¦èê4¶œP§²«¼Ý ù cÞø ¸–VŒX0$=BÆ3¹Åp²êš9“žDí'öÚ×@1µ€Ø©]Hõ» x†P/á?]´o ’‹:Þ{ëu‰Ü)G½õ1!ü ÅÝh‡?6>8Ôomû"Íèµ3ƒ–Iîõ&™²ÊrZu¢o·´©•3 ´x%rᤱi½Im·p¨çñÒªèµ3NM Žz›kW'´Ó¨VllzobÛ ±$Nhñ'Q¨”ÚS‹ú©c¢¡¢š7ÚõûÃ-IASÓÄÆ¦÷&·©á¢‘´”^†ž-š2ÔqÕ >QšÅ^CaÍ´£8æ’N‹’B¡&vÏbÒÒ{Ë´Í Eð¨Ï¶c½Þ@Î#B¯{J!V'¿T`ru€¨„Ô ØŽTZø+5u€¨„@ ê!P:@ÔB u€¨„@ ê!P:@ÔB u€¨„@ ê!P:@ˆP{îi?ÌÍÌn¿‡ÿÌ»µSïŠ'·c³Lª£ê~¥sQÔŸœÔ¡šù+G<¢ŠÚÚM2¸’»a*ÞÓií­´«&pº×Ïž7't1·¤3Ø?ÌÑ-F.êtR-\T+ñëá: Ôk'tmÊ3i:¨&‰Œ °‰v,7_†™:yN*¯¤‡:\'Ó¦J¤G¸h¼£ˆäë¶è™$œ`‡ø ?_%Ç^˜j+½…é[pV4ÍN¢ÑüDu4¨fx²Cbæ„zÍþ$L$Í´Õݶc¹7õeèöáöV7MЧRm(‹Tõ=ª1¥òwã·K˜Ml¬*¸>ù£‚2»Â›4ÕCŸ¿MœÇ¦~Læ¸m§1¾¨!6 ”E¨NÍ„«&Pi”:`Òu€¨„@ ê!PªóKÐ$pÁê€\(©÷:!ÊMj!Ûª£*ä}s[tmâÕª©IE°ºÄÒ#i‚jÉã„¥íñêAÍäUGesˆs¼óˆŠè%D§ÄÚ5ºš.7‰!1³Рnº¨™ì“ƒ÷ßZÀ·ẚ$Ý6IîV#Áø€ Ö63Þ`Üw¾ŒÇâ‘ä§M ªG¨P:@ÔB u€¨„@ D¨ÿi!(<\°: W‡ÿ¬¨„@ ê!P:@HS¨³»Í^‰Ô»l©‰¹ë•ÚëœjT'y9¸ªiN]VÅ–;~K ]%Yu·µ™í…µ™i†U'¤„%u~pŸK:ùÓ´ê„‚´CKSØ›P‹Ôaâ3MªŽ:ÅÐA­‘G(1q ÐŒê”-?u„NK Î%„€ŒfSG­)¡Ú»{‰Ä•²íGi5zò¡èê¨Z¨æþ.¡Ä a Q)¬:Þ0ˆ“Ͱê€Bu€¨„@ ¤ ê€¦€ V„êu€¨„@ ê!P:@ÔB u€¨„@ ê!P:@ÔB u€4uÜMì½¥¢÷ªÙ(PlRÕ±õî-¾3Y-@ÉHuêºüÞcp+asó3½4¹Û’Ù»ªQF¸Ûí¹§™©TŸÔÝ4“ªÕQµ4wÙ ÊïÕØ4uoÈ€ŽJ)ÝÅI¡©¼É¼YA1‘ªc»‚Ú{¤ÞÄ5:0žÌÊÀœâ#?a9BµOD&yHsš€*Õ¡ZÅÔ•ÕÛt9´¼£p—̱ÐTªsš€TuøD˜RêJGê›Ñq/â”)ÿ19¬IB4u&² æ4…S‡ÌÑ (8Å[u@“u€¨„@ ê!PIS§eÁ¶I¼q™¥TP‡/g&Pgêr²kÕ…w7ŸÛ¿þÜçz7Ÿï{idè÷UÔ™º\è{qüÐs7®»uló­ã[n¼zþð¶3½[Ͼû²Úú:Ïö½råÔ[œ£)ÕiiiáÈÀùÞM7ßÛxëø‹·Ov~4¸ý£¡]Ÿ^ÚçJÿ‘£w®ÿìúÀ_Ɔ®Ÿ?tkô<u¦.çû^¼}bëíÁ×>:½ãã3ÿûɹîO.ìûôò²ç³‘£ŸžøËØéÛ#WNïçaФ޽ÇL% N.\è{é£ÓúøÌëÐõÉ…½Ÿ^zç·}wFŽ|ví8-Hïï\rbׯíxüȶEÝ«xŒGCԉܥ¨Ì=Ò”9 †ì)#MÁ- ;?þàÍ‹û× ¼¾tàŽÁ7ŸÜóÛÁî§nwqßS£=‹®uÏ×Ûñ‹yŒG£Ô©b9Qw«î gBzrñж3=O½ñëѾßûÛXÿê«o>rõŸÛíÄëí<ÆcâÔ¡`hù nÌÉ4»*WìÒWN¸xäO'__viÇO/íøIúv²{%ñh”:ºð-GD}KÏDuT&ßWOߤ‘æö@&.ÝùÞö…g:ÿ3ezù?·ÌèYÃc<&nÕ Ã7ƒMTGuP'>8øÚÑWæxáß"ÛûÿõØú>úüôÃÏÝÛ·ò»o¯š~ãJPÇ0QÇd$«£qA\8²ûÉã~þ‡‡ÖÎè[õ½Þ•ÿôÎSÿÖoþî­'¾íoÝOÏ(}ù%ñ˜8u(hOX.üp©*…Tq÷æ<¨“ }Û?ð»ûö=ùÝîeÛµôoÊm=«gñ€0RG-–@OgŽÞá4û¯x½Þ(t€* ²±oÃOv/ùú®Gÿ*}Ûÿâ<¦!êT‚?èT ÔÉ…ÿ3íµùw¥o¯ü×Wßï~Ž„)‚:µšurà‹/¾è|ì_Zð×[æ=eÛ¶|úøø ¦«h<Ÿþù¥K—N:500p² Ô544466ÆcÂÔWÇÛW`›Ä—YJu¸&P‡8Ãí÷~xëè·Ž“ :.::Zoêõ[‚„¯Î¹sçÈžÁÁAz>r¨»|ã vóŽæÙ[|Ös;ˆû‘jrt[Ê /¼@oYEvíÚµ7v®Y²dÍν{·<ùHðU¸e›Ô÷È“[L„Zê GfŽ\ ã¢£«ãï&¾:çH ⃊ô<ú­o=ÚÃ;šu?nùñ:?®ÛôÈˉ¦ºÝ+¥³³“>V¤»»»/;]—.ÝØ¥ZÛVÎZ.´ç¯Ü–!"»Ù ã¢£#c´4oˆÔ©‘ ³Zî^¼—w{ßMýX.¢©&'t\|„©Ð©™VõŒüysûöÍV!õÅv¯úcz„ˆìfƒŽ‹0õSGÝ“‡Lj™µAGÕWÂ$˜.K59Ùh :ZÞéÿã*åŽçé¦NãHÄ•ð4)º:„*¼ÁÞïž5‹×?/ˆT““…Æ©1@»³ƒ<0Ø^%‡&P$:0ËKÏB¨ÃPÕ3;_¸ê‘æQGŸ®rY.rêðf Aê ¨ÃG˜¨“ÔIê¤uR¨¬Îd…0z‹'+|„¨ Éܼùÿ Ö§îeIEND®B`‚PyMeasure-0.9.0/docs/quick_start.rst0000664000175000017500000000366014010032244017674 0ustar colincolin00000000000000########### Quick start ########### This section provides instructions for getting up and running quickly with PyMeasure. Setting up Python ================= The easiest way to install the necessary Python environment for PyMeasure is through the `Anaconda distribution`_, which includes 720 scientific packages. The advantage of using this approach over just relying on the :code:`pip` installer is that it Anaconda correctly installs the required Qt libraries. Download and install the appropriate Python version of `Anaconda`_ for your operating system. .. _Anaconda distribution: https://www.continuum.io/why-anaconda .. _Anaconda: https://www.continuum.io/downloads Installing PyMeasure ==================== Install with conda ------------------ If you have the `Anaconda distribution`_ you can use the conda package mangager to easily install PyMeasure and all required dependencies. Open a terminal and type the following commands (on Windows look for the `Anaconda Prompt` in the Start Menu): .. code-block:: bash conda config --add channels conda-forge conda install pymeasure This will install PyMeasure and all the required dependencies. Install with ``pip`` -------------------- PyMeasure can also be installed with :code:`pip`. .. code-block:: bash pip install pymeasure Depending on your operating system, using this method may require additional work to install the required dependencies, which include the Qt libaries. Checking the version -------------------- Now that you have Python and PyMeasure installed, open up a "Jupyter Notebook" to test which version you have installed. Execute the following code into a notebook cell. .. code-block:: python import pymeasure pymeasure.__version__ You should see the version of PyMeasure printed out. At this point you have PyMeasure installed, and you are ready to start using it! Are you ready to :doc:`connect to an instrument <./tutorial/connecting>`? PyMeasure-0.9.0/docs/about/0000775000175000017500000000000014010046235015723 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/about/authors.rst0000664000175000017500000000160614010032244020140 0ustar colincolin00000000000000Authors ======= PyMeasure was started in 2013 by Colin Jermain and Graham Rowlands at Cornell University, when it became apparent that both were working on similar Python packages for scientific measurements. PyMeasure combined these efforts and continues to gain valuable contributions from other scientists who are interested in advancing measurement software. The following developers have contributed to the PyMeasure package: | Colin Jermain | Graham Rowlands | Minh-Hai Nguyen | Guen Prawiro-Atmodjo | Tim van Boxtel | Davide Spirito | Marcos Guimaraes | Ghislain Antony Vaillant | Ben Feinstein | Neal Reynolds | Christoph Buchner | Julian Dlugosch | Vikram Sekar | Casper Schippers | Sumatran Tiger | Dennis Feng | Stefano Pirotta | Moritz Jung | Manuel Zahn | Dominik Kriegner | Jonathan Larochelle | Dominic Caron | Mathieu Plante | Michele Sardo | Steven Siegl | Benjamin Klebel-Knobloch PyMeasure-0.9.0/docs/about/license.rst0000664000175000017500000000207614010037617020110 0ustar colincolin00000000000000License ======= Copyright (c) 2013-2021 PyMeasure Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PyMeasure-0.9.0/docs/images/0000775000175000017500000000000014010046235016056 5ustar colincolin00000000000000PyMeasure-0.9.0/docs/images/PyMeasure preview.svg0000664000175000017500000003036113640137324022166 0ustar colincolin00000000000000 image/svg+xml Measure Py Scientific measurement library for instruments, experiments, and live-plotting PyMeasure-0.9.0/docs/images/PyMeasure.png0000644000175000017500000002066313640137324020513 0ustar colincolin00000000000000‰PNG  IHDRQP ]FSsBIT|dˆ pHYs[[æÍÎËtEXtSoftwarewww.inkscape.org›î< IDATxœíy˜U¹¸ßSÝÓ³÷Ìd²™L˜¬ÈæE B!,áJ€H¼ÀA@QAYP¼Q$n ²„Å{A%à Â5‚ JØAY‚‰$L–I&™d2ûÚ]ç÷Ç×®®®îéžîž%œ÷yòdj?§«ê«ï|ÛƒÁ`0 ƒÁ`0 ƒÁ`0 ƒÁ`0 ùE ¶ÃæUÇÙ>u¹ÒÌö†6 lÓJ¿¦ÃÖmS _9Òí1 £“”BtSø¥o€¾n°ýö|ôµu¾O|w¤[a0FI…ãæUÇiKý9Õ>*4 êüsŸéf †Ñ…•lƒö©+14†Å•#݃Á0úH*DÑ|lÛ1úÑ<ÒM0 £äBJ;ø½·Þ§³£;‡ÍÕ”t Ãè#•M‰Öš+Îû7çî\¶Ç`0ÆC¢Þ÷$ ë6³âgbúÆ\¶É`0Æ C¢m\»x9Ý]½ôõöóµ/_—ëv Ø`HBôæïÜM__?áp˜·ßXóO¾˜Ó† ÃX iÓ¦ð‹íx8SÖ¯ÝȉsΧ»³'nýä)YõþñøsßÊ hnÙÉ3o¼À o¿ÂööfBá0¶£,‹B_€q%•,:jÇÎ9Keô Õùæä«Ýƒal’±=sþ¥¼ô×7 ‡ÃqëKËKX|Í\xågsßÊAÛ6w?¼‚gßz]ö.Zü»°ÊÊ«w6øÛLèÏ’Ó/â“–îeŒ5 d$DW=÷:gÍ¿ Û¶=).-╆‡©Ìm+añí×òJëk„ƒ¡ŒŽ+ÙYÆ©]Àe§]ÎîFˆ¦ÇÇ÷ð6°-GçF®á¤x>Gç72"£±÷¿Þ^Ç)g@Ow+yžÒ²Ž?ù|>ÿ|í=Ž>!mí.kyñq¬73 ÝÕ<ôΣT–”sî‚3òкµË+BÛÏi•f˜¦èQŠ] [5جµª±£ý–áý%yn'QÈÝ,ÎÑù?9Ÿ“íÀ¤ß`ȈŒ‡óQš›™3u!õ3§ð·5¿ÏKãÒᢻϣüvVoÈP¦hm·ñU**Zª¸ó²˜V35ÕYi¢u7Wj£_ʱm[¨ ~–† ~íëSßÛø?m»†Ú¦<ñ2ñBT#Zè ìyDf¼Ìq­3BÔ0b 9Nt4°³uþñ-TW$±¦ o}˜ö'ûèzi€¶ ­üà×·å§‘9@¡, ¹_ Æk¸"\¤×ÕÜTñß#ݾAPH Åcsp®Ù$ PƒaDÓBtåOSVÆï‡Š2Ç vŽÛ·ûõÚíݽÞ?Á¢`²E`ªå‡ Ù²½i[Ÿ š ¥ôµ7s5TÎ'gçàÃïµ4aL Ñ—6üe·=b|0¦ŠjÀ^m¡ûcû†šmÂ-¢¾ ¢ü„BÓ|´•µrßÊ?SË3C+4ÚÓj!šÞÍ5˃g s³2AŸг<¢†Qǘ¢Zk:Ã;cËÎa8°~_ª·ŽG‡dKùüB*?S„oœw—­bÅ[¬Îc‹“ò¦O©ÎVHÍ´mu¨Vj>èó•Ö×kÅ ZlŠ^vE[Áíµ?)¯îƧ‰Bìë§fq޹À¬Ü4Ç`È#Ÿë75`÷°q›f]£CŒÚ,-çôÃOáæ?ßAÇÞí(¨²Ô†Ó–žVú c$“¦oã’¶õéìZ{Û¸):ú‘JÔÈ,­©R}êràÛ¹odÎ8x ‹c †QǘÕD}ñi¬’~64iÖ5Ætµ­).(b¯q¹ô¸/R¹± ÖIΣSu²iëæ|59k/kÙ¼eIûÙ(~…Òq©R(­8w¤Ú–&ÿ LÂqÀ™¸Ãh`Ì ÑW×þƒ°~‹‡©0Åâiª_ËUŸº˜-3)l*´'ÜËš†´”ÂÅOèhW<‚F)Ø»þÇ¥£m2Á>Çß~`ÑÎqPM,$¯/žÙRTåñüQ|‘ëåñ¥‘køòx‘¤¨Ìò¥@Ù {¥`Lç;º:Ù9°“ª"…§rÒ“‚)-+¡¯·ê`WüUÖmÝÀ_}”íÛéß*RÖš|TôVðñ}ã#53‡½?™²aq÷ÖšåÁ7•Í!îHßpÀW7ùÆŠZ˧¯N8Pë]K:.Ìôz“n¨˜æ÷ë¹×+­Þؼ¤í‡ƒ¾8ű|6pg†MpåÝç̆rD°ŸDlFÛÐ< <Ô8–Ÿ^ä˜:–WÑ!â àr`!ò›‚|T ü§œŠ:dTs:°±ìº^`=Ò¿ßÿä<»“BôéWÿFpF5ãë·$n„‹¨*«¤¸¤)õ“yÿ½Âá03&ïÍ×^LKÇ.zõqÞoø€Ê¢J.ÚYÆ kÛyõŒcéëêó;¼(Í:­8H¹´Œ°­÷òYíOÛÿSiÊQN¯¾òÕݼ{ÓíýXœ¡5§í>—BaÙèçÒ8üOÀ§!9À‘Èÿ!ÍË—"BN#šhjÙ Q øð#`œÇv?P |1òïà “re•ðuRÇ·–#‚ãD`)ð%ॠ¯ó à¼5[ ˜Œ3‘¬¯”Ùdœ,q,ÿ†Ì…èW§`”+\ˆž†Ü›(¯#BôëÀ÷€€kÿBà\ Ž^”×F®ï¥™ûEþ]Øî¯&&¼“2&‡ó¿öõÓ~(.LÜ^* ¢,ˆeY”–S?s Ê1úW^ÅÇžÍõÿý-®þô%TÚ²˜¹â)zº{ …r‘X“oT—òÒŒ”*Û¼„¥¹…ÆçøÒvævS[é3½û< E8¶ÿÆáíˆö³»…ˆÖ—. A½+Ö Ž÷¢ шî"õÐÝ©çÑäÒÍ> ©ð{2K8xødšû€‡Ó<£8Ÿg_ªïR¦éJ‘݈}Œ½x6ÉúÿVW’¾Ìû,òáÜw°Çœ]׸²Ù[±"D…‡ÇÝò,)CYÒ½Šªr¦ÏžŠeyw×.ð³îÌX{Þz»{óÓø\¢tµö¼ªÀ‡}ÚõÅUø5œ³ÿ²„/yRjn­šªàc8®¥5!­yfÛ×:·§qŠpŸk]&žv÷¾+HÔD2a 2tžYvÚ„BÀVdX×é8&ºÝÜöd!šÞ³ˆVã>G”V oûn¢ñ¥ã„»89ò·óeEοØájGt¿Ã¿Z0®A´PH=ñsëŽA>~S<Žï6›"ãÚg²ëXOÆœ½såí”WÅ4Å`IâoZâ+ÁR~L†TT•3{ÿi+¼mÈ[>:;âðîëCz­Q÷Ï 7l\Òù.ð¢NŒ+­Ü,?1Ý˨Pø3îÇV)üJëtC•üÀÿC4Ò¨ 9Ñcbß#rl¢u ÕQ@Ì û¹Ö7— /M bw "äýŽë«Èµ†· €ÈöӈݛhŸ{›çÄ‘QLC† '’hƒ«D†Ÿ©8ú;ôvÄ.Z LôeBäÿkg­†íü!®åwSÃx¤Ÿ§?El¶N¦#ÏKÔîý^ELDUˆii*ò{ŸÙæÜwê 9>“8ßUÀ:äc}fæ¸w•šèº°ìÞëøüçòd˽Ô¹‹šÉŠ M:¥ÕÝš&G†ò…|¾ÌºgeZ”4(æ„ u‹ç?­×Y¨çên '+ùè¹?|a”ê÷a_ˆJ|Hš®ê\ÛÁ¤•OÁ¡5·”Yµ?)¯F©£TC̓ w(8½ôÑä<‚Ô€Äún&Ì'ÞvØŽ Ñ3å9×ò¡Y´)ë\Ë©ŠM¿O¼‰á*ľ»§ñ6ðXûʵü™ié ¿ç_‰·ºíéÀj¢­íìlm¡¥­•Õ kxëƒ÷ØÑÝŒ¯zu]M·w§t4·ÂÖƒDÊ[*9qÔþ-.Ég6݈葖azŸFŸñÆ{£¸C鸀g@‡•VŸ¾åuˆîg¡Û)¥À§ô ˆ€¼ ÿD<ó ñˆ.@‚çL||ÞÐNÅ<Çß y!ÓÕjœ¼åZΕÀªG*TÍBê 8I5aÙ}ĆB¼ð/"¦‹Ÿ#vò=ÈL‹œG¼†ž‰Ô‰û~{VË»íîíáù7^â‰7ž¤§d==azºlzBaºúlºì^ª'AíT˜\iážÅ¸£[³ºaðßO÷iêËë(+. <˜Ê©9ÖÑZrçõj­}çm¹²õÍT{´µÿ~ ¼pØ)”…Ö糌o{ÍפP§i©øg¤uRYûŸ³,ÑrñáMg“øçÒ¡2 s¾TCÔn«k¹ ›ÊD "¡#{ç¡$—Ò¤Ž‰ý=p)ñ”*ÄÁ´1׬`覅±H‰f\ÝoO³[Þ„h(æžÇà‘WVâß»™™(J\ºTwŸæ½Ùšm»m]6…EaÀïS¼ûM’ÉEã(k rÎɱŒ¯²`in;4ÂhÐ(l¥ñiTŠ›·´wü”eƒk Ëè­]Î=À¥O»U[[^yt#­q™ãn$šï ZVèß¾~aÖ1… ™'–t‹“áõöûˆUlŠ¿³¢{?,» ïÉ/{O%ƒ{Þ-$@ÿ³ˆæèôêg“ßÄ;>MÌvmã>ˆ7ûF$œçîÈ~£u‚Ã\áU€çe†ö;»*ž±y¢Ûv6sÉ-Ki©ldŸ#5¥ñÏ^Ø–êKÍéZçîé<2﫯¥€ONŸK°Dé…JJ³-¢>zÐСЯixņǶ.îø»—ý3á°}—ϲ®p7¤,û\\ér%¡ÐÉÚõ)O‘v€}*‘,™ã‘¾ 3úEdû'‰/Xñ"ã8T$Véɪj‹d6]þ„8%µ?gÉVD«½ ùÝ ñ¶§#áOw µ%Oíi¼ “ç"ð[“ä^ç\ˆþóßïrõÏ¿OÉþ»8¸ÎÂï‹ ­°f£Mw_nJCꘞÆÂCc¦¤ªêaõ®g‡æU_¿rÛÁ°Š”nXÜš˜'¾ÄcçAhºªsuíÍÁ4Õ0#ÿŸQsgÍÅ[.ܲ»ª>M¡ÂDm¢´¢©±½óo™_Ù“ÄÇ?~–˜uO¼—­šÏÌ hN²m/$¨ý×z§YÁFB ^BlÅÿFL ³aâ¤[€ìhè˜óZ Ù9?Bl¦W“y5­±@¾î·"Ɉ#§BtKsÿó‹ë(=p;4[Zò†Ö£¨Ù1™¯.<÷*¥& G>š×´ÇJóSGì^!ß±«»saUsgM‰êì8å kÒ¶RêþÎuÿGÄ^W„<œóís;" ¢/ÿð»,¯åô»)?—-Û‰Ïz‰2 ÖöÈ*c’%õ02pgMÈ¢=Gþí‡hÀŸ#~x¨AÄnzðUö$ÞoMbìíPyÆke΄h_WÜv ÁƒvRR¬hÜ‘ß"ä¾v5µ,Yx…˜–]9.H p´×S*:Ú× ÞŠfÜîÈ7tH£Î&ªñuvR®ÐeiÛÊ&ÄÈM"D¢ÿQ;è;H®r”§ˆOÅ ]ˆ söé*ò—îXˆØ Ýt+b‡ý9 9D,]V#ñ¸KO#Ù6óÛ£ÂôKˆ-úkynÏpâv¢)àh²KNIÎâDpï­èY›™VcÑÕ›GÖn)áˆâ¹\½èÒ8jYµSG[a÷ÑûËèWZýË9µˆòkÍüh¨Rœ¤š‰Ø^×0ÜBù³$ås%¸Ý/ÖGrt^/ÎFÊæ9y)òò/@„­ÿ$'|Ud½Óq%ñÞý±ŽW$B¾ìÑ@Ž„èÖæí¬éy™õòëJ§fv†è(i*gÚŽ\yäW8óÈEX®x¨šº‰F æ§èøNßVúT4JkÆyå5dšŒ•ˆ–}¡!>µ“Üi‹îÀÑ9:¯—¹–_G>#fô2â€ú&±¨‡èxä+IŽq›o²)A8\¬CF:NMn^>/˜!úÇ®gïöMøÉ•&jwk vRÑPžmáÂ9çñµ…3eBM¾Õª˜8y¼ÇY N6}½m°*¾Dž+­ÎšrkÕJ©8U^ i;[»¤H¸“Óñá îý#éMa‘qü­Í,ˆrKüýo»éH “ÂïˆÿÝ“}TÜ…YÆBÜ`É,söïÓù¼`Ö6QÛ¶é*nض¡7Ðc vÆê´„ ñ…|¨ |J}%ÔW×ñ‘™³™U;_’‚ÊÁŠ2ꦹk1’¡á' ‡ƒ åŽÓvèbP6‘«[)ýN$u4¬@‚Å#—‹{ð³õÊ;Yéø;Zé ÀÍ9¼Äæüq’M¦U¾¸Ÿø ý5È=wkžnç[¦UðKðŽÛÌ7&~ú˜£ù³Òž7)²¢]½=ø*äù×Þk°wÇ~z¡ d{)•v³&Ïàc;€ ÕT”ñY™×Ú­¨ 2mÖ”¤Uë ‰ÚÛÿ4 ¶âÔÆ4 ¥.@Å"P(¥‡T±)]^F<峈 [‰×³å]äÅšYÖˆÓå>”Ï/M-Óä„ìDf™öÁ­k¼´Ýyü3‘>¦ã¤)@4^ÏÊGyæ^¤ÀL±çê&$¬.盬%Owo7¥¥"@ÿµÑ¦¹5yýÍfµÍbéñ‹¹æ´+9óˆSÙgÊLÆ•We,@-Ëbê´fì“|Úƒ7‘š£÷ĕȓIè|îÒw!¥~C~YAb&Ðä¾LÛ÷+$(û^2Ÿ²¸qÒxá3šîô±EHØÑ2Ò{ÑÒþ7I]RЋyŽ¿52i ×5ßq-º¾©s¿{Jñ#A;2Ÿó¹:™f$SjI’3%kéccH}S²"! 7³¨þ$–œtÕÁ¡OëmY“j&pÀÁû0~lŃŽ*leß…Ž»ÿV\”Æ^iZÜÚ禬 ñÎåP>Êß\s'ŸB<çéÆfÎAEàõ´‰uöç¢4Î;7rÞètÖé”û&Rm½‰_\Fz„ÙÀÅŽ6*â't²>òÏÙŸo’zN¦‰Hzi&óhåƒåÄ—­ÓÀw4]ïóç¢9ËS픽 §lÖnÔ)«,¶qƧrÌGù2eå%ÔÕOfÿƒfQ;uRÜüI†ÌÙº¸ó=Ðσ#ÜÉ©…*, ¿†¦¬C´ÄŸEþÝHî‚£Ý|‰ÄzÇ q•—à-L}ˆ{IAÝ/²ßû†ë‚g"i–^¶Áøѿ“ºn¨›càlãµÈ ÿâcm£XHžý_XÔCF4`/4‰Î¿9H¬ƒ\û– ûÿ sÍ8´#!sÄ×aXBlŽ%¯NtŠîUÈó_…hÔGxì äÀ&Öš-;’'²¨V‹9ãfî>©K/ø ðøñû}ø üPTRDqIQƵA ƒ£Q?UÉ<³Š°í³Ýš[¾®i*Úíí)â…Z5p;¢q¼ƒä•ÛÈ 4ïi@®B²®Ü9èßG @{q/@2ˆÞEì—¥ˆ*1ÌDñ`ïåKˆðNRr3#ëÃÈÇ¢±_ú¡çŽHPÈÜRî ޜ܂h®Nûâ|ÄŒ°Øéë¾$jx!äƒèŽ›.^E>*÷«' Añ "h× Ï…©¦åÕ)©çy]$ûŒ¥T°fBûDÎ:aQÂ&ŸÏGUuªê JËKŒ]sðt0!Å—•湦Ë:“å…eÞBlšÑôH§ò«uÅkˆÕ‰Î{ÙmßG4²»\çö¤mG¦µø{d9ÙÛÕƒLž÷D‹¬ð¸Ö ’Ûc£û>î¹ØhéÑQ‰³M{‘8EI”mÈÇä0FNˆ‚8›¡YNüïTNêéW¢l@úïÁ‹á|òMÅMe|é˜s‚â«'T±ÿA³˜:½–òв·U#Wš¬a½(ý[ít0i)¾¬•¾?Å¡c ÈËs-‰Dî'ڹ܅Ä}Ö#Ŭ“MÖ÷sd²³h^ü`Ž¢mÈ ¬G!ZfºÏÄoay#ñ5MÝábnºMútÒË›_œCbÈS²küøR ±€Îð-è΀ój¿ó^mBìÔ³9ž÷%oõDu¯fzi=µãc*˲¨Ÿ9…Êq©Šuï™tù}k‹Âáùîõ>=²™Öì§´LÜD«¯5Ôjà¿"¾´žÛÛ V_éh°Ù:½èFÂ`îFlg'!BÌ=”ëGìˆO /aºÚù½ˆíðR$¥Õ­ö#ó·À/‰%„qÔ^7Øõv"¹ï7 Aå‹O´»l[Þ> á3-…÷"Œ#µQ÷wmol¿‹xGÕËÄß«tâfŸ ¾ß/gØV/6#Ž¢‘ÇI$ÚuA„þSȽûiÜNúµÚ~±äSÂÒÔØÌœ© ñ·˜´4ÑQY¶©‚o´„`qlÛôÙSDz ÕùæîQ9¥õ?.Ýk À·Ù9%r$ì鱯ÅíyÍò¥(dN£Iˆ0ÛFúBs0Æ!Cßh\g#ù-\ô£éC²ŠSC¥éOIäÜÑ™\Çä7šˆhç ¡ºW~4Ñ.ÅÁ“Œ “§LËt$äó]äŽ EãÓè¡Lâ¶' ûYSÎÝÂðBÞI~óõ[CõlèG†ì›²9I^Œ‘Á•,úø‚ÝË%¥ÅìU›M™ÄÜñ§VÒ¶+¥$Ç6ÓXU¡—àx4ض´wd2³¢Áð¡&çBÔßà¤çSX3ÉÔM«AÄtÄüòÿÀaõŸæWw‹ÂŸ¿ä4®¸æŠKñ ¨¥A èkëüsŸéV †ÑÇ UA6¬:Îö©Ë•Ö‡‚ÊtzÙ±LJ¿®ÃÖmS _9Ò1 ƒÁ`0 ƒÁ`0 ƒÁ`0 ƒÁ`0>œü†n†q_VƒBIEND®B`‚PyMeasure-0.9.0/docs/images/PyMeasure preview.png0000664000175000017500000014773713640137324022173 0ustar colincolin00000000000000‰PNG  IHDR€ÃóÕ¡sBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î< IDATxœìÝyœ\U™ðñß­ªÞ’tgOÈF „}UvaÁwDÄ™wÇÑqwGÜDGd\ÁQAv"ÈÖ°…}#$éÎÒKUÝ÷“@–î¤ëö­®¥ßÏ#]uÎ}ÒTÕ­ûÜçœ$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’¤ªU:U‡Åñ_÷Ï GÇûA<&‚l¥c’*!†âxe&›yl#­÷Ú]é˜$I’$I€CØ¢xÞ~¹büÞˆèí1ìWéx¤*´™ˆ?PŒ~8-w •F’$I’¤$LAKã»ÆPŒ>|h®tâs+‡Tçšâbd $I’$©j™¬cÅbñ3øßX*»Þ±2¾m|¥ã$I’$©7&‡êÔÚøŽÖ^_é8¤!¢¡§ØøÖJ!I’$IRoLÖ©ÎBîe@s¥ã†Š(â••ŽA’$I’¤Þ˜¬WQüÒJ‡ )qñøJ‡ I’$IRoLÖ¯*€4´D“ÖÄ7Ûq[’$I’TuLÖ«ˆ1•Ajò4ø¾“$I’$U€õªHK¥C†žÌ°JG I’$IÒ®L*uK­ »«§ÒaHƒ®›(ªt ’$I’$íÊ R÷/¸”~ûêJ‡!I’$I’$L*e7Ýp'7ÿß<¾ý•+X³ò¹J‡#I’$I’4ä™Tjò=y¾ô±o°©c ßøÜåŽH’$I’$I&•š+¾s- ŸXò¿_ûãxèÞŒH’$I’$I&•ŠçÖ¬ç›_þï~V,ùüGæÇq…¢’$I’$I’ @¥âÒÏŸö ›vûù}óá×?ÿS"’$I’$I˜T ÿ?ÿïßôùøW>ù¶lÞ:ˆI’$I’$i;€°‹/šK¡PìóñUË×òŸ—\5ˆI’$I’$i;€ß^s#¹õ½>ï{—þ”eÏ®„ˆ$I’$I’´#€J¬sk_ýôôÿ¹Ÿún™#’$I’$IÒ®L*±R«ú~{íMýª”$I’$IRzL*‘¤ûúím¿@I’$I’$¥Ë IÚÙ÷ÑùOqõ¿-CD’$I’$Iê @•ì¾yðëŸÿ)ñøK>w9í6¥‘$I’$I’ú’«tª-Åb‘‹/šKljçxnÍz¾õ•+øÜ%L12U“ž|kÖ=Çêukiߺ™®în:»:éî馩±‰áÍÃÖÒ¨#™:qZ‡¨tÈ’$I’$Õ-€*ɵ?¾ù÷<6àyþû߯áÿp3gï›BTª”ž|O-^ÄãÏ>ÍÏ.äñÅO³dårÖ­žb Iâq£Æ0cʾ°ï N8âXŽ?üh†· +cä’$I’$ Q¥Py,ÍÏ»™ˆÓÒœsSÇN=ø\Ö¬|.•ùÎ8ûd~ôÛKS™Kƒ£'ßÃÃO?Á½Îç¯Ìç'¥³«s·çEiŽˆš"¢\™˜(…MŠçBLœ‡âÖ˜bg ;ä sÙGtsÜËyÃ+ά™ ÁB†Cö‹N\Pé8$I’$IÚ‘ À:UŽàW?õÝD÷ä'×Ïá¯>1Õ9•ž8Žyà‰Gù˃÷rïcñÐS vJøEÙˆL[Df8dFdȈ†ED%~²ÄEˆ·Æ;bòë‹6‰{ÂcÍÍͼîe¯ä¼WŸÃûÍLño—>€’$I’¤jd°N¥\¼p9§~Ý]=iM À¬ƒ¦ó§ùW‘kp5z5Y¾z%¿¼ùÿøímbùê•/>ȌȌŒÈŽÌmÊö)RèˆéYY ¿&†bLEœuò+øØ»ÞÇ„±ãÊsÐ2(I’$IªFf]Ô/_øè·ROþ<ýøb®øî/xÏGÞžúÜ*Ýóí¹üÚŸpÍCO>@¶-"76KfTDvDù~»Ê¶Fd[sÄûC~e‘žån¸ãÏÜ|ï<Þ÷ÖwqÁkßLC®ap‚‘$I’$©†YX§Ò¬¼ãÏ÷rÞ+Ë×±wDÛpn[p5ã÷[¶chïþ|Ï|æß¿Æ¦-›‰² S2ä&fÈ´TÉÇD>¦kIüò˜8Ž9hÆL.ûèÅLŸ4µÒ‘½À @I’$IR5ÊT:U·|¾À.úfY±©}3s¾ðƒ²C{ö½k‡¿ñ96mÝLÃä,ÃŽk q¿lõ$ÿrMûçh96G¶-âñE yÛ'ÞËm÷ÿ¥Ò‘I’$I’TÕLj®ºü—<þð²çg?ü =øTÙ£Ýýè×WóŸ_h>¸¦YY¢*^Y›ÑrdÓ²lÞº…~ý³\wãõ•K’$I’¤ªå€êÓÆõƒV™W(ùì‡.ãº[þ“¨Ô²JìÖûç1÷ªË‰2͇‡ÊºšAãŒ,™a]O¸ø{—Q(yÛ«^WéÈ$Õ†ÑÀ×8Ç»Rˆ¥ÖL¾8À9n~B,’$Iê'€êÓ%Ÿ¿œõë6ÚñþzǃÜð¿7óš7Ÿ>hÇʶtnå’«¿ÁÈI[Ú2µ“üÛAnb ë±_úþ\†· ã5/;£ÒaÕ…©sÚ~ÃðJÇÇ¢ íQ‘|Å1Q'Ï³Žˆu­%*.–éxöéÑUé˜Uõ†ÿ8À9F14€ç1ðßÝLJ’$ *€êÕ<ÃO¿ÿ«A?î—?ñÎ8ûdš[šýØCÍù3†MÚÌð–‹;+Mr¹1¢ƒ":ôð¹ïþ“ÇMà胯tX5/†Ó€‘•Ž Š€8üÿ8ˆB«m? ÆGl-¶ÅSæ²’˜Eq- χx~Ocóƒkÿyí¦Á^uìõ„÷ÈàÝ)«ï¨t’$I* @õjÒÔ Üºàê~?Þ­÷ó±øj¯ÝôðÿÐÜÜØï¹\\~7uð£ß]ÃËÎÏB.bñ‚B¥C츈†™YºŸîá¢Ë.æ—ý±#GU:,UFDÌd`rÅ'‡ä`DcWWqòœ¶ÀMÄñMÍ=¹[ŸùÔú¡–¸Qºš7?ªpƒéॕB’$I¥3¨^µAÛ¨ý~þÓO,îó±iûM¢eXsa)%¿¾ù ÓCKk¨´Ö±¥+Þ˨êÖ89K±#fíêçù—oÿüì×M&kG™%Š>ÔÕX(L¾lä¸*—‰®}ö * jÒù ­ Õ’$I5Ê.ÀÒÇ1×þé7Lž}ágãFÕG¢¬yV–LKÄóïá—þ}¥ÃQuËFQüò8Š¿ßWN™ÛvÍÔ¹£N«tPª9§S*Ä :¯ÒH’$)€Ò³à™§xvÅR¦øbplUìôÖO1t/)°u~]Oæ)vîP¹˜hš ›/ûÉ÷X·a}…‚Ti&æ­q\¼yÊܶۧ}³íÌJ¤š‘έtƒä%ÀìJ!I’¤dLJCÌ-÷ÞŸiY[^¬ú9<¢1WýU€qÚÿÐŦ›»Ùú`žÍózØøëNòkŠ/<'ÛÑ0)ËÆMüÇÕ?ª\°ªM1§‹üß”9mwM¹lÔ‘•G5áüJ0H†ÊßS’$©.™”†˜›ï½“)³w~ëGQmTv>ÔC~uq§ŸÅ°éöî;Âûe‰r¿¸éz-_:ÈQªNœHT¼wòÜÖ¯O¾|ò°J£ªv pp¥ƒ(³,C§ÒQ’$©.™”†µÏ¯ãÉ% ÙgæîýƬþ Àž•Å^^ÜSh15@ÃÔ …BïÿâÊÁ Oõ'ÅÑ'£Í›œ2wÔQ•FU­Þ›cœLªt’$IJΠ4„Ì{è>ÆMÏÐÐKSæÑmÙ*ÿDˆûÿXã” QÜpçŸY±vuyS½›E\œ7ynëßW:U­ €ê¿‹’\½'8%I’ê^•_îKJÓ¼ïÝ©ùÇŽ²Õ:Hׯ1t=™gã¯;YÿÓ­´ßÐEÏòÂ^‡5õR¹Ð0%C¦e—سa/ÀB¡ÀU×_—FÔÚš£8úᔹms‰ë:Ñ£d¦'U:ˆ2iÞXé $I’40&¥!"Žcîyì>öÙ¿ï·}©Ë€óω»â½?q†… 1qòk‹tÜÔ½×$`ãŒ,ÃŽm Ú!‡Ù05ÃðSz/ l˜œ%Šà7·üîžž’ã”vó‘ÉsÚ¾Ué0T•êµIÆkØ%V’$I{bP"ž^²ˆìØdúNò‚ôWܳõáÔË€¶>œßëðæÃrŒz{ m¯ibÔ[›i=£‰LsïAG“aCG;7Þ}{ AJ}‹">8õ²‘ÿVé8TuÎö°QAͪ×Ħ$IÒbP"æ=t“fö¾üw»Æ\Dë®Ki÷$Ž(n‰é|´ ý«Œ»â>†ÅŽþÍå 7.CfØÞcmÜ'|ÌÝpÛýš[ê8Š?1unÛ+‡ªÊàÌJ‘²ÑÀY•B’$IgP"îš/­cöž07ª„ඦ¼…ö˜­ ù»¨9ê3q—íG|¥ÊŒÎå"îzè^6mÙ”úüºâ˜K&ÏyL¥ãPU©·foš*„$I’Π4t÷ôph±÷·ü¸vzŠ‹!ãE…uEºžÚ{#€aÇ5ìÖ/3ÊÁ°cúð~Š2ÑÝÓÃÍ÷ÎK}~ iMñ5c¾=¦­Ò¨jœC}í—çò_I’¤:Ñ{KMIuå'¦»ÐECsó^Ÿ;¼%¢¥ ¶võcâm¯;þ•ÜúÐ<6®ê j‚Æé{Yj<=KÛYMt>ž§¸)&;:¢å°2#ÊÓ\5;&CÏê"w=ðW^÷òW–åÚ£ùÀs™ †Qq#D£ K-‡§Ù@ÅÌl)俼¿Ò¡¨*´’€?©t )˜ ¼¬ÒAH’$)&¥!`ÞüûhÑ¿çvlž½÷âBga€}ÆLä“oû_þÙ\¶,î$jx÷$7>Èñƒ³_~vt†(‚»º—8މJét¢‹øìò Û¯OsÊ©sh)[§“ÉìÅÌ IÌÀQ vr0æ=û\:â;«>¶é±A=®ªÕùÔGðÀž?È%I’T3LJCÀ]ÝKs?ªë:¶Àü§ äû·’÷…=²9¦ŽŸÌÎy7s®ûÝ dš"²c«c—(™ÖˆuÖóô’E0}ÿJ‡¤Zv[¡ãqàñ~ìå4¬ÜÒz\G¯ ì_vÔ „“Ëf2—g±TýÎöVU:ª·ý %I’†´ê¸:—T6ëÛ7òø3OÑ2zÏo÷’ý¬þ ’Š ¹p/á°éñg½ˆØº @¡½]}C¦-Ä:ÿÉG+‰Êé¾÷Ò³âÂŽ;—_Ôþååµg⃈˜A{™}Ö”ËF½¢ÌÇPmÈo¯ttpt¥ƒ$IRzLJuîîG Ç Ûw`¢ä/6iʽØ$ò¤ƒ_¹§žŘÎGz(n©Ž$`¶-|Ü=øä‚ G¢Á´â#O,¿°ý£[r¹iÄ| ¢­e;XTø@ÙæV­©õæï¬t’$IJ— @©ÎÍ{ð^šû¨Lšü ’{;ïåwöKÏàUÇžFœ‡­ç)vV> ¸=øðÓï噪GÏèùöåmÿ|± ñ-å9Jôºés‡M*Ïܪ1/fW:ˆ8·ÒH’$)]&¥:7ïÁ{!‚–ÖÝ+”üƒ–·44íöÐù¯x§zqWLçƒyâ®Ê&£Fˆ`ÉŠ¥ŠÅŠÆ¢ÊYù± ‹—·wœGñ¿•aú†|œ{wæUmªÕ=ôNfU:I’$¥Ë TÇžY¾„kW“ÑܼócNþq~ÛàÆÝ€QñgÏñ³¦Ø³õÁ<Å­•MfZ"zòyV®­õ½ù5 S\qaǧ þjf¯õ¥ŸJÏù@-¶÷5,I’T‡LJuìŽûï 7:CSËסi$ÿâm…tM½Td¢ ÿôÚ¿å%I±3fë=ÖW®ú.j ¿ƒÅ+—W,Uåv|ø}ÊÓ4uNË””çTmš _é J”ÞRé $I’”>€R»ãm À±C£ÞÔ’ÂÍ }>%›ÉòÁsÞÍÙ/=ƒ8äéZ”aÿÀÁ”y!¸lЭ*7ä lJuÞ¸ÁnÀÚ®Öªé^ L¬t’$IJŸ @©Nuvurßcå"šÆfˆ¢”“„%À Ù{HBXüöÓÞÀ?žýNsMô,-²å¾<…uƒ[ ¸=¸d…€ žýÄæUÄÌMsÎ8ŠNOs>Õ´s†JQ‚ZÝ·P’$I{aPªSw?ú]=ÝdGAscúÉ?bˆ{`xËð~9åÐãùúßÿ ÇÌ:", ~4–¯+ nO.\º¸üSÍèɾ ¤õÎâ“Ó›K5n<ðªJÑOÀ7T:I’$•G®ÒH*;ïÿ+Ù1òù˜‡¦˜üâms(!0¶m4yã{˜¿ða®½ãz–®YÎÖGódFD4NË’ªÓwÅô¬,Ò³2¬Y~vÅÒt š¶æ#›WO™Óöà5)M9ƒ‹ÉqqšIEU"ð0pd‰ãήO?œÔŒ(qLá»dKúáÔ…ÑÀX`$0ŠdÝ l:瀕@O¥¬! À`ÂïÂëuÓ¶¶ë€ Ê-Åš3˜ 'üÞMжí„ßá’mÿ.U» 0˜4^×› Ÿ¥kEÔæky40hcçsÆf ‹pÎXAª7­5Ô˜”êÔ­÷ÿ¢Ðdk2#ÃKŠ éÌ¿½pkËÎ׋QEÅâž—÷5ópŽÜÿ0îúa~5ï÷,^½ŒÎy¢¦ˆ†ÉöÉ pá\aC‘žE ëŠÄ1DD=ë0Î<æTòù¹\v`P݈á†(½`ôa#§/eã”æSuÈ¿ ôàëy1QQÍ’,ÿý5ðÖ´©Q­„=O¦öc\X << Ü ÜA¸Ðª&''~³ úý¹=¸xrÛ?þHø½×GÇ^‹Óè_’>–O~gw·oû™TIÀ+€³ ͵ŽÞö³=YÜ ü ø!‰VMÚØýœÑŸ&rÛÏ Ùùœ±®VE{À3ëpzv¼ïyöqºèY\$;!¢qr–̈J 1=kB⯸9xó0N=üDÎ8úeŒ9€ Ï·3nÂè=ͤ!$[(ÞV̦·+Fœ‹ |9`òe­'E™èwiÍÅܶì¢öšZ®9uNÛ¬8âž4æŠc®¸¨ý¥iÌU¢__ ´-T†–Ö^U–ˆÒ183Á¸«©½F'iÚ‡ð÷?8…½_ö&Gè=“p1øB"æQà:Âëæé4‚­b÷§oûgÖæµíŸ·mûÙ2Â{÷»À˜»ZÎ^œ 4%œ'"$­§§ÿ¼íçÏ(W¥z*,'“Œ{€PÑ]­’þ½æóB{¾AÑVâ˜'(ífØ!Àû€w*âJ±/ð·ÛþYGxÿ‡P!X)“Øùœ‘¤ÔaÇsÆ«€ ïÉG7)¯"¼g¥^™”êÐwß@n|†lfO תmÃáùö”ÒþhkiÝùçQÄÈÑ­L›1™%Ïôï¦qE9㎜q«×¯åO÷߯­Ï£kUùUŰ’gË==t/-í7./pÃ=üaÅ$^uìi»%ÿ:6nÚë2e !S„øÙ´¦‹`Ì.?ˆã(þ¯´æ²Å(WS•WQ½)­¹2ùâÏÓš«DÍÀÿ$÷7ÀÄ”cIS’å¿¿†Ú> g÷–\½ò$ÿv'’.P»¿÷I„ ðû•@_ ,©¬$rD¨t½¸°ä®J¨h¼‡PUÎäß®f)Ï*OÄc«öímãðàËÀC„ Þ´s-„Dܸ”çíËk •™7þ>åHþí*"Tþ”PIþj÷œ¡20(Õ™¥«Vðä³ É´D´‹˜6þÅ·yÛð(µqWHޱsE~&óâ&O›˜¸ÊnXÓ0Îzéé\úžåÃox‡ì{ qWL×3y¶ÜÝC×y¶ÜÛCçÃyòÏ9¼•7œôj®ñþë‘ '­-0þÞŒ{ ÷Õ>q³©}K¢ØT·¥5QL¼Û—Üb6þ1a甽+µ¹Êlòec¦Ç¥3[ôðÒoz$¹JÖLH|u—8.œ›~8©˜N¸X(ÕÏ:Í?N!T;^OØW­R~BH ]Á8Jubç¥ÂIL?™wŸyßú§/ñÎ3ÞÂ.ø8ÿúÎqÒÁ/!—Ím? _ôºGµRhn"·¹“£¿rcç?¹Û|›:¬Ô¢(µDÌî Àð@æûi “‰/Hs¾²‰xK¢q»EóE*µü^¼ðN² ø8à€cIK’å¿×öª¶DDš^< ¼›êÜç°“WÕ¶¼ü•„Äßu¤Vù[V'’”‡W:>¼•Ðå3•=dË$"ì×öa¯E¯1Õ›Þ*ÿŽðY±·åÁi¹µLóžEhÄñweš €þ ”¾G“ê†ÎRyjñ3<¾h!-ã3ÌœÑûv#G¤Ô¸3Ì3¶uçàŽK€_üY†™M§¹%B…-ÃyÕ1§2cŸ}{}üùÃg²yêxz†7óÔßžM±!Ç ýönIÀ­[:ÝP/ŠãÎ'ëuiäò‹6ÜBº](ßqì僲§Lb¾9|bœôNøî'w¯þøÆÔ–j'°ýƒõ·$Ûl½Úª YÒc{f=î+”!,³ü?`Z…céɶjH^Bh®ñG ]ºbB2õø DZ«‹{í Vrd Ú€'$&T8UŸ]_Çþ›Á½™tKÊóeËûŠÖBìÉ„sF­îª2(Õ‘ën¼€#No$ÛÇeY*@bˆ»cF´ £¹qç½§3}8—Ë2sötç¿ê¤#Èmíbé«Oäæ«¾È¢7ÆqŸøãî]ðÂsâ8fËæs>ªmQ”ÚFêQÜG5ahòƒ´ŽŒ_¹¹õÕ)Ηº†b(Š¢D•wiÚþ÷Ø üo‚ñIªíÊ)IùϬ{„S^Ù¿óé@÷,nKmL7y·Ç2{X¼£aÃ[8øð™,|b ›7•§ïCÝûõ­€ &b.ÓÓš.ŠŠköôø²l¸uÊܶ'•&)ˆß|/¹Ò½9¥‰ºr]™ôª&“Û5áu%a¿¸RLþ†°×X¥4:‹–j×eϵžœD¨äÜ¿ s?ܵíϧ McÖ.|‡u³$§nÑï“l‰÷Þ¼øƒw ±š°gׄßßFÂï2OH’ üw<„°”-ÍŽÈß"TÇ<Ÿâœ{óUÒM,t–hß,–›€Í¼øZ³í˜~‡/!Ýÿ¾Ï–ÛuM/Iy¾ás!Kx-ïéBh °`÷ÇBò/µï‹;XÀÎçŒMì|ΘÌ$|ÖLMñØÿ‰Ëõ‡€R¸üÚ+9èäQ?ózmÃ!Š Nx/¶°y[à¸Ý+³}uéE®!LJìǪåkY½â9⤠@OwÅb±_•‹ª_“/kM†Ôö$ŽŸÚãã1sâÿ‚èÒ”ŽxÂÔoµ°ìÃí{>î ÷oãZ¡û•iÌÁÿ-ùôÆõiÌ5@»~P<<@éû®Oe€g»ïá°gÝÀ5»ü¬›l×Dhä23Å9.'T¸,.qì,BÅÙߥÓÛ û]¥}càaÊ›ø]@XÆü'BÅi)ïûˆÐ}ø<à= ¼sîàÓ„fƒa6ð¾æ‰ IŠªAÛK?8ø{Âþfù‚Ô ¼…ÁM¢ª~m~O¨X»XAHBxN#4DºØ±Ra ûÿµ>Û÷À»zŒpÎø°¤Ä±>ëþŽt*ØßI¨üa s©ŠyÅ+Õ¸'Ÿ]Èý‹îaü´þÏf`øúÆ[b²9&Žž°Ûc¹|d2&O›ÈÁGÌ¢mäàWŸÇqLwWÏ WU&ËI©NŸÜë“ùáâ( Qœ¯®;· ݯ!ìÿ4`Å8ª†å¿}¹2Á˜70ðäÄ@$Yþ{°.í@*èrÒë€x3¡ªóà›”žüƒPññeBõÞù@Éüo*VÒt;aO·4­%Ä:‹ð;¼ðz+5é*Ý.$$QœBlï'ݪÂ=ù4O®þŠPÅwᳩÔä„*Ëÿ%4Ä™Ix¯$ý¢ô!྄c¥í¶ö‘œAH(_E¨f-ìðœ"°ˆÐ•÷S„dà9„æH¾ü÷û„ i¸‘Ð1ýPàÛ”žüƒpŽø"ásóB…ï@ÍöIaU1€Rûk~Ì~GfK®Ã9"YáFÜçaÒØ‰d{©šKÚÜ£¹¥‰YïLJ̠µ-ëâu]+YØ>Ÿç:—ÓUè}™q&“¡PpUÊGg¤8ÛæÅÙ²joOÚÖ ä—©5ââê©ÈŠ"Òêþ»©Plþ]Js•ÃÏËK1‚paR ­ÀëŒKÒõ¸Z}øÛæy„°¡ýéÀM¤³ÇYøàpB—ÌžŸ¾G­À%)Ä´«OPzr®7 ®Ó€O’Îìv« •1o ,Njð±4Ú‹É$KÌo÷ð:à„×eZžþ‰£”òYwð_)Æ¢¡é7„$×E„e¼ý•ß6öu„ª½kÃÇrõaùî+ 7ŽÒP$œŸ.e`KíÛ7cTÇLJ5ìžGàæûï`Ú!¥¯æOº`as¸¾™ÖËþÙv÷Ñ6œ™Á‡îÏÈÑmškTÃ8Öw­âÉö{yhý­<½ñkjnbêô}8ì˜Ù Ñ2 ã¨¶MCK”,!Ò«õ3n3ý¦ÎñòçKlêZˆ9+¹bøÍê¯î³«rXEX®XªJu~”¼Ü}=¡Š¢FH¬ Dø<¡ûímލw]„*–3 ˜çíZ'îíÖŸÀøg ØÃ ®åÜŒ÷ׄ®º¥$võ÷”¿iÁ$oü±8Žò¾GÿP±ô‡~<>ðÏeŒGõ¯x7áfÙŠεŒPiœÄ‘„½9"|†°âœ«/„í Î&Yåïvï$,¡V2(Õ¨ž|_ùá¿3íà,¹ÆÒÆvl¥k’Ý *¶‡¼Æô‰½ïWÞPâྌhÆÌÙûrØÑ³™8y|¢ÊÂl¦ÉÃg‘!KO¡‹õݫȶ˜qÀ49r&K\±¨úÇmo%Åîg™8º¥¿Ï]ö‘ ·6·OGœ}Wjs @1j=“Ô~§ñÏÓ™§¬’,~a±Á–¤ÊèZÊ›¤,9à¿ MP’ZCøo÷%’/‹,ÅM„Š‘• ÇGÀ×Ò ç—SúÒÎ¥À{ƒˈRÝXŠù„* ¤ÇÆçå”t ‡vàÕ„Ý`x’°/à Ë2{³°L³¯Ç¥½yœpƒå¿+ÇösÆ@ºr¯",ÉÿƒsÎøᜱ:áør3T%LJ5ê{×^ÉÂ%‹Øïèþ'Ü EX¸¢È}èHø5¸°-8{jï{”´pWM LÙw"‡=›}÷ŸBsKi7á' ›Is.»ô»xjãýŒÓJÔߎ)ªo“‰£’»¸îYßÔÿçCœÞ©(~ËäË'§×Ì$q)uÿX?º½£?Õ&•–d£ýð¶2IJ' {Õ•ª^–ÿ~”íá´8žô–nõ×£„$OÒJÀ€—¥’wï§ËÍž',»>€°V%6Þ½g[ I¥±d¼/Ó•uI\DH– ¦ø!Aso/½‹t—skh¹8‰°/j¥}’ð:Oê)Â9£\•â}y˜¨ß˜püËHo\U€R z`ÁÃüà—ÿøiYZG÷/‘µncÌ=X²*N¼IQ\ ÀæÆfö°{÷ù(ŠÊVQ—Íf7a4‡y³Ú¶Q­{ÓÜÒÄôSxÕK^Gscȉ¬Ù¸ŠßÌûÅâ`¨šMmùî(ù…Wozºššî(e@\äǤT]C[fó¦rWªìÑ¡ÓHØ<~Àâ8þÅ£ÓÆ\e¶¸.Á¸Á^ü6Bⱋ€’^ÓUjpñÆ?NXJúlÁ$ð¡ª*éþNéÞèîaÏ{¼ ¿oQù*Òl¹>„}ËÕ¸ç Ç=He+¤'$j¾Ì‹Õœ_'4a’˜O¸Ù‘Æ£5øÜÆ?FØ#6Iƒ4<œKu3TLJ5f݆õ|tî) Ì8iï×qÝ=1 yha‘Î^FÇ›b(Æ8eÿ^€44¤B¾ÿÚF`ÖAÓ9ô¨{]Ü6r3gOç#`ÜÄ1L0ƒñ#C¿l&ËÊu‹ùÝ_®¦Ûüc(›:§eJÅ)ovß±öŸ×n*eÄŠu<'Kõª˜|)Y*6¶µŒJc®(ÎVs÷ß]%Y|ËšuÏ1bz–‰ûìù-¼êù˜»Yµ.ï‡ù á;óìi½ŸÓÙÿ¯¿šš™²ïD9ê@¦N߇‰“ÇsØÑ³™uð~Œ½s…à©GœÅ°¦á´ Åä±ÓY¹~¿¾ë§&‡¨©sh‰iø_RJT½ Ž~”dX†bjÍ@"xåô¹Ã&¥5_©Šé-ÿ]±¼cí©Ì58n¥ô;ýëüYŠ™„„c©êaùï‘$ÿ=w:«.N/œùaIp©"ÊSqú<¡YÉŽ~ÜU†ã ÔC¸ûú«Ó dI³ÿ—j3аÁåJ¢@¨P_Zé@¶9†P=—ÄVBò¬Zþ.ÿJ²m2 Þ÷ "€RèêéæÂK.fþ‘m‹˜q|}mc·µ xªÀ‚g‹äóéÅPܸ}ÿ¿Þ V›²¯zr¹\– “Æ1e߉46õ^…8jÄš‡Ñ˜k䜓Îçõ'žGÇÖv~s×O]<Äìw1Í1mW“¼ê¢/¢¨ýÚ$—]¸é6ÒÛÇ)ÛC¶2f¯!Eñ9©Ívqâ¥+•P$TË•j°¾`¿ƒ*Å=¤Ù¤¦r¾Jò?!$7ªEž°ï[oO3\AHö­%4ËxPR%ô ûÏ„ãNJ5Š%©~ŽúxoJªÔn¯t;ø¥Ÿ/·ûð×c¨Bås祈ªƒ @©líÜÊ?åÓÜþÀÝdZ"Zk`ÒøÝŸǰdUØëoCGº1ÄqØÿ¯!×ÀŒ}öíõ9}%ߪÅè£)lKöM3 Îx?ûO>˜MÕ|¢4íóíã{F¶ý‰Ð2U\µì¢„]#bâôšDq¦"Ý€§.õ2 —O§ÒE1µ´üw»$Ë€g3°Æý•$ÑXÕ/ÎN8öÏ„½ëªÍI¶©ü”çµÿ8œÐ§ÚÝLXnXªÃ€–”c™`L¥—JiyøB¥ƒØÁ‰„NïIüønб¤åz’ÝÈ:˜PÍ­:bPªr+Ö®æ‚Ï~¿<|?™ášj`„ˆ¦†oLmÜó×Ç‹,\Q¤X†aŠŠÄ…˜ƒ¦Í¢!ÛûR߯AÚ0©ÙS™Ìm¢(âˆ/¡mX’ïÞª5SæŽ|K6Ÿy„˜SÊ0}¡XŒö¥¯‰–¦ >|òe£ŽNg®R[LkùïÂe¶WÓôþZÀî]1û£Ü›Ç•8¦øyblJ8®ø’o ^nßL8î©Fñ¢' {%Ö‚"É–Ï6Pž‹á Æ<—zRe\UÕì+é9c ¡ú¹Òûrö¥ÚΪ€R»å¾yœû‰÷òø¢…dFF´™#ÓSƽ˜ü+à©eEx²Èæ­å;çä·í#x̬Ãû|N¥–÷×Ô ûÓj²oÈ™4§õeSç´ÝF_ L(ËA"®\ñ±Ž-á]þŽuqœ^3(SÜ*À˜(&Jë‹âωªöKôÞ$©<(O õ Iõßè½¹C-Ù‡°¯T—: W«ßl©r5²¨5w&—f×øí’,5LÚÐFª&7ªÓªÅd éÌ£rûãIV9ì9£Î˜”ªPû¦M|þ?.á_ý ëÛ7Ò0)KË D9Þ1jDø®¸ncÌÝX¶&Þ±°-}1äŸ/EÇÌ<¢Ï§Uûআ&Î:î­•Cƒ`â%#&LžÓöþ)sÚÈ݇…åÒ] “Êò•l6½f ÄœÇÅ Zgž)ßl=â)iÌ•‹µ¸üw»ŸªçJ±pFbð]/ÉÞoõ°ü÷$w¦V_O9–´t¥z к×gÕ¿Ž+G×îöc¦§…4¸º +Ä.þ‰Pé[ª¥„›FÕ,O²HÇÃRŽE4¸-;%íQ¡XäoºžoýÏØÐÞN¦!¢ñ€,¹q/æê§NˆèîŽyrYÌÚ ƒS S舉;cfNšÎèÖÞ+è¢(¢©¹º+U¿&_Ú:.“^Z„㢘W¾° ÊM®¾·ê Ϧ1×Òoº}ÊܶÇ)}¹fo&NÙværÚåîzT̼)ŽÒøLŠ^rѦ$]N«ÅBõÜkKw>aÿ ´”š˜m'T˜Õ² !˜Ä¥„å\Õî:àÃ%ŽÉ'S]d+!iR"èÆ”8f2! X-Ý©¥R}°u@µÈ–ð&q $Üzp]¼¿Ä1M„}oJ?U‚ @© ä y®¿íF¾ÝOY¼rD›˜¡iFŽh‡œZ. …bÌÝ bòƒØ¸6¿<âÁ/éó9Í-MD}µ%ÖÅÌštéÈcÏ›!ÓBL&C<Žˆ± Ñ´8b&0 â)qœ¼e[âØ`QWOãgSœ0fßæ”>8f·ß@Ì»¬å5QœÎþµÙücWWRzðÀûH?ñ”dùï/¨‹™=y)¥'>!,{N¯·¼î6Rz‰1¸ XŒ.q\ï]Ðf É*úÞ |%åX¤Á'Ñ÷œ²:‘P_ªÕÀRŽ¥\n6ÃKw&ë† @©‚Ö>¿Ž_Üt=×þé·¬YösÎŽŒhÜ?G¶5¢¥ Z‡EŒh‰hËÖÆ<½lp·ÅŠcȯ-‰2Pß9œ–anG£ÞÅðÍLf¯Û8bûžÊñöÿ‰^ø·J*Bñÿ=÷ÉçÒí¹Ýÿ„îè«”¼ÇS¯éÏ×ï7wÔ¨g/ܰ!…Èú4yîÈcâ8žQòÀÝs–q¾X'~C鉙Vàõ¤Ûx£™dûÕÃòß×'÷}ÂR-(Î,qÜaeˆ¥-£ô`*]Îwñ0ɶ©ø(p-ÕUE%õÇ5T_õê9 Ç}Ú¹a–îN/q\9ö>U…˜”ÙªçÖpã=·sã_nçþS,‰²0zF–Ñfiax ´ˆhØaKøekcÖmü„Gq]L܇͘ÍÈá}odPCNÄ7—]¸éÖ´§]þŽu“/k».ŠRé Ûœ'~e®hŠˆßTò ^ ¿¬þøÆjn¼Ð_„ ó(qÜù¤›<U☥@ê¯ë Hz1—¤‰K%ÝIé @/æ‚e@ßÍzWŽàC Ç&TxŸˆ]U[.­t½HrÓ(¦6Ï&‡0€R™ŠE.YÄóÿÊw߯cÏ>ÁðQ1­ã2rZŽÑS"ÚFGd2}/^ܸ9æéåÅAŒúEÝ+ÃZãÓŽ8yÏkniŒp¤jñûåÛ?Y®É³Ùâ÷‹ÅL @â8~å^ÒSz°—¼(®‹ê¿í®¤ôà™À8Ò»˜O²ü÷§@eN8é™I² –»I¾7\¥$if1‹PÚ™r,µfy‚1ÀÒ­øùÓÆÎnÞNhf#U£ŠoàÜ‹—&W‹çŒ…„ýcû¾°ëÝdLÖ€Rºº»Xµn-kŸ_ÇŠçV³æùçX³î9–¬\Î3+–°ríjâx÷sXCkÄôØ~P–Ö¶õ$}riLÇ–Êœ'ãè^]¤!ÛÀéGíyoêm¥6“’jÒÝ…\ñuË/Ú<8V‰›ì.ޏ€˜/•å‹w* À˜¸ž–ÿnw%¥'O$Üif€ÇN²ü·ªÿ ÙÅÜàÁ´yBÙY%ŽKÒí²V´–óÍ&T'oÿÿÂç)åI~ø0fsDÀ߯>GèLÚ=ðФº—ä¦ÑJjó&JaÿÓR;×ó9cH1¨!#_ȳ¡£íÙÐÑÁ†MílèØÈóíÙбñ…ÇV¬[ÃÚçŸc}ûÆ=Î5Ed‡eÈ´Dd†Ãˆ±¦LË2ebD.…wÖÒÕEV®«ÜM²žå(Äœzô {lþ0¢uØ E%UFììi|Ës¥ÜñwRn²ÿ¤¹­§¬¤ãöæzÁÄKF΀øè¦êÎÕâRš½y’°¯Üñ%Œ‰É»/ณ)}løÙŽY-šHVÙu'µ»÷á†np:¡rîèmEé¶¥*ÇõS0øR s¾KH(~ ø!&¥¾´lÏØÛ©ÎjÆþXƒ À!Ë êÒŠ5«xð©ÇxèÉÇxäéÇyjÉb6méÿн( ¾LSø3j„Ls†¨¢æˆì0 e`üȈIã"Æ´¬ÚoGë;`áŠÊSâ<ô¬(Íd8ë¥gìõùVªŽåãˆ//ߨþ•rîù×—8Š/ˆRiÅ™w¾°¦&›+¾™xàŸ}ü~ÙEíϧR5º’Ò€–$x.¥/﹨‡%ØGPúrXËŸkÕšcjñbná½t2p¡Òs sÕæRà-À‘)Í7°Àç ŸCW Rš[ªG’,'â9C5É êƲÕ+¸áŽ›ùýæ©ÅaåTÁ¾‡å8ù] ÄÅf:·@WgL×Ö˜®nèî†-]1Ýyè.Ä¢#2 ôÚ¡r»¦†ô›2.¢1å­ï¶vã‹ ô²ºxÐt/ÉçáäÃgüȱ{|n6›µˆjLÌßà/z2‚ –_Ø~O™êÓÊ‹:nŸ2'f Q¿uê>´ì¢ô6¯âèÍiÌSŒ£z¨<ëËÕ„½KIJ Ü—ð˜CyùïÌ„ã†ë7¾{ IDATÚÅ\[êQ¤oð ào€SÉÝz¾véÞÜ ¤ygu¾€æWí)CªU¥VOoç9C5©žO¢Šq‘?ßs'?úõÏ™ÿÄc/ü<Ó1jz–ÃOÈÑ6jû…~DÓ°ðg_òèê‰éî‰èꉷýÿЉ·;ÅBÌôÉƵE%uòí¯BYX gÐëŒ^wÆäWÄ464òæ“÷Þ@rxë0¢rü2¤²Ùëëµ3Š£oG ÿÂò÷®øÿìÝwœ\Uýÿñ×™í5½ì¦÷  -té© M!{Ÿ‚…*¢"¢é(EAQ: HïRH Ùdw³mfÎï3ûÍîdËÌ;s§¼ŸÇ<`gçœûÉì¹÷~î眳)õËpWyÐSMÔÔ îò /ê®\O8œle[oš©¬xúŸv!‡}< –d»q—œFòó5÷»ØV6ë²Ý¶@½—d›¤g:æ±óÂà‹ÀþØJ?7Õœ¹ìà$à^’¯âMÄαÇÕÀ}Ødà¿ÉÝ¡Œ"©r{̘ Lð2 rw¶3$IJJN G"<üÌãÜø§ÛYºrR‡Ð°Å#ÆN 2aT€@’y©PBA‡ŠR°IÛAk;¼·2ʆfxci”PÊŠ¡¬Ä¡´JB¶°¬Ä¡¬ÄöãÆ;+¢4{V›ãNûGLÔpÀN{2¨ªfÀ×kþ?É#¸+â¾õÉâÆeÙ’rn1v§”KmÇ,ÀV~¤Ì‰„0 –RöÇÀƒ ÙhM¯[I>x¶b'’d;7ÕZ\´ËFn/æRr‹²©tvØú±¸¯àÌ'÷_ÛÕÀûRœ{|€'ðäÇ4"Ép{̸ÌÓ(²_63$JJÎùß;opÉo®déŠV;  ²¦ âUNÊX½ÖðaC”H·©ÁÃah Óm…Þž7NC!›,)‚Ò(+Ž% ‹íó¥½ÜCYþ±aÍ:oÀ†×E ¯R[UÃA;î“P›êÚÊ4G%’vÀ}œ+V.Þð¢ßÁÄ[µx㺺+«ïÅV…¤j¿ñ?­¹ì-)_äcŽô ÿ‡cL>ÿíò`=0(‰6£°Ãÿ‘ä¶ŽOòõ?ÃÁýÅ\¡ñûbn4p6a=ÍçX²Ñ¯°_°¿iÞÖdl2ãRìwÕÀ#$óA$阑¿â%%gllnæªÛnàÞücŒMü  p˜0ÒaÌHǃËQ«±ÙðÞ CK[òI¹pÂaCKv]·¸a0¥%›„!ÇaåZŸŒ:>°çz ö>†Òâ¿ç‹Š‹(¯(Kwd"é²ÇÜè˜ð/W-n]íw0ý‰bnàx‘ u'`ç¤smÄÏ*‡ƒ³›ñ¬«mjz4«ß|o´w “lw"É%çS’ÜFðx’m²Ù¿È~ ­u€½€³°±ºéßõ@#p™~=VŶ{°<Ûñ‹Ž‰)´éòVºï(‰xâ?oüCÏ[À==ACéÔesŠ P[é°Ã ‡±%ÿÂxU”WÞºJþ%"…–Vú††µ†k¢¾.úб,J´Í°íäÙl?%±èjU¥9*9¬n5˜CGVl¬[½¨éÛÙžü»ˆ7=éÌqNNµ‹¢Pðp<™ŸÊÜûæ:Rï''Üê¢Í‘@2wY¾äb·“_•>š¨<1™œ¼7€Mf¿‰Mh…’‰ºØX™áíÖcWþ[ x$ú›I~Ò1#1šð=Oè‹\²š1†ÿ|¿¸ãwD£QІ(žÂ)²óìM n˜w r¬YoxU”ŽNoúËáuQ:"”—–qê>Ç&Ü®v°Ž™’õ6xÎqxÖ!ðÔˆòÆg^ZH'ز§\€ß;q{ª¶©»¢vÎê _uÛA4jŽôä{×ïô —\ñ,°”äæ8«ÁVÄÁ]0Ÿ†ÿBr SI¯v^¿ïãÁJæì¿ØêÞŸcçìËtòvÿØc%ðkà7ÀçŒA$tÌ‚¢ d­æMÍ\|íåüû…gp¥ÓB„FØ¢ÕÁÕÓÆ(õ¨¹k‘u o4Óï†ÁÀéûŸÈ ªÚ„ÚQY]‘æèDb€µÀjà]Þ"ê¼t"o­ØØü6Kø¿ñõ«| Ñ·—ãÅ<,Ž9p•{YÍ ˆcöJ=Vol|"å~r‡Á&Û~d»I,¸3ÉÏeô.÷ƒ,¦‹¹ì0›(šëw yb-p v~¾ë€­}ˆa ð#àbàZìTë|ˆCÄK:fHAQP²ÒçYxé…¼óÑRœR‡Ò!‚U¡L`ôPon~+?5|ô‰!-À䟶·;‰vžÛìÊSú 0hH ŽW¥—’׸Çy=Õ~Œ¡ƒqÓdŒÓékCk–oY˱y5„±W±Å@îÁV€¤Ä`Nd ³„p²m#¥æ` E©ÆÜÝ=9[ nÅVC%óåùE`0_h«úÏÒÒô‰IתÏUØ•ÏÁ“i<Õ ¼‹­Äý˜ˆŽœKžÂ&UÏ–5>ÄP|8¸;Wa‘<¢`bÒuÌ SP²ÎšÏ?ãŒ^ȇ«–¬u( 9 ä0eL€böÚÆfÃ{+ -­…—øëÒþ^˜ÈÃøõœ8ÿˆ¤ÚêÇ9§ä"ãðÈêEþêwù Šùm€Ôçðs`d}Mõ¾«Øø°‹æG§º}€€Ãí^ô“c–Ïó’hS ƒ­¦êK0öšdD ïþA4Qy¢Ö§¡Ïí€?ãÒÐw2Ú€7°Õ­ïoÇË ÇM‡¯‘{ @€0v:ˆßcT9éCƒbq|ø2ðbIE Ùw£"[¥ã˜!>Ð" ’UVú1 ¾÷u>\µœÐ e[…() 0kb€­&x“ü ‡»-òQÀÉ¿ÎUQŸF©­¨fÑ ).Jüš©¢²\«ÿŠøÀÓÅ@¢ÉWûå°JŒ³oÊÛvXºò¼/¦ÜOnr³ȉü~w`T’}þ ;l>ŸîA=yoxÜ߉ÀÓd>ù×_óàT`¶Bmà ì¼yÅVüå[Åñì´€3|Šc&öoÿ#ð¤:\$StÌHœ×Ç ñ‰*%k¬Û¸¯\z«>ý˜ÐP‡’é!ÊÊ 8dxg…áöu½¤®‹z¹p‚q¯mj5t&=è-¿t~¡ý£EÁ"Î;â« Ïû×eèˆÁiŠLDânÄáªTû1GŒ¿ª¶vÙ¢ÆÆDÛµu„cRÏþîÀ)Øï»±U3%I´Ù›XYÞÇï5ü׊í$÷Þª—<ìëÇÀ·<ì¯?ÍÀÀ3Ø¤Ó ØŠ¿BÖ†­¾;eÀiØÅƒ2Y ¾½ñ%ro-)LØjxUÌËc†øH€’Ú;ÚùúåßfÅÇ«  P:£ˆ’˜39H[‡C8Ìÿ=::¡­}ËGÓ¦-Z ëšz> >ù÷i”ö÷" çöe&Jîf}(dЭþ+âÇápZ=誴ÃD’þæLrsô!H´Vÿ·[‘” 8¡ß‘ü0ÆMØ¡šùÈ‹ÏF¾3ÀËõu)éOþ½üØ  \†¯Ð“ÝE°ß-GuÀ"ìB?™´+6Q°}†·+â–Ž‹ †,UŠï¢&ÊÅ×ü˜WÞ}‹`•CÙô ¡l=)HQÚ; µHÄ{áO¢t¼&pöÁ§²í¤YI÷1tÄ`½•`ŠHFĹqpŽ~—Èk·ZBñé#›×V,nöfsîº82É6'b“ñö†&Ù×ý@S’mrE+\Y»­Iy¡¢Òlô Ÿoßõ ŸÞ¼Ü Ü…;S’ó¶Òøjl2î4àxì¼}é6øp vx¶H6k*“lÓNa ‰]ÉJŠï®»ã&{þIe¥[(rØz’CU9lhVòÏ++"t,t|åÀ“ÙaÚ6I÷>rH¢‘d˜¨¹Á ¤¾0øÕ#Öœßòé@/l¬ªÙ Lêå¿&PÈÕ]þ|Ž­fJÔVØùÍâïÂkøoOnª9šQÅR²ŽÆÎ?ç¥&ì¾ù+ +!›n/ÆG`ìØ›äV#OV ðw`>:(ÙÍÍ1£3$G©ŒG|õŸ7þǺ'äPºUˆ@1ÌïPSiÏIšU”2c ýýË"…Š90o†»cÖÐáƒ龈ß.lzÚ£Å@‚¡h0±á£spÒ½oyÇDÎI÷“:°•MÉŠ_ ¤{AŸŒO€Ç\l;WlpÑf0ÉW€²1À ö·»’m=p6Jþ¥Kp°/0 ¸’ô®ìY‰­6Nv"‘LrsÌh5DÉIJŠo667óë~BÔJ& ”;Lç0¬vó É–6U¦Â´CÛ«t~¡¢´œosÛLL~Ø/@0ddÝ0#·ð[/úqpŽMè…†ƒ\tï¹O5.KºŸüäf5àãéyîöEl¥M2îòy6Ü.ÛMð4ŠüÀî»^ %ý [•6¸o†%KbÞǾ÷uÀéØ Át¨îA‹,HörsÌpÈüŠç"žPP|óë~Â'k×P4<@hD€‰£ŒÒs—lQ k‘õQZÿ×Id£a܈z.9ù"¦ÖMtÝßðQCTý'’EBNàfìb)2»Õ_YV×ß+Æü´fö"=µ-Ù걞ÞK²M=°G·ŸÝ ÿu“xÌ%n€ã½ "- ç>èÖ-ÀtlšÎöüÓ ÜììÜŒ­PöÒ.Ø O‘l¤c†%Å=ùÿzái¥%“ƒÔ s7rËR‘fU&/bhÿ Bëa¢†=¶žÇ÷N\ÌðÚdçˆß¬¨(ÄðQîÛ‹ˆ÷–-jlî󠫦¨ßaÀÑ9ÔƒíDÂÈ=ô“OÜÌÅ×5 ¸8$ɶoÿs±Í\²Òe»­<"?ßO±Ï€Sbÿ/Ùã¿À©Ø›=¿ÄÛ–/Üß…I3¤ ((×¼©™+nþ8P2-Ĉa¦Ôo¹+¶wB8Ÿ)¥Ad}”–Ãt6D(/)cáA 8}ÿã)¥Ôoݸ‘ƒúºÉ6&j<™‡Ë8 4 8ùá¿ñOd±‘s½Í”Ø¿£€Rà`’Ÿ·.Ÿÿèâ¶šcO£ÈO_&µ¡Ò¯aßçG¼ GÒdp.0 øÉGõ¦œÔ“Ç"é c†]ÑKÆýâŽßóYãzІZï0}|§—uÈZZUý—¨è&CÛ[aZ_cÚ ÛLœÅe§~›]f¦~lª®©dðÐZ¢¯5\Øô4Æyîæúym¯óÙ ¾vp5°[ª0Æhøï–>žN²M-p ÉÿL²M.zße;­è8°Å)´}»"ìGÞ„"Мì¼ãA'“=è'Ué\ýXrŽRP””Œz磥Üù÷pBCæ1{R@‡a ÿ˜i·+ü¶¾ÔIø³(ƒ«j9çÓX|ÔBU¥ž´ Œ™0ÚƒHE$]Œ‹8'Òë0àòÎȾء©èà;MßγíÔAIiª×ý"’N¡@à¼X Äé}5`ã¤>ü×1ÎßV-Þ¸.Õ~òÔ=$?×Ö¾$[ÃÁ.`ðªË¶xHžq³à اO®ø+¹«8 ¸Ô†‰wxn' ªðhû’Ú€×]´sÐ1Cr€’1/¿ý:Ï¿þ?ʆ˜»k1ÅLK§!À=‘Ï£´½fÓÿ: ¥¤¸„wØ›Ÿõ¼Ó¾…R›ë¯»êÚ*†âY"’Ë56âp¯]í[íw³% &å\ãhõß~4¥y­àÉ>’+^pÙNs};Òe»ßÏyˆøê à‡)´ƒwC'Ý®V¬ Ä{Ñe;3$ç„ü@ ÇõwßB¨Øa§ÃŠ) ¨Ì›¼\{,‡E› ŸFˆ¬‰¨×VÖ°ÿÜùì5gÊJÊ<ßfQqã'×{Þ¯ˆ¤‡ÁÜàà,HµŸHQô(à§]?©­ž22Ån›MEÅ_`CŠÝäµ[cÒØÿCÖÀm°«²Rg = ÆÝÜmQàrcÿ]ÌÃ~^ܘûÏhwJŠW^¾ê¢ÝþØ)RÜî‹"§ dÄ+ï¾ÅÞz‰.¦ªjàÊÿÖ6C$šÀ²‰BtC”ðç†ðº(&6bÀ 0gÂtvµs§lM(˜žo `âÔ±„BÁ´ô/"ÞkXÔôLÝ5oà˜Y©ôãçKtKF ¦:VËÀƒ R¢œßÖÃÒÔ¡ ÿíòo—íª€Ã;½ %/lã²Ý3Øùÿ$¿l²ä-ÀÍ]è­<ŠC @ñÊ¿\¶«îó0‘´RP2â†{oeöüƒG&6꼥 ‘&Cd£!Údˆl0˜ˆMú9ŽÃøõì4}.»ÌÜÚÊš´‡3vâh**½¯*‘ôræc¸6Ån¶}uÕ´†ó›ÞpLÒ Ml—VÿMD'6éôµ4ô½›`,$˱«ÎºI\Ž€ñÜ&µðOþZ\\à¢mJ7ªºq[Õ¬ Ä[ ¼‰»äôé((9D @I»Õk>æé—ÿÃüS_L¢9‰B´5Jt“]Ä#ºÉm5D[zÎuX*bÖ¤il3q+¶™4›AUéOúuU?œÁCS_=XD2/DàÖN¢—å©ôˆ8ÇKF^[9ŒpŠs59¬¯ÝØôèê”:)·’žà]¸[á0×=ˆ»ÄÕ^ØÕU¹¶ÙD—íÜΫ%¹áFÜ%Çx´}· ËŒöhû’_À]p?ì*ò+¼ G$=””´»ÿ_@ÀP^“øš3›Ú²{Óaìâ±ÿFì^Ói0`:b¯k·¯íÍ ª¦ŒžÈ”јT7ñÃëÓ6¼·?ÃGaT½V³ÉUË56Ö]Y}pJ*ý‡%Á°s ©.fÌ=o.ÑÜ8 zx˜áq¿·zÜ_®xø¾‹vàÀ™Þ†“ÓÜÞ|ËÓ($Û¼4|B­Ú£íî²ÝX¶Ÿ.)ÝÄ×¾í¢]¸ˆôÜÀñœ€’výû1*p’˜H*[*£­†h³!Òl+÷L›Á´‚‰&— , †¨©®fxíPFIÝБŒ<‚ÑCFQSQ•¦è7tø êÇò; I‘1æÇqRJ“G_U³Ƥ>ü× jøorn~äaïÿõ°¿\ò2v˜âxmOþ°Ê»prš›¡Xïu ’uÞ%ù`)P´§¸m·€n+Z3aà\¿ƒ(P/+qW¡zðcàcO#I%%í>Û°ž±S‹~}K«¡µÝ§ À°!¼Î^%²Þ`:¶Œ£¬¤ŒÁUµT•UPYZAUy%ÅEÅ”SRTJyi)%Ábª**\UKME UeÙ;ÝÈðQC©—ê"Ÿ"’ .hzÖ“Å@0'c‡µ¤âãUuO¥ØG¡¹ ¸”T+/{öW¨ ð+à'.Ú–ßEU€]Ü$Û€>Æ>HitÙ΋àJ ‚­ÀJÆœ·›.“û± RÉ<üw7áJ±Õƒª”¬§ dDU]b×2­íðÊRƒÉpþ/¼.Jç'¢ë6W÷u-À1aÔxƯgÌð:†×¥º¬2³Á¥QÝØ‘Œ=Ôï0DÄCž,b8 {–Š;9–HŠ}šÀ“À|ú2và·ØaÀnîÂkÿ’§妨‹6©~HnH|‚ïÍ ÐâÁ¶;°IÀñI¶›†MØdÓ’ƒC€¿:)÷×o€ïànö™Øy1_õ4")(i焪 <þ·­^y?BG†f‹2 :WG‰Ææ,-)eî¤Ùl;y63ÇN¥2‹+÷R;q´üÉC-’òÅ»£•Tݺo€ÏyÐO.[Ü œí¢m»ÊéθK€å“M.Ú°‰W/=éPìwqÆ`@/ûHÜ,¨ÑžÝú€ä€!à À¿=Š!UÕØäß¿>ÇΛ»ÐEÛö˜±+6É-’•¼b"Ò§`­CEYÿ¯ië€WދЖ¡ä_çÇ6ý·“ö¥¢m†ic&sÎ!§qÝ9?fáA ØqÚ¶y›ü+)-aÚ¬‰Jþ‰ä©e‹{| ÂaéªE_ð5†ÜuàÅL¸…^ý×åÜ'v.ö0–\å&îæ_Ì„ÉÀs¤>Í—J€{§/ùK¢Š±ÕtÉòrnÍw]¶ÛÓÃRQüû]#ÙáÜßô™\èa,"žSPÒ®hH€²Ò¾+Û;àå÷#´f ùm6´¾ÜIûûè€í&oÍ|“ïw;Mߎâ`âsæ¢ÚÁÕLŸ=‰²rM/"’ÏŒ17ø@”ÛqtÜ¥Øy RÑÜíA,ùà=lU¥[—`+: Ù§.ÛÍô4 o„]g+¿‰ó l¨¸;Y¶_§ÍÃ]¥¹Û¤]o^tÙîPcp«û]¿›ßHoL¡ý°¦"Y)Û,’*G9úÈÿu„ ¯|¥-Õi€b cy„Ö—ÃDš ãGŒáû'^ÀùG|…ñ#êÓ¼qÿ…BAÆN¬câÔ±ƒú؋仆 šžçu¿¶t¢wùµí>Æ }ûîçˆ,ÂÞ€ïY4é·5þ~$ƒ” ´«(ï=û×Ñ ¯¼oØÔ–Þ"Ó ­¯‡éX! qÒ^GqÉÉ1iÔ¸´n7[ RÃÌ9S:<ÛÎwE$ ~Uš×V,nöò¯=†ûª+H=˜oVa‡u¹Uáaï\¶=ËÃ8’1û OÛ—-§ïÏe"Æ`c¼ 'mà<à?Àå>Ç"¢ ¤]o£M;ÂðÊûQZZÓ›ü‹¶Ú!¿‘Æ(uCGsé‚o°ßÜù8N¶ÿy¯¼¢Œ©3'0aÊBEZïG¤Ð;Ûp?w—k;2½Í<nwÙvvByéérRKªŽÃ.0Ë“h¼7x;”´ 8ï*ðþç²Ý(àXbpÃÁVÖ½Lvα6;ï_‹.Í^¾Fv%//ÇîgÉjÃ&c½þé²í—±ƒLªÇ®ô¾K†·+Éû1°6…ö°ÇŒl­¬=W¸;ýPì°~ÉsJJÚÅÏÿÃkDhIså_´ÕÐúZ˜h›aîÔ­YrÒbF™Ömfƒ’ÒÆO®gúìITVççB&"2°e‹qÒ<Ü–_ã&ì8Zý×n«øî2´¤VNÙ@ê?ã°«+g[åÎþÀ«l¹ªíeõ¿[ãÆq—(JÕàQà Æ‡í¤khy"BeÀµØ‹õl¾rp²Ë¶âÍ"GÝýÕe» ¼ûŒ$bGàY²³¢S¶´8'Å>&bÚ?õp{C†ÕÄðfIÌêEMϯdj{Æ®^)ÞI6™·[m }û:ÐbAàÀKø³Úc5v’úw°ó°õwà÷j5Ù{RhûµØ#Ý^Ç.öQ™í¹u,°ØeÛ ì¿ï%`?Ï"JÌÖØd³Û…I>Á}µÞ@ý>î²­ÜÌ–Õ³^­Êþþ'ÄsImú°ÇŒoaW­Þ!划W ü{Ì8žþ?à÷’ãò3+"Y%à@$ ¯-Ò´)Í ~„¡íÍ0¦öÙv7NÝ÷Ky™+-+¡nìHfm;•‰SÇj¨¯ˆôÉÀï2´©H8I%I [ºä*[n£·ÙÒÝ:lÒ¬Óƒ¾¶Âé» ˜äA޽ˆ[†Z[•@›Ù¸_­µ»GHí"øì0V¯OXìðºbWøêqÿ^Û ûœêÉéÖÀßcLÌox¶‚mT }üo>w½I¥Ú½ [ü}¼«¬¬À~Vß#3†’>Ÿa‡¼‡=èk6vÁ;°Ó¤ÛHàà#` ‰Ýù“R& IDATÙçn•4SPÒ.jൢ46§ùšÄ@û;a¢­†ÙfpÒÞGçMòÏq*«Ê©;’™s¦0sÎFŒªÅ=Dd@¥Á[ÉÄb ¯9¿%Õ»äÒÓ*``ûWùfÎùp¾G}9Ø‹¥·?a+‰¼\ù¶Ø›Ü]½ˆ”d—zõQ;ôÓ-[øÞ¬<8;÷á#À^)ö— %Ø}ÄËêÄý° …—€…$¿oôÇæc«ënÅ}åØÅ~íAL}y€ÔÔ!ìçä l²Çí¼•s°‹*,Ã~Vu‡>?<\èQ_p¶ï^ì||^3Š}±IÆåØÄv²sCýÿ¦4ÓVÒîõ#llO"®ýÃáuQꆌäÜCN#àän~;PQYFye9•UåTVW æî¿GDüóáÅë7Ô]U}7†SÓ¹cŒ†ÿ¦Çë~§®ÇVcœéQEØÅŽÀ&;=žÞÚì§ [1;‡ß~¤–x;ýW€_¦ØÏØIñSI`Ç^ô.~ƒ}^cà…!*±Uo;`W«ÜÜ»Ži.ÅV«y½0ÊvØÛ/±ó‡þ »Úì»ØdT¢•w5Ø}oàH`šGñ] ´xÔWo:€Ÿa« S1 ¸¸;Üùil’ù#ìPã®*°2`0¶*w;ìTó€™)n_²×5ØcÆéõWŒ½r°†žÇŒHü˜QÍ–ÇŒDªÃû3»Jvzç‘_äÚSrІ&pÒ<ëEx]”ÎÕ*ËÊYtäBÊJüXpÎâ’bJËJþïQ^QJYyiÞT/ŠH607€sj7Ð^ìÿœÆþEÒáëØUUð¸ßaÀ—b°Ã¸—DZ‰&l¡{±6 »@À`céòl_*I˜µØÄÈÅÄ3ŽÍ+NF°IÒFìŠÍAl2ª;ÚDòcäÒml®›†þƒl®¾(ö\[=º›TØ€­ oÇ&V˱‰¬1¤g‘Š‘žÅ?âý[Ù[ïA_Õôü w‰`“©¥l£/­Ø© êÒ¸ qçlìçv_ûŽ­ <.ösû™]†=VôvÌý¼zYõÛÝ÷°•¿^¯Ú->SPržé„Žw#œ¶ïñ ¯ê[,@€PQˆP(H à†‚ÿ÷ßââ¡¢ÅÅEö¿%EòtÉ«5=Wweõ+Øá¤žsŒóð²Åéè[$:±{w‡§q;ì|O™˜ó©/£° ÏËRìçÿa/RǧP7A`zìQ^Â&èîÁ®º™n!lub¶ï3l%Q&æ&mÃ&-Ò™l âípÍx-Ø ×¯¡`6êÀþ}î!} Ç€ÝÇü>fÔc+¾S­ª•,£ ä¼ö÷ÃD; »nµ;LK˵mEE!ÊÊK)-·•z%¥Å…(*)™'"YËÀﻡ÷};ZýWrV;v¿[Ø\}‘¯¾]ˆ#•*Àì\sû+Eþ{‘ëǪœk±UDW™’ýÆVÐ-Ëà6oÆ.ºq`·é• ØäÒ“Øy%;µa‡íþ8ÚçXÒíble­ªóˆ²’ÓÂk£„?‹2¤z'ï}TZ¶ ©\ÍØ £™µí4fÏÎäã©7’!Ãj©¬*§¤´XÉ?Éj±Å@Ò1SK8Rú×4ô+’)ØUNóy•ÿb“ ^|<Êæá»¹¨X€ãî#ãèÄVeEj Xd£(ðUìœf™d°ó]®ËðvSµØ›ü»JöêŽ'M7U³ÄóØc†’yF É]aCÇR;ô÷ô/žàù¼Õ5•L˜2†­·ŸÎÄ©c:b0Å%EžnCD$S>¼xýpîöº_c¸ÿÓ‹>Mçäî"™c«›}ŽÅKa/T¿<åa¿ßÇVÐåšû±ùßû9-¶ÂVåƒ(6 —‰yÿzÓ€Mî&ºˆ‚ßžvÂÎ Ù%öKé_›À?‘ô.p“iK±Uñó° ’HžQPrVûò(ÑÃÎ3æ2kœ7ÓÆ†Êìí¦1yÆx ©Ñb"’?Œñ|E7s§×}Šøè.ìÅø[~’¢õØE f`ç8ôz¶(6±ø Çý¦Ërà0윫º=¿ÞŸp¶ð9¶ õ`à=ŸcIE6‰~“Ïq<œ‚ÝO³Ù¯°«¶~÷¼€¹ãvì –wü$Eë°7Áfbç8Ìļâ%%'E[ á†(¥Å¥·Ç)÷>r³¶Jý¸‘«ÒODòÏê 6>¼âa—ëj›šõ°?‘lð°ðì|O¹¤¸˜Œ¼=UPíØ9°nIã6RÕü[a×[²2Û-f‹È½a¬o;b“Ùà.luV6~†×b:û™—mû¥ôï ì"k—;•§]Ú±s‘NÆNƒÑÛþ(yD @ÉIíKÃc8j—ƒTU“R_åeLÝjõãG*Òº8"’ߌÃövï›Kt²(y©ø!0ø§Ï±$¢ ¸[ñ—ÉäQ'¶Òêl²ïÂ÷>l5ËÅô=D/[*»ëÄ&q§?#û‡¤w`?+sɾ*¨;}° ·lñ°5ð@?¯Q0÷´K°ÇŒûIbZ_cW_¿ìü.”4PPrNä3C¤ÑP7dûl·»ë~Ç¡~ÜH¦ÍšHy…·óŠˆd+'êx7$Ê5üWòÝ{ØÂ>Øa…Ù¦¸ ˜€]÷CŸâø°=ÞÎ3èÖóÀîØêÄÞlN´¬Ã®Ü<›`˶ tƒMbmƒ­–Ͷp—g°ó>ú]™Ø€]|æ@à“^»!ýáHš¼ ì ìOv|Æ[üû½r™]¥[²€€’[ ´/ ð¥ù‡t¹òn(dÊŒñ 5Tsü‰HA1˜Ã<éÈ¡aõ˜Æ'~¡H^ø'0›XúvÑ?­ÆÎñ7ø6'2á `àd2?]»(ÉnØÕT½ðζ¤Zo>Ç&ØÆç/ú}¯Ä÷=œž‹Wd«O± ¼žám|˜†]|&‘¹Õ²91-‰y{¼˜<ŒÿÇŒ•ÀØcÆw5þ†#~ÑxGÉ) ¢› ÓÇLa›‰³\õQ^QƤic5ÏŸH?V/ÞXëw â½a¿Vé´·Ï÷¨»;9Ö÷Z·:€—\´Ûèu 9®wïcƒ×dÐS±ÇHàKØ9ÆvÈж[‡€Û°Ã;3´Ýdl|w`+ðÎÄ&Óu·u9vÝë\´o ù}دJ·À5±Çtì¾w01CÛÿ¸ø ¹[5ô ö3t6² éÙ7 ð46áw'vq”d¬%ùý2ÓÇãvÜ}ÿZâ鉨c›ÛghÛ­Ø*ÝÛ° Élös=v¸ß”Øc2PT•Ø$C'6™Õ„M4a/¨—Çb/ä’­ÊFa³ø6ù7;oÜL .ö¨ý®[µ²!öX¬À&›_î4«…‡zú0öøCìç!Ø}nRì¿“á@PÔ%Ø}°»ÈHvˆàòØ_Æ&³òý½^]-ûçØ}oì àãc±Ø÷jöº¹;½ˆM&«ÂJÑõ™½)öó63&Çþ[‰=fTa?»ñÇŒô~ÌÈöEƒÄgJJN0й:J(ä¨]Jº}iY §SòOD Öäk)i s 'îÀIh#‘B´*öÈ…Õƒ3­ x!öôø<öøßä˜N´oŠ?VÆÿð;ÉʆHNèXÁD {ÍÙ•áµC“j[Tbòt û‘ÂÖ®9 ìE_‘h4—‡ÿŠˆˆˆˆ%%ë™6Cøã(%E%ò…ý“jë8§£¸D ~ˆHa3Æ|Ù£®^ùäÂæ·<êKDDDDD2@ @ÉzíE0ÆpÐN{SSQ•TÛúq#©¨,KSd""¹¡îÚÁõŽÃ>^ôenô¢ÉÍ(Y-Ú%¼6JUY_œ»gRmkW3lä4E&"’C"áE€ó 8­¡6nO½É$UJVkÿ0 À»HiqiÂ튊BŒ0:]a‰ˆäŒáWWŒÀp¦7½™»W|kÃzoú‘LQP²V¤1Jd}”áµC™¿õ¼¤ÚŽ™0šP‘ \sMóÆZš[ýC$¯ECå^ôeó[/ú‘ÌRP²“ö¥ŽÙíBÁÄ“yƒ†ÔP;¸:]‘I-[ºšÝ§Ë{?ÑhÔïpDrÞØ«j&‚9Ë‹¾x«aQÓ3^ô%"""""™¥ d¥Ž†ÑÔº ì8mÛ„ÛƒAêÇLcd’nk>þŒ‹Ïü ‡ì|/<óšßáˆä.ƒ1æ<ªþnð¨É0%%ë˜0t®ˆààpÒ^Gã8NÂmGÕ£¨¸(ÑI¦¼öâ۵Ǚ,:õ‡|Úð™ßáˆäœÑWWì½Å/Œ«îÖµuß”bH"""""â%%ët,‹`:a÷Ù_`Âȱ ·+-+Ѫ¿yÆý·>Ì3ŽåºËo¡½­ÃïDrÂèŸWMwŒse¯¿LüžJ÷&×}öÍÏšR KDDDDD|¢ d•ÈFCgC„ò’RŽÞí¤ÚÖ™Tµ äŽ–æV~ò_±ûôc¹÷Ö‡ýG$«Õ_Y=Ø 85u¹)Š^çQ_"""""â%%{hÿ Àq󠦢*á¦UÕÔ Jüõ’›V~Ê¢SÈqû|w^_êw8"Ygòµ”¸˜ìa·¿ýäëÍk=ìODDDDD2L @É+#D› ÓÆLfÙ;'ÕvôX-üQHžù׋ì¿ÝÎ?å‡|¶f½ßáˆd…ñK(m WßEoóþ¹×iLè û((Y!ºÉй"J(âËû—ÔPÞÁCk©¨,Kct’¢Ñ(÷Ýfç¼þ§·ÒÙÑéwH"¾õóÚqÕÕ8ÌË~qnn¸`ÝJ/û‘ÌSP|g ´½ÁD ‡Ïû"£H¸m `ô˜Ä_/ùgcc3—}ëzö™s?ü¬ßሸReÅìÑWV]VwUõAc/«”h»a¿VYwEÍ·èë@r¥Ók †;àqŸ"""""âƒßˆt,m‰2µ~ï´oRm‡ŽLqIQš"“\òá{+8åà Ømï¸äêEL™9ÁïD’:ÈÁ\ŒH‰‰Ö]QóŽyÚ1<8Ï®¨Û°œc‰Ô_IY€Ê‰QØÖ8ìG{ûá8¤eT?]ùÍM éè[DDDDD2K @ñUx]”Î#”—–qæA 8‰¥ƒAFÖ Kct’‹žúç ì·Ýœy$^òªj*ýId@QÌÝ&>à˜YÀ,ãpfÄêVU®ÄDÒ½à¹ÃRÊ+Ó¼!É%%í"§Èlñ¼i7´¾Æt±»Ft“aͦÏîwÄè¡|²Z Sæ“5æI?áÎ07ýânþ|ûß9ï»§qê9Ç jÆÉRK9°Ë¯rHÚ¯‹qœ±zaæ mODDDDDÒL @I»O/oð5×<ú‡ô"gýçX²èjî»õa–\y>;î¶ß!‰l¡¾ºz;Cz†ñºá®_µ¸ñß~Ç!"""""ÞQIŒˆä½×ÿ÷.Gïy6çžø}V~êw8"=D1{øC7/ãl¼Èï DDDDDÄ[JŠHA0ÆðÀ1æq\ö­ëiinõ;$œÝýއõAÇ9zÕbôáÉ3JŠHAéì褥y~‡"wvõ;  ƒ¨sÌŠE>ô;ñžæ‘‚±ë^Û³äªEL›5ÑïPD½ºf˜ZŸÃˆ‚sòê 6üÓç8DDDDD$M”‘¼7zÌ.ºt!GŸ|€ß¡ˆôàßçÿ‹:ŽY¸jÑÆ»}ŽCDDDDDÒH @ñÄó¶æ/Ïßäw’ã–¾·‚ó,ñ¬¿òŠ2^pç|s%¥Åžõ+âÇ8»Çøµù°c8mÕâ¦Ûü @DDDDD2C @ñDUM%sv˜áw’ã‚¡ 'ý8ŽÃAGíÉwöuêÆŽð¤OÏ-!`³›O[o2Æœ´ú‚¦}Ú¾ˆˆˆˆˆd€"’W¶Þ~—\µˆíçÍö;‘~Õךe¢‘!>lúݨ=âãÅÍoû°mñ€"’FŒÊ¢ïŸÎñ§J  Î%˜H¦çÿ3sc[¨èÂu__·1ÃÛ)("9­¨¸ˆ“ÁE—.¤²ªÜïpDfŒÙœLmîgaâgjƒ"""""’=”‘œµÏA»pÉÕ‹;±ÎïPD\pÒ?ÿŸCƹtdņ߽´Î´oODDDDD²’€"’sfl=™%WϼùsýEÄ5vN6°ïq÷Ï`ÌMw­ZLëj;‘Ü¢ ˆäŒÚÁÕœÿ½/sÊÙGòhÅ`¿¬Z¼ñà–Ô_U¹;&pŒq˜'Ù]ð𸠘¿5œßô®ç‹ˆˆˆˆHÎRPD²^(dÁYG±øgP3¨ÊïpD¼å`VÑü6ÇVK(ÞXQ9Õ‚Óp˜u¢µŽqjp(ǘ6pZpØd Ÿ¼ïçýu–s,Ÿÿ%"""""’¥”‘¬¶û¾;±äÊó˜2s‚ß¡ˆdÄ›Kè€æ7€7üŽEDDDDDòƒ€"’•ÆO®ç›?:‹ƒÞËïPDDDDDDDrš€"’UÊ+ÊXxÁ œ{ñ)—ùŽˆˆˆˆˆˆHÎSPD²Fý¸‘<ýÞ= 9ÄïPDDDDDDDò†€ù*à´cŒßQˆ$¥vpµß!¤Ä@«ß1ˆˆˆˆˆˆˆÄ ø€¤‡cÌz¿c)4QŠô¹‘¬£`þZêw"fí$gû ~!""""""O Às""""""’•”ÌWAž:ýC¤PãüÃïDDDDDDDz£`žãÌ[‡cõ;‘ wû„ˆˆˆˆˆˆHo”ÌcÆ1?õ;‘‚àp³ãJ¿Ãé€yl¬³Ë“8<ìw"y.â8ÑKýBDDDDDD¤/Jæ;Ç|Í(’N××;»¼âw"""""""}Q0Ïqæ½nŒ³Ðï8Dò’qžk ¬¿Èï0DDDDDDDúãø€dÆÊȳ×sŽßqˆä‘fÇ1μÕ~"""""""ÒUˆúÀªóÀ\ãw"ùÀÀÛÌJþ‰ˆˆˆˆˆH.P`Y~ötçz ØïXDr’Ã#a§íø Ξ~‡"""""""’% Ðróß­&r †½ýŽE$W8ð©1æ;õÁorÇøˆˆˆˆˆˆH¢”,`ËÍ󻢿,àp ÜïxD²’á¿ÆqnvÑßqæµúŽˆˆˆˆˆˆH²”Þ7+)¥ö NÔÙÎÀD Ç)ñ;._˜èzp>v o…ƒEOw¶ÿØïDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDÄ+Žßˆˆ$aP«•@»¯‰ˆˆˆW`*00À  û”ˆˆˆˆˆ¤_1°ø{1Ðý± ø0Ö·èDD ÏÏ€»»=¶IÃ6¾·ùi؆dà2à¶<Ö¯ÇE'""""áHàwÀu¤ç"§L®nxõq9ð[^ t´Æ^×e+àà÷ÀqÄ.""=½BÏïá/&Ðfgàzà·À ¼þá¸mœæ*RÉ£€7éÿXÿ¶oÑeÖÀ¯€½}ŽEDDD$e0 8øpp2°0ÒǸzs1=O@;€]}(÷ÌZèù>^`Û«éÿ‚Àt{ý\ -î÷ÿ/刈t—lð@ ×fám”, ð(ë¯ò+À :†-?''ú‘ˆˆˆˆKc+€µô’÷p6PâO˜=|Ä–ñÝækD¹çÇô^µW4@»Ñ@g\»ç=±sÍÀVܭ͵½l«Íw*"â¥d€fËïæ7h£`a˜Ï–ûÆŸ°ÇúÙØ›Å;ø_&õ6âáy_#‘¼ò;)p.ð ,×o{\|¸{ä‡Þ&.ĉ¨‡ã»ýü69—ˆÞÞ¯®;Üý9–žßQ À~ÀÆØÏkØrHþ^"RȪ)Ý~~ {ÃÅoún–¾œ÷ósÀÑ@$öóàõŒFäŸBüœ8Øsþ.«€O}ŠEòß6@0öÿŸ`9À¯xhG»I,q˜gÅÅÒBaÎx&=߇I´€À»{û$Ю+ñÛõ¸,6Ó¦¸v$«ˆH.;Šžß³Ó´d+wÇN¡Ñõú(ÏѪ ÀÂð*=ÿÎ'ûޝ¾ˆMøu½aà_#J¿jzþýûŽä¹v6ïk?ò9_¨PÒíblò¨»&àNàŸl^Ùu8¶Âì0ì\€Ý÷Íc°‹<Læý x8[Eq3ðqømH m?ÂÎùøåX?ÿJ Ý„¸ŸßI Í;±m†=©ü{ì!"Rû@ž¶ÅV{÷Ïú‘d 7Çú|õv¨ó±@¸xÑ׈Ò/[¿³$ÿTÅ~!"’Ï&a“fÝïì=Ž]í­?c±‰¶›ç‹Û9}aJ®Ä} [oÇmsß lSD$—Å/\•-€n¨0ÿÙrÑ‹:_#’L›‹*%3ÆÑs_S ¤€ßH^;(íöó»ØJºh·8[ñ·ø*vNñO*€nÅW(·ûƒˆH.ñã»ZÄ­ [.Ò¥c}aÑw–dŠö54XÒ+~Þ’«In2ò‡±“™îYDâ–†hˆˆd?]àˆH.Ñw–dŠö5T(é5>îçW\ô¡ä_vÐASD$ûåóÍšÈÀ/‘£óKÉík"¨PÒ'€]¸#þ¹LšŠ›h0èÄ.ûþpðרs}Þíçfàv±ÌÅ®x¸W,ŽªXÁNô<ÐÐè.åÀIÝ~~XÖíg[}y<°ö߉mï)àì„ìýÔƒbñq¿?ØØK»÷õòüqØE9º<‹§»"`ö=4îõ`ÿMÓãž{’-' ?™ž«FßGò‰ä*àìõm±ïa%°û^?ˆ]È&þßàÖ‰@Eìÿ_þ÷û½³/`ߟì{ý0ðKà³>ú­ÎÀ® :»ï|ü¸»/GSˆ{p4p$öó6»_|‚ýÛ܇ÝçÜ(Å~^Ã.ì2 ;÷d ößð,ðgìgØ©Ø}eOì$ôƒ€ Ø¿ñóØ…jÁ®ÂØŸ°ûH—÷€'C=p`·Ÿ×aÿ.ýYÀæ©^Äþ-» ¾Ææ¿G[,žoO÷Ó§ƒ]¥õKÀ.Øï¨ !ÖþÁX\ýƒbF`ÿn]â?ƒåÀ©Øý~öóºx»OÞAßûåTìga_ì÷z'ö»ó àFà¥cìËVØïÎ}1Øï¡µØÅŒÆ®J¿<Á¾ØÏ_—§°s›v·/öû|öóc°ûàsÀ±û`†a߇A@-öýìîhzŸCwð·ÿÞ‰/À‹Ú’è£#îç±ûѰswß_ÀîC“ø÷[)ö³ÕåØãs—±À"ìçµ»˜ÙØEÎúZlös°v_ê:Ž|kówì{è±ìþÒµhÆjz~ÿ…°Ó§œ lì~ð à7±ç¾Ðíõk±ß" O7 IDAT¡Éš…Ý_»¼NrÓ´T`?ÇÃè}>áØs­î> ßQØóµƒ±ï}vÿúûÝòöýJf$ Øcç®Ý~¾!î÷{ç`ß“®ï‹¿ “ÜØïÝSâžûý'¿Å~w€Ý¯ûýLì”<{bÏåʰÇÐ7€? ÿóà¾ì‰=Öì†=ftíÛ«b1Üý,Æ+ÇŽîé:¿Ü#î÷_ÀNýÓ›ø÷`{`»Øÿon‹ûý¡±þvÄî{Ÿ`Ïÿ¿Ýí5Ûbå]>ë#†xñÇ»&ì÷ONÀ¾_`WÁþOÜï÷ÄîS;³ùœï6Ÿó­í£ß ìßú(챬ëœïeì1ü¤vÎ78Ö÷QÀdzžó=…ÝŸžH¢¿}€‰±ÿoÀ^u7{ÜØ'¶½rì>ö.ö¼ä·Ø¿y_J±ß5]ûÚ~q¿ß–¾÷µþŽOC°Çí}°ß«ƒÙ|üy {þø‰Ÿ3‰ˆäOé9ÙêùÚîàV6/"Òßãm`ÿ~úÚ1îõ+’Œe+¶œÈ¼·G3ð]ll £ãÚ~¹Ûïf`O$ÚÞØ¹wúrg}ôö¸µþ>ˆ{Ý®½¼f†‹íÅ_P‚=wÍ6ýü;ã•`' ^›À¶[€ïÇÚ¤je·~ÝíùÉØDWq¬Áž€ÇÛ{"Þ_Ûç±aÉ*ÞSû=—L¢èÞnm»ß(û¹·m,‰½fVÜóí¸«ˆ‰??8"ÉöGõgy½ödU—³å¢s½=VaÏú;ÿ‰wv\]7'ƒØ„ToÛq{ãkp/} tŽÑý³Ðý†A96Y4йð›Øï£Dm…Mì%òw{{s©»Clÿè+quI·×´t{¾ûýÙ[_÷Äõñí¸ßt#®»øãÝG ´YÖíõ¿íöü$ìͺþÞ‡µl™4{œ]9@ÛÿboD$«ø‰óýƒ-oDõåîní^íö|ðÿ°ßQýmký/tÿ·IæÑÛwcø6é8PûFàR6'zED ÂCôü2l`à€Su ‰ âO*~ÚG©$Ã^,&ËŸx‰úøàwcÏ…ÄJ]«úÙF!&G’øE[ü Uª«v?i{0öÜQ$¾/odó —ƒ=A 'Øö%Kû7H´í÷ˆ/Þþ$ÜøŸØÇ'¯= ‰'¸ 6aÝ—BJü˜äöÙfz¯€ŒŸüfìùéû»ô£¸>ÁV¤»ù{œ@ŒÐ3صýQت“¾úîþ·‰O.œ“àv»”Ò3ÚHÏÅÞáepî¾Ó`sÕý@‱ŸíþΓú:§Hª À×bÏÍÆVI%ú~¼GbïÇÎØÏT2ïuü9v:€›ø,§ÿcÆq}ø™ìªâ=‚Ä MØDl—o‘ø9ßË |Ñ]7úz¬£gEe_º'×Äž=/Mt[ŸÐwRÓË`0.ÞDKIüF”HFh°¤Ó]Ø¡]Fa+;¾„»ùr"p3[ÞÙý›Tù{7i vØB×—»›j‘œŽ½«×½ïìð¶—°ùQØÀQlþ,޽ ;3‰mÆÞeún·çÖaØ«°ÕZ»c‡ u÷5l²âU¶Ô‚’GìßP÷û ô~2ÖÒËs‰ŠtÛ&@ =‡7±åpL¯V Â4¶—˜þMx­Ã^$ÌéöšêXl^…u›÷åFì…[vøÙžôæ\…MÌìƒ]lçëÝ~· [ÚŒ=a܉ÿßÞ}‡ÉR•‰ÿÞ@ÉQ@A * ‚‚((×ÁÀw]uÍ9'Lè.Ê¢˜HF ˜1 ¨ˆ  ¢$I|Inšßo×oªN…®êé™î™ûýŸÉ¬‚ zß…úfÓ¦œ ³‚ÊžMd·­ šs<“â6ù"îÅ}êÿ%ŠÍÈëù—ÄÅçz¹ùï“›î6ÊMÒÆÁFÄor"“Ê "x¬½›êº€r ìbâ¦ö/½r[ÍàŸÄä6ó*âwþh‡zÞ‡hõ¬Üg¿ëÝDš‡PÜ.÷%j|‡ø}²§üıí|"pýHÊûí;{ßãâu{qQŸ‚ߨ«ïÄ~¿)±Ï=—É`Ç~D¡g´XFfsb_û“Ƕۈýó*â<°‘’w0±í¦Mû ¶áüqsýäï·QÝ|p·ç~>Cÿ2wA ‰cåöÄCÀ|væZD³²ÝißtbßÚ²gÇáeD“û;{õÈ77¿±­¦Sn ² /#~§õ‰f¼{%Ó}ŠX´ÙfóuœO\kå3n®$ö¯u‰c~¾yáç)Û^D»…¾-$2ÞÏè8ÏaÙœ8&~ŸÉsäò^}.%ÖãÎÄÃíüñv{¢«ˆ÷6Ì{"0–þ“¸Ö=“8ÖlÔ›÷sˆ ô–sU®ÌRŠ¿ÿÚ÷›ÅTÿ&é6Qgc" w@î³ë‰LÇl{èÒd}ºmN\/Ãä½@zÍ·ÅãÚÚÄ~»qìx}îoWûüíÄwÝ‹âµô®½é?Ö¢n›×|÷Í}¶‚Ék¾«ˆíáÁ½ï°Mošõ{`ï;´±4üqMñ›ŸEûÖÞïó6%²«šò.£¸­­E1øy7õ]¤ÛÛk‰‡wy'ö^WÛð6Ä9èÉ·é«k–!IsÎâISú4d)ðI†› ¸åì·Û‰“\Õ“®5ˆf1KˆLŽº`ø €»QnŠr8õMÆÔ›oþ)çcæŸfæ³ ï NRé²P~Â9A» ÿu+ÊUõÛÓ¤M`êâ¤LÚŒ¤N× ÀUÓ(Ç(Þlæ=(§Oa•Ï\Âd†Ë=D0#Í>Û‚ê¦ÁßÍýÿZ" 5 ˆïK5òå.£ |>å'êÛo•5ˆ¦æiýêÜ›¸±ž ‚öïe²_£Ô”3ŒWPî#2odú{(ß„çíK<¨øåxÞ(3Ï ø›ÿ”òöX•=ò$ŠM —u™ ûÑùcNSŸô©{þõââ8ÝÞ œýp1qS½?™bðbû~oRn‚bóè:Û1lÈ^ÇǼºéÓl£ç5Ì?Í̯‡{ˆ‡6鲿€Ò,·´ÉZ4뼩yÔTŒ"0ž{+ÕY¹ëA»´Ì÷û,+Íü“ßq±]¤™Þé¾•ÏÆ<™Ø¦ëšš>ò5KS¦g&Ÿ8A¬‡ìÿ'öí¦:®AùøßtÜL›”MûÔêªj»h“ͽ:åLô»‰föU×|ÇætYŸm±¬4ð]LvEp åëÌùt˪Oë™Ö±Kà ŠÍ&£¼ÉìCñ\2A\;55~S2ý¥Ô:´€èŸózúgÓ¥çò7ô™>•f¾ÉóÛµ”›|/¤ü=G™˜^ó½›rØ›SÝ48þ¿Žè¿6ýn ®©ò室¿ìó‰kŠ|¹‹¨¿¦^èk´Ëq7ͨ˟¿~@õõõƒ(®¿ âXš®³*é¹áÃ-Ê@ìßé:|}ÃôÛÛÐ=´Ë„”¤9eÊÍü ÐñôoØÏÊÍ_n¡ÝA÷4³ºç7ñù2hQ) «2=2i0{]@ÿ~7¾’”iódn®ßMùû½¿År6 ¹oµ.ªúm¹’æmxꛋüšæßèÙeúí‡éMÐ…ô¿Q›Gùâ¾)`ú"˧.“·:åmäІéß—LÛÔ>³*åpR£ vÝf!2ÒþY_ÒX"<šbÐðË ÓÖ5»Ég¥TyQM¹Äï×tÃ’ößwiŸï±ŸäË|–þð-)Pš~³4Øv߆ÈêHopÚ4ÛšëÀ3i×_YUßlMMÓ`öZNd¶´±!‘yÓô/ïå”ã~7±i0{D»~)!HæË¶½ ^…â¶ÿ/¦Þ’hÐà’2K(?«|´byn,Q>÷e¯Ë)gëNÕT€Ùk1åÁDRUMq›®Í~“LûÔ†i3›Òÿ|>ì`þØ\÷15Ê`öºš¸ç¨³1åVÙë·4×gT”iz Ñ"#?ýÅôï3z‘Á×öTÕ¤v± 4‹÷¤ÜsšWåè¤LÛcßþI¹?õ©_f‡–ó—¤9gªOvù×ï‰LŠAžš>¿b~Ok,Ñ^×às’éNû&Æ˕˚:V© žE»‹æ½*Êö æÍåàz3›º^ø K¼’ê§ö©ª›Ü_PlêTeÑt'_îå Ó¯F1Ku)íû4Ù†bß4‡·,ׯk)~‡ª3Ç$Ó>gHu‡à –ùæ¤ìš'/È÷yu'õ7vUÀè,XHùû­ ~ç~ö­(×lÜ/™þìõ˼1)»OÍtUÀ (wÇPeÛŠ²m‚ys9ø}úÛ2kPéȆéë€l¹¼ATmïOèS¦*x í²_2;'寠6”WGtXfA€ëS„¼¯åòæQ˜5=p…êà¢Û‚aFð6ú53—'e_Ó0íÉ´ƒ Ve:€·Ñ-8;êàUL6™nòYÊßõ—ôþgͰó嚺ÊFϦ]J}kÔV¯ùšŽiðÚò šqç˶ æ=@ˆlÖ¶çiìµ9áKSu.qâø qÑTåáDŸK—7¨mŸdCô[’÷Câ Ô(¼9yŸÝ(¶qtîÿó(ö_Òd‚¸Ø»±Å´gQîïbû–Ë™‹^A1H°xõˆê’wÑT²Ÿß$ïï&žê×õi’™ Óv»n#ý»Pÿ!=ö´Í wK(wê, Øùùb¢‰y[Çäþ¿&õ¯ÔµD?vUýxå-#šãç}‡vAãÓ)þÆóh~úž®³·¶¨_æXŠ} زD¿¸Wõ*n"ÓéVæc5Äy½ß±-³˜ÉAW2]úk„x˜ô‘ŽeºXF¹/²Ý˜Ï»‰€G[çQÜ϶&âý<3yÿõ˦WP|øp#íݘ^—|¶?ÑuKÇQìrœ¼ŽþAÍÌo“÷MǘÙtý4í²ÀÇÅ‹‰@~?é5ߢK»ú”› ü[7]ó=‡b¦õ1L0ÓÏUD™.×| }wé÷™Îl»Ù´íK}ÔL¹˜`{¢ˆºN—· .ä.&žN5õG‘]”>•jÓ´o:lJñ‰ðÙ´ËèÉüh.yX‡²m¡XAùéd›æ–sÕ““÷_§~Š™Ô¶ƒþôw9íoÓ²MÛAڴꘖËÈü>÷ÿМ™ÕEš‘»:õÙ#ç%ïßJ·¾¯ÆÕ—h?¸ÁC(fïþ˜É‘÷ÚÈž¸gÚ£î¦}p-Ý.ÛÛ–QÞê¶é5(fÈ\Aûfˆ€Ã…¹÷]ŽÕ]5]öQ•ý y¿)íšg>Bÿë©J·Ù®#ÊŸG ÒÕç“÷/ê3ýŠ-+® x\ŸIOLÞƒn¿Ó‰þ©óþ­jÂKˆAÆU—AɺcÒ‡~Ÿ¡ºÎQ[Dô3>› zÍ·‚öÛþ¨®ùv ¾¯ÈÔtm»S•^?>™î”¤±aP3íJ"°·5Ñ‘r]t[©î¿¦y°´ÙÑ ½2£pÅæ¾¿`ùZ›& ƒHo@×®œjî[ò{Û'ã¢K0¡_Ùµ*§ŠóD>X²ˆâ¨’mäŸlϧ¿zMûÆDS‘t4꺋À¯uÏlAd¾‚v}««/v˜6ÍTëzŒZD1`8ǨanÓuǶG09š/D¶BÛLíŒÇêñw!å‘é»4Ûì²ou±>Ñçês)7SìzûeÊÇÀ6¾EqtÌgR€Øg6ɽϺ˜ikS>oÿd€ùü8y߯ {ÞÉDfû\Ðö:ʦ<Š8‡vÉ€ž ß¡¸mÏ%3uÍ—ïÇôºšœ¿æ›Gti1l]¶Ý©ú Ŭé…Dóåп+ iìLµó^iP7}/|‚¸‰ÕM_Aôö¥:S%P ÍP™Ii]î¦{1ùº¯?µêÔº'y¿²>xÅcà º_äŒZ]“úAÊÖm›S¼À¹šîÍÔÒ¾‚ºlÛ#núö$šj5ìV¯ùüF¢Së¯1ù]7"2’ßE4ë<žån6é² ¤Ç¨t?Få—7Ǩ©lÓmméz¸îë!ìðX=ž&ˆ@m¾•@—¦[SÙ3óˆãÖ~D¼{êÔ¿ê ZÇÅD3Ö¬͵‰Œ–ãk¦O³]¾1àr§j;ÊÇÿ3˜ÏÙÉû¦Á©Ré~9›µ½€èÓú³À+sŸ=¸÷ùÙÄ9ôë[²ŒÂ\ú}R3qÍ· Åk¶«èÞD~“äýL\+L÷ùñ%ÀL~—Ĉӯ"²°cr¤fi¬Ô¨-!2s¾J4ëxåÑw žrW5ÑH m›ÂM‡´éáûi?2g³=¦Wú{ÝJ1CL!}º¹3å›§®ú5ZD4KÛ᫾It¨~$ŧÅ[AÀwýÊ|—¹w#‘nó]©2[Qézxå~Áº˜­ëaeÓ‡Õý@?Û*<9z}â;/¢:8bðb˜QH÷ÝÅ pJû×\—87̵cþ°½šènáÕ[¼ìÑ{Fœ;"Î¥CfŸôšoG¦ÿšo6¸ˆÈvýÅ®$Ö^Ö{]Dt¯p,íúe—F§É'?!y å‹°'Q=rlz18Ê´ÿa§×õ“¨á¨ ª¬ÍèÒ]5õSø,¢éÞqD³ú|ðo9Ñã‹Ä O¦û ÛqDÖ ”›ÎÍ#š5}„忘[çIQÁõ°òHmGÔÆÄ1æ"˜ÿn~|”èøªAø©8ÈXÉ<šê`åžÉç£üÊûî íªÊMǹn®YAôçýbP¹ÔjÄû_÷þ>ÌA¿43fúšo6ù#‘QþQÊÝK@$¬F$£|’n£³K3Æ @›åD³àóˆ‹äüÆC(^¬B9|”}y¥u9—© (qÁʪ¿¥ÉûÙÜÜtJo Q}áßEU¦î|àŠÍ‹ úy98‘È(H;ˤÿ«K‰þ·¶þ½÷JØø?"óå™ÌìÐôu6S |§£ðÍéz¸ˆò` ]\;…²š^iŸzÓùpO"û)ÈãL"ûøG”»hêãx&|žÉ‡«óçK¦IGÿUó_(Ÿ=oWoö_{§Ûû^DÓÈçQv<„h|1è–Ù€³CºÜÆ`Íìó.Ÿbùqrðv"x±ý§-×Ö$º¶zÑz-DD)€W?¾Mô˜Ù¿bº´éGÛ‘¦¦Cšîý%àGQµ’n;ÓÕ×l—n×70=~¿Ÿbðo9q#ú ¦/;ór¢Ûí-ÿÉ<<†¸q߯W§Ù,ý-? |o±t=|xÇ(*¢i—f³LWÿd[?¤Ø÷Õ߈¦’ã(ÿ6q’];UŸšûÿÙÀÅ3P¯:é¾{/âxÝ5¸”žï—7Z©•Øz¯7Õ^ìšûû<"[cg‹t»‰ñèe,>×{=˜Pîų̷!²awþ9³Õ“êÍ¥¦Mš{¾›¼OŸªC#ò8Muiã_Éûû¤j+Tf-Ê#2ª¼]oÍð³%ïOô¿—Y<­÷ÙL4Í^ü”¸Ñ}8å§Ý icœ¬¥É+ë1*ݦÝïç¦õ)8žöý6,Ÿ üû‘5ÎÁ?ˆ¦yù~ÿHôù•ÙžhÖ–eó_(Ÿ·R¬_[;&ï¯g°lr…;‰¾ºw'‚ÈiVô[€]fºRS0Îçñév=Å€úVDÓnÕû 1Èý‰>íó6þgÆk$50¨q–Þ¬. ˜™åõÝ]‡ì§%ï=’Z¨­s»’Ï9ŠŠŒ¹[ósïW%F妗QÜ·?Kd݈€ß÷“Ïë€iSòa÷/7L¿OÞ¯¬Ç¨t=ì×CsÑ>+K‰ý{Ø6¤8HÆ=Dd¶ô{õùäýsrÿRîÿ+ˆîFéÊAÀ‡ 0Ÿ´LÚ½Œ3Aô¡û¢ÝÌâÝn#®3 ‰¾‘ÕßUÀ ˆ€wÞÓ)Œ,Œ¼šNk1µmlÓäý5”›yü:ù,ë€x~O1 ´#Ŧ³UÕSñ¹ðtt å@À!£¨È,ð‹äý°÷±Ý’÷ßòü»Z ü'Å&¿;71©Û“÷].òfú|rò~`³®Ã888Ÿd6fîtVŸ¯«¶Ù•ÅÁÉû?RÝqûTíL1+úT¦ÖÿïL;Ÿ¨sæ¹¹ÿ瀧WÏHêMÙ•yÏë8Õ(l~6pTåZàmÉgu-tÆá˜5›Îã3aº¯ùF%ÝÖ¦ë^æ0ÊAÔ4ëX™¹xÐÒxX…È 9X}Ày<1y_Õ íuÀéÉgod4éê÷ä½wõ¶;)^çÊ“¬´™ù£¨mze÷Íäý ûqþiGøiÓþQ¸ŽbSÑ…DÇΩ´®[QÎT®²6ðÁÁª6°«(f*¯Nù&me0Ay›~í~·q—’“>H[YlGd]ä¥M³†e‹ä}Ú‡ÖlÏÜ‘èÓj]ŠYñ£nþ›I÷ÝéÖýËK(fxÝEôߨáúkò¾nDÔq8f¥çñ-hZøÈð«3ré>ö|æF—!é éÚÖ&(oÿëLÓ²¤Î jºEtžÿLà—D?2]ìEùé}zBʤ'ßûû·µe‡iû9”⦧›³Ù ÊMnºþžãêhÊ™GQ¡šZ—•ã¤~:±gV%šù «/À´)öÖ-ʬB4^£ß„=kûc[kS@`å,(>å…È&KG„K­N <±G‡ú ˇ“÷¯aåìàû0àîÜû}ˆ‘*g»ôx6WŽÕçö6³ç>Þ3ì õ¤7•mŽ_×9¯r]õmŠƒ`<žÉcüÒÞ4ãàûÀ9¹÷ ˆÛÛœ¶¦| <Šñxè4îv¦œ­ßd›ä}Ý ãÐ7mz_›þ]¬B\ÍŇÆg#8g²ï:ÛûœÊ¶ö\ªG¯³Mò¾nûßø"Ñ¢íHVÞ‡všA5!FBÊìMŒ†÷!Úõ«ñxàÇŸ¾ý…úÑ*L¹ÿ–·#{65%X@Œ\v1Ã;ŸO•ò>GŒÕÆý—©.Ô^½h$µ¾»‰Ñgóv".|Ò¬´ÔÈÓçªwPìïn/b´î6£'¯FìkuûÿEÉû~nlC4QùÏËÎü‘¡{tC=òÞ@ñ†òç5ÓKy ’C©¿PÜ„hÂöØu˜'Q æÎ'2aŸÒ²üC)ö6[]C¹cîçŽ6™€[3>Á›¼™:V§Áða Tå­ÀqLŽX[e àXÊMº¥œi4,騸{ÑÜýÇjKÞïM» à¿ß ºAØ©Åô"”Ë\G9#Þ²Ï^Ú+“Ú>Ø–¤±±Ñœb¢âµ„8¼Šè[æÄSÅG¯%yi™Å¤U¶&.NÓ²g²ùŒ®õ‰ å9¹é.¤ú€»g2¿+[|ÿ5‰}Z—SˆfÍérV':ãÿ:±~–Мµ±E2ß®#×ý:)ߦo7T,ó]›DnCý —$åÛ\/NÊ´ ãº¤\¿~çô÷º…¸Êº ±­Oô—­‹©v|U²ì~d™û%åºÜà¾>)ûÊLÙ¡oîS1ýD@%û~‡ÕÌ÷ Šù~“rÆÏ6DÀööŠé›~ï%ÓÜLl¿U͘×ÞÍäï›ýÆûÔÔ"¨˜ÖãTâ·ñDw/"À”?N}’x*œ½O/N«Ü˜,g‘7M–›}ÇïûQ¾¹[x2ñ°e±þš¿{'óþGô©w$e¿Ü¡ìÏì3ýBb”Öô·ûüH”Z•8v}‰˜¬ ~צù§óÞ¸õ·‰î|Ù×¶(ó‚Še~’ÈVÎlAÓ¯ZQ¯~ø“*ÊL÷C‰‡0Ù úÄ~~~Åô¿¥ÿï+ÊuéꢤìµÄƒÐü¾´ 1Òø©5ßk‚È nòídúWw¨c?;5ÔëC\N^ÕvÑ6hðÁвÛAþšoK¢Sþ[“i—Á¦~þ+)—v2LPþNý²®ÒíéÙ–÷*Ú}·ô÷wbû® mIy;½žâ1(o ŠçÛ â!YþÚkÃuƒ‚ IDAT-àYTïÇïOÊY³œ~>CyÝÿØg7'‚’%ºö¹>7Íç(îÿ—µXÖåÉrÚd³mR®Ë@Cÿ”mÓ×rZf‚È–} Õ×|Ûj®ìMÛoôÛô÷ß-ê”yFR6íªÊÚD‹“|¹3‰ûÐÌêÄožÝßìD\[gÓ/%²†÷¢ü°p‘)˜Þ¾³¦>é¶;Èz¤±1Ÿh†»„ú Ê6¯%”;k®³/å›ãüëfÊ€ù“hÕ þ @ˆcUp‚è+ð"{ì’ÞûtšºæÎ0šàFÄú«ú.—2y²» ¦ü8!.LEõï•m×Q¾HÍ^ý.rú™-ÀyL6s¯Z×Ùºç‰U뱪yÜÂ^™ªyþ‹Ö_[ñ·£ˆ Ì~¿÷Ë)_ôe¯K‰¨¤;+¦9¼ÏzÙžêý¸éu8±>ÏÊ}6S@ˆõ”]¤§¯»ˆ‡"$nT–VLÓ”53[€7ui¹ìµ„¸™;›¸Á[\1Í) óEpMª×¥ÄïݸÞÀÔZ|¨b×RŸ½;ÕàÍÄ 6U¿SÕ>›½Î¥Ý@7S \³ü»‰óâ…”·Ÿ«ˆ‡fùóÊ(€P¿‹éëêb*ÀÀ*Êg¯©X´”þ™æ™•=¸8×Ö#Ï#²ÊLœÿ—%Ó,§ÿ±øë5ó¿Š8žeûÈC*Ê+¸õ× u¯/ÇÑSrŸ]ÖbY—'ó× D ½îšïZš¯ùî¦Ü6o¦€GÔ|—k€+˜Ü~×›~oꯕþœA\?žBõ:8ƒú¾ð?Y3ߺo’4+<ˆHûïrBÍ^н¬ûOsº,ç¯D‡×U B\0•úgÝëÔgÒÁh€'Û6Ý´Ctÿ DvÆáT;ê^·7S5[€™ƒ¨¾ÐizÝNdÖÕ5ÑÚŽa²Í¼®fò†â%Éßê~ïPŸQT÷Z|Šv’—Ón_¿‡âM{>[z&€‘Ÿµ¨súú%°{Ã|gSâûHÊ7®ý^ÐÜtz@ˆÑ›‚b]Uv¢z}}®fú©O'>‡ÒþýkÚ“¦„Š·=®Ÿ«[þ¸7ê`Ué C^FÞT€™7Ðn{Ï^WÑ­ßÓ•=˜yìërŒ¼rÞU6#‚äýæ—6+†á!»hsXÚ«K–öÜß.k±œË“ùs"«-ŸõØö·/ÍÍòG\øS‹ú,)s$Ýôþ‚æóÜckÊý¹åw‘¤±¶'ñ1 Τ¯åDFÌ éÖÙjÞ<âè4êOä+ˆÀ i¾¹ßÈ<È^mnÐS{? ùÉâbâééãèßÿÔfIºöËóä|Û›dˆ¦®M€ß7õÇd™m.vÒ2{·¬ã…I¹.£BŠŽ§>[t‚ØŽ£ÛÍ|“s)Ö¹mGÛÛ&å®î°ÌW&e¿Ô¡,DÖ䇈‹Ý¦}úïD“6}nD45ª 4_Adç›fîG·ß{Oâ÷mÊ^Jt4ßµoÐ'Ñ|s åÀÙ§su?þ.¡ø}wìXÇ*%‚zMþÛ‰›œ~Ý1@¬ã|»\̾>){D‡²?KÊ>¹CYˆ`ê7‰À~ÝzXBd¢=þá…I}n¦[pãè¤lÛŒ%ˆL™|viÕ¾4Õ~(_K9W· ¯Jy]ôëKê[¹iÊ}¾3±Öm¯ç}6uÕ9ËrÏ¿º!®=ÒæÀùmçGDk…¼üv{ÍŽMêø²êØduÊÍ×ÒÑ”‡©j»hÓGkjKb©ê &{]Bœ‹ºö¯õÒ¤~ÇP¿¶Ö§¼>ú’LÿÔËëúÝæÛøO©ÎˆÎ^·ýînÛ¡.›ÍëZZÜJtÝ‘z{òêºiëê[$L]¥çÁä–ýý%©sÛd‡û$å®mY¢/ò|Ùc:”…hVÿú_ó]HtýÒ¦¯Àô×¶¿t€KÊÖõ1Ye]âxQwx‘—ڊؾ.­)—½²ûØ6ç —÷–‹råÿÞá»Hu¹8’†aqú3q"[‹¸(¾‰x*{Ñ÷Ú°lH4íݲ÷ÿÛˆæiÔÈ4]ÖèÕe+¢?‘ â‰Ú½úty’7j»Yj›k—¿Ýe#¬Ó°­B·%‚®Yàï|"8Ù5ór.Û‘ZnL\$ÞJ4ø±}wµlÞ†Øo®$nªÏdxë}qѽ=qã±qy‘QQ5âoóˆŒâ‡öæ;AfO#.ÇÙ:DpâÞÄ67(½„Ø¿—ÖSV%öýmˆcÜB"ãõJâæoº“˜;\²Á&®$ö£ ‡4ÿûý nD¬—3ˆLÌ™°‘-â7º†86Ÿ?C˯3ŸÀîA¬—EÄqð÷Œÿh³ ˆcw„[Ää±`6X@ì»ÛDZ»‰ïógâ᥆g-b]ߛɖ)7ëù,?_l <†ØîÖ&ö™?ûö²©U¹“³².®!Îãé e+£3ó×|×ÇÿA®ùFeCb[»7qý“pϤ8J• 7'2³ûËßÛJWí-—Þ<ÒE’$I’$ Í£(f²=ÚêHÒJá&»Ÿq]$I’$IsÜW)Û4ù—$MÍ)Lw§Ú=‡$I’$Iµ¶¢Ø¯â™Í“K’†àåLwÏ£ÝÀs’$I’$u6Ø!Ÿý÷‚‘ÖH’æ¾C˜øf9°ÿh«#I’$IšËÞG1øw1Ž$iøîœ@ñ¸ûæ‘ÖH’$I’4§ì ÜóÀ7)Þ„.Yí$iî{“ÇÜeÀëF[I’$IÒ\sÅ€_úzûèª&I+…»¿q]$I’$IsÌvÔþ–c4Iš){à€’$I’¤ipåÀß2àdà¡#¬—$IšóF]I’$I3â>ÀNÀÚÀMÀ¹Àõ#­‘$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’$I’ú™7ê H’¤¡Ù ¸wîý­À¥C˜ïêÀι÷Ës†0_ΆÀ6¹÷·¦*’$I’¤q³ °Õ¨+!©Ò[€‰Üë{CšïNÉ|onYn°-p¯!ÕCÃó"Š¿éÏG[±´ p?`ÍQWD’$iªæº’fu“€WgPÌ4’¤¼}Ë€Ãðåf§×Y‘7omu$I’$if|’b¶ÈpâHk$)5.€ó€R>f’$I#å ’4÷¬ÇwI³Û#./I’4§xƒ(Is7¾+¯¥£®€4$%I’†È&À…U€}€û›7×§wŒ°^]=ØØ¸¸†w˜Í…¶vîMô?tðgàâ!.#³ ñ»¬\ü–Åsºm ì l,!:]ÿ±]4™ìI 4p/b½_HüˇT·]€æêv-1ò•CšÞjÀÞÄ` k·—ëbqÇy »ÿ¿€=ró½%ólÆ·_¬{66&š­_GŒBûÏiZæƒÝ€M‰ßì:à7À¿†4ÿm‰í}Kb`‚ë€Ó(~ŸÙt Ǿ­€Õ‰íûbôìaK†e>qÜÛž8ÞDœ{N%ÎC£¬×îÀý‰}nY¯^ç0øydªÇ±qíõAĺʎÿ"Ž× aÞ2¹-¯Ñ›÷߉ëûà”$IÒŒÙ8’¸a™¨x-&F+Ý©Ï|Ž%n(²×—Z.ðäìÔLû—Ü4¯Ï}¾ ðßÀy5ßaIo»¶¬S•ÄhƒªYÆDoù/¥}ïÁ¿÷ª¹¿íüºbik•d—ÁÂ&Gç¦=&ùÛnÄï½¼bÙK€£€ujæûl"Zµn®'~×y}êVgUàuÀ5óŸþ<«Ã<_Çäzøsò·µC‰~Òª–uð"0Qg-à[D§õg7Þé<Òß.{íQ3Ï5€·29 fÕë_À§­û|ÿ™²9ðn&“Uu^A‰Ña¾¤¸Îšüý`Š£.ç_ˉÁõ´^«¾ÓràWÀÃ{Ó”ü}”£?ƒâz;¶bšƒ’i~:`ý>–Ìç…-Êì üˆ8ÞÔK>AÿãܰlBñ;<%÷·{ï'ŽËuljã‰km càMÉàUÝqâoÀk(žwªl|‡ØžÿLbóó¹…úãØÆÄ9åDà"ð˜^kÜÔP¾j°‘×$ÓÚ§þ;&Ó?<ùûó¨?¯¬~Ü·Ï2ê<š8þWO'ˆJuß={=lÀeK’$I¯'nPênò¯¥Dà¡ÎŽD6O¾ÌãZÔá™I™K¨ª,ÊMwLï³ý‰'ém¿Ã;[Ô)µ3qãÒfÀ´ëèü?“r[ô>õ§o&óXµbš~'å¦ýKï³ùÄï[wÓý‘bp "¨ØfÝ|ºOݪìM}`±êu|ëçý¹2+˜Üîö&¶Ã¶Ëª»^¿CÓWU ì~ÀùæqqsÜïºl |™òq¡éµxUËùï‘”}vïóM€ï·\Þ5ÄzíbàǾÏg{uË>Êà!I™ŸUL³ q¼ÌO·}Ǻ-$‚uYùåL㪬 |ú qúºžnãAm‘,÷m½Ï&²ÛÔõvàù-—7ÕॶûÜ%Ä1¯ÎC;Ì+}m<xoMûÝ@62SûQŒ»• žCÜt§ÿý3TÒàƒ‰fH‹¨¯ûë’yL5x=q£þµ†eV½ŽéÍk#" Ø¥l¨iãÀ]ó¸‰ÈFù+Õ7»§Íw›ä€DÀê%”ý^uAåa7 :ëïŸDfÍo(ïKÙë¥}ÖÃtxͿŽWÕßVÐ|ȤÀ×ÍïjXnÕëtÚg¦nFlsuóª;ö¤ÇˆqB9ÈÙtì¯ò˜¤üÉ ÓnFõqd ô>¸ºâïwм†4x‘áÞå÷Ÿ ÎUÏh±¼A€ó€Ã–õxSŸ==×€o$ö›+:ÖåLÚe÷/ 2¿Óò—ôêú1âA^ÕuCÕË $I’¦ä”oLޤÜÌe#âb{q2mÚ„&³å ¥÷4Ôã5É´ßîSï¦ÀعÀ+‰¦Vó‰l’}‰æméMÙrÊÍ«lO9¸tð$Š}u.ß«C~ÚcúÌ? >žjeï¯Þ<8X_Hæ1Õ`ÖÄ){'Ñtloâæmà]DKz£»/‘A˜}v5qsµk¯ì>T¯ÿKhpÙ‹ò ëlþFlUâæõ²dÚ~MÃÒàw“ºžÜ›ïND“º½ˆ¬Ž4è|;±½¥V'‚ÍÙë'I¹s“¿ç_[&óútRöjà€Šen |„ÉÌÞ3x³ë©x1ÅúÞ|† åã÷!~§{’é/&4I€¿¢xS} ðêÞt›Yʯ£˜•–½žÜâ;-$šü¦e¯ öÍ­˜<ö<²÷}«‚×ÌŽ`Úl9m&ßÏç’ò/©™n"k:?íÄ:M¢ìHœ'òÓþƒêýoXÒ`úú‘ ¸yoúõ‰vº¿Oç°Íi6hðÍËû'Ñ=Æf½iæëðã”ðK¨>·oNñØô‡¤Üo¨?Ž­E\Wä?K»êøICùªc×T€¿¦Ø ù½u”?Nü7Ñ•Bº>ŸÞgYôê/³xåàá½zuÏOû-⸞­ßb™’$IR¥ý)öG³˜è˪Éc)bšn÷¥D¹‹êþs֥ؗÐíÄ t“ªà]D¶QÓà9O¥œÕuzŸe­J1—]œ7e•Ý‹òÍQSvJÌgA»›Ú©ó¯?Pß'ÚÃ)ãòïÖ¬)›Úú­ˆí㲤ÌÿQ’Ù”bŸoK‰þ«êTÕk‚ÈølÚ'^VQæà>ßÊ7†ßmQâ7Îg¡.'ö³&[}µiŠ>æûÏmD“ɺ~#3O¤ÜOÖSK”€Ùkà¨;&ì@dðæË|½Ï² z{9‘ØïëlNô?š–› À5)?iÛ x!ÅÊbê3¢?‘,ã""Ýä“I™¶¬× ê€7пYï*ÊÝ§Ì À½)ŸãN¢9 }'Ê}^ѧ ”ƒVoQ¿¼´Ùí[:–Ÿj0œø$õîG9Kï[}–µå‡eý‚†ùõ¹”h I’$MÙ<ʃX¼¨eÙ%åöi˜ö¨dÚVLó‘dš7µ¨C¼ xH»êWfG4 òòdÚßÒ®/µí(fý|¥aÚ4˜½o±œÌ°€ß¢ÿ÷«kööú¦B½ù¦Í÷ú5'|o2ýwh—ɶ'Å@RÓÍiU@çRÚõ wvRî³-Ê Ü%)w^Ër£ö`bÔß¶¾Iñ{~µÏôUÀ{€ç´XVº}õxSÊÙ|¿¦Ý1a‘ œ/;€P®wÛfÀióßïÔL· Å 7Ónÿ[Hq¼Žéëë²*øgÚì‘7›‚¡0Xð´¤ÌYD߬ýìDd}çË6eíÃÜ.a²/À&ïLÊõ9ü%ÉômÏYb ý„e$I’¤¾þî§™(öéuDôëQñôÀÜßïMñfúoôoîåàîê¿*匲ÕL»åoîßaYÇåÊ.¦>k°*ئécÞ0€ßo¹Ìª¾œÚÞ¼}6)×”qu/Š™‹ˆA Ú:9Wöê†éÒàßi¤ /½1ü]‹2ƒ÷IÊ5õ¥6›=‹ò¾Ð$ ÞC»‡ FN·å ¦O€ÜAÿ,µ¼·$ågKð€dÚ¶Í€Óæ¿Ïl9Ý«[ÎÊM”Ûô9ˆ4¸‚vƒ e6£d{qÃô]€&Ó·íâ"óö¤ü4gŸÏöàâ\ÖÆŽIÙ šÏEÇ%Ó¾¢år¾—+sõÙô’$ijÓɰ4ˆC’÷éPöF¢ÿ«LSÎ[‰ùó>Éd“¼÷1™0üÑô¥«;;L»øFòY]çÚ¤ØlùD¢ii[ùŒÇÕÝ:”ýƒ­‹©ø^Ëeþµâ³~M¢2IÞ7õƒõŠý}™è·­­äþo¢9l¿"×m¤ëb³Ê©†ãºäýÃè–Y7[\¼ßŽþ¹äÝ@s0+]VºÍ7ý†i¦ô±Ä…¹îŠûÄ®ôo¼€b³Ç[‰¾(S«Pl:3ýƒ9y?"‚q™º¾i§C—sÏu”ƒxÃØáÉû߀mAñûlHûÙlt3Ñï`>óšŽéy­ßCŒªéÖ ¹ë I’4ÇÔtX<:÷þzb¤Ô.ÎÉýÿ4÷»÷-аðïL1ûáØê1¨_&ïDu³±tp…ºækuÎIÞ·ÍT¼ˆw\¥¨.Ò¦SM}ÂMuý§ÁÆ.™¢m¥ë¢_wSq)1¸Nf-¢ãý¦q™£fkΣdƒZF<ÔÈ«ëËï~”û'ýòÐk4ž–SÎÖí7Š÷~³¤¾K9ˆÈÎï7? Ô´u;±od¦c?–ôÜ³Çæ;òñ²M–y·S|h1 •bû¿!ù¬©ÏÏ4“¾íütºéÔF’$€š»Plâö'ŠÙmä/„ÒtºW7™÷_dr ‡›éÞüg*þž¼_êJ™¼ï’Må†[–»€¸áWËèvƒž—fÌ4evå×ÿ b[íbÐõßÅ]Éû.™jƒx‘-›ÙŽEøóD3ÐÙfcb4íwƒ•üžêl™¦›í©jû¦ý.¢ûˆ¸³YÚé³úLŸëj¤ÇÙ³[×hR~_Õ€7m¤çžεÞ})w[pÚóù}ò~¿j37u9Ö§1Úv)‘N×%ã]’$ÍrMYUÒ Ò¦);н³é´é×ú”ƒ-yWïbrP‹µˆ¬Ì;ú”¶k+>«Ê0J/Æ?N1øÒOzc7]YL£Ð5hœé²þòÛêRb ‚.Òþ“¦cýºõC¢ßÁCsŸ­B Vór"xr41pÆ¢®[ ˆ@Ú3‰æ…mz€é} Öö7LëPv.8‡6ïÒ{¿q.¨ ئͯ!K©’ž“Õ±nù¦’U¤ÞD ÔÆÄ`QÓ!=÷, ‚Û·Nq¾é:\F¹)}KÞ· \­ ºìë¢82ôþô¿ÎšOq»_DŒÎ,I’V5ÒìˆûRýÖE›QYÿøÊYJ¾0Ååwµœ”#?:âºÓ¥ëª_ÆK?fõ¶·Ñobf5ú79ìg®¬ÿ}QAùÆÞëãDÀOMÊGmUà߉ ƦA3n#¾[—Á fJ:¨ÎM#©Åh}øhîý³¨îCv?ŠÍ¿A}VszœmY¾ªóÑÃg´,ÿW¦/xGÅgë2õ`º1Xp:.gÊNDFÌQƒÄ€C‰_¶$‚!m3µfZzì\<’ZŒÖ×(¯ë‚òéÃ’¦>M‡}Nç`UÕ(ëUçž®Òãå ÛfUöC×ÝõÀÿæÞ¯|›úîR™{¿œb ]’$­ÌÔtHŸðŸJŒ8;éÀ©ùÄÍÕÍÏ¿ÇQßÕ{í¼x.Å`À¢ÍO£ú:íFcòAžÏ_þ9Ãõ™ŠÛ“÷+c`ä*btÙýzïw#šq_’›fÅl» ?6Ì3='}‚òh¹]¤óƒÈÈ:³eùé쎢ª‚ad’¦ßyÐ>3«Ê­Œ™®Ãðn¢ˬ»“‡S}˜Ø¾o%Z_Lô“œïSðƒ”›cK’¤9Π¦C:âåªÀÉӼ̗{çÞ‡è "Àóy"@1S5UMžÓõ’}–^Fó¬†g ‘É“5Í^HðªúoTtøðz࿉&Œù‘UGúgr°5‰}=üû)Ñ®jwifÙ861¾Bqpˆ§A»Ì>›ÿöÑ< ¸ÝÆðÏI'y~ƒJÏ=w0œ |º?­C¨«š7I»XÜ3h¥VrK€'«ìØûl+às e&€ÿaêe%IÒ,d`M‡4 mg¦wäÒÍ(6eù#ðŠ7dÛïÆ:¤Ò&†7Q‰”®«Ý¦§:ªáúïn‘=²ð›äo¯¡ÜÝt:ˆb“ß?Á¢Ùüƒò1bgÚõ:×|›bÐê)Éßóï'èL÷óݬ×l°Wòþ¯Cšï?(6Íž;8eˆË8’b†íVÀ«;”ÿTòþL¢ÿº:éql‹ËFùq¶ Ïœ:ªŠH’¤Ùà¦Ë¥”33ާ[s¶ôæ>õŠýAD-´IDATÔò–7Ðù,»÷Ï]N±Óù:¯^|ö9Ê£{æ}ˆrà‡iœ¬ÑrZMPÌ…P¦ËMíBb ›q’6‰¼?í2óV'2h_E»íoål׺‘%ßLŒðúë^ýŽh¹Œ&×'ï٢̓i:ŒÚ—’÷ëÍüúeZ­|xô4Ô "Ø6“–'äÞHd€å›ÿþ’þ#Äg¾\“{¿9ð-ºe•õ;' Û<à‹-—û ÊýÿFswéoÚo¼øBòÙ‰ÌÃ~ÞI¹ùozìM¥}7îM£ÚJËïÏܹîMƒ™ÛŒ¢’$IRf#àJâ${ÝNÉššán@4g¹‰èϦʃˆÄl¾ÿb²À*_Nêñšov%ÓO}ž½Šê¾£V#2Ç–'e.¤ÝÍ[v£–lÝPf‘UyðÉ>óÿÏdÞßkQ§Ôªuì×\ö¤dúwXÞâ¤ì¶Í“ÿ&åÎmQæk”¿ÛQûK-$›¹€è_²Éû“yÙØ·O¹ˆ&É£}–é ¶ ú7¼x.ÕûÖzÄõ´ÌÏéÐ; )ó—u¼W¯>éyýe5Ó¯A<@Kë÷ÕËz@E¹.݉ì[QþùʧǷÏ÷™~×dúë:, ¢ €|ùÇ4L»S2íÅÔ$I’€éï M+·‰›–_kõ>[›¬¼øÑ/ÍMÄM½‰N¬ÇdÐìÃÀ3“ùÎïÍ#ŸóNš#o&2ë²€Î#ÿ ²óÚÚ”ÈZú9¸™¼/‘•²A2ýD€£©IdæíÄ%ûç>{6ÑÜígDÖÔ5Dó«‰À̘lJù@¢? +:|MzqCõà䳃‰ æ©ÀµÄï½ Ñ¡ú㙼áz‘aÔ&87î ö¯gå>{5Lü!±oÞ—ÖûlO"[6óhbÿ<"]HdÜ-ìÍç”Yo¥8Rhæ!T#^F±¯·®Ž^ÇdPfsbÔÓoY~ÿê}ö0¢ ivZD ^2®l¼’~åì%‚|'¿ËDÀèÑ›þ™éÍúÖÞ2³u¸€ÈLü-qŽ ÁÓ‰ïœç>Ædl1åæÒýü‚x¸thïðb_ÿp躄ٛÉí÷EÄv7“v ¶éOßáRb=ìHd´¦™®%ú~›è3ßtÔéýÅC}ߊ·ç¦S™Ìž\›È |‘…z9q~Þxåsã߈óo?§¼=ÿØ;™ØïOœ _Dœ“óN#Bæ¤K»Ktòbà”hŸM:.è½v콿‘Y}q­pKE™»‰ýæzbÝž^3$I’4°]‰ÀTUfC¿×åÀºÉü^™Ls6íšõ¼0)w+t¬’f~šÈxi[ïE´k•·qC=ÈzZJÜÕ1°¿{°AÖÿb⦲ÎLgB îjQ÷ÃsežMäYù Jê)5eºfÈTùpÇzþ~¾3ù|œ2!‚“×·üNÙëx¢‰dþø5¬ @ˆþJ«–»cÍô‡$Ó 2iºïd¯¶}ÿUy)1@Ä ÛùTÖý¤€7Ñýœpí’Z@ ÓyÜNÿìÁ]ˆós×õ÷sš³õS¦œµYõzMMùg·¬×ó*ÊŽs Äq©ªµBÛ×R"`û Žõ”$I’m@d/ÜM» Ó+‰f•i?›lº”G¬3ÈžÈ/çû5Ó¦Õ›YD§·¨ûÉtïc0ïY”u¯%DSªºf€íÌ'2g®¦Ýú¿‹ÈFmjª £ Bd”ÞXS÷ìõפ̽úÞÒ§\þ&÷à>õXHd±¦eW0õLôyÀûè ¿™hŸe ¾ ùû¸!•?¢ÝoðÜ\¹¿æþ6Ìàéœ=§fúC’é îP±¼ Š}âA”OM¯Ó˜‰xºT!èi³Ûª×Wè>°ÉÔÌë¾-ÊnHt-îõ/bÿd‘í×4ÿÊ¿ºE«‚{ã„\¶Ý†ë^‹‰LMI’4Ç9Ò¨fÚ–ÄÍ̈î͉æ¿77¿%nO¢8Špf7ࡹ÷7ßé°ü­)gj}“òh‹(v¿%Ñ\jÑDùyÄMüDPóê^Ý¿M4žªUzËyÑ,z3¢éïÀ DPëÄMOÚŒ«ÊŽû¹º‚î7ãó)÷ótÅQ!SO Ø/ÑoˆÙ6^J±¿­¯Ó<˜JæÞD“®Ì-Dgÿ]¬Iô¯øx"ð» ±þYYç0¹þol1¿=ˆ&Þ™óßµ¬Ëz,Kˆ¬ ¶6"²cžH4¥\Ÿ"žGü?ïý›Z›hvúDbŸÛ”XwûêÄH­ß&‚BýÌ'¶é˜­ôºuêßäþD€á1D‡ø ‰ýòBbÿ.ÅígKŠ#“Ÿ@}pucàé¹÷wÒ®³Ìs)f2ÿȼjk_âÁÀþÄ1s}¢™ß…DSÍzuÊ<‰ÉìæAöõ&ûׇ¿ÝuDß|Uy;ô¦Ï\MqÔó¶žÏdÓãÌ1”G ÄD`/Ûn6f2øv9ñ@ç‡À‡°¬~¶ x<¿ÉóÐB¢;Œçƒ}lFl¯WÇ¢oN¡Ž/&úÖ{±Ÿ^Iloç´,¿ ÑÄ>;¯oJd—ý“èWðûD¿–mŽßMËx ñ€gbЛˆ`÷)Ävõçš²çÀìø°%q|»¶W¿_õʧMˆw'¶Ì߉ó| )vY²˜ÈÊmë9³#Lóùýé½ù¯Eœ‡?E¶¡zp› {¯u*þ¶ˆØ_Ó•$I’¤9/Íl›}&©Ù–LîWŒ¸.Ò8I3›®hå¶“Ù¸KhŸU ÑgðÿR鿆[EI’$ivH€SiÒ+iR¾YèF\iœ¤ÀéXE³ßDf`¶¼gÀù|…âöÖ¥k I’4 µ8A’Íå¥aX)8sì¨*"ÍžwTå…D°83hà.í®dÕç#I’f €’¤™° Ñ_]6bìÏhߢ$)äûA^D»~h«¤ƒx]6à|$I’¤Y-mÜ4B¨¤f[ìçGÝf”’ÆPÚxÅh«£1•oº»‚è©«…G Ÿ FÆ–$I’V:¥áx,1Bm¶/Ýì9ÒIãÉ ÚxÅíäðŽåG%ó8iˆõ“$I’f€Òpl,#ö£³q_’êT;Ĥx¤Mz«ì œš”½žbŸ‚’$IÒJÅ 4 image/svg+xmlPyMeasure-0.9.0/docs/images/PyMeasure logo.png0000644000175000017500000002714713640137324021440 0ustar colincolin00000000000000‰PNG  IHDR\r¨fsBIT|dˆ pHYs ½ ½´½btEXtSoftwarewww.inkscape.org›î< IDATxœíÝw˜]eÀñï{ιuzï“™tRi ]AQAš  +‚®.ˆ]˲vwWt‘Õµ`YA”¢T !½g’LÊ$“éõ–sÞýã¦O»wæÜ¹÷Îü>Ïã2÷œ÷¼ïû;oA!„B!„B!„B!„B!„B!„B!„B!„B!„B‘B*UÖZ«Ý,Ÿ¯lçd­T=P¬ D+e¤*OBŒ;íôkÔAP{•¦!jKëÕMãõøqzÙtí¨[\”÷ó…H{Z¯@©_ùŒþ_”ª »“ù¨q ûô’Ûñ|GÇs¼ž+D;úëÕÆîŸ(u­ŒŒKØYþvm8ŠÇãyBL(Z-‰˜\;Uµßí¤“ÞÞÞm¿r§6œ¿"…_ˆÑQú<£WìÒKOw=i·<Önû•/iø·d>CˆI¤OÆyµêÌn%˜´°+úêåJé?2µ !&‘]CáVs )…s—~¹R)þ'Yé 1‰Õzµ~À­Ä’R@•c|t~2Òb²Óš÷6êe绑–ë`¯^6ÔÜNWq [}Wk=æ&¼ëÀv¸ ç"¹g쎾:æZ€«@k­êj7ÓB ÁÐc.k®€F–ÍÕPæfšBˆ¡¨wŒ5w›¶éúD!Äf5ë—sÆ’€«ÀPzš›é !†¥Bx¦Ž%wûdèOˆñÕ…c¹ÝåQåu7=!Äp´Òþ±Ü?afêmX½•î®ÞTgCˆŒ2!€ã8ÜõÑ{¸ï_žê¬‘Q&Dxø—O²æÍMüü?þ—m›v¥:;BdŒŒ=Ý}|ÿ«±µ‘p„o}ñÇ)Α™#ãÀ¿ýKšö4ùû_ÿ´„—ž{=…9"sdtؾy?ýÁoüü_>õ¢‘h r$DfÉèðí/ý„H82àç[7îä7<–‚ ‘Y26¼ø×Wyêчüü;_þ)ÍM-ã—!!2PF€h$Ê×ïúѰ×twöðƒ¯=8N9"3edøåOaë†#^÷»ÿ~œÕ+6$?CBd¨Œ -Ííüðÿ×µŽãðµO߇Ö:ɹ"3e\øÁW £­+îë__ºš¿üßóIÌ‘™+£ÀúU[øíƒJø¾{>ûôöô%!GBd¶Œ Zk¾ü©`ÛNÂ÷îmÜÏ 2_@ˆÉ.cÀ“|×_^5êûïÿî¯Ù½sÜN]"#X©Î@<úzûùÆ]ÿ>¦4úûB|ûK÷óŸ}Ã¥\‰tÑÑÝžæ4lbws[[Ñhºzº9ÜÿðûÉòøT–”QWYK}U5>¯/µ™O±ŒÜû[ö6Žý$¤?ýþnüø•œyÞÉ.äJ¤BKG;+7®aý¶ÍllØÊ††-4·n—RŠŠ’2NŸ³sN=ƒE N#?'×å§7WÏl´_y¸ÅÍ4÷ìÚÏsÞO_È•ôæ2“'^û†‘1­ŸI­­³ƒëWñÚš•¼¶î-¶ïÞyü°®ï0 <`øTl_*ÊRG¿átTƒ§Oãô‚Ó«Ñ¡£i†ÁÂYs¹ö—ñ®EàõxÆõwÍ{j¬EOöö´¯|çVøÖ®ÜÌÿþÏ\wÓe®¥)Üc;Ë×¼ÁsË—²bÃj¶7î8®À~…™g`æ(Œl3 0GÿÓ°Û¢mv›ÃÊ kX¹a ßûÅý\}ñ¥|à=WRœ?¦m÷ÒZZ×^{é-®¾ðV×'ò—°dãÿ’“—íjºbôö5à×O<ÌS/?Os[둟ÁC>_aæ¨d6Ù5Øí‘}6v‹FkÈ ¹íº›¸þÝW`™ixàÕkiÇáÒ3>ÂÚ•›ÝHn€ßõA¾üÝÛ’’¶ˆ_{W'<òþðôŸEÂ(F¾§ÔÀ(40RT ×!ïݣюfÆ”©|åææÔ9 R“¡¡Œ1¤mCøá_>™´ÂÈöai`ý¶Í\y×ÍüêÏr"xkMgz Ì·°ÊRWø”|õ·Y˜ù[vnç¿z'¿øÓ&ÔÔò´ ]Ý|ûK÷'õ‘p„{>;¶¡E1zÏ-™¾|;Zš±J ²N÷à­31Òlcy# ,°ðͶÐJóƒ_ý”Ïßwý¡þTgÍi~ò½ßpð@[ÒŸóìKyùùIŽ8Þúí[øì}ÿJ8Æ;ÅÄ?ÛJû%<¥“-Œ€âÉ—Ÿç†/ßNKG{ª³5fi†Úæ+Y¾rû÷eû°qÔÙÝÍ÷ÞM8Æ[gâbºÜ•ù­/ »7L=ÞÒjÀ– ;ؽ«‰ùo›×õÝ=4liô³y§ÌBñ½ZþöøK|àæËñú2`âGÛ¶{'/ŒóoôÒ…þ ­x)þ9}«#¬Þ¼/üè›Üû™¯¡T†TeŽ‘V`ÆIu<ùÚ/â¾~É3Ëùà%w úÙãË~†Ç+:üüÑßQ>-VHJŠ ÷'¾²3màŸë¡ï­ϼº„‡ž|„.½:Õ¹JXÚ5ÄÄÔÞÕÉÓK_ bfìS”›yoË)øf[(Cqï¯`óŽm©ÎRÂ$ˆqñø‹Å›%·8VðósÀ“VõÏÑ1sÞ)&áH„ùÉ÷°ÌªÕHãâ™WÿNÕÌ£~Š ª8п6J×ßBtý-Dÿú(SÎ=Õ±µ k·nâwO?šº|Ž‚‘t-í¬Ú¼žªYÇ÷úçg@ÐÐõlˆÞ7"Dö9Dö9ô¾¡ûÅ0ž¨À7ÓB)ÅOþðKº{»SšåDHI÷ò›¯’[ªæ_à sFœ#5©n´‰ìX­ýÜ>òw#Ka•ttwñóÇ~?žY "é^ZùÚqÕÿÃLòÓ|A¦Ý6t›Þn;~M€gЉ2=õ(]=™Q ’Êѱõý'Vÿïf€k¢ì}´ ? 3gè"bäŸwà V¹AOo/¿yòcÍê¸ ’jcÃ6Œœn|Yƒô’QìÀ5ÚÅxý¢´?ÜOçS!:‹ýét Ÿ˜gЉY00ïV‰·z`PóTÅŠÔÃÏü9#F$ˆ¤zyåkTÎzÊ¯×«È &–fh]`ÙŠì±é}=‚>föa´Ù¡ëï¡akÊ„ÜwùðÍ41rFŽÂ’EÎEÞAKPX…Zò÷¯$–É ’êõuoP9}øÿâüľ†Ñ6‡ÐÖh\UøÃBÛíA¯·[4vûðÑDùY‹¼ä_é'ÿJ?Á3<(ïÐM«<öû<ö¨÷é7DÒô‡úÙÚ²o`øëš cÿ‹69„wÚ#^~Ä0—&Z›‰Yd <±ÚOº JIóúúUx#¯øÉ B Þ½þŽy‹‡wÙDöÄWz·ÍOdf‚5‘(Vqlvà3¯¾äjÚn“ ’æÕÕoâ‹s˜¯(/¾Z€vb è à±<„·G‰9ø¦[ø¦ß¡ü}ž•„UÉfQì÷yþµeî'î" "i–¾õ:ìø¾byCŒ  c×çò©+nF)ƒÐ{Øñzdí%÷=>‚gxÈ>×Kþ~¬²ä³À@™Šåkß$j§ïºg ")š[[ØÖ¸ƒ@oöž>Í–Æ8{ôÕ<¦ÅÂú9|ì=7Ð¿ÞÆé9 «ÄÀ’…wª‰ò%o‚R`æ)zûzY³yCÒž3VDR¼ºöM´Ör‡ÿŠõôiÞÚ¢ Gã ‡çXVl¯‡Å'Æ{Ïz'ÚÖô­‹wÒOªî[x}ýèµM6 ")^]õ¾œ¡ß²‰~àHo¾×<ºÙË•g_ʹóÏB‡4ýk¢Çõ§’qhtcõ¦õ)ÎÉÐ$×i­Y¶z(ðû/Ü=ý£(ü²óyŽn#¬”â¦w^Ï©s±{5ýk#`§¾&`d+”¡Xµe]Úž% @¸n{ãš[[ ¾Úo´…8R°ý'ëm·]ö¦VLÁîÔô­·]ßO”2ÀÈ‚¶ÎNöhJmf† @¸nÙê7–_;Rø#£|#jø<'ø¼>>{õ­Ô”Ta·9ôoŒ$4[0ŒìXÜÒØÚŒ A€pÝË+—(9þë5æÂè#`ð“D²üA>ímT•cÔô¥¸OÀÆþ ¶6îH]&†!@¸*±býj”¥³Ô×ÂÇïÐSsƒÙ|áý·SWVÝîз2‚Ó“šª€‘ûs{ãΔ<$„«^[÷¡p3|‡Ìô…`ÕgÌ…8ÒxýÃ^–Ÿ•ËÝ×ÝÁi3âôiúÞŒÞaz)ñhþØ¿Áîý{Ç÷Áq’ \µôÍרL8¿WÓ‚•›mB.ö¤‘,ÿÈkˆ}^·_þQ®»àŠØ´á]6}+¢DŒ_ï ò)”‚Ýû¥PLKW½€U`à8ÊÕÂiÏg²âº^)Å{NßúÈ—XX?§Ï¡c”Þ×#Dö;Iï$´»4ÚP4··‰Žß‘wñ’ \³gÿ>ö4bd)”_±mƒ×ãît[}hè0ÞpXI^w]ý >{̬ͭšŠÓ§ m:ö8hç h Ñý½oFè{+6'!àõÓÚ‘~‰N€£DºxaÅRÌÂX¡/+4(Èu .®C/ÑœÀñË só²éìyíýüº“˜_wëwmæ±eO³±q ¡mQÂ;V™§ÒÀŽ.hé~Û:¼É>’Ϫâ .>õ<Î>étÌhú½o%×üõ•%XE&^K1­Rá8îÖ±u8–Þ‰ ¸¬ìÜ,ö6î+9µ3™S;“¦¶fž}s /®YFxo˜È^#[á­2±J‘.×m‰`·ÇšÓÃ9 ÏàÂ…gSWVsäÒ¶–ŠË ú}“M€pEs[ «6®EùfŽbFµÂ²~/ô‡]zPDcAßñ£†aP^UB¨?LKs[ÜÉ•”pÃ;®âªsÞÃKk—óô/p°£•þMQTƒÂSaà©4Q'œ3«#i²‰îspúcA©²_sUÎTfÞr yY9žÕÝÕK4jcYés,ºáŠ%o¼Š£5žB“¢õf;×î £‹ö³ôýazé¢ÐZÓÙÞEaq~â¿{’H®xnùËøÊ3koëæ\:¤ÑŠs |¦ŒØ3•RÔM¯fë†twõ&ü Ë´8{Îéœ=çt¶ìiàoo¼ÀŠ-«ˆì±‰ì±cM[{ð¶Y'sñ)çqRE=Üø5üvhå´/ÿ”¥ÿùìAö9ëêè‘ &–®žn^]óÊSgYøO˜¥›çÒé?úP3¢(wpLÀ0 ¦Î¬eÓºBý¡Q?oFU=3ªêiëjçÙ•/ñÂê—1”Á Îæ'ŸCAN¬ ;ÀºÛ®æ”{~·³‡üM;9çÖï²ôþÏ=!tuöŒ:?É @ŒÙ“KŸ'‰P4Ǥ¶l`¯Yv@a`qþ}¨­]”7°#íÄU‡–Çbúì)l^·Hdl‹ rò¹æ¼Ë¸bñ% s`±ÙöBºë*(\½•þÂ\²w6±ø“ßcÙ~†hðhE8&Žàñz¤‘ é7.!2Î#Ï>R0o‘5H¯¹R=Ê¡µcéÃ`€iìXóù½L?©ÎµN7å´ð¶ýê·£M“»‚u·_CpßAÎÿÈ¿bõôwÝhš&É"@ŒÉæÛX¿m3õ§Yä³ûÍ}¨6_œ3r à°@ÐOýŒ #ù_õæ3çâX&(ÅŽ÷]À³|›–“grúÝ?=î¾!RÒcòç%ÏàÏVÌ:sø*mnÖ¡Þ³18¼ß_á €†9tÏÉ˦nz5 [“º3O$;Hø˜ãŽ£A?o}ñÃáã§÷÷¾_ÂmR£ŽDøóßÿÆü ½‡Æü‡–ï¶ßð{5J)Ê K|6Ò>¿0—)Óª ºmÿ¢y~æœÐÞïëíOj!51jÿý¯XE”OùX;ýgÔ/?[£CšÒÜâ» ÖþLaq>EÖF¢ÑŽKÀ¶ëß…áTàH8‚m;˜ÃÔZÆKês 2RÔ¶yðO¿aÞƒïÌ3˜Ü1Ôì@CMiå€Ï†jÿ&'/›Yó¦ÌáÀÂQê©.¥»¶|ÄëÆ2<é& bTž^öä$0LÇ߉ò²GœžØ[µ¦d`0ìå÷ù½Ìž?)ÓªS6-7ìæé1 ¦µæþô{ê$Ö‚Œûø¯ÁžyhþLUQÅ€Ï<žÑµd‹Jò™=:E%®õ Du;ŽMÃ!·GŒôˆ„=ùòó4õ60£<Þ#}c²ü`š`¢ùm÷Æzï«K€DkÇòú¦‘­Þ8ø,>ÓGù) ÖQì¯:ò¹Rм‚²sÛÏ Y$ˆ„ôõ÷qïoþ‹)''^è"¶ÆkB_¢@ÇšÓCyAـݨÆûü^¦L«¢¢º”M-´6·ª£Ðcø¨NeoÏVz颯»‹k”M€Áx}ª§”SYSJG[öµÐÓØÌ½šìÙ´†öÒí"l÷³×ÞÀûÞvƒkyt“ôˆ¸½²z¿}êQjç[Ä;±®§_³r“ö½£+üvGìÆÙ5Óý<y†aPP”ǬyS™=…Åùq÷˜†ÉܪÓðZ±&Rk÷VmÍõ<ºAj".Í­-|áGß4u'üµqMC“¦q¿óVÜvG,YÕCkƒ f¨›^MEu)ÍM-´4·cÒ‘áñz()+¤¸´Ëc±»k+ûZé ÷òÚÆ%hGsòô3“š×DI# …C|ú_¥¥½Ê·Yøÿ [»4›w9£Ÿôs§ÝÁ4 fTÕú¹×ÿ\„±ðù½T×UPY[F[K'ö¤¯·Ÿ¼‚\JË É9aÁà ßÍã¯þÇq° ‹×6½„a,˜zú¸ä7İlÇás÷ÝÃÊë0sõ ‡þÊDlغۡ©Å½ùöN¯Æ‰ÀÔòjüCâçN5Ã0(*ɧ¨$Ÿp(‚×7øó‹rKÉòeŽ„¸òœ±dÍ_YºîY€´ Ä¢v”Ïýðž[þ2fÀ ð4…¹ƒ¿ý›Z5ÛvòÄßamÿÏôs¥oê¾ÆCþÃÊ ªÙ¼gA6—œ~Mm{xá­¿̧®|ðßi…ožIuÅÀÂ߆Í-ÉYeg·ÇÒ]3mÐÏ=^OÒøŒÅŒê¹¬ßõÖ‘¿—TqÝ…ÃÍdˆ$ صow|ÿ«lÞ± # Ì·°‚ŠŠ¢£m¬C{ñÐØ­¯ÇËœÚYƒ^ãMáÛ?eUø=Ç™*æ0‹Œ§ôÈ…H Zkzò~ø›c|f™`)ÊŠ‡Ýuõ¦]6ÉÞØÆní¼»`êIx‡8|¼:GËPŠó¼+ÕÙ’@ÞF~ð«ŸòâŠe(Þoyä`Œê…íÀö½{šÇ>´û`ì!§L¸Æþ0߉;¦¡©ƒ×^Ò€Iî@ËAøãCüß3&jÛ˜A…w–…yÌ*¿ülE( «·ÙŒ×*V­!zÐÆ2MN±`ÈëÁá Ó0I5ìiäwO=Ê#Ï>A(FY ß4 «Ò8²±gÀ9AE_H³zÛø¼õsZtæOŸ3ìQàÆFÀ$ÒÞÕÉßW,ã/KžåÕ5o¢µFYŠÀT‹Âé&9¹±Ÿ€ ?v®ýÎ&Ͷq,ù‡DÄztÚ׆‘M€t&`‚Û×|€ç^‰ç—/eÅúUxüùå&3϶(¬1ÈÎ3ðù?³¥CÓ°/I]üÃÐáXû?èósê0íÿ 'ïˆÄH˜`öhâÍMkYµqon\ËÞ®Êê¹µœæ!˜g ºwÿ‰úC°a‡3®ÕþÃ"ûm´Öœ3ï¬!{ÿAªÿnÁš[[ضg'›wlç­MkY¹i-Í­-˜Eõl“)‹-f–&^E¶mXµÕ!’й*"û”R¼}áÙÃ^J `¬$¤¹þP?û6³s_#Ûwï¢aÏ.¶íÞÁöÝt÷vwm0O1ï"/Õ³L<£œ¯5¬ßéÐJÁ«ˆ4;è~ÍÜ)³¨,~sͬœôØU'“IH‘Þþ>Ú»:éèꢹý Í­-ìoi¦©¥™æÖöµà@k3Ý݃ޯ …‘e`fAÅ “êi…ñU£Is°=5… ‘]±jÇ?œqñ°—š¦I0Kšc%ÀeŽvhؽ‹5[6а·‘¶Î:º;iïŽöŽ®NÚ»:‰DãØÖTA…ò*Œ#¨ðg+*K *‹Áçqg.|s»fG :ý³[œ^ÍÔŠ)Ì­~òLVv ­×d .8ØÞÊÓË^`ÉŠå¬Þ²îÞnrŠ ªf™„z5}Ý¡nèëÖ„z5˜±B¬,À£0¼ åëàПÊÊø/ÌQT+Šó l‡?¢ž~͆©+üh툽ý¯X4òÔÙtÙT3ÓI¥P8ÄS/?Ï/=Çòµ+qcù æ¿ÓË”“Ì!«ãŽ£ G¡¨&Žõ¸‡¢špÂèhB¡£'é™”**‹ r†ž3jQÖlKÞ¢ž¸ò°ßÁéÑ̬šÊÉÓæx}v2þ!&! êîíæwO?Îožø?ZÚÛP>…§Ä pªÉœÙÙ#|7 Cá÷ß§àÈ‹ìøha;ЊÃÛ¶ÂkAwŸC$ªð{ÁïMìDœálÜáÞî=£bkB;m”R\wáûF¼Ü0 ‚ÙÉ9Ùg²‘§P$̃<įžx„žÞØ)V‰§ÂÀWd0­Ê ²Ø½:yw¯fS£&v´¼¦µëð'G;è Cáõh‚^…× ^¼±àô*|^Fìܾϡ9IkùãÚe£Cš3gŸÊôŠº¯ÏÊŒËqß“€8,}ëu¾þ_÷²÷@ÊTx+M<5ʧ(ÌU̬5¸4#5‘mµGÓŠÕŽ:>@®-ø<‡jý©èêÓìjJmá·»4‘F‡ ?À o¿:®{ò r’œ«ÉCÀ0zûûøþ/ÂÃÏü­5V¡wº‰áWxL˜YkPZàÞ[ßímµGÓÛ±Ó¨S[Ð¥!¼5vŒÖ•g_J^V|;¯ 7™¹šT$ aûž]Üù½a[ãN Â;ÍÂS«v–ä+fÖ*¼ƒôÒFoH³i§CûàCþVx§Ýö»è”sãºÇðÉ IÄÓK_à+?þ}¡>¬Bß, å¯W1»FQ”çNÁwtlµÝ®ýšŽ”ŸpœNM¤ÑÆçññO—~CÅצ/(ÊKrÎ& 'øÑC?ãgümlWœ:oMlWœÊbÅ´*·¡iëŠm¦ÙÛŸ†Uó$ÓèßEkøðÅ×P^P÷½…ÅùIÌÙä#àG;|óÁÿà÷O?†áUøf›˜ù~/̪5†Ü;Q‘(lÝãîÞù™Dkè_Á iΞs:çÌÿ¤œ¬ì Tÿ]&€Øþ÷_¸ïßxzÙ‹(ŸÂ¿À *ªJõî½õ“µw~& oŽbwh¦WÔqÓ%HèÞ¢yû»mÒ­5_ú÷o)üùV–A^–Æ0`çþ£sËP6ΰLŽ›Ã£àÈî¹ÇÚߪ9˜âñöT ï´‰ìwÈ æpÛå7áI`klË2)”àºI~ôЃ<ùòó±Â¿Ð (L3XÛ`ÓÖubÜx,Â6á±N¿¾òcæ$tAQžLþI‚Iý/úð3æÁG‹² 0Ï (f×dù4Ñhªs7qD 6^—»®úx\³ýNTR^ä~ÆÄä ËV½Î=?ûÊPøçz0²3ª Ê Ýý©ÎÝ¡!´=J¨!ŠeZüó7yÆßpò seÿ¿$™”M€æÖ>ß¿a;6þÙfž¢¶,ÖéÐÓ'Uý1Óп9Jt¿ƒßççŽ+naNíÌQ%UVÿ0¡H̤ ¶ãp׿A[gž«Ä ¢È`ZÕÑž¼©Œ‰î×ô­âtk s øÜշޏ½×PrósÈ’•I3éÀÏû=o®_‘mà­³(ÉWÌšr|ß~w_Š27Øí¡6NXSSZÅWÜBIÞèÚïJ)ªjË\Ρ8Ö¤ ë·oáÇ¿ÿE¬Ý?Ë$?G1§îø¡=­™”³óÆJ;±ýü"6ZÃùóñ¡‹®ÁcrwRbãþ²õwrMšàh‡}à^¢vßT‹ÜâØp߉#KýaRº3N&²»4¡Í6NƒßëãÃ︖³ç1¦4MÓ¤¢ºÔ¥Š¡LšðȳO°fËFŒlEîTƒ…ÓŸá×-oÿ¸é–Ç0Ç ØIDATÈ¡·þ^­5ó¦Ìæ¦K®§8·pÌiWÖ”âñ޾ö â3)@[g÷=ô3Ps’‡…3L¼CL)ï‘öÿˆ´†èn‡pc… ÏÏõ¼óæ/re§Þ유Œû“Iþów?§£«‹@Á©§š 7¤Ü#5€¡E5‘&MxOl /Ë4yû©çrùâKÈ ¸³K¯aÔÔWº’–Ù„{›÷óÈsObù§]ìc¤%© ¤Cšð^›è>Žj”Rœ1ë®9÷2ÊXÊêº éøG>üôá_a;QθÂGþ;I92p„ŽBtlñŽÓû7ñz¼\pêb.9íBŠóÆÞÎ?Qaq>Å¥‰­c3¡@ÓÁüeÉ3̇—ÒŠ‘g=÷ö“’ÓpÓ…¶5v«Ænqˆ¶ÆÞö9yœ;÷,.:õ<ò³’³ŸÏ®")i‹¡MèðßþŽêùš)óâû5'[û_kpº4N‡&Úæàthô¡è1-N5ŸsçŸÅ¼ºÙqoÙ5–e2ã¤:,·6^q›° »·‡G_xгÞÿ¯8‘×èè^Ý«qz5N—ƒÓÅ‘ÌfáÔ¹,œ:—ùu³ ø’?W)EÝô¼>òK… þòÒ³„"!²òâÿgÜà¨ÆqØlÐQÖ'¬ÑáX׃,oÎ d3½ªžYÕÓ˜]=úòÚq?psÊ´*ró³Çõ™â¨ yöI²ò Œj•½iTÐpºœ~Ó§Ñý>ôó0±B'顼´”òübÊ Ê¨,,cjå* S7Ï>ö毖]~Sl€­»¨˜éG¡/œÄ ÀéÓØív»ÆîŠø¡dùƒdeñ{ýø¼¼/Y¾ ^ËKaN>yÙ9f—KQv>ùÙyiu”¶RŠšúJ)üi`€¼Úø@ĆU[ì$çf 'ö‡È~ûÈPÛa¥ùÅÔ—ÕPU\IYA eùÅdç“ÌÂJ`/½tcuÓ«É/”Ó}ÒAæ~“F ”"§däžëhÞÚbkûßîÔDöØØöºå°`ê\æM™ÍœÚdù'Þñצi2mV-Ù¹îÌc7a€‘§È _íÚ°j«3n…ßîÒ„lìöØrÃì@óç/æÌÙ§RWV3>™H‘ìœ õ3jdOš™°À*V 7£4jÃê­½ÉïøÓ5ĶÇBCq^!—q1çÌ;sLëå3EYe •5¥iÕ!b&lÈ®01‡hØN¬ðwô$¿ð»CNN ‹«ÏùÎ[°3‘በåñXÔN­’ã¼ÓØ„ YCåå8šµÛtò ¿†ð›pc¬sñÜygrýï#Û¥Us鮸´€ª)˜CEa‘&lúÇѬٮipà‡»b‡_F°Û5o€›ÞugÎ>5©ÏL ŸªÚr™Ü“!&lø4ÇžÙåhb…¿3É…?¤é[ÅéÓ̪™Î­—~˜‚œ‰¤•Ï¦TÆö3Ì„ ÇŽh ëlZ;“ûL‚þ5±Âÿ¶ øäeÉè1ûxøü>Ê*Š(,É—£»2Єývfjh ëšÛ“û<‚¾Õœ>Íi3òÉ÷~dBwô³”U“_˜+½ûlÂˈþ»šÛ“ÜágkúÖÅÞü§Ï\È­—MÌÂïóû(.-  (OVïM6h`ó.‡¦–ä÷ö÷oŠMåU3OüÃ?N¨ÂïóûÈËÏ&¯ ‡ìÜ,yÛO06lÞ¥iêHþ8x‡Mô CyA)w\ñ±Œoó{<Áì 9¹ArósäPÎ .³¿­ÃØ×ê ’ü"¶Û»mü^Ÿºâ£dù3ë ;¯Ï‹?àÃð‘• +;(UûIf€dÓÑXÕ ½äƒT§ÏVÖ–e¢ Ó4ðx,<^–ÇÂëý·Ï+øÒk/$ŒRÿæ:¤9oþ"Μuʸ<Óã±ý²x½–ÇÂã‰ýiy, CI¡ ‘0 уöAMaN>¸ðФ=Çð‘›—Mn~Á,?–Gþïî’oT¢¢šðV¥·¼û‚>w×í‚~ŠJò)(Ê“¥³"é$$(´ÃÁ kÎs§Ìr%M¥TìPŒ²B²F:ºHIH€Ý¥‰ìµÉdqýùc¯ú+¥(*ɧ¼ªTzßEJHH@h{loí+¿{ÌËzýS¦UË_¤”€8EÆNΩ.©ä¢SÎu:J)J+Š©¨.‘{‘râ 5„b{\wþå£>&Ë㱨ŸYKvÎÄÛðSd& qˆîvpú4 êç° ~ΨÒð|L›5Ÿßërî„= #ÐQMxw…âšs/UÙ9A¦Îš"‡_Š´#ÐDtÏ9)eÕ ß/…_¤3© C÷k"{<–‡kÏ»<áûƒY¦Í®“1EÚ’oæ0ÂÚÑ\¸ðl rÛëÎãõ0mV­~‘ÖäÛ9§Gm²Éòyßâ÷$t¯aL›U+SyEÚ“0„ЭáÒ3.JxM}%Á,™à#ÒŸ€A8»Å¡0'ŸwžvAB÷åQT2ñ·ƒ€Ažòû¾ÅïÆkÆ_÷ú¼ÔN­JV¶„p€D[ìNMyA çÌ;3¡{kë+¥Ó/E‘Tg!eäÛz, áí±)¿×œwyB»û—ÈqXêê oåþïþzR Ljìqpz5óëOâô™ ã¾Ï㱨¬-ObÎD2u´uò­/ÞÏÅ'ßÀóO-KuvÆ•€CtÂQ eðþóߛн5e2ÓoضiÿxÙg¸õº/³{gSª³3.$Ùa£#pþÂÅÔ–Ä?å7˜ ¸´ ‰9ãIkÍŸ~Ž ç^Ç÷¿ú3z{úR¥¤’À¡~öÅ&ý\uÎ¥ Ý[SŸ>Û ÷ô÷…øÑ=?ç¬ú÷ñßÿþlÛIu–’bÒí@hc­á† ¯"7G^^A®ìè3Áµµtðµ;ïãªó?Î[¯­Ouv\7é@¤ÑÆéÓÌ›2›³ç÷}†aPSW‘Äœ‰tòÆ+k¹lÑGùÈ{?CcÃÞTgÇ5“:8½šH£ƒÇôpãEW'toqY¡lä9 =ûÄRÞ>ÿ|ë‹÷ÓÝÕ›êìŒÙ¤ ÚþQ´£¹|ñ»¨(,‹û^Ó4)¯*IbîD:ëï qÿwÍsÞÏC?{ ÇÉÜþIÂÛcGzÏ2‹ËÎ|gB÷–W•ȰŸ`ÿÞƒ|៾Ãe‹nfŲ5©ÎΨLÊ=xtÿ½ûƄμ÷x,JÊ “˜;‘iV¯ØÀ•ç}œO¼ÿnöìÚŸêì$dâîdk4 ¶i›¢àÀ^CŽ?‹h$w²Õ¥ØQÛÍÜŠÒÚ44ù¿çyîÉeüÓg>È'?ÿ¡ŒØ6þW_íWnq3Íá,yf9¼äŽñzœq«¨.ås÷üWÝpIB5Ì„iÞSc-zj´·OÊ&€ɶo÷îüÇoð¡K?Í– ;R!I"‰^üë«\´àƒÜñáoppkª³3€!’ÌqvíØË¦–Tge€‰Û (D¨¬)ãîï|’Ë®½(¹}£$@ˆ$ÈÎÍâ3_¿…â*,Oú³ôÍYæŸ:›‡ž¾/ÕÙî37“}»¸’–a\ÿÑ÷òé¯~”ÒŠbWÒL¦ŒEyœwqbûö q¢`–ß•tN?{_ÿáÌÛlWÒ„HÕSʹû»·séU¦e;8„%ŸßË­Ÿ»‘O|öAwjãM€£pÕ ïæs÷|œÊšøW‘¦# B$`ÎÂ|íÞ;XtÁ©©ÎŠ+$‡œ¼lîüÊM|ä¶kÒzX/Qç7" <^·ÜqŸüü‡&äÁ/„™çÌ×y§ÌLuV’F€'¨ª-ãîïÞÎe×¼#ÕYI: B’“—Í]_»9í§ïºirü–B Ã0MnºýZîøÊMå¥:;ãJ€˜ôþë¿ÉŒ“êR”p{?€Ëé ‘t]ø5c:¼ÐÕ  ÃÍô„ÃS–Ñ6–ûÝ­h½ÙÕô„ÃÑþ­cIÀÕà˜úM7ÓB kS¹ZØ3–\ 5,^§ ³NF"céçÆš‚»}Ji~ëfšBˆÁÆØËšë»[†~äØ!’JóZ%‹^k2®€Jµx#h©‘L¦þœRj̇š%å\m8_ÕžŒ´…˜ì”âñµøïn¤•”P«ÎÙ« õ^ œŒô…˜ÄÖ÷)u£[‰%íd ZuæKýéd¥/Ää£Ú þr†:«Ó­“z4X­¹øÇ > Äþ¶b0»µa¿³J³ÉÍDÇeãÝ‘åo׆ó ýOJ"Ýhµ$bríTu–ëslÆmó&½*+âôÞÜ äŒ×s…È`[ ͧ«¬ENÖÆýƒýJéð)×åãý|!ÒžÖ+Pü2j„¬Wö'óQ);ÆDk­v³|¾²“µRõ@1hoªò#DªhT¨½JÓ5¥õꌦTçI!„B!„B!„B!„B!„B!„B!„B!„B!„B1.þ MZì©6‹IEND®B`‚PyMeasure-0.9.0/docs/images/PyMeasure.svg0000644000175000017500000003057513640137324020531 0ustar colincolin00000000000000 image/svg+xmlMeasure PyPyMeasure-0.9.0/pymeasure/0000775000175000017500000000000014010046235015673 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/thread.py0000664000175000017500000000513714010037617017526 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from threading import Thread, Event from time import time log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class InterruptableEvent(Event): """ This subclass solves the problem indicated in bug https://bugs.python.org/issue35935 that prevents the wait of an Event to be interrupted by a KeyboardInterrupt. """ def wait(self, timeout=None): if timeout is None: while not super().wait(0.1): pass else: timeout_start = time() while not super().wait(0.1) and time() <= timeout_start + timeout: pass class StoppableThread(Thread): """ Base class for Threads which require the ability to be stopped by a thread-safe method call """ def __init__(self): super().__init__() self._should_stop = InterruptableEvent() self._should_stop.clear() def join(self, timeout=0): """ Joins the current thread and forces it to stop after the timeout if necessary :param timeout: Timeout duration in seconds """ self._should_stop.wait(timeout) if not self.should_stop(): self.stop() return super().join(0) def stop(self): self._should_stop.set() def should_stop(self): return self._should_stop.is_set() def __repr__(self): return "<%s(should_stop=%s)>" % ( self.__class__.__name__, self.should_stop()) PyMeasure-0.9.0/pymeasure/instruments/0000775000175000017500000000000014010046235020266 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/danfysik/0000775000175000017500000000000014010046235022076 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/danfysik/adapters.py0000664000175000017500000000552314010037617024264 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.adapters.serial import SerialAdapter import re class DanfysikAdapter(SerialAdapter): """ Provides a :class:`SerialAdapter` with the specific baudrate and timeout for Danfysik serial communication. Initiates the adapter to open serial communcation over the supplied port. :param port: A string representing the serial port """ def __init__(self, port): super(DanfysikAdapter, self).__init__(port, baudrate=9600, timeout=0.5) def write(self, command): """ Overwrites the :func:`SerialAdapter.write ` method to automatically append a Unix-style linebreak at the end of the command. :param command: SCPI command string to be sent to the instrument """ command += "\r" self.connection.write(command.encode()) def read(self): """ Overwrites the :func:`SerialAdapter.read ` method to automatically raise exceptions if errors are reported by the instrument. :returns: String ASCII response of the instrument :raises: An :code:`Exception` if the Danfysik raises an error """ # Overwrite to raise exceptions on error messages result = b"".join(self.connection.readlines()) result = result.decode() result = result.replace("\r", "") search = re.search(r"^\?\x07\s(?P.*)$", result, re.MULTILINE) if search: raise Exception("Danfysik raised the error: %s" % ( search.groups()[0])) else: return result def __repr__(self): return "" % self.connection.port PyMeasure-0.9.0/pymeasure/instruments/danfysik/__init__.py0000664000175000017500000000232214010037617024212 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .adapters import DanfysikAdapter from .danfysik8500 import Danfysik8500 PyMeasure-0.9.0/pymeasure/instruments/danfysik/danfysik8500.py0000664000175000017500000002757314010037617024617 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument, RangeException from .adapters import DanfysikAdapter from time import sleep import numpy as np import re class Danfysik8500(Instrument): """ Represents the Danfysik 8500 Electromanget Current Supply and provides a high-level interface for interacting with the instrument To allow user access to the Prolific Technology PL2303 Serial port adapter in Linux, create the file: :code:`/etc/udev/rules.d/50-danfysik.rules`, with contents: .. code-block:: none SUBSYSTEMS=="usb",ATTRS{idVendor}=="067b",ATTRS{idProduct}=="2303",MODE="0666",SYMLINK+="danfysik" Then reload the udev rules with: .. code-block:: bash sudo udevadm control --reload-rules sudo udevadm trigger The device will be accessible through the port :code:`/dev/danfysik`. """ id = Instrument.measurement( "PRINT", """ Reads the idenfitication information. """ ) def __init__(self, port): super(Danfysik8500, self).__init__( DanfysikAdapter(port), "Danfysik 8500 Current Supply", includeSCPI=False ) self.write("ERRT") # Use text error messages self.write("UNLOCK") # Unlock from remote or local mode def local(self): """ Sets the instrument in local mode, where the front panel can be used. """ self.write("LOC") def remote(self): """ Sets the instrument in remote mode, where the the front panel is disabled. """ self.write("REM") @property def polarity(self): """ The polarity of the current supply, being either -1 or 1. This property can be set by suppling one of these values. """ return 1 if self.ask("PO").strip() == '+' else -1 @polarity.setter def polarity(self, value): polarity = "+" if value > 0 else "-" self.write("PO %s" % polarity) def reset_interlocks(self): """ Resets the instrument interlocks. """ self.write("RS") def enable(self): """ Enables the flow of current. """ self.write("N") def disable(self): """ Disables the flow of current. """ self.write("F") def is_enabled(self): """ Returns True if the current supply is enabled. """ return self.status_hex & 0x800000 == 0 @property def status_hex(self): """ The status in hexadecimal. This value is parsed in :attr:`~.Danfysik8500.status` into a human-readable list. """ status = self.ask("S1H") match = re.search(r'(?P[A-Z0-9]{6})', status) if match is not None: return int(match.groupdict()['hex'], 16) else: raise Exception("Danfysik status not properly returned. Instead " "got '%s'" % status) @property def current(self): """ The actual current in Amps. This property can be set through :attr:`~.current_ppm`. """ return int(self.ask("AD 8"))*1e-2*self.polarity @current.setter def current(self, amps): if amps > 160 or amps < -160: raise RangeException("Danfysik 8500 is only capable of sourcing " "+/- 160 Amps") self.current_ppm = int((1e6/160)*amps) @property def current_ppm(self): """ The current in parts per million. This property can be set. """ return int(self.ask("DA 0")[2:]) @current_ppm.setter def current_ppm(self, ppm): if abs(ppm) < 0 or abs(ppm) > 1e6: raise RangeException("Danfysik 8500 requires parts per million " "to be an appropriate integer") self.write("DA 0,%d" % ppm) @property def current_setpoint(self): """ The setpoint for the current, which can deviate from the actual current (:attr:`~.Danfysik8500.current`) while the supply is in the process of setting the value. """ return self.current_ppm*(160/1e6) @property def slew_rate(self): """ The slew rate of the current sweep. """ return float(self.ask("R3")) def wait_for_current(self, has_aborted=lambda: False, delay=0.01): """ Blocks the process until the current has stabilized. A provided function :code:`has_aborted` can be supplied, which is checked after each delay time (in seconds) in addition to the stability check. This allows an abort feature to be integrated. :param has_aborted: A function that returns True if the process should stop waiting :param delay: The delay time in seconds between each check for stability """ self.wait_for_ready(has_aborted, delay) while not has_aborted() and not self.is_current_stable(): sleep(delay) def is_current_stable(self): """ Returns True if the current is within 0.02 A of the setpoint value. """ return abs(self.current - self.current_setpoint) <= 0.02 def is_ready(self): """ Returns True if the instrument is in the ready state. """ return self.status_hex & 0b10 == 0 def wait_for_ready(self, has_aborted=lambda: False, delay=0.01): """ Blocks the process until the instrument is ready. A provided function :code:`has_aborted` can be supplied, which is checked after each delay time (in seconds) in addition to the readiness check. This allows an abort feature to be integrated. :param has_aborted: A function that returns True if the process should stop waiting :param delay: The delay time in seconds between each check for readiness """ while not has_aborted() and not self.is_ready(): sleep(delay) @property def status(self): """ A list of human-readable strings that contain the instrument status information, based on :attr:`~.status_hex`. """ status = [] indicator = self.ask("S1") if indicator[0] == "!": status.append("Main Power OFF") else: status.append("Main Power ON") # Skipping 5, 6 and 7 (from Appendix Manual on command S1) messages = { 1: "Polarity Normal", 2: "Polarity Reversed", 3: "Regulation Transformer is not equal to zero", 7: "Spare Interlock", 8: "One Transistor Fault", 9: "Sum - Interlock", 10: "DC Overcurrent (OCP)", 11: "DC Overload", 12: "Regulation Module Failure", 13: "Preregulator Failure", 14: "Phase Failure", 15: "MPS Waterflow Failure", 16: "Earth Leakage Failure", 17: "Thermal Breaker/Fuses", 18: "MPS Overtemperature", 19: "Panic Button/Door Switch", 20: "Magnet Waterflow Failure", 21: "Magnet Overtemperature", 22: "MPS Not Ready" } for index, message in messages.items(): if indicator[index] == "!": status.append(message) return status def clear_ramp_set(self): """ Clears the ramp set. """ self.write("RAMPSET C") def set_ramp_delay(self, time): """ Sets the ramp delay time in seconds. :param time: The time delay time in seconds """ self.write("RAMPSET %f" % time) def start_ramp(self): """ Starts the current ramp. """ self.write("RAMP R") def add_ramp_step(self, current): """ Adds a current step to the ramp set. :param current: A current in Amps """ self.write("R %.6f" % (current/160.)) def stop_ramp(self): """ Stops the current ramp. """ self.ask("RAMP S") def set_ramp_to_current(self, current, points, delay_time=1): """ Sets up a linear ramp from the initial current to a different current, with a number of points, and delay time. :param current: The final current in Amps :param points: The number of linear points to traverse :param delay_time: A delay time in seconds """ initial_current = self.current self.clear_ramp_set() self.set_ramp_delay(delay_time) steps = np.linspace(initial_current, current, num=points) cmds = ["R %.6f" % (step/160.) for step in steps] self.write("\r".join(cmds)) def ramp_to_current(self, current, points, delay_time=1): """ Executes :meth:`~.set_ramp_to_current` and starts the ramp. """ self.set_ramp_to_current(current, points, delay_time) self.start_ramp() # self.setSequence(0, [0, 10], [0.01]) def set_sequence(self, stack, currents, times, multiplier=999999): """ Sets up an arbitrary ramp profile with a list of currents (Amps) and a list of interval times (seconds) on the specified stack number (0-15) """ self.clear_sequence(stack) if min(times) >= 1 and max(times) <= 65535: self.write("SLOW %i" % stack) elif min(times) >= 0.1 and max(times) <= 6553.5: self.write("FAST %i" % stack) times = [0.1*x for x in times] else: raise RangeException("Timing for Danfysik 8500 ramp sequence is" " out of range") for i in range(len(times)): self.write("WSA %i,%i,%i,%i" % ( stack, int(6250*abs(currents[i])), int(6250*abs(currents[i+1])), times[i]) ) self.write("MULT %i,%i" % (stack, multiplier)) def clear_sequence(self, stack): """ Clears the sequence by the stack number. :param stack: A stack number between 0-15 """ self.write("CSS %i" % stack) def sync_sequence(self, stack, delay=0): """ Arms the ramp sequence to be triggered by a hardware input to pin P33 1&2 (10 to 24 V) or a TS command. If a delay is provided, the sequence will start after the delay. :param stack: A stack number between 0-15 :param delay: A delay time in seconds """ self.write("SYNC %i, %i" % (stack, delay)) def start_sequence(self, stack): """ Starts a sequence by the stack number. :param stack: A stack number between 0-15 """ self.write("TS %i" % stack) def stop_sequence(self): """ Stops the currently running sequence. """ self.write("STOP") def is_sequence_running(self, stack): """ Returns True if a sequence is running with a given stack number :param stack: A stack number between 0-15 """ return re.search("R%i," % stack, self.ask("S2")) is not None PyMeasure-0.9.0/pymeasure/instruments/keithley/0000775000175000017500000000000014010046235022104 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/keithley/buffer.py0000664000175000017500000001115014010037617023731 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range from pymeasure.adapters import PrologixAdapter import numpy as np from time import sleep, time class KeithleyBuffer(object): """ Implements the basic buffering capability found in many Keithley instruments. """ buffer_points = Instrument.control( ":TRAC:POIN?", ":TRAC:POIN %d", """ An integer property that controls the number of buffer points. This does not represent actual points in the buffer, but the configuration value instead. """, validator=truncated_range, values=[2, 1024], cast=int ) def config_buffer(self, points=64, delay=0): """ Configures the measurement buffer for a number of points, to be taken with a specified delay. :param points: The number of points in the buffer. :param delay: The delay time in seconds. """ # Enable measurement status bit # Enable buffer full measurement bit self.write(":STAT:PRES;*CLS;*SRE 1;:STAT:MEAS:ENAB 512;") self.write(":TRAC:CLEAR;") self.buffer_points = points self.trigger_count = points self.trigger_delay = delay self.write(":TRAC:FEED SENSE;:TRAC:FEED:CONT NEXT;") self.check_errors() def is_buffer_full(self): """ Returns True if the buffer is full of measurements. """ status_bit = int(self.ask("*STB?")) return status_bit == 65 def wait_for_buffer(self, should_stop=lambda: False, timeout=60, interval=0.1): """ Blocks the program, waiting for a full buffer. This function returns early if the :code:`should_stop` function returns True or the timeout is reached before the buffer is full. :param should_stop: A function that returns True when this function should return early :param timeout: A time in seconds after which this function should return early :param interval: A time in seconds for how often to check if the buffer is full """ # TODO: Use SRQ initially instead of constant polling #self.adapter.wait_for_srq() t = time() while not self.is_buffer_full(): sleep(interval) if should_stop(): return if (time()-t)>timeout: raise Exception("Timed out waiting for Keithley buffer to fill.") @property def buffer_data(self): """ Returns a numpy array of values from the buffer. """ self.write(":FORM:DATA ASCII") return np.array(self.values(":TRAC:DATA?"), dtype=np.float64) def start_buffer(self): """ Starts the buffer. """ self.write(":INIT") def reset_buffer(self): """ Resets the buffer. """ self.write(":STAT:PRES;*CLS;:TRAC:CLEAR;:TRAC:FEED:CONT NEXT;") def stop_buffer(self): """ Aborts the buffering measurement, by stopping the measurement arming and triggering sequence. If possible, a Selected Device Clear (SDC) is used. """ if type(self.adapter) is PrologixAdapter: self.write("++clr") else: self.write(":ABOR") def disable_buffer(self): """ Disables the connection between measurements and the buffer, but does not abort the measurement process. """ self.write(":TRAC:FEED:CONT NEV") PyMeasure-0.9.0/pymeasure/instruments/keithley/keithley2400.py0000664000175000017500000007022314010037617024612 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument, RangeException from pymeasure.adapters import PrologixAdapter from pymeasure.instruments.validators import truncated_range, strict_discrete_set from .buffer import KeithleyBuffer import numpy as np import time from io import BytesIO import re class Keithley2400(Instrument, KeithleyBuffer): """ Represents the Keithely 2400 SourceMeter and provides a high-level interface for interacting with the instrument. .. code-block:: python keithley = Keithley2400("GPIB::1") keithley.apply_current() # Sets up to source current keithley.source_current_range = 10e-3 # Sets the source current range to 10 mA keithley.compliance_voltage = 10 # Sets the compliance voltage to 10 V keithley.source_current = 0 # Sets the source current to 0 mA keithley.enable_source() # Enables the source output keithley.measure_voltage() # Sets up to measure voltage keithley.ramp_to_current(5e-3) # Ramps the current to 5 mA print(keithley.voltage) # Prints the voltage in Volts keithley.shutdown() # Ramps the current to 0 mA and disables output """ source_mode = Instrument.control( ":SOUR:FUNC?", ":SOUR:FUNC %s", """ A string property that controls the source mode, which can take the values 'current' or 'voltage'. The convenience methods :meth:`~.Keithley2400.apply_current` and :meth:`~.Keithley2400.apply_voltage` can also be used. """, validator=strict_discrete_set, values={'current': 'CURR', 'voltage': 'VOLT'}, map_values=True ) source_enabled = Instrument.control( "OUTPut?", "OUTPut %d", """A boolean property that controls whether the source is enabled, takes values True or False. The convenience methods :meth:`~.Keithley2400.enable_source` and :meth:`~.Keithley2400.disable_source` can also be used.""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) auto_output_off = Instrument.control( ":SOUR:CLE:AUTO?", ":SOUR:CLE:AUTO %d", """ A boolean property that enables or disables the auto output-off. Valid values are True (output off after measurement) and False (output stays on after measurement). """, values={True: 1, False: 0}, map_values=True, ) source_delay = Instrument.control( ":SOUR:DEL?", ":SOUR:DEL %g", """ A floating point property that sets a manual delay for the source after the output is turned on before a measurement is taken. When this property is set, the auto delay is turned off. Valid values are between 0 [seconds] and 999.9999 [seconds].""", validator=truncated_range, values=[0, 999.9999], ) source_delay_auto = Instrument.control( ":SOUR:DEL:AUTO?", ":SOUR:DEL:AUTO %d", """ A boolean property that enables or disables auto delay. Valid values are True and False. """, values={True: 1, False: 0}, map_values=True, ) auto_zero = Instrument.control( ":SYST:AZER:STAT?", ":SYST:AZER:STAT %d", """ A property that controls the auto zero option. Valid values are True (enabled) and False (disabled) and 'ONCE' (force immediate). """, values={True: 1, False: 0, "ONCE": "ONCE"}, map_values=True, ) measure_concurent_functions = Instrument.control( ":SENS:FUNC:CONC?", ":SENS:FUNC:CONC %d", """ A boolean property that enables or disables the ability to measure more than one function simultaneously. When disabled, volts function is enabled. Valid values are True and False. """, values={True: 1, False: 0}, map_values=True, ) ############### # Current (A) # ############### current = Instrument.measurement( ":READ?", """ Reads the current in Amps, if configured for this reading. """ ) current_range = Instrument.control( ":SENS:CURR:RANG?", ":SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG %g", """ A floating point property that controls the measurement current range in Amps, which can take values between -1.05 and +1.05 A. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-1.05, 1.05] ) current_nplc = Instrument.control( ":SENS:CURR:NPLC?", ":SENS:CURR:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the DC current measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) compliance_current = Instrument.control( ":SENS:CURR:PROT?", ":SENS:CURR:PROT %g", """ A floating point property that controls the compliance current in Amps. """, validator=truncated_range, values=[-1.05, 1.05] ) source_current = Instrument.control( ":SOUR:CURR?", ":SOUR:CURR:LEV %g", """ A floating point property that controls the source current in Amps. """, validator=truncated_range, values=[-1.05, 1.05] ) source_current_range = Instrument.control( ":SOUR:CURR:RANG?", ":SOUR:CURR:RANG:AUTO 0;:SOUR:CURR:RANG %g", """ A floating point property that controls the source current range in Amps, which can take values between -1.05 and +1.05 A. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-1.05, 1.05] ) ############### # Voltage (V) # ############### voltage = Instrument.measurement( ":READ?", """ Reads the voltage in Volts, if configured for this reading. """ ) voltage_range = Instrument.control( ":SENS:VOLT:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG %g", """ A floating point property that controls the measurement voltage range in Volts, which can take values from -210 to 210 V. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-210, 210] ) voltage_nplc = Instrument.control( ":SENS:CURRVOLT:NPLC?", ":SENS:VOLT:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the DC voltage measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) compliance_voltage = Instrument.control( ":SENS:VOLT:PROT?", ":SENS:VOLT:PROT %g", """ A floating point property that controls the compliance voltage in Volts. """, validator=truncated_range, values=[-210, 210] ) source_voltage = Instrument.control( ":SOUR:VOLT?", ":SOUR:VOLT:LEV %g", """ A floating point property that controls the source voltage in Volts. """ ) source_voltage_range = Instrument.control( ":SOUR:VOLT:RANG?", ":SOUR:VOLT:RANG:AUTO 0;:SOUR:VOLT:RANG %g", """ A floating point property that controls the source voltage range in Volts, which can take values from -210 to 210 V. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-210, 210] ) #################### # Resistance (Ohm) # #################### resistance = Instrument.measurement( ":READ?", """ Reads the resistance in Ohms, if configured for this reading. """ ) resistance_range = Instrument.control( ":SENS:RES:RANG?", ":SENS:RES:RANG:AUTO 0;:SENS:RES:RANG %g", """ A floating point property that controls the resistance range in Ohms, which can take values from 0 to 210 MOhms. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[0, 210e6] ) resistance_nplc = Instrument.control( ":SENS:RES:NPLC?", ":SENS:RES:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the 2-wire resistance measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) wires = Instrument.control( ":SYSTEM:RSENSE?", ":SYSTEM:RSENSE %d", """ An integer property that controls the number of wires in use for resistance measurements, which can take the value of 2 or 4. """, validator=strict_discrete_set, values={4: 1, 2: 0}, map_values=True ) buffer_points = Instrument.control( ":TRAC:POIN?", ":TRAC:POIN %d", """ An integer property that controls the number of buffer points. This does not represent actual points in the buffer, but the configuration value instead. """, validator=truncated_range, values=[1, 2500], cast=int ) means = Instrument.measurement( ":CALC3:FORM MEAN;:CALC3:DATA?;", """ Reads the calculated means (averages) for voltage, current, and resistance from the buffer data as a list. """ ) maximums = Instrument.measurement( ":CALC3:FORM MAX;:CALC3:DATA?;", """ Returns the calculated maximums for voltage, current, and resistance from the buffer data as a list. """ ) minimums = Instrument.measurement( ":CALC3:FORM MIN;:CALC3:DATA?;", """ Returns the calculated minimums for voltage, current, and resistance from the buffer data as a list. """ ) standard_devs = Instrument.measurement( ":CALC3:FORM SDEV;:CALC3:DATA?;", """ Returns the calculated standard deviations for voltage, current, and resistance from the buffer data as a list. """ ) ########### # Trigger # ########### trigger_count = Instrument.control( ":TRIG:COUN?", ":TRIG:COUN %d", """ An integer property that controls the trigger count, which can take values from 1 to 9,999. """, validator=truncated_range, values=[1, 2500], cast=int ) trigger_delay = Instrument.control( ":TRIG:SEQ:DEL?", ":TRIG:SEQ:DEL %g", """ A floating point property that controls the trigger delay in seconds, which can take values from 0 to 999.9999 s. """, validator=truncated_range, values=[0, 999.9999] ) ########### # Filters # ########### filter_type = Instrument.control( ":SENS:AVER:TCON?", ":SENS:AVER:TCON %s", """ A String property that controls the filter's type. REP : Repeating filter MOV : Moving filter""", validator=strict_discrete_set, values=['REP', 'MOV'], map_values=False) filter_count = Instrument.control( ":SENS:AVER:COUNT?", ":SENS:AVER:COUNT %d", """ A integer property that controls the number of readings that are acquired and stored in the filter buffer for the averaging""", validator=truncated_range, values=[1, 100], cast=int) filter_state = Instrument.control( ":SENS:AVER?", ":SENS:AVER %s", """ A string property that controls if the filter is active.""", validator=strict_discrete_set, values=['ON', 'OFF'], map_values=False) ##################### # Output subsystem # ##################### output_off_state = Instrument.control( ":OUTP:SMOD?", ":OUTP:SMOD %s", """ Select the output-off state of the SourceMeter. HIMP : output relay is open, disconnects external circuitry. NORM : V-Source is selected and set to 0V, Compliance is set to 0.5% full scale of the present current range. ZERO : V-Source is selected and set to 0V, compliance is set to the programmed Source I value or to 0.5% full scale of the present current range, whichever is greater. GUAR : I-Source is selected and set to 0A""", validator=strict_discrete_set, values=['HIMP', 'NORM', 'ZERO', 'GUAR'], map_values=False) #################### # Methods # #################### def __init__(self, adapter, **kwargs): super(Keithley2400, self).__init__( adapter, "Keithley 2400 SourceMeter", **kwargs ) def enable_source(self): """ Enables the source of current or voltage depending on the configuration of the instrument. """ self.write("OUTPUT ON") def disable_source(self): """ Disables the source of current or voltage depending on the configuration of the instrument. """ self.write("OUTPUT OFF") def measure_resistance(self, nplc=1, resistance=2.1e5, auto_range=True): """ Configures the measurement of resistance. :param nplc: Number of power line cycles (NPLC) from 0.01 to 10 :param resistance: Upper limit of resistance in Ohms, from -210 MOhms to 210 MOhms :param auto_range: Enables auto_range if True, else uses the set resistance """ log.info("%s is measuring resistance." % self.name) self.write(":SENS:FUNC 'RES';" ":SENS:RES:MODE MAN;" ":SENS:RES:NPLC %f;:FORM:ELEM RES;" % nplc) if auto_range: self.write(":SENS:RES:RANG:AUTO 1;") else: self.resistance_range = resistance self.check_errors() def measure_voltage(self, nplc=1, voltage=21.0, auto_range=True): """ Configures the measurement of voltage. :param nplc: Number of power line cycles (NPLC) from 0.01 to 10 :param voltage: Upper limit of voltage in Volts, from -210 V to 210 V :param auto_range: Enables auto_range if True, else uses the set voltage """ log.info("%s is measuring voltage." % self.name) self.write(":SENS:FUNC 'VOLT';" ":SENS:VOLT:NPLC %f;:FORM:ELEM VOLT;" % nplc) if auto_range: self.write(":SENS:VOLT:RANG:AUTO 1;") else: self.voltage_range = voltage self.check_errors() def measure_current(self, nplc=1, current=1.05e-4, auto_range=True): """ Configures the measurement of current. :param nplc: Number of power line cycles (NPLC) from 0.01 to 10 :param current: Upper limit of current in Amps, from -1.05 A to 1.05 A :param auto_range: Enables auto_range if True, else uses the set current """ log.info("%s is measuring current." % self.name) self.write(":SENS:FUNC 'CURR';" ":SENS:CURR:NPLC %f;:FORM:ELEM CURR;" % nplc) if auto_range: self.write(":SENS:CURR:RANG:AUTO 1;") else: self.current_range = current self.check_errors() def auto_range_source(self): """ Configures the source to use an automatic range. """ if self.source_mode == 'current': self.write(":SOUR:CURR:RANG:AUTO 1") else: self.write(":SOUR:VOLT:RANG:AUTO 1") def apply_current(self, current_range=None, compliance_voltage=0.1): """ Configures the instrument to apply a source current, and uses an auto range unless a current range is specified. The compliance voltage is also set. :param compliance_voltage: A float in the correct range for a :attr:`~.Keithley2400.compliance_voltage` :param current_range: A :attr:`~.Keithley2400.current_range` value or None """ log.info("%s is sourcing current." % self.name) self.source_mode = 'current' if current_range is None: self.auto_range_source() else: self.source_current_range = current_range self.compliance_voltage = compliance_voltage self.check_errors() def apply_voltage(self, voltage_range=None, compliance_current=0.1): """ Configures the instrument to apply a source voltage, and uses an auto range unless a voltage range is specified. The compliance current is also set. :param compliance_current: A float in the correct range for a :attr:`~.Keithley2400.compliance_current` :param voltage_range: A :attr:`~.Keithley2400.voltage_range` value or None """ log.info("%s is sourcing voltage." % self.name) self.source_mode = 'voltage' if voltage_range is None: self.auto_range_source() else: self.source_voltage_range = voltage_range self.compliance_current = compliance_current self.check_errors() def beep(self, frequency, duration): """ Sounds a system beep. :param frequency: A frequency in Hz between 65 Hz and 2 MHz :param duration: A time in seconds between 0 and 7.9 seconds """ self.write(":SYST:BEEP %g, %g" % (frequency, duration)) def triad(self, base_frequency, duration): """ Sounds a musical triad using the system beep. :param base_frequency: A frequency in Hz between 65 Hz and 1.3 MHz :param duration: A time in seconds between 0 and 7.9 seconds """ self.beep(base_frequency, duration) time.sleep(duration) self.beep(base_frequency * 5.0 / 4.0, duration) time.sleep(duration) self.beep(base_frequency * 6.0 / 4.0, duration) display_enabled = Instrument.control( ":DISP:ENAB?", ":DISP:ENAB %d", """ A boolean property that controls whether or not the display of the sourcemeter is enabled. Valid values are True and False. """, values={True: 1, False: 0}, map_values=True, ) @property def error(self): """ Returns a tuple of an error code and message from a single error. """ err = self.values(":system:error?") if len(err) < 2: err = self.read() # Try reading again code = err[0] message = err[1].replace('"', '') return (code, message) def check_errors(self): """ Logs any system errors reported by the instrument. """ code, message = self.error while code != 0: t = time.time() log.info("Keithley 2400 reported error: %d, %s" % (code, message)) code, message = self.error if (time.time() - t) > 10: log.warning("Timed out for Keithley 2400 error retrieval.") def reset(self): """ Resets the instrument and clears the queue. """ self.write("status:queue:clear;*RST;:stat:pres;:*CLS;") def ramp_to_current(self, target_current, steps=30, pause=20e-3): """ Ramps to a target current from the set current value over a certain number of linear steps, each separated by a pause duration. :param target_current: A current in Amps :param steps: An integer number of steps :param pause: A pause duration in seconds to wait between steps """ currents = np.linspace( self.source_current, target_current, steps ) for current in currents: self.source_current = current time.sleep(pause) def ramp_to_voltage(self, target_voltage, steps=30, pause=20e-3): """ Ramps to a target voltage from the set voltage value over a certain number of linear steps, each separated by a pause duration. :param target_voltage: A voltage in Amps :param steps: An integer number of steps :param pause: A pause duration in seconds to wait between steps """ voltages = np.linspace( self.source_voltage, target_voltage, steps ) for voltage in voltages: self.source_voltage = voltage time.sleep(pause) def trigger(self): """ Executes a bus trigger, which can be used when :meth:`~.trigger_on_bus` is configured. """ return self.write("*TRG") def trigger_immediately(self): """ Configures measurements to be taken with the internal trigger at the maximum sampling rate. """ self.write(":ARM:SOUR IMM;:TRIG:SOUR IMM;") def trigger_on_bus(self): """ Configures the trigger to detect events based on the bus trigger, which can be activated by :code:`GET` or :code:`*TRG`. """ self.write(":ARM:COUN 1;:ARM:SOUR BUS;:TRIG:SOUR BUS;") def set_trigger_counts(self, arm, trigger): """ Sets the number of counts for both the sweeps (arm) and the points in those sweeps (trigger), where the total number of points can not exceed 2500 """ if arm * trigger > 2500 or arm * trigger < 0: raise RangeException("Keithley 2400 has a combined maximum " "of 2500 counts") if arm < trigger: self.write(":ARM:COUN %d;:TRIG:COUN %d" % (arm, trigger)) else: self.write(":TRIG:COUN %d;:ARM:COUN %d" % (trigger, arm)) def sample_continuously(self): """ Causes the instrument to continuously read samples and turns off any buffer or output triggering """ self.disable_buffer() self.disable_output_trigger() self.trigger_immediately() def set_timed_arm(self, interval): """ Sets up the measurement to be taken with the internal trigger at a variable sampling rate defined by the interval in seconds between sampling points """ if interval > 99999.99 or interval < 0.001: raise RangeException("Keithley 2400 can only be time" " triggered between 1 mS and 1 Ms") self.write(":ARM:SOUR TIM;:ARM:TIM %.3f" % interval) def trigger_on_external(self, line=1): """ Configures the measurement trigger to be taken from a specific line of an external trigger :param line: A trigger line from 1 to 4 """ cmd = ":ARM:SOUR TLIN;:TRIG:SOUR TLIN;" cmd += ":ARM:ILIN %d;:TRIG:ILIN %d;" % (line, line) self.write(cmd) def output_trigger_on_external(self, line=1, after='DEL'): """ Configures the output trigger on the specified trigger link line number, with the option of supplying the part of the measurement after which the trigger should be generated (default to delay, which is right before the measurement) :param line: A trigger line from 1 to 4 :param after: An event string that determines when to trigger """ self.write(":TRIG:OUTP %s;:TRIG:OLIN %d;" % (after, line)) def disable_output_trigger(self): """ Disables the output trigger for the Trigger layer """ self.write(":TRIG:OUTP NONE") @property def mean_voltage(self): """ Returns the mean voltage from the buffer """ return self.means[0] @property def max_voltage(self): """ Returns the maximum voltage from the buffer """ return self.maximums[0] @property def min_voltage(self): """ Returns the minimum voltage from the buffer """ return self.minimums[0] @property def std_voltage(self): """ Returns the voltage standard deviation from the buffer """ return self.standard_devs[0] @property def mean_current(self): """ Returns the mean current from the buffer """ return self.means[1] @property def max_current(self): """ Returns the maximum current from the buffer """ return self.maximums[1] @property def min_current(self): """ Returns the minimum current from the buffer """ return self.minimums[1] @property def std_current(self): """ Returns the current standard deviation from the buffer """ return self.standard_devs[1] @property def mean_resistance(self): """ Returns the mean resistance from the buffer """ return self.means[2] @property def max_resistance(self): """ Returns the maximum resistance from the buffer """ return self.maximums[2] @property def min_resistance(self): """ Returns the minimum resistance from the buffer """ return self.minimums[2] @property def std_resistance(self): """ Returns the resistance standard deviation from the buffer """ return self.standard_devs[2] def status(self): return self.ask("status:queue?;") def RvsI(self, startI, stopI, stepI, compliance, delay=10.0e-3, backward=False): num = int(float(stopI - startI) / float(stepI)) + 1 currRange = 1.2 * max(abs(stopI), abs(startI)) # self.write(":SOUR:CURR 0.0") self.write(":SENS:VOLT:PROT %g" % compliance) self.write(":SOUR:DEL %g" % delay) self.write(":SOUR:CURR:RANG %g" % currRange) self.write(":SOUR:SWE:RANG FIX") self.write(":SOUR:CURR:MODE SWE") self.write(":SOUR:SWE:SPAC LIN") self.write(":SOUR:CURR:STAR %g" % startI) self.write(":SOUR:CURR:STOP %g" % stopI) self.write(":SOUR:CURR:STEP %g" % stepI) self.write(":TRIG:COUN %d" % num) if backward: currents = np.linspace(stopI, startI, num) self.write(":SOUR:SWE:DIR DOWN") else: currents = np.linspace(startI, stopI, num) self.write(":SOUR:SWE:DIR UP") self.connection.timeout = 30.0 self.enable_source() data = self.values(":READ?") self.check_errors() return zip(currents, data) def RvsIaboutZero(self, minI, maxI, stepI, compliance, delay=10.0e-3): data = [] data.extend(self.RvsI(minI, maxI, stepI, compliance=compliance, delay=delay)) data.extend(self.RvsI(minI, maxI, stepI, compliance=compliance, delay=delay, backward=True)) self.disable_source() data.extend(self.RvsI(-minI, -maxI, -stepI, compliance=compliance, delay=delay)) data.extend(self.RvsI(-minI, -maxI, -stepI, compliance=compliance, delay=delay, backward=True)) self.disable_source() return data def use_rear_terminals(self): """ Enables the rear terminals for measurement, and disables the front terminals. """ self.write(":ROUT:TERM REAR") def use_front_terminals(self): """ Enables the front terminals for measurement, and disables the rear terminals. """ self.write(":ROUT:TERM FRON") def shutdown(self): """ Ensures that the current or voltage is turned to zero and disables the output. """ log.info("Shutting down %s." % self.name) if self.source_mode == 'current': self.ramp_to_current(0.0) else: self.ramp_to_voltage(0.0) self.stop_buffer() self.disable_source() PyMeasure-0.9.0/pymeasure/instruments/keithley/keithley2000.py0000664000175000017500000006035714010037617024615 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument from pymeasure.instruments.validators import ( truncated_range, truncated_discrete_set, strict_discrete_set ) from pymeasure.adapters import VISAAdapter from .buffer import KeithleyBuffer class Keithley2000(Instrument, KeithleyBuffer): """ Represents the Keithley 2000 Multimeter and provides a high-level interface for interacting with the instrument. .. code-block:: python meter = Keithley2000("GPIB::1") meter.measure_voltage() print(meter.voltage) """ MODES = { 'current':'CURR:DC', 'current ac':'CURR:AC', 'voltage':'VOLT:DC', 'voltage ac':'VOLT:AC', 'resistance':'RES', 'resistance 4W':'FRES', 'period':'PER', 'frequency':'FREQ', 'temperature':'TEMP', 'diode':'DIOD', 'continuity':'CONT' } mode = Instrument.control( ":CONF?", ":CONF:%s", """ A string property that controls the configuration mode for measurements, which can take the values: :code:'current' (DC), :code:'current ac', :code:'voltage' (DC), :code:'voltage ac', :code:'resistance' (2-wire), :code:'resistance 4W' (4-wire), :code:'period', :code:'frequency', :code:'temperature', :code:'diode', and :code:'frequency'. """, validator=strict_discrete_set, values=MODES, map_values=True, get_process=lambda v: v.replace('"', '') ) beep_state = Instrument.control( ":SYST:BEEP:STAT?", ":SYST:BEEP:STAT %g", """ A string property that enables or disables the system status beeper, which can take the values: :code:'enabled' and :code:'disabled'. """, validator=strict_discrete_set, values={'enabled':1, 'disabled':0}, map_values=True ) ############### # Current (A) # ############### current = Instrument.measurement(":READ?", """ Reads a DC or AC current measurement in Amps, based on the active :attr:`~.Keithley2000.mode`. """ ) current_range = Instrument.control( ":SENS:CURR:RANG?", ":SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG %g", """ A floating point property that controls the DC current range in Amps, which can take values from 0 to 3.1 A. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[0, 3.1] ) current_reference = Instrument.control( ":SENS:CURR:REF?", ":SENS:CURR:REF %g", """ A floating point property that controls the DC current reference value in Amps, which can take values from -3.1 to 3.1 A. """, validator=truncated_range, values=[-3.1, 3.1] ) current_nplc = Instrument.control( ":SENS:CURR:NPLC?", ":SENS:CURR:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the DC current measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) current_digits = Instrument.control( ":SENS:CURR:DIG?", ":SENS:CURR:DIG %d", """ An integer property that controls the number of digits in the DC current readings, which can take values from 4 to 7. """, validator=truncated_discrete_set, values=[4, 5, 6, 7], cast=int, ) current_ac_range = Instrument.control( ":SENS:CURR:AC:RANG?", ":SENS:CURR:AC:RANG:AUTO 0;:SENS:CURR:AC:RANG %g", """ A floating point property that controls the AC current range in Amps, which can take values from 0 to 3.1 A. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[0, 3.1] ) current_ac_reference = Instrument.control( ":SENS:CURR:AC:REF?", ":SENS:CURR:AC:REF %g", """ A floating point property that controls the AC current reference value in Amps, which can take values from -3.1 to 3.1 A. """, validator=truncated_range, values=[-3.1, 3.1] ) current_ac_nplc = Instrument.control( ":SENS:CURR:AC:NPLC?", ":SENS:CURR:AC:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the AC current measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) current_ac_digits = Instrument.control( ":SENS:CURR:AC:DIG?", ":SENS:CURR:AC:DIG %d", """ An integer property that controls the number of digits in the AC current readings, which can take values from 4 to 7. """, validator=truncated_discrete_set, values=[4, 5, 6, 7], cast=int ) current_ac_bandwidth = Instrument.control( ":SENS:CURR:AC:DET:BAND?", ":SENS:CURR:AC:DET:BAND %g", """ A floating point property that sets the AC current detector bandwidth in Hz, which can take the values 3, 30, and 300 Hz. """, validator=truncated_discrete_set, values=[3, 30, 300] ) ############### # Voltage (V) # ############### voltage = Instrument.measurement(":READ?", """ Reads a DC or AC voltage measurement in Volts, based on the active :attr:`~.Keithley2000.mode`. """ ) voltage_range = Instrument.control( ":SENS:VOLT:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG %g", """ A floating point property that controls the DC voltage range in Volts, which can take values from 0 to 1010 V. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[0, 1010] ) voltage_reference = Instrument.control( ":SENS:VOLT:REF?", ":SENS:VOLT:REF %g", """ A floating point property that controls the DC voltage reference value in Volts, which can take values from -1010 to 1010 V. """, validator=truncated_range, values=[-1010, 1010] ) voltage_nplc = Instrument.control( ":SENS:CURRVOLT:NPLC?", ":SENS:VOLT:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the DC voltage measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) voltage_digits = Instrument.control( ":SENS:VOLT:DIG?", ":SENS:VOLT:DIG %d", """ An integer property that controls the number of digits in the DC voltage readings, which can take values from 4 to 7. """, validator=truncated_discrete_set, values=[4, 5, 6, 7], cast=int ) voltage_ac_range = Instrument.control( ":SENS:VOLT:AC:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:AC:RANG %g", """ A floating point property that controls the AC voltage range in Volts, which can take values from 0 to 757.5 V. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[0, 757.5] ) voltage_ac_reference = Instrument.control( ":SENS:VOLT:AC:REF?", ":SENS:VOLT:AC:REF %g", """ A floating point property that controls the AC voltage reference value in Volts, which can take values from -757.5 to 757.5 Volts. """, validator=truncated_range, values=[-757.5, 757.5] ) voltage_ac_nplc = Instrument.control( ":SENS:VOLT:AC:NPLC?", ":SENS:VOLT:AC:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the AC voltage measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) voltage_ac_digits = Instrument.control( ":SENS:VOLT:AC:DIG?", ":SENS:VOLT:AC:DIG %d", """ An integer property that controls the number of digits in the AC voltage readings, which can take values from 4 to 7. """, validator=truncated_discrete_set, values=[4, 5, 6, 7], cast=int ) voltage_ac_bandwidth = Instrument.control( ":SENS:VOLT:AC:DET:BAND?", ":SENS:VOLT:AC:DET:BAND %g", """ A floating point property that sets the AC voltage detector bandwidth in Hz, which can take the values 3, 30, and 300 Hz. """, validator=truncated_discrete_set, values=[3, 30, 300] ) #################### # Resistance (Ohm) # #################### resistance = Instrument.measurement(":READ?", """ Reads a resistance measurement in Ohms for both 2-wire and 4-wire configurations, based on the active :attr:`~.Keithley2000.mode`. """ ) resistance_range = Instrument.control( ":SENS:RES:RANG?", ":SENS:RES:RANG:AUTO 0;:SENS:RES:RANG %g", """ A floating point property that controls the 2-wire resistance range in Ohms, which can take values from 0 to 120 MOhms. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[0, 120e6] ) resistance_reference = Instrument.control( ":SENS:RES:REF?", ":SENS:RES:REF %g", """ A floating point property that controls the 2-wire resistance reference value in Ohms, which can take values from 0 to 120 MOhms. """, validator=truncated_range, values=[0, 120e6] ) resistance_nplc = Instrument.control( ":SENS:RES:NPLC?", ":SENS:RES:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the 2-wire resistance measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) resistance_digits = Instrument.control( ":SENS:RES:DIG?", ":SENS:RES:DIG %d", """ An integer property that controls the number of digits in the 2-wire resistance readings, which can take values from 4 to 7. """, validator=truncated_discrete_set, values=[4, 5, 6, 7], cast=int ) resistance_4W_range = Instrument.control( ":SENS:FRES:RANG?", ":SENS:FRES:RANG:AUTO 0;:SENS:FRES:RANG %g", """ A floating point property that controls the 4-wire resistance range in Ohms, which can take values from 0 to 120 MOhms. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[0, 120e6] ) resistance_4W_reference = Instrument.control( ":SENS:FRES:REF?", ":SENS:FRES:REF %g", """ A floating point property that controls the 4-wire resistance reference value in Ohms, which can take values from 0 to 120 MOhms. """, validator=truncated_range, values=[0, 120e6] ) resistance_4W_nplc = Instrument.control( ":SENS:FRES:NPLC?", ":SENS:FRES:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the 4-wire resistance measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) resistance_4W_digits = Instrument.control( ":SENS:FRES:DIG?", ":SENS:FRES:DIG %d", """ An integer property that controls the number of digits in the 4-wire resistance readings, which can take values from 4 to 7. """, validator=truncated_discrete_set, values=[4, 5, 6, 7], cast=int ) ################## # Frequency (Hz) # ################## frequency = Instrument.measurement(":READ?", """ Reads a frequency measurement in Hz, based on the active :attr:`~.Keithley2000.mode`. """ ) frequency_reference = Instrument.control( ":SENS:FREQ:REF?", ":SENS:FREQ:REF %g", """ A floating point property that controls the frequency reference value in Hz, which can take values from 0 to 15 MHz. """, validator=truncated_range, values=[0, 15e6] ) frequency_digits = Instrument.control( ":SENS:FREQ:DIG?", ":SENS:FREQ:DIG %d", """ An integer property that controls the number of digits in the frequency readings, which can take values from 4 to 7. """, validator=truncated_discrete_set, values=[4, 5, 6, 7], cast=int ) frequency_threshold = Instrument.control( ":SENS:FREQ:THR:VOLT:RANG?", ":SENS:FREQ:THR:VOLT:RANG %g", """ A floating point property that controls the voltage signal threshold level in Volts for the frequency measurement, which can take values from 0 to 1010 V. """, validator=truncated_range, values=[0, 1010] ) frequency_aperature = Instrument.control( ":SENS:FREQ:APER?", ":SENS:FREQ:APER %g", """ A floating point property that controls the frequency aperature in seconds, which sets the integration period and measurement speed. Takes values from 0.01 to 1.0 s. """, validator=truncated_range, values=[0.01, 1.0] ) ############## # Period (s) # ############## period = Instrument.measurement(":READ?", """ Reads a period measurement in seconds, based on the active :attr:`~.Keithley2000.mode`. """ ) period_reference = Instrument.control( ":SENS:PER:REF?", ":SENS:PER:REF %g", """ A floating point property that controls the period reference value in seconds, which can take values from 0 to 1 s. """, validator=truncated_range, values=[0, 1] ) period_digits = Instrument.control( ":SENS:PER:DIG?", ":SENS:PER:DIG %d", """ An integer property that controls the number of digits in the period readings, which can take values from 4 to 7. """, validator=truncated_discrete_set, values=[4, 5, 6, 7], cast=int ) period_threshold = Instrument.control( ":SENS:PER:THR:VOLT:RANG?", ":SENS:PRE:THR:VOLT:RANG %g", """ A floating point property that controls the voltage signal threshold level in Volts for the period measurement, which can take values from 0 to 1010 V. """, validator=truncated_range, values=[0, 1010] ) period_aperature = Instrument.control( ":SENS:PER:APER?", ":SENS:PER:APER %g", """ A floating point property that controls the period aperature in seconds, which sets the integration period and measurement speed. Takes values from 0.01 to 1.0 s. """, validator=truncated_range, values=[0.01, 1.0] ) ################### # Temperature (C) # ################### temperature = Instrument.measurement(":READ?", """ Reads a temperature measurement in Celsius, based on the active :attr:`~.Keithley2000.mode`. """ ) temperature_reference = Instrument.control( ":SENS:TEMP:REF?", ":SENS:TEMP:REF %g", """ A floating point property that controls the temperature reference value in Celsius, which can take values from -200 to 1372 C. """, validator=truncated_range, values=[-200, 1372] ) temperature_nplc = Instrument.control( ":SENS:TEMP:NPLC?", ":SENS:TEMP:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the temperature measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) temperature_digits = Instrument.control( ":SENS:TEMP:DIG?", ":SENS:TEMP:DIG %d", """ An integer property that controls the number of digits in the temperature readings, which can take values from 4 to 7. """, validator=truncated_discrete_set, values=[4, 5, 6, 7], cast=int ) ########### # Trigger # ########### trigger_count = Instrument.control( ":TRIG:COUN?", ":TRIG:COUN %d", """ An integer property that controls the trigger count, which can take values from 1 to 9,999. """, validator=truncated_range, values=[1, 9999], cast=int ) trigger_delay = Instrument.control( ":TRIG:SEQ:DEL?", ":TRIG:SEQ:DEL %g", """ A floating point property that controls the trigger delay in seconds, which can take values from 1 to 9,999,999.999 s. """, validator=truncated_range, values=[0, 999999.999] ) def __init__(self, adapter, **kwargs): super(Keithley2000, self).__init__( adapter, "Keithley 2000 Multimeter", **kwargs ) # Set up data transfer format if isinstance(self.adapter, VISAAdapter): self.adapter.config( is_binary=False, datatype='float32', converter='f', separator=',' ) # TODO: Clean up error checking def check_errors(self): """ Read all errors from the instrument.""" while True: err = self.values(":SYST:ERR?") if int(err[0]) != 0: errmsg = "Keithley 2000: %s: %s" % (err[0],err[1]) log.error(errmsg + '\n') else: break def measure_voltage(self, max_voltage=1, ac=False): """ Configures the instrument to measure voltage, based on a maximum voltage to set the range, and a boolean flag to determine if DC or AC is required. :param max_voltage: A voltage in Volts to set the voltage range :param ac: False for DC voltage, and True for AC voltage """ if ac: self.mode = 'voltage ac' self.voltage_ac_range = max_voltage else: self.mode = 'voltage' self.voltage_range = max_voltage def measure_current(self, max_current=10e-3, ac=False): """ Configures the instrument to measure current, based on a maximum current to set the range, and a boolean flag to determine if DC or AC is required. :param max_current: A current in Volts to set the current range :param ac: False for DC current, and True for AC current """ if ac: self.mode = 'current ac' self.current_ac_range = max_current else: self.mode = 'current' self.current_range = max_current def measure_resistance(self, max_resistance=10e6, wires=2): """ Configures the instrument to measure voltage, based on a maximum voltage to set the range, and a boolean flag to determine if DC or AC is required. :param max_voltage: A voltage in Volts to set the voltage range :param ac: False for DC voltage, and True for AC voltage """ if wires == 2: self.mode = 'resistance' self.resistance_range = max_resistance elif wires == 4: self.mode = 'resistance 4W' self.resistance_4W_range = max_resistance else: raise ValueError("Keithley 2000 only supports 2 or 4 wire" "resistance meaurements.") def measure_period(self): """ Configures the instrument to measure the period. """ self.mode = 'period' def measure_frequency(self): """ Configures the instrument to measure the frequency. """ self.mode = 'frequency' def measure_temperature(self): """ Configures the instrument to measure the temperature. """ self.mode = 'temperature' def measure_diode(self): """ Configures the instrument to perform diode testing. """ self.mode = 'diode' def measure_continuity(self): """ Configures the instrument to perform continuity testing. """ self.mode = 'continuity' def _mode_command(self, mode=None): if mode is None: mode = self.mode return self.MODES[mode] def auto_range(self, mode=None): """ Sets the active mode to use auto-range, or can set another mode by its name. :param mode: A valid :attr:`~.Keithley2000.mode` name, or None for the active mode """ self.write(":SENS:%s:RANG:AUTO 1" % self._mode_command(mode)) def enable_reference(self, mode=None): """ Enables the reference for the active mode, or can set another mode by its name. :param mode: A valid :attr:`~.Keithley2000.mode` name, or None for the active mode """ self.write(":SENS:%s:REF:STAT 1" % self._mode_command(mode)) def disable_reference(self, mode=None): """ Disables the reference for the active mode, or can set another mode by its name. :param mode: A valid :attr:`~.Keithley2000.mode` name, or None for the active mode """ self.write(":SENS:%s:REF:STAT 0" % self._mode_command(mode)) def acquire_reference(self, mode=None): """ Sets the active value as the reference for the active mode, or can set another mode by its name. :param mode: A valid :attr:`~.Keithley2000.mode` name, or None for the active mode """ self.write(":SENS:%s:REF:ACQ" % self._mode_command(mode)) def enable_filter(self, mode=None, type='repeat', count=1): """ Enables the averaging filter for the active mode, or can set another mode by its name. :param mode: A valid :attr:`~.Keithley2000.mode` name, or None for the active mode :param type: The type of averaging filter, either 'repeat' or 'moving'. :param count: A number of averages, which can take take values from 1 to 100 """ self.write(":SENS:%s:AVER:STAT 1") self.write(":SENS:%s:AVER:TCON %s") self.write(":SENS:%s:AVER:COUN %d") def disable_filter(self, mode=None): """ Disables the averaging filter for the active mode, or can set another mode by its name. :param mode: A valid :attr:`~.Keithley2000.mode` name, or None for the active mode """ self.write(":SENS:%s:AVER:STAT 0" % self._mode_command(mode)) def local(self): """ Returns control to the instrument panel, and enables the panel if disabled. """ self.write(":SYST:LOC") def remote(self): """ Places the instrument in the remote state, which is does not need to be explicity called in general. """ self.write(":SYST:REM") def remote_lock(self): """ Disables and locks the front panel controls to prevent changes during remote operations. This is disabled by calling :meth:`~.Keithley2000.local`. """ self.write(":SYST:RWL") def reset(self): """ Resets the instrument state. """ self.write(":STAT:QUEUE:CLEAR;*RST;:STAT:PRES;:*CLS;") def beep(self, frequency, duration): """ Sounds a system beep. :param frequency: A frequency in Hz between 65 Hz and 2 MHz :param duration: A time in seconds between 0 and 7.9 seconds """ self.write(":SYST:BEEP %g, %g" % (frequency, duration)) PyMeasure-0.9.0/pymeasure/instruments/keithley/keithley6221.py0000664000175000017500000004540414010037617024622 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument, RangeException from pymeasure.adapters import PrologixAdapter from pymeasure.instruments.validators import truncated_range, strict_discrete_set from .buffer import KeithleyBuffer import numpy as np import time from io import BytesIO import re class Keithley6221(Instrument, KeithleyBuffer): """ Represents the Keithely 6221 AC and DC current source and provides a high-level interface for interacting with the instrument. .. code-block:: python keithley = Keithley6221("GPIB::1") keithley.clear() # Use the keithley as an AC source keithley.waveform_function = "square" # Set a square waveform keithley.waveform_amplitude = 0.05 # Set the amplitude in Amps keithley.waveform_offset = 0 # Set zero offset keithley.source_compliance = 10 # Set compliance (limit) in V keithley.waveform_dutycycle = 50 # Set duty cycle of wave in % keithley.waveform_frequency = 347 # Set the frequency in Hz keithley.waveform_ranging = "best" # Set optimal output ranging keithley.waveform_duration_cycles = 100 # Set duration of the waveform # Link end of waveform to Service Request status bit keithley.operation_event_enabled = 128 # OSB listens to end of wave keithley.srq_event_enabled = 128 # SRQ listens to OSB keithley.waveform_arm() # Arm (load) the waveform keithley.waveform_start() # Start the waveform keithley.adapter.wait_for_srq() # Wait for the pulse to finish keithley.waveform_abort() # Disarm (unload) the waveform keithley.shutdown() # Disables output """ ########## # OUTPUT # ########## source_enabled = Instrument.control( "OUTPut?", "OUTPut %d", """A boolean property that controls whether the source is enabled, takes values True or False. The convenience methods :meth:`~.Keithley6221.enable_source` and :meth:`~.Keithley6221.disable_source` can also be used.""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) source_delay = Instrument.control( ":SOUR:DEL?", ":SOUR:DEL %g", """ A floating point property that sets a manual delay for the source after the output is turned on before a measurement is taken. When this property is set, the auto delay is turned off. Valid values are between 1e-3 [seconds] and 999999.999 [seconds].""", validator=truncated_range, values=[1e-3, 999999.999], ) ########## # SOURCE # ########## source_current = Instrument.control( ":SOUR:CURR?", ":SOUR:CURR %g", """ A floating point property that controls the source current in Amps. """, validator=truncated_range, values=[-0.105, 0.105] ) source_compliance = Instrument.control( ":SOUR:CURR:COMP?", ":SOUR:CURR:COMP %g", """A floating point property that controls the compliance of the current source in Volts. valid values are in range 0.1 [V] to 105 [V].""", validator=truncated_range, values=[0.1, 105]) source_range = Instrument.control( ":SOUR:CURR:RANG?", ":SOUR:CURR:RANG:AUTO 0;:SOUR:CURR:RANG %g", """ A floating point property that controls the source current range in Amps, which can take values between -0.105 A and +0.105 A. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-0.105, 0.105] ) source_auto_range = Instrument.control( ":SOUR:CURR:RANG:AUTO?", ":SOUR:CURR:RANG:AUTO %d", """ A boolean property that controls the auto range of the current source. Valid values are True or False. """, values={True: 1, False: 0}, map_values=True, ) ################## # WAVE FUNCTIONS # ################## waveform_function = Instrument.control( ":SOUR:WAVE:FUNC?", ":SOUR:WAVE:FUNC %s", """ A string property that controls the selected wave function. Valid values are "sine", "ramp", "square", "arbitrary1", "arbitrary2", "arbitrary3" and "arbitrary4". """, values={ "sine": "SIN", "ramp": "RAMP", "square": "SQU", "arbitrary1": "ARB1", "arbitrary2": "ARB2", "arbitrary3": "ARB3", "arbitrary4": "ARB4", }, map_values=True ) waveform_frequency = Instrument.control( ":SOUR:WAVE:FREQ?", ":SOUR:WAVE:FREQ %g", """A floating point property that controls the frequency of the waveform in Hertz. Valid values are in range 1e-3 to 1e5. """, validator=truncated_range, values=[1e-3, 1e5] ) waveform_amplitude = Instrument.control( ":SOUR:WAVE:AMPL?", ":SOUR:WAVE:AMPL %g", """A floating point property that controls the (peak) amplitude of the waveform in Amps. Valid values are in range 2e-12 to 0.105. """, validator=truncated_range, values=[2e-12, 0.105] ) waveform_offset = Instrument.control( ":SOUR:WAVE:OFFS?", ":SOUR:WAVE:OFFS %g", """A floating point property that controls the offset of the waveform in Amps. Valid values are in range -0.105 to 0.105. """, validator=truncated_range, values=[-0.105, 0.105] ) waveform_dutycycle = Instrument.control( ":SOUR:WAVE:DCYC?", ":SOUR:WAVE:DCYC %g", """A floating point property that controls the duty-cycle of the waveform in percent for the square and ramp waves. Valid values are in range 0 to 100. """, validator=truncated_range, values=[0, 100] ) waveform_duration_time = Instrument.control( ":SOUR:WAVE:DUR:TIME?", ":SOUR:WAVE:DUR:TIME %g", """A floating point property that controls the duration of the waveform in seconds. Valid values are in range 100e-9 to 999999.999. """, validator=truncated_range, values=[100e-9, 999999.999] ) waveform_duration_cycles = Instrument.control( ":SOUR:WAVE:DUR:CYCL?", ":SOUR:WAVE:DUR:CYCL %g", """A floating point property that controls the duration of the waveform in cycles. Valid values are in range 1e-3 to 99999999900. """, validator=truncated_range, values=[1e-3, 99999999900] ) def waveform_duration_set_infinity(self): """ Set the waveform duration to infinity. """ self.write(":SOUR:WAVE:DUR:TIME INF") waveform_ranging = Instrument.control( ":SOUR:WAVE:RANG?", ":SOUR:WAVE:RANG %s", """ A string property that controls the source ranging of the waveform. Valid values are "best" and "fixed". """, values={"best": "BEST", "fixed": "FIX"}, map_values=True, ) waveform_use_phasemarker = Instrument.control( ":SOUR:WAVE:PMAR:STAT?", ":SOUR:WAVE:PMAR:STAT %s", """ A boolean property that controls whether the phase marker option is turned on or of. Valid values True (on) or False (off). Other settings for the phase marker have not yet been implemented.""", values={True: 1, False: 0}, map_values=True, ) def waveform_arm(self): """ Arm the current waveform function. """ self.write(":SOUR:WAVE:ARM") def waveform_start(self): """ Start the waveform output. Must already be armed """ self.write(":SOUR:WAVE:INIT") def waveform_abort(self): """ Abort the waveform output and disarm the waveform function. """ self.write(":SOUR:WAVE:ABOR") def define_arbitary_waveform(self, datapoints, location=1): """ Define the data points for the arbitrary waveform and copy the defined waveform into the given storage location. :param datapoints: a list (or numpy array) of the data points; all values have to be between -1 and 1; 100 points maximum. :param location: integer storage location to store the waveform in. Value must be in range 1 to 4. """ # Check validity of parameters if not isinstance(datapoints, (list, np.ndarray)): raise ValueError("datapoints must be a list or numpy array") elif len(datapoints) > 100: raise ValueError("datapoints cannot be longer than 100 points") elif not all([x >= -1 and x <= 1 for x in datapoints]): raise ValueError("all data points must be between -1 and 1") if location not in [1, 2, 3, 4]: raise ValueError("location must be in [1, 2, 3, 4]") # Make list of strings datapoints = [str(x) for x in datapoints] data = ", ".join(datapoints) # Write the data points to the Keithley 6221 self.write(":SOUR:WAVE:ARB:DATA %s" % data) # Copy the written data to the specified location self.write(":SOUR:WAVE:ARB:COPY %d" % location) # Select the newly made arbitrary waveform as waveform function self.waveform_function = "arbitrary%d" % location def __init__(self, adapter, **kwargs): super(Keithley6221, self).__init__( adapter, "Keithley 6221 SourceMeter", **kwargs ) def enable_source(self): """ Enables the source of current or voltage depending on the configuration of the instrument. """ self.write("OUTPUT ON") def disable_source(self): """ Disables the source of current or voltage depending on the configuration of the instrument. """ self.write("OUTPUT OFF") def beep(self, frequency, duration): """ Sounds a system beep. :param frequency: A frequency in Hz between 65 Hz and 2 MHz :param duration: A time in seconds between 0 and 7.9 seconds """ self.write(":SYST:BEEP %g, %g" % (frequency, duration)) def triad(self, base_frequency, duration): """ Sounds a musical triad using the system beep. :param base_frequency: A frequency in Hz between 65 Hz and 1.3 MHz :param duration: A time in seconds between 0 and 7.9 seconds """ self.beep(base_frequency, duration) time.sleep(duration) self.beep(base_frequency * 5.0 / 4.0, duration) time.sleep(duration) self.beep(base_frequency * 6.0 / 4.0, duration) display_enabled = Instrument.control( ":DISP:ENAB?", ":DISP:ENAB %d", """ A boolean property that controls whether or not the display of the sourcemeter is enabled. Valid values are True and False. """, values={True: 1, False: 0}, map_values=True, ) @property def error(self): """ Returns a tuple of an error code and message from a single error. """ err = self.values(":system:error?") if len(err) < 2: err = self.read() # Try reading again code = err[0] message = err[1].replace('"', '') return (code, message) def check_errors(self): """ Logs any system errors reported by the instrument. """ code, message = self.error while code != 0: t = time.time() log.info("Keithley 6221 reported error: %d, %s" % (code, message)) code, message = self.error if (time.time() - t) > 10: log.warning("Timed out for Keithley 6221 error retrieval.") def reset(self): """ Resets the instrument and clears the queue. """ self.write("status:queue:clear;*RST;:stat:pres;:*CLS;") def trigger(self): """ Executes a bus trigger, which can be used when :meth:`~.trigger_on_bus` is configured. """ return self.write("*TRG") def trigger_immediately(self): """ Configures measurements to be taken with the internal trigger at the maximum sampling rate. """ self.write(":ARM:SOUR IMM;:TRIG:SOUR IMM;") def trigger_on_bus(self): """ Configures the trigger to detect events based on the bus trigger, which can be activated by :code:`GET` or :code:`*TRG`. """ self.write(":ARM:SOUR BUS;:TRIG:SOUR BUS;") def set_timed_arm(self, interval): """ Sets up the measurement to be taken with the internal trigger at a variable sampling rate defined by the interval in seconds between sampling points """ if interval > 99999.99 or interval < 0.001: raise RangeException("Keithley 6221 can only be time" " triggered between 1 mS and 1 Ms") self.write(":ARM:SOUR TIM;:ARM:TIM %.3f" % interval) def trigger_on_external(self, line=1): """ Configures the measurement trigger to be taken from a specific line of an external trigger :param line: A trigger line from 1 to 4 """ cmd = ":ARM:SOUR TLIN;:TRIG:SOUR TLIN;" cmd += ":ARM:ILIN %d;:TRIG:ILIN %d;" % (line, line) self.write(cmd) def output_trigger_on_external(self, line=1, after='DEL'): """ Configures the output trigger on the specified trigger link line number, with the option of supplying the part of the measurement after which the trigger should be generated (default to delay, which is right before the measurement) :param line: A trigger line from 1 to 4 :param after: An event string that determines when to trigger """ self.write(":TRIG:OUTP %s;:TRIG:OLIN %d;" % (after, line)) def disable_output_trigger(self): """ Disables the output trigger for the Trigger layer """ self.write(":TRIG:OUTP NONE") def shutdown(self): """ Disables the output. """ log.info("Shutting down %s." % self.name) self.disable_source() ############### # Status bits # ############### measurement_event_enabled = Instrument.control( ":STAT:MEAS:ENAB?", ":STAT:MEAS:ENAB %d", """ An integer value that controls which measurement events are registered in the Measurement Summary Bit (MSB) status bit. Refer to the Model 6220/6221 Reference Manual for more information about programming the status bits. """, cast=int, validator=truncated_range, values=[0, 65535], ) operation_event_enabled = Instrument.control( ":STAT:OPER:ENAB?", ":STAT:OPER:ENAB %d", """ An integer value that controls which operation events are registered in the Operation Summary Bit (OSB) status bit. Refer to the Model 6220/6221 Reference Manual for more information about programming the status bits. """, cast=int, validator=truncated_range, values=[0, 65535], ) questionable_event_enabled = Instrument.control( ":STAT:QUES:ENAB?", ":STAT:QUES:ENAB %d", """ An integer value that controls which questionable events are registered in the Questionable Summary Bit (QSB) status bit. Refer to the Model 6220/6221 Reference Manual for more information about programming the status bits. """, cast=int, validator=truncated_range, values=[0, 65535], ) standard_event_enabled = Instrument.control( "ESE?", "ESE %d", """ An integer value that controls which standard events are registered in the Event Summary Bit (ESB) status bit. Refer to the Model 6220/6221 Reference Manual for more information about programming the status bits. """, cast=int, validator=truncated_range, values=[0, 65535], ) srq_event_enabled = Instrument.control( "*SRE?", "*SRE %d", """ An integer value that controls which event registers trigger the Service Request (SRQ) status bit. Refer to the Model 6220/6221 Reference Manual for more information about programming the status bits. """, cast=int, validator=truncated_range, values=[0, 255], ) measurement_events = Instrument.measurement( ":STAT:MEAS?", """ An integer value that reads which measurement events have been registered in the Measurement event registers. Refer to the Model 6220/6221 Reference Manual for more information about programming the status bits. Reading this value clears the register. """, cast=int, ) operation_events = Instrument.measurement( ":STAT:OPER?", """ An integer value that reads which operation events have been registered in the Operation event registers. Refer to the Model 6220/6221 Reference Manual for more information about programming the status bits. Reading this value clears the register. """, cast=int, ) questionable_events = Instrument.measurement( ":STAT:QUES?", """ An integer value that reads which questionable events have been registered in the Questionable event registers. Refer to the Model 6220/6221 Reference Manual for more information about programming the status bits. Reading this value clears the register. """, cast=int, ) standard_events = Instrument.measurement( "*ESR?", """ An integer value that reads which standard events have been registered in the Standard event registers. Refer to the Model 6220/6221 Reference Manual for more information about programming the status bits. Reading this value clears the register. """, cast=int, ) PyMeasure-0.9.0/pymeasure/instruments/keithley/keithley6517b.py0000664000175000017500000003052514010037617024772 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import time import re import numpy as np from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range from .buffer import KeithleyBuffer log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Keithley6517B(Instrument, KeithleyBuffer): """ Represents the Keithely 6517B ElectroMeter and provides a high-level interface for interacting with the instrument. .. code-block:: python keithley = Keithley6517B("GPIB::1") keithley.apply_voltage() # Sets up to source current keithley.source_voltage_range = 200 # Sets the source voltage # range to 200 V keithley.source_voltage = 20 # Sets the source voltage to 20 V keithley.enable_source() # Enables the source output keithley.measure_resistance() # Sets up to measure resistance keithley.ramp_to_voltage(50) # Ramps the voltage to 50 V print(keithley.resistance) # Prints the resistance in Ohms keithley.shutdown() # Ramps the voltage to 0 V # and disables output """ source_enabled = Instrument.measurement( "OUTPUT?", """ Reads a boolean value that is True if the source is enabled. """, cast=bool ) def extract_value(self, result): """ extracts the physical value from a result object returned by the instrument """ m = re.fullmatch(r'([+\-0-9E.]+)[A-Z]{4}', result[0]) if m: return float(m.group(1)) return None ############### # Current (A) # ############### current = Instrument.measurement( ":MEAS?", """ Reads the current in Amps, if configured for this reading. """, get_process=extract_value ) current_range = Instrument.control( ":SENS:CURR:RANG?", ":SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG %g", """ A floating point property that controls the measurement current range in Amps, which can take values between -20 and +20 mA. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-20e-3, 20e-3] ) current_nplc = Instrument.control( ":SENS:CURR:NPLC?", ":SENS:CURR:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the DC current measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """, values=[0.01, 10] ) source_current_resistance_limit = Instrument.control( ":SOUR:CURR:RLIM?", ":SOUR:CURR:RLIM %g", """ Boolean property which enables or disables resistance current limit """, cast=bool ) ############### # Voltage (V) # ############### voltage = Instrument.measurement( ":MEAS:VOLT?", """ Reads the voltage in Volts, if configured for this reading. """, get_process=extract_value ) voltage_range = Instrument.control( ":SENS:VOLT:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG %g", """ A floating point property that controls the measurement voltage range in Volts, which can take values from -1000 to 1000 V. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-1000, 1000] ) voltage_nplc = Instrument.control( ":SENS:VOLT:NPLC?", ":SENS:VOLT:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the DC voltage measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) source_voltage = Instrument.control( ":SOUR:VOLT?", ":SOUR:VOLT:LEV %g", """ A floating point property that controls the source voltage in Volts. """ ) source_voltage_range = Instrument.control( ":SOUR:VOLT:RANG?", ":SOUR:VOLT:RANG:AUTO 0;:SOUR:VOLT:RANG %g", """ A floating point property that controls the source voltage range in Volts, which can take values from -1000 to 1000 V. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-1000, 1000] ) #################### # Resistance (Ohm) # #################### resistance = Instrument.measurement( ":READ?", """ Reads the resistance in Ohms, if configured for this reading. """, get_process=extract_value ) resistance_range = Instrument.control( ":SENS:RES:RANG?", ":SENS:RES:RANG:AUTO 0;:SENS:RES:RANG %g", """ A floating point property that controls the resistance range in Ohms, which can take values from 0 to 100e18 Ohms. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[0, 100e18] ) resistance_nplc = Instrument.control( ":SENS:RES:NPLC?", ":SENS:RES:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the 2-wire resistance measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) buffer_points = Instrument.control( ":TRAC:POIN?", ":TRAC:POIN %d", """ An integer property that controls the number of buffer points. This does not represent actual points in the buffer, but the configuration value instead. """, validator=truncated_range, values=[1, 6875000], cast=int ) #################### # Methods # #################### def __init__(self, adapter, **kwargs): super(Keithley6517B, self).__init__( adapter, "Keithley 6517B Electrometer/High Resistance Meter", **kwargs ) def enable_source(self): """ Enables the source of current or voltage depending on the configuration of the instrument. """ self.write("OUTPUT ON") def disable_source(self): """ Disables the source of current or voltage depending on the configuration of the instrument. """ self.write("OUTPUT OFF") def measure_resistance(self, nplc=1, resistance=2.1e5, auto_range=True): """ Configures the measurement of resistance. :param nplc: Number of power line cycles (NPLC) from 0.01 to 10 :param resistance: Upper limit of resistance in Ohms, from -210 POhms to 210 POhms :param auto_range: Enables auto_range if True, else uses the resistance_range attribut """ log.info("%s is measuring resistance.", self.name) self.write(":SENS:FUNC 'RES';" ":SENS:RES:NPLC %f;" % nplc) if auto_range: self.write(":SENS:RES:RANG:AUTO 1;") else: self.resistance_range = resistance self.check_errors() def measure_voltage(self, nplc=1, voltage=21.0, auto_range=True): """ Configures the measurement of voltage. :param nplc: Number of power line cycles (NPLC) from 0.01 to 10 :param voltage: Upper limit of voltage in Volts, from -1000 V to 1000 V :param auto_range: Enables auto_range if True, else uses the voltage_range attribut """ log.info("%s is measuring voltage.", self.name) self.write(":SENS:FUNC 'VOLT';" ":SENS:VOLT:NPLC %f;" % nplc) if auto_range: self.write(":SENS:VOLT:RANG:AUTO 1;") else: self.voltage_range = voltage self.check_errors() def measure_current(self, nplc=1, current=1.05e-4, auto_range=True): """ Configures the measurement of current. :param nplc: Number of power line cycles (NPLC) from 0.01 to 10 :param current: Upper limit of current in Amps, from -21 mA to 21 mA :param auto_range: Enables auto_range if True, else uses the current_range attribut """ log.info("%s is measuring current.", self.name) self.write(":SENS:FUNC 'CURR';" ":SENS:CURR:NPLC %f;" % nplc) if auto_range: self.write(":SENS:CURR:RANG:AUTO 1;") else: self.current_range = current self.check_errors() def auto_range_source(self): """ Configures the source to use an automatic range. """ self.write(":SOUR:VOLT:RANG:AUTO 1") def apply_voltage(self, voltage_range=None): """ Configures the instrument to apply a source voltage, and uses an auto range unless a voltage range is specified. :param voltage_range: A :attr:`~.Keithley6517B.voltage_range` value or None (activates auto range) """ log.info("%s is sourcing voltage.", self.name) if voltage_range is None: self.auto_range_source() else: self.source_voltage_range = voltage_range self.check_errors() @property def error(self): """ Returns a tuple of an error code and message from a single error. """ err = self.values(":system:error?") if len(err) < 2: err = self.read() # Try reading again code = err[0] message = err[1].replace('"', '') return (code, message) def check_errors(self): """ Logs any system errors reported by the instrument. """ code, message = self.error while code != 0: t = time.time() log.info("Keithley 6517B reported error: %d, %s", code, message) code, message = self.error if (time.time()-t) > 10: log.warning("Timed out for Keithley 6517B error retrieval.") def reset(self): """ Resets the instrument and clears the queue. """ self.write("*RST;:stat:pres;:*CLS;") def ramp_to_voltage(self, target_voltage, steps=30, pause=20e-3): """ Ramps to a target voltage from the set voltage value over a certain number of linear steps, each separated by a pause duration. :param target_voltage: A voltage in Volts :param steps: An integer number of steps :param pause: A pause duration in seconds to wait between steps """ voltages = np.linspace( self.source_voltage, target_voltage, steps ) for voltage in voltages: self.source_voltage = voltage time.sleep(pause) def trigger(self): """ Executes a bus trigger, which can be used when :meth:`~.trigger_on_bus` is configured. """ return self.write("*TRG") def shutdown(self): """ Ensures that the current or voltage is turned to zero and disables the output. """ log.info("Shutting down %s.", self.name) self.ramp_to_voltage(0.0) self.stop_buffer() self.disable_source() PyMeasure-0.9.0/pymeasure/instruments/keithley/__init__.py0000664000175000017500000000263014010037617024222 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .keithley2000 import Keithley2000 from .keithley2400 import Keithley2400 from .keithley2450 import Keithley2450 from .keithley2700 import Keithley2700 from .keithley6221 import Keithley6221 from .keithley2750 import Keithley2750 from .keithley6517b import Keithley6517B PyMeasure-0.9.0/pymeasure/instruments/keithley/keithley2700.py0000664000175000017500000003136114010037617024615 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range, strict_discrete_set from .buffer import KeithleyBuffer import numpy as np import time from io import BytesIO import re def clist_validator(value, values): """ Provides a validator function that returns a valid clist string for channel commands of the Keithley 2700. Otherwise it raises a ValueError. :param value: A value to test :param values: A range of values (range, list, etc.) :raises: ValueError if the value is out of the range """ # Convert value to list of strings if isinstance(value, str): clist = [value.strip(" @(),")] elif isinstance(value, (int, float)): clist = ["{:d}".format(value)] elif isinstance(value, (list, tuple, np.ndarray, range)): clist = ["{:d}".format(x) for x in value] else: raise ValueError("Type of value ({}) not valid".format(type(value))) # Pad numbers to length (if required) clist = [c.rjust(2, "0") for c in clist] clist = [c.rjust(3, "1") for c in clist] # Check channels against valid channels for c in clist: if int(c) not in values: raise ValueError( "Channel number {:g} not valid.".format(value) ) # Convert list of strings to clist format clist = "(@{:s})".format(", ".join(clist)) return clist def text_length_validator(value, values): """ Provides a validator function that a valid string for the display commands of the Keithley. Raises a TypeError if value is not a string. If the string is too long, it is truncated to the correct length. :param value: A value to test :param values: The allowed length of the text """ if not isinstance(value, str): raise TypeError("Value is not a string.") return value[:values] class Keithley2700(Instrument, KeithleyBuffer): """ Represents the Keithely 2700 Multimeter/Switch System and provides a high-level interface for interacting with the instrument. .. code-block:: python keithley = Keithley2700("GPIB::1") """ CLIST_VALUES = list(range(101, 300)) # Routing commands closed_channels = Instrument.control( "ROUTe:MULTiple:CLOSe?", "ROUTe:MULTiple:CLOSe %s", """ Parameter that controls the opened and closed channels. All mentioned channels are closed, other channels will be opened. """, validator=clist_validator, values=CLIST_VALUES, check_get_errors=True, check_set_errors=True, separator=None, get_process=lambda v: [ int(vv) for vv in (v.strip(" ()@,").split(",")) if not vv == "" ], ) open_channels = Instrument.setting( "ROUTe:MULTiple:OPEN %s", """ A parameter that opens the specified list of channels. Can only be set. """, validator=clist_validator, values=CLIST_VALUES, check_set_errors=True ) def get_state_of_channels(self, channels): """ Get the open or closed state of the specified channels :param channels: a list of channel numbers, or single channel number """ clist = clist_validator(channels, self.CLIST_VALUES) state = self.ask("ROUTe:MULTiple:STATe? %s" % clist) return state def open_all_channels(self): """ Open all channels of the Keithley 2700. """ self.write(":ROUTe:OPEN:ALL") def __init__(self, adapter, **kwargs): super(Keithley2700, self).__init__( adapter, "Keithley 2700 MultiMeter/Switch System", **kwargs ) self.check_errors() self.determine_valid_channels() def determine_valid_channels(self): """ Determine what cards are installed into the Keithley 2700 and from that determine what channels are valid. """ self.CLIST_VALUES.clear() self.cards = {slot: card for slot, card in enumerate(self.options, 1)} for slot, card in self.cards.items(): if card == "none": continue elif card == "7709": """The 7709 is a 6(rows) x 8(columns) matrix card, with two additional switches (49 & 50) that allow row 1 and 2 to be connected to the DMM backplane (input and sense respectively). """ channels = range(1, 51) else: log.warning( "Card type %s at slot %s is not yet implemented." % (card, slot) ) continue channels = [100 * slot + ch for ch in channels] self.CLIST_VALUES.extend(channels) def close_rows_to_columns(self, rows, columns, slot=None): """ Closes (connects) the channels between column(s) and row(s) of the 7709 connection matrix. Only one of the parameters 'rows' or 'columns' can be "all" :param rows: row number or list of numbers; can also be "all" :param columns: column number or list of numbers; can also be "all" :param slot: slot number (1 or 2) of the 7709 card to be used """ channels = self.channels_from_rows_columns(rows, columns, slot) self.closed_channels = channels def open_rows_to_columns(self, rows, columns, slot=None): """ Opens (disconnects) the channels between column(s) and row(s) of the 7709 connection matrix. Only one of the parameters 'rows' or 'columns' can be "all" :param rows: row number or list of numbers; can also be "all" :param columns: column number or list of numbers; can also be "all" :param slot: slot number (1 or 2) of the 7709 card to be used """ channels = self.channels_from_rows_columns(rows, columns, slot) self.open_channels = channels def channels_from_rows_columns(self, rows, columns, slot=None): """ Determine the channel numbers between column(s) and row(s) of the 7709 connection matrix. Returns a list of channel numbers. Only one of the parameters 'rows' or 'columns' can be "all" :param rows: row number or list of numbers; can also be "all" :param columns: column number or list of numbers; can also be "all" :param slot: slot number (1 or 2) of the 7709 card to be used """ if slot is not None and self.cards[slot] != "7709": raise ValueError("No 7709 card installed in slot %g" % slot) if isinstance(rows, str) and isinstance(columns, str): raise ValueError("Only one parameter can be 'all'") elif isinstance(rows, str) and rows == "all": rows = list(range(1, 7)) elif isinstance(columns, str) and columns == "all": columns = list(range(1, 9)) if isinstance(rows, (list, tuple, np.ndarray)) and \ isinstance(columns, (list, tuple, np.ndarray)): if len(rows) != len(columns): raise ValueError("The length of the rows and columns do not match") # Flatten (were necessary) the arrays new_rows = [] new_columns = [] for row, column in zip(rows, columns): if isinstance(row, int) and isinstance(column, int): new_rows.append(row) new_columns.append(column) elif isinstance(row, (list, tuple, np.ndarray)) and isinstance(column, int): new_columns.extend(len(row) * [column]) new_rows.extend(list(row)) elif isinstance(column, (list, tuple, np.ndarray)) and isinstance(row, int): new_columns.extend(list(column)) new_rows.extend(len(column) * [row]) rows = new_rows columns = new_columns # Determine channel number from rows and columns number. rows = np.array(rows) columns = np.array(columns) channels = (rows - 1) * 8 + columns if slot is not None: channels += 100 * slot return channels # system, some taken from Keithley 2400 def beep(self, frequency, duration): """ Sounds a system beep. :param frequency: A frequency in Hz between 65 Hz and 2 MHz :param duration: A time in seconds between 0 and 7.9 seconds """ self.write(":SYST:BEEP %g, %g" % (frequency, duration)) def triad(self, base_frequency, duration): """ Sounds a musical triad using the system beep. :param base_frequency: A frequency in Hz between 65 Hz and 1.3 MHz :param duration: A time in seconds between 0 and 7.9 seconds """ self.beep(base_frequency, duration) time.sleep(duration) self.beep(base_frequency * 5.0 / 4.0, duration) time.sleep(duration) self.beep(base_frequency * 6.0 / 4.0, duration) @property def error(self): """ Returns a tuple of an error code and message from a single error. """ err = self.values(":system:error?") if len(err) < 2: err = self.read() # Try reading again code = err[0] message = err[1].replace('"', '') return (code, message) def check_errors(self): """ Logs any system errors reported by the instrument. """ code, message = self.error while code != 0: t = time.time() log.info("Keithley 2700 reported error: %d, %s" % (code, message)) print(code, message) code, message = self.error if (time.time() - t) > 10: log.warning("Timed out for Keithley 2700 error retrieval.") def reset(self): """ Resets the instrument and clears the queue. """ self.write("status:queue:clear;*RST;:stat:pres;:*CLS;") options = Instrument.measurement( "*OPT?", """Property that lists the installed cards in the Keithley 2700. Returns a dict with the integer card numbers on the position.""", cast=False ) ########### # DISPLAY # ########### text_enabled = Instrument.control( "DISP:TEXT:STAT?", "DISP:TEXT:STAT %d", """ A boolean property that controls whether a text message can be shown on the display of the Keithley 2700. """, values={True: 1, False: 0}, map_values=True, ) display_text = Instrument.control( "DISP:TEXT:DATA?", "DISP:TEXT:DATA '%s'", """ A string property that controls the text shown on the display of the Keithley 2700. Text can be up to 12 ASCII characters and must be enabled to show. """, validator=text_length_validator, values=12, cast=str, separator="NO_SEPARATOR", get_process=lambda v: v.strip("'\""), ) def display_closed_channels(self): """ Show the presently closed channels on the display of the Keithley 2700. """ # Get the closed channels and make a string of the list channels = self.closed_channels channel_string = " ".join([ str(channel % 100) for channel in channels ]) # Prepend "Closed: " or "C: " to the string, depending on the length str_length = 12 if len(channel_string) < str_length - 8: channel_string = "Closed: " + channel_string elif len(channel_string) < str_length - 3: channel_string = "C: " + channel_string # enable displaying text-messages self.text_enabled = True # write the string to the display self.display_text = channel_string PyMeasure-0.9.0/pymeasure/instruments/keithley/keithley2750.py0000664000175000017500000000666314010037617024631 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument def clean_closed_channels(output): """Cleans up the list returned by command ":ROUTe:CLOSe?", such that each entry is an integer denoting the channel number. """ if isinstance(output, str): s = output.replace("(", "").replace(")", "").replace("@", "") if s == "": return [] else: return [int(s)] elif isinstance(output, list): list_final = [] for i, entry in enumerate(output): if isinstance(entry, float) or isinstance(entry, int): list_final += [int(entry)] elif isinstance(entry, str): list_final += [int(entry.replace("(", "").replace(")", "").replace("@", ""))] else: raise ValueError("Every entry must be a string, float, or int") assert isinstance(list_final[i], int) return list_final else: raise ValueError("`output` must be a string or list.") class Keithley2750(Instrument): """ Represents the Keithley2750 multimeter/switch system and provides a high-level interface for interacting with the instrument. """ closed_channels = Instrument.measurement(":ROUTe:CLOSe?", "Reads the list of closed channels", get_process=clean_closed_channels) def __init__(self, adapter, **kwargs): super(Keithley2750, self).__init__( adapter, "Keithley 2750 Multimeter/Switch System", **kwargs ) def open(self, channel): """ Opens (disconnects) the specified channel. :param int channel: 3-digit number for the channel :return: None """ self.write(":ROUTe:MULTiple:OPEN (@{})".format(channel)) def close(self, channel): """ Closes (connects) the specified channel. :param int channel: 3-digit number for the channel :return: None """ # Note: if `MULTiple` is omitted, then the specified channel will close, but all other channels will open. self.write(":ROUTe:MULTiple:CLOSe (@{})".format(channel)) def open_all(self): """ Opens (disconnects) all the channels on the switch matrix. :return: None """ self.write(":ROUTe:OPEN:ALL") PyMeasure-0.9.0/pymeasure/instruments/keithley/keithley2450.py0000664000175000017500000005656714010037617024636 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import time import numpy as np from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range, strict_discrete_set from .buffer import KeithleyBuffer # Setup logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Keithley2450(Instrument, KeithleyBuffer): """ Represents the Keithely 2450 SourceMeter and provides a high-level interface for interacting with the instrument. .. code-block:: python keithley = Keithley2450("GPIB::1") keithley.apply_current() # Sets up to source current keithley.source_current_range = 10e-3 # Sets the source current range to 10 mA keithley.compliance_voltage = 10 # Sets the compliance voltage to 10 V keithley.source_current = 0 # Sets the source current to 0 mA keithley.enable_source() # Enables the source output keithley.measure_voltage() # Sets up to measure voltage keithley.ramp_to_current(5e-3) # Ramps the current to 5 mA print(keithley.voltage) # Prints the voltage in Volts keithley.shutdown() # Ramps the current to 0 mA and disables output """ source_mode = Instrument.control( ":SOUR:FUNC?", ":SOUR:FUNC %s", """ A string property that controls the source mode, which can take the values 'current' or 'voltage'. The convenience methods :meth:`~.Keithley2450.apply_current` and :meth:`~.Keithley2450.apply_voltage` can also be used. """, validator=strict_discrete_set, values={'current':'CURR', 'voltage':'VOLT'}, map_values=True ) source_enabled = Instrument.measurement("OUTPUT?", """ Reads a boolean value that is True if the source is enabled. """, cast=bool ) ############### # Current (A) # ############### current = Instrument.measurement(":READ?", """ Reads the current in Amps, if configured for this reading. """ ) current_range = Instrument.control( ":SENS:CURR:RANG?", ":SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG %g", """ A floating point property that controls the measurement current range in Amps, which can take values between -1.05 and +1.05 A. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-1.05, 1.05] ) current_nplc = Instrument.control( ":SENS:CURR:NPLC?", ":SENS:CURR:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the DC current measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """, values=[0.01, 10] ) compliance_current = Instrument.control( ":SOUR:VOLT:ILIM?", ":SOUR:VOLT:ILIM %g", """ A floating point property that controls the compliance current in Amps. """, validator=truncated_range, values=[-1.05, 1.05] ) source_current = Instrument.control( ":SOUR:CURR?", ":SOUR:CURR:LEV %g", """ A floating point property that controls the source current in Amps. """ ) source_current_range = Instrument.control( ":SOUR:CURR:RANG?", ":SOUR:CURR:RANG:AUTO 0;:SOUR:CURR:RANG %g", """ A floating point property that controls the source current range in Amps, which can take values between -1.05 and +1.05 A. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-1.05, 1.05] ) source_current_delay = Instrument.control( ":SOUR:CURR:DEL?", ":SOUR:CURR:DEL %g", """ A floating point property that sets a manual delay for the source after the output is turned on before a measurement is taken. When this property is set, the auto delay is turned off. Valid values are between 0 [seconds] and 999.9999 [seconds].""", validator=truncated_range, values=[0, 999.9999], ) source_current_delay_auto = Instrument.control( ":SOUR:CURR:DEL:AUTO?", ":SOUR:CURR:DEL:AUTO %d", """ A boolean property that enables or disables auto delay. Valid values are True and False. """, values={True: 1, False: 0}, map_values=True, ) ############### # Voltage (V) # ############### voltage = Instrument.measurement(":READ?", """ Reads the voltage in Volts, if configured for this reading. """ ) voltage_range = Instrument.control( ":SENS:VOLT:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG %g", """ A floating point property that controls the measurement voltage range in Volts, which can take values from -210 to 210 V. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-210, 210] ) voltage_nplc = Instrument.control( ":SENS:VOLT:NPLC?", ":SENS:VOLT:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the DC voltage measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) compliance_voltage = Instrument.control( ":SOUR:CURR:VLIM?", ":SOUR:CURR:VLIM %g", """ A floating point property that controls the compliance voltage in Volts. """, validator=truncated_range, values=[-210, 210] ) source_voltage = Instrument.control( ":SOUR:VOLT?", ":SOUR:VOLT:LEV %g", """ A floating point property that controls the source voltage in Volts. """ ) source_voltage_range = Instrument.control( ":SOUR:VOLT:RANG?", ":SOUR:VOLT:RANG:AUTO 0;:SOUR:VOLT:RANG %g", """ A floating point property that controls the source voltage range in Volts, which can take values from -210 to 210 V. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[-210, 210] ) source_voltage_delay = Instrument.control( ":SOUR:VOLT:DEL?", ":SOUR:VOLT:DEL %g", """ A floating point property that sets a manual delay for the source after the output is turned on before a measurement is taken. When this property is set, the auto delay is turned off. Valid values are between 0 [seconds] and 999.9999 [seconds].""", validator=truncated_range, values=[0, 999.9999], ) source_voltage_delay_auto = Instrument.control( ":SOUR:VOLT:DEL:AUTO?", ":SOUR:VOLT:DEL:AUTO %d", """ A boolean property that enables or disables auto delay. Valid values are True and False. """, values={True: 1, False: 0}, map_values=True, ) #################### # Resistance (Ohm) # #################### resistance = Instrument.measurement(":READ?", """ Reads the resistance in Ohms, if configured for this reading. """ ) resistance_range = Instrument.control( ":SENS:RES:RANG?", ":SENS:RES:RANG:AUTO 0;:SENS:RES:RANG %g", """ A floating point property that controls the resistance range in Ohms, which can take values from 0 to 210 MOhms. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[0, 210e6] ) resistance_nplc = Instrument.control( ":SENS:RES:NPLC?", ":SENS:RES:NPLC %g", """ A floating point property that controls the number of power line cycles (NPLC) for the 2-wire resistance measurements, which sets the integration period and measurement speed. Takes values from 0.01 to 10, where 0.1, 1, and 10 are Fast, Medium, and Slow respectively. """ ) wires = Instrument.control( ":SENS:RES:RSENSE?", ":SENS:RES:RSENSE %d", """ An integer property that controls the number of wires in use for resistance measurements, which can take the value of 2 or 4. """, validator=strict_discrete_set, values={4:1, 2:0}, map_values=True ) buffer_points = Instrument.control( ":TRAC:POIN?", ":TRAC:POIN %d", """ An integer property that controls the number of buffer points. This does not represent actual points in the buffer, but the configuration value instead. """, validator=truncated_range, values=[1, 6875000], cast=int ) means = Instrument.measurement( ":TRACe:STATistics:AVERage?", """ Reads the calculated means (averages) for voltage, current, and resistance from the buffer data as a list. """ ) maximums = Instrument.measurement( ":TRACe:STATistics:MAXimum?", """ Returns the calculated maximums for voltage, current, and resistance from the buffer data as a list. """ ) minimums = Instrument.measurement( ":TRACe:STATistics:MINimum?", """ Returns the calculated minimums for voltage, current, and resistance from the buffer data as a list. """ ) standard_devs = Instrument.measurement( ":TRACe:STATistics:STDDev?", """ Returns the calculated standard deviations for voltage, current, and resistance from the buffer data as a list. """ ) ########### # Filters # ########### current_filter_type = Instrument.control( ":SENS:CURR:AVER:TCON?", ":SENS:CURR:AVER:TCON %s", """ A String property that controls the filter's type for the current. REP : Repeating filter MOV : Moving filter""", validator=strict_discrete_set, values=['REP', 'MOV'], map_values=False) current_filter_count = Instrument.control( ":SENS:CURR:AVER:COUNT?", ":SENS:CURR:AVER:COUNT %d", """ A integer property that controls the number of readings that are acquired and stored in the filter buffer for the averaging""", validator=truncated_range, values=[1, 100], cast=int) current_filter_state = Instrument.control( ":SENS:CURR:AVER?", ":SENS:CURR:AVER %s", """ A string property that controls if the filter is active.""", validator=strict_discrete_set, values=['ON', 'OFF'], map_values=False) voltage_filter_type = Instrument.control( ":SENS:VOLT:AVER:TCON?", ":SENS:VOLT:AVER:TCON %s", """ A String property that controls the filter's type for the current. REP : Repeating filter MOV : Moving filter""", validator=strict_discrete_set, values=['REP', 'MOV'], map_values=False) voltage_filter_count = Instrument.control( ":SENS:VOLT:AVER:COUNT?", ":SENS:VOLT:AVER:COUNT %d", """ A integer property that controls the number of readings that are acquired and stored in the filter buffer for the averaging""", validator=truncated_range, values=[1, 100], cast=int) ##################### # Output subsystem # ##################### current_output_off_state = Instrument.control( ":OUTP:CURR:SMOD?", ":OUTP:CURR:SMOD %s", """ Select the output-off state of the SourceMeter. HIMP : output relay is open, disconnects external circuitry. NORM : V-Source is selected and set to 0V, Compliance is set to 0.5% full scale of the present current range. ZERO : V-Source is selected and set to 0V, compliance is set to the programmed Source I value or to 0.5% full scale of the present current range, whichever is greater. GUAR : I-Source is selected and set to 0A""", validator=strict_discrete_set, values=['HIMP', 'NORM', 'ZERO', 'GUAR'], map_values=False) voltage_output_off_state = Instrument.control( ":OUTP:VOLT:SMOD?", ":OUTP:VOLT:SMOD %s", """ Select the output-off state of the SourceMeter. HIMP : output relay is open, disconnects external circuitry. NORM : V-Source is selected and set to 0V, Compliance is set to 0.5% full scale of the present current range. ZERO : V-Source is selected and set to 0V, compliance is set to the programmed Source I value or to 0.5% full scale of the present current range, whichever is greater. GUAR : I-Source is selected and set to 0A""", validator=strict_discrete_set, values=['HIMP', 'NORM', 'ZERO', 'GUAR'], map_values=False) #################### # Methods # #################### def __init__(self, adapter, **kwargs): super(Keithley2450, self).__init__( adapter, "Keithley 2450 SourceMeter", **kwargs ) def enable_source(self): """ Enables the source of current or voltage depending on the configuration of the instrument. """ self.write("OUTPUT ON") def disable_source(self): """ Disables the source of current or voltage depending on the configuration of the instrument. """ self.write("OUTPUT OFF") def measure_resistance(self, nplc=1, resistance=2.1e5, auto_range=True): """ Configures the measurement of resistance. :param nplc: Number of power line cycles (NPLC) from 0.01 to 10 :param resistance: Upper limit of resistance in Ohms, from -210 MOhms to 210 MOhms :param auto_range: Enables auto_range if True, else uses the set resistance """ log.info("%s is measuring resistance.", self.name) self.write(":SENS:FUNC 'RES';" ":SENS:RES:NPLC %f;" % nplc) if auto_range: self.write(":SENS:RES:RANG:AUTO 1;") else: self.resistance_range = resistance self.check_errors() def measure_voltage(self, nplc=1, voltage=21.0, auto_range=True): """ Configures the measurement of voltage. :param nplc: Number of power line cycles (NPLC) from 0.01 to 10 :param voltage: Upper limit of voltage in Volts, from -210 V to 210 V :param auto_range: Enables auto_range if True, else uses the set voltage """ log.info("%s is measuring voltage.", self.name) self.write(":SENS:FUNC 'VOLT';" ":SENS:VOLT:NPLC %f;" % nplc) if auto_range: self.write(":SENS:VOLT:RANG:AUTO 1;") else: self.voltage_range = voltage self.check_errors() def measure_current(self, nplc=1, current=1.05e-4, auto_range=True): """ Configures the measurement of current. :param nplc: Number of power line cycles (NPLC) from 0.01 to 10 :param current: Upper limit of current in Amps, from -1.05 A to 1.05 A :param auto_range: Enables auto_range if True, else uses the set current """ log.info("%s is measuring current.", self.name) self.write(":SENS:FUNC 'CURR';" ":SENS:CURR:NPLC %f;" % nplc) if auto_range: self.write(":SENS:CURR:RANG:AUTO 1;") else: self.current_range = current self.check_errors() def auto_range_source(self): """ Configures the source to use an automatic range. """ if self.source_mode == 'current': self.write(":SOUR:CURR:RANG:AUTO 1") else: self.write(":SOUR:VOLT:RANG:AUTO 1") def apply_current(self, current_range=None, compliance_voltage=0.1): """ Configures the instrument to apply a source current, and uses an auto range unless a current range is specified. The compliance voltage is also set. :param compliance_voltage: A float in the correct range for a :attr:`~.Keithley2450.compliance_voltage` :param current_range: A :attr:`~.Keithley2450.current_range` value or None """ log.info("%s is sourcing current.", self.name) self.source_mode = 'current' if current_range is None: self.auto_range_source() else: self.source_current_range = current_range self.compliance_voltage = compliance_voltage self.check_errors() def apply_voltage(self, voltage_range=None, compliance_current=0.1): """ Configures the instrument to apply a source voltage, and uses an auto range unless a voltage range is specified. The compliance current is also set. :param compliance_current: A float in the correct range for a :attr:`~.Keithley2450.compliance_current` :param voltage_range: A :attr:`~.Keithley2450.voltage_range` value or None """ log.info("%s is sourcing voltage.", self.name) self.source_mode = 'voltage' if voltage_range is None: self.auto_range_source() else: self.source_voltage_range = voltage_range self.compliance_current = compliance_current self.check_errors() def beep(self, frequency, duration): """ Sounds a system beep. :param frequency: A frequency in Hz between 65 Hz and 2 MHz :param duration: A time in seconds between 0 and 7.9 seconds """ self.write(":SYST:BEEP %g, %g" % (frequency, duration)) def triad(self, base_frequency, duration): """ Sounds a musical triad using the system beep. :param base_frequency: A frequency in Hz between 65 Hz and 1.3 MHz :param duration: A time in seconds between 0 and 7.9 seconds """ self.beep(base_frequency, duration) time.sleep(duration) self.beep(base_frequency*5.0/4.0, duration) time.sleep(duration) self.beep(base_frequency*6.0/4.0, duration) @property def error(self): """ Returns a tuple of an error code and message from a single error. """ err = self.values(":system:error?") if len(err) < 2: err = self.read() # Try reading again code = err[0] message = err[1].replace('"', '') return (code, message) def check_errors(self): """ Logs any system errors reported by the instrument. """ code, message = self.error while code != 0: t = time.time() log.info("Keithley 2450 reported error: %d, %s", code, message) code, message = self.error if (time.time()-t) > 10: log.warning("Timed out for Keithley 2450 error retrieval.") def reset(self): """ Resets the instrument and clears the queue. """ self.write("*RST;:stat:pres;:*CLS;") def ramp_to_current(self, target_current, steps=30, pause=20e-3): """ Ramps to a target current from the set current value over a certain number of linear steps, each separated by a pause duration. :param target_current: A current in Amps :param steps: An integer number of steps :param pause: A pause duration in seconds to wait between steps """ currents = np.linspace( self.source_current, target_current, steps ) for current in currents: self.source_current = current time.sleep(pause) def ramp_to_voltage(self, target_voltage, steps=30, pause=20e-3): """ Ramps to a target voltage from the set voltage value over a certain number of linear steps, each separated by a pause duration. :param target_voltage: A voltage in Amps :param steps: An integer number of steps :param pause: A pause duration in seconds to wait between steps """ voltages = np.linspace( self.source_voltage, target_voltage, steps ) for voltage in voltages: self.source_voltage = voltage time.sleep(pause) def trigger(self): """ Executes a bus trigger. """ return self.write("*TRG") @property def mean_voltage(self): """ Returns the mean voltage from the buffer """ return self.means[0] @property def max_voltage(self): """ Returns the maximum voltage from the buffer """ return self.maximums[0] @property def min_voltage(self): """ Returns the minimum voltage from the buffer """ return self.minimums[0] @property def std_voltage(self): """ Returns the voltage standard deviation from the buffer """ return self.standard_devs[0] @property def mean_current(self): """ Returns the mean current from the buffer """ return self.means[1] @property def max_current(self): """ Returns the maximum current from the buffer """ return self.maximums[1] @property def min_current(self): """ Returns the minimum current from the buffer """ return self.minimums[1] @property def std_current(self): """ Returns the current standard deviation from the buffer """ return self.standard_devs[1] @property def mean_resistance(self): """ Returns the mean resistance from the buffer """ return self.means[2] @property def max_resistance(self): """ Returns the maximum resistance from the buffer """ return self.maximums[2] @property def min_resistance(self): """ Returns the minimum resistance from the buffer """ return self.minimums[2] @property def std_resistance(self): """ Returns the resistance standard deviation from the buffer """ return self.standard_devs[2] def use_rear_terminals(self): """ Enables the rear terminals for measurement, and disables the front terminals. """ self.write(":ROUT:TERM REAR") def use_front_terminals(self): """ Enables the front terminals for measurement, and disables the rear terminals. """ self.write(":ROUT:TERM FRON") def shutdown(self): """ Ensures that the current or voltage is turned to zero and disables the output. """ log.info("Shutting down %s.", self.name) if self.source_mode == 'current': self.ramp_to_current(0.0) else: self.ramp_to_voltage(0.0) self.stop_buffer() self.disable_source() PyMeasure-0.9.0/pymeasure/instruments/keysight/0000775000175000017500000000000014010046235022115 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/keysight/keysightN5767A.py0000664000175000017500000000740614010037617025101 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range from pymeasure.adapters import VISAAdapter class KeysightN5767A(Instrument): """ Represents the Keysight N5767A Power supply interface for interacting with the instrument. .. code-block:: python """ ############### # Current (A) # ############### current_range = Instrument.control( ":CURR?", ":CURR %g", """ A floating point property that controls the DC current range in Amps, which can take values from 0 to 25 A. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[0, 25], ) current = Instrument.measurement(":MEAS:CURR?", """ Reads a setting current in Amps. """ ) ############### # Voltage (V) # ############### voltage_range = Instrument.control( ":VOLT?", ":VOLT %g V", """ A floating point property that controls the DC voltage range in Volts, which can take values from 0 to 60 V. Auto-range is disabled when this property is set. """, validator=truncated_range, values=[0, 60] ) voltage = Instrument.measurement("MEAS:VOLT?", """ Reads a DC voltage measurement in Volts. """ ) ############## #_status (0/1) # ############## _status = Instrument.measurement(":OUTP?", """ Read power supply current output status. """, ) def enable(self): """ Enables the flow of current. """ self.write(":OUTP 1") def disable(self): """ Disables the flow of current. """ self.write(":OUTP 0") def is_enabled(self): """ Returns True if the current supply is enabled. """ return bool(self._status) def __init__(self, adapter, **kwargs): super(KeysightN5767A, self).__init__( adapter, "Keysight N5767A power supply", **kwargs ) # Set up data transfer format if isinstance(self.adapter, VISAAdapter): self.adapter.config( is_binary=False, datatype='float32', converter='f', separator=',' ) def check_errors(self): """ Read all errors from the instrument.""" while True: err = self.values(":SYST:ERR?") if int(err[0]) != 0: errmsg = "Keysight N5767A: %s: %s" % (err[0],err[1]) log.error(errmsg + '\n') else: break PyMeasure-0.9.0/pymeasure/instruments/keysight/keysightDSOX1102G.py0000664000175000017500000005515614010037617025447 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) import numpy as np from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, strict_range class Channel(): """ Implementation of a Keysight DSOX1102G Oscilloscope channel. Implementation modeled on Channel object of Tektronix AFG3152C instrument. """ BOOLS = {True: 1, False: 0} bwlimit = Instrument.control( "BWLimit?", "BWLimit %d", """ A boolean parameter that toggles 25 MHz internal low-pass filter.""", validator=strict_discrete_set, values=BOOLS, map_values=True ) coupling = Instrument.control( "COUPling?", "COUPling %s", """ A string parameter that determines the coupling ("ac" or "dc").""", validator=strict_discrete_set, values={"ac": "AC", "dc": "DC"}, map_values=True ) display = Instrument.control( "DISPlay?", "DISPlay %d", """ A boolean parameter that toggles the display.""", validator=strict_discrete_set, values=BOOLS, map_values=True ) invert = Instrument.control( "INVert?", "INVert %d", """ A boolean parameter that toggles the inversion of the input signal.""", validator=strict_discrete_set, values=BOOLS, map_values=True ) label = Instrument.control( "LABel?", 'LABel "%s"', """ A string to label the channel. Labels with more than 10 characters are truncated to 10 characters. May contain commonly used ASCII characters. Lower case characters are converted to upper case.""", get_process=lambda v: str(v[1:-1]) ) offset = Instrument.control( "OFFSet?", "OFFSet %f", """ A float parameter to set value that is represented at center of screen in Volts. The range of legal values varies depending on range and scale. If the specified value is outside of the legal range, the offset value is automatically set to the nearest legal value. """ ) probe_attenuation = Instrument.control( "PROBe?", "PROBe %f", """ A float parameter that specifies the probe attenuation. The probe attenuation may be from 0.1 to 10000.""", validator=strict_range, values=[0.1, 10000] ) range = Instrument.control( "RANGe?", "RANGe %f", """ A float parameter that specifies the full-scale vertical axis in Volts. When using 1:1 probe attenuation, legal values for the range are from 8 mV to 40V.""" ) scale = Instrument.control( "SCALe?", "SCALe %f", """ A float parameter that specifies the vertical scale, or units per division, in Volts.""" ) def __init__(self, instrument, number): self.instrument = instrument self.number = number def values(self, command, **kwargs): """ Reads a set of values from the instrument through the adapter, passing on any key-word arguments. """ return self.instrument.values(":channel%d:%s" % ( self.number, command), **kwargs) def ask(self, command): self.instrument.ask(":channel%d:%s" % (self.number, command)) def write(self, command): self.instrument.write(":channel%d:%s" % (self.number, command)) def setup(self, bwlimit=None, coupling=None, display=None, invert=None, label=None, offset=None, probe_attenuation=None, vertical_range=None, scale=None): """ Setup channel. Unspecified settings are not modified. Modifying values such as probe attenuation will modify offset, range, etc. Refer to oscilloscope documentation and make multiple consecutive calls to setup() if needed. :param bwlimit: A boolean, which enables 25 MHz internal low-pass filter. :param coupling: "ac" or "dc". :param display: A boolean, which enables channel display. :param invert: A boolean, which enables input signal inversion. :param label: Label string with max. 10 characters, may contain commonly used ASCII characters. :param offset: Numerical value represented at center of screen, must be inside the legal range. :param probe_attenuation: Probe attenuation values from 0.1 to 1000. :param vertical_range: Full-scale vertical axis of the selected channel. When using 1:1 probe attenuation, legal values for the range are from 8mV to 40 V. If the probe attenuation is changed, the range value is multiplied by the probe attenuation factor. :param scale: Units per division. """ if vertical_range is not None and scale is not None: log.warning('Both "vertical_range" and "scale" are specified. Specified "scale" has priority.') if probe_attenuation is not None: self.probe_attenuation = probe_attenuation if bwlimit is not None: self.bwlimit = bwlimit if coupling is not None: self.coupling = coupling if display is not None: self.display = display if invert is not None: self.invert = invert if label is not None: self.label = label if offset is not None: self.offset = offset if vertical_range is not None: self.range = vertical_range if scale is not None: self.scale = scale @property def current_configuration(self): """ Read channel configuration as a dict containing the following keys: - "CHAN": channel number (int) - "OFFS": vertical offset (float) - "RANG": vertical range (float) - "COUP": "dc" or "ac" coupling (str) - "IMP": input impedance (str) - "DISP": currently displayed (bool) - "BWL": bandwidth limiting enabled (bool) - "INV": inverted (bool) - "UNIT": unit (str) - "PROB": probe attenuation (float) - "PROB:SKEW": skew factor (float) - "STYP": probe signal type (str) """ # Using the instrument's ask method because Channel.ask() adds the prefix ":channelX:", and to query the # configuration details, we actually need to ask ":channelX?", without a second ":" ch_setup_raw = self.instrument.ask(":channel%d?" % self.number).strip("\n") # ch_setup_raw hat the following format: # :CHAN1:RANG +40.0E+00;OFFS +0.00000E+00;COUP DC;IMP ONEM;DISP 1;BWL 0; # INV 0;LAB "1";UNIT VOLT;PROB +10E+00;PROB:SKEW +0.00E+00;STYP SING # Cut out the ":CHANx:" at beginning and split string ch_setup_splitted = ch_setup_raw[7:].split(";") # Create dict of setup parameters ch_setup_dict = dict(map(lambda v: v.split(" "), ch_setup_splitted)) # Add "CHAN" key ch_setup_dict["CHAN"] = ch_setup_raw[5] # Convert values to specific type to_str = ["COUP", "IMP", "UNIT", "STYP"] to_bool = ["DISP", "BWL", "INV"] to_float = ["OFFS", "PROB", "PROB:SKEW", "RANG"] to_int = ["CHAN"] for key in ch_setup_dict: if key in to_str: ch_setup_dict[key] = str(ch_setup_dict[key]) elif key in to_bool: ch_setup_dict[key] = (ch_setup_dict[key] == "1") elif key in to_float: ch_setup_dict[key] = float(ch_setup_dict[key]) elif key in to_int: ch_setup_dict[key] = int(ch_setup_dict[key]) return ch_setup_dict class KeysightDSOX1102G(Instrument): """ Represents the Keysight DSOX1102G Oscilloscope interface for interacting with the instrument. Refer to the Keysight DSOX1102G Oscilloscope Programmer's Guide for further details about using the lower-level methods to interact directly with the scope. .. code-block:: python scope = KeysightDSOX1102G(resource) scope.autoscale() ch1_data_array, ch1_preamble = scope.download_data(source="channel1", points=2000) # ... scope.shutdown() Known issues: - The digitize command will be completed before the operation is. May lead to VI_ERROR_TMO (timeout) occuring when sending commands immediately after digitize. Current fix: if deemed necessary, add delay between digitize and follow-up command to scope. """ BOOLS = {True: 1, False: 0} def __init__(self, adapter, **kwargs): super(KeysightDSOX1102G, self).__init__( adapter, "Keysight DSOX1102G Oscilloscope", **kwargs ) # Account for setup time for timebase_mode, waveform_points_mode self.adapter.connection.timeout = 6000 self.ch1 = Channel(self, 1) self.ch2 = Channel(self, 2) ################# # Channel setup # ################# def autoscale(self): """ Autoscale displayed channels. """ self.write(":autoscale") ################## # Timebase Setup # ################## @property def timebase(self): """ Read timebase setup as a dict containing the following keys: - "REF": position on screen of timebase reference (str) - "MAIN:RANG": full-scale timebase range (float) - "POS": interval between trigger and reference point (float) - "MODE": mode (str)""" return self._timebase() timebase_mode = Instrument.control( ":TIMebase:MODE?", ":TIMebase:MODE %s", """ A string parameter that sets the current time base. Can be "main", "window", "xy", or "roll".""", validator=strict_discrete_set, values={"main": "MAIN", "window": "WIND", "xy": "XY", "roll": "ROLL"}, map_values=True ) timebase_offset = Instrument.control( ":TIMebase:POSition?", ":TIMebase:REFerence CENTer;:TIMebase:POSition %f", """ A float parameter that sets the time interval in seconds between the trigger event and the reference position (at center of screen by default).""" ) timebase_range = Instrument.control( ":TIMebase:RANGe?", ":TIMebase:RANGe %f", """ A float parameter that sets the full-scale horizontal time in seconds for the main window.""" ) timebase_scale = Instrument.control( ":TIMebase:SCALe?", ":TIMebase:SCALe %f", """ A float parameter that sets the horizontal scale (units per division) in seconds for the main window.""" ) ############### # Acquisition # ############### acquisition_type = Instrument.control( ":ACQuire:TYPE?", ":ACQuire:TYPE %s", """ A string parameter that sets the type of data acquisition. Can be "normal", "average", "hresolution", or "peak".""", validator=strict_discrete_set, values={"normal": "NORM", "average": "AVER", "hresolution": "HRES", "peak": "PEAK"}, map_values=True ) acquisition_mode = Instrument.control( ":ACQuire:MODE?", ":ACQuire:MODE %s", """ A string parameter that sets the acquisition mode. Can be "realtime" or "segmented".""", validator=strict_discrete_set, values={"realtime": "RTIM", "segmented": "SEGM"}, map_values=True ) def run(self): """ Starts repetitive acquisitions. This is the same as pressing the Run key on the front panel.""" self.write(":run") def stop(self): """ Stops the acquisition. This is the same as pressing the Stop key on the front panel.""" self.write(":stop") def single(self): """ Causes the instrument to acquire a single trigger of data. This is the same as pressing the Single key on the front panel. """ self.write(":single") _digitize = Instrument.setting( ":DIGitize %s", """ Acquire waveforms according to the settings of the :ACQuire commands and specified source, as a string parameter that can take the following values: "channel1", "channel2", "function", "math", "fft", "abus", or "ext". """, validator=strict_discrete_set, values={"channel1": "CHAN1", "channel2": "CHAN2", "function": "FUNC", "math": "MATH", "fft": "FFT", "abus": "ABUS", "ext": "EXT"}, map_values=True ) def digitize(self, source: str): """ Acquire waveforms according to the settings of the :ACQuire commands. Ensure a delay between the digitize operation and further commands, as timeout may be reached before digitize has completed. :param source: "channel1", "channel2", "function", "math", "fft", "abus", or "ext".""" self._digitize = source waveform_points_mode = Instrument.control( ":waveform:points:mode?", ":waveform:points:mode %s", """ A string parameter that sets the data record to be transferred with the waveform_data method. Can be "normal", "maximum", or "raw".""", validator=strict_discrete_set, values={"normal": "NORM", "maximum": "MAX", "raw": "RAW"}, map_values=True ) waveform_points = Instrument.control( ":waveform:points?", ":waveform:points %d", """ An integer parameter that sets the number of waveform points to be transferred with the waveform_data method. Can be any of the following values: 100, 250, 500, 1000, 2 000, 5 000, 10 000, 20 000, 50 000, 62 500. Note that the oscilloscope may provide less than the specified nb of points. """, validator=strict_discrete_set, values=[100, 250, 500, 1000, 2000, 5000, 10000, 20000, 50000, 62500] ) waveform_source = Instrument.control( ":waveform:source?", ":waveform:source %s", """ A string parameter that selects the analog channel, function, or reference waveform to be used as the source for the waveform methods. Can be "channel1", "channel2", "function", "fft", "wmemory1", "wmemory2", or "ext".""", validator=strict_discrete_set, values={"channel1": "CHAN1", "channel2": "CHAN2", "function": "FUNC", "fft": "FFT", "wmemory1": "WMEM1", "wmemory2": "WMEM2", "ext": "EXT"}, map_values=True ) waveform_format = Instrument.control( ":waveform:format?", ":waveform:format %s", """ A string parameter that controls how the data is formatted when sent from the oscilloscope. Can be "ascii", "word" or "byte". Words are transmitted in big endian by default.""", validator=strict_discrete_set, values={"ascii": "ASC", "word": "WORD", "byte": "BYTE"}, map_values=True ) @property def waveform_preamble(self): """ Get preamble information for the selected waveform source as a dict with the following keys: - "format": byte, word, or ascii (str) - "type": normal, peak detect, or average (str) - "points": nb of data points transferred (int) - "count": always 1 (int) - "xincrement": time difference between data points (float) - "xorigin": first data point in memory (float) - "xreference": data point associated with xorigin (int) - "yincrement": voltage difference between data points (float) - "yorigin": voltage at center of screen (float) - "yreference": data point associated with yorigin (int)""" return self._waveform_preamble() @property def waveform_data(self): """ Get the binary block of sampled data points transmitted using the IEEE 488.2 arbitrary block data format.""" # Other waveform formats raise UnicodeDecodeError self.waveform_format = "ascii" data = self.values(":waveform:data?") # Strip header from first data element data[0] = float(data[0][10:]) return data ################ # System Setup # ################ @property def system_setup(self): """ A string parameter that sets up the oscilloscope. Must be in IEEE 488.2 format. It is recommended to only set a string previously obtained from this command.""" return self.ask(":system:setup?") @system_setup.setter def system_setup(self, setup_string): self.write(":system:setup " + setup_string) def ch(self, channel_number): if channel_number == 1: return self.ch1 elif channel_number == 2: return self.ch2 else: raise ValueError("Invalid channel number. Must be 1 or 2.") def check_errors(self): """ Read all errors from the instrument.""" while True: err = self.values(":SYST:ERR?") if int(err[0]) != 0: errmsg = "Keysight DSOX1102G: %s: %s" % (err[0], err[1]) log.error(errmsg + "\n") else: break def clear_status(self): """ Clear device status. """ self.write("*CLS") def factory_reset(self): """ Factory default setup, no user settings remain unchanged. """ self.write("*RST") def default_setup(self): """ Default setup, some user settings (like preferences) remain unchanged. """ self.write(":SYSTem:PRESet") def timebase_setup(self, mode=None, offset=None, horizontal_range=None, scale=None): """ Set up timebase. Unspecified parameters are not modified. Modifying a single parameter might impact other parameters. Refer to oscilloscope documentation and make multiple consecutive calls to channel_setup if needed. :param mode: Timebase mode, can be "main", "window", "xy", or "roll". :param offset: Offset in seconds between trigger and center of screen. :param horizontal_range: Full-scale range in seconds. :param scale: Units-per-division in seconds.""" if mode is not None: self.timebase_mode = mode if offset is not None: self.timebase_offset = offset if horizontal_range is not None: self.timebase_range = horizontal_range if scale is not None: self.timebase_scale = scale def download_image(self, format_="png", color_palette="color"): """ Get image of oscilloscope screen in bytearray of specified file format. :param format_: "bmp", "bmp8bit", or "png" :param color_palette: "color" or "grayscale" """ query = f":DISPlay:DATA? {format_}, {color_palette}" # Using binary_values query because default interface does not support binary transfer img = self.binary_values(query, header_bytes=10, dtype=np.uint8) return bytearray(img) def download_data(self, source, points=62500): """ Get data from specified source of oscilloscope. Returned objects are a np.ndarray of data values (no temporal axis) and a dict of the waveform preamble, which can be used to build the corresponding time values for all data points. Multimeter will be stopped for proper acquisition. :param source: measurement source, can be "channel1", "channel2", "function", "fft", "wmemory1", "wmemory2", or "ext". :param points: integer number of points to acquire. Note that oscilloscope may return less points than specified, this is not an issue of this library. Can be 100, 250, 500, 1000, 2000, 5000, 10000, 20000, 50000, or 62500. :return data_ndarray, waveform_preamble_dict: see waveform_preamble property for dict format. """ # TODO: Consider downloading from multiple sources at the same time. self.waveform_source = source self.waveform_points_mode = "normal" self.waveform_points = points preamble = self.waveform_preamble data_bytes = self.waveform_data return np.array(data_bytes), preamble def _timebase(self): """ Reads setup data from timebase and converts it to a more convenient dict of values. """ tb_setup_raw = self.ask(":timebase?").strip("\n") # tb_setup_raw hat the following format: # :TIM:MODE MAIN;REF CENT;MAIN:RANG +1.00E-03;POS +0.0E+00 # Cut out the ":TIM:" at beginning and split string tb_setup_splitted = tb_setup_raw[5:].split(";") # Create dict of setup parameters tb_setup = dict(map(lambda v: v.split(" "), tb_setup_splitted)) # Convert values to specific type to_str = ["MODE", "REF"] to_float = ["MAIN:RANG", "POS"] for key in tb_setup: if key in to_str: tb_setup[key] = str(tb_setup[key]) elif key in to_float: tb_setup[key] = float(tb_setup[key]) return tb_setup def _waveform_preamble(self): """ Reads waveform preamble and converts it to a more convenient dict of values. """ vals = self.values(":waveform:preamble?") # Get values to dict vals_dict = dict(zip(["format", "type", "points", "count", "xincrement", "xorigin", "xreference", "yincrement", "yorigin", "yreference"], vals)) # Map element values format_map = {0: "BYTE", 1: "WORD", 4: "ASCII"} type_map = {0: "NORMAL", 1: "PEAK DETECT", 2: "AVERAGE", 3: "HRES"} vals_dict["format"] = format_map[int(vals_dict["format"])] vals_dict["type"] = type_map[int(vals_dict["type"])] # Correct types to_int = ["points", "count", "xreference", "yreference"] to_float = ["xincrement", "xorigin", "yincrement", "yorigin"] for key in vals_dict: if key in to_int: vals_dict[key] = int(vals_dict[key]) elif key in to_float: vals_dict[key] = float(vals_dict[key]) return vals_dict PyMeasure-0.9.0/pymeasure/instruments/keysight/__init__.py0000664000175000017500000000013414010032216024216 0ustar colincolin00000000000000from .keysightDSOX1102G import KeysightDSOX1102G from .keysightN5767A import KeysightN5767A PyMeasure-0.9.0/pymeasure/instruments/signalrecovery/0000775000175000017500000000000014010046235023322 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/signalrecovery/dsp7265.py0000664000175000017500000002325214010037617025016 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_discrete_set, truncated_range, modular_range, modular_range_bidirectional, strict_discrete_set from time import sleep import numpy as np class DSP7265(Instrument): """This is the class for the DSP 7265 lockin amplifier""" # TODO: add regultors on most of these SENSITIVITIES = [ 0.0, 2.0e-9, 5.0e-9, 10.0e-9, 20.0e-9, 50.0e-9, 100.0e-9, 200.0e-9, 500.0e-9, 1.0e-6, 2.0e-6, 5.0e-6, 10.0e-6, 20.0e-6, 50.0e-6, 100.0e-6, 200.0e-6, 500.0e-6, 1.0e-3, 2.0e-3, 5.0e-3, 10.0e-3, 20.0e-3, 50.0e-3, 100.0e-3, 200.0e-3, 500.0e-3, 1.0 ] TIME_CONSTANTS = [ 10.0e-6, 20.0e-6, 40.0e-6, 80.0e-6, 160.0e-6, 320.0e-6, 640.0e-6, 5.0e-3, 10.0e-3, 20.0e-3, 50.0e-3, 100.0e-3, 200.0e-3, 500.0e-3, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1.0e3, 2.0e3, 5.0e3, 10.0e3, 20.0e3, 50.0e3 ] REFERENCES = ['internal', 'external rear', 'external front'] voltage = Instrument.control( "OA.", "OA. %g", """ A floating point property that represents the voltage in Volts. This property can be set. """, validator=truncated_range, values=[0,5] ) frequency = Instrument.control( "OF.", "OF. %g", """ A floating point property that represents the lock-in frequency in Hz. This property can be set. """, validator=truncated_range, values=[0,2.5e5] ) dac1 = Instrument.control( "DAC. 1", "DAC. 1 %g", """ A floating point property that represents the output value on DAC1 in Volts. This property can be set. """, validator=truncated_range, values=[-12,12] ) dac2 = Instrument.control( "DAC. 2", "DAC. 2 %g", """ A floating point property that represents the output value on DAC2 in Volts. This property can be set. """, validator=truncated_range, values=[-12,12] ) dac3 = Instrument.control( "DAC. 3", "DAC. 3 %g", """ A floating point property that represents the output value on DAC3 in Volts. This property can be set. """, validator=truncated_range, values=[-12,12] ) dac4 = Instrument.control( "DAC. 4", "DAC. 4 %g", """ A floating point property that represents the output value on DAC4 in Volts. This property can be set. """, validator=truncated_range, values=[-12,12] ) harmonic = Instrument.control( "REFN", "REFN %d", """ An integer property that represents the reference harmonic mode control, taking values from 1 to 65535. This property can be set. """, validator=truncated_discrete_set, values=list(range(65535)) ) phase = Instrument.control( "REFP.", "REFP. %g", """ A floating point property that represents the reference harmonic phase in degrees. This property can be set. """, validator=modular_range_bidirectional, values=[0,360] ) x = Instrument.measurement("X.", """ Reads the X value in Volts """ ) y = Instrument.measurement("Y.", """ Reads the Y value in Volts """ ) xy = Instrument.measurement("X.Y.", """ Reads both the X and Y values in Volts """ ) mag = Instrument.measurement("MAG.", """ Reads the magnitude in Volts """ ) adc1 = Instrument.measurement("ADC. 1", """ Reads the input value of ADC1 in Volts """ ) adc2 = Instrument.measurement("ADC. 2", """ Reads the input value of ADC2 in Volts """ ) id = Instrument.measurement("ID", """ Reads the instrument identification """ ) reference = Instrument.control( "IE", "IE %d", """Controls the oscillator reference. Can be "internal", "external rear" or "external front" """, validator=strict_discrete_set, values=REFERENCES, map_values=True ) sensitivity = Instrument.control( "SEN", "SEN %d", """ A floating point property that controls the sensitivity range in Volts, which can take discrete values from 2 nV to 1 V. This property can be set. """, validator=truncated_discrete_set, values=SENSITIVITIES, map_values=True ) slope = Instrument.control( "SLOPE", "SLOPE %d", """ A integer property that controls the filter slope in dB/octave, which can take the values 6, 12, 18, or 24 dB/octave. This property can be set. """, validator=truncated_discrete_set, values=[6, 12, 18, 24], map_values=True ) time_constant = Instrument.control( "TC", "TC %d", """ A floating point property that controls the time constant in seconds, which takes values from 10 microseconds to 50,000 seconds. This property can be set. """, validator=truncated_discrete_set, values=TIME_CONSTANTS, map_values=True ) def __init__(self, resourceName, **kwargs): super(DSP7265, self).__init__( resourceName, "Signal Recovery DSP 7265", **kwargs ) self.curve_bits = { 'x': 1, 'y': 2, 'mag': 4, 'phase': 8, 'ADC1': 32, 'ADC2': 64, 'ADC3': 128 } # Pre-condition self.adapter.config(datatype = 'str', converter = 's') def values(self, command): """ Rewrite the method because of extra character in return string.""" result = self.ask(command).strip() result = result.replace('\x00','') # Remove extra unicode character try: return [float(x) for x in result.split(",")] except: return result def set_voltage_mode(self): self.write("IMODE 0") def setDifferentialMode(self, lineFiltering=True): """Sets lockin to differential mode, measuring A-B""" self.write("VMODE 3") self.write("LF %d 0" % 3 if lineFiltering else 0) def setChannelAMode(self): self.write("VMODE 1") @property def adc3(self): # 50,000 for 1V signal over 1 s integral = self.values("ADC 3") return float(integral)/(50000.0*self.adc3_time) @property def adc3_time(self): # Returns time in seconds return self.values("ADC3TIME")/1000.0 @adc3_time.setter def adc3_time(self, value): # Takes time in seconds self.write("ADC3TIME %g" % int(1000*value)) sleep(value*1.2) @property def auto_gain(self): return (int(self.values("AUTOMATIC")) == 1) @auto_gain.setter def auto_gain(self, value): if value: self.write("AUTOMATIC 1") else: self.write("AUTOMATIC 0") def auto_sensitivity(self): self.write("AS") def auto_phase(self): self.write("AQN") @property def gain(self): return self.values("ACGAIN") @gain.setter def gain(self, value): self.write("ACGAIN %d" % int(value/10.0)) def set_buffer(self, points, quantities=['x'], interval=10.0e-3): num = 0 for q in quantities: num += self.curve_bits[q] self.points = points self.write("CBD %d" % int(num)) self.write("LEN %d" % int(points)) # interval in increments of 5ms interval = int(float(interval)/5.0e-3) self.write("STR %d" % interval) self.write("NC") def start_buffer(self): self.write("TD") def get_buffer(self, quantity='x', timeout=1.00, average=False): count = 0 maxCount = int(timeout/0.05) failed = False while int(self.values("M")) != 0: # Sleeping sleep(0.05) if count > maxCount: # Count reached max value, wait longer before asking! failed = True break if not failed: data = [] # Getting data for i in range(self.length): val = self.values("DC. %d" % self.curve_bits[quantity]) data.append(val) if average: return np.mean(data) else: return data else: return [0.0] def shutdown(self): log.info("Shutting down %s." % self.name) self.voltage = 0. self.isShutdown = True PyMeasure-0.9.0/pymeasure/instruments/signalrecovery/__init__.py0000664000175000017500000000224214010037617025437 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .dsp7265 import DSP7265 PyMeasure-0.9.0/pymeasure/instruments/advantest/0000775000175000017500000000000014010046235022257 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/advantest/advantestR3767CG.py0000664000175000017500000000531214010037617025512 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range, strict_discrete_set, strict_range class AdvantestR3767CG(Instrument): """ Represents the Advantest R3767CG VNA. Implements controls to change the analysis range and to retreve the data for the trace. """ id = Instrument.measurement( "*IDN?", """ Reads the instrument identification """ ) center_frequency = Instrument.control( ":FREQ:CENT?", ":FREQ:CENT %d", """Center Frequency in Hz""", validator=strict_range, values=[300000, 8000000000] ) span_frequency = Instrument.control( ":FREQ:SPAN?", ":FREQ:SPAN %d", """Span Frequency in Hz""", validator=strict_range, values=[1, 8000000000] ) start_frequency = Instrument.control( ":FREQ:STAR?", ":FREQ:STAR %d", """ Starting frequency in Hz """, validator=strict_range, values=[1, 8000000000] ) stop_frequency = Instrument.control( ":FREQ:STOP?",":FREQ:STOP %d", """ Stoping frequency in Hz """, validator=strict_range, values=[1, 8000000000] ) trace_1 = Instrument.measurement( "TRAC:DATA? FDAT1", """ Reads the Data array from trace 1 after formatting """ ) def __init__(self, resourceName, **kwargs): super(AdvantestR3767CG, self).__init__( resourceName, "Advantest R3767CG", **kwargs ) # Tell unit to operate in IEEE488.2-1987 command mode. self.write("OLDC OFF") PyMeasure-0.9.0/pymeasure/instruments/advantest/__init__.py0000664000175000017500000000226414010037617024400 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .advantestR3767CG import AdvantestR3767CG PyMeasure-0.9.0/pymeasure/instruments/attocube/0000775000175000017500000000000014010046235022074 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/attocube/anc300.py0000664000175000017500000002243114010037617023440 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from math import inf from pymeasure.instruments import Instrument from pymeasure.instruments.attocube.adapters import AttocubeConsoleAdapter from pymeasure.instruments.validators import (joined_validators, strict_discrete_set, strict_range) def strict_length(value, values): if len(value) != values: raise ValueError( f"Value {value} does not have an appropriate length of {values}") return value def truncated_int_array(value, values): ret = [] for i, v in enumerate(value): if values[0] <= v <= values[1]: if float(v).is_integer(): ret.append(int(v)) else: raise ValueError(f"Entry {v} at index {i} has no integer value") elif float(v).is_integer(): ret.append(max(min(values[1], v), values[0])) else: raise ValueError(f"Entry {v} at index {i} has no integer value and" f"is out of the boundaries {values}") return ret truncated_int_array_strict_length = joined_validators(strict_length, truncated_int_array) class Axis(object): """ Represents a single open loop axis of the Attocube ANC350 :param axis: axis identifier, integer from 1 to 7 :param controller: ANC300Controller instance used for the communication """ serial_nr = Instrument.measurement("getser", "Serial number of the axis") voltage = Instrument.control( "getv", "setv %.3f", """ Amplitude of the stepping voltage in volts from 0 to 150 V. This property can be set. """, validator=strict_range, values=[0, 150]) frequency = Instrument.control( "getf", "setf %.3f", """ Frequency of the stepping motion in Hertz from 1 to 10000 Hz. This property can be set. """, validator=strict_range, values=[1, 10000], cast=int) mode = Instrument.control( "getm", "setm %s", """ Axis mode. This can be 'gnd', 'inp', 'cap', 'stp', 'off', 'stp+', 'stp-'. Available modes depend on the actual axis model""", validator=strict_discrete_set, values=['gnd', 'inp', 'cap', 'stp', 'off', 'stp+', 'stp-']) offset_voltage = Instrument.control( "geta", "seta %.3f", """ Offset voltage in Volts from 0 to 150 V. This property can be set. """, validator=strict_range, values=[0, 150]) pattern_up = Instrument.control( "getpu", "setpu %s", """ step up pattern of the piezo drive. 256 values ranging from 0 to 255 representing the the sequence of output voltages within one step of the piezo drive. This property can be set, the set value needs to be an array with 256 integer values. """, validator=truncated_int_array_strict_length, values=[256, [0, 255]], set_process=lambda a: " ".join("%d" % v for v in a), separator='\r\n', cast=int) pattern_down = Instrument.control( "getpd", "setpd %s", """ step down pattern of the piezo drive. 256 values ranging from 0 to 255 representing the the sequence of output voltages within one step of the piezo drive. This property can be set, the set value needs to be an array with 256 integer values. """, validator=truncated_int_array_strict_length, values=[256, [0, 255]], set_process=lambda a: " ".join("%d" % v for v in a), separator='\r\n', cast=int) output_voltage = Instrument.measurement( "geto", """ Output voltage in volts.""") capacity = Instrument.measurement( "getc", """ Saved capacity value in nF of the axis.""") stepu = Instrument.setting( "stepu %d", """ Step upwards for N steps. Mode must be 'stp' and N must be positive.""", validator=strict_range, values=[0, inf]) stepd = Instrument.setting( "stepd %d", """ Step downwards for N steps. Mode must be 'stp' and N must be positive.""", validator=strict_range, values=[0, inf]) def __init__(self, controller, axis): self.axis = str(axis) self.controller = controller def _add_axis_id(self, command): """ add axis id to a command string at the correct position after the initial command, but before a potential value :param command: command string :returns: command string with added axis id """ cmdparts = command.split() cmdparts.insert(1, self.axis) return ' '.join(cmdparts) def ask(self, command, **kwargs): return self.controller.ask(self._add_axis_id(command), **kwargs) def write(self, command, **kwargs): return self.controller.write(self._add_axis_id(command), **kwargs) def values(self, command, **kwargs): return self.controller.values(self._add_axis_id(command), **kwargs) def stop(self): """ Stop any motion of the axis """ self.write('stop') def move(self, steps, gnd=True): """ Move 'steps' steps in the direction given by the sign of the argument. This method will change the mode of the axis automatically and ground the axis on the end if 'gnd' is True. The method returns only when the movement is finished. :param steps: finite integer value of steps to be performed. A positive sign corresponds to upwards steps, a negative sign to downwards steps. :param gnd: bool, flag to decide if the axis should be grounded after completion of the movement """ self.mode = 'stp' # perform the movement if steps > 0: self.stepu = steps elif steps < 0: self.stepd = abs(steps) else: pass # do not set stepu/d to 0 since it triggers a continous move # wait for the move to finish self.write('stepw') if gnd: self.mode = 'gnd' def measure_capacity(self): """ Obtains a new measurement of the capacity. The mode of the axis returns to 'gnd' after the measurement. :returns capacity: the freshly measured capacity in nF. """ self.mode = 'cap' # wait for the measurement to finish self.ask('capw') return self.capacity class ANC300Controller(Instrument): """ Attocube ANC300 Piezo stage controller with several axes :param host: host address of the instrument :param axisnames: a list of axis names which will be used to create properties with these names :param passwd: password for the attocube standard console :param query_delay: delay between sending and reading (default 0.05 sec) :param kwargs: Any valid key-word argument for TelnetAdapter """ version = Instrument.measurement( "ver", """ Version number and instrument identification """ ) controllerBoardVersion = Instrument.measurement( "getcser", """ Serial number of the controller board """ ) def __init__(self, host, axisnames, passwd, query_delay=0.05, **kwargs): kwargs['query_delay'] = query_delay super().__init__( AttocubeConsoleAdapter(host, 7230, passwd, **kwargs), "attocube ANC300 Piezo Controller", includeSCPI = False, **kwargs ) for i, axis in enumerate(axisnames): setattr(self, axis, Axis(self, i+1)) def ground_all(self): """ Grounds all axis of the controller. """ for attr in dir(self): attribute = getattr(self, attr) if isinstance(attribute, Axis): attribute.mode = 'gnd' def stop_all(self): """ Stop all movements of the axis. """ for attr in dir(self): attribute = getattr(self, attr) if isinstance(attribute, Axis): attribute.stop() PyMeasure-0.9.0/pymeasure/instruments/attocube/adapters.py0000664000175000017500000001242514010037617024261 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import re import time from pymeasure.adapters import TelnetAdapter class AttocubeConsoleAdapter(TelnetAdapter): """ Adapter class for connecting to the Attocube Standard Console. This console is a Telnet prompt with password authentication. :param host: host address of the instrument :param port: TCPIP port :param passwd: password required to open the connection :param kwargs: Any valid key-word argument for TelnetAdapter """ # compiled regular expression for finding numerical values in reply strings _reg_value = re.compile(r"\w+\s+=\s+(\w+)") def __init__(self, host, port, passwd, **kwargs): self.read_termination = '\r\n' self.write_termination = self.read_termination kwargs.setdefault('preprocess_reply', self.extract_value) super().__init__(host, port, **kwargs) time.sleep(self.query_delay) super().read() # clear messages sent upon opening the connection # send password and check authorization self.write(passwd, check_ack=False) time.sleep(self.query_delay) ret = super().read() authmsg = ret.split(self.read_termination)[1] if authmsg != 'Authorization success': raise Exception(f"Attocube authorization failed '{authmsg}'") # switch console echo off _ = self.ask('echo off') def extract_value(self, reply): """ preprocess_reply function for the Attocube console. This function tries to extract from 'name = [unit]'. If can not be identified the original string is returned. :param reply: reply string :returns: string with only the numerical value, or the original string """ r = self._reg_value.search(reply) if r: return r.groups()[0] else: return reply def check_acknowledgement(self, reply, msg=""): """ checks the last reply of the instrument to be 'OK', otherwise a ValueError is raised. :param reply: last reply string of the instrument :param msg: optional message for the eventual error """ if reply != 'OK': if msg == "": # clear buffer msg = reply super().read() raise ValueError("AttocubeConsoleAdapter: Error after command " f"{self.lastcommand} with message {msg}") def read(self): """ Reads a reply of the instrument which consists of two or more lines. The first ones are the reply to the command while the last one is 'OK' or 'ERROR' to indicate any problem. In case the reply is not OK a ValueError is raised. :returns: String ASCII response of the instrument. """ raw = super().read().strip(self.read_termination) # one would want to use self.read_termination as 'sep' below, but this # is not possible because of a firmware bug resulting in inconsistent # line endings ret, ack = raw.rsplit(sep='\n', maxsplit=1) ret = ret.strip('\r') # strip possible CR char self.check_acknowledgement(ack, ret) return ret def write(self, command, check_ack=True): """ Writes a command to the instrument :param command: command string to be sent to the instrument :param check_ack: boolean flag to decide if the acknowledgement is read back from the instrument. This should be True for set pure commands and False otherwise. """ self.lastcommand = command super().write(command + self.write_termination) if check_ack: reply = self.connection.read_until(self.read_termination.encode()) msg = reply.decode().strip(self.read_termination) self.check_acknowledgement(msg) def ask(self, command): """ Writes a command to the instrument and returns the resulting ASCII response :param command: command string to be sent to the instrument :returns: String ASCII response of the instrument """ self.write(command, check_ack=False) time.sleep(self.query_delay) return self.read() PyMeasure-0.9.0/pymeasure/instruments/attocube/__init__.py0000664000175000017500000000232714010037617024215 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .adapters import AttocubeConsoleAdapter from .anc300 import ANC300Controller PyMeasure-0.9.0/pymeasure/instruments/agilent/0000775000175000017500000000000014010046235021711 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/agilent/agilent8257D.py0000664000175000017500000003031614010037617024347 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range, strict_discrete_set class Agilent8257D(Instrument): """Represents the Agilent 8257D Signal Generator and provides a high-level interface for interacting with the instrument. .. code-block:: python generator = Agilent8257D("GPIB::1") generator.power = 0 # Sets the output power to 0 dBm generator.frequency = 5 # Sets the output frequency to 5 GHz generator.enable() # Enables the output """ power = Instrument.control( ":POW?;", ":POW %g dBm;", """ A floating point property that represents the output power in dBm. This property can be set. """ ) frequency = Instrument.control( ":FREQ?;", ":FREQ %e Hz;", """ A floating point property that represents the output frequency in Hz. This property can be set. """ ) start_frequency = Instrument.control( ":SOUR:FREQ:STAR?", ":SOUR:FREQ:STAR %e Hz", """ A floating point property that represents the start frequency in Hz. This property can be set. """ ) center_frequency = Instrument.control( ":SOUR:FREQ:CENT?", ":SOUR:FREQ:CENT %e Hz;", """ A floating point property that represents the center frequency in Hz. This property can be set. """ ) stop_frequency = Instrument.control( ":SOUR:FREQ:STOP?", ":SOUR:FREQ:STOP %e Hz", """ A floating point property that represents the stop frequency in Hz. This property can be set. """ ) start_power = Instrument.control( ":SOUR:POW:STAR?", ":SOUR:POW:STAR %e dBm", """ A floating point property that represents the start power in dBm. This property can be set. """ ) stop_power = Instrument.control( ":SOUR:POW:STOP?", ":SOUR:POW:STOP %e dBm", """ A floating point property that represents the stop power in dBm. This property can be set. """ ) dwell_time = Instrument.control( ":SOUR:SWE:DWEL1?", ":SOUR:SWE:DWEL1 %.3f", """ A floating point property that represents the settling time in seconds at the current frequency or power setting. This property can be set. """ ) step_points = Instrument.control( ":SOUR:SWE:POIN?", ":SOUR:SWE:POIN %d", """ An integer number of points in a step sweep. This property can be set. """ ) is_enabled = Instrument.measurement(":OUTPUT?", """ Reads a boolean value that is True if the output is on. """, cast=bool ) has_modulation = Instrument.measurement(":OUTPUT:MOD?", """ Reads a boolean value that is True if the modulation is enabled. """, cast=bool ) ######################## # Amplitude modulation # ######################## has_amplitude_modulation = Instrument.measurement(":SOUR:AM:STAT?", """ Reads a boolean value that is True if the amplitude modulation is enabled. """, cast=bool ) amplitude_depth = Instrument.control( ":SOUR:AM:DEPT?", ":SOUR:AM:DEPT %g", """ A floating point property that controls the amplitude modulation in precent, which can take values from 0 to 100 %. """, validator=truncated_range, values=[0, 100] ) AMPLITUDE_SOURCES = { 'internal':'INT', 'internal 2':'INT2', 'external':'EXT', 'external 2':'EXT2' } amplitude_source = Instrument.control( ":SOUR:AM:SOUR?", ":SOUR:AM:SOUR %s", """ A string property that controls the source of the amplitude modulation signal, which can take the values: 'internal', 'internal 2', 'external', and 'external 2'. """, validator=strict_discrete_set, values=AMPLITUDE_SOURCES, map_values=True ) #################### # Pulse modulation # #################### has_pulse_modulation = Instrument.measurement(":SOUR:PULM:STAT?", """ Reads a boolean value that is True if the pulse modulation is enabled. """, cast=bool ) PULSE_SOURCES = { 'internal':'INT', 'external':'EXT', 'scalar':'SCAL' } pulse_source = Instrument.control( ":SOUR:PULM:SOUR?", ":SOUR:PULM:SOUR %s", """ A string property that controls the source of the pulse modulation signal, which can take the values: 'internal', 'external', and 'scalar'. """, validator=strict_discrete_set, values=PULSE_SOURCES, map_values=True ) PULSE_INPUTS = { 'square':'SQU', 'free-run':'FRUN', 'triggered':'TRIG', 'doublet':'DOUB', 'gated':'GATE' } pulse_input = Instrument.control( ":SOUR:PULM:SOUR:INT?", ":SOUR:PULM:SOUR:INT %s", """ A string property that controls the internally generated modulation input for the pulse modulation, which can take the values: 'square', 'free-run', 'triggered', 'doublet', and 'gated'. """, validator=strict_discrete_set, values=PULSE_INPUTS, map_values=True ) pulse_frequency = Instrument.control( ":SOUR:PULM:INT:FREQ?", ":SOUR:PULM:INT:FREQ %g", """ A floating point property that controls the pulse rate frequency in Hertz, which can take values from 0.1 Hz to 10 MHz. """, validator=truncated_range, values=[0.1, 10e6] ) ######################## # Low-Frequency Output # ######################## low_freq_out_amplitude = Instrument.control( ":SOUR:LFO:AMPL? ", ":SOUR:LFO:AMPL %g VP", """A floating point property that controls the peak voltage (amplitude) of the low frequency output in volts, which can take values from 0-3.5V""", validator=truncated_range, values=[0,3.5] ) LOW_FREQUENCY_SOURCES = { 'internal':'INT', 'internal 2':'INT2', 'function':'FUNC', 'function 2':'FUNC2' } low_freq_out_source = Instrument.control( ":SOUR:LFO:SOUR?", ":SOUR:LFO:SOUR %s", """A string property which controls the source of the low frequency output, which can take the values 'internal [2]' for the inernal source, or 'function [2]' for an internal function generator which can be configured.""", validator=strict_discrete_set, values=LOW_FREQUENCY_SOURCES, map_values=True ) def enable_low_freq_out(self): """Enables low frequency output""" self.write(":SOUR:LFO:STAT ON") def disable_low_freq_out(self): """Disables low frequency output""" self.write(":SOUR:LFO:STAT OFF") def config_low_freq_out(self, source='internal', amplitude=3): """ Configures the low-frequency output signal. :param source: The source for the low-frequency output signal. :param amplitude: Amplitude of the low-frequency output """ self.enable_low_freq_out() self.low_freq_out_source = source self.low_freq_out_amplitude = amplitude ####################### # Internal Oscillator # ####################### internal_frequency = Instrument.control( ":SOUR:AM:INT:FREQ?", ":SOUR:AM:INT:FREQ %g", """ A floating point property that controls the frequency of the internal oscillator in Hertz, which can take values from 0.5 Hz to 1 MHz. """, validator=truncated_range, values=[0.5, 1e6] ) INTERNAL_SHAPES = { 'sine':'SINE', 'triangle':'TRI', 'square':'SQU', 'ramp':'RAMP', 'noise':'NOIS', 'dual-sine':'DUAL', 'swept-sine':'SWEP' } internal_shape = Instrument.control( ":SOUR:AM:INT:FUNC:SHAP?", ":SOUR:AM:INT:FUNC:SHAP %s", """ A string property that controls the shape of the internal oscillations, which can take the values: 'sine', 'triangle', 'square', 'ramp', 'noise', 'dual-sine', and 'swept-sine'. """, validator=strict_discrete_set, values=INTERNAL_SHAPES, map_values=True ) def __init__(self, adapter, **kwargs): super(Agilent8257D, self).__init__( adapter, "Agilent 8257D RF Signal Generator", **kwargs ) def enable(self): """ Enables the output of the signal. """ self.write(":OUTPUT ON;") def disable(self): """ Disables the output of the signal. """ self.write(":OUTPUT OFF;") def enable_modulation(self): self.write(":OUTPUT:MOD ON;") self.write(":lfo:sour int; :lfo:ampl 2.0vp; :lfo:stat on;") def disable_modulation(self): """ Disables the signal modulation. """ self.write(":OUTPUT:MOD OFF;") self.write(":lfo:stat off;") def config_amplitude_modulation(self, frequency=1e3, depth=100.0, shape='sine'): """ Configures the amplitude modulation of the output signal. :param frequency: A modulation frequency for the internal oscillator :param depth: A linear depth precentage :param shape: A string that describes the shape for the internal oscillator """ self.enable_amplitude_modulation() self.amplitude_source = 'internal' self.internal_frequency = frequency self.internal_shape = shape self.amplitude_depth = depth def enable_amplitude_modulation(self): """ Enables amplitude modulation of the output signal. """ self.write(":SOUR:AM:STAT ON") def disable_amplitude_modulation(self): """ Disables amplitude modulation of the output signal. """ self.write(":SOUR:AM:STAT OFF") def config_pulse_modulation(self, frequency=1e3, input='square'): """ Configures the pulse modulation of the output signal. :param frequency: A pulse rate frequency in Hertz :param input: A string that describes the internal pulse input """ self.enable_pulse_modulation() self.pulse_source = 'internal' self.pulse_input = input self.pulse_frequency = frequency def enable_pulse_modulation(self): """ Enables pulse modulation of the output signal. """ self.write(":SOUR:PULM:STAT ON") def disable_pulse_modulation(self): """ Disables pulse modulation of the output signal. """ self.write(":SOUR:PULM:STAT OFF") def config_step_sweep(self): """ Configures a step sweep through frequency """ self.write(":SOUR:FREQ:MODE SWE;" ":SOUR:SWE:GEN STEP;" ":SOUR:SWE:MODE AUTO;") def enable_retrace(self): self.write(":SOUR:LIST:RETR 1") def disable_retrace(self): self.write(":SOUR:LIST:RETR 0") def single_sweep(self): self.write(":SOUR:TSW") def start_step_sweep(self): """ Starts a step sweep. """ self.write(":SOUR:SWE:CONT:STAT ON") def stop_step_sweep(self): """ Stops a step sweep. """ self.write(":SOUR:SWE:CONT:STAT OFF") def shutdown(self): """ Shuts down the instrument by disabling any modulation and the output signal. """ self.disable_modulation() self.disable() PyMeasure-0.9.0/pymeasure/instruments/agilent/agilent33521A.py0000664000175000017500000000456114010037617024417 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_range from .agilent33500 import Agilent33500 log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Agilent33521A(Agilent33500): """Represents the Agilent 33521A Function/Arbitrary Waveform Generator. This documentation page shows only methods different from the parent class :doc:`Agilent33500 `. """ def __init__(self, adapter, **kwargs): super(Agilent33521A, self).__init__( adapter, **kwargs ) frequency = Instrument.control( "FREQ?", "FREQ %f", """ A floating point property that controls the frequency of the output waveform in Hz, from 1 uHz to 30 MHz, depending on the specified function. Can be set. """, validator=strict_range, values=[1e-6, 30e+6], ) arb_srate = Instrument.control( "FUNC:ARB:SRAT?", "FUNC:ARB:SRAT %f", """ An floating point property that sets the sample rate of the currently selected arbitrary signal. Valid values are 1 µSa/s to 250 MSa/s. This can be set. """, validator=strict_range, values=[1e-6, 250e6], )PyMeasure-0.9.0/pymeasure/instruments/agilent/agilentB1500.py0000664000175000017500000023130214010037617024323 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import weakref import time import re import numpy as np import pandas as pd from enum import IntEnum from collections import Counter, namedtuple, OrderedDict from pymeasure.instruments.validators import (strict_discrete_set, truncated_discrete_set, strict_range, strict_discrete_range) from pymeasure.instruments import (Instrument, RangeException) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) ###################################### # Agilent B1500 Mainframe ###################################### class AgilentB1500(Instrument): """ Represents the Agilent B1500 Semiconductor Parameter Analyzer and provides a high-level interface for taking different kinds of measurements. """ def __init__(self, resourceName, **kwargs): super().__init__( resourceName, "Agilent B1500 Semiconductor Parameter Analyzer", **kwargs ) self._smu_names = {} self._smu_references = {} # setting of data output format # determines how to read measurement data self._data_format = self._data_formatting( "FMT" + self.query_learn(31)['FMT'][0]) @property def smu_references(self): """Returns all SMU instances. """ return self._smu_references.values() @property def smu_names(self): """Returns all SMU names. """ return self._smu_names def query_learn(self, query_type): """Queries settings from the instrument (``*LRN?``). Returns dict of settings. :param query_type: Query type (number according to manual) :type query_type: int or str """ return QueryLearn.query_learn(self.ask, query_type) def query_learn_header(self, query_type, **kwargs): """Queries settings from the instrument (``*LRN?``). Returns dict of settings in human readable format for debugging or file headers. For optional arguments check the underlying definition of :meth:`QueryLearn.query_learn_header`. :param query_type: Query type (number according to manual) :type query_type: int or str """ return QueryLearn.query_learn_header( self.ask, query_type, self._smu_references, **kwargs) def reset(self): """ Resets the instrument to default settings (``*RST``) """ self.write("*RST") def query_modules(self): """ Queries module models from the instrument. Returns dictionary of channel and module type. :return: Channel:Module Type :rtype: dict """ modules = self.ask('UNT?') modules = modules.split(';') module_names = { 'B1525A': 'SPGU', 'B1517A': 'HRSMU', 'B1511A': 'MPSMU', 'B1511B': 'MPSMU', 'B1510A': 'HPSMU', 'B1514A': 'MCSMU', 'B1520A': 'MFCMU' } out = {} for i, module in enumerate(modules): module = module.split(',') if not module[0] == '0': try: out[i+1] = module_names[module[0]] # i+1: channels start at 1 not at 0 except Exception: raise NotImplementedError( 'Module {} is not implented yet!'.format(module[0])) return out def initialize_smu(self, channel, smu_type, name): """ Initializes SMU instance by calling :class:`.SMU`. :param channel: SMU channel :type channel: int :param smu_type: SMU type, e.g. ``'HRSMU'`` :type smu_type: str :param name: SMU name for pymeasure (data output etc.) :type name: str :return: SMU instance :rtype: :class:`.SMU` """ if channel in ( list(range(101, 1101, 100)) + list(range(102, 1102, 100))): channel = int(str(channel)[0:-2]) # subchannels not relevant for SMU/CMU channel = strict_discrete_set(channel, range(1, 11)) self._smu_names[channel] = name smu_reference = SMU(self, channel, smu_type, name) self._smu_references[channel] = smu_reference return smu_reference def initialize_all_smus(self): """ Initialize all SMUs by querying available modules and creating a SMU class instance for each. SMUs are accessible via attributes ``.smu1`` etc. """ modules = self.query_modules() i = 1 for channel, smu_type in modules.items(): if 'SMU' in smu_type: setattr(self, 'smu'+str(i), self.initialize_smu( channel, smu_type, 'SMU'+str(i))) i += 1 def pause(self, pause_seconds): """ Pauses Command Excecution for given time in seconds (``PA``) :param pause_seconds: Seconds to pause :type pause_seconds: int """ self.write("PA %d" % pause_seconds) def abort(self): """ Aborts the present operation but channels may still output current/voltage (``AB``) """ self.write("AB") def force_gnd(self): """ Force 0V on all channels immediately. Current Settings can be restored with RZ. (``DZ``) """ self.write("DZ") def check_errors(self): """ Check for errors (``ERRX?``) """ error = self.ask("ERRX?") error = re.match( r'(?P[+-]?\d+(?:\.\d+)?),"(?P[\w\s.]+)', error).groups() if int(error[0]) == 0: return else: raise IOError( "Agilent B1500 Error {0}: {1}".format(error[0], error[1])) def check_idle(self): """ Check if instrument is idle (``*OPC?``) """ self.ask("*OPC?") def clear_buffer(self): """ Clear output data buffer (``BC``) """ self.write("BC") def clear_timer(self): """ Clear timer count (``TSR``) """ self.write("TSR") def send_trigger(self): """ Send trigger to start measurement (except High Speed Spot) (``XE``)""" self.write("XE") @property def auto_calibration(self): """ Enable/Disable SMU auto-calibration every 30 minutes. (``CM``) :type: bool """ response = self.query_learn(31)['CM'] response = bool(int(response)) return response @auto_calibration.setter def auto_calibration(self, setting): setting = int(setting) self.write('CM %d' % setting) self.check_errors() ###################################### # Data Formatting ###################################### class _data_formatting_generic(): """ Format data output head of measurement value into user readable values :param str output_format_str: Format string of measurement value :param dict smu_names: Dictionary of channel and SMU name """ channels = {"A": 101, "B": 201, "C": 301, "D": 401, "E": 501, "F": 601, "G": 701, "H": 801, "I": 901, "J": 1001, "a": 102, "b": 202, "c": 302, "d": 402, "e": 502, "f": 602, "g": 702, "h": 802, "i": 902, "j": 1002, "V": "GNDU", "Z": "MISC"} status = { 'W': 'First or intermediate sweep step data', 'E': 'Last sweep step data', 'T': 'Another channel reached its compliance setting.', 'C': 'This channel reached its compliance setting', 'V': ('Measurement data is over the measurement range/Sweep was ' 'aborted by automatic stop function or power compliance. ' 'D will be 199.999E+99 (no meaning).'), 'X': ('One or more channels are oscillating. Or source output did ' 'not settle before measurement.'), 'F': 'SMU is in the force saturation condition.', 'G': ('Linear/Binary search measurement: Target value was not ' 'found within the search range. ' 'Returns source output value. ' 'Quasi-pulsed spot measurement: ' 'The detection time was over the limit.'), 'S': ('Linear/Binary search measurement: The search measurement ' 'was stopped. Returns source output value. ' 'Quasi-pulsed spot measurement: Output slew rate was too ' 'slow to perform the settling detection. ' 'Or quasi-pulsed source channel reached compliance before ' 'the source output voltage changed 10V ' 'from the start voltage.'), 'U': 'CMU is in the NULL loop unbalance condition.', 'D': 'CMU is in the IV amplifier saturation condition.' } smu_status = { 1: 'A/D converter overflowed.', 2: 'Oscillation of force or saturation current.', 4: 'Antoher unit reached its compliance setting.', 8: 'This unit reached its compliance setting.', 16: 'Target value was not found within the search range.', 32: 'Search measurement was automatically stopped.', 64: 'Invalid data is returned. D is not used.', 128: 'End of data' } cmu_status = { 1: 'A/D converter overflowed.', 2: 'CMU is in the NULL loop unbalance condition.', 4: 'CMU is in the IV amplifier saturation condition.', 64: 'Invalid data is returned. D is not used.', 128: 'End of data' } data_names_int = {"Sampling index"} # convert to int instead of float def __init__(self, output_format_str, smu_names={}): """ Stores parameters of the chosen output format for later usage in reading and processing instrument data. Data Names: e.g. "Voltage (V)" or "Current Measurement (A)" """ sizes = {"FMT1": 16, "FMT11": 17, "FMT21": 19} try: self.size = sizes[output_format_str] except Exception: raise NotImplementedError( ("Data Format {0} is not " "implemented so far.").format(output_format_str)) self.format = output_format_str data_names_C = { "V": "Voltage (V)", "I": "Current (A)", "F": "Frequency (Hz)", } data_names_CG = { "Z": "Impedance (Ohm)", "Y": "Admittance (S)", "C": "Capacitance (F)", "L": "Inductance (H)", "R": "Phase (rad)", "P": "Phase (deg)", "D": "Dissipation factor", "Q": "Quality factor", "X": "Sampling index", "T": "Time (s)" } data_names_G = { "V": "Voltage Measurement (V)", "I": "Current Measurement (A)", "v": "Voltage Output (V)", "i": "Current Output (A)", "f": "Frequency (Hz)", "z": "invalid data" } if output_format_str in ['FMT1', 'FMT5', 'FMT11', 'FMT15']: self.data_names = {**data_names_C, **data_names_CG} elif output_format_str in ['FMT21', 'FMT25']: self.data_names = {**data_names_G, **data_names_CG} else: self.data_names = {} # no header self.smu_names = smu_names def check_status(self, status_string, name=False, cmu=False): """Check returned status of instrument. If not null or end of data, message is written to log.info. :param status_string: Status string returned by the instrument when reading data. :type status_string: str :param cmu: Whether or not channel is CMU, defaults to False (SMU) :type cmu: bool, optional """ def log_failed(): log.info( ('Agilent B1500: check_status not ' 'possible for status {}').format(status_string)) if name is False: name = '' else: name = ' {}'.format(name) status = re.search( r'(?P[0-9]*)(?P[ A-Z]*)', status_string) # depending on FMT, status may be a letter or up to 3 digits if len(status.group('number')) > 0: status = int(status.group('number')) if status in (0, 128): # 0: no error; 128: End of data return if cmu is True: status_dict = self.cmu_status else: status_dict = self.smu_status for index, digit in enumerate(bin(status)[2:]): # [2:] to chop off 0b if digit == '1': log.info('Agilent B1500{}: {}'.format( name, status_dict[2**index])) elif len(status.group('letter')) > 0: status = status.group('letter') status = status.strip() # remove whitespaces if status not in ['N', 'W', 'E']: try: status = self.status[status] log.info('Agilent B1500{}: {}'.format(name, status)) except KeyError: log_failed() else: log_failed() def format_channel_check_status(self, status_string, channel_string): """Returns channel number for given channel letter. Checks for not null status of the channel and writes according message to log.info. :param status_string: Status string returned by the instrument when reading data. :type status_string: str :param channel_string: Channel string returned by the instrument :type channel_string: str :return: Channel name :rtype: str """ channel = self.channels[channel_string] if isinstance(channel, int): channel = int(str(channel)[0:-2]) # subchannels not relevant for SMU/CMU try: smu_name = self.smu_names[channel] if 'SMU' in smu_name: self.check_status(status_string, name=smu_name, cmu=False) if 'CMU' in smu_name: self.check_status(status_string, name=smu_name, cmu=True) return smu_name except KeyError: self.check_status(status_string) return channel class _data_formatting_FMT1(_data_formatting_generic): """ Data formatting for FMT1 format """ def __init__(self, output_format_string="FMT1", smu_names={}): super().__init__(output_format_string, smu_names=smu_names) def format_single(self, element): """ Format single measurement value :param element: Single measurement value read from the instrument :type element: str :return: Status, channel, data name, value :rtype: (str, str, str, float) """ status = element[0] # one character channel = element[1] data_name = element[2] data_name = self.data_names[data_name] if data_name in self.data_names_int: value = int(float(element[3:])) else: value = float(element[3:]) channel = self.format_channel_check_status(status, channel) return (status, channel, data_name, value) class _data_formatting_FMT11(_data_formatting_FMT1): """ Data formatting for FMT11 format """ def __init__(self, smu_names={}): super().__init__( output_format_string="FMT11", smu_names=smu_names) class _data_formatting_FMT21(_data_formatting_generic): """ Data formatting for FMT21 format """ def __init__(self, smu_names={}): super().__init__("FMT21", smu_names) def format_single(self, element): """ Format single measurement value :param element: Single measurement value read from the instrument :type element: str :return: Status (three digits), channel, data name, value :rtype: (str, str, str, float) """ status = element[0:3] # three digits channel = element[3] data_name = element[4] data_name = self.data_names[data_name] if data_name in self.data_names_int: value = int(float(element[5:])) else: value = float(element[5:]) channel = self.format_channel_check_status(status, channel) return (status, channel, data_name, value) def _data_formatting(self, output_format_str, smu_names={}): """ Return data formatting class for given data format string :param output_format_str: Data output format, e.g. ``FMT21`` :type output_format_str: str :param smu_names: Dictionary of channels and SMU names, defaults to {} :type smu_names: dict, optional :return: Corresponding formatting class :rtype: class """ classes = { "FMT21": self._data_formatting_FMT21, "FMT1": self._data_formatting_FMT1, "FMT11": self._data_formatting_FMT11 } try: format_class = classes[output_format_str] except Exception: raise NotImplementedError( ("Data Format {0} is not implemented " "so far.").format(output_format_str) ) return format_class(smu_names) def data_format(self, output_format, mode=0): """ Specifies data output format. Check Documentation for parameters. Should be called once per session to set the data format for interpreting the measurement values read from the instrument. (``FMT``) :param output_format: Output format string, e.g. ``FMT21`` :type output_format: str :param mode: Data output mode, defaults to 0 (only measurement data is returned) :type mode: int, optional """ output_format = strict_discrete_set( output_format, [1, 2, 3, 4, 5, 11, 12, 13, 14, 15, 21, 22, 25]) mode = strict_range(mode, range(0, 11)) self.write("FMT %d, %d" % (output_format, mode)) self.check_errors() if self._smu_names == {}: print( ('No SMU names available for formatting, ' 'instead channel numbers will be used. ' 'Call data_format after initializing all SMUs.')) log.info( ('No SMU names available for formatting, ' 'instead channel numbers will be used. ' 'Call data_format after initializing all SMUs.')) self._data_format = self._data_formatting( "FMT%d" % output_format, self._smu_names) ###################################### # Measurement Settings ###################################### @property def parallel_meas(self): """ Enable/Disable parallel measurements. Effective for SMUs using HSADC and measurement modes 1,2,10,18. (``PAD``) :type: bool """ response = self.query_learn(110)['PAD'] response = bool(int(response)) return response @parallel_meas.setter def parallel_meas(self, setting): setting = int(setting) self.write('PAD %d' % setting) self.check_errors() def query_meas_settings(self): """Read settings for ``TM``, ``AV``, ``CM``, ``FMT`` and ``MM`` commands (31) from the instrument. """ return self.query_learn_header(31) def query_meas_mode(self): """Read settings for ``MM`` command (part of 31) from the instrument. """ return self.query_learn_header(31, single_command='MM') def meas_mode(self, mode, *args): """ Set Measurement mode of channels. Measurements will be taken in the same order as the SMU references are passed. (``MM``) :param mode: Measurement mode * Spot * Staircase Sweep * Sampling :type mode: :class:`.MeasMode` :param args: SMU references :type args: :class:`.SMU` """ mode = MeasMode.get(mode) cmd = "MM %d" % mode.value for smu in args: if isinstance(smu, SMU): cmd += ", %d" % smu.channel self.write(cmd) self.check_errors() # ADC Setup: AAD, AIT, AV, AZ def query_adc_setup(self): """Read ADC settings (55, 56) from the intrument. """ return {**self.query_learn_header(55), **self.query_learn_header(56)} def adc_setup(self, adc_type, mode, N=''): """ Set up operation mode and parameters of ADC for each ADC type. (``AIT``) Defaults: - HSADC: Auto N=1, Manual N=1, PLC N=1, Time N=0.000002(s) - HRADC: Auto N=6, Manual N=3, PLC N=1 :param adc_type: ADC type :type adc_type: :class:`.ADCType` :param mode: ADC mode :type mode: :class:`.ADCMode` :param N: additional parameter, check documentation, defaults to ``''`` :type N: str, optional """ adc_type = ADCType.get(adc_type) mode = ADCMode.get(mode) if (adc_type == ADCType['HRADC']) and (mode == ADCMode['TIME']): raise ValueError("Time ADC mode is not available for HRADC") command = "AIT %d, %d" % (adc_type.value, mode.value) if not N == '': if mode == ADCMode['TIME']: command += (", %g" % N) else: command += (", %d" % N) self.write(command) self.check_errors() def adc_averaging(self, number, mode='Auto'): """ Set number of averaging samples of the HSADC. (``AV``) Defaults: N=1, Auto :param number: Number of averages :type number: int :param mode: Mode (``'Auto','Manual'``), defaults to 'Auto' :type mode: :class:`.AutoManual`, optional """ if number > 0: number = strict_range(number, range(1, 1024)) mode = AutoManual.get(mode).value self.write("AV %d, %d" % (number, mode)) else: number = strict_range(number, range(-1, -101, -1)) self.write("AV %d" % number) self.check_errors() @property def adc_auto_zero(self): """ Enable/Disable ADC zero function. Halfs the integration time, if off. (``AZ``) :type: bool """ response = self.query_learn(56)['AZ'] response = bool(int(response)) return response @adc_auto_zero.setter def adc_auto_zero(self, setting): setting = int(setting) self.write('AZ %d' % setting) self.check_errors() @property def time_stamp(self): """ Enable/Disable Time Stamp function. (``TSC``) :type: bool """ response = self.query_learn(60)['TSC'] response = bool(int(response)) return response @time_stamp.setter def time_stamp(self, setting): setting = int(setting) self.write('TSC %d' % setting) self.check_errors() def query_time_stamp_setting(self): """Read time stamp settings (60) from the instrument. """ return self.query_learn_header(60) def wait_time(self, wait_type, N, offset=0): """Configure wait time. (``WAT``) :param wait_type: Wait time type :type wait_type: :class:`.WaitTimeType` :param N: Coefficient for initial wait time, default: 1 :type N: float :param offset: Offset for wait time, defaults to 0 :type offset: int, optional """ wait_type = WaitTimeType.get(wait_type).value self.write('WAT %d, %g, %d' % (wait_type, N, offset)) self.check_errors() ###################################### # Sweep Setup ###################################### def query_staircase_sweep_settings(self): """Reads Staircase Sweep Measurement settings (33) from the instrument. """ return self.query_learn_header(33) def sweep_timing(self, hold, delay, step_delay=0, step_trigger_delay=0, measurement_trigger_delay=0): """ Sets Hold Time, Delay Time and Step Delay Time for staircase or multi channel sweep measurement. (``WT``) If not set, all parameters are 0. :param hold: Hold time :type hold: float :param delay: Delay time :type delay: float :param step_delay: Step delay time, defaults to 0 :type step_delay: float, optional :param step_trigger_delay: Trigger delay time, defaults to 0 :type step_trigger_delay: float, optional :param measurement_trigger_delay: Measurement trigger delay time, defaults to 0 :type measurement_trigger_delay: float, optional """ hold = strict_discrete_range(hold, (0, 655.35), 0.01) delay = strict_discrete_range(delay, (0, 65.535), 0.0001) step_delay = strict_discrete_range(step_delay, (0, 1), 0.0001) step_trigger_delay = strict_discrete_range( step_trigger_delay, (0, delay), 0.0001) measurement_trigger_delay = strict_discrete_range( measurement_trigger_delay, (0, 65.535), 0.0001) self.write("WT %g, %g, %g, %g, %g" % (hold, delay, step_delay, step_trigger_delay, measurement_trigger_delay)) self.check_errors() def sweep_auto_abort(self, abort, post='START'): """ Enables/Disables the automatic abort function. Also sets the post measurement condition. (``WM``) :param abort: Enable/Disable automatic abort :type abort: bool :param post: Output after measurement, defaults to 'Start' :type post: :class:`.StaircaseSweepPostOutput`, optional """ abort_values = {True: 2, False: 1} abort = strict_discrete_set(abort, abort_values) abort = abort_values[abort] post = StaircaseSweepPostOutput.get(post) self.write("WM %d, %d" % (abort, post.value)) self.check_errors() ###################################### # Sampling Setup ###################################### def query_sampling_settings(self): """Reads Sampling Measurement settings (47) from the instrument. """ return self.query_learn_header(47) @property def sampling_mode(self): """ Set linear or logarithmic sampling mode. (``ML``) :type: :class:`.SamplingMode` """ response = self.query_learn(47) response = response['ML'] return SamplingMode(response) @sampling_mode.setter def sampling_mode(self, mode): mode = SamplingMode.get(mode).value self.write("ML %d" % mode) self.check_errors() def sampling_timing(self, hold_bias, interval, number, hold_base=0): """ Sets Timing Parameters for the Sampling Measurement (``MT``) :param hold_bias: Bias hold time :type hold_bias: float :param interval: Sampling interval :type interval: float :param number: Number of Samples :type number: int :param hold_base: Base hold time, defaults to 0 :type hold_base: float, optional """ n_channels = self.query_meas_settings()['Measurement Channels'] n_channels = len(n_channels.split(', ')) if interval >= 0.002: hold_bias = strict_discrete_range(hold_bias, (0, 655.35), 0.01) interval = strict_discrete_range(interval, (0, 65.535), 0.001) else: try: hold_bias = strict_discrete_range( hold_bias, (-0.09, -0.0001), 0.0001) except ValueError as error1: try: hold_bias = strict_discrete_range( hold_bias, (0, 655.35), 0.01) except ValueError as error2: raise ValueError( 'Bias hold time does not match either ' + 'of the two possible specifications: ' + '{} {}'.format(error1, error2)) if interval >= 0.0001 + 0.00002 * (n_channels - 1): interval = strict_discrete_range(interval, (0, 0.00199), 0.00001) else: raise ValueError( 'Sampling interval {} is too short.'.format(interval)) number = strict_discrete_range(number, (0, int(100001/n_channels)), 1) # ToDo: different restrictions apply for logarithmic sampling! hold_base = strict_discrete_range(hold_base, (0, 655.35), 0.01) self.write("MT %g, %g, %d, %g" % (hold_bias, interval, number, hold_base)) self.check_errors() def sampling_auto_abort(self, abort, post='Bias'): """ Enables/Disables the automatic abort function. Also sets the post measurement condition. (``MSC``) :param abort: Enable/Disable automatic abort :type abort: bool :param post: Output after measurement, defaults to 'Bias' :type post: :class:`.SamplingPostOutput`, optional """ abort_values = {True: 2, False: 1} abort = strict_discrete_set(abort, abort_values) abort = abort_values[abort] post = SamplingPostOutput.get(post).value self.write("MSC %d, %d" % (abort, post)) self.check_errors() ###################################### # Read out of data ###################################### def read_data(self, number_of_points): """ Reads all data from buffer and returns Pandas DataFrame. Specify number of measurement points for correct splitting of the data list. :param number_of_points: Number of measurement points :type number_of_points: int :return: Measurement Data :rtype: pd.DataFrame """ data = self.read() data = data.split(',') data = np.array(data) data = np.split(data, number_of_points) data = pd.DataFrame(data=data) data = data.applymap(self._data_format.format_single) heads = data.iloc[[0]].applymap(lambda x: ' '.join(x[1:3])) # channel & data_type heads = heads.to_numpy().tolist() # 2D List heads = heads[0] # first row data = data.applymap(lambda x: x[3]) data.columns = heads return data def read_channels(self, nchannels): """ Reads data for 1 measurement point from the buffer. Specify number of measurement channels + sweep sources (depending on data output setting). :param nchannels: Number of channels which return data :type nchannels: int :return: Measurement data :rtype: tuple """ data = self.adapter.read_bytes(self._data_format.size * nchannels) data = data.decode("ASCII") data = data.rstrip('\r,') # ',' if more data in buffer, '\r' if last data point data = data.split(',') data = map(self._data_format.format_single, data) data = tuple(data) return data ###################################### # Queries on all SMUs ###################################### def query_series_resistor(self): """Read series resistor status (53) for all SMUs.""" return self.query_learn_header(53) def query_meas_range_current_auto(self): """Read auto ranging mode status (54) for all SMUs.""" return self.query_learn_header(54) def query_meas_op_mode(self): """Read SMU measurement operation mode (46) for all SMUs.""" return self.query_learn_header(46) def query_meas_ranges(self): """Read measruement ranging status (32) for all SMUs.""" return self.query_learn_header(32) ###################################### # SMU Setup ###################################### class SMU(): """ Provides specific methods for the SMUs of the Agilent B1500 mainframe :param parent: Instance of the B1500 mainframe class :type parent: :class:`.AgilentB1500` :param int channel: Channel number of the SMU :param str smu_type: Type of the SMU :param str name: Name of the SMU """ def __init__(self, parent, channel, smu_type, name, **kwargs): # to allow garbage collection for cyclic references self._b1500 = weakref.proxy(parent) channel = strict_discrete_set(channel, range(1, 11)) self.channel = channel smu_type = strict_discrete_set( smu_type, ['HRSMU', 'MPSMU', 'HPSMU', 'MCSMU', 'HCSMU', 'DHCSMU', 'HVSMU', 'UHCU', 'HVMCU', 'UHVU']) self.voltage_ranging = SMUVoltageRanging(smu_type) self.current_ranging = SMUCurrentRanging(smu_type) self.name = name ########################################## # Wrappers of B1500 communication methods ########################################## def write(self, string): """Wraps :meth:`.Instrument.write` method of B1500. """ self._b1500.write(string) def ask(self, string): """Wraps :meth:`~.Instrument.ask` method of B1500. """ return self._b1500.ask(string) def query_learn(self, query_type, command): """Wraps :meth:`~.AgilentB1500.query_learn` method of B1500. """ response = self._b1500.query_learn(query_type) # query_learn returns settings of all smus # pick setting for this smu only response = response[command + str(self.channel)] return response def check_errors(self): """Wraps :meth:`~.AgilentB1500.check_errors` method of B1500. """ return self._b1500.check_errors() ########################################## def _query_status_raw(self): return self._b1500.query_learn(str(self.channel)) @property def status(self): """Query status of the SMU.""" return self._b1500.query_learn_header(str(self.channel)) def enable(self): """ Enable Source/Measurement Channel (``CN``)""" self.write("CN %d" % self.channel) def disable(self): """ Disable Source/Measurement Channel (``CL``)""" self.write("CL %d" % self.channel) def force_gnd(self): """ Force 0V immediately. Current Settings can be restored with ``RZ`` (not implemented). (``DZ``)""" self.write("DZ %d" % self.channel) @property def filter(self): """ Enables/Disables SMU Filter. (``FL``) :type: bool """ # different than other SMU specific settings (grouped by setting) # read via raw command response = self._b1500.query_learn(30) if 'FL' in response.keys(): # only present if filters of all channels are off return False else: if str(self.channel) in response['FL0']: return False elif str(self.channel) in response['FL1']: return True else: raise NotImplementedError('Filter Value cannot be read!') @filter.setter def filter(self, setting): setting = strict_discrete_set(int(setting), (0, 1)) self.write("FL %d, %d" % (self.channel, setting)) self.check_errors() @property def series_resistor(self): """ Enables/Disables 1MOhm series resistor. (``SSR``) :type: bool """ response = self.query_learn(53, 'SSR') response = bool(int(response)) return response @series_resistor.setter def series_resistor(self, setting): setting = strict_discrete_set(int(setting), (0, 1)) self.write("SSR %d, %d" % (self.channel, setting)) self.check_errors() @property def meas_op_mode(self): """ Set SMU measurement operation mode. (``CMM``) :type: :class:`.MeasOpMode` """ response = self.query_learn(46, 'CMM') response = int(response) return MeasOpMode(response) @meas_op_mode.setter def meas_op_mode(self, op_mode): op_mode = MeasOpMode.get(op_mode) self.write("CMM %d, %d" % (self.channel, op_mode.value)) self.check_errors() @property def adc_type(self): """ADC type of individual measurement channel. (``AAD``) :type: :class:`.ADCType` """ response = self.query_learn(55, 'AAD') response = int(response) return ADCType(response) @adc_type.setter def adc_type(self, adc_type): adc_type = ADCType.get(adc_type) self.write("AAD %d, %d" % (self.channel, adc_type.value)) self.check_errors() ###################################### # Force Constant Output ###################################### def force(self, source_type, source_range, output, comp='', comp_polarity='', comp_range=''): """ Applies DC Current or Voltage from SMU immediately. (``DI``, ``DV``) :param source_type: Source type (``'Voltage','Current'``) :type source_type: str :param source_range: Output range index or name :type source_range: int or str :param output: Source output value in A or V :type outout: float :param comp: Compliance value, defaults to previous setting :type comp: float, optional :param comp_polarity: Compliance polairty, defaults to auto :type comp_polarity: :class:`.CompliancePolarity` :param comp_range: Compliance ranging type, defaults to auto :type comp_range: int or str, optional """ if source_type.upper() == "VOLTAGE": cmd = "DV" source_range = self.voltage_ranging.output(source_range).index if not comp_range == '': comp_range = self.current_ranging.meas(comp_range).index elif source_type.upper() == "CURRENT": cmd = "DI" source_range = self.current_ranging.output(source_range).index if not comp_range == '': comp_range = self.voltage_ranging.meas(comp_range).index else: raise ValueError("Source Type must be Current or Voltage.") cmd += " %d, %d, %g" % (self.channel, source_range, output) if not comp == '': cmd += ", %g" % comp if not comp_polarity == '': comp_polarity = CompliancePolarity.get(comp_polarity).value cmd += ", %d" % comp_polarity if not comp_range == '': cmd += ", %d" % comp_range self.write(cmd) self.check_errors() def ramp_source(self, source_type, source_range, target_output, comp='', comp_polarity='', comp_range='', stepsize=0.001, pause=20e-3): """ Ramps to a target output from the set value with a given step size, each separated by a pause. :param source_type: Source type (``'Voltage'`` or ``'Current'``) :type source_type: str :param target_output: Target output voltage or current :type: target_output: float :param irange: Output range index :type irange: int :param comp: Compliance, defaults to previous setting :type comp: float, optional :param comp_polarity: Compliance polairty, defaults to auto :type comp_polarity: :class:`.CompliancePolarity` :param comp_range: Compliance ranging type, defaults to auto :type comp_range: int or str, optional :param stepsize: Maximum size of steps :param pause: Duration in seconds to wait between steps """ if source_type.upper() == "VOLTAGE": source_type = 'VOLTAGE' cmd = 'DV%d' % self.channel source_range = self.voltage_ranging.output(source_range).index unit = 'V' if not comp_range == '': comp_range = self.current_ranging.meas(comp_range).index elif source_type.upper() == "CURRENT": source_type = 'CURRENT' cmd = 'DI%d' % self.channel source_range = self.current_ranging.output(source_range).index unit = 'A' if not comp_range == '': comp_range = self.voltage_ranging.meas(comp_range).index else: raise ValueError("Source Type must be Current or Voltage.") status = self._query_status_raw() if 'CL' in status: # SMU is OFF start = 0 elif cmd in status: start = float(status[cmd][1]) # current output value else: log.info( ("{0} in different state. " "Changing to {1} Source.").format(self.name, source_type)) start = 0 # calculate number of points based on maximum stepsize nop = np.ceil(abs((target_output - start) / stepsize)) nop = int(nop) log.info("{0} ramping from {1}{2} to {3}{2} in {4} steps".format( self.name, start, unit, target_output, nop )) outputs = np.linspace(start, target_output, nop, endpoint=False) for output in outputs: # loop is only executed if target_output != start self.force( source_type, source_range, output, comp, comp_polarity, comp_range) time.sleep(pause) # call force even if start==target_output # to set compliance self.force( source_type, source_range, target_output, comp, comp_polarity, comp_range) ###################################### # Measurement Range # implemented: RI, RV # not implemented: RC, TI, TTI, TV, TTV, TIV, TTIV, TC, TTC ###################################### @property def meas_range_current(self): """ Current measurement range index. (``RI``) Possible settings depend on SMU type, e.g. ``0`` for Auto Ranging: :class:`.SMUCurrentRanging` """ response = self.query_learn(32, 'RI') response = self.current_ranging.meas(response) return response @meas_range_current.setter def meas_range_current(self, meas_range): meas_range_index = self.current_ranging.meas(meas_range).index self.write("RI %d, %d" % (self.channel, meas_range_index)) self.check_errors() @property def meas_range_voltage(self): """ Voltage measurement range index. (``RV``) Possible settings depend on SMU type, e.g. ``0`` for Auto Ranging: :class:`.SMUVoltageRanging` """ response = self.query_learn(32, 'RV') response = self.voltage_ranging.meas(response) return response @meas_range_voltage.setter def meas_range_voltage(self, meas_range): meas_range_index = self.voltage_ranging.meas(meas_range).index self.write("RV %d, %d" % (self.channel, meas_range_index)) self.check_errors() def meas_range_current_auto(self, mode, rate=50): """ Specifies the auto range operation. Check Documentation. (``RM``) :param mode: Range changing operation mode :type mode: int :param rate: Parameter used to calculate the *current* value, defaults to 50 :type rate: int, optional """ mode = strict_range(mode, range(1, 4)) if mode == 1: self.write("RM %d, %d" % (self.channel, mode)) else: self.write("RM %d, %d, %d" % (self.channel, mode, rate)) self.write ###################################### # Staircase Sweep Measurement: (WT, WM -> Instrument) # implemented: # WV, WI, # WSI, WSV (synchronous output) # not implemented: BSSI, BSSV, LSSI, LSSV ###################################### def staircase_sweep_source(self, source_type, mode, source_range, start, stop, steps, comp, Pcomp=''): """ Specifies Staircase Sweep Source (Current or Voltage) and its parameters. (``WV`` or ``WI``) :param source_type: Source type (``'Voltage','Current'``) :type source_type: str :param mode: Sweep mode :type mode: :class:`.SweepMode` :param source_range: Source range index :type source_range: int :param start: Sweep start value :type start: float :param stop: Sweep stop value :type stop: float :param steps: Number of sweep steps :type steps: int :param comp: Compliance value :type comp: float :param Pcomp: Power compliance, defaults to not set :type Pcomp: float, optional """ if source_type.upper() == "VOLTAGE": cmd = "WV" source_range = self.voltage_ranging.output(source_range).index elif source_type.upper() == "CURRENT": cmd = "WI" source_range = self.current_ranging.output(source_range).index else: raise ValueError("Source Type must be Current or Voltage.") mode = SweepMode.get(mode).value if mode in [2, 4]: if start >= 0 and stop >= 0: pass elif start <= 0 and stop <= 0: pass else: raise ValueError( ("For Log Sweep Start and Stop Values must " "have the same polarity.")) steps = strict_range(steps, range(1, 10002)) # check on comp value not yet implemented cmd += ("%d, %d, %d, %g, %g, %g, %g" % (self.channel, mode, source_range, start, stop, steps, comp)) if not Pcomp == '': cmd += ", %g" % Pcomp self.write(cmd) self.check_errors() # Synchronous Output: WSI, WSV, BSSI, BSSV, LSSI, LSSV def synchronous_sweep_source(self, source_type, source_range, start, stop, comp, Pcomp=''): """ Specifies Synchronous Staircase Sweep Source (Current or Voltage) and its parameters. (``WSV`` or ``WSI``) :param source_type: Source type (``'Voltage','Current'``) :type source_type: str :param source_range: Source range index :type source_range: int :param start: Sweep start value :type start: float :param stop: Sweep stop value :type stop: float :param comp: Compliance value :type comp: float :param Pcomp: Power compliance, defaults to not set :type Pcomp: float, optional """ if source_type.upper() == "VOLTAGE": cmd = "WSV" source_range = self.voltage_ranging.output(source_range).index elif source_type.upper() == "CURRENT": cmd = "WSI" source_range = self.current_ranging.output(source_range).index else: raise ValueError("Source Type must be Current or Voltage.") # check on comp value not yet implemented cmd += ("%d, %d, %g, %g, %g" % (self.channel, source_range, start, stop, comp)) if not Pcomp == '': cmd += ", %g" % Pcomp self.write(cmd) self.check_errors() ###################################### # Sampling Measurements: (ML, MT -> Instrument) # implemented: MV, MI # not implemented: MSP, MCC, MSC ###################################### def sampling_source(self, source_type, source_range, base, bias, comp): """ Sets DC Source (Current or Voltage) for sampling measurement. DV/DI commands on the same channel overwrite this setting. (``MV`` or ``MI``) :param source_type: Source type (``'Voltage','Current'``) :type source_type: str :param source_range: Source range index :type source_range: int :param base: Base voltage/current :type base: float :param bias: Bias voltage/current :type bias: float :param comp: Compliance value :type comp: float """ if source_type.upper() == "VOLTAGE": cmd = "MV" source_range = self.voltage_ranging.output(source_range).index elif source_type.upper() == "CURRENT": cmd = "MI" source_range = self.current_ranging.output(source_range).index else: raise ValueError("Source Type must be Current or Voltage.") # check on comp value not yet implemented cmd += ("%d, %d, %g, %g, %g" % (self.channel, source_range, base, bias, comp)) self.write(cmd) self.check_errors() ############################################################################### # Additional Classes / Constants ############################################################################### class Ranging(): """Possible Settings for SMU Current/Voltage Output/Measurement ranges. Transformation of available Voltage/Current Range Names to Index and back. :param supported_ranges: Ranges which are supported (list of range indizes) :type supported_ranges: list :param ranges: All range names ``{Name: Indizes}`` :type ranges: dict :param fixed_ranges: add fixed ranges (negative indizes); defaults to False :type inverse_ranges: bool, optional .. automethod:: __call__ """ _Range = namedtuple('Range', 'name index') def __init__(self, supported_ranges, ranges, fixed_ranges=False): if fixed_ranges: # add negative indizes for measurement ranges (fixed ranging) supported_ranges += [-i for i in supported_ranges] # remove duplicates (0) supported_ranges = list(dict.fromkeys(supported_ranges)) # create dictionary {Index: Range Name} # distinguish between limited and fixed ranging # omitting 'limited auto ranging'/'range fixed' # defaults to 'limited auto ranging' inverse_ranges = {0: 'Auto Ranging'} for key, value in ranges.items(): if isinstance(value, tuple): for v in value: inverse_ranges[v] = (key + ' limited auto ranging', key) inverse_ranges[-v] = (key + ' range fixed') else: inverse_ranges[value] = (key + ' limited auto ranging', key) inverse_ranges[-value] = (key + ' range fixed') ranges = {} indizes = {} # only take ranges supported by SMU for i in supported_ranges: name = inverse_ranges[i] # check if multiple names exist for index i if isinstance(name, tuple): ranges[i] = name[0] # first entry is main name (unique) and # returned as .name attribute, # additional entries are just synonyms and can # be used to get the range tuple # e.g. '1 nA limited auto ranging' is identifier and # returned as range name # but '1 nA' also works to get the range tuple for name2 in name: indizes[name2] = i else: # only one name per index ranges[i] = name # Index -> Name, Name not unique indizes[name] = i # Name -> Index, only one Index per Name # convert all string type keys to uppercase, to avoid case-sensitivity indizes = {key.upper(): value for key, value in indizes.items()} self.indizes = indizes # Name -> Index self.ranges = ranges # Index -> Name def __call__(self, input_value): """Gives named tuple (name/index) of given Range. Throws error if range is not supported by this SMU. :param input: Range name or index :type input: str or int :return: named tuple (name/index) of range :rtype: namedtuple """ # set index if isinstance(input_value, int): index = input_value else: try: index = self.indizes[input_value.upper()] except Exception: raise ValueError( ('Specified Range Name {} is not valid or ' 'not supported by this SMU').format(input_value.upper())) # get name try: name = self.ranges[index] except Exception: raise ValueError( ('Specified Range {} is not supported ' 'by this SMU').format(index)) return self._Range(name=name, index=index) class SMUVoltageRanging(): """ Provides Range Name/Index transformation for voltage measurement/sourcing. Validity of ranges is checked against the type of the SMU. Omitting the 'limited auto ranging'/'range fixed' specification in the range string for voltage measurement defaults to 'limited auto ranging'. Full specification: '2 V range fixed' or '2 V limited auto ranging' '2 V' defaults to '2 V limited auto ranging' """ def __init__(self, smu_type): supported_ranges = { 'HRSMU': [0, 5, 11, 20, 50, 12, 200, 13, 400, 14, 1000], 'MPSMU': [0, 5, 11, 20, 50, 12, 200, 13, 400, 14, 1000], 'HPSMU': [0, 11, 20, 12, 200, 13, 400, 14, 1000, 15, 2000], 'MCSMU': [0, 2, 11, 20, 12, 200, 13, 400], 'HCSMU': [0, 2, 11, 20, 12, 200, 13, 400], 'DHCSMU': [0, 2, 11, 20, 12, 200, 13, 400], 'HVSMU': [0, 15, 2000, 5000, 15000, 30000], 'UHCU': [0, 14, 1000], 'HVMCU': [0, 15000, 30000], 'UHVU': [0, 103] } supported_ranges = supported_ranges[smu_type] ranges = { '0.2 V': 2, '0.5 V': 5, '2 V': (11, 20), '5 V': 50, '20 V': (12, 200), '40 V': (13, 400), '100 V': (14, 1000), '200 V': (15, 2000), '500 V': 5000, '1500 V': 15000, '3000 V': 30000, '10 kV': 103 } # set range attributes self.output = Ranging(supported_ranges, ranges) self.meas = Ranging(supported_ranges, ranges, fixed_ranges=True) class SMUCurrentRanging(): """ Provides Range Name/Index transformation for current measurement/sourcing. Validity of ranges is checked against the type of the SMU. Omitting the 'limited auto ranging'/'range fixed' specification in the range string for current measurement defaults to 'limited auto ranging'. Full specification: '1 nA range fixed' or '1 nA limited auto ranging' '1 nA' defaults to '1 nA limited auto ranging' """ def __init__(self, smu_type): supported_output_ranges = { # in combination with ASU also 8 'HRSMU': [0, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], # in combination with ASU also 8,9,10 'MPSMU': [0, 11, 12, 13, 14, 15, 16, 17, 18, 19], 'HPSMU': [0, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 'MCSMU': [0, 15, 16, 17, 18, 19, 20], 'HCSMU': [0, 15, 16, 17, 18, 19, 20, 22], 'DHCSMU': [0, 15, 16, 17, 18, 19, 20, 21, 23], 'HVSMU': [0, 11, 12, 13, 14, 15, 16, 17, 18], 'UHCU': [0, 26, 28], 'HVMCU': [], 'UHVU': [] } supported_meas_ranges = { **supported_output_ranges, # overwrite output ranges: 'HVMCU': [0, 19, 21], 'UHVU': [0, 15, 16, 17, 18, 19] } supported_output_ranges = supported_output_ranges[smu_type] supported_meas_ranges = supported_meas_ranges[smu_type] ranges = { '1 pA': 8, # for ASU '10 pA': 9, '100 pA': 10, '1 nA': 11, '10 nA': 12, '100 nA': 13, '1 uA': 14, '10 uA': 15, '100 uA': 16, '1 mA': 17, '10 mA': 18, '100 mA': 19, '1 A': 20, '2 A': 21, '20 A': 22, '40 A': 23, '500 A': 26, '2000 A': 28 } # set range attributes self.output = Ranging(supported_output_ranges, ranges) self.meas = Ranging(supported_meas_ranges, ranges, fixed_ranges=True) class CustomIntEnum(IntEnum): """Provides additional methods to IntEnum: * Conversion to string automatically replaces '_' with ' ' in names and converts to title case * get classmethod to get enum reference with name or integer .. automethod:: __str__ """ def __str__(self): """Gives title case string of enum value """ return str(self.name).replace("_", " ").title() # str() conversion just because of pylint bug @classmethod def get(cls, input_value): """Gives Enum member by specifying name or value. :param input_value: Enum name or value :type input_value: str or int :return: Enum member """ if isinstance(input_value, int): return cls(input_value) else: return cls[input_value.upper()] class ADCType(CustomIntEnum): """ADC Type""" HSADC = 0, #: High-speed ADC HRADC = 1, #: High-resolution ADC HSADC_PULSED = 2, #: High-resolution ADC for pulsed measurements def __str__(self): return str(self.name).replace("_", " ") # .title() str() conversion just because of pylint bug class ADCMode(CustomIntEnum): """ADC Mode""" AUTO = 0 #: MANUAL = 1 #: PLC = 2 #: TIME = 3 #: class AutoManual(CustomIntEnum): """Auto/Manual selection""" AUTO = 0 #: MANUAL = 1 #: class MeasMode(CustomIntEnum): """Measurement Mode""" SPOT = 1 #: STAIRCASE_SWEEP = 2 #: SAMPLING = 10 #: class MeasOpMode(CustomIntEnum): """Measurement Operation Mode""" COMPLIANCE_SIDE = 0 #: CURRENT = 1 #: VOLTAGE = 2 #: FORCE_SIDE = 3 #: COMPLIANCE_AND_FORCE_SIDE = 4 #: class SweepMode(CustomIntEnum): """Sweep Mode""" LINEAR_SINGLE = 1 #: LOG_SINGLE = 2 #: LINEAR_DOUBLE = 3 #: LOG_DOUBLE = 4 #: class SamplingMode(CustomIntEnum): """Sampling Mode""" LINEAR = 1 #: LOG_10 = 2 #: Logarithmic 10 data points/decade LOG_25 = 3 #: Logarithmic 25 data points/decade LOG_50 = 4 #: Logarithmic 50 data points/decade LOG_100 = 5 #: Logarithmic 100 data points/decade LOG_250 = 6 #: Logarithmic 250 data points/decade LOG_5000 = 7 #: Logarithmic 5000 data points/decade def __str__(self): names = { 1: "Linear", 2: "Log 10 data/decade", 3: "Log 25 data/decade", 4: "Log 50 data/decade", 5: "Log 100 data/decade", 6: "Log 250 data/decade", 7: "Log 5000 data/decade"} return names[self.value] class SamplingPostOutput(CustomIntEnum): """Output after sampling""" BASE = 1 #: BIAS = 2 #: class StaircaseSweepPostOutput(CustomIntEnum): """Output after staircase sweep""" START = 1 #: STOP = 2 #: class CompliancePolarity(CustomIntEnum): """Compliance polarity""" AUTO = 0 #: MANUAL = 1 #: class WaitTimeType(CustomIntEnum): """Wait time type""" SMU_SOURCE = 1 #: SMU_MEASUREMENT = 2 #: CMU_MEASUREMENT = 3 #: ############################################################################### # Query Learn: Parse Instrument settings into human readable format ############################################################################### class QueryLearn(): """Methods to issue and process ``*LRN?`` (learn) command and response.""" @staticmethod def query_learn(ask, query_type): """ Issues ``*LRN?`` (learn) command to the instrument to read configuration. Returns dictionary of commands and set values. :param query_type: Query type according to the programming guide :type query_type: int :return: Dictionary of command and set values :rtype: dict """ response = ask("*LRN? "+str(query_type)) # response.split(';') response = re.findall( r'(?P[A-Z]+)(?P[0-9,\+\-\.E]+)', response) # check if commands are unique -> suitable as keys for dict counts = Counter([item[0] for item in response]) # responses that start with a channel number # the channel number should always be included in the key include_chnum = [ 'DI', 'DV', # Sourcing 'RI', 'RV', # Ranging 'WV', 'WI', 'WSV', 'WSI', # Staircase Sweep 'PV', 'PI', 'PWV', 'PWI', # Pulsed Source 'MV', 'MI', 'MSP', # Sampling 'SSR', 'RM', 'AAD' # Series Resistor, Auto Ranging, ADC ] # probably not complete yet... response_dict = {} for element in response: parameters = element[1].split(',') name = element[0] if (counts[name] > 1) or (name in include_chnum): # append channel (first parameter) to command as dict key name += parameters[0] parameters = parameters[1:] if len(parameters) == 1: parameters = parameters[0] # skip second AAD entry for each channel -> contains no information if 'AAD' in name and name in response_dict.keys(): continue response_dict[name] = parameters return response_dict @classmethod def query_learn_header(cls, ask, query_type, smu_references, single_command=False): """Issues ``*LRN?`` (learn) command to the instrument to read configuration. Processes information to human readable values for debugging purposes or file headers. :param ask: ask method of the instrument :type ask: Instrument.ask :param query_type: Number according to Programming Guide :type query_type: int or str :param smu_references: SMU references by channel :type smu_references: dict :param single_command: if only a single command should be returned, defaults to False :type single_command: str :return: Read configuration :rtype: dict """ response = cls.query_learn(ask, query_type) if single_command is not False: response = response[single_command] ret = {} for key, value in response.items(): # command without channel command = re.findall(r'(?P[A-Z]+)', key)[0] new_dict = getattr(cls, command)( key, value, smu_references=smu_references) ret = {**ret, **new_dict} return ret @staticmethod def to_dict(parameters, names, *args): """ Takes parameters returned by :meth:`query_learn` and ordered list of corresponding parameter names (optional function) and returns dict of parameters including names. :param parameters: Parameters for one command returned by :meth:`query_learn` :type parameters: dict :param names: list of names or (name, function) tuples, ordered :type names: list :return: Parameter name and (processed) parameter :rtype: dict """ ret = OrderedDict() if isinstance(parameters, str): # otherwise string is enumerated parameters_iter = [(0, parameters)] else: parameters_iter = enumerate(parameters) for i, parameter in parameters_iter: if isinstance(names[i], tuple): ret[names[i][0]] = names[i][1](parameter, *args) else: ret[names[i]] = parameter return ret @staticmethod def _get_smu(key, smu_references): # command without channel command = re.findall(r'(?P[A-Z]+)', key)[0] channel = key[len(command):] return smu_references[int(channel)] # SMU Modes @classmethod def DI(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ ('Current Range', lambda parameter: smu.current_ranging.output(int(parameter)).name), 'Current Output (A)', 'Compliance Voltage (V)', ('Compliance Polarity', lambda parameter: str(CompliancePolarity.get(int(parameter)))), ('Voltage Compliance Ranging Type', lambda parameter: smu.voltage_ranging.meas(int(parameter)).name) ] ret = cls.to_dict(parameters, names) ret['Source Type'] = 'Constant Current' ret.move_to_end('Source Type', last=False) # make first entry return {smu.name: ret} @classmethod def DV(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ ('Voltage Range', lambda parameter: smu.voltage_ranging.output(int(parameter)).name), 'Voltage Output (V)', 'Compliance Current (A)', ('Compliance Polarity', lambda parameter: str(CompliancePolarity.get(int(parameter)))), ('Current Compliance Ranging Type', lambda parameter: smu.current_ranging.meas(int(parameter)).name) ] ret = cls.to_dict(parameters, names) ret['Source Type'] = 'Constant Voltage' ret.move_to_end('Source Type', last=False) # make first entry return {smu.name: ret} @classmethod def CL(cls, key, parameters, smu_references={}): smu = cls._get_smu(key + parameters, smu_references) return {smu.name: 'OFF'} # Instrument Settings: 31 @classmethod def TM(cls, key, parameters, smu_references={}): names = ['Trigger Mode'] # enum + setting not implemented yet return cls.to_dict(parameters, names) @classmethod def AV(cls, key, parameters, smu_references={}): names = [ 'ADC Averaging Number', ('ADC Averaging Mode', lambda parameter: str(AutoManual(int(parameter)))) ] return cls.to_dict(parameters, names) @classmethod def CM(cls, key, parameters, smu_references={}): names = [ ('Auto Calibration Mode', lambda parameter: bool(int(parameter))) ] return cls.to_dict(parameters, names) @classmethod def FMT(cls, key, parameters, smu_references={}): names = ['Output Data Format', 'Output Data Mode'] # enum + setting not implemented yet return cls.to_dict(parameters, names) @classmethod def MM(cls, key, parameters, smu_references={}): names = [ ('Measurement Mode', lambda parameter: str(MeasMode(int(parameter)))) ] ret = cls.to_dict(parameters[0], names) smu_names = [] for channel in parameters[1:]: smu_names.append(smu_references[int(channel)].name) ret['Measurement Channels'] = ', '.join(smu_names) return ret # Measurement Ranging: 32 @classmethod def RI(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ (smu.name + ' Current Measurement Range', lambda parameter: smu.current_ranging.meas(int(parameter)).name) ] return cls.to_dict(parameters, names) @classmethod def RV(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ (smu.name + ' Voltage Measurement Range', lambda parameter: smu.voltage_ranging.meas(int(parameter)).name) ] return cls.to_dict(parameters, names) # Sweep: 33 @classmethod def WM(cls, key, parameters, smu_references={}): names = [ ('Auto Abort Status', lambda parameter: {2: True, 1: False}[int(parameter)]), ('Output after Measurement', lambda parameter: str(StaircaseSweepPostOutput(int(parameter)))) ] return cls.to_dict(parameters, names) @classmethod def WT(cls, key, parameters, smu_references={}): names = [ 'Hold Time (s)', 'Delay Time (s)', 'Step Delay Time (s)', 'Step Source Trigger Delay Time (s)', 'Step Measurement Trigger Delay Time (s)' ] return cls.to_dict(parameters, names) @classmethod def WV(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ ("Sweep Mode", lambda parameter: str(SweepMode(int(parameter)))), ("Voltage Range", lambda parameter: smu.voltage_ranging.output(int(parameter)).name), "Start Voltage (V)", "Stop Voltage (V)", "Number of Steps", "Current Compliance (A)", "Power Compliance (W)" ] ret = cls.to_dict(parameters, names) ret['Source Type'] = 'Voltage Sweep Source' ret.move_to_end('Source Type', last=False) # make first entry return {smu.name: ret} @classmethod def WI(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ ("Sweep Mode", lambda parameter: str(SweepMode(int(parameter)))), ("Current Range", lambda parameter: smu.current_ranging.output(int(parameter)).name), "Start Current (A)", "Stop Current (A)", "Number of Steps", "Voltage Compliance (V)", "Power Compliance (W)" ] ret = cls.to_dict(parameters, names) ret['Source Type'] = 'Current Sweep Source' ret.move_to_end('Source Type', last=False) # make first entry return {smu.name: ret} @classmethod def WSV(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ ("Voltage Range", lambda parameter: smu.voltage_ranging.output(int(parameter)).name), "Start Voltage (V)", "Stop Voltage (V)", "Current Compliance (A)", "Power Compliance (W)" ] ret = cls.to_dict(parameters, names) ret['Source Type'] = 'Synchronous Voltage Sweep Source' ret.move_to_end('Source Type', last=False) # make first entry return {smu.name: ret} @classmethod def WSI(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ ("Current Range", lambda parameter: smu.current_ranging.output(int(parameter)).name), "Start Current (A)", "Stop Current (A)", "Voltage Compliance (V)", "Power Compliance (W)" ] ret = cls.to_dict(parameters, names) ret['Source Type'] = 'Synchronous Current Sweep Source' ret.move_to_end('Source Type', last=False) # make first entry return {smu.name: ret} # SMU Measurement Operation Mode: 46 @classmethod def CMM(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ (smu.name + ' Measurement Operation Mode', lambda parameter: str(MeasOpMode(int(parameter)))) ] return cls.to_dict(parameters, names) # Sampling: 47 @classmethod def MSC(cls, key, parameters, smu_references={}): names = [ ('Auto Abort Status', lambda parameter: {2: True, 1: False}[int(parameter)]), ('Output after Measurement', lambda parameter: str(SamplingPostOutput(int(parameter)))) ] return cls.to_dict(parameters, names) @classmethod def MT(cls, key, parameters, smu_references={}): names = [ 'Hold Bias Time (s)', 'Sampling Interval (s)', 'Number of Samples', 'Hold Base Time (s)' ] return cls.to_dict(parameters, names) @classmethod def ML(cls, key, parameters, smu_references={}): names = [ ('Sampling Mode', lambda parameter: str(SamplingMode(int(parameter)))) ] return cls.to_dict(parameters, names) @classmethod def MV(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ ("Voltage Range", lambda parameter: smu.voltage_ranging.output(int(parameter)).name), "Base Voltage (V)", "Bias Voltage (V)", "Current Compliance (A)" ] ret = cls.to_dict(parameters, names) ret['Source Type'] = 'Voltage Source Sampling' ret.move_to_end('Source Type', last=False) # make first entry return {smu.name: ret} @classmethod def MI(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ ("Current Range", lambda parameter: smu.current_ranging.output(int(parameter)).name), "Base Current (A)", "Bias Current (A)", "Voltage Compliance (V)" ] ret = cls.to_dict(parameters, names) ret['Source Type'] = 'Current Source Sampling' ret.move_to_end('Source Type', last=False) # make first entry return {smu.name: ret} # SMU Series Resistor: 53 @classmethod def SSR(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ (smu.name + ' Series Resistor', lambda parameter: bool(int(parameter))) ] return cls.to_dict(parameters, names) # Auto Ranging Mode: 54 @classmethod def RM(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ smu.name + ' Ranging Mode', smu.name + ' Ranging Mode Parameter' ] return cls.to_dict(parameters, names) # ADC: 55, 56 @classmethod def AAD(cls, key, parameters, smu_references={}): smu = cls._get_smu(key, smu_references) names = [ (smu.name + ' ADC', lambda parameter: str(ADCType(int(parameter)))) ] return cls.to_dict(parameters, names) @classmethod def AIT(cls, key, parameters, smu_references={}): adc_type = key[3:] adc_name = str(ADCType(int(adc_type))) names = [ (adc_name + ' Mode', lambda parameter: str(ADCMode(int(parameter)))), adc_name + ' Parameter' ] return cls.to_dict(parameters, names) @classmethod def AZ(cls, key, parameters, smu_references={}): names = [ ('ADC Auto Zero', lambda parameter: str(bool(int(parameter)))) ] return cls.to_dict(parameters, names) # Time Stamp: 60 @classmethod def TSC(cls, key, parameters, smu_references={}): names = [ ('Time Stamp', lambda parameter: str(bool(int(parameter)))) ] return cls.to_dict(parameters, names) PyMeasure-0.9.0/pymeasure/instruments/agilent/agilent33220A.py0000664000175000017500000002767014010037617024421 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set,\ strict_range, joined_validators from time import time from pyvisa.errors import VisaIOError # Capitalize string arguments to allow for better conformity with other WFG's def capitalize_string(string: str, *args, **kwargs): return string.upper() # Combine the capitalize function and validator string_validator = joined_validators(capitalize_string, strict_discrete_set) class Agilent33220A(Instrument): """Represents the Agilent 33220A Arbitrary Waveform Generator. .. code-block:: python # Default channel for the Agilent 33220A wfg = Agilent33220A("GPIB::10") wfg.shape = "SINUSOID" # Sets a sine waveform wfg.frequency = 4.7e3 # Sets the frequency to 4.7 kHz wfg.amplitude = 1 # Set amplitude of 1 V wfg.offset = 0 # Set the amplitude to 0 V wfg.burst = True # Enable burst mode wfg.burst_ncycles = 10e3 # A burst will consist of 10 cycles wfg.burst_mode = "TRIGGERED" # A burst will be applied on a trigger wfg.trigger_source = "BUS" # A burst will be triggered on TRG* wfg.output = True # Enable output of waveform generator wfg.trigger() # Trigger a burst wfg.wait_for_trigger() # Wait until the triggering is finished wfg.beep() # "beep" wfg.check_errors() # Get the error queue """ def __init__(self, adapter, **kwargs): super(Agilent33220A, self).__init__( adapter, "Agilent 33220A Arbitrary Waveform generator", **kwargs ) shape = Instrument.control( "FUNC?", "FUNC %s", """ A string property that controls the output waveform. Can be set to: SIN, SQU, RAMP, PULS, NOIS, DC, USER. """, validator=string_validator, values=["SINUSOID", "SIN", "SQUARE", "SQU", "RAMP", "PULSE", "PULS", "NOISE", "NOIS", "DC", "USER"], ) frequency = Instrument.control( "FREQ?", "FREQ %s", """ A floating point property that controls the frequency of the output waveform in Hz, from 1e-6 (1 uHz) to 20e+6 (20 MHz), depending on the specified function. Can be set. """, validator=strict_range, values=[1e-6, 5e+6], ) amplitude = Instrument.control( "VOLT?", "VOLT %f", """ A floating point property that controls the voltage amplitude of the output waveform in V, from 10e-3 V to 10 V. Can be set. """, validator=strict_range, values=[10e-3, 10], ) amplitude_unit = Instrument.control( "VOLT:UNIT?", "VOLT:UNIT %s", """ A string property that controls the units of the amplitude. Valid values are Vpp (default), Vrms, and dBm. Can be set. """, validator=string_validator, values=["VPP", "VRMS", "DBM"], ) offset = Instrument.control( "VOLT:OFFS?", "VOLT:OFFS %f", """ A floating point property that controls the voltage offset of the output waveform in V, from 0 V to 4.995 V, depending on the set voltage amplitude (maximum offset = (10 - voltage) / 2). Can be set. """, validator=strict_range, values=[-4.995, +4.995], ) voltage_high = Instrument.control( "VOLT:HIGH?", "VOLT:HIGH %f", """ A floating point property that controls the upper voltage of the output waveform in V, from -4.990 V to 5 V (must be higher than low voltage). Can be set. """, validator=strict_range, values=[-4.99, 5], ) voltage_low = Instrument.control( "VOLT:LOW?", "VOLT:LOW %f", """ A floating point property that controls the lower voltage of the output waveform in V, from -5 V to 4.990 V (must be lower than high voltage). Can be set. """, validator=strict_range, values=[-5, 4.99], ) square_dutycycle = Instrument.control( "FUNC:SQU:DCYC?", "FUNC:SQU:DCYC %f", """ A floating point property that controls the duty cycle of a square waveform function in percent. Can be set. """, validator=strict_range, values=[20, 80], ) ramp_symmetry = Instrument.control( "FUNC:RAMP:SYMM?", "FUNC:RAMP:SYMM %f", """ A floating point property that controls the symmetry percentage for the ramp waveform. Can be set. """, validator=strict_range, values=[0, 100], ) pulse_period = Instrument.control( "PULS:PER?", "PULS:PER %f", """ A floating point property that controls the period of a pulse waveform function in seconds, ranging from 200 ns to 2000 s. Can be set and overwrites the frequency for *all* waveforms. If the period is shorter than the pulse width + the edge time, the edge time and pulse width will be adjusted accordingly. """, validator=strict_range, values=[200e-9, 2e3], ) pulse_hold = Instrument.control( "FUNC:PULS:HOLD?", "FUNC:PULS:HOLD %s", """ A string property that controls if either the pulse width or the duty cycle is retained when changing the period or frequency of the waveform. Can be set to: WIDT or DCYC. """, validator=string_validator, values=["WIDT", "WIDTH", "DCYC", "DCYCLE"], ) pulse_width = Instrument.control( "FUNC:PULS:WIDT?", "FUNC:PULS:WIDT %f", """ A floating point property that controls the width of a pulse waveform function in seconds, ranging from 20 ns to 2000 s, within a set of restrictions depending on the period. Can be set. """, validator=strict_range, values=[20e-9, 2e3], ) pulse_dutycycle = Instrument.control( "FUNC:PULS:DCYC?", "FUNC:PULS:DCYC %f", """ A floating point property that controls the duty cycle of a pulse waveform function in percent. Can be set. """, validator=strict_range, values=[0, 100], ) pulse_transition = Instrument.control( "FUNC:PULS:TRAN?", "FUNC:PULS:TRAN %f", """ A floating point property that controls the the edge time in seconds for both the rising and falling edges. It is defined as the time between 0.1 and 0.9 of the threshold. Valid values are between 5 ns to 100 ns. Can be set. """, validator=strict_range, values=[5e-9, 100e-9], ) output = Instrument.control( "OUTP?", "OUTP %d", """ A boolean property that turns on (True) or off (False) the output of the function generator. Can be set. """, validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) burst_state = Instrument.control( "BURS:STAT?", "BURS:STAT %d", """ A boolean property that controls whether the burst mode is on (True) or off (False). Can be set. """, validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) burst_mode = Instrument.control( "BURS:MODE?", "BURS:MODE %s", """ A string property that controls the burst mode. Valid values are: TRIG, GAT. This setting can be set. """, validator=string_validator, values=["TRIG", "TRIGGERED", "GAT", "GATED"], ) burst_ncycles = Instrument.control( "BURS:NCYC?", "BURS:NCYC %d", """ An integer property that sets the number of cycles to be output when a burst is triggered. Valid values are 1 to 50000. This can be set. """, validator=strict_discrete_set, values=range(1, 50001), ) def trigger(self): """ Send a trigger signal to the function generator. """ self.write("*TRG;*WAI") def wait_for_trigger(self, timeout=3600, should_stop=lambda: False): """ Wait until the triggering has finished or timeout is reached. :param timeout: The maximum time the waiting is allowed to take. If timeout is exceeded, a TimeoutError is raised. If timeout is set to zero, no timeout will be used. :param should_stop: Optional function (returning a bool) to allow the waiting to be stopped before its end. """ self.write("*OPC?") t0 = time() while True: try: ready = bool(self.read()) except VisaIOError: ready = False if ready: return if timeout != 0 and time() - t0 > timeout: raise TimeoutError( "Timeout expired while waiting for the Agilent 33220A" + " to finish the triggering." ) if should_stop: return trigger_source = Instrument.control( "TRIG:SOUR?", "TRIG:SOUR %s", """ A string property that controls the trigger source. Valid values are: IMM (internal), EXT (rear input), BUS (via trigger command). This setting can be set. """, validator=string_validator, values=["IMM", "IMMEDIATE", "EXT", "EXTERNAL", "BUS"], ) trigger_state = Instrument.control( "OUTP:TRIG?", "OUTP:TRIG %d", """ A boolean property that controls whether the output is triggered (True) or not (False). Can be set. """, validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) remote_local_state = Instrument.setting( "SYST:COMM:RLST %s", """ A string property that controls the remote/local state of the function generator. Valid values are: LOC, REM, RWL. This setting can only be set. """, validator=string_validator, values=["LOC", "LOCAL", "REM", "REMOTE", "RWL", "RWLOCK"], ) def check_errors(self): """ Read all errors from the instrument. """ errors = [] while True: err = self.values("SYST:ERR?") if int(err[0]) != 0: errmsg = "Agilent 33220A: %s: %s" % (err[0], err[1]) log.error(errmsg + '\n') errors.append(errmsg) else: break return errors beeper_state = Instrument.control( "SYST:BEEP:STAT?", "SYST:BEEP:STAT %d", """ A boolean property that controls the state of the beeper. Can be set. """, validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) def beep(self): """ Causes a system beep. """ self.write("SYST:BEEP") PyMeasure-0.9.0/pymeasure/instruments/agilent/agilent4156.py0000664000175000017500000010277314010037617024244 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import (Instrument, RangeException) from pymeasure.instruments.validators import (strict_discrete_set, truncated_discrete_set, strict_range) import numpy as np import time import pandas as pd import os import json ###### # MAIN ###### class Agilent4156(Instrument): """ Represents the Agilent 4155/4156 Semiconductor Parameter Analyzer and provides a high-level interface for taking current-voltage (I-V) measurements. .. code-block:: python from pymeasure.instruments.agilent import Agilent4156 # explicitly define r/w terminations; set sufficiently large timeout or None. smu = Agilent4156("GPIB0::25", read_termination = '\\n', write_termination = '\\n', timeout=None) # reset the instrument smu.reset() # define configuration file for instrument and load config smu.configure("configuration_file.json") # save data variables, some or all of which are defined in the json config file. smu.save(['VC', 'IC', 'VB', 'IB']) # take measurements status = smu.measure() # measured data is a pandas dataframe and can be exported to csv. data = smu.get_data(path='./t1.csv') The JSON file is an ascii text configuration file that defines the settings of each channel on the instrument. The JSON file is used to configure the instrument using the convenience function :meth:`~.Agilent4156.configure` as shown in the example above. For example, the instrument setup for a bipolar transistor measurement is shown below. .. code-block:: json { "SMU1": { "voltage_name" : "VC", "current_name" : "IC", "channel_function" : "VAR1", "channel_mode" : "V", "series_resistance" : "0OHM" }, "SMU2": { "voltage_name" : "VB", "current_name" : "IB", "channel_function" : "VAR2", "channel_mode" : "I", "series_resistance" : "0OHM" }, "SMU3": { "voltage_name" : "VE", "current_name" : "IE", "channel_function" : "CONS", "channel_mode" : "V", "constant_value" : 0, "compliance" : 0.1 }, "SMU4": { "voltage_name" : "VS", "current_name" : "IS", "channel_function" : "CONS", "channel_mode" : "V", "constant_value" : 0, "compliance" : 0.1 }, "VAR1": { "start" : 1, "stop" : 2, "step" : 0.1, "spacing" : "LINEAR", "compliance" : 0.1 }, "VAR2": { "start" : 0, "step" : 10e-6, "points" : 3, "compliance" : 2 } } """ def __init__(self, resourceName, **kwargs): super().__init__( resourceName, "Agilent 4155/4156 Semiconductor Parameter Analyzer", **kwargs ) self.smu1 = SMU(self.adapter, 'SMU1', **kwargs) self.smu2 = SMU(self.adapter, 'SMU2', **kwargs) self.smu3 = SMU(self.adapter, 'SMU3', **kwargs) self.smu4 = SMU(self.adapter, 'SMU4', **kwargs) self.vmu1 = VMU(self.adapter, 'VMU1', **kwargs) self.vmu2 = VMU(self.adapter, 'VMU2', **kwargs) self.vsu1 = VSU(self.adapter, 'VSU1', **kwargs) self.vsu2 = VSU(self.adapter, 'VSU2', **kwargs) self.var1 = VAR1(self.adapter, **kwargs) self.var2 = VAR2(self.adapter, **kwargs) self.vard = VARD(self.adapter, **kwargs) analyzer_mode = Instrument.control( ":PAGE:CHAN:MODE?", ":PAGE:CHAN:MODE %s", """ A string property that controls the instrument operating mode. - Values: :code:`SWEEP`, :code:`SAMPLING` .. code-block:: python smu.analyzer_mode = "SWEEP" """, validator=strict_discrete_set, values={'SWEEP': 'SWE', 'SAMPLING': 'SAMP'}, map_values=True, check_set_errors=True, check_get_errors=True ) integration_time = Instrument.control( ":PAGE:MEAS:MSET:ITIM?", ":PAGE:MEAS:MSET:ITIM %s", """ A string property that controls the integration time. - Values: :code:`SHORT`, :code:`MEDIUM`, :code:`LONG` .. code-block:: python instr.integration_time = "MEDIUM" """, validator=strict_discrete_set, values={'SHORT': 'SHOR', 'MEDIUM': 'MED', 'LONG': 'LONG'}, map_values=True, check_set_errors=True, check_get_errors=True ) delay_time = Instrument.control( ":PAGE:MEAS:DEL?", ":PAGE:MEAS:DEL %g", """ A floating point property that measurement delay time in seconds, which can take the values from 0 to 65s in 0.1s steps. .. code-block:: python instr.delay_time = 1 # delay time of 1-sec """, validator=truncated_discrete_set, values=np.arange(0, 65.1, 0.1), check_set_errors=True, check_get_errors=True ) hold_time = Instrument.control( ":PAGE:MEAS:HTIME?", ":PAGE:MEAS:HTIME %g", """ A floating point property that measurement hold time in seconds, which can take the values from 0 to 655s in 1s steps. .. code-block:: python instr.hold_time = 2 # hold time of 2-secs. """, validator=truncated_discrete_set, values=np.arange(0, 655, 1), check_set_errors=True, check_get_errors=True ) def stop(self): """Stops the ongoing measurement .. code-block:: python instr.stop() """ self.write(":PAGE:SCON:STOP") def measure(self, period="INF", points=100): """ Performs a single measurement and waits for completion in sweep mode. In sampling mode, the measurement period and number of points can be specified. :param period: Period of sampling measurement from 6E-6 to 1E11 seconds. Default setting is :code:`INF`. :param points: Number of samples to be measured, from 1 to 10001. Default setting is :code:`100`. .. code-block::python instr.measure() #for sweep measurement instr.measure(period=100, points=100) #for sampling measurement """ if self.analyzer_mode == "SWEEP": self.write(":PAGE:SCON:MEAS:SING; *OPC?") else: self.write(":PAGE:MEAS:SAMP:PER {}".format(period)) self.write(":PAGE:MEAS:SAMP:POIN {}".format(points)) self.write(":PAGE:SCON:MEAS:SING; *OPC?") def disable_all(self): """ Disables all channels in the instrument. .. code-block:: python instr.disable_all() """ self.smu1.disable time.sleep(0.1) self.smu2.disable time.sleep(0.1) self.smu3.disable time.sleep(0.1) self.smu4.disable time.sleep(0.1) self.vmu1.disable time.sleep(0.1) self.vmu2.disable time.sleep(0.1) def configure(self, config_file): """ Convenience function to configure the channel setup and sweep using a `JSON (JavaScript Object Notation)`_ configuration file. .. _`JSON (JaVaScript Object Notation)`: https://www.json.org/ :param config_file: JSON file to configure instrument channels. .. code-block:: python instr.configure('config.json') """ self.disable_all() obj_dict = {'SMU1': self.smu1, 'SMU2': self.smu2, 'SMU3': self.smu3, 'SMU4': self.smu4, 'VMU1': self.vmu1, 'VMU2': self.vmu2, 'VSU1': self.vsu1, 'VSU2': self.vsu2, 'VAR1': self.var1, 'VAR2': self.var2, 'VARD': self.vard } with open(config_file, 'r') as stream: try: instr_settings = json.load(stream) except json.JSONDecodeError as e: print(e) # replace dict keys with Instrument objects new_settings_dict = {} for key, value in instr_settings.items(): new_settings_dict[obj_dict[key]] = value for obj, setup in new_settings_dict.items(): for setting, value in setup.items(): setattr(obj, setting, value) time.sleep(0.1) def save(self, trace_list): """ Save the voltage or current in the instrument display list :param trace_list: A list of channel variables whose measured data should be saved. A maximum of 8 variables are allowed. If only one variable is being saved, a string can be specified. .. code-block:: python instr.save(['IC', 'IB', 'VC', 'VB']) #for list of variables instr.save('IC') #for single variable """ self.write(":PAGE:DISP:MODE LIST") if isinstance(trace_list, list): try: len(trace_list) > 8 except: raise RuntimeError('Maximum of 8 variables allowed') else: for name in trace_list: self.write(":PAGE:DISP:LIST \'{}\'".format(name)) elif isinstance(trace_list, str): self.write(":PAGE:DISP:LIST \'{}\'".format(trace_list)) else: raise TypeError( 'Must be a string if only one variable is saved, or else a list if' 'multiple variables are being saved.' ) def save_var(self, trace_list): """ Save the voltage or current in the instrument variable list. This is useful if one or two more variables need to be saved in addition to the 8 variables allowed by :meth:`~.Agilent4156.save`. :param trace_list: A list of channel variables whose measured data should be saved. A maximum of 2 variables are allowed. If only one variable is being saved, a string can be specified. .. code-block:: python instr.save_var(['VA', 'VB']) """ self.write(":PAGE:DISP:MODE LIST") if isinstance(trace_list, list): try: len(trace_list) > 2 except: raise RuntimeError('Maximum of 2 variables allowed') else: for name in trace_list: self.write(":PAGE:DISP:DVAR \'{}\'".format(name)) elif isinstance(trace_list, str): self.write(":PAGE:DISP:DVAR \'{}\'".format(trace_list)) else: raise TypeError( 'Must be a string if only one variable is saved, or else a list if' 'multiple variables are being saved.' ) @property def data_variables(self): """ Gets a string list of data variables for which measured data is available. This looks for all the variables saved by the :meth:`~.Agilent4156.save` and :meth:`~.Agilent4156.save_var` methods and returns it. This is useful for creation of dataframe headers. :returns: List .. code-block:: python header = instr.data_variables """ dlist = self.ask(":PAGE:DISP:LIST?").split(',') dvar = self.ask(":PAGE:DISP:DVAR?").split(',') varlist = dlist + dvar return list(filter(None, varlist)) def get_data(self, path=None): """ Gets the measurement data from the instrument after completion. If the measurement period is set to :code:`INF` in the :meth:`~.Agilent4156.measure` method, then the measurement must be stopped using :meth:`~.Agilent4156.stop` before getting valid data. :param path: Path for optional data export to CSV. :returns: Pandas Dataframe .. code-block:: python df = instr.get_data(path='./datafolder/data1.csv') """ if int(self.ask('*OPC?')): header = self.data_variables self.write(":FORM:DATA ASC") # recursively get data for each variable for i, listvar in enumerate(header): data = self.values(":DATA? \'{}\'".format(listvar)) time.sleep(0.01) if i == 0: lastdata = data else: data = np.column_stack((lastdata, data)) lastdata = data df = pd.DataFrame(data=data, columns=header, index=None) if path is not None: _, ext = os.path.splitext(path) if ext != ".csv": path = path + ".csv" df.to_csv(path, index=False) return df ########## # CHANNELS ########## class SMU(Instrument): def __init__(self, resourceName, channel, **kwargs): super().__init__( resourceName, "SMU of Agilent 4155/4156 Semiconductor Parameter Analyzer", **kwargs ) self.channel = channel.upper() @property def channel_mode(self): """ A string property that controls the SMU channel mode. - Values: :code:`V`, :code:`I` or :code:`COMM` VPULSE AND IPULSE are not yet supported. .. code-block:: python instr.smu1.channel_mode = "V" """ value = self.ask(":PAGE:CHAN:{}:MODE?".format(self.channel)) self.check_errors() return value @channel_mode.setter def channel_mode(self, mode): validator = strict_discrete_set values = ["V", "I", "COMM"] value = validator(mode, values) self.write(":PAGE:CHAN:{0}:MODE {1}".format(self.channel, value)) self.check_errors() @property def channel_function(self): """ A string property that controls the SMU channel function. - Values: :code:`VAR1`, :code:`VAR2`, :code:`VARD` or :code:`CONS`. .. code-block:: python instr.smu1.channel_function = "VAR1" """ value = self.ask(":PAGE:CHAN:{}:FUNC?".format(self.channel)) self.check_errors() return value @channel_function.setter def channel_function(self, function): validator = strict_discrete_set values = ["VAR1", "VAR2", "VARD", "CONS"] value = validator(function, values) self.write(":PAGE:CHAN:{0}:FUNC {1}".format(self.channel, value)) self.check_errors() @property def series_resistance(self): """ This command controls the series resistance of SMU. - Values: :code:`0OHM`, :code:`10KOHM`, :code:`100KOHM`, or :code:`1MOHM` .. code-block:: python instr.smu1.series_resistance = "10KOHM" """ value = self.ask(":PAGE:CHAN:{}:SRES?".format(self.channel)) self.check_errors() return value @series_resistance.setter def series_resistance(self, sres): validator = strict_discrete_set values = ["0OHM", "10KOHM", "100KOHM", "1MOHM"] value = validator(sres, values) self.write(":PAGE:CHAN:{0}:SRES {1}".format(self.channel, value)) self.check_errors() @property def disable(self): """ This command deletes the settings of SMU. .. code-block:: python instr.smu1.disable() """ self.write(":PAGE:CHAN:{}:DIS".format(self.channel)) self.check_errors() @property def constant_value(self): """ This command sets the constant source value of SMU. You use this command only if :meth:`~.SMU.channel_function` is :code:`CONS` and also :meth:`~.SMU.channel_mode` should not be :code:`COMM`. :param const_value: Voltage in (-200V, 200V) and current in (-1A, 1A). Voltage or current depends on if :meth:`~.SMU.channel_mode` is set to :code:`V` or :code:`I`. .. code-block:: python instr.smu1.constant_value = 1 """ if Agilent4156.analyzer_mode.fget(self) == "SWEEP": value = self.ask(":PAGE:MEAS:CONS:{}?".format(self.channel)) else: value = self.ask(":PAGE:MEAS:SAMP:CONS:{}?".format(self.channel)) self.check_errors() return value @constant_value.setter def constant_value(self, const_value): validator = strict_range values = self.__validate_cons() value = validator(const_value, values) if Agilent4156.analyzer_mode.fget(self) == 'SWEEP': self.write(":PAGE:MEAS:CONS:{0} {1}".format(self.channel, value)) else: self.write(":PAGE:MEAS:SAMP:CONS:{0} {1}".format( self.channel, value)) self.check_errors() @property def compliance(self): """ This command sets the *constant* compliance value of SMU. If the SMU channel is setup as a variable (VAR1, VAR2, VARD) then compliance limits are set by the variable definition. - Value: Voltage in (-200V, 200V) and current in (-1A, 1A) based on :meth:`~.SMU.channel_mode`. .. code-block:: python instr.smu1.compliance = 0.1 """ if Agilent4156.analyzer_mode.fget(self) == "SWEEP": value = self.ask(":PAGE:MEAS:CONS:{}:COMP?".format(self.channel)) else: value = self.ask( ":PAGE:MEAS:SAMP:CONS:{}:COMP?".format(self.channel)) self.check_errors() return value @compliance.setter def compliance(self, comp): validator = strict_range values = self.__validate_compl() value = validator(comp, values) if Agilent4156.analyzer_mode.fget(self) == 'SWEEP': self.write(":PAGE:MEAS:CONS:{0}:COMP {1}".format( self.channel, value)) else: self.write(":PAGE:MEAS:SAMP:CONS:{0}:COMP {1}".format( self.channel, value)) self.check_errors() @property def voltage_name(self): """ Define the voltage name of the channel. If input is greater than 6 characters long or starts with a number, the name is autocorrected and prepended with 'a'. Event is logged. .. code-block:: python instr.smu1.voltage_name = "Vbase" """ value = self.ask("PAGE:CHAN:{}:VNAME?".format(self.channel)) return value @voltage_name.setter def voltage_name(self, vname): value = check_current_voltage_name(vname) self.write(":PAGE:CHAN:{0}:VNAME \'{1}\'".format(self.channel, value)) @property def current_name(self): """ Define the current name of the channel. If input is greater than 6 characters long or starts with a number, the name is autocorrected and prepended with 'a'. Event is logged. .. code-block:: python instr.smu1.current_name = "Ibase" """ value = self.ask("PAGE:CHAN:{}:INAME?".format(self.channel)) return value @current_name.setter def current_name(self, iname): value = check_current_voltage_name(iname) self.write(":PAGE:CHAN:{0}:INAME \'{1}\'".format(self.channel, value)) def __validate_cons(self): """Validates the instrument settings for operation in constant mode. """ try: not((self.channel_mode != 'COMM') and ( self.channel_function == 'CONS')) except: raise ValueError( 'Cannot set constant SMU function when SMU mode is COMMON, ' 'or when SMU function is not CONSTANT.') else: values = valid_iv(self.channel_mode) return values def __validate_compl(self): """Validates the instrument compliance for operation in constant mode. """ try: not((self.channel_mode != 'COMM') and ( self.channel_function == 'CONS')) except: raise ValueError( 'Cannot set constant SMU parameters when SMU mode is COMMON, ' 'or when SMU function is not CONSTANT.') else: values = valid_compliance(self.channel_mode) return values class VMU(Instrument): def __init__(self, resourceName, channel, **kwargs): super().__init__( resourceName, "VMU of Agilent 4155/4156 Semiconductor Parameter Analyzer", **kwargs ) self.channel = channel.upper() @property def voltage_name(self): """ Define the voltage name of the VMU channel. If input is greater than 6 characters long or starts with a number, the name is autocorrected and prepended with 'a'. Event is logged. .. code-block:: python instr.vmu1.voltage_name = "Vanode" """ value = self.ask("PAGE:CHAN:{}:VNAME?".format(self.channel)) return value @voltage_name.setter def voltage_name(self, vname): value = check_current_voltage_name(vname) self.write(":PAGE:CHAN:{0}:VNAME \'{1}\'".format(self.channel, value)) @property def disable(self): """ This command disables the settings of VMU. .. code-block:: python instr.vmu1.disable() """ self.write(":PAGE:CHAN:{}:DIS".format(self.channel)) self.check_errors() @property def channel_mode(self): """ A string property that controls the VMU channel mode. - Values: :code:`V`, :code:`DVOL` """ value = self.ask(":PAGE:CHAN:{}:MODE?".format(self.channel)) self.check_errors() return value @channel_mode.setter def channel_mode(self, mode): validator = strict_discrete_set values = ["V", "DVOL"] value = validator(mode, values) self.write(":PAGE:CHAN:{0}:MODE {1}".format(self.channel, value)) self.check_errors() class VSU(Instrument): def __init__(self, resourceName, channel, **kwargs): super().__init__( resourceName, "VSU of Agilent 4155/4156 Semiconductor Parameter Analyzer", **kwargs ) self.channel = channel.upper() @property def voltage_name(self): """ Define the voltage name of the VSU channel If input is greater than 6 characters long or starts with a number, the name is autocorrected and prepended with 'a'. Event is logged. .. code-block:: python instr.vsu1.voltage_name = "Ve" """ value = self.ask("PAGE:CHAN:{}:VNAME?".format(self.channel)) return value @voltage_name.setter def voltage_name(self, vname): value = check_current_voltage_name(vname) self.write(":PAGE:CHAN:{0}:VNAME \'{1}\'".format(self.channel, value)) @property def disable(self): """ This command deletes the settings of VSU. .. code-block:: python instr.vsu1.disable() """ self.write(":PAGE:CHAN:{}:DIS".format(self.channel)) self.check_errors() @property def channel_mode(self): """ Get channel mode of VSU.""" value = self.ask(":PAGE:CHAN:{}:MODE?".format(self.channel)) self.check_errors() return value @property def constant_value(self): """ This command sets the constant source value of VSU. .. code-block:: python instr.vsu1.constant_value = 0 """ if Agilent4156.analyzer_mode.fget(self) == "SWEEP": value = self.ask(":PAGE:MEAS:CONS:{}?".format(self.channel)) else: value = self.ask(":PAGE:MEAS:SAMP:CONS:{}?".format(self.channel)) self.check_errors() return value @constant_value.setter def constant_value(self, const_value): validator = strict_range values = [-200, 200] value = validator(const_value, values) if Agilent4156.analyzer_mode.fget(self) == 'SWEEP': self.write(":PAGE:MEAS:CONS:{0} {1}".format(self.channel, value)) else: self.write(":PAGE:MEAS:SAMP:CONS:{0} {1}".format( self.channel, value)) self.check_errors() @property def channel_function(self): """ A string property that controls the VSU channel function. - Value: :code:`VAR1`, :code:`VAR2`, :code:`VARD` or :code:`CONS`. """ value = self.ask(":PAGE:CHAN:{}:FUNC?".format(self.channel)) self.check_errors() return value @channel_function.setter def channel_function(self, function): validator = strict_discrete_set values = ["VAR1", "VAR2", "VARD", "CONS"] value = validator(function, values) self.write(":PAGE:CHAN:{0}:FUNC {1}".format(self.channel, value)) self.check_errors() ################# # SWEEP VARIABLES ################# class VARX(Instrument): """ Base class to define sweep variable settings """ def __init__(self, resourceName, var_name, **kwargs): super().__init__( resourceName, "Methods to setup sweep variables", **kwargs ) self.var = var_name.upper() @property def channel_mode(self): channels = ['SMU1', 'SMU2', 'SMU3', 'SMU4', 'VSU1', 'VSU2'] for ch in channels: ch_func = self.ask(":PAGE:CHAN:{}:FUNC?".format(ch)) if ch_func == self.var: ch_mode = self.ask(":PAGE:CHAN:{}:MODE?".format(ch)) return ch_mode @property def start(self): """ Sets the sweep START value. .. code-block:: python instr.var1.start = 0 """ value = self.ask(":PAGE:MEAS:{}:STAR?".format(self.var)) self.check_errors() return value @start.setter def start(self, value): validator = strict_range values = valid_iv(self.channel_mode) set_value = validator(value, values) self.write(":PAGE:MEAS:{}:STAR {}".format(self.var, set_value)) self.check_errors() @property def stop(self): """ Sets the sweep STOP value. .. code-block:: python instr.var1.stop = 3 """ value = self.ask(":PAGE:MEAS:{}:STOP?".format(self.var)) self.check_errors() return value @stop.setter def stop(self, value): validator = strict_range values = valid_iv(self.channel_mode) set_value = validator(value, values) self.write(":PAGE:MEAS:{}:STOP {}".format(self.var, set_value)) self.check_errors() @property def step(self): """ Sets the sweep STEP value. .. code-block:: python instr.var1.step = 0.1 """ value = self.ask(":PAGE:MEAS:{}:STEP?".format(self.var)) self.check_errors() return value @step.setter def step(self, value): validator = strict_range values = 2*valid_iv(self.channel_mode) set_value = validator(value, values) self.write(":PAGE:MEAS:{}:STEP {}".format(self.var, set_value)) self.check_errors() @property def compliance(self): """ Sets the sweep COMPLIANCE value. .. code-block:: python instr.var1.compliance = 0.1 """ value = self.ask(":PAGE:MEAS:{}:COMP?") self.check_errors() return value @compliance.setter def compliance(self, value): validator = strict_range values = 2*valid_compliance(self.channel_mode) set_value = validator(value, values) self.write(":PAGE:MEAS:{}:COMP {}".format(self.var, set_value)) self.check_errors() class VAR1(VARX): """ Class to handle all the specific definitions needed for VAR1. Most common methods are inherited from base class. """ def __init__(self, resourceName, **kwargs): super().__init__( resourceName, "VAR1", **kwargs ) spacing = Instrument.control( ":PAGE:MEAS:VAR1:SPAC?", ":PAGE:MEAS:VAR1:SPAC %s", """ This command selects the sweep type of VAR1. - Values: :code:`LINEAR`, :code:`LOG10`, :code:`LOG25`, :code:`LOG50`. """, validator=strict_discrete_set, values={'LINEAR': 'LIN', 'LOG10': 'L10', 'LOG25': 'L25', 'LOG50': 'L50'}, map_values=True, check_set_errors=True, check_get_errors=True ) class VAR2(VARX): """ Class to handle all the specific definitions needed for VAR2. Common methods are imported from base class. """ def __init__(self, resourceName, **kwargs): super().__init__( resourceName, "VAR2", **kwargs ) points = Instrument.control( ":PAGE:MEAS:VAR2:POINTS?", ":PAGE:MEAS:VAR2:POINTS %g", """ This command sets the number of sweep steps of VAR2. You use this command only if there is an SMU or VSU whose function (FCTN) is VAR2. .. code-block:: python instr.var2.points = 10 """, validator=strict_discrete_set, values=range(1, 128), check_set_errors=True, check_get_errors=True ) class VARD(Instrument): """ Class to handle all the definitions needed for VARD. VARD is always defined in relation to VAR1. """ def __init__(self, resourceName, **kwargs): super().__init__( resourceName, "Definitions for VARD sweep variable.", **kwargs ) @property def channel_mode(self): channels = ['SMU1', 'SMU2', 'SMU3', 'SMU4', 'VSU1', 'VSU2'] for ch in channels: ch_func = self.ask(":PAGE:CHAN:{}:FUNC?".format(ch)) if ch_func == "VARD": ch_mode = self.ask(":PAGE:CHAN:{}:MODE?".format(ch)) return ch_mode @property def offset(self): """ This command sets the OFFSET value of VARD. For each step of sweep, the output values of VAR1' are determined by the following equation: VARD = VAR1 X RATio + OFFSet You use this command only if there is an SMU or VSU whose function is VARD. .. code-block:: python instr.vard.offset = 1 """ value = self.ask(":PAGE:MEAS:VARD:OFFSET?") self.check_errors() return value @offset.setter def offset(self, offset_value): validator = strict_range values = 2*valid_iv(self.channel_mode) value = validator(offset_value, values) self.write(":PAGE:MEAS:VARD:OFFSET {}".format(value)) self.check_errors() ratio = Instrument.control( ":PAGE:MEAS:VARD:RATIO?", ":PAGE:MEAS:VARD:RATIO %g", """ This command sets the RATIO of VAR1'. For each step of sweep, the output values of VAR1' are determined by the following equation: VAR1’ = VAR1 * RATio + OFFSet You use this command only if there is an SMU or VSU whose function (FCTN) is VAR1'. .. code-block:: python instr.vard.ratio = 1 """, ) @property def compliance(self): """ This command sets the sweep COMPLIANCE value of VARD. .. code-block:: python instr.vard.compliance = 0.1 """ value = self.ask(":PAGE:MEAS:VARD:COMP?") self.check_errors() return value @compliance.setter def compliance(self, value): validator = strict_range values = 2*valid_compliance(self.channel_mode) set_value = validator(value, values) self.write(":PAGE:MEAS:VARD:COMP {}".format(set_value)) self.check_errors() def check_current_voltage_name(name): if (len(name) > 6) or not name[0].isalpha(): new_name = 'a' + name[:5] log.info("Renaming %s to %s..." % (name, new_name)) name = new_name return name def valid_iv(channel_mode): if channel_mode == 'V': values = [-200, 200] elif channel_mode == 'I': values = [-1, 1] else: raise ValueError( 'Channel is not in V or I mode. It might be disabled.') return values def valid_compliance(channel_mode): if channel_mode == 'I': values = [-200, 200] elif channel_mode == 'V': values = [-1, 1] else: raise ValueError( 'Channel is not in V or I mode. It might be disabled.') return values PyMeasure-0.9.0/pymeasure/instruments/agilent/agilent8722ES.py0000664000175000017500000002323314010037617024470 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument, discreteTruncate, RangeException from pyvisa import VisaIOError import numpy as np import re from io import BytesIO import warnings class Agilent8722ES(Instrument): """ Represents the Agilent8722ES Vector Network Analyzer and provides a high-level interface for taking scans of the scattering parameters. """ SCAN_POINT_VALUES = [3, 11, 21, 26, 51, 101, 201, 401, 801, 1601] SCATTERING_PARAMETERS = ("S11", "S12", "S21", "S22") S11, S12, S21, S22 = SCATTERING_PARAMETERS start_frequency = Instrument.control( "STAR?", "STAR %e Hz", """ A floating point property that represents the start frequency in Hz. This property can be set. """ ) stop_frequency = Instrument.control( "STOP?", "STOP %e Hz", """ A floating point property that represents the stop frequency in Hz. This property can be set. """ ) sweep_time = Instrument.control( "SWET?", "SWET%.2e", """ A floating point property that represents the sweep time in seconds. This property can be set. """ ) averages = Instrument.control( "AVERFACT?", "AVERFACT%d", """ An integer representing the number of averages to take. Note that averaging must be enabled for this to take effect. This property can be set. """, cast=lambda x: int(float(x)) # int() doesn't like converting scientific notation values directly from strings ) averaging_enabled = Instrument.control( "AVERO?", "AVERO%d", """ A bool that indicates whether or not averaging is enabled. This property can be set.""", cast=bool ) def __init__(self, resourceName, **kwargs): super(Agilent8722ES, self).__init__( resourceName, "Agilent 8722ES Vector Network Analyzer", **kwargs ) def set_fixed_frequency(self, frequency): """ Sets the scan to be of only one frequency in Hz """ self.start_frequency = frequency self.stop_frequency = frequency self.scan_points = 3 @property def parameter(self): for parameter in Agilent8722ES.SCATTERING_PARAMETERS: if int(self.values("%s?" % parameter)) == 1: return parameter return None @parameter.setter def parameter(self, value): if value in Agilent8722ES.SCATTERING_PARAMETERS: self.write("%s" % value) else: raise Exception("Invalid scattering parameter requested" " for Agilent 8722ES") @property def scan_points(self): """ Gets the number of scan points """ search = re.search(r"\d\.\d+E[+-]\d{2}$", self.ask("POIN?"), re.MULTILINE) if search: return int(float(search.group())) else: raise Exception("Improper message returned for the" " number of points") @scan_points.setter def scan_points(self, points): """ Sets the number of scan points, truncating to an allowed value if not properly provided """ points = discreteTruncate(points, Agilent8722ES.SCAN_POINT_VALUES) if points: self.write("POIN%d" % points) else: raise RangeException("Maximum scan points (1601) for" " Agilent 8722ES exceeded") def set_IF_bandwidth(self, bandwidth): """ Sets the resolution bandwidth (IF bandwidth) """ allowedBandwidth = [10, 30, 100, 300, 1000, 3000, 3700, 6000] bandwidth = discreteTruncate(bandwidth, allowedBandwidth) if bandwidth: self.write("IFBW%d" % bandwidth) else: raise RangeException("Maximum IF bandwidth (6000) for Agilent " "8722ES exceeded") def set_averaging(self, averages): """Sets the number of averages and enables/disables averaging. Should be between 1 and 999""" averages = int(averages) if not 1 <= averages <= 999: assert RangeException("Set", averages, "must be in the range 1 to 999") self.averages = averages self.averaging_enabled = (averages > 1) def disable_averaging(self): """Disables averaging""" warnings.warn("Don't use disable_averaging(), use averaging_enabled = False instead", FutureWarning) self.averaging_enabled = False def enable_averaging(self): """Enables averaging""" warnings.warn("Don't use enable_averaging(), use averaging_enabled = True instead", FutureWarning) self.averaging_enabled = True def is_averaging(self): """ Returns True if averaging is enabled """ warnings.warn("Don't use is_averaging(), use averaging_enabled instead", FutureWarning) return self.averaging_enabled def restart_averaging(self, averages): warnings.warn("Don't use restart_averaging(), use scan_single() instead", FutureWarning) self.scan_single() def scan(self, averages=None, blocking=None, timeout=None, delay=None): """ Initiates a scan with the number of averages specified and blocks until the operation is complete. """ if averages is not None or blocking is not None or timeout is not None or delay is not None: warnings.warn("averages, blocking, timeout, and delay arguments are no longer used by scan()", FutureWarning) self.write("*CLS") self.scan_single() # All queries will block until the scan is done, so use NOOP? to check. # These queries will time out after several seconds though, # so query repeatedly until the scan finishes. while True: try: self.ask("NOOP?") except VisaIOError as e: if e.abbreviation != "VI_ERROR_TMO": raise e else: break def scan_single(self): """ Initiates a single scan """ if self.averaging_enabled: self.write("NUMG%d" % self.averages) else: self.write("SING") def scan_continuous(self): """ Initiates a continuous scan """ self.write("CONT") @property def frequencies(self): """ Returns a list of frequencies from the last scan """ return np.linspace( self.start_frequency, self.stop_frequency, num=self.scan_points ) @property def data_complex(self): """ Returns the complex power from the last scan """ # TODO: Implement binary transfer instead of ASCII data = np.loadtxt( BytesIO(self.ask("FORM4;OUTPDATA").encode()), delimiter=',', dtype=np.float32 ) data_complex = data[:, 0] + 1j * data[:, 1] return data_complex @property def data_log_magnitude(self): """ Returns the absolute magnitude values in dB from the last scan """ return 20*np.log10(self.data_magnitude) @property def data_magnitude(self): """ Returns the absolute magnitude values from the last scan """ return np.abs(self.data_complex) @property def data_phase(self): """ Returns the phase in degrees from the last scan """ return np.degrees(np.angle(self.data_complex)) @property def data(self): """ Returns the real and imaginary data from the last scan """ warnings.warn("Don't use this function, use data_complex instead", FutureWarning) data_complex = self.data_complex return data_complex.real, data_complex.complex def log_magnitude(self, real, imaginary): """ Returns the magnitude in dB from a real and imaginary number or numpy arrays """ warnings.warn("Don't use log_magnitude(), use data_log_magnitude instead", FutureWarning) return 20*np.log10(self.magnitude(real, imaginary)) def magnitude(self, real, imaginary): """ Returns the magnitude from a real and imaginary number or numpy arrays """ warnings.warn("Don't use magnitude(), use data_magnitude", FutureWarning) return np.sqrt(real**2 + imaginary**2) def phase(self, real, imaginary): """ Returns the phase in degrees from a real and imaginary number or numpy arrays """ warnings.warn("Don't use phase(), use data_phase instead", FutureWarning) return np.arctan2(imaginary, real)*180/np.pi PyMeasure-0.9.0/pymeasure/instruments/agilent/agilent34410A.py0000664000175000017500000000427014010037617024412 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument class Agilent34410A(Instrument): """ Represent the HP/Agilent/Keysight 34410A and related multimeters. Implemented measurements: voltage_dc, voltage_ac, current_dc, current_ac, resistance, resistance_4w """ #only the most simple functions are implemented voltage_dc = Instrument.measurement("MEAS:VOLT:DC? DEF,DEF", "DC voltage, in Volts") voltage_ac = Instrument.measurement("MEAS:VOLT:AC? DEF,DEF", "AC voltage, in Volts") current_dc = Instrument.measurement("MEAS:CURR:DC? DEF,DEF", "DC current, in Amps") current_ac = Instrument.measurement("MEAS:CURR:AC? DEF,DEF", "AC current, in Amps") resistance = Instrument.measurement("MEAS:RES? DEF,DEF", "Resistance, in Ohms") resistance_4w = Instrument.measurement("MEAS:FRES? DEF,DEF", "Four-wires (remote sensing) resistance, in Ohms") def __init__(self, adapter, **kwargs): super(Agilent34410A, self).__init__( adapter, "HP/Agilent/Keysight 34410A Multimeter", **kwargs ) PyMeasure-0.9.0/pymeasure/instruments/agilent/__init__.py0000664000175000017500000000307414010037617024032 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .agilent8257D import Agilent8257D from .agilent8722ES import Agilent8722ES from .agilentE4408B import AgilentE4408B from .agilentE4980 import AgilentE4980 from .agilent34410A import Agilent34410A from .agilent34450A import Agilent34450A from .agilent4156 import Agilent4156 from .agilent33220A import Agilent33220A from .agilent33500 import Agilent33500 from .agilent33521A import Agilent33521A from .agilentB1500 import AgilentB1500 PyMeasure-0.9.0/pymeasure/instruments/agilent/agilent34450A.py0000664000175000017500000006127614010037617024427 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import re import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range, strict_discrete_set class Agilent34450A(Instrument): """ Represent the HP/Agilent/Keysight 34450A and related multimeters. .. code-block:: python dmm = Agilent34450A("USB0::...") dmm.reset() dmm.configure_voltage() print(dmm.voltage) dmm.shutdown() """ BOOLS = {True: 1, False: 0} MODES = {'current': 'CURR', 'ac current': 'CURR:AC', 'voltage': 'VOLT', 'ac voltage': 'VOLT:AC', 'resistance': 'RES', '4w resistance': 'FRES', 'current frequency': 'FREQ:ACI', 'voltage frequency': 'FREQ:ACV', 'continuity': 'CONT', 'diode': 'DIOD', 'temperature': 'TEMP', 'capacitance': 'CAP'} @property def mode(self): get_command = ":configure?" vals = self._conf_parser(self.values(get_command)) # Return only the mode parameter inv_modes = {v: k for k, v in self.MODES.items()} mode = inv_modes[vals[0]] return mode @mode.setter def mode(self, value): """ A string parameter that sets the measurement mode of the multimeter. Can be "current", "ac current", "voltage", "ac voltage", "resistance", "4w resistance", "current frequency", "voltage frequency", "continuity", "diode", "temperature", or "capacitance".""" if value in self.MODES: if value not in ['current frequency', 'voltage frequency']: self.write(':configure:' + self.MODES[value]) else: if value == 'current frequency': self.mode = 'ac current' else: self.mode = 'ac voltage' self.write(":configure:freq") else: raise ValueError('Value %s is not a supported mode for this device.'.format(value)) ############### # Current (A) # ############### current = Instrument.measurement(":READ?", """ Reads a DC current measurement in Amps, based on the active :attr:`~.Agilent34450A.mode`. """ ) current_ac = Instrument.measurement(":READ?", """ Reads an AC current measurement in Amps, based on the active :attr:`~.Agilent34450A.mode`. """ ) current_range = Instrument.control( ":SENS:CURR:RANG?", ":SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG %s", """ A property that controls the DC current range in Amps, which can take values 100E-6, 1E-3, 10E-3, 100E-3, 1, 10, as well as "MIN", "MAX", or "DEF" (100 mA). Auto-range is disabled when this property is set. """, validator=strict_discrete_set, values=[100E-6, 1E-3, 10E-3, 100E-3, 1, 10, "MIN", "DEF", "MAX"] ) current_auto_range = Instrument.control( ":SENS:CURR:RANG:AUTO?", ":SENS:CURR:RANG:AUTO %d", """ A boolean property that toggles auto ranging for DC current. """, validator=strict_discrete_set, values=BOOLS, map_values=True ) current_resolution = Instrument.control( ":SENS:CURR:RES?", ":SENS:CURR:RES %s", """ A property that controls the resolution in the DC current readings, which can take values 3.00E-5, 2.00E-5, 1.50E-6 (5 1/2 digits), as well as "MIN", "MAX", and "DEF" (3.00E-5). """, validator=strict_discrete_set, values=[3.00E-5, 2.00E-5, 1.50E-6, "MAX", "MIN", "DEF"] ) current_ac_range = Instrument.control( ":SENS:CURR:AC:RANG?", ":SENS:CURR:AC:RANG:AUTO 0;:SENS:CURR:AC:RANG %s", """ A property that controls the AC current range in Amps, which can take values 10E-3, 100E-3, 1, 10, as well as "MIN", "MAX", or "DEF" (100 mA). Auto-range is disabled when this property is set. """, validator=strict_discrete_set, values=[10E-3, 100E-3, 1, 10, "MIN", "MAX", "DEF"] ) current_ac_auto_range = Instrument.control( ":SENS:CURR:AC:RANG:AUTO?", ":SENS:CURR:AC:RANG:AUTO %d", """ A boolean property that toggles auto ranging for AC current. """, validator=strict_discrete_set, values=BOOLS, map_values=True ) current_ac_resolution = Instrument.control( ":SENS:CURR:AC:RES?", ":SENS:CURR:AC:RES %s", """ An property that controls the resolution in the AC current readings, which can take values 3.00E-5, 2.00E-5, 1.50E-6 (5 1/2 digits), as well as "MIN", "MAX", or "DEF" (1.50E-6). """, validator=strict_discrete_set, values=[3.00E-5, 2.00E-5, 1.50E-6, "MAX", "MIN", "DEF"] ) ############### # Voltage (V) # ############### voltage = Instrument.measurement(":READ?", """ Reads a DC voltage measurement in Volts, based on the active :attr:`~.Agilent34450A.mode`. """ ) voltage_ac = Instrument.measurement(":READ?", """ Reads an AC voltage measurement in Volts, based on the active :attr:`~.Agilent34450A.mode`. """ ) voltage_range = Instrument.control( ":SENS:VOLT:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG %s", """ A property that controls the DC voltage range in Volts, which can take values 100E-3, 1, 10, 100, 1000, as well as "MIN", "MAX", or "DEF" (10 V). Auto-range is disabled when this property is set. """, validator=strict_discrete_set, values=[100E-3, 1, 10, 100, 1000, "MAX", "MIN", "DEF"] ) voltage_auto_range = Instrument.control( ":SENS:VOLT:RANG:AUTO?", ":SENS:VOLT:RANG:AUTO %d", """ A boolean property that toggles auto ranging for DC voltage. """, validator=strict_discrete_set, values=BOOLS, map_values=True ) voltage_resolution = Instrument.control( ":SENS:VOLT:RES?", ":SENS:VOLT:RES %s", """ A property that controls the resolution in the DC voltage readings, which can take values 3.00E-5, 2.00E-5, 1.50E-6 (5 1/2 digits), as well as "MIN", "MAX", or "DEF" (1.50E-6). """, validator=strict_discrete_set, values=[3.00E-5, 2.00E-5, 1.50E-6, "MAX", "MIN", "DEF"] ) voltage_ac_range = Instrument.control( ":SENS:VOLT:AC:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:AC:RANG %s", """ A property that controls the AC voltage range in Volts, which can take values 100E-3, 1, 10, 100, 750, as well as "MIN", "MAX", or "DEF" (10 V). Auto-range is disabled when this property is set. """, validator=strict_discrete_set, values=[100E-3, 1, 10, 100, 750, "MAX", "MIN", "DEF"] ) voltage_ac_auto_range = Instrument.control( ":SENS:VOLT:AC:RANG:AUTO?", ":SENS:VOLT:AC:RANG:AUTO %d", """ A boolean property that toggles auto ranging for AC voltage. """, validator=strict_discrete_set, values=BOOLS, map_values=True ) voltage_ac_resolution = Instrument.control( ":SENS:VOLT:AC:RES?", ":SENS:VOLT:AC:RES %s", """ A property that controls the resolution in the AC voltage readings, which can take values 3.00E-5, 2.00E-5, 1.50E-6 (5 1/2 digits), as well as "MIN", "MAX", or "DEF" (1.50E-6). """, validator=strict_discrete_set, values=[3.00E-5, 2.00E-5, 1.50E-6, "MAX", "MIN", "DEF"] ) #################### # Resistance (Ohm) # #################### resistance = Instrument.measurement(":READ?", """ Reads a resistance measurement in Ohms for 2-wire configuration, based on the active :attr:`~.Agilent34450A.mode`. """ ) resistance_4w = Instrument.measurement(":READ?", """ Reads a resistance measurement in Ohms for 4-wire configuration, based on the active :attr:`~.Agilent34450A.mode`. """ ) resistance_range = Instrument.control( ":SENS:RES:RANG?", ":SENS:RES:RANG:AUTO 0;:SENS:RES:RANG %s", """ A property that controls the 2-wire resistance range in Ohms, which can take values 100, 1E3, 10E3, 100E3, 1E6, 10E6, 100E6, as well as "MIN", "MAX", or "DEF" (1E3). Auto-range is disabled when this property is set. """, validator=strict_discrete_set, values=[100, 1E3, 10E3, 100E3, 1E6, 10E6, 100E6, "MAX", "MIN", "DEF"] ) resistance_auto_range = Instrument.control( ":SENS:RES:RANG:AUTO?", ":SENS:RES:RANG:AUTO %d", """ A boolean property that toggles auto ranging for 2-wire resistance. """, validator=strict_discrete_set, values=BOOLS, map_values=True ) resistance_resolution = Instrument.control( ":SENS:RES:RES?", ":SENS:RES:RES %s", """ A property that controls the resolution in the 2-wire resistance readings, which can take values 3.00E-5, 2.00E-5, 1.50E-6 (5 1/2 digits), as well as "MIN", "MAX", or "DEF" (1.50E-6). """, validator=strict_discrete_set, values=[3.00E-5, 2.00E-5, 1.50E-6, "MAX", "MIN", "DEF"] ) resistance_4w_range = Instrument.control( ":SENS:FRES:RANG?", ":SENS:FRES:RANG:AUTO 0;:SENS:FRES:RANG %s", """ A property that controls the 4-wire resistance range in Ohms, which can take values 100, 1E3, 10E3, 100E3, 1E6, 10E6, 100E6, as well as "MIN", "MAX", or "DEF" (1E3). Auto-range is disabled when this property is set. """, validator=strict_discrete_set, values=[100, 1E3, 10E3, 100E3, 1E6, 10E6, 100E6, "MAX", "MIN", "DEF"] ) resistance_4w_auto_range = Instrument.control( ":SENS:FRES:RANG:AUTO?", ":SENS:FRES:RANG:AUTO %d", """ A boolean property that toggles auto ranging for 4-wire resistance. """, validator=strict_discrete_set, values=BOOLS, map_values=True ) resistance_4w_resolution = Instrument.control( ":SENS:FRES:RES?", ":SENS:FRES:RES %s", """ A property that controls the resolution in the 4-wire resistance readings, which can take values 3.00E-5, 2.00E-5, 1.50E-6 (5 1/2 digits), as well as "MIN", "MAX", or "DEF" (1.50E-6). """, validator=strict_discrete_set, values=[3.00E-5, 2.00E-5, 1.50E-6, "MAX", "MIN", "DEF"] ) ################## # Frequency (Hz) # ################## frequency = Instrument.measurement(":READ?", """ Reads a frequency measurement in Hz, based on the active :attr:`~.Agilent34450A.mode`. """ ) frequency_current_range = Instrument.control( ":SENS:FREQ:CURR:RANG?", ":SENS:FREQ:CURR:RANG:AUTO 0;:SENS:FREQ:CURR:RANG %s", """ A property that controls the current range in Amps for frequency on AC current measurements, which can take values 10E-3, 100E-3, 1, 10, as well as "MIN", "MAX", or "DEF" (100 mA). Auto-range is disabled when this property is set. """, validator=strict_discrete_set, values=[10E-3, 100E-3, 1, 10, "MIN", "MAX", "DEF"] ) frequency_current_auto_range = Instrument.control( ":SENS:FREQ:CURR:RANG:AUTO?", ":SENS:FREQ:CURR:RANG:AUTO %d", """ A boolean property that toggles auto ranging for AC current in frequency measurements. """, validator=strict_discrete_set, values=BOOLS, map_values=True ) frequency_voltage_range = Instrument.control( ":SENS:FREQ:VOLT:RANG?", ":SENS:FREQ:VOLT:RANG:AUTO 0;:SENS:FREQ:VOLT:RANG %s", """ A property that controls the voltage range in Volts for frequency on AC voltage measurements, which can take values 100E-3, 1, 10, 100, 750, as well as "MIN", "MAX", or "DEF" (10 V). Auto-range is disabled when this property is set. """, validator=strict_discrete_set, values=[100E-3, 1, 10, 100, 750, "MAX", "MIN", "DEF"] ) frequency_voltage_auto_range = Instrument.control( ":SENS:FREQ:VOLT:RANG:AUTO?", ":SENS:FREQ:VOLT:RANG:AUTO %d", """ A boolean property that toggles auto ranging for AC voltage in frequency measurements. """, validator=strict_discrete_set, values=BOOLS, map_values=True ) frequency_aperture = Instrument.control( ":SENS:FREQ:APER?", ":SENS:FREQ:APER %s", """ A property that controls the frequency aperture in seconds, which sets the integration period and measurement speed. Takes values 100 ms, 1 s, as well as "MIN", "MAX", or "DEF" (1 s). """, validator=strict_discrete_set, values=[100E-3, 1, "MIN", "MAX", "DEF"] ) ################### # Temperature (C) # ################### temperature = Instrument.measurement(":READ?", """ Reads a temperature measurement in Celsius, based on the active :attr:`~.Agilent34450A.mode`. """ ) ############# # Diode (V) # ############# diode = Instrument.measurement(":READ?", """ Reads a diode measurement in Volts, based on the active :attr:`~.Agilent34450A.mode`. """ ) ################### # Capacitance (F) # ################### capacitance = Instrument.measurement(":READ?", """ Reads a capacitance measurement in Farads, based on the active :attr:`~.Agilent34450A.mode`. """ ) capacitance_range = Instrument.control( ":SENS:CAP:RANG?", ":SENS:CAP:RANG:AUTO 0;:SENS:CAP:RANG %s", """ A property that controls the capacitance range in Farads, which can take values 1E-9, 10E-9, 100E-9, 1E-6, 10E-6, 100E-6, 1E-3, 10E-3, as well as "MIN", "MAX", or "DEF" (1E-6). Auto-range is disabled when this property is set. """, validator=strict_discrete_set, values=[1E-9, 10E-9, 100E-9, 1E-6, 10E-6, 100E-6, 1E-3, 10E-3, "MAX", "MIN", "DEF"] ) capacitance_auto_range = Instrument.control( ":SENS:CAP:RANG:AUTO?", ":SENS:CAP:RANG:AUTO %d", """ A boolean property that toggles auto ranging for capacitance. """, validator=strict_discrete_set, values=BOOLS, map_values=True ) #################### # Continuity (Ohm) # #################### continuity = Instrument.measurement(":READ?", """ Reads a continuity measurement in Ohms, based on the active :attr:`~.Agilent34450A.mode`. """ ) def __init__(self, adapter, **kwargs): super(Agilent34450A, self).__init__( adapter, "HP/Agilent/Keysight 34450A Multimeter", **kwargs ) # Configuration changes can necessitate up to 8.8 secs (per datasheet) self.adapter.connection.timeout = 10000 self.check_errors() def check_errors(self): """ Read all errors from the instrument.""" while True: err = self.values(":SYST:ERR?") if int(err[0]) != 0: errmsg = "Agilent 34450A: %s: %s" % (err[0], err[1]) log.error(errmsg + '\n') else: break def configure_voltage(self, voltage_range="AUTO", ac=False, resolution="DEF"): """ Configures the instrument to measure voltage. :param voltage_range: A voltage in Volts to set the voltage range. DC values can be 100E-3, 1, 10, 100, 1000, as well as "MIN", "MAX", "DEF" (10 V), or "AUTO". AC values can be 100E-3, 1, 10, 100, 750, as well as "MIN", "MAX", "DEF" (10 V), or "AUTO". :param ac: False for DC voltage, True for AC voltage :param resolution: Desired resolution, can be 3.00E-5, 2.00E-5, 1.50E-6 (5 1/2 digits), as well as "MIN", "MAX", or "DEF" (1.50E-6). """ if ac is True: self.mode = 'ac voltage' self.voltage_ac_resolution = resolution if voltage_range == "AUTO": self.voltage_ac_auto_range = True else: self.voltage_ac_range = voltage_range elif ac is False: self.mode = 'voltage' self.voltage_resolution = resolution if voltage_range == "AUTO": self.voltage_auto_range = True else: self.voltage_range = voltage_range else: raise TypeError('Value of ac should be a boolean.') def configure_current(self, current_range="AUTO", ac=False, resolution="DEF"): """ Configures the instrument to measure current. :param current_range: A current in Amps to set the current range. DC values can be 100E-6, 1E-3, 10E-3, 100E-3, 1, 10, as well as "MIN", "MAX", "DEF" (100 mA), or "AUTO". AC values can be 10E-3, 100E-3, 1, 10, as well as "MIN", "MAX", "DEF" (100 mA), or "AUTO". :param ac: False for DC current, and True for AC current :param resolution: Desired resolution, can be 3.00E-5, 2.00E-5, 1.50E-6 (5 1/2 digits), as well as "MIN", "MAX", or "DEF" (1.50E-6). """ if ac is True: self.mode = 'ac current' self.current_ac_resolution = resolution if current_range == "AUTO": self.current_ac_auto_range = True else: self.current_ac_range = current_range elif ac is False: self.mode = 'current' self.current_resolution = resolution if current_range == "AUTO": self.current_auto_range = True else: self.current_range = current_range else: raise TypeError('Value of ac should be a boolean.') def configure_resistance(self, resistance_range="AUTO", wires=2, resolution="DEF"): """ Configures the instrument to measure resistance. :param resistance_range: A resistance in Ohms to set the resistance range, can be 100, 1E3, 10E3, 100E3, 1E6, 10E6, 100E6, as well as "MIN", "MAX", "DEF" (1E3), or "AUTO". :param wires: Number of wires used for measurement, can be 2 or 4. :param resolution: Desired resolution, can be 3.00E-5, 2.00E-5, 1.50E-6 (5 1/2 digits), as well as "MIN", "MAX", or "DEF" (1.50E-6). """ if wires == 2: self.mode = 'resistance' self.resistance_resolution = resolution if resistance_range == "AUTO": self.resistance_auto_range = True else: self.resistance_range = resistance_range elif wires == 4: self.mode = '4w resistance' self.resistance_4w_resolution = resolution if resistance_range == "AUTO": self.resistance_4w_auto_range = True else: self.resistance_4w_range = resistance_range else: raise ValueError("Incorrect wires value, Agilent 34450A only supports 2 or 4 wire" "resistance meaurement.") def configure_frequency(self, measured_from="voltage_ac", measured_from_range="AUTO", aperture="DEF"): """ Configures the instrument to measure frequency. :param measured_from: "voltage_ac" or "current_ac" :param measured_from_range: range of measured_from. AC voltage can have ranges 100E-3, 1, 10, 100, 750, as well as "MIN", "MAX", "DEF" (10 V), or "AUTO". AC current can have ranges 10E-3, 100E-3, 1, 10, as well as "MIN", "MAX", "DEF" (100 mA), or "AUTO". :param aperture: Aperture time in Seconds, can be 100 ms, 1 s, as well as "MIN", "MAX", or "DEF" (1 s). """ if measured_from == "voltage_ac": self.mode = "voltage frequency" if measured_from_range == "AUTO": self.frequency_voltage_auto_range = True else: self.frequency_voltage_range = measured_from_range elif measured_from == "current_ac": self.mode = "current frequency" if measured_from_range == "AUTO": self.frequency_current_auto_range = True else: self.frequency_current_range = measured_from_range else: raise ValueError('Incorrect value for measured_from parameter. Use ' '"voltage_ac" or "current_ac".') self.frequency_aperture = aperture def configure_temperature(self): """ Configures the instrument to measure temperature. """ self.mode = 'temperature' def configure_diode(self): """ Configures the instrument to measure diode voltage. """ self.mode = 'diode' def configure_capacitance(self, capacitance_range="AUTO"): """ Configures the instrument to measure capacitance. :param capacitance_range: A capacitance in Farads to set the capacitance range, can be 1E-9, 10E-9, 100E-9, 1E-6, 10E-6, 100E-6, 1E-3, 10E-3, as well as "MIN", "MAX", "DEF" (1E-6), or "AUTO". """ self.mode = 'capacitance' if capacitance_range == "AUTO": self.capacitance_auto_range = True else: self.capacitance_range = capacitance_range def configure_continuity(self): """ Configures the instrument to measure continuity. """ self.mode = 'continuity' def beep(self): """ Sounds a system beep. """ self.write(":SYST:BEEP") def _conf_parser(self, conf_values): """ Parse the string of configuration parameters read from Agilent34450A with command ":configure?" and returns a list of parameters. Use cases: ['"CURR +1.000000E-01', '+1.500000E-06"'] ** Obtained from Instrument.measurement or Instrument.control '"CURR +1.000000E-01,+1.500000E-06"' ** Obtained from Instrument.ask becomes ["CURR", +1000000E-01, +1.500000E-06] """ # If not already one string, get one string if isinstance(conf_values, list): one_long_string = ', '.join(map(str, conf_values)) else: one_long_string = conf_values # Split string in elements list_of_elements = re.split(r'["\s,]', one_long_string) # Eliminate empty string elements list_without_empty_elements = list(filter(lambda v: v != '', list_of_elements)) # Convert numbers from str to float, where applicable for i, v in enumerate(list_without_empty_elements): try: list_without_empty_elements[i] = float(v) except ValueError as e: log.error(e) return list_without_empty_elements PyMeasure-0.9.0/pymeasure/instruments/agilent/agilentE4408B.py0000664000175000017500000001033214010037617024440 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range from io import StringIO import numpy as np import pandas as pd class AgilentE4408B(Instrument): """ Represents the AgilentE4408B Spectrum Analyzer and provides a high-level interface for taking scans of high-frequency spectrums """ start_frequency = Instrument.control( ":SENS:FREQ:STAR?;", ":SENS:FREQ:STAR %e Hz;", """ A floating point property that represents the start frequency in Hz. This property can be set. """ ) stop_frequency = Instrument.control( ":SENS:FREQ:STOP?;", ":SENS:FREQ:STOP %e Hz;", """ A floating point property that represents the stop frequency in Hz. This property can be set. """ ) frequency_points = Instrument.control( ":SENSe:SWEEp:POINts?;", ":SENSe:SWEEp:POINts %d;", """ An integer property that represents the number of frequency points in the sweep. This property can take values from 101 to 8192. """, validator=truncated_range, values=[101, 8192], cast=int ) frequency_step = Instrument.control( ":SENS:FREQ:CENT:STEP:INCR?;", ":SENS:FREQ:CENT:STEP:INCR %g Hz;", """ A floating point property that represents the frequency step in Hz. This property can be set. """ ) center_frequency = Instrument.control( ":SENS:FREQ:CENT?;", ":SENS:FREQ:CENT %e Hz;", """ A floating point property that represents the center frequency in Hz. This property can be set. """ ) sweep_time = Instrument.control( ":SENS:SWE:TIME?;", ":SENS:SWE:TIME %.2e;", """ A floating point property that represents the sweep time in seconds. This property can be set. """ ) def __init__(self, resourceName, **kwargs): super(AgilentE4408B, self).__init__( resourceName, "Agilent E4408B Spectrum Analyzer", **kwargs ) @property def frequencies(self): """ Returns a numpy array of frequencies in Hz that correspond to the current settings of the instrument. """ return np.linspace( self.start_frequency, self.stop_frequency, self.frequency_points, dtype=np.float64 ) def trace(self, number=1): """ Returns a numpy array of the data for a particular trace based on the trace number (1, 2, or 3). """ self.write(":FORMat:TRACe:DATA ASCII;") data = np.loadtxt( StringIO(self.ask(":TRACE:DATA? TRACE%d;" % number)), delimiter=',', dtype=np.float64 ) return data def trace_df(self, number=1): """ Returns a pandas DataFrame containing the frequency and peak data for a particular trace, based on the trace number (1, 2, or 3). """ return pd.DataFrame({ 'Frequency (GHz)': self.frequencies*1e-9, 'Peak (dB)': self.trace(number) }) PyMeasure-0.9.0/pymeasure/instruments/agilent/agilent33500.py0000664000175000017500000004572014010037617024315 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Parts of this code were copied and adapted from the Agilent33220A class. import logging from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set,\ strict_range from time import time from pyvisa.errors import VisaIOError log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) # Capitalize string arguments to allow for better conformity with other WFG's # FIXME: Currently not used since it does not combine well with the strict_discrete_set validator # def capitalize_string(string: str, *args, **kwargs): # return string.upper() # Combine the capitalize function and validator # FIXME: This validator is not doing anything other then self.capitalize_string # FIXME: I removed it from this class for now # string_validator = joined_validators(capitalize_string, strict_discrete_set) class Agilent33500(Instrument): """Represents the Agilent 33500 Function/Arbitrary Waveform Generator family. Individual devices are represented by subclasses. .. code-block:: python generator = Agilent33500("GPIB::1") generator.shape = 'SIN' # Sets the output signal shape to sine generator.frequency = 1e3 # Sets the output frequency to 1 kHz generator.amplitude = 5 # Sets the output amplitude to 5 Vpp generator.output = 'on' # Enables the output generator.shape = 'ARB' # Set shape to arbitrary generator.arb_srate = 1e6 # Set sample rate to 1MSa/s generator.data_volatile_clear() # Clear volatile internal memory generator.data_arb( # Send data points of arbitrary waveform 'test', range(-10000, 10000, +20), # In this case a simple ramp data_format='DAC' # Data format is set to 'DAC' ) generator.arb_file = 'test' # Select the transmitted waveform 'test' """ id = Instrument.measurement( "*IDN?", """ Reads the instrument identification """ ) def __init__(self, adapter, **kwargs): super(Agilent33500, self).__init__( adapter, "Agilent 33500 Function/Arbitrary Waveform generator family", **kwargs ) def beep(self): """ Causes a system beep. """ self.write("SYST:BEEP") shape = Instrument.control( "FUNC?", "FUNC %s", """ A string property that controls the output waveform. Can be set to: SIN, SQU, TRI, RAMP, PULS, PRBS, NOIS, ARB, DC. """, validator=strict_discrete_set, values=["SIN", "SQU", "TRI", "RAMP", "PULS", "PRBS", "NOIS", "ARB", "DC"], ) frequency = Instrument.control( "FREQ?", "FREQ %f", """ A floating point property that controls the frequency of the output waveform in Hz, from 1 uHz to 120 MHz (maximum range, can be lower depending on your device), depending on the specified function. Can be set. """, validator=strict_range, values=[1e-6, 120e+6], ) amplitude = Instrument.control( "VOLT?", "VOLT %f", """ A floating point property that controls the voltage amplitude of the output waveform in V, from 10e-3 V to 10 V. Depends on the output impedance. Can be set. """, validator=strict_range, values=[10e-3, 10], ) amplitude_unit = Instrument.control( "VOLT:UNIT?", "VOLT:UNIT %s", """ A string property that controls the units of the amplitude. Valid values are VPP (default), VRMS, and DBM. Can be set. """, validator=strict_discrete_set, values=["VPP", "VRMS", "DBM"], ) offset = Instrument.control( "VOLT:OFFS?", "VOLT:OFFS %f", """ A floating point property that controls the voltage offset of the output waveform in V, from 0 V to 4.995 V, depending on the set voltage amplitude (maximum offset = (Vmax - voltage) / 2). Can be set. """, validator=strict_range, values=[-4.995, +4.995], ) voltage_high = Instrument.control( "VOLT:HIGH?", "VOLT:HIGH %f", """ A floating point property that controls the upper voltage of the output waveform in V, from -4.990 V to 5 V (must be higher than low voltage by at least 1 mV). Can be set. """, validator=strict_range, values=[-4.99, 5], ) voltage_low = Instrument.control( "VOLT:LOW?", "VOLT:LOW %f", """ A floating point property that controls the lower voltage of the output waveform in V, from -5 V to 4.990 V (must be lower than high voltage by at least 1 mV). Can be set. """, validator=strict_range, values=[-5, 4.99], ) square_dutycycle = Instrument.control( "FUNC:SQU:DCYC?", "FUNC:SQU:DCYC %f", """ A floating point property that controls the duty cycle of a square waveform function in percent, from 0.01% to 99.98%. The duty cycle is limited by the frequency and the minimal pulse width of 16 ns. See manual for more details. Can be set. """, validator=strict_range, values=[0.01, 99.98], ) ramp_symmetry = Instrument.control( "FUNC:RAMP:SYMM?", "FUNC:RAMP:SYMM %f", """ A floating point property that controls the symmetry percentage for the ramp waveform, from 0.0% to 100.0% Can be set. """, validator=strict_range, values=[0, 100], ) pulse_period = Instrument.control( "FUNC:PULS:PER?", "FUNC:PULS:PER %e", """ A floating point property that controls the period of a pulse waveform function in seconds, ranging from 33 ns to 1e6 s. Can be set and overwrites the frequency for *all* waveforms. If the period is shorter than the pulse width + the edge time, the edge time and pulse width will be adjusted accordingly. """, validator=strict_range, values=[33e-9, 1e6], ) pulse_hold = Instrument.control( "FUNC:PULS:HOLD?", "FUNC:PULS:HOLD %s", """ A string property that controls if either the pulse width or the duty cycle is retained when changing the period or frequency of the waveform. Can be set to: WIDT or DCYC. """, validator=strict_discrete_set, values=["WIDT", "WIDTH", "DCYC", "DCYCLE"], ) pulse_width = Instrument.control( "FUNC:PULS:WIDT?", "FUNC:PULS:WIDT %e", """ A floating point property that controls the width of a pulse waveform function in seconds, ranging from 16 ns to 1e6 s, within a set of restrictions depending on the period. Can be set. """, validator=strict_range, values=[16e-9, 1e6], ) pulse_dutycycle = Instrument.control( "FUNC:PULS:DCYC?", "FUNC:PULS:DCYC %f", """ A floating point property that controls the duty cycle of a pulse waveform function in percent, from 0% to 100%. Can be set. """, validator=strict_range, values=[0, 100], ) pulse_transition = Instrument.control( "FUNC:PULS:TRAN?", "FUNC:PULS:TRAN:BOTH %e", """ A floating point property that controls the edge time in seconds for both the rising and falling edges. It is defined as the time between the 10% and 90% thresholds of the edge. Valid values are between 8.4 ns to 1 µs. Can be set. """, validator=strict_range, values=[8.4e-9, 1e-6], ) output = Instrument.control( "OUTP?", "OUTP %d", """ A boolean property that turns on (True, 'on') or off (False, 'off') the output of the function generator. Can be set. """, validator=strict_discrete_set, map_values=True, values={True: 1, 'on': 1, 'ON': 1, False: 0, 'off': 0, 'OFF': 0}, ) output_load = Instrument.control( "OUTP:LOAD?", "OUTP:LOAD %s", """ Sets the expected load resistance (should be the load impedance connected to the output. The output impedance is always 50 Ohm, this setting can be used to correct the displayed voltage for loads unmatched to 50 Ohm. Valid values are between 1 and 10 kOhm or INF for high impedance. No validator is used since both numeric and string inputs are accepted, thus a value outside the range will not return an error. Can be set. """, ) burst_state = Instrument.control( "BURS:STAT?", "BURS:STAT %d", """ A boolean property that controls whether the burst mode is on (True) or off (False). Can be set. """, validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) burst_mode = Instrument.control( "BURS:MODE?", "BURS:MODE %s", """ A string property that controls the burst mode. Valid values are: TRIG, GAT. This setting can be set. """, validator=strict_discrete_set, values=["TRIG", "TRIGGERED", "GAT", "GATED"], ) burst_period = Instrument.control( "BURS:INT:PER?", "BURS:INT:PER %e", """ A floating point property that controls the period of subsequent bursts. Has to follow the equation burst_period > (burst_ncycles / frequency) + 1 µs. Valid values are 1 µs to 8000 s. Can be set. """, validator=strict_range, values=[1e-6, 8000], ) burst_ncycles = Instrument.control( "BURS:NCYC?", "BURS:NCYC %d", """ An integer property that sets the number of cycles to be output when a burst is triggered. Valid values are 1 to 100000. This can be set. """, validator=strict_range, values=range(1, 100000), ) arb_file = Instrument.control( "FUNC:ARB?", "FUNC:ARB %s", """ A string property that selects the arbitrary signal from the volatile memory of the device. String has to match an existing arb signal in volatile memore (set by data_arb()). Can be set. """ ) arb_advance = Instrument.control( "FUNC:ARB:ADV?", "FUNC:ARB:ADV %s", """ A string property that selects how the device advances from data point to data point. Can be set to 'TRIG' or 'SRAT' (default). """, validator=strict_discrete_set, values=["TRIG", "TRIGGER", "SRAT", "SRATE"], ) arb_filter = Instrument.control( "FUNC:ARB:FILT?", "FUNC:ARB:FILT %s", """ A string property that selects the filter setting for arbitrary signals. Can be set to 'NORM', 'STEP' and 'OFF'. """, validator=strict_discrete_set, values=["NORM", "NORMAL", "STEP", "OFF"], ) # TODO: This implementation is currently not working. Do not know why. # arb_period = Instrument.control( # "FUNC:ARB:PER?", "FUNC:ARB:PER %e", # """ A floating point property that controls the period of the arbitrary signal. # Limited by number of signal points. Check for instrument errors when setting # this property. Can be set. """, # validator=strict_range, # values=[33e-9, 1e6], # ) # # arb_frequency = Instrument.control( # "FUNC:ARB:FREQ?", "FUNC:ARB:FREQ %f", # """ A floating point property that controls the frequency of the arbitrary signal. # Limited by number of signal points. Check for instrument # errors when setting this property. Can be set. """, # validator=strict_range, # values=[1e-6, 30e+6], # ) # # arb_npoints = Instrument.measurement( # "FUNC:ARB:POIN?", # """ Returns the number of points in the currently selected arbitrary trace. """ # ) # # arb_voltage = Instrument.control( # "FUNC:ARB:PTP?", "FUNC:ARB:PTP %f", # """ An floating point property that sets the peak-to-peak voltage for the # currently selected arbitrary signal. Valid values are 1 mV to 10 V. This can be # set. """, # validator=strict_range, # values=[0.001, 10], # ) arb_srate = Instrument.control( "FUNC:ARB:SRAT?", "FUNC:ARB:SRAT %f", """ An floating point property that sets the sample rate of the currently selected arbitrary signal. Valid values are 1 µSa/s to 250 MSa/s (maximum range, can be lower depending on your device). This can be set. """, validator=strict_range, values=[1e-6, 250e6], ) def data_volatile_clear(self): """ Clear all arbitrary signals from the volatile memory. This should be done if the same name is used continuously to load different arbitrary signals into the memory, since an error will occur if a trace is loaded which already exists in the memory. """ self.write("DATA:VOL:CLE") def data_arb(self, arb_name, data_points, data_format='DAC'): """ Uploads an arbitrary trace into the volatile memory of the device. The data_points can be given as comma separated 16 bit DAC values (ranging from -32767 to +32767), as comma separated floating point values (ranging from -1.0 to +1.0) or as a binary data stream. Check the manual for more information. The storage depends on the device type and ranges from 8 Sa to 16 MSa (maximum). TODO: *Binary is not yet implemented* :param arb_name: The name of the trace in the volatile memory. This is used to access the trace. :param data_points: Individual points of the trace. The format depends on the format parameter. format = 'DAC' (default): Accepts list of integer values ranging from -32767 to +32767. Minimum of 8 a maximum of 65536 points. format = 'float': Accepts list of floating point values ranging from -1.0 to +1.0. Minimum of 8 a maximum of 65536 points. format = 'binary': Accepts a binary stream of 8 bit data. :param data_format: Defines the format of data_points. Can be 'DAC' (default), 'float' or 'binary'. See documentation on parameter data_points above. """ if data_format == 'DAC': separator = ', ' data_points_str = [str(item) for item in data_points] # Turn list entries into strings data_string = separator.join(data_points_str) # Join strings with separator print("DATA:ARB:DAC {}, {}".format(arb_name, data_string)) self.write("DATA:ARB:DAC {}, {}".format(arb_name, data_string)) return elif data_format == 'float': separator = ', ' data_points_str = [str(item) for item in data_points] # Turn list entries into strings data_string = separator.join(data_points_str) # Join strings with separator print("DATA:ARB {}, {}".format(arb_name, data_string)) self.write("DATA:ARB {}, {}".format(arb_name, data_string)) return elif data_format == 'binary': raise NotImplementedError('The binary format has not yet been implemented. Use "DAC" or "float" instead.') else: raise ValueError('Undefined format keyword was used. Valid entries are "DAC", "float" and "binary"') display = Instrument.setting( "DISP:TEXT '%s'", """ A string property which is displayed on the front panel of the device. Can be set. """, ) def clear_display(self): """ Removes a text message from the display. """ self.write("DISP:TEXT:CLE") def trigger(self): """ Send a trigger signal to the function generator. """ self.write("*TRG;*WAI") def wait_for_trigger(self, timeout=3600, should_stop=lambda: False): """ Wait until the triggering has finished or timeout is reached. :param timeout: The maximum time the waiting is allowed to take. If timeout is exceeded, a TimeoutError is raised. If timeout is set to zero, no timeout will be used. :param should_stop: Optional function (returning a bool) to allow the waiting to be stopped before its end. """ self.write("*OPC?") t0 = time() while True: try: ready = bool(self.read()) except VisaIOError: ready = False if ready: return if timeout != 0 and time() - t0 > timeout: raise TimeoutError( "Timeout expired while waiting for the Agilent 33220A" + " to finish the triggering." ) if should_stop: return trigger_source = Instrument.control( "TRIG:SOUR?", "TRIG:SOUR %s", """ A string property that controls the trigger source. Valid values are: IMM (internal), EXT (rear input), BUS (via trigger command). This setting can be set. """, validator=strict_discrete_set, values=["IMM", "IMMEDIATE", "EXT", "EXTERNAL", "BUS"], ) ext_trig_out = Instrument.control( "OUTP:TRIG?", "OUTP:TRIG %d", """ A boolean property that controls whether the trigger out signal is active (True) or not (False). This signal is output from the Ext Trig connector on the rear panel in Burst and Wobbel mode. Can be set. """, validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) def check_errors(self): """ Read all errors from the instrument. """ errors = [] while True: err = self.values("SYST:ERR?") if int(err[0]) != 0: errmsg = "Agilent 33521A: %s: %s" % (err[0], err[1]) log.error(errmsg + '\n') errors.append(errmsg) else: break return errors PyMeasure-0.9.0/pymeasure/instruments/agilent/agilentE4980.py0000664000175000017500000001570614010037617024355 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range, strict_discrete_set, strict_range from pyvisa.errors import VisaIOError class AgilentE4980(Instrument): """Represents LCR meter E4980A/AL""" ac_voltage = Instrument.control(":VOLT:LEV?", ":VOLT:LEV %g", "AC voltage level, in Volts", validator=strict_range, values=[0, 20]) ac_current = Instrument.control(":CURR:LEV?", ":CURR:LEV %g", "AC current level, in Amps", validator=strict_range, values=[0, 0.1]) frequency = Instrument.control(":FREQ:CW?", ":FREQ:CW %g", "AC frequency (range depending on model), in Hertz", validator=strict_range, values=[20, 2e6]) # FETCH? returns [A,B,state]: impedance returns only A,B impedance = Instrument.measurement(":FETCH?", "Measured data A and B, according to :attr:`~.AgilentE4980.mode`", get_process=lambda x: x[:2]) mode = Instrument.control("FUNCtion:IMPedance:TYPE?", "FUNCtion:IMPedance:TYPE %s", """ Select quantities to be measured: * CPD: Parallel capacitance [F] and dissipation factor [number] * CPQ: Parallel capacitance [F] and quality factor [number] * CPG: Parallel capacitance [F] and parallel conductance [S] * CPRP: Parallel capacitance [F] and parallel resistance [Ohm] * CSD: Series capacitance [F] and dissipation factor [number] * CSQ: Series capacitance [F] and quality factor [number] * CSRS: Series capacitance [F] and series resistance [Ohm] * LPD: Parallel inductance [H] and dissipation factor [number] * LPQ: Parallel inductance [H] and quality factor [number] * LPG: Parallel inductance [H] and parallel conductance [S] * LPRP: Parallel inductance [H] and parallel resistance [Ohm] * LSD: Series inductance [H] and dissipation factor [number] * LSQ: Seriesinductance [H] and quality factor [number] * LSRS: Series inductance [H] and series resistance [Ohm] * RX: Resitance [Ohm] and reactance [Ohm] * ZTD: Impedance, magnitude [Ohm] and phase [deg] * ZTR: Impedance, magnitude [Ohm] and phase [rad] * GB: Conductance [S] and susceptance [S] * YTD: Admittance, magnitude [Ohm] and phase [deg] * YTR: Admittance magnitude [Ohm] and phase [rad] """, validator=strict_discrete_set, values=["CPD", "CPQ", "CPG", "CPRP", "CSD", "CSQ", "CSRS", "LPD", "LPQ", "LPG", "LPRP", "LSD", "LSQ", "LSRS", "RX", "ZTD", "ZTR", "GB", "YTD", "YTR",]) trigger_source = Instrument.control("TRIG:SOUR?", "TRIG:SOUR %s", """ Select trigger source; accept the values: * HOLD: manual * INT: internal * BUS: external bus (GPIB/LAN/USB) * EXT: external connector""", validator=strict_discrete_set, values = ["HOLD", "INT", "BUS", "EXT"]) def __init__(self, adapter, **kwargs): super(AgilentE4980, self).__init__( adapter, "Agilent E4980A/AL LCR meter", **kwargs ) self.timeout = 30000 # format: output ascii self.write("FORM ASC") def freq_sweep(self, freq_list, return_freq=False): """ Run frequency list sweep using sequential trigger. :param freq_list: list of frequencies :param return_freq: if True, returns the frequencies read from the instrument Returns values as configured with :attr:`~.AgilentE4980.mode` """ # manual, page 299 # self.write("*RST;*CLS") self.write("TRIG:SOUR BUS") self.write("DISP:PAGE LIST") self.write("FORM ASC") #trigger in sequential mode self.write("LIST:MODE SEQ") lista_str = ",".join(['%e' % f for f in freq_list]) self.write("LIST:FREQ %s" % lista_str) # trigger self.write("INIT:CONT ON") self.write(":TRIG:IMM") #wait for completed measurement #using the Error signal (there should be a better way) while 1: try: measured = self.values(":FETCh:IMPedance:FORMatted?") break except VisaIOError: pass #at the end return to manual trigger self.write(":TRIG:SOUR HOLD") # gets 4-ples of numbers, first two are data A and B a_data = [measured[_] for _ in range(0, 4*len(freq_list), 4)] b_data = [measured[_] for _ in range(1, 4*len(freq_list), 4)] if return_freq: read_freqs = self.values("LIST:FREQ?") return a_data, b_data, read_freqs else: return a_data, b_data # TODO: maybe refactor as property? def aperture(self, time=None, averages=1): """ Set and get aperture. :param time: integration time as string: SHORT, MED, LONG (case insensitive); if None, get values :param averages: number of averages, numeric """ if time is None: read_values = self.ask(":APER?").split(',') return read_values[0], int(read_values[1]) else: if time.upper() in ["SHORT", "MED", "LONG"]: self.write(":APER {0}, {1}".format(time, averages)) else: raise Exception("Time must be a string: SHORT, MED, LONG") PyMeasure-0.9.0/pymeasure/instruments/yokogawa/0000775000175000017500000000000014010046235022107 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/yokogawa/yokogawa7651.py0000664000175000017500000002160314010046171024626 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument from pymeasure.instruments.validators import ( truncated_discrete_set, strict_discrete_set, truncated_range ) from time import sleep import numpy as np import re class Yokogawa7651(Instrument): """ Represents the Yokogawa 7651 Programmable DC Source and provides a high-level for interacting with the instrument. .. code-block:: python yoko = Yokogawa7651("GPIB::1") yoko.apply_current() # Sets up to source current yoko.source_current_range = 10e-3 # Sets the current range to 10 mA yoko.compliance_voltage = 10 # Sets the compliance voltage to 10 V yoko.source_current = 0 # Sets the source current to 0 mA yoko.enable_source() # Enables the current output yoko.ramp_to_current(5e-3) # Ramps the current to 5 mA yoko.shutdown() # Ramps the current to 0 mA and disables output """ @staticmethod def _find(v, key): """ Returns a value by parsing a current panel setting output string array, which is returned with a call to "OS;E". This is used for Instrument.control methods, and should not be called directly by the user. """ status = ''.join(v.split("\r\n\n")[1:-1]) keys = re.findall(r'[^\dE+.-]+', status) values = re.findall(r'[\dE+.-]+', status) if key not in keys: raise ValueError("Invalid key used to search for status of Yokogawa 7561") else: return values[keys.index(key)] source_voltage = Instrument.control( "OD;E", "S%g;E", """ A floating point property that controls the source voltage in Volts, if that mode is active. """ ) source_current = Instrument.control( "OD;E", "S%g;E", """ A floating point property that controls the source current in Amps, if that mode is active. """ ) source_voltage_range = Instrument.control( "OS;E", "R%d;E", """ A floating point property that sets the source voltage range in Volts, which can take values: 10 mV, 100 mV, 1 V, 10 V, and 30 V. Voltages are truncted to an appropriate value if needed. """, validator=truncated_discrete_set, values={10e-3:2, 100e-3:3, 1:4, 10:5, 30:6}, map_values=True, get_process=lambda v: int(Yokogawa7651._find(v, 'R')) ) source_current_range = Instrument.control( "OS;E", "R%d;E", """ A floating point property that sets the current voltage range in Amps, which can take values: 1 mA, 10 mA, and 100 mA. Currents are truncted to an appropriate value if needed. """, validator=truncated_discrete_set, values={1e-3:4, 10e-3:5, 100e-3:6}, map_values=True, get_process=lambda v: int(Yokogawa7651._find(v, 'R')) ) source_mode = Instrument.control( "OS;E", "F%d;E", """ A string property that controls the source mode, which can take the values 'current' or 'voltage'. The convenience methods :meth:`~.Yokogawa7651.apply_current` and :meth:`~.Yokogawa7651.apply_voltage` can also be used. """, validator=strict_discrete_set, values={'current':5, 'voltage':1}, map_values=True, get_process=lambda v: int(Yokogawa7651._find(v, 'F')) ) compliance_voltage = Instrument.control( "OS;E", "LV%g;E", """ A floating point property that sets the compliance voltage in Volts, which can take values between 1 and 30 V. """, validator=truncated_range, values=[1, 30], get_process=lambda v: int(Yokogawa7651._find(v, 'LV')) ) compliance_current = Instrument.control( "OS;E", "LA%g;E", """ A floating point property that sets the compliance current in Amps, which can take values from 5 to 120 mA. """, validator=truncated_range, values=[5e-3, 120e-3], get_process=lambda v: float(Yokogawa7651._find(v, 'LA'))*1e-3, # converts A to mA set_process=lambda v: v*1e3, # converts mA to A ) def __init__(self, adapter, **kwargs): super(Yokogawa7651, self).__init__( adapter, "Yokogawa 7651 Programmable DC Source", **kwargs ) self.write("H0;E") # Set no header in output data @property def id(self): """ Returns the identification of the instrument """ return self.ask("OS;E").split('\r\n\n')[0] @property def source_enabled(self): """ Reads a boolean value that is True if the source is enabled, determined by checking if the 5th bit of the OC flag is a binary 1. """ oc = int(self.ask("OC;E")[5:]) return oc & 0b10000 def enable_source(self): """ Enables the source of current or voltage depending on the configuration of the instrument. """ self.write("O1;E") def disable_source(self): """ Disables the source of current or voltage depending on the configuration of the instrument. """ self.write("O0;E") def apply_current(self, max_current=1e-3, compliance_voltage=1): """ Configures the instrument to apply a source current, which can take optional parameters that defer to the :attr:`~.Yokogawa7651.source_current_range` and :attr:`~.Yokogawa7651.compliance_voltage` properties. """ self.source_mode = 'current' self.source_current_range = max_current self.compliance_voltage = compliance_voltage def apply_voltage(self, max_voltage=1, compliance_current=10e-3): """ Configures the instrument to apply a source voltage, which can take optional parameters that defer to the :attr:`~.Yokogawa7651.source_voltage_range` and :attr:`~.Yokogawa7651.compliance_current` properties. """ self.source_mode = 'voltage' self.source_voltage_range = max_voltage self.compliance_current = compliance_current def ramp_to_current(self, current, steps=25, duration=0.5): """ Ramps the current to a value in Amps by traversing a linear spacing of current steps over a duration, defined in seconds. :param steps: A number of linear steps to traverse :param duration: A time in seconds over which to ramp """ start_current = self.source_current stop_current = current pause = duration/steps if (start_current != stop_current): currents = np.linspace(start_current, stop_current, steps) for current in currents: self.source_current = current sleep(pause) def ramp_to_voltage(self, voltage, steps=25, duration=0.5): """ Ramps the voltage to a value in Volts by traversing a linear spacing of voltage steps over a duration, defined in seconds. :param steps: A number of linear steps to traverse :param duration: A time in seconds over which to ramp """ start_voltage = self.source_voltage stop_voltage = voltage pause = duration/steps if (start_voltage != stop_voltage): voltages = np.linspace(start_voltage, stop_voltage, steps) for voltage in voltages: self.source_voltage = voltage sleep(pause) def shutdown(self): """ Shuts down the instrument, and ramps the current or voltage to zero before disabling the source. """ # Since voltage and current are set the same way, this # ramps either the current or voltage to zero self.ramp_to_current(0.0, steps=25) self.source_current = 0.0 self.disable_source() super(Yokogawa7651, self).shutdown() PyMeasure-0.9.0/pymeasure/instruments/yokogawa/__init__.py0000664000175000017500000000225414010037617024227 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .yokogawa7651 import Yokogawa7651 PyMeasure-0.9.0/pymeasure/instruments/lakeshore/0000775000175000017500000000000014010046235022243 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/lakeshore/adapters.py0000664000175000017500000000410214010037617024421 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.adapters import SerialAdapter class LakeShoreUSBAdapter(SerialAdapter): """ Provides a :class:`SerialAdapter` with the specific baudrate, timeout, parity, and byte size for LakeShore USB communication. Initiates the adapter to open serial communcation over the supplied port. :param port: A string representing the serial port """ def __init__(self, port): super(LakeShoreUSBAdapter, self).__init__( port, baudrate=57600, timeout=0.5, parity='O', bytesize=7 ) def write(self, command): """ Overwrites the :func:`SerialAdapter.write ` method to automatically append a Unix-style linebreak at the end of the command. :param command: SCPI command string to be sent to the instrument """ super(LakeShoreUSBAdapter, self).write(command + "\n") PyMeasure-0.9.0/pymeasure/instruments/lakeshore/__init__.py0000664000175000017500000000237514010037617024367 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .adapters import LakeShoreUSBAdapter from .lakeshore425 import LakeShore425 from .lakeshore331 import LakeShore331 PyMeasure-0.9.0/pymeasure/instruments/lakeshore/lakeshore425.py0000664000175000017500000001045614010037617025037 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, truncated_discrete_set from .adapters import LakeShoreUSBAdapter from time import sleep import numpy as np class LakeShore425(Instrument): """ Represents the LakeShore 425 Gaussmeter and provides a high-level interface for interacting with the instrument To allow user access to the LakeShore 425 Gaussmeter in Linux, create the file: :code:`/etc/udev/rules.d/52-lakeshore425.rules`, with contents: .. code-block:: none SUBSYSTEMS=="usb",ATTRS{idVendor}=="1fb9",ATTRS{idProduct}=="0401",MODE="0666",SYMLINK+="lakeshore425" Then reload the udev rules with: .. code-block:: bash sudo udevadm control --reload-rules sudo udevadm trigger The device will be accessible through :code:`/dev/lakeshore425`. """ field = Instrument.measurement( "RDGFIELD?", """ Returns the field in the current units """ ) unit = Instrument.control( "UNIT?", "UNIT %d", """ A string property that controls the units of the instrument, which can take the values of G, T, Oe, or A/m. """, validator=strict_discrete_set, values={'G':1, 'T':2, 'Oe':3, 'A/m':4}, map_values=True ) range = Instrument.control( "RANGE?", "RANGE %d", """ A floating point property that controls the field range in units of Gauss, which can take the values 35, 350, 3500, and 35,000 G. """, validator=truncated_discrete_set, values={35:1, 350:2, 3500:3, 35000:4}, map_values=True ) def __init__(self, port): super(LakeShore425, self).__init__( LakeShoreUSBAdapter(port), "LakeShore 425 Gaussmeter", ) def auto_range(self): """ Sets the field range to automatically adjust """ self.write("AUTO") def dc_mode(self, wideband=True): """ Sets up a steady-state (DC) measurement of the field """ if wideband: self.mode = (1, 0, 1) else: self.mode(1, 0, 2) def ac_mode(self, wideband=True): """ Sets up a measurement of an oscillating (AC) field """ if wideband: self.mode = (2, 1, 1) else: self.mode = (2, 1, 2) @property def mode(self): return tuple(self.values("RDGMODE?")) @mode.setter def mode(self, value): """ Provides access to directly setting the mode, filter, and bandwidth settings """ mode, filter, band = value self.write("RDGMODE %d,%d,%d" % (mode, filter, band)) def zero_probe(self): """ Initiates the zero field sequence to calibrate the probe """ self.write("ZPROBE") def measure(self, points, has_aborted=lambda: False, delay=1e-3): """Returns the mean and standard deviation of a given number of points while blocking """ data = np.zeros(points, dtype=np.float32) for i in range(points): if has_aborted(): break data[i] = self.field sleep(delay) return data.mean(), data.std() PyMeasure-0.9.0/pymeasure/instruments/lakeshore/lakeshore331.py0000664000175000017500000001172514010037617025033 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from time import sleep, time from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set class LakeShore331(Instrument): """ Represents the Lake Shore 331 Temperature Controller and provides a high-level interface for interacting with the instrument. .. code-block:: python controller = LakeShore331("GPIB::1") print(controller.setpoint_1) # Print the current setpoint for loop 1 controller.setpoint_1 = 50 # Change the setpoint to 50 K controller.heater_range = 'low' # Change the heater range to Low controller.wait_for_temperature() # Wait for the temperature to stabilize print(controller.temperature_A) # Print the temperature at sensor A """ temperature_A = Instrument.measurement( "KRDG? A", """ Reads the temperature of the sensor A in Kelvin. """ ) temperature_B = Instrument.measurement( "KRDG? B", """ Reads the temperature of the sensor B in Kelvin. """ ) setpoint_1 = Instrument.control( "SETP? 1", "SETP 1, %g", """ A floating point property that controls the setpoint temperature in Kelvin for Loop 1. """ ) setpoint_2 = Instrument.control( "SETP? 2", "SETP 2, %g", """ A floating point property that controls the setpoint temperature in Kelvin for Loop 2. """ ) heater_range = Instrument.control( "RANGE?", "RANGE %d", """ A string property that controls the heater range, which can take the values: off, low, medium, and high. These values correlate to 0, 0.5, 5 and 50 W respectively. """, validator=strict_discrete_set, values={'off':0, 'low':1, 'medium':2, 'high':3}, map_values=True ) def __init__(self, adapter, **kwargs): super(LakeShore331, self).__init__( adapter, "Lake Shore 331 Temperature Controller", **kwargs ) def disable_heater(self): """ Turns the :attr:`~.heater_range` to :code:`off` to disable the heater. """ self.heater_range = 'off' def wait_for_temperature(self, accuracy=0.1, interval=0.1, sensor='A', setpoint=1, timeout=360, should_stop=lambda: False): """ Blocks the program, waiting for the temperature to reach the setpoint within the accuracy (%), checking this each interval time in seconds. :param accuracy: An acceptable percentage deviation between the setpoint and temperature :param interval: A time in seconds that controls the refresh rate :param sensor: The desired sensor to read, either A or B :param setpoint: The desired setpoint loop to read, either 1 or 2 :param timeout: A timeout in seconds after which an exception is raised :param should_stop: A function that returns True if waiting should stop, by default this always returns False """ temperature_name = 'temperature_%s' % sensor setpoint_name = 'setpoint_%d' % setpoint # Only get the setpoint once, assuming it does not change setpoint_value = getattr(self, setpoint_name) def percent_difference(temperature): return abs(100*(temperature - setpoint_value)/setpoint_value) t = time() while percent_difference(getattr(self, temperature_name)) > accuracy: sleep(interval) if (time()-t) > timeout: raise Exception(( "Timeout occurred after waiting %g seconds for " "the LakeShore 331 temperature to reach %g K." ) % (timeout, setpoint)) if should_stop(): return PyMeasure-0.9.0/pymeasure/instruments/oxfordinstruments/0000775000175000017500000000000014010046235024103 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/oxfordinstruments/itc503.py0000664000175000017500000003161614010037617025477 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from time import sleep, time import numpy from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, \ truncated_range, strict_range # Setup logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ITC503(Instrument): """Represents the Oxford Intelligent Temperature Controller 503. .. code-block:: python itc = ITC503("GPIB::24") # Default channel for the ITC503 itc.control_mode = "RU" # Set the control mode to remote itc.heater_gas_mode = "AUTO" # Turn on auto heater and flow itc.auto_pid = True # Turn on auto-pid print(itc.temperature_setpoint) # Print the current set-point itc.temperature_setpoint = 300 # Change the set-point to 300 K itc.wait_for_temperature() # Wait for the temperature to stabilize print(itc.temperature_1) # Print the temperature at sensor 1 """ _T_RANGE = [0, 301] control_mode = Instrument.control( "X", "$C%d", """ A string property that sets the ITC in LOCAL or REMOTE and LOCKES, or UNLOCKES, the LOC/REM button. Allowed values are: LL: LOCAL & LOCKED RL: REMOTE & LOCKED LU: LOCAL & UNLOCKED RU: REMOTE & UNLOCKED. """, get_process=lambda v: int(v[5:6]), validator=strict_discrete_set, values={"LL": 0, "RL": 1, "LU": 2, "RU": 3}, map_values=True, ) heater_gas_mode = Instrument.control( "X", "$A%d", """ A string property that sets the heater and gas flow control to AUTO or MANUAL. Allowed values are: MANUAL: HEATER MANUAL, GAS MANUAL AM: HEATER AUTO, GAS MANUAL MA: HEATER MANUAL, GAS AUTO AUTO: HEATER AUTO, GAS AUTO. """, get_process=lambda v: int(v[3:4]), validator=strict_discrete_set, values={"MANUAL": 0, "AM": 1, "MA": 2, "AUTO": 3}, map_values=True, ) heater = Instrument.control( "R5", "$O%f", """ A floating point property that sets the required heater output when in manual mode. The parameter is expressed as a percentage of the maximum voltage. Valid values are in range 0 [off] to 99.9 [%]. """, get_process=lambda v: float(v[1:]), validator=truncated_range, values=[0, 99.9] ) gasflow = Instrument.control( "R7", "$G%f", """ A floating point property that controls gas flow when in manual mode. The value is expressed as a percentage of the maximum gas flow. Valid values are in range 0 [off] to 99.9 [%]. """, get_process=lambda v: float(v[1:]), validator=truncated_range, values=[0, 99.9] ) auto_pid = Instrument.control( "X", "$L%d", """ A boolean property that sets the Auto-PID mode on (True) or off (False). """, get_process=lambda v: int(v[12:13]), validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) sweep_status = Instrument.control( "X", "$S%d", """ An integer property that sets the sweep status. Values are: 0: Sweep not running 1: Start sweep / sweeping to first set-point 2P - 1: Sweeping to set-point P 2P: Holding at set-point P. """, get_process=lambda v: int(v[7:9]), validator=strict_range, values=[0, 32] ) temperature_setpoint = Instrument.control( "R0", "$T%f", """ A floating point property that controls the temperature set-point of the ITC in kelvin. """, get_process=lambda v: float(v[1:]), validator=truncated_range, values=_T_RANGE ) temperature_1 = Instrument.measurement( "R1", """ Reads the temperature of the sensor 1 in Kelvin. """, get_process=lambda v: float(v[1:]), ) temperature_2 = Instrument.measurement( "R2", """ Reads the temperature of the sensor 2 in Kelvin. """, get_process=lambda v: float(v[1:]), ) temperature_3 = Instrument.measurement( "R3", """ Reads the temperature of the sensor 3 in Kelvin. """, get_process=lambda v: float(v[1:]), ) temperature_error = Instrument.measurement( "R4", """ Reads the difference between the set-point and the measured temperature in Kelvin. Positive when set-point is larger than measured. """, get_process=lambda v: float(v[1:]), ) xpointer = Instrument.setting( "$x%d", """ An integer property to set pointers into tables for loading and examining values in the table. For programming the sweep table values from 1 to 16 are allowed, corresponding to the maximum number of steps. """, validator=strict_range, values=[0, 128] ) ypointer = Instrument.setting( "$y%d", """ An integer property to set pointers into tables for loading and examining values in the table. For programming the sweep table the allowed values are: 1: Setpoint temperature, 2: Sweep-time to set-point, 3: Hold-time at set-point. """, validator=strict_range, values=[0, 128] ) sweep_table = Instrument.control( "r", "$s%f", """ A property that sets values in the sweep table. Relies on the xpointer and ypointer to point at the location in the table that is to be set. """, get_process=lambda v: float(v[1:]), ) def __init__(self, resourceName, clear_buffer=True, max_temperature=301, min_temperature=0, **kwargs): super(ITC503, self).__init__( resourceName, "Oxford ITC503", includeSCPI=False, send_end=True, read_termination="\r", **kwargs ) # Clear the buffer in order to prevent communication problems if clear_buffer: self.adapter.connection.clear() self._T_RANGE[0] = min_temperature self._T_RANGE[1] = max_temperature def wait_for_temperature(self, error=0.01, timeout=3600, check_interval=0.5, stability_interval=10, thermalize_interval=300, should_stop=lambda: False, max_comm_errors=None): """ Wait for the ITC to reach the set-point temperature. :param error: The maximum error in Kelvin under which the temperature is considered at set-point :param timeout: The maximum time the waiting is allowed to take. If timeout is exceeded, a TimeoutError is raised. If timeout is set to zero, no timeout will be used. :param check_interval: The time between temperature queries to the ITC. :param stability_interval: The time over which the temperature_error is to be below error to be considered stable. :param thermalize_interval: The time to wait after stabilizing for the system to thermalize. :param should_stop: Optional function (returning a bool) to allow the waiting to be stopped before its end. :param max_comm_errors: The maximum number of communication errors that are allowed before the wait is stopped. if set to None (default), no maximum will be used. """ number_of_intervals = int(stability_interval / check_interval) stable_intervals = 0 attempt = 0 comm_errors = 0 t0 = time() while True: try: temp_error = self.temperature_error except ValueError: comm_errors += 1 log.error( "No temperature-error returned. " "Communication error # %d." % comm_errors ) else: if abs(temp_error) < error: stable_intervals += 1 else: stable_intervals = 0 attempt += 1 if stable_intervals >= number_of_intervals: break if timeout > 0 and (time() - t0) > timeout: raise TimeoutError( "Timeout expired while waiting for the Oxford ITC305 to \ reach the set-point temperature" ) if max_comm_errors is not None and comm_errors > max_comm_errors: raise ValueError( "Too many communication errors have occurred." ) if should_stop(): return sleep(check_interval) if attempt == 0: return t1 = time() + thermalize_interval while time() < t1: sleep(check_interval) if should_stop(): return return def program_sweep(self, temperatures, sweep_time, hold_time, steps=None): """ Program a temperature sweep in the controller. Stops any running sweep. After programming the sweep, it can be started using OxfordITC503.sweep_status = 1. :param temperatures: An array containing the temperatures for the sweep :param sweep_time: The time (or an array of times) to sweep to a set-point in minutes (between 0 and 1339.9). :param hold_time: The time (or an array of times) to hold at a set-point in minutes (between 0 and 1339.9). :param steps: The number of steps in the sweep, if given, the temperatures, sweep_time and hold_time will be interpolated into (approximately) equal segments """ # Check if in remote control if not self.control_mode.startswith("R"): raise AttributeError( "Oxford ITC503 not in remote control mode" ) # Stop sweep if running to be able to write the program self.sweep_status = 0 # Convert input np.ndarrays temperatures = numpy.array(temperatures, ndmin=1) sweep_time = numpy.array(sweep_time, ndmin=1) hold_time = numpy.array(hold_time, ndmin=1) # Make steps array if steps is None: steps = temperatures.size steps = numpy.linspace(1, steps, steps) # Create interpolated arrays interpolator = numpy.round( numpy.linspace(1, steps.size, temperatures.size)) temperatures = numpy.interp(steps, interpolator, temperatures) interpolator = numpy.round( numpy.linspace(1, steps.size, sweep_time.size)) sweep_time = numpy.interp(steps, interpolator, sweep_time) interpolator = numpy.round( numpy.linspace(1, steps.size, hold_time.size)) hold_time = numpy.interp(steps, interpolator, hold_time) # Pad with zeros to wipe unused steps (total 16) of the sweep program padding = 16 - temperatures.size temperatures = numpy.pad(temperatures, (0, padding), 'constant', constant_values=temperatures[-1]) sweep_time = numpy.pad(sweep_time, (0, padding), 'constant') hold_time = numpy.pad(hold_time, (0, padding), 'constant') # Setting the arrays to the controller for line, (setpoint, sweep, hold) in \ enumerate(zip(temperatures, sweep_time, hold_time), 1): self.xpointer = line self.ypointer = 1 self.sweep_table = setpoint self.ypointer = 2 self.sweep_table = sweep self.ypointer = 3 self.sweep_table = hold PyMeasure-0.9.0/pymeasure/instruments/oxfordinstruments/__init__.py0000664000175000017500000000224014010037617026216 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .itc503 import ITC503 PyMeasure-0.9.0/pymeasure/instruments/fwbell/0000775000175000017500000000000014010046235021541 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/fwbell/fwbell5080.py0000664000175000017500000001323114010037617023707 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument, RangeException from pymeasure.instruments.validators import truncated_discrete_set, strict_discrete_set from pymeasure.adapters import SerialAdapter from numpy import array, float64 class FWBell5080(Instrument): """ Represents the F.W. Bell 5080 Handheld Gaussmeter and provides a high-level interface for interacting with the instrument :param port: The serial port of the instrument .. code-block:: python meter = FWBell5080('/dev/ttyUSB0') # Connects over serial port /dev/ttyUSB0 (Linux) meter.units = 'gauss' # Sets the measurement units to Gauss meter.range = 3e3 # Sets the range to 3 kG print(meter.field) # Reads and prints a field measurement in G fields = meter.fields(100) # Samples 100 field measurements print(fields.mean(), fields.std()) # Prints the mean and standard deviation of the samples """ id = Instrument.measurement( "*IDN?", """ Reads the idenfitication information. """ ) field = Instrument.measurement( ":MEAS:FLUX?", """ Reads a floating point value of the field in the appropriate units. """, get_process=lambda v: v.split(' ')[0] # Remove units ) UNITS = { 'gauss':'DC:GAUSS', 'gauss ac':'AC:GAUSS', 'tesla':'DC:TESLA', 'tesla ac':'AC:TESLA', 'amp-meter':'DC:AM', 'amp-meter ac':'AC:AM' } units = Instrument.control( ":UNIT:FLUX?", ":UNIT:FLUX%s", """ A string property that controls the field units, which can take the values: 'gauss', 'gauss ac', 'tesla', 'tesla ac', 'amp-meter', and 'amp-meter ac'. The AC versions configure the instrument to measure AC. """, validator=strict_discrete_set, values=UNITS, map_values=True, get_process=lambda v: v.replace(' ', ':') # Make output consistent with input ) def __init__(self, port): super(FWBell5080, self).__init__( SerialAdapter(port, 2400, timeout=0.5), "F.W. Bell 5080 Handheld Gaussmeter" ) @property def range(self): """ A floating point property that controls the maximum field range in the active units. This can take the values of 300 G, 3 kG, and 30 kG for Gauss, 30 mT, 300 mT, and 3 T for Tesla, and 23.88 kAm, 238.8 kAm, and 2388 kAm for Amp-meter. """ i = self.values(":SENS:FLUX:RANG?", cast=int) units = self.units if 'gauss' in self.units: return [300, 3e3, 30e3][i] elif 'tesla' in self.units: return [30e-3, 300e-3, 3][i] elif 'amp-meter' in self.units: return [23.88e3, 238.8e3, 2388e3][i] @range.setter def range(self, value): units = self.units if 'gauss' in self.units: i = truncated_discrete_set(value, [300, 3e3, 30e3]) elif 'tesla' in self.units: i = truncated_discrete_set(value, [30e-3, 300e-3, 3]) elif 'amp-meter' in self.units: i = truncated_discrete_set(value, [23.88e3, 238.8e3, 2388e3]) self.write(":SENS:FLUX:RANG %d" % i) def read(self): """ Overwrites the :meth:`Instrument.read ` method to remove the last 2 characters from the output. """ return super(FWBell5080, self).read()[:-2] def ask(self, command): """ Overwrites the :meth:`Instrument.ask ` method to remove the last 2 characters from the output. """ return super(FWBell5080, self).ask()[:-2] def values(self, command): """ Overwrites the :meth:`Instrument.values ` method to remove the lastv2 characters from the output. """ return super(FWBell5080, self).values()[:-2] def reset(self): """ Resets the instrument. """ self.write("*OPC") def fields(self, samples=1): """ Returns a numpy array of field samples for a given sample number. :param samples: The number of samples to preform """ if samples < 1: raise Exception("F.W. Bell 5080 does not support samples less than 1.") else: data = [self.field for i in range(int(samples))] return array(data, dtype=float64) def auto_range(self): """ Enables the auto range functionality. """ self.write(":SENS:FLUX:RANG:AUTO") PyMeasure-0.9.0/pymeasure/instruments/fwbell/__init__.py0000664000175000017500000000225014010037617023655 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .fwbell5080 import FWBell5080 PyMeasure-0.9.0/pymeasure/instruments/tektronix/0000775000175000017500000000000014010046235022315 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/tektronix/tds2000.py0000664000175000017500000000655614010037617024003 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument class TDS2000(Instrument): """ Represents the Tektronix TDS 2000 Oscilloscope and provides a high-level for interacting with the instrument """ class Measurement(object): SOURCE_VALUES = ['CH1', 'CH2', 'MATH'] TYPE_VALUES = [ 'FREQ', 'MEAN', 'PERI', 'PHA', 'PK2', 'CRM', 'MINI', 'MAXI', 'RIS', 'FALL', 'PWI', 'NWI' ] UNIT_VALUES = ['V', 's', 'Hz'] def __init__(self, parent, preamble="MEASU:IMM:"): self.parent = parent self.preamble = preamble @property def value(self): return self.parent.values("%sVAL?" % self.preamble) @property def source(self): return self.parent.ask("%sSOU?" % self.preamble).strip() @source.setter def source(self, value): if value in TDS2000.Measurement.SOURCE_VALUES: self.parent.write("%sSOU %s" % (self.preamble, value)) else: raise ValueError("Invalid source ('%s') provided to %s" % ( self.parent, value)) @property def type(self): return self.parent.ask("%sTYP?" % self.preamble).strip() @type.setter def type(self, value): if value in TDS2000.Measurement.TYPE_VALUES: self.parent.write("%sTYP %s" % (self.preamble, value)) else: raise ValueError("Invalid type ('%s') provided to %s" % ( self.parent, value)) @property def unit(self): return self.parent.ask("%sUNI?" % self.preamble).strip() @unit.setter def unit(self, value): if value in TDS2000.Measurement.UNIT_VALUES: self.parent.write("%sUNI %s" % (self.preamble, value)) else: raise ValueError("Invalid unit ('%s') provided to %s" % ( self.parent, value)) def __init__(self, resourceName, **kwargs): super(TDS2000, self).__init__( resourceName, "Tektronix TDS 2000 Oscilliscope", **kwargs ) self.measurement = TDS2000.Measurement(self) PyMeasure-0.9.0/pymeasure/instruments/tektronix/__init__.py0000664000175000017500000000230114010037617024426 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .tds2000 import TDS2000 from .afg3152c import AFG3152C PyMeasure-0.9.0/pymeasure/instruments/tektronix/afg3152c.py0000664000175000017500000001676014010037617024120 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from math import sqrt,log10 from pymeasure.instruments import Instrument, RangeException from pymeasure.instruments.validators import strict_range, strict_discrete_set class Channel(object): SHAPES = { 'sinusoidal': 'SIN', 'square': 'SQU', 'pulse': 'PULS', 'ramp': 'RAMP', 'prnoise': 'PRN', 'dc': 'DC', 'sinc': 'SINC', 'gaussian': 'GAUS', 'lorentz': 'LOR', 'erise': 'ERIS', 'edecay': 'EDEC', 'haversine': 'HAV' } FREQ_LIMIT = [1e-6, 150e6] #Frequeny limit for sinusoidal function DUTY_LIMIT = [0.001, 99.999] AMPLITUDE_LIMIT = { 'VPP': [20e-3, 10], 'VRMS': list(map(lambda x: round(x/2/sqrt(2), 3), [20e-3, 10])), 'DBM': list(map(lambda x: round(20*log10(x/2/sqrt(0.1)), 2), [20e-3, 10])) } #Vpp, Vrms and dBm limits UNIT_LIMIT = ['VPP', 'VRMS', 'DBM'] IMP_LIMIT = [1, 1e4] shape = Instrument.control( "function:shape?","function:shape %s", """ A string property that controls the shape of the output. This property can be set.""", validator=strict_discrete_set, values=SHAPES, map_values=True ) unit = Instrument.control( "voltage:unit?","voltage:unit %s", """ A string property that controls the amplitude unit. This property can be set.""", validator=strict_discrete_set, values=UNIT_LIMIT ) amp_vpp = Instrument.control( "voltage:amplitude?","voltage:amplitude %eVPP", """ A floating point property that controls the output amplitude in Vpp. This property can be set.""", validator=strict_range, values=AMPLITUDE_LIMIT['VPP'] ) amp_dbm = Instrument.control( "voltage:amplitude?","voltage:amplitude %eDBM", """ A floating point property that controls the output amplitude in dBm. This property can be set.""", validator=strict_range, values=AMPLITUDE_LIMIT['DBM'] ) amp_vrms = Instrument.control( "voltage:amplitude?","voltage:amplitude %eVRMS", """ A floating point property that controls the output amplitude in Vrms. This property can be set.""", validator=strict_range, values=AMPLITUDE_LIMIT['VRMS'] ) offset = Instrument.control( "voltage:offset?","voltage:offset %e", """ A floating point property that controls the amplitude offset. It is always in Volt. This property can be set.""" ) frequency = Instrument.control( "frequency:fixed?","frequency:fixed %e", """ A floating point property that controls the frequency. This property can be set.""", validator=strict_range, values=FREQ_LIMIT ) duty = Instrument.control( "pulse:dcycle?","pulse:dcycle %.3f", """ A floating point property that controls the duty cycle of pulse. This property can be set.""", validator=strict_range, values=DUTY_LIMIT ) impedance = Instrument.control( "output:impedance?","output:impedance %d", """ A floating point property that controls the output impedance of the channel. Be careful with this. This property can be set.""", validator=strict_range, values=IMP_LIMIT, cast=int ) def __init__(self, instrument, number): self.instrument = instrument self.number = number def values(self, command, **kwargs): """ Reads a set of values from the instrument through the adapter, passing on any key-word arguments. """ return self.instrument.values("source%d:%s" % ( self.number, command), **kwargs) def ask(self, command): self.instrument.ask("source%d:%s" % (self.number, command)) def write(self, command): self.instrument.write("source%d:%s" % (self.number, command)) def read(self): self.instrument.read() def enable(self): self.instrument.write("output%d:state on" % self.number) def disable(self): self.instrument.write("output%d:state off" % self.number) def waveform(self, shape='SIN', frequency=1e6, units='VPP', amplitude=1, offset=0): """General setting method for a complete wavefunction""" self.instrument.write("source%d:function:shape %s" % ( self.number, shape)) self.instrument.write("source%d:frequency:fixed %e" % ( self.number, frequency)) self.instrument.write("source%d:voltage:unit %s" % ( self.number, units)) self.instrument.write("source%d:voltage:amplitude %e%s" %( self.number, amplitude,units)) self.instrument.write("source%d:voltage:offset %eV" %( self.number, offset)) class AFG3152C(Instrument): """Represents the Tektronix AFG 3000 series (one or two channels) arbitrary function generator and provides a high-level for interacting with the instrument. afg=AFG3152C("GPIB::1") # AFG on GPIB 1 afg.reset() # Reset to default afg.ch1.shape='sinusoidal' # Sinusoidal shape afg.ch1.unit='VPP' # Sets CH1 unit to VPP afg.ch1.amp_vpp=1 # Sets the CH1 level to 1 VPP afg.ch1.frequency=1e3 # Sets the CH1 frequency to 1KHz afg.ch1.enable() # Enables the output from CH1 """ def __init__(self, adapter, **kwargs): super(AFG3152C, self).__init__( adapter, "Tektronix AFG3152C arbitrary function generator", **kwargs ) self.ch1 = Channel(self, 1) self.ch2 = Channel(self, 2) def beep(self): self.write("system:beep") def opc(self): return int(self.ask("*OPC?")) PyMeasure-0.9.0/pymeasure/instruments/anapico/0000775000175000017500000000000014010046235021700 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/anapico/apsin12G.py0000664000175000017500000000600614010037617023644 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_range, strict_discrete_set class APSIN12G(Instrument): """ Represents the Anapico APSIN12G Signal Generator with option 9K, HP and GPIB. """ FREQ_LIMIT = [9e3, 12e9] POW_LIMIT = [-30, 27] power = Instrument.control( "SOUR:POW:LEV:IMM:AMPL?;", "SOUR:POW:LEV:IMM:AMPL %gdBm;", """ A floating point property that represents the output power in dBm. This property can be set. """, validator=strict_range, values=POW_LIMIT ) frequency = Instrument.control( "SOUR:FREQ:CW?;", "SOUR:FREQ:CW %eHz;", """ A floating point property that represents the output frequency in Hz. This property can be set. """, validator=strict_range, values=FREQ_LIMIT ) blanking = Instrument.control( ":OUTP:BLAN:STAT?", ":OUTP:BLAN:STAT %s", """ A string property that represents the blanking of output power when frequency is changed. ON makes the output to be blanked (off) while changing frequency. This property can be set. """, validator=strict_discrete_set, values=['ON','OFF'] ) reference_output = Instrument.control( "SOUR:ROSC:OUTP:STAT?", "SOUR:ROSC:OUTP:STAT %s", """ A string property that represents the 10MHz reference output from the synth. This property can be set. """, validator=strict_discrete_set, values=['ON','OFF'] ) def __init__(self, resourceName, **kwargs): super(APSIN12G, self).__init__( resourceName, "Anapico APSIN12G Signal Generator", **kwargs ) def enable_rf(self): """ Enables the RF output. """ self.write("OUTP:STAT 1") def disable_rf(self): """ Disables the RF output. """ self.write("OUTP:STAT 0")PyMeasure-0.9.0/pymeasure/instruments/anapico/__init__.py0000664000175000017500000000224414010037617024017 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .apsin12G import APSIN12G PyMeasure-0.9.0/pymeasure/instruments/ni/0000775000175000017500000000000014010046235020674 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/ni/daqmx.py0000664000175000017500000001516514010037617022374 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Most of this code originally from: # http://www.scipy.org/Cookbook/Data_Acquisition_with_NIDAQmx import logging import ctypes import numpy as np from sys import platform log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) try: if platform == "win32": nidaq = ctypes.windll.nicaiu except OSError as err: log.info('Failed loading the NI-DAQmx library. ' + 'Check the NI-DAQmx documentation on how to ' + 'install this external dependency. ' + 'OSError: {}'.format(err)) raise # Data Types int32 = ctypes.c_long uInt32 = ctypes.c_ulong uInt64 = ctypes.c_ulonglong float64 = ctypes.c_double TaskHandle = uInt32 # Constants DAQmx_Val_Cfg_Default = int32(-1) DAQmx_Val_Volts = 10348 DAQmx_Val_Rising = 10280 DAQmx_Val_FiniteSamps = 10178 DAQmx_Val_GroupByChannel = 1 class DAQmx(object): """Instrument object for interfacing with NI-DAQmx devices.""" def __init__(self, name, *args, **kwargs): super(DAQmx, self).__init__() self.resourceName = name # NOTE: Device number, e.g. Dev1 or PXI1Slot2 self.numChannels = 0 self.numSamples = 0 self.dataBuffer = 0 self.taskHandleAI = TaskHandle(0) self.taskHandleAO = TaskHandle(0) self.terminated = False def setup_analog_voltage_in(self, channelList, numSamples, sampleRate=10000, scale=3.0): resourceString = "" for num, channel in enumerate(channelList): if num > 0: resourceString += ", " # Add a comma before entries 2 and so on resourceString += self.resourceName + "/ai" + str(num) self.numChannels = len(channelList) self.numSamples = numSamples self.taskHandleAI = TaskHandle(0) self.dataBuffer = np.zeros((self.numSamples,self.numChannels), dtype=np.float64) self.CHK(nidaq.DAQmxCreateTask("",ctypes.byref(self.taskHandleAI))) self.CHK(nidaq.DAQmxCreateAIVoltageChan(self.taskHandleAI,resourceString,"", DAQmx_Val_Cfg_Default, float64(-scale),float64(scale), DAQmx_Val_Volts,None)) self.CHK(nidaq.DAQmxCfgSampClkTiming(self.taskHandleAI,"",float64(sampleRate), DAQmx_Val_Rising,DAQmx_Val_FiniteSamps, uInt64(self.numSamples))); def setup_analog_voltage_out(self, channel=0): resourceString = self.resourceName + "/ao" + str(channel) self.taskHandleAO = TaskHandle(0) self.CHK(nidaq.DAQmxCreateTask("", ctypes.byref( self.taskHandleAO ))) self.CHK(nidaq.DAQmxCreateAOVoltageChan( self.taskHandleAO, resourceString, "", float64(-10.0), float64(10.0), DAQmx_Val_Volts, None)) def setup_analog_voltage_out_multiple_channels(self, channelList): resourceString = "" for num, channel in enumerate(channelList): if num > 0: resourceString += ", " # Add a comma before entries 2 and so on resourceString += self.resourceName + "/ao" + str(num) self.taskHandleAO = TaskHandle(0) self.CHK(nidaq.DAQmxCreateTask("", ctypes.byref( self.taskHandleAO ))) self.CHK(nidaq.DAQmxCreateAOVoltageChan( self.taskHandleAO, resourceString, "", float64(-10.0), float64(10.0), DAQmx_Val_Volts, None)) def write_analog_voltage(self, value): timeout = -1.0 self.CHK(nidaq.DAQmxWriteAnalogScalarF64( self.taskHandleAO, 1, # Autostart float64(timeout), float64(value), None)) def write_analog_voltage_multiple_channels(self, values): timeout = -1.0 self.CHK(nidaq.DAQmxWriteAnalogF64( self.taskHandleAO, 1, # Samples per channel 1, # Autostart float64(timeout), DAQmx_Val_GroupByChannel, (np.array(values)).ctypes.data, None, None)) def acquire(self): read = int32() self.CHK(nidaq.DAQmxReadAnalogF64(self.taskHandleAI ,self.numSamples,float64(10.0), DAQmx_Val_GroupByChannel, self.dataBuffer.ctypes.data, self.numChannels*self.numSamples,ctypes.byref(read),None)) return self.dataBuffer.transpose() def acquire_average(self): if not self.terminated: avg = np.mean(self.acquire(), axis=1) return avg else: return np.zeros(3) def stop(self): if self.taskHandleAI.value != 0: nidaq.DAQmxStopTask(self.taskHandleAI) nidaq.DAQmxClearTask(self.taskHandleAI) if self.taskHandleAO.value != 0: nidaq.DAQmxStopTask(self.taskHandleAO) nidaq.DAQmxClearTask(self.taskHandleAO) def CHK(self, err): """a simple error checking routine""" if err < 0: buf_size = 100 buf = ctypes.create_string_buffer('\000' * buf_size) nidaq.DAQmxGetErrorString(err,ctypes.byref(buf),buf_size) raise RuntimeError('nidaq call failed with error %d: %s'%(err,repr(buf.value))) if err > 0: buf_size = 100 buf = ctypes.create_string_buffer('\000' * buf_size) nidaq.DAQmxGetErrorString(err,ctypes.byref(buf),buf_size) raise RuntimeError('nidaq generated warning %d: %s'%(err,repr(buf.value))) def shutdown(self): self.stop() self.terminated = True PyMeasure-0.9.0/pymeasure/instruments/ni/nidaq.py0000664000175000017500000000467514010037617022362 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Requires 'instrumental' package: https://github.com/mabuchilab/Instrumental from instrumental.drivers.daq import ni from pymeasure.instruments import Instrument def get_dict_attr(obj,attr): for obj in [obj]+obj.__class__.mro(): if attr in obj.__dict__: return obj.__dict__[attr] raise AttributeError class NIDAQ(Instrument): ''' Instrument driver for NIDAQ card. ''' def __init__(self, name='Dev1', *args, **kwargs): self._daq = ni.NIDAQ(name) super(NIDAQ, self).__init__( None, "NIDAQ", includeSCPI = False, **kwargs) for chan in self._daq.get_AI_channels(): self.add_property(chan) for chan in self._daq.get_AO_channels(): self.add_property(chan, set=True) def add_property(self, chan, set=False): if set: fset = lambda self, value: self.set_chan(chan, value) fget = lambda self: getattr(self, '_%s' %chan) setattr(self, '_%s' %chan, None) setattr(self.__class__,chan,property(fset=fset, fget=fget)) else: fget = lambda self: self.get_chan(chan) setattr(self.__class__,chan,property(fget=fget)) setattr(self.get, chan, lambda: getattr(self, chan)) def get_chan(self, chan): return getattr(self._daq,chan).read().magnitude def set_chan(self, chan, value): setattr(self, '_%s' %chan, value) getattr(self._daq,chan).write('%sV' %value) PyMeasure-0.9.0/pymeasure/instruments/ni/virtualbench.py0000664000175000017500000017334314010037617023753 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # pyvirtualbench library: Copyright (c) 2015 Charles Armstrap # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Requires 'pyvirtualbench' package: # https://github.com/armstrap/armstrap-pyvirtualbench import logging import re # ctypes only required for VirtualBench_Direct class from ctypes import (c_bool, c_size_t, c_double, c_uint8, c_int32, c_uint32, c_int64, c_uint64, c_wchar, c_wchar_p, Structure, c_int, cdll, byref) from datetime import datetime, timezone, timedelta import numpy as np import pandas as pd from pymeasure.instruments import Instrument, RangeException from pymeasure.instruments.validators import ( strict_discrete_set, strict_discrete_range, truncated_discrete_set, strict_range ) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) try: # Requires 'pyvirtualbench' package: # https://github.com/armstrap/armstrap-pyvirtualbench import pyvirtualbench as pyvb except ModuleNotFoundError as err: log.info('Failed loading the pyvirtualbench package. ' + 'Check the NI VirtualBench documentation on how to ' + 'install this external dependency. ' + 'ImportError: {}'.format(err)) raise class VirtualBench_Direct(pyvb.PyVirtualBench): """ Represents National Instruments Virtual Bench main frame. This class provides direct access to the armstrap/pyvirtualbench Python wrapper. """ def __init__(self, device_name='', name='VirtualBench'): ''' Initialize the VirtualBench library. This must be called at least once for the application. The 'version' parameter must be set to the NIVB_LIBRARY_VERSION constant. ''' self.device_name = device_name self.name = name self.nilcicapi = cdll.LoadLibrary("nilcicapi") self.library_handle = c_int(0) status = self.nilcicapi.niVB_Initialize(pyvb.NIVB_LIBRARY_VERSION, byref(self.library_handle)) if (status != pyvb.Status.SUCCESS): raise pyvb.PyVirtualBenchException(status, self.nilcicapi, self.library_handle) log.info("Initializing %s." % self.name) def __del__(self): """ Ensures the connection is closed upon deletion """ self.release() class VirtualBench(): """ Represents National Instruments Virtual Bench main frame. Subclasses implement the functionalities of the different modules: - Mixed-Signal-Oscilloscope (MSO) - Digital Input Output (DIO) - Function Generator (FGEN) - Power Supply (PS) - Serial Peripheral Interface (SPI) -> not implemented for pymeasure yet - Inter Integrated Circuit (I2C) -> not implemented for pymeasure yet For every module exist methods to save/load the configuration to file. These methods are not wrapped so far, checkout the pyvirtualbench file. All calibration methods and classes are not wrapped so far, since these are not required on a very regular basis. Also the connections via network are not yet implemented. Check the pyvirtualbench file, if you need the functionality. :param str device_name: Full unique device name :param str name: Name for display in pymeasure """ def __init__(self, device_name='', name='VirtualBench'): ''' Initialize the VirtualBench library. This must be called at least once for the application. The 'version' parameter must be set to the NIVB_LIBRARY_VERSION constant. ''' self.device_name = device_name self.name = name self.vb = pyvb.PyVirtualBench(self.device_name) log.info("Initializing %s." % self.name) def __del__(self): """ Ensures the connection is closed upon deletion """ if self.vb.library_handle is not None: self.vb.release() def shutdown(self): ''' Finalize the VirtualBench library. ''' log.info("Shutting down %s" % self.name) self.vb.release() self.isShutdown = True def get_library_version(self): ''' Return the version of the VirtualBench runtime library ''' return self.vb.get_library_version() def convert_timestamp_to_values(self, timestamp): """ Converts a timestamp to seconds and fractional seconds :param timestamp: VirtualBench timestamp :type timestamp: pyvb.Timestamp :return: (seconds_since_1970, fractional seconds) :rtype: (int, float) """ if not isinstance(timestamp, pyvb.Timestamp): raise ValueError("{0} is not a VirtualBench Timestamp object" .format(timestamp)) return self.vb.convert_timestamp_to_values(timestamp) def convert_values_to_timestamp(self, seconds_since_1970, fractional_seconds): """ Converts seconds and fractional seconds to a timestamp :param seconds_since_1970: Date/Time in seconds since 1970 :type seconds_since_1970: int :param fractional_seconds: Fractional seconds :type fractional_seconds: float :return: VirtualBench timestamp :rtype: pyvb.Timestamp """ return self.vb.convert_values_to_timestamp(seconds_since_1970, fractional_seconds) def convert_values_to_datetime(self, timestamp): """ Converts timestamp to datetime object :param timestamp: VirtualBench timestamp :type timestamp: pyvb.Timestamp :return: Timestamp as DateTime object :rtype: DateTime """ (seconds_since_1970, fractional_seconds) = self.convert_timestamp_to_values(timestamp) fractional_seconds = timedelta(seconds=fractional_seconds) return (datetime.fromtimestamp(seconds_since_1970, timezone.utc) + fractional_seconds) def collapse_channel_string(self, names_in): """ Collapses a channel string into a comma and colon-delimited equivalent. Last element is the number of channels. :param names_in: Channel string :type names_in: str :return: Channel string with colon notation where possible, number of channels :rtype: (str, int) """ if not isinstance(names_in, str): raise ValueError("{0} is not a string".format(names_in)) return self.vb.collapse_channel_string(names_in) def expand_channel_string(self, names_in): """ Expands a channel string into a comma-delimited (no colon) equivalent. Last element is the number of channels. ``'dig/0:2'`` -> ``('dig/0, dig/1, dig/2',3)`` :param names_in: Channel string :type names_in: str :return: Channel string with all channels separated by comma, number of channels :rtype: (str, int) """ return self.vb.expand_channel_string(names_in) def get_calibration_information(self): """ Returns calibration information for the specified device, including the last calibration date and calibration interval. :return: Calibration date, recommended calibration interval in months, calibration interval in months :rtype: (pyvb.Timestamp, int, int) """ return self.vb.get_calibration_information(self.device_name) def acquire_digital_input_output(self, lines, reset=False): """ Establishes communication with the DIO module. This method should be called once per session. :param lines: Lines to acquire, reading is possible on all lines :type lines: str :param reset: Reset DIO module, defaults to False :type reset: bool, optional """ reset = strict_discrete_set(reset, [True, False]) self.dio = self.DigitalInputOutput(self.vb, lines, reset, vb_name=self.name) def acquire_power_supply(self, reset=False): """ Establishes communication with the PS module. This method should be called once per session. :param reset: Reset the PS module, defaults to False :type reset: bool, optional """ reset = strict_discrete_set(reset, [True, False]) self.ps = self.PowerSupply(self.vb, reset, vb_name=self.name) def acquire_function_generator(self, reset=False): """ Establishes communication with the FGEN module. This method should be called once per session. :param reset: Reset the FGEN module, defaults to False :type reset: bool, optional """ reset = strict_discrete_set(reset, [True, False]) self.fgen = self.FunctionGenerator(self.vb, reset, vb_name=self.name) def acquire_mixed_signal_oscilloscope(self, reset=False): """ Establishes communication with the MSO module. This method should be called once per session. :param reset: Reset the MSO module, defaults to False :type reset: bool, optional """ reset = strict_discrete_set(reset, [True, False]) self.mso = self.MixedSignalOscilloscope(self.vb, reset, vb_name=self.name) def acquire_digital_multimeter(self, reset=False): """ Establishes communication with the DMM module. This method should be called once per session. :param reset: Reset the DMM module, defaults to False :type reset: bool, optional """ reset = strict_discrete_set(reset, [True, False]) self.dmm = self.DigitalMultimeter(self.vb, reset=reset, vb_name=self.name) class VirtualBenchInstrument(): def __init__(self, acquire_instr, reset, instr_identifier, vb_name=''): """Initialize instrument of VirtualBench device. Sets class variables and provides basic methods common to all VB instruments. :param acquire_instr: Method to acquire the instrument :type acquire_instr: method :param reset: Resets the instrument :type reset: bool :param instr_identifier: Shorthand identifier, e.g. mso or fgen :type instr_identifier: str :param vb_name: Name of VB device for logging, defaults to '' :type vb_name: str, optional """ # Parameters & Handle of VirtualBench Instance self._vb_handle = acquire_instr.__self__ self._device_name = self._vb_handle.device_name self.name = (vb_name + " " + instr_identifier.upper()).strip() log.info("Initializing %s." % self.name) self._instrument_handle = acquire_instr(self._device_name, reset) self.isShutdown = False def __del__(self): """ Ensures the connection is closed upon deletion """ if self.isShutdown is not True: self._instrument_handle.release() def shutdown(self): ''' Removes the session and deallocates any resources acquired during the session. If output is enabled on any channels, they remain in their current state. ''' log.info("Shutting down %s" % self.name) self._instrument_handle.release() self.isShutdown = True class DigitalInputOutput(VirtualBenchInstrument): """ Represents Digital Input Output (DIO) Module of Virtual Bench device. Allows to read/write digital channels and/or set channels to export the start signal of FGEN module or trigger of MSO module. """ def __init__(self, virtualbench, lines, reset, vb_name=''): """ Acquire DIO module :param virtualbench: VirtualBench Instance :type virtualbench: VirtualBench :param lines: Lines to acquire :type lines: str :param reset: Rest DIO module :type reset: bool """ # Parameters & Handle of VirtualBench Instance self._device_name = virtualbench.device_name self._vb_handle = virtualbench self.name = vb_name + " DIO" # Validate lines argument # store line names & numbers for future reference (self._line_names, self._line_numbers) = self.validate_lines( lines, return_single_lines=True, validate_init=False) # Create DIO Instance log.info("Initializing %s." % self.name) self.dio = self._vb_handle.acquire_digital_input_output( self._line_names, reset) # for methods provided by super class self._instrument_handle = self.dio self.isShutdown = False def validate_lines(self, lines, return_single_lines=False, validate_init=False): """Validate lines string Allowed patterns (case sensitive): - ``'VBxxxx-xxxxxxx/dig/0:7'`` - ``'VBxxxx-xxxxxxx/dig/0'`` - ``'dig/0'`` - ``'VBxxxx-xxxxxxx/trig'`` - ``'trig'`` Allowed Line Numbers: 0-7 or trig :param lines: Line string to test :type lines: str :param return_single_lines: Return list of line numbers as well, defaults to False :type return_single_lines: bool, optional :param validate_init: Check if lines are initialized (in :code:`self._line_numbers`), defaults to False :type validate_init: bool, optional :return: Line string, optional list of single line numbers :rtype: str, optional (str, list) """ def error(lines=lines): raise ValueError( "Line specification {0} is not valid!".format(lines)) lines = self._vb_handle.expand_channel_string(lines)[0] lines = lines.split(', ') return_lines = [] single_lines = [] for line in lines: if line == 'trig': device = self._device_name # otherwise (device_name/)dig/line or device_name/trig else: # split off line number by last '/' try: (device, line) = re.match( r'(.*)(?:/)(.+)', line).groups() except IndexError: error() if (line == 'trig') and (device == self._device_name): single_lines.append('trig') return_lines.append(self._device_name + '/' + line) elif int(line) in range(0, 8): line = int(line) single_lines.append(line) # validate device name: either 'dig' or 'device_name/dig' if device == 'dig': pass else: try: device = re.match( r'(VB[0-9]{4}-[0-9a-zA-Z]{7})(?:/dig)', device).groups()[0] except (IndexError, KeyError): error() # device_name has to match if not device == self._device_name: error() # constructing line references for output return_lines.append((self._device_name + '/dig/%d') % line) else: error() # check if lines are initialized if validate_init is True: if line not in self._line_numbers: raise ValueError( "Digital Line {} is not initialized".format(line)) # create comma separated channel string return_lines = ', '.join(return_lines) # collapse string if possible return_lines = self._vb_handle.collapse_channel_string( return_lines)[0] # drop number of lines if return_single_lines is True: return return_lines, single_lines else: return return_lines def tristate_lines(self, lines): ''' Sets all specified lines to a high-impedance state. (Default) ''' lines = self.validate_lines(lines, validate_init=True) self.dio.tristate_lines(lines) def export_signal(self, line, digitalSignalSource): """ Exports a signal to the specified line. :param line: Line string :type line: str :param digitalSignalSource: ``0`` for FGEN start or ``1`` for MSO trigger :type digitalSignalSource: int """ line = self.validate_lines(line, validate_init=True) digitalSignalSource_values = {"FGEN START": 0, "MSO TRIGGER": 1} digitalSignalSource = strict_discrete_set( digitalSignalSource.upper(), digitalSignalSource_values) digitalSignalSource = digitalSignalSource_values[ digitalSignalSource.upper()] self.dio.export_signal(line, digitalSignalSource) def query_line_configuration(self): ''' Indicates the current line configurations. Tristate Lines, Static Lines, and Export Lines contain comma-separated range_data and/or colon-delimited lists of all acquired lines ''' return self.dio.query_line_configuration() def query_export_signal(self, line): """ Indicates the signal being exported on the specified line. :param line: Line string :type line: str :return: Exported signal (FGEN start or MSO trigger) :rtype: enum """ line = self.validate_lines(line, validate_init=True) return self.dio.query_export_signal(line) def write(self, lines, data): """ Writes data to the specified lines. :param lines: Line string :type lines: str :param data: List of data, (``True`` = High, ``False`` = Low) :type data: list or tuple """ lines = self.validate_lines(lines, validate_init=True) try: for value in data: strict_discrete_set(value, [True, False]) except Exception: raise ValueError( "Data {} is not iterable (list or tuple).".format(data)) log.debug("{}: {} output {}.".format(self.name, lines, data)) self.dio.write(lines, data) def read(self, lines): """ Reads the current state of the specified lines. :param lines: Line string, requires full name specification e.g. ``'VB8012-xxxxxxx/dig/0:7'`` since instrument_handle is not required (only library_handle) :type lines: str :return: List of line states (HIGH/LOW) :rtype: list """ lines = self.validate_lines(lines, validate_init=False) # init not necessary for readout return self.dio.read(lines) def reset_instrument(self): ''' Resets the session configuration to default values, and resets the device and driver software to a known state. ''' self.dio.reset_instrument() class DigitalMultimeter(VirtualBenchInstrument): """ Represents Digital Multimeter (DMM) Module of Virtual Bench device. Allows to measure either DC/AC voltage or current, Resistance or Diodes. """ def __init__(self, virtualbench, reset, vb_name=''): """ Acquire DMM module :param virtualbench: Instance of the VirtualBench class :type virtualbench: VirtualBench :param reset: Resets the instrument :type reset: bool """ super().__init__( virtualbench.acquire_digital_multimeter, reset, 'dmm', vb_name) self.dmm = self._instrument_handle @staticmethod def validate_range(dmm_function, range): """ Checks if ``range`` is valid for the chosen ``dmm_function`` :param int dmm_function: DMM Function :param range: Range value, e.g. maximum value to measure :type range: int or float :return: Range value to pass to instrument :rtype: int """ ref_ranges = { 0: [0.1, 1, 10, 100, 300], 1: [0.1, 1, 10, 100, 265], 2: [0.01, 0.1, 1, 10], 3: [0.005, 0.05, 0.5, 5], 4: [100, 1000, 10000, 100000, 1000000, 10000000, 100000000], } range = truncated_discrete_set(range, ref_ranges[dmm_function]) return range def validate_dmm_function(self, dmm_function): """ Check if DMM function *dmm_function* exists :param dmm_function: DMM function index or name: - ``'DC_VOLTS'``, ``'AC_VOLTS'`` - ``'DC_CURRENT'``, ``'AC_CURRENT'`` - ``'RESISTANCE'`` - ``'DIODE'`` :type dmm_function: int or str :return: DMM function index to pass to the instrument :rtype: int """ try: pyvb.DmmFunction(dmm_function) except Exception: try: dmm_function = pyvb.DmmFunction[dmm_function.upper()] except Exception: raise ValueError( "DMM Function may be 0-5, 'DC_VOLTS'," + " 'AC_VOLTS', 'DC_CURRENT', 'AC_CURRENT'," + " 'RESISTANCE' or 'DIODE'") return dmm_function def validate_auto_range_terminal(self, auto_range_terminal): """ Check value for choosing the auto range terminal for DC current measurement :param auto_range_terminal: Terminal to perform auto ranging (``'LOW'`` or ``'HIGH'``) :type auto_range_terminal: int or str :return: Auto range terminal to pass to the instrument :rtype: int """ try: pyvb.DmmCurrentTerminal(auto_range_terminal) except Exception: try: auto_range_terminal = pyvb.DmmCurrentTerminal[ auto_range_terminal.upper()] except Exception: raise ValueError( "Current Auto Range Terminal may be 0, 1," + " 'LOW' or 'HIGH'") return auto_range_terminal def configure_measurement(self, dmm_function, auto_range=True, manual_range=1.0): """ Configure Instrument to take a DMM measurement :param dmm_function:DMM function index or name: - ``'DC_VOLTS'``, ``'AC_VOLTS'`` - ``'DC_CURRENT'``, ``'AC_CURRENT'`` - ``'RESISTANCE'`` - ``'DIODE'`` :type dmm_function: int or str :param bool auto_range: Enable/Disable auto ranging :param float manual_range: Manually set measurement range """ dmm_function = self.validate_dmm_function(dmm_function) auto_range = strict_discrete_set(auto_range, [True, False]) if auto_range is False: manual_range = self.validate_range(dmm_function, range) self.dmm.configure_measurement( dmm_function, auto_range=auto_range, manual_range=manual_range) def configure_dc_voltage(self, dmm_input_resistance): """ Configure DC voltage input resistance :param dmm_input_resistance: Input resistance (``'TEN_MEGA_OHM'`` or ``'TEN_GIGA_OHM'``) :type dmm_input_resistance: int or str """ try: pyvb.DmmInputResistance(dmm_input_resistance) except Exception: try: dmm_input_resistance = pyvb.DmmInputResistance[ dmm_input_resistance.upper()] except Exception: raise ValueError( "Input Resistance may be 0, 1," + " 'TEN_MEGA_OHM' or 'TEN_GIGA_OHM'") self.dmm.configure_dc_voltage(dmm_input_resistance) def configure_dc_current(self, auto_range_terminal): """ Configure auto rage terminal for DC current measurement :param auto_range_terminal: Terminal to perform auto ranging (``'LOW'`` or ``'HIGH'``) """ auto_range_terminal = self.validate_auto_range_terminal( auto_range_terminal) self.dmm.configure_dc_current(auto_range_terminal) def configure_ac_current(self, auto_range_terminal): """ Configure auto rage terminal for AC current measurement :param auto_range_terminal: Terminal to perform auto ranging (``'LOW'`` or ``'HIGH'``) """ auto_range_terminal = self.validate_auto_range_terminal( auto_range_terminal) self.dmm.configure_ac_current(auto_range_terminal) def query_measurement(self): """ Query DMM measurement settings from the instrument :return: Auto range, range data :rtype: (bool, float) """ return self.dmm.query_measurement(0) def query_dc_voltage(self): """ Indicates input resistance setting for DC voltage measurement """ self.dmm.query_dc_voltage() def query_dc_current(self): """ Indicates auto range terminal for DC current measurement """ self.dmm.query_dc_current() def query_ac_current(self): """ Indicates auto range terminal for AC current measurement """ self.dmm.query_ac_current() def read(self): """ Read measurement value from the instrument :return: Measurement value :rtype: float """ self.dmm.read() def reset_instrument(self): """ Reset the DMM module to defaults """ self.dmm.reset_instrument() class FunctionGenerator(VirtualBenchInstrument): """ Represents Function Generator (FGEN) Module of Virtual Bench device. """ def __init__(self, virtualbench, reset, vb_name=''): """ Acquire FGEN module :param virtualbench: Instance of the VirtualBench class :type virtualbench: VirtualBench :param reset: Resets the instrument :type reset: bool """ super().__init__( virtualbench.acquire_function_generator, reset, 'fgen', vb_name) self.fgen = self._instrument_handle self._waveform_functions = {"SINE": 0, "SQUARE": 1, "TRIANGLE/RAMP": 2, "DC": 3} # self._waveform_functions_index = { # v: k for k, v in self._waveform_functions.items()} self._max_frequency = {"SINE": 20000000, "SQUARE": 5000000, "TRIANGLE/RAMP": 1000000, "DC": 20000000} def configure_standard_waveform(self, waveform_function, amplitude, dc_offset, frequency, duty_cycle): """ Configures the instrument to output a standard waveform. Check instrument manual for maximum ratings which depend on load. :param waveform_function: Waveform function (``"SINE", "SQUARE", "TRIANGLE/RAMP", "DC"``) :type waveform_function: int or str :param amplitude: Amplitude in volts :type amplitude: float :param dc_offset: DC offset in volts :type dc_offset: float :param frequency: Frequency in Hz :type frequency: float :param duty_cycle: Duty cycle in % :type duty_cycle: int """ waveform_function = strict_discrete_set( waveform_function.upper(), self._waveform_functions) max_frequency = self._max_frequency[waveform_function.upper()] waveform_function = self._waveform_functions[ waveform_function.upper()] amplitude = strict_range(amplitude, (0, 24)) dc_offset = strict_range(dc_offset, (-12, 12)) if (amplitude/2 + abs(dc_offset)) > 12: raise ValueError( "Amplitude and DC Offset may not exceed +/-12V") duty_cycle = strict_range(duty_cycle, (0, 100)) frequency = strict_range(frequency, (0, max_frequency)) self.fgen.configure_standard_waveform( waveform_function, amplitude, dc_offset, frequency, duty_cycle) def configure_arbitrary_waveform(self, waveform, sample_period): """ Configures the instrument to output a waveform. The waveform is output either after the end of the current waveform if output is enabled, or immediately after output is enabled. :param waveform: Waveform as list of values :type waveform: list :param sample_period: Time between two waveform points (maximum of 125MS/s, which equals 80ns) :type sample_period: float """ strict_range(len(waveform), (1, 1e6)) # 1MS sample_period = strict_range(sample_period, (8e-8, 1)) self.fgen.configure_arbitrary_waveform(waveform, sample_period) def configure_arbitrary_waveform_gain_and_offset(self, gain, dc_offset): """ Configures the instrument to output an arbitrary waveform with a specified gain and offset value. The waveform is output either after the end of the current waveform if output is enabled, or immediately after output is enabled. :param gain: Gain, multiplier of waveform values :type gain: float :param dc_offset: DC offset in volts :type dc_offset: float """ dc_offset = strict_range(dc_offset, (-12, 12)) self.fgen.configure_arbitrary_waveform_gain_and_offset( gain, dc_offset) @property def filter(self): ''' Enables or disables the filter on the instrument. :param bool enable_filter: Enable/Disable filter ''' return self.fgen.query_filter @filter.setter def filter(self, enable_filter): enable_filter = strict_discrete_set(enable_filter, [True, False]) self.fgen.enable_filter(enable_filter) def query_waveform_mode(self): """ Indicates whether the waveform output by the instrument is a standard or arbitrary waveform. :return: Waveform mode :rtype: enum """ return self.fgen.query_waveform_mode() def query_standard_waveform(self): """ Returns the settings for a standard waveform generation. :return: Waveform function, amplitude, dc_offset, frequency, duty_cycle :rtype: (enum, float, float, float, int) """ return self.fgen.query_standard_waveform() def query_arbitrary_waveform(self): """ Returns the samples per second for arbitrary waveform generation. :return: Samples per second :rtype: int """ return self.fgen.query_arbitrary_waveform() def query_arbitrary_waveform_gain_and_offset(self): """ Returns the settings for arbitrary waveform generation that includes gain and offset settings. :return: Gain, DC offset :rtype: (float, float) """ return self.fgen.query_arbitrary_waveform_gain_and_offset() def query_generation_status(self): """ Returns the status of waveform generation on the instrument. :return: Status :rtype: enum """ return self.fgen.query_generation_status() def run(self): ''' Transitions the session from the Stopped state to the Running state. ''' log.info("%s START" % self.name) self.fgen.run() def self_calibrate(self): '''Performs offset nulling calibration on the device. You must run FGEN Initialize prior to running this method. ''' self.fgen.self_calibrate() def stop(self): ''' Transitions the acquisition from either the Triggered or Running state to the Stopped state. ''' log.info("%s STOP" % self.name) self.fgen.stop() def reset_instrument(self): ''' Resets the session configuration to default values, and resets the device and driver software to a known state. ''' self.fgen.reset_instrument() class MixedSignalOscilloscope(VirtualBenchInstrument): """ Represents Mixed Signal Oscilloscope (MSO) Module of Virtual Bench device. Allows to measure oscilloscope data from analog and digital channels. Methods from pyvirtualbench not implemented in pymeasure yet: - ``enable_digital_channels`` - ``configure_digital_threshold`` - ``configure_advanced_digital_timing`` - ``configure_state_mode`` - ``configure_digital_edge_trigger`` - ``configure_digital_pattern_trigger`` - ``configure_digital_glitch_trigger`` - ``configure_digital_pulse_width_trigger`` - ``query_digital_channel`` - ``query_enabled_digital_channels`` - ``query_digital_threshold`` - ``query_advanced_digital_timing`` - ``query_state_mode`` - ``query_digital_edge_trigger`` - ``query_digital_pattern_trigger`` - ``query_digital_glitch_trigger`` - ``query_digital_pulse_width_trigger`` - ``read_digital_u64`` """ def __init__(self, virtualbench, reset, vb_name=''): """ Acquire MSO module :param virtualbench: Instance of the VirtualBench class :type virtualbench: VirtualBench :param reset: Resets the instrument :type reset: bool """ super().__init__( virtualbench.acquire_mixed_signal_oscilloscope, reset, 'mso', vb_name) self.mso = self._instrument_handle @staticmethod def validate_trigger_instance(trigger_instance): """ Check if ``trigger_instance`` is a valid choice :param trigger_instance: Trigger instance (``'A'`` or ``'B'``) :type trigger_instance: int or str :return: Trigger instance :rtype: int """ try: pyvb.MsoTriggerInstance(trigger_instance) except Exception: try: trigger_instance = pyvb.MsoTriggerInstance[ trigger_instance.upper()] except Exception: raise ValueError( "Trigger Instance may be 0, 1, 'A' or 'B'") return trigger_instance def validate_channel(self, channel): """ Check if ``channel`` is a correct specification :param str channel: Channel string :return: Channel string :rtype: str """ def error(channel=channel): raise ValueError( "Channel specification {0} is not valid!".format(channel)) channels = self._vb_handle.expand_channel_string(channel)[0] channels = channels.split(', ') return_value = [] for channel in channels: # split off lines by last '/' try: (device, channel) = re.match( r'(.*)(?:/)(.+)', channel).groups() except Exception: error() # validate numbers in range 1-2 if not int(channel) in range(1, 3): error() # validate device name: either 'mso' or 'device_name/mso' if device == 'mso': pass else: try: device = re.match( r'(VB[0-9]{4}-[0-9a-zA-Z]{7})(?:/)(.+)', device).groups()[0] except Exception: error() # device_name has to match if not device == self._device_name: error() # constructing line references for output return_value.append('mso/' + channel) return_value = ', '.join(return_value) return_value = self._vb_handle.collapse_channel_string( return_value)[0] # drop number of channels return return_value # -------------------------- # Configure Instrument # -------------------------- def auto_setup(self): """ Automatically configure the instrument """ self.mso.auto_setup() def configure_analog_channel(self, channel, enable_channel, vertical_range, vertical_offset, probe_attenuation, vertical_coupling): """ Configure analog measurement channel :param str channel: Channel string :param bool enable_channel: Enable/Disable channel :param float vertical_range: Vertical measurement range (0V - 20V), the instrument discretizes to these ranges: ``[20, 10, 5, 2, 1, 0.5, 0.2, 0.1, 0.05]`` which are 5x the values shown in the native UI. :param float vertical_offset: Vertical offset to correct for (inverted compared to VB native UI, -20V - +20V, resolution 0.1mV) :param probe_attenuation: Probe attenuation (``'ATTENUATION_10X'`` or ``'ATTENUATION_1X'``) :type probe_attenuation: int or str :param vertical_coupling: Vertical coupling (``'AC'`` or ``'DC'``) :type vertical_coupling: int or str """ channel = self.validate_channel(channel) enable_channel = strict_discrete_set( enable_channel, [True, False]) vertical_range = strict_range(vertical_range, (0, 20)) vertical_offset = strict_discrete_range( vertical_offset, [-20, 20], 1e-4 ) try: pyvb.MsoProbeAttenuation(probe_attenuation) except Exception: try: probe_attenuation = pyvb.MsoProbeAttenuation[ probe_attenuation.upper()] except Exception: raise ValueError( "Probe Attenuation may be 1, 10," + " 'ATTENUATION_10X' or 'ATTENUATION_1X'") try: pyvb.MsoCoupling(vertical_coupling) except Exception: try: vertical_coupling = pyvb.MsoCoupling[ vertical_coupling.upper()] except Exception: raise ValueError( "Probe Attenuation may be 0, 1, 'AC' or 'DC'") self.mso.configure_analog_channel( channel, enable_channel, vertical_range, vertical_offset, probe_attenuation, vertical_coupling) def configure_analog_channel_characteristics(self, channel, input_impedance, bandwidth_limit): """ Configure electrical characteristics of the specified channel :param str channel: Channel string :param input_impedance: Input Impedance (``'ONE_MEGA_OHM'`` or ``'FIFTY_OHMS'``) :type input_impedance: int or str :param int bandwidth_limit: Bandwidth limit (100MHz or 20MHz) """ channel = self.validate_channel(channel) try: pyvb.MsoInputImpedance(input_impedance) except Exception: try: input_impedance = pyvb.MsoInputImpedance[ input_impedance.upper()] except Exception: raise ValueError( "Probe Attenuation may be 0, 1," + " 'ONE_MEGA_OHM' or 'FIFTY_OHMS'") bandwidth_limit = strict_discrete_set( bandwidth_limit, [100000000, 20000000]) # 100 Mhz or 20Mhz self.mso.configure_analog_channel_characteristics( channel, input_impedance, bandwidth_limit) def configure_timing(self, sample_rate, acquisition_time, pretrigger_time, sampling_mode): """ Configure timing settings of the MSO :param int sample_rate: Sample rate (15.26kS - 1GS) :param float acquisition_time: Acquisition time (1ns - 68.711s) :param float pretrigger_time: Pretrigger time (0s - 10s) :param sampling_mode: Sampling mode (``'SAMPLE'`` or ``'PEAK_DETECT'``) """ sample_rate = strict_range(sample_rate, (15260, 1e9)) acquisition_time = strict_discrete_range( acquisition_time, (1e-09, 68.711), 1e-09) # acquisition is also limited by buffer size, # which depends on sample rate as well as acquisition time pretrigger_time = strict_range(pretrigger_time, (0, 10)) try: pyvb.MsoSamplingMode(sampling_mode) except Exception: try: sampling_mode = pyvb.MsoSamplingMode[sampling_mode.upper()] except Exception: raise ValueError( "Sampling Mode may be 0, 1, 'SAMPLE' or 'PEAK_DETECT'") self.mso.configure_timing( sample_rate, acquisition_time, pretrigger_time, sampling_mode) def configure_immediate_trigger(self): """ Configures a trigger to immediately activate on the specified channels after the pretrigger time has expired. """ self.mso.configure_immediate_trigger() def configure_analog_edge_trigger(self, trigger_source, trigger_slope, trigger_level, trigger_hysteresis, trigger_instance): """ Configures a trigger to activate on the specified source when the analog edge reaches the specified levels. :param str trigger_source: Channel string :param trigger_slope: Trigger slope (``'RISING'``, ``'FALLING'`` or ``'EITHER'``) :type trigger_slope: int or str :param float trigger_level: Trigger level :param float trigger_hysteresis: Trigger hysteresis :param trigger_instance: Trigger instance :type trigger_instance: int or str """ trigger_source = self.validate_channel(trigger_source) try: pyvb.EdgeWithEither(trigger_slope) except Exception: try: trigger_slope = pyvb.EdgeWithEither[trigger_slope.upper()] except Exception: raise ValueError( "Trigger Slope may be 0, 1, 2, 'RISING'," + " 'FALLING' or 'EITHER'") trigger_instance = self.validate_trigger_instance(trigger_instance) self.mso.configure_analog_edge_trigger( trigger_source, trigger_slope, trigger_level, trigger_hysteresis, trigger_instance) def configure_analog_pulse_width_trigger(self, trigger_source, trigger_polarity, trigger_level, comparison_mode, lower_limit, upper_limit, trigger_instance): """ Configures a trigger to activate on the specified source when the analog edge reaches the specified levels within a specified window of time. :param str trigger_source: Channel string :param trigger_polarity: Trigger slope (``'POSITIVE'`` or ``'NEGATIVE'``) :type trigger_polarity: int or str :param float trigger_level: Trigger level :param comparison_mode: Mode of compariosn ( ``'GREATER_THAN_UPPER_LIMIT'``, ``'LESS_THAN_LOWER_LIMIT'``, ``'INSIDE_LIMITS'`` or ``'OUTSIDE_LIMITS'``) :type comparison_mode: int or str :param float lower_limit: Lower limit :param float upper_limit: Upper limit :param trigger_instance: Trigger instance :type trigger_instance: int or str """ trigger_source = self.validate_channel(trigger_source) try: pyvb.MsoTriggerPolarity(trigger_polarity) except Exception: try: trigger_polarity = pyvb.MsoTriggerPolarity[ trigger_polarity.upper()] except Exception: raise ValueError( "Comparison Mode may be 0, 1, 2, 3," + " 'GREATER_THAN_UPPER_LIMIT'," + " 'LESS_THAN_LOWER_LIMIT'," + " 'INSIDE_LIMITS' or 'OUTSIDE_LIMITS'") try: pyvb.MsoComparisonMode(comparison_mode) except Exception: try: comparison_mode = pyvb.MsoComparisonMode[ comparison_mode.upper()] except Exception: raise ValueError( "Trigger Polarity may be 0, 1," + " 'POSITIVE' or 'NEGATIVE'") trigger_instance = self.validate_trigger_instance(trigger_instance) self.mso.configure_analog_pulse_width_trigger( trigger_source, trigger_polarity, trigger_level, comparison_mode, lower_limit, upper_limit, trigger_instance) def configure_trigger_delay(self, trigger_delay): """ Configures the amount of time to wait after a trigger condition is met before triggering. :param float trigger_delay: Trigger delay (0s - 17.1799s) """ self.mso.configure_trigger_delay(trigger_delay) def query_analog_channel(self, channel): """ Indicates the vertical configuration of the specified channel. :return: Channel enabled, vertical range, vertical offset, probe attenuation, vertical coupling :rtype: (bool, float, float, enum, enum) """ channel = self.validate_channel(channel) return self.mso.query_analog_channel(channel) def query_enabled_analog_channels(self): """ Returns String of enabled analog channels. :return: Enabled analog channels :rtype: str """ return self.mso.query_enabled_analog_channels() def query_analog_channel_characteristics(self, channel): """ Indicates the properties that control the electrical characteristics of the specified channel. This method returns an error if too much power is applied to the channel. :return: Input impedance, bandwidth limit :rtype: (enum, float) """ return self.mso.query_analog_channel_characteristics(channel) def query_timing(self): """ Indicates the timing configuration of the MSO. Call directly before measurement to read the actual timing configuration and write it to the corresponding class variables. Necessary to interpret the measurement data, since it contains no time information. :return: Sample rate, acquisition time, pretrigger time, sampling mode :rtype: (float, float, float, enum) """ (self.sample_rate, self.acquisition_time, self.pretrigger_time, self.sampling_mode) = self.mso.query_timing() return (self.sample_rate, self.acquisition_time, self.pretrigger_time, self.sampling_mode) def query_trigger_type(self, trigger_instance): """ Indicates the trigger type of the specified instance. :param trigger_instance: Trigger instance (``'A'`` or ``'B'``) :return: Trigger type :rtype: str """ return self.mso.query_trigger_type() def query_analog_edge_trigger(self, trigger_instance): """ Indicates the analog edge trigger configuration of the specified instance. :return: Trigger source, trigger slope, trigger level, trigger hysteresis :rtype: (str, enum, float, float) """ trigger_instance = self.validate_trigger_instance(trigger_instance) return self.mso.query_analog_edge_trigger(trigger_instance) def query_trigger_delay(self): """ Indicates the trigger delay setting of the MSO. :return: Trigger delay :rtype: float """ return self.mso.query_trigger_delay() def query_analog_pulse_width_trigger(self, trigger_instance): """ Indicates the analog pulse width trigger configuration of the specified instance. :return: Trigger source, trigger polarity, trigger level, comparison mode, lower limit, upper limit :rtype: (str, enum, float, enum, float, float) """ trigger_instance = self.validate_trigger_instance(trigger_instance) return self.mso.query_analog_pulse_width_trigger(trigger_instance) def query_acquisition_status(self): """ Returns the status of a completed or ongoing acquisition. """ return self.mso.query_acquisition_status() # -------------------------- # Measurement Control # -------------------------- def run(self, autoTrigger=True): """ Transitions the acquisition from the Stopped state to the Running state. If the current state is Triggered, the acquisition is first transitioned to the Stopped state before transitioning to the Running state. This method returns an error if too much power is applied to any enabled channel. :param bool autoTrigger: Enable/Disable auto triggering """ self.mso.run(autoTrigger) def force_trigger(self): """ Causes a software-timed trigger to occur after the pretrigger time has expired. """ self.mso.force_trigger() def stop(self): """ Transitions the acquisition from either the Triggered or Running state to the Stopped state. """ self.mso.stop() def read_analog_digital_u64(self): """ Transfers data from the instrument as long as the acquisition state is Acquisition Complete. If the state is either Running or Triggered, this method will wait until the state transitions to Acquisition Complete. If the state is Stopped, this method returns an error. :return: Analog data out, analog data stride, analog t0, digital data out, digital timestamps out, digital t0, trigger timestamp, trigger reason :rtype: (list, int, pyvb.Timestamp, list, list, pyvb.Timestamp, pyvb.Timestamp, enum) """ return self.mso.read_analog_digital_u64() def read_analog_digital_dataframe(self): """ Transfers data from the instrument and returns a pandas dataframe of the analog measurement data, including time coordinates :return: Dataframe with time and measurement data :rtype: pd.DataFrame """ (analog_data_out, analog_data_stride # , analog_t0, digital_data_out, digital_timestamps_out, # digital_t0, trigger_timestamp, trigger_reason ) = self.read_analog_digital_u64()[0:2] number_of_samples = int(self.sample_rate * self.acquisition_time) + 1 if not number_of_samples == (len(analog_data_out) / analog_data_stride): # try updating timing parameters self.query_timing() number_of_samples = int(self.sample_rate * self.acquisition_time) + 1 if not number_of_samples == (len(analog_data_out) / analog_data_stride): raise ValueError( "Length of Analog Data does not match" + " Timing Parameters") pretrigger_samples = int(self.sample_rate * self.pretrigger_time) times = ( list(range(-pretrigger_samples, 0)) + list(range(0, number_of_samples - pretrigger_samples))) times = [list(map(lambda x: x*1/self.sample_rate, times))] np_array = np.array(analog_data_out) np_array = np.split(np_array, analog_data_stride) np_array = np.append(np.array(times), np_array, axis=0) np_array = np.transpose(np_array) return pd.DataFrame(data=np_array) def reset_instrument(self): """ Resets the session configuration to default values, and resets the device and driver software to a known state. """ self.mso.reset() class PowerSupply(VirtualBenchInstrument): """ Represents Power Supply (PS) Module of Virtual Bench device """ def __init__(self, virtualbench, reset, vb_name=''): """ Acquire PS module :param virtualbench: Instance of the VirtualBench class :type virtualbench: VirtualBench :param reset: Resets the instrument :type reset: bool """ super().__init__( virtualbench.acquire_power_supply, reset, 'ps', vb_name) self.ps = self._instrument_handle def validate_channel(self, channel, current=False, voltage=False): """ Check if channel string is valid and if output current/voltage are within the output ranges of the channel :param channel: Channel string (``"ps/+6V","ps/+25V","ps/-25V"``) :type channel: str :param current: Current output, defaults to False :type current: bool, optional :param voltage: Voltage output, defaults to False :type voltage: bool, optional :return: channel or channel, current & voltage :rtype: str or (str, float, float) """ if current is False and voltage is False: return strict_discrete_set( channel, ["ps/+6V", "ps/+25V", "ps/-25V"]) else: channel = strict_discrete_set( channel, ["ps/+6V", "ps/+25V", "ps/-25V"]) if channel == "ps/+6V": current_range = (0, 1) voltage_range = (0, 6) else: current_range = (0, 5) voltage_range = (0, 25) if channel == "ps/-25V": voltage_range = (0, -25) current = strict_discrete_range(current, current_range, 1e-3) voltage = strict_discrete_range(voltage, voltage_range, 1e-3) return (channel, current, voltage) def configure_voltage_output(self, channel, voltage_level, current_limit): ''' Configures a voltage output on the specified channel. This method should be called once for every channel you want to configure to output voltage. ''' (channel, current_limit, voltage_level) = self.validate_channel( channel, current_limit, voltage_level) self.ps.configure_voltage_output( channel, voltage_level, current_limit) def configure_current_output(self, channel, current_level, voltage_limit): ''' Configures a current output on the specified channel. This method should be called once for every channel you want to configure to output current. ''' (channel, current_level, voltage_limit) = self.validate_channel( channel, current_level, voltage_limit) self.ps.configure_current_output( channel, current_level, voltage_limit) def query_voltage_output(self, channel): ''' Indicates the voltage output settings on the specified channel. ''' channel = self.validate_channel(channel) return self.ps.query_voltage_output(channel) def query_current_output(self, channel): ''' Indicates the current output settings on the specified channel. ''' channel = self.validate_channel(channel) return self.ps.query_current_output(channel) @property def outputs_enabled(self): ''' Enables or disables all outputs on all channels of the instrument. :param bool enable_outputs: Enable/Disable outputs ''' return self.ps.query_outputs_enabled() @outputs_enabled.setter def outputs_enabled(self, enable_outputs): enable_outputs = strict_discrete_set( enable_outputs, [True, False]) log.info("%s Output %s." % (self.name, enable_outputs)) self.ps.enable_all_outputs(enable_outputs) @property def tracking(self): ''' Enables or disables tracking between the positive and negative 25V channels. If enabled, any configuration change on the positive 25V channel is mirrored to the negative 25V channel, and any writes to the negative 25V channel are ignored. :param bool enable_tracking: Enable/Disable tracking ''' return self.ps.query_tracking() @tracking.setter def tracking(self, enable_tracking): enable_tracking = strict_discrete_set( enable_tracking, [True, False]) self.ps.enable_tracking(enable_tracking) def read_output(self, channel): ''' Reads the voltage and current levels and outout mode of the specified channel. ''' channel = self.validate_channel(channel) return self.ps.read_output() def reset_instrument(self): ''' Resets the session configuration to default values, and resets the device and driver software to a known state. ''' self.ps.reset_instrument() PyMeasure-0.9.0/pymeasure/instruments/ni/__init__.py0000664000175000017500000000273614010037617023021 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # try: from .daqmx import DAQmx except OSError: # Error Logging is handled within package pass try: from .virtualbench import VirtualBench # direct access to armstrap/pyvirtualbench wrapper: # from .virtualbench import VirtualBench_Direct except ModuleNotFoundError: # Error Logging is handled within package pass PyMeasure-0.9.0/pymeasure/instruments/instrument.py0000664000175000017500000003461214010037617023062 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import re import numpy as np from pymeasure.adapters import FakeAdapter from pymeasure.adapters.visa import VISAAdapter log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Instrument(object): """ This provides the base class for all Instruments, which is independent of the particular Adapter used to connect for communication to the instrument. It provides basic SCPI commands by default, but can be toggled with :code:`includeSCPI`. :param adapter: An :class:`Adapter` object :param name: A string name :param includeSCPI: A boolean, which toggles the inclusion of standard SCPI commands """ # noinspection PyPep8Naming def __init__(self, adapter, name, includeSCPI=True, **kwargs): try: if isinstance(adapter, (int, str)): adapter = VISAAdapter(adapter, **kwargs) except ImportError: raise Exception("Invalid Adapter provided for Instrument since " "PyVISA is not present") self.name = name self.SCPI = includeSCPI self.adapter = adapter class Object(object): pass self.get = Object() # TODO: Determine case basis for the addition of these methods if includeSCPI: # Basic SCPI commands self.status = self.measurement("*STB?", """ Returns the status of the instrument """) self.complete = self.measurement("*OPC?", """ TODO: Add this doc """) self.isShutdown = False log.info("Initializing %s." % self.name) @property def id(self): """ Requests and returns the identification of the instrument. """ if self.SCPI: return self.adapter.ask("*IDN?").strip() else: return "Warning: Property not implemented." # Wrapper functions for the Adapter object def ask(self, command): """ Writes the command to the instrument through the adapter and returns the read response. :param command: command string to be sent to the instrument """ return self.adapter.ask(command) def write(self, command): """ Writes the command to the instrument through the adapter. :param command: command string to be sent to the instrument """ self.adapter.write(command) def read(self): """ Reads from the instrument through the adapter and returns the response. """ return self.adapter.read() def values(self, command, **kwargs): """ Reads a set of values from the instrument through the adapter, passing on any key-word arguments. """ return self.adapter.values(command, **kwargs) def binary_values(self, command, header_bytes=0, dtype=np.float32): return self.adapter.binary_values(command, header_bytes, dtype) @staticmethod def control(get_command, set_command, docs, validator=lambda v, vs: v, values=(), map_values=False, get_process=lambda v: v, set_process=lambda v: v, check_set_errors=False, check_get_errors=False, **kwargs): """Returns a property for the class based on the supplied commands. This property may be set and read from the instrument. :param get_command: A string command that asks for the value :param set_command: A string command that writes the value :param docs: A docstring that will be included in the documentation :param validator: A function that takes both a value and a group of valid values and returns a valid value, while it otherwise raises an exception :param values: A list, tuple, range, or dictionary of valid values, that can be used as to map values if :code:`map_values` is True. :param map_values: A boolean flag that determines if the values should be interpreted as a map :param get_process: A function that take a value and allows processing before value mapping, returning the processed value :param set_process: A function that takes a value and allows processing before value mapping, returning the processed value :param check_set_errors: Toggles checking errors after setting :param check_get_errors: Toggles checking errors after getting """ if map_values and isinstance(values, dict): # Prepare the inverse values for performance inverse = {v: k for k, v in values.items()} def fget(self): vals = self.values(get_command, **kwargs) if check_get_errors: self.check_errors() if len(vals) == 1: value = get_process(vals[0]) if not map_values: return value elif isinstance(values, (list, tuple, range)): return values[int(value)] elif isinstance(values, dict): return inverse[value] else: raise ValueError( 'Values of type `{}` are not allowed ' 'for Instrument.control'.format(type(values)) ) else: vals = get_process(vals) return vals def fset(self, value): value = set_process(validator(value, values)) if not map_values: pass elif isinstance(values, (list, tuple, range)): value = values.index(value) elif isinstance(values, dict): value = values[value] else: raise ValueError( 'Values of type `{}` are not allowed ' 'for Instrument.control'.format(type(values)) ) self.write(set_command % value) if check_set_errors: self.check_errors() # Add the specified document string to the getter fget.__doc__ = docs return property(fget, fset) @staticmethod def measurement(get_command, docs, values=(), map_values=None, get_process=lambda v: v, command_process=lambda c: c, check_get_errors=False, **kwargs): """ Returns a property for the class based on the supplied commands. This is a measurement quantity that may only be read from the instrument, not set. :param get_command: A string command that asks for the value :param docs: A docstring that will be included in the documentation :param values: A list, tuple, range, or dictionary of valid values, that can be used as to map values if :code:`map_values` is True. :param map_values: A boolean flag that determines if the values should be interpreted as a map :param get_process: A function that take a value and allows processing before value mapping, returning the processed value :param command_process: A function that take a command and allows processing before executing the command, for both getting and setting :param check_get_errors: Toggles checking errors after getting """ if map_values and isinstance(values, dict): # Prepare the inverse values for performance inverse = {v: k for k, v in values.items()} def fget(self): vals = self.values(command_process(get_command), **kwargs) if check_get_errors: self.check_errors() if len(vals) == 1: value = get_process(vals[0]) if not map_values: return value elif isinstance(values, (list, tuple, range)): return values[int(value)] elif isinstance(values, dict): return inverse[value] else: raise ValueError( 'Values of type `{}` are not allowed ' 'for Instrument.measurement'.format(type(values)) ) else: return get_process(vals) # Add the specified document string to the getter fget.__doc__ = docs return property(fget) @staticmethod def setting(set_command, docs, validator=lambda x, y: x, values=(), map_values=False, set_process=lambda v: v, check_set_errors=False, **kwargs): """Returns a property for the class based on the supplied commands. This property may be set, but raises an exception when being read from the instrument. :param set_command: A string command that writes the value :param docs: A docstring that will be included in the documentation :param validator: A function that takes both a value and a group of valid values and returns a valid value, while it otherwise raises an exception :param values: A list, tuple, range, or dictionary of valid values, that can be used as to map values if :code:`map_values` is True. :param map_values: A boolean flag that determines if the values should be interpreted as a map :param set_process: A function that takes a value and allows processing before value mapping, returning the processed value :param check_set_errors: Toggles checking errors after setting """ if map_values and isinstance(values, dict): # Prepare the inverse values for performance inverse = {v: k for k, v in values.items()} def fget(self): raise LookupError("Instrument.setting properties can not be read.") def fset(self, value): value = set_process(validator(value, values)) if not map_values: pass elif isinstance(values, (list, tuple, range)): value = values.index(value) elif isinstance(values, dict): value = values[value] else: raise ValueError( 'Values of type `{}` are not allowed ' 'for Instrument.control'.format(type(values)) ) self.write(set_command % value) if check_set_errors: self.check_errors() # Add the specified document string to the getter fget.__doc__ = docs return property(fget, fset) # TODO: Determine case basis for the addition of this method def clear(self): """ Clears the instrument status byte """ self.write("*CLS") # TODO: Determine case basis for the addition of this method def reset(self): """ Resets the instrument. """ self.write("*RST") def shutdown(self): """Brings the instrument to a safe and stable state""" self.isShutdown = True log.info("Shutting down %s" % self.name) def check_errors(self): """Return any accumulated errors. Must be reimplemented by subclasses. """ pass class FakeInstrument(Instrument): """ Provides a fake implementation of the Instrument class for testing purposes. """ def __init__(self, adapter=None, name=None, includeSCPI=False, **kwargs): super().__init__( FakeAdapter(**kwargs), name or "Fake Instrument", includeSCPI=includeSCPI, **kwargs ) @staticmethod def control(get_command, set_command, docs, validator=lambda v, vs: v, values=(), map_values=False, get_process=lambda v: v, set_process=lambda v: v, check_set_errors=False, check_get_errors=False, **kwargs): """Fake Instrument.control. Strip commands and only store and return values indicated by format strings to mimic many simple commands. This is analogous how the tests in test_instrument are handled. """ # Regex search to find first format specifier in the command fmt_spec_pattern = r'(%[\w.#-+ *]*[diouxXeEfFgGcrsa%])' match = re.search(fmt_spec_pattern, set_command) if match: format_specifier = match.group(0) else: format_specifier = '' # To preserve as much functionality as possible, call the real # control method with modified get_command and set_command. return Instrument.control(get_command="", set_command=format_specifier, docs=docs, validator=validator, values=values, map_values=map_values, get_process=get_process, set_process=set_process, check_set_errors=check_set_errors, check_get_errors=check_get_errors, **kwargs) PyMeasure-0.9.0/pymeasure/instruments/srs/0000775000175000017500000000000014010046235021075 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/srs/sr860.py0000664000175000017500000005502214010037617022341 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments.validators import strict_discrete_set, \ truncated_discrete_set, truncated_range from pymeasure.instruments import Instrument class SR860(Instrument): SENSITIVITIES = [ 1e-9, 2e-9, 5e-9, 10e-9, 20e-9, 50e-9, 100e-9, 200e-9, 500e-9, 1e-6, 2e-6, 5e-6, 10e-6, 20e-6, 50e-6, 100e-6, 200e-6, 500e-6, 1e-3, 2e-3, 5e-3, 10e-3, 20e-3, 50e-3, 100e-3, 200e-3, 500e-3, 1 ] TIME_CONSTANTS = [ 1e-6, 3e-6, 10e-6, 30e-6, 100e-6, 300e-6, 1e-3, 3e-3, 10e-3, 30e-3, 100e-3, 300e-3, 1, 3, 10, 30, 100, 300, 1e3, 3e3, 10e3, 30e3 ] ON_OFF_VALUES = ['0', '1'] SCREEN_LAYOUT_VALUES = ['0', '1', '2', '3', '4', '5'] EXPANSION_VALUES = ['0', '1', '2,'] CHANNEL_VALUES = ['OCH1', 'OCH2'] OUTPUT_VALUES = ['XY', 'RTH'] INPUT_TIMEBASE = ['AUTO', 'IN'] INPUT_DCMODE = ['COM', 'DIF', 'common', 'difference'] INPUT_REFERENCESOURCE = ['INT', 'EXT', 'DUAL', 'CHOP'] INPUT_REFERENCETRIGGERMODE = ['SIN', 'POS', 'NEG', 'POSTTL', 'NEGTTL'] INPUT_REFERENCEEXTERNALINPUT = ['50OHMS', '1MEG'] INPUT_SIGNAL_INPUT = ['VOLT', 'CURR', 'voltage', 'current'] INPUT_VOLTAGE_MODE = ['A', 'A-B'] INPUT_COUPLING = ['AC', 'DC'] INPUT_SHIELDS = ['Float', 'Ground'] INPUT_RANGE = ['1V', '300M', '100M', '30M', '10M'] INPUT_GAIN = ['1MEG', '100MEG'] INPUT_FILTER = ['Off', 'On'] LIST_PARAMETER = ['i=', '0=Xoutput', '1=Youtput', '2=Routput', 'Thetaoutput', '4=Aux IN1', '5=Aux IN2', '6=Aux IN3', '7=Aux IN4', '8=Xnoise', '9=Ynoise', '10=AUXOut1', '11=AuxOut2', '12=Phase', '13=Sine Out amplitude', '14=DCLevel', '15I=nt.referenceFreq', '16=Ext.referenceFreq'] LIST_HORIZONTAL_TIME_DIV = ['0=0.5s', '1=1s', '2=2s', '3=5s', '4=10s', '5=30s', '6=1min', '7=2min', '8=5min', '9=10min', '10=30min', '11=1hour', '12=2hour', '13=6hour', '14=12hour', '15=1day', '16=2days'] x = Instrument.measurement("OUTP? 0", """ Reads the X value in Volts """ ) y = Instrument.measurement("OUTP? 1", """ Reads the Y value in Volts """ ) magnitude = Instrument.measurement("OUTP? 2", """ Reads the magnitude in Volts. """ ) theta = Instrument.measurement("OUTP? 3", """ Reads the theta value in degrees. """ ) phase = Instrument.control( "PHAS?", "PHAS %0.7f", """ A floating point property that represents the lock-in phase in degrees. This property can be set. """, validator=truncated_range, values=[-360, 360] ) frequency = Instrument.control( "FREQ?", "FREQ %0.6e", """ A floating point property that represents the lock-in frequency in Hz. This property can be set. """, validator=truncated_range, values=[0.001, 500000] ) internalfrequency = Instrument.control( "FREQINT?", "FREQINT %0.6e", """A floating property that represents the internal lock-in frequency in Hz This property can be set.""", validator=truncated_range, values=[0.001, 500000] ) harmonic = Instrument.control( "HARM?", "Harm %d", """An integer property that controls the harmonic that is measured. Allowed values are 1 to 99. Can be set.""", validator=strict_discrete_set, values=range(1, 99) ) harmonicdual = Instrument.control( "HARMDUAL?", "HARMDUAL %d", """An integer property that controls the harmonic in dual reference mode that is measured. Allowed values are 1 to 99. Can be set.""", validator=strict_discrete_set, values=range(1, 99) ) sine_voltage = Instrument.control( "SLVL?", "SLVL %0.9e", """A floating point property that represents the reference sine-wave voltage in Volts. This property can be set.""", validator=truncated_range, values=[1e-9, 2] ) timebase = Instrument.control( "TBMODE?", "TBMODE %d", """Sets the external 10 MHZ timebase to auto(i=0) or internal(i=1).""", validator=strict_discrete_set, values=[0, 1], map_values=True ) dcmode = Instrument.control( "REFM?", "REFM %d", """A string property that represents the sine out dc mode. This property can be set. Allowed values are:{}""".format(INPUT_DCMODE), validator=strict_discrete_set, values=INPUT_DCMODE, map_values=True ) reference_source = Instrument.control( "RSRC?", "RSRC %d", """A string property that represents the reference source. This property can be set. Allowed values are:{}""".format(INPUT_REFERENCESOURCE), validator=strict_discrete_set, values=INPUT_REFERENCESOURCE, map_values=True ) reference_triggermode = Instrument.control( "RTRG?", "RTRG %d", """A string property that represents the external reference trigger mode. This property can be set. Allowed values are:{}""".format(INPUT_REFERENCETRIGGERMODE), validator=strict_discrete_set, values=INPUT_REFERENCETRIGGERMODE, map_values=True ) reference_externalinput = Instrument.control( "REFZ?", "REFZ&d", """A string property that represents the external reference input. This property can be set. Allowed values are:{}""".format(INPUT_REFERENCEEXTERNALINPUT), validator=strict_discrete_set, values=INPUT_REFERENCEEXTERNALINPUT, map_values=True ) input_signal = Instrument.control( "IVMD?", "IVMD %d", """A string property that represents the signal input. This property can be set. Allowed values are:{}""".format(INPUT_SIGNAL_INPUT), validator=strict_discrete_set, values=INPUT_SIGNAL_INPUT, map_values=True ) input_voltage_mode = Instrument.control( "ISRC?", "ISRC %d", """A string property that represents the voltage input mode. This property can be set. Allowed values are:{}""".format(INPUT_VOLTAGE_MODE), validator=strict_discrete_set, values=INPUT_VOLTAGE_MODE, map_values=True ) input_coupling = Instrument.control( "ICPL?", "ICPL %d", """A string property that represents the input coupling. This property can be set. Allowed values are:{}""".format(INPUT_COUPLING), validator=strict_discrete_set, values=INPUT_COUPLING, map_values=True ) input_shields = Instrument.control( "IGND?", "IGND %d", """A string property that represents the input shield grounding. This property can be set. Allowed values are:{}""".format(INPUT_SHIELDS), validator=strict_discrete_set, values=INPUT_SHIELDS, map_values=True ) input_range = Instrument.control( "IRNG?", "IRNG %d", """A string property that represents the input range. This property can be set. Allowed values are:{}""".format(INPUT_RANGE), validator=strict_discrete_set, values=INPUT_RANGE, map_values=True ) input_current_gain = Instrument.control( "ICUR?", "ICUR %d", """A string property that represents the current input gain. This property can be set. Allowed values are:{}""".format(INPUT_GAIN), validator=strict_discrete_set, values=INPUT_GAIN, map_values=True ) sensitvity = Instrument.control( "SCAL?", "SCAL %d", """ A floating point property that controls the sensitivity in Volts, which can take discrete values from 2 nV to 1 V. Values are truncated to the next highest level if they are not exact. """, validator=truncated_discrete_set, values=SENSITIVITIES, map_values=True ) time_constant = Instrument.control( "OFLT?", "OFLT %d", """ A floating point property that controls the time constant in seconds, which can take discrete values from 10 microseconds to 30,000 seconds. Values are truncated to the next highest level if they are not exact. """, validator=truncated_discrete_set, values=TIME_CONSTANTS, map_values=True ) filter_slope = Instrument.control( "OFSL?", "OFSL %d", """A integer property that sets the filter slope to 6 dB/oct(i=0), 12 DB/oct(i=1), 18 dB/oct(i=2), 24 dB/oct(i=3).""", validator=strict_discrete_set, values=range(0, 3) ) filer_synchronous = Instrument.control( "SYNC?", "SYNC %d", """A string property that represents the synchronous filter. This property can be set. Allowed values are:{}""".format(INPUT_FILTER), validator=strict_discrete_set, values=INPUT_FILTER, map_values=True ) filter_advanced = Instrument.control( "ADVFILT?", "ADVFIL %d", """A string property that represents the advanced filter. This property can be set. Allowed values are:{}""".format(INPUT_FILTER), validator=strict_discrete_set, values=INPUT_FILTER, map_values=True ) frequencypreset1 = Instrument.control( "PSTF? 0", "PSTF 0, %0.6e", """A floating point property that represents the preset frequency for the F1 preset button. This property can be set.""", validator=truncated_range, values=[0.001, 500000] ) frequencypreset2 = Instrument.control( "PSTF? 1", "PSTF 1, %0.6e", """A floating point property that represents the preset frequency for the F2 preset button. This property can be set.""", validator=truncated_range, values=[0.001, 500000] ) frequencypreset3 = Instrument.control( "PSTF? 2", "PSTF2, %0.6e", """A floating point property that represents the preset frequency for the F3 preset button. This property can be set.""", validator=truncated_range, values=[0.001, 500000] ) frequencypreset4 = Instrument.control( "PSTF? 3", "PSTF3, %0.6e", """A floating point property that represents the preset frequency for the F4 preset button. This property can be set.""", validator=truncated_range, values=[0.001, 500000] ) sine_amplitudepreset1 = Instrument.control( "PSTA? 0", "PSTA0, %0.9e", """A floating point property that represents the preset sine out amplitude, for the A1 preset button. This property can be set.""", validator=truncated_range, values=[1e-9, 2] ) sine_amplitudepreset2 = Instrument.control( "PSTA? 1", "PSTA1, %0.9e", """A floating point property that represents the preset sine out amplitude, for the A2 preset button. This property can be set.""", validator=truncated_range, values=[1e-9, 2] ) sine_amplitudepreset3 = Instrument.control( "PSTA? 2", "PSTA2, %0.9e", """A floating point property that represents the preset sine out amplitude, for the A3 preset button. This property can be set.""", validator=truncated_range, values=[1e-9, 2] ) sine_amplitudepreset4 = Instrument.control( "PSTA? 3", "PSTA 3, %0.9e", """A floating point property that represents the preset sine out amplitude, for the A3 preset button. This property can be set.""", validator=truncated_range, values=[1e-9, 2] ) sine_dclevelpreset1 = Instrument.control( "PSTL? 0", "PSTL 0, %0.3e", """A floating point property that represents the preset sine out dc level for the L1 button. This property can be set.""", validator=truncated_range, values=[-5, 5] ) sine_dclevelpreset2 = Instrument.control( "PSTL? 1", "PSTL 1, %0.3e", """A floating point property that represents the preset sine out dc level for the L2 button. This property can be set.""", validator=truncated_range, values=[-5, 5] ) sine_dclevelpreset3 = Instrument.control( "PSTL? 2", "PSTL 2, %0.3e", """A floating point property that represents the preset sine out dc level for the L3 button. This property can be set.""", validator=truncated_range, values=[-5, 5] ) sine_dclevelpreset4 = Instrument.control( "PSTL? 3", "PSTL3, %0.3e", """A floating point property that represents the preset sine out dc level for the L4 button. This property can be set.""", validator=truncated_range, values=[-5, 5] ) aux_out_1 = Instrument.control( "AUXV? 0", "AUXV 1, %f", """ A floating point property that controls the output of Aux output 1 in Volts, taking values between -10.5 V and +10.5 V. This property can be set.""", validator=truncated_range, values=[-10.5, 10.5] ) # For consistency with other lock-in instrument classes dac1 = aux_out_1 aux_out_2 = Instrument.control( "AUXV? 1", "AUXV 2, %f", """ A floating point property that controls the output of Aux output 2 in Volts, taking values between -10.5 V and +10.5 V. This property can be set.""", validator=truncated_range, values=[-10.5, 10.5] ) # For consistency with other lock-in instrument classes dac2 = aux_out_2 aux_out_3 = Instrument.control( "AUXV? 2", "AUXV 3, %f", """ A floating point property that controls the output of Aux output 3 in Volts, taking values between -10.5 V and +10.5 V. This property can be set.""", validator=truncated_range, values=[-10.5, 10.5] ) # For consistency with other lock-in instrument classes dac3 = aux_out_3 aux_out_4 = Instrument.control( "AUXV? 3", "AUXV 4, %f", """ A floating point property that controls the output of Aux output 4 in Volts, taking values between -10.5 V and +10.5 V. This property can be set.""", validator=truncated_range, values=[-10.5, 10.5] ) # For consistency with other lock-in instrument classes dac4 = aux_out_4 aux_in_1 = Instrument.measurement( "OAUX? 0", """ Reads the Aux input 1 value in Volts with 1/3 mV resolution. """ ) # For consistency with other lock-in instrument classes adc1 = aux_in_1 aux_in_2 = Instrument.measurement( "OAUX? 1", """ Reads the Aux input 2 value in Volts with 1/3 mV resolution. """ ) # For consistency with other lock-in instrument classes adc2 = aux_in_2 aux_in_3 = Instrument.measurement( "OAUX? 2", """ Reads the Aux input 3 value in Volts with 1/3 mV resolution. """ ) # For consistency with other lock-in instrument classes adc3 = aux_in_3 aux_in_4 = Instrument.measurement( "OAUX? 3", """ Reads the Aux input 4 value in Volts with 1/3 mV resolution. """ ) # For consistency with other lock-in instrument classes adc4 = aux_in_4 def snap(self, val1="X", val2="Y", val3=None): """retrieve 2 or 3 parameters at once parameters can be chosen by index, or enumeration as follows: j enumeration parameter j enumeration parameter 0 X X output 9 YNOise Ynoise 1 Y Youtput 10 OUT1 Aux Out1 2 R R output 11 OUT2 Aux Out2 3 THeta θ output 12 PHAse Reference Phase 4 IN1 Aux In1 13 SAMp Sine Out Amplitude 5 IN2 Aux In2 14 LEVel DC Level 6 IN3 Aux In3 15 FInt Int. Ref. Frequency 7 IN4 Aux In4 16 FExt Ext. Ref. Frequency 8 XNOise Xnoise :param val1: parameter enumeration/index :param val2: parameter enumeration/index :param val3: parameter enumeration/index (optional) Defaults: val1 = "X" val2 = "Y" val3 = None """ if val3 is None: return self.adapter.values( command=f"SNAP? {val1}, {val2}", separator=",", cast=float, ) else: return self.adapter.values( command=f"SNAP? {val1}, {val2}, {val3}", separator=",", cast=float, ) gettimebase = Instrument.measurement( "TBSTAT?", """Returns the current 10 MHz timebase source.""" ) extfreqency = Instrument.measurement( "FREQEXT?", """Returns the external frequency in Hz.""" ) detectedfrequency = Instrument.measurement( "FREQDET?", """Returns the actual detected frequency in HZ.""" ) get_signal_strength_indicator = Instrument.measurement( "ILVL?", """Returns the signal strength indicator.""" ) get_noise_bandwidth = Instrument.measurement( "ENBW?", """Returns the equivalent noise bandwidth, in hertz.""" ) # Display Commands front_panel = Instrument.control( "DBLK?", "DBLK %i", """Turns the front panel blanking on(i=0) or off(i=1).""", validator=strict_discrete_set, values=ON_OFF_VALUES, map_values=True ) screen_layout = Instrument.control( "DLAY?", "DLAY %i", """A integer property that Sets the screen layout to trend(i=0), full strip chart history(i=1), half strip chart history(i=2), full FFT(i=3), half FFT(i=4) or big numerical(i=5).""", validator=strict_discrete_set, values=SCREEN_LAYOUT_VALUES, map_values=True ) def screenshot(self): """Take screenshot on device The DCAP command saves a screenshot to a USB memory stick. This command is the same as pressing the [Screen Shot] key. A USB memory stick must be present in the front panel USB port. """ self.write("DCAP") parameter_DAT1 = Instrument.control( "CDSP? 0", "CDSP 0, %i", """A integer property that assigns a parameter to data channel 1(green). This parameters can be set. Allowed values are:{}""".format(LIST_PARAMETER), validator=strict_discrete_set, values=range(0, 16) ) parameter_DAT2 = Instrument.control( "CDSP? 1", "CDSP 1, %i", """A integer property that assigns a parameter to data channel 2(blue). This parameters can be set. Allowed values are:{}""".format(LIST_PARAMETER), validator=strict_discrete_set, values=range(0, 16) ) parameter_DAT3 = Instrument.control( "CDSP? 2", "CDSP 2, %i", """A integer property that assigns a parameter to data channel 3(yellow). This parameters can be set. Allowed values are:{}""".format(LIST_PARAMETER), validator=strict_discrete_set, values=range(0, 16) ) parameter_DAT4 = Instrument.control( "CDSP? 3", "CDSP 3, %i", """A integer property that assigns a parameter to data channel 3(orange). This parameters can be set. Allowed values are:{}""".format(LIST_PARAMETER), validator=strict_discrete_set, values=range(0, 16) ) strip_chart_dat1 = Instrument.control( "CGRF? 0", "CGRF 0, %i", """A integer property that turns the strip chart graph of data channel 1 off(i=0) or on(i=1). """, validator=strict_discrete_set, values=ON_OFF_VALUES, map_values=True ) strip_chart_dat2 = Instrument.control( "CGRF? 1", "CGRF 1, %i", """A integer property that turns the strip chart graph of data channel 2 off(i=0) or on(i=1). """, validator=strict_discrete_set, values=ON_OFF_VALUES, map_values=True ) strip_chart_dat3 = Instrument.control( "CGRF? 2", "CGRF 2, %i", """A integer property that turns the strip chart graph of data channel 1 off(i=0) or on(i=1). """, validator=strict_discrete_set, values=ON_OFF_VALUES, map_values=True ) strip_chart_dat4 = Instrument.control( "CGRF? 3", "CGRF 3, %i", """A integer property that turns the strip chart graph of data channel 4 off(i=0) or on(i=1). """, validator=strict_discrete_set, values=ON_OFF_VALUES, map_values=True ) # Strip Chart commands horizontal_time_div = Instrument.control( "GSPD?", "GSDP %i", """A integer property that sets the horizontal time/div according to the following table:{}""".format( LIST_HORIZONTAL_TIME_DIV), validator=strict_discrete_set, values=range(0, 16) ) def __init__(self, resourceName, **kwargs): super(SR860, self).__init__( resourceName, "Stanford Research Systems SR860 Lock-in amplifier", **kwargs ) PyMeasure-0.9.0/pymeasure/instruments/srs/sr830.py0000664000175000017500000004032514010037617022336 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument, discreteTruncate from pymeasure.instruments.validators import strict_discrete_set, \ truncated_discrete_set, truncated_range import numpy as np import time import re class SR830(Instrument): SAMPLE_FREQUENCIES = [ 62.5e-3, 125e-3, 250e-3, 500e-3, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 ] SENSITIVITIES = [ 2e-9, 5e-9, 10e-9, 20e-9, 50e-9, 100e-9, 200e-9, 500e-9, 1e-6, 2e-6, 5e-6, 10e-6, 20e-6, 50e-6, 100e-6, 200e-6, 500e-6, 1e-3, 2e-3, 5e-3, 10e-3, 20e-3, 50e-3, 100e-3, 200e-3, 500e-3, 1 ] TIME_CONSTANTS = [ 10e-6, 30e-6, 100e-6, 300e-6, 1e-3, 3e-3, 10e-3, 30e-3, 100e-3, 300e-3, 1, 3, 10, 30, 100, 300, 1e3, 3e3, 10e3, 30e3 ] FILTER_SLOPES = [6, 12, 18, 24] EXPANSION_VALUES = [1, 10, 100] RESERVE_VALUES = ['High Reserve', 'Normal', 'Low Noise'] CHANNELS = ['X', 'Y', 'R'] INPUT_CONFIGS = ['A', 'A - B', 'I (1 MOhm)', 'I (100 MOhm)'] INPUT_GROUNDINGS = ['Float', 'Ground'] INPUT_COUPLINGS = ['AC', 'DC'] INPUT_NOTCH_CONFIGS = ['None', 'Line', '2 x Line', 'Both'] REFERENCE_SOURCES = ['External', 'Internal'] sine_voltage = Instrument.control( "SLVL?", "SLVL%0.3f", """ A floating point property that represents the reference sine-wave voltage in Volts. This property can be set. """, validator=truncated_range, values=[0.004, 5.0] ) frequency = Instrument.control( "FREQ?", "FREQ%0.5e", """ A floating point property that represents the lock-in frequency in Hz. This property can be set. """, validator=truncated_range, values=[0.001, 102000] ) phase = Instrument.control( "PHAS?", "PHAS%0.2f", """ A floating point property that represents the lock-in phase in degrees. This property can be set. """, validator=truncated_range, values=[-360, 729.99] ) x = Instrument.measurement("OUTP?1", """ Reads the X value in Volts. """ ) y = Instrument.measurement("OUTP?2", """ Reads the Y value in Volts. """ ) magnitude = Instrument.measurement("OUTP?3", """ Reads the magnitude in Volts. """ ) theta = Instrument.measurement("OUTP?4", """ Reads the theta value in degrees. """ ) channel1 = Instrument.control( "DDEF?1;", "DDEF1,%d,0", """ A string property that represents the type of Channel 1, taking the values X, R, X Noise, Aux In 1, or Aux In 2. This property can be set.""", validator=strict_discrete_set, values=['X', 'R', 'X Noise', 'Aux In 1', 'Aux In 2'], map_values=True ) channel2 = Instrument.control( "DDEF?2;", "DDEF2,%d,0", """ A string property that represents the type of Channel 2, taking the values Y, Theta, Y Noise, Aux In 3, or Aux In 4. This property can be set.""", validator=strict_discrete_set, values=['Y', 'Theta', 'Y Noise', 'Aux In 3', 'Aux In 4'], map_values=True ) sensitivity = Instrument.control( "SENS?", "SENS%d", """ A floating point property that controls the sensitivity in Volts, which can take discrete values from 2 nV to 1 V. Values are truncated to the next highest level if they are not exact. """, validator=truncated_discrete_set, values=SENSITIVITIES, map_values=True ) time_constant = Instrument.control( "OFLT?", "OFLT%d", """ A floating point property that controls the time constant in seconds, which can take discrete values from 10 microseconds to 30,000 seconds. Values are truncated to the next highest level if they are not exact. """, validator=truncated_discrete_set, values=TIME_CONSTANTS, map_values=True ) filter_slope = Instrument.control( "OFSL?", "OFSL%d", """ An integer property that controls the filter slope, which can take on the values 6, 12, 18, and 24 dB/octave. Values are truncated to the next highest level if they are not exact. """, validator=truncated_discrete_set, values=FILTER_SLOPES, map_values=True ) harmonic = Instrument.control( "HARM?", "HARM%d", """ An integer property that controls the harmonic that is measured. Allowed values are 1 to 19999. Can be set. """, validator=strict_discrete_set, values=range(1, 19999), ) input_config = Instrument.control( "ISRC?", "ISRC %d", """ An string property that controls the input configuration. Allowed values are: {}""".format(INPUT_CONFIGS), validator=strict_discrete_set, values=INPUT_CONFIGS, map_values=True ) input_grounding = Instrument.control( "IGND?", "IGND %d", """ An string property that controls the input shield grounding. Allowed values are: {}""".format(INPUT_GROUNDINGS), validator=strict_discrete_set, values=INPUT_GROUNDINGS, map_values=True ) input_coupling = Instrument.control( "ICPL?", "ICPL %d", """ An string property that controls the input coupling. Allowed values are: {}""".format(INPUT_COUPLINGS), validator=strict_discrete_set, values=INPUT_COUPLINGS, map_values=True ) input_notch_config = Instrument.control( "ILIN?", "ILIN %d", """ An string property that controls the input line notch filter status. Allowed values are: {}""".format(INPUT_NOTCH_CONFIGS), validator=strict_discrete_set, values=INPUT_NOTCH_CONFIGS, map_values=True ) reference_source = Instrument.control( "FMOD?", "FMOD %d", """ An string property that controls the reference source. Allowed values are: {}""".format(REFERENCE_SOURCES), validator=strict_discrete_set, values=REFERENCE_SOURCES, map_values=True ) aux_out_1 = Instrument.control( "AUXV?1;", "AUXV1,%f;", """ A floating point property that controls the output of Aux output 1 in Volts, taking values between -10.5 V and +10.5 V. This property can be set.""", validator=truncated_range, values=[-10.5, 10.5] ) # For consistency with other lock-in instrument classes dac1 = aux_out_1 aux_out_2 = Instrument.control( "AUXV?2;", "AUXV2,%f;", """ A floating point property that controls the output of Aux output 2 in Volts, taking values between -10.5 V and +10.5 V. This property can be set.""", validator=truncated_range, values=[-10.5, 10.5] ) # For consistency with other lock-in instrument classes dac2 = aux_out_2 aux_out_3 = Instrument.control( "AUXV?3;", "AUXV3,%f;", """ A floating point property that controls the output of Aux output 3 in Volts, taking values between -10.5 V and +10.5 V. This property can be set.""", validator=truncated_range, values=[-10.5, 10.5] ) # For consistency with other lock-in instrument classes dac3 = aux_out_3 aux_out_4 = Instrument.control( "AUXV?4;", "AUXV4,%f;", """ A floating point property that controls the output of Aux output 4 in Volts, taking values between -10.5 V and +10.5 V. This property can be set.""", validator=truncated_range, values=[-10.5, 10.5] ) # For consistency with other lock-in instrument classes dac4 = aux_out_4 aux_in_1 = Instrument.measurement( "OAUX?1;", """ Reads the Aux input 1 value in Volts with 1/3 mV resolution. """ ) # For consistency with other lock-in instrument classes adc1 = aux_in_1 aux_in_2 = Instrument.measurement( "OAUX?2;", """ Reads the Aux input 2 value in Volts with 1/3 mV resolution. """ ) # For consistency with other lock-in instrument classes adc2 = aux_in_2 aux_in_3 = Instrument.measurement( "OAUX?3;", """ Reads the Aux input 3 value in Volts with 1/3 mV resolution. """ ) # For consistency with other lock-in instrument classes adc3 = aux_in_3 aux_in_4 = Instrument.measurement( "OAUX?4;", """ Reads the Aux input 4 value in Volts with 1/3 mV resolution. """ ) # For consistency with other lock-in instrument classes adc4 = aux_in_4 def __init__(self, resourceName, **kwargs): super(SR830, self).__init__( resourceName, "Stanford Research Systems SR830 Lock-in amplifier", **kwargs ) def auto_gain(self): self.write("AGAN") def auto_reserve(self): self.write("ARSV") def auto_phase(self): self.write("APHS") def auto_offset(self, channel): """ Offsets the channel (X, Y, or R) to zero """ if channel not in self.CHANNELS: raise ValueError('SR830 channel is invalid') channel = self.CHANNELS.index(channel) + 1 self.write("AOFF %d" % channel) def get_scaling(self, channel): """ Returns the offset precent and the exapnsion term that are used to scale the channel in question """ if channel not in self.CHANNELS: raise ValueError('SR830 channel is invalid') channel = self.CHANNELS.index(channel) + 1 offset, expand = self.ask("OEXP? %d" % channel).split(',') return float(offset), self.EXPANSION_VALUES[int(expand)] def set_scaling(self, channel, precent, expand=0): """ Sets the offset of a channel (X=1, Y=2, R=3) to a certain precent (-105% to 105%) of the signal, with an optional expansion term (0, 10=1, 100=2) """ if channel not in self.CHANNELS: raise ValueError('SR830 channel is invalid') channel = self.CHANNELS.index(channel) + 1 expand = discreteTruncate(expand, self.EXPANSION_VALUES) self.write("OEXP %i,%.2f,%i" % (channel, precent, expand)) def output_conversion(self, channel): """ Returns a function that can be used to determine the signal from the channel output (X, Y, or R) """ offset, expand = self.get_scaling(channel) sensitivity = self.sensitivity return lambda x: (x/(10.*expand) + offset) * sensitivity @property def sample_frequency(self): """ Gets the sample frequency in Hz """ index = int(self.ask("SRAT?")) if index == 14: return None # Trigger else: return SR830.SAMPLE_FREQUENCIES[index] @sample_frequency.setter def sample_frequency(self, frequency): """Sets the sample frequency in Hz (None is Trigger)""" assert type(frequency) in [float, int, type(None)] if frequency is None: index = 14 # Trigger else: frequency = discreteTruncate(frequency, SR830.SAMPLE_FREQUENCIES) index = SR830.SAMPLE_FREQUENCIES.index(frequency) self.write("SRAT%f" % index) def aquireOnTrigger(self, enable=True): self.write("TSTR%d" % enable) @property def reserve(self): return SR830.RESERVE_VALUES[int(self.ask("RMOD?"))] @reserve.setter def reserve(self, reserve): if reserve not in SR830.RESERVE_VALUES: index = 1 else: index = SR830.RESERVE_VALUES.index(reserve) self.write("RMOD%d" % index) def is_out_of_range(self): """ Returns True if the magnitude is out of range """ return int(self.ask("LIAS?2")) == 1 def quick_range(self): """ While the magnitude is out of range, increase the sensitivity by one setting """ self.write('LIAE 2,1') while self.is_out_of_range(): self.write("SENS%d" % (int(self.ask("SENS?"))+1)) time.sleep(5.0*self.time_constant) self.write("*CLS") # Set the range as low as possible newsensitivity = 1.15*abs(self.magnitude) if self.input_config in('I (1 MOhm)','I (100 MOhm)'): newsensitivity = newsensitivity*1e6 self.sensitivity = newsensitivity @property def buffer_count(self): query = self.ask("SPTS?") if query.count("\n") > 1: return int(re.match(r"\d+\n$", query, re.MULTILINE).group(0)) else: return int(query) def fill_buffer(self, count, has_aborted=lambda: False, delay=0.001): ch1 = np.empty(count, np.float32) ch2 = np.empty(count, np.float32) currentCount = self.buffer_count index = 0 while currentCount < count: if currentCount > index: ch1[index:currentCount] = self.buffer_data(1, index, currentCount) ch2[index:currentCount] = self.buffer_data(2, index, currentCount) index = currentCount time.sleep(delay) currentCount = self.buffer_count if has_aborted(): self.pause_buffer() return ch1, ch2 self.pauseBuffer() ch1[index:count+1] = self.buffer_data(1, index, count) ch2[index:count+1] = self.buffer_data(2, index, count) return ch1, ch2 def buffer_measure(self, count, stopRequest=None, delay=1e-3): self.write("FAST0;STRD") ch1 = np.empty(count, np.float64) ch2 = np.empty(count, np.float64) currentCount = self.buffer_count index = 0 while currentCount < count: if currentCount > index: ch1[index:currentCount] = self.buffer_data(1, index, currentCount) ch2[index:currentCount] = self.buffer_data(2, index, currentCount) index = currentCount time.sleep(delay) currentCount = self.buffer_count if stopRequest is not None and stopRequest.isSet(): self.pauseBuffer() return (0, 0, 0, 0) self.pauseBuffer() ch1[index:count] = self.buffer_data(1, index, count) ch2[index:count] = self.buffer_data(2, index, count) return (ch1.mean(), ch1.std(), ch2.mean(), ch2.std()) def pause_buffer(self): self.write("PAUS") def start_buffer(self, fast=False): if fast: self.write("FAST2;STRD") else: self.write("FAST0;STRD") def wait_for_buffer(self, count, has_aborted=lambda: False, timeout=60, timestep=0.01): """ Wait for the buffer to fill a certain count """ i = 0 while not self.buffer_count >= count and i < (timeout / timestep): time.sleep(timestep) i += 1 if has_aborted(): return False self.pauseBuffer() def get_buffer(self, channel=1, start=0, end=None): """ Aquires the 32 bit floating point data through binary transfer """ if end is None: end = self.buffer_count return self.binary_values("TRCB?%d,%d,%d" % ( channel, start, end-start)) def reset_buffer(self): self.write("REST") def trigger(self): self.write("TRIG") PyMeasure-0.9.0/pymeasure/instruments/srs/sg380.py0000664000175000017500000000736214010037617022327 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range class SG380(Instrument): MOD_TYPES_VALUES = ['AM', 'FM', 'PM', 'SWEEP', 'PULSE', 'BLANK', 'IQ'] MOD_FUNCTIONS = ['SINE', 'RAMP', 'TRIANGLE', 'SQUARE', 'NOISE', 'EXTERNAL'] MIN_RF = 0.0 MAX_RF = 4E9 # TODO: restrict modulation depth to allowed values (depending on # frequency) fm_dev = Instrument.control( "FDEV?", "FDEV%.6f", """ A floating point property that represents the modulation frequency deviation in Hz. This property can be set. """ ) rate = Instrument.control( "RATE?", "RATE%.6f", """ A floating point property that represents the modulation rate in Hz. This property can be set. """ ) def __init__(self, resourceName, **kwargs): super(SG380, self).__init__( resourceName, "Stanford Research Systems SG380 RF Signal Generator", **kwargs ) @property def has_doubler(self): """Gets the modulation type""" return bool(self.ask("OPTN? 2")) @property def has_IQ(self): """Gets the modulation type""" return bool(self.ask("OPTN? 3")) @property def frequency(self): """Gets RF frequency""" return float(self.ask("FREQ?")) @frequency.setter def frequency(self, frequency): """Defines RF frequency""" if self.has_doubler: truncated_range(frequency, (SG380.MIN_RF, 2*SG380.MAX_RF)) else: truncated_range(frequency, (SG380.MIN_RF, SG380.MAX_RF)) self.write("FREQ%.6f" % frequency) @property def mod_type(self): """Gets the modulation type""" return SG380.MOD_TYPES_VALUES[int(self.ask("TYPE?"))] @mod_type.setter def mod_type(self, type_): """Defines the modulation type""" if type_ not in SG380.MOD_TYPES_VALUES: raise RuntimeError('Undefined modulation type') elif (type_ == 'IQ') and not self.has_IQ: raise RuntimeError('IQ option not installed') else: index = SG380.MOD_TYPES_VALUES.index(type_) self.write("TYPE%d" % index) @property def mod_function(self): """Gets the modulation function""" return SG380.MOD_FUNCTIONS[int(self.ask("MFNC?"))] @mod_function.setter def mod_func(self, function): """Defines the modulation function""" if function not in SG380.MOD_FUNCTIONS: index = 1 else: index = SG380.MOD_FUNCTIONS.index(function) self.write("MFNC%d" % index) PyMeasure-0.9.0/pymeasure/instruments/srs/__init__.py0000664000175000017500000000232014010037617023207 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .sr830 import SR830 from .sg380 import SG380 from .sr860 import SR860 PyMeasure-0.9.0/pymeasure/instruments/validators.py0000664000175000017500000001305114010037617023014 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from decimal import Decimal def strict_range(value, values): """ Provides a validator function that returns the value if its value is less than the maximum and greater than the minimum of the range. Otherwise it raises a ValueError. :param value: A value to test :param values: A range of values (range, list, etc.) :raises: ValueError if the value is out of the range """ if min(values) <= value <= max(values): return value else: raise ValueError('Value of {:g} is not in range [{:g},{:g}]'.format( value, min(values), max(values) )) def strict_discrete_range(value, values, step): """ Provides a validator function that returns the value if its value is less than the maximum and greater than the minimum of the range and is a multiple of step. Otherwise it raises a ValueError. :param value: A value to test :param values: A range of values (range, list, etc.) :param step: Minimum stepsize (resolution limit) :raises: ValueError if the value is out of the range """ # use Decimal type to provide correct decimal compatible floating # point arithmetic compared to binary floating point arithmetic if (strict_range(value, values) == value and Decimal(str(value)) % Decimal(str(step)) == 0): return value else: raise ValueError('Value of {:g} is not a multiple of {:g}'.format( value, step )) def strict_discrete_set(value, values): """ Provides a validator function that returns the value if it is in the discrete set. Otherwise it raises a ValueError. :param value: A value to test :param values: A set of values that are valid :raises: ValueError if the value is not in the set """ if value in values: return value else: raise ValueError('Value of {} is not in the discrete set {}'.format( value, values )) def truncated_range(value, values): """ Provides a validator function that returns the value if it is in the range. Otherwise it returns the closest range bound. :param value: A value to test :param values: A set of values that are valid """ if min(values) <= value <= max(values): return value elif value > max(values): return max(values) else: return min(values) def modular_range(value, values): """ Provides a validator function that returns the value if it is in the range. Otherwise it returns the value, modulo the max of the range. :param value: a value to test :param values: A set of values that are valid """ return value % max(values) def modular_range_bidirectional(value, values): """ Provides a validator function that returns the value if it is in the range. Otherwise it returns the value, modulo the max of the range. Allows negative values. :param value: a value to test :param values: A set of values that are valid """ if value > 0: return value % max(values) else: return -1 * (abs(value) % max(values)) def truncated_discrete_set(value, values): """ Provides a validator function that returns the value if it is in the discrete set. Otherwise, it returns the smallest value that is larger than the value. :param value: A value to test :param values: A set of values that are valid """ # Force the values to be sorted values = list(values) values.sort() for v in values: if value <= v: return v return values[-1] def joined_validators(*validators): """ Join a list of validators together as a single. Expects a list of validator functions and values. :param validators: an iterable of other validators """ def validate(value, values): for validator, vals in zip(validators, values): try: return validator(value, vals) except (ValueError, TypeError): pass raise ValueError("Value of {} not in chained validator set".format(value)) return validate def discreteTruncate(number, discreteSet): """ Truncates the number to the closest element in the positive discrete set. Returns False if the number is larger than the maximum value or negative. """ if number < 0: return False discreteSet.sort() for item in discreteSet: if number <= item: return item return False PyMeasure-0.9.0/pymeasure/instruments/resources.py0000664000175000017500000000435114010037617022661 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import pyvisa def list_resources(): """ Prints the available resources, and returns a list of VISA resource names .. code-block:: python resources = list_resources() #prints (e.g.) #0 : GPIB0::22::INSTR : Agilent Technologies,34410A,****** #1 : GPIB0::26::INSTR : Keithley Instruments Inc., Model 2612, ***** dmm = Agilent34410(resources[0]) """ rm = pyvisa.ResourceManager() instrs = rm.list_resources() for n, instr in enumerate(instrs): # trying to catch errors in comunication try: res = rm.open_resource(instr) # try to avoid errors from *idn? try: # noinspection PyUnresolvedReferences idn = res.ask('*idn?')[:-1] except pyvisa.Error: idn = "Not known" finally: res.close() print(n, ":", instr, ":", idn) except pyvisa.VisaIOError as e: print(n, ":", instr, ":", "Visa IO Error: check connections") print(e) rm.close() return instrs PyMeasure-0.9.0/pymeasure/instruments/mock.py0000664000175000017500000000606414010037617021603 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import numpy import time from pymeasure.adapters import FakeAdapter from pymeasure.instruments import Instrument class Mock(Instrument): """Mock instrument for testing.""" def __init__(self, wait=.1, **kwargs): super().__init__( FakeAdapter, "Mock instrument", includeSCPI=False, **kwargs ) self._wait = wait self._tstart = 0 self._voltage = 10 self._output_voltage = 0 self._time = 0 self._wave = self.wave self._units = {'voltage': 'V', 'output_voltage': 'V', 'time': 's', 'wave': 'a.u.'} def get_time(self): """Get elapsed time""" if self._tstart == 0: self._tstart = time.time() self._time = time.time() - self._tstart return self._time def set_time(self, value): """ Wait for the timer to reach the specified time. If value = 0, reset. """ if value == 0: self._tstart = 0 else: while self.time < value: time.sleep(0.001) def reset_time(self): """Reset the timer to 0 s.""" self.time = 0 self.get_time() time = property(fget=get_time, fset=set_time) def get_wave(self): """Get wave.""" return float(numpy.sin(self.time)) wave = property(fget=get_wave) def get_voltage(self): """Get the voltage.""" time.sleep(self._wait) return self._voltage def __getitem__(self, keys): return keys voltage = property(fget=get_voltage) def get_output_voltage(self): return self._output_voltage def set_output_voltage(self, value): """Set the voltage.""" time.sleep(self._wait) self._output_voltage = value output_voltage = property(fget=get_output_voltage, fset=set_output_voltage) PyMeasure-0.9.0/pymeasure/instruments/anritsu/0000775000175000017500000000000014010046235021753 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/anritsu/anritsuMG3692C.py0000664000175000017500000000523614010037617024677 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument, discreteTruncate, RangeException class AnritsuMG3692C(Instrument): """ Represents the Anritsu MG3692C Signal Generator """ power = Instrument.control( ":POWER?;", ":POWER %g dBm;", """ A floating point property that represents the output power in dBm. This property can be set. """ ) frequency = Instrument.control( ":FREQUENCY?;", ":FREQUENCY %e Hz;", """ A floating point property that represents the output frequency in Hz. This property can be set. """ ) def __init__(self, resourceName, **kwargs): super(AnritsuMG3692C, self).__init__( resourceName, "Anritsu MG3692C Signal Generator", **kwargs ) @property def output(self): """ A boolean property that represents the signal output state. This property can be set to control the output. """ return int(self.ask(":OUTPUT?")) == 1 @output.setter def output(self, value): if value: self.write(":OUTPUT ON;") else: self.write(":OUTPUT OFF;") def enable(self): """ Enables the signal output. """ self.output = True def disable(self): """ Disables the signal output. """ self.output = False def shutdown(self): """ Shuts down the instrument, putting it in a safe state. """ # TODO: Implement modulation self.modulation = False self.disable() PyMeasure-0.9.0/pymeasure/instruments/anritsu/anritsuMS9710C.py0000664000175000017500000002463214010037617024711 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from time import sleep import numpy as np from pymeasure.instruments import Instrument from pymeasure.instruments.validators import ( strict_discrete_set, truncated_discrete_set, truncated_range, joined_validators ) import re log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) # Analysis Results with Units, ie -24.5DBM -> (-24.5, 'DBM') r_value_units = re.compile(r"([-\d]*\.\d*)(.*)") # Join validators to allow for special sets of characters truncated_range_or_off = joined_validators(strict_discrete_set, truncated_range) def _int_or_neg_one(v): try: return int(v) except ValueError: return -1 def _parse_trace_peak(vals): """Parse the returned value from a trace peak query.""" l, p = vals res = [l] m = r_value_units.match(p) if m is not None: data = list(m.groups()) data[0] = float(data[0]) res.extend(data) else: res.append(float(p)) return res class AnritsuMS9710C(Instrument): """Anritsu MS9710C Optical Spectrum Analyzer.""" ############# # Mappings # ############# ONOFF = ["ON", "OFF"] ONOFF_MAPPING = {True: 'ON', False: 'OFF', 1: 'ON', 0: 'OFF'} ###################### # Status Registers # ###################### ese2 = Instrument.control( "ESE2?", "ESE2 %d", "Extended Event Status Enable Register 2", get_process=int ) esr2 = Instrument.control( "ESR2?", "ESR2 %d", "Extended Event Status Register 2", get_process=_int_or_neg_one ) ########### # Modes # ########### measure_mode = Instrument.measurement( "MOD?", "Returns the current Measure Mode the OSA is in.", values={None: 0, "SINGLE": 1.0, "AUTO": 2.0, "POWER": 3.0}, map_values=True ) #################################### # Spectrum Parameters - Wavelength # #################################### wavelength_center = Instrument.control('CNT?', 'CNT %g', "Center Wavelength of Spectrum Scan in nm.") wavelength_span = Instrument.control('SPN?', 'SPN %g', "Wavelength Span of Spectrum Scan in nm.") wavelength_start = Instrument.control('STA?', 'STA %g', "Wavelength Start of Spectrum Scan in nm.") wavelength_stop = Instrument.control('STO?', 'STO %g', "Wavelength Stop of Spectrum Scan in nm.") wavelength_marker_value = Instrument.control( 'MKV?', 'MKV %s', "Wavelength Marker Value (wavelength or freq.?)", validator=strict_discrete_set, values=["WL", "FREQ"] ) wavelength_value_in = Instrument.control( 'WDP?', 'WDP %s', "Wavelength value in Vacuum or Air", validator=strict_discrete_set, values=["VACUUM", "AIR"] ) level_scale = Instrument.measurement( 'LVS?', "Current Level Scale", values=["LOG", "LIN"] ) level_log = Instrument.control( "LOG?", "LOG %f", "Level Log Scale (/div)", validator=truncated_range, values=[0.1, 10.0] ) level_lin = Instrument.control( "LIN?", "LIN %f", "Level Linear Scale (/div)", validator=truncated_range, values=[1e-12, 1] ) level_opt_attn = Instrument.control( "ATT?", "ATT %s", "Optical Attenuation Status (ON/OFF)", validator=strict_discrete_set, values=ONOFF ) resolution = Instrument.control( "RES?", "RES %f", "Resolution (nm)", validator=truncated_discrete_set, values=[0.05, 0.07, 0.1, 0.2, 0.5, 1.0] ) resolution_actual = Instrument.control( "ARES?", "ARES %s", "Resolution Actual (ON/OFF)", validator=strict_discrete_set, values=ONOFF, map_values=True ) resolution_vbw = Instrument.control( "VBW?", "VBW %s", "Video Bandwidth Resolution", validator=strict_discrete_set, values=["1MHz", "100kHz", "10kHz", "1kHz", "100Hz", "10Hz"] ) average_point = Instrument.control( "AVT?", "AVT %d", "Number of averages to take on each point (2-1000), or OFF", validator=truncated_range_or_off, values=[["OFF"], [2, 1000]] ) average_sweep = Instrument.control( "AVS?", "AVS %d", "Number of averages to make on a sweep (2-1000) or OFF", validator=truncated_range_or_off, values=[["OFF"], [2, 1000]] ) sampling_points = Instrument.control( "MPT?", "MPT %d", "Number of sampling points", validator=truncated_discrete_set, values=[51, 101, 251, 501, 1001, 2001, 5001], get_process=lambda v: int(v) ) ##################################### # Analysis Peak Search Parameters # ##################################### peak_search = Instrument.control( "PKS?", "PKS %s", "Peak Search Mode", validator=strict_discrete_set, values=["PEAK", "NEXT", "LAST", "LEFT", "RIGHT"] ) dip_search = Instrument.control( "DPS?", "DPS %s", "Dip Search Mode", validator=strict_discrete_set, values=["DIP", "NEXT", "LAST", "LEFT", "RIGHT"] ) analysis = Instrument.control( "ANA?", "ANA %s", "Analysis Control" ) analysis_result = Instrument.measurement( "ANAR?", "Read back anaysis result from current scan." ) ########################## # Data Memory Commands # ########################## data_memory_a_size = Instrument.measurement( 'DBA?', "Returns the number of points sampled in data memory register A." ) data_memory_b_size = Instrument.measurement( 'DBB?', "Returns the number of points sampled in data memory register B." ) data_memory_a_condition = Instrument.measurement( "DCA?", """Returns the data condition of data memory register A. Starting wavelength, and a sampling point (l1, l2, n).""" ) data_memory_b_condition = Instrument.measurement( "DCB?", """Returns the data condition of data memory register B. Starting wavelength, and a sampling point (l1, l2, n).""" ) data_memory_a_values = Instrument.measurement( "DMA?", "Reads the binary data from memory register A." ) data_memory_b_values = Instrument.measurement( "DMA?", "Reads the binary data from memory register B." ) data_memory_select = Instrument.control( "MSL?", "MSL %s", "Memory Data Select.", validator=strict_discrete_set, values=["A", "B"] ) ########################### # Trace Marker Commands # ########################### trace_marker_center = Instrument.setting( "TMC %s", "Trace Marker at Center. Set to 1 or True to initiate command", map_values=True, values={True: ''} ) trace_marker = Instrument.control( "TMK?", "TMK %f", "Sets the trace marker with a wavelength. Returns the trace wavelength and power.", get_process=_parse_trace_peak ) def __init__(self, adapter, **kwargs): """Constructor.""" self.analysis_mode = None super(AnritsuMS9710C, self).__init__(adapter, "Anritsu MS9710C Optical Spectrum Analyzer", **kwargs) @property def wavelengths(self): """Return a numpy array of the current wavelengths of scans.""" return np.linspace( self.wavelength_start, self.wavelength_stop, self.sampling_points ) def read_memory(self, slot="A"): """Read the scan saved in a memory slot.""" cond_attr = "data_memory_{}_condition".format(slot.lower()) data_attr = "data_memory_{}_values".format(slot.lower()) scan = getattr(self, cond_attr) wavelengths = np.linspace(scan[0], scan[1], int(scan[2])) power = np.fromstring(getattr(self, data_attr), sep="\r\n") return wavelengths, power def wait(self, n=3, delay=1): """Query OPC Command and waits for appropriate response.""" log.info("Wait for OPC") res = self.adapter.ask("*OPC?") n_attempts = n while(res == ''): log.debug("Empty OPC Repsonse. {} remaining".format(n_attempts)) if n_attempts == 0: break n_attempts -= 1 sleep(delay) res = self.adapter.read().strip() log.debug(res) def wait_for_sweep(self, n=20, delay=0.5): """Wait for a sweep to stop. This is performed by checking bit 1 of the ESR2. """ log.debug("Waiting for spectrum sweep") while(self.esr2 != 3 and n > 0): log.debug("Wait for sweep [{}]".format(n)) # log.debug("ESR2: {}".format(esr2)) sleep(delay) n -= 1 if n <= 0: log.warning("Sweep Timeout Occurred ({} s)".format(int(delay * n))) def single_sweep(self, **kwargs): """Perform a single sweep and wait for completion.""" log.debug("Performing a Spectrum Sweep") self.clear() self.write('SSI') self.wait_for_sweep(**kwargs) def center_at_peak(self, **kwargs): """Center the spectrum at the measured peak.""" self.write("PKC") self.wait(**kwargs) def measure_peak(self): """Measure the peak and return the trace marker.""" self.peak_search = "PEAK" return self.trace_marker PyMeasure-0.9.0/pymeasure/instruments/anritsu/__init__.py0000664000175000017500000000233314010037617024071 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .anritsuMG3692C import AnritsuMG3692C from .anritsuMS9710C import AnritsuMS9710C PyMeasure-0.9.0/pymeasure/instruments/deltaelektronika/0000775000175000017500000000000014010046235023610 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/deltaelektronika/sm7045d.py0000664000175000017500000001144114010037617025272 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_range from time import sleep from numpy import linspace class SM7045D(Instrument): """ This is the class for the SM 70-45 D power supply. .. code-block:: python source = SM7045D("GPIB::8") source.ramp_to_zero(1) # Set output to 0 before enabling source.enable() # Enables the output source.current = 1 # Sets a current of 1 Amps """ VOLTAGE_RANGE = [0, 70] CURRENT_RANGE = [0, 45] voltage = Instrument.control( "SO:VO?", "SO:VO %g", """ A floating point property that represents the output voltage setting of the power supply in Volts. This property can be set. """, validator=strict_range, values=VOLTAGE_RANGE ) current = Instrument.control( "SO:CU?", "SO:CU %g", """ A floating point property that represents the output current of the power supply in Amps. This property can be set. """, validator=strict_range, values=CURRENT_RANGE ) max_voltage = Instrument.control( "SO:VO:MA?", "SO:VO:MA %g", """ A floating point property that represents the maximum output voltage of the power supply in Volts. This property can be set. """, validator=strict_range, values=VOLTAGE_RANGE ) max_current = Instrument.control( "SO:CU:MA?", "SO:VO:MA %g", """ A floating point property that represents the maximum output current of the power supply in Amps. This property can be set. """, validator=strict_range, values=CURRENT_RANGE ) measure_voltage = Instrument.measurement( "ME:VO?", """ Measures the actual output voltage of the power supply in Volts. """, ) measure_current = Instrument.measurement( "ME:CU?", """ Measures the actual output current of the power supply in Amps. """, ) rsd = Instrument.measurement( "SO:FU:RSD?", """ Check whether remote shutdown is enabled/disabled and thus if the output of the power supply is disabled/enabled. """, ) def __init__(self, resourceName, **kwargs): super(SM7045D, self).__init__( resourceName, "Delta Elektronika SM 70-45 D", **kwargs ) def enable(self): """ Disable remote shutdown, hence output will be enabled. """ self.write("SO:FU:RSD 0") def disable(self): """ Enables remote shutdown, hence input will be disabled. """ self.write("SO:FU:RSD 1") def ramp_to_current(self, target_current, current_step=0.1): """ Gradually increase/decrease current to target current. :param target_current: Float that sets the target current (in A) :param current_step: Optional float that sets the current steps / ramp rate (in A/s) """ curr = self.current n = round(abs(curr - target_current) / current_step) + 1 for i in linspace(curr, target_current, n): self.current = i sleep(0.1) def ramp_to_zero(self, current_step=0.1): """ Gradually decrease the current to zero. :param current_step: Optional float that sets the current steps / ramp rate (in A/s) """ self.ramp_to_current(0, current_step) def shutdown(self): """ Set the current to 0 A and disable the output of the power source. """ self.ramp_to_zero() self.disable() PyMeasure-0.9.0/pymeasure/instruments/deltaelektronika/__init__.py0000664000175000017500000000224214010037617025725 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .sm7045d import SM7045D PyMeasure-0.9.0/pymeasure/instruments/parker/0000775000175000017500000000000014010046235021552 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/parker/parkerGV6.py0000664000175000017500000001675114010037617023751 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument from pymeasure.adapters import SerialAdapter from time import sleep import re class ParkerGV6(Instrument): """ Represents the Parker Gemini GV6 Servo Motor Controller and provides a high-level interface for interacting with the instrument """ degrees_per_count = 0.00045 # 90 deg per 200,000 count def __init__(self, port): super(ParkerGV6, self).__init__( SerialAdapter(port, 9600, timeout=0.5), "Parker GV6 Motor Controller" ) self.setDefaults() def write(self, command): """ Overwrites the Insturment.write command to provide the correct line break syntax """ self.connection.write(command + "\r") def read(self): """ Overwrites the Instrument.read command to provide the correct functionality """ return re.sub(r'\r\n\n(>|\?)? ', '', "\n".join(self.readlines())) def set_defaults(self): """ Sets up the default values for the motor, which is run upon construction """ self.echo = False self.set_hardware_limits(False, False) self.use_absolute_position() self.average_acceleration = 1 self.acceleration = 1 self.velocity = 3 def reset(self): """ Resets the motor controller while blocking and (CAUTION) resets the absolute position value of the motor """ self.write("RESET") sleep(5) self.setDefault() self.enable() def enable(self): """ Enables the motor to move """ self.write("DRIVE1") def disable(self): """ Disables the motor from moving """ self.write("DRIVE0") @property def status(self): """ Returns a list of the motor status in readable format """ return self.ask("TASF").split("\r\n\n") def is_moving(self): """ Returns True if the motor is currently moving """ return self.position is None @property def angle(self): """ Returns the angle in degrees based on the position and whether relative or absolute positioning is enabled, returning None on error """ position = self.position if position is not None: return position*self.degrees_per_count else: return None @angle.setter def angle(self, angle): """ Gives the motor a setpoint in degrees based on an angle from a relative or absolution position """ self.position = int(angle*self.degrees_per_count**-1) @property def angle_error(self): """ Returns the angle error in degrees based on the position error, or returns None on error """ position_error = self.position_error if position_error is not None: return position_error*self.degrees_per_count else: return None @property def position(self): """ Returns an integer number of counts that correspond to the angular position where 1 revolution equals 4000 counts """ match = re.search(r'(?<=TPE)-?\d+', self.ask("TPE")) if match is None: return None else: return int(match.group(0)) @position.setter def position(self, counts): # in counts: 4000 count = 1 rev """ Gives the motor a setpoint in counts where 4000 counts equals 1 revolution """ self.write("D" + str(int(counts))) @property def position_error(self): """ Returns the error in the number of counts that corresponds to the error in the angular position where 1 revolution equals 4000 counts """ match = re.search(r'(?<=TPER)-?\d+', self.ask("TPER")) if match is None: return None else: return int(match.group(0)) def move(self): """ Initiates the motor to move to the setpoint """ self.write("GO") def stop(self): """ Stops the motor during movement """ self.write("S") def kill(self): """ Stops the motor """ self.write("K") def use_absolute_position(self): """ Sets the motor to accept setpoints from an absolute zero position """ self.write("MA1") self.write("MC0") def use_relative_position(self): """ Sets the motor to accept setpoints that are relative to the last position """ self.write("MA0") self.write("MC0") def set_hardware_limits(self, positive=True, negative=True): """ Enables (True) or disables (False) the hardware limits for the motor """ if positive and negative: self.write("LH3") elif positive and not negative: self.write("LH2") elif not positive and negative: self.write("LH1") else: self.write("LH0") def set_software_limits(self, positive, negative): """ Sets the software limits for motion based on the count unit where 4000 counts is 1 revolution """ self.write("LSPOS%d" % int(positive)) self.write("LSNEG%d" % int(negative)) def echo(self, enable=False): """ Enables (True) or disables (False) the echoing of all commands that are sent to the instrument """ if enable: self.write("ECHO1") else: self.write("ECHO0") @property def acceleration(self): pass # TODO: Implement acceleration return value @acceleration.setter def acceleration(self, acceleration): """ Sets the acceleration setpoint in revolutions per second squared """ self.write("A" + str(float(acceleration))) @property def average_acceleration(self): pass # TODO: Implement average_acceleration return value @average_acceleration.setter def average_acceleration(self, acceleration): """ Sets the average acceleration setpoint in revolutions per second squared """ self.write("AA" + str(float(acceleration))) @property def velocity(self): pass # TODO: Implement velocity return value @velocity.setter def velocity(self, velocity): # in revs/s """ Sets the velocity setpoint in revolutions per second """ self.write("V" + str(float(velocity))) PyMeasure-0.9.0/pymeasure/instruments/parker/__init__.py0000664000175000017500000000224614010037617023673 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .parkerGV6 import ParkerGV6 PyMeasure-0.9.0/pymeasure/instruments/newport/0000775000175000017500000000000014010046235021764 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/newport/esp300.py0000664000175000017500000002527714010037617023371 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from time import sleep from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set class AxisError(Exception): """ Raised when a particular axis causes an error for the Newport ESP300. """ MESSAGES = { '00': 'MOTOR TYPE NOT DEFINED', '01': 'PARAMETER OUT OF RANGE', '02': 'AMPLIFIER FAULT DETECTED', '03': 'FOLLOWING ERROR THRESHOLD EXCEEDED', '04': 'POSITIVE HARDWARE LIMIT DETECTED', '05': 'NEGATIVE HARDWARE LIMIT DETECTED', '06': 'POSITIVE SOFTWARE LIMIT DETECTED', '07': 'NEGATIVE SOFTWARE LIMIT DETECTED', '08': 'MOTOR / STAGE NOT CONNECTED', '09': 'FEEDBACK SIGNAL FAULT DETECTED', '10': 'MAXIMUM VELOCITY EXCEEDED', '11': 'MAXIMUM ACCELERATION EXCEEDED', '12': 'Reserved for future use', '13': 'MOTOR NOT ENABLED', '14': 'Reserved for future use', '15': 'MAXIMUM JERK EXCEEDED', '16': 'MAXIMUM DAC OFFSET EXCEEDED', '17': 'ESP CRITICAL SETTINGS ARE PROTECTED', '18': 'ESP STAGE DEVICE ERROR', '19': 'ESP STAGE DATA INVALID', '20': 'HOMING ABORTED', '21': 'MOTOR CURRENT NOT DEFINED', '22': 'UNIDRIVE COMMUNICATIONS ERROR', '23': 'UNIDRIVE NOT DETECTED', '24': 'SPEED OUT OF RANGE', '25': 'INVALID TRAJECTORY MASTER AXIS', '26': 'PARAMETER CHARGE NOT ALLOWED', '27': 'INVALID TRAJECTORY MODE FOR HOMING', '28': 'INVALID ENCODER STEP RATIO', '29': 'DIGITAL I/O INTERLOCK DETECTED', '30': 'COMMAND NOT ALLOWED DURING HOMING', '31': 'COMMAND NOT ALLOWED DUE TO GROUP', '32': 'INVALID TRAJECTORY MODE FOR MOVING' } def __init__(self, code): self.axis = str(code)[0] self.error = str(code)[1:] self.message = self.MESSAGES[self.error] def __str__(self): return "Newport ESP300 axis %s reported the error: %s" % ( self.axis, self.message) class GeneralError(Exception): """ Raised when the Newport ESP300 has a general error. """ MESSAGES = { '1': 'PCI COMMUNICATION TIME-OUT', '4': 'EMERGENCY SOP ACTIVATED', '6': 'COMMAND DOES NOT EXIST', '7': 'PARAMETER OUT OF RANGE', '8': 'CABLE INTERLOCK ERROR', '9': 'AXIS NUMBER OUT OF RANGE', '13': 'GROUP NUMBER MISSING', '14': 'GROUP NUMBER OUT OF RANGE', '15': 'GROUP NUMBER NOT ASSIGNED', '17': 'GROUP AXIS OUT OF RANGE', '18': 'GROUP AXIS ALREADY ASSIGNED', '19': 'GROUP AXIS DUPLICATED', '16': 'GROUP NUMBER ALREADY ASSIGNED', '20': 'DATA ACQUISITION IS BUSY', '21': 'DATA ACQUISITION SETUP ERROR', '23': 'SERVO CYCLE TICK FAILURE', '25': 'DOWNLOAD IN PROGRESS', '26': 'STORED PROGRAM NOT STARTED', '27': 'COMMAND NOT ALLOWED', '29': 'GROUP PARAMETER MISSING', '30': 'GROUP PARAMETER OUT OF RANGE', '31': 'GROUP MAXIMUM VELOCITY EXCEEDED', '32': 'GROUP MAXIMUM ACCELERATION EXCEEDED', '22': 'DATA ACQUISITION NOT ENABLED', '28': 'STORED PROGRAM FLASH AREA FULL', '33': 'GROUP MAXIMUM DECELERATION EXCEEDED', '35': 'PROGRAM NOT FOUND', '37': 'AXIS NUMBER MISSING', '38': 'COMMAND PARAMETER MISSING', '34': 'GROUP MOVE NOT ALLOWED DURING MOTION', '39': 'PROGRAM LABEL NOT FOUND', '40': 'LAST COMMAND CANNOT BE REPEATED', '41': 'MAX NUMBER OF LABELS PER PROGRAM EXCEEDED' } def __init__(self, code): self.error = str(code) self.message = self.MESSAGES[self.error] def __str__(self): return "Newport ESP300 reported the error: %s" % ( self.message) class Axis(object): """ Represents an axis of the Newport ESP300 Motor Controller, which can have independent parameters from the other axes. """ position = Instrument.control( "TP", "PA%g", """ A floating point property that controls the position of the axis. The units are defined based on the actuator. Use the :meth:`~.wait_for_stop` method to ensure the position is stable. """ ) enabled = Instrument.measurement( "MO?", """ Returns a boolean value that is True if the motion for this axis is enabled. """, cast=bool ) left_limit = Instrument.control( "SL?", "SL%g", """ A floating point property that controls the left software limit of the axis. """ ) right_limit = Instrument.control( "SR?", "SR%g", """ A floating point property that controls the right software limit of the axis. """ ) units = Instrument.control( "SN?", "SN%d", """ A string property that controls the displacement units of the axis, which can take values of: enconder count, motor step, millimeter, micrometer, inches, milli-inches, micro-inches, degree, gradient, radian, milliradian, and microradian. """, validator=strict_discrete_set, values={ 'encoder count':0, 'motor step':1, 'millimeter':2, 'micrometer':3, 'inches':4, 'milli-inches':5, 'micro-inches':6, 'degree':7, 'gradient':8, 'radian':9, 'milliradian':10, 'microradian':11 }, map_values=True ) motion_done = Instrument.measurement( "MD?", """ Returns a boolean that is True if the motion is finished. """, cast=bool ) def __init__(self, axis, controller): self.axis = str(axis) self.controller = controller def ask(self, command): command = self.axis + command return self.controller.ask(command) def write(self, command): command = self.axis + command self.controller.write(command) def values(self, command, **kwargs): command = self.axis + command return self.controller.values(command, **kwargs) def enable(self): """ Enables motion for the axis. """ self.write("MO") def disable(self): """ Disables motion for the axis. """ self.write("MF") def home(self, type=1): """ Drives the axis to the home position, which may be the negative hardware limit for some actuators (e.g. LTA-HS). type can take integer values from 0 to 6. """ home_type = strict_discrete_set(type, [0,1,2,3,4,5,6]) self.write("OR%d" % home_type) def define_position(self, position): """ Overwrites the value of the current position with the given value. """ self.write("DH%g" % position) def zero(self): """ Resets the axis position to be zero at the current poisiton. """ self.write("DH") def wait_for_stop(self, delay=0, interval=0.05): """ Blocks the program until the motion is completed. A further delay can be specified in seconds. """ self.write("WS%d" % (delay*1e3)) while not self.motion_done: sleep(interval) class ESP300(Instrument): """ Represents the Newport ESP 300 Motion Controller and provides a high-level for interacting with the instrument. By default this instrument is constructed with x, y, and phi attributes that represent axes 1, 2, and 3. Custom implementations can overwrite this depending on the avalible axes. Axes are controlled through an :class:`Axis ` class. """ error = Instrument.measurement( "TE?", """ Reads an error code from the motion controller. """, cast=int ) def __init__(self, resourceName, **kwargs): super(ESP300, self).__init__( resourceName, "Newport ESP 300 Motion Controller", **kwargs ) # Defines default axes, which can be overwritten self.x = Axis(1, self) self.y = Axis(2, self) self.phi = Axis(3, self) def clear_errors(self): """ Clears the error messages by checking until a 0 code is recived. """ while self.error != 0: continue @property def errors(self): """ Returns a list of error Exceptions that can be later raised, or used to diagnose the situation. """ errors = [] code = self.error while code != 0: if code > 100: errors.append(AxisError(code)) else: errors.append(GeneralError(code)) code = self.error return errors @property def axes(self): """ A list of the :class:`Axis ` objects that are present. """ axes = [] directory = dir(self) for name in directory: if name == 'axes': continue # Skip this property try: item = getattr(self, name) if isinstance(item, Axis): axes.append(item) except TypeError: continue except Exception as e: raise e return axes def enable(self): """ Enables all of the axes associated with this controller. """ for axis in self.axes: axis.enable() def disable(self): """ Disables all of the axes associated with this controller. """ for axis in self.axes: axis.disable() def shutdown(self): """ Shuts down the controller by disabling all of the axes. """ self.disable() PyMeasure-0.9.0/pymeasure/instruments/newport/__init__.py0000664000175000017500000000224014010037617024077 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .esp300 import ESP300 PyMeasure-0.9.0/pymeasure/instruments/__init__.py0000664000175000017500000000354214010037617022407 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from ..errors import RangeError, RangeException from .instrument import Instrument from .mock import Mock from .resources import list_resources from .validators import discreteTruncate from . import advantest from . import agilent from . import ametek from . import ami from . import anapico from . import anritsu from . import attocube from . import danfysik from . import deltaelektronika from . import fwbell from . import hp from . import keithley from . import keysight from . import lakeshore from . import newport from . import ni from . import oxfordinstruments from . import parker from . import razorbill from . import signalrecovery from . import srs from . import tektronix from . import thorlabs from . import yokogawa PyMeasure-0.9.0/pymeasure/instruments/hp/0000775000175000017500000000000014010046235020675 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/hp/hp33120A.py0000664000175000017500000001134114010037617022354 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set class HP33120A(Instrument): """ Represents the Hewlett Packard 33120A Arbitrary Waveform Generator and provides a high-level interface for interacting with the instrument. """ SHAPES = { 'sinusoid':'SIN', 'square':'SQU', 'triangle':'TRI', 'ramp':'RAMP', 'noise':'NOIS', 'dc':'DC', 'user':'USER' } shape = Instrument.control( "SOUR:FUNC:SHAP?", "SOUR:FUNC:SHAP %s", """ A string property that controls the shape of the wave, which can take the values: sinusoid, square, triangle, ramp, noise, dc, and user. """, validator=strict_discrete_set, values=SHAPES, map_values=True ) frequency = Instrument.control( "SOUR:FREQ?", "SOUR:FREQ %g", """ A floating point property that controls the frequency of the output in Hz. The allowed range depends on the waveform shape and can be queried with :attr:`~.max_frequency` and :attr:`~.min_frequency`. """ ) max_frequency = Instrument.measurement( "SOUR:FREQ? MAX", """ Reads the maximum :attr:`~.HP33120A.frequency` in Hz for the given shape """ ) min_frequency = Instrument.measurement( "SOUR:FREQ? MIN", """ Reads the minimum :attr:`~.HP33120A.frequency` in Hz for the given shape """ ) amplitude = Instrument.control( "SOUR:VOLT?", "SOUR:VOLT %g", """ A floating point property that controls the voltage amplitude of the output signal. The default units are in peak-to-peak Volts, but can be controlled by :attr:`~.amplitude_units`. The allowed range depends on the waveform shape and can be queried with :attr:`~.max_amplitude` and :attr:`~.min_amplitude`. """ ) max_amplitude = Instrument.measurement( "SOUR:VOLT? MAX", """ Reads the maximum :attr:`~.amplitude` in Volts for the given shape """ ) min_amplitude = Instrument.measurement( "SOUR:VOLT? MIN", """ Reads the minimum :attr:`~.amplitude` in Volts for the given shape """ ) offset = Instrument.control( "SOUR:VOLT:OFFS?", "SOUR:VOLT:OFFS %g", """ A floating point property that controls the amplitude voltage offset in Volts. The allowed range depends on the waveform shape and can be queried with :attr:`~.max_offset` and :attr:`~.min_offset`. """ ) max_offset = Instrument.measurement( "SOUR:VOLT:OFFS? MAX", """ Reads the maximum :attr:`~.offset` in Volts for the given shape """ ) min_offset = Instrument.measurement( "SOUR:VOLT:OFFS? MIN", """ Reads the minimum :attr:`~.offset` in Volts for the given shape """ ) AMPLITUDE_UNITS = {'Vpp':'VPP', 'Vrms':'VRMS', 'dBm':'DBM', 'default':'DEF'} amplitude_units = Instrument.control( "SOUR:VOLT:UNIT?", "SOUR:VOLT:UNIT %s", """ A string property that controls the units of the amplitude, which can take the values Vpp, Vrms, dBm, and default. """, validator=strict_discrete_set, values=AMPLITUDE_UNITS, map_values=True ) def __init__(self, resourceName, **kwargs): super(HP33120A, self).__init__( resourceName, "Hewlett Packard 33120A Function Generator", **kwargs ) self.amplitude_units = 'Vpp' def beep(self): """ Causes a system beep. """ self.write("SYST:BEEP") PyMeasure-0.9.0/pymeasure/instruments/hp/__init__.py0000664000175000017500000000230314010037617023010 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .hp33120A import HP33120A from .hp34401A import HP34401A PyMeasure-0.9.0/pymeasure/instruments/hp/hp34401A.py0000664000175000017500000000376414010037617022371 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument class HP34401A(Instrument): """ Represents the HP 34401A instrument. """ voltage_dc = Instrument.measurement("MEAS:VOLT:DC? DEF,DEF", "DC voltage, in Volts") voltage_ac = Instrument.measurement("MEAS:VOLT:AC? DEF,DEF", "AC voltage, in Volts") current_dc = Instrument.measurement("MEAS:CURR:DC? DEF,DEF", "DC current, in Amps") current_ac = Instrument.measurement("MEAS:CURR:AC? DEF,DEF", "AC current, in Amps") resistance = Instrument.measurement("MEAS:RES? DEF,DEF", "Resistance, in Ohms") resistance_4w = Instrument.measurement("MEAS:FRES? DEF,DEF", "Four-wires (remote sensing) resistance, in Ohms") def __init__(self, resourceName, **kwargs): super(HP34401A, self).__init__( resourceName, "HP 34401A", **kwargs ) PyMeasure-0.9.0/pymeasure/instruments/comedi.py0000664000175000017500000001543414010037617022113 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.experiment import Procedure, Parameter, FloatParameter, IntegerParameter import numpy as np from time import time, sleep from threading import Event from importlib.util import find_spec if find_spec('pycomedi'): # Guard against pycomedi not being installed from pycomedi.subdevice import StreamingSubdevice from pycomedi.constant import * from pycomedi.constant import _NamedInt from pycomedi.channel import AnalogChannel from pycomedi.utility import inttrig_insn, Reader, CallbackReader def getAI(device, channel, range=None): """ Returns the analog input channel as specified for a given device """ ai = device.find_subdevice_by_type( SUBDEVICE_TYPE.ai, factory=StreamingSubdevice ).channel(channel, factory=AnalogChannel, aref=AREF.diff) if range is not None: ai.range = ai.find_range(unit=UNIT.volt, min=range[0], max=range[1]) return ai def getAO(device, channel, range=None): """ Returns the analog output channel as specified for a given device """ ao = device.find_subdevice_by_type( SUBDEVICE_TYPE.ao, factory=StreamingSubdevice ).channel(channel, factory=AnalogChannel, aref=AREF.diff) if range is not None: ao.range = ao.find_range(unit=UNIT.volt, min=range[0], max=range[1]) return ao def readAI(device, channel, range=None, count=1): """ Reads a single measurement (count==1) from the analog input channel of the device specified. Multiple readings can be preformed with count not equal to one, which are seperated by an arbitrary time """ ai = getAI(device, channel, range) converter = ai.get_converter() if count == 1: return converter.to_physical(ai.data_read()) else: return converter.to_physical(ai.data_read_n(count)) def writeAO(device, channel, voltage, range=None): """ Writes a single voltage to the analog output channel of the device specified """ ao = getAO(device, channel, range) converter = ao.get_converter() ao.data_write(converter.from_physical(voltage)) class SynchronousAI(object): def __init__(self, channels, period, samples): self.channels = channels self.samples = samples self.period = period self.scanPeriod = int(1e9*float(period)/float(samples)) # nano-seconds self.subdevice = self.channels[0].subdevice self.subdevice.cmd = self._command() def _command(self): """ Returns the command used to initiate and end the sampling """ command = self.subdevice.get_cmd_generic_timed(len(self.channels), self.scanPeriod) command.start_src = TRIG_SRC.int command.start_arg = 0 command.stop_src = TRIG_SRC.count command.stop_arg = self.samples command.chanlist = self.channels # Adding to remove chunk transfers (TRIG_WAKE_EOS) wake_eos = _NamedInt('wake_eos', 32) if wake_eos not in CMDF: CMDF.append(wake_eos) command.flags = CMDF.wake_eos return command def _verifyCommand(self): """ Checks the command over three times and allows comedi to correct the command given any device specific conflicts """ for i in range(3): rc = self.subdevice.command_test() # Verify command is correct if rc == None: break def measure(self, hasAborted=lambda:False): """ Initiates the scan after first checking the command and does not block, returns the starting timestamp """ self._verifyCommand() sleep(0.01) self.subdevice.command() length = len(self.channels) dtype = self.subdevice.get_dtype() converters = [c.get_converter() for c in self.channels] self.data = np.zeros((self.samples, length), dtype=np.float32) # Trigger AI self.subdevice.device.do_insn(inttrig_insn(self.subdevice)) # Measurement loop count = 0 size = int(self.data.itemsize/2)*length previous_bin_slice = b'' while not hasAborted() and self.samples > count: bin_slice = previous_bin_slice while len(bin_slice) < size: bin_slice += self.subdevice.device.file.read(size) previous_bin_slice = bin_slice[size:] bin_slice = bin_slice[:size] slice = np.fromstring( bin_slice, dtype=dtype, count=length ) if len(slice) != length: # Reading finished break # Convert to physical values for i, c in enumerate(converters): self.data[count,i] = c.to_physical(slice[i]) self.emit_progress(100.*count/self.samples) self.emit_data(self.data[count]) count += 1 # Cancel measurement if it is still running (abort event) if self.subdevice.get_flags().running: self.subdevice.cancel() """ Command for limited samples command = self.subdevice.get_cmd_generic_timed(len(self.channels), self.scanPeriod) command.start_src = TRIG_SRC.int command.start_arg = 0 command.stop_src = TRIG_SRC.count command.stop_arg = self.samples command.chanlist = self.channels Command for continuous AI command = self.subdevice.get_cmd_generic_timed(len(self.channels), self.scanPeriod) command.start_src = TRIG_SRC.int command.start_arg = 0 command.stop_src = TRIG_SRC.none command.stop_arg = 0 command.chanlist = self.channels """ PyMeasure-0.9.0/pymeasure/instruments/razorbill/0000775000175000017500000000000014010046235022266 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/razorbill/razorbillRP100.py0000664000175000017500000001077114010037617025335 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument from pymeasure.instruments.validators import (strict_discrete_set, strict_range) class razorbillRP100(Instrument): """Represents Razorbill RP100 strain cell controller .. code-block:: python scontrol = razorbillRP100("ASRL/dev/ttyACM0::INSTR") scontrol.output_1 = True # turns output on scontrol.slew_rate_1 = 1 # sets slew rate to 1V/s scontrol.voltage_1 = 10 # sets voltage on output 1 to 10V """ output_1 = Instrument.control("OUTP1?", "OUTP1 %d", """Turns output of channel 1 on or off""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True) output_2 = Instrument.control("OUTP2?", "OUTP2 %d", """Turns output of channel 2 on or off""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True) voltage_1 = Instrument.control("SOUR1:VOLT?", "SOUR1:VOLT %g", """Sets or queries the output voltage of channel 1""", validator=strict_range, values=[-230, 230]) voltage_2 = Instrument.control("SOUR2:VOLT?", "SOUR2:VOLT %g", """Sets or queries the output voltage of channel 2""", validator=strict_range, values=[-230, 230]) slew_rate_1 = Instrument.control("SOUR1:VOLT:SLEW?", "SOUR1:VOLT:SLEW %g", """Sets or queries the source slew rate in volts/sec of channel 1""", validator=strict_range, values=[0.1*10e-3, 100*10e3]) slew_rate_2 = Instrument.control("SOUR2:VOLT:SLEW?", "SOUR2:VOLT:SLEW %g", """Sets or queries the source slew rate in volts/sec of channel 2""", validator=strict_range, values=[0.1*10e-3, 100*10e3]) instant_voltage_1 = Instrument.measurement("SOUR1:VOLT:NOW?", """Returns the instantaneous output of source one in volts""") instant_voltage_2 = Instrument.measurement("SOUR2:VOLT:NOW?", """Returns the instanteneous output of source two in volts""") contact_voltage_1 = Instrument.measurement("MEAS1:VOLT?", """Returns the Voltage in volts present at the front panel output of channel 1""") contact_voltage_2 = Instrument.measurement("MEAS2:VOLT?", """Returns the Voltage in volts present at the front panel output of channel 2""") contact_current_1 = Instrument.measurement("MEAS1:CURR?", """Returns the current in amps present at the front panel output of channel 1""") contact_current_2 = Instrument.measurement("MEAS2:CURR?", """Returns the current in amps present at the front panel output of channel 2""") def __init__(self, adapter, **kwargs): super(razorbillRP100, self).__init__( adapter, "Razorbill RP100 Piezo Stack Powersupply", **kwargs ) self.timeout = 20 PyMeasure-0.9.0/pymeasure/instruments/razorbill/__init__.py0000664000175000017500000000226014010037617024403 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .razorbillRP100 import razorbillRP100 PyMeasure-0.9.0/pymeasure/instruments/thorlabs/0000775000175000017500000000000014010046235022104 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/thorlabs/thorlabspm100usb.py0000664000175000017500000000777114010037617025604 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument, RangeException class ThorlabsPM100USB(Instrument): """Represents Thorlabs PM100USB powermeter""" # TODO: refactor to check if the sensor wavelength is adjustable wavelength = Instrument.control("SENSE:CORR:WAV?", "SENSE:CORR:WAV %g", "Wavelength in nm; not set outside of range") # TODO: refactor to check if the sensor is a power sensor power = Instrument.measurement("MEAS:POW?", "Power, in Watts") wavelength_min = Instrument.measurement("SENS:CORR:WAV? MIN", "Get minimum wavelength, in nm") wavelength_max = Instrument.measurement("SENS:CORR:WAV? MAX", "Get maximum wavelength, in nm") def __init__(self, adapter, **kwargs): super(ThorlabsPM100USB, self).__init__( adapter, "ThorlabsPM100USB powermeter", **kwargs) self.timout = 3000 self.sensor() def measure_power(self, wavelength): """Set wavelength in nm and get power in W If wavelength is out of range it will be set to range limit""" if wavelength < self.wavelength_min: raise RangeException("Wavelength %.2f nm out of range: using minimum wavelength: %.2f nm" % ( wavelength, self.wavelength_min)) # explicit setting wavelenghth, althought it would be automatically set wavelength = self.wavelength_min if wavelength > self.wavelength_max: raise RangeException("Wavelength %.2f nm out of range: using maximum wavelength: %.2f nm" % ( wavelength, self.wavelength_max)) wavelength = self.wavelength_max self.wavelength = wavelength return self.power def sensor(self): "Get sensor info" response = self.ask("SYST:SENSOR:IDN?").split(',') self.sensor_name = response[0] self.sensor_sn = response[1] self.sensor_cal_msg = response[2] self.sensor_type = response[3] self.sensor_subtype = response[4] self._flags_str = response[-1][:-1] # interpretation of the flags # rough trick using bin repr, maybe something more elegant exixts # (bitshift, bitarray?) self._flags = tuple( map(lambda x: x == '1', bin(int(self._flags_str))[2:])) # setting the flags; _dn are empty self.is_power, self.is_energy, _d4, _d8, \ self.resp_settable, self.wavelength_settable, self.tau_settable, _d128, self.temperature_sens = self._flags @property def energy(self): if self.is_energy: return self.values("MEAS:ENER?") else: raise Exception("%s is not an energy sensor" % self.sensor_name) return 0 @energy.setter def energy(self, val): raise Exception("Energy not settable!") PyMeasure-0.9.0/pymeasure/instruments/thorlabs/thorlabspro8000.py0000664000175000017500000000712314010037617025334 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from pymeasure.instruments import Instrument, discreteTruncate from pymeasure.instruments.validators import strict_discrete_set, \ truncated_discrete_set, truncated_range import numpy as np import time import re class ThorlabsPro8000(Instrument): """Represents Thorlabs Pro 8000 modular laser driver""" SLOTS = range(1,9) LDC_POLARITIES = ['AG', 'CG'] STATUS = ['ON','OFF'] def __init__(self, resourceName, **kwargs): super(ThorlabsPro8000, self).__init__( resourceName, "Thorlabs Pro 8000", **kwargs ) self.write(':SYST:ANSW VALUE') # Code for general purpose commands (mother board related) slot = Instrument.control(":SLOT?", ":SLOT %d", "Slot selection. Allowed values are: {}""".format(SLOTS), validator=strict_discrete_set, values=SLOTS, map_values=False) # Code for LDC-xxxx daughter boards (laser driver) LDCCurrent = Instrument.control(":ILD:SET?", ":ILD:SET %g", """Laser current.""") LDCCurrentLimit = Instrument.control(":LIMC:SET?", ":LIMC:SET %g", """Set Software current Limit (value must be lower than hardware current limit).""") LDCPolarity = Instrument.control(":LIMC:SET?", ":LIMC:SET %s", """Set laser diode polarity. Allowed values are: {}""".format(LDC_POLARITIES), validator=strict_discrete_set, values=LDC_POLARITIES, map_values=False) LDCStatus = Instrument.control(":LASER?", ":LASER %s", """Set laser diode status. Allowed values are: {}""".format(STATUS), validator=strict_discrete_set, values=STATUS, map_values=False) # Code for TED-xxxx daughter boards (TEC driver) TEDStatus = Instrument.control(":TEC?", ":TEC %s", """Set TEC status. Allowed values are: {}""".format(STATUS), validator=strict_discrete_set, values=STATUS, map_values=False) TEDSetTemperature = Instrument.control(":TEMP:SET?", ":TEMP:SET %g", """Set TEC temperature""") PyMeasure-0.9.0/pymeasure/instruments/thorlabs/__init__.py0000664000175000017500000000234114010037617024221 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .thorlabspm100usb import ThorlabsPM100USB from .thorlabspro8000 import ThorlabsPro8000 PyMeasure-0.9.0/pymeasure/instruments/ami/0000775000175000017500000000000014010046235021034 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/ami/__init__.py0000664000175000017500000000224014010037617023147 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .ami430 import AMI430 PyMeasure-0.9.0/pymeasure/instruments/ami/ami430.py0000664000175000017500000001651714010046171022414 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument from pymeasure.adapters import VISAAdapter from pymeasure.instruments.validators import ( truncated_discrete_set, strict_discrete_set, truncated_range ) from time import sleep, time import numpy as np import re class AMI430(Instrument): """ Represents the AMI 430 Power supply and provides a high-level for interacting with the instrument. .. code-block:: python magnet = AMI430("TCPIP::web.address.com::7180::SOCKET") magnet.coilconst = 1.182 # kGauss/A magnet.voltage_limit = 2.2 # Sets the voltage limit in V magnet.target_current = 10 # Sets the target current to 10 A magnet.target_field = 1 # Sets target field to 1 kGauss magnet.ramp_rate_current = 0.0357 # Sets the ramp rate in A/s magnet.ramp_rate_field = 0.0422 # Sets the ramp rate in kGauss/s magnet.ramp # Initiates the ramping magnet.pause # Pauses the ramping magnet.status # Returns the status of the magnet magnet.ramp_to_current(5) # Ramps the current to 5 A magnet.shutdown() # Ramps the current to zero and disables output """ def __init__(self, resourceName, **kwargs): adapter = VISAAdapter(resourceName, read_termination='\n') super(AMI430, self).__init__( adapter, "AMI superconducting magnet power supply.", includeSCPI=True, **kwargs ) # Read twice in order to remove welcome/connect message self.read() self.read() maximumfield = 1.00 maximumcurrent = 50.63 coilconst = Instrument.control( "COIL?", "CONF:COIL %g", """ A floating point property that sets the coil contant in kGauss/A. """ ) voltage_limit = Instrument.control( "VOLT:LIM?", "CONF:VOLT:LIM %g", """ A floating point property that sets the voltage limit for charging/discharging the magnet. """ ) target_current = Instrument.control( "CURR:TARG?", "CONF:CURR:TARG %g", """ A floating point property that sets the target current in A for the magnet. """ ) target_field = Instrument.control( "FIELD:TARG?", "CONF:FIELD:TARG %g", """ A floating point property that sets the target field in kGauss for the magnet. """ ) ramp_rate_current = Instrument.control( "RAMP:RATE:CURR:1?", "CONF:RAMP:RATE:CURR 1,%g", """ A floating point property that sets the current ramping rate in A/s. """ ) ramp_rate_field = Instrument.control( "RAMP:RATE:FIELD:1?", "CONF:RAMP:RATE:FIELD 1,%g,1.00", """ A floating point property that sets the field ramping rate in kGauss/s. """ ) magnet_current = Instrument.measurement("CURR:MAG?", """ Reads the current in Amps of the magnet. """ ) supply_current = Instrument.measurement("CURR:SUPP?", """ Reads the current in Amps of the power supply. """ ) field = Instrument.measurement("FIELD:MAG?", """ Reads the field in kGauss of the magnet. """ ) state = Instrument.measurement("STATE?", """ Reads the field in kGauss of the magnet. """ ) def zero(self): """ Initiates the ramping of the magnetic field to zero current/field with ramping rate previously set. """ self.write("ZERO") def pause(self): """ Pauses the ramping of the magnetic field. """ self.write("PAUSE") def ramp(self): """ Initiates the ramping of the magnetic field to set current/field with ramping rate previously set. """ self.write("RAMP") def has_persistent_switch_enabled(self): """ Returns a boolean if the persistent switch is enabled. """ return bool(self.ask("PSwitch?")) def enable_persistent_switch(self): """ Enables the persistent switch. """ self.write("PSwitch 1") def disable_persistent_switch(self): """ Disables the persistent switch. """ self.write("PSwitch 0") @property def magnet_status(self): STATES = { 1: "RAMPING", 2: "HOLDING", 3: "PAUSED", 4: "Ramping in MANUAL UP", 5: "Ramping in MANUAL DOWN", 6: "ZEROING CURRENT in progress", 7: "QUENCH!!!", 8: "AT ZERO CURRENT", 9: "Heating Persistent Switch", 10: "Cooling Persistent Switch" } return STATES[self.state] def ramp_to_current(self, current, rate): """ Heats up the persistent switch and ramps the current with set ramp rate. """ self.enable_persistent_switch() self.target_current = current self.ramp_rate_current = rate self.wait_for_holding() self.ramp() def ramp_to_field(self, field, rate): """ Heats up the persistent switch and ramps the current with set ramp rate. """ self.enable_persistent_switch() self.target_field = field self.ramp_rate_field = rate self.wait_for_holding() self.ramp() def wait_for_holding(self, should_stop=lambda: False, timeout=800, interval=0.1): """ """ t = time() while self.state != 2 and self.state != 3 and self.state != 8: sleep(interval) if should_stop(): return if (time()-t) > timeout: raise Exception("Timed out waiting for AMI430 switch to warm up.") def shutdown(self, ramp_rate=0.0357): """ Turns on the persistent switch, ramps down the current to zero, and turns off the persistent switch. """ self.enable_persistent_switch() self.wait_for_holding() self.ramp_rate_current = ramp_rate self.zero() self.wait_for_holding() self.disable_persistent_switch() PyMeasure-0.9.0/pymeasure/instruments/ametek/0000775000175000017500000000000014010046235021534 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/instruments/ametek/ametek7270.py0000664000175000017500000001766514010037617023717 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) from pymeasure.instruments import Instrument from pymeasure.instruments.validators import modular_range, truncated_discrete_set, truncated_range class Ametek7270(Instrument): """This is the class for the Ametek DSP 7270 lockin amplifier""" SENSITIVITIES = [ 0.0, 2.0e-9, 5.0e-9, 10.0e-9, 20.0e-9, 50.0e-9, 100.0e-9, 200.0e-9, 500.0e-9, 1.0e-6, 2.0e-6, 5.0e-6, 10.0e-6, 20.0e-6, 50.0e-6, 100.0e-6, 200.0e-6, 500.0e-6, 1.0e-3, 2.0e-3, 5.0e-3, 10.0e-3, 20.0e-3, 50.0e-3, 100.0e-3, 200.0e-3, 500.0e-3, 1.0 ] TIME_CONSTANTS = [ 10.0e-6, 20.0e-6, 50.0e-6, 100.0e-6, 200.0e-6, 500.0e-6, 1.0e-3, 2.0e-3, 5.0e-3, 10.0e-3, 20.0e-3, 50.0e-3, 100.0e-3, 200.0e-3, 500.0e-3, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1.0e3, 2.0e3, 5.0e3, 10.0e3, 20.0e3, 50.0e3, 100.0e3 ] sensitivity = Instrument.control( # NOTE: only for IMODE = 1. "SEN.", "SEN %d", """ A floating point property that controls the sensitivity range in Volts, which can take discrete values from 2 nV to 1 V. This property can be set. """, validator=truncated_discrete_set, values=SENSITIVITIES, map_values=True ) slope = Instrument.control( "SLOPE", "SLOPE %d", """ A integer property that controls the filter slope in dB/octave, which can take the values 6, 12, 18, or 24 dB/octave. This property can be set. """, validator=truncated_discrete_set, values=[6, 12, 18, 24], map_values=True ) time_constant = Instrument.control( # NOTE: only for NOISEMODE = 0 "TC.", "TC %d", """ A floating point property that controls the time constant in seconds, which takes values from 10 microseconds to 100,000 seconds. This property can be set. """, validator=truncated_discrete_set, values=TIME_CONSTANTS, map_values=True ) # TODO: Figure out if this actually can send for X1. X2. Y1. Y2. or not. # There's nothing in the manual about it but UtilMOKE sends these. x = Instrument.measurement("X.", """ Reads the X value in Volts """ ) y = Instrument.measurement("Y.", """ Reads the Y value in Volts """ ) x1 = Instrument.measurement("X1.", """ Reads the first harmonic X value in Volts """ ) y1 = Instrument.measurement("Y1.", """ Reads the first harmonic Y value in Volts """ ) x2 = Instrument.measurement("X2.", """ Reads the second harmonic X value in Volts """ ) y2 = Instrument.measurement("Y2.", """ Reads the second harmonic Y value in Volts """ ) xy = Instrument.measurement("XY.", """ Reads both the X and Y values in Volts """ ) mag = Instrument.measurement("MAG.", """ Reads the magnitude in Volts """ ) harmonic = Instrument.control( "REFN", "REFN %d", """ An integer property that represents the reference harmonic mode control, taking values from 1 to 127. This property can be set. """, validator=truncated_discrete_set, values=list(range(1,128)) ) phase = Instrument.control( "REFP.", "REFP. %g", """ A floating point property that represents the reference harmonic phase in degrees. This property can be set. """, validator=modular_range, values=[0,360] ) voltage = Instrument.control( "OA.", "OA. %g", """ A floating point property that represents the voltage in Volts. This property can be set. """, validator=truncated_range, values=[0,5] ) frequency = Instrument.control( "OF.", "OF. %g", """ A floating point property that represents the lock-in frequency in Hz. This property can be set. """, validator=truncated_range, values=[0,2.5e5] ) dac1 = Instrument.control( "DAC. 1", "DAC. 1 %g", """ A floating point property that represents the output value on DAC1 in Volts. This property can be set. """, validator=truncated_range, values=[-10,10] ) dac2 = Instrument.control( "DAC. 2", "DAC. 2 %g", """ A floating point property that represents the output value on DAC2 in Volts. This property can be set. """, validator=truncated_range, values=[-10,10] ) dac3 = Instrument.control( "DAC. 3", "DAC. 3 %g", """ A floating point property that represents the output value on DAC3 in Volts. This property can be set. """, validator=truncated_range, values=[-10,10] ) dac4 = Instrument.control( "DAC. 4", "DAC. 4 %g", """ A floating point property that represents the output value on DAC4 in Volts. This property can be set. """, validator=truncated_range, values=[-10,10] ) adc1 = Instrument.measurement("ADC. 1", """ Reads the input value of ADC1 in Volts """ ) adc2 = Instrument.measurement("ADC. 2", """ Reads the input value of ADC2 in Volts """ ) adc3 = Instrument.measurement("ADC. 3", """ Reads the input value of ADC3 in Volts """ ) adc4 = Instrument.measurement("ADC. 4", """ Reads the input value of ADC4 in Volts """ ) id = Instrument.measurement("ID", """ Reads the instrument identification """ ) def __init__(self, resourceName, **kwargs): super(Ametek7270, self).__init__( resourceName, "Ametek DSP 7270", **kwargs ) def set_voltage_mode(self): """ Sets instrument to voltage control mode """ self.write("IMODE 0") def set_differential_mode(self, lineFiltering=True): """ Sets instrument to differential mode -- assuming it is in voltage mode """ self.write("VMODE 3") self.write("LF %d 0" % 3 if lineFiltering else 0) def set_channel_A_mode(self): """ Sets instrument to channel A mode -- assuming it is in voltage mode """ self.write("VMODE 1") @property def auto_gain(self): return (int(self.ask("AUTOMATIC")) == 1) @auto_gain.setter def auto_gain(self, setval): if setval: self.write("AUTOMATIC 1") else: self.write("AUTOMATIC 0") def shutdown(self): """ Ensures the instrument in a safe state """ self.voltage = 0. self.isShutdown = True log.info("Shutting down %s" % self.name) PyMeasure-0.9.0/pymeasure/instruments/ametek/__init__.py0000664000175000017500000000225014010037617023650 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .ametek7270 import Ametek7270 PyMeasure-0.9.0/pymeasure/adapters/0000775000175000017500000000000014010046235017476 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/adapters/prologix.py0000664000175000017500000001256114010037617021724 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import time import serial from .serial import SerialAdapter class PrologixAdapter(SerialAdapter): """ Encapsulates the additional commands necessary to communicate over a Prologix GPIB-USB Adapter, using the SerialAdapter. Each PrologixAdapter is constructed based on a serial port or connection and the GPIB address to be communicated to. Serial connection sharing is achieved by using the :meth:`.gpib` method to spawn new PrologixAdapters for different GPIB addresses. :param port: The Serial port name or a serial.Serial object :param address: Integer GPIB address of the desired instrument :param rw_delay: An optional delay to set between a write and read call for slow to respond instruments. :param preprocess_reply: optional callable used to preprocess strings received from the instrument. The callable returns the processed string. :param kwargs: Key-word arguments if constructing a new serial object :ivar address: Integer GPIB address of the desired instrument To allow user access to the Prologix adapter in Linux, create the file: :code:`/etc/udev/rules.d/51-prologix.rules`, with contents: .. code-block:: bash SUBSYSTEMS=="usb",ATTRS{idVendor}=="0403",ATTRS{idProduct}=="6001",MODE="0666" Then reload the udev rules with: .. code-block:: bash sudo udevadm control --reload-rules sudo udevadm trigger """ def __init__(self, port, address=None, rw_delay=None, serial_timeout=0.5, preprocess_reply=None, **kwargs): super().__init__(port, timeout=serial_timeout, preprocess_reply=preprocess_reply, **kwargs) self.address = address self.rw_delay = rw_delay if not isinstance(port, serial.Serial): self.set_defaults() def set_defaults(self): """ Sets up the default behavior of the Prologix-GPIB adapter """ self.write("++auto 0") # Turn off auto read-after-write self.write("++eoi 1") # Append end-of-line to commands self.write("++eos 2") # Append line-feed to commands def ask(self, command): """ Ask the Prologix controller, include a forced delay for some instruments. :param command: SCPI command string to be sent to instrument """ self.write(command) if self.rw_delay is not None: time.sleep(self.rw_delay) return self.read() def write(self, command): """ Writes the command to the GPIB address stored in the :attr:`.address` :param command: SCPI command string to be sent to the instrument """ if self.address is not None: address_command = "++addr %d\n" % self.address self.connection.write(address_command.encode()) command += "\n" self.connection.write(command.encode()) def read(self): """ Reads the response of the instrument until timeout :returns: String ASCII response of the instrument """ self.write("++read eoi") return b"\n".join(self.connection.readlines()).decode() def gpib(self, address, rw_delay=None): """ Returns and PrologixAdapter object that references the GPIB address specified, while sharing the Serial connection with other calls of this function :param address: Integer GPIB address of the desired instrument :param rw_delay: Set a custom Read/Write delay for the instrument :returns: PrologixAdapter for specific GPIB address """ rw_delay = rw_delay or self.rw_delay return PrologixAdapter(self.connection, address, rw_delay=rw_delay) def wait_for_srq(self, timeout=25, delay=0.1): """ Blocks until a SRQ, and leaves the bit high :param timeout: Timeout duration in seconds :param delay: Time delay between checking SRQ in seconds """ while int(self.ask("++srq")) != 1: # TODO: Include timeout! time.sleep(delay) def __repr__(self): if self.address is not None: return "" % ( self.connection.port, self.address) else: return "" % self.connection.port PyMeasure-0.9.0/pymeasure/adapters/vxi11.py0000664000175000017500000001006614010037617021027 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) try: import vxi11 except ImportError: log.warning('Failed to import vxi11 package, which is required for the VXI11Adapter') from .adapter import Adapter class VXI11Adapter(Adapter): """ VXI11 Adapter class. Provides a adapter object that wraps around the read, write and ask functionality of the vxi11 library. :param host: string containing the visa connection information. :param preprocess_reply: optional callable used to preprocess strings received from the instrument. The callable returns the processed string. """ def __init__(self, host, preprocess_reply=None, **kwargs): super().__init__(preprocess_reply=preprocess_reply) # Filter valid arguments that can be passed to vxi instrument valid_args = ["name", "client_id", "term_char"] self.conn_kwargs = {} for key in kwargs: if key in valid_args: self.conn_kwargs[key] = kwargs[key] self.connection = vxi11.Instrument(host, **self.conn_kwargs) def write(self, command): """ Wrapper function for the write command using the vxi11 interface. :param command: string with command the that will be transmitted to the instrument. """ self.connection.write(command) def read(self): """ Wrapper function for the read command using the vx11 interface. :return string containing a response from the device. """ return self.connection.read() def ask(self, command): """ Wrapper function for the ask command using the vx11 interface. :param command: string with the command that will be transmitted to the instrument. :returns string containing a response from the device. """ return self.connection.ask(command) def write_raw(self, command): """ Wrapper function for the write_raw command using the vxi11 interface. :param command: binary string with the command that will be transmitted to the instrument """ self.connection.write_raw(command) def read_raw(self): """ Wrapper function for the read_raw command using the vx11 interface. :returns binary string containing the response from the device. """ return self.connection.read_raw() def ask_raw(self, command): """ Wrapper function for the ask_raw command using the vx11 interface. :param command: binary string with the command that will be transmitted to the instrument :returns binary string containing the response from the device. """ return self.connection.ask_raw(command) def __repr__(self): return ''.format(self.connection.host) PyMeasure-0.9.0/pymeasure/adapters/visa.py0000664000175000017500000001424614010037617021025 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import copy import pyvisa import numpy as np from pkg_resources import parse_version from .adapter import Adapter log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) # noinspection PyPep8Naming,PyUnresolvedReferences class VISAAdapter(Adapter): """ Adapter class for the VISA library using PyVISA to communicate with instruments. :param resource: VISA resource name that identifies the address :param visa_library: VisaLibrary Instance, path of the VISA library or VisaLibrary spec string (@py or @ni). if not given, the default for the platform will be used. :param preprocess_reply: optional callable used to preprocess strings received from the instrument. The callable returns the processed string. :param kwargs: Any valid key-word arguments for constructing a PyVISA instrument """ def __init__(self, resource_name, visa_library='', preprocess_reply=None, **kwargs): super().__init__(preprocess_reply=preprocess_reply) if not VISAAdapter.has_supported_version(): raise NotImplementedError("Please upgrade PyVISA to version 1.8 or later.") if isinstance(resource_name, int): resource_name = "GPIB0::%d::INSTR" % resource_name self.resource_name = resource_name self.manager = pyvisa.ResourceManager(visa_library) safeKeywords = [ 'resource_name', 'timeout', 'chunk_size', 'lock', 'query_delay', 'send_end', 'read_termination', 'write_termination' ] kwargsCopy = copy.deepcopy(kwargs) for key in kwargsCopy: if key not in safeKeywords: kwargs.pop(key) self.connection = self.manager.open_resource( resource_name, **kwargs ) @staticmethod def has_supported_version(): """ Returns True if the PyVISA version is greater than 1.8 """ if hasattr(pyvisa, '__version__'): return parse_version(pyvisa.__version__) >= parse_version('1.8') else: return False def write(self, command): """ Writes a command to the instrument :param command: SCPI command string to be sent to the instrument """ self.connection.write(command) def read(self): """ Reads until the buffer is empty and returns the resulting ASCII response :returns: String ASCII response of the instrument. """ return self.connection.read() def read_bytes(self, size): """ Reads specified number of bytes from the buffer and returns the resulting ASCII response :param size: Number of bytes to read from the buffer :returns: String ASCII response of the instrument. """ return self.connection.read_bytes(size) def ask(self, command): """ Writes the command to the instrument and returns the resulting ASCII response :param command: SCPI command string to be sent to the instrument :returns: String ASCII response of the instrument """ return self.connection.query(command) def ask_values(self, command, **kwargs): """ Writes a command to the instrument and returns a list of formatted values from the result. This leverages the `query_ascii_values` method in PyVISA. :param command: SCPI command to be sent to the instrument :param kwargs: Key-word arguments to pass onto `query_ascii_values` :returns: Formatted response of the instrument. """ return self.connection.query_ascii_values(command, **kwargs) def binary_values(self, command, header_bytes=0, dtype=np.float32): """ Returns a numpy array from a query for binary data :param command: SCPI command to be sent to the instrument :param header_bytes: Integer number of bytes to ignore in header :param dtype: The NumPy data type to format the values with :returns: NumPy array of values """ self.connection.write(command) binary = self.connection.read_raw() header, data = binary[:header_bytes], binary[header_bytes:] return np.fromstring(data, dtype=dtype) def write_binary_values(self, command, values, **kwargs): """ Write binary data to the instrument, e.g. waveform for signal generators :param command: SCPI command to be sent to the instrument :param values: iterable representing the binary values :param kwargs: Key-word arguments to pass onto `write_binary_values` :returns: number of bytes written """ return self.connection.write_binary_values(command, values, **kwargs) def wait_for_srq(self, timeout=25, delay=0.1): """ Blocks until a SRQ, and leaves the bit high :param timeout: Timeout duration in seconds :param delay: Time delay between checking SRQ in seconds """ self.connection.wait_for_srq(timeout * 1000) def __repr__(self): return "" % self.connection.resource_name PyMeasure-0.9.0/pymeasure/adapters/adapter.py0000664000175000017500000001306714010037617021503 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import numpy as np from copy import copy class Adapter(object): """ Base class for Adapter child classes, which adapt between the Instrument object and the connection, to allow flexible use of different connection techniques. This class should only be inherited from. :param preprocess_reply: optional callable used to preprocess strings received from the instrument. The callable returns the processed string. :param kwargs: all other keyword arguments are ignored. """ def __init__(self, preprocess_reply=None, **kwargs): self.preprocess_reply = preprocess_reply self.connection = None def __del__(self): """Close connection upon garbage collection of the device""" if self.connection is not None: self.connection.close() def write(self, command): """ Writes a command to the instrument :param command: SCPI command string to be sent to the instrument """ raise NameError("Adapter (sub)class has not implemented writing") def ask(self, command): """ Writes the command to the instrument and returns the resulting ASCII response :param command: SCPI command string to be sent to the instrument :returns: String ASCII response of the instrument """ self.write(command) return self.read() def read(self): """ Reads until the buffer is empty and returns the resulting ASCII respone :returns: String ASCII response of the instrument. """ raise NameError("Adapter (sub)class has not implemented reading") def values(self, command, separator=',', cast=float, preprocess_reply=None): """ Writes a command to the instrument and returns a list of formatted values from the result :param command: SCPI command to be sent to the instrument :param separator: A separator character to split the string into a list :param cast: A type to cast the result :param preprocess_reply: optional callable used to preprocess values received from the instrument. The callable returns the processed string. If not specified, the Adapter default is used if available, otherwise no preprocessing is done. :returns: A list of the desired type, or strings where the casting fails """ results = str(self.ask(command)).strip() if callable(preprocess_reply): results = preprocess_reply(results) elif callable(self.preprocess_reply): results = self.preprocess_reply(results) results = results.split(separator) for i, result in enumerate(results): try: if cast == bool: # Need to cast to float first since results are usually # strings and bool of a non-empty string is always True results[i] = bool(float(result)) else: results[i] = cast(result) except Exception: pass # Keep as string return results def binary_values(self, command, header_bytes=0, dtype=np.float32): """ Returns a numpy array from a query for binary data :param command: SCPI command to be sent to the instrument :param header_bytes: Integer number of bytes to ignore in header :param dtype: The NumPy data type to format the values with :returns: NumPy array of values """ raise NameError("Adapter (sub)class has not implemented the " "binary_values method") class FakeAdapter(Adapter): """Provides a fake adapter for debugging purposes, which bounces back the command so that arbitrary values testing is possible. .. code-block:: python a = FakeAdapter() assert a.read() == "" a.write("5") assert a.read() == "5" assert a.read() == "" assert a.ask("10") == "10" assert a.values("10") == [10] """ _buffer = "" def read(self): """ Returns the last commands given after the last read call. """ result = copy(self._buffer) # Reset the buffer self._buffer = "" return result def write(self, command): """ Writes the command to a buffer, so that it can be read back. """ self._buffer += command def __repr__(self): return "" PyMeasure-0.9.0/pymeasure/adapters/__init__.py0000664000175000017500000000341414010037617021615 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from .adapter import Adapter, FakeAdapter log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) try: from pymeasure.adapters.visa import VISAAdapter except ImportError: log.warning("PyVISA library could not be loaded") try: from pymeasure.adapters.serial import SerialAdapter from pymeasure.adapters.prologix import PrologixAdapter except ImportError: log.warning("PySerial library could not be loaded") try: from pymeasure.adapters.vxi11 import VXI11Adapter except ImportError: log.warning("VXI-11 library could not be loaded") from pymeasure.adapters.telnet import TelnetAdapter PyMeasure-0.9.0/pymeasure/adapters/serial.py0000664000175000017500000000614614010037617021342 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import serial import numpy as np from .adapter import Adapter log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class SerialAdapter(Adapter): """ Adapter class for using the Python Serial package to allow serial communication to instrument :param port: Serial port :param preprocess_reply: optional callable used to preprocess strings received from the instrument. The callable returns the processed string. :param kwargs: Any valid key-word argument for serial.Serial """ def __init__(self, port, preprocess_reply=None, **kwargs): super().__init__(preprocess_reply=preprocess_reply) if isinstance(port, serial.Serial): self.connection = port else: self.connection = serial.Serial(port, **kwargs) def write(self, command): """ Writes a command to the instrument :param command: SCPI command string to be sent to the instrument """ self.connection.write(command.encode()) # encode added for Python 3 def read(self): """ Reads until the buffer is empty and returns the resulting ASCII respone :returns: String ASCII response of the instrument. """ return b"\n".join(self.connection.readlines()).decode() def binary_values(self, command, header_bytes=0, dtype=np.float32): """ Returns a numpy array from a query for binary data :param command: SCPI command to be sent to the instrument :param header_bytes: Integer number of bytes to ignore in header :param dtype: The NumPy data type to format the values with :returns: NumPy array of values """ self.connection.write(command.encode()) binary = self.connection.read().decode() header, data = binary[:header_bytes], binary[header_bytes:] return np.fromstring(data, dtype=dtype) def __repr__(self): return "" % self.connection.port PyMeasure-0.9.0/pymeasure/adapters/telnet.py0000664000175000017500000000642014010037617021351 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import telnetlib import time from .adapter import Adapter class TelnetAdapter(Adapter): """ Adapter class for using the Python telnetlib package to allow communication to instruments :param host: host address of the instrument :param port: TCPIP port :param query_delay: delay in seconds between write and read in the ask method :param preprocess_reply: optional callable used to preprocess strings received from the instrument. The callable returns the processed string. :param kwargs: Valid keyword arguments for telnetlib.Telnet, currently this is only 'timeout' """ def __init__(self, host, port=0, query_delay=0, preprocess_reply=None, **kwargs): super().__init__(preprocess_reply=preprocess_reply) self.query_delay = query_delay safe_keywords = ['timeout'] for kw in kwargs: if kw not in safe_keywords: raise TypeError( f"TelnetAdapter: unexpected keyword argument '{kw}', " f"allowed are: {str(safe_keywords)}") self.connection = telnetlib.Telnet(host, port, **kwargs) def write(self, command): """ Writes a command to the instrument :param command: command string to be sent to the instrument """ self.connection.write(command.encode()) def read(self): """ Read something even with blocking the I/O. After something is received check again to obtain a full reply. :returns: String ASCII response of the instrument. """ return self.connection.read_some().decode() + \ self.connection.read_very_eager().decode() def ask(self, command): """ Writes a command to the instrument and returns the resulting ASCII response :param command: command string to be sent to the instrument :returns: String ASCII response of the instrument """ self.write(command) time.sleep(self.query_delay) return self.read() def __repr__(self): return "" % (self.connection.host, self.connection.port) PyMeasure-0.9.0/pymeasure/console.py0000664000175000017500000000412314010037617017713 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import numpy as np log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) log.warning('not implemented yet') class ProgressBar(object): """ ProgressBar keeps track of the progress, predicts the estimated time of arrival (ETA), and formats the bar for display in the console """ def __init__(self): self.data = np.empty() self.progress_percentage = [] self.progress_times = [] def advance(self, progress): """ Appends the progress state and the current time to the data, so that a more accurate prediction for the ETA can be made """ pass def __str__(self): """ Returns a string representation of the progress bar """ pass def display(log, port, level=logging.INFO): """ Displays the log to the console with a progress bar that always remains at the bottom of the screen and refreshes itself """ pass PyMeasure-0.9.0/pymeasure/process.py0000664000175000017500000000431314010037617017730 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from multiprocessing import get_context log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) context = get_context() # Useful for multiprocessing debugging: # context.log_to_stderr(logging.DEBUG) class StoppableProcess(context.Process): """ Base class for Processes which require the ability to be stopped by a process-safe method call """ def __init__(self): super().__init__() self._should_stop = context.Event() self._should_stop.clear() def join(self, timeout=0): """ Joins the current process and forces it to stop after the timeout if necessary :param timeout: Timeout duration in seconds """ self._should_stop.wait(timeout) if not self.should_stop(): self.stop() return super().join(0) def stop(self): self._should_stop.set() def should_stop(self): return self._should_stop.is_set() def __repr__(self): return "<%s(should_stop=%s)>" % ( self.__class__.__name__, self.should_stop()) PyMeasure-0.9.0/pymeasure/log.py0000664000175000017500000000746214010037617017043 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import logging.handlers from logging.handlers import QueueHandler from queue import Queue log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class QueueListener(logging.handlers.QueueListener): def is_alive(self): try: return self._thread.is_alive() except AttributeError: return False def console_log(logger, level=logging.INFO, queue=None): """Create a console log handler. Return a scribe thread object.""" if queue is None: queue = Queue() logger.setLevel(level) ch = logging.StreamHandler() ch.setLevel(level) formatter = logging.Formatter( fmt='%(asctime)s: %(message)s (%(name)s, %(levelname)s)', datefmt='%I:%M:%S %p') ch.setFormatter(formatter) logger.addHandler(ch) scribe = Scribe(queue) return scribe def file_log(logger, log_filename, level=logging.INFO, queue=None, **kwargs): """Create a file log handler. Return a scribe thread object.""" if queue is None: queue = Queue() logger.setLevel(level) ch = logging.FileHandler(log_filename, **kwargs) ch.setLevel(level) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') ch.setFormatter(formatter) logger.addHandler(ch) scribe = Scribe(queue) return scribe class Scribe(QueueListener): """ Scribe class which logs records as retrieved from a queue to support consistent multi-process logging. :param queue: The multiprocessing queue which the scriber will listen to. """ def __init__(self, queue): super().__init__(queue) def handle(self, record): logging.getLogger(record.name).handle(record) def setup_logging(logger=None, console=False, console_level='INFO', filename=None, file_level='DEBUG', queue=None, file_kwargs=None): """Setup logging for console and/or file logging. Returns a scribe thread object. Defaults to no logging.""" if queue is None: queue = Queue() if logger is None: logger = logging.getLogger() if file_kwargs is None: file_kwargs = {} logger.handlers = [] if console: console_log(logger, level=getattr(logging, console_level)) logger.info('Set up console logging') if filename is not None: file_log(logger, filename, level=getattr(logging, file_level), **file_kwargs) logger.info('Set up file logging') scribe = Scribe(queue) return scribe class TopicQueueHandler(QueueHandler): def __init__(self, queue, topic='log'): super().__init__(queue) self.topic = topic def prepare(self, record): return self.topic, record PyMeasure-0.9.0/pymeasure/__init__.py0000664000175000017500000000223314010046171020003 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # __version__ = '0.9.0' PyMeasure-0.9.0/pymeasure/errors.py0000664000175000017500000000241514010037617017567 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # class Error(Exception): pass class RangeError(Error): pass # TODO should be deprecated someday RangeException = RangeError PyMeasure-0.9.0/pymeasure/display/0000775000175000017500000000000014010046235017340 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/display/widgets.py0000664000175000017500000010624414010046171021366 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import os import re import pyqtgraph as pg from functools import partial import numpy from collections import ChainMap from itertools import product from .browser import Browser from .curves import ResultsCurve, Crosshairs, ResultsImage from .inputs import BooleanInput, IntegerInput, ListInput, ScientificInput, StringInput from .log import LogHandler from .Qt import QtCore, QtGui from ..experiment import parameters, Procedure from ..experiment.results import Results log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class PlotFrame(QtGui.QFrame): """ Combines a PyQtGraph Plot with Crosshairs. Refreshes the plot based on the refresh_time, and allows the axes to be changed on the fly, which updates the plotted data """ LABEL_STYLE = {'font-size': '10pt', 'font-family': 'Arial', 'color': '#000000'} updated = QtCore.QSignal() x_axis_changed = QtCore.QSignal(str) y_axis_changed = QtCore.QSignal(str) def __init__(self, x_axis=None, y_axis=None, refresh_time=0.2, check_status=True, parent=None): super().__init__(parent) self.refresh_time = refresh_time self.check_status = check_status self._setup_ui() self.change_x_axis(x_axis) self.change_y_axis(y_axis) def _setup_ui(self): self.setAutoFillBackground(False) self.setStyleSheet("background: #fff") self.setFrameShape(QtGui.QFrame.StyledPanel) self.setFrameShadow(QtGui.QFrame.Sunken) self.setMidLineWidth(1) vbox = QtGui.QVBoxLayout(self) self.plot_widget = pg.PlotWidget(self, background='#ffffff') self.coordinates = QtGui.QLabel(self) self.coordinates.setMinimumSize(QtCore.QSize(0, 20)) self.coordinates.setStyleSheet("background: #fff") self.coordinates.setText("") self.coordinates.setAlignment( QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) vbox.addWidget(self.plot_widget) vbox.addWidget(self.coordinates) self.setLayout(vbox) self.plot = self.plot_widget.getPlotItem() self.crosshairs = Crosshairs(self.plot, pen=pg.mkPen(color='#AAAAAA', style=QtCore.Qt.DashLine)) self.crosshairs.coordinates.connect(self.update_coordinates) self.timer = QtCore.QTimer() self.timer.timeout.connect(self.update_curves) self.timer.timeout.connect(self.crosshairs.update) self.timer.timeout.connect(self.updated) self.timer.start(int(self.refresh_time * 1e3)) def update_coordinates(self, x, y): self.coordinates.setText("(%g, %g)" % (x, y)) def update_curves(self): for item in self.plot.items: if isinstance(item, ResultsCurve): if self.check_status: if item.results.procedure.status == Procedure.RUNNING: item.update() else: item.update() def parse_axis(self, axis): """ Returns the units of an axis by searching the string """ units_pattern = r"\((?P\w+)\)" try: match = re.search(units_pattern, axis) except TypeError: match = None if match: if 'units' in match.groupdict(): label = re.sub(units_pattern, '', axis) return label, match.groupdict()['units'] else: return axis, None def change_x_axis(self, axis): for item in self.plot.items: if isinstance(item, ResultsCurve): item.x = axis item.update() label, units = self.parse_axis(axis) self.plot.setLabel('bottom', label, units=units, **self.LABEL_STYLE) self.x_axis = axis self.x_axis_changed.emit(axis) def change_y_axis(self, axis): for item in self.plot.items: if isinstance(item, ResultsCurve): item.y = axis item.update() label, units = self.parse_axis(axis) self.plot.setLabel('left', label, units=units, **self.LABEL_STYLE) self.y_axis = axis self.y_axis_changed.emit(axis) class PlotWidget(QtGui.QWidget): """ Extends the PlotFrame to allow different columns of the data to be dynamically choosen """ def __init__(self, columns, x_axis=None, y_axis=None, refresh_time=0.2, check_status=True, parent=None): super().__init__(parent) self.columns = columns self.refresh_time = refresh_time self.check_status = check_status self._setup_ui() self._layout() if x_axis is not None: self.columns_x.setCurrentIndex(self.columns_x.findText(x_axis)) self.plot_frame.change_x_axis(x_axis) if y_axis is not None: self.columns_y.setCurrentIndex(self.columns_y.findText(y_axis)) self.plot_frame.change_y_axis(y_axis) def _setup_ui(self): self.columns_x_label = QtGui.QLabel(self) self.columns_x_label.setMaximumSize(QtCore.QSize(45, 16777215)) self.columns_x_label.setText('X Axis:') self.columns_y_label = QtGui.QLabel(self) self.columns_y_label.setMaximumSize(QtCore.QSize(45, 16777215)) self.columns_y_label.setText('Y Axis:') self.columns_x = QtGui.QComboBox(self) self.columns_y = QtGui.QComboBox(self) for column in self.columns: self.columns_x.addItem(column) self.columns_y.addItem(column) self.columns_x.activated.connect(self.update_x_column) self.columns_y.activated.connect(self.update_y_column) self.plot_frame = PlotFrame( self.columns[0], self.columns[1], self.refresh_time, self.check_status ) self.updated = self.plot_frame.updated self.plot = self.plot_frame.plot self.columns_x.setCurrentIndex(0) self.columns_y.setCurrentIndex(1) def _layout(self): vbox = QtGui.QVBoxLayout(self) vbox.setSpacing(0) hbox = QtGui.QHBoxLayout() hbox.setSpacing(10) hbox.setContentsMargins(-1, 6, -1, 6) hbox.addWidget(self.columns_x_label) hbox.addWidget(self.columns_x) hbox.addWidget(self.columns_y_label) hbox.addWidget(self.columns_y) vbox.addLayout(hbox) vbox.addWidget(self.plot_frame) self.setLayout(vbox) def sizeHint(self): return QtCore.QSize(300, 600) def new_curve(self, results, color=pg.intColor(0), **kwargs): if 'pen' not in kwargs: kwargs['pen'] = pg.mkPen(color=color, width=2) if 'antialias' not in kwargs: kwargs['antialias'] = False curve = ResultsCurve(results, x=self.plot_frame.x_axis, y=self.plot_frame.y_axis, **kwargs ) curve.setSymbol(None) curve.setSymbolBrush(None) return curve def update_x_column(self, index): axis = self.columns_x.itemText(index) self.plot_frame.change_x_axis(axis) def update_y_column(self, index): axis = self.columns_y.itemText(index) self.plot_frame.change_y_axis(axis) class ImageFrame(QtGui.QFrame): """ Combines a PyQtGraph Plot with Crosshairs. Refreshes the plot based on the refresh_time, and allows the axes to be changed on the fly, which updates the plotted data """ LABEL_STYLE = {'font-size': '10pt', 'font-family': 'Arial', 'color': '#000000'} updated = QtCore.QSignal() x_axis_changed = QtCore.QSignal(str) y_axis_changed = QtCore.QSignal(str) z_axis_changed = QtCore.QSignal(str) def __init__(self, x_axis, y_axis, z_axis=None, refresh_time=0.2, check_status=True, parent=None): super().__init__(parent) self.refresh_time = refresh_time self.check_status = check_status self._setup_ui() # set axis labels for item in self.plot.items: if isinstance(item, ResultsImage): item.x = x_axis item.y = y_axis item.update_img() xlabel, xunits = self.parse_axis(x_axis) self.plot.setLabel('bottom', xlabel, units=xunits, **self.LABEL_STYLE) self.x_axis = x_axis self.x_axis_changed.emit(x_axis) ylabel, yunits = self.parse_axis(y_axis) self.plot.setLabel('left', ylabel, units=yunits, **self.LABEL_STYLE) self.y_axis = y_axis self.y_axis_changed.emit(y_axis) self.change_z_axis(z_axis) def _setup_ui(self): self.setAutoFillBackground(False) self.setStyleSheet("background: #fff") self.setFrameShape(QtGui.QFrame.StyledPanel) self.setFrameShadow(QtGui.QFrame.Sunken) self.setMidLineWidth(1) vbox = QtGui.QVBoxLayout(self) self.plot_widget = pg.PlotWidget(self, background='#ffffff') self.coordinates = QtGui.QLabel(self) self.coordinates.setMinimumSize(QtCore.QSize(0, 20)) self.coordinates.setStyleSheet("background: #fff") self.coordinates.setText("") self.coordinates.setAlignment( QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) vbox.addWidget(self.plot_widget) vbox.addWidget(self.coordinates) self.setLayout(vbox) self.plot = self.plot_widget.getPlotItem() self.crosshairs = Crosshairs(self.plot, pen=pg.mkPen(color='#AAAAAA', style=QtCore.Qt.DashLine)) self.crosshairs.coordinates.connect(self.update_coordinates) self.timer = QtCore.QTimer() self.timer.timeout.connect(self.update_curves) self.timer.timeout.connect(self.crosshairs.update) self.timer.timeout.connect(self.updated) self.timer.start(int(self.refresh_time * 1e3)) def update_coordinates(self, x, y): self.coordinates.setText("(%g, %g)" % (x, y)) def update_curves(self): for item in self.plot.items: if isinstance(item, ResultsImage): if self.check_status: if item.results.procedure.status == Procedure.RUNNING: item.update_img() else: item.update() def parse_axis(self, axis): """ Returns the units of an axis by searching the string """ units_pattern = r"\((?P\w+)\)" try: match = re.search(units_pattern, axis) except TypeError: match = None if match: if 'units' in match.groupdict(): label = re.sub(units_pattern, '', axis) return label, match.groupdict()['units'] else: return axis, None def change_z_axis(self, axis): for item in self.plot.items: if isinstance(item, ResultsImage): item.z = axis item.update_img() label, units = self.parse_axis(axis) if units is not None: self.plot.setTitle(label + ' (%s)'%units) else: self.plot.setTitle(label) self.z_axis = axis self.z_axis_changed.emit(axis) class ImageWidget(QtGui.QWidget): """ Extends the PlotFrame to allow different columns of the data to be dynamically choosen """ def __init__(self, columns, x_axis, y_axis, z_axis=None, refresh_time=0.2, check_status=True, parent=None): super().__init__(parent) self.columns = columns self.refresh_time = refresh_time self.check_status = check_status self.x_axis = x_axis self.y_axis = y_axis self._setup_ui() self._layout() if z_axis is not None: self.columns_z.setCurrentIndex(self.columns_z.findText(z_axis)) self.image_frame.change_z_axis(z_axis) def _setup_ui(self): self.columns_z_label = QtGui.QLabel(self) self.columns_z_label.setMaximumSize(QtCore.QSize(45, 16777215)) self.columns_z_label.setText('Z Axis:') self.columns_z = QtGui.QComboBox(self) for column in self.columns: self.columns_z.addItem(column) self.columns_z.activated.connect(self.update_z_column) self.image_frame = ImageFrame( self.x_axis, self.y_axis, self.columns[0], self.refresh_time, self.check_status ) self.updated = self.image_frame.updated self.plot = self.image_frame.plot self.columns_z.setCurrentIndex(2) def _layout(self): vbox = QtGui.QVBoxLayout(self) vbox.setSpacing(0) hbox = QtGui.QHBoxLayout() hbox.setSpacing(10) hbox.setContentsMargins(-1, 6, -1, 6) hbox.addWidget(self.columns_z_label) hbox.addWidget(self.columns_z) vbox.addLayout(hbox) vbox.addWidget(self.image_frame) self.setLayout(vbox) def sizeHint(self): return QtCore.QSize(300, 600) def new_image(self, results): """ Creates a new image """ image = ResultsImage(results, x=self.image_frame.x_axis, y=self.image_frame.y_axis, z=self.image_frame.z_axis ) return image def update_z_column(self, index): axis = self.columns_z.itemText(index) self.image_frame.change_z_axis(axis) class BrowserWidget(QtGui.QWidget): def __init__(self, *args, parent=None): super().__init__(parent) self.browser_args = args self._setup_ui() self._layout() def _setup_ui(self): self.browser = Browser(*self.browser_args, parent=self) self.clear_button = QtGui.QPushButton('Clear all', self) self.clear_button.setEnabled(False) self.hide_button = QtGui.QPushButton('Hide all', self) self.hide_button.setEnabled(False) self.show_button = QtGui.QPushButton('Show all', self) self.show_button.setEnabled(False) self.open_button = QtGui.QPushButton('Open', self) self.open_button.setEnabled(True) def _layout(self): vbox = QtGui.QVBoxLayout(self) vbox.setSpacing(0) hbox = QtGui.QHBoxLayout() hbox.setSpacing(10) hbox.setContentsMargins(-1, 6, -1, 6) hbox.addWidget(self.show_button) hbox.addWidget(self.hide_button) hbox.addWidget(self.clear_button) hbox.addStretch() hbox.addWidget(self.open_button) vbox.addLayout(hbox) vbox.addWidget(self.browser) self.setLayout(vbox) class InputsWidget(QtGui.QWidget): # tuple of Input classes that do not need an external label NO_LABEL_INPUTS = (BooleanInput,) def __init__(self, procedure_class, inputs=(), parent=None): super().__init__(parent) self._procedure_class = procedure_class self._procedure = procedure_class() self._inputs = inputs self._setup_ui() self._layout() def _setup_ui(self): parameter_objects = self._procedure.parameter_objects() for name in self._inputs: parameter = parameter_objects[name] if parameter.ui_class is not None: element = parameter.ui_class(parameter) elif isinstance(parameter, parameters.FloatParameter): element = ScientificInput(parameter) elif isinstance(parameter, parameters.IntegerParameter): element = IntegerInput(parameter) elif isinstance(parameter, parameters.BooleanParameter): element = BooleanInput(parameter) elif isinstance(parameter, parameters.ListParameter): element = ListInput(parameter) elif isinstance(parameter, parameters.Parameter): element = StringInput(parameter) setattr(self, name, element) def _layout(self): vbox = QtGui.QVBoxLayout(self) vbox.setSpacing(6) parameters = self._procedure.parameter_objects() for name in self._inputs: if not isinstance(getattr(self, name), self.NO_LABEL_INPUTS): label = QtGui.QLabel(self) label.setText("%s:" % parameters[name].name) vbox.addWidget(label) vbox.addWidget(getattr(self, name)) self.setLayout(vbox) def set_parameters(self, parameter_objects): for name in self._inputs: element = getattr(self, name) element.set_parameter(parameter_objects[name]) def get_procedure(self): """ Returns the current procedure """ self._procedure = self._procedure_class() parameter_values = {} for name in self._inputs: element = getattr(self, name) parameter_values[name] = element.parameter.value self._procedure.set_parameters(parameter_values) return self._procedure class LogWidget(QtGui.QWidget): def __init__(self, parent=None): super().__init__(parent) self._setup_ui() self._layout() def _setup_ui(self): self.view = QtGui.QPlainTextEdit() self.view.setReadOnly(True) self.handler = LogHandler() self.handler.setFormatter(logging.Formatter( fmt='%(asctime)s : %(message)s (%(levelname)s)', datefmt='%m/%d/%Y %I:%M:%S %p' )) self.handler.record.connect(self.view.appendPlainText) def _layout(self): vbox = QtGui.QVBoxLayout(self) vbox.setSpacing(0) vbox.addWidget(self.view) self.setLayout(vbox) class ResultsDialog(QtGui.QFileDialog): def __init__(self, columns, x_axis=None, y_axis=None, parent=None): super().__init__(parent) self.columns = columns self.x_axis, self.y_axis = x_axis, y_axis self.setOption(QtGui.QFileDialog.DontUseNativeDialog, True) self._setup_ui() def _setup_ui(self): preview_tab = QtGui.QTabWidget() vbox = QtGui.QVBoxLayout() param_vbox = QtGui.QVBoxLayout() vbox_widget = QtGui.QWidget() param_vbox_widget = QtGui.QWidget() self.plot_widget = PlotWidget(self.columns, self.x_axis, self.y_axis, parent=self) self.plot = self.plot_widget.plot self.preview_param = QtGui.QTreeWidget() param_header = QtGui.QTreeWidgetItem(["Name", "Value"]) self.preview_param.setHeaderItem(param_header) self.preview_param.setColumnWidth(0, 150) self.preview_param.setAlternatingRowColors(True) vbox.addWidget(self.plot_widget) param_vbox.addWidget(self.preview_param) vbox_widget.setLayout(vbox) param_vbox_widget.setLayout(param_vbox) preview_tab.addTab(vbox_widget, "Plot Preview") preview_tab.addTab(param_vbox_widget, "Run Parameters") self.layout().addWidget(preview_tab, 0, 5, 4, 1) self.layout().setColumnStretch(5, 1) self.setMinimumSize(900, 500) self.resize(900, 500) self.setFileMode(QtGui.QFileDialog.ExistingFiles) self.currentChanged.connect(self.update_plot) def update_plot(self, filename): self.plot.clear() if not os.path.isdir(filename) and filename != '': try: results = Results.load(str(filename)) except ValueError: return except Exception as e: raise e curve = ResultsCurve(results, x=self.plot_widget.plot_frame.x_axis, y=self.plot_widget.plot_frame.y_axis, pen=pg.mkPen(color=(255, 0, 0), width=1.75), antialias=True ) curve.update() self.plot.addItem(curve) self.preview_param.clear() for key, param in results.procedure.parameter_objects().items(): new_item = QtGui.QTreeWidgetItem([param.name, str(param)]) self.preview_param.addTopLevelItem(new_item) self.preview_param.sortItems(0, QtCore.Qt.AscendingOrder) """ This defines a list of functions that can be used to generate a sequence. """ SAFE_FUNCTIONS = { 'range': range, 'sorted': sorted, 'list': list, 'arange': numpy.arange, 'linspace': numpy.linspace, 'arccos': numpy.arccos, 'arcsin': numpy.arcsin, 'arctan': numpy.arctan, 'arctan2': numpy.arctan2, 'ceil': numpy.ceil, 'cos': numpy.cos, 'cosh': numpy.cosh, 'degrees': numpy.degrees, 'e': numpy.e, 'exp': numpy.exp, 'fabs': numpy.fabs, 'floor': numpy.floor, 'fmod': numpy.fmod, 'frexp': numpy.frexp, 'hypot': numpy.hypot, 'ldexp': numpy.ldexp, 'log': numpy.log, 'log10': numpy.log10, 'modf': numpy.modf, 'pi': numpy.pi, 'power': numpy.power, 'radians': numpy.radians, 'sin': numpy.sin, 'sinh': numpy.sinh, 'sqrt': numpy.sqrt, 'tan': numpy.tan, 'tanh': numpy.tanh, } class SequenceEvaluationException(Exception): """Raised when the evaluation of a sequence string goes wrong.""" pass class SequencerWidget(QtGui.QWidget): """ Widget that allows to generate a sequence of measurements with varying parameters. Moreover, one can write a simple text file to easily load a sequence. Currently requires a queue function of the ManagedWindow to have a "procedure" argument. """ MAXDEPTH = 10 def __init__(self, inputs=None, sequence_file=None, parent=None): super().__init__(parent) self._parent = parent # if no explicit inputs are given, use the displayed parameters if inputs is not None: self._inputs = inputs else: self._inputs = self._parent.displays self._get_properties() self._setup_ui() self._layout() # Load the sequence file if supplied. if sequence_file is not None: self.load_sequence(fileName=sequence_file) def _get_properties(self): """ Obtain the names of the input parameters. """ parameter_objects = self._parent.procedure_class().parameter_objects() self.names = {key: parameter.name for key, parameter in parameter_objects.items() if key in self._inputs} self.names_inv = {name: key for key, name in self.names.items()} def _setup_ui(self): self.tree = QtGui.QTreeWidget(self) self.tree.setHeaderLabels(["Level", "Parameter", "Sequence"]) width = self.tree.viewport().size().width() self.tree.setColumnWidth(0, int(0.7 * width)) self.tree.setColumnWidth(1, int(0.9 * width)) self.tree.setColumnWidth(2, int(0.9 * width)) self.add_root_item_btn = QtGui.QPushButton("Add root item") self.add_root_item_btn.clicked.connect( partial(self._add_tree_item, level=0) ) self.add_tree_item_btn = QtGui.QPushButton("Add item") self.add_tree_item_btn.clicked.connect(self._add_tree_item) self.remove_tree_item_btn = QtGui.QPushButton("Remove item") self.remove_tree_item_btn.clicked.connect(self._remove_selected_tree_item) self.load_seq_button = QtGui.QPushButton("Load sequence") self.load_seq_button.clicked.connect(self.load_sequence) self.load_seq_button.setToolTip("Load a sequence from a file.") self.queue_button = QtGui.QPushButton("Queue sequence") self.queue_button.clicked.connect(self.queue_sequence) def _layout(self): btn_box = QtGui.QHBoxLayout() btn_box.addWidget(self.add_root_item_btn) btn_box.addWidget(self.add_tree_item_btn) btn_box.addWidget(self.remove_tree_item_btn) btn_box_2 = QtGui.QHBoxLayout() btn_box_2.addWidget(self.load_seq_button) btn_box_2.addWidget(self.queue_button) vbox = QtGui.QVBoxLayout(self) vbox.setSpacing(6) vbox.addWidget(self.tree) vbox.addLayout(btn_box) vbox.addLayout(btn_box_2) self.setLayout(vbox) def _add_tree_item(self, *, level=None, parameter=None, sequence=None): """ Add an item to the sequence tree. An item will be added as a child to the selected (existing) item, except when level is given. :param level: An integer value determining the level at which an item is added. If level is 0, a root item will be added. :param parameter: If given, the parameter field is pre-filled :param sequence: If given, the sequence field is pre-filled """ selected = self.tree.selectedItems() if len(selected) >= 1 and level != 0: parent = selected[0] else: parent = self.tree.invisibleRootItem() if level is not None and level > 0: p_depth = self._depth_of_child(parent) while p_depth > level - 1: parent = parent.parent() p_depth = self._depth_of_child(parent) comboBox = QtGui.QComboBox() lineEdit = QtGui.QLineEdit() comboBox.addItems(list(sorted(self.names_inv.keys()))) item = QtGui.QTreeWidgetItem(parent, [""]) depth = self._depth_of_child(item) item.setText(0, "{:d}".format(depth)) self.tree.setItemWidget(item, 1, comboBox) self.tree.setItemWidget(item, 2, lineEdit) self.tree.expandAll() for selected_item in selected: selected_item.setSelected(False) if parameter is not None: idx = self.tree.itemWidget(item, 1).findText(parameter) self.tree.itemWidget(item, 1).setCurrentIndex(idx) if idx == -1: log.error( "Parameter '{}' not found while loading sequence".format( parameter) + ", probably mistyped." ) if sequence is not None: self.tree.itemWidget(item, 2).setText(sequence) item.setSelected(True) def _remove_selected_tree_item(self): """ Remove the selected item (and any child items) from the sequence tree. """ selected = self.tree.selectedItems() if len(selected) == 0: return item = selected[0] parent = item.parent() if parent is None: parent = self.tree.invisibleRootItem() parent.removeChild(item) for selected_item in self.tree.selectedItems(): selected_item.setSelected(False) parent.setSelected(True) def queue_sequence(self): """ Obtain a list of parameters from the sequence tree, enter these into procedures, and queue these procedures. """ self.queue_button.setEnabled(False) try: sequence = self._generate_sequence_from_tree() except SequenceEvaluationException: log.error("Evaluation of one of the sequence strings went wrong, no sequence queued.") else: log.info( "Queuing %d measurements based on the entered sequences." % len(sequence) ) for entry in sequence: QtGui.QApplication.processEvents() parameters = dict(ChainMap(*entry[::-1])) procedure = self._parent.make_procedure() procedure.set_parameters(parameters) self._parent.queue(procedure=procedure) finally: self.queue_button.setEnabled(True) def load_sequence(self, *, fileName=None): """ Load a sequence from a .txt file. :param fileName: Filename (string) of the to-be-loaded file. """ if fileName is None: fileName, _ = QtGui.QFileDialog.getOpenFileName(self, 'OpenFile') if len(fileName) == 0: return content = [] with open(fileName, "r") as file: content = file.readlines() pattern = re.compile("([-]+) \"(.*?)\", \"(.*?)\"") for line in content: line = line.strip() match = pattern.search(line) if not match: continue level = len(match.group(1)) - 1 if level < 0: continue parameter = match.group(2) sequence = match.group(3) self._add_tree_item( level=level, parameter=parameter, sequence=sequence, ) def _generate_sequence_from_tree(self): """ Generate a list of parameters from the sequence tree. """ iterator = QtGui.QTreeWidgetItemIterator(self.tree) sequences = [] current_sequence = [[] for i in range(self.MAXDEPTH)] temp_sequence = [[] for i in range(self.MAXDEPTH)] while iterator.value(): item = iterator.value() depth = self._depth_of_child(item) name = self.tree.itemWidget(item, 1).currentText() parameter = self.names_inv[name] values = self.eval_string( self.tree.itemWidget(item, 2).text(), name, depth, ) try: sequence_entry = [{parameter: value} for value in values] except TypeError: log.error( "TypeError, likely no sequence for one of the parameters" ) else: current_sequence[depth].extend(sequence_entry) iterator += 1 next_depth = self._depth_of_child(iterator.value()) for depth_idx in range(depth, next_depth, -1): temp_sequence[depth_idx].extend(current_sequence[depth_idx]) if depth_idx != 0: sequence_products = list(product( current_sequence[depth_idx - 1], temp_sequence[depth_idx] )) for i in range(len(sequence_products)): try: element = sequence_products[i][1] except IndexError: log.error( "IndexError, likely empty nested parameter" ) else: if isinstance(element, tuple): sequence_products[i] = ( sequence_products[i][0], *element) temp_sequence[depth_idx - 1].extend(sequence_products) temp_sequence[depth_idx] = [] current_sequence[depth_idx] = [] current_sequence[depth_idx - 1] = [] if depth == next_depth: temp_sequence[depth].extend(current_sequence[depth]) current_sequence[depth] = [] sequences = temp_sequence[0] for idx in range(len(sequences)): if not isinstance(sequences[idx], tuple): sequences[idx] = (sequences[idx],) return sequences @staticmethod def _depth_of_child(item): """ Determine the level / depth of a child item in the sequence tree. """ depth = -1 while item: item = item.parent() depth += 1 return depth @staticmethod def eval_string(string, name=None, depth=None): """ Evaluate the given string. The string is evaluated using a list of pre-defined functions that are deemed safe to use, to prevent the execution of malicious code. For this purpose, also any built-in functions or global variables are not available. :param string: String to be interpreted. :param name: Name of the to-be-interpreted string, only used for error messages. :param depth: Depth of the to-be-interpreted string, only used for error messages. """ evaluated_string = None if len(string) > 0: try: evaluated_string = eval( string, {"__builtins__": None}, SAFE_FUNCTIONS ) except TypeError: log.error("TypeError, likely a typo in one of the " + "functions for parameter '{}', depth {}".format( name, depth )) raise SequenceEvaluationException() except SyntaxError: log.error("SyntaxError, likely unbalanced brackets " + "for parameter '{}', depth {}".format(name, depth)) raise SequenceEvaluationException() except ValueError: log.error("ValueError, likely wrong function argument " + "for parameter '{}', depth {}".format(name, depth)) raise SequenceEvaluationException() else: log.error("No sequence entered for " + "for parameter '{}', depth {}".format(name, depth)) raise SequenceEvaluationException() evaluated_string = numpy.array(evaluated_string) return evaluated_string class DirectoryLineEdit(QtGui.QLineEdit): """ Widget that allows to choose a directory path. A completer is implemented for quick completion. A browse button is available. """ def __init__(self, parent=None): super().__init__(parent=parent) completer = QtGui.QCompleter(self) completer.setCompletionMode(QtGui.QCompleter.PopupCompletion) model = QtGui.QDirModel(completer) model.setFilter(QtCore.QDir.Dirs | QtCore.QDir.Drives | QtCore.QDir.NoDotAndDotDot | QtCore.QDir.AllDirs) completer.setModel(model) self.setCompleter(completer) browse_action = QtGui.QAction(self) browse_action.setIcon(self.style().standardIcon(getattr(QtGui.QStyle, 'SP_DialogOpenButton'))) browse_action.triggered.connect(self.browse_triggered) self.addAction(browse_action, QtGui.QLineEdit.TrailingPosition) def browse_triggered(self): path = QtGui.QFileDialog.getExistingDirectory(self, 'Directory', '/') if path != '': self.setText(path) PyMeasure-0.9.0/pymeasure/display/Qt.py0000664000175000017500000000421714010037617020306 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from pyqtgraph.Qt import QtGui, QtCore, loadUiType log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) QtCore.QSignal = QtCore.pyqtSignal def fromUi(*args, **kwargs): """ Returns a Qt object constructed using loadUiType based on its arguments. All QWidget objects in the form class are set in the returned object for easy accessability. """ form_class, base_class = loadUiType(*args, **kwargs) widget = base_class() form = form_class() form.setupUi(widget) form.retranslateUi(widget) for name in dir(form): element = getattr(form, name) if isinstance(element, QtGui.QWidget): setattr(widget, name, element) return widget def qt_min_version(major, minor=0): """ Check for a minimum Qt version. For example, to check for version 4.11 or later, call ``check_qt_version(4, 11)``. :return bool: True if PyQt version >= min_version """ return (QtCore.QT_VERSION >= ((major << 16) + (minor << 8))) PyMeasure-0.9.0/pymeasure/display/thread.py0000664000175000017500000000420414010037617021165 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from threading import Event from .Qt import QtCore log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class StoppableQThread(QtCore.QThread): """ Base class for QThreads which require the ability to be stopped by a thread-safe method call """ def __init__(self, parent=None): super().__init__(parent) self._should_stop = Event() self._should_stop.clear() def join(self, timeout=0): """ Joins the current thread and forces it to stop after the timeout if necessary :param timeout: Timeout duration in seconds """ self._should_stop.wait(timeout) if not self.should_stop(): self.stop() super(StoppableQThread, self).wait() def stop(self): self._should_stop.set() def should_stop(self): return self._should_stop.is_set() def __repr__(self): return "<%s(should_stop=%s)>" % ( self.__class__.__name__, self.should_stop()) PyMeasure-0.9.0/pymeasure/display/browser.py0000664000175000017500000001230514010037617021402 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from os.path import basename from .Qt import QtCore, QtGui from ..experiment import Procedure log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class BrowserItem(QtGui.QTreeWidgetItem): def __init__(self, results, curve, parent=None): super().__init__(parent) pixelmap = QtGui.QPixmap(24, 24) pixelmap.fill(curve.opts['pen'].color()) self.setIcon(0, QtGui.QIcon(pixelmap)) self.setFlags(self.flags() | QtCore.Qt.ItemIsUserCheckable) self.setCheckState(0, QtCore.Qt.Checked) self.setText(1, basename(results.data_filename)) self.setStatus(results.procedure.status) self.progressbar = QtGui.QProgressBar() self.progressbar.setRange(0, 100) self.progressbar.setValue(0) def setStatus(self, status): status_label = { Procedure.QUEUED: 'Queued', Procedure.RUNNING: 'Running', Procedure.FAILED: 'Failed', Procedure.ABORTED: 'Aborted', Procedure.FINISHED: 'Finished'} self.setText(3, status_label[status]) if status == Procedure.FAILED or status == Procedure.ABORTED: # Set progress bar color to red return # Commented this out self.progressbar.setStyleSheet(""" QProgressBar { border: 1px solid #AAAAAA; border-radius: 5px; text-align: center; } QProgressBar::chunk { background-color: red; } """) def setProgress(self, progress): self.progressbar.setValue(progress) class Browser(QtGui.QTreeWidget): """Graphical list view of :class:`Experiment` objects allowing the user to view the status of queued Experiments as well as loading and displaying data from previous runs. In order that different Experiments be displayed within the same Browser, they must have entries in `DATA_COLUMNS` corresponding to the `measured_quantities` of the Browser. """ def __init__(self, procedure_class, display_parameters, measured_quantities, sort_by_filename=False, parent=None): super().__init__(parent) self.display_parameters = display_parameters self.procedure_class = procedure_class self.measured_quantities = measured_quantities header_labels = ["Graph", "Filename", "Progress", "Status"] for parameter in self.display_parameters: header_labels.append(getattr(self.procedure_class, parameter).name) self.setColumnCount(len(header_labels)) self.setHeaderLabels(header_labels) self.setSortingEnabled(True) if sort_by_filename: self.sortItems(1, QtCore.Qt.AscendingOrder) for i, width in enumerate([80, 140]): self.header().resizeSection(i, width) def add(self, experiment): """Add a :class:`Experiment` object to the Browser. This function checks to make sure that the Experiment measures the appropriate quantities to warrant its inclusion, and then adds a BrowserItem to the Browser, filling all relevant columns with Parameter data. """ experiment_parameters = experiment.procedure.parameter_objects() experiment_parameter_names = list(experiment_parameters.keys()) for measured_quantity in self.measured_quantities: if measured_quantity not in experiment.procedure.DATA_COLUMNS: raise Exception("Procedure does not measure the" " %s quantity." % measured_quantity) # Set the relevant fields within the BrowserItem if # that Parameter is implemented item = experiment.browser_item for i, column in enumerate(self.display_parameters): if column in experiment_parameter_names: item.setText(i + 4, str(experiment_parameters[column])) self.addTopLevelItem(item) self.setItemWidget(item, 2, item.progressbar) return item PyMeasure-0.9.0/pymeasure/display/manager.py0000664000175000017500000002771614010037617021345 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from os.path import basename from .Qt import QtCore from .listeners import Monitor from ..experiment import Procedure from ..experiment.workers import Worker log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Experiment(QtCore.QObject): """ The Experiment class helps group the :class:`.Procedure`, :class:`.Results`, and their display functionality. Its function is only a convenient container. :param results: :class:`.Results` object :param curve: :class:`.ResultsCurve` object :param browser_item: :class:`.BrowserItem` object """ def __init__(self, results, curve, browser_item, parent=None): super().__init__(parent) self.results = results self.data_filename = self.results.data_filename self.procedure = self.results.procedure self.curve = curve self.browser_item = browser_item class ExperimentQueue(QtCore.QObject): """ Represents a Queue of Experiments and allows queries to be easily preformed """ def __init__(self): super().__init__() self.queue = [] def append(self, experiment): self.queue.append(experiment) def remove(self, experiment): if experiment not in self.queue: raise Exception("Attempting to remove an Experiment that is " "not in the ExperimentQueue") else: if experiment.procedure.status == Procedure.RUNNING: raise Exception("Attempting to remove a running experiment") else: self.queue.pop(self.queue.index(experiment)) def __contains__(self, value): if isinstance(value, Experiment): return value in self.queue if isinstance(value, str): for experiment in self.queue: if basename(experiment.data_filename) == basename(value): return True return False return False def __getitem__(self, key): return self.queue[key] def next(self): """ Returns the next experiment on the queue """ for experiment in self.queue: if experiment.procedure.status == Procedure.QUEUED: return experiment raise StopIteration("There are no queued experiments") def has_next(self): """ Returns True if another item is on the queue """ try: self.next() except StopIteration: return False return True def with_browser_item(self, item): for experiment in self.queue: if experiment.browser_item is item: return experiment return None class Manager(QtCore.QObject): """Controls the execution of :class:`.Experiment` classes by implementing a queue system in which Experiments are added, removed, executed, or aborted. When instantiated, the Manager is linked to a :class:`.Browser` and a PyQtGraph `PlotItem` within the user interface, which are updated in accordance with the execution status of the Experiments. """ _is_continuous = True _start_on_add = True queued = QtCore.QSignal(object) running = QtCore.QSignal(object) finished = QtCore.QSignal(object) failed = QtCore.QSignal(object) aborted = QtCore.QSignal(object) abort_returned = QtCore.QSignal(object) log = QtCore.QSignal(object) def __init__(self, plot, browser, port=5888, log_level=logging.INFO, parent=None): super().__init__(parent) self.experiments = ExperimentQueue() self._worker = None self._running_experiment = None self._monitor = None self.log_level = log_level self.plot = plot self.browser = browser self.port = port def is_running(self): """ Returns True if a procedure is currently running """ return self._running_experiment is not None def running_experiment(self): if self.is_running(): return self._running_experiment else: raise Exception("There is no Experiment running") def _update_progress(self, progress): if self.is_running(): self._running_experiment.browser_item.setProgress(progress) def _update_status(self, status): if self.is_running(): self._running_experiment.procedure.status = status self._running_experiment.browser_item.setStatus(status) def _update_log(self, record): self.log.emit(record) def load(self, experiment): """ Load a previously executed Experiment """ self.plot.addItem(experiment.curve) self.browser.add(experiment) self.experiments.append(experiment) def queue(self, experiment): """ Adds an experiment to the queue. """ self.load(experiment) self.queued.emit(experiment) if self._start_on_add and not self.is_running(): self.next() def remove(self, experiment): """ Removes an Experiment """ self.experiments.remove(experiment) self.browser.takeTopLevelItem( self.browser.indexOfTopLevelItem(experiment.browser_item)) self.plot.removeItem(experiment.curve) def clear(self): """ Remove all Experiments """ for experiment in self.experiments[:]: self.remove(experiment) def next(self): """ Initiates the start of the next experiment in the queue as long as no other experiments are currently running and there is a procedure in the queue. """ if self.is_running(): raise Exception("Another procedure is already running") else: if self.experiments.has_next(): log.debug("Manager is initiating the next experiment") experiment = self.experiments.next() self._running_experiment = experiment self._worker = Worker(experiment.results, port=self.port, log_level=self.log_level) self._monitor = Monitor(self._worker.monitor_queue) self._monitor.worker_running.connect(self._running) self._monitor.worker_failed.connect(self._failed) self._monitor.worker_abort_returned.connect(self._abort_returned) self._monitor.worker_finished.connect(self._finish) self._monitor.progress.connect(self._update_progress) self._monitor.status.connect(self._update_status) self._monitor.log.connect(self._update_log) self._monitor.start() self._worker.start() def _running(self): if self.is_running(): self.running.emit(self._running_experiment) def _clean_up(self): self._worker.join() del self._worker self._monitor.wait() del self._monitor self._worker = None self._running_experiment = None log.debug("Manager has cleaned up after the Worker") def _failed(self): log.debug("Manager's running experiment has failed") experiment = self._running_experiment self._clean_up() self.failed.emit(experiment) def _abort_returned(self): log.debug("Manager's running experiment has returned after an abort") experiment = self._running_experiment self._clean_up() self.abort_returned.emit(experiment) def _finish(self): log.debug("Manager's running experiment has finished") experiment = self._running_experiment self._clean_up() experiment.browser_item.setProgress(100.) experiment.curve.update() self.finished.emit(experiment) if self._is_continuous: # Continue running procedures self.next() def resume(self): """ Resume processing of the queue. """ self._start_on_add = True self._is_continuous = True self.next() def abort(self): """ Aborts the currently running Experiment, but raises an exception if there is no running experiment """ if not self.is_running(): raise Exception("Attempting to abort when no experiment " "is running") else: self._start_on_add = False self._is_continuous = False self._worker.stop() self.aborted.emit(self._running_experiment) class ImageExperiment(Experiment): """ Adds saving of the experiments image to :class:`.Experiment`. Needed to make image features work """ def __init__(self, results, curve, image, browser_item, parent=None): super().__init__(results, curve, browser_item, parent=None) self.image = image class ImageExperimentQueue(ExperimentQueue): """ Overwrites needed features from :class:`.ExperimentQueue` to make image features work """ def __init__(self): super().__init__() def __contains__(self, value): if isinstance(value, ImageExperiment): return value in self.queue if isinstance(value, str): for experiment in self.queue: if basename(experiment.data_filename) == basename(value): return True return False return False class ImageManager(Manager): """ Overwrites needed features from :class:`.Manager` to make image features work """ _is_continuous = True _start_on_add = True queued = QtCore.QSignal(object) running = QtCore.QSignal(object) finished = QtCore.QSignal(object) failed = QtCore.QSignal(object) aborted = QtCore.QSignal(object) abort_returned = QtCore.QSignal(object) log = QtCore.QSignal(object) def __init__(self, plot, im_plot, browser, port=5888, log_level=logging.INFO, parent=None): super().__init__(plot, browser, port=5888, log_level=logging.INFO, parent=None) # overrides necessary variables to make image features work self.experiments = ImageExperimentQueue() self.im_plot = im_plot def remove(self, experiment): """ Removes an Experiment """ self.experiments.remove(experiment) self.browser.takeTopLevelItem( self.browser.indexOfTopLevelItem(experiment.browser_item)) self.im_plot.removeItem(experiment.image) self.plot.removeItem(experiment.curve) def load(self, experiment): super().load(experiment) self.im_plot.addItem(experiment.image) def _finish(self): log.debug("Manager's running experiment has finished") experiment = self._running_experiment self._clean_up() experiment.browser_item.setProgress(100.) experiment.image.update_img() experiment.curve.update() self.finished.emit(experiment) if self._is_continuous: # Continue running procedures self.next() PyMeasure-0.9.0/pymeasure/display/inputs.py0000664000175000017500000002305314010037617021243 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import re from .Qt import QtCore, QtGui, qt_min_version log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Input(object): """ Mix-in class that connects a :mod:`Parameter <.parameters>` object to a GUI input box. :param parameter: The parameter to connect to this input box. :attr parameter: Read-only property to access the associated parameter. """ def __init__(self, parameter, **kwargs): super().__init__(**kwargs) self._parameter = None self.set_parameter(parameter) def set_parameter(self, parameter): """ Connects a new parameter to the input box, and initializes the box value. :param parameter: parameter to connect. """ self._parameter = parameter if parameter.is_set(): self.setValue(parameter.value) if hasattr(parameter, 'units') and parameter.units: self.setSuffix(" %s" % parameter.units) def update_parameter(self): """ Update the parameter value with the Input GUI element's current value. """ self._parameter.value = self.value() @property def parameter(self): """ The connected parameter object. Read-only property; see :meth:`set_parameter`. Note that reading this property will have the side-effect of updating its value from the GUI input box. """ self.update_parameter() return self._parameter class StringInput(QtGui.QLineEdit, Input): """ String input box connected to a :class:`Parameter`. Parameter subclasses that are string-based may also use this input, but non-string parameters should use more specialised input classes. """ def __init__(self, parameter, parent=None, **kwargs): if qt_min_version(5): super().__init__(parameter=parameter, parent=parent, **kwargs) else: QtGui.QLineEdit.__init__(self, parent=parent, **kwargs) Input.__init__(self, parameter) def setValue(self, value): # QtGui.QLineEdit has a setText() method instead of setValue() return super().setText(value) def setSuffix(self, value): pass def value(self): # QtGui.QLineEdit has a text() method instead of value() return super().text() class FloatInput(QtGui.QDoubleSpinBox, Input): """ Spin input box for floating-point values, connected to a :class:`FloatParameter`. .. seealso:: Class :class:`~.ScientificInput` For inputs in scientific notation. """ def __init__(self, parameter, parent=None, **kwargs): if qt_min_version(5): super().__init__(parameter=parameter, parent=parent, **kwargs) else: QtGui.QDoubleSpinBox.__init__(self, parent=parent, **kwargs) Input.__init__(self, parameter) self.setButtonSymbols(QtGui.QAbstractSpinBox.NoButtons) def set_parameter(self, parameter): # Override from :class:`Input` self.setMinimum(parameter.minimum) self.setMaximum(parameter.maximum) super().set_parameter(parameter) # default gets set here, after min/max class IntegerInput(QtGui.QSpinBox, Input): """ Spin input box for integer values, connected to a :class:`IntegerParameter`. """ def __init__(self, parameter, parent=None, **kwargs): if qt_min_version(5): super().__init__(parameter=parameter, parent=parent, **kwargs) else: QtGui.QSpinBox.__init__(self, parent=parent, **kwargs) Input.__init__(self, parameter) self.setButtonSymbols(QtGui.QAbstractSpinBox.NoButtons) def set_parameter(self, parameter): # Override from :class:`Input` self.setMinimum(parameter.minimum) self.setMaximum(parameter.maximum) super().set_parameter(parameter) # default gets set here, after min/max class BooleanInput(QtGui.QCheckBox, Input): """ Checkbox for boolean values, connected to a :class:`BooleanParameter`. """ def __init__(self, parameter, parent=None, **kwargs): if qt_min_version(5): super().__init__(parameter=parameter, parent=parent, **kwargs) else: QtGui.QCheckBox.__init__(self, parent=parent, **kwargs) Input.__init__(self, parameter) def set_parameter(self, parameter): # Override from :class:`Input` self.setText(parameter.name) super().set_parameter(parameter) def setValue(self, value): return super().setChecked(value) def setSuffix(self, value): pass def value(self): return super().isChecked() class ListInput(QtGui.QComboBox, Input): """ Dropdown for list values, connected to a :class:`ListParameter`. """ def __init__(self, parameter, parent=None, **kwargs): if qt_min_version(5): super().__init__(parameter=parameter, parent=parent, **kwargs) else: QtGui.QComboBox.__init__(self, parent=parent, **kwargs) Input.__init__(self, parameter) self._stringChoices = None self.setEditable(False) def set_parameter(self, parameter): # Override from :class:`Input` try: if hasattr(parameter, 'units') and parameter.units: suffix = " %s"%parameter.units else: suffix = "" self._stringChoices = tuple((str(choice) + suffix) for choice in parameter.choices) except TypeError: # choices is None self._stringChoices = tuple() self.clear() self.addItems(self._stringChoices) super().set_parameter(parameter) def setValue(self, value): try: index = self._parameter.choices.index(value) self.setCurrentIndex(index) except (TypeError, ValueError) as e: # no choices or choice invalid raise ValueError("Invalid choice for parameter. " "Must be one of %s" % str(self._parameter.choices)) from e def setSuffix(self, value): pass def value(self): return self._parameter.choices[self.currentIndex()] class ScientificInput(QtGui.QDoubleSpinBox, Input): """ Spinner input box for floating-point values, connected to a :class:`FloatParameter`. This box will display and accept values in scientific notation when appropriate. .. seealso:: Class :class:`~.FloatInput` For a non-scientific floating-point input box. """ def __init__(self, parameter, parent=None, **kwargs): if qt_min_version(5): super().__init__(parameter=parameter, parent=parent, **kwargs) else: QtGui.QDoubleSpinBox.__init__(self, parent, **kwargs) Input.__init__(self, parameter) self.setButtonSymbols(QtGui.QAbstractSpinBox.NoButtons) def set_parameter(self, parameter): # Override from :class:`Input` self._parameter = parameter # required before super().set_parameter # for self.validate which is called when setting self.decimals() self.validator = QtGui.QDoubleValidator( parameter.minimum, parameter.maximum, parameter.decimals, self) self.setDecimals(parameter.decimals) self.setMinimum(parameter.minimum) self.setMaximum(parameter.maximum) self.validator.setNotation(QtGui.QDoubleValidator.ScientificNotation) super().set_parameter(parameter) # default gets set here, after min/max def validate(self, text, pos): if self._parameter.units: text = text[:-(len(self._parameter.units) + 1)] result = self.validator.validate(text, pos) return result[0], result[1] + " %s" % self._parameter.units, result[2] else: return self.validator.validate(text, pos) def fixCase(self, text): self.lineEdit().setText(text.toLower()) def valueFromText(self, text): try: if self._parameter.units: return float(str(text)[:-(len(self._parameter.units) + 1)]) else: return float(str(text)) except ValueError: return self._parameter.default def textFromValue(self, value): string = "{:g}".format(value).replace("e+", "e") string = re.sub(r"e(-?)0*(\d+)", r"e\1\2", string) return string def stepEnabled(self): return QtGui.QAbstractSpinBox.StepNone PyMeasure-0.9.0/pymeasure/display/log.py0000664000175000017500000000304114010037617020475 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from logging import Handler from .Qt import QtCore log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class LogHandler(QtCore.QObject, Handler): record = QtCore.QSignal(object) def __init__(self, parent=None): QtCore.QObject.__init__(self, parent) Handler.__init__(self) def emit(self, record): self.record.emit(self.format(record)) PyMeasure-0.9.0/pymeasure/display/plotter.py0000664000175000017500000000503214010037617021407 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import sys import time from .Qt import QtGui from .windows import PlotterWindow from ..thread import StoppableThread log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Plotter(StoppableThread): """ Plotter dynamically plots data from a file through the Results object and supports error bars. .. seealso:: Tutorial :ref:`tutorial-plotterwindow` A tutorial and example on using the Plotter and PlotterWindow. """ def __init__(self, results, refresh_time=0.1): super(Plotter, self).__init__() self.results = results self.refresh_time = refresh_time def run(self): app = QtGui.QApplication(sys.argv) window = PlotterWindow(self, refresh_time=self.refresh_time) self.setup_plot(window.plot) app.aboutToQuit.connect(window.quit) window.show() app.exec_() def setup_plot(self, plot): """ This method does nothing by default, but can be overridden by the child class in order to set up custom options for the plot window, via its PlotItem_. :param plot: This window's PlotItem_ instance. .. _PlotItem: http://www.pyqtgraph.org/documentation/graphicsItems/plotitem.html """ pass def wait_for_close(self, check_time=0.1): while not self.should_stop(): time.sleep(check_time) PyMeasure-0.9.0/pymeasure/display/curves.py0000664000175000017500000002251514010037617021232 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import sys log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) import pyqtgraph as pg import numpy as np try: from matplotlib.cm import viridis except ImportError: log.warning("Matplotlib not found. Images will be greyscale") from .Qt import QtCore def _greyscale_colormap(x): """Simple greyscale colormap. Assumes x is already normalized.""" return np.array([x,x,x,1]) class ResultsCurve(pg.PlotDataItem): """ Creates a curve loaded dynamically from a file through the Results object and supports error bars. The data can be forced to fully reload on each update, useful for cases when the data is changing across the full file instead of just appending. """ def __init__(self, results, x, y, xerr=None, yerr=None, force_reload=False, **kwargs): super().__init__(**kwargs) self.results = results self.pen = kwargs.get('pen', None) self.x, self.y = x, y self.force_reload = force_reload if xerr or yerr: self._errorBars = pg.ErrorBarItem(pen=kwargs.get('pen', None)) self.xerr, self.yerr = xerr, yerr def update(self): """Updates the data by polling the results""" if self.force_reload: self.results.reload() data = self.results.data # get the current snapshot # Set x-y data self.setData(data[self.x], data[self.y]) # Set error bars if enabled at construction if hasattr(self, '_errorBars'): self._errorBars.setOpts( x=data[self.x], y=data[self.y], top=data[self.yerr], bottom=data[self.yerr], left=data[self.xerr], right=data[self.yerr], beam=max(data[self.xerr], data[self.yerr]) ) # TODO: Add method for changing x and y class ResultsImage(pg.ImageItem): """ Creates an image loaded dynamically from a file through the Results object.""" def __init__(self, results, x, y, z, force_reload=False): self.results = results self.x = x self.y = y self.z = z self.xstart = getattr(self.results.procedure, self.x + '_start') self.xend = getattr(self.results.procedure, self.x + '_end') self.xstep = getattr(self.results.procedure, self.x + '_step') self.xsize = int(np.ceil((self.xend - self.xstart) / self.xstep)) + 1 self.ystart = getattr(self.results.procedure, self.y + '_start') self.yend = getattr(self.results.procedure, self.y + '_end') self.ystep = getattr(self.results.procedure, self.y + '_step') self.ysize = int(np.ceil((self.yend - self.ystart) / self.ystep)) + 1 self.img_data = np.zeros((self.ysize,self.xsize,4)) self.force_reload = force_reload if 'matplotlib.cm' in sys.modules: self.colormap = viridis else: self.colormap = _greyscale_colormap super().__init__(image=self.img_data) # Scale and translate image so that the pixels are in the coorect # position in "data coordinates" self.scale(self.xstep, self.ystep) self.translate(int(self.xstart/self.xstep)-0.5, int(self.ystart/self.ystep)-0.5) # 0.5 so pixels centered def update_img(self): if self.force_reload: self.results.reload() data = self.results.data zmin = data[self.z].min() zmax = data[self.z].max() # populate the image array with the new data for idx, row in data.iterrows(): xdat = row[self.x] ydat = row[self.y] xidx, yidx = self.find_img_index(xdat, ydat) self.img_data[yidx,xidx,:] = self.colormap((row[self.z] - zmin)/(zmax-zmin)) # set image data, need to transpose since pyqtgraph assumes column-major order self.setImage(image=np.transpose(self.img_data,axes=(1,0,2))) def find_img_index(self, x, y): """ Finds the integer image indices corresponding to the closest x and y points of the data given some x and y data. """ indices = [self.xsize-1,self.ysize-1] # default to the final pixel if self.xstart <= x <= self.xend: # only change if within reasonable range indices[0] = self.round_up((x - self.xstart)/self.xstep) if self.ystart <= y <= self.yend: indices[1] = self.round_up((y - self.ystart)/self.ystep) return indices def round_up(self, x): """Convenience function since numpy rounds to even""" if x%1 >= 0.5: return int(x) + 1 else: return int(x) # TODO: colormap selection class BufferCurve(pg.PlotDataItem): """ Creates a curve based on a predefined buffer size and allows data to be added dynamically, in additon to supporting error bars """ data_updated = QtCore.QSignal() def __init__(self, errors=False, **kwargs): super().__init__(**kwargs) if errors: self._errorBars = pg.ErrorBarItem(pen=kwargs.get('pen', None)) self._buffer = None def prepare(self, size, dtype=np.float32): """ Prepares the buffer based on its size, data type """ if hasattr(self, '_errorBars'): self._buffer = np.empty((size, 4), dtype=dtype) else: self._buffer = np.empty((size, 2), dtype=dtype) self._ptr = 0 def append(self, x, y, xError=None, yError=None): """ Appends data to the curve with optional errors """ if self._buffer is None: raise Exception("BufferCurve buffer must be prepared") if len(self._buffer) <= self._ptr: raise Exception("BufferCurve overflow") # Set x-y data self._buffer[self._ptr, :2] = [x, y] self.setData(self._buffer[:self._ptr, :2]) # Set error bars if enabled at construction if hasattr(self, '_errorBars'): self._buffer[self._ptr, 2:] = [xError, yError] self._errorBars.setOpts( x=self._buffer[:self._ptr, 0], y=self._buffer[:self._ptr, 1], top=self._buffer[:self._ptr, 3], bottom=self._buffer[:self._ptr, 3], left=self._buffer[:self._ptr, 2], right=self._buffer[:self._ptr, 2], beam=np.max(self._buffer[:self._ptr, 2:]) ) self._ptr += 1 self.data_updated.emit() class Crosshairs(QtCore.QObject): """ Attaches crosshairs to the a plot and provides a signal with the x and y graph coordinates """ coordinates = QtCore.QSignal(float, float) def __init__(self, plot, pen=None): """ Initiates the crosshars onto a plot given the pen style. Example pen: pen=pg.mkPen(color='#AAAAAA', style=QtCore.Qt.DashLine) """ super().__init__() self.vertical = pg.InfiniteLine(angle=90, movable=False, pen=pen) self.horizontal = pg.InfiniteLine(angle=0, movable=False, pen=pen) plot.addItem(self.vertical, ignoreBounds=True) plot.addItem(self.horizontal, ignoreBounds=True) self.position = None self.proxy = pg.SignalProxy(plot.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved) self.plot = plot def hide(self): self.vertical.hide() self.horizontal.hide() def show(self): self.vertical.show() self.horizontal.show() def update(self): """ Updates the mouse position based on the data in the plot. For dynamic plots, this is called each time the data changes to ensure the x and y values correspond to those on the display. """ if self.position is not None: mouse_point = self.plot.vb.mapSceneToView(self.position) self.coordinates.emit(mouse_point.x(), mouse_point.y()) self.vertical.setPos(mouse_point.x()) self.horizontal.setPos(mouse_point.y()) def mouseMoved(self, event=None): """ Updates the mouse position upon mouse movement """ if event is not None: self.position = event[0] self.update() else: raise Exception("Mouse location not known") PyMeasure-0.9.0/pymeasure/display/__init__.py0000664000175000017500000000342514010037617021461 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) try: from .manager import Manager from .plotter import Plotter except ImportError: log.warning("Python bindings for Qt (PySide, PyQt) can not be imported") def run_in_ipython(app): """ Attempts to run the QApplication in the IPython main loop, which requires the command "%gui qt" to be run prior to the script execution. On failure the Qt main loop is initialized instead """ try: from IPython.lib.guisupport import start_event_loop_qt4 except ImportError: app.exec_() else: start_event_loop_qt4(app) PyMeasure-0.9.0/pymeasure/display/windows.py0000664000175000017500000011031414010037617021410 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import os import subprocess, platform import pyqtgraph as pg from .browser import BrowserItem from .curves import ResultsCurve from .manager import Manager, Experiment, ImageExperiment, ImageManager from .Qt import QtCore, QtGui from .widgets import ( PlotWidget, BrowserWidget, InputsWidget, LogWidget, ResultsDialog, SequencerWidget, ImageWidget, DirectoryLineEdit, ) from ..experiment.results import Results log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class PlotterWindow(QtGui.QMainWindow): """ A window for plotting experiment results. Should not be instantiated directly, but only via the :class:`~pymeasure.display.plotter.Plotter` class. .. seealso:: Tutorial :ref:`tutorial-plotterwindow` A tutorial and example code for using the Plotter and PlotterWindow. .. attribute plot:: The `pyqtgraph.PlotItem`_ object for this window. Can be accessed to further customise the plot view programmatically, e.g., display log-log or semi-log axes by default, change axis range, etc. .. pyqtgraph.PlotItem: http://www.pyqtgraph.org/documentation/graphicsItems/plotitem.html """ def __init__(self, plotter, refresh_time=0.1, parent=None): super().__init__(parent) self.plotter = plotter self.refresh_time = refresh_time columns = plotter.results.procedure.DATA_COLUMNS self.setWindowTitle('Results Plotter') self.main = QtGui.QWidget(self) vbox = QtGui.QVBoxLayout(self.main) vbox.setSpacing(0) hbox = QtGui.QHBoxLayout() hbox.setSpacing(6) hbox.setContentsMargins(-1, 6, -1, -1) file_label = QtGui.QLabel(self.main) file_label.setText('Data Filename:') self.file = QtGui.QLineEdit(self.main) self.file.setText(plotter.results.data_filename) hbox.addWidget(file_label) hbox.addWidget(self.file) vbox.addLayout(hbox) self.plot_widget = PlotWidget(columns, refresh_time=self.refresh_time, check_status=False) self.plot = self.plot_widget.plot vbox.addWidget(self.plot_widget) self.main.setLayout(vbox) self.setCentralWidget(self.main) self.main.show() self.resize(800, 600) self.curve = ResultsCurve(plotter.results, columns[0], columns[1], pen=pg.mkPen(color=pg.intColor(0), width=2), antialias=False) self.plot.addItem(self.curve) self.plot_widget.updated.connect(self.check_stop) def quit(self, evt=None): log.info("Quitting the Plotter") self.close() self.plotter.stop() def check_stop(self): """ Checks if the Plotter should stop and exits the Qt main loop if so """ if self.plotter.should_stop(): QtCore.QCoreApplication.instance().quit() class ManagedWindow(QtGui.QMainWindow): """ Abstract base class. The ManagedWindow provides an interface for inputting experiment parameters, running several experiments (:class:`~pymeasure.experiment.procedure.Procedure`), plotting result curves, and listing the experiments conducted during a session. The ManagedWindow uses a Manager to control Workers in a Queue, and provides a simple interface. The :meth:`~.queue` method must be overridden by the child class. .. seealso:: Tutorial :ref:`tutorial-managedwindow` A tutorial and example on the basic configuration and usage of ManagedWindow. .. attribute:: plot The `pyqtgraph.PlotItem`_ object for this window. Can be accessed to further customise the plot view programmatically, e.g., display log-log or semi-log axes by default, change axis range, etc. .. _pyqtgraph.PlotItem: http://www.pyqtgraph.org/documentation/graphicsItems/plotitem.html """ def __init__(self, procedure_class, inputs=(), displays=(), x_axis=None, y_axis=None, log_channel='', log_level=logging.INFO, parent=None, sequencer=False, sequencer_inputs=None, sequence_file=None, inputs_in_scrollarea=False, directory_input=False): super().__init__(parent) app = QtCore.QCoreApplication.instance() app.aboutToQuit.connect(self.quit) self.procedure_class = procedure_class self.inputs = inputs self.displays = displays self.use_sequencer = sequencer self.sequencer_inputs = sequencer_inputs self.sequence_file = sequence_file self.inputs_in_scrollarea = inputs_in_scrollarea self.directory_input = directory_input self.log = logging.getLogger(log_channel) self.log_level = log_level log.setLevel(log_level) self.log.setLevel(log_level) self.x_axis, self.y_axis = x_axis, y_axis self._setup_ui() self._layout() self.setup_plot(self.plot) def _setup_ui(self): self.log_widget = LogWidget() self.log.addHandler(self.log_widget.handler) # needs to be in Qt context? log.info("ManagedWindow connected to logging") if self.directory_input: self.directory_label = QtGui.QLabel(self) self.directory_label.setText('Directory') self.directory_line = DirectoryLineEdit(parent=self) self.queue_button = QtGui.QPushButton('Queue', self) self.queue_button.clicked.connect(self.queue) self.abort_button = QtGui.QPushButton('Abort', self) self.abort_button.setEnabled(False) self.abort_button.clicked.connect(self.abort) self.plot_widget = PlotWidget(self.procedure_class.DATA_COLUMNS, self.x_axis, self.y_axis) self.plot = self.plot_widget.plot self.browser_widget = BrowserWidget( self.procedure_class, self.displays, [self.x_axis, self.y_axis], parent=self ) self.browser_widget.show_button.clicked.connect(self.show_experiments) self.browser_widget.hide_button.clicked.connect(self.hide_experiments) self.browser_widget.clear_button.clicked.connect(self.clear_experiments) self.browser_widget.open_button.clicked.connect(self.open_experiment) self.browser = self.browser_widget.browser self.browser.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.browser.customContextMenuRequested.connect(self.browser_item_menu) self.browser.itemChanged.connect(self.browser_item_changed) self.inputs = InputsWidget( self.procedure_class, self.inputs, parent=self ) self.manager = Manager(self.plot, self.browser, log_level=self.log_level, parent=self) self.manager.abort_returned.connect(self.abort_returned) self.manager.queued.connect(self.queued) self.manager.running.connect(self.running) self.manager.finished.connect(self.finished) self.manager.log.connect(self.log.handle) if self.use_sequencer: self.sequencer = SequencerWidget( self.sequencer_inputs, self.sequence_file, parent=self ) def _layout(self): self.main = QtGui.QWidget(self) inputs_dock = QtGui.QWidget(self) inputs_vbox = QtGui.QVBoxLayout(self.main) hbox = QtGui.QHBoxLayout() hbox.setSpacing(10) hbox.setContentsMargins(-1, 6, -1, 6) hbox.addWidget(self.queue_button) hbox.addWidget(self.abort_button) hbox.addStretch() if self.directory_input: vbox = QtGui.QVBoxLayout() vbox.addWidget(self.directory_label) vbox.addWidget(self.directory_line) vbox.addLayout(hbox) if self.inputs_in_scrollarea: inputs_scroll = QtGui.QScrollArea() inputs_scroll.setWidgetResizable(True) inputs_scroll.setFrameStyle(QtGui.QScrollArea.NoFrame) self.inputs.setSizePolicy(1, 0) inputs_scroll.setWidget(self.inputs) inputs_vbox.addWidget(inputs_scroll, 1) else: inputs_vbox.addWidget(self.inputs) if self.directory_input: inputs_vbox.addLayout(vbox) else: inputs_vbox.addLayout(hbox) inputs_vbox.addStretch(0) inputs_dock.setLayout(inputs_vbox) dock = QtGui.QDockWidget('Input Parameters') dock.setWidget(inputs_dock) dock.setFeatures(QtGui.QDockWidget.NoDockWidgetFeatures) self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock) if self.use_sequencer: sequencer_dock = QtGui.QDockWidget('Sequencer') sequencer_dock.setWidget(self.sequencer) sequencer_dock.setFeatures(QtGui.QDockWidget.NoDockWidgetFeatures) self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, sequencer_dock) tabs = QtGui.QTabWidget(self.main) tabs.addTab(self.plot_widget, "Results Graph") tabs.addTab(self.log_widget, "Experiment Log") splitter = QtGui.QSplitter(QtCore.Qt.Vertical) splitter.addWidget(tabs) splitter.addWidget(self.browser_widget) self.plot_widget.setMinimumSize(100, 200) vbox = QtGui.QVBoxLayout(self.main) vbox.setSpacing(0) vbox.addWidget(splitter) self.main.setLayout(vbox) self.setCentralWidget(self.main) self.main.show() self.resize(1000, 800) def quit(self, evt=None): if self.manager.is_running(): self.abort() self.close() def browser_item_changed(self, item, column): if column == 0: state = item.checkState(0) experiment = self.manager.experiments.with_browser_item(item) if state == 0: self.plot.removeItem(experiment.curve) else: experiment.curve.x = self.plot_widget.plot_frame.x_axis experiment.curve.y = self.plot_widget.plot_frame.y_axis experiment.curve.update() self.plot.addItem(experiment.curve) def browser_item_menu(self, position): item = self.browser.itemAt(position) if item is not None: experiment = self.manager.experiments.with_browser_item(item) menu = QtGui.QMenu(self) # Open action_open = QtGui.QAction(menu) action_open.setText("Open Data Externally") action_open.triggered.connect( lambda: self.open_file_externally(experiment.results.data_filename)) menu.addAction(action_open) # Change Color action_change_color = QtGui.QAction(menu) action_change_color.setText("Change Color") action_change_color.triggered.connect( lambda: self.change_color(experiment)) menu.addAction(action_change_color) # Remove action_remove = QtGui.QAction(menu) action_remove.setText("Remove Graph") if self.manager.is_running(): if self.manager.running_experiment() == experiment: # Experiment running action_remove.setEnabled(False) action_remove.triggered.connect(lambda: self.remove_experiment(experiment)) menu.addAction(action_remove) # Use parameters action_use = QtGui.QAction(menu) action_use.setText("Use These Parameters") action_use.triggered.connect( lambda: self.set_parameters(experiment.procedure.parameter_objects())) menu.addAction(action_use) menu.exec_(self.browser.viewport().mapToGlobal(position)) def remove_experiment(self, experiment): reply = QtGui.QMessageBox.question(self, 'Remove Graph', "Are you sure you want to remove the graph?", QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) if reply == QtGui.QMessageBox.Yes: self.manager.remove(experiment) def show_experiments(self): root = self.browser.invisibleRootItem() for i in range(root.childCount()): item = root.child(i) item.setCheckState(0, QtCore.Qt.Checked) def hide_experiments(self): root = self.browser.invisibleRootItem() for i in range(root.childCount()): item = root.child(i) item.setCheckState(0, QtCore.Qt.Unchecked) def clear_experiments(self): self.manager.clear() def open_experiment(self): dialog = ResultsDialog(self.procedure_class.DATA_COLUMNS, self.x_axis, self.y_axis) if dialog.exec_(): filenames = dialog.selectedFiles() for filename in map(str, filenames): if filename in self.manager.experiments: QtGui.QMessageBox.warning(self, "Load Error", "The file %s cannot be opened twice." % os.path.basename( filename)) elif filename == '': return else: results = Results.load(filename) experiment = self.new_experiment(results) experiment.curve.update() experiment.browser_item.progressbar.setValue(100.) self.manager.load(experiment) log.info('Opened data file %s' % filename) def change_color(self, experiment): color = QtGui.QColorDialog.getColor( initial=experiment.curve.opts['pen'].color(), parent=self) if color.isValid(): pixelmap = QtGui.QPixmap(24, 24) pixelmap.fill(color) experiment.browser_item.setIcon(0, QtGui.QIcon(pixelmap)) experiment.curve.setPen(pg.mkPen(color=color, width=2)) def open_file_externally(self, filename): """ Method to open the datafile using an external editor or viewer. Uses the default application to open a datafile of this filetype, but can be overridden by the child class in order to open the file in another application of choice. """ system = platform.system() if (system == 'Windows'): # The empty argument after the start is needed to be able to cope correctly with filenames with spaces proc = subprocess.Popen(['start', '', filename], shell=True) elif (system == 'Linux'): proc = subprocess.Popen(['xdg-open', filename]) elif (system == 'Darwin'): proc = subprocess.Popen(['open', filename]) else: raise Exception("{cls} method open_file_externally does not support {system} OS".format(cls=type(self).__name__,system=system)) def make_procedure(self): if not isinstance(self.inputs, InputsWidget): raise Exception("ManagedWindow can not make a Procedure" " without a InputsWidget type") return self.inputs.get_procedure() def new_curve(self, results, color=None, **kwargs): if color is None: color = pg.intColor(self.browser.topLevelItemCount() % 8) return self.plot_widget.new_curve(results, color=color, **kwargs) def new_experiment(self, results, curve=None): if curve is None: curve = self.new_curve(results) browser_item = BrowserItem(results, curve) return Experiment(results, curve, browser_item) def set_parameters(self, parameters): """ This method should be overwritten by the child class. The parameters argument is a dictionary of Parameter objects. The Parameters should overwrite the GUI values so that a user can click "Queue" to capture the same parameters. """ if not isinstance(self.inputs, InputsWidget): raise Exception("ManagedWindow can not set parameters" " without a InputsWidget") self.inputs.set_parameters(parameters) def queue(self): """ Abstract method, which must be overridden by the child class. Implementations must call ``self.manager.queue(experiment)`` and pass an ``experiment`` (:class:`~pymeasure.experiment.experiment.Experiment`) object which contains the :class:`~pymeasure.experiment.results.Results` and :class:`~pymeasure.experiment.procedure.Procedure` to be run. For example: .. code-block:: python def queue(self): filename = unique_filename('results', prefix="data") # from pymeasure.experiment procedure = self.make_procedure() # Procedure class was passed at construction results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) """ raise NotImplementedError( "Abstract method ManagedWindow.queue not implemented") def setup_plot(self, plot): """ This method does nothing by default, but can be overridden by the child class in order to set up custom options for the plot This method is called during the constructor, after all other set up has been completed, and is provided as a convenience method to parallel Plotter. :param plot: This window's PlotItem instance. .. _PlotItem: http://www.pyqtgraph.org/documentation/graphicsItems/plotitem.html """ pass def abort(self): self.abort_button.setEnabled(False) self.abort_button.setText("Resume") self.abort_button.clicked.disconnect() self.abort_button.clicked.connect(self.resume) try: self.manager.abort() except: log.error('Failed to abort experiment', exc_info=True) self.abort_button.setText("Abort") self.abort_button.clicked.disconnect() self.abort_button.clicked.connect(self.abort) def resume(self): self.abort_button.setText("Abort") self.abort_button.clicked.disconnect() self.abort_button.clicked.connect(self.abort) if self.manager.experiments.has_next(): self.manager.resume() else: self.abort_button.setEnabled(False) def queued(self, experiment): self.abort_button.setEnabled(True) self.browser_widget.show_button.setEnabled(True) self.browser_widget.hide_button.setEnabled(True) self.browser_widget.clear_button.setEnabled(True) def running(self, experiment): self.browser_widget.clear_button.setEnabled(False) def abort_returned(self, experiment): if self.manager.experiments.has_next(): self.abort_button.setText("Resume") self.abort_button.setEnabled(True) else: self.browser_widget.clear_button.setEnabled(True) def finished(self, experiment): if not self.manager.experiments.has_next(): self.abort_button.setEnabled(False) self.browser_widget.clear_button.setEnabled(True) @property def directory(self): if not self.directory_input: raise ValueError("No directory input in the ManagedWindow") return self.directory_line.text() # TODO: Inheret from ManagedWindow to share code and features class ManagedImageWindow(QtGui.QMainWindow): """ Abstract base class. The MangedImageWindow provides an interface for inputting experiment parameters, running several experiments (:class:`~pymeasure.experiment.procedure.Procedure`), plotting result curves, and listing the experiments conducted during a session. The MangedImageWindow uses a Manager to control Workers in a Queue, and provides a simple interface. The :meth:`~.queue` method must be overridden by the child class. .. seealso:: Tutorial :ref:`tutorial-managedwindow` A tutorial and example on the basic configuration and usage of MangedImageWindow. .. attribute:: plot The `pyqtgraph.PlotItem`_ object for this window. Can be accessed to further customise the plot view programmatically, e.g., display log-log or semi-log axes by default, change axis range, etc. .. _pyqtgraph.PlotItem: http://www.pyqtgraph.org/documentation/graphicsItems/plotitem.html """ def __init__(self, procedure_class, x_axis, y_axis, z_axis=None, inputs=(), displays=(), log_channel='', log_level=logging.INFO, parent=None): super().__init__(parent) app = QtCore.QCoreApplication.instance() app.aboutToQuit.connect(self.quit) self.procedure_class = procedure_class self.inputs = inputs self.displays = displays self.log = logging.getLogger(log_channel) self.log_level = log_level log.setLevel(log_level) self.log.setLevel(log_level) self.x_axis, self.y_axis, self.z_axis = x_axis, y_axis, z_axis self._setup_ui() self._layout() self.setup_im_plot(self.im_plot) self.setup_plot(self.plot) def _setup_ui(self): self.log_widget = LogWidget() self.log.addHandler(self.log_widget.handler) # needs to be in Qt context? log.info("ManagedWindow connected to logging") self.queue_button = QtGui.QPushButton('Queue', self) self.queue_button.clicked.connect(self.queue) self.abort_button = QtGui.QPushButton('Abort', self) self.abort_button.setEnabled(False) self.abort_button.clicked.connect(self.abort) self.image_widget = ImageWidget(self.procedure_class.DATA_COLUMNS, self.x_axis, self.y_axis, self.z_axis) self.plot_widget = PlotWidget(self.procedure_class.DATA_COLUMNS, self.x_axis, self.y_axis) self.im_plot = self.image_widget.plot self.plot = self.plot_widget.plot self.browser_widget = BrowserWidget( self.procedure_class, self.displays, [self.x_axis, self.y_axis], parent=self ) self.browser_widget.show_button.clicked.connect(self.show_experiments) self.browser_widget.hide_button.clicked.connect(self.hide_experiments) self.browser_widget.clear_button.clicked.connect(self.clear_experiments) self.browser_widget.open_button.clicked.connect(self.open_experiment) self.browser = self.browser_widget.browser self.browser.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.browser.customContextMenuRequested.connect(self.browser_item_menu) self.browser.itemChanged.connect(self.browser_item_changed) self.inputs = InputsWidget( self.procedure_class, self.inputs, parent=self ) self.manager = ImageManager(self.plot, self.im_plot, self.browser, log_level=self.log_level, parent=self) self.manager.abort_returned.connect(self.abort_returned) self.manager.queued.connect(self.queued) self.manager.running.connect(self.running) self.manager.finished.connect(self.finished) self.manager.log.connect(self.log.handle) def _layout(self): self.main = QtGui.QWidget(self) inputs_dock = QtGui.QWidget(self) inputs_vbox = QtGui.QVBoxLayout(self.main) hbox = QtGui.QHBoxLayout() hbox.setSpacing(10) hbox.setContentsMargins(-1, 6, -1, 6) hbox.addWidget(self.queue_button) hbox.addWidget(self.abort_button) hbox.addStretch() inputs_vbox.addWidget(self.inputs) inputs_vbox.addLayout(hbox) inputs_vbox.addStretch() inputs_dock.setLayout(inputs_vbox) dock = QtGui.QDockWidget('Input Parameters') dock.setWidget(inputs_dock) dock.setFeatures(QtGui.QDockWidget.NoDockWidgetFeatures) self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock) tabs = QtGui.QTabWidget(self.main) tabs.addTab(self.image_widget, "Results Image") tabs.addTab(self.plot_widget, "Results Graph") tabs.addTab(self.log_widget, "Experiment Log") splitter = QtGui.QSplitter(QtCore.Qt.Vertical) splitter.addWidget(tabs) splitter.addWidget(self.browser_widget) self.image_widget.setMinimumSize(100, 200) self.plot_widget.setMinimumSize(100, 200) vbox = QtGui.QVBoxLayout(self.main) vbox.setSpacing(0) vbox.addWidget(splitter) self.main.setLayout(vbox) self.setCentralWidget(self.main) self.main.show() self.resize(1000, 800) def quit(self, evt=None): if self.manager.is_running(): self.abort() self.close() def browser_item_changed(self, item, column): if column == 0: state = item.checkState(0) experiment = self.manager.experiments.with_browser_item(item) if state == 0: self.im_plot.removeItem(experiment.image) self.plot.removeItem(experiment.curve) # QUESTION: will this work?? probably need to modify experiment else: # add regular plot experiment.curve.x = self.plot_widget.plot_frame.x_axis experiment.curve.y = self.plot_widget.plot_frame.y_axis experiment.curve.update() self.plot.addItem(experiment.curve) # add/update image plot experiment.image.update_img() self.im_plot.addItem(experiment.image) def browser_item_menu(self, position): item = self.browser.itemAt(position) if item is not None: experiment = self.manager.experiments.with_browser_item(item) menu = QtGui.QMenu(self) # Open action_open = QtGui.QAction(menu) action_open.setText("Open Data Externally") action_open.triggered.connect( lambda: self.open_file_externally(experiment.results.data_filename)) menu.addAction(action_open) # Change Color action_change_color = QtGui.QAction(menu) action_change_color.setText("Change Color") action_change_color.triggered.connect( lambda: self.change_color(experiment)) menu.addAction(action_change_color) # Remove action_remove = QtGui.QAction(menu) action_remove.setText("Remove Graph") if self.manager.is_running(): if self.manager.running_experiment() == experiment: # Experiment running action_remove.setEnabled(False) action_remove.triggered.connect(lambda: self.remove_experiment(experiment)) menu.addAction(action_remove) # Use parameters action_use = QtGui.QAction(menu) action_use.setText("Use These Parameters") action_use.triggered.connect( lambda: self.set_parameters(experiment.procedure.parameter_objects())) menu.addAction(action_use) menu.exec_(self.browser.viewport().mapToGlobal(position)) def remove_experiment(self, experiment): reply = QtGui.QMessageBox.question(self, 'Remove Graph', "Are you sure you want to remove the graph?", QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) if reply == QtGui.QMessageBox.Yes: self.manager.remove(experiment) def show_experiments(self): root = self.browser.invisibleRootItem() for i in range(root.childCount()): item = root.child(i) item.setCheckState(0, QtCore.Qt.Checked) def hide_experiments(self): root = self.browser.invisibleRootItem() for i in range(root.childCount()): item = root.child(i) item.setCheckState(0, QtCore.Qt.Unchecked) def clear_experiments(self): self.manager.clear() def open_experiment(self): dialog = ResultsDialog(self.procedure_class.DATA_COLUMNS, self.x_axis, self.y_axis) if dialog.exec_(): filenames = dialog.selectedFiles() for filename in map(str, filenames): if filename in self.manager.experiments: QtGui.QMessageBox.warning(self, "Load Error", "The file %s cannot be opened twice." % os.path.basename( filename)) elif filename == '': return else: results = Results.load(filename) experiment = self.new_experiment(results) experiment.curve.update() # QUESTION: will this work? experiment.image.update_img() experiment.browser_item.progressbar.setValue(100.) self.manager.load(experiment) log.info('Opened data file %s' % filename) def change_color(self, experiment): color = QtGui.QColorDialog.getColor( initial=experiment.curve.opts['pen'].color(), parent=self) if color.isValid(): pixelmap = QtGui.QPixmap(24, 24) pixelmap.fill(color) experiment.browser_item.setIcon(0, QtGui.QIcon(pixelmap)) experiment.curve.setPen(pg.mkPen(color=color, width=2)) def open_file_externally(self, filename): """ Method to open the datafile using an external editor or viewer. Uses the default application to open a datafile of this filetype, but can be overridden by the child class in order to open the file in another application of choice. """ system = platform.system() if (system == 'Windows'): # The empty argument after the start is needed to be able to cope correctly with filenames with spaces proc = subprocess.Popen(['start', '', filename], shell=True) elif (system == 'Linux'): proc = subprocess.Popen(['xdg-open', filename]) elif (system == 'Darwin'): proc = subprocess.Popen(['open', filename]) else: raise Exception("{cls} method open_file_externally does not support {system} OS".format(cls=type(self).__name__,system=system)) def make_procedure(self): if not isinstance(self.inputs, InputsWidget): raise Exception("ManagedWindow can not make a Procedure" " without an InputsWidget type") return self.inputs.get_procedure() def new_curve(self, results, color=None, **kwargs): if color is None: color = pg.intColor(self.browser.topLevelItemCount() % 8) return self.plot_widget.new_curve(results, color=color, **kwargs) def new_image(self, results, **kwargs): return self.image_widget.new_image(results, **kwargs) # TODO: make shure whatever calls this can supply both if needed def new_experiment(self, results, image=None, curve=None): if image is None: image = self.new_image(results) if curve is None: curve = self.new_curve(results) browser_item = BrowserItem(results, curve) return ImageExperiment(results, curve, image, browser_item) def set_parameters(self, parameters): """ This method should be overwritten by the child class. The parameters argument is a dictionary of Parameter objects. The Parameters should overwrite the GUI values so that a user can click "Queue" to capture the same parameters. """ if not isinstance(self.inputs, InputsWidget): raise Exception("ManagedWindow can not set parameters" " without an InputsWidget") self.inputs.set_parameters(parameters) def queue(self): """ Abstract method, which must be overridden by the child class. Implementations must call ``self.manager.queue(experiment)`` and pass an ``experiment`` (:class:`~pymeasure.experiment.experiment.Experiment`) object which contains the :class:`~pymeasure.experiment.results.Results` and :class:`~pymeasure.experiment.procedure.Procedure` to be run. For example: .. code-block:: python def queue(self): filename = unique_filename('results', prefix="data") # from pymeasure.experiment procedure = self.make_procedure() # Procedure class was passed at construction results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) """ raise NotImplementedError( "Abstract method ManagedWindow.queue not implemented") def setup_plot(self, plot): """ This method does nothing by default, but can be overridden by the child class in order to set up custom options for the plot This method is called during the constructor, after all other set up has been completed, and is provided as a convenience method to parallel Plotter. :param plot: This window's PlotItem instance. .. _PlotItem: http://www.pyqtgraph.org/documentation/graphicsItems/plotitem.html """ pass def setup_im_plot(self, im_plot): """ This method does nothing by default, but can be overridden by the child class in order to set up custom options for the image plot This method is called during the constructor, after all other set up has been completed, and is provided as a convenience method to parallel Plotter. :param im_plot: This window's ImageItem instance. """ pass def abort(self): self.abort_button.setEnabled(False) self.abort_button.setText("Resume") self.abort_button.clicked.disconnect() self.abort_button.clicked.connect(self.resume) try: self.manager.abort() except: log.error('Failed to abort experiment', exc_info=True) self.abort_button.setText("Abort") self.abort_button.clicked.disconnect() self.abort_button.clicked.connect(self.abort) def resume(self): self.abort_button.setText("Abort") self.abort_button.clicked.disconnect() self.abort_button.clicked.connect(self.abort) if self.manager.experiments.has_next(): self.manager.resume() else: self.abort_button.setEnabled(False) def queued(self, experiment): self.abort_button.setEnabled(True) self.browser_widget.show_button.setEnabled(True) self.browser_widget.hide_button.setEnabled(True) self.browser_widget.clear_button.setEnabled(True) def running(self, experiment): self.browser_widget.clear_button.setEnabled(False) def abort_returned(self, experiment): if self.manager.experiments.has_next(): self.abort_button.setText("Resume") self.abort_button.setEnabled(True) else: self.browser_widget.clear_button.setEnabled(True) def finished(self, experiment): if not self.manager.experiments.has_next(): self.abort_button.setEnabled(False) self.browser_widget.clear_button.setEnabled(True) PyMeasure-0.9.0/pymeasure/display/listeners.py0000664000175000017500000001054514010037617021733 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from .Qt import QtCore from .thread import StoppableQThread from ..experiment.procedure import Procedure log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) try: import zmq import cloudpickle except ImportError: zmq = None cloudpickle = None log.warning("ZMQ and cloudpickle are required for TCP communication") class QListener(StoppableQThread): """Base class for QThreads that need to listen for messages on a ZMQ TCP port and can be stopped by a thread- and process-safe method call """ def __init__(self, port, topic='', timeout=0.01): """ Constructs the Listener object with a subscriber port over which to listen for messages :param port: TCP port to listen on :param topic: Topic to listen on :param timeout: Timeout in seconds to recheck stop flag """ super().__init__() self.port = port self.topic = topic self.context = zmq.Context() log.debug("%s has ZMQ Context: %r" % (self.__class__.__name__, self.context)) self.subscriber = self.context.socket(zmq.SUB) self.subscriber.connect('tcp://localhost:%d' % port) self.subscriber.setsockopt(zmq.SUBSCRIBE, topic.encode()) log.info("%s connected to '%s' topic on tcp://localhost:%d" % ( self.__class__.__name__, topic, port)) self.poller = zmq.Poller() self.poller.register(self.subscriber, zmq.POLLIN) self.timeout = timeout def receive(self, flags=0): topic, record = self.subscriber.recv_serialized(deserialize=cloudpickle.loads, flags=flags) return topic, record def message_waiting(self): return self.poller.poll(self.timeout) def __repr__(self): return "<%s(port=%s,topic=%s,should_stop=%s)>" % ( self.__class__.__name__, self.port, self.topic, self.should_stop()) class Monitor(QtCore.QThread): """ Monitor listens for status and progress messages from a Worker through a queue to ensure no messages are losts """ status = QtCore.QSignal(int) progress = QtCore.QSignal(float) log = QtCore.QSignal(object) worker_running = QtCore.QSignal() worker_failed = QtCore.QSignal() worker_finished = QtCore.QSignal() # Distinguished from QThread.finished worker_abort_returned = QtCore.QSignal() def __init__(self, queue): super().__init__() self.queue = queue def run(self): while True: data = self.queue.get() if data is None: break topic, data = data if topic == 'status': self.status.emit(data) if data == Procedure.RUNNING: self.worker_running.emit() elif data == Procedure.FAILED: self.worker_failed.emit() elif data == Procedure.FINISHED: self.worker_finished.emit() elif data == Procedure.ABORTED: self.worker_abort_returned.emit() elif topic == 'progress': self.progress.emit(data) elif topic == 'log': self.log.emit(data) log.info("Monitor caught stop command") PyMeasure-0.9.0/pymeasure/experiment/0000775000175000017500000000000014010046235020053 5ustar colincolin00000000000000PyMeasure-0.9.0/pymeasure/experiment/experiment.py0000664000175000017500000002311014010046171022601 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging log = logging.getLogger() log.addHandler(logging.NullHandler()) try: from IPython import display except ImportError: log.warning("IPython could not be imported") from .results import unique_filename from .config import get_config, set_mpl_rcparams from pymeasure.log import setup_logging, console_log from pymeasure.experiment import Results, Worker from .parameters import Measurable import time, signal import numpy as np import pandas as pd import tempfile import gc def get_array(start, stop, step): """Returns a numpy array from start to stop""" step = np.sign(stop - start) * abs(step) return np.arange(start, stop + step, step) def get_array_steps(start, stop, numsteps): """Returns a numpy array from start to stop in numsteps""" return get_array(start, stop, (abs(stop - start) / numsteps)) def get_array_zero(maxval, step): """Returns a numpy array from 0 to maxval to -maxval to 0""" return np.concatenate((np.arange(0, maxval, step), np.arange(maxval, -maxval, -step), np.arange(-maxval, 0, step))) def create_filename(title): """ Create a new filename according to the style defined in the config file. If no config is specified, create a temporary file. """ config = get_config() if 'Filename' in config._sections.keys(): filename = unique_filename(suffix='_%s' % title, **config._sections['Filename']) else: filename = tempfile.mktemp() return filename class Experiment(object): """ Class which starts logging and creates/runs the results and worker processes. .. code-block:: python procedure = Procedure() experiment = Experiment(title, procedure) experiment.start() experiment.plot_live('x', 'y', style='.-') for a multi-subplot graph: import pylab as pl ax1 = pl.subplot(121) experiment.plot('x','y',ax=ax1) ax2 = pl.subplot(122) experiment.plot('x','z',ax=ax2) experiment.plot_live() :var value: The value of the parameter :param title: The experiment title :param procedure: The procedure object :param analyse: Post-analysis function, which takes a pandas dataframe as input and returns it with added (analysed) columns. The analysed results are accessible via experiment.data, as opposed to experiment.results.data for the 'raw' data. :param _data_timeout: Time limit for how long live plotting should wait for datapoints. """ def __init__(self, title, procedure, analyse=(lambda x: x)): self.title = title self.procedure = procedure self.measlist = [] self.port = 5888 self.plots = [] self.figs = [] self._data = [] self.analyse = analyse self._data_timeout = 10 config = get_config() set_mpl_rcparams(config) if 'Logging' in config._sections.keys(): self.scribe = setup_logging(log, **config._sections['Logging']) else: self.scribe = console_log(log) self.scribe.start() self.filename = create_filename(self.title) log.info("Using data file: %s" % self.filename) self.results = Results(self.procedure, self.filename) log.info("Set up Results") self.worker = Worker(self.results, self.scribe.queue, logging.DEBUG) log.info("Create worker") def start(self): """Start the worker""" log.info("Starting worker...") self.worker.start() @property def data(self): """Data property which returns analysed data, if an analyse function is defined, otherwise returns the raw data.""" self._data = self.analyse(self.results.data.copy()) return self._data def wait_for_data(self): """Wait for the data attribute to fill with datapoints.""" t = time.time() while self.data.empty: time.sleep(.1) if (time.time() - t) > self._data_timeout: log.warning('Timeout, no data received for liveplot') return False return True def plot_live(self, *args, **kwargs): """Live plotting loop for jupyter notebook, which automatically updates (an) in-line matplotlib graph(s). Will create a new plot as specified by input arguments, or will update (an) existing plot(s).""" if self.wait_for_data(): if not (self.plots): self.plot(*args, **kwargs) while not self.worker.should_stop(): self.update_plot() display.clear_output(wait=True) if self.worker.is_alive(): self.worker.terminate() self.scribe.stop() def plot(self, *args, **kwargs): """Plot the results from the experiment.data pandas dataframe. Store the plots in a plots list attribute.""" if self.wait_for_data(): kwargs['title'] = self.title ax = self.data.plot(*args, **kwargs) self.plots.append({'type': 'plot', 'args': args, 'kwargs': kwargs, 'ax': ax}) if ax.get_figure() not in self.figs: self.figs.append(ax.get_figure()) self._user_interrupt = False def clear_plot(self): """Clear the figures and plot lists.""" for fig in self.figs: fig.clf() for pl in self.plots: pl.close() self.figs = [] self.plots = [] gc.collect() def update_plot(self): """Update the plots in the plots list with new data from the experiment.data pandas dataframe.""" try: tasks = [] self.data for plot in self.plots: ax = plot['ax'] if plot['type'] == 'plot': x, y = plot['args'][0], plot['args'][1] if type(y) == str: y = [y] for yname, line in zip(y, ax.lines): self.update_line(ax, line, x, yname) if plot['type'] == 'pcolor': x, y, z = plot['x'], plot['y'], plot['z'] self.update_pcolor(ax, x, y, z) display.clear_output(wait=True) display.display(*self.figs) time.sleep(0.1) except KeyboardInterrupt: display.clear_output(wait=True) display.display(*self.figs) self._user_interrupt = True def pcolor(self, xname, yname, zname, *args, **kwargs): """Plot the results from the experiment.data pandas dataframe in a pcolor graph. Store the plots in a plots list attribute.""" title = self.title x, y, z = self._data[xname], self._data[yname], self._data[zname] shape = (len(y.unique()), len(x.unique())) diff = shape[0] * shape[1] - len(z) Z = np.concatenate((z.values, np.zeros(diff))).reshape(shape) df = pd.DataFrame(Z, index=y.unique(), columns=x.unique()) # TODO: Remove seaborn dependencies ax = sns.heatmap(df) pl.title(title) pl.xlabel(xname) pl.ylabel(yname) ax.invert_yaxis() pl.plt.show() self.plots.append( {'type': 'pcolor', 'x': xname, 'y': yname, 'z': zname, 'args': args, 'kwargs': kwargs, 'ax': ax}) if ax.get_figure() not in self.figs: self.figs.append(ax.get_figure()) def update_pcolor(self, ax, xname, yname, zname): """Update a pcolor graph with new data.""" x, y, z = self._data[xname], self._data[yname], self._data[zname] shape = (len(y.unique()), len(x.unique())) diff = shape[0] * shape[1] - len(z) Z = np.concatenate((z.values, np.zeros(diff))).reshape(shape) df = pd.DataFrame(Z, index=y.unique(), columns=x.unique()) cbar_ax = ax.get_figure().axes[1] # TODO: Remove seaborn dependencies sns.heatmap(df, ax=ax, cbar_ax=cbar_ax) ax.set_xlabel(xname) ax.set_ylabel(yname) ax.invert_yaxis() def update_line(self, ax, hl, xname, yname): """Update a line in a matplotlib graph with new data.""" del hl._xorig, hl._yorig hl.set_xdata(self._data[xname]) hl.set_ydata(self._data[yname]) ax.relim() ax.autoscale() gc.collect() def __del__(self): self.scribe.stop() if self.worker.is_alive(): self.worker.recorder_queue.put(None) self.worker.monitor_queue.put(None) self.worker.stop() PyMeasure-0.9.0/pymeasure/experiment/results.py0000664000175000017500000003120214010042511022115 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import os import re import sys from copy import deepcopy from importlib.machinery import SourceFileLoader from datetime import datetime import pandas as pd from .procedure import Procedure, UnknownProcedure from .parameters import Parameter log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def unique_filename(directory, prefix='DATA', suffix='', ext='csv', dated_folder=False, index=True, datetimeformat="%Y-%m-%d"): """ Returns a unique filename based on the directory and prefix """ now = datetime.now() directory = os.path.abspath(directory) if dated_folder: directory = os.path.join(directory, now.strftime('%Y-%m-%d')) if not os.path.exists(directory): os.makedirs(directory) if index: i = 1 basename = "%s%s" % (prefix, now.strftime(datetimeformat)) basepath = os.path.join(directory, basename) filename = "%s_%d%s.%s" % (basepath, i, suffix, ext) while os.path.exists(filename): i += 1 filename = "%s_%d%s.%s" % (basepath, i, suffix, ext) else: basename = "%s%s%s.%s" % (prefix, now.strftime(datetimeformat), suffix, ext) filename = os.path.join(directory, basename) return filename class CSVFormatter(logging.Formatter): """ Formatter of data results """ def __init__(self, columns, delimiter=','): """Creates a csv formatter for a given list of columns (=header). :param columns: list of column names. :type columns: list :param delimiter: delimiter between columns. :type delimiter: str """ super().__init__() self.columns = columns self.delimiter = delimiter def format(self, record): """Formats a record as csv. :param record: record to format. :type record: dict :return: a string """ return self.delimiter.join('{}'.format(record[x]) for x in self.columns) def format_header(self): return self.delimiter.join(self.columns) class Results(object): """ The Results class provides a convenient interface to reading and writing data in connection with a :class:`.Procedure` object. :cvar COMMENT: The character used to identify a comment (default: #) :cvar DELIMITER: The character used to delimit the data (default: ,) :cvar LINE_BREAK: The character used for line breaks (default \\n) :cvar CHUNK_SIZE: The length of the data chuck that is read :param procedure: Procedure object :param data_filename: The data filename where the data is or should be stored """ COMMENT = '#' DELIMITER = ',' LINE_BREAK = "\n" CHUNK_SIZE = 1000 def __init__(self, procedure, data_filename): if not isinstance(procedure, Procedure): raise ValueError("Results require a Procedure object") self.procedure = procedure self.procedure_class = procedure.__class__ self.parameters = procedure.parameter_objects() self._header_count = -1 self.formatter = CSVFormatter(columns=self.procedure.DATA_COLUMNS) if isinstance(data_filename, (list, tuple)): data_filenames, data_filename = data_filename, data_filename[0] else: data_filenames = [data_filename] self.data_filename = data_filename self.data_filenames = data_filenames if os.path.exists(data_filename): # Assume header is already written self.reload() self.procedure.status = Procedure.FINISHED # TODO: Correctly store and retrieve status else: for filename in self.data_filenames: with open(filename, 'w') as f: f.write(self.header()) f.write(self.labels()) self._data = None def __getstate__(self): # Get all information needed to reconstruct procedure self._parameters = self.procedure.parameter_values() self._class = self.procedure.__class__.__name__ module = sys.modules[self.procedure.__module__] self._package = module.__package__ self._module = module.__name__ self._file = module.__file__ state = self.__dict__.copy() del state['procedure'] del state['procedure_class'] return state def __setstate__(self, state): self.__dict__.update(state) # Restore the procedure module = SourceFileLoader(self._module, self._file).load_module() cls = getattr(module, self._class) self.procedure = cls() self.procedure.set_parameters(self._parameters) self.procedure.refresh_parameters() self.procedure_class = cls del self._parameters del self._class del self._package del self._module del self._file def header(self): """ Returns a text header to accompany a datafile so that the procedure can be reconstructed """ h = [] procedure = re.search("'(?P[^']+)'", repr(self.procedure_class)).group("name") h.append("Procedure: <%s>" % procedure) h.append("Parameters:") for name, parameter in self.parameters.items(): h.append("\t%s: %s" % (parameter.name, str(parameter).encode("unicode_escape").decode("utf-8"))) h.append("Data:") self._header_count = len(h) h = [Results.COMMENT + l for l in h] # Comment each line return Results.LINE_BREAK.join(h) + Results.LINE_BREAK def labels(self): """ Returns the columns labels as a string to be written to the file """ return self.formatter.format_header() + Results.LINE_BREAK def format(self, data): """ Returns a formatted string containing the data to be written to a file """ return self.formatter.format(data) def parse(self, line): """ Returns a dictionary containing the data from the line """ data = {} items = line.split(Results.DELIMITER) for i, key in enumerate(self.procedure.DATA_COLUMNS): data[key] = items[i] return data @staticmethod def parse_header(header, procedure_class=None): """ Returns a Procedure object with the parameters as defined in the header text. """ if procedure_class is not None: procedure = procedure_class() else: procedure = None header = header.split(Results.LINE_BREAK) procedure_module = None parameters = {} for line in header: if line.startswith(Results.COMMENT): line = line[1:] # Uncomment else: raise ValueError("Parsing a header which contains " "uncommented sections") if line.startswith("Procedure"): regex = r"<(?:(?P[^>]+)\.)?(?P[^.>]+)>" search = re.search(regex, line) procedure_module = search.group("module") procedure_class = search.group("class") elif line.startswith("\t"): separator = ": " partitioned_line = line[1:].partition(separator) if partitioned_line[1] != separator: raise Exception("Error partitioning header line %s." % line) else: parameters[partitioned_line[0]] = partitioned_line[2] if procedure is None: if procedure_class is None: raise ValueError("Header does not contain the Procedure class") try: from importlib import import_module procedure_module = import_module(procedure_module) procedure_class = getattr(procedure_module, procedure_class) procedure = procedure_class() except ImportError: procedure = UnknownProcedure(parameters) log.warning("Unknown Procedure being used") except Exception as e: raise e # Fill the procedure with the parameters found for name, parameter in procedure.parameter_objects().items(): if parameter.name in parameters: value = parameters[parameter.name] setattr(procedure, name, value) else: raise Exception("Missing '%s' parameter when loading '%s' class" % ( parameter.name, procedure_class)) procedure.refresh_parameters() # Enforce update of meta data return procedure @staticmethod def load(data_filename, procedure_class=None): """ Returns a Results object with the associated Procedure object and data """ header = "" header_read = False header_count = 0 with open(data_filename, 'r') as f: while not header_read: line = f.readline() if line.startswith(Results.COMMENT): header += line.strip() + Results.LINE_BREAK header_count += 1 else: header_read = True procedure = Results.parse_header(header[:-1], procedure_class) results = Results(procedure, data_filename) results._header_count = header_count return results @property def data(self): # Need to update header count for correct referencing if self._header_count == -1: self._header_count = len( self.header()[-1].split(Results.LINE_BREAK)) if self._data is None or len(self._data) == 0: # Data has not been read try: self.reload() except Exception: # Empty dataframe self._data = pd.DataFrame(columns=self.procedure.DATA_COLUMNS) else: # Concatenate additional data, if any, to already loaded data skiprows = len(self._data) + self._header_count chunks = pd.read_csv( self.data_filename, comment=Results.COMMENT, header=0, names=self._data.columns, chunksize=Results.CHUNK_SIZE, skiprows=skiprows, iterator=True ) try: tmp_frame = pd.concat(chunks, ignore_index=True) # only append new data if there is any # if no new data, tmp_frame dtype is object, which override's # self._data's original dtype - this can cause problems plotting # (e.g. if trying to plot int data on a log axis) if len(tmp_frame) > 0: self._data = pd.concat([self._data, tmp_frame], ignore_index=True) except Exception: pass # All data is up to date return self._data def reload(self): """ Preforms a full reloading of the file data, neglecting any changes in the comments """ chunks = pd.read_csv( self.data_filename, comment=Results.COMMENT, chunksize=Results.CHUNK_SIZE, iterator=True ) try: self._data = pd.concat(chunks, ignore_index=True) except Exception: self._data = chunks.read() def __repr__(self): return "<{}(filename='{}',procedure={},shape={})>".format( self.__class__.__name__, self.data_filename, self.procedure.__class__.__name__, self.data.shape ) PyMeasure-0.9.0/pymeasure/experiment/parameters.py0000664000175000017500000004142414010042511022566 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # class Parameter(object): """ Encapsulates the information for an experiment parameter with information about the name, and units if supplied. :var value: The value of the parameter :param name: The parameter name :param default: The default value :param ui_class: A Qt class to use for the UI of this parameter """ def __init__(self, name, default=None, ui_class=None): self.name = name self._value = default self.default = default self.ui_class = ui_class @property def value(self): if self.is_set(): return self._value else: raise ValueError("Parameter value is not set") @value.setter def value(self, value): self._value = value def is_set(self): """ Returns True if the Parameter value is set """ return self._value is not None def __str__(self): return str(self._value) if self.is_set() else '' def __repr__(self): return "<%s(name=%s,value=%s,default=%s)>" % ( self.__class__.__name__, self.name, self._value, self.default) class IntegerParameter(Parameter): """ :class:`.Parameter` sub-class that uses the integer type to store the value. :var value: The integer value of the parameter :param name: The parameter name :param units: The units of measure for the parameter :param minimum: The minimum allowed value (default: -1e9) :param maximum: The maximum allowed value (default: 1e9) :param default: The default integer value :param ui_class: A Qt class to use for the UI of this parameter """ def __init__(self, name, units=None, minimum=-1e9, maximum=1e9, **kwargs): super().__init__(name, **kwargs) self.units = units self.minimum = int(minimum) self.maximum = int(maximum) @property def value(self): if self.is_set(): return int(self._value) else: raise ValueError("Parameter value is not set") @value.setter def value(self, value): if isinstance(value, str): value, _, units = value.strip().partition(" ") if units != "" and units != self.units: raise ValueError("Units included in string (%s) do not match" "the units of the IntegerParameter (%s)" % (units, self.units)) try: value = int(value) except ValueError: raise ValueError("IntegerParameter given non-integer value of " "type '%s'" % type(value)) if value < self.minimum: raise ValueError("IntegerParameter value is below the minimum") elif value > self.maximum: raise ValueError("IntegerParameter value is above the maximum") else: self._value = value def __str__(self): if not self.is_set(): return '' result = "%d" % self._value if self.units: result += " %s" % self.units return result def __repr__(self): return "<%s(name=%s,value=%s,units=%s,default=%s)>" % ( self.__class__.__name__, self.name, self._value, self.units, self.default) class BooleanParameter(Parameter): """ :class:`.Parameter` sub-class that uses the boolean type to store the value. :var value: The boolean value of the parameter :param name: The parameter name :param default: The default boolean value :param ui_class: A Qt class to use for the UI of this parameter """ @property def value(self): if self.is_set(): return self._value else: raise ValueError("Parameter value is not set") @value.setter def value(self, value): if isinstance(value, str): if value.lower() == "true": self._value = True elif value.lower() == "false": self._value = False else: raise ValueError("BooleanParameter given string value of '%s'" % value) elif isinstance(value, (int, float)) and value in [0, 1]: self._value = bool(value) elif isinstance(value, bool): self._value = value else: raise ValueError("BooleanParameter given non-boolean value of " "type '%s'" % type(value)) class FloatParameter(Parameter): """ :class:`.Parameter` sub-class that uses the floating point type to store the value. :var value: The floating point value of the parameter :param name: The parameter name :param units: The units of measure for the parameter :param minimum: The minimum allowed value (default: -1e9) :param maximum: The maximum allowed value (default: 1e9) :param decimals: The number of decimals considered (default: 15) :param default: The default floating point value :param ui_class: A Qt class to use for the UI of this parameter """ def __init__(self, name, units=None, minimum=-1e9, maximum=1e9, decimals=15, **kwargs): super().__init__(name, **kwargs) self.units = units self.minimum = minimum self.maximum = maximum self.decimals = decimals @property def value(self): if self.is_set(): return float(self._value) else: raise ValueError("Parameter value is not set") @value.setter def value(self, value): if isinstance(value, str): value, _, units = value.strip().partition(" ") if units != "" and units != self.units: raise ValueError("Units included in string (%s) do not match" "the units of the FloatParameter (%s)" % (units, self.units)) try: value = float(value) except ValueError: raise ValueError("FloatParameter given non-float value of " "type '%s'" % type(value)) if value < self.minimum: raise ValueError("FloatParameter value is below the minimum") elif value > self.maximum: raise ValueError("FloatParameter value is above the maximum") else: self._value = value def __str__(self): if not self.is_set(): return '' result = "%g" % self._value if self.units: result += " %s" % self.units return result def __repr__(self): return "<%s(name=%s,value=%s,units=%s,default=%s)>" % ( self.__class__.__name__, self.name, self._value, self.units, self.default) class VectorParameter(Parameter): """ :class:`.Parameter` sub-class that stores the value in a vector format. :var value: The value of the parameter as a list of floating point numbers :param name: The parameter name :param length: The integer dimensions of the vector :param units: The units of measure for the parameter :param default: The default value :param ui_class: A Qt class to use for the UI of this parameter """ def __init__(self, name, length=3, units=None, **kwargs): super().__init__(name, **kwargs) self._length = length self.units = units @property def value(self): if self.is_set(): return [float(ve) for ve in self._value] else: raise ValueError("Parameter value is not set") @value.setter def value(self, value): if isinstance(value, str): # strip units if included if self.units is not None and value.endswith(" " + self.units): value = value[:-len(self.units)].strip() # Strip initial and final brackets if (value[0] != '[') or (value[-1] != ']'): raise ValueError("VectorParameter must be passed a vector" " denoted by square brackets if initializing" " by string.") raw_list = value[1:-1].split(",") elif isinstance(value, (list, tuple)): raw_list = value else: raise ValueError("VectorParameter given undesired value of " "type '%s'" % type(value)) if len(raw_list) != self._length: raise ValueError("VectorParameter given value of length " "%d instead of %d" % (len(raw_list), self._length)) try: self._value = [float(ve) for ve in raw_list] except ValueError: raise ValueError("VectorParameter given input '%s' that could " "not be converted to floats." % str(value)) def __str__(self): """If we eliminate spaces within the list __repr__ then the csv parser will interpret it as a single value.""" if not self.is_set(): return '' result = "".join(repr(self.value).split()) if self.units: result += " %s" % self.units return result def __repr__(self): return "<%s(name=%s,value=%s,units=%s,length=%s)>" % ( self.__class__.__name__, self.name, self._value, self.units, self._length) class ListParameter(Parameter): """ :class:`.Parameter` sub-class that stores the value as a list. :param name: The parameter name :param choices: An explicit list of choices, which is disregarded if None :param units: The units of measure for the parameter :param default: The default value :param ui_class: A Qt class to use for the UI of this parameter """ def __init__(self, name, choices=None, units=None, **kwargs): super().__init__(name, **kwargs) self._choices = tuple(choices) if choices is not None else None self.units = units @property def value(self): if self.is_set(): return self._value else: raise ValueError("Parameter value is not set") @value.setter def value(self, value): # strip units if included if isinstance(value, str): if self.units is not None and value.endswith(" " + self.units): value = value[:-len(self.units)].strip() if self._choices is not None and value in self._choices: self._value = value else: raise ValueError("Invalid choice for parameter. " "Must be one of %s" % str(self._choices)) @property def choices(self): """ Returns an immutable iterable of choices, or None if not set. """ return self._choices class PhysicalParameter(VectorParameter): """ :class:`.VectorParameter` sub-class of 2 dimensions to store a value and its uncertainty. :var value: The value of the parameter as a list of 2 floating point numbers :param name: The parameter name :param uncertainty_type: Type of uncertainty, 'absolute', 'relative' or 'percentage' :param units: The units of measure for the parameter :param default: The default value :param ui_class: A Qt class to use for the UI of this parameter """ def __init__(self, name, uncertaintyType='absolute', **kwargs): super().__init__(name, length=2, **kwargs) self._utype = ListParameter("uncertainty type", choices=['absolute', 'relative', 'percentage'], default=None) self._utype.value = uncertaintyType @property def value(self): if self.is_set(): return [float(ve) for ve in self._value] else: raise ValueError("Parameter value is not set") @value.setter def value(self, value): if isinstance(value, str): # strip units if included if self.units is not None and value.endswith(" " + self.units): value = value[:-len(self.units)].strip() # Strip initial and final brackets if (value[0] != '[') or (value[-1] != ']'): raise ValueError("VectorParameter must be passed a vector" " denoted by square brackets if initializing" " by string.") raw_list = value[1:-1].split(",") elif isinstance(value, (list, tuple)): raw_list = value else: raise ValueError("VectorParameter given undesired value of " "type '%s'" % type(value)) if len(raw_list) != self._length: raise ValueError("VectorParameter given value of length " "%d instead of %d" % (len(raw_list), self._length)) try: self._value = [float(ve) for ve in raw_list] except ValueError: raise ValueError("VectorParameter given input '%s' that could " "not be converted to floats." % str(value)) # Uncertainty must be non-negative self._value[1] = abs(self._value[1]) @property def uncertainty_type(self): return self._utype.value @uncertainty_type.setter def uncertainty_type(self, uncertaintyType): oldType = self._utype.value self._utype.value = uncertaintyType newType = self._utype.value if self.is_set(): # Convert uncertainty value to the new type if (oldType, newType) == ('absolute', 'relative'): self._value[1] = abs(self._value[1] / self._value[0]) if (oldType, newType) == ('relative', 'absolute'): self._value[1] = abs(self._value[1] * self._value[0]) if (oldType, newType) == ('relative', 'percentage'): self._value[1] = abs(self._value[1] * 100.0) if (oldType, newType) == ('percentage', 'relative'): self._value[1] = abs(self._value[1] * 0.01) if (oldType, newType) == ('percentage', 'absolute'): self._value[1] = abs(self._value[1] * self._value[0] * 0.01) if (oldType, newType) == ('absolute', 'percentage'): self._value[1] = abs(self._value[1] * 100.0 / self._value[0]) def __str__(self): if not self.is_set(): return '' result = "%g +/- %g" % (self._value[0], self._value[1]) if self.units: result += " %s" % self.units if self._utype.value is not None: result += " (%s)" % self._utype.value return result def __repr__(self): return "<%s(name=%s,value=%s,units=%s,uncertaintyType=%s)>" % ( self.__class__.__name__, self.name, self._value, self.units, self._utype.value) class Measurable(object): """ Encapsulates the information for a measurable experiment parameter with information about the name, fget function and units if supplied. The value property is called when the procedure retrieves a datapoint and calls the fget function. If no fget function is specified, the value property will return the latest set value of the parameter (or default if never set). :var value: The value of the parameter :param name: The parameter name :param fget: The parameter fget function (e.g. an instrument parameter) :param default: The default value """ DATA_COLUMNS = [] def __init__(self, name, fget=None, units=None, measure=True, default=None, **kwargs): self.name = name self.units = units self.measure = measure if fget is not None: self.fget = fget self._value = fget() else: self._value = default Measurable.DATA_COLUMNS.append(name) def fget(self): return self._value @property def value(self): if hasattr(self, 'fget'): self._value = self.fget() return self._value @value.setter def value(self, value): self._value = value PyMeasure-0.9.0/pymeasure/experiment/config.py0000664000175000017500000000353114010037617021700 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import configparser import logging import os log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def set_file(filename): os.environ['CONFIG'] = filename def get_config(filename='default_config.ini'): if 'CONFIG' in os.environ.keys(): filename = os.environ['CONFIG'] config = configparser.ConfigParser() config.read(filename) return config # noinspection PyProtectedMember def set_mpl_rcparams(config): if 'matplotlib.rcParams' in config._sections.keys(): import matplotlib from cycler import cycler for key in config._sections['matplotlib.rcParams']: matplotlib.rcParams[key] = eval(config._sections['matplotlib.rcParams'][key]) PyMeasure-0.9.0/pymeasure/experiment/workers.py0000664000175000017500000001427214010037617022133 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import sys import logging import time import traceback from logging.handlers import QueueHandler from importlib.machinery import SourceFileLoader from queue import Queue from .listeners import Recorder from .procedure import Procedure, ProcedureWrapper from .results import Results from ..log import TopicQueueHandler from ..thread import StoppableThread log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) try: import zmq import cloudpickle except ImportError: zmq = None cloudpickle = None log.warning("ZMQ and cloudpickle are required for TCP communication") class Worker(StoppableThread): """ Worker runs the procedure and emits information about the procedure and its status over a ZMQ TCP port. In a child thread, a Recorder is run to write the results to """ def __init__(self, results, log_queue=None, log_level=logging.INFO, port=None): """ Constructs a Worker to perform the Procedure defined in the file at the filepath """ super().__init__() self.port = port if not isinstance(results, Results): raise ValueError("Invalid Results object during Worker construction") self.results = results self.results.procedure.check_parameters() self.results.procedure.status = Procedure.QUEUED self.recorder = None self.recorder_queue = Queue() self.monitor_queue = Queue() if log_queue is None: log_queue = Queue() self.log_queue = log_queue self.log_level = log_level self.context = None self.publisher = None def join(self, timeout=0): try: super().join(timeout) except (KeyboardInterrupt, SystemExit): log.warning("User stopped Worker join prematurely") self.stop() super().join(0) def emit(self, topic, record): """ Emits data of some topic over TCP """ log.debug("Emitting message: %s %s", topic, record) try: self.publisher.send_serialized((topic, record), serialize=cloudpickle.dumps) except (NameError, AttributeError): pass # No dumps defined if topic == 'results': self.recorder.handle(record) elif topic == 'status' or topic == 'progress': self.monitor_queue.put((topic, record)) def handle_abort(self): log.exception("User stopped Worker execution prematurely") self.update_status(Procedure.ABORTED) def handle_error(self): log.exception("Worker caught an error on %r", self.procedure) traceback_str = traceback.format_exc() self.emit('error', traceback_str) self.update_status(Procedure.FAILED) def update_status(self, status): self.procedure.status = status self.emit('status', status) def shutdown(self): self.procedure.shutdown() if self.should_stop() and self.procedure.status == Procedure.RUNNING: self.update_status(Procedure.ABORTED) elif self.procedure.status == Procedure.RUNNING: self.update_status(Procedure.FINISHED) self.emit('progress', 100.) self.recorder.stop() self.monitor_queue.put(None) def run(self): global log log = logging.getLogger() log.setLevel(self.log_level) # log.handlers = [] # Remove all other handlers # log.addHandler(TopicQueueHandler(self.monitor_queue)) # log.addHandler(QueueHandler(self.log_queue)) log.info("Worker thread started") self.procedure = self.results.procedure self.recorder = Recorder(self.results, self.recorder_queue) self.recorder.start() #locals()[self.procedures_file] = __import__(self.procedures_file) # route Procedure methods & log self.procedure.should_stop = self.should_stop self.procedure.emit = self.emit if self.port is not None and zmq is not None: try: self.context = zmq.Context() log.debug("Worker ZMQ Context: %r" % self.context) self.publisher = self.context.socket(zmq.PUB) self.publisher.bind('tcp://*:%d' % self.port) log.info("Worker connected to tcp://*:%d" % self.port) time.sleep(0.01) except Exception: log.exception("couldn't connect to ZMQ context") log.info("Worker started running an instance of %r", self.procedure.__class__.__name__) self.update_status(Procedure.RUNNING) self.emit('progress', 0.) try: self.procedure.startup() self.procedure.execute() except (KeyboardInterrupt, SystemExit): self.handle_abort() except Exception: self.handle_error() finally: self.shutdown() self.stop() def __repr__(self): return "<%s(port=%s,procedure=%s,should_stop=%s)>" % ( self.__class__.__name__, self.port, self.procedure.__class__.__name__, self.should_stop() ) PyMeasure-0.9.0/pymeasure/experiment/__init__.py0000664000175000017500000000306614010037617022175 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # from .parameters import (Parameter, IntegerParameter, FloatParameter, VectorParameter, ListParameter, BooleanParameter, Measurable) from .procedure import Procedure, UnknownProcedure from .results import Results, unique_filename from .workers import Worker from .listeners import Listener, Recorder from .config import get_config from .experiment import Experiment, get_array, get_array_steps, get_array_zero PyMeasure-0.9.0/pymeasure/experiment/procedure.py0000664000175000017500000002244114010037617022424 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging import sys from copy import deepcopy from importlib.machinery import SourceFileLoader from .parameters import Parameter, Measurable log = logging.getLogger() log.addHandler(logging.NullHandler()) class Procedure(object): """Provides the base class of a procedure to organize the experiment execution. Procedures should be run by Workers to ensure that asynchronous execution is properly managed. .. code-block:: python procedure = Procedure() results = Results(procedure, data_filename) worker = Worker(results, port) worker.start() Inheriting classes should define the startup, execute, and shutdown methods as needed. The shutdown method is called even with a software exception or abort event during the execute method. If keyword arguments are provided, they are added to the object as attributes. """ DATA_COLUMNS = [] MEASURE = {} FINISHED, FAILED, ABORTED, QUEUED, RUNNING = 0, 1, 2, 3, 4 STATUS_STRINGS = { FINISHED: 'Finished', FAILED: 'Failed', ABORTED: 'Aborted', QUEUED: 'Queued', RUNNING: 'Running' } _parameters = {} def __init__(self, **kwargs): self.status = Procedure.QUEUED self._update_parameters() for key in kwargs: if key in self._parameters.keys(): setattr(self, key, kwargs[key]) log.info('Setting parameter %s to %s' % (key, kwargs[key])) self.gen_measurement() def gen_measurement(self): """Create MEASURE and DATA_COLUMNS variables for get_datapoint method.""" # TODO: Refactor measurable-s implementation to be consistent with parameters self.MEASURE = {} for item in dir(self): parameter = getattr(self, item) if isinstance(parameter, Measurable): if parameter.measure: self.MEASURE.update({parameter.name: item}) if not self.DATA_COLUMNS: self.DATA_COLUMNS = Measurable.DATA_COLUMNS def get_datapoint(self): data = {key: getattr(self, self.MEASURE[key]).value for key in self.MEASURE} return data def measure(self): data = self.get_datapoint() log.debug("Produced numbers: %s" % data) self.emit('results', data) def _update_parameters(self): """ Collects all the Parameter objects for the procedure and stores them in a meta dictionary so that the actual values can be set in their stead """ if not self._parameters: self._parameters = {} for item in dir(self): parameter = getattr(self, item) if isinstance(parameter, Parameter): self._parameters[item] = deepcopy(parameter) if parameter.is_set(): setattr(self, item, parameter.value) else: setattr(self, item, None) def parameters_are_set(self): """ Returns True if all parameters are set """ for name, parameter in self._parameters.items(): if getattr(self, name) is None: return False return True def check_parameters(self): """ Raises an exception if any parameter is missing before calling the associated function. Ensures that each value can be set and got, which should cast it into the right format. Used as a decorator @check_parameters on the startup method """ for name, parameter in self._parameters.items(): value = getattr(self, name) if value is None: raise NameError("Missing %s '%s' in %s" % ( parameter.__class__, name, self.__class__)) def parameter_values(self): """ Returns a dictionary of all the Parameter values and grabs any current values that are not in the default definitions """ result = {} for name, parameter in self._parameters.items(): value = getattr(self, name) if value is not None: parameter.value = value setattr(self, name, parameter.value) result[name] = parameter.value else: result[name] = None return result def parameter_objects(self): """ Returns a dictionary of all the Parameter objects and grabs any current values that are not in the default definitions """ result = {} for name, parameter in self._parameters.items(): value = getattr(self, name) if value is not None: parameter.value = value setattr(self, name, parameter.value) result[name] = parameter return result def refresh_parameters(self): """ Enforces that all the parameters are re-cast and updated in the meta dictionary """ for name, parameter in self._parameters.items(): value = getattr(self, name) parameter.value = value setattr(self, name, parameter.value) def set_parameters(self, parameters, except_missing=True): """ Sets a dictionary of parameters and raises an exception if additional parameters are present if except_missing is True """ for name, value in parameters.items(): if name in self._parameters: self._parameters[name].value = value setattr(self, name, self._parameters[name].value) else: if except_missing: raise NameError("Parameter '%s' does not belong to '%s'" % ( name, repr(self))) def startup(self): """ Executes the commands needed at the start-up of the measurement """ pass def execute(self): """ Preforms the commands needed for the measurement itself. During execution the shutdown method will always be run following this method. This includes when Exceptions are raised. """ pass def shutdown(self): """ Executes the commands necessary to shut down the instruments and leave them in a safe state. This method is always run at the end. """ pass def emit(self, topic, record): raise NotImplementedError('should be monkey patched by a worker') def should_stop(self): raise NotImplementedError('should be monkey patched by a worker') def __str__(self): result = repr(self) + "\n" for parameter in self._parameters.items(): result += str(parameter) return result def __repr__(self): return "<{}(status={},parameters_are_set={})>".format( self.__class__.__name__, self.STATUS_STRINGS[self.status], self.parameters_are_set() ) class UnknownProcedure(Procedure): """ Handles the case when a :class:`.Procedure` object can not be imported during loading in the :class:`.Results` class """ def __init__(self, parameters): super().__init__() self._parameters = parameters def startup(self): raise NotImplementedError("UnknownProcedure can not be run") class ProcedureWrapper(object): def __init__(self, procedure): self.procedure = procedure def __getstate__(self): # Get all information needed to reconstruct procedure self._parameters = self.procedure.parameter_values() self._class = self.procedure.__class__.__name__ module = sys.modules[self.procedure.__module__] self._package = module.__package__ self._module = module.__name__ self._file = module.__file__ state = self.__dict__.copy() del state['procedure'] return state def __setstate__(self, state): self.__dict__.update(state) # Restore the procedure module = SourceFileLoader(self._module, self._file).load_module() cls = getattr(module, self._class) self.procedure = cls() self.procedure.set_parameters(self._parameters) self.procedure.refresh_parameters() del self._parameters del self._class del self._package del self._module del self._file PyMeasure-0.9.0/pymeasure/experiment/listeners.py0000664000175000017500000001000614010037617022436 0ustar colincolin00000000000000# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2021 PyMeasure Developers # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # import logging from logging import StreamHandler, FileHandler from ..log import QueueListener from ..thread import StoppableThread log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) try: import zmq import cloudpickle except ImportError: zmq = None cloudpickle = None log.warning("ZMQ and cloudpickle are required for TCP communication") class Monitor(QueueListener): def __init__(self, results, queue): console = StreamHandler() console.setFormatter(results.formatter) super().__init__(queue, console) class Listener(StoppableThread): """Base class for Threads that need to listen for messages on a ZMQ TCP port and can be stopped by a thread-safe method call """ def __init__(self, port, topic='', timeout=0.01): """ Constructs the Listener object with a subscriber port over which to listen for messages :param port: TCP port to listen on :param topic: Topic to listen on :param timeout: Timeout in seconds to recheck stop flag """ super().__init__() self.port = port self.topic = topic self.context = zmq.Context() log.debug("%s has ZMQ Context: %r" % (self.__class__.__name__, self.context)) self.subscriber = self.context.socket(zmq.SUB) self.subscriber.connect('tcp://localhost:%d' % port) self.subscriber.setsockopt(zmq.SUBSCRIBE, topic.encode()) log.info("%s connected to '%s' topic on tcp://localhost:%d" % ( self.__class__.__name__, topic, port)) self.poller = zmq.Poller() self.poller.register(self.subscriber, zmq.POLLIN) self.timeout = timeout def receive(self, flags=0): topic, record = self.subscriber.recv_serialized(deserialize=cloudpickle.loads, flags=flags) return topic, record def message_waiting(self): return self.poller.poll(self.timeout) def __repr__(self): return "<%s(port=%s,topic=%s,should_stop=%s)>" % ( self.__class__.__name__, self.port, self.topic, self.should_stop()) class Recorder(QueueListener): """ Recorder loads the initial Results for a filepath and appends data by listening for it over a queue. The queue ensures that no data is lost between the Recorder and Worker. """ def __init__(self, results, queue, **kwargs): """ Constructs a Recorder to record the Procedure data into the file path, by waiting for data on the subscription port """ handlers = [] for filename in results.data_filenames: fh = FileHandler(filename=filename, **kwargs) fh.setFormatter(results.formatter) fh.setLevel(logging.NOTSET) handlers.append(fh) super().__init__(queue, *handlers) def stop(self): for handler in self.handlers: handler.close() super().stop() PyMeasure-0.9.0/PKG-INFO0000664000175000017500000003421114010046235014757 0ustar colincolin00000000000000Metadata-Version: 2.1 Name: PyMeasure Version: 0.9.0 Summary: Scientific measurement library for instruments, experiments, and live-plotting Home-page: https://github.com/pymeasure/pymeasure Author: PyMeasure Developers License: MIT License Download-URL: https://github.com/pymeasure/pymeasure/tarball/v0.9.0 Description: .. image:: https://raw.githubusercontent.com/pymeasure/pymeasure/master/docs/images/PyMeasure.png :alt: PyMeasure Scientific package PyMeasure scientific package ############################ PyMeasure makes scientific measurements easy to set up and run. The package contains a repository of instrument classes and a system for running experiment procedures, which provides graphical interfaces for graphing live data and managing queues of experiments. Both parts of the package are independent, and when combined provide all the necessary requirements for advanced measurements with only limited coding. PyMeasure is currently under active development, so please report any issues you experience to our `Issues page`_. .. _Issues page: https://github.com/pymeasure/pymeasure/issues PyMeasure runs on Python 3.6, 3.7, 3.8 and 3.9, and is tested with continous-integration on Linux, macOS, and Windows. .. image:: https://github.com/pymeasure/pymeasure/workflows/Pymeasure%20CI/badge.svg :target: https://github.com/pymeasure/pymeasure/actions .. image:: http://readthedocs.org/projects/pymeasure/badge/?version=latest :target: http://pymeasure.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3732545.svg :target: https://doi.org/10.5281/zenodo.3732545 .. image:: https://anaconda.org/conda-forge/pymeasure/badges/version.svg :target: https://anaconda.org/conda-forge/pymeasure .. image:: https://anaconda.org/conda-forge/pymeasure/badges/downloads.svg :target: https://anaconda.org/conda-forge/pymeasure Quick start =========== Check out `the documentation`_ for the `quick start guide`_, that covers the installation of Python and PyMeasure. There are a number of examples in the `examples`_ directory that can help you get up and running. .. _the documentation: http://pymeasure.readthedocs.org/en/latest/ .. _quick start guide: http://pymeasure.readthedocs.io/en/latest/quick_start.html .. _examples: https://github.com/pymeasure/pymeasure/tree/master/examples Version 0.9 -- released 2/7/21 ============================== - PyMeasure is now officially at github.com/pymeasure/pymeasure - Python 3.9 is now supported, Python 3.5 removed due to EOL - Move to GitHub Actions from TravisCI and Appveyor for CI (@bilderbuchi) - New additions to Oxford Instruments ITC 503 (@CasperSchippers) - New Agilent 34450A and Keysight DSOX1102G instruments (@theMashUp, @jlarochelle) - Improvements to NI VirtualBench (@moritzj29) - New Agilent B1500 instrument (@moritzj29) - New Keithley 6517B instrument (@wehlgrundspitze) - Major improvements to PyVISA compatbility (@bilderbuchi, @msmttchr, @CasperSchippers, @cjermain) - New Anapico APSIN12G instrument (@StePhanino) - Improvements to Thorelabs Pro 8000 and SR830 (@Mike-HubGit) - New SR860 instrument (@StevenSiegl, @bklebel) - Fix to escape sequences (@tirkarthi) - New directory input for ManagedWindow (@paulgoulain) - New TelnetAdapter and Attocube ANC300 Piezo controller (@dkriegner) - New Agilent 34450A (@theMashUp) - New Razorbill RP100 strain cell controller (@pheowl) - Fixes to precision and default value of ScientificInput and FloatParameter (@moritzj29) - Fixes for Keithly 2400 and 2450 controls (@pyMatJ) - Improvments to Inputs and open_file_externally (@msmttchr) - Fixes to Agilent 8722ES (@alexmcnabb) - Fixes to QThread cleanup (@neal-kepler, @msmttchr) - Fixes to Keyboard interrupt, and parameters (@CasperSchippers) Version 0.8 -- released 3/29/19 =============================== - Python 3.8 is now supported - New Measurement Sequencer allows for running over a large parameter space (@CasperSchippers) - New image plotting feature for live image measurements (@jmittelstaedt) - Improvements to VISA adapter (@moritzj29) - Added Tektronix AFG 3000, Keithley 2750 (@StePhanino, @dennisfeng2) - Documentation improvements (@mivade) - Fix to ScientificInput for float strings (@moritzj29) - New validator: strict_discrete_range (@moritzj29) - Improvements to Recorder thread joining - Migrating the ReadtheDocs configuration to version 2 - National Instruments Virtual Bench initial support (@moritzj29) Version 0.7 -- released 8/4/19 ============================== - Dropped support for Python 3.4, adding support for Python 3.7 - Significant improvements to CI, dependencies, and conda environment (@bilderbuchi, @cjermain) - Fix for PyQT issue in ResultsDialog (@CasperSchippers) - Fix for wire validator in Keithley 2400 (@Fattotora) - Addition of source_enabled control for Keithley 2400 (@dennisfeng2) - Time constant fix and input controls for SR830 (@dennisfeng2) - Added Keithley 2450 and Agilent 33521A (@hlgirard, @Endever42) - Proper escaping support in CSV headers (@feph) - Minor updates (@dvase) Version 0.6.1 -- released 4/21/19 ================================= - Added Elektronica SM70-45D, Agilent 33220A, and Keysight N5767A instruments (@CasperSchippers, @sumatrae) - Fixes for Prologix adapter and Keithley 2400 (@hlgirard, @ronan-sensome) - Improved support for SRS SR830 (@CasperSchippers) Version 0.6 -- released 1/14/19 =============================== - New VXI11 Adapter for ethernet instruments (@chweiser) - PyQt updates to 5.6.0 - Added SRS SG380, Ametek 7270, Agilent 4156, HP 34401A, Advantest R3767CG, and Oxford ITC503 instrustruments (@sylkar, @jmittelstaedt, @vik-s, @troylf, @CasperSchippers) - Updates to Keithley 2000, Agilent 8257D, ESP 300, and Keithley 2400 instruments (@watersjason, @jmittelstaedt, @nup002) - Various minor bug fixes (@thosou) Version 0.5.1 -- released 4/14/18 ================================= - Minor versions of PyVISA are now properly handled - Documentation improvements (@Laogeodritt and @ederag) - Instruments now have `set_process` capability (@bilderbuchi) - Plotter now uses threads (@dvspirito) - Display inputs and PlotItem improvements (@Laogeodritt) Version 0.5 -- released 10/18/17 ================================ - Threads are used by default, eliminating multiprocessing issues with spawn - Enhanced unit tests for threading - Sphinx Doctests are added to the documentation (@bilderbuchi) - Improvements to documentation (@JuMaD) Version 0.4.6 -- released 8/12/17 ================================= - Reverted multiprocessing start method keyword arguments to fix Unix spawn issues (@ndr37) - Fixes to regressions in Results writing (@feinsteinben) - Fixes to TCP support using cloudpickle (@feinsteinben) - Restructing of unit test framework Version 0.4.5 -- released 7/4/17 ================================ - Recorder and Scribe now leverage QueueListener (@feinsteinben) - PrologixAdapter and SerialAdapter now handle Serial objects as adapters (@feinsteinben) - Optional TCP support now uses cloudpickle for serialization (@feinsteinben) - Significant PEP8 review and bug fixes (@feinsteinben) - Includes docs in the code distribution (@ghisvail) - Continuous integration support for Python 3.6 (@feinsteinben) Version 0.4.4 -- released 6/4/17 ================================ - Fix pip install for non-wheel builds - Update to Agilent E4980 (@dvspirito) - Minor fixes for docs, tests, and formatting (@ghisvail, @feinsteinben) Version 0.4.3 -- released 3/30/17 ================================= - Added Agilent E4980, AMI 430, Agilent 34410A, Thorlabs PM100, and Anritsu MS9710C instruments (@TvBMcMaster, @dvspirito, and @mhdg) - Updates to PyVISA support (@minhhaiphys) - Initial work on resource manager (@dvspirito) - Fixes for Prologix adapter that allow read-write delays (@TvBMcMaster) - Fixes for conda environment on continuous integration Version 0.4.2 -- released 8/23/16 ================================= - New instructions for installing with Anaconda and conda-forge package (thanks @melund!) - Bug-fixes to the Keithley 2000, SR830, and Agilent E4408B - Re-introduced the Newport ESP300 motion controller - Major update to the Keithely 2400, 2000 and Yokogawa 7651 to achieve a common interface - New command-string processing hooks for Instrument property functions - Updated LakeShore 331 temperature controller with new features - Updates to the Agilent 8257D signal generator for better feature exposure Version 0.4.1 -- released 7/31/16 ================================= - Critical fix in setup.py for importing instruments (also added to documentation) Version 0.4 -- released 7/29/16 =============================== - Replaced Instrument add_measurement and add_control with measurement and control functions - Added validators to allow Instrument.control to match restricted ranges - Added mapping to Instrument.control to allow more flexible inputs - Conda is now used to set up the Python environment - macOS testing in continuous integration - Major updates to the documentation Version 0.3 -- released 4/8/16 ============================== - Added IPython (Jupyter) notebook support with significant features - Updated set of example scripts and notebooks - New PyMeasure logo released - Removed support for Python <3.4 - Changed multiprocessing to use spawn for compatibility - Significant work on the documentation - Added initial tests for non-instrument code - Continuous integration setup for Linux and Windows Version 0.2 -- released 12/16/15 ================================ - Python 3 compatibility, removed support for Python 2 - Considerable renaming for better PEP8 compliance - Added MIT License - Major restructuring of the package to break it into smaller modules - Major rewrite of display functionality, introducing new Qt objects for easy extensions - Major rewrite of procedure execution, now using a Worker process which takes advantage of multi-core CPUs - Addition of a number of examples - New methods for listening to Procedures, introducing ZMQ for TCP connectivity - Updates to Keithley2400 and VISAAdapter Version 0.1.6 -- released 4/19/15 ================================= - Renamed the package to PyMeasure from Automate to be more descriptive about its purpose - Addition of VectorParameter to allow vectors to be input for Procedures - Minor fixes for the Results and Danfysik8500 Version 0.1.5 -- release 10/22/14 ================================= - New Manager class for handling Procedures in a queue fashion - New Browser that works in tandem with the Manager to display the queue - Bug fixes for Results loading Version 0.1.4 -- released 8/2/14 ================================ - Integrated Results class into display and file writing - Bug fixes for Listener classes - Bug fixes for SR830 Version 0.1.3 -- released 7/20/14 ================================= - Replaced logging system with Python logging package - Added data management (Results) and bug fixes for Procedures and Parameters - Added pandas v0.14 to requirements for data management - Added data listeners, Qt4 and PyQtGraph helpers Version 0.1.2 -- released 7/18/14 ================================= - Bug fixes to LakeShore 425 - Added new Procedure and Parameter classes for generic experiments - Added version number in package Version 0.1.1 -- released 7/16/14 ================================= - Bug fixes to PrologixAdapter, VISAAdapter, Agilent 8722ES, Agilent 8257D, Stanford SR830, Danfysik8500 - Added Tektronix TDS 2000 with basic functionality - Fixed Danfysik communication to handle errors properly Version 0.1.0 -- released 7/15/14 ================================= - Initial release Keywords: measure instrument experiment control automate graph plot Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Scientific/Engineering Provides-Extra: matplotlib Provides-Extra: tcp Provides-Extra: python-vxi11