././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4496064 pymeasure-0.14.0/0000755000175100001770000000000014623331176013221 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/AUTHORS.txt0000644000175100001770000000242114623331163015102 0ustar00runnerdockerColin 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 Domenic Prete Mathieu Jeannin Paul Goulain John McMaster Dominik Kriegner Jonathan Larochelle Dominic Caron Mathieu Plante Michele Sardo Steven Siegl Benjamin Klebel-Knobloch Markus Röleke Demetra Adrahtas Dan McDonald Hud Wahab Nicola Corna Robert Eckelmann Sam Condon Andreas Maeder Bastian Leykauf Matthew Delaney Marco von Rosenberg Jack Van Sambeek JC Arbelbide Florian Jünger Benedikt Moneke Asaf Yagoda Fabio Garzetti Daniel Schmeer Mike Manno David Sanchez Sanchez Andres Ruz-Nieto Carlos Martinez Scott Candey Tom Verbeure Juraj Jašík Max Herbold Alexander Wichers Ashok Bruno Robert Roos Sebastien Weber Sebastian Neusch Ulrich Sauter Guus Kuiper Armindo Pinto Frank Wu Heinz-Alexander Fütterer Per-Olof Svensson Karl Komierowski Alec Vercruysse Matthew Zenaldin Canyon Clark Connor Carr Jannis Kleine-Schönepauck Douwe den Blanken Till Zürner J. A. Wilcox Nick James Kirkby Konrad Gralher././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/CHANGES.rst0000644000175100001770000010704514623331163015026 0ustar00runnerdocker Version 0.14.0 (2024-05-22) =========================== Main items of this new release: - Add support for numpy 2.0 - Add support for python 3.12 - Improve academic quotability with an up to date Zenodo DOI and with citation information. - Add default :code:`queue` method and a :code:`FileInputWidget`, allowing to more quickly get started with the PyMeasure user interface (:code:`ManagedWindow`). - Add a :code:`SCPIMixin` base class for instruments instead of defining :code:`includeSCPI=True` - Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. Removing manufacturer modules from that file lowers the memory footprint of pymeasure when importing an instrument. Instrument classes will need to be imported from the manufacturer module or explicitly from the instrument driver file. For example, :code:`from pymeasure.instruments import Extreme5000` will need to change to :code:`from pymeasure.instruments.extreme import Extreme5000` or :code:`from pymeasure.instruments.extreme.extreme5000 import Extreme5000`. - 17 new instruments Deprecated features ------------------- - Remove :code:`TelnetAdapter`, as its library is deprecated (@BenediktBurger, #1045) - Replaced :code:`directory_input` keyword-argument of :code:`ManagedWindowBase` by :code:`enable_file_input` (@CasperSchippers, #964) - Make parameter :code:`includeSCPI` obligatory for all instruments, even those which use SCPI (@BenediktBurger, #1007) - Setting `includeSCPI=True` is deprecated, inherit instead the :code:`SCPIMixin` class if the device supports SCPI commands. - Replaced :code:`celcius` attribute of :code:`LakeShoreTemperatureChannel` by :code:`celsius` (@afuetterer, #1003) - Replaced :code:`error` property of Keithley instruments by :code:`next_error`. - Replaced :code:`measurement_time` property of Pendulum CNT-91 by :code:`gate_time`. - Replaced :code:`sample_rate` keyword-argument of :code:`buffer_frequency_time_series` of Pendulum CNT-91 by :code:`gate_time`. - Replaced MKS937B :code:`unit` to use :code:`instruments/mksinst/mks937b/Unit` instead of strings (@dkriegner, @BenediktBurger #1034) Instruments mechanics --------------------- - Add a SCPI base class :code:`SCPIMixin` as replacement for :code:`includeSCPI=True` (@BenediktBurger, #905, #1007, #1019, #1047) - Add :code:`next_error` property to SCPI instruments (@BenediktBurger, #1024) - Make :code:`query_delay=None` the default for :code:`wait_for` (@BenediktBurger, #1077) - Fix :code:`expected_protocol` using empty dictionary as default value (@BenediktBurger, #1087) - Remove auto-importing all instruments in :code:`pymeasure/instruments/__init__.py`` (@mcdo0486, #919) - Add :code:`find_serial_port` to find a serial port by providing USB information (@BenediktBurger, #982) Instruments ----------- - Add Agilent4294A (@driftregion, #998) - Add Agilent 4284A by (@ConnorGCarr #1079) - Add AimTTI PL series power supplies (@guuskuiper, #942) - Add HP11713A Switch & Attenuator Driver (@neuschs, #970) - Add HP437B power meter (@neuschs, #979) - Add Inficon SQM160 SQM-160 multi-film rate/thickness monitor (@dkriegner, #991) - Add Keithley 2182 (@ConnorGCarr, #1043) - Add KeithleyDMM6500 (@fwutw, #963) - Add Kepco BOP 36-12 Bipolar Power Supply (@JAW90, #1086) - Add KeysightE3631A (@OptimisticBeliever, #990) - Add Kuhne Electronic KU SG 2.45 250A microwave generator (@jurajjasik, @BenediktBurger, @1108) - Add MKS 974B vacuum pressure transducer (@dkriegner, #1034) - Add Proterial rod4 (@ConnorGCarr, #1044) - Add Racal-Dana 1992 universal counter (@tomverbeure, #798, #1012) - Add redpitaya board (@seb5g, #1010, #1035) - Add Teledyne HDO6xxx (@RobertoRoos, #868) - Add Yokogawa AQ6370D Optical Spectral Analyzer (@jnnskls, #1059) - Fix property docstrings of several instruments (@BenediktBurger, #1018) - Fix checksums of hcp TC038D tests (@BenediktBurger, #987) - Fix Hp8116a (@BenediktBurger, #1088) - Fix Hp856x to append amplitude units (@neuschs, #977) - Fix Keysight E36312A confirmed SCPI functionality (@Konradrundfunk, #1107) - Fix Stanford Research SR830 output conversion (@dkriegner, #1069) - Fix SR830 missing get_buffer method (@seb5g, #999) - Fix set command of SR860 aux output (@wehlgrundspitze, #1048) - Fix Temptronic test to use ns perf counter (@BenediktBurger, #1109, #1110) - Fix Toptica Ibeamsmart referencing removed adapter function (@BenediktBurger, #1065) - Fix typos in docstrings for Keithley instruments (@V0XNIHILI, #1071) - Link Keysight, Agilent, and HP documentation pages. (@BenediktBurger, #1021) - Update Agilent33500 Series from :code:`.ch[]` to :code:`.channels[]` (@AlecVercruysse, #945) - Update AWG401x driver to use 'channels' (@mcdo0486, #944) - Update HP33120A with new burst modulation parameters (@mzen228, #1056) - Update HP34401A with new remote control command. (@Rybok, #992) - Update Keithleys' next_error (@msmttchr, #1030) - Update pendulum CNT-91 (@bleykauf, #988) GUI --- - Add a :code:`FileInputWidget` to choose if and where the experiment data is stored. (@CasperSchippers, #964) - Add a default :code:`Queue` method for :code:`ManagedWindowBase` is implemented. (@CasperSchippers, #964) - Fix :code:`ScientificInput` to be locale compatible (@pyZerrenner, #1074) - Fix exception if loading result file with an empty parameter (@poje42, #1016) Miscellaneous ------------- - Add support for python 3.12 (@BenediktBurger, #1051) - Add support for numpy 2.0 (@CasperSchippers, #1026) - Add codecov to CI and to readme (@BenediktBurger, #1037, #1052, #1099) - Add citation file for PyMeasure repository (@mcdo0486, #1092) - Add release CI (@BenediktBurger, #1039) - Update readme with permanent Zenodo DOI (@BenediktBurger, #1095) - Bump CI dependencies to: pyvisa 1.13.0, checkout@v4 (@mcdo0486, #1097) - Fix/pandas futurewarning (@CasperSchippers, #1062) - Change copyright year. (@BenediktBurger, #1032) - Fix typos (@afuetterer, #1003) New Contributors ---------------- @guuskuiper, @OptimisticBeliever, @fwutw, @afuetterer, @poje42, @Rybok, @AlecVercruysse, @ConnorGCarr, @mzen228, @jnnskls, @V0XNIHILI, @pyZerrenner, @JAW90, @driftregion, @jurajjasik, @Konradrundfunk **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.13.1...v0.14.0 Version 0.13.1 (2023-10-05) =========================== New release to fix ineffective python version restriction in the project metadata (only affected Python<=3.7 environments installing via pip). Version 0.13.0 (2023-09-23) =========================== Main items of this new release: - Dropped support for Python 3.7, added support for Python 3.11. - Adds a test generator, which observes the communication with an actual device and writes protocol tests accordingly. - 2 new instrument drivers have been added. Deprecated features ------------------- - Attocube ANC300: The :code:`stepu` and :code:`stepd` properties are deprecated, use the new :code:`move_raw` method instead. (@dkriegner, #938) Instruments ----------- - Adds a test generator (@bmoneke, #882) - Adds Thyracont Smartline v2 vacuum sensor transmitter (@bmoneke, #940) - Adds Thyracont Smartline v1 vacuum gauge (@dkriegner, #937) - AddsTeledyne base classes with most of `LeCroyT3DSO1204` functionality (@RobertoRoos, #951) - Fixes instrument documentation (@mcdo0486, #941, #903, @omahs, #960) - Fixes Toptica Ibeamsmart's __init__ (@waveman68, #959) - Fixes VISAAdapter flush_read_buffer() (@ileu, #968) - Updates Keithley2306 and AFG3152C to Channels (@bilderbuchi, #953) GUI --- - Adds console mode (@msmttchr, #500) - Fixes Dock widget (@msmttchr, #961) Miscellaneous ------------- - Change CI from conda to mamba (@bmoneke, #947) - Add support for python 3.11 (@CasperSchippers, #896) New Contributors ---------------- @waveman68, @omahs, @ileu **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.12.0...v0.13.0 Version 0.12.0 (2023-07-05) =========================== Main items of this new release: - A :code:`Channel` base class has been added for easier implementation of instruments with channels. - 19 new instrument drivers have been added. - Added tests for some commonalities across all instruments. - We continue to clean up our API in preparation for a future version 1.0. Deprecations and subsequent removals are listed below. Deprecated features ------------------- - HP 34401A: :code:`voltage_ac`, :code:`current_dc`, :code:`current_ac`, :code:`resistance`, :code:`resistance_4w` properties, use :code:`function_` and :code:`reading` properties instead. - Toptica IBeamSmart: :code:`channel1_enabled`, use :code:`ch_1.enabled` property instead (equivalent for channel2). Also :code:`laser_enabled` is deprecated in favor of :code:`emission` (@bmoneke, #819). - TelnetAdapter: use :code:`VISAAdapter` instead. VISA supports TCPIP connections. Use the resource_name :code:`TCPIP[board]::::::SOCKET` to connect to a server (@Max-Herbold, #835). - Attocube ANC300: :code:`host` argument, pass a resource string or adapter as :code:`Adapter` passed to :code:`Instrument`. Now communicates through the :code:`VISAAdapter` rather than deprecated :code:`TelnetAdapter`. The initializer now accepts :code:`name` as its second keyword argument so all previous initialization positional arguments (`axisnames`, `passwd`, `query_delay`) should be switched to keyword arguments. - The property creators :code:`control`, :code:`measurement`, and :code:`setting` do not accept arbitrary keyword arguments anymore. Use the :code:`v_kwargs` parameter for arguments you want to pass on to :code:`values` method, instead. - The property creators :code:`control`, :code:`measurement`, and :code:`setting` do not accept `command_process` anymore. Use a dynamic property or a `Channel` instead, as appropriate (@bmoneke, #878). - See also the next section. New adapter and instrument mechanics ------------------------------------ - All instrument constructors are required to accept a :code:`name` argument. - Changed: :code:`read_bytes` of all Adapters by default does not stop reading on a termination character, unless the new argument :code:`break_on_termchar` is set to `True`. - Channel class added. :code:`Instrument.channels` and :code:`Instrument.ch_X` (:code:`X` is any channel name) are reserved attributes for channel mechanics. - The parameters :code:`check_get_errors` and :code:`check_set_errors` enable calling methods of the same name. This enables more systematically dealing with instruments that acknowledge every "set" command. - Adds Channel feature to instruments (@bmoneke, mcdo0486, #718, #761, #852, #931) - Adds :code:`maxsplit` parameter to :code:`values` method (@bmoneke, #793) - Adds (deprecated) global preprocess reply for backward compatibility (@bmoneke, #876) - Adds fallback version for discarding the read buffer to VISAAdapter (@dkriegner, #836) - Adds :code:`flush_read_buffer` to SerialAdapter (@RobertoRoos, #865) - Adds :code:`gpib_read_timeout` to PrologixAdapter (@neuschs, #927) - Adds command line option to pass resource address for instrument tests (@bleykauf, #789) - Adds "find all instruments" and channels for testing (@bmoneke, #909, @mcdo0486, #911, #912) - Adds test that an instrument hands kwargs to the adapter (@bmoneke, #814) - Adds property docstring check (@bmoneke, #895) - Improves property factories' docstrings (@bmoneke, #843) - Improves property factories: do not allow undefined kwargs (@bmoneke, #856) - Improves property factories: check_set/get_errors argument to call methods of the same name (@bmoneke, #883) - Improves :code:`read_bytes` of Adapter (@bmoneke, #839) - Improves the ProtocolAdapter with a mock connection (@bmoneke, #782), and enable it to have empty messages in the protocol (@bmoneke, #818) - Improves Prologix adapter documentation (@bmoneke, #813) and configurable settings (@bmoneke, #845) - Improves behavior of :code:`read_bytes(-1)` for :code:`SerialAdapter` (@RobertoRoos, #866) - Improves all instruments with name kwarg (@bmoneke, #877) - Improves VisaAdapter: close manager only when using pyvisa-sim (@dkriegner, #900) - Harmonises instrument name definition pattern, consistently name the instrument connection argument "adapter" (@bmoneke, #659) - Fixes ProtocolAdapter has list in signature (@bmoneke, #901) - Fixes VISAAdapter's :code:`read_bytes` (@bmoneke, #867) - Fixes query_delay usage in VISAAdapter (@bmoneke, #765) - Fixes VisaAdapter: close resource manager only when using pyvisa-sim (@dkriegner, #900) Instruments ----------- - New Advantest R624X DC Voltage/Current Sources/Monitors (@wichers, #802) - New AJA International DC sputtering power supply (@dkriegner, #778) - New Anritus MS2090A (@aruznieto, #787) - New Anritsu MS4644B (@CasperSchippers, #827) - New DSP 7225 and new DSPBase instrument (@mcdo0486, #902) - New HP 8560A / 8561B Spectrum Analyzer (@neuschs, #888) - New IPG Photonics YAR Amplifier series (@bmoneke, #851) - New Keysight E36312A power supply (@scandey, #785) - New Keithley 2200 power supply (@ashokbruno, #806) - New Lake Shore 211 Temperature Monitor (@mcdo0486, #889) - New Lake Shore 224 and improves Lakeshore instruments (@samcondon4, #870) - New MKS Instruments 937B vacuum gauge controller (@dkriegner, @bilderbuchi, #637, #772, #936) - New Novanta FPU60 laser power supply unit (@bmoneke, #885) - New TDK Lambda Genesys 80-65 DC and 40-38 DC power supplies (@mcdo0486, 906) - New Teledyne T3AFG waveform generator instrument (@scandey, #791) - New Teledyne (LeCroy) T3DSO1204 Oscilloscope (@LastStartDust, #697, @bilderbuchi, #770) - New T&C Power Conversion RF power supply (@dkriegner, #800) - New Velleman K8090 relay device (@RobertoRoos, #859) - Improves Agilent 33500 with the new channel feature (@JCarl-OS, #763, #773) - Improves HP 3478A with calibration data related functions (@tomverbeure, #777) - Improves HP 34401A (@CodingMarco, #810) - Improves the Oxford instruments with the new channel feature (@bmoneke, #844) - Improves Siglent SPDxxxxX with the new channel feature (@AidenDawn 758) - Improves Teledyne T3DSO1204 device tests (@LastStarDust, #841) - Fixes Ametek DSP 7270 lockin amplifier issues (@seb5g, #897) - Fixes DSP 7265 erroneously using preprocess_reply (@mcdo0486, #873) - Fixes print statement in DSPBase.sensitivity (@mcdo0486, #915) - Fixes Fluke bath commands (@bmoneke, #874) - Fixes a frequency limitation in HP 8657B (@LongnoseRob, #769) - Fixes Keithley 2600 channel calling parent's shutdown (@mcdo0486, #795) Automation ---------- - Adds tolerance for opening result files with missing parameters (@msmttchr, #780) - Validate DATA_COLUMNS entries earlier, avoid exceptions in a running procedure (@mcdo0486, #796, #934) GUI --- - Adds docking windows (@mcdo0486, #722, #762) - Adds save plot settings in addition to dock layout (@mcdo0486, #850) - Adds log widget colouring and format option (@CasperSchippers, #890) - Adds table widget (@msmttchr, #771) - New sequencer architecture: decouples it from the graphical tree, adapts it for further expansions (@msmttchr, #518) - Moves coordinates label to the pyqtgraph PlotItem (@CasperSchippers, #822) - Fixes crashing ImageWidget at new measurement (@CasperSchippers, #790) - Fixes checkboxes not working for groups in inputs-widget (@CasperSchippers, #794) Miscellaneous ------------- - Adds a collection of solutions for instrument implementation challenges (@bmoneke, #853, #861) - Updates Tutorials/Making_a_measurement/ example_codes (@sansanda, #749) New Contributors ---------------- @JCarl-OS, @aruznieto, @scandey, @tomverbeure, @wichers, @Max-Herbold, @RobertoRoos **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.11.1...v0.12.0 Version 0.11.1 (2022-12-31) =========================== Adapter and instrument mechanics -------------------------------- - Fix broken `PrologixAdapter.gpib`. Due to a bug in `VISAAdapter`, you could not get a second adapter with that connection (#765). **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.11.0...v0.11.1 Dependency updates ------------------ - Required version of `PyQtGraph `__ is increased from :code:`pyqtgraph >= 0.9.10` to :code:`pyqtgraph >= 0.12` to support new PyMeasure display widgets. GUI --- - Added `ManagedDockWindow `__ to allow multiple dockable plots (@mcdo0486, @CasperSchippers, #722) - Move coordinates label to the pyqtgraph PlotItem (@CasperSchippers, #822) - New sequencer architecture (@msmttchr, @CasperSchippers, @mcdo0486, #518) - Added "Save Dock Layout" functionality to DockWidget context menu. (@mcdo0486, #762) Version 0.11.0 (2022-11-19) =========================== Main items of this new release: - 11 new instrument drivers have been added - A method for testing instrument communication **without** hardware present has been added, see `the documentation `__. - The separation between :code:`Instrument` and :code:`Adapter` has been improved to make future modifications easier. Adapters now focus on the hardware communication, and the communication *protocol* should be defined in the Instruments. Details in a section below. - The GUI is now compatible with Qt6. - We have started to clean up our API in preparation for a future version 1.0. There will be deprecations and subsequent removals, which will be prominently listed in the changelog. Deprecated features ------------------- In preparation for a stable 1.0 release and a more consistent API, we have now started formally deprecating some features. You should get warnings if those features are used. - Adapter methods :code:`ask`, :code:`values`, :code:`binary_values`, use :code:`Instrument` methods of the same name instead. - Adapter parameter :code:`preprocess_reply`, override :code:`Instrument.read` instead. - :code:`Adapter.query_delay` in favor of :code:`Instrument.wait_for()`. - Keithley 2260B: :code:`enabled` property, use :code:`output_enabled` instead. New adapter and instrument mechanics ------------------------------------ - Nothing should have changed for users, this section is mainly interesting for instrument implementors. - Documentation in 'Advanced communication protocols' in 'Adding instruments'. - Adapter logs written and read messages. - Particular adapters (`VISAAdapter` etc.) implement the actual communication. - :code:`Instrument.control` getter calls :code:`Instrument.values`. - :code:`Instrument.values` calls :code:`Instrument.ask`, which calls :code:`Instrument.write`, :code:`wait_for`, and :code:`read`. - All protocol quirks of an instrument should be implemented overriding :code:`Instrument.write` and :code:`read`. - :code:`Instrument.wait_until_read` implements waiting between writing and reading. - reading/writing binary values is in the :code:`Adapter` class itself. - :code:`PrologixAdapter` is now based on :code:`VISAAdapter`. - :code:`SerialAdapter` improved to be more similar to :code:`VISAAdapter`: :code:`read`/:code:`write` use strings, :code:`read/write_bytes` bytes. - Support for termination characters added. Instruments ----------- - New Active Technologies AWG-401x (@garzetti, #649) - New Eurotest hpp_120_256_ieee (@sansanda, #701) - New HC Photonics crystal ovens TC038, TC038D (@bmoneke, #621, #706) - New HP 6632A/6633A/6634A power supplies (@LongnoseRob, #651) - New HP 8657B RF signal generator (@LongnoseRob, #732) - New Rohde&Schwarz HMP4040 power supply. (@bleykauf, #582) - New Siglent SPDxxxxX series Power Supplies (@AidenDawn, #719) - New Temptronic Thermostream devices (@mroeleke, #368) - New TEXIO PSW-360L30 Power Supply (@LastStarDust, #698) - New Thermostream ECO-560 (@AidenDawn, #679) - New Thermotron 3800 Oven (@jcarbelbide, #606) - Harmonize instruments' adapter argument (@bmoneke, #674) - Harmonize usage of :code:`shutdown` method (@LongnoseRob, #739) - Rework Adapter structure (@bmoneke, #660) - Add Protocol tests without hardware present (@bilderbuchi, #634, @bmoneke, #628, #635) - Add Instruments and adapter protocol tests for adapter rework (@bmoneke, #665) - Add SR830 sync filter and reference source trigger (@AsafYagoda, #630) - Add Keithley6221 phase marker phase and line (@AsafYagoda, #629) - Add missing docstrings to Keithley 2306 battery simulator (@AidenDawn, #720) - Fix hcp instruments documentation (@bmoneke, #671) - Fix HPLegacyInstrument initializer API (@bilderbuchi, #684) - Fix Fwbell 5080 implementation (@mcdo0486, #714) - Fix broken documentation example. (@bmoneke, #738) - Fix typo in Keithley 2600 driver (@mcdo0486, #615) - Remove dynamic use of docstring from ATS545 and make more generic (@AidenDawn, #685) Automation ---------- - Add storing unitful experiment results (@bmoneke, #642) - Add storing conditions in file (@CasperSchippers, #503) GUI --- - Add compatibility with Qt 6 (@CasperSchippers, #688) - Add spinbox functionality for IntegerParameter and FloatParameter (@jarvas24, #656) - Add "delete data file" button to the browser_item_menu (@jarvas24, #654) - Split windows.py into a folder with separate modules (@mcdo0486, #593) - Remove dependency on matplotlib (@msmttchr, #622) - Remove deprecated access to QtWidgets through QtGui (@maederan201, #695) Miscellaneous ------------- - Update and extend documentation (@bilderbuchi, #712, @bmoneke, #655) - Add PEP517 compatibility & dynamically obtaining a version number (@bilderbuchi, #613) - Add an example and documentation regarding using a foreign instrument (@bmoneke, #647) - Add black configuration (@bleykauf, #683) - Remove VISAAdapter.has_supported_version() as it is not needed anymore. New Contributors ---------------- @jcarbelbide, @mroeleke, @bmoneke, @garzetti, @AsafYagoda, @AidenDawn, @LastStarDust, @sansanda **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.10.0...v0.11.0 Version 0.10.0 (2022-04-09) =========================== Main items of this new release: - 23 new instrument drivers have been added - New dynamic Instrument properties can change their parameters at runtime - Communication settings can now be flexibly defined per protocol - Python 3.10 support was added and Python 3.6 support was removed. - Many additions, improvements and have been merged Instruments ----------- - New Agilent B1500 Data Formats and Documentation (@moritzj29) - New Anaheim Automation stepper motor controllers (@samcondon4) - New Andeen Hagerling capacitance bridges (@dkriegner) - New Anritsu MS9740A Optical Spectrum Analyzer (@md12g12) - New BK Precision 9130B Instrument (@dennisfeng2) - New Edwards nXDS (10i) Vacuum Pump (@hududed) - New Fluke 7341 temperature bath instrument (@msmttchr) - New Heidenhain ND287 Position Display Unit Driver (@samcondon4) - New HP 3478A (@LongnoseRob) - New HP 8116A 50 MHz Pulse/Function Generator (@CodingMarco) - New Keithley 2260B DC Power Supply (@bklebel) - New Keithley 2306 Dual Channel Battery/Charger Simulator (@mfikes) - New Keithley 2600 SourceMeter series (@Daivesd) - New Keysight N7776C Swept Laser Source (@maederan201) - New Lakeshore 421 (@CasperSchippers) - New Oxford IPS120-10 (@CasperSchippers) - New Pendulum CNT-91 frequency counter (@bleykauf) - New Rohde&Schwarz - SFM TV test transmitter (@LongnoseRob) - New Rohde&Schwarz FSL spectrum analyzer (@bleykauf) - New SR570 current amplifier driver (@pyMatJ) - New Stanford Research Systems SR510 instrument driver (@samcondon4) - New Toptica Smart Laser diode (@dkriegner) - New Yokogawa GS200 Instrument (@dennisfeng2) - Add output low grounded property to Keithley 6221 (@CasperSchippers) - Add shutdown function for Keithley 2260B (@bklebel) - Add phase control for Agilent 33500 (@corna) - Add assigning "ONCE" to auto_zero to Keithley 2400 (@mfikes) - Add line frequency controls to Keithley 2400 (@mfikes) - Add LIA and ERR status byte read properties to the SRS Sr830 driver (@samcondon4) - Add all commands to Oxford Intelligent Temperature Controller 503 (@CasperSchippers) - Fix DSP 7265 lockin amplifier (@CasperSchippers) - Fix bug in Keithley 6517B Electrometer (@CasperSchippers) - Fix Keithley2000 deprecated call to visa.config (@bklebel) - Fix bug in the Keithley 2700 (@CasperSchippers) - Fix setting of sensor flags for Thorlabs PM100D (@bleykauf) - Fix SCPI used for Keithley 2400 voltage NPLC (@mfikes) - Fix missing return statements in Tektronix AFG3152C (@bleykauf) - Fix DPSeriesMotorController bug (@samcondon4) - Fix Keithley2600 error when retrieving error code (@bicarlsen) - Fix Attocube ANC300 with new SCPI Instrument properties (@dkriegner) - Fix bug in wait_for_trigger of Agilent33220A (neal-kepler) GUI --- - Add time-estimator widget (@CasperSchippers) - Add management of progress bar (@msmttchr) - Remove broken errorbar feature (@CasperSchippers) - Change of pen width for pyqtgraph (@maederan201) - Make linewidth changeable (@CasperSchippers) - Generalise warning in plotter section (@CasperSchippers) - Implement visibility groups in InputsWidgets (@CasperSchippers) - Modify navigation of ManagedWindow directory widget (@jarvas24) - Improve Placeholder logic (@CasperSchippers) - Breakout widgets into separate modules (@mcdo0486) - Fix setSizePolicy bug with PySide2 (@msmttchr) - Fix managed window (@msmttchr) - Fix ListParameter for numbers (@moritzj29) - Fix incorrect columns on showing data (@CasperSchippers) - Fix procedure property issue (@msmttchr) - Fix pyside2 (@msmttchr) Miscellaneous ------------- - Improve SCPI property support (@msmttchr) - Remove broken safeKeyword management (@msmttchr) - Add dynamic property support (@msmttchr) - Add flexible API for defining connection configuration (@bilderbuchi) - Add write_binary_values() to SerialAdapter (@msmttchr) - Change an outdated pyvisa ask() to query() (@LongnoseRob) - Fix ZMQ bug (@bilderbuchi) - Documentation for passing tuples to control property (@bklebel) - Documentation bugfix (@CasperSchippers) - Fixed broken links in documentation. (@samcondon4) - Updated widget documentation (@mcdo0486) - Fix typo SCIP->SCPI (@mfikes) - Remove Python 3.6, add Python 3.10 testing (@bilderbuchi) - Modernise the code base to use Python 3.7 features (@bilderbuchi) - Added image data generation to Mock Instrument class (@samcondon4) - Add autodoc warnings to the problem matcher (@bilderbuchi) - Update CI & annotations (@bilderbuchi) - Test workers (@mcdo0486) - Change copyright date to 2022 (@LongnoseRob) - Removed unused code (@msmttchr) New Contributors ---------------- @LongnoseRob, @neal, @hududed, @corna, @Daivesd, @samcondon4, @maederan201, @bleykauf, @mfikes, @bicarlsen, @md12g12, @CodingMarco, @jarvas24, @mcdo0486! **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.9...v0.10.0 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 :code:`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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/CITATION.cff0000644000175100001770000000040714623331163015110 0ustar00runnerdockercff-version: 1.2.0 message: "If you use this software, please cite it as below." authors: - name: PyMeasure Developers title: "PyMeasure" version: 0.14.0 doi: 10.5281/zenodo.595633 publisher: - name: Zenodo repository-code: https://github.com/pymeasure/pymeasure ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/LICENSE.txt0000644000175100001770000000205514623331163015042 0ustar00runnerdockerCopyright (c) 2013-2024 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/MANIFEST.in0000644000175100001770000000025314623331163014753 0ustar00runnerdocker# Files get pulled from Git repo by setuptools_scm, we only have to exclude what we don't want prune docs/_build prune .github exclude .gitignore exclude .readthedocs.yml ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4496064 pymeasure-0.14.0/PKG-INFO0000644000175100001770000011657514623331176014335 0ustar00runnerdockerMetadata-Version: 2.1 Name: PyMeasure Version: 0.14.0 Summary: Scientific measurement library for instruments, experiments, and live-plotting Home-page: https://github.com/pymeasure/pymeasure Author: PyMeasure Developers License: MIT Keywords: measure,instrument,experiment control,automate,graph,plot 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.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Scientific/Engineering Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE.txt Requires-Dist: numpy<3,>=1.6.1 Requires-Dist: pandas<3,>=0.14 Requires-Dist: pint Requires-Dist: pyvisa>=1.9 Requires-Dist: pyserial>=2.7 Requires-Dist: pyqtgraph>=0.12 Requires-Dist: importlib-metadata; python_version < "3.8" Provides-Extra: tcp Requires-Dist: pyzmq>=16.0.2; extra == "tcp" Requires-Dist: cloudpickle>=0.3.1; extra == "tcp" Provides-Extra: python-vxi11 Requires-Dist: python-vxi11>=0.9; extra == "python-vxi11" Provides-Extra: tests Requires-Dist: pytest>=3.3.0; extra == "tests" Requires-Dist: pytest-cov>=4.1.0; extra == "tests" Requires-Dist: pytest-qt>=2.4.0; extra == "tests" Requires-Dist: pyvisa-sim>=0.4.0; extra == "tests" .. 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.8-3.12, and is tested with continuous-integration on Linux, macOS, and Windows. .. image:: https://github.com/pymeasure/pymeasure/actions/workflows/pymeasure_CI.yml/badge.svg :target: https://github.com/pymeasure/pymeasure/actions/workflows/pymeasure_CI.yml .. 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.595633.svg :target: https://doi.org/10.5281/zenodo.595633 .. 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 .. image:: https://codecov.io/gh/pymeasure/pymeasure/graph/badge.svg :target: https://codecov.io/gh/pymeasure/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.14.0 (2024-05-22) =========================== Main items of this new release: - Add support for numpy 2.0 - Add support for python 3.12 - Improve academic quotability with an up to date Zenodo DOI and with citation information. - Add default :code:`queue` method and a :code:`FileInputWidget`, allowing to more quickly get started with the PyMeasure user interface (:code:`ManagedWindow`). - Add a :code:`SCPIMixin` base class for instruments instead of defining :code:`includeSCPI=True` - Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. Removing manufacturer modules from that file lowers the memory footprint of pymeasure when importing an instrument. Instrument classes will need to be imported from the manufacturer module or explicitly from the instrument driver file. For example, :code:`from pymeasure.instruments import Extreme5000` will need to change to :code:`from pymeasure.instruments.extreme import Extreme5000` or :code:`from pymeasure.instruments.extreme.extreme5000 import Extreme5000`. - 17 new instruments Deprecated features ------------------- - Remove :code:`TelnetAdapter`, as its library is deprecated (@BenediktBurger, #1045) - Replaced :code:`directory_input` keyword-argument of :code:`ManagedWindowBase` by :code:`enable_file_input` (@CasperSchippers, #964) - Make parameter :code:`includeSCPI` obligatory for all instruments, even those which use SCPI (@BenediktBurger, #1007) - Setting `includeSCPI=True` is deprecated, inherit instead the :code:`SCPIMixin` class if the device supports SCPI commands. - Replaced :code:`celcius` attribute of :code:`LakeShoreTemperatureChannel` by :code:`celsius` (@afuetterer, #1003) - Replaced :code:`error` property of Keithley instruments by :code:`next_error`. - Replaced :code:`measurement_time` property of Pendulum CNT-91 by :code:`gate_time`. - Replaced :code:`sample_rate` keyword-argument of :code:`buffer_frequency_time_series` of Pendulum CNT-91 by :code:`gate_time`. - Replaced MKS937B :code:`unit` to use :code:`instruments/mksinst/mks937b/Unit` instead of strings (@dkriegner, @BenediktBurger #1034) Instruments mechanics --------------------- - Add a SCPI base class :code:`SCPIMixin` as replacement for :code:`includeSCPI=True` (@BenediktBurger, #905, #1007, #1019, #1047) - Add :code:`next_error` property to SCPI instruments (@BenediktBurger, #1024) - Make :code:`query_delay=None` the default for :code:`wait_for` (@BenediktBurger, #1077) - Fix :code:`expected_protocol` using empty dictionary as default value (@BenediktBurger, #1087) - Remove auto-importing all instruments in :code:`pymeasure/instruments/__init__.py`` (@mcdo0486, #919) - Add :code:`find_serial_port` to find a serial port by providing USB information (@BenediktBurger, #982) Instruments ----------- - Add Agilent4294A (@driftregion, #998) - Add Agilent 4284A by (@ConnorGCarr #1079) - Add AimTTI PL series power supplies (@guuskuiper, #942) - Add HP11713A Switch & Attenuator Driver (@neuschs, #970) - Add HP437B power meter (@neuschs, #979) - Add Inficon SQM160 SQM-160 multi-film rate/thickness monitor (@dkriegner, #991) - Add Keithley 2182 (@ConnorGCarr, #1043) - Add KeithleyDMM6500 (@fwutw, #963) - Add Kepco BOP 36-12 Bipolar Power Supply (@JAW90, #1086) - Add KeysightE3631A (@OptimisticBeliever, #990) - Add Kuhne Electronic KU SG 2.45 250A microwave generator (@jurajjasik, @BenediktBurger, @1108) - Add MKS 974B vacuum pressure transducer (@dkriegner, #1034) - Add Proterial rod4 (@ConnorGCarr, #1044) - Add Racal-Dana 1992 universal counter (@tomverbeure, #798, #1012) - Add redpitaya board (@seb5g, #1010, #1035) - Add Teledyne HDO6xxx (@RobertoRoos, #868) - Add Yokogawa AQ6370D Optical Spectral Analyzer (@jnnskls, #1059) - Fix property docstrings of several instruments (@BenediktBurger, #1018) - Fix checksums of hcp TC038D tests (@BenediktBurger, #987) - Fix Hp8116a (@BenediktBurger, #1088) - Fix Hp856x to append amplitude units (@neuschs, #977) - Fix Keysight E36312A confirmed SCPI functionality (@Konradrundfunk, #1107) - Fix Stanford Research SR830 output conversion (@dkriegner, #1069) - Fix SR830 missing get_buffer method (@seb5g, #999) - Fix set command of SR860 aux output (@wehlgrundspitze, #1048) - Fix Temptronic test to use ns perf counter (@BenediktBurger, #1109, #1110) - Fix Toptica Ibeamsmart referencing removed adapter function (@BenediktBurger, #1065) - Fix typos in docstrings for Keithley instruments (@V0XNIHILI, #1071) - Link Keysight, Agilent, and HP documentation pages. (@BenediktBurger, #1021) - Update Agilent33500 Series from :code:`.ch[]` to :code:`.channels[]` (@AlecVercruysse, #945) - Update AWG401x driver to use 'channels' (@mcdo0486, #944) - Update HP33120A with new burst modulation parameters (@mzen228, #1056) - Update HP34401A with new remote control command. (@Rybok, #992) - Update Keithleys' next_error (@msmttchr, #1030) - Update pendulum CNT-91 (@bleykauf, #988) GUI --- - Add a :code:`FileInputWidget` to choose if and where the experiment data is stored. (@CasperSchippers, #964) - Add a default :code:`Queue` method for :code:`ManagedWindowBase` is implemented. (@CasperSchippers, #964) - Fix :code:`ScientificInput` to be locale compatible (@pyZerrenner, #1074) - Fix exception if loading result file with an empty parameter (@poje42, #1016) Miscellaneous ------------- - Add support for python 3.12 (@BenediktBurger, #1051) - Add support for numpy 2.0 (@CasperSchippers, #1026) - Add codecov to CI and to readme (@BenediktBurger, #1037, #1052, #1099) - Add citation file for PyMeasure repository (@mcdo0486, #1092) - Add release CI (@BenediktBurger, #1039) - Update readme with permanent Zenodo DOI (@BenediktBurger, #1095) - Bump CI dependencies to: pyvisa 1.13.0, checkout@v4 (@mcdo0486, #1097) - Fix/pandas futurewarning (@CasperSchippers, #1062) - Change copyright year. (@BenediktBurger, #1032) - Fix typos (@afuetterer, #1003) New Contributors ---------------- @guuskuiper, @OptimisticBeliever, @fwutw, @afuetterer, @poje42, @Rybok, @AlecVercruysse, @ConnorGCarr, @mzen228, @jnnskls, @V0XNIHILI, @pyZerrenner, @JAW90, @driftregion, @jurajjasik, @Konradrundfunk **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.13.1...v0.14.0 Version 0.13.1 (2023-10-05) =========================== New release to fix ineffective python version restriction in the project metadata (only affected Python<=3.7 environments installing via pip). Version 0.13.0 (2023-09-23) =========================== Main items of this new release: - Dropped support for Python 3.7, added support for Python 3.11. - Adds a test generator, which observes the communication with an actual device and writes protocol tests accordingly. - 2 new instrument drivers have been added. Deprecated features ------------------- - Attocube ANC300: The :code:`stepu` and :code:`stepd` properties are deprecated, use the new :code:`move_raw` method instead. (@dkriegner, #938) Instruments ----------- - Adds a test generator (@bmoneke, #882) - Adds Thyracont Smartline v2 vacuum sensor transmitter (@bmoneke, #940) - Adds Thyracont Smartline v1 vacuum gauge (@dkriegner, #937) - AddsTeledyne base classes with most of `LeCroyT3DSO1204` functionality (@RobertoRoos, #951) - Fixes instrument documentation (@mcdo0486, #941, #903, @omahs, #960) - Fixes Toptica Ibeamsmart's __init__ (@waveman68, #959) - Fixes VISAAdapter flush_read_buffer() (@ileu, #968) - Updates Keithley2306 and AFG3152C to Channels (@bilderbuchi, #953) GUI --- - Adds console mode (@msmttchr, #500) - Fixes Dock widget (@msmttchr, #961) Miscellaneous ------------- - Change CI from conda to mamba (@bmoneke, #947) - Add support for python 3.11 (@CasperSchippers, #896) New Contributors ---------------- @waveman68, @omahs, @ileu **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.12.0...v0.13.0 Version 0.12.0 (2023-07-05) =========================== Main items of this new release: - A :code:`Channel` base class has been added for easier implementation of instruments with channels. - 19 new instrument drivers have been added. - Added tests for some commonalities across all instruments. - We continue to clean up our API in preparation for a future version 1.0. Deprecations and subsequent removals are listed below. Deprecated features ------------------- - HP 34401A: :code:`voltage_ac`, :code:`current_dc`, :code:`current_ac`, :code:`resistance`, :code:`resistance_4w` properties, use :code:`function_` and :code:`reading` properties instead. - Toptica IBeamSmart: :code:`channel1_enabled`, use :code:`ch_1.enabled` property instead (equivalent for channel2). Also :code:`laser_enabled` is deprecated in favor of :code:`emission` (@bmoneke, #819). - TelnetAdapter: use :code:`VISAAdapter` instead. VISA supports TCPIP connections. Use the resource_name :code:`TCPIP[board]::::::SOCKET` to connect to a server (@Max-Herbold, #835). - Attocube ANC300: :code:`host` argument, pass a resource string or adapter as :code:`Adapter` passed to :code:`Instrument`. Now communicates through the :code:`VISAAdapter` rather than deprecated :code:`TelnetAdapter`. The initializer now accepts :code:`name` as its second keyword argument so all previous initialization positional arguments (`axisnames`, `passwd`, `query_delay`) should be switched to keyword arguments. - The property creators :code:`control`, :code:`measurement`, and :code:`setting` do not accept arbitrary keyword arguments anymore. Use the :code:`v_kwargs` parameter for arguments you want to pass on to :code:`values` method, instead. - The property creators :code:`control`, :code:`measurement`, and :code:`setting` do not accept `command_process` anymore. Use a dynamic property or a `Channel` instead, as appropriate (@bmoneke, #878). - See also the next section. New adapter and instrument mechanics ------------------------------------ - All instrument constructors are required to accept a :code:`name` argument. - Changed: :code:`read_bytes` of all Adapters by default does not stop reading on a termination character, unless the new argument :code:`break_on_termchar` is set to `True`. - Channel class added. :code:`Instrument.channels` and :code:`Instrument.ch_X` (:code:`X` is any channel name) are reserved attributes for channel mechanics. - The parameters :code:`check_get_errors` and :code:`check_set_errors` enable calling methods of the same name. This enables more systematically dealing with instruments that acknowledge every "set" command. - Adds Channel feature to instruments (@bmoneke, mcdo0486, #718, #761, #852, #931) - Adds :code:`maxsplit` parameter to :code:`values` method (@bmoneke, #793) - Adds (deprecated) global preprocess reply for backward compatibility (@bmoneke, #876) - Adds fallback version for discarding the read buffer to VISAAdapter (@dkriegner, #836) - Adds :code:`flush_read_buffer` to SerialAdapter (@RobertoRoos, #865) - Adds :code:`gpib_read_timeout` to PrologixAdapter (@neuschs, #927) - Adds command line option to pass resource address for instrument tests (@bleykauf, #789) - Adds "find all instruments" and channels for testing (@bmoneke, #909, @mcdo0486, #911, #912) - Adds test that an instrument hands kwargs to the adapter (@bmoneke, #814) - Adds property docstring check (@bmoneke, #895) - Improves property factories' docstrings (@bmoneke, #843) - Improves property factories: do not allow undefined kwargs (@bmoneke, #856) - Improves property factories: check_set/get_errors argument to call methods of the same name (@bmoneke, #883) - Improves :code:`read_bytes` of Adapter (@bmoneke, #839) - Improves the ProtocolAdapter with a mock connection (@bmoneke, #782), and enable it to have empty messages in the protocol (@bmoneke, #818) - Improves Prologix adapter documentation (@bmoneke, #813) and configurable settings (@bmoneke, #845) - Improves behavior of :code:`read_bytes(-1)` for :code:`SerialAdapter` (@RobertoRoos, #866) - Improves all instruments with name kwarg (@bmoneke, #877) - Improves VisaAdapter: close manager only when using pyvisa-sim (@dkriegner, #900) - Harmonises instrument name definition pattern, consistently name the instrument connection argument "adapter" (@bmoneke, #659) - Fixes ProtocolAdapter has list in signature (@bmoneke, #901) - Fixes VISAAdapter's :code:`read_bytes` (@bmoneke, #867) - Fixes query_delay usage in VISAAdapter (@bmoneke, #765) - Fixes VisaAdapter: close resource manager only when using pyvisa-sim (@dkriegner, #900) Instruments ----------- - New Advantest R624X DC Voltage/Current Sources/Monitors (@wichers, #802) - New AJA International DC sputtering power supply (@dkriegner, #778) - New Anritus MS2090A (@aruznieto, #787) - New Anritsu MS4644B (@CasperSchippers, #827) - New DSP 7225 and new DSPBase instrument (@mcdo0486, #902) - New HP 8560A / 8561B Spectrum Analyzer (@neuschs, #888) - New IPG Photonics YAR Amplifier series (@bmoneke, #851) - New Keysight E36312A power supply (@scandey, #785) - New Keithley 2200 power supply (@ashokbruno, #806) - New Lake Shore 211 Temperature Monitor (@mcdo0486, #889) - New Lake Shore 224 and improves Lakeshore instruments (@samcondon4, #870) - New MKS Instruments 937B vacuum gauge controller (@dkriegner, @bilderbuchi, #637, #772, #936) - New Novanta FPU60 laser power supply unit (@bmoneke, #885) - New TDK Lambda Genesys 80-65 DC and 40-38 DC power supplies (@mcdo0486, 906) - New Teledyne T3AFG waveform generator instrument (@scandey, #791) - New Teledyne (LeCroy) T3DSO1204 Oscilloscope (@LastStartDust, #697, @bilderbuchi, #770) - New T&C Power Conversion RF power supply (@dkriegner, #800) - New Velleman K8090 relay device (@RobertoRoos, #859) - Improves Agilent 33500 with the new channel feature (@JCarl-OS, #763, #773) - Improves HP 3478A with calibration data related functions (@tomverbeure, #777) - Improves HP 34401A (@CodingMarco, #810) - Improves the Oxford instruments with the new channel feature (@bmoneke, #844) - Improves Siglent SPDxxxxX with the new channel feature (@AidenDawn 758) - Improves Teledyne T3DSO1204 device tests (@LastStarDust, #841) - Fixes Ametek DSP 7270 lockin amplifier issues (@seb5g, #897) - Fixes DSP 7265 erroneously using preprocess_reply (@mcdo0486, #873) - Fixes print statement in DSPBase.sensitivity (@mcdo0486, #915) - Fixes Fluke bath commands (@bmoneke, #874) - Fixes a frequency limitation in HP 8657B (@LongnoseRob, #769) - Fixes Keithley 2600 channel calling parent's shutdown (@mcdo0486, #795) Automation ---------- - Adds tolerance for opening result files with missing parameters (@msmttchr, #780) - Validate DATA_COLUMNS entries earlier, avoid exceptions in a running procedure (@mcdo0486, #796, #934) GUI --- - Adds docking windows (@mcdo0486, #722, #762) - Adds save plot settings in addition to dock layout (@mcdo0486, #850) - Adds log widget colouring and format option (@CasperSchippers, #890) - Adds table widget (@msmttchr, #771) - New sequencer architecture: decouples it from the graphical tree, adapts it for further expansions (@msmttchr, #518) - Moves coordinates label to the pyqtgraph PlotItem (@CasperSchippers, #822) - Fixes crashing ImageWidget at new measurement (@CasperSchippers, #790) - Fixes checkboxes not working for groups in inputs-widget (@CasperSchippers, #794) Miscellaneous ------------- - Adds a collection of solutions for instrument implementation challenges (@bmoneke, #853, #861) - Updates Tutorials/Making_a_measurement/ example_codes (@sansanda, #749) New Contributors ---------------- @JCarl-OS, @aruznieto, @scandey, @tomverbeure, @wichers, @Max-Herbold, @RobertoRoos **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.11.1...v0.12.0 Version 0.11.1 (2022-12-31) =========================== Adapter and instrument mechanics -------------------------------- - Fix broken `PrologixAdapter.gpib`. Due to a bug in `VISAAdapter`, you could not get a second adapter with that connection (#765). **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.11.0...v0.11.1 Dependency updates ------------------ - Required version of `PyQtGraph `__ is increased from :code:`pyqtgraph >= 0.9.10` to :code:`pyqtgraph >= 0.12` to support new PyMeasure display widgets. GUI --- - Added `ManagedDockWindow `__ to allow multiple dockable plots (@mcdo0486, @CasperSchippers, #722) - Move coordinates label to the pyqtgraph PlotItem (@CasperSchippers, #822) - New sequencer architecture (@msmttchr, @CasperSchippers, @mcdo0486, #518) - Added "Save Dock Layout" functionality to DockWidget context menu. (@mcdo0486, #762) Version 0.11.0 (2022-11-19) =========================== Main items of this new release: - 11 new instrument drivers have been added - A method for testing instrument communication **without** hardware present has been added, see `the documentation `__. - The separation between :code:`Instrument` and :code:`Adapter` has been improved to make future modifications easier. Adapters now focus on the hardware communication, and the communication *protocol* should be defined in the Instruments. Details in a section below. - The GUI is now compatible with Qt6. - We have started to clean up our API in preparation for a future version 1.0. There will be deprecations and subsequent removals, which will be prominently listed in the changelog. Deprecated features ------------------- In preparation for a stable 1.0 release and a more consistent API, we have now started formally deprecating some features. You should get warnings if those features are used. - Adapter methods :code:`ask`, :code:`values`, :code:`binary_values`, use :code:`Instrument` methods of the same name instead. - Adapter parameter :code:`preprocess_reply`, override :code:`Instrument.read` instead. - :code:`Adapter.query_delay` in favor of :code:`Instrument.wait_for()`. - Keithley 2260B: :code:`enabled` property, use :code:`output_enabled` instead. New adapter and instrument mechanics ------------------------------------ - Nothing should have changed for users, this section is mainly interesting for instrument implementors. - Documentation in 'Advanced communication protocols' in 'Adding instruments'. - Adapter logs written and read messages. - Particular adapters (`VISAAdapter` etc.) implement the actual communication. - :code:`Instrument.control` getter calls :code:`Instrument.values`. - :code:`Instrument.values` calls :code:`Instrument.ask`, which calls :code:`Instrument.write`, :code:`wait_for`, and :code:`read`. - All protocol quirks of an instrument should be implemented overriding :code:`Instrument.write` and :code:`read`. - :code:`Instrument.wait_until_read` implements waiting between writing and reading. - reading/writing binary values is in the :code:`Adapter` class itself. - :code:`PrologixAdapter` is now based on :code:`VISAAdapter`. - :code:`SerialAdapter` improved to be more similar to :code:`VISAAdapter`: :code:`read`/:code:`write` use strings, :code:`read/write_bytes` bytes. - Support for termination characters added. Instruments ----------- - New Active Technologies AWG-401x (@garzetti, #649) - New Eurotest hpp_120_256_ieee (@sansanda, #701) - New HC Photonics crystal ovens TC038, TC038D (@bmoneke, #621, #706) - New HP 6632A/6633A/6634A power supplies (@LongnoseRob, #651) - New HP 8657B RF signal generator (@LongnoseRob, #732) - New Rohde&Schwarz HMP4040 power supply. (@bleykauf, #582) - New Siglent SPDxxxxX series Power Supplies (@AidenDawn, #719) - New Temptronic Thermostream devices (@mroeleke, #368) - New TEXIO PSW-360L30 Power Supply (@LastStarDust, #698) - New Thermostream ECO-560 (@AidenDawn, #679) - New Thermotron 3800 Oven (@jcarbelbide, #606) - Harmonize instruments' adapter argument (@bmoneke, #674) - Harmonize usage of :code:`shutdown` method (@LongnoseRob, #739) - Rework Adapter structure (@bmoneke, #660) - Add Protocol tests without hardware present (@bilderbuchi, #634, @bmoneke, #628, #635) - Add Instruments and adapter protocol tests for adapter rework (@bmoneke, #665) - Add SR830 sync filter and reference source trigger (@AsafYagoda, #630) - Add Keithley6221 phase marker phase and line (@AsafYagoda, #629) - Add missing docstrings to Keithley 2306 battery simulator (@AidenDawn, #720) - Fix hcp instruments documentation (@bmoneke, #671) - Fix HPLegacyInstrument initializer API (@bilderbuchi, #684) - Fix Fwbell 5080 implementation (@mcdo0486, #714) - Fix broken documentation example. (@bmoneke, #738) - Fix typo in Keithley 2600 driver (@mcdo0486, #615) - Remove dynamic use of docstring from ATS545 and make more generic (@AidenDawn, #685) Automation ---------- - Add storing unitful experiment results (@bmoneke, #642) - Add storing conditions in file (@CasperSchippers, #503) GUI --- - Add compatibility with Qt 6 (@CasperSchippers, #688) - Add spinbox functionality for IntegerParameter and FloatParameter (@jarvas24, #656) - Add "delete data file" button to the browser_item_menu (@jarvas24, #654) - Split windows.py into a folder with separate modules (@mcdo0486, #593) - Remove dependency on matplotlib (@msmttchr, #622) - Remove deprecated access to QtWidgets through QtGui (@maederan201, #695) Miscellaneous ------------- - Update and extend documentation (@bilderbuchi, #712, @bmoneke, #655) - Add PEP517 compatibility & dynamically obtaining a version number (@bilderbuchi, #613) - Add an example and documentation regarding using a foreign instrument (@bmoneke, #647) - Add black configuration (@bleykauf, #683) - Remove VISAAdapter.has_supported_version() as it is not needed anymore. New Contributors ---------------- @jcarbelbide, @mroeleke, @bmoneke, @garzetti, @AsafYagoda, @AidenDawn, @LastStarDust, @sansanda **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.10.0...v0.11.0 Version 0.10.0 (2022-04-09) =========================== Main items of this new release: - 23 new instrument drivers have been added - New dynamic Instrument properties can change their parameters at runtime - Communication settings can now be flexibly defined per protocol - Python 3.10 support was added and Python 3.6 support was removed. - Many additions, improvements and have been merged Instruments ----------- - New Agilent B1500 Data Formats and Documentation (@moritzj29) - New Anaheim Automation stepper motor controllers (@samcondon4) - New Andeen Hagerling capacitance bridges (@dkriegner) - New Anritsu MS9740A Optical Spectrum Analyzer (@md12g12) - New BK Precision 9130B Instrument (@dennisfeng2) - New Edwards nXDS (10i) Vacuum Pump (@hududed) - New Fluke 7341 temperature bath instrument (@msmttchr) - New Heidenhain ND287 Position Display Unit Driver (@samcondon4) - New HP 3478A (@LongnoseRob) - New HP 8116A 50 MHz Pulse/Function Generator (@CodingMarco) - New Keithley 2260B DC Power Supply (@bklebel) - New Keithley 2306 Dual Channel Battery/Charger Simulator (@mfikes) - New Keithley 2600 SourceMeter series (@Daivesd) - New Keysight N7776C Swept Laser Source (@maederan201) - New Lakeshore 421 (@CasperSchippers) - New Oxford IPS120-10 (@CasperSchippers) - New Pendulum CNT-91 frequency counter (@bleykauf) - New Rohde&Schwarz - SFM TV test transmitter (@LongnoseRob) - New Rohde&Schwarz FSL spectrum analyzer (@bleykauf) - New SR570 current amplifier driver (@pyMatJ) - New Stanford Research Systems SR510 instrument driver (@samcondon4) - New Toptica Smart Laser diode (@dkriegner) - New Yokogawa GS200 Instrument (@dennisfeng2) - Add output low grounded property to Keithley 6221 (@CasperSchippers) - Add shutdown function for Keithley 2260B (@bklebel) - Add phase control for Agilent 33500 (@corna) - Add assigning "ONCE" to auto_zero to Keithley 2400 (@mfikes) - Add line frequency controls to Keithley 2400 (@mfikes) - Add LIA and ERR status byte read properties to the SRS Sr830 driver (@samcondon4) - Add all commands to Oxford Intelligent Temperature Controller 503 (@CasperSchippers) - Fix DSP 7265 lockin amplifier (@CasperSchippers) - Fix bug in Keithley 6517B Electrometer (@CasperSchippers) - Fix Keithley2000 deprecated call to visa.config (@bklebel) - Fix bug in the Keithley 2700 (@CasperSchippers) - Fix setting of sensor flags for Thorlabs PM100D (@bleykauf) - Fix SCPI used for Keithley 2400 voltage NPLC (@mfikes) - Fix missing return statements in Tektronix AFG3152C (@bleykauf) - Fix DPSeriesMotorController bug (@samcondon4) - Fix Keithley2600 error when retrieving error code (@bicarlsen) - Fix Attocube ANC300 with new SCPI Instrument properties (@dkriegner) - Fix bug in wait_for_trigger of Agilent33220A (neal-kepler) GUI --- - Add time-estimator widget (@CasperSchippers) - Add management of progress bar (@msmttchr) - Remove broken errorbar feature (@CasperSchippers) - Change of pen width for pyqtgraph (@maederan201) - Make linewidth changeable (@CasperSchippers) - Generalise warning in plotter section (@CasperSchippers) - Implement visibility groups in InputsWidgets (@CasperSchippers) - Modify navigation of ManagedWindow directory widget (@jarvas24) - Improve Placeholder logic (@CasperSchippers) - Breakout widgets into separate modules (@mcdo0486) - Fix setSizePolicy bug with PySide2 (@msmttchr) - Fix managed window (@msmttchr) - Fix ListParameter for numbers (@moritzj29) - Fix incorrect columns on showing data (@CasperSchippers) - Fix procedure property issue (@msmttchr) - Fix pyside2 (@msmttchr) Miscellaneous ------------- - Improve SCPI property support (@msmttchr) - Remove broken safeKeyword management (@msmttchr) - Add dynamic property support (@msmttchr) - Add flexible API for defining connection configuration (@bilderbuchi) - Add write_binary_values() to SerialAdapter (@msmttchr) - Change an outdated pyvisa ask() to query() (@LongnoseRob) - Fix ZMQ bug (@bilderbuchi) - Documentation for passing tuples to control property (@bklebel) - Documentation bugfix (@CasperSchippers) - Fixed broken links in documentation. (@samcondon4) - Updated widget documentation (@mcdo0486) - Fix typo SCIP->SCPI (@mfikes) - Remove Python 3.6, add Python 3.10 testing (@bilderbuchi) - Modernise the code base to use Python 3.7 features (@bilderbuchi) - Added image data generation to Mock Instrument class (@samcondon4) - Add autodoc warnings to the problem matcher (@bilderbuchi) - Update CI & annotations (@bilderbuchi) - Test workers (@mcdo0486) - Change copyright date to 2022 (@LongnoseRob) - Removed unused code (@msmttchr) New Contributors ---------------- @LongnoseRob, @neal, @hududed, @corna, @Daivesd, @samcondon4, @maederan201, @bleykauf, @mfikes, @bicarlsen, @md12g12, @CodingMarco, @jarvas24, @mcdo0486! **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.9...v0.10.0 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 :code:`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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/PyMeasure.egg-info/0000755000175100001770000000000014623331176016625 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367998.0 pymeasure-0.14.0/PyMeasure.egg-info/PKG-INFO0000644000175100001770000011657514623331176017741 0ustar00runnerdockerMetadata-Version: 2.1 Name: PyMeasure Version: 0.14.0 Summary: Scientific measurement library for instruments, experiments, and live-plotting Home-page: https://github.com/pymeasure/pymeasure Author: PyMeasure Developers License: MIT Keywords: measure,instrument,experiment control,automate,graph,plot 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.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Scientific/Engineering Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE.txt Requires-Dist: numpy<3,>=1.6.1 Requires-Dist: pandas<3,>=0.14 Requires-Dist: pint Requires-Dist: pyvisa>=1.9 Requires-Dist: pyserial>=2.7 Requires-Dist: pyqtgraph>=0.12 Requires-Dist: importlib-metadata; python_version < "3.8" Provides-Extra: tcp Requires-Dist: pyzmq>=16.0.2; extra == "tcp" Requires-Dist: cloudpickle>=0.3.1; extra == "tcp" Provides-Extra: python-vxi11 Requires-Dist: python-vxi11>=0.9; extra == "python-vxi11" Provides-Extra: tests Requires-Dist: pytest>=3.3.0; extra == "tests" Requires-Dist: pytest-cov>=4.1.0; extra == "tests" Requires-Dist: pytest-qt>=2.4.0; extra == "tests" Requires-Dist: pyvisa-sim>=0.4.0; extra == "tests" .. 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.8-3.12, and is tested with continuous-integration on Linux, macOS, and Windows. .. image:: https://github.com/pymeasure/pymeasure/actions/workflows/pymeasure_CI.yml/badge.svg :target: https://github.com/pymeasure/pymeasure/actions/workflows/pymeasure_CI.yml .. 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.595633.svg :target: https://doi.org/10.5281/zenodo.595633 .. 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 .. image:: https://codecov.io/gh/pymeasure/pymeasure/graph/badge.svg :target: https://codecov.io/gh/pymeasure/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.14.0 (2024-05-22) =========================== Main items of this new release: - Add support for numpy 2.0 - Add support for python 3.12 - Improve academic quotability with an up to date Zenodo DOI and with citation information. - Add default :code:`queue` method and a :code:`FileInputWidget`, allowing to more quickly get started with the PyMeasure user interface (:code:`ManagedWindow`). - Add a :code:`SCPIMixin` base class for instruments instead of defining :code:`includeSCPI=True` - Instrument manufacturer modules are no longer imported in the :code:`pymeasure/instruments/__init__.py` file. Previously, when importing a single instrument into a procedure, all instruments would be imported into memory through the manufacturer modules in :code:`pymeasure/instruments/__init__.py`. Removing manufacturer modules from that file lowers the memory footprint of pymeasure when importing an instrument. Instrument classes will need to be imported from the manufacturer module or explicitly from the instrument driver file. For example, :code:`from pymeasure.instruments import Extreme5000` will need to change to :code:`from pymeasure.instruments.extreme import Extreme5000` or :code:`from pymeasure.instruments.extreme.extreme5000 import Extreme5000`. - 17 new instruments Deprecated features ------------------- - Remove :code:`TelnetAdapter`, as its library is deprecated (@BenediktBurger, #1045) - Replaced :code:`directory_input` keyword-argument of :code:`ManagedWindowBase` by :code:`enable_file_input` (@CasperSchippers, #964) - Make parameter :code:`includeSCPI` obligatory for all instruments, even those which use SCPI (@BenediktBurger, #1007) - Setting `includeSCPI=True` is deprecated, inherit instead the :code:`SCPIMixin` class if the device supports SCPI commands. - Replaced :code:`celcius` attribute of :code:`LakeShoreTemperatureChannel` by :code:`celsius` (@afuetterer, #1003) - Replaced :code:`error` property of Keithley instruments by :code:`next_error`. - Replaced :code:`measurement_time` property of Pendulum CNT-91 by :code:`gate_time`. - Replaced :code:`sample_rate` keyword-argument of :code:`buffer_frequency_time_series` of Pendulum CNT-91 by :code:`gate_time`. - Replaced MKS937B :code:`unit` to use :code:`instruments/mksinst/mks937b/Unit` instead of strings (@dkriegner, @BenediktBurger #1034) Instruments mechanics --------------------- - Add a SCPI base class :code:`SCPIMixin` as replacement for :code:`includeSCPI=True` (@BenediktBurger, #905, #1007, #1019, #1047) - Add :code:`next_error` property to SCPI instruments (@BenediktBurger, #1024) - Make :code:`query_delay=None` the default for :code:`wait_for` (@BenediktBurger, #1077) - Fix :code:`expected_protocol` using empty dictionary as default value (@BenediktBurger, #1087) - Remove auto-importing all instruments in :code:`pymeasure/instruments/__init__.py`` (@mcdo0486, #919) - Add :code:`find_serial_port` to find a serial port by providing USB information (@BenediktBurger, #982) Instruments ----------- - Add Agilent4294A (@driftregion, #998) - Add Agilent 4284A by (@ConnorGCarr #1079) - Add AimTTI PL series power supplies (@guuskuiper, #942) - Add HP11713A Switch & Attenuator Driver (@neuschs, #970) - Add HP437B power meter (@neuschs, #979) - Add Inficon SQM160 SQM-160 multi-film rate/thickness monitor (@dkriegner, #991) - Add Keithley 2182 (@ConnorGCarr, #1043) - Add KeithleyDMM6500 (@fwutw, #963) - Add Kepco BOP 36-12 Bipolar Power Supply (@JAW90, #1086) - Add KeysightE3631A (@OptimisticBeliever, #990) - Add Kuhne Electronic KU SG 2.45 250A microwave generator (@jurajjasik, @BenediktBurger, @1108) - Add MKS 974B vacuum pressure transducer (@dkriegner, #1034) - Add Proterial rod4 (@ConnorGCarr, #1044) - Add Racal-Dana 1992 universal counter (@tomverbeure, #798, #1012) - Add redpitaya board (@seb5g, #1010, #1035) - Add Teledyne HDO6xxx (@RobertoRoos, #868) - Add Yokogawa AQ6370D Optical Spectral Analyzer (@jnnskls, #1059) - Fix property docstrings of several instruments (@BenediktBurger, #1018) - Fix checksums of hcp TC038D tests (@BenediktBurger, #987) - Fix Hp8116a (@BenediktBurger, #1088) - Fix Hp856x to append amplitude units (@neuschs, #977) - Fix Keysight E36312A confirmed SCPI functionality (@Konradrundfunk, #1107) - Fix Stanford Research SR830 output conversion (@dkriegner, #1069) - Fix SR830 missing get_buffer method (@seb5g, #999) - Fix set command of SR860 aux output (@wehlgrundspitze, #1048) - Fix Temptronic test to use ns perf counter (@BenediktBurger, #1109, #1110) - Fix Toptica Ibeamsmart referencing removed adapter function (@BenediktBurger, #1065) - Fix typos in docstrings for Keithley instruments (@V0XNIHILI, #1071) - Link Keysight, Agilent, and HP documentation pages. (@BenediktBurger, #1021) - Update Agilent33500 Series from :code:`.ch[]` to :code:`.channels[]` (@AlecVercruysse, #945) - Update AWG401x driver to use 'channels' (@mcdo0486, #944) - Update HP33120A with new burst modulation parameters (@mzen228, #1056) - Update HP34401A with new remote control command. (@Rybok, #992) - Update Keithleys' next_error (@msmttchr, #1030) - Update pendulum CNT-91 (@bleykauf, #988) GUI --- - Add a :code:`FileInputWidget` to choose if and where the experiment data is stored. (@CasperSchippers, #964) - Add a default :code:`Queue` method for :code:`ManagedWindowBase` is implemented. (@CasperSchippers, #964) - Fix :code:`ScientificInput` to be locale compatible (@pyZerrenner, #1074) - Fix exception if loading result file with an empty parameter (@poje42, #1016) Miscellaneous ------------- - Add support for python 3.12 (@BenediktBurger, #1051) - Add support for numpy 2.0 (@CasperSchippers, #1026) - Add codecov to CI and to readme (@BenediktBurger, #1037, #1052, #1099) - Add citation file for PyMeasure repository (@mcdo0486, #1092) - Add release CI (@BenediktBurger, #1039) - Update readme with permanent Zenodo DOI (@BenediktBurger, #1095) - Bump CI dependencies to: pyvisa 1.13.0, checkout@v4 (@mcdo0486, #1097) - Fix/pandas futurewarning (@CasperSchippers, #1062) - Change copyright year. (@BenediktBurger, #1032) - Fix typos (@afuetterer, #1003) New Contributors ---------------- @guuskuiper, @OptimisticBeliever, @fwutw, @afuetterer, @poje42, @Rybok, @AlecVercruysse, @ConnorGCarr, @mzen228, @jnnskls, @V0XNIHILI, @pyZerrenner, @JAW90, @driftregion, @jurajjasik, @Konradrundfunk **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.13.1...v0.14.0 Version 0.13.1 (2023-10-05) =========================== New release to fix ineffective python version restriction in the project metadata (only affected Python<=3.7 environments installing via pip). Version 0.13.0 (2023-09-23) =========================== Main items of this new release: - Dropped support for Python 3.7, added support for Python 3.11. - Adds a test generator, which observes the communication with an actual device and writes protocol tests accordingly. - 2 new instrument drivers have been added. Deprecated features ------------------- - Attocube ANC300: The :code:`stepu` and :code:`stepd` properties are deprecated, use the new :code:`move_raw` method instead. (@dkriegner, #938) Instruments ----------- - Adds a test generator (@bmoneke, #882) - Adds Thyracont Smartline v2 vacuum sensor transmitter (@bmoneke, #940) - Adds Thyracont Smartline v1 vacuum gauge (@dkriegner, #937) - AddsTeledyne base classes with most of `LeCroyT3DSO1204` functionality (@RobertoRoos, #951) - Fixes instrument documentation (@mcdo0486, #941, #903, @omahs, #960) - Fixes Toptica Ibeamsmart's __init__ (@waveman68, #959) - Fixes VISAAdapter flush_read_buffer() (@ileu, #968) - Updates Keithley2306 and AFG3152C to Channels (@bilderbuchi, #953) GUI --- - Adds console mode (@msmttchr, #500) - Fixes Dock widget (@msmttchr, #961) Miscellaneous ------------- - Change CI from conda to mamba (@bmoneke, #947) - Add support for python 3.11 (@CasperSchippers, #896) New Contributors ---------------- @waveman68, @omahs, @ileu **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.12.0...v0.13.0 Version 0.12.0 (2023-07-05) =========================== Main items of this new release: - A :code:`Channel` base class has been added for easier implementation of instruments with channels. - 19 new instrument drivers have been added. - Added tests for some commonalities across all instruments. - We continue to clean up our API in preparation for a future version 1.0. Deprecations and subsequent removals are listed below. Deprecated features ------------------- - HP 34401A: :code:`voltage_ac`, :code:`current_dc`, :code:`current_ac`, :code:`resistance`, :code:`resistance_4w` properties, use :code:`function_` and :code:`reading` properties instead. - Toptica IBeamSmart: :code:`channel1_enabled`, use :code:`ch_1.enabled` property instead (equivalent for channel2). Also :code:`laser_enabled` is deprecated in favor of :code:`emission` (@bmoneke, #819). - TelnetAdapter: use :code:`VISAAdapter` instead. VISA supports TCPIP connections. Use the resource_name :code:`TCPIP[board]::::::SOCKET` to connect to a server (@Max-Herbold, #835). - Attocube ANC300: :code:`host` argument, pass a resource string or adapter as :code:`Adapter` passed to :code:`Instrument`. Now communicates through the :code:`VISAAdapter` rather than deprecated :code:`TelnetAdapter`. The initializer now accepts :code:`name` as its second keyword argument so all previous initialization positional arguments (`axisnames`, `passwd`, `query_delay`) should be switched to keyword arguments. - The property creators :code:`control`, :code:`measurement`, and :code:`setting` do not accept arbitrary keyword arguments anymore. Use the :code:`v_kwargs` parameter for arguments you want to pass on to :code:`values` method, instead. - The property creators :code:`control`, :code:`measurement`, and :code:`setting` do not accept `command_process` anymore. Use a dynamic property or a `Channel` instead, as appropriate (@bmoneke, #878). - See also the next section. New adapter and instrument mechanics ------------------------------------ - All instrument constructors are required to accept a :code:`name` argument. - Changed: :code:`read_bytes` of all Adapters by default does not stop reading on a termination character, unless the new argument :code:`break_on_termchar` is set to `True`. - Channel class added. :code:`Instrument.channels` and :code:`Instrument.ch_X` (:code:`X` is any channel name) are reserved attributes for channel mechanics. - The parameters :code:`check_get_errors` and :code:`check_set_errors` enable calling methods of the same name. This enables more systematically dealing with instruments that acknowledge every "set" command. - Adds Channel feature to instruments (@bmoneke, mcdo0486, #718, #761, #852, #931) - Adds :code:`maxsplit` parameter to :code:`values` method (@bmoneke, #793) - Adds (deprecated) global preprocess reply for backward compatibility (@bmoneke, #876) - Adds fallback version for discarding the read buffer to VISAAdapter (@dkriegner, #836) - Adds :code:`flush_read_buffer` to SerialAdapter (@RobertoRoos, #865) - Adds :code:`gpib_read_timeout` to PrologixAdapter (@neuschs, #927) - Adds command line option to pass resource address for instrument tests (@bleykauf, #789) - Adds "find all instruments" and channels for testing (@bmoneke, #909, @mcdo0486, #911, #912) - Adds test that an instrument hands kwargs to the adapter (@bmoneke, #814) - Adds property docstring check (@bmoneke, #895) - Improves property factories' docstrings (@bmoneke, #843) - Improves property factories: do not allow undefined kwargs (@bmoneke, #856) - Improves property factories: check_set/get_errors argument to call methods of the same name (@bmoneke, #883) - Improves :code:`read_bytes` of Adapter (@bmoneke, #839) - Improves the ProtocolAdapter with a mock connection (@bmoneke, #782), and enable it to have empty messages in the protocol (@bmoneke, #818) - Improves Prologix adapter documentation (@bmoneke, #813) and configurable settings (@bmoneke, #845) - Improves behavior of :code:`read_bytes(-1)` for :code:`SerialAdapter` (@RobertoRoos, #866) - Improves all instruments with name kwarg (@bmoneke, #877) - Improves VisaAdapter: close manager only when using pyvisa-sim (@dkriegner, #900) - Harmonises instrument name definition pattern, consistently name the instrument connection argument "adapter" (@bmoneke, #659) - Fixes ProtocolAdapter has list in signature (@bmoneke, #901) - Fixes VISAAdapter's :code:`read_bytes` (@bmoneke, #867) - Fixes query_delay usage in VISAAdapter (@bmoneke, #765) - Fixes VisaAdapter: close resource manager only when using pyvisa-sim (@dkriegner, #900) Instruments ----------- - New Advantest R624X DC Voltage/Current Sources/Monitors (@wichers, #802) - New AJA International DC sputtering power supply (@dkriegner, #778) - New Anritus MS2090A (@aruznieto, #787) - New Anritsu MS4644B (@CasperSchippers, #827) - New DSP 7225 and new DSPBase instrument (@mcdo0486, #902) - New HP 8560A / 8561B Spectrum Analyzer (@neuschs, #888) - New IPG Photonics YAR Amplifier series (@bmoneke, #851) - New Keysight E36312A power supply (@scandey, #785) - New Keithley 2200 power supply (@ashokbruno, #806) - New Lake Shore 211 Temperature Monitor (@mcdo0486, #889) - New Lake Shore 224 and improves Lakeshore instruments (@samcondon4, #870) - New MKS Instruments 937B vacuum gauge controller (@dkriegner, @bilderbuchi, #637, #772, #936) - New Novanta FPU60 laser power supply unit (@bmoneke, #885) - New TDK Lambda Genesys 80-65 DC and 40-38 DC power supplies (@mcdo0486, 906) - New Teledyne T3AFG waveform generator instrument (@scandey, #791) - New Teledyne (LeCroy) T3DSO1204 Oscilloscope (@LastStartDust, #697, @bilderbuchi, #770) - New T&C Power Conversion RF power supply (@dkriegner, #800) - New Velleman K8090 relay device (@RobertoRoos, #859) - Improves Agilent 33500 with the new channel feature (@JCarl-OS, #763, #773) - Improves HP 3478A with calibration data related functions (@tomverbeure, #777) - Improves HP 34401A (@CodingMarco, #810) - Improves the Oxford instruments with the new channel feature (@bmoneke, #844) - Improves Siglent SPDxxxxX with the new channel feature (@AidenDawn 758) - Improves Teledyne T3DSO1204 device tests (@LastStarDust, #841) - Fixes Ametek DSP 7270 lockin amplifier issues (@seb5g, #897) - Fixes DSP 7265 erroneously using preprocess_reply (@mcdo0486, #873) - Fixes print statement in DSPBase.sensitivity (@mcdo0486, #915) - Fixes Fluke bath commands (@bmoneke, #874) - Fixes a frequency limitation in HP 8657B (@LongnoseRob, #769) - Fixes Keithley 2600 channel calling parent's shutdown (@mcdo0486, #795) Automation ---------- - Adds tolerance for opening result files with missing parameters (@msmttchr, #780) - Validate DATA_COLUMNS entries earlier, avoid exceptions in a running procedure (@mcdo0486, #796, #934) GUI --- - Adds docking windows (@mcdo0486, #722, #762) - Adds save plot settings in addition to dock layout (@mcdo0486, #850) - Adds log widget colouring and format option (@CasperSchippers, #890) - Adds table widget (@msmttchr, #771) - New sequencer architecture: decouples it from the graphical tree, adapts it for further expansions (@msmttchr, #518) - Moves coordinates label to the pyqtgraph PlotItem (@CasperSchippers, #822) - Fixes crashing ImageWidget at new measurement (@CasperSchippers, #790) - Fixes checkboxes not working for groups in inputs-widget (@CasperSchippers, #794) Miscellaneous ------------- - Adds a collection of solutions for instrument implementation challenges (@bmoneke, #853, #861) - Updates Tutorials/Making_a_measurement/ example_codes (@sansanda, #749) New Contributors ---------------- @JCarl-OS, @aruznieto, @scandey, @tomverbeure, @wichers, @Max-Herbold, @RobertoRoos **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.11.1...v0.12.0 Version 0.11.1 (2022-12-31) =========================== Adapter and instrument mechanics -------------------------------- - Fix broken `PrologixAdapter.gpib`. Due to a bug in `VISAAdapter`, you could not get a second adapter with that connection (#765). **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.11.0...v0.11.1 Dependency updates ------------------ - Required version of `PyQtGraph `__ is increased from :code:`pyqtgraph >= 0.9.10` to :code:`pyqtgraph >= 0.12` to support new PyMeasure display widgets. GUI --- - Added `ManagedDockWindow `__ to allow multiple dockable plots (@mcdo0486, @CasperSchippers, #722) - Move coordinates label to the pyqtgraph PlotItem (@CasperSchippers, #822) - New sequencer architecture (@msmttchr, @CasperSchippers, @mcdo0486, #518) - Added "Save Dock Layout" functionality to DockWidget context menu. (@mcdo0486, #762) Version 0.11.0 (2022-11-19) =========================== Main items of this new release: - 11 new instrument drivers have been added - A method for testing instrument communication **without** hardware present has been added, see `the documentation `__. - The separation between :code:`Instrument` and :code:`Adapter` has been improved to make future modifications easier. Adapters now focus on the hardware communication, and the communication *protocol* should be defined in the Instruments. Details in a section below. - The GUI is now compatible with Qt6. - We have started to clean up our API in preparation for a future version 1.0. There will be deprecations and subsequent removals, which will be prominently listed in the changelog. Deprecated features ------------------- In preparation for a stable 1.0 release and a more consistent API, we have now started formally deprecating some features. You should get warnings if those features are used. - Adapter methods :code:`ask`, :code:`values`, :code:`binary_values`, use :code:`Instrument` methods of the same name instead. - Adapter parameter :code:`preprocess_reply`, override :code:`Instrument.read` instead. - :code:`Adapter.query_delay` in favor of :code:`Instrument.wait_for()`. - Keithley 2260B: :code:`enabled` property, use :code:`output_enabled` instead. New adapter and instrument mechanics ------------------------------------ - Nothing should have changed for users, this section is mainly interesting for instrument implementors. - Documentation in 'Advanced communication protocols' in 'Adding instruments'. - Adapter logs written and read messages. - Particular adapters (`VISAAdapter` etc.) implement the actual communication. - :code:`Instrument.control` getter calls :code:`Instrument.values`. - :code:`Instrument.values` calls :code:`Instrument.ask`, which calls :code:`Instrument.write`, :code:`wait_for`, and :code:`read`. - All protocol quirks of an instrument should be implemented overriding :code:`Instrument.write` and :code:`read`. - :code:`Instrument.wait_until_read` implements waiting between writing and reading. - reading/writing binary values is in the :code:`Adapter` class itself. - :code:`PrologixAdapter` is now based on :code:`VISAAdapter`. - :code:`SerialAdapter` improved to be more similar to :code:`VISAAdapter`: :code:`read`/:code:`write` use strings, :code:`read/write_bytes` bytes. - Support for termination characters added. Instruments ----------- - New Active Technologies AWG-401x (@garzetti, #649) - New Eurotest hpp_120_256_ieee (@sansanda, #701) - New HC Photonics crystal ovens TC038, TC038D (@bmoneke, #621, #706) - New HP 6632A/6633A/6634A power supplies (@LongnoseRob, #651) - New HP 8657B RF signal generator (@LongnoseRob, #732) - New Rohde&Schwarz HMP4040 power supply. (@bleykauf, #582) - New Siglent SPDxxxxX series Power Supplies (@AidenDawn, #719) - New Temptronic Thermostream devices (@mroeleke, #368) - New TEXIO PSW-360L30 Power Supply (@LastStarDust, #698) - New Thermostream ECO-560 (@AidenDawn, #679) - New Thermotron 3800 Oven (@jcarbelbide, #606) - Harmonize instruments' adapter argument (@bmoneke, #674) - Harmonize usage of :code:`shutdown` method (@LongnoseRob, #739) - Rework Adapter structure (@bmoneke, #660) - Add Protocol tests without hardware present (@bilderbuchi, #634, @bmoneke, #628, #635) - Add Instruments and adapter protocol tests for adapter rework (@bmoneke, #665) - Add SR830 sync filter and reference source trigger (@AsafYagoda, #630) - Add Keithley6221 phase marker phase and line (@AsafYagoda, #629) - Add missing docstrings to Keithley 2306 battery simulator (@AidenDawn, #720) - Fix hcp instruments documentation (@bmoneke, #671) - Fix HPLegacyInstrument initializer API (@bilderbuchi, #684) - Fix Fwbell 5080 implementation (@mcdo0486, #714) - Fix broken documentation example. (@bmoneke, #738) - Fix typo in Keithley 2600 driver (@mcdo0486, #615) - Remove dynamic use of docstring from ATS545 and make more generic (@AidenDawn, #685) Automation ---------- - Add storing unitful experiment results (@bmoneke, #642) - Add storing conditions in file (@CasperSchippers, #503) GUI --- - Add compatibility with Qt 6 (@CasperSchippers, #688) - Add spinbox functionality for IntegerParameter and FloatParameter (@jarvas24, #656) - Add "delete data file" button to the browser_item_menu (@jarvas24, #654) - Split windows.py into a folder with separate modules (@mcdo0486, #593) - Remove dependency on matplotlib (@msmttchr, #622) - Remove deprecated access to QtWidgets through QtGui (@maederan201, #695) Miscellaneous ------------- - Update and extend documentation (@bilderbuchi, #712, @bmoneke, #655) - Add PEP517 compatibility & dynamically obtaining a version number (@bilderbuchi, #613) - Add an example and documentation regarding using a foreign instrument (@bmoneke, #647) - Add black configuration (@bleykauf, #683) - Remove VISAAdapter.has_supported_version() as it is not needed anymore. New Contributors ---------------- @jcarbelbide, @mroeleke, @bmoneke, @garzetti, @AsafYagoda, @AidenDawn, @LastStarDust, @sansanda **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.10.0...v0.11.0 Version 0.10.0 (2022-04-09) =========================== Main items of this new release: - 23 new instrument drivers have been added - New dynamic Instrument properties can change their parameters at runtime - Communication settings can now be flexibly defined per protocol - Python 3.10 support was added and Python 3.6 support was removed. - Many additions, improvements and have been merged Instruments ----------- - New Agilent B1500 Data Formats and Documentation (@moritzj29) - New Anaheim Automation stepper motor controllers (@samcondon4) - New Andeen Hagerling capacitance bridges (@dkriegner) - New Anritsu MS9740A Optical Spectrum Analyzer (@md12g12) - New BK Precision 9130B Instrument (@dennisfeng2) - New Edwards nXDS (10i) Vacuum Pump (@hududed) - New Fluke 7341 temperature bath instrument (@msmttchr) - New Heidenhain ND287 Position Display Unit Driver (@samcondon4) - New HP 3478A (@LongnoseRob) - New HP 8116A 50 MHz Pulse/Function Generator (@CodingMarco) - New Keithley 2260B DC Power Supply (@bklebel) - New Keithley 2306 Dual Channel Battery/Charger Simulator (@mfikes) - New Keithley 2600 SourceMeter series (@Daivesd) - New Keysight N7776C Swept Laser Source (@maederan201) - New Lakeshore 421 (@CasperSchippers) - New Oxford IPS120-10 (@CasperSchippers) - New Pendulum CNT-91 frequency counter (@bleykauf) - New Rohde&Schwarz - SFM TV test transmitter (@LongnoseRob) - New Rohde&Schwarz FSL spectrum analyzer (@bleykauf) - New SR570 current amplifier driver (@pyMatJ) - New Stanford Research Systems SR510 instrument driver (@samcondon4) - New Toptica Smart Laser diode (@dkriegner) - New Yokogawa GS200 Instrument (@dennisfeng2) - Add output low grounded property to Keithley 6221 (@CasperSchippers) - Add shutdown function for Keithley 2260B (@bklebel) - Add phase control for Agilent 33500 (@corna) - Add assigning "ONCE" to auto_zero to Keithley 2400 (@mfikes) - Add line frequency controls to Keithley 2400 (@mfikes) - Add LIA and ERR status byte read properties to the SRS Sr830 driver (@samcondon4) - Add all commands to Oxford Intelligent Temperature Controller 503 (@CasperSchippers) - Fix DSP 7265 lockin amplifier (@CasperSchippers) - Fix bug in Keithley 6517B Electrometer (@CasperSchippers) - Fix Keithley2000 deprecated call to visa.config (@bklebel) - Fix bug in the Keithley 2700 (@CasperSchippers) - Fix setting of sensor flags for Thorlabs PM100D (@bleykauf) - Fix SCPI used for Keithley 2400 voltage NPLC (@mfikes) - Fix missing return statements in Tektronix AFG3152C (@bleykauf) - Fix DPSeriesMotorController bug (@samcondon4) - Fix Keithley2600 error when retrieving error code (@bicarlsen) - Fix Attocube ANC300 with new SCPI Instrument properties (@dkriegner) - Fix bug in wait_for_trigger of Agilent33220A (neal-kepler) GUI --- - Add time-estimator widget (@CasperSchippers) - Add management of progress bar (@msmttchr) - Remove broken errorbar feature (@CasperSchippers) - Change of pen width for pyqtgraph (@maederan201) - Make linewidth changeable (@CasperSchippers) - Generalise warning in plotter section (@CasperSchippers) - Implement visibility groups in InputsWidgets (@CasperSchippers) - Modify navigation of ManagedWindow directory widget (@jarvas24) - Improve Placeholder logic (@CasperSchippers) - Breakout widgets into separate modules (@mcdo0486) - Fix setSizePolicy bug with PySide2 (@msmttchr) - Fix managed window (@msmttchr) - Fix ListParameter for numbers (@moritzj29) - Fix incorrect columns on showing data (@CasperSchippers) - Fix procedure property issue (@msmttchr) - Fix pyside2 (@msmttchr) Miscellaneous ------------- - Improve SCPI property support (@msmttchr) - Remove broken safeKeyword management (@msmttchr) - Add dynamic property support (@msmttchr) - Add flexible API for defining connection configuration (@bilderbuchi) - Add write_binary_values() to SerialAdapter (@msmttchr) - Change an outdated pyvisa ask() to query() (@LongnoseRob) - Fix ZMQ bug (@bilderbuchi) - Documentation for passing tuples to control property (@bklebel) - Documentation bugfix (@CasperSchippers) - Fixed broken links in documentation. (@samcondon4) - Updated widget documentation (@mcdo0486) - Fix typo SCIP->SCPI (@mfikes) - Remove Python 3.6, add Python 3.10 testing (@bilderbuchi) - Modernise the code base to use Python 3.7 features (@bilderbuchi) - Added image data generation to Mock Instrument class (@samcondon4) - Add autodoc warnings to the problem matcher (@bilderbuchi) - Update CI & annotations (@bilderbuchi) - Test workers (@mcdo0486) - Change copyright date to 2022 (@LongnoseRob) - Removed unused code (@msmttchr) New Contributors ---------------- @LongnoseRob, @neal, @hududed, @corna, @Daivesd, @samcondon4, @maederan201, @bleykauf, @mfikes, @bicarlsen, @md12g12, @CodingMarco, @jarvas24, @mcdo0486! **Full Changelog**: https://github.com/pymeasure/pymeasure/compare/v0.9...v0.10.0 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 :code:`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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367998.0 pymeasure-0.14.0/PyMeasure.egg-info/SOURCES.txt0000644000175100001770000006751014623331176020522 0ustar00runnerdockerAUTHORS.txt CHANGES.rst CITATION.cff LICENSE.txt MANIFEST.in README.rst RELEASE.md codecov.yml pyproject.toml setup.cfg 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/changes.rst docs/about/license.rst docs/api/adapters.rst docs/api/display/Qt.rst docs/api/display/browser.rst docs/api/display/console.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/generic_types.rst docs/api/instruments/index.rst docs/api/instruments/instruments.rst docs/api/instruments/resources.rst docs/api/instruments/validators.rst docs/api/instruments/activetechnologies/AWG401x.rst docs/api/instruments/activetechnologies/index.rst docs/api/instruments/advantest/advantestR3767CG.rst docs/api/instruments/advantest/advantestR624X.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/agilent4284A.rst docs/api/instruments/agilent/agilent4294A.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/aimtti/aimttiPL.rst docs/api/instruments/aimtti/index.rst docs/api/instruments/aja/DCXS.rst docs/api/instruments/aja/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/anaheimautomation/dpseriesstepmotorcontroller.rst docs/api/instruments/anaheimautomation/index.rst docs/api/instruments/anapico/apsin12G.rst docs/api/instruments/anapico/index.rst docs/api/instruments/andeenhagerling/ah2500a.rst docs/api/instruments/andeenhagerling/ah2700a.rst docs/api/instruments/andeenhagerling/index.rst docs/api/instruments/anritsu/anritsuMG3692C.rst docs/api/instruments/anritsu/anritsuMS2090A.rst docs/api/instruments/anritsu/anritsuMS464xB.rst docs/api/instruments/anritsu/anritsuMS9710C.rst docs/api/instruments/anritsu/anritsuMS9740A.rst docs/api/instruments/anritsu/index.rst docs/api/instruments/attocube/anc300.rst docs/api/instruments/attocube/index.rst docs/api/instruments/bkprecision/bkprecision9130b.rst docs/api/instruments/bkprecision/index.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/edwards/index.rst docs/api/instruments/edwards/nxds.rst docs/api/instruments/eurotest/eurotestHPP120256.rst docs/api/instruments/eurotest/index.rst docs/api/instruments/fluke/fluke7341.rst docs/api/instruments/fluke/index.rst docs/api/instruments/fwbell/fwbell5080.rst docs/api/instruments/fwbell/index.rst docs/api/instruments/hcp/index.rst docs/api/instruments/hcp/tc038.rst docs/api/instruments/hcp/tc038d.rst docs/api/instruments/heidenhain/index.rst docs/api/instruments/heidenhain/nd287.rst docs/api/instruments/hp/hp11713A.rst docs/api/instruments/hp/hp33120A.rst docs/api/instruments/hp/hp3437A.rst docs/api/instruments/hp/hp34401A.rst docs/api/instruments/hp/hp3478A.rst docs/api/instruments/hp/hp437B.rst docs/api/instruments/hp/hp8116A.rst docs/api/instruments/hp/hp856xx.rst docs/api/instruments/hp/hp8657B.rst docs/api/instruments/hp/hplegacyinstrument.rst docs/api/instruments/hp/hpsystempsu.rst docs/api/instruments/hp/index.rst docs/api/instruments/inficon/index.rst docs/api/instruments/inficon/sqm160.rst docs/api/instruments/ipgphotonics/index.rst docs/api/instruments/ipgphotonics/yar.rst docs/api/instruments/keithley/index.rst docs/api/instruments/keithley/keithley2000.rst docs/api/instruments/keithley/keithley2182.rst docs/api/instruments/keithley/keithley2200.rst docs/api/instruments/keithley/keithley2260B.rst docs/api/instruments/keithley/keithley2306.rst docs/api/instruments/keithley/keithley2400.rst docs/api/instruments/keithley/keithley2450.rst docs/api/instruments/keithley/keithley2600.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/keithley/keithleyDMM6500.rst docs/api/instruments/kepco/bop.rst docs/api/instruments/kepco/index.rst docs/api/instruments/keysight/index.rst docs/api/instruments/keysight/keysightDSOX1102G.rst docs/api/instruments/keysight/keysightE36312A.rst docs/api/instruments/keysight/keysightE3631A.rst docs/api/instruments/keysight/keysightN5767A.rst docs/api/instruments/keysight/keysightN7776C.rst docs/api/instruments/kuhneelectronic/index.rst docs/api/instruments/kuhneelectronic/kusg245_250a.rst docs/api/instruments/lakeshore/index.rst docs/api/instruments/lakeshore/lakeshore211.rst docs/api/instruments/lakeshore/lakeshore224.rst docs/api/instruments/lakeshore/lakeshore331.rst docs/api/instruments/lakeshore/lakeshore421.rst docs/api/instruments/lakeshore/lakeshore425.rst docs/api/instruments/lecroy/index.rst docs/api/instruments/lecroy/lecroyT3DSO1204.rst docs/api/instruments/mksinst/index.rst docs/api/instruments/mksinst/mks937b.rst docs/api/instruments/mksinst/mks974b.rst docs/api/instruments/mksinst/mksinst.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/novanta/fpu60.rst docs/api/instruments/novanta/index.rst docs/api/instruments/oxfordinstruments/IPS120_10.rst docs/api/instruments/oxfordinstruments/ITC503.rst docs/api/instruments/oxfordinstruments/PS120_10.rst docs/api/instruments/oxfordinstruments/base.rst docs/api/instruments/oxfordinstruments/index.rst docs/api/instruments/parker/index.rst docs/api/instruments/parker/parkerGV6.rst docs/api/instruments/pendulum/cnt91.rst docs/api/instruments/pendulum/index.rst docs/api/instruments/proterial/index.rst docs/api/instruments/proterial/rod4.rst docs/api/instruments/racal/index.rst docs/api/instruments/racal/racal1992.rst docs/api/instruments/razorbill/index.rst docs/api/instruments/razorbill/razorbillRP100.rst docs/api/instruments/redpitaya/index.rst docs/api/instruments/redpitaya/redpitaya_scpi.rst docs/api/instruments/rohdeschwarz/fsl.rst docs/api/instruments/rohdeschwarz/hmp.rst docs/api/instruments/rohdeschwarz/index.rst docs/api/instruments/rohdeschwarz/sfm.rst docs/api/instruments/siglenttechnologies/index.rst docs/api/instruments/siglenttechnologies/siglent_spd1168x.rst docs/api/instruments/siglenttechnologies/siglent_spd1305x.rst docs/api/instruments/siglenttechnologies/siglent_spdbase.rst docs/api/instruments/signalrecovery/dsp7225.rst docs/api/instruments/signalrecovery/dsp7265.rst docs/api/instruments/signalrecovery/index.rst docs/api/instruments/srs/index.rst docs/api/instruments/srs/sr510.rst docs/api/instruments/srs/sr570.rst docs/api/instruments/srs/sr830.rst docs/api/instruments/srs/sr860.rst docs/api/instruments/tcpowerconversion/index.rst docs/api/instruments/tcpowerconversion/tccxn.rst docs/api/instruments/tdk/index.rst docs/api/instruments/tdk/tdk_gen40_38.rst docs/api/instruments/tdk/tdk_gen80_65.rst docs/api/instruments/tektronix/afg3152c.rst docs/api/instruments/tektronix/index.rst docs/api/instruments/tektronix/tds2000.rst docs/api/instruments/teledyne/index.rst docs/api/instruments/teledyne/teledyneT3AFG.rst docs/api/instruments/teledyne/teledyne_bases.rst docs/api/instruments/temptronic/index.rst docs/api/instruments/temptronic/temptronic_ats525.rst docs/api/instruments/temptronic/temptronic_ats545.rst docs/api/instruments/temptronic/temptronic_base.rst docs/api/instruments/temptronic/temptronic_eco560.rst docs/api/instruments/texio/index.rst docs/api/instruments/texio/texioPSW360L30.rst docs/api/instruments/thermotron/index.rst docs/api/instruments/thermotron/thermotron3800.rst docs/api/instruments/thorlabs/index.rst docs/api/instruments/thorlabs/thorlabspm100usb.rst docs/api/instruments/thorlabs/thorlabspro8000.rst docs/api/instruments/thyracont/index.rst docs/api/instruments/thyracont/smartline_v1.rst docs/api/instruments/thyracont/smartline_v2.rst docs/api/instruments/toptica/ibeamsmart.rst docs/api/instruments/toptica/index.rst docs/api/instruments/velleman/index.rst docs/api/instruments/velleman/k8090.rst docs/api/instruments/yokogawa/aq6370series.rst docs/api/instruments/yokogawa/index.rst docs/api/instruments/yokogawa/yokogawa7651.rst docs/api/instruments/yokogawa/yokogawags200.rst docs/dev/coding_standards.rst docs/dev/contribute.rst docs/dev/reporting_errors.rst docs/dev/adding_instruments/advanced_communication.rst docs/dev/adding_instruments/channels.rst docs/dev/adding_instruments/index.rst docs/dev/adding_instruments/instrument.rst docs/dev/adding_instruments/properties.rst docs/dev/adding_instruments/solutions.rst docs/dev/adding_instruments/tests.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/console_output.png docs/tutorial/graphical.rst docs/tutorial/gui_sequencer_example_sequence.txt docs/tutorial/index.rst docs/tutorial/managed_dock_window.png docs/tutorial/managed_dock_window_popup.gif docs/tutorial/managed_dock_window_save.png docs/tutorial/managed_dock_window_side_after.png docs/tutorial/managed_dock_window_side_drag.png docs/tutorial/managed_dock_window_tab_after.png docs/tutorial/managed_dock_window_tab_drag.png docs/tutorial/managed_dock_window_top.png docs/tutorial/procedure.rst docs/tutorial/pymeasure-estimator.png docs/tutorial/pymeasure-fileinput.png docs/tutorial/pymeasure-fileinput_complete_directory.png docs/tutorial/pymeasure-fileinput_complete_filename.png docs/tutorial/pymeasure-fileinput_disabled.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 docs/tutorial/pymeasure-tablewidget.png examples/.gitignore examples/README.md examples/Basic/console.py examples/Basic/gui.py examples/Basic/gui_custom_inputs.py examples/Basic/gui_custom_inputs.ui examples/Basic/gui_estimator.py examples/Basic/gui_foreign_instrument.py examples/Basic/gui_sequencer.py examples/Basic/gui_sequencer_example_sequence.txt examples/Basic/gui_table.py examples/Basic/image_gui.py examples/Basic/script.py examples/Basic/script_plotter.py examples/Current-Voltage Measurements/iv_keithley.py examples/Current-Voltage Measurements/iv_yokogawa.py examples/Notebook Experiments/default_config.ini examples/Notebook Experiments/script.ipynb examples/Notebook Experiments/script2.ipynb pymeasure/__init__.py pymeasure/console.py pymeasure/errors.py pymeasure/generator.py pymeasure/log.py pymeasure/process.py pymeasure/test.py pymeasure/thread.py pymeasure/units.py pymeasure/adapters/__init__.py pymeasure/adapters/adapter.py pymeasure/adapters/prologix.py pymeasure/adapters/protocol.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/console.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/__init__.py pymeasure/display/widgets/browser_widget.py pymeasure/display/widgets/directory_widget.py pymeasure/display/widgets/dock_widget.py pymeasure/display/widgets/estimator_widget.py pymeasure/display/widgets/fileinput_widget.py pymeasure/display/widgets/filename_widget.py pymeasure/display/widgets/image_frame.py pymeasure/display/widgets/image_widget.py pymeasure/display/widgets/inputs_widget.py pymeasure/display/widgets/log_widget.py pymeasure/display/widgets/plot_frame.py pymeasure/display/widgets/plot_widget.py pymeasure/display/widgets/results_dialog.py pymeasure/display/widgets/sequencer_widget.py pymeasure/display/widgets/tab_widget.py pymeasure/display/widgets/table_widget.py pymeasure/display/windows/__init__.py pymeasure/display/windows/managed_dock_window.py pymeasure/display/windows/managed_image_window.py pymeasure/display/windows/managed_window.py pymeasure/display/windows/plotter_window.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/sequencer.py pymeasure/experiment/workers.py pymeasure/instruments/__init__.py pymeasure/instruments/channel.py pymeasure/instruments/comedi.py pymeasure/instruments/common_base.py pymeasure/instruments/fakes.py pymeasure/instruments/generic_types.py pymeasure/instruments/instrument.py pymeasure/instruments/resources.py pymeasure/instruments/validators.py pymeasure/instruments/activetechnologies/AWG401x.py pymeasure/instruments/activetechnologies/__init__.py pymeasure/instruments/advantest/__init__.py pymeasure/instruments/advantest/advantestR3767CG.py pymeasure/instruments/advantest/advantestR624X.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/agilent4284A.py pymeasure/instruments/agilent/agilent4294A.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/aimtti/__init__.py pymeasure/instruments/aimtti/aimttiPL.py pymeasure/instruments/aja/__init__.py pymeasure/instruments/aja/dcxs.py pymeasure/instruments/ametek/__init__.py pymeasure/instruments/ametek/ametek7270.py pymeasure/instruments/ami/__init__.py pymeasure/instruments/ami/ami430.py pymeasure/instruments/anaheimautomation/__init__.py pymeasure/instruments/anaheimautomation/dpseriesmotorcontroller.py pymeasure/instruments/anapico/__init__.py pymeasure/instruments/anapico/apsin12G.py pymeasure/instruments/andeenhagerling/__init__.py pymeasure/instruments/andeenhagerling/ah2500a.py pymeasure/instruments/andeenhagerling/ah2700a.py pymeasure/instruments/anritsu/__init__.py pymeasure/instruments/anritsu/anritsuMG3692C.py pymeasure/instruments/anritsu/anritsuMS2090A.py pymeasure/instruments/anritsu/anritsuMS464xB.py pymeasure/instruments/anritsu/anritsuMS9710C.py pymeasure/instruments/anritsu/anritsuMS9740A.py pymeasure/instruments/attocube/__init__.py pymeasure/instruments/attocube/anc300.py pymeasure/instruments/bkprecision/__init__.py pymeasure/instruments/bkprecision/bkprecision9130b.py pymeasure/instruments/danfysik/__init__.py pymeasure/instruments/danfysik/danfysik8500.py pymeasure/instruments/deltaelektronika/__init__.py pymeasure/instruments/deltaelektronika/sm7045d.py pymeasure/instruments/edwards/__init__.py pymeasure/instruments/edwards/nxds.py pymeasure/instruments/eurotest/__init__.py pymeasure/instruments/eurotest/eurotestHPP120256.py pymeasure/instruments/fluke/__init__.py pymeasure/instruments/fluke/fluke7341.py pymeasure/instruments/fwbell/__init__.py pymeasure/instruments/fwbell/fwbell5080.py pymeasure/instruments/hcp/__init__.py pymeasure/instruments/hcp/tc038.py pymeasure/instruments/hcp/tc038d.py pymeasure/instruments/heidenhain/__init__.py pymeasure/instruments/heidenhain/nd287.py pymeasure/instruments/hp/__init__.py pymeasure/instruments/hp/hp11713a.py pymeasure/instruments/hp/hp33120A.py pymeasure/instruments/hp/hp3437A.py pymeasure/instruments/hp/hp34401A.py pymeasure/instruments/hp/hp3478A.py pymeasure/instruments/hp/hp437b.py pymeasure/instruments/hp/hp8116a.py pymeasure/instruments/hp/hp856Xx.py pymeasure/instruments/hp/hp8657b.py pymeasure/instruments/hp/hplegacyinstrument.py pymeasure/instruments/hp/hpsystempsu.py pymeasure/instruments/inficon/__init__.py pymeasure/instruments/inficon/sqm160.py pymeasure/instruments/ipgphotonics/__init__.py pymeasure/instruments/ipgphotonics/yar.py pymeasure/instruments/keithley/__init__.py pymeasure/instruments/keithley/buffer.py pymeasure/instruments/keithley/keithley2000.py pymeasure/instruments/keithley/keithley2182.py pymeasure/instruments/keithley/keithley2200.py pymeasure/instruments/keithley/keithley2260B.py pymeasure/instruments/keithley/keithley2306.py pymeasure/instruments/keithley/keithley2400.py pymeasure/instruments/keithley/keithley2450.py pymeasure/instruments/keithley/keithley2600.py pymeasure/instruments/keithley/keithley2700.py pymeasure/instruments/keithley/keithley2750.py pymeasure/instruments/keithley/keithley6221.py pymeasure/instruments/keithley/keithley6517b.py pymeasure/instruments/keithley/keithleyDMM6500.py pymeasure/instruments/kepco/__init__.py pymeasure/instruments/kepco/kepcobop.py pymeasure/instruments/keysight/__init__.py pymeasure/instruments/keysight/keysightDSOX1102G.py pymeasure/instruments/keysight/keysightE36312A.py pymeasure/instruments/keysight/keysightE3631A.py pymeasure/instruments/keysight/keysightN5767A.py pymeasure/instruments/keysight/keysightN7776C.py pymeasure/instruments/kuhneelectronic/__init__.py pymeasure/instruments/kuhneelectronic/kusg245_250a.py pymeasure/instruments/lakeshore/__init__.py pymeasure/instruments/lakeshore/lakeshore211.py pymeasure/instruments/lakeshore/lakeshore224.py pymeasure/instruments/lakeshore/lakeshore331.py pymeasure/instruments/lakeshore/lakeshore421.py pymeasure/instruments/lakeshore/lakeshore425.py pymeasure/instruments/lakeshore/lakeshore_base.py pymeasure/instruments/lecroy/__init__.py pymeasure/instruments/lecroy/lecroyT3DSO1204.py pymeasure/instruments/mksinst/__init__.py pymeasure/instruments/mksinst/mks937b.py pymeasure/instruments/mksinst/mks974b.py pymeasure/instruments/mksinst/mksinst.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/novanta/__init__.py pymeasure/instruments/novanta/fpu60.py pymeasure/instruments/oxfordinstruments/__init__.py pymeasure/instruments/oxfordinstruments/base.py pymeasure/instruments/oxfordinstruments/ips120_10.py pymeasure/instruments/oxfordinstruments/itc503.py pymeasure/instruments/oxfordinstruments/ps120_10.py pymeasure/instruments/parker/__init__.py pymeasure/instruments/parker/parkerGV6.py pymeasure/instruments/pendulum/__init__.py pymeasure/instruments/pendulum/cnt91.py pymeasure/instruments/proterial/__init__.py pymeasure/instruments/proterial/rod4.py pymeasure/instruments/racal/__init__.py pymeasure/instruments/racal/racal1992.py pymeasure/instruments/razorbill/__init__.py pymeasure/instruments/razorbill/razorbillRP100.py pymeasure/instruments/redpitaya/__init__.py pymeasure/instruments/redpitaya/redpitaya_scpi.py pymeasure/instruments/rohdeschwarz/__init__.py pymeasure/instruments/rohdeschwarz/fsl.py pymeasure/instruments/rohdeschwarz/hmp.py pymeasure/instruments/rohdeschwarz/sfm.py pymeasure/instruments/siglenttechnologies/__init__.py pymeasure/instruments/siglenttechnologies/siglent_spd1168x.py pymeasure/instruments/siglenttechnologies/siglent_spd1305x.py pymeasure/instruments/siglenttechnologies/siglent_spdbase.py pymeasure/instruments/signalrecovery/__init__.py pymeasure/instruments/signalrecovery/dsp7225.py pymeasure/instruments/signalrecovery/dsp7265.py pymeasure/instruments/signalrecovery/dsp_base.py pymeasure/instruments/srs/__init__.py pymeasure/instruments/srs/sg380.py pymeasure/instruments/srs/sr510.py pymeasure/instruments/srs/sr570.py pymeasure/instruments/srs/sr830.py pymeasure/instruments/srs/sr860.py pymeasure/instruments/tcpowerconversion/__init__.py pymeasure/instruments/tcpowerconversion/tccxn.py pymeasure/instruments/tdk/__init__.py pymeasure/instruments/tdk/tdk_base.py pymeasure/instruments/tdk/tdk_gen40_38.py pymeasure/instruments/tdk/tdk_gen80_65.py pymeasure/instruments/tektronix/__init__.py pymeasure/instruments/tektronix/afg3152c.py pymeasure/instruments/tektronix/tds2000.py pymeasure/instruments/teledyne/__init__.py pymeasure/instruments/teledyne/teledyneMAUI.py pymeasure/instruments/teledyne/teledyneT3AFG.py pymeasure/instruments/teledyne/teledyne_oscilloscope.py pymeasure/instruments/temptronic/__init__.py pymeasure/instruments/temptronic/temptronic_ats525.py pymeasure/instruments/temptronic/temptronic_ats545.py pymeasure/instruments/temptronic/temptronic_base.py pymeasure/instruments/temptronic/temptronic_eco560.py pymeasure/instruments/texio/__init__.py pymeasure/instruments/texio/texioPSW360L30.py pymeasure/instruments/thermotron/__init__.py pymeasure/instruments/thermotron/thermotron3800.py pymeasure/instruments/thorlabs/__init__.py pymeasure/instruments/thorlabs/thorlabspm100usb.py pymeasure/instruments/thorlabs/thorlabspro8000.py pymeasure/instruments/thyracont/__init__.py pymeasure/instruments/thyracont/smartline_v1.py pymeasure/instruments/thyracont/smartline_v2.py pymeasure/instruments/toptica/__init__.py pymeasure/instruments/toptica/ibeamsmart.py pymeasure/instruments/velleman/__init__.py pymeasure/instruments/velleman/velleman_k8090.py pymeasure/instruments/yokogawa/__init__.py pymeasure/instruments/yokogawa/aq6370series.py pymeasure/instruments/yokogawa/yokogawa7651.py pymeasure/instruments/yokogawa/yokogawags200.py tests/conftest.py tests/test_expected_protocol.py tests/test_generator.py tests/test_log.py tests/test_process.py tests/test_thread.py tests/adapters/test_adapter.py tests/adapters/test_prologix.py tests/adapters/test_protocol.py tests/adapters/test_serial.py tests/adapters/test_serial_with_loopback.py tests/adapters/test_visa.py tests/display/test_console.py tests/display/test_inputs.py tests/display/test_plotter.py tests/display/test_windows.py tests/display/widgets/test_inputs_widget.py tests/experiment/test_listeners.py tests/experiment/test_metadata.py tests/experiment/test_parameters.py tests/experiment/test_procedure.py tests/experiment/test_replace_placeholders.py tests/experiment/test_results.py tests/experiment/test_sequencer.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_all_instruments.py tests/instruments/test_channel.py tests/instruments/test_common_base.py tests/instruments/test_connection_configuration.py tests/instruments/test_generic_types.py tests/instruments/test_instrument.py tests/instruments/test_validators.py tests/instruments/activetechnologies/test_AWG401x.py tests/instruments/advantest/test_advantestR624X.py tests/instruments/agilent/test_agilent33500.py tests/instruments/agilent/test_agilent33500_with_device.py tests/instruments/agilent/test_agilent34450A_with_device.py tests/instruments/agilent/test_agilent4284A.py tests/instruments/agilent/test_agilent4294A.py tests/instruments/aimtti/test_PL303QMDP_with_device.py tests/instruments/aimtti/test_aimttl.py tests/instruments/aja/test_dcxs.py tests/instruments/ametek/test_ametek7270.py tests/instruments/anaheimautomation/test_dpseriesmotorcontroller.py tests/instruments/anritsu/test_anritsuMG3692C.py tests/instruments/anritsu/test_anritsuMS2090A.py tests/instruments/anritsu/test_anritsuMS464xB.py tests/instruments/attocube/test_anc300.py tests/instruments/danfysik/test_danfysik8500.py tests/instruments/eurotest/test_eurotestHPP120256.py tests/instruments/fluke/test_fluke7341.py tests/instruments/fwbell/test_fwbell5080.py tests/instruments/hcp/test_tc038.py tests/instruments/hcp/test_tc038d.py tests/instruments/hp/test_hp11713a.py tests/instruments/hp/test_hp33120A.py tests/instruments/hp/test_hp34401a.py tests/instruments/hp/test_hp34401a_with_device.py tests/instruments/hp/test_hp3478a.py tests/instruments/hp/test_hp437b.py tests/instruments/hp/test_hp437b_with_device.py tests/instruments/hp/test_hp8116a.py tests/instruments/hp/test_hp8116a_with_device.py tests/instruments/hp/test_hp856Xx.py tests/instruments/hp/test_hp8657b.py tests/instruments/inficon/test_sqm160.py tests/instruments/ipgphotonics/test_yar.py tests/instruments/keithley/test_keithley2000.py tests/instruments/keithley/test_keithley2182.py tests/instruments/keithley/test_keithley2200.py tests/instruments/keithley/test_keithley2306.py tests/instruments/keithley/test_keithley2306_with_device.py tests/instruments/keithley/test_keithley2400.py tests/instruments/keithley/test_keithley2750.py tests/instruments/keithley/test_keithleyDMM6500.py tests/instruments/keithley/test_keithleyDMM6500_with_device.py tests/instruments/kepco/test_kepcobop.py tests/instruments/keysight/test_keysightDSOX1102G_with_device.py tests/instruments/keysight/test_keysightE36312A.py tests/instruments/keysight/test_keysightE3631A.py tests/instruments/keysight/test_keysightE3631A_with_device.py tests/instruments/kuhneelectronic/test_kusg245_250a.py tests/instruments/lakeshore/test_lakeshore211.py tests/instruments/lakeshore/test_lakeshore421.py tests/instruments/lakeshore/test_lakeshore425.py tests/instruments/lecroy/test_lecroyT3DSO1204.py tests/instruments/lecroy/test_lecroyT3DSO1204_with_device.py tests/instruments/mksinst/test_mks937b.py tests/instruments/mksinst/test_mks974b.py tests/instruments/novanta/test_fpu60.py tests/instruments/oxfordinstruments/test_base_instrument.py tests/instruments/oxfordinstruments/test_ips120_10.py tests/instruments/oxfordinstruments/test_ps120_10.py tests/instruments/parker/test_parkerGV6.py tests/instruments/pendulum/test_cnt91.py tests/instruments/proterial/test_rod4.py tests/instruments/racal/test_racal1992.py tests/instruments/redpitaya/test_redpitaya_scpi.py tests/instruments/redpitaya/test_redpitaya_scpi_with_device.py tests/instruments/rohdeschwarz/test_hmp.py tests/instruments/rohdeschwarz/test_hmp_with_device.py tests/instruments/siglenttechnologies/test_siglent_spd1168x.py tests/instruments/siglenttechnologies/test_siglent_spd1305x.py tests/instruments/signalrecovery/test_dsp7225.py tests/instruments/signalrecovery/test_dsp7265.py tests/instruments/signalrecovery/test_dspbase.py tests/instruments/srs/test_sr830.py tests/instruments/tcpowerconversion/test_cxn.py tests/instruments/tdk/test_tdk_base.py tests/instruments/tdk/test_tdk_gen40-38.py tests/instruments/tdk/test_tdk_gen80-65.py tests/instruments/tektronix/test_afg3152.py tests/instruments/teledyne/test_teledyneMAUI.py tests/instruments/teledyne/test_teledyneMAUI_with_device.py tests/instruments/teledyne/test_teledyneT3AFG.py tests/instruments/temptronic/test_temptronic_base.py tests/instruments/texio/test_texioPSW360L30.py tests/instruments/texio/test_texioPSW360L30_with_device.py tests/instruments/thyracont/test_smartline_v1.py tests/instruments/thyracont/test_smartline_v2.py tests/instruments/toptica/test_ibeamsmart.py tests/instruments/velleman/test_velleman_k8090.py tests/instruments/velleman/test_velleman_k8090_with_device.py tests/instruments/yokogawa/test_aq6370d.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367998.0 pymeasure-0.14.0/PyMeasure.egg-info/dependency_links.txt0000644000175100001770000000000114623331176022673 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367998.0 pymeasure-0.14.0/PyMeasure.egg-info/requires.txt0000644000175100001770000000042314623331176021224 0ustar00runnerdockernumpy<3,>=1.6.1 pandas<3,>=0.14 pint pyvisa>=1.9 pyserial>=2.7 pyqtgraph>=0.12 [:python_version < "3.8"] importlib-metadata [python-vxi11] python-vxi11>=0.9 [tcp] pyzmq>=16.0.2 cloudpickle>=0.3.1 [tests] pytest>=3.3.0 pytest-cov>=4.1.0 pytest-qt>=2.4.0 pyvisa-sim>=0.4.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367998.0 pymeasure-0.14.0/PyMeasure.egg-info/top_level.txt0000644000175100001770000000001214623331176021350 0ustar00runnerdockerpymeasure ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/README.rst0000644000175100001770000000426114623331163014707 0ustar00runnerdocker.. 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.8-3.12, and is tested with continuous-integration on Linux, macOS, and Windows. .. image:: https://github.com/pymeasure/pymeasure/actions/workflows/pymeasure_CI.yml/badge.svg :target: https://github.com/pymeasure/pymeasure/actions/workflows/pymeasure_CI.yml .. 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.595633.svg :target: https://doi.org/10.5281/zenodo.595633 .. 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 .. image:: https://codecov.io/gh/pymeasure/pymeasure/graph/badge.svg :target: https://codecov.io/gh/pymeasure/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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/RELEASE.md0000644000175100001770000000623214623331163014622 0ustar00runnerdocker# Release ## PyMeasure package 1. Pull the latest `master` branch 2. `git checkout -b v_release` 3. Update CHANGES.rst with the changelog * On the repo page, go to Tags->Releases->Draft a new release * Add a dummy tag name and select "create tag on publish" -- we will not execute this, just use it to autogenerate a changelog * The button "Generate release notes" will generate Markdown text with all PRs since the last tag -- copy that into CHANGES.rst * Adapt the format and structure to the previous release message: * Divide the entries into categories and try to begin entries with "New", "Add", "Fix" or "Remove" as appropriate. (This could also be automated by the above generator with some labeling effort on our part) * We also remove the PR URLs as they clutter the log and condense the new contributors list. 4. Update the version number in CITATION.cff * On the line starting with `version: `, replace the current version number with the new version number 5. Push the changes up as a PR 6. Verify that the builds complete 7. Merge the PR 8. Create a new [release on GitHub](https://github.com/pymeasure/pymeasure/releases) * Add a tag name in the format "vX.Y.Z" and select "create tag on publish" * You'll have to paste in the changelog entry and probably edit it a bit as that form expects Markdown, not ReST (probably just removing `:code:` tags will be sufficient). * Publish the release 8. Approve the _build and upload_ run under Actions. This will create the wheel and upload it to PyPI. ## PyPI release - manually Official guide [here](https://packaging.python.org/en/latest/tutorials/packaging-projects/). If the upload action does not work, you can create a PyPI release manually: 1. Fetch `master`, build and check the source packages - `python -m pip install --upgrade build twine` - `python -m build` - Check the distributions (`twine check dist/*`, version will not yet be correct) 2. Ensure to have a git tag in the format "vX.Y.Z" 3. Build final packages and confirm the correct version number is being used - `python -m build` - Check the distributions (`twine check dist/*`) 4. Upload the wheel and source distributions to the test server - `python -m twine upload --repository testpypi dist/*` 5. Verify the test repository: https://test.pypi.org/project/PyMeasure 6. Confirm that the installation works (best in a separate environment) - `python -m pip install --index-url https://test.pypi.org/simple/ --no-deps pymeasure` 7. Upload to the real repository (`twine upload dist/PyMeasure-*`) 8. Verify that the package is updated: https://pypi.org/project/PyMeasure ## conda-forge feedstock 1. Release to PyPI first (the feedstock pulls from there) 2. Pull the latest `master` branch 3. `git checkout -b v_release` 4. Get the SHA256 hash of the PyPI source package at https://pypi.org/project/PyMeasure/#files 5. Update recipe/meta.yml with the checksum and version number. Important: Work in your personal fork of the feedstock repo (the conda-forge tooling requires that) and create a PR from there. 6. Push the changes up as a PR 7. Verify that the builds complete 8. Merge the PR ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/codecov.yml0000644000175100001770000000006414623331163015362 0ustar00runnerdockercoverage: status: project: off patch: off ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3296044 pymeasure-0.14.0/docs/0000755000175100001770000000000014623331176014151 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/Makefile0000644000175100001770000001516614623331163015616 0ustar00runnerdocker# 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." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3296044 pymeasure-0.14.0/docs/about/0000755000175100001770000000000014623331176015263 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/about/authors.rst0000644000175100001770000000073714623331163017505 0ustar00runnerdockerAuthors ======= 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: .. include:: ../../AUTHORS.txt :literal: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/about/changes.rst0000644000175100001770000000011414623331163017415 0ustar00runnerdocker:tocdepth: 1 ========= Changelog ========= .. include:: ../../CHANGES.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/about/license.rst0000644000175100001770000000207614623331163017440 0ustar00runnerdockerLicense ======= Copyright (c) 2013-2024 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. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3296044 pymeasure-0.14.0/docs/api/0000755000175100001770000000000014623331176014722 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/adapters.rst0000644000175100001770000000422714623331163017260 0ustar00runnerdocker################## pymeasure.adapters ################## The adapter classes allow the instruments to be independent of the communication method used. The instrument implementation takes care of any potential quirks in its communication protocol (see :ref:`advanced_communication_protocols`), and the adapter takes care of the details of the over-the-wire communication with the hardware device. In the vast majority of cases, it will be sufficient to pass a connection string or integer to the instrument (see :ref:`connecting-to-an-instrument`), which uses the :class:`pymeasure.adapters.VISAAdapter` in the background. ================== Adapter base class ================== .. autoclass:: pymeasure.adapters.Adapter :members: :undoc-members: ============ VISA adapter ============ .. autoclass:: pymeasure.adapters.VISAAdapter :members: :undoc-members: :inherited-members: :show-inheritance: ============== Serial adapter ============== .. autoclass:: pymeasure.adapters.SerialAdapter :members: :undoc-members: :inherited-members: :show-inheritance: :private-members: _format_binary_values ================ Prologix adapter ================ .. autoclass:: pymeasure.adapters.PrologixAdapter :members: :undoc-members: :inherited-members: :show-inheritance: :private-members: _format_binary_values ============== 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: ============= Test adapters ============= These pieces are useful when writing tests. .. automodule:: pymeasure.test :members: :undoc-members: :show-inheritance: .. autoclass:: pymeasure.adapters.ProtocolAdapter :members: :undoc-members: :show-inheritance: .. autoclass:: pymeasure.adapters.FakeAdapter :members: :undoc-members: :inherited-members: :show-inheritance: .. autoclass:: pymeasure.generator.Generator :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3336043 pymeasure-0.14.0/docs/api/display/0000755000175100001770000000000014623331176016367 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/Qt.rst0000644000175100001770000000032014623331163017474 0ustar00runnerdocker########## Qt classes ########## All Qt imports should reference :code:`pymeasure.display.Qt`, for consistent importing from either PySide or PyQt4. .. automethod:: pymeasure.display.Qt.fromUi :noindex:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/browser.rst0000644000175100001770000000020014623331163020570 0ustar00runnerdocker############### Browser classes ############### .. automodule:: pymeasure.display.browser :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/console.rst0000644000175100001770000000017314623331163020560 0ustar00runnerdocker############# Console class ############# .. automodule:: pymeasure.display.console :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/curves.rst0000644000175100001770000000017414623331163020426 0ustar00runnerdocker############## Curves classes ############## .. automodule:: pymeasure.display.curves :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/index.rst0000644000175100001770000000045014623331163020223 0ustar00runnerdocker################# pymeasure.display ################# This section contains specific documentation on the classes and methods of the package. .. toctree:: :maxdepth: 2 browser console curves inputs listeners log manager plotter Qt thread widgets windows ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/inputs.rst0000644000175100001770000000017414623331163020441 0ustar00runnerdocker############## Inputs classes ############## .. automodule:: pymeasure.display.inputs :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/listeners.rst0000644000175100001770000000021014623331163021116 0ustar00runnerdocker################# Listeners classes ################# .. automodule:: pymeasure.display.listeners :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/log.rst0000644000175100001770000000016014623331163017673 0ustar00runnerdocker########### Log classes ########### .. automodule:: pymeasure.display.log :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/manager.rst0000644000175100001770000000020014623331163020517 0ustar00runnerdocker############### Manager classes ############### .. automodule:: pymeasure.display.manager :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/plotter.rst0000644000175100001770000000017214623331163020606 0ustar00runnerdocker############# Plotter class ############# .. automodule:: pymeasure.display.plotter :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/thread.rst0000644000175100001770000000017414623331163020366 0ustar00runnerdocker############## Thread classes ############## .. automodule:: pymeasure.display.thread :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/widgets.rst0000644000175100001770000000301414623331163020561 0ustar00runnerdocker############## Widget classes ############## .. automodule:: pymeasure.display.widgets.browser_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.directory_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.estimator_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.fileinput_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.filename_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.image_frame :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.image_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.inputs_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.log_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.plot_frame :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.plot_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.results_dialog :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.sequencer_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.tab_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.dock_widget :members: :show-inheritance: .. automodule:: pymeasure.display.widgets.table_widget :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/display/windows.rst0000644000175100001770000000066614623331163020617 0ustar00runnerdocker############### Windows classes ############### .. automodule:: pymeasure.display.windows.managed_image_window :members: :show-inheritance: .. automodule:: pymeasure.display.windows.managed_window :members: :show-inheritance: .. automodule:: pymeasure.display.windows.plotter_window :members: :show-inheritance: .. automodule:: pymeasure.display.windows.managed_dock_window :members: :show-inheritance:././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3336043 pymeasure-0.14.0/docs/api/experiment/0000755000175100001770000000000014623331176017102 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/experiment/experiment.rst0000644000175100001770000000040414623331163022006 0ustar00runnerdocker################ 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/experiment/index.rst0000644000175100001770000000040214623331163020733 0ustar00runnerdocker#################### pymeasure.experiment #################### This section contains specific documentation on the classes and methods of the package. .. toctree:: :maxdepth: 2 experiment listeners procedure parameters workers results././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/experiment/listeners.rst0000644000175100001770000000022614623331163021640 0ustar00runnerdocker############## Listener class ############## .. automodule:: pymeasure.experiment.listeners :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/experiment/parameters.rst0000644000175100001770000000040414623331163021771 0ustar00runnerdocker################# 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:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/experiment/procedure.rst0000644000175100001770000000015614623331163021622 0ustar00runnerdocker############### Procedure class ############### .. automodule:: pymeasure.experiment.procedure :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/experiment/results.rst0000644000175100001770000000014514623331163021331 0ustar00runnerdocker############# Results class ############# .. automodule:: pymeasure.experiment.results :members:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/experiment/workers.rst0000644000175100001770000000021614623331163021323 0ustar00runnerdocker############ Worker class ############ .. automodule:: pymeasure.experiment.workers :members: :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3376045 pymeasure-0.14.0/docs/api/instruments/0000755000175100001770000000000014623331176017315 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3376045 pymeasure-0.14.0/docs/api/instruments/activetechnologies/0000755000175100001770000000000014623331176023174 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/activetechnologies/AWG401x.rst0000644000175100001770000000101314623331163024750 0ustar00runnerdocker################################################################# Active Technologies AWG-401x 1.2GS/s Arbitrary Waveform Generator ################################################################# .. autoclass:: pymeasure.instruments.activetechnologies.AWG401x_AFG :members: :show-inheritance: .. autoclass:: pymeasure.instruments.activetechnologies.AWG401x_AWG :members: :show-inheritance: .. autoclass:: pymeasure.instruments.activetechnologies.AWG401x.ChannelAFG :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/activetechnologies/index.rst0000644000175100001770000000061214623331163025030 0ustar00runnerdocker.. module:: pymeasure.instruments.activetechnologies ################### Active Technologies ################### This section contains specific documentation on the Active Technologies instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 AWG401x ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3376045 pymeasure-0.14.0/docs/api/instruments/advantest/0000755000175100001770000000000014623331176021306 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/advantest/advantestR3767CG.rst0000644000175100001770000000034514623331163024712 0ustar00runnerdocker######################################### Advantest R3767CG Vector Network Analyzer ######################################### .. automodule:: pymeasure.instruments.advantest.advantestR3767CG :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/advantest/advantestR624X.rst0000644000175100001770000003450614623331163024543 0ustar00runnerdocker######################################################### Advantest R6245/R6246 DC Voltage/Current Sources/Monitors ######################################################### .. currentmodule:: pymeasure.instruments.advantest.advantestR624X ********************************************** Main Classes ********************************************** .. autoclass:: AdvantestR6245 :members: :show-inheritance: :member-order: bysource .. autoclass:: AdvantestR6246 :members: :show-inheritance: :member-order: bysource .. autoclass:: AdvantestR624X :members: :show-inheritance: :member-order: bysource .. autoclass:: SMUChannel :members: :show-inheritance: :member-order: bysource .. automodule:: pymeasure.instruments.advantest.advantestR624X :members: :exclude-members: AdvantestR6245, AdvantestR6246, AdvantestR624X, SMUChannel :show-inheritance: :member-order: bysource .. contents:: ********************************************** General Information ********************************************** The R6245/6246 Series are DC voltage/current sources and monitors having source measurement units (SMUs) with 2 isolated channels. The series covers wide source and measurement ranges. It is ideal for measurement of DC characteristics of items ranging from separate semiconductors such as bipolar transistors, MOSFETs and GaAsFETs, to ICs and power devices. Further, due to the increased measuring speed and synchronized 2-channel measurement function, device I/O characteristics can be measured with precise timing at high speed which was previously difficult to accomplish. Due to features such as the trigger link function and the sequence programming function which automatically performs a series of evaluation tests automatically, the R6245/6246 enable much more efficient evaluation tests. There is a total of 99 commands, the majority of commands have been implemented. Device documentation is in Japanese, and the device options are enormous. The implementation is based on 6245S-GPIB-B-FHJ-8335160E01.pdf, which can be downloaded from the ADCMT website. ********************************************** Examples ********************************************** Initialization of the Instrument ==================================== .. code-block:: python from pymeasure.instruments.advantest import AdvantestR6246 from pymeasure.instruments.advantest.advantestR624X import * smu = AdvantestR6246("GPIB::1") Simple dual channel measurement example ======================================= Measurement characteristics: Channel A: Vce = 20V Channel B: Ib = 10uA - 60uA .. code-block:: python smu = AdvantestR6246("GPIB::1") smu.reset() # Set default parameters smu.ch_A.set_sample_mode(SampleMode.PULSED_SYNC) # Pulsed synchronized smu.ch_A.voltage_source(source_range = VoltageRange.AUTO, source_value = 20, current_compliance = 0.06) smu.ch_A.measure_current() smu.ch_B.current_source(source_range = CurrentRange.AUTO, source_value = 1E-5, # Source current at 10 uA voltage_compliance = 5) # Voltage compliance at 5 V smu.ch_B.measure_voltage() smu.enable_source() # Enables source A & B for i in range(10, 60): k = i * 0.000001 smu.ch_B.current_change_source = k # Set current from 10 uA to 60 uA smu.trigger() # Trigger measurement smu.ch_A.select_for_output() Ic = smu.read_measurement() # Read channel A measurement smu.ch_B.select_for_output() Vbe = smu.read_measurement() # Read channel B measurement print(f'Ic={Ic}, Vbe={Vbe}') # Print measurements smu.standby() # Put channel A & B in standby Program example for DC measurement ===================================== Measurement characteristics: Function: VSIM - Source voltage and measure current Trigger voltage: 10V Current compliance: 0.5A Measurement delay time: 1ms Integration time: 1 PLC Response: Fast After operating, the measurement is repeated 10 times with a trigger command and he prints out the results. .. code-block:: python smu = AdvantestR6246("GPIB::1") smu.reset() # Set default parameters smu.ch_A.set_sample_mode(SampleMode.ASYNC, False) # Asynchronous operation and single shot sampling by trigger and command smu.ch_A.voltage_source(source_range = VoltageRange.FIXED_BEST, source_value = 10, current_compliance = 0.5) # compliance of 0.5A smu.ch_A.measure_current() # Measure current smu.ch_A.set_timing_parameters(hold_time = 0, # 0 sec hold time measurement_delay = 1E-3, # 1ms delay between measurements pulsed_width = 5E-3, # 5ms pulse width pulsed_period = 10E-3) # 10ms pulse period smu.ch_A.sample_hold_mode = SampleHold.MODE_1PLC # Sample at 1 power line cycle smu.ch_A.fast_mode_enabled = True # Set channel response to fast smu.ch_A.enable_source() # Set channel in operating state smu.ch_A.select_for_output() # Select channel for measurement output for i in range(1, 10): smu.ch_A.trigger() # Trigger a measurement measurement = smu.read_measurement() print(f"NO {i} {measurement}") smu.ch_A.standby() # Put channel A in standby mode Program example for DC measurement (with external trigger) ========================================================== Measurement characteristics: Function: VSIM - Source voltage and measure current Source voltage: 10 V Base voltage 1 V Current compliance: 0.5 A Pulse width: 5 ms Pulse period: 10 ms Measurement delay time: 1 ms Integration time: 1 ms Response: Fast After operating, an external trigger input signal is pulsed to measure the channel operation register. Reads the fixed end bit, captures the measurement data, and prints out the measurement result. .. code-block:: python smu = AdvantestR6246("GPIB::1") smu.reset() # Set default parameters smu.ch_A.auto_zero_enabled = False smu.ch_A.set_sample_mode(SampleMode.ASYNC, False) # Asynchronous operation and single shot sampling by trigger and command smu.ch_A.voltage_pulsed_source( source_range = VoltageRange.FIXED_BEST, pulse_value = 10, base_value = 1, current_compliance = 0.5) smu.ch_A.measure_current() # Measure current smu.ch_A.fast_mode_enabled = True # Set channel response to fast smu.ch_A.sample_hold_mode = SampleHold.MODE_1mS # Sample at 1mS smu.ch_A.set_timing_parameters(hold_time = 0, # 0 sec hold time measurement_delay = 1E-3, # 1ms delay between measurements pulsed_width = 5E-3, # 5ms pulse width pulsed_period = 10E-3) # 10ms pulse period smu.ch_A.trigger_input = TriggerInputType.ALL # Mode 1 enables the trigger input signal smu.ch_A.output_enable_register = COR.HAS_MEASUREMENT_DATA # Measurement data available smu.service_request_enable_register = SRER.COP # COP Set when a bit in the Channel Operations Register is set with the Enable Register set to Enable. smu.ch_A.enable_source() # Set channel in operating state smu.ch_A.select_for_output() # Select channel for measurement output for i in range(1, 10): while not smu.ch_A.operation_register & COR.HAS_MEASUREMENT_DATA: pass measurement = smu.read_measurement() print(f"NO {i} {measurement}") while not smu.ch_A.operation_register & COR.WAITING_FOR_TRIGGER: pass smu.ch_A.standby() # Put channel A in standby mode Program example for pulse measurement ================================================= Measurement characteristics: Function: ISVM - Source current and measure voltage Pulse generation current: 100mA Base current: 1mA Voltage compliance: 5V Pulse width: 0 Pulse period : 0 Measurement delay time: 0 Integration time: 1ms Response: Fast After the operation, repeat the measurement 10 times with the trigger command and print out the measurement results. .. code-block:: python smu = AdvantestR6246("GPIB::1") smu.reset() # Set default parameters smu.ch_A.set_sample_mode(SampleMode.ASYNC, auto_sampling = False) smu.ch_A.current_pulsed_source( source_range = CurrentRange.FIXED_600mA, pulse_value = 0.1, # 100mA base_value = 1E-3, # 1mA voltage_compliance = 5) # 5V smu.ch_A.measure_voltage(voltage_range = VoltageRange.FIXED_BEST) smu.ch_A.fast_mode_enabled = True # Set channel response to fast smu.ch_A.sample_hold_mode = SampleHold.MODE_1mS # Sample at 1mS smu.ch_A.set_timing_parameters(hold_time = 0, # 0 sec hold time measurement_delay = 0, # 0 sec delay between measurements pulsed_width = 0, # 0 sec pulse width pulsed_period = 0) # 0 sec pulse period smu.ch_A.enable_source() # Set channel in operating state smu.ch_A.select_for_output() # Select channel for measurement output for i in range(1, 10): smu.ch_A.trigger() # Trigger measurement measurement = smu.read_measurement() print(f"NO {i} {measurement}") while not smu.ch_A.operation_register & COR.WAITING_FOR_TRIGGER: pass smu.ch_A.standby() # Put channel A in standby mode Fixed Level Sweep Program Example ================================================= Measurement characteristics: function: VSVM - Voltage source and voltage measurement Level value: 15V Bias value: 0V Number of measurements: 20 times Compliance: 6mA Measuring range: Best fixed range (=60V range) Integration time: 100us Measurement delay time: 0 Hold time: 1ms Sampling mode: automatic sweep Measurement data output method: Buffering output (output of specified data) After operating, make 20 measurements in fixed sweep. Detect the end of sweep by looking at the Channel Operation Register (COR). After the sweep is finished, read the measured data from 1 to 2 using the RMM command. .. code-block:: python smu = AdvantestR6246("GPIB::1") # First we setup our main parameters smu.reset() # Set default parameters smu.ch_A.set_output_type(output_type = OutputType.BUFFERING_OUTPUT_SPECIFIED, measurement_type = MeasurementType.MEASURE_DATA) smu.set_output_format(delimiter_format = 2, # No header, ASCII format block_delimiter = 1, # Make it the same as the terminator terminator = 1) # CR, LF smu.ch_A.analog_input = 1 # Turn off the analog input. smu.set_lo_common_connection_relay(enable = True) # Turns the connection relay on smu.ch_A.set_wire_mode(four_wire = False, # disable four wire measurements lo_guard = True) # enable the LO-GUARD relay. smu.ch_A.auto_zero_enabled = False smu.ch_A.trigger_input = TriggerInputType.ALL # Mode 1 enables the trigger input signal # Now we set measurement specific variables smu.ch_A.clear_measurement_buffer() smu.ch_A.set_sample_mode(SampleMode.ASYNC, auto_sampling = True) smu.ch_A.voltage_fixed_level_sweep(voltage_range = VoltageRange.FIXED_60V, voltage_level = 15, measurement_count = 20, # 20 measurements current_compliance = 6E-3, # compliance at 6mA bias = 0) smu.ch_A.measure_voltage(voltage_range = VoltageRange.FIXED_BEST) smu.ch_A.sample_hold_mode = SampleHold.MODE_100uS smu.ch_A.set_timing_parameters(hold_time = 1E-3, # 1ms sec hold time measurement_delay = 0, # 0 sec delay between measurements pulsed_width = 0, # 0 sec pulse width pulsed_period = 0) # 0 sec pulse period smu.ch_A.enable_source() # Set channel in operating state smu.ch_A.trigger() # Start the sweep while not smu.ch_A.operation_register & COR.END_OF_SWEEP: # Wait until the sweep is done pass # Read measurements for i in range(1, 20): measurement = smu.ch_A.read_measurement_from_addr(i) print(i, measurement) smu.ch_A.standby() # Put channel A in standby mode ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/advantest/index.rst0000644000175100001770000000056414623331163023150 0ustar00runnerdocker.. 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 advantestR624X ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3376045 pymeasure-0.14.0/docs/api/instruments/agilent/0000755000175100001770000000000014623331176020740 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilent33220A.rst0000644000175100001770000000034514623331163023606 0ustar00runnerdocker########################################### Agilent 33220A Arbitrary Waveform Generator ########################################### .. autoclass:: pymeasure.instruments.agilent.Agilent33220A :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilent33500.rst0000644000175100001770000000042114623331163023501 0ustar00runnerdocker########################################################## Agilent 33500 Function/Arbitrary Waveform Generator Family ########################################################## .. autoclass:: pymeasure.instruments.agilent.Agilent33500 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilent33521A.rst0000644000175100001770000000056414623331163023615 0ustar00runnerdocker#################################################### Agilent 33521A Function/Arbitrary Waveform Generator #################################################### .. autoclass:: pymeasure.instruments.agilent.Agilent33521A :members: :show-inheritance: .. autoclass:: pymeasure.instruments.agilent.agilent33500.Agilent33500Channel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilent34410A.rst0000644000175100001770000000027414623331163023611 0ustar00runnerdocker################################ Agilent 34410A Multimeter ################################ .. autoclass:: pymeasure.instruments.agilent.Agilent34410A :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilent34450A.rst0000644000175100001770000000035314623331163023613 0ustar00runnerdocker############################################# HP/Agilent/Keysight 34450A Digital Multimeter ############################################# .. autoclass:: pymeasure.instruments.agilent.Agilent34450A :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilent4156.rst0000644000175100001770000000037114623331163023432 0ustar00runnerdocker################################################## Agilent 4155/4156 Semiconductor Parameter Analyzer ################################################## .. automodule:: pymeasure.instruments.agilent.agilent4156 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilent4284A.rst0000644000175100001770000000024714623331163023537 0ustar00runnerdocker####################### Agilent 4284A LCR Meter ####################### .. autoclass:: pymeasure.instruments.agilent.Agilent4284A :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilent4294A.rst0000644000175100001770000000034114623331163023533 0ustar00runnerdocker########################################## Agilent 4294A Precision Impedance Analyzer ########################################## .. autoclass:: pymeasure.instruments.agilent.Agilent4294A :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilent8257D.rst0000644000175100001770000000027414623331163023546 0ustar00runnerdocker############################## Agilent 8257D Signal Generator ############################## .. autoclass:: pymeasure.instruments.agilent.Agilent8257D :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilent8722ES.rst0000644000175100001770000000032514623331163023664 0ustar00runnerdocker###################################### Agilent 8722ES Vector Network Analyzer ###################################### .. autoclass:: pymeasure.instruments.agilent.Agilent8722ES :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilentB1500.rst0000644000175100001770000002606314623331163023530 0ustar00runnerdocker############################################## 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() # set data output format (required!) 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 because 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) #Start Measurement b1500.check_errors() b1500.clear_buffer() b1500.clear_timer() b1500.send_trigger() 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilentE4408B.rst0000644000175100001770000000030314623331163023634 0ustar00runnerdocker################################ Agilent E4408B Spectrum Analyzer ################################ .. autoclass:: pymeasure.instruments.agilent.AgilentE4408B :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/agilentE4980.rst0000644000175100001770000000026514623331163023546 0ustar00runnerdocker############################## Agilent E4980 LCR Meter ############################## .. autoclass:: pymeasure.instruments.agilent.AgilentE4980 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/agilent/index.rst0000644000175100001770000000127414623331163022601 0ustar00runnerdocker.. 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 `. If the instrument you are looking for is not here, also check :doc:`HP<../hp/index>` for older instruments or :doc:`Keysight<../keysight/index>` for newer ones. .. toctree:: :maxdepth: 2 agilent8257D agilent8722ES agilentE4408B agilentE4980 agilent34410A agilent34450A agilent4156 agilent4294A agilent33220A agilent33500 agilent33521A agilentB1500 agilent4284A ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3376045 pymeasure-0.14.0/docs/api/instruments/aimtti/0000755000175100001770000000000014623331176020604 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/aimtti/aimttiPL.rst0000644000175100001770000000257514623331163023066 0ustar00runnerdocker.. _aimtti-landing-page: ############################################# Aim-TTI PL Series Power Supplies ############################################# .. autoclass:: pymeasure.instruments.aimtti.aimttiPL.PLBase :members: :show-inheritance: .. autoclass:: pymeasure.instruments.aimtti.aimttiPL.PLChannel :members: :show-inheritance: .. All explicitly documented members are excluded so that there is no duplication, .. but we still get docs for all members that are not explicitly enumerated. .. autoclass:: pymeasure.instruments.aimtti.aimttiPL.PL068P :members: :exclude-members: ch_1 :undoc-members: :show-inheritance: .. autoclass:: pymeasure.instruments.aimtti.aimttiPL.PL155P :members: :exclude-members: ch_1 :undoc-members: :show-inheritance: .. autoclass:: pymeasure.instruments.aimtti.aimttiPL.PL303P :members: :exclude-members: ch_1 :undoc-members: :show-inheritance: .. autoclass:: pymeasure.instruments.aimtti.aimttiPL.PL601P :members: :exclude-members: ch_1 :undoc-members: :show-inheritance: .. autoclass:: pymeasure.instruments.aimtti.aimttiPL.PL303QMDP :members: :exclude-members: ch_1, ch_2 :undoc-members: :show-inheritance: .. autoclass:: pymeasure.instruments.aimtti.aimttiPL.PL303QMTP :members: :exclude-members: ch_1, ch_2, ch_3 :undoc-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/aimtti/index.rst0000644000175100001770000000051714623331163022444 0ustar00runnerdocker.. module:: pymeasure.instruments.aimtti ####### Aim-TTI ####### This section contains specific documentation on the Aim-TTI instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 aimttiPL ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3416045 pymeasure-0.14.0/docs/api/instruments/aja/0000755000175100001770000000000014623331176020050 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/aja/DCXS.rst0000644000175100001770000000040214623331163021333 0ustar00runnerdocker######################################################### AJA DCXS-750 or 1500 DC magnetron sputtering power supply ######################################################### .. autoclass:: pymeasure.instruments.aja.DCXS :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/aja/index.rst0000644000175100001770000000056014623331163021706 0ustar00runnerdocker.. module:: pymeasure.instruments.aja ################# AJA International ################# This section contains specific documentation on the AJA International instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 DCXS ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3416045 pymeasure-0.14.0/docs/api/instruments/ametek/0000755000175100001770000000000014623331176020563 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/ametek/ametek7270.rst0000755000175100001770000000030714623331163023102 0ustar00runnerdocker################################ Ametek 7270 DSP Lockin Amplifier ################################ .. autoclass:: pymeasure.instruments.ametek.Ametek7270 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/ametek/index.rst0000755000175100001770000000051514623331163022424 0ustar00runnerdocker.. 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3416045 pymeasure-0.14.0/docs/api/instruments/ami/0000755000175100001770000000000014623331176020063 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/ami/ami430.rst0000644000175100001770000000022414623331163021604 0ustar00runnerdocker#################### AMI 430 Power Supply #################### .. autoclass:: pymeasure.instruments.ami.AMI430 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/ami/index.rst0000644000175100001770000000047114623331163021722 0ustar00runnerdocker.. 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 ami430././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3416045 pymeasure-0.14.0/docs/api/instruments/anaheimautomation/0000755000175100001770000000000014623331176023020 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/anaheimautomation/dpseriesstepmotorcontroller.rst0000644000175100001770000000117214623331163031446 0ustar00runnerdocker################################# DP-Series Step Motor Controller ################################# The DPSeriesMotorController class implements a base driver class for Anaheim-Automation DP Series stepper motor controllers. There are many controllers sold in this series, all of which implement the same core command set. Some controllers, like the DPY50601, implement additional functionality that is not included in this driver. If these additional features are desired, they should be implemented in a subclass. .. autoclass:: pymeasure.instruments.anaheimautomation.DPSeriesMotorController :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/anaheimautomation/index.rst0000644000175100001770000000064714623331163024664 0ustar00runnerdocker.. module:: pymeasure.instruments.anaheimautomation ######################### Anaheim Automation ######################### This section contains specific documentation on the Anaheim Automation instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 dpseriesstepmotorcontroller ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3416045 pymeasure-0.14.0/docs/api/instruments/anapico/0000755000175100001770000000000014623331176020727 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/anapico/apsin12G.rst0000644000175100001770000000030114623331163023033 0ustar00runnerdocker################################# Anapico APSIN12G Signal Generator ################################# .. autoclass:: pymeasure.instruments.anapico.APSIN12G :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/anapico/index.rst0000644000175100001770000000051714623331163022567 0ustar00runnerdocker.. 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 apsin12G././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3416045 pymeasure-0.14.0/docs/api/instruments/andeenhagerling/0000755000175100001770000000000014623331176022430 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/andeenhagerling/ah2500a.rst0000644000175100001770000000034714623331163024222 0ustar00runnerdocker########################################### Andeen Hagerling AH2500A capacitance bridge ########################################### .. autoclass:: pymeasure.instruments.andeenhagerling.AH2500A :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/andeenhagerling/ah2700a.rst0000644000175100001770000000041214623331163024215 0ustar00runnerdocker########################################### Andeen Hagerling AH2700A capacitance bridge ########################################### .. autoclass:: pymeasure.instruments.andeenhagerling.AH2700A :members: :show-inheritance: :inherited-members: CommonBase ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/andeenhagerling/index.rst0000644000175100001770000000060614623331163024267 0ustar00runnerdocker.. module:: pymeasure.instruments.andeenhagerling ################ Andeen Hagerling ################ This section contains specific documentation on the Andeen Hagerling instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 ah2500a ah2700a ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3416045 pymeasure-0.14.0/docs/api/instruments/anritsu/0000755000175100001770000000000014623331176021002 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/anritsu/anritsuMG3692C.rst0000644000175100001770000000030414623331163024065 0ustar00runnerdocker################################ Anritsu MG3692C Signal Generator ################################ .. autoclass:: pymeasure.instruments.anritsu.AnritsuMG3692C :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/anritsu/anritsuMS2090A.rst0000644000175100001770000000034214623331163024070 0ustar00runnerdocker########################################## Anritsu MS2090A Handheld Spectrum Analyzer ########################################## .. autoclass:: pymeasure.instruments.anritsu.AnritsuMS2090A :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/anritsu/anritsuMS464xB.rst0000644000175100001770000000155614623331163024214 0ustar00runnerdocker####################################### Anritsu MS464xB Vector Network Analyzer ####################################### .. autoclass:: pymeasure.instruments.anritsu.AnritsuMS4642B :show-inheritance: .. autoclass:: pymeasure.instruments.anritsu.AnritsuMS4644B :show-inheritance: .. autoclass:: pymeasure.instruments.anritsu.AnritsuMS4645B :show-inheritance: .. autoclass:: pymeasure.instruments.anritsu.AnritsuMS4647B :show-inheritance: .. autoclass:: pymeasure.instruments.anritsu.AnritsuMS464xB :members: :show-inheritance: .. autoclass:: pymeasure.instruments.anritsu.anritsuMS464xB.MeasurementChannel :members: :show-inheritance: .. autoclass:: pymeasure.instruments.anritsu.anritsuMS464xB.Trace :members: :show-inheritance: .. autoclass:: pymeasure.instruments.anritsu.anritsuMS464xB.Port :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/anritsu/anritsuMS9710C.rst0000644000175100001770000000034214623331163024100 0ustar00runnerdocker########################################## Anritsu MS9710C Optical Spectrum Analyzer ########################################## .. autoclass:: pymeasure.instruments.anritsu.AnritsuMS9710C :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/anritsu/anritsuMS9740A.rst0000644000175100001770000000034014623331163024077 0ustar00runnerdocker######################################### Anritsu MS9740A Optical Spectrum Analyzer ######################################### .. autoclass:: pymeasure.instruments.anritsu.AnritsuMS9740A :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/anritsu/index.rst0000644000175100001770000000063714623331163022645 0ustar00runnerdocker.. 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 anritsuMS9740A anritsuMS2090A anritsuMS464xB ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3416045 pymeasure-0.14.0/docs/api/instruments/attocube/0000755000175100001770000000000014623331176021123 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/attocube/anc300.rst0000644000175100001770000000046214623331163022637 0ustar00runnerdocker################################# Attocube ANC300 Motion Controller ################################# .. autoclass:: pymeasure.instruments.attocube.anc300.ANC300Controller :members: :show-inheritance: .. autoclass:: pymeasure.instruments.attocube.anc300.Axis :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/attocube/index.rst0000644000175100001770000000052314623331163022760 0ustar00runnerdocker.. 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 anc300 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3416045 pymeasure-0.14.0/docs/api/instruments/bkprecision/0000755000175100001770000000000014623331176021625 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/bkprecision/bkprecision9130b.rst0000644000175100001770000000032114623331163025336 0ustar00runnerdocker################################## BK Precision 9130B DC Power Supply ################################## .. autoclass:: pymeasure.instruments.bkprecision.BKPrecision9130B :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/bkprecision/index.rst0000644000175100001770000000055714623331163023471 0ustar00runnerdocker.. module:: pymeasure.instruments.bkprecision ############ BK Precision ############ This section contains specific documentation on the BK Precision instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 bkprecision9130b././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/comedi.rst0000644000175100001770000000043414623331163021304 0ustar00runnerdocker####################### 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: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3416045 pymeasure-0.14.0/docs/api/instruments/danfysik/0000755000175100001770000000000014623331176021125 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/danfysik/danfysik8500.rst0000644000175100001770000000026114623331163023777 0ustar00runnerdocker########################## Danfysik 8500 Power Supply ########################## .. autoclass:: pymeasure.instruments.danfysik.Danfysik8500 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/danfysik/index.rst0000644000175100001770000000053114623331163022761 0ustar00runnerdocker.. 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 danfysik8500 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3456047 pymeasure-0.14.0/docs/api/instruments/deltaelektronica/0000755000175100001770000000000014623331176022627 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/deltaelektronica/index.rst0000644000175100001770000000060014623331163024460 0ustar00runnerdocker.. 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/deltaelektronica/sm7045d.rst0000644000175100001770000000033114623331163024455 0ustar00runnerdocker###################################### Delta Elektronica SM7045D Power source ###################################### .. autoclass:: pymeasure.instruments.deltaelektronika.SM7045D :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3456047 pymeasure-0.14.0/docs/api/instruments/edwards/0000755000175100001770000000000014623331176020746 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/edwards/index.rst0000644000175100001770000000051414623331163022603 0ustar00runnerdocker.. module:: pymeasure.instruments.edwards ####### Edwards ####### This section contains specific documentation on the Edwards instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 nxds ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/edwards/nxds.rst0000644000175100001770000000024314623331163022447 0ustar00runnerdocker######################## Edwards nxds vacuum pump ######################## .. autoclass:: pymeasure.instruments.edwards.Nxds :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3456047 pymeasure-0.14.0/docs/api/instruments/eurotest/0000755000175100001770000000000014623331176021167 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/eurotest/eurotestHPP120256.rst0000644000175100001770000000036014623331163024616 0ustar00runnerdocker############################################# Euro Test HPP120256 High Voltage Power Supply ############################################# .. autoclass:: pymeasure.instruments.eurotest.EurotestHPP120256 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/eurotest/index.rst0000644000175100001770000000054114623331163023024 0ustar00runnerdocker.. module:: pymeasure.instruments.eurotest ######### EURO TEST ######### This section contains specific documentation on the EURO TEST instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 eurotestHPP120256././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3456047 pymeasure-0.14.0/docs/api/instruments/fluke/0000755000175100001770000000000014623331176020423 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/fluke/fluke7341.rst0000644000175100001770000000025714623331163022602 0ustar00runnerdocker########################### Fluke 7341 Temperature bath ########################### .. autoclass:: pymeasure.instruments.fluke.Fluke7341 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/fluke/index.rst0000644000175100001770000000050714623331163022262 0ustar00runnerdocker.. module:: pymeasure.instruments.fluke ##### Fluke ##### This section contains specific documentation on the Fluke instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 fluke7341 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3456047 pymeasure-0.14.0/docs/api/instruments/fwbell/0000755000175100001770000000000014623331176020570 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/fwbell/fwbell5080.rst0000644000175100001770000000035114623331163023105 0ustar00runnerdocker################################## F.W. Bell 5080 Handheld Gaussmeter ################################## .. autoclass:: pymeasure.instruments.fwbell.FWBell5080 :members: :show-inheritance: :inherited-members: CommonBase ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/fwbell/index.rst0000644000175100001770000000053014623331163022423 0ustar00runnerdocker.. 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 fwbell5080././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/generic_types.rst0000644000175100001770000000046414623331163022707 0ustar00runnerdocker############################### Generic Instrument Types Mixins ############################### You can use these Mixins as additional parent classes for your instrument. For more informations, see :ref:`common_instrument_types`. .. autoclass:: pymeasure.instruments.generic_types.SCPIMixin :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3456047 pymeasure-0.14.0/docs/api/instruments/hcp/0000755000175100001770000000000014623331176020067 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hcp/index.rst0000644000175100001770000000055514623331163021731 0ustar00runnerdocker.. module:: pymeasure.instruments.hcp ############### HC Photonics ############### This section contains specific documentation on the HC Photonics instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 tc038 tc038d ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hcp/tc038.rst0000644000175100001770000000023214623331163021453 0ustar00runnerdocker###################### HCP TC038 crystal oven ###################### .. autoclass:: pymeasure.instruments.hcp.TC038 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hcp/tc038d.rst0000644000175100001770000000023614623331163021623 0ustar00runnerdocker####################### HCP TC038D crystal oven ####################### .. autoclass:: pymeasure.instruments.hcp.TC038D :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3456047 pymeasure-0.14.0/docs/api/instruments/heidenhain/0000755000175100001770000000000014623331176021411 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/heidenhain/index.rst0000644000175100001770000000054014623331163023245 0ustar00runnerdocker.. module:: pymeasure.instruments.heidenhain ########## Heidenhain ########## This section contains specific documentation on the Heidenhain instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 nd287 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/heidenhain/nd287.rst0000644000175100001770000000034414623331163023002 0ustar00runnerdocker########################################### Heidenhain ND287 Position Display Unit ########################################### .. autoclass:: pymeasure.instruments.heidenhain.ND287 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3496046 pymeasure-0.14.0/docs/api/instruments/hp/0000755000175100001770000000000014623331176017724 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/hp11713A.rst0000644000175100001770000000112714623331163021560 0ustar00runnerdocker########################################### HP 11713A Attenuator/Switch Driver ########################################### .. autoclass:: pymeasure.instruments.hp.HP11713A :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp11713a.SwitchDriverChannel :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp11713a.Attenuator_11dB .. autoclass:: pymeasure.instruments.hp.hp11713a.Attenuator_110dB .. autoclass:: pymeasure.instruments.hp.hp11713a.Attenuator_70dB_3_Section .. autoclass:: pymeasure.instruments.hp.hp11713a.Attenuator_70dB_4_Section ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/hp33120A.rst0000644000175100001770000000031314623331163021550 0ustar00runnerdocker###################################### HP 33120A Arbitrary Waveform Generator ###################################### .. autoclass:: pymeasure.instruments.hp.HP33120A :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/hp3437A.rst0000644000175100001770000000027614623331163021510 0ustar00runnerdocker###################################### HP 3437A System-Voltmeter ###################################### .. autoclass:: pymeasure.instruments.hp.HP3437A :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/hp34401A.rst0000644000175100001770000000022614623331163021556 0ustar00runnerdocker#################### HP 34401A Multimeter #################### .. autoclass:: pymeasure.instruments.hp.HP34401A :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/hp3478A.rst0000644000175100001770000000027014623331163021507 0ustar00runnerdocker###################################### HP 3478A Multimeter ###################################### .. autoclass:: pymeasure.instruments.hp.HP3478A :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/hp437B.rst0000644000175100001770000000022314623331163021416 0ustar00runnerdocker#################### HP 437B Power Meter #################### .. autoclass:: pymeasure.instruments.hp.HP437B :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/hp8116A.rst0000644000175100001770000000032714623331163021504 0ustar00runnerdocker########################################### HP 8116A 50 MHz Pulse/Function Generator ########################################### .. autoclass:: pymeasure.instruments.hp.HP8116A :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/hp856xx.rst0000644000175100001770000002572314623331163021715 0ustar00runnerdocker################################## HP 8560A / 8561B Spectrum Analyzer ################################## Every unit is used in the base unit, so for time it is s (Seconds), frequency in Hz (Hertz) etc... ------------------------------------- Generic Specific Attributes & Methods ------------------------------------- .. contents:: Content :depth: 3 :local: ^^^^^^^ General ^^^^^^^ .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.preset .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.attenuation .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.amplitude_unit .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.trigger_mode .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.detector_mode .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.coupling .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_auto_couple .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_linear_scale .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.logarithmic_scale .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.threshold .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_title .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.status .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.check_done .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.request_service .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.errors .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.save_state .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.recall_state .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.request_service_conditions .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_maximum_hold .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_minimum_hold .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.reference_level_calibration .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.reference_offset .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.reference_level .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.display_line .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.protect_state_enabled .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.mixer_level .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.frequency_counter_mode_enabled .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.frequency_counter_resolution .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.adjust_all .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.adjust_if .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.hold .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.annotation_enabled .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_crt_adjustment_pattern .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.display_parameters .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.firmware_revision .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.graticule_enabled .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.serial_number .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.id .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.elapsed_time ^^^^^^^^^^^^ Demodulation ^^^^^^^^^^^^ .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.demodulation_mode .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.demodulation_agc_enabled .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.demodulation_time .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.squelch ^^^^^^^^^ Frequency ^^^^^^^^^ .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.start_frequency .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.stop_frequency .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.center_frequency .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.frequency_offset .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.frequency_reference_source .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.span .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_full_span .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.frequency_display_enabled ^^^^^^^^^^^^^^^^^^^^ Resolution Bandwidth ^^^^^^^^^^^^^^^^^^^^ .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.resolution_bandwidth .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.resolution_bandwidth_to_span_ratio ^^^^^ Video ^^^^^ .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.video_trigger_level .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.video_bandwidth_to_resolution_bandwidth .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.video_bandwidth .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.video_average ^^^^^^^^^^^^^^^^^^ FFT & Measurements ^^^^^^^^^^^^^^^^^^ .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.create_fft_trace_window .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.get_power_bandwidth .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.do_fft ^^^^^ Trace ^^^^^ .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.view_trace .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.get_trace_data_a .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.get_trace_data_b .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_trace_data_a .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_trace_data_b .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.trace_data_format .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.save_trace .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.recall_trace .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.clear_write_trace .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.subtract_display_line_from_trace_b .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.exchange_traces .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.blank_trace .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.trace_a_minus_b_plus_dl_enabled .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.trace_a_minus_b_enabled ^^^^^^ Marker ^^^^^^ .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.search_peak .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.marker_amplitude .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_marker_to_center_frequency .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.marker_delta .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.marker_frequency .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_marker_minimum .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.marker_noise_mode_enabled .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.deactivate_marker .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.marker_threshold .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.peak_excursion .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_marker_to_reference_level .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_marker_delta_to_span .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_marker_to_center_frequency_step_size .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.marker_time .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.marker_signal_tracking_enabled ^^^^^^^^^^^^^^^^^ Diagnostic Values ^^^^^^^^^^^^^^^^^ .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.sampling_frequency .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.lo_frequency .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.mroll_frequency .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.oroll_frequency .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.xroll_frequency .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.sampler_harmonic_number ^^^^^ Sweep ^^^^^ .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.sweep_single .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.sweep_time .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.sweep_couple .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.sweep_output .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.set_continuous_sweep .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.trigger_sweep ^^^^^^^^^^^^^ Normalization ^^^^^^^^^^^^^ .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.normalize_trace_data_enabled .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.normalized_reference_level .. autoattribute:: pymeasure.instruments.hp.hp856Xx.HP856Xx.normalized_reference_position ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Open/Short Calibration (Reflection) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.recall_open_short_average .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.store_open .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.store_short ^^^^^^^^^^^^^^^^ Thru Calibration ^^^^^^^^^^^^^^^^ .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.store_thru .. automethod:: pymeasure.instruments.hp.hp856Xx.HP856Xx.recall_thru ------------------------------------- HP8560A Specific Attributes & Methods ------------------------------------- .. autoclass:: pymeasure.instruments.hp.HP8560A :members: :show-inheritance: ------------------------------------- HP8561B Specific Attributes & Methods ------------------------------------- .. autoclass:: pymeasure.instruments.hp.HP8561B :members: :show-inheritance: ------------ Enumerations ------------ .. autoclass:: pymeasure.instruments.hp.hp856Xx.AmplitudeUnits :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.MixerMode :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.Trace :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.CouplingMode :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.DemodulationMode :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.DetectionModes :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.ErrorCode :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.FrequencyReference :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.PeakSearchMode :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.SourceLevelingControlMode :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.StatusRegister :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.SweepCoupleMode :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.SweepOut :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.TriggerMode :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.hp856Xx.WindowType :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/hp8657B.rst0000644000175100001770000000132314623331163021514 0ustar00runnerdocker################################ HP Signal generator HP8657B ################################ *Note:* * This instrument does not support reading back values, as it is a listen-only GPIB device. * Other instruments of this family could be implemented using the dynamic ranges feature. * Optional pulse modulation feature is not supported yet. Glossary: ============ =========== Abbreviation Explanation ============ =========== AM Amplitude Modulation FM Frequency Modulation dBm power level in dB referenced to 1mW ============ =========== .. autoclass:: pymeasure.instruments.hp.HP8657B :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/hplegacyinstrument.rst0000644000175100001770000000062414623331163024401 0ustar00runnerdocker###################################### Support class for HP legacy devices ###################################### Currently this implementation is used for the following instruments which do not support SCPI: * HP3437A System-Voltmeter * HP3478A Digital Multimeter * HP6632/33/34A System power supply .. autoclass:: pymeasure.instruments.hp.HPLegacyInstrument :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/hpsystempsu.rst0000644000175100001770000000174114623331163023061 0ustar00runnerdocker################################ HP System Power Supplies HP663XA ################################ Currently supported models are: ===== ======== ========= ===== Model Voltage Current Power ===== ======== ========= ===== 6632A 0..20 V 0..5.0 A 100 W 6633A 0..50 V 0..2.5 A 100 W 6634A 0..100 V 0..1.0 A 100 W ===== ======== ========= ===== *Note:* * The multi-channel system power supplies HP 6621A, 6622A, 6623A, 6624A, 6625A, 6626A, 6627A & 6628A share some of the command syntax and could probably be incorporated in this implementation * The B-version of these models (6632B, 6633B & 6634B) are SPCI-compliant and could be implemented in a similar manner .. autoclass:: pymeasure.instruments.hp.HP6632A :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.HP6633A :members: :show-inheritance: .. autoclass:: pymeasure.instruments.hp.HP6634A :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/hp/index.rst0000644000175100001770000000120714623331163021561 0ustar00runnerdocker.. 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 `. If the instrument you are looking for is not here, also check :doc:`Agilent<../agilent/index>` or :doc:`Keysight<../keysight/index>` for newer instruments. .. toctree:: :maxdepth: 2 hp33120A hp34401A hp3437A hp3478A hp8116A hp856xx hp8657B hp11713A hp437B hplegacyinstrument hpsystempsu ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/index.rst0000644000175100001770000000255014623331163021154 0ustar00runnerdocker.. module:: pymeasure.instruments ##################### pymeasure.instruments ##################### This section contains documentation on the instrument classes. .. toctree:: :maxdepth: 2 instruments generic_types validators comedi resources Instruments by manufacturer: .. toctree:: :maxdepth: 2 activetechnologies/index advantest/index agilent/index aimtti/index aja/index ametek/index ami/index anaheimautomation/index anapico/index andeenhagerling/index anritsu/index attocube/index bkprecision/index danfysik/index deltaelektronica/index edwards/index eurotest/index fluke/index fwbell/index heidenhain/index hcp/index hp/index inficon/index ipgphotonics/index keithley/index kepco/index keysight/index kuhneelectronic/index lakeshore/index lecroy/index mksinst/index newport/index ni/index novanta/index oxfordinstruments/index parker/index pendulum/index proterial/index racal/index razorbill/index redpitaya/index rohdeschwarz/index siglenttechnologies/index signalrecovery/index srs/index tcpowerconversion/index tdk/index tektronix/index teledyne/index temptronic/index texio/index thermotron/index thorlabs/index thyracont/index toptica/index velleman/index yokogawa/index ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3496046 pymeasure-0.14.0/docs/api/instruments/inficon/0000755000175100001770000000000014623331176020742 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/inficon/index.rst0000644000175100001770000000051614623331163022601 0ustar00runnerdocker.. module:: pymeasure.instruments.inficon ####### Inficon ####### This section contains specific documentation on the Inficon instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 sqm160 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/inficon/sqm160.rst0000644000175100001770000000053714623331163022524 0ustar00runnerdocker################################################# Inficon SQM-160 multi-film rate/thickness monitor ################################################# .. autoclass:: pymeasure.instruments.inficon.sqm160.SQM160 :members: :show-inheritance: .. autoclass:: pymeasure.instruments.inficon.sqm160.SensorChannel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/instruments.rst0000644000175100001770000000067614623331163022447 0ustar00runnerdocker################## Instrument classes ################## .. autoclass:: pymeasure.instruments.common_base.CommonBase :members: .. autoclass:: pymeasure.instruments.Instrument :members: .. autoclass:: pymeasure.instruments.Channel :members: .. autoclass:: pymeasure.instruments.fakes.FakeInstrument :members: :show-inheritance: .. autoclass:: pymeasure.instruments.fakes.SwissArmyFake :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3496046 pymeasure-0.14.0/docs/api/instruments/ipgphotonics/0000755000175100001770000000000014623331176022023 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/ipgphotonics/index.rst0000644000175100001770000000055014623331163023660 0ustar00runnerdocker.. module:: pymeasure.instruments.ipgphotonics ############# IPG Photonics ############# This section contains specific documentation on the IPG Photonics instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 yar ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/ipgphotonics/yar.rst0000644000175100001770000000026114623331163023343 0ustar00runnerdocker########################## YAR fiber amplifier series ########################## .. autoclass:: pymeasure.instruments.ipgphotonics.yar.YAR :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3496046 pymeasure-0.14.0/docs/api/instruments/keithley/0000755000175100001770000000000014623331176021133 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/index.rst0000644000175100001770000000103614623331163022770 0ustar00runnerdocker.. 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 keithley2260B keithley2306 keithley2400 keithley2450 keithley2700 keithley6221 keithley6517b keithley2750 keithley2600 keithley2200 keithleyDMM6500 keithley2182 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley2000.rst0000644000175100001770000000031714623331163024002 0ustar00runnerdocker######################## Keithley 2000 Multimeter ######################## .. autoclass:: pymeasure.instruments.keithley.Keithley2000 :members: :show-inheritance: :inherited-members: CommonBase ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley2182.rst0000644000175100001770000000051414623331163024014 0ustar00runnerdocker########################### Keithley 2182 Nanovoltmeter ########################### .. autoclass:: pymeasure.instruments.keithley.Keithley2182 :members: :show-inheritance: :inherited-members: CommonBase .. autoclass:: pymeasure.instruments.keithley.keithley2182.Keithley2182Channel :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley2200.rst0000644000175100001770000000064514623331163024010 0ustar00runnerdocker################################### Keithley 2200 Series Power Supplies ################################### .. autoclass:: pymeasure.instruments.keithley.Keithley2200 :members: :show-inheritance: :inherited-members: :exclude-members: ask, control, clear, measurement, read, setting, values, write .. autoclass:: pymeasure.instruments.keithley.keithley2200.PSChannel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley2260B.rst0000644000175100001770000000034114623331163024111 0ustar00runnerdocker############################## Keithley 2260B DC Power Supply ############################## .. autoclass:: pymeasure.instruments.keithley.Keithley2260B :members: :show-inheritance: :inherited-members: CommonBase././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley2306.rst0000644000175100001770000000044214623331163024012 0ustar00runnerdocker#################################################### Keithley 2306 Dual Channel Battery/Charger Simulator #################################################### .. autoclass:: pymeasure.instruments.keithley.Keithley2306 :members: :show-inheritance: :inherited-members: CommonBase././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley2400.rst0000644000175100001770000000032114623331163024001 0ustar00runnerdocker######################### Keithley 2400 SourceMeter ######################### .. autoclass:: pymeasure.instruments.keithley.Keithley2400 :members: :show-inheritance: :inherited-members: CommonBase././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley2450.rst0000644000175100001770000000032114623331163024006 0ustar00runnerdocker######################### Keithley 2450 SourceMeter ######################### .. autoclass:: pymeasure.instruments.keithley.Keithley2450 :members: :show-inheritance: :inherited-members: CommonBase././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley2600.rst0000644000175100001770000000032214623331163024004 0ustar00runnerdocker######################### Keithley 2600 SourceMeter ######################### .. autoclass:: pymeasure.instruments.keithley.Keithley2600 :members: :show-inheritance: :inherited-members: CommonBase ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley2700.rst0000644000175100001770000000037014623331163024010 0ustar00runnerdocker###################################### Keithley 2700 MultiMeter/Switch System ###################################### .. autoclass:: pymeasure.instruments.keithley.Keithley2700 :members: :show-inheritance: :inherited-members: CommonBase././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley2750.rst0000644000175100001770000000037114623331163024016 0ustar00runnerdocker###################################### Keithley 2750 Multimeter/Switch System ###################################### .. autoclass:: pymeasure.instruments.keithley.Keithley2750 :members: :show-inheritance: :inherited-members: CommonBase ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley6221.rst0000644000175100001770000000037014623331163024012 0ustar00runnerdocker###################################### Keithley 6221 AC and DC Current Source ###################################### .. autoclass:: pymeasure.instruments.keithley.Keithley6221 :members: :show-inheritance: :inherited-members: CommonBase././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithley6517b.rst0000644000175100001770000000033114623331163024161 0ustar00runnerdocker########################### Keithley 6517B Electrometer ########################### .. autoclass:: pymeasure.instruments.keithley.Keithley6517B :members: :show-inheritance: :inherited-members: CommonBase ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keithley/keithleyDMM6500.rst0000644000175100001770000000061014623331163024345 0ustar00runnerdocker########################### Keithley DMM6500 Multimeter ########################### .. autoclass:: pymeasure.instruments.keithley.KeithleyDMM6500 :members: :show-inheritance: :exclude-members: ask, control, clear, measurement, read, setting, values, write .. autoclass:: pymeasure.instruments.keithley.keithleyDMM6500.ScannerCard2000Channel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3496046 pymeasure-0.14.0/docs/api/instruments/kepco/0000755000175100001770000000000014623331176020416 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/kepco/bop.rst0000644000175100001770000000042414623331163021724 0ustar00runnerdocker############################################################# BOP Bipolar Power Supply with BIT 4886 Digital Interface Card ############################################################# .. autoclass:: pymeasure.instruments.kepco.kepcobop :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/kepco/index.rst0000644000175100001770000000054614623331163022260 0ustar00runnerdocker.. module:: pymeasure.instruments.kepco ########## KEPCO INC. ########## This section contains specific dpcumentation on the Kepco Inc. power supplies instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 bop ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3496046 pymeasure-0.14.0/docs/api/instruments/keysight/0000755000175100001770000000000014623331176021144 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keysight/index.rst0000644000175100001770000000107014623331163022777 0ustar00runnerdocker.. 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 `. If the instrument you are looking for is not here, also check :doc:`Agilent<../agilent/index>` or :doc:`HP<../hp/index>` for older instruments. .. toctree:: :maxdepth: 2 keysightDSOX1102G keysightN5767A keysightN7776C keysightE36312A keysightE3631A././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keysight/keysightDSOX1102G.rst0000644000175100001770000000030614623331163024631 0ustar00runnerdocker############################### Keysight DSOX1102G Oscilloscope ############################### .. autoclass:: pymeasure.instruments.keysight.KeysightDSOX1102G :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keysight/keysightE36312A.rst0000644000175100001770000000057114623331163024331 0ustar00runnerdocker############################################## Keysight E36312A Triple Output Power Supply ############################################## .. autoclass:: pymeasure.instruments.keysight.KeysightE36312A :members: :show-inheritance: :inherited-members: .. autoclass:: pymeasure.instruments.keysight.keysightE36312A.VoltageChannel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keysight/keysightE3631A.rst0000644000175100001770000000055614623331163024252 0ustar00runnerdocker########################################## Keysight E3631A Triple Output Power Supply ########################################## .. autoclass:: pymeasure.instruments.keysight.KeysightE3631A :members: :show-inheritance: :inherited-members: .. autoclass:: pymeasure.instruments.keysight.keysightE3631A.VoltageChannel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keysight/keysightN5767A.rst0000644000175100001770000000027214623331163024272 0ustar00runnerdocker############################ Keysight N5767A Power Supply ############################ .. autoclass:: pymeasure.instruments.keysight.KeysightN5767A :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/keysight/keysightN7776C.rst0000644000175100001770000000027214623331163024276 0ustar00runnerdocker############################ Keysight N5776C Power Supply ############################ .. autoclass:: pymeasure.instruments.keysight.KeysightN7776C :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3536048 pymeasure-0.14.0/docs/api/instruments/kuhneelectronic/0000755000175100001770000000000014623331176022477 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/kuhneelectronic/index.rst0000644000175100001770000000060014623331163024330 0ustar00runnerdocker.. module:: pymeasure.instruments.kuhneelectronic ################ Kuhne Electronic ################ This section contains specific documentation on the Kuhne Electronic instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 kusg245_250a ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/kuhneelectronic/kusg245_250a.rst0000644000175100001770000000042314623331163025157 0ustar00runnerdocker######################################################## KU SG 2.45 250 A - 2.45 GHz ISM-Band Microwave Generator ######################################################## .. autoclass:: pymeasure.instruments.kuhneelectronic.Kusg245_250A :members: :show-inheritance:././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3536048 pymeasure-0.14.0/docs/api/instruments/lakeshore/0000755000175100001770000000000014623331176021272 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/lakeshore/index.rst0000644000175100001770000000202314623331163023124 0ustar00runnerdocker.. 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 lakeshore211 lakeshore224 lakeshore331 lakeshore421 lakeshore425 .. _LakeShoreChannels: LakeShore Channel Classes -------------------------- Several Lakeshore instruments are channel based and make use of the :ref:`Channel Interface `. For temperature monitoring and controller instruments the following common :class:`Channel Classes ` are utilized: .. autoclass:: pymeasure.instruments.lakeshore.lakeshore_base.LakeShoreTemperatureChannel :members: :show-inheritance: .. autoclass:: pymeasure.instruments.lakeshore.lakeshore_base.LakeShoreHeaterChannel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/lakeshore/lakeshore211.rst0000644000175100001770000000031314623331163024216 0ustar00runnerdocker################################## Lake Shore 211 Temperature Monitor ################################## .. autoclass:: pymeasure.instruments.lakeshore.LakeShore211 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/lakeshore/lakeshore224.rst0000644000175100001770000000031314623331163024222 0ustar00runnerdocker################################## Lake Shore 224 Temperature Monitor ################################## .. autoclass:: pymeasure.instruments.lakeshore.LakeShore224 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/lakeshore/lakeshore331.rst0000644000175100001770000000032314623331163024222 0ustar00runnerdocker##################################### Lake Shore 331 Temperature Controller ##################################### .. autoclass:: pymeasure.instruments.lakeshore.LakeShore331 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/lakeshore/lakeshore421.rst0000644000175100001770000000026014623331163024222 0ustar00runnerdocker######################### Lake Shore 421 Gaussmeter ######################### .. autoclass:: pymeasure.instruments.lakeshore.LakeShore421 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/lakeshore/lakeshore425.rst0000644000175100001770000000025714623331163024234 0ustar00runnerdocker######################### Lake Shore 425 Gaussmeter ######################### .. autoclass:: pymeasure.instruments.lakeshore.LakeShore425 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3536048 pymeasure-0.14.0/docs/api/instruments/lecroy/0000755000175100001770000000000014623331176020612 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/lecroy/index.rst0000644000175100001770000000071714623331163022454 0ustar00runnerdocker.. module:: pymeasure.instruments.lecroy ######## LeCroy ######## This section contains specific documentation on the LeCroy instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. If the instrument you are looking for is not here, also check :doc:`Teledyne<../teledyne/index>` for newer instruments. .. toctree:: :maxdepth: 2 lecroyT3DSO1204././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/lecroy/lecroyT3DSO1204.rst0000644000175100001770000000055014623331163023721 0ustar00runnerdocker############################### LeCroy T3DSO1204 Oscilloscope ############################### .. autoclass:: pymeasure.instruments.lecroy.LeCroyT3DSO1204 :members: :show-inheritance: :inherited-members: :exclude-members: .. autoclass:: pymeasure.instruments.lecroy.lecroyT3DSO1204.LeCroyT3DSO1204Channel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3536048 pymeasure-0.14.0/docs/api/instruments/mksinst/0000755000175100001770000000000014623331176021005 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/mksinst/index.rst0000644000175100001770000000060114623331163022637 0ustar00runnerdocker.. module:: pymeasure.instruments.mksinst ############### MKS Instruments ############### This section contains specific documentation on the MKS Instruments devices that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 mksinst mks937b mks974b ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/mksinst/mks937b.rst0000644000175100001770000000105414623331163022732 0ustar00runnerdocker############################################ MKS Instruments 937B Vacuum Gauge Controller ############################################ .. autoclass:: pymeasure.instruments.mksinst.mks937b.MKS937B :members: :show-inheritance: .. autoclass:: pymeasure.instruments.mksinst.mks937b.IonGaugeAndPressureChannel :members: :show-inheritance: .. autoclass:: pymeasure.instruments.mksinst.mks937b.PressureChannel :members: :show-inheritance: .. autoclass:: pymeasure.instruments.mksinst.mks937b.Relay :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/mksinst/mks974b.rst0000644000175100001770000000052414623331163022734 0ustar00runnerdocker############################################### MKS Instruments 974B Vacuum Pressure Transducer ############################################### .. autoclass:: pymeasure.instruments.mksinst.mks974b.MKS974B :members: :show-inheritance: .. autoclass:: pymeasure.instruments.mksinst.mks974b.Relay :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/mksinst/mksinst.rst0000644000175100001770000000047514623331163023231 0ustar00runnerdocker################################### MKS Instruments Abstract Instrument ################################### .. autoclass:: pymeasure.instruments.mksinst.mksinst.MKSInstrument :members: :show-inheritance: .. autoclass:: pymeasure.instruments.mksinst.mksinst.RelayChannel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3536048 pymeasure-0.14.0/docs/api/instruments/newport/0000755000175100001770000000000014623331176021013 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/newport/esp300.rst0000644000175100001770000000072114623331163022553 0ustar00runnerdocker######################### 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:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/newport/index.rst0000644000175100001770000000051514623331163022651 0ustar00runnerdocker.. 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 esp300././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3536048 pymeasure-0.14.0/docs/api/instruments/ni/0000755000175100001770000000000014623331176017723 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/ni/index.rst0000644000175100001770000000060214623331163021556 0ustar00runnerdocker.. 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 virtualbench././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/ni/virtualbench.rst0000644000175100001770000000234714623331163023145 0ustar00runnerdocker################ 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: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3536048 pymeasure-0.14.0/docs/api/instruments/novanta/0000755000175100001770000000000014623331176020763 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/novanta/fpu60.rst0000644000175100001770000000031314623331163022446 0ustar00runnerdocker##################################### Novanta FPU60 laser power supply unit ##################################### .. autoclass:: pymeasure.instruments.novanta.Fpu60 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/novanta/index.rst0000644000175100001770000000065514623331163022626 0ustar00runnerdocker.. module:: pymeasure.instruments.novanta ################# Novanta Photonics ################# This section contains specific documentation on the Novanta photonics instruments that are implemented. Novanta contains also Lasers developed by Laserquantum. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 fpu60 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3576047 pymeasure-0.14.0/docs/api/instruments/oxfordinstruments/0000755000175100001770000000000014623331176023132 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/oxfordinstruments/IPS120_10.rst0000644000175100001770000000110014623331163025026 0ustar00runnerdocker############################################################################## Oxford Instruments Intelligent Power Supply 120-10 for superconducting magnets ############################################################################## .. autoclass:: pymeasure.instruments.oxfordinstruments.IPS120_10 :members: :show-inheritance: .. autoclass:: pymeasure.instruments.oxfordinstruments.ips120_10.MagnetError :members: :show-inheritance: .. autoclass:: pymeasure.instruments.oxfordinstruments.ips120_10.SwitchHeaterError :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/oxfordinstruments/ITC503.rst0000644000175100001770000000042214623331163024525 0ustar00runnerdocker######################################################### Oxford Instruments Intelligent Temperature Controller 503 ######################################################### .. autoclass:: pymeasure.instruments.oxfordinstruments.ITC503 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/oxfordinstruments/PS120_10.rst0000644000175100001770000000105114623331163024722 0ustar00runnerdocker################################################################## Oxford Instruments Power Supply 120-10 for superconducting magnets ################################################################## .. autoclass:: pymeasure.instruments.oxfordinstruments.PS120_10 :show-inheritance: .. autoclass:: pymeasure.instruments.oxfordinstruments.ips120_10.MagnetError :members: :show-inheritance: :noindex: .. autoclass:: pymeasure.instruments.oxfordinstruments.ips120_10.SwitchHeaterError :members: :show-inheritance: :noindex: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/oxfordinstruments/base.rst0000644000175100001770000000052314623331163024572 0ustar00runnerdocker################################## Oxford Instruments Base Instrument ################################## .. autoclass:: pymeasure.instruments.oxfordinstruments.base.OxfordInstrumentsBase :members: :show-inheritance: .. autoclass:: pymeasure.instruments.oxfordinstruments.base.OxfordVISAError :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/oxfordinstruments/index.rst0000644000175100001770000000064514623331163024774 0ustar00runnerdocker.. 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 base ITC503 IPS120_10 PS120_10 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3576047 pymeasure-0.14.0/docs/api/instruments/parker/0000755000175100001770000000000014623331176020601 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/parker/index.rst0000644000175100001770000000051314623331163022435 0ustar00runnerdocker.. 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 parkerGV6././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/parker/parkerGV6.rst0000644000175100001770000000030114623331163023130 0ustar00runnerdocker################################# Parker GV6 Servo Motor Controller ################################# .. autoclass:: pymeasure.instruments.parker.ParkerGV6 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3576047 pymeasure-0.14.0/docs/api/instruments/pendulum/0000755000175100001770000000000014623331176021146 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/pendulum/cnt91.rst0000644000175100001770000000030314623331163022626 0ustar00runnerdocker################################ Pendulum CNT91 frequency counter ################################ .. autoclass:: pymeasure.instruments.pendulum.cnt91.CNT91 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/pendulum/index.rst0000644000175100001770000000052214623331163023002 0ustar00runnerdocker.. module:: pymeasure.instruments.pendulum ######## Pendulum ######## This section contains specific documentation on the Pendulum instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 cnt91 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3576047 pymeasure-0.14.0/docs/api/instruments/proterial/0000755000175100001770000000000014623331176021316 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/proterial/index.rst0000644000175100001770000000056014623331163023154 0ustar00runnerdocker.. module:: pymeasure.instruments.proterial ######### Proterial ######### This section contains specific documentation on the Proterial (formerly Hitachi Metals) instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 rod4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/proterial/rod4.rst0000644000175100001770000000044114623331163022713 0ustar00runnerdocker#################### ROD-4 MFC Controller #################### .. autoclass:: pymeasure.instruments.proterial.ROD4 :members: :show-inheritance: :inherited-members: CommonBase .. autoclass:: pymeasure.instruments.proterial.rod4.ROD4Channel :members: :show-inheritance:././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3576047 pymeasure-0.14.0/docs/api/instruments/racal/0000755000175100001770000000000014623331176020377 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/racal/index.rst0000644000175100001770000000053214623331163022234 0ustar00runnerdocker.. module:: pymeasure.instruments.racal ########## Racal-Dana ########## This section contains specific documentation on the Raca-Dana instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 racal1992 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/racal/racal1992.rst0000644000175100001770000000030114623331163022526 0ustar00runnerdocker################################# Racal-Dana 1992 Universal Counter ################################# .. autoclass:: pymeasure.instruments.racal.Racal1992 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3576047 pymeasure-0.14.0/docs/api/instruments/razorbill/0000755000175100001770000000000014623331176021315 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/razorbill/index.rst0000644000175100001770000000054014623331163023151 0ustar00runnerdocker.. 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/razorbill/razorbillRP100.rst0000644000175100001770000000053214623331163024526 0ustar00runnerdocker################################################################################# Razorbill RP100 custrom power supply for Razorbill Instrums stress & strain cells ################################################################################# .. autoclass:: pymeasure.instruments.razorbill.razorbillRP100 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3576047 pymeasure-0.14.0/docs/api/instruments/redpitaya/0000755000175100001770000000000014623331176021277 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/redpitaya/index.rst0000644000175100001770000000054014623331163023133 0ustar00runnerdocker.. module:: pymeasure.instruments.redpitaya ######### Redpitaya ######### This section contains specific documentation on the Redpitaya instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 redpitaya_scpi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/redpitaya/redpitaya_scpi.rst0000644000175100001770000000061014623331163025022 0ustar00runnerdocker############################################################################################ Redpitaya board for analog signal acquisition and generation as well as digital input/output ############################################################################################ .. autoclass:: pymeasure.instruments.redpitaya.redpitaya_scpi.RedPitayaScpi :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/resources.rst0000644000175100001770000000034214623331163022054 0ustar00runnerdocker####################### Resource Manager ####################### The :code:`list_resources` function provides an interface to check connected instruments interactively. .. autofunction:: pymeasure.instruments.list_resources ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3576047 pymeasure-0.14.0/docs/api/instruments/rohdeschwarz/0000755000175100001770000000000014623331176022020 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/rohdeschwarz/fsl.rst0000644000175100001770000000545114623331163023337 0ustar00runnerdocker###################################### R&S FSL spectrum analyzer ###################################### Connecting to the instrument via network ---------------------------------------- Once connected to the network, the instrument's IP address can be found by clicking the "Setup" button and navigating to "General Settings" -> "Network Address". It can then be connected like this: .. code:: python from pymeasure.instruments.rohdeschwarz import FSL fsl = FSL("TCPIP::192.168.1.123::INSTR") Getting and setting parameters ------------------------------ Most parameters are implemented as properties, which means they can be read and written (getting and setting) in a consistent and simple way. If numerical values are provided, base units are used (s, Hz, dB, ...). Alternatively, the values can also be provided with a unit, e.g. ``"1.5 GHz"`` or ``"1.5GHz"``. Return values are always numerical. .. code:: python # Getting the current center frequency fsl.freq_center 9000000000.0 .. code:: python # Changing it to 10 MHz by providing the numerical value fsl.freq_center = 10e6 .. code:: python # Verifying: fsl.freq_center 10000000.0 .. code:: python # Changing it to 9 GHz by providing a string and verifying the result fsl.freq_center = '9GHz' fsl.freq_center 9000000000.0 .. code:: python # Setting the span to maximum fsl.freq_span = '7 GHz' Reading a trace --------------- We will read the current trace .. code:: python x, y = fsl.read_trace() Markers ------- Markers are implemented as their own class. You can create them like this: .. code:: python m1 = fsl.create_marker() Set peak exursion: .. code:: python m1.peak_excursion = 3 Set marker to a specific position: .. code:: python m1.x = 10e9 Find the next peak to the left and get the level: .. code:: python m1.to_next_peak('left') m1.y -34.9349060059 Delta markers ~~~~~~~~~~~~~ Delta markers can be created by setting the appropriate keyword. .. code:: python d2 = fsl.create_marker(is_delta_marker=True) d2.name 'DELT2' Example program --------------- Here is an example of a simple script for recording the peak of a signal. .. code:: python m1 = fsl.create_marker() # create marker 1 # Set standard settings, set to full span fsl.continuous_sweep = False fsl.freq_span = '18 GHz' fsl.res_bandwidth = "AUTO" fsl.video_bandwidth = "AUTO" fsl.sweep_time = "AUTO" # Perform a sweep on full span, set the marker to the peak and some to that marker fsl.single_sweep() m1.to_peak() m1.zoom('20 MHz') # take data from the zoomed-in region fsl.single_sweep() x, y = fsl.read_trace() .. autoclass:: pymeasure.instruments.rohdeschwarz.fsl.FSL :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/rohdeschwarz/hmp.rst0000644000175100001770000000031314623331163023327 0ustar00runnerdocker###################################### R&S HMP4040 Power Supply ###################################### .. autoclass:: pymeasure.instruments.rohdeschwarz.hmp.HMP4040 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/rohdeschwarz/index.rst0000644000175100001770000000057614623331163023665 0ustar00runnerdocker.. module:: pymeasure.instruments.rohdeschwarz ############### Rohde & Schwarz ############### This section contains specific documentation on the Rohde & Schwarz instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 sfm fsl hmp ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/rohdeschwarz/sfm.rst0000644000175100001770000000046414623331163023337 0ustar00runnerdocker###################################### R&S SFM TV test transmitter ###################################### .. autoclass:: pymeasure.instruments.rohdeschwarz.sfm.SFM :members: :show-inheritance: .. autoclass:: pymeasure.instruments.rohdeschwarz.sfm.Sound_Channel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3576047 pymeasure-0.14.0/docs/api/instruments/siglenttechnologies/0000755000175100001770000000000014623331176023366 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/siglenttechnologies/index.rst0000644000175100001770000000067614623331163025234 0ustar00runnerdocker.. module:: pymeasure.instruments.siglenttechnologies #################### Siglent Technologies #################### This section contains specific documentation on the Siglent Technologies instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 siglent_spdbase siglent_spd1168x siglent_spd1305x././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/siglenttechnologies/siglent_spd1168x.rst0000644000175100001770000000030214623331163027132 0ustar00runnerdocker############################# Siglent SPD1168X Power Supply ############################# .. autoclass:: pymeasure.instruments.siglenttechnologies.SPD1168X :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/siglenttechnologies/siglent_spd1305x.rst0000644000175100001770000000030214623331163027123 0ustar00runnerdocker############################# Siglent SPD1305X Power Supply ############################# .. autoclass:: pymeasure.instruments.siglenttechnologies.SPD1305X :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/siglenttechnologies/siglent_spdbase.rst0000644000175100001770000000106014623331163027257 0ustar00runnerdocker############################### Siglent Technologies Base Class ############################### .. autoclass:: pymeasure.instruments.siglenttechnologies.siglent_spdbase.SPDBase :members: :show-inheritance: .. autoclass:: pymeasure.instruments.siglenttechnologies.siglent_spdbase.SPDSingleChannelBase :members: :show-inheritance: .. autoclass:: pymeasure.instruments.siglenttechnologies.siglent_spdbase.SPDChannel :members: :show-inheritance: .. autoclass:: pymeasure.instruments.siglenttechnologies.siglent_spdbase.SystemStatusCode ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3576047 pymeasure-0.14.0/docs/api/instruments/signalrecovery/0000755000175100001770000000000014623331176022351 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/signalrecovery/dsp7225.rst0000644000175100001770000000032614623331163024206 0ustar00runnerdocker########################## DSP 7225 Lock-in Amplifier ########################## .. autoclass:: pymeasure.instruments.signalrecovery.DSP7225 :members: :show-inheritance: :inherited-members: CommonBase ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/signalrecovery/dsp7265.rst0000644000175100001770000000032514623331163024211 0ustar00runnerdocker########################## DSP 7265 Lock-in Amplifier ########################## .. autoclass:: pymeasure.instruments.signalrecovery.DSP7265 :members: :show-inheritance: :inherited-members: CommonBase././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/signalrecovery/index.rst0000644000175100001770000000060114623331163024203 0ustar00runnerdocker.. 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 dsp7225 dsp7265 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.361605 pymeasure-0.14.0/docs/api/instruments/srs/0000755000175100001770000000000014623331176020124 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/srs/index.rst0000644000175100001770000000066114623331163021764 0ustar00runnerdocker.. 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 sr510 sr570 sr830 sr860././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/srs/sr510.rst0000644000175100001770000000023414623331163021523 0ustar00runnerdocker####################### SR510 Lock-in Amplifier ####################### .. autoclass:: pymeasure.instruments.srs.SR510 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/srs/sr570.rst0000644000175100001770000000023414623331163021531 0ustar00runnerdocker####################### SR570 Lock-in Amplifier ####################### .. autoclass:: pymeasure.instruments.srs.SR570 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/srs/sr830.rst0000644000175100001770000000023414623331163021530 0ustar00runnerdocker####################### SR830 Lock-in Amplifier ####################### .. autoclass:: pymeasure.instruments.srs.SR830 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/srs/sr860.rst0000644000175100001770000000023414623331163021533 0ustar00runnerdocker####################### SR860 Lock-in Amplifier ####################### .. autoclass:: pymeasure.instruments.srs.SR860 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.361605 pymeasure-0.14.0/docs/api/instruments/tcpowerconversion/0000755000175100001770000000000014623331176023106 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/tcpowerconversion/index.rst0000644000175100001770000000062114623331163024742 0ustar00runnerdocker.. module:: pymeasure.instruments.tcpowerconversion #################### T&C Power Conversion #################### This section contains specific documentation on the instruments from T&C Power Conversion that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 tccxn ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/tcpowerconversion/tccxn.rst0000644000175100001770000000055614623331163024761 0ustar00runnerdocker################################################### T&C Power Conversion AG Series Plasma Generator CXN ################################################### .. autoclass:: pymeasure.instruments.tcpowerconversion.CXN :members: :show-inheritance: .. autoclass:: pymeasure.instruments.tcpowerconversion.tccxn.PresetChannel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.361605 pymeasure-0.14.0/docs/api/instruments/tdk/0000755000175100001770000000000014623331176020077 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/tdk/index.rst0000644000175100001770000000055414623331163021740 0ustar00runnerdocker.. module:: pymeasure.instruments.tdk ########## TDK Lambda ########## This section contains specific documentation on the TDK Lambda instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 tdk_gen40_38 tdk_gen80_65 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/tdk/tdk_gen40_38.rst0000644000175100001770000000040714623331163022717 0ustar00runnerdocker######################################## TDK Lambda Genesys 40-38 DC power supply ######################################## .. autoclass:: pymeasure.instruments.tdk.tdk_gen40_38.TDK_Gen40_38 :members: :show-inheritance: :inherited-members: CommonBase ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/tdk/tdk_gen80_65.rst0000644000175100001770000000040614623331163022722 0ustar00runnerdocker######################################## TDK Lambda Genesys 80-65 DC power supply ######################################## .. autoclass:: pymeasure.instruments.tdk.tdk_gen80_65.TDK_Gen80_65 :members: :show-inheritance: :inherited-members: CommonBase././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.361605 pymeasure-0.14.0/docs/api/instruments/tektronix/0000755000175100001770000000000014623331176021344 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/tektronix/afg3152c.rst0000644000175100001770000000032014623331163023300 0ustar00runnerdocker##################################### AFG3152C Arbitrary function generator ##################################### .. autoclass:: pymeasure.instruments.tektronix.AFG3152C :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/tektronix/index.rst0000644000175100001770000000054414623331163023204 0ustar00runnerdocker.. 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 afg3152c././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/tektronix/tds2000.rst0000644000175100001770000000023314623331163023164 0ustar00runnerdocker#################### TDS2000 Oscilloscope #################### .. autoclass:: pymeasure.instruments.tektronix.TDS2000 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.361605 pymeasure-0.14.0/docs/api/instruments/teledyne/0000755000175100001770000000000014623331176021126 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/teledyne/index.rst0000644000175100001770000000166314623331163022771 0ustar00runnerdocker.. module:: pymeasure.instruments.teledyne ######### Teledyne ######### This section contains specific documentation on the Teledyne instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. If the instrument you are looking for is not here, also check :doc:`LeCroy<../lecroy/index>` for older instruments. .. toctree:: :maxdepth: 1 teledyneT3AFG There are shared base classes for Teledyne oscilloscopes. These base classes already work directly for some devices, the following are confirmed: * :class:`~pymeasure.instruments.teledyne.TeledyneMAUI` * Teledyne LeCroy HDO6xxx series (e.g. HDO6054B) If your device is not listed, the base class might already work well enough anyway. If adding a new device, these base classes should limit the amount of new code necessary. .. toctree:: :maxdepth: 2 teledyne_bases ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/teledyne/teledyneT3AFG.rst0000644000175100001770000000060014623331163024206 0ustar00runnerdocker############################################## Teledyne T3AFG Arbitrary Waveform Generator ############################################## .. autoclass:: pymeasure.instruments.teledyne.TeledyneT3AFG :members: :show-inheritance: :inherited-members: CommonBase .. autoclass:: pymeasure.instruments.teledyne.teledyneT3AFG.SignalChannel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/teledyne/teledyne_bases.rst0000644000175100001770000000133214623331163024641 0ustar00runnerdocker################################## Teledyne Oscilloscope base classes ################################## Teledyne Oscilloscope ##################### .. autoclass:: pymeasure.instruments.teledyne.TeledyneOscilloscope :members: :show-inheritance: Teledyne Channel ################ .. autoclass:: pymeasure.instruments.teledyne.teledyne_oscilloscope.TeledyneOscilloscopeChannel :members: :show-inheritance: Teledyne MAUI Oscilloscope ########################## .. autoclass:: pymeasure.instruments.teledyne.TeledyneMAUI :members: :show-inheritance: Teledyne MAUI Channel ##################### .. autoclass:: pymeasure.instruments.teledyne.teledyneMAUI.TeledyneMAUIChannel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.361605 pymeasure-0.14.0/docs/api/instruments/temptronic/0000755000175100001770000000000014623331176021501 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/temptronic/index.rst0000644000175100001770000000064514623331163023343 0ustar00runnerdocker.. module:: pymeasure.instruments.temptronic ########## Temptronic ########## This section contains specific documentation on the temptronic instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 temptronic_base temptronic_ats525 temptronic_ats545 temptronic_eco560 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/temptronic/temptronic_ats525.rst0000644000175100001770000000027214623331163025517 0ustar00runnerdocker############################## Temptronic ATS525 Thermostream ############################## .. autoclass:: pymeasure.instruments.temptronic.ATS525 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/temptronic/temptronic_ats545.rst0000644000175100001770000000027214623331163025521 0ustar00runnerdocker############################## Temptronic ATS545 Thermostream ############################## .. autoclass:: pymeasure.instruments.temptronic.ATS545 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/temptronic/temptronic_base.rst0000644000175100001770000000050114623331163025401 0ustar00runnerdocker##################### Temptronic Base Class ##################### .. autoclass:: pymeasure.instruments.temptronic.ATSBase :members: :show-inheritance: .. autoclass:: pymeasure.instruments.temptronic.temptronic_base.TemperatureStatusCode .. autoclass:: pymeasure.instruments.temptronic.temptronic_base.ErrorCode ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/temptronic/temptronic_eco560.rst0000644000175100001770000000027214623331163025475 0ustar00runnerdocker############################## Temptronic ECO560 Thermostream ############################## .. autoclass:: pymeasure.instruments.temptronic.ECO560 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.361605 pymeasure-0.14.0/docs/api/instruments/texio/0000755000175100001770000000000014623331176020445 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/texio/index.rst0000644000175100001770000000052214623331163022301 0ustar00runnerdocker.. module:: pymeasure.instruments.texio ######## TEXIO ######## This section contains specific documentation on the TEXIO instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 texioPSW360L30 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/texio/texioPSW360L30.rst0000644000175100001770000000033514623331163023426 0ustar00runnerdocker############################# TEXIO PSW-360L30 Power Supply ############################# .. autoclass:: pymeasure.instruments.texio.TexioPSW360L30 :members: :show-inheritance: :inherited-members: CommonBase ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.361605 pymeasure-0.14.0/docs/api/instruments/thermotron/0000755000175100001770000000000014623331176021516 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/thermotron/index.rst0000644000175100001770000000054414623331163023356 0ustar00runnerdocker.. module:: pymeasure.instruments.thermotron ########## Thermotron ########## This section contains specific documentation on the Thermotron instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 thermotron3800././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/thermotron/thermotron3800.rst0000644000175100001770000000024414623331163024760 0ustar00runnerdocker#################### Thermotron 3800 Oven #################### .. autoclass:: pymeasure.instruments.thermotron.Thermotron3800 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3656049 pymeasure-0.14.0/docs/api/instruments/thorlabs/0000755000175100001770000000000014623331176021133 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/thorlabs/index.rst0000644000175100001770000000056014623331163022771 0ustar00runnerdocker.. 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/thorlabs/thorlabspm100usb.rst0000644000175100001770000000027314623331163024771 0ustar00runnerdocker############################ Thorlabs PM100USB Powermeter ############################ .. autoclass:: pymeasure.instruments.thorlabs.ThorlabsPM100USB :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/thorlabs/thorlabspro8000.rst0000644000175100001770000000033114623331163024525 0ustar00runnerdocker###################################### Thorlabs Pro 8000 modular laser driver ###################################### .. autoclass:: pymeasure.instruments.thorlabs.ThorlabsPro8000 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3656049 pymeasure-0.14.0/docs/api/instruments/thyracont/0000755000175100001770000000000014623331176021330 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/thyracont/index.rst0000644000175100001770000000055614623331163023173 0ustar00runnerdocker.. module:: pymeasure.instruments.thyracont ######### Thyracont ######### This section contains specific documentation on the Thyracont instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 smartline_v1 smartline_v2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/thyracont/smartline_v1.rst0000644000175100001770000000031614623331163024462 0ustar00runnerdocker############################### Smartline V1 Transmitter Series ############################### .. autoclass:: pymeasure.instruments.thyracont.smartline_v1.SmartlineV1 :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/thyracont/smartline_v2.rst0000644000175100001770000000063214623331163024464 0ustar00runnerdocker############################### Smartline V2 Transmitter Series ############################### .. autoclass:: pymeasure.instruments.thyracont.smartline_v2.SmartlineV2 :members: :show-inheritance: .. autoclass:: pymeasure.instruments.thyracont.smartline_v2.VSH :members: :show-inheritance: .. autoclass:: pymeasure.instruments.thyracont.smartline_v2.VSR :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3656049 pymeasure-0.14.0/docs/api/instruments/toptica/0000755000175100001770000000000014623331176020760 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/toptica/ibeamsmart.rst0000644000175100001770000000046514623331163023637 0ustar00runnerdocker############################### Toptica IBeam Smart Laser diode ############################### .. autoclass:: pymeasure.instruments.toptica.ibeamsmart.IBeamSmart :members: :show-inheritance: .. autoclass:: pymeasure.instruments.toptica.ibeamsmart.DriverChannel :members: :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/toptica/index.rst0000644000175100001770000000053414623331163022617 0ustar00runnerdocker.. module:: pymeasure.instruments.toptica ####### Toptica ####### This section contains specific documentation on the Toptica Photonics instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 ibeamsmart ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/validators.rst0000644000175100001770000000100214623331163022204 0ustar00runnerdocker.. module:: pymeasure.instruments.validators ################### Validator functions ################### Validators are used in conjunction with the :func:`Instrument.control ` or :func:`Instrument.setting ` functions to allow properties with complex restrictions for valid values. They are described in more detail in the :ref:`validators` section. .. automodule:: pymeasure.instruments.validators :members: :noindex: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3656049 pymeasure-0.14.0/docs/api/instruments/velleman/0000755000175100001770000000000014623331176021120 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/velleman/index.rst0000644000175100001770000000052214623331163022754 0ustar00runnerdocker.. module:: pymeasure.instruments.velleman ######## Velleman ######## This section contains specific documentation on the Velleman instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument `. .. toctree:: :maxdepth: 2 k8090 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/velleman/k8090.rst0000644000175100001770000000045514623331163022425 0ustar00runnerdocker#################################### Velleman K8090 8-channel relay board #################################### .. autoclass:: pymeasure.instruments.velleman.VellemanK8090 :members: :show-inheritance: .. autoclass:: pymeasure.instruments.velleman.VellemanK8090Switches :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3656049 pymeasure-0.14.0/docs/api/instruments/yokogawa/0000755000175100001770000000000014623331176021136 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/yokogawa/aq6370series.rst0000644000175100001770000000160014623331163024015 0ustar00runnerdocker#################################################### Yokogawa AQ6370 Series of Optical Spectrum Analyzers #################################################### .. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6370Series :members: :show-inheritance: .. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6370D :members: :show-inheritance: .. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6370C :members: :show-inheritance: .. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6373 :members: :show-inheritance: .. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6373B :members: :show-inheritance: .. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6375 :members: :show-inheritance: .. autoclass:: pymeasure.instruments.yokogawa.aq6370series.AQ6375B :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/yokogawa/index.rst0000644000175100001770000000057614623331163023003 0ustar00runnerdocker.. 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: 3 yokogawa7651 yokogawags200 aq6370series ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/yokogawa/yokogawa7651.rst0000644000175100001770000000030614623331163024027 0ustar00runnerdocker################################# Yokogawa 7651 Programmable Supply ################################# .. autoclass:: pymeasure.instruments.yokogawa.Yokogawa7651 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/api/instruments/yokogawa/yokogawags200.rst0000644000175100001770000000024314623331163024260 0ustar00runnerdocker##################### Yokogawa GS200 Source ##################### .. autoclass:: pymeasure.instruments.yokogawa.YokogawaGS200 :members: :show-inheritance:././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/conf.py0000644000175100001770000002400114623331163015441 0ustar00runnerdocker# # 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 from pymeasure import __version__ from pymeasure.instruments.common_base import CommonBase from pymeasure.instruments.instrument import Instrument # 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 = 'PyMeasure' copyright = '2013-2022, 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. # release = __version__ version = '.'.join(release.split('.')[:3]) # 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', 'PyMeasure Documentation', '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', 'PyMeasure Documentation', ['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', 'PyMeasure Documentation', '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 = ['zmq', 'cloudpickle', 'vxi11', 'pyvirtualbench'] def setup(app): app.connect('autodoc-process-docstring', gen_channel_docs) def get_class_name(cls): cls_str = str(cls) first_idx = cls_str.index("'") second_idx = cls_str[first_idx + 1:].index("'") return cls_str[first_idx + 1:first_idx + second_idx + 1] def gen_channel_docs(app, what, name, obj, options, lines): """ Generate channel documentation for instruments with channels """ if hasattr(obj, '__bases__') and issubclass(obj, Instrument): for attr, channel_class in obj.get_channels(obj): if isinstance(channel_class, CommonBase.ChannelCreator): channel_name = get_class_name(channel_class.pairs[0][0]) lines += ['.. py:attribute:: ' + attr, '', ] lines += [' :channel: :class:`~' + channel_name + '`', ''] elif isinstance(channel_class, CommonBase.MultiChannelCreator): prefix = channel_class.kwargs['prefix'] # Generate list of channels from prefix and channel pairs channels = ['``' + prefix + str(id) + '``: :class:`~' + get_class_name(cls) + '`' for cls, id in channel_class.pairs] lines += ['.. py:attribute:: ' + attr, '', ] lines += [' :channels: ' + ", ".join(channels), ''] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3656049 pymeasure-0.14.0/docs/dev/0000755000175100001770000000000014623331176014727 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3656049 pymeasure-0.14.0/docs/dev/adding_instruments/0000755000175100001770000000000014623331176020630 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/dev/adding_instruments/advanced_communication.rst0000644000175100001770000002007414623331163026053 0ustar00runnerdocker.. _advanced_communication_protocols: Advanced communication protocols ================================ Some devices require a more advanced communication protocol, e.g. due to checksums or device addresses. In most cases, it is sufficient to subclass :meth:`Instrument.write ` and :meth:`Instrument.read `. .. testsetup:: # Behind the scene, replace Instrument with FakeInstrument to enable # doctesting simple usage cases (default doctest group) from pymeasure.instruments.fakes import FakeInstrument as Instrument .. testsetup:: with-protocol-tests # If we want to run protocol tests on doctest code, we need to use a # separate doctest "group" and a different set of imports. # See https://www.sphinx-doc.org/en/master/usage/extensions/doctest.html from pymeasure.instruments import Instrument, Channel from pymeasure.test import expected_protocol Instrument's inner workings *************************** In order to adjust an instrument for more complicated protocols, it is key to understand the different parts. The :class:`~pymeasure.adapters.Adapter` exposes :meth:`~pymeasure.adapters.Adapter.write` and :meth:`~pymeasure.adapters.Adapter.read` for strings, :meth:`~pymeasure.adapters.Adapter.write_bytes` and :meth:`~pymeasure.adapters.Adapter.read_bytes` for bytes messages. These are the most basic methods, which log all the traffic going through them. For the actual communication, they call private methods of the Adapter in use, e.g. :meth:`VISAAdapter._read `. For binary data, like waveforms, the adapter provides also :meth:`~pymeasure.adapters.Adapter.write_binary_values` and :meth:`~pymeasure.adapters.Adapter.read_binary_values`, which use the aforementioned methods. You do not need to call all these methods directly, instead, you should use the methods of :class:`~pymeasure.instruments.Instrument` with the same name. They call the Adapter for you and keep the code tidy. Now to :class:`~pymeasure.instruments.Instrument`. The most important methods are :meth:`~pymeasure.instruments.Instrument.write` and :meth:`~pymeasure.instruments.Instrument.read`, as they are the most basic building blocks for the communication. The pymeasure properties (:meth:`Instrument.control ` and its derivatives :meth:`Instrument.measurement ` and :meth:`Instrument.setting `) and probably most of your methods and properties will call them. In any instrument, :meth:`write` should write a general string command to the device in such a way, that it understands it. Similarly, :meth:`read` should return a string in a general fashion in order to process it further. The getter of :meth:`Instrument.control ` does not call them directly, but via a chain of methods. It calls :meth:`~pymeasure.instruments.Instrument.values` which in turn calls :meth:`~pymeasure.instruments.Instrument.ask` and processes the returned string into understandable values. :meth:`~pymeasure.instruments.Instrument.ask` sends the readout command via :meth:`write`, waits some time if necessary via :meth:`wait_for`, and reads the device response via :meth:`read`. Similarly, :meth:`Instrument.binary_values ` sends a command via :meth:`write`, waits with :meth:`wait_till_read`, but reads the response via :meth:`Adapter.read_binary_values `. Adding a device address and adding delay **************************************** Let's look at a simple example for a device, which requires its address as the first three characters and returns the same style. This is straightforward, as :meth:`write` just prepends the device address to the command, and :meth:`read` has to strip it again doing some error checking. Similarly, a checksum could be added. Additionally, the device needs some time after it received a command, before it responds, therefore :meth:`wait_for` waits always a certain time span. .. testcode:: with-protocol-tests class ExtremeCommunication(Instrument): """Control the ExtremeCommunication instrument. :param address: The device address for the communication. :param query_delay: Wait time after writing and before reading in seconds. """ def __init__(self, adapter, name="ExtremeCommunication", address=0, query_delay=0.1): super().__init__(adapter, name) self.address = f"{address:03}" self.query_delay = query_delay def write(self, command): """Add the device address in front of every command before sending it.""" super().write(self.address + command) def wait_for(self, query_delay=0): """Wait for some time. :param query_delay: override the global query_delay. """ super().wait_for(query_delay or self.query_delay) def read(self): """Read from the device and check the response. Assert that the response starts with the device address. """ got = super().read() if got.startswith(self.address): return got[3:] else: raise ConnectionError(f"Expected message address '{self.address}', but read '{got[3:]}' for wrong address '{got[:3]}'.") voltage = Instrument.measurement( ":VOLT:?", """Measure the voltage in Volts.""") .. testcode:: with-protocol-tests :hide: with expected_protocol(ExtremeCommunication, [("012:VOLT:?", "01215.5")], address=12 ) as inst: assert inst.voltage == 15.5 If the device is initialized with :code:`address=12`, a request for the voltage would send :code:`"012:VOLT:?"` to the device and expect a response beginning with :code:`"012"`. Bytes communication ******************* Some devices do not expect ASCII strings but raw bytes. In those cases, you can call the :meth:`write_bytes` and :meth:`read_bytes` in your :meth:`write` and :meth:`read` methods. The following example shows an instrument, which has registers to be written and read via bytes sent. .. testcode:: with-protocol-tests class ExtremeBytes(Instrument): """Control the ExtremeBytes instrument with byte-based communication.""" def __init__(self, adapter, name="ExtremeBytes"): super().__init__(adapter, name) def write(self, command): """Write to the device according to the comma separated command. :param command: R or W for read or write, hexadecimal address, and data. """ function, address, data = command.split(",") b = [0x03] if function == "R" else [0x10] b.extend(int(address, 16).to_bytes(2, byteorder="big")) b.extend(int(data).to_bytes(length=8, byteorder="big", signed=True)) self.write_bytes(bytes(b)) def read(self): """Read the response and return the data as a string, if applicable.""" response = self.read_bytes(2) # return type and payload if response[0] == 0x00: raise ConnectionError(f"Device error of type {response[1]} occurred.") if response[0] == 0x03: # read that many bytes and return them as an integer data = self.read_bytes(response[1]) return str(int.from_bytes(data, byteorder="big", signed=True)) if response[0] == 0x10 and response[1] != 0x00: raise ConnectionError(f"Writing to the device failed with error {response[1]}") voltage = Instrument.control( "R,0x106,1", "W,0x106,%i", """Control the output voltage in mV.""", ) .. testcode:: with-protocol-tests :hide: with expected_protocol(ExtremeBytes, [(b"\x03\x01\x06\x00\x00\x00\x00\x00\x00\x00\x01", b"\x03\x01\x0f")]) as inst: assert inst.voltage == 15 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/dev/adding_instruments/channels.rst0000644000175100001770000003144514623331163023160 0ustar00runnerdocker.. _channels: Instruments with channels ========================= .. testsetup:: # Behind the scene, replace Instrument with FakeInstrument to enable # doctesting simple usage cases (default doctest group) from pymeasure.instruments.fakes import FakeInstrument as Instrument .. testsetup:: with-protocol-tests # If we want to run protocol tests on doctest code, we need to use a # separate doctest "group" and a different set of imports. # See https://www.sphinx-doc.org/en/master/usage/extensions/doctest.html from pymeasure.instruments import Instrument, Channel from pymeasure.test import expected_protocol Some instruments, like oscilloscopes and voltage sources, have channels whose commands differ only in the channel name. For this case, we have :class:`~pymeasure.instruments.Channel`, which is similar to :class:`~pymeasure.instruments.Instrument` and its property factories, but does expect an :class:`~pymeasure.instruments.Instrument` instance (i.e., a parent instrument) instead of an :class:`~pymeasure.adapters.Adapter` as parameter. All the channel communication is routed through the instrument's methods (`write`, `read`, etc.). However, :meth:`Channel.insert_id ` uses ``str.format`` to insert the channel's id at any occurrence of the class attribute :attr:`Channel.placeholder`, which defaults to :code:`"ch"`, in the written commands. For example :code:`"Ch{ch}:VOLT?"` will be sent as :code:`"Ch3:VOLT?"` to the device, if the channel's id is "3". Please add any created channel classes to the documentation. In the instrument's documentation file, you may add .. code:: .. autoclass:: pymeasure.instruments.MANUFACTURER.INSTRUMENT.CHANNEL :members: :show-inheritance: `MANUFACTURER` is the folder name of the manufacturer and `INSTRUMENT` the file name of the instrument definition, which contains the `CHANNEL` class. You may link in the instrument's docstring to the channel with :code:`:class:`CHANNEL`` To simplify and standardize the creation of channels in an ``Instrument`` class, there are two classes that can be used. For instruments with fewer than 16 channels, :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator` should be used to explicitly declare each individual channel. For instruments with more than 16 channels, the :class:`~pymeasure.instruments.common_base.CommonBase.MultiChannelCreator` can create multiple channels in a single declaration. Adding a channel with :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator` ******************************************************************************************* For instruments with fewer than 16 channels the class :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator` should be used to assign each channel interface to a class attribute. :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator` constructor accepts two parameters, the channel class for this channel interface, and the instrument's channel id for the channel interface. In this example, we are defining a channel class and an instrument driver class. The ``VoltageChannel`` channel class will be used for controlling two channels in our ``ExtremeVoltage5000`` instrument. In the ``ExtremeVoltage5000`` class we declare two class attributes with :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator`, ``output_A`` and ``output_B``, which will become our channel interfaces. .. testcode:: with-protocol-tests class VoltageChannel(Channel): """A channel of the voltage source.""" voltage = Channel.control( "SOURce{ch}:VOLT?", "SOURce{ch}:VOLT %g", """Control the output voltage of this channel.""", ) class ExtremeVoltage5000(Instrument): """An instrument with channels.""" output_A = Instrument.ChannelCreator(VoltageChannel, "A") output_B = Instrument.ChannelCreator(VoltageChannel, "B") .. testcode:: with-protocol-tests :hide: with expected_protocol(ExtremeVoltage5000, [("SOURceA:VOLT 1.25", None), ("SOURceB:VOLT?", "4.56")], name="Instrument with Channels", ) as inst: inst.output_A.voltage = 1.25 assert inst.channels['B'].voltage == 4.56 At instrument class instantiation, the instrument class will create an instance of the channel class and assign it to the class attribute name. Additionally the channels will be collected in a dictionary, by default named :code:`channels`. We can access the channel interface through that class name: .. code-block:: python extreme_inst = ExtremeVoltage5000('COM3') # Set channel A voltage extreme_inst.output_A.voltage = 50 # Read channel B voltage chan_b_voltage = extreme_inst.output_B.voltage .. testcode:: with-protocol-tests :hide: with expected_protocol(ExtremeVoltage5000, [("SOURceA:VOLT 50", None), ("SOURceB:VOLT?", "4.56")], name="Instrument with Channels", ) as inst: inst.output_A.voltage = 50 assert inst.output_B.voltage == 4.56 Or we can access the channel interfaces through the :code:`channels` collection: .. code-block:: python # Set channel A voltage extreme_inst.channels['A'].voltage = 50 # Read channel B voltage chan_b_voltage = extreme_inst.channels['B'].voltage .. testcode:: with-protocol-tests :hide: with expected_protocol(ExtremeVoltage5000, [("SOURceA:VOLT 50", None), ("SOURceB:VOLT?", "4.56")], name="Instrument with Channels", ) as inst: inst.channels['A'].voltage = 50 assert inst.channels['B'].voltage == 4.56 Adding multiple channels with :class:`~pymeasure.instruments.common_base.CommonBase.MultiChannelCreator` ******************************************************************************************************** For instruments greater than 16 channels the class :class:`~pymeasure.instruments.common_base.CommonBase.MultiChannelCreator` can be used to easily generate a list of channels from one class attribute declaration. The :class:`~pymeasure.instruments.common_base.CommonBase.MultiChannelCreator` constructor accepts a single channel class or list of channel classes, and a list of corresponding channel ids. Instead of lists, you may also use tuples. If you give a single class and a list of ids, all channels will be of the same class. At instrument instantiation, the instrument will generate channel interfaces as class attribute names composing of the prefix (default :code:`"ch_"`) and channel id, e.g. the channel with id "A" will be added as attribute :code:`ch_A`. While :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator` creates a channel interface for each class attribute, :class:`~pymeasure.instruments.common_base.CommonBase.MultiChannelCreator` creates a channel collection for the assigned class attribute. It is recommended you use the class attribute name ``channels`` to keep the codebase homogenous. To modify our example, we will use :class:`~pymeasure.instruments.common_base.CommonBase.MultiChannelCreator` to generate 24 channels of the ``VoltageChannel`` class. .. testcode:: with-protocol-tests class VoltageChannel(Channel): """A channel of the voltage source.""" voltage = Channel.control( "SOURce{ch}:VOLT?", "SOURce{ch}:VOLT %g", """Control the output voltage of this channel.""", ) class MultiExtremeVoltage5000(Instrument): """An instrument with channels.""" channels = Instrument.MultiChannelCreator(VoltageChannel, list(range(1,25))) .. testcode:: with-protocol-tests :hide: with expected_protocol(MultiExtremeVoltage5000, [("SOURce5:VOLT 1.23", None), ("SOURce16:VOLT?", "4.56")], name="Instrument with Channels", ) as inst: inst.ch_5.voltage = 1.23 assert inst.channels[16].voltage == 4.56 We can now access the channel interfaces through the generated class attributes: .. code-block:: python extreme_inst = MultiExtremeVoltage5000('COM3') # Set channel 5 voltage extreme_inst.ch_5.voltage = 50 # Read channel 16 voltage chan_16_voltage = extreme_inst.ch_16.voltage .. testcode:: with-protocol-tests :hide: with expected_protocol(MultiExtremeVoltage5000, [("SOURce5:VOLT 50", None), ("SOURce16:VOLT?", "4.56")], name="Instrument with Channels", ) as inst: inst.ch_5.voltage = 50 assert inst.ch_16.voltage == 4.56 Because we use `channels` as the class attribute for ``MultiChannelCreator``, we can access the channel interfaces through the :code:`channels` collection: .. code-block:: python # Set channel 10 voltage extreme_inst.channels[10].voltage = 50 # Read channel 22 voltage chan_b_voltage = extreme_inst.channels[22].voltage .. testcode:: with-protocol-tests :hide: with expected_protocol(MultiExtremeVoltage5000, [("SOURce10:VOLT 50", None), ("SOURce22:VOLT?", "4.56")], name="Instrument with Channels", ) as inst: inst.channels[10].voltage = 50 assert inst.channels[22].voltage == 4.56 Advanced channel management *************************** Adding / removing channels -------------------------- In order to add or remove programmatically channels, use the parent's :meth:`~pymeasure.instruments.common_base.CommonBase.add_child`, :meth:`~pymeasure.instruments.common_base.CommonBase.remove_child` methods. Channels with fixed prefix -------------------------- If all channel communication is prefixed by a specific command, e.g. :code:`"SOURceA:"` for channel A, you can override the channel's :meth:`insert_id` method. That is especially useful, if you have only one channel of that type, e.g. because it defines one function of the instrument vs. another one. .. testcode:: with-protocol-tests class VoltageChannelPrefix(Channel): """A channel of a voltage source, every command has the same prefix.""" def insert_id(self, command): return f"SOURce{self.id}:{command}" voltage = Channel.control( "VOLT?", "VOLT %g", """Control the output voltage of this channel.""", ) .. testcode:: with-protocol-tests :hide: class InstrumentWithChannelsPrefix(Instrument): """An instrument with a channel, just for the test.""" ch_A = Instrument.ChannelCreator(VoltageChannelPrefix, "A") with expected_protocol(InstrumentWithChannelsPrefix, [("SOURceA:VOLT 1.23", None), ("SOURceA:VOLT?", "1.23")], name="Test", ) as inst: inst.ch_A.voltage = 1.23 assert inst.ch_A.voltage == 1.23 This channel class implements the same communication as the previous example, but implements the channel prefix in the :meth:`insert_id` method and not in the individual property (created by :meth:`control`). Collections of different channel types -------------------------------------- Some devices have different types of channels. In this case, you can specify a different ``collection`` and ``prefix`` parameter. .. testcode:: with-protocol-tests class PowerChannel(Channel): """A channel controlling the power.""" power = Channel.measurement( "POWER?", """Measure the currently consumed power.""") class MultiChannelTypeInstrument(Instrument): """An instrument with two different channel types.""" analog = Instrument.MultiChannelCreator( (VoltageChannel, VoltageChannelPrefix), ("A", "B"), prefix="an_") digital = Instrument.MultiChannelCreator(VoltageChannel, (0, 1, 2), prefix="di_") power = Instrument.ChannelCreator(PowerChannel) .. testcode:: with-protocol-tests :hide: with expected_protocol(MultiChannelTypeInstrument, [("SOURceB:VOLT 1.23", None), ("SOURce2:VOLT?", "4.56")], name="MultiChannelTypeInstrument", ) as inst: inst.an_B.voltage = 1.23 assert inst.di_2.voltage == 4.56 This instrument has two collections of channels and one single channel. The first collection in the dictionary :code:`analog` contains an instance of :class:`VoltageChannel` with the name :code:`an_A` and an instance of :class:`VoltageChannelPrefix` with the name :code:`an_B`. The second collection contains three channels of type :class:`VoltageChannel` with the names :code:`di_0, di_1, di_2` in the dictionary :code:`digital`. You can address the first channel of the second group either with :code:`inst.di_0` or with :code:`inst.digital[0]`. Finally, the instrument has a single channel with the name :code:`power`, as it does not have a prefix. If you have a single channel category, do not change the default parameters of :class:`~pymeasure.instruments.common_base.CommonBase.ChannelCreator` or :meth:`~pymeasure.instruments.common_base.CommonBase.add_child`, in order to keep the code base homogeneous. We expect the default behaviour to be sufficient for most use cases. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/dev/adding_instruments/index.rst0000644000175100001770000000222514623331163022466 0ustar00runnerdocker.. _adding-instruments: ################## 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 <../contribute>` for PyMeasure, which steps through the process of adding a new feature (like an instrument) to the development version of the source code. Pymeasure instruments communicate with the devices via transfer of bytes or ASCII characters encoded as bytes. For ease of use, we have :ref:`property creators ` to easily create python properties. Similarly, we have creators to easily implement :ref:`channels `. Finally, for a smoother implementation process and better maintenance, we have :ref:`tests `. The following sections will describe how to lay out your instrument code. .. toctree:: :maxdepth: 2 instrument properties channels advanced_communication tests solutions ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/dev/adding_instruments/instrument.rst0000644000175100001770000003672214623331163023600 0ustar00runnerdockerFile 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. Add test files ************** Test files (pytest) for each instrument are highly encouraged, as they help verify the code and implement changes. Testing new code parts with a test (Test Driven Development) is a good way for fast and good programming, as you catch errors early on. .. code-block:: none pymeasure/tests/instruments/extreme/ |--> test_extreme5000.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: .. testsetup:: # Behind the scene, replace Instrument with FakeInstrument to enable # doctesting simple usage cases (default doctest group) from pymeasure.instruments.fakes import FakeInstrument as Instrument .. testcode:: # # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 This is a minimal instrument definition: .. testcode:: class Extreme5000(Instrument): """Control the imaginary Extreme 5000 instrument.""" def __init__(self, adapter, name="Extreme 5000", **kwargs): super().__init__( adapter, name, **kwargs ) Make sure to include the PyMeasure license to each file, and add yourself as an author to the :code:`AUTHORS.txt` file. There is a certain order of elements in an instrument class that is useful to adhere to: * First, the initializer (the :code:`__init__()` method), this makes it faster to find when browsing the source code. * Then class attributes/variables, if you need them. * Then properties (pymeasure-specific or generic Python variants). This will be the bulk of the implementation. * Finally, any methods. Your instrument's user interface ================================ Your instrument will have a certain set of properties and methods that are available to a user and discoverable via the documentation or their editor's autocomplete function. In principle you are free to choose how you do this (with the exception of standard SCPI properties like :code:`id`). However, there are a couple of practices that have turned out to be useful to follow: * Naming things is important. Try to choose clear, expressive, unambiguous names for your instrument's elements. * If there are already similar instruments in the same "family" (like a power supply) in pymeasure, try to follow their lead where applicable. It's better if, e.g., all power supplies have a :code:`current_limit` instead of an assortment of :code:`current_max`, :code:`Ilim`, :code:`max_curr`, etc. * If there is already an instrument with a similar command set, check if you can inherit from that one and just tweak a couple of things. This massively reduces code duplication and maintenance effort. The section :ref:`instruments_with_similar_features` shows how to achieve that. * The bulk of your instrument's interface will probably be made up of properties for quantities to set and/or read out. Our custom properties (see :ref:`properties` ff. below) offer some convenience features and are therefore preferable, but plain Python properties are also fine. * "Actions", commands or verbs should typically be methods, not properties: :code:`recall()`, :code:`trigger_scan()`, :code:`prepare_resistance_measurement()`, etc. * This separation between properties and methods also naturally helps with observing the `"command-query separation" principle `__. * If your instrument has multiple identical channels, see :ref:`channels`. In principle, you are free to write any methods 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 commands instead of calling the adapter directly. If the communication requires changes to the commands sent/received, you can override these methods in your instrument, for further information see :ref:`advanced_communication_protocols`. In practice, we have developed a number of best practices for making instruments easy to write and maintain. The following sections detail these, which are highly encouraged to follow. .. _common_instrument_types: Common instrument types *********************** There are a number of categories that many instruments fit into. In the future, pymeasure should gain an abstraction layer based on that, see `this issue `__. Until that is ready, here are a couple of guidelines towards a more uniform API. Note that not all already available instruments follow these, but expect this to be harmonized in the future. Generic types mixins -------------------- The :doc:`generic_types <../../api/instruments/generic_types>` module contains mixin classes for common types. For example, if an instrument complies to SCPI standards, you can add :class:`~pymeasure.instruments.generic_types.SCPIMixin` to your instrument: .. testcode:: from pymeasure.instruments.generic_types import SCPIMixin class SomeSCPIInstrument(SCPIMixin, Instrument): """This instrument has properties and methods defined for all SCPI instruments""" This mixin adds default SCPI properties like :attr:`~pymeasure.instruments.generic_types.SCPIMixin.id`, :attr:`~pymeasure.instruments.generic_types.SCPIMixin.status` and default methods like :meth:`~pymeasure.instruments.generic_types.SCPIMixin.clear` and :meth:`~pymeasure.instruments.generic_types.SCPIMixin.reset` to :code:`SomeSCPIInstrument`. Frequent properties ------------------- If your instrument has an **output** that can be switched on and off, use a :ref:`boolean property ` called :code:`output_enabled`. Power supplies -------------- PSUs typically can measure the *actual* current and voltage, as well as have settings for the voltage level and the current limit. To keep naming clear and avoid confusion, implement the properties :code:`current`, :code:`voltage`, :code:`voltage_setpoint` and :code:`current_limit`, respectively. Managing status codes or other indicator values *********************************************** Often, an instrument features one or more collections of specific values that signal some status, an instrument mode or a number of possible configuration values. Typically, these are collected in mappings of some sort, as you want to provide a clear and understandable value to the user, while abstracting away the raw data, think :code:`ACQUISITION_MODE` instead of :code:`0x04`. The mappings normally are kept at module level (i.e. not defined within the instrument class), so that they are available when using the property factories. This is a small drawback of using Python class attributes. The easiest way to handle these mappings is a plain :code:`dict`. However, there is often a better way, the Python :code:`enum.Enum`. To cite the `Python documentation `__, An Enum is a set of symbolic names bound to unique values. They are similar to global variables, but they offer a more useful :code:`repr()`, grouping, type-safety, and a few other features. As our signal values are often integers, the most appropriate enum types are :code:`IntEnum` and :code:`IntFlag`. :code:`IntEnum` is the same as :code:`Enum`, but its members are also integers and can be used anywhere that an integer can be used (so their use for composing commands is transparent), but logic/code they appear in is much more legible. Note that starting from Python version 3.11, the printed format of the :code:`IntEnum` and :code:`IntFlag` has been changed to return numeric value; however, the symbolic name can be obtained by printing its :code:`repr` or the :code:`.name` property, or returning the value in a REPL. .. doctest:: >>> from enum import IntEnum >>> class InstrMode(IntEnum): ... WAITING = 0x00 ... HEATING = 0x01 ... COOLING = 0x05 ... >>> received_from_device = 0x01 >>> current_mode = InstrMode(received_from_device) >>> if current_mode == InstrMode.WAITING: ... print('Idle') ... else: ... current_mode ... print(repr(current_mode)) ... print(f'Mode value: {current_mode}') ... Mode value: 1 :code:`IntFlag` has the added benefit that it supports bitwise operators and combinations, and as such is a good fit for status bitmasks or error codes that can represent multiple values: .. doctest:: >>> from enum import IntFlag >>> class ErrorCode(IntFlag): ... TEMP_OUT_OF_RANGE = 8 ... TEMPSENSOR_FAILURE = 4 ... COOLER_FAILURE = 2 ... HEATER_FAILURE = 1 ... OK = 0 ... >>> received_from_device = 7 >>> ErrorCode(received_from_device) :code:`IntFlags` are used by many instruments for the purpose just demonstrated. The status property could look like this: .. testcode:: status = Instrument.measurement( "STB?", """Measure the status of the device as enum.""", get_process=lambda v: ErrorCode(v), ) .. _default_connection_settings: Defining default connection settings ==================================== When implementing instruments, it's sometimes necessary to define default connection settings. This might be because an instrument connection requires *specific non-default settings*, or because your instrument actually supports *multiple interfaces*. The :py:class:`~pymeasure.adapters.VISAAdapter` class offers a flexible way of dealing with connection settings fully within the initializer of your instrument. Single interface **************** The simplest version, suitable when the instrument connection needs default settings, just passes all keywords through to the ``Instrument`` initializer, which hands them over to :py:class:`~pymeasure.adapters.VISAAdapter` if ``adapter`` is a string or integer. .. code-block:: python def __init__(self, adapter, name="Extreme 5000", **kwargs): super().__init__( adapter, name, **kwargs ) If you want to set defaults that should be prominently visible to the user and may be overridden, place them in the signature. This is suitable when the instrument has one type of interface, or any defaults are valid for all interface types, see the documentation in :py:class:`~pymeasure.adapters.VISAAdapter` for details. .. code-block:: python def __init__(self, adapter, name="Extreme 5000", baud_rate=2400, **kwargs): super().__init__( adapter, name, baud_rate=baud_rate, **kwargs ) If you want to set defaults, but they don't need to be prominently exposed for replacement, use this pattern, which sets the value only when there is no entry in ``kwargs``, yet. .. code-block:: python def __init__(self, adapter, name="Extreme 5000", **kwargs): kwargs.setdefault('timeout', 1500) super().__init__( adapter, name, **kwargs ) Multiple interfaces ******************* Now, if you have instruments with multiple interfaces (e.g. serial, TCPI/IP, USB), things get interesting. You might have settings common to all interfaces (like ``timeout``), but also settings that are only valid for one interface type, but not others. The trick is to add keyword arguments that name the interface type, like ``asrl`` or ``gpib``, below (see `here `__ for the full list). These then contain a *dictionary* with the settings specific to the respective interface: .. code-block:: python def __init__(self, adapter, name="Extreme 5000", baud_rate=2400, **kwargs): kwargs.setdefault('timeout', 1500) super().__init__( adapter, name, gpib=dict(enable_repeat_addressing=False, read_termination='\r'), asrl={'baud_rate': baud_rate, 'read_termination': '\r\n'}, **kwargs ) When the instrument instance is created, the interface-specific settings for the actual interface being used get merged with ``**kwargs`` before passing them on to PyVISA, the rest is discarded. This way, we always pass on a valid set of arguments. In addition, any entries in ``**kwargs**`` take precedence, so if they need to, it is *still* possible for users to override any defaults you set in the instrument definition. For many instruments, the simple way presented first is enough, but in case you have a more complex arrangement to implement, see whether :ref:`advanced_communication_protocols` fits your bill. If, for some exotic reason, you need a special connection type, which you cannot model with PyVISA, you can write your own Adapter. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/dev/adding_instruments/properties.rst0000644000175100001770000005664014623331163023565 0ustar00runnerdocker.. _properties: Writing properties ================== In PyMeasure, `Python properties`_ are the preferred method for dealing with variables that are read or set. .. testsetup:: # Behind the scene, replace Instrument with FakeInstrument to enable # doctesting simple usage cases (default doctest group) from pymeasure.instruments.fakes import FakeInstrument as Instrument class Extreme5000(Instrument): def __init__(self, adapter, name="Test", **kwargs): super().__init__(adapter, name, **kwargs) The property factories ********************** PyMeasure comes with three central convenience factory functions for making properties for classes: :func:`CommonBase.control `, :func:`CommonBase.measurement `, and :func:`CommonBase.setting `. You can call them, however, as :code:`Instrument.control`, :code:`Instrument.measurement`, and :code:`Instrument.setting`. The :func:`Instrument.measurement ` function returns a property that can only read values from an instrument. For example, if our "Extreme 5000" has the :code:`*IDN?` command, we can write the following property to be added after 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.cell_temp = Instrument.measurement( ":TEMP?", """Measure the temperature of the reaction cell.""", ) .. testcode:: :hide: # We have to fake this silently because the FakeInstrument cannot do # a measurement property, it only mirrors values that you sent first. Extreme5000.cell_temp = 127.2 You will notice that a documentation string is required, see :ref:`docstrings` for details. When we use this property we will get the temperature of the reaction cell. .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.cell_temp # Sends ":TEMP?" to the device 127.2 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", """Control the voltage in Volts (float).""" ) You will notice that we use the `Python string format`_ :code:`%g` to format passed-through values as 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 send the appropriate command, and then to request the current voltage: .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.voltage = 0.1 # Sends ":VOLT 0.1" to set the voltage to 100 mV >>> extreme.voltage # Sends ":VOLT?" to query for the current value 0.1 Finally, the :func:`Instrument.setting ` function can only set, but not read values. Using the :func:`Instrument.control `, :func:`Instrument.measurement `, and :func:`Instrument.control ` functions, you can create a number of properties for basic measurements and controls. The next sections detail additional features of the property factories. These allow you to write properties that cover specific ranges, or that 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. .. _validators: Restricting values with validators ********************************** Many GPIB/SCPI 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", """Control the voltage in Volts (float strictly from -1 to 1).""", 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", """Control the voltage in Volts (float from -1 to 1). Invalid voltages are truncated. """, 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", """Control the voltage range in Volts (float in 10e-3, 100e-3, 1).""", 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 Mapping values ************** 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", """Control the voltage range in Volts (float in 10 mV, 100 mV and 1 V). """, 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", """Control the voltage range in Volts (float in 10 mV, 100 mV and 1 V). """, 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", """Control the measurement channel (string strictly in 'X', 'Y', '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. .. _boolean-properties: Boolean properties ****************** The idea of using maps can be leveraged to implement properties where the user-facing values are booleans, so you can interact in a pythonic way using :code:`True` and :code:`False`: .. testcode:: Extreme5000.output_enabled = Instrument.control( "OUTP?", "OUTP %d", """Control the instrument output is enabled (boolean).""", validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, # the dict values could also be "on" and "off", etc. depending on the device ) .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.output_enabled = True >>> extreme.read() '1' >>> extreme.output_enabled = False >>> extreme.output_enabled False >>> # Invalid input raises an exception >>> extreme.output_enabled = 34 Traceback (most recent call last): ... ValueError: Value of 34 is not in the discrete set {True: 1, False: 0} Good names for boolean properties are chosen such that they could also be a yes/no question: "Is the output enabled?" -> :code:`output_enabled`, :code:`display_active`, etc. 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", """Set the measurement current in A (float strictly from 0 to 10).""", 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", """Control the measurement current in A (float strictly from 0 to 10).""", 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 Another use-case of `set-process`, `get-process` is conversion from/to a :code:`pint.Quantity`. Modifying above example to set or return a quantity, we get: .. testcode:: from pymeasure.units import ureg Extreme5000.current = Instrument.control( ":CURR?", ":CURR %g", """Control the measurement current (float).""", set_process=lambda v: v.m_as(ureg.mA), # send the value as mA to the device get_process=lambda v: ureg.Quantity(v, ureg.mA), # convert to quantity ) .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.current = 3.1 * ureg.A >>> extreme.current.m_as(ureg.A) 3.1 .. note:: This is, how quantities can be used in pymeasure instruments right now. `Issue 666 `_ develops a more convenient implementation of quantities in the property factories. `get_process` can also be used to perform string processing. Let's say your instrument returns a value with its unit (e.g. :code:`1.23 nF`), which has to be removed. This could be achieved by the following code: .. testcode:: Extreme5000.capacity = Instrument.measurement( ":CAP?", """Measure the capacity in nF (float).""", 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?", """Measure the capacity in nF (float).""", preprocess_reply=lambda v: v.replace('nF', '') # notice how we don't need to cast to float anymore ) Checking the instrument for errors ********************************** If you need to separately ask your instrument about its error state after getting/setting, use the parameters :code:`check_get_errors` and :code:`check_set_errors` of :meth:`~pymeasure.instruments.common_base.CommonBase.control`, respectively. If those are enabled, the methods :meth:`~pymeasure.instruments.Instrument.check_get_errors` and :meth:`~pymeasure.instruments.Instrument.check_set_errors`, respectively, will be called be called after device communication has concluded. In the default implementation, for simplicity both methods call :meth:`~pymeasure.instruments.Instrument.check_errors`. To read the automatic response of instruments that respond to every set command with an acknowledgment or error, override :meth:`~pymeasure.instruments.Instrument.check_set_errors` as needed. Using multiple values ********************* Seldomly, you might need to send/receive multiple values in one command. The :func:`Instrument.control ` function can be used with multiple values at one time, passed as a tuple. Say, we may set voltages and frequencies in our "Extreme 5000", and the commands for this are :code:`:VOLTFREQ?` and :code:`:VOLTFREQ ,`, we could use the following property: .. testcode:: Extreme5000.combination = Instrument.control( ":VOLTFREQ?", ":VOLTFREQ %g,%g", """Simultaneously control the voltage in Volts and the frequency in Hertz (both float). This property is set by a tuple. """ ) In use, we could set the voltage to 200 mV, and the Frequency to 931 Hz, and read both values immediately afterwards. .. doctest:: >>> extreme = Extreme5000("GPIB::1") >>> extreme.combination = (0.2, 931) # Executes ":VOLTFREQ 0.2,931" >>> extreme.combination # Reads ":VOLTFREQ?" [0.2, 931.0] This interface is not too convenient, but luckily not often needed. Dynamic properties ****************** As described in previous sections, Python properties are a very powerful tool to easily code an instrument's programming interface. One very interesting feature provided in PyMeasure is the ability to adjust properties' behaviour in subclasses or dynamically in instances. This feature allows accommodating some interesting use cases with a very compact syntax. Dynamic features of a property are enabled by setting its :code:`dynamic` parameter to :code:`True`. Afterwards, creating specifically-named attributes (either in class definitions or on instances) allows modifying the parameters used at the time of property definition. You need to define an attribute whose name is `_` and assign to it the desired value. Pay attention *not* to inadvertently define other class attribute or instance attribute names matching this pattern, since they could unintentionally modify the property behaviour. .. note:: To clearly distinguish these special attributes from normal class/instance attributes, they can only be set, not read. The mechanism works for all the parameters in properties, except :code:`dynamic` and :code:`docs` -- see :func:`Instrument.control `, :func:`Instrument.measurement `, :func:`Instrument.setting `. Dynamic validity range ---------------------- Let's assume we have an instrument with a command that accepts a different valid range of values depending on its current state. The code below shows how this can be accomplished with dynamic properties. .. testcode:: Extreme5000.voltage = Instrument.control( ":VOLT?", ":VOLT %g", """Control the voltage in Volts (float).""", validator=strict_range, values=[-1, 1], dynamic = True, ) def set_bipolar_mode(self, enabled = True): """Safely switch between bipolar/unipolar mode.""" # some code to switch off the output first # ... if enabled: self.mode = "BIPOLAR" # set valid range of "voltage" property self.voltage_values = [-1, 1] else: self.mode = "UNIPOLAR" # note the "propertyname_parametername" form of the attribute self.voltage_values = [0, 1] Now our voltage property has a dynamic validity range, either [-1, 1] or [0, 1]. A side effect of this is that the property's docstring should be less specific, to avoid it containing dynamically changed information (like the admissible value range). In this example, the property name was :code:`voltage` and the parameter to adjust was :code:`values`, so we used :code:`self.voltage_values` to set our desired values. .. _instruments_with_similar_features: Instruments with similar features ================================= When instruments have a similar set of features, it makes sense to use inheritance to obtain most of the functionality from a parent instrument class, instead of copy-pasting code. .. note:: Don't forget to update the instrument's :code:`name` attribute accordingly, by either supplying an appropriate argument (if available) during the :code:`super().__init__()` call, or by setting it anew below that call. In some cases, one only needs to add additional properties and methods. In other cases, some of the already present properties/methods need to be completely replaced by defining them again in the derived class. Often, however, only some details need to be changed. This can be dealt with efficiently using dynamic properties. Instrument family with different parameter values ************************************************* A common case is to have a family of similar instruments with some parameter range different for each family member. In this case you would update the specific class parameter range without rewriting the entire property: .. testcode:: :hide: # Behind the scene, load the real Instrument from pymeasure.instruments import Instrument from pymeasure.test import expected_protocol .. testcode:: class FictionalInstrumentFamily(Instrument): frequency = Instrument.setting( "FREQ %g", """Set the frequency (float).""", validator=strict_range, values=[0, 1e9], dynamic=True, # ... other possible parameters follow ) # # ... complete class implementation here # class FictionalInstrument_1GHz(FictionalInstrumentFamily): pass class FictionalInstrument_3GHz(FictionalInstrumentFamily): frequency_values = [0, 3e9] class FictionalInstrument_9GHz(FictionalInstrumentFamily): frequency_values = [0, 9e9] .. testcode:: :hide: with expected_protocol(FictionalInstrument_9GHz, [("FREQ 5e+09", None)], name="Test") as inst: inst.frequency = 5e9 Notice how easily you can derive the different family members from a common class, and the fact that the attribute is now defined at class level and not at instance level. Instruments with similar command syntax *************************************** Another use case involves maintaining compatibility between instruments with commands having different syntax, like in the following example. .. code-block:: python class MultimeterA(Instrument): voltage = Instrument.measurement(get_command="VOLT?",...) # ...full class definition code here class MultimeterB(MultimeterA): # Same as brand A multimeter, but the command to read voltage # is slightly different voltage_get_command = "VOLTAGE?" In the above example, :code:`MultimeterA` and :code:`MultimeterB` use a different command to read the voltage, but the rest of the behaviour is identical. :code:`MultimeterB` can be defined subclassing :code:`MultimeterA` and just implementing the difference. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/dev/adding_instruments/solutions.rst0000644000175100001770000000355614623331163023426 0ustar00runnerdocker.. _solutions: Solutions for implementation challenges ======================================= This is a list of less common challenges, their solutions, and example instruments. General issues ************** - Small numbers (<1e-5) are shown as 0 with :code:`%f`. If an instrument understands exponential notation, you can use :code:`%g`, which switches between floating point and exponential format, depending on the exponent. Communication protocol issues ***************************** - The instrument answers every message, even a setting command. You can set the setting's :code:`check_set_errors = True` parameter and redefine :func:`check_set_errors` to read an answer, see :class:`hcp.TC038D ` - Binary, frame-based communication, see :class:`hcp.TC038D ` - All replies have the same length, see :class:`aja.DCXS ` - The device generates garbage messages at startup, cluttering the buffer, see :class:`aja.DCXS ` - An instrument and its channel need to override `values`, but it has to use the correct `ask` method as well, see :class:`tcpowerconversion.CXN ` Channels ******** - Not all channels have the same features, see :class:`MKS937B ` - Channel names in the communication (1, 2, 3) differ from front panel (A, B, C), see :class:`AdvantestR624X ` - A family of instruments in which a property of the channels is different for different members of the family , see :class:`AnritsuMS464xB ` - If you want to document the type of your instrument's channels (with a clickable link), check out the source of the :ref:`aimtti-landing-page` page. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/dev/adding_instruments/tests.rst0000644000175100001770000002504014623331163022521 0ustar00runnerdocker.. _tests: Writing tests ============= Tests are very useful for writing good code. We have a number of tests checking the correctness of the pymeasure implementation. Those tests (located in the :code:`tests` directory) are run automatically on our CI server, but you can also run them locally using :code:`pytest`. When adding instruments, your primary concern will be tests for the *instrument driver* you implement. We distinguish two groups of tests for instruments: the first group does not rely on a connected instrument. These tests verify that the implemented instrument driver exchanges the correct messages with a device (for example according to a device manual). We call those "protocol tests". The second group tests the code with a device connected. Implement device tests by adding files in the :code:`tests/instruments/...` directory tree, mirroring the structure of the instrument implementations. There are other instrument tests already available that can serve as inspiration. .. _protocol_tests: Protocol tests ************** In order to verify the expected working of the device code, it is good to test every part of the written code. The :func:`~pymeasure.test.expected_protocol` context manager (using a :class:`~pymeasure.adapters.ProtocolAdapter` internally) simulates the communication with a device and verifies that the sent/received messages triggered by the code inside the :code:`with` statement match the expectation. .. code-block:: python import pytest from pymeasure.test import expected_protocol from pymeasure.instruments.extreme5000 import Extreme5000 def test_voltage(): """Verify the communication of the voltage getter.""" with expected_protocol( Extreme5000, [(":VOLT 0.345", None), (":VOLT?", "0.3000")], ) as inst: inst.voltage = 0.345 assert inst.voltage == 0.3 In the above example, the imports import the pytest package, the expected_protocol and the instrument class to be tested. The first parameter, Extreme5000, is the class to be tested. When setting the voltage, the driver sends a message (:code:`":VOLT 0.345"`), but does not expect a response (:code:`None`). Getting the voltage sends a query (:code:`":VOLT?"`) and expects a string response (:code:`"0.3000"`). Therefore, we expect two pairs of send/receive exchange. The list of those pairs is the second argument, the expected message protocol. The context manager returns an instance of the class (:code:`inst`), which is then used to trigger the behaviour corresponding to the message protocol (e.g. by using its properties). If the communication of the driver does not correspond to the expected messages, an Exception is raised. .. note:: The expected messages are **without** the termination characters, as they depend on the connection type and are handled by the normal adapter (e.g. :class:`VISAAdapter`). Some protocol tests in the test suite can serve as examples: * Testing a simple instrument: :code:`tests/instruments/keithley/test_keithley2000.py` * Testing a multi-channel instrument: :code:`tests/instruments/tektronix/test_afg3152.py` * Testing instruments using frame-based communication: :code:`tests/instruments/hcp/tc038.py` Test generator -------------- In order to facilitate writing tests, if you already have working code and a device at hand, we have a :class:`~pymeasure.generator.Generator` for tests. You can control your instrument with the TestGenerator as a middle man. It logs the method calls, the device communication and the return values, if any, and writes tests according to these log entries. .. testsetup:: generator import io from pymeasure.adapters.protocol import ProtocolAdapter adapter = ProtocolAdapter(comm_pairs=[ (b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), # init (b'\x0201010INF6\x03', b'\x020101OKUT150333 V01.R001111222233334444\x03'), # info (b'\x0201010WWRD0120,01,00C8\x03', b'\x020101OK\x03'), # setpoint = 20 (b'\x0201010WRDD0120,01\x03', b'\x020101OK00C8\x03'), # setpoint == 20 (b'\x0201010WWRD0120,01,0258\x03', b'\x020101OK\x03'), # setpoint = 60 ]) class FakeIO(io.StringIO): def close(self): pass def really_close(self): super().close() file = FakeIO() .. testcode:: generator from pymeasure.generator import Generator from pymeasure.instruments.hcp import TC038 generator = Generator() inst = generator.instantiate(TC038, adapter, 'hcp', adapter_kwargs={'baud_rate': 9600}) As a first step, this code imports the Generator and generates a middle man instrument. The :meth:`instantiate` method creates an instrument instance and logs the communication at startup. The Generator creates a special adapter for the communication with the device. It cannot inspect the instrument's :meth:`__init__`, however. Therefore you have to specify the **all** connection settings via the :code:`adapter_kwargs` dictionary, even those, which are defined in :meth:`__init__`. These adapter arguments are not written to tests. If you have arguments for the instrument itself, e.g. a RS485 address, you may give it as a keyword argument. These additional keyword arguments are included in the tests. Now we can use :code:`inst` as if it were created the normal way, i.e. :code:`inst = TC038(adapter)`, where ``adapter`` is some resource string. Having gotten and set some properties, and called some methods, we can write the tests to a file. .. testcode:: generator inst.information # returns the 'information' property, e.g. 'UT150333 V01.R001111222233334444' inst.setpoint = 20 assert inst.setpoint == 20 inst.setpoint = 60 generator.write_file(file) The following data will be written to :code:`file`: .. testcode:: generator :hide: print(file.getvalue()[:-1]) # to strip the last newline char. file.really_close() .. testoutput:: generator import pytest from pymeasure.test import expected_protocol from pymeasure.instruments.hcp import TC038 def test_init(): with expected_protocol( TC038, [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')], ): pass # Verify the expected communication. def test_information_getter(): with expected_protocol( TC038, [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010INF6\x03', b'\x020101OKUT150333 V01.R001111222233334444\x03')], ) as inst: assert inst.information == 'UT150333 V01.R001111222233334444' @pytest.mark.parametrize("comm_pairs, value", ( ([(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WWRD0120,01,00C8\x03', b'\x020101OK\x03')], 20), ([(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WWRD0120,01,0258\x03', b'\x020101OK\x03')], 60), )) def test_setpoint_setter(comm_pairs, value): with expected_protocol( TC038, comm_pairs, ) as inst: inst.setpoint = value def test_setpoint_getter(): with expected_protocol( TC038, [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRDD0120,01\x03', b'\x020101OK00C8\x03')], ) as inst: assert inst.setpoint == 20.0 .. _device_tests: Device tests ************ It can be useful as well to test the code against an actual device. The necessary device setup instructions (for example: connect a probe to the test output) should be written in the header of the test file or test methods. There should be the connection configuration (for example serial port), too. In order to distinguish the test module from protocol tests, the filename should be :code:`test_instrumentName_with_device.py`, if the device is called :code:`instrumentName`. To make it easier for others to run these tests using their own instruments, we recommend to use :code:`pytest.fixture` to create an instance of the instrument class. It is important to use the specific argument name :code:`connected_device_address` and define the scope of the fixture to only establish a single connection to the device. This ensures two things: First, it makes it possible to specify the address of the device to be used for the test using the :code:`--device-address` command line argument. Second, tests using this fixture, i.e. tests that rely on a device to be connected to the computer are skipped by default when running pytest. This is done to avoid that tests that require a device are run when none is connected. It is important that all tests that require a connection to a device either use the :code:`connected_device_address` fixture or a fixture derived from it as an argument. A simple example of a fixture that returns a connected instrument instance looks like this: .. code-block:: python @pytest.fixture(scope="module") def extreme5000(connected_device_address): instr = Extreme5000(connected_device_address) # ensure the device is in a defined state, e.g. by resetting it. return instr Note that this fixture uses :code:`connected_device_address` as an input argument and will thus be skipped by automatic test runs. This fixture can then be used in a test functions like this: .. code-block:: python def test_voltage(extreme5000): extreme5000.voltage = 0.345 assert extreme5000.voltage == 0.3 Again, by specifying the fixture's name, in this case :code:`extreme5000`, invoking :code:`pytest` will skip these tests by default. It is also possible to define derived fixtures, for example to put the device into a specific state. Such a fixture would look like this: .. code-block:: python @pytest.fixture def auto_scaled_extreme5000(extreme5000): extreme5000.auto_scale() return extreme5000 In this case, do not specify the fixture's scope, so it is called again for every test function using it. To run the test, specify the address of the device to be used via the :code:`--device-address` command line argument and limit pytest to the relevant tests. You can filter tests with the :code:`-k` option or you can specify the filename. For example, if your tests are in a file called :code:`test_extreme5000_with_device.py`, invoke pytest with :code:`pytest -k extreme5000 --device-address TCPIP::192.168.0.123::INSTR"`. There might also be tests where manual intervention is necessary. In this case, skip the test by prepending the test function with a :code:`@pytest.mark.skip(reason="A human needs to press a button.")` decorator. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/dev/coding_standards.rst0000644000175100001770000001036614623331163020771 0ustar00runnerdocker################ 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 separate words. CamelCase should only be used for class names, unless working with Qt, where its use is common. In addition, there is a configuration for the `flake8`_ linter present. Our codebase should not trigger any warnings. Many editors/IDEs can run this tool in the background while you work, showing results inline. Alternatively, you can run ``flake8`` in the repository root to check for problems. In addition, our automation on Github also runs some checkers. As this results in a much slower feedback loop for you, it's not recommended to rely only on this. .. _flake8: https://flake8.pycqa.org/en/latest/ It is allowed but not required to use the `black`_ code formatter. To avoid introducing unrelated changes when working on an existing file, it is recommended to use the `darker`_ tool instead of `black`. This helps to keep the focus on the implementation instead of unrelated formatting, and thereby facilitates code reviews. :code:`darker` is compatible with :code:`black`, but only formats regions that show as changed in Git. If there are conflicts between :code:`black`/:code:`darker`'s output and flake8 (especially related to `E203`_), flake8 takes precedence. Use ``#noqa : E203`` to disable E203 warnings for a specific line if appropriate. .. _black: https://black.readthedocs.io/en/stable/ .. _darker: https://github.com/akaihola/darker .. _E203: https://www.flake8rules.com/rules/E203.html 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, see section :ref:`docstrings`. .. _Sphinx documentation generator: http://www.sphinx-doc.org/en/stable/ Usage of getter and setter functions ==================================== Getter and setter functions are discouraged, since properties provide a more fluid experience. Given the extensive tools available for defining properties, detailed in the sections starting with :ref:`properties`, these types of properties are preferred. .. _docstrings: Docstrings ========== Descriptive and specific docstrings for your properties and methods are important for your users to quickly glean important information about a property. It is advisable to follow the `PEP257 `_ docstring guidelines. Most importantly: * Use triple-quoted strings (:code:`"""`) to delimit docstrings. * One short summary line in imperative voice, with a period at the end. * Optionally, after a blank line, include more detailed information. * For functions and methods, you can add documentation on their parameters using the `reStructuredText docstring format `__. Specific to properties, start them with "Control", "Get", "Measure", or "Set" to indicate the kind of property, as it is not visible after import, whether a property is gettable ("Get" or "Measure"), settable ("Set"), or both ("Control"). In addition, it is useful to add type and information about :ref:`validators` (if applicable) at the end of the summary line, see the docstrings shown in examples throughout the :ref:`adding-instruments` section. For example a docstring could be :code:`"""Control the voltage in Volts (float strictly from -1 to 1)."""`. The docstring is for information that is relevant for *using* a property/method. Therefore, do *not* add information about internal/hidden details, like the format of commands exchanged with the device. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/dev/contribute.rst0000644000175100001770000001341514623331163017637 0ustar00runnerdocker############ 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, it is helpful to start a pull request (PR) targeting 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 already after your first commit. You may mark a pull request as a draft, if it is in an early state. `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 There is some automation in place to run the unit tests and check some coding standards. Annotations in the "Files changed" tab indicate problems for you to correct (e.g. linting or docstring warnings). Your pull-request will be reviewed by the PyMeasure maintainers. Frequently there is some iteration and discussion based on that feedback until a pull request can be merged. This will happen either in the conversation tab or in inline code comments. Be aware that due to maintainer manpower limitations it might take a long time until PRs get reviewed and/or merged. In general, review effort scales badly with PR size. Therefore, **smaller PRs are much preferred**. Try to limit your contribution to one "aspect", e.g. one instrument (or a few if closely related), one bug fix, or one feature contribution. If you placed your contribution in a separate branch as suggested above, you can easily use your contribution in the meantime -- just check out your feature branch instead of `master`. 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 pytest 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! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/dev/reporting_errors.rst0000644000175100001770000000046514623331163021067 0ustar00runnerdocker################## 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 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.369605 pymeasure-0.14.0/docs/images/0000755000175100001770000000000014623331176015416 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/images/PyMeasure logo.png0000644000175100001770000002714714623331163020766 0ustar00runnerdockerPNG  IHDR\rfsBIT|d pHYs btEXtSoftwarewww.inkscape.org< IDATxw]e{ιuztRi ]AQA+.]˲vwWtյ`YAT !gL$sOwܹ>2;oA!B!B!B!B!B!B!B!B!BB*UZ,ldT=PD+e*OB;kAP{!jKMqzt[\H{Z@__ q |GÇs+D;(uKYvm8yBL(Z-\;Umr6"_QCIOyn%+J?2 !&]CVs )s~R)'Y 1z~ĒR@c|t~2bӚ6e绑`^6NWq [}Wk=&v "g쎾:Z@kj7B c.kFPfBw5wD!Đf5sƒPz !Bx%wdOՅcQu7=!p?afmXTgC2!8{_Q&DxOM?mv:;Bd=}|po})Α#K4_{=9"sdtؾy?o_>h r$Df/H82[77< Y26Wy;_)M-!!2PFh$Ѱtw=8N9"3edOa#^~+6$?CBd -׵O߇:ɹ"3e\W+__Ȋ+U[J{>%!GBd Zk`Nm 2_@.c|_^5ٽsN]"#X@4B|K}å\tž4lbws[[hz9TQWYK}U5>/O[6$?ny.JBKG;+7allʆ-4n—R2NsN=E N#?'7Wl_y4sO_ȕ2'^1IWښ-yï0 <`Tl_*RGtTOӫѡiYsExw{jEO|V֮\we)c;׼s˗bj78~g`(l3 0Gmv kXa \}|=WR?mZZ^{-V'djb5O_ad65}6vFk W`ixki3>ڕHnAے_{W'<E(F(40RT !ݣюfƔ|9 R1mC_>ai`\yr"xkMgz ̷RW|Y[vnz'& ]|K'p{>;E1z-|;ZJ N31lcy#,ͶJ_wTgi~p@[ҟKyI8[}J8;?J%<-ɗ/NKG{5fi+Yreq͝M8[gb/ 7L=j ;ؽoݝ=4liyBZK|2`G۶{'/o x)9}#޼/TTeV`Iu-VHJ '3mϼ|.:չJX5K_bfSyo)f[(Cq`mR$qś%8VsV1s)&H̪HWṆ~ 8п6JBt-D(S=ձ knwO?|t-ڼYg@l7"D9D907B)OK{SDHI򛯒[_ sF#5nX>w#Kattw~?Y "^ZqL|A6tn;~Mg2=(]=Q ѱ'Vfk} ? 3g"bw VAOo/yc긐 jc6n|YQ5x?OS!:t gY00Vz`PTŊ9#F$zykTzʯ׫ &fh]`ي}=>fa١akʄw41rFEEAKPXZ$ uoP9}ľ6h\UBA[4vDY_'J?3<(M<<¨7Dڲo`c69w#^~0&ZYdgx>K~@k$jg ")[[ָ@o>͖8{<9|=7п9 w%oR`)zzYyCҞ3VDRMriڢ G XVl'{z'wO[x}M6 ")^]߲~Ho<˕g_ʹB4kǍqhtc)$iYz(/=(yn#w^©s{5k#`&`d+Xe]ڞ% @n{[[ o8R'm]VL]O2ȂNhJmf @n7_;R#|#j<'>>{ԔTa9o$4[0Xؐڌ Ap+(95#`DA>mTcO 6H]&!@*bj׍Ss|SWVз2ӓs{Δ<$^[p3|`g̅8x^i3iތaz)hؿ{q \ׁL8WmB.,k}^_Qش]6}+D_)PLKWU`8ig^){NȗXX?ϡc#D;I$4P4ߑw \g>4bd)_mt[}h0pXI^w] >{̬ͭӧ m:8h h oF{+6'!ڑ~NDxaRX/+4(ȁu .C/ќ sy_wwmeOq mQ;V.h~:>Ϫ .><>tho%%XE&^K1R8ֱu8މ,6+93S;f}s /YFxo^#[2J.m`ǚ9 …gSWVsҶ }MpEs[ 6EfbF²~/]zPDcAaP^UB?LKs[ɕp;sKk/pMQTSa4Q'3#ispcA_sUTfr yY9K4jcYs,%o5Bf; azZEaq~{Hxn3ko\:s |3RMftw& ˴8{=tioo-챉cM[{Y's)qRE=5vh/A9 &n^]SgYO?P3(wpL0 άeӺBQ?oFU=3ijٕ/1 'CAN ;ۮ{~M;9=!tu:? @ٓK'P4Ǥl`Yv@a`q}]7#Ub)l^Hdl r˸b%s`B*(\\w6c~hhE8&z 7.!2#>R0o5HR=ʡc`iXL?εN7귣Mu_CpAȿbwh&"@Xm3Y}6_3r @O #_3X&(Ŏ]|gr?=!Rc%V:s*mn֡޳18_ 9t˦nz5 [3O$;H㎣A?o}_mRD ð{5J) K|6>0)Ӫ my~Oj!51jXEOX;g/?[C Laq>EÖFK߅TH8m;ZKs 2RԶyOa31@CMiφj&'/YQ.|2<& bT^$0L߉G[d`0̞?)ӪS6-71 {$ւyhLUQŀ<ѵdJ=:E% Du;M!G=460<#}c``mzKDk8,>ӏG) Q:Rs Y$qo)''^"kB_@ǚCyAـݨ^LM-6cNeoVz颯kMx}SYSJG[ӝ̽ٴ"lvkytz}Qj[;_rö+vG5<yaPPǬyS=qܪZ&RkVmoIu͍-Yegҝ]3m=^OŌ깬֑Tq݅d$ صow|lޱ #̷mC{ح˜Y^M?eU=*0ȅH Zkz~c|f`)ʊu¦]6n`Ix8|:GP+@ÞF~e(oy`{>`!L0߉;^ҁI@AC3&jۘAwy*lE( ٌ*V!z2MN` Ó0I5iwO=#>A(FY 4 8g9AE_HzsZtO3QF$W,/K5oFYT&9?v&́q,DztڐMt&`|^/eUx&3϶(13?CӰ/I]X?s0 'H`hMkYqon\ޮ!g wCa3"m֜3!{An[[ضg'wlMkYi-ͭ-El)-f&^EmX!*"R}^J `$P?6s_#wa.ݍtvwm0O1"/ճL<5J4;~),~sͬU'IH>ڻ:ꢹ ͭ-oi@k3݃ޯ e`fA iUIs=5 ]j?q񰗚I0Kc%evhؽ5[6а:;iNڻ:DTA*#g+*K *qg.|sfG :[^Ԋ)̭~LVv d .8^`Ɋ޲nr fz5}nքz5B,0 ëП/QT+ l?~͆+h툽X4tT3IP8S/?/=+qc ˔!㎣ G&phB'** r3jQlKޢ̬x}v2!&! wO?o?ZP>Ġpɜ#|7 Cߧȋha;۶kAwC${MDl=bkB;mR\wF0 9gP$̃<įx)VWd0ʠؽ:ywfS&v'G; Ch^ ^*|^Fܾϡ9IkeC3gqߓ8,}u_@Tx+M<5ʧ(U̬54#5mG:>@-B, W1FQNwtlݮpNMO~Cצ/(Kr& 'C?gmlW:oMlWbŴ*im۟U$EkP^PI#G;|O?Uf~/̪5;Q(lDk_ iΞs:̍T]&_xzً(¿ *Jw~& obwhWq%Hޢym5_o)VA^0`sP6ΰLãߪ9T ﴉw p7I`kl2)I~Ѓ<¿ (L3X`ubx,6᝱Nc$tAQLII/3G 0 (fd4hs7qD 6^x\NTR^~ V=?Pz03 !=J!eZ7yp se$M>߿a;6f,'U1п9Jt+naNQ%UV0H̤ pA[gĠ`Zўtk s շPrsȒI3=o_m୳(W̚r|~w_276NXSSZWBIJ)j\Ρ8֤ oǿE?$?G1=J;"6Z񡋮crwRbwrMh}^vTp߉#KaR3N&46N︖14MӤԥLȳOfFlET-o0 IDATȡ^5K8piW֔ 3)@[g=3Ps3LCL)npc/re유Iw?@ 7#5E5&MxOl /4yrK KaWل{sOb]c% C^>jR192X G>_a;QθG;I92pBtl7z\pb.9B?Qaq>ťc3@e3Ҋg=pӅ5vnq9y;,.:<򳒳")iMߏ)5'[_kp4N&th1-N5sżqo5e2:,6^qG_x8^ݫqz5NőfԹ,:u ?W)E>K ҳ"!gqlQ'X׃,o d3YӘ]=q?psʴ*r yI jiTp~ӧ>0B'顼b ʨ,,cj* S7>毖]~SlG/ vdde{/Y ^KaN>y9fKQv>yiuRJ)i`@ĆU[$f '~PaԗPU\IYA edJ`/tcuӫ/}A~F"dhbkD`\M͜d'צi2mV-ٹc7a _ڰj3n҄lr@/٧RWV3>H 3jdO*V 74j5ĶBCq^!q1;sL3EYe 5i!b&lȮ01hNw$CNN [3 XN؄ YC8t pcsygr#ۥUs鮸)CEa&lѬٮipb_F5oug>5Lrܓ!&l4Ǟhb3Ʌ?[̪έ~T3̄ ǎh lZ;L5 e1x>*(,ɗ2Єvfjh ۓ<>i3~dBwU_+lˈۓgk\ȭM(.-(OVM6h`.oMU3O?N& ,yO06lޥiH8xMCyA)w\o{< 9ArsP .ꠒ"»m^d3 ;ϋ? +;(UIfdX TV֖e 4x,<^+k/$R:9o"Μuʸ<xiy, CI 0 уAMaN>=𑛗Mn~,?GoTV>w~J)(ʓ"$$( kΝsr%MTPBF:HIHݥdqc+(*ɧTzEJHH@h{lo+{zSU_8ENΩ.Su:J)J+.{r5b{\w>&㱨YKvSd& qvp4 簠~Ψ|L5r= #QMxws/U9AΚ"_#Dt9)e /_3 Ck"{ہ =?lIub@hc "7G^^A3t;?[Ouv\7@̛2}aPSWĜt+klG{?CcTg5:8HpEW'toqYl9 =R>|՛٤ ځQ|(,^4)*IbD: qwsC?{ IcGzϝ2|gBWȰ`ރ|៾enfŲ5ΨL=xtƄμx,J ;iV}Onڟ$ddk4 i›^C?h$wեQ܊ڍ44yeg>'?6W_Wnq3,yf9zq.sWpIB5̄iSc-zjO&ɶooK?͖ ;R!I"^\oppk3!qvˁTge (D)|ˮ(}$@$3_*,OY:/37}a\~ҊbWLEywqb q`ߕtN?{_lWHSʹsUe;8%˭O|AwjMp s|ʚW# B$`|;XtΊ+$lM|kzX/Q7" <^q&/™yLuVF'-e׼#YI: B]_9irB 0MnZM:;JɌRp{? t]5c:S6ݭhcIM7B kSZ3\ 5,^ NF"cƚ}Ji~fB˚[~!JZ%^k2Jx#hLRj̇%\m8_՞nP٫ ^ )u[%dZuKd/ þr:ӭz4X > b0aJDeݑo׆ OJ"h$brTuslm&*+ s`[ ͧENJ)|!Ҟ+P2jW'Q);Dkv|R@1ho#DhTJ5ꌦTI!B!B!B!B!B!B!B!B!B1.MZ6IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/images/PyMeasure logo.svg0000644000175100001770000002467214623331163021001 0ustar00runnerdocker image/svg+xml././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/images/PyMeasure preview.png0000644000175100001770000014773714623331163021517 0ustar00runnerdockerPNG  IHDRաsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< IDATxy\U߭ޒtgOF }UvawDęwqwGDGd\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$IU:U_ GA<&lc*!xe&yl#]$I$ICآx~bވ1Wx*?P~8-w F$I$LAKP>|hts+Tbd$I$jcb3X*ޱ2m|$I$7&^_8!J!I$IRoL֩Be@s㐆(╕A$IޘWQJ )qJ I$IRoL֯*4D7q[$ITuL֫1Aj4$I$UHKC̰JG I$IҮL*uKaH(t $I$R/~J!I$I$L*e7p'7<+XJ#I$I4Tj=yoc H$I$I&+s- X¿_xH$I$I&֬_~V,Gq$I$I @ v}?S"$I$IT ?W>l:I$I$i;/KPU\5I$I$i;^s#>{eϮ$I$I#Jsk_n#$I$IҮL*R~{M$I$IRzL*m@I$I$IOq-CD$I$I @y)K>w96$I$It-b/Kljxnz+%L12U|k=ukiߺn::馩¨#:qZtȒ$I$-*ɵ?<6y߯p3gBT|O-^>.Odr֭b Iq0cʾ N8X?h +c$I$ QPy,ϻҜsSN=\֬|.8d~KSK'O?篏'sEi"\(MBL֘bg ; sGtsy+ά BCN\P8$I$Iڑ :UW?D'>198yG˃rcS vJEوL[Df8dFdED%~E;b6{cͼeWÁLo>$IjdN\p9~]=iM Wkp5z5Yz%mb/>ȍȌȎ̐m)RYY &bLEu+ػDŽs2(I$IFf]/_RO@-"76KfTDvD~ʶFd[sC~en|kI89hL.L4ґ @I$IR5T:U|.fY}3sC{k‡96mL,Îkql$rMh96G-E y'mґI$ITLj<²g? =TُW_h>YY*^YrdӲl޺~\wK$IVW(.[J1ˉ2͇ʺA,a]O{Q(y۫^W$Ն8ǏRL89n~B,$I'%6zǃ77>hʶtn咫I[2Anb_\ 5/;aՅs~J Q|1Q'u%*.xUU89F141LJ$ *X(Ll*} *j $I5.17L}gFGyVLK}QuFQ8WNvԹNtP9S* :H$)xvRbplUO1t/)u~]O)vPh /Xa}Ti&q\yܶۧ}Jέt%J!IdLJC-ŸiY[^9<1WUqŦ`zNk/<'0)M?\M1ߔ9mwMlԑG5J0HS$.s)w~GQmTv>C~uqŝ;erz-_:QNHTw֯O|Jv pp(,CQ$.ϯ% gƍ ^^Sh15@ B O''͛2wQFUޛcLt$IJ4{>MKSm*DX QpYvuySE\7ynW:U 꿋\'8%I^_KJӼݩǎ:Hׯ1t=g;YӭE^5R0%Ceسa/BU_Fښ8ᔹms:ѣd'U:2iX $I40&!"cy>ٿ}ˀ?q 1qktԽ$`,Îm !05Sz/ l%7vsھU0TIk%V$I{bP"^؍dNWˀ>rz{ mib[i=LsAGaCG;7}{ AJ}">8V8TuQAͪĦ$IҐbP"=tfw\DKi$(n| >Ŏ 7.Cfcm'|pۍ[8?1un+JYB$IgP"/c07ඦ93qG|ʌ"z^6mٔK&yLPUfo*$I4tphvz!EuE{#a5/3c~2K}~ iM5c=ҁjC}_I:{KMIu'ECs^;% vcm;<6 j{Yj<=KYMt>)&;:2#\5;&C"w=W^Wڣs Qq#D K-@l)ҡ*?t ) AH$)&!`hѿvlBga}Lo_\,$jx$7>È_~vt(8Jt ۯOsʩsh)[ IQ vr0=\:;>A=G?%IT3LJC]Ks?: =9y7s d"cc(ֈuE0}JZv[q~4z\G _v f2g±TVU: %I:T673O2zo p/gغ@]}C-:G+ҳŽ;_g⃈A{}֔FPmottpt$IRzLJuG w`/6iʽ$_¹ŘGz(n$`-|= G#O,[ri| e;XT@Vt$IJ @{^L{;wKUǞF)vV> =ӏ噪Gm| -9JsM*ܪ1/fW:8H$)]&:7{!+44xzqWLy&F`ɊŊƢY wGa|{wUm=NfU:I$TǞYkWܼcNq~Qg񳏦<ŭMfZ"zyV5 S\qaǧ jfJ@-5,ITLJu 7:CSËסi$mtMTd ڿ%I3f=W.j +W,Uv|}4uN˔Tm _ JR $I>Rm CԒ }>%s/=8Zay!lЏ*7 lJu޸nڮ֪^ Lt$IJ @Nuvurc"f% {HBX?NsM,-մsJQZݷP$I{aPSw?]=dGAsc?b{`x~9 :", ~4+ nO.\S ӛK5nO~gwoTI+ ͵=Y !VMџ&r VE{3pzvyqY\$;!qr̈J 1=kB⯸9x0N=D8e9 Ϸ3n=ͤ!$[(V̦+F |9`e'EwiܶZ9uN۬84ciU__-T^U183F'iڇ?8_&G=p1B"Q:4bogퟷm2{Z^ 4%'"$(Wz*,'{P]B{AV'(f!w*J/YGxP!X)asƫ G7)"g^Ѝw@n|lfO תmhkiQѭL1%qE9qׯOƭϣkUUŰg==t/-7./pÝ=a$^ui%:6n2e !Sٴ`.?((WSWQ)2ӚD$7ĔcIS忿> g\$v'.PI @_ ,$rDtJhPU߮f)*OcmC ޴s-Dܸk 7>H*"TPIj20(ՙV ɴD6ŷy(qWHsE~&&OnX0z\ox{ qWL3yCyCy97j '-0{>q}KT5QLۗb61a+lecǥ3[ҏoz$JLH|u8.~8NX(:?N!T;^OWR~BH]8JubIL?wy/3.8Ώq/!m? _GRhn"rc?|:(D @i /HsxKqEE*^N 8cIKDD^< 簁WնuV[V'W:>3=d$"aE1՛*YiLEhwe GRyj3mQFQG5ah򃴎_)Ηb(Dwi oI)IϬ{S^ٿ@,nKmL7y2{Xa[8,|b 7C &b.Ӛ.kluܶ'&)|/9r]&5u%aRLX4:jeϵDܿ s?ܵϧ Mc.|u$nÑl޼w gF2OH w<-͎"T<{UM,th,ͼZ~/!uM/Iys!Kx-Bh `B/;XM|Θ$|֝LMR+9Q?zm! Nx/y[+}uE!ǪkY9 @Owb__/kM$1sҔxo{> oZi-i5@~P<<@Oegg5lDh239.'T,.q,Bߥ ]}caʛ]@X'Bi)}<= sӄfa6 IAK?8{f M~m~OXXAHBxN#4DرRa>zpı>t*Ia sy+ո']af`[b9&c|d2&OG̢mWqLwWϠWU&IN( Q; ݯ!4`8}270@$Y{.@*rx3PeB@o*Vt;aO4%:;z+5*.$$QBl'ݪ=4OPwᳩ*%4ęIx$!྄cAH(_Ef-"ЕSd9H i1P۔p"sB@IaU1Rk~~GfK9"YFa؉d{KܣYǁ̠-u]+Y>:U}q&PpUʐGg8ٲjoO 䗩5Ȋ"Pl]JsK1paR KZ}yMYpBžG%)ĴOPzr7 ӀOv 1o ,Nj4ڋ$Ko:eZYw_)Ƣ7$Ee6ukra+ 7P$.e`K7cTLJ5G`!O`asv6mkT8w{yh<kjnb}8 2㨶MCK,!ҫ3n3KlZ9+bꏯrXEXXJu~}=FH DeGx7fيεPiđ9"|/ &Yv$,V2(ը|_3,vlk *WPྌhrѳ8y|lg!KOݫȶq49r&K\mo%g8] 6OG}Wjs @1j=~ә,~aZʛ,9 MPZCo%/,M G SΥ{ˈRX*t vՄ`x/ 2{Lǥyp+s@r",sᜱ:r3T%LJ5{^%' EX}H5-8{j{pWM Lw"=}BsKi7' Is.xjJߎ)oYC(~'$q)uX??&d2IJ' {Օ^~8nף$OJw͞',>V%6޽g[ Id/uI\DH !Aso/tskh8/j}:O)9\}yߘpHo\UR z`øiYZG/nc=X*NIQ\ f{(VQf7a4yڏQ{SxK^Gscȉٸ`Mm(Woz(e@\ǤT]C[frWѡH<~8ţӝ\e.^6B^Ujp?NXJl$*Na{ oQ*l>}ո =He+'$j̋՜_'4aOّ5?F#6I4<Ku3TLJ5f݆|t) 8iq=1 yha^FǛb(8e^44BF`A9{]6r3gO#`1L0#Cl&u_c(:eJ)ov߱n*eĊu<'K|)Y*6Jc(Vs]%Y|˚u1bz-Y. ;iD9@N߇sѳu~sGŰ Y~&shi_RJT ~dXbj@"x&5_-]cí58n;Yca$=w:.N/aIp"SqM|4{F2U\좄]#bDq"݀.2OE1w$ˀg3$X/N8τI8LXnXÀc`LJiyBNInz:Pͭ:bPr+֮~<|?j`„oLmNj,\QXaą͢!RA0Sm(/mXު5S|K6yS0}X >|eNgR[LkeWZ]18yblJ8o^nL8F' {%ւ"ɖ6P am'Tղ !ĥ\:%'S]d+!iR"Ɣ8f2! X-ݩR}u@&q $zp]1M}oJ?U @ yFOYrDiFhZ. b bظ6</9-MD}%֐̚tc!BL&C< Ѵ8b&0 )qe[`QWOgS0f>8f@̻5QcWWRzH?d/=y)'>!,{N6Rz1 X.q\]f * |%X':P_R\n6Kw& @>_t=鷬YsΎh?G5 ZEhhU٪p=s_nS,0zFfiax haKekcmGq]L͘}odPCN7]ִ]u/k.R ۜ'~ehT^ jn_ (q<U☥@ Hz1K%I @/e@ߝzWC Ǎ&Tx]U[.tHr(6&0RE.Yʍwc>Q12rZS"FGd2}/^ܸ9AE+Zӎ8yknipj?YɳL @8~^Sz(8һO@eN8I I7\$if1Pڙr,fy1ÀҭnNhf#Uo܋&W猅cdLRXn-k_NJVX9\3+rjxsXCk~Pֶ$}riLǖʜ'^]!Gyom6j݅\u/<8V./w* nw%'O$ifN yBY%KV&T'o)I~0fsD>GL=ФJj&JaR;9cH1!#_ȳMlбV[c}=5EdeȴDdÈL2ebD.wEVM(Ĝz {l0u E%UFi|swRnzKF΀Ry%ɻ/ณ)}lY-HVu'np:rmE*S0R sKH(~ !&l۩jX !Ҋ5xxxyjb6m( LS3jLs0 e`ȈI"ƴoG;`ʝS<(d8gV//rח8/Riřw&+x}~EϧR5$x./%GPrX˟k՚cjbnt2ps sR-)7 CW R[G,'9C5Ʋ+᎛aT8] f:@WgL֘n-]1y.#2 ڡr2.1vã xt/Ïgȱ{|n6jL/z2 _~Oʋ:n2'f Qu>6iSz<ՄKIJ ܗCȳ7{ IDAT\[Qo oSzv ygu¾W)CUVOo9C5Oq?s'?ϙc/<1jzO6j~DӰg_ꉷЉ;BƵE%uBYXg^wW464@rx0r23oG w\Uם5  -t M!{*"(EAQ: HRH dwmf3d̝;s<`g읹~眳)pWySM /\O8le[oxv!}< dqF5V6ݶ@dg:J?7՜$^Mα}dݡ"r{̘ L2 rw3$IJJN G"xb'd;7Z\Fn/Rrtv'_ÛR{|'4"p{̸(_63$JJ;opod銏V;  UNXaCHah mޞ7NC!,)(+% CYa:oE R[UA;P4G%v}+V.[x㺺+Vj?-)_c cL>`=0(6䶎O?\bn4p6a=Xѯ_idl2RwՍ#$A$阑%%gllnncM  p0aHǃQ CK[IpaCKv]a0%!aZ:>z >ⁿ狊((Kwd"/W-n]w0bnx u'`sm*mjz4|ow lw"%SFxm~ u@#p~=VŶ{<)V(x?oC[==ACes P[ %xUWޏJ%"Vúk.б,JͰl?%jU9*9n5CGVl[ٞ7=qNNPp<:R''͑@2wYb_><17MfMhXcW[ x$I~1#1=O\1|wDQ()Mnw rYoxUNouQ:"q>&ܮv6xqx!Ԉg^ZH'ز\;q{v _uA4j{\,8V]0Br SIv^Jޟctvc%k7A$t̐dM\|gpBFآ(ku o4ȠڄQY]Dbj]"t"o6K| <,9p{Y͠cJ=Vol|"~r&~dI,3e.,0(w yb-p v~뀭}a #bZT|CK:fHAQPYx酼RR!UL`Pon~+?5|!-䟁;vžS 0hH Wy=~qdkCkoY˱y5W@V`Nd pm#` E=9[ nVC%E`0_hIתUi< K&U5>P|8;Wa<`bu̐ SPΚ?^ȇu( 9 0eLbf{+ -^89"Ew ms`d}MG}^chS K0dD A4Qy֧?w2ڀ7խoˠM{ @0v:cT9Cbq|2bIE w"[!>" UV1 u>\Рe[() 0kb&x -QɿUQŸFf ).J\@WJovX/Onrȉ~w`T} ;l>A=yox߉d>ׁ_T`Bm yV[3|c&o#:\$StH *%k۸\z>P!ʠ8dxguzpqmj5t&=-t~E"; eiLDnT1Gv٢DucR)ﻱU3%IXY5׊$< >i" e&Jf}(d+pZ=誴DLrs!HV[ 80MءȋF3u)O  \ГE-Gu"B?+6Q}+▎ ,U&W}`C l=)HQ; H{Ot&pYI1t``HFqp~kZB#V,nfs826'b&@SmrE+\YIylo޼ ܅;Sjl24x}6p vxH6k*lNa ]J﮻&{Ie[(rzCU9lhV++"t,t|a6I>rHd 0#֜@/l L&P]|fJVkoOn9QR?&+ +!n/G`؛V#OV w`>:(13$GG|7Ǎ'PU@1PSiIU2c"90oc龁.lzڣ@h0spҽoyDI:MɊ_ {AO\l;Wlpf0W1 m=p6JKp/0 Y6Nv"Lrsh5DIJo667~BJ& ;L0v ɖ6U´C۫t~osLL~/@0dd0#[/qpM腆\tO5.Kf5yElM2y6.M4^ % [6o%KbǾu tA,Hrsp"PP|~'kP4<@hDslQkQZIda܈z.9"MtQCT'EBNfb)2_YV+f"=-KM=G ux%n "->-tl ܌P. Olc%=zi% s7rRfU&/bh Ba=N\d߬(Qۋ-jlaѐ9ԃD=O5 8$ɶos\e<"?OρSb/؛=/߅I3((׼+n8P2-Ĉao+wB8)Ad}t6D(/)cA 8})oݸ6&j<8 4 8Ods͔ؿR`.⶚cOO_&үaG Gdp.0 Gԓ"c]KYzZ0}|uZZU&C[aZ_c Le~]f~ldZ5\4yÃym vp5[0h>NM-p LM.ze;8)}"Gބ"A'='U\XrRPz磥pBC1{R@a i+I(j9X|BU 0ڃHE$]8'0Ⱦء;MγԍAIi"N@X }5`>1V-޸.~=$?־$[.`˶xHq O®+8 Ԇwxn' hڀ]s1Cr1/:Ͽ?ʆk1LK!=ϣf: w؛ӾR믻*Y"56p][w% &\h~4y>+^pNs};ey )wC'ݮV{e;3$@ wBaÊ)\{,E FVְ5gJS_L9B5Jt]#m5D[zuX*b֤il3q+4AUOuU?CS_=XD2/DN8KF^[9ps59:)][0=^UDΫ%F%x} ˌh_]p?*+ G$=_@P^3ڲ{aF^i0`:bk͠ȔT76?GaTVU56]Y}pJ*%s .f=o.8 zxqz_xvކ|($ۼ4|BڣX.)]v1*pH*[*h!l+L&, fxPFIБ jorn~a\2vxmOʻprXu u%`)Pmn+Z3a\(P/+qWzccO#I%%>۰S~}Kݧ !^%`:UTUPYZAUy%EŔSRTJyi)%b**\UKME Ue;QC"" .hz֓@0'cUuOG T+/{W +'.ږEU]$ۀ>>Hit΋J JƜ. R<w7JՃdDU]b2Rp/.J'6Wu-1axg:2Qؑ=0DC,b8 {;9H}|2vank妨6~HnH| ;IIMdӒC:)ony1_4")(i <^y?BGf2 :WG,-)el;y63N2+R;qC-ŻTݺoyO.[ mθKM.W/=Pwq`@/H,! =!U>ΛE+6-b"ҧ`CEYiWދЖ_6mic&s!q9?fA qڶy+)-aڬJe{| aE_5uL^'v.0\&_̄s>́J{/Ktrnw]RQ]#\a,"SPҮhHҾ+;#f m6I&o|w;Mߎ`sL=rM/"ό17@qtܥyRA,=lU[`+: ٧.4 o]g+ l;Y_]ۤ]o^tPcp]߁HoL"Y),*G9u |-ibcy֗D G'^G|#ӼqBANcԱ؋仆 utw> },ހY45~$ (= oԖ" X! q^Gq1iԸn7[ R9S:a\=81Oۗ-e"`c 'm"]oM;QZZӛ!(uCGso8Ny3'0aBEZG;p?wk;2<nwvvByrRK.0˓h7x; 8*(XbpVֽLvα6;_.^Fv%//gj&c헱LǮK+16njl=W;P~sJJkDhIs_ZhaԭYrbFmfOgITVB&"2eq<ܖ_&8Zn2VN@?㰫+g[le[q(JQƇkhy"Be؋lrp˶"Ge $bGYS8'>&b?p{CfIEMdj{Ʈ^)I6[m }:АbAKc5vwwj5{Rh#^.Qu,e %`?"JdۅI>}@>̖ճ^'ĝsImnjoaW!划W {8?3+"Y%@$ -Ҵ) ~0v7NKy+-+nHfm;Sj2H8I%I [*[n:lҬӃ A[Z[@ٸ_GH"0VOXbWq^ cLoxmT }o>wIڽ [}~V#3>a=k6v;H#` R& IDATn4SP.j46@;afpGMq*ʩ;s0sF=Dd@[b 9%ջ*``WfpG}9؋?a+\]dzQ;-[ެ<8;#^) %} $ocn}~AL}y! ls*,~Vu>?<\Q_p^||^3}IvsC4V#llO"uQꆌCN#n~;PQYFye9UTVW GD7]U}7Sӹc~VcQE&;= [1;~x;W_ISI`^.~}^c!*Uo;`Wܝܻi.Vy0v/ dTw5}oH`G] xWo:a S1 ;il#P*2`0*w;T)n_5cWrnjHQ͖njD3Jvz_ځSrІ&pSPr鄎w# [,@PQP(H E%Et5=Wwe+ᤞsō[$:{wq;|O/ Ra/RǧP7A`zQ^&n!lub3l%Q&&m&-ҙl px- ׯ`6}!} ǀ>fc+S,D; n;LK˵mEE!K)-z%(*)'"Y};ZWrV;v[\}]#*\s+E{ǪkUDWV-6o.q` ғy%;a8Xblek?2z'}TZ \ 4fϝ7!j*X?j@1SK8R4+)UNyb ^|<ỹX#VeEj Xd(Ufd]vSJ'M7UcyF ]aCR;/5L2ĩc:b0%EnCD$S>xp_cӋ>M"c}Ka/TrJOD 6>aj?l|O=UP9nI6R[a[2-fȽao;b.luV6~b:m "k;]ڱsNN(yD @IKc8jTUR_eLjG*Һ8"ߌÍvKt(y!0ϱ$ [Q'l>l5=D/[*&q?#w`?+sɾ*;} l5@?Q0KnjIbZ_cW_.4PPrN3CP7dl~ǡ~H͚Hyd+'x7$5W{>a٦ ]C=3āޏlNî<`˶ tMbmͶpg>]؀]|@^!H Ov|[r][[ / tn(dʌ 5TsHA1<ȡa'~H^'0Xv?76'2 `d2?](nTζZo>&/}=WdO m|]|&ղ91-y{<njcw5#~xG)  La\Q^QƤic5ϟH?V/Xw aV鴷;9Z:\u 9wcׁdSHK9vж[۰;3dl|w`+&uu9v\o }دJ5tw01C [5 3t6 7 46w'vqd%2v}Zcghۭ*۰ ls=vߔc2PT$C'6ՄM4a/b/䒭Fa67;oL.[!X&_î4z0C!}nR쿓@P%}Hv_&^]-}o cj;M&J)63&[=fTa?nj~EgJJN0й:J(]J}iY SOD k)i s'Ih#B*ȅՃ3 x!<߁No?V;ʆHNXD {ٕCj[Tbt 9 E_h4%%6C(%E%j8D ~Ha3|٣^^en(Y-%6JUY_gRmkW3l4E&"C"E 86nO$UJVk0 Hiqi튊B0:]aWWp7W|kzoLQPV1Jd}Cڎ0P \sMZ[C$EC^e[/RPBēyP;:]I-[ݧ{?hpDrثj&9ˋxaQ3^%"""""dÔ 8mۄAǍLcdnk> |/<.1<n0%%0tp^G8NmG(IǙ,:|WW/ֵuߔbH"""""%%t,`:a_`ȱ +-+Ѫyý>3oDrWMwseLJ&}ϚR KDDDDD|dFCgCR퐤֍T䎖V~_cևG$_Y= 85u)^Q_"""""%%{h q󏠦*U JV~ʢSq|w^_w8"Ygak=ODDDDD2L @+#D Lf;'vX-QH׋?|fdK(m WEoiL ((Y!й"J(PCk,Kct(fwH"qՏ8~qnn`J/SP|g D "Hm `_/gcc3}zs?ሸReWV]VwUAc/haVYwEͷ@rk ;q"""""⃐t,m2~oRmLqIQ"\{+8 mEL9D:\H]Qy1<8Ϯ۰c_IYʉQ8G{8eT?]M [DDDDD2K @Ux]Ώ#qA 8AF Kct y$^j*Id@Q&>Y,pfVUDҽR+Ӽ!%%"li7t±Ftaͦw|Z S5I?07n|9ﻧq9 jRK9rHگqzaæ mODDDDDL @IO/o5<"gXja\y>;!lz;Cz_~!"""""QI.Gy6}V~w8"=D1{C7/l DDDDD[JHA01q\iin;$A9zb3JHA褥y~"wv; s̊E>;^۳EL5PDfZÈs 68DDDDD$M7z.t!G|ߡ:Yjƻ}CDDDDDH @/w㖾,2^p|s%Ş+8c8m @DDDDD2C @DUM%svwァ'8AGwuƎO-!`O[o2Ɯ}ھd"W~\;~e!>lݨ=om"FʢJ %H3sc[u__1)("9E.pDfLmgaâgj"""""=ApՋ;PD\p?Cƹtdņ߽δoODDDDD"sfl=%WϼsE5vN6q`̍MwZLj;ܢ՜/sGh`Z_U;&pq'] 5HRPD^(dYGgP3pD`V6VK(XQ9puqjp(ǘ6pZpd us,%""""";2sߡděK77EDDDDD"O?:PDDDDDDDr"U+Xx {)HSPDF<= 9PDDDDDDD*cQ$vp!@1 czc)4Q`Zw"f$g ~!""""""O s""""""WA:CPDDDDDDDz`[c; wHoc1?;pJyl˓8v oEOwDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD+$aP@W`*00 _1{1 0ַDD π=I6i؆d2<֯E'""""Hwu"Lnxq9[^ t^e+q.""=B/&fgz m*R7Xoe}EDDD$e0 8pp20Ǹzs1=O@;]}(Z>^`۫t{\-/tl@ fm, (+ :-?''Kc+p6PO=|ĖkD^W4@@g\=sVܭ͵lw*"df7h`aϖƟ؛;_&6y_#;)p.,o{\|{&.ĉ69ޯ;9Q ~krH^"RȪ)~~ {ons@$FB8s.O}E6@0`9xhGI,qgBax&=߇I{$Ю+,6Ӂv$H.;Ӵd+wN(Ѫ *='Mua_#Jjzv6k?9_Pbl&Nl^u80\c<L x8[Eq3qmH m?X?J݄I;m={!"R@V{d 7|v@x׈/[$T~!"&af=]?c9}aJ}[oms lSD$/\-n0rы:_#L*%3s_SH^;(Jh8[*vNO*nW(H.Zĭ [.ҥc}awd54X+~ޒIn2򇱓YD▆hd?]H.wd5T(5>W\_vASD$͚/Kk"P']#Lh0.ps}fvŮxW,XN<.I~~Xg[}y<m)bq?KqE9<"`=4`M{-' ?FG*ma%^?]&։@E_/`ߟ{0K> :|/GS{p4p$6_|܇(~^.2 ;d ,gg؍}eO$ ؿ؅j؟H'C=p`a.Y^- G[,oOӧ]K.!X\bF`n]?~xOATga_z' FcV}1ŌƮJ<_sv/|cs`a߇A@-hzCwމ/ڒ#睱sw_C[)s"EZlsv_:|kw{ҵhjz~ӧ l~ 7k" O7 IDATɚ_NrӴT`?}>s>Q󵃱}vJf$ c~!{`ߓ S'~wݯL<{bʰ7?==ft۫b1܏,+ǎ:#_Nӛ`{`onv{`5b]>#xǻ&ON_`WOS;6 (챬e1v78Qdz=ݟH}o^u7{'r>.ؿy_J5]~qߖOC}߫|y {3O9V6/"m`~1+e+ȼG3]ll ~f`O$ލعwrg}>{ݮf_P=w6;`'^[ڤje~DWq{"_a*ÞS=Lnm(m,fV??8"GgydUs=Va;wv\]7'؄Toq{kp/} tA96Y4йDmM%w{{sCl+quI״t{[_t#G Y$ͺއl4{]9@boD$-oDn^|Qmk/tIwc68PFR6'zED C2l`Su O*~G$^,&˟xwcJ]F!&GE[ Uv?i{0Q$/od =A '%K7H/$'= ' 6aݗBJfzf>V{@3صQتO.v3Hep`s@Γ:H bVI%~GbT2u9v:,cq}=ď MDlo9 |]7zgEe_'Ğ=/Mt[wR`0.DKIFHFh]ء]Fa+;r"p3[T{7i vBחj׽𶗰QQl,;3menaثZc u5lUԂGP ~2st&@ =7pLV 4Mx^$Xl^uF[vٞ\M]l~ [ڌ=a܉}R@Q@A * ((w]u9'L.ʢHF 1 $I|InooNɬ z߅f ʞMd s<6"}%z6MFor" "xrb/r[6*whzއhgDP.%j|}ı|"pH;{u{qQث~)=`~DgXFfsb_Ƕۈ*<w0M qsQ|p~>C2wA cC|vZDitbڍgeD;{77Sn /#~f{%}XfuO\k3n$uc~y)^D-$28aٜ8&~s^}.%v{6{"0=8lԛs sURT&6Qgc"w@Ll{d}mN\/@zͷ~qx}oWw݋?֢n|}kMo{`;4qME6%.E1y7]kwy'^W69k!IsIS4d)I ۉ\Փ5f1KL` Qnr8Mԛo)cf NRP~9A u+UӤM`LڌN U((l=(Oa\d=D0#>ۂZ" 5 K5. |>'o5iܛ e_3WP#2od{(߄K<x(3ϠX=$MucNS{8p1qS?bb~oRnb:1l^Ǽl5?̯{6,Z4뼩yT"0{+YA,+q]龕<ئ뚚>5KSg&8A':AtLMjhͽ:LfU|tYm4]LvEp t˪OֱK &C\2A\;55~S2:zgӥ7>f۵|/=G^r؛S486n 室k|^kq7ͨ˟~@( X*-@:|}=˄9e o_nA472hQ) 2=2i0{]@~7idnMr6o.mmxꛋeMЅQG)`"˧.:mІߗL>*pR vf!2Y_X"e)gNTk1DRUMq~LԆi3|>`\15`稳1V4gTiz "#?3zTդv 4܏sWLcI?_f9gOvLA>b~Ok,^sN&˕˚:V E* z3^ K_PlTet'_ ӯF1Ku)4نb4,k)~3$>gHu '/yu'7vU,XH ~~(l/˼1)OtU (wPeۊmys9}2kPȆlATmOS*x _2;'寠6WGtXfASQ5=pۂaF653'e_0ɴ Ve:-8;UL6nYgͰ嚺FϦ]J}kVi q˶ =@lֶi9KSu.q qTDK7mdC[C (9y(qt(_dػŴgQb˙^A1HxwT$&ivn#P!= wK(w,by[&ԵD?vUx-#}vA)h~_X}زDW*n"Vc5y߱-AW2]kxeXF/ϻG[Q϶&<3yWP|p#ݘ^|?uKQrAoMǘt4ŋ@~?5K[7]=b1L0UD.|}wlٴK}L`{N ..&N5G]>jӴo:lJٴh.yXmXAdsՓ_~Զw9oӲMAڴꘖ>МE:#%J՗h?C(fɑȞg}p-.ۖQ5(f\AfÅ]]5]Q y)g>BJٮ#ʟG /3-+x\IOLnjKAUAɺc҇~Q[D3> zͷvtmS^?>aP3J"5ёr]t[y 2p澿`Z&Ho@׮j[{'K0_ٵ*D>X⨒mlϧzMƁDSt4꺋ulAdv}/v6TzZD1`8ǨanuǶG09/DBLw!4ou>s)7Sze6EqtgRg6ɽϺikS>od8y߯ {Df\:<8vɀ ߡm%3u͗Gti1l]ݩ ŬDп+ iL^iP7}/|M_A:S%P PIi]{1?Ժ'y>xc _Z]AmSҾl#n$j5VFS1]7"2E4<n6 Ǩt?F7Ǩlmmz!X=&@m@[S3~D{ ZD3֬͵kO]1rj;3R~9+s=9[\}R3qͷ kD~L\+L%L~Ĉӯ"crfiԨ-!2sJ4xw rW5H mMi?2g=W{J1CL!}3囧5ZD4KۏᝫIt~$ŧ[Aw|w#n]2[Qzx~aeӇ@?*<9z};/:8bbQH pJ\87̵cn[{F;"ΥCfoGo6vŮ$^{]Dtp,eF§'?!y 勰'Q=rlz18ʴa ]5S,qD|o9 O qD #5}忘[IQHmG1"n~|A8X<`MǹnYAbPjû_>A43fo6#QQK@$F$|nK3 @DC(^B9|}yu9 (qʪtJoQ}EU|͋ y98(H;KJ?"̍u6S |z` ]\;^izpO")L"Ghx&|ɇKIGU_(=oWo_{^DQv{=P̷!aw9ՓͥM{OC#8Mui_j+Tf-#2]o%OY6gk$50qެ.]%=Zs9[sW%FQܷ?KdݍiSa/7LOޯǨt=Cs>+K{68H=Dd{srR+FA 0Lڽ3An#3 U w),Nk1ml5y:,x~O1#ŦUStt @!,ݒZ '&;71ۓ].f|r~`888d6ftVٕ?RqTL1+TL;s湹WHMٕy8(l~6pTZmgu-t53aF%֦^0A4XxxXȠ9X}y<1y_ ugod4w;)^ʓmze qiGiQbSхDΩ[QT66(f*N&me0Ay~~q>H[YlGd]Me}ڇlܑj]YnIK(fxEߨknDq8f-hZ3r>|F!&(oLӲ jEtLD?2]E}zBʤ'ei9 MnhʙGQZ~:gV% /)-ʬB4^߄=kc[kS@`,(>&KGKN 3 7m_9r]m`G.Ô^h$gv".|ҬÈwPn/b6'FkuE~nlC4Q{tC=@5ӝKyCP܄hu'Q '2aҲC)6[]Cc6[3>:Va TqLX[e XMi4,騸{jKM  Aة"\G9#޲^+>ؖќbⵄ8[SG%yiU&.NӲg 9.g2+[|5}ZSfrV'::~МE2߮#:)ߦo7T,]DnC$\/Nʴ㺤\~ʏ Ov|U~d%>)L١oS1D@%~~r6D~%LlU͘"TDw/"?N}x*O/Nܘ,g7M}Q[x2e{'Gôw$eܡ3BbHZ8v} ~צ޸|׶(e~VlAӯZQ~*LC0 ~~~+uꢤă 15k ndWwc?;5C\N^v6hAoKS[i~+)v2LPN*}wb mIy;1(o !Yku IDAT-YTOY~>Cyg7'%>7(XrdmR@CmrZfȖ} |jMo-yFR6D|3oD\[g/%p)ޏ>;z1h 6%;k/fh @cUp+"{t0F.2y8!.LEmQH^.r-yL6sZٺU뱪y^y_[񷣈 ~)_eK;+9zٞu8>}6S@]"$nTVLӔ53[7ui쵄;[\1) EpMץݸZ|bR; 6US>Υ@7S \⅔f(Pb**gX晕=8##L%,58eC*+ u/Sr]bY' DZ6oG|k+~כ~oꯕA\?B:8?Y3ߺo4+[z&s%{|gSH7^tz@ѝb]Uvz}}fO'>k=[7`U C^FT7n{^Wѭӕ=yrrU6#6+!hsXګK.k˓s"-/G\S,)s$ckw'1 ΤDF j<4O+ iߝ<^mnS{?bfIä|ۛdM7dm.v2{I.B>[t؎|s)ֹmG&W&eԡ,D䇈ݦ}D6}nD45 4_AdfG{Om^Jt4ߵo'|s ٧su?.}wX*%zMۉ~1@|\̾>){D?K>CY`7~zXBd=I}n[plی%L|viվ4~(_K9W Jy]K[i}3m}6u9rϿ!=mGDkv{͎MduєjhGkjKb &{]BҤ~P֧>LOΈ^nۡ.ZZJtݑz{i[$L]%sd$mY/|c:hV_]HtҦ׶tK1Ye]xQwxڊؾ.)6砗rHu8aqú3q"[(x*{ڰlH4ݲۈiԏ4]e+? ty7jYjke#ӰB%Y|"85r.ۑZnL\$J4}wlކo$ndx}qѽ=qqyQQ5o;AfO#.:Dp67(ؿSV%mcB"Jo;\&$ 4 nD3L̙-786?C˯3AEqh cw[`6X@DZg᥆g-b]ߛɖ)7,?_l <&?U.!e+3|AFeCb[7qpϤ8J 7'2JW-<E$I$ ͣ(f=HJ&q]$I$IsW)4$M)Lw=$I$Iد͓KLwϣs$I$u6!HCf9h#I$IG1w1$i@H$I4 ܏7)ބ.Y$i{eF[I$I\sŀ_z&I+q]$I$Isvc4I){$Iip2d#$IF]I$I3>NM#$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I7 H wCιˁs0_Ά6*$Iq ը+![{CN|onYn-p!C"G[ p?`QWD$i揺fuWgP4}ˀðfY7omu$I$if|bpHk$)5.R>f$I# 4wI#./I4x(Is7+4$%I&U}7w^]=wͅvM?tg!.# \sm l,!:]]4I 4p/b_HˇT]v-1Cj` kbqy =r%lƷ_{66&_GBiZ惁݀M:74m}Kb`(~t ǾՉbaKe>q۞8D{N%C}nY^0ydDZqAĺʎ" a2-ћ߉$IҌ8ax-&F+ݩ|%n(חZ.äL4} y5aIoShYDo/}bikd&G=&nシbKujl"Zn'~y}VgUu5<<_zs򷵁C~Ҫu"0Qg-[Dg7<.{Q3529 f_|9n&Uu^Aa`._ˁ^rW{}?z;bi~:`>- 8K>AܰlB;<%{'uljkm cMÉUqok(wl|؞Lb9D"^kPj$ڧ;&?o&Xb~'K[wӝbp "f|OݪM}`u|2+&ö˪^CWU ~qqsl |qxU}vM\5zbg{u>!IUL qO}Ǻ-$uYL㪬 | qnAm,m&v-7%1C;+}m<xoM@62SQ Ct3TfHyL5x=qeVk#" إli]F+7wD%^uAa7:Dfo(K}tx́ŽWV|ȤïjXntgnFlsu;LjqB9t nFqd >w4x UhAxS==o$+:Le/ 21A^uC$IoLe#b{q2mڄ& 45ɴSع+Vl}mMrlO9t$}u.߫C~c? >je<8X_H1`){'tlom]DKz/A}v5qsk>TKhpً lFlUd~MwܛND4|;V''Is_[&tRjen |3xx1| !~{/&4IxS} tYʯ;-$e ͭ<<}̎`l9m&/n"k:?:MH'oX` yovOi6h'=fiK>oNo?E\W?KICcT u?N7ѕB>gY/xzuO-⸞b$IR)G˪c)bnDs֥ؗ t]DQ9OuzeJ1]7e݋QSvJgAک?P')֬)㲤QٔboKTkl'^VQ>7mQ7g.'&[}i>mDɺ~#3OOSKk;&@d|ϲz{9lN? 5)?i x!b3?,""I 7пY*ݧ )N9 }'}^ѧ VoQ[:j0$G9K[}eh I$M<ʃXe%idVLd7C xHWfG4 dҮ/(f|a4o̰ߢkB5'|o2whɶ'@RiU@R wvR- %)w^r`b߶I{~U{XV}xS|1a /;PwfiL  7n[Hq*g70XYD߬Dd}6e.a/&L9%mYbe$I(uDôQMfoo*匲LoaY.>k*ئc0o̪޼}6)הqu/A :9Wi /1]2I56=$ C FN OA,$gKdڶ̀l9ݫ[M94v e6d{q]&ӷ"4g\ƎI E%Ӿr+s$ijɰ4CPFLS[>d10;;LFY]ڏlDii[Ձ:^e~M2I7}跭o9l"mbʩY7[\ߎ@s0+]V7iĮob[(SPl:39y?"qiCsuxmAlHlt3`>yC֠ I4tX<:zb.4-L11_&DutpkuI޷Tw\.ҦSM}Mu.m_wSq)1Nf-qfkΣdZF<ȫ~'k4S7~K97? Դu;odc?ܳǐ;MyS|h1 b!4ütF$Pl'm/tW7_dr g*_JM[Wvf4ev b[b].jx-َED3fcb4wlj.YڏLjٳ[hR~_Հ7mε})w[p}~j37u9֧1v)N%]$rMYUҠҦ);н-yWbrP;k+>0J/?N1Ozc7]YL5hRb .cCCsB Vr"xr41pƢ[ @3mz} 7LPv.86{q. ئ!KձnUD `Q!=, ۷Nq:\F)}K޷ \ 82ΚOq_D,IV5REQYYJ0w#?:ӥ_K?fobf579g}QADOMGmU߉ ƦA3n#[ fJ:M#h}hCv?A}VszmYÁg,W/xGg2`1Xp:.gNDFQC_$!m3fZz\M}N`U(U瞮fUCޯ|R{b]$tHJ8;ϿQ{x.`O:FcA_9ۓ+c`*btzw#q_fl ?63='}h]:e쎢ady>3ʭnˬS}ؾo%Z_LScK9C:Ӽ̗{ "y"@1S5UM}^F󍬆g ɓ5^HoTtz࿉&UGgr5}=)jwif861BqpA>< I'y~J=w0|?C7IX3hVrK'l+s e&ae%I,d`M4 mgw(6e#7d:&7Qݦ:n=o܏t:b?1bg:|b)'LlWC?(6͞;8e8bV;TL:qlFq Ϝ:H˥33[s>AD-IDAT7,]N:^|9ʣ{}rirZMPPMBbq6?2V'2h_Eol׺%L^h&'٢̓i:ڗeZ|x4 "6'Hd#g\{9-e;' <- FswooB~IozM}7MJܹMی$IRf#J${Nɚn@4gϦʃlb*_Nov%O}꾣V#2ǖ'e.[vlPfUy>dkQԪu\dwX͓&mQkQK-$_yطO&} 7x.zõ; )uW>ye5ӯA<@Kz@E.݉[QʧǷ~d:, |4LS2$I M+_k>[/MM½Nd3#N#o&2벀# ڔZ9/A2DId%>{6gD5D󫍉lJ@?+:|MzqC䳃 ѡ㙼za&87 g>{5L!oޗlO"[6hb<"]Hd-Yo8Rh!T#^F^dPfsboY~}0 ivZD ^2l~%|'˝D2uL-q Ӊ>dl1xthb_pٛEv7v ORb=Hd%~3tŝC}ߊS̞\ |z9q~xs߈o?=;O _DN#BKKtbhM:.v콿Y}qpKEzbݞ^3$I4]TUfC^Ls60)w+tf~xi[EkqC=zZJ1{Ab⦲LgB jQseMYJ)5efTpz~3|2!׷Nxd5 @Jc$ 2id}Uy)1@ T7pZ@ yN]ssSYzMMg*ʎs qBR"`$Im@d/M +fi?lG3Ȟ/5Ӧ՛YDtc0Yu%DSf'2gFmj BdXSפҧ\&>XHdeW0LyhŸe !?o\6é=fC P }AOMӘxT!i۪W>-nHt-/bd4ʿE{\݆^LMI49Ҩfږ͉77%nO8pf7ࡹ7)gj}h(v%\jDyMDP^ݿM4Uzy,z3 DPMOڌʎ7)tQ!SO/o6^J;oJdWDmMx gbЛ`)v皲%q|W_ʧMw'߉| )vYm9#LE?Ezp {u*_Ӂ$I9/l}&ٖLW.8I3hٸKhU gR鿆[EI$ivHSi+iRYF\iXEDf`g|֥k I4 8AaX)8s*"wTD83h.d#If  _]6bh$)A^D~hx]6|$IY-m4Bf[GfPxh1o詫G FƖ$IV:x,1Bm/9IxG%8i$Ifpl,#q_T;ĤxMz b$IJ4 image/svg+xml Measure Py Scientific measurement library for instruments, experiments, and live-plotting ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/images/PyMeasure.png0000644000175100001770000002066314623331163020041 0ustar00runnerdockerPNG  IHDRQP]FSsBIT|d pHYs[[tEXtSoftwarewww.inkscape.org< IDATxyUSӳdLE B!,JHA@QAYPQ$n{A% 5 JAY$LI&d2]ם%ydj?|`0 `0 `0 `0 E U>u6 lJmS _91 BtSon|uO|w[a0FIUiK9>*4 sf хl+14ŕ#0H*D|l1G72"^)g@Ow+yҲ?|>|=>!m.kyq73՝<ΣTs3к+BifQ][5ج%yn'Q,?9`ȈQ3u!35KᢻϣvVoPhmU**ZV35Yiu7Wj_ʱm[ ~ ~S?mڦ<2BT#Z yDfq3B0b 9Nt4u-TW$o}'zi ׷姑9@, _ k"\T#ݾAPH csp$ PaDBtOSV2 v۷ݽ?`E` ٲi[  7s5T'g4aL ї6e=b|0j^mcm-B|r?S3C+4j!5˃g s2A<QǘZk:;c΍a8~_GdKB*?Sowb[cOVHʹmuVj>k Zl^vE[?)ƧBfq4`#75`qf]C,-O?A(ԆӖV c$o㒶Z{۸):J,R}r۹od8x c QǘD}i~64i5Ət).(bq/R ISui|59k/kټeI(~qR(8wږ& Lqh` W~0i_U-3)l*´'˚OhW~`qPM,$/žRTQ|kx@٠{`L;:9"r)-+`WUm_}߁*R|TV}#53?aq֚7!HpW7ƊZ˧N8P]K:.zn+ؼ퇃8ű|6pgMp̆rDDlF< <8^:W! r`!|T :dTs:^`=ҿ<BWFpF5$n*)y03&^LK.zqoʢJ.Y kyc;(:8HYOSiQN{X5>Ba8O!9ÿ!˗"BN#hj Q #`v?P |1 reuRǷ#D`)% 5[ 3d,q,̅W`+\ܛ(#BkB\^F陵E]&&2&~(.L^*,eYS?s 1W^Ǟ-%Tڲ)z{ rXoTҌ*ۼvvS[3< E8֗. A+ ш"ݩ> {2K8xd<8g_RJݏ}x6VW,wǜ]׸["D,)CYҽrϞeyw.X{z{\t}U5/yRjnc85!yf:qpk]&v+HD2a 2tYvڄBVdX8&܏d!޳V>GVonㄻ89eEοjGtÁZ0APH=sA>~S<6"gXOƜsW4`IoZ+R~LTT3{i+m[>:;CzQ 7l\.N+,?1˨P3V)JtCC4Ҩ 9cb#rlu Q@ 7 /M bw "ȵ ӈݛh{đQLC'hD8;v.Z LeBkg!wSx?ElN#K^ELDUii*{w 9>8U:c}fw躍d˽Ɋ M:ݚ&G|̺geZ4( u?Yn '+?|aa_J|H\O5Y?)FTC̓ w(8<Ԁn&'v؎ 39Y)\˩MO*ľ6Xʵi _jlm kx݌zu]Mwt4֝D[*9q-.g6݈葖azF{C鸀g@Vug) D< .@L||N< y!jZΕG*TB 8I5a}B/"#v=LGԉ~{V˻7^7d==azlzBal^'AT\iŸ[aOi(+.<ʩ9Zrj}mT{~ p)糌o{פPiguRY,rMgҡ2 sTCnk D "#{$Ҥ=p)*1׬`覅Hf\oO[ބh(WV߻(J\Twٚmm]6EaSME(k rɱ`in;4h(liTwek ]=OU[[^yt#qn$ZV߾~a1 'tUl{?, /{O%{-$@gߏ;>Mvm>7F$~u\Ue;*yv6s-Kild#5^ؖKZ<2﫯ONKDJJ->zСЯixņǶ.3}ϲp7,\\r%)Ov}*, 3Ed'/X"8T$Vɪjd6]8%?gVD  #Ow %Oi "[^\rϿO8fMw_nJCCcagU_rnXܚ'cAhsu40#Qsg[.ܲ>MDmo_ٓ?~uO hNm/$zYFB^BlFL a[hZ 9?BlWy5@"Ɉ#BtKs(=p;4[Z֣1.<*& G>״JSG^!߱saUsgM8 kҶRuG^W<s;" /,)?-ۉz2 *c%02pgMȢ=Gh#~xAnzU$oMbPyke΄h_Wv vRRhܑ"v5,Yx]9.HpS*: ފf7tH&uvRei&M"DQ;;HrO ]s*X t+b9 9D,]V#KO#6ۣK-kynpv)hKNIDpYVc՛Gn)\8jYSG[aûWZ9khR^0B$s%/Grt^/F9y)/@$'|Udq%W$B@yJfv(i*gڎ\yW8EXxF NVT4Jky5d}!>i9:_G>#f2&x+Iqo)A8\CF:NMn^>/!ÇgMɕ&jwk vRPžm93eBM¾8yY N6}m*D+ΚrkՁJ8U^ i;[H #Maq,rKoHݓ}T܅YB`,s`6Q۶*n7c v | |J}%W񑙳U;_2ꦹk1' vbP6[)N$u4@#{;Y;Z 9qMU 5=wkn[UK7&~Ҟ7)]=*kw~zd{)v&c; TYڭ 2m֔U 4 4 .@"P(T)]^F<峈[]ŚYֈ>/M-Dfky3>)@4^Gy^L&$.%Owo7"@Ѧ5yfb+9SgLƕWe,@-bf|7ĕȓI|w!~C~YAb&L+$(^2qx3EH2{7I]RЋy52i5q-s{J#A;2:f$SjI3%kccH}S"! 7$tOmYj&p0~lŃ*le߅V\^iZڐ禬 P>ߐ\s'BٟozNHzi&hėw4]琢9S픽 ln),qrG2e%OffQ;uRIٺ=σ#ɩ*, CğEH|z q-L}{IA/g"i^Ðѿncl cmXH_XCF4`/4ο9H\ s8#!saXBl%NtU_hGx &֚-;'V9f>K/ } PTRDqIQƵA Q?U<ݚ[i*)Z5p;q 4i@B9G @{q/@2E엥*1D`KNRr3#Ǣ_HPR ޜ܂hN|Č$jx!莛.^E>*' A "h υ)y]$TfBD:aQ&GUu JK]st0!ŗ湦:兏eBlHukՉ{mG4\mG{d9ՃLD c>؁hQM{8EIm0FN8YNTNWl@|MMe|s'TA:U#Wa([t0i)?šc s-D'ڹ܅}#ŬMsdh^`m G!Zfoay#5MbnMt˛_CbSkR -΀j^mBԳ9%oDufzi=c*˲9qut}k>=짴LD5j"Û V_h:F`Flg'!B=GO /aR$խ#/%q^7v"7 AOl[> 3-"#QwmolxG߫tf /gV/6#I$uASȽiN~S̜ 񏷘4QYo`qlSDz Q9?.k9%r$y(dNI0FBs0!Ch\g#-\CSCOIљ\7h W~4.Lt$] EL'YSBI~[ClG웲9I^,%UMVҶ+$6XUx4wd2&BSX3MAtaWw4K A ksV ǠUA6:˕ևtzٱLJmS _9ҍ1 `0 `0 `0 `0>nq_VBIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/images/PyMeasure.svg0000644000175100001770000003057514623331163020057 0ustar00runnerdocker image/svg+xmlMeasure Py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/index.rst0000644000175100001770000000523414623331163016012 0ustar00runnerdocker.. 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/actions/workflows/pymeasure_CI.yml/badge.svg :target: https://github.com/pymeasure/pymeasure/actions/workflows/pymeasure_CI.yml .. 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 Reference 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/index dev/coding_standards .. _about-docs: .. toctree:: :maxdepth: 2 :caption: About PyMeasure about/authors about/license about/changes ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/introduction.rst0000644000175100001770000000532014623331163017420 0ustar00runnerdocker############ 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: https://pyvisa.readthedocs.io/en/latest/ .. _Prologix GPIB: http://prologix.biz/ In order to keep the corresponding numbers and physical units (e.g. 5 meters) together, `pint `_ quantities can be used. That way it is easy to handle different orders of magnitude (meters and centimeters) or different units (meters and feet). 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 Example The GUIs are not restricted to the instruments included in this package. Any python instrument may be used. For example, `this script `_ demonstrates how to use an `InstrumentKit `_ instrument. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/make.bat0000644000175100001770000001506314623331163015557 0ustar00runnerdocker@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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/quick_start.rst0000644000175100001770000000465714623331163017244 0ustar00runnerdocker########### 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 Anaconda correctly installs the required Qt libraries. Download and install the appropriate Python version of `Anaconda`_ for your operating system. .. _Anaconda distribution: https://www.anaconda.com/ .. _Anaconda: https://www.anaconda.com/products/individual Installing PyMeasure ==================== Install with conda ------------------ If you have the `Anaconda distribution`_ you can use the conda package manager 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 libraries. Installing VISA --------------- Typically, communication with your instrument will happen using PyVISA, which is installed automatically. However, this needs a VISA implementation installed to handle device communication. If you do not already know what this means, install the pure-Python :code:`pyvisa-py` package (using the same installation you used above). If you want to know more, consult `the PyVISA documentation `__. Checking the version -------------------- Now that you have Python and PyMeasure installed, open your python environment (e.g. a REPL or Jupyter notebook) to test which version you have installed. Execute the following Python code. .. 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>`? ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3776052 pymeasure-0.14.0/docs/tutorial/0000755000175100001770000000000014623331176016014 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/connecting.rst0000644000175100001770000001352414623331163020676 0ustar00runnerdocker.. _connecting-to-an-instrument: ########################### 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 VISA address. For this example we connect to the instrument over GPIB (using VISA) with an address of 4:: sourcemeter = Keithley2400("GPIB::4") .. note:: Passing an appropriate resource string is the default method when creating pymeasure instruments. See the :ref:`adapters ` section below for more details. If you are not sure about the correct resource string identifying your instrument, you can run the :func:`pymeasure.instruments.resources.list_resources` function to list all available resources:: from pymeasure.instruments.resources import list_resources list_resources() If you know the USB properties (vendor id, product id, serial numer) of the serial device, you can query for the VISA resource string:: from pymeasure.instruments import find_serial_port resource_name = find_serial_port(vendor_id=15, product_id=0x12e5, serial_number="sn56X") 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. Instruments are also equipped to be used in a :code:`with` statement. :: with Keithley2400("GPIB::4") as sourcemeter: sourcemeter.id When the :code:`with`-block is exited, the :code:`shutdown` method of the instrument will be called, turning the system into a safe state. :: with Keithley2400("GPIB::4") as sourcemeter: sourcemeter.isShutdown == False sourcemeter.isShutdown == True .. _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 (our most popular, default adapter) to connect to the instrument using VISA. Passing a string (or integer in case of GPIB) is by far the most typical way to create pymeasure instruments. Sometimes, you might need to go beyond the usual setup, which is also possible. Instead of passing a string, you 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. The Prologix adapter can be shared by many instruments. Therefore, new :class:`PrologixAdapter ` instances with different GPIB addresses can be generated from an already existing instance. :: from pymeasure.adapters import PrologixAdapter adapter = PrologixAdapter('ASRL/dev/ttyUSB0::INSTR', address=7) sourcemeter = Keithley2400(adapter) # at GPIB address 7 multimeter = Keithley2000(adapter.gpib(9)) # at GPIB address 9 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") .. _connection_settings: Modifying connection settings ============================= Sometimes you want to tweak the connection settings when talking to a device. This might be because you have a non-standard device or connection, or are troubleshooting why a device does not reply. When using a string or integer to connect to an instrument, a :py:class:`~pymeasure.adapters.VISAAdapter` is used internally. Additional settings need to be passed in as keyword arguments. For example, to use a fast baud rate on a quick connection when connecting to the Keithely2400 as above, do :: sourcemeter = Keithley2400("ASRL2", timeout=500, baud_rate=115200) This overrides any defaults that may be defined for the instrument, either generally valid ones like ``timeout`` or interface-specific ones like ``baud_rate``. If you use an invalid argument, either misspelled or not valid for the chosen interface, an exception will be raised. When using a separately-created Adapter instance, you define any custom settings when creating the adapter. Any keyword arguments passed in are discarded. ---- 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/console_output.png0000644000175100001770000012401314623331163021601 0ustar00runnerdockerPNG  IHDR@sRGBgAMA a pHYsttfxIDATx^Ql$O~yg${V+[UnH^J] C;~Ia$  F' $  KHP/'>4UζR3FNVRo'YȌ̬̪dM"##3#~9ۿ۷՟ٟ??5_7yoW~KO$Rjfffԧ>e^3|I>߾~~[j?f0L0 n8A"@eF2PO?g*C(TQO_u?ࡘ/w}[ܟ?PҏԿQ܆$O}??/2~s?>Ϫ/TSmwcff5$Nb[?5O?'2>~'OyoNlWӷ_ 狼7d"uv_k?CR7YF-_0w+?^S_? =/Ouq _U_U~!x`~6ec%Ͼ^w'z}ߚ,z3_S<#;\T`OOz NOW~%{\WoQ#v9ݣ 2%:%Q֏؏yqo ?o.>G&m2f}Sk^^o!uaw~W:-/Ͼ]w(̶-}K| }'F< u( /ϟvm^|>OjeeooGo~W}7:74f-3_Z3;MD}!o-e.z4X_S-}n~'!_VLfԺ:7_Wtot4t "%Q_W[yS}V%"d~0ϫC}[s(}k_W_1o^W3_oz2i~!4OS:`/7u:~o_֗7C= Jb7.UoH <T pgy?\>jgg\({'U? [ȗ}x+&g"CdHZLǒO~K4-_x%t~׳*:V;3^0iClRU5gTӧOՏQu?~LJzN93Xpdy2}sT)o|)u`Q9Q޽ &0WU~Vׂ`Կ,/sIYv_V/}G}r("-`˨T;.BFlw3o*EyߌM ܉" @?g,\~~w,GGe% 5^_|k)T:_$_?{2)R32T ]&\3ax3s[}Uo UJ DYUr _`s9ɷ-?P;/eѾ~>$~tCoZ i|74vE>5u >y_ضkbW2K߸P~ޮwI}+2>{I|;5p@l3?wn_M/:T7|M> LouF;_W/SO {`p9?b("s0 W <F2PQ (T`*C0 !@eff+[1@eF2PQ (T`'?F0 @Tր(@e H}zd<}g$F pJOB0 OR3_|wǏÇoŠg*C0 !@eF2P/>{v;WGkko{)=PQ (T`*C0 !@eF2[VWt_mQ<_* Fmӫ+u53]=[ffԖTHt-57s7f_zMPޛ!0I3O 0mퟚ:T8˃r|q[?6B7N#OTQCuƺZv(孤lPzff~~V+tM?7{$]d(?ur|9U[yz{==b^&U?SEo#nƄ5/-smRUqJ[lTzMIi'kҮG~}K`9UYWYDTM^_nݩZvwIz'\UjWqlVHt7dR%yR׷316 v=vjul_Rp4Ct~pM/9IWWz0u:\mu쿁2>VpnZĖI},׋5?iףi>ޡ %l_z]u@mtUキlnǦ_lxYϐ[}B ,NdRމ.|`/_~`g_N<%^o#=e ;ʕۤ|<1o./<޳3p׻+^\߃ce+G]'lҎǬn,~9u :'!Oye7leŷ%^9~;ח7%iz(#)eU_Ǐ~~Ϩ/3gOqPk7/^ݢT}%}؊\qAq'2?IBWfڱE#͝Z~){Ƶ[^ɦj }Y2Lԋ.G=0V~mw_wxEu7Uu\m%6p1b^Gы4O{dc|S^a/v0ɚ>g^^WC˾^p fQOWQyZWǵ^g烊ܰa+Oc?Ԁ4LP~:SCjR7/OhlƇ/5떞c1(oz]TAPZLzLCR/P_IxD˟b{e_hƆNKO;3Oܯ?]?u{]4w0{ޫ?,4,kqM ==ZM5ޅ_RެU7xו9J.fJ8RyW1KX_ IӐX|^C<""S RVDz-cpXI\]aI8ߖXv-oyӓU뗻i/Y\r﯌wOg?'x,SeFbޫ7&%G{Gz 7ɲS_-K*\ӕU\\si44:Ma~].ԦgBڝRχn{u8p\^3'cWl],:߼/YokKy|c6Vz2s>eg! Lp~cҎ*=]ڌts:j6ɲdϠ翼X^o^9xKTp]3z)$Jރ u'(w$s7'=+zP؃(W >q܍& v~]ǃrҒ.2[iud9y\Zwm,+]u{Kg J7r8,X fU_ܻ"u|--(Sz2wQ8J VVTd;u3apqRT׭OOVyχ|N+]8_J3icEj ՌhGR'0C'1롪^g;`ԇWU8hdP[nCӟ}z?Dץkaa ..ۺbW3jGntTk>ۗ' ITeK)? i7ͻ.~};au* A7K /a{uTX[^kgU|^ +i.G^BA``c?>z],Z&%~(4DdGK7w[n5<#Q0~6f^!zBe^'IXKx!aktݕYzyFFׅٷ7lg_V˓Wd=P[`QݑAO;eHKģ<  Vo~օM)ӺR:C ;};JO/OZ#e`?EJO{2ƽogBqnsc$k#a'mʖ7Ѯs_oiuXza+͜i3w+ 3v|]eʽhYmO;lD==H~ xĄ`fbLʱ&v~Ėp~s4z7rzfҎ*x=u>žNU}Ҕ^(>WdD^U*z4_ӧ=}λaBz$Fef~CY㏐O#M9*mpXGxBmcȄzJGa4dAJMd?۟oBzSxhOJ z)m/ ޔU;<͵Z>)?ؿaCK ϡߑ?^׆^$e0<}K;?)~o][xҕPŴOWy_1~J޸3+^zKy.8&ǏՇߊ\.;A^Dh/*AUׇы2^3IOGQO3~w ck6 Kڕ.e/>6.ߍ\);pMմe>cʳYoP3|Į5ޅūH:LmWM'YvݿERb=e p"?WϷiZs Td)e&K-8~J) #%/Cʵr[>+u.%h]-uem%.&x")?%ׇEYy^^rNSv \k=hQw!bڤXd{q>2?}-u!&`{Fql&Y=*:1]uv&xঽ?4jq>$|w=+LT=$8bQ>NhOL{3 QTl!& ?zF =0F2PQ (T``*mӭ97<4`,+uuffwﯙ}u>6~:U[s?N_FMGOw<}T& p 斷4&_N{4nOrA9ݰ9Uڒυck~1oGӡJO60<PIM_y4*ܖgnt6,oQIR|#ZmNI ؞ױ5#^Iu$_6eVA Hqn`lj5Uojˆپ(;iNL49Ի :] \4icqMT7eF=|Cgii_j_Q}!ڸ} ̧,n޾'dq&-Ay0vCw`wJL}sN>l_']tʚ{2L yV==Pϟ?7~G7(=8/:fiU_X2Ft⢷孾qm3僚gvsGyyOAgQ^f:MݴIj i<}C7Z)j[zojN떾6ɥT*]'_wkP]/={Ǝ|G'Tߐk\mٖQ%P e^gWί7Z3hTnWu8:3w-C~Z%WuZA%\%2(]^[3StzW HW׎tq}~vMK *ʵN%Iϓ+J'GWy'YqFzY7:a#g re$ :$ 4X޼ ^m,ٿm8R7.~;u*ܔEAﲽ.?Nyp_׏t_>G߰2$z}&גb>6WTgcMrM^z> 865@& FIOΎâg*ڈ33sUW]r/YX@fi{~v9e!n^6=JrE '<); 42Gޥ93'.,=V&2/zJ1.mk,% kM%_6(c3W~\Wo|3޼Tء3uQcɍ.L+JD͡0]:fx~ϨT{ K;I^2?nԶU [KdZ6#w_9NƺΚ?=^- ܩ5y鑊7-R]Feb{;2I9dRy+e S:қlfe'yo|xO +4q I{/st2iEjdN4r>[PZc Wʙan[oxFU >T&iNiYmCeL""c? 6)|~?a>2TKXG΂$aG~9Ȑy㺿˞2|O\x<7~߬yUŻyo2u܃wqSL`..mFP-Nn `\^U:UȽy&:~T6zzHű7TrUzI*ͤ zFu/lp^BQ͟T7oU$%^OWMG" A=tww;:QKLH0i<n!f7I(Xz 31o#=edcm;*#?6IkfK'ea^QK^{s FyjakS~\WuӍx{ϿoX1C.M Lh0PXjKI"Q%p޲wzbt#Hy̝;ɓ5*=Y3 B 3nQYz~N kF[hO'gO]{mxP{^&_n$ɼV7\ȣڼڱO/~{Ȱ7FczJ}-yG24?׏2Y0S^m:ڕ^/05aysI!A``5M9y,\O/'`VPU LIzs.[lt^wz>3dg5є2/G=l/'a)L`ޔF& !{ۦ[in6y4LF+ a};ɶ4闉7Od8yҠ<{vW7~L i  fLϷyP|o1|x_z d*#=0A?!͓JݽzY//s+VN$)COxEe`6EwX;_r)RmsuaQF'T62+?Y#sogvvyFL|y%u,J_V4=^YsI[Xvǹ~GbD&07A*:7Ԩ?hU8b{1Ie,)i1wY ټѕqQ %)7b=|V1fȚXwXe33lP~/!;M/o}\Gҽ+C>?LzWloh:~1x>H)oYu{r)N{;7p==ust?4aQ"iC'!#3nY1()$;> z3>yக3 IbzabuՌNd-;oXK{pIwnF&y{ +pyܮZeX̛T|7 o(ijO42oۆHp)nQ^vvtӱq1YԉYN Gݮ>Ux̾W5g?'C.rO-`r=}$W: 2Tҽku(N 0S/uHhϩ> \>e݃mu>wRޞL%Z yۓ5*M>W#=!ƾ|G'ޔ69k K>e[{T2 Q&(uvU{S>o5sGL䶪O`eRtqtfcOROr^kUσa>:K^eQf!kūH>1# NO.Ê|thqSjcrݿvT0VS9.=3]1/[,xs8Ϝ#X,Ӓu:]e5?,oI3"8cjrݿeOo#U^6WL_RXS׼O8.M@-P`R`l(zkC.N{ġ933O2{M}'!WwaّݿR3צr5gPI.hw7Cd">1eO"FƱw}^]z>3zD{l+n.Zgaݞ0TVru (Xy+ lsM͝ﺼ+'S>Wn9yǻK{ }O؀|^_3KDi58G\ϲjH꽐k~]~u?]>'z*|~ez}y!_sʽy^StKyf&tx) ӥ6r=*Ӽ^ٰC8%m+_j<{kCưq҂>8k{4piԤEGN޴Hu+ዔ rȤvW3A+]M6N SOEڼgTt=@AWhF'ֻtFҟ !CO͟_V L JD[.wo½E05q}%G"9P%=nҋ@'(}gxnfj|{OcloppZ%%_&56:v².牿Fʺ~FtGh5?*=?'xw%q///34+U76ח)| Yxͳ,v=*3ZRypYnY{G% SpB޼7:vٻX ӥhJ|bzaC*'Ct5}O PST Qck:9y?]>'zT5^?8]Cx{ϿoX1C>V4Ukf,%O:xEJկ|:]SvO:ωD~r0Eҝb'JΙ^c,`Wz ըu )zwM^Je{VOF'T3赐Gyc2_l4MH:'#YǑu{~z^YzLWs\8ɖ`cLk _|r,[͏0gWY>1ǻm8 (|[er2[rgףbWTwZOlQ#M)e^o锞hVL`ޔ\&0Č9٘[;D@*ʅp؉N-~e"G(%Ae'N4(>1OgXDM`]eAeHKe糪ͯwY{rFM5vt72gKoׇCRWSZa7ٺa[=O[64*=o+S̹?+0lLVyxP{>TU>vhLMLZ-K+|{ח暟U2ڿ%OQ׋IXeLRZ8?īo-g*Ꮸ/,xZ5Dyr{+FIlox]N0^g7202uy;,oDM_S-!B&Oh觿~/Mb먴XZ;\ɓG PLK]u+|7~Ѯ7 ><ǿp%L"?. HɓL@*^yb!`/oi~1iʐIz߆j?x˅'dMkצߞq8 g9WIEU#|>L(kTxזv:\YORU̪|KO #3i#o9]\_R,u]>G%_/&>c+<_,N G^$dF+ޏ0;$?晲=+a4 Mo}EJF14z"INNӈ JOTdV,ߝuObb糊151k<= /ܱL?P |n:Y#\_*R1>0}<]M=׮ywsknpTU>'}8޽"_#e2Ϸwu}KNϒ[2˧$g V|+FgEO+g.ǦcEfzcU3;>|i,I JtQRwzǺgxP̓el=-$ ^.f$zEECd^ ݡg`Ȅf~3z\)T.D zV}D($g(T`*C0 !43OښLSu5@Fc}5C_zoZ޿RWW,~>|VH(s' D }ծ7j# FVW_׀F3OR{%6\t5si\c+KF:?͟TMվLɣZ ?BOYiZ^?%tuw6\8y=@sk窵\=_mp8Ns@F/w]lӻxy_귱˦t^߸>%A;(cn3T`C=qh`+ke7A, KިՁX\ֿqTc}pAz~μƾ閞]󨟎Ϣ*6 FYNS7-]]}1jz?<~RL(~H珔!ɣn\1=z:\BtSOJA=Zcݡn$=BoV:d]uxT*YMO-kfҺ]t7CkjfѠPQuw .Iz<7^W:v?t^R;ɲ䏫$tE8J>KZʤ%HdSO#/gk?V8̳!6Jӟ;O?oMO\ԕ/? ?<4mׯs?!x?eY/?NlpIڣsվ_NYnKy7r>Z~.<\w_ViWxUp=xHɺVIL.}d_mIZJn{4k<e` [ 6̜<?&g_r7҂w7lpQJI7/_`'eQ^ ѓ}k2ǤC3=RZq.H'㕓m|m& K*ܭf! ԋKowz$K{ `fg]nl; y 2v;' [>7~e{Ie;[wg/?\7Dr|_ïG^XȞ͗?SK蠏{8^nzՑ*J&7^JJCzLG{ek[{ vncv5|b_dXZcgMןzt=NG(:=RqykՊ+&#QC&OP_Rv 2u_ ɟ4-u`{sy(1 #+AY7L>j^a `Bf~6?̫d{pt{ap l{AweN5ݽn8lX6/Pys;l92C"{ɉn7F-N0 d'?&-f;m:v{ߑ8*'YˏT~s=߹|?َ<<NjQzRFoʥʘuY>OOy뉮?\{zx>}Շާt{d'}\<]ڌteHc~5 `\^U :U;_&ްj4&UOOS8ʙȡ1cÕT^A7v^n~Qw 7\$lyg֪{koɶ$[zg"XS㶮 ֵ6HM"VqlgKzʾ*4鿽VGZ!@j{cʃ.Y_Z݃ذqZ;v`cY74/t2m!yyj\r?cg/?Nϸȟ\WQy/?޾aFhUuh0JĂP#Wx7 읷^)Fav<9ȏTև=IL H_=*=Y3wCR3nQYz~KsIxCQ)/3?j$ C2u *؊ WQeo~/*߼U*_TBOJm\-SdYA"a{V/;7ГWb(-\r\}Q9ҟ?޳'cg_UO+NjQ-Kz\w~w#-Z=i0G7=Op#V7q~*|n_p56'`[Lc9{z.rku5TIZUCC?Mo5ߢnfoL![v찝a3a!h&37}b~ Lx{GW6ص =2}vOdQf ROmnoVfxGz?˘"ҟ5=iduQX%*xrqwEʣ7$"|gzI91ӑ]`q|XhQCD>~]zT?eKn?6@ΟU\N=9{2I>^2#n=d2hJG-.'a)&g87O"^@f8T|G,Ml/NdȣerPXxIl[gR0c*ɲ -RbC37Sp=};֬Ok TlWz $aؑ+{'me4]r~(CO7 {g,Q=lI{Ғ-R'D[\ )?C-qe{| ;NR$ծ[D\d?t= 97u! PͣG0:^ؓӯ4bB& :1k%/w/-yxBcf'`O)}>|id  tZ⩩ե1kg^,d2 dhYпȱn>im{#<,!4Ӷ&YQ!.S~Ү/y8N{|e=N@Tqs^ ͛?y_A]4fQ"iC!)!YҺ iNPv=},{Or+6 ڑ^+qx(L& <wmҎGGzN_[ut.iI>~>|VH(w/,S-f.(Gm^W]5OsW2ol絻#K{8(F(`w=M'ج{[Ȟ-3o"52W(6 p5id.Zr T1sPv=AfD($g(T`*C0 !*63Oښjmӭ97<4pF}5C_zo򾺺z q DDe& =}.oXFܲZߑ]X荒mu~}뿓̜ZQ+:1^<\_Qy%7||X& iL]6o!^ u}O2AҸyV&)"\w.: YtvutNoJ];[er26ٴu8Gt?~T>|+FgS ԛmu z[WVfR/7Tte o'Ûd.u򹴊\鱕f#\1 O*_j_Q}A-τޟ} ̧lu-::;j.N'A^s|5U(cñce\W+]ޗ[\O]tʚ{Ѻ@ɂ`)1z{?׍e8wzs0ӕٍcV%~o{sW@m,.z8Pk>`؁`?g^cuKwWyOAgQ?l_GݮtUx̾W5g?I?R#eH(=AvfzV ID~IaePM(^k^rLe[ $ Q&(uC[jTT&S[eF_W2VjLuZAKz%2(龾f![z\^?/Lw#]\; >?T Zi{ xcyzkMg+޼,KzHGYtQկl]eZy&۷Ne`Su<te̤[o2lМy?]IwHXΚ ϝ~H(O:5az鑼\7W:=˃w(oV<ʖ~WiOFn:u 9Iz^cOS!6뼹+>^!J)ϓ\ Ⱦ\bOu6"SHe{4+:e1(эh#zJў̣+ECj6%w ,-xw{Q q =~R] /U8=o&+#L:4C,7azK0J=R/.;Mxd`lMf0FcpINa#Yn0d%ߥ+Kt6ڒ?|JȲv7.Lj^"̰0@ $9.u1e /Y̧6^8:1f+=il4u;3vN0ldg+˛aavKJyp<볋.^6+e_鱴 ZYhXƆ9O»N*mSw=]D7~oꬽ2 ,c#z>қ&S>8ƯM20E&0O6A(]Y9:^s4sW9}qq{}VzT+xþjLT==Me*gz-*w$eWRfRy zF%/lp^BQ͟Q[a5>}$RܰYVxmjyaTYeA9 m)Iٷey{.Kv`Łzc|R4$c^|&cyWɟ~GYs.]3!y$N:3c_=̻NԐE&W 0@PodVh(S4%%bA(yב+[ΤwW#=j0a9ȏ4f=iMnk zaTz$fj:gzݎɣ4PZNyVea葡/N zuWӐ<-K4!!?BAWśN|k2}t U`/e˚yRlMa9^us&H[Wа u{'zdx0̾oD{a^(kX&#n([*wADH%U->lN({56Qs^e5=r׼D֡CǡCğoQA7~3TO7-;kv̸ømP}'Oo.}oأm(۹^̥6>IuhU$;oGSqO9$`{qxޤpf9H f*dnXV%k>\zG$?ɽ9y0r!G) w8m7(\"Uy믦Jf 0\wVRGO#e׻ס{#ay7Y+cj㽈V{/?S r}a=!ΓiϞݾ}UPM  S <L ?+=T^ iXGAZO2 OQD]JMO<HCG*z.,2߲ FmjT/-5h)Q2gdI^>i/sE& #ЕIʳʃc ;7le_%SֿI|V!޶adIH_տ. Os/?]\ʳ~i鉗?+5s>|Q} ̯fa/iAA5Fi1wY XQ%(:]iCobx7YnW$h"dMOdY:- ,_XȒ>'e^y@e?oyv)e !aiu?э矤]7R@ҋ{zaqs: =!LGHAF%1i2"?K&eNJ T$3 t6ъ S3Ûv^w$yh %k$VPϨ!Q4&d޾~1tA=6]>EQۦ7]]5O6'Pd0;8WK{pIx/6-E +;O}$eik+HbKauxm{9;x=A{pwDT#@eF2PQ:3sjJ]┽dnTnaȃ`Uju5b{@efy_]]]I}yJi5 D:=C>LmR|ٳ۷Z_ixqٮj_o-iۅe:V׷;̩OKϖ5X~{_ɻP׷4I@,͓ByU{:jc( ߽?,~.}SjfnKVa)œ;+ulQӊ-7.fft2y/{_ 3y/?S>t'a6m݃S[1"=LOf(%tle 4ۺ ̈ZK<^/ ҍf(%mjb9n4awzu5eheJLj\V竭QO;slJ J#SB ի{}9Q?ly#b@^)`t1{{?׍q8wzs0ӍэcV%~o{sW@m,.z8Pk>`ؐ`?g^Epޥuau&=mmԯ GݴΎt CKKQ6gmY Vke3ڟ^=.6#jsE ?3tsiWeű *+eU0ɚގN kJQXzaP.fEHg/3gԋˠGhpk{e›uA;T{8,UerGzKI.vP=gSH#A##=KFI.lU>Q^ϝ^Grrfi uc5]zZrz?&%=yJ2ގ0xP=dR!?qhuռR|S:d5ePHC-vd0s]ZCRءPi ͆,/iD hx %5_P 8Sh:Uv 5$fzpŶwc  rYcemJ7nޡY{Ge8>ӃoޛL$8-bf47;Y#&ׇjUN @o^nƟDjLd<=Mycd2cÍ̶d]QA/7?ب^M.K6|X?Pho>kuZUiOcB3O 3æ0̚Eֽd 6s㬧-bd7TFo!]Vg^gW 0@Pod?,@4%×bA(yؑ+[g #T]쓕$u"(i{)3$ysWUmo^`O kF[BO'WўEzXL%p279ڕ^89mr'{0?W/$ϸw*ze|^&Cni}/I(}}J~| d{ uVmFIg&%A; {B'-VޖdoRf2̐@T9M"!H4OUJak e7) q #/mRLǙ3;1:oVN?ϳhCf|dRzY ӻ8Y7WGr[dIx .${)ƍy̻~OǏÇoňL`~}j 4v4M~zNOsђ_{   ε2Y@yN![7tz7^6{xO+v43lM6ʚ@췵6љ2D΍+1i2"?K&0Rgw}R+g}3 F1lEyҟ꾒ڰy=oa@lFpv{R2\PMY>lڗ/8ggdW>xHINRz-ne2ø ʫ^\~ĴSHϨsZ}u1 Fj7ޝLO7j//UMTgcQ=\7ޥ&X=t.=/ZL0/w&)*{yn(Gu]@m,zܾI_)\?zީ:,a,aӾݻVR Ց*Ci_LZsc'm Gfdj: vu 6U=iOO%!yz,Fڏa4=){9i p]>^SՇ=$Ҕq<}$3O ;L=Wz9Zټzޫ72P* ˳|˳|ش/_>U zͯ&9iQ ^o.nAj/70Vy^ZеΞz}|^_LEi/?Ӟӟ=}iyV,Gr%;n|`~.J!\<^H+XP͝ͱ#2\jh!zCOcvXno ct, H^K (].{b<~buWa*GoՂ_ DN/')\~K.tiWxkxܽVB?Y^c̣1Kn[Sz:^[5%|Iokm D -g5we2fPꋫT&:).],o"]GYe9,׺z_1h]Pz#9S0E~ُ&6D<ˇ<ˇMM걭s}͍?_ I$\#{\Uև'l_WNJ5ʬϸU I> Q/p rleڛFvNC/ʎgygy_W\wvԊ~( ԟuv!w???)dqɟLZs"( LF59?ćYMC:hL{U"0#Z7:#}akuI3/;?Pl([]0=68a@.1\W~d69˜6%d.UMdW'ɛD{E|_MZs%˄ͦ]2bS'!E}aP5gSvW(PVM7JzspFw= x3]&or+`O~0ச7\5E2y}!=wkYY.nx@A$/?q^~O&9i 3.G:%(mnr3gSv7("3\x ̯Ǝ(ύlAJ$#e[N,}qUM+^ L֪캼:hYH?ae{:iꤗ9YycmA&3%?7c_V9 ͗džY>Y>lڗn= xoތ.o+DFi/si?aeǪfHO2#4$M=02E ғ+>/~٠jϴg%w0JAW˶Z'UqЭೱn[(Xv+(9:3={i6;knԣ~(v΅̷~t'r7sϙmE3DrY><HzƲwYzO1|Dwf^$,oqwX~8c^O˒F8ZO\ 2lغAvr?WY.C +?N}&Kz_n 9fW1Ov~p\Io +3'E7=Lfc%CdF3.-B?mu:?'7gRˇٓ9 5=UկJϴħ yǞ6}oo=\U2gn߾ 3}R$ .oI[5M !?L,gUѐ< ?~T>|+ƽ3w@]5OseX$  Wk ƽ'`6v!p_ƒavz?jǛ^ 990F2PQ (T`ezN܌.LSu5nT\OL"Qf_zMPޛ(3ꊊnx^CO\$\ffԖd9=xnΌ~TObhS0Q"9sG4r>=WUN/wsnwK/n/JW!կX>D*QQVg?sz?H9_.̣\(?(ܖڑ@T@w\w(^n趂aS_"XW( ReC9*Hw!;Uef{.9z߶uKO=Xq<|bze#_P,{2u/Kykױ_KP9:?7Ϋz8_*xd??n[,<.|;ow99Kz]tN}=K*LJTTORb`r$L)E@0J.e7б7/_{=wzq6j샳MOWKwGM2<[%d}oGIi\ ƙ S:'g1=R f $MI$-a^.s>ܞ/=wvΛ~ixo.Ӳ / ,I xt%vM͆ 򳾒x9+dnEҢՖ_t\㫪t7菬G]5/b=<>o:yob+"VP= s+}qq{}VbzA/L>mM5i)oz [&yE"6?mV3&C=m`2=叐2<6Nb͆V:Ѵ芵M~.sRv{}m0vr1VJ|FБ]~ r)?ӯz筼3t^$s瓥u@ 6,Bnle<9;J{=޶A+XWycTsuQ~y˙%KyW'I9wx>__C aaMCY|u6':|\ӯ*^ :d8GHPAn^&@<.[ёMw^W7M4=؁鑹 ]_i|o*˛V'Oy({v;ssHLo''OH0|^!COzȇWwaӓer Nd^"-Kȓ miHR&ݞ.o26{`ݖdEwN|!=.kDaSx\`Ske_FO,?Ϩpũ8?8w>pyi%ĵwvHϨ97vUgw[q|Q}Q&mݨ~DMM^_UW[W+K˶j6P15xos(;<&9:M3݀թiir=pNOY\/vdޛ5M ̅iIm4q~/g3Y^~|7i-'ϋ#eYd.Su8ak4'֩%i1s y`'ಽ"KmǕ՛K6+keH7aA鿅SdݿyʛKGʳ⮯`+߃W^LAB;M_ׇM}nO;o-m mLOJiz驎SuGs$ʐ+'lx=ά4`:6]j nQmh~ړ!z$ "A XK"T/\^)7zw}V~mw7d i]˛Rd:q.Aݎmt.3idyIg8dud9kfkyF;6A/Є[]zΧ\{=8jw\Cӟ}z?VXHݳBɞ|e[7kaBZડKܸsɪL)'9_;ٿy*zjI,roǗdt367;/_KsI7rNJVރ'=n5<# zfbxC=7"V(4]z珑?ESCuZ5C7,C'ۈ $0`ZCOtf.9[dfS6 I1(Ir qc)OGq8A&o~օM)$<+;ݷ޽Uo$swt) Æn.^*?eҸmQ;.uNq?̰g6xe˛h7O&#VU_1l2mQ[_>E˲l~ڹ_g[&>Z=df/PLt{GyXi$ȵ)%7Q(=ް(2% sF#ͰI^^&M3=Rnyc3VCzC]6w9.+;A>S_>}?)e?s99?(wؿyʛk|R~VÆ۫/=,ϓC#!g .??>#2<~Tx/r]#)=^UJ*UkU]G(Hz:꠰kJyl4딟dXO7oɰ]^V"dqvnl岽xCݮ~_v6<ۛ5|nmcsY*?YI@l0LmL\tݿERb=e@p"?WϷiZs j 3.{]L۞ z@糘.^2|_)g]fbym&YϩUgGL6ag}3dpy-`л{3 Qޜ:D($g(T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@e>{{@|3#P.zF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@e>GG@@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (T`*C0 !@eF2PQ (8O ;(8K 8 T@.Ө@ '7p *8QpQ`rF(fPYQ`ƒ5%F2PQ (T`*C0 {?O4>ϚW>~OD}sSw=S>|sg?~}Zͨ?Cۿ[w~>3/Ќɘ?ӯ?~c=V>>gԧ?=쏪?~?!|?QiOt渟ޒ}{{~_@kҿV~w?3?w~wW_J5}IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/graphical.rst0000644000175100001770000012455014623331163020503 0ustar00runnerdocker########################### 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-fledged 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. .. warning:: The example in this section is known to raise issues when executed: a `QApplication was not created in the main thread` / `nextEventMatchingMask should only be called from the Main Thread` warning is raised. While the example works without issues on some operating systems and python configurations, users are advised not to rely on the plotter while this issue is unresolved. Users can hence skip this example and continue with the `Using the ManagedWindow`_ section. 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) self.emit('progress', 100 * i / self.iterations) 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 QtWidgets 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', default=100) 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) self.emit('progress', 100 * i / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Caught the stop flag in the procedure") break class MainWindow(ManagedWindow): def __init__(self): super().__init__( procedure_class=RandomProcedure, inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], x_axis='Iteration', y_axis='Random Number' ) self.setWindowTitle('GUI Example') if __name__ == "__main__": app = QtWidgets.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 :class:`MainWindow` class is a sub-class of the :class:`~pymeasure.display.windows.managed_window.ManagedWindow` class. We override the constructor to provide information about the procedure class and its options. The :code:`inputs` are a list of :class:`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. As a bit of background information (for basic usage this needs not be known): the :meth:`~pymeasure.display.windows.managed_window.ManagedWindowBase.queue` method establishes how the :class:`~pymeasure.experiment.procedure.Procedure` object is constructed. The :meth:`~pymeasure.display.windows.managed_window.ManagedWindowBase.make_procedure` method is used to create a :class:`~pymeasure.experiment.procedure.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 :class:`~pymeasure.display.manager.Manager` uses an :class:`~pymeasure.display.manager.Experiment` object to keep track of the :class:`~pymeasure.experiment.procedure.Procedure`, :class:`~pymeasure.experiment.results.Results`, and its associated graphical representations in the browser and live-graph. This is then given to the :class:`~pymeasure.display.manager.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. .. note:: For performance reasons, the default linewidth of all the graphs has been set to 1. If performance is not an issue, the linewidth can be changed to 2 (or any other value) for better visibility by using the `linewidth` keyword-argument in the `Plotter` or the `ManagedWindow`. Whenever a linewidth of 2 is preferred and a better performance is required, it is possible to enable using OpenGL in the import section of the file: :: import pyqtgraph as pg pg.setConfigOption("useOpenGL", True) The filename and directory input ################################ By default, a ManagedWindow instance contains fields for the filename and the directory (as part of the :class:`~pymeasure.display.widgets.fileinput_widget.FileInputWidget`) to control where the results of an experiment are saved. .. image:: pymeasure-fileinput.png :alt: The filename and directory input widget :width: 24% .. image:: pymeasure-fileinput_disabled.png :alt: The filename and directory input widget, disabled to store the measurement to a temporary file :width: 24% .. image:: pymeasure-fileinput_complete_filename.png :alt: The filename and directory input widget showing the auto-complete for the filename :width: 24% .. image:: pymeasure-fileinput_complete_directory.png :alt: The filename and directory input widget showing the auto-complete for the directory :width: 24% If the checkbox named :guilabel:`Save data` is enabled, the measurement is written to a file. Otherwise, it is stored in a temporary file. The filename in the designated field can be entered with or without extension. If the entered extension is recognized (by default :code:`.csv` and :code:`.txt` are recognized), that extension is used. If the extension is not recognized, the first of the available extensions will be used (default is :code:`.csv`). Additionally, a sequence number is added just before the extension to ensure the uniqueness of the filename. The filename can also contain placeholders, which are filled in using the standard python :code:`format` function (i.e., placeholders can be entered as :code:`'{placeholder name:formatspec}'`). Valid placeholders are the names of the all the input parameters or metadata of the measurement procedure; the valid placeholders are listed in the tooltip of the input field. As the standard :code:`format` functionality is used, the placeholders can be formatted as such; for example, the filename :code:`'DATA_delay{Delay Time:08.3f}s'` gets formatted into :code:`'DATA_delay0000.010s'`. Both the filename and the directory field are provided with auto-completion to help with filling in these fields. The directory field contains a button on the right side to open a folder-selection window. The default values can be easily set after the :class:`~pymeasure.display.windows.managed_window.ManagedWindow` has been initialized; this allows setting a default location and a default filename, changing the default recognized extensions, control the default toggle-value for the :guilabel:`Save data` option, and control whether the filename input field is frozen. .. code-block:: python :emphasize-lines: 13, 14, 15, 16, 17 class MainWindow(ManagedWindow): def __init__(self): super().__init__( procedure_class=TestProcedure, inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], x_axis='Iteration', y_axis='Random Number', ) self.setWindowTitle('GUI Example') self.filename = r'default_filename_delay{Delay Time:4f}s' # Sets default filename self.directory = r'C:/Path/to/default/directory' # Sets default directory self.store_measurement = False # Controls the 'Save data' toggle self.file_input.extensions = ["csv", "txt", "data"] # Sets recognized extensions, first entry is the default extension self.file_input.filename_fixed = False # Controls whether the filename-field is frozen (but still displayed) The presence of the widget is controlled by the boolean argument :code:`enabled_file_input` of the :class:`~pymeasure.display.windows.managed_window.ManagedWindow` init. Note that when this is set to :code:`False`, the default :meth:`~pymeasure.display.windows.managed_window.ManagedWindowBase.queue` method of the :class:`~pymeasure.display.windows.managed_window.ManagedWindow` class will no longer work, and a new, custom, method needs to be implemented; a basic implementation is shown in the documentation of the :meth:`~pymeasure.display.windows.managed_window.ManagedWindowBase.queue` method. 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.managed_window.ManagedWindow`, the mechanism to customize plots is much more flexible by using specialization via inheritance. Indeed :class:`~pymeasure.display.windows.managed_window.ManagedWindowBase` is the base class for :class:`~pymeasure.display.windows.managed_window.ManagedWindow` and :class:`~pymeasure.display.windows.managed_image_window.ManagedImageWindow` which are subclasses ready to use for GUI. Using tabular format ~~~~~~~~~~~~~~~~~~~~ In some experiments, data in tabular format may be useful in addition or in alternative to graphical plot. :class:`~pymeasure.display.windows.managed_window.ManagedWindowBase` allows adding a :class:`~pymeasure.display.widgets.table_widget.TableWidget` to show experiments data, the widget supports also exporting data in some popular format like CSV, HTML, etc. Below an example on how to customize :class:`~pymeasure.display.windows.managed_window.ManagedWindowBase` to use tabular format, it derived from example above and changed lines are marked. .. code-block:: python :emphasize-lines: 11, 12, 18, 44, 47, 48, 49, 50, 51, 52, 57, 59, 60, 61 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 QtWidgets from pymeasure.display.windows import ManagedWindowBase from pymeasure.display.widgets import TableWidget, LogWidget from pymeasure.experiment import Procedure, Results from pymeasure.experiment import IntegerParameter, FloatParameter, Parameter class RandomProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=10) 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) self.emit('progress', 100 * i / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Caught the stop flag in the procedure") break class MainWindow(ManagedWindowBase): def __init__(self): widget_list = (TableWidget("Experiment Table", RandomProcedure.DATA_COLUMNS, by_column=True, ), LogWidget("Experiment Log"), ) super().__init__( procedure_class=RandomProcedure, inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], widget_list=widget_list, ) logging.getLogger().addHandler(widget_list[1].handler) log.setLevel(self.log_level) log.info("ManagedWindow connected to logging") self.setWindowTitle('GUI Example') if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) This results in the following graphical display. .. image:: pymeasure-tablewidget.png :alt: TableWidget Example Defining your own ManagedWindow's widgets ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The parameter :code:`widget_list` in :class:`~pymeasure.display.windows.managed_window.ManagedWindowBase` constructor allow to introduce user's defined widget in the GUI results display area. The user's widget should inherit from :class:`~pymeasure.display.widgets.tab_widget.TabWidget` and could reimplement any of the methods that needs customization. In order to get familiar with the mechanism, users can check the following widgets already provided: - :class:`~pymeasure.display.widgets.log_widget.LogWidget` - :class:`~pymeasure.display.widgets.plot_widget.PlotWidget` - :class:`~pymeasure.display.widgets.image_widget.ImageWidget` - :class:`~pymeasure.display.widgets.image_widget.DockWidget` - :class:`~pymeasure.display.widgets.table_widget.TableWidget` Using the sequencer ~~~~~~~~~~~~~~~~~~~ As an extension to the way of graphically inputting parameters and executing multiple measurements using the :class:`~pymeasure.display.windows.managed_window.ManagedWindow`, :class:`~pymeasure.display.widgets.sequencer_widget.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.managed_window.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.managed_window.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:`~pymeasure.display.windows.managed_window.ManagedWindow` has to be modified slightly (where modified lines are marked): .. code-block:: python :emphasize-lines: 10,11,12 class MainWindow(ManagedWindow): def __init__(self): super().__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') This adds the sequencer underneath 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 latter two either add an 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 create a sequence file such that the user does not need to write the sequence again each time. The sequence file can be created by saving current sequence built within the GUI using the :code:`Save sequence` button or directly writing a simple text file. Once created, the sequence can be loaded with the :code:`Load sequence` button. 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().__init__` call, as was done in the example. Using the estimator widget ~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to provide estimates of the measurement procedure, an :class:`~pymeasure.display.widgets.estimator_widget.EstimatorWidget` is provided that allows the user to define and calculate estimates. The widget is automatically activated when the :code:`get_estimates` method is added in the :code:`Procedure`. The quickest and most simple implementation of the :code:`get_estimates` function simply returns the estimated duration of the measurement in seconds (as an :code:`int` or a :code:`float`). As an example, in the example provided in the `Using the ManagedWindow`_ section, the :code:`Procedure` is changed to: .. code-block:: python class RandomProcedure(Procedure): # ... def get_estimates(self, sequence_length=None, sequence=None): return self.iterations * self.delay This will add the estimator widget at the dock on the left. The duration and finishing-time of a single measurement is always displayed in this case. Depending on whether the SequencerWidget is also used, the length, duration and finishing-time of the full sequence is also shown. For maximum flexibility (e.g. for showing multiple and other types of estimates, such as the duration, filesize, finishing-time, etc.) it is also possible that the :code:`get_estimates` returns a list of tuples. Each of these tuple consists of two strings: the first is the name (label) of the estimate, the second is the estimate itself. As an example, in the example provided in the `Using the ManagedWindow`_ section, the :code:`Procedure` is changed to: .. code-block:: python class RandomProcedure(Procedure): # ... def get_estimates(self, sequence_length=None, sequence=None): duration = self.iterations * self.delay estimates = [ ("Duration", "%d s" % int(duration)), ("Number of lines", "%d" % int(self.iterations)), ("Sequence length", str(sequence_length)), ('Measurement finished at', str(datetime.now() + timedelta(seconds=duration))), ] return estimates This will add the estimator widget at the dock on the left. .. image:: pymeasure-estimator.png :alt: Example of the estimator widget Note that after the initialisation of the widget both the label of the estimate as of course the estimate itself can be modified, but the amount of estimates is fixed. The keyword arguments are not required in the implementation of the function, but are passed if asked for (i.e. :code:`def get_estimates(self)` does also works). Keyword arguments that are accepted are :code:`sequence`, which contains the full sequence of the sequencer (if present), and :code:`sequence_length`, which gives the length of the sequence as integer (if present). If the sequencer is not present or the sequence cannot be parsed, both :code:`sequence` and :code:`sequence_length` will contain :code:`None`. The estimates are automatically updated every 2 seconds. Changing this update interval is possible using the "Update continuously"-checkbox, which can be toggled between three states: off (i.e. no updating), auto-update every two seconds (default) or auto-update every 100 milliseconds. Manually updating the estimates (useful whenever continuous updating is turned off) is also possible using the "update"-button. Flexible hiding of inputs ~~~~~~~~~~~~~~~~~~~~~~~~~ There can be situations when it may be relevant to turn on or off a number of inputs (e.g. when a part of the measurement script is skipped upon turning of a single :code:`BooleanParameter`). For these cases, it is possible to assign a :code:`Parameter` to a controlling :code:`Parameter`, which will hide or show the :code:`Input` of the :code:`Parameter` depending on the value of the :code:`Parameter`. This is done with the :code:`group_by` key-word argument. .. code-block:: python toggle = BooleanParameter("toggle", default=True) param = FloatParameter('some parameter', group_by='toggle') When both the :code:`toggle` and :code:`param` are visible in the :code:`InputsWidget` (via :code:`inputs=['iterations', 'delay', 'seed']` as demonstrated above) one can control whether the input-field of :code:`param` is visible by checking and unchecking the checkbox of :code:`toggle`. By default, the group will be visible if the value of the :code:`group_by` :code:`Parameter` is :code:`True` (which is only relevant for a :code:`BooleanParameter`), but it is possible to specify other value as conditions using the :code:`group_condition` keyword argument. .. code-block:: python iterations = IntegerParameter('Loop Iterations', default=100) param = FloatParameter('some parameter', group_by='iterations', group_condition=99) Here the input of :code:`param` is only visible if :code:`iterations` has a value of 99. This works with any type of :code:`Parameter` as :code:`group_by` parameter. To allow for even more flexibility, it is also possible to pass a (lambda)function as a condition: .. code-block:: python iterations = IntegerParameter('Loop Iterations', default=100) param = FloatParameter('some parameter', group_by='iterations', group_condition=lambda v: 50 < v < 100) Now the input of :code:`param` is only shown if the value of :code:`iterations` is between 51 and 99. Using the :code:`hide_groups` keyword-argument of the :code:`ManagedWindow` you can choose between hiding the groups (:code:`hide_groups = True`) and disabling / graying-out the groups (:code:`hide_groups = False`). Finally, it is also possible to provide multiple parameters to the :code:`group_by` argument, in which case the input will only be visible if all of the conditions are true. Multiple parameters for grouping can either be passed as a dict of string: condition pairs, or as a list of strings, in which case the :code:`group_condition` can be either a single condition or a list of conditions: .. code-block:: python iterations = IntegerParameter('Loop Iterations', default=100) toggle = BooleanParameter('A checkbox') param_A = FloatParameter('some parameter', group_by=['iterations', 'toggle'], group_condition=[lambda v: 50 < v < 100, True]) param_B = FloatParameter('some parameter', group_by={'iterations': lambda v: 50 < v < 100, 'toggle': True}) Note that in this example, :code:`param_A` and :code:`param_B` are identically grouped: they're only visible if :code:`iterations` is between 51 and 99 and if the `toggle` checkbox is checked (i.e. True). .. _pyqtgraph: http://www.pyqtgraph.org/ .. _PlotItem: http://www.pyqtgraph.org/documentation/graphicsItems/plotitem.html Using the ManagedDockWindow ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Building off the `Using the ManagedWindow`_ section where we used a :code:`ManagedWindow`, we can also use :class:`~pymeasure.display.windows.managed_dock_window.ManagedDockWindow` to build a graphical interface with multiple graphs that can be docked in the main GUI window or popped out into their own window. To start with, let's make the following highlighted edits to the code example from `Using the ManagedWindow`_: 1. On line 10 we now import :class:`~pymeasure.display.windows.managed_dock_window.ManagedDockWindow` 2. On line 20, and lines 32 and 33, we add two new columns of data to be recorded :code:`'Random Number 2'` and :code:`'Random Number 3'` 3. On line 44 we make :code:`MainWindow` a subclass of :code:`ManagedDockWindow` 4. On line 51 we will pass in a list of strings from :code:`DATA_COLUMNS` to the :code:`x_axis` argument 5. On line 52 we will pass in a list of strings from :code:`DATA_COLUMNS` to the :code:`y_axis` argument .. code-block:: python :emphasize-lines: 10,20,32,33,44,51,52 import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) import sys import tempfile import random from time import sleep from pymeasure.display.Qt import QtWidgets from pymeasure.display.windows.managed_dock_window import ManagedDockWindow from pymeasure.experiment import Procedure, Results from pymeasure.experiment import IntegerParameter, FloatParameter, Parameter class RandomProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=10) delay = FloatParameter('Delay Time', units='s', default=0.2) seed = Parameter('Random Seed', default='12345') DATA_COLUMNS = ['Iteration', 'Random Number 1', 'Random Number 2', 'Random Number 3'] 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 1': random.random(), 'Random Number 2': random.random(), 'Random Number 3': random.random() } self.emit('results', data) log.debug("Emitting results: %s" % data) self.emit('progress', 100 * i / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Caught the stop flag in the procedure") break class MainWindow(ManagedDockWindow): def __init__(self): super().__init__( procedure_class=RandomProcedure, inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], x_axis=['Iteration', 'Random Number 1'], y_axis=['Random Number 1','Random Number 2', 'Random Number 3'] ) self.setWindowTitle('GUI Example') if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) Now we can see our :code:`ManagedDockWindow`: .. image:: managed_dock_window.png :alt: Managed dock window As you can see from the above screenshot, our example code created three docks with following "X Axis" and "Y Axis" labels: 1. **X Axis:** "Iteration" **Y Axis:** "Random Number 1" 2. **X Axis:** "Random Number 1" **Y Axis:** "Random Number 2" 3. **X Axis:** "Random Number 1" **Y Axis:** "Random Number 3" The list of strings for :code:`x_axis` and :code:`y_axis` set the default labels for each dockable plot and the longest list determines how many dockable plots are created. To highlight this point, in our example we define :code:`x_axis` and :code:`y_axis` with the following lists:: x_axis=['Iteration', 'Random Number 1'], y_axis=['Random Number 1','Random Number 2', 'Random Number 3'] If one list is longer than the last element if the other list is used as the default label for the rest of the dockable plots. In our example that is why we have two **X Axis** labels with "Random Number 1". The longest list between :code:`x_axis` and :code:`y_axis` determines the number of plots. In our example :code:`y_axis` has the longest list with a length of three so three plots are created. You can pop out a dockable plot from the main dock window to its own window by double clicking the blue "Dock #" title bar, which is to the left of each plot by default: .. image:: managed_dock_window_popup.gif :alt: Pop up a managed dock window You can return the popped out window to the main window by clicking the close icon X in the top right. After positioning your dock windows, you can save the layout by right-clicking a dock widget and select "Save Dock Layout" from the context menu. This will save the layout of all docks and the settings for each plot to a file. By default the file path is the current working directory of the python file that started :code:`ManagedDockWindow`, and the default file name is '*procedure class* + "_dock_layout.json"'. For our example, that would be "./RandomProcedure_dock_layout.json" When you run the python file that invokes :code:`ManagedDockWindow` again, it will look for and load the dock layout file if it exists. .. image:: managed_dock_window_save.png :alt: Save dock window layout You can drag a dockable plot to reposition it in reference to other plots in the main dock window in several ways. You can drag the blue "Dock #" title bar to the left or right side of another plot to reposition a plot to be side by side with another plot: .. image:: managed_dock_window_side_drag.png :alt: Side drag managed dock window .. image:: managed_dock_window_side_after.png :alt: Side position managed dock window You can also drag the blue "Dock #" title bar to the top or bottom side of another plot to reposition a plot to rearrange the vertical order of the plots: .. image:: managed_dock_window_top.png :alt: Top position managed dock window You can drag the blue "Dock #" title bar to the middle of another plot to reposition a plot to create a tabbed view of the two plots: .. image:: managed_dock_window_tab_drag.png :alt: Tab drag managed dock window .. image:: managed_dock_window_tab_after.png :alt: Tab position managed dock window Using the ManagedConsole ~~~~~~~~~~~~~~~~~~~~~~~~ The :class:`~pymeasure.display.console.ManagedConsole` is the most convenient tool for running measurements with your Procedure using a command line interface. The :class:`~pymeasure.display.console.ManagedConsole` allows to run an experiment with the same set of parameters available in the :class:`~pymeasure.display.windows.managed_window.ManagedWindow`, but they are defined using a set of command line switches. It is also possible to define a test that uses both :class:`~pymeasure.display.console.ManagedConsole` or :class:`~pymeasure.display.windows.managed_window.ManagedWindow` according to user selection in the command line. Enabling console mode is easy and straightforward and the following example demonstrates how to do it. The following example is a variant of the code example from `Using the ManagedWindow`_ where some parts have been highlighted: 1. On line 8 we now import :class:`~pymeasure.display.console.ManagedConsole` 2. On line 73, we add the support for console mode .. code-block:: python :emphasize-lines: 8,62,63,64 import sys import random import tempfile from time import sleep from pymeasure.experiment import Procedure, IntegerParameter, Parameter, FloatParameter from pymeasure.experiment import Results from pymeasure.display.console import ManagedConsole from pymeasure.display.Qt import QtWidgets from pymeasure.display.windows import ManagedWindow import logging log = logging.getLogger('') log.addHandler(logging.NullHandler()) class TestProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=100) 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 up random number generator") random.seed(self.seed) def execute(self): log.info("Starting to generate numbers") for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } log.debug("Produced numbers: %s" % data) self.emit('results', data) self.emit('progress', 100 * (i + 1) / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Catch stop command in procedure") break def shutdown(self): log.info("Finished") 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' ) self.setWindowTitle('GUI Example') if __name__ == "__main__": if len(sys.argv) > 1: # If any parameter is passed, the console mode is run # This criteria can be changed at user discretion app = ManagedConsole(procedure_class=TestProcedure) else: app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) If we run the script above without any parameter, you will have the graphical user interface example. If you run as follow, you will use the command line mode: .. code-block:: bash python console.py --iterations 10 --result-file console_test Console output is as follow (to show the progress bar, you need to install the optional module `progressbar2 `_): .. image:: console_output.png :alt: Console mode output Other useful commands ##################### To show all the command line switches: .. code-block:: bash python console.py --help To run an experiment with parameters retrieved from an existing result file. .. code-block:: bash python console.py --use-result-file console_test2023-08-09_1.csv ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/gui_sequencer_example_sequence.txt0000644000175100001770000000023614623331163025013 0ustar00runnerdocker- "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)" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/index.rst0000644000175100001770000000026614623331163017655 0ustar00runnerdocker######### Tutorials ######### The following sections provide instructions for getting started with PyMeasure. .. toctree:: :maxdepth: 2 connecting procedure graphical ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/managed_dock_window.png0000644000175100001770000023115514623331163022510 0ustar00runnerdockerPNG  IHDR94SsBIT|d IDATxwx !zU`7UT,`E16*&HH^I# $0}ܛݝ9]<333&fѣG.~!}ԨQ*&={,DDDDDDDnj; 3IѣGF%""""""r?>G6n,]DDDDDDŒݻw"""""""7 fLN"""""""%uDȍ.ys>] 3o]\;"%""rxn8R 'JySqGl;߻5i 0×z"S39&W~B,98g-)Ǿԡ*F512jrtt?QF纾K뇇 ̞=|0u)QAžEZv$Ki%J,;KY\(9)ad%yzPY,ei(ߺj{O5M.AnP&DER8&%^vYyKCNNٳׯ/HJ,X;aÆt5 clX{$s JجVl #%wa~GJT tf35iO5rV]ʰbH_A jMacb@SwNu{NGHtlKN~W2v$8b NaxҢ]ײh Rhz4 r*wv>c`і 1```)go9 PU'Oa$}aܹENùs]W{wӧs\2`ݰHQG9Ǫi[щ8Y/s,YzVnxnF=gYrfP6*v480-[6pBYn\=;V+=H{3g7eUWB3'0qrXkԥf#l;9OԨbb#0;[P: =Ty<~S廓þլY{hFEDDO#s#7)))WO>$%%%V7ߐ/#**@ %{j ip9#M?X*v^uqtH}E엎g6Rϰ3i2L69`&Knm(K )vb',Vwu8)f6`nJQ6qq/K= Iu'Gf;!tW'274gcc8פLb pӡ:7Nq'I\(x^|ᅩSB?@ ͭ@(QOHHӓ=zh"ׯVU׹HIڼXF~DGw=ILO3{Es$/LޭT̾Ϝ1)8EUlR1+XHMH;?`ۑR2٬ 3f,*޾JR*XL&OMi$%1;a70L1=$ .&R 'rI=;gWLNɕ2u)~??z v/vxJJJk?Ά ׯJo&G@@͚5lٲf H3!āC_ʵn0 ;+n.&qg O򆁑EdYqױiK&6: kl`wr#_ ƪ')U+wwi^}Bf3&[opgdda˰bK !9.n\]1c& )Y'EDD(z ^3s&}u!C]U=22듖Flll&+Ҡ^G6b3KFgiaju?vn˂R t"Q%jѐ O0L 1 }qcDì[Sُj돑^/czjn͆ ''Rfpl̑_<߸t=o?#!v'kꔷر`Sꄺfulv l%!2Fp)QFݺu½{>}ffә3g ȵc!i7z9O8fJRFNft6:?{ӱJvt\Kj$G7bt1>L l9nFl7:TrZ_'R љ87-TAuZvlפ1۔UE#湇/ ^ KZTJ=ɸUlAupMO=Keդ؝(S.8Q5o(cr fҧO,KFMaaaFvsP"""%R)患aǖa%=%s֬)=(ꌃ6+)I$$ap -q$D#59%GF*q~xH"&نC)_<&*ɄnNLt%Yq(=>4W|W]F }k}i>mWZ;^fY;iqGnB` }zcLcY̐ySYm56uƎ5枉z߫W>m_2kavxg#:ՆX5odA 7J=Ê=u;<4ߟcp Эe=7Z1}$^9҃v?2Ts#""""""7jwװ;) 87}< ;#OS B=ywJvX;?9՟fw7/5۶Z*-u&;ALڀS"NKDXpא8^'Z~w7; m+q ʯc2?^|;N&=>էdw(+,0ypSc\)3(W#vi;FwMhMIQ9x DGGw(%/T\C?Q7=o`gg7u~xOn\ypjEcF6f.>EBʩd Ȯ؄gŊxg'X9d9ăXW[fpM88v0"LҮ$c[F=._#ߡ4y{0-^Dq׻*.8x fw(%͛9tu3a2]3;QQة&/3a\Hdmq&ɪ.ʆ01,\?[{М]Җ<5`/wnG^vA5h7̋c[o QV'ko^{(Fue j[ ^nYK\#>cwRA6CX 9 nL&v=za6 ȑ#Mf  yw뵋GDDDDukV=^⇯pj[,|܎޶ ~ՙ'<@Z:Fzےx֠Ԉg -grs&30LpI0ԤB/[1|A.jchO@un{~)s5XL~ y'OfStmPZͻL.""""7%%"]wڝVHYK&{DE=8ykh?>OLXz4v,kJcgvkߵlSg#q~;٣ <5 bG5 #M X,fLi;t[#};`.1W00r4]ՠj gN.[|Dg֪ETfR1-` hJ{fѵ{i{Mno\5[3l8]դJ+F>i:?E浩V>]|ρt5cIs~; #nQhЎ{'ʙ앭G]4^{In<50/"""r]P.R^z&xg}yrp5L&J}0wv2R2gyY|a 5<qJh@Li~Tw~x |Vti\gpYZ(e:2jpk'51續z?'y%}{o'`$-gL߰=FyH #__}oܞdY}g2A7b2K+/wѬ#a  2;g&dv/}!=; |& >pZ Q|;uN\ͻ1a}-=՜R똫cH;8c X~DZze.u4V?ͪ ə<}XуR.yLG0Wݷ`t(HwPɄsv3vv,;ٻ]rD4W}˺?YkY~3eݔG5e׼Efv }&^fLnUmDDDD`sE b~U@|"Nq!f OFS9"9nuK: wǔx#3NOgE4/[`If_XMD*gqԵJL~ oi{qF{R>=sd?q.FdlgYL4+)s*%J8 r^o|uvMX v0#܎ 'S, دFR4~_?ϸ(b3∎s#0m>]u{""""rmiD]e`v㎲~/ \ O^vv~{իx'p[ n`&0"}ߛD#xnY4ő8a`K"\ս0i̞iֳ#9x6#哾"[/gNdnlۛR}qo̳+sNvɞnp$ք_N@&m~J wK8Kwx{|Dç剈ܬ9{lA)gW*桯"g#8VYh ǣ{؟eOn2RIKK,V-u{>ZΩtmbSNrkǚl; H?qRnhHr#MRAg&6<&nt;ʿ~47A/t{gɱ3#W[391#w0#ɣb˛R9m㗴"3؄q_'8E!Sr/&t9g_Ǡ&O_6U|>{|Ga(fSW>+_pűl aV .}M?]{fHb 3\q\V\I^;?LvيA+p* e5I(/֍R|ijba4nܸh\f̘"r#9Nle8GY8m )9T""""`=[Μnr%Ly=nu*ԅ];šCDDDDu)RE6)rDDDDu)R6ho&'""""rQ."EfQa\7٬%D@@ׯ/0?Xreq """rԩSS."EUVH0o޼|/A J CJh֗iUDDDDDDMm{ض6ON/j#cgݤUh mIПliji*e?qʥC^C2:!"""""" Y:ziR|@*&Anĭ7^HM~4 !\D݈:P'45[1荕~-_ߺ5+ZWLfzmEy/U*Rm<9 iWlYԝ3 cf IDATuVGĻiU#К- f[yi<ۥ&#X?3FҭEUD[]'oxe&h{qlc{4zheB7kH02n'j ͬ]D݈c4iv #ߍ_1q~[ɋ~?0yDf [?~-lv _]A }k}i>mWZ[8\^wp$/-O\~^WVtr[//Ⱦ#; MZB[+tl^l'\]#e߼ mV6|2}ʖٿ'X&3UӺQEME눈H rmu_Uo#,>9;Z [4.HG u.e+#nV])F +AFׂ٫f Veqws~ Ӭ+f6TYbmTJۚCy'f\҉gqfLH9"-dV7&̔:=_%""""""7٣ JeKZV\;'et[RB}HSm'fx1zrjwL%{s,XvX}x:{NGou;y ms5IM9Gp*3Գgi>)u6?L= f{L$Qh8ΜM׿LLo'fo|2z.{Q#:k )q;.XN@GW5g~m5CEb|?)v~=O޺itM~%)l HHhXf\Uhذ!r >(UT)v 4e5dɹ5}|>XJw?[O/lS[x02[Y4Yi?ՐRg$i6]dQ?:nƤ'X:u"gTvېCJ`Sj;BF#ΨY7+{Jۊ+m]6^;{Z>=݊հsrۖOG׍ ϰrej-X/"""""Eg֭9r͛3e\$5F@PPPf0 =4MkVa|rf:΋R=;JJjFmiU5F?$w{>灧~/>ipmIêx 1I^R*q,>F8c\-i67N#ǬO}`}i;C9[7]i[NЯQeB֠c{(:ډٷXq һwo~^}"kfY`EZ=WK1[r%={ײ#GnvϜ?zj>|-?o<:u͘1=G]DDDDDDFѼysϟ_$u+Q)er"O&wlw"""""""׆FEDDDDDDJ%"""""""%uDH D]DDDDDDQ.""""""Rjr¬NDDDDDDSQԩ$11¬ZnRxzzRrEDDDDD(Dfׯ_Mȑ#:tEݻ%**C???|||UVqRb/II 5QOLL}Y>>>^Ðb{n222hӦMq""R [laϞ=JQ_."ק uMw[7X6lXaXF)0J"r=*~PGED STTaw""WEӼ3/UqJEDO}H(QOx"r1LB\D'ݏ5"""""""\D=rK@ .K~ fp*Oa؛zm"|J^ƈ:ݘz^,QbJ_hqkl,***%$ h~kl(7nGzWz{(b \ǢrTɽa-> 㡗 W@+?y7"JƱJGB}<ڈb5hZכ+kc|)c3QҔ MX Úp[wЩ;;o7֬זx^ǡRKA|٭<sh"JIJO~ޕ*O/Pcԇ*_(\df8;;Ot5 w8@Ω?݃{=.)V6؈6Cu=)g3i꽸OEf.ũ菼MθUߥ_yvNX;ڥ0U"jOU.oyŹ@Қ7xiG{Ny,o:l3ɟ&\ȁSDߏvoog ?Q?^qٝGmnTwǡRKAD~2_Mcz]ؚhf<>rlQDmg)؉X*wزX5MEJqCd.u7-|L ?NU'UkR[>*s}|/_ʈ-*ڿ,Z8fo{<d:65YF؏3g(݋5m'쐲iJ@T~lNOQʽ̉.-N s4ƺ]|س6`~=o>ڒ*Qޝ]x}'ai[3jtya~kpc>p#kʦqh4nQ}Ц֓I1}7L 40u1lgYz_\}XM/5csx]e(%v8~&n:!rkՏbA\\>kr+Ҋ/*[7Fwʏuvy/;- a@X_fJɓ'a3LXϙcy#a>]ǜ/{p8'2^Tw2uwC5&C+`]1w'bPpC5=Ngy QKrh ->TIpSGVp_&:(.ٺo7K1)?Mӧ]7>/gie<0o'OlJKxyH닯/ .?3DMfiL\єgG5sfTY5a@;pf-uia9 WƲ)вTsi {τsjRRjh4k>n'دL_pp jVIv|P5>Y`F|ԅoiE^88uOmkݧ9cZξ2NwaNESywK5[ 3ӷq6in a;:U3jHᏵi<Ngݾ~v57@y~iB|Φ5'..9#0Ѥ/jʚ rfx!+D݄[)WRSFr2)R*)"-'-{%}4 he+B@4\3e:3aRK͠poԽѩB(e/⫅;ƛ*+n3'/3yrqcwn_,#;&O=]*#QZ .Uhת#oN-YlŘAUpk=cwX p=ù!.5$ RJenݺѭ[}RP΍FlE >?< ó;aP'Ykg.؇tb[pϵ\56%tET}&+]+D=Ljށx{yQG>Ra` nN8aƯM{$!<{ܡ }J9'w lɣ|M \0;3F-DGy~iLP t[2Tж[tN*C{S C:ҾJ8O53 =ˋga}]K| ٖ͊kZ1y6ti _eqwsrv(V\~0sZ3;N(.^UA=]`wZn}Oxn'F4K[̉_S7iFjԅ7&|;?BRCfտCq,sޘЇzWfЄ)LP=' ϥqOւ‰H.U0`ӖjIXRr~=#~+_6$l!2eM!4[T~7g- _Fa3*``7S:f33rIܢEr*?=5VikClLlE7#sus!h},)>Y*#dۼ@c^ ̓<)ʁKr00tN>hwOvf9x``V/>Zξx 1֠{o̾gXXJT=~돯9 ðwPٟ $vDf냟WVG\=nb1.=x70nߛt^_ӊA&}S=m`/Z#97+Yeb-!kcBH9Wer-rc&x!*ƔLpTƗ{nѐSLg\JE ơ/fZz5OOc2i]d$:f%ɿwa.:ps -q3<^&䙨goABmP_[G]W{y+9,Jl~#t#+S?`ݾa` ;4=wxMp>\'"խ[7UFjՊ돷K$^NVdE~24 o~1/bDpG$Wtuf6eg=ml}&ExW~}S%}<:}?ӹx%437ƒ=j|j~ܸs4qSsBhY漆>\)Y{th7v2u;8d2aϰbm/Pv"np=|xN1ۘѷE?@4翹+5r\~w0 73rlÇNs]+"Wf?5އ^K _OC9sj{\R`Ŝv'fq<'|23k|rw`z*;UGRVҭ3fx`ķt8{`p˷s4"hR^%qXt.\z`ذ=1e~ _ԛ]g@EUV7bXjj D}y%ڃgjyړO>!lq^\ٵ60{R~lV ;=ϰWQWn<=%k@j^XH؂ElDMX _MeK#(?M[BBh>Sc'?Oᇤnם 25'GzwSlmVJ$ɮHNڏK<_ޞ3tʠOXw,l~4K_&pb[;bد+ݑ5|j}ynxf`ooM=q+B-GysՕ]*Ֆrw 69ms|1) oBչSG*m򮗼^y77eOP9.*y=[[t.{..۶C]4Y V}|R{\ >ߩ*AeQ ,Pʕ+s!V^MtttaV-7)___<==\rq"ݞY݄HDfݎOqR"/MI ®RDnR>>>ݻ5j`2}nw^%Yԗ$㺙Xua׮]l۶GD$_|||N:J\D7%W."%tEDED P."""""""uDH D]DDDDDDQ.""""""R"׹C!5k֬XU.rhժUq """"rC?4Ɓ;SH/fRVD|u^\Mm{&??P[>Wiq{(ťL_'gȫcPR٪ɟO!Ob†sSDFeꝥ?DDFyG6Trfለ[+qJG?f83v+gjVCa%Ѹdm#j5o jOXwp-3kI^0u 4hKV688d,gã(>gKzO@ $HE*JQl"c9*vQA9(H^C {:jdyu;<[cٳ| y?2zkf1߽3=XYx03Fa"M6&y;;0ҹu?%,-ۍ*BZV2m]j"""""""S=d4?ӕ Ld.IkNigҎR<Y/2wf^b#vz5ҽU]~m :Ք eёlAֱml?Dx1^}Xrp&ې|šs<ܳڇV~D Ks<^u?x"""""[ԝ'| Wjэgn wxzt+>؟^h>r"SڀnŐZۗ}p[ {,7ЫO<]Êh\о3 LQ?Zӈ[?pt 8u>fƼjj;k ?rMY{'] 1&MI&g7nܸ`*bVZHQ&O/+dBDZ5/}(߿gbn~_)-;|s7a?- { _Z3oMA49#/?l+D"R |CO*Ƅ\;%"""""lugĵr7]G R8vlFCzΔNgdrɈ?Dp)8EDDDDnLE2{c -M Uzǔ63~A=6 f Ow<-w{G"""""7,QľZq\\;2n^^w\~8ӝ2],"rV͚5;R:KE (ARwӻ@NǨHAe7hYwp;ODl߾)UԢ.r]w_X+> -g}l2\g'p: )xTr\*Z0lK \NќW +\*/\^\*WUOKc~]}Ma…t6l?ORRӦk֬y`/Y{g+]r7fҤI :VZHZr%M6:Vko̙tM:UcEn\<1!f 8rw."נf͚JEDD Xv}_p5! wMJ1'^Ti;.^- ޺xzrSvd~x"r3]YsQ)&jSmWt#aj+K/o_^ۄa^&>áJ]EDDDDEQmك^[#ł PH88h/S; f؉0Q9oUwDDDDDnFm HӬ"""@ǨWRX,YBBBBAV-7@|}}RJqR0L.Б.L;\hح29,<u'Ÿ8m9ID7<8SߦYEDD &)t.f<<μ Y3Y<$l&L(`DDDDD4=MY|F^zq /Oű.g;h/LKLބ"""""BHa%Ũ |Qq\<У95)_DDDDD@]E_yzQh ]n|:mDzEg)E4뻈HS.RA<0MGY-<1!f 8rw.""""R,](|<8t 1̸yLx!6a@\&ɹE|{~$/XvߞA"IDDDDDDݶUZzKp y{R|LX0 7-8e?onX""" b)V5k,DDDJjVLhզt ۮ٣ p{jlhUtwE9XJ+FyntyjAF /FU&.\"e.Dxv:4#gƦt|VDDD(IߢnaOy ?IYd l*әL 3}]9Oe?!8VH&κ;e*o8D\e[𑝖JnN~?>B=Dj4뻈H+ |зSdă5fde*׺Q?Ƒ;C=ć_c(*UNۡcwrDd;x\jZQ48B|>J:E4+XcZ?E_ؙ#ЙxQmK swGdDU8>ag@UM?eCLu3Yfg;7+~E9{[:Ο1Vgo3X5yEUiK:np i}oecL,kϗ'ZLОɫcػ3n2,~>g {Y9tm=6%.n+??͇?OwwỘvcۧw=W{k1LsAH;qLZv|:~M`۞/v|gvzbv,)OZrr [(u7FY=#Գgs'Mk68L G".w׀\C"""""Ţu Fe kMc}1ЦaLJ;FFdK= ` h7sI-CiopN6mH=Lf>}I x!pog*u8raFb&Le|=[ mܮ J=}h;T,@ʻ18T.,]jk5xjgLb/dzYi]wK9`ğwg:EDDDDEQFo}OubAy\!OǮ0DlnU:i6|6s50[8p$AH9m4r I >:i.CH &` LV|NdXX8/Ը%|?d72q2!?[{f Kϯf'u'3qrjVlgZ*N7DPw'9Ep빚ͼm;aGdi<2G2WK}zvvC֍mkE1 f؞.dPY{Ẻ\hvk{uYXOlG H)WSWy2ᑿg;IK$TQ~a칬ɪ Zƚ/?cCg>f~.fكedc 8~ȸM!Ѹq_ƶ d咴s6ӗPfEޛ;Lܖf";w'tLZs<~M;q\ |:ITyV5!&5bynp(M˛lL:S2}6ILg괥˾UKۂ>HV[Quw/DGG}B[\r+έ y/{ދxVL%s7Xv 4Wb~)ݼ[(ƒd#Z e]mh?>T^;=]Mll)/sCD\ן 5y請5/:Z#98| x{#*}-| 8n/0 zA|7L۶KGezOXLK%nQ=y~+:>ϴ_b+C@7>2{R lfs%p4.U~+?K"sukk^YWHcˋ ysyr80q/jF=WN!C~nYNJI@)|+RDTfݷZȱ 3'F@fH 8WZNfEt\ngbp%I&`Rڬ]Ciٲk%ܐ+!WCgJE w^L% B5+.+Y%{"A%Npە-6)X<,LK"""""%ubXM`vt' -+09:\K:F_s,N¼ \|{ۻȿD]H8HNҴoM/JR*َ-, Vj Wh'dc -iU=_T͇jql3щW^ɉ[JE-lBy'sTOo Ng q"nUe@~CXд-K/qSU;"uMȍϏ2p#p?>ޥ5^)Q\Chx{v"""""7%"7u?NVFDDDDDP.r2BYځĝdMIy.D4m kRb9҈[Om!A svn&bgBnFTrYXOlG WHbX՞+-fTy#R߶U[ўTQ]u3C`em&܃ssT*GH@yL=w4WD]Femݾ ѧ ~ nǙk2~jdclG CqO(OeL/?=j{zw Ĝ%s./-Reߪ%mCNGW3wJiOwm{r<0'Pg?05"H/E;ju-Xt# N~kJkpYXOZ=0͂r_p4őVuQzLL $%%trח1O|78enw;I?x bGٸr/I rd_{rJtRN.}L!z.|uBbV#5t!JR|݉-φb>wm`vڰAo$6l6 Vdbas¥rtJkB ]fj6yy#f[ީ֮snJ#b`ϳٜ`1ȳ33xN|q]Ü;Mg)u5:Y?wޚtXJM] έ$.vH!q#m5>|CK~bfnX(IXXl.G^v%Ջ^zѽinPF,[Ұ;s9m+aHşKoS`.z[؝BEؕ3]͒ǓIIݷ/ssP$rrь7a109lM4v:Mboqӿh4|jҪY"|>ѶAEB˓Ydێv"!S#kHNwԭg5_XjYд-K/qSU;"݁45Iu7}]WݬN)LAo[?WKlX#]-| g, -zt>[Jvm.?)-. ;0z묀64N_X|hԾ^"%unӼ4O3- 5c?a;jEV\ O*ԅJ7]XC;SТcBr&``BLJkP-:Srs-ot"M3L5H6ֈB[r_hqGqu|/Ƒy",{qb%ZO}Bi|x֥:lF["(wp :߯c&GhVJ}\ ZE6.NDβfK-X &T3xEnjt_5Kԝ)}._eB$m2v=EWV8H:-__4׼JȏoUugPlvNOăטe꫿(#6e~j>z~#&tp"5 H(Dpţr\hY>z}Xp8w4z? na^frP3gQiloʝj`X05Dr8&6{sq1s cW.EDDDDnLͺQǽ`t^a5g2v:udL҂^~LK6łlguZvF"`wӨc+kֈ:\/~jcvr5yH(fg<;zJJY? Z]ז 5җcYvV<ڗwhElۘvw6,)1{L&46o|6-WDDDDXc$q x#CK3.t )Y Bts6ި_is4u6O=`$0E,KGO}Gѱm,n.}33s'ȍ[sz w @gH5W1W0im%[>glkx4//3_9ȅ؜V\\p˟ZEDDDD]$ritpw]xus!b>.Nq鎽8/,C=,)v GܬM P.""""""וbHsK,`A^>Sv?$Jw#MW}0uT V*{v-1~km+X,lS~]M?oIe""%Q&꾾_ aݺuw"""\_G#of9 yoFc2K&a;f+mgexR40~'n\"y#'4]D$*Y###ٳg-")) ?DFFw("""# 9Ӻ ieaIo2qV|?*HZQTǀ77a.#`?ٯ_C亰pBw~MӦk֬yx_"""""r+WҴi+cZ˻fΜIԩSբ.""""""R(Q)A JEDDDDDDJ%"""""""%uDH D]DDDDDDQ.""""""R(Q)A JEDDDDDDJ%"""""""%uD5|DdžC>flM>!ZRnMܜwKe|AÎ|k֣B/~e.qxz7LcU`lk_"500L<07#؃NOpQqz,-WUڎKkl?AaMQÑ2 ~Txd.g#r&NTԔ we-|' cs\}cgt7'|%uq&2p>ȔXזoR8׋xNWd_%OI#~ͫ/q_UN|bW5ELVq8%chf_]G}Jy,̼*m&z)vlMK5U;2ؿu&lioce-젍rF2Gi춁?T"oĤwR{tWDv\:L?txb9iYx !n_ǎfٛxN<&00䪝 %j46Jx~]hРAaR*5loggg_sR/TR 'ixz{ <ݳ:33ӅN$%3AȔ.t;v:z >tVq0ώݥ%_'*XwY_0zG/ڢb& Mө~7rF3o5&n:1dׅiƻbbv,ЉݎQw a&#/^x{:HO/y5' 8N HKK+xWZZ?#2Ow23NC[*Q="1iۥ'*%φjw0W6,@9JK9۶₫_%*39b!OY\\@okOOȞt_N's`֧Ӊ^$˗\J>OԦcȽL.Q ۷3gŰpeE?qps籁50k;9swXģğޙKn3Lb&+Dz}])&B"*cwdYl'l$uk ,AV_8:FzK~%f-bք}wK߰+Ni|6jPcΦMشi^izԷpG?QGhz&5p߷ػc0kcġc_pImՃ`>rlKwc 22a|Æ)2a9+v,MJMxejm /bٵ/*%?Q=]s{,""""r-ܚι4JAeƁqG~Wz6/aJ:*Q뻑&O:f /v(Lt_W z;9*u&.Ł9Ɉ°թF6Ø#DF^3I=L%X8bϷ rIܻ ֲv6Nf89I_mRdW r#d ۙLW5~/8 `#q_ nؘFuL#BI汃zWnTA ^jӠA|J8S-$|\/[yɿl<6bg`O a.K{o#g|v]i;' .s/7c{_ImQ%$=?+XH=Ir޹u~ tpٳ,$ⴧB_^+MFBN'8LX,&3yaP;F88ϋNRB*> =pJv⏝ yPj?#~9gvNG9v7zW8"Z-xH:DNJ[8H?zX0M&6[y+V˹+f[y'fwtlI%=JBB1 _l{o?{EP@AEERzOH# );? @I6:<;lgnB夋%""""""rnnMԭmi.FS΂RMI'Qm Sv))0&y8saņ%'ׅ1ÙV &.D:"{w{oc摹w9Fq%:$\Ja?`^ˉ:HŪ q}`oy99/גRٶz ɕ6?/ٸg!3}ŏ IR5tdp|8l6+9YG SEDDDDDJy'%;F'2 ~;q5B/g^Ϥch?g:l9ZuWL4'᯻;ACya~xٖG&=Ѡc`_ >)aoB4$IcY!M 3B)ZɺT Q/,c?a|?a$x@J+//,Yx4`w[v%eCKDDDDD*JE|ZxCRz+|ڶmDDDDD;Q r*͘* ł"((y>OγCϊa6laSrذai IDAT~7#""B繘\.ݡgNزe V"%%4c4 ..Q`׮]dffbt/ԨQݡg5G^zDDDDDD,VXAVJeߖR٫H%m6tR*DJ򴘜Hyj*oNvx7>|xGH)Y4!H jѢ-Z ::CZDJwe˖K=TD]DDDDDD"RQW."w""rD]D=5D1 !;E"qw=9"""""""zEf24#ڞinq|v#xw󴳗KgyшHq:{=F 7T"UzE*o$Gpl0t|n Eh8sV\Xxq'^لnG_Jx'JwX)Qh{kI7@)&-#ϙJE*~9nbzמ$.-'g=JiGuy9 F?e&%:VRqѫI|5g?v%|K3r>.vL B7qG#1}.?7M,.+ܱ?8B3iKQ}G**,D]lƘ׮`ݓ0J^{h._~vEωzkĘi݈$\l`,_@^ FD.8nLـY%>:. J3c&&vbsuXvoͿtû5%f8q]&fOZ^hkQF n wv^D jb̌X?_:G"(>Y~_GP#W>I QAWQ?F|n98,-c-i/%~ rű Kdڴ $4œ; ZDU%Ot%myxTtKhM8z𽀑_M> VQ)㒔?prcEno3/}Gۤ]("h97txmMceur^\yGnlВd~[=j8v|ǘ.Vx k~1bV :./Wu%_xEоN RVxIS.R {Ɲgrx/~av\>'#3c< S÷֝PxJ_yx`fn;=A4[xc:wnN-qs_6b#ZskXkkjĂbQ3{>]~žackd|nK`ncÛo-=>[޽12W?=_fLcGY0fmbycn|b?NFҐחd׊H%4䋧mE^#0o}T.!!!㏧gFCNҸיg4 ѢȑQՓЎ}y%fj}ㆳG~0yO=̊ } ;o<Ă6aA7ma!~Z.w]~2lJ(7͕)AeV0%"0-t h(EX 7 &13%?2{+˝s۹n49Q;m38Lqf37E^45 $=n2^aQ< ,vNّӕoM68ؗP4l{-0"{ZwcD8]ӟZӓqٵQgӁ]xe=]O|vNxy9cVn]ӣGcsYSh:[0q̀~e99 Þ䮎Tv֜J%)_~vj9_-39 nzAՋs B[5z\Ջ 7n o] xNT;"ck8 [~_c`W-DTgN"{?(eU$%"AZ=wb/gV1W?{=4'"~ _X`0}+WYw#76;}R`?LKME.rT80v?VY?&Ι/pstҍn<Â<WpA,!Պtp8ݗjRX4$L&(" [Q=xlVlv bE|~x>阆D.,xUh8y A!>chiIKdb6iCݘbb6f,x_혚XCxJ**kjܙhxZro͓K`ٳ 'صh"S-#j|.fVn|V4r+L+1E#.I`17cTR&x D*<' g]#y nrҐILZѓ^gAw1\4i:{BMabc f\@Nnو4q_!#y<[zN( m&u `m/1犷w3-LݶAX|І `pDlFOe2+=4'!5It 8C~TDh-<ﲨǜQ gZi~|asgj2)B=4ulIp>Xǡ&Hj\4o&\wa`'Vׯק~րT'+@ߢNdV,JB ˟Ga5wH|>y~Us>aVp_"W=GRl.uc>wCe] pl"CY%E="k$z+a g{y>>J(<Ů&5\^&5kW ILµ{cƦr;zoyOvf,\ߔhHQ~}L旯?*:VG-N]_G` n8y.vMΥ='r~~3\];IffGsKaeA7sk=~Tp?=,}/bG$҃93)_o=8_Oד0g? qw=8wȽcEϐqKTߊc̙GKuwP=15d7>Wr$ޑ̡􋵞{ҾB-o3/\ݨ6F5t?H>z F&Vg/OE ?eбP75)b?}ӯU'zȋqʹ(mMyovݮp|DwM{yW="kҤߣ#D1DCTTC)L\܂+8iG! 22qONJJ 'v O%\˥+"RX,fΜYߌLuTDŭv;yyyի9_7aQ\9N\D*wYJE\SND*"['S].",Q)Gԣ."zaD"Ru2"RѸR."Vpp0.+ZHD*ł"88ݡ E)u)ٰa 60sADp\lذAz"Rєz\[7fڵ$'';b &88ƍ;rAuT4W."""rUEDDDDDD%"""""""urDH9D]DDDDDDѪ"ҥKHԶm[WH%СCw """"R,^mwrDH9D]DDDDDDQ.""""""R(Q)G#JEDDDDDD%"""""""u*w&7ý6ru!8;qqxTsXDDDDDDM ?Gr:ÿ&"5JPǏM6nݺv.fׯ4M5jPDDDP.R嘸.\FKnC~z2.">WJ|CGUɹH(Qjr3>!H%D]!vBITYl:Uo//aL,>,TH~z֭[0DDDQHUqm n`< n#JEDDDDBHf)#u#c4ZUk;m惉+9phDDDDDu*ȴ[Zc0{0_kӨQ#w!"""TU OYuQ7| uP.RU/>-_s-czEDDhHUe}ّEvvGIZU.""""R JEYx^#1x{{(ž4 }<{ښ`1N"PsEDDDDBHUJa%r?gDv`j"DDDQ.RU962㗬3w|+\D]DDDDTU=x~*?i%s64tS\"V}D 햩XDDDDD&%"rGY,pw$"""""Uu*gWY0 DxA .."""".RUyxub19bZX-=HŢUEDD)Qj\Xn*F Y'$$;6l&LsIqnw`v)2 ,6;l_oJa%w؜ c=ׯ?5ڮq;iQ.vmvmvmoᔨ[X4 mG ?Ț$W IJXSʁػ䬅uJv%3jԨݧHyTN/m۶g}n'//X>}:z:&Lu2a&:"yٜ $je׌cw>Jƍ`hwLA@v&GR _s18nM^$ <548D4aT,HYpeR=(<ܕvsme3j^ݘq]IL`֭'ʶI="ed߬qpZqt{s3;Z]-s76X;杢^՟}`2Gu)iJEʊ3#E?efebaُQNDDDDDΏ#JEʘq5 /+^ZMDDDDS.Rl _ujtrpm{+җ@BB)q."""%JHYrf15QwEqvOZ^QGnX}JsHll,CDDDDbr"etaw{"!>{HEUEDDG]xx[9V abIqX`-Ӹr-t7^^x+ØYH6[ßKϢy)CRm,/﹋:=6 ƣBv?tuP.R|rJ>qwEֿ:pAv勈YhH2!~V_/FB&QWYYRѪ"""RҔ%=oͅ0eW=6DҚ+a(ރZsEDDDDBCEʒa7=)'nfG5 ,Xr Nn9"ŢUEDD)Q)Klv^M. &.(3*I0,_5}ZR_sEDDDD %"ea4^/½2C/˩g:~6'ܻLÊgϙ@H}wO!2Ƈ~Y9ږEjnY2Wϗo !C&/굮{c xm AƯͬ,\il_Gd=bB=-he.?K<ν4XQֽ͉yR璐KݐCF9}?*%"eѪo(ywV;a#lϪϝ2K;WwyKx#:b/*X>]av4;KϸF˟_`v֧$osP.R>f"nbߏ6բr2yQ׎*t8Oj_z ?r/ߙu< js`BK0xl-[ ;J2|7!gTdJEʄoE?k%ͣ/'ɋ;\gϟ6.:[͆5g$)""""R(Q)+8?ޥV/Y 3n[w|ې:bL=+ϲH5L~MjZ.~."""")Q).Ri{Umڞ%)d2&;/Yky8tIg,5['ūPCƁլOYG:SǮc8U^۷%iφimwl[Mk{Q5m7w$w NQϔTU!%SxѕΖd-K;N*FHU0-zWsw0"""""ru$N5J7.dmyۆd-I:¶X9l?[t[0N]0&7[g fNtmfTTrGtBtAg_wX^'-j~}]xWoH+)f6.aɚ6nuԊ0g,^ ﰆ\ح-V;Rnfo87 jhJHUeoMw0zX| 6}SLߖfQu4dÜ_ƠZJ1ܼE\|emgsY^}" ÜOR'C`MYq#Ԯrٱd>;1O?ibBIgQw`Hybi}9#H^ 3lz v52wwM\6(?;~k*3\[/]sػGfUkzƠ%pWvORY*]Jr~a=i߉z S2M2v"vS@6%beߎC5nJ Okշ}ANɁƝh_//=(9Iw)n)΃Of5XjN|n+u,O@%aU+y'-q4jxPI"3:fZRώezS){ogrrpP7pHczR.IzE*^f'/Xh^/(xEh ?}.Vi - ٗJ6kHP?[23Hw{"0#2LchןYs5Wz!X-4hfRq30u[2 umt  [&4Y=Y4qk4}+ xTYG54 O_7q9ڬ'6VӁ 4X8p8lح'R.ݎՑÄEsٿ|100TJ f;1b[qvz_F 4O:v=zMٲh) 7ӕ uX?%"UIցMlܕF+ȟr [%#Z6 yp4lؔW16l6牴˕fVdGYv0^}\egaw8(w9|l']17hC5'Sa{X6p:qp,rpڰzu !-S/p\4}S;j/:NM[[y&{W3$S.Re9@I8ڝdSWFt ȟ#ppp/(V-?rؚaIzj:Epnѭo ã3 &iRIM?ԭٓΡI79sv`Bj jCrJߑ]P8H'?r8jR`хJ vƷDy@ q훲p#VciѸC\7)s(~N>ȒҾg{jv]wÆKe`"m\~G^-䑗GuCHeA:l\.V>9H;8o ٘bؤ^3!C0dNG_͎,u6XN3kT>M,m ` %#$c:9s 3#/G%vvI?bk'i';vrZD)]G\I$}(6c%_>Yvŋ=<]%3oƗcN+UWlG:'cWs%izԲy{KLW:,Fj9^KTgoቕm7w$w >¡=g0 BÊaˮ:2Ҽ{3~w`7ĞGt@WzuojQjQ4 {ٳߏ\&AM:}!?O\צU4D1%"U1^ ם8`s=k8e8No7P u)2|Ӿ?uڟݟK%akoSY&AK^[[ J=2)졍ueӶ[:rյq@,06q33,=nֈX:sM87]Kld'p Gs%<ө[]gh"""""nu*BxY3\/;^`n#: NzE,N`\< Jo[sXEDDDD&%"U\Aw0]+n& Wj̠]"""""nD]q勷q lX[xUM劈樋T5$oť݃1^i7~m߮5=HD]q0xxЇ>C#QY5k&vمXiޣ9EHS.R[UW>%}HD]G{RqH4G]DDD*L//C@dƣ%|8.OS0Kx8V?ISO=șʐU[#"qfn3yZx¬9HD]DDD* +ƑlNg'=f,Ad}>Vd;q8sӻ "-QC-ObQu)U}ED*' >l]dKo@Qs\WkHPbʹsэL݋ cW=' }@z?+)VrEĨցgb:WDxK֡Cw ""A)C": 'ƍ䡭aNv\C3 l6Kf)+3;-ѿz&~#&/?_HĥhRc^=CYșƍ38DX^^9s0pbWRRR89_tR )a/m۶g}n'//X>}:z:&L9jӰw)o4]4 }FTi2."""""卆K"""""RިG]4 }FTi."""""卆K"""""RިG]4 }FTi."""""卆K"""""RިG]DDDDDDQ.""""""R(Q)G#JEDDDDDm.ny+wu%&2Sa \ژ?ohO V 8x\ [G2qhU_Bw(쭓y0<@bzC|H|# u275lzm'W/˚us0M~e-,3_.Y=12;]^4-9v+~\ɀ)lߑjfaٰ{ErM-[c׹C^o]Hϐ5)?h;ߓN%Ոe ,xժ9dO0>U. 5kWP%Vwo ~X RMÂ`?͗b`+)Dx{ފeل]{zc lO_E7_qvACsUL]9,%.Nz~䧼Vbƒi#6:k%K7Ъ?'.:ѧh۳9-kZCI58XFsz$<nHF}Yy۶'l|0j뷜2ݽBnyhԝ|՝/Vɛ{g ;m[}Nv.f5,YYd 5"hK 5zB>qC?UJC~%Ҥ&_1_9JǦן庝ze tqZ_Q."""""rVdf>2V)Ps[7n%V-NaY;ZSySvwa\ro|{K? i/w98 GRq'çV]nYذ:uϱ2=6tw4cwtL/tpojyy&kogӊ.mwt0™ٮyL D]DDDDD,DDWǵet+˒T⍯O |p󇇹9$lz:| Mp枌&xˏw^ɳB'238LVi˭įZ V/r?ԩn;p{~2NxǕɼvr|\[NpmdΔ?ٞ>̖mkؑdwP[;#k:s xw7~q ͞_r8|^׍s\o6nVM%""""""Ş_40)&`~f!C(a1ncHw†c]= !{݀ƌ_\t\ gYP=/K_XٖAi8ݞjU9K̠2>GS :uބ?Rk*UѼD[ٳ{ ,51T I )êX^gRF\_sW5&* 1#%<Ȝ̩T%rUp*6ɛm۵i䶦cspcǚC t"œ9s۷y{睫.<)))L.Y֭[_DDDD<|;9t\UKP]tYx:B˲Zp}8VښSg'>>cq8ojԩSڵ9?~| z#ԋY[ebY:W]߱'57f̃Wi/"""""lufJOGQRx:Bfr?a7x%""""O= IDAT"""^DQ.""""""Ex%"""""""^DQ.""""""Ex%"""""""^Dy:=%"\\\C"""""""^DQ.""""""E\˗ӬYB9u uVڷo_(֪"$8NzaH!9r$/.cG]Ņz:)@˗/gDDDpSzB9zE I,]JZ IDDDDDf͚Ѿ}{/^\hI:(Q)46A~yǘ0ACEDDDDJ }-""r&JEDDD.@6m """g""""4ș(Q)$NO?cɒ M[JJ 'w+DDDDD"4i҄mrwShQ.RH~is$c8ZLNDDDD[\m۶ѲeKz- R(Q.RH,It"""""R4iB&M^:,D]ɉ ."""""CӦMy9rd_jٲ%?c[d޽rl%"""""""^DQ.""""""Ex%"""""""^DQ.""""""Ex%"""""""^DDfڵPDDKxx8aaaz:\DoǕZv-N{: b ֭[d"RޔlY^O٢ _Jq*^U2v+c[h`urϷϑ'')Q)j:7'2<ϓ䳿c&4LFyD <_iVidf$ya]S!$&rNx}f!07=Õ!%6ob8XDS>X/s+͂܋872ɀf5X_Fk?f5ٔ#mb8;FwIx-L:u^ VQ)⒘ȴiXz,r!ZWqs^{cPd>M5NŢH;WJkUk I?qs<0iV9} /**RJ,]GZSGhӯrm1\\ŴQD?**U=^Д L;wz,xm0S~eŊ]W>Kf.60ks{4Y+8׿ŰٳóymȓeNc~ڏ?]X8 ϟ]oWn2qυ/fpϬ;|O0nM3\6Σ3.|f2-`Ʊ\wbͯ_#\rOό<:ıo0yyV I#&џmz0 &N?xׄa32@*s~Wclo>̂_~ClX([5[ԠL.|]~B,9Y^==4WBY=^F@I4ޚghd͞z2J^i`"3SdWFh7{Rf}>ǎ)-DT:O$S:e ]2>}^&_7̓|qUEʆR~ 0Mti\,D}8bm=cbmEޞ˝_; 5?A7 01M'OcW;Q~T^Lc#ǵ֢ (o]iQB絀^f ?3I#ۏvNxusfneY+O5ϼ_;'Y{HڄY0BZ2O9XFiꠧM$e|WkNUE^zѫWsn;\(ߦwP & =SQ)RTxAR.Ra3 iwOW=f泏/_̮ w1c )!`P\cij2fnntt['ԟWp] [q}$%K3If.;fȭ;Ѯ}G:^, &XQ!;XVn7+@"*9֐+O9 ɔ-(Abbvq=z\+6{0!GtZZMܮ|^G~f|.>Θ阆xU8xn,-q5W ,-GYh j֨A1xw+YWFe,"<%VT6jܙE455<6[d.)[X\k"+Uu_sv7n$jbfVb;rS3궿W|rmyj~SO,=*ER ݞMs>s3' 4Oc.[*]z3g沉S36*[,®73?crrFDV sU~;iFux9WM_sϭdWoXcm(qd6FLL<۴c4ܾ`F&V~ |C;.S_J :ۏm7 3o!JM&}BMD\fyIaZC)VFNHؕ;yryߊ7zvԮ]pr)@h`M`V1,N=w/|j&ӳ~ | ~͵s:V}x WW>OBl./.u9` Z>k?UԳ'%FQE;bνg" j*Z+1`̽?o9qS&`]ʕWӆ|ݍ{xFIw1n,O;WrO&nlnH@/9|,K/dmaT:r2IT%v(Vr1m:tFyw_~47fv| V{'w`En[|ѓZ7{b~G*ˋȻEj$Xn'Na7zr⛏Y䄜Lh*ȧ<]nɊw^bΞlMX+^HQvmϝLft- ˫>4_塕};ncoS~r7,c!XB(P\Ωr˘(uAIu(ܝ7JR%aLXRfM*(YǣgO5Enydk4,l\[V\ә~cn;f&,ݱ>gt׮S4+EQճd2Զ7s,aGVNtdk^OLe=taj5<0(:C<{'tDZs;=U%pL#o`TR۹ghq|݈d{xdb*CWt1ɑkq5aA#5;NQ׾ˇ}lZDTL^v83 ]gxC)OLo5*4ɧ1a<~t1Z71zQ.n~CY&y|~̡BJONGr9kh^"*VՈߨ<%OO3Ǝk2q s̡o߾w󤤤0aBNj>FI0uT BWH!X,̜93J2"Ro=nq8u̩Sҵks7~xQ-j\['S]."ō,%"ԸHTHq:KsEDDDDDDzEīFD#['S]."ō,%"pG?EDX,n<WP]."ō7JEk~z֭a~pݬ_^z"RxC=D]DVYf 7ɞGD伄F=WP]."ō7JEī+"R.0Z]DDDDDDċ(Q"JEDDDDDDu/D]DDDDDDċhwbnɒ%ADDDDDy[t"""""%J||έ"""""""^DQ.""""""Ex%"""""""^DQ.""""""Ex1S1[Cnv͜ɞn_􃞄:eSl5*?D]rӹ5xm&}VXۓ+} TƍEn޼yX,֭[iԫWӡ\%"DH"˝nWJdM0JEDDDDΏF:&n єou3X3,G.g[0`8m}<Hu&g*C31^XRAGG](.2s`f:HreXEsƎ`B:hwJE+ƟE3pk与L7 ZŮ"XK*޸;Ook]cfaX9},eTe9%"Ea_f9poڍY{[7o|+QL"@HqD]ǻSqHQH3 0Wg7EvaGC֭[wG:qk{AoOHHO=G>{2_k.)ڮ|9 })ROGH{~ }.ehp g"E"66VEDDXR.Rt*'oq)Ҟm_?֜+a/FsEDDDD=?ϻޠ2u"EE+ 5=B娕ɞ=;d[,+\ X*5Ae,"""""rzE҉g47>~'%p(ofIq*pV.~L3U;x"E,ww=BD@*8e}[6"z >ՉnWפrT,>&naݞ 0FӪ"""q1bOqVjxi+ʠom蟔 IDAT_lJ7-uqEs?=k7K.dAvi^ٿ)ᔨ]1pe{C {]Esk6.mHXx ĚٓXcLq"=#GaS.- %˲eh޼yWސA9?S.RD#92bafCռ"&MSk+~2Mpm6n'V6N,;Soe,D5&6d8QSKş˖-[t_yɗr6z!gGD](69_ޛ}lI9zmg(psu{g9ļ'JUs9^֒Fg7HwS#Ghu)0 8SEDDDDkUY61;ۤG25E\LZ1Q'*Η=SO# z)Yw:ql'U VÉ &ƍ+।Yti{vl)|ސO9?8S.R$,8IQP.i>ex0|IgC\,Xrf&۸l6vSp9\l&޾7Q.RT 1A>K~E9f~) [|]ja>8Rv4zWڝew?RT 2lRr ҰwHHpd'ڪĝitEKnƆ+ۂH%mоn+KEjV;Lj#iDDDDD+%"Ewc8#md4غh [WЮ]t0"""""ru\* $oXȼ%[Iq݂NRZX>@5M:ұaN]07i#MpBܠ-E%\n]}!0:ӼY)i t.QRXW3ݍ܊A",^l{twGv 3'~^2+ҎqT/SHb_Ϩ-y:R.RZ㸥7FjAT l @1SaLjY?g:VU_0 _w!"|;XV~#N|Hߗvv/5mcYĵTSeylQy񔻦 5|M?؜E#J>~]U #y f.HP/7)]i`)η޻[iTa⟘WEw-]?I .o1 SHi31vnPL>_nk'6$6̎a ndmFYP/@\n?H k`-SFl|or`%~[Z b`/M9Jwz(n)LF XjLl.6q任;u- qlR]Ec5|(ߠ ّyʎ=^kqǦTcFT3998Q>?tHcPYO-[,`V]y}^Ә1 /7IO;DPِc%,{IuC:HKw 9oaLTo3g"S)\%F;G!?BCK9)o4NO##:GHl!I'% Ǜ !t ۚL,1-;WLh8vJˬx++bj'W?mQԪ=(Q) oh&6$O_7q:Xm m M,M86)nt4!n\-gW W3Ds:ql'5Fv+NǑޮouG*#p9`a:q2#6\$أ QXzؤjdKpdI驛ױ;%%۵ ޯHPFjF0N"1S&',.~.η]ֹ(%UC6}ZD+/6ʱ2QGKI.j5 SLemتUjRƇ5#H\rLYo6UWB6c Lܤm_Ѻk-ܧRannb]r$5"RIr'`V،XN\0̄XzK=Fp,[&3wWc!Z3:5)_K!6K˜?!$ag:WE;n%5{%CzNNo?_;d]2[#拿]uJIf qF>o sb/ͥl+3ݻtBr4ښziF(tx[IIqv=DY :<}!LXWY&GFix1%"52]^0swe8=.o7R=j/2֪7Z=؞Ci[ԱFA4=u_4]D:=NG&^^;m5 =hM&vvf9#cc5"v tߒDJ+[}?Ֆ7\qamۄVͮ4`PHeRwX#\?;~LS#ok%wnL%ݢMDDDD)Q)uC [:o|+x <RNHiۋw|iYL??@HdzȆ>{'ow)|補/'>B:.wl֋晹wӗWm*S(JEDD}|egMvv:;=EY2U8=E$󋑼<ӡS>pm|>_"iK*)"r^T]H e#NnC+xVTFԽ~7 ><].%f]YD Ҭjej5lIQ߱?wwwy}Pf.~,KRM ٷWݯ+4-ErUӮQMb:>卐kv(>ԡʈ9^Ĺ/k[֥jpjty׆ LٕKנVL-~_vQ^TO_D23{/υRJEDDrn2OKYfb<6`ǎ<#7lf\<>{$~ǔlZɬ#qm뇳姬ܼMfps}w0 6Ig!7ܳ>pKX^z  ՚ ~.z b2LsRu YxkSbk6㱃sWP;ȫ3~ G43Uْt(GPزSzܙە勈1vXsȐ!CX3g}=}sE'%% :s%Kкu>.>>cq8uSҵks7~xx%"""""""^DQ.""""""Ex%"""""""^DQ.""""""Ex%"""""""^DQ.""""""Ex%"""""""^Dypnz˛ί&8=Q RSaKFOD?e*֧`du&ep =nyoB)D&r󎚵 noMŰWc;KI7ly5qbc9`e\W9bαc$׹aE˄Qh~5=vo^ndjiI^IȹL}5[[/fNLܚF_OmLl#]3=FmZ$8Fq .GIx?_' qu8$~7j6$,R걂'^[wS."""""rf4א!TB:Eвߪ}=4gyƄۭ]r }maLhv Pzw.o݇0( ƊJ}WQߒH%!(`Xuu]u]uTltUPTEi"B {g~$ ~^3<7ܙ3gΙ3gsf}N7#;7n N+Ӊ鄍o`󹓹 F:.ށnwk`R#r)J7ʭ$;WuQu=\U?nyr7|:q8i<ʴ7|Z@@]DDDDDJ~14]lIKbe lHF}wVdنL(`OS̨jߙ%Xs|7Uw )ϧ q|?L&L:#ܬ1Fy}Ue׵} ;vSEt-ʌߢfxqXiJVn#*=I${jc ^>DpХʴOdb~Z-@H RlLL;`RP䋿߁wauEf<79n]MqgtQu,\KyO(m5ݛ랼vڞy GwO8Sٙh oڞk[m{ V<8~*kM 1/x8 0ח'cܙ,2h;<={ 5E/I8eZwOfӶ̷""""""2+$ (,WLaсSXH M^^̻/!roΞLNaK$\f<^y|u9D~r^ L="+/-vyp~l{jOyͷ Cヌ?Fmc/*27(,'Fzv7nvOSq}xOrBXR0xMwui= 0ϯ.Q."""""R/~*lOׄ,6ouM NL$@u^ Oc@`m{&_ʟWR-tq/KRp'Â=gpۼz7-f ~|q٭cU+&ĮKȶqO$Ty Cۿ!Wqì' g{f!kزrw$})]y֍K2P_Y@~ :*NLB<۶fSY4e{(9n:#7>;>-f)[ޚ7:W1d~z9;K01)J欧۠x_^d*o/f`8;ЮW/ `d.eYwi/}Bu70J.xؽ=w;{\Go_C^QSu{~ O2e}Kob(+sUOƦXxO%lD{O+BwRg~[-+1C0AAAv z 4H? 6'ڡә}G7꟣`$NO~ĿN1<9ض{3kGK^8 T6;Yt!8f0w9s\>̩?Қn{ޙwa>=8F'`h'˾<8iMtt4ѭ J Iy\Axx{Θ?V`Yq`sS%|gg,.=jٍ~-EL'bܫ GrzIJ(LI&7(^{WEN['гl3} 34BV,YK¨tvR`#rYIZoesx úf._5ovv0Vq503kH^g1dՃ<(7iD]HXvb$$:`m&`# " 'x*WF13LJVzwyaOn>F79BfF6m)-/^x&8dP11mu={|4wf$Uo6XiP;-Q~eEОxrH"[h-~%;Mdx0L `62M+UL[vcdaG`3'<].ێ_pP٬Mj *C(Ewl5CPHL_2&QQG|s B[UޡY)8P.{l#S~ktʯ& q%<Ǣ0a7K[oO!< hQ EACIۢLl&wz4tVO|}.JPC'0ۮ< Kdhk~rJ)wԺFdcl`MGibev-6P7L\VM׳ygMls(ϏۅSHֆUTd@`Ӭ=Kþ\ 2=ꟛDZÍebֵJqdyUz& L{!i{Waa`(řݝJNUd\ 􌺈5 qnjvABH 3ӄPm}sQffOjZىopQ.i=0[6 hs@]4JlL&i(5+pEgUv:K1L+f)%.N pe4( ?{Edf6z]i$m" 3<l#ʳfg+>7&+;N'*΁VÁąaelqiw۽+kJ^=Yؚcnx0sco`?7c/g?<tÉӯ5_u][jC׮M vcPDʶwKJؿaUˬ㳈_M=[?wuc&aOFF~&0طC8~^5*JF*Z-i_/¤$/R܆* =#! 7w? Tƞ̛Og[f iא|Qi$wwa;S(v&>6.ռ_Dѱ;$׊Xl& 0E! k44; -f4M/֢Շ>E!-uFS)GB>Ea-kQ})Pi:=ur$XZT֢ՇX.""""""b!Qisgs·Yp! ᰮp.!kާ>/`dR,C#"֢;>pDʮ14Gpzu'դn06_"痙2?Q!DҹL~kk h4qrUN [?&FDWyLsUSO` AAЏ /9{ݧ-aN̅OLabqN:G2'Z?S-iu־؞u1qo`4l~5;_Xbr $uΌV ~0*}lj4Z>&]oκekz]`tذ?["$Θ- -ujh EIڗqbyg|./wKܹs;w-dM{fpo=>OilQ\3/ܜT6/zƗ7i|yMN疐/H CuɼbMyoڥvӯşc<^ FopQ~a/xbD/_t.ZdYtirrsMQ-`ϳ)KcSLzi}6[CV~p3tn7;_JGíѷA*(PiWJ2t ?8Apf 0l~ Fg!op""{_K_6S\ 11eֲ/˕p%+7mu>#G1US}W#km;p8&_p]Q9p|~#{/MBb\{*+M7nfxvDGsﰱ|ؓKtlwκ+yLL#/˔en݁_^`iՆ^MeIQgO ?1miПOHN/`b$8qO`$\F:o_~y >v:ݻ?}쬵י11t4k 1᱋㫭3WWz^1]]G{?a5|(]˾/X. F\tv m,,_}mt>p4-?dΕ`aY;~2˶/F窩 3iOJ9.?ٵ= .| |?&܍wP:HjSY~02imIb;Xq睼̚g2ky1a;Vs?]eyv%fw3=\;-[?c\ڳzHڬ+JLܛ_{֐GyBT\tޗ^LW~~sq?g-Z_{!?;l/O4?/铹{Ps7IK_ϟ_]vr2Mx%|g2?]K;Iv7ԼIgnwRP5/^ʓ'}|6WWo_4^uFxx< "QwL.Ԏv?)Vx^^e` dXz=rЃc`:p0Qg0,^isU\+Eg%eI<54gΜZs2gΜjqO||<]zn(_0G_sŔQ8 |]KXsvϢӃ2Ncw~\{Nօ`=TNId>WyNO@BOfx?GgH ï;ÇDk/4}3&NV` a5SSK*3DŽޞ@ƘL20<] >#y$ix۹s>\\D|㼀c'?ЉYnKm: f-XC']5L:Ēl>}/%~tarm;/x23O/κ )OY2/]N=`xwdI)o55LVV&fY>}6 KL)Q6j[~ӆ$遐L45]@Ri*|‰⫲IDAT6<(No5/h!'qM|S*y**WqxYza:m|0z֏=4^>PZ[\ʖԛ?-3/;of}IY^+˯ċ~)Ouw&wON>:{~RmT} S^2`7qb~veKT!3g5PjwP' eƁQfuھY~W0Y?{<3_ĵ/wsI/ :^Q L#ÕNZQQo -c-8`[EDd/xHS Zݙycg i:q^n]d^~gYmԷ:P1íOZ ay{HKI#[xGٗF)%!WgVQq d7aײۖaeUcռO}a]Uژp11Cqn"#ן2Gf/4 %Uyw0~MSHn /]߽-!{1O?y!Wtmz(1{_s׷ wnW@]9;`XCOx?5>[s)p礅_^#o.w`f#t쀇=)x`iu*WIsڂ ?~M5n*F^t/}7h&9gjڇSP"y ޹80v} oF֦( =M $*wY00 7H} L L7}ɔ1\zμoz^Egv^χ~`U$άO_өf#p3@ma͎*'lr m7_=ȕR޻,^ilH"}RٻAe))䄝H3"$`d%Ō 2)`d٤rbCܲYVuX߱([J{A&KfdJUS`mC1hEwNzD8QȊ;sc}їy,!/c{_/wd}cqTy(anJL9/`2kz5[t)uIY<7-=hqQj-K}m~LJN1{Xll߃.&>B޾ E&&+#1d-wVDiLCnY}X>b;+bJ2x\Mťxp9i—$3%pH8SXƋ,Mb۰ 9-,;f,d֜-n2ս9:-R[{z <Աj5(̠(+="|b+ԊMSieDžVK<{}rw5=5oŭ!;λo略&]ڰm`%W19 퇟?jT-/r+v'fPeu[)úl Vd{0v[_^5꧶Γ~]3TBnms!=2ݵdM, }mh:4N X/?{9;&23Cn#i3Z w]#2|Ǔ0!vO^q_a-.)aϘ0dLglĵ;>LLg&T4N~&WcR3e 806kwnĵ@1 ule?;_'SͭTrON^3MA~6\޺=O㜞't۰ț( 0_<0 |b (xKNs<09n%;sƓk2>ȩଛoyW`$^?33 cIW>XE[{0vDv}|0M{I,k(qq$ g~W8@y^pƸØq<'VZڪטEUe^F'0-L;n|GrUx '_PJfBW A8{+^Cg b_u9/'S߾V6h>54۴i7x"UK.eРAxB&|~?| ^v6mDbbbsgEZ&_L޻l\.o@jZ C `8|d!5_0:Ny4jbS4e˖1xIkzF]14U11;^4UI94bfanZ<e3xwe7<jaVy~>̼rNm*m :^IK@]DD&-#6Yjڝ}k:j8JDZygT:7T/Tia4FB=W6ju4#]#Ѩ-OZ%~ZȊ~)9QCid#HeTGUx%-uFPZZYh.K'E>Ea-kQ}HK@],^Т߿ -kQ}XZT֢Ԡ)Picǎm,HYhQ∈Xu Q.""""""b! EDDDDDD,D(P"""""""@]DDDDDDBXu Q.""""""b! EDDDDDD,D(P"""""""@]DDDDDDBXu Q.""""""b! EDDDDDD,D(P"""""""@]DDDDDDBXu Q.""""""b! EDDDDDD,D(P"""""""@]DDDDDDBXu Q.""""""b! EDDDDDD,D(P"""""""@]DDDDDDBXu Q.""""""b! EDDDDDD,D(Pgsg@xhѢ΂P E; """""҂w Q.""""""b! EDDDDDD,D(P"""""""@]DDDDDDBXu Q.""""""b! EDDDDDD,D(P"""""""@]DDDDDDBXu q̞=!"""""""JB䴥~sIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/managed_dock_window_popup.gif0000644000175100001770001014342714623331163023721 0ustar00runnerdockerGIF89a81 & 2$C+R c #3"-K%?e.#.=P.Pq./0,101G1449=>''>AL>Xp>l? ?@]@BD:7H6%KFJKPaLPQR'TQURVuXTZT][p\__\gw^]_OIajbrbyc>cedddee).edfdgH0gfgghfhigiilomd^m{noptx{vw|T7}уeKrXƋΏ(#~rŒ!0{Ww̧ݫ˯ǴûĸƩɱ ŪɿʩACη! NETSCAPE2.0!,8 & 2$C+R c #3"-K%?e.#.=P.Pq./0,101G1449=>''>AL>Xp>l? ?@]@BD:7H6%KFJKPaLPQR'TQURVuXTZT][p\__\gw^]_OIajbrbyc>cedddee).edfdgH0gfgghfhigiilomd^m{noptx{vw|T7}уeKrXƋΏ(#~rŒ!0{Ww̧ݫ˯ǴûĸƩɱ ŪɿʩACη@K3a$\!Ç#B("ŋ3bܨ#ǏC)$ɓ&S\%˗.cœ)&͛6sܩ'ϟ>*fH*U 0"5jʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ È73c)  ĠrE˘3k̹ϠCMӨS^ͺװc˞M۸sͻElKSaL2jTУKNS νcNySbAPo Ͽ(h& 6F(gfv ($h(,b乂)asb+yH@)DiH&d}/6PF)TViXg)pa*ˍ":2hlp)tix|矀iv[w)¤޷efڇJ"ZB^*ꨤjꩨꪬ bBU()歈Ψ歼\+kd^j)*i+3竤[bi|nl'"k覫kff2(뜮bJ(,l' 7G,WlwZ/ *")(ر(rh(.w* 3(ˣhw(-w ,z2 ̷z 褗n騧nق1d!?H"HL& 0D(fAYP-n6;Wo0@GP\bD b~t;$< !"| bNF:򑐌$'IJZ$48LM̆C.D_!AYXnN,gIZdY&I)P"E("sc $3<%dAJ`D'LjK b代$Ԩ@EwvdNK,NHE( Lg{JDtj@XD:ѪZXGJ)T`11NIxW blXO`e~ɺ `KMb:d'KZe.(Q1UbT1A0-f Q^"#9bh1πMBЈNF+:b$;8Gٓ0"1f2"c(! bd(q[xS']:J4i/MbNf'c&l,D(n \' UyֳL8G{Mz7 |?"! 84TD&PE@߮gU‹6J?7Cv6[O 30g_? *',x1=7&>nPԝyXϺַ{`NhOڍ L,DQTl(nQ ^}ڕ.k c-, jLXfڮp' ?se֚LnMқOWnm:-:ӎ~o=ԓеOqO$Rۺ'++VU$$aiEұK<%$q O`b}OϿ|'|5n '>SZP ؀XG{\Gd|Y~P~PX&x(*8vmׂ` MRg"H}#؂8:<؃>@B8DXFxHJL؄NPS(y ِZP P @ Y (*ɐ P #IPP ` P ;y  F E Ő F%iyXZ\ٕ^`b9dYfyhjlٖnmɑy |GWI;tIzw 9Yy9 i @  @ F&ɒ. 9A Г;>ɓ 0K`M9HY9)rYșʹٜii@ iy$i *yə0 XCiy=Iٞ0 ɀ `YV Zz ڠɗppP: OGy Y. ;#ɞ) }WI *9:DZFzHJLZZ5 0 0 @ % @ W Y wP/i)   :i 8 &99 C9t  ` p Mzک f Xj yZX P @  `vp  ]ڥY: @ )Ay 0 !꣸ i Zz蚮꺮`1P P @ 0 *   xY N@ jp P P̐ [ J ð€ ГnnP j @ ʐ yOɮHJL۴EĚ Q0p 10 N`S0 &pf PpХ  P@)@ܐL PU@ `QZL0 e@ T 6P   `) `jN;[{j p &P# |`@ n L` Q D )ܐ ͻ !0`1 P  @ (p' 0p  ! #%'*,)ٻ3 }L9 ܋ !P k@n) J 0 P !p0[pAPe :p Q@ 0 U ` x`Ȑɒ<ɔ\ɖ|v@tB c\I[iZ.ʬʮ۰S pd kA>  ` ;n 4:DiȬ@p @|@[ 'P[ P 0U ,`ǬP `kpASP z7UT Q <"   M^;y ֝O p fYP N @]Y&Pp`Սť '=`NPzSR p 4p {> {J:uMl~owr>t^v~xz|y)>脎J2IГJܠTm Հ* P ǚ<{ !iǐp K Pňl7 ų0 ǰ 꺐 _ܒ1:s.pPw-z ڠvMxuyqsyv#sNEy.}}-@ y&Z *w;:ݥ pO < < Y = t2 ǯe . "?X쳰w>P.?mxy.nBFYOqAERoc]O @瑗pr/!0f 6i0zYvwy闸ptP*Er~{ɗs] ۮ u P@  M  װ poxk?ewaU qUT#.XotjNy0<_u@QV\p%pA s!-YD]v"1H3 -Z>t9K5męSNv`NPtTiS?lڴϟ6gR'4mMuhx!e-Z\Ck^H̯41e\MGW˫5#H#D2I%dI'-{Yx%( ,` x//Qy>AFA+d(+ 1K%@DY#@1r1CX\Ԍ4XNaFDz1GCҮApK]wW_6Xaw9)a=K F ~y PxQƟc~xA %hyoe1&hvB@m&AYjQ92/R1$> ZKO^NԕSQuE1nXaCZ sa+"^F\.ּAYF:ifzbI٦Y1G~DAp b%  (% |qf-i&֞B_X`'lk6(9ЕVN°QDvEX=LŖYVfK%SzY=wq XTlnN(#RA ^*e0ȁ$:Wgy矇ɧzެ686'}Ń*=oiN@|!FT8J1Gh s` P',(&FA VPN(q-$ `%f0VT*047V1@"! #ZpÀY9:10 `8B3Sͭg=2ьgD4'{V7U`8XG LF $q7x؏ U P8F i |ܣ 7W$$@ Zk" 3 EV,z6"0J,`bpE<ܐB (1a")%, s4"~3nƢm$ @ -X8 0Frc>O~7-|O}(P9 Me,$pdc 10#~ l *@  Pp P;M]_>@XBp`VDXFxF@ "(dW=PV@h /Lp2ch'x%4!{Աld P Tf4h8j0qC gR#N;΅hK3,qmä8Shz& RnY̩+8SH" V8 1 X, PWZ lu ^ @% N>Pp` 8s3*PTB #v&w>/;a W@/7.*hc&3:q:@$&Jx0 Job\ʌ Ġ0A~o| k " p#^Q?, )h+K8@}Fyj]@E{!H0oFWnU6L^,p&H@ C[O{Jt5dزZ'` 0bf< Tf4X.AHzI X Є ݇ÁܼЌaU T(ĀX@ \q r x+ъ2aT6!^V[0&Pf(l-`G 06Kw+! @U/DL$,@aPw9N<r`/aÎ׀3vEoW8x72|Î2>)lxqԆ)L@&( DP]&T@%T|jٽjo<bp%N b}E1b8t+'SWbc_B!$p˧hiW1,?@Rr3y>Pa,xeIZY: i/d(Q fm,B vBDxŧN +/8+rV(d+)Q W$#M7P3Є+ [hX7#@_gtp9pS:@=  sxNbti@ Y.I/X蛷1|J;сZAًiϡ9TN +Ba,4{ tk  T' = xp:y \]H0*gZVA{WxZ B l!?b РAXdPBȋMUdVd-tE&oZşk5[ORoad@[tCpE_F_LdJ D y\h!I0XTudRZOkG$f\\kE^t_t2}L[{tf9aF{; r!Œ0 HЈy4ɓDɛŔs`WЅbHnXQTJbKR;KhPlڻڅa3?mAA@?;FKر%۲5۳E۴U լԬal] }] WQ'H6X ?0$(V)Mȃ(RRI-G!=p(BbXЂ$0 X2N`rȂ2?X2ZM:gZdh ,!԰]TZ\Pt^{|[uعغ-ՊX(S(<80s`8M#J9&xE=沁`Ix'@Vs(mHHX҄5H= ;8VMrG`;VwZx_`mbmh:!a8=)y: !&"6#F$V%f&v'(%FN޸([m 0Ȁx XZ PH07%?,EcH(`DlvXhznXΪ: FxHRxh5@7@Dh[兏ZRlZU>^ueׯaf[\]^_`aeb[ T9c=GEqfA9'xeRfh""MP+XmX 02fVKJYFWSxWȃ7WHW_q}-޷ZlPu{QuQuQfhih>&6FVfv闆阖陦隶&>fWzIdeb+an8*>0ncsxB(s`cgcs*Єj&E.^Y insm0l,MgZdf@DaaQefp$P#8#lͦ=&6FVfv׆ؖ٦ڶmcCaަ[HU0D$`*VH%hV -N ." 0?vQ_T dP)Gq Pq_!hgVwWXY F`І$G.s^`^hU݅h4ED^(@vbИvvWzY`8\Q Y`\hvf`Y\A商N?"0P'! Txq&Peu__`߉_(ZƕQ=iG<YeW@ZvZvYYЅaׅn'Y8^0Zz=!ad=Z\=UG@e0Ox7uTW x{!FcTX~?yUމ`e[h]Ѕd`Fċ=zk'z] Zw0RbGշv`Q\ hВ&xhP7GW0ZuU(vo\Y~'E`uGH v*65x` eTypeF)X!?$2kѬQvڷ7r#Ȑ"G,i$ʔ*Wl%̘2gҬi&Μ:wYR:a(ңpū-_RzZlҥ,\vՅUZ%%pYz.ުdiekj]^|Il-XXȒ'SlXCeY@2,FhW_5زgӮm6ܺw tơE.m4*fn V,]fuVoɥ;o^{Ll֬Zx5Yo]o>~7w .Ak : J8!F}cU9TQ5^YB_RމXrMuW."5ָ_#`B -t2 zf!I*$$QJ \SZy%Yj%]z%aI%Jf8͆1!r6hՌ6ʙT(y'H@CE^M*(:*ӗ9)Zz)^dfphrXDž(yz*g*=hC& Ed>+FD}N:):!㩚Ɓ"z-ᲊ:="$dCFΡ{EI7hۓUB;0, U{,)*H -xR Ti1{1!<2%lXw[+(P$284޻3Ϯ?:x̾s?PI;4QO1 צ鰶mQ8re}6i6m6q=wۃH'+k)P ^Ss787=ε< O3XO9衋>:Y:ꩫ:뭻:>;eGY7 NK%-T1)oKz;< uDw[]FeZ5q)RV"-r^"(124\>55ʨ=~# )A[$5* ܡ䂮4BYgјF3AqE*P2JJk\$,]Jg8ge]򲗾%0)a,e8AN(J|W:8axrV>qs<':9BKZJgtW#Wjڔҁxx7I>)hA0WZ4{% b6M|3],g9өխzq;շjHB= \O"v-BxH#k_Jܨ4##cHL k?Y)8ZRXI4U3n w|U;*jS(MCEl晎$~bMb< d\.l8Usr*4 OUWOϰ&G6:ҪR'o((HI޵L Sɛh`F=ٗN}7 ZZϸWD!Sw=RSm85l I[d +f]eMԂmdh.*nu[6p+"7e&Ӆ/e]DnJXo)Sȩ  2ĆҀk$$vu ̑8>_]u0Qj`e <ՙ"F hR1G,~ {㦿8\θԅq2 *fcFlH#8IqRDrMLa?٘YЎe3~6-mbAs %%h೴?ܑkF}h0^iw *~)SJms[F,TQm#Õ \/xoނҗF,`+l8@!̡*byGq*9j)1A l3(1 > 0 \F1A-dđ0DA5,(|'1yA01 . Ơ ֠    1-@d5x*H8 7LI ØD偒0@<&ID4]}C0t/Hy(_7@\2 9lE 7PE ڻ䃨MߜЊ b7@7p4 J5t\4lݴX9EA 4 1 &ZB.$C6C>$DB :!D=C>TC@26ÖԱ$B,C*141 O)C,(C59<lB4e5Q *2 4 dVfb~ _%``,4CE^"m/(i919hehh&ii&55~JFɊi_6C,*$x@ݔ;PA@|@, 8+\#l,)tBh/씆D=1D @ & T@3@X%B(P2 (@($ <BD>(FN(48 V$F<3 7/0My9x\%3@C+Ht @C'u@!*gjD孝9B7#07@8@$B+$21A0A,l TAT0/ 1C#@ DA,@&cVB*\@T8AT@-0Ѐ, *@C(*BeN8)HC0xC'@& xV(ƪ ֪e(` ab"9p,dDfQe*9Ch6tiV^k6f;Okkfmb2X@ *Mq \|@)@7< s$)%3$+D= 1'40A,7t9TB7 (A) ") D0H H!@)C,- V $m#/P$3'<@!\(DC:P9c)0A;xBBЀ*tBt0/B!C#dfDf4̤%.,A)#C t&7T@ B*@ ,̀Op-h: hB'( #XWi L3d j1)3p@70c!0Ch/$lB'LA7\ 4Ò*2`-VB`0dno0A( 0!f0QBUN8(q&7)a/gjշ ?檩l ^C: NA6T26@ @,<#x<@ <@(ɽA;TC3\ 2tH_x((Z!8/@|,lB5.T& @"t7 (i#L@$<~1qD/:!E$AtYb*( 3H% B"<@[ T|de2<3T((&1(Bp|).xCl")f &A3H3X@+30A;0+sޖB7ph p1BB4^9&4:11d/03,%Pq_q5$ D@gm0|* B5ܔD٪:@D$@!PA7CA;'G hA<1*+3tX,C{r/TG7 P(fAh%Xy{@ C/Z/A$0 ,B,B,4.s!10ةުf:w<7(4*w~o<aij?1-52HM=CB)H'A9py'@,L?+VI{ $;%2#x0*3!0C\9&p +$A<336TUW)uϤl<$CO2zb8@T$h. n:Dæ<' )+T[%x:Knj@l"F a+a 6fM7qԹgO?eQGY˙EjUЅ7:[ ZjKl[F7W6dwv;pGW\aÇ'V⹏!G-'k쨩P@(ĉLpBJ3$axAV苠 D%X RZ9 Uz1Lb1'J饚K"mlp,:h哂2]v f‚&(("0- bRƖjHd0jQemYVii%*beJ 4BK>{+Ȑ0d~'w&0~kL}M X4,kNKm`{mdэ7ӼqU4㪁GUud^9|Eh'ggf<.;F)^_dنehQevؑ?fj9Z~!^b!b`vd.Es!f.z[K:Xx \pvkhdBƛhplel1Yo9qh&h)v)%aڑŖRHiXdaa\iXbigRb^n/ed1zoڡbyYaFx`FX:?lR^z_pq^zHDE0@.ErrZK!㭭|+_ Β- C /I&< 5 qXt` g ӠF5~0&71M8k 5g8F6Ū x4q:.!Q.dG\/\ܑ_1HB]e-d9%F|t#!IIN%1IMn#yAҨQIiQ7"i (f4FRvKa/n\b#]g>#.]E2jA^(r|Ŵ,LՂ$-lNynS@bsF"I5@mZ- y4.e B`ei !C|吤%5IBZa f#AXƜԘ*AӕѰ;Kэ9EFw,HDgVe5&F :R܍Ua|*?YWrT.LsEZn1LT---Y> ml. )qB- 93gWqL6U#.PEԂYa[ْ5kZʥq 5B?4R.%!RaED7UL7T.at+U07]{_Η}߄cr=b]"wa2[/8$Q1-%-aAZbgg_%/["؇Zeo\`D(\ǘ#h_a3)j1~MpzI5 򕱜e-o]f1e6_T:Fn38*_Y,p|e ßj Fą6mZ7*-~,\T%E8q8u`vl@x6a `*^@ `tN b|hA"@J!   `"S)!:!ؼ?? t3CJ*)NuZZZ5[55D;PDEOCG``V`B`Rh`6:@:z(&f f!`PB,>$ Z!.@NQ6! ʀ|)46t2BjYѡ*2guvgyg}g6hvhMC ˑ-Maީ. t!a `_.`t` H h`lZ!aP l@ ^f `aDBQ"W]N6Rr@yea6$C3!Pc6s5ws9s=sA7twh48D<@ja ! `J V:L $5m_nyP P@ N` H,!q(7qL_62+*LNtw~~~7itCtÕikOuug! mQmM:!@4A!3aϢB zo!awA re (II|wq` 2!!B208x2Ow #83 <@ 8  "* HA#ޠ8! ,@ J;&c n_! ) T8Q@`! U`ajKPÐe22Ё " oc8UyYҔi7W\\:1Xb`h@V {g@ T!$A|Hf#F6~˸QFh3HU .`OD(QERJSTRQՃ"Hb&V>ȓO1~34lMb]!M Űdc@>6e&mUg|V]SiZlvsMGfffjVumw7C D{WA -d>y( 0%΄GmDR'^i&; L>JY(jT6GOS-V}. sLT*p lJpl,MSTLr #Hꓹ2h0tӘ꤇,vΐړ!iIIaڱCG1k)ht,&(xDlt: ߓ=g?KYbȏkZꊸl¤[8DHƄGG䁔hG5-Mc>F bI,`4vU(Տ\dcҵ?2L͠aW8(IKZx!';YP-h1 WZ%K t$ ci$#OAG!l?U^#--^PU9)ڇckAF;n&55utELge@'{dcD|]zb [2+x1^w*R::os=K,W_'Y'^l>, 9nǓuC?m"PB40 :'Nɩe'XhQ BXhv"F3r5;%Mu#2z2X$g :яwPmW "%Od3bВ\滹Ixժ)wLe.jv{e ߼k}o`1!D@5!9wk2! %/le p3F[!9L`ǎdЀV՞R[}~ZNh@-CZ͏1|.qkiaŨ5Ѡ.g"=dwwMY L C(= l9 2q_gNw fw[# 3d8&{, 059r-H@tZ۰q;cQ&w5Si9с*.~6,7 d3cDC2+S:5`,-yw @ S q@  ;{0 C``Zs='lĖC'CW|^ht|`  (Ls L  FF|~d&~`_ffx~ՀnҀ[ 0 9hׂ,9(=Q  j@c:a"?[2Q &JH::! FJ=" *s!hK=&!'2"#B~xt]A7|BllFH^6W c`(s&@ `c(vex붆onE ~p|@Y|w-48.:>@JaX1!pk h 2# i:d(C(>aIJb$}XE@86KfOEGG||x -6 pzN PذaabvBnuf5PIDPByf5'kWh 6`p&&%f;y+NrԐiQ-F0)qj,hQ5 2H+(#'d<,Y$ )HsJy0y5# 0njA%XH7}n Ֆ3[8 (eC &cWfhPg` j? 3p[TYPUpY2a\ @1>GH>Zb@ciI pEOI+a8J'a902>g@a :I3( K5 ]Pr椌y(Rf^p `[ т t"ouhz)@ @ Z @ vmX3P }pP0+!\(/h-Bxa5Z:2G5*7h9$G }-[ G= so W2Ӡa:cJ|xYk '`& @ A N pkG,g2f& @Hzn )gnn Olা:bHҪ_2 #P%-AUvʳ =rD>%"r4rqcJYq`* &1Zg3w1ePq & @ w 0} éP+wkd'4 vi'`~Q @DYE+RK{+S)SY ! 0x <ƒ Љ5!Ay,.=9$ZrQ! X5 yL :*B\ aF&=Z۝DǵI_&$-  } OI ʟ v_@p+o " 6  "7`Ȼ)++Ġ3a_@ JؠAneR˳;aGq2:2P к9#;)6!*6.ka\N!-ZN?j1 ʐ"[G̼0 qv`x.NnN7.0GBpf{P-U'$JJ4q ).ݮ]3BV N.d26B6x=(=PT>>J;h!#O2a-@" :+2騪-JV❮6 /#O%id旕TP#lXj1BMG(pa`)&^ aSIKMO/QOO/W_X?߳,<2C>+*^hq0@ZeAE"@ E$d&nӞ"oeuC0PnW7w0JL [ +p.ZoY/+Y/ǚ2oObnWww$nKKk_H"0Bx-ߔNӰ-ۑO& P_?/5CH)S`D,5l#^c/t4nO2FM4*^8~UԈΟt.Y\)/>}9 OA%ZhQmGx1SQN:ugΜ!C^>xGYiծe[OiS9"t!ÎN.]O\aU cȑǯb>ڽ{ZFGr2G9/<բt#2 2 ;+c0A4)hw;bpK6jxD$&QƸюv YL{֥y<%J}`FasU$mpG&0oH/;ӿG%0NSw XbFsюxdGS `,p$=(IV3|p<,`RRR% [  vP`KX#,/;U 4-sgSTgN)IKF;1b`E#|ZZrNPe8ҌxZC>XG;ϯ|3E\BgH߹,`\'8"-x `E78MHnsΞ#9[B;$sa]+^ 6p OKSBWP# :!Os~fŀ+ Z+lI/W][_K3.mx]釅e`!YWZYUZ}GA^<iF1ȠhQF%)["9Qp}&as.q %nc3@E- ZB.C w .rc5g;)b`xmHc [-0d9V-ǀ20.!AS0@iF+]lC3 _aMA|gX^p(XH0F5@_v03xnB6[@1Xx7/ xRXH@,MhH`6ȂW>J(;\x>$?,@Tv̠) PcĂJH-J(?:PԈP&TJ KpV,HXz],{ԁr}k&KKK4o%i@HƭƥRLop\;r,AAG|Y@KPM0HdDdd`'0$8XxrMpC wD2`؅jd`>82XX*`(J3d|Xxɞ)0E%kč ~H P-8P@0hh;d8 p{&0PhjP֨8 I 4@gዘ+pQQ"lxȉmn KkA$MR%MRq #H?8G`ThpJBpׄM4Hx.X n88(f8WX^(dh7H`M7e0,H|MxЂWPNQIk>LOLYDaOQ(Of ~o89Sɧ?pq@x~@e*Rph:; MVTP3LKQiM mhp&ry  WqרѨ` ML!]L$ GUC ֖XRz&%AJlZGf WH'0W(ӈ,dp'-QN\VMNLXNDx)R@GxA<[`WXWWB_HTTy#U(PS0STʙ9x96(;6cx_`N0d #̶0g$Jg[i& CNᔝ y 0Lpp 2r)/: cW2u QyzM]Eq|r̰š2R̻7D`Np%}2-$pvh$`R7Hp?X@3(^HPM(U`;<`& e 01eYDO])!؄3*(S:UKYS |@ HLD5783X؇P003~ # W:#vL`=r<\)\8|H\(iZy ΀ScuX< _Ʉufp P؈:AWP]5֣WsWW* )\H1Y^Ԝ[I[xr1B2[]EjmɁ6MZ[a@2SP7OY*=؃GqG\l`f!"`[KZ]ވװ(i9KԄ-BfWІjUdi ;F0k.k1@>knk~kk悴kk>.klNk&lNl>lnln봮lflFkͶlFk>mNm^mnmFaɞm&>mm^݆mln6ahʌhh2 iI?i2NdiI! $pKḛ0E8w` ˄Cp^ZҀy]c} ?pGNpoz͗Mtp op gp$p O pc /q?qOq_(pgЅiЈbaq/(v(mYC%nTd:$ĝ^%VVqO(⒡TonB7P<8Uf7Pdʆ0Pub%=q{m7Gs0h^~W8].B$%wV! G%Y%iNU|ep9Ђ0"&y\,teCu]?paW~cS>%y$tB+(|eR_,E(a.@u8^_`owywyW] zdE4herulvgWh)1!ݸD!!QU5 `HMt}wъ2BӀ#'?"O'vD҃_gݒv^xt"XWx bo; 8m)t8gpy|G/v8~Wt6P*)/|?|÷i>(|ȏ|^?OTilP"JaE7 H'ih"XSx38eAi;O}>JZ3?PmiPS>Gp0_o[@7b L0.~.(v60@O?x?x:pa V{&P`iJ0a-.\nUW[ /EYx )r$ɐN&+7lFåK <~@!Ǐ!D/Ѯؾ1زfUSe2[b',ݺvŇTA+x0xo 'ƎEgѰjhys]2~8>%M-0cxkپkc#+]/6C_}hxeZx !sG22(tD<Èyt /U2xؑ 2ؠt@!!brءZ8H&X@ ETWH)SPЃRAY]UW`Ev:hu`3xt}"H6‡2- 46JVoN5Kљ7͗bMoYxr25t J]xF?A6Q ?͉{7@7P~DQT"FK%J˒RJ%)tR[brHN'{QER?SQC;urh%N1J)#d4| j ,x0ď{xŚM:A/Hf2JP2mf 2v#!1Lq%Oх<^#f Ȉ"D*`Zη~=e ѨWwW= ۷B箨.S@TP[FoghË5)PJ!N2w7aL̘ʁY0wȲeR^:bF2^ʄ>se޻? =1Xl>_QM^;/TZH%rG!fez}i+F'X@A-{x.]-2x%^kdx@ .@xJ"O5 ),[z7w 랇Ttjc!אbHYagC(ؤtІ%n:ӁbQEh,˛A74 ?[[ AYH<8Dx`" ]0E/ ދ>B $p]Blbb1CeoPC(PЇ3D07Y pÑ S8R(:OX\4Nto1o{ GDCÐ\F%p`!,>^-H3xvxJd,46/'4l 3XAR?Y 8=q35ޢp bO1/| SPZc`OІ_X^xCQ ȗWDThQ4K8S+F1 # gԎcAuGmNA5!Wxi[6R xhYQryTp*'gO.+'>!ZåHA4(Q%1Ǝ,.c 1`W fH7hnR!tSn\5#q\bqs2T(D\6[4 gk,Aс,Yģ1i)?!xC^-Fך($&;܊R9a=8$tX Gui嫽cf $Ka!SYkѥ5ƫH #ѱpdАt8͜Tr0a H&TJ 3(D@X)Xaql뒲$TDAܚGvIG<xB J1;'WM2T-5eO|QZeٮ{cm j?'EɲNpF{Q +lUhؠ~&)~A,Cśp>x\kݺqjx"uӮ&8ڹX_׾ŷ,0e'J5is/v¹Zڍa%2 F_NsG> T#Bڡ0tB PE[:p5}둓[,;8aI().5]=ssK#;sU[C<Ij֘^&$ZYfFcN8kx%Q(dx,lP\@ Z=X^E 5G`A`Hs>Ҡh#: %V d-sO7|&S>C\+ ;P0@(7B+,.Ԃ7@BB_e-ǹ@Vх; z@ `BFZaFh(`2LBM`q9d#X@!x>4T7A+6HCC TAucx_ Y9;0CĀ& C(&(^)E"]bc!AZކἡ4d` Zh/C8A ,Bf4!#KĚC40  78C\Xnb75= R%x8Y8 > hVj>2dC,GTՙIBP^&C'X@<\ D0,"XB2x8%ؠCvF6jpW 8R9c:XpB&^7TR#c:cfE@A~Q<X(BVYo@$oO*^dbYCK*TC$PB0,4pe0B,X@Z.NdWd8T 9c6d8fhY78MA _Zfl6kP<xUiyl8Ă6ء8(&gd$e:Q\fPBcQzfhfR>U5\3(0)g&m{& &=zhC2g[>'CDM<>CjI/Յ7$Il6g7''aUtg'8fYHh`C?pӇB=N:/BO5$fk}7'25?dCuh)*D.\F>MʨXxe4dO~#$VE}@~鑦Y^b*>N)>dB801Ŝ~82C /  f*М:g*ԝDŨ>?hꃂ(#&e.lg}VJ~R`*rk"&;)t@)j5`6L>TAVMu]FC1b2pc,*/k k(-k6>+|Fk(U`lk,7A hBa+؀8 (?8A ,TA)$A܁L,8AD H4 0i/\B,BM5I:`Ci؆ h~r5 mmέY!H&O7:*gF*"Pj%E7l"D+$ H. C+8@*@lC&P.l;l'$ B+!8 X-ڞE,d$hƒV7.n}Z'C"\/I?C#d.%,'5P<ĝ9Cav ,&+@L;D6 ZoU)D"DPp p _$lNfeblf&V~oh/ZEn%X+,l7P BuV@$90*ԁ"tUn#ÛI%rODD.CC00qr ,Djo|j~ǂ;.CZ%dn+(\C<AB&@o$t9* B \CC8R${D%B`/`/ #s2+2!Gjf0#;r5MRvBP*؀(2L/4@B5h2 C,DKTSu%JkjNF753uXG`EQƒ|lW8(.aovp+6CO@q#wcW4fN+Fcg2r#owuGbI/ ܌u{7ae-j0tq'y2/|ju#O#w}}/]bsTw ߬74s,|G7pfw;Ix[?z_{@757XCx -Pcxxdn{?w&x3Yy x [/x;;/  xx879y_L1O锺8rړ`X3`LȎűJycy'o9{9uy+z9pV9z9A]Y7Yxbw7npckys:}/zjb]$S6$E`5ǘeS>F< +v"Ry]U9 yn`ȅZQg"MzK6{ӺWy{;ƮEהg`:F,{Ge(-1lWWC#OxùCxqZdCt'#| ƃ]$q6Y#p\Ri5CdqѦ;`\ik$_XER ƣPYzi;7;ýƻEz_|?`}ՠPы`8å$?L݀Cm5&J8`@O9S܋{A:?u˿}>E=sTC?$V !IZC>*2p`>g96g7 gjF2!?2x~;C[D 7[ic-.\nU׭[ YtfL3iִygN3턻 (tG!(&҈D"$Tډ|%|jHJ*z***U\]|eBsXBNNHe'bkp1C_f`5&(Ŝ PdG &Y:G}MxGmq)^؄SsAUB$`&8~X BhlgB 7~Y-L&b0,^Pǫ 1 ǚ]:RZi`by雠"(D ,ݖnpW-&G~wy'} QcUsBi qG +~Q3D<%Gf1B_T) UAjHPGjN($<U—lqśKq^yx@9dy5VfshבzIZ7~垛٦<_楥`>őE ?_|qODA;`p҉G#p@*g%qk"]eݯ m! >$#ml> wX@ -|ɣghX PEmB_FI,v: %O*rؤmB`_a$z$XSP7#&8EJ4h(`5A_05!~i(*v0L `b7aB9yCp"#2q` D'>8l7D!Ys眕*JeviTF7/@b Go &&a44(>  ƫ8v_lʠ"h;xFb'~,h9UBy+;;T ?qJ Qp>MB XtTs!(F p mxZ7H^$+DJ^ȋCL6NwB:hA/EE@G3+^.Y@b4ġ4c3F9* 3pS , R$G[YH*%, E?UkNGh mCx" @6Af)O{E О .AfA#ː5b׈!o|P!FIÈ +a'H|7\dd){x/^o8$< bDqY;a3X`A~ʯHPSF&4(Fz'aG;:p2xmo#8EJ NtLmA wx#aP52`YofDGk1;}bil!jV 5JӦ6[6ġJTբv[e+_Y<ۤpo (5ps@"|XQpI1iYٖ iTc_1E(,<6ъRѸXx +"|ՀGlp$tlr r|a *^ڡ hA|AlAc!a,uЅ\"22Aq`3!Aq3]A1W2 !rеP0 =R7w7{-*F,,VG%W%"&͢+j2,p'EB%a(2r, cg!r!2<#cr`$m3!9dlz̀N   `(VVנa<@,@1YDqD!2!>12A`A(a<L!D\ST+6%( g 7}IJ}S&u78!9_2Ks!-`KB$ô,ȴL]*+=ul%2D9P(> b`@_ T4aN JA{t ^A ,`j5CcC4N%L!|3a<BaNARA!RSCHHYk66t TXX7r%u%l94"֓MU=NETN!|d?!J! lXJA R* @:ϚA0Us!sހ^bSZVN*!j\hF$"6aDaba'n2a!ޤH{5b6XsVgwߐgs35=uMGZ>ߑa[eaOt3 ԕ؎ ꠎPH@4N@seE4b6`GpiaZ!*AB$ !Na">!e#oVoe_Sb f X/gOuSw~YMrQrZAN4V[mZJ̀ AUH8!\A!xAUwƗ| o=To37sg>P3gm7\#FAdO!(r7|ns4"r6[ **6Uu+؂/vXX]=kћDpvv?M73 8olA :`XA 4&`́ @!@^AW `+ daTk{ru|X;`[p8bG!|aqaA8vqa?!x!>!$/w|ew"faW덁!Mg1XmDʄփU=U p(8mo *(aN]a$A``aM .Ab;bxA|)ْ/olof|3궱D!\!B$AxoDGALaU$8֗9#u5AIB)*(yydW+?WZ9/d=af9bxavf40٘%x9SZ$r!Wg9w'RwF:%jAfpa|s$Zѹu⎕"tDj@&ګMIРC+aU:gv!P2f f|x|ooRr }}x#J$ ۰39j!\g$xj2;&[mfҩpos{#?z1C±!{`!b!a%Ws%; g#9kھ{-a!+YB=;[QێU[sZ6ªg[Yj{uҷ9H`N뛦kjkz!kb4[-[Ѳd%D&(e:bsC!!Gȇȋȏɓ\ɗɛɟʣ\ʧʫʯ˳\˷˧|O, Zl" ['4\Õ:X&-&\:UBk"}w"i\j|X;s\kZYha#]'+/3]7;?C]GKOSYں]!%tkazAt&xaf\څ\ׇ`A<(s*t♪!2#`R@؍ۻۿ]ǝ]ם޷@" z1 &zvד=&A"Bajم@'*>䵩aC^GKOS^W}Ձl~mmx^@o2UyA2q^꧞꫞Ѽ(=*~6BǫZ<(!9^},B2b@{d&p >&`a@&'_+/3_7UgBsB)4]`:2gic_gko>2, >9?XG5|[bH!*r%=`_>_ǟ_qހ |A"v!qO& (GIRLsGX`ZY5k̥.k&#%3x0Q2f:ЌfS\&kxlCt}itLHb1B .x#:):h" PJԢ)3Ć>O{7eJUDԦ9N.'JֲhR9#CBե:KUCr2FDy DCK`KikL[ymt~t,쪘#^z hت=Qk$QנegW.nwޖNeK)?{ml ( a Wַͮv M.V!T LyWw*V׺|K:m8iּz-9ѝȑs"g;կpJ\C75 4IbGwlVr)5 xvZɱwF+;[Q [N5Hˆ5I^R:LK cdBv.{`L2yvH6pL:xγ>O PgNFYϐ'MJ[x''d7FpG=F* n6d+L $03Ƹεw=.tl}p(b=X~f;ЎMj[ͮn{dG&0N U-ם%k2$T|@G~+X bG=sD?| ;uM) ܝn-k\v{\!n/,;&ƢX\(3\yn ;gv@Ї/5q7Uǧΐ%#CȐ-LN\擹_rӰGCpgȮO3='޸w˜>9qdpŃéK*6 6cЌ48IY#{71 g _كdwl>f=lL^;؈#ĝ3{S_lE[^?' ';9z#_D>-oѐ}6kU[`&tӐ3x3'{Gt!E%&w8;9ittu_}q@ Ȱ~Y7u)xx:xW#y$}AyyV/@$ {Ǡz`b谀{E@|p{}'|נ 0 {s| )M7[}G)'}E}0h wهw| 1Ep`U9~~Iw&q Ts g G7sHz р8/wǁn!huXGI)Xx@7 ް([q=6Dx  p p  İ}nvD@ r ʰp C p zɠP РP D 8y@7Lt p f~s $t H `ro P  =Sv" pr@16 &@ rQҰBQXE = g0 t pu@]` % ` 1ސ(Ր wIzp d ~0 O`P 0@]`zY y&1tc9) l程N7aDVaudn#I$Hgrgo&ސ@ QR L`7p[' @@1~( ` 0 @ (( @ pMՠJ$ r M$ p  Q 3 W $1 ` c 0  p  & EB:WEp vx3F T@=e I1@ #A@VA @ x j)     נD)  G YDP ĐH  ̐uZ D Ġ i&`P D:ZQ/ǀ-iEM6wpuڊ*w q j@1 J σ@ X@ p PȀ6Հk4W@1֐ Ր  i*tܠ :  pP X `t @= (MX @9 à Ù2 (9( #1|P |P IG[Fʪm6wqiIr'Xyi``l1{#@J@e 0 Ip T @ ˙ n P) l 4@ j I& &NPL`&K )` . \ * 0U N L`DY0 3`& L0 9Ȃ<Ȅ\Ȇ͐ ffm[sN(.' ` ( P`p 6 @1Aa: j0@ b @0) Ip'@P&kJ )0Q L ɐ &0 `UpJP`14 аQ@ `o `ڻmft[; e}g`Ɓ5ƾ$hj9 T# Đ < o |` @͊2 IPr0( i & 0ɰ L P4 `U k`Op  !`̀0p (pP N ! \n 1` Ipf 6`r | =]YK x0=I 5aqlՀ 0 Lҳ ! L@  Z p  @a˸0S  P 0 ` n y 0nn YpP}9 N  ֜0 Pݠx=p'p f ~uIڽH}X_P10]o Wt *,. 0dYrڜ5! `Ҁx'|` @ az0 < N?   M @  AP a @2 {l)x@ |ٙٯn!P ,PAQ0  6lN  Ǡ ~Z]K vo` vgݶps 0 ̩` jp kz M`,@`ZP P ~`# 0Y@ ``ښ#>W :Z@ N. } +n/ 6в3K(p `kP]pr?t_vxzq 1we g. YkcH(9 ΐ 6 & p U6 yP `𔚔ٰo=DK '  p nP 4 ދ A p }(: `xPL˙ u @ o 0 Չ*d׎"v,X(L| F,1=~RH%MDRJ(x XGٱo|:SJ)Y TJJaX<'oe͞Em5tt4jc*Z^RDtʋH&T7* HA̶zTqcf WT:fHXɂC$Ī]@ՃGИIU 6݂,mU_ 0\Uba6W U[ݽ^x͟G{fWc2r ?;r!@kႋ[|yB'B /0C 7C?B;` p,d9# ^F[!IB UIs,PJ_-΁BGpDjvʆp/-9a`@h AVBCv"&f&0%"醏4"Xb13(faD7pB#E;Np3pJ1Y</ qFaݰ HqdrrC  D*`! ZB&1e.uK^җf09Lb@&2q[-Lw1 mA Px8nvӛ' g (Bъ*w9O5wP y 4@ѨL@ E^XED`LKt{Ò dAp$,@ +D/7 YYC "(v3pHE@0CnP6a5X(D.La14aMVխok\9ņ>F8DaR.up= tp-P͒t[LpY 0j[&ʻ2Kù,"\Bl1!_dQZlaڭڮگfDPaUNY!pHٸ`QZZ^|{PčC-̾}Յy[mcF-}[[W[g[uFGt[7uׅؕ٥ڵ\pCkX0'5:SBņopZ6p8OZD@o[ht@TNRfR~hXneqEemP\]\^`a&b6cFdVeff^d4HCSmHk.[rg@P>@EH}gD&hMiM6mN悶P >~6iVfio^dfcjnhk&Bơ*\މm&Vi6:礆ꨖjy>KhV|:'Ȋf6FVkvnԵ븆k4ji=貖&6fjnkfhV&5EX>~޳DpFXD86FVf6Dؖ٦ڶ&6nvm>fv^E`GDo@ɦcl=> p@phfY'7ocFgw pNekX7nff>oNcCpgwopph>Viq.G^)*+,-e >#5#$'%7&(sU:;r=>0qVsk67wrHIJKL=sC_D^o7G'f[NtHbteZ[\]oNj k3/7oRi6SU VV~XepTel,GVmpt_?tP/"?vdheXfYez燺Mjey:tqr:7*'w0OtGs#gkvwFxfyzgYWewTqOqGgߏ v2GJblތv7uVF_8Ox| d@w0 _otumHZ&xgoXxao7OefyLJ( yHeYC@xXo0pO0yWzo4'TO{wzz,'Y`5qPeX>mo1[rƷ}GPoޯzwsoG:ggt[`~(MU^`NЃ_{?xtQ0w~`~!?Dɯl7E~xtU*Tj,@U7yS0G,i$ʔ*Wl%̘2gҬi&Κs'P+jQE2m*EߦR*VSY g'ذ`kqrW[bjv-ܸrҭk.޼z+Npߜ!k-[3dG1dȎW\u5G L'q ѴI06ܺwϧQ3ٷVjEk\f}Ǔ/o<` #Vqȓ+KhHyO  MjUJ#>FsZx!/!y!q8"%x")nhP<8#5x#9긣 ڈ8x#?GF>EEXA@;h:\ve;\jNZkS `t!'uv5yԌc%qF? ,DB)cJE/'M6Έ?2.ZgOC 8CT6TIw4TŕWriw wq}y{,ҕ`b'Y&}?c$@?aP!\!!{$. <02S8AY*8K6ߵe[ҊS s :<TEE8ἥ~O#:>1z~8#;=<=$3C:.31[+2,{ G>8Б:o:ro3o6(CFf(} L$ͨT`2(M%F@) +GSlu:* a 6 D >cG y?b02\ ,]BEI"1f4x౱82lօ/{v#; `ZǙ)H2 @_;&6-C晕  c0M^ PǘʑHHH[+RfHCP*WFCI5F) /$On6;鱬e; oU/ pn܊5M|,dq"·:#&)ZA JhTJMρmd@Pu-nsoJ"k %:" EpF71 ׈gzPi_i&4ɺ)]%UEH!U ,`f9$ q0)찄:rSPӚ"d`F6aK-4[}k^]s; c#n9Yli$ފA1il3Z@ 8n::xN~29d(iL/Ges:vF3f>3-gd q9˹tL~ęJ3-A FLӔ*DQ<&udZPcڬjM*i#=uW2Hdi_ɕ\5uC~ߠp: >`GJcF6$GHLa~qm`񭬑X&6Yu<,9g[o,1 _+qSd 2JK9#Q`QC2;&f[x$g\p~S&&9c˽#D֢mG/{۽-^m_1HjKHӨIԧVCF֥S8#Lz~G;K34fD"v$(LQ:DHGto6b˽-f7w3q?Dg"DKըhBMɒ% 3P9̩J! }HCOwXc=/|#h>>Qx 2٠>">gr~sC g?_#*c9SZ &FٞMmx%j426\CJߐ lҥ쓣~D HĢԤ"m @̈+>(DXA^5m)[JLMbuE"?D ɄH _򝤙M36LL$MΤM$Hp%D &eE"ALa :5Let (5tduE 2<ͩECY2LZi"*b6]|A̸̤9.fIbtU !xe&^?YK@_@X@0H)XOX7PdEMFFq$\1aRٜtpWi 4@Hr.FP4dQ}lQ ;IH%ˌ q:CC1eȌDX=,Y$uť|IݜX'%Ax9pм ɔ^I4?g` b&b.fccJfce^&Xf:edfy@LΨC'@,8dI<稩 mhMtT]hXLKjd}eR t<< X!HĨ@**z*ެª4H<j Gt,M6hOf lg֔H6>+5;DTyzkok$@:>A EI(F-.Mh}m(FZyN0Z+ţD r,ZHg6F侔nMl:jcb=솦EjJ>l< (2IllJU";DC1Dj"pH*1->/FMV,phmp-cBIR/I7@Cάyc r5i"k܉xFٌɐ֜t."0袔.j,nS]JV.Rn0hk}nZh=ep`bd1԰ p  `Cgn 0T>(fkI4.̯%>n^Q݂+ڌ<>bmȑbɳT`VnO`EH&RJL`E5rڇkTC-* (:S%^aC87LDZG~.WD-6+F/hIr0up017pH2,  1șB*$A tŭ@ITQKgDZp8Jp)CʃtOðn0Hgq|/qT58qɐmA;Ûس`$u-vmQ}W3CK2!mFIL4.tAE=rV 3p1?u! K  @VS_3Do; ZZw D4PNtjQUa4x~lQ$aRZ\v>_dk+3Jh.t4q`mHkaĴƼ8_0xK804C7nId5nIO4h I6`7LmH?yΎXlvgH] jr.xDxRW=s65A{106 43;s #@A )8@ÉUD[kr,mu^{^?>DtW^]4ULCZka6)g؈Fn2 V1Q3 DI;xDC): 1I̯5+;\ ś3 9ßs zfm`X; `rf|= |b[+25{,/^zJw62U?@&D:)G6)4 3@A)BGЏk'|B(ϽA''(x:|'''|B3>};~({BdB&xB'1<TB(~`(Ԝ>B3&4>Ϟ{B>~&{>GH>o'~o~>(D>((' Ji??(X:dgB*!1 5ǃϿ=@X`p@I`A2X`…?Q]V~ʷ%>,l<ٽ&.4Iڢ%$M[I BOڰ9dvP}~:=-P֟~-;ˇ6׆\!neJ)ܴ)SOEn ˷O8͚vPܽ&̙AGL[""?2I GZ 1P8pEE .nGEYQiqQy%o&f!&#~AB R0CGAg$q,S}1$flxIG0äo î"A O&1OAP %?]I(F!mLv 0‡{e(k/Rơx) i(C,wtPxޙ wK`|g['c֓w X[ibA6k{tҟ ,cO3ﻃl".:>jI> ~Ihlm F g/~)J@i}$1v8mv8,fԡH#4 G>P ddIOh œK*'&,懴VDG$Zixֽ0Ʉ~ jaN -RCg4Ee|^ ,Aƈ)a8!GhBK.L1)Lbl`QDqm"NF" I@C8C\ЂO$m$  0PJ\#p97Mߍxk,b7~Or6lS{q@aKX.!L=fW Nt]2B^:Bұ(IfPc}e3kYe`6 _B(_dNMVdj q e(ëm0+VEjhȥd'Fa'nV> ϰH+h@$/5poG>HhE*QPͷk@J \J aXhc0NBD `捦!܏(@.O@g8p OONhŮS ,aA4 8 BaS!jlB2i'2)VD) MQѦoyiޢ D-iǩʜH~L` H uz ~8N ; L0LbEX8 Ҍ.1!At.vԐ)1pΤ!QT  !~!ap׶62 d A$IdAAg%NA%tL6Krr$pr*ڱIڈ(2)r))q WckzbE$r+y3~!   Kj\ +}: `(5  P1pa` o.D^n$pe!(]OM)# ڄh !0.  1NX*^$!0PBY|΄!rM$ Kĸf&!Fr&τ'*4JzR~2(r(r< )<3=} 370*2_*Kz>FpIA葔AK-묊 2&aA(3OtO=a!C*gL>5Qs!?  R Nf\BۀIB!ҁ0+!VcU^)D-.hFoE} Nm$dFk (GEOY9 eRsS@.Ӛt[[5gs KcH!vBt2RS1IHN*0!0B` `#OUP_+> 5UQ%v+s?Rn &E, 41V ԘL5ЈVڀ&)0X(60Ϟ4ivna!j6j6j3,mEkbAl/ V-Vr1􁼺tPvH欂 K0Fdbw6q!7rLaCPsb97uxI`_ldIVLVI1 @\T#TB&TfVkaxvDqhh5PƔvzՈim*J.ЌHѪ{bEЁ. !%W fTƠL(֋f(V.qeΐ.Ar(a`q>ũs1+vI jtQWwd!.B!\RFwQUrѩ<4tvWWzոLjg8 ֋_h鬘!L18 |J1R8Rnx<dю! ʁe( QdH L,( 7a(`%y).ؼ 3ȟ,_A jj7-$1%>xti8i)kZ X+.gSZ:լ[~ ;٨ 6g);2$P"U!D\tގ">L-?|Rv$ aOw-҂w>(Ek602`3:N15WZOA!HacO!C"=b(â,?k-WnJ(wBIdFBWb-dQfiuiF[^~ fbinI?Br0ޓUuOO$8yģ0)1{ C y5>d䣏@ic;Zs/t8<:G l'dH(ydi_jyo~d3F~e \ig{첷68s/G DpVR2<m5;rBH#wafaAJ62/픧 U -9N3oT޶? Pc` 8 ;r3ѶpH+ `SMO(1pzrD8Cԕp~+s 3Ӓf(JaP(B C Z-1on0P` $X<>YC,đN|P@N !NDq %-oD pH`=InExJ/!AL08J@BdIR6d(|  9&3&ˡ2"NG%b aQ phF6rg2 _jGA-X8˻F~/P ~o~! b jtͨ?`хtHMR  Q?R Ә40iMOӛ@L ԠDQsQEES UJ(jPTW"TuEJZuTZʿ~(L+\7 ꕮE{ut*IM=Ys[$Ⱥn>Ql8ə_H7$^GЩ-.">!RC1 |pʗA bE;"wm.s+JwԭuA ͮw v7-oxw.E+7f}w/v- * ^0}]>׹+ 7;\ᆡ=,CX#"QݦIcxvF6 AX;M 'qw@;8R-^w(hB00YYP\Ό4,c^߬6C[bgMlg @3ȐFu(GNnC"ȷ`DS7O~H(Hyլsg:^fTqcSq33=ߘ7&@'8D6m _y8L*"1=Q#lC:}7-":4 }Y_QPm ?Βo}!֑(y{GPNpoq< \$Yuh&6X}!4{H0 б~u)b-C,87CG "ͭj'ij#q (ёqq%"Nҡm'+<칾̘GW Olh#(Ol x ~i0OPՠ/\5=A0!7͠/s?jc v}'$'~2ryG y9'8a( тBP42 `.kHג *Ƞ+(44.! C!~p  ! 7 S7zs ow$lB!T ,3"K}Ryfx@'3l5b7h@?m; Ƅ{1 {7`= |{ MI`>P -xW @ @ d 뒰Qx0ezз j  @=Ƨ} + r0 nrY38xysϞD*Ջ /jqV 3zL8Ȑ MΟǁ nҟX%_:~r^l۶lS W)pu;p7 V0`tqZ\u-̏5ogСE&]iԞҋ 3Ȕ1=t ؂;PC$sJҺvz.Ea,(w$ҞckHhĄ̃)xЋ!KoiɈ.SP) !JpB Z*n0&:l.,5[tEcZ{-j-z-CǸ ,X_8 R; ɪʥ3˔Ҥk="ȟ|J4*3SL>CY!8$ˇ8Ё“C<S1N20tFPCuTR;`sF6l7zU߀so$s op`%)bKD(6Y PZ 3z| wMI&g^'kr#SиmA-j_,%R,-1S6URVxa?;5U[1YHt=.9zxq IFd{Wi]Yg!mvBoeq˕}fR]ߡo7J*T^W0E$4M{lA}Usti ~<&ŝs>6ojJnv~%P(|r+OF`5aO}tI;[UVu|GY}Wǝn.(h.2rOH2<ęU! qq|xj\ќs K|OXu[w\(9|l >? Q'2A E DɺġiH-xaDѥ"Ѕ|#$̗6bØ`4J20A r# 8! ,PO=IDNҠ`xE,RK :8y/+a(nm%'HG<ࡐ|hCPE@RLtqTC6rqP$#Id=fh0ѡ-"qZ1(ڎWmtn%Y6%&}&z,|@HBJ ,XSOUO X wb`[?)2.VX s` 8h &$0A b`,k5 oPAI17\@we+8WV,acX"|"dlw`" ,,QiV}d=;`);)|q T!a8+^t98G<#~$_|F f:@@$o W\wWP90Xd "_@ }@$P@ ` ՀGh5ă&p3z0˅dhEUuٌjl==tjl9ǐ@*~*>s|Rhi7M#`2Gd`LU8! s"x&L A"4CXE"OzN4='FBZ;D3| JW^YFThMs4(f.4P;` p  0P bRP*]hj@,K+l`BhS# LH+ *i8 l’xiWqH8.<~Xa\hb~w88g*\QLg(`vcO(-& PFD2|+~\р*>8*1;V4.]m!ڶdx!u'w Wo ̀ yc$_Gz} N|hhk Z%w/Lf~L~ה1^2]EA@v3Bx0ҫk:53x7(t@H{%s8@ޫx (x58Y>q{R(m҄t8+v;k4 ?'J<(43B.8Uv @<)hꂇapB<$VZꮖڹ[fCR8+hx o:)t:|:x\2&_`kʈmXXGcBYd$|b4G{B]fbBBˆYLFj"ohPh m$}k*th4p@}@y#0lXFv)lpm l#x}q0 dTF\94KS]YR8;ƈLI t0}-x \qȆua}4!JxH\JESB\lB8 IKI ((dJAƜ| ~ɒ˱K>H&HKb\8K®$4<"KX'b lŜ~ˊE#р(,,\ILOLjBܰ!0OPMsOH\PF 4YܴDL$P P뻩PX2ȇoy@ (|#x%Ob8^F8!, =R$ЇubE%sbm%RрͅTNII*PЇ3$S9 1A)nR0@,ԅ80= 1mOK<ӽPRN<9TPu&SphEx~@hBuEGE-PQ2QJM ]L M,TP}VhfPTP[%]}eh̄#l`;+})qxiHTkHc0Uhy[bc F6ObOchmX "HL@ለNJ}yr!"aHnMՇ4t8͡ cBF`T2IXRe-DɇYĸ |k#lxk|hM~ll@QYE`Gqw$BkǦe`ژp@HJlZ 8=S!NfӠ-\S@W'`g0 8"i``k4 P0`V0(B  d$jBE\U w]px@i2~e2X pxX&t&X^۹H[ؿ\HV7J=\_FI$/  c>_X*0wGEj`X pR)0l (sXPXZ'_@cփw2(h:b@oЂ`@*p,`lȆM}N`Q֛} 5_5^c߭uGP$k"Y}$dhbjz*؄xiNXvHR8sxF8,0ÁXjp|00M~H^I^-XI8gnPb_!u ;(ewȃ .[(5PuUPւ*M 0jY_i[fk_+慵n^jx 3llơ_( X>K`*h*{R ಙH|xL><(7pncHR~*E‡`iiiH}w0XP*8kl=Pv8XP0s5XXXiH38 f.cƒ\kjmp Uт$aPl@0}؛15XbZjNx> nP; H tv:PakqKժp[jHp8dNЄ,h㖍_`_pTlieU N~qTp0o0h-v%~X؇h jo(kmxZ|xSv8=Pozl| lm|؈֮dȆhmX %xh`opSpVhrhRV28_we_s;J04cLtO_xQv9ԚMȂ=w*PhԮ' W@0Ux / wЂN]Xp@)x6u8Hag >S0HK‡ `XS.o` k x.mhvTڞNex',ɪQl@q/E BwH*Z}É `FyǙ\与S7Y&IUo|}9jƌx6~l{֑Y̬m{ưW! wVïhE|P|yR K*p89Ȉp)*=]oPɈwMem>}Æ X!:kʨ1c-.\nɍ&OLr%˖._Œ)sL^* 2g>);2ǐ@z RǏ0BuVbͪu+׮^ +v,ٲfϢMv-۶n+w.ݺ\E 8mۗ/_x鴽{.^Xӆqt/O2dDB%NVbʎC,Is6ڶoΝfJ:ytT(QH{(iRR.}:֯cϮ};y`q}ӧ/6q_̯ڰF(toQSdF(Htn 2ؠnJOA QI-\<ם#X'+bUXU<#8ɓu}A?c(_UXiI kzd`l BXWbi'Q܅ix\rL9a,k٦o"hA^t?_?۟??/ H0\:0+H Z0c;0"! Kh0)t!5)qnW.ս{^֡ QhE/J\"OoDD/ ǰ^j/B'ِ6)L ~(r#h;1z#?2cN]i\$#GB2$%+iKb2$';O&hF35drO3VpWR b$a"m_3&1iL`O%3gB3Ҝ&5oT07Jrq",)Y~ceF4q< /ac3'??2(Ai̓"4 ](CiL,e3oS##9;Z%tڈ6 xӍp(C җ42)Mkjӛ4:)O{ӟz'Qjԣ"5(%qTFǩy4)-A#!bD:4j]+[ۺ?5r+]o N88qьrĪ5ܞV [1\'X:Vz()+kb6,g;ς6-iKkӢ6]-k[6-mkkvӰH< P(8-^հʽ :}RFm8nNz;T*w7/ykޢJ 4"ʐaMuR[.}X8WӸ)N]`8 ^0C80+l c80;8"1Kl8*^1'l&H͠]|GC7.A5Qx KP2:c_ / cXl"l+c9Z2P`XQ ^D7j^3 CNiS,|];;qp3E͊^4ݞDU$m8];O:Ԋl)U8g7M.[t Ox [<;σ>=Koӣ>Ú#" Z` x xzC?"Cf&1 {{9(#mx6<) .??ߟ4c{UH66]=qֹ@:PR`Zb?ð4rW2.5_ ` BE8X8ƑrE2 eH:BaJ!EiV ! !& @MaaLvnGua!*!Q ab >4 fU~! J L%!&"v# >"ˑay&/朗U,,b--Oҡ얜`Ušb9"_!yC**:c )am"w=q3jcE&q!+#a$W[ӈ;c<>!:)}_jS3nc?}&!8 c9z9xñ]?J 5C6vW/cD0\GzGdHHdIIdJJdKKdL~$*d/U'fA"$)$)Cf!QAR*R2eS:SBeTJTReUZUbeVjVreWzWB( xi#,e[[e\\e]]e^^e__f` f\2&47dqd(4c):~$`Ž[.rfgzgfhʢ#d5#x&Ifkkfl"CTB& M&59vIi''*'2)|O64CdF G|. i8H1I<4H}C:H jbXUX3/ثO(ԖTت7TCBQ3BZFSCL i,߅ j6<Vf6^C_F4eîƒb>,t5DѲZ4 C5)1kE44ٸ~JF0E*7I3!ێ";T5"NN:7mҕ22Ģ5m< C:X1H;T֑N-Xz^5Ьl:E>V:)1-^CV;/6(C5//CW)C<-G"5xf-:VC:jo{Cmʐ)]ML .:g4L5x7*,*Fz2\댽>PBH2S4 2B7D8 {ض뺰;x)B<(04;DW/4j .*O84ǥ_(<0+r,,d#y5p/b;i2x4Z9/17LB,T]C3ΑA 4#s""C3BB,A|@9+&:GC< KC+2m8oaj9* lڶS,B5GqOPB)0+Cx73C"1tC# 9d B3B\C[#KSC+C7//,8k^)C0h#H$T%B;-Y4lCzK| 5HT]|e58@CT@(2@@,(1 "D<[DeA*$,k60(*dl4hB5B A1$+| 0C@)L=_&L@$ 8A)CpA /D4b>E@5Vٯn>xP$V TW,p  :(ke'0#a(eʂ;[=E;ڴf[lXcɖ5{mZkٶu{7sys׭vW4kN.ŋ-Ʒ(3|sf͛9wthѣGl'\oٚ!s Y4#<Զ Ǐ!CtpjQA0jҰaᒙʼ2 S.Lc"$YE8xz|fě^Ф >P%(% 7ƨf%HVJe~1xsDڤwJ(^VXY@ )fCQ<0*k(<8e_i`<^~%bJSM5ƚR/ ;,1Le\~e\L 5Xs 6hv7yH_M?P FA,_X17_~yGMkNO hb.iŁ>z%Fbs$ b>i8%2Ãs5 'TdEHن:676&-"acd?,fx')Jg*Ii&˦T襟ziƩ4|<:AofRǟhbRA_‚ Yu7ꤌ;-=j @nQ؄*! hT.%( ":Њ5| 8ܰMW(ŨP_(ZR&T#%^ѷj0C!Ѝ ZsT1 p -VFibiTF7+xXS ,+KO &3'd*S3CTen5l4fNp±v D,ld# `Pj'HZGXc~(2Qc4#@_F ebC;P0M 0AA Lb`tUtEbJ+j״|@a l<8At"I!+a'3cD_p+L3E PAb"ĐgI9!HIZRCo4Q.5Hj2l8#G0/v@D H@!~V8 JTH?1LN~#Nד9N ;pP Njъ6a;ܰ`Ua [ f 4g#&VtYPbf!.x>bLb\TH 3@ 8UMǸ-r\AbR]S@G_4`cp82@1\Y!\>ie3I$6kCwy k\C,q(@2܂;SbCx$񎛲jcSn02*XCo֜D2nNE kX\ O_s딃 |"BxD9Y#։^uPT!!,l  FTQ=.픩\9m`*e|߈3*0V%A3@dC )p@&$wT x/cտ(P  ED&LײAGͪ(:XZ;Mx, 6ax( beHP,B<+ PM4f |ɐ(_moI GcLA&>ddٲt-SH&5s,{H%,_ 7"J$,hE'1SHzFb VߐCFQOe%@tDY P6X` 8X`o(D7ldyx@A,da8@ Ò)!hb6t:QEf` 3x`hE&|1 #Gd}܁ _x>Kg^Sд/f5<RyXcB p^}@+X8@+Noh)ݵbGfHA`5+ 3PkqXd"LX^돶1 jBkhC‚NXh+h B(э$|D/4n}(1 bp" h #P'+/]yn~k//-ޖ[4̺gf J! Plr `<' K,"~(!j@hjA@, L[0 h@6 /lo D L΁ @ RN8"!L` N@Rl lN SQW[_qXd B,4!zZDa)THڡ$ H` J!Bnк: "FdBȃ ,< J8m(rX^ 7r-A a " JKx!P `؀%`%_&cahD ee^(R1g&o3hlla|9A8&8f0lSyraj rDgVdyv!laxi/$xavLCxtA/alavfaA`IxAd$S2pJjrkk7sS7w7{78S88S7ePk':!!:35!A5z)AARmfta*z!hA0YKhlAra%np!dzhlanA3#$s?{A"faF44iaXS3wa%stLT3]0g5sTYHYS8eHH_6o2Kv0͸2M((K )C)+lpRt<ޡ5Ἦ/ֲ9a|̆Má!T1sBc2$Qm!>!1gTCm!@sa>R2KQ,S sPwAaB?Cs4 jSua1QKV55H49onW{WXUXXXYqu>M6%903:]CZ_5,A9A\33lF1:UuauD}AD5 CJ4BwBq!0tBu`CiFk1q.YBi!nF0d5-zTdeSVeWe[eqUI'dN0(-KwgefKKi0R,-'M!N9AN[/ra1e?t5cuQ3^]x6Dl%SQM1{5P`a?Actx`]G?CT]1;UHe{r#3R2-ZUZu[ s̕k!Nx!]1UTFiTTsSS_!drW2TSavbwa]UlvD;F#Bl  Tb|l3sFi7nud1KvX%}w16Վ`Sj' t~{gs!hrhôhcAd&+:6jUtc8&kaklqaye!mm׶1mWn-ndwV!S LUcK5GUl]5VYqY~8}o2|sAs7O)\utMttcu\`W2Wqw12ea{uwvqsW1wa7yB'yizi6BlDP{glmvyy1|UY}ܷ'o(K13(wK7*toX58=/hcXWVSV2;5uYU0nWVJyb}!kAW[w el3J/C;C52f`3`qUeY6sJK7gU8Y?yhT=TYsS/t!|anfiyYwmTsUSZߖ9!L':4n"C2n1y3se23a92x9/CeFڪZګZǺsaja7'ْUZkFu=L*Yra ۰[۱#['+۲/3[7mGk/0\W[Yc[gk۶os[w{۷ssa$A%fzJWg皹:z))AyLI[![Ǜۼ[כ۽[盾 aۿ*d*\#\'+/3\7;<9yW3K[Cos\w{S<ȏɓ\ɗ{~M1tYK_K6Ƈ麛\ǜ\לͧɑ;ʕ۟Agܺm] ͥ4QU#]]qfa\-iƫ[m<;?C]G]ٸ%ե\f s]w{؃]؇؋؏ٓ]ٟٗٛڣ]ڧګگ۳=M[yrXS˝1c3}S˴]]ciUY=="~!*/3^7;?C^GKOS^W[_c^gkom~ģg>~50^꧞^뷞^Ǟ^מ^Ё~^\Չ^ҍ^h :#_'+/3_^:c_]\ ?=?~_c_gkos;?#^>KO1]u_۞?܃>ɝ>_ן>}+Iq ?**#8mtH͏88i?Ӯñl2@~CnXdvp. SbaNצҊ)2J*2w%,lM'57t=pGWt8K3BD8J[ j%eUgж"*?SM>Ƞ94A-'u-Ω>G/W=s4KrIN1_:뷏FmcO%\nB!~1BzRX:0#K|&5gaz?r!@yx(B02r?GC#z[=|/9(@0HDbD|拢Hm%ˌ)Pl c#-vĨEt# P|?8B.1,Q5VLLV"gs$' hDc|h&7Nz4"1L%*W >HFOk\#LJЂԠU3|@2ԯ D%J6HD3jэc$;Ac%}=T-UQ8AJ9pj ā (c+gNˆ5K7GSH_z$Ytok ;DHcZ b tPp hE+QHsrQԧ>] =ѐpD~vc y'r:c K& ,1qD_;YΩOd@~{V ?N' ncOI~'bI|$21O{|V5?ϗ>Zq OB"(<Z+%zL|??m&}cگ.BPBc- Q  0 ~^@`0m]YcQU0p!ՁX> I 0 p 0 ذXvx v٧ @nwt> Ń8 :xwOvQhH~@pGXLXgyV_ ~'nV xvwwրgy@1vA~Pzkz{8v؀ C >y#p Ec@ p wSjuLgGw牀q*7MUM]TU_]34'Uf7]wQe0}q<V@ @ tKteGu\)tt*`'z،؈vxm'u svwwyt|wwv AxFXzxy'yPu@{AzpG ܧ}4 ~i~ Iw7wGwGhߠ i tw44B 0wGq9); =iGЇ "ÁR'( !]m]PJXXXɕr7 0 7 } w$Pw |D tpmYOЏ~ t iWx~vW? m}dؑ0)XZw~*ƅ'隠bGuj*f~WZ׆}7uxOրtHׇqu~pZpt F0xp "x}wθ '+/`rP,6sss @Bn&$lE#@1Qg!/0 S :vf_cehD92šűG=г@JWEVd= Ud9j/p~&3DP=EI]:IUrҀ__enYprʀ I!J dɠI 1j.ɡ O|z u΀& Ұo * ~Jl ZǪoFգZ~Zk*qV@lL+aLq̵oi2im@ @ ᐣR=-ʢI,dd eWBsh z  a_ 3j/1h/z&:O p%a aq G&!.PRӲQR"0Qm@?mP֚JMԳ pJ5/VF{Hp'<#k.VCOllгkbj3ECYbXkej{kl+=]=q;n'uK6254pIP XSqGN:vd'r9U(s]Qs"sՠ* wo# +P VCg Ǵ p 1)$k8[Pm?hC;Ջn 姝R;[SIK*h{-R;Kc;֭7\C/6=K=[k,ֶ#p 9C DRȡ ;qGr7%>yڱUz֕0ǹ6\  zsȋe j'fQ֠q@tk_Dr{p-г .;R,>jr̽ـ7T("'lLk 'T/PsȈȻs0 Yِp ǰAC*<>Zh/+rMz9á[\!ľdP #t AE`dt171 =hjJi* M3)#¿ѾLv\OM+ã ͬ</<[p8ӡ9!˰v!"߃ :b@ P ɀh 5T| @usS8K=W(lS e}Ѥ նڬڮ XD>0֠Kt۸i.dI&&+#&B=AƼ9+;5[2a8Q5Z*Y PzCȋ4˯c ergj!:q=`kP {t P ٭(2` q`  ՄK5kP{C 5% XPO]DրmA>@^vJXSPoE9#Z 3u2Couנ }jC.} םбim :ݥC#` EC:5;Aͬ0mf@>A;iF=Pp <.ؾH.OwO<   ck?ֻ֫ ۀ뀾_p*7u92$=6<$A*se>`NZd_ټ/N֠ v!7;O7@=-@ދ P @ hdaP_䂢[ L۪ Q0` P?ӀQr/rIzڧ ҰcOc - .RʈȿP > >CS.gBd?s1_۠qmqAEC`4zD\! RͥԱ:24%D"B`0HHB@ E?G; ~wp|vpnP mR /S4owCv>ڴaW̚`0\UTUl#:eȐQ,XW͞EV5Ȑqdwݶ6]x⣜w]#l~=gX_>ƍ?vO`>y!fΛthѢխ;NֵS:ٱi6mXp>iCr |^>xդF[6q3h۷j͟uoxLpvr.|#@D0AdA0yAX&i܊h x3 R `ƙc$zB1hl@:0 D'-G gx)h)8ĉ;KF $kXjI&H(9ynN;uJjO? OEtlq JCz)oZQQG%TUUW_UuQul}:4_K/g|ʲ }l~;N,*k{q&)'9FMo{/>/%_8`@ 1ԐCbBqc(4ie|ƕB ~ɚw&e)TI' 0d)Ș'KA)-A)օ:*j-)ePh(fxȫlva1H#;pg2JrȼԴEJjfafyqƝA9+ldasf9<8.Ҡqȁ=ve7 *+;mY)wj]xF>3^O@{? <8 =\xx X!4\ y 9Yށ < ]fh9̆ap2~bDm?ңp&8DM94,OSK UȨ,;h$dld 64C"^d( r"fp'8QZW$[us^r/rpt6ըÍ`:Z׺1v]wpWC? 0fl;`C4y޼g/\O_ _&5INh|*_!!}B QP4cїH&y#4\ r ",RmIvڰcx n(/`s9 _!6ٲx /2r.zӞPC2~!B"TmeGq Q߈RuE AȹrX8!J$@ c2ё9(F\TRƥ cLL01 u)̍ѧ;p"ҠoJ js׻ )zU޾:Uv_$P(!R B++۳?Y򃖶ԇ4$%@Tphʅd?xc(0 7Mvֳ[ɏs~,xw|cglx蓟K?=#FȀ;ლ* hG2&P\`.:iK@Ҭ*ix˛+3Eҋ4%)m[||@M/p 6Y쟠oLUU-zy0*C ņP?"!Wf+B|{ff]5CLD$,5IӲ8Q*hĀCf<Hs҈F0ҔG25z[%^[ O5n:hH8u n?zT^jl*wlB[jࢆa[&N nZ0D7>Ne &6f\Xh^{ꭌxS^9F:"pP->dX.&1}?3I)Wt#$ezjʥjc6d(&iq fNRGc9[amn;ۼP_oj72ݴdl()&~pck8HyH,X7#;#;L»4A~ѻ\;iR"sw;ٶ,*PjP4hhȭkJ *=' 13si#4>;>Hw(3 ,?0`vxiBA蟳H:/{͉ " 1{Nv((* :)-bi@9a)rްٳ:@kQ 66+K:edFI0kUh6ij j Xp0BBa|Ȏ4IhxHr*D ~\txp0(,~8f' 9S092CH, (R5,i) /ɘ/P/`hk`S4+ ~`˜<5K+p3F.,7 k.?`@`@aC;elƴTKO 1 Ɔ9AXAZ}<@1ŠڛƃZ-!Xcx\p`a0:ć򎈴(\B3@ k*e`o+BiQqCASIKd11>NlbJAJ{{xPJSyJEoi$).=Y  ǰlbD˵$f|FۃG@#bpit,LAa k9bJ^C'hjȌpI!c8* q ?ѫ` #!\}@# |<ȧHr=gr {kPɸ "00`> -S'5Nv3?8J hpf.ȫO(O/2TBQ|P TOKc,=ˬ"Ի;P66 Rh"m1ЧɈ: QRwpLS=+:Ƭц_(Ôf,'#bOZO}''7 thkD ɒ,C4}zH8Ō;ČMD(6]voj>M ĨAMBՊCmD Ig I}y!KNYԷ|6AM>X(@i$,Ơ0 "XH]-pw{Pθ`U -cQLڑn}5C11sy~[ `؈`ZM 2$\/I,1 ٔ( /p =I\FXKXT5+] #h I J~VJMBZ+43lȇPxd S}ʈr L G\Z1eX3.J!xI -Aup hh  4(\Dh鄿/FX?JAȵ7Q nT\-JOJJƐ=vՕ<$%+ݞSUcZ;*Km(_O~HNHrqd5) vB3=`-ɐȒrpY9ju_ׯ 쓱xD#dɸ9D(KDG"\c4&M5M$0#< Rʰjaa0Ѕ:b#b@4x 2TcfhEdCcHFhR01GKg] p_y ;4B dK:9tVguy}PB&ð9B)F`dFf\JaKTL΅cO6iB쁋]=8xne|cDxp8d ~Ybg]4. .|\ɦΕ K9K8@|hxyn⒌!~s25}ҠR+׹Ȃp v\i-5bK=UO~6 $(R:A]UPa|S8WM5^\M=VJVr))"J_!k+ J,(&N >9ӣ^[8֑g蔈l@|v̇ N &'ȸ,W@^ Gm IkϝYaK"apQ v!0$MhAW埊%lIp0XQiv 4 xXIi@>) ؜}8# r4$ `泫Z24)giEux@4 {)J}t_HapӱSsx?B7t=CWEt@e`&6t)9p"(T=U+Fll٤/3YPc8`hGWHsZPh$Aq1xPvS P-2f4q!i'r# $k)4 &onoiKRΰYe`yv }xL1 r|:SFg~Ho+lMgMk(y {UHx6t 1͵Όu>zfnbhn0Pi_*Ɇ ~1¢qwrp+"iO BцO,U4{.y pwl^܎cȌ.[m"\&޶9HgQ I'6hx6Y(؃BylwvPdHWxR' OM gˆ% jT =gXzozz">noR@DY2mm`Ŷ):ibx걷lv`snb-8[ǎ2eK/t aӦ-|Ǐ"G,i$ʔ*Wl%̘2gҬi&Μ.DgѷB fPtZ\u ꭬Pr+ذbǒ-k,Zځ[6iȐEk ?dX)Vʣbn qBѡAy>oĠ// M莹'YM\;`9Nk!ͅ߻| \h8-Ny;vѧKg7LiNfk,/O^ѿFN\Ć ɂ _O?S̀ md蓏62DsM< z%OG!jt%x")"'I@)FQH S^IEUXiA 9$EZ^V\s]y #P A HCa- 67cIF1D1UdK"ֈbJi蓚j EHsP1dCGK9 jɩ-mqwz:[A$juAw 8뙷*GR<N4dO82R0TA:N<'Csa(p߇{9"j-zL/7E%vRMucUW=G;/ U]- \t%@@ BAl39>)LM5- B$'%O(O8#?Y,lqN5`ҞcEcl_8E#8hi-O#O6-6ym)i#L2쐁<8GX44sT4͵f"-N< ē[k.nI2hA.W:s/W6/x A {AC@AJ D0L%)<)~,Npę栉"J&2b6 馍1'y1]|_EFȆ x2PGF5!Q-6k[;AS(P1sQx8Q8q.+q:e&!S E=@th*Q\')RQHۊ(+)¸ SLB"4`@"ޡjtBpnB2 A~D"!D&dr3(K=73ţ7םMnSdHT8Qm'yLȈ{GLÐİ1>!I p0SeI%+1#&8?ùKѺx:|'3~p0}"F 0#4:ȏS f fE <:A_Ou%f3̮TfKID=Jg S8Ֆx8SF:Q=J1Tm!DCζ-6k؆WVlt4kԪҋNqk4~a EBT_HIctx{2I!X(#},+2Չ5| QߨD—Cf8#!2l-Ac&h$o{ܙjII2=?`"s5Z":[V(rN&ws^5_=c9L*ߐDn@ : \ @)`at,/3|އQ1H?!hCzB]ǁ8Sđ0 hÑ""Foi$3m£pDU[Mt4# J+鎮KF]e%S>p:K#xy'ె;V2/(q_<0dF,N Q&.n6QL3\Ӱ~8ኪ׸wo8B.<$S򁳼.9Ƽ6C{|Ѵ5΃.Kc"9ғ>n$aHMVLd3^υ/{Bi?@Bzhia.|mnT^5( 8$w?] 3`xŇA 3k^ Z3._yȓoїUh=)oǾ=GzK~=yy|ν|3\h>qkǾ ]>?>|$]֯agمi T[^A ۅP݉ Ҡ>L᜘`Uج[u߱Svr[L?S(`40bt[DAX \(A!BC" DpZHhD8?q("ȳIC!jf"(LU"#"9%A_vlN)\ט#8x7]qHD3z).b#"%%eQ''>8( )Y="<,`^@ #@|"Tc#4pAL*:~˫B<`#"^΃CR9jR>{lndɔf8F'B\H7D\ZJc bt%Ĥ49.XF>!NdYcP TQU?@ SD:y+L$DFD8dm-4r l2ILDJ?6:HtO>KZ4kE$Yq\lg`!q緤/]6t8tCxh劈'Lfrcgw⿔We#^ApI 5uH|> C<(>HG<'F 1D-'A0d5HЦ̂zڔ#LNl"`AC*8.(s%A|hA}:R( h78aA^1Bx6:$ȧ4-;> ,܁iD }"N>E+$fj*] ܣӗgg%hJC22®,ܨ5l`?@>\#e!B<H(4.r08A(%5<4C00' 'Y-+d"ZC/*-$#G" >tB/~lD'L21A(hC& ?k-L +L16xF*\l>#B<igžn%`C3( H,`'ML~2jId"@Tm2p/zV <P( B$y 2@(61؁/pT4- PI&DG.Ay(4\4",AzK\z DC6(AU#щEx"4(%F %,7P̂8T<(2r7$'@& \ BȊ,8. |H(7+xC/A4DBCC*pO,4+$? *VjTd~>BPL *L0^ \YגhV-Ђ+,+Z#: Fk 7^> (Cp0ɃR:h.d(C+5`f?h8!TpG:x*< C 6|,0+6"2A, > $\;T"jilb;(F#́iQu7+)6iC/A,C5ȁ2>$U6"¶涸C+A,<0C>A*4DQHjOjba9}r6YD] j\ 7p:H&>،n%m%&t0h6tp'%23)C+A*ԫ&?rNN*:1~2YS3s2;Q4>C2 B',"\`]pgdT,,78{pHJ!ÇG ?а CBL:V( GtB:B~ n0 5`1d!B!h|D$PR}K``;(K{DV7\7HH3D> #;Cs3̂z f8‡1)H5f%Oo,WײbrӖY(20ujƵ\s+>܁m$BeBC2/48Qt?'4;B +; 3K84K|x+r>24ԨA'F8v6\1)@V 68<蠇ys{o -1hƀ<0w ! )h'(03! ر?C/02?|$H:9T0|oG;z%xW2bүX/8{/?@e5\+sbxo]@ *Q0,IHB5B!CY6gN<0 >\V VE1G#4?BJDC-,3B/C?_ߴ4 !((85@/ #ii`d 2dFy²0X(A-8pttt(`$; !2\B5BAw(A56%C+p`ͷTD@$8\4BB,; L?pC\'(2"OLndD'f/&.׺ߺP-fN#3|K+[ R{r57awCT dy(r<܁1IǞ,B*)hɰeƛA B4T 1lw2""DZ8<,|C2Ah,^[@$3C(C{<%4%8+@yH8p&TaCg < ;raKPbI~QT֟p+S26LYZ8;Z [n ThSOF:jUWfժWT;}˖ hfƐ!@09IBt ۇZ >lp H!/Z}Ƴ&YdϏWmxZr1[6La"D  -;< !b1Lu3!h'KJ8gv*YIR!p,gZ MEKv.TŤ(<hS Ήe` Bd}QS>va 00 δ)V)pƒJq %~i;rIWmC!&WcEnjEl|"5=8*&LbGhaţ/rcuhhǓK=HB}8Ɔ5*3s t(AB@eY>{*' q4kh7|[Vp!$2*.ypԈx9敺FQBm"%!&\.n69@ &baU!P}q',Aeޑ)&PXUP;Xj5 21Nc79>*І5!U. 7lPHvgLUcu,P`7m.R4Hc-0 TV@ 濗xo,ฃ8@!~f8 HW< HVPE+l|@P`7&؄*wUx@qBtW0F$$'0AMxC8NdT X@ (qF(Ev H0& %kjځ>rqYgɁ |]rs1U` XA @9XBpP$*@`d P9>Lgi4ߚ0\BlO: hx H`HHU9 8"j781jl|魟ѠLt9νQ&zSt:E$ A/]uOG/ 3\@(P@t0 @3M!l"> V<A!T T! f VnL ZXB!4FTF`ꎔnofj|jVpmc6 ^ V.<,$`!n (A:Ol": R@  '! T A#@" *8` 'z59Zp`hp$bn-65a U!b ,P B~B` "&A3P ^ E HAH஀̜,! o ~A~!A jV40lq![Hq"T+4COr$=Aa&!L ~<ۘ܀*b,@! 锠i,nt!>`aA>` l 52I2/(*hn(20"Q12?#?(o1c7e7a >! <@"R@" bl ֠ .mg܁  @P;! @!@l >!alݡZ`܁H.i֠ J!!s!%*kO0+072u$s@#C2CTŴDU. b!咼,$;TA6쒶(#DA!j B4AA:x!N[Bhn4FaZ"SR@/eo(3sK0s~usLU"CAMEe2[I!P6%;CԤ,7P}Eg.5E00[z-8 X\ PC##3b ătJDr>ADKQ"?9?T@?uVTD 5P>  !T UYKS~&i1mUd$kS5l eecl wrG!תF⶟RnnK#o'soIg)!$r5p qoNqq]llLYwwCmywu!vOUvvC w{xF|yiv0wqW0kWL7f7|D|"T#2v70W#]l|%w~|Tw}w{}m{e~ x~KW}۷~~)8xŀw3E|7ϵk'{yGȂ/]W^BF5Dd(2t4rbbI#0PDT.2! ^A:CHdDl. *Y* Bb!!! YnZaO~K%.I^tLQ8at\kD?Qx^!vcYXA46h4B r Lp5*Jɷ !RxL@P@aa:` `~AA*`.,*@h@!L ~Ap` 4AI4Pɘb'ƙY}#xz 3>T  Z daPdW!b!28RC5? (!$&ځTy9ۏ"~!`h`aW~a A4 A EWC@!P&LBg RJh AbN. t @f@LTah RN f7wmzO6i0Z{YgKN@l5P!Ada3! Lª!RL a0D=Yle ؠ8A! `"@.~!5 LC/9B QĀR}4TA%aT!a"`,@9KAR !a:ހXZ`  d!8" [ S` c$x}k;Rť,~U`. R:Cc!@!X!!w ᒖ!*xbo% x*Uk1K1Z9] }C+>Fԉ+uDldZc*tݱPZ5 }!{>~}i֬a [8;ʚ-[ [nܹtڽ7޽|];`9s Y_׎!cǏ!?*[EYC/ѤK>:լ[~ ;ٴk۾;ݼ{ iP-[(6tr{\Ml n|<޴̡M6 R" 0)$Q8QK?T0@ {(s2r 6O3CO>`Ϣ!(T#< ?`!< O0`㏢TJ,4B*(>Obـ 2L:QJ 1HcެT/@SM6N jvUϧ BFd[uUXK$[n׻YKo{֖tuib1dyfg}.p? qOLk>%drrڄ&d#N8ƙ,qيǡ}šC\s 8%C2BAc<#QΣ8#v(ǃ8 s p*AH.Y.wz-sbBVh/x?ywq39q2#-cj4#c?\8i6ڏ?!MLkP(2B5xqؼ2ȥU}db=WO%~~adcv0OOHglsiYiY>BcY54NA>2VV'w$Е1n+K‡.;K1n}[Sw$*qLlt@aL88.̀K/5 ^P-4AP4"4 a¥@Hrqm[Whxp_Co`DDB'jr'?yhsx2NnR*mXJW$+tٽFrzdY"y>!`<"&Ad*s̤q=7č*Q)HWZ17j9[ڍ,| `Q`3\wfs=Ylp&8)N͜-(^Щiy%ψJt=X2ejАt.-LNVơƄE_ Әt65O0O ZR/o ]ϸLmS ؔ7uˁt jPJY R3մuluTq 6%WHCk8u-ansiO Yuam'1t8g? ZmU)e5d^XѬ$vo w-q*wms JwԭujwMD"qDdETݨ\JWڵk%9&a w x.+x n KxF}?3*Xg{?UJo,n_ x4o߸%mF7|" .r/E,Tײ EU(KyT,ky ~WBC 0[ϢZ"yWz/JZI̦tKzҔ/L#W$,i4njj&>_ XWo\z׼ ` {.d+;WpAuzSm)=?"{0hjwe xSz|{A x^j2dDg{vYÁc.qt0Fҁ G.|,o_sm qWZj<˶iH#K}Tկk}\׿}d/ώ}lo}tMTؼZW|\rx$e(^XI% 橞ɞ@ٝoѓə퉟扞ɟ JN)j M^¹ *چJZʡ !*#/J')'j+/ 1*-*5j7I9ʣ=?s; CJEj>%GKʤM7IQ*SJwF GZ%y0I @IYz" YmC^ `9@iנwʤ @ *Jj:o0@v ৉Jj |: Zʨꦧ >ʪZʩ}qFZ Vg Se P 8GjNJz ce:/êڭꬲڤpFમʮ7kZ }Зgw:Q jY"Jgy1E vPfvQx .gJR+t{)K% qg +zD:{g`<6vg0[5[We gK;y > z@vk  Ҁk\ b|d"4΀ vpkWih=c Accc` kp$a{}c!{ w;Y{$ڤ\4 {ѵV^qnL˺OFl _ gUeַ4pU+C_; b+Ff{ж;6ͰฏkN 2h;ր]P`U $E}k q ְ[_K |뽴ڴ{ۺ) {lAfU\ylKy*?z+t0~`A0 @4 #--Ґ0+! g U<ykW]U p 0p Pu Ր g~  k{k 9;  P ՠ # F   \+j ], a O ^{]l"$ű] ޠx'p 0 ` r0 ||1D\ ` Y lB ׀bA0  bg JR|`k{,v0{ų @ L n x)u pՠ Ǡ  ` "jwm p 6`zDžG{*%M`flʰY, 1ʛ 8łз, ^Š o /@ ,h {5]v> ߬ 8 p0 5 * Lȭ" H= $ر NL Q| b\źl؈] @ 0 @J-ްP wڏ@Ѡ(  f m`X͐L p@ $b @ 0* `ܿx@ =L, fկ^a=ֶl۸n3@ pe0  m  f]ށsSz^ҋM à1` > 0 wpp ,P0  &Ķ ` S!]ł.RM є t4p1M ɐT{ ̐ltM ` ` P mon3^ R0 ? F} (p N  k30l-` I@a઀-޺m~&@ рl`(.e< 6P0  {@ teU L 4P }p YP8P +`R`cŰm氉ᵝUL - 83 1  pj0  Io .~ WP U y0 0 QpkPK ÀL !Z]Ez0NP % 0V np į1 nM_PR - P AA  =N |f# ' IO p@z@ PUhPP2La6@ ,P )   P J"F,XA s+C}9VŊ!ͦjb)hRZ9d-(3έyIZ5eʄYY-b1c*i1aNZUYnWaŎ%[YiV+ ΍%Y_1sRTXgS b"9'1erƊ 喯Po^[iԩ%RKYYcv&ED2h¦,IB\EO(Y$AWlAqDBB 䠐4f$aԶoVXg Q:InH41IbWd0`*Q[0 ЄXM:=Ҍ֫2ᆅ+" ir"@ d`7X‚ P5>(aQUh$1y U.#c4ղrK.K0T+=84Ӡ$EINX`'^N`\*QJp+<-ѰsRJ+*{/kvIfayntXBn -b(3" T 4E"V D n2*\/hZીMv&MsVA-Pŗr\n[IcLDM9Hŗh vZ$ŗ7Dd/\ ^e%_x!*l@&p]Q&(BI:4YetfkfsY_a@A_F(:*^h,`;Ej"ylⅢAhMcM: Rʸnl`M 7_౥ <&a&Ktėa&`:߼B-;zN;5ORNs y $`fZh!Ɩ;.x)Nw\=s찊c~qPf#uE! hf1$e h0gTFX9Rh'3̭ ,!Jec`@LӞ=aL8+2B/q:3 2 ъ8Vs-$izcKgDP<[Ngk Rw[; H@BUT x/l 'XNPN/"f9[dysBwf/XC0 6*ʑ]Ѕ7Z D': ?Hx^.ع]`ڣ:7?Ѓ:BMtP@ \8JL^ o"FpPIBU`Z  Q$g9͙ 딧 F7tX hP HJX<ebsNO6xkRJsDs=YbHa bbxE-x5dB+b!\(b)^!Hb /x j.xHOTn[-$S XbĘ,ޱ Y"M2^ Xb lQ _/R_2$9UtuK\ E\c1 K-qqUFemqa"*:ъaӮ+DM{ZԦKGMRAWan(l6D;Hx,`02K T2 ݌zV|q,dA mhKDjSɠ,~4xGY abt0ܧIiq1錦7{Qx5W:Uw늅ysaڤ+]d c4SNlf"ԗ^zI;0kOct5nR퐖C.NoZ ^!0 -"{0@acn 5~C>.w,t ~!ҡخ[d*x.c\nZ4VTg kʤ,bQ|[j7V?h @@p#'`3 6297C;@5ݛ?St49[XILtdAi>Q7:6L8hDtLǾk ID1 ^NJuG:Dp͋@DuClH|Hȉ8HH:ȌH:EHR|PZpA(IH,IItțIIIdȀDƪ,LXȡI:8(9;:8\JAHăK*,I|||K˷tKlʩKK,ChYH$̼"̲"FqlLlL*#bkfi{q5` E6>+;;&d1e GcMp)ĬMۄ(l*͙;  #F1oM:2,kҌN@*#Y`I,ͪάL$`POLO\(Ob4F@P:4' P!\ $@lPe=tE6D;8Ђ7JƙI< Q  ]mQ@EQ1QQQ09@UNGQ }ž R$MR% }ݒ%YARAR-R.R/R0R1 S2S3R@?RA7 ,23S:S:S;S=S5R5 TATB-TC=TDMTE]TCRj]r[M_JYjEV`!V0]pfHci(Qw )HZ>L fp8m]b=N=F^fdiפX͞5Xk  ,dOXLhjZf c_cXc-^d@no(  $6i\kO.k̈́@d)v^ep *fh-hnEeNZ@ëhce$f8Z c_(mauUdxBm] 5f`@d֠ZNh[eŵiuB`%l^hFPFb26hmc@$A†ldt\pm`H~w.ރ]jdgd&MfmptNXEb_8mpdQjfZ./0RC}$ &^_-j!1J 0)j8X쩆c V 琮l-]f>_H\ήэ`YdvY2ݜo؊pc@uebH 4% V!xڗkb _@n.hپXd0Ixq8ndmnhViHMZ8Nₐ=`$ mn䭞cpcnMl"Zny*C#J؈Dqkb*HljO0lbȆ7ܰq kJ4k)EhEPYAppkV(jocY2XPQ0ę MqaPg^s&fW*g&ugE˵ZV&7C\N nޞ f+>/_ȉ"?p"?kY 0pSxkmVnjExUxbH.褝tkUAtYj"nmsvm'rE:rw<@^`~p2vHQ"Ol_@Gڦ G*0pWvɩ0qab p=OZi@ ޺pq.?)%[0C뉥!>dXЂ$ d a=:ib30A]`UjB3|q[HȉSBeUkΉjads®* +؄UЁ[J  X(fhȉRtXPopdkx xYCgǏk >e߮u@9\Z|J؂MijHx mp̉V (U(ohvktUdr8M(jR|hh(sX &-1bҚ5;V1s, @aՒ QY(WRlSF׬i,d5aکRuX4ƒF'cKt)ӦNFD,4e6bS5i邩u7aJ«n# b;LXycS֯&ã^D!cLXyhц1z 4! )ҦOvW֮_U [n;6޾.|8Ə#O`ǘi \hk҉ Z!weAC9IVp> f|14/HT9:\ 6l'PF;e@ÍQD $Ӌ ,) Ї@sXM5I!5t?CT 0p`$d1B$6ܱB2tP@ 3TavlM XP*0 (! Ri}H5M'BL4¬D!)T`Jb,cBhQ"VwYUt L5$@`w)P '$}Z &b YM\,oXbxKL0u4A"M5 CLCLcU)RA"hG6,8AIx0|L@s ؠGwPL/:q)/$Ì[ lI Y YD@ ". 8)c;E/ПR$qCRNPB)&|M+bҁ`%\`&xP9y<@IElbHӿ0  h@  U6Rqy(N)7PIA+:qB iE`WL"w .] NFLTH` ^PJPB ^ABPB p^pB|A J#|HG`΁E'*U z&@^$:lB> [$эR@!J=,CỸ *1 )OMf ITw8B0v`  N )8u`F.@`HX:H:Y8:M dB9rI iԣ">qgܸF7PuUjիb70R"x+r]`d2=#7dnBHLDk2 ^6,a; ?0.|1`8b*`A*1a Pm x 'd#` $(I`,f&p J/6.qk".@Np kXP <HAJK[ LNXG8[.̱ 0xHWhdj&ހ_t^[)a'`0?8*F0x:{-]6: hZ0N0'yg\ËEP _Ĩ6X|R.G9bjs.0 pɃ}5 +HHD"ad/RWH :A)fQS܆n7񒭘C 4xh*@v5/pI/uM@JA 0C궰$*leY1 ,,M "xE2q8@ 4 V \,^Q3Ƣ;N橅jVm{{8. UMbq9'@;gFrE(:@9[ 5qXY0jyXmeT8VZBNp98A wtyiY"b B?߻_c Zp,r&Hb)9sBV"D` ^H$##w8Fw$ty@$ &xxA$pa_@E9ħN) `0J?㞸E{/+lBq78!d'ň" h̍ 1$& @~ŤB]>((QB\Ku&щ%Hы&1BXA$"T3PP¶1'ĀtЕB؀+tgK@$Y&,&@xxZ$k} 1 靀T.*PA14)^O(+ЌAB \ 8AJ8+8'Ղ,ֺ‘ЛT[ ۾gI| D3tB_O})H-ep9/ $`,fm p[B)<9Z!q ()[+V5,]QAC2\T"!2Nႝ% +,DB꼁 4kuԵY#(-3B,TCO E ]k /C20'*\uCzB;4I$^)*)@AU0B>r$/^7XB7@iBhh3pܱDx(Bى,"YZX@&@JT*Cg8@ #i;,h0|D.V@DB*BzC@,3B6@BkU |գ.D9d[9eڦ@͂]B1҂*x#8؂6t@Vb/'t7d\X$* 5̀IC/hD؀F()0 ~&aG*'=NNT3RguZ%3A*0b<_$N-A<7TdD5(+-`"/8`f4b t A-̍8'0UĀ-̀0u\' C++`1t^gr#2)I-/#TB(p+AT6ق|+#B'%\!8E!B=Cc`՛~ނ@+B 0gj6B*T#$P"B;L()..kh)F$ %gp+n$ +@TA/cB."X'ȂALB)P$DZ#x+CHTDX$A*A}*4N,+ FtWb0BA)`P48B1B!\2CJ'$Dq:;iklZ3ĒHEB$Ȃ+P-6:B@X@(B0*y0&d d(@"X@ M:k&C$q$H+, T+lH/BqB&(\( N1B.B`sI' 2- BC9, fH1,88U08 f#3B(g])~~-!yA+ \؜#dXn)D.B02Af^H") +6mpz0P<@,Hy19]-<@Br_v*@@,c#<$tT$"H MHЀ-t0@ pi9 նB+̀w$%B:$ |E/'h "@At@@IAHY2#&E&A,i/ձ#/85)P*+m8@֒4B3B--9) QV&pA0^n;₧k9p~ws3-40`PWR+xO3A'cq+h[8Fwlm.rl\sZCvL4+82B4|;R 6BLBEtS'2oy.5nFEl8h-[_xC9-8osZpCSK9L;ScfUyyF#Chv7_s.l Q-P#53Z]q%h //7e+hSzhp'e2lP #D0Le£l83A#t:ﺭsMOvy߷ۂTe3@+Ls|ty9g{krmi@,uJOUGU$相Wӻ;;w{{|0 o<2+|".(2)v_zd,S[z7Fç@/1+.:||ͫ1'#Hwa}|7q+G;'4r{K¸#b(B)k:5s}{׃=;76Rȣ$ ,ýۻ=)}=߿ };#~+3>;CGK~SO~"X{ӂ++~>>,}>>}ݻ};)B*,}K~[~'c>+o;ߧ[7ws-~ヿ}.|=ı\ 4.|*x A)VxcF9v?1yeJ<() &L9pдgJ9s?5ziRK*T9xCkL0kΌYs+V_,ڳb7rԭNP{wo` 6|ɧx{7)W>\T/TT9w3sځitjիY6fhSٳi׶})U@6&\aDÉ7~yr5wz~Ȑ{v|q.}z7GK ׷~>P_tPq֋ #/xɇC =tItGёFp'pࡑGm\  ':"ПWDxnE7r T2<Mǚ:Pn뮽:<~Af D]F}H%&Ubt84P$ C?E5c UdGeAٱ`N%JqV*W}Z E4ݖn9cD=ŋdNE|1By(ɦ9>S85@䧁"TؠTpxހ)K`-x7f8d;&K^Ye#ޠC}P$W; Vo9SRO3eePWFlqFx'xfkŠ)la^^ݪ2k˼etыPtzt1wp:d7sA7:›{WE5A>8j,#L85Nu3Y b_uG[=Ѧ+ٺ~Yҿ3{Yy v٦|Z I)ŁofZA40˳1-z:@ j㠆x@rP ۛp ruND]8!gHp;pva _.ka4i966˂P@.&Gwyt\p|$+B"w+1uTE7~c?yck`b8 9A`,p$PQ{.oШLDA% ߘ Nw5% [WY2c>l>}c1$jrK"K@%)5iCт\kNVǁdK/Zg-f `!s֑" $QIg) =(+PF:ȇ"!pGtBtEHpC0ZwF:FA>.5+H tc>C:uitN~H 2V_Hl^R<~xGE-~ql9OM $ LXD340 dC-Ǟ; mPuhE&" B4꜔T=m2`K+CR]aEbsd$h< T!̤.u(Q&~<jm-ԏiu#$mu0Wٻ:h 68gܕu^%wb~#9C0ֱEd!I1TlS"R!bF0$ spB8- lW8B w"|Go vM (&!MPAМ!gXÚPP4iT-E+`l`E8!>lq<( 1 'Q@/!*[, 7ԕY0Rsa"j;O^1B?^`ˎ)ҿg FFW4D:I:8>2,D `&X `э5  @!~/&(p'.@ o L , .0  *  < ,@Al $ l@l@~AE('z(annV 6  @"Aa2|T` 2@XP ]p bF> 0A TA fZ!`o<$@8H! dA! ^a >\! !@ m@(:"L` d+a '7銮"zLǬ( Π8 | N'] a 0O'p @bMl. .!  .RHa#pWP @bm! T=A>o^DR @ !ޡ8! A ` 12fi"l ݖ:7с`n NoQX%  br: F.⤁ P:!!d2X^`B.tT,'p2)pG,Gybhp 4|Pr̄T ĬI2HoGJtJ~gafq(s a ұ@mTba l NT fzQYdLFitJweuft6!H5`@H`ƌP,*Th [eqiui\K &` $@MYa FmN gAEA1oաQ1_&2< gWP:,VmUV[@b@4AALWZ7=,; \ӁՑ]<0a@ `^җo  ! @a`~Lf5b@ @!l@ p N Vod5k"gftJRJ*Lz0H4 rn d T d8JA( f3 6k'XqU! ma kA8vOcCb,@AѪGRnqR@~a8+AG!`S!*`  `} p@p7zoAZxxx `yuZuz;7 ^b X6 SZ!.TTA"!9A:2A  ZL ~M; ! EMse}4x*6J)ʌ:5 (̇(Ί9 N琶 3AyXfA< V TVPr gA&x Y!/Y `0h(p,l ax9C B `ͭ!F!`k␁+RVaat@ a\:AY@tDwIw1+B7! OBy ysow;< b X ^@l^.`M< b`P$ j "@ ī@fb!L4@ L/ VtNJ&Ԡ{EZZK(y "!:*}Z8':G)@aL0^a  kP:f@!cA @` .l8j`RU#N !!Z@+j< a^t!! ZA #!l` 59u9-:q:-ѹe#FP & zOɑeFA$URA]F ^ $jes6!nA dPefE = JḌk\z|[H)7_AZ1j8+=w^aEUA!Ga^? Dln}!dE DhIA<|aP f<2K!|VV@ā3!=A 󐣡BaW[jPvePk=w7ѳ[=!~! Ȁ.`eILj0h;ުlFf"F8.JZ {OY?9u^ߚA|LhU O\|'N KM[„1EtI?qM\ɲ˗+k 83ɳϟ@(NA1(]ʴN*8iR߾V>}TÊK6ƄiG۷/A%:&d߿&&LVj/FJ#K,_2k9W-.\nuKӝS^ͺװc˞MηRnw*>|.d!Cfe`?;9,PCSb0>DQDH_HR<a݅ރcIH eaovXagVZfxۊ,0hn5Ms5ĭDIǡ? uYݒ݉\E܇3ǔOe5%{Awԙg2՟  fPDװEx| sl$Fiɨ袌6hkF82яsZBEy'qݩL:ɒR6΀P"pP*B믿ګk뱴KA>+BfvH(h覫EZ#W@é8#M6ȈJ*JMݓ*%aU *G,WlDw K$$l("V4̨NCڌ4?$t>4Rܡt oxC72|g=4B}ɎՅa?%oO?D0o}JZov LX23'HAE̠7nЁ$X6 W~!kDI! w>jhX8H䞤&:PUCDҐ0X<.z`SSe%b6 cgxE4q{j>PHHяL"=+4a!'i1̤&D!7IRq A%*gIZֲ$,w.Y &"q]70f‘^+IM *ә̦6McV,IrSr*o"+Izol9BW8IЂd:iEDJъZ4[霍7!QBG HGo BdGWXLgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZ5JYOz4/JֲhMZֶp\J׺xͫ^׾ `KBi~u b'KZͬf7z -`WǚMjWȌL;]KͭnY pKMrV utKZ;.vz.xKa,/z6T/|K./~L37N"'Sΰh { q:,(.&!,H*\ȰÇ#Jkŋ{"Ǐ)rǒ&;LQ KcʜI͛8sɳϟ@ JѣZ/Sq*^Vfjӯ`ÊKٳhӪ]VGux.\@^|LÈ+^̘0IS<)@^iޜUϞ5M:ӢQN-jϪU{r۸sͻ1 h:μУKnG\سkν V7yrӫ_ϾO/_(_& 6u9(Vh v a!h(50(X,h8cDh:TV[f)0P+4,CADKApʎXf>FG+RJݰ0@p'P[b @"BP+&DJL Al袌.x]BCXfPu#P%Db&rP)$2Й%*ЙS6j뭸(A&3* >] )N!IO J$@03zJ&4:X.xBAji+t Ђ'Q=$*mD>4A@  AdN[$xv )ӐRflɼ;`@clQ*+1r  \ۧujgpex3/`~z0$TjDH.(mhY IBL"F:PϴM֓XI詍 (GIRL*WV1ә,gIZNҖe*fɐ\Җ 0F!2f:ДK4IjZ̦h(ù5A q:H:׹bXх|(pp #QEq2wХ3n#ܞtq03l)eH?M쓻"GJ5 VV@L"9Nrj[?d 9L'6JrBZ 'ˤ`3j tp- p1^|a[#);ɯB;UȐ}/4ڱv^C 5QFg8 [ SdC~(joeɿY]t(P "w| 30R}Gk/ +yu'&ǁ(o2 B$tְ؀ k ) ց3uאi0e'0w9 P2+@!GtKg YXpS!t cG0PQS(!&PFaeluxt} aaꐆ%`aayFi1{_cP &S逈`88aL涋Hv`nӴWxXƘ88ʧX}eڸэ'pxGaȎX8Akɷ{Y@kn ^0q 0 ADžesvVQ ؐi :Evv`Qta e5@C@#i .P< ;A v 0 AF`<BYBC0 PS8 |N4' b'0N N Ř@ l9 %RŠ Ϡ npx* a ' `B:9 M9qmic0 i אN ͠-q<p P^+f7i iD-zb`( h0y˰ P:7 XF րA~P yB!>6j'w4Y+ôAj q7 7D"zEQ(DN` Z  gPPPy B S rbZSqq~p1%h q|`cZ dG'A: b9 `;dc /A ~3 ¢$ p }qq  Q=IӶX_|0@:4l1գ\cź_C0IBJzi_D0@:H|}hw C<:DУ1*AEZxG ?`DB@I*K  iyz!ѭj{A᧿9="+!;&!'=qQ,912[=6A<B[F AL1 T;U{XZ\۵^`b;d[f{hjl۶npr;t[v{xz|۷~;[{+k@kBx [:`bR  W`` I@}+ @䵨 z`廿z4hչ{O0 kذ2 j_k0j&ˁ3ؾ+/3fK3x+Mk~gĆX {9 ́@ |-\}V: @B=h-HI*ʋ7újAUV]?iQ P0.=0 | )ɔ'd֖\ \P(׫&kP -7 a׿1` ?zx|!mي c&M2Rn3i ຤]8c:iXR_8`h `͝5V]h!,jJ]i .:>, &iYx 1!1 oh9!)/܌d >A HaLvr aI7bg 80 ` %eӉP1 j Ylqxpx>1?JZ>) V.hnȞ|] gB š  |>t` Ӑ5m 9R>.sې Q y-s (.ǥsʽǭr_.t]8WѪ6Ƴ`pz+ IWR]оgX PAz?t mn!k1;\qjv'QĢf\Zۍeű1}O@kaipchQ0P ,X.n-lӠ а0~S8?zV-W-?ϭi@] e(V@L鈱 Aҹ  O~0g 0 ɚ~}1ygfC0 b 2Q-`@zn*_:p8okѼY2YX_^_P й5D6q8D>/ ڿOԹ  v㕾% ^N< n `K߿~MQF=~RH%MDRJ-]SL5męSN@qFS4?@}|jTVŚ2cDq]}MX`y7n6Ҷpόjn=W^}X` F1*H6հ˯,+;1c=7&1Kw٪Dr4Zl%J:n޽},1oʼn6x_GRĞ ZpF9tݱ~k gXUʯԁ9XN.@<μo5dt;ZXh!%X?t.{콪@CU QLqEgF˲;:Pk2H!T1Gl=F񦤲.2qFZQʯhE:D3͕ӮBM9 A: #<̉O?4OS=8O QG4RI%"J/4S.6TE=3TQ#@ DGeM;[5bUάZwW_U`uXcXeeauV$ 'UZl7\qyv3V;S7^y7rMB~&`u5دG_۫8cExqɂ8(dOcy"x &yeoy@s-GfJ0EyF:iyiXjl0>iڥF\Kq&l:;ZN߆{M&n&;XP o:w(Ypo>å5Axͅ'x㏗vgyڕw>z_z>{׾{/{'x7?}}߇X\<i?S77@0@?6Ё!8A VP/`5Ab$`EhA#Da 9xBЅd e8Ő7!lCx(aHvC"xC4b~\b}Kb+2p_cpD'gDzb1c8GbGHG>1aЅ0~ ʐ Y%ZD 79InU\/jK0 dDdࠑfsG!8I^Q#n2 `/*C4yt&B;"@܈\˶2Laa[] c`$C9B 3J ]@2"ԅɅ]P_X'E6(l\.l00,Z[DÒbCDNþ $ ؁J\x fpc]`嗈@DNے>?Fs}B^N0N!IvaVWXN"Hw`Hf@hbI>佈0VY6n3/?061%>{Po =(ta!oȐ7`sCA9`0l `<&a kɋ.bp1E0‹Cf!{ %2$2!i(!Rl"E-FSb(11ͨ5n|#(9ұv#=~# )A<$"E2|$$#)IR$&3Mr$(C)Q<%*SU|%,c)YBd oˁb! v BТ0/\B}"nѨ[ĂRfI3E-)Nk E.q~$,`NvT*#F-buֳ#g,Jx(z" !B#j9,mJ.IbLG<<*l#J)JS0d*})L %k*׶ri+^V!}^W t5\2}S)ؒv!)d3‚PT XJ5p-l`Zm%;[򶷾m,= ^,ܔ sR7Up[w\=vӫU`{C azqJ?̃Я!,!H*\ȰÇ#JHE*vǏ CIɓ(S\ɲ˗0cʜI͈ ϟ=y ړУB0)QD*U)զXƢEׯ`ÊKٳhӪ]{r@}K7nݹv{^| fKÈ+^̸qKp#KL˘3k2ϠCMl=^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOy1\5.H2`x&NkA$! AW-D\HAԂ(فjBpb"ʊ1BA+"Di$r&"i~B X,SAJV` M.RA&~ˑti'oKĉ/oep@!q@ @8P53B܂P EI% [)ꨤ'A $iBL0PZģM<d61A{,J4D@X1L(<{P"L 2vjgK3)T;P-2_,j~I ,k76oN6(~K; O.~JKR :+0AC$gH'tJ)*􊋰*DAz˖.,rTVB/-tOtAو\Y-]r@9eMR'K)3]kREHB]gCySn}] 5}z=LР=.ݚ{**r *;)*)Ȳg.~ /o> w,_2 Hd gPpȠ7k (L W0 gH8̡w@ H"Q{HEu&:PH*ZX 2 %` 92M={4F3pH:x̣> IBL"F:򑐌$'IJZ򒘄"2>Ȥ(GY+EHVH H(]IZp\rb^</tbL2f:Ќ4IjZ̦6nz 8IrL:Nfhl jơ H!Rt c87!fȂMN]'.f3jV}UhMZֶҏ \J׺xO׾k^ `Fb:lcG n F-zы9! hG̚Mmj1 zsZ^,\q[H pKMr:ЍtKZ-gC  f.xǛyc zv7i"7KߋT\~@m.Ur9`:H1:FcpY{^+!{XpF?LN )kRϦ^wuhSέ6]M̮w ZWuIFf;ЎMj[ ζrM4]T N'V"q7gH@B9DYFyHJLٔNPR9TYVyXZ\ٕ^`b9dYfyhjlٖnpr9tYvyxz|ٗ~9Yy]xhf W`@ ˜obKcV2k^PЀю9h`uYI)9 y i۰ ' _Ab`% I'$ƛlK VsK ryL]0ZKYq=Й pdaq dyAgy2sZV'1𠂿<1JggMɣ%ʢTiPi['椋YG$ZLإ`b:dZ&cfJHVa['+ڦ% sԞq` 'e8ڥr&fUltO0tJ:jF:ѡvrZj˒*eaJ7`PӰQ?aj^g,)In 1 >&Ց#`ASl jPy/ꭦ^|*Ѯ=miA{?^)0#W,waU h$ 1۱4j S6@ˤM+ ʟrt#Nj3 1 w8x; =[t 91!vbWa@[[bеhĈGd{jGl۶|pGr;xTvkGxt|+G~pF[^`2YkzDax99k? {4ys{yĺ[G{sGۻokF;;뻯 ۼ;[{؛ڻ۽G;t@K o{44(1{c[7ۿs4;B@C,IJjG<԰M'OT\ edQ\]Qtz1$P %jܑ$p\v|xz|~ljƼ 9vPFNfb*1 9lKP(A qb l<*Ƞƌ R+!ɉRLIװ]9-ۃ!(A d8 p! { 1e< 0e7$iր sX ADY̖ A.1p`xLg4@SjA!Q2;l 12p! I pD /#͠5qӰS/3$b!/ME6G?иS1^K*E}Ҹe q. Mm àR6ńl@ a3 EL@Q0T`!@gXO(y[hmz!p#+x*sPz cY{mܲ R #K (Jm#BcK a&Mb U4 $!5'pܬ}MLLwJ86pnQ%Q KA =Ofg~ק``)0ܹܲ 0 ~M;;aVc@3]; @.dH 0 [sRt Z|%.@(ⴵK5!aZ1>/@>D>rBaa MEnV<"5JNN NK$7K'\1n>; ]n327P D8cko>>tKJ 6N\N'oR22%ckn_<9sMylφ~뺾뼎yV.Ȟʾ`k^~؞ھ>Ja>N:1J33 -怫2`6fArЙ / !L  1d8 Ib.?G1PR?T_V,x@&Zc@q ^Q&`'0ym[ Ћb. ET\?V``՞7qJɐRZfeVq>Qa]M?{yMOŸ_Քɪ?uku/Jb^@a ѫ/kq?` .Cen/5A  #9!Y v@@ DPB >QD-^ĘQF=~RH%MDRJdSL5męSN=}TPEETRM>UTU^ŚUV]~VXe͞EVZmݾW\u'0]}X0JuXobƍ?6oő-_ƜYfo8Zh L1cԸҭ]?ٵmƝ[4\pōG\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ_| ~A0$@緁g@ '+A R' ?1DG$DOD1EWdE_1FgFo1GwG2H!$H#D2I%dZA`h $H*p2K-&B(D-$S7%~ XkKm[o7\q%\sE7]ue]w߅7^y祷^{7_}_8`&`F8afa8b'b`AFsb<j?&yKKf9ɑ[~2ySfwg٬g&hF:ifi:jj:kk;lhlYlUm߆;nN@S{m;HOA= g|:pqɓ{<'ǜ/#qq&TA9StEbpZvApB[r7^FO"p"dH'y# e'lϳ?r3ڞGG {'j9 /,3 D0+8 v@,hbc HbRЄuE*D_ 5."T6|FR QKZո]_YժWŪ5mԺV2i[61W`!k}fik`v^îuU`^Uش΀ld%;Y^41iFD;ZҒSVҘe-kX4B[ZޚU^_3׸53anː4`;]Ȗ[2kk\vw# @-A׸k˨վf-02 8 " eH{GsbFce\"¸@Q dPsXwH0 EA2F0D\X.@ VX?rF-"eŐ,f X痗 db̮ftRVfNZۇ@.[E-rQ Z8ЅLZ #Ʒ]e;3{ _8,j cEsn/|+U;uiVkqn!h,J1 XCcmW|E1qgw,"^]"h zݷysGW]"{>9;1da]#/luzSx0MԠGbB` 1^}oxQ5ԁ́)*P=y Dv8 ͷ>#$8/Zy7Յ?= ca/χ~?}W~}w?~Gտ~W-[|q-"ApA)p?s`^CÖ('2[l S(a`n Rp@*$xRh# AA5A AAT_J*j!!;B+.B0B.B*$0<1|+tBPNu6 CjP؀Ü؀;dـ@̊ĘCCCLHI0ZD PG$N\9Ğ9QID0"XET/9REр \ \EN_^,K4>p =\&> Da,\\kZdk [FdGPj$8+ W Pf$nGrampDqpLM IZ "EuG_ŇHzG!Gqd n$+ HX ǗȈ FLDCtIh(EGrI4ɔ, ř (H4 @JTJʜJI ʍv$Kd,KLDHKED 4PJ1˅ˈKĜɽHKIK pKtK<̰,8lM<ĀҴDŽ̢LLJ|=LM`NΊ ͨIȁP$ (]N  L䙔 x8LO8 ~HϏ! !, H*\ȰÇ#JHŋ {=cC1Iɓ(S\ɲ˗0cʜI͛8s@د@ ZpТFLt'SKJիXjʵׯ`uZՓfӪMkk[m߾+w-ݺl -a LÈ+oǐ#KL˘3k̹ϠC]1ӨS^ͺæ_˞M۸sͻ Nȓ+_μУKNسkν{sqËO9@IIW!H CIE"IJfG,l)Kz2RR9)WV򕰌,gIZ̥.w^ 0IbL2f:ЌO(jZ P5 [Bܜ Gp Cbv’'l;}9 gی1#m?aߦFh*V¸-hKpc N1MZi s,oGvq^*(i} D`+XU2d">ͫ !H'SSI@e^L28>s20}xγ+=π.<]0ЁOMېN|.@ b` Axݟv2c)CA{c44Fx`8y~ TI;Z-Kx@ ,Jj8ĝ#@AkYFL1pؖ$~,Ȑ0 A@2d&=I9 8@^1[hCvLAۚmNq#|dwQi!`p Ƌf4< Ad$@ܢH.x!OP(a",IVSN zEM m P2"0iAk ]j}; ?##1 ngkC,{ D;)7cz:>wof>ѷoc?_3O[Ͼ{OOOϿ8Xx ؀$AY &pzdKIjP &K@ KU0K5 @% ذt8H0vSC(zHJJRXYTXX(YZb^X`vdXfxjXl؆ ctXɢHvxX|؇~8Xx؈8Xx؉8Xx؊8Xx؋8XxȘʸ،8Xxؘڸ؍8Xx蘎DSȎ1}?ԎHUj2^dЂ5  ]P_c]@[ `z!Z .0 )xŊ6 Tj8ah Ԓa @pp ba1wHbTIvN1]V\5fr[@-`!D,idfd0 C[`52$y9x xY` iEԵ99lq!ܠE4VF'AXcuD  yoE$IٛaIw锋!0mG0L)SyK)SʹF{ Ձi`j*ќI~ 1@%z $D_%^,A'nTA9% pږ4:8@S* iҕ":,96 Ē ZU0jsql 3XcXr[ivw PCLY&B%ҁ] ]H:abZzB9Ѫ:E:QTDzWzjWJZWJW:Wɺ*W͊WZ WպvQ$ʛbȭ!ah歂D쪮R41dDMԯ DJA{j KA ۰ A:{@IwUGU > Bx";ı*۲U.KA0+3{8:<@I?>W8S'{F۴N$^&~(*,>g]_D!s":B=>f#~4,V%AMLUKy$t@XZ\^`b>d^f~hjlnpXr.) cZa `u> [а"BqĿH%:&3LLG 2dI 19dDy^.N0PeċT9o ꁑ  Ga٘# +GXE8pξ N~>3V~JN<&^~/mV-HR/ZľUi@|zb~M鄡mNTHU1!["K]PSŢH'Q8>OIB3?Y7o*`EL?"4XOabdҙ,q˸  go^]2~VDNAƄAE{$ad}$ xP)84ċ)O_0`2?_ȟʿp6a_A.? }9𿀬 | & !  DPB >QD-^ĘQF=~RH%MDRJ-]CęSN=}TPEEZxK(-ԤU^ŚUV]~k1\VL57þW\ukJqx`]Ҩe6bƍ?Y0^~{6/eҥMFZjƉ OEm/}}\pÅ+\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ_|ǟ_~0@$@D0AdA0B 'B /0C 7C?1DG$DOD1EWdEIEgQ:qb1Gw-kGHQ Z1I' p pW F(K &X KVMM7+FqBY g|7כm3 Q"p0Oe4R⼦qY@ g$O88AFq =2 >mVj =m%"j` %c8u c#@6\qO_5h3,q߅8_[~B8` %PÂ(x#O8D"x8EgvVbG/@"@(S¼1dgy>q!`DXhWٚ&=EyB @!?PhCGoǝ4FϽw߇'?rw~<晏^GoѕN{ޅN|O|S/Mt8@D @6Ё`%8AU8C(/,H\t7![b`/TB 7aeH99b}8D0G$bwD!POuE*NRb/wP_F2͉х h5эYkcCF:юwcG>яd 9HBҐDd"HF6ґd$%9IJVҒd&5INvғe(E9JRҔDe*UJVҕe,e9KZҖe.uK^җf09LbӘDf2Lf6әτf49MjVӚf6Mnvӛg89NrӜDg:չNvӝg<9Oz'a8/|A{h~$rQڂP`BZsʡZ,j!ʓH)^QI$Hp+iKUR3iMiSvTtP5!pXATuhQէRuP*SZխԫ_f2FV8+PJPV.dm4WծbZ !r=_⎽Ғe+p  {KIMl:I(d9pH EȀA4ڄֵxmla;SAk"DmHֲ"-HjKr rv @! e-wk[ 5HV*zq3\75̫ZoAǗ,B$ E@oQUd \-[b 0BL'D$&[8!23hcVn畯9b8C&0CKN2ʃ%M,d&([ed`ظ+{b2"(", oD 7ci $DKXX i|AI=p XZ) ՒU¹YvYE7?y !f΀!, H*\ȰÇ#J.3jȱ#ŋCIɓ(S\ɲ˗0cʜIMz v@} JѣH*]ʴӧhIjիU-bjU+׭^^ +6+XiH.pʝKݻx>L74H`^ƐKvL9rɖ3˹ϠC-ӨS^ͺwMÞM۸s,{ Nȓ+_μУKNسkνËOӫ_Ͼ˟Oۜ ja2B<4JC}F(aK bTJ7 =B $H+P !P-8'(8Q< NۥC0a)AA$MBK$AAm)8A IYl"7C<@)uM&enT@e,bA; Kt# (P!BVPmv駰' MG%i %h(AhT\R~&p%A 4,$ 6+ @ 0Qj-S er "![*t: ZI-BKMM]g/>G_ HLB: 'HZцÄ@D!I2BP(8dip(!@(8A )̡w@ H"HL&:PH*ZX̢.z` Xi@FB0F0fkI#HHb؅.YF:򑐌$'IJZ̤&7Nz (GIRL*WVBo,Y2ฆ.s)[ҕ@-zˍB-?X!bGLCF#Wi\c&D@bKC*"p͐BJZT990+#Ƞ*iA_bJ%:h3d_F8+$(Y_(yTtN򥨬MwӞ@՝J{YT:P 9ZUoAUjEh5!XՅ LZֶ\J׺xͫ^׾ `KMb:dėZ6kfii:!x|C jAu klg)jnwpsQ_za ].x0SW\#V^wnwUvmY;~%OjKwBc_'HFq^;86[X8pi[ Ov=; ñ41y7$bZ*e;V;A1_0C(q&Er&+1*ɫ 6S&cF1އ#UƲ)q<\yOm}ެ R#!Ak,gl#x8!cLψK:*;#ĹYf'MJg4\!D ᇓ5Mj!uW͒cìg ָ5j\^u.Q M?>I40]}u0t\0;x` P 큌v _>@|Ecd+H4~fjb ,z}E8J1 է1CW%F &abAA !\u?|v6yT0!7?[{Pi#.}І1 `^A4ӀGgk='LD-P!(C19A61ct%,zC:}i:;i =kcBb s((6 e=?@Βd(g&>: :kWw""#R{c(COa k'1r""WxhJ|A,(Tm/H6/MOQۅ1 =/Z[jj!A oY~mQ'{7J&Ԧ׀D H(WUjGp08GF G  ,Ȃ.7@0@y1:؃!8胝 lH<(}JTXVxXZ\؅^`b8dXfxhjl؆npr8tXvxxz|؇~8Xx؈8XxgHwX]YY%])) Ŋe:}IgE"e(og Iנh{{AXIwVs%P0I ōa`  `dwvFIx֦jswI`[PuZP Y ِ Z% ,5 ȑ  %Ge~&^,2y53YXg7<ٓ>@B9DYFyHJLٔNPR9TYVy8XUZWՕ^U`H5dTfy?jSlٖ;pRr9Uv Qx|IO~LYtyKԘK4Jyٙ9Yyٚ9Yyٛ9YY.[wWќwyйT&F3؝CAwaVej y֐wy=] ]t]@] zrܔzXPơ *@ WUgu;wp G@\x6S$bppQю R0s3 Qdm;: E]`w'Y)(z 1J5DU QY:gjSl:pjh u]Hurچ~zXPv Q$`8ezi5yj*\D?R :PHG@} C#FZZ@ᐏ>&R|^QJ:FJ_An"uqQxj|   UDA% ʑ FixPx wd ;1cp Ucq$KDd+KD`5{Eۚ% @K@; !u` *Dž% ]ԴP$6^K!h !fжno[a00at/%!$e !n[ʣ(i𰸋B$ѷxZc*fR\E#&Dɹ\YEū{ټʙ#&k[p *$ 0 g B蛾껾[B;%bpe\a b`1p eGa <\<@lJLD+' D"<>T&C(:,B.2lA4\ t8@:>\@@\޺ 0Ġ+\ءF_7QO9$A?`3d\?f|j=lp;r<%fB$9|L? 6&; ltLNJ,?%ȖSə?Ȟ ɦ|A ʬ|ʥ Lɐ,˞ȃʸL? ?# #iu@P'fܾ.Pĭ= qک۹-Aq-ݎEؘ( =V= m5 =K&*&[ Vf MߤIF]א 170r[E 1abڠ3M D} XЂ RLڅ p) 0 $D ) 01 ؠ] Cr ` Q~R5 " tۢ! E PC^ P #1߀ ACNa#ct{HZhw 8+!Pΐ隞:@Q徤 o,eYxqD21^t "^pN! ޠ(>ZL! p[[X-W@p .=B ʰqR5l 9]ð RI SHo2Q ` .SFp 2l' 0H& Wj" "@azVFQ .QD-^ĘQF=~RH%MDRJ-] ] =}TPEE RM>UTU^ŚUJ[~VXe͞EVZ8پW\uśW^}X`… FXbƍYdpu)_ƜYfΝ=}hҥMF]Э][sjڵm]n޽}#[pōG\߹?]ztխ_Ǟnvݽ^xOG^zݿ_|ǟ_~0@$@D0AdA0B 'B /0C 7C?1DG$DOD1EWdE_1FgFo1GwG2H!$H# %dI 2K-=jv3nTLnXK"klēp'y? ?|e R9ip64NS} kSR-Hm3i4rTgNmZ$KG~Nh:iHiAى~HʈP'O"dh=!`6m2n %#cHgڥdA#4^w(\1}]EaTa'5ĥ8ck8!G&YŎ/ْWfē1egn wB]á>hJ`{&MxP!uHdnkQ:0wh'QMa5 h:'jꟸ#UuЇH!|-d-xW)uG.gіw^|]1;{/'k^'^'lr5_}lvt#/@ӌ5@| 0NP1VPD@48+'HQ4FB=؈$28@cHX'G;H~Јa2H#6qCINuRDΉW$E~id  bP1umD4J)ن:9clHq@ZF5@PѡfFZωd%5 Knғ$}2IRnNKVJV.1H^鐸#fvYL !bf4yLA)3a #60jVDgFţOzs'O~hPjEPfӃ QIU+Cچ˅fE zсe!mˆ mCv1H)pS ^+D0 qU p̌4* @k;&W -81!]: ֧!p"o~X\8c{Plq r0ɫ`,6¸I3AGTx8`A)r`Op[7( u ucU<{WJc88p8,@@ P-0gln& c*7e'0D@ fx\e2cZ( x-A2yBg DAQ0+EY;:AB @ ECWzAfZQǧo1YKzAp&KA0 tM=k)F Q\ֿFP:Ɗbv#W=mjWh gxv4 i oQ}ϽnX!.=eCUHo! wF'7 )lb7Nͼd"@ 0 C8q+yȾNœUnnt-|ayā~yx]23p; )QXCy._d+1wy<7/m/Q?yǼ\~w~-Es~ #vԷ+ge?@8=UV{K2}9!Jv7$2/U.0i>p"}7@?#w+B|~/eKdId$@D8d@k L@]$4DTdt !$"4#D$T%d&t'()*+,-./01$243D4T5d6t789:;<=>?@A$B4CDDTEdF?)G* @s@HDZp ^^k[RdBM$ Zp8[AXXEYNE\`]HeTOh̅bF^FO ZZGO,X(ltGCUW(iRXG{{G}GǂH H Hv<: 8l <ȊHHHȐHȏȒLI$ITYȬOrɚ,40ڀ؀IICR ؋JɕЀ LɄҤLPpShЬ|DTʇ ̆MLTNM}# CN,)ʬLL (JNNMOϗЃ͈8͆HP J 0M ͤ+ OX\ m X#PmNpЇhxN|u}QuI WZ,-M φhpPĀl+QHRDZ!"}mP%uL&5MEJ1N8mL9]Ӗ@RraQ7uU @ґ}q:^#)RTpS TBu'yFEKS :TTJeRHŀ? UW?uU(!NU&ŀW^ ͈R=1mY]SPeU@V V&M՝@VAVTkpQsVTou (`tK{u\ |Y`8E S.~`ؔP8[!,H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\yR˗0cdI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝ3TxU˷] LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(`U`0&!C F(CḮf8@ Sh($hu;0@hI窂33V*&' œv|;n:H'V$3oMv`\~@ @\1[xfL{\u $Exҧ+|E `"mhk!6yOҟ r[(,Q7H]6I`ξRSJ R`lpP 2'xA}Y %P r p͐lx8H2q`697 "pwq !̔VP2qT20 Q؂=v;Q3nᅒs1 zabzazzx/ x x0#WHXр&w{xG0hUvP3w7'a&HghHKHj:81vX (X#Oçx9XXN̸<:QΨژcMq푌1 hY8U過XPIrL0E%:JVm9)!PBoaVs"9$Y&y(*,ْ.0294Y6y8:<ٓ>@B9DYFyHJLٔNPR9TYVyXZ\ٕ^`?Ƞhbjlٖn p)%EPts#δky6{ٗc | W#ib3)6٘J @ ?CHy5y5ؙ u  Isٚ<"90^I0#y/L S0U4 /m"̙.3"j:i/yډ.ٝ-9¢ Pfт)-ٞ+9Ry*)J)Zr ) ҠqL5"Eo s>#`cq]":)CGP |\2 P V( P 9 'R L1IY S(p Xn`b:4ODhjlڦnpr:tZvzxz|ڧ~:Z xwGYBkS~N2;8=$c A@izo%4\!ګ8: h0tj=Ra @Щ2:\m_p'wԐ,qʮ:%êΚگzڏZ&(r>3o& À0pCAx3ɱ5 @ $p Z1+ˮAز;!jBI=a#AQ%A#]1'3R3 **H "1ӵQ:WӉ]chjK1nUl H22z s!@ KJ^ ib1᪪P4 ɐp#Pi£&!cky1л; isQiFi@K ڻcۻ%YRBc%{VBR;%E %Eh$km@+0+a۾@^ @p@lKj @4!f8`qz ArrjlBE!dϋ?OA_M| !M 4FQ_-H cj 1@{lU si tLp0; XȘ*ڴȐ<*#1ɖPq  {1HB\_Ħ>i?, 1 bMYs$ǻ<"-$lÜ2i rȬ<\7|Pw`B#,RO% tC ElӰ0I YT#L0$B1uDbUB@ Wd RJT/uLwPKLeN`Q' Π @Ee IDD 3w,א Y&. 1hP-3+J`W ΐ S ;dq 3ϐ")@ ,`0FSŌ3( YHT>ϴq Xl IT/9FԠ `RUl(OM'=Șp;6@mGŽ}l p{xlP w$^͉ͧ` c mv&-}: n9j{/@->[ ieK > aFPm+dzN(o A|1q#|t&2}ChwD9pD~=v YuH 12.6 GˠQqz%z  )ay%-Le+6*tCT.P~Aofq! Y9˪z ܀| ؕN`!MM 1LdQ>^~Q`_}Qn~m`QP o UTU^ŚUV]~VXe͞EVZmݾWKquśW^}X޹ FXb?Ycƕ-_ƜYsbE=ZhҥMFZͭ][mƝ[n޽}\ōG\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ_|ǟ_~0@$@D0AdA0B 'B /0C 7C?1DG$DO WdE_1F؁k e1tґȓԑH1ҧ~G~JH p 6Ң& 6H1 1H I$ZI4j3}0O ЈE4QE%7d̊(]Ӥg6M&i:)L33>AJR7ZKOgF[U2+i<]GMig R'~Iԋ(eTJ kMH; %ttץ3VDuTA(aStQڧј%#c!j EiA ߏ Q 79v?X?V8V{/R0hmY9ƹ"[:&hohfZM:j/Jzj>j`;l?$l׮0m߆{A㦻;o"=uՍ#"Q g]'z]rdwߑ]ww*שx䟇>'!b>z>{ݯ>|IG?#O}Zsߧ?}~%a_hoD`I 'դc `@P?`A7i]AP4 U@Ѕl eȿr3!zjb9xv ADw(ТWF%vp0&Mыԩbw&07bF #^%)519l܎ bd@1DGh4:8a8#X '(օ`Da``R:' !d*uU^ށ/b` 3Sҙoe+X-[Z8"5 9Բ] gUIǫv$xI<$R_jLmpNjaֆlO 5EDm:.cPxcY*e{R:n<7 g9#"CKf:NߛP-< E_ӑUڠʸGDK0/\H'B2R:boҬ>Aw#͈VBOv\~MV;Ŵn)%yZ8JLG)Ӧk_\o%̯T`d<4m=nl}mqF6wKnuDvw7oyB^elj7h)^B-8Ƿ8' nWA(Oky 3ȮcqІ7p;#XxR ?+È_!л3 k!~f; y $"?)B8À!0y A1D°|384C 0iy@8ò>D`5ATIJH-Q\H iĢIYqح9j;c9%]Cy,#:Y>eڠ؇KLD@dPP;*SlTW 1hYZ<OŴӓM$h&A_0d<ӊ3ʼnF;T3jkd%!:n;^Ūl%vc-p*ZGiKj2eHkɸJw$ԕ>{ݐd+.x)γ}'z9vdlﳀVr_=mFHXu%~Y_a8ut~7+: & -U E82XP!(,v·$!R 8P*l9-(<!VS ьf a ,ʙnQԠ̩~m1n~s*,RҹΡ3;=g~$AۧEg͌&F*U-B{idVo,JM:;8 e>JcYj6sm]B+u|k NrVHO˞nh[ζn{MrNvMzη~NO;'N[ϸ5t@ DgV&z_sFG!`\4E/ uc)Â9֍ ^/1NݪNpkmtW>+MVuYߢ~UE-»÷SO2ߪs~U|B/S:=GՇcߩ~S}rK='%Gߨ3Qo2!|ϏS_ςٗ I`i ]?s劌Hy/ +Q@ t  ] j86ǀ X L$؁ "8$X&x(*,؂.0284X6x8:<؃>8V-V(]i D[PQ#Hj2gW^h [0Y(@fXhxjl*B  D1B^~_`r=ex08('%5x؉ VXtH "QdN Qۅp Pjqȱh 3~! :Dx!& C`rjЍP`D61f5mb]oƎ_0Q1@hYSF2+`E A˲1 +((iqw~% @ C#ZPaUrCl&R b pkRyT V Oѕ6TdG;0>mPP*b {s6 `& Fx@' Qs0j>8#ďA $ֈu4 pFb$=9#1"Y+ry"ʹ%Ҝ!2XPlԙ eh Yv" A@ݹ A)yBZ^Pw1R 24! )$Wl QUzzoy:_F6#Q'1*4Z4*65j$#zS:xw`y6(aaa MNOZb`+$ڥ^ٚ`"9=@fJ{Ԡ;k #Crz6eˀ@UIuyu'qJ jʨ2`zک:T2u ѨJh J@_ P Rv" P R׫ dxFjŀ ZJ)& 1ڭ:%  ʮS Z Űx:Lj7S JtZm; Z{ʯ!K;Q KV.",{*37%K⑳a@[B{SEGkDұ"-;R;NRH{ܑ\b;d[f{hjl۶npCqr[_jQD~6@ t ˑjD[ yKʑMQ;Wyۺ;[{{ƚjy;[{țʻۼ#p;[{@+!`ǽ;[{蛾껾۾;+%_b!4caD ˾;h5 JQ  p AxcL;|; P ^Qܱp(b_@#% !|DLC\G,EL$%aȡT\V|T3%;a! Q.&>qIYH||ż($A  ڐ l#x(qZ jW۠ jʔ I#TDL0˴\˶_˼왯!PΌǼӐZP||la]xeeh엀(wvc0_X<Hm <]M^% 㰨 ÿsѯ&_;U=Lݘ%] aZFVV;造0' pwLbp 5נ@E|R@4ʡ3c 'Ze-B4 jjyLv}V}z}# PA PUULYcmŷش wx}٘}@}6#ALA=` P ֌̲=۴y`>UM_ ||ɨۤix YMťٜ W441A舾MΖ.ݘݙ~dl  c0ܦ..  P٠3 ^/Nĺ Y 9 O5؞0> }<ܘly( Nޚ^̞MR]-8`2.5h=`=0S `Y9> =lwˣ,;A< #,Qtۼ 5(H9L<G˿YV:]_X}Z_f_hۍk N,މl}x"h!Kqsék a /ِ .e~ zާϮE7t'FqU 4d0vx$6uc۩A.*+ O{ڨ5]C|(gFgZ#0c9FsYlʳ NHIS #>7яΛZ:t5=K/ț-)L,?0>6D&i[0[JZ^ēӿviyF阇` c ɸȄ(^ 6!#5Q":[w[ SZFw KpH'.^P `B/NfBfN [<6)h%Z#z(q pr4>g<NxitH~:q[(x%Q!o%ɥ>=|AIy7q\C pBa !'1B iCBx)aC<Z Y?ݺ.c /&U )pD?` idz_xJl_BZEG.!hlkk%JnJZ|A9CZo{GP jT7UDCJlҐf۠|cp͇| D]݃о`HӋ, ̃c11 #yh5 t;9 #ãH[ȃȃb@* +< 2lhp<[?\@txx== pW(BA@=?Zsc@)D4* T9n{X` P`EhZ30.&`C-0`0pT H1ȋH -G.XHpȐTǕԱ\ @Gq$Y m@& ,@#D3_J(scɥA;:ƻG|"@\(Þ˻x4D)Ds Iʪd `&0ʀ*M3䒳L|J³%bP ȔL #̶ tL ,ʌJ dM"MT1ClڴMzM Ǽ)͒`E ۠Dl̼APL1"[ʃdJ1$OO^ ,4\t"m腇p"ߨ}X]~Lye ؘcZK %:)4HuxSa|H`PixlfR`h'Em `ʊlЉX8y HC#H,v4 fJxSlRy|Ù~ɋb~Їlh-aщ }X#V@lm(bu x%&] Qc4~Pmx Pxeh$`MTp&Yp* qk8x`xXP ` e@Xh/J0Mxo08NxoN$H|xH^*khXYx>6g=c&Ђ} 8\%e``OHfPic25Wk0UiXVApHtRRPif>p~Ѓa0UxY8γ9|TpW0Yȇ|ЇohІ,P`h*k@E`(`hVXw@؃xpg* Hrf/ܡ<^xn+@d݇'XUhhb=xUik9Rsj8ˇC0`(gx(Yhnk*@Qx*XHX@>d6>mxtX X}$XC lxGSMi@Hne+~PUfhwT8ٖ'CYf3q&~wHXPAB~`-,$_Xy@,Um H gt`oY asHYx}kys8kf%,C<t?'Hm*H[% s pz|Ђkp(~A0=0X@`$Xpe5PU~hxVmtc?I  p'߹b*t5P|p 0R'pkwo` v;y7f$C[N?dk8:0t'e女BRwx > l#(txRpLke%wlSP͂SXxaZpVHW&Xk`)lynp Y&:qp0M@S5h<0QwgcPlxO<` jPRXueM>ph(~o2Xn`tNQ:-f P" p Ď視:XJp<HY(HpfV@pxx 5mU=|3xh56p@xЇ#|H Hچh6JdN@JHYXX@< ly,h „ 2l!Ĉ'Rh"ƌ7r#Ȑ"G,i$ʔ*7.iKR%xI6e֢[>iI lɲi-`LŘ:M6_#<0gsAt8~8+8;8K>9[~9k9{9+]襛~ޤ:뭧M>;װ~;/ ;<+<&>7K/?` .?(< 2| #( R 3 r C(&!(!F<"%2N|"()RV"-r^"(1f<#Ө5n|#ؑ^б#=1~A*/{$$"E2|$$#)IR$&3Mr$(C)Q<%*SU|%,7LC,BLC4"f,HXl*$d@ Bf $!jAMJ 0VcXbu D5EVğM) o0t f)B a4X@A xBUF`'A> ox րfqD Y-x͓ NMAo`)lD# @AhAh Ei(ѿ xDQkb ( J’d8}pPׁ@ؐ\$u4(DBJt:Bwªp\C [s]  l**-J_ XR3(j`ڂĘev?I3xaZтa/K4--` F nBK -[-x1G"E[DRk^׽2lF)L ͷ=0FW0p0,aS^vUHKU<,ij&>1S.~1c,ӸƕfA&@f~*"<ࡧ־‘ ٿ?nQ%]{X$4#0M#&}L%`V<Nh!}/漹#E0{~>E h"$ `D 8і^~ctDҼ9f !,H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\i2˗0aI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷p5x˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f a㩦*C* a@?1P ˨4*ok֚Ͱ:Я,쵸q @ ᦻZp"A$8fn|{@< @X#j00AFg8)\P{|g\o7 4&2A 7'53:mkBߐI8yL@iM'q*@.H#o=7!C#ȴv [K$8MbXЉ@YżA-,3ݮ {}B60 CД9A0)@ԍDvG$AXy4MX/\c*a*M;ՍsmqP4!v&AOr+R 2t.7)@:sC;4s3P3ݘ"C*6ib \0c<Ӆ ƹHA `l@Ht(̫FBF.La,bưcVx&> "al("FL"_&…NZ(*VX.v^ V(2ZfL#T6:ncR(:vcP>~ M(?1s !XX@wT (\xIN eG0)Rj/ PR!+gT2Fv\y##^HȎ-ᴽvLE+MeI3`d5_)dC|6Mp"r#<<7N?v Y@NӜe=)yO*`g(SS$BЁ!j14 83QCG;z CI*%8+X@1f,mRTdȢiN4`m06VI0?ҧIUo)tl*VJ ,%fƵ- "f!iD!Ҍ42 U<pn Ќʀ`/AA(V -pB ߽fJ̠3A o{]#fN;'Laɥl  M5%/&m8}gL&č[,>YdL"HN&;PL*[Xβ.{`L2hN6pL:xγ>πMBЈNF;ѐSI–6|92my%CmZL]O%LZLëYcɒXT]M_%ŞR`ݛl(e"͖Zi[3.ݤjsM8>n%K:7u.=$H7o!@7}$3F8".qQ0xF2U7HSن;HB9Df49vCx@`?o6T)뤷Fdyza@ ʅ R|hOpNxϻOOƯ><C~DSɭO9Ch:Lg>O4@{~D`@ !qH5&y>/?4F/OF"$C hp~avtBةvxI%.5*v+O?W:8 ȇ_&~ ȇxhei` Py86*2XlsqY RyTo@A`bFV4crV;-(` 7H{#Q}2 \ A Ѕ^WGc V8(Z(b(҅) . g\ Ї҆X6X1'1'.@E{VQ豉dҀz^Xx{W 0s Ϡ ɰ}  7ڰq؊zg xpb'Xg͸ҨXqhڸэ(1x쑎 '؎W g\QXҐ p2Xv(X )T!MtYȆՑ XU8,(02 ,6?;  @TE/"H PR9C)faԔZ 0_Aq_Olnٖprɖb_uyExqٗs~nYe5yYG4٘vIh,ffZxlf-q Ո!uyjYy5™ٛ9YI1y>1A0u bD^% 0jAkB eZ΀ es BB75A՝YDI[ SYwE DL =$5eZ jD5a 0) kW EP y8Dϰ @ عWL !ME*zv٢  Ƞ7 A驠IjFHQyPTĤN[1dbÝi 9naIif|:Bv Jw7\ ʨ1zZکj:zڪ:ZzWq@bTɫ:ZzȚ63Ьz ZzؚzM:Zz蚮꺮$ٮQ; !@Ⱄuگ;[{j ;&԰;F& sJi ڱ ";$[&{(*,۲.0Z1P#Qy* `9 G+o{3|۷~[y Qv fG3ˑl۶rx6ABk[mLU$*ֹAAi#Uy;ΰ\ Ϡ28 :3ػb 0˫Qq;[7r1: ɠ=Y)Vt2H {`!4i[G@ 0 6nL}<\|1z% n&0 [y1*<+p!1 #Ž?Bnf qU& qz(|>s7Lz'_`$:|eΐ;"Yr ^̄^܄^'/h Һ:LЀWzLuwǔrn ckA̵U;K ˳Ba A aJ  XK%|栊X_,M!ܸ5L,1̪Ka!̛\ (%8ّ1] U< d< v, !e  Sby\̶k M,{6<MM#}! %}`;`I5hQ2΃̂Eh*L #eAӥCѡ/$L YT+V{xH-4 &0 J;۳DmrY-^bJN m`+xk]%@ @ N@Ci=h׀lx{IiA ГQ ']y2(`  Zuى=@ fC#8|js m БfAکw-wm<'Ѓ T0=ɔ׫f HxMFLC ŠޡC v dkQg ьm!a}ޱ ݑ `F-M\ BT& @k?c#P 0㯐 o Z` S@>a9L`x]+))  M `6gΒa<:  xQ>Xd~wfՃ(`QN-p{udɬ= 1 ͬFuO `p #Nw{==RD ⰼ!GN!3@ (`Nࣨ.h Ɛ !&A >pa\`6b拜]~ $RwP a b ,Vs)@mP{o* Ξ8, - M۠ v= zIpت^L aa`] oݍ1oW>+~ w(2 _!5! unO/) ^U( @ !Оw? ]hjbP_\[jde9-X_A .uZfoyYn?꯿؛KQ?u}L` DPB > S$^ĘQF=~RH%MDRJ-]3g2mw M< TPEETR4>%ԇQŚUV]~[iXI%VZmݾX)͢ X`FU^bƍ?yaɕ fΝ=-h颒/P[쐢I+n[pkPōG&i57=Yglrݽsk}yݿ/*|Q~~$@D0dAХ#B /İ 3C?oCG$DIDWdźRt1Fg)k1GwLF2H!qH#DR"dI'3\I)J+2K-4n2L1,2D3GM7߄3~⤳N;3O=O?4PA%PCE4QEeQG4RI'RK/4SMܮ y4PCM\LQG%HNK5O׃պG=WkmuSa Z]^%HU}S^Cv; Tpյ ~sU[Q%ܯGdgGf{ GkI&t1kdyŷ~eZuxmUdGlgqVkgei6&a≗ev37fG}GuQu~g}fYǬd&Of4ub7'^'GgLqFez 'd j3mo*%Wb`'XRbVyHXbYF|"*X<1A ek?V pN &ffx@Z)h㿧^NF0ny6xe>(x Ik=XuvȚHD>"Hx\h> ۩p0\QD(C J@l i,h f M,#Ca [~ )؆5'TT0xC~7L`#b\>p \aAQdlc5q 1 x8a >lBD0 -"YLHA%c7Bpdb$H8 84067'Zi;n lB6X2-@V,@7pX8<sf4?MX q- JX: Vd PMLt"DNhq8C1˰*BPEnDP*XP-0)d!?JӣBo-`f XAC6B#> yXO'oh9y .P@ %ͻ B[ o(HպքX.0I @+"_# J0 P @G4H)[\!H>rRư$,P `e J0A0+&`LZۀ+{x@ =S2W`C!88w$@N  x lBx]@ y.6wD a@ (*Tx!< @ HA9 o Rĵ=MS&q?(^+ )(K$a_GdaRDOX_CD4*Ss`Q C<'+[_@M[tYWhȂeW8;2Xx MF _@Gl|\[]HᲅZZH`$^Z{ L Z(_[G')G$ ,[IY 3DA!ɒLS(yTI,GxXIɜIɗ9 7 0??`8%ʪʫʬʭʮʯ˰˱H-0C뺗?`8K9$ICbKD5$\ SػM QLdJB{LL !,H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\i2@.`ʌI&˛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ L?I!$Ř6˘3k**͠CM4PL^ͺװc˞MLlͻ賈ȓ+8i`GKNqZ[νwСB=ӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,! 4X0/P@ɡ10 d)P*R BD6C tN)b hyߘ @j)g{ , @0/-0˜&j,YvP-͠bfz-B˙ b /hjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l?!33M8#2@0\P *L 5,|5$ܡ+\2״L20l?lr9PGIOGD$m@Vcdcċ-r6 Cp|6ؔmEj2ڼ 0 ݄}xtn8#nC](ͱ N+d-z~yqG.9儗.Atn{uCD.@rr 4yO'zG=z@|@6co@C3Y_B8Ua R="#cUǃ;  \2WP=~h(Hq\ns@P! 6@KTd7<:0`pYoxR ^܂ԛG8?4JL8!YdC]bH @ IGq hm,#>dKK"C8㱨G q#dA\r k&h"IɮpF<!=I?AfC#J2I:zyG:#@%RPr%IS(U gdf*/Q)O>y`D-+$ЗA͙g~_`CG@JP.RUO* 1HԹY@*E/t0vQ[#6EvemĥJmPLH451GU"Nר2y?k-! ʈPR^ Z9~:lq(%T&EdoUⳠ mh1Ӻ^,oݛPֺ35lږ7nuv6pYW5=rG:}ta۹Z2ͽvRz/xW.=z-}o䠋դW;eK^ Ѕ.h1N׿eE,UwT}[_H-a< JCl` I-8Qd1JEcVW@0Z~ac3b-Cp|hj_vЃҤnek5z@!A 5 :iPlԗ`/ յ dL1Қ(P9&b%֌񳍂<‰@plNeƐ8T0zQ֦я#Hc |//UG1:nM3ŧ-8OGFqzׇ3D`8Aok!n Ş/ѓtNԴy Pth\cb/_Mv]:)#C ;ogXz߾x[! ߝ~p 0nW// 0#e/!+$6 =}W!S8FwNQrgϹ@ߢEs8g;e41~ 4A@0a!~p7XssX&t6E 8|CWSAV&5&1G B';j7" y p |a 1 =x'"-E~@PS ՁQ,noЄ1OV0rR8T$Eq27i Xshu"+9Kmxe@tWau Z=$ P) Фg@!   ap vPOA%aeCQnqd 2 R& )p(P P_̨9{TuSu]0xxݨ/((Y:BmQ `8pSyJ7HvuQu(PuVf c8Xer̘xS %!GF0 ^9&wH&H0ҥ4Dq=p!tYq:]qE"SkuPؔP{8hᆔ”E yQm41\ybwy v!} )tf !逖׀ ,ey ~yq`mijYjrECA(9!V?qf9Yyٛ9Yyșʹٜ9Yyؙڹٝ9Yy虞깞ٞ9Y ͑Y6p Y: j9P dJ6 Ҡ 8D  0w1 )U .z~* U 8`}XQ*0T T<E5 @zBj FZ1E. M|Or&E*`T @LJ[JBQ2:ɐ>b76g )>k7rfʧQ``PuBI99! @Bk #*'O' oj40aɯ3/U , /3/{/۱ k/";R&+/(,..2.4[r8k.:ҳ>+.@2ACD,Fo nOK[,MLp6R Q(xg ͠n hXp(qr װfQ pdHM%)QJ0 طO& Q j:# e Ǣ!)Ź{ۺ;[{ۻ;[{țZ¢ 𐫲 KtX0uK( ΛQŔR p 0{2k{% {*0Ai|} #`| /; qn^K5g*|,-.,2<4\6<6B7B%l!>`ckjD< y M5/U{hju*1~zl<;^s}RaDzp~ٝˮAPPs !u^X}kњ&L3jAf1.Ι9A3.`W >Iae0@!92x/ZnY!ܞ`ƣe}@0fԐ)a %AsVnZQ !pb>fW4 `SW6.b0 (bP"ꨎ>N>ap^Ǟ6q7P Bm{ӣpmQ{*>2(˶fNn^-"i ӎv&sl}u ^vQn!F@PB{1>i~W @yc; qӐV];0}4CB )Ogs|)#}\'eqߦh ׀X`)+@, ql?YkqmNm-q? P "ox!T*.=ti8MԄ5f>~/Y[No&=(઱ 0} YBA 2B^jSR?!+/[D08p >QD-^ĘQF=~RH%MN@q']SL5męSbhI\pPEETRM>UTʐV]~u]ƂEVZmk281[uuX`7Q[XbƍEdʕ-e˝=F`7ХMFm2BS#'kI>Ɲ[7`r\xY$4\r]tum7o6սOW/]YGi6ݿϨ kǟ_~z ?D0A([p1oA 'B ;'ϻC?1Ģe>1EWdE  tFoqAg!s2H!H"G'#DkJ+|"TɘĒ+]0D90EjI}ɗg}Ҍ 4ijO?[7,ny?G!\4RIssr,I23"q0Ǚ#&IzSWe3- '8jtnMS2գRUWaUM9ZT&G~dSbgvZk 6J(@|l" @ȚZ۷R7_}6b+~M8h`a$`ÛPE4k_ոc&#p' hZ^0MH>coƙ8lix7$i Y%lVi_AH1dI!Fe%zd~Z$F;\GIxXAމ<7"A"ZpfyeGO:q4o&$`% X&%$:!!JYl?ULt9nJn蝿$yp{?؃)Ο(tF屘&~5t>hM8@ЀD`@6Ё`%8A VЂ`5AvaE8BЄ'Da UBЅ/a 5kІ7t kic8bC!UFD#6щa8EDW Q .vы_|8FQ#5$cȕkocF:֑.ckd F4ѐ GD6ґ'AUq+JV#5Ɍ%(+IR"@e*UJSҕAbY֒%)%J^CJe0^!f2LfܗdD4) aӛvf5Mr1 ʹN,3kdg&\e^!wr*Pe2f2.gNsz5e7wBڲg>]$$+uM|x)f0̘<狗"NwӟAfT"ER-CAS I*S7uFc"s6x`!b>LU9Dv=mjWǼLb'!4q1qgS;&[cE|ꓷ䊹oc ZA1pɸ3l4|d@q;4fXu9W<&'y80,V+yQoM)3!zgu #\~Htb,yM!dG>ޱ>Aa:GMuo;Bzw2g^w9#vN 7|ю,,Ũ6ФǷ G!c8}Sozӣ/=;O&z}~/<"bXR#I5|' o`D!:O=va8f zGz]b `H*j0ؽ?ރ+k_C {qN|78:yao(YbD`3@us.;HtOຉN)<^ BA45mAZ!nyᢅgQp< LD|s7@)c E$ W$;%Jq EH)sc[eLi,.0 }Q"$0p1\۲QwAL!5uF6@44hK4hg1mdȄ~9xI kľɆppɑmEAÙŇ@SDEâ0dZXAX3Q".{cvÕ9IyI1z C;QK}qyylPqRKI{kEY[7`K_l x Ⓠ~($~ȚIcI :FdAŁqBi\KPܺqplwhք@st"M4L5]F̿C$LZ4ɆLC{@$4H‹=#t=p DHxTdȻ~lB/v dAKI؅]pT kȃ_[9Dpt dJ9bJ(9Pp,h18e!%x@i|ʙ;yLa@[A9t@a",$,URe[Р,PRP,H* 31R5S 4#vkӉjeIR>Ey@:x4Jw K<σh1LRxHXJ&5R͈+'d7!dUMbIbePXvU0emH8ѵISm"pq y b;Ag˯q@O!UdRud'cu@zzze3.hQ섈.x VHjĆe؆qdkR˾O+A6(;7Xm< VcM:*ZI$2UR+KCPX؅b40%5;P IPQIQPtQOOPeZOhZM [M || ̓QLWZQaPTK =dōPOHڪeZQXERQZpMPɨt\X]qڮmڧک\ZM][-[U][utx\۵Zu]Zuݱݰm]$[P`[\̭Z lTHQxQpQ m/Th̽uPؔQi@P@6Mx߃6"%v`J`^Lk@PX[QPCMv[5cJ0lRRyPO `PQ``x\Qi%ˆQx_R΄QhT^T.bqUXXƇh4@H3|pua|`?0-cbJiRlhd2PJ``_ed1Z$PI#ڏ_Qxq[43|QnϴZ=`uZq^_s]tZ5 ݜ$^ڕZMެʼnx9|]?XPq"0\Sf]lڬ]^ }^$$i^y`qdMdedU`|uܭ\Mc\q\΍ff]Mi~~O]\kF|u^t ]i{iiiMN`^V^^bjPWȚ.xdg^KfwPJiIZ8Q8ΦE\`pΕ\T`^"^k^D.vo[xM0TUJbdK0)L@Qq`Q >bFa.EZu(R-Vۖ۸=dž{c0 6x4 %qUbUڠ$WmibayQ@qEgk}dpd.dib-cxp̈́PP62PݴT^]ڬhpMJP8`.>f&f5fb5 g~fՄLpqgg曕D;qMtЂ3O$. xD=@[ikkx!epC4v[XqhTcl[BOFs~ۼv`c<»ƄTTU7V!j!L:صL{Q$Ew8,LdAN-A;܇ۡlb\~m8m4{E?7 B> 4,0 ( 0 zӅ$?}#O:NE-uS<$r6St HI#"K$%ME14W4VQPZ6܊.8$͘⌳WPWydI0t6 ƏG&NEjJه0&:&ԧBm}N?oSBYJcI|p^ nF)GiqئKXui$* iEB|v) ~#K4#Z2A =4E`@6(\rjOaTŠb`3 Uh0`0tD/G٧<7]^^+ΒS3k9#t }P%a8J( HW&!w0Pg1'!04qsM?aAi .7!(!׺S $,"aD89Hqc IT.fYRXm<s ;=#+" )AGTVPeR5zoQ d<Æ_ #()3'Dk$BҲ%.9%H~Rx9VVAVf8M*ֱf)/} L D )q Jn,41h/('#8,b{Xa.]_\ *R ZKZõV\1$^tC4M;^X-nّU??HH 81/JU/Iy<JWC+-vh+!)~BTeq \Ӎns;!ֶkt8X6(X-p@z)K:COzq돨v2w+Q8i2J[v2d܄x˙܂4>7as0| Mq3NOЩt#5oG>sVׇVu6zӮvkW\v;^g]Ê]kb)3XQnG%ǥA=_bj.&oUo:US+ ~ 1>[n( >sF;jol_?_?lA>`df%} mk%!F!I^!`vZ~!!!! ơΡ!!!!  !!"."#6#>"$!r`$V%Z4\&nYC>, '("))"**"++",Ƣ,"-֢-".."//"00&0\u\`JDD_3@dcM1cc<3F7LXʗMIT#E6lHsQ:8 E(aGaèchC8_X0lC{CDQ3$C83a8O$0EDUU^$B TOP0 &0$C>*(!d6|0XC3*H$7 $:|>L@n7|2X9B EC++f@%C 78$0 Aw't* B6 *$7(,a6XEBH)dbB 8|+7 `:@"/LC1DB$@@6ȃрC)^f?C'@),)i7*lC3,|* ?|*<*pC+X@X,a@C'H0F?|@P T3 D4l'\);AzR0l,,:DD@$/ARkwF *E`R3k 3@$@@ $@0lB0l-8@)<ϖxt0*7(:07C̭3 !D0DBq@T,\;D@714ؖwm6TA3D+'@(B`Cլ SP6+tL&+B,PԂ!7P`dCx, ?L#$0HC>|CGgsiB B+\) Xl 3D D-C= \4hA/*C"zC(L,6/.4DK3DC>?0 p@%$ t@x@zB:C3p3@h3O;7W7;L>I2pd2jC<C:`d<\4Lãпz4lm7h>Dgu?XBhd8.X6 l pFnP@%%dW7Il\4<83,ylk4<(hJoCvxI DyFtra~#~/>GO>W_>go>w>~8B~>>үǾ커+4>3>׾4?~GOC/L!2 1D5C<?1X3\ ?% C}0L/'D;1?ÞDu,d@[/Y-aC!F8bŊѰI#GSʬ}8"  2QRIƔ9fM7qԹgO?:hQG- "L:1L^%tj B:dZ Z*8$ X8h(Ck:SC!!|s GqG ):{h I:!h(tΡtf<և(uh񆮆0*r#`? N6sq30h ?R%,1 ׫B;ΐA :&E5y@4#"mHtsP_#EqX2q`4c(hdZ@!,$*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0c>a8s.ɳϟ@ JѣH*]ʴӧPJJuͪXjʵׯ`Êm*"ĂVeʝKݻx˷߿В a+^̸ǐ#KLyW3k̹s^UCMӨS^ͺװc˞M۸sTD̤= Nȓ+_ṬK7UaR.s|aTN;hI[Ou;)(%)>DAgQv * Vh!_&ۅvo ((),)$&t4h8cB- H! I1&C*PF)T@0+(V"+bdD elIG)-u9Zm Y"i="(6њjG6'X "P(؄@ĩBgHi!ꮼ$4ʰ+ʱ&6F+VkfvlC" | (z |"h, | "k'p*G,Wlgw k/Cʭ~4( W,*"g8L<3(@qH'L7PG-TWmXgmB DdBkH[`!:md+4|O曾" b)B* y#ɿ I'T(-M砇.:ߚx>騧ꬷ.n/o'׮DW? b_e.4=fB?iB/g E3,Qb"+D"(@ܠD{̠;D& *-iB!N $"h A C|!T=!:2DH7HqqIԏ`TNcХ@CT HoO/ Ca X3H)<4C# EBh &L*#FBh`BfUM1#j- W$a[U:E`ZB{e`B`$D 2v w @3bM,X2A[Œ01rQ c 8Zd (4kEr!…!0#.:[({2|q _ }-Жuh&Bp!ʄ{ڞ@Nj[4"?mHta Sz^PTJժZXͪVծz` kbhEYKδ$\JלBky4׾Jp_KFd:PB6aVZs0f`]& Қ85EHcmeRYHnw[W ndȚ Mr:7ttKנ5K8ú5Aؚl *~d|32oE/+"&0|0y0z ` 05_xp~E`Kx(NW0gL8αw@^J !ez&׵-f1 :_EZ`a b`,SeLh*2jz >Oi3U'Q]Ojg\{TgAڥe7E4-;R/t/]iKs/CiԨN[tկn+ktָsΣ߯=X@w36dкYE2ax,1ex5 7_%`z@4< ds3+1Н>لA`$j_e4 Ed! ~{"@raw# u9xZPKm+P '# $D 3( %𵋐 #WαYF# |F|ױCyDT!ty4]?Dڸ WTȲ @j"BD"#ذ] d&pE.Fko B3@pix\"xE aL!2t E<>+>&E΋ Q חW?G a| sմ7sIooF 4S Ӵ ݷQ $ 0v za 9?ppmŐ U8'G+ 3x:{ R Z"CTK P }u04 `o h"  &r3&Q q 3b}G?BE~ a NrpHDs%puqx)P wPD}ATcgAmxrz(. CPͰG]R Ps w]D(  p O` ӨG8,U ,5M HPD  nGj? @R`}A ݇v Q~s Qv' =E Z jOʜXIV (  1%KJ):x d2PS:2x@J ` Ͱ?g=!z j ,{ RѨ5!*kٍZfPK\#Ch)^t*Yf)k!DZ xY ZP"zU&A'2p` 1kq87TRr @Pz`kOYzPQy#*6?]¯RN Q PQbb8 `n±8c68*"+?1f(8˔Ұ`1<0 QFYamXAk<^`Ґ')&]k*Q+*kqQ`Fp: 0΋s˸\bw0.x0V@m@CmX۵}p85  qE(PЅUB-0<0QA I38Jl 9DL;ʘkyp v ? w@A EС# QXL=0^:e'd9|!f` p“:P Ѯ wz:}x!qf!H<8pN|F{w}ұ :YNjy<ǫ?]݉5ar " 1< @Wx+ 3<]ޠ+wpBY. <P= ވ;8vp=) ЄL ݱSQ)@Y*0~k1 8ۻ=۫^=&p &18-^CB X/3O;_ˎ ]K䫣jfn85 6Y:``Fɰ pEL9Nvp ,n;!Q 1/s.%qGG`Nҽ<Np\@y.P_no^^Թ&!^2B DЛQ ~!H*~5p Q sl^w O !;,]0 q,24EFo 0Ȧ5r s8CqHP 1*{rV + .%ۑŸ␶#gЮI   @ѵ&"0pj{ s`@| HL @@ J`VIpʠ@]蚘QF=~@@)iP]xSL5męSN=}t6Ќ Mv'TTU?V +@ D @J+޽9r&+X`… .F \kXdďc,p Ăh"I-W](b=8kVlZ0R?.IdÍ,~\9˝ Ćm"l{];LX$Ҹ#P5KFX0 u/$@Dp'ln 9D C1f")XPh& @XE%%¼ۈqrGR iyH%;XXo  C (׺o`#+ϖ%?⏣3E2\S,N<sO$OA"F Xf bT @z Z P]jQS Jy5;]rM ^lQEQt67BCXg׿zU0`J:EZf5#](H=ԮiCRsg9Fdg5ǁ &*Y^y{wߞ708QuIU^r3BJ"ȟtbJG&yW]x"?BR'dɸdo9gw晠 GJ)Πq{.iiAri B `(&lF{q:l%(离n nH&p(o?>}˚r/7p~[Լto~POgG xsuowH;c}#~aGȎ{ ^#{~ާi;/Hvg?%xT AHe$ d xX{HF8O 8A JE %ť%F @O" )P1Ѕ"aAB aÁLO?Dǀ8|3$bD&Z_S1IQPx(,Fp"(]ogL$ 7CpAh0qI5/l4Wǁԑ:HINd@2Sl@~GߜЕ G2򖿜,8KQ̥@x9$_ә:&F=t@1 y$C##89ΞE%9Yy8, 9OS=/wRC̈<M#>:({OBf8)kt"aDENiVQ& CQ@:A, -A DKiP TR|Ts'IlD'Md,xE*PGs7 @4tD l#j5"`eB05;c\:j7d[`>`"0Blf |jrR>T 0E$R~xֵlYZ} 'Q&2WָIdcs$bN|F@},q&MS7wDٚ,ãAq-h'(U>t! !FtsDHnn-AL1ywi|rBpV4JvFdcXkvǣLjAfߣ ѧ qE@f2?-}| !DAj-V`~AӒ]hYg7eA (>`US!]pp>b꼑%9X<6ѫ@ר9?gǝ8Im8Oq^i*2x㥧@b@ki kpha !cA5+ p(,ȇs_HA TByY$ ؄“PX+ ip` iihK L98\m`ha ( X08U8ɘ _h\pC@*TIB9e[XsȂ0;RBX8V@YP {#WȏLah!ȔXG`tx Yx‰X(YEOy) $$;aGqH ȩx[ B8h4bFJ**7G1q4ȳGO_(bPTS:ȏySp*ylr{ CCh^Č-98Ӓb Jl\ތ*4CeA7N0U-IpkYFW{ep0(g>NT4axRhPcӈ n, 3 06+ɩrCSx)& Jb0L|Y{X>ed%c[p a8͉ DzCcXa.kjehBݏYlZ_ 䁰0oHbkki|YJiZhmxA윸 E34qY$XܿeXʌ0IɅ.`W{2n|ŋ ]h4j eɅ X[D1:Zhrk/i_)ppVK+qepoodqS[h  ]Ho6 0k Wq Gj&7Z' +Yܰ I`QZq12gBN} vTqM-td93OtsHIP rDuQGTw#u3Z/XS]'\7uZ`'UGsa7auJcgd!JohgdVkvu`nߓg_vownsrvtgwQ4ox_wzOz|'|~+jguxwj7GxaFؑic +H!1 !x cQ@9x@7n0j88'20/P.rKhns膂?*؞枣1ݠȆE/z}⥖./{/&dQdT0  '× G gLJ-nx/ rkh|xP$o]^͞v"F l *ehyP7 پz牖A*Hcvgئ)/`=U&0hѳLj(PΏv0~g-x,h „ 2l!Ĉ'RhbBqøP@~zBL0mȴ1摢2gҬi&Μ:wa Cy me1()֬Zi*pʃ7-Ɨm >7Sqo۝_ć ;x1Ȓ'KN6 _y1)G&ӥ 4qԪ)NZM0TAؠ b:xB8P`J 0[S2g]37'P偣)zPUS(6/o|]@6(,Zhx^br27Δ׊3M.Ԏ;R<#?iC]S9.YZ/  nMPxa73+>d`א1AYzkJ2-mB" س#N8G& N%uPN<AR9-QK=u7"jq1bxNP=G)f3 ]\`<-/q= Ptdxt _RvuDfPoɿIP Da$Fg $q> ӷ6Dl@= , >k(C&]C $HI_:h<&4!Lr9 $FnYn ći1c@c!f3v3ڔ&Yұin}W+O)rjEdyW s}abV+{ d#2"uC*ĸ5IoHaqRdlCb-np[vbu*ܶe#u+B ji}c'Še],N$]PMdb=#ڸEz6tU&b0{ ʷ#09; 98a, ~# ;0%a sx.0-Ln tC;^ mZW,@׹3qjL 1zqal}'%1c,kS.ұde"A\+'cS҇ڋs喍\lNg\ϔdιeL?^(<s`;/yFsZàe`&#:.D`QNIF!=dCd,B$&5G28H#Ե~v܈Md(˗ҧ0HfihSQx+bl W;y| p Jn7nVѣLUC%p qɝ疍x`, 8bo k$/De<3cC1nyy0?=%r'2iv/^kr}9iq>?Aᯆ}9/>/7>[xHïK#yg6p$ QEQAj6h8B."3CI`h,a& BBdD :@@<& U/B0C j`,,@Z@VG HYA`ax^`"!C 5J$t@ Vzȃ@AA5?0bAt1n>'łHFE?44L̃&hD]4"ATLA?,OdB$U .DH/zIdB-H?La1 $OP` vN$Q`PjQ.%S.#BP^ eS^%VrB6UdVXJd%ZV28C/er)Z%]^>.V\%_FDBd\%bj`B]Ka&dք.Qu3ܣAc.BPdb]x&dkHȃԌg*VGkkMAI 0Mp@@AP? Uno^g5DWD< AD0E\?H 6hRA:M/ GDbUA&vA,*CBcY@4B@7$T8@AgN4M`DZ!5 '@HCH[EAyA,AtAp4Cd~h鈊|@{2@̒%A D>aLA&D :P^D̎~.)bĄ6< A,Ö@$F@@cR>i7XaIZ&B0Ae@ ٛD\z3/FN/V^/f/:v2V E/h///Ư/֯/Ca8A-X1|[B'x?:HPD0Kenxɕ1~4xS,WćKC pA@)sSLk*D X@Ā|#l3.M"J)AZŪu @{[jb8?=zso ,P Olp(<ʺE 19co> q0J EY$4QqQGF R!,#LR!wl')7:R-/ S1,K+LSM\7S9S6S=?}A -C-TDmG!4E%TM9OA UQI-TO/MUթ#(UauSi[q=U]_Ha-cMVemgM6ikSGmo Wq-sV]:]wWްڝ{W_X~.Γno\)ӹX9=YIvI KNYYIXY/g)eYyex'yY~:}~ y?|xRGHx |'}i5˔/H(|iH@%@ :Sz.0G%|8G9Ƒp\81yCN8 $I~r'3>̆Z#HBt#:K"/TA,(WZPK+7HxT"ChC<,(kxںe-/N,ml6XFl ,9`tsd$\1>NE[D] =.ry)4#̄(qQH Ȕ0Fh`Yaսp/*\ ¸ <( xL-Y=vWQA5:g=-5)J؍|U"_mU~-`$0V!b60{ x!!B@>` Є$ T` +7$BT, ? 1 l"c8 `9 ! D@)k P`'L (hE(l#DO25>VNgg1f Wf1#f__ *@ ((CR@}tafԅqTgQ%*U!IJ Y  _ @PdC5D9P1cChRBx*,cIaik!$}o۠ݶZig蒝egEN!,g1Z 0]_a`$1|&֝n@jNN vp eǒt ׄ~C 7!\`[3~cN[8CӰF- IEV!{+}%5]Z q ţ%"2 LM郏/3g2?4hC,3st<ӡ^"}@/fcaˮL$8яi,R)]`#G!Ni!źYDDoD)O|B^&_Q&"~ $E/Ώ OO0!b nBO|O!Ȏ|R' HHn[m[-ݸ`HD2!hLB N%P¬ptafFp20m A"ux\)ԋ .ǎ%P4k NF(P20F/ᶥ`%ִ°[  eu$K?[ jB>רP2 Stem  @~܇!ab[(&$%`o1$1Uj B%Z'u`guAA[rNciA 3  q'̀a !A  C!  ´bA,\@J\ !Dm % <L[AaA ΕM+Nd.:&d2$4%`؇¥r!'Dt" ,k*0̬ \% M$7Z/ " **kW FF DGRf jR"##KFI./%.z(!SRyy'Sc!2b0 5b$0U nk F"%͊Ih JRgs;(!جHs=3!aA>!lr0cw0(Чp2n3dr vn& ޠA6i9.%"wZ:G!;DD(8MaEEqӊSxq9A6?Խ _8A!0 E6g.bz!h$".&*&LÆT"!?tM1HHM0IGOh7J! LztDKsl@Bpa>PSQ At\ M-24.y tB@ BQlFfX4>"!g('/uVu"SAN9NA Bj` 6%@Re?s(.eBhYVñ,3m l"Y FQXR7dZu(@*j  |b)R` tA&&]6(WO!J!!\@WMBTHa4TVG|zπs``%rW  jA': $f%z|Bc%c+c*u 2!z! #!u}Hz,ffu$\d{$j ؀ 2,!:Z3fJ@]s:TH8uk6liV$\3? J"X 1eaf%m+6*n 6A@BoW!"nV!a*T% @ B Pw2V T7^7u%ZWr"WN6$w{@6BSQN[8%tyw t+X6 lg  qoyb@G/7{l{{q$ȷ%$}S~@~*?NUB Wz_ !rKp8! Plr'BAL.dQ #恕2XUlw] "g[B3Ȩ Ho2b B,Wۋ@U#~ Ax2}x # &J!T&!`qbX2JahzFr(4j  aה:n)c'\ ʀ 5sФw~1;2¡AA!2  r !aV(BC' b/8 bDhCvW+7lv ̠Zb⊫1Ɓ{ÔՕ׆&$%-p5Y@cwb)U!:bAy2P`9~$Y^䩧T%2!"Y2A bzP2F5oyx-fzn0QRrqaua%!Xq %^Z2xy% p`*}#E@؆Zy9ڷ/Y(z:z;{ ;[2z3)b2zAʳ5#{=[]Gs3dM` "`".N;lZ !!:I;`) B2"؆{F{B ".` <+c"J!x[¼<(ǩ^۽ Ћabv`n@s"V2<`85\^8)6MM{7Qk:e :&҆霴h ɏF%X8Ϲ "R˜ˉ :c@ n^>{ /~3^ 灢Ei]NaGWiZ>Nh9?~rC~wՄ,jded!+j^1 bZ$$ R.aBtG+t> ]GN~ B쁳^!^|؆~>h ze(~cB'|,ԇ{, ~}b zq-vsSb5?(z-Q???eS?#IſRO_O?NL0OGC0… :|1ĉ+Z1ƍ;z2ȑ$K<2ʕ,[| 3̙4kڼ3Ν<{ 4С0AH$@R(.H# 5ԩTZ5֭\;AH@@ر]˚=6ڵlۺ=klCl}7޽| 絧&/Ō;~ 9pEH[6ɜ;{ :h:լ[~ ;ٴk۾;ݼ{ <ċ?<̛;=ԫ[=ܻ{>˛?>{2ۿ.]`ȒG` .ȠC @x`^ana~b"Hb&b*b.c2Hc6ވc:c>dBIdFdJ.dN> eRNIeV^eZne^~ fb 6u jY8Ad]iftF0 MO8Ť3\]s 5 )3Ls犛L1@7P1vs~41OI#4ic@v 5HC664"t˩0Y3B("2<@cgB 3|VgJ& t\)(@;70nCR$@CHK!SYLCnX*.:w^4CBż'u@̬ 3!*  &Ψ Ol$D<`ND2P4($ C1@6_X$4@uI@ (X=5Nd А0sLܾe4**+޲(PO] e=Poc}Tt* epY59 : RQ)5 CK,z MaJ a*g>3 *BD B(0 l,i[)!6BB4 &XЊP3TI#ː 15. z ;k̘P"K7p"I1ekB7`CԨt[B7[BBz1 }#˨EˆQڇ(ư'Xm.+iEt!^{[|dp-Td)eSS,CY(#(H…>D.ACYH걖,ELY--gB c0jX5!.z34Pd`u3DLw!UdC[\^5T(|b>2,CѮC 7+H?PTI6w L pB!o`r"]ֆC#]o@ ʥ 0r0B :{@0fDaـIhD`@AH|@rZd+67DbQDliB pA7E DH*kҔ/1MȅxE1>Dp!nĤhV(Ѐ,PA!hpeg\k5#@ S:hЍ'p l`qC~ AB ]6[+o1_  ,<cozz<> ` MHXNC"(B.*3KA" Ɗ i Y-`ʵ !@ roŰCHy9ljde 9@Ne@PͿ#NfRa \1qx z1/!õhт.!0;@A9B1!r.P)G eE.v>70ⲅ@fPJ?DXǐy lA` ^d(2ƺp=*{SoP [~BQ `QFHe !u0BlH"6(&x_g)\yF 'Pː^{1 c 6[x `b@w q"' @ 3}C8qx 1w>K3;(0 fH,)H| 9- IqNഔq .)1t`뱍"HYDY@Q`YOdB pS pd>; S "ĩ90 ɹ@ ٙ ;10s@v7yi yzv6v)c7v)0v:V9 QAቡ9d J*Q*J''Z+z QP%pQ6:;>j2;*PFGFJ (, QB2@:V=2ʤ?EJIzZkXڣ!b WVYU`#ʦ*nh:**j2z uz|*-!,a >꧙bA:Z?AA*24P 1%aj BӚKP0W@!rʬΚW0 pP# A1vbXJ{躥KPKP`Pa19P ( z4Z-]& !7K@:<;AW*QpMM gPuj aWkXkPv vpn 7@&TЮ3oƅj6JaK1@]g Ep2pQ `B6.56 Eed 4#%Kc ! /t/ 0be o f p!Z{{۳ʰ!ú⪽ q 1m ;.f  5Kvx` /d7 2D'2zj pRY뽂K*@MpK %Qz ~_a[H+0krgZz` P*-@:\=v m./ʭbj߻ڽ [P a m>  a LÝ3:<0mmku01{a*3ʻ fA=+]k Ț Q=[mlv5w,2P5|A2 a:0 @נ]Z j{څ{ڥTɡ,ڻƫ<ݛS - 1ېڲ,6Yk*  :z0)P $Q̵P*; $ +  1 M*<m@+md?kOmq7)[];bMܽg]au\ y= q\-mA !{hA rMzPp s[!,@0"\a JHqŊ/jqǎ ? Irɒ(OLr˖0_ʌIs͚,Q'N?*(ѣF"]ϧ BU TwWŠKٳhӪ]˶۷pʝKݻx{߿  È+^̸Lj KUW(k̹ϠCMӨS^ͺװc˞MSHVŻ Nȓ+_μsЖKNسkν{wËOTӣ7ϾuOϿzr nJBnР r n^6 *^z^hE0l6c8E- w0bWiū,U#pxAa1pTbl IdlƯ-w .hA\c18<̤&7Nz (GIRL*WV򕰌,gIZ̥.w^̖Ib742f:Ќ4IjZ̦6nz 8IrL:v~ @Z&S ևІnRJQ8Vmp[E7agn,&ђ/X[HjҖ>n3ҚTJ mӞ@ PJԢH8&岃TJU0WPjUz` XJֲIF<.6]]7':/iu6FxY#]c# m|ǻX bíx X;=,B,rVF1FҌ5֓.;̺  D_bI }2) D9}D:Aq ،2׫&YW MM6Woƿ K0_A w @!^#[2I,CU X0Ƙ dk[_7EM6ߎwd*fDiGN/_1 hv 4A[,` Фl$0sqTȕ1BUM D8U|hwF7lFl7N+ !oNAښL%V2Qj De82"mY@d1 l 1|!fآX,z[IYc2M@(/} #Fkլq@ׁщCҋB_ swrVܨQI$@Ȝvĸcyq ]U`(퀁vZxO*1sߣF=jv/ⳊO#Ώ~/ث&k'O~.Oί/^KOMAnbQWgR`; pF CUBXRfE!Z'V PdHRpq+ %f QPG$؁hC ZL`5Wa Tq#-# cSy3f`PT$= kc %fPY'3x$M$X Q`T'C23t 3R;Wp(.`&r7&RdžjNX$4 2P kwHQ3Q0Y'v &Sd ؈Gn$"an􊞱, ((W@dˈ.i@(`0uKڸb a 퀎ʐȋ6ǎ~! ب0>"W_ѐ)>im}Qt@!>#Ibd-ɏ24iµ(<ٓD;)m؏C酔LFɔ4 E) dTؐ )l@j@cW2diR NccnԈY rp\%@PEwV<8 BmU(L"OEgX 0kg0` @ IW9ڠ/n Oe 2@w` dj)iOIš6 It.y OQsӒ cO()3X^ ϠrL0 yY.P Ĝ^`EV iase\e6`. PJV ' @oxPG5 `8JG,?@0: % dU갤BC .P :@( ^DRyr+ VQ8:-DZAʚv ., +ڐGH:W0bJ/`ꉏҌ+ʩGګ'J, Ɛˊ8⣩qڨc@[,IѢ!c5Ф_j0  PeZ!5C%I ݊ŚT ˰х]QY|;+H@Bவ:,KB`V2ɦ-KI2k )^E7Ӑ_14_UW?YB+ @cUG]hP!wJKT9+jG;r[VPdЧHi>#l=TondEH=kaH ?WѤ5JNV S,5{KT=V.0$VQkRfP<{=@:K.fHQu" -֏3T W%ƀ\o$HP.1OpSm+`! = y r >F2d-O=-l]Tm/KK+Mڂamܩ ]]Ͱ5izWy ̃U}ZVpMmrMVWڱ߃D%]Z+Aa?_lV1 }@2X!?eÖpA n^ C7q^ƾN%*q-n/.TY5z_~j豑@?Wv #@_jn鳁?% "\ny Ӏ t`nri겁C `WmmPm{H=5JC׮^\9䎔~2n~+yn~MzJ{8> 3)BI۰ | L~Awb1co):1-aAc'(8YRM`*oy @wN#3(ʑ^h0^p c6z1:zcp8O*XQ` p08WӀ /JPfyK3^W,@Dz€,< +*` ,#c[=R*騋WA]1q Ұ YU;ֶ c-<썣*?.V pS<0WV&Si¿#cR:ů+P z[@8Y] `n: @ ^3C@@ DPB >QD-^ĘQF=~RHQ Ppտ)Aແ,TPEETRM>UԃؒU\IhYC%'b0YEPZ$GU\uśW^}&w7#=@ @,kE Yl[f 0/MFZj֭]fnm'׏`܁ ԧ[`m l!5խ_Ǟ]vّ0Ǜ4~5@7 YO R?h]#@D0A#!dKG )Lek!/YA& U/bdE_1FjLhI4 U(q QQOfD2I%dI$ip`& `K/3L$ ,*&A M !JTFL?4PAUfq *FH+6V SKєPO?5TQLȁ`V 5Y ,qT_6XaB dJ `:ݔZk6ۊ4Z, ŗ,[ @6^y祷Z_USnEe_OE8af(Rp6w!a/8ce$eōG&d k_be_9fŘg9gwv2^9h&hF:ifiGMi:kRд;lpN;mזgV臁~;  g nzOOkIgJD[z x'xG>ygy矇>z~Uƹ7:Vd`/h{g}߇?~ GWQX: K4-. l%p|y&Av@@xDa Uͅ+a e" la gCp*8t}8D"1( @lƈOb!ҷ@4(E.v}H E/ьCBߨ2эoD9юwD!i@0я @H=Ґ(B`Dd$%4iq {@= d(E)#@ v Qҕ1#$4KFf0/EІDf2E5uTf4)%*Q HH ivӛ:! RMrӜDg:Qp kD1kHԙg>չ0n/;TL7>p>t@92 O ذ G/֐oO HFGtXE @W.E0`Ti(P IDf). d ̃EmZ",e3v# dLZ )8FnC5ЅlT] hS5K]#H:DXu E.vY8E>Z+LgshxDj][Yxcz.j!K]Q,Lь[򂏊-&?i)|?‰"G ZQ̄@KD6RThA [Ԟ/ȑϔ/߰wQW}w?~Gտ~?$4DTdt <#x-j,  @ l.qv.`,/cC u|4\1/ȱ!\b "%Tc* ,-./0L464$#T6r8C@UQ(hc8A0KPCil!#܇D;mч=D/ T'2YDADkчGl00  <'T(<@1 HsA\,EըG(w BH*C` k ?CӘFh4 H$4d~b$rɤEL/hlF~쭃z|}蕁ȂP# x POhȐE̾?pp†x úHxɇ_xDa qHQGpԙP= x}dȧtʼ0>z `8aTX T xpd~xDQ): JKx< &6 ǂv/ADoDjD"h}ԸG2h2 3̊xńAK$0j~E6//.@Ą~ƃL@iŹLMALT\[lJtvpAiz~x㴖$b8 8m͆W̠ԍ9uNk9؂P W(.p"? ͉4"Y xN 2O͡' эA648 MyP`ɤ"C, ˡMoQuY{ x4"pЍ|Xy؇)R WLx̃M~U*_KU†#%y6P4[ NrS ,A;a#N!AtrTs\C:=EG̤Tڣ!P QUSES%U0NuAPtoˌAqW! #MԁZʐE Td 0ҦDU]F*Fk@H3hU/ł8s pXYhU Wp׉ uXxiwxu,A(h U Dt4*y@PёH҈1L0̂J//2HJeQ`тHGZXehzhDV*ؙ &yhɄ)`8ӡY0xA Q  `uװ Y۸uZ?$LDXx*`)QҁXW胭 0`P\8 Qi[h֨˕)1?uYͷ% 8EQWMVC ҍX]d\՘G0![4Q;WxW8 ^^%Y3 ތh\0e%'j b3Xeu_(LiReڎS]f`٠Ђh K\& ~ !M" \_] ٍ #!!ljىSF`μZ-݋3D\HgPxs>_\u"E8띑3g@( pLიb;m(P>`#.'%_a%k!DIENeFe݋ Esc8q؊3 g0df:Y؊u Y ڋ9@%*:H5X@۽x upϫY_eL9 SaH` GH7:R[A¨x9(@]MfY8=0fHkxP YgKN;hih0ԋ*ՋN ^pܥ雖V PP}f[$ yP^ddFiP (Rj]]= /R t`10k&zȺ%(8v ]Ѕ>/(O#T󔄯lkdp-܌w0xxW pYl} 0`-hB8H'8fIîmldl>@A2LcjN?~n`pbt `=6Ho* 0SXm0) xP n.xJ\vඵu؆dn xxp 7K> q օ p!Gr=v$s&w.-Goqr.b>s_@2#36s39+9osE3O 鈚@PȊnB 01 *nqhGPJ^hfpoD1 Woh(w'EGij_H45wz1SW`(PR`іˇc(=bX-_xW YJXkz0j nMXUppYx mqj?W8v9J`p,+@u@xJa eUP\u N(|؆qX} ~hqXq'S{ x~}/Wy8\O}ڷ'~`Qaw'7GWgwg4t,h „ 2l!Ĉ'Rh"ƌ7r#ȈF,i$ʔ*Wl%LfҬi&Μ:w'РB-jiG2m)ԨRRj*֬Zr+XԜQ7S@jaײm4۸rҭk.޼z{35q3-lx(Ê3n1Ȓ'L3uk3ТGJMأ̖nMزgK6 6ܹiuڴ.ʗu9ҧSn:ڷs;cz-L׳o=kӯ >~tx~~ 8 x e&a: DL8i8Bƚ!8"{ $"-^E\Sx#9#=#V0j)[MJuQJ9%U P]z%Vn8!ifgYfl9Hp's)'uy}瞁)(Z(*z袉2BԤ`Zz)4bj$)fakRDN0p!AfBɐ&^MTq "\iM'Ԥ@&h:"4E8fRY0,J jL)Pwp&;^Md4%Q_Nl((,,(!Ǜtk+ d 5F ud3iM@x`3AGW(&/:( @J@)r 2L)9Nd6^(0BfF(#' ay"pJ4h(.Ey`:FR EM1\U)' vm4Px9&#\,NXGͧqI(I] zhr @5BOsBQLM@n)L &fnE0nw& ̙ N&D 7&(O*kǚP̉ pV rLU\'uЊ;B (vRJ2kxclh HEzW RDԖ&)jT3Ҥ5w-]:HnuAbK~6-iS־6mS?BLgo>7ӭu~7-yܬ m}WN-7p!;_8#[U2s85~;I#US򕳼99 nr 69s>9Ѓ.:LfnLN?/y#G{.u`c|ЇڎRxa*-F<8AgQ0 mv`U!BIf #Y@?sd/?k}8~`Q x0?,x,.ї,Yğ5`ZJ8DQ?> `D vr PC?_[6hC2\ME 80B\5*+ k@|aP^OS8pEY>P f_D~!~QDdL^?C?8 M838Q< jCrb &b+I:cFf"RcR$N$b@EOAFbOa f%Md+>D4>X+jkp>:ø@+ALAO'@ai`)(h?%$%p&$'C:8'j:`C($j(`î8hlȖ*>~򃜢8'Ŋ8**lB>C~B&T^&m&mm*lÛ>$_΄k:C*;0=@>T)`PĊ_6l`5د.o}-!nj"5|*ƺ3l뒩+ $l.b'nRrSL9J@J~*G, |5|)fO\?H! fʊ!M!DTZ!;%NXM=G1z;L2T;[_wQd6OϨ(fg@ڜvDC[P+ݏOİ3yu;D=v%&m@!M@8?>D}ThVBN75J!V~Bi#(O5&M@CMZ΋|.fd&;"+t8D$x'@c%C]2.D9^w??_CC ԟ?}N=W}ՃƁr}jVUDg@(N`APp@ӥ{1bʼn%TlCA|`\ɋ%Q4IrÃ!a_M mcmxC!lHzoi~ ~ )AT{M=?Am.Jc!EɌ='I%BAB#MM= QbGBwkg!SA5UN Ӫ.[qU׭jQJʅ[ BQ_pz(X3>eI;G=YӪqs"NAa{M22AnSF>QzY8TִXc.Vt9czmʊM &qM`HbWn |Ppt1T09mۛ<,> _rwÔc=i4[)쨰^lyUmwB՝ND?*鄐nʴ*|g )bƸ!8 -sXI/2o*&DzGB @.!>jTm;Aȶo!q az\Sݐ.ڤ;ۯщ)Ę';jl1rg}Oq-UB|KZd iTxd((9  iP! e& 5r ќ ahEe pCGP8 0CРi Ijcy@sB`C^Cq̭HjjkyUu&lj9bE1lR[x! &!x:x[?7Z'sX'6Mj*U ȇf 8tR)S~tG(>Xbi(lC`0>2U#?evMVNH7%gJ S$m _hR,&]NBH+5H S!hh  ;$F5C[LASvdGZSVϚII< 2s JhյnLL6_OtM QĈJnL8=tG-Z`haPee?p VfĎ܄G ?k@GԟuܭQ)O +\T5)+HG; Fyc_T5'EOs|lXi5)z 2^iC>aÉxq |3G0&e3?7OF/G^4by  =hnCcP7*I 'n塎l5̸nl@s!09VX7vdc>z0v&#.ݥ5  @(8MJmzZiT`x1xC;ћGEq g06A qp$ט5N`wԨ5 a 'S;\69Ln| 0֪< qE4 2 CҘr1MmgDA 7ΰ|g2yHoph#ZV/ʦ4Uʚ%޸oDS#8Іݵ1,o2vf9<Ώ$92;#(F89EmmSԦՎuBUe͘qFMLkP#g҅4PqkoxWcOFw2Or+5^pFMx :2U;iɐ5j`F-NbN۞D, P$.∮0A^ځ %|ZnA!!LCDc`2n2 ֎bMb`)J!|Aaa!8g(\,.|&Rpvk^D F&|c3 InĮځ ١ Hg %1!i3~枨2mjah4N!qJ !\LJ;'Fyga 0EʜA h3(JJ0m1Ĩqd@uBLب0G ,6L|b܁ Ƞ  w #ŧ V ɮH ' MNhx_8N!(jB!tŒC;H0A,D,dn)LCIRq l.+xdtBval" +L)LLȒ -@֠ )jxtTͷF!m~f q:b {V<5D{6ǞB;11!mDÌ@,dC#z[׶<Qq3wD ^Af}B& 3%%3!F,|~ Hlc41,䫽|C8Q}.P p"6b|isoև}`(&&"\*!J@4*5BĢ2xR`@Ub6 4L.ey"P32B .iߖXҽ4YN-bl%A 3kl\E's3}Gw#! ރ!cҤH>-)DQR'~!w ! r=%tLE,8a;8ġ\;=45 1RN&Rz=kT(qtkTJMM2Tb q9, 1b: hÿ*ތ*&%>#6 Q,|CH0j!V^j8s^V&7$~ݣ,1AeQ+ Gt-u !n/)xwaayn1''b=x74NCRqv3$x2ӌ65DaBoA^xbucOphŸxcLrX'L(.+f !TuW*_ srW1!X5m q7h3в+$5X#$,k"|İؐǷAJ!W1LT:zaᶊG q'k6@1+ُ O9YQ!,Z=),+'89oE&BkBTx3ykUז)s$y@Ry#!Նf+%!H!'Cޡ+O!Ea[c{E!z@Z@+a,gQtt80"+A!!PzCBODz#:I`/CA;@u!a|">ơ*y:UL,n٥)DC6-DG#ti#Tz a"\* ^J.f&w3bri'b(w(rM59=[W$Tx+Z'%TAz&;AaC;Dw;ir (?;ed'b!~?AY!,X[OU68`@!{+Jd!"?;mNtc%4:WA} d!!!{̀q:B!ACY $E~_7zAW#Ț<b[!*AIqk:ރ\4@LF;>!P>@5!~b Zp]Ir'Ze>ٽU;w7l\Z#Z!0wH>j!$pچBc ͘'R!ʃ -%pEBB݄.qcR-8Do%KfCH u(4By;{ :[ #z 1sC;wg{fy`P]6={Vڻ{'-VkTddշ#K? )БR~ oPIfWP@4΂mOZ=IgigKIl9rIhsih.h!hNJ颐Vin⥜~ j騦jkj kPJkEd2kO`ү4S ʓr,KFKm 0]|f;rL20?옎8#4㨃kc8͎C6ۨs5$UH6>X֜3;J8?o4+ǓM6`?5^< 2HL͐ON'cCC̷xD ON F` oYO%(a'D(=@` .'*%G@V[!+@ }G7Pe$#'X b6AF`"U 4X.`' pf J UG0lO,:{sN0Ak(P)P@1>5h)o8RIJq gܳ t@>@;V@?[Hw;!hE6jMn&KC$Lc6t<HG:hNS9@c$`D>P4X !޳ }h"rp@y 8Ϯ8 C Gh Vb8&mT"YX7 x):<2oa-#@p#pF`LR$:P*^ft(5$5}3m#lؙBUc 8]`p\C 8 -Z `z*p36 ^LV@(Y3a$E8P&h< B  T !!`LE23 cokg2C 5`+2[ 9I p `  ɉ EZ G ]&` i Ű 0 6:xSKGq б3° {۩ kU o;P gY k ' qf5k } [`abJ۸IїS+P +E{dZ l˸kLb{"й +Kk狾髾˾  [k{ [п0ܿK=Q~"Y ( L<  ak_++<$L{,,VQ >陖v:Íkž1l{q׋İ 2e @a=:8'ALa $Ztax!,8 hp1 ڰp<ɯa0Y0 : `JYmla4& QqKX; 7#J N: a Ey a o4 &Q wp*P+ * 1 'ai1}IAA|P aP`6 RiMѝ\6p1Pn Tp:a kA=|Z QPF4>) Rfo*|Ǒt `lE0`z:4 aZf͜hJv{gG }T%M A  @띟e\W̌pU p-y: eFeɷ aE lpy!rA~ Sϰ 02 ^ηpy 6I~՜N x>K{A ˺{ͮR~n.;.=:k, / P Ѫ%" :0?2#_ 0O #9A 2^ Q!y !pIP~KOYS_yRe_~\soFa%Ep.il(%*/!%o1"{o~_(@ 1?q/ @// ;\__*IR*"# 1%dP(? *2 X0 .DXB%NXE5nG!E$YI)UdKt`O'nШaA. ң@NZUYn+K4TNQr T4 (zu gH]/ӄB5!Acȑ%O\4MIS-ָUȖb[I>) \U(ݠt _ܷ aETp4Z\ߧ  2@#(BեZwŏ9f~ nKf"VHD!,^dDP" ("8r¨, ` 2 d9o *1SqE[tE92O&%y.' /">  `IPP *"ł!*;07 Hn Сr/6A=\+, ` @tPB 5*R9( 0Al: NR x-ƃ#{#H[Ҁ ;G8o$itXPb5XdU(Q1F N BŠN LIAmYN:!/V(5rР& z03U_1VU&0~xb++s'p֠: tU!xrlhN, ?"[}2U;^0H RA> A3ij齨m%Y,5(vm(ccuJfjٓxU7pGGM锏'czXv(. .tZkֹzmG\œA .y &K~xj'7vo9qƩ)YP/(0Z7L,HF~{~&ُ}k%iDYu_Iޗ?FPbFAp#T;"Ԡ}7+It9 P+dBP tFah9@QC$M^XLj BJD0G{"AZD(K!"b Qc$c/rD3zGMX<.sc@;JR@$ G c#YYE#K$ $A:ORa) NRde+ӛAdN? RD !, ``*\(KJHqŊ/jqǎ ? Irɒ(OLr˖0_ʌY͛8sɳϟ@ JѣH*]ʴS 翫Wm{ʵׯ`ÊKV)ֲO݊۷pʝ-݅˷߿ LÈq:*ǐ#KL˘3/$ϠCM馧N^ͺװc˞Mۍ{ #w&?μ _ %]l*Ë9j|S7B)aP5ʢX߀HPy Vh!d<݅GK~c1XJ"tn&!0ƈS#b<-#|M@6PF!Xf喕`)fsihlp)tix|矀*蠄j衈&袌6裐F*餷“.4,B駠< sS"ꪬz,Kҋ+Wk@1PN첃U 5ҍlbTGBw+k٬ dLBl P;7ƲAr O:laH Tж $mծ@Ƙ@3\u77IAb bTبP)N>)A KmF́e$&@(P )pb@LLA `Zuax!٢4#LЭ@ bEqYo2j<,+@YB~ d |??'&(A<9BN"U!Vqʶ"@=yU' 2 c@r,pfX̓z  @-Q X WKtd %qㆡ Dn4hk (,2\!=/ rVA;7^CqPrh>. d`nF.j Ynoo}0]KV(BxhCG!SMkAYs g`#\дOX maNf;n%!DƙMl{6Vo6$BB!f / ηiQ ~3&x(c{!Ʒ©l\yp_ģ NJqS=!V/qG!"9 dC-,0G vg.t' Oe0~W]o@jqη8AQ H7@*0H (AlыQLik9N*! (7*VBhBZ ysRC@!gH 1:B7"Loz{OTX<~Td޻֭C{r)ꈾ Zd_F5_v#*èwC& P+#worCqj1o3'vD qE3$x=1-(0A(u'fwlh`0 NA(vH(@ ѡ<]C]cAA5 (l1,b:QfAHsGbPN!  ۰ Fp'5_sW]WTh ]86~x?(0-8;fx |A2@m 1v"hnD*P$AvHspw lBDԋ|!h [GtK(.cED8& /D/&ꠌ=!ĨC $xDR(3/ր $q|hI`djҎ@aB4B(Ѥ$ w8%@Z:tHbB8@!" D))(c` 3%, pg)Dx`<)KװxKcbd@!˰[nTwr piXD$X[ a  M#BP Q\DqE{ )9o\҂$S  ꀗz%.> K DJ)p l~a? `Hh/Ës 3K &5p %I-H@#l,Nq`=a+ ;IE0yh H9Yk t _ S5\S<ȉ4 rЈ Y%7=_TT` ;&PT Ki#0;81i写8E6Q (nL#vPVzXZ\ڥ[=0J`e낕S?XCp47-K(i< !$ Zal5 cĐ]7HqHpH`FpDwAqJw9qn>t? Eu0Y=᪑ğlxI>G4le@ 62Ϛ~z2I*8d; @$dd@Zhj J<O{d<֡` ~ 9 Z7 dPbvjaAZ-V](ф T}B% *#`/H & f ="r ! }>1PI-0 )8a-ըA<9 B.P8Ea?aE "  q1 P ヴ?.Pl]2(_ 4tk8Qbwf*0<hv !B+U!( j(@ H}AV!G8+'(`Z{~S**S Ul r  1 5+ҫ<A!X y`L@p Q,t@2:F+6!p̚PPv*FE*n n aʐ)0  u=1 '-ѓD =G:!VnK [ ID8 Gy7 p+|dhf<Xr c=݆"a!"a@f b {Mǀadzmff{8ŵQxQl?As*<@ ahʽ(CF`%ʨ{~'|+7! H˝j̋" ̈QK#l͏QдCʈGTѻn0L!yY\φ ͙ЈW0 ?Ш(ڀEгPK=\EYvʽ}Sj\(-*2=#Wm I JӁ8-J;=Ӆԡ4F}C9HJM?`oQmF+8@UXHxyceH`s֋0l}Ќ|y6r9XBL a!s ^8Q _ـ4mgeu@x8! pZwלGP% ,6ny-'N")`ځ  ^P L^a܊ U6! ܽFR PP03[!R}Oe9ج|l L->H:AnsNp 6$o&~*m, 5yoWQ0Џ69Z *@1|(8I7gٍRnxI 6弨6Z/d^Q: ``v0_6!_5I -?^v^ |n `_CxK  P/H B `RY6fS0 Uѹꬮ Y3e ``}L+µ  m3)7FM/` Iz;S~. APQݓ K@4Qz "`0DÛW Mق-_AU?p? '@ o ur6Av O(tM3 l;JɒHO Jpxj A0)87+G)4: pgY!و"ʐt#7 s،,66 7 !r( Pg `Ѳ8 ~1 [! ֢wqx8@ LuVcXh1 GC`װw\ ` 0 54  @/4`|x/6p5 -Ч" шW 3PB >HHD-^ĘQF=~RH%MDRJ-]<0N ,( &Ja-V(MܹFRM>UTU^Śjs9\CPVAR00Ug!NtW\uśWދ~L=ܤ[f ࣓`| C:5XA 'R=ZhҥbA- -f) # n޽}\xGb)k4dn%5Ybl)^svݽ^|I]U;`S^|,kWF[?$@1cxE\*>'B /mZp;1DG$&[hĦ(E_1Fgj1Gwǂ$1H!$"q jdI'.g (2K-L12L1$s% :La$ՄӡkgGp qk3NCO*PJGS^Iҗz(7 GlTRO5QML> rO*W"pq֔Ih`Z `U#g3 jtqFbUH9gV%_~le\su\u} _e `]17_}_U! z_F8z 'b/ء3c?9dG&dOF9eWfe_9fgfo9gwg:h&hF:ifi:jj:kk;l&l&f-ykΊɴmfo `N(g2uF%}Xi1Hi!H䂗D&N)ARUFqZZQ{$gYȮi Ǚ≷ӗtUr S ͂E> r F>'U8(E™R|,  JA;xp@( ,א06o 0b"0H̐oDzAp!*A=kFAQ*^e`C* 8P` )"N ADFV$`(]QD!nQb P1/0`>1Ho!p@бCpIC A p0H1:F|+0VQKvlIv0-)$0s"9,3.28$<~ In+9D#A J2#:BI ?j4?|_cc~RZ2ho!(Hܳ#'`H`8P <t,Iy q-&,W:Ap|_LI RW!pk-e-js$ SMb?Al@@u g_BQ ^P͆$B`ȇBt))RD4# D,Ae FDAؑb56mP䙆~"a֘ů܂bAF)!S"X ]c-e1ZK ,bee7`[09Cjb 5x_0qulZ%9a/hg,` ^\zn""n2]".p[.ȃ2a&Ń`qo`·/ oDQCbS^SbOHYrdPF%H*\,EKjAۇovL jv8 .t 8-<;9Y}t=hBЇFthF7яt%=iJWҗt5iNwӟuE=jRԧFuUjVկue=kZַuuYd$?}acG?1cWDh>m jg l|!hPHV<1r߯ȇ!exc  Yc7xp7x%>qWsixٵOmk47{?\4 7{`rc !АmC7'ȜYnd3݊^΅'y0 0s gd`z.yThWd H;<)v y I<yϮM})|[@wg\#X@F ƣf!wIBM|w&(oI ~!DWvC| o xHpBY}.g/RPMHBi҄3J/HZ=0HTpcǣ6b$ÛBJ_?Ù# 베?4?+v``:myh&0?4 L~X`30  $l @qO2:Ȼg &l0oy9bB=}{m Tt4\sb"C91bc~91 A5x/\0 3yPB<b B@:{)C ;< bB  6l@K:đ374n RYNS?&Oĺh:XFka4FOtE F&qddC g|"B.B(Fo|5L1@w|hйŠBuP:Hĸ`GUǁG$ȃah$Ԓ?> T9U@Ӓ0@-4<%7p8 qx6O6d :MTNT@/ S動܈âKD8ǒXճUKH>#U|PO.ٗeH֬XJ[]4`7Sqք T!.֥a YlJH+}pňcess} ֝)SJdWK,@FsIfu&~CA6Q Zb IIcX\*IІu itW]' yVHό Ui|a[VHͧ@dŌ̊@0 9@4ZJIĊgҼJ <}XHX-ږ!R Z9qOZhLk uT,g#[ 99c\V, ],U]au]h]0ץ]]@iuϝx[_)e Qu^LXM  )Pu5ЩdY(]__kk[eߍ_߅[)'`_[\q1b.( x @V׳$X ^X" Uk"0HH_^_]ԩIOS\6 5NZGV@2LPkZ(N)8͒Uay[Ȇ&c8`bW{cQCFE4dEdHdGmj^LdP&cdZ஄cR_@g8Tf?.]@Y&Fe(]69-e19c%c6[H ]g9>ql8ލ@5  FɄjf< 9p$qЂ܈!2 *p!B}Ї=g]d U@A@#шY,C- }VmNh]#;z^SZ6@C"rx0^Xfӆa^[Z`: Xk >Pq~(H %Xw MW &_> 'xBȹEk@8GrYkHC0NpH^k]$x6dЇo'U*ZׄMJ`B @3pl[[93Dd?b&g%_`Up`B(1aCmc# b5 0OCO@)?Jh!iP?WK]ϩû;^9DbJ=3hc=Q ι mxϵ_TwH]a~R76^jlf !'"7#G$Vr[e&G'y,-./01'273G4W5g6w78s7ϑ*/>rYe<󢉕tx?=9u)tCqpH>?F/EJ7UF03PBWѬPQ'R7SGTWUgVwWXYZQ7 *I Hw$OtAvH_gE?v1 rf' =ViˇmI|vr7wuq$օxAVjfS gHpx,?Ʌv|ʉȊx|ƏyF7yCWGwgyjI@Y#[w!X7F׬v%҆g 0Yp7tRcti<Kg vf ag0OCɆpx@%YԬd_aizHЩ' ltv|pYf h-v% l n`y` 7 7q| g%Y G/  8)ȇo}jaa%*+ (po~:'SZU81Ҹ gl%jz)`v'+K,h „ X1YdŌ $P#Ȑ"G,i$ʔ*WT*M&΂r'РB-jb=',dJLWH;ibZ¨EW٭jײm6dԼ.޼zk4-AT %v8q,L0E ͠ТGEy-nyMF5زg^hPV1\P0rSр_4trl7Չ`Y3;_*9 p 0׳o}qXp L%-@A4e191]tBnp"E5,-]0yh@U ` "-t &} 5t+QNPĀ( AJlEEsBe]Q/@1)X]zɓ%_* 1܉y&i$-ࢹd9rKK8>:k9Î~;ķ><|y;3ۼCk}B0@p?>jr>髿>>??n8(< 2,"8 $ 3 K C(6J.`ƽ6![H氇VK (!F<"%2ш>|lxA@/k"-rVhC0QU8#Ө5n|#(9ұe]X*ⱏh )A<$"E2#$rHRdh1b$(BPm<%*SJr|%,c~ %.s.)aJd/He2fHcs&6MOQ!$?~l<7of)y;?Գ^| '@ЅEeAARMCKudC *Ba&%HFZ?qҙҔ)=80Z,#eM*zfT!?!0AB! P ,rGa ]qVPQ0N SUpMg06 h;e(&^ۣD1- # p-͞5 H(ե>;u~8I"5!(oԒChgBӌH0D!+Q A- >+ھWO $ś΀bAqØ)Ʊ-yL:) QCH o+ J\RĽ! QLSx>PM: %@1e!N*chT ^NauP^ETL8 d L8c?!E,>ag!z D(B^G},#XVa}o< ) 0+1h|c3qpw?Csy+X Lid?.#MXD4B H4oTR`6m6x8AqܱO {xT/ E\ x2D^Hj@(-!i5;|a굞2p`uA`:,8x-AUD4ARA@A`} @6x z ^(ƥ= D M|((Ac A.$C$ D-@ҩJXh>EY!SC &ED2(aLOGfG~L$N  HC76䩰#JÏ!cN&eE @JȂ4`9R*VOjM\PFt9pVe Q ;9K12z_<ae]4&AX%c>TD\ _cf`@¬Tfhj]d"I,$aF@jަ&*D6nEdKHC' q 'sV#RdCbsVIYAdD$IXZ'xU d)|@VhoFxQZSgOBCdJ|VHAgBމ#?t= !^>(Mq^[X(~(( @'g_#i`IS`Z' PD*L'Pc%y5dDD Lih 0LC& ET8(P DBQ,$灰2( 5—SL!C_KRՙDyMj4E"!$?ïL@MbjPMѰpŁ%AP+y*O>*$-éQ6zAJX ȃmFel*֐ uR"iy$z*\鱪 c)g<+dBF%atj_\ƶSJ,FN,V^,fn,v~,ȆȎ,ɖvǰD2_ɾT@$ {,κ,4DlDjm : |:o0,Vm0hAC\N_OڶEU=ھĮ.H@A4@8,[d jB>QN>T>3??3@[Ta62pAJJ0 0Ȃr6m1 ,`5GG# 7mA64$^ëc38+jq02CFkDC12D4_tmLTB|0AX2@@8Q k.)@x>B-HĂzDCF|,tigP臉AlJ??ETG$$ @T@A<r@TH!STBB !F8bE1fԸcGA9dIE3We mxuI7qԹgώDu&lUù d%qU{ x 8Qs`غ expϝp lX33$bJY+zA\=c0[˗1gּsgƌQL4ukׯ%kɬ_aT@ALؠazY%ƹN3$@ml`lDkQ*"'s l 0`]YI׿&([Jo"M̰v<78D#<@,Lj >mgKzLCm,hͶa&*@ `(&$2ʠR\hؓh ^#bXڂss"& `,؃dY'\ $*;Jh\aBx@]mQNYe%_l%Zh%h%+e Wf@ _Zj<jE[b9k\ktY%OpEx\Fg[̚pQSib]C:OݢZj;tEl`rٝ=$2C(sb14ȗK򴤼 %"Z`yhojb`z6?-Wf @.0IJ)da1Y0EGA n*XT&"4a-fB#(+^ѩ049>j9D!Z1.b%t@_!A/d_ݏG$ݷWE]-U p8!01!*aa4P02bA$p!,  * `A. JHqŊ/jqǎ ? Irɒ(OLr˖0_ʌIs͚,lxN8{*(ѣF"])ӧ ńJuժXjuï  سhӪ]˶۷pʝKݻx˷߿ LÈ+.jǐ#KLraU3c̹ϠC<*mTSovװc˞M۸sͻ NpWNμУKNkUMͽËOӫ_Ͼ˟O>|QcϿEi߁& 6F)VHa~vᇅZh(~siwW*(4X)<@)DiH&L6PF)T*yP: 8e)"Jb(v F.ta8bmWG_fc \ֵ+-jgK[6]lkނ,v72}ml*&MqK%u]`w x58X rMzW^ծC^qݛn?SVL?^@J\y^bΰ%gց`xSGL⾦([R.~c,Ӹ7q_cm,d?Fqa|d!/O OX}Aq`3g"2f.35mys,g8v^ Lgҋt=jBЧFti?ѯt]jFӑY̖]-cbڔcO;ˍ1 a4 T[^ @]VG{qXW5k3S.;x O8*_Hh=Oz/ֳ^%5T!۾=|oz~?5.F8aH>@|nȆk\es#}sP }FΩg oKR lD:A3` w3ep Z-0 Q Ȱ ~pP =}((  M4@zF' Wa 9 @ 0ha'J`.xCD]F!w#X6 pOPLUU:lq{0BWgD[PpA  .#!` a4 %)A(NVP5kX($('0Nn0Bn@K ^%ا?Ͱ` WghQ`aP 8ڐA$@K aPK5h\XQZ؉ 6@ᆆ p{rVF`AgatT`O Dwp 0pq]p #THÍ1N#(a ?e`b ` Ȃ04p:0`0 Abu;X$K,n!'_w]F}=)I 41PP ɄDuޠa `PS& 0D M 10uTQ!1_Q @ ( :Pi8[ ڰ!~05Q`eqa_21PL >tDa UOS N-yP ѐ K5(OGfb VsD[0 f 8=iѕ rwѠ$n( `X)Ip:' 0P 5E?%` 8pwLyݠ gK AN+r?xTq[ı [*$$k*A+ #+Բ/+6$=47۳yqH *D")sV0NpA kH ?pV :ôO;s ʈL =#dnqk] a`QPy0n  Ju5)5i[_ pLPyqALp@ \Ii~m5 5<>{"g I=@`&,}A[$ r=> DL>D1g-34-Bu g q=BP =xa& I~N=jJ kpXwڀ=2}BD#}ai =<=x-B-D@ッP 0qHܤw-,<J]. zi`<~ [N}J= 8e ֢{3.d>+ @A〤pٓ q0L*1 y< r.mk]}~ p>=]A@h჉Зᰠ˽(;=C0g#˿^>O ^;D1~ kA=K=m^0 &\|ܞ~?=̿l ~m  3_a|{?-ˌ~{,tL:sH`GpQyL 9SOpk\.M+H)~7п@??^G K?F:,po=GSBP.*oVٞe @!S $l{^` ϐ j9N?_n1 @\s"A $S7? p?A+ $PB >QD-^ĘQF=~?y&k#A0iRL£SN=%ZyQWRMPh,1Gf~H †nuVZmݾu8Fc WD ] ^… ݦ"WD krBaca4U;Y` lRHq6{!$@V*c\xp}eG\r#d{"N,81CW \H[\M#>Q 1.?0B '(m B l 30Cmjh gy~dQGD/c4y THh<0s" s x!ƚhJ%hHhqSPX檌Z8~GI~Zk q1+!Ygp ¶\`%=p ˇL ^&̐eܙu!S& Ja/y :1[X܊~8aHPo$cGyLVq%yd&R`v"JZqȓzB+"XD׆PZl 2"%>87`(pi&˰'a쏝`R& '&1c ih`ֈ(<(*I%pmmL9#$p(k| @CrWgB bul(c|E[jq^r\f|R{ŖY`[j=]u98!Z{$H|-`F_6gTd?l85A ]k ^! 0=I3  _Nԡct(` U™ b ´#din@LF؋SSpA ,m" uqAG򱐹*AG&>lݐ_cH18F[aB~xD 0Ɂ ~0#rE2Tv-/ѐmdD^8DB`>0H .DҔ$"UGҕ)t*a pUWҗZ*_Sl@PbD? Ƅf4 Sfpd37Ƅ8m٬H!ѹNPs dg&F_J.@U0u3!>B 0ՠխoV)2/FRʤ2'ekU#mvI$Jc: N*DJՊ^" 4<+R$y5 04j9L6$Dڏt%~np *Cƒ\_ mYr}BWznv{ӨBnA6;&$oz#]rЕz;xv!n|_|j8[귿F06c7bQ]7!썰[36fpV*L~QbCX0Ajb$#<Jc#ffcxZ91̺!C^r<5AF ylL&c[F'& f6_1\"9r偠#nP;X*QzRȇT /[tYӮҌv"`>p!f)`\hxAΡt`gHH$:T1DձFvr\X'*{ \6̲lpQ%[4r@B]n~E !wYH4! 5b F4@GfW\-ſXlnhdN0uMe( xw8|T5)b @9#aiг˅ Cw5yGF8އCAGzALa'wZ G|ugߺj. QGkvwG{ϻh?xGSG|Oyx7>TxW^vO'u/{_EeE@J;>q3Ly׾ercByɣ~!@zu,_ywK3nzGp q`N_-})4Ed+jݯןS08tlgQF?m$Sy蒈btRyй%?@Fi}3PPP iВЃaQP? GIKIn 8?Hf  XpPpR7A XwH<ؒm@`0ouP8Nת䄳0T&ߐ`#"ÁJD2$~P@ֳ џaZT|cH\XhCņDX-p,7XZq@#TLDUt~@C0Y@ XP1:STugH)1igDizsE:l$HK.r:R:6xdHh*HbHā(n,-PCCņd XxXzSx[`PLpDl1 k?pªD'jɭtyxpه~i}8,yP Cq =KiyCT)cʄJdʫ4L L j k@81@Tax5bI`5k bPM$jHDpJiiߜ Z37zùrq,cq >[k0dtb́)8Oa@D :΅8萆@ÁMɄTMJ  i E %*fxF$Uϩ\LxXS U5!,d!NPL[H_x "E5@OxxQ`8_8O_J(aRQ#5hj]y G6Vax̠!3ScPP;3 `w+!*ċb ;ZKN'YT& Nr(@bQ 99~D\5$ 4̐jP^Yb SUZe[g$]B!\`WZVU\qe@ PEo 8g%x # $WbZ}Wq~}{]UkV~׃U'!3:؉؋،؍؎؏ِّ%ْ5ٓEٔmG / nʤwdٚ r,{וٟU}ϫ('E2ڦ}$ /qp.m9"H&!'*\/M*B!$ڶe dϫZR+LLL}ګXY+ '/X~F pW^YZeQe;]n)caa`e_6eNPdff)f[e[nlm."PcngRp&壆N}1UCl*r}ګ͊x>(|0ty>ʢ#Q4L)hz CjMLLΈA.) Ъ$~i0yP@t$@#Lph6%6%nY3^X7'Μv pl8,~pum5g"iQ? x8iN{NWbTMd ̗% H̰i6 ~ eȵ$U-ΈhYX@t@ ahpoX`8^օuܴ9vf~DkMt{@сb`~ZqP(j#L(iT(8ІLQZ^J.|Ѝ,0,V:nqApp[d87̈UX݁MHv4vHGlH)MTX(n "J;Mvo XZHxukވl@݈x 5 ePu |JA%Ip?bhOц1) qȂ .Voq0QЂ6q d`v38E80T UX^N8IF!Rb(IUu%- ̰r:ω.4%Xkn0mBh`@uXB%0^ 8@l+HtV`l\aw4PHxژ!b8^(Z X^rF-)zӇX8Gu DO2vhTM'1ֈ`Щu'?f$0voqO gu@%5a W ؄:5l 3Io?PrTEfvRXw]jMXy`i)j@ER܈a k`p'd@|ɞbZP"_ ^{X_bЅ(oLGNY،[z͈hp ]4ԅv Vk@q (C[\[şDžb D{ &<{v XUp8x93)|C5`gh\\ne` 2T!['Rh"ƌ7r#Ȑ"GZLȂmͪP1 %C^8Ka(-j(ҤJ2m4VCmNNe.\ȵ5-k,ڴ]SVAZ`RKW5jW.l0b "npfì wUYXǚ7sLmc[v-0jϪWn⅌_֥ 'ܹŎ7;A3o gC0Уك g𿈍_c@,q]FL;{ӯB LT%2(L>1>;;pio?:-ME.U,B:@3d8f1J( 7lcNus1#OuYW!vE㘮g >m15;KQ:%sBhpB]<*4<;E3s`3<\7SmB8|Rs}>8,32_Х;Ey?@FbL_f6n!]w-VmL)qlz&0yPmG4996jfF; *>6AMcv!-8 Rʈ] 5٘. mB`';&f 1aC\Ia(#6>ДBžu[bISl)X%cHC`14P$3/ `fcz|N>pHD6FG"U\Άp%GwDc<^ _^%s6gd!)eCxl"?F${`Pd7'&ԠnMCKxF2~PM2D(ꞩ'2bX Q'(zR l!JCk7T2U#51O"԰CӉ O͊)ЀR"r(,ٴ(Ս qi4|*$YP C@s$1  aR b:NqmC҆#"G 袅mj!$ A=]lc@hEFd*WiJI w)7n cl ;qZFSU "z 7#G!2nz #n8{LdHdD&mX#wiaSV!^ hlF g16Sve !Sb $ dk1O&-@QI?XN s@&U!# \cLCX 4X牳UU" A ,h?@4ul!;XB!H-1i}Mz!!כiD"x5qr °P&Qg6mL+[3M0D.5aLRe~rn|axHĄʐIo>xHT0rd vY #(p!r.D6_B*mkӼZZ@s7tZ׺2fDz7PXb(iٱyz|GzUчT4~W#Ġ)Z8ܭ"TBДeD.lJ!$Hp!x3x΃F ǁu(Y$(c@?Dz3xl< @)|nVm]n?z3@ghB>-v3r?!r?!l3}_ l6-&I!C"2",:"C S䟙u.0DHC6Ж-2X2aC|y`M+Q|e-m&61@DGC~'<U? JO =15ڡ56ޔ@(X@6Ș~(RQL46$D0,?dC2`Ȩn<@RX"@DǑ0T=dQ 1ΰ3bķɤT: 6@6\%YLSnТz ZY%ZNuE%\ƥ\e\&$T.*>.*,D[BA :}WҳbCDeXeF8,,_upD&+h@hh$bjJfdIk_b (٠@ip'qq&'rgPDAG4tuVu^'vfs6'Z@wwgusotfYPE 8FV.[}HBDeBf{6l'Gܧ%\hWEE&0h%YbhzEhFgBCT Fq zEDX@Dt5&br,]YiȠ" 2DzX0(bh Xe 8F8z-J\Ih6 C iE1!I0BEpg CdGh<%8`x$@260$jF 5tA>~튂|*,(`X6 BB2Dcl*Uઙ$CozjN-X)cƇ8vC#P-A#$( )܉zBT2&gje'@a?XC&b8ÛvF ½6$~ǸZ˚**KL+8![:d mc||:FV C+R\֎f޾hs\5.t(*lB.sخ8 &/˔n+lgiN/0/8/g@R/ 6^^BzN&ĔZm8 /:KJ8kL,@e*BHoZX;Gp]]%E{uC ) .肟ZGЌ,lRCB;pBQS  -B EABH"SM` S F1@/ *$t,(9t0Rd2F!1C0qQ$B[BXqC1 qȯ2Xx#?ذ60I et(D1#7K'qC8P'Dh!Rh<2,Eб(CEB QC7\@R$ԊFxX*B\(CxCd :D!0 tb}<,10g3F 67@,+/%!7.)O6sl2RoAIE̜O/pas:GI^#4DRhxNtm/G4C;gC8d GG )_FaG_?X4JVTI`G4M;JL+t4Y4ONCPERVȱjR?OS[uB1TV{5E,XFVd\O/B/D\2F'Vܵm@^΃&Sf\? [vC 6a3_Msc\,4TCvdg\Q3EW@gȤhxf7f vkwu|66Guvcl#RlC05A1B~[sT;t̙8 S#a]>탃=ETHG ok[;xu^ċSE>y hP;d00.@4<'/<7?!G$w?HZ|okDgcD_[DW5Sv}iCW|u}~ ~#~[8L1hۊ-[c~ks~{ԋ~5 ΦcJWs67F<;aOW;?g?7Cgw??6ǣD"tCJ}r5aʑN+!@8`&T0AFLDZѠF z\H$!D/aƔ9&Ljtܵ?:hQG&UZOpM[:jUWfպkW_6ZYg͎UXm}\lޕ[T/^}x`w2Yڵoz7ޫI|,'fϡG>:XױgnT{wܽxۋ/^=z|{ҩxu/9 , qIzF )|'*PA 5̐) 1M$U Qd9Q:fyG#LAlH&l(',Ē. %s GtlS9锓52 O MO S/CB.E*rhڳKz381OC)qXt2RKWJYI[dU]c PLS" cMVelba_VkͶRo(q ܗ%\uauEWxm^{ews qxNm\(~7{ux_9Xc kp&pyC[iuay>=Do^4FwH%MZ_BdmiY4A +g9lֹl:ò?[n[p9Jd\ow,pi1\ͧ9]I/n)YoaWq]fW}/תڋOoKo^z~럏~UZ]ѥ]_g])d@KdAm,@Py?m`32QGLELV FFLN8C<`2c0^rCd2%|T~#Pgrr_>hk\>" ` xyf,<i|#0YdQ>Ƒ7h+$fg|e "Xp 2U @2Il12AK!c/ن'Yh$q0*q2Np&D7XzR-)CAӈ`2xI$5ĤW7RN0;ũUC&KgpƤLehMafc&x,k cJk'<%h1G;mxC Hd1dx <đ̠X#@Y3T( ~2˙&h~RR4(+U&a"bJx+`"Sx hh x({#fRs@ A X  T醎,t5,VH %>"SØ0<`&/^ (@PS4/CeT$Nj5F*QP8& QS&ا:#ŚCM^b@AjQ*d-L: 0H)(!9X[ְZ`aRJ8U9%T@$ tMk$a7,iɄ xT.q@-\@Ѯ] 8.p1v}שu<_Lubo='xO_|hl$Qzկwa{Ϟ/` z^v|7|/_!wop}/o0 ş~+x^?th{/pPCPt~-105p9=A0Ep'ptJ|TbY]a"RPs"gqp{Nr}pw/~v0~` g"! n* " 0 +Ȱ !p e &% հ  P=e b)1}dBa8 -G&Ae.AqG!+BqY@ baauq`a2(ء`q1  ڀ1"͑& ` +Ho!@ }H(B(Q Aj!!a*!$- #b!P`R < HrQ<( %%c(P2 MRA@ b.ώR)(q&yg(,q).'D'#rn&&Zb ]A*`^F0$y*Fa2)2-s3&"3!3A34AS3/+( RLjb+.k.(qa* 3 b.&^L@ZUbI%s*89: !D$!" OAN'3LDP%(=3>m(&4>$?Ih3^hA5;I,E@!&ƒ'@4+(25A 6iB6 6_u!ZC@ k}&)drab܁o4a#zG}4H{vx'_ cIG8R(hs/MBKGI8LQyT&<+6Z , 1 3V!-(BAN%@!:a'>!&?!R?RF4&t+4DbA  Ϋ R"1Ea1PD!RCa1tG^TRӬW]@.&2U+TP!:" @ 3(!~%p%x"A \S]GQ?X_Xۦ6 aP \!p'2\AuBc'+Q"@蕕4(%gAn xL\/v&W[Qw"&f_:2^bSJEfL A\Ww(bfxaig76MjS ~!  K`Rg]5jxqn k6rkkM! @JhAnn!L9v)nXK/a R!D&{  \uav V  ( +(6%` -sA!nLyOcSWzBrzwza[ƀ {(|( w!*R-7|zHia~{H1bSܶ&a0|B 0A#3H!:}p~!pcb)+8!Bg!+u.8w(s\i6&AaG"? & WB&($!xa\R^gR}Gix⮚ط|B^]GwA{ǀuE$ U8*,@-I&`x_u`ui !G%^B#K-y&j&I䁉cc v!^b4Nx8Abgru_bÂK9܎Crq~brabziyI1_"Űf|لe J|9&N2x.&&P2d ׹mw 2DϞyj c"a>8d\!:O e&pY&0Z)`4[So5u)x@ @%i+&[%*b:nZ(~@`{x&\Bn&B!e 0)J}!bIAUe8ͺ:3z񶨃bd`|b* d4Tt5 C\&dv'q4tv=&J%v ng!&&% |,9% &va>_v]Yvn@'1rѕr%P4έ!p<&:YUh!!B;r\'*g26ەd +nJƄ}:@tj {?4@W-c6!}?j:^h裘bU3G b af^CcEAbb!(!]q߲x'!+nH?4V'tq?_)_ <0… :|1ĉ Bx-@bI'_ő$J6Й*aFȒ4kڼIRNp8{ ϧСD+9PfѥL:} 5ԩT5kŌ<(PC`&KTصNC V"R(ڍڻ| 8pxfmic.0J"a6:'П˛oxۻ?Kz}`H`` .`>aNHa^ana~b"Xu$b*"D&:Hc6ވ㇨=g_dBIdC OE@jqTdRNI PIZne^M:$Z<ejɦté&, B $A~ hqJ y3Pt@!@$ 42pp`ls~ j4@lc-(:@4IB5 kd4j *р@4 =!@I@x8~e,=3LL"䪶!:eA/p*@o ApDCpP PM@4 3Ad6@{("UB d )1Όupd(BT9]WM07$\uc/a@A3$slCYlc!@L2D-I> 3H5OXߞI 9js 3Y᠎4I۰LdN=3BW1$Ԍ#M}#^{ ɳ: !Sᾤ~ '95O8 3$2? 228y$ q6Ap {0h hbo@dSQ7Ld{= 6JG4L{8,▪Cu(у^,XЌC8^rb sF2qz\)qĊwg(n@|"gHBJrLY<D.~'?Yqn#T&,3[DIErR\-o{d#˅J\ s FKSl3R|5dڰf*R&3Ic|0'0ɩ!3&ԑ ý!,g0 P6^fM?π*t8Om\Em@42xѐg}3zԜ KC 4m "ȗ;#B:sبʋ֢>ꎥ;myz6Ǜ:u\%R Ȝvud!ƲujE ׸L\Wy_ U-,|:O*v lJ7md/8gRϊvqΒveivkG:P@v OB!@noܒ$G@Jwv,wx`ҺV流ǻT7L}w x.+x np{Kx/ kx? x$.Ox,n_ x4ox< yD.$jLdgPc烍gX9-HcOȺ8CR2}Zgt9#uMqj91@!Yƞ2j7H@YKZ4M!bX@4> J5A K%3Дj D42q0dP Q@ #ՒE"i4H*fP ba%,IsK> 1Bb `W N,2u$d ,ʃ`ϱm;Yp#[ Š0Cz %+A"̈́fd#0qo;чl"e[H8'd@ ,AF{tp$[8HC@ !ܸJF P1PHd.>ڀF$ !`I &tG4aD Ha ذp쉉)VJi9:! U_ bPHkfH!MK#[(4BAEp8@ fPeH CH1"Ҫ;)C L 0dIx"kl!W$P'rYtZ8#Sʕ'a ~et@  0 vS iƠ8r |P b ~#Xy a`~Vv @ p i4x}A ( Fx~p  P2 n' % -HH;L2xz@xЃA   TXp &W0 o &H pz1Q kh {p QK ` ` @ .`5 ;v  ]H|` x~q 6x0 ` ?cdx`D؉LsR X( aza8I8O Nq( ` {MڸDSؘEihQH$J ɐ  )Ii"0 \$ia$--2Da%& =>)5Y2$9) cSeMI NeSISǃYڐ ]sfgiaU-n cu 0uIw]R} P6fmlhiz,6 ptAc΀) ЕUڠ PksY[Ț xbg&kWciY 9i 9F V54vI7v) P I 6ty5ѝ i`9.bƜY 0q$h A (ogm`' 62xwcgg0 )Z0 tY9 ZZ76?&EXwr-y@ b'jp6. Y֐]F@FØ/:c@_%Oh2 o+ sb0Sy 4@rZz 's%@| Q Шp(DA|ShM{}-!o`0|9q-bu00r`/}j(Px{  #YKz]GN {%g IiW51P .2  Gq$4{qЧwP Ksa+b6npZ0e1h'N3pUQ0y qP v]x&֦y2}2D`Wia4 k`pc{(w'! 1P1 H X(p` 3v6Q8kv~06 @p1 ;X  + axP>+ n  ib!PM#w{la/Ā|( p @vq{49|(P[>Z,0YrC&D xK-A9Kc+,# [оP?so.b g#f'W{:pu( "&} ahWt_z'1 %7 *MRK8 x 1m xpQ0! o Jb2P . ! ) a | Q PYR6u wRpt%*i ʐ#pu1xaq Q 1 6|;*bx mqa Vo K PH H @ mX*b5Z ;  8 a sh 8( ~ oxP5i   ! a& .p Tx\ |.{0m qmռoX ݨ @  2X/ Vx =vA `ϋ{(8n a"{0ζ0+R 7 p|,!Üv[-c=pUa ̋ pm m O e! ΧײԧՄ {i}`}7!Q = .ٸ M Ʊoܱ o̽0,LCȍ`\ `< `s۾3so= yp{Mo0~mq  || 7x1#M-17E%./0E2~4.4~0N5$3BEG^CB^BIJ QMYW?;jW`Z^FL_Q~AfRynn NsN.$VTg ?B=E~ p脞C賦DXaLQ^>pT]S@$^>BH^J`@Za:b鯾AqV88QHQ Fߎ.'p˗Ȏq4.nyR랞/A2 !_|z0.~+kҒ)S q=$QgpA!6bBpԅ,A@ NWG!AAC4˔Z@&Qʏ2W<`˘8`4k.@<ՃWaŎ%[Yiծe[qΥ[]l༎D &1<6ojp'#ޱ'+=*c ,7|lRGE+B9Z É* k% `⁃ ԁД?ѥO^uٵomD Ƀ2X0<p-‰:CjJʹ j;HٮPI83Jb *q kazO[܃V"w@&P d`H6~dr+KWo`=A!,0 *\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cL_8ɳϟ@ JѣH*hӧPJTgłKjʵׯ`ÊKlSfӪ]˶۷pʝ+R ڍUE߿ LÈ+^"R#KL˘#C̹ϠCMiwO^͚hMߤ_o6>B0իNN:aνD̉+Ej{BMξ=\;T PxXB" E''~7J~='zE(Rl7y'%@sȄ$Jt B * TB̕X MD F#<&܍ }Ex 6#a\F4I$!Z}]ڃ чP#(fBGH#D (Z~rȠD`F$&BزF:"+pHx1HFǦC$ &9}J*rt(ĵH-eFT᫼b96tB&gEЈ!Nfw(b.KFjJ`L j~.pT@.z,.„BI&|p~f^~œ+ ǤU\qP $[ ʂS6:Ӝ#AE3ļ!cxaǚ/z>;f t37ʗ!픺mH%΀L{̗OJzߺk6Ԋ*1 Xl|}w ^E tkAH=*9-A &LaTQGU 8X&hT(o,؁"!H28Q-8)ȃ&#$2(h'h8quhf& wk?6 :tE:CUtyA׆&фHSoDH x6 ewF8Tf~HV؈xeȈL8X舘XH8QȊ芖(HHxp膋7}y' y!}ҷQ| Ǩ"rhȍȋHhȎ(莹(刏稏n| Ԩy yĈV1~{A| DJ4Xɂ!i#ك)+-9Y/y351ɑ77 B9D92[HYJI&ANPt`RYOIVYXQ\y]b Qf9h^)moɕqsiudw){9gIG ԰\ -cfɘ9bٙ;Ay@;!:1&`A_d a 6 b pCѤH9S᠚_y0Qɚ) DO@ƜR~ 8Nd ǵ>';9u9A:zj ڠ* *q Zѡ* :Jj ʡʢ ʄʰI$ d& p:ÙM Tk 6q_1ui xy;ӝAoJ Q r$ 1  slj_`n1 qv=Is!Ӡ UP_X3 k F= =G d A a?͐v 6 J@ z1h#kpCs&C3>g`NRu01u:uZq tdfGaP 1hG EAofp? iUtǭ .@~} t a` 8]; Z@Q f>Jr0  gIr6Nԯ  7P 1 0*K` Ća:Ұ >Q %F<О q`w \ A Pb60 Z P m"Q0 ОLTFh0 T9$6 &YF[(|eф  tNyt tY5 D~U CC@C0 7PQJѺk0D;F@ Q;IoM ͹ < 1a_Q̫DKҋ0  Lӛ\´]A 6˼D0 F<24\6i9R@ >A5 Rx\I+ 頼 Q<fn_3bLm?pζoDŽKl3s\;.0aǎI1\@YW{Ȝ1lR8Vkʪʬʮʰ˲<\i[;BK[Q !ɑ១Y:u6 ؜ڼ¬0) ȥ{ V ty c|=]}=l0C9fxx  "=$]&m ] /xW ! kf:S<Ƅ5@ħ9fEO`0  W Ԕ\ .Vـ yN.c@wt\0PV q veM ׀iPMy5:lX եExFO€ ȅ=-H݉ a9.- um P D`aYţ=ec 5-Z-Ӯ2[զu a 1.j`hT=Y=׀$SI o:`P oް T4E<{@: -)- Q2 1m?e @gލC6@:Pm'xpn6L 9H!Ϫb !W5  p?tem[ȁݼRA aN Yd VrYݦ 39u [dF JgOp/0 }9 \CYP Z;:@6PܮB amraZ*\?.Z$Е f Afڍb{Btwjc%6 Umv 7@:sY0vP = k ɰ PȐ7S 5€f[-N^O E0 aZ)~{e}ڝvI`= |DޞW RaR^ F %Q^F9 qAZRX Y$ p A P&` XeEP `O.͐P ːqA: iRuE|qR5nUTPs=d0m`vC80.ܧD՜nG y@ o3s_z t c\(F/m.O䆚 RѠ\p MPٛC FO=(6+ j !-@` ZU>QD-^Ę1f ,%KH%MDRJz*C2+h!qTPBC*.ȃ-@Ғ=>J%^j"\%`ʌ+.^E FX8Sf pMñ:2gfKfruK׭:I[v/aDk^ԕ{ P?e~hҋD $<[][+W/ݽXnnkݿvFDY}ǟpq1:J?l@D0Ad0z$t7B?1DG$DEEzFo1Gw|E4)F8H#D2I%tGÄ2J(#1@*%b3$,J05 ȸ sN;3O23L4PA%@> RDQG4RF9RM7<)!PPOE5UU{ @UAL_V[o T\ $RSw6XaMIWb3NYeYg5~u6[m#v[٘RV-%\sŨUtQkՅ7^yLw^89]^z^vI]fav$|8c76 b/uj?F9eSlu w\gYckߑgHq裕X`Rc^P `l~>ZX&XLʵ[ڞO&Rb:ok-{6 @V-[^WfFyH~.G2D1=GmtЮek.#]d F_hTe{gwN|=x ;pمuUWqQ_Q~~<xǗT]$z^^Iys m|r rI $8A+Bhw9zsDcƀ 3/6"3iw;a-`Yi1I1X JH=2t'2E?v: ԢTCjc>U "t(̈́4},!T]E"Pt - FՑ `O6' #:54FԺ&k NPLAVD{? rm$㏥5kPAC7g8ivPp ,v$Տk_k\Ljm7⌴`㉀$.?i̵bC%Q֨q++D* ud[t%B>+vd ?nw1 R>$ jrl y|z3 FyH4Ca`L@D݉`#yM-"0GhQr L 2|WcWiL,IhpH<ëE9(v rG pD@q[}\蕈XN"PyfcJ8yzVd'~HcXE>hQ@D|jh,"&ZOBôCq_R->;v[k$"UJxqC0=犅P3SH h3CAEJv-k$RBGbir]S.pU]ToX g_;dꁄ+*l(Y(Zu]`w| Eq#7yQl/1 !h= G@:k\|)-J'|ź˜O$ G7PކP>2!fzTW{ApJq=B;}u{ܿQD0m4"W2h$@?Pv@qGCқAZe3h(=1~xuw?ן3{F'3 AC)Zc0/xpP ݉k@>vJ?$+4t9@ A($2㾁h>yaO3~!@Yeb@;k %г+@#Ѱ%w'yХv0;y,$#"PCAC #BDۆ~DLS=7|؄خEy2 ; ?$R4SŠDĨIE$;}A=DM S e@vE*Q@1v&BBQ?7qe\9|p&F >CB#)bP=s@ iGZW9E(`8)QJ<E5l,zG; #hq@1 QȅmB؅] IZف@Y ! Z=SFHIPixP5(CGI=Dɒɜ(B,kʄ\| 4cpK ?=Ɂ@_e(#_ex![ ^P)SQZ~| Q@J˼@/ˢ؆4DTdɜqpP%9[2x JG(p-`O׬[`ā p^ ;xZ BL_9̌0@T_Nf6* S(x?(X0@O87hNYH@A5ݕc9@v* *R+-.+%ʡ܌\`MHӉuNhӇxӉ(a,I.D> 䔎< I15dNs4 4LEMNOP(ň}Sqf@%(2@+3@%x_tMt^XH k(OZ 3%| .@lTyЕ@ lmnoV*'l%H!2 TD87 JX`yfxXe[}XA`Hy7  UxhmPD{!2'$hEOWp868VxY7u_h ~ LpF(F ASXAv%k3}_!0\įh%Z_(Fx_t?5{[ف0VP>Oч2O=(tF%2sY5EظLbXSXPՁ^Y!e0]U󙅏L8 Ҷ\Zh9X`\"ed,q^ȊpV*AzHTHݚ ͍p\̇Yph$uJ:: 1013I ` & 1 MUPu•+pcၚ f,ƒ 1#&_-gE[vau+EV <՘ qb 5)_uLNT?c`AcmT5֍r1 #0b^;6E7ژY>.pegvH94Ư͙a9=0R8y@hߠNХ\CCGсf6Taڈ0%5ƙ͹@p>ޔ;eȱqk&TJ64Ñ< *ʮtx@Tgkk*fW)6*S)XQ~p rx\i.xDT-ȡȀ@+&;w rF&RBՊ]uXƿИlM؈Lf *Dqo1 vEyF ^ (NfnR*|0>@r# D% ? gEʒf' |iu}`q ~qކ*Ri(s(IXr rsUrHp>ox@;X?27o56p@׈Nv$H>:bpC/BABtn ;K5[HO8[h8POPu`x ?ʖg[O Z[GvuvIw`Gul]wmgm&fgHhpꙕbH=srswpm_o?~}~gx7ف؄l'E8@yk9݇x W- :p@ijh@Dz &4-»w ʰ_48{&w(NJ tF{|q G|5_8?GKwǿ'dɟ{1|& zy(/#ᇈ荇p` gJ׷j}}T bӡB1|yA{r}} l0,-"-},zW;e##Lj+O(|/Bg`"$r7,h „ 2l!Ĉ'Rh"ƌ7r#Ȑ"G^~7p6Ve7=QJy 4B-jԢ8ِkѨRRj*֬Zrͨʠ2 P*MƍSMDuX  _BbugO.l0Ċ3$3JR1:XdEج?$ Y(-YQk7]~./xgo9ҧctPVn-:_U@D ;<T6M>i@L'3\AaHT\8(rO*jD0 @#NP7Ґ<,cWnt RPX!J5(%{l\c*;/ڋ຅  аImrp5<CyѨ@DbA)1@:Y3fU.|39g%M#O17kI5IEIe:+WN2ڌ Z6]#4\?5?C3eVg:n2cESv7[TmOk@62 DxM`Af< +͈V N(@1mS2XO ġ83`Rw #2QMX;lODI!ΐ S5zQď?DyWs(<}CB<1Ș@1J=!pAƱX}a,hL )7# [K|! c n{XA^b|TXAѭ)<p?}i2 ()RA5a1"fa ,+H`5Ȑ Aa!YO H+0N($S (E92, #H0ˠǁ )AZL<#iIV2 lp<D,II$El!aV!Pe2o\fTfAzA, Afa@;1&FNuƋ ",t\fҳvB cB0 x [  ],%Bq \t:5XFэr.R9JYZ["EP~:*ә4x\)"L4"*C*\T5=*RԊ3R*թRU(TխrD0ձfEQϪֵn}+\FqxOv+^ݪ浯~+USNg"t2r|6pRR!*r͙36ђLղAԨhkc++msrE-p+\Pl=.r72ͽPERwΨ.v;\pGuɈW=/v.;"/~+_ A/ 7>0^,#. SثP|/ s5|J嫉sj [lи4pRX>A~,d#dCNXYX^)a @Ph^1wYd32 }sQx:`\5A?p3;g:G#ԣc=S,g}$ m偀ҕhP!5=t wLǴ8rTwH:@fKXlhDM6 lMq6l/ؐaE O`ؼVjdz:-%#H l/Ԙ!2mX gYmވ,!m|Ƿ|B{2y 51 xLҸA0*dS?0!iI 5.eØb2: H߮4JVژ\1icJag)1D-߸' &,byA2 g8"1ۇYbCDrdpY)эe2'/uMvzZ()}ψ5nq[9 "dLb|9$O% R 4 \00س q H W@kXdv( 7iC܁Ƚ[TFX" 8V D @5,YDG -BȇD@\2BМnB-DM @@ AA_AhB@ Yh@9BO@ T6i7PC?x@` \@DAS:tE0@) + 8 (ȱ 4Ж@pB`A@ ` AJ,qX @,A;!BW8+ D ^C;-)D3`|@H!^} ,Zmh2`TD @`L4K%@t\=5d\ʼ͒@<  T@ lRB +.,tz% !8&¢FADB8ʕ0A^0D$#vL@@D4`K8|JGP<#, DЖq/AC2V1lNBdJA@)xF D'!B3H_Xc_2Raj`u ;N)B %E`KDbL`@$-6K=N$ 14UAL2CM(,$ARAL@*5CdP V *<@:@@LA,x%Yp l h 7CGBA-@,!_d@l'@ %(X/(Ăh A@$*”h.ݔj}2LaF@+ $B` BL*: 2B\!H3T@$ dlYD2C@ _*PDH%ef&CHA0"8&@hLDfA/TPB0\73,ŞD<<̓Gpk.0%D2d'/N%邪 qC-Vj2OB@ՊTHꡬjxkjC,lA/.,rá" \G÷x-G*F-.(ON:XijCME-Ġ{B@Vlώςm0yܞˊ$FdV0En0͂շ$n)%Zڂ΂TBq1|SD6.hɨ.D-a CV,J1Ԃ/J --ERdCA/B-GXE /Z@UZNV+F0p},R z.t0l ;3 C 0 0  L 0011'/17?1GO1W_1go1w1VjM`Kjp[;DZ7q#>D4rrHC( #r#wzq$p2<\% Izlr29De%z)%'!*2, '|2t[.6+یtEm>2u2 c/r M@hA^,33A`}Iz8NJsx+tx@s~)@D <HN=sW@ƨoqTAܥC83Bc768G?Lt~ Q,o4HR?dU4A[AA؍J@@t_I?6(6H$B @@Ok+>:OA*b!_(8xMzKw]5=#_D;(B\|}EFtH0[+14[luV(Ll8Lt@u1jȃKs4(vDd5hs9̶eǶ[HøtE vsH>C>, AkrLlCCwiadEgPI7sq]\7s̒tB8s#DO'DvqwKRO΀gMF\W|sXIvRDW isKtjϴe B3w5O|6N?psH(8$WXÊK>LcE'ەGD@Fo>Yw5wDC CݬwF(K(E ap8!=|  uE@IiʓcB?1yC#/jwrFAU76T ?(S4; A AGXȐ rSXE>XCT8ʦgIIJMe3Wz':]?ās?IW4L^)@AW^DBo?~wx0:ve@48B:eA+{D{4_o{!mC|Z\K G DgדI8ːs$`§RD(|'إphAN?@ /2}wA_9D;D-gQ$&|0LUXH\C?S_iJ^F,@<Kg41|K?M $@?}ѻph&dn &B7l3V@(Cm),<|>ChB 4Q_=Qt~R<_MڧDˏ&fTBfa(G>a] |Zq,UN:5Zoƕ;n]]vdȐ5QIdޕmKl:3ڽoܶ|*1dϟAT ڟчckȩM vm۷q Q@ ?~CMjss~C..92ȝ\%@.iD7o-_n~׏_~ / N"$&B1 6jtB QH.)vPD1v RLČmqpyQ֒jmL(9"Ϣk=QJ Ż §@iJ//,34@ʗ;RGPe#&-?T6?ա1uG!Tj# y(Q٢OIQm:&xgYiu\i  HV(@O ,QRV2ҁ @p ո:DUԥow3B c 2>'p ] Bƚx~-vqG 8d5Qg1Nލ9)heUP"Z|FQHجg":8iʭDߊhɪ QR秡뚖:)z92W]BHτa 椦QfO.M*;ۇ ; ƽo4j[ yyf ǩz p \s< Iy|h\>X^y(Ņi_hyb vɷ-H/ie:۠y;^]6{ ҮvGR\+R3^7 Yd75aV0y EdC]NWե.u܊p)aw]wJn#żyqMc~ J0,b7s#&A=ʗ v 9aH ob͇CX0Qb8"@ځAFV¦Q҈ϙB Bn}vY'i<ۅEI(rx`>\Uj$sJc'm KxIBiy ?xӳOVra7 $ 7`Cׅ@d̰ӊV iy HEM;6N895RB1^di$-!0=6͢[ iL;K "[Ip@sse\CBlԶD=f\7dTg{`QdE2Jr^u:l,^a '3کF< ՍW~sG V*`R=5H (8xQ'_:7kOq;NUÌy#..#QB1 h֯ 7X|<6/"W8#!+/j@)'liDDEamOn4"k\/C[#di`FxBA!d & VAo4$(;D6,f3iEJPf`r(aF&T" X?² 6fh$`-bέM,-0uLmt'! I0Bi>a!$@!)m7 s$3)px+-.Q P8~ V"!rQ0$v@278:eޡ&cD#m YGq{Q1)BQnRo,-q#P g 1 "A . rc"5ҝ r 1α 7bF$U:1#rp%Q?h&i%1 1|'>n(D'"53@bt,"Ll*#m#A!!m+ږ"Yi  -L2$ Bh*%,.&*2 挒/ .z"B,.dzA^07bV!:#b2^$ M4AarF L4]s dLR aђN2m붌A"b1%!'3Zca"",`!"  tN J F f`O6 " LX8s$3UA LhA b K \!AN n"J Ԫ: B \s <B]=r@>!Ll!Zs)F%%.3:"nEa"nR B"C-# IHP#!Ea.l!1uGu 024R"Xt"a zB=!Bc r V"?T4"vGy@RE>a,"b/!DѲBBqa A B 0k Q qU p,aEB,T#Sz8M}?[FUZAYa"'B!ja#B7 b^G^Lp I U7USG?6M9 ` @8!(:&V` "T  ,$ aW nasv[A?9qKro'+a*Z=:"QcAϞB0#N(~Ja|8aqJv5Jm1h1U8 pg?dBgnv6:0bhBfTo`q | "6NHq l$ĆrDqMsM=atr9GP$!2tisMqaS q5rawGRFuղwV35an[xwku3wxwZv3[Ax|yzr7%ɗGdB :{n7Hӷ?62~d}q״" WTW 8s=,bjo@!4^t1Xnlb3p2݌s([ 3xŦ˔ZmSF Z{MX~SxYnRp!xu8~s#8 ^2®"Ixj"^ rAs|x9VBI8!8 šRO#KXt8iju $2#v"ʖ eIJX|`bq w%a?EUw#` >s"c@%jT*" @ҁѓؔY2\!"~!Z!b$ `! -ldU,o$aLN(YutIy6hK y s m JEjbd ewrY{Vw*繢WbF"Vh(a4fO'~Yը`t 2l  0,bY ]!aZ".ژY٢&0#U--+V4e 5 8̅IZ T V. `5@!X'OUz%ZYmwy X!Ao B>eAe'%" zB%FVBVᙡ9ŽKf,bRmbY@5 30(zJ+` ((\8wQ3R  da "BFV  BeAmO"U{ڇ;%*! DZ"` d3 |nV : YYDQb:@ fR%d8Z@]"@U`DN(#dD p#lj b4⟍xvUKmIE†g(S|fZŹ"د3IE*w-<" D"ʅ#Ic:K{Y[#t| :fCZf2&")YkLxOd #= b+}η0]yd'?C} }V"3L|8 [}Eֹ\7hn+w}{]!8d#٧j=9ڽ}]"]r+}}ާ}ާޏ]AjA~ʧ b,*[Vk9,Ae,A"H?9U~YIa^[Ua~U B!pR=i#c>sI^CQ~u꧞"^`;~%y"8Ⱦ>~پ>~>G륾~ !"=>."6_,ޞ?G_۞OW_>#< ~ ,"H Juy}?;?C?*O-ZX+)R+ɿ?ٿ?_!_" R;0B \É"F"D 5.X#CE&$YAURcY%L׮ LgT{ 3СD=.!0TZ5֭\z 6رd˚=6ڵ`a 7ܹtڽ7޽| 8 ~ 5(8jג:~ 9NlX4S :ѤK>=ZլYl شgӆmkq%$ S;]e>{ \>˛?~pM7.h Nd#7@b}ag|7    zaFGa~6]C]a~b{L$H"I:RXn1H59ȑ="F ]mdN> %$WQwuII%L\~J`vҘzi&hYӚl&f9&`XQɧQJdJhO'~AdE(Gb4)ENtC6iAihtj kJk}dkR*d- l!b,&ˬ@l*KmJlZ-jm.;rn.ꢻ.v-l;okK:l *ÐL_qoq r"Lr&r*r#(8 |z)3=;Ay|IHpAA,ROMut t~5K5][5ak:6e{z6ֿVͶN wrMwvߍ$6!DAD@CT4kx/xN8dT6]  Xx袏Nz馟NQWzN{:9G5,O|/Y9?}Oo{o}/Γ6柏~{(*NszOoQRô6${,`iQ**p IJ@?( | ,aAy΄,liD+Ѕ i>GUȦ6B,?:!F1!Ck< ih::{xgXGm<#{iP {2(lhOQH#ρq~Tiτ*GP{|Y`#$+X Q 4#K$:+QRFAAE4)$dk|r"f\@pƣJFL b|c2*'"=h $UMS .5Lx褞J*OdԈV2u fxU'C`#n:mh# P%cXd!pK,K]R֘F2 [T ꘟ8HB;( `Ȃ D=!"ҀC*$  t4t` )¤ y@Z-J1AAS`BEjGy]h=UwpEkc@1ӂ-L`J6#:rTЁb ABT` !\}@ @@ 4D aC~[9AqBLؠ܂"eIR2, E 0G("e8NB: LF͆;i4C F B !X4D5>BSD@[8&!}p"H$ZQ:pB 6-$q3"C)l6$,>+|IY7(s-'RA.̜ cк!HA3oxQ Y k9$50 ʐ' P Gy&IU8A}oJӰRzqLnBQn Дc ٚ Mi1 ƀykY  7 KA ƐJ=N q5$2*NyؤH@a Y,Q q Y7ss8g (98C8" BSfu 0 Ii Fi Мk@ He hKX fZ F:Vi KI *JjʫꫲVYd49KU*GpHZ\*Z4_y0 e٦7ǕZ2X9 S*3JY I @ 6M ]0`Cx$a Ia VIOI1ު %){DGyK%%۪[+˲% E ۮCcڲѲ;˳=? A+CKEkFк't~* QK` R+aY ]۵T{XUK^kgCCT %< pڶyk(<*v +Kk˸븏 +($:@yPz빅·+Kk˺뺯 +kK7뻿뻼+;5`Njɫ˼ +˼k ,˽ +Kk ث˾ +K 8V8yڛkl; L{1E8L L%l7D>%0&,38SPD;mE0 4E Kĺ@ N`E|{MY+ڀ 0e %Al$ 9 [& ؠSfy,"iL&ƑC  P 7ǍЁLɗlə0Q < QP ȱ]dqb0q   \oTn&c^ L<`0h@]@REƀ `4 b@ !E #̮ `0ɷdm`  P=`qE"T4P P ꠽Z@ e zϢ;R7@-A{BKԋfSMUՉ@J#AIR$p /;2 fPWK;  ~M؄ Q,`$0*ҥP3* :u/ KsMS` (E؎3HK*! !]olټ5\I!Ctt` xxqp|mVىTT 9HΚ8^4pgMLDdF\ TM ,m1޲ M9э  ϸ AN as =Г=ɿ%056,STI]0P0 |Q඲ "˻ nA ϯ p `` ` 00*n?=}| ]\+ Nƭ(>M{R^e#Ϥ `z 28 *Mє  5 ζP@aSgj  {+SD저hh>NIA"&ɾNGxZאҤp @  }rlfyN?RA*Jr.i#-$ p*H*P*lk+Lh, ֨ H &  롰&. ̭@9m^o  (`N0 N`=A'-H=S l/: ,W$ kLu } 0*p1<UE@ pLDO>*/ p } `@ 7d!c*l NoXa0nO`oDAY#8^! 0 `QM xt woɟ3o+R#(0A .dC%&t{v0gChMD)UdK1e΄Ipe&-kNH?d`Wx-FS2x d2`’I3Yiծe[4<87!ݸy#Vqg㒍&Maĉ/i!N<  .2Z O3E8h !2q2t*o'.wqW;o޾}˃`Gvx `=Eŏ_!=Q@|N~ϗ/}Aŕ*򠓄9)1q>HkʳVl-'D/"4435󚧶gki 0柩O@kQ{C%qxG$ B*pl@CuTRi['SbgvVmUWW UsآR7iGWTmTbEkЄ &3B;ꬑR!*fI'؄0\tu(}.pŃ lGώ2 jΒ ~Ne̕fdUa'B!eMt$hIhJG(٨GR9 ^I]nxfC݆ś|sj=VApSd cYfrj%!(dm!Ɵx!7 \v&dnY)P}QsL|b P,_)GBH1QD:ekA׺j*@\Zx=$~[0™jUX;ht! lYn%si)AZe-C85hA|ҕ\dC9 ÃPc G8k$$IG5Աe#|%Y?$C#K/uVA hCm 6UR4A/dX9 @QH$)6`'thUf(sP6%ֳ'x4+/2EI GBiHhSGeN1F ' wU| 9%آ8޹89IWάQq# vf9#K/\;B|P/!qd;f ؐ[?,#B)8@2x ܨD- F{[~YhlW)2a*OjlL)9"DJz}p2f1QttS%KʿVCrsTˑґx싒La,N8U-_(\!\b2"msCЗJlQt %X`B92s$ `{k[]"(|Ku&d.'kWA'epr }HJE \݉Zw!q0؆7ȃ=ƈ fq~:b.v"NED[H5?%Ogm<){-nT8e6 $M泅.!ăXDyS<jh!\mhPBK$ p= ųO3Vs1APW%Y}J /aU.v OY%0X!\Y|thY| @ꃁ@XJ 0 !78   =ZSb;?(0y@t!ǘMh% k9Y"IC10 2h r}H!( w[ 닣̒A0 ܸK(ȶ[ c[#PP0;Kp*+qN#S,陔{WD[EDq " MPOOOO2贊Ḣ ~ Q) LIZ7:AYȱdRJ] %YuC !`To]ЅTp׃T~3ȹ<})d8Өu=,V4ȢhUP Z*yPJ} ,d JJd؆PG::adRݲVD S*=}y[m zjșyMpΫ֜ AՌYni0 R[+:[ 2(M2ۺ͉ƅ\M\uMEō\]ɽ\̝%\ϥ=ϥ[ÏqpY]#u֍H1ݘU ]ݭz}hڍ GrExS0yxp4 ]]ޭެ2:Y8*zHí6RꨇD}_Y{]˅_$(UʮpHT`HizmYM`^F^;o8[FAs$]$cRgd(6>Sf bD>kӺ`h&F'Hk(0clmI@`@?ne,@σX+V8$Be;C)yȇVpH&_On PH&*)fe\\`ܯ, VFF5g@cX%(VD>Bd`@@xi#'Uo%gsFg>KH.TX}]Jc^cXلpC X<轵sVt ƒpH@xY@C*X@.=m3%Ifijچ|*`h)=]ZlhCJ:#`eUșl0~Jiћ.fEPY+.Mmm7rCL~z+hYԎw Qq*zцwC`H3Vf Ȩ!Fn-n=nfn.R}x% @ΑZnhl.aje VFRr؃:V$ay*(p>@H8N +P(8nO;ER/ue_H[~(^?xivDOgbײ*1eh@?pj=>^D8YHux8ږx*WIe膆d_vMPg[r}w7rT8}v0l0=n>>7Nwt ixcYv3!vϐrP?vvy"/ʇ%pa izNxkPyPas?z|-fPnaF@Iox5x:^B(ІB5|}npdJ`Pz}u|Gvd8;6G\c` @m Z`)B( @ dH`("ɒ&OLr%˖._Œiڨ> *t(ѢF"Mt)ӦN _`y U?V7ubz] [y2M[w FG2^yf @7MC MuS2Ξ?-z4Ң Nb"q OnNtQ^y.q.}D7sI};޿/>)tbSwpXV-s6"ee`fBRX&#N[cO?#>Q?"Tv?ZAv;2 v<CY.͌u}(QX"C'Bb,FL2T8wISkhMf9 1{٧6*)љ?y砋2ڨBtZbriBtfyzZZ骯 SjiګJzk["JjBF,DRrۭE6뭸[.2ۮ;[.vۯIyD 0 3ܰH lR0]cE('rtdFX$2Ӭ7㜭4;KECSH4OCuPs[sݵ_c]gkݶos]w6흷JtJ*HJ*b8Cv/ D@K/袋呃ТA,'/޺_#^㞻 _#3߼CS_cs߽߃_磟 ?h"G:o8/$hӗd~c`d\,A$xkXP1 `idp8#TcH-rkArŲ—$p *# ɆQ \l? 5 !3: `i>-iL5D\cĸ _%@5nД1bnq1} `Y 1n 3/d "M2P[F1dB&xӌC2Y+"< \1 &{)'N"CPFD)JiZD\qCpAD`5IB?sk<"HI!RHNd@F>BEpN Iv0Xj +f13:*0Ḟ`, {BƝdlY7DM @F5j #8H+#t"`HO t0$LcJƠFJDgl A [$:eH)b82%⇈"KA,DdFKRÃ`"K`Ba y&&-q'٫ k["Y# ,^@;,z!WFgK*nT~ p -Ѿ#E/l!X(08_¸)Вq /A~1 k4ļ-ZŀK\6Hq܉L~ПS\"(/ //>\pX: k\)#ҋ8[[LBE$U9r1]pȺE.Ȍ |ARwHY"m0eA]{fs˲'b܄92k^r x5|xRޠhnx%9E0K-'H[,E1=iz"#ȟ iˠMDjUf $Cv]Gg͖.8"ծM ams;^7"񞷁 ރw M 8Io2>Ⱦ>A@<wBhc<xdODDzI0EנtDZsŲasY$413BqBo9CaQA ә>Sia4QiHaWn82C6f6B*~JTM: K$vŰ:BS`;ٜ!,+\cG&Xb|BD^;ף`( 5"CA͵4>b̐i_>x7> e?{LAYQ|8$)! d\CJ7We)lБ ⳤX9F6Y0(ZC #Q02<ϑQLyD[D_߁ JHC!%`Y3Û~KPNmDJq乆5XC0<`Z2P B,SKp`C1cT 1QO6,^3RDAABIYD9~_I!ChhAЀ,X2 O;DUU4VrQL7E4t$U,C]x-æMDDU-'B-,t_4DC6,20DXmO A٩֐%l*z/ )j#R_̢O3QEKVDC.2ED70D l42A(I,0N]c+-A,0 H1$!w(])D$@c JDi!AxxQ[)\ DRIlZ cDT@0DT] $dDA0*8ATO|KXIJC/`,]"0Dl*K40g('$BPScALD\@=H,-\4A,F-"CR@^DAA@TV6A@D#*6 A$ADD/4"UDBE@E2D9:f?B%"0!$YEZ3@c(HBhVEY9B`΂7Ƅ1V(D')DT-T)te@ZeDOTmEt l7urB3@IF тo*A,>j,:PA BY ܂!mN '4DE B$.ٙZ)!lDWb*A\A&C4:BT]At hpBĶ'0C@P^-PIBZ$r7H,܂.HC-4$!0|eZBCn,J(CBW.,LQ2 #,,[i&yBjh,p@E\((*PxB)0ľU+Îw1nBuFS9Eɂ+@`R'!D "h-iBٕ)dYNAd-D$,CH2BFxDgud)&)D1 $'A< 圬>@O D, @%ĨQԁ/ND3gDD##2B(6kIB[F3q/%A@4D<LD %층(CAL$BA,AS(HrPAFKv8@D-}0A%uY>> qBe&$;}A1pD4-jA2,Cԁ926RrDYβI( XAL>a12,qZ=\#D /式/.DB8;b/TP^AD.-QI-qUdu/0BY3oJJDŽvEOs%MYޙ/P1X5KG.BA+U-.h1-La->UQ3amX@2LqN-y./|aDRO.,LB3xA#E#I5-8,6Տ?_Sδ-uR-Xu4idOvZK@dPPZL/ A8zY/ԂDK-D4AuQ1մ(G3K^X=s6F#-B-DwJ,0B†}n]gDXhQDyCdF^D.\hv33JQgDxdITu[oa-0xQcvLt7PTrDLxTYD{T˖~7t#wB)ේ*Ω)wzK\gK8wأIU++A=‘+Ԃ“G’\4oyybvATA)rLJy9tlP:,ܞ :lȡPxp A~`JDTĥ[z:jG:yKl@CDOĬk:I`z qZ$:uzD z:{{gL B LƱCDɴDŽ[;C;CxzI@;A4 KӚi <~z[ĹC ൉dzĬ{8;N|,Ɍ,, $C[Hq$C|Fh@[J>?AD~I($D,t3&ҡ,=Cȴh|pi@Y,d!XH}N81 5@D8Dl覸2~8;K> K[D|bH#AgPD$47BO&a;>d9h?Pe2B 4Dix0@"zCyp HU_@{xeJ+dȠ!C ,iaI3g S˖>Ό l)px.B!0VVyTMc֮BYkٮ7m\sֵ{o^{Wn:IL@=#!\3-+6Ź S| b8&7&J EFxwρ0;@hŊxIk8I%r~}ld۹w|xs@@O^87" <6IJ<A >hmJbor) Tx#>x%tH"viCRpYj laYr HHX1+&|( '*SfP9HB4!o27iRmB"Ok6lC>h1(?ةb,+C5 )5ݔN=PLI~X;H (9hՃ%AID(yh|!!H + PC- F 2PV kږb O.؇s 2 tq:Q:|ݗ~&(UՇf~V IƚxjUx1(eWdџ0%W:Q;4 WV஥EVg:=eW,2uY5,_ghbiꮽWCu}GIxh V`XlRUe p1jm;S~r9\=tN!#g8~bEcVxu;\urGhO8?䕿+egaWX|~Ce0XpʀnV׃],WOwdYR@| @O,$ֆ?~!a {q)=58@ 08@Ó԰/ y@mL6B#ITbJ-+^Tu'8*`tpaXƾ%OZN(. >0:dx4"HG>$ht (EMm#Wi 6b 쓞 ORoJ (-c VғP$iY(K[K |@5U?T7K dd8LKg>s<!,  *4h0Ç#JHŋ3jȱǏ CIɓ(S\ɲ˗0K2\Hs͚8o(⿟@c JѣH*]ʴӧPJիXRhTWcԳhӪ]˶۷pʝKSx˷߿ Lᣪ+^̸ǐ#Ko@f1 e0D´ Z06K(E#E3n1YآE0la0;C7/{0. GYC8.H"nS ]b ־&B TsDWj < `61m1G\DE)Z iFLfl7BF~ e @2hm"[  Ńb,ƄLCؐBq.DH0H,P%!Mb.% l`Bj)@6 򌁴c-Dz`*K,P W@z'4ϣ(p pH/ 3/ jD`J!op*?|?99B >3RM03{OOC(a!֦) @ 7#cBAC F%3m D 4n @q 05]a@{8Ar QA0 saCCpa45A9@X "PP4x@S|@UPTpTfb-Y7KyQXS@# iUR4'K 7 S d=1ce(BX84 '\ 4) 6e~sY n@E (X Ј tv_4BPfaA#Cu38393 PwPЉb*!vz'z@?}@ E FeP  V94zoTqs4v$G@p.;0Đ7\1 uS ` ?°8 {C q W:Mx'D0s1u 2LB=   P) C`ٗqScyq90N$H8(D ~cL!@ZĀFU9z\)Nə N#!Y OԘaW) i)H ɚ  y8pCٚ5^9}Qᒝ  G)-ƝIVl"Iy?ɞyMG9YPzడڡ : :- J0YV /04Z6z8:<ڣ>@B:4' -) daTZVz1vG,IzY. qimʦ)ҥ0Q GpUf:W/v:+*) 0J: euPo 6:Arjok7L|^A VH )1^cl^uJ qꢗuVp85*'0v.Pڧ?\ҐJ_\խɬJ*ʪ!> -خA \/` 5-ܧdYJډG+\90PY6 s=XsS lS?^LU.s0aD" |֯G\v3 5\{'P ;& w =D8MMm O;81 PF8FcL{.2N{*|J  6`[儳^)Pu j.8 ?ۊiEzXҀH P7P7P%s^M:S @`kUaT߀@5 7_v$# -h13`g#[-d`1k3s+{Y R ! eF@F0G7?@ Fk20f AA4L9ý  O;pP¿*ao5U taƕx5hH3%lEK ǬݠqQT,+NX Y%6flc]cTeƎl2$ ckq0 Y`futIď 1zŝ N ;0 cTE7$\c ki?!=?rX!j$ĺ3BjaT&_Ԑ l mQGkD  H0<#=GҥYD}g%pV*Y] `9q h3J-=\![. >"* ZM׹̏ ;.}V v ܮ ?f*=Ԏݮim1я=ڨ]0٩|yمڴ0͒. ͵<ۼ-]/{}-/-~.}}⒕ōܝݩښ .ڍۺ=++ϝ*C܄߫*J Fl9lP(o`q̶߱>b:up Z& _HSON EuQ7FubE7 Q7 ~fAma&%> ,H~ܪc <*F.^Hz2=^.ܳE}n>#d]tl. =ll=_0;iV%_^`@t| Qa_ `Mb >mgꁞ^QžOQ?bG )4nRna (bͮNfG.Uўj1q(u.>a 氁 ~P.r QtP/:,> p@~"O.Z nK<6D5.[DI4O^}PF \<Rwy` J$P^AѸ8. ( e[@gV +@vO@ /{NB\<'<` a!q\s( }Lol #IAj/a Pnx QV 0^kQo`T'H/wR/@ DPB >QDTĘQF=~RH%MDQ^A(P@6ձ QP]J=}".H6m?>UvJŚUV]~q6i+ dV|u :p@8j]… FXbƍF0@o'ǝ7d/"{ 8S@J#V;l?mƝ[nޝf/@{[C/D6.S v^xA^5ϝ9BK.vc7~MӮ0AH0*0Bg+d r&t; joRG>FDLvJfDdF7=d3#7~LL"` CU0$RqƩ1x+~.$S4HZ,AJ7Rlh~v4Б@s "J JA#s 9 !;4SMJFPfmx,U*ꮓM<ǻy5U])!$(HwTbB8b5xeGV<6[C l6l kMj+/Q~6ٚ^#HV{D)* ]|a0Ёȃ!I)^7o%yr=5JVdW7 #je4HyMNjȖ5!ȘeHeKY4"$m@њvr l}:V*&cBn{3@^ ]0 c pF\##k3"D56GHqEFf<%(r1j} La薂Yj)[hC\cxwwdeRtjTGEy*^6}E r{ RŗX`@p@k @bኂ0!27w Y@F AzL#*P$&ŝ \x!Æ6!-XJ8 Bķ 9CXWH Aa 0MGq<CfQP+q!H% П d hA.o><XA`A2itVG v%9vx e, ZbQ'r Dc]Z0`&A<[hpxYI紑\&'}ʝ;Ozh`; /،N@A(0Q! ڻU4" o%A"q>2jdKH7ҏDNU@gJUR 4FN2!99bp{@@~*41E(A  `jQHA({H>耇!+WsO cĻE""!@, dH@7, ef`Y)%*q`!f.Pω`@;5Yvֳm#T<&Ej}r, bbh-lz b \pMUc%%ؼ2E,~BüE/Aл^׽]:!NJKE@N6GYXp&$P&\${A^;<jA;~SplpAF0g4B1$o=`8wVrpp=s+\$xqn,EנIW1A g(VP `܏q@qjK12&6dI!&q#5rTjb+F%h7E 2vX.g;(蔺ץb.Hc0鶷}x˛ s 8A9YVfymo4'OBAZa, q :)^Iӈ@"]b |1lKb% 8#A|7ǹI"GFl{Y@i&ޭ'WW ZV=Ɠ LXI@3t$MFS\w{R?|fx?xX}n{r$ĕ$)a7 }"т![zq@z4k=ˀ« H4pgH1ЇAXV:@?_bz +"~7|T ?[ΎW?yߐq;@`ۍs;?UQڒx𿆀 9spHxj@tTh -0)Rg C7!4Bd0@H*(L;*Bwi@t@B*+C5cb :C"(=†7cCȴ? $8wੁpZ"#<J iM*X GZx.sH0QqЇXXS&6B3Xȉ؉WdTFJZ `ĂІIpy@[q$ݐky(xHQJ3,zĐ/ț{~Ȁȁ$Ȃ4ȃDȄTȅdȆtȇȈȉȜȎHe5ȁH4 LxHI}tIɜ|pEƁ11ɥDmpdIdʩGk 0DAȆiʂȆ J:$ڂH 0a6k+BS˅ȕ<%*۱dʂP0 8x oЁ+<:'0(ɾ$M49,yGȼ(2m IJxiD`CZ pU0i:EZ*(`Jg+M8$ `Mb5Nع4"H3o`΁|Hb>0O4JJ3p՜N,sA=(Hiͽt'Uʩ@Z SX~J u3P,%PшdUk :DL(G3|:> `K2N%ωRx8X<<Z?x PkI=Ã)Ȕti. BUUXȇ|m 4˰?̙ ص3pKymh  qlb33`='Hx/}Љq@\vڱ4PփHXUH}p}AY„8z{ t% 0l?ք!SK׈}Ml̓X,d kS` 5Yh u=Wx?<ٞM(;dh DU2{LY`X (لMOYڰ)2_HO [P ۹U홆t#[(i@TӭȦ`݊`í\m*ZH \_X@ujɅ \խZ\`5ť*ٵݻÅʽ- ֚8q9Ub]絴镴tųSﭳJ3%,E%q^ M__'tt6`p;Fd ~2AFa@f.^FXm~p`ޔpiP'caF '(?FjAg,OD.v/ .b6+`5^XQb!/."~q44`x9  jdximpxe`>6a('8NpRqt4j`e O:kuHsL}p֎QxRy߄Pʆ2ƌ\TDii+dd+ih}=-|=iUEe6k1݅k #Bm[;$`xxx1)9[JmoxZP`:oc E2^ў;Sx+<`px>BN0vuL9p ^%$Zv)k{&<{7 Nu͂duiF~Ih\$UU@fXM.(P^F+7-؂1J*vg.e@P,t$?cV+ 4EQ&WxZi(Ć4}P.HX#ivGOmed?A80L 6DH[ n>3҇v`rW(tXKS<{eDC@ i8XE)=$ܹ5/ZW.(%+0CP^@)õ:=M +sM@_~y0& `I8U3$dTA !`7")O0/bK02D*긣B-~yIR-x$I*$M:$QFiUZi%D$%]z%a9Sy&h:m&q9yGt'}' :(z(*(:(J:)Zz)j)z):*z***:+z+++ ;,{,*,:,;-Z5j-z-F9<{(.;"Dn?/4ZݜBc 4;(P;Ug=X@6 1_<$1ܰg}UvP<{7n> tesu;WLmCg ix 6ӌ* i3sE{F>3@9Bʴm?Fi@/uؽ-Bx)k68n4򀥎갏b?| >~$wZ~ H À#T<5=0eFGt#_1Gjѫ S(yryFÁhȃZC@…z X@6\8DzYF!)N!(>%^%^b(@aKtS>w8C|g.x dC=Tm%ȇDcVF11doBp `PPU G%ІAB@$cB` n!! !. '%$+eEP,SaqSB]:5H&)0` !PW?c3vaUяQZD@B\P̌{`U BTH8{7΁Lڐm쳞ↅLrgWaI a 'S#' h*P`N$Y%&RI ?X `g !(DL]'!uQEӊt h?PLUB` [$UHPs /aXxOkڔJe  -P%Dv/4 T?Zd"Sjx*X-gZ{*ly@kBlP;Tpc(\c#H(D@6Ci) 4Ád Ya-/=e]fEdמfdd/22P]͏)-t@C&" `ˮ"H-ёylHh\4! /a",aziS!YW-6a)|qBx9g~e"}l~ tx౮@͑A WHXʝ)Q%DXY\@r[OCϹޜ `PAZE,@M֠B@JɖDLC  D0@(D1_V0 nK/-BUUL@ aU8#r DU ID@ 1haT(F@a!:oÙ|T! ~GD"0HH#`R X3d@0ȡCAE"*@*S"Bba),J(bfb-b),+*#11&c*R262U#BCa D&$@@5-$QcCA `F< Ą##>`6D%NhATB3U#B] @n:F͢,8%$FBd{`՗ B,➔]n }X`<\dfdF.DyddSC8V1D:Рhġ%R&%:D8ԿCKM ØdM>>jĴ% 'D qS BB(A,e`C1(J T& R&8HI:@ $q&Œ Aaeen nC"Fv%T*TMLYz}a zBQX[&1I6pl6!_N>Id8 LBq}dq- (G ʃP2aniz $J#xB#{yF5p#B,&ȃ}AyW6-p(i$$BC#R?N %BˆI-X@SG" ,Gie)LR.:)RBԎ J?(\Bq+%B[@V1@%#&'L#$D<*@-X^ <Ꞙ[^J㳼A,`!x G(4`z!uh$y5.BAx\ LJ 9ixNV۹&Ħ2yꓢ D@+xT%*KB+[>ҫ'V}B'.2iBP}-2,֓+֫*r ,@Bgf,6,~+ҩpO ,N*~*Rn:,ԬZPZ:l)|C>Pю~*-BiN-MjZ@ )ds-Ulܞiea: FŴlB8&&.`j-}@@pCKa@E8C<+m@Klj D DD&5AD<2D,84*+?h''cɐWC2hͿ?L1܌C9-D.??Tr2&e)L P28W8/lR$<:ӋcZD;ZssЈO>3=4?#to@W!f1AAC#B+CsFF4uF|8MIL/DK4HIkI;TJJt tM󫂽P tu/w-3į2p|L55SFcmsE|hhV[T<}ѠD 50_8R>$6<?HCEÍiŠuZ=Ҽ#E@B_%ă:6D&RSta(1D/e#D+ F=h',BpOj^@2CГ 36vg؋Ս=A*B2ClX2040 e@!vo"(6dv^ M./DR"@8Ḏ^WPhv= T&D+)Bt@lq7rU8?oC7'@w:6~[ 3p<\LfAi#|S!5P ?t<|@̇5ax~o6M$LxZ8s 1C?u@7@#T5$T 5$Dr8B(LK.-C*@BB+7d*+\@ |$rK޹P,5ZDCLU,Ch/CBm2Ń7?>GO>W_>go>w~ >_藾x>ꧾ>뷾>+郾Y350) ۧl>dکBL;wr:GO?W_?gO>W H==#`u?05? TjC|D?׿??~?@8`A&TaC!F8k(8(9dI'QTr%H/aƔ9fM7qԹgO?:(PQQ+iSOF: BbVZv UlYgǦ5ZmᲕvn\w死/_]=%UÇRSqcǏ!GV ᰝDۃX F+ 64/q@\K!M<r BV`4@ 49Yu[+@ /`C"z(P2A$7ޠ/9dF(&j @a ubF<0DI$[,0AQ 0&Cl7ĸ (gLt3IB T,, B* Qô,` O"n!M R )*BH4!@Q7hf d~L;16ybH2e d8AL h<Pa h7AX@ XAvL& ! :B9 ,%2YdN$VXH a>׽ xBX  DF7 BQ@ bJG "x8|#ِG8Lt bJD. r3wJ&HsNN E0A[(r-rQa -] YE5,^袑}oAOf%Z$]+J Dp$$z!`|AjIfr DP"(fvrH",<*[ P@N#zZ eh:tfg#rGn@N"Bza0Ԑ 6K AznI`un bJp %\\`#R@$`! 0& Q!`Pd“"q\.#^##:zOnN_ !SЏ>0@p   ‰0 m0`pQAvAɊbަ%^$SN P!!H!jabD! `x @,NZR̃j.EOeHOg oJpi!ܫ406(v)XLyRtbJPSb&D2(%``[0 ." ret!za@ o )R*pDbdo.2%< Я,*6"! 1ub1p0`(s3k2w 3TI4MCb4Us5Y5]5a36es6i6m6q37us7y7}738s88839s9993:S5Rf::@0;S?R;sdU,4 SeI(b@ `g cd#BRfiD[L^jC"#6UH \a^H^A`aԶGBq 7rc# !T @VW Hw&@b"@Cc@!'7 :fD#"XRkn/`5|jEI"vbdµSCB0<6L 874 0 P@_bS|WS[/ \$x)<;%@ }s DMoCp!ѣ7PA*_T (ƝfNF ·iEZ$\w?$o=΄S'" Y5h Z<"8Z ۽[p ]R*Gs%r+_%ǝwvg(Cu~["Ta!ӷuVA>KV;S"] K}P@= XTC/'$X~%ž+>bY *sr bB?:  H H"[ '6  ĉ L2p 1C׏hʫ] 쯽*i\57:k:9kVmIA`P @<5;V?˯I /(`&VpbZ:~^p7PlV=YqM1C/9#+8p;p\c{Xy1 IP|;@;X @Hy:j:R1QK5{D2#QORJ,֩wM(a#xj3zn<@wxC?@m! bRDb}xJ4 ݟN" Gznc3ĵpӵKxaGw- 0b*A$A0J3ȑC(᭻P@Di@7 9J$xAT^5ۙG6}Ac7 E213`ȂjtWPgzh IW8A+OHI$Qf& 6Cs`Y <$ !Mƒ@Y~v򍁸 [ $5--Ir QdV(P)`hüu : PF׾6ZiC+$6TjdM( ~^dEkcJ B J4$b<Б+"4_2%TO&_C gg&^C(%"يebsܬ4 2K!E4FJAns7 H )G*ۉ|T8YH\  >Є8[ B/ь҉*g-H$-IsQ'x7|wҘt!,P%OJ@PuD\+ 6Ip'Q zt( E[GST<4ָHY!\d22(L1o`6 $&]R('ÒIVu`+1 =#UlCDF]]0Y#I@NN`f;:D F8r AF  D)H*80RC A wb+^T; ) `֮F6=  UZb ,$(r a@+*K1q@PNd-\ʾImAP 1OX!{'`GH,aNBF(x"`_v\.t]B"?J$5I$DjA 2  bb0!=No^ ỷ&ݓ/y )3En*cHÈ@d8/2u- H 20a Ts0TX5 HZd6ə}M $ӹ,5892 D0pkTKFaϳ&cd>BH:fAX/-J7{)w1rlȓ6m["8GfF2%)O~W$Fyy 27 hKD6PWs2g]yЏi94T' \zŮN3^/[?ޫ`!;Fɝx;[ $ӈo!}L{9r)ЗgCSi)i/)993Ә*tVTy:w4ͥ )7e0i9ɛ8A i'ʼnfiĩͩy)i ԩɉ)yiI~D)I T~rE iUL#IoGـHw *98sI*''U *ơR'#+ʢ-/ 1*3J5jbw}`x.s>Q 0 \iͬ[a 䵧= r 1S0: Q `'Py1$b ̨dX'E İ p0a! &~`E k qN ƒ>I0kE\ RF. K!˯oak Qn@ n z`x ~  `U2C%NXE6x3kț~مV-+*AzŴB`C"lV '^ -( @RJ ,+3X+67Psu` 6B,H6-l!9Qf^ aH "4,H XnO%0 @&2"-!?(a`!:! ^{H<(fm~_z?%b)JEwH ` o,B1 * h*N[ KfͮQqdx5Pd!KLss\ CnrlfdAc"c#f|fʂxsy2 >`#=z;pYede "Prl\ 8ԑ2t, "r]mcXzȲƈ4xP&DD(GD1b7 YLX7ְ`i"tBLAQDD4(l$bdFv I^|Z.urj9:"8@%rIDC !M>@f3!Ch}YcEm,`Ҩa@# ?]@1!un ]) $0:&j*:p{=3#$:G  q5<Diҩ\{<`؁a0*lРi# C@ECE @p ڃލZ_'xciVI%C@@.r@9gl~|{ 'Pfg+Bx)/1CRS81S=ˠzhlm p 3F>Q3C@<)3(lsk2,qʃի{ԛкA鲌&8+(G((s)pb8@A3eɇ9A(I =d 3& ͨ? в,B葹Ж胂0WxRxHv0Z8?ޱ ²B>AE,$5dA|=ۯ+olxKTOiІ2R(@Ї@BVTԫX 5VE葝8 XȰ|yU?} }`=[B Æ1 W\Dp eӃKF0ue{eƊ@E/<㇘{~; iGL>FlH|{|0~>Gǂ x I IH\IT. B,ɖII+-3 G@ʇJJ$ʫJJJI KKR"(˲z$K|K,w˂˼KK˸ ͭ  niF+޺=Nb]<  }/f4Q5 4R D2w OM$*%(e Ow zPgpV|osF0FPFJ5  _0V;\pup[D*1 "1D0@-_0 GXJH@Q Ox@U tq ]w6# <}A.0TfX5@Sp@!]>)@j` p'CiHFaH:P_G X'z?p8؍>[8N;!A7lN/ ) a6<ya(x!x@SX?@ً툐&`c w:֘C@v"lG#V,`Fg!0Y&>P4O'?y66ٸ0 =RYFJ/8rs\sѕ] `ɕb96Wf镒S a `oaavyxz|ٗ~9Yy|T`BɔXɘteY@j9YyYiٚК )Iiٚ– ) a`GJqٓ)! fiTל *y?)!tٹy)yPq9! LOtW6 ddb9 qtQT) :HDA9'Jɓ!JV480 y)w!JGנ)!!p Z!Ġo8:9FI^a% R`,r qqނ\y`t@tɍ Q Azu32(u8!m  00xpp (V*|qJ b0a % 9{QuBCX+%ږQ!d2D So` K{U ΐ)SR0 ŀY@'Jŀ2^a O+q0QD tL J(Ԡ%yѤ! L4 lk%{4#͂6 7w8QL; vtpP ;sA FNvoZ $'#JVKz5`!9K Hv(2 0(FIꓴraaIy r &ҳ]cL^# )( !0(+| :'D Ly& :IaKd`k!%A 8">, `PU C/^$ IdMPJ+'pk6,`yF/X,CZf,0 Px ͠43ГPQQJam˿9w6!CX2Y0 % ˍgFeCm =`u).Ót#Б7`\' j4䫘[!l5r B/ LK":홹 :Pŭ!7)b)^N:c)e+6ƖR̹.rLL'ebKOqD@+Ȉ*Ȍ) aō<ɈJɘl)MɊbɐɢ(a |ʁbɨʈUʰ'˴,˗lǵr2˺pn˾XRzа|9\ |",ѱ|#,a"]̜D*yR vcDt K$H}L =-u;c  8:E }udC1+y 9]I }UWY-u_Q؜uGH6 gC@7 :}ͦ /a퐯q 1ivxZDp'h6okD0v9DSaצ֩m'nLR֖xBJDQQC#+'ؔ VGD emN q~,GICo܃4C |&DoۜUe]m5͹Y^0Mu}"5`&YM"@c0㑢t.h}M0!.O '@ꬮj!C. BpяNwО ⏂>NٯKx,.N%!AOmn>c^䔂1Q!a?l@`3;($pclx0|~b M[M&_/GIj0 m,P0+ڤnҰ ȹ gٔ"&'U_a[,Bp)mda "C! pd zU: c //fg1+n)} Fr iO>Sn'oa1 0\]@QaM8y DPB >/' ^ĘQF=~D( .%MDYƍ#L5męSN=}:Ss޼EsN3-F{JSU^jB+Ȥ ReEVZm1NsYC 0,9MҜWpIcL5X50d )crXfΝ=7O1d!1g1ݶmܸY+gm"&h 1ެ(ᐓ2 #dwǞ]ьq5a˦}zz`B\hx,A_t4[%`QH@FI^ Y A$vC( ?1D}kċ-}Ld&c[HDJEh^ `@ j!8(e:1 Ȓ4h ]IHoDH  YĹ2M5dQ=6߄󢨈à@(2\ѠYJJàN:!74RIJà/ RSJi &IM z(Ö|IeԠM1T-GeYg3v bgZs2‹l6^l Z"` Gx nb#+ln9օxd6%/iF8ahٝJTئX* @x%tPT1-$1[e>bpe( 3x;2"e`ɹhF:b.=R&fxաf.`hQO*eX1h,@?Hl<(o(hCr33aj6ڮ ?)b!M 03a шED"&p"̡Q:=2XŜP#4_2NMLøF胍N8H;5 1Ǫ B 8HBҐL ̀HF6ґ$J.HVҒ!ׁmdғ%odQ.~4pҕ6-^}c~~ ;HV%BDfp }%3- R eQ'8f̏0g'hGh"l)0:JLcH_:HdhE''|%ؙS,xG֔]DugDuBRjČ!HAhE1"A RliQD' yh'V2H,lS?LbҀN cP=㌧V'BT4B[UOլBmӬT:W0U QAmZW8,X !Ph )z@Y`%;٢ }AVN @d&uljU0T"!m\q&snqJ( nr{'$I;a*82B\vCCǪу pUz5A|}{0le="#O߁{eߌ״fb U GF<"1Ue C'n7$HF2*7 QxBKWڸǑ<;9O2d,rEHe.w!K @V)rĩٰ[f6ӵT Ќ$ )ed=;5V`7!҈)! %=iɪA.@{ >=ό-J8 hH"qULjA`,,ô0H"L>vslf? 2aCg܁^ ?w!9 1ٳ0DV0;ZQmӠK0$تMšL)`hpK%BFhayC wӸ}?>r3R%GyUr/ye>s7yus?9DNzҕn#,ї>uD=H'#}pl=6>vD:׽ɰSa|ģxFBZy:e؆!{o-iG2sPB-ʽ5zw77y HVu~3B uA&y҃(ah!]r`7(dnw0~z{!A!:!xUBoHPrxmk,uB:"5[A8KU=@<O@g0؍ p>4`H'e 7@U|h``{Aa;[<kt8BAe!%<+B_0"Y Cl @3e@ȄJ)@@R(: IlHtx6&C"C@ &IQ(h-`4h˃wPq1`?9B+ض?DEK)J7'm;"l)@/{:3 A_0Y@b5Н4˾l_ = +>hɃ$%PɎ 4LLIZ(bPg a_5 K-@ՍI:C,|V bXL -iؾY{A8`قx†܈nd͑LȮhv xL*6Pvٶ` Y0Y T2VO5 I Z%aYe8Up0_ƒpЂ X%8%BcV %ish.4 )()eB>88VIahP* u]¤NAX1B0/Zob}H xG8Y8:P4KH( faHZp;2`LhW܄0새۾3- SKsJj2/  -XNC@PIo8ņdxX64X ::Y(IQތHw_+mm:3;4U?8!nU2YDJbBă4 xN VDWhH4FN*8#5.@$_b4H)w, |6*&]E @&Ӓ]B-tGXv m$AhCQ(ĦuJ$j^TY㣝z.@E3œ@ɕnhքO]TD@l(z~n((`S@PJ4'UZRa:z*N!1C*P41A0+w0fo\i`CӳZf)<[.6xbJQ0 l^VBchJ4ׄ#<פRca6 ԒA|1 KPYk̹d}O6e]R9|=H@5$ca.zpiY]i1@SMku1MD6M|\wEqR؜s6J: oҴqwF6v45I7+ADG) hlд~g7F0RG@HxN4O9뭧# ~ A5nE Y<6vNQo'F>DNs@h7s3<@Mha11;n.+ D&4Q9ұ[JZN)A =BT`!+Ȃdmx!TMq\AVa'Bh)"-r$&La+sLHbM#ev䑤@ !2)M8HLQ!d4mS,$e8: F!E6rbTgE5'@)O2i " 2 5vA\._DэĐ IhAiز3@7FNJ`7qCg)Gs 7 ҈7K TdhuF2#-B괪[O "!8΂!f?r m*jյj4d@0EYAf '_(heU\@:q!sXfl_xdnb";x"J8DqЖ= mkԡ)l+mqႶ6iAV(cťQ^#1LW uJtiW!Fx d}.zӫޚ| % koEhYcȢG8x@xލd @v 0ErW/ tZAP_C<n.`>(Җ7:+j/IY̱@!RFV_>4Q+lϽ ɐ³b!@0Sֿ~?/cgUc}0!I>'ϥ?%I^B *NA_DAT_^ j._Q D2h3^ ^G1R B8J?Aĝ &D @>!H\"TD`Ve! Naʃ>̃ITqaaIX W.haY C@hTƹ H12mC [ V"Y(AQ X@Ā XDU|3XAȂ D #DD@T;8`bz)U%^4L- @AC,d@4ACD*/~@ăPdH)S8i6l?i@#$uʐ=+能aIAT&G?Dz<&2'ҠxoU6o@o^@0&?WOp6w0~eNp}p pz08W pwyo!D l Nٰ%1Sh7-qԄ:1CC,am1w111111'1E03L]1!2"'"/2#7#?2$G$O2%W%_$&o2'5TA|r' c)2**2++1)orln9z: k'jሙ `R"6b!/ @ 2;  `(d Ŧf+HY(0{P)^q # K* a-%$ ` I `2ʼnW44(2x">H)V3dA Nf**)K1Ԡ; e *azDGPh ^!a4!!#3LI~IA_Qe蓁A$&XFhq=B,ЄNX"w&R& Ox hN""`dlV .`8刉2 GQ͢Z0c * *!V\H2@AP ]eà#iˁ 5 2A H僉lo6!~#$,*?ȂX1x/p„\1h!S$[,gy ya.jȖ!FYd7Z*fBeB `vq)YB'hajfbzEaQF\ ef]rU^,\~dnJVS&Ufk Tj%^fE^naR XfA1-ZJG^paHs^v; dx|9wxK`Q eB圗A nF1p6T lˈ2|AYtw9LYZ "b^خ ,$_.  1pC \V -QA(@]@d9 C#e"AdFE.`DF0qiv[NxHA . ]/$ZxQWI7\вl674&3̜5Bt|;c22+H-M49Y)㒉 pXz^I#2(q`xU^-LtK!^3;  ęT @2ZĦU(ZCz5:x3d&B ;COJвE9zS-.HAɼt]W;pH9r/VΕ@v*R+Iࢪ}5aX.6Ao XNe1YngAZю5iQZծuka[Ζmq[o\5q\.us]R81]nݍ8[z5t+\#8{*h/Aʗ/jM~. 0P gP8p` o}E Հc鸰~;bئ '?lb T&/ /M&. P0,8od-oS'X,Xe5{+]P@r=cW󟧻@ r:1VV?:hIAC0װ83 #ީQ-Z0`]`GŽT3ǻ@5@( oglU| ivCw^cdAdh6!pv"],c!_{@n0;ٕfҎW+ w8$#ӏc#Cd ě3+t rr > }އq>NncG?$+$P@A#ĝ[@/βc%췿o:AnwpOHv.8C y~#`ǯE(0X@ ox(Q yY'' QFi;di#XPfx밑y [G.W!bx\/13zBX LqD@6+0ш*g4_"t8Qq,m6W AGAab`. #`"@Jc Pi!bJf hP  +g"!`-!Pڮ#2>H6  CxaPV|k" "@ -M %A<"\  p"/B>c[, rx "v`A5 Q!؈ /a<A6"%bP">]1֩!,Lbc!0P#7AA7!W  f T L@j,_МQ7⃲!FKбRVPڬ. 2!P!qLqL2o VAVma#$M#@vv@ n@pD!K2*zDJULr(5C&+b mV 2T Aǰ-+HR)5] 3% R,a+"R N PbNO-7h @ "y2!`r& -An& ,+܉+D 0FRRJ: *"B47&IF 2\`&)+2nRO;ƁP B6a+C2+. :; 6E V`To|!Ĥ$1qF*+a9N:/#D "4Q;;  6!`< bS m lS735!6+b9 b9ABAA  `Qi(b` Ha$=4Py$%FK 3#2FCm42pTGK]k*-" haCTS" ϻo N;b ̒;>{ dtK5G"]`71B J!N5,̝K5˽*LTcRk.MA# q'*PRNs0 t r2@'l$; 5V-bV BQ@ B` O B>4 `5?J3AM"uGǔ dH;DJT H%,7lD3#5٠5RwL'"u!; ѳ$a@jP7bO T#Y`>RvbK "1+)f@Kc6#!4ng%bV(g%kA6#PZm S!,bnt6 fe J$\! !0 Bi1!" wnKg``-tLv! Ze ̒ UrngqndOSL\ Z F[un"s P5d+ 2"1*w'bp*V JA `v Q: q.4bX"wz'\bl Xƺql1FbL2mUVn"`wy B$B b G:Woz_'jM~up2nnax. #6!ֳ |7x!d7"*bj4"Xo*,0$B!2f f|p#u!wF};86 T_WJ|n_3"٘ 9T]V` p"7vC7@Bos |T <\BA<-"馡1$aWRfA##@ tQ,p!G!xvkS 0)'yA{.YD:n!a A:B`:ޘ)1J)A`q,YxB@9"I'"qɉ˹-!x+"褳+֘J!гjp 6}|1uU\u۲Y!svv o*t (ߛab"\@6r¹Eϻ\&Ć{ یv஌ ^D%Nwá+4@3+3e[ d$x1$ƫ``HBO "+@ r[Fόk8&"䡆늱 b\!dAz^%8ܫ`O蚽ùȑ<P@rO<zg[Ƶ6k߈ѽJ@/|Y..ecԵE-ͱ4TC`}7@{&pP  sj]`W!Dű0A|F{}yn~-]5B e(|J 0'wT ; \ Ra R]\\RL+S}cq3b8$)E[CiR$Q ۥʽrDDkL>!#Dkajk39S^`R>gT&}%pK>Ÿ~#a `׾ ~>﷬~YB e3B,`. #;R@~^ c%&\!Xo`E|ፎ3f#܀!~M ">?δ"!e_+"7qB < G06+Z1ƍ;z2ȑ$K<2ʕ, ?Ν<{ 4(FSjiiwqUP3v*4֭\z 6؅/c.3@ͱlۺ} n0 %}msZ8o=8Ō-+!Mi[9s6 <\0e+HiQ<ѱU̴k۾;wEgӮ <`vqN  x a`y SS[ FୋD?$yK^[ EC Mzv>9tC3ijEmagAhۇ&'F8E!0BBP%@>jES08L 1'eRNd7DeZLA H1XPtDB%'[s,DIx Njmh.jшjhR$JTJ'HAЃY$9A_%A* iAP` kYnPOFrJP<_uDCGϭ> -bm΍Enc@ZU>/-ăAL:EtmXU<{w4ZD+xܢoOѴR : q"K&r*;k. s2W6[&?OssЇ,tF;[J/t=tROMuIWuZo(\ vbGf}h/Zr:Mtq~Tp 6BkrS# >Ge#9q@xNz+ 8@Ds[TPM̰e`8PMvF4AX7OsZ9X@.A }AEDXHbы~;E_LvC)\ L>P PB2Hs r jPzBx^Aلj 0t |!HD#ڠXwD4])R|8 W)H'0 2 JS /2YaADaZǿ !B;$<U 88D8@|${8y$ʠ$/)6AXHRJ\!PGH#ŠLsȟ:L eo8eˬeG72X5P|,O f(P R;u#WX  e`cYX.yBIT>LI8yU? I9A@ 0 Ph7P,ht^bΔD&˨G6,6Te0ȏ$ %48h$ G+p)LqAi|T%- ՠŠ~m!HC]2(AQqs)LG:b* <Ԩčay֬`Nڇ8~aC UA!(uНOR xi!z'! !B}@Й Nӌ, + B)j߉0D ޑ,xxGFt>vOQz+xH4?I/4 2 !tM") $0#c70z( YE@0"x B }G @$@ G@UːJ¡"@UgW~V ° 0s$   ,! BTW,D@WO7 grS~`{HjH~,+X#l x|a `GStA@'bTyN!G`sH ? [ pð &b AG{b W P P @  d Āpxl7l[艖sz+HSS2`6 @牋2@ `M! M7 WX ʀA,0 h1 h+8x(E X` 8e `pHhΨ piƐy؎S2& ` y`t8m!  I* p9)YaH g+ VfI a2ilĠ P ,pP xh@YDF~ .gI `Õ Bfm vyy Y^j%ӂɘ阏 ) 0Yy򙔹%@i_ad_opo톛@ny/@BVy6p y׀ yooUaUę7oF Ґo /RuWCv6Un/@~%M8lNfpy_(d(*I]|60  s('Qx  lg $uЂl c 2IӨ) ELjC7@PmGaRv#Z D P 0Ӡ` FBrA FtAZPzEZv"p&rV?RvP)НfQ?AMyg6pQG!yIV T2pML" W) 3XJ qEfx!R7)p "e g<{a=* nZ | ! xEҤ!XGw$?L"e{5eDi ?Oa=Jvp8% D zh a8ta1߇znB ye7~^N 엤q[@@ T@ 3Te !4&G7Wakv⁜0@zz P ѠX9 V, a=6@ApABx N@ {[R7ǔ\vtL ʴ rV7 `u |~WA݃ M'߇n0 R96 FTy 8$:Q S )>@KM[nZyTe0!Ű`A9} *rL$Ѝ'AQ}^9c{ה) ! B IIP 0h;v DJ` 3z!zD&/DA p ` 1WBO3b G;yP yI wO Wx~'X F{;="!F"! qE0YIS ! :p X  Z~zѤ&t"=n`Q zpz%adR|LG't2!l` ' KEa9 l. 10Tt; j0KQ!H` UZ`$RLV j w]е !*X~Ɏ [D`#AktsԸŠxE@ M@ S gZ0 { tA P܌ : aso-A cKp/z` J0v-RAsQvp|uHm@KVpR '\}1 tbaAgde1 `09tF` @]P ]1fEa1hH@ ْ&; (nY h0 w㱰-A?~ `  `Ҵp hS )\b nXQ N (|ɏ%pH)`q S8ܾK"{{~j K/oUk$1 /e2H pD.?l2Q%8 F-/&hf#eV `>T 0b 7 r5_n xD?|'1JU ]x=3Ћ^6]Ao`cp!fmngOԩ{1((>e:[Z"JQQOaU#ojSmE1 O6G_Q?/Pa ?nC*o2o"GPm"q_ՏEdu/A .dC%J01Ϩ`Œ C*I)UvTK P-M9uOA%_QX8Ӕ6AIPnWaŎ%[و)6!0٧:̨!C~;>qa Nf0P !O7 (iOQ3[iԩUfmr\ۃV;h*!<ؖE^|L xDnX+WojT)BHrQfA)0kŭ(hCh I g䀄nA(Jj.Uza N!FJL*D'Ńdi! BCKhĘ!=.8쐡CspQ%CJ,Ԓ!,*3%"`JQJ=iN0r,*F+cȗ 9G!dh gKL3tSNGO$ (mBȶ`HPSH1`JVn|!CB*G@cO3 :mQkv[n{ZhxWj/Y$୷]kHQm5Hr4%73#5Pc6&3[&5a VydKϛ! 'RPDdZރdQda--6:C:# УFzjN0;*&h#i*]ȄQIҧ8fNgBuL|,U-7=RFs|٦jn&kr^6u7$DInT>tu;8=hX5dH KHmV晸ͺm/&g~{W韣DhR"l OPJ 0"_h?-yO$` r, FtUy`7A fPDFO>\eȓ_5SGA,U!+Ν('.Q{HԤE L5`(DOϿVt4I~D$J5G ԈDzeQY!݂va[(2%1Ё@Dxy@`H(<FԘ=a J( 4!bD(jXf MI' Qhi&rWD#xiYߙx#<@(Z'Env硴H%Ԥ( (K.$݉mѿ #ʹK/HG-Ԧ@1ԛ,VS`_usAl,:pKB(F,BY `t"PJKbn܌7 @)-$9XG4ݎ:޸c,R ,Y09jp -KB{n5Qh@F(3-@!̐)ɗ' ?DlII>Е~Xb: $'a0 ~ !L"~E)Bp" (!p&P: ec/l7#!P 2ŠiXC 0tA _D#HbE!ZPq"afIGĨb$ŋbxư,'@a&L/xZq h.L6Zd]#bGBH4Z6 E⏸XdlCH.zA\r*|`>#8/f< ^,1rnJkă.tQ/] H D0QLx d ğ5u\%-baE ^ X"MZp77ڠ`M:jqh{60 ğ7H^p]-F΄0>L6M(͵XDj28 -'wJ j"R,PT "=HTR0Gf@Jջ0Gdc oa@.bqh#Y"ɤ80=]CA%<'VH. )2$yp-L (TAJ Ĭ4 ܀^V@Y #D 2:8'oY˨SĔHL & ꒐uٗ$9A'r?NDElA`=D( H7e @Bw  [UCdj=ڑ؄NVRmBS& "n;";+6!m¢k=!Pfox"oj'ɋP&$;Hӄ`Hސ@ .1 F+.)H@%^n2ԚS2IFH8QU2JЯQLp$SF8m<- L!01X|tkv<F%&1 ([ AR瀗 SF҄d#EŁxH +Qx{iț wiŢ(&zh_WPb)CB/<Rlw#=>H9Wu0*{ՁE #*j6mK R:F—V,7tw&w“RXNFT4 & ]&0WG] *M  w2P 6P&x(w,hт-028'X6X8;h=8?A7XFHCȂMHKhOSx`SQq'0TndM?Bz)!rS@cF #m iB91<@ 49 \⹠H=7<4gE!aP 9 YТZWn 94uvj a: wFu Q?6Їѣ @9*aJVP @o m0 : *Kqy?JDzd sB#*B 7H0 P/J50=B< LP 5g`, P%e+ &%P(Xڂ 0\Ws;xB Đ9 P e 0 S p J "H6-t  0/t p r{R4MIJCH5qw `:*@BK[$F{c3 p3 d!|54_/ʵp k:` NqFŀ6{ KCp? @.!۵MpE6kt3p/z"A+[/T ޹<з?B@ 00 ηJ*9S:k!JeN6P <D0ٛ&N [*JJͅ Cк$5W<)M \¾d{H.!JS/9qxf A+?0 @t&L5A\F|HJLNPRc!3!y<,! t*34 EGI}K6 28: aZ\^`bQwYaI: QEc|~׀Ձ}!XnM=--x1rvXS= P Cߐ ف=۴]۶-DZ!ϼ,͠Uoy=]}؝˸qv -F` `LK܀"Q >^~ >- ҅b`A|=`E( 1&!K ׂ :Qa6lٖǏ]ؖ HL&̪g)%L ! pEp; 2IBG y @  V TU^ŚUV\M7PVtmm*|lJ%u`,/$zX`…_56Y-bK-@e z@S @L,exwԚՋr͝g<@괂{|(5663^ݿx6,$p.ʨZH* F g.!q0[lh`re E^f%ct%YŘEwGN*%Jf:#&F[n喁xQH/(ZiV X[oM[qRV qkeqmAA^{pU;77E 7a'Qa`>Ҳmu19d4VdO1he(9yYQ~Oc8 Zg{9趨 CF}J!>U}j.jjj5YF;mFe=5{;oua2ݻfsGynW~0qy駧^Wjp=Ǿ{zZ ?}/L|#|ߧ~_rT7?z`•j3; @J ܔ@VЂCD@D !1h , ǽ Ѕ DvA݂IA#Z|&P y7io{ax<(Ez%UE-|Ur[b;"9] ,eSTInw2)(iТ:.D$vRQ DhDVҒ27Frx+%E9JRKul])UJVʈDLV+V;v <}#fPiHcE%4]6兆LC8?X*!x:}(DV"BNr>BdjAυ|4I;PHdH  xPt\}C@(Pq H+ .#g@pB$SB@!`7j$yx80<O "G|b|Rҋ0$ lЄB *TcqAgɃ'[H-!GXï@m$ԥNۛ2dBji9h$҉D ˸eDpu5X_"tꥵy!ɞ8!@@Ʊ dlЇ?uV3oH8<Xg ]rB- ܜH6>p(*(¬n޶y K{߇| ]-"x y䜫pBqt/ 7ڜ*䨾p&!-U񇈣cfAQʎH?q?g۸yN3ʨnAAuՀAn;-\غL. ƀ#"uW9Wo{-6p{ExCqoY 5Ae7,lH>6_Li?^Bڭ7z݀!ȼDS`lfn< gxIaӏc*"(ZX{GT=*y'ACF}3"?xA&㬸靔Xl=@ c2ux0p}&yp@F!@18bK@AqлI6eA42)AA!A(5xA U"/ɦl C{J?рm@>~ὥ} =`:Ɂ(6ƻG+,BhK $1,| ?_<*̛6R `URH?P@Otc(kA`<ˑDԁ 4& m`2CQBAEj6y&hzE([@ " (!HA_ж]Ğ5,Xe% BKNb̸CKHFQT?sh>di@1e 8BUXp1ciLz .ȇ;n: DG hG\)Hpvph0EHP$Ec0k$.\Le0[ '1xB(Z!ZFQ_(hh$ZZbJJ (S|9(`*p{/rklO R(X '80  &JʯP: 1RI+|RDTyX,kRWJXKՌ@B( „pN͂ hhOЄXěWCl:H?/Ld\10x452hG 'QZH|> G@0-ΦؚR(&(`?d0$Э2ILps oуX xC+"p0MCp$@ {̅hЅ-BeD $M( "atݓ9::(f8Ԅеp?kfNTM ءU` x$12=pkhTH`B6-};[UV"3EM]o A 7|Of7ЫjDG!!"`,.t3v y x)& bxbF)Zy i`6C(IQ68f] : M@, X{Ff?bcAF͔OplP΅p$x  @n߼}t;:Ky!P4 Q|Gě.MI<`Dg8;T" `HAep?}1ĐJb]W X>A8'IN熁;sf ?3Pl dB3ksRԂ袃=CEkWLE?poDFzDXeRd(u|r[#*#UuZNx=0cH򚨃f4 NI0rXAS//rg'Џ/"ir> D8mq#U+~OgPKx!B~w`3W莾@BxkxGkq.yj?gvy'_pt^r\= qzsj(Vҿx3WJ|ztz2PE(/zp._{3v{Bw6'go3v }"zQ/.Ih`0 On !>ƻɇ4OۆP&66{~Q 5@#Bf{W%aZXLn%$ n<ʠ#߿kiuP vZ|(xpddž$ g #% jg`LeK_ &, < 9%,0 %vP! VJbD>[R4 Bx)w'РB-jt!m)ԨRRj*֬Zr+X(ד">& P(K-!0uzITPĪpE(U4F;016q M(+;@k 39 m4ԪWn5+j`?l~rҗ/Fcnzd wK$_bOLq&&wշ)H O6ӯo>{@CBIDVO%4JJ(d 0x$e v $ JV`fKx\@71_J0tWxPGK#=#A 9dW1@(4Az@ K/‘D%*&T,l ,&C$H$Ot& 0VBx[2/-DMB"PiD:(J:){ OA % ?|Yw5A\B0h=cx%vQ[:41a9YC?(83 56%*$%51)NXz.骻.zUBQmQ`Dhc]Aﵯ* :Ғ/YĠ)'.$K{(ħB[:;DS'!;+<gH4N X. <> NN{Ht|CB+4>?㐔c~Sv-I"Ch@C-Ar$[E,A l`/Ѕ.">8[)'Hq@28G4lй7*n`H"PTDz@Fb4B*0(DuJeAO0 K yzQ @L!=0A(TdqEŶU5 q8(DBX-쨅Iy`,DC+ E%H`,z@B CH hI3qqUBpCL$0BX@$ 2+$ ND)#ą@&82`=I1DT#$ àxD2"SdN,17K -x/DC B f-МD-2/i$BTP/10B<샱APC6h506,Úi/L)1B>:/ C-Q-D.gJMQjvHPC-1 ,T&82XǕNHE-|,.lJHوbi!EfrE#S%g!lB4X٬hܬkQdl-.DǞ2BQ]{pS"EJ,܂-l/4-P.4,B&P)d]YB`VV`!F)kZkPl.D*flfI$އBZjНDPOPBi.JDv^ٔR&%>o /u=m^/8PW|A~lUU///Ư/֯///00X&0 dkGBpOP_po&0B0u *d M0 0l}(:l.BBOFhpUh pqq\7C՟!qΌWVs"b lI5Q]d5s:/D(DD;;;Eg3|܎ih-s$sSȃ=DsU'OL@@UwHkhtB"tl(4hp|Mj~NESBCl4:BKD1U-} FvG3eCT@A?ukKȃ2H]<Ò[, {<83BuP&+cN@dG];5hiP}tNȷ|6kDw?L7Tw3BA @]qF,2MoCBFsPdU[VD4CtAx$6 ün-74Dt E!{7OO 4VU<3W8`v1܈KDwgUN8?yB4QrVhBvjW 8tŒ3yO@B T hJSZ xA?(7QtLҘ6TSP?NJB;k<tL`σFKğQUU ;UF _\˕OXҳSP~V7x7Ql&P CN'G'>tB$+?C}B|/.CJ{AeDнGƒs{YD+l=B>CԿlhYP &;5x (h@<{ `CC'#( F ;~2 N4董ǏE .LPԹgO?:hQD<pEѐ(UH5ܶu:uGiL h?}x Xwm_p}: ~kl]Cs/4yĆ-./<ޢ\pqe{(uo߿>Cxڰڎ20P:OkKpu4{ 2]*9܏`sY P ,]ajhV}٦!56SpImBk'4j{(p^AA/,p`VfQi<~TWL>HE(`W;n^`n +Uv涆CWu4)RыR3A *B)A%")C6x)8ڰP0 dniC4e4HxFT1AiTb?*! @L40-Pq%G^܈D}EA!!E7FrR%sG$$E2G(Lh %{dd RKc&">d-t@n!Ćj#X + -8}A˪A,dTe›V^L$k,!9wKt9"SZp SX&uv: MlSGe0;B @!1uFGPr!XP>F}R@%(1?"pbD9ÙhMG$c9d#GENN>R.;&&JE"P(P^ QzTZC-?*A< %{Sͮ^ȩO}ꐕ k_XaqUiCJ1!@mPGk oa C#u֘humP9rp79ٙ\b:BUԵ1V[ې̂+[) ц &csx&+@7zhMM:܂bd h"(pӺERu훗cA$Uk[6m Z$IɔCntϛ]dQk ۉz p vϿhGvD6ANQ풗<. '-fQM&mj 3\$HnQ+b ~g<e^11Ԅ0P.H>?=sXblx$^x|CeA÷skD o'`NN`f% p]!dae gD haA,8ث!2/@A@X!o!o$D#H orv;&h,a$^BDId:ej# ybxpx`) '%j7r(\4P)@ ND Bǂ/C41 F!DF N!?/o(OJ!-t!TRH!*m'Fe"aT`dl!$X:F 8L8!A[ !dA\A#x!*`Id$@ID !Ƒ`\h@=T!OL1r 2TaC{dB`Ͽ !LB~c#mކ"-7dP6AFA4aDLc!~a4bH^A!F 78& p)"B&q@^'f&,H>>D;İ-]-"Qn.ӇbD';2311.b*.@L3FS5']S8<1Q4b!6b47߇-7fS.#/Sr|#98A8Ţ 0:4B;iJ:k.ъjS<ٓ!l(vSo~spZ>s2B#2! XsA3)6(!n#|9!^H;CA;tD%?C. ` HcD}c^FD(T2H4CmH!tBF"tJDO6 [4J%yGs(V "THt'VK'tI fK@t%qA&ba$CB84,,MՄ0.'/Q'렃!3fcA* ,DOK%MuA$TW4>-_@&C@b>!h!b *!15 !L#L #f`A9CI+tzaS?SON?s7!7,v|vf!wu6m}!b?a4R'x_w7Tm?4}48M`fׄՅIe7qYax4,O`wV)Osxvv݂⁏mIˉXDؾ!  l4ؖ5f,8؋@8+Ԇ+4},‡]dtAuAf6&:UNw9yr9,,SfP0!IJʸ!29NZaj.:.C%uS Ogc9m>T?u7`7YXЃ'<D3"h.2cA8=E#a3]s{ d""3"!'IA0Q`Rf(\Sј9CUa/u6!ڂ1!/B "@eB@fϓ}w֛5:cSvBp' zdR(V ex#P-z1Z7 @D!&%3a. t{w . `!̀;G9] `'!_S!{/am0{BD!8A4B Vd # nV!1* 4 V!^!XA$!WԱ9G#%v`wZM9fСFViU5A0[Ggy"!Cs+}Xe!ҐO3);P#{ cm,!4R1GJuۧ%c<ܣBbXncYb\>RF΄3<0A24CG鱞K9% ڕc<=gSS,DD[t3Suρ p=[AF:O.ƔEwJ]Fb!`s2#ݧ ]I%f"01a| ~PqC&Y1!~B;DV31^5~8O<>@8E!&1+Q>ug>,k=w~uiuB艾k 靾I"o~s޷ZK`s`1>4 ~^)~Kpsi?d!?5x='4CTY_yQ?gid(v yHw!??k?[裿(Eroϥ(aVrDٿ?? <0… :|1b+Z1ƍ;z2ȑ$K<2J  )53Wڼ3Ν<{ 4СD.]p)m i:qLJM5S\z 6,Idˎ WfMlۺ} 7ܹtnQ2G`jE81^;~ɔ+[9ݤ_CzXbͬ[:v$;ݼ.V"`ʤvrн;ǽ<%m?%ܻ{>P3%>C"4}|So?G`jX .2cNHa^!]CQ1TEk=`!b.w#jtE@Pd"_1dBI$P3fTcE7C<0y0XdV^e1QCbR0ejfqyc0d~ h2Eri'> iNRdxP jayr) k?Fi{ʫO lъvʪ>+f뱭BGemBIѭwRޞ(.Nf$n"knދoQP$E{fJ>Lq [08r$QYq"ܦ?0%3[E|6_9C6<`Ñ4pMv4Ld.'(mM?]UO82֢- 6Gj ӄCLnW9N#19Mu`5M#6á5Ի !a|6M~r#[EMd'75`e "h mq׈4Q5jH3oTF<MpsHܱ ©i)%I q#O[EE( )/%Ѩ29LXl) ;6 x1DFd 2,K(.!0q , ~L"Mda!*dol#p\EYW^d[6Hf :5dcD,-hRDAf,J)"H@HPD#40FYk` !@@"b-/b- FDvj`"ELq\k@Ed0 ዒn#>ơ# qC%E;y(Ȭ! aj f0=}RA}(PSȈ3XP0U[d$)7(rvEQ1H- Z6Щp"^_h@Ë++,0PRm@ZĠ6 T$#)2N<2"Р)JV'.c\" I*@a%Q2̢99*;Bv UZfEQd~j+`*0)FWD8. 5trv l$,EqT"Y[shp`gAD! pE wHqJc^$hI/ tFΡ"?+4b  )Lz#}ȭ# h^'f'B]9 cDabzYp! maRE&ӕv4 ͰuuopGp=D!tl,|A^3g`Q^ (L03uxB&ŀ WQ ăҠ78Cb#qK ` Q e 0 v0 Ġ 6 u P ' C\8 H0X!0 xyĐuu @+D[C3}"[8 eeRvsZHh 6 HV|ȇ@ f  p ZZsh ?Ec*$P 4d$A^OȇHʠ H` 0 ð 8h.即0"3 Q6 sxq#Vb8vql53!\IM718Np$q D wӦ 1 Xsą@W9HA1T8 ` 8 @v(Y` d #B.vRGґ[$@:  8W|菵 9vӆ9;i xH#ZЕ:it'٨P(q^e` HR9 ʼnlY]yh8|ȋC#y0яDuI))Tc!]* ~֘8M&i} H!ޘ@aA |X 񙒋7i>]9C9% I:/*QpaA Ƈ) E31H &Y+).1J5ڕ-:#7 i=?ҠAz0aIIB TMJUjWY[ʥ]_ a*cJejgikʦmo q*sJujwyRaq{*$O¤f Jr"y@P9q׀ א{ $JRI N328R `Lp|pʫj2J  !ʬJ9SRk  AS $?z*  A jl*׀  `  Jr ˰q   ڰ!+^њ 0`fP a  89:S4 +([@ `Ȑ  n:D]a{1xZk AoP K;Y}~ }KP1jJl y@@ ?{ pQ+i+ t~ >MW~QKś^^A1`hу 0;@3@M` VzY 0  `  3i;_,L<K L %ˋ39w:K+@ ` or@ `'SP/u` e CK l 8s x#y*Aï P ` 0ČWFl[AȆ\lCHam9iiE, @\* ;C@&<@ `/X Q`1DTz|L Ҹܸ l@p"AZ,\` )zYBۥlE|5 jb @ IL>/, ib+&1 3 ]dF3͟|A԰4eRl.<}$ Q &Ű  x` z2 @ źY4 08s!%hϰ g![4Cpʧ5;݊ Nд(Z@ 㠬`{]+|1[!{c;] U_={C="!A@AL w0t .گ[P)m Ƽ   !Ur U) @!]&QstΠx ﺪ ,`ߜ`m߯ K)νa̷}Vu@Gmεly0G  o= ZM7jQ4np`$,Z] D[7P7c@ S_ %N 1a.C<*1}bQm@c1Sza4"C[NQ\ULU)8!li-;"Z5p޶t>wKA#0ctnomA Ҽ.˚ac^#.3; с%<L$N"Q#J!1ԆŎa>D{`:-{p!.p>>SK8}e7^Ql1 ' ܂Xta^aC^{z ) GQ ?.$@k8ݣ@ `7 i/ N7;G"B< hF? IO!1o<2:AUSU23נ (Lȋ_1͟Cz D0.ֿf/[&pp3.W7Qm<r<3C^7`钯)L"!n_'apِL b ?Yš @ ǒ,^<2 $XA .dC%NXEq)GP6j)hd @ht7& l1]F2PZPI.ejq/_48춗=,rqΕK.φr _&,cȅP$eBq]")\lXɝUf fkd93ۂIL4\8}o}l]CvӠ^e8CG&_yևEQ@HP` o_ZQ窂ҹ 2#,rF2Œ!Ad˟lK˶z 88V @Jg 41 ΠjǘZ)xHu0tI(,k1R ="@Q"eÁ*|2H.~JL)3,&bre&Iӡp38l1.h lFj(l",h4y-?CuTRR/+a!A"pe?qv ]KB0R'9,~E7q^-Țgͥz,(0Dagڊ1L؎ }u]x U a Hw$j7n#-bu x4(27pLw̼q& $8 w 2,3`p!k9xH/]զcR=t `I2&+ICkG%T6닸{r훉ic/#Xf`]ћjgtGq ǟ}[5qt"UBdcKh~|j6rԝRJ l.ʋ Y0 wgǟ$ԛw~) R "Zy{,Ia wQfpfqAgo|wH=uYJz?-ڳy~iQYqEo?<U{"!4YD6+iC2J-Dlh,#Tdd"H{QoW$h<7 Go>G5mM6e|pc9 B3(*Ms1|ZɂD-|R"xE!e(`"%SdC1 Ak}c CtRʯ~TfyKQp? >t2ƅ0HcA3cCY;YF0kcw$>Rxf7XS$A1e iL \ЯqJ; ;xK~ yPP/;Ȑ.:rG4ࡠ@;]@jofTgG=S dz A ᱏ96ү1ieIx"z it}r. ӌlTDjSjĀTAT7cH'33cfPlu1MtfaRQթ2 F?8hIB.zkcyT=. @O84sQдDNw mi?ҁL1!P jb0>7)eHBŽu+8Z !%- !Vɴ]{KƢ!?"lBi؀D޸n QҭOag-G{/5G9Bv +R$4`'Gh5>2n=h!B@1_bUBSK?l?bB1J)/Cpƒ/!xiP:8HB C4_1JS)(  CİH5!DlDQCg^4h\ Uz!Q|h0b@UEJ2pjT 5D7dƸ!Hp,-+=!_-tyD pmxM~L{XRj0Axճ?HdF9ȑ$CiS|ȅȮ`'`UyH >g=Xӕr59!LL>H)Y5Q <ꑈCxIwY ˘ fdjIl*9Xm{JQlĸLAT  ǀ<0Y9(>"䂦)AKaZa)qpJ!],%8~̧|Ȣt)@ H98'=g 5)?M&Z?mD̈WhD ЮYp <娄݈A0TYCYxM(x,|NJK-aWF @ȳ̧X@hɁ(,%}P E~x%iph,ּ$8RyCp #P u{ŠLg,!P#AEh'Cx4~x cS d`qЋ SL p lp< yC|Tgd)X/2'9{*싒ip8 pRD )'$-ΌXpTІ~P6]?2EK)U eUfmU—Uՠli$Ch _TQV b-KIgQQ4* TlURrWaU2W 'zk5|H`,XLt؁X<ä*VҦסLɍ$=ؔ}Y9XĠօ) ڃYYhYCYM؄%|eRxox!]FqN1Cp<[Z_YQXHY"ڄ!} ڳ[(Z{EZM w pD9&][m\[YֱPzW\ 9B|F҆\ץǎ5{ul`kQo]pق =^ؕ֙=ښ\}&XP]V8kZ@$_[U%_LJ̅0%N,,_ 8-\܆5`xaȁ_hA( )?-HP/ s(-e`vU]]V LᄠFhap]6`f^~b9I8,U)k07p0o`6\bՅ[-E_7b(pbp8_ )8m  +_d=qccȐ㋜\NMPjxG8PN?H >`k]8?~0ۂQ6`OFBcQ._oHxt)1_\@`>fGTvo9ݍρő(=`&j(֝uJgwR赚 |b7" )NX2ӌ>[.de9~5mXȠm%v-~8Ve餎8GBxd؍g ImƥNqP)˔ڍoYYeoGM,X3ֆvqp@{ F gMqHN^rn.]AnNH̓PVV>-I~rU&r5'0e1D֮ 0ʭ#,&2 d/] P95s44_sHGNANU灨JpZu#S߁!NHDL-sek%Hu":~icvA_?X`@+W0^דA= RuZuoeSt\'eރB|0phC pUUr8cCPEW>sGv2?)8%7f^p^UxXF^nGo](`/5xY @FfdAizMO'ے7Zw`1^aYvze@*y@++ c5`<[n_xop@IiZkb'g +GF+QuG{o|@n Yȇh M x|x,mp}8e4^цڹ_zv9kHh mPi^}щt IM9zHz7op~DWYYV/`[u 7/}0>}ϞA!? f /dyIL qܸɒeb?6o̩s'Ϟ> *thX*RNB*u*ժVbͪu+׮^4BfŌ9zDrdFx`%އ2i ,x0ឺJ \C>.,y2ʖ/cάy3gu,z4N'FXMMÎ-{6ڶoCU[ZܾZ1; |9ΟC.}:᪋:޿/~C?}" 2؀ؠRX qrءi>'+hآ3X;أ?#v@YG"镐I2٤OBҒQRYW8%[r٥7a5Y_٦o~f-p3P5\s  I2:8iM6}RZbrکZ٨n)1AաxWq=C6I D$@7A$`R|C&H$"$/bs<Q`槕q #\b aW  n 8HBX 8QBB p"ArE*zQ `Pb@J> F93‰w&I4R7>d4&O$ w aY,z.Ɩc/A0!GIc.h&c݅1 at3/؈BW.u0/`Bwj‹:OMDtQ ^bδ/xAY .B;^70Y\&U/L>dwİqte FV[x_-]♴}-(0>8&ab3, Bp1DfqFnEP}!gHQ>Ǫ!EIV1N&\dX!g I{"e2JfG6XeL?gt,.@й0; *gF͉^ҹ0~3 i !E-:Md9SDTj=ղZ0D=a5pk^;65N;=C@ D @vAt'~m@P;ƶS&\ 2D;FB jt/NЈNԀ4 n()8+nLK8Չo8)gP 5H̝Ao.9I =cn;BdH:v"LGu(J8;v67NIj];<%۸i~N `9-ŧuc::ѾCە>9DC'h800i kAl+ЇM^ϓ_8,E;=>#[U¾fLq}l 1而'$C\\DYH,P]1z B``| fIGQ5` |/^WT̂OS0O=D%_J$&iA=lZHH2\Ku D)ŒhQ5aHPC)\|8V>d2CȂ7TT.` De1RTن}Cᬂ,4Jm0 B47O4;DbL*O]D4\ɟJ;5CHb-&aT4d>]I YYL `M <8QCC3$n@>q44abP 34 a<!P8@%hFU|0,%x0遭SF4.A4BTD$ί  WH4@+Y,'DDR̂,C$B'!Le.C$,l͂f<l(D\>M'UEeZSi9@XA%4S/h+-Q*iQ6#PQL,$`+.7A>$PL@WpT),DD@RA@+"ԂǓRXݨz!XȂ, W\PIfDt@v H$l)$Iީj> <J\RNo~,Dtt35"R3R kJl>D}A7d2[l9C/ 5ĜZ<đ O&Cp.\@a>Df-8-BrH?z -P/b-dBhU.Di*~ET|p@Pl{c=?`hN*AD7$7 L@TJ/CD 4xaB 64^Hu0+ =?"_ȹv&ۘG^8Q#]dƑ,y[B0@>1#*XHC,"?؈81#r"x=IFR{|GBP !YJS ZD!b< R[r AvbR4f2/ 4Jk^&IXIWQe,53:Nw3io|eEb+k\,#F>S ?)B}T  BzP4AJQtVF9ZoF2:hّUf5(=@XRX^:Sp > 2tAPZʹ+?,p.4e?!U!U$ !,H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjݺ1`ÊKٳhJ˶۷pʝKݻx˷߿aU @RR^̸ʵkK|PA̹ϠCC4P%Ŭ_˞=ADEضz pa MУҳkνNR `Q⽫_Ͼ{ >Ywߏ>OYCA߂ 6Tf"0(@ D!sҐh(t@ DEki1HC <iQDi$[h G6P%dbɌQf\t% Udih.咋HRKBxךm ./(T&\gI襘fV/)F4gA"ĩ*MKBY˨*-KL/B BK16'jB@3 A/Nj.BL, #K0. u39I)S0 , c@DG m.A0%R3) bAeXR D,P)4`J)pJ%"!B0 ,KPytH@ Ą P,8@4 B,H /FmfB2)] Qd=&̂@HBGq3P+tCrxd .=Q$vFt9qG/@}r .Q1 J:s$ˆar8A>d6R$1@SD ,M~BmO<]p@P 0Nd# Ad+ x"HAHȚ m- {Mk ҫ@~U=AԌ[q+%bC_ܢxr>m 9m{yuAF+8H4B^,a'REJ9M6yL6y1L䘜QI5X3!kdlP!xLÓq3B\'l \?(LfK ,#?AH.2͑@5 (`@ D݌<bJq"]ށ")DBh t 0¹ 8 ԂAb1X0 E+V!Hsu$ &@%@Z£!).!l`C׀GBl\+)YID1;BbQЂ:%*x<,X\1CġR- ն( -VA 65Uxʃ`N"[B0Q񱐍l">",f3+rd"ZRmjSː^ heYrֶŭfOZVַ'`] 6nhꋖ:)AG 7ן:! v!"^񒷼=/zz^׾}{٫ox^}s_2up _ }5_ q=\a_FU0 kYmim|Yǵm["ǽ5o\$5Uc'8y."^,0]>35nfC,Yjvs׼g4s g:ˇ~p5BːQg45q̒'ntMAReW]jծ~5c-kSӺ֤5u]k^ׯ6k[ع>6jfZv%k:Yr*Wz&l]=:-J%),A _T $ce)L%!:kg>jbll#ī]ZAC{u=`<ݪ\'ov̟e|7xm>c)X1RX@,*fi_7 ^bٶ'jk`عd<9gya~v]o'yM>w/ky^w{w{>x>0)Hl(V%amU:V)jA}g}OWֻgOϽ]%dJxE"r 6| 6hA_-nEOO|- h .djDBt|5$k I}mE2p<$y a@QQ`Vu 9 3kks>|3 29ޠFK6CN ;A1ay?30uSaUdU DPi\؅^`b8dXfxhjl؆npr8tXvxxz|؇~vT+.u  qB C 7\ -`6t1 b 63Wf! N0c ""  xPAD@W3.& pKRyT4BT S}X4 #4B" E5.` r S  YB 2 g JxEShc *G 4, 9 e(q uO 1*I.9O 6I5t A1yB wB9GDyH]ILٔN&u0wPYVyXqWZXFIdWq`ieWpY !u "Rp)ZkzSmYdA\5QWC@  JP%U&T<0NL/q(Y F UL>*RHh)i  u593#\6 uuI0 )։d0 əL%O $ $PY qѩ#ytJ& DArٰY %1aQ$3 QyQ P)Q WY$@FlB%:>7 Y р!84| ʠ9 ؠU/P GWఘ ,`;yL;xxUf@4 X I2/ A` P %+ Y7QH5!ڐ @Ju'1 k rP<*?uI,fOϐ L5jfJ $KP3?!6hZ ,6S>@\.V* ́5dT21Jz3pr eZ:Aj#` 4qntPazD @qC _vC{O` =ڮ6{8ayi@a(* A *j @I[$߳8d! @- @ 6SQ$}5a4d qaXT1 B0*  9qM OSAL_L  oE 0B\q^=Z1 aK,p`6zy9@>YXB8z` Y6!N?П{`40K)h 3jS>%; SQO lk з fq 40 ?u kxB x@ )٫-K@q 9$0 ! *Es-?PRJhYא Tذ0: A++0 Lso=X߀x@? ʀa Ћs2ǯ%R4 `Jz^9&K` K&@ U (U?YL UT0Qp,QF) Ǣ FD*lb'ujP(*\@FAEt ):D . Љ?'г, 3*W ZL;龮"ߦ)|YB -D Ag)0CCFˬyc &&X.KvjiYMщ4M!=3*Ҧ)>DH&y`yЪ05-71-Me%FIՑc?'kM,,RMX5K* sE z|W}ֳhA1#!v؈}v؎؅fo@ vZa՞٠ڢӀM3A9 b*QM 1۳B): V\?=]mܙtŭ=].׽O4M& !޼ U{;.E]7E+ P$.WC/s)z ^q>$m . e` lN.N 4%^J]@ !> $a/:9a KA ?N݂D2P7< `< ws 09DLT&ݘ0b` `1/1L`̫m1*k$+C " @ # P ֺP #Fi 2),,{0^G,"O žƒ;О\?* aھ+%`2t{ D\N$@ =$l"n$ !T69 ڠ 4R_  +0 %o&_ R@79b`/zB,*o+146G|,]Vm$ΑD:dm0Wp&//bGiI~X1ߔ yIj c>;cd0?/?}=I X3 1PRJ/ϾO^$&Ug/ A+o d 8/4CΤZ$ߴ{T>n\ PR?3V~4@@@ C0?pe ,A-^ĘQF=~RґDRJ-]SLl'_1=}TqL ġM>߿U^ŚUVLUXe&MlNmfMV\uz7ߴ>}*ڄ'M U_Xdʕ=NLeNN]gS-֣Ѥ]lO6aB?|̿DI1mŌQѥqFvg*j|BpF DR.)6PF$){OfA#h) G2ПNz3O=mOّ~AOF3.~ǻF\H 41b"y#(RLM,"ǟe|0Kժ^…Յru <{Ex "X GayT#oڧɂڡ1 3hqT"\]IޫhD\=;|-MSC2[1?ү*~?+'_ţ)3`Ɓd8$VЂy/v:I68B&,aL"+/cNȩbіjȷ_BE.jC"d)Fq|ģ{I,ZՆ1B&V Eh#g ħ}TfZE- _  ] q-,R SY͸HFB`C,Q;. PD:(P *x1 Y 5Td#uicm|7r (@ R|H30ORPeی 3ӜFjy%W0@$a6n c0" s 0Qa\v "RD _:ӢEktPtKh$ 0@ ТP@ QJ @1 wUEjRċҧP@ Z)O6 ( Xn,d12yJ(ծw KJtfX&ї A`iFaɋ` H)҈bk0C -@IFNյE@h@V II9" ! zD 2J xީ"Əl%O/t1[Be*.xV 8hYŠ-P] H^ .,]bjIСczo^w4ԀE`⾈}5Y ]:r߂.$"HXq?mP)qxE>r'GyUr/yA{dIF#sPV>JoTmHX5OϟV) F8"f20'Q9;in'L6`9 WdiWIetfܧ#򼚈Qh_D;;3<WyR ?󠿴"?z0OKMSoo~i6&Ovsp4b*Gk)S^CP>: d/Horw(N@&8@ď~HCIl%˿@(;@3>K YXXp#+@ -q 1-x@ +P1$@?BX1Ӡ"B%ta'&")$‹B.-@0/$CC"CA!<;C.lDB EfH:H'g@I$PPIykIлIPp ؃3 lʩJtdžɠtġƬ==pAPtHL ʧG#{ԡZJX8ˌpK ɑK˹Kmɟ# 0 VOj d|JU+KhK[x(eIAYk،iX2dXd@tLMplM|͖\Ga،4HH ׋*dǼt$a2fxX4qd aȆo(H,ЧO9yςP Kϭ> %+V,( C0~k K W'Y1~?JuL#uh&HC&QZj a$ D0͊_҂5R^hLNGRE`L1S2U#J7a(Y]Rg7"G_>Lu{@]ʅ_<%II#>uL/X;r++`06 13|QC_`Lb% Q݅\PT}4`]McV@/82ɅemVMa}k%8/]2/M֏ rW8WjeWky7U ~5XG=؆_7aX3DX؋X؏s ّ5F*ٓUY(JٕujٗٙY ٛYٟٝ ڡ5Z)ڣUZIڥuZiڧZکړګZ{ڭ^گ[ ձ5[-۳U[M۵u[mېk۹u|o۰}F[[}=-;u\гȕɝ\̵ܾxk۽ LNɄn\2]ѕ0]CA]܋ [ٽ][۽]%ᭈyD^^pH}]3[%ލ]]i]?Y^'04ݍiނxLl^΍iu$pІk9ޑh-݂%_kH<2EÍ;io4Hͬdavjߙ]kн@0YaUQZ$w@k8M ΁d0Ud%3~P tid>* `; \QgjYkȇ &ޭ^[; `шCȡaJa@ _0^O)St(D4^3,~!a_k(c|bd(bR_3ɲU.*:ՌmR%NP +w(ON4R4JNI,^&0.8}:kc(:cXieP#{推 !.;ވhxh=`u(ߠPʔ=%` -(^9ͺhxPZRX.-WP Ja1=?%d` XO(-nw.ų$PX06bK@T#6Wfn+u\x7Ylwyp`xi|jl \?2`e\֐eT\0Pgg}RTeq\9r7rb#呠"bՁ.]X:\WsPչYx^]xD4_:nS[={X^**nŧ8hW8xYEI|W[çDi Aˎ3m,QwEo:[o)]~~p07[爌 jKP-qǘ)雅 ^X}h1!p,h „aB~ & "ƌ7"ĥ#Ȑ"G,i$ʔ*Wlcj͌&Μ:w'Єw2%l8 )ԨRRh*֬Zrkʡ`vWaj-H-ܸr)šܑj.0Ċ]<Ȏ'Sl93Р/.mr>nujղgӮm6dc^ w‡z8ʗ3gX9yK xڷsB_=<׳*}yӯo>~? 8 ne *": J\Xx!3!z!"%x"X-^"18#H.ָ#=(SEy$)Uh$QJ9%mK8!]z%NDa&mXn9'uZg'yg^V' _ܠ*h2(WqJz)VG)F:p1)EbD *~>TkBq.# 6;O8*p&B&@e <zKBQ@|.+I{B:a/"4 / k/4oBo`sk:\1%S1A+b25*IJBo $8K<4$ᬧXbPE;tHG4ţ>l 5a$5T/=<Ў;un3BeNt;%m?\ HF@cfj:B_DBEAcMPkM,!~Lc}w/~udSP,E [@Qp(a(n c"snj.l 1 p;'“b<'@1φ8a]eE.ƧB q8ǧҘJ#Khb` 8桍T+ScK,&2$e2(:3)M@sּt0V%52nk2ӶNKr6Yxd"ܤ8K2 e,o @ e&j>zdHT߅S@3ѵc@Q]s@Աd14u!Ҵh?RÐtSH͵n+H;2,$؆:pRu gOӮG2/l LtjU"|b5*WTᡁ\AT9y,ΩD]YrD[ W٦’\K K谱uIA$Rj QM[s.%d UFA:MGKږCp֪q.W\u)Ε-uM7.xKսs ]w$4i8 APwd=U])G>xSޜɿ&F'ˆjU+iHwtb T,8@V튥! ~S<\A8"C5˩d6 9;8ktN~2cvX4ʯ2 ,_^27Z#xW&5Wgbn^3f,aCU"8ͣ3=9$ih8 KE:4IS:#Z5= tg4|9VZS"\_i3κ`U5,v\cӬ;Z:y^u3sx|,R, " Dw'kS<2wRBv4?mTk7X؇B_o4`Ĩb vɍ.ہRT uf*|AeĬm1> @4|JFf6ZHz'wH/Q0@uŒjVjf6Ɉ[+ /!H X:Al!vI1V$ gGk|6f֔d+@RZ`Z< ?Ul7Q` jgD|r:Q@K=Vu+*j8E5)\ujUNݷ>D?}M?C/ /MԼ/LX6 &. Q^ ,"ӹ(@ 4v.ɆxQ,U~y`M `XAĠ *J/|DdX1 nHDVX@@B+@ӄD<A8$D>J/D A@BABF4r]FCA$t@ &$ APG< 0^xˆm' @!L&28" A,H&&RZy!*"Z@D)٨<թ:aB8?C<"(#3"N-6#4~Ia4Vc 5Z63n747c.Bx098C/S;c\/cP#=cc>?$A-`J@.d?@2PCFT(DfT`FJDG>G~d0DHJSIcDd@Cý?$V42$D$PI eE0Q&NL*elC2VXIZIN%U  S|qVaW~XXB$W$[FڬC'JE+ʥZM$e]"MHhŃRn#|=@%8Dt`+@6@Ƀ;HAV\Z`hfmqWzhHG<@f?SJ%0!2lf8?L"6O>Eju[erV`֦s&@>tR\'5XjKw6gxxJyyf'4xBJ{{J|z@M${ΥPg@Fsg`.(f(Ȇv(6$G(|('kvިWp$Eď(ck Ǩ(D(Dhh ] )JAA<)XIDrn5)*Ij֚)E"dBijک9ibiiI*A웠jS5>j)^)FdC2"Ψ95 i:̃F[azJ#8v V~e'9I:x0p_֪P 8X\2Hߨ꟢.FĘ2޵N  븊+$D }53ʦl^!h,le̬jN<+-&.-6>-F-V5`clF4؆؎-ٖٞ-ڦڮ-۶۾-NzlF&($ ciBվN%`Pn[c x Ernɚ,m #QAx-Fls}mݦQϺ6jmRjKAhZM6D8LM@Q D>|QCp C@J5EpzC C8\ThQAؤjB5IheH0"i84 8CXa4,¥W1`62@:7a\"D@B#-,\5=k1@-JۋC[/]-1t,„/(;鏛.<45@8=Aj+/\/p=DuApBL?D.>@ 0Y+@c !F8bE1fԸcGA9dI'QԖ-[iRGQ%6bM8lW\z ̵4`"&}80)ҥBfպHl|6;3"di:pf5 `LH[W ,,c(ҫxqcǏ!G (^Ĵ"E)֩ZKvmX&+hp 4@wJ +ϡG>z1!C[8JL{ h7 4~}K{\LE^ LP..24)pE@A Q3 [PDqaŖ[Z0WEYL D,#1rP;/< 3$R-/N'AB2d ;#9;S,L'O ,CMTEݪ2 TmK1TM9Q&#!MJSQMUUYSCu5[[qUW#_PQ%ua-c Wb vJVieH)o W\:S[`'qwWm6Iy_f=g'npvs&GI9kYʎLPHuqƉX)?Yis̈Kfƀ$^AGIkNZ饙Vfryg Fbָ[lɒ,I4zVFD޼λ#m8KfpιkdV! &~ٯhNy"O]?kżG "> ߁__}tzG"vFa@0CC}|o_VY7$HA-Zx v؅0to`@l#h)\(0-tHXԏ.\>qxPšEd24=C$fT^,h! dPCPBVB8".e4uhDPoH7!؏B qXJQ+Y[\F"(0AX& =R2~em45Te6d@ 59D3("/:}(1Ib"- -yE,0Bp2P[cP;O3<4Rnv((dnJo28N4ɢ(9qzdA j @A0!ODqm hڱ~씧=Dg C5jD3T.5L] J3f7?`Т@3&p[?|ŸEi$՛n';KؕS}E$"̓FIi]X.tT U7’8Ch"^L<֪R9Rm\窕z*,zEIbh6!nwUm·HBdg8Ȯ6?Ȓڐ!6ky-4a AC[1P>ρC'\'r͏#y" lwzn3Vs\nCˑ͐T4i0 @"}}$yyq' 1ZlT4(:ݠ @($,P"D~5q'BǕ Zʗ yE{WyiΈ`8"\!^\.ADŻ1KYqDz 98ṭن8hOyX"C=)%Q0&WD9dn7bYQ1vԍ/W#z\У?vm`Զ(acW6ȟ; cd/\sy2b&Z8#3ofg#iHe^uD?`˶EʝGW\yYS3# wc:%%"G@pvu̞̣:| $ ;xEq1G>"'HlZl @>,Çn]h4͆ 3}sK ف   EoCc6W;^m\#z[){h"@=HХ'Q#H(9zw ;\c{㾷bcB&B#V0%E@?щ~.H`嫞婳=Iaܻ7! D$yb~GP} "Q뎃w 1h*a@nlfڞa #"Ѷ!"m$j9!  pOo# 6"ompl"A(B  c"b0@kBG\B W/$ޭbЅpʯ  QZ1!"H' Hk&;BI 0υh!t#dq^Vor6q /pPD7:l+"Z#!Xs:3S(#*S#a+Ǔ#IJ1/"+.3\3>ْ@ ;;餓,|*B%tB)BqAA,"@-S(&Cc70 p\DRDiDY/rBE;F/=F[FoHp5f=F3"hG,@Fmae&Ŏ4+"xA^:&D 5g BK$t^j:#B "BaAc ^*!!I3dJ#Pm/ pEcAS2K&^%M(FZ5'4V+D|Ui.j 65ZYalY0Gc |q\? LKAXU2A;%/^U8 !U: 55_@`%$: a+a,塪cA%A3240F/+nA :TQ^dzd& ZwCl K#Cn.vRD{AgAy"t.+NwsPhkZ6:b&C"dlk6L/!Lm(#!bT5nvgmdvpoALpp7 #A"#+Wi CN(!.0tI/ba !Lѐ %Zw,>r&s'2l%(:t!j}TRA!Ik"$moaEuaxi$t'*4ԡ`%º n AU9زv|%|Wq_@r6h!&ʗz WM3!HA)el &Vm,r Gxax1 ($". N` (R!:Z (ZyӅ"@` ` t "~ >~!>%~)-1>5~9=/s>Rݼ !,K~Q]ܫ#<JAfT$d]"Z"el!!3q=a"dA"$x_2EeLz,2^"@ANq"r^+ ~ardof!?8!$#*AY <0B8-|Ad JTL 2;.\ű=41ʕ,[| 3̙4kڼ3Ν3e}3(@ *@w| 5ԩT*3Xj% +W&2qT n @%)"APLlU˸-\FXY;7 >8 4 POSR96$ VsD pMM`:ش+ X :'/]s D(0 9ܻ{p@ k.lcFӷFEP@IG_9`qsU1aQVm`W{$2A @<MጾQ+*;\v?%bJidO}TH*+1[D$ uF/.A^d# FFͰ#~ a,%~B0 2!uM2b Sc6FYmbs9@3\D`h$rFܑ` 4YO0h?S6Hh~1 a1) Bp P O T^LI2d jk#H<jA( ^4.3~5>De"ȍB:wj,BV_HڢQP H񌙪 $ bx`@'MR%{,ABZ`. 5 Ŏ/NE%RCW\sJ(cN--X!8$T @ C2a*wms J׹ 7 )AQ5@)yS o` }I2d!FHlAX  JR 0 P\ a V*ฆ  `BZHcBւ  j!"/ ]CMfoVbI\"R`*##yLn (;#x'kPANђfFڄb[j# oJGP6y2Qˆu+(2r xθthe@bR*X B㰫Hi(WԨ#@7*S`$-ž1o L_Ciů}%hp# &FbjgM좐M.ύt{V3lY>CA`*ib$>Se06&t5Uh\:[3jœBap!;vQZ*Oqr_w e+!Rm$S 5lp0߉ !^V-dZhBm?%$8}2_I6q/L GD7XX^Pu$S a(M"pò$SB{T6/o8YA+k! hHZ"QpP@7TQAAjaᮇE-XǼ(Pru0g tH/Ώos$Z.b(˥oOp- ʠ XTf0UA P y! !' r)`'bQ %8`kE *(i63 Y,.G "!L1 ƐUބ Ӄg|R/ 2F\qqp1 P fNUp a``.hU0 R tZ @%4 0U0Z;"q!QQ 6 Xv`PhAyP~Q  k }2tp.xR Q OSQs])a F 0!3&" 1*e` 2xm\3"V$)2Z 0a 0)dWAAŵkq4a@P TҲ gxY@!i8F0@R>3* O`/F׍k<R @rhV4bJ> 2 R?);T1%h P ; 2<2 1 /c @ 1mi, // } l@ Co2Smq30  `S;2So#38 m,q)3 n; ԓ p-S3P A S޲2 @ c?I9Qs9ɞQ31tA 0Z#2)eCq"3*c?jԣ8 9;Q9~: Z7#2JOIy3,:-㎝-1 12t#u|<;jA Imʥ2:-Jӟ'Z&*ਆjCP 1pc<|J1o191dJo છ C o9rz*Jjʬ*  :1_ZnyPpv*Qa*]Zʮs =r @-r: !c0hTQX`k˱ !kEaK#7"N+ +5&!бECI%۲nL-TP a"аMCLK 4Q1W;B[D{`dC'jTr5$4ѷLk뷕` 0kJAAT`ø$;Yk@8k) 610"ó0KB1L!<^#{ K$pIxV4p+9&A:p g!!AU(P 1a/qOC@KAy01a K:gqr[;" #zZ0Av"ޛ30|l1! n$ KE2/pg@+E0-CA` U% v P;$KUpL3T70pfUx  FwB\|p|gEp+šXƜț(,cgc} ɐV ,*U`Sa*K*t\(جCNAAnӜB|;!M|y~:.eM<-AO+;N1A;An^@N p""0|K6ܾ^@ҎCq|ˮ)O>. TP14@?#A見O߃%HI .Gf/)_9\; |LOOC'SOf)hUY?PW]_/cCaOgQimCfq/!sowAu{}_>/o;/㇯kOC3!,H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ -cʜI͛8sɳϟ@LEJѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻ#U˷oS~ F+^̸cLeL̹QR<Өc>)׌Eɞ- /]`(ȓ+_*ͣKNzAn8H&Ó(-C}{)W`ٻu P#߁}ʂ(u-Y^qHP,x}b',H⊔Y&D'(AaP,HW&@ D6]LRcL6pŸ$TVi7M\veOZ~)`BPӒ.cʡ9SJl©|.rc#ᒋ@碌6JٟfƘYX覜v BKK-w@^B 40xj뭸&u* @p@jhzQXB Vk@91,A Bb_3`4 ԍBH9  'P1 T@| rP .Ƕ+g *Pͅ~#C4 ,vTʥ˱EAr+MA[0(I2\wuIPK) 0mXk| P3 ,!--3 i_n,Т,4@D: P:I2oq4CΜz DDE*$I и -'ګLO 4&p[6A&dq#pFtAwx (Z/KR0 @2@ˢB(BGv @/febxѨ Y+!M81Pu;(1x# خ`1P5X Ȁ4.4txhRqEJh0`A48.$ Y؁#ɸC!!$$&7$#FX#$r'?rdH%L S!˲?D(AXY}Dx` @*YgAWЀ D8iJ` 6iєУY\4 .1+ME˱ĢziFv oȣc^bA?DGFyX /^AS>T 68ͩNw9P*ԡF)Rӥ2թIQ:TէL *VSp[ͪU:֣$5*RNgJ d]M^ dD?DX.},d+:!,f3{Yp b7fִEdU Y6ֵ%-gaX"G4\niYϊ.h[ָٞEjZֹmi[[6npm!-NūԳFռSEoU{U~Zu/W VdůYV׿o@5{\`0~0#,aS05\aK1 v3|x/p7,6Faoq@r["""F62,%3N~,e*?LrH^,f.M&d` Ǜyl1!vBf0}><+:ˇr2/̏Ns6W 39g|P+A`a b\,"@ c"fV:ۚk Jneq  !@!AP Բ14[[2Rƴ2 YUR F3ƾ<,L!ɢC]Yk[H*`e[p J$}dO@ AI"%,n{!|K D 7B, B_|Hh+lyA("p@z>  \hOpNxϻOOx$sIa qA7(؀v\U 0 >bT6XBВ+c!H.(Q⣞ܚd@0M,  X D"p UO(/F. Ẑ[Ǣ j>&b  >+s?'Ԁ rtׁ\ P+Hp yR& R+s rB q P+9 x 'zA:R0P I"ap j} 1<@K01H*p a' i ?ޠP ` `9)zjb eC=P¢<akE+(G9(*T AD'j6BЧ@! ˀ rp ФZ찏0m*: :ם :f&M ٦r~c. & rCo4tv`1DHy`&4X , Fr䚇dJװV VЯ<ꀉrBl `.Mu `)jM3 eR @Slz"T :XWз  R*l X{r'e"*{@VZ\;z2fZ˺A1q;2tć7'&T]cB»a pțʻۼ+['{Cؙkڻ; {] =ܫ@վ%1$=QPkE V30&3&`Kk`E{ ؀ Ev 3| "<$\&|(*Z +l’2{AX15 );Y ; G` P!OQ`9! 9jQ<Ǝ>E3c=R*WpD?Dd\` ' a RR 2@+<|"\EUkRj7u+@40 ;C0nHlTv `.p|s 1l2. jOhC 0#HirHb,M,`5G ]ţ@ 0  갧LDL Lzv(Px@-l#b,gB ?yR+=S͓Ó+Ԍ . \@c';,< k{" ,]1\!/@u5#A?,HB)PhǴ#aDdDiayOSMV-FN`HEIm8A1T]ߚVlw)ULdpE{kMK$@VsK@ \}c@]0P4-oppysEyQ Aڙ-pQEUCG =ݐ$@}=޶P(15톔B=m[Mg]]~|Q!.!a {ڏ߶ Pa%ݞ$>ۧ?M+V(Id1 )/=PfPpo"  ޞQE`R0 @^$CP0 PX% `AV>UXH`JcN%}BZܹB_P5ؔ "M|~%Nܳ|()ʑ.%D| Mz^Cmc`uDR{8D `p:k݈}( bD1— JH4In>3+(+Dxʀ0Q .pYN1?>or!rsM_$޴wĈm [aR-!6<3>_]|1A @G\E) X/.B.Wb?@ Ђip'W+q pꀍH1@ >!0S}8>>M~4SD(-/-ࢮp[`&zP;Tɞ0vYOf DPB >QD-^ĘQF tЗLup f m!$Fႃ}:ęSgp6vETRM>UE~B^ŚUVn:\PB+ źЗBS(U^=/? FXb??X)p E\i"OV{L7d@^Cug\t9Ğ]v٫ Rb,u V MJa^ 9!`2 <7 0zȚ7(?1!DOD1yBdz [~]ty)]RJnѥa챿ƻ%\SR$ZR?zEIk%.sk0(zFppX۲M7߄3N9|Fҥ!;S#)ӡ=,K&aR! 4RI'9y%ЦpK޼SQG%TSOE5U ulU҉UgV[o5X lt6Xa%Xc#XeeYgVZk6[mJP[q%\sY ΅7^yUSzb]\K^ `+l:X\F0V9ֶh ?fRZpi*y#ieemdk#%q$Lm߆V5nMx9o E冯]ni׉䛾x`!"IP !dU&xFGMy~?xsM}Z/7uQ BHQNLGRlyAqPh|+hB Y[(9 ІS1B<nшt*A8<"1AqD*Vъ e`QABא}gD XN'HH)d2юwT d!"2DŽD^x8%ܠ9IJv. xQ2Vғ M1aGA:e, &-!2!DhkYd9LbnxH a5 癮τHMjVӚf6Mnvӛg89NrӜDg:չNvkqg<ՏD)Ug>i~H2BƯ9Pj,hB:c hD+e&c?hF5 "T!ɈFE:#:RЉ$iL!T֢oa7&'/iPq@,W|h~*T,8Gw8jSZ^D@)V=+mMUf?¥ϵծSqfE.ƕmlT]{;@8T61MuldQ!FF2,dEL#DX`B O`<)6!S ٨ѻbH h[mHhIGrclpbV!QFu g XFAw p[DE"& RLg5x`@, X2 bg/y%_VxeX%8RFk pB y_ \XH7N4THB=F;}GΘD!P0d`,b1|[  C yX`B rMUnkeθ慫uuυ޶S+ѕ/%+:8xHRoխƴv'XpUlæ;UMv ̈e7{dBN1lS1{T,~XA! GϘpb b1a&qH#W2qa#x&52 "|D׀ԟIu2p p0|WWl"]H 2 0B1I!3g.f&zi Y{Ӌ(|S0X`?@ <:hpsXj3 ;2Y#-0SoXa੽Z Z?KYbA/ˈsB'H`heX$MR%8 %PIzY(+P3Hx1BYPY 3@8"eА'|($ԐC17bv@RADj_P>$ 7@(KDXhLpDtQЏ2ТWYشDW;:A+JS|e 0A-L@BB PtBzmT -)8X,HC …`wX%2s8G 3-X"ǒmǷu8tli 0 0‰U8pNBЄÃXa4'>HS h*SUH"0,ȁIIHa$"d(M&Ȏ"{$I:)HGX%@Ƀ@Sn87C0UtF )\h7EFDELQnX@KŃЁGp/\(̃ E̋oqy˦T4 9X%H80ۓȼɵx )bx5v҂1x4p;?7 8\|鴋sHQ዇q\& ƒ,@IL(یH< \K<>R+-ʃh|Mp xI " jߚ^kX _X;+^^؅h?J$|\ \2}P 2Y\Z2 >LQAY02^UG?Q',@--i0ӉhR=a_ S\8fNh ɵi5Y"TdؿLpDYYR=T;-"Xm̨f `]Z^ CCZYUbXIޙa-̉ꀅhhjݨZW?{]u_VtUp@֋3pHmta45 [x ah X@&@׈xPRY] ْ5ٓEYؕeٖ&q٘ٙٚYYtٞٞ=e١%ڣEڤUڥeڦuڧڨکڪګڬڭڮZڱ%۲3۴U۵e۶u۷۸۹div[[[o[[X-h%U U\hbr\ 6b\܄8>hMRX܃АM=]ԭ" vYŦ}AOfO} >0]M$mM^$=խxq}%a^i[uE~HCߎxȇ Qaiwhsh8 L$0@1@.PCfǚإߡ)|pPUH\\ V Xp[y+\ (Mau1Rl8bg PP2xiMO+'ք$5U\e *$9*88<Ω<>?@A&B6CFdmk6HFvGdRcaHj&亙MqQFTVUfVvHWvKdeYd\Z6,^.<( 6X]ݙ*@eM7YahVR bŰcl>,xӉ!! 3>:dgtR1Xs挡頙{7~gS 1&yVfN"Zd)^Ni bH&eֲieFAgEέmhi阞iϱf髞>Pޒf 6@)"j8X׫j9JrT Bk.E:g~듎1携nmȇXkc F؇iCz ѐ~0Ц }?hg΍= MxUe L~Vk4bQxzjW4JE[ɰ℅kG9k[m^CR7jfT9 > Dqp7j]nnahe dLQ Ȇy_+Q~i W:l9?p8Vppy(3 67q rR//6v0ipwtRq_6(pf!/fבjQGx-^6fr r}i"uXyȇxX-cm c&]8O ٹM ?1>; Ëpu$tj߆NR@5FWؙJKnttIu͋0y@(+tT9>% ͉7 #CАIga ]N뇳yUkloGvnp_'n?Wq?T7sQaЅrWwmww_7uwwzwyw#H~XI鶫^wG8xoQgrx6nϊx *8(yvxW sJXy>yk ^yxy_  @Oxy8z?$}Ą@xjR@J Ps {{qm3\ g{x|H.%HqHmRY چx9qyJ|_M6犳e||У̭ѧ> kbw}}S}ǜڗ(8h/YW Fُ7!W?#!W7vOhG „ 2l!Ĉ'Rh"ƌ7r#Ȑ"G,i$ʔ*WlR@ |i&Μ:w'РB-j̙km)ԨRRj*֬+ذbǒ-k,ڃ\*-ܸrҭkεxРt];(A3n\UȒ^KH8E=G<.\p2ԪWg5ll`6 c;6nHԨ%6}b =9ڱ;ǏN<׳i>ӯo?{ pm} 8 :_E< ~J8!~F5ɓz!b"%xb#"-(1"5xcY1V=#MHьAy$I4D?$QJ9BLJ$TP$[zae5ם9Qik uy'y'}' :(z(uiQ8,zZz)j)z):*z*ji6)D:(z?: Ѻ+A,~Tү*n]rKRZWOEtv;זًN.$O:FC@.6MC3$TXa{44H3PX Ab/<ӄSo6B 1DC|R3g F8n ncڄӔ4=O8!3DLM:3Yf>iի Mcl@LS3  kN t1mѽMw-2ΰ~C-YcMZk>M ns9$# 5 l,P#GdFTW*?2 CRM?C H Bh/`n  k F4|%tkd `sI 0qS !# k/v zL@aCPR H2,8Z0dA q(@'T&8*ps` H倁 o/#cHfQ `aQAS,3:0 IAь,pyEr@DBAQ_kC| )%2@ " C~ @̐8rH. ^$$2U1!#MQ ō'@ <l"8/~ #W1P*$;-M1hAx7c eb-bA)2bð֎LB 2} r@D 1ra8Hb. B!k+P> QJ эBPB,7V8d"A M7¿0!S8iu aD˗"fV2HT!TACtRo{ ṕ Jb)P@p wPR Y*E8! qBxu8@!t!KӸ0 b  IS\qx$Eq LHrgȅ1k @0E=bFG !cS e"H""!_%  $"ҹna<c ^V:RE+. F$v(lD1"(ȀQFr bh/$`ȁ|0D"Z 8PDv5$B #m ;?|ENwؽfx(tS(R)x[(#4~/+/?1B0@6* (0Db4:Bm#/Z((Fn݆%,Ђ3±5ȃcAPPU%Ed . *DAm&.6Li(()B4FjE_ăH쩪T-?īNurЭ'ID*`>L+Vk\Dت[A~;B *F`Et@ΫDE$\?,jKA(BԀ6`1[)iD2|6M:8@Ħ9@5CB f<ȩMHl+C+/!9E3lB65d$*!jӺpj2(:L/`-JnmdX,t &!.N;7DkI@+۪* DP`mjfm?<j2c88j`u.ۖ +*?f֮>dZ꬝ /Mr)A4l2fPa3V/(C4DSZr6)+AHh|^L)(Z7>H/\LH:#j )3 "BQ힦>`3-CA@ Tpc5pA 6X~EBdS:/NC<0p$p^ B,9kS0Nj hAGQoB޸Ά87I8MRl#[Xjo>$X𦣖xkuUnBD%`񋄁a.AbYqOQ%2A0M$$ i ->߄:@q*u2))r)sdB2lA-ϲ+ -O-cC]rx1#s](3._1(>D<34wD׸*s$I,,nBB.;8*'+{I1D):;A%8oP(E-" s=;4B?43 .3DMC+,NH9QtEԬC2` GaH AC?m\ASt*0z(HS`-cA Ttz'N8 A@mP`>DBK<;A,\6[(LuO0r 5$ u3 =s33F<,F<7@h< C~qcCt9G}Q6J8Q!U'K=l7Dh6D %^w='wH׽===6#D\3>J}}}7>c6]G_>g o>Swų3~Ht~>D w>Xu>kЏ?'/?7??GO?W_?go?w?K ;ď柳??ǿ?׿???@8`A&TaC!F8bE1fԸcGA9dI'QTeK/aƔ9fM7qԹgO?:hQG&UiSOF:jUWfպkW_;lYgѦUm[oƕ;n]wջo_xqǑ'WysϡG>zuױg׾{w?ޥ.|{'-4UDԿ׿$K&+gt @ @BpIP ) 1P 9j: QdFLQE뤑g&mHǢ(1i EFb,Iyh"fah@NDZ%7WI sF0d!8p1F\'6!&E -@b>FT%.ށ6)ge@Ȅ9 ėfZ X0dDt._'bٛ( fE2f>HBX;u=܂ [~V (d:ݳm ?!x=$"T2Dvۡ,أtC rAGLn(r0&\ 010!B88] FMQ$SDbsǫ v! w `-& a\#g0@A1k И`V ":'P !@L04Y J!@' =Q%xH" `YRDN Cj5d1 c`OXwY،2aPzxǀb$ l _#@vh,L(@i"B! _h!0'u;<ل)/rY.A)#s -ȋ}s@`آ0F4wRJ4ryBAu.7kR&LbV$27T"?ZbDrC(y)BhqѪvۅDU5EXTFk%2Vε4Z]W}_X5aX.ucFvDd .O`_Hј5mHiQ{ ծ'N8`[~6j 5[^Y5\Aqq{\cbLcԥnt,-ЍʘV]*ImoL@d[^\zIzIpB\mf tU7g ~Ra orcB&q*d2z/BX HªDI XlpYcYceEEyooL(7v7 Q9'G@q(*u_\ xW=i&sQQ ;LAc#7 CrY={,TX]BAi <9ˆص &0a Ѫo> U|0 wHnOF7j5.D&uA\Q6  iT3!U/5i!iT 'Xd8FdAd8aNIE ]f 2+:" p b:` y  (~P" 3ȗlr#K I`+TM^2"Fd!(} 0)z)EJa "DZ0E`X0$Jv(Asv" t!,"!E-HOPI!GtFWB|B 2 Xj'kŨt2, c2 ʧ Ղ@9|kRZZ ?] k v^PW2tUg!824%J @Y:"*a |`cA8! -;"frl`wro HfN, - bza\@ ƦR|b&f` ! KDAKhP!rAb:@bj~0! R!>!>ll$39#": ?pp ! j0\ &32bD*B Srj{z zAs̃lˢ˦lAj hlA3kA](; fvAL*| ( fj~J aj>$:]u A?: bbhn$B'4=Fxl@S 6J r~1jIp!vp᥌t D ##ghAL 5$8"L+|b$RG" lafsH( NF: "QCN"+?bjE1T] P5%RO /8 v!2 r"VP7c ] 2)AjQH(WbU) SU-U\uY[/Z 7n!(e]57`,6avaa^abAێLc@c}C8S8V#τT#d*e'"c'VBb`;` `%rvgwbf j/b( iviiggV `f"i6kVi`6* al @bbAm!dnLkcn,vo]cA!8J w(6pFnd*7obpcp1wqq@(wtKDtB*tUo t+viA*K +<8UU;fǝ<"~7)#-iCSҏs}iIo9[޼1zSSӈehZ;֐f5qi^sZמ5]ꆨ 96bQ/i_$&mq<[w_{qonvC~DNp?8!g8#pP xq; 9E^p'|7yU.qUjY?ArsЇN!FO!t3F:ԉ. VԵu7N՗.!fz=k8NR t8~}!~{Oo]Gvn<v“f1v?D "ve@8%!JH"[آ_.P<߷b~,LQ"6#Q>g/,D KD̐sPt)twRoj$ғRi !tMmb~QwXx؁ "8$X&x(*,؂.0284XZ 8{O}8k20D Q8HzPXA&D b k`W1 j W( * 09`'i%}E"<(Q ` SH>؈8Xx؉8Xx؊{&  I3  qBSlxQ z`ߒ89 mu6sUP >X1,$5UExNp )3E QA8U gr 0 @ s7 <3r @ 0 XBQ@;#2 S@&.Đ=C0c a m|a 1& g"b ! l1ٔ ^3e3s"'d2A Ā cN\? "` f8Q p niIư :hi^?=oF}|IFّYy٘}  ԰[Pΐ܄y{W|ٚ1Ǒ旘隴 tYPyٛ[ƛ4Pٰ[$YY/8ڐBřp Bɗ1zϙ0o3zSy 1PJq1GDsJ*ei3Ba `R&Q(1 9H)%j y+ pa"DΠcŢP \C ?' gRJ  4 ,Z"cBH"# rIB԰ 9E!pI jC~0z-T Sa ;/0& @QwI23Aq4ITf:EM L7`}Aȥw:#` 21gD@p oA" ڊy~ëP,yj@ԺzPKJ4 aО[ ",VU9C/ *E iHko.s0-Z ΦO: ìU)a #;+>)dG4>pw#q͠MJ*{B,0`0d*$M[ڰ 0&`WJ~a5C.[#P'fE a n1d4({;~ &P.YH s /&tk٦ Hk-Ө 'V;B ׀ Z0^pC *À3S&p6j2` z ʀ(>¢ jQ(|Z p P( ["u%0  DI%" wP5U*ɐ z{g ,%Ð &! Y&uqJ#Zғ%,@  *R# '>@؄#! 0#-cd;쳝iq C" -}p"+2[\* 9@|gGzTnlp,JpQzA'[1࿏l ƔlȎȐɒ +p:C?Q"`Q ƘH 0m J4uiqf Plϧɒ & Y' L>c2@mĭĴ 3F1am}@ q18 +0޳!Vߵ< t}m A} uA!]|zhc-lTޝQ {McA+~iP6Ԇ բ0.*N(bך t|`@cTV10U0@>Wn]q;n|1v 0o.! ohwu}Fka騞 -CP궞1Z~.> ! ^.]Y30..,5Ls~׎>,`.LX.>Ӑ T}t%/wX,%ڐTNcСmPR2‚@0qa1 װOu O.dy .W12_mihB^fN@?RM 0,ޫ** דsy, Z9Ş麞lM2{-!u4ϰ - MUXhhOkMI?$ɞL`UNmRI1Qp"¯GoInڿ/?_3Wn@@ DPBq >QD-^ĘQF=~R/oJ-]S44&Wl(%K;9/D"KT,5/?YVUXe͞EVZmݖmF,Ta@0.Eb2AWndʕ-_ƜYfUУ(j3p@NmOtk\pōy pB66d7&sC潻zݿ2.(:'_-\ 0;O(dA=]x  *(Tjn[z pH)%YR'#s2H!$HixEh M @q `cʁqeGf&a!A3tP:3O=ț#hX0D*Q%RN ? jT9CJ`%#(Dc39AGNʒtFE5UU%~bQZ*=|EbnE z!ry-^p\_^3]eՑ ym%z鵽&Pz~2_'Vwpwg)qC ǿ0'E2% @u#Z_8 wcU7u@G : #*9_!􎣬"-"W&H*JҒ{%HMv2N 60ODeSD@&Ԩd*e9Kc?\E"`0h9Lb~qE%VbτfqL<`!Qf6)7aLD~XtY6NvO+@Ӟl`"! '>:Pr r!?DԡM%_2̓ #(4:.(GA:Rm%EiJvRԥ6cKe:SsŔ7lStH;iP ըMzT6u8KujToÁTu)TUP^VUv&PղU3\8 unū[Ёه>NYRհf]Wz$wZ YNֲ-Թ ] _3nz,hUHxJjeq-u8r$.ehCio\ 5#* ɅX D\B1b@4JL$4DTdt,͊dEJЂRL*S$(GFb,`a21f4Dƙt&(ah4*Ke`=((+PaNLT:%0V #

Z ҼK8"&[NBV^FVfv臆舖ri./c菾 j6q6JNia,n=iHqP>%Fꤦ+VjU.4>~7//hI:}`u`cn8z$lhjxɋ;^H;ti0}"6jľgHix޶tll"q8lǮq&m.0Xmv׆ؖgF_ږef*e`|q 6m& V¯$~(s꾟XJ eooq@ԩ[`EnDن>nQn HU#'p"7oX<:n 'n /BXg>mD!t)y8xYFҠFBc \bho"l3Y9hȄ@H 0ʬx l#lXb:H(Qf抪N hF#7 y~Ps }s6s@o'Ag1tD7=AwtH=tJo=KsLNOPuROEr4xVWuL|F^Put"4D'@!CXRnu4rɃ`ȂksH /v3 GD.k(i@e!w 3uo341pƊ'wS>pSؖXӇx ؆C3{_<^';EEa'^ͷy+0ћ_;Ȗ{Zz8)/zvo̖zzɁ:zsA_${3G#ϣW{wzGNؗrm:ܻ{!9goH&p؆tyu|@=ˀOg^ b޸7txH8^g Yg g}۸x؆_ }ʀPEg g7_j~OOoN?~_ Gwr懬*\q(h „ 2l!Ĉ'Rh"ƌ7r#Ȑ"G,i$ʔ*Wl@/gҬi&Μ:w'РBH(ҤJ2m)ԨRI*֬ZrJQFbǒ%+ڴ4m-ܸrSk.^psal0Ċ3nxчxl2̚7ۙԿTm4Ԫ]T®gӮm6nc7SXz8ʗ3]ҧSnѯs;x5K]C}˖=?!~7 PW Z @0 :ky$2!+8]CM!xQe"-xՉ"5xcO1 5,#AEy$I*$M:$QJ9 yas%]zeCYNTYi &i`k9'9fyww' *z(Jv:(Zz)mM)zզX)z@ZI*9*jPkkz++ ;,{,*,R5IyakuT-.-ኋbzz0N֊Ǯx3= aK.C=͖S5hg@<04 cAQt4t,5 @:>3BI[oqAdA,:kONk2] \3={*&7<;Ŵ6tZb^緭xYBiDM#L0^Q8w/HV5a#4m5cM,j 1@TLM͒2HL#$OQ/vۺ i#78'ynaY0v,Gd`n yA'|aA ϐ*;wO]AD#iMQ` d 9ŎT"90aTr"B8qFई\B6ƀAkU P3Le3K6 DHF nD*ޖU5A( [Ґ( BUC f bH%RX)҂P" ; bda, "n<BD$a=mF}xi! q0^40dP)Bт p &a@ :Di ^B(wT @yxb>R 2 _l(@mGB .M!$ ۆp4")0PAE0)9bN,C81`FD 7@[ xALP[js<6 mxv5S6C</$[ [0 F ga</ۂ#3/Y pdY=wJux!mlыSy.2|QNE LZ@%pI[baj[ʬ-\hBT4eDG׬Qgp&FN/\~bƀ, \$(0}aφwa)YW%hІ4b;!vBbmY!P/r, 5Btoa;YabFw!c7H;s}?c#, 9#_O>Ϙ);.s_-/iC???/ZЂw4XFk .Eƛ ȼN`O_B0-JR`NX B``iA (t -^D D@A@ 6h8A/#C@?$&a 6 a/%(@\8 a.!n@1PC2`x>0aF?0-!!*6#@B1C: (B "D:>0@B#bF20 8!<(΃hAW0 + -3C:pa(%~bAC"3"(C4bA@ W;&G޽B-DD& ?;*B,B$(  (--0%R:;OAV%D^Q : -@0/ B .aAA\"]>fBA;a12" 0"9Daș@j*<0Ɇ: 054GDiRcZt;eD]z|<r *F)A2+GmEk ʉbMNADmAtAC5`}jAכh(&ܚJ ]AC"mK8UXV:pR.E B,~.DnAX$\LntAnRmE6$ʃ":.T6k-a/V62N/VM+v~/Ϣ/u/zJF6*)hޙ>K! Լ2`pAPA_1D 'G_y}Vr8uqqPCk Gr.2/.ho/g/2@s1_1GmI<䯜0s1gh%E:1_qBP3A?qRx/p k۳&3U@AOAlBW3S3/RuPﹲD;DEC/FoIKA@)PlP444V?K] QtIPtmlYȥ&$,lWQ<ϴ><mD%rOX5 c5(6W;4hA,8X,N /56*5mMd8^1CŸ_''PcWt#5[6gDA -vgoe|B6klF-q6n#ELWnvmdO$?ajO2.#`6ENb"wn@/^@+P7/\+b׈SDHMV  !BPt)DTĢsBx 9NAsj >8)HyLAE::D+<)`l_C8ɎnB~znk0hKD(YBpB>eL C % $rE8HPx9f)CbkD8MTy_5C(23A8lk7D`t>T<\o2^H*;0\CO'E>sĬ'>7D7(O>C(=O>3e>/9w~C2藾>ꧾ>뷾>Ǿ>׾>~r(7>]?ӏ7?'/?7??GO?W_?go?w?????ǿ?׿???@8`A&TaC!F8bE1fԸcGA9dI'QTeK/aƔ9fM7qԹgO?:hQG&UiSOF:jUWfպkW_;lYgѦUmW\Y;n]wO8)oҋpaÇ˝Ni1qRKoǑ'W.Ylp7M`fiU'YAm#Y`+9[>>l շ*.@Ȼi:e*JbdĂ x@Xk QĒ/f@F ~  D) @Y8J#nBQ(!DBDΐhi!Hr]bF.E J@$V $Dh$*HìV 9.@P2NT)b)JO4QjJA0%Ɂ:h !PP#E_i;RDSmo`꤀C @V%(p[Fd9G _x wL#*f@b ")(_0%xg dc\&wIV[j0H 241\FC&yFi5Ld(HE0(e ` `b$nM(L^ 2!3h('Jylvp Ih"%R 6!ւz%3 '!BAh&;"O/j ƆZ_ah!aۤUȆD=JCk9hs`H]r,lh _K]r)}2*8 =$I`n XC|C/ðLu H@Rt /fq@ },  0IMzRgb"bM˜A #`AV XsQoaE11 \"fD;ŠE/[bR ]^B H0 *B⛄bR,%p XPu'5E=rq h&H ZX{5ᜥ.~-HeDWଉZgA&R1opCPq#4>hk 0.@y-? m}kƌc!(8w#4 @$,-/1K bDc@Oadx R&1S 渗$D9Ӑ09c')#kJq}7\33:ڄ%Al>۞'FzEp0k&P^)OXަaYYqv`W¤84C$6I1{A8 XV2E^6# )W$q-@ v@ DM(Ɗi D؄@Bf8a ؄oU%H\Rc-@GqLcAjQԥ6Q-ꄜ@ r`-i#bY C#0ڝD>:$N,hd0ae9"v=GDc(h< b&t"nVߦaHBy&W_20a@hu_In VҢ Yйl Ob T!(41^EbABB{4*"aO ;>] eS%Z(p"5œHaB)lj!E;MF+ELC;CO!OuPTMX U buQ$5*RRA"R9R=URASCSETITG5U#,`^11|1(hQmuQoW"U(~WqUX VuQYXoXP)oBZ (u0@ZZ%P555^^U^_N\E_0_!0uA,Aa`UB@b)%_1vF"$5d5cGD3>6 R6J e[ebd  deBgffSVBfmhihB$i63b hi6k #c/66 HkB +Ja" hkUbǁnld bmmhAb4D6n`cd  N<@+¶!8Vo"m JB2W!Ft9ap B0 Bn7ou `Au nb Pw!jso ivW T v \*$oW&ls-+at% A)A*%`{W/Jw Kyp@$&!4-8@T $a @Vh($i"Cw x"SK 6seuAF:L'!5#‡ XWbCC p'Q Bzb%b{`v  2{5v3@nǘgb  'F xoȲAjjC؈ Ut(zD+ x \z 1x5%« 9&tA "sxkBb၏¯m6!$a}! 2'AY8nBn78"xv88Auo k xalV!A ZK m2Ġ+NN"*ye%"X 04!B"DHXa< Bx*u^hy*W$`osb| p<).xBz{4!+2NN R`nBL *r 7a-zbq'\$|ŠC!w 8` D":x) ;{;cbW'a d*X b!L\O Fz)a8yWdAحo":-4 J&{%8`K|%᤻;![{(x[ZEi{{ -z#y>0{#v w{[ w9"{{0\{>:D@E@'b! 8 \ ;ⲕCh&9u/96[l9`b^i~Q#J\ RU>,M ^ܝቁ#b"Bnk!b5wA3WE`7k_ǡ`ʉ# !,H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\20cʜI͛8sɳϟ@kzxѣH*]ʴӧP%իXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿:$SB̸qW%˘3'$9@#BD@rB-SneW˞M+EmsԤpP,ȓ+_< Q}")@ͳkν{G"$GϽ_o3ZtB? A߁z7")8P aP#f؀v 9!Yq(x,IDdGk䐅 i"Y$#6 IPEI\vR0iP}i_lJGJЙtix$%ZB 蠀i衈~$(&*[KK/-R /.PK-jꤰ*a\:Х*é1K-.jΪii -䒋Bl-͆+XbK jAvj(FTBKc,Gͻ@@rK",zP1@-rA駕-,L,\һb;,@(iA?ds/ 3/P[$L0ody#$ӑ1 K)Dq43,LQm%,-v@9xDC6"$aobxr/ޘg^a+)4DK){@4crG@9ʚ/|/pR@0^,`2$;FW n/P)A!-D>B @GА, TtHکA0@7Ǒ *EmL ۄDHH\%B**)$BW&;3-@  ` @`UAʇ @[pX,% x[,K02ebbF`p_"k!Z ^_V%%7ɗKͫ~9إfBY-uGRD#$2q%.Rim]ٗ.&*[kdEvI < cfkj 8Ir4id̍f:IOi X~S=zH " !#BM%T1QmEPV(jf|-!@ \@  x=\u!HA'6`HFS*6ˍ^%bQ QmJ죇Ny7n@1MCXE8k9+W}`-FHi-lft_%gcARB%+ 1+Q-B/R E.Ok?_YMLn$` 7~. h?NC8YyYϼ69s+9c.t7FgғOys>uWD/eu\#a Fl4Yk{Qyٝ m-(>6F]¾3ĄNBqaH[> )oǽmz޳O+SsS|K/ww_~{߷|/Iś#nY't]rsu (u HuhuuAuuuhȀ!#%('HvЂ.0h43X4x8:6>ȃ@?DX q-؃AȄ>;8(5H98Gh1Bht@aWt w0ql؆npr8tXvX+Dwjg ))r2q `XఉJĉTM(q &!0S-qˆ7SVȊ e ! ^A7!sfv͘Pָؘ(qCKR DX D0a*@ K aV *0ٸ(Ȩ@[Yp>`?^ 0 ʣ fj0XӋ*q !cgN b@j0IPaet:L*A@'DLB@*Y l- 4 >k&&;K`A`V aD9Y@@P82#ňh\ "C)P4`#?ƓYN:Ap;U;# ؕ9 2IJP ` 0q9Yyٚ9Yyٛi5 o`a fgp4Ai6w{ip9)`EtP P8$#GQF o&S1Ip f T+ )kf`WP yKF ʘڟ N" ymr5s Z3r2b ,ϲ ~/祢 pmQ)@5t@ N!E  ,t d ϸt  €yAt HK)aGt 0Z3F@6R$ P6FʦzD` R32#03 qXzI@,qc Hzt +q0Zz8٪ ph T% āSjSyRʬO`X*.L6! 0pш *0$ m 00rOH*0 fh0Ͳe[yE v!:` aM:8zⰶ ݠ6fT3 0B(PS3s:phr9^&Sb00T kE;)IjcKאp`T0 Fⰺ19 @ `D@sP)f26E gV 26nlE0Tk~) ~ " -a &|`E{K)L) ? #+;pO6$ a f W{kX y{B+ŶWt )VP0 `Z;QA>&cgT6 K$ȸ;řcߊM RW 64#FL=\ | 8f /O<AE#$G q[ nSA?׫*TKV$f Ȑ Ƞ  đ1 8$\Q K;ú CEսW͐c 0]{ +,;1^kA (6,p^Lb$ рB/< M$`i6Cq3,%<L\$ LߔQ E eY;Tt s6 ecPb ~0 @@ M5nƶb )x#'PZCeLiZhPb.2 ?/,1M:M\t)s;eѦ` d3Ol! ` )+ 1MJ ;#7p6_:{ Kv:Z ^FJ1FS)!#n%3 b# hؚC z ؛T D3,ʔ:Tʆ LLkȸ 3,P+@A jߕ*4/T0>LF0Pv1-(KF-:.mM9`Wp Q$eĝT+~@y) 3.| L*Jْm0?BT~}=ؔZ2 L*p gwGg 2 GK53#Q;v6tL0n.|zNT!v>^j`.gd"9A>됮ꗡ7.: n= ԯ|՞~׾I /R^}>I~ Q^sgd}Œ22o~(؈FtU~ч ^q0"e > 4_.3_:Ǿ9@>?Do=C_JzKK1c`+r2ROnS7p5J \{%a l/(]jv"ssSAok:b73 _4,B  &(6^S9*ٳk!!إ4Fd?=e2Fn ,dGy 6|/q 3R]r c.TqQ  _)`i @ S)U1D4L@@ DPB >QD-^ĘQcE_&VRl KUFLV/' cQNcHBqo TW`4M>UTU^ŚUV]V W.f 8 21?@ Ĝvk!0"YA@.|A`Pڠ|yOT?p8lQ#p!6CCH<RR2Dhz$/d0htc'xC߃5i%ʒdȐ1 QL2!"@@7QA= 4TZYV ;a3Q2JO9L<)2l&tHQ3}nu)戩1cNhT3Q2u.hoʇfdYvЕ3^8Ys.gqXF.xZ@kI:d<.$GLW?O p R.DA:Ԥ䝧ʢkKgR"3zJWDN*+=iMg#vlO{ʪ44`#s'CD/'^N5CPzzH]WKA/E4{q )-n dhFA 3k!l* B/G>jn>=B|D!@$! 8eiցB zַu7r L Ʉ@RP54l 0HqH`aR6ĪеX4CEoz7 <"B 1@ܐX }PHB 8`BD p)rY!`VL@C pAn X/mF#gyq2$ ETiֱ,\׃H~ GV rA\fl9IkhQT,`/A8AbY8*R5.r t t6{0aow m@zዟxb S&CD 5E {==BmerXg`A \lZ0Aq<<BZ~$ nr'.xڳ~9pf b@> /r "/oEN]YXVC"^n[/7& p܂-|^`|Y:ȫ+r d &O_ B Efyu5Y2w>tWP@1󄜷O:bW[#!!tru;E_'~ tOGR8JtN*S@uY;u׈˵ VHoY,}J^۟Щ&D Q;.ԍWS}06(FG#$Ç}ÍCCҀ@@id=a x X- ;Tz1(Cmdq¿ȿLL= LTEېQ1«pR4}lQ;qLLppW[êYFdT>` ۰)ol\҉X!a~Ys<q|4NCwԜ6s`ж| {4G4)ȃȵH((4-;LI$W8v ;U#ɘ\v4*Y`H,c{Fʪ-IU8Em[ʩ~lX…xXbʲ:t"j]ܖ"HXAYBKɽ&A¼A4|AT@tLd칾AȌLUT\̹L. i(" $A@T ٢~~A\M.qxI`pDdBZ04NpԬiA|N!`^pBx00Bxyzt$,K3DcNoD+ lXR ctPK Bn 9nT iPq(ܰM Ay(EЏ~| -#uC8ycyHѫQc Z@ "-@Q8FJx늷 mmppXSgRK҉l9x0Őy`6ug`Sc4 S:u>P~hOy@ D uІipyA85R6}"\t{}qx Im4ՈHT0rSe[d `p dUdB>*iX$$xiN؄o 8T;diU-eS_`i(`WgփU*pGqM1׌ibt,$k@bsloeX{Ir噓YJX ;lQ82X(5#Aٔ=uWሯnPVP0W_6*ړEZTZiH +U"o脂1i풣 [U5%. -X:Ce2ۻUX\8XPV8X bH[DuYf\ &heX@Uk - "D ko@`"^@a bմ }ѐ,0^5ȸx X(_(w݉=}3Z [ Z7X ^ߌ_Šۢ_#`'W  `3E6Ɗ˲3 GaaN>e6bGa)#v_ i'bHE;*._\ ^*2.644V6F6v88::&b?9A>>&CCFEEfGFGIIK6KM^MO6OQ&cR6e/SVeIUvehW$ \e2k_&a`]+8eexef:hYN3՞cV]Xf@qFQ&#&#qfvvv'mn&f^g۬g!) %&x{"gJ耮ЁV7}iQ@hp/M^eZq f +c6VnVh&y\ņ^kpj?@[iԟ -BZ@jSEi(6=,^lhpPe@k H;8iоW٣58=z gYuW#՜pSnꑠeǑ>꥙ͪfQ d c/ ؄i'0al0ǥ0>mk-l`kki kxiB91.p+dXdj엲ưBiV;[oxN` d^֋Ul.PUYX@kpz} b'08YPod@Unxox[nM`ہxp`k*&opD5/*Sی0TH7:Nc9YVd`r&Omj+E h@IP=уH˳]r=0Lp)|H0H0T89Uh@R(D a}:y$.kr'ט~ެ[_phk5w5YbDX&(sYu2gL[`HljPXPX8bcnkk1%@5e]H@ PVY`T F &~) ɆيYpn4UЅ(0E;VG*Ոa@H;YyNt'7Gzww㞪)犤k{NPs8sЁ28zX+0H 9EjioDozj,zw<8X@2op+Ps8Pl] a } ?Ǚ7 2 G;f݁X(+JHowvH5,ӄ_Ph?&͒60(Z XH W кTOwovwoƃj'X07^D(EWD3x˕p`-f|C/bF,i򤉈"68~8+8;8K>9[~9k9{9衋>:饛~:'~ꭻ>;S.{M۾;. ?% @8<ۏW'oo7 J@A1nɟI'>:SA?ÁO X ..G*a?A64S %PNAF-hЅ1#O=[bY(E}d<m4 ( ?C5vЇALh1M2S,5K8TX5yPK#"d(X֓cEAgl=[%95O{I@AД&4O'04HO V9lA PP6.emOp>E(@>H}lh3P|B5 (XֵtI!D8a,c%Xlض`5@ $p:Yhw' @.a{Xjbrpt@Pa}m(1Sb %c,;%s x.?Q>:qM2sB Z~+jÜG υBmYndcX9SP8CI7?]\5Om=o{-C z#̒p8 Sno~.$ 48 1 9rX\ )9O򖗼4y+\΃y<:gn΃8|դS}i&zimI]Uzđ<rm 4mW.xcW pp^'*+J񏘂[;᩾U @VŃ)z#tQ~F~&cMD];'aJOfmr*xڷroFkiI .WDJjD~D!SnM ;98i蚴~DǯWH5x]D_DX? - )E( $,i&`NO5><8YzD2^nat>aDaa!  ROяG &"!!">"$F$N"%V%^K&n؟2O '1[,0x(D``(^.MX I?":R%Vޤ;dVv WzXjUڄT%Z:4EY<\. 2le^N\e"=*K^.\2eD|`V~Gb*b)rW1EeZhfQ]hrP4CdfjkO?مnGo&4< (;h;( ak5!a\D$ElXqqj=GcD DJJ0SpzRNnƅyF[mg^f9hEH@DB<.u>hhI'DqI1(^vK" :y~qhrN g$l6$'- Fh &0E>(M5)v*%>KV+ƹ AFBCxAuF/BNV/nvo~//ӝzuE !֯/&5tPcҾn[@ x:-M 0QSX€8jo0[Tp0@  E ϰK- Ll^ $f00!]M[-10n$zNo#5<!$"d[ؔ(B1_OLWr\$ O 8d=010Oq0a28`o_l\KseXrh-< 88TX2D'2++2,Dz,2-r*R 1NQܲ/]d2քc :86-\#^KB``MZ:"E8$*2D\4X5$4jrNЕ0Dh5`.MCGF :`C 0_M4f.H?$4s::54pZ>h3H4P=DT`0OۃxLOCz2IH15;B3vu<ۘ,T4uI5;ImD5 tc^eMpLW]5NÒ:HA\x<Е\' كDKF uPDb.tGG=eI87iIBK*8I|@ş0(@API,QyiTr,-u웯"f4MP 5DD1w;0 ,2:J@ʂ x* 6l!E '\8)x,F05DFC=:dMyD0;E1C2`7yS-|Ȃ x+D]+TARZI<@YIdTy$D0T^tK@%BD<~IlzA K hMD1 uLáO-FtApcCA'C^,AbhMH:O8@7d`E7bUy{hl#\x D+@05$DEM8T0XÆ@DXCy4Ĥ4  #E.6y FXx3 ̧ 1BHd B dN&tSIԁNh[D $NgZ|kElA IpZxD+A0줤l+x@|/4D3|AD(XP|NaV1CHy@H\8@(@ 0`5$頉81(fQ\s"c +eK/aƔ9fM7qԹgO?֌FpFSTUWųYUςA[$`fJ 'tAT07K&/Y 1 P'9)M-ֱ\&W zuF( ֽwo߿VPݵxR>KyryؖX PYUk0pUV Yf+zEeLpp(eVpkƙIFnaϘ\x!en)"Yh`o%X`= aHϫnᥗ ^lQ%l'2mFp+HkIFlfpȚ绪Ƙuz &az1ϘaYs%YxIvQG!%̸ J`e :YF#'E\B[)lɾ"A=WE/QrfZR]i^o~;p^/]}֑o^Sw^>u^/u@_~_x`Z@~_~c t!(0#aAB'yL aC&, j8@ D!q{6HP ǖE)N3@~ 5@(Ze4Ѩ+"P\  M( \G=R@#c`@b#!FN2vdAA p<\ )JE24%H_#`Sv$-qٵTNG:1Ao4fl)c.San pPAf’ُoWΔ0!t\yP@ʕt3!=#N|QL A` iCa{ K;thy>MDlNqX@ 0@GʉJgSQ&#]K.B!~h!\x D:S. )MIGD Z .e(" 6e$5 8Q0+FZ{f` a + [㝮jKyuH YD0"@A& %D!yv'QR4>` d Ú(@(-!ѽdõo``PCnF`zЎNr.toLE,^/Ax{_  CB18,u #K7`9dۇA4ioHL#6я"(!'pF<,565ؠ6&xqg腌aG|^ @nj7µI@Ђ,| 4%'<#:>kJ#H'9+!TsV`yI ukR5)0{w4/@+xHƇsW/ܓ҄ 9@.a4Dd'F} pG@08c1 x|o=px咧OB49E*z1 ihP~ oyD||q̃WZ/z[Z ~TJTվ\ͤf'h|҅~&: @0'X 00/!⏆+pǂ" &0u/&` .) %D7HDj #b4C32z: oJB)a\mlT !$B\h" y "^ b־k ) .pp4n:4nh'' q!1G$1q+2"p ,Ev8FQq-R aZ1CG*Bd1ukJ1ָSv1gJqF )0P ! vuڐyqKpG̓.qvqA'8+aQ~ !1vQ 9{o )!A( M'!p!!в$#En Gl1#L`ub bw >"@%"Bc"`"w0!Txї +Rz'CB 4l $ p D+p|]c|c" J* bDY(}X` 8,{b;t@"ʴ "06#& \3=q"w4l` @"a(s :3 $Xbp)B{kb%!Aiw9R]#95e!b7(!#XFNpp`$Q1"x F֊8~a(AqiKXyI" СFR 48 7RE}4 &"+lHHWb$*6#@mP8U"l}Ws:6-R#s0ށ*#y"!!}V'0̥rC(Pfa.+X CXe!_qB(q!z@Bѡ)/9' h#Ȣ b5AzY]a:ezimq:uzy}:74΃a: LԁBzH",jµxM $<֪:>c` Z!w< ݺ|:!"ՑB"JG z/sw%"J‚ǥЁ{zsgh ჳa} 3.*`0;mPrN3y@"Tyt7Br R$,B `[vd FlQƁ*BJjٻtuP&a4Z{篲08zS.^hMM ?s BraU=i!I!>ga0wQRc\r؏WZ؛1'x;کȈڳ}"yCYb#;=!r,쀁D7Н'aV!$ aD{W!d2L$-5"A!VCVLI+ x'p5W R =6eG

a bM9 B}8>0.@\amP"% `XgƬ@"dJ" Hf %bH!Fw)Z:3BRt8>4#,/8]71z "X7` Yka~Ѫ2VW D" dq :[;!,aC'"fPt%_>"`( :bP屯!="x S ^ow` Jv{<"@dN15ڵlۺ} 7nZbf@/w `e >8AkBS\PP hASd 6A1N;ݼ{Vlsg&D]q̛6̡O"v bG˻2hM/7DPWRFi@6F,)p}`w'` 愠UMKCP)TC~DANHPH3-=&\dBIĢ0.:EląA..`N> ey B BLJe/'2dK0V-b/-DKB2-3,vewzkхP1Tg9L ,Ћ]Fbc2S4rIImeʪOɅBWP3/ j@=:ċA0>k2.pKFJ.@> '-ЅRlX o΋,<@.8s/rGF$2|cNSk‹? d.." p&,e$l2 ,#-Lse˶5A(3C/ԋ.#sJ/k$3Rl4.&S<⮻[vK-) dr Ws#6-G۽w΢`x#DKΎ?Oo_say梏Nz馟Nii{N{w{゚{qN|o|?}$-_}S~0~_ AyH Aޏ A"_/"H[?JWhf b?p$,!О$N8^(p4TabR8C;a z$$*qLl*ɆEph4آ"v$B|2q-N|"FĘ9j"7R 2A <"hL#bŝ#pއ` ^q:#HCː22 gcOaCyd] 2aAipƩq?Q2@AƋ(41 В $YÎLtl 9̵R!j-GRQ1̈3qtrT2l-,f aR #13A- Ѐ t-AЁ&[P-pG'ѐ s'pq! ضi9bG6!Ās P 1)Rb8rc1; cr}r!bʰk` u1& 1`s0nā #8;1 - bVp+_rt@ ppv PS5B C/łqo -x!p$ rw16`v`  K}4pQ.a#!,Aux@7n DŽ߅y(`%P(p&h~86 )ZbQ#Ap яR,lw B Pz!'Ԣ`Gt q'p@ aL@ gxxT09(]vAs,PPpR @ѓkua"!f`q`i!):?& M"R ncn({q07P s0uq7Y p(`4 :A-r0 +^R)⛙B P,@ "w85Gu"+1cJh ( r B 4RW,87j0 ͒|63 r P' A Y)Pilo9@.Eٞ 5:3c3ã5⛠);ua uAꒊ378)3Ř4/q(z)R'ҙrz55O1: ( (޸k& aB(J3dv=X '~# 69}i&zP# :WӛŒ8 ;Q )HڛxC<#9=s*߱ nZ*3՞j nb-܃49A.j9' D33 pb1<2VSٞJjzʮZ`lQfVj=oe,Z*JZk { kal Hnjub* g +$;%'&˲)kkf% K! j&v0?vJ9ABpy0?`@rPoPsSPKto q+kKv{+yз!Q;ѴR˸nK6p1 ۰ Y+= J -d4+k꠻iR q+˸P;`<+΋+〺[h3OR+ Л[< k;۰N[+@x`A ;1 [ Q,ØV 7p@3>/;/ ٻBL N3= h0D'L0!|F\ 0˻L,OB//dB K'!J+>t4'|gPdlƇ2a&P}2a=d]Q|6pR {@|@pM5$!ѥΘj Ofo /OИ=[p\ɜp`,R #p0Ъ  l/@el)kVm 6- *[ 0eRD ("RPأ J0=pV6PNr֓ӌMp!=*_'PhԐM$ܔaUоf׺x@3aIp ;+  !xA оBR;lZ-<  J-nM7\!J'j F.n6B! 2+ K0#Od- g7.D,9֐M2喌Ka6].N՘qJ$mLRntl븊v9 k D` R q iA [<{pJ*z޿k 쳳 “%/l>/~N9qKPS>o>Q~.~6L1n<]p@ N$'/~+o6/D` ̄ . DO.}A/NJ_@oWYXO:/8`=.DKJtD84L L-\,@C7j (44m P@4DK4uKlDB,,a': SHc/} d@n+ȆzJ@P)P)mRt&AT!MQ¦Ale"|tA&J,; bA&4P@t3B pd:DLL1Msm),#.2LAd&hcZd-Wf-lβ-Zyʵx0k=0,ImrKkVLݒ sPKr@2}r /GOP.r-AK tY~78#:OA 8?X0PKm~KH2 q ۠b@Z*+ ‱kar B&8h <2P E*T3f ! D 5pY?j_ %D )H2A?Lc`O(K( xrB nJ!"*dAeF:2-p*m(y0 TPA R bAXܫ,48 Y` Ը PUJXb"MBdbx," 8 ,?DH$jcB+"+B.h-GH}%P`b Z H9Ύz),h@(wQ/b\C@"ggK%. @@ 0A PdX3F@$WXFB O,,J#1eM:3E]A !(@8jA:X=:$$;U!:v!NRr{5pI*)\gL䃚,$ ~@6Uي!lH6ZEd@N3UHγt|rp ;Wx q BIf"E܄p7;UH-]qci pTd`0 l*b (%zɐ28Qy<H5l!6Yċ@D M#8T,$a@3Q6h`\0@rD#بd9WO)vYU2<) TTKln8f;B ,:;H< y,9"#!5N˴yGM"""BRfr}+s1vrl##2U)(Q=w!!fEKZ)~X=V6DiS־mg;~6ݐq!FBn[v=j{v# lrAxM@8'*p\1'/*) Đs{r#HW10W9DbӼ6s?yo>tΓt3GyqtOW_: et*F;!fwBҞuWoz܉>wh7{]zޡx^}._א[򘗼I ?vfvms^ڟwwzyޯw{}מ߳E{җG=at78o`>"[~!}h/䓿~)??~~~@ p  x؁"8!Xxp ం,X+u21`%,؃A@уDXFhpEHA0%3X Aj% !^_xJ8dXEh(f8FVG  39̃Vfn8a~8#P)0s'NsG Sbr(xn*B$"KP01D2"DBO8" $x"R '؋z`DB,юC8 Or+-2B "ZINrn6 ` :l "9"I  +r-` lb ";#9Y $\ˆ. >H@%]Ay4H4yȔPɍ0`Q &hxXOY& ɕ`dYqh|f yЖ"?d( =3 ë: i ՚0nM zr?tC\!BD>/ 4`ʜzKj ߡvAa1-; č禞.>.M_˻,y~묑n0߱W; 퀿 kNa P^Cp.N/p.Mc{h@ .!PRǛF>"I@.@!X/#,2H YTp_2? oZ!P^1 ^BF_6U8O`}w4GOIOq8O:_BQް;2_>3F01=  %`b/j~?pPP fZ:oq!$WO@}!?B` Qߎ+cHg OTIp Pq5V >]LY-@ PU (XS5oDLY`.@ W\1qXaZh4KgQ8!m.^l1d\Բ  $g ơlQD-^ĘQƊC ƑDRJ-]SL5m(N=}t&,U},$W>).\I` ȂVXe͞E;3]ړؾ^v+T,cA"7u |l\`ƍ?Y2LR&_xma׆FZj֭][vaRAŲ}\pō?R ?]tխO6Dc xͥۮG^z0W?a0@t~b; /T"B uԹCg-"hN Gw&~>1, eD(rG1[hM"qI~<p/@I/D/0J :B]pH̉~3N9bz10>6$ Ad1:q"~d> r}dJBҗ.( "p/㊪Tf4#aj]hG2Iz&ta2zXE"!J N0'Q$g>O~ӟh@ωhG4i H4ʹ97N I9'*HR#a/Z"b8w8IW]숙WJ*4>O ͡  * 0@[bZTƨU fd :MHX+!S:8¡EUIpVACSskbXr.]䣚jlfXvֳ gA;ZҖ)5mjUZml-Zֶmu[斷.V}\w5nr{Y#]Sr_%]R!s3f߅?t N: xq\BUy޵׾ly{׿ў@1,򿭭}}`s 쳲~/PA QÇBD0F #H7We eKx@ ac.t,bW.HMJC]F2b}u{~?|G~|7χ~?}W~n[}א&ʿbJMf)yÿ?$@Jqk+@J ,b6g` Ik@RJ/Azp7( { e%aH8‹HL A&O0 $`{ةPRw8^,й)x@(5,CƸBe`,<œY+TD nȂ ̠" I$@b%6ANl(0U豊828A*SHuX^D8 nHaӢL qb`M'Q,`JPDPiш6BhL΃$` F;PB``@BZg+QH lԈ 0=Enp̈7bXYиD&xCPTYYàpr`TpECxɂ((ɲHH e ȓ\6jW҈XķKXK oZɈȫ2yяlqLmxm<[8"[[D:HJZH'_(b!ǻ 8)Zh̫D 8Kud<͂xƉx lpE 4 hT̚:$Y\cXͻЅ,`:̈ڌypB ]Hɓ:? llHk$lʙa @^@ad :N_ ⏁a9Մȥ@uόn"M =PU1mpXAN0ֈ< }OP)шF7 ]: ZYЂȅeQȸPGk?p Ӌњ8R&݈#҉eS+,-]5Ip@\ĴE\`҃HD &TKMGEpEu]]pFMSEU\؋ϰ@UcSmHTIEU;M^_`a%b5cEdUԪghijklmnoGz :8?9 WrxVVhWrTW9({ysW9W:W|sWv{Wu5W؉؋،؍؎Xq5fU&$ْ5ٓEٔUٕeٖuٗ٘Y$ME=I@ڡ%ڢ5ڣEZY٥ퟦžڪZڬZ`P%۱E۴Ue[ڔ [ۻEەx[X۞m݈?ۺE\u ŰA\]\uU \ʭ\J]\ܼՕ"e ڮӝ&սM\]-%ݙ] ՈY  ^ޞ^^UnL ]%݊x_N]a_ץ Kсd -(n  Z{j_Y@`ĉy-4@YqG& QrRQ`F _%`F&s*4]S蚝^Χ %C%m1/F9/gc``brb07~ЧXwb00.@DFvvpvbI-2`y@> .Pj:F q'ʔ4 v%eQ$ AbHL^1//.nʋb.ݑxqx`02e0M@yhܭa>x,-V̉H.r8(cdH_fȆMj7Eh-Kэ܋x8KKn2h/hމH x΂Jm 6gI`es aP@chh{N4hXEDȐqІ@2V }c2Q"^ [B&xJmhkP^ߍ~@$1sx91džj$QceuHwamg~ /c2&1Nl7Al &v m.gN0&d~ u~ֈTT&Qj2J5Y/(]NGPnQlQZ Y0'𿖌kh 0aQN5Qmy5niQlGW׉@p FGg` upb2/0.y~dN v1s;!Pe`e$wNWBj\9IeqUq &q y8#\3yr?~rqtx 0h a.Etul/ OP/^ A0L Vg`kD @܈2ّ4BvP`}ptq^kps͈xpǵUKPWnog$cHwXvPmz0ΓRaU h~(+xug ``S d:TjItL&- wH h % OlŌR*y(zz6zzB0;6s h; 1fv EYh@f0 N } ?6z͝؄&ߨKlxq̂߉.$q0$dyVCoG X~h4iY Ƿh <6_y@Kqp8y2b4hcbRpp*Ixc(4_ nW&#>/&>|a׏< EN!0R`Ō7j/Ƒ,Nǔ*Wl%̘2gҬi&Μ:wI\k>Yr(R<)ԨR{A@2c]V j ԁ4Iyl/Th. laMFl2v1R9Va1Q7׸"E ^̲gӮm17\/kwV_~Ef$mΙkǓ/o>}慻`x=EMLAwx}) .F@J MCjif  ~MR$^%@Z|TSH9#=a` '*#u(hҐIsQGf5Pi$%]z%L@j4II`Q7FcA!ؚ}'BVdMd)*H)z)jנ  v(Qe)NCBIu_83nD >jي* ;JzJ4PQľd@M$S&qkEw44O:{.fj@ClD@i U @  qW/$ [50`jkOj4{yƺ5LG( / M3pRjM`cX5;%AtUXB{sGO [}5ֲLЧ} b1% 0WYDq: =l8J6 ?|1D9PK8[x.mE<$.?3>nlw:PCDoF8c4i6 T 4Vya a޺FhHK5>`zEpF㈒W%2p sUm&Z(C2r!G1F#60QGt[?9Q{D-("BqTq&rCL"c \E2\hFR^(F$(C)Q<`>XzcI$.$td`<&2^$ xDz 0]br"q7˹x +au.:At'>YQ3FւZ0"T2t *ш% Uh1ltC}(H1 0}@8:ԡ})J9*S6͑xbGn*ԡaAkT)*թR0B`p

my>>2/p!hA `4.=2&C`} x8  Dx>p=eb$(''l11 0N(gnQl/k6B Q v6PtI8` 1"8)e33&(-  1)҇! A8"zBGK8XxȘʸ، g95ԈPqxO؍X 1 e(h9H ؍xH8ܸPN%8h ɐ嘎 Ii uNy"yN$Y(iN'-Y QX9+Q8"ɓ ) 53I9;i+I#ɔ. 0)=I?iACYGEy] MO)QiSUW2 e sABq {11~'9 q91٘)Iqvvh'H.@+p €9yKɖY[_a cɚe)gII 2.Po Aʼn9͹ٖљit-"1p dyA )I㉏i婏穞ɏ鞌Q#YyڟQzj J D8 :ꡄ"Z$z:)-ʢ(j4ʢ.89<BC 2zED =Z/Nڣ8JЋ[Y]ɥ_*QagJqsʞu wjo ᦇNn<0ڨ:Zz!l{ש1nQt2wq2р ia W- 1gUgs@k1'@ {N mA f-wbr! ˀ 0eR  )n! @mRũB 0 xm "i!H؀ ;}bA @rPh#xx! ;. 02 ~ K0yw1{-92 -2sKy;M|G663Ѥ6!dA &0I LNN< ~еv`k dd8V;/Lj ` ` .Tdrq|~x[mqiѸ{c1{_۹;[{8+\Ⱥ &KhdxU+a PWup 37XL!:1ƼNқ XK! [Ơ ְ :pKr;]j>`0X ֿ''+)p@K 10'&A ,QYA yWy*A)00&HQ"P+*r[vs)%R95 1| mm0S12Q BW|SVsS<B SgGI|(X @/UZ g @2ΰ"LqPa0cE$ƈȌȎȐɒ<ɔ\ɖ|ɘɚɜɞɠʢ<ʤ\ʦ|ʨʪʬʮʰ˲<˴\˶|˸˺˼˾<\|Ȝʼ<\|؜ڼ<\|<\|=]} =]} "=$ࡾ|%圲L Ҁ1SгܲDpBi' -3dhʓ Dcq ( n gx.   Sf `Q < à N  `C>k0p> Ct I~T{0Jk@k[0 '&hX&`Cm P层T <++  4 FطP 1 l6WQPl`Pl  o;pF @ Ph EFExDT-f @ ~GFj%Kѵ,>^~lA; P t0n!,ǯh!V@Ի{g*`a -.AS6Ƨ ,`$H 73ݕB£f!,e`}H*\ȰÇ#J4xmŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ> PѣH*]ʴӧP&L&իXjʵWɨ~KٳhӪ`ѵpʝK]˷߿U}È+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|%<?硵蠅裱)@BjikR 襜:5hiiS骰z꫱?4j뮛+Z&ٰ,d.b>+Vkfp뭶+疫+k&4#+3Τ7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\wub|m,8bmhlF t'wݗ0ހނn'7c&LWngw砇.褗n騧ꬷ.n/o'7c6{ uJЋmATib  A#DE3 120Ofu-R. /`?/#؅@]/bE. b[b0H J^1 ] R (R)[ZA(y0!]>T!DF<&:PH*ZX̢.z` H2hL6pH:x̣> d$Q =!?ъq I%L6rO'C)12@ %{!ir=^m̥.w^9. c2.S::QS h:1gRgG^"qlC޼8"t5|^y䥠|>g?~(,?ࢰ:hCʓO eD7уRT' :}h(}GA:& gZRJӉ'8:<$#3)MoӜbR XPJTT@:Nͩ@-VE)Vsςh ;kj6PZxͫ^׾ `K-/!X-B|L {$j H`BBU $D၁$@Uv + 2f;i,AGA8!D%"*rR!JW„s aG~,tviEe5|` 㘇@q\(?R ߹W"@` 4(lV!'aDd{Pa IP@:H¼ҏySd]'BЏOQ UG:E3R}L*[XVݫ(=Ӏ.Y&ɖA^/<s\f9͋a1y\.T3I$qlAX~8z##"Y!^Id<a09No5H['H*F 2 "Y 04BUDD/A⁂ dBt Bk $͐  aOc7o%@Mx"|*A1 ܗi-rJ@_ oıXci`; 1l8.H8Y8ܓH8qm ɞV%3hk M8pt+aa}G@ka2Ja L H+c @Jߏp$(H0{C' nJaw; @<|C1,kغv-=Q(HP>dT#H,j_ q|>& ؀8XJ5װ؁n "ȁ1#X&(,.1Ȃ31 ~]- B8 Q5hIxAKUϠ;(OMȃY[(_HahceȄ}Qb#l؆n(rȆq8rXvxt|~}8 QpXE8 {ȇh8xXV䈛(؆hhAX㉥ȉ(Hh(Z8pvB(dPt!8x؈ڸ؍(8hሎ|q V-@(aÎt!ȊhɋHR 7@ 0xxXhHȑ  (zg(]؃1-357ɅgP@B9DyAiHyJCٔNO9T)qXZ ?@X3A`hYpk9mgYrtI`xwyYs{y~Yf)iiln m)ɗYqP524`b(dHfhhЃ 1A4Iɚ8.0i <D<Q9Yy虞깞ٞ ^ +oQ.Prp› W7 X#B;7xQ 1)PqL` +7 T0y'Ұ V3G b 7%6 -%4 v 9ɰ=" ` & 5e0 t) R {Ez 'Z1 À @ *:{npy >J]^Q p7@ zs7` 0r q@Z+p60㢥x}`3.%/F F3.3d` $ p>13@/S P p 0ê1z0 0 r@ $ */5$00B70 j d "qXRp 'dJ :;P4>QQ+ 4 k+۱; >Lb&{(ky+ hA0[2;cQ68\<>XBkD[RqH JNѴNR;T[V{X:sgZ\۵`b WhAh@ciJ av!/ d;W Z$ j;iF` @iڠY|k3Z8Iݤ81X8˰ +'qm T\&!H(0m0r#p rq(=T*"P<`%btkk@= 6pN yaɳa O@ Zѷ&n 0x'"`t_x ࣽ<^~y\qJ0a\."U)Q~\nW'=`iuV'=ǔW=r zG Eg_v1*S4 !,a_H*\ȰÇ#JLxċ3jȱǏ CIɓ(S\ɲ˗0=n$pkbɳϟ@ JѣHGVZ%PUqIJJիXjʵ`-YuKٳhӪ]xKݻxM g LÈ!ZĐ#KLϠC]w\ LװcfLZٸs;۽ Nx^Ɠ+_μУKNسkνz}Oӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔VS,f!vJ! Z j_,P٪}+}*{kzly.lw>+-vNkm~0O?#ж 5zD6یiٵB~ ? 0kparm>3X =֙owW2>#x@qC tlO7lpiPu7mYB7A@=OS|ge=?@ >u&A7W2?䓏@43A `d]}@ T@+r 4&@MΛ>P2@38C-G.@%#zBșFq>@h?+g/ :PP0ڧߓWNyPJ3s s@ x#g??[ HAHP`Y0n U!f&LU &.Ƞ g w@ H"HL&:PH*ZX̢.z` H2hL6pH:x̣> IB@8" 'K0rw:|$'%Qro @"/7j(GIR򔨤0J,gM C@/v^ xb2E(f:AftAe-i͂T4Iu:':l….za آ$S0| P?JЂMBІ:D'JъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwSdҔ,$U)D.Q@8 i5_g^ ' , \骣eX‹U" ,֚'ro&ArW,W1"Ѕ|uE%]iq 6Zwa Xh؅UeYفl}]9Xo,` `(0 4;D_{P6_ vGL(NW⍴a qX7A1QYD5mC y+2wExˑ┘2rH+(e0np@xs3g@z~20,9ѐm ZV2N{ҕʥh*ӨtEFO_]RMkPZ*?ҡ_!zuo=\+o"lZy٩~vTr;;mTk*# ¶s'awC-?NO>1>8yX9c6rb.9ԕ8I9Z9"d NP@,ps 8Eq7L )'H⒍e=9T 􁄁ʎЗ<qiƑ' X*D@!̀ep,!S':<}IANя4%x_ (ȯ wowvwc~~ʒ;QqS֦..&GAa 8x=&BQ9stv9"%' m&/2XX'bI!逄 10A0>6 19IWu0(a=Qqo1;Paa38?88c@3a)0 C {4'~` V'Q1 r a4rJ7tIaf 9 YXy6_~ 1xy ea7N _+a<(l`2p3F \ !xwP97'ވ$tx pb2ݐ86-GQxd"t&7p ?0w9'!5˰ |7װh P.0 ,Ȓ?3icPMq xSӠvMް{-{0.RrAs'x  e/ :{q"y_38x`h-GP @#!t.iJK UAP L 1 q.9B` -S2 P1cDHiӄ@?l0׷ _ @) A7ᐝq|Zzڡ "6` ϐzqఢ,jG,04ڢQ5:8:2z>BDjCzL(֐ɐI0JڣDA>ʥ;8 9 G*U'6qtKrj> ˸;{?AS< GкQ{ۻۺŻvKY50 a!|Da{K{;k˾{廽;۾{+ۋ˽+L<˿{a?E,MGBr;t{#%N['|+lQ,@@2~-;ܶ=?A<)LElG,!k8ĭaDK ţVZ\^`bRQXZ]nA_u J4hflnk1t~ϐbwny|c4{(ޝihĉ9/@`p3Fh cQW0-!#0zpS;6~pKnE @\)LյqLr;mNdk`p@f%> pb@ _U5Ѝ6.ʘwB1ԣe~ZX@B]Y1Zm\F% ҐuggTG\$(?͞[%/1#/fj0[.!!Y(_1691'@O3BO_ƀNv 3vq^ _71 .e>X U8%K T@ca d+_EkeېU%`?P 10ࠀ z.0| AU/l? c@mF,_ ؟ڿ?_?_@@ DPB >QD-^ĘQF=~RH%MDRJ-]SL5męSN=}TPEETRM>UTU^ŚUV]~VXe͞EVZmݾW\uśW^}X`… FXbƍ?Ydʕ-_ƜYfΝ=ZhҥMFZj֭][lڵmƝ[n޽}\pōG\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ_|ǟ_~0@$@D0AdA0B 'B /0C 7C?1DG$DOD1EWdE_1FgFo1GwG2H!$H#D2I%dI'2J)J+2K-K/3L1$L3D3M5dM7߄3N9礳N;3O=O?4PA%PCE4QEeQG4RI'RK/4SM7SO?5TQG%TSOE5UUWeUW_5VPb(&׆(bzXHW`!&_t`5ڂf[zVo{!Zqm6]oǵv ueX_| xwz}7wa ~uW\pŖ[%'8ak߰c%6x6_6bFWMeWf_n5fgι A蠏6?D@IIlfxyp @q 9g&[VF["jkmf{9^[g&쁐Wa'b Gܡ-/b/fmfsax-f֖F m` U 9^SW 4!^1t%*p$` ƝБ{s$Y 𥉉0\2ESB=B}N HTwI`l| .!b z v 6&㊁B#)?И`JcA,?0ΨcmP:Fß8-Cȓ A r yh "nƹ!ri! -vYN8ࠆ& 6Z F Vfjv 8$AFO>a Q:L9c::@J6d2dJ?O?.OA@A8ϋ췸-۬r;-Eȣv{$&:eKP2, ۼpFr 0(4B!gkP2?cʥq\Y !Cfϣ˫<AZT2BJK8P AlK,yB-TкH>t3a GM8kߖ*C:6O&]d=㨥7 3#h]I`J:Zr 07Ɲ+{7G/Wogw/o觯/o HL:'H Z̠7z GH(L W0 gH8̡wC@ HD9HL&:PH*ZX̢.z` H2hL6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:vugJӚv4۱NgӞ> PytD<Ԧ:P!9bG8. ^A/iGAzpC;AB8Aq yP#qve}v9h;(]yA$Z-lAXF% hCKz8KmcOZI-LҶIJMms+݆ip;ܶ.Kmr{e-R׺G9⡊<+ޝ.x"^Eozޢ^~ J=2_w('UUx'Hߣ{mpo9 kאp.?"(NW0gL8αw@L"HN&;PL*[Xβ.{`L2hN6pq=ӹ~3 AЁFIhA۹х~4gJYҍ5gL/:&# Q׹ԣUhTOӉ]ilָu^Z"5a>ldGٿ~6kFqXζӢn_["6íq>ӭnvΣy;"^7 p[=r1F m" ~o[v3A/B-~KFN(K򕏼.7cnмo3yֈk⠆Ї^-1n\wpgg5d pε\; A;CԾ+ ;Bmk@ J`2mj;Vv=xb>؇N8<$"?<3s|C/zCA_O~}_|}?#?[>{|㛿5M<˺4M׿ugmMj(j~h8Xx؁#9e`<T  G?Ȁ 2 P3 ɠ8lS9 6ڰC4@.WPQ/B> ᰃу q%AC w M@ 1(Qx fMx^ِ 5X6?Ӏ e!؆ X0 T} 0 p{؇ T 6 EQcp\! @`  H A4Ȉ2A[@j Q d`  >lX)Ћ!y@ pT IݠBYpːQ@u)W  &p(A3  Q561)0r,@ äܘ   zB@ Qp T@x ?P ?c`@' &(iP>;1I0 |  GJ1>@Y͐ap1᳗a 6%З = xaAr=}ha ᐇAȅ4a 0 0 P=c  ҩ޹ <9 ( @00 )Aٓk{q ည, P @ PQ` =0 О `@Y Hc8XQ,`+a a-ZʀQsMz I#惃] Ӡ#bJʰ Tz!W` йTZ VpZ p 8N( .Yp:1ʩ᩟:Jj:ʪ*1j cTp 8nQ کڪ Ѭ*J `E>J :JTc7E dq*`ѯ\1{X k ۰T+;PQ{LVKR" $[Nq(QѲ.2;c\4{8:<۳>@B;D[F{HJ*[E@BVh UZ7\(}(m 0b4h8<@)䐃HL6PF)TrҕWV\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+kVT,%!C' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmwNTHa <p7걎G$%IF2O Ćy8Yp%HGQlX&?iOtnCANB4?4$H$3QVTK!m䃤?Zd&fC,R|uڞ1~@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:6d'8P,f/NCBђV9-j^3dmkAKZֶfuYrִ8 [RָMl{ZJֹeHq\Cͮvzw+풷=/z-)כҗo|Ż旾n_^u`Xo[`VXaNp a'p5X8,3Mwc,d,ȹl %3!N6 F@Yp;bwY 򗹼beq۴3bN3La9{`fH b<&n.[LWehѹn%[J֐ȴ7i@ӠC QӦ>jTիvcݐYgH^z6!Ml^Nvfvkh[Оw-_o;"Ma[Nmnqю7m.͵q\Fw{o?ލ i7vx!^iWn7{ GN(OW^Ad#P60 N4 X SZ =7:`3ޠnC`)ld=pө~.q ;_t1D {AU`E񌱫3V$*nN5q08=2 md^(97 4غ@Nl @MtP!< 3!s| @W'(0fE*  CL#"A( A1@Z1zO w.L 0|t 0Gwq  L.(a P  |g}{O  -@ z s0 }Ϸ Lg Jp/ ' qR Q0` Q pQ] S@-' xqT @ }P .H/Y@ ({` pg O}A 4OPP"'$'01J W@ A|ߐ&@}s WP ;H S ?-7s s8;g @xE Ұ ؐ !zW 0?s(p ?8  `.ڗ ` Yp PY ` Y ܨq *I 49Y Bwt88t@u P4K)C 1 @ 8 =IY 2 0 9 Y)@ʘbybِa |vtrT69Y =9 (I.(P9#w.Gw GAQhC Ř! hT P À` v) ¢vQ|˜?H ȉɓ, / ِИ_ӝ깞 ɚBYIv,7ٟ:Q Zqj ":$Z&z(*,Ԣ026?u&!,n\H*\ȰÇ#JHŋ3j|8np6N?%O0N@(IfK8sɳϜ1_ѣH*]ʴӧPqJ-!P0Wk)4ͳ3](۰QʝKݻx&ĉTydXuS S5ǐ#KLE/#˹ϠCע&7_CPX۸sKάy Nȓ+_μУKN񻊽ËKӫ_Ͼ˟OϿ(h 6)cM<fati Ԇ@]h(;4";7)O4CNГGd"EHR6ґU5(v^,G?2{^-Eq>0IL@1e2|2"MhRt6ِm@rs(g:vs|DھD)-M>Rh|NT5MGӒ2?miO_*ӢHMRԦ:PTJժZXͪVծz` X %eIy5A u\[᪵k'(*! j,(+5J׫9#`ld#mZbh,I XE3AeC"`28{U(,b (H"`^H32 w L04ۮC![,` Hl\ N d `Gd`0 pAGwv j{#C x!/A1WTÕ@|bC[CU`"P@$ DW@jbbg b@q-& P@^ "=> YF(0 "ŕyA]ʨE-z ^b 6 ȚY?E c 0Ho?C/x̢τ.-,f!b*۶xޙ,eDś# Y"H/BL.ԼhCL["ٳbϸ/ >y|s}$;f)n`XufrC=vMzη~NO;'NCXϸ7{ GN(OWr5-٥8Ϲw@Ї~**&;PԧNutG]1q\cNhOpNxϻwѼOO_xE=qϼ7{GOқOWֻgOϽwOO;ЏO[Ͼ{`O~$OϿ8Xx ؀8X6@I[Ё!,H*\ȰÇ#JHŋ3jȱNjaIɓ(S\ɲ˗0cʜI͛8sܙϟ>y JѣH*]ʴӧPJJիXjʵ+KU`ÂJٳhӪ]˶۷pʝKݻDSݫ߿ L*Tǐ#$1)ɘ3w<(Nr(*ӨSK|(QU˞ըרB%p*p媒H U++ *m|F% UvRo{Nu7gWTM۱i˟4ㄼG]6m]ryAG߁$ Q0DlQ4PG40ue1R›+m[(݀lUҊ'JTUGc| )M6t[hQ81#VwЀ6ڐTV+}8Q &R+YP''PfJq| (~,NǛ(QxGg詷ց'VF*GZD[7JZ81IN  ΐ aL꫏bK)0aXzPp LL :Q&N2!jǍ9&HN(+BD't@"N8-p 񶭃ϫtDRݘ`V+ 0 {$Z͚ &!NP$L$LdZ`≖k0LT' ~ 7Zu8f87nT+\*8DJZl&SQ`K+!ʆ≈Z1kqd %L(ABa$8TLJ"'&K(5&[gpИ(aJ@uIP&@(,gyIR _*'ۂ}pH~,vc,-b1[D^1TA&}&TₒA``6epƄJPBV{Û6)A:D$ ?~bDaHB %Ix_ JTB>:AVi{4 P؋ `5eop& N=(zP L %403P8/)D' M*:>W`Zx{IlT&b@s%>3]d!`[B NzhHQFRb ' jW"j "xE-^YP4ĀB7Wb 'BO !ER?T!ՈJծbO6O@$JХYB_̈́%( Lhh [OLdЄ[@`(ձ&ːA~\}jb'kO QHO)!S'Q,,eWKo)[ V\ Mԫ[9ZILbҖ 2"1zD] :НoՆT b51V]eb\nfKym%rV #0޸ d[^@ U+ v'La7ΰ7 G[ఈGLy L`0|pX8α # vL".1&;,OL* +{` ъH"hN1 `p*( πF3/~'8a(#ʐh'Mib BdGcViA0LVZķgMZָεw^MbNf;ЎMj[ζn{MrNvMzη~NO;'N8 pPA y y} p!FWNs[8!p@51HOҗ;PԧN[XϺַ{`NhOpNxϻOO;񐏼'Oƕϼ7{GOқOWֻgOϽwOO;ЏO[Ͼ{OOO2ѐy!8Paː!t5@j0  8 0sP qC@j!@*T *HуBX<>Da +h7(I[1 (P+^8pdhhrjRцZ8gxvUa  ϰ uH(rwxm H0/؈V舓80ahH 3׉gHy׊y7xx؋GHxxxHh(xx؈w(׸wٸwO8Xh縎؎8Xx؏9Yy ِ9Yyّ "9$Y&y(*,ْ.0294Y6y8:<ٓ>@Bx(7FyHJ KPR9TYVyXZ\ٕ^`b9dYfyhjlٖnpr9tYvyxz|ٗ~9Yy٘9Yyٙ @ @ ؐfPq؀ g7w pfs Vs)x m`s ᠁)Nr)39e ry虞깞ٞ9Yyٟ:Zz ڠ:Zzڡ ":$Z&z(Kل2 3:d:g>@}Bj+ C ѣK5*7E*R\ʥ&ѥḃ 0Hpr:tZsjvz9!|ej\lʅnڨP ?{7ɰ0 @ r1  rjn@ & =?Ġ 0 P i.hw ON iA%ܶ hAe` = w $Ʀ1_ .&yF j !kpQ' _ P  /{N){8sam- 0 _+@Zr/ r1".-^ĘQF=~RH%MDR.]ye b[#gAElV] +5VaDTRM>UT &Nkײ] [lc3vk.cYT63/UśWo^%L[-iڄ+ă|Θ dʕ-_Ɯ٩l_^ӌQrF[R)QpDì P-nޭC~vpeRԝr͝?.TyUPܵk_׾_]ݿ_|ǟm/@$30AA;#/0C 7L A?1DG$DDWdE_1cFo1GqG2H!G"D2I%d3#2J)J:2K-2L1$43M5dͰt3N9礳Ίд3O=SI<4PA%PCE4QEeQG4RI'RK/4SM7SO?5TQG%TSOE5UUWeUW{&}fU[o5Xi5W_V}U}E6Ye#}]6ZiŬ~"Gj۩6\qU),rE7]fu 4ƀޓhc uŷm7_vi`fa8b'b/8c7c?9dG&ddWemY9fgfo9gwg:h&hF:ifi:꛱'k:듰1Wk;l&lF;mfm߆dq;oIGosN矿'pG{=zsBqԙ{GMg2OS̭?/e?۷r?@)|+`Ё`%8A VЂ`5AvaE8BЄ'Da UBЅ/a e8CІ7auCЇ?b8D"шGDbD&6щOb8E*VъWbE.vы_c8F2ьgDcոF6эoc8G:юwcG>яd 9HBҐDd"HF6bd$%9I9]d&5Kvғe(E9JRҔDe*UJVҕe,e9KZҖe.uK^җf09LbӘDf2Lf6әτf49MjVӚ׿f6MnvӛߴUW!0Ѐ3sԐ4aNvӝg<9OzӞg>O~ӟh@:PԠEhBP6ԡhD%:QVԢhF5QvԣiHE:RԤ'EiJURԥ/iLe:SԦ7iNuSԧ?jP:TըG @ ``M}STVժWjVUv2 d@!,H*\ȰÇ#JHŋ3jȱLX:(ɓ(S\ɲ˗0cʜI͛8sٱ@ JѣH*]ʴS^ ]Ojʵׯ`ÊP`҄V&#A}"Kݻx&JbKÈ+^xI-8I$˘3k̹g*MӨS=K@Ѫc˞MmU• Nȓ+_μУKNسkνËOӫ_ϾG˟O0-OKW( Rˀ|񦠂6`p D˃x"`H vgBP+ˇق,"&bA4ָ^/ L/b#CDI,қ 9$PF)rJ,WJSv嗨]儴d_J)`曄q%B[N) |ɕj@s>ɦ@bK,裐Rt/5Z)B)P,h&LDjꩨ`&jvUj @K[S)&lQ d1\iRS3!18P)1`5'8&(0Koirjb5'B1L3>!"HLHH*~fȢADb}V ic1Α6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WVJ{,ǨYڲ@:ry^V0'x l%oL2f:ЌՒ1~HGnF&7I>b g9'b'1x̧>k}3J@ԇFA 2D'JъZͨF7юz3GGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMbJ*d#RR3YrF,h V=RSڤG}-l* *ns pKMr:ЍtKZͮvz xKMz|Kͯ~LN;'L [ΰ7{ GLIJ@d t*V#Xŕ83C -#&P ^*=8I6tĞT^$?A~X9̓?6pL:95>/dtgC"C QI ZȅHF;ѐ'MJ[Ҙδ7N{ӠGMRԨNWVհgMZָεw^_1miNYa$Jxڲal'`dA.ndgլ$ ,H vo!,R v@ B[ p4Ȃ:a|A3!բ2ް1bk !-#a؂ FmBcCD(.A g_mX2Qkdc06L aPPa#0^7]x!hbVUDۂh{(s}(S#<hCF6`v@:م1tC|dDW@Ҿx@"&t p P7cl[o XFlZο7'q65Z@fÕ| wEh}]͘w-ЩpGg~~Yo! f }Ār'?u dbym} "8$X&x(*,؂.0284X6x8:<؃>@B8DXFxHJL؄N8Q0N@iIkvX0XTZX@f1^MO 'rSg(2`۠b ezHO!9 9h8ySP-$HT}(xNJ!]|؋؋xȘg[8ԸT` PhЍc`Pа  HQ|WF:HqL8ȁkGN6a3E{ HB^ِ0 "Zx(eX,IQؒܡZ* 8fQ>h:)<@(C99@pVLiD ;醗WyYy(Haicie)XmoY،~hwy9(d y+)C 11 bedٚ9Yyٛ9Yyșʹٜ _ Ey@:ky0~ai64z x a cHp~@09)1XùT'߰5ٛ ZpN Q`bM0|jH90/:4Z6z8:<ڣ>@Q8DZFCJLڤEP1QZVJZ\zb*VjSPMʦJNgeje*Y FW_ʧD ]Jsju  :Zzꁺmxٜ yrZ x ' y\|{p XGD`!hH'm۷~] =ܹ#G@`"q"&HMA e ]}>^]!wM:G *Xawr b= 2Q|A<  !YLV%ңD/F|8~g!,H*\ȰÇ#JHŋ3jȑ+I`Hɓ(S\ɲ˗0cʜI͛8s܄0@&<JѣH*]ʴӧP$JUX8+`ÊKٳhӪRO"ݻx˷ߊ#)YJhg+^̸ǐg$݃#3iB,pϠCM鄚5Y4iRA#O˞M۸FHiw Nxq[q2μУKNسkνËOӫ_Ͼ˟OϿR)ҋ&EKA1(Vh9bvaB|($&HLAR,2p5P.h@,@N\}$VD&䒦e%VTV+`)\veTXmUX~ih外l5;n,iigVTLV ʝZ& "Kr 8VA`U)PR@(*dB7SUTR@L 9ꬴNG ;t!yr“0i&8)PZJ+ԑUTIW@ 4LЫ@T }:v X*$!ˤ@XJ 7W8( J)4(,K%Hg#{M:a5#6#5 L% J,?9-d ,,O;D ,d0ү[9@#P8:IipC#U-DcE`(5؈'8`cvQYeըA#KYuPd GV褻td!)ޑ'EF~Q =-}+.Ɠ* GOtHow/o觯/o HL:'H Z̠7z BHBҥc_KB]#-˜Q6M2̡w@ H"HL ~cPL9X(ZG:.z"Hoh!yHl#P':x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:v~ @JЂMBІ:D'JъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\JWV`h9A+Ae aOQ $D4 a(nу 2&]Y7B, (Ati nH.` Z( 6` 0~{IC Ge<&2L|`@9:*FeؖN/m-X }EF :ȹmA߁lfHK, :&T )0Adi9;V_ ;N-:E-r1\&H83lJ2$S@뗁y3!`c<b6-Hۅ(7AcSO;'N[ϸ7{ GN(OW0gN8 2~@0fә6a i? XR^8:;t~Ly~4>I8D{-ul!~65EaP#˧>uS!g(*~BZe3>񐏼@O[W7{vxbm؋Qxunw2Ȍ!~1xؘڸ؍8`w蘎`؎Ȏ؎8qя5tiȐ(ȏ 5\ iH H#ɑ\5Ӓ.09- 4I6y0:<ٓ1@29AFyʕL@ =):I7i8YF3\OCɕ; QISiUWY)Ioɖ])c[I/)yDiaɗ5YU6B Дј9ɘyٙ 1s0h~YyٖH9Y.eYUT6w{}ɜ J锼R)3)i)) i\)1 y)ٟ:z \J: 0"!J#*%#Z(* 16::z0@J4ʢ&ZD*3)ɞI+ɤ Ii뉥MO*Q] UJWjY[_*npr:tZvzxz|ڧ~:Zz::֨*Q[shݕ &*Za[ܕ 4iuJ  : iAv:ZzȚʺڬ:Zzؚںڭ:Zz蚮꺮ڮ:Zzگj#]C[{ ۰;[{x ";$[&{(*,۲.02;4[6{8:<۳>@B;D[F{HJL۴NPR;T[V{XZ\۵^`b;d[fۉl۶npr;t[v{x2#zۈЪ_۷dȷj۸[Q ;h+zk:Ra1gga {mh[ ;:ܪ!ۻ;[{țʻۼ U3 o٪ ˁʽUa r" {bu_v !,!H*\ȰÇ#JHŋ3jȑf4 DPLǓ(S\ɲ˗0cʜI͛8s3㫃`IѣH*]ʴӧPB*O$ Uׯ`ÊKٳhf$PdAJA j$m˷߿ LX &nvH2qǐ#KLeC]|ϠCMz#ݷ 8]װc˞ԏhͻ߿q ԥ ȓ+_μУKNسkνËOӫ_Ͼ˟O1(hr " F(!iJ{l5v ! Th@q("K80Ƣ@y+\%H&ٙ JF)TVi9c,NNͨ2OXhJyfĉb ii'f2PMь^4@ i/A0d!tA$!P\fip@[9B+!r@ f.e)AChhHl뮯akA@~"PJ eJ" @+dBH$kYRnR@n{`Q;UUǜ0.uAWUI,< 7,ذJ9ZK1CStJPft@XK@"P!286׈(FtXP*҈iBL2@A0&q@,+D`3 ;,fO @ *H[K3 -HS8z\K+A\*仪Nͪʙ -,UPvMƯoUL*KKע׺vM8Y-tҁ x@UD\6n.gz٥ g+[UdއkOPbֶ(FK_1DZIE6Kh*2A CT0 gH8̡w@ H"HL&:PH*ZX̢.z` H2hL6pH:QW@=ď| )BYjC:򑐌$'IJZT@N"(GIR0*tRc:X̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:v~ @JЂMBІ:D'JъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:d'KZͬf7z hGKҚMjWֺlgKͭnw pm8\*#͍tKZͮvz xǻ^Mz|Kͯ~LN;'L [ΰ{klxW@Ƈ55K7J1$bD 3>0h. ,X:|$ "?IUE-qY!/v9)2>y&TTysA*6!3 b1QsQ*腈#".C7r#FEx b -a$p~S}ܼ .~,>v:O3?&a;dH廳WV%UE($6!Z15-9;5_ӓЏ>N3O['O{_*P/y>P?4 mhCІ]  @{ J׀w x8x&R!z#W0/x8"Ex:x<(>ӑyy w}NG aHLP'S8G.I7ZXQȅ煯Ichyaf|HɠI!eHadY|{x8|xCȉ*ԉa ,| B*PЕR8Xx؋8XxȘʸ،8Xxؘ {P|H蘎(؎8Xx؏gaYaq I ِi )y ё_ m y'I)+-/G51y9%ّ;Y? 3AGJLiєPQ9TٔVyKZѕ[` 1dy_fg \ٕmoyqIsYb9u Ŗw y}閁 )Ii_i{ɘ ɔi闙)\{Gd!yٚ 隰9yɛ雱I{{58|ɜ霈 Y)iWv @ HiIiIɞgWאHp DٓFY4ɟ&*I,i.0_Р Zz ʡ $Z q _ -/. @6 4: ;ڢ@?*AʣF*<:JCz=RS8z57ZR^j\JbcVz ʦ 2):s*>)iI<ɧ:{*y@iujBڨ:Zzک:Zz** /3ġ`+qzF F`rKѪJ A ڬڬ@ >azjyJ:Zz蚮꺮ڮ:Zzگ;[{ ۰;[{ʎ۱ ";$[&{(*,۲.02; H* 篱ǯ@B;D[F{HJL۴NPR;T[V{XZ\۵^`b;d[f{hjl۶npr;t[v{xz|۷~;[A:Bj^۸;[{*"m[{KOӺۺի ;(`sA }>ǻ7[/r^H ˯1;[{蛾껾۾ 7mPp J wkA!,H*\ȰÇ#JHŋ3jȱ#L>䱤ɓ(S\ɲ˗0cʜI͛8s(r`$w JѣH*]ʴӧ4?R%(h0 ׯ`ÊKٳhb"9i`&M@IВ%pE7߿ LA37(&MH=L˘3klPPM@vL)/޻S^ͺװ][R۸sͻҵmQMȓ+'+W^ЗKNسkνËOӫ_Ͼ˟OϿS]X`#K·HA FxY/f8vh^p$h653V(0F-yH~<-tAH&I/3P1iTVie}tu\vi.^*ediqeWip@՜q瞃y|*PMVH(F*@TŲ@&/*+/pEe%BAv]:FE31ӢS`V8鈣P#P+ .@{dQ,jJ'8F*"nutrK:k{T x"4L`@8P/@l+pB ,Ȝɳ5 E+ԪA<@-|B<OGRIf)ͪ-2=3F@ǧ4K=w5YOG$t?BιhC;5:'H Z̠7z GH(L W0 gH8̡w@ H"HL&:PhdȎX,fw,zp[v eb6pH:q5؆>Aա$0zƶH2h$ԶIZK&7NF %a6QEa<*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:v~ @JЂMBІ:D'JъZͨF7юz Hxv&&JS͕.}i5c*iҴѼ)NTT=7*ԓ$*RzԥڴN)TJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:d'KZͬf7z hGKҚMjWֺlgKͭnwF2 AMrptkM`28ml/?]7My]^-{7}OsŸ/Q P^a;'L [ΰ7{ GL(NW0gL8αwtFA1 xd0L2d`E)wVYe!l-;00ш$9'-ZAt*U"S1!p#$7[UA)HEJtEbc`H-_ԂF/(Y#@xqZF,A 9/e5ldCm2(eH/ l =yȔȚAƨD#*)5\:v0̲ 3ވ'Ê򘇼%yo^'zяOWֻgOϽwOO;g@ 4C <=RGkn>Q qp >XHl H PSL !1   M's! 1Q0N(U'~,؂5^02.!3x0X:8ȃ7DxXHw JX"oO8TXVx16X! #~[!h@}]i0h8^Іj@P p'l&(7l 6\ܵ4?F/x0X2q)3C'xHb؊XFD((}B>8%Q&x(0qHو؍ꐌXx]Gh8Ȍ]Ka^QxߕϐyٌHD1!   `"y6:<ٓ>@B9DYFyHJLٔNPR9TYVyXZ\ٕ^`bU8f(Gg⸖N薫ryxz|ٗ~9Yy٘9 Iy阜ٙ)I c 񚰹ii隬فbɛ)iɜb2Y ֙ کٝI9Iؙٞ<ٛ9IY'ɟ9iz ڜ'I jYb(PF(yɞi1:3 5ڝ7 e@ (*IKMzOjQZSs:"%QFzjʠgi kXjX1H p ¡AxJ|E ѧy1~ZFG&:Zک1ZJ*1F!@:PJګ:*@Ŋ:zǚ:*Jqڊڭꭻڬ ۊЪꚮ:.&CJ*LJNjPRTʯVj,aJc emj !;[{۱ [YBJ/'9P!R;+;Qց299WTmdzB@#VȰL0 Ӑ2"Bd X Ek i*! _U nnkJp0HB ;`h1lUp d ? IvNf 6Vm [ qk tIa ,[ad0H!`0?16jc;l` d4jqB! ؋ཌis5G p vɽap{27@jc+f Fa K2Yʤd 3bd t \l  |0"|.\v&F 4\6|8:<>7L `? Ăg@vзIR$^&~(*,.02[G r6r< ` @ R1 #g *W4{t$P;npQGQ^bcsa~ve.V!,H|H*\ȰÇ#JHŋ3jȱGmФ M.ǜ˗0cʜI͛8sɳϟ@du)eghvBJJիXjʵ׊`۶daSm^_ʝKݻxn,0]̸ǐ#K.˘3k٢Π"MӨSS~%uװI{`tgͻe 08ƃ+_μJ.سkνËNӫ_:a˟OϿ(id}6&sfF(!ghN<d̈́vapȓWm@Y;mዘ U`[Lg~5<T-@6U(PX6.v[,y(ГQ1:&]$"Д> )'$nAB@(Ts8BXj T<}bgU8iO$Nڛ"PX}ꓘ8IP4y2M<"O?lÏ(#=# ؄2N8:j$϶{%6ӘQP%C'O& c(WsMOiN0I?P8d&dNU\qMu$\"*vF: cGy`,mNC0@(}# 5\ )M5sM XI'1  b0Pa1DJh6lZcI(K8\XPBIӇ@A5L3T$U^cGYPl4!>-9ӈ/ѼFnH>ro%z@3Jf$>ؼK͸$1|,?(!#{҇H&T[6͌ZϺ8ӏ@;<5^Lj6o͍DG.ǻG$C81<TbX!9vYO"q4 0 ӢeiRR#ۨ $^KGP\䷩P&Ә?}kJ֔ESHZײTq0?2FxJF2x#Lwp ,tӠ|YG8dL)34eT*j@1&YdOjS+l3&mQ\Ry% ND7Auh0֘1C Ap@͉V &Annd~Ә/tMyxL(,Upf̞-.7-h?r68m<z LziAZָεw^T d ^x׻BAZpbV5iEY8@HP̆4qniF8Kzη~NO;oLV9ɝc{ GN#vQ_mF=h5´rd_CFڕ]tr@)dHF2! g.ַ{`NhOpN]VFk(O› F0^0jpgO瓟FGolo"f7,t"ϽwOO;Џ{;sX!E,^AO7C0t ^ź,Bsv 0 7 00 P P ' "8$X&x(*,؂.0284X6x8:<؃>B ?X?lw};}wfkq7~ Z\؅^`b8dX^ o v   gF0r8uHrH4P 0 h p @ W 0P X ؉8Xx؊8XH]ehxնcdJfS!kxW\8Xx֘ظ؍x\،abPShPVe8XhZ`(XZ@hb0I8 ِ9Yyّ "9$Yi rQgu@w؆W $:<ٓ>@ +$@+:PBRKzmʳQEJ dK0;Ь۹;[{۹ xk 0  d- 0 0[{؛ڻ۽ޛ|K0 ` йZ U xZk ?ki บ; 0 ` | 0W 철@{˻p |` 0 0 6|8:l=@,*;; \Uȣ' l9Ѧ{ ; b< 5f|hjLC @ ` 0 p :< eBK p,˜ƒ<ɔ\+Ƙɡ˷̷s [ž`6>Z=y+; Z  Ư0ȓ .  y\  L~ Ǡ Ԭ ܼ BK6bۭb,wk xɛkʧ zmʗQnԬďJ͌ͯ P  "=$]&}(*,.Ҙ0̬ &0͟ q  Ϫ J-~\V}XZ\^`b=d]f}hjm$=b K҄ @kP- sڬ`} Ы r ֔]e=ٜٚٞա l`  ` L  P Hџ}ȝʽg-=ԟ JL  0m\ DșbR\2y* [< Mݚ=m>NL .^ ^>^.` ٔˆ M= -<W2]<>@B>D`͸# :Ռ ݡ ƐԔrZ e; Э0 T< T-Ɍ ==t^v~xz|sU n q 犾钞  J ߧ ۯ p @ , d G }mנE^~븞뺾@N^ N @=\.~'dnقf r.T.׃M]~y w薠 P{{PߵM,/ ?sNƮ\bq(z@tIF'E^K2>xn{-v͸ >ǜH XZ\BݭƞaP{ 6TܙZ5?(*J ƌNYU{_>|/O\<꤀ Gϡv ?_}>Ŗ0  A{ѯ@ H 0235>~jF/ϪJ?_؟ڿ` U\?_aIL RȒ&J'5Qā-^ĘQF=~RH%MDRJ&UI%45qy&Qbz勗0jESn\M>UTU^ T%JSGRZmݾW\u}r AX_H(8)]ƍ?Ydʕ_v7b#ZhҥMFZu _mɓlÏB)ԦR͚E&KVi8͝?\+W_fY,^Z^x͟G^|ZؒcSMlLǟ?#ƖI0 D0AdA0B 'B /Aq6{~ƶFqJf^Kv% jyeeQci**֞xav ׽,N׀'Bs59HЂ`8*>\SP* ޖ <Na2 $c{;, 6ŀZ.֔qkzD&6щ JQ,@ F$Py[ȴ8Mx m<ÎϠ@@1zѓ؞8HBҐ2SІ.$: *PYŘ61LaJTjVC7Ʊsi*$:d.uK^(P$C_ GPd-z!b8#k)Cƹ payG<*P?c/չNv9G21?eb"3`66d8ei>ӝhF{i#y>F\(4mAP"Tc*[JԎ9-Qԧ4_8qRIs */qPp$C7\*Ppt$'NUџլgHjT{&99)*҂: `L:Su9%+Z;XZFE=wl5+Z־p1u@c,`[P\nMߋ[^W'Fqh A a*) U"E/* US$?&OF^֫vxXk^T[z] Fwսnvw=o!ĵI cS4ooʇy-q7x%>q;pqYNJ^~;GtJE>rw' qf $yeNt ~M ޸My+nJٵe˕t @ ʒzLzE1P>vAGo'7vz+Wza  HE|Q"p GzƣNs=wgv{ޱ~hϚxҗ~bcCTȗ\k^ b s8De~?|G~@ . "<U۔1m} 8"Gտ~{KW=W:{| qD1x RI|٦/F7p389iZD|TQPAGs=ɯͰ۸ͮ} NԅŽY[A[r.͋U[cKYŵܟb\M[,ܶ}80m;6ξu)]s<]uT]{5}m\ٝ>Խ:n(5] ]^udH^k]vmK-8͕Z-=_QߨH46-EMѕ5h`d8^nNघ_..r% .0NZdC10vxv`af >aMMW]eȓ 0^w#^//001&c2.h k^^bݭz:cv`5Gp=:zznAvP*VތUHxЂEEhTpOp_@a!rqc Gd'J&XTS*"~`d^U_qh>S˄a5b_tPajD4&}tlz-dܽ*g~YirUffgakw\)btNz:bL` Q+PgEe i~*Ye}<vGC'r&[ҔVgЂo[)*-d~Ym\>pcyjMߥۦg&h0 ^lj~[j[Y L k2kZ݆gn떸nk 06knڥ84pfN>sNe4 ǖ>>Fl=jzl'y>mi />hj2V6J^ |Y뉛ug9wF>"Ttbi(ykVoonaƗWZXaXvX.o^io{R`4@VpN{nXh4&Cj] ϔ3.i{:l `nxvaq&nUguXcPa97&kM긡Urv-(G)T[*W+'z4G0PP'c^o5sesa~1'_9:s<_>?7tCwtH ^BGwH_=g>7EKPLsM?֠u=M/t2ST_QoNuTsS8gJWu^uN `vvQpR dO\U/VhiuvlLm_zrǞowJ7iAt@DUv6v zwxKPfcwdR}bQuvM+@87[d~u閖%6wZx4.`'PlWtw1y{vo/dGP $ajF.L{f(L\ȅO lx~0s,y-|7Dz'>_POf%ɭ\5 wqG4/zKw4{P  /w/<{]zQh4abx cgWPÄ0Ђ2Pʟl"mЧNCpDeHv}I{هHwLRf0Z d ;ԲϷu/JoO(R[J⦘P.x~o{bkN(M~؟zY,, 5\SWSyglV~1W!j!F(J,2 2 ?< qM9#= -S3,#I܏QJ9%UZShD . Kdɽw%i&fe_dFA{'}u,5(hId`#dy#J:)&^*ʨ:*~bʥsg*ꦠv(r.JY땙' ;,źvjYqNJ;-p+آ/$)9l՚{.t*nz8.{/.^.v*|0¯m쯐&<1*> !_ N5P8(c8A x6iT? teGHR[ҋskZe5Z[M0`bCa-Þ +Tl aOwHEZ@I<~Z\t?oz_'c|G1xc|ys] IdP&X`* Mp@HChLȠV2h!+~Ȑ6Y>b1DG2%;Z`0la Vp-c [n{̅(Gbf2Ӭ5n~3,9<3,;}ß A Є>E+Zy~4#-ISҖ43M8O8]\81T`P4H>Y" <׾5-a>@"&!anp6k>7ӭu~7-yӻ7nB 7ME |?oB #pC|xqsC"79O._[˼&DL`B"b-{miiYR(sTkhm[V:ֳs^z<$ /"݂`kw@`N0̺x/?<3<#/S_3s qOD" FN 1B#"$I!,2.#3Z3>324N#5V5^#6f6n#7v7~#88#99#::#;c;hB!B0` |@%J+B+ HB+ \@*X+o4<䇪 ?%(+F#&J2J$KK$LΤL$MdL6 &PBOdOB!1BR%$RjX+>%T.M'tFtN'uVu^'vfvn'wvgq$&B+ $0B'@g7H $f' /d,'*FN*V^*fn*v~***"*&@4AD|93Hp7p)$ ,@, gWҵ)+f(T^+fn+v~+bd쥂1#z zSQ+*?ޫf˾+,,&.,6>,FN,V^,fn{>&O)a"9DB@.9A"& XA|AX:4(D2흢&2yUݩzR|¿&izf-4؎-ٖٞ-ڦڮ-۶۾-ƭ-֭--ߺ-R)$d3Ap09BAۘ4"@)N&A0 ED!v+-mn(+jm k + )?++}&/&./6>/FN/V^/fn/v~/./(CUxԂA(BD&,A Vm3:::3;;/Գ=B-" AAkp.. A/BVC74C DGDO4EWE_4FgFo4GwG4HH4II4JJ4KKt122 5,3,308C1Ms1h15_c6lhk -T&OP=/3?dpWA#@&_r1 8ZZ5[[5\ǵ\5]׵]5^^5__5``6aa6b550 5L5cRX584,624HsH4X0JFj200?. m-C#"H 7qq7r'r/7s7s?7tGtO7uWu_7vgvo7www7xx#76c2HhO5PrM'Nܻx˷LÈJU;NL˘6̹Ϡ#:@TC^ͺ5H԰aMmsN%SNq+_秚KN"SHB;vRoGE |}uk ~HG=(I]vG(DʀFyJa`~*&'(fV^y]-c08(q#)㏉g_8PvRXcyd'ʍr#*^]X#*RJ(]nU 4J(<"+@)ZRiX!eئybyJ(5覛,>$圐FZ(4B bU"WLqBxTP 0$"S)lD ’3 Nh²,8doZe #(4BB+M(&AL밓jq1Fd)X*l&[xE`vjrQd!F%gEƮOrإNE's |S( V 5C NՄyZj&~ZX9Wiv\l| q  :L5+nja 8f˲8n W-bx3a!9T18LepLTβre~D0NhDBְL:9#M\8^#|H|.;./j}F;YEDtAJK$EF, N{ӠHCMRԨNWVհgMZָεw^MbNf;ЎMj[ζn{MrNvMzη~NO;'N[ϸ7{ GN(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhOpNxϻOO;񐏼'O[ϼ7{GOқOWֻgOGS_%>}O|_O~P|;'[$@2{$PWd,׏,8!~+엓-2};G`GA(P( ]1 8b||P#0}_1rHO{ȁh"$|L%"ׂ1h}P!76x:Nу@( ' rFH(\(Q~%(|| s&'4`buh +ilHB8׈`yh鐂~ m8.hTd} 0 vA~c` x { 7bFb8ۦ{7)x^A0Or`` P8hnwnX׋"h8 Jqx}8 hGX ma h vPq 9׏xb/a֐Xm T  t PaC8LY( ( H"ɔm]0 0Ϡn  mHHw7i `Wv} 6 hd> 0ov)0 x~2ƈyHB~x t@ d`h ꠑ 0 ٰ)hpRP ؘו6)am_s d h`@d  ݩ丛雮8Ⱐ@~b\n7m8d`   i` @ᙎ Lo{0 x :%3ڛ|9Ș00  ` gi `xЧK"nЅ(?p0 PDŽ}Q)l:(z闅ʗzkJ|Ѕw `qy؀ !: xJq`@j  ?'g 薨ѨJQaКz֩H9kGP:7 `ʫ0 @ *znZ**dGX 詔kW3 #j vɳ@ ډdi ; Z~[걊:!K#˭%[**K,k.gۮIH=[pTK` h@cP# R V ѭP`Oh)ʚoX溡 r}m:}0_@[M{)X@qn뵎 $k0)h;ƗI煩 ː[ڼ0 w ~ ;I9h cplX۵+~f.֩2똢* َ4[{J^iVJ;rj  )yW<0@ j|*YNnj^\=ULN C؈w&Ǯ8Wπd>  ` >` ˰݂&Q>{9޻O5"agGn$ p!!`r@ f@ˏ|J`M˳=1n6~[Dz} 1q-^h8~cF%3iRJ-]scDgzAcdO:TRM>UTFjUڑ]~%/?j&Hɓ~($`Bӌ?.9XSbƍ? cȥsYsDumsg0d 8x\N ?0O6yLoޤ83ōWs6L;u{o.1]ݽ^* #3j&#0B 'l,l< M)k|hÉ/y 6`'I:~Ӊƚ4#nCq`X&̸ vdI'ܱ.aZXJvt'pF8dG&&Ma0fx'Ԗl8MҸA3?z3L&kgfP2'#Ѥ~gYtbR"ղ̺8vTÈՊRy D2rOG%X"%$ 'Ld$׈dlSG./irlA;z.|պ{_Iڻge~;qdL^q?,Njd~'*qDPby`+:fR`Wx.a}C8e8æ̟CC0d$ #3I"Ҡ_w(9, F9ΊEBDP%.E8(]EX1 Xa/qt l'9ђ1#5PF0(lÈBQuEQ \ bYC1 L]g&iΈ/VN 6yJ)b"ot9W]Bg.] g%K#R̭VԢ%y&ĢǯIGPc,&Nq󈿹hL}qsC;ȓL >c9y;'As)ӃjD:CmhHGA]2իh4Ѱ|4&8:Q `CF9*wS19x'_F<ie6u倶9ФV15/f$(p?ymxE-3jcy4KdI:!k\(C,JĮLmp7~tf򠗈aghC8,%QYv-ɾ$Vlf-]^UǟlWUA\0"yDlKR4}HB ``l#r/|^7`(<Jkclb/XFUY@Lލc9ή"Y T?@!4%TU,r#`˗-Vx#Y 0$Iʀd:ꅰ<6Fp>@i`íBt^FNŦ.Z~Wge\cX1}glȷj%N}Q4P(V<#HgfW#G<37ټ*RXGDE !mo4aUPM;lW2)YY2MszɷN-:Rϛ:V c@-XC֛Ѝ\"ܡȣ405HIqdhKIyHc 8B !\9ɶT&|B4 ()C 0(H pb@@P#< TlfD*g<7쌴116P3: , xxXɂF!D pk*Eى;sWD' cqJ03@QpL0/j 1|0xɐɑd0FylP蛍"Ï@IɑIIǪɟʠxxH"qघʦW4Hȋń)oȇUH@\8Ə(ʹsʆiTB J{}8ihuEA=`m?J` 1"dxy8\,*xO-c9N*u%ȱJB֙& 130eȇoX`Zr:܈diG@҂xx8heb+p8P̻نQGBޜ#Xt@saC䉈PE ͋Bc&z=H1(rΈɜ NPyh;c @)+gT  0xF5Lvyqlp0O$H<>P R}x:DeQk1xeHI8r 5 gCP-*!4 LTDQ"]*#( y4rk`⎽yS6'Y' 086}ط:c"bEݐ)ae(,a@Fs A37RU/TLeACN6pȮtV\5~:C q T– RFwt[WLSh(=J*lf^$<`(q`x@0+&X1x ~x+UxWi0Ԩ@c,!4qgx3Y[B0uՁ4;/_=X)w-|v(X@`(b@g;yyEÝ=}I%cg(YY L3zE'ڔ]e4ZYܩgX2HګĀ F&h/Q @W kX>9&(UȆEkXQ@Ur۽ܤ-yq9ypVe8e8s\ǜ䈶.ޥaa@זP@ Lׅؽ}2ӈ] k( kK7xؐyel ID: Ke凪Gm@&ZmύmHݦuh9?tHU; v`(ݍq(PSOUHkC0A.d'qĔ%*}Ez x}Hgt|c>;n]wX)5%VZث6AKfa4֟4-7Q?b c`eۭ vxنhiG@ L0ABdmL&d#J ~ \J/HdmdGA_%`8~<|e:(~b}T啀 ˍ}r BlS]%L^ՙ!c  `u0=baЏjqfn~FvRvz%1"۔A^^ULI q|% TFrڽ[qB6b~@jZo譵9v0APYZFY/\G 捈Ȇ}"U0䦢>jCa_Rh~SgnmkYWBqz Qx!` !HkPpHH<QbTXC7оR~ο 1V"ω!zu@8:&QKk&*dlT>6 u8X~bZm^ߕxhQ!Pɮ]!2e nU><@GbȍsqH.J&pqԜ򎑵A620vtTOh WS낈XwV A09x4zI-\J" 8J>p۞]"! nؖx:a@ 80I~T%tMޟJO,V3yAi4Xy*'_hLv[&?^?Ktm'rixT-阈,P^@:?_&=xz}tXNI6et]ob[,N8$QbX8u{KDeWؘu8ȌGpۅ(!v+oZJZ掇0vٵ!(+07yW9?pt(msug,yNi'A8 xRj(dNJ| l8 ~r/? PܘHyxϚ_!@RMH{qCYWxب[#PjwփQKr8*l}>z/LV†-ZoDzL$O\d_Ճ匆M@! LjvHX(GyȟhZYxp%y^H[Ktklk!۷]7!C  l2lPmQLOF"?"I7$ʔ*Wl%̘"ARqD vɃo_q锇M-ۖ^$V(.l0 . DXG opY`  ,xeIJgӖvJ[֎c ] p~Ln-t ki2雧O[~Kwn~ jjԆ:unϟqcA<fNKx3O>OxP#M6ӼCc]z%:e"BdKGR^GPA_$y)RU73L#osA(BUhcy)|3My합6ʐLMe~z+$&eBC gKrCghG1mr5Hn(4Ƅ: Tq?MɣVϜV Q><$(c?.9DE)P@g\ӌBKyא5iv[طnw R@^\ q:7M11 k TV!@`,sf>`jj$ C(Ff"3JFMrIȜO:5~ ^8,b{/}c(Y0!%3mL3*ڇ*+-Ģ;!K!XɈұѫ$fq] lH3tLRNP +]bs+-D@M!bX*e+Rqs D(qlЯN<`#`3n Ug7΂%BYZVR$f;qxfbv]=\g6JaMxrug)zd1XIF#2_ aPB xPF bYa aE*^49Ε@$"KLT_fJNԃq 2@6c`Gq3)|xCz+JʘƀJ| .EzނPwȠ<U ]B  XE/J83@F[3"lIAPOsVIgyXwϐ0 C>T] 6 ihjPGuJBikL(ld&yQ0>d C2~ctTOͪVt8 3  ĊDihpX?ߎ'xDn_!{Ә̒qa7؆Ic9J!**/z3&Δ)@j!4Ovb3kX@p/PAE0A8$N?Ds7uG.:$|~Q$W[?jy`Ec;*+~EЎ-?D|vh6,X$:p Mզ%S&u ~  $f@qD0ȉ%B3p+*,8L#42(݆K(a^0]\ԟmRD,YP<T TC? > VR?ȟY3Mm`ͅ:\RUBQ!U $T LH %Y $A Ѐ88&(BCԠ8 m*J]VIb M|ȈMd]5G8"JWN|rC[][!U˥!2ؔ XAqK%YRD 0$ ƠcBCٖcS56㋩pىC<5Ch%\u|>V| G)BWD8[@#N'|BKpdEԔ@XeT.D鈠c<'ă6 )"|[@ҽΙ@ə"m6RCI8 86xC'H&2<+C.5 Edq_|6@<\R],1؅xViRE:\xFn$5R[ F\ ѥ@&dN)XY%L%f 6l:DL?\ ˜) m56D]Dَw沝gfY%CMKHLjnG$Em BHeD\CCPEBD 88gJU'IgV@D=ȃ$'e hULAoTJ E"KmdQHcoDVXM(iv( `c fJcBC";h\_fķ!&j:(ƈ\V@Tq`?<1Uv4׍RN=,ǣҒ:iK8*H* 4ML8Q)|_:ătEjS<9]*\&:L;B],&nJ/?v(*z6׊Ȧl y.MpL(-bN-*n 1($J@ʃȤ |'<)ҺvNdM·8I;wTlY.Jn!vn.~0M Anl?Ȥ0 J .*/ 64,*Z/ܔ/ (|.o큾\)`HE*A%8o3z<>FuL/"CA;0Upڎ'h ~,p4 XӰ=p< êh"mJ03oX^;pWH5TTS 2"hfC\T@ U)Ƽ /1e1T8trE68<0DNMM䉭$2/g(7f,$UZ pt''"RE)'*/$M8CN2-CZYr/k0 *4 *dY SɑdH;+o|ԍT"1CŮ~(CH4Ә;~p1D&ň5D,/8T2?ueǶciY펕$T^SvT3j7R܈ E8Pn0lyHU Vk<$4&<RPY6z =51LTs&B* KٖuKUDmBn^gp[ 9sH̜w7GaG8A({%Bgq\βxiC5`S2?@ jqV W8o>DRU@~xL+N9Bx,xK9+v.yllꓬx"(C 4X*QwSt*C818,膒+W[iSyjWkr:zf7Ɇz-:a7UDpT<;- B1HyFdW":R8C5+QV8ތ  ]+Rw0($, 8>S;mxKT:rZ $-X@(IC8 q%XaCAr<\C8uǑT8 =6䑴5Zzs3Sk;D+Zy /Bx,@$/A,~B*$*,8R6(CH%5DQh^O|7yXȻڶCk͙|k|'3N\:d5\7B#B* BA-5=o PeA`TT5@z`A&TaC!F8bE1fԸE~%ǯ_?B>{/^;d` 6% ,N]quv:Ԣ']'/8Oj7TUfWQZ;UXgѦUZApHAuЈ] 7A Űl= wgŤ[;re˗1glQ>x!1lXӥKy: qm82ƖS7ζtB}:^<#YϡG68p.^^(-Zڒ=$NK׿ '1&2*H~immH?tmP7lŒʢrY:;B~AGoБy(!T츥LtqF^AkQ)mFy# 0pYABG|ʊ2䇾q3IK\HE,Є^N.nt!5xzǛɆ7EbH%30G,@UYi%HSgdT+6FG3q @4׬ZsΐgFD>g PzPfE1gG*h҃idt'[,`>,CEXe)q"{wNXn!X)1buu*,? ɟDcf}yj)i9mgx-\#Jt i]s1IٵJx` P DlfV`[d>fu҇iц';si|\P@\㥛~t騫.%tͺ]'^{T}᷻]/0'x*c| )i,*}{r \|A$&}!@OԻź4j;z ucꐆ&gdC{nXA n̘<}$ʔ`BV~O| Ηo}0߄qxbG{H$'JH';ZA0[C?r7> ^јF5x{T>ҽH8T6 ta .Y4d N$7T)32c=l$.:ֱ)Fi'㺪Uɢ |s4ְ3Z/%F/C@P4ءB0)}S!">Eq!!9ģɣ %0V?Z82Rγ"u2HY\5RTY yt΂DC!Q8tR>=옆1攽gGs#-A5 G;Pˉjtdiz#De>ORt(Xm#fLU)42: e^R5k(IxXq,v04Q5/ozOPꔧ}Oa$5tC (I/&,(&Y˧N[YhֲRC{WqՖ# O:y5s$*Yvrt+< O.!UF ះ}={T6kuE.;^nKRH2.cᤑ5M-J2[v&0;՜K(/I]O5uBK.JDE'ظp܄+#Xz;RO4)4\ЪT+6y!=f;6wteI>lt K] _p8a1Yb!;3&ۀWhQ@)vfF =hAa `hTK594& J)S2t[)]TR\v'Af2ԥF[VuP/i2!e2GR1%@r]0| _*4=6AihC4adC@h\Yxi+q_yzidFYeQQ+>q#98α#A f0Br#ɂ$9yIF#O.f[!n<Ǒ!t 厺VZa* U*E@!&"²,r,$,ȓ,و$3H+.'!82hq*^$y}҉2(f( !@a*=vf+1M1R$"r1+N,)ֲ56WR'738s83'-F,I.FDAF7b''1Mg#`gaJ!AC*U2 ==`>>=3>3?s???T>R @4AtA>A!4B%tB)B+4 B%TA9ATCA4DE4B4 PDEU4EYtE]Ea=E  *3GutGy>sD# l?{HHH3IwT *TD3UTE@TUˆ !:i.ou2&:d@1 /M+q6qa,a52P6Kӗ‘8QO b j'y}KxF̅Fd M:èB!DVo&6&fW&&F`a!Sw(TR5<>FH~|unjՃnusr5\ X5bXxg ]X;IU'bxU\'F_p\^ !b+  !"""!_&@u-Xu"Z4i_sVGWa/6e5cR5dFv"J.0)ШR|Jxk)ɞvxG|`a:$H6i:xE$@c}mjlb !@ @6VҡlTVx6a7c0>EFQtq̀n0+L=̀BWBlA#=A Bbmvh/h2!ԈJ!<zwn 1T.w4&Ws,j!>cKV$=h w!pŷNz!̃a@c&wu7dyW@!|cAyTN! ̵rv\zj'dࡁ`gd4gs7yH&7N7BkDa6n8!3WG`a!@sy'JbNA3%in`v 5aAz; ^A$VtndD!t38X-:z#@VӕGAh da Hz:T% #$NB!Ha3(a|!|AaczƈCA3'bAvn&5(,! a>€0MYQ|x*v!No7gYa!wYeaulӁc8f!ޱ"o3'!HRHAh!l ! FbP!1 ~ǘ鰙Ө`,!eg@! o8b!Df$n|6ę?&b|$1M |Y=#{^?f@hb)!aLs:FZrdH Y!b@FME ,P^2^>eޤ`AĀ!{dzK)L >э~A`Vaa"$d*lh@A,`@Ȃ>&ya35՞؞h^eL.`5x$>XGUx r?*?g&5ɽg+.=|aM l`"L`$`"`  ` "(@, V!e _{{:5L-ɔ )vPHHE0CB\G,L[@V$~QZiUNIeVmWE] fbIfZqO?A%,ŔQ;jh Hr N[Aj>fNJi^Nn)YWUuJjjf27k ;*6DC x`$Q5LD` $ǐҞ:Ij~?KnYUn렪Ms N<383M2DL r".##|M(7y.2όr3cU6Z3N3NM|#D/5֌6}ɳM2VDsZl^2p,f7G8V3U3mMCX1MRӭNU_چx4`O]x喫<X_3sk+O|!CN7cU?\N{&9N>{w7V.zS>N̫WY}ϕMVّt>䔃>LSvߏFqޓ,;o+vo-Ur /T\NBvp$,& P\FIpbze!aeꐂ(£0'lvDpІ"LK@`R+FqZ"O0SD h!cl>E07e^Hm[Zx2. 5,fl(Goduddxdc4#y xXKf4R3 pP Lu J͐3 `U e ! j1u YKY2$a k"0 S[`10ΐ40D70 n8 r=`,tȐ $*0pT 81vB LcLʗC7V,0k@e9){٘DhJ%9y}qp Zti J: ^ u371H )<i25(J5/2W rJ16;EsC7 bs.Nq< KJ`ck+ZS~@ I 鐼şX^U[ <C. @ | @n׎ٮ.Nn΀ .Nn/Oo_ Y |z 0 V1\.Ɗr#/&Kz.O9Q9;=\@Ͻ>OEoGIKMOQ/SOU`iNc>Y_Uٰ nHphgkgF`#[)sOuowy{}/Oo/O&u-/!І ]/Oo/O/m~0v = 5o7c*~= A .dC%NXE஁ 1\l%VҤe"m{dΤYM9uOAQI.I)PMJ-t4jDWaŎ%[Yiծe[qΥ+tY~ u`A&\[=jˆM2Yٖ%\4˞Q(SСE&]iԩI?*5gDEfi:gŐo'^qɕ/gsѥON]?{%wVٶln91BC}p@ 4@TpAtA#pB3ˏB;CCqDK&B)qKƓqFkpF~` b؁\.&ҮI(rJ*-i+qK0sL24,yC3-;qN:GmA # lHݦJklhCgP;alnC 6=$bxD$&QKdbD(6QqJ+y9% c$cxF4QkLKFҝP>{{^^RJ¸Q9  yHD&Rdov H\F,EN'=IP1GT $FѼ>=[@[ P=#Ha]."d&Sdf3IhAӤf5yMlf3f7YM9A$g9yNtSdg;u~Df %2M("0ςSvBMFThE-zQfT< R:[AZJ:rm yR_L$ᝩ&:E A uDCP YT8O\ig^I Q|t4() iĬak[VKHڱҕՏ ҃Isi6 q`#^5`dqD2(F#$JJ(u IRM{Z5"S5q0EH01V 6kYl0$Z4Yy`Ȇ:dVģVɻfZy=)_uqV0Xn. ^#umY7.y?xpq "ZFw&p;`4K< >V@m}G}qRʤ8>x5/$*6 8XSqIe2i$YnCWY'0oLG Y4,-q8EH*֤&^rOlxd%ԐwXu]c'{~v!,h_~ci2\}F8k()#4hx2ѡ i-X4exX#0*hi@87-bO(Wj6j;<+@6!:ÆFxiw[>4az1g 2Q 0:Vƀ\F_v Ă ih}¯d{tS epl<_ۺd`p +pCi(8?m Cfx)X>ě"rB;",B#10Sxt`%04t@0kh##@K3!:?Uz듆it(e۷8+gUHa( ƛ!dXd3 VKWEYEZE&-oX~snH ~g?6D< `$Ubb83?nl x(`*!deG(+`5ȁU7 k>kHi o+`+("ۆ<07@` E%Hۡ z:)J"!>jh',I[Z1Yww ((@ k ǐ 7l 8tO@& y ?ی=e bA\?DD=`XjiЊB{ g| o6W0؀9dATJ_YW!sWt{TAiG> 'XUK>L$"' jbH!EO!̡70IkpTSbiV`Q2X>,wP(@:0ho' s؄NSy 9h d [06aDp)X00HR@s`&Do P 8[h Ђ H'(0&[>py\sP_A,n(pp18'5S$H@opR= `@axE&(-LKTLTMTN}Ejd7;apEd7/Y.K'P``HeyC`|h"}W?PVPtX ɴ:؂U5S````Uh$Q&Z<:n`*( S:eS_sp*wU$c8S*3R(U,X%( pHPk(F_=KO$DL"TŤQUhʃnLgLZPli8X HUX|07N@f7ЃXxJ(Y0Y$xUH4QQiopJ6xTb@ `oX(NA]uX@nT81-A@OC'pkes\SД_h=T  0YXCH\$0?^^b;ôp Vl=Juʝ\&/h*Yh'wp%g]7MdOT`k HXx(NX8XJA3@MXOPGp=[X80sȂTe0ĥfHHt]~]$I0[L)8 p7 DKXQ-N ʬzEnlc7~c8c9c:cntL;e:SEUUZA8K> _UxHZG`o@(N20`8Nw@AF[x%H%fbXuLHe&XAX8?o<[@6bEUS5xS]Yjm I(ahpPNP0]5iq(I3~4h.h7x+Ւz$yi^KtP xR%-DeuPYH8C |EiZDcT8Hh`U8)xU@-dX5l\5pY>q6-k,fXnViWxJ7xox7psnx఍ڵ5YFn6?bV_ic~lȆȌlʮln<7߅>?aF3AmﰴhUG$H0|8S5fp2XcFMe2˃uXn< ȄJpCH&phjoMpsJ8ChEX@?m,AmܵJD  pXaxJY\%EpA(Qi„YM3X)p0p`90%H( H:(DJ)5-l0`l8p¦ q QS7[" Q$q!b\a,Y~]%ił4/†#Nx1ƎC,1`apWea,I'N"0`m[檂UY'%̲d:=+-LyP&S:ТfbX C6uC@8SR%Ѝ:ZW|u$ģ :M46TI>ԲTK" Lp C %N] L)J)D30"`?CYdU$u1٤ 2qeo[r٥_cYgkI5ds 8lJ" 6QL2ذ,#37TtQH49\$EL,&AK0, X'Y` 10-$UH9zXuA a/K(( &(ь+6D`TbNK{ bpbC&0 A%ܒE8`017:TLĢf# 3ܰCS\ 2p1DؐM1|R,٨M4X23 fX8x`'&p <;k`ȝN8qQRP*#Z"5-qD(ҁ!:!R+'ʨeb2Dt4ָ< QXJ% pCFj%Sj_jժZ "Ck/Ḡ5 ,a[Âu5b Vld)[Y>],`ҥ8ioJRlD0ņMMӓT:-DF7t/|ָbDE.r1֡"q. [,g ַAu0{zЏ*vZU53~̌fjQ񪕬ekBVS(_"ULULX/"8 ^1M~s1 c0! kL{ܣKlb !oE,rQ ZX}-`8Qʋ}c؃1*p'wm_**8Z2/9b3cKnmifVpv3199zγ3"D6xf4xȶ84ch+͋|hg‚D|1-)ZІt=i6rWʎ]oXh[:go_,386%@s07f3ƈA d,C6\bȱtms;^7w;7qԺxCnkw/@]| hF85C|8+nS|8;񇣛<5~Kn߷;p[K!<{~77ы.#=J_:ӛ#͸xgjy 8Ի=t*i+1Fa N!yɿB_@Zק*f/}w}{C?ҟ>7{px>m/Ѣ\cZ.APiT?` `)2`:B`J)!<-Cq`¼/<!M` ` :`E]Y` `='t_`BX(UZ@AɏA!Iqِ .Axva!aaz!~aa  a!! ""!2b"6":b$B$*b%>%BB(**m(BXBDA–hBh(jB^i (lj*|vzI6+X/l v.8<“&h(pfdgT8N$j*gB(hBhBxVBr&*~B(j'$ꯒdhI&*8A(krD#(lI$2Mj"l*2l:[#\*%&+T"$BB%t(h)p)죮vk()PZfk(鳂ꠎ+ -lljA˂|B&8M ee 3Il*R,rA@pAn_B(Ac+tBB8hb Ы'h!X'`pAF!hAB%$&-Ʋm+(*BmYL-L+mFiIBlrmŲ'*pX)rjn\"md'd 'A(k++%Ah%T2A$A)̭^n/I(- 麫hp5nTfIخ:5b:A+`B&DBA),n߾B2/2r*u,0Kg (TjryIav` `'ˮ{&xIcsc7fmD'$:A)B:X;"AiP%̭jBmEbgɴvItc3b#jetOI:ubJtS_gIuo vKwv[bVwt3&n7Wg&`*˖5!)hB(측BZJrA(h(L=7[w'vhBIk&ɦr8wkbj vw8u{wx_wwksf[fB& O/O%$8/4Cy$0[Bp'иx% 9U5kޘsbffbrs 8_w3&wS(yߝ&8où%LBd7&d&X2v8brB,L;;tgq҂{Zycy/旜wwwvwq:wI$hBzK& qɝ—j^[ƅs::Ol:/szf­:zyy{(sg bla0S9Sc{:g#H縯0Ӊ'Py't3}λ:x8;Ǘxz|nҁ@@&ë{\b\fxI($'PBV|]{o8ȗ93MA+;:d 1sB4C+\ lIj*Lsk ;G3}W,1s ڳ}r;h4NBݘ+;s;{}Wѣ=#~&0x'L%=gO#=$}{~雾4B H,%V>П~~}[wy@`0#;Zc=to>~cߝ4"LB,sc|>?;k\@˿D}D(,?8?ƍxp`~'aDMxc9vdH#I4yeJ+Yv-8G8iygN;yhPC5ziRx(ԨTh k ,xņŊi٦]n\(!M?p` 6|8 Ҩ)(Uh%,+8q*|Y^؈{FuNQ6϶='Ο6}it ظmhEqq7wN^?<~{v۹wxdʖ sԠEۘn۸ҭAcյek{Nx&8*9唋A$ - 5O@e.,J,G2Հ>g'civj0&6 Ƚ8,ܒ.P>|FtOT,f{fytoFؼ1!P4@P$DHc @6(9J-L5 2i4Kd6b2Ux)M#qvi6袍4(hi.E#lTd+E*Q6lݖn%O-<57*56SӈI3BI0՝ r1 nxqTeTS zҬ-$n%Tsד d.U 0ƚxG48 effUG@~Ywd\ʦꨥꪯE53gSϵP0Ce!^kvl2HHi+xG{]\ڧ>_,Ѩun%^,x&#mFhel`08'iF;ͣOHJߝ}=N} ] w֩՘Zy'k`j4&yhnLJ4@e'w!vp$h|eWyEgf&GࡌY* dcđ9x!Khb܀|D.gcp>(#mڰ6TfG1baXD#EZ`F^2[]qhX`LC@IZRAɡB1^0,%Q!)IZirDK[Q*^:=%HюR00bg0 ըiU+A?bMjpE&zTZW"F"(Bc_Ea7qkUIT ,uizW^p4nUJT"\. d,UbY[:}]SJ8јC`@) Қv`lr RC瑫mu[]^$1T 1Qe$3U@_ [.+v[_ $&ANcb0-6D-u{+8XbE-Q^dDV1FؗdYR? }S#bd@7 BA;<TJC .D;bHZK"C4c:pHM= L4MEt%JTOT 0%cNt3cQ'RS :<dI:(T eN{h%<JUVsV7=U> Sا$K/fY/Vj <آbL4R4HGpJIU\D|d*RT#pBQ-,4#]wZu\:L?oKu9"`UiD^U_58Ra_JZBo"JcL]J$i[OV$Xbccvd,ԴcKXw6:Vf1{ndqGg/Vig`VhL6\_V hiOX]VjVQ6Vv}X`AFlVHL68b6Lvp2f4[ bpn3B"^ooj Wr7p}ur3*]Ve5t/sw^qCujtSvq"u_B6,t voסdbum`H=q7j4WR)x7h-Z¡WycIW$xeHk4xIײ@8'uLHFK}[Xf_i [vs؇X؈X؉X؊X9hыhcI ׸0mX瘎؎X؏Y ِYّuK#b8X0C!?'f)pa5…@SYW[ٕ_cYgkٖosYw{ؚٗA&y39?<y9!Y9ǙYaϙyY9!y5E !y@,;jkE"}>"Zv/a*:;k`z$*Gb:BaAld!Xa V#~#R@#Z ZZ]#aJPm9~ *APL!z!AdAAnA!b^A!aNŅ!S#e AfUd޺",椓9] DbD4BC\)M-[@ Ha:` L    @!46>@ԥ$A @A`@` v!  L! *>A ߖ##ۥ!9ܻ&)@ (a " @T2>FL!!:  Z !@" * VV|@"z Ad!A@V! aNaVAjAlK(2 >$Z`(aك%`{(']A!R@ $# R(c |aA`` Z.!Xa ^b=&)NTRE (\Xс$@An}Օ <A^`¡! `2 ûCBW`#ݤBDGa> :4``Axa` ^a#! aaA .aСWa#A#bAP$$b   !"`$@R a!>` g!Z@ $@# nG96=\|>C" c`. RAnL  Va = a  ĵua!`AЁ ,@dP 0@)[WXXZ -ŋ3jȱǏ CIɓ(S^H /_ʛ8sɳϟ@ JѣHwZ4WN2#+ e`cV,_xF okebpӌ!:&7tj@zg #BӦ-. wfTwe&"9ΕRQXFYqIIX&T/FIs͛$)-dTȓ+_μ_I`+_x+b" 5b-)k>x&-KY{Ūx<3 ncA% ij6% UO?s'kEt $9jh:#t\Ku HfJ?6PF)T˕X+(Ӈhr-R$MӊJHH1tR@ P3M+2K&ҠG#D 28P *DcM)4JuxC BJ%(a)`F1D5L̤SĢ4$HFp$ &iђTW+HfvE+nԢ. J‹ 0)d)YT@b  (@bC`,4pO8͒+'$IFYdJz6pO%Wx?8Ï4،;t H0T .7G\0WO2 #L1Db- L1Ŝ5`#5i# 9>68)<@#А,tH<عFupCC@7L ,"5MMY3F.tÄ8̡wCd@}el{h,bH#4a 1x8xc02ƌ#R`2p@E01_h".<Bt *#y_0#B Ebx $'IJjt4, i@< `f<,daCC눑~Cꀝ-A"K2%/(1f--LСbGH 8RDh6nG8‘l15PJy2`e_\XΒ#ĥ.y_#fb?~D4q fᆂ d`BfIX{qbHN<_Aah8R2lMR>wb5*@?ă$ 4ӰbHA򨣎K-8:(Џx8# $m*8Z`#6^:<+q0˨ijWȥ~6kyƐ+d`ц9ӎ1^ͩ?nM6 8da;\JX,0xM2Ia4H@;d!81?`, |oijƍ,Zo4oL?+哏@X@3t/>.s4s:$ӥ+Gq1h3l5hԼ2C@<5vUnTO:nNL2@m@,# <=JȌ>PTψ@23<88o#9^Y?0C;?k3+H4?ã2 y^G#^ yXX8Zq1X T z"엌kErb!0^-v •@A | I` h!DV@xj<yP \Gюl i1ԞD@GpEl爐tS6*fSSZD~pG&F8hX> ,C x H@@I$Ei;pkrbYkb (gCΠȐ@ ~,D&}M+ASMX+I*{%=OZu\ 1d71J݌yG̀j>$d)Y֒ z%!4i*Am=!(6JdwlM=F=@ӟcQv` l`,?,HG)DBSBHIQf%38g$&Em@Wд+xd)I n iN82C+F*ikN3u0 !?x$DlzÛ ].,0$%RjyNXE^'ry. 4hF:&,'VG91dG% %K?PFin@ ۸S\D5/U3uw@:^dMuD:hHHQ4 H 䭩}EO\UJakb!0Tg%Gwc FcLJf =-&Z9Xv7|Ţ%}"n%GKd'Ia3!c:llcDCʙ6983Aa9wcֻde,iO3Uyc6<)/^t,qE=K/8a~x%GeM51PZ@` \XLCMG⪎h鱥Œv=hq<)a~Q\Mm8KAƊqU64~x$Vx ~?нQ"tE ?A@FÀ{V?Qsmo6xnC} t(/G.&G۞pEdYCH/P $ -yJLOY $>|ge^8 ]#ZJeؠ+Fx鸧dF$PG<1Rx|H2P  ~ Gb#{/a?¡wxֵoKӮ2m^Ř~Rto8Dw6eD|0b}kd?wmvj c[-͏|ESܩ&yE)Hq i9' }x`Xap &PP1# L}go~?n Ȁ%mֱ6Bkx'4s mܴsw~@/+p }0 xPy;fm@K 99[]K@@s0X~-- c`qi-F` A8Q  0% l#Sx#W AԷ)Yur:dw0f^8l}{hZS?K!(ȏyo)qc=h>6@{ !֡1f@{͗>P3  N4g$}q Ws<)YmaFgBq\#V @8 My;PG6d0 M3eI6j U0cGYe5 #V4da"wl$Ii ðd~r#9'u!A Nx q)$ CC` %4~ᄶd9 t)͗ %R;|sɩo(V9 4C2$4S}b,ՃF`28 0Ɂpn8#t*Ehd`NafYN@AP:jz bנ9h@C :j;r9k@,":vVyW&v{;-! 0t6.S;z11Iw)۰mנ0@% 9Vi .-& ( ː]q7 mV#u$F1i`0q"pY ~9d¦6 sJĀy Q}&eovB:!'&f$4 3v6݂9KD!!Cک9}i4DbD R3U#ÑgVV"@w;^/ 7UHڬ2gD:pLCb+}a%}#k( @` : N! ";PYc1(,%I7D[~#jLkm 'S UK  p' \.+)f,k уO194/~s9>p `9z0cUF r{']  k+Q#.BdF3KHV!9Od p}d 0uM%xh*- 5&f ̣˯xyQ4m>І}ぢIU*]`P70H>6rrB `k) Xй`gYbН4صGTdl$ < zKof%KAJDd¥4㺄f ii$#ñmxgnh&3 0FnrHHܱ @R` &p 0̲yZrj$S.` TimF4O3RBYulm%U2'amh$xͻ4 .dD1ANrjvnذ tܷ<246c7Uu<|xx ` +]ţȎ,A+#<ӹ+:?k 4p@Ґ\=ž$[:$^qfXB:ojiP#>)dF H'9*pa?Y֑F- [} P rJ2 } @`v2M`ӏS~4=Ua@3>D@ f␖t-ysn k!`  40 ˛3dZL񦚾2*'IL\%`Ұ Spvؔjk , ~ }0 p@RV3h!3I6i O-F, 4 " o x [byM8{wD5)`ˀwd]W ( K,n.p &?9.vL@@ PI؄0N~փO d= \I06Zz!Vl(hXMVFr +8ktա  A$l9 S~ޗd鉎Uؐԩm֤0M<B.?\qia᢮ Ӹ bd 0f >pb܍D0nf0-ּ)#FWoV,}STT > -tE#܉kT;󊢲5.N#vDOH k &0~r(tuלĒSdBdt $Ͷ8P~ 8@`= Ԡ| +]Eӓ6K vh ~`w2(_"O~SFk?0X֐/%jm._dP^ >QD-^߸q+_;2PQJ-]Sf2ܶkc̘ā{MM>UT gbfD1zVX[<`Bځ-Y?kf*\HV/QoN|Ėwbƍ&a!WmAH0l$-o(rq!|"iy̑7='x9q51ƗwƳLgPRG`EMOeHi8jʖ"JNi!IRN(Dy&?^e8F m8H2 T)Wo'y&z:>Mi[}mکHꆪjΆhk #N `Ý졓g\71i{p Jom'~"m\%>zq]G##ƚA |;>|qǭwIy2F@ɟx?q|8ASV 5́j >e@q W ?d($N0#!6WBk(0B~VBD BHJ21P[ϣaCX{)9ԸeAІW0Ӏ7ԙ 3 \B@@P0, eI4|&cBo$\QF;|#"M#ؗ8ƑLpd*p@O2BI<$#*VQGN gxB3hC>CI,әT4%X^NɬqL865yLV܈( 81 } D" i NpǐRE< LvDQXgPJe= yP!ˢ,kQp d%L`Bݵ QFb wԦ5Muh ]boOpd6)o4'n (%l;aO+| v{ 356M / [Y!&9 xIjtG q8.0[^0@ A\rUbckt&~Nuc"?DԠb7l9ۗV+ bT8t@I qkA]xZK`(aQXPKҌn_ѓ7{#\t>p,",!i@" XuE09pBx R@+*$"(brnj'A[C%9DL M NTXl17ꘗb6CyH 0 ke<S('hxx_v,bÄP:e6fHv@؁5Gw)([2$Ȇ<&"&Æ[ѱ Ǎ҆}D+UHI4!THLJ4@Ć͋.]9xwsٖT(J|BˢdGd@ԍ9rjM{͠}nHED8ڻ D>~xqؐݱ>~ Jadc69'dHH\^dHDˌ>@eO(x 4P+5J(g{ln 4íU6hu LBQM Qi!X4r5.xi7aS$ݔY#$C$S< lH}S6]7:Sp)R^4Q3S\5m]sL'7QhA~ippU@/ ԔT"rEOC]8GҠ?U%I*%W/-U9FUEV;ՁPOxgpU'r]+[=iXPJɮg@I}h%p R}MvC00'`iL jGA8WPXX=`c8ס4gX#L!&zXmRM1!XH@Y& ]$נg:x؇pJV k8@ XWL0Wk뻆Ky?J5Ku[X+UuW8+ wPB-\ Q.ՠA8h(`@\HUYx3ѽ9\Qixk!@kDYmihmч?^6x%^ q2@lCqXorPvHqJZ&.p4aO1ar0 aPƜԽ2ڐO;M`h7G?1x#vMȐ%Sc`)qJLMQ7hMLWipH ؏DʦG5e{(..hi !'kÒ9F;,̨k7S6g1sp,%~_&(̆ Ng˖Ӽ#~b~ "'Я54)hT20R_2Ȇr;;9mɌ ]5ϮUqPc !OIP׾ I~Gr$Lxݎ`.KR5o\CxUy]zA1eİ ܸ yUFj>Z^oCR/?͡-xPiX0v Kt ~P>pv"o(u> Nq A^q肬d*q=>¡,pC(LcDIX q 0)eV_r/Xͳj ڡXђr":#(s3w1iBG&VC|%Mwi 䆜*yINObVFtC?']DqtYn *C\9 OD.`&l,q^&K2_<Ok,y8\"rk7; r@O&v whux(oX2_~*ovUwD i_U>Gљp2uyȋx6)?`1zgvTG˚PLAډvphC/ eHzӟx8[L!oP vpx-~/?~xrmro\$uicOߛ'iɞ CA Ǎgq?~7rLo6 ;,i$ʔ*KU˕2gҬi&Μ:w0C1i(ң6M)ԨR(?*ŋ5TRdCj>Jum-ܸrҥix*YHBV u3nx?lg~Z#fMl=oaiGmvZKhY,99VAyQ7s#zZnfS@a&Oֹzw6o>jV2R 05_BeuD^iP:Gԃ>a逳k !~K#?|SYՇe"9ڵ_?(0! v`a180>']I7$]XuiWaxcK"4:LC؞Jau;&ٓ8+R&lN;nǷ_QJ2 <iaFF<qqadĵ%|)-AA1a7sG;'^;'A M>>D#c]7<#=KC4ei Me'OFuFN}SM$JNq|cݰ':?~3ؓMoٙPs|h(ۛGlC3])Oo6x$#qqFc"Y+_:V7o1z` R=9*Q ^ ?ha< f5!l.DFE7ATa]qp<0a0С E !Ddypγ@ ;`8sd / qPp1\\k< B 5pA `|#0`yP7;hC-FY@dH4ҥ9E3]C~C# oDPRf:ae5wU!ز,QɸF ,;!F btcZ"pHOx^P22C87et"2*;j`hUHcqig~TcpFFJV*1X6 < WB!Ax"`,}Ă;A+ m6 (而TbPsH@\w̉BGxy|eV zJZkm@pF@i5 ( xx$Xd(Ցi`}tp Ee}.y ie'yyHهJJU5v]P;5gX |C,1EBC8, Q6C%P5B^M>0));#`cClcI̓7jCTdHe 5C<B$,@|C<0DB$B>xPF4l%%^^~3C$<@)xLA2C+$A0;,8(C0x@#@*8A F&8!C<j&kNtF,x @  8i \ e@fn&'0`C2CD'& DAkJ#\v~_v7;@ TxC\';Dp@2:TB:L Tmou.oG6|gTFC@$ԧȂ t*t@)p(fg"D'C'T&B4hY2(p)hETH5X6H<I<a~;D6H}64>FOC86AH 6XdUZxFY8~ME**D1><q&*'V+zAgx"x"EI8PDfVeiku+^Ȳ_B@BHkg`0\*D~kα+ήjeURDy}qk,˾ jl lϽN@Ī^,T,~,dCl,bǖ,:ɦl,B ެήlf, -ѢIl&-6-0^>8-VC]Z-v-1׆^!0u *tضī 坼ޭ*eu$8D)l h )>ADX@Q~f~.膮.C(/mtY C,.҅...//&./6>/FN/V^/fn/v~////քNlί///00'/07?0GO0W_0go0D!u00 0 0 0 ǰ 0 װ 00011'/17?1GO1W_1go1w11111DZe@qB @*()@({((2B"\&2Bp(p'Do(%{MAABم~rr)K"*-ojfATr- &3.˯B.0E*Y8A.;D0!'B2o (dIC:;ԐB(3Bꧾ>뷾>Ǿ>׾>>>>?? ?7??GO?W_?go?w[5Eb=MA UQ{;$> TWdI$* ̰Zf JI-ؑ8 SmgE5 U5S#Gg]HuUPֹKn y :-ZL-h-6Y=/XG<-ZRRЇ[pܵұf]dA֭йЈ_mbBNYYne8 V5!?O7 `̯-1_NZ饙ni4Z1X ,?9z0|udYf]֖$n{?-/!O\oPɟTQ5`k "i9 zK[Yz;;i"iO<[lEځTa\~sϙ_(Cgs yVoX>Lo!% ^|8ѳ÷Gq%_yinux+8E k\&-&AAlhCeu[W/,w%d i:vԓl-eK.[-_ 4/vѻÈq<ӢۜE- b&F^؁I| j`Cula:miҐ6 gh&Emg ;͵p21 k6ѝnuvng䡘.A_7AM`$6%y+r2Q҅ܿ%&?9e;{Ϣ6挍iԹ%7Qr4 C7!U@x%-Yx{-)TO/mzsd~ eD}E4d ߢ} SCl5,zX:<2iy'6j)3˱}o{vO"x҅_8s G?>_E1n Ђ/zhaf!^T*k1ְaaxN:`ADpIpma¡!>6DPH, .ތ0 p mӮn6⡀"!BE}xJp -MJLPD)!-o2aMpo M.̀Aݺ-a-11'wbH2aOhOd pFWh'ԂJA ^n!pva 'baTAzl tpaxP=6aͱ1a@d QN&0aa휍{pNtL!"12#5r#9#=#A2$Er$Mp"ph jXwTx!PAdnd$^`T!t!p٦С$MKaF0 VA! J4p1n ֠ 2G/30s0 s$~S!U+RFkNAbOj\wBGT" |B araXOB7-O` LP (Q,yPTa Il! L8]ܒ-  `An!IN@<)?4@t@ @ @4A"KM#!Jd@><{|AjnȰ(= P +AHp+r+4,+R"A`-Վ"֒.0tMٴMt-S1S`A *3AN2/!"!43[`d !A:$` T!L4b ."T! P`&!bm"|ٞ|,a >:j:  lV ,A,N X  LA1RQ ^` Z*.  " @Pd!@@ Ll $*A<?TAA6dEvdIdA@ T!#`b ؁ F`XfL :$t` 8N:JN_ # L*DzM,Fa !<` L "Rh \@H!,@.n. *  "@hI P8Q-A @`tU l ,> 4l@<`H-A!\ Lvq7wuwwK0X6e(R za@ a"OKbP 5ܯ4mQ9/W nsv hLDoL*f@D١ .Tbf@RV#"Wt` V!Ad*`8@bZ+6.!fi#`LA! qU-A_ahR`P ! hY`^@b JAWs6Rey8X!`@4B/j:@` jJAL@hai J bdj@kAk@D\`abPC%n:!n"SApLaJÓЁPB;a)9a,@mD!h`A,R̀s;لCKa8yMf 881Gxsg%77!ȀOz7wz{QhQEWW4WZS&r E4'\!h9, 8aB`# hz{h{qX A 8@ $--Vat/ ڨ` La7T͡c9 u<_+!ؠB7A`xT^9!J=?p9 yYbv#daqT=` X !Q8 LdfDZjA @ b*Nn S*N ` z}!8q >ښ7Z՚W@j"lA >s)g & A v%KYMV(:j|vPzb:S$|#չ? faaB`*`8@\`aᮜ-"e6;>A СdYR@ aU[џU}!e@<9a| AAg=B!`qf} ;二RɿUZ1~#z<\$BJBV6,Pj XyPʗ[}2dH;Ȋ#vhlbbv&Ur`1&NX#ؐ1@̃_R LX>(sB sj@BNJu8ycS [$ň" lLdMY4_6%KV4b˖} -aZ)[9͜;{ :ѤK5S_Z9CV`p}k*(Y %TR <̛;=ԫ p*(䓐4(̏!?ʏ2"CaTdR" zL3z`)ts@t M2ИI2 /!GR1ÌWp4LR@!Q$܄`'P'ZXPI-IDJ'\Ta\'O7L` :PAf 1NTJ%Ҍ+e|#a%T KpL-D7eq"0#h.hba8f"GP,zJ2tJ`E DT!4x8 Gfʫ.!-ȡJx" `DOjHa@8B%7s ecW (qF3 M1$6\0J,I"||J(W1Kb#"nhoqǡKmVP6[m`2&۽J}s:se]5r"~0y?G W})lB /(0ܒ68a ,LMa8qK LbKB]/vv7:l/,`X!D#'HA@Q!`Lt,ʘz4&a 1xH L ;,J4ь/ \XPb (P")"X3:e~~OKV8K^/ ;ئWhF+T,j v '(*eAX( `^W$ȄqX.:\k̊%rń(m6 rD\"Ŝ3qAw؅} Cɤ=od. _`'=[B.0bq^-tT_/#BBz i5ȅ/xbjxD.$+yLn (뢧6r0P¿Gً" s/1e%q}!Ϣ/| #-a~ŵA"[$¸0.~a #E-2fd^ OOTzլn_ Xzִ){J\ʽd#&弢-]U;yʃGSEL6S6r{_⬿$r-adS,0LB;U$2IZoa/np̿羪ĩll3t4=fA}ӭ/bW -~| U{W{Gh Ȁ Q|B7uPu  Ч_wuEvKCJRC^צmǂ7~7]p$Vb5h79;ȃ=?ȃ@ ŤPBIKȄMOQ(SHUhW@XbI 7sxg4ikȆmoq(s$vi! C\V ā|KKKZmvp@]7q(Ha8yV芯(Hw@2` z8d-AuH4 ]$x&8^},XL.70y٨ȍ(v}@騎Ȏ񘎃BG @v@)Ii ɐ  )v` T֋~huG3԰`llg(T(pH]c*8J{X^^? A)yGeBr$cKɔMO ' A SuƑ( B u]җ]ɁV1ֈj+(mٕ]_m]0d> (X[l͜ P b]sڶ= q-s= `X ؁-؁=m=؋؍dd0~  m@d0S=ЈJˌfa* T ۡ`w ĽtdlH٭݋-ԃ-؆5h]= M 0p`  =kSȫk̜ ;]gpж=OKN ͬ;!.d8l')+f k+2Lװ= A.CND>`pI`m0,N Ϡ > e>| xHЋdБ U`º pnKp O \*^#n^U\ό  n꧎n Nn뵎뷮 X<]N$= ۬{| Z> Md|- evJ~h ouU}ا0\ɗ Ūp m^.0 { wM`iMk wz?xO  ( (135/6 ,O.5_:KEoGIKG 0 S=CY[]Bۀ @goikm{ sOuouoڬɐ/Ho%l[@A {<\ wILlppP L *go%鰻pd`v*P]~߰_ݏ(BOo?A~$6w>Y /`$NXb.^mG!Y7NݿTdK)aΤedN9 -Hhџ<%I(ip1ҶE{_pZYhm/\ ]y_&7VSDѝwC"BąRj:ԩdkgsGl:p|L'%tQ.y=/Ao=ggyAI%l:CCCTG9lRŨZl Fwagq#qQquq"'..J0/(!FdQyHd 3d'5d*4uFq''=36?tOF~iƙG~g*q~4ҩN=早M@tHyT !pRQpG]T܆mEbKV%r3qbRaM$Vq٧Q~чH2xICȚά鲛9ۢZ˭RdI4Y0 6`;, A .2"$㬅)eLSM5XG3J)Fd5)߳ĠfیhsN<#pf⠣ϷÃ:t@.N/NB;@+VksYG Wu&[bS0%ESrqbJ0I/̨J+12ͫ#vlȍn8Bvfɉ597pB36≧KxiAOmxgMu*VnU-Tu}:g:e_iTGpwKDՂ֢q0&}B<}c!Y1`c6׉?Lh´Mgja t,a3a mؗ)1h!Yc `ф4# @S0hH+Ov1ig'"X46{+x!c i-YlƤMg~̣8Ȱ2- 8:ERHFD:0.KC2IdJG/!xTqL@+])NjTřNdg#OU-^5 Gqf>=no+ڵkֹ r;_NѵkC1ә^NJ&p |/D h> |l+M8 p12Odb*12! P"an݊x$2> cE(F*B,l"ŢP \ qYɉ,b6ovlw+ BR :#+# g8#] U5"rA g:a4ai4C1,ȐtP9ˡ(s32ଖURe^CёEc1 k^ +95.lR"s~mA }qel1dP;-?d?Q1\࿿3`p"PZcӄi"Z؅PxJˆPPh"2MȆmJMDax-2=d ;"L>1[*bн+ R;dҲ3aa(Q`K`C9MO >X8>^ ly2Oh[:JxPaX!ݲq,t/@,v H7\Ū9{ӡZ!#c*c\08@8Q8~`8ep0,z#A Аm@l@< aAphPWXi2n"RX 0[ca)aHh1qQ| Bɓq8|> <'& |ډ|Z4t$n yxJ"k$=k+uKZ ~uD(PǨ`}~ЇiXIʭt#̚}qwyE|]4^F*~#Fጀ +&j҇pmFpUmh|ѢУk,1a(K Ȇ8IxW'PkHiцYX')Yׁ1Q݈`r=+I˒I4;A\q 9~a!lɓF+e Q*3 M1(1|aSpӔpG -e>'`&lh; )Lݐ3Uq('Gt%ZLęԞ gSh+qd@g>=:ezPL0uiNy?"MKl!~ x\:΂I ErN8{ 셼pNx`G8 OېqqOrLy@Vds DWmi`Їp@نbةbYX`r^c-exSe&VVȷimNb Ɍ#ڄ\S WDӄJi0WNT $cA}|)}pvf6`$D`HkHb y>)&;Um>s)ZmF[PnqNn^nq(~W26ɠ *樘%GIJ2X6+.;JlbMh0k$kȖ*̈́L`a~O- .pY0k,dZe q^,lP;Rl6 Rn ^k Vnz^a7 !?FV D<88x9v+pkiKKhuXv(JIJb&b̈́_\muitA@B/tCpmntGw H>nJtKgn' /hSۀ&ZnOuӰT-FcPJ:y Ɲ v`Y46Xge_uљjCxrRp_N_ɨpi=8 MIq> Vu2^Bwpe^B(>@YXY$0"(d4n PU=0F ˌ=y`׆}1pYcѦFIMx&XXn6[Xgw+w7(0Hp#pM?{tqXpzc^b%y*iX=lxypp[1 .zsUGuۧz'p A"L_M?yGزe`?~r%˖._۶-ŚeSs%KKO1dni;cۧ2*̪%ḫ^ U`~?ŋl1K+7n;v̹v }tI 1&Xqye?v8n+Rokq\F*Ҧ2¥k5^0 0Sڶoέ{7޾.XE՞ѡA~ !"PJA1Ę!-Z2VjnFxM[Y@Qi0m.6k2 E5]8f<%c>2f6lsc|W'VQ$MYt<4#P2cU 8$hESd|` C6QsIXA 4<fYhL`s<[ʴe;mMZh^ >C阌@Qyi4,&DHs PY Sj#}q7q\# 2H?D$ Iu@ [C 4bL-ry6`K!KS)#a$qoL@ZFmh!#\c" 30.ִQ# 8Ύs E :CiYnh&T]LMLn8<#m]~)g3f:LWV<(Wcad8F_];u4Z@i O3>4nZcQY4 멨Z*jn[@-sp@C@D0x"y;߈a $3onDC]c4 "E]B+T`*0*R6CS?ː͂#HIN@X GsJedBGxN`6 T@Ga 8ȐEM'Q "I:Ad$}BK?GT>G76m6q mSnyL<@.Ӥ ;`~aMLr~P&o) u$ NIi S:x"b@%j̃>X'מ%-mk[Wu?\k`CDꓟ'@+'Jgr0hEx4rG#Bq&$Lyةl?я` }pS'OT7 Rc(>5D!8WR2VSa q C"rlZۼu0nFbត/8:+x)@r;t%aFve?& ʲG{8/70J& mYh-69r3[6o QqNZ@BBd1ns®^I1x 60gJ+'RѐJbYi7ֈ5b(NmQbڰL{v {S2jmCJR;Vk&7 mXQ%p^ {S"5$C,a.5[oW՛b8uc-h^vKG7YNtL{2'>N PG¥0!}I0>dvKKw=O`GR%*;Jb ~c(c&2gUҶ ey2&-&T eQXK^83s&ͻ㸓4}kUj7 p>cx5hA'n*PgGz{>yo0'%4#4 Z(xQzK6J2J*r25dH0cuϓ~f?oI(6@TՄo8:,EH ǵCHMLZSmYLp[\E ` tC86| ` .ZщџXXd-^J a( m4`@T 0/نDyaJMTi5 éu0[`tEH_mn_ @d3(Pu }@M$CN 4 Y;xD꤃6gg ƈՆE$v _S \+]7t v2VHdO/!.\--Tt\& B8.TaPLtLUZ@T$)%>D_SF- 'W$oDʇ ͌D%i͈=6 7Yb`RDMfgkII.4Y߽OӰ7pAӤA(] YvaĨO =0)ғZ0ŠfVUZE} 8 cDY*mZOF(71D 2ŴEeTO[E1Q5J;p%8}A2M*źuăR;(@66Lܦ،L,XDh12,J`v\Oά7ōmEܡRm+vPyp83a41ߴ(ȫmkStXAAgx\s\Tvn1-]THnBB8X5C[45\ iNN ۖֆ;v&QJAب\T 7:C3tC@CguicaHډ.\2RDZΒ.#uioM9NOᶊnd5pF9/͕.hamp!U2+B-A][]DEdlJŢA)E1XJxxLCaWO+8?|ٗ1`A>C}SZgI4t{ gi>LقSKC((WawY,u.*ߜW'l(Frnpt0U:gx<7H U}o?V3@,yw hNNTTXD4@)2x,M%8lUB\z.aol"JBſXce,6dCl7n;D? waB=/7rܹc /`ϟa\ 2k9®#qE⒋D*,:,#x1(̄"pacjمL;m(}Ă<`2Q'`Q'k9DW%8pukdrL5ݔN9+ILU$[U g™VW_ux':>4Gvj"v8 fylfҢѤG<ꙞJ\Q]]Jze{5|R/<R1,!,2"hS+9TG(yxjyC7*YEy+P8j&@DT(IR}蠅޴/pTO-5ŦRըg[_'W^1L Ula&XGkQƚxj69e5U~qs:<(|^\*H߷.cK0! HFH|M7Eʗ> 3`@^P6ц#lqvf硥Sǒi):l,ƥpJ]|~[JBg֦dG5iIfĀG%PZC,'I@*jʻT%i0}T4 (8E@\ %(cҎx(=i.5XDёn1(Ç) a u0֦W̮vPa< X<0*ˏ`)7 )ͨ&b_H"/@#1gPHֲV3 p<,2P\8^1 VPl ciI!ýPI雄H#BP(Ċ6aXd]g&7ck_7\ kC( lFB-98C %5+ AXuGtJ[Ma=;D兞U٥$>@p]4v;f;-c `jLJ*9)s)3any[ bo[\Wr[18ts]^׺E0\9xw^Wo| _7fPoyܦw0u\` F\ p)< Wְ7LavaE\b)FU~#q,JS2by, A00f`@˼)Pt:l6,jCF,;(eXe2NR>^)!B0FSl@;Ҟ:IMȴzc#?HSpl7XֵԸ|76}"a+t%#. Nix XNXE22C(l96y5$ud$Qs4VMRr:-{ۥ1AoMq%ӂ񷧮Q].59 `Ž'q0b!N!jOL $a v:$( [ e"avg؇'#0 _0"Z4aw\ Am%F^|AAZFw~ќ .QPwZXP h!>jB |l8"wHh J,Ԑ" 4 Hz=aankQY! JNXo܈d eb ߠKC ϐ! !6z5/ 9 iQܢ6sd/(-m1 f.@Mh`k f!A6`1 aV A0r5LD!a %%!,>aA`0A-5!䑶4S-^2inN QD1P!Ӡ>A  'BAa=`~!Ha;  -¢&"aADsa ^ n`* @&/Ē,2B-H2M,.dL2MZ@鮳>K(""JR&nA?1s-|3*p  r!6!?e@%AD3RQaā̆"T7O/p+C|AA:qhA(;I$%I$Oa4a !A@n d CG#l`aP$^A4y.Īj\@Qqt/txsr-4.!+I.׌`/qI?f(bhb![д H&We"VgT)e:6u8d*ҁ%TaN. VQ_=&uh6jb05sPHNJ*A?VuvgYnNBaSysz C&T#dgp(ae)#>q?h_!N!PA! qv(^% ڇa,G%vb/qH&1ICcc?vdx!d*e:! dRaY nVP`Og!D3e?j"p'AnueZ&n h!^܎cu6R3/`& f]asԨ?sܱbLCޡKca Du@ iOXgg r>fPO_#~a)|ZTB8Fj#"d!baD ^AK;%7`%sƲabzQ{c4|[JAH3(,a.)!hyW7g) H7:fZԈgY%w%xH@*1[kKs0q sxjVZi7BX6t ^YKeAX-Hw!"3N*TAP!k,$ ʙ)#Xw!\TkJ>`ڣG Lׄ#z ,9G#֓gb{LyI|?fJ4 `/BWAdx>B4^Z hX5B@A !T%ǓQGF/Ad >zGw|'aڃ hBa5-N,P(ԡ!! j 2dBf)A*Zbws"x>@k|ۜ([jkh!,29-䇩xeB!"kJA e` /,VTpa3yGz+7{+v$c$GYV!lᶸ!@!RA^%¡?73IHijBvjAMHW``!A 2И-[Ba4!AUf'.~88*!@aAR 8<'US?s)񨐛 rD+aTL0tuA‹QhkϯOk& fC, ;f|H j-Mbv#j3,y'eB+#xva[l7묿M^Ӟ즇D󡫯 Ze BէjG P0XAr˭qLb)e[Rzaz"iemwxN%Wh}"~ ٮ +ωgeRk<ǧ7>RԆ?( P)V؟2;'I3lW83k 2qCWwCᬙ[︋cyA>xg.G/=˟P*矓YOuCScq 6 et!J01NWDl.z#LNJ(#1`F:R6 X8:ZI\"DLZEBA( AEu(3?J(˟qor'DPzˋ¬4+$c8яpִaRPÅ!~.iG"N}"0ϛi~NW?Jm-^3ez/HX@`0#]zY!dZZoOD'0ARg/\i0"#K8us\H"&IJժ韣@:P@R[#eBrJ6a <᪌$eGz+'q* OT9}ˣ of1 $_2i4Aٰ j4+ _p5 iZ  P f` av  @!0p K XF @Mg k CA\ڪldj `ljcPk$Sz|j+WڌWh^cDx#?#` `E]y 4`!cKPgq aou'@ ˰ ` !q P57hҀ 0 &-`Kc175]Yj>ZLK   @E 104cFYHXL B0PYQH#I1&!']I5't931x:KGI>9 Pq^,K @.+@07g_Dt7O<˵GRR2BKz1GH.L l(O; 'S[r:Wۓ1^S)L1.W =ƶԩ|PWJ%73SҝIDY븧:ja{dk-@ Z *I;j+959Aǽ!~; 9۝H'G5W;12+5?D4ѕCWjd &SxD#L?l-X߻:) k(kf 6+|AlR xz݃w/oRrd|9iFlq<| =Tiͮ!q aѴ"*% =US`#1-W,Z;+\Τ|jK;]B;JvLAoöGMu˝(Ԗ1Rzy,ˊ |ܒ=%:7A1,,27rnj`)hz ۄr @ +-Ҝ1ӡ 6l؇G]ϴlEƐ]9Il : MNsۑG`̪nvk5eܥ!q%cE3]  1/]7A-h,(Wߛl>nb 2($ !Z* g4 0ApgdFpf Lߐ46Yig^gd Y0`0a0ӭ M'}E50`gk 1.3nFSDF7S?ZϱN{mB$@ ( 3 WQI` I0us1 N  {0`}p@ւp ƃ '`gXf @V{P L۰! &@ I-DӝN)>+.^ :<4[zs@P\, |âMMMN8}#` 00Nã @ A` (&P ~ 6 ,P Pl@  nYpG 'P6 ,i (pl pj`` ` QݘN.'-Lޡ2=B?S>(vdZu 8{PxsJ!RW f0  k1`ր iy$I`sQp R`aB(xR,c0I!+: oVdkЪ+Bm\c-䩌Ƀ,}=g *caֶP`m+5*#\^% I@IXe͞EVZmъ iRE~xma䕱ZrIeObƋ=Ydʕ-_ƜYf%VhҥM~|Z겟Ytꨴ'rmdi&-D%pe8ᛀAM-[Rޱ`J5#'`ޤ8t7Vycՠ!GjAmIni|*^sD&x\ fcy%1'ŷ*}_at J+fa[.3co=@se>xM}_`qn͠al(頺ǚg0fx9aX$qb0CiLgDYUֆdIOFpTjmb?C H\7Pk|C X,@ 7EyGd, Rb`…2ш͚fD&6щm)$Bb@6\U߈V!NFF24%Y}0+^!2ڸR` Xd! H wB>R 7zɕ!U@>J#Z@A1Zic8!d%H7S, Z0*ჹ,zb(.SRvi0؅ŦR;2ћ MLrӜ9la: >T8ъi `)1u$!`08 *-|fp+;:qH4 a, ~䃞  DHBb1VyuN_Rf pn56Dti<7,Jqin{q#Fp$c>`F2pDePG81gCXEr㱏yX~E4`m$ê˪z2gBsuk]5#&eSC\k;ffFV W&%laC׺Vq uoi#Y\aJ~?$Vmkm2yCö?q,F'%̣ 2Z͕t4Oak*NۦK3 l/ wX֥"<~7zܩvK䇕:n`]59o=dW%Xx g;n{ٌ3Җ9̯ Jg76zη=]DB+XGܧnb06 c\y'gFi(B{@R~{׵ӿ?0xZDžkP;ËX!x0A;ӿų@s@1@X0?"! D@ &^,IP8ATx{A9 ^A ¡!Y>30=_#=K&AU8KH )B|IIhG8DTDmKAdxB1QL(D)+R0Rx]F1ɒ,I\PQ%R5SETUUeVu՞4m ZAUJ(FM"@aDc.$ OZWjklmnm^`FH[5N;^}-p"ciRp+Z؅`~؀؁%؂5؃ bK]d`ItD"،؍؎؏ِّHc$MA0pZg44JMO*(7@{z5ڢHڥ=ڦuڧڨکZګ=ڤmګZگ۰۱%۲5۳E۴U۵e۶u۷۸۹ۺۯ&xZ LÒUE- Q-1.H*Eܒ`\Ղ06}Y.51HF1))2)ҝ20h 0\׭%5EUeuݍ}]}]%5EUeu__ޗ}-HF =UxX[؅aP5@%5%24\cv7uS9V7r`x ~;,9Ca?8?@AEAᒰ:@ 8( 8"+BBA0*(#(A@0E0'1&5f6v789:;<=>?@A&B6CFD@pbBHFhCZRx :6a7T+"a$qet J( E( Bph'a4vhPfhAh DjN0jlfpaȨ|bBxFgNe]ubemh(Em}grdJ~ MCeq~drv膆h臞舦苶茮hr&(hJ&Bv> xifvi~ EJÞ Tp빦v `뮓JR⨀6 `R98lsҪ ( !ʮlC}ٓЮe0̎ lFڶ30ûmjcֶ֎^vna3_xncՒ(vb(U8fHoVTl po)z[ Ɇi( phު& !ii\` ghΆVU'W "nqq)lpde'"'|2m0ri2q '_,r0&)w+2~rrV3g6g ?74 /;,P&ԋ.$<f`. qǫq,ϻ8#P:餋):Ỳ@L?Z1̣< 6ی3NF٨3r6\!tcۜȼubN MFAdM<Ԝ88}6bioO}dc6AFݐ4M .49A`Hߊ:2CV;M`Tyn騧~+ {NNb~P龟 <qɉ w?%ghL8w&?32CG_=AwYtwC6|!4?3?1wJg!2v i7K4'!e@7# ].~#$MQc0 443anȡg$a8iPgU=nm]"Q-ZTwU-cmm+yQD8I3*JXM|\0@* 7S HTB*IZ%A-a,2M2TRSNtea U$`RTO/A !eÓ<@9̌a aAy'u0%/dM ' YB7g [k2O`Ñ!6!́P0sA;pKDZ26\ O^8u8̀HFF2@#il?N0 4' HRҵC|dHLnxx&H#Pa(KKHHrU` 䬙 ZEX4}B0N tL#OSZAiܲM U*8qB">-!ĩ@D!x#0lc\#M8n{Ypb˂^'36x4v>AM?M0 XF2]; s 7gz c^mcffDW47P~Ӭͱ"."C=y+2$lR1D>} S gr[qe$l- d\Rx8ه:*1p/#((6&}uG;9ԤNUv DFu]Ŀ ?s .=s]y P٨)?0G!32@ >l:(MJRe e Ӻ6֊)ٶE.Z`m"4>ȖQ GI߲A$vd\ 7ۆ$iw`'lA"|x9# vnK#'dmAs_1ϝ C :Bj6m09n¢+Qz>:#5ycoii 5#h>AIxAܺ3 H)n1eqmKлo%?]9q Sw WRhXڀ' eqmAҰ uw  TsA8*yqCMf![X&x(*,؂.0284X6x8:<؃>@B8 v0D NPR8TXVxXZ\؅^`b8dXfxhjl؆npX$@ q!  VwH!w~B0|aP)o( 5xQH 0ola7z؊8X:8x8 4çVA01ZPb53 ƌ  |8exZ@ 1 s0 4P< &pQ QXPgL S04!P9  H U W҇@1`J(i1 !$I !8^D)*P 8% fB ِ LD  ]Q 0`xANAZ'O @%ؐQ@ c7?z r-!> N23טA ၚI)9< lؚ !)A=YYqF1yy9ٜ9Qֹؙ鱝ܙiE??ry)Iy00Cp @!Ԁ sڠ:Zz&ў:$JP,ڢ.02:4&ᰢ97Z2&0Z2pt4'V A yGGУl81 pt΀ }m7`Ԑo20ak0 0 $壼iQpuym9@' ͠9 o: yQ }mlL`' KpI0 xV :t mB/`Tq`P  Hx51P &!0&09 0dxe kb`8#QJNey` @ ф f'8P iX1 **$#pG `O0p 70T(i V ЯwC (v "  `w T` faOvfPQղ4;6+8[9{:۳<;>+AFG+IkLNPQ۴SR{TV\^[`a\hnp۶qr[t{svxk\++۸{˸۹{˹tgHꐙ_ IIwX @!'[ູ!0[lH Ɓ{JLJr M 6 0'Ny['y*O7^;'GL0]I'Q@a K k:q :ѿBq ,n G`4Io2s ze O! @ÿ / ) P !\Ó2NIK`QdA2  $ v `\( M@ Pal@q,ǿqx?@CAaȆ#@~)i90 ȬAP0șl"sØO23 DpD` qDbʗ:^Sq ulz,Pɺ"ЄA 3:V4c5؉ܬ:W> ɠ?Cўql a  1<2͠eC=qG 0 F ~U0*1Ь|8F=.4Svj(qB YNC/1aѴȇQ&])y:p/1-X5ࢋ) >I%z] T3Em.\0K a Q}S]3U)B`.]E&q rM] 1Z&sOHM@1P Շ=R׼/-eɠ c@Fl} qlM3ڼ)zɶmSع}@уdp=c>}/d j2`{OJR@=l5-=wLǶl}-6AQ9_h' ˰l; ~ 7+9 $L@0wRQ ŐQ i 鵕eJ@ Qc:p 4#A$" eeafg-H ڶ4fN$Bz 9dlThj߰{Ӡ6噽{p@Fw^|>nz< )>\o j# V6~^-f@lmh׃\QN]Є~ bJ+@  U" y&hm@ U33^3Ӛ  5xla  $j @ P # 37c.7\EM?N_ K=0Јી+# o PpAs^ 1c ~=$[7YrC _ڃa @|5 װ£ ]To { wu8$7?Ti1D~^AY) p`AezZQD-^ĘQ(I!RH%MDRJ-˗L5m,Hb8}T/A 4i.`|`[l Ś5#?,tQV\T\IܵN\YIW^})_… FlS-{uJXs!9؂ (@Im%`mƍun- ,oo3-(4V*:e  a~ؙk-"CqVn6]|Tx_oZDy%R:K4&  $7ùtG$Ċl (y^yaqM܋2=L)ȏ@kVz"dҥ>BLI),ēPFJye,i @Τ2(ѺZzY(e hv)vb8 JPC,PPG-LHK8i1MH[7T˗fbnQRj&OopSd,\'8P҅bi`OբB(e6ToӥQjg%[ @\t߅Wb eb4l6^t"Zm &x3 F8%./|tV$/47c?&lM.ZhYAVGe9fgfF*U%ZhETc~WZ"xffi}EQ%FZtY6 Buk[Jc !m߆;R+lFe:Bw't;]hn㟇>zI4Es3Z{ᅲ>O|? fG?~~k\=πDDE{K`%8A\yq5A;H:8B>RЅ/_>heo{1au("a)R-0 N]Mo I!\> ъWJ$IGS:)IG*WWn&i)AK^JD>-e)ʂp%4a)K21`oKnvS%l#y鈴(FA&gUV2Y1q#ϔfaOk,pF:eR4h3 OeZ#1MV4$l&!`Sg5q4*2LYr"rD`L*L2G3ղ{Q_XYԨGЈXjXcq/3YŪHG2s`X2FDAid"#JNAJZpC; @?K9 4cVc4Zl1O8G$344я} YF|!u#3G;+*C&cdw 2fWogЪ& J W*[pմK@IDo~5+[B|G㜏p|0:G?Ac<# N׸ZdOit^ItC&5eDM2GfblD1|nvF&ޒ?΢P208FV.Sj9%2A/bŔ@1! 4ߋ;mr#۹=r7; PP P%Fi A@ uOm{,t>wr;Z#y!Wyfoc !(xg V;VwFx{!: ^+L`}7]#h@Cg㸎Oq]#N-e(yFf2}c?<}#QF|ؽDe#f C ,;٫m< 7 }:ۆ˳DӈÇ/7#xs) gW20AٰdH/ /vw`B(x5"Ԉ]6&>b-4G[q0 Aꡉx,zۄۡm@l8rCyBx B(̾x{S0~pȶ̈śN\B~kXOx[^!(RIRŒ/l ̐N=u3|1}QмхynRB L} R#%:(жʈ/Qu()0@sB hPXkx==f`+ɉӊTS^ϊ(v3-p/7ۇy@4QkvS@7Hy8)sD0QP mо  Dհ.m+…HXUVPWcEw9 %X:wĕCU(T轃ĈwpRRTsVoZԋHR֑؆qș)n 2KQI@U[x׊G$)Kd` I؅@^2IB‡1Ψג(!TKSRWcKeO"d=B #^=VZ2ڭ}z e `ڲ>(0hYu[ (#P!oڷ۷D$)¥Z eìi}" \iQA]ԓX!}Jc֕օx1Eץ]%^h+-#}b^+ -IމN %_3ފhe_4^HRm6z_.!%6U _` bP  6^=`Vf%_ ]haV}a`fjlƅa_bM bkU=b`jx5q`'UHxڸhf)\b@3R0.2Xr~XS/4\&N , g* {ˌ]46apXń] H0* ()q^K~ ?+n1hx`3X@ґ@֠xً a'ᕂP^v#5_`.s~ooR0o[d(i*Z0p%J}b֘1CaBip'riJ *_ /yPLe TS{hihTqWrHU3Ђ7x<0@ ؈'K0bpp?^2W CU0CDe(7y?6 g @x_0$7COT@Ot38߇W~:7NQ9ȋ ?DRabGrY,KHRnzx }pߠ #^ i({.W28ַ׌vѹ{ BH4P/:Jrpܕŏ!`cI1ZQ)0|߫|+r؃\wVviRwWb'~aG~\8w~~!/RV|7A|?g_\ۈU_,h „^P!Ĉ'Rh"ƌ7r#Ȑ"G,i$ʔ*WlE?+eL̜:w'РB-j(ҁ A0&0WҨRRj*֬Zr #Z]ǒ-k,ڴjײiTcPҭk.޼z͘;.l0aK1Ȓ'Sl_s/s3Дa+V4ԪWnͮgӮm6n.޶%;7‡WWʗ3o^Epn: hF Ǔ/?}9TD~ M 8 '߁{$H|:BQ  "H !ƏɃM2>N:#< 3P P4H x#=Z Dइ8ԈbdP cA4j 6OP]zg#t5""&mD' +r@ d '& f-B$( ]2h"N :)޶B@%"%Jx`;z*iqKDz Ym’*jW;$X L,ͦ:MCP8#ڒ$T@:;.U*'Er@I{//03I:;%Mi ;0F '@#<O TbK O<C|2)78+ 1<35|3=>> =r}4Iѭ!,MH*\ȰÇ#JHŋ3j1!BuIɓ(S\ɲ˗0cʜI͛5A $4iPO8 JѣH*]ʴӚb JӫXjʵׯ`&4@IfŪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+1 dIJ:59oDoܫ+ed)[bo(.l%L‹ƒ {32iK"Z (T?H ̓ 5hJ!Qo}P 㘇6hM]b/ :_BT ,`3 H5yp RK~  "x rJ3Cc) VьP 0P!-T!b r*QG^ PTښ@H^UБ\!; j"5 VH \d ;;pX&5xbA`S<Jywqp.>OR܂1@$:k`s  Dҷ]4>*: 3!lX,#ч4:MG{^W0# w`P2 d4dt3XU@L׸4R*1. u8ԡsGLk< 4O^~ `gKa eHY~IC4HٽHq2ٕry5fk^E %5ʎN5$ Xl#gzIb$|Y&X񉞛Glw7bnz|/w,`PЇLu60j"9 c,eHZ7%jJzt~ ?h7|I$akOL| c bdK3Z'u k-Z_o}\ 85~>^3d{.n] ‡ʛ>=k5ysn6%@ @1vn N_20*,Fzdm@ld,Oz8R8ϟqMKٵuM&,5[Igo1\0OR2-pͥq2ф@|1QsgkM@ҙ%}s|Wήm@Z+kِ2-s2 1.{014;6{:<۳@B;QF{Ej0/[P;RT{0]W\۵^`b;d+' ؂ lk/Cg06@HqytR=?(*yȐ@pn<q.1wQ- [3 )ozUpih)tpqg'1hYiTpNa \0'i Qb P&  OuL   AN 'Y8 ̈́I a ajP P P' ;z ` i(bG@xP 0 v"y§9P Ya  º` "qE3l  88p ( v@ "9(W Fq,e\~ԄYFL~B ZK#`F0!F ijl #f@^yz|{~,Ȁ<Ȇ\ȈLȊ|Ȃ<|ǐȒǑLɓ|ǖɘɕɗɠɢɡLʣɦʨʥwVpPSpb0OZ \\|ČƜ̼ά,\a,O||7os{#6WJ\ ]У  ױsNL١ ! \1 {@M'NmPҢ@ k'N&N bg'=]|K3]'MR=T]V}XZ\^`b=d]f5'Y :Q-t]v}xz|~׀؂=؄]؆}؈G! 8'= 0(0BJ-5n(= Qx zGZ4os1J/~I1. -! AN;6Wn@p[bIޮ!4hE~x.J ރ@^ޭ] W7q0>3ރg|*.J.@ ~g C+'>H^?M0A Y{u pyCG> 1 } c1ܝI> |+~-ʾ oM^ ꀑ/J+bٞHaO+N># އ ؀ /_2ޮ?6o @ p n#\UTe,dR?P Nj#_R 0n!o"AԞInJM҅AƠ7 Q _Ib>Q aa@  #)! ~: 0;a} ; A -- `N\Pn?B_#` +@ DPB >QDB PꕱWdɺE%M8A"C$UY$db2vPEETRM>UT'5*ROlUKRaLB))Ğz@pU'FXqѮ}Xdʕ+O$RG,fY4Ĵy hNR V_ 4kin,@A&vAh͝e3+ b_Ǟ]Be,@jۛrz^=P\,tVdf*bXj)%̃0f!xEB 7䐪vje)+-"j( %Ák3Ϙj/\ȖYtQ-]<[\|j[䅘dYfyI)rO )ʾR̫ѹl2M5dM7MPqN\kq/n&r?ݶ:CG=l';[ugoi=wUC ɚs݇'T\->xo4z^**l꧟~&mzHC]Oj짢>ۤB1/87d  =eؠwP5Ȋ﹝B@<FA,YP Yq~c8qS> >N%#q:9!$XHUxF<@0|g <:iGkp`f&q{4*u`>`Q) 1},-c rX%[M[@ L>rϡ3F ;3mJ@&$H8Qld261 Q(.[1}CITq@|I2̒/@A|j%3Ǣ =&ς +Rq",HP 1@4o8΁cu1" Swa.PiUF Ot[)db-8Z|h0ς AQIg"PX#6JK#8 qԽGtЬr{׿ Kc1GYC=\:kH$H8 ԥ`6ƑV*xyc|5XX1UL`o2">;>8B8 $?Fn"J*$HS6d i@P˄#x2t/D3!BG6O|SăXF6`0;au 9r6ܵd iH' 93h`J).<(h €?h#IAUԒM<vc &D,& +DlNӅ Q yE;@9A oH1XK{2 4Bl`($a,`A',FC܏q̃obɷp^Ҡ6Բ SntSTNtUntlFu 2J|gdo 4| D*x!#tl=߆6zֳuz%36Da$0}>۵c ,;tkЈA X >uJ5xɴr# ӣg0"Yh&zx9 izz#f.h+yX]èx!C,".C>4?lyH&kXBLɭ ~?@.24+EkJ6x9dA;$E<}h.`brvcvY0l25t2:(пX>qZt%e4482xHBB,}ƪyzQԵxqwƒ($X C4,PDXB~'#S c 4Ej+/Β>q,~~xs0 8$>xZA}᭢ǎt4bw`r ?wӆKdH90=B0P)S{B/lEZ$Ybʛ| $5]Gբ[DAyH{Jd2HSK ƒp$d˽xˢ9J ҏeaFpw親˿T́"أS`4w6HDM</X`;LUAъtB]iO,@xxuȅW[1>`y+2Kw)@ hH CA8`1] P>ԇhKsI5ߤ6$HTp#(TLljOEUHMRBKlLZUO'uWQP[VO=H (=5I f-S1⚍^ukE)fC ! 8Jr5XUsef%8KvT5!+#4׀UO"8X"xAEV؈U˴cX 0Wظ!ف(0֏eYu Y8 XAٌ0Rmٜ5ә-ցp١Y Iƛ%ڦխ3MrvLVZ:ӈYگŝ %oW۴M%E[[ۨ5 ]%'r۽Y4E-[eܹ[M\Zƥ\1\ͳ5uJDEE܁h]5%5ϝxXץ]Q-ۂX}8];ڕ RPY-a]](^ d]-#؈R؅9c0|ީ^d1݈\,s>%1~Xe%=]E; _[pu@-FE -݅q]ԇgq p$,- ``aߟ݈g_#iylN$gh-9aH`෵` vmۘ߂x⁸@"V5UpMt18(ZH.f# %&]0 5o@*曁=612` SX0fHb%Ip'8!RLM?^] D`dehdb؂~8h>\VNFbO>]R~&4CoP= W.p"߂XQ6S⍘2͋&*g};r0p5rbm@NugN?^3e؆ t<u( d` a%m@y} [eAnDaPk9YΊ ֹFQ@eKYMH蜮x!노Ѳ$E3arv\yPlYj&>l!-vlQ !ǶxI]킈lTpCjQ0FXm柶s'M^_jj3`j#mMTlxlnة~0"yk!~V3釴Shd?+nJ@t ڪC ێ6wDGO Ό}K_ I tLH3O'u\rRW8qZUY<8Q4QuRuI!H^c/ #d[g.г*- l=uv&,Fxp:u'Oa6 ? ?@E9>qʡs'd16wo0twe DA<~'UiIDiD{S{ bD KP8Ю'y:}HXKfW ʇES삸x }w3zF'w hz-0pT XɖS{*zXdnlw(# p JůFpa~ Oئ|7(o|8ҧw5kx`C;P|g!ExSmP6 Jh mppwl(SHyj}i~mChʇ؄ n(k'9Q,h „ 2l!Ĉ'Rh"ƌ7r#Ȑ"G,i$ʔ*Wl%̘2g48nI\:q)8j>-j(ҤJL4p9UN <۲m 3q7qD`` A<8< =Ł36C@LXSљA DKԃ!,m^*:d4řUqBt OO:IREI0&;OsǤ0!AAVCkjLp+^(#V<PKJm-\Hy k^Ƿכզ[k+YP`,G6ՒKHd+z<^=w"cB  [vTH!FʹCa) RWB8m0a"oUdXtF2DjxJ݂\qӅ}Sx>?0Zbc>_c'wAsmT32t8v{@/h~vzz  2 B$  p' ă7ct/)XD-pC " $x &xC)K-$GXpLă#X'8+G(>XL9Q؃?(Dh.0^h8!8@x2/ P1  2x2>5>( bW12%DI3HwpxOr(>H7P1D # ((1x( 2 $0< >H& CSp'k+BDYyzqeP  ~qq/#{q@F._w*t1EW`=D7js[zfo{cvw˹legFb:ɀE֓HMm+4C5W`sꑮk'5+y51}ncKҫH=;:h;L dEq?: H[-@+/C0)a K@w %>X0k!>Y;7UHQ> 99b~{NS>"  J5 b!ٰI6</5*RP4v z1,4a/L!: @f(L AgW)A0b) _rV.̰'2"80D%9p <d 60;t1wr #4OHCl'Ґ9l/7F6&('80ɱ >@NK`=Oɻ,(k7[fe"O Z|"1?Gټ+I ͆D0LKǼ"p>GQLf2!rUr)Be1FA.lLʡ B@ULx%{ɠ',7Y!Ax '@!' %m&TSB5{!3~t!$A`P p pW+&K{ڊG~Ab).+"˿_'^2o5 QD-^ĘQF=~RH%MD>-NK5męSN=}T(FCGjOQM>UTU^*JtX~VXe͞NfhݾW\u6]}XZFXbƍ{IǕ-_ƜYfDiGhҥMF]S(dS[lӹ[n޽w=|Åȿ@D0nko8J/B /0C 7C?1DG$DOD1EWdE_1FgFo1GwG!H rH|D2I%2!2%J@(K/ItL3/L5dMq3JSN;3O=O?4PA%PCE4QEeQG4RI'RK/4SM7SO?5TQG%TSOE5UUWeUW_5o!! Nf *! aC)߈ R4 c Ё.D@(0S6(DxG+fԡ!x08hp,@aw h kHHA("ZӃ >! r  &2`<:BXxG`R&0/UtP,V!Dq1!9# isL,`ߠ l #@ @`JY4qtXN8V@` 1 }64aud>mlkh 6x`5$A:ZAypOL;awb,:C6X3 [ȇ+, X X?xXq@#&`||C{>8g3BP  M p:  6 *Djl5c$ Bbhp<`As+:|4#DX!p@%1pG6 Ȃ(ʾ (hLCv Pxkx b OH,fTb3`<)OKda BU}n1ġ TAF`9k|C@G%0lEFG:M|U0718wt`(b|Za:,PmXFa>F Rچ6'H]:"`̃0&``"I >a 1]1}.5m߁T@4,y$ X-$p@)w4 P@paX Q>w;45`A7@Sʈ& 3k,n>x5 ?`JYc+PH"ȃ*q aDdH\@-&X':`E `jv岥%Ǒxrm0`(A&GkpHF?\<C @CDY@4  ),UD# `@ a*0 Z! j}0,5yHzI_:!:Lc߰?~wJrB?}p hxB4 0gZ@WR 4Gow0m+A(d赒As~pxVyЗ&appH6X~yuH,})Iǩ%q`5܇هyЇ m@Up0 "D #!@yxXeYAA1p d&:PSB1A[r1DAq`. zi 3?-QΓBޑf l"i AyYÒi"MM@QΫÒq&3@tůiQD5 HI(DQi@1NPPDQ<<Tz!pW#!Edžx3FoDp$ wՃp6"Xd6NDÄMtωpyO0QCx_(*eЂaHJàMxXOW)pB#ȕ0 u &n:h3ih2QЂ@XaO9JpMm ih`_S ;0 eo!k(E \(]4 ]؅Z]ЅO/ _Z3uˡgxX2%CUT@Hd@g(ӡhԓHTG`aOZԳ8P=ISPT`VuE65Xի%[4\^_`^a]N@cEQPV$ bmVdT=gְOl(@lE .BO&0*fPWtjpw }P|A6-ЃVpUPQ`XuLW \PHXՓ9OPOXu҄XYx!Y Y}YvWX%$uWф-LP oʼnLڅ(YoJ١Uڔ}ڃجDfت K}M HH0[ޔˉ\McQ[[xۻLMM %c(]X$\H\,ܮ$mm\$-hW=[M(ǃƒY &N\ۙ X6%]%ەG][]dEə@:!^=ܮ[,\}QZpGT݄xHƙxTgxÞ8]"۽ ]5Y@IQ_h_k_1Me=ߜ4[%`#Y8ޓ*}AUNͮ,`=av ?!_`>aGpa5vNCYb46܁b*NTՊ&V.b `Z@`1.#%I!\`́҅)cȇlP?*c&b@^ၰ.с=dPdyi.`J,\[@^ZPy^8eBBKT%d>]'pۆ[V e.fadc΍,[[]XӃ\`cĀ$fƸfہE&>^W` @.QZς$΅2}gŨy5 |vZX{ pg W.ƅR:S֌)쑬P1ᚁ`Ŝ |J@bPex |(OXExdhhc3nxj.:iq] ? ]8Nvj3EFHj5K?k$ةW)|Hjj*I։xi),)ґA ecUJ%l@[hhPU;0WFQ8u&,©n1VOO*)hhS5A G(q8m6{ >;ZzB W l܅n=eoZehфnAS\]ꖍ.>%je\.'@(iضZXl&O_m^F0>z NK&O@ݬv恘mP K:6Vx[ϴp7 P}vVq: *wUԠzagTW?"-8r!}.V3u4#A IRtf9"E;x݌1iېUZP]S^II@[*fm9Ji(bY?*Nq:$6'5en(\A7ЉZY) e)Bx+jy ϣjeaeՠAꈃZK05)iryXn2PabHtU#(" De '@nZ+&\c[Pd۶:R˕B̛I(8&>%F16[% * )2*D@X"P0Iu6 SgboP @D+tK? !@ڜL8Վ=42[e5" d8PC@DmR)ԴO`a2(S dI]8Rӳ s{ƴ6q@?BĜ~K0l/18Cq˶e#ރa1ޒ6fr)dHN PiӠnQ5^e/᳠P88Pă.DJtI;P4aY l$yy2gOA U"4w#UR"B zc \/{I4ш{(UQqS1 3#4(/!/B%2< Mghn"(,,[&3X&/$OR1u/쐡1c#%:2$xH8?䑎XD'٘0bW! C24!mbVi rLLj/Z 4J̔a*DeH:N2s MgB%:̧a˂|N"!ECgq >w ySy1Z㘇OӠզF42 `H9sa+"ѣ%,ƨNj 5:qU"#\fN hȑ)C4h&)Vx+BHgjF<##=*f:_nÛh` Vv=miwg .!O]`Z+A ^\,1CtƔ! l 66T|]?ْ]dtAP!M:OAyT[@؇>.aWk5Gv, VCm[ _ܢ 8()o )e܁, iti],5Uv/l @Lku^' 9{+_n/e8N.J; 拫pc)`z%Hs2CbG84SK-$T~г (<=Lcx{gA|0kFlUsvg0e'ڲ)m r5b#CR<[HY!te$oqg1LА2:7s#U8lY#9Nc3hiHǤe'Ko,Lۣ HTsihyrS.s/kG2#ČΞ|pZD2Ӛ6=7%EcM;I>͔‡ ;-'[7x1C\> D<A P.OBcL5G;AA B^ J5K:^Na.BALTL^YMèT6X7y8>lbtCl"7!YԎ_ABQ.1-B&(Q{<@_lAd C?׫!aATL%D4C< KG5ݢ9BRB/b 6Ĭ1 ^F)fC4<>fnfiHi>e;J8|VNr&kbfa-B0tkn*tFGHc&qgcp@q>'t \a.gturA$m'zgd]Jܖv|'btN؈l}' Ԁ{l(N$fa, @]XXJeDd:"E3ƨNơ#v|ߌD@O (&tv FDaaE$Epєv^WP~)i`HXWPΦ))+&ک"QiI:.&)`FꢲiAJ*6馆jjAtF)B*v'4ljjqb*:)e)`zD Da. !5U+$JkFǴᘶީAX ~DͳՉ:!kE. !&RV+șD]8j D1"̈?c+AP)Y,|=H6 S5P "4+ǾINhF(6Hq1WJPx1D,`,B,.< 8@83D AЀ D\bHˊ@2!CT;,|A - mf":DRq%AI6D@* ") DV@!"r|侈ļ.AmL948U@l0n䲮<.b,Ƭ) B9!0BDC0AnH \/FH~; C1@@C+ dU @AdõLl58S@1nA Re :C@4G~|H~S ~'1W%GK1 `!Hd_̎5 [pE %hg)Ӏl1 FE1q 'e YP1"+$G#fN&SrZP1bY&2\2)'@D) 3+ϲVnOв.G. Da+1]~17sxy3Osuȫ1)5gu@h7Obe~39F2[4:*33p*CD2*'$:l*@!B7@+H@!C3)ĂT!X(C*9AX,+HB^.41D0$C* pqCl*D *<$,81C*s#B\X ,C4H7DC|DA;H`@{ l:0I+5D,$9@ ,D@<4$P9(*@,,(:<¿?!>ȁAЀ,|C4D-8@7H h]-{C\'xC$HpB$9˻#YG:8 e;XC@T,tB+H:7C48<$CC T@6L8-:8$&2d5}; 4C+@)n޻/5a؀+pKC<|,;,B::4TC3Ȃ,| 2@@'\ dP>L0dA@ Rg:GjXM־8bE1fԸcGA9dI'QTeK/aƔ9fM7q~]笭 .hl,m\:PK2Ҳe1;L&DIkLȒNʒA\fL)` *P!G,5›#BP6X2G||4l2F٬ˁcWL*ӟ_MUv|{幐43J6WzW2d c?TrcLJ(\V 9H3|9.'=ԫL1boݗf;A6>C}=P vB898Zfh9;Rt6l3pdZ+`q0 C8`#LЃ\r??>l o+M4çMMxXU|067rZ}ܽ6lp Opvo=W9h6 4aFrAaVrcW>RqYPߑYQӃ4%@ ӐYP0iP )(71 րXѐ?mAzV H4o52-318SwVV#᜝+w8שĝY!9Q뱟ZZq ѠZcG|Z wq?@@;PyRj$@ :+:SS3톣;`o?:DZFzHJLڤNrҡ PzXjZɥ`b:dZfzhjmd" ehU&*&wP&PaGT2k7m`͠9 : HqLkJI0 @iz/ TX n$(D4a `QpdJ&@ 1 r P p'QH J P xB  -@ {R Rъ ௮{򭺐zD '`{kao ۱!"[${#& {,/0;2[A7k1<>?@;B[=KGkA=\\Z TSkU{Z\^_۵`;ba \P˶knp;mKo[x{z+y˷{k~;}K[{+y ;i"EG ++k4q G&LjFK&Za_d" Q $kQsb R% '` 5( 8eA (w ,x! a@ +' 0 '0E! \'# 뾶|o` G %(9\bG0F! FDСHħAA??@TV*ŦAC@D^LVT,)PƥA᮹ ƙ T 1 ]Œlt JGKcQ *ȃJWlȉQY2qQ6`,0 /wqy]IpyY Ť< wJ4:#˴S˅ɐ6Dpſa,],\̩ W YXLlɕl q[-l4P P y{<Q< a 0 P.Aq2X'5ћD' @01-} 0Ck(mmvh 0`րo#L/cT)KՔj YF0΁q nM;dImVh60| F1_gȑh\,((M ,!Ç 1Ë ` PۣB0=JP a^aF* n!cҠر76 D pΜDg~FP tlm .U A u юޠes! A2 .G)Iߒ~EHe:> B aP\Mg^ 7&XVp pa A>@'Kt4GcsA*E†1UV29]ԇ5tNWjǟf(evo~zkzb7tL'|~炑I&Fk>cǝ@wZӃވp1;?A>ςSV.}Dk'DQ(/<m9k޻ c'qem8fHC°78$l!J(h(/I|X;![,4 +26! }F ((%>wc˜g Y4M~@z8ځZf (Ɋcm$ըUuT; q3O9=,82d$l^iP`B8ْVzy\uXBX{``eXŜsk8`pG;AMc} 3PXX m.d1v@'ks ;!{B( Rh[ CP }(930hhp}0'pxxXULۇ}@%$5 y/M22!л0cg󐎻EBJ <mhp5.Ip:D mXmP @i05`zP0OTPRE@EU<%fXL/Zf#T?^ iAFb9NF!pI^lKbLdE%Pve<|1 Pn4]YNX>P6HO\N#a60@0fkgV=f@T_ifMfF^>YБp$PpHg&PzB>cN^EH E2g~>RP@P WN'XvW>SFH -PvNS]?'jh]ڡg p0ogm:bA] d Ppqgx[$~0Qa>܀3#P]jO xAjPknn Pf1bHgkXl @ސY،&^6d֦V6֌N/Nj6(Ml> bnCx/&nbMFoo IPMޑ66wp p_ p'7j1WqY^apqqoqrx"O7$ S&7 $w$O(?q),-10.c]mHY/6#4[8@@@1[IeU9gHp>?W:Y.3Jm Bptvo\ŘCx"sS?VwWYZ[\]^d`a'b7cGvI96:wΒΠ]$jqOW+BOv;^jvOv0iZlg/~pmn'1!,MH*\ȰÇ#JHŋ3jȑᨎ CIɓ(S\ɲ˗0cʜIjɳϟ@ Jѣ4&5IFKQ@ԡI>ȌhC::ĥLơO& EiLr*jJ*dԔ-0ŎcQ[!Y$x)T D8k,"rU,3~]} ?u )Sq s܏9 ~^5O?1%i-aR l/%aRP?BS$!6:qU-PZ CW27&l㌬o%"ES®ic|@9Ȯr;`2B$AAx7't H`k<UI5r`#]qSD^,`}Sd3mI0HnUXL 8%@ is; ;dWv$* X{c39˚9 TjG*Tf(Ʉ N_X}}J@K CP_Tΰ@pcNznsZ'HX3Ĵ$b9t@ D!&1b 6ڴvhtZU_\NMcސLVcv8|vw 77nj}aYx7$xrX;'N[>k{ GN(OW/4{]ҙ32^|+JqD|(> /oJ%$8ջ4.yP:r*pCg`uXs;AIHýI(vd> >}wYN|H6WwɖORŝ{'GOқOWֻs#0~5Z7wb :?F'tq1bvhqs^AWQ}sXr1/~OOϿa1WV0 t(!Ҁ 0B'1-H؁ "8$X&x>EW9C>3=-/c?E;(*H.nģ 12h7NGs [XopTɰ T%k!^Ih76wlw_%4׆`4!}rr`t|)+FP8NFHp& z0@l{abP\0CHxa'h0Eւd1xHe8XxؘڸG2KwܨV o qhS!yXx؏9Yy ِ9Y/qTHl" ^(r& j mqq 5MfeJ /X&G erVB ' hB`rِC(o(&Y  `  gKpR cư  @AxI a n%GɗQw)' W&VXa0c &k (R Q)@q N p}B fP |&p? ?Ùxȝ9乁`  %՞)YqS)9LYI0*L` LP? Ji'Jq:Ltr P BwA \90dY >'&*p@'0 @ 9LڤAN5?RV*@W?@\ڥXbn&{LaCgtzJ%aUn: "13I}Z_څ|ѧ ZZqJZYrJ#0Nʥ[JY Fj\JZ  gؐ3&@੦-8 : l0Zk2a*d6dC712 ݢ5P{>0- 4ebLNP4ES 1Xqa޽Q# ( Nej>TeA!A<D0N~uQMQW0臞YK r~Q<$ $HE5.kꨮANEo䄰##ڠ>Ȟʮ_baCbҾ4E<:#l:ގ:!ńh$A  N 8 A4fYtKM tlGnAIo$H >zP?L]p^a 0Y ӎH%9׮ΉgN~<Aմ40 :  N ;>y<,IddI,NӲqChk_Ps~N4=tvNkItnAOċ!tPE .AdK4 fv;b;_e` 0?zV1^mz N`^0O]_ǏȮ1 3נ6ɀ 0&6 Lgb  ϐ ˵L`b<;t:!Nph&n2ml(n:Hy86` i.Z:dצ7olH&6|S^|zx 2 /  @ w 1w+0 DMlGCegd mI Ǡd'GxQuƴ F'oqnl ʦ|g}~gR$BsA-)4y&L3'g36[2n鸦ְ 7āk.ҩjx G ¡&k\ ?YgV[o51È8̠F0\ %Q2XQ JXpO @O1ZPBaVYdEy E~VPi5$Py%JFbfEߘ4ZB)XL $Ġa>$MnV$Pn %ZKdim{tymu52mIVV@&[$x0pzgZMPnQQ%mG jI;0Gyh E̛|Yf 9[."AkD%Q;m>)rm2egC/QPPF9%gPQݠQ@A%vmNER<RHT\yy^kq!A "QHqT)ׇtכ[Q)}`Awct?S.ץ"1яb S`@q : EJ6>ghC}G?n6g8+QB()T;P# IyKELR;Tg| F|D(7 b@TÈ*^q dBxE0z TUլߔbsLVG>0Ca`/B1fL I 5)( ­Q#%bF.cBeuΕʵQ"M gAX}ҸI-76 sFk%J H?PaX{w6>>m}X8>VQhzD!I$ !*(h[be3 0L[ߌp3myړ,4 R5Qc`hJ,K(Go;Ân@X8NHM2vlص,p_aXg9O  q#L~#$ 61l{Š0s~peAYOvh0@W d ch 21e E7Ud"D9(GV!lCFH3lN243 /rF$wsfIIuFFBԎvC(4Xh ЋN4҆a)ƚF6A `E<2*2MPÓT¶5O0Ii?5$OoRqDG:hOL5 xͫ$dTF!.yH\QMP ЬM {Q졌/ (EI7">Yasg`9àFo7Bq*2འ۞%b  6sHSo8)B߸<<// 248\^_`E3?@0PkX khƺғC+G*$G Ɉ0= ؼӼe?B ;?DE? G X !ًEӄ  4PȆHŋCOGċpTy ϶(U X^_] a`\ЅYY> fOЃoȂѺ)BcP$D3 QK!Ќ.q } S3 4mӀؠxa1 ;ey£X= E"T}x2`\>\m3ʹp[ܽ~P 1bZ_{R9ةݹ0w "ӆp H@l#xǏ[6[F E_EP\*?q ͻ xJ6 P=2 VH߉ 鴬yY ^,_Kxb_@ZP@p)q( 8ar{~<X5B]߬}_\kYF 0%b}xȇv4 ȹV44@ź L4l9٬ͦ}D@d๫H< MpÊM<8^@(Xhd e!Q&bf6jzk=Bx:Lxxhm0Z M3.~ XS?60DыAx^<) #]i hm5ȇx vl~lI>&{ⷈ'lh&ߦ<ٶu (m+C1v oR&a! ØƐG$20cpy xc~_0 (B4u3~f[$m TDhp`Z4#GrZ湈Z$O%Gs:(}V_\\$ yP5@vZBXh~R͆qؽ?Z exNbpt@ '&D_Z60oeEޓ=qC߇r*ah O]v`VwW 'ٞ )wP٨,rV/4Nv;/V5fF0ðж;XiHd0C7iHKgqx pІ|ɳv _mPoC UjK-1PtEt=;Uuj v%v ~JiL&e[h}yb] rSH5ם[ mOWj0^wo3(llbOhx{7f΢X y<Gongw`1Z[uj~||[soX_ F_qW\7Ηd`1jgvo2DpU¦sوPP>z߶(R&ބhW I;We Ƃ%mQ\Q!L,h „ 2l!Ĉ'Rh"ƌlB^4,Ir8PL跏%̘2g2LN'?>iD`Lqkipӧo޼pOu+Ѱbǒ-k,ڴjahk-܇(Cd2.޼)p2x ڐ,`L` D~7s3 {$,e,,yQ2D؉),ΫOײ-(Qm~v'4Q ]9C * 2LJIea-;5C3x")2 Fl/qڄf*? OnA[5T`<:*$MF.pjL49?=zcn[y,M%if! ¦AU[>CDT2tCV O8#52y!Ԟ|J:)hR)ZbP8h#8#*`&>$IM@9QSz+] GW٩PBE;c@$YMJ;-BhQ28YC[;.L{.6g;B5P\ 6NA>Z/ LmC P-sEzͷ3kE< 4EIGWqb(s2-?T.jz=/ɳA7HZdÑ9'++4QK=uLS !P) .xT}6i4@)S1IJ,E7zy7sC@nra⊛>9啿'D! 001[>:饛+K 8ÞzZ>Z@QC)V ; n Dy֢OO04rpc?;< g \ |RW] cLy C :F&Ԃ_$ 9+R3K9rk$ lɇ (K91L:" 'OzFl*'0 RLlLpBF?RyFD ҝjtdȢd;&bșƹPGO2EPK l\(MYQ~&[6Z DXJEb )PJ"PT dc~ԩR!#V^5HSf-@sՐQ<+\Ԭ(vu:񴓑dOXj}< AX㙵+d׽l,^er,V S<_T\JB]*_KGd4+a~c$ 8p3n6(g?TZѬ%+W_ fꨭnk\eOIGmae* \ @8䑯mCT9E.c4jl"}% >ѣt\3"MCw;٤nx:i~(ֳүe9:6҇jQsiu 8( )KPl}tE OiA|m\I MjzWUI.<||HF?KkPۈG<l%MG2>i /?@5~W HB|' NCF2k#5`kLShyfG?A1hB ^&P0(JA5aܽB+K@p("G<w" H7, (`@,˅U ]zkѠޡH,*AeU$:fCjkd D 0";N  G"Ppb 7 , oD*'6@A ?J'~0a9 -UطF}x?>F(: t<` &ðC)H&8A S\*؜s|0C(xp3`@)E2(C4<(C'@*Dx&"P?`}@;>*C8H@C<)C-̃>H:x/bC:Dh/1t@!|T(?,DpCT*P 0BBT@  D3HA0cl쬐ɣNu?:% +"8Tm P,,|@)XCS&<4 8TA |C3+(Aӕ7=!Mb (-0l-H,-+XB(e x%( (0 1<:%C7$C3DA* `?x+L,ܖL+$qF JMA2T5X7Ђ@D@y!APF40,DC,@WH 4lCX$AHBjRTv&7m=<0AtE6Y1dm VEmhnFPWCt@ o݆HAtBg68Ej@^mO` hғZjɰFtذ<@it88S@|˱ +ƫkI++櫾+X|Ɛd Y & ͍6, :bЃD^,fƂ~Ȏl (ɞ,ʮdU,H֬،,BwWah)e!ޔ͏,ԪD^%6-f+^("! BԖ-^"!DVmIWx-Q <ݮŅ`Д#4#ܭF$Yx+o@| P}jp-tgj67[a,w? 0 >O3N)?3tP`4c#ΏH>WGNl< .GNoX&$? ʰ*4z#,alя >*D405QC PMֈAơ%f0ul$(~DsJX &80RDpGLJ1";E1>30ys`Q-tEH¨TO U0X$7)1C!?-%eB!(b&7EK(rIrxaGAK5Pfx(xA %( ƅ$f1a*H2tLΔf42Mqs$&BLnn &(™M; y$Dg q۝߲PKk4B,E҈3\hǢz~5LW?~sF21==|n|Ka(9IV!eD'[k`-̮$#8g m Y>|%5A 0ynK6&zX` T!}_IZSl?:/y]aޡq:("F`L 4//)zy}x`_F z^EbJۓvLB`1+];߱#u}M'CC*2 |O&,ySG><QB I;܁hG]O{ثL+'7%/Z#D;gtHDυlX8Wqxa0_4s@^qkFA i"ޗQ>(rmRo[!e&sư QO X 0&T/(I~  6!s`}(ʐ!tFyw$AvC>FAp }yQrHl.Ӈ()U3Nj9HȘʸ،8Gb dc{]R8 1i1hy  $ÆX xnш P Ґ |} W & 02Ht 3&[24# T-Y`q".P4Y6y|jt h ]Ó>hcP0(F `/xP8b9dyhbjٖnpr9tI(lYxz|ٗ~9Π oPT"Ԡ 0Vx$q.^sǙA2) '4Z9uٚ9Yyٛ9YyjpX)" Xi" y"ҁ`f@`k3ߙ!&ٞ2oa1Gٟ:ZCpY8X;Y  kBƠAB: 5iu UE"Dų Pt3ᙹ2xgJVp?:Ej6kNs!5:-S=*o"J᠅\!1ik2G@аgp `4P б W&0 P '[ WőPjP  jR/0'HpQ0 '&CY0  pV aSi8`   Az yiX9hYy #"'  ` k Y'PS Z50 "ΐ? `с{2M!' (`' < ˽AfK۾ ;+[{KK|,|̛a Z"L`(\*|+,.-<0L2 a=,?><@LB\H|JLlMON{>A׾ 0n0ѐ=l"|K;i>@t] ˀ *[\0QnIm.0fp %e +Zc @ѵ7 j)TĠ+ b p [  AF 2s4@[b"KU[ ﰝapU@ K P @ >n 1G:`Q ="E sah 0Q!{JoM(Lӹ pJ }p1b{0@p4@z(žku>?NU㱁tzP f=Gs~! ف#N{P dg aH[ a1 +A P d= Gŀ|)1  `߱] |+q׿ >ֺ fp( ! 7pỺ ʿͼP PԅR=1bO"wqʯq} !y\ S+[=*] DPB >QāDa1JEQH%MDRJ-]S̘l޴ISN=}4 1HZ>UTU^IKVX~V,K"EtqXmݾWC8śW/E1bǽ Fxƍ+Y2H-UrfΝ=VX&hҥM7@ͧ][lڵOkLkٲ\pō E\tխ_/~!^x͇ŝ{ݿ_Cyt~,D0Ad;kfiB /0Ö,jMCG$ !#sD_1F4ke1Gwt͢<܍#@H#D2(d*olotl1ȀG.G>y4<~ EGv s:ӻy*z@Qb֤:*"_C Yc@@l$jj6t lT~ 4`qt8¡ g#8q c3!CmC6 ql8QmCa;pMtG!(Fһ@-"/0.ьb4cLj@aڟ.q8Ɓ5~$(2L@+) 2uUfPu#+4~.&>w8r#Z'OеXg]mR19~Zt?5B(Y^3Lq7g3VgBrưqSYf2]e2o4' T~b`?щZ6-qQP#Y@q@~p0ȇxXʱy9C l;ʃ`;db@AA`1ʡs;[;nq<9Q"t\{c@a,{99 s~p02h~Q~U8 AB+T!\1FPBaT kȃ&`Ƀ˓əBț?"ɝ<6d}ыR1daJD,aTjJE\CʥLbq+#01Q41(/2K".-0FD$ IKD i;!4a! C>{n>|ջ@~~Kiyb`t|#rAt7*͈2gqƬMMż>}TĬJ4̟~xp`!Ѥp$UixBr9C8 &`P81`R`dH<`@L!C]!O<H C%p ~3v)z -osк0J 17/K1*#3x k!eDyH:JrݟLӃ^C8ўʹd(#M>2!EӆmPxFMWSl8KӕYWG{TSǛ;bqM !BsqrHYK9D pp?HuD`:0 S0LT8TmkPQ~IH><ɤNOI: H~G0\Rx #*IK;8PWX P-F`H ,\aIU1dL\!J2,EzDŞok"5_K& u0K2"%}u">lMa?:?;_`0g4lp+5ƒB4,jjc' Hȅ0MGI[GY_Xpd5Suؼ}^^#|Yq YR2R-CIH*\]݁(+FR1I8q#F X3--07ȃO(^"LCePY[` ֓ _p0ˁhGK~fΓ&㕼㽚4e 5 qR>Ne dP!T W-y۩Z$TމԤ5"Jb EՔFn*΃(`zzH $;;TdkPh`U0X^_ 1zch裮>jzDncvˣ@dHp h0:-M0D*AxZ؅^ /y= HhsAi&pM'(@U ,7ڢCp;7ŋx0i8 $m?!R0 %R!~ 2GW KPʄ8@6iFn0Zb."7z_0H`5hIHxYlᆠV #؄,GIGxxQ,(XwpcX@Ђi ucdINfX4lSe A`0G/>Wq$ 0m8F:J#\JSbpXgH! _!'ppԞ qJe J?"-NJx% qH1~ܔSg6Zb.:~S3Gs0Z85@E9Ut8_Dg#;I01xD)=ӈrztG7FGuFWnJgV Pp]shU ֝&ԷGROSRtWGdVHcx_ЅZO 7>YxpnuGZ0܇܃́Ȇg0eI !/Cp+l^@bB7RM6xxx ExAx[jG`4ǁ #kv qyss0Iw#Gw w7wyH0+7b9giHz(USexG A;wЮVV$[FK0myPx2{`F =)٭ۡsmxX}4{aqxR4tt>q`DPdURICPlc1ɠDJ3 OׁgxvӞЇt"hh7{}.!|gx?|8y#ĝjYȏʏG%_+}C; !HSoJm „ .,!Ĉ'Rh"ƌ7rQbWb$ʔOe%:w^F6.6lҥo`*_?bǒ-k,ڴj(+@ɵr;"m:x΀X;cs.PSk&5S5=Kc`3s3KˤhǍ*0](P`>{Ŗۡs(Ρl%h\Q/mk҆4/o38PX`#Y8-I$iJ?CA-DACK-Lx#(]X'f?aMNg~NvHS>$8(zjE@9qEPÂզaAFD6iM>䓏uCM:u&P ?|mu qEScF+[O8Iڤ[9t(s)<Fs+(W޸q@\X!7Ï, FI.8D8! گ&CkĎ|$$Yk"- Ĺ <BɼOD?VAx&Kp5q7pRRD)[rDq+{jad+ AAQP!,˺]R !C$1"5ɛS't8Ҩln"H2QoP0{F0+M#(I|?Z[H-1x|iPt6l–1md#xLGះd3Xi5f C8㘅bk_F zH"F>[ iL}(GVf@lA+wq\?)V׿>Η<g8L•HF _Vlɖ(zSTXj`XZ z^? @tZ|́fFCB 8?C8< :$F:48H< 8y\i`ah CH!V8EX!BN6윩ayC .boK8?$a8 DCޡ? <%6FI`!-nGXɡd(Z2E<Π ĖCs|C#Ġ*]e<|5C@8>Cl0@,?;5Bݟ6r#ED2, l @ X;8AB@B>d0>2AA8~,$tRfC<5CD5̃2 6L'B> C+TBPV`g  XeE\xEC?56C+8@-HXCHQ\0lC3p7! B 8&<8HC #dA0-@@)C>:,&t,|:H$> <'A,2HAD@\4<pB D%Uۖb X )x@hC&c1< *,8x'4Gڰ7p'tf/Āep*$2"Ȃ, 2\jD8 2ȃx@txR!C3<D+8@x$@l;8B%KCmL4F8tZrr5@JkVOl<SN,D5|6A ݂bY# +؈::`5ڈ5Xæ0XiWf? ACL Ŧ? !kÒ56Ȃ>ăeT]R˱̞4jv% CIr tG:ƒY4W=Qoq MARQؔﶉ%}Ӑ2;*֭nĒ8Ή.`\?c%&8xM뭌ِ> 8&Jq`PxJ }*7RmyJB I1q|ZABڰ|Z)aQzgְYO(ʵ2B0Fճ'7& 9PRZ[QauJiV` b!9^^%2 y1of90#0AleEFKĵ+]fHUmJtk[!+G:g<.h%6G1"٧O=$P&MpnKkCwB:Qf-n0kS$}}z/T|5"6&qM~(84Gˎx$}wb0oH C xqKX&c"x( #km `8Oz Dsp]ԩnKZ?;?vG} lBnQ#ظ[; 8ɈڸsG\fS lC2:ekU5,S_lsew>A%c?KO{1 } B1 ec E_' i 8 X#+ y8c|RZ2D2!xς~ :BEEXDd0SDhb:b| x0TԆw6C!̣Ͳ #CX4!QT!&URtVH$Ea#w`5GXI 6(EzD>8P{La" B* ?c=x  -}* ~>$8 Pr,QhӔ4XBaRBBpFh-GE , !ƈ ʢm)Ax3;ш 4 RlE`X!L8fA5P e*HaQJ/[MY02703,maã~X|y~ 2(EKB=D]F}HJLNPR=T]V}XZ\]"P Aՙr Pf=)DEf 0 /iˠ~ z )7=}ا;+DN ٬r#ک2y{۲ړ\ۨB۸m*  ۻ} B?K~]ܣBd{;@Ih8yoCYX ֐ݵ٠( ?aQDm!8% dA!u  $A3:s=ZjA a v %m0F6.3 WOCI>)&>0IC P69n ! )PP;P$ 679VC>Aq0 W] א @ I@)k%T ;a9\`p cTp a qA_b Ë 0ݼs}BDV1% AGߑP 0}p&P ` ?=V2 )CI L s  p 1pU678 Ƞ 09P>T>v3Cų `%$)Z0 Ӏ b ;1 ֟^Q @خp$m' a  ,'Ă ,`(R&Ljb pp 6XI-]SL5męSN=}t ZiC% -ƁƆedBHPj4AEVZ5 okM*VUiCRw N pH-au[ PAYצQVhҥMFZuf LVpo=0)eO"3Ndrjl/_m '߬A_]v KUb=8`63譂lpE/ȸ*e.(:--z)(0C 7CeOXf*+DC :'b 8bd&O R FDR5gX FkHyL\nc"3.I  =a" ``eQp.&!@ dFUnOG=)igU"\j"bV*ݥe]xci&efbifZ8sշ m,}gЇ >X/Njm`fq'.%QA;Z .8 Ř6az4A 85 2x(! .pqX/ 0 [†pEv [ 9Eu^x+l vD^2 .ta\tЊj`&H8AHZA ]< XE4Q.x4:0H2xq XTx}"Ƃ‘+VҒ[l%*'gL2("l5ʳƓKx XRhe.u9җe0`0Lf6ә/QLf6MnvӛP0MrӜJ*ԹNuӝlY!Ug>O~Ğh@Z0P8@P6ԡ.VԢhF5QvԣiHE:R^*%EiJUo"xI/ RT,iNuӤa7-)bRըGE _AYFAϜ@5WjV 21(&bbdjZպ֤Bfh(*Aԉ&j P5l() XfGkAj+p5Hb!;BtMlf%$> RE$A HfZնU3C0 yI(t o(H^Q ȺVv TYk\檔 U(EPXd dL@ 8/ubX6W0ozYJC RCr*b DfwoEʆ"r#lkWh`p%DJL@ Wak8br29 lP$ QP\b}6v<CЇ"P &x6b8& Wr9kWz8_#H12mp VE-peW"3g>ku-1Q`B%™,QR)Ϗtg`OLҗtEPlǔ૦E=jR14)Aկ5OZXַp|梚vykaؙn+jq,~4 ;hW%Fյm=nrkv(.~rY6 Bw+BZptxKmcYGFot+0C" x5!w7>riA;J/w&peyq ټ@O#LGG:.'N';_Gt0p5!w='ϛo )׽vء -ҁ/c{cdzA]Ӊ(ԘG(9c%!`ۉ'ſZh pǛ6\g"8kX6.Jc])#KVC[dl }@<($~?~0wf&uJ x TL~e6'Xsܩ !1V"B>mh `Đ1@ K@n$({) k*<$ù*;dQTn ܳ+3'`Rk'L!8485RyȅZjRp!"3*"?4t7t \@8:l30;C ?‰98"`R6@TD 8+3,\J3)\:"X6K6P4@L4okStED!\8~J|[9<<4I,[`9LTC> Oe|Gd(EfF+Ɗ;Ƃ`jFÙ9;Zs|UCttG{|Dss{{ b@HV$7]\GNE`ȃ46Ath oSFHh}<9{$'ߋȌ즊ŅT@_FnJȒ&Yt*Ἀi%qPˆ؇k~`e݇E:LzDD/J϶KK4IX(( Mm\KIQpϭ{KҐQϗ#̭cdL 'P=  pPPP[HlH#UO-IQ t(qbg_ِNP5ݺBdEы0ӆ |ԛHE]JCE- 50TpUxF T@Üʫ, l lP ͆lZņ^^mhRwhv`vp#9)eqhRG JH L3ON=PuMdPWpLZT ȺWzBt PغPt u]pWM3ÿ́OdaG`-@K"; Wsؑ$WsXPN3Y+QP%<.ckpYMXTUW1XQhwyՄ٘YQPQ3K;K(Z.MtOڂکɩڮڂZMګɫumٸ};ZUZY[[IQX4Qu]E]W]T]Wp۷ۻLXZW̭[m\s\q+ ] ܼ}eYҍ\yOxŝ\ MP`P{eZޕ]3](ՕYx.`+J iPlPNC,WB-iR^Q@SWJSx_M@0/12/4@0q{u׀ YHL -׷+ ^QHk;;PW-|}MaXqX]WQ`aWDd؏AP( ޏ_ 6aWٕm.⛝^xY/`!n\jOغQXQW7r9L!5X*Ws*.WC~c=d0u{%;{L3]QgXUipN\PeZQXg3^O^_-]W[\\iݺ^lr%fņde[6Nm\M! \U^^]Um\=fj{-uXtkfy6[iHQKz|Pցhs`|NhhXhs0uhh{6肌6`2.6n6h1肑&N1+ETTgPP@3gTxhȠXSHa@W~53XLT v Dm7;k%F@`3aӒXUP͆0ERq0KO!Ԃ0OIk/jkK"Ie|k8Tm@lI ca>l$l&Ǟ Nk%kV%RS-ՙPUJn./X֞1i`~e5@de0vPndiI}臙15H}I|*׿]d=jȃ^oԀIoI`6qІ"PK=PPwp)eq 7Չ2̺UmUD[(%%'qd0p$UT.m@yq/!ndUV nfV#2&`f`0x1`)/)<}yy00ׇyHP/R//,_/ gy`IQr) m7 H$Ը|qnlńH #|K`@y7P<j0VtotJOuJIJ^XLVVϑ,,mHg"Ǟ QddWi'vhm0PewfdoEkDqlIwdvwvidĆsO 7u7Ug@#Oxi /r(.&y0 denFnwhPfyVe-n^wxxҢ7W_yL닷B_3͓8JN~Fx YH6e_ j8ȳ8%Ml-X3!v Ym#5Ə>9$DTEdt4_m֎RPBٔRNeL5le %6*Y62uLGYMJSAdI՝}'YWxєMfRʸTM4@?މ)z)JƔ2v&P䍳 6¨ML<8a|s8#J(+|b5QXo!JJzfV,.Q3)]{/vgר\oݙ@k~i=zE)M><I2(aV0tB0 6XtKB3ș&8ASo~S+!R ͐90G4p@F͙?$A q r&ؐMș_Aa#M +>)P957oL2*~1#T&uCF/LqF(G10Ab1p,PdvmK,8if?h#hB"4gئ]ZhbSXdpU3LO`tBG`^ub,C:OR\c*ar3d׳M+_4A<5n#b=L0byjTU'LSSNdғ5].6Ib&)Kgfƞֱ>c?Y"5a$]0NU-R 23+2$k",?0 0qu? ICY3m7Ό8+32Fu{jUX:ud. g:bY3%5Q9٩OTaʒ6wmٌ8Ǜ!-_,Iӯ)iN g@jm߬ߨV)"2R3\&1i;~8"(Yg` `ɜ ?F|(Ơ:rQ[t֦v щ ^*2Ңt؆ ?ϰL5l5XZC0S }6 #!R !`2!*-RASKJ(`y!"ޞ`F.4)V } XlHF<<)"L8+",΢,"-bʼna"6F4}dƈ_1>\a2d΁1N#5]ZhO!%zK3c'(C)L{+#xYgH#j_#f`ZTc/]2RJJ5G:Їd&8Kh;`4EҤP ȏ045xGA,h?Cq?@RȀB%?$$,FI%IPFJjJG&EΤH[桐I -dTe^&  iԔ`l:|Hq?h&,3<妤݇KƌRDAt֞H-iQxg vhX&ɣf]>XC<ۀBZC1xGqf,'V>W88:eVh&*0%IN)JďRCu(hLLRI`8H@C *Ɔ%c%\ Ѡn)# Rf@ &*y fm,([}r*j1/)@6iD.F. J2ʬ6 ᚆƺҘLJ.[ 2VA/όϯf֮..${H6j$?b /&olmh&}W6*h,@RLpv~$RЂ>4~б̭x(ǚLW2 $z/#zoBHEƐ/ fo[8/O0/m PQ:8,S;M n ./g,\X 89֩l1 0RJ-| b\z07qo14kRAe(0.LRXC$R"]l z(ױo~ 8¤ehp3Ev{o1$+,\ߢ1fG XcAi('{r$2+K$1nidҩ쥇Z+/S@YgVӝ//33-`is265_36g jR4f393$f$exW#3<3h3ּj胗M;˳?3f@0FNY&j?O#%`d?+ոmn,CWi~&#@*(EDgRPE+fQj@fԊH$0<BI f-0 k8in#fNWk LIĦFѾ-UVCOc3ClBS"g?5׽%}[,4" V+n1jX 6 LQOO_0$}r>*L''[ȃD5jK $uRh5kAR8Œ_76kvB(E6CPЁhs). f̀z%tn^Jr4(cfJб0`C:L^7ZY+E.VR B1BQ+Ç*UB>BA% {Wk&g8-k*r+A^=vx`n@pB8BOm8{JxRhH8]$@%(E1,⸒/9ږqi<><JSk39W"wƇ),C9-Y/X8 BX@9ysϷx`,냏yĄ/:,E7:&8OO~S:P̓j:OA?#ka_i @%8CfC#úFD/@h8z$e&?poyÈ;vEsA^gkJ3O~gbWSnoFow緀N#~>-%C~>bo3>ψM~M7DӻU9M0a)z鞾N>Ma{C j(~>fy[Ed>a|hMk@o_7Np!F8bE_AbGA9dI'=1Hd".](aƔ9fM7qԹgO?*1qK o0}ֶ,u ۿ 1W84`۶*k 6m-Ì;נZ'Wysݮڷ,8Y$3B Xa3x}:pY>'H8/ Ā-5$h`*q˷B: .P 9AIgǓ*"$yQ3oPYbkb X.H ^%&$YƔѧ0R b̉x2 8 Yj2TS9;%y%0n":$lLqńZ1Ly1OCiR.0V,(%#o<1~Y%mz1(PwpwV W<0kbmggt :̚5E'O"l%Vă_Q7y'[d Lf` xG>nHR dc=uh9+.0&ZQNYe$'tXZJhk1Y`dBVf^ CFOXXM@05 85 0` xWNAZerd\53m&CS&LG`QmĆi6{D 0jALؤ%R9PRR%Pbdy5v^q,s z`hLY}Cx':q80778! E), =|@8ࠀT@@#x# Vx=z aC'(yR@&zRz:R a,:(0V6W$LPP `@Ea '$Ge!H;hH$4!IC%DD)[? `D0p0:3 xD Ő"Z@,PU\# ( (k$8AVяp5@4~H"%5MDđE?` lHcD(Foģ!mqdR3Hq ?i!G8d}2(Q6T%}IQR91 58ʔ #D <͆fh$[k4<% !H?ԉxDrxR$ u(RT=02e5YWPkEa)7]a4_ɜaAĢĆ(g>عnu!c!Y慭Hkrm̮gk>{d[hVuwe;YMomq[JHz3n\^*mf$" A&#ӌ!-Buv*r"J*1 iR$q)++)e -#)t.r/d 2///!,S `QN\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0c(JHP ɳϟ@ JѣH*]1ΎJJիXjʵW<-I(@ԯhӪ]˶۷p^4HDr˷߿ KV_ ^̸ǐ#KJxRIE5M̹ϠCǵL28E^ͺװ=M۸s랈 Nȓ+_μdrNY[xËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0KN4o4s8[ts 0gj{LK3_ 9|npS2)>Loޙ$a}{A?n6dsy*`:SԇQ:)0[JXHE*"  <2b/GG8CoˀDlh} CQx: ߐ&>=ȋ[ma|蕑GK+GhcިLjcLL+|qGE:t$%BJrRPPsTP 0ģjaJt&%SAgtllc!6Qg"e:K+`  InՏ$_5IXT$r'gIZe֊ !Q6 wjFCΝIN,EKlH:khKQW5[.D#lh'3,8~(Fml1EgxJ[O(Є&fP|lrBw.sQ\DDݮA]4N @(+VTBҸO-n[^ﮗ m 0"'P8Bqf_^̠KX"Ȇ'A"J^b1q1\$ڭnw􍃐q{JFȍsF)z&OQԠ(5\(E`eRK.Ȕ\0cr'9cvsɘem.5ybF5l dEeyu8 Oz?y!y2&lԢܦkO?$ԣ4|ꂌ:4kRԩN5@G!uk-\;$8B7!N~Gd9d@h/qDX2CFx0 C"b^P(05XWr?`Zڠ0aPRW\=(B@$XxdžRх$cygR) u"u؆ Q q"bm,. G!Ep\$,BH 5}B!z#zb70F= BP ,˨8y t2 H`(=x!$ Ǎ &u!o(!4c Dr9yPAư| L' A`! ɐ2 S~0@ .g7kd>ZX p/w =6i*4'ި*-VK@Bt" AD0Plyx.} (F@Ldw G< i/M(,w.`dI}X,@`@I x-9!1҅ވ촗'aR)@:88™&5ZMq"I#Xӂ%a!?9-YP`Ex9"n M B`U + p!n*3󉿱83Ҝ?de 9"!!ؐi$ pJ8"pj,9 Ac ,(-p[9òJ!ې #,)@='N ۠:Ѐ+JJњ a}82`*/S:YQYHFj-bZ_-ejTɧL oⲧڇucw2Z0 j &E.hCiP7vBk<8#`9碨PwBrdmO rQeuc ?{sФʠfcx]8aKЦa _x. 5:Az. .#`9p4L/b}+, s يN0\S ] ].NW 9`#Aܷ@ N^ €^ I|s"R]@1f* =Y.l) 7 ~N 2S-1 P.Par a T|7 n5 P L"Ѓ Ȯa !q!Qo-P&0 ^ ޾ ڎ _5E r4P!, q 0j(~ Q 1P +@e+/_rxPYRv )ay6ⲁ:FDRJ-]SL5mXl|H m@ -U|l`-Y|T lR}<.h'l1N F404}X`… FXkRf /5j~k0h+6kv.[D;u.NO .i -С>Kw^nur͝(Jxreg[7%xͺs @KV_}pW/iݎ o x-^ 69 'B /0C 7(ho $9D1EWfb%|EFo1GwG2H!ƾN$2I%dI'2J)TX2K-(_y%0L3 SL5dM7߄3N9[oN;3O=OnkH3Ȍ7 =4CeTDuQH'}RI-4SJ/TSL5R?G%TSOݓ !P5VzVcuV[aUV\wյZy\%0BPeYg6ZiZk6[m[o7\q%\sE7]u5Md7^y祷^ S ]{_IUHqT8afQ""TVBa7cf+qh,VHA Q8T\fg>`O9g] ŕfB̞*PdTV^ygWeZB%#(:$$ɚfNmS6*0A@~ߥ:G|&*.{H`#M"=((TyLMޯ&tWw) ˞""WwIQ>;XpWC}xUD胉7*i<$Lq W܉o~ 5uAA}}WŒ0C@U U>V&(ꛟ>ɽ/l[< X l be+AЅҪECPa53Cܥ-n HpMGPI/ɄJf9O,D.vыݶgDcGpjc'JX%Mc.1#9HBJIDd"HF6ґd$%9IJVҒd&5INvғe(E9JRҔDe*UJVҕe,e9KZҖe.uK^җf09LbS< mә7џ)%yCx4^%x6y 8չ<Ri3MSs$[I:WD%@U70tD ]@"贞( (`%FQv "6`(idE{FMOWeRcT1(!p*[Zymse*̀FZ"2T2dAVSM vċtk ^ 1};\׽o|;_׾o~_׿p<`Fp`7X`+h`V ?4pY) , IG:DSXƒX|&ڻ^&k _A!|pd"8峿;^< PXB>yg1ϼ~"ӄs:9DÜ£=sy`ۇ~P@ӝl@<ջ=&?Q㿋=Qp A+AQ BB+B!\Aq)lAA8pD#IRPQ0y dyb<#D@CP7C{ÕYB!l#(<,=lۓͣA#6C8&CB|BB DCD|E&DC9$<:kj KEg  Q"#[R('RHPhaFXRLYl=\E^AQDFpE;tqFt>MOElR$Td W]G@ G!FY8z@qOXG\ rFj^|OtpGLAZeFm'@pL8P`A0((@s4)}O M7-Rs{ҌKA7=7'5T?R(6sKG6D[TtF}tUNFDcRx{́UP~KUE MܼRQMU3յK4¨SsW7MTk)T5؆q@|T[mh%3~~<kV~ }~ 3eUʈ }ei x A=UzjTFEG3Y54̓5$O5f _BXP9@q -m+uֈ8X$WVWUF=YuOr+՗`X ؕԖ 6? XٙHYsY55qІdmqP~k ZگVZM]Eم%xZPwhv[wv۸5imxg|Ժ%XƍVG6K؝-Ӳ^_(b@C\T5YǝfTܿIT٭R>]\YOA]]5]ӭ=]=]EU^\P}^W=E-^^YG_Qy q ۰__k_o5c;6mV=yk,//./6h0piPm@-uZ[m\GT]Vi^N (xZp @]٣}\Ǖ~]a_$N[}] 7&^'F j}-q0$naaS-^G[cd#7_:Z==fc> _!cyv `F GndFIDn1 1MOPeOe2Ȁ-}`} `Hxepap}'巳e(ԝP td(b׳-۾Y]mY(8%Tml_=ڶY@e`9>9 v&wVgP=Zm`f-O.6h.HdDP vF lyppe}mqqUY|_YӭnNj8IlY8``/fKE꣎.v r6ceņt#XV -''؆wXk5WPhu뺕뻶w01 v. &6v`O4k|xxls2ɾ˞,.ҭgM~F0g}%^bes(]Kفh&jp~0dxl"gHxdN-j׬hx4VIj @헰iijdH`lPlolP]݇#=S^mVm0 e0䉎۫WdgX䁀dDeЇ|`uwq4hN3_xxϦ>W^jv\njjm.\M m Np&[txpgixyV8s7/(6Hm@65i9sV@Almxw0(lqXlJPD^?g6Fd2hNvpgeh\O :I r:+k[e`HV0.'6qƘv.XZpPvAR~5gNč|?*?p-i6'm&M?sf&e6wҞAWgxA/qnP.k0`~t0'4/y4Xy_uȋQkcuЪk] yIvbP^'[Ø_?@e]4f.hb 'aa=~$߁6ٓnE{o{x}dGkd[ckF u[{U~`pÏ3/R:W^ֹjͿj:17~oxop<,}mqHzx]7ͭ /'pa@@nf%a[=Wtf{f6P g`06`M1hwNNlu{vXuv $(0„) B~Qh"ƌ7r<~ mܼK$?fҙœ:s8:n(ҤJ2EJq:N}jԝD-]:(,ڴjײm-ܸrҭk.޼b7_K_DNiPF31S`;Y2g[W[C vU`:;_ 0m۸k7ċ7.8gKOIqRШ6\,^5uacKIA Na?,gBڈN8)`I?T}QODO DFxQV8x#9#=ggTz?%dT6I؆Ic}u]]Yُw i8#L(FN&_t;;ڞgk)(BM3IDESiijCbN Piψ .6jZXSN:9dBc*hDT~HUa:"D""d .hP9BQɣ΄TxaV5"BXM8EZ8QPCs^?]9/{/*B8Q76dѶ<ũt< 3J9m#6~xlol0>5b3R6Ŝ-;Q81r)V[,U+愌4\ F5NQ3& j@y<=W;XS;S4,7}P49ոJTELmipDu?`=j~Zk{zeŞaiZ&4r9y:#gbrѺ|9Mv֖"Jus[%:k[PܧE[W6$B '\{>#ȇn`[xm-RP.!p'D8dhQ\n&x&<p ЂkP}X> 006D<x/)!(r!%-=̃riQGIb ˨5Q.%NF47oc^\J- }J!I% L%!B&QO4M:U)KRIYh4&za%2LzHEBFgL!1 qiL!*x\CzyI2U$g'#0+aIKh#*'@*о@x裡CH>ш@T9љYg;sziGIzΰcڀIsӝ/'^q9#xB/r%Ar*Tqa}N, K*.6"[SU,3 E 5~Q"Q*Q.uB&wqP#=T⯼ObY-=e̦a (h u`;s2%ŶN3oKXZg6FC>&쏔Ƿ.v,F1asЍz}K0~bt|[F".0!K^p(1~wCOg1$V>1/;@-P;sE:J_(0z_] LjƆuŒ-37!@5h+|q ]E!{Ѣ[GE(ʂKy^ZmJ\Im'丨fw !zt<)D1焺}CsbSmڌ JzA)QC4K^aUʚ{amZDu d pƑ~# x/=Ags.q&;n8UC%B4!i|LO`u.a1J3 "|Q5wP~ҘV, s.HH7ka h& )H!JjyuǕُ|yD,R*BP;Ǽ>_nbӂxN? Q -.O S YVN``T_! |nmG:1bn;O]REG[*d5;/xI AreΤuh/o듎kVanc;CM?Nj0lc8$ދ. @nW{F:^EY ( >`_o  Eghv:/H`lQ'\a}  +BM_ ` mmZ1 ! 2EH]6RD d6 }_X) bZ*X&.[ L[GlE:A)b*)n,΢whl(X)()E-"0*ͅX0TV `-ŧdN8\H3f6f! @t"l#:#րxc&!\:#>a0<60]>$B;j]^C6L Y^xI7ApdB`@Z1E$$/,@"fA MCFI\wY)@1X MqC70G_R[$[|C2$D2ٔ~xӡM))\52)E%Dj\t@-Lj\\͠B*š£EB4hF=M*K^p訮Zl*B$IL?IxFB å֪*Dd\(߰)J䥳4p3ll[fkZ@;.?t-Cj_B"ԅ+E1giEAP3`)lZ r֫?H?LC?88%(̧N—Y~@*,e),^L0(:@I?7*0GqR%,,oV%>R6;L2X5OB4/3a3Ʃ,rf`NTf> 0T?|C|0l2+3B؀̀6+@8XL4  YT0tB !DCF@%\@*nT< :lDA0C*C> L*7؀p5B AD4XHPC2DC'tBl0D70l40H @3 0A-5 A 0>L;0n]$$0<4LĀ,$7pb 3s)+nan3@ U$;00A,8 #?4,*tB*$4,$C8XC3HC!|H'|7*@L5<3$CboAӪ# )16w`hZ`B`u+!7I$}Sx@x vyTt8 h3:HBuA6p>07D?t.BBy*Z@D@)0T@@,$@Bz`6|6D ԁ6Gɏ$*:-&%1XC,PCbO,`?\cEzB@pC0(ͮB3B72C,89@"4h5pM+27X/(A ;$A69 @5l9BLC@)D+&\&X@-, $$_;B>t,@8Z3:8C<6w40 0!d/m>0u+@!E#@'C] X$TD,C6`C02}iB++BQD?L84`2T4&sl~CX8CBc8 57˃?LɃ5T sC1PǃI9)x/>z''#CbD^Wd0X!a!Yql*_B3GC PC8\ҭCGcC[ @7 6t/bEҥc(.‡i(pC`? qԹgO?:hQGhW+@.OF:jUWfպkW_R]@thI+zdbEO+R@xmpaÇ0VX.]!Gx񓷍'ON#cʡG>zuZ?U˧K}{}^~}׿ P ,LPl!P ) 1P 9A QI,QLQYlaQiqQy R!,#LR%l'r0bH!DR-p 4Jʹ,3s#fk*̮(lfq*J& aȠЇ!uԙ/4)R $J"fa~I&H|HB'mWO&pq *5~8&ѸtcM+abي OdaX@ )HhNDwMւPiQ:"ȆE]eh|r `^ahRv`>e*z'ޏAF3T"R[ש)""k4zI2HX't`ț$ Yꩉ @$9*R%Z,R!eI"b H@&Nzh| yF3$ahD C/!|z(4e藈bX`^i `oڇ͜HbܱȚ*@O**V%/IܢiZ7'pXc@'`I&#ga"HN-h1DQ*2ZE>5tn b-`SprXd^8)C" 1"RMDX 9W\.Cb1 3 q&c,9i` %_Đ. xf (+E7b]!Љ=|+[p7h8 ?J@rB"G IM*ȎE e0:Y_aCQ٠62l(j LR!^̸ǐ#Dk`P$k̹ϠCMӨ~ WKc˞Mm@ N\j_μ3)NسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$O(gtr,Sr0?r4+4s8ߌ3:w$0?p:#c""# uROrVu%õsc& A،cmvښC3L;PO?ClET8 M '2gix^ğhEM#$<[ςdKg H4'oGҳz !H5)JRҎZjS4i(%*lhD!VR$(@QgHD7E> ntsOdFWe=kFokVպկ6+o djSaѕ-=eEtV H*^px58֕.pK\BsgA׻ӕ-51\~7^y!"6j_fں*Wg :to:KS]s_FZbq{ qaoQőum+pqqz*ſun{*Ϫ,xvQQ +rS2 c8~Ț]/ewM@ABGODP`У~hv.e"Pt{a"n#Ŏ¡fFec9r?1 eϣ>R55Be)TPAD. ۫'W$Hбl.MltU;B& 6c;X(tܮk/hrǙ801mGWȼ>]CF*VSX[@eTj6?KhgJq G tp -̪Em?PZ;昛(`ϗ b;@@$ yo[ 'ҁdNTPU kF-|hODAߎ/(@J2#XG8`BA9҂$PR / BA5-+>D#L A}GA;VL:/ !H8bٛȂ ܎c[^;R i~#blqK#U]ܿD Ιt GʁUo$!@ ƴpb{2( '3g0,.4V 2~Ax wo? +,xz `-H1zGuUS?-RǦn|`z@5.kG98傁WnH.}hPgy.=Lq a8{r kI  QW-YF1uV6w,9x6X*Y({x.Y؊x(arh#aȋqoci,(k}h4o,XfPo$ ots"% X& H&,Ոd,8`mQ@#3zB!q Bp!h@=/oP>Y,=af#Y* f6H3+ Q ;I ,&Vk3 % 4o,skQPmf?:R$aT jYDsw,AhDnlو H{¢ kK&-u h e3؀+I;I- -e82YΒq+Y`=V-Y`(h>09,@)^D A7G- 5`@9 /i4y'f_d yS6_- 4cԜ1 ?Cel.i4$X4PktwwҡǢbi-*:6Z-2R<o$8j5MVFzG4 ";j57@VZXz'O!>5HZf 9R?ڦnpn{ 1(йAvt` QiE 6 XQ  sb:8)Zƚ&J:Zz蚮꺮ڮ:ZzFsR꯯r ~0  +G꙰;[{۱ $խ"%$I&{s@@unngQ4r @=r,#!@ qaz qLk!C#L Ps< X W"tDCGJ] ԣ c0DNS<9О 1Jf6 d?"9 Q, LT{  u 3` V4`s6BhK Q@+1 RA|aa P4HV4@ QR˽+G`@k[U p: 8gPY{" A{}tJ NA *P^P Vu" 8 [^ 0HhZ@?F< 4"0:SmbR`A8D=ٺ4X<#Ea:[vsqox(x-L"&ћBȉ<ɔ\ɖ|ɘɚɜɞɠʢ<ʤ\ʦ|ʨʪ$Ԁ @ rB˲ 'Qwp2L˾"p Y *fʼ<\|؜ڼ<\|!``aѻ@ $" P* @ '@R GͰ/J0 yT  ˠ1POQ &! `U+%5e $29Ӑm'a MDK o  O K ]@0?S *@~ Đa;-qnV@$A1)"  H@@WP] l0 Y Y q/ q } k  1 q (! Sa?P2yԇPп'  4` Ƞk qWKYIek;NwJ0` !i /q m@ 7 a-t"@ ""} 6`/0ҫ f`@Ёn@, aQP !'@MX]Sл PN `[dQձ8 ap?',!` I(Ak`  R!Pcp-;'P00a  d. W&NL1 ^W@ ð !kCF  nPݐHn|`Мk| K 1  °6m&ŀ"m?<}֌ 1 Ȱ *i N|@m@p!ؠslӠS !00 f  ~0! Vy H ,]`$-3 FeTc pڴ P q]P 0 > "@?liVMz } Z a į @ @ . o[ 1OPtu p @Aa Rr ,@ DXhE/UQF=~RH%MDRJ-]SL5męS'Igi2ÁETRM>UTU W]WVXe͞EVZ*W\uśWȮ@X`… ^ 3;nȓ-W Y3͗ 6ђK}Zuj֦[vղk~}[ԓp.7%@̗hytӭ;.={ˋ{^<7Ϸ^k_<"r0@$@D0AdA0B 'B /0C 7C?镎NDOD1 UIEE_1F$W*"RT!qF2H!eB U>eH%dI'"QF9QҠ[ @O$S,3M5rE,qV QB KQ,K%ȓ,Z֓3xĉew 1D"(6Cd̍}&Z]SjD` ;<$\vE +9Fh&k%Wjs(HX$"ȒzȐxDP:.ģ'|V;O(2HăQy xx״ge秧zdR\{JS'(Z4&T \h|kcJ&D_Y R [Ђ_hAk"TB~\`5"+.~5BX@PA ЅN X [@PP?D'"|a"Q85>(8ŗ̐%3I0@ pF8F2hA CPN \Žec92C 8@( (/"r =6ґg >*2A2 #E)E t @.X9%8q N <|+GK!?$Ӹ?JRCFACdđqa KnwAf(Y`P/@g:չtDLFA @$2a(` MӠEhBP6ԡhD#MjE338D:Rt 8R$AN':"(aP0EQ2T'L!.5 f͖5!Ң Uzַm;L-Su'嗥I,p5ٺAZpH).Slk@ 0x#2j RC)Au} hEЌ` RI {`7C8.lh{';ײ*$_0۽ `"pnpj|0*b w Vk>y nV:0VUx@8$:d$bg@ɯD#RA)h 뚿[pU@%_(y8(o v0a!4h0?7 UCX-bwl@d:D5?DtTG3%d<(d@z\DiDG<22 s@=Ȩ髒/7 +VC(Nt ? Ih&$Zp% x;@Ë$`:eN+oXN =[35ʨʩʪʫʬʭ9eTFeUMyc3-" R8dYM>[6J:N dL>i I2ehej5aeWeHcifh&ofg&)HQ>YZn{Hf2dDf {6.Z.f(hVfh hhbvhhh~ieq&3jK v藮znn&  v Qx&\R-ZMqjPMj 01鱡>seӂ  Qآ-Q)yn h땰罖 y뗐lɣ>ij=:&Ni^_NdAζiðpcQ~ftLرӖk.l.ggu.l&甎4녒n1j^6V 1k=~fn En֮z>NQo<~r^?lVfXf.fnUm}PHypZ@q~z҉iNv`~lymqy&q0qyq@r.qpP$G(r-q-[g&/!ﱐk~p7S~86 #gmb2an#hq!q8qr:gІmxh<tAr_{rH& xb Ȑp*1`t8tqx#T' 2ZssڮtS.N j^/:Od߇mh0vG IRFyz҆gfXj03gM>lT/x0iPnn?TwunJЇypk| G hi0w-s|/ f9䐇k0qaʨD 0 .y/. YҖwx4a6pd-5g #dl wm`&ٖr9/zПdGt w{א1'6HQL|g}78;جUmXzY~'|z/1 fyͷ`0P_OIdv'~ G 'ׇ2/G wpv7,weゐCGzp{O{ ,6Xeh „ )l!Ĉ'Rh"ƌ7rqܸ.Ip_gҬi&΋%e X΋X>x$m#)ԨRRjաZz!=('bǒ-k)HN#)@lI9DBN9bI=g93@ctt®~;nQ8+8 @ ^cKHh&qQ=!aPV_$J?p0D\`ʐk̡1y0ȼGf?(!@!3Ȋ. |`F &0,t r@j,&Z>!9'w|! ֏3H-1eK6k4ADrCa(l"q*Im  4޹VbD 3ASeD)F x3 +2Xi QZ?Bx@"o+baʌptR?ƈAHjbxZ\"|QTQ!dD##ƒ;`u+05팢tf!R A%bgf ^Pˎ@m<\V>DN FKf5U47IDi'n`I )d(7ЋJ˞ D(TU*䀋ࣩz>#&HUVFc)ZUqlR @A($7xT x `^k%< hs~$!99(/FJy60@\yy)pCl4#>ʊc}N.tt+}0&AU7DԶ4jVӺE_M#Y־~ISƦsKc3epR_O2\ MOE35.ژGX)L LZqv@D1Q<ԇKC"khN*X@tAb(@aERDuDĄ>_(d(hUQ> R J@!,Hpਂ*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲeGR$aI͛8sɳϟ@ :੏>L% ѧPJJիXjhGQ,(vٳhӪ]˶۷p5*Qx˷߿ LÈ+^$@s? ˘3kYAlԹӨS^Qd'u۸sQPJy N}5̄УKX[ سk&ËO^_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8|X:  M =:J3 rRMu@_}?XOZg:_{v5jsnO?ww\:wwO3[@7R^yh? 8_[J>NpO?xʼnc^噗J򹃪Ofs?@=;e}ߣk3N&hcG#d<ڰ攎e*ن7a G!C@w|FAơ@8 rvDA b8 :*Y;0tΟfȁ PXMT?$ئ@*sA dԞ1Dsy40WMAM,%S~/uAAC8=U  d Y+zZYDO<&)H h*h@̙l@!Bo]>Ț)7Dh-Uv3̝?d\C-8;ETKHJAPpu7I]2▔X"U<ԋ07&>ODphLc1A^\7ycy Bc<'Y>fr~( d0, r,x$V =kYxG#orPC4ahEH7' E*0CZ4=6Iu36WʓV#hY9̯F2g;eV WxHfilxΚ==xv6mk_[ ݶmg⎶'}m`P8n={7wmjn0.!d2q%IkY{`^y- 59n "sg\ ,w9O]ܓ6ГvC08Am{qV\t36%x3}'Kj8X+)Նw|߹ߥ >hM/XMHdGsd>@yu[HK~}7#gc>q|#"4Ktp/Ӳw )>'iP!ψؓŢd7GXXsZ ~O K rec#8U?v%a 36xyX7> ѧ44d9UZ!0``R(_``:Hi#PƓC%5E"M0 p# U? !'7x"0/P)J6uNcT"&t0b5Ax vh47WD|@T$=&0cz#=J#9dwr}U*RJ|rG2 HN@X҂ naMOU+t+7u<5!``n 4|P b\`r RJ†.qJ!ۀ ѳY2T#H{ pa09 p"x.p#`@hVȠ Ae, TV>4{} 9i%JdUeTa] V|0l , ڀ )i.F  ':3Y70ZGtS&9#Z3륔$Ra p(2BP a )TP)0A0@]J Dpݸ-- =5ec! DpJ.TJh5` 9s 3*)-КB|7HIgPnP e jj.٧H`Be)iCw٫a*(/9Q QRZ:5`Q쁭DZ-J !^pȮ0Շ`-6R⡫Ӡ몯 cӞwcDԌ1 vhD55 I/Y@1Ԥ ./pU+(@-(k7 / @ƨ{bã>9u.{81,&[. f@0Р8H >>фF^/9bpk*JRk/ |vJ4K-0=37ApHPJ7 ù_ /@Űя0i˻/`L +<#` KзsS5=s ZY;k.ُ9/45 9; i. WP/Qc vw'`7 $c0] KH®2 A?UJ8m`RG#sH<ñr AFBX5.\ÄaT|,ߔ>_n^c .lR# iNa~{ƻ Ѡ 8ȍ>gX}KD;a7K2ɝ"[1* : y='\yB ! V{Ϝ"p ]*A#{F`c*Vˈ`m`"@?`R>ԿA5!8%O *; ŀӞ4!8VAX)¦+<[x*\ KcLVi5(77BD<+ 7"(r\z|~׀؂=؄]؆}؈؊،؎ْؐ=ٔ Pr@IRE)  +8<Ӏ@]+ _e;Crۮr<7-+t#ܽ27:KܹrB^LݰbQݨB ޯ"޼J޳bުޡy# ߥ!m#mBMlԈt4O-[a}_BM0 C9v#,:ҀBY]$8N%Kp ! \aPePdF@z\cC(  Ta  (YNR&@ T֐IS4UL Zw{K0 @P!/q|N$0NTU ׇü{pp !x8[o^^("8ȗ$hU RP=0D UL0Q$`:U6N|@:`0:Etz>PBI8M]6Uom` =O {IB: @q=p J K'R P@PBR, >&/$$\Fv<+V%Q X!'"b3N?"kk.F$[OjC"aԌ!i_x|~?_?jpŔ?)Ӡ(`'ͣ|ը/(/(ߗY?_ȟʿ?a Ȁ )6P]3,=%B p Ű@#'a كzd P@`. <  4P@YWyQbVLH%MDRJ-]̶ Xp,xJ*:L#, ˡ,wjrpji$O1dEVZmݾWܷ\pxCX6ʃb { JLЌG fΝ=|*ChhaEC>xX 4(@J*ЁͲxmTlHl|M@kݽ^;@-ߨL/8Ek\L#F 8/bI@hXk#/C 7,x#M"`#(R)ዛUPiGhu;40Ah@ Ae PM3 ,IFq+юw6A lEC,E \ByyG,E/ G2$qA}rN$.PGVI8Fѐ+2c[6F# D $1WOP@v Z,`K(f @bقbd$)%/le<9OzJ$-4S h@ODKǑ0 bqQ,Mt iHE:R4MRԥAELeR"p5iNup*OMnmG @sթOjT*բHjxzԭ.SkXŊ(3xÓTYVխk}Z"W"h+\zWk`*XEla{X6[WusŬ{6Vvֳ)C 6EmiU{na`K6muKږmps EqKunt \b6<ֵn1A  g;^׼ Gz՛׽o|;_׾o~_׿p2TM?T7>(⼱i) awxl3 `$Nj*Ja"U$$Lbu: ?$D 0d&Kx8.Mr2H'@D QB* "799vsE M`I QD^Ė ?Ϯ-6gYŊ=a"5,'LĎ&(t,!r{ЊL' MΚַ(.]kP $Ӧ JJ hak\@@!@ָ>/j|mlw0=dF|7`1h@J9 GL)Z3xJPőH%F2QTUdE2^HHjYEF[őZ^A\E_aE`,EbDd@cEetgt=f?T Rd"qaCͥjUXלNCY`@sp@R  1M = 5MUi L1SKM5eSO98U(^_`a%b5b9m ,d@8=TDe2WP UR)HS2 -h؍mybp >*0Y~Ox~1<؍i@pPO@e(c؎؏ِ_i/n5f'Sl5+?7$< E`ό 2d覛@7P8W S>8nBI[+bMdS6$J'dDלW(J \Zw:E|xh3b:%4rivDeVЎ1gA*eiLk)-:#+S>[%ҡ Dbz贋O8S"#ٚ+;{@УjAѹ뮸˓3Hb`l3)@ZVdpB SozIU}\h4X!g-O^t< $n]!P,a%#"[5*>B _:2N'8Xk5[3aYC@ q?N+]T8%(}۰,a }B$E&++cSDgzkS;}Ͼ\}1p a@4̣:5տepP ꡅ~C5؅0O0 –>9a7Ir/qӲ5寀:a@@(/ d`}D2@c12 1U;Q4X#jAj(d>wi@LK`נ6iYSO!b}Px̣Ј@|vv vp15|$ @Mn*o¤N79'hUGʌCO5::YJMWy_B$^J!-rāÅI<ڷ {G7z*ixCJGBvRޜS'oI0suP$"d;d>-% :'>b s#L:ΌA I'B^0FdBAÎvd0ь*21xё" PIS J`pH*Xfk]#!VBq S9+5pA;2<2V1Se };6Yy|%3\xc  fd#௞h CS"f(CS4" ^FGb *Vjc-kU  ѳ>!_CXH.~ &#i c!C-l1mwF^ F[ PNjLF`0*NHTKUW=>f,YE~Ib7$l R:fip_L0|\Dџ`buDC `MD'X@< v  bD 0L!G Жj* w(&D1<@ ^р- FxYt !F<0Da5-WG>z`ęܡ1.aM8hݜu_GX!`?"Bj%ʄ6`ǃ^EXb'͕LbN*)"G@x@D Xy"0DZ*xIc"3f]E=5:$ A5X :7>D'Ix8:.$Dd# D;X C. >>ډY=[b@&6B.2L΃= 8CʗnD>6BA(G&օڱeр! @LXJ^J K DLΤOXMdO.%M NSNeSbMXTUv|9%VbXdRzY>0T#g%Q-(YΥbh%QB%]aHh@e,(_&N4 lbFO& XqdnMe>cBRbriD2T% \1؂f&z1^hC 19E$^cBlז[VTfp.EüM[!8al2'vbDoGGev5dAd?&[x6W5zgDPzx'i7>~'i B8ڧP @ra1Fn悊B'ZӀr~(gUVCkLn(hjBЖz¨SjX(:(P;)ZϏRJZ>(N)VdYn)-cЀ)))Ʃ)֩)b!N؇0I)ʇwjd>H/8/1` ܌|3h?:HÍ(K 11Q}45%^ZD6CP6jsB)ْC1Gыh q?8M`G, ?mg)@D+@w2^hDH90A0C@÷N ĭZ,7D(^B xBn úctT@+A 9+֜@p' @pCATAzو@gE` D6@b,>j!@B0>ĖCFEEvĺ@@@B`BgҩȢ,lD0P-C2Ch,A,>8BHBA$&1ZD|C>@Ω*b*lxB0L@\@7nB۵ nĂ+[.FlB%070Կ PG xB$D@&"A@@hBn,H@,n<@pH2!]ڀ* .SU@0B5&Vi8.D3@Ah4&)HIdC'Dt*A1S?G., q 0B-pQ2()X4Î D: hg ;p.* .C4Hﱰî&AXÑy 6D|jDD6ԉۑ DLD!J>)2ÑUdB0@8؉hG5CK(y`G|B"DL6&D[ C:3$Cx- 8EXG*D;y0"2L=z'EɃo<Th0IVyG'zD#m^>6є:' ;ZF~0tZ8̑ԥ8` IG,ciH?c8B*Fbc(m[:Aժ†1&5*~5$f}MT1j䁍ghdIFQfcӤ09jWiHi4%\I6bBLNTi^t@8Kƍ""T h%sSLBc]MڼFҤ1NĪQL5.2-IdFqcyfv .I+epc0]Z1;׎(7!8yh#V;| !;6^dL슒sB<3f-Vjci`F<eE^Y; 83 0œV Ì͐@ 5_ytrA~q $:yV.3ns*cݙ`QHfY *4d鮒76<F"{Ah Z!"^NFL2TC2~j5Ȧ#K(LuduakT[h]2嘳FJ:!PCMK ` wLh4 6M"CPF}r yP Ȇll?HtGU1ِK KRXTg!`YP%Y5md4。̑K<vfH|1f24X!tȖFa`y8(pƆ$Fdv!+WtK( @TH2G!F"pA` a$I$hQ]WI3HQQ"#C` D¨ 0S826}DX!V/]8M"$sYPYX2Å82[WYE0#DEh/{%B5CD_1@UoҲhHz#Y5:^FTrIϰ 2/H^4t3#  # `Xw'vqx@Dڠ\ /ii5!)P0F6}F,\^`)[4 vIdp>X 0C"c%}]9y]YPɍ]( ظMSs.FM56wx#Hp2p57tZ?hpڙ܉]T}i TT^/əG xCD@\H\^r[c@Zz *r:Upjb⠡+CJCnTEyCH @.PрQe8y:L5rXc〡 w B&Jh{#q0ɃYĤj EpETA_;u]_1K9G)%Z6?@5)0*4O%EnCL xƣGC* BK6 <B,0`HqS<buKԳCm9r)o{pOţLƀE_rd1.wēKhFV#) @XDN0 ɲT$K@mK~ɞ ʱ#MIuuJ?r&ɧL9EtLT NHD! ! M ˝s]GEDûȐ 6,˰#x7P4` ɱ\̓p%VjAϜI;Ѐ`еsCBZA@ e 6 fi<Ѵ{ϼlP @UЛĽI) FV2;rJ)C}AqZP~tK0Mn:kr32QLȨ,>1DS=dTdPJ^6\tq&"&QT3jS3= khr !փ$Hyۇ=QDcT` y6 opG5gN߂F3{uvSIPɰAQ y $%1N i6u7S@A~I n_1T#07<s*q~'aW_D >SMdq ӐH&TT@64j@_:>̮PFSSudb,fzxS&Hp,rn5!^J_5 peSDFUQm7pOYt%9E?1 N5?11D:|=?_.n4O9eFvGt9KkbHj_ubqz?;bX]V?ɯS Z !j;@7611%QŰ#HbPF\7!-@%C >QD-^ĘQF=~RH%MDRJ-]SL LqKCtp*l5ETRM>UTUZ6CDPC4,5MUe͞EVZmݾ={HOp*r8^}X`K bV ?Ydʕ-_>.]bZhҥMFZj֭][lڵmƝ[n޽}\pōG\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ_m~:a2uŋ'x/qKF~(p }'& 6L?Dzp>ODƿ,yhlr!yĆv'#9'g6h!L1K-/lhq,HB*F"S"B',!p/4PAL1V)ߡCw2pREx%z X o@)r5a@ 5;z81Xiy&A?0E8CUp ҐUT~EG:xS&(,1mёNglc)#EcqO}>q PG}U$GHҖ)>Mȟ8FRT! &n9MjK6z:Dͣ,3䇍,4(e59Ozӛg>O~ӟh@:PԠEhBP6ԡhD%:QVԢhF5QvԣiHE:RԤ'EiJURnI]Cx8FT吜/jPS1mMkzS6թMy)8ڐ# WjR^*1.l4 %I3gjZճ3V* &ֶk>ߊU2R͚_UUIHb i$V2XJ9e5YD!.lj5K؁6"݁jeKٗJLmnS eVTA2׸g.|[6ש o;ݫ Jҥnv۞v׻nx=]Why]4{ 2j|*ԸF~_׿p<`Fp o%tWDh[k#| bF%OF;WSܐ?9!ԣo{.wiYz'l82 OX&)Sd[jcDk$%'ר` mDt G}Uzַ}e?{:/%҈,%o{ZoJa@J@%$; Q FggE?~lFq$ 2U0k)R ̮[>龎pHh7T0*ؾ:025Vrz{f`{Q腥 3DTdtԿxl}È0yYxNHm`xi>=#[c ,/0\A@ki`ɇdx8 HBNZ8ۊ\ 3BػJKLMĽÍ{ a3dnq  pQT`x63(tn;;cDd]hb E"V75+<m\MmSQ0X0M30p'-@FD eԈF(U.5Wx`[DGЄOHOTOTVuOpF]GDX0''0D _OS;To _o}tUPj53XWG@`} pmv $8׎ ؿLXP1@B0(^O MTXDׅeX$R&xFI0`IPXLٙ՟ڠڡ%ڢ5ڣEڤړMLDUMЄGЂ?OYMȄIULx۰ ۮeZK]5۶ [[[[ "mE\X5ɭ\KgΝg\5UuՕ٥ڵ%5E^hSPp=紊8KDmi?^؇\cW|vh6  /u*-dҧ^V03z"04牉g IPyEzcʆ\.uQ%P`)QX6-` 6a ӝ.|(vb'l1d@_ݦʁ[\HIz"^^`fT{ux= *~7 bc[, GT q=u`I/3n&nc]J[ѓ^X䉠&!ShsHq!ىǜˌ04 SIH,(UPHYZxNJ`nXHɐyo:`sԈ,# .mtOCGj:pYHnwq[`쑐9sц憠n@d CJ1yUxGY2e%E`?kƁSBD c_P? [ЁURenn*͉i2m``0 聰p+YYbv A_P_oHX( P1u(;qÆќĈv̶6قUhV{d e6Hk Z@M8'nv@)V`7 0x`jdž(ƆP4a큨C)XX Pw(ƘFɇobHn0H(g1u/DɇX(Z,Lne=t `xS(iIS:h`vj~ƛYI݋(YHOF6@VZ1`qw{H&xU`Gv`Pi8quvwl^ÉgX@HS݉yH ga,'Vw:kZ i\VqS7y y=9I)9{&9饛2}u8-~;C;;kC ܯ( #> Oؿ3铬߿?{2< ʉ06C7-cZ6`Pc C(){`FWҰ6&8 7^7F<"Dp6 {zpV"47B`8-f<x? /.@#(9P0@:~#8o<$" 26?^i$&3F&uq&C)Q5t͟R9D|%,ayJ0n 5]e|sIG%2,)RH!J.ּ& ٔ 3&8ù@m2 ũuaQ<>@'>D|&F'~%c:a(2t[hhS ud␇vwYI|#?8,'xxLzPB|lj5U,t+<^txt\/ *~|<ݮ4B{3@*1 ~&ug} d3ưHr}]]rC(wԡzzo9}T) ii(HN2 ms0DǮy@-esxC^9Kƒ"hc]ذsx q̚UVl9~sI3}jh` 1_CFH@H0^\>Z0D>aE!< jv!j`q-ea 5Saɔn$I@ve[]?[m8[^ #8Xj%8 ]*%^&^5ڏ<4HnC8)4nn@AtHT@ Faw!e[>\%]]hXeO r Efnt`Aj[-&cQo@ԀHBΚCC:(up!VEtN'uVt2Zf,f8ZI@  zn@fosRB(.8$@Bq9bAu(J'vO%6pgn'k&Lyz#{ZPPEd C/.E8&lSa؍dx?jC?̮Pb>k#6.Z1E26=FBQԎDBxי6D&j>0n @tP"E D2$Kkh&}ED@6hFe8 ꉜf>li50kBT0Sx@B*54B(nB@9@6*)oQKtP-aE@ nÂQr0A.i,qAA {2[;APԱ`C<5qQ-o2Q,P2ʪނ5ےkN bgCVs2Q0kDK4(2*ciP#+$d?+Cg/ڦ-G3Q@.1c0,GV8ې4sR 97 5N.0";г=9, Ap*= RgTe@#H8hmD#.oAP 0hBCS* WM4JKG16D=CA(hJt 42HgXO?9@@Ui-lG A_BXk@L\&é5^3P`ʌg5@4H>aˁ ?۶A)0pN-OލSP exE^(6O-HiKahC0x06' > w 73dHB8PٵzqNp0M6$l"(YWH;ZN[a I>=4Ҥ!5A)@6WJyBÓL]Dq2;Ԃz2"\wҰ@ BЙM>tPhe/ľjK>@`błvd6DlCrH$!8wLR ?N``_9BlKȩ994 FEA3Ol`3D?CN/#VToC +XwrK-\X%P?CCXN }؋BP}J}ڏ{Nt+ٿf0TSeC?N}W7)m99=wNXC14чF=W@phG ]p"#s3>R,Q؟*~Qi. PZMxזl=T:Vj3>~ < ؾC-툵hf+Oq?QCl'5@@ 0`A&TaC!F8bE1fԨ߿4s6.JByQ`2!-g0q/hԹgO?:hQ? P+@OF:FgYb )j\U 0Ioƕ;n]64VXӻ5mLg 0w$ɡu a;=tiӧ/]HjׯaVC@86@'3-3XV:9HYa/$(`bg׾{.ѧX߷Dg &X㣈7FLPAKȈe )́N@!"p) @li!* aQ'j!hQǩ^"([zHƦGV1#!,('B'-v/ ZhZp 5ٴGq&9Y< )GiTHmIFGFXg~gilSQIKLX+RYmukg q櫹.dx[Oӎ[鎚 NۘO\&na Q>)<_o!.{E r\ͭd"AWf]G" d ă}[ZKy^.!(v)hݗ-h?ZN"XWju! !֪4cKi^@CC. $y<@'dO!k AlsQȆ 5 Qi| +X4d.H08 -A֐/MtPD"pB 8CN/D`И#EwD{iFСQ,JXn"ADhPp d!/D7#JURMBWcNQm( - C_Gh:|t3`& 90 }̃臁1$؆ 4,l -;?Yd0χ́?wBm# ki 9 =@AN}Ԋ9PBnAQcihG$@}g)Kaړ"^HGͷ҂43=)&7sP*aH:袂8~6uYCqQ 5@^$u gL`լC1bmXHS/E v `t.y@ :%Ag*G e,A d$)#P55|`lۊRC4PQ,$RnM \>\HF5|; C &0VdOb J!tR+ $&xs[@ B׍Oݙr,F`)hRohw@!A˥+i:C( lԒ(s@ q:$?l#=,NDpC4s=֬(~#u0o G> CcAuGlm,!B"!){ `બp "A: aAfb>]0p6RZ ,P(&(h-a^0 uA i a# g e: <0 +G!"Bc%p /B ;bհHAԡN ?& !,^H*\ȰÇ#JHŋ3jȱǏ CIɓ(S<˗0cʜ0QYZĉN@ Jѣ IE$RϟHJJu*"J '`ÊKlGQ IB`ؚK*Z$@QN Lar%'E˘3ʍuW0iqtӨS!NQk ۸QsH{ NQB]-hzn~A&سӫ_R_l8OTEYA B D CD(PbTX]B5 ]%]F' h52z _"n"6Cئ#Ђˋ842 }H8$h.j{4b FF?QF#ElH: P2w4GigH?80a R2Rk|ej6Ć"sK]F4!o`HQdؘNt)|C ,)jNf΋5A%z!H<1 df@a]Zqez)Ċ6zm~!@b,۰Ʃ%V"4l< ^PW@  jp)@P:;7Qp]J7D`A 7M 0G/=Ŀx{ Hr Q-;LA ; 6s)g'-TbOi{hyg FISGdaA H%q8q.Z9A &e褡a`<@e0@ 'Z0ؗJoQQT~ ӆ6':Ͷ.b>DCxQ<َqda^WQ{^l쉄0))3 b6!~/SW*"u+R a|tUV QF+8rj@Kď 8/9y 4~z]T~k.0MaA 00vBwOf .4ؐkR\7ŖaodUrFqF,'4kwG GPD(fb'rbptv"ʀDW{㢂>&VUo78zB\@-rvy_-PYTB 4+'q|!ad_XOa,7e\rhM#5q =X+wU ̲ ^.` eJ jC PDQEY[)F&ʐ4r|-w.&oEnV1zĐft&AفxFBpICP%Ae858P`HI `bB @ 8-KƐYCHq@P}L p }vt^`UhAAY(/ņ.E4||nq 8 9{R{+ˀT94J4z7^'0zrx`P+i q$ U2-Dd:^5|ד` +!vsPV yyЋӢ٤voB`WXKpX2ltt&1YPHi+=b6=Q|qu1]&-TQp*IC- ZvHi-hSPH. ffi6i87P079QdR99r#%VWQ ƞ~t ѓ.^5ga a S0ziX ˙gƠsp)oӉspvpn KJ@Cn=) +tH);q7E^1@``; w+$DKCzH);Iu Y`#WEt ѥ^z R?@@-! ভpz@Cs?yQc Apl Ħ 1RóHƨ TJ  8XpL T%AE k(@ZzEXÔG@!i 1;VAȀdW;pm.|, @jȐJ;:ħЧ(FB%k5J`ga^"6*%;d : ۱&azzS ;nñ%9C.9*Ӳ490{3:K96۳@K9<;F;~#@~SF8Hbô4ˤ.RkAn;XKõr3v T:ekG`^~؞ھ>nOZ;Ԑ)FQ9rѧf`ɰ2o=% 8 ?_2u $_&_q*sp 2?4_68:<>@3 -+/>?N?R 2XZ\^`b1?^<_fnpr?t_v8t~/}vpb4f>_2Tvlp25ɐ IC^~{?9(0M&_oPH-oXQ?/F/-C;?ye?M(\7JOp_NoN@ DXPB  @'D-^ĘQF=~RH%MDRJ-]S̓-SΜɒS7o^Nq& PiÐK ,S*QmkPV]~VX*ZmݾW\uft Ӧ6xޡ[6Rue EpJ]ZYbhҥMFZj=O[6ƼIftn޽}ɴ)5ל?w`Zm X6uHe{-m]F9q/<M;0@9:".c98MiHPA 7CgfhpAxI8b`f"xx tb9l2O%(hK/̩A1(yQ6}&8?N;3#j.Vjf"iB( R(LFBk1yPr1d-";q"G}҆ӒT"pxǚ/W]wW_6\Ȏ9*=3tCY6Zi+FPkf `K [,#o Й(&q&=m(gNЩԝ , 7UVh]H"8<b,yie_9f&@#Q=hxOxCf]j:kPں~)kg{t-|TU(R(*(Rݥi$P)f"zhʨȘΐ YtOGtd< 'BA65J!x `N@߭r䗏BƆi&&Rbhd'P`ފ.i&'D?&JA#+1FxU./Ŝ M30 U/K5V`9"V5AvaE81 "ME݇΁iHf`o7D kDDLn1f[Ƞ \!D0ną0N-H\!3|rmа+*aC4\/Ȑ<`8v8D4#t0H@sd()3ȉC!k/JpPcis]4c8 A0ȕvhD%:QVԢhF5Qv03 A}2Ι] 忊TBx]^@eJ^(E$`s!jPZLdHZhf4>h dDP( XC$AMLpD`oTo  3  U4Cq8GR3+D4!>YAz„b̃Zld%;YVֲlf5K 4|"pP<ΐ&՛H+w*RDSk[N',*1 61"Ks@*S0 XReots"{,,`2 }Rv&r2R cYU@ *1 |)`ph1L6u`S`7p%l"cqr"{;7muw_{>vgG;حNu)tE,w'I/t`"Nm!yx#w|%x?~<-c~憿?zҗG}Uzַ1y=Kl{\p(>E|7~w~?}3?~GO~+7|3bt @ @ $4DT|>?0{zDXЅS0QX& XxWX0#D$T%d&t'()*+,-!Wp43L"]BAE }CG6G;JKCDLNJK LM}AESTEOtV,O](8SȃITNP QЄFMQ1@?QQU]W=R;p'Dռ\ LP8Me]V PV%\LKЄlP)5q2Љ؄n2txOP?H-CHP jNGuPȄOqVPEXs8iPPV&(,N%(E͈ΎPLȂ$8RpXUٕJ֗YuSW͈8% &J* WW5ڣEOPZUڦٗŻyeWՈLCAҍJǮEڲ5[{մU۵SVqQaڍPTn8ۼ[S[`Y4ZؽELȄu\uSuAU\[͕eIK05-KPQe]ԅ*]U\8%5EUeu޳i%ߑP8=~y=}~Ї1L`۳__}.1f`18H(4 5cȴ nn(68 0(d@ajD gxb a?PuP ch}xx+K.+ "!0MvcPbP(a x( -6]Ć?_.Y؅] c5Ec ՉHy!uQqX@XhidH1^PPdbaUNEhcx yK |qOP_QS~ T. !ujb^PYȆe8it!EvX5ɋ`0qP,V ;CXۓUظ([VflxW0_@y_ƫW0%rlAI(5ġD.h:ɐgUVb8vc00`m(`?3fhe`8{FBRPR8"!" hZ0auP}>㉈y?FD40ʈgY[l@B* b2I*0Y,έbԂ-{ #Ckĝ,#ڋ[{,f+2lr-ў'U01 Xΐ?&B[h81+8;8uw[~9k@2Q9饛~: t0D>JԬ:;n#<8y?>瞟Dy;<'z{=O84< ?/?K??^cr?H@@'2<> 36dC(h#w .|! 9C $}S` shThF< C"(EbovB-r1uJlb'vf<#"xn|GCv[& ~Q&L4H p>ND(q%@F` 5%kl/ QJP3SԎJNЀ+s|$w)a ! G&AA|IiRˬ&1;M6)qz$ A'Frg\b8Aw39iцq3AaXzkfjX㣞%bZ]#:,1x!]mM>xC1\@%C06Ws>ryL7^7_+ېC!!:pbT! /O ^ J?ZYKNwԽn9n7m F/`-n0d_.)弄a:[2741dQ~t59rSa];ucЃˆH.5B )[Mr֪l q|hob %C`rLjQ H%P5 2X(qk뜷x$Hf>T'&HNU j*]jE BKVd<SحVRpFؙiVS=sN~c׎3gb 5[_=x:$X;DZiX6d 00}6e>H"֎j5񵷗baikfxۨlXcPNCj`0A6ݑ|!15.ysEg2!(i]2K<L>}8ew L9FlNjtP&@w $G`$=6][EwJ-d} C[ ZLL3/Z812}]̣N ?05@+$тQ,8YP?i`.MMSLLƱCj g D @A! -Kl MFo~5 @) aԶaAHR), QPD-D1(a @0 I |TB}Lb/W;%] P *ņOL5VKvKb.J\M&_b@)/Qʔc8xcRJYWeNQDJ4c#qIX@`ӝj(#J|@rH4 `|y'{ef?("d)z8@c(}!XKeh&Mt8l:ED2_~h=@Bk0F%x@AX%ĮA+(:AX$r \ RS9HēJۅ[a╖)=qQH !^ZCR #`"J' 4)( B "ڏ)< ;aN JlVB1 "C2t2%RmB%ΘUg(*(h#P*5SM0J6CT|"J !hx>BZ>`H#f5$v˱4l;RԔ Phd"< )¶8D2Ͱ(=5QaRt=6D *D$0k%+fPfMʲ%]u,=]S͈Cte6 l<&S*I @z| RUʈIFYjJWeYCFcI橂w1,ȜS2A.J2Mժm\m I ,?De]9  !. B0.6 o)jz +OSBM/@` $O2PvJQvҥ8@/Ȥ;Aѯ>X>o!S0(?Y>0%fopNl!iF L 0 בN فD]0+\#QHrІ, 718/) CqpK ݾ 1(@|0ҥgFeAnq@,<@L$UDi}4Eq@6>\@0gB+t7@HrTx,-$ 7 Ĺ Yp E)DME*E(#a+6'@-*I3AhK>9@U)tAtB0D#EVqH+N _7?|2wm>t&J>6p] 8mxp>MC"DA\oQ<ou4Î$ p#nGCvH@@$5hDggHv =li$kv v.9bY6pnqkp:^7t[ynSoOH-vtow-@4" ?7ww !]7yt v7}{r6xv}7zð8v07gV5@ u8 sWp, @H$7~@8wvL\>8֍WxLv.7/ajb̈he@aTB\?C$fIC3p;BJNEP א$9ʓs :#kErD^ a9,y1wPŃ+\DEt88F-'&7DDE8@)\S"}OH -IGv-s$HTTK|;<&7lywB'yD<ݫXJ[@'>cx,|ɻ^G\݉eA`9ȓуQ?`C3Ь޺_=_ U}٧|=ث=/՟ǽ ۇ==W}}, vJ!>M 5jJN>͉b_w݌4}\現 `w>> eu ά'@R1/,GJsar\o"Tmrv̩_%B% rW/u!?@` 0`l &T~$0@yQL(naGA\HdI'QTeK/aƔ9fM7qԹgO?QB5Ja0b Gp[2~j@ ѫGpUm[oƕ;n]w4T RRn)ZeCUԴ@B.y1gּsgϟA=$%CZc!eHEKa L4R$HC֬ oή4A0# ױg׾{w?zPQT .XKFvI +bi 8A'l!PB>d Pz%T) HD*Qu*L&NΙPiq\X\o!\f -j)&Hj ЯU&hʸmA!."IL,5l7Y%[|ɥ %0%+.(obǵ=9LYҝ hRbD! UQI-ԹP薁\fdJ )(@j=#C5y Wa<˅Jܣ1NViZ0Y0!L Ey Ce 6dY ]=J&%?$0#3Ɖ/B`DvHă< HCډ)RߖtSrG>$wV10zsK:Xygrҁܱ3}dVkU!HIZ'l`V(k"뚡nb4w%_.p( g~K#y8V)˥&Q| {C9tpϞh#7a]B`d&{E'mZO^y~w!,_H*\ȰÇ#JHŋ3j$`*N 02(J˗0cʜI͛8s<ϟ@ s䡍D]ʴӧPJJիX"(@(' )YӪ]˶۷pʝ۔VW*8葨t LD^̸1T;Le.k9g,FjD3 D۹װc˞hДCս2CQ+*μF^7P:24w#bNV- \4`Cէ̩TXi((BCA j!hᅳJ qe^CB\ARӈ0(#\x~B&6@)$l<ɈC&L6XNF)TViXf0`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.M:-ǽ8nziLt{88A #|ú@H>2#'Ï[@X+foLC0?篿v8?Pbm C00k? ZU2q G0M 2 ˃$L;w+|lpi= h:0l#^.u^$d!9@T[B 9_D'zp$UAIa*T}I6fX^RinjAՀ'?]mJ @ُˤ@R BEl Ǡ&ϥItg2]_8,óy5Xk@)l`Ż>$&EHXgë/ژ 2BNjMs)5!xBoJf+\\ h1$8 `YfçbX*A9 A ߘ hqCl؈r 4+ J@h5 h5|`w%R`rʊp7Q~a<6WXZ&~ Nb "jc>pS0B&O>.JsJǢ":><'/ǯo} M1 5DrwPE-yxC&R~XۊPu_E~} @y*Xa &ѳQ#=)FRF$!y o{iL c`9;zSG uMuo0}A raU'@PIi9Fa 6g545eI8t c3<9W5ӡSCB9`Kӹ q9Y`g0S o4Cw ׀ y8;E4G(?dH<n@pQpA[aOvx8߀SZ FP@Lm7O -! 0 o4ɠqbfYH 0q}px ZD@1} Ub` laD:w?+]ӳ eK2gʦ3ka>1X0r@R]!C зpbQ>OacYBs  ` @TmѤ (:OSI} 0ЫrB+){!If$ ;k!(j1L1 0 )TR? M<\| l2NA  0a,0\Y([b$l/`dP'S'// / 4\:CS@BD}&-e)NӐ0RRqQJD Ay4eL3񴥙]&S?Ά  ְ ;{:pdÔscٰ fhlPK7|bmƂ es&%H36\D<Ks ڌ&.;8=vCtC0!meB L~m.h j=ߔ&`N;B-F~֒JN-LP-R>RV,Xʲ\,^bN,d^rh,jn+p p7s z>Bj S 2 ڄ^&3nOt9S0YdDC;CGBaL_ N+>=b@.+Xt+믲*^~Ȟw.x.!N(@ AmBKߞ ۞1^~?^"mo:pPl[(_će?^Q>(_BA?e.D F CfS@dCc?}h%)Ԟ[pw.$װ`_'__YYjI@7.`+M4g濯}Mkb@߹ Z E:` Si<1p  @VDPB >QD-^h1=~RH%~&OLSL5+R 81&D9.h.Z~ LTVZQCUXe1YZm0JlFa !, Y"|2SC]aY$:;Yhp#f[w+:T)\9t,8%2YJ?n rfU$K`&\N!͞ИPIz# fMx(wPCD @(!&,s '-ebȄdA*CbBvl-G !Bo1ǞnYfjq0`>\!l'y'UFtʑC+E!d|*ajF[nAZjQȾbL"3O=3aY2b i,[!4RK'ta[l hAPiQsQf[f 2%[lӄ0]:Iwױ,&T:b. [*F`}WiM@̗X@ub[j%\z XA`5_z7_}_X Rb F.T^D8bb/f@T]p%1&dOF9ƙ'TхUf^y%sfDx fZ"x:iZ骧jk{6lfFab0-h;o . 0o:<7qWr<'ykw.>!%U^QE{1&ky %g}{}A't`Q. dX@006ЁYW p<"aE}x` a0 $,QІ)hxÈ"PpyD(D$Љ\#(S捆>t @0T ҳ>V2tLH((h!|51LhB M@%: JVuvPbc2 Kh e,1 aQOT/AJW^\f8-O󉡸? kN1?!xS7%k-<;2EhB nV iCPVԢhF5QvԣehHR2'EiJERRT/iL#CԦ2iNa*ԧ?N:{¨GEjRgFT6JQ:U3QjVZƏuի_V:Veլ]%kZպBuo+ڪԸծɛkRWկl`{V ְElbX6ֱld%;YVֲlf٧jilh9u$=#1@iU@ 1R-8 (Őb\>Q5K BqO`ۛC\h  RTaEdBZ{(aHp'"(6pQX rpC0dqBt'N@H$(pbJ$@%8!Q0!86I@"R$8`B|N0'jyiS{x"R&*yWР^LFgA}"IDPxdC!"r !#H!x5F-vcBһ6"<{(@Ap[X,>( h885Rp+X{ KSLa783lc7ueDae@H2ȲT>5H΢PeˏZHC2h`06Y߬ Po(: C,eәVkPZ+B[w$BQ3y $P!bUEFSla%VbEd%fRfuVNyiVc]mnZr ,H5v5,Ewy\`WzZWl~X"׀%؂5؃EE؅e؆u؇؈؋׌XO؏*aَWXQّ$*PpQّeYʆmxٜٝNٟը>0%ڢ5ڣ=@ڥEcڧJZ2zZ20ڰ"q-i4E[ҐkM֙M[tͨydmV[[Ԕ8nڋ4q8\ɠDžǥ202\̽͗†k]8ݕrbmkYKq8p pڎ皭ݕQI^pe݇^8mۿS~-}_S(_ue߈߾;x8֍ 1 ``-/\| !_JiPvpvp5w`(҅I൐/h.q^*ᦉ-u& yX.}X.vzEnym5f*,b}(bH 5YKq(m~. l`bb-b7yP(N4-~Kѐy6֭ s^wP06_ mh@RbiImMîxՆy, @!H؈Cތd$ن+ؚ֑} }M/υ)M@"л`_\pUM2a6iaHKOVYuH vP,[W_U؆< uR4]S@kPe`\zg}b"!hcmڝF08YJ4P"#/p Nh!!hflȆq`gP VٝH!j5 RZ0dxWᘆ,jX=j_b%j9jH!jGkZ~vbeiRH\ިJ`~8 i;xmȶ(~¶}y_w fMNHm0R0Ek(kޗp|l Rn_mcek y.,H6(.>MjMln~N`Xo0MP[Ulhb im.ޟ=Tb<N0b5^ exy W# pDz8771\0 pڄvpiHq*2>q'Jo n*Jw"vi+!V^?UCpe2!X$!A@> 0F/h̀{6.f9!{T؆g8=/hijNKeDOAyhF>[tBRSLj^` b'bGucft-bb 6,iW27qxUhl/pd0hˍwxpphHN .p"!֮cqpF!!'Xqc j~P;x _eg ض>n. 8 jdZ6V:2І@e| N ey8zyj愈$ NVG 'ZzK8W h>ROa`rK>2$" {k9dOGF%' {)F+`z|%猸RCV6o>^:G~؏ $0612`N  e; 0 ` {oƍߊ! ~'N}7?q~Yk6j,h „  &'Rh"ƌ+ LV,:"GGtl%L&Μ 'РBXZDM2R'61ϸTٴj P)ZQ U%OqN/S$ TJ I \, qb{Ygqu+`l2&`gӮio 8k}8JY7 /{N} 1{EWϠO%(eqsyn:TWtw MrUX :E@@5X!Y MC6 CW]H,Oa((^\h5ы=HP-cB(Ms  Yo$v@|_G%[iU"liQH)!T@F켯CsBr*z!P>ߤ 4EBN9ͫ wɆ@x@IAzA4B'Hѻ* >*A`%02]1&dNyG$"@,$JH\dD ]`8DC%pJ`.PBKch8OBq h2 d&+0ЍB mC6~6ѐ~cْDADQfP) 3nģ@KZ0P.{,jBJE/&d]7!R?\%3G%ȉ,)'(<2LPkZ2%sd+9f>Z[n~sLR8ӹ:l=Y$k,A_d*0h@Mю~>VMHSZ-HA+=_ћ4 9ԦsNj0۴Iicm^dyYֶnr;N`ܺ׾5-a1ʬp6\ƶ <=H(wW̽a}QE딝YiE   0L$8@Rt0Y9d CO ЂQ GA,pF +tda;vl0vVBVZ )W1(eHd'7*,R21@{Aޱ~_D.޴.xE )D B ;ver8ȵΏVl @ dXQ H,6WCV!H7.l <%_=jE70T"Ƀ Ђn- B1ă@*8AVD6 TEpAh@4#x*|x)@ Dٟ J5`adƎ@XC*"7d4TD%lͅ~ը"4ɡ@C XWTFASħpJ2<4>_4c,@,(U6C)7?BQ@3<d40A#=RKF`$BndFaDJd0Ph$F$G$h!,HH*\ȰÇ#JHŋ3jQ;Iɓ(S\ɲ˗0cʜI͛8sɳϟ@ Jњ E*iȅG%M%0CT Buׯ`ÊKٳ-2tTVr \x߿4ŵ̸ǐ#KVDQ(a qL齖N^z`:vHڭs=1+)2<<&B " X!x;3|۽RuNub,7z nf~ȸTP|g{7Hq\i"f!Qto $8} iy#)(y])=2 l&)d#inP*7'aԈ*TBTx\ɗ`嘦YR_!^`YFi>4"dixQ&Џ1Ŗ'tT&袌4(lg褔Vj饘f馜v駠*ꨤjꩨ>f꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o>c<>N?$?ÿ8 # >j.? txEJ`F} 6H?BQ#>ac` E@c{Fx!ipkE:aC1ÈUt6 1lsFP4Dd4!Ѧ (Ͷm{tQCF!٦1("wPQ(Z$5Iq4@1"rLZA1 Y,ey/JB,G1yLRbIlHS fmvӛ8kF 21m䲟+`?UxNC ;PL#`^eskdb%4AяG^Ђh!L mW@|gx6@fQ ʐF+ȏT O:'c)D(U!NP>y"&IFUJc8YT%0"G0#6] Fkk{qq%@nȏ溕]%KԹ(5jM>]Y8y- ̼4d0o~=\# uƋ^c&tB)%-wg oAyu-ПeHt63 H:2W%i\ސe …ƿo~]|uβg6v |M p<}`w/p <,~kІ8B~flBt.A+1V ,my0!!Uȏ_> $똹o\Vsi`A;G^u"k|ژ1 /*4XV}^}L晾nL@vgY"߷ x`f38gF@ү>7l^P҄bxaSҡQ33BA8M(&PR/Z8(Ah!1pEL xd/'ԖA"^BZe{ɗ1ق?}CKs"hRy$PIQE620Feam c< f~rZ~v<#'m|gT釒 r}f wЂ3p3FGp;plBt }BrF593bX)5 AAa` "Fe4 +v5)35020Yd8TF `7?Z wBqw1 m^`3T!КB39@H i*8?_ G3;p~pɩ? ɰ}d vbVE%os7RtH`iPU@tv $Bj&3F;xqT@aJ *3@ z9YpGJp QMdT!:**t>'j30B(A%˗DT oP 4b!(iXi[JLY)Z*E3GbȯXn c 1l۠ 4⪰SY \ᨥMj53)'ښ"TK` 9w5 ::  i)f\WeF9{6z3DpSyYaO,Fs*ؐ Q ذ@E,w0 @x7 VqwlRC7`e g^ 0DIXsjP4v;<0Czz`H%kSfE"G)@=1:1f$P!9$Eu@'[>k>{aaz`o0>,L|DdA>vhʼ aj՛tKU{>!\ۯ+| ]OpK`qzEuրyuԯʺtدؐY >f;|[CDA(º?{B|PQeoqGl5\kٯae0 `7rd ιCF]\+P`F}]d V_@K 3P&P_v?_쯂€x#SsY_Bj7դ rA<_8e[] @Y>q  PMP7SЌ))sSTs#5? DQ]; a`8ZUS,@`Y' IR)[<>֌p}rMU(YYy><  0 kUkD:f1װԔ7ZuTi! PHP7@OJ79Db=1A䪰H6# 0pCא\J2zcOj",5*Pb y8:-8E5Q=P F =! L7[8$a4< @kao7W5c]>1Ϻ\3&am})~9R4u^ ^qٚZٲB]=;cگY۰#ڳںc۳;=۱=;=Ais 81f}e%7]1HI[=6ϭ1 7-ws3ɔU$$ݗB`9t@;9 >r:>r:DaШSm6? Ǒ@Jj=-?H(,n3Rѽ(%n,'>bH3J}dsR_;3=?~AN7 Cu@a?)3JRr6?Eq@1=1wrBV>JAnKazgq{~n:6QF=$&F_ ,npr?t_vPBzD?]4x?6z?Y_t҃6t׋6:Vq G( oj Pw fGotiW6P``a5 6lsVjS)ЋOg?_Գv(>%vPE .@ DPB >QD-^ĘQF=~RH%MDap-o`.n}V \ăO_M1%ˁӜj`_Qe͞EVZmݾWܸ,%@ql,(nk`wb:5> ә ׂ?ihҥMFZj֭]+TV8Ѐ{ EY):݊խ_Ǟ]vݽ/ .`Jx*+r(9\'n/lyǠs ,&B /0C OۆRc Xk'xРUZiMrLJ^Yej)(ž dI'2J:)5^8 @p3dy=0ȁ !`*_Z`fSC"1*q NxJQG4RI'H "U& ҆1 f!(A4f\ ybiHy!8e R!XcE6YeY{oB%E`axHAa'6Q(|&]2 BӁ^)F@Fe8`&"yΐAxV hYȃiꑠ= OJ(ky$< ` - b0@ ~D+g: %lu8H  |AI @2; aF `E!Ϡh"!L n;ў9l0x+HUB r# #`ۡ`$02 5MQ%4O)H%ԸEnr\6׹υnt;]4 nvsȸv~ $ @D7Pԅo|;_ nj_. oވ Poi ,`Apjg45as/Jܭ% H_f AIF8A$4@zf{㴠x#YlLc2&5k!Sdmu1,Q|J9Gd({H+h.D@|:D ` <t%=iJWҗ`dӟu+ݐHKRDR_h N(<\H·1[^ҽ=Sh,g d~2 ,IS߾D=nrFwսnhҦ+ "3 Pb Tcfj]CJ/X @"M0 X1 "(ME#  Urv3`o.ZLic580 4.9J5+ 65ސ T*A}D 3բm`NSP a 1s!ԫA2EZF!$[D>B3apZ@ݚi() $k7~i66d06Ci®h1\] ^7{[x{+[E]< ]{ZC bZ$xP@6˼Kn ì^Xƨ…Vk5p&@_@w ˾r5%,҆FUԏيcvXh20eA sx{2 l􈘍R R$;Fȅ\Y0948Bw8Bl>ctУ%qű=HlІQ(^X8FA )3СgHjO{K}P@ORy(Q1hTu9S,u3CZ@&iE}b;l(45 j؆d0 [zX }ȿS%/15LWЄkuHF Mݐ:,ލpDdzZ[8Fc>ۓ+Fdhւva`k6v(6.0+yPyX}V׌c~^HZ 0dȔ݂CqiRP&C rs`6ZE0;`\݈RX|8(eh^)bFnapsO@`ȻڹM)yӅ;~=u 7I";S7&;eZlF4-hyKsO-D$uځ m@ѧf, مf(cT`pCrbp c08`+: A20?J 5gqPiNX$k<,)x)AxӑOgOM3A07eD L@@h8HB x:@Cq‚XRQ ՛f!xf96hx{ZpV?Zh[;?+V(HؚFZXQB ThY kXq&g8djTPAy`l((ghX8=X9 [N Y6`e@pDgH+XSiR9 :{jl]ꯆfW~-VX38_YXh#tpPNH-I@\bF&$eNۼP5H( <+@hY >~Y–HX 9f8 cn@V ͑兠VXM`Z~YÃ&/%R0R\o9_@@ȅ p[܇38% pnX) _fW<;@9I>8x5)t1M8;JG'7(6-4nz(v`spO@dk󁀁8j<@cyg̛քxnJOw ؂@$+0?W7lAYO)UkPdfm<&b m$oP{ d e{#8>pɇxb:.yi4ǁF:SeQRpM#uoflS =sxBC0P&JYk>S$ ?o2:0('ag腶np2_@ Xha+Y`zfE_`Nr:hjIP>(rH;6/2U,BhFʅ {rm;^E/N-hgQW(et!c<[H׭[0QF?Z0TlV[N 6`{d[ AB@o{4UITfފeO4B(2S2kX  ,ڴjײm-ܸrą-W+ vTv@ϟ T؋]j4(%_%%&t}0x@AP L SЉHM<&o8M4V 1n! ʗ3o9heڷswA{5] ͗dɪ%Pdj@b2$ |7P4buTK)ѬU3AםsUG-@@"9*C x[A` 5`Yfu7$E/@)Y;4s˔e,Z疏 xQG 1D8В]X-K6N-ev4FDc-Q7ˁ IX.$ (&dzGj)DIA)w:N˘ Ւ@FfUi/ -\J- u:AfĂ&dPT f0jAI{2K-z/M/.D(/esQ6˫b1w@*Q"ú`D칐‹RƚE/5!<JtAw ^?1 ˞5+P6&+tO`[2fsI,P;imjQؼXC5GIS KYWK6?s(C^ıѽ}jمabߙ~:l=魗ˎ"i2P*q=;g*AIyų5XSon<#K?={Zy 1P J[N2o$zs˟>/Ƒ@?>< ͰuTb2 ^ wPE4ήu$<#Hx^\>!1!Rbh.0 C RV"Dp1Csb|3<jn|#>"^\b Q# )A2rtxGQUD! ]$&3ɸQ* ˪&SU2uDb'בG6[]Veh IIJї<&2y@`2re)Hl<#@(Ph@VԩVj>i.+B}`#*p Fne:k#<޺NGJ{Qb^+Y 9Ѓ^x8R)H@pg0xa$/p3(n6À0F.@C+=`wG&#<. ?<  :-8Ó@鷒I<8Y OȊܑviZ1/>| i`K\qS1T쮜ǒBق_Gn٥ROZX ix>[H2, ^MD@| P{t > F]iYՕ]yPY8Y HZtZ 2(PC: ǥjV@><[r ZlZ U>h`9@8vʅԕa™0]N8o^ѹI\2h@t8½5CaC< 2|L᥍:Gwv!ͬAhL<6AY=.'`B 0!1Ń6A3{ C/z 5:9P DE&+c@l@ ](Y(atMuxiCéE]C?C?1 :?|x[m1mGy`]"Gr $LA YAZqE9ed=Z}ܢm]UV2\=ޤSN7D]5X?ed$5u`9M?xWwC"C]2eJ8H:2$Jތj<6^AX<ܣ]fcO栉V5?&yA{%:bCm= A@ۉ's$6∃S۫Ύ:OmO|*[W9x6&6뛽X' "6rW|8Cf(^F+IzH9E2"AtZWe؟" k[|qlivڬD9C<e%è6,qfl\-rh5ĕ4`6 &_Ak Z4fegF+]\zeeݡ"y6l'*6lඃvÄDH#!2GE; )b "5WG`o\AAQƍ[\+\|^=UDƞ՞Bv6kLv<A<7GL2 \eQ(Kt&/E\V^?Xp;D<_j$qzXCe}/ſt?(ÖQ <*u4ErqIQrm`l a *1^͐@DJD?hoG񦸜j±!ԏTܶw181V {o2(:ot]D.LjGgmmwpaLSD6$ "j(GY@s(D`C&_Ϭֹ$$2ב- S$Zқj-2[).܎6swB @#[|2E'Tr$;s\gSWZ8?8w`:4B#4E@_Q2t p:WtECU D)O \hr2ՆHS4?(ȶ݁2[GA>״Q# ^"ԬTD;tK yGg5@U\ti[*'3t V R?n}5X[:2dvkC3B2oc1ؓLb*s.W/(@ 4ݪߖQj$0P0LtA34vdv!_!@{@̀B2XBfD8dWx/\6AȄg 0 xW,@D$V> c*L Bl 0fA,(wE(7~D\Z"QhK1ԭ8_۠BIlp !48⠲*E* Fq_N"T5AC*僆HC8T+/ xB6%K"DZX @Hr|3rL[(*> 1!G@Vks|m̛>ql} 8[G>E1z)v~p1W(@TGJjG4S\8 ~Q3dZzk$C׸_}NCM6,@`AHxaC!F8bE1fԸcGA9dI `. kW~W0]':hQmKLZoɴq[jw V%'WgѦUm[!Sl #|x3ηZRGAzxed1gּsg Jj᪈SM:?ǖ=3[V!>DTAU~BQ༂N mױg.;ܗ]uȷW>@08-3*sp 4-rb$U+LF˅ EΫB10~ @i #( w j!H(~ 20]ZaH" |M'D ŃT V9.1' 蜂ޱU(S= "AKp&P=Ip*ƶDM@!(Nh'  :[#eIB/UX f*g8ぃ*-VUx(&J9,#^(,-sb0?e%]("qƋb1b!y\ #0䱴a`.%SZEs~0Z9Ȧvrl5XghPP55UZT2⸨Cx'>NST ]╱2%2 eXn%AAhaΎb3kZծVUY[ٲlqۙ o+!5q\.=/]VNՅtUWw[;z75/p^~u{/G}Ăߚ.| b_/ v!a O1aHLo# c Yk<O Dsý{!C}'ы<(#I0x~qa0A؟?½́W>e{F@ɒns%!p )@1G:#K^z!=Pt.gHaaˁ፽C.$H5%G̣tn L6PN OƑ~?87n9kD YI'Σ?GX%ټ\yG1R ^f~g;Mz_&Amnpg?aqTG$U@ JR@ԥڒ6\æe8Ah\K!戤̰.iP p@>5"C|^7bքH8njF:PUQ6F2@q6%2mS v 2pb.q&Q60!WTۤ ~L2(|8dö?RP<؆DWe.c|+uˀ1<+oV=֒E= \Ӊж ` r0 r¤S-O~<&H1<+ H#pGd qRH,}rjB]Rd}Dc) H L@03ӫuMhYKYWk`/-{'WS7\;IXГ"7nIfӿwbo4Dh?"n4q@ lAnکp,R%7N6Ny!d]^Y;0cLrjRKU1dL#}O'qF=CvBh5_A-3V^hmyr63p)+=@_9p5ApROI}:H"o#PB`KV\t%!}Є0VW5W'׆6!{QpXw(oTewG|4 yY4Ȱtԇ p:܃8| m'5XF,qvBC8GrTHQQ`8#2A58#֡$7~gkpR}CbAkP^ *lc 8  xue6]ft wQBzSS= Tk0zpNC;!4 !5P+, `?I VE7O҃@B8utiPb `ɲ#oC=! 'd~Vb "# -3Y#o3 wBϰq#t΂92 4 +,#vCqA#'UiFQCv3a1:2ׄ6IHG1wzjdb޹#yIO"Vn" 7_r|I!@R< yԠ; D)SU"YjzZ7!=@vM#Z #]*if~!` E8܃6b4WU!X)B!5 t8ʠX#uP!hHv<.P9Ͳw5P Uj٣Bl:/f?`"vJ/sZy-f:?`P00Ч`*uSɨB5sJbjڂ|2P@Dpj P 2@ h*Oaz-Fp5M3 P64FL_Щ`s_BG" ! i8|Y" %@XJ: % j%F409&1fwY% #z1C7y&Y yq EDpA(;$+ 8"9ְj4+$^k=#%XMRwABPy[mC +˲R(C!`.%dIK6.@V $I#N h # !y[.?dT bT-ۘiVXۺ;[{ۻ;[yba2򢗢'T [nB5)'pi@BOe2I_ϗ|4wKG \GHahӚm' uP a<Pu]](pҏpi ~zrgT@1:]'1 @'` U`{ʳRUE&pb&W Y%QP.][!ƣP hr4QE7L1!mPBuB_ ! ER{p#9hC=X]ڞ&Y,?W7.ډ]bJ%˰(FMo}۾=]1D^U[!h7.̤}TȿK[@ՍwP#YT#ox3ÞH8t`m?Rp@ m!Z >"#) g>`=fB@_j um3 ezfPa,ZA<"gfpN@O%q !`TU!V Pqf{r-MI `h -=]Ԑ¶==&mhb%mQ֘ h Qt`  &pvژ `?zL m *V u-nQא0 9On'{ / d 98pc.0(@ KP 9G p @!h>Q 0,ap..!6IdKnY gK2t.{7h_"DM~LG9TnCG`P-XR#ZzQD#7^Fb7rQM["TOryd'fvjO'lb/p?'o?6(v$uG{$}7MR5 J?$?dR/&o; ĚXiQZۥ _b Y&oPº_i%3.P/ǯ Zb?:n:Prq:=O=Rb ÿ@@ DPB >QDxF=~RH%MDRJ-]S̏hSN=}TPE=i'RM>UTU^ŚUV]~VXe͞EVZmݾW\uśW^}tFXbP1Xdʕ-_,#fΝ=72MƬjאZlTݔtn޽殺[ёGnV]6mk zr#硞]\iUg%Lci HKV^)v6i Gأlsɚ!( 0B(kD(HaN#(:QœAQ1Fy64̠x!OAJN'%`oHqƗ4}\rѠi%'k4Jؑb$bx|2E & ` on hw!Y 0gs%DbRi<1N xjȃPA#P£EHP_YÉ4^O%Jh! Q2a!`VU"z'HAc UJW<) FEVm)*F{ B'ޅFC*fڊ<@ȃ@nF6Hڋ$L膠5>0L}a2e RT apWBb %!hM @.~>(kxN5YftT) ( p;^;$' m&1J7<$W t -։~IG>A7偔i N$ ``hb~;Qj\!!82DkAh H:@f-(ܤ&fFu'JD Zj B IKPBV"iw]RY 0Hμ4@&A"B lF`m z@8VA\*ؒe $>@@P1,<@ z$!0 HPi/)9d^^U307Y  ≂hRC2I"kJ@A=\{ ЯbJq W#r@L 0A bAr&  LI L.^.TA!eJ#LgPXR8x b2̏C@G8 #@Q,dtǁӖQ$beIdۄs X<d01Y$2ިE s&4Ra-˥됁x !.i|9A&6nSo;p2Wg f"ÍhIihħ(E.j!Ѩ". ]--n Z`5-zUvX1x "ծoρPHD݈0Ai*xeRF0XB @1(;U%!M,<;ZzӴZfB὎b \D6_̬p{rĖ-ic]EC6\"&d.׭ er](?'@owջޟBn.r1$G#Wpg]6w|I" < AЦ K 5aoEb$fR X-ebǸ3qqX>d" *t@r](m G,. @(jG.hAO^e2c6sѼ3Ynfg:ss߬'qF WkiExA?( 8$-җƴONf E* Lԧ^I] 5e=kZ{DF1,@(@ ȃ*R1Z6*b! 3C  ?v VQ<"/MXB Ena Ļb@ 4=p - Q[ _xƭUsQ$3; K(X8#zdA(D BI-8 C BĀ.\u5L CB[<‰' J9;\C:t@B\ 1dE$# _9D(4JJO>DQN4*T a5<Ă W T[]^_`a$b4cDddftghi 須qk g82Yeq.mlFy!xG! y45l p"GzG@Ǹe~$H.xg+1L$Fd8'xǁǁ,Ȏ`[ mhh/4n Ȣ!wȁGIٰ:-;/0@x`e 0ȊXƚ\\ɭD;HP}8(ъșJ u$8ʐ4XHh|DuȄ Xl~* -Gl˷ @`¸KK,f=l/`x@͆]0!XK<#\ɹ MpmXd,4 qFLˍŃJ .M"IɁc(]P0x ^x͝ $g8Az =L87քOJ4dL , xK4z^ Cdjl_4t)^HϙHf/9RSq~0L0LƱɁ,a8p\9\6_E\RA^l{F9tTPf؅:ƘɃǁ`Zms)ڪ`bRJŷR_XZЪi%%F-tHuQZpU\8eZ ([rڭm)^by zHxcxdYiÖShڅ5p]`opUME [rbح*HO0UY_eشtMKU:!Tа؏`Cy"Zf8 W߂A VU3XWߛejta0I׊ª1i I}$ޠ>ܩ*>^)i':#C- bYDxR(]@ctᇨxұɅ6,x8m.f QbQ]$ /eXV"_@[' nfh;wUJH4_ 5ѬZc,Bv9|9>e󌈵ܠ@䬺*[^X+MVq![+Ū[.-Ha͉_q&r6sFtVufvvwxr~f< ]p*\f.[`f^e$cމbV+͍XelZم*XTo^0+6G6FV 2̭|ZϨJ:Ufv꧆ꨖꩦꪶj> 0գ [ i&6FVfv뷆븖>kջ&kHXkjȖɦʖ-^/vή5)fP艾 2FVfv׆ؖ٦Nm9SG"E#cpfn6nLL*r>A\R|&D&e&n;FUu#0ےF~2F`FopYmZ)gmm-Np o 6>p\4GqhoC(^MgG5}$VZ O. '1ҮqN H '_YlH 7P(rrq}=p_r,'s2)qsh-6gGg*$7g1o*Cr 5!:MtGsalsWWhVX q"Wx#dFn9Gl_2f YCm29WWX'uȁ]zM.`uN"ч؆dPb6CSx7w sK OsV ry}Ј8(qЌuw0}qPq(uzO?Ǩe 1eX^LSsr?PdF)g@ˇLedp XLvxkHSx  `(fxpyA rm`(mJՈqhvdPy /x=_%W`{OFqHȮ_WgJl|l$|ʜ qN(ʦ0WUQ_]Yhxz|hw(vDɦil@X|`L)]FqPGlPHgvTsOAakWaGJ~؇Հ'/500Sqp~W~Tt~~M/„ 2l!Ĉ'RT !87rc1i A*Wl%̗fT83WbŪe3&РB-jt责G% t^RB.\-G⿍G8ϐHy-ܸr%sKM2yZ 8aA])[uCN߇61-aTnТG[<4qih5Ͱ# n6CjԮ 0b@ a12:ŭP 6e;ƺ #3])o83䍘`(w74 VC6Z9 _zW}yZMM{E8"oQ]sW:ńWx 8\2%$JB s$I*Gh$%]L)eYjeP&T_Pbr)t&Љ62h'PQ^BLffDZR]t%RR9%BR`xa)EL&Ď;X;m:*| yPz[:J)HLd0y c̱"kcD;mHJ[- yuM-ႻQv(W.;]e-J/P. W:<_xP5\R;12d.qz"jLlaBzd-ʧ<0Jė$Qoی8YcORÁ22LQw q *5" `3(`KB+I(@WQ寅[ɍx qA+[j~U$"gc`KB V1K2*iN^bd"*T1CA "(GcBpcN{(ȕh#5jxNuX!juaIMB4<ʆ/Ph X?hH⡍WsD00\BJ$&TbR!f5KY$ y#GlSPl@Kd9_3Ƴ;'By:ZTρjGơ@2 ͖QHv95nTQ 'K#D$,ES* @bXf@ lS1 cr%BR4!He A!.adŪ[ǭPʚ:?:k5R 1-nꑉ,-2*BW܀-T?dc;;Z++!!,_wʇk "pm.Ds)wG?0w}9iKN%/l_G BBTm9\C<䇘 wS< 0C8[C T0TB}J y! aC4 QEB,@pD)mGW$͵=HCRXxxBN: !P ^=E B0R8ъ>\ŬX70Й(qDEB `ÁA@#a p?nIEd/j!#U]Bp$b1IHR$KIrAL$MΤM$NޤN$O$LcB@)YFvB "DCbbC6TMը$6E#VƛXCB!:56CSTv%AF:% 1CXW̟\_PRT2JONΜ`FdNJKI@ 0)e\Uhg%eI@jbfMj2DilViflN4-AݼE8e5 }B&tL $g%nF,0B8E%48B]'G?RA6ct'ۼ L+?$O=!5^5G<B¿$D?|.(T'%@$(|*I. qF("1g(B\=Y <CvL0Te!ި&FtBA2%5ŃK 0]?0^HXN ᔂL<wZsd(9ICLx-CLMB"5YPyRIYK)?~ר)U CѠLG? BL$B4)nɾܘO(ЗHEjQi\aʦmC8jb|jECDF`LƸ9Dʭ+.]@*-WB8F6 („-1_4?Yj4VCB]:M.r%1ddiu-0w&b EB(BhB8lQ%.-R'"ڬj/l[B:(D(+@,;AB<e-HCh\B@@)xCL6} o/-"V.*Y@0  0BDB8eBx[BTBh0Hp 2/H$(.ܪ;"Z,=-Ep+)SB< $,$ ' 3CB H@ kDt)D1P.sQƆ+D51C$!(Ez?D0AD@7$-BJ./+r_G'Br oCB%,#Ho@2bLeB@C*,[XOB BI2>pG20 98$D300BX@hd_6_/6X{kbgSi:`a:gWE(BHT_shB v6nECB$66nvSSvnwGXlG'mŚ6rO7QdazF"(& 7\w\+u7DY8> S<RS" AwSK^oxxv7x DD@4ܯ4I' D1F!D2x.@p<]玝Hs!0X1t B}BóoB4DNaBDy19CD%E<@T,b/xEGoB䢌t\Ⱦ"&cjJ;A)dC4A*Ş.s";&)D-_6$6D$&#px oC 6M7wr "94z C,yB|IGDz2互CHC28wD{"+?*J+@**DGӓ''?1O>Sig>Sddžw諤>ꧾ>뷾>㬔>%-Uc־c36.??'?/??GS[?ckK{?.p?ǿ?ۿ?@5`A &$PaB 0bE]ԸcGA9dI'QT%E9́1k漩+N<{TO3EhҦF:%*5(՞V}2 ֪\z ج@ ,۱fkbK۳Jڝ (_tFWP%{TqcǏ!Gbe˗1gּsgϟA=tiӧQVuk֓aǖ=vm'{ֽwo߿>xqǑ'WysϡG>zuױ[{wk>|yѧW}{?~}Y} P ,LPl!P ) .A QImQLq$D*$aQiqQy R!,#LR%l'kTQ)[$+Ydq / 1F&zF#_&f_;}Hq& Gxs 3Rh!Vc"DXTK1F C0!sL*dY$}fy&u~&3Tgx Q @XMltFdkVC4"o#摳%'H HF[ )vI0CNX#8M5/҂" l q?QcCB`o(X%|. qfZXJ449./ "PЂUhؤȚNHHL(lKa"' OftQ<P* j dU *GFot8N<Ɵm3"*DN%a\hn|f &dkq7)JLȝPkq{'!QO-fO_onj(ʝ"^v f80cm!FEUˢH$.Q$v=FBGnb%4 bZ^@M؆6yhBDW2HN`:%؋[qD8H AMXWLp;3g0EWH adACc vA2=,F[b'Š'Aʊ3E0&YmPe$ Ǭ<⯉԰GC$!¡A8%.A"lq" 1jq1324!6EM0)ʉԪ"GlDQϏ"EG#&2Vd4̉Lr"(Dv\$ơhނ&7"01i"⎜ 99:q6(J$N$8QCS.hŘFRSZH.X4 K@@N]ӚW^KFFd8i8u8 kCQDzO9i+QݫHEmekҌ ka%u`#?8ecd)؄J62Z[:Yb6 -x@v"? Ta'n#,Ep9Am#ꮥ{^ !,[H*\ȰÇ#JHŋ3jHjǏ CIɓ(S\Y˗0cʜI&AQlɳϟ@ @HУH*J+(DZJիXׯ]C):$)]˶۫ʝKwYOƭ˷߿  Ӣ+^̸ǐ#S+0q3k̹3ź ӨSl4PCj۸s(ږ?NCɣKNUM ݺËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(O@+'4l󲣀O7,O@-tBsH'l2S6?+ RK@QW/vƄ.?d;sщ !={28oH P"Bc>fU0uGA^EI@1p# [o4D8[q Cd8D{xR`-vC1+6HiOH2E6fƅlvH?QQ ϠDQ L" anV8я>)d&.w ~ݟ`'S;IFrH2{)~PRDymPQ 35eY|הK4/G8aT8啳<@Y dO?#/.G~1#͖iLcټ:PI\rGgΈ 7]@ %-c2, F]1:{PSUuA (K8l; =~£# E+~աuMII^EWLKH?CwP+#O!TLWT# 'y2ٌ.4bP0B'ήDˬFZ'&)(OTt6"650Q? P8UMLFg; y*c3M%9ʦ]?Qao2n"w(A#tdP/ryPӆ@3^5赙,lD-y뫙B&[~F(H58EQJeC¡>]x0*fC6e=i$چ6tl*_ji2} D@0}xXEYֲF47og>"PIN@9+bq,;CH1c^ЂtBh?O'-!1ZEh ԰@e\m|t6 U;}չrWt]iwָuka1!Ǧ6 kۆMa|?ڋZy=Ѱp3)u 2hJDn0;Sƒ%hF[ zp:$xL@ n^ImB 0JޑJCYiő@/G*ҕ30IvasJ﹫~ \ߊFו~:0=WB*"+$||FйΩCB@fMtp}UG@ZN.4M3 wMUITbI @h</%:e?|&]`GygpYE0c"ׂ cpty{[*tRnO鋏McoPvxM3}ퟪo(3CG{Ao@f8%!kאgr Qf7,r]*(W%ѷMj h*7&x(*,؂.0284X6x8:<؃bUE'@f0p>8& 6bI$MbNiPX}dcc8%cp)p_VQ$b 1pb Q za yz؇|~8XHTQX;t" $:jxHX&( 3\bf&AhȊ|"jx*Xq* 6Xqŀ+0iVñmd  1bA\%wj0Q nH ɐmh)Y8hGS%Ip  r(SxE8"op9u?q [W8Q##Y5a2Bt4h*ْ.0294YFpYkD hUB_飓 cX` F9' %ԔXbD; QoT$P) x @0= ?d py$V)  {#ts` oy:RG!N@$Ùgx0%(]?$ƶf њi$?dQa@כFQP%~I[SҜҙڹٝ’Y%( 9Ya96Yv6U8r#gQT}75'_`M!mP 65:NDGD=pz @PI PP1u ki  %z@Jь ^` g08 3 +JP !@\iR`KZ+ yL D9!@d:XkPpN[a Ga*[@9Ch qK8)}Jӗ:'G¨ [8Xu 3'* q`A d (䔡dt7(@ J9Ӱ(9pPmQW8J>@&a :a 6`:TDS ذa aE7>2ęd<R$^3 Ee$C(,+/K<۳+q dJB$D[KrH$JGҴN[$PC2T$Vu#\#^;b!d{7j[#l۶3p#7VL+eȠ 1 zQ&Y* Ё } L pJv9 1W TJQUpq ۚ}|( P9Pa,{Z,@{Є,鯲*v9p  pŐȐ*Y0  } '(0 ҁ- =$ Ą ͳqe= m;_隮J0`р 7 @  '`^~ #[՚LCc`hP! 0@S p@ 0 P `9mu  ,/JpZu;=T;=|Z } b| z1"  8T}M.X  3]/z,.3  aw*탢 >0n^&f5!>>0,.&vx0\daGҾ/Cߓ>_?'|Ŗ Ţ < @ wO w*L0 *O !R2\oPU QtN (O*h'i +Lph*:\TdA ._QJ*ZѩD 3LViѭS {Weo,V F=l8 0 fo?v:bV? !H5 g@8+U]"L_odѣ bV `  @ ?+/ 09px@`T8Vm*-^ĘQF=~RH%MDRJ-]SL5mNM2aӒ" bW&5bNU^ŚUV]~VؐO{ 1I҆D@%uśW^},,*GPu0bƍjYcʕ-_|磁Ц=YhҥM4tZj*=m[l`Ɲ[n޽}\pōG\r͝?gItխ_ Fٽ^|ˏG^zqޅZj_~mv'bf@D0AbaP0B '0LE ?1"y,꧟DjBh(ŘZfOD:MDQE^<ѥ^JFdI(Q+ 7+R0w9k\K¸"T戒JD$R/lhF0a@)nK8Yv ^Wj2:#MojiԈk96YA{=lbφf챼{̆v=mj3'hm(l9QB/S/ﮉ(TEPJE еb,Q"x3axXcd/R zIGI?~)l9~(R5x԰hX( 㛇ԇq>dIܲ H$PË;0@aB'ԇ/p;3K>CB`p#: v86B襗š$ m8{x v q8yx CT" 2]CAqADxA<3"`RY=^NDIadyؿPLSI1TC;;l Z04jl N>eRqǑkdl O$kklٺG̈~~qx\Dy8灇yH lPuuGC1^:Ld\ =t@xȇ;Hg 4-ɛp`48džhŁAl~:(@h$6{19Yqx}ؔ|tĈ_邬9AJw;$w0Lɪ@q̋xwJ0 1dd1V$^Q;OH^RH!'SЊl[GUt<.MeL~z ήv 'X2 4O,K6] ӒOsAOsx95ɺNM{)L/=СS@xB}K}X!; HJ Qd¨cɴjsu [ J}M !M 8:y"MѐL%K>oA:8>Q>WzplGSAqps3HόpmJ?t-;3Iف# ˟)l5]SkT ESlp"MB09$^2f2@#Jmݦ)3 o`Y8}xS|S<葖ލW?cOH6X ;`n,Kh Я0{;FAAhp K`n PJ֛٘8UTqӐe`oHfd$i0ʹ{Y^;t@X#8*yݍ./ '@/1'2Gs iH6%UZ;ʐ_79': q F!8RQ NimҰ/ ҅R3n1Ȓ' Mʚ7s3hφ?V m4ԪWjdֲgӮmv7p7`:qXҧS_9|dKW0M8/o>:?~A7Xz=SYx6W?Ls FT`NR{6!j0*$Q%X `! L"=5xcLp;}PT: E# F(Q*@4eÐ58bTF%aQ, юUE> &B!PЛ=s\Aִ0>9fI!(Q 3eG`F@*#rE Y`H*-*=8QyQ'GDJ/Tj7@P7htAdEjt:\ApRP a|@gFA|P.3Ir/ֆ-I )CUjL]0/kXFitB`Gd=K2$c4qA`ZPD\8ttNAPH+4r-[gF_b FȃuI@%4k0ə9/tJ4q˭U<9)Gjԏ4m#8α؈>^mOdƍs&C%CHf6s{9NYz48 84Ď#:nJ C\;;>髿>>???LY?'p, 2pl (Rp / rp ?(p&,! OF$('*m!sȭ_谇>|tQF<"%2N|"]#7̉0pa 袋^"(1f<#Ө5n|#(9ұv#=^" )ۄ! Z4E2* !GR2%3Mj'C Q<)SU|+c Yʲ-s]겗/{i5**YAGE4oi Qp0:䌰̣ɆLd$<'\DD%DAM( @6oIAv%m^"BA F< U':#ʕGPP$ X*^eʋ@"xjB7P>%`IPBxB tSiU`(4BYBXૂX WEOB)\"HHFe -%TAq8L)YXH]٧UBT=%ġC\fG$PlG-m9BͻnJ)G^ #fR"'dH ĶCK@ATn ނb-z W ZT5 _d *Q) €b-Fo })0 Y0FASfDMFq($ X"e X@5S#ܺF/^Q&Zb#(Cix }2fV@$ pA8юW~2HT @0pFz9N@F x- P@>bHv ;=JiL-RV ZXΞ>I` b sR1Q u-ɠ.C1cD7 Y2iElJm>‰B@?l"b0 |c /Q6AaьuBAXb-|C1 k^B8hk@*/ BAz{oa`#(?𫈼!@ D\N/.s,xBa6ac"!UJ#A~􅈃p3:I8GBA\:.'b=^;!,ZH*\ȰÇ#JHŋ3jxjǏ CIɓ(S\ )0cʜI̓j͟@ Jџ>:ʴӧMU R`PPjʵׯ`Ê;6';ɪ]˶ۭgʝKd,Q; X:0mݿ Lsǐ#KLyG3k̹5E5ZӨS(@Aƚ(ոsͻTФ0ȓ+_R 3N`5vËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(cW2Ģ4l3X:#W#@㌣:l(9MDE 4@J;=.B_'_[eFU<ͨ N"?l}1*E8)+{35 d&9!xaJw"؄ U]nۡJF}X$<>B#a]Emn("#t@GCb($El逢@*ĊEEOX:!EhE3ZѸeoDdhc%_5n"@4jk4*O*EU@\j>~KE 2 :∈VI)ȬYMZֶ :ކVǪ46R0_@qU `HdM>I6D:cBV>E 8g))hC0MHje~XP>=]Ñ+ (d"y, ajkUp%=J;&k騚rE2?$HBZfWëM$c?BvN/7ʒxOꫢU{+25ȿ TQ:/ W.aKD.jΪ$ e;^Fui`1o.<;t5kB>@,1_1Z##DF%)\4qL-k[<<%gʹ" k!vs ׮ݱheG;k41 zZ\F2!<괂^+r]VDN+ @:e5DS_{rp#@ҋw 16U! *cI7il҇CC4yk#nz<}p`\Px=lՁG>b(<~&{ߴɏ :>I%;$ omCV3:J_gf,d'PN%j'HdN6l~2 r ' Xr%pVcqazYU}zbG!X遼eB>*"8RF)BDŽM脣P*H8RV؃X=ȅ^(RGHfx(cA35eYr}<# ±(mk+mxG~, ib|@ PIq~7!%ra=2 ! Ab@X *ŠHf08${GoR5%Vg! dȆvC4qk#dČ2uz8oؘ(߸&Xr(긎|(1a? ȁ4^cqw9X΀O0X' hC!OzrL!Oˑ5*vȰ 98Y? ! x!7vX"pP ^u;  !,wPOW@]Nw%}ؒ\ٕ^`b9dYfyl1᠔h)$Nj n<N@x%P}BTAT )%)OOZQ4U߰SPQy%d`ԁY?YU:aS%EM2]b c"0P~HѰD2JĜa bRoC]$ViBD(h9&IYٟ: ,VY^r #p9Id^N3 !vRu3q ^9y%4uC* T4٢%pC 'x6%ֵ6 @H38>NzyE#QWz"VF Ol ٹ 1za) ұ FqRLQ aZ1 Q&E7h z8 !XՓ@ 80  8$aa>q !  C QAЁ  {E7c#c6"Y"݊:x;斳s:"*sTb%VүO%{; $ jذ&$z!["D2$+$&{@*#<{B p 6 j-$:{Pp; ԠP_)ΐ9 ZO!ay %4   !Q  ` a!F@T`! 1 R 9E)Q ` P21N! 21Jǹ09!O@10#:0 p AP g6"LO, PJIE <K !Q0΀U5:yԠ zCiC\#ؐ:eNj 4q:@O `Xu ,p&Q 3 }p  un+:$U˰uQ0WHĀpYo q \#B~ω6Qz;#` )K@ l @ fpQоl~ib  OLЩK  n\̙΀*bac@m3-(! qv ` 2`J A aC@P :p:( E  \ +`J"@=*¯@<z0  u5a xpq̀Ȭ @bHP?oY[ L`9ߐL8=DJ|}a!F"uyd QN1ۯ|%2'>)F8hN]@ ic7 hvZ Ҁ jHi P ͐T x#gHN9 2g 1 o\2nO m *wـ {<qRҰ K `H a N ŀO}d~#7J^ tnJ,u ~ >` J {L 纰,7% !tĀ:. V̧$gБ紐> A( Î?^Ǯ(ga2.g#}0@Y n1^ Pf.~%\,ap Pn??0203  @ 3 cV&/3 Q0Kaeǂ WI1OQ ,>*9J@6U@J ƽA = oI@ ! *Y 0\ ! ?a"m r NI +ߣ3)e@{QFߣA3 0OA +Α Cato!d`rq @@ DPB >QD-^ĘQcBK=nRH%MDRJ-KZ #5męSN=}TPE 0TRI`* ā@1CnRHB) y2z+He-qL&)AN]"A'FexG 5j A5kO#X2ae>sH.un (qIqP'.ʄ+BDzRˈ6夨('I @L . e dryD*b6OJ EռIEYgB%=i@P"LA6 Y Ѕ4W< o*b"əFuZ6=l,ԨR[v lp| @2HJ*KȶrZB)apwņ7'ȉsЙG8=^6RM\*:AQq;L YH#C'8N1 @NkL YÂ@9Y5l0HE<ùC;Dܬ= O [{?D!q| 3E!Qky,q@7.}~@y8d7.2tF `6 S/6&xoE CMi0xSFd\FaC྆6H$c"B?P<++2h4H8iv3^wP{!\ /.9!QȂ@},9벆8h3H*{ 0Iľ>840p rd/.yi 4J.úa,{̈d#1X4P 7ؔ\Kx~o0y<-L4<spl2L~i &l(8/JԂ}`6C I`e^^ǘ a@ e.UAZ] ax 9XM! #؄XՍ_"sbx-^İ^a( 0/Hh0ʉ-5v)!hv_[6 h>`=^ +8$@1n/hb*J3"8E*cC>OT%7vJ'PFe1dTe뢎3XFb9c[pL.]{]41 eX#Xb0WƘDi 8Íkx9#>X́{P8o8gr޽F 69xxz&<N"Xz +`]WxȃHNhgg EF YPh4!.hyJ.\yK9dЩ& Bx.0ҭ`i8ihv B[00jHi^ 拓Fh&!F!zf嶆k帖ak ykk뾮g Sl.nlS~Q ZX~0H:k ^IXVuЇ&` CnkDɬ@`ȃigX٦ 53f$ 0UHP GALVfkNH (3@3ѽ9(U`7{9iNnN9G )8*Ij^)m FkIF1 `]%fr( (>XJl_N3)b3 pmyNiJ3*l /kSx`(R`ؠ`@3xxpxqJ{Yk שr@nR/( PS0J4d@ɦjmؘqp5P*.-"gH~k8@lHy`E$TDyltP Q~#чy3F W8;egvfs v7sAv w{w}xqG yg g].qxcr'y]d|Gy3HpSẻHWpW_b}JU(kXI[mx Qx/0W?eȁ?.P(gky0qdz=x:ߵ7/YR 3a>xkzk/.t` c{"1)a/;0x0/׎!|Z70J|f "-+ g%` A+*||`V R,h?2l!Ĉ'Rh"Ɔ.+#Ȑ"G,i$ʔ*Wl%̘2gZ\HPVH^,`yK*-kŠUpBe0;_Ct&A~M,3W$-+.޼z/+TuUE. '֢V"(5.#ed!4#J5زgӮ<֮@(6ĊkK '27yɒm! 7~P4pU^PƎӯo>ݪjx#Exw<`e' ADIZY(\u!z!!? f^JQ:ά(⇫#5x#9#y#A 9$EyAn$M:$QJSZy%Yj>%a9&e¥]f&mfo9'uڙ# w'})k'z("k(J:鞝.j)r9:ꎏxZ AB*ijZK-z+ζ*Ji;Ѱѱ%в5г E{дU[еeV䭰ߊ.ĎknƞnȮnZl0 ꋯo̭™2# Wlg<GLq"w *Lr0lr4ό֌;'@-D+loA14QK] Yk4CУ\=6٤@-72"A=wu}C)@} 7 >8~8+8;8K>9[~9k9{9Y:饛^aǍ@K4$;~㾻?#3C?Scs4"&Sb R>OHdI4(_Oz/ ex`LQE:D?JAA~E dh Ȑģ HLvi'XAB!-XDDKu*E^P p<@ݻ$cAmD߿%aT_ ŨŇ#KL˘"" ̠CM闋n^,Ӱc˞Mvhͻl قhBM N-mf*ӷ`Ll1$K9 zP߀h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'm#@0 sC*s5Xw58 -Pvp-@ؼ2?q?8 $!;@?}:M8gIkOc3.PؔW~q عa1Mƴc0f6l#P2>>#.4 ͣDuٻ@$;32|›6S}aq} R0@82cß;`B؏pP @6bUV(XA=mcc@߶qpA@21O l/ ?RBw{6L =ĆK<8}cA: d dP7pMJ@pPÓơp0(zǃ<ČQc!FQԆ?>y!Eh6r)Õ><*_$ܜŪQ}) lCDt"8tR#* ũG 6y#väH8Vd&# h֑J`,teˁtÁk"p+="C=2Ĉ% X"8dI#!! 3r 㡳>a k@)+~nz6=86&R 7yE GmJ'$) p%&ajyS `yDJPE΢`00jRe8ZthHx|+bgxi͎IUbYeul*%+=qhl.bS8ژm9?:O˨o]qЏHaSE nLyq#?1Ppۓ#$^|!t0@|Al<ǪoOR ƁI2-Xd&J[B9"UJE& $}1 Ldc9,)˨8M\6dlX%}Bi`PB }xXcn4=>y&g5dc .@1;UY&{odӄ#3a@AV@M x}Jl$_Hd0;߾!xx>%WE?k}vՓ863'0BT4@wV7cM6/!(qCnrGZ8AK5Ϸ,~7# HLqU ;/Q r F@*i?~2S2@<˗+7G|7,$}PQRO+F@YM2Q ڕ]@ uLCh dzXn+|(~nf~7Pn%gm2 6xq)Rxx#u1Nʈ#A 8s@QTv $Da ]}o5q6DjI8m2BaxE$0A>_0i±7hj0X XT ç!>Y7& 2 ]`4KF*yz2ЀY థ2PcӦFHsz+:2?`2 *+ 2YjTG42 #=#sy0'#=Ӡ>l+*R6iЭV $cȮ)A )EB0 ֠I9 +:R垄Vr$DmcM +J]ٕW) C`gS*)+%'˗j*Ҳ6-8R[BH+-JrN-M2T,P{bZ,\۵ǒ`k-_t7٧dRqi*3(wJ$bd2`O+*u[K~[@$@\sz#w *kP2arbb`;[{+!\b;#{u 75|h0[K!#e5['{7 t 9NJ3)Z0 $;+TӸ,, 3Y!-z0jZҒG{ TpB`zχ VDreTl[3!V=Ȇ [ 9Tj=ֈBSg@f,נrOLpט4,؊،؎ْؐ=qȔ}ї=+ۢ斃#;{Q7Ɯ-~Q=&\  !|mj*-<۬}# 6Apq{ ;TU#Y `uu_8l֝Ԭ N P & p߽#+έfi'S]0e`n mG IW -"Z ,&g :#%MQ@ AiP  s\ l QP l q(VR.#̏1 Va7q ]V=U ;$M1.-} R;a 0 i  @I k Zi͠z a 33ga@hg 'Ydg aƾ#p\8 MnEN˛R7?)ȉ%U^~8Px1iY<_RDŽLQH٭ 󐏒$NPr̕,򐰙:%VoYE[$,?).2(4_r8(:>(@%%"a2H&JaP%RoG_]rZ'\y .pYE` "vo} P~|Od&c݋!7FЀɰ /?&o&:ҥ?;D=jR:=;`_@@O1JJeA/%ߟY&?Ͽ3_3O%n26ie|` 'l_.\ Dmd>QD-^ĘQF=~RHđDRJ-]SL5m7A :TPERM>Uԏ-G\t=VX2SVZmݾjEZz14g U̓68bƍ?v<ņ h֍l5ϜMFZ5ye[1e 0#ҫ}8mڶdm6p͝?WYX eW[0t*UP^z@4yKv{lk&(ZI qP l'rctn%9z{"N6 ` ě.xX2 3%g)=&9(!gAȂMB2 FNdyȁZH pbFF i B5'.7¥ H|yh"C"1 Y#HhFiEs% b Dƙ:g"(mN !#I9蕇Det@Y4N: ԉ blM{ Y] 9l39 RHD]Z or8Y͕ !hF34[ q{heb=N , 07={8doX 3Ecc^([" A "N `&+s;gs6j %! 8y!Cjb'@cωN֨ 1#_'Qbfށ>}M*z!(sh,d Qec7#GIZd!dg8#H4U`bR`b 94 7@,W$_ GJ"":$,Ayl ;D)C`E: t  eBA J W`B\R@GLDm6"bI&l4./a!@pR 5t/ y,C`}Y9*lh!z T +rI !p$HU %xǃTaưd`~3a#h;6x\mgV1Aj#A-CX2y}t@ҡ kH'~eQmXQ 8$ː:)" ʑQ%`C*3@5,aa}cXptJDžcE4|.ÖQJt,ahv$ H2 a8Zg kךgaG3ɞճVȰ:iDY' | A`[`1_qqÞ/lqY6/tJn%tlp'\$bٹvqӈp"W F-Wch(eΰ[,õi+Z,׹u bE`*݊`/~^bf w?݈o|۞:-t L ExWf"E0+g6xp%_M? ^-DztgAЇt hD/э4%Mқѥ@ b`p!a0FVկ&uXjqַ.5mk^׺vkavld/Fv…Tkj((%@F:=!Fw72vv=oz0p  PEHw>p/r"x%>qJȸ8ґ7;)>r#-^ 7AT Q!Z Cɭ4˳î AD! ˲D˹˯Sˇ6 ˿D:TɰDʥ dLGXIJH̾ t$8DXRCJ˝K$##"0dltC눈q``ظ $BGxIȂ8$6U 6bC]};](&?X] X P!&Pb,bBP y.݈ rdȃa@h|;JW0dbދh&h ]G%IMe(>K66 (h b h@6(}#Rha8dj `0sVbAѢeb98& #Y ;DJHIQfe-STVe8+w\0wCp[((s(!mq`vnJipfP~RoPbF.rph؜^Xf,['_ ӕv\7j12"ޤDTȅB,]pS0_+q-Z gM5^^T_ ?^]‚ȅYY]$()*+,-./s)oPۅ&nmYVr) L- ,4]a 'B7: ,jd*p@6s=ŋ^H׮77L+JǖcVwWXYZ[\uXtZ[jۄdO Ou&i67iEklmlWvz8 vQIH+&9;Pugvwwxyz{|vqwSNRvd"]CGt'5/iRJ,UڑH> !Cl}Cڀ3F+:ƴ,?6H?h  F| oP&90Fs`2s:]LKq50.5 3CO[L88g1kOe4v@CԂED:H<&8"pWF˙i'2y-qWgtP87I':UU4qU%Q8VD*t O AaMiv9` jJ3|5#8b~p@ 5'³,!H|FxN /O"Y^B8FMjᇻdd,+ 8uHd>yGcJxJVUZս$~H!&NS,la%B\cϸ&W0| B'b8Ҳ"w]"7@!d&ae,31ن8F~ >ǀ[5ATzDe/\ta>}.tEpԕF5=[ N`FQ3 PJ$}D5\%y\b4>2N'‚/X7>:Xwi!b:acCf1dV"H>!qf!kKQY)[F⑳%\Q$:́d9,jB<‘2ġb&ۅ*е:]&Cs t`?@.8ӑ&E iW)\n%XY2."=[W"HhA2O*+&igdlW6rz4e()urM0g&m4L01ΑOBÜ7L6na'I g7K~ONVs!U,5q9OgJ(Wjq\m*o0C~[؋"xw䅢z'$w-Ҍ)e LCIb$ 8̝$թ1y5&V3k^הT.Wmx=+4 QD{ A;Ac--2Ncq1"L6@x=qB"(6T!^ʒ t0D" )NAg<#1JMwM{ZȅwFyd ^CAۣ$FK~ˬP# u°qE4˝G8׾CljC |Q eC @^>EDм0W]d@HF8]V8Dqxi]E`FC CC8N`JHYC@`XjYH2lC p4LIMa^CGRĨ-t}T-D8OCMRFED X1ȂF2.D}œrItaE5ɀ8 SiD\-)CtX~Dx1 vbd(^JB.h`݄VGx f(Po)eKeDJ/FA9DALWC8DT+A]^DCqLB2 2T#pb̈́"AL<<QтT.d( QBvD ?8cE`pQ ^DeY CҘAAD5CLK$%zE`E4E)HB2ܖ|[C 6IR`W$BTJC LKԇྌ:(6d8ff]&%CH- C7R_6LM(F0 mUFDQl%DCKF!EeE4G4!~KZ)< >8O8t^.4D D$ebqIQ ym(C&(/=D@PhCSB| A < dtMsl1H)|C*x)C̉ͪ]$eE\^ KA60cef6R^uY<L215)UD{1D:zZS.+dk**W^C@SF-lDHCÔ$4lF˺v^MDڥtV DVdCb޷hm+TQF-WkEmCNCLl#4$]ݝ@AAԾL_+R)WIdKphPrܮX>t@jdȎvU@1xVjB._}PΈ)nF6_|lzR˸( 畄A/A(/:o>F/BUh/n,///b¦'CЯ/ޯ///. AW B MD`#0Kbg|ZHn KҾ.GDРDFBMmC|poQ / K6,B,pF_ֶ'mjgKl1BRzJl]{DHO`iV, qqo~qGp)?0 rc2܍Jt0!wEDA -$O1 %KM+s++&'{2.S@"7RSmݑ0@Dh0:Fl'8/l26TRDܱo<#0$X"F !8$&@2g3=D A7,p<7LW:J-+CKc=GJD86?n,ܤU:H#䊮8-D4WAmo.f ʯ芎Ր-CTK5Pqy>K0sCLS&ttFV=^:F3y8-\%o5\[E[St0k1fP0ALS⌤0 ,C&|6UǵcLO]et %L#4cCp !5F.5MJPhvC(G:lo1NuuELcГl8܂ 1CBk܎x-nP D<(Ch2{B赫#LyWeL 1DBN;dB`^!BxCBBu.&Лx(DB*;9CyWy-@:O$,D:IYE3D; k?Ĕx77zDnďWg8F|Bzus'0@CDA,ª/(AzgxлC LB *-7p<@Cz|\DR6D;*ՆX P<˻ϋ=Z(D9NED A|H4CBJCZt&5گ/'cA0C,A\Gh_BWDWUAC;M|y!?L@o?>p>=yQ(tCLλV+!?Yh~ #+>KQ4Kk≧z}QHmF\hzLD0R%<~ R },(9*h @\H-3/LSMP)ʊ4$ț!1(69|Vq̈́ˬ%C!T҈Aǃ'8')O'uh*BLVhcM[q5F fIU a[d^kP!BL /)1m[0LAq{*`~,`*l`_xCTUCwLG ? 5 ùG>xI  7%L'<\'kր?i,bqu#i #E_g!P6߀gy g$}|rY?9lZG{ H*!%-`"I,UO^6m64IoC@0E#TyLzSFSUYѾ}5&AIOڵ{ͬ֙ 0*)q0Vֲw~Φ4,$komҭ!Mzη~NSKsО"[ /z&:\*Z!.Tk kgkǽj ]c~xn RJtOl ّ[@sE1Njyh%) GCu#3t.EDc%XP83Q #B dJX5[H3#=s5ɘ3(2w{i@և329&R*.r=@POh+EH}\} ט3:,XՋ *t+FXrը "QWdG7Rӏ(*@g_-*Ibd6+8<ٓ#>B9DYF$xJ*ٔrQp@ I A@ iB  IǕR5V0b*[QUSt)(|ٗ~9Yy٘籖9 rdy'IdxIQ 9&;f ) `sUqcap! ! f%n Z c\uq H\q=ԐL`-j!,oY{|,\$2t\>rz\a~LJ'qeƂ!aq cFTu@"d,dGTsP\VdL u*A`1$qʢ\V˃ě,ʰ!Ĝ0 +ϵP@ Ap^I!04` P! 5@ Skl:YȀӠ[,";HNYK{oV$ ? , 2K "Ipq:`P̺ ֠S!2S *)jӛ&0AZљ+ 2Z70MNS*z<(< 0~`:P0r ,!`*P0 l{)&z $ W jQ 2ŀ P VH \Vz)u5C޳A ` P P&^⺠ %N*-lEa P㺀 >>{lo E @ 5߫ R>RnEP7 @䩡)TN<4( lf s^r~vxz|~>r舾>^阾~A>Qa@L\IZ@붾>ƞȾnna }y >vtE! ~  1" @>RAZ Nh71 װ5@ o36yf 7 `R0ff0 3@ @ `` 0GV fZQZyp h2 0  ` ` b%VP# @ _a"0 rp 1W'( 0 fQ0<"@ y` ` K=/2VP 3/)` " ` )by?ҟW!'b?ҁ%Wz?{ o `@ |4)B >tD)^#=~RH%MDRJ-]SL5męSN=}T(HEEPM>UTU^ŚUV,u}Ve͞EVZmݾX`śW^}\n FXbƍu y0َ:ƜYfΝ=YhҥAFZj֭ZZlڵm߶ 2n޽}|nG\r͝?Nry'ϣݽ wM8o޸p_|Sٕ~I@D9mdADxB /0CPC?1DCOD1EWdE_L~!Ql`1Gak1H!Dlh!I'OIqJ+W˥H$NY2L3-_R#Y<1!b]ڢ50e1dQ&$ds$Ib%RKk_VZ iJr%@͍MF;ZK=$>LţDN@eV_g%aIoP Zj%Tkבh͎&ZoA$Wf#:*$B>*U^} NJ1aNU6E[^>$V nJDeTܢa;zAK6ގBWV,:>*ŝhTbJ(W&N ŀekX|YY:3wQJَJ#*h].Ԙ$1Yde!b-%˖PGEKaP:px av8$e2;-~)QU,867ck<e^͵w6v=v^vYer=e$[:d^(%W [aq%=u^ѻؿ\&-k)g^ @`c*>}`MDZb~@Ow`[XπO?`E(Uď8`@6#a PB :CY?:P? h "![Aщ30Ռ'vы؂UֲU%ikV֠hG;5꥖ ՚'9G;9H*ٖllLf_%\ȏy>0:Gu,b.d Z>ä?輧iAl\##Y "`#_x17:;fBN6!>ĐQفAC'ZzZrF'95KdnI~wF0lSkBp*- /='$BGN{}>E}]?OƋ !$1k| $rnϭŬg1(="БIdI6I75qh G`v#ˮfS{78JpGu&2{Dw~w_D.wߋCa*8,y"O4>ӻӉi#=@?u? K(-A:ɿ 0k0q(qS~p)A6}0Bx'yh't(&2pA%sT{=hŽQkZZ>B3)~:C;$ qBϫH^ ě1 Ԙ>39mZ~LzDR2ny7*JĮ.4PL. !"[A852-E1Oб$LdDSE`FlT>}A.FX=gYp=@ITudz{,wP inK~8E,8y8h6 9x树jGwnjGHθG<1A6~GǜUs4l zsD DP .%@ܰӺIߣH5d*Pɬȟ . Dxt0~0K̊*@)gɌ Q@q@ .1P@ŋx˸2,z,KSKyKy826œYÊ1/DŤtm3zG}pD#+@/w HIHKMϢ$Nh:psε5׈A:,iwCʕ0,\ =$-iH4PZ>R th9 LɛC0Ql ЩRh@:TB 2A!= ܆"U }HQ,yD4Ҵ#`rrB #+0CxX)MVI^9J4= :8--TUS;M=U g@x0,F@DUM= aȯmp[~mGRNշ@@p؇@w8eUbX @<,lcy3cnJ@ﰨˮ/3$`=/k5(b(D oB}2.r DYqlK:US4,ot );Xi'P}W1؉+YMuؓER"mm xS*]`ٖ1ٞՑOۢ0tٯ٣MY%ٕu&1MИ}ڪERSw ϸQ2Z$180;5.( `Ъ+!7/lUTq -AM=4 U!ȦYPyP?5U =ԥu&E0]uߠ?@@y5IhVȝK.| C]`:xX4(I1; ]i^   a47 ZX.qȼ&Q4^>" Z Lpbx(dxbi >uZN@~q 2ݭn 6i=2,ZV9xC`wvonݩ`j5 p!q>C蜏0AgF `׊BT=xdr ΋'qӢkVEAi >:G^ q0E.[#W '*"YN./j+" (Mn5oph9GoZo Q&7^t/$yp߭aTn-D~Pd}p rI\tUxG~sA~~(~'G"0w!mA}olRp ` „ 2l!Ĉ'Rh"ƌ7r#Ȑ"G,i$ʔ*Wl?3 # 'РB-j(ҤJ2UsfMMRj*֬ZrhbA̓- L׸rҭk.޼zW6:H´?0rm1Ȓ'S,Q `M(2ТG.mDK0_gӮm6DU+̈́e)8ʗ3/ #4a#, Hh;Ǔ`f7<ӯ_Fz{V} 8 F &E ? B#$Zx!Jr@)"q)Ř#ByA}СcA 9$=8I,$cB"Hy%Yj ) $%eys BȘh&q:]ty'yR@!,WtH*\ȰÇ#JHŋ3jܨC =L+H Ȳ˗0cʜI͛3{Y ϟ@ Ja+5dTPTCDjjjѫXjʵׯ`ÊKVbφ4CR Kݻx1)߿YG#b 0 (M0YXX˘3kFsϠCT)SLE^ͺװcK۸sͻŕ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0l<2G7No#<ai:83'6A[|?OAH/LWmXg\w`-dmhlp@c8xl'?|S@z74MAxǝ߄M2ww?6;"u^NDӁt&HO*ŭ TMЇ>Oge8@w4u[f҇EjΕNHb%R˭`|,LSء?R]!2>{A-V#PFڢ*GJ%n?xE¨4 H<\!Y ,5^ TIc69ni!hˢeFwPIг 3m~c ה`f+CW5mC2)`8T@<$X4a]Ta[Į 618)5fR:[@a]a7+Nf;ЎMj[ζn{U3oH+lnVO펷blvޭ{WcLV7YnOY#NI-_s֔Ə>ߏY;M~yȣɸ{40|q'@8^jĕ G uGEnuXf[6N˛<?ꔱc|i;+Yh|<žx乆2ܮd0rpvq)jaVc_7d  @ps;'.cE`1 y|`@78. W=wW/4u9i) CC;ې H\rJΐ9,z/;G`ICۀTbQ"230?w#Ð2rIbx;`t 0)v w0xX1w7j󇁘03_0#P} S78weƐy7 f0E7c6d`Q#S =bHӉys23H: րE|,8K;((_Xrظ؍8Xx蘎긎$Ȏ|⎻(Hrbpȏ'#b ӳ yXqe!4xc4:u 7)1 5;0S(Ij;.!4YcB<>;9mRhsҐLR9TYVyXZ\? 3xg] &TseDe%PjHX rY%@ yY$  [ 9I :sb`y%L $QГ#LW 9$T H@@# apЗڐ #0 [P=u>M( >F Ѡl6M qaPs]%]? 8J؎ْؐ=ٔ]ٖ}٘MO}PȀ/#@֠Ye `.* Y  p ] }`Y@ ?N J) *0P Q]}m oys i*@C!q5# Vde0 `٭SL%p} 4 @0{:ߍ: `j q!`  Z JL, A`~:P@zO HQq"fn!@-s6cn Xlb:s m >@0Y1 TP z}ꨞꪾ>^SsP}xQ  AD7# `7 j!iʨ>P ~0 .(t*PC-* }P 0:-_~~ Nm? p x %~1ܥV3B0 /ЮVon Pa`E.[:0@ '  W:p:pp$* K 0 5زыI txzmN 0 _ N務m~]NEJ)0 u  hp )Ppp N0b@ fȐ(  $T.|s0 m/ I  u?__}t_cp1 ,aDPB >QD-^ĘQF=~"nx+ء,pө Ě/QFH 7Wy;H V6~ 1k `IWv)Yb0A 0ak^|X`… FXbƍ?\`/d$3(RNln!1byEFZj֭]|Mlr ukd@[u[v7tf<2Sn}YsY`._Vܹ *cp:mpeY3Ia]A+B 'B /0C 7C1k/q-h9H#[:^1GwG7g$ҙzc%]h\ \v%]xiu]te[je/2_$^x%0%]pr-{f?EP]s1H'RK/4SM7SO?}pL+9%L\~$0X|T[o5W]wŨ2_ ZbXh(i(aEXasa^L5UiToalUŅ6(dZZ_[\M]ow_t8`&`F8a>lq W'b/8c3%WWB6dWe_9fgy[k9gwg]ָhF:iN6MydW:멳k;l&lF;mf[m߆;nVӔ^IPekO9k>{{zS>F?g}߇?~wg*7+^` 4 \ 0 1. j E 4!B8BЅ-aOBz! qx^0=D -*$E }(#.шJ C'&q@*p)!8F2Q4\ * GqQ- -h!hBy  a8!46#$# IDҒc'Ihd YN*ґZ@cBG4R!|+UJH!l-Z/uGa2%0wYYӘ|f/ 9]@4c6Mn.tlAo#b j\H':2~ӟf@:PmQEhB"10"Qha P Q!&;:RT\L1 d`'G-0AH"0 WRըGEjR_DTb0Gp4Qx%&Q"+X:VլgMjXQ" D1ıt(z l`5b<VbkU)A^1pAj a5YFa+@`|⣩Q5mnuیַE2Q0 ED\-շmtN׺A3hG0D!(yE^[׽@:B[}" R xD#5FpܿO ķ~[5.awыa E(@~/qWb8N0kxqظWrl lx#bP G$BDMpGxp7|iH$*WnCqoyE>r'GyUrXBяud/yC 6<MCqmlO:gWGyB~__7)u#վko`q{ޑz|PCvq~|qUSwI2 |G(1!4xŠ, |18Mb)lR!`}TRK*P 8{W?hFqRD YJ(|+ַ8  @,l}' hX*!β Nk7YYۿЈ=^Hy%mۄ H090PSP)tZ]`@4;|Ԏ8RԈR8YHQ YR wh$s]][ȅwkh Ã1D1/<A$B4CDDTEdFtG<IĻJD"D PwQ|D 8CRtE!H]WE3@EhR09ahstDžsF hwFU$Ga7p zȂkT<1ȅ<7g$l4cBjCR:Ȏd6S$g\v4e P$E pȴP ɆɆ#P(6Mp4`{k4Ʉp-hAʃh0hJJ\ԈJوIj 58kIʬKIylTVE}o0LX L̈hLpLƔĜ::δLL(M| ĆcI "Dd \޼MMMN8΄(FFelH nyש q8U?VhKMEdmr/҆m-O=кxc_Ply<4HYeZ*}SU~Jb0S`}d4e[]qU]8'\i^;Ulf3:ng28p6祉؂7msvg!wgygp{猹m贃`~&hY8.ΑZHU`hhWh_9sI0܋H(?̈ ffH ̋8ޖֈ}>lݛDPY]0~B =eƻ"UxTkdfIrN lKT@Jtꃘ`}gx9|uP>~ 5i>(Ցpd^Bl`ȣFf(@Y؆ޕDP^F`SG>lp[TxlPME``c:x#FmSGcȳIA:F`W_Q EqwfƜnG*  M!7rx#/+W&&Ŕ qF(n' (YHX8qpdhm,G-rh `hx8 (s3WeYF,WBOim\uHNWr<_QGu6?+QmPuHh3rS/s,^%t#u{ǐE'AtH rlLFZ|?6w7rX&x7@L/bGXZ0x'?x:lȆM߈XP::i'@qxw_ __0X zou/kY{Ⱥ Pˆ [W8Rͺ֯2}4E }v>P8{ۀc_y Yֆ)&1tӇ])le :|,hp ql!Ĉ'Rh"Ɔle#Ȑ"G,i$ʔ*Wl%̘)ќ'M:wDI'РB-j(ҤJ)ӨRRj*֬ZriװS7-k,ڴjײf۸0ʭk.޼zナ/.l8h~$1Ȓ'oML2̚7s3ТG.m4J4زgӮc_7ȫּ3o9t)+:ڷs\/o|v ZLUk.ӯǐAPqղ}x mwt J8aT=DhD)P!!?wgL-t"1Q@-̌=#$C$M:$wPIbL@Z yPXB%Wa@@#q1H"yy'i" BA|*(9P4:)~F Z):*R!,WtH*\ȰÇ#JHŋ3j8 !D C|QFn}"$Xs#sɳϟ@ J(_5]ʴӧP:cC-2P 28V!NhӪ]˶۷pʝKBMTh'~ڋP[)0჌JL -̹3Eܜ@E%:4A˞MmoͻwAgԷȓ+_μ9ΣKNνËG[|ӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7{Ԛ*0Uc^;Î*w-mC6z?zf߭|߀.n'7G.+*::ODSry6PB_@@>qO ?٤3i84A=ms A-i麗?@O7w\ϐn#;mǫ>hɘ< R? tlqby!@lH?#P"ϳ7cؠo2xCk6`)TnBaF AlGSreJyJDn."R~yZh0kv[$@Xm#A6q@!mQq[ я2d\FE$dȐm!1M*S9!A?0)84 PKS6ŭW] +Q@ @(U"# 2딻^of&建:p%lyղSl";qhUee7Y`"C%Hcml$00*ע5LE77Y#؃\4TU:1o de`cݿm[)h) 3V21b0*t^WEX1&X,2ƺ[yTzCb,Β uؖee\Vd*+ t #,`Os#ևSL"Uh5)]+@ާ<&;PL*[Xβ.{`L29|y6@3 , _ w3Y 5idp:]D ~KG:`GLo:B @J?fRj'U*ĮI4vt5[~j\tʹ6Lilہ?+j5,6D=*#q'i';`^{vAqA47;9]S0ˠFb6aWOi4ApN| 5FN6AOGɠCoai~hMh3vr \z J4;BBxT#uqI$#אQ ҠiNpIU$ dYȰ $dXin&#m1,Umh%$xȇ8Xx؈8Xgv1_H&  Q $0_0$IlfPtEp5 #O($ΧuXPr x%GϘ$x@Xڸ؍8IpEQC9&'JHQ۠rqwx1b!|  z~Pw? 4p(P,R  D T% ,VjCU2$  `EQP"is'a= RT  CS0LP vc[0l ;PYwF[ 4t5Ep5Q4od W0n![hQ:G\~ QhၤIA A&iHSc؈e&jjJp$r:FRvJ$xB| $~>#Z:r#6ҨJ#22 #z.tA<:W!ZupAL*ꪒک:#z1"ګl #6Z*qձj?!Z"ں#ҭ"Zz*u ڮBT(z;j!QH p$C `JjU L`Is j ӱaO p k  =% 2$k]H*:U#a B@]Fy2jw0J*u5 PD`z?a 0  &{r;ذkzE0o! L1\ n[ +;R! ͠V$P 1Dද h0U1b%Vv@  0 õ `*Kh`nQEwHA1t y `  rm +p 0 d :Fk1+ ` [ cA @+ ;,R ˿s[ ov  0 { ; $ 0¯? $mѽa k о@y`898f > ?ALC\EC\,aaR1  X \|`a\$ ZOec9+ p rܦ*DR 0 O>W>>'i  ?@ʱ@  ` @ʩʦ/So@Z  0,o0ʪNq L| 0 ` tQ`0QBͲ( yδ i, ^,Nj6 ~ibnNm?+(. u+krR΀0n׀O$ zV AFn[j0p0\Q"$T0UQ 3 ^XInvD$k$ O^O ;P qY P A ʰ0 $aFyKk1֫ n.f )] n; V;i~Ki ;V@KqK; .%>mF~P @QA nS;țSGĠY dpN+4D P  a .Nwk0$ O)൴1  _$. aʋ @ {N k q ɵ {%$ ;m>PR?T_H`i Mo1@D67XO.@h/`a pa/Z_.ACmA(ӡa_ ps?|w|OaAkOqY_dOA@ c~DFOqmo7o`?ү+yќ ۏ/ = &ܻ UӜB.0 P\M PB &Dp^$("C=RQTE QʑJBQc̕ AҴ)ΕbFPEETRMWI#^ŚUV]~5o?b3imVۚi2{4]e+.~˖ذqقK5-_Ɯ) kUZhҥI?.ꫬMkmM\o G\r͝?]t9pQ]vݽ^xIG^zݿz|ǟ_&ʼ@\h>'вq&! O 3KNmO?Wdy ' y&!jnlK,p84JlDIx |HB(<|4*>3̯awc5s%s:)3ϋЀ$# @̈́QEe ?j͇ԳRKRM'ygNx'"⇟}Nǟ~N `Mg;kpJ /4Ig^yQpgr)pǙ`upN-b~Tn'r!=%EFjHfZ -P}wl)Zߕg9,2"E)I&5CyQGɑ*8lHe&,Y¦+BuٽmO@ (P "֠F7 %k.+y|i~[2]b't0.nݓO{%Ciz,乲=RN\G.d hǼ444O` (D}'wbv T|y~l^[:$kz|-FB|鿿j xЧSdxIlɧs 2/葇#R=`LUH IUm y_5pt 8 Aoq YЅ/5+Uֆ)TGDbhi'wn$2J?̈Uc"b\>4DP8G:6E2S8xc(RFX!fDN!0#YQ6𘸴-&iJ&Sq"'G(CJ(Y4fYMchڀK]rP{e0'Ĥ fZՕb6DG9MdYnB2IG4|H9N%GhR0Dk:٢lCc 9OYOggA6*ԡiC@GhF3 >8csQҧh^oJR!dFԦ7}[I:X>gk5ǐ]U?X 9h co㘇VֳAF:$ :U=(VHk~ֵb4^!ֶmnuxDt\VP\6׹.h ]VwE~@hUQĺߵ66/F^=uo|4D#L|_|FKa<`GB`78+E/򌙶pQP chąa p,Ccn˩D[tBp8B(;m`6-c nDrФ!V8U:%wY؂B=$y8\Jr piWm[ 釙S0iY!ar͢~M:Ʒ &|/cRn`"X DBaEz8Ȇ|&q?x hDdxҐ~#jpfƨE--6X2x2dqצi}zsemР$7sǝ3fw/co nn}۹7F3(~F^T/z vb;_`%cJ! BRp;BmO _?ϔX <ᅠOPegE?}jJ+huKWI a->dw M?#0ҿD@Hi@ZaZTѐ8`y@K9@ @S Oh5Je~p:46#t Ht8Hj $;  [PaH% t,, Q8U !;-Tå^k@X þ5?8cC7̓Lc{ +!kCTUBGTl6KDaYH+F룄#PtJE"yVE0z!Q{`b]`Fa4F=#ٷ@:8B@`#QP;<.s[/3px>NmnO?`O8ׯ{+>spg@_ֿ/_]&l# R0e2إX+~PA19 ă6q@΄.A] !KdX\< 7dE/0 bV:#U0cĬ oЉ@ACNEo ]H𦨒*ޥ wcHFB<aIhH@"X(g.C/LGlh$YkP24)-NEGI,SŇt$K2r%- F^*xx)\ ym J2xLf&]0B"Zۚ@1cH×E?*+kcن88T ưgq A h& ⚥ĩO D!X*KՕp&_M&S:$(L veQ'UXX =ȕcoVU_ؔ (9nPլ*8xh]f;6 %eIj:B6"dD:Hg"NueUH><:j2nLkZfZ Rڕ|?E9Z&MgNoV6[6;// lƯZF> d]2wUZԒ6=lmӄucg,9,a^C[hb1\bkEJk)'ܤQJsi|%ŪiE}Rk"]>!IȲr}Sxs>8uqӢ#:$yƛ-pJgS(,ikM9i'uyi)uZJf\Y nUjĚYfar]MbNf;ЎMj[?9.zqq 03K*f@. &}2f:=08'472" ᰃ43@]`C_ci|qi0EW9Y;0tOt+vd*/$ֆo*~bes_/x"@zhzB5Uw36;}\0AcQ:۷<0Fw[PBba^%Lc?&_i8] d'f#;oA`CӓE)^p8wu7[A،B8x Yk٨bqj 4*x9H+Gx39Yy Y!& ! ! ي i ёI)y,y! d~60`wDJ(bI}J!68lhbSGwהQĕ CwMؕ 6o` tGw_f xP`@@G q Vz{))#y٘V57x(5yI:bf'g I'si x:P( dy&1n0y&WAQU&T}`f QPY' `p`TZ)%D(=aE@9n"nayJz& jF:&;#>8zТ 0*:<ڣ>@B xC697^U^2:ș Md5R 9wQjU ACׇw0K|g c? 4i p6 ~7ZM`j=BU*) J'GaHם!)y(a pa eJTJ ppx r9; O ~! @ bɐ*( GZ%65 d_q*0+Yz cNu 8Ĩm%F 3[Q; cy/1h% cj{J `F-; 㬲pEW(@07  Ъf  P W!b8 vai}<A` 179jo ~#!np jsZ; PE8G q`% +d"qgOG kA[hcz%!.L@=975`+[ᰰVXM+,MBi&;hRk&蛾d+&`%[\r%Xҿk%T2,%|P $(d N s: |aN@!&\%,S0%2 +"B="QٽZE]"` A Fm ЖKooQW X"-R^~ ?^҅M] ҰԌs^MKJ q(n -|$ b]1 [>?+HT ڐ7"auTbB~ gnsZʟW> Ѡ HMPؾ q[]pp}kz1Q O$k!Rݩ) .d!@ վ o`P 4q p D@# 1H2ep}C4D Oz 0 0 q{f Y @ !P o ,fO ip0 Ѡ PqەoqQ R F.T5 P 19@v / aQQ Y/SA 05 ǽ$ԱqZ" D Fgl _'op{ S5 nbP []!>A}NAP.X 0m a p) ]fnl -a-%p{A z.A j= 2  0 Ao p݉ԥ0@ UTUJ=@Q(XdwĕPcCDy5Y\śW/ނ6X`Tbga3d-_ƜYfΝ=֬bХ: 9[ʕM[lڵm m!UnōG\9X.ܛo)CǞ]vݽ/MyKG^z_8X⃷v_~Wi~A"0AdAa!paH7C?!"(OD1EWd1! Zl1GwHt+|$H#Dr3l2J)[vlıJ/3L# BJ @C1dM7|1H|N;G̳O?!vR3PEeQ4ZuRK/ŔAL?5TG o5UUWS4UYghZYL'^SJW)XWbK["d}z]WR)EWM>ܗ EoCGXPۉ4Yx_WwIOVߞ>A,rx v_]_)]%NP"%E'4(ݨ>ܮN%BeQf։c53EOf"pDWyF!VqKk`f$V YpƋ.K%w 20d^|C"S:h?_q';Ž;wٝDtLqQG%M=v N^?׽wǎw'xل7~x%Z^(;yB~L)%QWGl `3!:v SBO~cd&5INvVq c E ď-m*Gt #ӏ7 @ikNY޸yr\_BLfҲtlf4Y˼pk <:K!N)nh^ <fjӟ h?9Eٸ"v9ΉS8!O< &5Eg֯±}Ї@}r7N)yT #`J dqmK}R%s GQ)*Xg[bF a IoH7ؑhc;d5G2m# L?J }U'(zUdʠJ[[*sKb:Jdd$#.AGFO}Cr$?t2]_d`WeږPz%H>щu"\y7~XPсes hFEDqԠ&bN. qHj>f&.H2 ~S=S _\h>LGLadQ.l®X vPd=mjO%nZvv_2xHo-5ɺ%)>,]J q1۲A5mN!LA>vĭl]1q,Mv%mB!IR Gf.~ R/ $jћCݦU d.]=4u)[=@<"21۶^k(cN]7}G7 W7 y{('C STS{39 ʼnNr./4_v(Ɣ7PH cs᭗RgqWG'^;y9 B.Z!!Bހ`˱?S@rOyHDD {'o1?epH0mߡ?ɞ/Уx9;jeIt9ӿT h R2(?hA:p>P03"j1 kc L@@PaCAAyA>౔ 9$ %.pBkyp;xz@11BC??y"ϰ@45dC& ` Rzax+`-܏>\ M"P1%7 BH]4It(SN̴?ۛO|P&SL0c'X*kE:?a) ʼny됍[j c qFFhB FsLp&z~X7tu341|@.8ϐ@-ӽcmHl'd Ȗ\h#aC颈0dH;D H(Tp+bGZA8~#X\:bʚ7ƔZ(* JJԌ ǨDɜ4lа{u(x`Ӌ\D %j$LCdGJ*4c0/h&kKP0d{#DA4h*4tG H b CJ0c(lh|V G:vLCS"k5Dp.p0C)N͘HÆ ͈& $`"6c3 Oͣ0k:'ꌹd &  #؄{b/*6xm(²|)~MXt9d  o48. {  }x bc02"Չ]M <`F3+R8%PD'3EӫQRt| (U3M<ӦїAi 6A%T@R0#8"-I- 5Qt8h7 UT+U5L ݘ `x=KU]Չ\\]>"c9(XHiuZC mdTVU@TL݉?CQgxSW?-D7(F=Sar\hR@W,VLSBؐ sRXc(p)pK Q,X{!B\Yp}, 2)ITzM ~P:`F<oŋ#i}x`p)> P]M؄yxZ %[ ¹N@ *[`*{JjWk69-7pȇ> ʶb݋& YU5_ u%<[Ұx7@~ C!rSHQqEڕ)L+B -] t=YU ͉6iYߴ )u۠R/dPyTޖ͉8;x W P*͋')-P(^ma3`} D\]^XS69ݿr0ߗ .mH މąaCx^"O!fbu'I}Eb-F!8I. /;\ 6bc (2~ 4aM]"XN)l\8g.9Fdy<ErxTCFd[F"DTDlᅌ2@>Q>gx+JcZ#dH ZdP |~ec`6CZd!~d8ei!6fZyDgEf>b WsFgmm`U_axR=f惎g.(S1cW]ih yf8S?]Vjz&lv biN` 8 XXߞyRf Ufꠈ#0t؉jFQ^i`voNf(Fe&騆Hf nH'QYxh~j{f-yރUYD2Pl?qnh#x^kjn-1Ƙ钾7}x.ugXˬ0~~fn0V^1cFXBfɮcvﵚBNo&o o6pа? _ Ű! xaFXTllpgqn* qh<(bUy>#Y'hXx/&n0qR!wrWH7Ѝp&"WHޗ8ŀY`qpm)s'EQHWX7_ݔSbE+Crg{ƘL`.X聛o\ZXi @iB4 G{zfBėM--׌PKX| Э>@eїxC@B@8& A'~@ / M`yhGPјBg !,bwH*\ȰÇ#JHŋ3jȱǏ CIɓ(S$˗0cʜI͛8s3Q= JѣH*]4(MJJիXjikׯ`ÊKY%ppʝKݻx˷߿TcMPÈ+^n(P Ƙ3k̹MɞCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G, 3??ȓǭLP$j+0,̔AC36N?;-D'r<\(\G A&0 d3y,u:N[u@n@Xt6@Ȕ<<#<,:⨝]TX,7[J^rc9t@`>:t3P2 @3ty* 2l3:;ѿ<>Alp7PS3<@)͓S)<6!03N2p 1T|b H2Uȡ ;3l@1)Z t CtLm4T&GYD XQXԢBp/6*9acьBx)9n F@Cq!A Ѻ>ʎ~2MdÐB@ a 7 hU M#`ok0( AY OHp (": B"4` = Đ(f`  00`xF aL v(Zˀ#p1 @э4T[ɀ 0 ;(Ơ"a 7 ۰~ P Вh`m(p ( b% 3 m$;T d醀#0aZ 8n Z0Qn g nn _V 0 o=@ n;n*,.024?6_8:QD-^ĘQF Vl_p)\p?mKG5męSN=}TPEyn[Qi׮M PTU^L/TUevVZmyriM`ªbhJk\} }'/Wdi)_Ɯh/b)IjF})]Nx/fXcȰ}>F7  Ӡ]x=kB&S arКykDPAX8ž6LD `D0#N"|!oZ*K0ó<0DQh>4栔d-Zr ƕD$RC$ajaϔ@9 'h Qi *0Yfơ& GP , :8p@;N%&ͻ1sOK_R%YIKG `BڢҌ uB0TI@ s\Y(V򀍅!ظسU(TBc,RyXlġ)\HUbaTYJ.\{5# 80MP^עdbL 0G,@_-◡Q7s o%(YH8ks!D!϶ŗoZѓ!JewDPA!ˆn_Uweq2͊كdǃ9 \|Qe1fmqe2k1ˋwm9z QPy%NvepH]pE< ^@oԘK.WFyţ%=^x}t{^=/&u=]ܗH&QX4/!s]Erfr'|'$"g!}RF(Z~8@Ѐm @6ЁI%8A Vp`5A L#Bp@sDa UB @ B `)7au°a8D"!4AJ17Ob:a< Jы_#Eb Ѕ! 5ac帐Dp Q,яӒ&#ʰM,5HFzNphd&SFR!]d(E9Qp!)UJV![9KZ:-uK^JƓI, KbӘ@yeLf6'$JgVӚ\ c<.'f8He &hXƹNv>.1J>najg>9! uӠ a$7 )^&t"Ԣ`?Ga$8Ho0'EѨe$pe`JRSO,-V5kB85iO -H?2l0ժWOTC[K*V:V9Q\aqխoVJ͈#lK\>1LC!dEy״*e1VֲYi_ IrJYJmAdQLЏ>, "+t:җ+cLw4.liOzFf-]NJen)VV)k}k^4sq;vۉj&ՂlVv95%aLX!vccyFw-Q &گMwm9zw+Q=w#;Vfe?/=_7܄2p݄d5m;wfAW\^UG#l#_y̧bO7yus7> GNtg$mWPZQŤ3>+sgz˼ʏGz1`Z/rԙ!?h<rpb)vn̴Vpj 0|B6qʚ\bZCx9ϐh4g<ؿU;!dQΣ8Zfl!_}CYb8?n 8#MiBzdpD0iwcCs?7? k0[|Eh9)cYH> c jL#*7T6RkH,2%ˆx@1Mh<"I,A\ (<xAB$$CЬ>;xRj;0 2Q:(JMdK6VE2HXEl4W|߈EJZ$HEC{,äjL=s ;xb\X'b6- hGFÈK'#GldlFs z!#M)q1F.Hְ†PچS Ȼxl7pĊF,ȃ~zY[L6qujppmïq"TmІtx Ȇ6D{ɢ0H4*fkFi{q) 쬄BX " 4t؇gXk:˲@G:%cKDeclxkH9s'@ӽh(Ox(N(h8='~L Ʌx6 &T)2:hJt؆O:3X7Q=Q 'ȑ2耽L `XX2S~PzI# !dFq0I :-ˆAX)PPB\wzYA pNpNpJP Uh TdPqkʃw`pT8(1!AH|RHrу>CEPx! Ue1+wU,_U k~))i I~W@P *iym؇yd’¾"wՅ8He\̐+ 뺵[4A6ە!7%ܛ+cr;\-9_xY܆܊3P5cF1̥8:NJ@Va\uĉ&Q5]-QHa&ňV\Tȫ8nbPH5۔ QAy+5̐YX^.[ў Uil@_e'%Va)ЈqHٚp-lGx[=3!Fj؉Zj3~hтplEk< KT%P܌ 93Yd2 oI) vAmeU8x_pWh4rV@U8q ZY} /fw@_Y'߈Y:Qc Yb(WP a=$fPXɪѬXY@pՆ[ `2` .66NiІpፈy(ϒDžIU &hgZh&&VAgdh6qh%>p&iذ_&۾۱fJ:i"jjNƻbWynhchFkVk/!,OwH*\ȰÇ#JHŋ3jȱǏ CIɓ(StHQ+cʜI͛8sɳϟ@ JѣH*]ʴS.L҄իXjʵׯ;5KٳhӪ]&BQ5YbKݻxQQpLÈ+^̸ǐ#ɖ˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ?O?"J@%0,sr35Mܼ<MD' T@3l{(p@I/(?(#D<3ZZ2A锽8ψC2N8٠ n[:Ho2?'@rMcM4C; @;Nh8S C0SӇA|k.}JAdz4CtNP)0#QO<7&ӔL>1z8W}ǔ+y̒@ 1PDoL2dK{L䱲qc5ۇ!6m z jch0z3 E%D OBJ.H2:XÐ` ?XD@I l"JB)>zpB(HhEH b 0uQ6wƭ FӪFEu 1@QfyxD}QDRAFFrP3?']P(Gs$[*HiJ|qD ANd:ĂOeCl{0(2nv$BBsMm (BQ8ЮI':SP(Hιt#;@ <Ó̧>!3!3O68% % (3 &pn؆5JъZͨF7юz HGJҒ(MJ sÕfilWsFSb-ipz6V9)L.FMR3BϥFkS NID[SzrIU=aLjZdMZֶp\JUGv뎴9kBWi#HA%6L<ĔH, Y CB7vC êacvd:b%"d ;,n=T9a !?l9\ kn!^`tcJnWFxKMz|Kͯ~LNTpG ~ Դipzaeoҭްݻy7s#yųp}7-ǻ7~36~n".qPⶹ8i˦ C(OWNJ$gkH g-;*9ӗHOҗ=]#=&2!+F2&g0}xL}1. lL#9φ!w]4.ggw B(96`uLcjahX 5F w'PYƝQb6 W ^ĜQ12_hH1_$FPj@@X_ ݘ: /#6i,cwe|x}#` a  eYQ w\ .!1 {0CF@40{Y؇X GgG TP j`Px a:x[+ 0 P @, +YNQ26C&FC5@%sG2q C<.B:Y )&s :Xl2@! 8rz .G i@x,dLu`66c5R3, `\H F]U{<<-F p $? p! ãw DxL^u+Fu :`qeI1 P쵒"/uj`aI3@ A^x` G2 P 0eq 1 i9p0&:`49 )  @ tK/WQ ` Цp|~npq0}z  A("p)BA}pt!eV X- ,Ў|Mmuu}`!FPBi@ C -Ȋ²( ,e 4Q,А؜p=p?h MNa!! * 9 ZI EMu0YP 9 geУÓaLi;!khp6ջ  @ , q( Ѱ Ru `Jh  4kq nf?! }(p `1 3!3H I Z. fJڤ`)Wڥ`~:p y*! j d` tNY( c."#Z  }.""@ #T#^X1<^ꦮSC?@~C@M[N0O>.`󶦺` )л .M lR.mN*L.G-@%O!ΎVF!촉@Ք: *A>%H6&-`R3J1 ;oJC/=_KK?~_A)1 Z\O0!1hj'JKR?0Ĉ b}/04JO &a,> qa9AwGX #<"+{f?P˟O$q!֯'Qۿ؏B6TU?_@@ DPB >QD-^ĘQF=~RH%MDRJ-]SL5męSN=}TPEETRM>UTU^ŚUV]~VXe͞EVZmݾW\uśW^}X`… FXbƍ?Ydʕ-_ƜYfΝ=ZhҥMFZj֭][l'-@ڽ\ #f'q͝t-YfnJvǟQ"o(9!󜆝 j- S\ނHIz("@%nfy)t'^C$9PnŞ PbhB:Qb=eGQvt(hHEԤe@zRgb7)ZBY)N Sth yLI֌BDA0/AejVOĈ!#R:bqЊqխB*X MߺW,'ۓ00mkb/ Z"Fc:*ֲ',c1M U2 Y`Q;g01+HO/;[H!HEBA ƽY6".H30boS  `0!Xۂ'!ȁT^g/_4xP@8q}pSJ@`O?4Z#_N>`P#?d7DA @@c :4H-(qQQ(*Q~P}(H8( c#1xc!?"B#4LNz25 9%̤&7Nz (GIRL*WVڑ?1c ^e-+qb>L1] xb1tl^?M0#db5nz 8IrL:v*R Wq AJpeqTPS %rP694iI@)d!ʔG5"`<{M"@KŤzNTJժZXͪVծz` XJֲhMZֶp\J׺;Y]WA}u_X@Ma9dVH5S QiIƒ40*FD}bF2eD5rHJ%Cc%&qk P`u 7ARqwj"&c<<%b/W;0.:$ 줥@[$ w^ͯ~LN;'L [ΰ7{ GL(NW0gL8αw@L"HN&;PL*[Xβ.{`n8˿#VRdH7߆|Zs4aKQ^B">YcU>Wٌ`Güi$#cK4QIF6lhx4cߨI]dlyG"Wz ',`CtIqN$:ݸ ,0 +Q8uPsٔkS9@LX )B+K!IbI3FHw݂f (@SbU${ @LpN0CAb FQkn֠y0 p`LQ@ 8Agas@ʰlk`.Y]% )*!"Y,$82Pw,x1(C b?g&g|#ll-c )@|YFMA  6bP!X |o{X zB a1nM601 g 6"n 1Bq8 oD#5}f]DZ7'B! '@8apI2~_vfxch}X?P0' gz.  0 ;4U'qyP[4q3aq@4 gx@|',XOI xG rRP&7`p\Pgr??C G &Q a qƄ SslSpâ D `t>p 7) A'`A xog (t`{Px0 PnPrU W4(,ӰF!Xq/ A'Z ~6i#x4=% %޷xK< ͐ qK (X0 gQ~=WWrpKP f,p Ͷ z   uDz 2 hhm 1ِDe B` aAᓺ  ,} &% Bb9c$qŠ8cwnawY"y@`9m֘9Yy2 oCQDᙚy= 0 @ w ! Lpq"S TK%QI tDQs 4N I CypLg$T ZG)oџ n V!pqeESP q y}4`*?=Q}IH8C <0 ? &0As j  @ !tp -٣Eg *_ZT`oA _|A Vn`* ahs|ڧwQ@ڨ*UY1QZ"D]0i *3RêF5  i_cါ]7! rڪ@ Pk zJː 8g KMJ' qC ! PҫN (^ ŀ!}sS yy{+{Ю# @ F `x(L"Q z>fS_u BBF 3= / Cq,@I8l0A|aq"nh;q'H lP  p"i ` e|S pȳ0 t)~y"o)Ðvq R ` ., w n)^hހHP "Ns aٺ ؐ1Z- \+ Bȹ*B- `! wpX'nF{:~%gwA8x*2C9ځ©9ZhG ".ZP DllȽZp@w qN=vY"N{&6+sgPk P !Y:9J: рJIdz o ` K  X 8"DLSi4QP$ s#G')Ş#ʮʰˡBƲDD`I˿|@F ̥ NG@bA4E. oL.Pc.;ib \ll LAa<?a.`- ]@@ІP !L)  &/--0i[5 @1=@ aGѧ;5 AA7q_qHH6Q@\-a=ZZXl]nFAJ:A#2g4pI|-S+ <=Ռ~S {1aI7a $dL֞.7֥}0! ͣګ=1lA d= `\6Jm=p|2V&1]opFC]TݭoRm L=]}>^~pDr@9 {S ~Bb5}ܰɰע}; Hk f{3oM/$q-0 >.Ϥ1@Lu-M~.-nX,xԒ1"?0kF`AMH%)zh>+j>2VPrx箢}k.a   TbGN|$/lHl>%=إX {}p@`=^R :531ӵaH%Ǻ$Qc4C0C`0AUI At%6 G0 +36j@7T/ 'KQJ~sN4 &P[~P~v 1Bs] O/&oqS4[43PmIH'p'p_~^s' H73LQao Z:Gq"+'@L pysnh/f*q@ɼ\EXRke2#UH-]SL5męSN=}TP?~btR `)V.]D^ŚUV]_q 8d B ɬ"xb@E,0Wƍ?Yd@"EJb]=ZLygM߼y%3:|.bE~2 l<~mԧm6lQ +oߙJƞ]v+_^:fR;{G^nXtzo:mlチ7dA2'B B 7C3p!4DOD1EWdExȨgFoBXvɑ2{2H!$HT#dI'2J)J+2K-K/f0D3M5|Rd3N9礳I!a|飢4PA%*RR vAB4RI'ρr3`Be OOJG%TS{{?|'|G?}g}߇X@4~Oyp\8@aD`z@\$Ё{@VЂv /AI`E8BЄ'Da U(?Ѕ/TQ@!,LmH*\ȰÇ#JHŋ3jȱǏ CIɓ(SHj˗0cʜI͛8sɳOIѣH*]ʴӧ3G d`ԫXjʵׯ`oJ%h5ٳhӪ]˶[Aƭiۻx˷߉sjKÈ+>j ԤL#KLe"hϠCM:ft^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0?4@8+<;N:@KENL7}6ӺnLX#,P>Q?t>ጳ׭3œ8V8c7ɸ`BK:5^|┆3*=s5xP ܘk[#vv-P2*譻>E=貓@b{$|:9#:~@`J 2DO8ȍ*D{O<C3ヌ7Iߏ>Pw#K  x4#K(1U} 0@b 'b h(6!R$  8o/!&S8`:J⇙ @V y pyHe2z]=%P آGr㣺 Kcbè@1Q[9 ")ʼnip P|$=$$d>wrPbr5rO "9 +d~ @JЂMBІ:D'JъZͨF7юz2A;@Jr #$^`J||LitJpMDHC";3wqCYA7y"D)`Щ*rqXM8nx!{pPj"j SBUzuʢ `e]Kר] 68b$Cy3:XAkdL-B$iC %GZ5\^ Flnw pKMr:ЍtKZͮvzΜW(5<{7:PQ^nCEo}Cb(G]s"YdØuKॐxk@ x$9@K,^ ~/d" 0M>Vd$F 팒 ?X*~c +%rc/Q 3N~\R'Y\` (Qk3噜@dY- v .6@ d@d@1$O1Y|YHe@ T!Gbh@s?HJ$ sXH8!Fb:?!'f odg hܲX$@ ȐzAa `@Rb A e aCzZsV.W%p'd`  W ~1 ӐN~P 1 q~G,Ƿ7'~  <#00 q c/5u\@1 9 P u=7  qp p~r 0&cwepRVe ч @Au|<0f>$@ ! TC p a7  | чL` \n@KFR(-8T2! ޠbф}?xn@a чs 'u tp8 ]Qy `[qX yh(IS`Ðz\0 ր= 'R;AM ! H)8uG{5 4 t N@\3(a  p ['0PHpVu /( 'XH Q (6 H, >f| 1 &We0z # Pq8p c1T pB}90ȇ?8}&@9@  lP |,[Xa H! 0`  I D 8 °p:wSMx uq ؓ@ i@ 谀pѠ $ ` g@pؓ 0{q4 1 Q 0 ! a p p93w7v ` ` Eih a:ǘ c g8 Y y ! k*N*DZj)d90j5j!|7G11 lt %QMR6t_A@ J!0: Țʊ1RjZzؚںڭ1JG @8A ML`A! : qHA1qVbǯJ:X9hG +@N v]PPopE ԰b p aP[ eY Ґ t &&7 ia%C[O  !4cgo  Ő@"^ OYV` S ` a \#`( 9 gH se.ͤq4 ٘ Vy#,)  D݀r!sz,Ee'88#8Ḃj0q0I< аQ DKp < ѓ<n 1r y1z8) r I 㧰3E ,[~i@I ر/ Ia|(Г 4LZyܙax1 @0˯e.BL <4dJ3 #Z  `,rYxh UXP Ў#s,r0t0z.Wz(`EL,#c 8 @[, ל{a T@Q=x }~"!I{}l :ћU0#j P ? mJ^] E# ?"e 鬣6i 0 Q4"8Elg0 MZ }R 1Oڍ lڤ @}"R*` >-xm 4 } NF)K:G #!,QOh1?!8K0J9.?FHC?  5;1?.|@TN?pYP %8O> 3n:?F @ ʨfD r-`` ݸ k!-P, Rxe Pc^Tkup^^觾\q.=j~rNHe0.žVvǎDFۘ6ZcM#` ` Ӟ5 P9 י]6FЏ1A8 $ `- A+d"$R%Tq~I0c,/.Oft5L57O63*  1CY$w Psr@Y/,1 ]`T7փ!uv7+68 JAo r| `H t'8i=[Y7?.?@3D 6}6x<o H 3v|H $TNf ATZcWN/6ߐT^6LTac QQd^a OQD-^ĘQF=~RH%MDdaTSL5męSN=}(+PEETRM>UTU^ŚUVzqVXe͞EqW]KW\uX`… uXbƍ?<0qdʕ-_ƜYfΝ=ZhҥMFZj֭][lڵmƝ[n޽}\pōG\r͝?]tխ_Ǟ]vݽ^x͟G^zRӵ_TiC~F(F#,t駴x#b$/L. SѶ'H*ƚ ODtd`pōdAlJE0؅Cg ka{$patD I GP!S4~t2i @ yRλIC!3S'OC' emO|LXSNGAQNjѠbV5V M 5W]C w6Xu ae7 ,R0lP,S($tDxۦUݝa!! \*YSG5 U?+8}G}p(x >0JV8b&^q2JxHM6^,h89'Oi؆GE>a&kB ZpW C0v 8SFhV~FH"~κ gJqnlwz]9퉶~NGlԇ@THC\_4xYԁD!%s#^|TNp u>!3VWq|]һQy7=ȃ|-/)@T<J/f/sfT*Dq Lmɰ&`^ ീ"ҏ0ARP7<#{A"( 8l%TNH%.p'Or?pG01)a$U0#hXpbI $"M۬ư 8;x@#! ˆOa lċ HZ Bַ♒(޵<  XN*IBR2pE b!@p/A @8W}E+;1a+eVQ$B A81Fg$GDqA @Ӈ @&l*iuDOҍP 4 @v(^` I\9B#b,!) H<qTWQ3ڝ>K݉׫qyAu@$FQj80:ψ̓ BXjQYP@ blj8ξD$⡏G$"~<;?>E 5*8,H?g,ـeFqV $iac1 ĢmNte>%oS q⩁0H {+dO3XN=ysrL$BAXW)njoDZb*KBw %pxz@:EI*AL+`` 'FqL*و3/I\c#glu㇈Ix j%͎qFͫJh ߁`Ad xF3 "I^Yg RdnpTu uPd 259S *'d P@á!й8)3D|Y?a Hs릪 ȫEN}xC`9[$Y@ 6|I]|k!q}S^spc=˦S ""kH6NmEvdţ5+ԑz>ph7!M6-7p|$'Cȋx'9w7T}JG}Ϳ~O>T \!,NH*\ȰÇ#JHŋ3jJGUp(Q$U I2(QH ͛8sɳϟ@ JѣH*$-Q\YSԚ$E\)0GKÊKٳhӪ]˶[0;ѢR 'DV LÈ+^#X H%Ø5w ӨS^ͺ룟Fw U0Ki |@@ʃyH`#\3УKND-*wC = Ј7xjwi˟Ozu<&'QiYSGn &o%߅fvH`&"X Ԓ*DaL-WQ<@ eQ#& A"br^PF)%FMi7.]#[#5*D"dihZOwŲWp)tΙIx|޷f*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmh06pw*NmdN83w܀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o觯/os W: 3`8:s :CH(@# 98x#6 t<^xncG v@B-涱BCG3('B1t05" c0πex?G6p, 9x̣> IBL"F:򑐌$'IJZ$ظ&6LlLB!Jp2"e)rJ2!\H+JҔ?P#L8K eA)LbR$af49hDפ&ŲAeěe7Lo9'DFr2,:N g>q&?N,'(;w;UcnMvan8MՅ 1Dnm mw]6R_μ2-):H]9Sj Q6qhblo*g߬u;}3~:įv4{gH*QsǬ)@n&wj~NO;˖{1{ kS&GSb1%Sϼan9r󁭃y@4aVln_T}|h(MdEH7k?9dz>1 ()uЊÐ1'Rp& f(J0s F 0bİ>^-0[0 = q (@X'(0epqAqPQ'AU ( U 9 ! 1& '{@!LFP ېD& #Y'` D iU_A5&p fs?y%8dGvNY ^'7Cl8i2Gg]Ib)fb'\ 1|zڕxk&ڰzA`'yY&[Yr٘9Yy)29Z ˀ ZEr)К95GBIٛ9YyșʹٜiWY@u0/13X /H; Q lhPycxU@3L@ ;ˀ oa ~&9(8PPn Ӆ`IX/p " Q1 IȚN  6Q@)) ÏV苳c  c Ѱ 1y 9t @ a hJ 0P  ` i Y8[J;p :xrB #5p'Q 0 Pzdⓐa#/F0Z:@ZJ :o``f`Aҭ0VSS 9b`:\a\ʯ(گaЯ0~ާ[ a[G]K۱ ' pP&#I W #AANP*[AAN$A"j;KYA Q%X1RkTP0@ LPU{A"J_2l)?[ x nP @y\k0ԡr@I00+@qI cRU`Iq" k@n QZ&7 4;6Q!A9@FAN` 1 5 IPR|D"< ^л5RJ6۩wBu*y@ %FPyV Pi AQQ 4P*{4;,2Ro"(+?3)\2< R%)“P U2 aj¢Ps'1Li `лTHۛ K<“ ~U|VXZ\^`M< SM-ӻ-1 *c `nM} 4P gcsL[}\D5s؆A3m[ kӹZsUJ=KmيQ ٝ͡kكذP} `SU|-= =A5[T ΍ ]͹`^s܋ 0= |л٨6M݂1 }=lе @ 8&hMsep70 ѧ-ӹPߺ Y4tx 10`$ѸA1- |Vpڲ@G33 !> p ( * Q ~ >8 ` j:^4Š=b! 6Qg`g0 p4~.%l0 @7MnA0x]~ѐ3^: !~`>:7  Kn i z|`n 8 e СN` P .3 ϸu ~p = |&JsUp P p Щӭ3 i u* N! P ` x&~n8- :L0 1 À : :0 ٨:N88<Ч8r#R?RV!X\=]^/5Nd_qDͨlnеsny?nq?Ou/?OMɃ/o/MOu_͐?|2?p-N"7TO"#,E.vqC2&p4(Fg#b" 5ьDOv8HBFp9MR#O0E%D% *-~Ha~ (,8't4cge.G B B"DK|B=It9M\p#R'$\2 e75镰җ)T9'cO,kgZÛ1G?NRd HrִV3DtCmleK,y IK!&EJAR\%quYA3 TH6a"|*"DkK!TP?8T- rvKHOtǛ8™\Dђq`7|/ 悀aB~_엃eKΡep) Ja8ľ8z.Npo/ bܖƅfs/ ؇.v.tec#sNmG%GY$9X7D$3rH\ @H -nk` !,VчPs_Z#. !( 0G8e2Y@[ 6U@._wG=I]knZ,A/u^Ė-O^!A=m(;!UBjw7Fsm{ܶwč+m7Mͫrޛtf{K:C٬Mˑ|/+|*B#rIB&w! CNYUXZ)()B2Ky0"arF*qArb;_qr%e6D?10X0NNXB}q7";E+zYi%RZ# 'ZKdPjHTl\*ЊE\ﺔ5blc=];"rɊr!pIQ<0؆()z l3#!×r <1zU}F7xLQ4|>."@R yP#Z 7vG!?BpW/>(5; O (iiHu %BDkVl؆pЩ 3 VH_H>phW@S8>08>h][kcu[,مts\;#`Sp@ \hCSbHԻ;"(B+BP- ;lR@0BT2<N@yx5pPqhR;yÕ?pXp؇Ly2aH#H2L6T;)DBEPj3Y+b<@h3_C4%H’@W@`C?,FP \%o-x\S10=(,&>H{< r$SLsyX~~빴ДpЇ]Hoi H1o`O{D($EZbTH9Ȋ u(2Ec* $9BWY8X/y 5 $W9JX˦\I8d8. ц g̃ ('m $ ` pqҀAx0?bx8̡w@ H"HL&:PH*ZXt6<.zCƟptčA@:'v,c.iꀠVBŐCA?.򑐌$'IJZ̤&7Nz (GIR򔨤?(12S@+_Iv!dfIKJ#Q>^:jmXT1]Zfaゎ\+oYBhJQ@5pNfiBRbg;3T0U~&< cO=̓qgh.rzhqρTRh X(0 '=)BaCR SMU2 (Ԟ2MIQcH>S8v5ulA 1(@Zrdw**0]UbxCpVcf< [0E[1V@&׉UP &aರ;lAbX?2B{T3K~V  1pL((jZ `S8R a=r:ЍtKZͮvzׅ.+Mzw_}/$+|ѷz+n}?a G+3$|>a y>d PDuc)Y~ZLfR D_6pL:9`}sgb}V].A ᪅mC4:1;Z=L 6!ZԦ(,9s OvEЌ@ @@{* (A XE " lf @ 0UMQH۬GTrZl`c H*PLNO;#Tuz@&$L%R%%uRlL )*fDoa 0?)(`@פ V :)$JT3@+A)Jamu)Vw,1wtCVxFy؍TP -w "ܷDvhc> b7P}`#Ϧ$"7Ȁ9_}DҪOXW}]RMuwOOGOC/ɭ{;D?/(`OϿ8Xx xCj7 Wyg< Y Q BGe XHs0 aw{ ?qPi|Lf ;&P:0?6hyyդ;jb`!p ; S}wa0tep;;[7U*vsa h!#~U8 0%@lWB O` `hŠ 68w@y&C;0 foЄ3  A0H;pp ̈ a P Ug  8H8@= U [SD>gf8=?x9P5pP ?"R"'/+0a yco0Wt#)a$y%*ْ)(, 3).i4y5:ٓ//a`1 DiSP=a\aДRIaNYZy[\^X9]I`ibYjyklVIJD @  yΣb Xyb9$(-LAB {!@! 0  )ݖ. pmQ r@ &N0n)"6T`t՚P IV@&e`IJQ'Z! $`& 0"_794`kQ L0waϩ "IY{'kp0app} A F5Q`A Z!04 p8:*wkF3 vw( @ 񸤘QPu$K $6Y:( !6c! ˡ !.*Q XXZ4:Ѧ!>l * 3h4{~'$ "dz5HjTڪ:ZzګZgꦆҥ!/ӧz} /6*btZ%: :Zz暮躮ڮ:꭬B@ *c3 i 6  |Ҏ]  {S J 0-J(- k|3%'W-` ȱG 'CPD4P P +~ю#` NjAkPNW=K[ 6G|C~k~T˴Xk `Dz @ @ˍtNPoy~HKT{G8~bKPs۸1CгRqю붒;K QSz PkC;WQ[ ԍ[Ѷ` A b8+;;{5y Q{;k01ߛ5+`C`[ P6vP;[eŋy۹?+1 ,!,[;h;Kێ[ ;;ێ_  ;5v@t֠aY0 ]H(84L58QM`w uAaA 1 )R[Qi,;#  ՄX +<}wOh 5<j 4^~5 G =KyVl VXa Vѽ!N07 \C% F}J3+KkTO@Ev)4?;4J ?NCp b.40 ԠBD8 DЀB#r Ё<ru5CФ p;ӎɸ AG'h80IC_0C Uz4d D50(+ 7tAw4 55ۀ sC3Ҏrn S$DEx4^@A :M܄5$N][7DqU#o0C } 8DGp ?i0i00mLPj7A@PXj{}6W[GE! ݮJ a r(?7z5ʴƴTdvᶉ5FгOz_{A'B|8N 1@<`\ 1\Y_ 0.QX/] DPJ>QD-^ĘQF=~RH%MDRJ-]G` 1."TPEETRM>"@~!bpHB38fUZmݾW\uK A!Y`۶94bƍ?Yd'LpYZy*-AB> Uh֭][a>ܽ}<ֽH pq;\tAxUj0e~dAh2"c'ked0DA&-ZE_ ;h:wp 3FFGD2ɸ$̊ 1Ѡft8QH%\-E* UrK3D3 3wd0}LR '! OA=ĐM&38 c|NBLOGSOc'p! SUWeU3% 8(EWa']V&1o 2%XeeY$4Bh(,02[o}  #,c\y祷ޔ\:-I{&`#7.>Ţ?Ġl8cMxaj!C"79(y6fe]J8@e&ȘBÒ1R@@h:kXF5R~R Zkf8^ZdYlnv Zt;pMoC~ig vdwr/o ss?(0"Z=uW/I(yYv)t辽wwRdxkOaI7}㟇^؝>{N>!#d 'Oޠ|/|O{?/_H]`ȑz>)%84S`5h ^;H( Є Җ Sі٪f5mч "'"|R#/H CÆ #Abp Bp%VEΦQ8KF8fэbEj#cLH r4dmHd{Չx"8.v8cp!'2mYUqL8rֈ?a 'N` ?Y`K@Hy`UDT;39?)&d< m A4±ď0L7u 0& 4PJF?GCfӠfcL@b qE.i8 `F@C?~`EܡD`b wPiFDR(BVaGp"$"`qW5gFTM+zVP Eq|+'U2_ÞX BV>bkdtagF%<o: %W#Bnc v $ x1.%dۖk#!VPH!̓5$AZUɸ ;RB;޷8W!TH7psur,W"hoѧB&nY Y)\ (EA"$̌\L " `$ pc[IYm`)yeV8ғЉ(6 PW\L?$ū ^<1mx Պ$f;bXRghZ/bqp<ِm歀;:^dVcfؼ.} ]΀cmz`hb! pÀN3$Z$V@qEx!Dyx aB6Jj\w*1+D ~]`ayM% X<6?{=0s@xJcR;a<p,W0gLvquOIړiqaIӁ$}[A)d.LdTl^p9 U. `$$7d#̄yM g(p2x J6!4w qہ@hx5yq$z@8QHW:.!H-Hj_h#FA Z.N :`@I b$Pxdye &h%c^cQ(1%A䡎31u!yt [ȁ -IHP.&<8jdyN[ϸ7{ GN E&'HcYp8?C0|HOҗ;PԧN[XϺַ{%)&[J1E-jc7H&0n@a$1jP 6[I$~臨W daЗ Rb )pj5,K !a8g+l oB|$J1RQ9,> L¤s  Je*2TF/@Id G%/nQ۪kCP,aB zs\! ` G }/1 w C v0 0p . .h 0P*?f 0` )/@ŀ Ex0`!z= \nDŽc!Px R3tKa]_d8fKeg(j؆likt8vuw,BK`8\a0a 8舔؈8艃(\ '"+2EXpx P Px1< 1 x- NN0Gx 1 } <L`1  ޘl LT_+A~(k1eU@gQ`ȏ&RwIA { YW!HRPАk U ,9$aZQ?PIz,H CIZw'r@@T- PZrIPrKz1P R ru*1I0`8kX*@>ǜAC@V*IrDǨa1ʧ~*J#q@CP`کqC(6*ZU*@LV:|z#` r9ZzU0M:JV ˺يZ@<-qڮqCDx6Z:J00 B Ф9SZJj+# <#jary<n:@Fp᫵ z~3/ۧj(;1Ҡ .~:ڱ@k S p ⧐ :kHG07iL肥l_`[*CJ8x A Đ5,G~Jmat%10{m1]) t+`B`Kb**gn^1]~ 1 K J%[A B9[* ytŐkGmh ]j :w0 g. +N{0jp x0 + pX rޫw Qr @1 v! VW|)\6 Wrz Q3 `Qr0\6Z0B&\ ƀP{ۀ6  @i@ .yU {Vd  \dK8WWr8 p O:0` .380 ((ɓӥz,*.eHaʮlb+E˶B˼<\|Ȝʼ<\|؜ڼ<%0l<)@9/( :^m! s dCm0 m=p oi0$.t$$]&}(*,.0.SpHmҌIZJz8B0@ 1=&ʹ Fp l3v'D0! X,kD8͟wQ}BQ @ X\Cp\,01 onj=5@@0@e 22wFCˀ10| qL?=#Ȍ-F@H0.0ǵ /t~<= FK*md0n|( 7$=< `^ v5%gKFAIA6{m$=Bx $ ۀt'kH `ذE5;I4o pçY* }NdV%` g]-:KL%OvP?r ]$!`"%?$nᇐ>Z"1236Pј6w!g& $Aq.J Ql=V ;z P NΌJ l!@=3lTjB~P,"nIF`,pLʩ~6;n^ pԊ4$ 0 ^ Tp]l ~K>ݴ鞤_4 Nݫr|dE4^Fn䜫*0ox+/{bZz !?r.*J$ xmH(qJ?x=qCx-Mmq!G%m W4OR<hqj_n4D AX`wo6opԮ=/ ocQ@&Q<=lmk6Ia~ȧQD-^ĘQF=~RH%MDRJ")1eb6 @(#6X00YwTC#6b ذV]~VXe͞E:gTz` M,7(t`KI2iRYdʕ-_ƜYf'!N,k7XӨN*t n޽}\"3h*=8lW&Dp*jzBoW-nzݿ_>1%i C! :'Jm> /0C 7PX2fbUTYf8AN7i9"<? -=:2H!$9"xZ0p" 2$Xi $L3D3M^0ê*}s?yOf*pRڦɃoNTOMI'RKC%Uj7hP;N;hG͂K_5VYgʟ~@?ѮKVi6Xa%z=v_eYgVY6[m"jH%\sϥj}]w߅7 /\ Ǖ7_}7zj~&`1C`8b'bmb7cl8dG&dOF9eWfe_9fgfo9gwgT&hF:ifi:j#j:kv;l&GF;VmK !,HH*\ȰÇ#JHŋ3jHpǏ CIɓ(S\ɲ˗0cʜI'jɳϟ@ JѣH " Ј@DJJիXjʵh(bյٳhӪ]˶ۅ %jۻx˷߳r2È+^xCMKL˘aFɓCMi<#<3BUN˞Ms۵)$HWNc#_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*%c뭸 믷*,;Ю6{_:+m}c-Nv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmuhduWC]?a;<6e3|8i}v @l6[> <C|8=Ï?#P?'6V?z[wlh>K:Zo 86<:583|Gt5UѝU))uhU3DVFom*]!c1xիp21HcUyî:n,ުuiGbQ @70-d# rO nuRYñ45&Zֶ[͢u]<{K}@s wr؃uao:Ʌ/;/dpw eȰ1 dHRCɇJpx$d 6DdŮt>I kD-M#~Pv TUlCA!D/wLD,0dC8NW|w# rA䆄W~ c 'co3%Nh@`=}Nw-0i H nk]84ۉʵ|X GN(OW0gN?8Ǹ͑u" U? o#^&v}ax{/pkgmk㋍l m{Y|MGyym^G'AS{7Gvz)u.{Y#X?)b@LmXc' H![L>āw7P`/$p@NJD3D NPc/> `21I`sG  X3FP~ G1&la3g1Q=U^"!_d [ePj/!G@B8DXFxHJ$7Lh"p䄢r:777.CaTBg`?:e("ö 1DM*q{0 !7aWX' (Ő~X 8Q@.?.2Hy #`K Ҁ  x ٰ~Xβ f--9Gylٖnpr9tYvyxz|ٗ~9Yy n &pNis7  0`, 5pf0 H1КH q{: c'RF ֳ/ 8a(f&PsQS y/x`X`0ARa 8/Y  $$00'`gE! Q a,<Ґ ` U %l@  )y0 ! h r i ^ u [= HZ@   W`  g Z-;pb΀ 0 8*qP34 @ݘ2W B 58Ӈڇ@Zb2?Їq|!o@*ʫګª:ƺʬڬЪd0@a*a *䚮檮躮ڮzʮJZ&U;D̃v `@ ᪧD DyQ a D"K%뱗a T )e 1*+_1! b|( @N_q9;J· Ҵ 0wBr`N J0\a  I` ;!sIR;z n Yy gAp![!eI`A~l 7!a_4!;yA l~U|xb L[8Kȴ)F[b۽/6'dIt  QG{qq~BL U#( `Dz`@k BZp & B` – (1 ۱<>@B QH(P` 1aLP!`G1LNũ¾' Av|xLu|}ǀǂ@ͥDC]D}H JFNPOT] qB=WZ-P Ӻ0 gaӽrnTC0 ?@?.\ymHD]?ؓ=κ  ct#I1ل مM-`0ؚ&=4僤  -۠MC).`پmXVWMؓД+0I mswC m=ٕE ޘa}ݽ-m1ض=M]״ pμbR)T}}[*, X1C-]r tE='i߯\ޓPvp3$Nm,b"WAj=TN˽ *PӀ2Smrc IQ 60JiѦ-P> P q=%! h5Yxn@@i9l 3]*(8^)@nNߜaA7}@ X# 1 bNm ɞ/Pqq͠l` n@ S 1a ` n-n]5 ^2ĐP .>AP G0  -- *?P)ҾF@!u:p Wah.w" w P.B?6?@W`p A_m. ZZ > .+¾.YC YY`_ pP 1Ppo_.w>XJ1R |nQ,1l혮 P 3u*@ AN|Ѡ J rQ  uz` o/ @ ] Y ם ?O+@ DPB >QD-^ĘQFk \t KȎ-]SL5mڜum `Wlժe$\=>UTU%N&G~jUXe͞EZ\?ۺ7\uȮ^X`…Eaƍ?|+B]Yf΍ZΥMF soeY[lm[n#2oōk\rs?) #Wvݧ޾]xPyݿ_>F?i7_~?F8D0Adt!j/0C 7\0CG$Di0# D_1FإwG|(E/ D2I%e#J+,I#òK/SW$0D3M5qHqM9礳Λ3O=䳢2f>%< ECe.PPG'RLK7 oSQG%u|+%RWeU`p5VYg Vi5W]!tW_6'{ XcXdeٍu6ZiUZjŶYk[_6\q[w\sݴ\tew~Ӵ]yTc>j^}U:G!`%إ !!N'h(I㉀A&p8"T()ddfdee|5fh`{2yFyV8e'kny竽x~'~(Ҁ`ȁ~v/ߋ&p7L T#E.PK1\!FA.*9dbb`HܹяAZx$Hn E A$#5-! #c̖N(~IG:;a I#*uIG."@̀3 pe25$L '9Mi= F2ȇ $*$FMrpD"MĔ;`7kl2~w4B>сaV"$![AfP% oU IECpV2Ѓk|$h*v uiVMz 4S)3:=!HRz m:w!{( AXִF BJ ,( *`y& lc8" qp vlܖ@&鍐޻ROWn, D@j\͋K8Wwʭ(T#4Qo{><,y9.ܛ w XH@8k4t(UGd? y#E@RnZq7cwct0yrL Kv p{Bix:e%<`syA/[J<.0?>pP`<'݋bR3ǸC{` Є&tk/,χ>m께B_0S >*~'s)Ͻ%Bvн %Py;R~P>x«8(@D@ 뽂~Az@@q3yAj-3Aۣmػ@Èm;2z'$q,ֹ("?9$t L(T -T.4ܿ!?1 q%q(y*JU3ĊJ:xHz& w''xw0CmAw 黄X0C  k g ĂwX28Z HTLEKNb'BUX(#o2JChq*3l< Wx%5p5ʈ)@Rq0[xu|uTlرVpF;~0EK䣃26Â0i,ŎƅH?KI߃I< s`0,ƂBC ' 1xʨ>$+0m{LFąE(%,JDz 0脁0(A6$y-!k$XJhKĄH7 lIX( VNSfAPx0´4M=xx{*(ЮPl |0GP48Jq3tȡyDyȁOId>lɇ ^l@4Op1zHƒ( *|gHBȆī-|C4t=qH~Ȝ e  = " "5$} $U&- &u( (K))*--.S0+ 2 24 85uӹ`7S9ӱ=>?@A%B5CEDUEeFuGHIJKLMNOPQ%R5SETUUeVuWXYZ[\]^_`a%b5cEdUeefughijklmnopq%r5sEtUuevuwxyz{|}~؀؁%؂5؃E؄U؅e؆u؇؈؉؋، _`HbP,--&X9B Y-0_( ZY_-# %: X_Ѕ Y1 ꅠ5ࠒYغ_# _?M[_Y< %Z] -X)[ \$.[-ⒶH %q-P 6[s<q; ح7k]3!* Ս %5^M 2 Ue^q !,NoH*\ȰÇ#JHŋ3jpOU&(S\ɲ˗0cʜ)QӤhɳϟ@ijBडU L0ҠPJJ՞@^ʵׯA6@[b˶۷pᚍKݻ-5r_'3e„Èiǐ#KLpOb̹s䛞CMӨc Nͺװc:t۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*|C#Ϊj5Z8_RDM*+k&.|"Զ7mؖHܪW*N K2ݖ]'a:P2ٰ+ouH3ً,l' 7G,Wlgw ,l2UrTK4L.6Rb@ ()2#-uF0Xct+-@) #-p-tmx|߀.n'c 9K6_Ois9yi裓闲aWxE@?3y:@s{x<<@V?i`?#OwN4#~[N6LB4~?# <]E?Oi/|h!@ӀGhC <Y+98(#,aOH]4̡w@Dx ĄDt(q# AB&L@Y`i` ~c4Տq0U$ȯ6qC 6$#(CL$1*򑐌$'IJZw1?)jϑ8A.\3'Ay"6DQVVI6H"BGpo#ɘ/eF,.ZLe̦6nz 8IrL:v㹩͓ &F|rjd?5eD?T=(6.O:dl4/#?XHEґ(%JGj&=VfFQxQ0 }n*ў _ h7b㸧у8.|+EMlp D ʙ0!a $~QFEh5aJ@WUuE?zzQnX[!ت^*_|сȃx1x0_ 1xVFvCH8"jo%Mr:ЍtKZͮvz xKMz|Kͯ~LN;'L [0oakGybXG%oخ8F-~1bH)1Y$]4mXCq21T'YEN~r,eGP2[mD]ܗIԍ9DeܙEfCmܛA#~NsPb;aS&L$XZ8j 16<ʷ;Y P :0QF.IB.AO /1TV|+Z\`b<QfLE_rK \zI9\} i;ἢLj\0ȓ\ , )AǞ;V#!Ȧ\ÚʬU [ ,x{˺, 4,%j ƭ: {,IJ0˭L.j4`˿mɮLr+ͼ6\ ʴI @ά\l΄* ϴ@l?<Ώlml̔lӜ3LD8%/?Ϗt0tMjj%C@&v#m8HnVH/#˹s`1lUT+`<=%Vk009"tcRalphnPY;h qylQ0M#Im7t6CPAqlS}PyŪn}VQX۷Ke\ {6EoPgR$6 rW=R DhRQ &C={+W-4btTy ـRQ P+c^aws ,"ܥ,HS=P% n FVZ#ؽ\d^5ު5SL.&5Bz++4M)cCZ ;^|5}z>E.IYư}0ENlI@ AW+l T>Loݙr.ѡ,\T>璌.ТȮL p^isFC|56aޥkiL |ˣ>kV#ܡ.`贼 ,5_)NB$y%W[Ϯ_%_%_%^_*^B & C.>_6%_͠x @Lɻл_ _k|.M_|ZXB=_DSTBO_>%"uO]%*_&?|Ý?o2oFoIKMϳ -άА^ MΥV ~+DKl˵pl Z\Oʶklо n1Dloʤ !,NoH*\ȰÇ#JHŋ3j܈Ǐ CIɓ(S\ɲ˗0cʜSգzAU4 Jѣ3i򈴩ӧPe!1ӄ[KٳhӪ]veXbݻx󪍫߿9fÈ+^̸ǐ#KLe0]̹ϠC;ӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘RNHt$jꩨꪬ꫰*- N!r /pL1kaz" L[ 2f #L|VL. Cl@+lk AfNKl' 7G,Wlgw ,[ (,{N` ,/<r0@U6}"P/B'M/_)tEKKU@S\w`-dmhlp-tmx|߀.n'7G.䔣mMOcP#z\ <ဓN:@8/[?:5|L\O6@hl0/P:|O5?P??niO@`c7z GH(L =..L c*j8̡w@ H"HL&:PH*ZX̢.z` H2hLcq~*P㘶Ao H带T:m Ce:AH2|SA|?Ё:(4hP1 z(D$щR肒QjA(F!Џ8(YR(.-PKcj0NwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:d'KZͬf7z hGKҚMjAF m8c,Zͭnw"oC Mr:ЍtKZͮvz xǫMz+ߙB#ota_#1jQ [HM~'`>J}7`+2C]+!!D@&83<g< PH_a$Әpr|dw&@|s]@b\6Cnvlc5۹"y`e>HsDgX \ ߁b~ ?8E7^'##M5p*`4.!Ur@ǎZ /n`O:$tP\ (򊂌b `E7bmCl] @g8+[@$h^;sDx ̀NְGD-8@3%p"9AAj'$A[L,B&cc Ub DC4]>8 OB XCdQLq> ]Q1# )`Y k @ފb! b4_}Z@N4 xB$'ixq<H3"asE_s]8@b#L@ƺLF˂+F b-x~Wؾj|~dG4\=tы`0*dc4 Ds yj€ 17wa1{W(k1'$Xy!b f&I&6-I{* 8b# P `:;F0b-`c c6 lHBHajZH<0_HEh*(dX-}lGvq fblgux/؃?xU RXAXVn,Q8؉8Xx؊8Xx؋8XxȘʸ،S`?u(Aֈe QQ#asH(rn4N >GʉADډ vxrFH ȤCJh_pHҹ9(3:giډlڟ[Jd8nډxzډfʧu٥ ?Nj>BVn+(f0 !ʆfׂc0`l@ ʆ4P ?xs!,NIH*\ȰÇ#JHŋ3jܘV(5ɓ(S\ɲ˗0~͛8sܩBBLD3+MZʧ@PxJJիXQʵׯ`A:Ņ-Rm54'Q?E Kݻx˷_fjaQQ5e¤ǐ#+˘3k̹ϠCF9xӨS^ͺJM۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨ&NZ(N:H֪끯+k&3*lzN5@,P-yJD۱䦻4eJ' 6[ݻ,l' 7G,Wlgw ,zlN,T '1,KU3-)@DLѨB1+#4D--TgP1Zw`-dmhlp-tmx|߀.n'7G.Wn:,?,Ϊz&n?Op{3<O|ߏC/=gw/o>zH>Qca 6zé6aaᯁz'H Z̠7z GH(L W0 gH8$sPa(" %>?ӆ pbuR*jOW}QO?q6񍏪;qụ> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0Ib2LdtfLiʹ)?m <N8CZlfu#0!3&@;ρn @І:D'JъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^VhA dV)- (` @eQ`K,f܁T f9J\ ‚-B h! \cBA"I:og f$unVD ,qb-DA ?0VKh D ؄5{e2ލ,V]DüAG-_@r^ M(ـY  (BlA O GL(NW0gL˱w@L"H61 :m}@day"(o,f!oxc\!._DCJyAbe' \ P Y6'q 4OF (Cee_ g1ϑ20 &'[lVvG'#xTvq,<@WiphHwmFX ׄA@eFg +H a@Pq^Vh_6 qV]aÖs2qXfv`g|vY{ȇ@6i!.pfL_8zчkVEh^ViYfpH-PeXk񆪘5jx؋8XxȘʸ،8Xxؘڸ؍8Xx蘎긎؎(I9Q?:&hc*&ve,av pG Ps?>   bb 4D^;7w eU`(UI Y1c58  yKaF'?1X 0 y)FY' 0pAE GYUC^SMіH9]^y︗||ȋ(FH`ny @XicvKS `Yvhiv ?`< ɰOk h Yohta8vɊZh h &)Tyh"TͩoPyTQ䜟9f&gT  iĩsHQigC@y@[D8B t懟AёfS } j<` :Xsj)!#jd!,HoH*\ȰÇ#JHŋ3jqI@!55P@H͛8sɳϟ 3JѣH*(2᠙hj,biׯ`Ê{%ٳhӪ] 6US\t%ŕVQ _:T%pPB Lz+^̸צA'%VhI``丳ϠCM鮗L>ͺװc˞Mb&εsͻ N{cУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼믵 5ƎZ !8F"dnlfvP|+FPߵ^Ț;.74H!5kn2[ ,ph!2#2'l;8qck(@;0#w L{2*(,01)\t=\a/HB/16125XWt\÷u`ud7vhwvlsvlRtϝxbp]yw߄vwq|t+8q;5>c.U9yݻ]鹤: .bܺ2;Í-%-kN{lobJ #,|S$@[=Y- - {ߗm2‰~`ݽ.oEpѻ{]s| N-l dCHŒ(\ SB0 cHä̰8$ sÞ찇@HD HD,PlH*ZX̢.z` cq!qlcd>яy#qxF4jp3CajAq$fX}A !m(rǹ@GV%CyD$7 7J>$4N80hcKaȥqˁh2Udc&#$0MUHlf39 3:p`:':zS^=E}ޓ[EW4(BЅ&T"D#ZņR4h3$ ˟(HCq&BHeTAXZȃ8ͩNwӞ@ PJԢ#6=R+h1R|(JTF$U-!VծzS%XJֲhMZֶp\J׺xͫ^׾ Q)X>;,bUV̤ɤcٚN 0q$,.=Khdߌ0bI2jk"jiҴ-fKvQI>0!?3Ztq JVs{A$?05aJv/J@ tzY0?Jvr*| q7ZoN;'L [ΰ7{ GL(NW0gL8αw@L"HN&;PL*[Xβ#8-g5`# tR0a0JPwq     ݠVw`zx 5{ $(u z0x )Eh{ָp ,y("Y 0r'uvѰQ.X˰CP #5U(? 3$' $ zP=49A@Ny?Ð ` (07 A?P @h5@wS`DD 4Q (Yi8 >` IDytMQ98rhC֩)8@T8 wg!.`ڇ%,XbA *iڡ_!F""Mx)TH/%:G:@LKhķcM%S\1 *.:d_%a|V aH&mAq|Kdk%yԋrKZ2c\ юd%&,WI6xnDL|PƞǗɦ\Lş,Ďd,Ɩ H˝,z<d]%HdRXʶ\M6% v\P<ؿJ6lPl~sz뜧),Qw!h  - =LX!  msmҥt:Pu8} =43Rgm&U_`?sa#?cP?-70M @wu0 |߈?(O?7G.Wngw946یy(w~:?.B3."3:Oꅆ,{h^ 4="Oܫ,◟(ɨ/o[mi2xi:cYX p8$Z2Al ď-`8 ;0]IX?LU_p갇@"*eFL"R&NOa=)Zq'T7ECaЃ002$4SElqp,Ii0xI>zd~ IBL"BE:DIER 0HMƎyJC:>J5 d+gI&eF 0IbL2f:Ќ4IjZ̦6n2J޼)BV%iGȬNf];;y̧>~ @_4~=;hɔip &}PG? PԢDĨHGJҒfRP`iKgzC8S:lͱ?x͠JRB]A5}%4Dqmխ3 bXU(eMYj1LK8JӼuWM?Ș*կ=_X aٙ-vc#9y,Se1{9ɒ> Z͢t^cliCt#a]- +Ҷ-n# pKMrxH G=\#tpRxuFFB}m?׉dc Df_}a@pLKX5D!0& D0Hv$a -"?0gL8αw@L"HN&;PL*[Xβ.{`L2hN6pL:xγ>πMBЈNF;ѐ'MJ[Ҙδ7N{ӠGMRԨNWVհgMZָεw^MbNf;ЎA%iG6o\0cB i@!`CB!0-f qYw- i]nrD o 47oO7ͽn".H-lB|A(w}[ 0H;.1H68nC{䃤&B;P='6FJNHjENvG7a@ԢBl1x?] wA p#Pc\b.BOO0\/.v1#ׂVdCs\W'Hy^p=Mq }݂ yGKk| $ w)vA\dP & |a7P U1@buCu@w"Wr ~ g-G0Ȁ fP 9>r@yWxlg >b 0`Q_r(_ Lhw ar08tuvDy!  ! yMxP d{At4pq QSp J0Q ^+@sW\1P A6 (A`BX (C1 uq ^X,tGwpNb1@ }181{! !! @a p $1! %G׃0'Apl ٖ%w 1t@ hQⅼmنW1 O~z&  ix+v ҁ5`u'!,P6K7 wvJy>铉 E?sÏ CY;DG rT”D)Q9jKٔ\68Yw`)[b)7e)VD@y1wk'u7xrY Po}opFo /Y@ Gy 7sK9x y9HIiqVo* puq.n|iǛ Fu qIA Gyo V& d wRAtј4<Ҁ @ C> ) (0 ـ<2! W3  i#P;1;K%03VS^r 7!1 |) ^^=#Ezc,ڣzm v*D0 Z$'Jv*?2upQf1XBj@,Jc0~@5s:`6CD! 0` 7 3c@@]٠ p73]@>ZW{ @3_1B`(sW! a{,!cNUzQBJϠ S0Ϡ -CE s3nC3Q`w *W␪ثzVyy`PZ\ʠq Z犮qXym׺>qpY:-tK|7bysF޶ 4zi"KyDgGtQ]V'  Z-'s@PBQ Bnj}W+SS3xAvx1ZkoWg{a4oq+nrj|a@ ,]XPPo C p_pKZn3H:{4 THfc r+yq6 HQڷ=7ՉyleS:+ tAa I;Ki{IRB  TO* "!%XuCx$nдqu3GÞr<:!J;7rZĽmG=+1m@ۀ< 4mkBW*J]k<̤L3a4O FxQ/\| 0_PU!Km R/`60 ā ɰ)a*v/F F2@!ĺl0=֐([[{ɼBH:k@5̲< P ؙ* _N=6hD@lc5 [vj? )]F@qQ;lsa"< m4 E=>ʿ~ U=hՂoys;d ֍Q+Qn- \rH PaaVr7Qmdn ,R3qКyHwݯ >{c0  &ތH,p;a|/{38pAƹ" ۀ30@D (ڳoG/o^+ ^m~Dٜ 0?:{ I۹. ֐x^ >zmF38QlDܼ;'|H-H IO܄TEH[׃(K+~Isw1N7Fy 6y>{7.DrN`Af{)ZTM>yK*]V.qs e_^>*mV.y7Fu~_.Ӝ}Nwru\d0g &&^ >Oaqyy[.ꣾV{)囎JQXpn畜G7A^p*^XqԟʮGMn^~YNc^`A[Xn4+}t;oOޏoPQ?  _LNqI$ #=U#Ea0g3}@9#{b<1t1=ODoJ OvðSo>.\ gy@ 8$1f?kr;r!,NH*\ȰÇ#JHŋ3jȱǏ Z3.ɓ(S\ɲ˗0cʜIMɳϟ@ JѣH*]ʴӧPJMnիXjʵkyKٳh2'0ڷpʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװYM۸sͻ߶C &ȓO<1x_>ks};yϫ_Ͼ˟_7ϟ(h& 6F(@L:πbMz؇&Z⋃Y 0hUݨM@)䐈:D&KPNTViXf\v`)dNh 7tios|矀*蠄j衈&袌6裐F*餔Vj饘f馜vJ84z**hjgluqꪛc[Y2L#뭓%L#c!*k&6F+Vkfv+NE`¹?koKޫ/pl0?-0Wlgw ,$l(,0,4l8nsn,mbPF'J74CG-TS-5UW}Y[u]/5aMm>,jKvn#OpC Xs0w-z76373v"θ?Wngw砇.M<s0jz~9,D!k)@{FZ,G,+@A$DR*/A+3j&+HL:'H Z̠7z GH(L W0 gH8̡w@ H"HL&:PH*ZX̢.z` H2hL6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WɘW삕.`y,i!?~@ `0F-ja]0Č00 0Q ^♇I!r p3@a tg80h ĞH79ZS$0P@ Ơj&=]0Jc@)~(Egl03s5DEN: AK?jr pO_9\lviMcDߔ@Ի`UApT]W*LbVuPZ[2.rIa( J4Y‹Ըc[!TVE`Ef-jUe -;-1a)Za(h2ZvRnլ?iLdz  q`Yn'yK0 Y/{6+m `Ny@&9 m;[e ASR4,b!3LU,"ڱOX +# YmӺ ;,V }#f*381#g-raB7lKdvt DWlr`Š0:,x3f\C3,.d&̗490@\r^PfI5}-Mju͜ƋM. O-tMhBؕV&E(P1PZD(DAkQF2@ &hi` y1CvqL`l_X"؇r+a7mf'*_@S["]BT5S<@HIia iyl@~%tdm x&Ba6W[FyIҕΐ $G6;Crrzau'IQ3a w pSN&nGa.怇~44as%Q7[ ~L`sOnUG^ 4> $h$۠E"v< _ pOKQ m!'kd90Hk  ('cA.3J~~SR*PJ~'ptxAvF sJ H f=66M~yf,40hfj+x~5Ng!XؠOD@@PT)h{{r6JO$P0.`@y#AtMִV50Z5bD&MeK[j|m NJ|MH։:dϚjֺ=jѭjZ4A:纮1 .z+~᪯1r %9,i)1r:&Q˩ [[ k1űq ;&IQ,(kGa16`0 D]fa SL+ɬQۛ4]QEk7 K Qf>:opk\!,H1H*\ȰÇ#JHŋ3jȱǏ 8O[ȓ(S\ɲ˗0cʜI͛8~ɜ@ JѣH*]ʴӧPJJի{bʵׯ`ÊسhӪ]˶[fʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞Mvus޽Zn <ȓ/I74t@xԳk?)vے}OdӫG}}1˟Oh& 6F(Vh H`a`!Eh_!8y*24eLX#OiH&;J6$W^<)NaGXf\v`)dihlfy#P{oֹ:X|jO^[$nq袌6裐F*餔Vj饘f馜v駠*ꨤjꩨ ɍ!O۟k3;3L̲F4a#Y v+k覫+kF@L6K Ku GT6JlQ{owǂ$l(,0,8ӬB7,DmH'L7PG-TWmΏZ8\a-d;:h]6im6q=u'|yݱ IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0Ib2-9^􂙵qQ <0n`! R6Kv~s?:K3mjfhv$; Ox21gBnrS D\0Z"4 oT@ځЉ&g; qœ.2/mZ(H y)7 N€0,XbYes຾nTtA k/0pS!jtOz5^m'9OZ6L]{(\(b  I0M?jQrbhbyEӢ:ideJ"ݬA*_49rZd3( b!-|+ c8QVD(D \IA,L#@ UL \ ^egD F/@,DExW˶4f(yiWB m31Zy 8!w c0n @wpo^Fg?"Ё")CrR@E.^b}2Q/ 2Apf:fFg?eH\BxLBCz/{Sb`ŗ˦O҂i"ѡ˨=ݚ6 krXfFm "g)g^o@Gtٽޮ y]ڗ6yEc2@̹oOƝ3lg뎌]]x;Bōov;UwghyKfY)a4|g),q `b\1ȽK#1v#76؂cdk#F ғSL'o+iko&5 ex5j)ZZQ Ah Ѫ 8i/zv F>72Xf}}{!y|1 %6H0DNPVa-w"+`Xv#1M1bsB\s"1GӖ{Lr1dg gk3i/1` 0PvZQx#۰#0@dBme'ev0 '172P=)0`0^0tPy3=G/(uW/787߄`z9p8ė~| ư +w5 i8@@~D&u`uO 08U{n`F 2Å!i|q#(Dw"$|Q uz!A+X )gi0wÆnf1}Wfh5Xc`vr+@QEVUpH%;O`srq`*YCF`0+)?| bPz,*cэ UOP 0DPMi#{ygfu 7 N)/6+(P4Us5 Wxއ)t!wQY*7*S 4%.H)d-/ 7*y CtzՔT+71! Cl'T2cY0Fhp c}9/? n)*Iv@ T/mw2 ۰jd3r 9U0](QΩkqega"Yh0g5+PFVjQ6`FI)O8ngńa!ij J5|Ty ȟ VڇDwɡMdJ]Ct*&{1 &y[Ě<*X #Fa*0RN]ƠM:`qXZlrIcQM6 UqdfzLjlڦpr:tZvzxz|ڧ~:Zzh7_ƨj8a J2aʩz3 z !,NH*\ȰÇ#JHŋ3jȱǏ 8n(S\ɲ˗0cʜI͛8sYΟ@ JѣH*]Zt]ALJJիXjJџ@~rKٳhӪ]+TybʝKݻxg.W LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ N!שN]ˣKNIVuP~}\Apī_TC*m5?~q4_}e&LA΄ NxbD@TbO`}[}(F%b}@:"(;,h#PGv@-H&<PF)NLiXDc\veE|)dd dlylt(A dyu9W: 9X,(g6({ ̣j5iv ?jꪲ}Z*+Nܪ뮠}$F`5}Ϩ&{>#) :+yAKe,v-W~+TK;@$k1#0ԼlD.s +mG,Wlgw ,$l(,]0;i4F8zs9Lf?,kmL[tPG-e S X߈,Yw`-dmhlp-tmx|t~c| IBL"9gh@9;%+ 'P_*gL *{t rهY8$ wD0A] e b@C8gvݯ(gC錧~ @JЂMBІ:D'JъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:d'KZͬf7z hGKҚMF_ նp-``RC ~CAЂ-r[ 0|hG)x@xx`m.ߺ7C~@ %Bt+w nmA 7=@Z ci.t{;^V.%`[ߚؼmv\>ol\ @x ^w?uoKہԂ@!` _ -\`^(kfV ]pyE-ֶDΈ5g>mkN0p{ p-t %&tEH*נب<A DE,Ey>1gZ) #5Ȁ TNĸ0[BǥhFh _eCMIE/yh,7$[H4v01Emd_#4K^tH_"r$`6`f\HҎ8D$1A:]R//[D +^<8rX*sjOL@ؙ aHpvre>|J1n\kPnx5A*|.-s^βT Spc)hT;X v~0`uvt O7}@\YCe +b,oˍ?b __cC@Z0a * \h Ȁ1u  ӆoҀ (4 &~wÁh4J6w3dMƂQ/qaF335RD*28NEh@|?$Eh+Z[.W[^x$u^ W5^.in$g酁s#%Hz8td, mHr#}o[q8(g(\r(f^"؈j tU R腡`C6Baxe-@w+W$^h#|C0x"8RCֈ"˘؍0N(&Qf/#HUQ/222'u u`a+c`M1QgWQF#!1Nr[83O@1&P4'@!#D&)Y$MA7&*h7J\B ;I` =|$P >pC]a劎 0/ ɐ!B5'cR4=?IQ5m8RJ(щHI5`] `%@^uEr|# =$~ %P0IBYNQ27r|Pe !=Q0p,# G"a]?@ ji01`qIMHo֥ WYPCBIEh9lE љYR.D%?E^-trR G6)c"c({G97&"Fn`7Dn1/ $ E Z^&U4Z7J98FC5 L ?iC)cp3cL ٰ rUn4(qxJ/!aYC#DtA[epdO6֠/8ۙ!Щg^8jH=@HPh&g7}WP_J6n3dPE 7ppAi=/9Q%7pE id) 7p 5` 6Ne Т0:+:S02,=cG  ۩ki#3ot12BZ 2$ t&bo L֐ AI@RO*1q,5ԴS&b#OALOR0@i[ j `pxh;jfY*zv[=o ZŭHW0| [2Z 3/ e aԠ2qA";dkʅ ҕ:rj7![zek/UZ%R+0DaD&e4p0D*\ОH.S]+H$0אI!8P[Ņ\+1 7e` @$FD;51  @ ߠ y JԿ;v^ȕ#4k1tD\?P/78Q{pvGHD)00 ?ʑ'ARdý.W[^F6 [ż`6 ŸAh< \8[L64yP!5>H;0iL -Faዊ||~,n\;ȻQa8-ȿQh( ɽ!F`^kȬ8ŜwlɕeJիXju֯`ÊK,خfӪ]˶۷EKݻxT7W߿ <+^̸T`^;L%gm3O*ϠC ,Y:p=^ͺ^_˞M6ضs >7ȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+3l}raA0g "AHK:#Aj g"T A,or#80En `2P1kg DD3l"PynPK b,aܰ AK#-d0,4l8<@-DmH'L7PG]l(RWmXg\w`-dmh6в%] #3q!mD0? A0Q@~?@]K-/^}w}~xk>Cy@Tn7銛. N9.q鲃`9aKn;@莼 w7B,Sߡ_;A+> 4zoXߠK]Z`9~n 8`g,^& q @^7YtC'?P 8(zƋ0|OIg=!4{=E.* 1ދ/v&E*biyEvqy@ʨ3Z k -lJ\U8F^ gH"%' , HY(H= $:$#tI12w&JMz Y+e(Y6'i {䥇|=]PeߌR(c.hh1XZB8$ oZHb y!ڰdgsl'́x3 R @HR4<)Ct/T(8YM4cfh phGNJȑ|4"0LgJӚ8ͩNwӞ@eb"HMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:d'KB#,i ;bKIb5jYvӅm`\{Vm:S;[څofve.\;BT%iq]춅u{=nAz+W-{\W0ݼ/aB_n/"fJ@!,O [H*\ȰÇ#JH3bȱǏ CyPF(S\ɲ@]ʜI&MmɳC| J&ТH*ytӧP/JjTYʵhY[SYs]V$mĥ ڶ]tQ-߿K L8ˆ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(VhfvOS|@07r04SA@#B`207sB AL`MT,0@ @K@B-d~ihlp)tix|矀*蠄j衈^hH)2 .g`1؂f 4NXzөD @ff@VC #j@W @p0Ji ,A[Ҙk,~Cr K-71-J.1@4S@,Ђ1 t,⋗_ " @ifRf뮩 .({cnK2g[+2$'j8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|>߀.n'7G.Wngw砇.褗n騿J!,b 1H*\ȰÇ#JHḋ/VȱǏ C~̘Qɓ(SLIʗ0cʄR̛8s\XsΟ@)ѣDʴS>JUj݊O\ÊE9k6Ǫ];XtదeKBvE8w߼} ^xÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfva~a.(,0(4h8nJ<@)DiH&S)Ԣ˓dXLRYeM:@\Y@PrY2C@| 0@°)2!t1YS*!|K`DA2 C虳tDf d"?AРr&g`:@ܲ -Dh,|k.bZ4[ !`-. "!rTJ@nk,l' 7G,Wlgw ,$l(,0,4l8<@-D"}a@!,HH*\ȰÇ#JHŋ3jȱǏ Gq(S ɲ˗0cʜI͛8srI*UJѣH*]ʴӂ?KMիXjʵҨ@JٳhӪ]+IlʝKݻ;*v,޿ Lxm]Yǐ#K/]N̹τn{ӨSv*4հc˞M"s.]U_޶wȓ+ sУK5سkwl`kN u7s˟Oz.o(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlߺo\iO rȑL&h*/r.#O0GLb3 07,I:7Lq tЌtJ/NtTWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/WogN3 zyT^'AbMIQ#`L,18\C  6aP:(@&\?gDc4#@ఊu  "p@f$Dptn @1%@R P/&PF0~QeE!0*rF(&x^QQE1z1[`.D70",B C&5 8~ @%-`DI(4/tAT$P&,k^В f q`FSb-pa_ xCb\$P€߄A Gz68!_ 8L8!+R T@3, 5-T=*EP\@0*XSp)(@G,(0 M|0%jQObX ,mD*RRFMRj0(@ 05Jխk_Q;dB&B1<Q 5;O0!(@, (FY4r'B4 Q (F(D RBxnږ3 c5; ,)4{5rnoBA襮x?-|^и|[_|⿟ho+޷_sF'aFgn3Gb"g&0f"8wc@cHNqWȫ%=#=  `E(/Y?B(/c 9Śl@9q~@;=jf4^@8Z @ Zf=Yuq=f9|0-2ˬ~P{u]:-Oy0@kaυչ3AlJgӏZhi)k3/Kŭ*_f˺ iCc=\9(c!}nNZgY,<዆V(p#0ƕӌnbca8y;5H,j! /80 *q > Ē̏$CUiQY O$OSK//FY~9 E.^;_-fvPm EǓ>OZpBީ#,@B8DXFxHJL؄NPR8TXVxXZ\؅^`b8dXfxhjl؆npr8tXvxxz|؇~8Xxq؈W8Xx؉8Xx UYsp(n mhl@8n!,O+yH*\ȰÇ#Jq\~(jȱǏ CIɓ(S\ɲ˗0cQ͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKN:qسcνoڽ_|vӳ6][O~f,"h `ρ 60)56 ^v(0ɤzhY͇,ZUb0Ƹԋ2hcP4ި<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+kc,l'p8|#Wlgw ,73& %#IJROND3< Po0R$L@ -,TWmX;3:9`-L@5#r͓8 d2t-߀Tn'7G.Ot6\wA H.GOT Q$.9 @ Dn r8۬4O?P:Wo=(F {lI|#OA^zAK, 5aE,bbP sHi 9T2@ xg8 @ 0 NFh ;x@BR f2Z  @^ÉUW}A! js+1` ZߏE:sNTkĂA Ha MA.Ò N k@?í0}\=W;`a (Ѝl0uQ `H߷gX.(%ؘK:AuzrHB2b u]/% 0`P P'7 pnPg OMj=~@2X`/  %!v{ 6yP` p pyhdA h9^x sg]P @ ߠ 7 WL^sp?`  P PW p ̵\2y4Y6y8(%3A 97  yyCP 0 %-YB"Ŏ=3 ŀ I \Ő ' ) [ְ ) P  ~p pI]&` I V{x ` Y VAy ?C<ٙ9Yy陚ٙ_ ` f  0 i6 A%I`yyАא U ɝv 0 0) 9 ޹{` J PyϹ UɝP0 y70z ڠ:Z扗 -` Ƞ З ) ɝ94J`8 ,;Z>:0J )3ڣFJLڤNPR:Mʢ1* `  é 9H p9ʢ&:pjp:0 wv:lrʧ|88ڨ:Zʣ}B1Qlf6 x @ :jz:ZzȚJʬkʩ6` D @k 0 ׺@ ޺@: z芮ڮj4 zگ:  ]  A x9>oP>'7K{9"!+ӱ[(k.0 7#˱6k4(k:k<˱;['B>{C{E*˴8=MKS+OKWkQ7˵&kb0c; \iaabbo+tQ\aj[i\i;}}yK۷+۸x˶z+;˹{{˶K ˺;˻뻻kK;ȫ aԃm9b4 :ճ?B7ܳ"%gz&f; [뾉r { +BlB  @ (<|2 !B*$" 1V2@XR TRFV.%_MԔ@UPLm \NOrIQ cM8mv~h^u.IbS#n>ڋ~"> .n"` >? QQ#k[n'n>+鳞2~S=2G QŔ=@~ =ңN:Nn%~N~QFn42C?3!d/ABfN챎  1^0IAU U%pĠ 0Jl3g 0*p.?UL=U*_p6@/-O`)/NGN/P/=U.07 @<C @ m, %᠐4  rĢ/1??4=E*ĈSO=@':/N|\Nt?'e?d&6Z @4lGgO(z2FO(g ܟQO2DAsB7n4E?@3w2GcN@@ `.t"!6xct zE đ!]SL5męSN=}TP6aETR/W6%jS^ŚUV]jaL`e͞:C}R*ZuśWޣ)-I8ZD YdʕyY3B7Zh"禬nKZlڵ)[d\𜸉G\rcw]tխ߅~]vݽx͟O-e~ѿ_Cd~kapRR/$~GO~*0C 5#b \hZjZp` %uppC6Fbb/H~gqqndI'ZzPHxq&R0J1$̦|fILL9礳)M@dflPCΙ '} >ŘlQM7 £9$dSUWe=2%=[V[;(6tG %5Xa% }A, `6ZiGH~|ԉqZoơf!Kԡ0\w߅7gJQ Q*Em!4^}a8XYb aa X0'gv9d+XEf9 MvfHg ?UgnD7F:jTZE EY'AҤllk&_'ݦ>;n:=pQ߹| `?!z1pUqt"9crˑ#qt)|rG[3ȔIguۺ pbyuoJ|%hKx㓊XZt)xaB 8-:d xUEgm(SgA^_YpH 5kOD,"UD `D5 | PA/ I^)6q aEM40JA΀\XH p+ i@Q *(r XH8Љ{A&AD"{2Aj :!{b+W&p!a#c'o0aҨF0B !X#_L;dn8  #aɒH}/PD%8 |0ځHJ8T2plD , "bjѓ-xE gb0^}9Ͳ [x-f.IZ;jSj& h$M⡍W!xӠ9yUB@A`P6}6tjP~P@ )Pr :U[?ebY:QRpbXп젢"0!dHRl,r()X:yG!I})DRR`ONT $- B(xq O碐< #hAU\U>¤ZW?'Mh%G>6$^Y@ dD`rlBqXAT[ȇ: D^s6=Iv@ 2k~q{ nHWNBFϮ &vjL\`PZ]JuCT.{k  D&[u mHi~-Ey# ~31CzIyn:KT5y5q\Idi1~;Pao~_ ~<0`@^d&q-p"7L&]%W9`(vb+wi5v se211hl29`Kpf:̀5r)ra .SlIso( o<tE^E7_UؑLLt%stoÄ580iRi4KD]jV7 4tFjZӇ49uuXלzLrkb3uma/c Llj;ؒ.tiovLmr/IsՍs/7tDyo~'w>7Gxw ^&Y@pO' x5Μ`9r:<8U7|/ s5ǹq=2`]]G"Ù@ >BzsdR:Z t Y]>& {ϲu ;jV^vq{VN{/kru^({ xOg|c{ǼN/g9A!yd/}gtä=}g@@` =;{K/~W%IJH򥿐Br}0}GԾSod Z? !,Jh7H*\ȰÇ#JH!tҁPt +Iɓ(S\ɲ˗0cʜI͛8+ϟ@y QF)RӧPJJիXjx-qٳ5[ʝKݻx0ۂSg˞I8A ȷǐ#KLP`-9`bY^ͺװc6yV;o8Y`G#LĘ1^g+_μq^׫Wo1TzL\ӫ_Ͼ}_Eϙ`D *V^(΄L< ?S ~f#8oQv ! b(&2y 88㎐Й6hҙ  L61 o&?ȼB@1%di80*( -r|g̠b?I/ΈcߟF*餲cGse1ƨ*ꨤS:„Rd?꫰T: -H0Ԥ+@(0JF+t (0"j(;i[4GR 5a:S5ƫֿ`lsD\7ìqMldfwT($lK!Pq'r0CuX1Zq70A| TះS\ n@P?0*!a@1(0aX&TBAa#@H!1$,zb GxɗqwЏO[د07 ~_o;2ttx!+U@l" &@LP f|!qu !  |  ( 0  f @f` p{{n  PO o3 n )`  S P   'o ^ lx~nprl fum  }xq GwZRss( zG 4tEw(XF q0 &S0 P/m `cnp f P ` 7}>&@ J`   d wlp}P b o~ Rd ?bazq[ fp Yư ` ` 7 "9$Y&y&}9VP0~' 8g&0gcf `` @ 0 ` P ` Ǒ@ '6 X|l7o_ k PT Al( T0jN P 8xPݠ9 6 b44   HNЈy'_*E @ 1iq^ U_5 xyșʹٜ f`"5t0t٩EljL`` z #  xAUP  )BV W9vX! ` Pq p\7  IP lZ '8v`[`얏 |&V\@ @a` p3uGVzXZ\ڥ^^ }fp  z oyeЋ g?,o PF`   })YsufP 'bVPuh`'@# )ֶW `0 F/ N0 `ESa'` T I P87:)e +ɇ1qǛr5JƩ ;[{{ ۰`ws`+;4@ _a (Р4p ` {' fpxp  J r+ oபf xp   LU}`(&\'xapP{x o` To:X  zN ư : Wfd (sծ뮜;_ 7Wk_`H POwR o |1Дp   h 0 '0 qp PU[{P G 0 0eо&  [0 ` Ԫ`mf V @ JP @ w˻ "<$lyęsJ nqEs \5|68:<>@O~ %X@@ Pp z=z~VޯvA  !J~ꨞ  l꙾P` W~' pI t^s X ڪwo& ,&e^ P }`~Z~LN X^^̮ Z 2Ea?.* U ˰"_v.Qt.>5?Lu*U¤zEJիXi} WH]%u@rGZ;)(U^E\SN=}s*"64z ĠKqlhQFUhSljLJ *eT*SJXZuŋT}XhTه?~,P+C!_Ɗ7Y_YKZl)2%J[ItE͵mƍ!_sJ*f4j͍:]t芩O+|ݽ (fKX\3_սO'LŹsϣ€e6"Be5> F QB /2C?1DC|B]qeJ:\yeW31EwGЫS/F LN P Q0?8%LSP$_z"RF%-P.ZI7߄3N9tNФ9UAeJeٜUwRst6]_Pp]wVR`_pb ҩb 07fx.O3)CwUhc?Yc[1&ِ%YOQ*!*Q\1Ѷn?%8CQy’F:ifi:Y>Y3:k" e{"~cEႢ>I_Agk~-6䓙bjG7i|`D` y5_ykb1yuGX-; #hE^)d QKADE &N-7@ЀD` @-t4(C@3O %3 TP A$A p0%HQA1A ZA6TBIXD)6@Їȹ_LBqĮ h^0!BSs"{_䷚/&c*(2am !юwDZ5=14,h )l hLZ(j( ?'%X+|B%f=ҔDe*-ҕZF2 ]ӂ&&PL"/$b4" m50gGtJB{&XW`1Ug89NrӜDg:չNvӝLUOzig>ٳ) p6$tTp hWTcBPJBL >UA'=0 DRԥ/iLe:S4-nqa*-:-`A *dBRGLZDBe_|1nΘS13zeb$Qp¬<E,Yʬok\:Wծwk^Wկ}k;;XְE @GI4C%@guE Phzx\ A9! T$~S)vXֶmnu[%SgJ?JQ쩁x:Af&tC(|Q gϙgVFjs[ϠIֽo|OS-g)Q*@P4L$B3CaZWapiiO.B U *aJ#(^'FqMk^qm1N< (,!] #ܮ!=ٌh25\fppW?zЅ>tGGzҕt7OW:?KtvEmYMd}UkkL  8z>ww{p=6D&)pm54^45g >{E?zҗz< ?xOù~a( ާB4>}?|{Xq8(= 0:@YdJ<) vo&,o|w|}oSo> J˼T4t+| d $ @?# : l8<_{>.cdS {.M++9J ?J0?#7ÿcB&<'lB[I*<<*B°B*O;+7"B*D5d5L7L6C$:9C;CdC?C@CABBBCTCDLDEdF4G +ECI@C{PCMЂGAR<;6d[nΪF`BCH0H[BE>7Y2Gab$c4dDeTfdgthijF`lBkktlpFl|bs$*4s,ư2GjDwLİ„JtCm|}~Ȁȁ$Ȃ|*RI>*{F@AR#04IL ɘəɚɛɜɝɞɟʠʡ$ʢ4JTʥdʦtʧ\JLJJ0ItʬJʮTʯ˗ɱ ˲4KɴT˝dKKʣIK&hZ G~:Q9T`+FHA3" ˴L" p#(c_ 4DTdtׄؔ٤ڴMѼM̅\( ^, l]@$ބNM|NNNN\`3P1hd SļAD ȜLʤ!L HϬ t,M4QLQNNN NNNN%QQ %"L<La&pP]OP9SLcM 4U5e6u7mԠ:S8<=>S%6[7$}KOZ SL *] ,0PP̈́%M ]S;Q%R5SETUUUeWXUVuUU\UU\U^MU]UUa 9-V?]CM)c qHM I JE2.mPS Ll[EtY-UuuҔSd=wz{6EXQT/j2VP+ֵT L0L h2ZX|،؍-x]dّؐ%Y==T]ڔ$؂=Hج-ʜT mW8Ӓڡ% XdWR-ڥeڦ%ٔm֗mYY$pePP4uڳEۥ=LQM۷۸SF =:ٮ$ʄlhRDžȕɥʅS-*[[EEoTL ͌ [ФR˅ؕ٥u~}VP$ [a+zG@A.xPEe^hS^0X0G5kՅsEM^l9Hu}M_____ӽU HYCQpf.hC0`PY. ȅ[!5FVf^aN %_]_m#֌߽` ` b"-b$>]b_pv2ݵGdQ ^d2?%Pra߅a;<=cMdZa\ `DvGHIVFv2EҮY,b}E@ n`c4^2wV;ܪ@E D&>fC_@&b}?&`if_j6bhflVbmv^ 6ZNNTblR`2^!(4Ʉ(Z=GI < h<@f膞9h舖~h胖&6w撔gINK ;j6FVfvji:G"(6^H t^.^R@ еUf3L e3 ޗ〞j5,~ 8 x * C DQdFYQ# 2Ia'S.B/MN/2L0ԂTRb!3r!TRQ4L#5i3ϠeYhy, %]z%5X/xM6ly_0,iNc tTrI58C_Yh :(z(*(:hTR9},hJ n`08IS qDzuAΒ`'@}&$:L᠇h, LUu׉EV ax5g,<Sz-; >D]>!1b@4J'@K&UTP\PJ)HA'r-眀)Q1F*3`\r"FR 0@ $1 $4tye)L%Xe1לn9j`S 1 1O##˜h4ה%4ݙs)J))m6q=7u}7tw@kvM/L3LP)(s+lᩩ ;Zj>hse(0C)]OSui4LC :@P,( bwY42wIWmN%YJN)тN6/kt`@V]g{=ዏw枛Tۮr  to$SJ?CP)"qH$laD a )L.V.c8d 8xd=gliOLqmJҜ ( 8ai0yDi\-kS6M-,8AF-r^"(1d$$%gA~7"H40,JAŭŨJ BpWM,ְ` Lx [!5%YFӼluKҼT !@(4wf2b4>G [1|&4%E0oD߄>dhWp/{k#-xt2st"phĂ NHQ *HB" X\T( Í@-5C[9Nt؛)*6)A, xE1  8 P0d 88& C@%*a*0Q(C YH *fA P)7 8T':Bf+^׽7 CB6Ȃ[0:N4k@ O!e4(ELх N@ØFR Ox;`a1D@؄7T 7\p (T 1<8Q`7ʉؑ#P0A,`\HZة`q_=x2n ]_#, S\ uR57lbSE?<$?8IjJ':Uv}$ nT o0+JAft C! β,'%FfTp 5A^A zx{gD0`hE {x! '0AF@РP@ ?#(:YxPj!F4¾5b 38`/5h#eX뜄1|B\lܷP!L&֡ZJl 0AG,pŦg`WH @%aH,oWX  b'Ko"*`Y2AJa ^%}4@3Ёv"ְAPYxҵ!3|-laK<җ * W/%)щ61 xVd!A0$ 8 !3lPQ)lMJ1Ρl uPu hAXD`= \As< GLN9ND! ?y*&h4nn pE LH>(̘DI??ӯ?/M0v bYpKHQ\R-=C@$(@<$d dɂl؅7x$@,$471;UlٓqhU 7pBA35x/ lT, |(mBqBEY3pB DA1"t@+îq2XCL @Ͳ9֡nQ0 Y X]C`Cp"&<ĉu]GtI Jxȋ}DK0 /HetC |`),x LA,B! /|kD (A1C$ D@l^ԁ)Q$-2tR] G LN6Ltd Hy-̕TH5X (,<..$X $AA$B&P0P C24Ĝ,C$c+̔0BF9\X/pɁ;@֠ X4,@5 ZFB+Q/0_TA%!C3\a C 7pA@9hMHB5B c$Fŀ!x͉p LB&aa5@^P/d,]FF $<)"v ىCD"0/(YM$@ ´BA%@g:p,´)h-GldЖ9͝'L[TxC+;QH^F]37h#F%g)u2*U<Ae4;F/@\MH:ȑ$`2EFb ' Ҍ!&V^B6d04dC~/d0lhsv(QF xmBnTFB@!B($(LPBje)NBL-1Cq%TDA7F9 M1;rU // ( @!%$6zVhEx74/ A!ad4D2|C4@>*FN*V^*fn*v*†Z.2 eF hlFFG0p 1B+'T B2ԁIhgA!`,@Z TB1%R0E A1d=r^\(Ԃu]A4聚A1b47W3$O8AT+@P%1m%,CTI^ּ040Wnyʮ,˶˾l05̬C(DBِQj+B@7Q!<*A),EA3xTBq<H@!\! #tFTV/^S@.8C3<$vD)Z'BJJ'&^* /.閮fj0DlC6di/@Ct7*GDJdʢB0A-*C -j4o P0 C$D@xQ1`%^C/8)C",%PA\\@%C(JM7$,,-,A,4CXd^ B1B'TZBOZK¦QmX ( A3@ɂ9A֬,.7d ,11'/171B0[J0F8DOZ%h -*D%B/7@t@w0)VQ|NhmCҖm C#D"0A\xhjړ&A,@(:~-7<^ -0Wނp`^ D 0g,+0'<="AVZ)@21| $ ^.l73?34G4O3j0B_*E<%*jTG'*%2).Tbo1Ԃ2,(j43aJriz7 /C!}:*;* :'/X pȂ ,D9ؗ;4}C-CϳT0,CwDt!}m0q49E'JGiW-lWU_5VgVo5WwW5XXuX0(k;-\2qh=+/,;/hJ)5h)$M숍1b0uQ$M2(D/RF:̂QKe0J]3/4CB/2LO$< t<GFڗpv4@<[h7lVg4`LN5r/7s7s?7sl*E/5t28(+B9?׉D.qlN_.Pܷ.C'Da{ɬ}.,l9 5Xe"&ylD3Ł5S0 OP0'R%>HQyUW7w:::zU[5v+D|%jx3brApDbǸPxL~{goT(9nD?n(8cx~Bc:,h{[GÜ/h-{u;?aCPzSx}{B;;>'/>/=w;?|v:GË{y7~黈}˽ܓ{Gދ>׾}7L>_#go?w?S=飾??Wg[{>k??@8`A&TXPWC!F8bE1fԸcG q5+TeK/[6:?`2dD^?hQARTiΔ҅VUWfպkW_bk,jv-Hoƕ;n] Ve+^/=bd)ЗF6h .VFreqgּs^ H)_VQA6qX hhn^s4#F'/]ߕ>zuowlK+rң ^F^w2`r_BPmڲsȷx;*ɖ_!$ 1P 9A QD;@YlaQiqQGGpGtn %Z*%l'r)9R-/ S1,3<1 /!OebGf$ sܭ]|ЩbLE˥EI)K1TM9OA UJ;"h Ȗb1cOYi[ 7aUx%^tI&l@:чNZVmUiUq-s9ס%JV%_eZlK00u9r ݁%ʖv!X;c\ b1bޔ@kea]yᕣV"pSo[Vsf.`褜s%Sz7Zw鮳6Zla~^vmI&GV O|tV5~m 2HZ:ffUyNꦣof%f|nl'kޙ\vW*\I4I)Ɗj T~ _\ JwY~O X{ R /.?G_{^]j_Wt%0zNWbc':Oa!A N1A nAB%4 QB-t aC@Z`T)y#!E4D%.MtE)NUE-n]F1e4?s7u8G=}HATD/96dY;KM<!cd!AJQ4 qJUR6"Us@}%d&=KV/9T`y/³eq)jlLG:O7k^39yN06BɄV/=阇&NOr?$ h* ZĄ 8͈s]dܙF6N$^B"ByI~ -uK)"t5u!MNNT_hFcY:00!P ci"%"'p CP0Ҋt7W/t伢LJzCc)?yN03ڂМ:y5ɬ^cP<9Nz^M80 p`V,fYP*Ѭ,O{atb&eZ#"fj˪ufn$.ZSiGjh="n g8C!Dud5>#M/Zp`m0A0dX,X? +>q`$g (ՈcJY6ZjEk؂x$pk)<W8!bu}Kζ qWo\c!X 5A !͵w"_L+~׷QN1q|gخhP/,7[J6K }#eX b TD!P$EJ ~t@DCZ5%|t:*-ҌҡhMҦ^4CQ "ѷnhU†uj[ZҧntqhWCԮvbmtԗNuK=p) ÍmYbۢ趫AAnqZ.5fZڔvw Phҙ.6l[iǻv=pz_x;pS!9%pGz^yn'nM8ǙeҜ) Qbe:9-t`':wTPEAkHKE6lRHh6C_cPܩb)F q=H(N,qE!xq"њ(#/ll));౱)RXP9se˾Ԟ&L7@ 0Ju[ ? ~(`aP: }Dkݶjo٢ǪYa䚝9_ܢ: np/u&bmBA>!>A0%/Oӈl2H2Ԅ+*O14M5?]8m o=E`PEnJ2,Mv00dOv.0n.׸ON4A4Mm  PP0k ! )Pc0vF c0>AE3p4,A$Q%1R5TQ_qYՍ5DJ,< a>v 'qR"AASO3!1d: '!FJq窪$w1 A0x6o/ncZb^D{V\ bo5+Nr%L2%kT:KZ֒%h&g%QRNR@'kr(YR'Y)e۰%)[)g'Moی2 *a(ǩ'+%+r+;)&R+r,a&m-*y.-o&%u-{2&'-y' s+U0$3(U.YR(`/R13 2*E/]b-r2&k\34'k5(%[6wo!RB4o@ks.0!aA!Od6 N3=s=3A!aaA!K?աa!+. 2^TTC!a f !?@+Ų?no-US3ER-4*5ER%O-4GSEw0y4GA)W)s4*u62H]F@(ID4Js+O2kT7J4K&”z|3..%DKo3!`3sO%A%3t5a%5%/.) fHrdlIF5_K_2TH5*yUGLq3X54TFGWuK)ZLQK[F\)Tp(2]9!8-UP5879 BϪuR+TPO!R"R5KQ!u֋AT# HAC*Q.@AHTJr6W]6!XWwT+%eqe'fXHk-9kh6gYv+E3+hheg@!T5jv\늓^'V5%ڕul 6%^[u0kvnЮ9}RTj5a `b!!2'\BCA!67bVVfDi*V% kyLvJ{YVeT%je?UjR)z@|uW%JA*qzy%u_aAڡ؁—||ѷ|w}} ` ~ ~~ASpAW %ʓ Ap` Q%s;xK%΃ viwJLo7wWbx7nUYvvGoAaᇁ8x8I%VfeQڀ 88` W|ɗ||ݡa·aal XM5q[B"%TE53"@@JQaxsDQI6͌xYea_’su%YUPt$iuw %TY 4+!'5{nO]q`ok-udns\yv8ZsjSi3|}y9}y y 9y |@՗9Vu~y浊{n)#4]Aaw7Ǘ({|;ǻ@ɞ~~@9)o:ɶz?Ϛ6{iʅS?i[W#'>۳ kACgXƮ^{P{*o+4,X_x1L6K^̇W\iߤ[N{?gZ5kOc7u]>n,wp燚qNSO ? Yb@ 0… 9|a +`۶~JOHuk.L 80} d7`.ꗪ̛WsT<9ͫԠtM̵֮vm6ܥ}]6hЌ.ċ p߿ϳfӟX /\.@qi;emۻ?~^GwPM?c-OȈ1;#R#}K)՟gn(#0H?|e Q B\8  P[9^#) Rg Id\D 5B>X-YWHdmYAj u!B]N?Wf3qFvމi|1uFn:(VD)Xvmx艃y9^7EUK %w|}C\slwj9WLL-.~HmPotRkFڈ888$ ̆$jI<7eHF2<)@Z:dee+ B\!Ѓ6!YbɏXp"񙇙Dqva'3 BB9d+[Y:ug!1QDb/xdב4clo#~GqC\l$}:q \BVt#0I%P!G 9b qFNT!+_ zMعLBkKI5tB s$p5dqP͘PXG5Ǵ dp4^b)ۤ+2C ѳb*?QF @)kP:܉FHf SrlMREbL\0BqXe8jU C}8s:\!YΒI 3̸kW$<6iM1-T<6_[:;eiY\He0ϧ$jE)J1W" 8sΪ& hF3G$P|v U!d`zt6**T!6as\IwMoP #gh$%FQ@u#9I*浨.NJRZBՕQA[^ ߌ8g9rw#W6V@+ 34#sQHY`6(6⫐Кk]|C7ks<{ߘؠtNmCw݈C|[CmˇO3~ѐSۀ5kj\s~?8 Iԏ|W}\o,T5->onu]k'Uh]ahs du1%c!YRB  A kWPƐ jwTxb P <&V  Sa6hFe䂷cp+B(]R]ߵR߅>p 2r 3Q[ȅ]HJ2&@i?cgS.e`A''_\Gk0QE(f vAU 6nSb;;F&y84 Q?s`>ygJn((q 1Ƈ. 4`{Q}TrVj<j}JW'x&4hp"Enwx;Zw_C@BB*`"@ : P ]NVf!bh.j q hݐ!MYEn؀ D3EtJ J7 tf 7sU/%P U>(5;LѶWz%ʮEH B HpB`"B :@3 pr}l t$Eyahj۰\n~ M');_ qZQ dA5{خOx10bn0 5ܐkԄ9>44T|HÇs *_14#pk˫gp("BGa pXGt8(1i3 ^i;== | D.jc  6`&`٦m w0[FAM ' Lѱ74k  4*ɫ,K6a,- Am&*Z>*ѳmu»YqI]S taq tx|"p&;3 g(h{v2;;! ">0x*e_& ` A@PpeP!WK)sI8;`| ~gP B(LQYPJPCa&Uf+A!@tq kDn~& i0ulh&umw>5 p %0\@ 1P l1LPM+se5tBU%(q=/l"yFP 1!a?$sS v i`cM VTѠX S7;j H08ƀk P1l&& 5#Pկbpq5јѶ(^֔b:!gZ {a^jlSj96Y" %G'y 1dB͐}f,L|d ga <,KИorɈ%VU+I%4ǕjZ6(I`AGԠ໮dAAP?3|2` GHCŸ P(q5b (Q SP,DF0Ο 3f3):ե{7"v Ji`0z9~V-rЏ&B4`eFA`` os]Nʓ3\ 3.xܔX@DB)0?\JwCqbleSWu&(Z9q]32*GeIAQ/h䇫'ب.o «NupH@8`CĨZzP4P ۰OE&&d `z/!.}jx %ڮư e"۾C mRa4­}(M* ʷK^} !"}6xvt ms+i竎R.F\A˲Ҋ)ߖҠpn āy aQ躋n@.#]v#?,ApS @:s>ŨXd`@.92 t3/9u~fFVOqUZfZk {Ŧ=pPC2dH c1_fWIetזu~u)7vАo3y; WݿE̖o@/h#f$c082g &)Yp 0ю믧r0: 1*Pr(pthyƲ,uI4HrI%D#iasG''t!6ð$ 4Zs* -h9A'z!ȍ!% J)p$ H*u/'r҈U;6ܱO't= Ma&'iFG5?bIˆJ2Rb 2y&z)I&u푧̠ͬ1pz/cDDmb g*0&^iO2veW~ ꗲrJd\2GjߔPfM ErSt>J \24,N==y%"%ROEUK &uguFli|p2JGkJkϓ8lhVuƇtHĄ`!k3'gNo16]鋌8zj!(tGeXcCVV䗌G喳oy*sLw x Nլؔ66i#Z8JD2``1*6a$yY#mc`P++ L?+Hx"e$Wtt/0pONt) 28h!i Fd}󡧵xT\psuAM5qDGk Sѣ5y !X_Uf5{ "7i3Nҕq`Hv9&D~k5!X 4 5^&F RЂ<҆6Щ}Cq#&<'eETgH;u FJh.¡Px0v ^މa DP?51eỳ8,1$1kKA+&#OLEt_z#C}қ[6 YES8/d O>K(@$^T $hA@5!H(& T$F(/h]tF- ͈4cgԎs~D1J_" xVB0.eJɰjP!DӚ\19SRb`"Ic+qEӚU NcD{l{6yLÎBbJ"+K ͫqjF\9-HO!bא4qs"-H;hR$# ̀ PBxG.2\f>x.~]c_¡&@s][Ǽou1 4GJ!Ɩ%MEY1; ƽd6a˘672Erfj|x(i/ e(]CôX.$n,`(FQ2E+F= GDj7Zx')# 6T96P?+ , cXuayd,rh5/<($,<*/2FlqJ]z(4;vKWB ]ӝ|uqxGQLv=BC6܂,j8Do<9(E-vafYЃ`1m78 2@AA#C~{h_)ZG+<@BcHC8(ͼhzt")m`pVK]M# Qs)2l05ѷg#G{}7ܖ9ҫ}m8w  +Ԡ8@B]Xty `HX:!Lml=p\/zHc:xGGFLu%3AadQD5L_stjx8r&K; d8 ji &f(F@ d\DYTu0GWD-+G&xC%NAecֿІ&ͫ0q4!Y> 7#8+($:ȥdh8š PRSt CinI.HJډhi<m=x7`6p Toy 1cXjbjُ:1Z_V4ތ߂ko(7kk(h;1+Dh3 ;jgGB8p^o6EC)(>&v0M\`C .Fmo}= ud +qڢѡBZ p %WN}Xeٺ.ą8_њiʊi<3G2^_r&&CCn#pHss&x|,`8`p&olL Zu` ^x(q`p  _arJ4NU Q4tMD)%`*Z0Nh 0`xexMgR~h @>rYVJfofÌF lJ+: sIrs)LusCxQǚ xމX~Xw8=`X Ɇ/en?T#baZ pnpvo( 5) sz&E#syo.o(2+au(i;u= (zpp`[TOH 0L@'Ag@="fYN8GAG؆!iKyE1XؖҘLO'>HvLthZX!q}z71?za?hLFxԟ TMK{:skxd7RimXѼW#u/L?\[|Nݐo 8K؅ύC(33xSO\qTMާHz߉F?ֿ gN4'p (PaBŊ%b Nܣt :bq"^\raƈj)sD4o̩s'Ϟ> *t(ѢFy 0(?x8$FKM~^}K eg ^nߺ@.\Ĉ c&Ss,f6HQ 0l:$2dP$Kw!QbYxyTK@ey@N+ajU0@:n֚9{grGxAD.:;T0,L2)N *82Qj:Y9 &T чfQYbA⚫"SMACB8KN'yS$ޑ]YZ^ʓ:ZC*Gkȩ[OI7]uf7U )WO,+в<ۢH^}T}:EߒQ$tnbv'GddD+HCF̲(S? aC@LO6CJ pM|҇wq "("3:*bPeؠ%_c.x劈 YۈIXO6~DpX 4 bRr#! iq\lQvxG>$z:P#Ё pdQZHS'l(Sm0 g#;doD F^ic"@O2ĈP򖭊5<#Zo3ؒ#'r8@o",dÎ'?'y/t$cMA(0.7D @+jыbe-Bu,(*T0E3җ47|&DړCXE4XI)Qjԣ+5]P'R!Y6155"5Z*WSЦ'NeA,ַ5D٨3)X @u\ kú +OBĴdX65b6)P(+a77kӢ֕a#X5ħ-mkKȟEq|-o{2(okrFB7W]vb7(RP"j7O]7]O+Ѳ7oF^47]{]7F}#8hCXn#l t0(ĝ/G [r#NAcK(Xupc\F!UDi|NE8!pCJ )R7ȫ\dTqrC\mj^39n2D'WΉEg62 !,N)H*\ȰÇ#JHi$NܵkUIɓ(S\ɲ˗0cʜI͛8s*t@@ D8H*]ʴӧPJJhPl>QKgٳhӪ]˶۷#z&.#k %߿ Laf0@@ (X<z̹ϠC7o3&fP$j,Ѹsͻoي5 j5bE-Z]سkKV 2K uǨ]o_ϾIjhA"pQ o߀h= 2 5(@ uv X"1@ <d2v(4h#I Oy z7&L"X8 -@0(b -difoue0\) 肌3Ԅxms@ȴ)̛qѝ|&袌괡@ 襘fK jꩨCjjk8a1. AN2dBF"qIɆ$Ϡ ٺKrd&HDѓC@u;b*tRD5DZթ%0~AXp [ &cVo0p# *C(#. tC!f(^A(`1MyG)cdػL/ҁb0Ȝ#bo) <DQDD=iB2 VF b  ;y{Y8Ax} -RbC 0ƷByBsd@H~^b C)N>QԂpwv8D6Q2@% G(H7:ME-->BAr["D^@iCrT1C2h`E9^x_%y3*ҍW)i"| R#%x 8,c\[ ؃*Pq D S/"y4` h! 2%mt0Q! $&~0loY 6q X2e`S-8VUTZ6ZZ7Fz3(UpUP|G8=HU I=86=ZC(KZ@QM؅0kN68ka5oX2 ِI0.{  2T 5 ` Y1.0۰ ĉ 8 @[8G2! a)Y5QV!0 Ϡ​%Q 8K4s @  C X؎ w Ps 9qِ7X 0 }# @Hԏ3 ( QF!9 $h4" P   8Ab4>0!LU`qI)AB!`b9dYfqj:R1 \)/sY yx II'V9(nTw+8F'721GT L XIT)myWL )94B iA!PJ~x i񔫙v&`ge7#RP`te 9A vwfjJy)`#Nb@Qz@ 7L`knЁ}pcn0@JNӝN?L} pQ FaДp)5M yǑ *y `G %%=9 ݙ#J{f $  >S D I ZLڤN* Rʤ1SJ&> #ڝH/0dZfqjJk7A HU`a;&V:cbE ڨZ5?EDpOD tưx:/veWVtN5r@H4@@0vp*NګZk7ʩ*-5[xCƺ40` wJךONފ65U `@@?`ڧdnJ6#` 5Y7Z?@FP zN 5kZS1bzlz !@~:[Ա"v0 ;4{E[;KCЮ;E ;p@}9 q^ ו0 qXG٥ 3@ NG!ڤ] s+F3] 4m q3խ3N'͉$ <1`{#mjkM"q1# dy(XќPq HĽކA Ѭl  a3^#t+q'7 9n;5~=/ q(&ATiB/N.O>q>$@>%\]^S~aP._g2`em!&&dq{>} Q/n~牾aN鐎ݡ,!f OJl.>*fꗾjd~!}awn}.~w>NdKsQHen~^/c.v.`N3Q O!$QAI!>I)A  Rv _)  4)e' s 1?)!{&&64?83A6O!=HÙ>>0$o@M?][`O?!WDAR TO"?" R"_"-_,anJᓟ @ ٚv+ڣo/@r ObA O B PP٣pa_'MsTO_?Y_@ ,z,:ޯ a, ߝoϢЙ O4PAMK*%Mer ݵ4G!8Dt IQTI ZSB =s`Nf3L$F40iS~'Ť&0#D28fLQٚ (gFn&2i.LoU… FXbƍ?Y2ārfɃ c1- U:Ƭ9tvmû&IAᆅ/|*Yʑ/X6h9{:y9_ws7_[o_~`j%<dp h{(‡;:@D4DI$,i0FyCeAwGD3oA B5(L K/Kb##" ix3tS2\>*s=7:mJBE4Q¶'Q@;1 `cE7[SP~'T9QARIVgKg@3mI2LZ 5be3L4UU-@hVFm2"0re\dZ9QyT6fS5 `U~5<V7VFx2lU4`aFuG|Pc`HE)*c ]@@a7iNsv#CaxKːEUE cNAͨ.x@MbL U(T5M?.HR;%V8bʒ5emP3lb6!^PF 09MYF+bY04(Z;ZֶQd{Uֶ%!Q2L  Vmr;cB P~6 4aScv2L'bt?兯 u7nUjώf]LLeăn-8` W8lSV:1a ķƶ*Yw.472x:χVc37EFth2p4vHPҟ/7c97/Aj3ԩdVzl\Y@n%cK[1~7Z \Z =m 1ʎ0G ]X;Ƒ=x1؆0, "û͠3Ξ7xvg vA{ d':OpFclB톸lbSt@U9m\Gl7fnNaΘ?1iӆܴ6Ρ*Ġ 1&v_)]vGءs0=6<1=CUpM !ہT¦z"0.rqw78: +I |5_1[7?zқ't`cc,\e? Qӏ?}sRi?}j*5O1pa|w_|p2~4/FGw~1~׿t;[?VJ \:dgS?* $MRED:XHD6jDM$.>N R4SDETdEZVXY1R[lW]^\D]`#`b4cAbDe"edgFT+QtK(ķĹK˻ܨ˽K˿4LTL(tH|ƔnjTLŔ\̀!,FBH*\ȰÇ#JHbt3jȱǏ CIɓ(S\ɲ˗I͛8sɳϟ@)H-ѣ⒂[+PJJիXj脁` 0@@gO]˶۷pMA8dljN(js LÈCs"K^@"3k̹5 @iZ,&@@ BȒbM۸sЛ9o0 &b48KNu:5h 10W,Z8ξii p EO߀hl_"\5 Co2F"v!J!B 5hrJ*tWa1}(4hA(@1.4?CݨL6I.Q0rT~N)d 5X"K(- Rx'UΕY+"Q袌6RG2m2sc29駠 fצ30N꫰*5 9]꫘ +(L}f6!5Lx@ (f8lww-B{8<6CS39W~l`OAx(3#>;ߎ=?'f o;Bv 6sG 6!:JAF(L W0Y崱} F$Nkq6uYi(68R)!U0p \ !C3!=% x IP04Bx̣c4aG6+1},#Z?(0˂5퉥 RBJR:,MBXH&2l̆4 @A- "ߴi\B 1`wot t$̦6ͣc⡏xL81C_2!En[0oOQHIP} eP$}lAъZE jxxEB D/abꭘ(3!k4tؐ˲ iM! h#ҚMh7⵰lgK[A-2ae<.K CmA !41bRc0]O A覗f;Ўlޘ;u1mOʣm=-dҤNmk#%+^yכ7}zaAb,F6aRc 10 !s-1L GP->@ Sw:>:y`G;AbN:ԧ.OFn{`NypAAp0>5wxϻtӰ> aL}O.,܎e|ۃ<͙mzR]oS7|gO^.U|UaE.jq ^,01Ba[9 #/nbFrB/ / d]] ؀xN m Ġ o 0 *,؂.肶~0 voq/{*{@B8DXFx 'thov~/8TXVxXZg~ h'0Peh R "Qxp Vb,Axʱ|؇~o`!8(XXH؈v@Pp؉Z p p}ڧ}GFG@oE / ~` Z P{`{h %2 Qڸ E  b  h]x옎Ȏ>  Àv`>@ ~ Ǐ9Yʐ0p "9$Y 0 `~ 0{kX~s u&GVJ([\GHIya aQ RYLyJyTSٕUɕOb9dYfygeOYSVoiS@t9n ePO)~gɔ\P)aZ9X9YIQٔzp p 9除  kk99QɖaxQ@ٗiiyƙȹٜzyؙڹٝ)\虙b°u-BIvogZVԟpM44VTCS>S @]*3~%BMIL2ʡ$L.4:Bɰ t:9,ڢ./ZO0#sq&Tp9pT 6*+jf U=p24p9H:03`?aʥOJ;DOw_7:QD)oF f` oe%zj 9o ZF|ڨʨoک 0ڪf@ګwyezȚʺڬu %ںڭ{f|7ozķ 0ghs:n a{b";{K ۰x K Pppk{ )*+{%[yZ6[|`&K0@  $ Lˉ۳ +bP (=Z\۵^`b;dKel۶np?kFJˮ+Z apvw5uꍶp*gK Mb%"g{G g{"k{9+$k۳۹#  p9K 5 [{K GK Ii 뼨+P ˼j+۽J;[K K ꚾEzֳ  wwg@[ ={ a p `+۴;\|Xg  %$".,<\6|8lØ=\ AܳJk/ <0 | .kHĘ`bܰc|hjW˶닲W۫  v<h&Õvž^ Ȍkšȹ] œà!+>\ —ģ0ē9{;C|{^ˬ<̉|Ȝȕ8 æ9硿x|g" [; ׬,-<\|Ϡ @ @|l0 7| ]5=]}ţΡ,X \ M|q:\Ţ0 \7:lьͲb=  a0פ|}{؂=؄]؆}؈؊،؎أ=ٔ]ٖ}٘}íU0M̢ T O]F6̨ ̠`ו 4ט]۶}۸ۺۼ۶ Ԣ<L?N`ΖܴشH `yh6 د3kע \ׂ-ڂ]ؤ0Ӥ @ 0 ڧ 4Pߢڠ3"ѕ@  ѽءQ@-NϽ={6 Xڡ $>3PE-JL.NR>T^V~XZ\^] NI0 d"lfF QZ`QѓbQ@Q@u G`>^~]n{>S(~دQa0- \puݿ0wE . Q}ڟ .䠐 {NL`Q >TNg^ 4`l`b` x T0Hb@m xLT? N`oln܌  Q\< i"LʪPS"M02?4/ߑ S` lP @Q4VI4L~ o2 vJ ppU5?d_fTB%&nU&N"vp0 m-6"~> (ğg?_7 9PS pP ]  LƍԠ`4W \{ePw~_O x ο.%r TboRNp? 5F (oQ1-ޠ든n <TJ@ DPB >QD-B%R$e6ubRS%BFQ=!SU^ŚUV]~VXe͞E'PLV %5.A4}%SNpIJ'2tTWl V 3jҥ YfΚiъU*R~d%-F4gMwcƝ[n޽}\pōG\rܡ m $IaD`c 5j4D2I%dI'2J)J+2,Ɏ(E` 9Tye(` p7`&M'DW01# 66q!DH-eQG4RI']5%c϶І{P2 5h)R{Ed\,p `FD#42yG-l{DE6YeTYg6ZiZk6[m֧iEW*A%Wǥ$QTqK3Zl%ZJĥWB ɕffyE@أQ(6afa8?)EM2Q0!oBMASWa2ɜE[9U{Q5KObwg,MKFؒL~K~%'1aQQ&Z5p"P(AfmP>Vi&lrz5zek}Y3\ߕ-VdR/z駧/drdU4#p x#}300CbB.D1e_`8@ЀDv;)4@ FO4*vaE8BЄ'Da UBЅ/a e8C*`H id#EhA8aL@\0k C(/zaX) J@EPG#0fD՘F6эmG:юuG>я}HBRz#"wd Fj V3Ĥ!3IMvd(A9JQ'BҌI& FD"2#\(0~1#ѨȄ C@uCɇh!&a5vsf89NrӜ &!Fjyg>O~sh@jπ F1R1/Ȳ\꒗0WbvDJP ?F@pҕ1 d Ԧ7iNuSԧ?jP:TըGEjRTT;iT:UVժWjVUvի_kX:VլgEkZ 0;6 .=b_v_)>GhU8@@h,d##ȅ.4YvֳmhE;Z]2qEmjUZֵֶmle;[ֶmnu[ַm{30 15 R®kEP .k̖h)тb0aE/Z0HEpokڗ׾o~_׿pL7,ʶ474 . khN 0E*ibZC *^e .r8?1m/ޞQs_wmqV߻<3'Gzҕt7Yy_ sx@hӻz#αяtM7o{UtKD/Oˬ bHlfXb2{Ƨfz1yW|5ywm>.vhnwn0"Ff~W:zwQqxK>||7򨇾W?ǸY~wXǜ3| hg׭akk[_1[DTdLͲ $>r L` ,:{L*:&=[K/3;4Ms3#-ʌC$TB:+0[`3>B,ʳ,,d/01X,>KD Kc[;>T-[BȃB,D;@DTEdDD FHIJKLMNOAWDQUdVtWXC[EP]^_E:@: Fx kk#74ڋc`ѽ0؁\+;#-@4BIP qpwxy|{|G}G|Ȁȁ$Ȃ4ȃDȄTȅdȆtȇȁt3"g}ȍȎ>ǔudGTɕdɖtɗɘəɚɛItxW?TƺcFs::8AF08<K6,44#NӌԲAȯt~˱$˲4˳D˴T˵,@AA`"d˺˻|Lj˽˾˿d@"4d=d 8lýF`(1p;d?>17%a$$M <Qt͕ׄؔpˠK,KN@&s,5l$LJdCD@:˼r?ۅ\^^ٌ\͘ [Xȅ]8OfC7pȆl( gxπ pG LPp O"O^ T@*XN8H0֮`@IP^_(^%eRhiciin`60V0%^Wnip`hfVjYnլ[5o@E5ȁUp^0_U6/\i@'X.q8epCGDWEgFwGHItGd`t\hH@Bhd~sX_[Xmf(SeN^кm_[@6W%(^=ܙ[ַ\o`؄pbXde_;dXb HAw'g0 \x6obȇb(td0v`nWn-o0b.®WgwxWlVpV9>U pA C}aBO6e[h&p0*(W%p S(pNKL5 rf+2I@ `^np'J@:0s`-X3p畇ej@sPbOT 0((ǧel@,nx2(bȆoNPwkb(٧ڷ}D&@T̃TDthpfHm^n87B0^NpHHh``Xhkax@N0b tm T @炂n2QB(e*P2bAD(NЩupB1 $u  RvNN`ɲCJJRC˜s ֬Zr+ذbǒ-k,گyXWK.]zq葥OFzV`XK1ȑ{eVXN%JG! "?il.2f\n$ŒoruQQu(Fn!nf%w\t96휎+X"Uh0cS)֜xU+T"q$\"̠+!0x P9j(I #'K47@ x!!!$,zt+X!lbJ.XM"Hb2 cM1DUZy%Yb)3x vFXa^iK4XIp-xrB%4,!98PG̷ Ih,}81 ,(&"5 CL4aM+TL+ @ J76 +!2H343!|#L1}ؐH"7jpRJ4,( ,% &2 1#) *6DS ެ!A)XM%/`u1ء0[uW^{W`vXb5xYfyh 2kmղK-ز1$'/8~" :<4S!DF)D -15@(L1D35݄7RB,#Hd)xAjWN[p/~PAxCŴNL0~pM l7@ kĴ $7ӭu.R 0mq1ARodzmQ28jS cT}Є1GX`7Elo~hE0Rm1!lC̀xd[ Y"؆k c V4 -XApGƂa yU&0^% '0*,<[dJ2bxPB7BYCXF1Y "JN7sOܕ%yo&xs~cD@ )aA?Nx,EcEu5A0,Gt`3 XE$B _l/bk<Κ/QaCX)tqB4 &C؀A@8V&QFFD*4*B 8H  ` .RK[ѾM !Y "/(r)W4á&u.ܠ0\/D \F 8 0/ M%!D4X# A~ H4 0D 8dAX% LB7% D@\,Bx,!)B1KaB(,B D8At݂/±R,8p 0D5㡠0#11#2&2.#363>#4>#؂۽H%. )B0L) vTB `-P/PB. X8,#/O4lS,@X1.B P24C .@ R C-4',8@% B)8b@YH@!ZD@ h1BF&  ØE#UVU^%VfV`0֚0m` o}`\ ftF-e1B+\/F,5,C-C)\/݂̂-8S4C3 W0Ԁ)8-,C4)L)D7ԉ,,|QB4Vif)xC7Ba 1D5,Ђ1,ܰ0‹0 jz,;-̂,\!SC0L S./4U=y'zz'{'\WuB/ VB,8fH1̣;4CiVchB\+pV40 H- 1C-d1g,CrB-,22+xH2x+,0xB- .BW֚ICym1(f;C)\+,D7//fC3tf#/-,ݐ})))Ʃ)"IXĔnoceF\BWFwx-nP(,P/,>FC/B-x?/pc\W0UM,b\-'}'橾i % [ •\6\& !]z*d-d.ɤL.pdj-|j Cb*FbPj+8±ެ/̌Δ^aB*}9+FR붢"c´حނ.욫«5mlt#+r,,D1̌y//cBR-+K0Ԃuc3•b}R7z$x K))CmȺlFάe/5Lņf}R/00RZ-YVV 2m!&A\ކR2k/):BeBޢ>Fŭm7:*Vn*gFp-S/kL9Bj1'w'0L-0͠omv*"j ( \!.` 0! c<* '#Ӱ 2̭'16:2C+|~0b#PG1̜:3;;3<<3=׳=3>;3o0v!bo̞1jŁV&4u-(E#mu]TnjZrц+ko/X_~M4N{q- )2Z P-'֢lrB/Lp֔>lzVhqשׁtPHúl~JϪE4^5^ -XS55P3/\ !BÎntgHkm]+vH5g dtHKEEh׍]kvs BOO(+TY)Q#[sehSGƜm0U-L#-.-;l>ovo-*/q_f,vegC0-RzkRFT[007C-. $,v(v`^Xb녂#L-ЂB&%P+P}`Rmqrm s772L!䁘99ہ9׹9繞99##''y$ [GO:W_:gzˁp|:ww:{z:::Ǻ!4-gk(B*?pwQOy*q_SOזws¶S鳵_;go;smn+zyB0^t;۳166K;׻2^Ԛ5#( dFO%+>\ץ?<O.(_VM0C2[W|۫Lj9dw~>KLw7?N6OM0($d'n[REI>ʨ"w2h0Ͽ$)???@(8`A 2RaC"8bE1fԸcGA9dI'QTG).&e$jԩWp V 5p$UiR[bzE(Q2=jh .?;Ǐ!?\1dX-\I`$_]wջo_\7鿦'njx)cŏ8nre˗'Ƽ3dy;=ytiwK6)ȔYǖ=vmȏdI4I˖PFb~ujիY)BX@ʞmX)ehpyѧ[vƦUݾk??[~vS4_MLp5Kd꥗zɒz`)͖*Ҋ+N~XazԋQi$0>Z4KA 2$H!C#L|L(;1&Tai,L=3RQN}=Hh}\-0lSRG$E0M}ČFPi W\sCY9\JqES.bza%{Z%{ eV誅 TrC.cZdNyPT1_+^;tb3!iHڽuE^wLΎJ O,͞5I,ˮ.,{Nܛw>|ξWc_vcr-y$ADa Rf a^R]3 Obٮ/7%v瓍ݽNZI AOKà1|Oa2(406 V/>!-æP/e#.ʋ 6E)V2Q\J+;.>Ƌ+2CI" v' ʊSCсE˓r5DR2e'J#zS/iLgy3DH%P4?iJStHmFjSBU=IVj5fէWP ‘&!` U" 6ѹm$v: '(G:/b.bQX"-2XТ L00+@%-PIgcAXua-h_7[fIkbm:Fk\T¹%D} .b]‚Iwi aW}fe[^z_`Vo]x{^~ka)~0# `0Øo%.p|3s8-E X=p7`C1rexJ.2 `G^2k_7yrMl+='y ` 3[!4$UF5]vjcZ.5a&-_U_o_V`ѶMqaٶm`Ssa a2)H6Nde/Ce}`!J fkҎfSsYW* AfA.RPl &*7=jmvI=RK=*GGAWaVty۶yv@6czqSSlQNR_r3Atr21fv m r?ߔ!5i/Jca^A:(2Ap+BgA$3X6X2p41)Q>ٓѺVyo!Y!= @a1Tn%27ӭ)6@.ǡѲ!a OI0(9M'x5& Tբ!`.€A+b27!`wpM]4[ZN6:Z4:z7)ZZhoww Ơ'13I}RI!1QTd.0[1mBvVHH<;}}GUDAkwq @n!0h]kcRx,io]21)4^(O:Kŝϕ51P}9.]vT hT+Oa! 0^ş ~GOmCZ TڒѠ ?a5yAvEQ=4 {]H.~=5a޾W`'~!Y\gODV/Z`f &/,/!W.~;\;Fr$5:7)^-/!EA=}=1 s yT8xӷ…  /DyY&O6M2ȈٶKٳm,[M̙*S&q9F_~A}4ҥL:} 5ԩT:7npoZx7.]kuI&U,[!sF \:hŊ)Q>ezH@\~93C~0bX)ezȨXZ dN]p@w7>̍ԝ=<0>2ɀCfL|cy~]o <4PDlND<aNH!tKDS5=SNDE%wy*bIaU?a 68VY.VZm\uݕ^}XyTv+,c 2 e%uS .f\:7AL2HcC)l:sQy3\sEBg9I;. w>Rt` >F"5TM?8M2_=t 6ӎ; 뫲J݊{O?84N8Ն꤬wuBN=*jKh~H<#M;dP4ԸJTl\tم^|`ؓ9dY%f[hƀ)if\8l :z(QI2F6dOnyQ#()ܝ6Cl)5>oզHcYJ'P_M{VZ`<8էQ錕fC`{Jq{srO'.4Q8H#\&oG V"yp-DLqN>:P 0 '0N Nj|I͂=6/6Y<Balҷ46cN^}m^^mK2[2M p: *pq+`HۈMC#6rp ϙo7lgqmc+Ѣ|*?2knlR-d>8q' ʄ-3Mibx#zp@._Ǐ(&qg'Z5|4j];z(uLej1C;3gzEuo1yId<Þ'UϪ3u 0c0SB}".d\ vJFJa{s4TA1T`2 a؀Khӗ Ei֩hm:T, @k=hc@':튠pX#G6G Qdq\++Y8qbz KtitYEy[RjяyȮdLL"0!kpI}\ 1J9*3!ceiD 8Vw̞0cOVrj(Z<ت`p;^ZJU};UWG&Q x#zns$tQmh1nw[d$5d Hxd&OR#`|3Y_q/&_bJvJa` !2yCk(,9acG>0p nCMH T@\ E(2` ;!.Nqv*b?-=^u mp{c({n7)kXOݍ Q$) vud\,zb`x`OVb@'AJBz- `HaFJ XACN¡t ܃c (5 46Cꔱd%|` .(hk'ub;uۻqwi`g}?U{W ڇ.SZwmB+1= CSG2V( T 2ā)G`'r$vsy'`C%PJ o EQtncFhކۓw* c D ђqC7A'xсXO!;<) y 'g &@ k PH HQp-C8hdhsh@ 9qK` x P | E נbp03. R+5ClAqEIv PnR1 ~͖v'Il"enȒ-_l?p rtm ;BJ-B9rca 7AsxzX 6z1 H `0mڸr{ 4gsPh ='(E @o!w'9t dۃh95K@&) g*b :IT!IQ 9l!){ .(0(EIiim A!(FY)8 4 6V O% HT`~q @  PH$*B{T!`P ;$ Iop@ y0 ֧0Ø&%ԀX` mbmZ*>>3 EҚVрO1o rrafY$"D@&)9.`1[q;n5] hw`Vps'"E(v>E@4S @q Ip:Uv@{Iny{9` ltJ  :o0 E AA :,bh ɈY0  _p汣m꣞VkD#ϹI<äJa?[n5kPbc@wʯ m*7o oszbvnI12 OU  &  _N$rCyɩ5sЄF!( 6``@ 0\@ r _㪢':;r`A` %oqP#c[bDk%H dڶ4 ې ҰPn% !,VaJkm?pʊw:6d)}jZL7-&q]ۀ槪hbWU)b`  ݠ ` ںd`B1XE!(#e o0 p` P wRmҺ3]` k@q0OdK" LJ"w뀖밈v۶gu`~TM2 ! 67ij+ -] h x{&a{; :hRSy0 Ok } 8 лDp{CRqRU о `f @ Џ[;V '7!LH!ɒn7цP]@v dՃAlZPP [ &0` D'b* >lʱ,Πh;`p7 q]i#f`C0 "' HӋC5@nX&] P ` beAm' Dr܏r &ռ͒ガ`0  c@c Iڛܳܠ4OS-g=*ǹh_Zn"5ұ1 _ >h;1H`a@ 0 ` ː x5d|BHpBH.C3` }oꥰ SA0yP,c9n @^ ꀬ lxy p+ # ٤̣A_˼N>g1Zg1B331|Z#`pzH ٱʀ @DC~`%P &  {| I`&`V÷~27"@B X0rtqFT k.2i܎P" qRdo˼nMqR @UM ` `5n .M/` ꩞ sc o| ~g H0klsnYɐ QQ "ٸ\(hpgp r]'yU_YO[_x2 pu&x;q% oHy,T @:q͛ׯA9ԧv5C#6I'`PH9OAFt-e @G(} EԫXz Cv [t7E+VWH#F !b H0§]]ddc%/mKmt!'iԩM{v]4֒K,q`KlXMgPXɕ/g瞥o˗/82˰{;ŏZd(8.H2c*}c$(epik !!c 0d 02oBc!ɤbZz&3:Rj*QPI+櫰|ZAdA꺋Rca+Zp PΨwZ2dfx HSmM6G~'FdMIg7qLP3UtQFγBkH@0QgF;QǖsHJ|A#lQTy SZyV[K&` @e.za'1@c/:BV_" iNÕ,y1fDjUXlG2 %i g 49R-. vD0F0D*|)Mh(%Y٧DM6SVٱ7GǢ.|Pmq~S5 On]6zGI/ @.'BΎo֓DR v2:hgZDV]cUjyqY2b.a'5Rgj]\[u:JqFuG, EQTʖ{E4~}8!,؇!IZ],8C' d*D`P^ynyWh!Ԁ>8WVKfphgT$"/BڑQͰ }hGE1x qhsXAVY*Ӄl؇?!Bl}ؔ#&q+ ."C!@h(f%,5Ї,k3G!u&I^2d&9c nT/Y.Jn9)++m8! ɀQiH##5d0dN3Hoܗ (J5("jUݱ.x! _azx ~xf-]ZKJRC2NjN 5zC`d#7FLJdeNMQu"v5j@ÐьT(0ĝ AHB=6΄ FB9J>D iG֌c"E%]R~#@524OR";\ݗԂ,5M-S# YeoDe+81k `CVNrnM 4:~8U"Oo!9~m2q6biEv /҃ @QFV&kKĠZ$!FI@B}tR4q3A,/D$e9$@85 BЁ8!C(3QL߆ȉY5qQ NG7ajONr\pfv| ѝnZL~a_0uEM,,ϑX.ߺEZBAg4vր|K $fOÝ(#A[A1?Bч=;rd8َ"ǜ8d1!8~<"8H$4 }#4 }S%8j9qPoTS?AX.Y+1:$8+H4W0ӐoH\H&?~plH!PRqcdMBm JT=}al His>^nGyTK0cQd m)0,(_u -h [ ``[=E6%hM`! {RZ8]ڄ~U3(bYe]dG"Te ΰl32  _F~h9m`~.Wi9p}!yy]%qP ˆctǸa4NǸL[ ѡحH4U`PH1Qg+=IÈj{w[(gځ m*e0`+QZ27w vhy(_A89+O/8h˙fDsfG.lʣ8}sJSF@{J5\d :;c#1u> ~5d^ohW(R(SzKFNdyRE&(yuLoF^xal*AȇwmڤS2!Юx} S pfsJhJh<̄SHu>vц mxmXm6poЩ.0\ՓqpŪn" /0̲uUxv0UXxoG5F1x>ylvv݇X /U)Z:ʲMqT6ܺaȈ>ss0vtv4 , L`mD0έE#%sW_pB@?'Z A$Dc#O&Y/B!@/r1F  gZ00O?pvy@WLy`(".~|uk>x#t5tSD/cE 1k|4sXk(j2B=jNwxYTq[gtH^Q*k UqR5M(\aÆE0:*1cEBV-idqkcl (yxӐc<.E}mx%8{Puw?Xun(jK0p)0~is_| ug^㑵x䅁Mywpah2hk g^1hF[Íc49gXpȤLKv0(?駣:r۩1"+ТKC;TqL!* 0EuEJ 57\=aTn hp! LeCɒ&^yC /; bAoM1q$~ۄFCu @nE/z .%3,bwlpH3JB,ALCK"mE2 &Lk0-j:QsZ*ȹ+ _fh&mK wk#fN54:'Fs2F(qH&א/>O#kI *q{GW^QbvYܢ3B`d !23bУUӽM[X,`NXH|МI Z4dh&ao-,P?0ؐLVb[gsaц5Ů4]]\?͹,FM?,^f:06*҈G8f?9G2nӣ^*uu( \􆪽 tH`R\fވT\6YJja=W6P ATFT1 3ԾGk!ue#WՋx,&( OǾD qvۖU4PYC<5A0mfPI;L0T: Q.IEr8F ;4Rٽ ~]:ڈ(>WHr#J\ތ 2cHU/p}_r9BD(DT%be,"q -Ub% > .|3TbTYz+F1,3>Y{ |%UCZq]H$7ReUZUbeVN$:3S4V5b66r@aT]XՀI/fMc*B-.?D!\85X"HQ!نc$CdИgfU:]/mKÎUdC1ŲE@΄o  -Fyf2 L]t4D5p 3qsWrtRgԕ.TZ=&d5jYZ~c86!e\*Hc-8߬?C4ŬH+CTkfb8P>_4cd@dEęx&hhi΃?([dᴃ2C&>$I1p f?d;C'Ƀ2Ri5 LC 8'trNui.P]tvv}Yj6x%ɜ\ID-|F|C-!0 BfՀXqdZ,C(5 ۈMt]ILqĐ2h6 Mޔ%>>"M`EWJc 4(4YpB8ju*Օ-()q:i9@,z#[_}zBUe( Qa"br'Tqi=ȋrCd$ThfYO:@8P賌(gHF[13 `nꈬYCo8ba<404×jl)ΰ>Bx!Cx>xZb!I#=^*?| *@,02 $)'8C(drh&Q$Vý`ÁFFUHaٺ"Mb\'ٞ>'R>ݳ E6Y -pdnr.+Iʃ~tTLJڹp"pJV!͎BAa,L13$Q>k=%_@m/mUD?l*@&Ԃ~"%) ?0P.DM^Mbԙd$ 8r$pHp;`Af`C^ +>֊a.a*qk ¥nv`SayQGZݮ . :BHBTx,3hԁ/?٨ / P c)P>q*C1HV\~f$Nֈ͆o]/'Gsǵ1 WK!ĺ -|,Ղ,$38%WYr7CCIDj\Pd`MC䫟ZqLQqܫ61嫒iH++sC;4p1S%d)LpFqb 뾌M3:H 0<?x<6Ƀ,A-R93L8s-]"4ZF3 cx>_O2@ERҙc t,UCkV_E]DQ&Ď(`0Z3c.̴z5;_6WE1:88;$8a4t6OI^,qZOQXfn)2VvhfW:sf2$e0.ۆh+4WvuڦLZ͒]_@0Cp.|HS1 If.uf%+@$\ 0FC1(0v);G6-#e[N]n]LoOkwhvXrPv~Rl#3)mlcns B]G2_4FpZ{4k)Iߓs562#L(C@*xC|@%\(@p^C,A0CD7EVFAV6o%9z̤8-J FTlT7o3Dr-':~ i嚳yy;)y}yWˣa7]_yTOzWĒjB邜]_ wq3G4 2DtS?CH*(68.>(9SדوKo04l87y$SL%q3;꽝YKŀ" h}BE_s3k5rr_\8WnDC]4t,F{LC}T4| |{(|rC&ߵنKzEO[c_$e:d|ȋmțK$ [mS/8LT :!8#Xs;ʄ0tӂt{ţ`A B 72ykqA1 GZګڳ}ۇ?Zܳw[}L<oUw<Ã6}*`H[c~=4=6[>cƄ*>[>5<˾[<|LM#}%5@8wxOˮrO빣t!Dtd}1໾?B4c2fmQ(|?MBł?D: 4xaB5TbD)VxcDqfjqaH"7cJ+YtJ3\,)e#\x% B=IԨSTŊEVpIz-ZN_"%*I!ː?-@Z0d.[zd2:i%ۦO߸qesa)VU1_)j @@"TDQ=h([p^}yI&%xbxGٟɕ`t ygG1oDWI}#&Xi6^lV{Xx&ai5xg~~7`]V8aI:/x]g%Dd 2JE^r 54笭@s2 3DEhxyTfĺMGC-7xu@4Peh l1B쥄s;u#k< C)ziЂ8B4ɆiZdCzSD8$~ Bi"D/!ΊS3>QZ(F2rQCY.0~_2YIYɘ * .C4MM. `XF8h}ǥkk5<rCh%:9.0& f[r3C'VO'xD <\7d}X8C$C][aYG)~@kG+o@F A̐' x1\Vtʳ+ xXAбh<^3JUPFӓO"p+PI,b} @H# gbaJ|s(8^ +D}HpĈFНVl^b,FĎb0x|^P L*aƀHJ(@ >B)zA2'ED~XPQfaj\">/hl"c2Vd`-A !en6AAހIlLAbޠ`Aakeh.Pm'N^a`f0C00 1ELfP,[Xl2uG1PABa`|! `6Z Wl!pA³^<#H8lF!]A J*VF!H$ÁATVFt!3O 8"Hl33'8|h衈&R䷟TTYV@#,2HC i1Z!\ޥ説ꇆ%0)H /Aช8f0$Q 0N8?պW<3n7L(P,y01D#BytEBT"0`ܹ6l>a(IF,hVE VV5*Z^CbH,LH3L{O?l3>ps:Fs{8c?K;p-w^TSv0I[HV#5Ws74sWn|u 8$}SShJVYnU-KʗC@n{߮;`{y{V[O`L-@~.Wo{W߂2.Q }/o\uߡsE[J)>UL@o+_>& u.=6 )uЌh(!MLŸiM;Ld0h%4LXC1!Q*MBޓ`.S `WAƍX̢ɵP&< }4Ќ5!Lل8")C-AT,ڒ< 5}ۢ"Flȸ}Dm³C4,#OxVI7$$wȏ2,a:f)q8G4)X|$S`ZQK#Ijf%-6Lrs5<>P℔GO}&&q%,WI˙8鸉.-|,ĤT=QuZb4h͆:#'e ->dY#iMɳmP&B+a<۸3:Qg|ajiZS4gVl!-)E@ ԿnAjɊAPjѴ͢0L MQB j:~#D0i SҗЕ&'8\*p2F.\4ȃ&:Q0OQYLᧀ d'Kٖi`)fBxmq4 lIȐ J&oM?d3!ä4:!b,&(qLZ:Y8]arA AX!ʚɸlIS?3lcmYIcdq-rjlI2kqC&ג 6L(4Ą4sӧrs(P nby De<7+ }^y(ݬi`( oN:dD;Ve j+xcrȧ۳ NxGضaxă[Dd9ְ7硍5QD~*KQItV  J[?qO>IW# X@1 ]X<~|Ї+<h R :[yl %)T, x8 (Pl!ZXR0xܢ& (Q(F80(}D..a EgN r`pScL[D>} @ Si>8|.ʲ\|}6L-AȻ=]- SQ5E}_`Omܼ|ji1p-,r-41˽B|Zs-ͭԸ K] dŗNҠ\>F @ K]ڤ}ڦڨڪڬڮ۰=۲]۴}۶۸ۺۼ۾=]}ƝȽ=pGuH_< @L+ }=]}>^~ ު [1ݗr 2]dRe* ">$^&~(*,.02>4^6~8:<>@# N  .FᘲpN^!q!Z!XZ\^`b>d^fDz HEI)ޜzX{xz|~MB9Ne)<ٔ^~阞难>^~ꨞꪾ>^~~2`RX BB`>^k&6>^~؞ڞ:`ҿ L\ BPB<$MV~^~?_ ?_ ` ڭV@=#@ > 6 Np m @ 7=HJ?on P?KPBC_7S_W_b?uAfN@䈐KG`PPg{<0@m?__?_f cL  b08 P)C0@CLdxX 3o [3:N9T),Y]`4FNdNaC3O1)Ī{\Ie6a   cL^m QD-^XA-츂ɒ.AG~6 u`rvX5:?aR' 55 <" i|H 1D9&'1|Dd'2ي8H!L _t`0ҳ GKYwXg̘8%cgZ7ሑ5MnM! Y@&4Tw hދp4j6Ga)42 ԍ@ "&JYiA 3=ʿؙݔ\+Ŧ Snp!H `)hG?qNFFp8 g)_")WEbh=hqbT eڨf}Mp•?`+ȍ08p/_+"ҐEAk0T$ )>ϥ[_c{)j[34)(Zftp>W[1&q(YxEB>1*r0Y"m3 D5R}e%Q2&dġmc Ck?'?:}3~ t`;8hAEHA6dcM#@28ဈ_Æh890Q1eHM`bQH1m-v> ;bRrB-Sڊ.;]~@dȠ5 k"jo@Hʊ)B?D\wѣkrKs P- EQD{)({h_ܽa! JLq ^ڌ hYHPAx@O3703ȃk){ d M*B\1#xpX}Ld5A}H3zH?0 c9BTDFDK/CAlk( $p )5gdx#؆N |;#C"N|Wj(pR[` Z1=YȒI8dpj|W` _@@u\ͰEЌ 8 eh( MX),duQ(qLTd@3izzx`3#uMkψ!5l;yx83waw`qhưh1EBO4M5"\tOC HLhK fhXA *xQXbU JU)`< Iiǐ2 YOԮQq!~y=coox5 4BRB![M;"2۵0iȹh1 xQ ̉ؗ\qR'S@M? S;H ᱩ4%xC=b<0"%!Ư kg{Kx ARPW `bSjx+YJ;;ޫՑ՟plLX.R.VbLA5ɜeyhE=y|hk2u?͢,z1'DS(pZr׈S>C1sKEPX@5Xp ،܍ (#hZj tSV,-̵a2up2x `$X2`j%$b2✂)>9A_ J  y_C c>F[/82DL|ܫ ЅRB:e8]~І^A-H 3RZTư]yx=I\ #鵵r y`&ӊ0eg4ІN c= ߑ1$@!_}8Dhx/I?c@LDA=*wZsyE !fO"X2ZȪNoOA=`|Q~ wJ6i3bȇ|m@8$ .Di4 4kxapQpUgbq4?1`Ѵ= ;F 1^E~Q5τ!vc>F8A:3O[Oƅŏ8whZZxZ0`pĭ+!` J8Y gaq 8vfoh25p_Y uf_uУ Κ髃I&YݎEhksk7 <螖Mn!+(>C(fRx_ KXdxo&Yx(C^ F# ZY#v xb(vkʏ2#l\-@gNgjR!LIxk$j 2Ծl}C Yh|gdBh)[+i]-cO^hmx5X`X%jU@8*+e0O q'اH#fə)cH 㞇a2ˠS=Gk= 2NkwpVdD["`0R`XP 0T0߃aiilǣa(GmA8ib (rC jA iwP XWwXD[vXuh8>/7e 3N~_Z3(i^s$F6hҞdvD/IJHN&(0! sl_\x j(i@q^ZMRvL^s`t2( Dgv%Q \o-mЇ$C)F)|Hn( >9؞̳ߛXuJH,a7v=+fcwiHM(RHhZ$$k@C9"t@s8l(۝W,z(X4JP;5n(N SsjK.)PLB-cm4~8U~X~Zk*1筷ue fgo)(0(HPvKN0 3Ps"#uo= v@ШZ'~l gй^T9?CÆ8~SD.n0fn yҥoqἇ#Λ/ݿ C#Ȑ 5iG$QTr˒,[fi&Μ'7)2L?}̨#ФJ2m)ԨKe?j1H0RJ-\O=˧bpٔ7xdC˃OTdz'Β{ NEכ5\e aP@R2@FB TlYxQl! 0ۡ{!GQaXDLcioD#=6Eh򐱌wlFmDGmFͰÛ2W ;Xc40ws:dvڍ9O RGPNቒ}5}c_~6'*P# bZaE)|0@1ZN%37IЁ XPLΨwxgό?wy]`vi:0ʓX;`$S6lM6؄v43.1`.>Ln^313>HNbM iԩGby+W M;|(IGU?D n?1䢋c4Za6ḑl)ya(z93+0n:&5 E;X6(<̓-8Ҍ3#MdoF4CІd,c̴z gĕ+L#Ňb5OG?&PqVC#ʥ /NPC`8b,$=J:LF# z3 &ksuOO(cͰ߬eC 2p[;찳Nipʵmӈ{43uxXǕѮe8Cِ?b.H:~cV)8qV+NlqsN׸ln|6tI1PMӊnj! -`'/S ($.(@(0tDW64`a"wT!C(Kq CcxG< Іx1!plQͨqh eu1&|ILLISz.yR8n-(41G4b!~J~P +L Z&؅y0d$xS]#-B+(x`N B1k20+ZZe.4I}B‘4?srW Hw8-Jl#j)LH&.D<%w ÉF`l5, U02N}*Tky\N" @M]5FW™Д?pdf q p 8 p$eTi(Ph&akKh6е(!*8P,$ u(lh*ѴRԢ"|tg$]J׹xeۘi/)_C/:LFtDgX َxH*J:r׆ĥ.Ш`P",c p^k}!`-0 ِ7ND(b!ќ]%IA >c@ ǁ`C:+C8ۉMylC}h[:cL}}V00n1>}>F8%'# ( q`xPG ;ةunnwqJ=L$0Dbq14I`"wr F bfy`%t@Y5&fA,␐X*N],B8ۺԕc0<%8A:;whCK֕<-2cws[FN̆%LJff~fҼ%3ƠE.HFAp-D"pQFIXPt_`QY=%q: Ib̤c\˾:\9 8;#MiqE*Bt&8"|J6!@؜hzƤCҐu=ݩ$.DU ]Z{D6ePF:Bo 6RBf&q o<( O~yh5 0A剂eЈCHsp_?(= aetxK2ecC.ٲλ{v(oJ@͘v$|{S$~g1%[HYK2B$B2H0B1b]!(虄F!]TIm?2H5AC(`niE E-AX8 gZXoî}˖=?PSx6d88T  >U8,4,H<݋i?N?8F} }eLR !Ue[YB?L;t@,pÁm͝QP682KITD& . &DyHY욺O|IESAduUZ)_EUH2`^Q,F֑E>è<700VWRS%uVu* HV@zBh؊NCpCx֕D蹴AKxDj,D~hNl, -5ܑ:x 7Ibhd= b45DmE: ? %ÒH]Fg84g ~#Z?C>H2K,GFx5̆5$?uY?Q2(f'!:[ƥy*`h%- 0@1LXaֈ(kϺCJy(ILv=t =C=ԃ;NEeݛ~庂C5HF6$;?0 UM<KQ~@ =!¦ې8lgiPc7AB |B-I2 ߦŇpC E2Pe8 L*蘎u`#8LCȘ̃ᘖ.dnd>L!?o$YC!m1HU%Kbt}4Xƥ_.oT*þmFB@\:jR:@%A8D:!.䧧a?86[hA YGRZMhn١.p.Jn'f6[<Z4܆mKC"SD1 0 o!,UD[>ȧ8\%eC,ѾQ}LTSE F;aBjOȇB0Tl1΋1ؿStZ 1geF *Sj&d*n .It 崳"Nǐ24L#Ml4Pxu\u(4hDCgK3S,2t@%1t@)muPY/=CU6d}H3gZ}bc'285i5^QY,83v tB6 rgm$geO6q󈍵HĂp-+5gQS@0OWu_7v_io7w+]B`Cc ,D-k/E?0X0T|8(Xo+L@cMuuQ tLĂ8mp6T`$,,u7GgKD ,Dy-S4}%ƃo(guޅ8V׸8/98V5y'9/79/ޑĎCE8wy8ȎOJ84y7,TRLjNa(+EvgBx܎ARK$X5?:GOz_ASx0aPz+'GEݻh/>W//Ekv_7)s>g~R.3᫾΋ I>>~R>s퇼F~}??5+i_?#t`f5WA?чA՛????5???@8 *D &TaC!F8bE1fԸcGA9dI'QT2c ,aƔ9fM7qԹgO?:hQG&UiMF:jUWVukW_;lYgѦUmϧmƕ;n]ջo_&t AqquK l+_Yfl1#@w\oh&&1q/ 5@P`(Eui|`BG3X@3PjZ#=!AZa8%$ ?>_ @0`?ť]f-P7*+`8L>$ 6<cdw (B h'oM/ HD =aCHG;A2yC51iHF@毄(L W@0 gHư`BoH"HL߀INr"T*ZqXJ;"!C^T xЂ V<#q6n'Y<7^d bHQC #| XX"A M@ 1 ; (GIRL*WV򕰌,]^ch007%ҠFv]6PT6wtf4gҚf)IJnқ,N7NˠFd(x,tOaL F ŅƁ_KZָεw^Mb4QvC1`P&E/!Y^X/prNvMz.5!lQ!*ʢ帬;'N[\ȸ7{\fEjQ[X Z!xv9>UFU\=Evrh^8(4! PآcЩ-$M Y4c(/ܬA XF-J=_.BXhyzÅ.v\pNxϻޅBv=6+ڰ,`v؋Ω7{GOқOW C!a㿭k?ۑayS+H1hA a;O[{ L"? UX~ʯ|-DA \O_ϿW}Z|g7I w8Xx(׀'X&x(*,}U|a`ajtW&s2CNe P~6B8DXFxHJ6 +Dɳ ٰ TXVɰ `%Ad)7tC@R("/!4 _((!  p 8.<$`(D)Os؈K6'j{{Rc|Z  q wfxh[؊励5fWffeS @ I) |Ȋȸ،8Xظ؍8(Xx xfQze7p[ fЃ 烧Py ِ9 PW qG 胩('|$I.qp r`y/ٓ>@B9DYFyHJLٔNP97TY p!NⳉZZ `YEinǒep=؃' 'ٗzYO5]#ْo٘ VY9j)tF4H3es К29鐩@ )%y9|wY y+ٚ9|ٛI9Yyؙڹٝ99'm) ЙIk % Y:ZzH  щ ̙q YɜYŹqI$ZٟJ#jƉ1 ٟӉ:<ڣ>@B:DZFJ+Z'٣@Yj3Tp8PZ5Y ɟGZfz pZ0 qZvIKڠy隷YwZzʢP eYYW:A k P gڦtʪબ:꣞UP ٦@҉ : ꫵڬ :Ժ1 YX@(>G x @ 0ګr*zگJڪ; ۰ IP** : CU  @0  %( 02;:{<L:N +H pW& [ 60ktz6K @$el۶npr;t[ + @ @K0 uu  p5PY @ kk;)Qk}NE  o* Q `z@ ˼; Z0+I{؛ڻ۽;[{蛾껾 xP Lo 0Q  QZQ0G`P PP ˾ "̽+kZk NZ`-l\0YӴԻ '] p`#|XZ\{ѿSP L Jf @0 b@ `ˀx@nP }0|ŊȌȎş<;<{;N`<'u׋çSjjUT>YP @4` `6 ʣ\2©<\| }f@Ƹ l @o5*.%I~p]_ k0 @ ܵIP <\~ϫ[]Zv|0 ,<]uh apvx0 Eм!5 6',.5+ S 3J4 쿺* okB Q пe  @/`\< &grՄ)9~5lmPvN5 vөs H q @ k \50Evڢ=ڤ]ڦ}ڨڪڬڮڰ۲=۴]۵ՠRP@UIP 4PPvౕ f@lQ PQ0ˌ 0ޚm}] ,[M S \ N0~)Nuu7[Eؼ+ {l p ט @2>4^6~8:<>@B>D [Y e| f|ZLx\@npZ4k0IEr>t^v~x;^$oNo>  a NuZLéA' Q q Ѓ5`  t♀ < +ꯎ-뾮ߋ>^~Ȟʾ>߮ l ˉP<P Nk`,JSP QЦ[P ߼n2N?__D>/ߠ55>p & / \sZL VE /q PZɱn N뚀 ; >>D_FHJLNPR?Tף@ 0 pꡰL P @ p ౨ Kb^?_,>nB]` . a0 $~մTW?Z߲N봏?_On ᔠO ,^,N P` P ܠGŎL\DԈMK8uTТGĈECI(d+*Q:Ip.0C 7C?1DG$DOD1E2 ϒ=#)IDtRıG0%OTdI'2J)B+ĒۚDsJ%I@OH9Xj Fb&~S3 )RJd0pDJp\ԐD5q 't,/4SM7SO?5TQG%TS7=ITeuUW[UWeeBHEt|%A<5Xa%XcETie6Vgg, K ykUU -eR^y%\teNd<܏>_ST$CHQC_]eJF8afa8b'b/xd1IiRUdUK˸e_9fg愉Mu&XugT<I 5RR9J_qkĉYVev5M,kKt c( \7:0SH(9tSd; fF{yE%=Hzt5iN_9Λ߇Ѭ\Ŏx?uGb׭sH]y$^z lbFvlf7φv=m`b+4;w=nrtuսnjwl: t D#^1ez"f,8>v*xyZPIPCkHG,1r'GyUr</ye7yus?zЅ>tGGΝ*lWR$yͥ>uW!O56_{>vgG{վvF: Pv!.u^ o1ya<\3ꏇ|K>s\|5yw>@ <9v󧿼Q/}eO@; FRzwuF`05'_G\D_zE0BucwOy#aw?<ʏc="3'ػp>88 c @y9qPSl@l L9Æ $9:Ӌ  sA@x 4/= Z<:3H&R!kK<>3 dxklȆ£ӏB{{gH:- kx27899g;5]8IY3 :xH9.̆.D \5-lěS;@-|DٻR4SDTTUdVtWXYZDE@>QXA%{YW'H8,ax"_xF':Yp">ZF[)^jgHigR il s4GuyLR iG`呆il|idGl`PGgxL:ǃ,EzykhHidX̉RxRgSȗɘəɚɛɜɝɞlۅ]H']>'\x2h8_Z ][Fʃl(i9ujy4CpEĆL`Cl3+KtKě+KxqDH-pPLt=[̠ES3S`;aP9Bk3`2h` YMZ(a#_>'^Yx GrdH byI{Lq0vdX!幆$HDHDixryHsyuotpІaGduКJlC 8Y>8]ʬU8@]XYh$PSbxH5%@Ԃe[_RO>Qۻ;~h WXtOduZ\=t,Gi@hp50oDHv0s)XuLI_PslGbV8Hjz^u EUe_*%E;@4X[&`'f(][f@8p؇Duk Mh҈faxVxR(,E VuHH6X['M:9O[LJ1e~bD'XUPePedUH^vaЁabd+uwxyzgBv}~z}مfo@YCPZ05x8fGVnUh2Jl؇ .EbkP=HYiXYiiV@rbebXe.F`b (dmw:a`0P%hssW-ZL@A(Cx㺶.&uM߼2&[3A4sbr:+8Gښd ThPDpG0<8TK-d'(OuxRH%h JHX@DI5\jsЁP5NP( DM'M<=0f8-X3pnpHmbV0'HbMT 0e@ x`e ,o(5XWyO`, u'qwG:HYnrr T؃:fUR U_N?pJ5xN`MW@OpCm oH<L opR(b(xPsXs>g^GWga$;HTW@^PkX 0SJUU%V*RxI78mDb.V*WXepUpT2h=M ^O?;XpN?aza_yr>ބuVH؄V7X0Qs xn8㍁`Rwh5RdȊ ,h „ 2l!Ĉ'R|hgFv^p* F,}5JիXzxM\:w+.[bTEJT(P&=zh@bꏬ?xQذZrRKH pK.oRf}.8pc[B)%]3ZDu[1Ǹ s6 9$ȲԻft.ĖUd# 0i,BCַ[t ' 6(J0t8GE ''Id,йڷlQ ]$ l!FLoO%ˣxԀdm S X@*@ *B!8Ra: 2eFMd/ .0R-@I38a4 02 /q@ $P'D#2DPIvLC Jt@!k 0 F4sy2  d)N8qx*'Q)18A5X0g8'ʲ,i D, &d! ;,{,D0&ΐ0NH$J,L4لSOSPCQIS HDLUUVi #cXd‹Y,4cYE^J`& (D-1.!ed!Ͱ))D/B@LuXJxP}\ zI7:! 7tr482&tG/D:0hbA+81\7x0^l]d 00@ s.Q,D#05,+8S-A8SXK/Dr780pH,xX$aA.b%Yn)y[,I%PBL4LDVX$L$1,%b۱ 0Ѩ+}+o3nprppEZq k_ʰ2їi( C(B Dר<^4@d$&3 2nq\B! v=]^ZW2a*h]|`A =HbzX=bXD<6,xʊ4P s-JhXL1HBuXE7XpPT&PhNO68l-,X'3r6a=,C6Vqy]+E1(a jf ,iZ{&6hANQ'n*ʅ,0hqG͈* QЀ B#?2-RmH 0 <p]ظJ LpLOeap X1EEF,%g$֑U4&b_$fȢ(0C0dbӰ1\rf=+ZӪֵn}+\*׹ҕ/@AhӬ/Hdv :asKJi D%2A5YXE `1 *$P(.M`h.<< >E-'a&" 쁨<[NPZ$HT:j)ĨL _؀1 ^! ',;νŗfy !A /7SHF%B)"2,v^b%+MIbp=ZaVrG fK!S7D  Z@f fH<%PM㼡* <NFn  45-a[L_0%3ܖ Cp!'5+R!]]bpٮ|e,/XQ@$|UY"HLxq @k"Y .L\De !;*H'AY <0'0Dtp M܈ĂT"1pQH4*< A()ܱl@ FZD# J ^:.f?;Ӯ={ P1l @|x `3k`  11r@F`7B{(''MgG ,Y@) p !sEуg -\4F7R$Ӛ ," ,E-2C0 XT"&"."#6#>"$fI/L0pH/x]ܑ .Ă/lI؞}B/V2LË)5-,,BB/"MB42HX C,2B,Ȅ{T}L c.B2.}/6E4 Z,B1+/+2 Y)6.$DFDN$EVE^$FfMPC`` V*a (dB, VT=L.@F.Ε` /-.(,P/&dFdB%>$^f0X/)$L.XB˜E-$LAYّ0Ti1/(ǠE3B5Y3܂޵싋\.^f/K//Y$ c4"BR&;̥[P "Y0tƧ|'}֧}'~~'$ @ΧGHB[a`I$.0๐¶$# ! BLffKnf㿌X0JT#S .EȀ(,$!' /1Q-܂i$!]I8]拸Cri$2UcN&iceBgfΦdL5")z,WT!1NV.c^eY8i ȔQJX4'‚B.4Mfg- ؂8CXX"*檮*ꪀg VHFaQ[I. AJd#("LK[j"h*.$c銶Mg뚅LYX"' 鎺+j%b@)"[ ~.&Ž6"'*N,V/hdN")in -C,+*X+zjJqbc/E0j ,ꝿM$BLjEpjn,\BV^cLf˰&!`"+)c1+JVLVea`-옉c|d2"XKZ|,hdjIp- :-d),X2m`n,쨫N9> *.閮.ꦮ.붮.ƮOhvm.`B,.Mbn|z.rN߹H}~e~%B!DŽ ,"L_Vctt>oX &ֶY,~">h>B"ȋLRWX"mIЭ!L-*".BUN'.,\P.بK2&~.g/ -P..i:j )W"Ђ-ܕB,.4BL'*o0l`tNqqJoD^xE0 0qc/a/I.(.BJ-Je] G-0j`.gBr707z7n)r)]2,Dz,"LBVV#Lb&Qn% 33qfȱq^36g6o37w7388399W.7zzs7{7r3|{׷Nuk(`kKlsPw#r6MΝ}?8GK{O|g@[8O& 4%dBv vD]xO+_wev8xc˷xcD&|[Āw>s72o|N,_97&rg?~Bd‡7$[8yw~V6< <39+::'7?:O:W_:go:w::::?-(euuyvy. NBKbhnEow,28;'/;7?;GO;W_;go;w;;;;L+:ZwǶAhp-eB#?OO;Wg ?s}{}珽ُk9ۛNN((NQ넽ĆjV-O@#?ǿ?׿?|٣5@IԨb ,!F8bQ4ezP@@ G ˥+@Ŕ9fM7qԹgO?:hQG&UiSOF:jՙ~RK_(4 :"XR]4QA\~Q^%G{؋0MstCN<9c3-_Y3˞5-҉OUî_(ClqOt*n8 @G_?hq2ʖ/y6lU+c)E]佧Ol2{k.^ʶD-ùS Ҩ@! K I4PŨ<ɬØ: ꮡp%q4:SDe2 ^pnD%먜i.,$L'tQ”J,rI(ݔL:L&JC<7Ĭ&#MQ/LrH\>DIE'U32lUY!=5[<5TRq]o& b$k'…RyHK -2 %(< 'a,vGFbjAAFd_ d_BDN.A)R+'`B~A'DQg!M49_dWƋo>YaD %~$a 1gDv`1zᤗ&駑 lB>l9N츿&DiɎ켩ym\qg$tr.n_Nr1c.d3VvYc1iVqCHI? y@?l!#%/=`{ U] r!aš`<, ,`C Xc3խf6Nc hƂv1crD#Z42Т1r?1L_fO@9ʹV>a 6.\G B Lqf HD Tt@pQQg9oաmiOնw2i;8!iqc;vMy[M 5LI]j[HDzpy̱5Po2e;"PqLn7Qmgfg7Xbc_r%79P]stw sK&~RdQ7j@V-.|AlK #flc0>$#,nTY' (Bue7O3'6wm='Ie"Kom$cO!x#> `OaL |->~Ј{-^^sm|(C>摌C( 3>ql=m>{ǻ/nDg8u!9gۚ^A0b"A)ebΒR'(;TV'lv"--mo$.MTMgblrsNABs$}rB& P(@  4-Q-5[s*4RL.)) `AO)./"4_"@d"8R'-j^DA T=p! ,T;t[U4KtDttM5M۔(INN' P` ڀ @1 Pk3_ӡSʕ!&a]uT)39V%¾8NlGeO ~AL!!i&AnAj:c"[Rв[V4RUS ;p\)"Ma&vA `33 n,4p/Vf@d7R)PPr'Fp&"P'6&sq@/U!frVkf!b*$WWa! >ARDDE^ݴz3lnl٭ll#3 mۖ{ nqngPbխ0N0e_/(orQlrt;s3t)ObRȀ,uEuga"Ġ$QWgW l 4:J,N!B!Afb B!1/x4bb%t6BQQvۡ 03aa u |ax!rQvLN!&0$90w AOSn&֭HntMܔV{)])*\vWg 9!a<v0A , ~oÐoqSSTc!./s!3 ` ɀ&PXxa 8yf3$;Tqΐ'w =0iDS!jRQ3v@dϯZfA" EFKvOxǼ!`<-.Ph&jo;!>abzۭj4 8~qAAm` T$:5M)ה PaAep{z()96/(OlP[t7-@z"vMXsx!^L` lcBC J쀤]Aa V3 %@nA~v'qaUaYĘc,yҚU޵zQ\0ގ ULarV@`y @o%/c:&Aqe6_{lq}t)z'!Z)}CB!'!L+n h{!n @ Jadaz& DA^Y߀:4ĀEakáBBF\m"0R ɸ ء!q%ޞAaNA6~㵡&}>k)/lH6?*|}ɂ}؉=8Hz.] `f \SNDNd½] 97=a ߹컠a^2]!7'~~82S3~NAǴ*HM&N^b5G]3i-E i *abdYר*nH24ɀCfLNc f]Dmg 7u`UWשK*#%ZLM7٤_wT%5+egFofpnl~ jr5ưh?/'#bXdkE~Z.PiP&R>++[8N Lhfr|"*\Ņ8垫kn;O*TjZ6ȭN]x}=Sq4AHSU%<6M"Cr&s*lc. sT_Ɛ"͘Ռ؅IF^{䣇Lo$HmoѴl1Sh>ဪfY9_{O?㤍˳$oȻ(l@ l ȴl\ N}, *Rp7).hL7HY4,5y8A> JB@{k_Q|Ze γza_`:qilP1ʑ$!%]g<)b< IT IՑndXnR-n#ъMRc 6 nDaNn){jæ +B7|XTX-C0Cb*^4a-T?1 l@q1aD> O95 ڀh$?i|i=9>L{(&N3btV$'b#.V5!)c +qUF+2 GqT00`!f:Ω_-d_ Aq 3c4 J$^ǀ1@{!"Tน rP?׽ufHqa -$Q\,1ĐE-)nK . B~hhAp4%9[%6taF IxT  1 kLc#Cw(=qDN.e¾[id["#}"=3,)ԇl }H+/hQ70^s^MYkȮ1a#ռbt2?A~v4@ ɓD 3]0Ƞme  t p':   @ ߀@9p=@@ba)($LAOQh4"gB` o [ȅ0/DCq^qw{6[0 P5tH5U350 wH95U|>~xn"Da 0; O%>* S)+-`H(*a p0yr @@ ` f 2 pbpye >ui>E{Wp 0L-KBC00)i#6>阸G_ S})<3Lߠ RX`5aWH Uˠ vȚzJ |Q#M *A_b A1M SY*@ a0/გ ` 9$2ى#@ `p& L З P[ٕF< h$΃VIp !HWt3b/Vʅ"7נ d) AH2( é+Ҡ`yi *򃭧9tCJ@Irǡ): 2*< H vSJczѓvZi*YP1 @  i 0 ۠P FV / 8p UT1#2#WxiZ0/i ar[7 Е@1 %!<)T X Vɔ(ݚL`J5w z8O+ PA K`*bku'd h4 EIP @f`   fdg\)$3f=TP/1L! 7L5 @\% `^d``~_:@LaOu` Yh1f֚ʚy,ó/u I WHj{2:KiB zU &P ;Q~1ZA:%t5::Kd[ I6 m~,иA 3#)4Q7!C_1 lF ;̅ע »G*QL`up仚;Ջ"<] |P/z ;9ݸz_Cqh`q]^@S<_o@:<v;yP~Y)yCF2<lg!+ D }5CrGSC(5r>`N/o"7kd<<,<<~1ɠt )m "Fsͺ͹r͊|*<88p w#6 2Tz| uN-|mH `HP P5P 2'`&p[W5 mB_IXop1IgqènQwߠY{]U *"`7)ˋjbVb ,aD/Tr:@@ZRb.zlp3VL!;\ yR41M4~&K`P`PB` B`<]N |1t( QQ1z޾|R^-tU4q ~.aAU .\[NQ|>3^J9N2n꿎` 0R82G$NTļiVߜrB3pGژ@7@J 08h@.];u"뷞th"%p7Y^ 阝5B@O-)hL'dܿ,C/ `^!؜FOhs4NGmN1;1,vE_\?Oq a LMcnX`1+3v.#j{UAr[ lUW^6-oJ [L0Pɀǐ'd">1T6~_- {罼- QCRR%[زeQ-`PK<sʐP6!!"vj;O\xx"Ш%Ϫm3B ňs P߾ #jXbBPbDo/}(?xo^8d^Zy'{Q?xۀm m'a.~Z>C!q!GJJfpxR0c*Bc #e(y+?~*E,[ 4ji\hx418 A b!@F@l\ckm!<(/f32~ (wixIG$"+75sO>iqF:xg ?ۇ;ه'YtG⛏>[(?P?LF@00抂GxGx'BXpva?GBQEqf3>g3w,ouTh 2 bR&XM-|Pemw4eKԑ&􌑦3U];c҆ fx)y:|&w01?y .㴈FUyKzgi0|erIYȈɀxfkSl*Rʟr:"[dI#p 7sH=IyA 2^}Q{נX-Q܄_Pczxs}wyGj 6{YfZ& wtJ:uh?Ty:jkqLJ 9 Fwt&PHJFfmx͞mc@ d;Ne`gƩ !UA^r%5j)"[y#H3@2zD?\lT}s cQ0S6b}QGy|1CbZ⃌dLC;؇?$֎1lHяt!1R=iL6p@eȚ( dC6ifnSi(klѻ!TIf="d#09S xcˇ5܁,uk:4)N)AlH@eBxH#]f՗=Mb?a ( P1j1 ]0-#vi,#"(@C#t@T TQP^j\N""ތ(E{i$_賥*#y$X Pq&Ð ~C06 l*G?2&/(>Of ҸShEMbC.hh呡J↗֦h,x2gH2Q(KV 6Ys ($lݩ~;m , ~0k б1)xP~iQ&8Ck8ޓ@Hs7yxg8u*) > !$#C:<+ p2s'0qY0+)l?b{EO`70<77*<4e2 P@5,Œi؆gZ܇J,i/`wFaAG;)XIA`\/Aq@-eP"2 ivyk6o_(̏BBsyp7x@`19/%; 2$dC[ (0М_?wX6n`a6G@d2kPbo0 B)z+DF1p6S@82* l2 1Am3b%yh2v .`xn=yE psd9 qK989Y둖`C!+,B! x= jiA3DxMhӛj[ ғBuHp0|G`SLL Y ≇bІgPhpAbSMQԛ-,gcArcA  H˼4z9 =3,CŌ v)(H"0$Z&bȲ k86 /yRl̥#.+k|;r ;׃hYR)3#lؓT6{~( І+eѣiuRAH顉 DwyB/%=J9ΏP]G磊3TPdR# ?"8 oHZ UmHQ!Jx؈!G; ˣ~ЈiR" E0)%e~D(MI wІ3ҥP3fӄ%1hq%{0SeQ Vt194JiXaT}=B CS_JTC@PDRRؾf>^T|3(bYe@T}tpeU"!܉`)7LA45 pex|X m` (YIZYԠF/"]ٚ3.ӸY _$:z"Ȇ^\ WAPkHڍ0Az}eq 5μh93ѥn[["Z+(8Cc[ЋwM"M HntO VYE RۂY~@gڣ]EUBS ڨM}K`E/SƓ[۬X-U й߅@@Pw?0:!0 Lf XZ()45_]ѭqc|yJGWT_]v,tW GV+~ӱ 9w@vaN.4h8B^xl1 O{4Y (A)MlY(?p0H܆8Yl@@XT$U% !QM@l$r/`+"Ն_[`aqԹVβlJC1fa.e4sXiePb`e!r\eK;C&=`Vb&(P cc؅5@5X`&(8T._ Js Qdp$$)!`pVdH'iO8GE0whsh0H1h2c d nC5~*:\{v;ԘV,F-Pxgi"p4@x+[Tl6oHi#gMV9n$0H=pk0/k/fXe{B> CYK*䖕[ T zi(X>fwHpE+̑36(eS>-` }~ضow0Y}ޏ농 ;2pk𩚪,28f]du8'^VĩED?L/pX{Mm Vh\AX 0P7 y tPj `UY@pfjPUq+X 3lGEu`/ a ,2X位B-v<euȆ9Tx۰KsŖNXTswW&&ف`"ȃ+ƪ< 67xm4xm<2'0$uѥ{yMbH3 X uuq^O_vaG(xbYe2iN%5mj8u`s0]L,(فq zyWz/gıG=seB+H% p&h…^<1(307`9nyIkK:CL`SnPg^k+ks}@ Fa' VJmh u1//7k iCj@ӻǎql_{CH+C%@CX"p&5/ 0~8p(]8 m'=(!.o4l0kڴMnm-]t+wQrͫw/߾~1$Ao#NX"D 0TǏ!CH .ka,835s5EH 0ȃ<0y5\X'(^z)"" b@!eaD@)"i#N4Al?(c…̆M6$+v28L51<[bGrR u^2\OSީf^ A3BC2ܴ?LCEiԳ;bѣ<ģ֨? ZSj:x7Sv"䧯^$cĎB#D1Ђ ^7'!<\>H?R<2JA5~2fq:('æF]8$;S\Av;?F8hO>,rL_;+rSuhp%h< 5\|R̓6 ZMgZ\b'[h[mЋ12kf 1#-nz ` >H:#2=m3O,l,޾f~tҿ'J( O8FDBILZ1IsՆTӎ`DST| 9S?AGOxOL,90P#H]m`+$þ@QC#\-x47"x"đbC@Dq S_)`wQ"$8hJ~4$Wd!gȫ%X(I=:~/}eR T@ߵj QR-(H.`xph4$bLA*nȏMij 5x͙Lc(> 0@ﲐ8BiiUhžtO'5lǷ5\b);f)`؎XE|Lc-x,$ `! 8R + B1]QI-Gq ̂52#&cM22e`:SբXnNBIِ/1 b,@0>"0PgB@ bEj )o(jyw&pL"! *q cQ'W&nU|hCpM$l&3jmkuۀTjM Bhѓ[}LaRnaԟRj_Y3ȸKAڥ@) C-.YsC-4##@)X^I_Z]f_V` S(^#/"("՝CC1DL%v(;yϬEEN2|ZI3X<@@2288#8J_\NLcVіz9剄apW ;C> P$\Tr?b!sD:h' !u2Lv YAC80Ȃp1C> \U4(aA6`C6dԢ ŝ<Ѐ4hl1%B0d[PW> 5GI] n8ePAg`,B# 0@,aOdyT]r7v#abcq\F |5NQ9`EEhQu`@ &OPЄ QC(C~\éC8L(m2mqV!FcpeGq.Wq1B<]7'C 2@-(dc!y'`(q (BGmS<(wLlό9N;d@>1CT6`CEnfVCYȓ6,ӜEP(~ZSH!1f!vߑyhy(~^"*" qV[XAl.p.VD3B <1xvFerg.>hQ =,C;@T0$ߙZC6.n &ø>:<8*\ 25jJx%eXYBfd;tBT0x, 5 RV\ᐞb`'CdGJǴuSʬ,N,LӞBD{'*[QѕrhfeūuA{uc2!F-8eߊTtMقrD%;,\C^mi )W^%zZ;Q{bEhHV'kyVxl3ENl~4=cu?-/h@-4amcnۮ'ajGdB4JYe#rF@BBٚFde=P6d,gH>!f{[M=KnboNED䯰P-Jce~]됾.8Jn._n#$g!O./(ޘA<$8@-$AQgeD^zo0* Q]In^JVf⯢86zf>LFTK o؎$\ -D`: t 02`BF5A*h O1]pZ[L/BRooa " % w-L21G1NA%G:@C@%6hm""\-؁0tӯ?4;%-UPp#/S3b -A^L:1B1q`*D:Cl*D 0GApH>|BsS /b穰$ sA03h 6n^0"/ID$"k&5Gx$Yn 6bFss$eiL/,]qE::GC*LǏF4~3R2{j}f#G}"="cܫu 3n'6oc5? a:  $aD1x8TxdH#RD(*IAI2d[ KwA%e飔dQGi0dUJma_E=T I,\ȠŌXa7~98$qefky?FJyfӘ.h4q1Yi0Dqɧ? '@rl.'A䐅s^}E>#c_R/miey%ɛh%DvrrǓA䉇e  D"BBGdFjX'FO.n6YDd` @†_y×ię 0'pD)3b B\cx:5aɰZ<:a%U iVxzj!jZ7H8$ pE4@ CkX=ihOf? Rx_dBV 6BP:C>j!D"8"$}x6xl_3 9pD%ԑhA*jd* 'S(tP Fjdc]Xhr!b(Bxrd WhlC! 1b;W2aٰ"F\# Tr 0y$ "7:3f4D ̒e( Cה2QBH~#+Pl܎s,20(QX\ 1&C'O^(T{x̣L:TT/0yQ2C,f痔jdGtNA57QؐTST8,\U؆5PV`vG,p &}Zɇ3MFF,"t L^X3 ^"Q2=#t x\-xF>Zccr$4A e;V̖& D)@8\JOV9OlF#^}VuZCTU,DC,*`$(Dԕi--v LX.>˰Q FPЅ'݃e? e(,@+hA g~'ᐨ5m "P[@F/ A0ѽ D\:[F(HA*Fba"PBl!<t(BQ4K bKU[`Cc6>:Or6k,96DINO4!u%a4PPYrQAzTR)A/ B!H?5TF@T4-{U_qL(5`A:$c`o;ńX#a`V:J"pAb!`!L9"!,@pR =!Ab1=A!dA!Fl/`Ɓ4A TW5_leCL TWQ2;N#("ɳWrCA h L N N@ AR@ `A @` bLR$`Apt`*`J@hAcޡ,4A^׊8$ju-z V.7%Rt뤱%Vʷ||5x.c+7, p!Z8`L>! С \!`:!l  6Al$! Va< R`F `! X "l!.fX@8!dA]a ^l@ax!$U z3ekNs{Vi/E7s"6{E5Rě `  ` N$aT8:AĠ A Ё Xbbs`.|bAՠ~ A! leL@"Z!.@ ,ᓅBAmzܗAU*̀>pZyڧi ЁhXa;@""@TA:ڹ(7i޶ґlݘ{)#;隸6[ b!M)P h`#!g,>:8}פ9! vݠ`VAÁ `OeV{Ia[l4Zϣ'T]VЫa x卼Mi1aV4׼i/qrzýMW*`^.:"N9 !lY AkQ ` pxY eJv:X`V! LN ּǣx:ǡ4[l(əJ{ZMeOs}!@!!L*R "`@d `" l` d}فEZ '< ޡ,l f   L Lz8! X"Q!@ ]YuV@z|Ons#wUp܃`#ӃjajA{ m$&VR>a%j<<#a|aV!lSAv98=\ Z#C |։z XtA {>/rx<ٔed/yIi,M {4!f e Hk!E4[cTj0s$FP] fRSUy%_U(Ô(n>D9_[ay=#G?K/|t+4A3 ISl۬d򒍓 4kcm>k Hc`8Zl$#.e}P XmMo\kלKYLx[PVyᐹLipmZ~KٳhӪ]˶۷pʝKݻidDOa1\ǐ!OLY2xa&eϠC-i͛9@벡rL2qCG NxCK#k!Vl؝W'E&m<֯9Ξ˟O~;ċ6kwYiʼn7x}yVfVhfV`C)AtZe![>*&(p8\^N#3՗g?VJh(vRr)_ubc-Zy~ꠜ6{bI&ibVXX鄚M&ɉ` tz"ɧU6餓W >믾MZnSFZ3oj,?c96WB&K`[{D`)jzeq+(6軃[gg} lmmSc4*\v.gKjnJvRV8TL3/ߌ%7(pOv32na6R2ɤeYe^M c-CkД8mWCrj)羪6'*ά=e8{F(R x=uE &eLt?P6ڭߊzZeR)MyL-W3yx/VKFr%pPGg۝KDUz,s(0 gHǙD &|C'}Vw?c?8l|JG8YQb<%P63 4 &mx02R`8FBmII gF8쐈L")xT0+,@ :r)@xFJޡHH"3dxb@J K J:  xlZXR |b0Q 2ޑR` x+L0 ycK9q@nO}*#JЂ. LxPܰkA,piDG Wt:`80 7ޱ"&X2䐂xX? i4;@Sϥr.HqV, /8|}â eσ`҈ Xj8-:Xq`u;"1I1@G'>Y| x, I5bGr h8  @ql"K d{Fn֠VptF!X6mPk; D вl\B4ͫG!ve>kY+sJlb5GgJ)0B* pր߈? Q9.iQHI^ )Ta4C1 oxq2[X3a Y9@xG04̑Y>ZHu M4BVZA[h[јް2egӶC\/f(p)A7ku@QtDndkb`h3PmH;1`D e RwqQ>`)E b 6w>7#8(ɇ' i\# tNwxAh8 &N?@O0~Xf.cN|FOvbޛg{w:}=cV4nBItS@P>FD@@ 3F<f`6ư@%m|ژ/Pd N  I! w( {V3f pPygqyGhcQqzgzqv{{V -H{fW77x8F P S k߰ @Yl K7 z0 0Z  Y  0`f fm` }GK֕Kk`À y` `/ zp +`6 , kdyy"eh%(3'qsjMrBjrFc=;(i/%9(24P& aPQ R}R9a\Lր &  t p `~` a I``` }$ Pd 'P 80 XPz(h6:Ȋf׊{‹ؓcU` `STP3Cd?`SgeS96U4 f~PS>:T>UQ4b!0Kg4CQ# pO6LېN QZfo@'_1 g@ya6Bxj.z.(xsWy#,Bٚ$C+xڠ  W#q`jq N0 CEC:MB?09 (0 C 7y'EzY{#wh3Y'7 CI:rDy֠ )A nœ_!,o)d-U Ph1hsc/+-2rFYE٢i 'mZ-xi5493` 0IBgP R1*X<98;z8x{^JsICG=%p:tѤO S:qH bž Zz(d (:A٧֙ R6ٜlPRm Ƣ%O!8$ ~Sr62wW9jr5Ǣ](J3:j aa/@ѩi ,SW)T# "9<)X!_J-#X#a+J窛G6V"B"z:"X:b9ɺDeXHj 4Z9ꯛ!s=C2BzΚHيrrɣ+{#<㩸h&+! zrijg r W!ֱ# aY3,!- )A .1 .B!hOc| H  `yhjl۶nr;t[sxz|۷~;;6v[;mѸk;" )AOZ;{-0`  f+KkX[`;5y;+6/@p)i3tv*d ` {؛N{X듪> ;[6_ѻ)1)YDCPr . +P| << "$<&\(|*,.0l/\1|68:<> k웹)0{OἆHHZ\^`\ d f|hjgLnpr l\v|xz|~ǀȂ<Ȅs<DžȊȌȎȐu tuGIT۬} GBX,ƪʬʮʡ˲<˴\˳ʸ˺˼˾Ŷl|Ȝʼ̙0V+CR2$O.`S <ν\|ΡG ̴\ 0\ \ p$] JR.qɛ1_Q+B;.D#PG"] "<϶P#,.` PaИS0srDZ* B_lҸ P T]V}XZ\}ΰ 0 ` ]}h[zpϵ0 Ơ Q i}xm^l r=o-ϱ<_-< _ @yP=ϡԏ]Y ``5-:=5 <.  0 RJ}̚$ }r<ۼ^lŷ ۯH< p ð Rm` p Z̍ۺ` = zʠ 0=X\ԝ5!Ӹ L)Pp*D!}Sͯp źp _m >^~nsH` d$^&l< P ݰճp 4~ )- ;P 0nM!Đ 8N8ٯ  fPaU{>Pʋ*یBmb gʖ]v~x~ f؂v-y>} rՂ^L]P@ p؆O M Q}ې^ꦮ՘ ` db g $Bp'5 0@B5FpTn~nN~؞ھў@~NM.!P.4!K^^,N 0^>~NM~`"/ _/(nL `ӿ\P8?@PD M0 ` >P50 CѲdpZ@;.!*`O> Cjmk/0 @vxo#yn` d0Tg_p-_j?` >h p d ʀv` C^Ano??*AzS߹lAedy,6eSUi d] >p @H` ŀ @i Mq( ..jq B(B@0RVI<~(N8j,`BK\t#E 3]ęSN=Y3'6d݆~M7*PU^ŚUV]^`62ưƏ?uśW^}k0>Io[&HA%Vk}.Yl<{Vg:97imZ7= uꚺS˭/vGNFr2>I;شӦiV׮a7hl|۞^UFD/arN]3ODp@ (0pcyeZlFXl$j2a<^jQHtLl%J'3b1FeR/:2l'dh)F$?!$H#R-~1*%im믿K-0~ øfbxG*I"xRGLʻcmq|-O'}9挑Kۑi܌x#w6gf˒5V4/\hhĒIOVY?@ d!0pRjEpeX lȆ6g%aCحRUyT %X{F8aaRUMkggG˻v&rx//E Ey @ ktx™Mm5ٜJmϪwqT:I,kmNijw矇Ħarg}.|Dx)n9wǚOGZAxrZgmm<֏xcl+۝d , q#eLnGɆ5o\Ag((MgCMEeq!M`@ҝ*Ƕ@ِӒ)) ubpEʁ1&%iոFYC.]hF='49A=Ep`i,en/88~ Np҆6>9JRmʠG*AUc /I8k ix & F9I=2ӌ;f4D,Bt2вÞ+ihRkr*p"4~ch&&7xʃK &MT+nڡgqdP0ǂBs@>O'ǫXr7JFaiga %)u:R~(c-v8jq1g$CYK`l#/cĀV;xM!*!4hZR9p!s>$"#@Pp rRCQI:qDdrM?|bŮB-xۆ1rctH8>ִJ`qFU) #EeG25@ֺԤԦv(s7{ٳqIo}&n-A2=b B8c@"B$ָC0ƑF@D*F:G6{敬mg<g9^}و 'T1=^OCz*M+4UnNEO@[קč9>e+:q7Ժ^i0DlO|l~W%aHYN cKO삲 ;@ ia5᠃b oEpl*^z.0`o>M1AaWHS?l%5NLLLqA궳wIf&Ձn`0A%+Yyo& Ȯ*k)=|+ĀkX6@;yYyq`IlɗԊy5 tӸ$xpGz3:Ә*b*MtIɚt_;Z1肍r5S9 dEb Z̊\E<&d'~ʲJ 4$K,<ːF&?4%܄x @h] ,l`JWx@&A)7|X4 :=ȳȯ+eh'!+q {ۉa=Z%/öu!C < ft]89 xF"Ϯ"ұƎRS=l.04@Y{p3|{Pvhg0crRJ4I!R,I5-.yd55K4DJ¦=H!*Z 4`Xjq a>PdІDMM#3{Eic50 dԮxX؟CԐ 1+%(- (W8 ԇix*$Pш>mK2(0q=-KV$/YڌC*=bIB"9y+H9&$B>D1,ȹ+T~WE$xK hDXc]F,,܎%ΘIh X|N)QX,˨evQ79TkON9 .sU"NP|hV]Uga$C? C\~PTW~H!ENPR5$#u8:E6 Sሤx_p蓐U8 伆Px=P8u 4M߯ݍ3ڦ`G˜d(H>p0(;Žu =A`@CmPP^ʧ[=½Zr_(ߢLՇ'2:$ %@\ GTuSwQ+ Nb0`;(G` K@ Z5Gvx5(`$p(Dd"  *fJjeMjtt J-\hXӐ,4=KFX敮c`˱OqS!8"XgNXuUx;z׶:`{UmPW}~6h _: \7۪2*hj lz6%hihgh͛$鎬P%MC&lcMF!i%DRXM03=M t7XZFj#U7L?q@Txj҆τe6b &1Ʌˆ~PKXjXt7)Y}-<>l#c}TΉ izE` 3ȗa{Wa*T _Nxf2֝$*9j0 ɇxX5f7s`6n CrX< ީP:ِ#*Vq>O%!B9Jd`@ue-@>w懼 q ӶMh,pJ hTveE˂9Ds`a/*1?ָ]vTYi'pOH0U Z|X2X;>ߒT7xPp(ry(#m:in{@TUCyMԒz*rp ԯK. YJ]ók~k`8h~^*ȍLbZ7|QRj<' rB9"c1cZ,h RjUq.g~=R_) ʔ*WleQX9n۶q05yj%ҤJ2mR_}ʓ7N…+,#:V3Zi3<#N}LN|w5L WݷMUٸ~9#=xPjL6$ET?:"QJPXy2S|c -%Q4 N6'7Xc W-NU8TfT)J;mI*e$C:e-+= U@Arn)rnC@AԲT>HZQ0$[qR8hB wl#F*٭SRz(54>Uq d89=k>^s,2XC4c\?E3m(e^OcM[<_c<⤼ކH%VE;2a1AMdW8\{=M??Բp]K$J&C, QAp" !-@)e ԄlѐB`l)"j (}J01x AckÀGYBG@IiW&D#*Q-R5 # Iǒ Xz $`/qJP9wHH62 F 3A Z0#| ̉ a㞖)u,l${# RR݅]0,T+e b>FbW0U¡d<0bCL},d#})A$cƣi0? (/$[2PbÆf5āC"S?=.r' Q$aAG2c8*[ ?(u~ȕ;|0 hCĽ^DR?N&hUi,^[x j6Ӳm*T ʪ&ܽ03 s0C,XĖdf9Z}vdIS@8z ,ͨ !+6~L:TRPp30|K4cԤB(q,JƎ nШ@ c^+@ ~t챋q0Y8ưwBrӞ-tc@U.gb&%@` A^ GǺ`DKȒWĢ+|xX~`2O8EXO1[%)!2H nB;ŻLcb4Rgd8Q89.v khBQF֙˫&8}HQ*nMA+R%N)A^\B;fDqk|4RaK#H8A ԤHby~<} zNC9G8TӼ8D67 >7ι=2aA* qk \鐇Wg$RGd]YL$3<#/N&'5hOMI%gSx܇+{vvXFJ܂pV]І?Ԅe|z &. f]pfu PmtA|xIA 8|@)>XCL^5`B(_W 0@;X4]6^XdZ%u^E|[>|458T[E_r umURMl2?C}\Y!  "q= $M]g" h$jB)@)aA` r**A< r!rWi~`4H2c <]6F A{JcW;KXY4W=ɉa!0T?ԅ<6(\28Q3F*#::#F!Q #H K| <Ь&7*B *C0dp68Cf 4`:9t6dN3vQK$PP*;qpA<.G}JXɝ?nj|C*% $hh#uzJ߈@hbp$þ}C$%A<B2d_CbfϭeЭjXE "^ubOf`$Cȅ`MNUm&{{EyQuVDjXA@e&KpYXC4*4C$A)2pY^fD&g j$=53 qgu z$A>jیO(!. bG9i gkZU\&CXx84Cd*P~+й ?F?0Jb WpHh eRn3e.*R#gi=⪱%1PhX;ADA8,BDB)#y5i)ј<2<:lDnþqX"*?4n+XDg"%ȥA%}lAj dKX+pB' *}I8)5JAP"e:APKdǾ\5uKhAdde5ŜANDRMe&KDY,AIZX ú ^XnmWxlX!QH+%5!@bn'(Puj+K46x H'C+q(Y,"$0Bi !=Lۤ&K f,ѶbA٦lAlKADC9Y l l I=綮",-6Ͳb]Csq9ѪdghҏdmA1)>اK).@!5tCvCA)  I .nJo"̮M14\Ԭ!of% ,&~nkMJ0nBwŞM "/A,!g؂K KX;,B0\lEM1 q/A('ooҸS4mӏ~*1'p/l h-s^#|0"†Jjض&-6|CACиM>E$ \(閱))^lr!0'%(x_.ORKB))LGr N?\ e.AD4G4O35W5_D10w3-L3α+|O gRlDbT %Q84 ($@ *C4~SO3cDO44Eg.8(gFGJ@#ጜ+PC/O 5,{4Ek0r!*k?MwRC:3n%ܬ:d8mڴ3H;0! vj`>z*&JF=uN[C+D65Ё/C~4A('v`a6b'2`(6(Ac2e#a+v`Of[6gv|vgh#gri6jbkbk6lkiӶmbWh6ovaops6ppk6(j6tGwlGvnW(3woovSvw7w6nxx7yy7zz7{6|bPC|5zmu=%JZH]C6"+-G!D<fxCдEw]# þFz6Qkx:PD88DC>!HF'pJ`.7o3Oy8ox3$5][#-Sqi5dUEc6lG69ǹ9EAEI<:@%F5f+l< }wn-Ai#ʋp$ٝZb'Q)\0±.H;96{,N)Hz?C2.NNWn"nwRpr39:qwS r!DY\D7|Rhg=T*Q:{9;IEw|=99U<7?inE ~3jrTB%!q|aRKCM¿|RܛUؓO9ڻֻ:g>}t5xԽ.~CTlW⏝sGO>= +o`> gzKn :ƑN|WW@8`:@.?!F\8adS7N>}7dI'Q$H_K-KRJ7qԹ9!6hğ? @\ţ@FjTAP+hP 5N;~/ƒG-Y~9|:Q69Z ?xˈm:qz&;q>N/@ugl^ǛA*@*4 l`?`$# ϻ Ò}8th=sէ!g.Njӛeq6. \~ 28yJ4֔eIKAd0#X۬'qġF;hDI WN* DS3dn*+laZVG7x,̱1hI0ÌI9,d C00 ,ѻAF3 E '@xiIǙҦ^C@^LW\E3k#=4@mAaو Zl8OL)Sqx|a|:1d`"@DGATi2yC@yD\%D,)AG+$2E;蟦%Ou恇`hZw8&xaexOp Gx\cEQ^{Y`%mgG!֘!k%LYT$jb^č.AOKM QmچaHJ6޿PEEhW%f#}B!&cPWzLk0y.'MiV՝ hGi^s)]z P;x`@^)tGze #0 0$; S`N}|b`KU-FF19,u<'D)ʵ<Z8qíBi(AإZ >I4mE:>Qov@8M(F1 a~YJ/@ a B8sWQXҪ!3b!ZL*7v1@2xbh@)^/ $b+1ํW`F*X.lE1QfsmC^OdЄa Ds7%q:3D%&3 Xo!Y!%93qFv"'s-.a@MhIdhcxAH @qxA1R_A@A1!!D2W>!f`T!I nOaX·o'E|P8*B<ҁK &',˲%2fBbD*c!8Mz0#`~ O0F"9#cP6判 obe1jHB"2o N@c8.ETHD pt>ϜB.K:e(*1fr*-.WaL8OqK!"gfB *C/&ơ%DE ^X-s1|140(R@f|ƇA0c1BW&b D%|!,ٯI zJhH!v~ z~/_~B01:#U˝2(,"btAO0(3cnCr B'ywh2MR)>C(CDz!+a+rr2#Bo$rFb%2 -CȲ2k é ^R~2b\@h˶7&9 }F"R s38"82;s.^a ʘ1=N3?*q1Ss@ t'3 4A=(4>4A='t:BSG nCEtDIDCC0Q rCo3,+1CDso>42G}Gt@Ac P9(4FAe ѳ44GwJo2O2#^hl3l Tbvnl5Z.%>[I7pQ%uP@\V6F$f az.@ !g +G+ @ivKÉg&Pj`!! als2Ėl/"WՖ;D jnyc Z8,Q_Ը4R;t.TS{[Ekqs[ .[Q}"fPx=  .@V "Lb$2"$V! a$ A >L`B .* ប* a*x2F"V&kPH"tZoԨ0qC|}ʷq!3ŷಕ;a@!DA8 <! h@qZ!bA, !p`hR!P`R A" L VDx. g 6!l*!N@ LRb`f! !J؉8Y+c6(&e z:34T{ 1&R|qQz>TflVnvnjA @;@Bjg=@ !!@ X A -!!\"V! f`V8 H A"AZ A"& m!*ay:0Lv7x]Q}T蜔}1}Cv^ 'R%5Y*+rA 80Y $@M@ !!7aޡ@    \1#>\ ڡPfanZ ^` L +,`:#P5U6$EsFq1ќk*ٌә;Sٚ- hPt`A{ ;. :k*:91L4!"N ``Ё @`em !,` V!!6#A<`Ak7aaƟ!^,j70C"#A<y]=Ikwׅ3UvPG7z*v#_ei06#/U#bNV`vU.1RCl6W_oO+_nAE6\ɡK-Vš[]޼Wv@t\cxu;?#ᡀy7g5W[=iy36BwUqm" ,80… :|1ĉ+Z1ƍ;zXqPC8yR I Dh!T/]ټgyѦCsi4ҥFw"ez)T@;NT`MVC[t?4򤾏CbWZ t8A!KYAۙjqOpӞp9z޶4>?9N<~>(f<(Ï}Pfv!e.x4618E4qkYB.aТz(xCF>-t6Y 38Bz!@ r,!V.66m&b]t| 1]#L 82,0mAH@~D! %@00?& >h$T!x@)\с:؇*` ?*oʣ?*Ow4bV6ѩڴQ+ 4Sª/5?a?à"ʙ_ڨlYvX% X7:+w9Q<(t:52j*g6hAG«{** (yuo4:*-v'*w7[!oEwkBO2WX8nqpw-niWgw>;`Ȱk"BCrtկ#3{wvFw&#"ʳ2j{c"p|W~&C2Afw%!~˯,Ny 0)@  ` ɫZד_z=qKk [+X     +Kk˿ ,Ll  ,Ll;  sֽap +-/ 1,3L5l79;=? A,CLElGIKM+L P]°an _ a,cLelgikmo q,sLulwyb<}<{,ȃLȅlȇq rņP  lɗəɛɝl ,ʣL3ɧʩʈ @ 1R*a6 Aɫ˿ ,Llnj̙ʯ< %ɴ\׌ٜ́ ͦ @`<ƁLLƕL\>NUS5UbnR驮.Nn뷎빮^nn `1F`0 5׎ٮ.Nn56@ 4>`  pp a !P{0NLD א Qfpp  ؐ J@arI@N0 0 ``S@ ` 7!I M WP P W ךx#Ph2նpQy  ' %Ʉ} e  p0fL ^p ` kP 1P T=G7f $  pcP  4 !6iT. qU̞[gѯ8I  04@'  P6Q >Plz ,4-@ 4";5 0Ye`SB8Y'N5TR\suW̉^vXb5XdU6uYhvZjZlv[n[pw\r5\tUw]vuZFߕw^zޞxw_~_D!,P"H*\ȰÇ#JHa/VȱǏ CIɓ(S\ɲ˗0c 4 ةO;g @ FH*]ʴӧPJJ*"z,@ ZٳhӪ]˶۷ f p LÈw4W ع 0 Q1ŠCM{FhΟ7ik7&+ NxHj<஍C@([bmË$"(7O/Gܣo\" q w5Fc B5X(8@XIh(TrcNa/\vY+@I ya b ȳ#Bf\B8W1¤]* ) "Jrep)y∃3 8H硈&Zv^| g襘fJX{^̤TQ>NTvN hy媪jЕ+k$K55BVvzyk> ȓGM:[Tyz5: ois?%t@53Nl C+0J×^]f1wk,+I4KKtC3g3`\oƺ(H5y<2E+5]Ct4bq40Go"BWrξb,L/2 2@l/M4$O^H2n/[+Ff"S.S827CA4UJ/nYP/30}U`n0*/8u)Q;|6Mոwrvۧ@4ۖY`h/(= 0è.œ44  ` ߇<},@ز,2@+ Q- P E`p]4?tPBt@^塐 ZXfCpv4;Fb't)nc a^aÌhŋ\^ 3 F  6kX :nGJPu€&(޹S:5"4@ .u (2@PCOu)>d:65he54EU 8kZ%Ӂ,bݫ`C` .۪&2i!(a.h" i'd5BV%@*Jt6p%HVSE m-Z& D-dQX"uo r;:G&U[MXͮCLXu]Mł'+/ /|O^Ey ByD#db ~4qf pᩁd&& ' 1˕GdBЄdT!B 䣆X1+d %dقL0B(yKY}D+뻓%&I!1*B/nq@jA H@!=/ P!0:?R c8И4UP,:А..DR)?tA?G԰T w8vfu#@tl" rf[AF` \c^Ȥ v7mo>w׍~8[n.7XA/NR A6Fz+LqGc‚"OsFg^zo8,<@NzBruPGbsuGAz~2~cԺAub'~vO-/xIh )@vAA~Y.񐏼'O[ϼ7{GOқOWֻ Oc({Oq};ǵy7;* 5 $R|Wȷ~҂~oſ}_QEg] ?8L=#!H8(  (o{n7W/SaE$5 ||A/a.-,\*)!p|Ɇx (A A(/&AևFH}⇄<}'{ 7-%P"[hb(#bH[|dH]#$,$q^]jXsVxowkrzHu臁yXtXex؇(|(~vȅ|ch+fȊxHXx8xh؈Ȉrȉ(S 5ȌLxy! "Ԑ"|x,mxg,Ȏ؎8,(X8 yX ٌ iQ1 TyQ!bؑ y8i؆ $)"q8Y#0(' ɓB98ͨIi)9SySy=ȔD O);6]FٔC9f)RD4ɖZ^Xbȑw9|X*m@Xxh<ٔX ɑriy +i#iٚU9ș٘隽Y ɛH9 Ĺ) ,#~b0)膪؅i?(-螜1(3HB3+h'1韥Q71LA-*|z||~:~ڃ!2ݩFX~aT1  ZázÂ4hi3=j^(6< WY T! z*Eڣ2DYJIz,Ѥ a*W?*[*ٙIȞZZ*ZY , X}8z؞# j חz?0j_q,ʆ!ɟ9Xĺ}ƪ|ך*ҺWd麘Gx*窩z;,+ `0 +3 ˰ ` JYfO a}0[x  ;07  dd *:`VC(zڋ\YiPy`(  0Dr *Ϡ ` {i @ٚ~As_jv[K]{ihp 0OTڠ `@c@m|` ]`1FiWJBM `cڗ${7i(h;`<`0K>tX 0 Z^;+Kʻڼ{#ի כ P!;`` а Xw2˖b)A^ǼKq:;K PX!E+ N !L09&\~ y5LH68:|<KK`{۶᰺ 0>R\#  D/dZza3ܯ9y<  kO$>yA p { LS[sx" { >@ o_ƤAɣZ\4,"D[ʧ|Dh[ະ c k  jmXl: A` ͼ$x4ڬJY5˜J O4p ̷]m> P[+,` d mk1~ˉI"ԼT9}r9"yұ JNPrJ+ ۀEm8,I@QLѩR\*a ؜o‶`t]0_80`L~1{%[  g;ݜՆQU\ k̦nٵb˭\Dж0!``ڥk DM+ױx,k0`p3zYlQ+1ꓓ}-6Gȸ"L w=ɰ R׊]EP4 } ѓ-цܨy~2GƖ( m@_̷!K23K E D 'dm c[ɼ>˰r1߫]δ' ;10.ifK<Տф^ιƎB1nt*-ϐ m > Ī ʠ  =K-N;Y@aם_\:ݐ~(hm-zc=> 3) Uқn2LM^\3.!Ʈ ` ?mYd@ܱ " ِ t-YY_L=?^㻎4WD IJ/ ./hoE>`himD 0{5oPɬLM~>X$+(ίrji_?zl}/ oxu\}D5ǬMǬ @o߱`ɝRNn*!mo:Jj_ݼHci0/Ґ0ܩD,<DPB (D*o^e|H.E%MDdE-]V `Kl|SC3ʏ_PBU]՝Ʌƨ5^Y c['/ `^U| wQkcv;7 ++$y0t!͆ݒ 4uG8)z"P@GMimzm´XZ1jmI X0c1ǜ1@ |pG~6ǨtI4iɠMZ `0ڷjΜШ;GD#|GAxcNZ41[).{џ =PU~a['wWov7=40uq%8kFt/  _BfZ P4q:@Fx/$} hQ ]*x3T:g6ptm3|>+n>[BҪZ03 Ї?dt솰@9ɹP~I\᭢%1H OD5j xL}|OK9D8#2㠆 J65(Yk#>\5;4Ptr>Ɗ>>[x9&$oOh.{ћ;(va. l y{| >ɩ;0yyxukP摾>;1ɓ=bD=lA1jqFoXDFn 8o4!bӰN\{jxFgz1z,jǽGYGtȂ#vɓwR 9 ƃ0JR t8sҍ*KsFtTtΆˣD KKe:0kX̷&qL{Є:<\RAͦdyPC?ؤJ$Z'P#JdPPQ15i9@A<=F@Q_Ў2rGq,.+:] B)j'~p؆xD)$( QCdBP L-Zㅰ@ȫ8&a{ܜ ;B fB=>T&C~Xd(C"܀к}ڻs!~^HSQ[p&f~x;NjAP;&K]e a.Cr[rڋdKu$4jSXIkL7S3vp/` hcXi 6eAxp>ӊe Pu ,^opBӈc. KVP7*1.gP*@y^h6 $Z4tuvN"DƢ{>sP,␢ ɈRs [2!gʘ_PmpfF"5M-CƲsQcvPVF^<=/Mgtk \,Ljp0L‚.,Pi({lGɦ-xl&H' 7+:@pb널<cF%͜EM5n ڲxhBc0pgxD`1 1YՋR1FY6ZVd]h\ q ~h_ *2` >sI<#աA TL Fdg el6C}Yn0_ܘG&!(bh_A*%$% ҇ Ԃq?E!^ (ϲ$]WvJ Ca{^Z Ȥ=3 -0NF7(x|jr80{*,tJi G| Egtst+\Wc<--KUxs= XHofCgْ=Gdkh()t! ▄mB(SJe_f5iW-ծ^HnoFϽ2xD#cL8Y7`C a%vc@) Se9WaxaZvZj(#Nd6m䦛PpC8 %1>1td2ēO>hh32DS^D7j8h#%rRfUy~4+ښ{,£ϲ,2j&b?ЩCD ugĖ4hI-3(V-89ď]&?K02X6UMqte#V%WTbF/!<2o-4>H_%f-ضDd|֤ff5 hd!eQEuF耠lOWDZZ 0L36ģMm265S&XmQU,.݇;<]ε#VFOʼX5tg#$DqP'O;`|ju(^ݲuc J%0.؆4`!:dp >(e) ŝOʛo{6!Ӓ5mhC=?N$q[DhBzp$!YHDD@ " AbL% xSL60.cZ!FX%Mcot! %29pUyVtOM1+2!r!k-ȌHyE)mtbH,-D.%. eu$PFLJ<4\llkWWaG,aGSԥ;(ZPudG0tE,MW*%@ɬ /ǒ #p"T:yK"$IK`K1fYqP)T3ӕ`C`5QM# SFN @rHvVexf7 +>mL^2~tI+BNT&APH#uLI.aI>:mXc .юhO_G(Wt݇B }\#5v *Dxj),&0BƖX$09`XY "1kuh;QH>-Hd4WkHqXWץRC8KN P@c')k~;$cl>5ANT"|qL8ɴp;qujě嬢)M(J#I))X"0.;Y'B!:xo;JfA|t!mHni2rXWryYrmI+)O]0,es$.X ♅!Ml.EȺfAX"@6Zj EyCB:ii#Y<14dC8Tjl|5YRVKc,aduV~uHBh D hxZ= !)11~t@\1 `,Tˍ'PzI/\.q1PiȃQ228 υZ}.v0 M;v!|~{ZH2\@H8H` u /j\2Bon`p!3%-dJ=0ѽ}qX>B<<nRܧCOtG]egzQjLsA2FS~-!P7D9ƇL["[ѝ_j4C@-H@4#(B&?B?8 H@,>6 DZC1%dM?J?ȅq6@$5L՗?xD(»,++*D%* <%>2䳘GÒ`Gd޼b8ɑLFT<+Z6j("6]h.37B*)B) Ce5DS-8 (FL_:6tNJɃPiSN"HI>|PB$@D E 6Et& O {e>^DC8icj ÇILZTx5D`\X@l1?C\SP)ԏ6C o QDQxB0,T1C8l#4Ba5% .)D%X'< ᾗ<Ď2`A)DA/;taXM3Ī]HD:,bU.S;Θ&_:iE <MDJCt 2ΓA= -کMa3d]VkC +L!;8B0,褈p<4%xZ<|Cjd#fꨅbdM6,pPnEh( mTp% {pyE6q>HQ0]6 om"AX>zJg5/Q3hNƐd>IQȅP[8:#Zo:!̂2,̈A, BZB#ԬՕKL;`C֨Ohq:%aND |h(2.ߖsCCJx<\%OQ|CμH1 QhCmƛ+dD/?2)}<RPj;,܂5(3zb# 0 B:L+$ 263 7'Ò$I<C6C7vFMC;[4}D1L;؀BN Rf: !X8<A>ZKTD^TWuՎ5&̐4}ZP D]ɰpm5E|; ja 0$6,6xC?\44G;`\MhteNfĂB".&;tDYSCME|ȃᏑ\(~MO{I4`;Ty P~f#rCj ?h)1L5<*:֒uzA|'/dͨ*R-9DDC0 $BqoHd:|\F&l uwz:SV@Z&1 |gECVĆ[) ~wUUd VXYn|>ی9¡ĒIPzucgF݃[y{)6-Plmz>q4`rB=Sj4E@${`[ &TaC E(aE*BcBFJą iUqlpZňdȴw_Q~`)τtj4aӉWF4j׊\'2]:W jM[v'Tkȸl_`2ջYb틱qV*39r}Qݶ~k̝;sh!C1YuԩMl߼t3F&۳7Hv b1ln;qaè} |~:l3wwPvġ׮V(1.Ti.B;KD++ tcC=DUE~d/]kF 0Txj.N^谞Sb^0x OZSt9*H @yurdg8dz͚n89v;߿0}nzFy恧}W02(Ut@u@/F2lpyڵ@bmuI3N JHF6g6:j¡m/7s_D!$.;Ơ qXL$G8ҴhІ.[SHd#l'Y, 2#2˛P:EđD08PIAe\UFI A2`$_hJNj"TJ> cx0vB2eاU>ȓemxI m#I #}#Ñ,镹?*I'IiL<|L mLd"5AC56KJrp dcXG43`KܺIQ1Op7]F:QxJ? ԇ]Ya(e-1 !BsW>($23G02~hX$1;`mkB#vR+)^ȘbiI2L*{HG41@BT|9U-JyY5V#&2FF h y ^BlڪacsuFj ΂$!a`^ b_O8V5ﴚ~NzM!wofdnq|y4Jx6"q`GH4QcMfKиrWڈ!"$hs*I@0pf*)N);J0HyA7adG8~1vxA'P~|_-ʅ"Z!wr<~rUJ1K1.YxNcݑ nˆB jb(@KфYȂfqZ%B)zZgIc23qh$wMDeUS111Crq(jeHf69!$c^ڌW"E,܃kڨ*rѽJŸ2ڛ-Լ&TƣN=M?fjbCeo<NLQ 6f1X:SpB*Ril`6wBMeԷ7J||G~31J]M\!iDSJoluzb<"Ol \ Pns < x!W~nCaw7x" #<d|o 6aK6FOEbn!bVv V㵲N7`,1 (2!J!9#~@6.F")jm! ddcNzT8,>.m!. Dvq/O0z"2aA",.2/^M䡬a$Aȏ L`~ @] &J!B*d!E .Qz%6"`>( ΀aAQ"C.""C]6!X(r MlzJ! $2 .7`" P< jaP>!M!K+ o aQE աϱ~nG>1!A`R1 Sz0$qL^)NUA$. G!0=$~  z~0!K!S&D!B!^a!'qj0R"=v&ԥlR$Ҽ~Yb26"2+|%2np dh:lMG#b+ R"`J / mo'Lf  `" b!&(Y*ll,P)C)ҲrY# R^SԸ$0>bF2䡛chT H) I\ r!n @d $[ ^HqLTT6*Gb/WqbdBXY&E  U"2  `  [ra'! , `!PxqUbbA0"Ih_ S@h" nA|`'aN~>6j5l@) 2+ek ؠ@[j"" Dr/wE𦦽ꡠ*.wjvڠfY2tcmlNsm6Z8F2k5bW$vȭp+Bcj@[ @:#{HsF^s/pt-KiXS^:2Cv`7!܃k6m` ETx(a8 oM62v ('!8U0`@rw&AllKi&(ƔhB.7iݷ[9uu~b3F:fR2$n.xlˁ9i@uƂ/BW4j|WbR+B4k8X$ aRҁRaAA= #)!Gng֌2:#hb8L V`TVe`Kj$R:FXΘ`Wb XVfF~N]WcOI x/1*f$`3uD^C8. A`p9uAڦ(Fҁ}yj7!FBALƆ9mpH3tQa ysL7B}DKXa^AiUb`qqb”S ܠV$AvSatn ub? *" :38k̢/vX: /VfNxPT:S7XrMKqGZBAYbx f J^O `h+!~guNKY喈܂)2 2'6(eJȦ34<ڼ,vk"9"A )2*"VL4J$)~DTkG,;bXZ/]@h,W@|Zgġ2ɶU >t[ťG'NiY n!H3t662[9"B!V2+3삾Աg`ߛTO9EaɑC*@FAG99oBΊ%%pGhÑ9l'^,2VCRIO~R`il?%_; `h6ԇ~;j!yBL B\e,!vA}Fj > f 5L6B{Od+ Ec֡ {H!HBP+ "C?2߼3p٢ Y4!b&h!l^a8Ua4*B_wMP?,ab0yƍ .a :|1ĉ+\/cɒ9w +X1̙ټ3翚:mtG?~:Z8=l 5ԩQiZu#5@*$D`,4UM)UdKwqy oqdMGTyɔ+[12۶51ٶÛf>0d*3vA㶡MѠJ X,d'OƍG*?uИ Ukw55@e!D)eV@@Ig^UA!&5T?3R3`^Xf}m_1h6Og!ަn6A֏?#Oqd l/]OOlLRNIeO1HPGD # GK-ȔRBK,nJ63בWAL8ᘤ&82Jvh Oc,C4s*Jш m>4)Aug2Yq$^sIəzZیc2>\>U԰:u!P%|Y{񩅦0 L\e:y.L29d8h&`(ދY;dx3i;j u4hjA:N]815&wU9<2#ϱ.l@  1D'--"SG }F8ACQe,4?n4~^N+r^KB)?؄uj&M32R/C DC:* N]w6QMB&uVq#) )pԴ,1QZk')KZ"])80F4|cEel^Ԙ0jljx/lHP(D)d >qg < oP2z !+AbY1 7C d*!ãAl:kNBS H!ԲuG8b &b$#uFpG;5LmX^*IE6H?W `jlD)J9&:6y_&iR9-B*aXH0!ɠQB F"G> $H4a2 q}!0`q h ĊMFP P'eP `A/D͈m(%Ӥ1$$Бy '2 >0` .iI%a PI#-3Ȉw36Z}S*"Cb> JHٛ [IZ  I`  0 P€T ܙ f 7 Ӣ0ʣIJ=$XF#i{:F{cua+B[j U ꧜Ȝ= =9/ &15 ǥBj:17-JnxJ9D6!?Á`28 D!ȠbEƔE@rيN` HJM-P&?֦ P Ò<p x;ꆆ:}<9= π >2VPL>. ?Z%ʄL\Yajng P} 9* )xB*) !˲$szJţ_*9K!%d0d -KK%K dUiY@|`  WJ@ @ p! " {K1d3 xCj,:{<ڰ 'za|N˄P+A eY[G[J7`@Y:ЯS[:h Pk6HV Z&qI) "7Lquh az狾๺:h;\_k εc>`]tRһ 3c1=-[ȫ @{_ 17/۾SI K8#c8m ) PA@ -3y K )a0 ,J:%џm0m2;$ou1S,-۠;Pې!x䤖@F൭kz|M)' `~11bPNFR"0 _ 0 U 0`uQ9܁ Ez PSM; P RJQc,c|,lp/(lMkVv̾Q 5|WU3iᡇ )@ Z)Pˆ1_F|iQ͹W+V>v (㨐!AA״!JҰI7A?~{Rl[|ɘFoB%Τfu c)X{Ijl&|?ܸ ˀ e D\,VuӠ/0IU|N>h͋ɇB)"ѳ}!!qpaS`һ#2%,ͯ\%T|x!Sa; 1 q@N!KK 7o ;T|!/•7@ Q5z" 2u1d-20G^Dʘx=6/^t)S4Sg C!ُM(yLY ڤU0x@o p0?Sb ۺF' #2s<(F˶\0 +.RYYÂIL; Q 0*,0m?| ͌*- r7U;x3 !&M, !04t)Vrpev8&E Q&o!^C /el;x5 pa]aQ-s6&$W5RPȳM6 'P p!%QbNͮF Ґ'p2,_J>9`aN[>` @iH{.2Ie~<3)~>P  ,冀]-&'?nې #.Lcq9ʳ=sw>~ $76 ԡ9ڛ~M\7me);f\N`  ! `bźM<*s"HdpeÓ@O.: =;A`3⠂&?(MRm 0 àUF4$VX6֕@= Pv_@ fo0]ɴH3o- ÁP+%5GF}Ԩ:) %OCy6A[0s 0''ͤ\~9/}HQDA/\IKXbM;fs!.O}h;2ۖlђ Oe#&TlP]lj9,$TI :iB q2pCπRvr*"p6j(CsP=Qwn9RHÝA82h*/#ELru0$TBor;W؏,3Έ^nm/2m9DbĆy9*Sу6{R&,*.F֙x=(:JךZ--9vs\Cw0w3eDƼ}}$($y<+0 bR$"xGXx՟LdJhF*lJg1SQ>i,&ҫlydqfZ;1\~0A˟1]u e+FEJZ e!Wk(!o_)n8/ H>fq,*uZ@0 [<|'=%!ȡ 6QQdH>L_#e/8e7&wM$+>VĎu@qXF|\&cZW1ȴjjY=ZԉAK^F^%Ō2&#K]*"Cn3W Bqβ5<7f֊ gDYȢ`GG]%̍eF`r95 Nӟ~gn<70g=Rt{4`#eI#WcHd7@)@m a028e#p1ю/xa>0kH2em~s\2?F7 uщ52pvӊ<^\%g<28 qrZUH?av` G ~gy3Iu>{#rEh]룃M:  ^3>}@6kÒֈgx<>A2^K)OSe1B-3Xz'+CuybCk>X{;֑xr4^5yg?NM矉-`pmխ3$be=v[ &C%xamA KyI :KAL\|j hw{Y@yh@7}(~ zm+t)A.B2ػ0]+%1%PHg~@&3樜QH@!_;j+02QgxpekBODW{ZBC;6;Є4VgkH>D 0mHDc;:پ6#x*gxh l EmC;Y13? 10E8:eq `@G>Ba( `Bp*#r5u@c,Rd0i+F\U. \{wZGqYÌ#/`g& >5 H3 bky2۬lȆ'w g؞k!c:_G-Vѝ}4'sNHOŹTIKLۤ iPPx>gЇ pIGc>P Շy Rk llx!ViT/41UmSQ{˿M/ ױA7AQKd 49%`4<>%Ef vH|(=˼il2 [‹z*qS[5ԽZOшX='륆@E<|x e=riRDR9TTKUDUW[*SpS~SÈHjȆXx%Vduň8 x4X7;+cl-SyE 8ǀcF5hq5'6ݾP%֒0D[PMΐT qPk`s1@љ jadžl?2h.::5ΎF8Ac]A:˞pFmqGjmLدy߆^ O 7IpPhx@7l/pwȈX iQ؋21`ciȦ (y9⚤l  m0CEQ@FyppD?mtbyغHVKN@d990P*!bQwxȤ0*R8 /REGlqtOo#SҦvP THPMb~D͎0} bw`iXo_X `[fgDZ:]vv~i2 rS1=A9^j&y_̇ pHLiryH0h}H'DF(;hpGd`eyrZu00YɋovL&(ޥ{{{{ -:/QNH)ȆOxɑEK_iև`yCb;_ӛh'J? |0o{l]Dk'uW<|o. Fowˡ6~n//MhMEd=cmQjXh\Bګpǻ` ߙN'V-˛vqH&ovmH_† '1"dI!ǎ? )r&CGr%˖)UȀ1W=|xqCnK&.6.Q"?_9-)8A>o.g9};XBeHԏQa ]7~,β+8Z!u$N[3G6a4 &Hyט[H:$QĤ(EӔWb%K,BSNA%-^2q7!vn][yẹ F8?\VMQha?C^44tb۸\L+&#eN$? 3E :i9Ԓ5J  ^g'a"]RG^S%tUVZ_Hmz$(iMcL`C*~Vҁ 4ӌ1C$ &[#nH* 2\398>Jd&Jg۪ ْӄ+{:I WpvȂSIh qЅ'Nz:O<3PoRFX!3=иS0ڎ%mP#<6{m̑ C>:8&D·&3;|X'59y9,4[G獩{7t[oqНU00X?Mx(훇t``hc6S̋>ΛCan~;5`a8~GEÍl:yX4[./O p+v%g!Vu'7͎Bc(@ N+rJLi 0^°f̓wPH<=!ju^&4cؐ`! d}Ϋ `~" pBO&'/⍁ީRk7:9Z`'7IhI8qJ1WK"yiađk( ڐ~jG8gd )!yQVDֱZm1-"2GQNTa@="t$zĞ: G">MϡA`-#!$d|5A,G>q CR Ub,`W Dd9ZncAj .@3fI.摘,,eő(M%Ł˝4 a9 eCGZL_ qCX^nST$fDB!Yccs6%ه1T >ȰTh*דzX.}K ;DF[666 G:bjֈǭo$.*( ?|*R6m"6;6-7"N6@kJyn-błmqTn'$9Hv s*WDO !m Ҷ-?Bƪi_k#/0L"Ѱq܋!9/J[p 7ˍh~ezuS"A;=)5Ꝡe1/:$A΀C_KߴgyoL5!PWDs }p5+8U]W0K;?1anG8V:xsgNl_qxZna.sa>Z1 W^$'Jr& m )])u@;āX[D$ϙ_V]sY`Ҙ=͑TEuJv08Pյ=__8BS;`46M2 JRDRaSX!Єǟ@\tQAQ a ѵ3hDHPB0LAFӡx<<f!Gg91d#Bb$J$*^yz[*YL"9w>2A{Ia=ԃ2`fQy3$*HoHY#vb11"GܟA% )L"LmZ1 Ca I$eH4`;-DH A#A`ɃaT.&c9_?d@fz_i"a'vth㨔c`Lc2g2CJMCAÇ@a;@HBJn >a@MZ!Adш_/TRa\lb c,аKjXh AD̂C8L) AAZL1W\%|AbbB-B %QN!Km`6?l)$a F1iV&#WYp4u LE-_M~$G=T[eelfԠ,&%CXqW\4H åTA`,oDh֥>TjEXDjh`,4tHaCPniZk&PX|CjI*z`.Qo VcyXx–Qkk]'`3Vtk|#oڅL'&oͫʹ~k&>iN_e| Cšg"2<gJlXl֥Q{ɿLBo&(̮LLfHxm]Թ+k/h+Bm'V]h"eֵBKF(FcAhƋ&׺1*m,VF"\(؊mqLk*.$f++-V ^I.nƕZ>b,ܬՂ6DDqnM پfV $.µ$~vn$%B0EKX.@ڭ6oz1>/H n'Jo?eޝvEBK-bojwb&n^fc.KQ)ljNn!։' 3pvp p Gp pB p0 p58YK>- /woD\avggvȶCp0q0qq;Yd)Yڱ*qu0.woToH Zkplq(25XB~Z crvM5 C&g2'q !$ (qJDI[  U/l!'I+43HХ3H\a 0Wa](Ma&PC,T5XSKH:Ăon>h2 D,`0̈́L gs(qCP(2KG i>BpH|BZ+)D2$\Inu?3҂ܳ%"q-DEDOU_NtOOuP uPuPt4S'+\|-h|n5&A-vDJT//d:6&0{'2Bͷ2A)X"P@>#X] L/*[:)֏m@|C,=(A1<, &B*A0(CTBAe #$1Ȃp +ֻIzBB1(H/<\4X2AƯa* 5+Ń' )K)\&, J yi"75p( Я_ 6tbąH O)`J"KI4P:6ӡd  ~3@>kpjy$ BB)LhTQYdɃZd9 J ??^&xx'Sh\m4I(9jq0 ӧmn#x^نJ8&g7?)؈jB+KF8dbxlI$`QYr0lJ݇Y>4Ggh^Rhɨd((GgƏQ'wDF7lL5m3DI' f e^M _ #Rl%ߠ%^`bgZ!U#A)I&o~:m͒Q4]R z%Uyŏ izQ.AfWA&F2m`tlʶxJ:`G ҇7jf|dqZ/j|YOfkh\k}}&uΔ~1Kx!7cXnW|c^ߡY.~!Nzf[D䑒Yekj`+ *G<^ZdOO)䁈L#MC iXчe#" :!3$臍u,_QbL(g)_8f`{6i#cp6@(X<8?֋>oG>~d\TiNcV#Pζy 7 s^L&u)[_(A9 C}*cY^?WFe"7"B4 Q$?XE"> ܎+Qi"&FZ8%V7|1G,ː!0-+=HFҩ K`ا &LibD1-$IbD1q." Mx.<|~0A`#B@K' )rDvRټ(FBҐF3@ `)R{S^U##@ u,XH(dUw +%4lu]U' v-B# mF5'k)U|N7*cv{]qQݷ!'HLn5g]kW\fw ?Swϻf3',!'i LQcs<k=$y=Þyl<9VEt&Om.C tQ3"WWڝޕY+ІcJJ&X9zՒu?Ac. Q#4 (g$W>ƨ,*o i_{y{Ѹz>q={g[w-0a< !1w_~|l%YWE/`Ϟ~P0l+Aa1fs,0 C!  p@ApPl@ 6P(L 0,`+b QC-*K4u(Q  +O*O7sR! @+% &|! ,`<)" P`f `,!,Х !!  f3LB@HAN `$ "`RhP.ZHS7CTDX3~x@b6pDkF0%"8q!\`Jb ! X@R A At V!v P`+b@ A F`,`! N$V! Lv̡ ` a~QCr%^FGTEDn5!EKP!Eb6nTsUW/G!A ` ea <"8:@LX a BA @ rM.b`b@A`8 @( Vaf@ `$@L@"a20"hqWCTO8$Eqb @F͎FEfkL ٕa>L@Z9AVa!5F@f'4IVRuU1~VQV'q!Xn(u)AgdlSnl V 5i "X@ *a(Z t+8`V ahnanR r x`a$m!"8pʾa"Ox6,(@UB^"^6fM'L9F! ,X!@,0GL Q`"!hctl7l8B dNQ3, Ґ =! > |cYR%hx&s6peYv"tQf0Y}23+Wa+, f " T\: ԗl "Ё |@U 70.S`I-@`y d$6a* ! "Y!!-a7@ j1!FC4iE_͚R!`VY5dTRlfV$7Thoz:ኴhaE!{AoP dSbgA!V6x a+rc!)hAFa$Rק""  ~8+V1HOBMQJẄ́w=RZ20:FAf⦑Adiup ~Z31RBK`$NZ"`jڴO$G{*0:4D}LF0fH"-LD)XP['X|#-o"RLb{|r;"Ҏ%"ol;ۻ:VIۼқ۽[盾۾[ۿ<[ $!,@~DH_xeÇ#JHl3R@u #(߼t 'Pł(_ʜI͛8sɳϟ@ JQpAŴӣPof8#lT'~d%͘QӪ]˶۷pʝK"P F)8po8f*K˘3k9t %N㙂G05۸sͻ6E%m:ퟱL>УKt^cT`ɗN>hvK6J6lϿ{=ŗA4A F(%@XGZ@]m;5l,ebmH:@z%<$ @)HQ2ND"cM>~xXf6mC%)DC.H#8YĆ矀fdu͡.QeChM8 i%HII'MPGdꪬL׈#|yg뭸뮼+k&6l` -hMTNJ(D5 Q&NZiڡ@)MO(%b(Ȥl' 7ye[tTlw qa~ӟ3YA؊@Ax2%eԬF^Cf5 a 0GB&E!)Z3XLӦ5NsSp|tQ\ː`3!80٣j˪"b6sa/7 zՔLIY* YcbE1zAa, dCxͫ^׾ `KMb:V@4zbl19f%PI8AIT`F[3nԮU(JmUERj A pKf.t\E-J 1"<#R8R ߖd9P7)Ȳ.wYH)Ta++j &Fg8`5*h1`mSzBЈ.zdQ _ |f zS( GMRԨNWVհgMZָεw^ԃMb+s5d1Y$cØ2!ed1DR˺֡P`0>vnt-pN!70)@ħ q( ~7~>s< @m P@{8Ϲw@ЇNHOҗ;}6-_LȨ2Ȃb;%mUR®d2y`Q!~ȃ>8wJvˈ:P$$<d0]hbxCLߩGOқ[<@C! 'ːR B* 7>i{38EO[A=\)Obͩ$>F` 쐲ILB>8(X x ؀ (8XHSC U P @ p cKb{QH34DR.E ae`6M`&fBWFxHJhZ^eKxOReE =Ǖ XƅS؃Xf`| P m0 0 ̘PivI$鑊i<@N銴O9YyșʹٜЩ9[I Dَc` D ZyvQV 19ٝљ9ɟX8   Z i6('(ɒɛc&z(*,yْYZ) DTpycgwd@I-': PRLvX r(y,)W:, NPii g+*)JtZvzxjȉةI 2=:?QcCQ1e6%iZI:Zyzʪ:it y ut(8:Jj IPg9U ; ` J p:٫z蚮I٪9 J LzLJtyDuWYW `8 Jt yJ+۱"+ ` #,۲Ě@ Sk yi L'J ڮ'.R;T[V{X˪Q**\ Nnzt+NudI6ʮ]10 Z][{۸z|;; }0x zxҪ  Oi?YP ʻۼ빗kʸ [ \Na$ׁAU$Iɰt80 b@* DD +<\|>b4[ 6I Lf @LpP 'o?ITP |8:<LЮ{ںjÆ߫} :ྒuN`SQ;=jn̼e \lP fy@e >oIpI@ ?ˆ N'oɜɞ@B=D]F}HJL=l@ pw,R~0l0 vJ `4>k4`ϟpz|~׀؂1ݵ3=yѢ _ pׅؒ}No`oNؒ=ΤP*{\;jť YADRg&y}ٓ}ן -љ J۾=]}ȝʽ}3 TleP Zna`U apya@n`ZN@\4I֏ m>^~=θ2ۚߙ a0ے]+ `x8zũMTX֫ۏZ ] > B>D^F~HJLNPR>T^V.,i[ \ \xmL\aZ xl8߅ W>^~8 Mm班 E^i۠Bߏˣ-/Sk w㶝㏍ @^<:麾>^~Ȯ AϮ0 5 XP  P g . } pۼ>?_>D٩5w= a@٧pSc46ɍ ` 3<;n4_68:ogKidM BO > i@) = `QM;^`4>}~=i- 595Ait5)=^0 -O_ߟfKM֏?M ok  [G`똟 /}) 7e LqA4'`5Dk dz?_ǏI% Ԕ`&2ijIk i&Q^Q dGAePJ-]SL5męSN=}diIPE@M6$'&&HU14E*իX~L8qܾ-.\hŊJQ@ J E@?Ydʕ-_ƜYfΝ5S hңMFmڰ%L [ Sa'YuRʛ}\p+O<o 82_p+H|H5 8V%w&vqDf2Lfm0*KxL"UPapL!f-+nTހЂ̘H+Yz1y‹g>O~ӟh@L|E(z'R*ne @#0aC̩-@1r̢]:SԦ7gBuZt?5B Dnx)G W,K^P U@#9mm vNto1'NպVC/*h8[e]k^Wկze-;XhBm_X6ֱld%`"jf`]|iA[1ڷcdEbQ %!A: !Rv:Reڮ׸E['5!Blt#.zºŮusj4v׻oxKEUW:"pFDTӊ U^ $H-xDocu'vvF-&'AeK=hBЇFthFt%=iJWҗt5iNwӟ516ytUjV3zse=kZֲ~uuk^׿v=lb?%MBj@"O-nDXs7וZ̮:-Vo:!p4o8%>qWxA n?hc? g\%_C=s79 ƭ+Ӯ ~KghV3HZ[ԑouaz<ھsz #\R:zvdm'v>׽;w|@ ]rų:g ]<9|5@ò-:E]eȠOSVfoTDE齮Y7]^x5l`#mGs_ƣ_?? g#?~?ԏ5opL#נgHHx4dt@sqzA^8v\X_{{9Xh8Kkll>H ᢗ^jÆkcBiJDjiPMQ)P dLK4@O E),E)kiElipi_PLFLdXShijklmnȃR@v@@t<)ԆpAq@'\&Ap04C3@B!,B4L@[i<(dȌȍȌ Gjr I%g/U= $@>|8DYxY *bPYbJ@JZP[h>HIDgxklpib\DXEQ FxPd) b,ibtE F^L`HAk_DfbBOo@`LexO `~?YHUDp1kPZ_dQM_YH^xMY@Ժ:xNiȆilU$%5F;r y ]HXI1Mx@ #s%p/"bbR0a\^P[@,J_0GZĮgh kDXN]-O+kt=dL`>LFx3kp>;3atFxY[eb~8 `(UkPdHe`d(Y0`TXiLfpitP%0a3E.Zbs)XiPOiFXY?LYq(lx̵MԶu۷۸۹ۺۻۼۺU UE;`f&פMsh(ڌfRfp7`XePŅPe;p>K1؄K^ee`axVx[ELd]ڭHH6j((odeS),i1bg=eXE]Ђ0 YĽEUeu_ueserW7?|@)#0~]0e@YXZN([MGSePuXl\"aXWf+VPXSCd4V4=O0ixLL^صui RXYU5ce*txe>VuaE xS(5789:;<=c:Ldx;\ff0ؔfXHmwYM-Rp]x ]vFb `kP:-F"^L>-Ft1El^hh bP ׃H` 0d (p|}Xl]ka m0e@Ԭ@fY^ZL?`da8Dwxygy[_04t Pa8ɸLhB0#"C} ~u0epDO6pB'`0Z&@ZH%h TX@NK\U`<S5S` XZ0ep'W?=s8*XȂ2pd8GHUoP0'H`(OW 0(00hb4~0,k(fx2؄bkN5aaz&6F7.a: {XA&_hS؅\7n82HnY  HHh``[KOxkp)HhHXfX0*(wh&X 0Dr5 ݊ަQewHHBv82xXp`ɮ8_xR8_ Z()\h݂̑UauNP>f] jM(C @$8sDXCfF,Hc ݤYxO@X0}y n02X6(ZHYJed (s_cww;[qU}v&'Q(Ȅ :yu v"0Q_ >H)I"C0h p70nBP0ch>BkiDkdi hn+Xf\]whi~7n`+uahIv\kop?@X8|R9D*o lW0%$KbġS1 $ʔŐ!Sre˗)YʜfH8sĩsϗ=}3dvfĘ&XOvlj5@/^Z=9,^5c 5sbs !鐇جeƆٺk˹ƒ-$XbY4iz#TJ4bLt 2H ! b`90d8A&leѲ95 Lyc`uVucqT-lDQFQULN)Y~f΄ndd>ӯo>'Si.UK\(2$ 6! &/R<CU@?X #TEK-䒋2( R /Ҍ @۽vlM{ /b .UViaU4'" 6D$\3Tf2L#5(I7:!:q‹382A)X/DI%rAAP BgSX$-C7k5YI ;,{+9L1o̐/)E`C 7`X6 (@Ҍ' (Q\ŢR0bK/ayUa,}ؠ'R CJPdq Iɣ<@kM,)1ͧlDDpcr/ĠA+' P@ `iX 0u,]{5~,pod N/=x` #h"uM!&8"$ @/K(iJ{PҊ a:b 8,TYiey$^7'H'e, @gxiZd)^:1ǂ4%'y nr +Rr-"Gs lPD0:А,S,xE(s7THz۵H 61`|X(' eL%L{!O# B!aآ8L"BdBL`B,||LZ3pJ cV-!@,aGbYbU1&f0 N~ pChaĉǦ AS 0 k<"Jfb(00~!@d\a)KMr$(C)Q<%*GY 16YEo$ *DԷ]+J '#Ltx,8 bpM* ep FQ 3u>Ë0a Eչ= 8,Z$! <8kZ7ȉIƢ T`Wf0+@uQd4ݐЁPHh#ӈ0+ۨ|(Jt'ĮGjRj@gdM' R)ƠEb(* x+n Nx, ^Q$3A02G03K3n$MfчbpG4Dv-$Qmh4l&xrġB8QqV L  W07aY @F FBp@mTr.xv&, DK0,M{7Vx!RB -b 0<> w. Jx N`9A= <;N(`@&hq3NRP !9"_±J07b8!xh-bq@NQPia2fCl?3Y#Fp*3=~3-AЀn ,쟰Zn&j cb+hq- r2 " `@P(Ġ1U791x`G0!^N3k1:`©pZ4#9@ 0ehy ^ע`0G+ f"0PvtV2 @%(D =p F !b(0" ex8#.S82|;2}{[FӾ1'ċahx)Z7d1Z隍1scmӐ:,ZDPFg e(;a XDCܹŔ[̺)ÍeL~M)L[%Eva _XVv 21!cj:'LQu0H; `^B9bNaԢͦx2ҎUXiWưFDð:U$]-C[-y^0\V^ fn v~ "Cd`Ǒ{eȄhU0퍈W AU`FhEUŋTZD_-_PB A10hE.Ԃ/D2\ H.ha.Am_ы-̂%/1]La$`l^hE-bH Κ/B@/B)B-!$N"%Vb%^D"&V.ޤb)Áa0biU0ϋ0O”^@ )B ދ͵B/V^.'jЭ*Ƃ2j-/`1̡2,\j"<ƣ<#==#>>#??:ZU)FN)V^)fn)NhBN1pH΂>%T&z:gsz^ [V'bgvgtgPUjEba)*g9dQN".lK'bvbeKR L~****. D *F( :"iEV8t(.|h"j6c&`=}:a\NIޏ^ߒiC6*+Vɂ-&V"$|^iJ$*#h>!ȫ'{LhM-H(q֨Φl:HKv*z&"˾,Ƭ!ͬڪqZp*ab "*M6_ģ$Vkh=d_V4ވ2c\d%z̖ٞ-R +ܫ`9d޾Vi F$<J~VLL,ž0ܙ]\ZjD\ՋzϭhPZl.>"L/ZilV|M-Ђ~ꢛݞ^*.fdj"c"t^z-}///Ư/.\|Z- BUhq/=庂*pK>34G4O35k/Ok/U̒(,38+739&RE3??`d5?73BmA/4C1$Xܸ;S;݂,4;sC4HH/36ìorttB4#4L74JoB<E[E Fn4ML5Qu4SHn''ۍQC)KtB?5ULt&N 2˦c*uqnUZkIwSuTKu85]mHAHVtWOOgg/0#0aa+b/6c7c?6dGdO6e+6b_6fgfofWg6hh6ii6jvj#jkOf+!6m3k6nnn?-ė߮LB_#?4s?7tGtO7uWu_7vgvo7www7xx7yy7zz7{{7|Ƿs߰.Bp7;qcI`&.4|88'/87??<NUw~~#>4C888w7#LzYx.w F<|&.0B縎88yz8~8uXaL⸐GO9W_9k7SxU@y99yo9Yr{99zyx9ӹ&97?:G zz":H:x3:t{::C8w:WKU(z33::׺ߺz:'yz8PCs:/;7W:+7w:9󸞋(<;z˹9(?׻{;;zU㠦;C|C;7?MUL4Wz Bѩ{|'C<ɗɟn@@ V{8ґs= |{7s>~'o~/w8>O}s>˃<O?>W?p_?AU8$)nf?go?w 0Ⱦ<:(U?6WrS/+|UXc?@8`A&T`P !F8bE1fԸcGA9dɊR^P%AjLW.]&ҊP ͈KtQF:jUWfպu8_Qn'm~5[v?q9ϬHUdRMfpaÇ'VXWԍ k-o'o<$^E!ahbj`֯aǖ=v큎ͪe2``Ń/k%7F8pɵf{wv۪ݼ,4|0\%cˤ -510,tj}'QNv clPiYƇoJJퟁhEaQ܇ǚ + Bi Q:ht*P@y+R˅|s,( Y9y( GyIP;T)Tt>mvr%߼zɫ7]C^0҈CY\p4YItPlqfj?o3g[X9& \ >o٦s8wp 0yXgO5 (y0f} `pG2x!J ZꭹڥCBkYEYrgx ,pęIƊLQɫZ}IY Wɏn騽;КZ-9Fi)ƙAX8@NBiLZUfX`Q!iGqv ;w(+.Q(N^srVxy09x׾BA7Um}6a%Kq3kqkၤq3G5Yn1/(0F= N0֛]wU ^1 u R2qCBK "? l2CgjHHࡍxpX,㤧a*g&RUWPEq%v_*TI! dDxӂ𸏊Pv8 -C³<!ۥ tڹ!@FG9E}ܓR^][A\߁mAcMC)X*܏&iGѷù:L:ȡN{zAݳ^wc8eG;ΏUuY{pDdލ]ƀ/ /zc2a3"J/a 9x CA8~F<°;rq Pp lP^ ~DAb*Ms *qʴr. nd@ 2>DLDK|WRq+'c1N%XIp 2  2+y0kk TBlg܂茒Wʼ$LܳCPl,vbtJ. "d"^`*/ā EsD#SclzK0hPYX=NiU6C-:4{ ? us7' 9H` &t[4%f3{oRFAGaA/B2^"N e1o@#jppGmT#zY8:COI2 %mR쪮*MO!2ۤg?m CAjZF1UU%V?EVA"D%AdINuCATNYXabrlՖu'bNu|tUMH+Ͳ[?MQUеm>=c-Y&0HUi20M_pE}`*r.a6<-t)b7 pxC?:A &D'{^4T q_i_]M,ofc ` $7uDzr"Nʶj xfpVDEm6 j=cwACd7A%xRKZF]6C !nK@ vm?"nFVEko'uo;HI&pYbp } A%@cJF,XbslLsm1T/Eefчdem[VYԹfEv;DA$tH%3h E%>p(j,jXu@8AEV+Z6t#N=7=utgO=+"fa3w5fzWuv&"{womIw%NE i^ L=@.`BC"8!|sTMcVQsk^7X؍FyRW@6oW.-x u÷dԕ 979#G)E 92MЂ1‘A^e-HB`8󬓍󓹦jl?÷ ŕbB\q2T,$ NC. 2J BY"FZՒFXY=XQ7f[F]vByT9.BAmCb-3 b7N8~ֈP9Ph!JzTzIկU3Xsrl)hϕf/[xejjM@L9`20L0+7A X䗹q5Nd\b)p杶t+ᖪDqW n0+:n"DX(|Nia!i8p"~NF"@F<<ԍw$!mfmk*nM]vL"ء4BlۈCLg&Y^ޤ],G;õ}_A85$~x j.ެJ+u80Lfy\zhr=j>N@>azgnzϮYa.U޹s۳p [& ,)"WC]ѧDMk~.UAbf )'w-(3x黥%%HTBgV-rKQ|B%:$|=A7l &f%f} .y߄YS_ BY ?!~_+`a8AKT.G@0yK&ָM}B?gCACX8L'I(w>ACdc)#Ϛ6`1=9t= 1w ;v׮Y۾;I۶g)}SԍˎE%d]+ۈ&n /s(+1>,î81vjt0JMuHo3O V; 4cd$4`#4u2#fo8ZϨ'N?f]̂[cٹNS/xEӏhQIzs!\dA 2S REp^m]c<d+>PlQMs?| / <%ΜN}WaA'O]vDj&S?Pqea?L j`ȐEA?/((5e|)%yk?tO,RGp;' *BVֈG;cPGhd$c-X+ɒ~3$ S 4EJKuۘ(ilvQPm2 N#̼ jD EK!ǫ|*VF$%JiO21FM|1`R  C!eF4*iP?.'\Vļ䥉uBMqÏmQ7o:a"|0 NB1Z(3PB B 6 EeiKkB*x$t*QDAU~NDQ\$ؼP>Y0:&%a !L8)IƁkи*ƕ4`VU$gSm>2p8`0iZ*ѻ$e"\$+Jwj3 &NBMZEPhv)3IgQ4|#(4PUհ) ZEF="|Aj%]q狤B7=faa0 D*75H"=Qhq m0yF8-VXc[bkph7dZ0$ cjΚI2w=kӇsjʆw'Y;u<9-LǔQhpxeMG@>#8"e<2<x<.JxSf W̬&8Q,ĭHe/+ޤ$7ӸThPa%џxPD;x{.Jd$yfeG2m FKx kAiW;f4q vM)g{kFݵ&aT@I,Tl(ÞEt ~m5{#CL\lc*}X"8RXmgΖLyI%tBao?[f=vɄ8 a@.gD>m;0x|{>1M~qc]ExLcʀǶ5f6Y 6ڠh$%C!_պ%6U5g=e>R"GOF2)܎R( ATPE~iY2n6Dj2tpo;G!m^G2m /?m%ʳTnw8n,Y@1' P Hb PQ1$w `_"7s4j^3Q?5d+BdARCqd2G8&t2S%I"&% Ep pG!o7guv# A` _Rأw`pd;>`UC?0D@0G .`57{ ؃ |-#dt진x"Lh ($@6u&b }0fpV Y77V3X0 x r7G!5` \%6 !4Kh:<E$r=12 X &0!p4|zS 6 yPrS~3C>lh !tA v>\9.Mw Wv%aԨjwD؍(i#KqCt戎vk@  _hȏR;> ƐYgs=A#xKx ?9u|CD 8JɔSQyK|%R}M.OqPcq[m'o~TnKy&qfF1ְe$P WQ sW27&V(CE? n2**Ǎy."3-%SEgUw] %ЧB~a7xi@#%a WQH;Y  7f$&`x` zd4~Þyp Ѳ*5 6|%Dx"0 eӱFhGJCG.(Ӡ IJϠ 01p1'ؠ `|&!Kyw Л)0ߊ5n@]y0 yp8ݚb* GA1`Y k#&QR  8ƤV(a ЙyJc,:`Re;cNQ7 + Ց&y%}w qǛ>uL ` ꠜ\ {a P s CqT@$! O3.$j p=|łR&~%r-˟Y̸۔wCqcj(&c x0zLC0(dmL1c2BG-,x6"<ٰ @P%( Ii NXEC1ďl [ j58̌$a` g@9JQ+?[)4 Ȓm׍ې 0INT4  ` !Ɂ ` ɡ\"8a.{SP9l(q$sBgAnRi#`ihYb*KnD^^ oPSUڰwQZM,`+W6@^RB`.e&Ku/ggi{g @ A!s?c ,2>Nӊ5~>(A] X-\W& Q ,t8jě0jKUBq(き`o\=(7 a q  0 20 k1 &1ttP;6^oq/ZUL(e-ў26  {;΋r!@VooS> ? "4ذaB NX1@?(:tml4}_o*O52+;top7 &mQ+!:̵ŞPe|)dyF>P[,q9,MB qF +ipG{qcj!\GjNl)q;lxbnqB詡 'Ƈe8: I:qM*yR^) +$25,DT.4Н7qm!q3#-Q2EuT"xhgHuխ40Oc|*GI~t&K-ҠydLLS5wkSYf~^[p oAj2] (5J5̏k ~& Nk-lO58\@\ 9*=vn= jh)ek;f܂ dyܛhq4n|",~uc @d ,VC-捳2bq^KMj7`)6Qhdq 7c"݁1zc:2Gg,ؚAdfr~ɍBZneN0JDI}d"̛>ʹclly aw`*^PCW^@2&wiy:1rk]L]2mQFkfWfbjRmTy)pA)̍Pځ[ޣ^~#A84bhyk*tLq${B:"}-ln&-"5QbU7UL`Y5÷O|}#_s+Y@ <c~1KaixH~ 2He`k,!5 l`$GX't+zQ=Fmx&{#ikޡfjF{s48H%À81 lg;qL`[k@&cpNGJ)eCK[&WF( K)IJ-DyO ɓ@;%y22frQl=5ˢ(e+P,iȰ bcGoNb*l^5zGp'(C%HqSR4HGa `M”/fL.`aL HuR:'0L jL<GWyVC({[L4H`V3ܾ^Sݎ΂!W_ \=fV^u+ c d(VIhI"ɇ0Ybݶ'u0MdqǶ-u84T.}h`L~+[K}R`"Nb"l&4Zt4e^e+%(;r?2 ?k,3"`dS&_;m< ͲE!ic-rf#m&¬i\c;6q2cy*Hy[rDT,^X$%13lFb"w{D?d@Ml,BQnhfd&qF1ކ mFC$֎fTnBGT8!K]2AP3ר0H,]kq`m< \;-eԊ#ܒ4&ids7! VU81~ o@/aC 1C @Q Ť&+8 hԲ\p4U7o~@"N ,p ,&0,GDWw$0rt-!GruC^P>w`aGR{KoMzkD-9kg;Ť0 0fa8eJ d55aЋf:^s\xxLF'q-8Tni*(:}C^zV1\f?ۡv=I;9d\1d0yBX5AHk^5 FD{#Zr4 \oB ypqƢneaxrH!HJ! Jt\<ȗy77>68-/.W?+~@A4.I!dž$J"'#oHxP27 K$)*Dd8 Ԣ xdi؇xY쉓0Ģx J0 Fk-j"Ll!@"pĨdMp`M) /r Ԥ! kȇ3O*z $\8[. wSث IL lЪӼXH0>@RXY$؅\xg[pf+1 5aRc V2yP;vh-8OOymX&8@P"DuˤHfTFa2:PR(MsQ€:>7+$00 m"@Yܓ̘CS )G1QxQyQe+-9*;OR*=:/ARyPꚇp- j>c O\Y,-e,J($"0(!0$ ^̴ ZHxgЕ1*a u p^#X@D-|E/kq Ћ#xĘO- 6W8lઞPR~h-Y2gy8Ujlol Q_0𐚐BtZR2U e4vHݦCAc$@#5*pJbdn7DUa؞\:i h!8=y؉RLx0N-lӆjQ5b,e2ȆS QB!]'YBN oD(M[R((b[B7 \0Ή P ( TL~P.9M: lbHgxrQr* X0/ot5DJr$|3mxE ?R0[Pq腄޵B-y0֒ DH sKO~P,*U2dx_ #)GO-i;4 -#T>Lj8m0%JiF8A8g`(pbvj7d3l1azAܒlvV4Zb7.ݳf<ĥc7>71ecH$R ۬i7%1B/-=-!QL nw' 0uΖ;\ ƆLxR+pmZ wpsyhs'߻ jO$Xs'vc8_tط{"(yZVxa07?ӡ]ʶ F_'ER^.ek;|/kF DeP([g{'_"S8brR$2n'=4 F>3>z87;PoV]6;X W ^J,B a@"n 0$uAƼ-Lv>k0L,E0^ܜ}EHsw:y'?Kz8@_jc$6`9ns ܩ>7j8eӆ>Z:ɎvzS7P+xĐ:ȵ!;D B)@t hA ^Q]v :`FT♧,!ʾ!ifD10\z 1)!Ƒ /FKڒmP$ r<(cQ |F1Jdp`hC<9iˋhR7 (CԂPDR1Ұ)a_he Gq 6HGXs9Wc2ax#ͦяQęTyGʶZbHS !#XBw-#ªDZ.459  CWj~*XMH1 g@167n\Gv`xV]9Ir`!t)0F-b k#ðP g u9"<,3>xiDRU8%ifa KC&4oQl(tg]`<2xG*J~K@ H$+dVԲ eBBB BK x0`< @o]P %BM (!OA4 l\Ukt;~|̡s9NyOfe#?u2d8XJA'enYlr ;g}=mk^7K$,xO;`h6\ًY׌3L|0]SZBl(/l```6x2$C\>Y?mXJaIrLM~E]A&H}tKL//D6C))H8PC2()@6($, 8L*<(A>3'h pdH^PWo0$[@dDX gVZ2s>H\hmV'GvI^hy4,C>P5NDE{?0bYgL͂ BEP j\luCfDdFPRƹltllMžgDCt-,L zCK8RW&$RӦeW".r ͅ;"լx+΂ƥ`Cq-*bm)Y""+jHkkN-wB-g|C+/8l6x_C+PltBppmFDNS\yz !54Z QOc]4wP}QXŒ;|$!*$aɃaM\}OCl4fGنavo/_u D)B?Q@ 1ٚ*_IB+jҠ̕tA;1x?@ w8 odjR~/du8y_8\"@8\ 毀xSuH#8D1lVoLwlxEhr[ fa6X娥e6$2`$-qOA=8/x^E휪 ^VwlRtjS($X*4Ì/o`$%Rp;z`Y;x'z( <5Ζ cW2̟͢OS.7fCoVZUhj븙C0s]}'wx2{MEI̎;,2tP6N-BA@ &%_ƕ|; NƒXb -<)̕H!\yɞ3o߾p4t`\q %VcƇ 3v1I4yeJ+YtfL3iִYѡA#O"?| ijr9NfN`s(#ǐ`NJy~'J:q،8{7.3eۄw͇5wUϲg[~)#sXjx36Yň 7~},bݻyJDp`FE H)p-Z"`@@Z\&iI3_&RJFFovϼGgc|pؒIֺ(Fx42ц+MB1E\|eEᩨ! J( `Rv gC p̘ gL>)LdkG?yL w|kǟiLJk )gq0Jfx 15c ggVLEʼL5Iz\.9@ ۥ[t.>2H<)̘uI='Q khc'r=Fi!0qx,FȰ!l5V6lWkNCF2uar"jې|H!ntlUg[U_]әXap~ vȈGyݖYش&x G[nsB~qiN","x#EzQ 餕FB<(J*5jɥ%G EQk'1#?uZ'b <3CK賍Fq4& kqҕˈhj#t5ڶ5ߜv8kȕ&.bQkāk,1w Uܿqr'2k^^]SYƘe!6lEbm"W&ֿ"J}?}|{3i)g98ӯDbr'˚1#h: *gŸ[d!,OƐFClx039rBfiUJN6È8@! dj-X!&mј g,dr}tBM#Lk1@iTc{<EGqHr:pb)'B(@U 1 -̄& E@wkNb5"2^Vե >X61ˆCncK2Rj$,ZQ"iYK tPvI0.c-wkT,6PG]Ą',UƖZ$Dd8U HDvD^F'}mBSCWRJ<ٗ:~wg=k:zY2qp4BF` #ˆZ-yҘ6A,wkpFA9xd^Ya,0'\sPJPϦidO:?l(l2VF9Fُ}00 GwxCdX<`sZMaҘ”h)ԈWd}ƁO}=)@Wg#&M ӄ(%jƝ7۔GDexQH8E{6X|aSܦHWs,3 i%)K|UVc:}AiG S&E0ق>db2Ȑ Mh#"ռC"mY6ؖU WOl5ڀLHuRR[Pg99iJ.V::6adx_G=j$Z9.e٨VƤ"KJexcDE=cژ3JJZ\e\>=,P?ŪNv8?q6)W7FՒIy :.4x҇&_1,r\ʽ`S  /=fu.u2h VƮU)of* m%S<4ОܠlyCPm/1'k]zԹY&ʨt8k|C^(Tk~~5E28CvzWpOPȵd"UFN!X 8lc6k ap ktZtARj,^58#{ ~ă{5Ϫ0$I2#6l2`cؤԽXORŵLu'X~H<ͼ&6,Խ)}&Sg<'Q!%*3Y q$A^lӆ \:i9 ¼F\K_d L/oLLfS382lxkaL*l&.eZ^*NPli ވ92lf%cgѮA`P%0Llc [j 'D>* W1 ϲ˸ΰiPG1blz!|%,b=AI]HQmNh#"$ ,NAL ,]ՏPH%(c9b26%TmB+CA"nQAc\JaFb*xAy J5aB6D AAqZ/7‘$jb2E /_4!a! 6D13CX.Gpz\+a-CLk+!6O$2SԊE /d*FպRr1( Ԯ<4J!DgΪ2 jaC+)ndO,&Œ3S@1%-9׆!ư#K`# -pr/ BB/VCy `꣰])A*O?L)s@r:;_ so9t%{L47>byҐ63pXOZ7IE"P 9ʼnH>JA2S̬B#QSE-뭇lh<]gXbA!#O4hRab!"+jʫF{#t3ϢH#IB4cJB2t`C]O/JT`fRTEANAab-dFQ Gx.$hP',JS$4BbC7r1I!AƨOr2he+t |oqQ+"h42444f+t"WƹΒt#a/r#Rfq$^2!@A4U2!>VCu06JN5h@5\EV+d!"AFkWy~_A;W4q7! F>P FJKa!|*ڀ &!t,"bC 'C15Ƞ Di] m&J6k*OKQ:P_6dPZr7ҡAhA!!`3 ̂wF&5!a51 v ACMdS d`B!4œ6rǰJf-#6dcG/2aangOAhW_D="jA/!onSjLiq`ALس!W+ &wh&b- b䣸Nnm16@MAv4F,]j33wt6V#1+@az1!B/أmq*ovu%%[ oP\!B{V[f;gJ+Az!.B/&V_byP  ]{Gu+"BznKJ !PVIz1(W6w*0`BaaB/BdB!0|Kd lB7aG-XV06k5Ҳt)b-K2D_'&AAH"4A#L5a r AwgsWA밐DYU >;5")3X6DUq X$A3T 3yo_SCB5`/ƀP)4Qb (MDcQ6 M!sOcS> 'dĪU4cھ:A䁼Ӥ{5G;dQY|[Y|}v:$࢕Y Iʹunr[#JJ(D' ]HI{AaT $@ڮϊ3Q! :I[AwguvlA&家7b+Cwx=}۫;{gAp%Vod][2j𬁁:sy 0 Ȟ:, F˽psCg,CAmpAqR>wQMf4:G2"!DK+0ß]o]zeRPo$#1Cb-0XӁ!:ڜ{H""+D/x8heʰ2qݣLzdRllBeꕪƁ{,CFTuMZ!~L r+cģ$޹}3Pڠ0b%!T,ո{H]#L޿&EQ$ߕ钝_V-6R+r_G|%&fG9"@E/ϿuA9^$j!!-hG^cu%J"~S3bCkQPA&^!jP9lDQb&ab3$ _CÓD O !{?ie9G|^~E¡.a2BfGDff]z%"lɛ2"?s ?EiL_2w]}O @!=#8f`aLF !4!<8zN"v - 7O1 @Cy"JHQ"Ë3jȱǏ CIɓ(S\ɲ˗0caC\db_O}1bB %YhgY' 1j&q%Eڴf-;d [lQ?@~j_*SA!&J k M4vy3O?lƖ%8'JF#04W6 bq\9nD궻~6j7PzM9$`; ki? qd:]C w|#5H4؈y0$.Lɳ?m.C@ ,38H+Qg朓6+kY}yM9)l^~^OzNVm9{$5(cyLcm#RIl%|DlCf3AC%m)``9;vmH#INejJ׺ڵW{%rp\h $q\{H>*F8J(6Uk5*˴:B GF/;(H+>ՏNftw\rLgPXAʈ 2ef±x\ >B;jb/I؈EUJ2 q{b(#*xM[iih:FO?AƤ97nVڏiӴ5E^DR^zZC8%3η0˯gLcv҄(c6ItL2G^2]nQ=@'ђƂ}{XlQgSfl5ї[ɐc[Oth"\ X.c-IBtfY<2`vKB}b]`9F8~Kc|P(%ݴ/tFc Cƛ4h#p^+g)?Հ=|AGgu[ax{+ 꽸eX48̒lӘjq>d6yżo6uU~S9/w6| #:53>Sby~;2Isj#/?4^ZtDE]~rqԔ6~(y*?5~|&[4e'~&w؂.7YSSòcp|,x{{eh.*R;L؄BbglXy}"4huV1IY 5WN(00 !v@(y&~'~`b EZh~1؆3Gczd!CN/$09Keba\.c8hnhk6T5[uȉmՇy$ቌWW(|lhY?6tC:Ib_6?7&ʳFΈy{5@nHh,+~CNP͘^Hl8 ׋O4[hBS^"xo/pȎ HD&ّ㘏5hVNJhO &'1D{p00 Fc""oB t*06?׎$"yƀIbT)?YXZ\ٕXDy9ɔ'tSPKcE0 P :q OK:I&Wye@ ^)!!ZX0 b镐\`a1ّ.kt Kh%*CA 2 H&Кrr **mē>IK_ 𙲹 99PڹV }V)) M!9& `bVKh9!a \ ٟ:aza`\ b:ڡ " Z#*Ja:4Zi* j<ڣ>?B \`CzALJʢPRڢP*yyA&c _W7dZfzhijڦngj10Yj I!ƥaQ+[ak5ĉ6c}eI!a 䧭Ҩ0Wm:K)!!ک%| G:az &v@Ö.@xK҈_:r3qج CV'0ᮯSSȈK ӊ/} 4 pP X !p '|rL-} 3~ !"pPr1#R` \Ѱ 2@ {Y O ˑ pm` \ԯ n 0 fl0 `o~sba }h$pP 9\ 0OՔ ϠAF .hR ~`*P$"[K ! {2C{pE2EȌ @ P* 5 "; "f@Z| lo ЛI&L Rfތ |2B   Đs °Xԓv Ǫ-# Px b?mP~` 0 oP Fٜz_1ʀ,#Hņ \Ad pҫTC6n06<A ɀ `ۍ6  ې k4pMM p@ ΰy= 3 x E+>yxP ɠ 2@Ń őDA s- ) / Ot=K!y1 }Nj PCJ ۇԇ   {)@p")1H}@\ D pokP`0 zM"ˁo@ ] p W[ 4+>p { MPrx\<, ]QJs|,Ðۼ қ  l Վ B*a8P0 p٠ 0 U / @ ) @ L  vP °# u5 c  8ψ0 0P -C!T] + Pƣ#y 6 SOe @ +!sLK. &K|;,*"ӈ׳<#Q(5'`>0S(# X*>x&>RB0 1x7@n N` `(0.@b#eh h,|`N L`8 x/NTH7`( <`x`CTc#YGЋ\oe'_O ~K =P}YK6+:UHF RH D* Hd,1:MeOL 4" )2 &H:p_xF \H3EL`thDVd)xH9$`FJ R,C@7N*o\T94nX7NXL`8m^֬8$"C001'!mv&ֈ\י硅Kd]%BS=yuF$3(D)-H e||[ qDÉA,X0]H(Ә ,rװ~#"A>P'QT4'QpG&7Z8:c bԙnѾbpI oI*z}E$ G6XlK8]k3c 񩽋p,"*Q5bM:(seC8wH5m0'&? =HSf~HS7a^ˏ׶+%6a]SHL!a@H>@|hs25px Wx)Z*sNz*)iEp҃t$)Yp&x6pP{pX`,ӊXxH03/i>#+T%;l;×9?½Bt ;#[ 6UoȇW|qІ`z`kw6p 6",s%t@.6eXSZS&͉bY@~gН߹?Cq%Rz<IjkQ"T?7s0 ,!QCjAa"RwʼnXZk1xنpinߑFC ]zqd b ymg䁇J[j@yleLH 63<$YƑ/qC]ЙjblKoM<Ā.~)UbxxS"`NFmXX yP0}p(YWnLSurl-; ~ЇiRS_}%[׍Ң '-W0T% W~ wH >|Ȃp{V5& "CWWp*0x PWxp mH~qJ@X%4`Z%=G Qյգ?L }WkH*cwi y@=fЁ:S66I6DPA`0$Ho!b*yk5Y}f&aR)VXr`(i0h[(Y1oox+blij vgi tP0ogCUІhtHaN[k0idP?PwfS p%&l؆h8%TAT}V'6B6H8Thehxhp0@awjQ._?3ˮmmAU %9sO 0ؼR,穭@mPhVX}hۊG.j"Ao`(K5Z_ȇe%Bk9d!* Nt.7OD0 yHH޽)S'PˇW(su8XXxnV_MH(XUXbx, i@sRzd_`Cw[`H+60]y` Oli0YX&v{QwxBI켊9{yۈw>A XWMC-0 /~7Nc M$gU?Z,GTi:g{;H{AAP76l A \p`‚"Ɖ.ȱǐ"G,i$ʔ#{l%̘2gҬi&Μ:w'РB|hQcFI'B(ҌN-:mJUFZU+د[ǒ-k,ڴjײmd҃ Ve.ÊQŻ۠]C 0Ċ3nqM5To̚fGZ*PԪWn5Ͳm;*l=*`RG#ʓ3_9ңSn:qg;Ǔ/o<׳o=ӯo>ov̽5JIH\!5;KiBK.j/z!!8"%x")"-"18#5x#9#=#:ҋ.ZA !5`01R.`XK.`9$ey&i&m&q9'uy'y'}' :(*hF"HE "`D)0X/cK- 桥z***:+f5dNB9&KɂK:,J;-Z{-j-z-;.{.骻.nKY>jC1!,D@Y2(+f.:0K<1[|1vڥ*bL2C a԰Cce&cf3=3A =/Lh% u2pѯFBl.9L4e}6i6nz- 4o$urTqFX0.Dsi733ۉ+8;FbFNE#M]D$g@[@:.@~;;; ?<<+<;%2(YRJ=$85#s$e)e2|f+_IBjtJ`A %0` hDh|'1_a \ةw*~r,0P ==  :lb @"N~2[$e){I DHToJDrOI,gH "< RJʻ_l呜sܤ2sH,Q33C(Z%}E-xA[9vS;}:`X6Hr 2Gִ̺sSM#$ #@ +A~6}juX5W}lh!D-n0%`j6g ~7KR <1ƻ~r `?r@%kQ~8Ļ\dFQL"8-eX0BEL^񐳼}upEN~ $5 -b`Z".9okr0.h N >VO+O^_^'KR|fvÃ`B4.KAáHKh;{ !,@~DH`veÇ#JHŋM?kȏƇ'[y˗0cʜI͛8sɳϟ@q1JIKPߴt$MdQKXjʵׯ`ÊIK୲'{]˶۷p*`yh+?xKɓ7no7d~3c6@&MZfC&ʫcc˞M۸s[Sfݻ1?` \8/n87ɔ-Ӭsbw.%@.ׯͿ( @ ܂ Z5eNrB\ =p51D`.hcb8(G-:`'<ӏeef^gT 5RH1PF)TViXf\v`)di]UANP˜ g/!矀gC&3\5` QD0ĥ̉8)A]84xY4R=C h@0`y0M)k&6[lm˛!Hf--z+kB-t)HMr!NJĽD@DN7 3c065G q*TY,S.N,@H?,oa Nk l8<@-DmH'L7?[,TWm_NpBdmhl=0 ,2,cD~ D Ba nt{n7-zƷ}[w-puNM8YCq0X xkR" \VUS&oW򕣼ejY R<&G*NS'9K_1j8Gt@L/D!;Xb3Okҷ{`NhOpz U;c@Fa`9&&rB _EzU;|N|z*tfnЇnyw9ZOA cU(TVЗϽqr|:t3Yi_z;ЏO[Ͼ}/bГ^y/(1isN@t*/tA 3bB|(G hswX}]k׀\E G7]Vsx|y+gs'z@uVg ,؂Чsg|۷z8:<؃>xsWye q{g 0*5pg0]c'.auW^*`8|5g;G :׆:煨HtX 8g @o pEdhxehrshes=˷s┇)§rX7}8Xx؊X{0 :tLR#w0O{ಅX`88n 0 B(@rݘ긎_X X{(XhxȎ9YX/X  es S$9ƸU]d'.cc8 p,ڸ/ 1-9+ u@ y,P@U X7 @M uPAYVyX՘6g H 莟0{Pgd\ qƐJRT԰Lk"(Y Z* Yyy^٘ 89) 0 YyYUP  2ə) U 8y I 0 IZٜΩ*9ȕu L`N iPeY7b԰HFa?ٙ)ٟ)z  `0  9 @ @P @ 02:4Z6*QJQI_N)\PW Z >` >Ma@ Gs bSMI`0c/ hjlڦnpr:tZvz|ڧi؉TP QNzx 2N^Lp:I ꧨڪ JkJnZ yiVba ZP  .kduQVt'kj؉f@ pZz蚮꺮h T@fI04 YrJ &K@LpP LPa*nP } ɮ۱`b٦`6i  ^D @w)UJ&'6E%Z۴N{xRۧeP Jl34P I0IP  `b0cIy9~ڣ K٦`i  @ìѳ*!xYեS9 fjh  p;۫{I RеSP [  T  &LP .j Q IpNP 껾۱ Wxi*6@06䪟˩\aN0 J Qf Z7uwiP Ep 0P &|&*,.02<4\6|8:< eP Y [ L@ @ f@p  4` ?lnpr@B=D+*&fQ \x@ILSP KKi Lx[P /Mʱ\npr=t]vm ,Y(Lʮ<6<ӯp {\+)Nckb0XE &}(+ ,Ψڪڬڮڰ۲=۴]۶Mpۺۼ۾ژ  @\D Y@  P * S< bP !1-ۨ/2ڊzl = Z + ;@wi,JPN웚-]- ">$^&~ⲝ `  l -Q,"ң Q`ƞPグ ΩR>T^V.ž=аb@Ζ0 \.>)pFҹUkWJץ ڭ~~`؂>~~W.{Ϭm *=>k,QxU A<⊾ꮾު΄>냞ڵ\-]m fܛ.> xQٖ= \ [ҢpN>^~ ^G>*^^> ҔP $Mژ P G.<\ب ?_= ^.0 PݛE0-s J QY !-if "Z/9NPR?T_VXZ\Zbd?f_hDaxz|~Ug?i_b>,ao. a` 65cŨs^x$ /`p@D?o?_ȟʿM a0? D?_{?b D: 0eh%QNE W/aΜ]@ĉ,^KTF IӤGM:Te!D 5@N=}TPEETRM>U:UV]~VXe͞EVZmݾWnW HHA2H!$H#D7 &,dM6@L%?$G`o!#$"RQ$礳N;3O=TNXrQ8+?(-$W(PHzF$S zhs?8mɅOUWeUW_5Vz-yC%+ E&=eK{E3?&Q݄S!Y[o7\iW]‚%/PABM })_kkT7`&`\ (QuipEDD_4=Sx3]F9eWf5OF9^Ab8< EyMZ~!:j\? npb=Fsch] :[>Z 瓽n;ooxO|y?R 5gx_8@f/zy(eR&,A c7 2/?τ'DaB& M#%І7auN?bMC1GDbD&6щOa0g@ǕjabьI1DQ/mp (B\vjHюw] H@QDDd"G ґd$%y0yQ ѥs7yus?zu<V0vi_;?B}Zb~~-<7l0l4g˸~ KAkY8;qJ{`?Q(YrӰ>s; ]bf`xd8Ca,hLSFL<#\vK83i@hp5Lx7G{Ss)Xwk9j(ɒTx38x k`p8dtϜyϜOd?:h*h`&6RlLp2@[f@8($2(b0؄*T7DF7axVxR(~HtHH6Xd'tFdLR1eq-X10I43E4U5e,-DPh(Ԉ(Z(LbpYX؄Z[bSlAY(SwR8ŪPQGIMxn`-0VЁO7$4oPu\8j@B%=B`oBdG( 0Y85XN_b]RWtx%X]]]EYXaHЁ#a~؀؁%؂5؃E؄U؁HdW ؅foIChZ05xfOfhThЮȺnvPQ#o9P%pWf]YIV@{b0ebXe-O|`0b (d!]VvwMDYdYeH@gHa|MȒ,ZL@A(C@]υ%5E=\ϲ8 D S1d4J _eHxL:<@͵=BT )^]Poh 03RW6` x=R8f Z]h-:i8X]WxcSpN>Inq6b  R @LfЃshN`I`U hM:pJ5xM`MW@OpCH^ oH<L opR(b(xPsX|WdabVfk;N^3UE?C0:eT&ShNXOPhH@M)I7p-~YbCb1V*WXepUpT2i DI JcQn[@C>@Nқs0MהM ؃Uk8=W6(ZZ`p:^ (Ip]g^z>]-h @Kñ杙ÀfK\CePրV]Rf jS H?xP&H^GvH,XSxfs8-Xw[(EUJ~[I+s'8*PN_YO^tpL6N%Xt=5D%s|LR\peWEJtLkLNOMPRWT'uUQouPt7S>6_p&Tr:*pe@M0Y'0FHXY~0H'f5:b%`)@GBX@Bw@1s80veZ莆,HukN(ژ:0Xu12)kx `(,pY6HpYUgwyOjxkZ@I{S:0@ "d(M20?H ckk 6 S؄h=>0Zq;$lX5X@@fpi(R\fH0XxNxx֜qFhGM>0a`nW ]F?U>l-g'y`H&^ja@6y%|GWgysbxȃ_hu!;H8>a@(oh08XX`8Y0aЅ[Нo<1b hP\S% Ecś+uB IY=!w. +a„"+ p(9s< Q9n85qeU .Ā0_ӈSfM nbrҭk.޼z/͌7nBFN^&OeP(sv&.@:8.MZiF.mTZrWh(eLd+u #aS4=,l1-!S|-rh'VVn:pd!(Н;Pэ?9"k٢|Λl4 JӋCz2a\+N  SXC'$00 `C)}ׄ"C< 8 (+TU @P 9/.@ wd("` э8Ix$с:$)պ$l(*D ua`l!r,hC+50hi )`cM&G?Ҷ4C4.xA ^.jQ ZF-Q _B!]Y07YFI2 L/6B/!].QY" /a Y|/ wBKX1, n4|A>c -d_ܢ2A`Sos>1,!F>{d+(02P.2v11w"lE1<eLx)|a_bE,(ՋXF-ePX1`"ΥH4ncJ :Q1aXN.mic\ӍE1 Q530̥,l ×^WQ aHcO-a>6e3~vb@#`O~4n 4Fj& V"C,L ȼA/B؉}:faZWeBm"=5ba` ֡,$7vG~\"!b)LPo4Ҿau(z(_ה*$ؽB 1ڣ]pe*CQZ]Yc.- v`GO.z-[%\ƥ\%]֥]%^^%_%~ re*.#DV Vb/Lx<)>)W&}冡es)IL}푦PAe&H^/|fn&t l~%HnYadI*uJB vK[ Fۭݤ r$b$ !fWQb"ۛr5WTXbVe,lT&dlYQY%a&0vc_+uReۛ`!-/Pf-Pe>^&gڜXmiN^0LlӼf}z&x*`mRftÍ:(2H^ ][mqRk˘tf㐊$<]n̸b[I [/h( bZAVbZ)jiA"橞)"L7n]6^=$[[~]̗ 夞% #x~rmΟ*y***ƪ*~.,lJ,V\blZ,Nv,ګȎ,ɖɞ,!4-xpL&v("v$M`GQd^-&.-6--lTklojILӺIf\"%~-ި-ٖٞ&aiMk`F͖Ύ4'l+%-.nv*6N-Fm2׆-m^."\v"LdNƊ欔jGRw"`f.HRFEn Џ/Mnl|-6oIN$(-vkl|e<㊗n=noBƯίcn//֦/+aJ&/.l:/) FBQ&n[l!xoklۑoŮ! o#0o ?ư 0m0.RX뺯'dl,0G3.PHphNޚ 'ni׬1 iq.pv/!+v2"ǯrl dALkq"L>3??3@7.4V\2U.4@GDO4EWE_4FgFo4GwG?4}F%PBFa(C4LǴL4M״M4NNjL/\[B4J/#R/5S7S?5TGTsO)P-e,QRO5XX5YY:3Yu4VsVdDLR5E^5__TOOY4BnuaK5`^?dO6eWe`ua_V5exI.FGvTi_8ek6l?vft[#ugRUh@ls:6j?\o7r'r+l*egudPhHgD77j8h7xx7yywi47zO_hkѰ805L}7j&2788w%W" dvd7yKxhwo8w8#b!50vAF88@ 0Jq:pw8ԣ rc?9GOB1u#Ò~b9ymupk9sy|(h9׹@{9eHxƚ7bw㹢/:3zyx3nƜ:g$ 7v:77:GvFw5o:uz:z:zyMzV:gϺ'/;?wqC>:ðGszw{yC??8d\<ú_dd>yGnvC/ {0+Ʊ/<+{;ǻڻyW.W;"B)dă>#H*^gz<0;c|HÛ/6@>D֛=r?A7{}ዽ/z;><ГFϻ<<3<ýK:G,8%06dC(`C,F)d#>;ċS˽,Cl^C88BA#%'?7>g>X;C;;tAC݋@ 0`A/>8ò4_ P.cB'QTeK/aƔ9fM7qԹgO?7}  2c!1cu/_P_)`.!{fVMW j"?T o_C<_?f'0Wx4*& Iҿ>xq.dj 1iаΝ4iٶl| -9L3#&-hk0x۫ ,Lp'y;AdJ} *jC2O1uPqa' dFA%[JeB3l p#LR%8>$,T 0@ 2*ӘFKqAMuIu܏^p!cZa cj1& -C]r8RsH'd*S0҈#(!g4Mg v1U-wj$\xӠG2 G< @-nDmgkdKmiT.ou.bp!Po?V'UuSuu^VXH&3CNXa@gpFvcȾ `p0E'4MYT3MtK xZNf!{{ },!.裑1[Hy8t$'||Ë.:dwMmT^myefy.Y']s7؞34-\pUxYzAc:1v'6#mD4q lZLng_BBΐ ⡄gTӰS6pL$<4AN# j_<d JWѯ]јF5)@Q2LN7lֶy֨SlTFSʚh *reX?>1av*R;oi<AJQ[bJ8 cvPG!U&Fd.5i,n,Nt3AY6jQ^;ڐmj3XF2!S; 8.gl4dyAd!x?2cfB}4 lMSѻG= Gbkxv1g)A;7%G=}3_YƁN(w ³ә64' Dj'QEV:I`f)O3h>:jCP$Ҡ@ DD9ӛMv mj 9 PF%D}gHi\Pqᑇ1G$XAaı* +k0 UNtpG|Ird,6pAthְcO@66C|y0Zqx3D~gz0W Mk]&95/t [0mC"@8򹙃1c S pFX>ǚ >:2]QO5w}hQP S_FT ֑$6 k3/c?2I"WҳM}s `AgAMu5xD)6yڮ18QmÆ7"`,)n\PJ.(pzAQ 2s<'8O9}QІ?Noyl4^u> q;exٖ3Ms<|G]K=CB5-`:Р|49 0v#hRvzBwcTY'wOAp11"t"}EY1mN3:Ox|nMb̀p -@e?Z?0 NDAmNX"k{m618e h,D$,, ;!D"o,(!8.ʸ(ʏ Ώf¡Abah&bAjn= DbPb# 0jp%Cb +9<́:,A9$fptNj {1eLA4`dDah^afJ !Zg I =Ѫqta36厜K,[d<ǯ2]Vͬ fQ!Ǥ@i:9I>AqPGjX+,1ګr`zbcdёBDMt)*l)U,Z<`A.  AHd7  ԯ`2AT߾ Ҹ"iTDˆbB&dRtt~т|Rk¸4a 1ID8"Aԁ@$@&XNA@!&$2 B *bkafm!C!dIlJtH{aErIƨ+{D!h!Dӡ\a&IXd@"aҙ$8l\p0!1C|TP0$sL)Bgr^ t>AH>4-< -D @dP35Ei5M5#a! Ƞ *2@p8˪PsS$B:gD6.2!^ ~<)h[0Q/H=>A2"2b/ ߀ @.S:F$ā 33A$L/Q8LDsQN=Y4\4`䨤TB % @D솠m'FAD o9DE,ԙ("4ҡ1fG-p N%>tK8>ĸLtw UaHRGI,QlTu CVSȂѳp(%+vA@DLoiF<$NE6 Ub{X{ cY.X0gR3Rfl$Nm*긒k}IŽU E:NVQb$ƴ'B_Eas8nHIa8n䡊:RO'R=COyKSg V{0}BEiš AJXu#v)⸔Ok6M9Anx!ΐ.!.H<?'DK6FӁLD&s({6Ԅ5Ld2ˈ@,f3&YĬ.(n՚#$튇(9uyD|IZS3.ht2G-J;!FAFơ.[EcѴuu@}"Ab6ʸPg` jBoڙ>_Z*z)*`zaaQOwB-d !zк0 :/qhB5[KHDb^gLH; ÏM~KA&D.SLNAL#@HB{дKD> j4 t!Ѷ'ٹmB'o,'? d1ՌMvBe>k|T2.w*@lnKfwo~KN䳆\sbH:6gdD'‡ B̨sMw"9-49'¸U OS7l:ŏI/5)rA,BbizPS&@#+^R{kS`C{b+N)*7D+Cߺ *yqM:*E*~afD$h`G cA!=r'γ$2=Ib/5 Lq"4d[ˈ 7b 1#N M$J0,V0Ǭ!SڒrmOT_PI5Q>AQ\4@E'b@:xz#_G r?)  f].2eAlMV[Ic?Ad0z?~C*0… f8q08D ௡ȑ$K<ʕ,[|߸p='_m9NP/m|ƒlZSɌՆm+Wb˚=˵hY`ٷb7޽| Ϣ Ó&2۞8@*[.# <aĉ뗱W[3xjٴkI&N=n :Tyԉ-Zi>`Uvˤ]ǭwu-o;<kؽ~/ȼ@K`X#2PHaE3O#ZAS~a}U2\V2O*ԏ6Î9S}V>(?pX>$"wBjaFd XP?#N;d$Y)eBUfdL-v`  <<8\!YIg` 2+#X|V͸5[V8dmIi^ڐNγ 1U5ּm;`Ud(Op:Ή=8<ڈé8$Óf!Dq og̷vh*hP. fk>L4}BGbs+YX9 bܴcÎ23S$.ۘwq>|R2Xsͽ?=f+j?㐻E` M|W[dA>bQI>CU֒A7?sYN2y8^"a+x> SX;^u-S<+c e(Fc[vg=O?+v[K\C:__H>}`{)`)J<+P?PTpBOG8puMݨg8I5a9t#ÈcX)LYǀK'lZkq<:Â!LǃEUK-~sady䏇$ 8m@M2p/wؑ.l#$+4+0*!z'&I6EhلH.m(~RRzv9`A4H7Wl +鼹qT(i8QO >ɕ`4Wԏ| v=L%LG q&d 7c<2`o8GbYBJ"]5IqFVk2c.|C4F-C5m IdSJ|dx y2x"ե]E/tLD125m &Y3u`ȇ?̅<hg<1.]: w>؁M:qD#=T_k0HbI;hy~qDڄ |*+C  wW]<7QQH MW?:M!!,K>iX"FI1Fqo0T$ GHq['S)1^|b F ` mQ[ id-Il+I+۲pdHpYnv*_zKq{y P2Z8a }\"˰4VԆ+ :1ʞ*mP3\MH;5+%Ӷiy>Hfqx^ч6.юYv^ 0h։rPs(Wֻeldqdq[2P 3$G< ctXQ1br`Hsp| 26]T8у>\#㽧|mc:k'%!Qe2(ٺu`]h:|pvJ[sku];Gb6$cBS do;#?ޖ쎵Y~|bXt!$Ҭ<-XKx1̔;߷ASq<#1z%d8>j"vI`B$[ځGWd}zmp P$da&*j2 f IT{gz$0 f٠ x%:D PVαdcHq!X6SsRV`4{$'y1 ?-[t’B z0a`IpL #{0A1.!:QBBz*4gP@"w`qP5 Ic? @iHA)U.![uXMIS}Xkݒ "%nC"Cj p~ P1 aWx~.9`;09-@ -s Xk@_g! ~&YEQ{098Dz0  BAgl8*B3UMe4Y#a@ >?ƌ9#6&F A` ^J!3T>pFAۢ&e g md]w6[al;RM_l\9^ we7P5?Bwi%߷.t`ېFQ;k&2 >0 ʰ ` [x&$:{9;Ѐ4-lm¡65,t<ǓKX(!QRAy ň[h} q>'aYX % y96\Q>0 :Uӄه\q ذ a`Nzp%b1;|S8޹5>ɲ5!iI,Jy[P{Cu%h :D p4Vb| y`v a070a3`fTw@ͱ &" `5i4Zi1)١W'Ђ JtPڰ2eY 1t-pfKpE@ p Ӡ t ѥ/7$t %eќ$GP ₩Ue$Pyq`GְpZE =֠ t% ʋ$a  2WO! ʀHhs e@p DEǣ>ZF. qS 񠅂q Tc"'B; 1'`C[_tj #_W#\q y0 y? Q,yw Dc2^Wr Ȑ]76$p2 \b2YwJ`opLI9->C-#?@Z*v] ti}mSQP 1  lap e y1 a 0 )Rb XYqC]p<µ#+W:RBg# dBcTg=Lp"p dJ&S1 ,_ g,lG vUE4t,3 @A p `p~ !{X+eR@Kg Y8`yF/hgFX'QHlM᲋1'P;Gd ᐅnۥ'"-9j}:p @ P  ?] oSXr '\0\lmNnk@ QOAX ْvxEf!pBN$`_C#1:D0_&k-`J?2h\]ЊYUԇy<IS%L?E༒$}46(0 p v5ˋhYh@ І7PDSǫ}xp)n|"appU'ˊ)ĸѧ`{neр(tӣX<,B\4I|?NL` m\ @ f6}qG'cP4<KWA$l5|I}":`{J3T i@DG#/k塜MG}s<('p,  `Qp5%ڰG,Oc|QmSCBCT MN:[ 1Bv>h0 !:CMY)8x-u۱2֠o"%iXW2}!T: -q   <Yځ  uZ4DMzܗ XPl[U"P %n*k(QvO d1z 028D>Io߂V ' 0 (Ѝ*`AФ!J>L~N:/ ] KTuvdk1t(j )^zOq :40IFaGh8^ O~E 6`& O !͚`qPp"I^.Ne ,>`z`6VpBhB.>hL Tg+?F÷;'[=Ws=g]](# ] ` &k9yL3R#_?0`! ;.⫷2L:ƾ.쮃"ClhhKV ,b.@T~"A ox _N['ҡCZE %ak.? .mܔ-R7 "Y ގIdfAX ϑn Yf%7g!%0 z'im"L`IP\N'`aak, @Cq`59|#u4'gIahq $]xFMlenfa ;}K ߐ @N00 ^'-ؐMk)xq߀BLwQ|941ꭐhQOe2|>T&H dÇ %NXAɶYП?+$YI)UdْbtĨdƕ +}. ENC42 7‰NZU(c.,_vZlJ'1yƉSz%ĨRmb:%KnAclC:' `9%Hpyb@ @ GmxѾ\lnDet iƶkM|X` 4LFӳpΫBms IIc3ҫHtl3(DI(zQ_Ǵ)e#jSГ#lIG$:"y`06mxlhv L65mxwI&lv  P2 2cu"شֿUp_3k"R%)hGb0֐AL7 C 8(!rt m#`?RF8QO20d" iǘD9zMo!)1”qЇN& `;aE/?[b-q v9˖ڄwC%ce m J^XYHmȤG:*@yBULct3A<fӫ!zbc+yYۓPI` ? 6Af!o4 LVgpPp;\>+^ wμ<оdL#(]x bqtBF`bN/e5>p̩%yȦ12Ƒs,  @-l`e]pF`VdVxH rrpT53uL@6r;CPyxT엁e. z1>(e0gT%?s cl{KpJҏ|C5#I%o@ fpF7;["XKt 6<}8et}CDPY16ߒe7r`,~B;}8snG)vܓ,4p4F`tuG<q} `#&QV!N?˭ ɥRu$&L0 W/pG*l߆?Ăº 2ǴCf1U!$e绘=7u?!.5PBp3M,z cZ2 D١1ӌ;ca/C4CC}0o1 n$3͓q`]e{Ŵ=J@o]`Chg?U1bH>Qecx587PX1pp4a3 c5H8E>)}0ZI4X 8BwHx#[ gqgJ0!} ` OS}A0i,X3@-? x0؅;C LPQP(8P}D g@pP3S >x y؇*91!\ \ph8BuhJ" zpp4SepgA^Kq Ñ$(8ފ2Z+ژ@x>x:<`?; xZp`F@xxL/k~yp"Dz цVzgps;y?Ė9AݙH&rb BzEx48؀ ;j$ ((&AM ( jC `Hp1ÀaX&@ZZ@e4a1xyqp0x%A8r,h>u0DG#R[’|v J3c`@|=j8㑇>4?>(?3; U!gpq'((b IFW`YV`@kTxZ::0`4(uxƴp@5 =5]̩`BKy6ckxp28ː d^ lR@c\H~aJ ڇ*B07dpE8UX<; yA38M:L9tMs:C s$ (Rg&䄈匍gpkH3m*d(NH@&i˓.$p7"d1gax L qp(39pP QVqd8#WÈl|r"/:h/=8Q  7p!HBc\hE=g0 H^x. }3Ny`֤M2>|h2'KLT/ Txh!(xJ (8# (jk˜3QWQ\UT}LPqI\) eU=-$ w^[ydeavh6T  HՊ茣p uq0@gw-"X\0O5`0`Z8Xb7̞؈xpňx(M5$#4Z= Y`ۓ=ai2rOn 5Z!/hy0V 3CT{6 Zڒ!M 8} =[XԽb]*k*d A840imAuJ/59K! op*j\xE ` ] S #(_\0]Yji&ʋ7܆Ej -S-0ޟLqX/a1 ȸ.ӽvX jŵ`RkP75 ]W̔ HאPvx i1[" 2[՜a2XS2|8 M އΐ۩CJ% 8k_N#WeQM ֔#P$Vـ`p2ib=VWuMQC((h׻4[]h @PIʠ]+j1Vyަ֌7ȭA]zH۵- lY5#X%zXf@~M(7+更R/ Ao1a(d`.)=G3%|8a1F~87 p&^/oy-][G{:@zu0`.^8r~ p "L8_mڑY~*̨q#ǎ? IPA~ r"P^BCjec:w@0iB(r)ӁIF2y%ZPSӮ^ +VQ2 'oqh#v,]IfMj-2i>ju.]]ysc>ۖvmqb裏_H#?Ԁ7Xy4 x cdC1fph&O6ۘÚkLlW;b#2H($!R6R.V 1,l#^bQR(.2<m2aw[R\?iQhZZ2>WxمQR#V"[je?h?e1Q֏:5i}.<|zJB$@@$B01Ce1F30Q㝝M() )2Ng8 #/@̟Ȗ?~V >n1Y–y] 8mȡ1UɣDm+kn Yd,M@ a B MSr?D! &% \O8064 |,`U93+5631jbv/*V&LZ6ΪaҸZϜe59&0䲏@CBqLՒKG,@t 0` 07뙧T:3֌$041(~jV8kȵэZ"o Ӡb->GA@,$Y! Z _?a}cvJ VR0.G8!?$lbȁc0ZΗdU2H oxP2 #C!<#⸘3>!-Y&C' )8ؕ5 L"FoҩNy:sCF<)5LMJ?Q lCiyCPU Oq6nġ\;ū# AćP i Y좰"?|at@ )%P1Qw] Qi<G( Wߝg`,Zn kh97\fm0yo<4S ղڤ\@pG;@0ø4B[+xfg]@F?MEAc)BQ/O]Mͥh0h'h, k<gd&$OT6_ݼ6ÌlgĈ YGmS "%Y>Xc"0KD;B cGm@H \x@1e D|<4L5\jXD&#p>W?(pC9F ·AjŰ]DLTO6s F&glFtdm Hfd<|4ue$=0#0 0KtpDCl%2%Wf$<5[V6`cNZ+/u϶d?C\>4[8a4x\a08@<\?$dC9Ηlb9]]їqpxe@GHHC~E6jMM]>H7TBfvN m%ni$2`A2Cp"A=1ۚy"8C2Xye|Hִ'熖\)BkM Ŏ?;4QRԔ65 dgPgC:`Uҥ@5CHivp,PpABx|F4⩟C͎B5|M0\< бF?榓͚TMŎfEJw(ՅC1C*j|[pWS?|C3+d/\2$̬=6h6*H e4aoElR5h(ρݡ<@dZCqS7A)DB0$h'ѪB>t O]g0zC;@V{Rlw{2@۾8|C>%zĸO[R~7 }7;?,^h8w5wa 4i>y0YƇvydfǠM"FTmT tGy)qy[]|C)"7.(#\pQ.;r2'}<k+0&1&hfL(?L`1i \p(gdFiG?~P!0S,Q~L5ݔMydN(kᨤJ}cpiap#F"(XuUYE0x`^8I2QD92p#*gU&jۅ+ۻqQNiMyfz%jB|7#ȰSC jG yUGJy^pI,WtǝK%n3 u=,6q@y?00"2&sUÚvydКiiW}(?aa~#T 0xABX󃒑slB lg@5ӕAݽmY5q4^û!zjZ&Xp5}шGWBP& ;W3ːkHPZƅC2@ * ҒfECQn$MGm{I^Bi oE1 ] PI<H6Ա6cQb+,ncsCFgb o|. dc(c,CY)U%87Z~ق# F ^ .;8)#GL,KFNx< NM!RH}xH /?lX3 mh42ʃ:)EšW#SHI50MGp$8F= J)JPZ P(0P*%j.ux sxG%ФGXi6HD:{T~DI)ÖLh]2qCGڢ 2p2Pu죠-} ?jI]ƷžcP6İzr \aBWh g#69+#dt@xDQ(0`?q2x³ 85ޯ*PpvA hx#/mDRJ!Oxv\"FQmZI_Ed-p0£D-I{ I[<_ <m2ܿPNظ% C!p<U JYkyV08;3VKN'?w! q2G$mSlgĬqIVɞc6e:QqzLLp#" s3MxRz["` 0HCI5]pdAyXJȌ흎!% +a KRC [NA$] C%"!܂YhAAl(Hb!#@:dK6jrPeO!Ď l$e@=*P.P=耒AȠ aAVb?1Z^P\LxpŎSr*#VaDs,M,E #"tVΡ6f^"ߺިk$L C1Fl%PdoeD.)Z~L dajp IDZYOh  $ P iX( (‹@Qq>go& |$AN~H.kl9E N! 6M.VN,D഑"+O 0e@$ !42b+m  @a`X` !bJ+Y!`@a"nB+6RU&fx>As C [!"".Ì썀 uD%-A*&b˹R0̲sܤs*#1#bVbv l KjrȠbpYJ?$27bʎN®0mn0o"(3o61@!]"!nΤ0=H xf”ޞZ`ю"K$2(A""073Lq[1,ّF`bYD.sS: ! aO@c+k_!$k"=dDA=>}>Sy !@s,RTКjPa` CcQ\2e%NJq7g*?jEg S&LFBLHG >x!2lޅePp^q&5lO,h6PO=U?#FABP FSlB!2[. 2/o.A65ck `^Nה0>u2%L2Y+EW :b!Pȧ@02aSYS5 ~3lsU%((0a@vY @ OaBt$ao.)ja} !j!-[kcofSu% vrG]'GdT.C^RѠ)ҏP28XCX .UiS*QZ5촡3@[>%DUecr\e$f/b܁ J;!S#gKgEX/D 20i sUUj-.|D`6f;t mU`p43ٖ4c!y?Ni@Zu9LbTٝkۖ!PoZy+ADh*j&t7%H!aWe(=._湀ځq!Lan@$Zڥr`+t"} Ɔ.tښg}D7#z9Aa|XZb`CH/\FW0 ,@G­aֆhR]DgKH `\c@efEZuٍơvx$n AȲhELbwo)N#ܶDc[F#x@AI`G9۰]oE$=McD$;Rưכ *b:;_ z7Svu3mIA[.aD$UgsSAhL)2 Z0JRF×SfE>(*Ybf2sEӧXrւv)4ֈCp9Lt"" #ŇuvX%͔LڛEܠx2o?(_/h )@$L(Υ;%Tq2RœZ\[[&a\d I?-02]7f[JN`iկS,*|q;>C%/*)b\[ .4\͂M!BGN@[E \ǁZj ^>}E`s1|%=xu=؝;0 @-LhSA£vaAY[0!^OW:S1*4>i5^+ !$&rdƦI]81w@kr"D$,"AS!cR 'WɅKL>Z%ACp)ͬZ7E{w"Ҭ?n"&Alio;$Lk[obC 3ARmZ*"L/ߍ1b?@M ^7.$,bh8K=&q %:l["!" XȰ"JHq"Ċ%^ ng O2mܵNy-9SÛ8FLl۸p7-\y(/@L~38~Xmʵ`ÊKٳhӪ]˶۷p0Rj!.\qcb޿]i֟?GP6! 00lŅ#^|"0`MgXu^|O}Ń0È>ybg5~!*āw#]XٶУKNp[0p-X\ފ}9a9O {G wѸlTs RiO:C8i5EBu ($^ dJю<{^7UN8#=6h?3>q3O.?Z1\Bk!P!<>m3c %tixy\upW+'״CVH\9^> iD.dh36L6UU.YbQ;c`UjÐ,C2 ̠kީwx(_bt 5Ө ͇e֏>M2Y K ɀ<O78H0?Jn8jN?JܮacLF43<<F;<<)L\wi^q\1#%?OSc{-ǭ8s8S<.T;emM8Yx;kD2f(氻.ͬ 6 8ld]5AiU#!C;ġ"30x+h;s45HdzbU`ml@f⤆K>ne 1%c\x)0k82.>!8vLj:Tڅ )7{>_TiOA8n"@7"aUTZǑvh8 wHe+;x҈|&# Xx l1PoTR5d3v$~Nc8pg1 }p!~ьz)2y#4GO/FRjذ/[Cl*zWp9~Vq!OP@JWGvw1!#2eQ=o0(y7u|zu'\fO"]5("‚1TG74ءxϡv=GU"'qO143߂tMx\@s [X'Hv''-"^(;TXXsi,3C5 Gw!0 †+ㄈ{NCjeX2ă{4tMSHZh;X݄m膐"Ȋn&rȅ !sdM #d8XxȘʸ̨y҇"hq8сd،qhXh-r^48HehL؏I\8ɨ @\ΥOTx ("HMc HlAH,Ӓhq),'"!9#)`0Y(-BB L9iѓaB43yY)HI+]Y;iHъpsL9NyDHAȕ("nمmh}iַ;'2MY 2SA,-9C7!KYD:[tD b)A?QXu9qБ)t2vyʉsDHțgA}IkYtci9 0SMe×t(2i@ya:9TMe[ 4s~  PT`_ 9 ʙ9GאGA jI)*DF:zGJzם˹=8z) 'ڔ @ #:4 )Jh H)Y!]ԀIWʗpX3 M+8YKʙfУDP"a \ zڪaP:a`\PJbګJZzȚʺ̪::ZꫩJaڭ * z芭麮쪪\`*jz*05b$!c_!2bI H4[{{ "[ɗ (Ɵjh r;=Jh?;*۳>@{i\tA {I)?c+CzZxQ?86ۋʖh9*޹d@L7GB&z۳s*[i1Zl[*귎۳K K{35|B 0X!eVr#?ٲUdy5ٙ1:Ѻ9ZRΔy5y H5!ٛ-$x:+kGTɊ! US?[zTx>I4#ĺ0UV$ ŀs7|#X"J! h ?ġG4\{qC~ױ5_@$Ha~{J1@17FR"Jyt0:0fO{G%]x0˰~mf`P Lo` @ @ E oPyP ȳ/!c ^ajm F8e pĄH [Lf *l,l @ŏIHְ[jzf  0 ,`i ? opxT ?  } } BX` ` 0! y p_| x L  @<y@ؑ y 0, q Š  o Za_P "IPF ƐÂ>@ }P  V< Moa ˠY~ ׀ yu"r l pv;p }P P ? NlF.P ~ ,kMMp?!0 ap ЬBĀ  W  63Q v 2Q3~" 0 @7``x@ A 'p0  ^+> l00ipKѠA fP r@ yP pѧM7#ԉn" Px  xA ُ ^ D 1¸75?$0PڻA㪐 Ȱ O  "K= O}N S[#Q#D rPDb]G}b0ux-?s `odԁPgА#1huҀ ] @nFn@V0 yОBh#iSuA@@ D`:qOdQ'Q6u~RQJӞdذ}*@bďVY&JhPby^Y~U7y) Q}=ʩnr$`飒eDAeABON34;HC/2#qTBC )i#g1FĹGa(aR!x|C?)FIFD <ŭkЋqZ0ZkAғ`UD8¶ۄ`(k]%7qQܐv:tU]xkt:E%(}y#yGEӡF.#KXN$v ]@Ωd_~Z҂yEbk9W禣Xрyag9G2ƚ>ֹk\&l:mZ׆;29ӭoywy^Wz쵗~}`U,(Ձ.] JDZtaohsX=M_DN$y~;A!/a 7@- @GG0~D'óADt<x#s~ vc)l [(1ьg]o|#xdb o7ψn$yıd[Ȏe+D 6HEa[cQm0MEd4Xa9e(Eٺ6tmL0z3 d#6dÐW"MM*f10k#(6 yXmj{_:bx e:չNvMF> XdiK %8Io ܔNi $ lHXH4؆;L`&@L$IpB ! leiNuSQ:`l j?J#:L8CpuґL@ L#f&!ւH4 }éإ:}p#ِGKV=WM~O:Bjy` #0 (@Go`*!*@B4i*/lA>PYu5a &84ȗw80hQ8Ld bh['BlgC ܯґ^nqzޛ>Td0RЇoxiHW*؄f'`k!O*1b5xSPb&_Xmmb,hAQxSH*HJpw@b ^d.m}: q*xVB+C6ȑH8Ua\jVq.0M5؂dxN0fx |gنk8_m]E |X08+&gP& rhX淘yЇVYkGjtd0V@RЦ$X hm).D`+e#ݕh#JͦȇeTئ29[~9$ϸز@#Z%A6@вGڥF9~;hBW'F)g | [=k={=?>>髿>>???@!h .G" D1DxJ`ebWR~2XAݱ3i C(&Y>99ZĂ9Uy(PdĠL >?:ғt]Coy `.[8sK:ֳLZ><#?#Hw@!,?mE&H$ŰÇ#JH@] 3jȱǏ CIɓ(S\ɲ˗0cʜI͛ua ϟ@ JhJpF*]ʴiGPJJի{:ʵׯ`zD*ٳh_R .۷p]Iv.ݺv-޿Ż` ^/ǐ#KL_̹ϠC iӨ52Dװc۫ۜuꌫ[N4o+_μУKת?~{gcw@ųgӫ_^vm˟O7װ&n!CtvF(Vhfv ($h(,⇴Ǔ@Tߍ8FgcOD͂eBA` = 51y.h#e8T 8\v`)dif&.ll^y8a HQD0Ġ̔8(#G'餔fpfڦ-v駠*ꨤjlB-tvR# 65'E`Q 2P86F+Nlbfv+k覫+kM)-KK, l' 0!4dx=QY-@qŸ8m0,352<@-DmH'L7PG-TӼdu>"dmhh)O,2A5{=rw"  '7x8r1r3-S砇.褗n騧ꬷ.n{뚻rj-/'hJ,C󯼡/dOtk, ! qC٧/O~ǯ8 `?/ HL:'H Z̠7hAba\;V0 gHf.tQ\E-з"}[ V@\8N W`C.rִ,l `@6pH:x̣> IH:b4 q1& 7⒘̤&1Nz (GIR򔩸d*N 3-^Qe`o{kA`a%{j=1}E/~A Ꙟb(2nz 8IrL:vSbx/n1g,4ƱsX@q,"CNI :9ZX/fA %!Jhqb`e*SgFJ4rp$C RfF*NSx2xE*MhŪqN1I=Q    @L@4`QS OJbI LS [N@ \U0 gHILNP~\g]F =ʻN[H Y.ęКQnpJ^KNHۍ%'^- ^~ꨞꪾ>M %m <ݍ> p͔P 1K͘ P N :-~؞ھޞ>^~!֓"&(܀@ߖPh ToĢ,7>B8LafP ?_ O0 %&(_*~(^N"@B?D_F$JL6_ڬ=u &_:^ Ѷ (6,3 &/`p~Xu/q{5~?_LXN_? PV }(?_/BOv ^娊9nyDdjCa M b n(O(?P$40?_?_pߟ?!@@ DPB >QD-^ĘQF<"JB"-[pvM\5m޼)5g~땪Q@5zt (DDZd-\~VXe͞EVZmݾW\uśW^}Xp^02ˣI@T+^`ęf::+Т4=dfj֭bZ]Ɲ[n޽}\pB+K0F|3SM7SO? ,93Xl O@HGżWNM=$  J-TeeYgESN:+5SSN0|XyZj%Q^ÐI%>(jL^{7_G OF% QmdƳUTh2-)-ҭo# w^}?9dGTZρSEB ӉUg)o4@$D ՈhIfi@$_ U\F&:N .ЂY څ6iF8N ;3o: i[[H0E6ɋ\Ԣ߬E8(?N@?`F3vӝg<9OÞg>Aϙßh@:PԠEhBP6ԡ3IPN'?5QvfHgHE:RԤ"hJURԥ/iLe:SԦ7M?;‡I2HF4b2]4W<^s'8ŹArWjViOS_kX:V@3AYAfk\Nծwk^@jn1Qv-%]PfdCDl(aA[PGKAS̶֥ ҂mlՒֶmnu[V']f0l${ R{CTG61͝gu߉] ӝwCfkzջ^?Ѭ0,'Ӵ ܌bQ)G_6u2p pd#u^#\P{πp\ÞnoE9st8`p$ٟ4ġucC'Pf?z}n]P`DMDA8Ͱo-䅋X .f1 B<-YZ"[ f#DhP2b)<]jj05ad p/ i?Ѳl:ˣ&1j|g|mhFg81U}e?{ڏl1[b,o[[k!؈u6n ;|؟ Ȼ[[hWq|kʟkXYN9ZY(WG0_/;&x;jpt?aY;xY,&r;])f`xW',F̶d+,e4Z6l8gdH7oX%> ̓P;s)XM6jxW4N5x k`XɳD˴T˵d˶t˷˸˹˹llY;h"PXh&]HZV82Ȧ[Of@8(c>ıbb0؄4B#U˶axVxR(WS52T`U(pmFTBiiPy`PQhQdG( 0Y85XN_Hb4c<&n'XU`e`RdUhix6YoЁ0[4U5e6u789:;uFDS9KhApHOpCZ05RspuvPe ͦ[0B2bkP#&ibY@FV@Nh(g[]e\b^ (d,wpI\D^UdA`0Pi@^o ;ZO0awxyWym4U+_.PK@a(Ж# ڰImD(T['(tPfBx`T _ yX;hX O0`*e(`I`P*Pp (>=`Ђ (%Ѓ X&`Cd8GHS]!%Q'H HX*3e@ x`e ,X 30dV5xN0Ӻ;z\6@x<XX>ʼn^hfH*p$Kn82xJu*h>X]t``[(ȭ#ESbVd DHk (kL*H< 8Y۹ۻp %;f͂m\A .$X$_]u[(XQغYe@f8:p%6vXɃyXἴ](~׀]+ %^3aP_Noh 03RW2`uxH#NT=scVHf:0oU@[Y(OW9@B5>$YX- @7xk!}Ih3uNFdVfz;gN^\4]UE?C`H x&XS?FPhHc'Zx'@^N9 xkh_pEX@pWP_@ (f DI J@_Q(N8eX8afDpMMsMeh=X0Q$s@d<>`Mg?w5R(ej>xaaPS*::D#a ļ Iu(RY0l-Qpn>pS H?pfG3Fc5КH,XSxfs8HP)XI(@T_;[=N0 ؂UaRP8*NdYЄPv[?@W8@`TU@H(_XT%^ byd 0jVj.vvoFooDg73_p&('?Mo`s58aE~nH}AV5^i PnJYn(563i?o8X7hȺ@,Rp'RY:lȪeE$E_[f(Խ 0M@X3 3~l4fPSH(_(BPsN@*\(/֍\tmhsNఝM>2u` hWpGW) (YEH0"]50sk IWM7a&oh0dhGW0=aWqd[<0(hXL b&oЁ>Nh&HCih A[H9`;@;P(fN^H6mw_hW8HhOe$@uk5%gw}Io]'9k#uR_]PfxP_0mChބC QnnEpF`4х<`bX-ALt(hdn\NX]/]PYNl 6BYՍItl9G6}s‰NQ(+W(݊fO[)؜ӱN6гLX)Sނ,Y%l`ΩyIX1aH1mҧRFզX&z+TZR%ەRJ)ШK `]a: -^EPI(Lh6E#KRvݺ%l/GA-_&XP pdJÈ1qҍs%eqˠ3rbPQ+,hXMNHX 7i#/b: 2ԻhJW|X35ʤ0gQe : J8!Zx!NX \`\ja ..v!2$"18#v`]@.2ItB'ִ# Ҍ+С!

H-Scr1eNj%Ҳ'3)#/H) +xEub (/p]d2J! /! % X7TYP2n1o(CML.QYHg'1fb1jhB ,f["]؅KgB-y5 1 u"d&Rԥ2N}*T*թRVj/* ` cXJ 5Qј,J1 cE: Y,h'ed32R#X/b [L;/Av01zX"W{X`#P,ƀ,| X@}Y/I'+ҡ\Rֽ.vr.x+"C ]1+_e(h@Ëi"u @R$cԢEjnXjP^(IOVxo߬f:ዊui-O%=mgz1  /zYBbh`fa)ۙRV2erYr0 t".z wȹi Zc0lq bW0RF.PYIn,,~a_U 8E-Ai~.Z_h46(aaUf(1 YbSpGv&u0a>6e3~6mw赋z I`|8W̤4)_a߇8Ew\71|tX1eU%u@m!dv+Gۯ#bXGp  >ڡ˗}Pa"r ȅ-Ӽ69ΛZp/;caHika s漰E/ݐao.~n_Ӱ/y#=95ˋnRhR%]ⴿnL9PTґy ઴3<}/vQ]̝tV:.#y3SLafw`($s`ҍ!M!GewloC2ߢ3ws-8JS/ʿ󋂞vRN=E-#k]\߅حB]Y9]^SY\\"ް ߈W-$p^8x=N䌓=Ձm 59!Ճ-a0hReV!- RE;BO9-Y!T-!!!  "!bf]RaU !tx~R(Fh"#E$ BЁ~(z(臎(ܨh(A())&B#B1BWes!p%P5?v [Έ%e.S>])Ʃ)Zh0i}-.T1i#m*2)&.jiChE*(L5|_Κ*^䘚j*N^*j*:*]׊h*ƈcLxj++50=$JZ٫ƪ"f+$X&%+S>UJƫ1g&UN^CکNVU^+.Šj+B$, tkZ½RYd1ݛU)ꫳV^xb,Ŗɚƾkz"\$찶T,&l00i5QUf,p^,d9+1jά~-R,-fR#x^b쵆ޭB0\Zl.FN.V^n.n.vz.憮.閮.ꦮ.붮.Ʈ.֮j- q"LB&Ü#C./6>/FN/V^/fn/v~////2#/\no/T5/00'/07?285\*2+g20 2@{hq e2+0310,^Բ-'ryɚA\2Y884g6o37w738+8d*z8)83=׳=3>+93ֱ҂C e:vrs([ҫ8A68 <4D4HH4I!@.צd@k3ZxtI״M4NB4-KKw3ۅ8Q5R'RIoO87uL/U_5VgV׈A;84A@&$V5[[7f 7\]4FOu6{8(HZa6bO28q*5_Ӵ_d?(^vb6hvI/7Xkd tJ tvEar[&h6nBv7ch<]Xkku.ksr)ASktO7uw3os lgE?Y:5+ ]}/>T<:`C;,;D>l3/CW;t2ȃ4:wi>l6g飾귾C%9?9`A4<;/lC&;0$:@_&TaC!F8bD1 `:y8d 4eK/aƔ9fM7qIɒ*o[ԩt۶iE 0IO(6y^7CI7]1qBk6 N9Srԛ7UթQpaÇ'Vq=:(ҥƩ 6dN_<` &6Ǭ{LMt*|)@(ǿP+j*rϡG>zuƔ>Nt{6N1iڴl5BksE?X~Ȉ''pӍ7߬l 3ȌкA QĶǠqlgy 0 vfJj,YO m~f ڭkh#+-Lh0,3l,$*XF4wi'= cp'? $ђчz (o7Ñ%qܪ\0͖R% TGsWaUYZs!7b.x mPj GDQG tmvFSN=ŭP$.3B KV͍Wyu8qɪD 4t'x )ׄgуleEB ~0mƿUz! ^Yna^y,1MxgHG?bs'ݽJi g&mw)AEJWavԕeN[m(N{7o vapÇi Z)BJ“]@cI}^8&ZԵAOI/~ ~G7G(d)4@ 4Nʉn!MxGm86 i|&pTxO;?_FhP{I#_ue1A\U6~ P֪5>$!AӕYJH 2WiXѻ5eSwq#^8 ȆV% 5C!:eG q!D<yc ?ằđ&Ѕ. ҋ.TKxΓ-%xɣ8+6ec döi(ܜ&rjZ䡨mQ#j*aˆ_œ :;eb)MR (<[0c"A,~Cg 0`a JX&Ĵxh($ф3)41xbKRV #gJ&yބqjZmQ>:]\є4P1̈trhm<`h]ĭ^uxbWZkLu[~uv$kE[֗޵CXmrSUءBW.rc ;~N: <#Oxm8!8<lz ;j5k!孇ݾdmt p>RK}`xġ )89;f#yY[+o- +d5PyIa6;(d,dU?0EAtFţp#pGNȚ!@]tgGNqdT~m%*N{Htc)<:Fvw-92<>\ d ÈpD)@ !)\ Gō yTz Ly$432J3,zj:aeYǰ _|#!X؁$ 9"gOCdWI` hugزi,+>iS-5, 潋Ձg`dD!J^A@l6`,D V/K,ox+jcz:'4J*f c&: YNdnn&QΚm yP MHD!A\M 0Op:,`0DaiB<"I!oa| j[FOaϘ/DtpxDi* fh dp,¼,a ]-2C4@喾P(/!ҡ@AgB !X:!f+jJb, YbJ(am&!AA$: Dq&$ Bvjz%*tFNx o z1n2+qsD^!h(!,^j+@!ё@"!sJM*K 6,#AHNh"ʢ^e`gFX&α% \e^ t` 2$ABl,CRF$KVGI@ @ a& !,?vz'P'l(āj@j'sFFm D  ʂ]C@r-w -0,6cv4n_0&j?:eIF,.3vS\hlVȏ3GmhLa>,] a3dӹjPØ%QA `kL95e9!9?2Rs8*S; CghB@B-#I=@EUKd !! Ex=- ,P|.B>U5VGP4V?0/Aa/ 4DOMc e++$edQn>eap#mB܁ *t u4BVUiBjL>cP"kvT&$-p%<| Ҡ+ôy|f(Kq?VokcWuvqÄ#B>dgvD> ">0AA(No (QBYjT~d w"3&fLNaz{k7f-VV>ϩA Z{ta5a`O(| XDlGTh-B"]? p ȊXk,2VEo>i%"7%m]xj,AN7GG_CNm`7%ZB|E ,V5.E`gV]ƎEa2;nvGO.pWB`vp0f!ZX@m}X!t2"l S&fAG2'b^B&f6y&ظ)~*[ȴ!i sa! 9AQ^1p_*f:UD3Oi%6z Z&McΎ< ODa(˰Vh QECIY4yx$FA-(U)Y(Dy0Gxey$K>!< F :yue0:Oe5O:ZKX Ƞ⁄pXrHfc߹#qo~Ȩ#.@A"!(+ f J L&yָVYwQj!!c05Xkׅ=,vW<Oa\+F(6Ea[hėej< Nb)phzDeɠN!*ҕ|=D$Y;H1ZIJ[j6Py%=#D# #!-\qZuJՌȦ(_d}* t& \Ɋ26mez ;rĠMkߦk캁 3p&M!J*2q:k{-¹ #a[,wJET@dND$D4\ٜKt\K2X1*3!iae)\cz!v`KS=L.♭EWMp*plRiZȏi:zIDEBE)"۶8ePJxyOO: Es9(ٛ0裆-$=BfR$:.BYZ Ŵˢ m< }8 4k&:ޛhb7m ;l z!l!^A~1u(JhNEdCqlUwd 1bS_ Ӂ*ϤsG3#KDb& d$]T@q  6L1ĉ+Z1ƍ;z2ȑ$Kj7_y/]:y.43Ν< MN1pQo[~ĺ.Q`(Q b 6gMx/!M}ڹs̘.cI+o۳y(mۆ2yPa`6:~8ɔ+[pa1&_VČ:=BtY0/!yV_:MLJ ʋ.cܱS.` X0ᾌ+82ۻ?q !v?KH\: b$ $CLD)M48U"nR[o5  QQ-?JAThUef!S\.Ybd1\8'3裏J=(AvC{~ j=@sXn"bLc!Hi(:W#NMV&Z5jE<ӎt/LMhL2Ar DzKo!$0VEp *+Vuv,Mr0k>*K&+DNӐhε.z q}莣n&/IF,q>׋?ɌF;YPJ HB0CJne"o?3Z&F8⊃t6f<ͫ.zq"tS32>()ƹ0p@M!yuז#J(+MZښ]bdV%7nUaxiO>  o\` ĸ1kV sh!ob2ЄbL[Ng5$36l/G88zW&jIlkA!/?qV=2#0F-vYUH YGkS6Bw"lT1Z Nldoc<ʈF ib". n$j#;4EyHcI`|Vcɑ[2t Z2Ɍky1`b`KC5fptq͕| v!UD4B;N؊\wB:ʀ_6!T7ifWa6c¶2GxpD(N տ(gal<2C+ov=J x^}ՅQ<Z1Q 3ۨ.l˫dc4accGv(>#e<3~7y>etGʓ9UP P e>'G`"e P 0 WQ r,dPi\H]w\)D|8"9>gg0 f`WI-Cu`]@^h2$95SbF@[ K p b oP  SѼ Sh ɐg"` 5 0 C6u@;z]65cX( "mh0qUgj&98RV2z|ohpHh\;4e&`4  + XN ;Q\!8Ab1ؐTwQTiasOKXc0AzaVvϠ^`{5{43B"]*3;d *G'e!ум"x8W"D{C,EHAJ5Q#1hWC0 p !`pr%/ulI1{|m1L55ȒRȚƀ4K ES,l ` b&&Y1ʥ̣|ha [L< 0HA}uɈL ` 1+Q!Ȁ o  `o 5Q5bTRn,Y> dK 4"74ah`4CrD7Q$.eݫI?u2ɻu)))x8O_LY[Dl] Yx6$Mcڰz o/: IWt| :q UN]+h 6\tV`ϐsd7܌dXS#q=̩vm8x16cfk=fdˉū A@p &0 y3cdt!|0 m9otU3`!@U* Tv#7IP|f!45 M !j&#qmY 4Wp*d& AjxL R' 0 (ߡeJR Z+S~/ʠΫX Z 3p|;r `7]ِ2i0h#Zg2]-&94;ぶQGfp @ `IRbp=m œH9%IF% p@S.;A6ERa$ȒB6=4? ȡvS4R0pf˰ 2L^m0Hn1]AʒTfU};W Q#`ؚ o0PU~y >ej״_`WТa-ES*}QW^n)C3m2i<b%D @o .nt<@ކr KQ۠ AXhQI_ c Q_zM=bB iKZ/IwGaMZ92M!120hQ`2rGcקf@>\AIIPw+FE3IMg> N^_Y: f6ZJЪp߉YeV)x{/-G洓2d@]&$`AtnmU $zT43`3 `J𘁪 O޿۶`%N85nG!E$YI⌑L~PΤYs&?"laㄎ5'~4SQe o_}¶2x:;-\v!')4]:{rN C!Z "xq'Mdib0e82@'2f aQSCc1}Y~` gѣdVm9HӮɉ2hd 'w|/`2Fj,Jeg(85R@>pvPbbb C SI=!* | Q%둩O-wzRTSƕf1'TUg r! Q~!"z2(4ʅ 𾱦w$K2|4dKtMi3~㨅jEkQ&@A,yۅ}1[¡gXxS`#/nv 5HQtR"ŠH4fD%DQ Rm$DDt ]NCp9`p?a0H(QPdH>0Ɓ g2 iP;|6 ,bF2rqp&w1} 7i8#1llcG~#AHQ"qEb~$R?Ze' Q&Beq/#Q "pǤUqruJDB<6T0 `;a$kZPZB@WG#5pB;4Tm^@F21vƥnȶ,&݉AhfV)0TwLBWJ4a1dʴxKQ~ e/Ic3r :!gx:f @jmj`~=y6яvxt";הnx͔e?N>,C,Q?fa \lŀLo,CxE)bXK&?#Ɇ\3a:_9b2vA~`{y8HA Udd:݉.*G˛1i$G`DKV$F$ě!}TO<0DQP(~b(e}xG7Paq]ۿ߳Ļ4"O4܆~Y-jPo8?{ K {gKvXeh~$ ָp }ІD!"" (y77>6苢$9XiM@lP~j@pP3 S 㩩mN!0Mj.9ꔾ Ur81y(Y)) h8x0_14 ΍i*O0%5 0P̢9F`ĠLoh4pſڡP)|*"]-7jRlTM(4xЇ` ~dac΃2ѡ`әHC@RXY$HǨp[pP2 $9ePp@PĒljHJįT[.1pJ'$VpZ"sQP%6pl8xmW9lmE,LT}8i`gHly [Qj X†\)0M!0["hX g0 H^q1έUa\ I7@^aeֈ(ӕ'8x8e @,: [cphmx6|$yk`ޜQ':8(^ȪB3Q51`nPFz 0L'ز˕_UyMhdK @|!2!ύbِ=y艱*`[ RRxa0F PdIVӓДv@dbD|>cPy&0V'"e p8: ,)*@95EeN" NAN3Hxeiډ䢟9mHtJ-єJe3ڜ-É+b5+c2 r)d())zv@ hy~Nd[!0chYFqrGx"u&b'wN9ߍzQOb:#~F*H βNy8Yhލ i!NBi"@#k=A-c5B7%kf{aV0KgX'ժfm|"Yias  N$idD(Dc.N}Ȼjq0l_p1>rtinG‹/ٳo&5jNӮ Yof :$@:Pl}XakF#_ Œ8ovw yWC6(Zn5k.h | PЖ/=;d!RH@fDF/d`?"s9O,y(e/2Ԋъ3pxgneQ~l,$h1 j ct%יgFA.2h(0 %ն& : 1 0i9f5x-GuXчixXF (*_&Chi2X"1sA>0K"^F@BW!p " Xk(> 7,'a09QyO';aO*u`[UOe+RB~dfF]~P"HB!r&r23es (t.`@[Ά0 E +дأpO%ъ-u8Je=KRp@eupۙ؈a~^@@6Fm7}vhOD/`-gmgN (Hk4Hp ZŒXv3Ws$zm3NQszbUzJwhp`R1a ~;, *:.kwʻ琼_vR|.P dM@^o{ׂL?7Oxqؒ`.5QJS&}4h;ا.@yHp!ÆB(1;L?L)uv34QjZW) YV@P  5KJON #T?|8_>& 3d `8s>Oj7d%A3f6\3~ʲSI834ڈ<7q0 qUT[T2v@/O;q`6OYPBC 3B؆TÈc<0\pA"$1)(b%5elɈ8 #vgjT 0,!#Ź}lm<##Ё!VW@/"V R! Q\v\gSڱ]J4KB(2!8.J$ (+'<I`hc#ԡFC"$ChQG g"~(;Km1]  ɋ @r̓@*B)V1`HqJ]:,&ABu%,I"!} e V˅xQS Ԫ376`Ѩ:n#ZeTKvNl'clPb ]~xa '8f+#AШ2v`0І8"|ja{[9qR(V6!îf60!TB{Jұ̡1*Z*UǮͫ^5G^Է%:lCةB0$(S!E.4'5XL!Pa ]eh-S(X>Cp 760A 0eBQ? :pC~H80 6* ~BoU# "GьwXcvÎ!lw >RxG;DfE<1`S.b’c ;rnFBNǾ R!Hpw>"sG ~LE2#xE9#?jG oNQP2%!xxu b7?ݧ8ä>PU!C8X隕Y5JC{Dߤ˹}4V 1 O;Ђ.K?pN2,@RC>2HC: (\8h 1'^z}Pȃ5DIk` UqA[z}Ԇ>ă1F`CHt^]V?$OEHC6$v%5(SFh^( [?:?5[&=ᚰ!G)D[cͅ1RP7!XX`CiC] ( DϨ?H:d!l!$5ܒ BMȉTCF$1y\?t4Hc%U.^f$ZچM!Y0NOn2 5P"z!%R6%Ń;X<l5tWBpD 2$CM¸Bazjqd^NeF8(8PF%3P'EKI\DN| ]m88-3p]mGu$ɴoCUƒ9Aa#IAZfKXFE-"&K!}.œ'p? PB͍GdF@%TRx{\ }G^E-` ?8$3D:DI8"H($?4)b%T4H]E(B鬢LK6+E")#} +MH7H P> WWZ:,ާ̃8QA:Y4\C;5HCHlȄ8EϼV4 !0䫂l-b,@BAX)IuCH T8,+QhX\ɮalL4Ԇ=^G2͏˦DFhA%B0/6 RQ f|@cK^;*Z .?XrnhNqjY+RE"7Xõ-*_mtbG&y.1oZ3'B8Y/qiq O#:DT T׊S ?e^#I- p!brXj&vj1 7|C-P"L"\@1X{BB6+p(P/J;|{ڤxpފV.ElCO!|Ds?\Bj[( EB$J@ ̀a68`OB(`C3;A?jhXR }< 38Hv1H.tO* wo. B`4B ЂzjSdEGt `4$etA?lDX cwXRPf OSW`o41 ]8.̂Mi1HC QU5Ch7<0u^Chf?nMvHCn2<ю.kvmG g zQb@((C69i[Pv=4ȴm]_40D8(64Mp=ժɃW|;h?xqCw|K306ALC$8)TX 46UrB*pDau2 Ix{vc#2`&Y;x>S7xE0&Q/DB,DTx%C+ pv,e;t*ăIlICh1ǴsFJTmRDB%DqNM(ØxtJ,c^$DEBAE'%2Ay 92*s\]8Lȃ. ꕯD;%fpt]lyz83OP^xGD($XܖE4\\ױzgd4t-F|< `:>/\lED~y;CtAzDQN;{SBsx[A2tBQȉ(mK{~cSǸ8i$.̄64t{Xu/nsz`aO?( T'4C@%LQ=ʉ2C(ʇ4ҹo T0IjRXySH$m;.3`z1af#}/}g A'&,u T*,O=Л&@XӨ}ShS ˜ՇHIt5y\0.ޮᓾDn3_GD p*C, wd6PR3RXc~%OT%YDP?\RE{n = 83ľATmyN&~Ŀ~ځO޿ 6tbDȓmq,`L"Í ӥ_p C1x/?C5J4eRK6ujTSVzjCp+q-]ImցPVɌXUQmiqbŋ ǯ]ƋgbHcdy@~4:qBxk%heKQ\:F͛rpÉ7~)€;v!ӮMmܷ|18yrM+ 0edw~P~ڡ9y>C˘$xJ/Ԯ&whs&kFvEG!~ b:nv|ExeA6Y4{FxAp&9Y2d`w dU]'Kh[g l\1aC3Aġ<C" 81$'4d/Dm#}Ўј&5&d-r(˚DžB3yCqTXGijel0!!+T#!XA 'h1X!/ GW-S\uD$]LF40mSڱWmB5D(jd#89T摮 Ap`\~G8PphHn0 8jd%9q$J۷̩=f*F*(4T yLi#64k ^ 4zD2JF6L4+4sHI-e- fV/4 qʤ2Oa dӢuIqR( >Kʧ'KÕ"n GSZWΙ҄fQFA }\̃cZrU%_m#Uuj#Zw*kζ!֜bA 9x#VBQ!=HmBL䱙} qt' 1D]* ;u(F1XC), :sp-rFR/ivQAb!H@@ (R DPD!, 3B-Xa`"YB2pvmMe aJ$ӽZֶ`.8>Cyx\aGF2!W) q8yE:! (Єy71ƭ"HѼ c}[ȑw\nbF9WrUׅ9Ud_̟r]ymf%,z#CME(0 bX E1(DA'!4|*۽JBq55Ȇ:Jy8#҆6!`WkߖZs@"En$=r_-6u!$|)g]ȳu.tdГ0ӵv(6G E$z82< } |]B6E[M!jڬrvkYA 8};DN|I8VB` bYʘ9y3em.t5Vb-G]Oq|g)#v_~E$6# ܁?b2zdMQӧXF:BBFĐdm(abM"1Kq)эFc*)Va~ 5$0c!ZQ7oJKF!4A(<8R% 4R%%.6Fc \2kGx4R&x eC۩7(вVn#aI&wcC "'ۍRAdnѠ rKb5L+<2-0/*7*7^?Ba;R< RdZbD WOkΕX55G-,deF13AkL)E&3E0inߔfb.c^;(P[ON]jA'J _S"_,n#A,naBg43; iʱp*Ir,ܮUHW=N3fJ(!4H fV'V F#:XƯ$d G"c($3$*BC}0t>Ȭn!KmaCLÌȀxDL!CFtZ[`fAkiTZr mMC8BI{0!i.`+feMP 0E(B3 NcMNGoiNcGa(I@.dx Dtp.t+S??-w!xINJVN6#!mTU™06M;A; AV/&lup4\Yx$J!f &HQ'aEʳf?Z}7UaROuO2$$7a,BHhܘeJfSx>@a%bi JHYTq5oa](!6IuZBmT% kްcc!dc2$&PRb^p)'fa~֣e!&#,h%Voy v[K2_E4B4P'&Bp4Dj!rgeobAz4xEncK&&`ƯgrS uoW]jzPh&s :)ޖq!,n!AYaNs<t_3z7kTuwZG&oɼ(q7vjVs55 }7jN Al&Dw1cBg:ڋkʃӸeA G7N8!6_*i P`!7} ~&V\%ܺ0HL gq>4a4J?+bLJuZ)ÏCK7o(|fJ9(ĵz;܁l{Q+ < Jơd^vF;Ⱥ4'c`ʺGl"|DHZƻI>/%bjH"l-6W(j-4R%rSQcp0!gBN6cԁR<*ˑg 66!!{Oxƒ{)I I.ƟDNǍ-nc&ycPtl qɏQqs%A.w³Ӊز)A^Hr M!@A4r!r/Y#aa]L+X6v ,Gkڳ7PިYT{=-<ӿ}8.(A(tas{ġh)b}2h1tjN\ml:#6w41<&٣m<@$3~r} q ͭ5S5I7{yy `G"drd )F$DV5cӏ +N[O'DB1k.T\6`{!РXe| vܒ32"&Aܒ ?1=/d JSZBr\]]|5WjGg3A ۍZUРx3^FB[V!up;V?<x g>An$I&| XUPs:`*6U5R-J%Unoպc.BA_"ꃣi?4+ H AuLi JHŋ3>xBƮ];2(K2ۿƽ93@My8mǏ_61ӥq8qҤ0M8_HիXjʵ`ÊK,UFJ ąlFnZn&MOfE۶dD+U^/@:8ne8øsͻ .ͅ\6pu?O4(e'n<ҁiPg ngGzmdyz~(V01b V$^tեVs4ͭ`X5>Dlsx17͔S@ӏ{M??IsH&LZ"ʄ}rL3C` C}SK/ KPN4Jv ZmEW-ǁM=bTd4 Ov:N.<ī5l1!C8Ӻ6 L~om 7Q84&3*y!l܊jE?}WR332ʘ< Z\U2H26p=ucOcD騧) .XFyM8zc.}#ãLmӎ5M[%zL;vQl~Y Su8}$KdL;xC>GHK2rP<8Wy㴽[%VhhTƒ^53pfPU[&WQHقY|*QknVvAG24Acpc >GJzt%GHHbL~v)P)M:nC:XDU'Μ*V\f7|[c= `gU.I(p1HSl"FmEhOFRݥd4ϸ`*j.c J:} '|Ow۸P4d1* a<5&>_ZHp .ꑩUYB<΅ a<ڰru#n\j w}[Gne5kt׉}-D2@r83X לV}'x2 ԏ2(5(4 Nɩ:Q3q;N'Ʃ CA2JTxso[ %<~QzHű$x,9:3 x$*NfICcgԙ04DЀ ' egհTGa t8Jqb00t s}3D ͞CUtP! :xE)P X8X Jh$s7mt<M/ǹ vV⁓[c`G D3#(bw7)B箑Y4 яp<`G6>L pvZ:|ԥ} 8615{ 0`7e;Ҡ˧{sIY]!D^q HUdILޥ-$d5y ;>#kgJc>Dǽgă ;#AyщE/0 J1 GFK ҐWĀcR`W+$d01;==~~&rvKP x $aKBX8=:!pS3q&3$g;.G0cpˠkG%wb8Ys o΢eYc7W=X3ɠ(%a 6=1#DU(rPuS3Zd~b0f "Yqhc6h `"l5Nt J'AM 54,WPpXWlj`)!.݅6$҃\a\H3H Qz44(lZ1rn}0FgX>PS8Y>u+ !eD6Pv9)%> 1ff( dXIhpqSB y!"1 7.эpM0 tg)ٔUPUB1Pu(Adp c|i#@pÐFr9@f!B%HNc`c_ؒtq Z tPC>a aC1S04zPf BPY+Q #8E:7b?6)Y( ɐa # ؔ:fiIh05C"#E-c)rCDJvNUfv1`ٟ4` X/b)'wGۆ:J FM!y҃T>5 *B !Z&zX>XQ (2* 1\3z8 #\F>¡ YV HN JPZ;XzsQzXB\ڥ^`V!Bdr'jJ-Vf!J3#QG⦀7" b7XA0*[:- Jp ]QY N / ": ?& iش2@ KV Īas**xz:/ Y:a`p*O1 HbKvZIb$fPs*UbbP{ ˰fk+۱a0$[&{(**+.˲2;4 ;a<۳+D[F{HfN\ O;F5{X#2xs  :d;z.eKnK 꺮2t+n{UzK3t[mkx[O(ts /[{5'۹J김+E1i b[g9^cda!p+[)rkzbѺb tJ;ؤc6-a+(90ػ%+;--,һ!+D.&::˷Z5d1kZ ޻7 aB{n7b2iA ʼ9c9A ,_,"9)=ļm }#dz' NI kcb19!aeU"iRnBL){,G; Z4O3$#uĽS,`F^:OB 0i#ZLsel;=L?ƕdľvB Q, S^',Xl9A,c<ɪqȪC60 0`. }pS' e 1 flq0`B { r`yP *o`jSk   f : Hش.`M :ό t@f p(Og ih} PmI>i6@  =: } "E0 @  ʪL y ٔp y0   Ő `Ҩ@(!{05P 6   лMP ` #a٧ƀ l  00 F}DS pـ א06aP s ~ IGMX% C0 f -@ "w,=]H x 3߁> ۠pX p կArP -M/ O 7pap M'`軱}PF8 o Phk6ʀ~'}  . и;ҫp y:rđ }P 23N ~ fṕ 򀪫z' ,]r veQPm ĐPo G !:ѐ 0 u@oQHث: {Pep! @ n eb #/! Ox@ +@ A;`ʩ:p0n 0ֻ۠ƌ 5E PSP  y M Ϡ o`X` GNϘ5 ]B'P pe A}lsl3x/ 5;e1* Rݾ? o@ ,$p/ydA} ɀ x !x^FzA o'pt? tQPÀJ!VԔ㻱(Lf$R.pP 5x m?";`k `]4}- {% -:с"0 oP- o  %r/ǜ2s?j<;tWgu_=v9RtF=www_V[Qx,+&ߧJo'^4kz{g_qۇ?~h~*E^~`>~U.r>o`%8A+<0|Ny.#S <e#XKoV@dd!<H|RDֈg bLِ#N8PkX3^@ 0A% 'D(DJ<@&5C4ktu炊BZCb0_`Zd#)xG$PhtF V$@ @Ax sa GNqCD!Π7XE: b6HA(Ix F;.R2xaJ?Fϰ1C ` 0 ﺃ&eI`DL!f X$!Et T*A8X<HZX  #3%P `@ 9pz AFlLG\ӞEraO3AG)`|#E2V KzCXE3C,H7`QZć)p>X@)(9Phd LG2\'X8JW

z¨jLK|xL|~Xp(NW' &-BwH )pxȂxhHg} H3nX*'@@z P( *W@A0H@0SOHGX5` PQ@`F X Qwo W`:XU0ȫ`` Ͱ;$KEgX:o015`'PS`qhxHxxp|pj(ă0xP7n1i,pqJ H|fpR2p589 zH`)"!p`ӁWVЁ5< y"JItXRYvC@8]kBwX˳kH6Hkh0IkQje9't,KuwՊES XymOty҇5`LX| &_xhGw &``0=A0xqe=0 $x ؃|pVk_x=x'P4sRפѠ@={ßw32o@vm3Ho#)&y0i58գ[Շ-lRo0lh@ix0\R$xg'S5]P0~ U~$X xXM&M*v%Zvhl:Lj>G}Aypp'$^J0š7i3`pkwqD!Zj[ÆFMCix(`̇겴|R-}PqC1>p<\qTrkhiEyB& AqM!3tix`銐JU0幬l$rd&бtX uyh&|iQEҡqc ϔX9'$@'A^\`Fgt]HIJKLMNOPQ'R7SGTWUgVwWXY^[u\t^g^oac7NC[[hAj߷`lv _ߚ~{\lWqψtWwKv~ Ј]vr's_}wwwvA.IP?!pxx灌0luox_vw ypyyy^?O_oyzz IWtZՂFr3xx~Zhg{i {mwrXbx1oID1((xe@@ȧ|ȷ̿xΟ{'7GWgwׇؗ٧ڷ}Z[b 7`&0|la7R(x8'7GWgwG L R 2l!ą pC"\$Ȑ,i$ʔ*Wl%̘2gҬi&Μ:w'РB-jG$|@UĨR2(! $ᐯF+ڴj-ܸM+ۺu/.l0Ċ3n1Ȓ'Slr`]l W6FxXPxI |Qci!z!!8"%x")"-"18#5x#9BFkG]x$I&M:$QJ9%UZy%YR#`1on f/k#q9'uy'T-@+gRŦ[*(:(+c[F!)ٲ- 0,,,3LRKB .9c-j ;,{,*baB АztmZ-z- BѴ n.;o\VECZ0 <|p!,pr24D[!;z1!\r"Dy$-pG.|3s;/%B0C@'<2?3B Ax铞2'( /*K%: CؐP,x 1BBZO"-c(P|hd`)T'HFP⟯>nCCDBF _P(.@  Anv ~gRx D"`0 F y"DA~+{2 @" ;wB!:Q@n5It *oJXd`iG ¢:BJEJE#$VsՍԅ[ kщyļT0p-PG~5xKpɘV$uPļ6. Az"vo(R -yʮE ZR_O @ښ MP(ըhghbaU23  W̪S;Jd0Y:+U@Z%3{Jlvx'mM^lMwmڷ/nbwߔ:[LS S;Y~?4! v`}[#LC=ck Rq`-&SNSQw/2UcGF0d( $OYu<@j1W X6NT3\LVPd>wÎ&eC3ILhh ?яN# 7Q{B_0lӟH @/xCtR|^l]6?l`?ilЇ6=өѺ5u bmC[typzwяy͔Ie;Tg[۾n,}?dc0̖A\"ƀG2lm du ~;F>hch8r,C7q{6zZ6C'?:=Cz2`81 lk}=E?ztg&ܜ04!h̦~ 24"hrxGmoF[B9[dȏ*R:i6i}@o 㵛Xoȯ >8C؜5|0̝ F'/ ²z#^ڐh6Աvud?:Q:p@tp-@xKk}~G~~ UGw`  A;mr _rtfAG'v ʐwQ7: p @x# DH~t'p'ւ -F Q d][w 0Ƞ!# WyK\2 >Ё-23{TE9!| uRc`Gz\m} ɠy7 vzx؇p sG]y~ qA"H}Aoȉ RX9#v,Ȃ-&azg dhpF 'AxΑbz&rPr0]#H(;7v(kȄ/z7wuЖiG!vq.nX73r/Ƃc8) Yt/{(iDyum8 0wvIww 3x tf0 YӒOpi4 kGw~~⧎cwA 8ˀ` B`0/3-pd@hyvfI<vW] 膹gyLi'lR 0~ 'ǑyI IxАti`vm7̗Gad` qiU'Q a9)m&ll< ]d>p 6! 7Ki fld3&qi+d\/p""A癞P9MH6Aw /~ziZڞ%'o%ÊJfv'i“ MѠ QXuіy@6 8y11+"Hc gCx~m$j `1v" ^bz/Afzt#iא9Ϡ wHa i)t{٧7P 68Wc I:t|ES l8`` 1&M{pCӠ"2n2s#Jyڠ 절G ⠌#xة ] 3v1 l,ɣ DJ:_H^sp&& TIqitڥ *y]* 1 0 `h~-l qX Fty' 8yP '*00AI,$ڬUjAf*`mЩLW`nF>9)܁(ycs= v@ dQ,c jgiHKژIwdh:tmpae7tN7mqfq o0 &_ѷDag٘T:m >( Ұ~ߖi: /N´ 0|>( s2 ;4"iQ}.VMBgӠ7:8w4ΚJvйe^-~/g}Zl(   O/MmdYw>%˯GCG0~*vJѓ/ Τ4+t>x4Pqp0 +w{:˂Ŏϰn8t Jx&.m|K2t`pkX^kN&Fɍ;'\ UkHٽi>H^`u 9i0N**hj/$_~'m ͹:, /Ft:w8/ א Qr>gA' p>zHv}9 ᧉ m2C@ᨈ<E]6vk &7|Ydְ 4?)ۢ}&ⴸ< 0m9ߞdC6G󾐑e ;룇mDxi#t;` # W {zlB6i.:uC(/q"LߴiCMv3fy+9Oi54@L5męSN=}TP>OoK27_QURV] 8}EVZmtO6N[j1 xهF š/* ~N*I)M$0F5Cy.&Ng)6!IqBE2J2tԣRGi|`1!ӝG5yCBl`LqEq:Ϝji dJ:OΈTTȱ>DjR>khmd'ihm''TGE,0¼ NGEtitIG,%XMxiiX Hi PDkSQ7M~@͔SSUEmѦpె92h#0q\;Wp&x&dO e|XFgG]jqE7͠TgP4" ٱ ǚնkkcJq+cfCjy(jMZ30'fq@9TMKNfR{0I:jT\fr:n>{9yOVlA )3v2˃Yca^ᚹNrUDp.'ȓT<ߌt^3HP pI2wuk7%j N5*Fk"O>vTigJ*)O.5mN39cL%c覮*mc OgӬu-1 tJAN ȄfCdnQ o*Ӆ`YTU:)hjɀ6dDɉ6f-.*67{IqdSF #o ij6QX. %q.@?lTЊ@ǁҬ6_= HD$ae`4gxmVl!Nԣ` N8 D!xy8L]E?8GWh^pE[5)0Ʊ$/)=%Mq)ŴGhD(uKKxmJ&>!{vv ԎK2J4ט/ :\ͦ2c6 @Ƹ4dF&u`W 6,`?kt"j^ b W 4 _e֩(ls>L'-sxXCˠ#!$@j]KpGӐJkO.p#02IҧFQ=pЫSig+ЋpT?X\×kTmH+#e#S{jCS2TX6Ax3pt5rc2{*`_r50A `#E67?0L0Fh<ފ8*E%xªEH,@ } FHrVAQM:HyRQ r3ptIE񈙋{X!0%jKN bD -ϙH"d>= EͨD#^˷I(DEvA'wYt62=6O) 5_eSȔx%yjnw+@F3xDu%%o6}ev_-@20LI٘n|%yK@Վ.ni>!sB6b+?3:#p+gg%88 ep#`0jn04r2X@?9*t1pi1wxi0t)܌H*)#cx>>P!4ܦY/0ԲixdH*C̊q.`n݈kÇz::*  2A-kyAN f*V?wcWDt r F PIAl1 " d_ram XvFqc(kְDJ +9= +3I65xq`a3(ljljx.HGƈKk`  H!)9 G,&Q"(1鈏` ܆8lȎT*|}OXJʡwPy' z$.r!xP?s4#9a^ʺ8ShP $$hʼnD1B0*A q#d- ˞BM qx0 HkvvЋAXk(dPPZ-sW蜙< |MdoЇ|JۯݙXE{uy(Z٬"[}D|=}x ًG[M.%QXX8`B+;08~5̇Sr]]W 72Xފך ,ٱ?`Q3dzex݉g b@dr1%dй 0|(:%ނM]-i PM5"! Z0P$ah_M!50t mHyP ֆ b{Z2Ɛ 0 `9 .AQas}bx@$e`w Q) 9 q<^C` gr&M h9ɇnQcKlpB.9Tj5bDa`hXkkЃ` `\3 <b xPR\Ae q$PyQyTlefڈ1YR&AUBD4i9*WlPp` `xX> Kpe~`r) mi 0hv6 vWidPS=o:ICJ|dmh 2aVq؄XaXh+ɤhxkhJQ0 *ђ_xXl/wpL>9^ig՘+>z&jƝTܔ*liꭵ=(U>x Uʢn5u(xWB :aяc8( Q\ N1 ~P~^~!bAhK˥y~Z Ԇ>CZH8 k(@`-ލ d1xQU) F3;>0( _]fc!i@pֽՒrfmQvkn꾓-oԘIb&uWRU3bHV7*< WxmP@8D(W5}9*3?ʠ}e xPd@Zdq lXmm-A,p%ko2P`muk`FX]XmpoZԟ+=^p%LӨC'xۇ<Q8DOl?@`8sqؽڕ-ex^\i>KL-F&4p`XцH"h?g7gxZQ@'>  xBW x8]ZJȃ7yt 1 x%й4p ڽXQ4de`ဇb؆`LgPES֓1QX68a\lpªv/xOrptAgq4{\j? $mvpw= xP'vrupkh/ʇh賛yE k78yX훰G7vZ[04hrZ1; 6wx" ޳ ǠPv'%i>U?DiuX45 e0~h4.>)˾ N>Y{[) `U'ZHxf8Rh\ʨݰ_9XZFxg(aP],h &gӤ#c6lC֯_Ay &gc r$̘2gҬi&Μ:w7Lv۶(Ҥ52mt`dإ m: 7n}L mɖe .ѣF~N>Q quN`^ Slp0XZjnMJÊIÆMx&'VD^W=zT03LVl-mkgz;ikKnͅd}$r?x-o=h7h`-~T*|GTV[uUaEZhm3<S;qs^EN7"B!6` A"I 1—fpg0Mg70$ P4p4O,o's )Ne|#4 t8 y@S:yʸ'};c8DyN(6|d <\vd4A6f`2`d\g2`ϫ I{舆yu?֜t:N4bg0QJ-;ңxC*R OA bc ȓ #o 8?cG>s3@h A;0Ŏ _ +Z rkn/Apz45(cD`ksG]}YMSCծHh7O<8,"V $r{VնMq;w>>`7܄</c'.nX5eH8k`:d@Cư3cu`9/F?GE] <%+*^qLn p_'~ ƶݭ0.JذtX O!*2[Aq,Cdp5#yt!I_QM1 ! l;kIEiWŃnDq{r,F &a*  ;⅖Մc )>2ڰnb Hq2˸xŒmv@v~KCx4d!r%H_'s" #NWDL ^A) WΉ(󘇊mQ0(TuRS؆,a4!ܨ2܁ -%|2DNa3`]4``¥-e_ `Z؆VDCJQj;Q)2t*\sR]hGE}$mYPb-dB֌CҀ.22*)JP/E-XTx(ªG*N0` ذ+QEk3Q`(2⤁Kn1'^q]Wԡ ?Ƥ>p|v-Ȇ4.a yd;xRf5,yҨܑ5j92J3I6 _A x2Z-$*3R$D( Ğ J]A]_q61Sy q `\s n ]@}C/`cx!lӸ= ~,fi 7 F2ڝeIWY"ӱFh9hrˬP"&;zEd1ƻ2-XVtp *%woJPTWE+ ~U77HC않*I PMA 9(&/htV w\ mϋNy[Hds´@igB#?SC&㡪ac9MQ{L|Ti+HE1z{}I\zLn5<,ze\c'r{:E<w|RD;M#UC+C6hyg8('>\l] OtH3`t>Wὠa c@΄@lEV%-œ 1RHQdYh0K$T: üɴC$Ɨ=21X3(\l%)+,[Pq ?@DKs2|b<6C%E!)%A^AQig]J\XtZC> ^IQ݄؍.*eDC,C?b: Z9 +|ՙ̑QMHF@@DHxA.p5WUD|3ŁZOW,*C2)PΤbXF~TA-Ȃ0 2{zV`XR$Z9lO &h+ԋC7v086 a5|Yf^YSǘ")8h/V28C)1H^r,AKe;T ɐ̂,P%*,+YD 愨*-G`C8P팄dZyEAK¬o`aZԢ,J 6EL5tQva4DrĐr#B4f. 1d5xN"t; `=4!9(p< Ɖ& ^}x&ldG8! v 4, -2DPeEuR2FyY˞>YlCPe@" 6G%#7Ckq H_}L H.gFՒ51dS1M#8>U0gHUl֨,[ݧ"HR@=T*5AI&ornjׯGP ѥ*؇20@AU*2fO*xGɩhc`u7` &bTt鉰Ba 7^@z33ByXC  e7Y1}KC2L~h 02`vK.\)% +|*,N "-982fG,-?xblh͙&/ܐHidyZIZа!@;LJDbB/C;ړ>2+:Ϋ|(# @p^ԡalӞk'tG20ev_hKKpfe1ycCnm4pN#D^-ޖo#R0N 3%ELp \A'h&rOt>rsvm$g%qR_Ux8Q1rp8盦v? 2MW#"s D)H ECo 5!@|`ekBU='jQz0ڹdT.ޛ<6%puU:V߼ +8Svx'K2^i0W@ݐ0 V8c5!JZ h-}j&b eDzl JlV.8#'hE0L}Nw,K0COv&Mִ pK r*R H#&±pϱ*r&a)4 'JA:"aAPo&A lDGҨK..6̢ƀas!a6PdB2sd:U<*@Z 1nb."5(/c J63' 4.6Z(#h*! 2?6(ؠ8. #:2U>A@ qA#N/;<aU,a(JMcԓ%#=">.v8K?F\D@r3D̄FBPc 5D|!tf!"`ؠ_rnPޣ8K6MP}&Tid(BrTJ1 $/L6A8i<e+L(ZDƌ[Ki%ٌ9QɢQfLu3` ($!@ J>d(733"wVQOÁU( *$jXD(C(&.DI88G|h>J"4EICh(IJvput*!?lDI&V]"#y^qXB P3)Q'ft2$&HFe%M2UPbu#"5zJcRHRY7Ta F@$B FQak/+4K'fãtFAo4!hf{a>PYUT~.':V8neу4<&l%12?fVZRf"kaW!I E7yt ϗ(ZU!`!JP N6[DŽ+Aʳ|xL ƠRchI5x%Rmu&Q>tnga,FVaA'Rq&ns`y6R F9N! .$V&a&+ `|pjEf2fekټHyLRP+JBk &ChvUf8*󐋤i8Y kBSXcU45b&|yJL`8b!K*"f@.,^c`na\&H.S5&IYǹKJ#0ciB֣Ah4fKnK9>}z!LJa0|R  ̠aNXAP!\ ٧&4fAи#a8oEؐ.%ui¶cBIͣi|0-y.!0g996oل/I I7 S^Z>!bb& ̻ !.#A01/! *0;` DD [>z,1x8xS),cob*l;p;< {UkamLNo-O fxE7&m"2Cr,,`)r7FE̻è DE4F.ActLAH!"Ư#qjB0q\O笀"oքu`a4vK% Tm-.vhU6 BdɗN!ҧf<|ZI[>Z^ikCM>'=hdD˜VҠXBvF|2y鞺tӍGE,J!!U- `a+p'jo, ]A;)7ܕmf,%Pŏj3d4kMNB.Ƞ lwsݥAE DBw%BA.NFc\XqF" <0…`pΞ=#cL?uz(H!Ǚ'2Hq^.][_q@ 4СD=4RDF]>l#,K˚E,Y?~-#॥I҉۶޽|i&L0up#jt`s$IYyi2ٲv!L`0;ݼ*u$V#KzV;k>ƶ]gLJ7q0#i(bw}@mPlL=d`aM#l-`6@SQ@r? I4NlކLl6\C4P8b>"4I`!4s54CMcLUAie6A7^T2#2Hldn A?HHpƔ.tP4'L aFbJ6̓FX3 89wA \s"94?M1Ƭ5]2Xnʥg%h4>5Uki.WpEX!B|ZEC)L 5,Q [ 5π#eᾫח9@SM8rP Fc,GsZdN (ȠTsD}9N;hÏyP{R٘Wɓe o#403MCSmD vR5rga͂BS<8J XsDL&]>t1kyuC tmf2LeT&XRh?,ccLc8Gڗ|[ 7˓Ơ6gJ4s ^<^h9FF?&9?,i#,Cq] ~DgWlm#e lƐI?wks3HB2.ɴ'Ȅ,&l, 2c[ #Y &~Ǐi8#6m!A3pP#/a ;w& fIP*Yv?e*2,`7c@ +B ͠(c pa$kTQVd !*9fydy }Tk-Vģ'?Y $d v]cd6{{,Y>X8!@FNzD O)z%_Q XC&GN36 PBo ?rh _GZk`25.ш4Ң.̄S;ymsf.j(@!;k4f49Һq8^w%ņհ X8*Z:P o`^Md{Ꮢ1(pc(@m`Dl]܈](7Po#k!q@L`bo;PM\Nڽ'9÷wQ5 2cÏa-:atxc2Hq0yroLātcx80XԭH+fv]B!st #"@tA&jhiF\YwO:7 _!p{qﵩ{J m 5i5X^0"IϬ'N\_'9=$26% Rf5tgJH/ `o > b Bd7d}$q*Kln(@*qc"xh97A,%m F`q_ t #@  rP 1a.DRC 0,fv,cTd `na6hoPȂ+Wj@>OP$0V'>d-F!Jhg7Oh .0 p 1PC@ʰj fF(g6%B*&&`bqP Vy!DDR#g٨tՇ<|Cr q. yP:D, +6:7tfHd"t SCHhvc >0 U u6ir12h }%{Ўa`PWx 1p80a>DH%dR?X;90#1 0 %Eni"? `h#5Y%PIp/営) I\`i) vDx#m84-> ɠZLcj1+ pBg}|Ӏ:@2@3u /jnU*  e:Z`]@$˰ $:JE41ꦒ~p y ` @ y0 v.s.d"6!8GHF$ ö E6@&"wEon{Q-0o`0 pf0fc+2g+$М1vـ:3js -Q uM+#IiѪJ+QzZ u:Z,V3NUFD@ P/DyJb"PHfR jq ]aYFX1 @7g# xʘ^ưI9 ? {, 8.h Ӡ0 0@"&\Ar k{q1A" `\/=a3Ҁ X0 Q[^֦?k_WsQ d≮+V6qbW17%ɖ~#_,0i1<^F )֬zˬi ac =hg1L'],w #+|ڀ"dR P-kl$x;g7;Pۆ PqW{l>B8O7*@nUVʛ~l !@!kKiXiVuqp 5%cQX{᡿ ~?J0wĘ :y{K7OpI7Cmtm:0/d=)7k+3U6\5b: q88u#1Hujf7>Bb r@7g>/a ]'2;vLʽږ=) @CJ sPzh11fd4ܩkj 뀞2 " E'\,xs&.j{ (Rvd@v֚EVcZ_@/ Ll{!KtӾ1|+Ÿ6ByZ*Q< ֠ILfadIȔc!\$TgCE@Π)@0gdP }*d E%-Gq  p\wP kj.r8f+Qe{ 0Szxy܄rxYÐ6ŋ70q6qVlr q)JY$6#}e) v 00rc=؊ ^mD|"M"Ŕ;"+-kͺ㬍!ɹ d asq66x>5&/s@&[$ 1}DЎRXjö&A&#!c3`ѝ` `7 h C _XH-wamʯC6c1@0lef̞dM*xr$2I6n/TH+8o01Ta<4U26 곶HNᔽUD/H*f@شqM1b-1]&@m旞hp7$m)Љ?q6 yNk5IAsXq6)v+;f dÂwWT lȦ2>- 9s* *Dg-<o%٘ Zn,Ʃ/!VylP'mUܐ ;[ӰT9 BR㊲ 0>/[}hQ ?Q-@ q$MN>3$ L( +='?E(%%1?_ݨmq /# RQ YDK`&N,op8'Y4_/+z 3/۩ZWy}ŢAr >` 0T< i` p"T4/ / QÃfl^9}?i3W}Ë?$ /K>8OJ6d6 c0_h50`֗JK#Z .W0ÁAXE5nP\@]t##/۶mWsI9u4O%3]߶8޼}*ʏ(Ay\8i>L۳t yΥ[]y_8Q5w P3Q8qe 9'|(SofؚUښ`]'4lagkwctoW]iy,7jٵow[`B$bR{ *8! S8nH.GԒP;Z;i ~<-7wȘepyJgDj&xyȐeyFsqG{#Jy\ң.YZ XDyѸhw";WKf2(Qx]&4uLM*J3n21P pͩA_ʞXˉM! %M }M76PY6u֢l,A) d 8?@+y Iۋq` ~ 5?jvxx}ЇikhӼdЇgR[x@*Ž=8ُ;Š;Oܻ ygЇ?i,@.` mpB9( ajR.cB+DJr"/씴9ʋI#B5\ä+>Qۆ?Rvj+i9 +aC[-XK)y RDe\F#0M[ ҼT4]T-U}؞eE`(C^<$p#Fkq8f B`82Fc b 9nXl\vPwhƺid Z\8X|𻊏pID[cqRAO?- Cљuk#ux $ lQQG= TK ɬ}aTD|KԴhC15 }Ї@ |2: 2yHmeA"$yC!T6( }!=yK5EB115)`lT%J4L ,X6850Xwp{FE̎;v b<g+LMd2dMsF t9Mʝ001L1C2eT#2&xІl)(a6jJ<>ԙP -<;HtGMؤO$ 01h }ġbFERAs@<8by P%I+¬M8 ͹+4kd@|C0$ "&xk1ܑF!oG8I kx'%-T O' $6O Fx`eH,(5AZJBeUJh6e0TUw&ՋtD0:(qB@Ge4$Bg]iM<۸IO ׉x4 w-YBԻxdd;x<FHHw!*ƾ\ܠܘ x>6hq-_C"W~pgX/ZSiHÆZxB0+Y͋g` CS-,s= ׿Dr**JZe׻Px 6xpO&g*JKT$kpԿ}Z, p4 Z| ɣ3MEBYh`eM`༇Э.xS Ї}ԇpЖ׭Ip@Cl؇i1s Ԝ] jrS^}aqxChmkM˽+ImЧJ(A(q' 2X2z ,RE-`>ʵ ݇*@ޢT. 0t$,J9 :liA_wae $/ra^ .E`i8tRPxFyXD1 p܆lX vX0 b-Rهa 㨉cUF4d "9HiGdЄFX&V̚q8 MGI)8 XN v`gTFU桡^ <{F4'b=6:2 txL 14}tHw Gğ}AH9PĠRmN7CNUFP'_u%4Dqbm.XpH$/3ߏu *8 G2DQ9!l}ޓOV]LR~oHNM㛌[?&љgO(y Gk.#+ffr&!b:[mmj`881 v`Oc,~(i? Rjι+ PW+?# n;%$rHꪩAG@BndkP.{ dA⎃yݖ2o1C_\$?)}W=mL.p? I 2 @8Rr8 @(y(VX!"jDc) %gX8ٛ`E bz50oȈ#%! yr@3WH+%57Ex$YÊ&o|aD.rԦԠ" a6?DPp^QP$˚&G1+KZ0f̈Pj6[ b\D_>YLc }-Ա 0Ht~SIU@:^dlR4r+!md#Fˀ h>Kex>aP}&0InMdp0Do E1G2q#(R'}X#H/)HAT"=-a).hd1! B`b(OWPE& ZlC[KFS$ ($Z,<1bfg8lX/1~|%ŅƸ)!ÃeOlYI-24an5CVvv B,< D:ԉ!h1NLKKAS ׏c>㥠W+?d@Z%(c7H:(~6J$z B`#DP njKĨCh *} cp ŸKP%&dZnfo&+bffniv& '8h^ Hg@δf3&ĄlnmXjFnoxgy^̶C<(a2? DC8K13胥z뷂ke\:H ȂB*5hµY>(h>Z :4G ;tYDMC24d6X;D@ž+<䃂ƒ>X ZFCl*4C6L ]8ح x D2H@%ȃ2\k'ĘX;<>hlrnz?'<;< נ Dm@-dC ;DA4tl7TB'$ Dy@ +@:HB7:^"D C2k3@d}C|*8A1m,XC TA4H7<C.@3WQ×~nV<D,C<8$Cx:D;4Bt)NA%,%"t:Pp *kBA1PB-\B A3}-0A0̀>' 4pCpC$&!F*mk"p_ʶ:%,颫Z*H* 8p00:@V!,]#D@7P 0?PLX@*@CC4[50C20X BHB(D(+t) C6`<58FOq3;3χn B>| j2؀$8\/:pr$00polP BxC ΰ%k7\!P $A({ l0|)s$8 C2@D ;9`CFKC4t0'@54}1p234tJtR>6`C><RJU!y C/:*DBTAFDC$KY3+DX7A72 S+GG,B>)1B)̃/X!J#vb'vKk6|ùBCH)ЁX Bu9{2T+@D@ ;PAv0VD2<l:$=;4[oCDH@@!$5dA07D1I1bwy1<{&?m6iMD4sC`CX<\,\365<,H<60|sr ijC8X6L™NኻH 5`l>9OJ4*}Ct8l<A3&h&6`AgBVVWXCZdwšSa4eA8C8C^!8KxyҢBB`%y%DybSd ь7fcLa<9[OH&B9C4zKIܣ;ߊcל`'HdE"zz^Z:^ntį:EFសz:yF4zc{oz}z ;_{+_zTeCRBY.{[Nk;_{Jv;gz{]KJh%{{ۻ;r{?;{ K Ɋ?qiAY>t;D06O ?\E *B5$C< 3hk=N>$,xM|C6T07,@6 ~T'Xx@(dź ( 8x v=,>Z U}C(e 10eu[4JL ߠ=$īxQRsuղ .7ߺysmZmYK2eDУI6}ujիYv:!9(%̪wkFڷ/ޫH 1z lRfU3ұfªؠ5 R$&/tFeakdfc 0uv&ka#kZ&xi7&gmqR>EM<KDHfUZxt3e'}`8 `_Gz *a x`L)%ALFɦ8эVwN xC=diƗx %>-Yy"\ MJ LUܔN=PE)yAGdA9GqĄh\AwAK8ƌ$I)Jx1Fo8q' XhNbVBayYɧx1floyɂUu_xhbH'҂4-5TtxttCw\\%}نVS&@qSBĄWIuHlp .N{=YS~ȇ ?Id$0ZH!hVDk¸uBt:Ua+8q,%MSD__{S8zGmqO0$XJ  @!Pا|ymqZV:e YEy0-R, \'^q'X Lq(4Gqb?tIExWlAȂ~&S#7uh İXz@)x X" Q*A P?3 /`oC`B  )(!(Ђ h8X x<^`XF)dAXV!.b;F7q @"` (Ƭ#xclY#q9a]jId0h0JE2QVD?^8c `hqIhF?X $BH)kC J[aX]8əY4B?qNyhNT #yqpBȉ5/*q?ā il 3dp\Hٶ1qcĶ}$K%ő p# ef Ul,OFn:dEFk Eg UjYZ׾li[[,խHґAcKe5DVƽVoBUq#衡ayc_Izp4,[Mקٔh 7Ԅ=m_ \ 1.c _$=n^ 2ckz)!psUx@68ڛAC9ycA\d#6U[&w!JېWOe/aL%̌(7 "mv5@\g;yg?Y`sAق!WhG?ґ)]iZf-MF"a BD跐`u~uQp)Wpgh6+>G a!0F-haY%gҙt?.9Kh5s^x)^dx#Fq^wyw _xWx?ߎߗ+;DHQ5![1kQB-h_{y{_|W|?ї_}_1wͅGH2aDpa磯AAmC EC"/o$P P=b@PRkbKv"mo"<7KGSPW[_cPgkosPw{_10-DT: "lP"T I0  P ǐ  ӰIh&a@ R/׸mDH 5 Qk^p!@ ^!%z`BQ!""-e Sp!`pcQgG F+ DAR F$"JxbcG?0!`qb!!@M##hۊMב1z h!/.m w!rc @($Ā"\ !#;#92>R$G$K$O%?!@#%0$I2!^R!k&oRT !#8!B (R@ @ (  㪒*À R++*,+,2,*˒+ 2@ -ˠ 2! R.2 " B,R000 1S1112S2#2'2+3/S33373;4?S4C4GS33N,@)2 H!bAFB P& ! ! A999S:::;9`/1b`< " =ϳ<B< !>;S>?S????@? T@ @TAAAAB'TB+4B/B1B3C7C5D;4D?TDKDOtD;3ArCP!^A1$2(A!p=ʼnx= b6"pRIIQf5 ,a!A8"TKKKLTLÔLL˔Lo!F tMITNtDR!^SH=G"Z QQU" (&!Z<5@-V`URGTK"D-PQ < !(aMWsUWq(4G1MScVwYUYUXbP޴nYZUG( !hZU\uR!A ]I\U^6 U`[a^_q`@p`VaaG `Bc3VcW b db-v!6VeWeGBU=Vdc6dCHQ[gsVg1"RjughvQijvjVjk6!,Hm8H*\0!)\#BEŋ3jȱǏ CIɓ(S\ɲ˗0d8sɳϟ@[ѣH*]ʴӧPt&իX1WCKفΪ]6^6 ۶ݻxIWlUHܸBwǐ#K|4˘3k̹ #*.8Dxװc˞۸s5[Ӫ[Hƒȏ0V\iνڭKӫ_Ͼ˟OϿ(҇L!8ofYe7 NpÆD0AD=#(0*U4h8<@)DiH&L6$1D &DXf L\v]j)dQ3\odC=-C@nG?Xݡ‧ݡ裐F*餔Vj饘f馜v駠*ꨤʝKRJIfi˫*무j뭸j+DbKa8#y`#5GIJ!r 55C+"juPz+k,?'0I M"+-WLK,gw ,k1!L1ֶ4Q8wC22ŰobL7PG-TWmXg\w`-dmMW'@ tZG--vl|߀.A',F! = 'Kb #T褗nւ.n/o'yނnAoTKzng+½s0/S.8#Ayje9sHL:'H Z̠/Ⱥ7\#*F(L W0*^a^B/l!BY@r '`ȅ_"qH*n.q.z` H2hL6pc3mhγv<b&*IBH"F:򑐌$'A0CpA[0F̌@J,bJ,g9KqKnWʒIbL2f:Ќ4IjZ҂"K,X2An%jDX @ -! aB>~ (@*ЂMh>І:t ͧ 1 V4:ftS.JϊMJWҖ4f:T Jz{ғ?iP*ԢFM*RԦ2N*T*ժRV*Vծr^ +X*ֲf*DS},vBeʼ yB fG:FMbXVjP6 i\֑fl`lqEm? : 5"bVaInw pKMr&,<Ћie910|P)DR 1Lz^ߋo_Wb 0Stg ,;'L [ΰ77{E1g6mEvNff/ -yƔ1#=q%I ϐ(!_1d&3Ǖl$c@ 8t&L2hN ^CC.{π 1"sq 5b/oU!WbHEyk {Ӡ3-RzPUgH2Y}j!["Ђ@@j "IǸDֵNIQVdImH"՘rQmIzէMrNvFw"I5[?2)@ ,%^43 xf(^\Ƥ8)M;#gǰv5GqX_(,aLL%-@Kh"Nw'x?^IX(WxOi?EgzϧN[Xz=f0zE ^ϨPFz]]2F^{\7&O!'Y >Q!N+pOxw ҲF0@ K?p~ۏQ L@~7X~ wqxqx` gz'G~6&؁ "8Gw{lgz }% q1d}wi7 9(qAB{ 8 GHJq>聢@jR_e0CB NPu 0v'{% qH`8tXvxgE{' wg~|v' 8 0 ȃ=88Xxxs؊芶epe e1%XxȘ8(UP }dpi }U ׃&` ^@gzR芳}<@kL{N@ +׏>( 0o}8臠] x ~ؑ] "}%9k*Y6yxi7 I0 q0 h( @ =09 0I8\ٕ^`6s9Q~}eIZN\dhc}gcI pw10阘9Yyٙə 0 ٚ}I  QNvxLL NPOǙYP 8wiٝ9Y Iiy$)POQZXdp>' 0 = Q i p  fPBijq Hb3`QrJ:%Џ@LpP ljLIp!PR:Tj N0ZʃI ZpH=JT >p< a 6qWrY)=IpU: j Qe 7* Q4Pʠ QI`I@ N0 T0sISj᫺* wI   bX 0pޓ Y0 z o_b 0qJ} pP b8Ш @JLLL ~ a J l4pL`@ Y*C) @ Iq1kﴰ > N >+ j 11 @ pSp(yH >Z;?[f{hjl۶nprcWrs wszs۷~ JcZ+s~0l0 tP f@l+s PL`ipۺ;[}GW˺( 3'c PLZ 釤 P ] 嚵  [{ۿys0 `0)@YLP ;bX  b b4Y@Z JP y ,@B||Ȝʼ̙ˑYc»piN\͆* evjŠ =7 +<]} =,0p }ɞ P 0 ᅴo=`( `i@ ҶP @ҳ  kLpLL LNPR=Tx=Ԅ<7êkH՟9 6\`s 0 / Ϡ  ! '` τ=U،؎0 ؎}٘Ж0 My6G @  M  67ڔ0P$ kp p [ؙ=]]̧-أ mhۡ}s0! `r\yϢ{=Nޢk=ڢr ڢ9֢iǑ۰Lw  9 ۯ Ǚ0 ^mނ &؅ M.Wr0ޔm} 2(Z0IvvI 6wLNPR>T^V~喍Z\\6bɬ=ar ٣ͽ=]֖z|~r^~NMͭL3I<  rj>` 7 v KؚfДry چ^~븞뺾r^~ٔ>@i ݔ٩*֕^>^~.>~%.ޜ ٔr= Z0 IF1M0'-` o0O "?$_&(*,.02?4_6oN^,'܋0 @.D? GPR?T_VXO]`4 \sH? @pd MW}{G ٔP`HpJB@JOD 4?_?So?_`L \>}a }@1w.@&?_K@ DPB >QD-^Ęa/Efh !JF"H-]E++U"Ciz"@~B C~iØZ4>UTU^ŚUV]~+.)ZJOC4zY\~ UQ4itHѓj5Ydʕ-_ƜYfΝ%%KK$a(=6Yb7@(KDe%mEe^9fgf_fw~#>f&hF:ie=!̏ޖ~3B$E \%MPj\OFimn\Ӧn[h|AŠDڅ\GeI [&!$%r/ygt:F^vJ OՆ6 d`#A=j t@:\cHFkД _%;0pj58"l QU1~1א6oPH:ic#]x)8ՙzqaC >pQĆkְElbX6ֱld%;YV- f^qٞHrN]S'=iI, -C ~J195Ql\84[RqkX׸5lr+ gɐow[]naa kHؘ41 i-lY1 >o~_׿p<`XC2 TuE$/OfNm TlTFXa%1`zVjC!f[ W54k\Gby~%hVK4A [d0ag[BK6e"F'E4xLn)  ăl/6 Y` `*@.G_x v:|O0!7^GMn-SVD#&P#;Z~Ɔj]Y &K%3f0la;S0>xY[x e3[ 0~8 `(Uk`ڴzFRHkkP#4Vs@H75tX%0;4ýs)Xih8i` , AekhY4T5d6t789:;C9Ddp=Cdi] XRhf`fh(RfZPIT7({I;bMh4edXWw`"4WԴ:g`3,(05W\š75A1P6Y#/ZeCbl,-) )Mq`XYN(Z0=YDX([P .f+V0%b@lld@ZP0 e` kXFkA`pbhH(;oPC=mtBJi@'BIVdMIk;d?4˳D˴T˵d˶t˷˸˹|4$˷ DЅsf0nXrh(YDwR0 }t +,b `kP^kYFiCY{ADl^m@b@kH` 0d (p|,MWAbІlJeFmDQF`(ZL?La54DTO?E!p ؘM*hkN_=6RP^h_p'H2Y`PWB@$pT _ti8S5S` XY؄U&`pՐ8=s8*XȂ2pSl@npePX'H pX*3e@|0MhȂ'D|2xeHkVP?tI\GHIEb@Ƀډ˸0f6^( SЂY pAxHX&3^"k[7wPD0X0(X0*x(wP*HtJ5ٓEٔ]XyX(Yt?`a.KOOhh $;kW0C(Xp(6JhH7W(=pORX D:-X\SpNP>f[000QUsHWP%To:sP1a(SWHUU8t tHŃW2p7eDTUU(;M^@+UE>J([xSN`]1uHE1WZ'YVs 98.@V)XC?T]@ (fЃ @N J XhN =`hb@0,xSW\d\OH(O@X0Ts WPU`|YI\ e/0fK荁荁|OϜϙ e 8 c_aI:>PC0w ]N  Z?@j:0,;|n8HS)XIeu,YxoSvNNX XQjtlu*a1 O3?@U8ȇhH6B%oX^%BYWdx@5Ipu|.~{g|g~.hg>h~h~hVh x`Hh:^h'8%A|J@MfK5DtȂ'.`00ptT?舶ξla=G@Ix< Y^Fo 90PN PL T%(D+PN(r&H_ݎpJiXfa@Kk6%\E=Uց]6U.cnp1m0_JYpkA]Lc{6TWUgu ]1Ncp ެ͊`mpm;&@l䅩Jh=Er u=0_`EpE҂n80APtpn:D eX3uQSxfk^v0- Xx38*,`_~9[A,GN8qփZXlXU(a o 6Cxe6qh'q?hhҶ^``aa` [V(ecJQH&0$0^hf e؅smN9~ ;M 9P[V8vph@vN_j3? [<@U_f8]ShGWR20aHzͤL8||N7GWgw_bp_,M3c(=e0@A=Ag=OQ08bЕ'0YbW &"P@np @%'-TJV"nqP]:pY`'blTxWœ0h.Co&)<BITU &9XL޼z7o߿~  a%0q`"3 yqet Ct"(.[:IQbjWE\Y%O'`)+h9A;~e;epx( G,fklꖤVbE1 ^S4687zH`G#CY/IЌ+@2Ĩˆ* Md9#=#A2Z ^)$ET4yiK.Nl2(-DC18-4J 8@ <2.]iBb lC D8#``J3I0@&R00ʘi3'QVlR/D)I8 x+X0 /p-jcCPI)h4&P/0dAB .;/{//%0=K/܄KKq0eb]xP  4F-Q 1 /X ^-@#LX`鵰81: 7L(@ТbM,L,+ ,S9D3 L9DZ 2Zݮʝ?@8@ 7I˖ /N3 /B-|#1΋B2,(SK- .|*\K4(-R;j3G- ,K)o>74)d&rK8Qi"0/`X-|fDC1!Ja [F3 Sbb(Ұ6!s> &_`WCX42v\zȅqXFJ1 _ː.bcBcxN 70 x1`1^4xaiMԣ2qbx2PG"E4&ȑǟU)SCɅ0va_"w0A .UԀӒ, X/xB ĸjXQᢄPJ >)P*ԡuD `(y+:wA0р۫Pyj k Z,A ¸<Qh<G^Ţ,kPՂ`C\g@}"%ca;-B{#1Rv@eT^@XDQ*ђ=-jSղ}-l]Y #Id:fr 3',vJs梦gdN]1tP@{A**@ C:y*c+ES=J )MXkmnu!}L/byZ3~ T2!o 4Z-:%^9/l 1ķHZ+aƪ /J>,21*.[ +7Ԟ2tadЭEhc+1f>3E;[bӼhVW"E,1pōޑљ:xXD.PӸgv\TSQSSe\R9Ԝzl9kX$BOc<$/4M-?׾5DhReISГef#ZBƖ:Ը YCeo/j %qPN[ZyC-KE^/53vH b("|s`!X HFm..(qw0h@sjZtZ5G5 MQ6yvgAR}CqC5񍵑<os^:.f?;c/P=D/~i/RҺCIE>a\9{!KPm0zJ\.&[7}pV*2O-􆳾飋i+t, "$Fbl"L @ `&.`ѐr<$-pɮ1 mfeBHejb*j.jb}^ U""j[1..#363>#4F4N#5V5^#6fj# .B<}\UPraN. $B=#>>#?c؁?@$>$B&B.$C6C>$DFDN$EVE^$F&$"<  Hz/L"JdJʁ$KƤL¤L֤M$MK$OKO%PPdF%R&R.%S6B#N@c왰c#Hd7Fi $ZZ)m;=}d8% .__%``n-Gʥ'Z!]aB0~]q\`2a_^!^㒴Z&ihij%jkhT!V179of)ę6=`f]-sEq6]߽u>G9wF!tZ1 $rs'xfpl9X|6|2X}6.jT|B1y%pɱ^r*2A؂:X(߄2XХ%~h1(:dZFZB&EQzxF` uJg^ߏ]玞yz]"i) s8_׵q5Othfmiui楻M $XۚhcR#b uzNjL٩)Ʃ]JcZ)j6jcN4jrF:g.禖0@hh()q]'j0hZ5*j*f'jzj"jB Z(B&i>.EY+ƪfkk^Fk3Ć D/@"˾,ɦ,͚,~Ζ&Qp"'h.08>-FN-V^-fn-v~-؆؎-ٖٞ-ڦڮ۾-ƭέ۲->E@&-+4&..6>.FNnҭ^Rnn>±ņim)6Ir.붮.Ʈb.έ.Jm#B.nh^V/&./6/t.N/N#nn V~//6Io?2f:O/֯/Fon>G:Я@Npj+Z/p<c]psiTp'00A o[0 0k0qp +pp p70Ӱn 1t pү80 3I0\8 p[1K1_1[q1 c1T1qq0'2 1#<G Or! q888D3)G8rr\"@2zp-K2.Jp{/sP@3 ,2,/s'-%r,73/۲@Hs1/o5062"q7/724g8q99?@D:S33288729W=s<>3?;s8k%q;;sDs>C5K40s2.30{p _pK(4AI#*O4.0Ocr4MMSt 5S˴(+IR35TK3KWP7uHtVoT[uW;QV1UWc5tDV\5XuZ5WWu[sZ^uX&{5[ ]Ru`S'6aRSHtҦ|$&$P5v&r&658&&g|jk36k[*[f(g&mh&gYj6&h#'%wptOv6vw6qolǶk6kwm_5&686ntvuw:whvk x˶}[{n67p|qvx6yvS7wqw~7zwhO|㷂gyz{pSx}7cxlw8sKxxkkw8s8Ǹ~;xCǷ}oG8|8'7;x~/83kxmim+8^e/o:)9(( ))ģ(*'9o((@;:(8zccwz:8$(WzO[:8:Ǻ ;(G1'7;?GS{OkWzC;K'{g+;{#c;;{{{:{|W;{3:GO~ B*'~p=>臾菾vV˯擾>~p{/1}ٻ>Q_}%XW}NO?Wכ(7ү3/?Ҏ<<$C2<} Nl@K &TaC!2(*]cGA9dI'QTeK/al98y7%KFSgy2taT]/Y Я}EUg0hU 7fԸjW_;lYgeT3ϝО]1^-%%k<d/ 3vb)x5J!GxqǑ|m%{|% \f NZ M^[r]HVd|9oŁ5R׿k~Gt⧟i(.6'F&}|A/yD!3">kaQFzFg'xGc1x8KxCu$!p8e,[TR5lMpƅx 0A7\\(~GG&^FQH3INt(NBI-Sê&9`UČzF|qvY 0%c`gOT]H4'Z:dugu:uTM\T-sER3ZMUXu\*tJFo /e"4 w#׈2j$i^ai>%=idjJG MqTtI.dRobH݃&\fcl6ș yq&lqǝ gPIm!!^h@̐c$P0bvCH3NNZ'!ϙb]O2@]i6Fk|vAiN.bqg1c12{A=,"t~GlY"uvhmIcgEU'G t>Sѱ^LgyOg0k g25羍jt'`IÜgr#"fMAARP#5MG!lhE 8bH~wta̧q#{ dqc:yt5cJ.gf~-g]@H$IRٸ?*EC<)A'nDG86.cQ>?"攧=L|1 i\Q_h@a kcܻ2@9)Iw&n6puiCv2YתP0EHcCO *`<|쪑UH-Ρ6$Dw9r墷VUg{*dW,E"$<yLg uihمn c@E/c"hDqpUi^M]Z 691&ްx>XF22 аUU i#@x^%٩Yw6U{X<c`A_;Xw^d`ҘG:pE>|Z6#/M#NH{< }Mi.,g 3lظF 0kr%`rI.;FNF"e<4AW!9fG='}5]!8| EiS9qh'zQ< B~O9 A<{F'I-k?;YdGMRUF"f{z:r."U)nIhPzEUDH8jfĐ|/- f)sBu(KqtBGqP&hP'ARlGኾ+ܡF +FAD&D SzbȐ<0Dbk&ab;AsP$DaN(a!TBAOWց aAePqFX$ d 5 vc!J$=A(^A10[#1C 4Ɛ$mheV69lş0`pWz XAGy  HpdMJr{0"HD!a)C822bܲ".EkwtplA g@ !Aa$; +4!-!2*A.B=1k ,Ar8j$5kès5()jD% iء(?Ҍ- .&&.C.B%A" 24kd$9Fس"?!gŨjGI?5$9 F tC cACA2ar 3,@J5G!Aءp DASoZ'?К@,BKZO+ˆƀt *;" 52@"St&!@_j/mq!2 $QNdՈTG1Gڠm5# I!^ bJ8>R(3MLlg%*c)pD˖a[xCysVuZ:$'|ħDQ#?#@aZZKZ![j[5:4.PР∮ Pfh 5ժTmfU'pARHr8pDf%:NOdAdi)H!v %g^J0T+624K >iv2fw?"a S?Ncv#bTvOliv6&n0fGx.O64vf/mmClht\##BID@oaApR6G(g!J)R't mbWfzDAB6u2ƆE bvi8['agvd xn@ji; y97zkz6BN:2:2@1|dT 7oZ EW^ڈpb ؍8!!6ni1',Z{C[yvL.'JŐx70R& M)c؀uT x KXRNxŢ0TE'v!Ȓ^Ag&m@p􁰢Fhyxb/RR.хx B=~N=N*XSODH'wKª`dkhE!p`\0891 "#"A@L_HҁJ2iesn!!poHf[)6e 0hc E_pbchvU'GqQ˘ؙ #"@ 3ed抜I_BXAxƌ''>9 ^ގH:dԙh3Z,t6*g KTb3Z+%īf?zz úuƙ o@fEA_&ȣ *+M'Ǥiw <Ms\6Dت[k 0Jh܁'m'F* 'ZrJJ}Fqo INL.rͧըB$T%pօУm[4AF V As1># f`pvKHϼi0 ᨙBAnH0WT6+/oex@z0f0OH;G)~M9#(H_IaD(X ', ŜA #"I"U(܍\(1<÷c8\'(hWWY)S,Mz{ƳZ6An0 -Ǒ v;X3HDhչ)NN&i3zBǢUa.`x[ BD@!fvƮ_?qs28w 0N\{>:#.eW{x&n(5ӹ|G蠹Mgԑ$ޯ߳gSbm@)i qH,`,K1܋MWSU` .M#5>\i2!j&2و#58Xr`4]QV^b]'Pvu7w7:l38`6d򸶌dx-1-ɦ"i҄d4Hff ?00.UgYTZ|2#K8+xf:hWABHgaVL%$LxMG_xl6׵\iފaJͳ1dS?AkYJs@"(gf(+SQ%w@۸ĩC_|х94;֫%o\͓V:o8QLBI{$a?'ҷO8 r+rb &O(Ȕ"t*Z^![847O8hÎNc?Ct36A'CS;Cn4ܖ_L VCu嘟: b$ ")i-'-0F'#6cM||>H>IZhσ9WO}fd=ϖ3?B߭\t4J3Q1F4:CЇ>3=$0yc#yL$H&.iK; VV#P,nutByh2VyL2$6 J6\u.IRڠ $*..IZ0e[bbD,n僪(@^ۇ YH ?1tH 44b<*3Y'& mV,!%y@ggd`8*\싢@1M3s'|0.F2! X„SOHrbrG m*QbQˣ/NTOj /kG4}pjxD@sD/Cm 5pp&YMWJt&N2J?טG^H紑q|`\B}Ǹ &CQg=ORTE`G Mn >Rt c/juᤙEPLC` #ԏEna{ vyT s:qHJ/] 2G`;ƇPW#>ŸrM@,wqXY<rcam2xw44׎zf$CcVBd({7HtM]hGܑghtfʸ#,w|;FZ|>vO2;Y!Ժl[kʸYi% &3Pc*C`"Ip)ʗ, `GhX0* 3h ;Q_q@ys؃#e>XDEi04P)Ð)Ѡcx7&.+'M4`"Q΀ gp\&<%d` ְ%X)-@BCCE(3F~j[>p5et6'  0 5c%h<g ii!3p` u y AheC}e2y^-HCذ|቉=A!0\YTˠlu  'P   ktaA*1$ݩiS5ARא}*~aaS)P_IH`MVٗq |EYyARfNA0 } LC* o0  y`v aL!0` p7,Y͵pPv #Z4wDAʉ m2ut VJtLt5m DW 0fQ}0e P1qP V I0`p F`Gjtذ 8-I+567ewc)1Y)Ua` рuڄywyF\ xOi0g$VvҠ  pV,yP 01S>X!_)Ve~dh@@>q fc/•7!U/tc~ 7ې `2|X> K pjg{bo*@zP-AҀ5zy-Tp@ y`LQNlhpwrg@w9X6 KD `3R/xxY$%r?Y)1.2K?$Y.4 T2 k惍p7$Ba Ƣ 1;"XSѐ' p op y@ a 0 UZtҀ `o[ih+\-pw(B}+aSl(<К1C-='T-p VMf>a >ư `3˷v) )>A~SDEP rSN e0 ,)8)Š A Pԧ cyALyh㭥\T BP0VF2R݇׉y!s" P>^a}DQ`@/8gՃillˑE[B>+y$eh -1´C0h K0e& q}C-s; hg&]<#d ^s}L ` J,b/"LZɫʰYB.AT=x2d#/P1"ICuR5C`|P*d?xek-%&i9>pl.ym]f|f5*OW{x6.~Tg`HT7;R=E^X ~4N 㐏y݂5,F^>!/gqQ_h7-@'\SQڰ~. A@p &0 ӮIA]]DnPNVps*g-۰p(Pea* SᛸQiЎɓ 03#/)6pp00 (0ԱdNCwurU0ԽK. X*Q>_ԲĞk>cNX Y=jQU-2=5gA`&P BQ*@E/n0jao_] IcH> 1|A8Y%Uc._vG`}8)F 8#,R∡ oe"l?77nsm Qa]3o7%,hTؽ4{PP.[ݔJ0"' Qdngc ߠ ox0-B$XA _ɣ(?LS C# RF)UdK1c3F72gˋwvh%Mq4Z# 2c+M^:m@T)IƒM[8xґ1h^ĉ/N,[wB "l1>k &;QY{tGcTqtXƎlDU#V!ч)PSB+# 5\ݑv"g<7>晈[Ki1 *<װNTjmEuu1/vY>aȱq"u"u0Ϯہc1%*I0i72coaFIQ@\ezÆǙgv]fg V^'q\F u.8yBHV9qm1UV6 X4NYul!9]}BYeo? :3o  6yKw yr#P,Wsu(r }zG}G^gBh>j@"ÙﳞTw$b/S V믐e\fJ^d0~dZ]@](D!qm cy cV{}l?$Tσɞvs06jF6q0GcKEvcLo+ G?|0^`.b2g`stT(:xF](4ц!H?xRHPeF9eiNcqec݄cI<8EX6@"?ч ]13+zkғG 960C˄fi;P`$㢌#!ɏcJfBE^6VC!8TĤQ4vt"k汓f (*A4SEeX$r( (?F ,HgkҚB RG`&E=@v!ĈXDɄ4 8)V14jD6CD @d1mC@TFIIK@ raIhfqk`0iuVmQ "z > Ŏu|i^QzŝHf=4}|ǜp# Q1T$a(ޖC؊X):-S|4{'@qaEKmfځbe'Y/&0ZDp+Z8uЇ5o\ޱL;~F Gri)~e ?Ie\}+!R% E db; iqqr0$" Ew,6<]p c(͕\hFRQ9k._Ex rGB\qP#wΡp #]3Ce^GOiO!s2BiOiY4hjRW`NJ:RCj1 Y0U "X~sI(:Ƃ^ZHfj N ejҼW_d,?z9VD؇<,kc'=y7FmcQ4* Ѕ 0%achKt2i|mm?@o_̬tmR> EִXO txcr(E,^X(F1Y ŁrKN",)G2<|;lTm|WLx"%+wSpL1*tH7Cj84֮"XuԣRREvJ_.q+¬EG";)-r;O{q ,p,@&ج:.#R Pv|E^^{LOx ?Ql9m``x_l{?0 naqv8 Ϋ>ZY h8?"?ȇ( x&HҨ.a@3Wذ31ȃ! 3u)@b3x氻)e|P@7)7iFرAȄ˿̋+Bip %NixH % ${ρl0!#}xb9lh0( ;!((Z"(e(0x \ pSpX8 Q#I0M3sm3Qu(%}!*"&+ѸKR }~q -.R4 9ɭR;t:>(770y5gpq'((X@Ҩ.`FW`O`@I7:QyH l <وh `1H#*jB'1 rqҘQlD X Xv٬dXtR@5("0$ yLI@:"d 3QȆkXCE1RT Q|ƒMX:qؗ C#۬Z+e[AIkp2lekVMY7lXXwu[?MP!YCЕHBZHx*!-IK+PNx`\۾$<̵X6`k9#"))xe 5ƴI.j~t`\:$ynGBtVc0ZFF %W ZVsmOjl[RN| c g#y0-mGmS4 )U2x fĆ'ϣA(Bͦ4DeR Ao+]x@ј`9ܤ~gi"0}6Z-\: Q G-\f(pDZh V/uΰNMlv鞸0&o(1ǩ[Z m}6aPvc CQvhs~D*Ed>" k(eWe F&6).+tŽȩ͸0xy(h {&m^hk9Dо^><@3("@:YAhXW䇱X "oLMKp؅IpgO-{2k]6H/1#]!:mHm7(R9uf]_Il{g8(dFpp k_HhHpxd|k1Vios툐 "C O׮" $(M'm"^k| Pu`$藜p <hi4pv@g~ x9Ɏt wk8&.>iHȆ[qeidSrIn!,}@_N_"N~rP PzuwZב[Q`wh|1xIm00\RT& 9EzrErxHwu'@vwIo$K'd\ؼSkxYHA# tX`x0}8.U.pkqHZ jGA!xNJeY qsbY 8֣yHw !(h߉50Sp3rvBw_;wҤGqyЛxxȺI{ɱP0Ï +῭vHdbhsxJ ٸ"Lp!ÆBpܸ~kN[mnAOLr%˖._¤xgQ8wi *TaAplXk0Hq(U9-8NWy<7Ϛ5vc`&MZ"3Y2]ΜUwPdɛ'\#>5*@O<:(@D )E9@ iAT;X's<ěJPz%Yo.5xcf;5Pq#O|yĩ8sm2G ˤʯ~6)];w9%nºu#>` 6X cA&YB@C!n VpQ+`KDU4R6~1xWy%4O;`ShxGcG"yd?p%h3/S8LS dMA|Um~rDLO84ҴdsW_7/-#6xahg'JJP C|Bb"D9]@>L+"48pS`sAٲ]EMF68,vPX0-{{7+5D;c?$śam٣ĕ35<3܋1!CDaE)|`H)n|CԲDK&N40 Ȓ6WP:x3{9\1g9Ӷڕ$/YNj|-Kԏ}HHIR$;"QjSB##u ذ>%Vc B*!!= &(@aT-a3pB`HC*9 ڞkE$BT-Q"9Wяcvz5dLcxShA'u"6l4#1AA WBQF,zFcC\`b/ܑ,LO;g3$!iPPyD@*+ d6ZԳfؒ6֖'JB򷡵?j&pP"B0Mex/by">XL!C^rX6܁(e?,5Q*6ƖPKTZPb<i(eG׭($)6t4ţS*Y4qQGLC*t F0BY)cE5!vA x02'$5p1\ טXcbMی@! d<ˈYK!+zD0:nL;_GO9RֺF|f#TB(XYق*E`7 x(Cbn~'`gdTtKNr ͫ2L0x:N~wv 3桍6]M*_y ԦzvP @xX H-hG)ԇ`$YC2&-V"QWQLk&A5!uzSq= 2g6S呔JqN+?L%ً`Pp)ATEUZ|DLŠĒ7!,\@Bp"Lt< (@B[A4?0D:A PMDA4E<`CXc 6_X: ?RPۍ=_ PH)]FBB/(d{!T&>)UH&.h$D~$a{d8$@G:X]̃8dQ0H(n@10M* 2yUuP3o"ęjJiVC<|,R;X 2G)&I6WJV&h RtqBD>TB-,A! I %C\SAXÉ&$-IR;LS mӐ ~6D}AWR%kC#)=TY>T "D $,D4ȬB)څ?14ImlL(JnkI5llg- S1)BCt;"ŻY<+&DWZ(D-t,0*DDC3B,+-`>VNē(B<`LB]Cx||R -n>0qSC`Ɇ >n"!\nBA6D:;(C\ g ? S-(/1WUjfĒD*-fIJs LY_ <{}>r2CdR&Yr@,+^pI:1` ];00WD]tKs:GtQq F:' HsE&4l8̈:Št qLADE6V9:l]c2lC`D8|B4KxBW)vAdb$<&DXs]@L;6 tJF@CcE_<ѵC-VFiGERp"]5rՐDR5D /,nr6D58 7$6X#dl`7jC1&*t[6Dl˵PK)*( \iy5|A}0@;($K,IӄE1SRGa|0~ឱY&I}EC*ñұB5=>:@$T1)OXkRnC?0Dc43D}EF8PCC|āF^h4db p!SxW$ݗ Q҄RI<;= 3xV5(C[? }rC n߰7ƫӴsh}'&Hx+÷A FЎmx#Ԩ12ms*/~u!ڿG幼;~{v횹Ew/[0pfxgڑo48Z-~K چh"qVp,tD E\ lXf>k^k}aH&jǚ1feAyĈȈ殽:Fڱ.C&3lGX~)ŗ6"0<~\s,]p'w !W) q8yEx2%ݼu G6Μ51d ]^bDqM"F-C- ;y,A̟eMo1,("_`68 وb$,ˀQa Q*%6X<ڽG\1VAUc~(,2y3H]pJ\c>2x3iXY͉i0# fQje55 2H͊M.*x.u<'"~$r[ay :Qo i Æ/(pؽ 5ĽYɊKР8]e,lsIjHmi{ѝP;ľł(wQIq< o&ʰiTjSpN\}H 7áv!)u'1 zGmӗc w|$sʑpm}CIP0.!:=~cQr&m`X'F5IW<Jg",kbp1Sx ្ǘ~LV;_;*1aȽЮ@I8V".`1\Ibw C^|Wmh r)TMu(&X3 EQT.{QU).}{뭔8BqJn%D:3ĸgdJH{ F$-$3!+'DM ءS L1\ ?L`eᤂ- ;Jl˰Eʎ/g$dAnI+ab$g2u(L$1$ 1QH'F& 2!*uNaj1z1B~bb~ia$ K2.KlQ!eaz Eı+J$`#X,$גE"GϿ@,(Dd&#V#9A$O$(V"e!#ڌDYȠ7RK$oE|gn$BOBibJ0^ARKę1(RHFQNl HhLzNj9vT\1nʹ\Ւ+@Zi֘,zc}|`aa-hEf.OSQ ~PXhH:@Dԁ!ġWB4`E ine5!dӎBaHa1PS; !"ʔRleZ1u/#ԡ>&l.06q(|!&lc΢`HB(n0A(R)I.36DbjLLƤA*ҿtA{;;nD0TV413'2 4jJM% ,@6G&lc2 7ba|uG?TwR1tBL|f& DMC#D7Tԇ f?zLblcLhatMҔM35F(Oh~JVOʜja 2S )| !7$.d5<5!cO5UY5c((IZPRV'a>TΩ!?UCZta0 ]vU^6A$FJheYu;/K<)AEbRi~9`nQš#U>.‰NL8=6#sa~nUg:eMvDPN`U"CC& a#l-`*"3mn_LKqfSc!>zvHAVJ^fgT؇S!džH_I[Xa#6|!IV&}SwCޜBAYGآ7Š12)GԴl%:0Ba@#XLxJ料dٜ}+0b$C#8P̔M2<fah UǺ^4n$=bvY}"^z}z"jBaa~+Š"K#D:&CA^gJ*>Hlc$ntz"5OQ3XmK&وF nY,uZ^4y,{ikJoQ6%K>9vj#[Ī'h .7`3VBlOYOm-z}k5L@t,D)&{*aұO"r\ s$'f$dR |IԚq#2['A@@r4H*]6Сs +ih=R_W#:k KZ]{P"w .f>-܆oN"l qu,> IuBR{Z6"XsoN77\!), 1t,mn%Pgq1?b4<9\[cĆ2黎.X迨!Ϯg@fPA` ̍hf-6lCI">'$P{;cC-V.h&|@hF!>W RD(Cc$)?d4ϫz/)b%L,ˀb7`RccDؑuI$bi nG4$dVu xvԯ!FQʰKܾALƅe2\á%bݽ&_a<#)C vtU}yaܔK9\xOG ׌/}ZOXm<%'}""@8 {rbrVR^Gޜaa"y% ˜(~c+Rr  -.,'09F7ulm~>1_"2ʝI\'Kۀ~A5Ct E\O^S Ǿrգ R]: %Pl}.PL2bb!@\cp?XU)Ҏ B:daR "2!,kiC ƍG0@(Ç#Jȏ #5>t)=v$M IYɳUhc4? rY"ЕgETC|V&(TSx.UE^&(:EŁ5P̤<xY}LhjRg:ZQk8T< [kU 5$Lr#pZT?&bC1lV8{$l2eЛ''(l9cG_e`CFp 8\ 2T=3NE[j5#q`X^kBG_6gclemJ%NC#V:"?Ζm1p32~Muw$Qr[֢54+uMT?/%J=W  5NCX9Aw6h\8GgWo3Ivgi˪a6co[ _8lyiS#Ua5ҽBPHǓ >j3`J2bǼxXC>Pa*49H_:L?b lW %Ȯk2* _BA F2ѱc1a мvlLЇv.-e <]PѸ 5Cȷ/UQp!0oYo1=GBR]c!҄:@Vk;dVW>&rBĆcad ] ,nəm)t>)* D0T?4o=HGz"c5sG!ic ^@ UKzJҢu)`Q j1P%x0]CazpU` Ii##@Črc r/m%"' ٻa5=FxQZOڤMZڲ$;ڳ&sۺ`۽ }2\0RSyܸ]@-Խ*W?tK7]%CF S "@P QP ;]}&ZFд1- .DQ+D+1p[Q0T>A-pൠ p$, 4q4P ưu.~1݄a >)06D^F~"}wLy`}R>I0`Q`&[0ily]vw!,Im7H*\!X F|EVË3jȱǏ CIɓ(S\ɲ˗0c|+̛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ L8.^+^̸1Î#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkν}O>{ӫ_Ͼ˟OϿ(h`|s ~EJ2M VhW@xb!AN$x$.4b:&(NԢ@\3<*DT#A$}P~dITVy#Vf\v`)dihlp)tix|矀*蠄j衈&ʓE6hXP nBL ASqȌZRkuL@1 #R@@ c@xSz'I*BE%@ܘ FA$.=1'AI@QH52t)l@ DnP2Č ,! 1468, E3T*R0@;09,U- V R I-eA B@X)8"ҭ Jp GvÑ R0+ B(P)1pQɴM-Ir111Z&dՐX,i¾&1Z0nJT9SLK9< TJFŐ0#FO'LdUYv5fYb'liGղ$jc! dEֱ!-BBkԶVpq2ש}Jk[Rͽniw+^Εgf,|\趷IovXwkwë o^b$6aƑjU\`]X(N1R{bx,~ϸ6~1s$-q,bAQ|&9LnrR,'7U2UiA0`1)9Ӭ1a~3,9wns՜FMB7B#s3JE3'y4KJ[Ҙδ7N{ӠGMRԨNWVհgMZָεw^MbNf;ЎMj[ζn{;4q{ >vMzηфm8;L-[ϸ7{ GN<>9SrW0gN8Ϲw>zȀ#8wsF"khC ӅNp$G8`6䟲sn.-#3=@{ߘ 9?Q+^9|E#K87+D ɼЗ~OWֻgOϽw/lȆ<~'u z8qj7?进ȇSұm{{ڶOyOŝ_L8((W\ ؀mrH% X ~ X!؁K"H$r%#'P,$/(#.8ER68$17(c7ѐiϰё"9$Y&y(*,oYd`@- v3I1蓜wBYFyHy;豔LP9{(TzqZ\)(ѕf8ɖpy quw{1PW~V9}!8)0bxc| zPY @@h PGÀpL!S_1Y]p)М9YyؙڹiƹOҝ)yٞ9~YEqٟZzj(  @ *@9(s(}":$Z&z(*,ڢ.02:4Z6z8:<ڣ>@B:DZFz7HLڤNPR:TZVzXZ\ڥ^`L: y :br:tZvzxz|ڧ~:ZzFڨ$j7j\JvX9ً*Aթ5ʩ\:p\ 0e @y 00T p !,Kl7H*\ 6Hŋ3jȱǏ CIɓ(S\ɲKbʔ͛8sɳϟ@ JH*ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ5L˘YN̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËO_>zz˟OϿ(h& 6`\<(h@פ#NBPU/ DM&T ,ؔ.#3 8D A1@c9)J"~28L A\LTV(-L9P:V)fF5 y9ce©MroB(B~x螈 壔閕f馜vpBL@ꬴj뭸:?&&lІlԌf_6ߊ(㭷%EƞMH9y,I"#'I A !2Z\16THE .ES5` 3J7ʙ@8k*Y@\d9`P4˽<2"pq d@1 G"+8L2D 0Ґ,11B@4;Q3%NPJ1x"xu24c@ g~n^P1+ +B9A3dSA/rFM;B~lR1B_sƻ6O B'NTmyR4In"4 Jl DP4y,x88N ]@ I$_:@ b#iJqm `p"!1d$R Dn 9pZ㙤;&B ?ChP:,|C$Q.@f?D(`0 ``:lO!wLH?gG A$w5@!2$8!K7"!$0Z2HN$d&v7PfXF-H7 xc1$p!FBX;.BP E~rD` ݨEb`n?7dfw,ܧ$4 ~\:(w3&g'؅$ jf"hɁA$YC \͈/1|/a%NVp8AxW'8  LX7w(d@ -*= 3Q">DCKBH-l+BA bn#CAh x0@J9 QG ULM1Y0/~3EzA6J2!,R!-AZ{E1"/kt"Qx>íd bh#\Be$1].dX|OE4.LBR- %¢Ir@Dg @8!E0~\$dLH{Zj3 x N a/r)\2F2R 8RzAoĹ ,/f_؜T}y6418Z<גX0lװ[:dbpY!_/" ;)?3mA[ `u )B~\Y[:3Z(۝,v!=r;Y9{ޢ@F?Z])!rRGjȢ^XU4.&u%酑K^c ‹&,Olf/1)#QT  DʞȋDf`{!7P8S9aF$S."87OO)RA򑧢W09cNϼ8Ws\hRH?W<NNi3uf `+"}^?N=)>QtQ<]6I~g}z;/x;wE7Y~%H}g{>~'7j ?Q?+2_xgxWgC x8ǀ~xc Qȁ" X,() 'H"H-h @B%1FExFJ(L؄HHPR8Q8Vx"?x`aȅDXYSPxql؆npr8tXvxxz|؇~8Xx؈8Xx>a\በ8Xx؊/8Xx؋H!GHȘ8ǘИh6Wxؘڸ؍8X8DБ"߲aI%`o&Ha ؏lX)(ю ؐ9YyɊ ّb K&*,ْ.(/)1IXx3>@B9DYFyHhKJ!9% ۀ.|H X L9Uֱ00F`Qm`-teՁjɖ6d0& m)"  o)g" i9 +pƐ>9 q*a:ٙ Bqq)*a M)Dyi 7Ii։ƉiYI቞)pɝ)fI aEyS99}(p #7 ڠxF08XI4 V t  j$:YQqq4084. G @ʆ+DE:తY13' ]Ș}Y)`Ⱒѡu) "7n:Y8z*۰ Ц츥8ڨ:M ozp0TZ |:sQqʪAb$JJV J꫸ҞJ-yb,ͪ,ʒJ,z򬠪$aA޺*$ڠ@dAeI"eb,*?<`:.,eS "ʯa:K( +۰2P 'K-0{"˰ k&벜*Jb;:)<۲?[)6;%CK)A{'M³8۴N$7R;BR?Z'+KCa`1?P)g'-zs)#k wK( }k%{{;(y+K(0Pt׸Q `᪒;9{K k$'Z';U"DŽ tѻ0 {k 7@ ڭk [+ٽv{[ŋ) fZ м K L{ y<$pzb2 ABu" $\Dz0_SBP" & P` a&2B@?J $QS\ ?ixwl)0ja ڡX}|JL~m+Zk|L:\ǫ~V> ?ZC09 ;C@ fAQk q\Sq͟1 A"(|!WD  D`HF<|!*-`,fzo =ɼ_ίB]a]LC ,@pҟ ` 9T1ѵ'-+/ 0 /MU1dҰҵ28tw` 3MQqM|ҥp_ XT-+ ,M,*])-\&&, *sum%m zaSm-'ѣQ=،>;y5ӑ1aP16̗ -N*֞ i):|եb p)]| , 亅A_r=1m,,"=MMa,`򰚍 a цM)qօM mٍ֡u M *l۶1MLڦ{ooMA]ʨ>'-S->m;%7d,ˁٙ2N~cIAH>2N>)>!0i 6@b]GA[NҤ({ r F.A&nPM}WG:n3^.ps0^^o0I鐱P@0NjUX_C i,_:H.km R~_]/ -&nُ_;Ϗ{A_NLo1@` H)p!QD-^ĘQF=~RH%MDRJ-]SHD"HAb1lDTRM>UTU|t(0?rB4XȞEVZmݾm}paÇp̶HXbƍ_+» %cΝ. hҥMh!tZ씢iƝ[FI b^ 8ܝ%V]tk ;lСEV^xѪ&rOMBw_|{v|[￘yD0A]xj//̼[B4B 7"' pDOD1ESo=ˈ븾TFoTb 5G!$Hk.dI'>F㐃2K-䲩' K3D3M>ŃSS~N;bcO;˛OAeIGzL~܌C4RBԳ=4SM7]P6J8%TS2'3FO5VY;cаޜ5W]wu,OnU>XcлqQ2_E6ZiU G<==~OoE7݈R (e4Hu7Zc5zWZ_ ag`9UV/{V^ =8b].l8cwYKؘue&3LjSgs[3l6c|:j߆[7v+Q;|Zom!jp+U%q'kl'./*r7)Uy0Ey+~h>}Rl[{> ǛYا_{,?|cH @/R1Ђ*_FwAn1j!``L‚9"9NY8Cue0#a`abb@NJblE*&Ub!1_3ݘg Ѹ\p>`cٲE9Tbc%F=qPnc DGA2Nx 0C6RE|H9"0H.)HQCHp<NC4e*oIUREteD1X.D,q Tr P/JaŜ,}yLd f3KiV6"FD; Ox"xE@|ւ@0/!Jzk? &IAD"qW+ DIqT e<2`_4T28t5™ UC+(U a:X"{!B@r@,yг#<QT h "H !ꈈԣFUjVխeQwoPEb!/D!' *@PZ'C0Ӧꔫ@ mX؞*-|d.<Uv4@#UhC ـU%iX$ !,Ll1H*\РDŋ3jȱǏ CIɓ(S\ɲ˗0c~E&X%ϟ@ JѣH*]V/]JJիXjʵg`{ٳhӪ]˶۬}Kݻx˷߿ LÈ+^̸ǐ#KLk3k̹ϠCMӨS^ͺװc˞Mks/eĻ Nȓ+_μУKNسkͽËOӗČ˟OϿ(h& F(VhfvSEeӈ$h(,0(4h8GQ|YX,al ?'6J ,-ZeІ:4B(w>%OYXX04 HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMjwܣԦ:USji-x 5U7T 2v28Af8 P g*@PAU<ˠ2QPT:hAH\cq@خ'(0Xb L\76b5ŀd+"@l$XEO9"-`=7bT p b-$(jgbXUQ̼|FK_kEH [FFXԺH R)ŵD]h0Ar#,,hX! 2B-0bԫ!/&I^ 5q/b5,x IQ́|c@&`E$!F J 5E8H7N 2Z21P"x`u!Cb) G=Be@1"*[  -B^卾W؋{b0ſp`G h-&/ -@J\ t"XG\kqA|BoA  C "#-Q B?9! ?r0g  ðBr ={1{ ̴ e ' +4 y ` ` - ` C @>` `'qU|qrGvRQ KeP8wm# r( :IÐ g zta U 7@ p- `5` v} m8}PUw$t# 'pvH U~Á * (9Ax苸 v8HR gP02 @D!xX: :x a`~" Q wH ƐJp ȋᇳч؎G55/s ҈h0_ P4ؐ@:18kQ (g =~{=@@m ia CxЃxؕ;9vg،i)bᇼ@8I rqhɇX  J( .{"qzs@ |ڧ~ڧa>Pz)| :+i z:򈓌oZ ᨟zH|Hڪ zRU0@ n1*ъ%-*آ--Êz-j5ګ'9I9 $Jt4jKO܊yݺ LIjz_1䪭"Īʯ" +JʇJjZ*ʊ+B0J/ k#[ qk/P*,"$)"3 5FK)>+F,yF{` 17N `H vA)(Q"DKGk!_!FKaEjkXj\t("Mvx˫ ~ |K~{0Ҹ&󸊺P:[ !d=Kz(e;ۺyx[w{N˧+ѻ īK{ [;k ˼gZʁk;pK kk\L  ; ̿I/Y&B5 Ezdװ@ 01՚ T2cUÒ +_DF԰9f}S ,7-J8kSWgj}x@ puMsmH]/MHmsךTP2{ `1[ِ3M> ԄmCG ڇ}أ yڧ ԋmԎډ;=ٰ]ڗ=ۗ]۞H-ע qփ]حϝ܀yܙ աڭt <6L600cOD# \7-'80}=̠ߠ0lߢDM>Lj"^;.Ar)> m,N+1\W-mn7sDFNHBn ~P>S .[9i~k~6fhn[7Z^- Q9D6\'l$M*#AwP^);^nA<;%ҿLꨞꪾ>^~븞뺾U~Ȟ.#q>q@ 2f^ !/j^5Ǟ a[(.a/B i-ɠ1 `"/N~W8o -#0#Fo0=/0򦞡Go_S7ג4dppĒ5)Oehd5/ovd 01xtq 4? %F @ϐ6X `EUAW#E5Ӏ$DD-^ĘQF5Q!~ՉDRJ-]SL-%ęSN 9 QOEETRM>, W2V]~VX-[<)8ʾW\uƭ5X`… FXbsDZ9]q@5ƬR"yf8U`^@Fc&WZ6:<[Yݽ}N)gE \!v]:pG~yz۽?;6?dy«wRej1hc2߄S!5ӼI7N?$HMLC{\]v!Q.u1RK5j:/-L"C3\8%*ҴIQU_5cM0 eW_iBcXcwryf}45&lsTQ6[RuV>6?X-qlm\w\2ߍm'z-ywa&xE -M5at8F6Z0S3(EQ;&d9X]F>e_>h)fo69kqgcgF:l]IgcR늚O&쇮^(dl߆ Cmƻln8?Z{AGu$,!vN"`h 8L3A 083T?4?![c#|h=I, #qKAǀ`C*Z釾&I&5 m(KDyV!3oBlB'lf7.$ /PD2 *fnTbIgaQȂa{z1Y_@QqwWu͊B ِ R 㢇2Hs.F]AKɡt2T#pA9`^z W4UNBqHO qL̸RDJFrg@eB%5uL] L@8Bis,8@T*VP `0!va!JbP4Y@ B^!y,a1Wʵjjld#^uW2YO+,gY 5CÜƪ mt^jJ! JT"MIp5Y 68.J)ӹo*6:PWW@BU&"8|EI-BA n/E%S9QG~CJ»I w^F,WB0X-/Tư;Fe\MfÒTd p|c<@ 4 e9Uvg2q rpg|^x!8, DW:mTVt~hs,̌ #2Fr6C H` U9G1s%쮗r;L`2S5Ca\+gq+L5D08!E kA& xr1ugdXxB Q쇨Z7ץiC0P`~;iT#=lvȔD@xR6fsE(\!bZ;bfȽ)&$X o1ݲwo04 "57n\Zsz h@*:S=+OExWi.0J| Ih9N~C /Ι.F GQ].lM#PY @;7L69 @AThOd gt=.+2ä/ {A-/x PE$y;d|FZ=q D-܂jՖE&|jL"^G8Ͼ)d:R΂$?,6S,8+nx s!>& 'Qccti?c躟 7R7lY?ɿX eɓ{zd% v9Z+) Yx*, %{G9%1:AtCh9$ *䠆cm'!ӈ" PaÌIpP.9K[D! $?90-dRpZ:!i`:Y0 ?5tœ ŀQd  Ix烋 VQx@y)۸kAթ[ƂPpj$(EH`F~$$ BG 4ƒ~Ӑ<ƺ葃pI?" 4]<P(; 5Ayt<)K 3D u?i8sR0묛Hdž`tQvt0¹pHd+P"䘹$$*K7|K h$ YJ&6pJ@G0( 8E LH9p#yp̷d"Dц҆ ̛K|; 0BPjC }@0iuxq,*pxb*{ɖ|,$ A ̞p jxOe60l *O 3D~deSȷpM͞P+ Rt]&MD q 992 $Q/p\>8}#'`rc?HV%T e(8˥_5 CY€C2:6^T 3ͶCy09;bcLh#UU ^cS=KvO!|ڣHfچ~]SNeX JcT&knLh'ނ_%s^024 y38([$I.ƚ։vuFD/Rh  F#Fዾf0qԟj.~YՐ^b&Ie\T,!aa!֨^j(jͺI&Q%HOii" jfq֦Q[예lB]h*XkZQD`Qcư.ԛHL{߀LvNXhfW-IWIUhm!.ȣՎ}qDފ<ՁH>.A 3 @!(~.eR=n @n@@Vlъhg dj~WH] Xi~`@Cf qpȆ빖R<ŒW@oY`iR !@'׋;C $ufb? 'Wh k=(XAX,\pّl4~IslVsqm|_8>-w>YžMH.Ht#K7ALjQxV= W 8P`uʁX<]H |CD'[ 'UM1`Gʈ4Z `\XHS񷸂.݈(x_/mnH(k C_Jxvknbo~=!r%Ɇy 3Zkx+UxCuВGUהG 튯7:юVRxv`xdߐt1*kGm{Oy.q/Q *rx05{ ~kpP4h\{g٪-ZyZ>/׈tߒU smЎ.(C#`g)Al@,3v ^{#Wksi?ZE# czlHT ڤc4uk9Ab{:zjXO/JҢ苳EU)$L0楐_Q$ND>s燾f<2Wݣ$KdGe?DA-)6^U|KXCB|JV?4׼pv6] s 6U)6c8xx6ވ25ȨgЋ1R |48p1'jӫtA5?Tg8BZ&3|@6!WHP7BmGc[v4n7(7A[D!<]s>`4=>Ki>[?y <Q&O$;W6Ǥa-\-V24 F) X3ph;! 2/rCj@Hk.!X){A }8Z'5JaheCw(Ɖ"uc$Ȓ ivI)hn<\ώ&!1]Omq䱏#ݱɣDҕDtـaCj%d}+s$(ikyV,$'ruďj`QeD eDHIK..>G•!r$H͆ "aRm^=0D!G,F+Fr&KήsL\?7DM ,*BFB.2T6;4#LW.Z691&u-MAԚ@y9g̫a=TLO@d,S A6d4l{_~Jݶe >,aJ}uݵjG63PY^ːTAiL6I*dԲgkTS M@{P`GZi,ƴ 8Ԫ:~M- #M QL^et8? ؤ:9ғ>݌Cui2;LL :C5NPKQ;\>PCnX o6M-(B_+Iq``~jxB%f ߔftAϓ2}bZ~8ٕǾϜo{=#2{Q(pj%/pnT~yP68|Qi5/m/~ަK?u6ӿK-ra(YG-h,6 ,N %`R f $vVOD`|] `l ب`)(  fY !!Ĉ. v B!`1Ey M_z5x!~ &2 $̅Z4ΏKkDlJ!QbMNNP4NAp9A4̷D0eS:DDE㺕u"qWv%AbǧY&D DW#  95B,4\K$^BLg0@BQeD(Cx@DCHBG34D ]0B0e@n @HB?5^:0ƧA@A߈>B(Bv.D[pBAЀ5$ApA|* C<@p54 jB݌rHf D@ARD;HACLCL pڀAjCB@0<_Ğö4`[4IЪKOv`.̀|s$*'A BC-–ec ѧ:0fzGO"nJ@nyY4tBBo.BB4a\ZHUv2άdnA+_뢯YF6(N60l<5HD.$ % D+ȡbjAA1tw<@؀,B$Bj) @ qL/)4|LL^LX8HC -B,Hvo 2CWHclp Y@VJ,1t+[Dk[6$~ D) # tQ㝭82prdr)(/*I" )n /-.H,[[-+'3'm00Ep3]4H Oz+n-0%5wTtnK;,.39 _/ݦ T q%Y0LA;@JB-_D/, 8sF7M2rH.Bǀ64.x84&,tߒ/CJG4߂ t Q\@u6IThApATuSch5U V/n Rx"Z A Z-"u]ȉ4B#N^/"k!,Lp/H*\ȰaA\#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJ5Xj5+`ÊKٳhӪ]˶۷pʝKݻx˗oտ LÈ'LǸǐ#;VL˘3kNص͠CM4Kp ߚ^ͺץɞM6۸s= ȓ+7)LXУKNسk.P߽O|ӫ_Zj#cɥKH& 6FVHfv"H"?, 0"A3h8j4Jc/"Di$AB䒻YQ*"@0*X ) (d)&Vi&l"@9DKU4J* )R*|(̠+i(z&fUD˛2q.J)y(D%`*jkg!e+ ILBʝ E̚8ӦD_!q禵0 )ΚJ*"%.% 2,h"3′ mJoG(^# 80&'I-FfShJj|Q_&Pb DAMr@QT%!,Mjf#%5,"1vTLTA 9,"Y0qhS3^PP2P&M(L; ? €*AAG%}$q"'sB;nQ-MĖ9E_N".P? aX#v @oD@g 2P;zuQ0ъ@bK+Bq:pc'yzeD 7ީ ꝶЎI!0 qc)\[2u89(i!W )@0F9\UPaLL*=PڤNRVMJS3$@`bWASJP;jd3M>H h xzО !ӧЧyTy=(F p !꨹k9!z" 㑫&K @볊jN;=KZ9*?*Y [_a>k$jK$G p۶(pr;N ekC{q K{{ ۵ۧ娸Iu[$w[Cr_k<p{ a;뺰[;[ȫ;λt Ի[`V{ջ˽[D{蛾Bc߫KZX[Z Kѿ !\\||, ̿ < L|"l"^l=P*`,=:|HL\N|9;\#@|-TV|X,AkjLn|% uziLbl=9 /0P,7\-b/ .,ύ*-0>2F&.+.f])=S-1Y^[]^_n.DXNetR>y^~3^NT^G'=Ov4n㒮%^UNJ.֔@.j>~nAH.-B쿋l)̻!1L<~5a!N~Aw^%HU8UK.;~Fv$| t5?_C /y҃ _?!@(o#"O%.0i/P0181@BLoĠ }0cMqKX^b_Afjlnpr?t_v|z|f0q ʉ:o_7c`,[y]_2AS `&_ǥJO AOV _/@-@)o?g3_率_p# o/? 1OX DPB >QD-^ĘQF=7H%MDRJkA-męSN=TPEETRMG 7o޶g:ΪS]~VXeBJ*VZ噅WO]sśW^}X`… FXb1YdʕYsʺ=l5%2ljE[â%DmZ߼K6q‹gXav_Ź'; <͟G2\zllď1ߒ!f 7*BAƃßskAzk*D9)b>FMM˘ ,ZB"daAx8#=Lg  ml.D;xocI]? 3ip r'JȺYtA5١t# u%_%" mjkV5.5|DAɒ`v)Mnu{I-j Ń%$k `bDLxJK+ġbYcW0aq D(u ![HC"B~y 'd7c0Xvs2v W"EP(C9g.@Mҫ# yFU"!Iƿ';KduaJ=8/1\7_G QhwGfQF-[vm4fnxJoȸ<AB n."Ѭ!h Ƒzd=܈ <ֿ!5@)R !z{cR6mCz )2Ё$"H7cNuنqSHR{!2H&Rs+ c@`a܆h8ch*(0dd e c x q~‹ƪJ[3Hqq`Ah! #]載}&?mjcÓ A($9ؼWXvi( dCk1c`d=C ˌmx'; D1kWdPS'cvtǿ,<~Ǣ蓃$ax~ћa}ʨ\(,02KXN#34kx!J8 P8Ll><;ozlHغ(Lh ̲ILK&Mڰx`ǐDv O $˄@ ;|Mi\ +PNk#%GʏQ8kÛÈxX_ 1"TxPFu*@ M,,D  JM U+A{!He6|phy3PHp I`ЂXt4? u  xx f QK~-bC9$PPf#+тЅpԌ[!{у$X҄؅Q$NR\M{Ji.1]MH)+©M ,FS:չY@S CcPB :e2p99Ҧ4Ju&Uh< UтyHIILՂ[%OB@ ~\5e=l$$@6]@ UՂ0\`3pʯB EVm J kԇ< lZmd1]Uu1pMNm~X_T?%у0)ElK `UqSA؉@e1;[PՄL9VEem/s㇂Ea;20Vq@ƒeRMT"S@(ҟu}=]1Th+oŗ$ @e!b΃X[X9^ؕi |!e0Nzx(d;͏5JϹ!̬ ܍H]ւZteňw<-O15KeF3]Q4\w\H]` ͝ν]@臑=")S ]qvBň/ެ }WmphQwފ@TQEY᥈`K~1ޑV$f}xHZ~8_H"iIL_Zm6-}p7Y必K``ޒURp_钘x=`LR1Rq!zLu8 =i%&XtuX;0x yN[0L6@2؛Xpb9\4vaN$ ~HlqhcUmP%-3VtӉY$exרXS69hΙԛ8e-oMeC~͢~yɒ(=dtmzF^vs†QO;7JmrOwtDEl/ i#sW߈Ofd0ۅ@iU_E^dVW~Hgg]ͩhvyh>n' M]UfYEHwh0WwPL.1z}y p|߈C1 6s5Vx^ݔx@~_BĒW SpRy wE phoxURQwP2g8tak=Ũ'i/O6OzĹ8,x[~U7Gegwx ~ SiKpKa[gІz~F@ٔ_^G<^)_su xp㝟_wzyPE|^z{XĚ^3t/ S-{2pS7at,aXv뗈Qp-bPK2_wc2{΅m0)x,hl!Ĉ'Rh"ƌ7r#Ȑ"GtHd!%Wl%̘2WGp̜:w'РO2@FJ ąK(ԨRR*֬ZrQZ06p-jY!Êҭk.ޟ* /̹ kF0ĊKүR2߿ŵ3ТG܋t@f%]vh\G6ܺ:;4lxe]=8ҹEM0rӧ/$jV0\׳$;uٳi{>5D\?C7 >RuaVp_6-(` fL\N!ҁŗidux(Ad>4wb A$4#m~X#A)F38#$Q`~IYA@<69@]9&ueTJ"TvPf Ԗ^6AҔy' |yƄ3c<:T*f=aqNz^H%B]I\꒕ګ!J8(+QlNv6%*X$˲"u-B Jjb-[iJ :.+,;/m/m!n&|gDa;0;0YW&1KpMq%Cb5l2-ܑ(8޾|39󆚶38WRllI+{Ms/=5Uku@0(5e<.&6ܵ u-q}C|}Pd7w0j"u+ޡ =KK>97Z-P95R~w>:YnÂx魻naۮ^V;^7VnL-"m? %= !A@,cR3¿K2p 9{0 `FYW\ !t}Oh6чH@ xb/$H+N,0 eDp qFc[RqF 8+FPj\D&Hh21"]Rӱ##?Js%3=Jjä'Cy %*IG[ S+20[ѯ~Ǔj MEry^×1Ehi0Y $ܦg N\Nd'~$S1YΌ0Lʩ' 3Hti 8N|E ? 9U@h& \(x= ҟD XHGGcqKat4 =ACܰRD6MAQ !Yt WMG@S1h7BP Z;bڕ{%;R N0EPQ&=A xIJiȈ` "aH<F% 6Fc q L04 j)] 5 4}ACT 5=bTD` `AQBu %^ ځTb %¤0` ]V9&iiC!!ϭl1˒T@+މ@v( `4 Rp2ҁؖ (HF"VB/OW9H ) f0?Ȣ\(n@d 5H ]@} ,8F7ӭu~IWc:B!~3f-;Pxo}{7=&U1nUP6<ڨ"S~l2?6{<oV9;LjY@滐xX#6ʙ'#BxAEޕA0pADk*!eH;:A/)j^"  ܻ TtD fY@)4x^#Ӆ?D0wſX#6(3l;B3WTa! Dna4 DPB, D.|aHDXBAtD38C8ʂ,C$6LA'T@ (A C@At)$~A)?ȃ6C"ޭD@,C$BBU@)-̂b'a>ZC͗2U\HYG.:8<6t6`F6C8C}Fsp8$@-'uuT2:, I%݋ө(WADrVDB A hzh@ %6Ċ5 E! hA A)%&).6)>Fs%)Y)NDvm!,Np1H*\Ȱ@]#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJEqXjʵׯ`ÊKٳhӪ]˶۷pʝKn]pV˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺ5K]c˞M۸sͻ Nȓ+_μ98sKدk}࿋O~ϫO~ˏOS,aP 6(Vh0aTBʇ 4J((20H(\@12ra**9)c\LrJJ~8J )唂 /T▤" (L Yi&U 4 g( *z A/` ".5ȼ@Z3!Ba1A8l!@'~EU 4g'"833#u\`9 q D_P'ސ6W8`ĩYDX1BhB!ܟbAbC0 &04&Z!U)Zh|oA;J)(&la e! -#RBa- L(:D& ;Dc0(ZQ}W)ƽE#7Bjy`D Z0 !4# Q%@IrTH-ALTD<`'l#   M)@2!c&̂ qE]n%ƹGU*B @Ai܂b@YI=9pnchGj b D@ @"4#Bs @G jP| iERDJRn-BHO)`0t70A ᠆JU SU. dfh8UATP:pliąC W\ս55-l5z=bˠGd'KZͬf7z hGKҚMjWֺlgK1Ḇ@脃 p2 jH#ӘFA VÍnlQ mk@WEw " d] /P Lc%7k # +గsH$y@LD#! H6Štq`@|Ykix!PH`6ɓ櫳R")$i:((k[2ށ#ߠXC64` *A"4 6u \ %A@^q E""U$ZgH5 &@J`Z#r70Ҍ |#FHHX 2"tMT'18XK5y0Nb X8`ӈt. SH " xru-S2$)l LB? ?ȝ3$<HlCB@`A$ .lnH)@" !E9'i !]a *"#F2D<Hy, /al f9Ut /6\1Y캰h@?P!?}T"LE#DvBG7t`$o$2\6Uxq 5$ C-4V%.Ž5xAa`Æ%(jA6 @ @E-,A ,&0 dFFF#m;&bwH0@ba"C]O1DNLPGdpyBBLM e>H.'d 'YT0 t1|_6Ʉ w? D& a5 A xj Veqb.H°i!LS 0 `;g`I .'4axHwP "0 7RQYq w>W Ɛi A30W 4`x1 !u%@cg0 39|F35uHMP Ѓ]t Ds|  `P4` v  Cjk{ f6R GYamv T' b DnP P| 6T W#C3x%` Y 2 D=RfGlX g V uh pU43 D ׎nY)Pdnk Au  Q4` 4(  ▒я%T6X ,%quT7 D9P €B5yX.2~dn=@@V(a ir%n\ MY+X|ImW yYtfmZU;6< )YG, Z U@Y5a{U2[a y)ɛYx5hIΩyYYy٩9T؉9)Fr ɹҹ,HD Czi陠 ڠ qEڛ Y>ANa $Z&z[u,Z)+ڢ, QP$g8:<:X) aУDZ>J!F:JHzJʤi"( nTZUz1? _s `V8RʤjQF 1tJvZm{\:Z*qڨᨒzu꩟:ک<ϹB:{QYeW!f= ƪRjyOiiI*؊ Х[*TيZۚ P1#2Q; + {k [w:̣ X 9JKt *:"?? 4.1˯ X? ˮ> /.BDM -Q+!QW+R;+kA _++2=%Ŷ$!<+9 q+cS3m[+_2q|oK{8t;+K(ӮՂq깢5*a)ۺ;[{ۻ;[Ƌyk:;[; p`];1 WHX!`0 kk+ۿ lΦs <~YD<"!<&|(*\m.02<4ܿ\8:2@LA\1qhkNйTq LY7\Ųc\ H f [|fl]Q t̾[uIVx,Z-pxǁZz\({(K bQ@əLf tȮ<+@ ,lY]A9l ngH׬pM:.#ܘٞ٠ڢ=ڤ]ڦ}ڨڪڬڮڰ۲=۴O']ӏ,bL`ķ"pp 3;E n ѭ;foRлj 9m[쭼ӛMߪ0 Z;. N|&h@H-ќ!10BMᶋ |8N:.| }s]l001 B⟱ MN a~8S1 1q - Iʮ -# 'I(l;1 Rfy1 1qP+[/X^DPA閎'B 婁 H0`` q 0 )N-\!π,S.+Y^^5 SUA R \?q:#~VGoIQ{YmYpUMH1ed`~21tb]Nb !Y 421غ &o(MP$ vK2LuC/0(>G0ݺi{u.QAHZJb_Q0"_1 2kv@jݬXW56R7ZɻAk`e@US' i@c FNZg1`na\Xdkf5h O-`Wp@ Ia4⭛>eQCXs @R ` T_ QqKXQ|'-g$ A% B >QD-^ĘQF=~8 H%MDRJ-]R|RPzW/⢝,C9ETRE>UTU% s!WHAe_~VZJ7\uNwd" rnBЩ^ƍ3cʕ-_f .#Y1Fhԩ[슖@-aTZgWōUNm]rmZm z^v ^-<|H_l7&*j@S2@d#k0B !ZlB  rTp+RqcH@LBϲycE12 D.x)H 2I.# J?մ12̒ ɓX#B1#>0 l<5f ;I@qFSMAٓ4F4+}ʌ >4"1%5%lk#HKe%ci  b%dWDU}%֣D"P7ꄍb(l#d1%%4"mq&V/Jƹ4]4ۑ-^sBd2smLy؝ U 3a a[~0m7}4 c0eccNz7Ȉ-H5+"@.hǠ~ Be!uF^0*}vΩ&v[SǖnEy療V_xP6HhaX(&]ypٻgI?5'}# BBVȵH;tI~Y15Em'^"Y ZuJ1ʈđ"ڋG wp>| t>1Ϛxb*|_J.21g}1dG"`H]ʼnxzIz% %cT&@!@ 3:Z\D5Q9|8SAs$ (HҦŚE}0tCy N\/":06| lftcLHG'l>WB0v4H A Q$xDG(8b䑎ɐ!oG 95 !d AzcŇ$@Oj^8ɗ0R"h#sFVBK_+`iD<`42YDbI1^?% ¸MqcA1\˙&1Cѿ>%L:$, )3"3Phkgϐ>GRD6.BFWB CINv-b5+c#ँFy$tiLR (8ЍEy 7^G*S13y)O6 ͨWj]ziᅞ#ˬլS)CYZdG8VyVvד,!z5lb7r:T4;;YʂdI!y쯲-D,IV0^h#A-$tM X+ֶimAp[PK`GnyK .5 `zlh%AZ;p"~F:rgf3vs hwAptmH8 L =zGQx[$jPI]wྤhwϔ2B1hjq dG TU<эSE.!~ ԦchЫ; %d M_5 )|jtt cFQ^xӥ{*}n閠#n.ia m]I2t=(Hܙm,[ DVBhߜ!E|ı o?( UOjduLִFQ>na,MǢM#b,[n''ӒǢHe#/g_VV{P c)>BtgK GGGucG`7gߑI;м#FJ>GK+7aZHW:@rp_+ >\p0h?{:5A\4g;@ \)c4ײ8! | LA, x2*D & ly¢%ٌKtCr365 %4wTk"lç z#Į@|x*$6DĪ3iʮD<߱L#99@Q EU,z:x.z@GBZtŵD3; *2C1^EʱҦ`_ 0;+TCjl[VF迱pLqvرAs h*FLĔy| Sx 371lV+evP*GS[N NYQHΣ0x;8 χ[ӯ54I4KO1 KyAxO,K(E y< .XG E0\ʜdx ețKdЖмȬa2D=,ѨlQ/,bKbm m.H_҇?PLҖ|R қ*̌xQ@GRx4! t t,2(i؎C\٨8ܜN Ԇ0C] HRIX4y)tYFL C6M{ 4̧pѲ}á[րwS6,l-s۶(9 TV Zaڥ]y-xªQ}eڼ[z܌ȕSa5…bwi#b$aݽXqPA/HbܳYI5ㅐiH5>0h-cГm]=6~0bc$RmBd8բX=Um9~83Z6Zd,&Λ ~ )``6 [N1`+X]dfgaaeL>ETEmancq&紀`dK^uv0&yq뿞 ixfk.L.I^hŶ dga7w`j͆?vF#EFmp$7fʸfmmI&AxB I@BfN `%V* ˝NfksAJ:(F] Ao g○[~w MoGke pkl lnppDE?Mi _oxdex@q]q7qgo"]Vlo"r=+oթ..s8)*ɇX4yցsg^:i9s/g٦|=e=oZs@;pC3WuFt-?o/Iw>g*Ms2'YO_N@`3٦y6G&ip:lŦ3uaCtRYRW Z(i'1e8IFX6f,O&šONcG d[musol7vd@KXQDECwRlvxRFG5?bywxnaW_x5ef4-؛7O N^gҐR_|7'yD ~iyTQۨ yP?HF$Yi8atVZv5@ $]xpX;f:@iHXU`? h5XT d@$phJ_/`,Jk&x/5@v6o+pkp0'؄wO6@r晆Q_s0xesܬ ` ,w D )B"( ۵tG,i$ʔ*Wl%̘2gҬi&N?'РB-J[k5"@qjXI5JrAI'iPD#tPZ!Hg.޼z{w.ll W p&84Ex-63`#Q0D.X6ܺw7+^`do:4Ir3 xHFXH!b o8; ^ߤ?hV\2R}SK~8˿?oPҷ<`glC #+ $ F^.ط> LC`/hgR ~@Р!aj(!J#YHы S%RW<Ŋ^ 2n`<#.׻4ұ 3Z}(Af|HB2{bA Ꮢ$&mEc&C)J (SU|%,uR8y-cb@D2Qsܟen%IiRs֬&6lrs&8)A&Iiu.1 !,Lo1H*\ȰaA\#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ,Ƹǐ#KL˘3k̹ϠCMӨSKNaDM۸sͻ Nȓ+_μУlح: ,5O[4A^'"nP5 D&niEBĨ<pEIA^b*KwB_=F}|#8_Vy@`.1OH&) !F&xs/j hD_G?<(@ \5R`8 B Z"rEdvڃBX"٩-18e0Q# #&1PE1!k\Co` ߲PV %g LXф%9m |F BBů)H@EiЅ3a0`(Q У-A LOm} %`)w2IFty!Lf2ou\uAW*kBA&OJ1fa2ׁUąA([(x*l^:.X<1 PZ%^NDy/\|6;um(fA E!_Wے'/kޖ]a[wQgz xKMz|Kͯ~3 L#ʇ[R $AF l344#ZRB^AA8Y@R < NP GDZhK$,)QA q Z Hl$y"pbdϐ B nH#HAjL@!# dS?]:qGHDZ"`ʦ &( A ьPG ~,2Aѷf @u#X@F8!E0dk&p6mFpZ"1`!E/xqr V(aC1A\|!WcBH*FXPy1HnϤ6.sJ'GdO0/W-ĸr c׈-ADCyKAaDTP@>H `1 {N` pPgA.'H (A'8wLAt;C4zI}`퉄"~C}Q% zNv le{  ݰgEAJ jpRPEf&7 ap!:bNvDxCy k7W,8Ah'pƦ| &1o Ya r8tv Qx("yXa ʰ ' u 5-mڇEFR= ts}@l(u} P 1 a #ed PuC V \8(xexX:a #'V X eAG Y8쇆u(/ /3gk1 h^x4;0yGRX\!$E 9w@wm%Bw s }d } WYhcp ` a hAkySd7r N7 m$5 2 ] (as ? BYf7Б sLIO9p'E9 ٕH;鐑a`!(9 d $# N{Z9 q ؕ7oG]@0q1avYw Yd)?ɒcv QuyO.9y\Q qMIP!1+yʟ 2 {Atq c nstU^ )O"9$s8Y (-jɒjV"?Eq;XDZ!QE*HJ R7)/ !7W]<?av7VȖt vp t@ls:cpʧ~ڧq_O uA @p s \ډNI :'/*\J^:*>q(ጲ**1ifl@'a [7jqʤmj ;Aa@G1358d5$!;`s(9 DcL2$;!|56 \*,,|'L",/\0)+% f+ 3 ]V;`V f@@kY}:\7 ,ě [0-}&q }4b!]A&A*.qZ$ӎ'56=t:-`c0`D:bD]֐`i0`Rm4;,$mh= i@lFRw z 0 (c@_=;< {g` <``eT-! s-acq֠]@ۺ !}`؅̹K0=ݍMFRmQ M-ʽW Ks{QBꍺc- t1>w >^n,}%ŔˀVq ^M֥d`.~q 6nK}L

89NX.WoY.]j& ]Đd%@+EPFq~s_:9m)=B! l*sN@ 3wonz>-|1KT~>a>E>ٯ~#2&p pᒾ6J4QX>޾~~Š NӞ51 Ξڮ^@GU  qqmL٩`@@޵mga@ ._L .^N+0L%@۬q5 -^n?Oc!!$6m>PVG^o4 ac?^@mm dڕ mEA R>SW>֠Yn kqqٹ jm@/? Grt V㔻_jo0 F /o㧭9 -?Y~Z! ^.>YޑSn_42~K4 ?2]?_k .@d DPB >QD-^ĘQF=~!6%MDRJ-]SL5m޼N=}t%IEET鸁L>UTU^ŚUV]vdZ_͞EqNվ7k,pō,r͝??tխ;س4m tٯG]dW6_@0ḏc~U/? @Hu2Z`c0'~jчBIgpBh,xC_ ˆ80p*I?HG9LF'^CRE!/',XP(tNg'!$.11bxH 'yl3P 84/'¢1hsρVg? 4QOw#t wQ84S%J'Ny3bԹn j6=4dzPMuU E⫵Mb^ߓUJpRLHf hhmh#EҖ[oOt ,3gZaSH PAjG9g/íеd @K-zUW=|u :F~͘gGMO񧍁g"+4NkgqcxP藒셰hA"nZvy<FpVͮU^uH:Y]jl EX{:J1!|FjSL<b&Cq.,SpG)Q%d-p4a,@ñm NvM,r7lrPTY4B7l &ȹMjiS5F Y)3Uҭ1#F0" wH-Lƈ~ (׳Ƴ!)tUӞJ12$=-8G.e9Jb` fy(JgQDb@pucKozD2":F 4d/וB$_df};>c"0 0aj{U\C޼c Gm"}!|)ӷ]QꒄMJY$4EfpbCe;BZ""KB~h?CLk?Ki 45sgCli<-6z;El x>B:#!g[>PJ9:<U7(i7y`p؆ C @7%l݊)IcPEC2t sm4Tÿ8t Hp@Xy!b) 5ARRH E <Ĉ7O [Pd83Tq 1;V HGT$H?amjBƢH!Yq lXl ;S -Fs/qi I ܣGH@(蹋8+tIxSq~NIphhH<`4FC`0(B"x`L 㺘˜HcsI lWw{wyG(炒 S uɳ<T a lD[z-Ր\ PJHJ` ~b|.@CFI{g@uC0(L~@p5vبjèF/L zCӇP#(@ MxƈDX4cC~[py #,:^MҘE< @xM̋e@k"4X@w4Ϙ!X"̋mٻ]k谒ՆPuڔň"[H@B09DWݓ&m'i(uX%fKeخ-t˶e>[s=/Tv0 c[p A `8& PV:Z@]@ Sh ݄(U08:Ԇxq\|7qLeNi}_X KT` h~+i8\\h؇ֽO-kݘ.hZyРWxX)E -1ՊIW`XANK t&F mx-)r ̥ʭT s_ױ*>hk~ iZ-! hb8RXmqB d} Cf"0iL> 0edJ EeX^rXe? 9EZAdHQ_yipY΋ 5IfA9gfAfoƊS D֍ Jg>!y]x~O~F lIi~+3hXSɁ L=L2Ijk`>pb3F h QPH Tg课^>V0``8+dPWYCgvgzfj{ k0bJ|Vy@F5bV쮸M>Ah , ;h6#p0bg8v dfPd a(Gl9& > 9#ܮWgXKۺdKQ`z)f>gfKۦGmЏ_ c(oFdc&Xk+Pv[_@ňV il=.ޱ8y1̊`upp'˜,Ka|UGHF﮴,Q{d8c}C᳠>_ 7(l]}-Xq!qnjrݟrγ%nVre6d 4兀YQ m6mpl:W1^ >?ef>{r9 a K_&pim6򔨛F>FA5tOxȄطx(akvkN,o88ɳYp0>\//mnP;euJOf? yVcTfŪAyPwmn78f HrrQ0vwwpkgETVZ+Gpjv]Dt '̑txv&Gmd Bw)akY~7],m = o!HAx!xX_Jzp(#,0'! Tzz`XQRղ|h{wW|{m e gPvw/PZ: vx/{^!Ӗv+w?| /xxdlwsp@,m@7 zQ~}Óir#6u# ]tnW2rP=,+`c# l qDPt_qh+ax8e3P%/q6!2Q9-u3Sd(N#=ANHTĚ(0 >Ûx;L79fLtQ%~(~Th #Eɜ`|%Rθ<𣖶-HdJsC\m%2!Y.,f.G\0]23~pQ1ywMA@~#% CykI=ϩϔ萙7Fl(eđ쳡+90o8 >r nr(HEB@2qยv, mdTwGC*S7 Ǘ.8k$Kg*TMJ|nC}G:FAEϒ:6nP&cZ՝f+!1Fh~$kʨֹQ+P"ҵb2#y˯>؇m"%s4]XQ0'㮏,#m$[iZ6.qq ós-p+7(*\smR÷-]ha,dSӏyhNG=Hֳ)H@ڤ(l@WD5QF "wZ0Bc0JAc^?|07\Ű@=gX?3>/Sֿ>h' D>f}Z#vzC:o%cO$I9 ~샛ńGIDd藷60P zאbENl W0l:Uf ģxH7JFL^Q n\ MF0482PG@a-!Ւ:5|3G<䃰mD?Xa6hb۔uh *! * '`m!D @|]\A(F, PbH x0!:\w|a A;h4hExA#ȈF*#A«2.Lb?C+\.E6A @4@AAT&h FBFpB?1@9l2̀ #4BfcONmAv 8XLn;p@eZ8Mv$'D9F@@dAA@-# %F\\IA%F0FH$ѝ"ffGX4$=J?8ЍbBlH24I($C45Wb33mK8.'IlH-U,?Ɇ`K,$^%^L8@-a8$7ęJ@B҂.*X?4?t3ATAPD@E`!+ҕ A]W$C6FC . K\j dvj>ՠ8bĭ## 9rB<B)V+Xݤ$(3bAX] B@Ceʳi !L+*> x ;nrpoDB9BB )+*eU*‰dz I3pH'0 8D=v:ZFIY :-`@\o;"/ X<?0P4ƨQb9"htDB /HAta #XoxK 뷐[ I>7@ $` ّMW4CL,]D,P=޶uAB!;/k <ϊ!::NlЋYpp(AT?i<"_7p,#eP o!nߒeМ7C3D`C y@]oT"㽸"k1 '-hht 1 rxA vx3{pWXz {x}vtGtQ Sor3 p& p{~q `@V (x } &0\Vb' nF(Ѡ ` pvv ` o6~A ,(x@(P NnwQ@ qi JEhQ l"iu , 7 搄0; 0xр7XOyՅi#tbm H gW wA|х v% 'B jx@;Յ6 k1d@ZV x`H gWEgpD1 +UpKj*QZ8Yʰ g KHa KÐ P P g u Ÿ 9ZB 1 ]ȉPiІ˥@AldJGfW ` Hi: /G4aY_+bEbg ` - yBAk|8 xA vX  f(`>\@h9Ryv0 q8)vLW'^K Xh f8ЕQZk)yxGQi^E^0 \bpa inᗪ% OYyٚ՛Yyɹŕѩ @ rؙٝ\I֩ )aI@:#19LJ*qLٟD6sOyɁ)PC A9Ud ʡ'ڡ":$j"ʢ}# *Z4 !/ĩ0#WdRuHH 7 :VP UWR[J]XSF*jlڦn:@t{s2Wz|ڧ~:ZYiѢ87| 5!4A$/seVfp&a:V1$ % a8sJ*P }ױ]*^0:Њf*ʨ**j:jՊ:Jߪ Z S0Yʮ̊  1Ě [YKk{!;#[%˰"隲JQtBOˮ$[09˳- (+kBDO9OjJqsSP R=C_˵a[ceA J+mKokU]ǚEz۷u~ŷ[_uYk+U54X%l   u Da+kCCj{;E3 [S$ k5gQe[Rt\A|ې[0f_9`}T۽a0  {~++»VKzo!뿡 Ok \F] ` ;!ELa$l-"\(K^q)a. 6 5|>)=B,\4H;FIP*T\T)L,ŦkV^l\La_<ƪd|ƲlkLU7 TETW%zLKC2iǶuO?E9A}_ +Ry>=+ɺɚU|>W|@@_e caʨ,  GS ƩS `A̸P`,-Y&κh3ܧLjd `Zx@ < e0 0 L-Fcp mV3l ]lі 4I100d%EkFҰz -`@Pv ,_-ϰ p?BMU. ;|PPE`,[〻'ƤvZV b=-@vl Pk=UL]Jֹd`y~O{]،؎ؐىp]';lٜ]٠ڢ=q=ڦ}ڨڪڬڮڰ۲=۴] 0P5ї ŕu ]NJzܜΝץ]ݧ 8T ]ڝٛxݒ_-ܭߌ1dž<k+ m<Q߱[y86^P Lj]ؼg ە|hA<W,L:] O}IE8_ HI~PTӋ1`b[egNikpr>\zn51~>Pvl݅{OKZ̛`ƚ.꣎ݢ*!O0*W MG}!ཎ.^*l BBA@4 Qe YR1d0Fa~!qʙ>b=}nn@> <!0']0 Cvv Oʎ]o ` IM_]M^TM! `=sM^1)~_AH=;dd0 `qL'Ng>R]1[)mƀ:l Z\Pq``Ұ8a|_*~I]I?Qe 0v, RmC!0/ZnZ TѰ0 /`ֆ;^M7{Ƃ+&$4` FDxb kk1b$0BvLq 6H$AGWmk=?Ŷ]jQMHte-Q褫$e8 坉2,vC*`ԲUI'*XC282e>(m62?dˉ$+bpD& @>d"etu2/<l"L91w6z5'>kAHˑY?A zMaj0ᢵrlb5;O5^HhbҜx\4ʰ,Ă;LIc5 Ya%%ȶZ@12A&xdFYc␥K"cׄ@ԏGvԨyH#w=2B؈k :BdDymdCBɏ~q<5qgwz@!D]G yl;(xOӷ}2D҆@A+|xz@BE!6~!} 40>8e +* >MfG ?}AB[ ^"᤯c 8X7GcٸȞB&I%@C<@"T;a1fBCe 7Z@3s C8@5"gt +u#a#/ `$  L-؋#(;#hj5!\~( )4P MӶyB~ # zBPBS ZBYX2Py;mȺ9)Bq :-6lCHM@LYIlڋD#Pčyx0J  !-Z `Cuy6@ Riſs38~e3uiو }FxWWr A $ <(є҇uDqzGH4 t0`Ǿp8T .kMmvGz7y0 8&o ɪ?=XMxp0kTq 2 n+1 v07mJ)&T;C6Pe38نQ*0պ5l [BJJJ;q~8MŠ拥ME۟풆A<6ʼnO=F}!]IP5X=Bt|mL ‘2\Dqk BTiE$gh(- Vn !Qt 2`ЬjUx z׃vsbO׀t!Lcص,m:M$ )^5s=BY k9$U釻T|mz ؕ HTɐ  XHǢ cXNUՉTK ~ dPaU v;1E5ZQm ~" ESQ=97jDbbXc jI [UK;$9d0\x8,mЎ5 5US}'k1|٩C]]>\n[# 0v@%a]pl ̵wEW,P } -ا cѳތU"U[Fː| y*l a=ߣHu w4FSۯ@ߖR_ F I6kN_߃C2ȇĆ! niළm\X"%peQ'v$ Yjb+~_/)G 3 c(6.!52"9v0f)*ҟT?(6d[9 \daʘ^ˍ1#  pH%t=j 0V;%jN>eFV4Alp9ෑCYib8 %[jG$4U_4H fSMK M6I2r&t J.@ AƋ4s& bv6J%6<9V I]8hgm.e(\mů_~މN 7[Ɇb9C70 Fjp9Pc Ѷq^-Ol^[#eݺ\6f~kmkNjiX?y8#VjR2@k 2 ePUSiylg:Q#,NY[Y)P yklJeeJLe v~҆hZkf~Y ^`c^Tu/pT<'xMw.n _@n(Kqk<a"aݘӞh |B10 \n[>po{;4^prN"RFٮp4"a" gQk'P)pe qunIU;fq⇴P$./i8hDRʍ+Qg 0(U ~؃U~- 7>f֎#0!~h x(/jhGDPmRyl7 d6 :uhd%vSߘv0lDA@n]H(nn%/ClpJMX%HuXu:omKAt̔+uq7quсfCGeVqh93d%V$NH:mІB5wIntdV o i xRwb^[?eJ872Q.>~x3gxp^xrv& (?tiҥtG pUmky 5ۋ)wa ɟ.آ?o3͖ibxUo}x1 z&7i e 0q Ba ڸǓ y_tȆ# 콿4j2Yr~~jt /_|^[~ w}Eُ^Lַ}ZNaݟ}Mqxw,~vw~g~\1懈}{[b>6Wg}~yqo,h „ 2l!Ĉ'Rh"ƌ7r숱cgh$ʔ*Wl%̘2gҬi&N0 WΠB-j(ҤJ2m)ԨRRj*֬A @R+ذbǒ-k,چ;{-ܸrҭk.޼C='Я:$Ċ!/\'StO7.mh 0o\pL7pزgӮR8=K5eEϟʗ;41sƺ 76]ѷs%,<^#eXxo=pz_Ic6ag< j[8@KB GL#@hiCap!-v#yT/@?"y9 9$RAv Bis)ܓUZIyDX'*F\)`,myk]F8Pft'}Zd$J-MsІ~N:P("J2 !\D)[ OӪŤ:_/0#ϨJ'JH:,V~.F:2@ DjҳR]slӎi-V,Ju<;{6[ԻNxP;g0oO[rqSw < 2&2QH,<-W<3M3ϜyLqA =tF+}4Bpj$CRꌰ:r!^kXVҵQQ\".ut h_*؃u c^Vum<6$@ͦu? YΒ6YbP/L%i,K+[R2!dPDآhpζ"CV{$!1aA8a@5>顤@1du@i& 8Ax#+4`S~k+h2j>0H3Rr ' mdAyC<q`+%dQw  Va V8ȠhpbF>ah8@">lȄ }@Qo n8KL/ 6^ō_*#͕@"`&@l$ N$ T @sٽ#~7ӭu~7-yӻ7o<Axy-py!y[ <-Džy)xB#Qo?炇D?X@k(yYTr<0XT^p8Ud:ҥs  'E«9ճ7Z:Rf' 3C(=;"s`nE-8dqzl/%~[](%D@\Ӿ#C @FPݛ+*xg>Cp#}~ 1;-O/ٿ~؃>H Bp@>L>1`>E A B `A` fv ~ `RT!,Lx0H*\ȰÇ"Ad3jȱǏ CIɓ(S\ɲ˗0c2,]^ZY& l6(O Mд2,#4<(&☉&HFX%5kJfIS 8\#3f,E#Nn,Ab -rDT+` k°X)ZKkdL*B 2( 1IRP2I,+Q(&j1P Iɜ[?mF+㹹B%f#NDs<C#u+PgIA~5a-t7fM? S Ho_8Ar@RH;x8^\dSV$" `ADa }|G%`\F&>"F)nPza{ kk Q ~hPPEY\tgF h Q̃ؼ}t@=! A R>ÌqXA( 8FPa D{ `R# Zf`! Ph4b~"$H0DP Y>@" +p %~Ga8BZ1, 9S"IHϘ1B$+ Gq D#V( -v>Bf'sh$D DGj,M 3z2EsYC0ɿ$_I nR KXD# 0w@5I`'-IjeȾ"HvӚ 'fD{4(E^)v;TΘ?"r_ '~3=7hM(rȊ DRCXҞ3ωzUKVd>AҖ:*Y@?r <( kpħ. jryE"\QTPjZ,*/ #AZQvJUXcGU t^mkPÊ#xAkZ!irU J@ xj[0сZ$hZ1 V D|D0 F (RVh p [h ꗅTȄ$ :!#jDLڤNgB TZVwZ[`:dzhj[Yl`p:gtNzڧ~:ZzNԨ:Wa bFfh〩z&۰ :az0 Z L) #9ʬJ:ګJr:cʭx9dT2wd`0mpBc~AGt穭Cd  ې 0,ڝ^*#z "ʖ:躭zF  >Vfڪz:0 F @ @[F{Hj ) +]OQVzQ ` ! *<[h+ ,pҀ6qM5m* LL#`,!R-2-2DگaqGnb4Z Q j}vE;,һ\bpGL2fTqv*+= Z}225ft=F|Ґ ؐ!+R(fecs& Ɩ&&QQ !L`S*a\-p-I T`u ؀FbA4 \-W\0SP M0/qM3)=@(Z!l_bz( ^!ݧ] 2U)P`6č 1H #a2Mb ]o] G۵Aa ia #( Xɐ ]d^M^`qW2$R65 ِS'7y,~QV`dj-2)  xol ~ W9ts<; *릑3arՉt~=` ^ځ2N0@ϠI xͮq3 s`a+ Q[qUK1B,UWhԎŠ%7 i(" ԭ z:H_ `NR?]lTa aa>^ZN ~E*`_,- ?1{o|"< JMrtP; %z-U(LdOҡ'=aIgЯ?q[|zA !/ ]- g jMz!!Tc60^ q]c_Sa ,:_Wq !Q,(%d !@@ DPB >QD-^(F1ʓ/BwMD\Tc'SN=}TЁEڑˤ P 4 V]~v!vƴEuiڄm N\ll1>iW^b xZō7ZsYd&LX4' D~2`-O !(׵y /mLzX䠱Zt5 v Ԙrކ<J!Q ЮR rR e"͟1 0C 7|i ZjB& 6Ã⁇o$/372fZfc Ζ\(b#P\ .kq 9KJ+A"դ D|ZH`0.[FzN;SobM&TΡ6VP!PxӬ M OQK:RM7K0FU{l4UUWPVj>ve*JWwW_#epWeez(!2"]Xd[oHLh(ۍy\w! wS?]H^F+(BAmG .΄BbYVLY0TQ2q!ҍS ෆt%{f}4%T2pHZ+du"W 9 0lygjGgB#[cԵ4O4zՑ( [6c Y u[ra3UEyNik[lF,6w?m_6v)U4"7fkFGx7&^.!]C#}cGIxo">j y)Vq,܁U_mw,Si߮لCNH-v"O3`⡍,1&%!R h0$z r9Dy01!HN ׈X2w™ .!qԂW5:BteL,Q,BC)6D#L1JN1q H3^C1hqEhB1>ll y^%ƐEf < #GFjriF"!8Nv~N/BCHbQ Zq_QT'܃H;DşU-eQF³ <+& n/"2,l'IM2D1SXX3U;z}\;33tO|?q:y]ߪw*!£ݭCmKL'wIT8q캯}J:QT{Ṃ279OpG; $ŘGw $>?)USC8@Dk?<]L/w)܊i nwI%l0s?K6 "@v3[?@;v12p ;х" kA(c##4؛P6Z(29m/yPyȾt>!ԋ@‰ ‚ u؆d "계yAB 6|.Lwl#ih16 BB9F;w89B4i76TD!AD@hv`.0q TܱDM:XN)0iv(r>WE3x`p!XpxbDq6xQjFE?fsDt a _p EuTpc&2zܵ7 ĮGqǵFNHP'{| upP(/:t4X 1h;bGPDUHȺtɗ00HIq*ƚl?ɠ!J?A &dʨJDʩeqʫʭJ̫ʮMʰ$˲4KʳT+˵t˷KKȸKi˺˼KJ˽) Klmл YCDLKDyL L/ns0KqqH iӊW$M L51?|i ~mEȔͲԭλA( LM9ʪ𘇈pyNЭYbICZœ8F,pH"(ݠ iM|괈h}PP tڹlm #n)O1tpZQ)AГ '䰙L Ћ(!5}%ؚumPQ(%Rc@M!`-G}3uS7NqRKYHEr|!>:}4A@2uT)2 C";UH*U1HG @LMm/O} LQ%RE O5TQUVuWXƩ֑[%)^$_UYTa}J]9VXUd}Ub Vh7e`VI^Vb5VpPWr 6rlo5aXxk{wpVųjWMդ؀TZ5V5AahL 37q )5?@:m};U$dC0>00 8״3 ,ک <ehӮhSȃܱ-Y๴8 Y!B $ YZNyή@@bhTXPȇ$WY5 ( edWXP\HAN Q;tbEDNG\Æ5CjȂ@xHF Up`ؓGO> cAh0U00e/ƈ[hY+x\Y٧ͱ_.>x݁p` (#p3wftC@ߎQЁ U@t>_ep Pf0Pd`x%H db4@N rBdd!ex$p`F@v>0I }>܀ZE_N &:> x L2Ix6f!K%+Hx샚J1 /Hx8БߎHgC=kᣓǦnՀ!,Ox-H*\ȰÇ#3jȱǏ CIɓ(S\ɲ˗0W֨PX1sɳϟ@ JѝB*]ʴӧPJJhWʵׯ`ÊK*b |U۷pʝKW(X PJ LaÆ-4iL˘3k̹ϠCM:WJװcWVJmU9ͻoR"{.\Ō@YУ%tͿk.x_20c(Q NϾ=LR MFϿ?Q'h"@( $(6(b Vh<€cE($+cL|)z]"tֹQ(x|gg ً3΀&Q|ʙv@V(馜n)(*.]#N:h)Pp#P)ꭔ}k믝 5#?o,&h _a J-|Ȑ-v~.Ȥ(GlLFRqDqAo @V0 0KsꥱF0Lx1z@$jZ(I*U5INd%BxJ d*3̧>CKE@dςCAJ:Ua7 3l\׹@$`*LG?:+<@T 4F4Ya  - <2kL##IGPR/Z&` d "H2@DN [ 8S|0-!`:t +DZR5H+`D$A (!:80H1bȐb AAI"EBzE o0R\^ Y!xFd{&Q\A˒e)FTD IAOK Xچc 2JbdBwۅl( HrR dl+= )+B!`ux kW|6 @@I[d=H"!'PD @ $v 0 KJA (Ao +d?=ȚJŰq_IA~; d T8Hэ:]D, -Ad|D 1p]%(-|Xb H[ "jNEbsP2" 4lHH\Qؒl/J\)5,!J_W FF 2 5dkܢ݄9b(++Ȍ@Y 7AL#iw&y-$-i[h{ ]i'8% PAZ: '1w <0)*l p2@Ax @+_ x`~ H)t*Q1DX9'D΍w@qerU,9)8 e>#2ݒ8~6مe޷twljRy[%1ZTe|M˸(Ms /ZDXZ$ E b("v%QYlx`HTo U2rioSPwX[E PƀH` NB?" ` ` tbʰ#[1~a ) #3U{ H~0TzA\!ho ` ۦ}@}$UFP% H@ &0 NG`/ N' ƀ  G8&!xՁf qpT]qNa hPNӤ e 3ih?XV@lx(N;!} A N0@URpA!P QxD Q1 @Q#sHƘHŀWLQ w' Ԙ؍XxHJ!E88NbhSO`C`8YBs iMJ ِ9YyYБ  W$KA:91 ᒋ.!-A,a8)<ٓ$;U:Q;Gi䤔91?i@I!RY (]ɕ&A%aׄX)gɖ"!a aX^Ŕz)AIWI~*Y{)ɘ|i)Yi m"8iњ II!̤  9yR՜Й9ԉ4#=Qy0ӹ-9Yy虞깞ٞ9d 0T@&9%T!(I2 0r ڐ#0y9T*, dТ0Zؠ 1@)@BZPCz_HڤNA&V8z\*d@]:dadz^n*! oZJ_ƐvO! GŧmZzO Dڨ*1 C樔 k&1"cRaѩJ?8T`ʪa  zź !opQ t*C pG Q㚮ܪؚ"㶥ZM#z!^mj:rYBP$tT (Z*0`0 ڟ0A+>{ "˯ZBapڲ貶a`2ъ<+>B;D[C1ѲFL۴NPR;T[V{XZ\۵^2`0Ha)c[/`ai .96ORCHMzx|۷~b% z* r+Q,)}c0z9ۀ1E#TȺٹA޹ mJJۥ kE*qUĻ )8|ɥAr+7櫒~j,: ,*J-  ̠\yL 9$̞&|£ i@*,,.L857,9;;+`B:T{` $.JCiU# ds!M. 疲O1yep 츢P*p;9=Nf ۀU A_m6`@_"GAsKvӐ] ߢ"DF@ ro0? Qt/EѿTP㰁Wq1?aA$ eő-j  #ON-PxҤA{iiu ^oCuɴR"0(k 5A1.Tg_j1?B}%ѯ߿DPB >QD-^ĘQFRH%MD2X"7NL{ 1XDҒO]MEE%AM>UT8D2hU]~-.?e͞5 ZWE\uRl^}hP?vdʕ-"dUkٺZh`HF5CJ Ml3%Ɲ[7W9,AcRn͝?]55lݝ/^h>8.3tgS}avJ>4+rфjQ) TO\&}dMTn3Ƙkg}<T*4NWWvIY3,g}l e+\uCQGeKJl@+ɖ \!QE]ueȥ7qyx>6l^ 1gՖj%XBEibJ+/`ǡ‡?vU/Q%WB ۗ6Q6bE;QdS#YVyfVJp:PJovRv4׆;:y%5;q#lc]*s7CGkӡL{ yO ݘeyTdN `Qy^&. `ʭwuh~|jџb*o8zWO}QA~dpJ?-Kp]ؔpc @Ƙ½~efRZG۠N "! 9[Hq>B zO^#% kQGN8! zHI cgMӤJHZ$e,I/ʲ(ۀ\>/251 ?ylҖ/:4I<`GW~3}ZcZ4C =-H+$Iy+^Wܜxƣbp}4^?+%i#G9)q C)QΎ \%'8qg ]+lVu "!a@1tHjSdtKauw]Ogǫ;Z{ڽTꕫt`w{Cr{w]t{?lQx7†w|xW_,1P} yї5:K2Y"iӇ]c8\I S.z4$+km!W~SS&:/P hm0_8%Dg᳕mv˿pC=0-mH3˘@pX摉[@б ѽב:Pakex9dkJB1HapM@XbRmhj {؋:y#.#*L+B&.$PA 20TCI’2|:4C@4vCC:AjK )DFz 9QypDDSZkQGt6c WbEġ%RYŧ8]tW9_Z9yXR$fq1tiƚ0kmƽnGp$r4sDGx hIv wGx{L)[D|zWGpYH|R SpHGXG%#d BT1| (bFdIwDIȗlGIt 2țLǷG4G  E$2`0ʩ8x<@!Ŀh#~XG$,Q J dx`XIh" k4rN=hM7 Uİ- ~P9 Hx0<~| kW?XĈÌ$cXJJDqX: $A >9D@< ӊRHP'c¶r<>?P@%FA1n(;|؆SSXްGI ԇDͻ=#UƛT=PU:ָb@:IBFhrmBdeG!,Iv7H*\ȰÇ!0-Yċ3jȱǏ CIɓ(S\ɲ˗0 "͛8sɳO ѣH_:KʴӧPM쟳i&ׯ9y ٳhkԐEݻ [߿0as+^Fa8fL/U XWbcMӨS% &h…Kͻ0iRBqIk -{(QlE8֫O)kIyIU*T%( G(C?4B@݀ aEaJ)J}@*(h,xb((@/Ȥ! h+@2.W(L8A )$T #^K0Z|RA _c$AߕxJ*A0etr#^@a !)jq!|u6#Jh] - ?f0`F( 7r` (ҢK;(&6F+ ,Bf'!Daªܶm]B'jY[V& vh2&6Gܢ(Wlq`HX3NXЈHl]Y.a0Pah)(s*_l$#3&Pֳ`&LH(=jdmvYVjKoID#Ӊsـ~(n_}#h%p1wιKUx(J@;*2.ܸ玻ow(|]67N 2<] }hbI=LIn*>b -|I&#D@{\CAF@Կdk H VP)@:pG!{H1K$4 W$:L\0 ^gHbPHH$&6XNbPhp zXDeI`iH&2R ecIEhPD߉ ?221x @g25b ? ~H%@ 9~&I!@`NBJ d(ɒgB&12E*N@tˍc@dV MΤЌB ax$)nzSg <kċ(4yɁgB?SBg-bˁa2 @K(0  Zds@'JQc]XH"gs-HAʊ "걡3i 8ͩpT(xGx -hӦF6PTiTxQ ˂H`MT հ$7i4\{UZxvڒ( τ)&iD(b؛V$0(g~6Ԗk Z]걠m2ڦ5m$G[N l]2FUZ scG]œ-ml0׹m2T`%&L1A0 iAjhs+R( vZF `@˵dW.7@p|kpp%!Cqp;NN(jh19 @ȝSfV4}"b_:PZ4g#ȵ@9C$׉A`Z E-d 3`C p$"@;QdLv\F] l]M-pS9Ϟ Dw_ !#Naf!q,M9WȥZĀ^:"=<D|-L`<hF,~;~$VQI-'@RJ A:&dc0W7؃) gCn a`+@B"7ED ߸2 5:A'^G$aӸXFq*Mk l f:W Ps/ 8 t5Jsވcq Ԩ *zp(5 ` T%*)WF P %HHىXEѤLQ0Zt|1`:Q@ E  Zz e!:q S@ ;jJ!&e @H꺯UUZC@Sq7 [ + ZXڰ.2 ى1;a˱* p` f6{"=4PQJ?BE GIKSMOQSUWYk[aA= OC_#qo*DDz+r8a?㔑e{@ȴ=Q{۹;b61˺/.!2鐻Zk  Aa aż;Ы ӛ+'q {RbԞeAG!Ln>8cn(5KeNQϐ +.A~ RҴ= ƾ=1- /(`ݜ.2~/?1N8g9`ap A]Paƒ >QD-^ĘQF=~RHqWRJ-]S̑'!GpB=}TP}Ø.,UTU/>xtdxUؖƑE ҬYum=7OR)śW^Nf v۷"Ydʕ> ^Û*fl6?+zyݿo~ɳ".ݶ"0@wo@ `J49 xB /D@ 7rp9*R7D1EapEtp'}F+x4/pHh 4K|BpP :K;ܡvhFF'.gd#s >7'6YEbLJzAqEe?5c9HR=exYE<4_!9Єl(XT` 7 `O'XlCacR [4DA| :$ ,3%S"(v;*'YZdl`j :CFٙYA v clY뷾-F.Hzkʞֵk $I s[qlc[!z݃$ 9\ዃ"󩢕*iwA=|3X9A\T"S*Ѻ"݄jm#W ``f0 0k^ >uE&9 .VռG)~dTfmH&B ׿#^&Z@XW-AL` (K@ee#'fTeC5N԰A"Q!rM4SRIENMC(ِXM[9~ %RAiJ1j^kH>)l>2i|&Kwd&J F`R삾Dta{.S&m:emvc9 )Q/ !Hu۽vN O6|X*mLζ7w9IuDJX&8۰톈8DRB!Ȼ]% E3LjqR|缦xCo|\Gtꜩz 4q 5"%эuo}-zQog#|\| s*&H.wg-d=CmPcazћq=`B"B{ᜬʴ@tHEBE+r忚o~ʬu9ʕP!@1 ~0zbeOW4 ia$ a +U:myc ġR3 !p!pX[h~8$, y$wp8p (J6S  ;jp i  R–U) ı ÂA , ɪ L7xHhËc24 BA@9 # ;اc9?TA Rdyx;DTD 27p'O, $6Z[ :LP ~xBqūX31HɿY7š5(CưgCiƨKk8 mEdٕ(|Gi⹎0y (1 WvȆR@H> ȁ "p0PԼdHiǨЇcH2hH IXmErnRy14̋mI8vP x!ͱʙʢ I?xj|$Lhb2<ҘJȆd؆>zxA)#6|Dʫp !8 T48 ěIz4'ɼ0 |E xkܣg8@Lӝ 7pa$Fj"6p1 +MԨ`ԵvYȉ UlbΩ(Gx(h8F´dhSb%l/qI e$IVdHҔ4VŸ,m`1ؖqJ1.jƚAgs` /1ڙj:ÈmP,ypa):kghfckhV Ñ6Hx .ȶ6}種Qmag%uNHޣ6AMMbh׾v&a؆gti.uf_24iYDTIcpnۆ.;yoPo1eyoZp3h, F1g2ѣ\f HH}l b𕀊d. p 'q٠6.}l[qXjq܉p ߑȉ, !q:^򬑇v}2x7L#uy'=Ias›n/'v/w & iN9PxQ E `dHpAl)ԨRRE*֬ZrYÃضǏ@$S-ܸrҭk.޼s r46DhqĊ3n1ȌxM<qf;>~IO2ԪWnx0^;4a/nx~-CL:;CU;tT}ijމ >_+PSLÐ+l0~ <J8a]`.?DӀKQ8bj1Ɛ"-0lLCF'.<˵B>y$cMˆ$\CYc-4ЁS#?e9&FCL<و!4"]c]Ms2L] JMliHkXM K(wCT1h(< W$3*bCfaʅYTS?3Y ;]:(벱lF `8*0t[G.ynua:t@h yX:T$pXinRe0AՊHR 3c-dғm nc0yc-5],׾N׮ _UiWMcZv NƯP;8ȵi2.IdhdնK#nlTv-oX76`$-~j6HfH.qq8W}% E|&*eG$ HPul>H9'e<,ĽnRU%I9C!"O`H0!8:Z<34Ń0:#br F *R=DC$,"ގ4"`:bX5yt!QawC,#Cmt|%x,$=P@ݝÀTDA$]*XVE`EAGD @1D1DPD LLl,1XO\iDA,AA'`֝8pa\Bă4$Ax1A5 ?0,2DN$S6`C}<b8X> ]S0Hm<`2E!P [1Cׁ[ȃ84"%&FZ%n"'v'~"(("))"**"++",Ƣ,"-֢-".."//"00#11#2&2.#363>#4F4N#5V5^2@D.B8 6"H-#:#)A9 2Ђ.Z3X`*)*ꨫbꩨ:haAZha 뮼"ʯ(I BQ<(V)f-0(:O<" )b+ն  bbj*غ+D'|1a QhA[g Lr(`#, (0,s̖;'fE @2*}H)@'L Em5Q8qA2JWDc4A?LP@(m7WYwONpwX=.}F[M5"~wbt 圿@42w"b騛v:o`2۟hI(@.|mȇJ+!P(ԞFTO=e~&\EhBPTo}[oAx_}Lc  y3 !RC@fF`A Ѕ08!(Ȑ]1@? HD( C BÁ ED`CHa.H2.E QD"aąHǞ(B nb "ācIȘ \أ-$m,$7I ? (>L*5 1l)WIM EBE7AtCYbnr4@F'1җx@hjFr4.tU rIA.q0zn~3$t"8rS gIdE.I( "JzSh !~RWL.x!Q;Q: # WьӑFr1АFG\AN-4T-,Pӫf\!iO3ɍJ W.-*xC41C2弪5F6؈ y$fBZɊ0xqع$W*V֔\ʟUfdAZ@M h-Ua`U,FeֽDȀG'wkՑZI!!YvB$Mpz r@.c$nEe0z rAEJA 80E8 HY@xY⫁SA ,Xa,.i@ 6'!_r ˉ+LQW d0"2`>+@V!\xKz wB2^#"di^+$!S +J@6 ʅ-b4#W/AL`B)Rс>!D-ElYf16GD,{fn 72B Q-"a98@0\&GÏ0WDEPG `{ol @0,@fkJ @819:"#{Cc@BP!Pa.@IV acaH!x-H`Z lEDȱ| &@D rAB[ӺEҵ Tt@ / q5@8VIT uT(YB @!X@ H/X j01_M 6rMnQGMup]MH^Jk[3H½.-Pܑh&Bw`X@Jd0@ ox " ad6YwD O Du+6Xx -.0c>0Ö a4 &HO0 'P`B fg$a{`~!;@ q ʰ VcTW_E] uUy`Sq'[Xm6 (,Ye 5mXQ TD\c`lȐR6h\{hSʼnc t W^vxR8 m+S 0 H Vzʼn16! : 0*Rih#QmX=%,hE J'XN^xWuM[`%PrHSfh8,%U Q>McrԌyI7{ 1pwFc9(tr`nx٘3V # ~9SI 5_IL876%yW Ě,+*%y[ Ib֙c p<7N)扝Q Fs&)yi 1 7Dq ʞq&iA*1 h ʡ":$Z&z(*,ڢ.02:4Z6j'7'AqQ! >:>z29915!4AN*P VOڤ\^]JGģfJ-@ ϓgIa%U3:QUSzZ9X97u !JJa8Z: *aڨ%qVEJZq#*B*!Zzګ:az瑥J0 @!ƺfڬԚ!:alʭ9gxEj]0GԮB^ʣY06!\Jd7y ;twκ;x${l1`(۲.02;4[6{8:<۳Q?>U%F;@ʪGZLPR;T[V{XZ\۵^`b;d[f{h%n+c Sx{,~;[{۸[虜gJ`VM%3ee V  ڹ[5 "㰶ʺZ@7 -֬[t5zl{K$;sC;8+$@c1{-{۽#;[k蛾AR쫱&{ۿ<\| du;|VZ{JưxК \_'o;4`[46ý ĵ 1L4ʭ:}fwĴ TV|Z,]Lz;fwf|)5pL-s,*yᠺ)}<'b\!zą%:$ܣl`L7{Ǯ%k_;š` acP@J,@:Í'm0{<``$AQa= _9Zw ʿ$=8!"v L0 ABAxLČ %ܮ9|n6{& DE@P^6CT0iΓ( DTфG7Q~UXX@1'%)M^x0e<`ZclMV KATC1fqN\R,Ik]@pсգ\}-{Ӵm}p::^T=!UW_A4LD }1$gm@/X]ۇq=ݲB}`F.oUz?Aɐ u! ގ̥-4-򠿽ߧD1] ְa V+{. Cڋή0`Hk'.hu m0Mdm\EnFրK]<mm".GS0TR;++m[L<~o~ ᰲtm<8 ڀ5!:}~NU @~aLzzį<|+_(Knȵ~ql M WKn h`j.>qy 3^}nqqMv1 > {$#|^Kmpq 0q %}A&z +'ڽSLwvv00h j=0`ЯS^=wQ?k4a.Y"-uu[]m1 l!H9LLgs "ZQC v\Fo{\ԮQm%bט'/ukjE+׀NoP߄_]6leu_֎~`V}<0 }r{ܚq3!.g D 8 >QD-^ĘQF=~|(O6j%J-9h,\ęSN-F (OE/~2RM>1%vQ^-?]^H)_~ԔZmjpByon0Ax|' bƍ␝WJ!<է#KHPІoX/K$#sEH EϞA1Vֲ!O;דBqmh˓ֵxUY-RÆP]z~ǧR}[*mfb-ň[[6׹EB{A""AdyoxoUvp.6|0x;_׾vmzJa@FoL8k BVȁٌk 2Ywnpu4Y"A]zŠ)iDq6%{/2xW2#00 7ٹ2-BgZR#M->elgފ!e0 嬮9M)E=0lOr3 Xϡcab&$dYcҕ|iC]͜AFWԧ$H}Ҩ_&zLVMښG˘F}ɺvOJi|i-gg,ͺ&ζn񏐆JQ{ls+vt`ۢVIs-%*l6YS?*DPVa*釈kBI@7ӻIB8.&+BXI>l !\C5= ?IA +CC` C@]5WC`CJ5,=BW +dFhH Bd HDCA76IQCb%@@Pb3Dɔ 1td|tVIhh.,~4s HyDjT^KwS R y >IiEIo4+3oyˏ2KbF{c!q0ȼL6@+9qE \;Ik өVŃȚ w v@njLTi eI8Jc٤͛G dQty"I@I@ eSDKNX82p[ I@i0t  N){cᇹ(MV$g؆p0Q̌hu|㳠ᔔ mANLDP<3ɺ7KkΏP C>7I chD M `hDC(+H;}M8)P c[s;TRܼjt} 2aNyPT"cnُ9Lӕ[SPa8 ɘ$ 2ȸSAC]S ^,ȃקUl [PYv#v-aW)fa@PW i]6& 6X5n`u01~tJRbJ*VAo\; 歸HQgĈ4'7GWgw7 T7FHp _ú8UZk w1X+A*c*ذb7Fv60# lT nxft[YP `2X%H0p*8&w.u <`T8|T:"+XX>/:sʠ+қxXعx@x ?-3w@yF1g >h04m诠bPXp?hAʆl0Zm0!1=u)Wq^?v:q)uq#Үl),x_ |}~'7GWfxHǰ18775 axyC϶hG^ yydb-HYy[k;WQ!,Jn.H*\ȰÇ#JHŋ3jȱǏ CI(K\ɲ˗0cʜI͛8sdzΟ@ JѣH*&.8qKjʵׯ`ÊE5p]K طpʝKݣepjkw Lc{Rň#KL^WLb ӨS^(׌!0Ժ Gxz  ^RNo5_h/ӫ_^IA^Ȝ˾H3:-80E,<Vhᅫa1 50+8!,s#c0 <\:yG,DiP$K1x/  -\v\h!ҋ0 3bz)t։TR#3J~X @#v&F")h5ZXE\*j(I Q`(j뭸 ʮ:(h:O& [7Ϛ;Woal !ep#0FjE&z~_҂B_L)pi k.Nrc=&j1Q,7?oX E&dQHqE n?@XF BtʃVH\90yJ\K~I. ( P/BTbה-><@"[, ]ra#ͯ] cP9,:*7XK+p"TpB3rЄTYE01TH h}Ey* DGHg Ȋ ҍ" A}/er:P.h[ bF-V= H!¬`"^'¼;[\^/ tz!y8/YCX Cz@EX%qKv+0vb  S j/cfS0Erh0 2awzq(EDīh0ou^(F"Z2$ /6] 6H-> e$յmSڰȽYZ9HxyB*!ٞ PFCScA ϻ>X>J bGȟR`9D|`Bw{| 'vAMg}3j N?ʀƐ}6N7}'$\T%1mO %8?(h)C-gTK& $!\Mg@/8J `H8YAL؄N(R\5B Z(] 68v;:@Q9DFzJLڤNPR:TZVzXZ\ڥ^`b:dZX9iHѹ   ShYz::P1Z( p*@z 4 CX˰ zTzSQN+Ơj'XaڄQjtڬRh1)y ~i0jf @Tɬ0wPͰi1 ֠^Jc@V/1 ;i*: XѱؗԐڗ/W`@qn /+:n۳)~p 0>Jp *Nѝ_^@W'ů9ciInű`pљ*@Vல4F)e 7e_H;d 1b+= +13!|Z>Kؠ#$ vîJ+k=SwE1gE<[X % k=ki P{aq\BuED¿p`r/2g0 A H"lV+XH$ f a)L',Rڹ)6>@5%vZ <W!tJ K`Z ;uq[K<뾓j @PtqPZWL |b{-0KY"Dv}\!P`0f@,Ɍ\## ,Cnct\![D  ɲ:hǩ+<߱ŗɓ!ELT14; A_]+La-kE6Dր` :k C$kye-3ХD3g;03C> B$m #pK7- $5 |QZ H3.%16ͤO}sB#BA-] Za_a0a,jk[֤PA@) pͬu\=.ړY$P~ `0t5 h1FP ͫ @˷t;[ Lj EK;RR}@ ?B@D)` m 9zҖ ܁myCA=@B@>F03ZdbҗA=gLD}k4` mPw@- *dFrd ӁaDDԸ1ڀB%0 ~<PM!f 0 -1`!"ğI54`=K^!ٮ\ +7ؠH=TeՠsB6ZPH>AGB3Hrg> qP`W0yA"ʝ?9 ^a` fMhA~,AhN:馎5G 2p f1 !#  70~CE.?H? p1y}1@f@ n֚. =  v!9~A U^ _D˗A֞oa@J@`@ _ &4?Ad55(/]E?c:EYq@V%[4tQLA8wF` Z[ϞEdg ko#>D@J8?COGA^cEQ;4a^᝝D.ِ Xb"nw ۰,.>A-` .?>L]PxAz')?yT3JoiOAdȏ@H)p!QD-^ĘQF=~RH%MDRJ-]SL$=}TPEETҠ?y0@βTV]6We͞E'<*daZu o,^}%tuFXbƍ?>KDpZ fΝS@?q"Z\†Yƭ:\8\8GnG8 ]|҉v㗒7"еmMǟ ĉ?>nZqq&"cgߵG?'v;D Hǿ%ɸ6nx4 T$BwI@j0ЂY`[b (L1Bb/WuЄAUTRV0p@4Dp#G\C5<?aC]@D"D\$ .4"!c%5 2K$" 'NJ;1\(pTY?1cG\K]FY#|cG/'"/~(\#wx %$cj6hqJA8/҅Ә+= ჰIm$m X4; <ŪihI3_Hf m aH#56؈f1Hd4SKk^Mܙ Q4$0.rAc:d%fgJ *&(+&C%%mg:ԁ(uiZ- E;M_K<<$II` 0[Y䁍t[09B0Av ְQv?S2hL~jcajUӷ6XJ|'i…ӏt2cзzeIV;ȐN241#bQş\dV1"D7" M]/bCG! v͸tS`]'DX [ޢG03 0ָrQLk1xiSjKjD[*ޭV.Ҧ1fpvcnUv<#M/^V왲 e!Bҟu+B`M3DgQ#:Oe\f-"^Wd,R  _%m8D$t=CgMwzЅސ,xfOz)M7N;ի@=!QַuUZ׿ά=lbHa/φvەliWͶvmnKvh4]BFw}oݬ#=oz]i-xxOo]6xp!>qW<xƭ]h}E>r%GJa pe>s.nZ~s#1yssG]wt7MKΟ>uWב(Вg,&znbEmz(y3B >)ف#OWB-C)ВBwLpC,&e>1. 8._ #(a .ɐ{ +Q*_f, Yre,$C-l򇈨HhAԒC<"l?ox(?f#%Ip,'oD`G]MF Cџ~Pґ?Bi`eLBXmF( ;g<a@I軲XEjp+yw 7x X)@5R#6;*#?+ D%, %d'()*+,-./01$243D4T5d6t789:;<=>?@A$B4CDDFDt>cD($qG9J:uHrEDiHDCYN r*ŗcSE[?I@)@_~:`[5-"܈Zp Q0X @dpV p-ƐHkʱAXHXi@-8G~@w4p<@Va8>@uС  HCh~ȇ7XOTHb KXƈkXmxhDjDo*NX3pH#}G~80/(h՛?J`S@0 'QDqBtbK $Iʧ"(D*0&"J*+j,¤Da#r)k%;착 À|vK߲>bBϠ$xž*8$I*kWFo2%Ih('5)B&Xj,U ,}LʫαR)83V4A!%C7V~~ Xg,Qp-W{T %Ll;v-7OUJh2t{~&xF)7n0,`2P%Psk$zw标~,%["yP%._AbH!I!NZAB@ⶰC >#/"9/@%rk5 S+G8E4Z8kl1 QRį @A ĹF7$(Fwyb)A"pC[(H0 ]k@NZ G q@h,:B B3ȡ%` cH4MUL.”=\jJ(P EOscHAo X;'Hľ@ \-Rqd YRpG\&( y3Dr02"5yӭ*'#8Xhh?B |3%(訍xAa;q XKȍ#QHI^a:9Yyّ "9$HLȲMFqK:9!8A7a654$#"!!A 㒛;:987!6A5a4$#";!6qc A8765!4A$a#"!k~;:3j9a=)J٘[+fQر탙O)5ə9Yyٚ9YY 0)"Gٚ9Qw6̙`GO9ہYY׹U#ɝ9 T9О)I0ٟ:Zz ڠ:Zzڡ ":$Z&z(*,ڢ.02:6z<ڣ>:cIHJLڤNPR:TZ.VX)۔{3 HD(ri! jJh)ejpJ(]jwJiʧy`c )ZJz~٨y*≨jI䙩:Z2yڪ:Zzګz9P )  ې:@*^Jy* zZ꺮j;ڮ bA *HZe!Zưk,[l bP $ʰ1Q1 @J@X,[ 3.UFjkKȔ h0y`gvf yfh_ɳU(p)  $ʞ:Y+ٞɐ AKa@q f1X0! (I (@ 9&6ж  gٸR}Tk.(7;*^mL[֫`G%])ܺy;] gYpMi K{:ջk۪{ 2R&T"[%替S2UBJZX 1Xƀ2<n&KP;1G KƱb hI|m0Ӏ 1qTȄǁ`%0+ `1!@Aʗ! !p,,Aȗ´#AׄhmŌl=JTj lLщqjl=*7+ְ[SAO+5P 5IΞ1 ߙ¬6t? 5]lJP\,7|PA Hн`;rBSadͪ|-DG QHZZS*_<L=4@TC8CҭCp?` N ##0A%MLƽ0`pQ0bx*5(-"ޣd >3AȰaH]EPp"D9 0S>]GD .09 7qޖ<j` FHi救 ?=ݛCldl>OUWM ǯLTU鎞DB U1NH_ |q%}2u1Q^.{ B o1Hr ;2گ `eNb0"QN"^&?i* d9T ɨ ? ރc!OD?54D84` + Zv7OqT Pu K `?qt`d1?S/(p5ZO(SA1 6n:COr@j}3$7ny_ UOE@qaԐ8o=~ߠ[ଛ?;3?lm^sUB3w _Cn B! Evrs>=mtQ`6r/7Eo>5TpZl 8c?v> M5$BE RqPBʓ7D-^ĘQF=~RH%MDRJ-]S̄C~  qBqU4EETҒcUTU#͝zh1˜Y@FpUZmݞtV\uԪS'N0 qA FXbƍ?V3!=TN-]=Zhҥ7aرʚ[6~^[n81K߂F\9pJ[]t_'ۜL] l]x9/.\ kfɿ/{u߿ !^5.0$@7(O=0B 'L첄( ,ڣC?1D\?$#0WdE; ${FѧΦ*Az=2IIH蔄Ao82FGK1߳.+D3}z2M7L242Jߔ ;< ;i E; @eCtQGGpEE\hmŨz&!G SU?tN 3JKrNMWOg1ؑGQڬWcJ!CYRgWxU*c+1MTX}H*k'[ݻro/bR Jmv} 0hf%*i[+T&v}Y`ۛstݓem gq٧oN*ٝu?M3bQJ܄ !紙fڥ:e7jI0L^[&{&6Iz:!c3kצҪnSQp;B\TmEblt0rZS]ȩ+GylGoHg͎¤mƽxvwOC±G—^WDPi{jƿR{Ϸ[q}רOt?|g wc~k H?rj; '6Ђv "C70qY %Dq h2t*GGH~&C0EZe'UUtmؑm;cS2,[\¦+u´V4gKn P׺LVU {q Awipm_A،# 4:v?1=h Nl⬞w0ՆsIfH7jR ` _X)0bBfլX13m(9}x'b$6V:V`!޽\<6FU̬αa]AbִrQ6*u8PrX?}y*\>4L6G!5͛DO~5o&jRԧR%iT3P]=kZzȪuSk]׿6dplbCFv 6ֱ=mjW d/EJv=_g玒nvv6-"u7ao~x}p7K-rp#E5b qh1u8,&~rCFoyq\1?9msG<[pqkѕnb%_zԗbJ4_~u$];M KQQ @d[D"vC%[FaU#5R^ad.PϐpzZ/ Ehy҇wHK@vfS"P]Cb #gф<WbGU i&2;%CtBp4*,}F-Knb21Ff%X1):lU```5p?x1 a>Z~;Ӈ0)"jj2i kTPRLHEha Cgxfhɛ@p:[P$@ &/d6zu Ԛ 7è8:;Ԉ# Dp@4D@CTEdFtGHIJKLMNOPQ$R4!STUdVtWXYZ[Jō]8q^ņFb,~ۆB6LFf[IPƅ)XX[ 00ԣFr;而fu2 qh醄F#G;È:… pkÆs'HBG oH-~d,\U9-aBu`ɇ%!k ”  }$GOlGDAKlC(0&}Ѓ0`4 ₿mx/Ra…H4{47K1u4D̀3$4DTdtׄl"ڴGg.$)_8<3`\6 CZT05D̈!,Hp1H*\ȰÇ#JHŋ3jȱǏ CI (S,ɲ˗0cʜI͛8s \>y JѣH*]ʴiG):JիXj"Е]ÊKٳh[˖m>9c \˷߿2LpOGKL˘̹] XS^ͺkmc(N~Q xq]bŖ\qKNuβū3[ҁĜVjȮ_Ͼ{˟O?j=b (c&h2ňN# A/Vhˆ"@/  .0(cV\h8rF" 23hb̨L6?PF)K1Έ@: |di&G@l (L1ל$?f,(g矀ʠ@蠤HJ,c y)ivꩅ*J,8@~j뭸wJꫯ0J# (p%sXb(lF+jmBDa# ARk춻n& Bf(sΆ"@ oh2 Q2#`ꆉG aiA Z=,@ zD# @?J2\ ;x0-@tXBHLXV8ÁtJQś`B \ HÁ\hLG ;  -xt<4B) DQ.%!n'HD!Nn$HF'EE/\DnXŪ i" Rs!O)\\cfZt6`/huqR\gTw@`NZU Ɛ.,x!X4HY3G1+LbY):uYa,`M6J ňIq#6Z $;*-n^⺪vNlpMb΋hԜA!pb FOm LS$ W(@8p#y@ NaFly"4`z!2tC=ɺke08$.j / ?<+A`A^BU1pCs \. @ "k!ZPZ 1fҠ/Ӈxl  *#@ 6PBPz !@%"_?R Dd'`CF0`A2d1b\hIL r :)( #εT/H(&$(1T W\fsD s0RZ4NG1@!D=m ;`3Cl _$b 0Mډ,T`DaB%L0`wW !V@$aS;}"ॅosq:ūh; oA>n< B{@2Q `Bm<<73Yx>Z4( n/>V|@DG ؗ;6M+t 1GB*"lDҽB SPeG>0W`Nlv7y#{ ){' &x(-!cDe(|E' wF pQe :W\'7hGB:@K! u8{'q&L^àhUS0H:0`s mQ Qb 9KzPqpO:6rY P BhRp7W wWjP Q\w4S!P ^s#sPN)!w 0uA7V (8! 'V6OS pFnZ e`&pv(LaLg|ᖉf#H0bU6U M>S W >U 5-HXAMMs 9 fm$W)8H!rP\PP]P  q0 EWZXĐW1pziJz@   a s ` "Im pJhh@ @ ] ]vd VEZAw [U`capria#h xWXJ] Ya%^)9 >U]UP icjyspIS 2gВ<^d7LDk:xZYҗEH2 re + (V9'jxTt!]E v$'4 D%RQ ൙1 G j JeQ _ >,QSwYY_` )P QIs٠p s9 1 p*B9^5A"APc9M`S^@ /#: Q"z8::$* b8tY>*=MO, B:-_iUFg?`Zb:Q"A"Vz3ty24A3aJx5!T㦳12V01*@0"Wx Ui.ک?IqUS!9u|JoJ&tb95%1!\s:7ĹZӡJ I?ךDѭb L꭭!S&&1.4&J::*&kҮ=[N SDZ۱ ";$[Mb"') +-/13[5K7;9%HF@CEGIK{MOk1W;^\;ac Uk`Ja f%;qC`r aF}+ 9 J[뷌;[+@d 𮟀?6 P> ;Ṡ+ P:;{=k;+ kK; K‹̛{ ++; # '3C= C+"9;K!aA%{  , $T\ $:3ù% PB5A7,9\;$˥AC= feیlkr}q]Lx}ѽ|̟ ؃]؆=w·M؊̜؂ؒ]ٖ}ٜ٘ٚٞ٠ڢ͵ O eԪ=w TE PwR]|:\ܣm>p!m=Э=9mLw b]ިM9b<ܶ}I=9P!^u%@ܵ` J N`ӷ$ FE #,>-wHH$-rtuc$-xI)$쳒_@aưr@>#LҍGM~!S UF`M>K` T pm\.qOGg{.QNfnsO0@̗'дt h峎^N`TKÑ+ lP!S n^O:]}#L.C^0gTpR^!c^:>,c@ אPL1q X BߡSNQ|G|7߾ fpp0 dN~?P%FM 5! '?n1uSw ;./1d|\αUTtBϴ6 e5&:VOQ`!gHi?`gߞxSO]0u TJwoRyή0 }D`IekF;pj`~\)VMI>f,J_tpu^tXxOXaDmݾW܋[q[2Txf%'vGgGBCRI%+I%ȊLx፥ACdQUUW[J)Տ"J121Ȑsət Keٍ.gWgEԒ4ͫLi N0g<iZ\uVu;΂B<]7,x7`NMq Ȍ_Q" 91s 9d32i4ys&W'xdo&wQH Yv6$ӥ-(6K4ja3҇/ƚav"^)k<)HP 9?pχxD,<hwߺJK#7ソ~d>HEpA ^Jٿ7(OfU=C@/ 03 =ol0@M+ܣÈP$ӫ\( Y@A{AU@ WQ B⨕&|*< 3Hd »8 C?{3',N,6$8‚HB9y9@>,칠S9 DbpJD5|DXJđ;<<@+ E32BCRtE¢{,:1#3=Ø[̋E[FA˃.ClbTTfd;ADhTZ kd3ӣOfq 8⃺"T-\Eۡ#o$qDQeX1NH٤ ;Li2ѐ̺,oå"iTN.L$qPI)x nxl ( O`*ļNabȤhc8%~QO D̫4Pi~ts $ dDH㈖`ά;Cúh }c?3-9Rv /؛Mw)n)1S$U C̷DpA#ҕhH#25LD5@:BKPH ʰ!>tRбxH%ӨdyITv;UGT. pU #D̑'8JK[%T%&e=^]Q5"j՚L$d͎A}Nl/v0TQDنg3V%-O=Eu)0\ dV-D- uMWB&}X{jJ͒1Չ To4SuSX JgT[ІgОFHI.EUDSSĿ|( 5Y %[]C}Uh?êuK] l<lH!%۷(#SxEa 4۹v]Us\ָSKHE&mGf㪔;4}Qӈ Co]٬PiltPQ.q߈0e%%PVt0\ %JXuPD[Qqm`|/ޓHkPPd^鑆u&/a;.,CE { .Y8ۚ51zV`p$8&f!O1b@ @iI@y1Ѹ0/=}_ 8J}$XxYې۽b Iyx 6pm^^yߡ!qjnfx8!^M!Vfxpgtm6p(0S 97i> %yf'la͂X+c;@vqX#5'r>Tr.ځSx$%''S)Wr*<r.&,/ )1D]x4w7/qe*8w5os:@m>tBGD_eBWt>FH]F~Jm-t轢=Ow &Qu&SZsU_#׃ufZ/kYOu\hVwu^hYu`['v;vLVeGg隅WwovL4PV𤠄Tա B@@y;>=FNޑ;3 gv:PN }gs@2k<W`pwAH茥m_fq :ZiNy2H` Ƞ0h~RP -pZi|h`R؛CȆqo xY@P `xGPypа 98X8g;Y0OQ q؟<xXt)OHˊ7>h „ 2l(pе "ƌ7rذ. 0#ʔ*Wl%̘ǝR}6wi0]l5}UHhG~-*M:RRj*֬Zribkвm-ܸX kcAqw/.l0Ċ3n1Ȓ0hk2̚7sϢG.mtis]F5ltO-qbs;hA×3od~:ڷs+Ǔ/o<F=X\`2oz>@2' * a4318!ZxV'>\!!2qI-"3]M9긣~ ANx$Ib7 SI>O<pH}Jj%>k d@ey&f$#dN춏5?y'y 068Y*$r:(J:)Zz)j)z):* Z*y*nPz&W+Z^? =ͯ; {,BuZ$w-݊€ EVz{.$(D&{oGiS2&$&B@AT`P+01/ kTg`<0 dPf8<2 r l ۱I* d|:LaPŔ<4*ћ Au6"D{E*raxPH+q 7 R(&X5 5ȈsɲP%ڜ AdDAhTE{^|Q69B4ʣ3xEem6O:=8ëF83cJF^BzzPm3:4@#ӤF߸)j?ueA1#JN>?y??(< 2| #( R 3 r`c$v* $PFP 8(BK/JSLaLfP?(Cbp!,f4<,L2JM8cC ?0G1`h ˎPVi%d'8Ȥ !ʅВD7PpBJ*s&* e&eˡ ^ss()":蟔.@-ċ-AxgxJʭJ(K13 RY- -n:kh@"*j'BR(@9JVi/ K/)`Gݒ0ɞ" haAZ! S(,P1 B'G .'/a40A޾r ,( ĂL|Xbə)+©HbT5JJT2(X8QpH4KO>j-Wb@\м1ċf@ -ņBnzqnyTq>% k(&3&09+ %UJBgI!RU(7<„0D&t+Eܝ8B$i.:C~ 9=@|&bA MxZ yZ&B RBqv '&ѿ*ݎJ!LG6ъD`-Dኽy0  q  MY5aP*AР  J&TQ\ָƂqEDcXC ]."4"Y!TPH RO$G͒dJ@!Nf(0$G9L Bz 0$j8CN$H.y@ ΀,9Mc8 SPE Ž)8&Kb%'?n8ː4 4śĴ@Ld((1FJXB &H.pE> {3ܓN.Kd %e>]Oz 'b3 DtXhFQ0JwBѕ4",`G?'Z"q$T:b7Lԑ8P}hRGLªXVU kH$sh @Zg'ֶ"F3=UօѵxՉu _Mb:d'KZͬf7z hGKҚMjWֺlgKͭnw pKMr:ЍtKZͮvz0/v# 2&>,LIvf+0/n!5Mx f'5}*;Lጬz*)` -4Qۘ!(qe!ƊA\9!:ֱWB"ch|:$1 EE^HsL1f61Ӝb*k;Ue^8Y9p.> 8%5ivsi]Pwr'z!4ͼ1+z(4sheWVOM$I"kYӺ֮4i<'~6bbS([aA%6-|X#ErrA;H{%VIS2mږeY{1m7ےH,).AxKѶ{ -ܑ\xut\%o.Z9,KyAbr\+HO{"gp^6ovHYAG<nѳRoUuŽ[OI֩A}?eQ@Mݾ*;^w?OO;񐏼'O[ϼ7y )^ W\YC+$Ij/OOЗΕ rM4>oU*j )o 7ÅLO}?d_-`<}y@dմW7LʰAy  =0"𐂢؁y0%8pG(7#BQ.DaQ-ȁLy68U|,[Hy'68(G8Ia`^ @]cXRHm]0 +1a`؇%xGX{1/8x(ЉqXHB=0]dҀ0wq|ȋUM1P,Q=U0@0uU%3(phs>qXWڸхDQ0\WB`ъ+я?H@~iXdGq#'!5A!Xbx8)@>H$9U!,mP%573B[Љ?@HՉ8DQ)':qXٕBj^_)*4d9>!^ IuxГAqpɋ0iyP8Y@ه:x9} #l(k bhqШZIaQP)~M9=rɜ|}~98ig8`GYy؝ {ݙIЊ |*q :Xy찞ѓ*P鹡Gph ?# Qpg Hdhh0YXGBZFzHJZD @+¤Qg!VzX*~Zѷ^*~a|dP_6he|= X؎m?*vzSQ5uzǧ#qqb|qqJ5@&H68'$~9riaUZ: I?c  4P y𠪀3#mi,$kրZ  #7 (b'+0wtjh 0:Љ9:X)򪮮); 6xAzqApz0٣W>5BNe E^aL d pN]S`'W^\] Tn|b\B8L= !g]i>L} 8oe#k750h # ْqBhފ\nTY^˔~iz܇*PX4 E`A1$5k ݬPO^] qԄΥ~:ađ,]5[ՃG5ү` 'Nԫ!S¥ w&x1-g>'ԬjˇFYT6 t ^e`@!*O#gR9PG14P\GҟQD-^ĘQF=~RH%MDRD 5męSN=}TP HZ0VpU^ŚUV]~ժ吂I 0R .\`ݾW\uk,wX`…xC1>`aʕ-_ƜY3`.Y|6ڶMFZjٔikڵmƝ{ң17,nōG|k/@V`iխ_Ǟroek^xY*en0ݿ_yEKg;_~?$@tɱ֛ Q 'B OHSĹ0DG$1!zI( KdE#kJ8IwGb< U gD2dC,lcpQJ+Dm1zƔj ,$̿ 1kȒ1XL;ē+l <4PR3NGAeQ<[ M!GsRK/3^KSO?AKpA/CE5UU9[5VYg>Zo5W Cu6JCU(6Ye}3c6;AMLZm '[qѹ;rYgqhrTy-t}GyMMg'x WIad}X I߈)X*MdCpT9 T4ujyU`r悄U?/hUzߚ&iUmV:kh"VۯOj+ll }n5ouo:귖$~,kp.pyr<my\44?gs?ڛ"GÃ^!p9woS:2?Bڧ]kM i44ŏyF̂am\! gw ݴZu#} F73 *EJy FG#*APr*Dŭ^[<!“fꌂ5d/ᶱ މg rV$^at ; DDH"0EO%!2eQV2!P"3^ąmD;(daCF 2gp,Xx3:"ґJy9=ҒZ .ɂ/4B!'E ѰD%HFTRYMJZ:-~b-uRd9K`R$)Fϴ2"AӚv*% kxMnbiOSŸc@Iz,32*1; ҏu+ovMTvM 0 p!gP`ch6 F;/UgRmoc](@x8c$J̒13PiIQSyD L;ӟTh=-"֪^HgQJ;Ȝ e@uBM%.Hy ɀ8U!>plkWA}뻟`9rE#kWя-##eoʗ*X9Ճ!mhRЉn1$ sXT/0 \ ̩xdZ Y#-uY&3X.Zt$xҏm̕e7M2ivtaM߻C-)X2J+<`%\!mŢǺ%{<հ9b3 O3ʋtf2z7Gҕyj ]\ӭAE-6lԧLT3CPBJaJoBfqk^1ula+k];;6H-v L.pE ap6Q}SbJ?m@2%wu-7,;Ή=4!{v7o~fݵه2  Y(坚`1"Q;׿i3?x7# 얈#Mɣ+qx6BF 0v!ր}}Ocx,8H/CHѐ mt0k>x>؂|XQm2,XK cߚ6?8op[fZ@?NPX@&Bpp,(܄l"x>кXSR2PA70.+'` np‡-HB鿇@H3$8dHE %p/$=2C0x:>%(Ã&ÃX<즐[q(D>%k" @;?HB Ây"3e,$`H8m)5|fyHyȆZbɇla~0xpqGXܥ߁qx<00:2y,lx@EyDȄTȅdȆtȇȈȉȊ}HHa#/HgӺ~ IWMLɇiЇod[VIHz%~x2w)E5fHW8C5 9!ʮd&i0-5lb#"車!,Ct/H*\ȰÇ#JHŋ3jȱǏ CIR,ɲ˗09I͛8sɳCF@JC)4@dFJJի%i݊ׯx2سhӪ]˖ʕ)ʝq ˷߿$ L ^̸"K˘3kޜXCLj5(ͺɞ=FhX Vq]z)_@y\h/ij? SˆN\ˆG!MD9O8l򰁃'MZx`#P6h?2?,A^%- XpGoh( J[I(3"e0t(>DJ *+EX*PFTV(3pɀ e(*> -Dɐb$19gr\Nr)B)(B}jhDjBGZpy<3hhf "* Z*?@ )L$穪I)z J6j2$f0<N4H&Hgi"bjоj*NVmA* ςPa9ʟGn/@w/)D8 s hʺbÆu qKB8,,J*|)(=-&O2V$ٍb8| y(%B [ (f Lwu3D (3ܴp9<(aZDa8 @QH{e_-QrD= C8P%1܏b'>$ >b9cPLHAPEQ@0@.Ꮷ]1oGlN=ى c6тyo!':T %V^JDpQ2 Z'뀍 G(„L&`I0O ~qG0H pY4aHL"Ґ$P@N7 B6a8$C1GRO\d:~3Pแ%u 23 $.EVH` VXMxғnd&A1p @6VfD,e2,m2dDdqk*a{ńa0 Kr%H(XFwc2 K:ILm gA[R\gc)~Ğ KG\?yUNsu\TP,'FoILB(@ Q(MiIWD\ʐҔ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:a. [πi0zqd0v  hW> h "Yڶ/ -za _f!c 8m"/\HЕ.&k_6Mt] #"΁<>fi0jYWx| C\Bͯ:?^_A-/90s\` >r 1aQ&Ia2 [E1A⏴X ,.F^&1Hl7%=ZdXǴE a*Y +- 'hCEG,K&HL2hN6pL:xγ>πL\rkXu)KEю`ݖ m4tL4 tҠGMRO+NucҡV#扮_&̆BuW]޵a/)mĶUKP ?N7O*r;A0!>Moxa q1z$32XC NyDC fYR[<$p7qcP8GN\$o6; 1 !y6 ҏ\]hC٭oN{Dx@j~sUociYBhb:y~cGwt q  Xo Z7ۦ| qoXn.ׁ|3(*,؂.0FA2jh@7XkҠv &BxfVtzLjNX& pTgv[`b(7  ϠcfXK"ۀohg6xpQkm`\Pq|}}H}A1uJG8Pq (j~~:HcP q:Bdzr򐋨Wn6gXxHgy8G(f5 Xff8tfHfpd68.ffqhV%.(׎sC8197Y+ 7>֠ )](pp-熛s'1-}'`ېt~܇tؒ雷~69`ɠ vW דńA^灆 I1sN>Ȕ4KsUiuh ;5u _Wn` 2yh c, ⰋYqti@4p!C }y t}JR97I阝i@8h5si[` ¥ 5&6,"c {Xv eog6 XHU`emÙcTVgfKFVIfJi%™oealQs{E1T`@DEtqeIŠkYa 0g":$Z& 1 zop }['Y6"hj+U>igU8X;Ja]^RIEuW q |p i[:0< $Hc VjKrY񦧪pj'syn,UЊӠ joU7c@ǩvkXnY{!!qIVsXYyf)e^Zjbv〇'ѪZV5}1NJ)q*VZJڭ*Zx꺮ڮy7jW1|KjxVYگt cP[+Wd [W'! VHiœ{vIhhU0tpi5 ቲ.0P5<]SpFu8Ui,i ?["`D LI*ma Qrqګx#@ xtLg٦`@F`< `E}La {˵X%wyf]Ea _Sm! 0 V qU@U?51 :\08ygtᨚ"P%1r7X%=$ lz]Hź-1)F;H~+\5q|@P" p9y5[e1 r` p ƂinX[g_%ᐯ/]C@d@0{ԗP l”{[/Dxګa_ U< y_^ATDxJxìg?H,IMrq\?DX [zƜ$wåP rJx[ LUK!pĆTwb,dDL<=LŘT; ,,2ӵ"mg $$^!` c@]Kl,= /7=uഅ!Fj8SNWY>1T2_^-(=e~5 浲 MlME=o烒=.,х|><,W(Mו1\m]љ)<N+&-.-2~ެnPBݍ%"{"罽~"7fJ^lhӾnTǬqA' ")Tt~yTt^L`~qp ў欽m T.pD;8 B.[ߵ cjNFr1a ԋÀ8F$r;mpPu8*KΘpo QkfC{^q vPq[Eo^rU^ȸ 39J{i'~)ˆNO>qO4hsk g{?~A80ē, #x b\0ڞ̹d /ȧ},!qjfa빬nϢ^8JʠmS<? 1oҿv촎կQ/O[?sN`oC` aA]>QD-^ĘQF=~RH%MDRJ-]S 2SN=}TPEEZ0FpVU^ŚUV]~jM. ZmݾW\5ڥW^}V  FXbƋXdʕ-_Ɯq0D ieZhҥZ][lC6?$D3}4IapؤSFΐ-9SOA3&pgP ,'"~4DQK73/`z$~湴".TS+s.UǟS'bx U[JC!~n=R6YJUzۇfV/a6Acն[oo[qDž[rE# St߅!⥷^ YQ- yWX:ax9+g͌G&qC.9?[m" B'l,t_Quy.FafdR"ITZUQeE zA?y(l 8e'RAسG^ (wR&P *Y*F 8p2u&6 `b*PxâgL(F3?H iB#x>h@<'Ȭ,?H,!ç eBH9"V(?a-!]/"רHra{ @H#q A8AXKA"/ ;H&ћu1N ##I4D‰ ߐHh%a+}A)fl3N,Xnq`͇CH?䱍MWz[5! %i d̋D!>rXj(A!ҒF*yINvғe(E9JRҔDe*UJVҕe,e9KɌ`e.gKyRt/9L Ǵڶ1xD9͒5x7渦D!9f8% ӜϹj cg<9OzӞg>c$ % !,@t2H*\ȰÇ#"ŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳ'CyIѣH*]ʴS< իXjʵח@j׳hӪ]˶[BjKݻx˷߿CÈ+^̸ǐLrʓ/S̹ϠrMӡS@6yT˞M{?r0nL܊U;+⯊+_УK$ljPURɹ ?U_Ͼ=)U3@"WWZ#L@|oAh C .K*0'r_)\xB$a'"xX(PR\ ("x#($"& $aIK'dPF)[TiVrRaJMNihzFl^ #i)gsX蠄:&88($4~KiS4ޝ*'I(Έ! DIHANXtHLc䪫z8NnI(U6KAY(q1A8خ/5 J޶+ QK0(#Z a5  1ɪF@  A-+q`*%ЂK1?f,`F&#Y|R'FIŞTA(جPG u- l( fd?m 4@]Ipmƥ\v^M߀#'b#Nhy-`DBdGA@೦D v}M8)D-0"LI:Tˈв qJW9~@~l"J(ϖ&B/? `A8a B|X@M3)Zp=4o{'1f1<"KR N@%"ZZ(IDH\nBW $Z$ H JXƒ>Ih!Nȃ(GqrȄ~ @JЂMBІ:D'JъZ/0Zd #`G'#Mi]a X 4Қ:60 ,ĘM-! a_-r\!nZbp=w]GSӉnG+^L{Nj^w#oxݹ0%*E R`XH ΰ7{ GL(NW0gjָ%7fIg@L"HN;",D鑿'C4!@ `:yE P h $!*p#Hxs;҆!ȳ?24Z 2b} BJC8HQ)K{!):1~|6Pjհ<X y HCOă7{  H=cuȣ-yu6Hl&&ƪiy#G<^mX{܎.}v!Y7͛t*ZPMm:\iJBitǮ rm "TNC#.qtҘJ+6yG?n~<%$I.V\0H푧D @u0Fq/kI4s.hG@=S"u6z#/ȨBm:&e?򃃯]6F; ~w?wQw bÓo3Os^ΞG=?=zջ90@G2s48ZH#Gio $'ӣexSX6?.Hᱳs|wlo"cq f~Wx'q a 9Wk րs1{ g|G,؂$l.aW)4%W g&^@AilNvE)iņkww|"Kp-6 Vtt rfu" 1Lbhnunp)@qXvhLB`wHbcGH~O` 6 pb`'H؈&lXa逈cU5!*i?x8{H" 7' nH" h)qRRp0}yB{ʸZPpox>4'wa= x11A@(Xall0o=Hh+0 ~яA |Ng}PGpv-n')]jԠ|a8I6ma-ГCiD؍D7K76NrX*ZyJiAywq(Vx["C'fj)Y0 Qn- lّTtbiX@x{`YbWEiLj)6 䎪5mL8Ncic9 H `XS)Pi8YSIŜqX5x8)YѩR^Yys6蹞YyMؤPwYhMw{&yLXl9Pic` L iPfzu`DUOm ڞa4, rɞ%zPUP1p gg1i0&zH90LO/QUlN*VPXE\`)bZfzhjl}٦p !vzxzҰ4:[Z4`o6$B訐N9Y hΨSp*M N0PL〫Z؄ʫ7/)ا*p`fVw{&iU ɤv*ӄr?o 1C)@$_UzM#` L5@g*3FR:0` v%CkSS!*ᷠX-Cа,nˠ|)KWAm"eF {ϖX+&qa  *zf*†kUq-` P$Ai@ ! EjQI#}9Xd: `9WVfY4Ԥ * ,@+sK6s1 <#ӮKje;K? [K{ĺλL+; ҋԵL뒥qLۺƛ4;۾K+Ky K[|sTg t8kgJkp 0Bx o''‰%0\K6lF2< k8*@<ĖD\?B|JgFNoR7P\X eV`ldTsd|@6h.fncjƷ%1r<9pǪ- 4ǟA,!':H,|,0,,ȗ2 ؝,Űɑ$ = >܌ڪlʋɲ&\˸ \Rr˼ˆ#<{ykd ֐ዱ~DW6).9` vɐh HraǠX/ՇE0^V~XZKl[~Q~&PԹ_jNl.oqv{WMmxnzK>p Az .iSxඣx& 9׎| p6 Q ِf` s}(`㬾P  )]w QZp Q fnC 1>= |I 1 J> RW`L0 9 1q ,`6, ɀۨP!T`o9(4n Q: a 4[ntL 6Ow`jj⠃il z/OJuw0AJw!ok"Aj}7كJ|Ұ(#L?!1OҾSVL@Kq^O?KКR !,?r:H*\ȰÇ#*!ŋ3jȱǏ CIɓ(S\ɲe.cʜI͛8sɳO<(JѣH*]ʴiM@8JիXj݊B=JٳhӪ]Rj@z!& x˷_BʕVPd+^̸@KLJD& ϠCMӥS^ͺװc˞M+SL Nˤ-rq͋KN}$pݪXbqO|\L=@RݛתTPxu?@(`g A+`N\DТ y+)W)"*,H`"ހ3b+7 9RcC')!x{_"0(FRY Ld~ )\dZL*x/VI%Xbfq1"F34gB(Y rថ&bI9ʜuJI稤Z&0%$(}JzJզ2 ) )ߦhkq)gD),#JZYP-yJ@%< xޫѫ & A'/LT%QJn {+%2om ȈcNX"JĎx\70"HziAP(҈@Z`7,6D]2CL:hB@:AW$4GN"PM[ե R84@Ԏ@T-Z AI@ Aص&;eo 1 .O:0~d}0AR'ȕ|8Aͺd -"/ 8?VQЀ+dW@xd{A;5á=c/;J/׈v0$(T@D0-M6&';M7!;XT ]PCtLg aIqYJB3B` Y@W D(4 °/HE'6Dv .!H>H>=)E}d ^H /6SB Ub fH(1ʡq/LL"ć42E( .X`Z yNbɄ@ 9O b #gIʇOb! 8xD+cILPȄ& ̆x ݦOl-Z:$X ,yD:3Nl%2FvS f&DY XŠyv& ؟0ORͨI&!̎r@@ɐO<›Պ:WҖ3/%B, h 0$!) I^ ә2 $'T(UB;05kX1" X"ZVuxͫJXB".,~5Ыb[+\CH{pAll<ʲ hњMjWֺlgKͭnwpY=.p:ЍtKZͮvz xKMz|K&Equr?wI1R Npb ƾM:F8 Ha&ŐE4 b a'>1Wa0񇑲|x99@F\6c510d xH0l  B@$CNP;e(&r%hNUV  CD#80tP@@ bІ 5;G1@Â1- b;-:CQf4-VDc`F[̢:E;\&-|kIJǍƵ!f;.Q`qX:K#م1b}pV͔"RvX\yY-}~5jv&wQTK7b}p/$  CWci̢.fDžrg7y=>t.M\i3" қ7=mWBtzR^b$vӴNr_-4UQrʦxD;~'HEbqv{^^gIp+ǗFD/z>}3[Ɵ#?/{"GeDtٓ]m׾ yvz+}+ ZxЃ(wkR~k7QR~k՟>hA3BWDj/$%8w׀8Xx؁ "8$X&x0[*(-/284X6x8: ;@8 VD  2e GeӄPh_ A"FbW~!`aX VAxcdf4?@Bx+~H8ZaJxXaL5 h]01 hP00x-!00;@iQX' 8]0h((:1 0ʸɐ0@h(x0h 0X؋x0 p"qH؎​ـ2 ۀ(  PX`瘐ixYp9 ёА͑ j$9f2aS0 &13R:I[mZ'5bPCy[0 J4)P=k4Q2%0m yf% x*1t[@`0d^ jY+阴uSZ8(l- 2Ib(g$9#i"B()؛Yiyʹٜ;iڹ?ɝiipX}¸ 8 (HYXWɟj j`p@8 တh y qآ. 11 Iў15x踊D cj   !0ʋ`Lz J՜dH*H!H iaj"i GJmY:*vI*5` ARszp ڪDh`(;a$H8 a0MAZ^ SJ0 ):ں% ZFIzbWڦrƀuڮ+#d#f% j5N鐭& _4hA ؆ !- jrda$PięjI b3[5K7QvTE&>ا1KzʰFQ˧YѐU+VP?:Ѝ 0a+T )0  ymP1JJAD>8Z*_1 I QceYЋ+Х Z1;{xyK;{c ZwӰ i +[n8xu)J˽c'Q` k܎@ɺ놠Aǁe XHśn #;CАH@%]pbm8 ,#\˵]CZ[[_3¿ hA<[ P2<X;HN,A1 ;ΰu]52Y 9;U[!+"vxM⣪~ջۍ8n^&qY݀J QӖpɛԑ#A.(H`I`~f]N<$1O iCޏ!: iѰ%჎58ɮ.~*i*^ (]˜ё A{5"Dށ>.v áp>z黠*g!/v+@:ގ.0ܕ$Aan7.Y,&/.A9\hX~W}cw=؜ m ގSzE  u۰#opm 'n>//_R_7dc=_Fz E_x.EXd[<EZLO9΋ v h ]x@⪜"^4 y~ D]9)>Օ OuC0 CP﨟]q /qҷeuPFPOu1/ׯyP iU>J7_Iw r =96j`1P`!1v@@ UHqR^_L]~6"yϚt4`Xu}rmwꔷ`… 1D Z 1@`m_Ɯa^ō,\ͥ lj֭9qq¥3!K ZGq]0}a̯xo^pͯ4\)k_~(~2y$pTn!J>  B /0NQ&/A *%\r0EWdEe"ÛB @6`hŖ2H!0Ty%R,AjRjD"2K-^I͠ZD3M5dsAjIkM;3PYo5W]>"5Xa%֣AiXeevz]Yiv26[m3?Z7\q?j7]upPTLv祷^O6[_/S&1f᜘D'b4Y7Uh=&dqdWfc!BgrEKޚwyY|gV&]R((&=Oi-pDG3*˪:mK ]^;nF"fn6G eGrͥ:*~& 'k[&Yr?a˜&qP=uUaW=v̖t]vo' XwwXxx㏧ Eyi8ҟzBOUޢcߕ{GG|}7}ң1?}&cJ~\ፓWhXc)uU.)@ـ[29PV7™4}1T?% 4VXecpa41,ؠCHckxh*Um14qTx1qB4l$P Mb0 '2rXFEl FQhtl32+xdCB@% ?B&jIlftQL8F$&;&1)7K*GIh3 @D_T 5(Yml((${>Er-9F0!MZ mKtR[,Uqd 6Qş6J7JWT_IG.U 1H,`'WŪ"N4aCǺV XWI+[务tw wVu.$ _:ؘ$El 66Q0c%Jl0Y-2-LZiEmjUZֵֶmle;[JG(Km"JmL$B vjFv[LÅG@:׹4@ԱQ 12T2h]rwRhHG^ Po;:gHF2xޛH ;0j1&݆\ 5P0 d\Ԛ +6bv 2miGA"H Lm1やW%mY1a ·)/`5) Oy/H^Ȁr3|pF6̟8ic<9xv nC-4 &z P088?8451ot+jRCEF5N n5e=kZuuk^׿v=lbFvlf7φv=mjWזv 5l7U^C=nq[ Mqefw=oz׻+ƱwUBǜ}D(G!Dv}m 1g}3 -:[Ԡ 9qzb7t(3:}X3qpw:!N骪{񈠗i+ZdZ sa;"(rskx xS`;"˰Cb=v75>Nw|?xG|Ѵv' -Hg'}ˑWީ 0C8C8H@?1SdU>"~)@{H֐ zOPU^lGl5Aǀ< y$"`npk6oEgOCQ{_GC;?+BȈB@@x@@2RP)S)0*U0XfB38%%%0A2oA`,N"ƒӁPpT gXP8X 11&p8+w@?8"8'P `#c‚(AHS@ 18bPI4 $&KDه|p%ʆ.3mp+x >2mi@0qEnq8l@ d`ST02`&݂l"@F 1โBtuTB?G~Ȁȁ$Ȃ4ȃ,!ЀCȪRHkȪ#ȭS-dł @m.MD 1I/dXɱǚɜ#$_ɦmyZaʣDL1!,]mYH*\ȰÇ#JHŋ3jȱǏ CICUZ2+0cʜI͛8sɳϟ@#j)PBˠPJJիXjzshFSz*%Rr]˶۷pʝK&)RFO=j| TÈ+^̸  pn( fdjaȠCM ZM2*fGy5۸s޽uGN y0y3eKNuŞ  5p8$*jӫ_uS(Qˏ[^`5 @N#N|hI}$F%=um LhL0E` $&Y"W10C1 @/cc h@FD+p@P""LT(K,C 565)=HYB BpL"@,Psh2C©t1!*h'2j)F"grʩ+#hl楬Q0(Pl" %|}J&lb (|-Ȁc( o*U5-}%Jm&>"JL8ZDˑkCХ*PlTjB&HRNȎ{P&lJJ)P&b\&$, P1.֮<&_WHDJ8CDxl,`'T6?A}dYL2hN6p˙t󝥛=g~.]Az>tt2}4+IҖ-3MshC-RԨrSm\=հ5J63r-Z. $5&_@ >G@ qbMcrV{EK%Cݗ@ ~,6727?pqߒ,#.q@M⏵1؃c>ry %,S.Y`b].i\Ն@ю.|1p4gn#vF[XϺַ{`NhOpNxϻOO;񐏼'O[ϼ7{󠇝r'623b/@_hTSi`E:#ϐ7d+\2F-G<0) cAu6~\ Ձc4ae`_>43f3V8pxsaq(" s6pG1 +G?&(7gd#Ȃ#Q| Fq@s\(zCHQ|W?(3tPXdsS|(uLY}[t^Yig@׆dN8!CW upzh/>8^`?҄ф+xC\ק~XA`0c81:^؇ŧxs^^Aǁ08@8cq1: ^(ϐxmqqs4Whqڈq qyaXz36 T1XW` `סZ7xذr@Xg{ Y4a(H} {Xdu}8P|.Ydmrʐ3R1'$P}s@7᠑;9w}FFp 8c ~.NJqzr\&[ ʠZ91_&r {J)m pY sɐuo! jcZ ~tA"U}otr׀\ۀ[9? wED [3G~l1nЛi?‰cĹws9X~l)?XJw7 uqۉ?sY|XfiݘZّ𞀇pA@gIX+ԓp^pz.x1֜kd:yCɏ@ QI؀Sq1iyڠtF<yq kыXv_/Әk.N*VJLI/Gv=VaZf^Iv0:0e(L+gG!xv =\`[G8]cEJT\ sذaeJ(*s%u8cgG3%3_i@i'4 >06ve֖ ` %  Xjc ` @1 ޗ Qa b7'aYjC ɟ1>q٫ As[yAZ [w @k@ydmy_:Azڀ&?ײz y԰y';dPA dxALZHAҠt m@IwBuv0r?(ڀ[x{ ;-);Q*ʰW۸Ppk鰴mѐXҀ@:7{G3Fۺ=q?0^D[d˻ tzY>ptf A3= ^8 m˸یޣy ᰠu{=Ј0~sZS+ =_ھ_K k>˰ bְr0|`"@z#?щW',F3Χ_62: r9>%Q`PGAl>b$Ag0pf9jH K FH^S| c`Ĭ<&>VylArV;*u=밀 ޼5( ^݅^煹KͤH`= JTsnc@ \6 \N}P͗PNŽېZ[qΗ ) еʙ_ᭆ0k >$ԋ[tnk*1N(жO~ [I0A\MacN@Vo`l(FrHZh@+O.7c~68==x0C6bڨОˠJ'pTr: ~(\9لqKEokq/t/avy_kA?` w.\!h1ˑ칱 g^񵱡0^ p @]Z͎KM`K8a$af#? طaŎ>(@o/X?_@@ DPB >QD-^ĘQF=~RH%MDRJ-]SL5męSN=}TPEETRM>UTU^ŚUV]~VXe͞EVZmݾW\uśW^}X`… FXbƍ?Ydʕ-_ƜYfΝ=ZhҥMFZj֭][lڵmƝ[n޽}\pōG\r͝?]tխ_Ǟ]v_^|͟GozyzM6d?MyRA,B #SB 7$,0D"fGD1E_Ѧǁ)l'wqP@G#D@!'J 2JJ+2K-K/3L1$L3$jI4ds!zM97t9@= JGM>R eTJHh$hAwt|g"W:%54% YƠXT[GR f<$X AXVe'TgF.&T"ՂRe5/Q 2`@RS' ~>zh0c'?3" #0f-]u~=[]_%ӂ0H[`%ZPt ևA>  A&C%a eأ@ amC?a8D!6aD&6щOb8E*VъWbE.vы_c8F2ьgDcոF6эoc8G:юwcG>яd 9HBҐDd"HF6ґd$%9IJVҒd&5INvғe(E9JRҔDe*UJVҕe,e9KZҖe.uK^җf09LbӘDf2Lf6әτf49MjVӚהf6Mnvӛg8ŹX 8iw\ Pgl#؝7"!Xsu/Y@(-O JQY/-,P;Lb ܧ ]SL'eohLG'`B)C!,\nYH*\ȰÇ#JHŋ3jȱǏ CIdXRx*@*0cʜI͛8sɳϟ@?J2UQ,*]ʴӧPJJjTRJԫTN kٳhӪ]˶ۜXOҨ˷߿ Hj3 @*o#KLG`DaӨS^͚QŀI#QGͻ#Jkjf=%@  ]#Ne2})Eصt1i&(`D8Q|&(U 3PN€(j+0@u--Ă 8N h1 (*#oDID1PyM|b3\Xɉ<.嗔MG'$ I&K1s3o,&%C'A()8%QZ (L"Qp  Qfڔ/J+!b9 |e8ZDDQh#f*4$p!P,{lA<^dgE {+nL;(Xr#)MAR <)5"Lڂ+Y*Fp`"ILEe#aӗif3?Lf:3&4;Rfє&5b"L:v~ @JЂMBІ:D'JъZͨF7юz HGJҒ(MJWR0Liz Nwʖ~@ PJԢHMRԦ:PTJժZXͪV'l*XǺ1+Zn}X*ׯҵZ+^׽Z~*`+=bD䕌,!ADj.;Urvg>MjWֺlgKͭ\۔'-pK*Ꮄ )r2}F+݌R.v+Nލ(xMz7|? z}1<@/"#.#bA Nt~b$\;hh ` Mm.IQ#-ے!#q +dEEN&;PL*[Xβ.{`L2hN6pL:xγ>πMBЈNF;ѐ'MJ[Ҙδ7N{ӠGMRԨNM ի^g0Ězxi'[W?,+؃Gtlz 4jlYٻM=y0ԞGPg2,{et21wnnT} 23/@;l&/~\~.SlAMk LjQlӍxʇǎ#-N::>t{֥ƈ! "X9az?"M;Hn F!xH?!7÷WC?J+l}#bϻF"\; tG|>G"~My;牿<vs}}uoX;hH;䷀ 8 8e?8_2 Plpsgco]ChK 1 sZcod?1$frTQ& c xUh@ yfrװmX<؃&X0 t( 'C (uJ$ fx7xk!G!Q @Ia8g#x8yi1pY3؃1&6  `xAVtA%&8@0FP P dCm8[؅ bZ 3),@``u`+Y!/@H7*ŋH(2 ^%!}080 ~7X ( 6S؋g"6(4|C58-Ay@  (QƂ1q|pۘHX 1Pn% `Ő   =lxpu8p<QHWX'#<ɐ 6b[ɕ$YKcɕM(4sXx@CƐwx(Q58a^`œo 7ty)B8VHؘPb6 Ƀ^l8%JI")z/u(FwBQd0ӠՕ=b /}ɨyusNg נkj\g ⬈SŋQ[/9[;uI, 8p@f {[*,{&Aj.& 6 ՜h"ma  qgJ.ȳ6,B;ʮY` 2bhT*٠ld!o %q 0kկM  R5AmF4b:7e)ĢmH{@$b@腆Y()lП>"Vsm0 J_:oSȭeRXx;wU{K `=b juPmu^0 &`0}԰E$s ^#JoGg]JApsfV .ᐾdV뾰R JauA޻ઽ{ uhK+dPp:Ո(0ȍfüA>eʰi 8!v"E'|5ƶ!P5VvKϦqUmq.7G g^}6^l`Կ!Ki&k5FYWpPϧae]Uټx#٦'_3T 6FX1ק הY6{-98cRg=AvW4l 9{иYe-AX]}jdXݕfA}^ޜ*9` 3  ~i#[ n77;Q ~QCP ">$^&~(*,.02>4^6~8:ҽ \bB ~pG>(32n~`R~p ZN*傒bcjJZa;t t^v~xzg>b~s"m[q9=& M迱Rq0KTt^0# Y((~& % 1A")0 R,PlPV Ana PlO`N `!qȾ"e  .. .=y (`a^aoduhy@4 U^e -,h,2?4_6ߵH8ÄA>_c |8F=@Q/i X ](Iaogckpr?t_vxz|~?_?_?_?_?_ȟT Y1@I@ º @~a /X /༠`A YDPB >QD-^ĘQF=~RH%MDRJ-*3Was"V[nղԧ[.ETRM>UTU֚->k"HTUe͞EVZmݾ,_&!,\tYH*\ȰÇ#JHŋ3jȱǏ CIdEUU Ҥ˗0cʜI͛8sɳO(STѣH*]ʴӧP LE*Wjʵׯ`ÊkT*Ҫ=e-ٷpʝKݻ S];pQHÈ+^8!HIPI&නϠCMZɨ'ͥc˞MmG",\țBꔼyͻEI;]X5ixֹC %Lҫg$@6i Z$9ϿE~QDE']%Z!"J$dK`,B{&$ pb'Ј%)db8*"(C6Sh򉂡bep8σ'dAH)elz'bo~$R?CL`2#@;1E%"'k2)EYgJ 5\ I&@6D覴JR3-m!Bɬv*vN-?cf<f)YT %ik.Ai<"@, L:P# haQl篹, #ҋ0X@LgL@hG%jm&$l'iB&fcN B8k[q-П0(D+,!0?Ws{ն]Џ=(L&Ⱦ>s ۛ @wۭ7+}mcK,xв831J*j0@/.A> ӄ((-9E0rE{@. .7oUj/gK0{7:oG߯ >L#H Z̠7z GH(L W0 gH8̡w@ "!D SR/H%:/ FH*ZXt .z`01e<6pH:x(3n=05iے AF2{$$'INp#$#)%Io&9LT9$̙PBAeGn9J]r$,aYX _Tf[6H4sGMlZ٦@ij3C]3ise Š@b C +OyĞg?΋TEhA-Ps#!Z`F`Q(⣊F;:$#ݨ@L*RrLgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺;^?16JMb:d'KZͬf7zmҖBKڐMjWֺlgKͭnw pK\L#bCzsI[-aeC` .x=ۏ~VMz|Kͯ~LN;'L [0i{80X$n^.WL '>cB@kQπMBЈNF;ѐ'MJ[Ҙδ7N{ӠGMRԨNWVհgMZָεwkĆUȕ{-@N!?QaM6,jBSmP5H17Gjo 061|`A(u BL0P*G# _Sm+|&8Ixڍ?aB.6; A=y9Pm2Xn?$k#F]ya].#=!r?!چ8q~d7rD;Q峣 Eo`_pپ<as\i[riC4 47 =(QCy9ɛ c[cI&ә10=aSpy5h^"$ykqGn1 < Cpqu(b5cXi`3)f1eVg&(8v| ]m pZ *2qƀc J&8cǢ8q$@c6&?&$.G/Fz9cqP ڤ1dp pi0i+u^:Z+Y!Nq.7pZ^E41mp.7t:A*, ? v¢#]7i_. a_Տϰp` 摘{> b`cx=ZOJ^e}Ic٨ul),;PqZ'`@ڧc؂Ʀnę=zOcК,Y!28v|Z] 2 Gj dPŊLcM). pQR$! D6)r[qaZ֠ †!/+Shw}6h+CZ"RVc5g8ʯ22ZY(iR3؀ujak3F k`Z2t 0cٖWgtХRADGuFymTwxg7GHc-7`vGƸcqc UT+gw? kKd`/<ֶ2-@_a/|zyii)2ݺo[zaci+261b)20h{C֫׽10lH`t+i۠08O}i3,ǼRq\3p{i,2`z1R3``QeGj \5O+kOfVnsO}TǙcnIpw-gL7hLgUA)LQTs 1'jA4!۞$*d+f*@Gr 664a#xT`p NeC?`[Ѱx9n ^0r骷dv̆3s,d>`ʋ|xy*# {@{TGzfLCQ;sqt:< g>+/ rh5 OoW o|njι3ǟӗR=ΐl8AƟjļ]9ӣ#q8=A#;)XUƿz8;̣uv0JH6\sDMG`=g^ֻp;cIs3jmw]5MX {m8 OM:'xP Tt:I ב=0@ذؗ3.pٝM7ף:?0ٕ}ڇS8٬m8 ڢUڵm?3ۥ@ٖ.Є\p*boc5 ܶ׋Mؽeݍ3]}0 -Cm+ȝm{/)`[(Z0RjeB>R&#NE#.$*z',.0*.㱱5nQPq>&wq0Bg[9 PpmYwe 1c' W^) $qo@@pR!ru.dX ]ɰ 0F1 PY`8p `wS a ~/v(%0l qP & ?M첑 ^~؞ھpn >8X ^[=?<`41t01>^N-18 c -8.taߥ Q.qcQ!*~ ny0yqo8?@?B_DFHJL=O@0UOVXWZ\bdf?W?ZdRRu]n5=,z~?_?_?_?_?_ȟʿP ~PM`$pO ߲e9FP /-?OX@ؾO1+@DPB >QD-^ĘQF=~RH%MDRJ-[JRҲEN=}TPEETi[ `.\^ŚUV]~Vl[ԅ a[zz˖ԱuśW^}z% d@!,\nWH*\ȰÇ#JHŋ3jȱǏ CIɂN\ɲ˗0cʜI͛8sꌘjϟ@ JѣH<Ŵi=RJիXjʵJR` L5*T*Q^Ӫ]˶۷p. KjT*RFEJ߿ L0QD)0j KLr$*-cCtYQ,(Qc˞5zJҊQ0!@(@^μ9N*UvN}2P3f@ 4UO|G.4̫XT /']ϿY'Z(8& 5<h߀VH[GxZ!Qh`(Z"@/QD6B~(^ &}#<@!~RP 2O D1I&8=ViP#ul?^)G &!)@0#?yQ9|eAN} ҚjRd,P?|b gd%vJP zQkJ/ŀ$}jȓf*P&Pjꮼ6ĩ@P"]F+jjK1X`ZWe2$+ r An& -8Q|;@@6ܰ" N #w7b ̧(p ! @@8A@<{K-.\sKPG-C 4*@L D?Omhf5A8)&LbCe| )+o0$J .qB+-gjl/9-sn#xꬷP.Ôw/0^`2t}0* õv+3 sYC,bX=1WbC-m76α8{u>ƪ50l9|1#_UN^OF8Fza $\Ǖ+Q#oVGyl̽89f6ైC#y@XTG},+c4?KocQǟ%=KgzAQzuzY?HF2ly#wkA (6Sg<0@} lu@fH>|#  ǥh+>[Wt M WksԂUyGht 0pnh HhWp[nIT.0^&}#rF@~R8 ,!,iw~9D00`8csT A6;js=jxDm8 j%'5&iuPT7U(M8; iIr0 Xu `e]h0(x;`"l  j(usi(7~ra|揟~ot`1jӠ irp;iu(` 5`y~"IFo; 7q 򠎮d:BGшD:jU Gt7O:?sH(H1t0Wӈȸh۰_0lٴQ}_>td'1hZ:i_| $DEY ;.IVK?־z(Ѕ5%o>?    4q3 1ez>` KYɺ^Ėl\c :9y+?8`)kT?xgЦ.|54|l`:.háF>) q_uj`Jl̻ރ %!Ź[iȗm 3 [v 6jcg 0{ky<[Vr m!*πjybg)>i.9|nw_f {ޣUO*U@hUPAm*fd0ӛ R 9S { W ڬ)5H*`v as\QϚ3:!мJA;XyѳѲ"+|ңB8,^\2d,<   Ԗэ60Z|F}%@P<t| .ӈbVb}>*թ +}7!֧ Hl 917 z:ɠ L0PY]6 F:gzIҒ-mYòVnY,*^)PPڢҨM:aIc |S8a՜}wm?;ͽ>syM͹&!}֐l;1l' ɷiб1 l` f !lp ;]LuJ Slo4!zFQn0'J;ae8 v\Q|8h&q[3^hlY>9<=v! )l>yM~gwqa=Ҡ")_(fk)U(LbZ7hsxlQdM~!A1莮?=W9ʜiIr`` `nuhp{^^0P 4,sһ>䖩ApX l.]n>ص_ }7I.>FǦij1 @.n`|YYW;ٕa A?c\qqo ?^٥6'M|,:3Ȧ W:0%=?OtEpa`NS_ (UQo!d?YiE^[Jl<qtoo_VaprNk֠ A  iǶڪ8&fڔ\HYX+ d^N&/`\/WXHm@jmnz{odд` l |ӯd5jλo/٥/^v DPB >|nA}&Qx=7?!7DRJ-]SL5męSN=}TωM mhFB,9RU^ŚUV]~+~]Ȏx‘4X Ư*.8ɒX`… FfdF\86k7^zXhҥMFpř!]fȯv.; BXuS)Wr͝?]zׯ#i`y#xFx7]zݿ:lz 8A07L3D0AdL9v;e,2J1DG$DmƁGڡhǼt'L2H!$55FwȠGlj'Ņ2,K/3L\Ik o3IJ<. 9!3O=䓺jrڪ't!e ZfiRK/9gGFqMQա<qЃKJ"c @Lo5W]5QѲ~t|w~d{wgǧ~;AZxM@ЀGzFv@6Ё`%8A VЂ`5AvaE8BЄ'Da UBЅ/a e8CІ7auCЇ?b8D"шGDbD&6щOb8E*VъWbE.vы_c8F2ьgDcոF6эoc8G:юwcG>яd 9HBҐ~ЛH&Rjd$%9IJV2ód&狃O5P@Ҕ;; uJVOje,;=Y2ue.K]җ $VE5`SAE_Uq ʘόa 4mӛg89NrӜDg:չNvSHⰏ;ɧD.ħsZĐz*tɀ:s7yus?zЅ>tGGzҕt7Ozԥf<*̨X o1:-PYje") .c f0hI(i*Բ%H #B (f#FNDEvOt}&Jъ$D1ьr8tM)9<Ө2.Y(wPI:eS uLP@Hu>}ԗ4%OITz\ ~:pu^jWIbէfuT h+hLBt @09̵k^f" ,^FX}dm%[״hf2iv2lgc"ڏ`jWֺlgKͭnw pK@$ urZW'HvMt$uKMz|k}+L.<ʸF #"L [ΰ7{ GL(NW0gL8αw@L"HN&;PL*[Xβ.{`L2hN6p\Kv33+AІn/< fьV(TQGGzp4] }i䡄0Z+Fa,uqsޝӽ>/ՂMbNf#?Amg% `k{V6Ǐj-яW+w520[NO;'N[ϸ},}o ׉C~aLC$&AQ @xC6h^s9 pB]p̣磃C2(UФT @91!eLz,Hm/\hu?H` P8ѠR;CJb G#0F)ւ 2v/F. O|;>zMPnX6D^ q ɼ@侐*TW=С$D{IcAh%>??WTB]''a"Y0׳;)(*pD#iA_=G|n4AQz~)D\G8\!qtV& 5~DtM@\"HeL Rw m SRW a0e V ` ׀| q omO l `u R.y?wOf0 |Pz-X᠂;&'a?e(gi8.@_}(xOv``T6Hrc }6]@e3iyQR6[gFCZYf=PXNJEy@B;D[F{HJL۴NPR;T[V{XZ\۵^`b;d[f{hjl۶npr;t[v{xz|۷~;[{۸;[{۹;[{ۺ;[{ZEA  +kBP Kȫ˼ !ɻa ikxzدy_L_ٰp Pwɵ 0iyɯzm,wjYƯ,n8| [ +Y!,H*\ȰÇ#JHŋ3jȱǏ CIAURPҤ˗0cʜI͛8ssdS)JѣH*]ʴ)N- 괪իXjʵkORz)hӪ]˶۷1I%7Sw%: ߿ +(fπMBЈNhb'i'Mi}ʕ^3iZp(@}3*$ɬ&;Xle#y)m.kexƯrjC?L#ɭp*1f3S#luUVQ; c+^9+lO)p߂55Q-] -%QaS."'!l!ˮ8z>W@B9DYFyHJLٔNPR9TYVyXZ\ٕ^`b9dYfyhjlٖnpr9tYvyxz|ٗ~9Yy٘9Yyٙ9Yyٚ9Yywuق1/K0Wpzϩ9Yyؙڹٝ9Yy虞깞ٞ9Yyٟ:Zz ڠ:Zzڡ ":$Z&z(*,ڢ.02:4Z6z8:<ڣ>@B:DZFzHJLڤNPRPU*VzS\ڥ^`b:%}) ۰Yd1p!,kH*\ȰÇ#JHŋ3jȱǏ CIW(STi˗0cʜI͛8sɓ*UJѣH*]ʴQBJJիXjTTR}M%حhӪ]˶۷7IxJܳsI˷߿ f*ԩS̸ǐ#Gx(6HQ0JlbɠCMMʉE!F>M۸GMjzPbLBc-p+_μy8Æ(0jĊM{N#@ )bZd;h놥ϿǼK,0,"80(G߅fU&6J( cLLG dΆ0(0h!(<"@1)ZaFtQ73Pd(TV)'bKh2 ?0#8,:cxI#Y&( 2 f24l#>8*RK#@~Jc \EzUք6ɧ \tSȥ1sm|˳,j i vҌ>79@0P9D\ۧB'LmۓKB$еP Фy5]s }gEG*eJlGC*R!|dF]TVAySj iL}*&DDm JZtRHpDrULuY*BԲhMZֶp\J׺xͫ^׾ `K¾OT4ALk:d'KZͬf7z hGK Mj$պ)@k_Kͭnw pKMr:ЍtKZͮvz xKMz|Kͯ~LN;'L [ΰ7{ëuGLhA(Nqt0gH8αw@6fdHN-E gc Fe3Qe\Y~@00tYؐ6pL:xγ>πAwN=i"A4PEGV̌imq3pX,E~[q EI/)I:G> ""ߺ|pkZ ?ڀ4炍y 3 @ixAH6zDLR )( ZN('7-#O"AZ0o@ )\сM8eטrg='/W3q7^M^'/So\-D"@ pɣv'V-e$ /;xļ$@xh : 7I\0oC_.N b?kW۲GOϿ8XxApm x: *EᦁGw 8$HW(g D@B9DYFyHJLٔNPR9TYVyXZ\ٕ^`b9dYfyhjlٖnpr9tYvyxz|ٗ~9Yyj])_Yb59Yyٙ9Yyٚ9Yyٛ9Yyșʹٜ9Yyؙڹٝ9Yy虞깞ٞ9Yyٟ:Zz ڠ:Zza䰡ڡWI`I"(*,ڢq]I W8L8ϐP nAA1ʄ_|;././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/managed_dock_window_save.png0000644000175100001770000027245314623331163023534 0ustar00runnerdockerPNG  IHDR5hsBITO IDATxw\e! nEG[{QmZjZ:q/Ժk{"3DDD?/\&.9"4&C߲kA|(NKOLIW&K&RńFJMuI/Uqa<m--+" sVNVܻ[,$&T%E3"ruuˆH&QRRRbb"9::";S#bxNol/a NfjcD$36U%gHʛc9C+'GkJKHS#FDb}S8}PHJ)4hpSSS*T֬X>###ݞV P.4ӳtt0*8Dӄq@}D=]2D2c25UXGlleg#U"RfƽzȤ$83QR $&">%:)1]-QcIiJĺr3Ci|P";::j&;::㻇GxxxFFFCCCw3=@232xXRFrB*13ʘ&-dyVfWᓣ,S%Hezbc3TX"=k*V: 1 72d(Ml c2(=!95.*]HԌ3e2qZbrr\\¿93NUĥ\շ1t@e*Ǘ&vxBsɩ-(P(>  ]|||+( Db]*-6"4&gfki$yd&1}fOOM'"N_R§3#QZlRD8'KM,l dbu*11&S-2'k^N7=J8"EbhR_&xIDYTqfӰt7Toj2RyN\&JKNHU<2,Dܹn~T7|hhR$"Pdbbbgg)j5L@8WxDF2JOLVI&I iE^GtZ9iұnuƪ*5.MIRU32di*^J-0b#H,"""]!#%E)qR]1FH,lB*J#x^#}@2݊ʚ%噎8M4)` Κ M{͂tkgw'"R9uU__? tjbb>cL4@e!JEH|briL$Sfd&1TJ5#D"[Ӛ >->.%COKJU1D§D:5!>%Sbbbk%Gj12""#MW ';S)3y]^PyF"DnqG[8J4DR=B TEl!6665L\\\.WkdBe+f*3{;IuRž%L9 '*32T%&'GR*&R!HL]{"ǀCuYFlXx$3%'Lf(R0t %ѱ. TKLb| q4] }GGǨ("rss 655•g Fbdf8DH&11YHSSRSNclihg2 )JFħFL7$%ubDħ'(҉8cT**]ۛJy^rB\t*%5.,Lg$35K^1{Y345TC-tRRyIt-$;\NNN DFFjn:::DW^"P X_`d'8&j2=]`'547ו9UʴJn.E'fuKM8Zd)HY:Zp J*@G T of+[$DF2g'k3]ur[V27 vĆ֎fL@G$yN4@FmħD:6_+ԧr1K1]t[#7m-iR}KoD lADDn-oȖj;GDD YhY#ywt&ؽVj5%wȥMK׀W<9^}Ԣ:kKi;(%3>#L>jZ;g"R;Z??בVkU3 O&''+.De2kiPa}(̭5=X"ffpHff+'K=wGMOWronm+E>&8&*=J$1+=5f$R GDI$bI1Iw|V}5GDgp \=nXp`νf,C oO>D[.B˵kׂJ郧rL!6:V*"">UTDD"Qvh9GDJˋ-߁C}g7]eU"}9+w?csqYA dz&#~>a'>4܄HNNٝ|||N>RoFFFFFF]!z5 پjjem)l^\+X vNbnDD{Rqޟ $3z$$\;p:'q"jb 9p蚊Xf=:̘h T1t֗Dx'Դ\wk꛻t2<8Npm"Dϟ?/B k0N{~UPaѣ(^wnٲ%VD۷o|bڼy3pǎ\ʍ?u33cFv"<0ĺopl!mVa:@قP؇)OOpJRT@Ұ7J Ĉ幥-] DogԻo,H?MB*wI\}ڿiJeZ̽ս7]ewh׸LŦ&ׄgdff&fy$g?Gl='˦Wgm}S>8x:L9P lt>~:HDz{vM$Xܥ僛ȍeU{^%ޤj ْ%D"⸬I(ͪU67weǛ>ob%ݑW{EGzxĤZi>IH$b˼Ca;#"lR6sDNh_GF枈X?ƴ^v^sHy|b/w\ԧm'@ X7GCV]ug {t{*VnSuODIk};4hZzg W׌PWպ/:YZ|߷}j4%Z͹.{H\T`= 48Z#۽:ܮcvçj"շpq+m}#5F[*2^=`C=|)Xn?[s 8J"b)v?rl1%=zC{Ӿ!;hj1%_^)bQ Ys#cu.ZK9&Vimo#~k?355n 92M5m3٧"3 f]\wW=7D=qAsG|>RjdYLeP+)Imڌ[SW33/733޻'/q&'57p5[7nN׹U 5kauHdְ}#0Gq)cج +Uw"E&xw(i,ĎאH\DDz#Ib^Ed#]FQmI۱?9"{i̐ZD.Nyg"Z!T 79AeΨmPop_d|덉dOV I|Lc]wFDb{gǷ?d`fniA"kZ$b b1y&[f}z!XLGwP(--ѵ%ƫ VF&[Eo@%^dD ʄ6g|FI|z)t_uVSߴkې |J؃g cęY[;eICc=㲓KbEUjxXƳ=WTY5_f2UUnq_8)-YMw}S*Jjffh,L=~Ν;wܹᖁ6HνgT 3Љ:7#:2\"3Ʀi !%9l\cws/Ŵ[snb"msvpTo)qV~<(i~K{qٱdiӓ9ި(M}~Ct.Y}Ƣ*CںָwW, ƫ5x^`~sK,pSeg6P*o;^5kڵ]Ɵ=K)c )JLM/?Πw?L Jmҭc축•'\]VydFc[Ua @$mydeo,[L~u."5S>n3Fv+i&EO/~}7㈈ "7X.3B]|9}IqgӺ2IlU/ED.<Λyv#>XeTAAAݺu+*޽{[jڸP5'vh_nG6$oݺSRu@yN!·ݸb2^p8v=|  o%).Lv="U ^>Ai;@I+ҵY*ڗvZ#;v뙱]o}E$rA|y^;@xWե]@PD'++K.vJPPPi%/{@|}}K*000*~G @"Bi;@Q){zЗ { A'(3EE-k|yZbP}gKkwC\ī'C4ӫ;h66 ge4Jܰ]$Rݺ`c;KZHV|,}YW;-weՈ65\\3uMl=<8FDoǵ_\Ô]O3nmk\ZЮnݖ.HgQߪnn՚[|61g;}5{1G2ԏ6o\٫uw3(>}k 2hN )cRէ3HZ^nn^ :;Է18PYiC7}}'=7q̆ /{sѹ}ooc V]znM68h;[c'FNܼyfVSq/Z|Kx7X=N>OOl^]W>N2:z &"O=r%go|̫.B߮?}ڞ}ks"3FM9q| qaI:Xpܨw _3|6'u;$""U!\8ј<|E u2?{ vFzR,n/L1_y DD͇ HϥWdK^dsf۾j#nfPRM,9Viik%Եĩb:Er+N gb&||Lz، 3V3v)cֺB5ފŢ8Twgli)M, @b˖.l,3 r;0mwB7:<'c1eDF&#YpD""YX(XHB""eL]GDY\df!7j?og "(+N~*ONuٍ_2rȬN9|ug־C=d-8 II.OD;w+5k6vXwwVxufF@Dĉut6ʪ/>^l׳Oo?iiLDG7wKŕo}P8S30[cpɪ%'Okkyڣ[%eǥ[7a)%uiz?xj$y,XDy DݿQK-7SAb˃szr׭9p "((VQ>ܸq5 IDATZ|ZƾؿzWpy@)qh~g5\zD9V^.^Dsq7KHRC͛|=\gѲ.oQcW{^=\;j!)}5^k_nBoę6y.ڴwzRO|NZ\IP_ܫGF_0T{xgE=@nP%6y{{wĉg.zk\@@@?}2XwZIPPP׮]Y`ܸ[oM3xfVE J޽{O>믿L``ݼys& QF+b#%իWElFHx\im\B;@Pn ;@Pn>i_ӧ B>>66 (rz]v`ʬv֭[kR$O>]UTRS-Z(BJ߿_.fnoXզo+s-ߛW߸ _U{cΏ tuuڏ[+ȇ"d`jTR^P]^ŏԥ][ӝ 2Ly=ecۀg?F'Q^Fx{G3q'Y I>fpHxL| kt bv4exOj{}77Q(ŵw6;kw[dŽzɤ/j >9vמ%s/?};?v_&y{Սt+s}eN]twzV&>gI;ߎ [̽|a(6Vȍ1(Mѧgw"RJǬt aLn"‹]GyYv8YOW3.ѹ9w1>D)]oȤFFxCS\{iu^n˹=u Rj/?VioAus֋62ȵD[Y eӉX⩉5>ψ ]Z@ypߪ0ms.KeR鯪ޥ(ҡ*AiTҺnaOCT0Um7c?U22:?Ǵhm;b cOhakPoHQV{quz̺[IھNϥg/ղgǣGc]/'?+e1͸T?\ܢʂ5NkzYWgɅ81ȓzۻ1t{i{wMija?`t ڲwj})66v^4WW{^<%yE}~nߑe o;Lj2nmks@{՛v)Uu>nIbϔmmp:o.ur?..-D wWtU{̾$&$bHwڟN;beQssZZuMUlt[x!APϸ/NU%.Mp-c0m5[:~I8V.Ƴ"|[vw(SWYZj=g`ϧf-nτvCvԜ?Il9ҧG}Nl=BDm~3lbi#PMӞ8,*yĦfuOH 9ijKmЗqvƯDD,~7^}uK!⟮͖ j""CfGGe?jnt#,d s7=u!S.OmJ7n?93S y*mFJH_=HiNg= hΤ~n5 (͉۲4ζwcaS{2M}knGi~}hFDz_ ]`T©YӮu^>U}{Q,TDŽyhbT;4g_VxUOw76yW1T? R,dʖϏ|7}¨֛_3hA }^Kn[xeصEB\9CiibCbɚtm|읷[OVl춣8vs492{L# Tqyvp݁; 2slđ5맼өqrwZݭLMmj}6x88FmZȢEIQv{90ά/_B?61)G}j1Ok3vdc`K1&2k5i?麴m*WG ZR{^8WYrɚPDSQ]uN8lEр.1J&532Е=IP2w3,&L{pTei#u̵Kg^(KbԦí{_ DD }{L>tF37ˋܪĜܴjة#S%g=<^OqbOdPc@/KΧ2'fz{gcC=)cZt3{+_W<˱e5c &s'2>lҧ{vViv[JĶ}︐]|4΅V]Q%SxZ!NUqy0\I~W.7Ca'ekc[}GaDDyѽ7܇gnόHGQ~'}܇=jE썅M܄cʐ?}ѩuZusV1Hdn%4JX,I6YaE"b3jYZf0H$0մ HMd&q;b fA-h&1NDoӯ׃j 8̳YX񪘈5puqqqqt$1F*.?C/g{<f ~Yv[/Qs۲'\_;o\ aÑ_dt \>nGX=|Dm%<YcLHNk׸$1;85 7|ӯ]-[}鹚 [\mY{?^R)3tXlt}  "!-03TAxzzc,Ȗ١?%yǶ*8]VvG١W_$$q7UJ&~7bɢ~88o޳cڟ 2c~k|"̳> W f:>LӬԻGW[1ro}ΝkO.cԽ !)I7O1oUкC3 in_,h֬.۟~iܶOnjNeIe_;Uk7k{sRF.)VG /$|cs;o z}7vA-[wMc"+y_[QWhK1Ǩ?68?קI'm w k Zf5!Vncײ>>ӮNkݸCW|Zk˟?}fk}[{9~6xidnPu:w/>y][O?Y:PK{?m` 3Z[9}_l,s"{?1y*kω/g5t6lGI6fVwj;q˽ODֺ-Ns?<طyv#]>kOm1rm`O/󮪲ۿ{׬Xԑd5 QQX5zJˮ]{+AAA]vgqnx"ںU*];vIWrA$=z4C= Y*T])000@yf|d+ʲ `J_PfPvPcK /i( .ɮ]jJ˵kd2YiWQy"أ+g\]]O>P(X@377d]H%%A8A%A]`Jv"}wssVPiTZC <0{4Tf%;U(e5kּ{͛7J!ry͚5K- \ ΈP*Fj Jۿ @|(7 wrW.2jذa1Pv ŋbթ;ON혳Wwwٱ=Ӊ2\߿CvfN:|U w>}ك==b- a$Dr1?ǁq3%gӴo|f*SRTCϚYX$nOy*QΎn[Ӷ̤ԮEsϸhAգ=R2.;!ܽG\GZנz2آ_=NYJ8~V/x|E u2?{3o9 @ G Q ss]!6*&^_ LYFKwO>OǪD$r۳[2w)KdfՏ.JMd&'A AqafӪ?1u.rJ=SHE s>˯*ɥ6zꭞf^""6Ǻ0}"3 Q W7v7Pheq!÷Mقw?|1*">%7B"Gikfs{7m.Yuox}Ւ2Ȼ^f;ʗwgak:sR-="{sIpttko?o{Ze˶a<ԯwG]:Wt}5^k_nBĒ{C_^K0xF\g}Rӷvswȭ'zgz)SľL=7̾'|ΐ_}ִk&E֞VkCwP֔kV]i: ~J}BXVؽ֠|7i(88rO)u>}P(X@377d]@95sx>?}T$թSGϟv!SȎ(_ ߓ[nRJ\.?}tiWP! w /iC׶w( P J@|2QC,_zն617D֝  L] C{74?f|ϼЕ:|gn)(y=tXrMO>AMk]}1I/{d>'67ql𥟳HǦY6a+]7¡W3Uuܣ3[6['F+KD*wwճ'/]:k3j5IYPv-p[N;2ojpKSO&h((,ɮʹ{'۴7 |cSӘodz~i/ftxzuX;@\9osT :%Sʒqg28+\㸜 ͵X@K23d"ϛy(QGإԏy\ *|z V;o IDATy7W$2ڵk>>>Z\vM&vP >sNа䶕ZBB_5\=9';59+rKV㻫kppӧ  (y2յ OR/Tx(LLJhCII%!B_5Ww[ss9DrHc,P~/d"m̬6_BBw;sfΉeq*Tjyf\9 JJ% 6ys m?ώkWFtMQ>=ܵqF"?~|P٭X sxwWTƦMvJD/_,*7xƍ' *}99odDt#ʎ.]lڴiРAD9~77נ )qYs;riڲ !Wj!g'\88*`~\]ss֭:uB|(k:ue˖kz> ^xw ߑgώ Aaxtvj>nWȞ;sCs6{*0sksJ9B|Y:ݳ:+_ٺ.:y N] qLZZ@99t#rCTP{S\ &UjwjҾnrز{v;<Z#H4Տ[tXYYQDs^zi&4s>ؔ@I=%sﹱ%8lԈ }]IYkiZЬak7Wan6WfF={Z?jo߾kg}V b:za oT0q̙OӘw>eD<{Өdֻ}:Hϔv+oأG̹G :FNvc.Ξ?N&iS#ϓ5[uwpXۖq|ةun}[*Tt=zؽ{]{葚٩]wv7| 1xӑǡjyu,&Mף5{J3idÿ~'vtƚec޲O ۷PRǨmt8Nst(~xӮQm۫B36&\j:; k֬'.8nw(jb?" 1̌OhիW9̘1ۂ}ժUBJJJvѣGJJW5Wd ^LKy\ l>ʧi-)πEn< KsV^T]7h|kD|n'{#Hpoʳgw~|sDD,'=xvoG=F_n'''`SM-ygҳHW I1hwqMK&,Y8PPuuϪ]V;uZVk]ֶݺ7` !{~D""J !|/_\.w$'O{.†{|ԏcfsTߣ$ҳm׮-9Wt.U[kh&Ldmmmmm=vجM:-FeܤaF<+M|s2vCGvK/ݸrs:aNd#ěx4TI[3Qs"v1vؐ:MkoXe^M'iLnh2\!ۻ>c>>^}||{"4@3xT͜ԭe6ONЈ?cc$DǓȥGVgDҀ{YYtj,Y䪎J G4 "nۚg%DGxF>:}x%Nm_KRnּߓn7|w}j6nRߚj9φIȮľs_NՔDdDA"Yu=L~~B>|֭[-,,ƎhꀕqJ*,9^\"K EER$NJ$?HKDT"p1Q$;;*+"CgXsG!XWȈK yK+ڴn/996kR.hۜ/+.=N̬MbŊWk;dII;]vcǎxKνZ)WۛM|]OYsfQ iRw}ɲqyē*Pݓ&qruҵsgHb*NN\Rwp)&"8:+uKd'Jk "ήk{~_|+=ucDDZk/dgn}]DDbZ1P$Xvش\5ٵ2A~̫A*^Ztuuul֬D"5``rX*WŅ"1Fʊ'jbQ^$6vJK\J2HYd?[ h.:ϜJJVދ,/7W+X "I-)T1au!;dddq YYY5\ }v=z4O2;αiWF R!K_bE|W/X;FM&KqtBO.#"& KofbZuyW_Su%V##x#)<ֽȾ[6^LMȭr:tHƈĬ6%^* V޼ |1);H`xmڴ:<3zh] }WWm} {x3c2G^z1O~UI[9N圻E<&ou[HD}w&OM )RS;^L.є.oY{Pm~0a. Dܫs8t箯 g/g՗DTWs`VJ&8DlnS~+֫m}ew7orfhRȾU^e^۝-wگ[YsW 탣&T Q;*KΞxMpAifzڕ&ٓK.ݿd{Aq\*R8;D6/MM-3i$^^l͓2~{z,uP >$=䣄S–jCGzb̙3tmРy ֕MϛsY{#9I+';wW4A3V|^9=J)$[߮]ZmI$0\y#ζى-kj,\C𬯓A[^σh}ġEqa--mۺBQ1ƤA dok)pV]R6{)R |ٰ]B ҚSw dak%%$ <#wqrd%}{MЋYX%8/D`BZKm<Ē26^zzݱj?Q\ܒ%K&Ol2ݻw1\헦5É'vZVV)mo@ui-.|:AeOD W3 wjZVСC={4ȪHI֕߄b]IIwS3;ʲ2 jwWSvݻKRTBEDW ;zhddd7d2ƍ{[WZd(ɼٴ2<){O_u?=ZD6-,Tu +s:+TjU!zQ,s>^p 6`D'eY~F6k0=vag*Src?eDmC-1\޳⻅GD HdbjF]z^4eȐJ *yt`7gV]SF jpK7vDD9 Kbf{پq~Q6E瓨Yo1J&xC#&fU:]%+ULH`vXuGV 'G|""|=j;VYdlW4dNK|@ψXYYW@;E;p2pjc9Cp,r_-҄'>˜BFF5\bfDR"YSsSQƼܙstAaа@5lmܫLnb Hm+Y -GU_HItVKӺ=Ν9DPmwd;E/ϧ,"g`mzXz:jHe k׎ђ%O>,:y{@`;X963/{ɓLӧg6 %wW`F@{GvVVX?Tb9'hͫ@ 2{O #"M:bֶ*nCf>|+/M?S|םR* "00؅Wٹ~#t5(Т,Ӻʞ/` ܟxv_/{x+|x| X $%%%$$49j<}{w1m!+;;)c8ЪD~v-%!>c*ebfL?s S3Wr#/$Nϼ[xA?^Yvί_zO 5{yߧO`D{1vF%HC[9=ϼogh,+1 NBm1'n7BM>Y_hヒMDD j& Dq<19p ;ص@Vi KD!P92"NETT(8xixnO+ 3p*@7N2Yx緹uH$vx g-?f.(۽Ņq.17cKY4/ڙ"XYj"*䧨~_e;@!8\YVAӳ\ ɗ>`O}c% g\BD{<'qs:YP0[+|V?{),mWʞAS|hLƎwM)xeD$f]ۋ>~Mj˗^}uyDžRKnӾX`;{s}w]pÈit%~45<(΅8ŲCW"C~ٕY;ۚ2یt4!\ٸRsC Fr3=y۴W_x{_O3a幺 ڒQr47H`@?,Q}޺gzڔ_y|G~uA QN?ٗWq#yg[~%'>ye}{BVzAoS4FSz}dwhxQc9ۇY;y`+{Cw4ĸS[[Ɛ+:2D.,OyG3;*7tۏkZji h̏il8"*9`ި7z7YŮ͜eRvh´'LYψgκ=Y]gsW\?XZ^nu5JM}PQ0%il5rRgGU{:=}2u6wN"dnrDD?osUWPuzgs\)5DmyO_n1E}?E9C%J5M\pSRbߏ;_ QٶV,h9yʱ~"RQ_hu;qnOI|wt*M)?kyh& 7N~yٶx:XPN=HÕ=oŝW&2" }aQOl4֭xST*{Xb*^3192gmZN7E>ԼՕ 6tJoE#pU+_O4h__D/CåZUY=D|DKs䖤2\OS7qk~&γL5yj+;{{3)O3_3Ezl穔\5#Vwꘖav><ٳgFo~9w/~'acɹeL,JGFuD|_bV&i@gg)'ZAcNbSRO]vz qkf po啋=[5%/svsqs֎nN6f tSx=ϟF @=},ߑRĔ,|@S|FQunӏ?Iƹ:H5GdwzwRi:δ6ib|Uұm&׮ˏoUhڷgl*rbT& e­Umz]P>}:""€˩S @rޟE_Du꬜CSBD]/KܳF=d݅ЊW81 &˗/ _ AjZח}D$''QbbT*JzfBfLKbS% Wͫm_?X|I?L-Q5n{SrTNb;ji{FU `nl6w&9XqHUͭ"wo=:i\vHx 6ZMd`$|HyWBZGvoP%_SJ}Jٱw:w8Z`+-*ͩ}zN[_˯Hso+5ԁ0k]1&՘',]pòt#!bU>GBgQQ|{}5TW >c 3lHh9kV~R">xo|6 ^w$DD{fw{u0xё_:e/{,-FPoܕ䄋v蜆Ȭ: ق'|蚖_A]To>³;.jCDDStzEW[<s:W׫_<4`:`n"oUyٸqBöo )~]!$=MWt->Ǘ]eJ hέD-_myc 8A= ч ?]|^/T ?\ڨȾՀ^ʍ_7|ˆīl5{oǻCΩktهSu#r}zk7)7OMá7Z=\Df\?|D٫^jҾʉ:ob??_m#?'5' ZҦl~sxg_mUa\.SEMaĒɽx:1Vϴ)_uگ1J~1*+$N\0_wШ?˗ٲiZxyYt{tuktm嗬Z}19Nx;tʴ?:`YN_o>#9"c >}憿wNMrzkpðp`l;1te".jtf<|kN_*\rx.Q#:<::pߺEDx{~#sO9O}\%>?i W]rڵU=ːy~}6盏~s_ⵋNϞ X揾ռoB]x~:y>yO_.]s/Nzqe`G}me%]FnmeDVrki:eDDLSv#g^HHn+g{to_qYKDW/q[~1DD,wW}#o|4|ڱ~+Ϥ-壕W*=][^/>3zʁ׭|ݽE$BLvzqMږOD$`]^[(}1#[ِ)`oM;5oK~܋/I=J@_OxSwMӶ_'*L޵C q]i{d\FD:zuL|f qImr! G)_/J9K%جA2nv.b1N1Xi٫g,{uʼ!^ZE3XYđ/M " }j`k#F<6I=hZjkСCՊjyX{6^W9Wڈէn92HjsLu[t`YFld{O<w3+3ޡJ$}V*?.(7{SaJ31γSvr8j3]. /9㔝wNK;.v|GV1ƴ [7lGGar~FHҶMEVqD~ 'FLxgrG8޹`/=()5cD zMWC06 *N1Y=ۍ1)YWMfxQ|3{]d7kv}S`@`9;Z-qQOx}LqĊ1􌂋i _dx/?o^7k޳WgV&iX4l!+C"Mx8ff 18:rR Za?8]!reᜇmnS!u)9vS}Z=&uO<}fӏUϮ\lWƹ>3 瑓=1R/xfXTz 𠆉"cwmq$yQηp-T$!ƄL|KaVV)c$%R^oy&V|IYƺ9TW_>(wIt=1/[ɼYz~~~~^-)׽|$dyːp`xS'KiVL;s΄j*ߝF^*pg^=tСCGܺ7E"SW/X;ַ˻IeOY/d1~gNɶ;_ wbDĕR ҇|;NЅUe1Uy#vӤ,3Z]:gΜru, zwn>|}E "mF[D6\=.p1ƸS~Owg=ܿ߻w]d+nqcҾ8#HS11-nNv.P˪.~~mo.(nWqp+Z‰Ĭ,]fg;:.սU\O~EzL&dISYAY\jio޷4ᮼw_Z1;66~xP3zĭې"WYS_~Gޜw&¹%"I[h5Zk.vu)ccrDb̪?.ؔX$Sg^>\XSWhiEb{StQ)zr]cڏa?3Vx%۔> \-qLkZ{6a_wNrv~ݔqcS  p5&m{6kv}.QI|B[8Nټt _8}B]tJoHD|~'Ri;9M5 {6ˌ>%UV_̵ѫW%?l,&w;S7 w_/%o^~:~a3`8G]w 4ҦbG O`ODNQ-zwMpu_IQzh϶ט>7OY\?꿦 i?E˨O|x u~S%u>-Yµ5/>kgO^v^#д#Z>6~ϱe#B<<9vޫߴ~CzX1,z<;cNi#^_ǚq}LIo/UiQm&5F 3<|Piݾ=ok-Z0dl,_}~A!fBGsоW ߫<6cׄ'.zkG=3jć݇wzu7Ƙ$o6O@'y1$AW,xxzfu%PU1Ƹf쟛:z_Ft;utWs.m̓v ﹠ߍbĥa w6CnIܾST-u1Sܒ%K&Ol2ݻw1KR1{n&M4R?zki'Nڵ×[˖-1cQV]Ƙ(( jAh4ZV.h4F/Fj:+,D}}6dռ +%Kd2L&JuuxJ|9D+4DѣG###I&i45q޶V/J׼K 0hjE64E{BBBAAAvvvTc,NNN sfddX---]\\]2m^RqiRƽe^ϔ!4McnW>=!!㸞={6L5FP<ǵjժ2Wfdd0UMw}G8ڬ_@| Bv'>̙3zKxPJRP646lX8XoyCxY cP'؆jy)7R> L"^`XQҡmX=eZR 6\zWNxݥN$D{f|.΍uhCwVv|>9C,Tw S@*qϟ ctpjolƣ&v_fo=?yq숼-l86`|gYddxY:P+`EnBm!pcG iEѾ_ ꇗz:Z[)|:=O}4 甆1Ƙg=B,c%m|cpO.{*OdŬ|>*ӧeWשe:!?3G"Se 8*I}WJDdMO ; g{{綑2quKrsT"1)/9uTC[gqN..-aD}WwMNV}V>""1Z\VUƈi5+j4"o#!TʕDfPV&Hm* Jz>Wf.;_)~j՜~1{=,3dHTZoHD|аQ-=! Md|9#<κO/"/䐬ˇM>qnmRs0b,G8z]>d!ҭ[p'"jGDnN>w%r qX b@H⻞cy2a'64@SxQCw"yo/04chU0m}7ah})]Tq( SQH$QJeͳ4g؅@eUF1{4֙;OK:MHS[;7"̾1h-Nmm_{s#섵wl%43111VZ(Wkoz ^,L:XE# :+LV6QIB6/K}`k L^F# U6@ܸ)ݙD+fMpD+Ϡ[<|@cǴ-/oH^ĹH 37ouS=9]0I'Wهn'rOQ҉a$%U0C,_vMRJDN8#:K&X5gmUq?_GGSVN=vI}S'Q{i4Z k6Q!B骰H&(NSJ9ռ7N$$&]\jԨ%W~WJ9ˀO/T K]P@;ClHls\a[h7nLUҿۥV s;÷}I^[y,9"QSDĊ 9kkŪ#VQM`H>*:DUo}>)eomYb3d\` 7tx;YuK4w|Fg1 Gb1ے=}zzHqɞAFh}T p-[$"Qk)`0r9CVnth8'_3pWED= !yլ- W^u 已CΘuV!4~v)fS}iVKENco"j׿'xBEyܚ')q+hw^ܦ\ ^v ԉP麪+Mt_tcK|F&fLǥ;\~m|% N,ƬUs-hWUi#9jIeg~_S8vY4w]7 ^?^/llM˺oh]WF8C؉>nޮ.FR)WWsZ7_Ec<<${{ϯeӻNv}p8ߩBvSjfLԦsOwZjS![ײG[lME1$$1K\7(BD/_֯+880n3kKObbtuS._Om#66VCCC9) =6)uB6)P{!zWmMonU(11N8Qiӧ1ŰSN/]Ja%ZB|0ONZ U* {Ak_?v̙3|0o6888}O-[Ry]DS;Q1/Te2UPU:ʄ-'0^CWFwR[{Su"7O(J1G ;Vm?JZCAh`Pjs<{ӡT!_zDv#k445yǭr *6eI|zSoMxT Bґ7:D:UR3ꅘO:6@?AMo?GL[ڀfu*m%HS^mGvh`pj|~=]LۼB1C\;>ߢ/ǖ97O ʈCkh=z%jےAj˺ ZVvo=Wf>h(%1ҞHLOq)2{^g|5cO_E7W9JH R*-|Տ|ׯB9??.A>K*<qߍFV… RT*< Ht%wӟ EFFӢ *uFht5͹s]ué#b{p_'H{1uKݞ: :.iD,ED[P~vw=ԧnnQ@[QNݝ H*>Y8?t؝sgXfΞ9s9d^=p>ztN|mdn}횤=l3MɄ=I|q![}:W˶R؂z8j+8jVVV\.r8ѿl6喌EA|q(_?X<BD^򕒱{/?b;Y\:دZSl~gHQ-lpG/[w9Ӻܽ?|aQş?ypWeiKG2RUAa!P);26]wvzsv畈IVbbK6Rt>e?ԩ=gf@Xʭt>ݒVM4UDv<}^')anܑ+{*g SW%&Bu!ʦQ+,#OoNK8jؐRTWWloȝ)%"_w߼8}ӧ$:,hRFu-Q.|sDza= Z a,22(7;B*={fcc`BD?r3\ #"""""۲5}`5iԷk(U ݍTT|@9v۬{yJU72ڲCӣvo'GlB5>` B7+Klͽ>˝wVkjjK Ѡ\^2C ˝giCIIJfܞxޓyE6r? "nw^4]PUb_lxV TESN F0|GvS};Yҽ@Պ?ϛ̔J8$5o">)F喝(]};Bٳg?:u㍌~5 ]uӌ̪\8{nR:zpH}۸˓t_Rlll~?v m8SS;6UE$&m8ܶqBRn$?Oc;SrXB$߿= w?~˾(cOTw-LX|g;)P^D`BBص+D ]>{Aʍ ǜ)iA8!P١Dieb8̾7r>ӂGD%`E($~~U>2==N"~5\pMϗTOsR>,0/R>~PmP'n]mժĉ~5 ]uHo]*"[Wwiݥ=q#?URB5qZNܺŋ/_oڴiȐ! TgH H<;N̾Ȕ"9c`%/$vxjԪU+//[n-^[Zo<ر聿m5>#鎕S2h+:p ďS1@?hݺիN` B)5?/kQT*Pa@VaƻjZSa@O>}Ϳ!T#(9u#cCp;P}lgBה1v2q/BbBQ#Qi#P͠?W|suνlIw !P݂;B[rf_ɼ3|tDs7tZ*(%xFjoߓ*& =<<$PUaB5EdxF%Q vk*U= .`N!k*]wh7ϳ(([DՙC7NnaմFjFrx!jɸ6}EL̍5ٱWn4-|aSBU/o~#TGlb8M*B_yfBBj,R5ռւ CԽg=xjesrG݋T4ʽ@~%jlO=ʴ-&ԃȔR} m<->W^]C Vc86< N#j+"rP BW{UZm~e9SASjVd/Ǣ@JTxؤ O_^m- !{7$ݏ %ZfWQ^N/3?3Q2.QQᆦ#+ әI ~3Қl*B^Տ5;!T3a:i(Q9SѼB)ίl@K6t<أ짻*1!`!T'm|iX/"|~`[ {U{w{5IeXzp8\PX-9rB[YF ly&wC2[[jnr5v)\i~k~wly),|1YQ+fuaUfOyI_} ϕ_l^#<Z4BOK2;+#GԁLJr 4ͮޮ"P&v *c1~ԕM$^?EűД{Ӳq} ؞.T9rfnvJm9?A͗=[`vC?:cBtu}&,ms~hpX2>*x4G XZJUؽM?L T)3} IDAThf}R._?RʱGy$Ԧ㯧&#t;oa ݁1ZͲ2v&UezD p4 ;S S}mm3'+-3@KaB?loyVa[2o !Ɣ6|Msoעe:ОN;MT!$k#.~%}w9eܷؽd]fOk׵7ރ_S-\`!@\h2ĺX^0ȵarJNJe}p;nʍ7KRv^aֿ$_:&\Q8?{?<ݔ-f:Ȋ.{&;K_یn_oz).6? @/9x6]ukofxm{鈃M#fsչS=F,1ҁ$ڹp^.:R g -! Ztܣwr8f;B29 Ւ}'Ucwv9UsKERskFsmbK&ZfF,UαYpwnVg ˫>=QƧX.7ԔrsN/FTRQC%fY: FYjj, NILSgih礦B,j((6EHjaT#sw%!+BQ? @G}V/l|z7%+H¥\?}"x`,9q!/tx9k|?Be4>ׯ0_NsU3w߽g;n!RRUMJa!0Y BiҾ&&|MVPW*}SɮW!溿ޘ'/[qU>|33Iw^4_͜9;!1qӖbk_6Cn=t玽8B_ĢD2L̑2oݾ}Cg}? _enH^CS>ͭpKqt+vdYY g?pr|oLE;p5郭;g2uLv>mbte;5ȽWwBYXXHpx*Bٕ =9ʄ l1Fwϩz,:ϮsQ,[u@W$~ę)VRd? X^8 7k7Y,6` };)"﹊)BJgޠ]zϭ7(p3}=7^,[}?kor !,q{6Mfc_޼ -uxHNU&=<8(ZXEJáB)W0]ԭ8Pxj}o[4'gsw!(;w_ R 9-̯+u~?զu{k= a<˶ݒ?z0OHҝhHp*a\"ĺew:b!-<LqP3IZĆųfL>ACӦ]kb5;WG74YO_sQ.\txد.b޲;г 9X6i7p n/154myͣZOY۵[\?mj^ ñZZXwqc>noiبEa۞e~OPWv؁e콿m^h@2n,qC}`bv ܼg7+EL~ Zo7i, INj*(j$56Kn'7 i# =i %m>(#ZSUNz_Ґx\uSc Neqb$ \% JgR6zmOm-U]Zcy_ť?nFܭâ;qgἐ> gϽl^Dߛѩ oί*:Og(bo+yaҖk^{ϰkS\ @Ll:;-]- YZꬄv'0 177; YX< uQf  RZ߿ T^REJ<iKEհJkrCF^biimG1W4|f.`$ҿ~i]vP(*HNKg{0oZQ_EF$?#$IzTc{%y}MoZZ0|/uB)!J$r/pRP`LyŢ#&3MTQR0pZX Q1) '9>³Y՗EQ͚5{˗/ĽSP(SR4! iVVVݵ׶m["|(ˏyє>/P3}iꄦeO$پ )`b? z-QicƱK{Zi2x2KTCҽ@3"$$fs8plhUy<k֬VQ6|~n+Fe޸/\fY#TG;:̽<\,ui{N+~ϥ~=0;wl&T<ӌe̽^^ryOM{,O`z,BߙZp̻1 v.r@oofPϝOKìM9ivt}_gd'm0( cwJkB*$$rNw^m\ۊlv`|˵th{ϲ_DABqAmmHIt?j0|G888H VЭ?_'^nc%G'R6V`!PUKj4JXu4PywT1:]XuleޒqDBN;BզK KF))x P( OIMS !5r8 3C͈,sؑ51;;;1;Bզv^IO%|{BR͛7/3phBGq jkU!_BK5h#TmBBB$ Bg#TmL!B↵UAJDDJ#!f6X<~vxLS9Z)H,RR@UUҝ@;BgЏ.ⶦ<1Xz8;Bw Ϡc[Є-~ ˰xj3G u>-@wO{XXjjԭIϿ]Y3wTg՛ԔHkHw/K ?I|Ѫ!KG\JKtϧ9>+^$2lӛ䔏g=\{,`?r-no|nĜ˙WGYY W*·ښM]]!Dko+)((*hv~$4ƻqlH&Sӫ-#<%ms{PU kֵWUVu0h^ $ݭZGO'.&?IՇFMt xv7kE)67=W¼M] X^?qٸYNlp/Wb]B59BjVfKg w_7FtqӞZoWSj"ifVWcw33333oWr겠s7Zrî/U8qmAI TUA]AO6<}tnbRxڅFckif'Y69p%g3?!YYWviS*[p&%)]CW]n:;5M#z&9>):z"r2,Gn?Bs=Eiq A¥#v3x2g,2k2{*ZiiBd>#dunifcqp>!?[ywOKwmDO[Ϫk4!LAV6iced?b㇛m6(!:~&6}WOfu࿝/7Yr6}9O,Ip娧 mѶUAo !DBNvolܵ|"Y`c D@!DcY|B =<٥rh7z,?GK#-C~#¤=X[]Ӡ-[Kϯ )"&-cLQݺTRR&~.' =RR ! 27Vڷk\|E<~EKC[-5.0M'&*ji) MZ=cVO=i0Sim1Fߐ ?^s4OvR c`!i&(tx.7nIF[%g?e3@ B_~ hū퀾gCȷ7񼼚sxNv 醥7bܯ^GwwW.8j$tB<8}UmФn:lFO_2]=^]/Q/Ftr x6ø1ŧK훡;̀Mw!4Mt8P0бiV||.M3X0Weιaiwp4O4NCw5wY+̈́|aMV;y4(V[V~u\zy[~-O,39IEst0ei!4rbAIN?ݸ'Bwc&02:}9oie*ǒR5jģ*^v?ETYDJvœZ։ɴJ. g[N9p+ Ԝ+]3Ww;U3{ƴs3flKڦ}[oXCRupj*HUIEa?0U6Qz7Ysb~k.P:C|wD]ìω&=U)` {;o]%EUԶB䀬lu! <;yYII]i)lޞNHߗ ~[O`ʃ1DL[W*JPYmIؐpc!ed(S\kV{%b > dT)B6GJIII6ЄNNHſDtFu؍ Xսʤ9ڎ\_ɿ~S 4tD}4X&)%хkBĘ냬N*!T5 7b34MTMx .n#TGk:d IDATQ7~O]\~\n3ѤT _>-6r[vmYy>-q!^)=l3nq{۪Am.c=E?Np{n,BW=C))u8)|ٳ7^v$,Uu5mT!豘3_ouV;UEQ#a/]ֳ{(Ry,U B)y+x3Ԓk7堅O{3T8I (wXގdeăע'K𤂍x*$ pY&&Aqy<@[޲dyBUKZ'ϧ{uݷbx6M|c(x:?b,!,37[=I"}"]aP6_ 24WfKW _w(#35Sθ< ?{#]o[s;.:7ۘ|B@EU!]7aH3jzoRמ~L`شtE6OG!!,m!1B贇'Є±tO=6CH?=T*y:Ex92s>Ff+\v`}R;fߪG?عe˖-[6/5j6nZGjOB!~۔r#1~]iE+vl~G;iBaY߽6xiy;ISVuvr,}/S9E8jvǎ.]U*|DSP2@lk}) rO'2o=J7+Llt:wfz }Wmtiojf3𬦓5!5!GN!D͟һ8Awӥw#)ju-tܗ?Jc*[ j z֊NF$|y^-B!T2dHeo?yή&;$&,+,,\L]N}k 9۷ovI^Ut8vSe3;88T} &z ! È.y'P(iZ >###@ ?>eѮ./44{kBCq(_0NK{*E(&D˗'N~a۶m\.r8ѿ 6pEX,[V%XpJZYYUo֭YeY,V|V+ \@;/\йs=rHo]0*jB %-xv6F{8]1Aa~0B5nt^{M- yRsZ.ǞA!+0ް~ ^zcP6C;RJzMڻ:Y0BbZu9@?čf -B?eff&*)_>e\yE!d2I=z$. 7 |>Xbp:'q?%&&J _1tV4U)KJ瀼N.=ےIw.DW#:^=CԾ}ě7%MO6l`չq+o߾5j7nH 愇Fv!?9BUG݋T4ʽ@~ea2ȱ BS傝{d$pf_ܬH*u(5C-L'F)Yz˱(гR>'06ᖜx𣒵!^NCNag~(p8\G@Zk7Oέ3K3%yQu 73[; SY!԰@p;Q0|GQpm٬;ftVQ,>tNE3   %t胢R6Z__@UAD;vs2U*!=(rh60FuJV(fp%z@ hxI{;F;B!$xXfo׉Sm):΁ .|Yncrc>$ 4US*숌H+*$#<46=+O'hó&f?BOނ)>k#+4gܵc̹?_~3$7!Ct&!|{va{/ai5wУ^> Qmcm%#G9»!0`PǤ7j& F7^e

9G BunE3JaޕnP #ffff%]]؆2݈!<:¦c~6@Wq^fQa_JIZІAm^x-N—rer#SUϜ{gHguPS1r}ϨU2In9j zj*VS /ryAF}D t@}c;h-;8H>wq+vz.ᗳ]-tu;/YbP];B@JBT ţ}zRQ p[?rTs8jk}{s^r*Cﻇ_>`qiIw3|gzΊ=3e\A:*~oSUw'@G<;~tjB<C̖wZthVk!>F~:3Q1KG\JKtϧ9>+^/$vߜlď77V#$#T`BaQkq͹|g!S?; Dwuaɨ[yK>wTރ3Gq3̩_􋪤dg)J}5b)gg0\کAt~wI%oP 9Rl>n{ڙӢ?ɝ&-2:Xn<4AhܜWGwP݃# Z,|iVԈ´/iܨU4An-oJꭼƶ*@ BR؛;oE׈duTҒRPUoiBN\dxͤ#[?zO̗}}m\'ٲds qwiNx)Px@^ 5m VŻc`/o_*xqnIN,MTVSe2T/)h„dvӱ򣬰x{78y!^q!TS0|Gt ן]$'*Ĩ8%˖r,rLx\~(iA߅omy>hyly{b]>I8O՗ N e^,JR2+2}ɣ6?d>?i>iU(Aȡm$ӡ g.=[qRd+(v@_k~!ۛ]x^^+lĿy ,eSksUZP]]bO0|G@+XCY9K3%yQQFE)RmR3᝗_B>ӲRKỵRn g߽:w/|QGXwz>OjTӞMY2.饯m(Dk5YiZ=o%D)tVj|xYgyJq^͟J/\P $N77:0/#_QCDDV&^pD%%=3]0^1e_G^)T?hB\0|knŽ;vT""}f2+?JrM6ޠ͕t O{L~7!wwPrUNs8\8I h{Τv7 +eW^v,;J ;BUwP1aҳsG= h>ih7IS[dU9"WU,IvLT^KUGeӰQju/n^M[ZǎD!Te#D;ܢIvY,-ÜWzT«4ݖ\gA1m+)aQyzP%@|݁+'rP'2{? T! B |{Kά=d{ (+VX;wRAolv}ٌ,g%Asbb9?̡|fcg'=۞=Bv;BDݐ}kyJg:. dkhFv=ىbeff&J#~޽5 b2_-I?~SE!jP0إ}yYhBL];hQ'n6kxwU3!j3 B" K*lܶs ͊ƝA!`*"Hx~oc2Rtio B6wkSw_4[X{.&^IBJt*BH<07 E}@Kl+B`!kmpFj5ތOjTӞM˟d\&;K_nQp~[7D|wMkIcgo2lF вj)Cw{N{{NKm^@[h -вG!vB"'mINc;!ǎwqeYzeWy2ޟ\^Cn:z5k>2?f =Շ,'is9oMdܳu%+%ߨ '(sO9~1L? KB;Px슶i*bgnf$xucM fg0~ع9Q9 iS7 -#Z4s/s53PplƌLW|nŷn4  IDt3u5S:v  ^G*DuUAn5(޺p=:p@T;!箽lh/\^(w{JC%՗U5:?`wHWN6i}s8p⁆siKuRqsYօg#C|$^sg*wQxD "rrij'-wzpү/|v4#@T\-pw_uD1[Y#8󤤟x < T[S"ʬ:}kɊߟm j ~7cՑ4e8^a ^*˽ᖶ|ˆ˥V/7g6;XUwJMHdOp.TTjio~m=ʽm<:¹83C/b/ںD3xmWZ+TMeIj,j:'N.sܶF6gg dʺ3粟KRZ`̠?EUO]"k&"E;zzzMW)[QunC1|$ t`xX[tȢ[qUnW]ek& o޽{ŋyE("JKKy^<JDYYYZ֖!.7qߡ|%5~PW1O%$]-W1.B + IDATfRͦ=ƀ%f DפgQNu[yGTe>*W$ZSp;cz6Ou>Ǘ]&J Ʒ+~so$xh/_H$I$^zQZZHw.x"q,2 ӧOsM mMO]h4F X:l3 ?`BX[6r.#Nnan3ߎES[} WHYV]ۨC8yr ?XmL˔@$/Zܚ ;&$Q)_7pdޅ(Wh~O cr pbW^jPĦ+妋D$mz]=) x?.}Nב44⯡mX' ڒKě~iv8a ޖ_+jL~S-7r8$xhG˘ s3=1rΩWrKtŏna7::yAxyzLjj*EDDh4FS^^~Se|aN{]Yc=6fi/Zf{t dOVDrjHmN][T~||oę*]޹_}>ÓSU٣숉=h;xܰ4keB_foӻHrA5DzqX(^YEI> zk ZD$js=} _:nK/nu~5LҒ~sBm&߷]WQQVB.pr6$xe^z$xpT*H҆dϞ=uxkh]m`UM1^ GπWf6AUU.*==fÎPQ\X>OPzo6f|lefA][)Dߙ ;gYZI&>9BdyOu;JFvZڐWs.RtK?TDʒµ=\8P愒lX_ HC;@jky ϸRyE-ZUb^VF{HB|z̵Su" +!C- ;rHwѾzeC-x8@4;@+S+V<*[ٵuB Z'Wcj5I9F+m×\f;L" ]Q 꽿Dy̢%DDqt!cnG-S*u)k\F!RON]lheN]TՊˬ]gd"9%)D$\,UwDҥ"x+ReC#:u)I&jfVxxCQ"^:9T 5~9}͘*>EE_QVN;#H$R^,1JB}3|GOf6Clj7 ӆэmՐ).rGb”ީ)˷*]DieuӽcU+EQ|.gs]OLy ^+NS,a(O$"MZY5\ saHmMe˥˃8}-n!6j>Ғ&x*wU u0Xg/iJUDD>'H<#Ga2X ^*n BV<112ǔlkݷ$W:+{GF;+F7Zjt={O#FdˌӃh(**M3P9ЩúTloaMX5[yՍ`m^'ub%RYl,A$\-V)>ޣ|˗S{;=?Hڙ)[0̧Xo.89jGڱk }Pݗ^k8,!V* ""m~씈GL>J$eʲ-r !jaA J%"ыUQAbeCuSrjUk_1~4U*>fBVE1&&f]ח&nuNZ - %!Iz屉0*΃i_D]{Ӄ&~"]T?ԆeaBj)J]Ĵ%]͹džpmʺK KrGOWQŬ\ÞkPTv%=eDTZ-]Z)-Iyƒ݇&n3|ȋ7?8(Й~rV١RFFƶmnG`Xnq/zvjF~EX:ӦMNK~;@H]4Wrrsed|_NmqwMZmyLÙQvAmTS1%Wx"Z`A]j?W2_pDYsY0^JVHB|xՕaUj""owdyfw"bXiۗLٴ{ǢdF ^v7l1YYUu^nęOUߩ#EPB`!Xxca{3Q_.H<[AL><۠;ɜZ]nCJr 7j 3gn:: CVK} F illp736 X^ZZzȑ{ ]e8)#ui>g%#;@GW蓗ʃœ|mڊf{6֬- fTbӥg{]4%OsQŒ*.ɨwf]ۡBDT^Nſ˰? È4Aix˛5gneeŬY|EƱ21 IN[NMneSH7`]zr pg5Z}6Ok3Ur6;{ՙe(86ccF&,I*Fs -VKP(H$Gv7Řw^zUMz"]hqkG_~ߏ\^]$. sP⻕.`Ah;@G΅gTh^2hc'Ԗ>)۾ᯓ%NCfw)*0[)M)\L9rcȌ{-LɨgreTouh`yyQuz@jf|B-٣j{c>c￿Ha B9c8ݴ~(~C/uiv: wu;Uqru9 YsK';e᪱;O5exT4G]1VяdAI9F+^9w*p[nNteffQDDewt; O;{c zfw{ u޽111SLd /s…c!9w2Q^-A|X̵ubMA5Ԝ\Ƕ>ح+u!YӾJxQ]"[^K%sXuEݝZ4(A DQ8tװ1J[L&?1lذ`WWW"{$'':T*JR}.o.>nޙ б+.񃜒kXA>Qʝ-I\ /L;2+bw7HUQvrӗ+2Rr˫~LDīU|~S h%Њ(#POFק:݄~a]$,d㜜t{jbb#<2`WWWDbߍU6 ֫#бV_P)<~TyMqǒH?(2ɟ#"!F7{`(͍M) r]<VwƄ;0BӘ.AfH [ "٢;5I$Lٺ9#G4h0...һiv3FsLV< wN[\M*=zesxyܤ]#|e̔<~c !?.rFTꪂ#ېg02 - !cC|谄RiM)RYgH{Fӧ;zslq ;Q7lmO3)So]l(4#X6ߔΓ ^ #A|""r\l=m|j t.R4S{5X[g'hg'\JRD"}$skg;ٺ]FXlU7(C;C|f[L&z-'U7UnWkt/Vp40-xp_.{oAMFӆN 0'%%%99֭0G-aEօ33PR }aYA˳D՚[7,wŽȪ6nr^5w "".$L~h-I«y7;C/}كm}Mw(9_I.FpmS IrUK@acQ#t-Ŧ`@ Z\*z7+ohJ0> &k 5FΛEV}d A"0| .`"<vy:;@=_}Y-c8s|AEk;28_+3*c=fg{1@Z3%؂S=@oMxzאe DR{s$UU>P(.|}]#&qgn C~8yƋ^Jiן~A}|^5EH$wjfNs/ 6M/33333b{a9y:2}px|vf~ܴq CZϋ%?2هH$RV_8KB}3|GOfaAxP}^+Jec+_=Rv'b\URUjwϦ]ؿ|a]vaV7L=['kgSʂ‚DܤDzjDQyICZF w6}u+:zHj&". v|l#]E{75¿ "Q]UGϜraO麅Y^gB|ә.q|Yk9{Sɋaܣ-&"P\5:QQ>w4l |ddF14AwT[ #@G׭Y5a[A|y PJJJrr[a'Ouv ;@n̴uom!wVSk `wz>}:eܒV^z+p.Q\.( 'ZJM]n\T7jm8aH< ٥"##5:$<Ao 4aV3@$Tgw4XŹw0vl]_pzϮ$vSzXmBM9u/r=8lWwDRJvR(aB0aӖ̮3` k3w}+a '.|Ҳ#=DUceDiĄ%cC E 65񈝳xXk<Z!'AcFXMMԣOfaqcFFHYG2ۗ\>uIrז}teykl*V?kt&?qé|:.7G҂RXa E^%i^w#Y"bj IDAT{wIIazQ*ҳcfÝ/NϣiݦvvNIR2=Bo_<kE}wqb8װ/8N}\#9,~TLW7):;T)se9wCsP.Ze/><0ٚ]UWpqXeYV~&i%scvu2D0ngY 'JY^sd^~V[%\bzyKYg!}YV,P}hGj*{_PQ wĸuF|6 ާY|my<+3v9Z6IDԗ]Y g@56+_+t׬29dL|u_.EVـ>1v Ѧ2*-AX:FP_VY+%Xf;hZk8׳R)hE"+hmEt,p'm+W3nqzY+_rȕ3%kV$C/(w:hRX)+*?yP!?qۮCiSzY#ic*./ k MDzI{HN=oIzRst:'8:y yF"\]hxTg`Q}hYЉV Dvpc.l=QdnJ|#9A#۠j9)8"V{@SRt5 SJS@tgqAzR'z JɃ8'~džY _R:"ܪ1q9rd.Es 3aTW<µcw֒=)qh͛7sq˲1  HWt=m8+jWw[ta]uuZ-gZnBh4SN٤Ƹ 2F["n[a :Gx0ښl 6[6hG;9EYc#bĬ6p;`#fDZ٠zΜ9RT*J$_qkoߺN(@g`7;@|y:}w丁AƣU8~J?4lFSr`њ> gǸɀ4йhJ0mQyB %V'9'uHG#\>3Z?qe8a@̲hyYWﱓb:)Me_ؾZ psDUerOOieEh/<C8$EpTU~ؒSj5>sRNъ-=CC|NM"j[H%v wWUUjw7wp$r[_nembu?&Q#We-Sc(S ٔ )i.Ax^WdbyFM ϯ ?=/'luu1D IYGADѡ*|`8nϓ1/nkje%^>޺_xL ɤT].,2*(Y/_/R "EPeYw@|p0b8>q`O/HбBH}!̺8_R^)2_p 1Uj"I|%iwrc\ir凨*+DBER)kyb8* Ϝ"oWDB١o7]=sw 8Ii5COضoXDhȗ.?[Dyzq]]uטY#s_cw}a&''3*y>; @'&Vjnϗ0{nӼ 1/xYb5ꮆ{|Ǜn6h l{n  w`7;@|vn  w`7;@ ]@1a^1>/b. 7X\'hHrz̽7y5ڪ[NҢ V?WSVe|z+G sP^HܟSY]Z]>}lCezfjS>ϟ/.,'"/5ͺßZX>+/ZwW >9HJQiν2vB_)_RT?gtw~2AF-)u} x'QK$\WM uaX%"6h-%{QW +U]sd G#+G%mrDGsꗬJBIQ_PL"H$2DG^Xİ=N*%J}p,CDbey_rb.+<]mMo}?cH'"{‡ W3Brs!M]fF~X :9;j&]n. =0oBx6/bffW\]u0-3ì=._$">.^)̴PS֊ BwQ8딵fi}8fY",==3$ԗe8H,];էçF tě'222J S~T$YG[ztˉ!w.Qw9+ v$=C^Twdm"*kjX7P5$cVbUI6󫕵u% aԗyny{nn&wqϾ%"r8ԩV'Vܭ-m`Xel*Wv̩IﳟSzrGvf3gΜ9s^>H|*nŞT󃵹n(pZH[~n׻#$)*1#3g~ˮN^3ndۋN|=x=bs*AT}] vͫֈWgl!=Ʌ}h#׉y|ioLC]}=_7랭E4)r~`p׸{C4aM.םZd>fo!^j\o&n'VˌLE^+|%ݤ 5 ֌V4(4wr"~}u5sc[ɪwuu;ZCmo=\>Y%X]!~~WpQEDb7Mڥ$d9=ߋfa `ҪnM>(?0'ݺ^EN>x1?2,*z=C+\-+䧞\_ RC';_Vq^ 婧^z/v;O(//H$ DZ T7-_v\x/I̟3/늖j~qx)S֋$}䣷rnX~9[)l] ?la374 bmMUһ4j3׈Dum@ ùXt"~R],_U^t K7Iɵ/ź3D\1#$GNZcA|eM#t|׏ugN9oQU˅p۳\\=!,,j-o=Đ;6xXyէw#1""IDϐ2V;)#tpkUʰa+VVHf rKs.~7FQ&?UČV;wu߰ ?>oG3tEQ5] wwA[AP4tŪ7>xj)#"&C2w#.nS *ywnjӳ\V2NgB|e͛+#tpB_ӅzFOs"B-Otz[[`T ,xk?MWu G_cUuweAhy=ThSS[vJ$V7M% q!9?o,8FdEQ>ؽ&Fx9j;8-aYM6pukb=Ln wp È6(?b&xeNmgn KL 6fK8a9遞5@D111gϞu3ӧO0 ؝> 1=ewODCrM Mdgg>#Ml1<#ew [푑ѣGJ:wwiU֭ٽou{ ܺ dțfZrcnq%K|=Ŷn1LF3 Sc^Ewx1*In!wԞr˗:3ĉRi{nI\\ѣ;vٝ:3'hΜ?٦L;\\o;bZ+3Ntv|7ZQR7 \Z IDAT78lڳfW9 a(7{0 y>ͷ|Wpo: dt;eչ#!w3Zynh!EkpR+cmh-w`7~w[7;Uۺ `M;VrILvdm03VV0`w`7;@|vn  w`7;@|vnHl.## iG>>>7\φ!<pk w"##a1cغ!ԩS-,g p8ֵ:t u+aUUU-/g p8ֵEii(nQ<F NǺVB|@x6 Gc]k t,pbK0gu7voP EXU T UMҳє:ᥡ^?i:~12mYѧ zDXhK*-o7:[y:߯ʼnn!&?5۶6 yp{<(p~6g}$b[)ZQz.s"XִmS'֞v{6s럙>x xFn? t*<.o}Q]]‡,xiSz~nYBw&KE?fLS秝*f*H[=4ܰd[?5V}nH*u7jbi< ${AD,` *rw޻TWE!|?B A9cfv7ݥj7"G '2gQ{M쨏]mV `A Y5U8B~˜f:!mg0/j5McbbWFk!nY9‚zh5]7xˑSS2'0og9YkXZVOW\=Wmt?ywvƊRt ~,Ɋ]`ѩϼBH]HSYI|7$:|tZ]ʷGk(t7|&.}5lF\}KimHH)tOs ݊O,r4VSb;,J4紇 _hJP Ey KHc gMg@܈ {>,)٥{&}̿P[ q]ʋ'? su*OBmİ&/|ͮUq׌-|~&zQd`*ӒELJ_vxdC1$t~aBgoĝGֵ@هmdyG&DwKq;0J`tR% ?ec1D* hvsN4ϵùY P]L߀.gB7O'}=ol(C:tS|s6!w6A;?.bL[3gx}vn+OTe'Y%u;Y2d2=mBކq=yN:_gIDIЛF{JSv( py;)iMD4-4֪`0jYuv qtc)ɘϜj/a'.*$M&6|KZM2],ӓX<׷EAdg11AB;^a Qz{L@(թ بe$93L %!F+kI *vI:@ұ5jب,#L9 7'J8Ǽ__fuz#g V ،)QB#]wmM'˽r8CA8 $2Ӳʊ"qe°Lvo#oTǟZF+z(aU|{(+YQIXAj!"uy>B"'3Q 1y$ 8(,GP,I2ͱM .*Am~ei29էi*HOIkI5#IЋ[v] Ld㔒xVrZZb.s1Em_t; C@w i4Y fC K-?((gr GTS($I øw"0ia18>`gV$S# B . /J5J9#`;\*r;ӋWP*S,rݣϱFwk'4LXixb4-7.Ë'/B)m9j8q>X5,ywAJaȟN5aCQ/j1 ]zڼ8yj}'hIp?(_+@L~}Y>猥a3tD4wa8nvӟl0i\)Z.P*+/rjr< I9%OWoӌ]/Gػa- $6|᧵8krreo ѪO9y7LGK?!,vbG H8+,{Xla KsK:uVܴޯs|p]:U 7τtqcb;#p lhw}eu˩7i/vB(?>׳- Ytq$F!:ɸ_O ݮr|mwIu:ca.7JѴ\U~55>`b!4cÌՔU ^ZuxxYv[ izBѐ7{ U5M ɯB(תuR9XP!f?w)6yO{D+]]־H4n<  ?7&t^UMAuSH^(J;w4Z[ӵ:*\kX:thҤIuu H{gmmk 0`I/8ѣ~f ~(t3qTLu/_F#i/¯ۈQc1 Az0͢zCAJj, H[i=JAA!Doz֕h#A35C_ EA",t:IW(8N$Nb(P4X@(}GaA"""0 k뺴 $#""h4C HGƺB; ==?u]ZNhzzz_ E "Dۺ BE UAAi7P   JAA@;  (}GAAvyAE```[WAAVbmmݸAc[WаkjCj8Cjv'O4(}G!!l* g{#F bp(V bp-V(}G!q@j)F bp(V bp*V(}G!qv QáX5UáX5\  hAH{ĭ ?WeV))osY9<(֭@j"bU9Fi4hccլc~kmlP↬9s"^v uK HGpR_MGFW56W6lj nkY J3`S7/Ǫm6SԲpY|[Q[WIxFǵCVo=|<;^8P} [z (k9GGdhjf\! 7vpٔ1\!ƨPJKKӔt,:>3_MN\A&8HJk.)Ł:~+5=-AxK=&%E7Z{ׯ_9LqWf>OɁk! )N?Dm]#DD(w)|y8Jcձ57qT jqXUc '&j/7 .h,;A,cm1Fae}}_֑׿+l5ȌO9"HL^~~~=7E7 c}r 2 ߜ?^aX3թ̻jlSB;oluhSRR}PS{w=:CCt[vCԕ]bCK4^J&{ȅBH#,tTXfCl,T\S]7p c c -rdSmյ6Ӕ5`tzͼBɌ'G2QeXB@Hf^u]tXJZνu`REBH>:RGMMۨm3Z!@BF!;O!3/^u2]Kʢt1RV0qs !dΛ=c-4UO}x9d,q^`māsw@3m5U5ms ϛ6*cE$) UegL|>8nGK !jIKn>b\S}"9:ͬ߮0 m UU zNmÍm"T[5U]Qx?IpS6UUQ3M !d[uLY}^,& v$>={̜ - 4O9V _m7T3# ⋫(AFqƑ*Je1}XP0Q⧶^O" {+iiL=OV, ɒ%BK/16_1?{ 4^j0H7!*#, Kݤ)R qfvކ*Jj ׻TqVɻ}ob׼xyKBs7I{|ʩh?{]|Pɪpdߪ%W<v%W!:\plbٞ?*Y Y'^Fǽ]%w}/!$n-[u2IӽGg~;D򵅳} y-"԰{nǛ[P!dܥ 7>8_ MHUgҚ0貧 _g\~ܭvXk_<7ZTX\BgL*`gs՚_υ;t}y3cXzτhE6]Kj,Py12Ϣ"_YWokn5}2gRL <<3H_g]Lb̫I'!blɨB\k±mCW? ub8uw?E'F\ua<^N-4Ww.njX%BNȅ[Hy@3߹Mb%b4I%2ȍ<:ώb;:sRk|HEBX##-5T]2W(}r;֔ #7DzP_ct o8a17$Diw}H_է?+"h(}G!BA"BI8;FOCCCC2$l)fLq1VfT0zۛ䧤AB8+:zL| zOf&  'GԘX6r<==kׯ_\$I!ymi\[Qы9+SHNF{ HH1s 4=$c8AZ= eqgNJABjQHC@շ$FJQLOfs3n4u^9e,ǹ2`ځZm~vSm쁬!'MTA`I14Ec%}n))x1up g5Ɋ zrƥh~=8w?,2T'j˷$TdIn#b%ai XtC q↽m$)mS53)ScPU"))vy8C߀Ei-k~5^(zqʤNTЬͱu?n=+Y4Mɍ !6LC49ӺHV[V<S:Nt3{+1HJ[H'er o$HuTCٹ0rc܇+(Kc䗲zݎ=Tb / VƷtwy>A';d Vkt =^]d0͗Ovl u1kDiZNc) I1$ؑ81wLܷ'KATRD^75.@ 4^Rr='nݔl=L 4L^TB*Fz֯(d琍 NԠ/I߿Yzyʌ Ϟnq,WVZ%eY"噊c~mOTlǭ6_Xw=x6LRJiO)t IDAT=eŐ: eK22Ϡ/'A]C\ER+LB.dK "Dgz[)rNQFjNY RpX#{g%AvkID|1PsxvWT!a1dRxKyYyڝ4,~w}rV!#:+ B8KxBͫ:zڈr\gpG%`I̽7 w(w' y篾iUi\L@ C~[a 륫%}"SJEI|41br4_%$9Yt}1H{?] jb>frxdatR*uRrͥ Q2xP xy pi~td>=p>a4G<ܬ7φeԾ]:$a];{eȒE#*=y{[FNǷ$6$Ih 0ÂAnar)3-X1 p=a LgC-Huߌ<*ЎCh ^c@*z+  n=#fu0p\,{a @C\ZD"5ŗbvYveMP Bb/ǵ۞VJ v#:몀U;tL~v}ܒkn@{?uIttmĻ6Y9Wc"=7[l(3-uMxQ@H2uy?D?'&_N#!T<ũc;=Q`z'X6rBtu%!tܭ} v.QLR4.Lut :9l/q.|k,5+#$XꫪYhŝ)Pe1mNߺa L’;i\X3BAF˼4FCGݧLqӹā&z?<:U kqj:lL}j0y!c'6w۱oV'/|By]-yufh!}󱫋xN#GUW !hrθ0ZE^^no,,! eaR 1B@smCy۹׬~ƫB8VSۼF >b>@\o& 5Nº%?K(L4ջ>ZB@Op jPj6L;+K'/}4d?7\3ƪ_,>z$J_Ai17LfC=Ӵ>i8oO3fta~~~PA@̾:!aաA ֕nhG@ՓGEEafoo:*>}խzlj O]qZyyy.F,--[>HIAVTO^PPq,,,^xQ Be@c( Hk'}ȣC:TLPxiB4Maaat:#D||uҎgUߐX;.xh xď}N#~/䐣hv:ny_~?ɜw! O#w/!Cĸq㌍ں.nHkc#7@f}8p4]DhQ-@H?@S~:PZT}+tNTIbX~ $!:_κ[B2zX+zC|UM-gXR1j.5&UTG%_N&'-cҶ0y6Sf8,ǭ&')6 )I9s.Wd#s*HIȪZN<(u^ 2aR]*3Iݥo8.{PU?u6PLPx3 7oFDDL8QLLR pkg͘K_sʕ֧M_6ݳ·\WdEzHWH (8;nzQ^t8gRCywHւ'/{qo9vD Gksy\eG@b=M2Gq[j}_yq!}u$vco~)R*_#"M엔0=k}_=mvDtN=7dY^$v, ^.<Б3Я`ח bǦo Ȝ4{본ov)u^^Vhp8뻴7Aƍ%$$/_޶U{/U؁AzZw3Ϻڷmew/ely\|p.b1Bb8_89c"/ [|*ce>S'fu5}pn4fw ;zjz^h,ϔW6tX4Hۍ.&* 9eBF !E^Sе[B]'!Ij;I q"w)\p!Dn?%cqZ { ᬡ-%lG:~%$ %&S^˞`! y Wk(TFQ ~sdgkHb@gĂo_aXVy(VTةb43e=FqZpm°d͇B]k2fVybR|\ca=異GsM]ͺzJ'EO˚F^m*--m{=Mϝ;WIIm+&8d F85\"Ͳ/~ǧt3\~hR)[8u_y8eN)Sλ.#ҳ/ wC^xĺW i\y2|Ų /Rn%kEƢq#cccc^=ePuYNLra0|_r2Dk|Bƕ!V9&M$$!$r94Uc>ō#p! 1.RIIآjDZ̙?F\<6aTffm{u^Λ}~gFVZ)]d1BfÛԷK)=ďS8yLO\#~pʦweydOQ_Nw ֦?7 =@iLDVDNzI_I}#_~ lc2\3Kb-+Gge~}Q)lj+>0cP\gKӪ,z$vL4Oǝ;XU Sd0T;Xz=mON;bCL2)2'_L/{96Bn[Ay͒74&d8Ϻ\.֚-nYڟAH$?25#4Ci޹PyBXuv?0 ܴiSFFPO &b5aNQp]hČ>TL|Rj| oFIaL31`@?뚝)7ן=ݴPF.pf/W[ ^%>R⺃sڲἡcVǸYjVy+mW][5O ~+/%s}ld9,mWmrISCr$'Ȳ8P8qQo͚W-X b4^Ψ>J"@Dunٱ%Uo*nre4M&gƜnWADݽa| Y":DB^qIf6勮Fe,~hoUwj*(JQ.=hgf O|wMUłzh5]7xc}-ԕTt\W/cw_ι[w^Mf?K_UYEȍ~I)Snͣ-uTUm{EWu԰s,y~. {ɾSdCȎr~Oy$o+qgVM,ԺM>!Ie~DgݚQTTFz.pWVR3~*^cEf;Ol7ᇇyxC2azjjZfVs fMul}/Ͷ浪duh@nDh&Dq D-iX,N_GANVLf(5eIIUHQ^i^qYʧA~ ȄKsf=2bթdH.*·}ruj7S'>Tǚ߾[<:sgNj_\%~t2\~:6!x="F'G ۰'Q2}Jy;7!Nx, 59>hpt |Un%W=q+$z_{kRɛ|bHBܫŢ7]l{aqO/z'[ٚ2ڂپF?4?| boa/ J ?eߤ>ߒسNe}FO"0񽷝GQMXxuTWI; 2S{Bq|>1eSg PggN䓰 !`wX3ܻ7jLhSDZ'8XY>C N*1C.?"< yDZ17'ҙǘ_t3ćbCc(Oy[!S]MSf H&ʗDGv{1?`RդM0mgLS@ENp؂ ctDAǯ$d\qʻ^9̷"Xyq]A.=&ÇPo[F9YF\~C1/0ґJ¢2='>SR1 6NK5EQQSNKd{2`I:u7!gxhhhh'%暓Ͽ2]#*\6$kD?PsyW_t㢄m/qtc)ɘϜj/ٟ=>o0{OF1 2)BnУ[⸙wZԋITVߥ3tn<ɫ 8O\9\1};c FQrTq$U)먩]e1aKJ"Y_Ly3e(똡ŷn|2_y0 7n yFzTI .BP7{ 1K4U]i;:'- F(XK85Y#o4h2Ab Y gM`[oy ܯbZB DS||lrkԹQy3PO~{022wE?VuVԘp5!dPtCuŕ/%;܂Lx}ʌLDe[eޤ^KGKKKK̹D(?dlpꀃ?^ە%#)0y^b=|V;!Z]NTʎMF+2#q! z/9vO/Ri2sofdYءǾhj7_| =&CQxgGz(++o4dc@ ( N6?B2h?ͱݞpk>M@5%âϭ\D"]=QQw]pSBƕi{`a7yY/+x%vLy%Uw2l%jVe;|^!)wK\_ːU,ŝ("2応dD) %^q^O^w[ָkglU}&ILY!QV:rKI|Bp:^^-QYLyD$I@B`tNp{]FU"*j| ѣGۺ :8X >21j[g Vͻ[(/^Lwx"P++Uy<nN*>d@۞V~ؚjzp$mիSZxaZ͍JU0)m᝖f-5C1])+UO~zUb#h;bA @De˦jwr;v{s?f:﨤̹i+}Yk[rBZ~Vk3YE?x,[0z$ɿ.׽M1hq)N^^1b4Lvj&51`^nHJ"VPKMJBi $%%ѓIa(2ߧPHN t dB@Ԫ* `tyC! bYp*k`ft{ܵqĄW &K-J/"GTm"VUQ)&SjsUUT^qO1)+3`2r iGxfFVy1-.ssЏ#TxAr['':ɓ, iI՚~2+1[r[Ύ5* Acp菘`Ɲ8Lo'/(ۅϊ!@K»'tڇ޲UvK&"LP0~j .>9p.H2|K&ǐN(Bfp C"Z27>{Ba3iϞG’;YWƔIM%zpfWt `Y6K9%m-6x7o;J S'4_=F }Gˑhp %B{B벰pl is:Sh(wo6Eÿ]AA7!F߾a gcnO,:;H_C&vBba<1x3ndoA`e97ӵ0)=xjHzoHsfpycਃc<oޝenAfKpC{VVִn~'  H]Zͯ?4n JiBa֮  u}A'A@v,@J@Z V@D;  "м͞pNo54yKGFW F4 :v9=џRu/JA]hZ8}"YBEAf)D=cK=T)a.=,'`U+o_@'  H{ ;7ZꟅMRW!t륯mF$Gk(t7Bn*غ_kTsZ*ZfOꦭc1r B~k];*}jپv<B~Ym_¢o9jt3rD1n4d)iX;_6Pb*;+M \4Gѳ\ZW!)AӰv;ptj>9$뀣2d<^)cdhp !'xŚ`B]vK_pUZ?\\\x;|&  HGjOO>+itr|/溟E4z#1hkOsLtC a׆T+,|IH 3\#S-k/(.A0Yu|LH|es^9 WهmdyG&DwKq;e}=:mƛl&<#SBlqբٶō .h+Sy>RAo¸u|<:sgNj_\%~tJ&iڻ^ kCy]{ҳf/NY<@Ut=?zG@Hյu$GT{;T{=i~?%*U>RR`T o4h2}JkܾT(6hp]JUŪWB;^a Qz{L@0 2蠖ąů+Ow U-OMۻ8%D]<]4`iT)i5N` K%{4ecZCN9 HKB]3ǹs~s{{;^nW{]6nj_Bjq;~kvZ0$LWRa8򍖝|ƮIGoNvn< ޑt+SNG/6,6Te v?1DbمzC`->@<<1|̧b_Q'-|"Zdac<~^Ԉ!@,"risFDRIQٯ'GlU0ƘTUp"zGDZvhiykKJ[uB=yle~pm Da@bkكe1~N5Oӑ#ɱ.w\.$IRSytWz]l'1oQQv?= RRaNb)"K*D"c왦uԔ׶.8#*=KnJ"5^F*l 0&ql\K$ts6\lT3&8|#[*) XvFuuMPDUIJǢd "q5fqFD&09;f۹c Mw[fGKq5LsSy&jk9c o)-\W)kj~YЇ^)VsjY]j=~[fyaJj^tU9+F.5{ut7}]^\3癛ӟGc֣Vݤ+я!|ˑS |nuޟ10%O Cċgu>04Ⱥ_0vpW縙2\]FvСC}Knb|DNm*;4yS663aOXTb2L ~1O=wh_6*5>!1äwϚ;<1)ͯTB>uY=KYfc6Wp71L>6/:8n]TMD$FLyڌq-$"8o] M7ͧ4e>,L_>\ ma_K۵k͟,_6/3i+[͌&^|,K}?7Y1Ԛ*6j{d&i}L; Wcov;66VN꼼Dw{Ic;.z.}f;*6c| [v,W7;@f<ݽ1xu;̏S<ޝvO$nV7>9;cv8BWq_D}ptW@v\VW=Fg]}vpQk4ι 7ݫ@EιFz!?']RY,1[,Ju*C~N ?h,((ϷlFRƫW!sn2dĮ ;@1Px ;@1PxWsIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/managed_dock_window_side_after.png0000644000175100001770000017301014623331163024670 0ustar00runnerdockerPNG  IHDR9JsBIT|d IDATxwxݥ'WB "]zQ"g/X16Pz "Ҥ^B]I @Bgݙ}wfgM **(}r5t{N_#G\eKgDEEz*B0cƌS r={nDDDDDDD͜9QFN 7 ݖ/""""""r3EEE=z(r4{ԛ/""""""rmP/""""""r Q/""rKĴ/r [.' ED:f'yZZ1Id.xQqG l͜Of˯jA 0RX7,{8oNyɝ=M|YbWS7-_[T#zR2959d6e ""rq| #Gi}7oooRRR:u*w_R/"")ms`7i2WKXr18>Z(9-aNxxSiϚcPU:=;tk^69ۛ¨ݠeL%~8}%4S/ """ OLԩSׯߩ_<Ϛ5M6aÆt֭*AVC9Cҵ+vadH033ٷB\Ym&F|霴.?iqslVtՉf~ĬOkRi=M8bVn;FBJ[ ;бqn嫥ؚt$L>SyA~>}8yd*R?x`&Lcx?22PEDDJ# N8WmEۊ.d&qzNdg sx#{k[v6G4rǪsqͲw$M'v4=WBY69:V7;8GhԕkWu*ES0sdXkԥfg'Kÿr]jT1i~V}jl NFgT>qɒYAuqڱ|'Qo\> >|N7G>馥ӧiiiEJHje|ddd@~/R"""ɑA&|q׏bG]sm3??絑͖0ѴE&#&mj{Lۇ@m(Kf+1ٻپ, ws205瀛(׭"ҺSٚJ .)9ۛѫ_2wgX{ '\k N4f:4%/-{a,Zi+?H R{ J7{w?&& *p!*T@LL EJH~JJ >>>ٓ9sJ~Z_DD..`vuiHuەS~s>p9 _+>ڕyYOr"#곕u6,Ӭ̔ OKJ ݊0abI?ed`ϔEZdW2ޔqdf݄A}2Oe겼MgNU\x tz_9]H38tW_~8E/))`6mJٲe1$%%"""J{ױ) u,pݸCL[ݲ01mNMhdذ9,Hq'w<\L|6.1j p ɔ""rU-t ^#}8i]Ҭqqqԯ_," ^}7v)ټv;$2ą8n·yC",vO@La=hS=cL\8́lY]Xu,qO6|3MVQsP \v Ly|+`?fgN`ۮ:p_O|r/[Svu[XJ0|u"kJ9GkO苈[.PTgz{KlȕBp.5wqhg 6eB"[# 2 37tjlX<Ҫ;WƒǁKp<C&؆=߬`dEmMnTʝjqh_p%ggj88+ֳxmuiBC+شq& )$?sH9潰TE=;GVN:pL[z ޡh3=gYf@۱gt'&&ҧO,KF vB\̸xNfLJvF*'C-zyS' V3HIfp #y%9+_4lr'(KlLb10ωt;Nz`?@| /_o<\,ٜL#9,w}pN&D:!~ROÆ xr3e|O+.ftRRȴf3Lx 3LRs+~=""";YiId]5̔d̤t5FI1(p2򭰦,) ɼW'I<]# Ndsd8}٤&$弟#$I f\苈\ _-ȵKȵέ݆5&0׬D苈\苈\i """rP/""""""r q(rrJLDDDDDDD EDDDDDD! EDDDDDD! EDDDDDD!-T|[ 3֧<߬Y@``ai^#G 98b+c҉@L?k~G`K-.-8[Zbt|cV(RTY ^>~75JŊ5h?|#2*CjYDN螽d5~ʁ׺t̪:ӠA[^^i=ACco\ZׯND*4'L㇇P51/s҄ZU*QvgkctAȦ?sHWK{%H^CQJ7K`3음Y/5^Gvml<~{ꗣQ]hP2#kѢ'lȺ?\ }4 ;߻ٿ+o|w;~_f3r\ģ.2ե`* {lo<:Jϟ_N` n?Uozncιܕ o|4wXmΊrן<9f.veX{lsqw_l֟o%><#߮%p-&ﱃS,ڶ ߵcO*UglZ36:Ɖ91v{}_A5uDDDDDDDr~TS$B9W77wC8i[ڑ٫nh݃hz+<-<ٶMVmΔ8zڐr7 oJiKf. Ֆ`'Di8NyWDtm(CL_so&ҧX~[Πg<vLh:Wʉm*?oǨ?9)){!55.+ *W\E>77ɂoK`[I.vu|7۬=rg=0e3埣d-d=^&|*V// ٙ ٛ]y ggoܼsw69d`8H%.e Lz3E#ߩ3x}g_DmUq+G%={0ʹo߾r[n{%22"""""r])<{&Ls {151@؞Db4Nw~VV1O3{*XJkFYS7Oǩ|KQgƙGwc˷M4e3y +#6+gDH7nY!~oeDx%IMMU_H7fٲe>|tJHRH^^^xyy^EKPL[7"-g3z`)WkY "Aܩ,K13W#[fQ򳓔_:#i~ˑzdsyjVBgez0nM/Yt|V"=:Tr; OB0fYP7̝*u@di~$$$KDD& SNp)Ea`Gȑ#+W$""""ET@Gy=ΐopER1yѦ'_ˠVBZd܋6ӝi`j㖺swtitVu׸ռ߱03йgy=^wp3ukGz[["D%s>!{OE./D>g/7/oTv *yEod7<ߒ߿97i<7Kq wRR2J3rdУ'fp_EK`2 t) L _D@/O>]i۶lfӦM4n_Tiĉȥpn""""W/"""""""א>)ASH1Q(k6CBűb0k6CB:aS/""""rR/RDy+ܒͲQqvr"6(J:g/``I ً|t\u2ԍw0.tKHPkߤgg_k;-d`eN'%+&MqG"+u._hR;;'44VmJϭ>wC/Wcvۉu߹!Oa|}J/w9:m&rw k[@?&+w*nA|Bj'j$3ANNNM&Lf NNN8Y̤ˀkR!$-6ewk4?:7=7cgdXL #G,r@n[^q@_DDDD()^-ojVlB ^wc|; V0@>7d2wzF`9R;]~D:.Mw2vegu}5f3MQԘ7G~r7Yr.P899a1em潛qhGw5鐽oFѥQ hF~#:ℑwaZkՏW pǘd DR)g-bT#g}aD}077N՚*M~nMkRZcz84ƧGTV.lmǽONxC3~qoQhЎcu?3իSY/w{u """r]P/R̾zWf}y|P58$N؉K^ 2ws{܊` R.J׏tpr=/_'Ը c'KHK !;ew ')D[ I{V%'""""WѥEwƷ~;i)9~]{+zu.l:nG Ǘ ~%{o\'a|r 2{5Eټy37ofۮL0].5Kzb*HK,ѱgǏ=N_0N>d~j^GZ*iWEDDD()qvOWw|+o<~/2gR=צkOM Aw&LCg0ܕ&mý؉W8cb@DDDD2/Ri;v?Yc/{|d Ԇg~\3g^]=Η'009.6yjD톜tGpK;"ݏ+{k|z}8kITg:c_/ [0_qx$؜ ՗Wzxg|\oFrgI{>/f#wVez~a4Nx}=]0,Q&/Kv)R\QmY΁)q IDATͬz3 Vp;hY#Ț-r H`ּ4WRϴs7CJ~}|%ϱ`OC=c}F6>0_ C ~=h67^neϏ1bau]ݫ>0%g!& a//af!N_^71^o)-y拑nb)S/?3wH'{v٭lc׺_xg@]ʘUiFF2_GDDDDDD!'\%e4&]ߔÚ't7NJG!rt~ύ71wn& ͆sO,3hC,'5A fߦ E0Z gL6hOrMo$^WBǚk!3nDtD9b7Q?0s},6ԊÄ=:@_DDDDDD.rdbƓDX{!!$qvxǚC 6%ɻ$a&8؍dc gVx=\Lr{-T(:=G{36g`N= /zq/L -p,NXX|`?|[;27EDDDDD.Ko"51v(g?Ns )b !'∷H"x>|/2!'<>˨|[375r8R{{ɬtq&{/_X͐yV>g0Q 67;}He+Iq fώHIvqDD2viS_z̶_mBmDq(v[9Kk+s$ kUՅpKo3}<{6770=dvN)[έ.mׯ Gn'00mMY{,9 88G 3!5 X^IRndo'uۏY0 /tl[4;Êppd+|wNs$͞q'"rJIxqgGNmBmDq(@߈+T|>tz}<ҭvUd](>'3fn/o= Q=w{;պ:e c>➈3n> v|ðQ?qLhVmēKmb\#ѥ&U/20Qv7vs'YװsteE;7]X/AŸүQe"֠#H8;8cV#UUJ""rX@m@mDq0EEE *ċh.{.lիPێK#rqិn+Ƒ̧?m̘1N:]t'^{EDjbڀ:<9g2c[z3ȼ L88~qn $"r(6½\{д+YXzjY苈HLZ_^8nz*.f_ '$>=TDDJEfz`[}W>G*m~ܥH_$+>wn:kVv)DD_l+G '^?+!\o`( :HMgͬ6 `Ct玧ʶrUDM8>*8Uξ{5+{\ms8et"*G@ ]>[zr""reڛN{pГ[snFl0CGUkfРeGl" .ܔ̄`pd HMe&p~QDD.j#ADh䋜'""$ 3n`G}S"IGmcvQm+Kx55|7Oz(LR<)>0Yڽ5F"gS/""cqNI0y9j%%=)I6<9d%)^O:/vҳ._ED]6L+"oLD|"P/""Yˏ4v30R,wVωd,;vg|M&/v0w*GfʶF'Ec) PY9lD Wѩ2pda n%L.Z06SXaիmP/"rmvcӿ݌Wgh2>)PX|z38mJvۦ,w>F'HKg,8d6xJx:l9ԟ|yv`9}t""rIJxҾ/&F|j{ێCt<3/ODDDDDDDE%ɬ6:>pۣZxc7ep+/ȵ}+hʷEs9٣:w?z+~a"}KrQ47a߷o!*֨K;UkR6v鱲"""""""'3H?;y&O'9Uzboӝ :t2,x^DDDDDDDf;Qfv!o~X*NW*Y)f»FEQ|$F.&|+t}KR>#[ܵeBS|LcKP"""""""WwnGzә*?g/[1DDDDDDDe$/"l֭$&&_ER?j*\_:WsE^ʨ ElݺF6mJ(rX~=۶mȩK~K:W$\QJޗ="rJLLaÆ] B4jԈ'Nv1JwKsE^{ɯ}gr"n]y1 !Wa꽜zB犜z8WTlY5aݺu4nܸ8ܺu)bHԘH^ptH^W\{l23iNCʕK(rjd2v.+{_ +߹z/P'㋌,$EDDDDDD4ȵ,}tgG$od"[zkz]$/`+K{F[AdEv:WJ6*XT3"h؎DtSxf[Von"[y24x_ 67澷KXɜg~98>-gHmzGmj94؋3s3glŚR/ЛHs_KQ|ӽ<q:HeU_dȗWK Z{.y>TXrα6zS/\]]qs¿\]*@ 3_z<2-VּЈ6vOƞhJ֜(z_TVsJ=+\QqՓkާ{ h^ _?jwYK&"ڃp_ۼ0=H[&/må&38:5;)z'µ /3|wkRn8.a>Gr6HksSxQr k|>ei9{3fA=z9lɋI-D&:tR2%p{\}sIOgd R?zL@Io;O~Ƭ|gPހa*0^ޟX6Q=l%j#t\)<%Vm{e*?GXf% RMHire^eqz>xv }fmߓg8 ?sZ,}65\@81W$ߎ5kgjx!CeA'Ue0eTAWG,sǛ+IT7)dH +Gz0zsZA&-x55XA~}MkT$,{$֍ďX( ע{H_ ܡiPǚ b[xjn2 2E5CBqflx,̚5KޣkcQ31?A*K!3l͒}?BwɟS"7 ּ2& z#F:uSj2m`; wIBעlDtGQG-{yqK$Šv,Jh6<㞜 TsZ.z8k'$$Cg{fnqT{PV's3t_аj?o7ȗjaקY0 '8]_D#vz=v۫be틍hSҦ,Ͼ1#no\!h=Ve?Β78"ʍ2ypSF:U&0|(s2<Ϛ +Q5F\ѹR8j#TKޛ#1,WaĝSm#.BH)h>w;ΨCΛλ `im䥤|htX-O&zhLxG9 b>,C̑ L7])[7{1_6흏R#flaϸ+Eh=ĖYL:ӊ x3+#aYq&;},M9H&.aV=Q̙E3V]otLv;I`hYo?bw^ەc-OٳYLa >dYԄgF6,Ҷq M}^ϔ99n y2j_y1I,}y4n-?V#q&߭qlw,L7'm˄83nYȎضc-tGw2gX&z?r{YL՟[kFo֓wf†\(._c.{7ȶG0H9-EY+"C^mޕ@î-;Odϭa),HiMyu_G{VkR!}svfJJx=]XW/Yyn w>ҕpg}'b៹|tTO7g ÓY nUhײqb̫yd7;U^%_ojF ' 0Ԑqe޽;ݻw{kQ<`zD*gz |nN^t؉Ho &q-<;.ć/ވ)Y%eqle*R?&T߿܅"+؍X-?__B>?]e1ftl f۴NJ41y=2NU@7ʹsKK[h2tM}M^B0l3e0Fc,~^*=%euUX(]AypH*1:V(pm8{w%w~qj.t!op{igpHapYX4u9{VfL>͸g_Ka€HxytzYe}]cm+6B}f>yb af~ L}W.ȼUպ'Y6EQg|na8RbM{]ADDFjp2,(o9#i=H6iۮO؏p2Kb`A&3@R\|E3r3;aY (TCvI)^ގ[r#FU^W0闽y%eCCc̴y= 4}n׬i}/2 UF̌): ]k7ϧ0e9*GG8FʉpƷ'1n{R_jp>?LK3 /.޽{ٻw/bݡMnY`ގƸl">PKncyfyM#Ɩb2~9ꀋ|Q_Ky Ր]jcf _ςLRLfQ7sڜn<Ώ?`1OIm߾ f~2x#wa`*1ٞ` 8MQi,})Kg?K |:o1d;ؓ9Td&B9q8Yyَs4 A.Sn8|? 6bbw>da?=i=|=9EƟf!_/A^tS=p->ϒ}]{mĩM:WzP/zo=ȴ,5~z-~Eu@_9Nُb 7'zoe2[mQ\R ´'I<|ag??~O9zh#ji%j`1E^C@o;ý5}o>7odIYā$%W.HZˬdpٷEܰ\g0v. 5ߏ[o]pWjJ/s'-ր+W+qː< Vy̆^D}9QC񿧧s zZCU[6CX%1vA7>£Ƹmd \Z|Ys D5X8';02y8,DvC2n}229X~Jm R brse!bY֯ p$yxfU 1dL} :mOaLAzf*cR\mΕйrg㣨?! $ -ҫDQΎzϳ*6 !" e~@ nDžٙ|pޟyܡ&S;?+TUVY J8&T;Q6ڟ .esuG ֦?wp|T[&ON9vshSeCw--mqRvs.m}\{uSFcƤQ\|g]e֢!O_E ըf* u[L]9ICϬ{_~n IDATO@oCbumV.ݭ(A>Xx]^FO]tNKl sZ/ܯ:tu5Z+]:'b[)7fЩ69*>,=𷩍.2q Hw_WO!݃tN.z?f%O|YwFSRtxo/O7C:6?48ڂqrSk%j5bݯ"szȳGP#~:L/vCTUσl+ȎkFι_6ڏ>M}M#[q/rɜ髴+ZjV5,84e+pޜzzP=Y%$VkOW>]ֽx.|=?Y5˕zZʏVaVUբԽj~' ]8MƺdT^M3NGd2b14ErO=qT<}4`};j_n0?$ٵiIIjuZ{KgJ'y.]qz޽P+/=5̲XZ2L,*,Sŷz}K2i%t/h€oY&]6A_Ԓ\ӗJKY$&}QFGc{N.<&}4B}_UӘۮ>UWq{_3ȐK;gܡfw3кWY7E:~QMij ͣռ2[%v5SMa2+L_:R3Wkْ4`D߭kPGjKataytan/U>z]4A"2kF ъ4U&uf.9 ~\G[dR_]zn.\gհ{a[OS[{T<8>~+O=yf־hŎI;ko~otQ3c^=L$w:ř3?R?<_vM'^E2G*&N%S >.**JWVJJL:\.V^7q?VP͟~+,*ݱJy\^n<Ř,;ezե+ee]?=pMGΉDzFy4}q4yY>8/k|˒%K<O۷oޯrĉPֿO8I rxC}|>>BBܹ'N7rH䨸Xs᧢DO^~NNfwmذAJJJt(乵/..ְaܹK@QQQ?1tGCa}'Ҿ̏ry:@Cȭ]Sֺ|ҤI |Ivv PZZC'#qQp.n#uSZ*YMgGfߢORhHs% Q.oMVvv<WiBg=v+ wvmG/׭1ckrEk|G;V}z7ߪA/MW;ՍM3:_MWV3kSj[vD*uD=Z:w'w*=WfE_uX3;;ck.c9|Ϟ=2 !9xз_ *B:*%~9ֶclҎeTpʵtDy\^7OV%_(.!@]Qe,gy}a-4pot`W/2+$LLϗO*t{;Ko~1Rw=EF6X꧶Gh,gy]:Ir\i1&\VEFY-ۿcSnG7[.Tkcy&оs >_@۸F헭Ӛm9rԺݳuyΙzm\$Y7&2w~gV~2c"f/icu#FU╗Cʹ:#,_Ғ?V\9MmʒW7%b:wWB3*팩@jzooM ks?K?_QIfɱc6?խ0wι 1cZ*>a&vKRh5}G-n9*x} |P?>Rp2x9@5|}h(Mԟ[ hw8s ƭ~DD-[]Zl"""<xNƗ\͟?_s᧢DO^'%%{?Ŭd|&77W#EG-;;[a(--ӡxhPC(!4Y̺PC(MVvv<WxeQ 2힎1l==8VU|:o4*u4 cu#Ե]+>oNOq}S ] 0T*ܸP/pnwumarמa>|4׌_/P;c֯ܤ8A#x?fo PDP]::Q[s7!iT]sz&wPQKRŷZm:gGoaUǓPׄJyvWuAQ=ϸUDF$薠vI}uٔEʜvFHRtkqDFm/U(5*D͢a9ekgtWrR'8}GyՆvmƤwRRǮz81ySu g4}J7_nWd[?VSWQy`m`Y>Bo.^ +^ש=g+T螕 Z/Nkz;ּfiK~rt?jQ>Xw_^Y3ӎ'_F_r[0IsD|Sߡ 捀L=}S7]][pJu{Vj؛˴n;5RULA:uiu_{4m|;jRz߭WY9FUɢ(xvW7b[4&P2Gәkr`nz_=I}"2'F+,INKmv.)HܦC' Xuބ!vHJ#/;GmM J!yc-JyO-wz Jv%Y 2*K/w~ifj4{֖:ґ7Wk;I!ҕ?_dW!ba$|:[k %[IT{5bp$WB*Lգ-116UJri|E--עTUA0G)&zVl ړ*Ր8‡޸itMc՟ڵe.QU{*\ J+rj=*|ǁ}*UqJ;ܧᚐ [q/huǍߪ>e]gTtE)s~ IQ\"#HY1-vܭ)6&О̼w`1YwȕkD 8ItaߞSkc;%e>?T52vi1Z2ovJq6h#o-Ԇ 9H*! 9^їBܪ[VEq mեcsYTMҲg{IS]d~][mw?'3Pg|m\KGjp\F,Eʼnu5h¹)\ h8JKKt!G/hB_6KU'Eu1j3Pg'e9:NG;քpWӕ1tkkp蟈(SijV=.){jIlז%Y:OϦkض\) (ۢe;+h]ڴ-\)(lVp|FlQζ#UҪed UTCv;qLSL1 Oxsj̘1uZfn?~%KΌU=>aڙ%ɥ WFhC#] |/JIk]ITZL:U&MjΖaP߾}Nmdsı̜9S#GB7Vlj;e 3W~ft>WFro?+9FUڇp^>ahrLa)RAi8rV*yaqFm*WHYeVHЦM퉔G@sK%/՚߶RߔZ mʵu|D W3NmQ&CQ[U(y(@a}#'u;6;LԇoÕ,ɨTέڦn2;a{`zܐLAV>=J۪Į1ʿ>uM5]a&I΍r>:EEE1b;w (**Jh5oGq`uR 2,/4y%<}r}G<ѸЧ> Vg>~^iWᦣ7iseb<>- KlӋ3QuO\=7h@#tcôdMv⎜Nk]GBxRtJצб:| ^Ţk^-д%%ДX:Gi4]?}>6O}ۘ^hL,PuF<9j+l2)=j͟WA ((bR}hN,7w1=@km}ƶԵV\ړWn-B˧(ȐT_>+$Ex3vZ\GwwB&k] 0dzٺ=_ {t,OU7 4֫E|~Ćhum{m:}&.[tQ'cx9-7ttRۖ-%7I;6G樫Ļ'~BJ&E{BT SG) FB?22ҝܪ}Rs~X-DmV>7r5явeܹK@tMQonQ7K--1AQ:_)8.>~ϭt^5Tdd"""Pb}2K]~c]~ Ty `|0>py h0ES;ۨm0<xU N9LtNZ@eJ^9/=ߠ̵,Ox rcIL]3NTf|DX `IFvn6ufjtG͚*,kZƓCGx#[];z~'CCk, E%0NμD\n WtE+uJ ~U7PzɀVVV 9-7 WnSfx/~BF|,YZj0IO{7rgO?osߩ t^98[{7 '3V>衸7c}$#13̊]Uz:@BB îs`ӑnì#Pި3]fd:2({u5LL_{b۷)ъPbbb۽{***TTT@pEҳVAסLfŶU|8r:9992L2dHCs/_\%%%iݻwd2)--#-ׯݻPk|qG&IVHxzijÌ_*u.􋋋)OB^?yruڵ#MZjhx}nq&V*7gjQzw'$3!Ĭ@=#U\۷Oq;8 u(..{>IŞ9 БKGo`O(rJS6g]6}n%eަזM3|Gvv `8pRUKe2Nj;9ٛ9v.Дt޸΢'&@"Gzq):%$o~=x9M$<="ע)ɛ>ozzxC4NjbaU&j`r +8(HAկhFԦq#իE_rh:>mZW u?zp`xuלuNw*.FG!sIz"u=X OngMPJ6Y"Ɋt#O67\&s2)*|ۯa,-QAaVTj`0 o[6iͯ0,:T0JrY ]>a.nncOjsoS 4l4OEt'G}IRs}q Oun=O(vC{f]­?~G{qzr8=yr!/P-;w;޾%Im%AZ*aA1`VLŴ?,b2Ej+IJH2]!-coLVc+r7κKQugה]87=Ji0Yt4:j{M7 uçJ |z˿d ڣ&+."tM55g! rv׺]A*kOɺOH T/WZ -`l"Gߣ)Pynej,Y5_ 0sK#Gclۣ4jPQo֚?J5D*|9>lrZs~JmW.Kx )Q3u8wt}6ۦ"MahjOozx97uodWUs)%)FA5o QKG*_7|@vv PZZC9=S߮9_徢Q< I!Gߣ>L>N-<!Gߣ;^{e*TIIɡWi9{7FoQJi\aaa^1ROGjj*"Gc>Gt{ 5iM9>:ec̺ r; }kڿsl.Tː$ٛ/+iLx9>Z1v *؜*W yy&\IRRRg`2>ٗ)w$np(odit&2rS2,2ڢm<$9>]uCu*݆J$Yb󂉺t_e{:Fg#qi[ˏ~uͧ;eR=K{wةY4y:N@#Gcy9KgXտ՚=7Uy{ms@`}*srwâd=\N>ɺYx:B#U(ð)kP`h=d >(;;[aЪ 9p]!ƹC+|`$2m+oIR=Ԋ'!GDxBտ&\XK7OӸ r }7 O4TOxr c!l[Rh 6GPZlJ]N~ a!Da}(S?ƈ?:^ E_4B 5dv8N#[hEŊm=JI< |9|7]ܥM•ҽBfPjl)\PjYLVOPXg4Z=B]}LSNդI hGS}w9|c9sFyON>Kp=A:#z7zh#Gף4Y09gb&+;;[YYYPC(!f4Y̺PC(M>>BBa}@ŬG>>Bd1>@Qp\z: @D}nM9-ZM- }@ŬGPCud1>@Ѣ4Y̺P }|>>Bd*--aM:!(o<^mՒy T SAÕ&Qr4Y8Jmy6F k%# mt\MKffChtyMi}ZƉ#?xǍB@휻i[RQ٬JآmvOGv\O%q{rD,q_((/T+\CMUKԼM2<д1F@٭#aY;dH2ղ7LL.]]c q:ߏ?qiP;U6Cv!Z/&MX<M]\"Vw,WAa›zh(RK淭*sT7eVBk#x9;:wKBH %Хw*EA!EY bJQ@ҥTitҷGBOB1!s;;sg>s܈k/"YpTVTXӾKY8*[SA"YDD#DD5%"B%sgu=DD$Qɷ4u_DDDDDDąhD_D;ѻWtblէU*Y^9y3X)ޒ5pl=lX'Y}(U)-P`g&eݒ8^%Ѭ5Jes6> E`P @ک,k3.VƭQh&čNm_ÚmDo˲{l$st_yAUeBؘ-:v׵)#QPPp(F(fòuITսٵxKѭl V)};h;dC^4.qqs Jm!KnYʆҬdA iDYJOKzwtb-s&gl{'W/s]8%1C<qbbb+(h1ncp0ejR ՗*5CI>p.8q5 .dR*qp)lg!zSU l/bg|qCǼRf3CjS2zSo8q\ZaXLQgq( ضBu(U `2335W<`*L`70[)^.qbbDA=sňQ("A|YoH>~xG3سmכXR=1 ,8zxF\+U{iؐRn;;H"]9ΩB>ض)4m.k#(?=A VáXN+(hC]@7YR(#)FD_Dnfb\Cob1yYdnb\ovsbaϲƉ 9R> hkcZ/yfnμAΕ.ڀ3Y8lvX.:o+VenFR" Lj hLҳy Z=v b$˷r6 f"7bD# *ň FmaD$YqݎdZPnu^ՎqpXݰf: Fօ,;Fۦw%7.iF.V*Be(]8YV._鄀*W WNFkD,gVG~=c%{I,؃tk#Q)FdP(0 >~ޜI4&g\ gSPE(~>싳AP! >6w_=C{ Tě"I$d;p8̘ drc'#U;!M/d䃜tT$L bĥ##bDAVb"*S#+Xl;lY\eqTDS΄B ̤ptRHI?Y(ۆhaٰ9 {Lzd[]r%~`woN߷zW^tmPBefYF۷s̯,e%kqTv'ᜇ7wO]!!V4y}pϴ;}/T+#rnK#Q7srKQ/""yb.sv0"=&o *\8BJJ ))^*eϻ[)饥8A?dؗ{p'2qnӃ^ؔn伄<%0nw3eh[n -G }Kq4[?+>^>iEꡔ -MHtg ;|pO<ĪךQ{|~} L9`Dþ4T+Pf pGKPيQlK+%"TƒJo.R&>߈ eR6U?oSğze?ߛmnj*t8 mnRTِ!;i;V+sXki,T:O~˽E07|_VNwЬVy*|%Y 0p&d6̶BH5TŤ7~½ P&(r-33\_@:}^hI: ED_DDr^Ό>J-d'כ[ݻ/j.ࡁ9B}9~4f3)@DvOmGA35FLF^ B6g󱃡w^6|ZCNLV+%,KYmx|UN|9 u }ݘy ~s1)jžf:?װeFoN3" gwqn[Ïpc|zn>y` +~}{ػ{ĎW"nVkß uOa>a>+6xVdj?"xiǸF,yf.?39cgr Pb&9J?O""; J}|h8W`QlЖJ`·{ዻ1E봦1nbf~·Q 0۸?Kq 5ֳ Wjh 'O""S___==)'*bmő#ZЪe:LR.z5WF89f)t^ЭB"l}͉Z4vdX.43$*P]6,9c]Ehܽu@V@ 椬Yq"7~DD$9JfS}܋6+}2zw~B$ODDG[џ3~DǥwLxь N|UX(Bvw;n89CwBQ*#+H=7^e[ލq>y6yzr5cƉtϹv쇿_?1DO`1Fwbq;Z,V< _o3}[mi﷢S/K\ϓ|l'AAEϿWBq"7}};늷SwŽ?CO|b B A VΥY6k'85>W4H&٧| ߆thѥ)G߼J}&pg;姑ƶLiT=&}! w""yvwG~sҙCjQV=NDmZRi0F.)S.զgSwX*ĄhCԮX:SUsL>z '3n2`.Oۚz-;ezuqQxmˋ7ndk8ax`J.m\_zt_Ӹq~u=D ŋӵkk ͖6֭q9Q5WAnYG5j=Ȭ3ٟȵ; bAl&u#aTĻp^=vYjRٵ;wP|| 55gk=L Ԁѹz0>Tewk)~fPmn [و4wT)N?%kuͥcFrs>|ᓙƈǚε(oV|¹dvM ZP/""r3$-Wޔ4;9Ó Њ8+x}JS Ld|f;9>m$2A7.b>ތUuJӘ{?o+kH[!Jw32i~XVV+V+8L # r0TYw&r}|pƿՕ*>lJb؟KIcP mue 9jQ|]zY;b_ :Sb(jtه4;`#Hֱc&ETLs'zʴ?&)<y0LxOOd7q2O=gY|}72YUʇV!㹉iAJEDDr]2\#d< _Y<=qOJ$~EP—͔j^tߌˆq_~qo+z[W~$…)|%Sa<=2f\\RhTt1Gcpvf%491}s*V )1!0yzQؑHbGqtF^\ۙv^}d{.s>3]SFGس? DD{`Udfmuā>[ׂA gq,rˎtطr4A๹gq8ʑUC_lL:aU UObŽD g<',c˵Ljlh?Ixΰ^ƜѥMH;㱬Yģncy LybV PP/""یDx<*BR򅎎#)4"x]xnQ4(|.o."?ȿZ^[{YW3ԑ|xo5vLxxybNN&nIJe[C^),Q|M|aQc_]8}'}փ{SʜO¤$%`$%l«8ez=̀ھMEhZ 3Sx=W"rz ""Wgp6 EoT:yB͝V}5Q=s_ƎBJ2VǺU;j>s*exVʭ6,䛷Cѣ*嫶chV&ųz:bD6o_h0m7`QÌh7qNf w;No: AVLaH{֬dO%"""䉧Ą,Ubwٌ=XgxO.)^8!f\N>Y9ăU4QS~O s!Ȣ,rqf⧩W8ŋߴ,y:X@VcP&b78y/R9 Oڽ:GM$I 9!bF&^UeDӟx8W3߲>ÉSZ4ݱ%$͌2Hm݇8{XgiH2Y#|6jdX_f? Ày1R0Z[0BwFPm,Vf#KP'DvҰL, Zpb}\I "Hi{O36}9uҎG?FҾK}܍(08h-[زe fVv2*OYyߖPrIB|/ˮ}FppWlYǸ΁4yu)+Ǵ'IlZ7:[:٧,r7w0Cz xwLTcwӈ'Us+f8ůΠ#hpd&T\8ow&b$ϤpNf5Sk_j/y-;v}3i}w1g7ܲ}?{"BX>wD$?3S",t8837gnp |Qބ3ٓ a4hٶ25{w:m"֑¡fi.N_QA4pr Fdţl97#iAŽXp0ʡ(S${ao2@ fe[ʄe#|;ѫ~${F>OJiAe(F(unjш8Y !=c#}tG$Nx\(O;vf߈z)E!l; {YDa*^<f y#ȧկҬ}о[8~7-3xAH>ĺEr8ׂt IDATn ևF0k|O$=Tf@|4S:hps>u`.0|ӖmC{i_>Di3`1o܏o0^\l{*1-]h؂"&l=8KYj/%OY˗ypmm:O|ER>l5I5xj6a>ŨQzN.&pۮ% (FŻ'T 9#%ƻ-R*zͣGXލCtB0ڛ~x:bc` 6 §l7~)3 #g}3!""7Bz~ "")Fȍ}i꾈%rKSEDDDDDr--4--7y\ٷ 7nnݺW!""YňD_ni9uiӦ%J0h BCCF1BDt-1"'hD_ni~~ׯCeȉ֭KYfM8[ϵƈJn֪| |M9,""4u_DDD$^.""r苈&MEDDn 0r"""7JH. ~o0}zܨp6mʐ!CPBGH.,^|?f"""""m޼ҰaCƎK~r8JErɺuy]G '44޽{ZDrN[C:uxw:th_MְaC~\ٷ}dɒ?~jE5u;"oqu!:::/WFE$ߊ0H4tjEUe;D_D5uDD\zD_D=uE՘LB^D\I^zF_DDDDDDąhD_D4 >  ȓQWh7EC~gD2KUO==6"~Oe/C9DmdЈH6 ΌH~{ M>zEruy䭧h_"oso6(BB(\R5hl=k\*+PRfŷHM]p6ֿT^'yt_/ǖLEŕSiڌ;+gY0"5]Mr&;N璗p}#Qoү0m=pxM3޶y R SX靀˘QV+F7A<2'H-{jox9;r*Y8k?Oy=RQ%/)) b,>V=GLg쥲voC2eeHǒը2h>1<̄a`9)qF^e<ٮ??W|ä<{4{,FD8NL'oGŨ=nu;XWYڔ.|$ZSiM܀%Å8|mTc˟2cohM9{o~w$}59s·/`wƳ ]cyc0g` {2F֌L78ƿq.<?l>¡q?Ȥir |~S]m$rE*ءx[ye 891IΫGqx'Tm /̋ϸ̇|c950]U_Q.~?3 K ܲoʯnґ61uG8Oǡ_`֩X6^}ꏟgy:qc;6ԘQ䦏dWnt ˣg=Dn0%"][oE',nVM:&|TǤgg&v{;r e=H2a?0~(P-"&_}?2YMwGL/k?%1;fz NɱVcwQ׈=9q l+瑎 ξ99z* Bl~^_KEoA1 ٖ&%ӾݔvBЪ)gr.tر;=WL¹s鳖_86XaY0u`Wܗ.`Kj /#CioӐ]X6o2}^eX,5\VW[tԉN:]*TI=>&Qt+YcߓgӫEh}o[vOьܲpvӃM)r͔ +Ù?`Ɔ؋zL)}&֞\H3>j/;?ixa U?Ur3js}Wqٰlx&erdj(NO0<&~&Ca" zV#a'k>zg]!5Ͽ{**7ܔv>x4J_cZ$ OQ5.iԍmbyOZ4Sgqc2;94i4S3fqƵ.M3n^^W0|3aPx _ y|B gy?,gw0q#E(7~I?p8sD@?{NjX'9v(ݹP^\ifSxݻ5/CQQM&N{Ƴ#6E#@ul'#qSDyk¤{^+m2.ѿ[WөS'*UDJrg (|2Vs +a7= 0F{i7C_8?#{|:??{/O0(ߡQ/{j J[(1~>Ni-f9EKyl* ޼`KqzOtrxKJ(U*TvO'#9&A}cWYif;ǒpb%RMBl `ql&A.l:2櫝&Dc.CJXH9l!h*F6UH`Ƅ?8֩a}WpḆLK 3{:FuFۿZJ-əM9j1BXv6 kz_"U;j= ;,>8I:ۏй:*Uw77y)UKP:ٟf <+q곿rɉ8kUa#prx <:OraիR[=rwҿ8 nx i;)GGʧ/c ;EsS8ޏc?:@~6nw.gS9ժ -Zueft \:|ɾL>eLyW a I_ZJ?@>+#te:~ư^g%g:M%=fc#Wy6 Nj<3NuҶCL/y/zlk xbMO!6. oߢ$F%PVxE0;̋3Sl"%$VvF\I7 ۻTR!JBI@A*" `]{ose]*" *PЄ H!}A H ~k+̜L8>>S"s(!ɨܭy 䃣$%%)99exbSQ\QjקZJ2T3Wy(#EK'd PT~:oZ}5Vc7*ixV)"š̔afEx>^5KJJ:F8-ETFF }AIږ:U~:M?VJ*צo_7ݬ_JOh2_ޏSPw+mh;yooU`O _cO--?n=w-"]2}u\[:h`J}yX ƕ]t~`I&50DIr$F <[&!&in^\Cwszx엟R͙7>@!ƒ%c5v>[-̆ fޥn?P7A( {G+Q͡2UA%zlCٷ'<ࣂZ1_q=RZBx]jk_ei}tRVV->૪^kn 9ZBB<]܌* u]}4٣VAU T Qȡ[X}Y__^笖j_w)@A( E|)HyP{4vW[7~uKr<]">˩F%мm[b,Sct TSP{ro4{uAI25E?s'+Q}^"ʹKLvܯX-a=X IDAT.ϔ!ʚ?~N.\sګ$p֢>,mt?/+ڨyfҶOIЎ usKP{2at͟LwW* ODy85D|}V͞9?mElU"tԹ7̔aJNNt)k_eꇻ/РGVK')ʒ w> _e_g˩3ts̑k~ޯ4)ZR5T{>5ˆ>L! kf> BcռYM B|!)n٩O>E+^k5?V())q#yɡEk 7e -%7|+N:JX;Jhu_cKhk[X)$k :A|`YϹNm0x*.PNX_]=$F5s25jYصB?,)RWh@ Y Cp >s٢"K8wiK^ U$tVRmʳ+.Vm muSTL"uɳϪOoQ֍֔?Ө(R+T;(<ܦ:j+_wAi>+^:|WcDUUK4ṵ7z+9B~Շ!9|Cvf!*UIytTEK呣կ폯+)==ݻw{|Aa6CuÈjo9'Z9rTiwd=zZaȰDCj`\5G\-;p<]`>^ޤUY:f)Va顉*,RhxA?D!~*+?2tl9+ NAY&Y5d2'Y/sŷ,պչ*wT}25S\3 \JRfz 7jfYl8'%33S.+0uUUK%e=v)XQ'[/bSQ\QjקZJ2T3Wy(C ,"uYP}D0:@F3>Lf !]զ&?Z]/xP% +m %S8M̞|nCdr TQQqViיp r^~m|V``[hM9}%%%)99ex^YE/M|p"UMi0RT zM233e*:M{Hw-ZMo%AU~_Qw/y7\uP]Ƭ*eWZZZ#0*\?G6:ZS<]`DUtMY&질sO#oT#VՓ#LfD5WL />@!Ƶ]iߞk$IHw>POZ(¢}U*uiMhn1AdjSt>HZTZC+GGc+=W.0k-unCL%MrC!}ԜzKܩ~kQޯu_ G>cclV?PhptOUGD*?D3L5e&ݫwWUCZ҉E}C},j޸D?P͛4[:jEK;Qv ŸyMc"zM7umڿT~!pҢIUQfWQSewr̲Tk?|D?>CW׌9a9Gt=}eZM#/Ԓr״T<92PQ%tC-zu=!s~6W@M㚪VV$)!!;Ap ff*$p8H=B˦kzɹMe*Y7o7M0}Fc?QVe̾PKAn>pAqt=ttuO"d +o3)mSTFF }ZWQPdW+ôZ]wphz8`hn}.m(6<]/>PL ((ͤ}+g)o(aVPYUޢ^+4(P 8t )n-@-aDk0{>^(IÇUK4ṵ7z+9B~C:u6(GG8,m=޽S !neRx ێ78@lwڠ&r; ]jAp'KBGhnY7itWtE&-p`>P/1up'Uva(?D ̒njDL׵Ǯ7+5@DY;r~~YP*IYύPrrWȗ$Y65vHWa>5;Z2e^X.Y4\٭\*o멛L?Y;wVj+yLg|]jApmj&!f9*5LU.d^~m}uc BZ\?x@PTڶkfً2k:4=4`nUvkIj1*T Ux `E\^a=z(.GXPKܩ "X%vvjm{\_[=E\3T4HЕ';;8[G@,6 3TDBR4e{gw[O(:.XMCLP 8\n~]ؘ$%DjO2tä9t*á%;NY(^%k(eX K<xáã3Mj|Jp~6W?2Q= P&~'f`,Y*,5"$BEU mrh~LkRs&vԐ*C@#n*RR,TNtW빶i闋^}o Wϝͱ`T_ŝ anZ:W=cdڹZS3 LնGK5QӶ DAp[f,ڡ-C%vo&(앍熨wМ2Crir4KT\M廲qW]8IЗM)zBMD(_%U*ޙ[OxYK{mJB)'ڤX.wdS۾պS5ڲB`S|QWX=ڼ1ONC2"e/V?~9C_eWw]BP>t=||=]]iYojPOuUp5j&.>@m"ʖ\[9OwȭBrg?>*гwvMr9V_z@5}W+k鏏y}g*Yq/ӶuGp> \u[T2$I]+uO58c}g9w, *؜Pcuuy>LdOPьU`n]N}}nv)١:oyK||kM=4HYMf]9JsvO&T\bMEFCEZ L)陲噝^5vЭOo53tťӫحӝZãP{27˖2U fi#/WkPL|RrSԇOU3IDkӲ O_ OqiGWۀ7Ѹtԝ2T/iLvL@M_]SjZy_-kÈU5zV{P5}8sIRc܍θKc<]!?M~6DO3>@a>^}9w9hXj$KSuYM=Iff PrrKϱ)W>~^v0LfA(B| @xu5xFG}È>^Leddx @"4P3JX;Jhu_cȡͫ|zTTq!\eA@ T)'ur!12X U_m3L;wռH l⧲p R _>A5gιK[BծSftVR6mʳ)Tm{^A ih~03f WX&WJ ڴ`KΏUk| A37Q|R[rKW+s_35IPJ.ajntѠbdsepьGKJJRrr LP6ŦS 5ԮOdTxgQ-Zn \?󍇞kVsԕq}5Vc7*ixVH]yk7'33Sa0P P"}k#x>2332A/B"tx]j#x>P{x>^>P{pVRVV-FU<]< 33Sa(99ӥ{Ex>^5>@aD/Bx\ff222<]W Ex>^>@aD/Bx]jA/B"}%%%)99eoNPs62vJػz*k1U)'urzo> 2mt]8ۜnt7Tc1S7 ;]ڒvbd6+0¶iSӕ YYY.g9 Nc1R?HŮPp heb>pnu,r2$ugѢEfŊnNc1X9rTiwd=nȿU൘Θf)Va顉*,Rhxq>?(e֭U˥<]൘O,J R>:uދnGţu>xFxC-ւlpnO8(ߩ4KhE~nگelC*,jѥWnI_ګQWK8@#΢lX ;nTLj;cj~[Xn+]H /hg_f̟߫fkYI29)>2 ~OiejsZfh(Ag~tǃ` tԐjњ93Ӛf{#ftb-oc/S}}R|ܪvGpHV-a5jp̆ >t^F']~m:ZXWf-\ _NLAJmUisavRyf~6~ͺw8dT|p6Uڞ[QQ, ԩM7:}(Ze>tCюWH%D R` Hk9N7-~jm.NjX/Bt}2T\_! a ZO$6TJjU ;M Uea*yUn~.lɡ*8cĮ] Eӿg'iꏿ+%N1ir'9ڼzV&[b|U`[32;dZib8pz.xFMC9J/hs9pXe6l8r;V զwiLJ1bJ;w:~(sh89,N4jح֊ ps@ _2jY}!*k}\ZtC4Zz(>*!ȧq.&QWyڞ\VR9cĐaΊ4KP%o.Ǯ*a^;cġ?Բʮ9v][fioLG@gdRXP[$LrhHC14xڵRQ+0!jVMEv)_bA6g2nt}J\rTڥi( LHp=FLA vUI $.\f `tWn5)~`+%йb4qu:+)8q.A&Wߴ.9 ~fY[TCN>dǃ˵`e*$)~jjnnASl|d]\%9ZB[EɬjLj\3^cFҨQ4ʁjH/B4y*X\\5mFz';F*rRKdҠTy9{U%I2%4^t\Q$Og|f~]sGxpb_rwʈ_V(Ȯo>֊CO$袛.R>Z@g}MjvNon:Fbx;] ?|i#q4hOԨu?v'}x&NȠބ!Ex>)B?kIDAT^!Ex>^!EGkU2[pfuk]y-εU_aȨ>j/*^}yzk@\V{rh4^ V:fYpO_Q UTk낧bjxiiw6ZN%ױ9V#IyhDUP*~H6YJIUzԺEKU7[D/F*lz5dJo!.Q!IeZxukszh}_)ڕJHh: J+4$UimW® qZ2Y]1X;ūu5 c4Q [%R=:F=4oWmyKW1=کET.OnTN:Q=s'WؠGdxZ7Eҷ~7UG8T6_zwSZ;V=iݸE[u?Bj#u/}O_^/>{ %SkT}zVYyp>s^V-~ٴ^ ՘'\&٬&U}A =~{U/=_`8e^&GO?4Y;O mj|V[VSt3T,V>wB}:[7sf[?VKcLmfxFZ)]Aj'aNeOHР 5Ogw%"u;_I1׷o٪bCI ըd6Y]ڞťs$"^ Ojƚd]scw$k>;w~wH2ո$MbsCM%Nm,߹Sk NmynVPdkc"u2379}=:7$qjd3d2dn4PWֱqn>̢(&>B]؃M?zW?m."ur$ó)(H2%RqA ;4bn*أ}Pd#c.~ )p \zYŪ60NMakNu&+O{ߦ EE~ְKTܧ={5NjءW̯qu4RCNP'8H̊=dx!Mk7ܮEI/j^ |&ߑt#%fHE*:4׮!7iڷG #k1hzLL-L\'0T^ZVo0*TPx * P73s P;ˡ2upxf9߼On\[`UL 3KU[gݩd/-U)qHJ/%o/WHrh5}ˁFufiXLMh*6ӏ+zX-MM5W”Gƃ,M,j6m%$c<}9dNƮe_LJI u4{v~ZlG3@&G<6'/ӖuRN)z]ui3tdis&<o.W64nng]ONS]{WA*12}&?S~-ӫ΃d׸ow./ LSN.]'vUR|7={j sfFuSBzbιƧOo^@g&L`7u¼y4|;Wx?Ir۴4ٳP-[ncdOOr'27!Ex>^!Ex>^!Ex>^!EQ.m}$"[h_áVjFT1~QAa rOL2U醙QWǨ[LB#[}St޷CgMYU'PWt9M Bд-CRݠp5o?}S:$ɾ.5V{6 e]ơ ^C^y.ɕ.t/,CO*[ܨ+Z.mnݓOg)'ֵzz[W+Pxdڪ}T9#Um{[o?tRV*P{yV=l՟xMQ2IJx`v*sű}5\?~GоMS4lCor$"x x4[c,'ޮ2UM=\7ˋV;m.ɹvn~rSyqӺ 9`{2M {Uu?~~ԝ͒\*%~n(2NO]JOӺa}kj5*X f;Uku̠,a:Vد`&)(:D̐={g$l ]>|MMfYVYVYҺӆkx]3*jC(kxW1*V,j;e]i:fgjj?*N0u^9Y3w:mLW7'Vw|5Uk%}PsLY48 vSxt uqza L l5D)Sn~Sq0c-Ico?-W>Yc64VĐӢv *ZN{5n꧱c՜;տ{wܧ=[:[%*-Lk5撚.С/Ћ~yNW|?CWUPDEQ^(y5:/$$^]JTlenPjٮ>dKlV9`$CEУՄtdPqQxQCFYS+TV~$:T@'~4zj\mڬ kuCJӯxtdk6XPiKjʘEG>zWjŚ%ɤ&iKx%^p~nGbc$KOIA2]F* ج@Ua`Z#;Awemj( OnNR4!nSQY?o80!@!._نb RPK%%hS ms5#o>IA&yPB3f3ZkǮC- UUVWMQ漇ԽYS5mR&;]uh 7Qh1$H-u{N6nўs{)Δ_b;YÍ7j]^+MWb8m]pmMke;vVs}ռiS5m>TomXzI`p/0PG]0԰%}TV-Zu"Q2c} 'D)g}.xeozg N~G]~ש:+Re$_NI}?뛟 ߶,~hzOԩu4C}h_믿_4H]-yu.}T5fu IP—tsu=)XxNށ뮛_SZdMˬ_?_U_|C?V Bs&R)K>bq֯9^?ui̚_! gWkIזJ2Tm"SS:hZ%'dS ӞE %I&E^޼`&QGsQMKjAƞ}^}x6]cb>#6*R&?bԬk2;QO T`&jڴ[5 4?[uG~M׍#ըQKj~o'ɩuS}Trǎ_4wRnH.GWOE4װi ׻),n;7פ! iGo=_7䯰jI6D6Q@WL-հAC%HNEdi4 !BAjvϻx_{Y$97.Ҫ[Si„ Ƹq<]P/̛7OÇ?msuSPP I4)MKKSϞ=k/xX|T=XJ뱑:g*?v|[^y`-[ncdʼn7}t 8M8}z ~ҡzƾFi[a=Km_?it!b|c<]G-?tF}FOq"}A/B"}A/B"}-[%@=E!Ex>nrJu֭N^eggO>utHhMvO,t=(55N^}{g+W/Tttn6jժNÈ>PG $XQl, uM}Qjjj|դ#iĈf5~I0u/B"}A/B"}=]A8))).NPO'}Z.???35EE={)ԢZbD/B"}A/B"}A/b'zP ԶxMٛIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/managed_dock_window_side_drag.png0000644000175100001770000020733314623331163024512 0ustar00runnerdockerPNG  IHDR94SsBIT|d IDATxgxU{w -ދt HW ` (aE"Qiґ!ٝCH ` yf7gΙ7{]DDDDDDDȑ#MINc]DDDDDDDndM>7z鈠DDDDDDDnh; +Iѣcό9tvatG3wq-X brM) JEDDw)ۘ2V}xDDD䒔uFa#I3\(H&Zm ?wpKLSux<)Y785SG%Ԓ|6$ڗ:Utʛ{1F ZMxy8c="""c]q]_~%} DfϞ >Xດu qBf-G4!Uf%t$ <] Τd,ntŜ0r'<ܬ+Mc$Thݛu|qH ŚL&iX&RJpNN&$b٧u (((lٲ̞=}}za(T>|mۖ{5[nnXDDXea$i5d(Uvfb3L,3 #k8Tr+N Yo{ gy^vl;@f Lc8Zd`s_" H{kw$&1-&Ԥ,n)_lډ8Fun]?cH[oiYWHΏ$AG$mCI8ץme>azj Q)P 5[u]-?E޻woΝKٲeώ${̙3P˴i8yd߿?111nXDD8#psִBzRH'ӞdE[++xS{_[\==;U'#({:ri̕װsJ 8w5Q!/9_zԬjbC z;1[@ݺ ;ֽ-,\y_VPZV=X|1AH'\Ç_q9#כ'Yѻwo UWuʀoHMMߟ}]EDD=-tLƟ>`L,:`zx\8~>Q Gݳi)!4ki:웿sie#T;z1ÛR+ؽ*Twܳ06ǀ[(q>$̺CٙD .Y˛ӳoG3x({ Wz4Lփ$9{JF,`lUسNR?۸ qybm'nRugMBmydS\&Mh^BZb"֌$15͊0abΪi$d`הNrvdCi/JSIJnHN%p)3)رsznj] t]I*S{/~TTe;tг ^ܙ⩩yI8z(ׯo߾+\||;b{nN*:"""WUz3d(Feq1 0tS7gÎC;K@nO.UEqxJô Ė`b׆}S tUr Օ]k̊5X)Ma֟{ojB6fWyވy! .R6X ZpGg$rƞw}í m_*R.xMO7Ik\7o}~lyݻ7Pp}WHdƥTiJ{ddرeZHM"5{JWO/Jd1JFj2id&\>q;)$fM.xaլF|DW&MTʟ/lgbN6ㅇ=3V\JaO . o\2Mnr'$ Jq#1UrX W<\ح)$'fyLx.fجi$'d-/"""o]ȵFzr<闺+IZb,iIZiN^r,F:'wRsaMTXc왜gg9~F*qR9w>$`dzn3"""ru(QsN""".%""":tքǑD]DD䚦D]DDz`K&ȵBH 8DDDDDDDnxk׮ߋբD]DDDDDDQ.""""""R(Q)AIQZU &M trmN~?B؋1gy,щȵ!DL~C5 >>R7ʎ":K7'2V~x K-g?~%""""""r 6=})<FըT&`o:`0}@3FObpZTTǟYwʑԼ#ش#wgpx֣<4e;wavz@p@ ǘBo4Ah4%}JIm҄1뭤l6vTӅ'Ěk|8R1we㷏ѥa*գ+>`$lӡS*54X=LXO6~Y|esl2yBDfȮ4^*aiC_ޗ*""""""׮n/`N|>m[__di;ʏSxl'\b_9s-T!Oѿ?jk,k'=|67qS~Tq.bpʇÉi6v}:k}ڽbg6 زޫ͚W?όmKXw0kAvp'#;[9o3s DVWe}l=yNs1r ̶ ^"˞ʊЈ]ěS y{sD^=ԃ2;RoGDDDDDDFSpmέ-yh4v<g~ >{lvy=fw7/ ڵ^*v-Cu* ;ek7!AEB)$a`vIKd:Xrv9~gZTp5AhGU ITXܥܽVB\LPg kLu) /l7OKl<|-%ٽa|~qt(%?TRѡ\s 7aq`s泛9;ABdϩ؄wJ$X9dd&8dT{f?|sp 7MN89v0#Jn$m[=/8?'¹ߩsTy{-^L1sW5 QU~f3:tpt(%ަM8paaaEDDDDRę0.hT "N减0%xe \Brge?,@eL6ֆy=8='YJfaS_,I?pwyyۖknʋ~d/iӉC8>[?ŗ1"Tw+IP&MXjǎ#%%dG$鉧'!!!EDDDTDt/Yw >>b208a'8~8˗wtH""""7|u;f?#_̧ZLm_O{c1+mcʘxT]ZQ zGWtὣhUs۩ WPsݣi +yK[>&{.@;o5^MXo|=X7؊j߾Ŗj%03v&q-UCfHUKcbg !$$Ʉݮ[^+f3!!!:tѡܐLƠA.#=|s{E&RX޽{;:kܹsi׮klf۶m4ir˜>}zo&"r94ڤMDDDq W~19?f &ϋH6\0$+, ۥ/`HpG>Aր{g2cqr Ʉl 'ps-*_fܪ6NAp2O8~xV=..Wmr3V{@_$""""rCR.zlB^8s(:[[`KrzlMlC7S#̹`kWt$?o'&gں'ӿDn AfZ JQviOsG:Tހc_d|Ss| )oql؞{'ʩ;iRuX6i`^DDD䚠D]>{t'}xjPu̧kgq=sS[q ,0ߖz[ewiF}{;vN/nkͧwcD6WbJE!3ӱDXBEtF0!e,aWSrdwKkhE/{Fօ0S`?2bc9sԍh< }rnF1=z/kO7T:jܞ2\#T_7y?+MxGjr}Jz&/n?:U[>U~wLہe86ʻpܞU"9~Վ =06kswN/Y' -ݏv X|Ц옷V/J!sGU 􀈈ΕG]D XԚ8a/mj$x~ex)6:/YȜ5xLO14YM?/˖~Xgгs cC%3`#t .~Z);O=h||6g&вtx;x$ڈJŲ ,{q[Fw&gM巄PB>A닿oNN89{Ӯ FRԥ1w`b_YH!&#sM\f<1y,/?nODDDD.ܢ NFaMw= |uD""""""re6QbB:<*H-]qM JGl3osE~7`#Vr$Sn (:q2MfTaNCZGdcx)D݈_㗜KM4-]!\D݈C;R7,*Z1ʹ-_ߺֵVWlVzm%~/FU+Q-<5g?lYД߅ ]f IDATZ+#mݴFX |g5`0}@^uEHpetoQ+SjG/?am[e׼9B+RIo=<2=nFXj47VhdY/"""""r:3:ľp(O=;g<ƣTgšڳg 6u(<`D*_y?<Ɉe5xg^{j FĬUG}u%nDg7 u 9]$zOA~|q?Kn+@&[疰^~ỷo^6+?~?e#oGiߪiݸbvDDDDDD:zZ~^YA#o!,~1\1[ @G* u.e[+7ùNVS)F,+AF߂٧gd6*3&vYyw&/kBר5{]6Cx7fڙpfLhy~ϛ#@0_L >{JEDDDDD{NW#)@Yξw\DǺINj+C)w!AAn$%`' d!":/qea=mbhs٣sʛL(sQb}GKn<1Q:ͱ8a۱c!i|=>c """"""7 pD Jg%iOׂ'?Rt"{)"|(m:wsޞM;yuFtE5ΥK^|8]X9sLI% i絓ٗ]6k_Uef,Xp'1Zu=Kq&""""ric~CE^g1tҲy,/KNwʦlQ ӹͼY8tCV}0]v>m,ei58??0p4-I6R:ՌOdet*B]̬F !-. tHV-XKQ#f/_v&V3hv"vCZT"^>v+Vxkw;>? _3,8ŊQWTZ_DDDDFռ}nDm|ܵ'S|͝OҭH-A4'b{N? kp4Օ^ 8վ؝Ĺբ?4xzy9yԙa#+14wAjw3%9^Zmxkƒ瓨7ǫ<A,'drgY0rU|mM(  MسU+d(QwC̙pgyn/MƠA`%Z=W^X={h#^v;9SgluHrFԯFj*>h_ϛ7Ν;rӧOzQ^ԭ[V.""""""RHVwLu6qt"""""""WFEDDDDDDJ%"""""""%uDH D]DDDDDDQ.""""""RibŊNDDDDDDSQܹ3'))ZnPx{{SJG""""""rUi~f3 4(jw!8@XXCعs'qqqDGG;: Ϗڵk;:C}\MV}}}r`a&IIItС(ɵiΝdffҶm[G""R(7of׮]JQ_."WSر׵gh1t*ӮND"R@)K^;SҼa%d5m^g񱨨RX׏ͯ%1;;nCWأ@p񱨨R}#n$a]tz !.,o2l7?)@k^@y;L|kQ~G|71.*DOx]dt^usM[smQLmg)؉\wrX5ME-G Cdnu7-LL=JU&jQ.Z?*kz;_ˊͼJ:=[8fLx d31uTj>8ej0:kB/י!u'SjC`TAIlJUOS̉)-N k4§|س5D{|8%U˗rh@"}Kҽf A=yf5+Q6< zx;֭w^ k涉HYekSyS-ʗ)K}=w5bGܲ:!e)tLEEp%::_z,Rn-ٺa$I"SͶ~HY,n+m ZT#fhug{M^ek&d֢B`TMWV`dVvBqtӕ8ځo#X Ծ>aPj[nAӴE 9wNo؝]o^فje)_3O-Ҁ>VaBPpu{~5v~Q;Ѐ=me4(uwh[)Vg\FۘI>T\pEX{{Ӭ|MBCҘo"nd19<վ !CYw;\Jb،x& VFK~}N\\EK\\%r^L L*n5yv3鎋JZb4t]C7yW Ό%ݜ `It\,V'pjhތyF9K3`o ً.T.m4U1R&#`֏3壇hV8ʎ8ӈϸ΀{ٛrv=ζz= ̈7vG9kZmy%6OcΓ1v,cOy^'s3;8u'Nai;e~Ϟ`gyzqr\Յi[qdLǴM4Ȱ2Izf(Bmǚt_/'s_˿r;fdؒLp>xaIb|iܸͦ7n#0uSz)=@ի/(/i(NZJHI!RRb+r2yӲg;2V^GV/3YU?wԽ?Vbi\ֿ@OMlɒw1CR12ՂK:4SSſ21\SWây6B=ed_xMg_kҔrs0JQ};J*['d$yyg{1j`Uq8wVhr0:a8-=rpYEEp{t?_;k>$dBөa. hہ5wJQۂ[ypp6{-Aӡ#hc xvL9QnPnTIxuYZcH;-P~gZ-C{ vCofm8Wczܶ?B N4}-.9'.Oz ghgݜ{zےv*x[r^**PXӦMڴi]>Gp}ԝV+AnunTIU]p2v<ޙǽ_Ef#3qE+ `#-Nڂ(z ]L!\x̩9oM0pdEŽ8OY#LU0Ǧ-MweMoR  [;X =rHh^3e?ͨ ?L똝`u@D%9I… $EH'1~V;i)k6$xH'L=z',@)=|!yt+|@33x47Œktj~8JaC3&@ ~{ty p\h>>眿)ٯ{u䥱i?z::}d2aϴcm/Tv#bIQ0O~r5).wަM*UDŊKta|:3o+O|,_?$9"rix}x>['8<:;GɎvpW/UD=s؎tz#G>O&uaS[bzt+[N`lj\J s4F gCvL`Xms1|gNԞmNί3oz=~#=rn~ ;|F:3b$%ygGbVzu5nIJd*jH 9}yen҃fjғO>!|Q^\NڱI60{SAU|lV2 ;G7VR O7?Ɣ]'gH>XHl*DMT_Mas#0?N]LBXބS6'`'?Mn<+\.w֜Y([Q7p<DZe/X G<2:tK 4goD`'vvşDrXC(Cxt 5wgLՁHɉ绑4)}D$^DZo`ZcL^vSߎ'VYXN>#kP&^㯭s۱ن\>sfzi׈RQQ:Xˆ}Ҵ9e/=MzV\$yEtLP d$-' >W3๻I yьКvz26 ,yGyԕS՗pw5Ɔ5msa|1) jp<>5z_/=jn*G7J|t "xC&7]ZYmz6㭤&B~g=N0s5ʖ)O/\FC~_**@ÍAIe+VO>ERHns̡sΎ+Vгg-;bn'..3_v׃yq뭷býDDlfҥg\ԗҡ O2Lt?T:͛Wf隡)"%xTDR_."WSQ7&UWeJEDӏ;K}\ME8*s:%"""""7"QfӦM4iҤ(ܦMvt kԗt=i^J8U)ʪ7UTqt(~~~"D"r0vJ\D59# RDnP~~~޽5k:""eٽ{lEZca1br"Rbխ[;v?pDD ???֭PJ"rW7^^^TZ"*<%"R釮ȵO}\ >UEDDDDDD(Q)A JEDDDDDDJ%"""""""%.rO\OvQdu9JEZrt""""" Mm[I=M}.{72<٬_g+"""""""hu.& / +>ՆfZ<{~)ӇiI (:)A(DDDDDDD}j dDGGrtό˧{1n"ob<|ˉ(3tz_":ٻ(ʵْl:%$ޕQD ؎Z9vXPbo *P@#( ! $޳:h s]s>3̽dw}0$2}Rǻ`جl;={O5kq`ggж>1Y>morkZ5vtK?;dUV-؆xe{]a?al`X0LX,,fFow-s;It`7ҩ!k5ˈ/Y=vmJݚui{[,?+Fdž14[__a7g76p J {T[E#_GOV}< [-=IX8)Ӎ-n۬Yd?3۷׷nZ!9 IDATUh_ 6oڴCqqpCՀ7}4#:m:!6< KP䕦0rȱ;I9.2E &K7EԦhk z5"j`ޕ.8` \~ 4[i}ZL;ӻB F@Szw$eo2NwsFwcSp{n6EWQdyJEDDDDD)dz'FɇȘ1 :Bm>ۧ+hG.A6;]Lua!]yn}!a'Z-V+A 3. \$.g;#ۺ)ӉDDDDDDb+D=U?Fjia5Fo?;'dnvOLL*6`Zh4f/aaEh\FCrL<_'""""""r6SILNOa 7@Ԝ[~;Ú-)'J2In:e:696¯vk _#7gՄ1o"6-wq?Mߧu)f3j uwާ/Z4o.A .Re)EDDDDJHɷōՙ 4|O`n0Ϳ[r 6 Namuech#-2c`zԁݵG*q&n+qWm$T&iF6Jԥ|œ/`!Lx#N`~aX%zȸ8ڵkwu۶mK~dusc(s#+5<>O9?aM/o7E9y;@J4iy_ۯ:"b\V <8CsD"R~;lOmxJEDDDD.LL&W+0axjx3ǻ(!%"""""rK,͵3l ۼ\Dp"r LfDDDDD<kQH)X."%ƝG+1v7`X"""""%]DJL|s =D]ƨ4n*+w&+'cKnNO'""""RI)Q WҶn@-]4F]뫒xb>}wÇ`?O%"""""JE*+ÏEDDDDʛ>wܒNDJSx13C8\["O%""""Ryhz޽IHH ;;ԒZ*0l6ўr@=D>[qѭ~{EDDDD*M0LhѢ$Jnǎ$&&P**>z7G\Zv2\˴[ /O)""""RhM=JJBCCY`èx$-kR+,"JtKiѵU ,h+BfW#[=H%YE*+s}Fp}nO*aOp`wÿMSTZ!=^m2ـzyxu~Uf0{d8 M:Tt."w7*D׫Ky/3IU\DKR u7qc*ar}s^uC n|aVX}0W^~qq{tX0?Jp111zĞ(Dp)/ }'\}ΞSJ]{[{gV#[D51`ՠf@ %""""Rɕz0Yy覺lOWkz/u1yU^Tݓ>)oFXx {~"Ӊh{|)ܝAܔ~ct/5GtQ{-ǎWQIlg{P+W|8O0yc~SzՊZt>7^ߜzMCJ-z ɻlK ݾDuRN |0x+#,` _yM営1kiD48oir Gxڄ{: 9RU!vn!Dٲ b̧mJ#$i|0o`ZѼ7w+D.^['slA~sl )ODDDDr*1&r3opʦeao5_0v\w ~ ί*Pd],c^zs}ZFޠufRd/>TE0t|'&4 ūx'S Q.""""cmſ>}{u$rX.n/Fc3;n.j"o.F6?pt_SM';wCwg] (:~ ."f CH# -G2i}z]G_MY~O|d.gHI|8yjIUR #"""""@w"SG0N,FcEDDDDr ;(((8CEDDDDcX (.954뻈HS.R8u[;%Q7 L+EY :{&DLHecsܩ[yx}-nz!zz"8$uǚ^ooZ=Sރ\Bzٷqٷ=,WمuD!~}xM䃕om$'"""")%;7ZގaV6BN]ǟ9 Vx7%Fvo0}x=Z咒ƙuG+Dqpq2y`.?U [ԍq5ov̝y޻ז6R3f9%)SLYs܇IZ j%{rő}%҈ F==_a޺!4)J9ݹo4{wve;֡N+}vb8o }Zգv UiĶK.㧧}ԭG>}QjVZ1mO+ZPv3=?S'vo.zqahޣ 2 6>-|oή0m}b5*}N{EHŕa/-ҶMkyzy/WOߡDN,I9ۤ"""""RJ/QEL6a|fz{"+5fﱤ|,Ύqy}N.$1< %r:#HVh;sl׫8?^ȿz_ l7/39yrFݮiqhx^ C~`CzN?`?`Joc>0=>Q4ؑFUԭbso&(£9sc-WоMڶm{1f{2Hʫu!&\UPc<۫cLQ2oLDn4>HʱFdK }55ځ.eøܩ,6CG.ĄMw%$1{Za2|3Anˎ`nDZ jU}[?jzIdv?%QLݡ_0:,~-ū9Û7BĹ,*mM~8S؟@dKy _jzGM%zEQo/?tl #淉םnNfY߃T63}7|x:)pJǨ_ /}42M5iT/3> s,#Vepd0},ՙ(`O] hw%?{o/"}t,*AkƤd:{I붓6oQs&n!3gk:{W჏6ߏY. IT'k\"""""R1j<~+97͇yb /5Lr3Fg1E4 :G|z41ڲdTb6^yAIynڄZj8nWb/&硴A]烕d`-J5iw+-<< cq%#QF(7Hi?k5񿈈Hg?=x`O!rI;w. mG^{5&jtˈf\>0de󞞞{˗ӱc S5j M8N-εkҴisn3hЇ=Tߴiݻy4iR)`kbbO˜aD>G96 ?_|}|9”2 FDDDDDDJi}unM >y,^_LW~bBPOh $Jg%"͑cW8skz}GAS~bb"1110K)~[O)!l .gHm|8y]Q.""""r%Fprk JlR7bꈶT3cƉw hGHY2,ٛ>؝c';PVO+r]*~#N/p߀T-60]cEDDDDP.R 3TG@μBv'QTց捧☘;e |}n/DDDDD۷EzzzIW-PHH6XOR"i0A&LE}]Kwoƭ+&u.""""%o߾LVJZvABBBIփ[H^ ׍B)dEQ&뾄Vseg|K< uCzUU|,)D,zUUʼy<F0j_o]'\IU{I* XjzKߎԋ(u _ݙ쇚`=%"""""RJ4QWww)-2Y +`gcO(ex6{< ťQĴqs57L]DDDDiRg,`!ڷ.Zm"mRWgsfiLӪ' W )eYERdz]SL^>DԸC$,Z曅)t"#RwrhWa>DDDDDDDDDD7"""""eZ ogmO3fڊAW7`_nfۭ~oi޽O_ye|z^~X7iO%X}?Cvҷa2`>sG_\rZ9(H{ӌSͣJ{< cXHӛC(]jILCjzaO͖m,uqUr\*WU^1~xKs2hР ]pe{V>O:v&kًO|c[FNE롿hc#=I@.FZz@\;0$gHNlsBK780;#i?Ђ۝I?=&Z#GDDDDDʜn sݞ˜nxXύ|_r[WOH4oIcaBxqe&J6OZk|5])|Wk@Øp|xuέJEDDDDPluR@nG+J|*K}Oyy[|߸eT"""""[quCF\0ar9pBi?v,DZEDDDD*'%"6u}]Cuob^'{اH{:D*+KS}3}`Һ\uU.];?Qx ѬKO8,"""""r1Ԣ.Riz5;Xi {?cc5k"v.]N[gn6[svUR{:)tmȹs!:%"R!{~'MbΎ" NFpmI9K α3]hآ~&Q-ilC> lA)]r.>\t}ȹ(Qt\tmz0ǃ)bqk\"5 2rl#dg. """"RqiHen)_i3ókhMf}:->@V37p?..d߇T(+VtRNڐs!&;%"s'pMP tõ?tt| ٷt:s)p+W>.]Ov/KB?%H)pg78Ԋa a:' }")b"69.-[ّ̄7e1XCbQHvCqKp&k_ [d7%m{Hg?殌,DJ*W.bi"Y3J`W):n[S#Њ3\8È3Kxx8v15BǺAD|)("7Y؎Șl!e:RѸr!v (8Œ ε0)AD"sd "XRCpoݙls+a-ve$'(c_$YgEJO _~$&};e;sMEvhf_%[ٕuZs+#UI/8Eaw`OzY,NO%eρab>ѺeZ1;8Κkq ~ {i*4r8<Ռ~jkM%=jOT@Nbׇف6\r֑nl;]*ϰSYU-gd7n~Ow@"R ,V NͶÁӰ`pJƂi/L$-+30"t0~iȮGQBnnZQ""""""³- x]\G<3P?_l:p+wm'ִ~&[^kYT.bǴ'Y-j;'}n=R#fr[NaM5C{/Ai;6*~+Vm pVw{cX-)tq]ƽ$'#$ydYϪUؕRyxӑ9-?+\(< Igƌ;S88T8Mxha>dΤveC6mhYF$ NM[Mg%xd%&iަ9ujP;B@jՌZA."""""r>mQweFuAdg)'Y0ܧ${:ţ/צ3wHv5uUvyeݞ,QSUTq+HwVRiuv哚V@PH @B IKvc-gVbmB&5k6T#*lYvX:^G[' `u(YEDDʍi-c'~ͩS;zM7l+(ⷻnkj[cP=3uU&Quqd;xszߒ;'']H}:>эFѱ4qrc >|f d+TD\si=M~&Sܻ> 'Aߚѓ0nÑ.<}:5>yy@^>yGJ-Uzɧ7U4;~ j7#4 PZ'uɗ }Rĸ]!͊o FaŪ);%""Rnnaf\L .f?&p#o5'b}ZFH\7dMᓉp%; 'wt_;}?~^MduE,-@C\־|=˟łc _^a_̏#f04BOe)o<SB<Kxe\,O{."""koL[لac{i|0|l6Da2t[ctl_Oܵᘀ=]яޱӝwfwJ^M0Mu'GE^ {\?I|HyJ;DjJ^͛XBp`` 9qSLp/d3sKx"SH!d~$7=v| .EDʥJEDDD«ӑfsoSY`(Ϟ` 0hFxh>)^]iI#0,#""|u}t"""R^&3F^&YE`ևmلedW:oqڞn ȷEӠ72Xg,.'7 09dd Ҙ~j_ 5Of]kSSS‹\j.E=$$u.& EHH|gO"""5\y oppHZrj0LhS ¯GuZt~?cﻁ?NNF^SuO۪Syd߿jժ<_ Ł="""B}eH^_Kӯ3}N!xnv얍ߓid~"!:ߒ9x d 7'""eXoիIOO/͘*l6O*U8x w&778J*EDDDDDիWZqH1UV=F""""""R!ivrDH9D]DDDDDDQ.""""""R(Q)G#JEDDDDDD%"""""""urDH9D]DDDDDDQ.""""""R(Qʫ /Wae>?J䲻?gs*9vn2~4Z/U2r-w=D@Bj_֒sƍ_0k UBC՞! }aF)qt1S}\yrd.ǍͫJtqS0-Ci Nlߴ *4~<*yQaVUn}zt/ݸ3Ԏ%4"<ӫKgȘw$SyX~I,H%bOEGxiksϳ sEq&[X|:Ѐ{3m؞d:Qx{U抧ہh:91MhV p6̖qZ1E1fcTjԂj` Of:gLDy8{vgt*uHѥ]./Q;zZzjקƮDKA(_ɲʑ8Qs}nzy{de3鉾sv>^?6U'Ԡ<~*?Wxcd/T na|^c)u޿6w&Zb@ }@{Ao_OL*Dk+Gr zkf #O^Kq;k1e#&[1v:{;/lgT;I\IۚPLz Xֳfc"܏uH[I\Z{ ';_?N&鼽 sڽ}M`KVLj.mPwY@ł31ż@[:m㳑|QczrF"m+zeNg*kx6v'mZ>뱃g<M?rs085)䇿=$e;W竄l G&zI3Єoo{aW #douN\Oݗ 6/iUdHQ=خ#>C[ԉC$9 Q.""""""' &G]}/Zݜ?SFN|tegZ*ܰirTВHyϭA>?9?]x$~9Yd9{cFZmC޲c7\Xa<?fOвHゥrgi-;0 bEXaFjW5y̘MM?rkH"""7r'BlX`KgdP55 ;~?]j6t2ԍ9McY9W` -mhWΪie$X0U7ܩ,=5 >"{}"u%,+Cuo{ӝ4m5Y ގݣټvU\rLz]BD]DDDDD!Pf[f`[.@;dCպĝ̣eWxo"6f/g?~}̛ZOo߱ b;mP{ {+S5kݻ%"""""LqEj,)qsp>~*!JGx?ƗO 0g62 W1/浣FS:p4T ߤK t-o|~-jIb+ñTJDDDTrfPYQWMO^ӌaKǯ~D0Amxd|4B%<{DW3a7R9*LOFg#7s׾X͑}Xڽ&mD&Ì>*ѻv41q\Y~ͭR'fZ;֪HbmG7 :x|;۩]F\ۙ:rX̀cZ6%p ѣC:2aܹ<L]r%m۶\v}؃NKGܢ83;7\kWgNe:I'2nq]E%f]DDDDDD)Vbp!|>ΫTQ.""""""F%"""""""nDQ.""""""F%"""""""nDQ.""""""FnٲeADDDD-_!gJEʸ֭[:7p-s `;uRb(._c„H٥""""`ٲeL(Qڵ+EDDGCEDDD.a%򳈈(Q)%6X(6<== :a0dݻMHM-hEDDDDDGCLA;(LJEJ;:a@N-|FWKMTymB{$뚣.RJV<1ttwlnʱK^DDDDDDJEi5]čٛgnhDDDDDNuUǕ[SNDS]."riSOD܎r)K\]kQ\X,n=31^is[!~6^N?}3O|Y.Q)+DZqb/ףݫ(( F aGkS7׺>>>U ,=njsOT%|%!ThKl/dճlen{[EU{RʥwUށԖc>6_Fn1;R>К;Ge7O-%UbyTa|YM*j\+)Q)x;γ?.fuxMb3*pJ2w '+ ߦO2|ƒm J :Gg}~gҹT{ ^ƐU QWQ)R yFyYw$}ś؃wvMuÙ?a..\ݣ>.rYߞr^V,=FwWkOKG**,D]i ᅗ{p->9o=AbGb/.Gd’=1Fh@H7nGw6Q?ۢ&]]*5h=CނGu3|vnyc^vƞq~ Ԙ"juS`y3M3n:WJX5N7o+ߙڑU3c>u|~~MYwLE;i_'*M"r8Wԏr:xf6}Ox3l)ƉޒBV?לϭ>իafQ87frּ5Rf0~@hk6 800i|կ5OMÝj}'3r|hQ b'4MvrcŕK{ك9=B.wˍmcj>\].TQYxIS.R_ouYoaRWySP;w2 ˜ 01^Yҗ35џY;p e?zuѐR]5ʹj//%;)l%'F|v7{gy,O摿^6slmnWHMŒy3y8eĭ)!Sc+}sEwY+|^LXu}:s,Y[HM'?瓍}goxoU2|@_o'dyG^YĎ-[4A-p]G"KŊ>}9)O%m{,+ޜ݊'kV(dϠv=a-g<یGG%z(_|5g.(B$Z}ͦ}HMojBLhtlK.w}]~&լiLh͕)ANKu}`a]ݖ42L`AA\irA4[4Ydz+SHw^yr~7Xw՚bUQ)'w].Oy _>9w>uH]kЍr"7k\'ExT8fqh7z?4 3/ bwZ9TTCqJ=^t{62NҸG1 jǣ‚1d?k2vt=3azLJaĘ$ a7r;5?pfJd]< n8OZwφa`jp}{ϛݲc])|:˱Toʉc8Pl$SA9:823pab0lOa`'` ½9ډwYRNl³/7SѨש& C~"=4tniӰ #B x/ _P&R>8/)Q)y}b`ਇQ%G`xՉ!&x3`(7Od.=yxQF|8ug/0 pz֥Jx(>U2#yLF}H~Ո|`܃T]6Á qLJ|6y?~E6L_OEQZHg_/dcX~T :ww% h1o075po6/&R~ԩSu'7w |k>4u}'eرe7/<Ϩ۸]`fZ )04wG) T_R ^w<ė/u$07r JaeAsi=~ q3=75/b7n;›OLz6$~~]2{2O\ĄG^fms7v3nc3G:Q%"ꗽLְ7۾;w%ɹӺ;g('zbbčd}<4^܎+:g9c{>4~=n~',2?Fѻ^*GUDzy98y˶ssާ[ܥum76~#DDDM׷t/u-f=z1tPW!R&̝;}^Ծ޻_'==v‹R(?NUW]á+"Rff͚ur).T{y9WbSN[n5G]Dܛ\Τ\DWYJEĭq'"e3.u樋[S/EΤ\DWYJEmp8VHa6q8:\DwǕ cӦMԫWOqM6)Q?Au5P+QհaC6n_ő#G\E #,, :\DwǕ[SCWDS]."ri껈Q.""""""F%"""""""nDѪ"eʕ+]HԺukWH9жm[W """"R,[ew7D]DDDDDDč(Qq#JEDDDDDD܈u7D]DDDDDDč(Qq#JEDDDDDD܈uOSG5=[϶QP+ 9ë'Ê8J1dObuu"däէ_C?]Ss ;KcǎUV~oٲeɟILL0 4hPDDDD(Q4:G/e¤\uS,> >v;=wR`i\ }4:J׈>?ɜBe|o6<0饐EQ1-Vl]pLE}`~|]S.⡼+գN1MAuҷ4ADDDDܔuTl9ACDDDDuOepx\JLL$!!aR. H@DDDDD(%" /Naͮ42:Y:>D]SǯWel% #K2sHTF9?\""""".D]SpԔսPޙd:\S.⩬_Jᩍ/ls?ACDDDDܐV}T: >\ٯUD,淎cP~6 -PDDDD3)QX&B;ҭ702Ƿr].֙ƕ]\aUs(QdY f/Ȟ6vN[Qk$J"""""樋x*(ߎOOg 5kRْgs=39b┤\x{]Y>O_3OfeXRЋk}\R.LW$kLA1T $䢸Jna7184p8KG]Oehx&f5^/Ɍ5(Md+ Z]DDDDGvD]^:CDDDDD9"-}J2IٮODDDD3G]S,ah/| RUEDDD|x0K^v][[\"rnjO'exD\!%"˄S$߂d:]樋x|66?稛9""""".D]Sxչ!W7樋 }T^m}|2v琗wY+>> : qCJEfu߶TuS.l[Dݞ;^JEDDDD\AkYNiES=]RRqqq%{`rFɉHfLmYZPGS^V}Q."2ElDDDDDc)QTS1Ќd:]ބ5~ӱ˙IHHpu""""4G]Syw5xqz19ً DDDDD<uw`Yʱydsi-e^NV}q:%V^M۶m)((ӊau"h\DƮtfr iqz"U};}>srJM&V/ vf|J99uhm۶uΉL޴R))|0z?\s8Ϋ#/^ goWΎ1pCtsfP'>>!R.=XGؚ$enm(Cm~I|9a/GbH>Y8WWJr"""""Uţ^oo',民m%4&[Z:ߎN%K-noWGqEwc ZXDח/)Iw.""""D]<ӆ;rXDۮMG3~L}goqagø`~ yfzhJix4 }`$qZqƱL~VvbOB+ŝMo/" $*T09!9uh\{!GÚť8%S2>֝>-D]<ӆތKIgpZ]DDDDGx4 }8ZLNۘGv%~um_]t!;s %|$J"""""uhNn;;ÿfӋۮkK}'t+A?Sbb"aW]DDDDΡќ;&B :kk/5}97ʧֲ?3(QU-tVϺaW A-NR7a=Ĕa-`2N|9"""""."dPHJ+YdR ؝KR>WssMT.9"""""rq8=i}/q/LVaJet:W2qrUEDDDDD]ę|L:@{F<;ˬϙSŽrSeӷ)~Ef˜,x8yμJE.A>|.X{GG8Ux y癱|{޺|11'89&UEDDD|֫@D +cOaSKO7ݐfӹ| uP.d!-j X6B`ZN ic/$^6}n㶫"uZ"""""Rug2өGoҧDȝ]-(W7;nKZ>wg#P """""R”8ً<{qsF3oVoe zz8G8NJޣ2RUEDDD|3}(-}Vܲ"۳9pOÈ3sDsx3cSԣ.LEo+C|DS3ofI-g~At[9;fq PP껈"NVrO'26@ص3,:}@Mb -6a6MT;"`{z?t{z ɶCצM4DDDDD<u' Nk*Ӽ/&lNgkwέ{X"v=[aa6ΙFs v"V5ө.$^5x0i2 v*$w1' }2982ع~'Ć{{`R݌D4iw95EdEplw`;ϾؙS{tK;W/e;*絡O)!"""""DO\G0o wyk6M^%{\\\PDDDDR. h}]3s83&/ꏘ櫖3oa ~tADDDDEq[&G%0q0Rɴ{h02رgv,AQS5DѪ""""r>jx* omέs+P/.ߢ㽯 JEDDDD\ApOUY0%CWpu0'110hРC7UE<)#@DDDDD(%"ʫ5w9'eˁ N."""""ΠD]SƨG>ㇺS/"%|?f:@Ϥ9"ʧnHus17o7>^EDDD\`رi6d a0ʦcf|v-Oq?QMa$ˆHuB˘VKue&1K-Uh 捹eOSrkvg3c(.cgb/IHHpunaծAܔ>r!|ȅ!euOY Ӭ0%eyxɩAkٌ_tS=U/FRRC7φ\>r!|ȅQ(Q8v~1]1/3M ny?Z4亱 #/LG!'w%$ċcNy """""樋xF@Iw(GfIlf!]{sc(ZϨ@,^l6 ,, -runaժUAܔ>r!|ȅ!euOcEҾ  M?{ 8O$ͯL TDDDD#)Q4>8W!"""""R."Ƒ͋Yrބŵszw8[cX6e[g ZNtj\.,8+b۾ 4j-ьr.7/"iԡ -c.HgY]+vzݙ*գmˈ ,f%#|.xueH|k0rIYe3WwjM Z#&q,j5qu4D]DJ7 WP4 f,X_뚇iexbu1KEqk /ɢX]i m#d;NW]]KfXvU ܉AWGcڷ Q=sg*NgϚ1m|97f-Bp 3oO Jtkʹmg8faRJ5|ROgΟU5-YY;Y U T$" kn5&> 5zkc'??[ʓ:H再cR!&uع3ް=l+49isQ$QI f3~MbwwM` Ƈ( #1עI\75"vsіʆ M;5'& `2R]|;Ñ-"`#<<l694_۵}i?h(Ci3o#[si19)&C8v8LJ929J zs l(>ALC=gg;G2gGe:lF%ux{&-I6h8Q@PT`DDR398qMW_׊v(!K'̀-YADEbDr N|ر͘uavP~& Z5fw;oM2!qcjR`"ZMBͦBeˆXW'T&f$i s [Z2fHY;0pyo mf ),,Юr\ճټ>\I<EllN7iԛ f 8#ގ9&5LIlq Q@ƍV G/A+Q;"߰s,y[#G9vvA,XB&8; ]rz7H"R*LAtls3eE-ܬz ߸vtH>'@T.tF.SH#Wh>\st^Ӑ(iݙZ /pU^.q0e> WdۂꃟNMc|W8.Ҁ sHٓK *VI.wr1UQNşog8;Jʾ@ Bueb~` F͎qcѣGCuu"eܹs۷E{yngl/<{`ʕm_KDDDDDεl2Zn}v(^Sҭ[o-""""""N%"""""""nDQ.""""""F%"""""""nDQ.""""""F%"""""""nDL__şF|x"sKt|pE|Q‡>ɶ{d} 2;3fODrǬ;f{fU9HD]DDD q-X^yyye{ T}\ (r~7:ppsd6oC2jUNx_v?]'quˉYG-!aç7ѢZ 'H_4GlH\\]j׬7fQ(`Έ4]tW4fw%ԤVg~yS5D(=?6]kE3| YX]=q5pCzTN2{Cf&<Ѝ5b]6My_Si܀6/+" m5g6o\SK/PJEDD#O;D-ΦyruLۖLrr o.yjŶ|;zʯσkf?Hϟmrv7?}3<%m`-lw A.&&6MxSn~_b_3w4oOM+~gᆪ>ϔTRI !ФwP@PP_ ޻{G/RH*H{BS$L2Z{̙3<3ٳ{Cppqf:8yhzvYssq|j+7/cL4Kv! 0ҟylv,}6STmN8{}""~L.H"##  Jwq|sg]X5>tҀ;viXkf@@F17sYYW7,DwubF\wsk 6ġY bXW7in$O9cqa|/(bʅC"{ \ۉ Xɓsmw&#vÆROJ51,J=ty#""ETDDD +ɺ.Ž=_]Er=I80yrXq܀y|4 !8zb&3-3"dž8,x;-Ttb#b%BSIsCıjVA'߶`>%]PygH;?0^_U)bcÎgXЬ 2]eͨıZPQ( 5"""RYztb 5%ɷr<29 DoDo摖~tãU"6&>eG&ƃ D]DDD'[nu5ps(Fu@i|d' kۑFۦ1aEdϟ2iǑK:}&\@\>4)FDw^x yV&>0[6 )0oDZ9pcʄvL\qe5}1koƳpvDD%"""Rn\(/ëVK[n{Jv<֌zэyI:o{M,'^Zo$侵^Cg5皎\./>?oymt%VwϴxǠ/Ev }r$wѺo)z5*neV<գ% k≃ Q;1q-k&4ۓtR!(3ލە勈1zhsСގCL={6+Ҿwܱ'-- z8*K,}X"""""rEѶm3N*^I&ѣG7vX%""""""">DQ.""""""C%""""""">DQ.""""""C%""""""">DQ.""""""C%""""""">DH197ǥ-ocF xOgjU@x\3LǙYhp];V\6O|[U#"$Jz8Go70z˘A'm?r6z&5M+^!]gg!=Fpo\ٸ2i)u} `9)`.çI،6g,ݡmETL"F~sa IDATӧ1),D]DDDDD8T&=i{YWo ϧ <{s]Չ<%s^/K8ŠWk1m#LL7}uy%o5i[.cwp0 wvߝ< xfnc%+/>mshoNW|oƴ[>eܩIr;{Y|?0H.XF5)"[+C[&pyx-5o^+x%5%""""""`&O˹MzM5,d2wڟUX_ʤqA\4~qS9GDnx-#L^Wnjf;R>!V)L!>.VXӹgfqZY"hu\;~ڨ9bY8pxt~ .Y wq$-:Leo1㫁T?p&iiҊA`D\@Afc5Ё}% @D]DDDDDcyt2c_7#9dlY#+CH$(FMK+9-a&{9a2x8*UB.xӏ|N]xCҠVm.l{1 pp<L=l$mMŘ\4uZ^Co㴘\xDy#7o晍ĐX8k Xuw~[w}u"sVz[3 BOV[CB&YqQ]|M5*\%# +AW$mOʾ||V ~-4 `Br9;-ht,_tgdbË,ݲ=)X7c]?&S^۞YB&lԯI[ JEDDDDD$pa~!瑓{"tPRТ4/J[|b TD.|̡kFһFW&FP<x+3oG A0,Tv"Fh\f.9y!T8+$Fk4!=͸zUX *Gh@@L+n}ZekN.GY%mkFSkU\5zs= @X3kAHh09YG&Wj 1MM+wuV֥(?D”WSz͔I}%n~Ag;ۅ|ʨniauP}ٰ&g>1qca6,VXѡr !s-UwFAP^͒t)\ҷ 9Qn~ays^&q%ϻaK] "++܂cY-_omnH;3yf1vK0f?z׶yNyJq;LV\qYawz''Fp3O ;oӘ9.p$ŸO̙u֩K[gWnm%""""""JPδ0i&`}͛aC#QQ=+nˬ7G׆xլzDFXX$uvrT>i$t`×qA|49WTނ?ĆU㊉m$ZYk1ix+P pUsFV MLO|^Hbsûj4@՚x6f܏\{A{4gO3Fm:q gϦ_~E;V㤥0aBS;g6K,}X""""RLf nͧ]enӴ'GsR'pCVKNh"ڶm{v>za&MD=λرc{JDDDDDĻh<|ҋs3_`/* JsIk,/LkΨϱ߇ѿFDDDDDd̕ގ0e YU7{;"ӈQ.""""""C%""""""">DQ.""""""C%""""""">DQ.""""""CoѢEADDDDDJu2m۶ADDDDDJ%""""""">DQ.êV=_#+""""-J8:KH0ٳ8~{0 Æ)YURz"$"""""լD]Li6_1v핬L]*ukH)8.M'#0 7СVvݻ v.(EDDDD,OQڅa1uRbOÀoqPT"""""4]DDD-Zqt-""r6JEDDDC%򷈈h껈H1Y"uRtL%K,0gi&"""""@-ر#~;u)(Q)%ScU/"""""%oʕl۶v;0tR9uRdICԢE ZhA͚54hP%:H))tw)Zlɫw]*+Q)v1yR[H1dzwR[Q.""""""C%""""""">DQ.""""""C%""""""">Dy;sYv-iiix;"&** z;\D_hǕZv-NN:y;bYb֭Sr)ݎk껈4Zh0DDe˖z; \D"oQiCD4#ԖHYv\4uDD>""ţD]D|:x"R|r)Kݎkш[/lͧMaDMgWFa+M%=~gE߆rv&ш9|'iޟ\ohD]Nwj&܀/2u͟ gQ܊[^zK\/οO \&\r:l*(zOY)uvKlI6,C'whSO.QJY*ijt-q[]co73n6Pј/0/-,Q[OWm0rq7e/+o{nTTYIHY،߼<+x_#+tF\>Lors՟H_H3NH7ȟM=.7aSje*WIo<>@:7áol4}LND\+][v0Mfh$&TJN L3gnmp=o|2 Qr-%֛oJݸ$4c9~|~z>jP% UO1ұ^6#|AUb+'fr]C>7b-q쩖|jQj~cc!g lUi9u~O= sQWӪVUnɀ61)|qU[wMt[{L!!VQpIIIaʔ)g-.=VX) կ07>Bj |}&}_y6 3=V…f܀yh*ocq,~eK.ZT֐%tK3JЙrjmu;$1 i# ?**x/iJEʁ n|E^9Z Ўgָ9dt}-gocxh8u'/%:_bJc.9Ղ==T&k6bZs3Ys7meљfd>_v _u~哼2# pÑ>o1/xufE31Az1v/ʎgҁt<˯|'. =(|}سn:o_ߌpBzԦ<>ꮶmIg巄JZ v)Q).יMhqy{*1ks 8+}bx~I;]=y~U\6n}/MN!TOi|6oQ:jR7e)M _^UTirn0M<,DwJ}86bÀI01*^H!ޞ˝̼khs4 oEabNL®n3YAԼ>LG֥BXt%CZN:عYsˉ=rM/>3GxץLV̀я3+3'̧-w!ʂю!}+1o2ML7?=lZsZ**Ӈ>}w饸[#ͧ1c kID H}[]ۃ &hn޽f {Sp-'V'ׯi~a5h\'  5Η^Bt)$ꅾ>jO` ϴ#:W~HC?**%Q<Վ$%"AF?^ᏜB1o~f׬hWw2}t|`Pm\c,be[ٙm_ 4mx~ГBN#ǡrQT",`//3슮\ܩ ].{X*e,V+nӕơPT8ޑTLEʁT*b?v|K q1Y7cZÉ?6ӊjv<824CǓiXNt莼 'v4BJǞct,QHs$/9vZԪU^o%7'U_ŪRʱPΝiC{ђ;G4,<9 $О*Uu7\8 ń^wOϺԹ{&O7AdEFO)5R[[m5,C9 ܛ,gOEG˳y.>y/=o:3g{|gskrʔIs/Θwάd$ N͎3.:6f`tHNو83ԢWn>l3{ko~g4G6MӨ e2w -&Lm+b=-L=W%&}`vlj8ͳL]-dB)T-j5}*2)@?0n<\ }o1p<:I\m}T&6Mo[G7BuuR~td둿Ֆ{-\C뚕\ GJgv^,xx1zhsСގCL={6+Ҿwܱ'-- q:ʃI&qevµDDJb_~)wFy\DʢvQ:'MD=λرcF]D|)"RjN\DoYJEħs'"eڭS-m֨O(EjN\DoYJEgEEEv\V'!2bvv(>Am5Ў+QiРa"">v~z%G-q%"7n̚5k믿HMMv8""EETT7v(>Am5Ў+QH٧\DxtwD]DDDDDDć(Q!JEDDDDDD|uqK,v"""""R۶mr\%"@Hh"[SEDDDDDD|uD]DDDDDDć(Q!JEDDDDDD|uD]DDDDDDć(Q!JEMTnqSkcm3Vc0?27w]:ъզn%LL1Xq1$;g<lƌC6mnݺuU.""""R(Q7ֆ { ]mĐ70nPU ||GҪ[%"""""ED]TpW݋'&=Ȁ.58x"~*z[Z~A/^pDDDDD(LNDDDDDDćhD]_9ڗGh|A Z_͠ KDDDDiD]_၎1+(QWyoNd2:Vpy;>?D]_/O u";V0!'QWxmV^YODQF!JDDDDD+#e"""""FSE˿'b͞]DDDDDubghfҟd+_%%ቈ)%"ʹW'!g}bxu:9mkOZjdS.lu4ǹ g?cEDDDD"Z˥WգZxؕŜY[IcOJEAŮ/k3k a0 IDAT[ {til[JEc?+fLc5Jw42 jDDDDDBkE:VvZZw=I5KpIII$%%l"""""D~Q7OcXٙ~`ȅ߳7WK2rL#"#"&J6Fx5S.wL.N% ss߳|g:g;9w?ul|Ili}ON#pzFxe8_DDDDDND]\9΀,uo:r%:ج|<so]z9nkΝ'sc(,IlI>iKԪB"""""D]#$WL7'Ng\vy\rvvc.F"ޓ ' >5B<B9)>tP }dwxp"""""R<:Q.aY}AV&\DDDDi껈'"w}K%;Fz]0e窠Zn6lx6mvmvmvmvmvOl?/S.Il6Wa=`/shٷ&ﶳ&iikkkkkcСCH0{lW}cYйûYIuQA2O4iiiYd ۷/RL2fF0DDDDDlѢEmvG4i=z8~cǎugl%$X1H]`aΔv;-[z46~=ub &8(c%r0g{49JSEJ-/զM`wz)|VϭQ/XQ'FU]C]DDDD+6g:ݻϼG!zsS\$K`"""""exAd|ӶQ;dE\q[\w.11QIH(Q$k݇W.n۫Pǣ lALޚ+a(VkEDDDDBSE<ɰ`wک0dQ<K|<&qkԍPbF]DDDD+x+Ch{Q+Q6QIO%{KryKyt"""""rJEi%a% kEDDDDP.qSi rWN>;^@fc:]^b|ʏ pL"""""D]BbCߠU-82lXj-9=xs^gg5"""""^D]"[kC8*N#{oeZOx0Y&*ɆiO`t.B)Q$K]zl&8 }JBKK񞋋/oO:v~e})aJE3DC$sgզIwv=c9r)u 3:x"V;>F\P8}[YNbatյIڐ RX'ͤZPW\No`Gv:bɦulN'0.zvyo&""""הxPhh\K˺ADrٽ!_g^g.qBf$a`j8uE<^3"k>,{Du[WfDY{<2c;mጯKh;@;dUJEDDDDApO0lMwfމ+gl˩]#Yq٥;|k}= }>;'Lj_,Bl"` !Z˖rt7Oe0[=S|/zҥ{}EDDDDDS.df~yD.bçZ3?Rm%{$JRrHSE<ŰW'zg5 s_mF>Ml@i;ٰ)f>{bbbV(""""RN)Q7촽:m϶KZ&ynOdۦ:5;a өA/Q_\0j-?x;9Rضy.;a6L3 btBx"<ג]>>35U."""" ꆋ+2~ЗIӳctweDRFL־l' DvDaBOZ.{l)F!IDDDDD` ٞî 8b#uS.Lrod &˹a rx"""""~Hr~tR ߉"Z\&-ގODDDD?dr"ʱOF_6{;X<7v"""""^rl ,n'.Km1,ux;@D]_YkP+h?AY(swb]H]DDDDʡ1cx;uek̈g:rz]M-6 Q͒9 X/Bhrq7Z'vy)- Ux;&rce;Ưл0ǻc=c ؾx.#:2ܩ"-b[bdٲeA|r.zȹ!euV@Xiqp5Eyx;ӠY! U0b[ha{Q$%%y;Qzoȹ!eufgWӪ m׊ڏI{εZ `e'2NADFH; """"R~iq|ڧ"ngǪ{x:8lS݊ I;{.]G!R)Q7$kEQ@Wnh$?n ftKNݨ˨kJEx{?Otn!&0 e*""""◔>|;OJ'0wVD% xRnumJ&N]4O]Xph#2p"Hhґέ\nY}G:uwIJ 5QRXżV3MpLwZab,^Cy& RCr 3cѺd9-6.mYAKC7EY3oGsNJETi3oI@lϞUo3M XϥC$ڱSeY@ǝܛr/K ֌ ̝6eqrqWe{D]^cg WqS RWNuN"qzJӕCfwM9W~{2%zp>{Oe֟ҽ:qgm^2@$)&Yv]) H4Im'~Mh`Pflr)Ynܑ Fd1WLfL>3^[D6\N׉ζӉ˰a++?cJ a9qN]n͎ЙRVdށDztɕw6v.Ån;ej+ж]u.I'N gXrRZԉĎA`ԍLf˯3UAt4CXs69t29)9|(7G38V-/0*F%Ifz&Ե5.Zr$šDzI0#=u-؝ێlr&\޿ v(,N# 1AzV8UN"1*S!?\1.\. 0\]ֿI mw%Q0}H)0^]>t6ކF *3$Ԏ#e7qgmg<ՌBe,LdlhߣK#b嚥2kdanrnb]j<<!$I2}GW],2HB!2 2E,((D@+(*ZG*R"Z['R)V.HŁ`Y"*0dq P8\8瞓~s_{ӻwozfRYxuı+raAfBŬKVԥN|`b>`kJ!J: cak*csRǰ}O6wEHLJ`6<+t)LyIH:v6WtkIu3xZ5bϢzuJ':ՎP!y?b5' l6n /v>7"-}D;1Qќّ>d[UiN46fJ*4 4%]vXDD@\l)Dzx*N{]%֝uZYHR[S;):@4Na_cž?g)ڰU'R:;pzǼ=v.@TR29m. )FӧѮCEʢgϞ??yrss;C}g͚E6my.I$I{1cMI޿Vtw'Ns3f'%I$I 'uI$IˆA]$I0bP$I$)%I$I #uI$IˆA]$I0bP$I$)%I$I #uI$IˆA]$I0bP$IǀB8"*Or6|]x It)az㔘 4 6l}yre0N6rŇo]|ϫMlrdcA]$#"I6QTTDQQ+G)tADJ- ^ĈEG 郲®ײ5)IķKIt"Ʌ\5e m߮ܝHA#ro+%0ah,aM=0C;\֪)uW~P5cuY4j؈U@ޘ${\ڜV3w3"m6u wKJqʠ.IQlZ8g\KF b)cѨܹo.!'g&w&((9 Ff2K~pX"7 ]H7tL^鱂)sKMS5OVٲ,8! +;|8? ͕pы񼷹pL\-ePH3,^AK}"VL%% =/ҥsxK BLJgŊ˳șgDL>>I:%I1Q]V*U")>>AU5"H ‰4:vh䅀D?g&b4aG[di|V.nDPC_z5 z*4kN')4+ڱ~|Y "Bڦ %S4Id2SLs?яr;3㴤Dחh|U&-A@27!D "@D\5rʑ$O%I1"7Ma_ZM/_DӸs9a''}DDRkJvf l%"Ar7B%*l% Tj?bRqAbc1lddqA([f eo[q܁xWOB–Mm`Bn=zw_~8PI:l|K$IǠ.3ː?Ϣl!#G' kl pZDrٴcA;nQ]jwuT=5y IYt@ O9?Da~݉>TDƟn_q#IU੯X|j֬ۚF(Ct%Iұ))7GS73nd}niM+&('dy6Ϳy[RLvxD.toes(Z+/}LpaܻH>;az#kPf- Zl߯f6[ys$ݞ'ҽGm>_o{8'IA]$[^1dGd2=4>5s\>L[&x h Pq}7gPVV݋WӢQ5kiw_nzyT>7/Vh@{R6wʅ̦yDc2IDAT(;]FY&n-9Awg&Ɠ}R}s9N.d_}v/BVV={<7!ϓ ر;k,ڴissI$Iӌ3ohJJJh'ҹsr3uI$I‰A]$I0bP$I$)%I$I #uI$IˆA]$I0bP$I$)%I$I #uI$IˆA]$I0bP$I$)%I$I #uI$IˆA]$I0bP$It*م{i,&JTT3{EE{d_Ȟ}JZRE7]o:5U# ?{Ĵ~ee,p/g&9>*I9Z1Ee~O}ӝcw/Z(R{]׾[LBlQ~sJ *VA!L)SJhd*]&[wk z^<5M%Ŝ.o`]9!Ble/xHJ\lٵ- }i#~?^ĵ $ISAVx7/ջ!Zh#Wﲸf\Wp ?B?M#gfzq;ӕXLIIf>܄:P9} ]F-7>! Ě@ FVȟ5O󋶏Yʧ! ُ]9weS--u&]1w~ʚoenx}=qjǡ^?$z%G,J̤eyl^&yb-\ċWtDjG:P1]Cb\VMH×rG*X1:]ԡ.i7ޕmu'‘A]$IƽN}ϧr` +Ū<&\usֽ_<CuוN)`ժ̈́ITTQQQD3oԢu-L_6FQEk%؊͹q[*aױfC5jl7Ȉ/,Pz{K=^^~E]-Lv8aҥK榗ӹƻ ߍ_"18sI{/ir$QOnMXb81{7uvto[L[(ᡁUx}'l"ci}dݿn' bh٧7^b/ %I$B?tjRnӆVFͨ|!KJ_0)g<*f3. -hyB$ ).>"잗˝]TIIgwy9r.^ʪh#7iZ6 e{2F~zN?_r0F}?ŽN;;N?hztsv%[azj561mt*qp q{Ί}? %I$ʾ]BN4 Ձz0ymiM[ :gFE{Pe<>j3{ Q_@(.∏-$`\Fsy0uIIЏ)1.K`ϳ A~|/Մě/fC)7ON82r7GӤJs ]uI$IǥP 9݄-%}$>!2TP@ad v%PN>MpR_N'r{ܷdlU?;/+/eiO HZh֢ݭѽ#>Xr(+b(,dgRPOńM(y6P 1Rg|S Q@R^]w-%&kQ6M T[Sɵ\S_,׺ yk)p?{Bѧøyo5ٳTU-hߒrgKA <\Q$It\ې?.aYZ#H;"M?*\ˏO 4 b\`CO!gVB(\>ϼfcx<|&-e1<Hblɧx[ >Z UgLד)$U?e]Q{#j<{9[y|ԓ|д;F@lFh<]g|@;ROC_[ηooŨU9iLցS8KeqW=v/e9|n垎(h.I$Ia)P,ڧâG3k3 $hǓ1DdWӘRB*`9'2 Ij}+_{177v=uhz .[˾~^XWiDILD_M& j&ڊ[qCh^>ѶnGz k}B:?;0kT_FdBjԬI͚5YՓ+BI$mm)qTHFd2>~SDeB9we/x('UB Οo2eK?6t(QB}9uHYYYxP̜Ϡ֧Hb3f 33s})))o&NHΝ7fW%I$"wpf?csǀ"㢾cxpr m}@!h 5I$I:RO?=UE\l.JDDDDDDDڏ?Ș1ccSMS勈\XwޮCDDDDDDDYf.Ƨ|}}K]F}8UbԀFx:O苈%A5z+3= jhűȎtc[ `ݷ8W/1}SNl o]w'WF[K4qLϬ}[-z݇pY^ݛwl@DD߽=?g0p@`ԩvm]}Ddl͔ve'f#"lMIvNQhKV,J zL#P}5ƻ [q3^iд:`H`_ܳ(8KSi&X"SNe~.~&mQ?sL6nX{͚5k9EDD\ʾUwe#Y͆4Y1s38{4Mm&Uz',DխI3l[A۫zPn `aLYJX! iLvuW aPK?[Ft.tmQM|b-0bhө! s_ 6ߪ'-+zB&dM s=!zvVx&Inz~Y]9[ѱ^n"߿?ӧObŊF}uQM71qD9r Drri7,""J΄6q՞vg:?31l^:tt֭[ŮXg2 ۵A$:Tj<9f9[/"ƍVn#U˟xBj4nM=4_wL$ (f/vϿV51E8,F4{6%,[0hFEDQFzG\oVV ~uFuQoW ""Rr[rݏjժ8pUsF~FFӇٳgl_DD.,x`D@F4Kg=dIA `p,/,N7'n-/~agĽ=VN-<ԧV֦ڱVnAV22$o2r86L VKaPB#+BOVNӰb)?g.YV ̬\LNw'GsKMMSxV1ϟ~߶L8pVbx{sqx14"""hժ*UbvወRHOײ1 5_AcY7>^δA,H"1͉ჯ{=%+.>c/9&ga+fUlI_r-uiu)·5 Rc4Xrx Î ӎybǖ HO#ˑAFܼ0xx{cBD!\Я/7#GED䂐P.6,fLp=7xiuV'&&ҤIIMM=ϊܫѴq{Wf=8gqmbԪƦ5ۘy16͈:HOǾ|L]4 ŭ}rb^60k@DO_ZMgゕ/nax,q ̿ųIDdfK&x7C[Y &xTCvlXZ,62 l`}"5vL/y~_R\\FDDa%e/cd VÄ\Bep*VomX}éپȾ5XJw!ŽĪ&`DZuuQVs`o$Owꆺ"ױpM4lIٸGBZPe隢*O?qJ,~aZyX{0jm}MC 2vyӫrTOg?/5 EDD.U[f_8a0=55cZhրkv?%""ra돿n Ӊn 7l[єx+~{{f5a 7<즁g`!>E7$'%ٰA`x(>͚yŧccDJ7P22s w}pÂ-EDD伴tҳ88J#TO1eQȩ1I?Bœ[ [Vq%bHf_eWyf.q'=pL0 LN+^ f苈\J_=KΫ.׌lAX` gt""""""ri(%ѷPwD]?lX4y|>Gۗ3!{~A6)e[@gyxJEDDDDDąNo~OjroqW6Ejugs>eVyz_x?$?m/~<qY,!Ժ&=pcλi[ÊD w*qsXs7߬g㷂cK7;0e|w-q/U]|.v.W/3y >5}b֝|o7V#cFh᥏lq+M>|i{ƤgzSt;"""""""NV[ F\MeO5WI8i1VZXh#֕1(1ܽÉnu-O|m|ױC^1tlŔDTLloaՊQ?wH$&ؑ7wmi8]X#GPe c蜫0yW0Ibk#x y{DubL ߜ]vIrrC9oH5\%}K( nVLg|| AǦX" BztC~)<ھY#|3O&S8LJ}WlXI>66/}0`%U%pw+Ænn&̌363|P#Ϡy߬1Z+]t:6VgSxZxW%]X,tա֮]ݻqu("""""3Xg0='I I8p!1 iOif8 )DžU3.afLu/;랽O)wO4f* ƚC8JJKX#wDt~/6݈l;g#bMc'JOS-Xd $'',W$???\2ZPo>ch緟oGhըo 뒜DG83%&c|Aڲ,"P k5$ ikd?ȿ)[PVV?kkmLr_όIח%FdG5w ߖa8Ʃ-e)ۡMl֤QEoBk6l3"KLrr2AAADGGc2M49|0rʮIDDDDP)S϶s~?7|[Ï|O#yh>z>oGp6W)4~7jyUtV]ݘ[b~ vM|rJ3-;^P 1pM̲h ʗ x,I6\ϋ_HuOֳye }^<1[2,xVWcrgɽ'BTTatѓ BTT{uu(""""rXsذag_̤:ﲋL_brrOg] Nb|(B\苔;+rMTwLA9?d:'3z|t|30O8<n#;$24Jsw;/ojKt?uN%+5YD[a憛Xqssj!g U#C ֎SvqvLDq˙ư{9tPa6=Uۡ4r>苈%".Z:b&.!ϓz\&%>|x(̏c4pt,^XM0F,ɗn!Ԓo/aߐQs*HKݹ#D|Zx Ղׯlq83u>s`c[`gcѼ.f?WtqL_'Ӭ~}s?.L 0}L]4A-cy0u|~Y~ˑ~>ZԡVJNkZգf{~1ir$M|ޭPvz`΁qeNʹ5|4J׫Cݦib?lˏ^K:uhк/Oz:=ZM $(qK9df,sai.gJ`^eAW׾}g|vhQOJDn48Ѽ7upT ]=,esWw˙g#Fr=_{,y6\̚}/.}f/}gΆ-q?(S 7S13wL[φv] f\Wbč̘?ѯ[2.<&7oe|| z筅ٸ5n_u*npzB1;>foΖ%xjN0Ollχy4X5""""rJE\є`C,8H Daz+Qa&$L$ ֿa[#FA~gOy/r˕iݦ+!}$t>*&fcyڿx~-<#/<XjNTp ]9*/^C~Jf,eШTr7=jU93jtW)!,55 vMH IeP#CqNv53*<܊9|0^/7f/лS-sDK6X!G2owpYç&z%.\zADDDDέ<dw,rk ܰDp؉?HHp~%r$'%h^]O׷tɜyd"ne?ᳶ!X3owp&jAdPDDDDΎ}r`m&z{n<·.ي'}N3diFߙč/1*jxvY }y,=ֺLJ%|iד%tO#Ö734Չ$l޺>>ttر $u <'/:/:̫&p{;Bn-jO9vY@Ҏ/?/8yN϶*)p8gnTrEz93M(8O  +w:Ow!_4gwo [ثޟ?ǐ )~ikZ]^|޻G:rc&1ih4^ ͣz>Fvv/K>dƷyP ? Gy!OݝxgzFYyp8~M_2'a6|C-&@o> <)|M#eC$X[c8ksumu;DDDDbƚÆ su" ү_?Wqԩw>[ #<4I4q_> NjeÆ hѢ|25i$Mn@/qp=+wa3Ǭ smܜh$OD"c&$ᬋix&~3>LDDDD %"gWƣ[z:[ӻ]CDDDD\DryԞ;DDDD.\JE\9]ODDDDDND_Dʕn: K}3`Ag"""+W: .\DDDDHnNk?%"RnڵkDDDDD. 3f8}5<)rNC9%%"""""""%e!w!5+""""""Uc+:6%eZoU'<&Np]uӠtX'y5gҥk]4[;*ջOL5͓_rHʹ㉾FY s蛩yDDS^;Gk;Y W蘆t-S sOҺ |`OլFWr]䟲5mbiڴ#TC IDATϬ3Үn 12e$̤WxG=bF1/IwӫMCjWNoy/z|59w'rv+^tUjK$a3yeԉAL~qotѝ""""""oMo1[ e4]>QɡGsϵ|{Zȓa2$ ~Zʝ{Xlxd,vA+Շ Ci/ v#)X4&RSNj` yo=/fȷh4?'%)kvɬYqG6W#"".NVA]7fs2qV/>HJJ#W K*tS}~}9L7q+,6pu"ߙǡ|pWҰotp9[>D;YvwGǢ|L "t{|g_/#Aڲ,$n׵bH$f*mY?r#  ,&0sێ4OLOuo],XnCӆtrh~m))e@)Ofűpa|KXF 5+ض2mN%""5 *^?=]3^ɻտ]hsk۷~_1r̷w4_}7j5vQCأU*WO1g0^z}A˻н׃m֛Ed-aK޷oXOI1y kե=hZ;Ik9KV!S?\'V>',^|Ѕ#666lXT^lpT4UՑ `8}B ݺ@EdDJD_DDJgOw'ԗ=p^Lxu?ʯYׇ|[um w eۉ59 X%춃Gh ]{pokA9 r:}V/z&9IY,!ssqZܩMU\Gĭ |7%]ͣrm(5 r$I-Q%,džN~8Əd׺LқzlT9d_r5\VӀ͡M=@am- ~ KgAɼ]{B2#왰t.usv">Y k苯{j0O_Î7 ňi*ٕ"1ėMo僽NB@`QUoff}NfHKXCIk澈ɄuU@h@6Х=C >ǃ?* ZwXR DDbm/+qxvfఱDP I0![` AjByEټimUc/_.8KPuxNٚ/"rÀ5 i >њC 2Dy@`5;ᄬt((4~vsʋ"rjJEDaf1s^b} 󷒕jĕl9R/f bȨHrχu'"r>3oO=9E۽!?j@p'GJEDXhܛC۳uȲh(zw'а Y|ݱRC4>Ua + wSYF4rۆW‘Oғa.x_U"'%""rj ><+=[>igDu@mNoL'a$]a1 |+xPdZp*t5]UXmX=-xzZpS%"r~1;w&f{t!>T,P vr+CÈD+|:r!'m HEΚҹ1||s&iy3w&'h:fڪ|ThLI_ |69L8xOEqm :B>wcy?n9?4~V xsp6)5_DVl-y+ f읰^n/r`pyh Ǣ$oMUCcgEԌXsذaeR… ֭[%r>Zp!}=}{C9G#""k@Ϟ?89̛1ciۓ&M*nݺk.233INN.˪J`` 5jpu(""""""2MwڅbI&eY\ݻqu(g$55!yH2M333ܹsYV)BHHK,qu""""""2M5]_˅ԩݱlCDD3>!y(~A(((DΑqyD$TPb{.[>V Bؖwn"o£'OsdYg-8=(S''>7fxs8noŧ{-IӾg2}r{ѠPڄfv8@5(^me=.""""""rV\INv.^>>x;w]T"""""""2&>RJE\lٲN\9n:nݪd"rru_{E╚Jf\9׼ysRRR\yA}\\~hhhYV'rߺ$%%aq MS/@D.e 4 dڵhѢ,Kڵk turtr'"" DDν2MkԨݻYd eY\BCC FEΒNDRcC8/K2_/&&ӤUE.f9?1ջ4=ڛidǾiZj5/7_$&O7㛁>FD.0UOq|IO}AQ_ "g@ЎS96I8._HAipli̟O'wNJ}//o?B*7_! =^򾿁 <"vlN׶c'wTnYOVߙʥTg5vc[2g-=<1WEwnui=߾s3 nS`*6ɣ3?PQ)JJE\or[+<9=ZOoϛ6ƣm'30ե^ 8e;YƁEptrصuZnzd(?ހW@2φgSۀi}qY.±e]ʥ[ O %7YUڹ{~jCo{;7[넱xB2O~#!={E/8ʼ/t}W'bKՙ;q~Luߑʅ^\I+y67ϱ0ao>ί=BRg MGaE#9f:6?jLC7a˟ͭA'naWP+* i-Cޒ_&%+<1Mw;3*9#aSjvovQ`ysQFtW!22v#d[^Qy;fLgjUrn?(cGդ*V~Q|!gz_`׮HdTU_c(""k;K c'<'E-6<՜O96*b5:Կeg6r֎5Y^gEJI(^@(j4KKHt$&\׊~j5ٹ'o=F~\Y'**XIJJb̙XWZ9^m塆s>dwI W*4|AWSRVzoSYV9ƶ!x v{ԣJH*TO-!ݴ툾3߿U_`f+:OË*= .7r>s5%".1^범1o滆/twE3k\'/& soeC<,R3%oIeӟh=_GpА+hJs=$kb&3jܳ`;#>qӋIܔ1WVwHQ5gS2lW .@EO"RRhh(f[)fTLJK6M`܂<2Y30ϠW3uLP׫c3#>m񏲬ly 2s>꯾}ְ1M\tyeLۑ4َ8Jy2K ڊV"z ٲ)ZAysCM3D1T3gm"`j6nn/YMW6. *47}'7V0Mtm:taF#5n55T01pu;KLdʹq7 ۮ&01M;f`?^T>仕Zk掄[q7W@zS<ҹf<K9p:5W!Ys⽔'/g6bpZ[1=wd_ĂؚtML'U{WbI`&zW^r<ᑦo<9#_xiA>AS͏.C8T[z|wnwY7zrNJ1UI\4_5C0P|L!/Q_plP**K9W}AYR/j}v9WӍ5Ͽ9c&39>U@wh_J,:a0~'X_6nkZ<ܯI׿^"Oe?zF7O#9i=!P01 :Xf3qMWJDQTXVN'# _*V;v"e ' )>o bxIIyET\7ZZMR¹C_O Lr[8t3l AAHz2DԈ&:&Wܜ E]˿vb'ŪrubggqrgJoK,Rr~7oG 5~̴#;Vwt89kRq/oAwrSw w3d<z rN2뉸ݟ>Ȅ*OˋY=}Kkt”ML_k{c ''=䐡ϋ =Oܨx(bSGQ[\Ϧibչ>|p8xn_ssi6o5% &/ϴ;LDع ʊFE\yx {^@׿x>>ȧK]ʕ Ks<8_L>4?>X8'nnLu_6_;2ե$(g>ýG[J!RY{:4е.>uN}|?a.N+1{|.'ymͺZv2,UW="JjH\CvorOYld#nZWYx8pDXڵ]׸¼PW) s}n m&s}bo>'qװ/@j4IF"a]LC_cxy_<ls-(E.. y;9;_ZJBk5돗ޜp[o1){d2qf7{: '\ʠf2&?qgL};<Ÿ '~z|#zhzaQ:}FP\z{^8[YJzS \Shgba>_f4iq5xh=;d*}׍>{ǜ䮂[^:OlyD_|Jëzt=azydu^VCI={ƌsJ:9>};xZ}|/hvm둡*}r]M;7K]-**˨g$5;p&wp|GfkVƴ~ʝ IDATSof~Irj?jLf7K+1N3O\9D=Ғ6.zt hڵ$]5LݪڹFsyR7Jw}zb-6EK_OsY+mshWOBN,޵343ǭ6W[b彟׋J }_K_G{os2@ջrj*}£a=va"I;w֚5k?pVtt:wlv(Qc.~o.k)*02_gL%$I*͕RWNDC7@ Pt+4Xoߢ/jskp]geҤI5\cvP'7wjδ@f3d3f)mp8t:O!t:95|n7uT|6*W\xN-YJrW=1: 'ǡ^?}iYt!h }1$gvlŦ4!$4!$4!^MgϞb|ÇWnn***TTTCO(""Bf>~nnVumܸQyyyJII1;hWTThȐ!<$h͛70'xu>Pxo>(#}PCpR^M<&L8Ñɑa0;Hx I=Ozz!?}3*[.B͢:z3V}ߨ_RBC"4vrrrmvO5|_r+ xh 7sW[[)Z󶪢t>g&QkDߵKʹ׽')^wޭ?ЗXt 5 ^ S|mwq5z~JnV97h[҅J=pJKWq<ڹ|JޤGD'`ޥ~DJa]  Agj`h+$ZU{yZwq- hz*RwNR3˱y0M'Z]MFքY豳su{]4KUXw;tl\O h6)@ Z:؎S8S]JƵ[H] +wj=hrU<ѷgEǛT)Wo.\k;oyYKJFwz⻕{2*+UYWGsnQ+Gvv즤fN9? E=oM~ k ?M__])Vɵs6W{Tѓzq^ SXX;hV1-*nnxMWݗkۏjo˭TsɏQ}w8#>$==#5999>oviviv_o$0.y6io999֌xhvivˤIk+[r c͛7O=z0; ͞=[cƌ9mN YtL7ydM00F@av($/V>}Nͩ L>]Ç?vSNn-_ܛ|rEDDONNV^^͛"o~*&&FJNN6; ^CS|Xhz:tS^^a~=>fG&D&D&9xU !rrrmvOЄd:cr/x7wفqRtL;EhMWrFE7S6jAGݭf y꾥d^,mZnYwM;sXuثُߨ7G4Kأ՛+80 Un%@Iudm"Yu㹽N2F螏唤گ5Mzidj6MC&W??Vm>8R*zz7 ?>.z[RF 뒤6)}tդEʚrFtOQ]49*4ݨU4|ңC,!VW{WԔ~z;:y5&RwI(LuCkрs=~C"^/rCu6Jg ڸuq=M$޸KVlԆWo~(z׺WtC2Oo7I O0ʾ.{+="|jna!Wܥ)O#[b4L|^MUofQѵkMOB[roxM?ZC\ k聠tsT{k%k~‹#>0}co-Ԧvͻ|uKY%YhX8Zlj>p2*v@'=Eco>_%kt_wVo#Qs e%8U_?J1VIr+/u[ ;]>^-U];Hqvº ~h((i%hˎ#۔t4vZ0ux&Hr8]3.Sd TeWol}˩/+m[:᪛;['I!u?_dԋ뵋G*"INme=5y֗I6(F-(`knHR(!>LfbccQQa8p~[s%4O Xuv9 $]v!sSKӻO[澬W齻TunzQ֞*.uxcxTq*'ẐI.ҿz^ƽ{ӽ􎒭b<cקF(*ZY{P,rAmrwv6S\l#w`6Q~W.ΓgXs5o?AzثōYY/ V`UFToҹ J MPC*\h~6pDM9N& 4})䜻uWkU[Q2kkuj)jyƗZ~*ϐĪZZQQI z?(;Zw[^[Rd8Ao-SK'T֭J=.yNr FV-~ud y{ҺRժJتGySm]Q#dStKN'>ИY&Md\s5fٳgk̘1ya+K.U~)3苏v۵C*IXҸ^:4Pe}PZ زBn:wPc:ɓ5a„3v@c#0-^X}9630}t >M:h\.9>6M.KT/ѷ(UϙՋw&(5/>ov9\.կveS/U۱J~)׈wi7$v>F[J̵QiY#Kʭ\.iQ˪;ť͛Ku'RhHcmUZj=XJjTmJ)(\ܻyg IFm6nUxd(8>0CP>C:;}VıWꐯ-0;>9 GCήH{j:)s$"4I=?^-arxG1>;(=qޝQl)uz[UG5}:?ӆ;h{>:Z+!׫hHE{NKro&Oj(߃r͛GA&T$ hhw1x<Ҏ\ uVzO?N4]4N>{S-ߴG啕:!B^^99Ј+##02qvwa4국*S%?_oPcjjzi'XGX›+N葮f/`[6Y\g RdAI)))A&%ʗ0)~tofitsGŮZ6@ }Mdj?Q1-\{6N֘k oи\DC{mS?8GwL&GROO(xf5iL:ƽ3QcaPo^oi^ KVHhZ} |MS$#L-oi~b|~1w ,ua[T񕡭:\"}Uv۫cWzqiY%&ˀlu<ڶR~XmRX.,`,h>%Dm*WNM5~T,;ys0%QUL_-,SK4,>D6ÐQ_IuZ?VOm`)yU`Ww6oWZV Z][yĆ5c" R$by>pr0; |QBMzj=^͈V@7jT WAt(B%Qf-jƴ-*u*.l 앨 `}G:\S'iri9lr9]2C~]*wkOew7@!5p\-BC:y謬?gfff`@ӑ#0`>ֵֿ]]՞n咫^a?|XaȰ%s6jf))P;Kt3fff\DIكv[3bb9'o T\U*רNa'a C=nBLb>ɕ,9Pŝ`-ܶJWmS~Z!FۖQuVr'iJQUzrh$wFDEFQ=;jgR}ު'8Ni*5XQ]۴]]dZ9W=~Sv=w=ɤc$/rMwc+>^Ŧ@lj{+vK/VSG҇\! @Cb.jgG&QRt-Wh hx9996; |QpzZ^g RhH_)QeR}S~DEuٿ諽}]ڸP HnX IDATEzzJ=}XG?{>&@U(4VElH̎YHpt,2;QuDE0,ˡj۸5:9PQQQCӤ(""Bɧ_aajkkU^^@5- R -_G=Y5k`SNssseX4hРYb򔒒rJb(###kZUXX8CiXZq'IZ)uEgUtʉ~EEI/гgO}w}MM:w܀5MZfa4}DxTVNc=8#WV֞.&Tr}w~oD`Ԕf<+[ZzzX]7h\ș8L&'Ne39Zc(۞՚]lLf~XؙHڣYwU,]￿,*ܢ]4EM 7 hVu R5Oy.|u=4U!hW׽zUQ"T{ܿks>Q F6NHwiuu*#yGw IS64M] ŭŲ7o.s['YTk"kX=X,׎ |2-[J붔c0<ڻ} w~X-a^UlI+V2O^49996; Lfu~?4bp-Z-IU|* {'mɟ^ړvI]~P0Yܿ|\{JL1A*Sm-UuuKGh|HKś6J{fW$+oJIڪduUZm]ZuV]&IL"LUtU:{L5}W(!#cRGC=rDyL,d m*p,@"TUTÐ S![-k#̝|; EVIQ£"d1H%ϾppnΘSm5 :~܅;kW}֓2Ds{6qudwjJzfI =G5 7u 7gNOyE.qEm(\J$bԾ`ŶMSl"aH-ԥzi!850:ygjEk}!_SNت.~%l]gU1;H#?B@Gݶ$8uzYdڭOoK9ڃ.{{>D\=G@#PHO5f7Gѷ*9zz {,V9:գzssʉ~TT<Ͼg{|*V<NyP^tu ~itbvLw+a) a3F}`SN#""vZߣ~!ǣk*"" ֎;|<vܩ`C9sjgG&kJ܍G;9۴te﬒BePe:)Ѫ(0]?D4^触jÆ ZrJJJ2&#**JJMM=}Zh]vi˖-j蚎PEfr 71nYy9u0>JuA[#Wwkk#Em T-OUڄY$ըN-/Ѷrq_N5]ZEVFDEN!qiZ%+%6@u{=N7;KNɶPr*-^M%ʲkr[[Km[FV.um9tG|"|E{QKNsf k^}G S'סV}(uwJy6pR%u*ߵMEJUp,@9 4b$/lբk~PEdžJ*Қoiι|NLPP#-JZk~OAwfkH2m\QPuA6 tI}M#p_'~%KӃtyڵ'e-[-Jnf eggS$/[i SuzO T ViZTgblLE"K?[r<-)+r=4q|l~OF h7S6:@Β-Zsi>&DIus;{͙Zjd OTsi`Z4ldTfb>૜{qv k@L ?ѿߞ|0 /2 =ufJKUP[vJ &''Ga(##P"|3K_X潢@c}Y"6A-@cC"G|q^{eRU#0 C_TR)X-ѾW[3.RLC}`&}gڻkm)Sǐ$y*ow.oVOј@` }'c4U$պdsUq 0'ƤBf.U=' tְʤOvJJ I>`2}yhF+*%5Z}Z7&k|`}*)h>YUF ,[mvB"{gXͿVM-jΫ{^߱}ͤ9Qvva?E1>'Y?r^9-\]T0 =ϩն?ԛO7`dUKn՝epʿD)m|Rvo/.ӕ:klY̎Yl]0m/nVGЃ_:5wjJ 4;Pc$/qoR^`H,b_ܨwjh#`2}2 8ɵ(0bcҋ49992 CfshB|{VZI^SwIū{_"|C!?^kA)bBxLGzc[0;'D}`&ЄУmZ:wwVI 2`2[KeWhVR#Kh:=X}¸CUHSMӦ?*QKŊl?ث];jMwKrRxh1w6oWZV Z][yvpuwCd6I U ~ƨ)S'\-AtB'J=؃hKNa!iHWh xYYYR>0>ov9\.5nCN=eZ?贰co=`>~[J ԯQiY#{*;k:KJAg8VW(;;0"ViݪmxTcr[*CR-+eUڸ+e=4D9\N9N<3~ǡV}(uwJy6pR%u*ߵMEZH%9zoÁ}}.͌.!H(0A=ϻB=l+kX. CQuѷ4!$xUHhBHhBHhBQu}}L$4!$4!$4!T˨D>M>^F}`&}^<sxMJJ!~}z23ѣ@BQuD&D&D&xU !˨D@B@B@B+##0@ӦiSDF]10J]fDž ++U_}gp:x)"plmnb*8#*w|J^^!~=o#pLFM= o RdCe%2L 0G\r9lr9]2$Y/&Al2Chp7;&/.38_Ԑ86]KzMn[1 &C%8RrV_Ҳ:G3886k %ҺUTf*TRKّ88Zy郷* SJ 6;.'B+82ЄУWpx[gSzkȀ4E̎ A]*}Jm)(y =KIaG_Tj۪4{ 7OUAՑ7T(\-yjljcwSqwp`'<.3,%2V:gFvd9$IZ4?Kke8"Ԧ |E+"|;S+3tnfGsB$~1'}t::Wi/4oU2skdͼD7xWzz5?{s52V}.Pv.J3SöCSgZF^}b5e5\}UB:RRNY:v 0wT4Y$RѺzL,n5V="c<h7S 2TutUzC{ҺSuF()ެTuK ](aw6=|;KH VaY!ONTHex:T]""[$uykV?{EqْBB !$z H*HQQ_E,Ŋ"OD"*H/J!B e?=D ~?s̝!3;wWRWn߮ߦ)jӴ8W{MeURd S]6%HfJ7ꪛJ]P2tZ%5$ L>IIJ , OVBRnNPfR Y)J<࠳_iR`p2qشB˷TPs]PjQكMڷ'Mkd*ݡsKv&ENUr8t.*+$3#uY:~I.M*.|rlrXdqRWƪl #{8 C#+*G&y 5]gU_i"vTNqiqz0PppL *'ԙaLΤ( >x`NPS*edORbJJaԪ*~uZ=9@%2;&H2, ^m?vJY'+F 2 2t:4>j?&(ir(wCYU(W{M2R*6,!$kzjSKc8$=(R}pLXUdO-dTI%hY<9*hrYߺUKTRE?d?,׎yD_ԪJ19l6lv9VaUjo[2 9Sk?P9LfeZ7Ehn[{V.Z(W{M2beH22kLPf(nzJ~?3 Hbi.}-$On(-nK؏zE{M}k?*o'hV)䯲u۫}/HWC::2o1s1l^2+/rGǩSu(.@YFyܠmSt4}uکb5պ]M\5I%*v'l叚aH&_h|u@ac?~ѯ_?W(.\=z\պ^^^rp͚5j޼y~T @!j*EGGdf͚;z'O wBp#}A7Bp#}A7Bp#}CiG[F IDATb2[}]T ;k[Iϛ>'k-gUvAsZZ˾N/*f6/m0Z^0En̢Kt:#CHցů*RZaG>ГuN.Ո=5)3VXT2ڽe2%7ZJTjv~F?8S褐/7*jM2r IJӖUUnS2G=?꩛j+"W5,-XA @/YmFj'izԪ^UUk¸s9CJx{&^%J VN퉷i35TaBe]þS׏]MkbXF;4epGPW_9ɓ{(Ӌzo-k.7'Kmn!_+)f>}BZ5>~}fUlj=PVjoiFō{ƨˌڵe~_=ޱCM;k|O^Yte5iǢj|-]{ܢ;&~Cެ0޽_==Mq9d+㾍D-Xd3 z==\%˦om4i^ڵN|CN[t];BMTRE F +pcY@??4NA=^]YtóKu𷡊d-vmkt%LJWn$$T8#':z6V;K2+}ڙi~Mュurjd2TjU$*zʶ&&RE5",:q4!W>ԭ_i5$JyWke^mj`5 2I 5}j &KvT[Ҍ(xYTS'ݡouR컠3ƫiɾtY-!{u\x6cfrp:$#vK(ę8uJ! -yOŻDI'RS竳X-v2)x?~oU䧰sy(!$%;N)d>UOݪMit2ޙ}:J*.%p _!*wz&|FiYh-\VJ%vwб!7JN<'u*^%B1TQpXtr ]7JH<D(ZRamڿv9'隖AxkM=:a9>[m3Q'*WRցlnRS/8 j훫R$7{f:sκ~|^ I7Uk7E}+{;Kʆ]d/=V}?E2%94{rlZQ Zښzf^}CF~X}h |6žPOKj>t4$Y"i{UUl~ WKo9'~V=kz}js+ޯ1/Vd~/ cl{5n{]Z]jkwaVVIzcCEUmgoܬRȹ]"*G;UpwC+ߠrjF7/._`tp9~@pBfu5k֨yQ5ȪU:WΚ5k:vM<} A7Bp#}A7Bp#}A7Bp#}A7B#p~K4$~dU)Q\eصJ6.}͓4km (:FERPA=#c -5R5?ҵk˺Qdd'=s$~MWP`G~sL2]X%\PҟWpQv43֞Y{Ұ z뽕vP ޹Oͫ/XUZ Ԥ)8ELߧRd>P#fJk˘%Oiz껣2׬Tb>A? qDW5: hif!#;=|pN[,#XYV(f-dT7oM|̙?uW~-&MzW+;ո{([:Io5[Ӿъ" P2.vjbk)ըpYEԌP: ZI^j6pB_'/yW"}<}?ҡ*75{_u,W1.%G>T:[c\R_֯WbƊ:uhJ0k]MUJe1Mޞ!)׮|:?T*JU+G}k]36ja]19?~P@9ut^*kd)5W~[䝖+}#IG<-^$g=dWCG=QIIFZ|t~&UzJ.|}*ҳjWLoBQk'kTdm1d(5>^sЃu2Ci2Ss(&?t]|,`_c2-g7IC>H/߮Cqв*97m߻UnݥgNGn]qL$j4=1_2F7GZX#TAܓq}(PRN?|e(-|u)˯s cz롿zqIҵ⅛tZ{m*_~i:yCi+sڭoi &*,R }veQzjUʖ pc'~P5M'szU#]i~*~Az5Ke|'EVj}uýdGXi}-mi%뫔z`]3T;H]qjDX.YӤ&?w)voV|LE@$9n{AOg=˟9YTry%=vnr<Ö,-f\5QN?tnӖ%%HUJI~~>rGeVr٥8$㝭ZP֡9m:wo"o9u`z:idit痕wϪWRRRaa.*hg?)jLwv1 unWֺMqqD??vF8QTgP0$ɩ=ß)> ~hJXf][=K;ֺ]Yß+ә%5oMeRZ1}̧'4EƩ}v>=\Y))JIMSì =QEo%eӲ1_hctgks9o tM̔=q>UFe̒{s9UJ۴X $B{}[,=A*YykT\S'-ւ>zuvl[+* @ 7VܫZʫ\z-A};_Q|`LP}LrJ#ޢUmQ[ewzUhR=#>^}W]VVm__>@~5L;PAΌ; TY2cfmsHrh6 -Z}PM8RcG7RhFjز,+=/-VT+fTX} zY*ݮ+ġeصL+m&(LǏ7zE… գGZK6-u֬Y͛GPַv~Tэz\:Q'}~\]uÛ ^s^;VZ\׹p֬YԱc]on)T=G<%}>J s&C%ںqȗ'덹5*ZsnZ յo~>q\AAWWѣ!F>nn:y{{oh|=weee чGc>wCЇGc>w}x4p7ã1tG0 _j\=t?::ڥ~}V@~b><@~G 7h@7n,m(֯_Fȶ h9te˖ڷo_m@ѵw^nݺ@}x裏iӦ\6lX`P 9s9R P4_^ӧOW2e4p@U\@C>@>jѢE b>@>2 #_~Zb3'Nk$*9ٖ&$$HOTU 4P˖-裏Zj>P@̉…'6l}@ڸqۧMjرׯ_쇠5k]]H ԠAU\Y})d|@p}aÆ=z\ 'p5mT?sluVlY9r@Mp#}A7Bp#}A7BX]]Ͷm۔xWWEhhBBBIah m۶nu֮ 6(&&/zn}KU?R5l0 7ɺQjuwxM_ݯ{6 _/S>tKjC|B[ =@Kwf/05- m8@_$X#:qMsq\7GŊ+|u~km>m\**=_2fܣuʔ ۴jNًǚVݗVBqVAܒm`uyvsXys\#W/gGS,yVђtS *RRejvԓv)Q(A("~'K?!)e[zeKO}0sX߶q~R-y͜9~p S)5sůR΍fMRZJv/yOM~^oߛ%$@oIM~Cmμڷ$Ô9ů?RBR} }K^3n-N9o瞉k]=3]Sc5BhB훂j7(E|~hu;OXz- o? 7!cNQ(A(|iwj۫#}jsj㈽L9g,7OSVdIZ4jU $Mvo-s(QtJ4V^.\+^1Z)e,~JQӏ'ALիe*( }9Y 2ӝc鯯 1gNɧP B~ֽͫP _^wܾ]eYװoCnTUe+aTMyU)rUipkzaֶB= L=OFA0S(kW,g:.͆ڿhiGJVαC<&+j}elͪrJ2NQ TV?Fi]aoP*]!J_]$îMJΏ GB[gS šߤ&q:p Rzf/KDCJWL[WQMuGZ0eR$v#ƨgƲ$MAo|D㞸A>(~{LmOoݮޒtmjU_] Rbi#jҲ$+X8-ji"{䂑^Yl8#1_Ɨo) =&m;k?QiG c[~>Ssd~hivϚ8[S食?nב7?;iƃ]>DФ9P_?xQS9O6ҵby}.s~SW)UNM}BԘ:~"'gXMz}ʾL{wWg k`R`;u{-),YR4gΜʅg2b?ŏOߛ8r˾y~2T]ԧM{̟o==b_Q%jk/hmc5F mӪײ&h{1ﭧ@Y%"u**~{i]_P̣Z8--DY:n|>#{Q?'*qSVVƢn}`Tp&;n']}㇨%w#Tz\}5oT UUYMî0Q㸾#\%UP}qoY CrMա~eVhvcg{r*mT4ӡ'pВ߶C):$(d0=wT I|PVgٮF,R@ݎjQ:s*xK>U:]c:pĞñHN]ֺO^{֫tyfcZ^z^oɪF}Tޕc@y/M2OZe j{ԒiTžeUG+}KJxH֭u.-y =WzAʩ_RPުa-wuԎ)3u(;[kڪԐe5*~*QQ'PI$*|O߼YsLOKr{93{d)yM)P%ӴxLIJrHI%CtjQe,d?T*[竖 +ߺPK# cT׹&IN{༠(Ov*x̒Ǐd`-*R\o;.8sg. qqU[nTdddYJR  Ϲ[=)=rV|K%Md׺7:k;4xx-Y3c%Wſz]?9sf{A?Hwo|K 𮑶 +sq=%P9OJOy-{P}ssN1E[ܣ;"˫|3%~jc~d OЄ1lثZ|qf;Y)B#몲MY6C4xxi$AkDbrJ 把 E=W;MԺCMgڐ䔑_3&cNI([OE,XmHS?stMsz:Zzz4вL:a%9_t'²k_jIy@vTϻ:T>zG gHr*-om=OxH8-VT߻Tٶ]6C'yQm&h?EMSq[iidRz)p?T+ybW'ӝtb헚&\&q\@77t޷տ q,NisStnM~:j>>be=𡞈V@5z}-=.B˩RJydI Qlz]6잞Jtf3}5] iC#RŽmv̪G9_wת=jsf&Z1*W.zI_UNqH%MegPUM?3˳!QF-ձ^&5\\uK*VmO>&c}Ft2˫Nᚳv0DC(P <6GSV5Pnn[?jʂyz?u˘jCǨz~ F8urHuTw`|0Tͼ:Dڭ/;Og޵m}Af/5j6U}m^h% W3?ׯ .T=j|IHH$Mf͚oYNg|l֯zwF[]m;%vUۜ5k:vMn}<hWi}VHHNf&nl6t*$$U)hZ!!!ھ}j֬)o"tjlMah ڵkk֭ڴiN:@ QHHj׮ Mah 5n@b}A7Bp#}Eܚ5k\]9v~ h޼Vrپ!F>n!F>n:p5r*pք r\>hР\"(T@#0TV-WW䀡>n}ׁY%ߩ>|*\У!<Ѷm\] p}A7B0>fpG7By¬n}A7B0>fpG7By¬n}A7BPٳG{qu5"Y*nF Ou!Ffy¬nF Ou!Ffy¬nF Ou!FI?5k[\_FuZW.[~jV.[}2exbVjU;L.2 A^Amr2yWdsjFvKv==BdJܦWh[ ouqLU^y*Vja%+f)3˪%l6<A@IJv*lpZExS ըЦ}YB+*(ʘunjxYdeH:?($uT=7mY,{v.#\F[ύ7n Vy_asgEyJ/҉ e٬ŋvEYr4o? LłdIVbفJLR`pEAH9rS2˯l=5+66QNWT}ygWJڱҝNe٬SeU ZXH*8ظtw8S4>@Zj x|t;U[TӾJXjnj$YJ>zHUGFʺ -[&eWa5کu5_WO5^lW-sA>gp#}A73 O}%2F Obbbm6WW\A7BμqWRfp#_nw=N0STl(ajtӗ }A5"E1)O~iūvYOg4@Ž<*F%˼n~7: A P>ੌ4};MSRP@Ζ{4=p-Tzj+|J2)Cf?h'ckݩPZޯzQ78Ҵ틻JquZ̺@F<}(UOܠy_>&T\GRӴ^}H"qLL]T;YNT C,zHU/+LѢ(T+&&ggpY±d2)&&ԇ,g9Y뵼(| AX&hVs-X3VC:S0o0Zr,g9Yr免iF~\]HXpzqU>kOBB$iʔ5kԼykۑ~XU:mM悏'LA]ȓZj*ZVRtttxyyfYfRǎuɓ'>ౌD:8G,Z,4ox'<*VCOeS?VCﯞ˝8iVdum>.kB>L *UFeK^ ˫B\T/ A8즆C^A$*ݖ/ut|| (c>i2g_hOMM97 ^7e LUN\~Hk-׶Tɿj尢S6j_u(1S w6S83x4n>l{QLrdiV}n)'SܟxJ*" *vlVE ".C:cOz_FVY?C]%˷+iݺu~nܸDv7 Z g/**U`>੼oԽ-S6h3Oo5 V%Ygg(1)K}x+-~!%,Z}cd=4V?L&R}URvl>tSG6+TYU)%)C/)2̥UjbT͐-qʖm۶W}SeRo:w3&T>-^i_Y,L5[Wb,%=ê#CU6X9߮P%HvP2x*Y^7[*ZrO5^@EuW*vw(-YSy5W[Uڟ%Ӧ= *w?|BUBUX%~}-**Jjru50tT>Xm6]W>"ʙkVݸ5SYEh̙ 1112 ^} )>;4sv>;Ҏj_J}("ʻFڨQ-tQ2^&pBާ]?jeZУ<æĝk.. (WW}SefL쭐P՚@D>੼;d|&UP`oV @aǬn}8h+|$id)fݨ̺9} (TRiݚɇZvI7d{UqJ*!zj"(Xeueu]*Vʢ H74&Hh۴!% L2~+gΙayʱɾ=U+w} iC<hiOj>]`{ۨgYb}r{~zz%ӷZk̳jP`#`W[zw!1ZCtBQJ^jD}ж^]wΪ5 +P> jR?k͟O6.^Y.֮4H-<<'޹{^*I=n]/կa*潛, `=<A$kF]D-Ia鶇[ ?o՗4o@L{٩.@}} ج;@ѦB$@t`62YJ5}vhţuٵӛէ]*0 @~4Gm0{.N=pc]4FzLzj}&sz?L_!j:;@Fj3_ܳZxtFVg%PxI̓n;fkp<I2' c}6>I Ή2nkvY77PLE&O9@}LV;w FC!jdP-zb 7+9@DIF|Gn )׮Aة(\dzyϏ}UHOOaJLLv)8'WiBmwi_l+ qjìW!vuuf 8eJXiJwz zwk6kx9H.T fۑ ߶7F+vu2= ]k5LC^wj_}?{e;Y(+lB]+݂ej|`o_-A27@}6םG] =ՑeioUIߵGrBiTKMRnJB[rA4-n -SK$6Qrm VrJfEhK5Ag4;2`n]]j j1 TUx  S/ݨ^UԒPѣxR덨DŚVxz8=U̻m]N9֣. EN YPamIVƓz=zk@<͞7`5 1QT*vzKg@ ,N-[$(>O}JωuST]VٜNU.pdU5ZGjE>Vn#tjHvR6]͹j^C[S0KK 1I*W~]CߒBl\vvkJ><v}F‘>@a~&*!O?oVtyzZ:}:*-XQ_wsK;fbB*MV-dڽVksreZ= 5g;;q "xmsuWߖho![XhKW˛57Qߕ9NK ]kټbl*ۓ{ԱDMA_6Lv ~ִF4 IDATV~.PaWlL).JYd+0J@-!`*(9goқ? ,sDzjr="-xp<P3-?RW<?Vݏn UˆJ8!/еOkU  :O)URWV v QkkKḭn#V/Z6}# sEvY&X늎ZZq*?SGhӕ2@xɪv!.}FfL ׶st=ڷޏ6~rm׆M%s Gߦ+[+] UdM6u[m8~ZNB~"8Opg8cnQ˦ @KV4~Bうث/k5R(T^^Ip|@xrj5# mb.4Tp5n.@}êm}%Ƌ7i))n# UzCթYBBBD]%.`>PyϏu} Tdl/Վ rDU@ftFm^ ې$V;ViºO51>`9Qxl *\*Wu:U2 C. cޛW~ڭyD}`?;Bg>[-:偆ʽO^vv&n\H%~・oTFm*1"ik]K2,2١wPFFwN D fMY7K_~}{1a991jc}6>`lZT:|8  e/ߝ[3IDKnO?ҲroJOOWZZU  [[?LIп_Jz%wː]9_ԕIgh[i]'SA4b֯ݨ Вg߯3F/ةZ% vNsƕ]I10R&ITqkNc* j }qiw_Oa{iUGh\ IrRNyK fbq0 %&&zp})W6~^3?եA^(_B ^vvjs!a}6z!}P#JKKv }|Ab|NMYR/Vڮ)8Fg-ɩbFeL^!|>S`W +i }`_>Xv3J{C\!MTT~^C_XuN55ڣm9Եf覄ڒ8z?S:>W ͂d1Y&V1"ީhj(/P;T[nSA^NgRG@}5tau,r:2$7;kݒ N'򤦦s=NS@APsVlN6.Ym?W[N! =0t@f)T~ɡ/+48!]- fk]ٺ p@CCPs榊kS kUv|Zh6I^@3e%ںgi-ds:p8tP%$$(11e*0t)e@[}P,w`:v wg+GgM6W'}бf5y.јoZ@pjc~vSFwَe$y4+==]aЫ@E>!Cj$!!%GB54o@!}|a}6z!}P#@F!}|a}6z)v@E>:%>>%8tDo}|AB0GPѣ!IOOWZZU C>>U@>u=>Vn#C>>j$!!A.x뭷]Iq{=,[)+mW3Rޮ YEve-_>5_. oׅ*55%@u{}N6m U-d6+E7%Ж+222]8N6dh#A@1 TUxTW$ë9j)zԅbpʐd:!aqxʕ+]ZhK8.N6#=VlN6.Y 'NTeb>Zc WP%+}A@17U\mX2[;*@s6y2g1t@-e@[}P,w`:vޮ ]}?Fޮh G9u`b-HTO=5o'EX]z(ۭGE.E=K&~ީ.RZ)WP>kt։~jJs g)PW6*eڴHSzH]i#mľW-Z_w!EPs;!sW9ZVGwv5'DUFoZRN%Z?oU=8v)љ~Hp]s]֣1Ƨ*ٴXKrk)ʱURQ*>yWl/){e #OP6R]K~X% !+HP6?kUOPWV}mjV=]UKP~h#׃KCxUҺ"m2YթK[enUqkI 3*),;cڙWgtQS,bյM[w_))*hZsmP5\$ B#v6n$5 t( fI#t~BDF>PaAB"_a -<|[t"EFG򟚯s+(@aRIᡪWqvg*j Fڳ3_~Z4swT}ʵ{dxXۈY*`|khe~{% *ڷ U}g"^dpbOjpf]Siʲ+5zv×sj96l8rf if1JU\jNu6FW_y]k4oUUC;1ŪKCYjʽ iN.8ELڬrUteZ_.iTP3E?,/יC{) UVSNבOnC.M֣qhg WgCaS.h,j߹{vq>mĩ]|euWk}if} LL UP(wAB"FlT`߳Jާv Saa0("B[ R$C Qll߯d$pH3h1+FLA v%J [n^0_W6& iBSfp9u2^dRp8M3_mMEpujN o]U.ɱoC-TPCC $>?Ѿߴ8K6UhUXZUu13f\6DC/QXBofZulu)*v]V p>Dm\٫hyf SB('k.UNYBŊ=~hO¬6IxVlnnQٍ_ReV:n8OLiUf V.4M:Th`{be5$>j>R:Ը]/ ­]*ۈQљ2ԱO-I~0Y}[P/&Md7u¼y4jԨj{kO0]rP7p( }D/MT^OO(]B}5W TAtm&)@.t2Znަmۖ5y*dZ\>KN?-ׯ__]/<rg;L_ouE㪃]ڄ[Ze6oA7?+dj҆5sOo{.h^7Mҋ3 ᒭz,=iu_N=0wE}B9W5Lmޜu|l'yڶ)S%VjU8E\?@R$<\ RueEY;ܫI8mW!$YbGha2I똨vuhi}.,ɬ4.QW]C&I~q;ϝߝLf5pF$ssuhGgIĪcEyatcȵoB*5:EgIg$[]ze?/_4}Mzz$Sz4N| &ctlu\[@fQܝ?*&r>Dݟnkn?%nxG gs9ó)(H2-Va^V1G(2⏿"H_DcP[COkXXfƉ)l~qz~j2ӽ6)::cu@i]CP_;>v4V$]NP+HjqzT|$ɕWE /h^JbݖPBB<vwjOD_SIDi &5}j,M&CG'0TVRzjw0ʕ|F*XMBti:?o,>!N텛zCrkU-:SYow"GIJNC(q,M_],ɩ]߽X<얮R!|>xbF\3OxKS3)G+Yytl#$c"}YM{O1F>!C>>!C>>!CͯkX[5'ߐYhИՕ*4=\Ѹs$B;k#+}~Zpߧcr>[UHʴy,‚ոP=69TsV覺a62^-I?ƺbC+^]*QާۜUTguEr[5 Wk1ŝFGi:VECf:m4t7l-^c+ʬ~n>Wm#BZKrg ^%I}m|%7=v~rJ2 ?=#m+(ޔMz*KcR+˱'ujhZAsYmr%:oRb5릿kF!Z%Hdf߷D*-hHO_sr=s{hnV>Ҏ?}Q!..{gr ͳeV^UftLЧ^ K79$#oG3u+Z+DlWʳWjx uzfnI2+UvNO6ߥ|\^ʞύR0$JأʸvK@y|/@$ɹ^϶kԽ;3)KU!?u7F>֒z P*V.Ԫ/$9ksVulr٢N*ظA.Ѷ7ј+תꡳ|k& 9NAM+7uoiWHP/7>24 >nʎvtP{uj w)m"1 6qI:4J=]{/QS{/UYG:ƶU灷kC" \lI#eG_ǾH?R&*ڶMd_Z}Unޜ)#%*) PP`^ Oۮni0ōV?cqk^Խs?TQ.e{>h=/BUT{PZ >IߢϴpR:Ԧ@YE9ٗ;.Ї+h˒ wEG__r],.QUj]Fnփ7LFd.mиnSGk5[;ܞy;n"IxulC3'{&#@2T\TFnSUZv$JKej?r? g J˴Hzm\k=߫++~0ET[5c5um _5hjyƒL Le߆2ѱ/˯n{dZdk>L7P9A=Wq^şjt 7-\fS#uZD+ɇv$ɦ +w=1k5C11 1I25RH[ǮP+_fRPpJJ菶Q<}ot% IDATHFrLr)scBߪȪɦh8h~{‹$JEi8vߪgJ|XMw雥ڦi7֫Qw/ V㪧I7AXM^HwR pJIVIe!j)ڄjad^a-D&_GI* sU6 uX~hRBMns}RP"jݫWEVY)zj$k+wIQRy =P̊o+WfrKR\Wߞ|+y>!]{nBJU\\2{3׿VnRrSvn3Bޚ8ZY?_e]5oFjXY*]I7ݮ;%sueħXRzuCϲu_K&]sX%Tܨm½r:h+jMy:'|y6;x\ꧺ&Bo>TI2|cU[PsY?qfhcV A}e+iwZw46)ƚkuu|Z꣡$9?m.mBw+mv]F9fDĩh=eIQMC?8D~"E6mPzCa2FiTi)1^{h>JOt9a@tmPo,?GwNn}eOxW{+9FVś%lRU.s=VtЅOѓ}h4_4?e ^キ]7v"9}zl*p T'X]v)X}{Lͮ͋[$m4i1n8o ӨQ=INp}SRRԻwS><اic{4{bkZ֎ׇߺ6y]N+ўPV[IJe˔|}l6 9d̙2dI2e/LQin~5Wʵ:PL!_rguӳSmuk;oWqVboQ̭7kvF>>!C>>!C>>!C._l2o \rrKP0tB!}>!C>>f̙.Dj 99%  v pB8RSS "F{>-T1t4*z!}|AB!}|AB!VI2e'I#IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/managed_dock_window_tab_drag.png0000644000175100001770000020257114623331163024333 0ustar00runnerdockerPNG  IHDR9JsBIT|d IDATxwx !;HW:XMA,\) +("DA@&]z/iٝG0a){;3缳gsfƠǏ7 z_DDDDDDDC5}%Ȑ!W*&|&cfΝf:wɹQe={6C5NO7M]/""""""r3Əovq4gΜi4_DDDDDDD_DDDDDD:D_DDF?^ȡ21{ <'JEDf'i:V΁D2L7S^kbf˿)kLlvKAm"3&ZzH:'o7SiV{cInI ^>kЎ'nڍ6J"͖LGjX(_ TKSE  ' `ƌy_JOl޼euԡ}ݰSed͊=ZBisna7 ,.Vdy4MLp+GPw^9 _Gló]BJVTKSp NۛSr&FLLL$F~u={bӠX *7iC* """*D[n̜9ӣyI~n8uEuI=ԩS9vYGFFҫW//agr) Mi^֍̔DN6s$<:;+lG:r?}=>ԯ[ZP{L6lXÞ2%W<-ܺ~Y :2- 9nV,!|?#k~VU=;Y>נrY>ju &7boԈϦ])>2wy<%?.#e,_3!w05ᣏ>AzF'LMMM=+ӭ[7RSS/KJm6{fʔ)H=EDDɑN&~g;k<Х^o9ymfgӠqNfV|l{ ;4$$;nЅ^ڈ͎e${Ӏ_NodcGM:Lm g-9,O7ܒsַ4sVg _ANWDJC;w mG4[ÙFFv|"""E*/ ?/hʔ)áC(S xyy]R='''KN;w.ͦED&)[IߴWo}: NsGE׳H^MͻO'ӳp/] XT=-+W_cHNna7-`ZZr@`5L LRSX\tK^ }(H'%ՊNYc2NexuYiynS/""W|ccc/>{;===k:Ě5kѣ\K_bb"!!!4hЀ%KbXHLLEDD(AٜTUoH]/G 3ibfpk5+.:/i$ħb˷0e%W1/ҏ*2$e,xbsW(Ųg*X0dA&qvoHb{2ɩpg !MpguO `\4kBLLL[l˜?֭Y뺬RV-233IHHmEDD e]3kӈ(GvyiiSR[3 u#5CN4 :0O>V-ӆD&Ŋ]EqL6/ZMq11pp#66m~|'νzެGܟ!-%WR8"""7 3{gϞ֭Vf Ǐ7[h6Yp+V^X0LlY)$NCqOw\mdAiBW8H;CRflp78/kf 0}2Ldba?O\^nV GRmÑMB'!e%s2 _?)ƥ>!`e-ߕT/7 [ɉd>ŋv[ɉI9니U_}냝D2/3d$jLQ*Ѥ[`KxX38SyN1YHgAk/""rR/""rL =3$%"""5%"""7{*G.t}\/苈\G\"""G+W韛ȵBuDuDuDIT(n[.1kCJ~ &$<;(x.+f2NDDDDDDn $J|OWc9/6N)I_1طzO).(yJEDDDDDĉ.n~zq_<­u*PleZ]ϴ :=5Bhx:g-J?@^k[[1fG~M7Rvs^^i;%_o7\7nUQ4.$co{ͨ^klէjT9UkˣÖo~:5"(ـƯdݗCh[;ek%䞉0TRμX?{7VouY榞gݼ6 _ޠkR9LڎS>*{}REDDDDDu蛉s5hˌTB!~)!so"u.kR1᯿~妮YAf~~d*+op+Xp#L};vΣ_ivOx gy+a{<6wYoIsfww޸m7К|f 7f2G bOhn6~т O>7'w[gl`qt(vuDhm {^߉cUL{#.q]"2J 'oÁ7J^Q? D}PgͶf~Aq &t4_Vѷ9g^`O ml>TyB/-ڡU''EeϞ=PZR|yg"""""rùDHH`ӑ7߂֠PRT&./[X;g Ι{`&Ŵ?e~ٽ*6-[$-dgXI`oVE:n ?\]}ptClV淯Gu;|.u&34z=FbD x\G%W={X,l١\֯_޽{tv("""""7KϞao8أ4$D F|gcͨ4sG6^gG{o<|3cM:{λo)H@ Gz,;<#$ITLΙztnQ?;k«Sh68z0ƅ-V#[nF'Q)[lK?"EK/M||<~~~DDD`.Nd&irQ9BR\}g m܋x^zRKD0 =zZaXgEDDDD.1~xo߾_ϴuLYx\/^Lnݜ5c̙4o\I5byfT+iڴix=ˡ&}o"""".%"""""""ב~3>#3&H!шȵ(sCB1`ú^DDDDZD_=~JˆWWWܼhЅDR9 X'xY,X-Y뽔rI_S7C:!"""".%"EJEZ6~|6Fحda$93xCG$3e\FkN })M%NCYTT~{ô,qƬy'-ux7ەGDDDDyJE W juzoQo1/)S``(B(Y-m̓wOc"ZƞR37r͟..`1 \֌U(H`& zg6k4! >7cg5XČ Nϑ#GrʎquRmR֛){W}kΓ苈\%"NNZa>#IgŲN~.h3ُ#c9)2 Xq;S&KŵY>[1P_NJQsŏ;c?Y^ ?m*QJSOXҾA*֣ȥĚN|8 :6FŊh7vgA։L-75.:71qЭԭRʵ[pϨضIJְ3M-^e&8ŏ@4RN;aPƃ<$3Dv _XMeW<+ Kxz׭!.Q(f F<ȗcl[6+`ؗpдy~sS>^PׯX~.S/ ٱ8OpG| % ==Lo%$,g%84'9u8u:'su3r<9=ѐbyX*Щ_ JkV ]?#3s5kP;JxUI dMZv {e}(QtK`[ҨD(zN)7r-hV>#oXw5x.$w=ܖn`Cu"Yp?M~ (zw+N\Y5x$"/߱ĥ)TsJ@QN!MXJc>K,ǢsNo ̄ʘ<>~̙Ǧ2\n=I&Er3ά'&e-RJ%FM{OoJ|*Oq|Y|=sr"($۪DN%ۉMε$wu{A;Ss.ESt'QNO^.׮3%X8s2joDh݃CM#!;D/BCOVLe'""""\yĜ 4]#Ktsw y';ן.]clJb,9fǾD ?eij q/1M{r 2T`Xk;_dbğ9ib; ;zf< |Nj.O弓؉>?Ŋ]gmb˖-lٲ櫾%r`_N`rr۹'F b8s.1'H!tNWB5CMݣ)e;wfqrЎ+ggSl88p!Gڝm(i UX?s6u_-O k)WO Ycalˌf<:9&pUpIxڽ33>y/-t?:ƜzpEƌc]L}i`.,~iGϧ zNMpVroؗG~wg m_/MJP+=q~OݵStݳ;f3wW` :Oig~a8k?F5zD/ƽ?_ۺ6P1~xo߾ΎC䚰xbt0?#-Znʗ+kCL/3&)ĶZo,xޣ,}nV)ODduZ>0?N?_aטԉ*K{ȼ`kv|N IDAT0ov漼~uM*GY1}F/'xgU!ߚixs,-&OHБO:e_{7z7JDabxYJrl6ӆj?GWȿ2c,ܾ+R{0MޏGÿTdԊ]{1/}ˀf;oV՟lzf/Рr}>ൎ4 /7rWbw0g0/.Lq^s{X;/~ofZ=6gC֓Omf{(Sd|f>!.Lye 6Lxu?fÞ}ZoAqÂTC-Q$ߎ\GLKJw譄56b 8lgOkPk Z>zKgknn ]"04}uKodߩr`Z5O@/@itڤ$^bT% Jyb1|ռbNomou|AD6T:q#v %"JCwiFc`t*r%1S &,z=P|b9}zZJb>杒0| 9! !XHx2XbS*Wv#VD>S#U`ܬMڭXRqؿ\:xÁ+NxyzUA\+>p4s'H hDKq" h3 Dd,qhDHǷ߿LGIO'i7tS?{U3>w3)uo7d-F1kjs9şZ}^^O5=j|W[–m2gq3d%mm"4q:up7UdA ib$##waͽ)xߟOK[˄OVS닄[ifd<-~,{2{[>N|܌_oФ_>䋿jgL࿌tȶa'6]0f4Nnξ T >d$$=>Ղ]!;esVhVʩߔE8fyY 7^"L-zQ#? ܑ9>ق _39űNkƍ߿ 2n8[$5f_,CXXXN) s|id} a}|t_,'wԒW{.&.Uo_CiR!ިqv?'~ߓ8[:\}0L uyri6}3^oWҥQ;]Na_V$tkt/|ܶ2UOdo 8}4='Be>vo`ٚ}sHgCH:uڵ+ Jc?, 4y‹ϗ=G Ma'[x1;wutMݟ>e!"""""We˖_f͢M6޴iӮ]EDDDDDD䴆 2{"[VdI;V$u_]ΎBDDDDDDڠ}}}}}}H&/.DDDDDDDfemڴaϞ=_U *00___ʗ/PDDDDDD ٳBZ Z߿{P mFBBqqqEDP@ժUUC}\O~PZlYUe˜8mΦYfEDPmذ۷+G}\5/EEnL ԩSaurIgqUP_/"#g:/"R0Ma MSϡ^DWUM""?""K\t("0UG}\O5"""""""\W( ζA)?Jhdvd\D"-`PLA0L:ôsp-Yb2~6α_gy;-̜ [kkB"-PQQ˥ԡ4)>TO"G>6 ^Rr܋_F- |Zo&T M)=Ⱦϻ&OL/ԥd_Cg뗠 kO68Sx ߡ]P^4#?U{`2}Vwy -^ .7wGkVәI5\榎-p//AŠ*! ~oï ??#^n;YyO5nmSoárKqw,Ky;Q jԣ|-ѕX:}1)Q[U.)- ၹIƱ1,o-O<|X6e?LgI8Y wvXͫkƝZ yP]4 08{_ٷe+GNF\+T܁}T" ZՑZZ]feۦ5,xڕrͤ~ ^eS-yuJl;xjn2Lu{k\Tl8e;GEEpK\\?yKޣ *£ѣh iҳ^yJn1ot-Ԧ 2i/_ ̢k8b봖Q:sdj 6l ՚ݿ{1#Xja nO81%+yߛ$cGUzgJ.m3WdȢ=xMr ǿyoA܏ S_Aɑٷs<k{ %#d*?;AvT Bncr; /1c5??Jx|ǧJb&FxCZ AZR=8y.Qt{=L Fċ#b;buVL?<&͞?q=g5g%RDLG՚#dYOv)&wCBP]-a!8,q2X bGv"r=;؛;wY=y ZA\ a/kSPM$T ?hང #>ϹY1m^/wo@]_r }ib![mYH >C wUcu,X8L{+S?` &Вiډ>Xy%T6e@_nW<}mNJj-ox}y3]yk8_Z&[,c397 &6Y(Q ~-rc"|!rbJm!}W*Ϥ7pQ;~}K`ggO2 zshʓfgM4O[1= zG>H)^ez]t<>wc|[s\y殞X)/A&saIM]Ps0;@߼wQ3/5aPnof7McmRƉqak:9QT8UX"+V,?k`0+8N^2E_zhdnd;o=_ ˜W?$ko?}EsP瞷w8>@oS{Tg%7qϳQ,>mN}#:3~dB)ЫgNОϛgf4Ɯab$`pr 2M+@ '8ȍ3֛]}ߧ/ZߣF+#q7žxGKjA\Lv MO~V?i7/u/~OvͳG/\~Ǐ7[(-^ݻq L-/a6%uLt(\ܸ{ڴi0Xx1;wut$$$0}z]qtZNՂy *oaO/]o+ `;b]]f͚uQ9ѴiӮ%=MW|>]0D7^~@y'=XDDDD:P$yk\I;X-[&M\SDD.aZ-[:;S_/"W}j?>M\Q6g\׋)BǓ6kgS_/"W)/[efu"ߖ)}___֯_Oz Z____g!NQ_;z\O#/_{l2 jAK8/0Y0p8:K}\- X,8 QGFFv"raDDD8;Bc&١\׋բnp8رc苈(Ν;IJJrv8""UA}\-,YR(@ ˡD_Dj:gtaB뾈8}}}}/r? |֭W8k zCuI&^mVZ FDDDD䚳cBqժBR\ESۖJVQ6t󃎢lEDDDDDD) 7ѷogT "LUn$Jթ3?ݙR~ UH!)lcphl^mu{uaSxw`i=v'1~!ԽܯX8"""""""WLk[p@?5猠MP>aC͸eէ%u*l"66Ҙ:O #oŌmZFҴu] \K6㖊'9Z ;R㲟Jkt3mzQc?nvN&!ndߩr`Z5O@/@Hg?.苈S]9⢉ $A\t,sRc Tۗ/crԩ4o?Kaĸ:9|}KsC]ɍ~eNX7r6ks̾[JlPWe wڇ!3Gy77! v;C.{+ߚCzG̽zn:M$"UApK;+7򮔣*R˙0a):>g IDAT9{0>ɗue]0ߖHT} tID_: *[!=9 &f˒vu""""rj$#RDDDDDDf%x_L@ZHmb|""""UfVu:)OJEj+̟_c, (8*,DDDDD_}ڪd9S?f0eWDDDDDS/R[udӹj9&w<]T7/"""R(L)iu9ssw"Rdl_ٗgI 쳹:..RRRHNNvu""""RJۗ$rssIOO/]K-B@@W'~|I%KWq`T2׬vu"""""r5OJJl6ӦMܭr[n%99Y.o|?5+yio`w,:A*D?77޽{.Ef޼yqdajB+.N[>y)*\kT `m@CEL_ȇNWb4TN)hbt)/R[p]=K0+`p`_@LʢD_2n>?KY  iթq/eaBft6˓؛9r~ -NDDDDD \z&3GlIcŅs1TRSSw"""""rJH a{?lm'_q;|cQŇ'R[2F#^~l#F``Z-J[.**ݩy?|{ Sv:]͙?S̼fmsW0ONXODʉߠP8:x\T+/"""RTxo_33Wqߕ4p=t_>;.~ ύ|0>La[BHh-β> t05Kl6ͥn0c+FVDDDDD /gKI$>wM#Sؐhz$Yַ ~}QZ2IܔE{EH Sc?9O#tu"""""R!*|{\SlDBlՈLFu4޻7w?3YYi,tyc'zϡm͑5'w}7cMOH O/"gn؝A絡q?hSDDDDD*I'O;>e/""""RmkMrr2#==|$g'6M\'zbƿ@Zj[955ݩT= ;fN먨rޣ}ڪdy -g汴|)UzVn9;n3&&0iHuD_qgSMj:&zq^XL^icWb!9 9%)sn*񉈈TW;6J^ֆWH#gO3yz;iк?:4,^%vd=vUjH_c_U  iFP}{Mu -;ney"4K*Oyf>2U8@ʚxDc_ U=}Rٔx9lm^ FU *ɝƽhRw` M :<Zȩ)cX[x{. {l)>$G m7WA [|:Өqgk0JѧUctqlݜ6 [1I;Ǯ4;4u{\\K׆ؘ&>RthCgap&ĵ׭"89?rG<2֥7:4wǥ)c _uw~9ɫ♙Ν;vSp"""""RUó?&V \ϬLLf,xjWӣ'-r#p1\z<?Njwix1nL 3&b y)q[@rCnXbt]OZMк/]"f =L?ս3yѠ(%/J٧uo^G|sd$cM<inT#/"""RT}>F l87ls}b3~f ;1ҭwNmLW9,D_t)1'lǷ6;;l|LVB|3lw);v,R$aX8o1q~l#o'6<9+xb,Ճ5798E5-z*4_d^>}οw2=BxLkM|׻Xv8YnX43[0=wF|=|CF!?>5=Z >mH5`LDNQԏm-' a|ZA\tW؋ `d?Xqs)+aFn҄Go`ߏYx"""""ƍg :qT saeZwէXb&gcn?Bӓ4&}-9Ggff嗽x%Kеk#&ge:u*C quR4t_DDDjK/{h":utuܰ2MF߾}ϸɓ+pbˌTy7?̝~To/B'7_瑸{x9߫҂jJUEDDD%"qōo͕,>z0e5|߶?Jc[颕&oVHD_ٳx >vu'an؝A絡qV 55?T z*^D2Xϛ@>$[tkEmH/vy(**JIH%P/R,]BM~Ǡ‰-˃(ʍ {&];pAfVf,"""""R^4t_2H4ȳᇯ*m?L-`+(9^kHD_29رכN]BjDs^~='&0#N,iùճ~_~_~ad /"ų[/@DDDDDM,}:Ec rgc4X*޸Bn'K%staiw 6Ɯ/9%/pL}o Lf,z,U[zsO0y| <06ٻ*ߙʢ聆#[vcS@+ؑNWy DߐT=كWwfzwz~ߎ!ym&I8хk}bsdzp/KGbh ͪR$ &*:b䲵 #. dWtoM`{25?nu2(aiHTC=iq5l8]bd|vZvh1y3^`n\y~CÚXHfyGڹ8_#|~#DϨ)[(39kobli$'e *ݶ٫s~mѫgVHBrrR*x8<*`J}t W4>xk77o>Ma4%-I;⯻5/U/w;9y'_j䖐nŧ2EܰfJߋ+H^ n\4 55ݩ 2swsw.|o'2oEssg!o̡'ZPx>xufdGxk6a|5C[3-nsmG!MOrU;rQoy.}WNO\FP]+MYK%|( 3xꏟ[j̳0QyENIVn@SC OHVyﺮaɊDf`K㶝h\ːgo5I^^xyg}DtjE@wvݯ7lp:_Ēc?4عu"ǘ?缺I>F'y{Ź D_{S_TWq<{w'i k\vEUu_HUEDDD$zI$3gO M)#zJv ۵1yd3,nvd|?/rJFsx(*wD_DDDD:D]svK)h!7|tZb`2⇕o.']TW)))ALLCC-R[| %ՁHyR/R[Y[2W&}3(((8*,RHqDĥJ⣱S%k}eWDDDDD?P/R[w)?s$Ϧ0Y!I""""UO&AAA;#Զ*uN6A}2^1U~@@˗/'!!ҜPhZeĉ4 Wpms2-v-֛ff@o3v(+I>PĶHUʤD_2asYD>=>6vYn\еkYl5};JJyW߰a͛7/}Iͦ"e"e"5J%[3i??lXM9.㪁yKN3=i>4jROD#u&UjjCjBmEJmEJmEJmR/R8C e>p-OMFI<: Rv8|?OӡO󳒛SQ """"""#x9 o[a׿Cu!;vK,V30dSyHrr׆ m_RHYHYnա^Y)mI=Ќn1{;^*:y+VGڝX<'M RN/Ȱa\F}Zljðv;䆻oW3lI>`לGn%>i/"""""G=sElےFI-dס^ q5]FlZ+HZɁ,!w'䔷@e˙5GwG/"""""D_ֱ龅njR+޽Ox>ٍ{(Mi*""' IDAT"""}ƽ;c~1CDDDDD*} rdTrnoAvWeY5KX= $s|H8uHܕA $%l"&(c[9AڹZڜf8gVyXzc7ҼwOlQֶb iJ6Î;Qi߶allc&Zpm\i) a$l] { $Ƨ,`ٖ`z7WeZˠpJVf֡= f%l6^=s˹"qB'֡OZm0G&V/5Z,J^Vv. FEVdoY5!>L65tssm寙ţ})PGŏFq(NEq步w1q0{ףIC+w-"({[qfmf޺m\m.<EHOwи\ؿ;1M1D*o=" voZ ԯO{%+) aZp0YJ`:p2v+Sd%ךbb;$TUej2g6[VNۦwK-U+a%<.P3}8G 5 (k[qp`BVۚ pA;|v,bQRF ID_D* _sQtR㇯K2{k#7~<|9YG-c#gQKgV)c[qflckFŒ3iF2SXtI^+o/(.*5V0Y4R(k[qwȸ`ENHV9$rssIOOxj>(..&''"YN:E0$S]X"|6s#,2Id7#о% ƌAulȩKfa}M8>v0cuQ^+ķE8]|,f=[QWkӵm\>洊&a֯cwX;­$%+,^Oj5Vy]Ȏps;O}V)sdgϞOb )iiiL&ZhQ,)))Qn]W"|Ӿe6KZftUJG]RGMԉH0+$'}uڼ<;iĆ8g9D ej+M8z)cحMV<<ݱꂼ8e[1OG޸Gs VΝ?v7귢cZҨm[rW,aV  ^u.Lu@Z)s$_h߾=g/**e˖Qͺu\ÌonՁHUb"U"ZL`>\tO \ɱIRƶr&_ Irb!EWNH9 )k[OiSI`εʜ觧coT\}RnnCqr{V$Ln旾ogF%DDDDDDDjd mNrjz'rfߦ>kA;a~GU)q9ٹoVny2;0*=D.ѷ+<1ߙäs"XôFplfM[r<\5QxcOn7~R.xv׭)>%""""""R\ۣ?o~=E@n̟k32q6iO1˰ξUz4(aGЪre;1θA޻)Q hk87Tv?kqdv`Xl)KW#@^[װ|R._ͦ; IuOd_$) wZVXlzEDDDDD*KYI+v;vGҿ`?>[&pZvgn}t%x0-; i<=l"ᄪ g< %$ԓٔ>m B$$6.m)+-4s-w uc|rRH>0(ػhZ'QT F]V4פ}gV&!Z_f"0؟lz)Ha'3ÑCF;u"p3`%<܏L  0Ҡ7V }\;G? 2l18L?(q\Npw?Y3Ѣ񈃛:K(J8wǯ\k,Y쌡y#ҵ=ܟIqv&ko pfvZX8vv6nVga۰9 ,ýԶsEDDDDDD*K}KL':x| 5}уQ)>Dv|P"UWDp| >3 Sb#2&IѰW,%83Y/tX2"gcy;Xqnn/cc NZGe:ddPJ;_eq-E8G~ w!Av͢ÞǾ}x~Y DDDʹ!?γy )h*+2^xyR7DD;}(n+ÿdož]c^鋈T7.‹`{Skn| SUwu%""R9/b.#f#fۼt0MX\@&ih@̀wӎϞ wpam.H#/b/OοgL`|1nbod?S_ED*Уx>;ꞿD_NߗTWFv&%A|qQůYNY`wtfpJ>s\p?F6Y-]l\s=I70a""Yc !н W~6C&og9@Z4dF})C_XeNظq#M6գtqFʼw&""Bs9NكC9 -F|ςo7=}vf>1'pjtY: ./1Oe=9bʜƒʕ+̬Șj -6aaaݻ;v_>>>xyyPDDDDDD\UTRJxxCjJWDDDDDDDj%"""""""5}DH D_DDDDDDQ/""""""R(A苈 JEDDDDDDj%"""""""5}DH D_DDDDDĥ(W?+c7zF #_'8FA"ߏL'y3S _)fǼywzuN n {;a,-f%,YrjABYU9}Μsz,xmPAnKxY8:Äepf˝_}hÝpYx39g?]ㄈK1X.&--YÇO}O} ^W_>'PcnބY̹k{7$="i 3MԷ|ontOXחP|"Lƍ38D9s0x2;|}L]d ]v=>!a>Hm|?.>9Oc9 㜁yixy-SN] T5m4{&OkHrT6?ؚ'5Ü?F}|/>1' 鶯{sU*|>:{¯G8 xunmѐdok|ri]NpA'?+t1)ӯ#g: MJS2|9y f=[]_q龷"2}bV?H}Kk?7Ӯj/"""""2$'q|^XBOoo O9Ot[>eyu5y$=%78/ߺ]x*m0/?AHr^Y }.V+CF^^a۳}xz{탗#3/^͛vp'OFQ(q#<3>~RY7>^EMdxs֙sM+qQq$ cJ|rS#%>ȧ07W$qo{`'4 ?70y֣pq|bL\7Bvadɋ u&<}1Rx#"o|Oe񢨠!Z|9ݐռ>z1N^'YnI^^ѿ9JD_DDDDDULx{;;Z4&-T)S#,}4&ogLR }!$5{G]IWp:p8y&6`&PȦ]f++qcsmIް)qo[)w/&<<.ϰ4=5ϸ$2&0AŔ苈71uٶ9?}tgŻX$uoL:tHowr'A_*)vx0_<),qظ`dͤmjO+q~"ENT~ef5ENb\l8r4Ls+8rv+*8%ϸXR2-o~bg+ǿM/_RY3]Fܬq|!p ^4[gX=uVj*VL6 _psc ؓټ-h%""""""rJny'+2\@HG{'</9$5y9/Z̻`а +1| _\G@? ˋ(޿׮jG7\#yoLJ,<ܿ'ME Ak}|| jܗ2@/;u'YYv<(Rׯ>I2Ys;x|ya~a=xA>y8mGhX8_aupx+ZTB|뉜x=f2Ä7cZU$rrEs ^w׆Cq>W*acx"+^zTSpԣ/"""""J>x>3g_}ְdG_'?{Eq7N*!$E  DE,bQ&(BP ]C AZ 2&s=ܝyv3̎IzbF ߹J=͛n@%dQkuҋuteR:}:OQjEH"$x}>^D/B!EH"$x%''{:>Pɵk!@!EHp+Vuo}l˖-ҥKUr槃@4zh-Y\M>PNڵt*+VhԩM7ݤuqI-TV֙eH@2ML "ʉavi~^FF$in2MiPRS ڹ<P㥚5Mթcj$k>PNnaRnV%'3_vv4o:~Y.>srtiǰa$7JN6tUr7>PNNw~Io/@THu}KK;7w]Z_Lh 7o?zУT`'.GT^'^ϧqH"$x}>^D/B!EH"6On:edd(==ӡ@VTT7nP* zޤ"$*upK.ʕ+~z}NnP˖-=VZB<ӣJOOi<ΓиG[eD@ ocCphxOУ(R IDATfiTbƷYMH/cj?ܮ7?~n?|35_cͺj><`=Nw譟TK\֗zѣTr_ipokOQQIopӸ6jrA5;u^g#G=ٮTEQ5y|r42RPETi-)Voײ[D'm=Lr(o*sdkM t_wtAd=cIh}x[O+2sC[_>OSO""Bv{ _'\M/ig{dJY^3P/i&/D~_Oֲ6wWB@^J%711#Ky9{5rڲX2$H ۯIwՅ>YNǖd%g^\ڻ1]2ueԨ&YP\}9~vLs1(褭]6 i)7›R֟ʼw_gK==MKGݯo2\=P*r$} h/]u&㋯+4F r΍|A{.]~8pX9trI2 [%/[[?ɱ+=,ՊZ2JdeB]s}uu^?C9=;HwqW;_H/|}zO5)=- ~֧?$htHC͝<_9d/»,z^,7iݷefm٣=iS_6U-T+]A.l[m;569Z55‡^Ӣ#̌oTCuX#ɩ;摒eoxxncë~ZF;wnҌzqpϚ>,!~SE|~__<:E9sϺ'}xxrҮneە5%}3Rp׼]?6U5X- ks4 9;UJJ3#N/kܜ֖%rL_5Ey>Яg`K5~?vGӊK^{]ɯܫm?҆]{sz 3,L.mTokV4Sxm|#swUjyaEEXZd.*`\feΌzsmꊇct We>W7*d;kp?"oJ;jGVWdDbϾC;w!4eķW1E]]s'V_o죚stQ`m'k~VmE" )Hц)tjGucP TKАo5eIZ5]U*6륎q:oŪ/$T{"ٚݮg/OS~סc]ϵ<{9cK6rGZeQ`~/ܯ9Se^CUO?,WiJK<;P鮷NBҷo__N,+m OO+| 5UQ+zi䯕v8 j{^|2PcuRoQjn9Bi(ѣ)c?W>~gu v,ޠh?:K˟xV数_:S;ޡbV>n CU{ߤˍd?B#<)!}@|w5@w}ER kܥyRUɔiigtչK7uq-r(KUD#YV\. ?*G/,UE2MPd\RMqr^pxQ8_l~a ;2*ՔYСsz-<(Ӱ;-{Uli[EVq3F(*k2k_VP_Ey< 5e?'JP9r'TyF53HYt%?:)}U=A5bcpJ]4YӶiQU% &u ;1k-S4}u]k5r[UvJh+^[_I7\i_[j= -|z@T{wib󸮚zuwqC:=5a y=oXtvjZ\>qMRzE :ЦN4~ z(Y]9hn;_c=Ó6MSQWWOn~N.{Mr?7|S֛"e\}}xdȅ&?te)2,Oeɐ:,WdZ=J6Vi*Iڹ7Tբu,->uG9\C>J{Yg;Zo\s+%93(U贎Rt9`Y$9(EX#T-&]z~N}Icϔ}53~UqujheJsHreϿ׎3^ˡ%Ra}wVsjuY{|YY ɥ]h2lHJJj< 5u)i=k Mߗ}\SҮ˵-)YUy}E8*2]J~aE[:ʡw̱}X*77OE2#Py? $@%7'g4ޡq7=utg~E=/g 5ǵk+B<:rc;ӝU=6^uy\97>듬r]-;i(ԡ==55+8M\wI;kz|ogM|ܾǢ7qI?&~umi0RVFQZ u[~%K~BQCݡzVy6Էu'h8_O.J{lٺax^jg;wWi(7NzW}w4ahm C<=B \=irrK]޷qs&ϝU:/ktFḴqۨbjR뢗9~.X~LK==ozMIk*>VW}RB9x1~xsذa͛j[nY}Ȑ$Mz4]c rcX,͚5Sm=otvPƟ>}zM49*rWhxOi$*4.x#ڵ6nӘG@F/oDv +**J.Y X,r\t(m=oSy}VTT6lؠF0T.K6l ?<> iӦZv~w8p@RTT6mP*zަ"$*4.@b}>^D/B!>P-]!(Av^D/B!EH"$x}>k 5u7Ǫ;F4vE뺚̥˧_U^8-$%qbMUGr}kzZ~}?e{ &hG ct̚^E)9 IUޯo&4t횝G}mڴ)=( >s E|^5B\2GvG3ׂ1}GWkꍰѿP6GUf*PH((}L0, =*;;ݡ)ʃDU]5Vykؖ999Rr t|> [| Tj  U=TSs= 3}W+)#3쉠W$2]l6 |C{WLܦnyG;\ڹjҊ<3E,|0P/zBvj3O޽Bݽ@9!|c}=Ww.^ݮadTyO?szJDU=cP:~ׇR-:LX}Wati4,%*,V|5I7>O_ZkH;r4]vFjs }goB&kB49YpH_f߭ghOk#!Te(J**3S?QN A14|νye{N>;W՗%3tC{~iz-*Kpe3}W UoU#T!>ǔh78cS"5S"ݠ9}FC_S8]â곜#?ҔgnXd E?eAںzϫf1&eD9]Gў@`>ೊթ~5)(0PGJMt|=*ZOmPgћM_|}K♟TJ$&tӑ(+ |f3yv< 2B,C}6 0c%h>g>P)1tUE \W8GQ sJDUE~Ik腭}k0tU~4]dmSAARhʉDUs∎J QPPб1DSTJ |U@/f'f*9@D*WZD׌j׿j3wtH_ب{R?Kyu:o}HʈDU=t*=}\K<1_U?a1>XjsSRRR;pzcڕiPB:11$k]jaNJ_QQԅ@D>{b|OUbj)>߃/HbS\](ݕ)Z?w=qQ[ݡA @Y!чOС{d Rݸ\͟p@ $Ԫ: ds=Uo|^?]XʊD>{p9t0צ0IdKoB2HؽPެiK5y`ChwƍOzꩧzꩧR_ёçm+O.5tcPYLwef.z۲\Q瀖F=8CяkRp\i'/WRSO=SO=W?~9l0Oxȕ6џ7o_moV?E;}rcaUk?X.)##C4uj㶳'wҥ7Z/B>i%ˉ bbUr &hԨQe3HNNVvN;kL>]z&MD>|ۆKӮ_2wűC6I7%mx)&eD>mC+z/-Cy:Or۪&$iEEEn7a޻hU`ç-_|na Wkq70V7W[M D\m̌ҨuV!eIpo.]|35A7@\wGVgtXчOs}CAV),ۮ˗5OMWâ թ~5)(0PGJMuk0=ino װm@/'RN5蠳0pç}}jCU{!U6+;#(G$in_u7ֹ~Xw$}C}6 0c%h>g>P)1t>6ؕq f]BU"X 96u>q2=Hwr(uwڝU t6EF~Ff+ɗ$"5VJrr>NfM٣F7g{hWK@k[{˝S:hąɺ cX5 } weFBZ+T1!jبb,gd.ֲK/ٮg,ɯ`>P)nѺ"X{VTjC~7L٬1RTCg|_BDW~5X-eDp'K]7&[3Nw^iu:_A hF}t}'MG͹ 2COWݳ߁Qp;Xز{ԯ{֗ՙLtXTp'gR]bŴӝs[c5=ĶRsXzw*x>ڝD)G_Oا=|=ګcd$9O5Le@ĥ$O3{6feJ\ٿ[W5pHЦY:(:ds@ʗ8,*27~qr2iLQ-G dwn-!ި[iLTr?,AW"B5SVFLw1G9.m2sRE6GQ{kk~f:.mUC$TEKP1,[!\Aiq8W-H_X__>S6:0zn3mޝiW'k>J 8>l69rrڝJLG:]eqnSʮ:{ b2Su}|]A Tf*RXDh>!|SOGX~}oz/.,UN6NS˥WkJqʥC?l]/>ȑ+(FP ODE]Z5g$9VhgA%=Z(T S Pz$Spz!zXoXoDe>D5}^Z+9t`/Zt2Jl)T\ڒ<_ɛ-[:[.[(-յ٪Ve$JyО_ҷ)5u*D4g-K^ͻe7f%j2>J^#Kv(*vl5 $~is=)(fii^a0oрVQLnEC/Q}~Wq_=3v)ٺp 3ӵfL.kS'\9±wwHU8A|i+Z zh 2iQ|HiYwuœJ\K}b/slٿ*3*AnTΎTnQ~2ljԬlU**i3U0dI~NҿFKunChի܃9'moUsEاUߪ+RiS7ҮZ))ZI2H}Jە"UP$HUpKf+u]8R$ѕ!FY ?X#v2|+Gف?꣡0ffo,Ҿ}ي⏖8sũ}/Ҏ2 _SsŮ=g*N\N~\}ERsŢ&i~^M[W/вj C.(^ ꆩ @90;dYQd5r8=^D/B!EH"$x}5 Yl <\^l]43m/ùRZ0]Q2ՏorP8M»2)1K_K7jN| >V% TPPJ]jϹAC[-;h[QPJ҉T VU+S(4Gް0N٠BIfJy9_T7NIR!s`]\5H[,S杫ԺvM5h^mLӝ7UbbC5P^=^K3MIE{S-%=4umzoӴE_KWTۃqFzH;U-'ܾXck#/ڵ5jع+^vLׇoѽtV5@-.~PߧN!MQ8ᎊQ͝g/#/D(O[Sέ(6{oܮۗސ4y*dX2C[\߿]=M^}IYO.I՜CqnkWԢh_4"sxgːy}.8K^ KFnO9OL͝s7(u ==E7?8Ce׊kxrMh6[G~4/DzJr/{\mmGaoVq~G$G8ey+kr^;E@VYu~֎wq$[wk:hJ2$kB :'\$MTܣdVZH(P*Ir휫k(̐_OCsgiCaQn9Ґ,5 \5Q0C&aUv4(&}4ֱ:g jfH~u4.*X7lSFm!j05\0eX Y dpe6G?W~riǗުy(oz⑷|Yr(Gho ,rtt0#KfDtX"yWF(JUGVUHe$I:[զ?[d^/rgVJ}l#X11G_THN:(}\u8WkQUQt/dQ5ʗ$z:|6~N/o&닛WĢP)+CYG:]g$7j?^t ]eワ5oz4hCa}Syg$@OTnX"lUS-T}Xm۶.;wϺH7"H7?woѫk+S3loX_(u/ss/qhNjȗЮkC3-5-k+mJ*S|e8'^EeGVWl$4u)]ɟO֟_ҩ6ŗOK5Q]=fR #>+{zn{biDkMY ca{I5i?^OI5ln ]_NS&։ooբAC5hr>/do Ui=}nӰGjvݱLM.j*t3pf.ꜥzRz+B jĺ&5j땦S^D/B!EH"$x}>zs^׺]Us %ۙ9kΐ Jz~j]r,G FbS~]ι4]]U Piݥ;(8}>PgV(G(MS__ͪ+Ju;JΩ?o!b;a}y|UEFUW ѷoPfh?M*v?U72TuՈwW+Gu7C羠)ok5}/b 3RQ;5MI:7 UwtG5urv麭`MSmEC)9P-롟må`lWGz6%%ڙƩEc)~}5W[i-ܨ;jG7+\'!{ރ[5}Tlu{sGv}FL׳+(cZMh8SAꦑKCU˯tܕz=0WP):ٳ ըG7%$^G.Z^\S|䊂D΄3E_|A=(]1g.]74 %sҧ} og ׹З#ϒ1=Ydd*w4pF")j;C%b9YmG'mm3Jc YbwWYci`Oآ'޵(ƱvfA y^q)Fj>ʫ\-$kZJ'$)悗5A]p ~Vq4LOiP,VzU p}+_ RiQ%I qnp :es4/5~ Rm^팘zqTҐxn|UM}Uj'ɾR?/ջYk9DSWM!'ԥ]+V(Y%x?p&o?Oof++;W3FWLT^_lV#u[ |G9 ]JK.S5l:ᛳ#wf,ەy܆.m -p{(m&uUGb >UJzT^nt$4CUnlwU/mt҈1n7vMWԖES;ӗeE+ӫub5uf4>9wlXc^m (a$ñ} I Fw])۵7}=w|_c?>B:h'֛_Զկɷ%ckmy퐊__"U.[KG]G>-Q ЦS 3`(/$TU Izg[ɵ. /h|kB(v'(\|./;:gwC !4j @tP ~-{wT,tAJA]~sawgg]fo3݇1>uy?\ݦmscB(H]^7{NxUhXet2,C1~}2 t(+b>\S@smH22 CF PW5jWU-:kݲ^;+:5 ]Ud]:R#W_H\nR7T^ö8oOҧ{щtZYӴ/D6l @>y۬sNO7O껧[e 7q.;H"yT($K*SERb}Un[Vl/)6D>Qzu٢k4Je>\I?HAA RPp_ kjUsuzd\A}b^ř%#w̜U쒑?Q ofouGEaQC_ խFbctzᇧH$SSbi8;jtִa?JSЙo6" f 8q¬YԳg"m{/8;z K,Q/X\.W+Ft̡]vT5OiWש-{n5ߠ$5m罽ϼPPX0&MR.ݨQcV;mӇr/[ӊeGtmm8ɗF5kڕ?TMمcRpu%\NM^}(2zp#$}>nD7B!FHp#$}܈%%%:W}kڴCpa>nD7B!`11g>9,PZݞ-p5gA4d IDATp11F엌(!y+UB*M}P$iЏ?:Jd JHQKaedͳk@RRݻMڽ8,ǦV,]&s9HeR^+?dFSTCQRRLǻh˿\$ŨEo.Cao.>PBl6COKґ#_ff$i%hРZlOիW/%i֬VpYro߮f͚?K8$@ Y䐫CpiР4h*U_~%PB.u>аaC;z衇Jd$8Yf4y7>N{ȾIp#$}>nD7B!FHp#^.dݺuTzzCb0%$$:+u=wr%$X֭fS֭] +V(99d_ܓybeffA]Æ uAWqE\]ӣ.0\C]]'pE>+ @d2:+u=wz9z\Xt iOUm0 VfwX7/~FFH[w觛\ +q_}15pA됺P8}mv8?ak5*( u8H_Y%[QHw:E8;Yl(,*u;>j*'(*y(L]=0ZBC~wlnD Ke-Bqr)VQ&g{k誧[_t^=W~zުn9uy{5*VNQ:7+ Q(Wrq%}ݕ^5Uo5#uS֕c=qz%,jܽsETxVNV{OM}F-ƒIJ$ z#?%tlIC澪z}Ž }˥-J:رuޙ^WO>rڡԚ3fa:ܩ\4rWuRsȯjo^=~JuwD\ŕHηֽfXv>f1~3wYp89%ֽӕiHrhd8{SuGt}!AT#*DVQjy7q%TMdvnymd y+j;%N0dHEVo~ڢÐu1D*Bj~Zw|yӃT#b:豩e>y|mիȨJh7D߮>"i}wj 1jS;FJu@ ZO}TUP _\vܶ]NxL .2+gGۨ+TR~i4~G(ZC~c GFTNE55X[GOӍdtBJzzLrrqKw#zzrfGҔoO㎨OɂvVQ{ddj6Գe]6ɶcT[#+(b:N7HtqV=_H7pz<=v^j\9i׻n_S3Q$B~z}aȸGCwޙDO?РБ[c~Vh~~ İszHs^yVoX_xڻJoK՞iz 6U6Z72Nw}]Z,OWl/us\V3}BtM6*ؔyag~!ʢn`XM2+u;9O'zr=]cTjui;dwiޟk4$WW(d0l2uvO I~r0EۯzF-RPjN[ZB>Ua}Vp{gיs-^.ؒSZdTV=3OЌpCjf)(yӖ)0$áJ_-U&W^ꭳbP[7yZܴVGwЇ/T3oVt\%A-TIfUK /?J fQ /_]7|_] ޮQ((Ϊ> jث8m 1wTT{FvAaR&SVVKw;wRWǏuުpyc@B+NSnu2WkrtX,CQҠԪu[5ͷr(s9EG2[,r8282'ZrU,]U6*B'o.YJO;0دE^ >1"Ő^бzS.~ACa2jN8m%[f-*Ӊʴi_ZGB;Xg 5Tg?UO>o2[)~2]ұJ?. $ہTe)aeU' .8G֭_b :59(걽Ҡ;iUFoz bnL_أ:PTd~a=q Qs: 4+SӘ[3Fc4^/3wbuy5UG?T INP_CխVEDWT{GuCiv-DسM]ں>fMj\"*TCf+?f>.-Q(Wjq5ӰaÌ:T5kzYme'33S4fLއ;4iz9%p/'p!٬ӧo;Z{{{ji&MR.ݨQv\Dv&zu>+?zL܍4FpE;^;u=w:D+,,LY 0r8 su(WzJI\´~zժUK&oRph$Qp7WB=OUN]VV]0N:@]\ <>+ apPXu7B!FHp#rK,qu ѴiSDp͛7wuNc3t7B!FHp#$}>nD4z*'[TBS=(_9LѬ̡e(ɘd4fQrvV$XCŘ>\M49qŷsg 8u\[ ,x*zw6e9{ǙW$2( @!J5Wg>XQ}5aoHOUC5?zI2 ڊ{\XuTx=tݍX!Zڕ9M/iX\ < }cTZf͙QSWwh>e"R,TFUEXv;~AxuVmݺxw ʺXYS/A1h㴠n-=2)|˝Y cU1:H&?!@! h<1L>oR RD`Wsն:xW6x*ktkMj u:ńDX&yi=`d:U{'Én:Wxzx_Ξo TsRDT&Ӏ }m0tTuW}$Q^^ީo҉DT3]-T5,PJhc>(!!A-0tTzl:5BJ$rdj˒EJfVӞUpdA>@1 ŏDpZU}=PQ lУ ުePFԗ|zc%'Pѣ8aWNE~ $#Ǫùvan2xg4䅩fUuCup\P|Hghſ_ a?(~es"awB,ze{}=0lno&sVmI;msEVTr!¡o>rHБS YyJ]NXO]Z1W$}vOG}V6~Ͷ; (V$9ڗSkֽ^'M#vgjH.ǪŇU'3g޻"?Hg QWw3)f{*0n:WHggká0 y8FMsw(u^wuJ;dش~f oSU<^C~*wMG WG У8|- 8QL:t-W>͙6Ͳ85m^^߉_㲝 bB>PҼB4jj[U_d=oܦe_@XoMa9}!@sb V(N$@IҷзWsvP[JJ`c>T&F(gT-۹?HktkMj uHq^P|Hg{*pә{"UݩMK{+*"tOJ%dRpU3\aȲoʘ%ٝKB}=LFKUjyڗW"r5WSOn:Ow?$龞LE>N^3U>d3]5@̲+e[8;z'zγ^QcU@'?#\ "P5kQdY^2-=6k[fӹ}8Qrr PbbC(H' mTENY IDATGr&!H5I,-pbﯰ(6LN?L[^]zܡ;PHg2m-Aw?%6J+vk+귯SOjչjTUA}wKlx4Q @!Ѐ<{{9Pg^ϺL?TVC{*Zvϱ{zt|Vd@âڀ3}O-7+q9g^qPT'jP|3cKHH`~>@1Gpo5_;iJu i5!$ɮGk>@DSpiLEU TdI֣yڱ=OY6'bA/S>poU1fQ;XuД\f-ϩ;IPK6ݮٳ|Pת~W? (JuP~ 1ɞ25Z[9vin=h 3i9V-UUjԡ8wh=PSu ːwj4ޏ)͚ }S1$am_]mڜ꧚h؍$@iD3pu m&}>#2oQmω׃]CuK:n,k:Oݦ/һb _)TϷNyZ$$v/6*Uhx)ЙH|&i暣g vɉ%''kݺu-0tp).}2fLLѽcX}WK|dLцM٪jU oݺUW;p}):᭦7WRmyDygNцi5ڙ'KpjVk{@8> #e:8fM{t /am[0AUQ[;5PЌ4Oe]zhԱP\XuTP ~`}Dx*了},mL=S%;ODT4/ţU+2TAAAJvu.sOA٩̦s_3)4!H寰hBѮ iFpV(>$G36VÐ$9.{,mk~TO_˦_T렏oۮPoWO,0PJ=੼h-5{4*>[lݤT9pRaXXfEvT+w[~ON Z^^zPuEޞ[Ku(]ժv>D>{x}(^ $Kx8RIyzv,!-;տuY혗N n'99Y֭Ӳe\ J  sD8m]5 lHwN/C3=ܨz _vaO=U/Vfc+!daںuC@)\AQq8WJ/}OשZz6mFh0#X')4[3pʇp><}kdTCw5xL|A{@c}.MV/3*E6MEKpeXtC@)\AQq].-~|C:z%oMj+$Ȑ!-dE}>1P?O(84DУx">1]ޒl˵'ǒdTy[+Zl%ޭcdڿZU/n}%Ħ%۔iQX\kUKe7qTے(iS,Ԡ֍7u4rkݒ%w[}VڴJpG"+ؔxlU+ q 6-MZMj W^7^(z+jaY嫰jӢ&mH %SIYW=WGsA$J-Q|T'8[gM0=MkAZp­5ZVGzfSfy@wZ3w[>qx+'P. 's%?E -Ѱ^.?!Ń\q(c_ZmSlO?~WҦZ8k់u(|KAn%P֮eW0oBUnnۮL*Q*d)SU⽵}Kgm@UUIfpU#YlwUsEdKӰJA ũ犡#Wj_j.?$H=J+ $*jfMf(QW]PT$ѥ!F#*lJRVGrdQ? I?tHy|cҎ*,"?ZV-Юؖjnb)bUC II?i10{ 2\YqZCWC5ԨF<jT ViA @ 0dd%aʸZ6l^j*eYe;o7,Ԥ9~WiaJZ6`>G*b(+@Tps7%Wj*zb XPPPsrD%l6M^*-AQe~EZey2K4mQLQޅowTsŞ w*ECE{+ [WYF튲W=DQد7M1t >`RH`=tdOpU]!:tz CGOh ()H]zJϕG)8ASjݴQ9rL4=BQSܜSc Cư(TJIVQYURF1ܱ[9T+(E+US>*i\YeuNff$i̘vɒ%j޼e @KJJRӦM/V9aҤIСE5jk}>nD7B!FHp#$}>nDx\_f/_/ J] |8G>ig(]`[*>_7>(ƃ-sQkzA4*/(CG!¢Gh^wD)s_Vղ:`.;\J8:}miLh@:K!5{ezK2_*rLjuzRS쒤z+3kP:]Æ$hWQXըLE;NrP=~]Tj5umô!@3ﭨ~]6 TكezԪ^5Uo,|\)^~~c-'ܺPU'^jѓ5Uq,\ƱRpUm&nAcUFpdTOt|NOTLW+*25t ݽ|.P(}l:<^_LISV'gҔ;s"=,ImTmH&ӾwԄtCm_eFm^H?ސ˭cߠOn;-y4uhn~/$o/6IQ/eW5qtݝ;LMJ/| ]:c=e6i^){q:"OlVo4c||}d[RnK_S30W(K>vmICCEvDj"j?v|%yŨ]Z:3EG I&R^"$njoCGJԧ~$ަ^-$Ǟ&QX&I>ʘ9]kmLfoCʚ$sk(]W5 6IgQs䓨?Co=v^K:EW&ɻzZyֺuV]~o6(""ke*0밎*=#W*ϟثU9>Օ8uSK4Ts[z=J=Y1DѬeK$|_BzE guGRӎ'aUNJ8^p0]eË1 T> kӓr2hL&Cι;ynD7B!FHp#$}p)|4Z{l;wQ?Sf@0 (+M"UVU(?/*R4"1졭 J $R BQ:!D ;auΜ3>{}ʌzuFTW>1 :XVK[~P)mK.7WPzj8r%NnիPVF$T?Rc&s0am*>GS``56Ar+D=AHYuѶЛtǸPs_hѣUϦzoGE?oOn-}TH6ÕrJI%sYj`t[c];C5mKO.Kј;]ݨ&;g JV1=TKժSnɕEc(:|6PKcL\2H[M(_~ŝ24!q]Tjߘg=[mցȱzPV֣r.ι)٪itmzW;iN~wg(%wII7 mԈS}rJ*\iՔOG4 UʡÕwFG6Q%3hݶ4/ΠFK3F fh4ZTR! jeTRw(񱺵I~7ք!V,ߡ 3mSS2hnJ{B|zz%-tЪbT Y*T%n{Ťe7\S:T槉oN:/h4J2퓴`j/5k9LGT05@ݡK6ܻF5_u;:Q]җkN_#ȖV-4|\O}{:thW5z7 9F0PEўZzV6M?-]zy܇6¿K̾j7/էA'T苏^BW56JSVN}5*donWA:uqCn.?]pPO~|{LNS A[Ӈӣ܇\:۫YܿIUS1Q^="a}V;sժse1 efWdgHt T]%F#?LԸM='r`Tᨔ_!D(hrT߯Q "\nκ%L鯌ӆ;kR[_9PiWu2oѦ}Q+cJuqE7WʻKT\/K&E%*ƆѶQKW\:}>%$ɨwO4o(=UuQɰ4nNL`<~D>ց[hJ>~l(I}F״ak lUZ\"w@d/UqK%ť| *.QNtp"qIzo";2S^Q_WJc҂0dT,_MtP v5k×O UTdv>,,)RGKp1[&ʔt6i^1>H dܫY+ceJߡѴGw[}f+ɛϏ;2g5`RpH lOT/(pI}g~O#oXZCRYTj3KJTjgP$%MmNcjpMTOQݮSX!cfNV\ʩ#եPS Y,.jn?YX3+ 2(h=KŽ*jNʝAGῑzoRdR:M9Yg'zQ"O׉9*|+InnP4E1ucΗ:FYh"ZyT=^qr7on^ޜeejB\}ۡZ<<2Cٹ3Mb;a ޫyt~C=CΪ2%Y,~5| A<Ƣ&>=PvQIJ;WO?3F^ݮgczŘd ҍ[I9%NQf7xz함C֫}(*RQqʝu ZI:,SyzHGşQߡmb=bwHEerJrSтowK5z|H}ӼBVU^]o#̊Y/ޚJާUk :YIDATxU?NKIZ8Jv^Jݻ{ne6hhDI<̜H4!.GmvԷ7)%I>8GvDur$}=hfUGrЖ h߷?JW(-* )foF հE/D'ޝjMbBm6l6jfdY1[-R^O:V ֿ6f}i]#d UN}=7nE* w(-Ӌ&=3^ lc]3%Q2F?_je4-,A |gb6,IшF i{eaiPjU?Dzl]/I)4,\U6,  $96k#^pǐ*,tjDm۶͈>pyHzzVX5JQQQ>H;ϪF}||j$m۶횅|\3fA}ܠ>}n>-c?_x>^!Ex>^!E̞.ޤx$> y,> z,Ԡ]vWjjP||||j1u9bD/B"}A/B"}A/bdOjw2+ޗIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/managed_dock_window_top.png0000644000175100001770000021003714623331163023366 0ustar00runnerdockerPNG  IHDR94SsBIT|d IDATxw|Eݥ ZH#+HG@QAذ OEEPA齥.w1_]2wYY 3z^DDDDDDDrM>p}ذaM"""""""w3/ft9?%""""""rW3gΥ`AzNU"""""""ws2|pӥEDDDDDD),,رc~CDDDDDD74.""""""R0(P)@w2(َ!ݪR.""w01G6~^&p?%C[Ҧ/fA|>ލ߯~&eIvt +1&W>m-WTqg=Rrtxo#&GW<&O~6lM5i$w̚5 {,e)P;A Jsxdgq3dl;AmMgDkj j\RFi KQJy1>Ԃ9o\zh(P܉- Y޽3I9 ͛Ν;VfMlW,""Ҏq!́4|x K;cZ&X3 p*M@g65T,DW ڵh;@Z"N凍kԛ9v:37ظ7=s{$ ?C[2(.eoiIЅ>BfUXcQX N) eJ⪷rV:c`oJ96```Ig- OBEаM+'7]?ShKAz׮]IHHvY9 ԩS9sUχгgO"##]H^_8ΉrhZʉhEW,zFn\ݸ8-54edt Kb*f83[nPblH3:,WE)Uq]gm^˞+/ӏN``߱VJG~o~}2UP}Gwv"p*T 9ʎYG JLVsLUqؿ5|h_~ 2Q?~MUk׮稬V^z1e{DDDbdON"^8ű`R-yKUܮR3QGgYAmN%fA&#$;u͓B+Soy.&^ɻzXF?[ajtۙ.bOlN8Ŧ7ԣs'|8Q)w@ʉ9`"y;Z"<)4w"9-WD: UW>w3ϟdɒ8q%KryrTNX<==ԩ ,ٙݻcZu,N8aGr#9Lj`R/R۸.p|`nS*s9kQI8D9=lx(JZHŚbҰ٬ 3f,"޾L|2XL&OM)1;a70L̙m(Aa{q\\MI$N8\5Fn'9!;vέ۫27ᙘ+IVE"" t܁^o:S<))kN8ƍ޽;ٿJ& uRX1f39hH3![S2St؝Gc {9'C H <ڎɍBt_Ypױ_zhKbd<+6Ȟhp+Fҙ?Y fS|?}ZŴ{6߮:wP-K ܙ1iٯncNb%69{m\]1c&Qo)Q'ED@pBlܘ!~e޵kWYׯ_OSzuRRRxb%KQZG7fѷ.KZgaa\y?vmǼɄ:xMxկIFL1O0LQ5}qbD#[Sُ +r^>ʲm\}da`ʼns{رĖOK 24y neU$ٝ(R.8ǵkܩի}=lW f_x]bXr4jo 35kvӍ)8*La7̘ ;4+Iq$X3[pv3ج&La3 DZu+YYH&| n&Rb"JP?'l DěpɂɞJB'Bcs1ORcW7iqGē"U. /3nNfDbcIfLnx.fجFǤ|z>.""r{MʍbErlI>Ǚ-b} Yf>O-\UmB"^qU#g|>g1H%.2:+ٮ)0R=]EDDP.""rv}ܾ\*pPzr"""5"""w[Eۚ(U͇L@ `D2W]_Ê)Uz};up*cu#iR9P ﷦F u-B~h`= 6d+]i\8U)R>a0ZDtxk%2$1j`3*-K:yc)Ҍpc6V)C:C3s{k#/_i6ںc݁?x5<4f!fӧXO8x?l;ž{]ybcK&o正eiou;""""""rv ΀s=wq 9:> nC:촶$&%?u)6BAp6;Ekp LY2(G&M3R'R5%ZѠl}:P-hZ&glPb?/v9ro xMSn~}ϽLxZ2]1CeB*z+'yСCM)|}}L2Nu/&, {|v3^-~!&˵  = N >`f߈δ>ᇿNjti=3 6YޙA:VN.{_.pé!3? pt#cg f2c-D]cy- .qw"|}ؗJ1`9\R-p!f3͛7x[lÄwSDDDDDn+9X'΄OD\NÌ=|21@X;>e?q̣K[G5ɫ[{g<j8xc>U\}txlW)O:j.9sv*yS2ZxU6sōcy&\ȓ^A =BCCYj'O$11nd;wSDDDDJm?NbCi~ 9x);v.ŪX16^ޅ9gFQ dlDG] A.`UDZCd"\弰 b9=L{u3=@HBiWO=γm_*n7KvRE]-[RaM) [3xyyd”?pGa`OԩS/^<$"""r"Psr<1oΤ=!ȿbrIB|թ*}OZ l< 7 o0/܌ёGP'YΕ6I쬠Bӡ/4^G@=Ĭs[Bz\ϕٺ _xyҡF=mLj#:_Ny!tv? ^mHQM`7tm;W|͞X3΅`<\֒6J3rrdn׭ of =M+Œ}HkLZb]vf6~g6m 6d6ٹs'7$ĴiӲ{6BoOzDDDDu3wqnb򼈈dЈ(e>3]|l'uMH~Q.C^JeR8:8舓u0sRε Aq31_\y.7'鶓#w'tBDDDDuEnaט&-m}|Һ&#޺HS.r Y܊P'|˜v~e X)^A&o3#hZr l4׭H{Bo٨`U{fďLx9e]nvnDofԪX 5oz/=@hTיW}CfEDDDn E _D [~eэgރD᪃xC?AZZf/-c# ȸY .|^_ᣙQp/ѐ6Ep6Yhފg8Rn8iɰYǜ2h4cX;hq_;1u~Mҧyp #F=gYc~݄m/ĬנQ3ʠ1~7b1P߸3:rvL/Ղ e8YĴmjoO0vEU po>law+#%Bϰ`Y=LjaΉi/F|qW}LYs.@]$?s.3 \/Far A~\-.@X/úqo /)[9]iۈzҰfo|2˳CjT jH݈S/;`? |W74es(5#8i^%ͤl`2_ziC1GnzХ_ܘɃv#^e) d1\}`p/H^)d¹t3vz,eh;]6\9}2d]zV>C[S ,uxb`vY)[$kÃeVKH}E$ JF|]sj >Nsf4Ο ǧ?9s`?nWǸxe#3Le =w!`Nҳ.gGr!̀ "qЭ L~56pvV^mCW4쉗 ~~XGk#4.Ce`vRa = LGN쮯t7]Tڈ-9Ic?ɾ# LۧSk^Zڑp EsI*l$s9l^>.0k"NXiGiҾ}{6#O&Cga+̓ 8<*샏{(/.]صk{gz"A ӿ&Z;a*쇟^_sxg-ODDDn@]$~} 9UOߙXHpuym;B62Ś` /=yYD])3m#YјC7wRa{޼ДNe/̚K>̰*R>OpKofHKKO6+FǼ4 k;)9ke|K#:>͔Or$F*vs<֛ c{9?ܰ%R؝Х]?|ө`̄IQVҸeEL8ROtN"""" M}S4wq7Z7gYl377UwāT;wCs>ZI2Ɍ:0y[zOq>Lxhw뺰ʹh1uݜ`h:uۋpM x3fq{ Z=soN_n8*wof8(ONC5S),|1DN2k%i@#ցS P7W/&?joBi\HDDDDDD:,P7.nbT Lņ~og3ol;o0V)F+~E0Eٚ<<` ؋1 B6#9̺7QatFҒP2ᬟ fw(zrjwL%p,XvX4={y>F~6@񹈈&/p S?}\,~{Y~x<أ‰eֈ$ ?Z=X?fdNҹ9ղtH HeGHΈMx`+{r<feb5Z0a&*3'""""""M|ߣ$g>vwǿq_y8 =>s#yYyN$9S` &Rep.:f<zN~ G/Ŵ{Rj=$5˹yF>ƄyMEV,Nrеa1:;/4ݑZ3g#3yuUiՓVx7ZN 7\pNGG;e[b;wV̩ EDDDD>>Z*!l6j֬Mɑs/S~zK^)..w3DDrB w3 "r;~\H)"r;SNܮթ""0n$""9@]D <}ۍd&8Ev (POҗ7 ǖ^|{&nq/ܽJPwK5-lJ^{ݟ3|0ǾaZEIIূ$mx? mS~iO0vY!W"nK˓-JJwAOy1l -_9M}aCDnKxOʥ8ڈf0 ֬vfR{=9W~˟ 8Wwv.y6Q<7i-+e:+W!-㱥T+؜PRR*)'<՞'~+׳nA-)Os){}lԯ_ y<<$py>MɸWiǖǓrrkC{/})))?}wmrƭt/] mo䲅jW.٭<<a"ul+Y:|@|u%)c^JBpX=YF0ySImrtu'iHۗy{0m_۷߳%";䭳!4[$"w~kX{.ΜS󙲾Tvά\Fd)/6-HB67Q` %׽ys~Jxù:`c 8a^u>\5`?֫5#NPBY2;Hck8`*,9Cg[2l ΰ$d9]KV5͒OP3lh=w/7818<3< {e86Oyh:V-/x(֌}I;a; ԤibaG3syݓ0q,N5~Ɵdw駘5?o| xT9;>X٘%A]x#WqY |}2a-OF2U h0u;hJl;z)Q&r;fΝZjxyy]|N!rڇe0ea?jwg}g1b;Ʋ 4џӣ>1=MFr$>/Sy+GMb+ML7 !^59@ց uX \՗3p=6-gI3ʐg&'Hr.Eo|8;ϕr$B&yvROwr=Ң] xfY9K>z{xnLGб\!Lp"ɇ_Ō1" z5u/004~ٟѼ!cp,593f_~ކo^r_{ɦ>dܯ;0R3:6^D3aRuxk4v8utoJ>7SK'DnwmAbB.nd&BH,.RQ1C`yȓ<Z([Y.6-Тm)\&wjy:[?㛭Dcr̃+y%tK>KfѰr߲ԬyEoqft *MOgˏ/-?WLYOX>ԈY7?s^;-jN˒,9\OX /WBL|[_.aƜ ?v xY,Y<EŏHܤ/ok֬.]h}DϜCgeY>˙QKXI hDY6*=r@ן:2Mt$Usb4vckQlKf(d2Q5+v˿I<SF9gJtx f-Z̑el6bW3oo}FM_oQ|!q@(oZʢ_ QcS筙,Y+uM׾bK,#whG0O|wvY8a bt}3 >g酜钣do_Yp"6cxw]i~!9,zCaޯ/઩& UjEW ,un\INL$Y Q9^"r#&w5x UdM!mhgq*l[@Sz }<_PoE{z-s"Jx0sntPc2R,Ëz7"\ʴUl؞~ &w?aJq738 Z/b4peݗ[Z9ەp+ӉލbmA *JMo ŏ s<)z+%%ƍӸq}ڔSNz=ky$w@ :Tv-Z{֔Q]=-Yй#0a/x\c¿x.F`+T%<1ahgx{p} P#ϷYӦ ;|kc2 LU[GxժM"S3^WK Z>ؘ@GGU3/y!*wA&p.ANe2qrӝqÄZ@Y]$gm]=cWT6h6bcz IM"-;`„gn|?xtK {&OI/h;y) OHNiK1QEB"M-3fᤩ,}Bʅ3f.LGeAFأ󪀗9#f<;( v[ByzH2vګ*7NҳkP&yWbON +eYy T~#?/Q-OQ}фGA @u}ɇddpf%4僸G&zםNvr-R^e1vRRR~uSEn8'wzr2~'3gڵn ""l6dɒ߉ԗ(#V5[e6SEDMA9)"귮\Dn7g)PM_Dv~jEv}V^뛛ʼn\-[ꈺ'[l!4447ܖ-[:w+ˆHԗ&\ ˔)ÇYjYܥ|}}L2>>>vL&!ۆlnM)ԗ ~zHHHn)"w)G 0r""yno>ԗ ZLND *U{noNTTT~7GD$[|||J*ݔA}n B?@]D 4}/ə\]]DDDDDDDnuDH@]DDDDDDQ.""""""Rhw_M#խ[7_U.rhذa~7ADDDD䎲~|M}qT}v5yYM ۍyYm/c營%+qo發)[z"x_tcj\uA"H܏VM<< 8m.o⫞]xgcBWOܻ}pÏ1´o.~'S}J;߲戈d[+q-G_1yoӹ=B_5θno#bnN e(Uv+[G5ȵ$gfL^uyc&30L`6Ō)e Ov0"֫OKF(U<Me/Ф %3laB*6Gk0q^$-^P ܂NTh׊׳-l'ċ;i>i dh)e,ImaywUTkǿsJz@ wE:((*PEQDZ{^`o 6P4!^H9e?hMrB5KgϞ'9]&xdAo x,?R`Om-<.V?o"ix:~]IxM3&nN}%ؾl, YY`Pet HEDDDDD"T.y%$r.fN#} oh\mMktk46ȍ1s\?~C7/_oMChlх>mhxD0ӧs){qin1MIp+zM8f5R.gsN&/po71c=rtgcoc_E;r1NR9` 77w OsùnJB, a``YqN%pK/k: p_S 剈HVz>cheNԠۃss?K IDATOQ̝J6`zi4f/eauLi`[%d %",+F |"""""""gS#]XdPF=q-Vr疔{ݙfQi] 2Y\wWrs?Tp^^2F}ncM l[f Oߧuf3Z u;O>o4K72ey}RR=!>Zf]cF#<>oʁ:t~x<_4?ц CN`ZB+I8͝xO9?؛f+o~DᏙF#J􆧘_P.gUr.""""R2岘\i3W1~J.{uZYjrӛϼϫvy9DDDDDDS.RUywgăV~v&+'c[n.O'""""RE)Q W䳧Ҿ^$"n\O(""""R5iHUՋNe B<(Q ?"j*hJuܹsK9)sNRVM卧Gqfߚxy:.T{CBB٤fREL=J%"铁t*}oQFʇ2+ICJ5QOHHbЪUlV;vHllC\\]ѵgd`p+),+^RDDDD)D=;;={f"`OQI6ZsM;kQ+,"JuKYѵUl h B7bW=HUE*kCx}ohH'c9;a_ϦTMJE,О/dۭ̟)D5>=i"""""D]*sd^=NvNn@:xDQ/.Ng5 ȩ/ff Hbbb6*""""R Cn|,7&( =.{t_ ><ʱf5eʫ5DSY\T#DDDDDJe8]\NJb5={Ny;u5mÿ!;c ^1}zZ=)%F թ~#&jox(.*u熙̲ ⑛c ݃ufSzՎzt>]ߒC,zJɻ/oo/g-"""""eu?,f>y~ekOY_K4F9HIQCDDDDDΨ{5hL[H<{vKXiu)xڧxt5lT:wneX3^&$j9 W։I/>>Bnaj|\kěG7wX:]݂so=LxH+>> v?eڐ1~D4jE9J"UAbƽίfaxb $FEDDDD<umx1wwg%ͼZN{߽˷Q=sx!?ȝXi' .#fx3ȹY^;DB"""""F'}z]G>o˧s\(Ry]ʐ.`j5||&E,!~ޞ~V 8kQrQ h Na7quß(Q%"Uaް/kKCQ0Ju{xxxi6'r2`̰~LϣV@=RMԃYjUi6)ªUtOl^zzau)ա'11 ZMKNpp0t(wƮū}!."""">G=66N'a2v;5835w]DDDDi19ʹ"H]yٱˤNyj%"""""D]+KInҖ:CqTq\,uۿeIQ鶜Hbbb6*""""R G]DN0dl͢NrZBDDDDdԣ.RUm 8Yk{M7 }TU^Uy}b19b' 1!^ LDDDDjS.Rո⧳< 5`k/V})wJEW9y$ꆁfhw<+_Ητ(""""R)Qjx~<jyVHƬ/=B/_O'"""""et[vOGT Q/Vum']~aQȮ_CŦWG+ɉxJ&Mg^Y KxCh~ 5S1g`FJ} nդ] ր6yq|ݙq5Qw\ĸ1 qT:ߣnDrkxd7ﻇQ3&Z{,41|@m0Iv]2 R8ia_flp<4?~{Ы[7&EDDDD*{Epگ>{w'phW.gI8 eDamӀ:usėl+cU_Gʩ_9۟K>iG@xmZJ##L摾mH+mGtk hߐq6̴e{\TT^9 ;zєع7.oNWrŸ;HBB‰- 9g[^DDDDDT%ݽI3?3Y=c ﲴHm3c:_˷c͇\;'p(XוOV`ҷiq[]GsnyOnLJ+IJZ=f ~#_Ibk9@7y \'n!xR~!߱!a=? Ng \'Ul<]Iy^CΝia}v0ié]{ظ5#m)Ѿ}`V'J?Q71qPmEGS?;x2ntw( +ݺ, ):m\UKX'ė}6SY4w톎C7GE/3~mo-? 5kt'fزVFS^/OfSu9ӞOuxם?1ZFM)q. :H91U [4Wڇs8k3򽷹E\lt222ξ@?O*""""R5!SF}]?y'bk&XI'!u-Z‰:XmVLgQZֈ"MjJ:բ8p5ꑹO; %cڰۃ :ڒaf31ϴxHg=-F@żw=7G(tףYɇȘ1֍9҆q5Pv]z yZ>: Ax_i7!kTnk>ƙWOd7W_O &7mɽzdg/bÞTEoE4jZ 6s^L ivL+q3-&8p-)|c|c-wERor Uでjo\"""""R9i&u=JwY iUF v46c۳tD'5 t;6In֌k  S_0de󞞞={+Vйs)&L`ĈCDDDDĖ.]JǎYnpd8L6>}ޤIʸG]D/_+~~6xoz>^ϼ3-H"-f˛ꏌoWE-l"""""D]93dj>tg`mx5CLrOLL 66O&""""rwreRÏ)>ԯa/_HKeL^|MJEDDDDJ@Hy{xm.0N.rՈ+I6C0=qbך."""".R A'9 %|8!sǩs 4G]DDDD#'WR,( ˆKJ }_[r"""""rJEʓY4~ )@[|To~o0/r3/"""""urW'wMF=?- ٝGFQy2ןĴݧ󹁯-~}vHOO/ %888ORi8jA6,E<ǻcUq>ÇQRMԷoߎjM6٬Tq;v !!$!~$bw{h0׍m|u_ª;2S9ÅxG麡 >WzVV{.&E c޼ytXq/YG$ۂrE}/}qѵ_gyS·c#Ͱ)ekJsmYWscO!x6G<e^IJqq7L]DDDDiQg*`!:.[m!}8 RWgsfkJj'TkK+R39NgnB|$&"<fO^Bˣ8(u~_Xn_ЭH9+{ϥS? Q@6{rӔ/'i_C'Ϧ:xnE<`B&yl2 Vlys`B#޴d`"""""UZQwStfدEK݇hoP oG| 4E_KdPQMϟwpGz2t}yt;F^q/^O?BytAͺ8M{yo5 0,XVukCa,:?咒cbAh>KW]$fLpgc5P/N~rۓ}hmnA=)weףnxW>:WD+4$<{pUؗ'~ ua[}[wo6ӗx^=ObN#Fv ͆ժ1l* ʦ]N-@R&& _ytۻX|h̭%rpzWfd@Ɲh]i0qIyNB3U+wѼ} Nzd;]_u] XOu]3\fGTX1ͯd̔m+`V?bk IwwIRg1+i $ ] |'yeLY'R2q4X r[Q_økM>mL0/ٲ-V7{DDf!6Yۨ\*WUr\Y^ƍ3\*͝;oB>_ёU+d-kZ€)1YZt\}>"+ȷ <3\Dw充3Wl̰gQ~iKoӧ`ܹ 0DuG\{[7>QWLYb;w.QL3ٹyֱ+kP M.Ma-ބ 1bD)(""""R.]JǎYnp8J޴iJL43=krTu _CL-E& hE`Y Gq!<%zE%0tzN՗1rBtY|Ftrf}d0LOҲ҇9"""""RG] E±ƥpǼxI\9~3w|QjjIЭHU>!}/fX,aty;&EDDDD&%"U|~Y5C 37n L+FA""""R M0!&RUٚs]{p&WҭKKakS<;uZtEgZE^DDDDD.zE, ծy5~h;1y<&QYul;-`gp7y6 IDAT}7t e¥(,إ2tRAڐs!C.vJEB.?ĜEx567 ;ۓ23pW2ѸUM,|cZ4x 4$=TP6\t}ȹsqQ.Rɍ }_ Ԏ?I[5 )2ނYI;c| Y.ADDDDuƹ/S7qPfC߮Ùzc2k\MpNb[q:ra+Wz:tmȹs!3%"Uk'qu0 5t|7=wWMaaw:qog;c>bĈR ^DDDDw*DžӴuk:x1%K7`kYDPHu)9%"r,ԯ˖{w)ؿMi5W|twu58Wj՚jg}D{7|7zǨmJE;~[xX&2xW핷жT5ho:y:9#%"RFm+Hwxہ]zRiIZ:Q` vhũKn%~l?L].ͨbl< iѵk;ӿ'>7wTnEkYvglL^P/ K,$oXƲu;I-0ut}Tj%0ٻ7nOӂoTc.ёZJri,4[y:sR."eL_y4̓rHܤż=5v`b`JzDx} ٷl:sU;gKf-'#(J2`g7[4̎a q˺' ]A>{h,u# rz(n)dv qY,ƴi9Xݝ߷Ѻi*=8Za5lтؕwJE>mҺG[j1R>]X?H? DDӉnGՋjʦE|1("lCFJe!#ۇcA!AfdPp㜇II"2BYI;cI!!v2ӳO63ٴd+:QӮ[INP(A>Hldy;}?86·'1iLA7Y3lZ±K+1뜴jSWRРnE3%FÉf=ea58]Kʟӆzwbcu:p5*@RT@}LUjN'yxV.MؑN8&^*! VkÆq@3/}wnnw.g#hТVdG4^Ow@"R lv.ͶӉ˰aXƔRbfstH.G:8v6 cӥl6N'.Ån;ihśة6Jӫ.WÉe~ Kn77M\v 5A!VzUև.3x@k9;#2`Dvj&n 1rgJ V-@B It@7`WH0>Uvq.sqU"Q![5hddT#XnY|HA&eRCd !0;,7DZg&9AT;"0|1.\. }1\>M=7_!lr] U&$] dggZVT*S~ :.%%B(%(("##=g_>!gsz5ulu$T7U5Wg$nGc'PK ػj{B;pI}?c@:mCŁXحh*-K4t^t8Mi5hU/ފe4jџ!{Ί ^?]%>u٬KjJXo7l`_h]:ӷdm!$*<8_߀[QkrQSg*sgi5] AdEff\dEj@ȉD*' A2Jg$&&[)))Af8%))<e5[4ϘB AuѳMTA>|~1?5ZW-;d>Z`X2'?~%+iFbTRvjvI܂ߘIn(wE/PM=x70ioV/+_TfZlo N\ګAwO>5N>_8/WcТWnҥtuv;Ǚ+ziӦѧO֛4iR{SSS1MW\TlQvvC.hYDDDDDD^k+>_>T4ZFADDDDDD-{|IҨUm,"""""ԣ~,,͝ƊV  F3K|.TeϟX+IJ<4g;+,"""""RyGob0\&^:<d2}S,9a0]ӏ@عoܔ7&_,DDDDDDJ³= x]\<+0?_kwηsWx7= ٶ}6[KcXTdbE$WML\zx3ݘ=$w٘1ic$6U޲}vu( [d",>z H۱+YzIO;ؿ}Vd媵lٝN4ƽ$'#5$ydYؕVyxә%ޭ?+y6\sϣkzv'-)+xeKlcƝ)߈ lqtpgq8Û#|՘/.wi?eQʑϽ/e,I~$.۝b0a.gҘ0WVש+qXip yw8pfd<moZ;q.AU|(-B}:EXFڱ \)#8@C>A 3lB:l6+yT"""ǸWf~\ɸ03~:zHrKFvT(3I\t8՛\w1:K5i 窡|S/ai4O^/^i;WuLssp'mjWz tW-^gw>ib)^n $,<ܦ54lX ֪CiZKy-f֥͆ns5L֭YΌ[]DD=aѮI:mq=oI:q4kFf[@SÈ5i8jE3)j^ukSOk 7\׶dwq׾w>gNq4ibc_$| d#TDBsBh; ~{*[oIKY1pdHOUߧO <ǁO@^6WN>|*ӛF-:-&MD^ cQ1uNƴ(^;zMË5S6wJED>şoݒ[bf)Q{Mox%Kkk"!6Vr?I>Ug/|](O{n"""{L[ՌG"6cݗe ]yy9 :>޾ D`S>v;8zFfƾVln0Zjn$jԌ834]D"=j}o#gbϿN.%KDD**w!R WRqڙ-a'n\,yi^Oㅝ,vt:wD/JGqcXT."R!]P.""""MOgF.}Of`!q<{?m'c_P3C,Dp>]՝v4ˆ {=""R*J](O{n"""abeU}n-_N Y0hӎ4)HK#?>{;U V#Sۚҿ-9[ rɬk~ }kiIxM{CCCqGM8X,nBCCK| t}t("""Z^uQ0A>#mlLIP:6N7 z551M {Z~?kqq X T;vk<Ȁf0 7^qxohXƍ3|ފ Ѹqc Cil޼oooJtLrr2Nի}.!49p62=ܹs0`@/'==ɓ{+ܹ_>nҥtuv;DM6>}ޤIJޣYf$B-44'ZFFcaoeToFVr_EgedzZńM+N /->O713Wԉ #,2n#8~4E_KdPQMϟ.:MHfgOyGdGܝC؈ {<޷Ob}Z>o@+^btAk\p擇΁?9w(o-~f'[ǏfJf lGǨx%t|3dz|6U:aR2Zp8p8no.šrRvy>I-csKCf>Ç}.7-æڪdm`B_Lr aȴqSt vx }7>ƣ_Mz2t}y2IYo{/<j}}/5*6%"""""R52q30ԝn̞~so3,~k{6JZχ+&n%M\6;s1"y'z&0XqOI]AF,mbvQK I^l6l6+Vi/$ 3e#7L)wJ?#x{ۈ17mA3I<~d}^;]u8:+S 3nh)A{ECҢYj%(A3D";&9q=}sߟsrrs_N{|Q7pv֣.ZԍFҖ3n5~ q8}92}*ʎ]΃Y0o1Un}?G3X]}E`eC![:zS޽7ȳ:IkGn1Wֺߓ=|S(QؾYG rs Di ߑMv`+3< :mԯ{X,X0nXi=f>ΗG00N7]wS7SAĝWwlB}]Wn~{qS󏿮 NN 0wz? lіc:1Ùf| 6pxx2mw6ǯS_WP<9$CBS}n=ZSm?3(QddgD9L.-iwO^NNb#'\K EbY^/}xLAtn mh1g*oAXTA6VÖl3nV%T^3F;w~Š>hFocέ;ypfgJW>ƵyX\&0r27r \e cި}ߕRӾj4#zȿm&}gºrUrV2j?6 'Y3xO z`(l̦֨Yْ Ƕ3XWqO<:Q_rӽW|Rؘ=̟ 2or|k"],@ʑ| rwת1~d6툦V %""""""T-W%`YNcUwM A:0qz96J\%[4γ_rF3WDxOРBy£3dVoL{A@uul}UcizG` ,OH"##D_aT"s|5%:&zgRRÌaOaWS/2< >[p鋴29u֔'|6T µC3Š9sk7;3/zW&|jx?43}΋ghb`IYNjwqO"73W?›Ctt#M?>Dp|)cplYĒ6to=G4rHcC0w\z}A}tƍX6msx/';>K~ܢ85=AgWQeC ϶Ѭ~ͱtRZju}l6L-(ɓڵ?7vX2S׹e;p˸ߒW/wwn@Wa2s;wqԣ.""""""Ax%"""""""DQ.""""""Ax%"""""""DQ.""""""Ax%"""""""D[tCD]תU+w """""%HCEDDDDDD<uD]DDDDDDă(QHVyrl%"""""""i۶mtСTUEJIpBw!"""""`ذa,[TuRҪUCj*&NHTTCzrk`#dduV!мys:teJ-I%"j5qi輈wtRL&ȹ(Qm۶-\0J"""D]ӦeŊ#}tƍaAfv4{҈VDDDDDIt4Tf0vҚͤD]LscthaRgqTHM5:.tJ9"dŊ#+IDKfK[H)II)@DDDDDTV{]CE<ݧns)fTuvqJEDDDDݥs%"""""""DQ.""""""Ax%"""""""DQ.""""""Ax%"""""""DD'11t <GS7T"^C="t!v8o0ҘrG]ھHdD  9s|QrOuqqwZ?r4bQ}S9@ TyĒ" Y\3: %zrN4zn%\Jb|ѳ ]FnyrQ~ZO,%??Gf;^KMEUbzT{axY *U\;)Q.wz~ڏd-z݇Sk2fg0Uŵ-/PfEANV[C˿b%2|"(s'u׏"r,FI_E-cг7qpԻ,:3kޚՈ vcҏqs9z74+)ſ?J.atyRvz3LIwHEŝŝ\|ȻH|XͻOͣOҺq,7[xOa;t׽ O]xrԊD4!o׼IJB;<1 !{b$;6".&ȚF {3s/PR,m yǏDJw0<>??IՈB|{rm&"}{]E{o~5+ESRm|v6}oׇ9O|TxoI! 7 '{5ߦC#,*LΪ߼+UـwXrx#̱y#<6: g_iӿL⑎5鹧z lQ b4NzrcŕwCkQtJ_άyz[_fmko߄*q2oeFlwf݇Yq|䀑>;ٓm<Ǜe~z/u&aʏi76`?_#5uӇ//ToOl|={^]˻],~_͎b7(p2;93OE Ԕy2 |a>8;A̬{+SعCjO333)|D*mFr_L-˗`ڴigg(?3ykvK|i#k/vg@Ռ~؟_S9a#iV]^Xم,}) {4onLLX6thK9.w]~:6IJ %LHY` WPMiC9YsƱP.Ϭcٕ)s93Í/l疑O8"Mis0j֯N9XΧ_=r^ثSP">̮[&͕1tiRDtD̽?bI{zc` mؽ AwOPd݉0ӧ}8~T02aYZj+`F]iɕvSc/浀ah8oސk/S"\ĹJw:̂SCz7?쉋sm͘BZ3WyL 0pRu o[rXu՚3bUQ)#gϞY.oyt\>9w=ɄOk9:ؕ~aп|xzؗuQ盩W}؃Ѡf8&T߼t%.?'93D=0RUxIR.Rgˋi?+ b135aFE3wi7L6LKYSNl|tkd~ޗFW؏]$[q*rO{97/!c`zDx+,*4p.<MǏfӉHPf QN6+P ma¢*b;q~s*dwD8-C:-X,NG1cc? #˞x.˟cĢL Tػp9Ngyn̄t5FT$<{0?iIXbckmd81XUTP9Ѡ.qgg@ьk|s2+_mCHboc=6W.#t>3P L/hNC:.#edPNoYjw/#痾MiI5Ѣ[/ܾ_eҫWFDW%E="8gxz_?*Y"?a>y1E98_1>c9VjL㙴ÉsXqf{Xȋ,<+ܹ598Q )(4oaCFRn8՞O:L*CO鬺{Й*СK]SVg81rw׿ `!g_~OWg$|Y=Ӻ٣k_ychU2Wdp_z1,;dL|2ا]f787wk);j׮H*'tG'gM_59z4#oɋOľӪr'{'!h\PG!(uAQ uc/3·fCnA),(fn/uK܌^eopgc X֓8#?l 9{PtK+}!v6_^|mh8*EFSWȺ-m/]AJntl>yY`r <>0;Ny@{zYN=X'c=Q{&7կJ\!l;+|\kckCZPJbc/!R ?c2FS#Y+W$0ٜNZȼ[9ZJYӱܭu}ӷ>v=fёDFFMwu/u-f9r1x`w!rI;w.{}>Ozz:'v=m{at)'Oꫯ}ff͚uerN+tmɓ'ӵkoر."Sj\ [S]."wYJEģq'""[S]."wY.""""""Aԣ."M0"r)Ru:"rqwD]DB!@ӏ/`N%Lpb"""""DŜH˫Gت^sڕ)GM˖-OܢE?ɿaԯ_ݡJE6K7a;W/`2OJx%ZOJEDDDD2u Q:&»w6O2c}oOCbJEO.f){qy<DDDDDND]D3[c)C4G][9s,w'""""⥔x+j>ͣKV3cpT+_&s~dx%"ʱ&f;}{`dtOX"""""N֢L5“Z4>m;sːxׯ0DDDDDNҪ"R{WMJaW[űwgw0w(""""❔x-a^c?g)WuD>e0 C""""1x}m:g;>;I'-XµCrr2qqq%{`2FsEq3iDZTfƳݸ6JtqqqJEDDDD.zEUr~[6O]3Mbe1>/D][E1Pr&7bADDDD4.u vS3/O`#}`?Ch=ɉxx пaE7g pGUEDDD(Q6Wav^s{8𧂿 cx"V#aH4G]k{hWwdBNQV0 yc ǧ* `~:hwy}^?m^_ZķdO @\\\XDDDDbr"Rַj$<2Iת""""i)F!G6f#ZJEU/dd2*f=wС{$I.|Ϊ srHM&V;XߧREDDDDD΢D]Z6m\s"U|pz>Rk8ցg/3gobҫUϦ IDAT?a:2(ww"""""Q.^=Xчٜ$gfu|5a/o1l$UOpM5/YINDDDDiwj `4l+a6nd*9krG?lYMi1tX_S$]Q.^eCߝ9,_aM #0rZU+Ƿ8=\x ?E;pąS2=;w_Ƨ@Z]DDDD<u7H 6Sa|v~jʻRxl:1.""""JE\,yu1k3yĎ-,^Uń|ɺ?Qv6NoM 9-߈u;_9I))&Oѻ*z7\\k߼ jRph#ՎIcADDDDD)Qq%Ѐ8XDDDDD$R۳g_P?*/ߕg֠9~Xx*rV.YFvЪ""""i).9 v҉ $2Dhݭ M*30D]ąGڊ4G Gn>73o>Vql],EHxD ps&CU !""""rjze 'k^́,[ZTq9.HQ1۱WElزߏ:edHԵ껈x/hx#dzN݁P>{e{Nm.\v_+e0兹3*0C浦o)!"""""D\@\<Vst'f_ Z\Sr&QWEU/U{ ,)pa,nDbb9IKU2|/YwYSɞ.99=H"bU3gwŏrfNv9C [Kp wqs6_%{\\\PDDDDR.Nƪ:.9]zCe[CyX׶ ꆫrqE\ɤLrwg*LcT26Fm˪Bf8ۚqrԍx+~w$"""""FpoU%#sw0aԯ_ݡZ]{B)_-J~DDDDDDR.l>h.gudwwq%"*&#OFPNALvw"""""IsEoW]bnn#4!V}OD]k^ٿ9xŠh)Qj96qgN*nY} hw4JEMӰY|;<*wijsw|"""""Iɉx•y[#+}.u_gو\2bT;"ʙASk,ɌiaBw(""""❔x+K5bo)daIÂi.RF."""EF樋x+k؎sthה)i9깻Y11{!0;"UEDDDDbG]kvkR~J6<>y`,AjZv,[v vc¥lwU%'))Dwx(}6|C.uJER>M2g{>&A>-8,;II nf~!x(}6|CGKudh [7玟aP@wyHG02txb'?BCmdpk)4G]DzL=8<.Z8ONZ3 8>҃8NzZbY1r|O_cѢE\!gCG9}>RD]8v9=:cwփStcXvE[`-6I{K(xOCEae:!ҘG.,ILX29}b{G2  *6Q D]D.5eqnrN%peb+=DDDDDh軈7reY*R `I&Dm6bZu,ۡ35uuLױ<l~ŶpF4+@EDDDDuoۓ18DDDDDXJE9q1 Vl#Їtj_s2ymKtA,Tmڑ*r҅6/͠BLv\<ͨ(rwbbfC` wEy~tNr]KuTmײ5t_.m:_FlP1K,y_evp(ׅAQ]`;eY,uc+R]^I ciV}9/%"R*Y"=oAp6Ngڊ\,\+{͋`etY4m &hkAރkwa5dld$DHjj]X!pM4˙`)oBoq;9~ [0sur}s>^ӏkf2k&*tGӾHybp`l`).ܓ@:.Ɯ?+1K,Z׻8 .%PAR];ɮڈp&k(uU'wvҍ~%q Qׄ\,kؾu?ѠՊlG ʹ)nq ~RR8&ćbkja;$xs0MCyvk8.ɇ up+)9ghOe]&dLmpc,O35GCd\vI$B"r18JPXJFpa8D)kY9Ghȉ~ 9BgO@z0"Qyd: X-v hcdu8mu nA|fϻ!ΰEPa$WxZF%6\$"fawjl8LVeL)!VV;vǩYXjZBf8:NѼQj6ѪuU{ GχÊN(_5Ca72Zٗ>y;1c f43a-dk`x8-&'"DHX0Ge#gF:GѴAb",$P0Hh:6gGQE2| Yj7y 22wp\詵D<#H0ӡaOf5S :>m>s\S usdM* 1rG ~皣3j$Fe#U'j._:S;ʁݻeP7"#,,>LV_lS2sM:5?;q\޹>&p䤱{W.PѯOzz:'v=m{++VM6\"""""rKҪUX,6zq&OL׮]qcB'Q.""""""Ax%"""""""DQ.""""""Ax%"""""""DQ.""""""Ax%"""R2V_B*7#٘[§s+t> /ǗO,!҉Q%xŝ JEKWU/f)DD%"""RFX{x!G#// ^Z_0U&a*ݡ\3/86}Lk$Yk*)"rAT]He%N/n&6GWQj5EPǙ@} <=5k\N|\32 }v3ͫPQkyh;dɓxաV:\~HV1 BWsE8j~Ic}}/o  a%v;i.)+_(Mhe_S."""em#Bh(CMjaB'~go XԱ.GSvi&e!:igA'G-`M՞kx ״Lu b0бL`LZ!Tԃ&R'}ig ?ɧ>?#o8(e HQ*""" nzMbKK㭁CXPf/[_0ja!84H?68H$5<)d(5VOɵ??b=aMrNf.iOlo;|p¼Lqkپ}{aÁPM0DD)QKWM~eq֚pPjT#;g-deu6ykl}?}GW[_5 11|tNbhv:Xz,^k9Gad2ɳ1 Hz>MWk>dMny`2;q""rud\{aji]O_ˎР9]HWݶ β:ք!}*?^WJ M[Cnhnk|>mH5^Z0oF&AkF3#l;vcuKU<9[ij{p)ub ڎ݌ktܘU0UMǿⵄG|xZ?;R=I;М ??;E_HGnW/"r6رc:Ra^;t}4&O|3dZl%"""""gJLLyg>uTQ}L:Ν;u'NԈ;Q.""""""F%"""""""nDQ.""""""F%"""""""nDQ.""""""F%"""""""nDQ.""""""F]3_ƓKs~e}nQo2I_7/GHP8U; O|G az}]Kx{@s*GO!`=T2NϷrvy!>Ɖ=ͪәɳyDY9 `e|gR~l/s̶ycShB)ߞ{|\C,{mEZA?wkur ?"ο9/mo2ǯm@lx(ax}h?gXT.=^A?{1yQWUr|Ve8Úc_?AO's{;=1kE0p>-@6&_]p 5ndPPP@AA& l=f0c1W*Lʵ1@#˞ЊNJڿrW=CC/tfetà V m,÷DId6=K[uOnNx&{|t#pkO~ P=p-)MaimO՗g =f7đrx兗`vzMFh-#'+ IDATiR ׫}V ѳr0x>gqy1}nnօho/"; ΄Y|37)H|8G`Npuxݦ+-;9syk/'T,CJҏd1cp5éP*}\xj/Lj$yN#d-&1-Mw#\s}γ^Ӽ}̭؛խ2d&իW+Nۉ qL>oaxNLk8!mjSJ-oDz">Y I ЎV^.GoĎdgϱ[# {YdecO_Y&ޭ^d-$I!ydn{>~!|n&F]#P~4 7+E$v mGNfߵy3I[o^'{ :g( ˕p۩}ٵv>MI,߈as_+3c>#閭I'{aM҆ }_SO1㹇ճӠxjTƤ[ JEDDDD#YdstVA@?9Glr.,99ys 4?˅kOdxF籺 _C鏱dK֌%n\?vb0c}16-!DhK q]I[H@n69X\10(G7~i;!]MgMΟ7XaXs~x`Ԫ4s1gF.xO𖃸1a9 WZa3X>Dv~`80+\褵YӇ;o_Ko^tYnEx$# LL/׊c-w+/زk]ȧzM*m@ұ7g3TFsv;?k/:*UsOt`?}s6Ll6;>Qif7Y>Il[XyMPF3cwu>Ʊnb2Y0lE1*VB>n_OVs,y=؍w7-6qb؟~ GA>6% #7Mi΄)4dm{ƹݍuHը-Y*tѺ,̜M|wN-/{Œlӓk&sdr;̯y Wk&m1noyb2wj2L`s/ƷZu-e+Lp-odl?"Ǒw$l,xִvA8v,9͜/3JUL?@R'm8`vRr@>E4kR}6Ʈ=?pEc_y^ŪUX nZ{X̸+Y7f$wa;wߙCN툱[= ~$+v4%q{WU."""""▌hy޻7FWdձk;>ɣMlV۬ؑcw#x- 5?worC i7èd}坍T\3gK~U쐿O_C@",3|ÿ|Z3Ivsçs]qk/Q刎>ZʅMpD9[(-_#/"rO4O`1†cJ:y淤}sn7`;0`)fϞM=.CIKK`Χ/(81]d -[\Ŋ/{~qw/+l٭v&,EYI>F4㖼x%~D7o~qoS/ZQ}L:Ν;u'NԈx* Un-[_c̲ 3I]~h1I:8vM ׈ƿ-^( i0R6~\}ø`Qq#JEDDDDDD܈u7D]DDDDDDč(Qq#JEDDDDDD܈u7D]DDDDDDč(Qq#JEDDDDDD܈u7DKLLtu"""""RLr͛7wu"""""R4]DDDDDDč(Qq#JEDDDDDD܈u7V©?T"""""rz<6dΣDXlɞ=ǃY4Hɺ;P,z)!!!ʕK_DDDDD*U*͛:&NtвuwӪ_8JnM})!\ҥIO/Wu̟og+;wwg"""""rbc.O\Ža=ueгgyz,1 K{1E%"""""NSEDDD.Bbb"F(QZ*\4"""gD]l&ӧcɒdd\friiiLܥ$BQFnݚロjժy1{TGA-^DDDDDʕ+ٶm-Z୷b%r%"%dɒîADDDDDQFhԨUTo߾%k39rEDDDDthܸ1 w_ԯD]DDDDDD"hтDV.""""""rʗ/޽{Kn%"""""""nDQ.""""""F%"""""""nDQ.""""""F%"""""""nڵkIKK#%%ա\é]CqjE4qv\kbh۶C(+V`ݺuJQ[."qM}FF\Ekܸ1--FEm`W4("RZW."nM;OmQ."nO<)m punGm&nǵF]DDDDDDčhD]D-4313M%0Ob`.M o)|z]m~y;_hD z}yo4rAOmy1Q[.14.Rmzq'QxLjſ/ jLgVa7ù"5űb닟b'>bTP(STGj-w!9|{Sb\#=_';\{JJEJ; {7kG>+x9txQZ9#.œ~$7OLZX8c3:ԼLO{``μ9ktPW(G*4n wpjWvfyc&&ބuমڲ$sXO|l4r[7Mܟͼ>ajW\8Zs vzL9bkwf،X?)ҽAec*R>^z_b(W,OӮV%pňY$;侀y':%,{1Zv|ڶUƂqn,d/>MR\%}?S ;a՛&qP1^AH1}U"*S?O`-6 ,KJJ ӦM;k9v뱢3|G;:~7\C4[3S ϕ/ CӹFc_EV} îErDWMH7mݒnUμ>jݪ-FrE|0ӋW,pS.TQYxqS.r q^8{ʷu_!E0wG^-_ ~4L#ύょy~A:&?΋{kq_F^5+Ş躴iӐqc.OKcrXs7]O™fڏ| >[>'yLΖ0|U;>_Kߥњ_P6->͞=18<#_i|/߯æ=uܱ Ŏ?|Mq ~v_Eay?k#{vΣqse̚Y&<132g'Sl8+N ioz ,=-[ӧQN~?3bRƎa?ʬf(C:U2@dYF"4(Ftu˷|xsy ĨЂN BDGj>&F2bwdkhvP_~w%01MLcWǻA0~TALEyGV[iiuCdvYaU~^!G!'>;E<\Ĺ&nE+Oez ϼ_YwYPhnmA22LLV lZsZ**H޽;ݻw?m|/6o},1/ڧ5xcg6L݅Gy5<֯,̘Zt-93e~l[- {g\AD $E^'C[-bƵp볷R**QՎ'%"5}ߙeϿ"^c/boR/&0sL~`P]`$E^M/za/׾X&GũȆӈq(e%/_M:e 4K mڶUϱQ8]KYzbp80i$&:xGR60M;)ɩ-Df[Xo8aBhȱ)VVѹe' Ӱ 'p4"ύ2e0}ƈ(Vp}sYD3W㶒}bi\BXΝiIޘ{.<$>ߒ%;NmFzi~t7Eېӳn+Ն~Իr[j +K|BLHm{^Mh寱n,{**UҎ#ݞMԳ4!>$jx{)S&jy'/}N#ٿ ㊯gZo}o2jv/v .10A<27@ՒߦibU+6A?hۼ #_ӷu(qt5NBlazct`-N&V~&2‡{ˢsG)<ґ'kLPa=SIs4BsQvr HPH8a0"ÃUL|!_s2OQ[Ҧ{$$$Pb8kH$,a90n`cW0 7V ӟGYKn<}%F;֗iE`PmyiiK I_7=d8/.){7zSMQCLH:yS&]O%6hI=&2la7c`Hgڀ\=8"">U /0meiQ#%,<_G^f*iJNJ;`܃ΈmHw`lgʧ?߽8eoMf7oQ gy<^.MVb*4gv덞lz o'>J~.4sxefd5鳉\:\wr3_Σv_X[^^Ǒobeɡѕ=;~y:8+f"]E<5;_+Ob+ٷ =VW(""""♔x,2^M72D6/u@(W'""""ⱔxuaa>;I#>͈R """"Z.̼]Nk9V#ʺFt",%%%T\4f& 5x} U8y;_- qo.>>*iD]SFP̀K.KDDDD)Q8&јoY09dXϬ&p`Zk?9wO7*"7,xduWpE"""""M񽒷nEw3M"D]Q2CDDDDD5"+_Ij߱֏o\g҈_yN8>'n ֧ufM-euu$"""""rx*˹&>9WG#"""""x,/pgSbqkQq M}TLL^-Q7uP. W$hwOݒۯ''=-%Zq %"*o뷷".<EQqM}Tyd1|#LkEDDDD\Bre"v33ӼGK*i)QT >۳mI6w1uWP.|:2:q%O9 uh }]fr"r -n3xkNJJ"))x+iD]DN0 8q 6ۈ+㋹FKFEY6'Ӿo٧%ꆁ˛X _|O@'$""""")Qqpz^$/,\`e`d2m~pB m9F~Օݻ+i껈3v`fc';yݰuG*>z!OKfꇺd8_DDDDDND]٬Ǜ^Y83?MӖ28dwB,Ռj֐{ȭM±Lb^c ":. """""ND])$>8LOlg8%]NK.nLdܝ]Wkᣭ_+ 񈈈)8Ǿ좟+{?:)L؝'NDDDDD.6q#JE8˺oϊքx>ŁLP?7-G񄾱8!UoaV tކ|u"Id?j땨"`xѤk_6w`;ǾGؖH0սE<=r ?>=WW(܅>Sd4-8DDDDDH=3X˲E&?S_%JV'ƔoVϕNU/NXDDDDD8%"NaFFfϚGI"Й/>SgT/|=]RRIII[%HSE"Z0 gՏ Q9y_]ӥM,פri;ٰ)Jb=>>x+D)Qq yJ4?K2u83&oj5 W-b΂q(kHjDۚjDDDDD\D}qgeݨ|8NW¶{Ȱ{iu.BǛ;SU-ө.|ov5#;y}O;ܨD]DDDD Teu4.AFDDDDDѮ"l:9uOݜ;;f19%3(QTy35={ 5>Q"39x&QTyd)aIDDDDDx0q<]R8uf#v0pYk,疿@JE<}y6vrMo񉈈x&m&' 2an̴?^Ƴx&%"Z8?2{L+F.F""""r ?~C8/QT^u<5Wv'UWeѶu#?ƈz59Y2˂Hآvue!wXn-M݇g0{px/լzo>c{hkvږaD9+v,[!φ~>\!uEÉ-F;֦Ssv{2;PA, R;t[haHJJru!碟9|ȹQ.ql7M:ë<ǀMm~L35nlRגo37 Ga^GX7iG0DDDDD.]Z.ilktv=u/C;hObn^^4 Vo+&PT_6YtC7 9|ȹCJ3%"ƾ}M!(Z _eA_*I'f+ٙ?&-;^F\p[,$YĢs(;W%fN⺽d,GͩLgK@Z︔d8B 6^[oolg͵ٷ,]}GWUmޖJ%( Z((X"VdDEGAtT3(Q"  @zB$0r g'Y9kmJs8[\gYpKvdo_h+&^ K+V{B#?ɨC{Ύ59|%JKr DCDVLNf4t$"W ͊<{p4YYNcbjup].ՆT;HڇFrUՊdrڝXmsJۍ$V%U#M}ӯN+wXrF3nj u#2ӵ^uU80u 7{~pqq7L sfr"r'x.N2 "@ ^[~;Avf6xɉm ,I &M҂Gg$<~&̬bCJu9/Baffǀ[ֲCd/'l2,2sST͟jEzqt1pV GSt a& Ʒ@{9$BIHܘD*0[')LvnًAsnҷΑ"Wn*[vR/<3ذ5)3v5vv;vVĪ4sm5c\4 (/[Ɏ ޻4h t ^Uz#M{ؒ(&}V5 WNV.QkDc?F$'m B*b{ײt 1K4NS8^-Dܔ#Ua򿎎mN˂Y[dƿAo vtH/CɗХ  -1\.\f~_UbTQ6Fw̞;f(xF4 »DÂꉷM)U&:1c[vi _@jpc -uѲK>O g(pzL&ujSDBr)"""UxG$Q9Ҹ~եY @W%=;]r]f, -#~]"ohC1ߐZ"'oD6jJ!$f@1Kףo1a_:FmUyjtcˉM}x >5eDel0-9ΚRoDŗ1vކwM3פa1=o(QhސƑ33psyW ڼĆRgDLbd14R""(udmÇ#C^8IƳ}riikxSF@!`ZqY@Ji6}7?LtW<>n;=I5|w7OCt~۽?pD~a9YMe?3c?r <<=p[Ⱦ{H[2Xϝ9Cl)H$%; LJ:oX|r/z<kcl0a/ hڂsf)|35c0%,҂{ш%?ԊG Ci@h^oKCFX8v83O.K­qCYɶm̟J᣸% Xrd6aэ=B3HaF[l8@Z#o#syŏX8"3&M~01p "9A'pesԬqv#'pn-+^%6c%W8ۆy^^&lᛛE HxFŸ?ݫ|Ow5+F/"""R u='RG)$_׮fcU`Œ_deuzAu#N굨q?]|"W0n;?̤MLgsAA^݉($#W'ZZ~~rrjf9= EDDj6)Cq?[x 0C|vIDATnyy䕓lbl?K;yŸ9;}Zmg @a2^iykM]y.zM֝69N> 0p2Y$?~J|wu[e>fkv,z!c ~B."""UOg4 Ss4_KoɍNz1N{'3\X F׈M[Ҋ;{t:҄}VMlї&2.H<&jĤ9=FčsjnA'Ѣ8:ńnQ^ հ/y]ߐO wt^GN~5j^^uʞSED.oU 7{ՙpߢdJ/pV#ᵥep4Bʄ>Rn?òMi%bͪ\'r$7S`34@S rD9ْl*RL^Z0rBo[j=QSZO=1'>wɢҽyOL Yde oa6 *ӏ4)4]=7̣}$gyoB?XV;z6`ʾm}6%s$O$WGRXh;ak^d5SV \_Y3.m} e>4k|rEE6'M>x;+v84= |;aE|8GF-3sBǏˇI9L{ xe]~_.| oo kǫ!Iǣ>݃= aۧ&xm3sG^Nx7oO @x3[6~60yɧSe.gل|B{6w:]W_sS:GcRX޺;78b>&yO爉fBB+T ?_eЭwȵw~.y` 㱟xV7dߖ`ߝDZfgKYv&AM{2bP#>XMp{ K. ;Ҙ֜)]_iy0xD6~8 $`8#JNɮº0 39^ٞC(soG72h mq$` nyDujM67Y^Ju&Y4ޑ]+k^\awݳ7ͭ.}^Iqv]" ‡_my^;/ň;CΖ;1d_^v=×:㗟Knn:_BF~4ըp{R[*b}sxT\{,kևC̐#$ɠɨHݳM6i&Vܑ^v *INmK$oMڽUcw YLiH\r ϻy c6gb)tc;SÈl."""""L5)b=K䗹pNǬuݑ` 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) Set source_current and measure_voltage parameters 4) Connect to the Keithley 2400 5) Set up the instrument for the IV characteristic 6) Allocate arrays to store the resulting measurements 7) Loop through the current points, measure the voltage, and record 8) Save the final data to a CSV file 9) 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 = 10 max_current = 0.001 min_current = -max_current # Set source_current and measure_voltage parameters current_range = 10e-3 # in Amps compliance_voltage = 10 # in Volts measure_nplc = 0.1 # Number of power line cycles voltage_range = 1 # in VOlts # Connect and configure the instrument sourcemeter = Keithley2400("GPIB::24") sourcemeter.reset() sourcemeter.use_front_terminals() sourcemeter.apply_current(current_range, compliance_voltage) sourcemeter.measure_voltage(measure_nplc, voltage_range) sleep(0.1) # wait here to give the instrument time to react sourcemeter.stop_buffer() sourcemeter.disable_buffer() # 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) sourcemeter.enable_source() # Loop through each current point, measure and record the voltage for i in range(data_points): sourcemeter.config_buffer(averages) sourcemeter.source_current = currents[i] sourcemeter.start_buffer() sourcemeter.wait_for_buffer() # Record the average and standard deviation voltages[i] = sourcemeter.means[0] sleep(1.0) voltage_stds[i] = sourcemeter.standard_devs[0] # 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): """Execute the procedure. 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): """Execute the procedure. 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. Storing metadata ~~~~~~~~~~~~~~~~ Metadata (:class:`pymeasure.experiment.parameters.Metadata`) allows storing information (e.g. the actual starting time, instrument parameters) about the measurement in the header of the datafile. These Metadata objects are evaluated and stored in the datafile only after the :python:`startup` method has ran; this way it is possible to e.g. retrieve settings from an instrument and store them in the file. Using a Metadata is nearly as straightforward as using a Parameter; extending the example of above to include metadata, looks as follows: :: from time import sleep, time from pymeasure.experiment import Procedure from pymeasure.experiment import IntegerParameter, Metadata class SimpleProcedure(Procedure): # a Parameter that defines the number of loop iterations iterations = IntegerParameter('Loop Iterations') # the Metadata objects store information after the startup has ran starttime = Metadata('Start time', fget=time) custom_metadata = Metadata('Custom', default=1) # a list defining the order and appearance of columns in our data file DATA_COLUMNS = ['Iteration'] def startup(self): self.custom_metadata = 20 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 As with a Parameter, PyMeasure swaps out the Metadata with their values behind the scene, which makes accessing the values of Metadata very convenient. The value of a Metadata can be set either using an :python:`fget` method or manually in the startup method. The :python:`fget` method, if provided, is ran after startup method. It can also be provided as a string; in that case it is assumed that the string contains the name of an attribute (either a callable or not) of the Procedure class which returns the value that is to be stored. This also allows to retrieve nested attributes (e.g. in order to store a property or method of an instrument) by separating the attributes with a period: e.g. `instrument_name.attribute_name` (or even `instrument_name.subclass_name.attribute_name`); note that here only the final element (i.e. `attribute_name` in the example) is allowed to refer to a callable. If neither an :python:`fget` method is provided or a value manually set, the Metadata will return to its default value, if set. The formatting of the value of the Metadata-object can be controlled using the `fmt` argument. 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, Results, Worker from pymeasure.experiment import IntegerParameter, FloatParameter from time import sleep import numpy as np from pymeasure.log import log, console_log class IVProcedure(Procedure): data_points = IntegerParameter('Data points', default=20) averages = IntegerParameter('Averages', default=8) max_current = FloatParameter('Maximum Current', units='A', default=0.001) min_current = FloatParameter('Minimum Current', units='A', default=-0.001) DATA_COLUMNS = ['Current (A)', 'Voltage (V)', 'Voltage Std (V)'] def startup(self): log.info("Connecting and configuring the instrument") self.sourcemeter = Keithley2400("GPIB::24") self.sourcemeter.reset() self.sourcemeter.use_front_terminals() self.sourcemeter.apply_current(100e-3, 10.0) # current_range = 100e-3, compliance_voltage = 10.0 self.sourcemeter.measure_voltage(0.01, 1.0) # nplc = 0.01, voltage_range = 1.0 sleep(0.1) # wait here to give the instrument time to react self.sourcemeter.stop_buffer() self.sourcemeter.disable_buffer() def execute(self): currents = np.linspace( self.min_current, self.max_current, num=self.data_points ) self.sourcemeter.enable_source() # Loop through each current point, measure and record the voltage for current in currents: self.sourcemeter.config_buffer(IVProcedure.averages.value) log.info("Setting the current to %g A" % current) self.sourcemeter.source_current = current self.sourcemeter.start_buffer() log.info("Waiting for the buffer to fill with measurements") self.sourcemeter.wait_for_buffer() data = { 'Current (A)': current, 'Voltage (V)': self.sourcemeter.means[0], 'Voltage Std (V)': self.sourcemeter.standard_devs[0] } self.emit('results', data) 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 = 20 procedure.averages = 8 procedure.max_current = -0.001 procedure.min_current = 0.001 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") The parentheses in the :code:`COLUMN` entries indicate the physical unit of the data in the corresponding column, e.g. :code:`'Voltage Std (V)'` indicates Volts. If you want to indicate a dimensionless value, e.g. Mach number, you can use `(1)` instead. Combined units like `(m/s)` or the long form `(meter/second)` are also possible. The class :class:`Results` ensures, that the data is stored in the correct unit, here Volts. For example a :python:`pint.Quantity` of 500 mV will be stored as 0.5 V. A string will be converted first to a `Quantity` and a mere number (e.g. float, int, ...) is assumed to be already in the right unit (e.g 5 will be stored as 5 V). If the data entry is not compatible, either because it has the wrong unit, e.g. meters which is not a unit of voltage, or because it is no number at all, a warning is logged and `'nan'` will be stored in the file. If you do not specify a unit (i.e. no parentheses), no unit check is performed for this column, unless the data entry is a `Quantity` for that column. In this case, this column's unit is set to the base unit (e.g. meter if unit of the data entry is kilometers) of the data entry. From this point on, unit checks are enabled for this column. Also use columns without unit checks (i.e. without parentheses) for strings or booleans. 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/pymeasure-estimator.png0000644000175100001770000001236114623331163022540 0ustar00runnerdockerPNG  IHDR;nsRGBgAMA a pHYsodIDATx^kGƟ72I/ٲwƚwέD2`,'- XFyc0k$wckR(]UuuOMF1S|3noo@!)/! Ņb=|i!a}رczBի0-"XBŅb ! Ⲃ+ 2reE896!PR%ry_J(j.  W za|BL!VH(.[᜙*ɨbzEʝHCT^mpk\)]n߾@^9ߕ++b_TݡUu寸5R\-✛BNo1po4/ӆ~~\u-6}/*}! אu _4jMQ:!ۿLbc.=wVD|{jc%Q%v[LF+lGǮr_=kmO7Ej$/ӑ(0Ĭ/儐I] z 9R{GoUiȈ?]a=#!41\d{-?=X%o+b/r߃jŹLWi6hBHWф7ŅbDGILtbE+P\!VB@q!XBŅb ! B(.+P\!VI FÉ'Z|(.$SN-ryŅ+..!wynq ! B(.+P\!V%.}G zGlB_-l !}ׯ^n?;JXʗ֛%^==em ƥcCoBb˸|Y.Ch[/@қi)07D4mV^F:yHڼgD?}*56O'%2;m;:# @Fmo /9-Nmi\v"뷟7B}c.'{_f(B0e3^1ȉ|2M BoQBHIkyTîxvF0'EC] t"v%֠tjnf-(>`o@W%*.f$2<7EasE[vYtUBH KBZ>=-HA z<&30/JEu,͚HaZ8DZEQ $Mc^0ev[Ot2PQI0[eBE<qVS,vMBeNU_Wi_X28+VAq-'KRdFqm;)EE:7ȴ39#Qo"Wn72Y/S)Yk2yuBB%yy[):HŋOã:IpB"ENER\?BH1B(.+P\!VB@q!XOH>&~(DGL%:"q !MŅb ! #."_ 7y~B)E 9Y@~)=1x`L y+)aƒ(y1<E.#-i#t)@',H2/hۑ1fZť nytdu]븩y*3ucǸp&>T\ocbA2mBqy "y1#c#s<ۧ>-";]wuHt+GE`rLKAXDŽ6RFXT~c(wFAusܫ:9F\1Hs5' iœ%Q h@Uq}+v #(T# 4;8TsgLBcH_%UDI~#$S4 H[^z?j4#΋H&. ȱ M\*x>Xtg5LuAҔD9/pdl:q1 i]ƮAĕNj9XZyL#D] 3#'E^k:26Hą?\$4B,p(BB@q!XBŅb ! |ZDQ4G$%:b/ё Hi:(.+P\!V8<ڈԱ>!xRs_:/> B'(bAEuN$>>E3IX;;L,E&W?A\w. C,X ?';}'/%B}zf I }z__wD œ/9k 9j:޻8b,TKaQM8m_3-ʢXSnN"7( \'; vyK"'ļcgv.U/|S/H4w]sSTU☤=u+zj˝[vrsp׽= xDv%,pRd;ѶtCh9}ѱ.Q~jkiuCQPbg|ȜDνw^n̨ɺc@n& }8Kg ; C?6ü.հq1B`nX=$.>7q3p}{sQzL:t$uzO@'NY"i}kH?`gV&>#%aŽocǻY):v-0ocսvT "nc*F_ؔAN0@s18ӡ.U=.EjmEw1Q+QzWQRN?rZ63sOZ4.{``/`(Hz2SV|:XV@;q ;UzK]F͈,u4#`UXBEx >Pybl?HGIxrJD*!-Yt&|jH\M^Ѝ âxU/;_i8zu$uKt8&È^U^v|^w4Ը_(28+;,.I T /XI?fz&r`yU'Wkحd1A a6r #^^qLQxδ^>+3,`EDRӶo%..ܮrްQ֮b^GuQO*ژP;۝e'c.kϼ!n{P5|.<=|H{#FUp)+UٱmGYm!;0C,ӽ]v%n/RӶaZ?* CL qe}|as#h&URﶰ%;2ĿI!(9]E~Y|RDS_aE/|J [>oqq1 ۏRvfgwqvsNa^O$7 inf"_G}|ay#Kr8 ^籎W B(C]nbF$#wWAx-]dһ7;"%ըkj֮/r\v#>Yq#rD@G1^50bi!*QOHh(;S8HmQJbqjy e*}(N]}d9z gw#xx=&?=]s1ZTKDk %.rC[ʥkqk-/:kzD-Fǰ_uK:B jp3 wkpᶳ獪Ugw7s_Zɇc=:EKσO2=9gBt1rMBGnd-22N B$1j wbD\0"roȅtP\!V`ZD5u MRoZDq!Xi! B(.+P\!VB@q!XB19x IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/pymeasure-fileinput.png0000644000175100001770000000616314623331163022533 0ustar00runnerdockerPNG  IHDR*RsRGBgAMA a pHYsod IDATx^{LTLEtv=ڔ %並㫂՞@:RZL;3C5mt7Kfw [DFHL}=G]vd}ΕJi ؁ƮjU5+QKi-}$rZU m:;Ǝmuu5Ul?tؚy:E]a%-ygSGWl*Jyݡrfgr_~]ELb ߼QFw!r9,qy Y?R UakZƩbю<fk0H҃%{jԳ:-YCWKvfʟ>(6%El8V"E+-nDK{5LG5rB5T\.-fja6kD# 5 ںxFtt-qa݈W{T=w*]ȉm !X #"FR +!t-v!C.с D[}W>WYVƙvE}_ikJq勑+wGDBG_vMh-l8"{kB>e!H@ !!222Eߟue56olJ/Ɓ|nnnx<1L" 㙚 !2C $ @B $ @B $ @B $ >mڻwH088iӦtv}i`N:xDZFSv>@B $ @B $4QA$׶ #Dh]>R&s3$h2rjӀzbJI5U NN }6Cm"# l l刁C/kҪfQ%4M,?|(>o;ho=X!Shl ppǢŏ=J5 &o}Bk1@5>l$z`F GgE,ll„h#jKjzF[KWmNC(Qڒ4`#H/采ۍ# ګ_kzB $ @B) ~$0ib2b2o]X#9&?Iɳ?j5YRHl5YRn׋4edx/.! ?P`NXR,H C1:;;)wknOL`DV5\h=,R_ P19?5*")yr3y{Usld]X>/}CMy;Yyb¢bN~W㝌,Zp]O^]WAa@($ zgrmۚ)7+=wKJѾ4ϳELVjP>ɉ̧F7R7]!wq{=foL4> DR ^{vͺ3)QhG")qN Z, 8ScϜS3Nm}!j/f9#3s3Sn5|z d֒uA7")ӰԀl)A7"X\\ޣϔU2b,]j>caaŋ."W.j+H k|uύ ?t*H@ !H@ !H@ !H@ !H@ !H@ !H~rrc̜s>:S Lu=C,!\ѱDnBN;ݾsN=N^J` n;555Fub]f:}۷o/((0 Ղ&(扬1ƨ)T 2Chĺ.+;;[$ A7HĄ" L"aȉ bkFj?dɊ52B[^#3֭"pQQ|V9q}!.^\oСx A6ZkT>/kNL>Q˕' "w5([Jk{?i"$<#ݩ$u?To~[ã)%҈B(%eɉ£GJ9eFs0j!DbFI^v5"WFO|Pŋv+FqוplPvw)w{#ivgS&9`>~D;BHZ|WQ|!565[N*l'j-; "˨ LX{Vcx̸xI$(%ﱾϡˑ?n:944TNKj'{Ϟ="5ޟrii3ſSY|BYKL]8X'`BD 52L^L$+(::DĘ;d $"a|>_?xB. JdvIfbZ6=cF҅7onذ!;;" gΜlF҅}\.nh Y`  "@P @A (D@S/lSB(O#B" ]L !a&:n(:[OV07j`"eRuC۸+{@`CO,}x=C6.% JDpmNGiЮYfow OG1!Tz~Oإ(O.5AR{+9ÃD t?.C zZusZzxf }i*BC2B[f_}v{olؗ}iܫ-˶W.n_9y}4\qDe"d0M ӥ%EE%J K6fTUdn=ש,^Gq1}_?MtLEв J9QYQ`=.ǝz\<^Ezk\--j ||#'^jmPZzZjL^b:M $Ò{r].I -tky*ʟ f׋\" gvzɨmSmQ!x?70?t,l1d ~%حY`INCXPK֞{5APkN."r,̳\ٻv=@ZKVCnVuO.~i@ $@w {qbno~Hÿvk|:ZG&91ci[yݹsp2'xVTYa$NBґW)XЪ:17 6- ΏJg:,{ѷxoFpZv˲A&F2{ qt/ ~|csgϗXϗZϗCO8WC˵qWde'IA2fG)k[QKRyG ^MEx0a"kԔjMM,bD򶽓6>_([O:dZɒxmķF!Ąnz y!o{'gx4-,$7ARjd\ZZ ̨HBnk{H5St,, )XO }O?y7Pn,$co`:"`fPL.vfg(C;gxn]]xDad-+V<!` ,(hM:O>K^ yGJ.C&( T*lRjUFղo_h HaBPad+I!/D-LE,s 0?+ֆ_1Rᥢ@dadryq$%"d$I% CSr m4m,/w+dCye6m2Ͱ5V|۹-1>@A (DЭG *y IG-FGG-2o@RJp\Vubbn[9*dgg{(**Z @v܃Hb21lܸi)`>`!  t lA("_#)^A |0 8]Dwx<j?DKPS P% \LNNSSдjX,=Mfс'1%Á tfff8X < Qy<+&p==:j(@p8rFv8H@pp#&QbFŹ<8"PzpQ: Xsf.Jvڵk>(7q*Ҝmʚ5b"!a0 }tJ K?")'{Ds@ 1;;ˍ Fxb%e, Jf(, If1L  =Ԉ?rlljI(厩Lc5 D`2 "@P @A a?#)ڧIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/pymeasure-fileinput_complete_filename.png0000644000175100001770000001024014623331163026252 0ustar00runnerdockerPNG  IHDR*RsRGBgAMA a pHYsod5IDATx^klg'Ə@{b>!".T1J!2E *D$"dFPCIj@Q]#kϬgmi{ٙ{ٹ׻! Ad/PP @A (D aB>oz/˶Ew@AC?8{t8|o_$CF^; s.Rw=+i7Ot;Nu&J:c+yuНI\yH1o`oM@C'#Sl_/!2evGK{겴^ t=H;4x~W~|K!ch`P+} p|hīQu8lCko{\:eQ\l}[Zc{nMج~qת}AZF~p2qg}Zo{e2X?;hqUIl[P^!] S(ߺA ׯ6ѭ\2*g]'6j&7I\U?'ͨx2돿*WZnYVP6H5e$q ȃ:]{\Lēi׷`ø!c 5Usm>iD0<:!` S8qMoaG H&Ń@"m|u1 I#Np1 ٥NFN<&ٸy\s$7e:xݑCD !B O 4+I9O|km[!45Si6>MyAKn%/ L_o.pȩ}@A S"@P @A (D@>DvvH4e,XQkf4"͵ل D" nȈbS:~!"@ ԫ`o/O׫:yav÷N DTGuK˅I vwcׅUci _rup30&{|K}5YM?ng>jJꚩAՌV!M u)9ϝs.#EykDCzf*4lpoٰq[wask4~]WuZ:7qKG}6ͭɶ m;׸L`'#tcfo42]&D^ 6Ulst|[ˇ25HHn?Zr>X*} WH'oصM z9گ;YE%ovuhZƽ5䂀>8/JCPk,%$4$4*ۉ0BF|+@&mSd4 .0H8/\ޟ!^CD@y.ɸwR턣mXR הop#7H#]U:y ZI$ Lր<N$ԲkDH$RXX $P HjWX?*yy^",#Ġ@LE+4-ČI$6Hs'D^^/#f&Y($!z{3Ysb `K"&y"0 ~ŏG9r V33`ptt4b+L*d(Sπn~j:QO@"S|E^'s r҅ZImeo;FDeqәɂ>8k=M# 8Ձ H! 1paA8u8)W@szaZm+/MQ"jH K/,Tتb[BQ-`Iw\b.) !;"Z#Y$11&a,A&^ 2l1+***9'''-qm&OY.a!@PZ$H*qM%iAcxxv~Tg%%%奥 'F =OOO`Àe]Ac: -ɍE$kzVK%_I "%yi>KZ:HA,i5"ϖ켒VoP&c HKO*Y8$ >I|g s$v&VՖeFbQ)"9XFV^0!RGci~Oؘ;n6#~oFaBRaa_BHaM XŠ챟ּgaB%>l ,+-_TU$ܞ| ?E1 "5#YACp8<,yc Ru44 B# w,VoDFñH( {>షSR+( n {xy C1 "%xsw {=C ? GcsfѻIHIޞ'@z弁aP)ɳ$ ~%%H=(daKNHNH[Vj0x2{)gq6 z&)޺w^~e^F-Z}SWР2bjjjO𲈮 & @A (D "@P @A (D "@P @A (D "@P @A og$ጜK͇X,W>Z$ Z@D[v6o݂DGv^tm?)] 2Qtl= -R=Tm^SV)& <\jcӷn]b[o7>1^+e9쫫k5M*g~6RgBPYTfm\wH7.B']F[ѷ oVJElyTD sTtŁ4EcμAԊ(I**q l\)[(B23# BJo~夜jg*bμѣݕQc2CǓҁo}R΁IHԀa-TQB_^SYTTq n #[3L {mϧ!}Aǵd+s{ڪ^!IMFc*@1H3~Z.$ R$w%Ehӯޅg!L4qi=Rmsݕ+W>P|S9xf/|Ri}uqHVR8^γ,^Y,sQ3%DFF)D/p8x3 ZY)dFF~.2RӹIT>|@dk*ͱ9PCb? $@A (D "@P I?``GNIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/pymeasure-fileinput_disabled.png0000644000175100001770000000403714623331163024360 0ustar00runnerdockerPNG  IHDRs7sRGBgAMA aPLTE𩩩kkkbbbᘘ·66``6`··66```6·66666`6`66``6`·6`6```6`.L/66`·``6`6`6`·`Ϋ6``jjj`66㽽˴°仏َ99仏xx9ee9َ99َ99仏ХxxxxxxxxxxxxeeَХxХxxxeХxxe9e9eeӻeͺM< pHYsodIDATx^횋_E8=8PK⑨*8uB ǩEvefӡ9ﻳ3w7AD*s0"7QsyYKK9ٳ77WXBM6oT9afc{Xq-b[\e`, f2G4!m+1 a~BmvDKcǟv#_ ;'w*\*{4- ^P$UpۊbOl5To&[ա*y׆mm_K/ 9 cYcPY;Β]ڄ / 0Cl{ąia;ˊ{+/qT±9V1x7`uh<&lVv>Dqv|~o .NC /_KlyIfČ0-ߝe~{F%س8p9)fCBqݟm 7/܇Pl9)&'ߚ,.]#1+@ vn&G0! w71X^8)VjtU ѿR m fڰSZ#ЈqF05|ɾ6qp1&G{ %}A"/9wgBIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/pymeasure-managedwindow-queued.png0000644000175100001770000025401014623331163024642 0ustar00runnerdockerPNG  IHDR\ pHYs  tIME"5l IDATxu\TYiD;;֮unX{-|\[QQ@:{?fHU]{ܹsyyι\)3b@1 f 3 f 3b@1 f@_@3YFԶbN0 HC5ǽ1UC[O?7 Wizkoy 61ԫYcA*Kvww*T*0B1BѠ\=M,%dwAq*S c o@UTJү7 7.xr3'(f r9+r`$ yX 8E/_zYʥ'Éw4ヵM-Ʋ!SGgxzh"_##"xbLo޼8|snA04}k:͝Dj ncx""$mˤʖH(C?[Q׮]tǰ@CcҥKϟ?yÆ mllr{<~$?x/L7ҫHHf` i1fHVx)hݔω\B},wh>nh5KC4>I$l;}q+7]%qf}-9|'$*Qˈ[,QQrrÅUO Ջ%޹2Vx}:8~I$*ȯe޸HOսۯ9µ|}k8 N_{'{yu["c .)TгgϞmVu m`Oo,X~1& )!N76)m[G x رcGڥmfsΟ_ZX7oʪB j[LKdVu ;juk하goyv8|GtVў_D/tGR~[[^ $*Z׷u)!<&B-=~ਈ0"UR !7 $èElllmmT*y*ZjZV}cM DԮ][T:Z,ˤ#"b^jj`L.JDN7&d4;Tq1)ERK}Fu\:M\@HdN$ԉjRgy,#R,} #>_^*1,3w '1@"## a Y )bt0SBnbLax ,po~mX̹aͻzOs'?W#nAoOdrrqY՝^4͏PCJCXaxʕ\$x?V@tf/p2*eZMS!f N,7UJy(8Z-wXW:_^nyP@P`A7* zswի:JTwgu8buth}D}֭nXyy 5_}bO]tIXd6٫5DyŤ߆AQ Րҳ^Q;IbǶ_k۳A܁7Kv0ګ-7{l8x6bbgV-raNT-$?[׉"b ]Asf)n8![m:(Z/T Jqf3g2xt򱪢u҈3Si܉V J(p6} 3g_ƶiC'_[8pΟo2M+;ȪvX>E6vSfjKƯ .3pMGW9{uO*L]sÜn?:"O.?7LS$)\3D3gXjhS3 t;oD% #ҿ?y}AWWߵ?)o b>^U [V՜MDm>8%GQ oyL`zxYBG!E+Qr<ĸ07wʽFn#;nO+Sa}Fn dwo5j/%ʔ.canSr"%'j&$%$tfYo{}% - 59/u+aurv>QB;D];w!OgVINyʲgoz*o wI^=Y"mn3mՐ"ikܦÀ~ =͒^>}yCLҔxk.Wt3}+\XvIRI/^QLEWZ~V "όo mSTpe/kM 83ȭݘvOl;IZmʯwɊtS޲CU۬}~{湿*ެUB.d܄9dL:lV^[V;U١&MѪ-YKdkN=+s: .لLʁ8!'e8saġ1F)CCZv8k9doV{w]owt@ԿnU"N~2~iZ\~;JTaODG |8_k6ho}dڍuHM[zïzU >=sD4/.nۍpEQ.ˏ :{&&$KوKvZwءqA,xn53q+>2:ƿ01&d82%d<}WU1l /dtzS' (2k\;[&V7ξPh_^ xd3Q|֍EӶO(ʸ; 3G_P)M .60DB~r1S??pE""]ȁ=O yo֗=ПtVnvo%/5iV1c4{NdRس< sԘ zۈ GT-ꭚ3d6eޝ:s/nԠ ;ׯ[(_:=uax\_OUYZs+SڛYЃ|%[Ѳ*mʷݧř{2c?ѣ\,^NR"&YM1Gٻz&ImrC7n!q(,%"N,qDDJTڬ[c\ ~.˳\NԽک5]|z79?mG>UmyHq"rn Rwxx㼋(lj6>2ZDR班di_`D11&3n@ NrFrS;wtePVԑc. Ӯ1oGv$NVnش~eL4My VQu{ˍ?2TGX,% N\Όui3Zo bLM-v,*AWEL(82N$W]cD/cDRxN`/S^YTyӕwd/vd N2I L`)Mr7sްI|{^ń݊[&2%OO Wɡ$g2pv),s,nx.Hm^$Θ,+Ro@&{m o4OLgl6r< :-pvW״R7vؼs݉^[8IL%-]ך^Qɉ8*\t^^/ o,, qb3iH""xV/8A` pD3IDNY55e\ N,7=1F7L#/ƒg+  @8"A\Hn?IK X~Pp0|BJ v[þ^'p<1Ã`lRG'2Wrc@]7ɀ7/7hȧ~ G-yRK7zrxۊֺw:ŃE mY,Psc{r-T?:n ]tJY%8r涅 EnJ w䰹]FܾŇݻ@bWAXkIJWZW6Ez٢UA{M E <$=߿.PѰ]{6aqr?&m>'gy̳dZBLj7,d 'd`ɛ<9g [&OQ`Ǒקg0ZZ&}+f'"bbN62SՋ7b|j. fDN$qm3}n&LغKLXK&Newhbb, IDATqjߕ $.bN͘6c]Ŷ]:\r)C-G4qƀrߒnwQŢW1zh䶮zjOZ9i&|~~:1}}ÈM/Rߚ~i dzo2&0tѯQ+"l8.ԙԪ˃7Tߘgujuo=*էBٸK7o-7Ӥw/.;6 a0@2Wz2/B4"˷Ni2p#A>-^R}UwGP-`vnæ :ss3@Iu6\nY~)qn]to Ǥ/@Z̝gq(YyXj#6ue1:ޢT LS2o; ""}[Hiݑx_?3DM 3Rq3^ÚɈԡ_nP;#b-Mږk=_% ^ ƈܷt@&|d,خ7&#1J^04DNJ~ߑZZ2a@aFr!qmپJ֕4)kh["ήw62?Nܰc?5"K:}ƷvD9lδPZ%-^E?)bqŦdgIE"6lgNt퐇_?#vĮ?v.V}Nb~Ը .לk[,?}*x Ϫ~q2]E}=鏏1vu߻AbG3@nӹI?:!șS-dq)sfgs&7CcO-pݗ3}|?/FϚ'=2(l'Oz}eU0g xj1 f\*GD t{c6 I^ND;:Li#ee$YLf>cRC`|Fz퇻ӑȬ`e=v$.1 fȅa̺I`\I4oV3Vx'G,Vz /k<+<1@.ᅜŘt.s\Qr#&t#60+.ǹsp b\6rr k}aYds,͸t?k#e b40+c_J2NN<,X tGU4ò8dSc3@nܤl:qN|=3@ I9}8 ?vb3@nE xȅ YIg1@a!7 1@0SBnbLax1QܥMθW?\ۧOS.k4ǫ6}!6{r&ޚټ[*""Rݝնiurr3|{2A6N(Kʏ߹{a7x s#V:6a!7 etӼ;i隣µr- `%"<`­G f5:[T#*=pPQ-(LisDaI4u>?Yd:SF-xU{g~E-7$VCbKz}G:J"6M,/Zv^M7.Y$uiczVn6Yթ}֣A;سE{K9LO/+ű7\yH I/|Sj!_fUu#k'e|o;\jR7t@D =v*dS5a,K̫0sȦmVRscueܼi;gv3ͮ\v}-5=mѲ 6.\et rE۵krĥ{w}Z㖮\k3ٹc=Q hulO]v?$\5ggTV5e=q|y`Qlag%9"""Ip1~6r3ydUĐ`$pSD<~&9) IJ9r'µ剈xc.I(g[мVb}6[ 4t@DTnG}=Wߪ92I'* d1Z4RQp29SWO+qXb(NȧIeZWnyee֩WY2r30&$K>$.1lr&)I̔q!it'YTD}cZ_t@=b'.ݸyz]ǻ\U 1CnRʿ8!\C/(XPe z*`[ā:[fa~};Xp(nAI DD[O[(U KqC [V՜MDDLHcIӗ,+_qaUqٔ|3wM81mߗ~cylU>Iwa쮙^&BsѺ '<0ȵaDnҍ-7sx@҇\{|-lUbanM_Ɨ& Ӱ!boLoz֍DBMR30q'> GSZE&',֬f5ۮɽc&W>uvepmSZa6K&oռ0}vrrG (}j¬mv 1vP]E7T? e_FMۍ5(Rՙiy/݄eDe/w "0rPL?[ot}Witi%G[sʇF齋>vi4wƂa9$o{j˙|lD$0\eR U>gk|h&0\[|q |-J "Ճ%jMHzO_ΆmֆIDB:5*Uw3Ւʦ^պ_>iH$3QHx)C'^IL8MSoiǯ#Zѥg6ңapwȞ-7iڤWµꠕ}7O_ XkcLrv8M}|NZ1!:ptN ׻MSf' JmX}оGy`݆_gRȚ{%c3g.KzqxN&5{Uެۘ]5cc8Ga2U\~Ș>’;6iݦwP }|خ |6<0Lcڰ'5kݤӰ wb{}68|.ۧu_]7K&z/|<( h9hAtǶlAM Gni-68nݰ'jwмsc%"]voӺAc쟫|\ V>g.lY7IdhD?yNIpll+[ߒ ;|#ab'<>h;F9]6kZV;<^=~ê(, <.ZO6}e Α^Oo0v1kpnzFGG[ԭv=;5뷷W9~[9r|xW-nǻ*VqV>=ۺ3Ȳ Ƹ|۴'1 W,g/2F"a'ֳHx`8ف!WgyHm;HzϒH\ w}22ex]KN3*UJEMl%ΤdvözZK+cD$l4eBb2J^@5xrjfux1bHmEDï?uE-gTXrubз!oEg!o u!Uϛe(Ϟo\QTڠ~Gcbg5X.AKE;pھ$/|nkX١c@ͪ^^u+e-|#AK@""[(d $a{7ao\'o !!!m NOIdl.N^vnͤ۬1?eR Y~ :SzS'/3}ɑ>yVŜoo}ڡ{CCi}"8",~[&կĉcq&&5.~A>S8[-%ꔝj;y6Oݽ}kZe"]#?m7<<}o+N=flmiŅ>23&%<8"V #cQjOCnҴv{O̔%}[;Vr]v. K_ G\:X^O,wP䩘!IdgzR>+;{9 >0wcdC:zyZn^BvNv_>a :|#yϿޣxך+H%jF]>ؾKoFˑ I91F[^hLKR'm<#Ҽ jQFFX ϑ'1EUj"\aD iă\.>).A#0!)>>Qk&(`:y&vȺe=ԽS HHdf#syi {Y&tRF8˧33vܛձi~v7 q$ޤY1͈wȓ>^jS&իSju}UpmWߖU=n\ hl/|(k)ug.0M| Mxrf׎33$< P& /+a^}m؇ӗκrv]}`}C+'_„`d)њf܀#mZ:ܙ b]{Nzx|}h0P<ʈ.鋮l8PfPe{g?Uy U#GSIg{6< GfkZ#j^=αI5oYYqCS~M{WZOp⥁_i6L3?dc?lYs"\swغY^޽\rrpٓ[߼It+^I-s.M?\}Ɍ?,6jS0 1Ƥn]?\xQӲ2 8Bj󜱛R|& qLzѻIȡKqZU $.bfN"PT>۝< 3dXWKw-۔mTghɴ\}}5]u~ :,cUW0X"gV/}~2!mڡԧz!̓"}1{`ɮC-jS )?[gWK*;Z>P|ߕWVŦdgIE"6lu^zūW Q*7¼ IDATKgNo>gO/Z\]~B T*}A:G*v7 LG3pDip|w!oZܯ&~ǥ?}z}^!G' R,8rf|\7;n||@44ot>WfBy,uk^ QIK[ʂ'b_0ܱBA}}pD;m=(J0-X 8b귁W |e@*6s-1*&3^y R(..v||r|.y8"}̫ILx#9xڲ[Am*9#N_sYt8(l=G$6/X$DyܹP>1Im:C#>_MyѩS\<\A' IPŨHj"5AyU:UtIM9LT*uBFhL KdRgYrL{aFwTQ?`nRl*ܤ@18#EbaΰsDXO>]\;^&hR\u3䑦Z:?urr Reۏ>}_4>>^Tf+++|83wxwmS!+ @n_&oB$%%3r^f@Mc^?~{{{LKcZVe3DHeq yM=xȵa!7 1@09UV$a{I _ & f簁 S^@)MG۶mq1W $ 8n f1}{w4ƣ0s*ZAk?2.Jug|U3[{oj"Jo*ł7[[{ł``@@HPӷ;On{uԠ}p2ko>-Ƿ?MٯA ' Ba8iŘ!7k݋=qϴEy=>u~v\VDޡO+]CzwyY!i4BíuIdkE go7m|&!9ipb]zk8k RX/l]{  !Mr7& [n)@vkOݭCO4fY<GuzCk(³?]pkWynOdJ?wG.e`2v9ŅF˜is{άq5dEupKןv˱wwvdIB!yh4:M%l6S,+7:hCn쳯\npvkWpW*~S SM/S.h"jd ʙ#ٝى?20%n_~sѦGק/L.ZH,D>b٦wo[99MfY{uLhSCwܘ{&s3g|/O- wǬTj$}d{Lދi+pN@vۄ }h#xoM. 5>gQhD:`Hl]|݂Yy|89B?p, YfK X7 @V/h~~(ʕ/=<ƝsUMZ~͘c*ŋ7N؜u(~9sL2ղe˘j@5ƼR.Yd@ի_9hŋ -۵M uƆR"IJAV'ΓLit1ntAm >=p.>H٫ӡ PVj9պIB6 fE) @$M}hؘ̠:t4Ts&S.Xy$[|ſ;+s>X)I=1?K?$q4 3^9Um>jЃܭNu~&A6 P9 3ec`$;:!d(gqL/ n6u ,g XHN7ߙFpO-+q3 IHOIM㢲(0RRS5yja_ "555?0(Xd42u5 L/' &P|C~hXYeY'TH,(Ia)gk&IG =d2G=77:nyPt V/% Ԛ*5L/ZMy;NU}Ƕ$Z -=Xw-GH\Ϋ_t3n޳/Opo&_@F׼Cj <B$C?5لNZh-<3_};D{7f7t}qAx#ѵ|e9_;=_{g7j^t&Ճ`%3TyK ӎ_tkO'IB |8(EM"=5$kxe c@WVr~=M̕%-[xunuQ!>;`#u#d2˹Kעb%7o,"a%: .M]$a<{8C:0ԥ*p kw|=m.i,_ Q+k]l&0pPjUVިw__=q+3pL}dE_ׯ-7z:YЭsQ-`?rpP~dsS.iْo>3"E# E,4֫y3Su\< !xEӯPغ맋^_~$<}6u1jpMNdO?ktizW-F=Ѩk_Wi}ڎ~IXͩ6Mj`??zWtbJRk3(a쩲D2[%~9]Υ6 `UJElr/Opp液 7Z:V^P=lިGvvTcα%'3X,ၽ|V) j5'O~[O0gN2̟O{3d.WHː?^dƯـ ><'O;f~;njY=å{:}1N|#B!LG/KkwϋM1>ߛ# '⪂yWХ2ֽ6S&}0磷we)Ba:~S_OOӆYS=zpϙKgw7/Ig>;On{ul/%ܼQ3Ɨih!.\>pōр3\pPj2g޺`/Brx!˙;l:r*Gi"е||$ajed*&o֟”y.[f߻ '?;MVD?OV;l~|s[CݯJԭS;/snJf;"d}]~}m}rJfy>Q?p#۹ !GdXD!K :M ] @fB!m|fF! yr%!k>{o. &m-ә:˻պK|Cc9[^;Wb:w$C&p-u")DkBh-zhvT' ޶c߻Vo|G ,G, VDjIZ {wf\:M}}b/_p]' !.{ VM\,},+maG*4Oh;^}F,tLgOer1jL/` a$wxoPύo!Or@IYڀ+ջ2쎶+=f .\7igݤAmVt*e!g$?27ScOkӤη&$}sƽ9˷:6 ЏBW.;{#?H>M?:yKdw}/\y(GdӟM#?,Ԣܲm\c>5l#_i Z"sjIjOç⳶zquM~FdDņ놇5׌9Xxq)̙3eʔ;:< :8V_:K[e|?_J٧⛱TLS+mJɟKdE[EEJ%[ERQoVq모*hjz;NWdr۳ܝΊuۭJ*;QEZuǎݧ*z5 *:B^wʡghǼ.\I;Wݶ++߄*jA,_2πwկ E @)&Ԃm P#n}L dʰn&Y>"y\?6j8Mt42P. @eMr(\h66M2]ugKavoS*'oغԴs0T} z 5ujQ;F3`Lu #F h9n;߿G~7搳ْfd (* ajL tS{<|`p&uPOWFmȓR*b$Y3SSS ޚxl IDATpsq hjH6x\?]#Z0_iOes) 9]D{ejATIP*pHzFzJjw:!%5En8*1רh]uGWI煉qDŽ>~@W[!]͈ ~#8jfHMM  0L]jjjM26OZh^2gu9x_n蚲?i-$(g&%%slfd'3CͲ,˔obݘIBS26I Tؐuuӕ/~\zlL&gd+ՠhkb! 43H[a^GduְS6IB= g2Fӯ(-}Ju1Ϩ7S2̌\z@Z 09̺I}ܲ6 hopovqWr/OJJ7l¡ 4<N*j*6\N7mPܠ9붭Z^ O#Y(BF4fSʅҢM<3Ǖ9*Wv$!> >&nwMVFbQe~;5\GZ7 UQ2kF[;h"8Cf(oį]Kx\^!_Ͽް/9=uGzk.tf(Qt{|I̢}_bRsWSʒ&k 9&T&jE`* tE"DxpKZ${qPܖꄒܴ~a~7So>r}1νNl&A3G>bguɎVlΉ3>03h] |!zSoӓok&._=_O7sf^v5,. L.)3Xݥ߅,b5|3u6۽}{L\Ǯ(ܞ6ۗolc~r˶5z\QdA` TydZTS@88mf(pY$J/,wnv-~Tr nдm0OlYߝ4]tޡ^R~VGm@` oP%>zvz>9Wxi4n {Ekd,4 /t< @W [|)DvnE^q Roِ[ +1/'7C`3Nx !#./~mI^1K~w $[%ge:0_S{`'E5k`jPnu[)3g23g Bq~Pd.ܺՃZlοU3୑%ItZ{,X5w7§͓_;P!Sy(6j83C.'~zb|O9?{:QTΏ}-K]~R*# OP;;Vd7mUMB\7gn43{ <|IZF/I@=a"[2CuF38l.b$ 38^X.;O6 p 3ᑝE78X.yl&tT@c KKOΦIvvvZzZXHXM$ |뮥2Yg Ϥϧ'[xhx@@lIMCz*a 3Tl 6hG(h4L5|j Ba2j>az^IFaL}3T% nlr2!6jpkcqNf18ιl 3$pVp 3׏ $-tq{c d8*FE[AT$pp UMAmRd_bbbh cd@%NR MQ5Qgnz`!36 $8(@\ S 1|@fFm~l&> 36V $✊4-dAP@2P.F pDWo 0MPml&qڣ헢8&dgwb8n<ԈA> ԣEIHO; *UaGq*bM*9r)Q mas[eηk.0 B85ij0`=(:sYgiڴMII9~x~W):)RE\Җ$v#ƍlrU7`pǷk yݚ۷o bQq8&r$A6.n-N9DDM\85Q8NUqMpPr$ F#gR>oU~f Ɠ?}1t]X֥aq6hufIHE5-QQIHj Ui&‡C5Ct.6D1j$CuA,#i$A&}]/N衎&RumG ?<~jߖ!wqqsL8 2Cݲ\MA)s׏^Ѿ﹥νfيXISw s?BkOUrw:uK<^-iڤܽVzaLwK>kť rH\Zq  3TQXoxZ!|1qW} Ar_fMAQ!Uqj/2mِzzCbQ tĪ~F c$?~R?Ho64U?hFϽȞ[gfNkgM?OEQ%w뷏(r?q!FNz~+RC52W|iעW5_muJU~Zd?)l4O}^t,+z7lQ_\>O{w^wZfsg4N-1mېѫGmg "~kGO{]3?ؐi㣧>u+~CmN1&Yәş!:8fOwrs2.ZyUl?oOMz6u8O=.2(ZN2SEp`}]BcEWИ3-3c5qOg΍'E(Wq*,0E;n8^JnӹmS{v qY>и˓}Ĕ7H{?󇭦S l(E{s3Lбo|ۇvk練4.>>ޞZ{UiKU Cdw/]҅ϯbw o$-%YeăPFQ8{`yT(rAԼ4˰l0mLpQL,+BQr [BjmWIX's65dQ/V/_$(R`^L&Ɋ(3(B(BQdkЇOv(^]T{f.QaQȲ٬hxyT}яj=Vj/3pܔr?S ފP _GXnӖ4:IKT RaUkCU~f&7RB=ཝCslʎ. V] L[GU{>r0Oigg|`N3+Nhh{_tY4SdŬsՉ+fEb6!+*TO[tfR|;P;X: !!yknؙ9guλ5jYs0%oP$DAT|CN-jEIR,KAffJl$}?~xqw;{skѱH@$3ƣoOgV_C[[ Ѱ߭vq%[r.&z=I;K~6q]5ݼRXt-ט=[;5Kٿiݿd6#YnRmD]kO8c٘潺1|c_v~6ys-t'5@(1(B7o]TF^8{$!v{%߯}:%}ѷu w3.m3%}7)ێƋǒw:}!_n3bM: nPU BŗҭK~S{6ūyBH(GxɬW]{B( 2y?@5Ouwʀ7cO|J(oW$UJfmDv] =kb{fIB!!Fmݺ5>#^~/S.7.:vA~6EW*3aYNTX{?BM@aO&/OUxg۱3[C!dYq9 {g[#-Im?YKlf+}k߹{/B_Pv.\NW;YBn`-%'nk 'ԸQU1& !g7yewwvoxhV`[EhDPL6ίLLjy˘a@ I3C@tkFKVCBk5>TEiuMR<ٽ!w?sM );xw?xuwA/kB+Il~Wo7;tp_:_kءg7Imq5g!& =er_j<.]BtiMKUl ō$86MIxnxe [&Ok"1רh]uXһ+&Eqh"v0ޘ: v{0 ``?YΦ8CWe "6M݉rk4a`͛ٹe0m:j9#3aX -Vl_ucCmsޱ=BYA 8Uf0AaS:/˲d24N k4a!0d,\t\a]iYm=4v~!BQ<>ަu)yk|G_63!FcN)M/$DŽV8|t>V>> >&nwMV-{jٛnR|,Rd 9}"dցd4˴>08MQq*0M UThB 3Iج6ɳˣ_ϴy1C[xxE7bN dD3d@;͟NiG{6_GT8+0AmRo8=[ՉIˎ'<Pz Q}|uq ԿK-'r,@fjM<.}"yf81J. 6)ko8*͒: \)h4uAf B6[7ɫ]ɳkxy'b G9-dM2n~ɱktm:f꘦ty:\Jc$Sε'XFḷ;čaL2&NggN|9|o˜dp6){DMlݤ\ ǂ uY8^F¶ 6IȆ|Cq!b/\tµCRXu6ϥefIW†}Y'5ٺI=z8hAҸGxj> 3b$gd4>jgڤ^w0rX75pkҸW#6P &9)ǎɽ1}k[=|#&a/; db$./}k75aoږ-=Їa8(8@1~ P#-COjqK꾶hMCM=ݔfC??FǸsd@AIsl;Fm>Az:\KFJ99g`$Ñ9cz="ݳ?{8mKic% =j'@f}Iy_&8[}~:n/ph\CD}&]gbccnv8(gx$\(2M@i&AAI*lKLL}c˗f 36X7Ij?*;f, 6X7*C摿2EuAztb_q$"pNe/qF 3ab$G~|LhuzMgOC áGfI9}"dց=zp`$MҸEb k<<:L7#dIPol_'jw!8]OAF'MbNsfjd$ӥ3܋pkإ.' IDATE} Ym[M;?Xt)w&uo@NV&j 6ɥY2rNϮ=sݾqVOnWPw"L2`N 4}ߕ|ے%kv1}>M<8q"~6@fRTgѸϽC/0L8h2'6M*8dck1S4uO"հ)csB;o]pf#4!mk֨8-w`lPy_ l"ew$8{ $Ti@}aU76|۾{kâ0ɡsTld;\Zic,xohnf&I6a:$MrkߙjFQn9|$&3"V)`M:^ p 6Ih#F?t~X'%ߣ3o5ҍ pf&AlV$wjۆG/t3/?OT,F6MϭÈ=4s=\dNlS1@"3%cڤ/km t?m;/yv۽OO-$oXW [xpD,"&AlV$DF~aLu #F h9n;Y(gtL2P%$N|p%+HcyG;.:4qm2&_)[{a>1P. Pl_t>M }}$l/kB+e]6]#K_!>cA*{"5[y|OrnGuO'sֵnjI煉R_vhCcʦէ>U=% hղٺI oV:gV +ftzBRWܳ)Wrl\4ocC,0jڒ?TΝ;;ݨrc$;:rj7DdsÔy.VF^ Al*0k]tW/i}:YIB Жz" ; P#wɶoطZ}]'}9l [|zPO~b}Y-c|5œ9?dN-݂<6Mr;cyۢqۦss/Y=ڠA=f\j):s}~Mrm5ٸNCnnzN>s9U[ORgtlkDpt+74a :d8<$t \vYlY-oX>@M@fjBհBDgZ/{ ݭM>:cz:3@ln=rw?>{cQ!nom k MR.?/?xw4؆ǞEnG]TW>QQI.wQB@G}ɿw~v߬m_L3cjd$ӥFIBx:{WY @@lnQ,@rng;0?2\76۬$BG>`j}dD@lnsW~'f_|^B{#VdtӺI'L3;7A+S~K2 6woٔN (3@lnl=ЕlN@fóźIA!3bIIaQ%g* ;L/!9X-[2Pm&9ݹOI`@{ @^|ϸB3j@G@f $"d^8z]7 3aPT'@]Pml&V`PENy PT018`wM 0"qށYM2P M} 3uj u@fAmd626 nIE-5KdJ"*)U]:Ub`_MQ{;n^7 3U $V&NM@f`z-8MIdAPniPsLDz:0aFmbLNTu@f $P#.j#uL/IynԏYjt N,%O-5yܓg3S芟3-Z}ꟌGW^sUtnaA>jRSITAUyʲKGנ+yqͳWS{wF}*⽠*U/}kP*SѦܝbt*nr !-i_VW۵Sj15 sMUr)KJ5<ҥ}UZ&?2ƅYf1ŋǍgןqΜ9SL]OʊK^¬*J#],ߊZ+}_)垏+joNyAʶ|E\6`=[QW&|æj%XV^+ٴVm'҆}r?EE[T%;r@{PE]\gg[lY(J[B%[]ջcNygǷrܨ#TrLW1>XQ庭T7J_k% W^ʑ@+^$I# jĺI`D({&X&1f/h@ 3bz5rb87MI)Ǝ$ Pol&p"bz'M vUTI*]7!> M0bPA_ "2` j8= c6 P*:@f Af*CmDË1M*]7 MX`) @^0 ΃u@fAm"1juFG@A8Dm?v8#_޷{SkwfɼR 7xsn$=@Eg9)3׍';&15.n isࡷivfo.` 3ajL tS{<|Р tKMFGFҽ!=%2`}l_dt.GBwvTO˦|pp4 kAMhm8.2`+R$sUW$N.7VrN}Ҳ!Sb$@C,Ig4R,W82_:n㙀Z޹sgFǨM=d(XMfL표qebNk{"rB}%`î7?IP#Y7Ii8' !9;S*9iCFúE*ja|ػ 38̺I}ܲ6 hop" Xs¯sٙ糌N9/ΤMr ih66N7mPܠ9붭Z^ O#Y(B1eef+ىN> ?Sٰ_8K_s0!> >&nwMf{SZ! y!8e˖ P9jFSvʍ1ddὑZ jʩdNDf*Cm#jw${!n$Zܿ~Wh76jN``1UXU<Ԉ0:mݤkj9w: 9-[N2Pm& ١ZN6 $q@g1 @MPpWM 2P*6j @^ɥzY 2` jvs9dKAmP,k L/w\`SU S&zqu$T76j*$I 1`߸XA&T@']T[[& j0t2p M 2P.P'9}![@r6 y΃u@f $S6c2P.]'CfQd @vIN@f PkJyVY}Q[o Rd>Qp`ujt4TIE-\b uy aaC=ڜ-(u@fAmM6 $POؤ`j Pdn7]ɺIU&AAPv8xg1z8=c$kPA?qzۯBp6 $8 o15bzuurej%n3lcY626& pun@fAm1B˞k~5MIc>l0yՃu MB]| ޭSmt`-FL/$"@12Iλx>Ʀa$IPol&o!.cajP ,`{2` j vB=Mzc6 xq3`e*<Ԉ  P9j8AyYu8IPol&PFL/gͺI 3֠6 \dMzc6 &d@<ԉ _T$+^ȑznԏ$76MR nܐtAվg` P#g^0lm빠ֻq߫ T6d|IK:\/n 56pqCdQ!{4ӞjOS ]$@̱A8Amy" !'[|6LsJ9Omb5*:I&`IF qq4RTZI*\hΜ92e 8M>6'M\7A!,'[&Nu@fD&CZ7q9-񟔌m>jhgjBmS|KD7n\FWd}ya:P+jFNTpz2`}lNQ@f`$kP@fId\L/Pp2`}l&Ձ9sPRRR@u 5Miz t5\AP@fzh6Ovo\6h{c^wGfvLBfIj=pBmԈ$2PMPj MEJޙ69}ŤkܩO=~9/}/?9&>@kd{7nH:~Ѡj߳_|w>ܞ?<}RYն{Fhl޼dԤC;DIB.m3%}7ߗoJ 垄hp4_} |}b\Hˑ]w{slϫfƋǒw:}!_n3bM ?J^(eh [GBKٵϿO\4n.9đh{U״iS+ //*?|"MlJ~`m۶%1&qKvL7 ;%Rx>vNɕ]#ڴt!!ץuػc-ŷe@]]}^9䞣tj?;wm<pv?9ڷ?{wEbnE@JP ;[8Nn]" ˲1cQQWE]~wg>w̾}pQ6rhj ;n^Vt6P^z_N+N^u9D^Ȍ\׼Q.....B:{ݴH\5UNc2w%ǭ&LUU|:898;eHVr*Mcs8p]ִMfy/^why䱣O?/UEm#&+{vdab`%{(1^2_.iR _ W+kVy+fJY{im&-yWrRǐI͐i7lYƹO-cO }jyؘW鮕,B zDn)][Z ؔ26#g^Mh2-[-{B0,[>x橠Jo#QkoG6i 0H Β~ѱ {k7,M#ZZBLL HO6xK䔮[7{=:6L̯UwR 45\qԯա~r_oʇSF~ ?6&Y5_'rV/"]6Z#|_)e%o9hհmuS~j6464΍q'2ͧGR9D._hګ-U0U Kt[]=GmRߴOD".)ZB./g޷LNE|>WE{z)8,O\g% Ow'P^^)dVhKy/]McrEH8p,lnjK23<&%#\8ڦt,e4#)oYȀ'NNBgͦtL$ύ궩Ej,O4ExzԔ<B9Y_e{9{*6yZ:,4yyMLu\Ba0Ԕ| #_U{b$eb͉Ke!feJ:BU)yΐ4*>FR4P%r9wsJaYْy~FtBrWcvB/3,J IekW%'D_ܢ.eU:|ף$599999%3- MpGc߼xx̽, \''ۗ&%lgG]Ze{KۣD$/y웗OHuu7͊_JmY+[/_ݻx5IӔ;j1מG?=ȑt5ֱ?|UF^^f1 SgkMAYK>)3|W ߘ4{n{jUD IDAT'֨n%=ny;N݋{wۺj-yɏ_2~C>U+O-*-;ٓwRݝK}(Jiģ<7pk% -2|zGEuWԠ GUWzvUQS6JG>y("M{VBPck}Wo>JpR{GF;>Kkg-UTW^:cњ׮ܸԧQCc)#~r#5)500'PSN*v FHl`s=l_X3 'CA 6 @} gM@%L/ g( j3|=m M@3I6& gP $ _Oj3 EAm~8iLVn@PՊ*<Bdϖ_Jt7r. !5d``lJ5&/%5)=Fo^s@dw<#&@q,KM{ !1F7tФ(JúΰB 5+)4<'ͺk-EQ\c^#>^IО"}u`l`-]_KgakH[Bt gr6&@)|癉vFߜqpٔҜ'+Nd-1!#R:0BmGm:Udޞ6l̹Vn!Ɲ !$?G'9:bjѸ 3@(;wlz]}uyΩv뚻bBעӠMm B= Sd&e,STV6=ڼ܅<-lta!gyPPͪ$?I"2&@!My"ӂCQEQmNdL'p,Rsw8:u_T.˪.-dj9jy&Tt9!iQ.haY3@Oj ӷ6ny$URdz5I#vmeXsجz4!lᕼ5y: x&L/@xB"52MJ`S&Vo-kuwy鹬HOS.V&B+L{u2z Y~a+sp@j4R-nQNoldgPgȾ=43 bZm/[lpU^ᕈ߽g5lZM  Q.SRG]N;>ڵ+88-n}a7߆ψ2<#L/ Pis3 EAmr ITr6 9$ *az9@QPiAmr0P0)gF7nl; 9@16 d ^h6Ma^Y2[~>Br π3Y 7/+9=7]} !ˆh[; ۯQk/% !L}D[C n멟f;ѶV@w@.SGقQ9w~~%2E 6 h?bdK::XxL^i cYkZ>xBM;6m MP{켶-Bd{Y+ig"gC*_`Sy &$BkkBIu r){'N4E=ɓá˦OvQ?sa@)43l~ #ZW~Z4Ud]_wNh%&:1j4a$K߱5W|{ZABqn`I%?, 3h݆&U!g&Jg:u$$6 - wפ>9st*|>ߥ>S9x{Ƙtا`ie'G]R=B%0qrr|}.V7]5؍~r멍aQ˼)e\5wq՞N:}vs`am}: 09r;uL9{-$riP;>ҫGd"i^~+'C]76 jJ+WLйnIRnR;qћngy aܦ[ !Du[v)QCXyvaڶla,-9Z('3aynsZXdNVӢjф"0gbK?z EQ?(7[ѲNм>c__f&7=*Oy&nIJ۳ieug3 Ij3ze,!ś.4^ajwY9#& 4+CSpJeA=׳Riˁӏ>nħ !WGnrV̔F5{7tj;A''u||jv7gV9y~ "wņo\<vaPϊBe=W~y7*K ?Y?ak;lFnBܫYuMBE}l~c75~BvWh__ѵУ)9 bv9j@6 y_C 8U, E&&Lv 9ah$dsD%`%CK*7MYEӳujڊLg Vcl]ènI'> I3=:?3I&d+_H)MCI_f}_ZF7۸~ByQ!w[V&Ϗ.ߛ:=&W:_ZХC]W`zr)OMķ4tgM7k⢞Bhu`yC=tK:wJCs+y}V@$}yi0iЮ|BQHrĹ9 V!ih9yb)9P1w5+k5} ImokImۚs!"AXw=e~ڪ~.B*!sǺ r|gu zʜhj}r--C)a2,!&ȐҺ [Șfiow= %d˹&QMbI*&G,5|B'.!Dp83af.t#;63:­Ϧ9_>m@J6s," 0(M@ITr6 9$ *az9@QPiAm@:200l!l_"em#\?j@}mI%X%X%X%X%+N X%X%X%X%v@L OvJ 9.[jT KKKK3 @%L/ g( j3|=m M@3I6& gP $ _Oj3 EAmr ITr6 @}pP۴pm˲ɉ oy4PdnfnjlJQ \300(;SR=< CRR#" !f&f>0\#grMY߽ n:R0\#grj~,˖}c#|_pmpʩMR#j()VcVk PõIXe\ %@}`e Jg(bJg(Nwޛ^JkW?\кE #^٭E%8nqü#$(}9/<&($RY<67ĺ;GOͯeޭYLgKx9Bo k-itE ŸؒɯOa/Zr"4qְc^?'^^Jpc1nykۤsV\RAj6M"TqBG.e?>&Tڋם:+^XR]|Wws*SQ͏prB~~zpd]ĺe!NzhEng^SDW!6$u=i0٥]&jsk JI*P}y.u)B8l K4ں91:ބ^$mʗ'_޼xšɌc%Ac|C~K|&Z U(k߲+"Eocܵ>Hæ]Y~E{;iPNXR<?0`)vﺪoKhcw/¦90>KJN ݷ%@Rr&念[m B+W(;r㺻5&z$s̽z]>a[oMɼlbs`}+55Ku ;nI>PV%(X&I!/X4˰ȣ6sw_~Y'(2n.U~Rj6y #y}N-2"֢ ^y_~ݚQ);g-F|jXd>1ozW8,a_ESN[:?l ȻW|݀eX]+#>s,dqn{J4]\B@r8>V8jh}? [>B̰(b]W1q\: y-7ua)jï 2yw6 4밅aQw טgMȏ&J_ӟ - >U=kTw6S,Ns&6Ч:|kLXY^wM2I [~M^=Lvqէ?av]+VJW5;4_y]pQ{j~Vp>>7^j\V=;p<ҟl`LRwsT(R] Zٹ9v{!&MaiDRŨ9'ڤyd˨nKǴz9?vuքR>6a&J JggƦ(<,DGD9?'>Qذ:d躺!>z/N8#)H\D5SYJêj"Տ82,EEC[⌜Q [oFr{7oݻ L;؃e R PtE+Oi$rRb38 hSxtdYB(VyV2n{ljBXTf#d)Zy@ )YTQRσs>(&}PqiӠSSS-vìJ+>(nLa G9T0Yz)v<Q=J 3_kGH<t +;x2/i(^s0P gHР (2)XdaXFr].%z?4 mվ+Z1sKJT|؈WB`ߟ3PZ^Kb{ NO-r7l֕v?~\?KQ9=?YXr<PX@iYp2^(* 2M{c )t&@8|JrZMn%6'%$F9.5q?o@~7(a14,4<_?@9ftbD\$㉱!OdcvxG 3o+}8ԪB@nƱCOBKksӣLᷰLt^?BN彉HҬҮa%}.aYUsQ(#(&8D\Dž`tV+rr۬v/4TJfפfقyfS>~X0^ 'ȩOqtXWMGg0ilc&Ã{B= IDATh{^v1uRݐ宮 'WRAm@9Hf܏kh„?'ͥۢ^o6Mb?~nRph͛~^i/d+!o⠟{댃"rbM{u=9nSr[8qҢ?ra\Weꣲz ȗѳ7:u蜹^}[H:짷Mr3s(^]N2iVvV6Wvo;կ&Q,_9wUE'Wx5&zV0ob6büդkÊZyq.}YuD_MIÏgFj(e7Kiݪ>mP6A(^8O3ύvL͊|M[׀bY=dzMm²СZ^'B5+bE3;7mUU70 ?AP|4^}i->#:,ZuMif=,Rgߴ~ߒ{ ȩz`Nٵ❡h*M76 {Q(ȏ?=8<붓ںwS,VĒ(Ɩ_ Tep-:Z:GɪuJXzo[n{PVU/gBBi7ؖ$3&;Lݰiܓ ѲRI-]525G?)500{,nId>#/I(zæJ^6\#guTRI EzB}QˆO`w @}RVeAq' 8~pͳlcAP.k J6I(Q% ByP3@VRIqqzz7I$#3Ap~^pk jIx_&  \+$BaldlnfN'2 Ap~^pk Po"p& 8Gm@JI 9)< g(*m M@DH@djOX( {BBCm&:Bmw/+ + <7;, + + W@ܤ`A@!V Vz9^>B+@+r(7P=# + + [@PT,SbXbXw3@QZovtu:lLf*d} 5# A@-VA5wV/a˓j )3:*tSo۲G.E$IOΣn^j999Gj֤^:dxKm} e'c'#k#tq)GU~wEDe0DɻAࡁ]ݻkcGf7BL,8<ck֎XͳZNgܻ0ȚqowҰvu1&vl/;I=OrYBCnw!]A"Ζzk;]Q3l{vvvoQuGCG#Ŷ O?:ZU ynznV& jzxNCH~dh]ޣ_vSLt`˪~u 0%&ƆqxjW8iݱUW/~;I-yrꎺd>1k_@զ}~.V51A~uvl[ۧסDF|=Q.߰F$"DA۞I%1GhV/FcvtDZb|=![ow/߆.=7zJ%1gK:V w6)jj7=|^9׿~_OlTwee1"~OߠM/eDVϯĝZ8ioG{5EA!l^ݛ uևٌfCW؝\ѤZ".EDZúIݟ\?ޤ/ )ˎmTϷا_멈 fB)VT/Vۿ\sXXؼ?x7fC~sN5k6i=h)̷]]=w9ˎAWFrl,4F[W ^_\r}P" %mtִ̎turסn^^7G{%Wkȶ ;b>dwrŲ +f)_?^T֠礡 En>x`~zsZy̔%_:io潗,سTrrpy/?&R] C[y{iwƵ*Љ*29{B6 iײGOxnrv/s+q!w{ +UI7o{^!ɻnqsI{vx2C!כ<9uAHsϧX, ۞j``ػן&&&;öU^XFj*ɸ6k𒘪c<{z !ndm9|. '/۾eaNYB(l|F]{,4jusN$(,fQD]>oBv_(.M(EQ-YrCအ3NL:{[V1wY"ȤQk|.[aNoL/lԒ|=e ! )swX[mLcE 7cޟ)|mj!pZws jr[ ]ng矞1tYš?{r \?I,WJyV 9%<Ӟ>f+y~n]}XˇM=z:e=J 88~/ )6{^j1 gnܾc~Q<B#rU~i롓6֛yǮ!+%(+Jb~D'H46*CxU;M\~9uUevO>'ӧS:2+T #?àY;{ Boſpɕt0]GZځoدXF <-/3 tΈ:g,C6/aµKǴqBF\ ˵LIG﯈q|ZNraӆ-g3ty}| 8e[_P6M&G'fpGMiQGBⴷ^uEs'H !fݙ+V$¿}yY \1O;a=%GͿܛoUKKs.;vmOII>pp_=qՁl|oO_ze!Ĭ!'"k3p'чFHja*BnܐW|sYOѝSoc/kIU~;ïە/^bi/\rxPvqC{3v̓+ q5Vo5}[GqRIіW Ny3v.O"y4'XD}}+Q5D֬s t{fj+G4nիר[W%\fae9)I<7>z+oQAOs8zz"BT ĤWCc5笟MH=x:Wſz%;MbU;c% ƫkfn*FĩQj')Ŀ^ Ua_ pubNKsIqRZ3"wwk4㑺'II,XOe~-w<(y=]WV0l]k57C;VgD SkN$"O8(7+Qv쨙͛n ٰ:=MRV'uX%mلy P*ϟ%E5^~xwl}D-ᦣednˣ!T|O 9Sy=0۳'N_q~a9mצzOwڦa/?:7l]=[-ɵ1SmVŤ*g*2 y)S_jPzo:bJ X45FjHe n~W }N 7|Ґ&s} VVr* \.<-}׎Wl"WY8`~PE,#gY%NVup G޿n2.tjoUE/,PP.S@}RIo%B ],5?(Ҟ>2kڵK<u'IdV9w]u1DFFhqUWwz4S0oh]{GGsTScX\WYՌys7VHy1wr,+ƼdyaFMl>6IW/RN M[yb0VLי—T 4MBW\VC+xB-e_Kg1s)=Qɓ#XXq^)'۱ bVE>l7 )ShXF=NQQNհֲYvhsដ˾+i̡%ZYiWĭkW? Mȇ'sjvN?b*msF.Vo@[@Rv3>egBѶ2D~wO= Tr:Ӟ%dZA$TE' -T-4r0T=_p禠~Fmע{7.ƜWbɠk zwK{LW+F:v"LƅwLl,$)}QgͤiM%?t"UEF':+^浬ҷ|е[{.%[_>~$֌i rc8;7mˤ:mqúWۢ${߮q2o|=д ʃT [yv9tlPQ;DuS3Jpxl+NAUgn-BW\H?Ҍk>~2gǬ$oӲyT3B@|ع6,ۑ>yAn&ԸI%#N߯dns-X2o_5uO,bPcnञs]nj#%Dxy3Ys#&?7G,gY8'W*)Q=i 1jEހWJi=g׎W%/5~o?4\-inqЦc_tblnT5]-4rnkqj38گ8m>ܘ}*3QƎ&)F +YjCzS B/1 ԰۹s݈5'47ѯn֒\4 ak&, 8z Te吠R齢' Gwl߈oA?-/Xv vsQs>48mPz-.CSC L2]m & Ӎn W2#'7L2p[OʒY5%i%沄gxၦZwި4 Y:u>Jzt|F!<6;]8sqa!Vv;e36ֱ{ŗ ]с1_g`di6Z>ylE{. ,:/َG1G/fH V˾㛙Bqkȍ:LmqYtشsm{;WxH\ێ7ʐB67j=fĻ`^ؔ5vB"Vu:c)v̖Ѵo?Bh-K}o'56rN~o6yvUoRk9Jg9sV-=p *l׊;+]<9lĊ}롁ҭ1ff3VNk89Эߠzm'MJ_+ i>_(9vwus(Ό3N!Vد~͎c-wnaE8?,O"B']T eo<ܤ2<>˥# IDATDrKF37TNB X1)ԨYXA ~JmRY[f*7+aŦ ; E!Vد7Jw&Ne"~A&m %ܤ$s ,wK^#7XANJkeϥ.:x` fY b &>B+@+r(?UIHB+ ;MM&=xTVڋ>BB+@WI` g2M2,W"]~HE`j[T5PT%\.O!m j$.R*G".͗V5@d!MBOD@}# eرHEu'M *Iӻ() TP3:*IT1w (RŴ(6]_-CӸ~g 0 g?{wws E")"EELJS *w޻#XE,R(`0r;f2Pn⛁ $kd4 PP$reVI*>fV];\vٛmjWҾ@1@ ̀@67)]X`WXPsϵ90ͩ"~mvٞiIzwN?Ŋ?gIoͣ8Ç8@((f(_nR^agdhP6ؖA$8Agb_gMD9&/3o@ 0:hzҪ6 .+eVnw1ey7Em>p%T#zw#'8 zD/ Ted2eOt{-}wI۵p _wꖤw]>w??++n^n~+Q831x܀n| dBPMVq)ʽuogXS"+ r m/)@ 3 M"a0Z8B=/9WT*ꞿxտ'zӜ~f(:j͉Y'q{~v⮣Mh(w$͙)ozN[,!Sq κ}SdQ"rşLb>N]c җV+WWNz͇8vܙx;ND6! 2x_*HуwϏX1kea˹<'r.edE^3&4KWo(Z @L]iQsV,9# = k&GUkcԠ `Ehi<:&̲ )2PgM 4' 0pe]!uhAokR\t_AȺMG#Wtxsη9nݿ>i;.x+kîM]Uyja۷mX4؂ -߼x%Ҳ5UƝ ' Fzͤ$3Lx ZE뚚gls+-nɄ(x)aSj\nM&E-Qw‘ZE+?yW1f%{qqZ!yt Y6tǑEfH')ags$hqeJ& Y+a Lowi9l||@O3QNidVu/rIA5I;_`<>u2Tn1jlocV kj{?{vUvWV@39ԉȔ|W|ש*~8-u7BElu6˅2aYTa֟%8l %5b r@p4X ,a~p Ly^x$4Q֤vw6゙Ugo`&KoTk3ێL;7|S,{׼e!X:>m9ӛV/2;= iAf@ ]7Nc0L*rKunmȥI;fhVÂwS+Kւs4\$0A0:]!S}U]WRZAT)I5oϒ g1hBA?ZH9:MWqǬ**煋k=L9;C}lIv*&nz|qV&IG&ukN7Oʻky[2lo |I :Nhn>A$ 7X:\nke"ގIU4<}WТCeM4 DQ$4Hˆzk9GSr/ÿPZ mϲ2bQ[=vϽc_=˾VWFfZx7=]5U6܋/)/)3 &MG5=uz{Bk-qbIj 5й:ȩB\-ƸSV+zLWHWKEɻBT#i0["sTrkڂSW&[t){Oh3{%WVN)jH>55^pA{IUX4h/ΪZjJDBY$lS`2YE%O_]c>]Sd*IPǀH)XYim玟nxZ9rPNJ&Y uY)i'Hs/__u~8߭fceվe /?TL䀿GGr Rŧ  h4( o9Ѝ&Ԍ,\=|Ь=/)nC_4ʹi[Vl! M-Č:vm|qYi^kɐ{R|Qnmj^W(540nSj\nMIb9zO}_f浇U^ WضNJ!Ipojּ0ctu#SNb7T]փJZ7Iy{$I:%<}chepbw^y17P%:J1̸wyZ2)̸p6Ö]zk}`·\ྶ0]?ǚ֩w)<8yw9-֘,0se_>yYȋ:ZŕeC;/Zփ p.*p k#v G m87IDv-oV7+ ^K̲c$@Ҁ\Xud牴l XͿ3jff*K< X\'Zm3n94uZgQr:ᮔm u"EJ55BU.7wJLqE^w]CpM~s9tlvYIu~Ӗ=wȒ$ضOv;Ncu;fF+%M5zΊSi5&7Պj%!*l&+޵pKEa^}PݪmknsFN\rX͒n«>?^b#ϻ#fٍS"~{g=q I~ho}%m6&).-|*>0̑#|R iit,۟g{ZK;DId'D|C7Lep,U.]=B oG? eB Q369foFd"._x81;̶a~i&lݢomX/'xĻ"~Ǿ=~t:}aG5盺5v]Lc͟WO [mwf^2/QqѦor3ⰈG^#z[~ko!y=j2ɞGl?LS: p!3 3P$ĿUu}e?9ʹ=z/s[x]K8Wmaokk{!@|Q sk3(][PuHi9/BQ-"Ӱmа@ ̀@( (мp/l9e/r[ ":0hXPB XRǀf4x͑ kPF곒@]E 1)/$ AɥoAbi0 (B@q r=-)xqQAT3%U& A#x<3S3$ @6(7 ъ0>I@ -PFPn@  5&!Z#/Uz9GP3hhxB d3  MBb6`jIi;RO^ȇW]tE@ &!hżUD+NI T*-(,@@ k6@M:7wkj1q8r| EQ!1 @IoD7F[5 !@ ̀@4E}nR`B^ʭY$O8NXYu#rgoBul2OMul0@r? E~Y1\;k3 Ӝ{D| PQ"d/FC!( @|(lc3Аɡm-Yͽ&I0qd_T兟\|֤K{\|tؙhȚvsl]9o.&z)os I6bRI \ws_ThXPɷ?cG4F|5?Oh +8Á/pp}(PW%|Oko4m.|C/ߩ[w]>w??++n^n~+Q831x܀n| dBPMVq)ʽuo]Iy9g'<"@^x9z^wN͒W \ ˎ0s)g@(#JD46`0>-P>saNg/^l_4?!^&}wqx*?YzqtAQ oŅ, do:iͤw,NXo̖Qzj-ŽY ?zoȹ~ޛm{T!9mچO98M_Gu o[AզlK7ݮS%3J݄­w]Xtֵ-~Ϫ >C@Qp觡ь.+b àzh}?[D)ϭ]%kZBF%f5iżVLYVDլvRfځ{Fo8W5_|p0tn>GDՙ Fӯ_qqɍg:a@ |wxs[BIG0"*p^)1ewMUQyp|NexTmeowD46f/K+ńuQo 4_^brU9A٩nV+@ E3&z? gm ML&udrx^qcz[P%UFOkӼcϝA9`A5l=8xW3Xvpl8w.u2^-=;qW8Z"jWUlr-\Q|r]Emϯw=%|=yY|PpouNͺq1MZ>\M =+^ [C#hr@UNl/ IDATh2i4PJecF`8NȲߐ:Ӡ7S?>NMRܻ[5Fɽt#3)2uOL]d6y7~GYGMZ]ةO7L,/8pv\^Fک ͋W"-[Siɐ}\ ,~0hĪL:I2ʄ ѐ@[XUyV6"LGy-ryԫUlI gϣUTfaN[ 4{K ҬIʤᨓ ukQc{]ax>w30ՇENݠ&9gc`?kO=Oe9gv}h_ 1J&r **9VY;R j8Kߞ%b8)* t_:}b}^:;ÔӺ:ǖo]C(ݤR_:n}d?|h۞wפNt fգWZyw|-oZ{Kҁ̀@|!JN4{Oб~u<7׾ZهH51 bW8/ga`٫,_$.U@iS?nM67Sw&τʂ9BVU-0&|S׺5 ,UUN4("5LeI++g#3-Ԟ.Čr[唗AsmB E%B@̐O0EXoܡ OP}BEe$$F=jqA\#yXZqM9trj$$W16Պ~k Ij2 ywPjt5 fjR˭i næ6O]tnѝ'>ɢ앗\Y9=t#KFBziӗ#$U}cNӠ8~dk*t fJH}V(&EQ$ؙ_2e/.N>ӵ>ELUq H?zɪ @lֻ>g1L<rle-1`^8"`Y/O/gh 3_W}\2En?뇥/0]KQ/9ZeY@YvITLx*?EgOG5FAh8Pk~T t=-Evh$+S/Y9lHz>nFӱ5& RZ::*蔸J\R.#)`ս̐k!|BZwPʂS-$QW߮ Q/OKuF 'Yʗ5juׯo++J# x-r@W@)Cι;O@K}f\8}a.cml KFtZ0CSno-im +nB;#҈.hreޚcW1J}2SLi;;pղhSz /-h¨VDu ެDs!U^*MZ|Ŀr=fO`9L./e;ƈIu*iOg81dʼDmg*|5Y X\'Zm3*+gNJzX^9slHv:姶2Mq 8QGʉKܵƆ 2ӨHPXW+RPTXS#Trlzsμ\qׅ:4 a73@'{l5Th\!7mLكxz^,YL@ށmd'>F-]cmRQ8yVcr(y]NJQښ¦o] Teow խڶ%ܮ袇\m_$#*Ϗn~gj{vYOzxERd3 MImK JO s+3Taj<]#((F,;}R/Q$ىef mY z'on,Hm\Ao ;2\5p8:սorU%K͟)So?Ч puEBm:1tXS|)$zkiB 7fS &nr;1ԒӰ*UuŽj4@7uk께9փ ~DL댸xD6eլ*1o!mv3ùMS uOI qϗt&rߏ=vЌG*֭OX4y F]ڴ廃VJ $6 k\nTՕGЧ]h m{{fdd5uh{F\gEdUQL@ l-vM)Ӵ,uhڝ:sb9E VsbA/ f|~Uy~ϟ5jrT7OFD]!s \#~(<:wOOn ͑<܁-m ],+}k5g>6g Wfh Q_rFudyg=7 Ʉ+|qfEZ+7*E9K⦆ۃ]Ò@ 9hB(#J25O(}}pvmQ=vg M~DqhessV{ M]X1$@6[Ditkp/_33>y֎+iqqX(JlFE\:|$)@6rV"wᡄF r$ @6(hfK5Jb^*_L͐( D$D+wT{$@ Pdf$@ % / Z1/Uz9GP3hhxB d3  MBb6`jIi;RO^ȇW]tE@ &!lhUD+NI T*-(,@@ Hk /pPchj1q8r| EQviC d3 Mr߈ntk CF@ 2/7I(K2+$ K3{D_.MmQ .@0@ ̀@#6X,v=P( XEŜH^%t.*3joՋ>ln90 >/%B2n;?$nRķ H}VRT+660 yjے;h'LLIU^gM_ͯN8?]6/7R}nRs5F,[TJ9n |~9ޝ&ˆ dCy8C\jCkZ'?Ľ6ȯ&m.|C/ߩ[?an\w5geW֍ˁoW" g&Ѝ/p89RUJ5.7 E{μY +")~0/Gc섇U$ /GK p{ΉYҔ0=asud#Gϥ VVPf(_nA,B=/9WT*ꞿxտgk|-?v/ЬqE={ e\?rb6= *SYrmCvTAuޯ:@䊏jS6N_X'#,5M~ߜ0Y Onj;0sץ9/^jYq!b iV\{&oݜC!Y_?6oi"1v^' qs"l,iIcڴpBT6VygIwەqwsXy^Ցifܪݑs,Ϭu9'_ѝqA! gBGz[2lt:"ʓEQiw_ i4zI" &T^u^rخN_o L_36dϏn|n5+<nAg lwegj-Pг_,ۋ/p7aյb,+~Dޅ*/6 u}(7䚕Oyp WqނQ2euP!p>p|LrT)W_v=R/(^8xMG% 8*?^gLoy3@NM\*%dEv%R1hgLTST\|a"3sG/v\%jbYyYRQb락C~#vf@Vx-v_vUL,3Țg~t.p{ >QGYmq!WJZ{ iocW5_|m;4/ry彣i}fչCWӻ]=V^x#;w8܉bg -&٣C~3^i񡆦+ըH47_s+ƃc{=l"+ńuQGyM9ܠ65PsGLǩ sAjiWOPpgmߩSN:whcF (mn`2K%' ƻ.f&/ K2*ܽ0zr/nf_v'E|L y·[?rE@;WLP‡1SV^QySd!eKK#hr@UNh2i4PJecF`8NȲߐ:Ӡ7S?>6%.doP4p Q}ur/ZdԕIf7<wđ%+Ζ~FoSZ]ةO7L,/8pv\^Fکl5F+ 4dH>.?Ut4bm&$ieDžh -*^<+e[itN&DЁq{ҷ=ИݙWͽ#_P<3<~@@zpUU% ઁXɏor$BbGg2ZâAf5iX"YIQ'SK6f5qAV >zC"YnPpJ15hdti짲3;S>4xLp4q^]&NٮwZpwJ ) ,ʄeuSYਲ਼AX&Ti*` `3yTZ㑬{D[&;Gbڌ fV],R^ {qj#ops-#L]lc*>]GhӪNoZ|&t,!Rv$:d2֪\L.׹ٶyX JD&W^\9A4Te ;FIz0z/MG=\^%Xn\/Ĺv:8p͎N)jFr1jj;4N`^hᵌa4\$*-̖he3y$FQa]_0++a3vg_? iሗm'-z7*=uyӔT$o4x +y$8EJ2rQLI\aR>1J&r **9VY;R j8Kߞ%b8* t_:}b}^:;ÔӺ:ǖo;nRy/jiߟT|4mϻkR'^qѫD{k?]7%Af@ ӍAt='Xނ:W~KۛkM~C$ךYȈјtwb>3ƥjΚ84Fى<}$~ݦO'sw='kb_RQ<~UI l2`l3TU:NhPd2i*}'[/ⳑ jOb}MU9-rˠG tZpyҢPuR!~fȧm",FxoUԄ'O>JU"vβkpnL#Zs ],I\-:W95PWT`jEo Ij2 ywPjt5 fjR˭i næ6O]tnѝ'>ɢ앗\Y9=t#KFBziӗ#$U}cNӠ8~dk*ZOy$Қ |IL&H3ݿd+r_\|,اk;}"L% U)%++-=T\rPG CO5醂䃧P56d=UΨ`M>SZ`o9L= (ˮ < O(upFt   ÛABRo׊Bʔ}KiVΦ,: 5R^Ltl"N$,:%߅H zu/3$j~  IDATi_P4ԶeB̨o׆(LP%L\NTޓ}Eˌuks5UW׷摆vĝ~} ryaIR ,=3nsw%)̸p6Ö]zk} F'_K7*hA,vhwƌ;ȁFO<]%W$4W^wԛ+7siv _=<΄kYVcYP 밎<7d$C+lL>J{Rg_%q%l?uٶ=Sn ;rvhŝ3ʆigS~j.۔wIp`n+9uH1]hlɞ]ܑP n1 u"EJ55BU.7wJLqE^w]CpM~s9tlvYIu~Ӗ=wȒ$ضOv;Ncu;fF+%M5zΊSi5&7Պj%!*l&+޵pKEa^}PݪmkX.z9qնa5K2K0 rx!<֎}fSה;SoHC?Xli67ؠMqiWi4aaeJ0LCMke<È~պ/\j%J$;1< am}v2͆u14={M ijz4FsC/^i QOnZ\-951ɖԛM' zOXy_\)c4nPKRՏN^/F#4|SƮɸc=xL3>y"uF\alq%n)fuU0yo/8|h/ ߼nZ {zO2f0u{ץ3Y~왵f4<2Vn}¢d59r&jڪqAz] ٙRڔY&{*]g68*4Kv_rEʩ{el)8cF>s]4.T jR6Xz~IոfߠѦ-hRR ɷ9lXrx?>.R\?FKh3#V&ۄ@{NiWs UE1 +ݷ$:16ZsFNӶhkgvΜCkm眘tP_jUޯ_?$GrQF)US7SM]!s \#PWX 5 ],+}M{fck#!oT= p!3 M nc E9K⦆ۃ]Ò@ 9hB(鬜I'[T]ygY!h${ M]X}c ol[7qxͨ/Cb(A$r1AeDKK ZK+j%.<7b0 Q R e&!c/(zݫLG4g335C@ d3 _d6MBFpwMGr@ D EMB PP$D+Ep١tXݶJ#G x<{;{ Ol @IlU-)Mq'_+B kH<$2r4bei i|BJHT8BiIͧb8A# EqS(@!/4&!ZntkܺƐр@ f@ Bi B^ʭY$O8NXYu#rgoBul2OMulIb[ "ǘ{5XT̙iN5?cZ_r0 >/%B2n;?$nRķ k6&fxFO5mmj4It&&*/&]\״dOt{-m_Mڮ]^S$?NV܍ v8ʺz9{8w\rao *P b!ZZ n{J]XhUܻj8p"C@GW4ʫzdݓ}; @Ks2f㫡 "P %?>'w̹p*.L!v >&f+GG& |+Jԁ>A J@򝷛ͣuk*g@HckHlX1f>WT*kuKϚnC&ޅ5RfHT ~Y O+E,Yoszy)5~x񝘅7#gT]ߺ(e|CK uyĤxgv7%\+nĚ H@.f8l.I(L{驘d)uʩ Wpt4ً̾%/-gv~C 7Cϊ. B2rsV P{yJPNEhE0_7_iٵ4ȲX&1Bm>]+o]+J!oڒ,ϫI~yPgK.Ij<"_B ikwٵ5 {H|V'>-/jWYgrN&ZqL HoR%קu}I Y?tva- T$v {{fkv{+竽cp݆Lƾrʞ̮cFth:fܮQ͸o>g?ߍXtdcEvCbun/rl='TC+6nzkN_<*P𑼚sݸo; ݾ :z2յ+ݠ"݅=kVuH<Q4`DOSEQ~@Yzj̡ y;pťKmxxih^mpOKec|,'jQt~%tIҕ+v Q'% {ȡ0#(7zLN$ό$-z7.RP]>)ahҝ3gR^6nӶ_0*rb%7XvHl tV2 NsЉ}S;rU]be)҈%ZZ!榤8ʲi鹟p(w.t4w>@ E$򀪼 010$cnɫ-($I2$I`ggy|j/nʠ,r1W8ϴsf4;kϻl3ti+!wO{#6勃3nO{05qK{$tj~ZMxZtiA.4koU۬ڛr.-Nݧ"QR 2s" oȚA7RpLcS^RT\:H)÷(Zᱭ'j3gUuuEEKzZ;I)ky2,pi Mbl6F*! 9NV rBYS+;~V$y|jq@V l?u̯ ֹ%a {6we}ؤ#څiot)yYOvWn3o'=WOhgVcfa0X}==]"c:]A`/ڗ~[Eu`@ێH=%o)07hI2Ti9ۮFN5բQ>eS_tyۻd*u#Yr^{t7ܽ_m^% -1p#ܩ||kׯwtC.m .ttla΀_J@j,b2ɭ1aygd՚ G]9kO5I|g:2L?\A8iu蟷ee!P3 +ozMݾ*"ka߳0MQ@rH⯮EDݟR*$ JD<=emȼnËrB^DXvm!f}xwe?(͛>|]qv h{j:36LQVxt48Zdh'l\?Ir&Sly "٫%Ie!O$HZJ m=Fj0])-mdQKGӴ4RpͲa6tȲ=۶q]=WQrrިUlgOpm:՝:gQsfm&V>;3ju#ɵtZh3$U n34z)p5Npm0֦4Mq82 R]/Uz ԢUp#_UKKrn6ʢ$(śI{{Q [^Z8sg\jcEWGZv@Ң%5<cbgeI$Q[\ Ѳ2byC} a2 X,$I&4hsߝT =%zE}*,e:xYsY<T+UidIf TmhiT.BN1okRL3 |c=Gߪi:i6UDX3:$Khqu[cK j_YHVGۺmYT g&QVJ}(k:tbI9o7~q_?SOMS$Z۶U=Lx AxٍBڴ^7Is$46ȬԒdz8{ǑA]mt EQIF &6G.9s;/x7-nV-izo[N1x9Y%ol5ȍylXFʌ#GYgЦ/iÜ#NMu[܊_[Z} X}a%]1尼(ڽ:O8v6W>:&43Y,zc&&n_۟v[g֓ÿy pEg-8$؛'jA3nx&&}18vĒ~^d魌I*  27;ԍC7(]=$Iq]؉ 弄?zmzN\1QÅ&OZ1w]p?[7$ Q3$i7y̲l"98!ui}f/X2֯_?{l{͙V>"{έzGERE!1321t͎؃s/ց3̀)u$Gdxֺ h@{a4ZSZ$LK6a ƹkVĘ WN0;{eT//_t  bG`Ɔc="&EEIʥR]*EǠ~_F,.$fzDņ0 -z<7vrϰt 9ӖLBiu8Y8d蟫gojW(U >ntٷ`JX08:lPhICWW΢ A`?hXW~WiWL SUū X1͙:}"ic꺱:ه0 ]:Y+]&a"fgp(8_/ _|8|=ɟ$r;z= ]f"%jd-mm0!9B $Ԍ6lq@!Ԅ8DXB!9pinXG,U _y -]]L< }%4n1 I2`҆͞B3 MB1M}vM`ҀB3 UmR5狉%+?~ m;t^yq}[y3/IDAT.ĝ0B!ih^mҵRaRUW|[qUl?`{D;R#-?gGQ,#~bV[7FĝKCyF'6R2&S r-l-Z0chN@;}0HߓnBo7?qǚNʏaO'e>Bo.^.W<6M5|"Ryɥ?&9w @Ks2f㫡 "P %?>'w̹p*.L!v >&f+GG& |+Jԁ>A J@򝷛ͣuk* DHckHl_XqO+*=Z'+zp62Ogjye;o0rbٽ*;+r4/)iمso D|tCiAeelX=kRy`~j@4^]6a]+gWM7uo++Nl. :8/=vإfx8#0wsq7:mvGNYG/O z閎?4DYC+>nDɨ闥 tRΒf0RLw߉Yx ;rAR&̷?$PJLڋwޙa{VlwS5bFpo47mͫMbX6$Y&޽TdJD:[TU+{:{fߒlM햳 C|!mnd?|֡gEWD~! 9<% k "v ]6e_s @mR{ ?w*4xoʸ o7dφoPr}Z_ՠ0=@Unܷn_|lP[M9-nm1Bm>]+o]+J!oڒ,ϫI~yPgK.Ij<"_B ikwٵ5 {H|Vv /:4N;Z7u/M au}<y wZ\N?6u{ڪ)%E5U,<=}ҏ勃3nO{05qK{$tj~ZMxZtiA.4koU۬ڛr.-Nݧ"QR 2s" oȚGZ)qktZs{#JJ JhˇǶqϜTե/iL2'лSah@{ I,H%Rz!GݼAN^(kje^b$Ob\m=}=Ȋ.!C:C{$,d1NC4S@wd_0mnmVQIٰvܿs?:}`}љG.f,m]=}-PVQ}(7OF6|AYɭٻrw ڴٌ*V$\.Ky,6ΩZJnTS-jS&=E7?;+@VR10b%;3˟SK;fNX DR*V vdX[;*;9uLzЀ8tО52e·v,g[~؄%~)9گ~HOr\\VKs8$_2LT)ej^uH"7QT(jTVJ}[kێ״ub/͊ %6<~wly3xǮo#*Jh7eV}9cǵb/sZWb9Q[@HZ7 u2I˛b)D4s4& `p9$d:Fj:%zտM#6f[G(h`xd9q{ռPmȫCbRT#_/=:[CVsP UG4 AL|]qv h{jjIu+{IwXZ:>Zdh'l\?Ir&Sly "٫%Ie!O$HZJ m=Fj0])-mdQKGӴ4RpnflU7jUiپ\c6ƶvuguwԜY۷UΌl=kHw.AfH$ Mf0H@Nlp4Mq^Pąk{g6iѐIUozKOO0\>cT`HJ,TWEs-n >ܭ7~q_?SOMS$Z۶U=Lx Awtik#nI0Hcм$c#BsSK&`NGu5U(E%Ffq0/x PGv╞ߴiv<(ؓ'~?eA_tse^16wcMݸO>~c@eڊ.}B\ =U嶉Yf+g%nxrfQz5{5Vmi;iGuf=9{@+ޛWx߂C}h-/16޻n*lwcM,eIXt lP+SӮ]=$Iq]x|x$ ~0pb̅+c'fz +gQ_ 4lGw[,]{h5F뎓f~#ّ{[z۞OշaLi$="ó%-DA9d㗆I+&ښa ƹkvxACieK68r36õ60-.JR.]:uR9,:J0bAtY&d9) j]j^8:.vI!1#".6Ą`0m㹱o|-KX%ɟdJۮ!x`I%WG\o>+{?VRu@Qf3 qNF̾S‚y`Fn͕UjTU+fusfN_H`:nG!LCGJA!h FXٙ3ʃ16!333""B7HKϧo|S{>pd'~3ި)Azx'tyvG{Ҳp<@y`@IO "fxq eff{h7^ H͜m@%ls`h>cšqgދ< B i"Mk5:0&=9~̧=7!0g@M{Ofg>f3PHJؘ3!JwFI)œ!MBU#FQELhjr333 Ba΀Ppz5c?[X>d-mm0!9B $Ԍ6lq@!Ԅ8ܴ6 !Bs200 Ba΀G0y@!œO I!B3 N/ Ba΀Ч`mB! 56&!Ba΀Z8B!9BI!B3 pXB!9BjB!`bR&Ipp0NS Bs8π47mMB!œ!6 !Bs>B!9B I!B3 N/ Ba΀Ч`mB! 56&!Ba΀Z8B!9BI!B3 pXB!9BjB! } &!Ba΀Pi`mB!  !B3 )XB!9B I!B3 B!4Eff&!B!ԛB!P3I!B!B!B3 B!0g@!Ba΀B!œ!B!9B!BsB! !B!B!BsB! !B!B!B mIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/pymeasure-managedwindow-resume.png0000644000175100001770000023155014623331163024656 0ustar00runnerdockerPNG  IHDR]DO pHYs  tIME%"W IDATxu\R% b؁݊gyvgw}gas֝cٚ.( !~ޯ{y<;;;gS x@~@~@~@~@~@~@~@~@~~}zD8C㛥}@djdODtp=>ReK$7>d!?e֭DԵk/]1,,,000ۛ1vgϞ={AS~y@ODŚvj]B$$r#04]SߘҶ^C =\7e3":Wkt6ZP}3kQɼi_LMWcI\@_+{J2VWmX\x~ɤXoY[;/=dV,o+S쥺{U^V_#Fщ>[9 ?9yV|~ttX"GsW:R]np ] /\HHt}"(ޱEt-0.$c}mP4zzdݎg"w=gd@ڸ. Zj)w>zHɒ%+VhzQڸQDDSQ[^v~ÚSoDD$um2[*tgzE-> mDDB-Zd"~[ub+7_O|"FJL+s\K e{DB>MV @o,X~!&)q'Du}2m"{Nx5;3vؑvڡm۶~ܹs Q*Txutt4YYY/_^VW[r'Fm&Zm_4!m H!.+[KLE,tC!=* ۋ]}z1cdzT)&jd6D $5$֦D1ץTUTbS7[B H[BO]r*e3U*|@.}>ީS/3Ҿ@DgΜR7 ;fDq= I*^{;f&1^5Z=cHЪuDDgŇ})T_8LM>5OMP r>]YYŻעgr6 #JB;ÅLkDD|RӎM6R<] wZO>֮]KD}5V˗_|IDEAAA/_8r&B4?(=ʻ +ǩSD&vV ӲWWN$;GGвSm"8ƭ| _ٗh!٭7 X~YӃwSZL$$u+Vj+ǡAJ[?7?\5xyzzVT8^`OOi%\[xjD&^ jWZ=ijV+^Hx玓OѻmUmSJҙG?޿jđcO4DVXs} 9ӓht߆Mݳ4U?}t_ ,"U)6*A )w;y΢T2msVMdDΪfr/+>>>/^Rᚇ5jD"777;;;^_y)83> zmK2%x|E7+TV:Ez^.*X*:,JUjUVsEdI#jժT*q(|5-eRX1Ai5jN0Q&K%"#"&4$Fψx"0&AzV+3@b$^s4 q*&RH(r1OLQr)o(ƟL.JŰLD^ٸT8Y6#AJTvu"^,˥WiTj@N:z 씐3ӨtԪxG(hc>18&|iVҩbRV'ĥN u族m#NjҬN$ԉj[]] |G~Y>W׸@~@~7+++,4D ,E Sv@~*_@~AYvEӸ῟cK9snXh>oۯ}}̓%\HsEqVufͿ}|a_eO0ehy+BP/]YWܕIiTt6.îܖMR&& 1i֡K]/46NS}רu:y:#/Pg {n"՝Yݦ&b=z[u[9\^eF^E,o?ϻƱ.:8,NG2ےUنBкoloàesjHlU~%1cۯY?%LYϽ 4k$CZ5o;u஼½ii㧴qkC7/];L/RK:qRSQ7~_fϕ7jNnL҉!^"戈xWy""Nn*7{bCi%EPDL ôj8TjVoʗVFQ+1Le}D]$."?⸏]O͘qb}̈́/,ӽ-vRHYGx)ڗG}siLZ?ƟŇ_$/3bȦմ~)ps"Aڂ+^.߸ki\|3_JKZJ1g`w>%"rno+,)NeHLo H3YW8ps3K]k*sr "&d?׸iUDdX&K)ncI mߺأCYzmeձ]kޘjҢDfn:Μ-^vñ"u*[ bǂQIdUᘱD83K9*h4kxytv\),bR dui㕽F޺9FCq,ppVbฟ۱g |zhC_Dbǁ5+L J_ߖׯS7LN r_3k?hZr{U˱c>D gTTfeq!uI6*nֹsˊbD8}9Eʁ'e.0pr_^ZiҡE!Ww-^;|偵\%Y۳: =GgYagOo x1͈#[аuj|䌽40tMnE>ihYgq2Nq7.pi;!¸~; \|kڧ[dռC?:1lؿc𥤞~,F>q[.ZTAEsn \,H{{u'y}iUX~~d䕇F BU:M2zjSjJۣuvp7l݇&SȮӦL_V3b"R?UlЦ2~;Ϛs_˕ |.VK2F:aO*9?wrbb"bw Zzp_珨g'Z$F$0$%䑼 Rҵtǥ.(vlwPl~ }آxqB/L`D L0!0ƥ c>+~o([k :=I)EDÓowkk-fXWdWcg/8red.<~(r]>zi'Uчze\R&їO'.J&"ݥKM -!,nb\ώ."!Q+U~z>YM鷧܍˷5#!F%WvA]o/4Wa&l{m}q{uܯ;R\fgV;t?>_VcF,Jlwiq^ ~up|7".6BĄ4343F{WoI>wd/!}N-qzm}͌:|#Ɛ ˙KFVmopqIҾ8Ƚ~x"~:rX(sPT!{PލT7>U$/55"Y|JJ|$ihs͝icXy&Z=yK$,ULT5B'G$.˚Myҹ>pnRD){F:0Nj h8z+k]NsjROFn9M-[g?;e̢r17,]ȶ 1M׬۾O=1Fιu=Zr{գnڕj7CXW ,?S1K.m;OhkTw:轸ߤF;ͯWr,1"F%' "҅Y/k6lfe[V,*ŅeswGU;3&$J &"esxbpDDmlZޜ7^%\ԲT}ϵj@f YwfuK~TrV؎OnƟ3wtr5BNxύwz,AO(KHHGhKER:6Vn?YBҥY}.,g{7kߏyUxHq"o Rwxx| +lk6:2ҭNDҒ班hi_^gD11&3n@u NrrFrS;wtePfԑc) ݮ1nEr"NVvش~M4My,eb*MLMD,(&A "?h_9nA ""o{fu"&rS`!0NlX<1IDD'1AzF"H "&LMEI \r c,JgAc'AH`G$KMk DK^K '/nDR Pe z  1ZdZ=BPv_2Z:‘s nTy璻捞޶3n jqѩm[V4~%Tĩ`ƃ&\W ]RkwGܲpeӭvNQiZ6ˈWW$Eu4({~]M%_-RA?Oڎ_ 2IIqg*ti#Ugv5\|~^r`.<;2-f!Jy&Nml&\2F 2S3Di%/i`Ǒק0 MIR7ˤ;OuEED,>iVfz:V_ԆO-X$nm{m؄[tBxɖ kĹLM\M=7N2D6%[LY" F IDAT3fжKK.f(v'pTQ|V[ҭ.J`,*ܾcڽ'm GLn&i?.&I+' d_BN4m)8QfS/GƘ.{1p>ȆBI[qS@xF(e{֓}Y)Mܾtt7Mz_etS-.PH`L04 v5k71C1NI9*bwc2)\R:tc$=|0W8VrrC~ln/E ʛ۽i7P ˁK7\ oY~)q.]do Ƥ/@Z̝gq(^yXutc>5E~k=!EF ,e.)w@DD&t͟+K"oh?>"NM3Rq3Úˈء_n:_;#b-Mڕm=_E ^ ƈܷt@&;Xod)y:$ a8oq$qm>z5/c4bYDֲ}堭+=iRהv2>E]1l+eޯ~ajD^o& \shBi5KXs7H}S8"oƢSYĤ 4ʉ3':v/Pzߑf;/]bYȟz6.}XIjLq]}_/[K~8cwܩ_>>n dTN_ )weh >E J,UЂq:".eq.u݌ȍ34'>r猿qG˥c= ˓^]y+=E2#}}B(>ѦN>L[죥vX2Mj? 5'_^}Gt$2+PwpGO9~KX0i6"-Lc /kn?.|8 ?@.nŘ9r>!˹r;_#0+.ǹܳp ?w!r s&}0'\\V,Ÿt?kUe t;WسXK=l1IcVglX&}pb r;_YO?NȦgN`'w_gAڷiؽoZ>R_p/o.0r$7>!> YK_!> ]޴[IV4nӸOAͣ]>>olvMj۴W:J9 -נ '%\ӽ_x K#VPw"a@ny{n5GkZWJD,Y[O?ܪw2ȷ>dGs,{αrIZVriӑLisDaI4u6ޟYd:sF-x}ge~E-'$VCbK}G:r"6Ml,/ZVL׷.Y$Mƽ QݘmS/Gb-u3g W6Il\n1=Tƶ`!e8,qm[~yVץ뗎xu,ѿrǾj7.iJkˣ^ص״D_<=3ĉMLe<'VZXXsBx ~k;VvٵY3ϓ::.XPKw7bpIv~WD"S &PCw]ۡeD߫?H+M\<)Di$x+SEI.%HRu)D%c^'YUt31 B[z+"  U%ArDDqYǛ(QsмF|}6[ 4T@DTnG}=Wި92I'/dr:Iarn^V b8"& kZi\g˕Rn%#/b@"$-q'qakƗ5IyHb ID̥~3i͢.& ΐ#ĮszxGwrq7 R r׀{E/ mo]dSUaGŽUG!0;O-n[c1Ov>p{~Xo5t+^{džl,|'XqlxG~3*" .鋞OV'Y>D:1AcV+ŖVBӷrdݶ9X i[ɉxϩ˒vY[b(wI*^-KDR'/7w-ou.>5cnZwϺ" Kֽch例ro=N(`S~,8/nʈߔ5g>cnCꤎZNEQ^uBXfZ>6>XvJXѵҝW?Zk"ׯ }=|n֪/֮5o5ImTi&]gUc3/tجP=:DD$vi4Yǜ{7yGK’ճRK65[a9>EŊKWn'RGU;WBAC/ : 8r-SSn]2i*T>_+:% EL*ѠAo麐zY;'1ى3'ڷi^ȋoBxyzY[Ymȵ""#=~7^*hyC._zEb%,-4 \y'?@y8~)>!JV._ŅAKC}PQةz8vi%㈈1c}GE?`ȥ~Ap=@)*T>/qW8=X*;?vLou!9pPdwrst}nrM\5ޖa;GZѰO|U7'֫z mnjLJnߐs1ק7i=z"!&]Vcşը_Ts 9YhroXɕO톾]~|9\=E,yFvݠQw hjp>;H9!mC.e95aֶV;xɅK^Ϯ \2=8|%aSLҵ@qF9j}|a0"Ʋ7 "0rPT8 7K߭8#͓ -VyHsqC#NZvޅE n\m4 ;m{PŰ%o{j˙ۊH`,^1 ߂?4Q`]Zf/=;FlټOM_}9\Z9x=}C[4n{zcCήעOc߮cLݰƒ{nԧY {S?uV!>CXaW4)cdĽ ВGٳ ʇ%=?xy`1ƴaOkԧIank!i:4nZǟ' o?pp;ƭ{?ʺ]7na®‡Vm]J{al6KT?ԩȀH}t膝2ٯiS6 {|x~6M}|;gV"҅_}l>[w~0(Q`}Mro#K"#e"mWv*$ćgc VlQ [+ko|a`ﱳjZE2hL2r7h=#^rxҎ }nZ^>B$Guwʉ{/uǺu\-= \D|f͂kJmm{wzNYЭT< bĘ $KtZ=Id$:Vs"G!?;ui_-Z_a&O=h5xŅW>!&ڰ3'mxzHdI~mxs%:imR{o_vƲi1"}NՎzƌ[+0fH0S_.*pwPj^!Gڳ]QF$vˈN"rNWyMeYLx !4rz-F1FoM떴PU> C ȽzP2A^ڸSbHbb%]5:q"1'G$3Wp:UR˧ 1T<Р0ޡԩ4\cˈE Hy'&q2D_u!g_/4ru3<̈ROq (^JY# 9{|5 +BBBl,,ml N\Ȯټ]|ܚI[wYcx9ʘR+t;_);7:vy6?$i _w.ṂYm17kW,,p7:"u)􄠮<;e2Y()|۲YSgʢ'|Yc[0__wٵ#DΌ5a(d땑ذz}SK gSO;.17aa%Gmނ}MӟI6-o}ILCqG{i %=8hP 4v^^(KeH~g6N(=s,w^|ZUS -995~o-OD]w}ޚVK5ΞxԤC灷,۬n!^)kDB't8u]ѴPۃ[&D2 ~׶W9"\sgغY>XrrPٓ[߼It/VIMs.i;|wtXiشqv`GM ʸoݪsnKj+CS|]#_#|~HdSŔ͜E|(xF$vcg,\\`ΐa_.ݵpo[azv{R^Le{dKJ4Sa㭪b톱93ڽ iv9>ui9XeKD1Kv6˷hQ7H=sZb_aUq5L5 }W>m"&hР׭{eFTܯ.!'Μhߦ}sXTe IDATiϞ.VF]v|Y_^*xnyc}ש_Ygg(͹ꄄI]N4Ûr|܏㸴oY}GO神sT>|aW 9Fd|^NοHB߶!|G|rSɷcاf+=uo~5,uk^ QIlJ59ԷO$޿z͑;aS6.l¯\(@N{/3"C^?NC RJIeʖ-qD2N**?p9yC`w~_Iɾ^\j EZvޡ:&ղ|{|͆-_RzfsCf?>bƒ<}<..4O\]\_0Rr$uRLlL|Ǭ|[?UiѾaǏ$r&<͠Aot\|$ #;O{) 4}ZEr߾_󼳓s~8xr<6*)?dQDjh23SEt$H Lr3SE Rn{/73UJO9Rq犏*'%\1~막8#Eba0sDX\%k[ӿEM=?~ࡗo-{}҈C{JB8G_qc5k-.5vWWg1vK*ݳc@-]*D+5taLM?!iDyQOعwa{_KwĸeEx%:ɛv,|WvwӨ\W%8JKHžWf!98<KBAf/}S'Z]שAH" !d 0,%i?ewRGʱ"EqDmp/U@~8CɁEb8s;T5,!΃^x;S޿cһL!q]_j%-"=)%I!S17~bۢ$KH}K!q{_CV1ZIJŭndm9ԯ^P)"~{hqϨN AB(]a#K~ۇ{4K2/ؒ4BxSzPK>f{"5Tggm[]jќ&wW,! :3{M 2~ް~ӧR~C9SL!M6n,07Ia,[=ߵKL{|"! V><5o0wj%k~Ov g`J7(B[v'FuNBDxŻߙ斈/k-s{n/RV&}VP!QNO0 TP/ QZRbw*Jlީϼ;!쪭vY7@ ~_g-]!X9ÕQݮNp)RWG9N|1n ٫3T[]Bܰ҅ȐX 坐.|y(pw% *e%5!dH|ΐ!gkY,8pJx*2BMK =ij|,9YYYťŬ ݰؒL&*SׁĀ\e@eTg%wddf(/HJHu_}Rv_7uDO'[/5gZY--YYYPH>G/9?'$&9eYe@R>HRBb/N1Xk5}Wl{T0Z~$TXXP r{zR_G%HiT̾2Cχ2@~p՚5#Z< ;PEAE UϢ([%G=\+8/Ȕ0x9©jFt_!@-!*.((`KRBCCO[FA UA 큍܃WN?_ri ꌺ֘n7*WV/Dܞ.өTElAH/yjʊ$RLyeΎ6>:~fFW)ԝ37aj#<) Oy/0fLrSIJPGwJ03ox?.DHMWLYDKûM[?|~wƬ^]lMjqw-3B/#Lk{Th8?~(>^H$ "l\q{j{#: 1IIr=c]/XVQn;~p,E|rk%%;0PI ^lh_ DP/8W`3D@HϦ@xy^,* Ni׶Q Qa.p*_%Ǘ8sllʜ0ur&$Id1M.(#$!LfK3$׬D^S_Fy˞˳-^{V}0*K /F{hB2VKd J JcriA%4f:!X#²gȥ"NY$!IIBSO;MW.[~/-Wn`[PK<KyпTYff뇔-ϏF 6 }bHhujy_־bVnlhɶArLKAw\g Ttz}^ WWDFF NgLLLbb"P뉢(vpx"?_`HpxXhB_`: O6#H1N\6 M~v5HUpf F;V&0R-rW@of4Ny]嬑:[Yq/0u)@5q8Cz~ve4ޅ=CS<zKWόQW<@}L>]W\n;_FA@jW:rUo*+Bv21ƪeHD)Y.@0%ͣ:Frň0Z_˪bQ[AN3"9+gO9rdJ\Ռޝol:`ݷhdMox}Us _4}fƼ'?zUːJ_=?~oP"h٦{+B(%{͚яu~ ~`۷3f|9D!( w|yϜD?o/5J57@ 0u8Na 2 !cܛO񖖻>yq!m3~ʣ^+m,Mһ|TgWgu|l?ߝSi;Heߍ9$>DlzQ-4@Rrx='9~g^[/WU/=uӦBHQ]!8Ӵk蟷)P !,-&M{$ -9y Zq&!#B8W,X:䵛z7mo ^K˻_h /zk|zCN.&\׌c;vGdrx+"~gL$7veW/VY/6iT ! !h<DZKz8Ri+N/;?ErBXR7vaJѢ kHQ!c-7l ťywDuhq0жSeGB,&[Hdv Byꈯ)o~:`?nXwуޝyѮn (--DQjx!KΧT\U+Ow[੷曃,_WWԻeڼ!-cL?x3UNY1dNlS8_vq?,$7b<^޿XPt*4rܒ]zssu. . *A k{3ɯ5x&28.3g~/zs_≿$As#:yM3V/y]td}G Yb`JvL}jj&KGd5U/"'}4?/?BժJ<ܩ ^v[%zC|<Ǟ녏_JעbR|0ߵw_Mx~CٻeY(2; 5(h|,ݚB ݠ!=NmҜ%B\ו?~(!?^Pc!n:?Z tM7??/_!}j'PnK&!K8D9eMkXF5i2$Ý'hPӘ#CF_O N IDATF .A SFAQ 00A@~пrA0o!D#hF_!tѿ8KWo)[ۣIi]O/!btuq+q1 %>r?@38廂: 9r@k֥7OM7|_KtKn;[z5i(\l  &/9mݞ钶uM͙ѽGM%F6ixɡ"MvB'Ĵ5U/m} I?9-=,XZCS[FH K5I=\z GqsM6) Q/ywk\9'^ө,Σ+_c&}\Hp<~eZ><![{o?i9;J:Hmz'ߚ>Vtҿ%ǁzvi9b^IfWgeR+--n~tlsBSh`؊Gn_f/?c?ڔ!P23eYYhk c~ah]M_!ZO.5p☋[7i;G'd.\.Pv|;=xWoޠyV-ZjѢUak[oq4m;o~h鷙%Ba׹!8̿'N0MYb7rT/~eʒ:w<1M)oX)17aꗛre!*6WB05hYsw<⍧<3kf/uUXY&[5ZU>m&ndݷxgQέJ%EG׾<ɷ^uA@<2>!}_HQ͛u݅\rt¼-s~eONdG-ޑaR E)J/bh_$qE6˦NvBMMiP9qz;)m{ !hڴg[z]4nR3U!S"Dd='2?O_`N}wR[@\1/*9鬞# _Y̿Bė_wPUw4Kj4d;ʙ}yψgXZ|z{=zY!秬2-lvB_3pBy*؞ݒ­8b&'FC6[\WΣ[w4I*g98:1X8/X-Ӹ!A!e/4Gс盟Qn:ὕ[ ;9yN[&, +}IeVJߘT/fkFt/~E}wlphs&y$9oX݇v Oy<[/xM tSBXJ]C"MU d׾~ނ_aCmbpX?|y\xtfI+Ԟ=# p3nlǀ3JՠvgRAWxJE ;рtr%9 we/Ah%@a%7kLJy5'=<~/~NAąjah*?m[i"#p<''p -m>qR5NKa~8#8?燆&t󃃃=%+@ie~_JO<} ??5'$'z"/R!w=5ʚ^_ Y)9Fܗ6-)!)::t"/ go WN3&&&11)\DQp8<|^HBðh.3~Z.dddhqĚ::BkX%>@8tRs D~΋%it%T!KpcB^`,p€[}Fewڛ{ "#\ff뇔 ?@i4=\4*46-_Jѿ@ZK# zUH0E/iГjA*,*@MxOCBпFq:( P)@uѿ@οDT!K8t!X ަۦ (R ; ̿xK~*j])HIG\ ?EAVM /^$vc6^maENI V iEQs(Sݒ$Uxl<_WFZ4k)d0NxBDǴlræ ]8 ;Ќ4N$qb;g!_¢蒒̮]{;cbb٬UXX-˲q*Qi֢ɧuo{}qHpEFRN 3VsB2d.槦 jV$>CZ1@s#&)F ~v ccfMJUN+" IQ3*" P oKE=yva#wz<w\4yαIt$?Ȳ\%C]`2Tݿn♯}ξ-C,e` n)Ej9K!}=v}׿ݖ楣_0۵Uq.WuiToO*dFFO۵u9ت=KX*V^ݜ银>&6U-PgTEH]8DEQL>3{5wײ722-nOwX3FDWRoL!uqd};ZnWճ]z}x沃oxJEQϥ]ؚE,qu7jJI aW+R_R(Op~/OߛKJ<5#MvإޘCY`C(ׯNfKzʊ\G.bbu=%Fk`7$w/MM͖pѤowmwgD-Uڬͅ(J"fE4Y\8_2iX4rЕ/u!t/84!Rq|$uRoozoVq}.])Q)!Yv_3v{ދkkrWrˮʥ}9eʒƯzoN>CBT|!ÃM\瓱^Mn敯\恴T"+rreP_6;{gv=5wԌ?sW?>%͞X Ǜ߻xY8~_%_cүaufeΥgꛏ=ʧy |JSsL5V~0 qّSE~ ~ 6muk*V|j[+շ.-c使=pWkqbW<&᪉]DO^^vwiVAG|蛯ٸOkEZt;汷<{':d΂ob-J47zl{6mkJo={Uu?ڈ?>D-}TWDl5 ,If|lPÛbl~IeY"{!+Bw=*$ksVcWHp6yW(EV(ێ2nu-w8!Ǻm8_w"gBk[_\fܙ--'>JiQqXibCy ROS5I("fIQE 29Jl>>u1jQqi<e"RKJ wktxHf/1 \ŝȺQ"s[NIw!16IVPAB";Kˉ3k\YCs?U*Yv:sǟ^ǒ985ͅ;Őea&}ߺfEep¯i]*>98:6??,]oLn#ʋ"U!+RyԒ˖VS9&<0HR }55퓸f?{Ot}>zemA+*>u>[ nެFI5 !edhqw5Xvj;zmQH ,8ȩ(!d]bk'X`:e"+NKE;EԩHuۚ%DŊN|0 $KTVmztzCnsA{Z6蘔ECزCj;|b'g}ԯl)1?VTLB;\]]'%^-o7eoK~7nwbcOo_ݬ?MʫsS D@+o_|Hsmߖ1 ?~_uFkaA?[jPⱵ[Y(wQL1ݮpD9I:XG<4iJ[rI𖸦_ТΓ.j:gKy6]վFO:g >^qkK54畩1olڴM^{{uNdd렣MϾITi_RDW/[Ԯݒ=yEDZ]zkNتGMϼ~lǚUk=Z,]0r% vOɥ $Q>1pb)W _zcםҢȨVE F5b׌yӟ sNB".f嫏gf]_mÄϿ3B(ѕO|0?"NK.osV?Tn?(׌ ?ksu>q+z0&)%e _69OX/ַ_y>zט9uzz͞XT&\?T}(JB"翟vS +,m.PwW%!N#'vr^] ܻzq#E IK %bՔt"ԕp*ܽA!)B(=uͼ}hY}Hя?Ç !dY =o=LQ]o?_6v#&MqS~[X851컜a'ߥ3G_s][[߬vuÀkڷxzSBde/jmڃ!jG^S҇NdIUsIa?_XϞcupwۺ=/ӰM"6[36gYwZbU=iR4!}{N7U;_Q(So\][ݺ&-?h_RVףaΟܻ_~y{WPۏ*F!uCD Ж}N[_}H%A;&(:=?tzֿT)2g3R܀]īSNg?mm`'~ \Su=_NsKVE-v("|9NN:۳/_1 5_:g Uqx n{m'Q5f1Cu%iMlpisT!zrtrEO!E\F&!zvѲ-%MNyښ5k*~ܹ3\@uƩ@!((00 ^zׯiYQnV9Qo|fELEό$I>YsO; ! lmK΢"j|.Lv=kV(u(aiY*mP&sY]ϐ,!b"N!3pS*s g폌2-.)>~x:yrRbR䐐#lpYL?/X88exѼruKFȄ}dI8sᕿxh˟q&( N77ϢN@!*2Ja&%&EEFUg!ٷ_qI-ЖX,Ow,Y֮)ѱ{N)&YBypR ~_&欵8j5]0#'\SeH";Mt%%&L^eaY8._/6BQzxۆJ%6nb{q`ς~Y"%;mIP[ XjRpu'>!nfh@,*m%ز}c0[&A_zP/in'USSE\W<#E+[f*tdA0~@U &ͿT ݭnK'17`舉E5GǙZu U5W'q1ToK!]N}y즋eShfC]' A~I)=q8k[U^ ty KP#}R3Zio^%  F#΍QsSe ԿKK%f*2|222׸:$B:g\ȉrENV P8ZPu_[ߗʏr*0I ?B{.s|ꃦ0։7p{3U%L\7to/ٳ~bގS lr՝W5 d$sV`\ &4:KK~\U Bq޾vkI1ʬԲ4!(>L#6zG~JqMJ.A?4Ld<W=Փ!_Jm4:&N /6k @x4:B/ YI)='l4໢Q94N|{YOy(w:& 3*6d IDAT}}kAd Hb/)n1!|߿d Ӥ -CRwuʭlc81F_"?@п$߱m[)9oZj ?Pw_ W#]Ÿw_,B,"hܻSxTϷ}(탽y[}xc0nlBצߵUit%T!_s׿}mVciKXj .9w"S\Kf]k׽Ė++ժK(3+ &?̿To!̹sȜS#KGZ-jp !|ۿ1| pT/A ͿTŋMS4<%:BϿbRRRX4 1lNl-NTaӤcQ*j*6'f*N*=m}yrԖ KewuaXM}ჾp։IqD0@_JO@vK+&<`pkb*%SPtRU5ENllE5QCvɚ>Dl>wʎ b# ^xu+U \0}!|߿uӂvpbD$i8;zFToKĺ!}1yn۴vuZ71 U2K1)̿xDцÄ?i6hsY%F Gt=/ABp9[3탄Ba{{0P)/9+mеaP];˒T//R.[ܭ K[?&Gc71!|߿coQgVnB }1w,y+yF~Ps?@ֿ$L}g\5ݚ6hu Kv? SJ*\ n熇`h!B_/ڼ%UU`I 4:/ð#gWͫ>оOFa%-so'Nd@UWZQ: MP)?̿rѴԉ{/fE/}t`q\.NcCیo {4:Ɣ5( :4BVp"@~mĖAg^mXѩ.~2\2L>ojK.cLúm{|%O{i p4T!9y-;o?_0-nulƽ̊[Auv4'=}Q%Tצ<l\5RR~|hG.KK0K{ř+?O!Uj_@M}̿x=B/ɇs?uw܌#`p:B(qm'ymnZvyȚw[Fin6`(BZܰN~`Ս^sӊol澯N׳F&9\$ !3?;W K Ͷ 6D|P# @IFu1M~I))0m` 1zϯ GMHu7_ݬY+7=k݊My-R"L™YZȥ="}R`ZuDD4oa_RgCǎ}islǁ=]E86TSt$&@R~)G:vzi 0%@ۄV}R[95Uu;_Kƍg[\Q?@ֿ$,عcM)=^Z;n<WrAV0U- ꬡ /ޏKBa m訦zha@s8slǁ= ^eЪ&!BQP#pRj_ 7,я@4:BݪOj>c'tw|nإ٫ j5(\R{Ks8jVoֿmTx`%BпD9Ӯi}P]/;jށ1b0_^Ŵh)dx;K &tZT!- %-1HKh R ;,Y>E*  F_"?@BпT3dں,[ ?@v>~~@W/D`P+_# 8GPq-K?ڋXt ::VSIxKP)~gʭV4:Pu/2$ut%rh7R ;z o΂o Fxwi1I!_.43@5jjy \5_'ZKpBuB:*ktMKP)4oӺhf|K:BZDs`|6`B^垇+zX(@ ]̿xK:r(UӤ vk'6 %.iOesigyg8٧a[g6I8L{1+TտT_H;$)njdE*2&6_"?@BпT.33CJJ K2ɰ|6eςk"el~| *EiRiD3j2BtצW_|S=X*njk/ /ABпBM΂@UۆR @* yS: .s-}wuήJ\Cݏ9t00R/Au3ΦwT`aڈ%JVt7lEA-fgP):vtde /D*޹G,Gt%:BPe$m%m\[Y`oJR ;&IGIпD xm'de%BпۻfU 2ʏ>n6Tabzه0M/AljZVMo+o `_# Mځr0TaBױxX@p nѿ v1T-K:B@p b F*0:tQk4:OC\KeSiðo6B gLGAc J:GM_Jѿ=,!֣lo`*EUH'J,,gh0GAtp4WĿe((%ajx̲1M/i)W_XUGAbePSqSWo+@~Nc%${i׋m9S#k-7οD~J:$GAqBXÔlYF|@9#0@RZhW,V5*J Je-Ԧ>1 TX/IRP"W}85kiL0RHQ _QtGC 87/^KhkaαR!܃yRxB@h%k|Vw΁Hn]/?w̿xq_" W~łe;mmJ|Pu_vu{V9P=R_ ?2R0"_!wqQlmltHJvb"kwu`__ T,kDE%uٙ=3ϜgfG%҄%EK!??φ/~ APT6mڄ (v?DDK KPׯ_#<"<" N!?!_-YWw#x6 @qR%D\A)/ (,< @ ~ : _*EKמ|HWpumܚ4v1SJi?~3C|DѶ^IkFoRdl- թ(edz?:~ B Z֝ob3l=;hRDz ٔĩnӺv @14k_%f3mj^m4Y/Ӟɗ]|s# /bhGN:5iR?ڈ__>0Ӳa.$9w#JE#֩ݾ] WGA"BdQ7݌7hץcc˯ ,Rw\CY;Lj^y[6S()ٺsUY//?KPLh֨ǀ}U_H2Lu`ˮcwBWN{|1vh޹KZZﯝ#J0#R=w{ 掞^7pT㩖Sv]X[TỲN 72sNL+שW1({jHPtrlOzʳҪѻyE3.!n-mj֫:.9p[gDJ+;Yy"GgcD ȳ!;m.Cg MVfk\b2Ò~u APTz4FX[U5)4SVB GUJq58D&ѸP61OCҴ볙d𸹁p,KDs"y3mͦS Lp6 IDATuyD)2CԈ[W/u*3ӳ҂[qiX7jzoJ˦* DT =ȥ9+z!)|\(3Z14uY^]_!(U; (ø4c$ms=;mK!"_]]&95\Tb0͑ׯS "_ps@' "3!7Y#O?vɷh)AR sg$PCԝS_6ؤ*,>W"#H͠xh</hW5_߸zuU>RIO~qW]7;)!!!!!1Mp+T}}+4*!&"fU+ Yhȗ)} 3E/>Gu-LQNooƳq,Cߡs<)޼*I,Ɉyq74S/ÔM&4(e2Liai8_ad\g7%*;@lF}J;;;-(u$w|]ͩw^իիhDx|(Y捪rp )M'Ó3b^J4`gfMx~ÐY54seuώ}"ѠZ53 ȗ72&/{QfZǻ>b Ei/zuСa,R`s Ž T;NשָEm3 DF?&ذǏ^JYa(QE== gxy׵,9/*T% KJ쓼ѹt>]|󂀀___%߽ ;h`&-AAA%yw/BBjB< a@a~ @R%?i%EK!???- _@Cv@PX_#2^+@# _ߋ݆\iO ZӨƐ2J|+ɸUY2Rԓ>2ђ$ ]z+8Oܽ렁/PS_[f d_\{l"<:( &QGTuhBJƌY7YQMQȭ0+Iyߛ² ;>N(kշShB踽N#wp63ҵk3z2fPR%M9xqF8g 4s7U_-gB85Il v!;Mx"Q=) 3rg+ɝx<9`0fTPZAA!YR:L[#p r?ڲ}Tl-jw#1!c}DƖզ]~4SW.4Ӓ&$X$y{;+;4OY?s1JBעo tj'C_?,uF?36EQEw<6^JnEqvjw4fc`oS6\Cxz֎< ;[Nrpl`:zxg3Fm26z1IDRtӰ9atdgJYI%*}|x4Eנ p;@Ma@ !5%EK!0Ph)A´BK~ KB/ !L;%:VgD#FPC#(&/#I'y}Lk`"F|{&e/NY˵Aa$фEcvպ߱01wscǔ<<\Nc[3Wk9C2-w'&h8vw7Ɏ<ׯu-m,͓teŹ]=J !LSK{B褳}=\?i6 ?wX ]=:]!}J/WAdWǂyu߭_|6Z]Os APF f7_A@!}zbwyU/ܧeCk8uzE_YtXm;2> z5rc=;aqӱ[OugݘVW:9UD!ˋGl`FlՈ5]7Μ3Ӣ: G KFcp!x>2|aqbmm)6adaFrjV):gIw;r'yֹRrx<ыD?kך^=݂b$7#5Ff("}Z{eUġm,۲~%͹s1QלMrO?Kn1=߯hoKBQ&?}ձI%] ۲ך߉;Bů`_)_#jr]_]͓.tv뽴Ay X][(,0l6ncǣ])_5%uC>ޠi㊼%V,B[8FE3 [KO"ٯh1&֨uN`ieύ89Ci9c7&m x g/v5)BQ&hѠYaOl"2oC棝"wQB(ҏVnP~ @mBcy1%|5ٞkPt򻗉 SgA/SE؉KM0<7_iצ/pN+Sp₦M; JdS8jd&"P߫@M~ @(5M?zf^bg4_q{L=؀Elaͪ qswW4˻vL~Kߡ>Zw||u8=x6ɼ9^3EV|I(=J?wќ6>ovtͮԎkZɮnfEy4,[Bqt2ϸeWٞ5U_Z:v}|J;GTI}+9:f,qb!("URO@=0% :;]JzcunKS!3q |sm*,YL""znJ->]l}~t U ydJ-sUFꋈ4wR!j?s,9\Za޸iQήkm"Ҧm5DT4g}H0fhZ8lFmxi7rMsd;vkk^SAȡLMclunP7\cc>eeddJ -Ȓф6[ă[guv7$ɗָuQaa RFJ]wqȝ+SG% [s$oӤ]ݏJMz}m'5ٯNo߰z7am[1{÷B^\ݽ`m\] ^оƵ'BRsavNt;W9ԥ WjG5|J{c&M8i6c=tP%YDv6j}PS_P7,]禖G~\Yuw4toÉvH}>;Lqtt̂+G]0e]4:6xܕU+pm zTUݔSܵ)"#4M!Ҫ=eTΒm#ϸ| |>>i{P|ϳtJYzU;YӊK7{e+i>5aކᵴUjV8u|Yкӻ[rHVb;DsJ^&޶myʌoO瞱j̵Pn@9p쓼_%rO (1:Ȏ+=0Ph)A´BK~ KB/ !L; (,/!BBҨ_ѣ0'@a??- x´BK~ KB/|4)}}gG1P~~~-@~/ZpK_#%X%X%X%X%)뛭KKKK2E9p쓼q eB~,,,,)K%((?g*^X%X%X%X%y N> !) !L; (,/ (Z AP0Ph)A´BK~ KB/ !L; (,/!BB/1 #CЄ1EQSFP?/;⒒9%%'BL* 8N@}`FCU뉉q欣#Ctu>ZaApg))qX"ӗJ$h4MrAp@Ҫ_RU2 SWȚH)^pmpTcJVS@N AJ\DaհB Ap.8χ5UZ_@Q]*_m;J.sZV%:"~Ci72J:;bcC%!~Ipجg2p[P_eK(%Rԙ>&+ܶ$ W׺G~:0 ə" H*7-eilB!أ#v[oϑ=]rߌ)b #}oRDPvn}}K^^Jqcđ8xLc޲`ߺ|\^AI AJ\DE-.j1K*Po^8{QRwRVЉ`Ѫ ;IfT[~˯x=&LELBEeyIkC+8m29]{U5R%5E Jux=0m̎jk%xu=`!#TmΚpT=PEV$FOP"8xz%D2oo$ ۖfi`!L+^zJkZ5jDˈZE_,kǁW5ӧ/ 5tǒzڟ>g TQvC0NIq3$S!yx1Opc >Mu*Uf_~y͞ ]27bܻS]S(HI2|s˖z=V-`AB*jp~W/)Æ{nu(hȞ-r No,:,:]F8zMMܜW^SS@^@'uO{X!ġjwn{Twș'=n4 çli#tڣu f;x8~s.<]g.:u(rF"S"CyY44X u(zy_IL~'7_2ڇ3_̴mI}-7+"*iE꫰,6&ߛ)is1so\pg[+iZum"P*//bp>aB!ifC_̙{:9 ki#04LjwCo߶eg ϴv׬XF=Iޭ>.nk|[qnvy-≕rԞ{!_G cB>aWW/MaEיz.vڄiBr/gGDKWLNWP&k]|F"-*Ki)S(?aT;IŸRQA IDAT$)hF5kݗ\BD|mP<>aRJC[`+TVLC MY@}XV0NܣMёa=Q3bL&")_lBLn0g(꤁PrLYZϘ V_*ΰֶukjִ{`k]ﺠJ^UEr&E;a&04ZyiēTEάe7#K5qlSqV,=6}cW ;E/E68ÔINA: !V*4aQ$g&fhqv0ojjn5O*Ttฉ7|`T08>;tŗטhU(x >Q&%cL]\(}c]^gabywTjiN} 71rC; RASld؆gZPF{9VM 5N!Hɿ?hf Ύ,_(O7M֫dŠ !;Y41dZUH}btEJ;g/~Ԭ^Yٛb_D\aJՅY|)^bg0_nYަBqR"t_aF蘗)|]=ݜ?:vI>ċjvnVECFu5|b:ÏȷʬLe677vTNnݲ09=_= L>Jt,jБv*}މ-:ARϒٙ-~,m`ie)HyMsGu=mM+C e”?=Ivg:4rcF\s)RZXٟh^u˅ᝫp"ͪ6pr8(;a(+in:L8o ǠR:Ա j>^?/\{ٌ>KFö+#t# Ǯf(e+teJ%GܻzijB^ֵжʈK_D]Dyu~s|{#n=vb'UTWP7 z+[ƄoM=t0j|y C%('Ci>ܔ=3din|}оt!ka"[zY'֝QδɸK6^*kJ_ָ"ICI\j8\#5UjKbПXN]"O^ڷd&M(-KOۚo}?3k8-{Hb |J=G-0=% a4?dUGZmcVA64]8(Zp>>ߴOq]Wmܻ׸f&5O1 P:c~tͬR3Wo#֝t~,-[PenN=rtmpCNu4g }#Ju=||A ;s8)2%ɰuJ {,+x<{=G,>!fXZ5ǮQ0 BQ&/^.?o@ulΚٱ44YI)/5TU̲OFjd2Ye<Qp^f9l\k/p 7(`/|< )P?oy>H@MVE(RYlT* _ 8?my~p: qY;(#JPg \vd{FjX"D(D,>0\#QZKffQQ:|livjZ9]pkKzz4MH4>ojbT*>0\#QZKJĔ*߶NӴBSvP?(#D.Ap0\C`!))/# EUD@aS%?i5/*/!GFe AJ\[~"0 EQ9h#  *jQj KŀoF!V VZSxR10 + + o AiA@!V VZGv(h`G!V VZK_*hkEh#  B B/CVmX!VX!VhP..B6HWiI6b V+e!z>[#UI,h`L5cFO7@MK?oSC WڹQ]Z יۺe"_H2ŌG FίN+|xN}'qbEG1<&]L|Ϳu<˺;tSjhŵe~wr,BQ}G1 N ZJpF!=`iAeuv=g/cVÂ2Zny1Ӥ31d&ie+˙$eO>vEg6GvgO=rM^k^w`>#Wu5:&Y%$˷_z4[k̲9{Yqkѿ~ڈ>qhVڇ׬:]t|m{S6BeQGǍڐp5 qϮ8z.HѯʔFIG՚ՏF|&߂gmڜ4kC r 7<׍7TG 7I"~x6士jBiY|3R~9Vm7U\ɼrČ ǯlbKȅ+Q5/ j,ꗔg?enݪըݨI0Խl֠3ng,i۶yF^/[Lyg\F;t6+++ `rJ2EQigdd|W _ꏳ!fhWmޞ]0}/=yFzcvdy}0SFgiOLU'_UAM{}kN:5po-3yOڬn;E Y[NeG^ܷu=WmN:.SNJNyx)iVwqogC#_)[/!w գAO*>?ZVM\=:x|gFF#n &чz+*&?_}}$*xV\=vϭD%!0iUëQY{eЪz]\>}W& J'/r(+q/s8QdVcO/35;gOc'?QKD_]`[H6!Dx{u=\wrzc˵尵RiB{[wZekK trѤe{lィ+unT˳U+om*, Xөaygy,Sugggj6,^j..L9O|Pר};fjheڡHՊ1W7 ˣ7?rZik0Wòwhkyx)vd^mӳ  sp# dv.]dUxhҙ!jU#§7e%:+ֵeB䉧Jq1ˀirwY !<:t[ՔƸ%^@B-Oֆsy+I%LII]Bԃ3W(Z02]t'z{5&?qмF ;-Bm{n~iT;`ֺ{vlS=9BTO(v[O]>(6Eb ٯE5xZlɶ-+ptjw]_p!ɤAe^:w]A0/c1ӂ=:swVI[K,o5xm-]߮w^5)N]n/P:llvzK'H5۹"w\og/gw{O/n6˺ fM!ƣ&SQͮy¤wܙxd*+Jr!ͅaǯȾ56CƵX[y+ۼY[+_K !D…$M8BgQG=S\s3!盇߱ԿkΜw.J !tӞ(akvL"{{ģfVn];-0+h%\X=zwu- <) \1[[~?S_PjBҬ_%D3&q[͆]kkR@BX FՅHBXF7L#"{pcƿO[8±a\>ﲇQҖ%,:>~HbRO/MM{؟p>j-0Ņ5_ɗHo7gbgױi"$X~9{HGyDfҕ !jncRT~wm#Av6K ! k].ktyň%q-:M !,I;wt1aǃhu=һ*Jfu;vNرJMʤ;,ZYDβ*B/b{o"%ums 9Đ:6olsƷp΄< !iՊܽ -RX|-!-(d:]aiV5ߍL- rUX m~#7tݰUVBVϺ1y WzdtYJ]9M8 ;ms>rT%K)Շx~hF^&R&)edD_K5 ɫ$IOǾѵ)B~xh ,HބeW<*r \>2,=϶{wN. X]3 hԨ>"V+ O=(K(toV`u.p3wv~`܀]^]~/>@YR@(qFݫ7>h̛uݺfIrՅӷ}l8p[c+ۓ bqbRSHfikahpBCJa[[{n+צ.rv;tEUvbʘ^ЎdWe@'gO?J Xq_3;PV>ڌw1w}򍧹c+dv÷kmw˰Vr f}~\ֶlc٦ӺMtz#/r +>e?Z4X[&%0ho.CL~"ݤUotZxZź3u2?0&>f~{?RiQmyA.ĐRʥah"Q"tIB낟qLӌyvC&/qpCgȫ GEBsFYԣwZ^fB!6!%xs5;.g1s -byy[gJK*I l- S^bUP(gT"U$Y" []"&4ʨ2u89/#~Dz*%YMme䙆S֫H%~5 3=rqP8e{y¼w&/&;T}pTD}MurرFl&=ܷ,qf';} pedgHĆ%{t} 8%*T ^­k:O4>,`ۭWew[0Vܴ޼ pMVY L/_!gW`?~L۵jMꋏDǒZVb߽ㄨ'?;"VN yT1dJ[Jg`5K }nՎw,5݄3a39sÏ/>6 [F<ƙ+^&eziJ4+S`83K&q l 'ϟ`UŘ6ѐ1Ö2y&϶OC[4Ʀ5OSM@~4Ҳ yw3$KVpKBVFB䣋v^q)SظWƎ?{/m\n:s5FU5K}t?adXJi7'vX4Ms+5^f IDATenfvx6zh%+E#'zU;/X3Q(~фe5w=5>Ը"~~G2me٭S'B *[[zģ4Ѷ]IHώ]6bWhpugRm'͢^2<~?Sut4x"vFS Oyli6?;ޘE؆;7gam8f,p u13|X kGnl)ZM[7N}{xH!\ߘ1YAQ;6n2Hѯ:L!Hi>.\WфX#~e+(Xq,z*^*X:x~+)}}}ȣ + + 'U" X6B!VbXo:_N!H뗞~eDf7xNaG;Bפܫ& zzߍzSGOR,4سkrڊ %{~-Іe,^^` ;[7;Ng5ZC8tͱwj;[69R_+yt/ ݌X*0[i-,T|^dkn`$9[wG܍Yc@1/IR$wՎr o}|NI~XY߆޳4{zE6t•Ulܨ=QZ SXAE-Eqy/ޛ;h pϝXm-KJ.N|et\32#fn6nG -cC"&>]2i\>&\g%n,~`T%P8񥔰RAIkٔZwE5p>;@N֯Ƿ=OX5 fF e㺲% JaH/h.YZ- 3icغfO؞X#x*ʞSErX=7/p Pv'\گ#{|.Gh~EK_yEIt_SrbP/Z2 җj^QN][,#:t^.\yGb?ߕj~>"V}lڦxX')Ȧdf:SA "Nw;zYoX1)B`Qdө8h &r~/e1^="a+%Jw_ p>5-rEOeyv9I#>*aQwuL҄i30@k*3ةqiY6>Ϝ.Skr/vysb-Ȭt)i:/r,ç_lqYk0Em:Y>ҝgw=mAZ-;6-Nin{*/~737~+/\u8Z9q9N)OK]8(s9 ݹ+1- =wѹtZM-2q)?FԠr٢}ozBeKU8Vi" 'M̩)[kY'sJ8:л7*(ҔLY%rs?3:֖~y`m3NDHU~EkLbٴ{]v2EBw?Pab`;zύ<Ǽ16)Dgк& ,!H/ Fliv9=aބm< 2Ol&f-im:?X<[ʰsF;f=">D.Zc[}t06gU>3Iܠ}P 40+HUffާ=H0_#:Ť/{k=|CrxEJ/^sWG 4Jo=Kމ,GiI%Gzl{pV$dz[*-J+LJoP{tW\R|gz/"?UͫvC{?ulҐ7. ݗM=e/Y ?N+ \ܙO)VVھ(iK |WGLFia=ۘTB)XUaXNa@@JxL]@ p ^F[FaM@`b)ь8%,x ^MTmj)pܲy: R2K6&m+TrR:cWGNMRҾqo,y)5@<Skb7 ~@ /6AYI+ NOri^qN2DQ#*)`A#b{er]l9t8PْsZcN`>,ivϸ}\(o")y"-\#Xƿ좕jU=}=%O DTY78} ˈXS'KG8f`8f9(#h3Q gmzӃbQ7?#qu [hs6 Kyƫgu1yVvoX]cJ(p}#D_P9nIJ%T74j%p&\\+aɦ:r,VU|Wvq þ|1]}=L`PXUG}٣h&:bSz;Uy;Zq1_&_$jW[_Pݿ_=ue#f/ƱE˴JRKS !r[cF%_$Im&(փR1ѱqiWB^rM3䠦\O]S< "44sNB`prB*,)+%XI)5G;hT <\YOV[Qo,X|^e#6yuqim:N늩uS>ERbfjBEYE#phLG-#qwY;U#Lqq~VsV'6|ZeZ^- ?Οǝ3zyOpyPA}: C*Rk=,q&)3B%.1%Zi;LiښkaY^E̾G،dq[pL'NOա òuTUٍL$Ma"!!XI*B8Eb,'3)5Bѯ(tZ- ?lVЙ%v;yaa64.'= I[ ]KD& =~@ Q Ɨ K5ʕ1QZl'N8cr*6)kݿœ ȿ81&[8U,e'W,I>,ŕZ*ƗcXt;;".ÙMtђ &r*Zq(z:A<{16ӦfTrbW1S}Fgzrw.u ^Q'vƬa_Ju0o1cxt/mIPe̵vKJ̙uJ , S=_Si͞#>ju#E̿nbt7_X͘vқRLU62(U0EΝ;#S"Yi-ɞlDfu%?,*.!WTV}3Mrg7w9+;҇7趄U.W,R q_ݷVb5?)qI+)/EC@ )EDD,MF#hʴO7xŕ>.77VBáM1qnK*>k`r*<0^~@_|%"Nj(+Cͦ vKo]N?`dfD[CÅ$cd@q5cO:2W?hVG]R 5LZA L ʂF hO I%fRYպEiH]ax Z@ H? QB@_B @(1Tb\A]d"#JL~@ M; j0QŽXmfn!A𻔨雉 )/O/E.h@T#\E:#2@ @e ( QQ [FK`ѐL̶W5Q[%q`ogz(@hPld^\\MB^6*ȠnbUϖLDvn64n&A/!j0o{HUBW/Az9YOTa@ @e 5/_Qhp[saf5fA>8 _M; j6QtA|S0 @+mZ~v$ey W oZ[|~DT4[ #P?@ H? F#!򗢞t~ށve&(oĖ9 -~W7[~<sk,"w[nĢ:k7Ύqf|\q7}|ϧ>6@ ƉqV,;/TfSi,! RQb=K_ ^pW6NҮy_`Ȫhn^|\0vؓȟS_n_ٿuިqG1[+Ǯ8qixjez3МSRU~D~`U-wB}Kw`/5ӿ84fvaiXeكZ 㾢b-6g !?SVܥ٣_`ÖLeX.=|nMYu%vvVueqSn[m[pm {SH`{ IDAT_`kοe>- Zt2AýϯXU܁tm'[ra}uI ? _"H(VXMO97>>$&'8":>0]Qבaz# @&NQ_?|%sUe/?b)=9l.6lH6os/^uq1M]/_Dۛ&/P`6< ojB+f/xs-q:~?۬|3<^d:|;uِgR+!…Z7 h@ԁF3vʼw4tZ^el?-]˝l-ԩA߶WCy3NX^MկoFt(t_e ѥ1cO*n굕auIj1F_SYE.GQL(02 H1% BQ#[ڇ]N7!,jpLcZuioRP{>SW/,ǁN}ݚeKz@g?GujƅG(V!!#o>ˮ}Z=N3 # gRVtOl ϝٖK]}mHx4qd]rjK7Egʔ%gn?AXBȹ}Dׅ=UӅW1ٌ)-kBeY=9/M\-1J\"(VOy.NP{8 8@(:L)bG>UE*.wآG]Ãs/F(EXA_6=aտ8*cߴ4>pp~;kv7vM o~tx;-:fۯgo81oɅBF}[S.R@Xߎz~s`bz"uγUSoޏw@g8B)=8yO]fvc5c؃f*|@<"V#Յ413ΈD"nw(˾BC8}~c~v69~=ioPǟ5^#w[kHy5+\RY1GGu̟rǺ1_M'6377޳t𦩝<:lt\՜DXaU\d&BM4@*KD.R땥:TTbs!j5nđռi Df:TςS}P 0ʌLԻY\=RKcꪼmz8L]CYۘʤPĚM5?w :'bCقFf~b ώ,LnMGc o3 H? +!H_ zH0i$6D+ҜO` L+K)ɾJy9YsK&o)n\=<&fvV[ŝ֙5gRd%g)۸ 2hJ\}Y%ŖbKӒ 0rFst1}~JtkJ$C} DzAEhgc%9Ӗzn~AXH2$أ[MBIϢI{hn})K֮;o[qZM5) E>7u+NӸ_c>8"E{u R:r%]I\ J= @0r&&Jdg:Lb)RVF4Cv-H!Թ|7sf]kUpYǕ*䜙5F 4e 2d?Wa"L|̒.<~-.tҖ^V>G  %0Xu:<׹kEYW#N- n\ۆXHV?lYң%BL7cl9i.(xb2;[-qr2yaY!/&Gqn*.z[̼VepȱM\q&lY~\u* UŇR">PBi`8Sgdc uͭzPXpb8Gp m>ʇwwrd/GrpWʻgo.WZ:[:x[b*|-OQ\wE+,ގDnt>R&W\&ǻn5,y+b3>+:sk/u"st:#@c%qoeTd3)IJ3/7.`+ujbECGgv,rBcf.uhkE'j0sqwsswsswsu p\A"%yz e_l_?aدhu  o<Պ&Xd1K0%+m^A#=ϙ;):w5i: JnkUsN XR?`IEEw{dׅqqṳ@`X;Њ|xiby ,{jV~}ms@z_>zۼpĮtonڍio%D7m_rSD'GyAWQʌWOZGwbPRu(Z'т^ ;eh IE4-OWզ?m;{T :8)0ŏkpe]qs +ܤv=>|v7>S=<{+2s%2]8PH &4tv\Z²;/^>]` jFe8֠V*& _E-[O[|ᦸr& ۛO&8遨Ruom;V\l ]x͗@޵6sWg|o;`H2p{XVe75*Rc8ZR* T{Tƾ;JZ2ĩu}DmVnX6f<ڌW$ !sao͖[0YRc{뵒Num^'y,žS,a'jٶwv060}_`4ߤ&uq|H:{[5럝gۢ˺S龅GTߐ t&\^l휯yV,6G7i:~\MFwXwbٔ+%+s@W6?u }C<q|'0yuΫI˗udNYl鸃ȹӛag]7gN֤V֌[S{|ìE&,-,\9ȅ0rUm7ڄ.JX{  fRDĥSkhi͆Xq"zo)ele/|=GBѯp[#VhA|9"7>-2ΫΡݎsj#Y|OEΝ)HB!1p@VbdMh=V\\/A;o-|}VR^>th @ _4I#2=ȟ 㜅@ &>.W<,SQa 0R6ҏC?.<\HV,,ç_B _hϏ| s!VgV>3 RSN+ _5~$'bJͥ"A5 j]aqP~=VU(O/5;zp|qA>"CXL{;{WW'@KX@N3@ (QK@K@ @)hQIW/8{YidξoPѠ%D f$1tGy sqa%QK@KHZ9^&rt9;0 c0rr)8A#N;vn?@H'xHG8q/8\%@ @u Ɨ֪<dgʊT޵#{ lvؒښ ӥ/ޭI`1ծnj,z-ĩŃ]7 ;uKd[;O3W W {3^_㮔Ëlت˘ujٲkBo5? ZB/O$wkH`SRhߢt]dXG! ?ϻi|&L4gwl>s </hIe[ Ԩa@`΢Oӿlצϗ!8^\mA~zr-WzylK[e }&Ws1kVN!ȯIc^Pak9#jڈTL.LxCԫk`÷ ǹ&!깹x޾iT!˖dJ۱̈́ȊA`&ذCTލBOY{lX;S+8vl?Xe#5 2`$ liTwZA9_#QǧzCiNP;1߻a3:K|k?ebu2g*:fx<仑m:+]hi.:yݺmMhtSUڍ̼wno yfK~qڗZt=a"ʾ|:Nwt7}‘ 4̜߰~7f)wګOVO?{8A `<9oi9lN/$_ >rzü aQx$pe{lӪK!d}7z`9tlHE,>zϬUK&7.<`ED!KĄ0KˠmBl`N Y8{sخd >/t){/kx4;n]qjs! YB/$tZEC9GfQiWnޟEޮ]MBd"&qBhbna.}>ޜ pv'[~pТ|}?s\f֙?*re"|pzC~jz'6NJxI?t,wʺqq +[>[664\?6An;:G:Wn3ݺEMja~9{oXrc\_=s?[^MB!YyȄϥ(~2rNZPߓu5F2f^m("l: | U{4ߘGǟQ6rd5ڡZƌ:FJ֣ kj9IDV)uHw EjTǓVy@ͅV׸GVU R= ROA-(32Rf^plغۊth?vj߭6؂Ę|=Ccf[9/xc}5d"C_քM\`{fC+ߺE_ˍWbg; ٶ#dob/<@ 2`:+ v%AI.mk#ډV&E1WR50o0S=kbv- PO]ԍ gGK4!:00$+bKQs WwugiUu(` 0müX2Y)]~:u 2UϥX&hp{7-> zqi58% [S;X}_ Zʁq &M{kt8 uYc7HlKK^ɚӪbҪrʻ쫻NzɧXb+KϟjyNkNnٺݚ-ZXg֢_%EG9E9Khvdǻ1.m]<"gk@fʦ\M+I6xE"!@u8>I$C/<չMPx^B_^ dҌLb C8fG../;{IǶUՁ߷_lQ_ =xɉn^h2kWwb FK\"I=:^ک"P|qauzlU0N.&brb;H%\HӃxސP&R*V?1^KRj{7]g B?ň3K,%S+7]ҕ1ŐԳDE~ &&Jdg:Lb)%RVF4Cv-H!Թ|7sf]kUs-ʳ8kO帥Lste='/6X9kH=nAڐm+M1L.ⲔLqbԆw{Lf77+*䜙5F 5A&@_y],JujQp6 B#y1`+g-KzT Y{[jbf8݋R:%֙=($i>vĒӗaUlf]}a{7;nonhoA=;z̫kj]9O kɡ(%4Z 6o2) #I ?%*aC{/^eE<IB ŢގDnt>R&W\&LWQ KS,{kMJ;ers\\WOYKPs<̡׉ȋpꌸġSϔ$)ͼLjbI\|ԩ >h7Q:3sC[C,osǗdsM;^8w#g`< VOZֱ~Ct?ecgԑ(|-zZNKx ڕN`fMwNXU;C-aYz^\qÍ9)n fZz =ÌtγkgΘߥɭ|Y{o.Um5XZTH$|m$cl=mKƳy}Ћ6lo><ȦkKyֽ}XwJs=?'Xw!kw#?m8Cߴw e%gn8GkTZqFT"A*c%vi` wfaNya@lx~k#kzo̞tڱ{@wAލaNP/~9oޝlFb @mڹتKڎkE]0VEՐ  .tRnA\tJfYxqјg6t8X &Yҝ/ =yƃ+0 CrN=ѐJ߻[QZhĤ_xcp~v3]Kytmsڸa!r?=c7Ry=ǐ '{nmmشvCfo;Oݦ,znKN%^BYEVoP͟dt7o-q_wlǖ;kk)pK=vȽqhee=R꿨*?DG***%iiĉivd/&YM|fa Cr=e/{%{ơOϡ~ߠϿ~𑷶V7ї,;}7fW֦/^u@J\NǏݚ?6.V-m-7st>vߟvd>7,d~Ժ)r*Q9~+y`vɴ]M{v.}mڟ쌳ZΝ?>~S͝euUmT ؎ ת[n8ӧN?KÑ#Gz(ғԵ_W(^{K|@y̬޽z-($N`ĵ覞ߟYh\ua$$M` kq/H~ @?@$_,!~He YB%\;~ @?@K"v~d Dr .%"/@?@\K%D_ɵ~$K` kq/H~ @?@$_,!~He YB%\;~ @?@K"v~d Dr .%"/@?@\K%D_ɵ~$K` kq/H~ @?@$_,!~He YB%\;,G@z_} #uB%$K` kq/H~ @?@$_,!~He YB%\;~ @?@Khޗ(@?D[rs%@?~@?@?Ee)jIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/pymeasure-managedwindow-running.png0000644000175100001770000023314214623331163025035 0ustar00runnerdockerPNG  IHDR`i pHYs  tIME$S IDATxu\R%!(b!6vw]ݞݝgq6gݙ?[QQ@^Kw^,3>|y`' 2w N$"RX:8ZH2a%_>d!<eӦMDԩS]1222$$0RJ?yɓ'׷n8rp;hD"2o߬R ѵL|cxVx ,xMOjPBѽ>wl2zPKC4olyGI\qK eɭ܊ViP\x{voħjמŋ}w cUJŔ*znxYP)KDž=rÈ$=ɬ<`&þ༉m?~Cu:ݓ'O[AxȽ)<IEx{Uk4'Wkj;DШ5:ч{4sLG27i O$lPR",YwW_yvM.#+  wK$9rpNŒCJ6ѩ'W̰LJVoYy*,}s Jk!2YB} /^|[?uN^gMxw*b˫#Ϯ]yⵖv.^!~:[8(ܤY;%]߱H\iLA_$pLYksN{ztن+ϟii[}+I,uDsGªVAաK%޻Q2->y-ȾN@\M7oo܊jyƶm23yf:t~!j\r/_%"++e˪a1U+@ň36uAMF#dLၲdLe0lRzHfﺯ1Dŕz~}~"ӟU/0fr*%dƔ(&2]rڔ(ʴ*Jln˝cIz˴-QĉU Om*Vo%n۷o5GϪA8{!9QLL̩S*W/Bxȵx3Kbo5lV}mSWPG/扈1F$h:""^̳i_tDIoMѦ''X!֨\ɠ+ cK`] &5z"">6EQǦJ˺i3>qb {.}V-ճgOV"^z~MLL~ŋϟ?'"EϟsWbope=DaO}u _Q";+ EjًK"hw+\yܭCBfY vdYq~} ^8ol=CYY [ӽ7]ZL$$q#^j+ǡAZC?_5~yyyUP8^㰰0//f%\E7|dÓ[$^ jWZڧGW:īf)+^HzǶћkT hYJi?ҧJ#4DVz[ym^ :ӣXx͓ U?}L_ ,"7)]@$S>f*Ezt=Qg$U%=L18 9^}}}={VreuիWDvvvzSn.~"S +mJuz2>v5obWTMEz^]UldT.LCѳ[>oQўZTbܫSA׽;m7+jO[/,'~ $*\;Eq!*"Mv;zݰwёDr+ aI~]Q9;;TIT*U*UZN֛/]fMR kjT.J" zNQu2\*1AQ5zFKMF/ ĄQ\$J#IJPH05D$DybzZ/KyCLff&7efrQ/ `&Һ'Ȫ! UZb\.NRrÇZٺ90FMNJT&1}3mR\܇a%*!.huRB,ϨNSg(~) q⌑&DN[[5؛/ |_ < <Gv]?feebbb )Cڿap@x [@x@ipEoo;3I4IucVj8׭W̓aK8GKj؝Οӌ_njAw^;qhٲy+?P-]}Yg¥g)iYwdZC]*<'*2vx'GG sBxȘo"ua%-۰~͈ۧvۮKͳ ;iqeI9x#"!uX o׬X0KEedeҜѮVf $0SH 8Y->kk8cZrLJږE:V> Nq-($/33 zK|y'<ڗw0w ML͗xχ1b"Cy qNL%)[mZDbˆSwQ;W5#ӣ$^,W("kխi¨WS_s iED;6JZ9Gsf|mywWups~ [.NH$7x=/xcܱw/:/ܙƟL}EٰheHHi.M[FDIMgԿe]^92E'oXx#"e&2^-扈8k7w_rBF#Hgܖgq:7<ӪvoWD S GD$wQb҂^}NW*WlzՋXq~T-I]EC:Ԍ'L2=zZBaiog!oΚ~Mi{yA]|qMwU\LW'-5$.:x&i3D6' Lf)%2l^nĊEUNys+p~aRڿXR v|_X"62\RTt="#k 7wwߵo?,c bޟ,VSGU\LDe;o;w _ԋN(1}x3EI+Qj0b\{VkYya^SW'q@d)AƒL{ FdRY%--+!G.%1Y#0!%)I3玟*jtgϒSJRVo_ӻK{7,T$d.&.|˧3(izee)ً[Yɚ,=E{;\kٹ@dWUM3'ϴت%mq\Z;]17Z,ōǞT5$C)N8ڸa9ףDNupeؒ EOU&aϦV>yiml7!DzC̩1m>$Cu^\7ZSҌ#h=[giն‹DDB{ֺxfe6ƒϽ0V>7-lE8]ffi@^oYeԜ=r+7l#fڼvfW4`@f.ek+ HJEo;|C249GaRNO9F`_} Owϳ֪^ ~d\__BҚ˺iA sHȀ?wo }wЁu™5Q?j2e&o-g/W>n jebȽ?lFX3{q׍MFS-^.Kc Z^ŅGWzGZ,_qc,%j&qmlbV?)M಻#g: WO/TD~yǂUCF[[?8YҭUzoz[}C i^߻xhٖws#"2quLzJaF;ϤG/n,8Y9 2.sw6ğ{[i SoFx7-:2smf}%Eo~zf~$k"ETħ3OjbAB½sg> 55ydB C ',;x;FP~f xpiS"}sKuDԧve"7x,ڧ[n +3MDD%~*_eGe"oS1wKc'qMd[u`u;z-ޟW t%sgք>b"b7_jP_ k H`Iu3u{y'ƥ-ejDsO/;K_PذW<4{9$_ l`\C`n6WE=4pPPIO)"G?d\Snn?u;_5laK~-%w>;'ۋ"ǀ%ӯ"uliH{{)-rhBJXi3U.<8ܻ!n侎DqF"\a2fH{s /"Eb]ԷÀv%OoŪ=Wߨ6.^uY951rfؒ>ٙkZ7XP܄o5lw`E+ȷwA<V)QО/XH%?"յOKMMHd/<ʺzڨ\2ǦI XqMEIĽU*I$.ULT5\ǟvK$.Yyy|vCzF:0Njm2bK}cm3R0NkѬ_C].̙}Ʃe,ZdmcwQM͟lE ybOqoc榵+_+0pxWV*3[>8gN#ΗZ_ )7޺pȓ-_iA 9jrWr,61"F"҅͜$k6E8ul'8""^iu*Ǜc\Q\f )gUh5[U)uxHnĀf"o LwhT>} )lk4<4ND*oi_eD11&9o5 N~26wS;sxYhƻCG[HAdWu#/xWӉ8Y{4!X<82.9^415 H}~Hcًzus X4ޱ]0wX rK+Z2$^ϛ֚ahVE2|vlua΍ggk3|ʓ>OO[W&6 Ri].'uۼ;w;ܥٺ _J:-d,,-U'.iD5ތQa1q$z"uCb2֣X]{K {'wr p}|orz%{)tp3W:{Z{T5y=+׻֭8!H}%ֺn:*޳BٰݸS@ ӔVog!%eC = p w_.҇qVHQ 7N֕#A&\ѵj]{p`pc|~)\@W΍M_^?sA.@x\.-Z{Ve_bX kMȸtᑿ[iۛL36 ""쒱TjC94u ~#?5Eck16ㅇ ,qiKDDF6vʟ3+˸"op ?"N ގS9É7Wm^LF"+bp@MmݾgDLKTsc8N8QGp8Hd(іm5/cц(bADެMM~=nRv2>E^:t+q vےIhD޵zi. mYy`~öoXoY׍Y=+ ID:3e[@*ImzU!ռh{H>:ڷ9#[q%.6 <@93'"@#c6ScFd }p#g1[gO,k ^ [!g1&| 9r>!˹r5 [#0+#sg@xܞÖr 3$7'U\\v#q~b9dg2 !g}6g}As=e6;->R>5#}pYb r5 [Y>NJNN77@ڴlعg7*rp`7c@edHÖ> قaKÖ> $jꍔoU@_?_?ߎB5+:5q6fL>IUDD5/t8r- ܫ?>|o>|˾:|-X )H4Q9^0QJBF!բ,RTeSn{9VqPW_4齶3nkHl]޵]L&ҟȦ15n_WDsu{Nؔ1[UڴT;l:ovn,([,uu٧N*Y΍j31 pd}M[˅=)62,V3y/^X%#~aڪs'""]đ18ŵ>RdμʘY߶u4WYV߲ ׌}o^X•m'LlQA7_2oa;{c'ۺgǒLr{T5F/ZӳFn~&"TTna;|I|wU,^rfR"D< %-*A`ϱ}*cɑg!:xyU=uU+nJ}ËU صQmDGa>2<xN0焨,W("kխi'9;.tI1QUܶ[D"S &POwZնehWG+[0)a&h^%x)UEI.*ʌ8$aK{bU0Hl塈~:Rx-8TqV:w⎅FiaJ;og鈲umWkh(@%"⤦rcݵ-VJ͕):Q~#[#( x[v)gm_XL/))9m`YFeGw~RRRkթp0Ö&$K gzrLi$fʄ K]X4}k+Wq1Q_fgm{p!v +N:he :Kp8fa/v+׍Yc>K44u1UV 9onTUxl37/n >vx@D$^Jci"=o>x4j^DDĄ^>s2B֪'leO(\\L9ש+f{~;Ga@ z6훼ʥE=AP^TvXULs $ ZURF[Z Nt.‘}V7͜pd^^zt83}!MsԜ=r+7l7>թk?hVN>]kzy'=Jݗ&p;b~gu.$'^ҧtIbt_ݘDILH@Dpؒ a/Þ= 251uwsw)P&/e|[ ۧωz+vx;uM6y< {*5V\+:&Nyc=;ծO ;ܐ{Ö=V̧FkY[x{y߾{;T>|{[JLJRxe/^TR6*/AbkZ~ٖ8#"zcs?z,**<@cζ$rG/}NQAx8l8}c属oW^vQ;U2ɍ]g V/iWO6x ʉaKWg5-?ރ<2o??%7^*zYu}\*-=7qHx{tp <xuJӯ&!ĞШP5cijt pkW1LLj}Oo\՚G[0vPjSA@_6\~՞":4CWuUaE h6wo`k6rnrG dCz =1vz˅ K~Z {gj-wRLk[ѿٿ3jMu}iin1"ƾnXJ/Evh2ýp/g;pD=}]~D %GG;目ʇFt)= ؿj6q&a93*J#4SmيH`,^cyAVVVe!Dl{6gi&yo9&?^w. RclH@DzU?tTߦB$$9}2meڑWZ<\_Jo[(?LeJ8bߠq΍heCwΠ}{98Y`Х {j/]7[ˇukϿQ1+.Fiա̽{gP _?{_<^ cL~z~&lcL Ѡgh۸ݡI裀+ؓHrr?^ N:z{iIȜ-1u+i}mR٩Q2Tmy'ʱʇ1Fëwɪt/J/M%ٺހ־~-9Q5k;vDnWV.Ue7H<7Y˅w odXp>6dDsvmn[73m%"]-4kid1l>.{+<(gf[94gx$n}A)} ;?_x@ 6e^}WH͎_f 4jz ^oy'[y0s@O1o8m KWdn^6Y}wלſn\݉#_۩[QW8D8hN;weu#:Owvs;ǡO?}܂g,?YrvS3tNuX{_GE8,!OJ;logq~ JZrwvl}}V5,b -.[*WW>{FL`CG{VgMgYPͺWAM u4 xBDD^}.үo.mc$JE&f ҽ8GMK㈪v"C%F5m!WW>y6 S[u1^( P #Xd,Nf!gqγA?'Jm|p\\133ueǿ ٞD^6$:qٕܣn5ڶ ^RZuH(*HP6H]) )N[ă8qIzWN/0FH Nqeɂdk :"4wq0:Gilqc)A N]k.vks~]ҮVpb0\)"Nbk@irDuA񞗑/\'p Kk(i 7<:}ί_ӫfRaly='h§7/7:-9"T&"JNLPg>$k<{#/^>r-lRJiD!@)+[&/3>ɑ>GVcŝ{wjg~^}C-D鹄2EaH L# y_-mpؑxӺu, ?)<;k9os{Ts*A`TI]oa"/ 0=S^t:uSo+.|qPjÈ#bLk92(4d8?^B&@2.oG3e6=ZWeXeF-I- @,uEzDRNj;|fxHѤ;81A.>,㫨7_9:lIoֺZs-~љa6<"NpdR-4'W]~j\-_9ٵxAZp LԠO :zM_ ,1#A҇-e83I~=ׅfNkyy7Ҥ@_oK11g4}tPƚW:؊=|,sƊ{IŝH(C MXv7*mWޡn {f["1Ɖe&Q7lNg飙RKLSG#҄y)7vvzO,^|8Bޠ/~[MaK+9r>_ fn$=\8֬AB.O`3Wt7un+ϴ[>XžN]:4h[aኀN[_uS4Rfb'/5qdbǚMDֵa KݛsWZ?G"+[a{KH_ݽ%TgY{+Y>̳q-ˑW5k],2dp"e3􁠢<:e2YZu#sͱ_T$]>"=Ye/#D:H)*J?YϔEڍuWӶ@?K!<e>~{6 v_BJw2FӂlosN NS˒׿b_H75 mc"5WeBd;ΘbxM_#OF5̹ 'wޢ nY[EbI=:UjæMnn\A8nn6?,b8m4$rh“/|{F{yl784#e8ψN~ty fԭk1eZUdZ mYxYb_aB oUmظإ֎:̙yKϟLH!?ACv̸@|tposȱlӦ5^n{XF%Z\%Qf˛E&}) Eׯ_ֽxb򕾾rRixsw8;udT IDATM6y`<}ҧF/rJٲeT*{nZuG*{v֫]c je z8 gp}w!8#SS,oٖr?2J>zz>/@݄,öxw8<+`Nȑ7lK߉V=rxOntZ3bL8h3%' o%?>zK*M n[DBͽ+V;mֿ_!HuiM'L*׵N~Y9rmq:(dUL*W8tqJV*=Q~~x>6zoC)7 |kM5勖["U;+⾤OqyĩKΈ ^FpU{ ~<bgruq}B.>ȵR)qqnn|G|rT*l?Oȃc6DD)O]Rѡ~q <7ح?9yG\qdjETؾåOv.x̓|ug(!!.!zѲ`Aw}|㲾~aK/_DDFȵ **\|%q>2&<Шt֜"$;/{) ?z"/9o[y9s~<\ރwIPũHj"54FybU:Ul IM$xbTxH)7"˙JO9jq*'%2l k8#Eba0sDX`|JYWFt8 <#i^zUj0fI@G|e:L!K:VーzV^CMÖv؁O;m۶AÃaX.,;;Eٖq@_ͭ[yyrŰ%\%<@^ Hm !qՄK!̶{ӗ*DJJה`ee(C:;f%#^;9|M!KH?l.|2%%G!zW°%^ommkƘVt_,2t_ʄ< -*#?- <|:ÖVZa' <|~ [@x4t8Um ߠԫIaÖr 燴q˸-f΅kPQSn u:-1~]}8.?- <q(%D=|/w?+]#l7{^C-#RY<~焞2"Njb>lP!Bl=r%Nd߳O}&,YuDLa뮻 ,ZɷּcVV{wՆ4OVwm7(B  Կb~ssNyVM]G_|־_<ڒҌ/\D|?i=TkZEIg}fSKNq C_}rs˚_'Kb %C"!ywl_ɂ n瘴Ԗ}r/:eD?wnܪwv+M"[/>w=~'<<$Kƅ:ԾG޲6g|m3RvyճViftqQĄ`VesXS+FY*/W;U IŎW+vQqaxjha`_Kz48;Xmf,U_+ wEMvמ;Z)$i[/@x8XٶwHTzwww+{ltw""Ó}c`uO;K?xzק[wMM(J?֬-/}AUP-:v% %wі_g|m1Q@x8cifeeDjYwE/+U:K Y|+w%:ޗ#?-r:u ?HտmiY>?'\p_~º$¡ur#jH<ë.'!5ۑ[Z^0t]$[`YV74+Ro#oGNnNBtlZ3GYEyNndgzs;-p*?wQDDj1n_/;=jŁrssSSRmv;4\|[nn7  @5Rw:R#n!}CiyizF[UUU!qOEI?M|ΧqLU`LjPGs!E,..vt}X?(-cHJ-^4k59ѡA=VXELez=>dՐݖ W9ZRB^j80#<(OEEE=hCLLLe~ږ૒C,>OnybjYz:Xpp^YGZgNɁr恶%|n"ޘGmi:E^fPX,"ZYΏo: , m_turRkLfS.aL$ #eyypHYW.L(.|0jxAJmѬUMQDR;dZZBɪ{ߦ-=D[E$<}|m^Ep V+}n}ܿ3XU; EےދL6N6Qr2T"a! Ԓ}ZX>0rM?Q"aa?[cõ%G>(78uέDJzi*mK;>`lc (G8B*pZfz-X4UEQ,K3Q("'DU,EDc̓%Izҩ){6s`! U7|WPFA(7^@jUbJTʋ5r[L\@jJTʊ+B-/*&QVEQDQOưԱ`:9vE˗ެ\緰s*0DMIÓK4 Y`j6djHT٦ Q[ػ.DZ)R}M%Z8T|ژΧ)q/^6zrq m E}Wnտ?П;LpbHB+,7@x8jY Z-Oʹ)?4wXy/8)+ܪ 7HGOKh?Ĩ{>hNⰦ{|NuO[-̭i[dTpŖϟp g?6UY$q6+< pV&P2Q6xkyc;nsw{n<"MMbz=W^bKrwjo}?JDDp'OO_Ò;.}%QMUG[ǟa˨CRH7# Kݳճ+Gt'39C'xD!=?䶃򷾿'۟{\* 97q+4s:Ai[ jXq8T~p5ͦ!$$@8U BWAiT4wM~AkxnfmGOܩp ^<7?͂i/.mK]F~ NG4)RCS z_VAض(-!C4G35lΡQָF xlp+55QQQ>3H0**ʛ[4, xDP` !Z`:~X^Cm;r0̤pێmi^mK5bRV0 "}(mKn---xFDDDfzfRR 6Ժuko$Ӎv3Qv;(=7&B;?<䌌 Li.˛a%f DryYe¬h[.#l cgZV67.\!nN:oZ啞C̿0cKroW0gRu2 <nK~iohRou+fmSFf:!Z -#}n͖*ʹ @X'@=M@taELev]Ц3Oe'3oж7n63ſ%;8@x0P!TW|Cflm|2z$UCOB HX"qCD4]=g7?!<[\{sٗ|Ah[ {ަd'`y(Xxy>1'e>D+:ows[Xc:|񕣻' L8!9+" b5L1Wx%lg-7>H”gF6C|Њx~<Ojsfp 7AےiuVY\~pmmO< G9 k\FHǸ/l{`C~*Y'{uw{:s\e}bw1g5Sm[8e_SZwۯ6x?n*$2놴XziESjvu]ä(Hzwlt[yivWijc;tn;LD+2w}nWdtcR7~("C=.%"Rˮ,_Ah[ IJh_mfK|p|ǑGz\9v=**"JxV1{՗'=}ձ?|z2q҉^}bwϽH7N{`̲Sn|GƟ"<:͝yaO_t^\VA7Lkޤq7evN^]XӢ%o]DĄ)_[#/-+TE_M'>}cj;Y,Xx<v[cQ qу#4(UGmdZ##`g%w_hm%$#kۻgҏk7iLj-O?8IWm~u^A3s;\5S-"~`.׎3 Sۇ?/&b;GnZmrpa0)gY~fz䁱iz渳81uս{ާ/dAoiIs\Gɯ;?]d?[FwQgL&""anj۱|GE$E5A-EDlG6.Vq֛Ͻ?{k)r5wOlalLT*J 6[ح}s(k,YgUCD4=.M~+MDv%":bz&v}pWѐ_Dz^['MrJ6u<rmu~s6i6U$WAh[B0١5eQ˚N qcW7^[Sh)"R4iÏM|4Wݪfq8fPi5y{}ŏ\Ȕq7~w۷^77ʧLƄ|S鬜s:k P)_=J"@6t2yP-[w~}#?glkGtU=YeLYSUEMDĞ.U][fvZ3InPVroGi?DgRImT-1:2oF$Hto_GiX?Ka@g'Fg̓Gxk2$u\q96vz''ꠌ}}?m~W7kItgiw-+֓lo=*[us.#hÂ妟y{i?I78NRvu"{u|3d\ɶiьuVp{/fm ACx+$5ۻ0r>A!Fy'*R {* .ߴo>}"QMz3α#,Χozc'O1LR;=klJxbCFs IDATn/O*g4ٲUU4Wm h@5S%NQA."H^?H\"{(~f;GKB[ޟ}\'T}""2*q3nW<UqتTq良> k{,)_1UfM<ï>4l9'twf5?]Xc.x1?*v[? @=pN(P>~:`a --IX}>t*!Q \ @C3*) K𐛛j9᪨rss z@ˁVzȺ9hâגW-Yٰ.IK,gϞ, ټPhB3J1ꩴ4=#ӭJi BQ32s{mbU9m (V{u[''X7i3~=vhn(aw# %"2ۡja򠘥mI [K,E܅;kj> z ъp E4qb%B#Aiv[%kk•[vظlʂ-[_)lmGYpE,foq\z/P(H~V5/ XxpMr8kJ,Y񴡝,"nѴ'oq-Rȴ'wL ` ,f R͜ë6Mےؓ:ttxpH{U?q8gBKO#_<Κ=R/v[Bh@r_)C&"u $d5i#tWS_i5+_أƆ _oy_Ҋ7s?/h7o>udh V}σ3&nj;-8ҴJwmKm= ˈ]`O>ggZ=<X:lpKL(mς{oGk﹮yɒ/?HD/j&"w>"DrẄ́C8YtEz~\̛SCE1vmyֺġǜt\c_tgϩzE;oT^ZбC:tl4ֲw7-rPzwåwz}HE>6 d=f%ƒ˃>TKHxl~ْ۷[\*rf=7qv)vߘGg+/c"<ۢ}BцM{Wj5,Fm |_{{V' ʘ1m9g`p0{q[g}R]arwYo7rWxМAXw3񙧟xzs?R۷Ul9S~u20Ӯ?^y !jU5xM0ka´oQ\sOLzE$E[jmY{8~ˮ /EDDڶm!_5%N{cn%mepU70˷<-#8~pݖY+Nx]eNlKv,TKg廉vO|rO-S{|Σw mnQ*IIjnqX]I~S5p˝TqoŽoT./w-`]aS jWl1)Y1RrRɊ78%"%+#!L.jt^wX&֨(q̘6ؖQёQѕ7&Y YYI^\\wpLlI4+vGo)_W 0UX]9 ۹胩[F>RQ\,bֶͻӶ~Hލ0mKw3bJp)wӏQĶ8rH6KD f|WEngEm"^xp^Deu;gK)ֳ A*?mKԛ v8߇/ZX>@A5Qx&B'hѶ݆˜RaaaTTAh[Ym=#\}2,Aivl+,,`Iaa22$F>8u5bCD `TNQhkP.%Ģ?)n---xFDDDfzfRRnжw T n;999##5fiZEEvh[xJq\W0&pX̜y9B!ny@+|y\l%,-P'Ea&ƒZىmKAx%dX(` BZM@Ѷ`3 6:BR{hݺ5s8E @pZ h9QͩmK/ږ <:B9&@Ѷ}B, q|ph & -!ҩ3+Fp;:E#Att ]LږLJ bByж#cswNYzm<@pt%G <жd"}wX:󻨯!O 60&B > ':?: ЂiMvض}[qIq( kTdTfFfzjR9ORy՟P)a֧!n%%.ay;vIɡ0vZvde08 mKBf Dmq0quX׊ՃaÆCZG_yfE x:VZspp`|nFGF8)1,DUUդĤz!U{m_98-5|4l_O#%YњedZ VYU?zmKа LྚPР[=80D?6P槏O txPD4MUՐ ( s1spֶT8bE}y06ҀtpU]ƒ.%*ZUrۖ}iҢmƅQt͘US@x=9e_ *Ȭ.wťF3ࣶ%O콭ǾMm""56ejm/{Ǭ~pwH3]SXz=[fwVMjᡞS^uӵ&,Mbyլ.LY N_qʽ:c6⻟KGrmꆡW~\4M+^XiZy^2t؈AUl|O [γ/.4ͽwԇ/;wĠage9e*/WKϙ]ŵ7~Ȋi d8ٱȁ(F+ݺSGv^Z$xV5sqjUOU돦K&֦׾[zbǏd[49:YZg'Ύ<>z㱋=W˽\.*-5'KWZ_8: +/‡_{oy{3\j8#"}o?.iayc=')))HϋUOwϱ'[J79{yO_(yBPƒԸA#?M߽yKfX1)o^&ԾT;_[=Ҟ7jh~;Oaf$g|YY[~Z}nx\;+-i>8y]3Fe.WqN?e/?>[D?ԽWd21ڮX¢cb->ϰx">?s=!mb<Eb( R寪*5VQ\R5wESrW5}j'eTUD4׾ı kVWŋ;ݖ|{ojQmNͼ;([lWd{iK?.CfYU%,:\90Fc iݖ\{5Z{1i2D2! ք~m^٩a,ٱ!_#UUADTf;vuޜTDŽ筩Qhvk֔=tn3{ՏZbޥu gPٿ Vgٲb)R eH6Yχc;3Wc8˾{ע/D>>5nJ JܚfQ+*T4kjT5{44UsmRԭihr&)1MGY+q*/)iB y4MQo4r}sF]D|G_4H-iѲuӶYrJF+"e+s%dMD4QͳGoC}'h-\u;deO_]3ιW󊡃Oqs8}RBxP7+NR\tڱ??˛:CH{<{~=maK$9x.szf*>79wl:|7g5x梅>kh-<\8mLI_?-7kKF#v[>h_-Ea];-kJN&qEKWo]?mBA))}"0GxE{s4yA$-&-ιOHxSM?懯g/,v]dl~ٸ!1qs~fiV)?}*1M|VjjP=8^^ cyc_^};O{.E7hqm^G0.W%5(ܖ+ɷ7&8lT{9sfťP|P\mz2<5Rܶ8tShoQxFmX}kE+k_śT.t}o_#GjaM_@$eOϧTJ|sntNߛۈrO{wLHDw>?4HxA>3oj 5n%G?AzNږDb~ιÛvvՄ tol=ڳf]Ar3;7HI8Wߩ{B+ݴhIAˠ7*9`vnK ?w˲i3^/iѧw*F!ҩꇩݖ;Kncbs~r~΍[2]wg=1%""b͈Wܶ% \bnUfKhѵݳ"  NtDqݚf,vo?H:^tvzp4Oy98Ak[*Z3gֱW^/,퓣*+4k+ԥIXEy{+[ާ]>7s k} -Y={4p: (o<Ժ}|JRlCjmĥ匛98k[RvVH[;tڛ<(S-p\.%nǷJ齹w=&3t`8\iN:Q +ӼCdddqIqxXPE).)l4kl -īι[Ozvy;_uؑ5o\D?8o[r׌wk-']vBfZq3wrэM=.rV[1XíZEI'[tLԥ@7_6+< [nxIOa--+ݻoo& Zږp??qe_U{Bzt5nwZl=KLZmEjjEyŤ2 $CbB۶o+++ a LLH϶B m)vٞdʈm,]?.\#õiʂ-[D{v.bf:wỈ-KxR:.@x6:s*?Axpݩ)KH^媨`p@-Y2Z#""kX\z5͈j!tڿh♟)O9"Mܾ=ψS]sR%*gN#; "RQQQz118u nKꞹ7>j[zr×g5&I::<8Rx\HpN5Aq'JqtW~y<)"po/Hw(䀣WSdNPoՋ H%2)cQ]+30)Ӫ4_xVH&}a@ֶ&cƄ+N̊VXbkC2;1%.h.x!{a@$mK8>aޖfDjP<]K82` ub%<^Mcl5Xy>5zN ,OEx)h-Efl] 9$ԣ= |R§:sHI2l>maqpRg6@ nK1__~Z+hGerL*&,2ٜU ,Fݖ*rtڿe3y7&b6)==eW % o[r-"%떮)>*,([0}  Ex/x;sw-3vԳtLЯT[XcRzoy!$9(}|XS@x@ |ےwVڲ♨H@x)h>=&'޺1nB`BR׭)ZX3E[9"kt IDATfH6-?Hۖ,Mڴc<;@x#-zcJWOc~D qL> <G%(ߵk{YJęC!<Pw~@-Ezokٵcޓca_:=rX!@4n.qC+ȗw(|4k{yX^b`3Bw8xz-z:@xx%u/qW-]nC9 @@xV2qht(tFk_Am|%/ye7Yxa_Y;С Tӯoe9#9,*96EMc0~@-ŏ%X Qݖ@xQo%ԁ%=?Hw[ұ{hݺ5g4b_8ͦAmI KljPt@y?Vm-(WOR5g <%u{ʿb-U{ŝ/ψ㘄6EQOa-IyS(<nKЯdLؽ9C |Rt[[UUVo.TM2NMވ8az~3 ޡm %|_~׮ZwY2l6 +hmK8>aޖf NqPK6C3{~-){5 pwL{jy~z¡Ztkϖo޵JC_c@x|#hmK '"%!.ʰ8& )Ý[fh[m)O%*#  1 Zے%*i>p>];us%|9iFOzh@@mIJxaOm6w>|kl~!( mK{~-/{=mk8mn{o.f_Ak[rZޢwoDբ|P ] ZRDS0uMUR随_\ LkTXoW-AA߶Icܡ}ZE\30 ` III ,I=lYӦ3$Ϩ _0(.(>?OR+Q^v@ȶ4߲*y7&cM[@x@( „+o{EN:wز%kCsLC=M~`%Wv[>yԫ$|~KSX0}vAEĶAf  AHOWUadȘcV."qaF.햊?YXLk^&+xNmL˦vڏr7GxӠ hLCA߶$֬SVoS5$"+)c%dy[vK8eu)Q_> uɥ<@v8u猋yR7s+U2:;_Z~g {N{.i 3U80q~--`iV}3&GSs%ߘpQ-̙:K!7n]<-n;'ږ_Ak[R㒚4ٳõgͺ'wnުIbW4FEfom1h`{^ǓY+"PAm)5W=nݽJIg*,'DZDDJ}~ĽwլoI?w ` *8\~-rtfqU׸S++4kx aSJ]:N?SZE/$0+<@NZoV_%*#7X|H*;x{1[sߛOL%KTݳgO@x*m%KTz뻩3m/qk" r7Z/}bss(ʫTWa;0j|m+\]zl D 2?Hۖԝ]cV;!+?wuFfX#oϞ]5DͲ[]c!sQ"bG'EZ94P4XCRъi[=\ޡm_,;Cܮ&)fvv-]r-XYԡuEy><5 30oਂےVVdm1ެ{7[)ˋ{֔C(\:,«!àm zV}hГwx-cܳċ+Lۓ:ttxpH{񸾇<'N{ GFy~m)kSegGv.[r6ž +h-E '+HO|f/n f@'ߟe٬R. Ͻ20?HۖDD,1-Lj4pD+ǧ9&+q+uQ C+(mKZ?wEٗm"" uiq1!Nk6k` nKڞ>[OwǢ~vD>7k}}b8& Ѷ dR_SY1uXR/7uym,}sݠI߾D<h0kŘ+mK9e-uIPDbz\w ni76@x%S+T׼rɉ8b[vQ pϜ+l""̓VWo <Y#>or՟e4WCnD|BA}$ ` l[]a5Pr5p {n`!DȊBD0v[`jcD>A! h[q%F3E1M  P/-0MM0&I@x@m—0pdL8e@x%Ѐ -8wJ`/د!Q@ Am zBR{hݺ5MPq@xɰN2.ږ_-B =B /2?mKh tp;v'<AĄ(PCxꅶ% BܴɄݖ((IUfI4h/f_-H!:M~ږV2*lg&o dжm5Ԓ2s:޹,&%Hf -r @ibnCe1_mJ0pZ@ж@x x=n:}dlӑ)fcH-DžhP~ږPHΫ_L85^s$ %ޭR$y7K @x%ԬɁf̎ 2@xꅶ%9E*?mK ` MjcѶdd ف:?9E}C!Axiжm8@x0ljb!nyږks+U2:;_VuϿٚL Klݲ"y.HOG"Nś|DDQ czb%Woo6lԨ!Y̙rz;vҎ?匑~R8a5f_&p{֬+HqfqI9;uO8ݭ=:#rO[9 (E*y#402nK{wS"-""،x`>~Zu$,*w!E*(?ۖԊ nL=¦~Ҋ6WGvXdI={ ٍLGx|,-YQvqWM525,@"qvv-]r-XYԡuEy><5Ś.|ƄG*0DU@s8kJ,Y񴡝,"n4MD4WA~VZh_>s쐦v !'u8Hq}=_5=fC1lfO߈%['+mfm 64k_HjW1ǩeM h[˨?4Ԉ>*y7;M'WfvxLHU3͙oKv w`V-͙$` -hh©-zSTT <֜>6f0VjusE5?([-M <@Ah[2MT#[d:Pװq({<@p#"Љ7 ,E#: <AB jcfv Ąpс@>yh[ qjqݖ -v/""4*Moف48']RRHDw(8c_ <Gi[/jZ e*=?mK0S$ 5i}Tfi뤃P:9i#C)D^"n L Yжm(xۃB-=DQ3!.VSi:~~jvb4(/{<|iڽA@_h[~ѶdȟbcWXI?vhC??mKzS1\mQ3ȼg:_yX6mK/ږ̯ NqI)y~ѶA (?\|352yжdن`låقVL4vϺ6YvK.)pbp@`жÖ4K;+36E$yږˀ_E֭Vp]ح荱VLKSLٹ8b~ږ13qpӡ][!W9ht8~6Dc)LCX'bf{@Mid's|)c%@??.F#.p$Gl03~/:|!E) <mIUD=wm:yTϣbuf.]AGN}JK{ry~ѶB*2V@hczBRE-TQS=MA^A-163/&x)ѫp9Ds:{<Wcy^|93/.Ѷ?[̥Lw&ےn!9hLCAh[ :ȆFa%-CI\w[H@PѶm >yn;7ye%+ -N:@ sGh[~Ѷd—Wj{3A%3/ږM)=7nK:λg:fq&BLP;@0P)жmIa!}LF3 mf_-)T wq 'h=!h1[~a{x$ږ f|#R^Tr)LWq+$V<1jBk%En"ԺK'O\;ƍ<[]~_il96PߨI%< NےeW󒧹\JXB!+ 0OynK`L,@ے%*%^\DD\(qV? <B-ӎ;&lEm[ۂuEq-1ORhgcWΘ!ːƐްz%<tcBmKԶv[wqQdwAJAP[<Ϯ쳰Ǝ[,{s"?P_v) f!?gQ2i)"O qΫ$1%4^c]zڃbiۨ: z'<>}e@/]v~#ܺtE\wnҠ.30PCMe[8ٛ1D|Ir\Vuvm{$?"uVw= :AsTY^I 6Dg~:whײ "DhAV:5wfty$pԭdӆ֙~q>E&WE}w$/20tFcNx$yeP]([*1^#ˮ^]SC3'_sYThNʷKըѢm#VfvyID%5anhRͫ^54gµhmOz_khejbfa%D*2KϳMzao2bxfV|͈e* 61dPO?"gea謷IBF.~u "^Tz@PT~cMBnVj䩉4SQɚ\"'(/ohAs.It;ߕ֦C]*9&-'G~&Q@>E07|WƜpNͳ#v뾵Q=A=L<eK|&S˥Q:j|I;$FL/<~%rqKP| ܨ?Hd٪Wϋv6vY!?RpdA*}/X  BHRR]G7n̍E.-$Ӗ< HJeKHʐ?- y(&< ʖ<! ([@P2L8 y(- y(C@Pdp@P*([ʏ*Q @U  ([En}-bBdW7tt=뇶Wd#Y EGFnvm:!.-@~:(juf?IqMmEEY ~#&%r]HO}h>l8}|#iϖK1!w&ثS1NMzcڻYkh۷w#F3JATRڭ_jVn̓gKfyBHnqjccfty{ x8M)R%yvx?y? !$'hg"b>n6kH*Hj:rokߜ"v6{\BǴ۰6Ռ,mu]&_~2G[)4Ր$%[k;E+M\7\꺳q B׼s[L O"-C&YzwS6EQEv86QBhi+@ tvPj9HD_u$OPĹFT49!-\C3 bH eKPQXj=()Eh\G4lEg\6D?:=F pt4gxFRA$Pa*})/#>sk =?vꡈ,<ήn=*KaZk:<5BT߻u*T98A%B'\XDlY/G!'vFVÐ>qɒ.t)3{Fͽ+#T/\7vgb?4oǧ{iP_1nl6Qc亚!f,"#⺍Y6E@!a#^K"[tjm5ew6L9ͼ^E΄eO-?l휩/0W&L?m]Oo][ NoM]S=8w7A=ή',t8ЇbYzݵ_/3h9kGCH/dke%O8 !kfɎ}QRGڶU+~kҘgqńG!l-i/$ছ]e* 1%:.5SCx.'d)B4[:ODUz@P2d \;OtjPDk>Z$C7Mhϖ$siPK@rsp SMuڰ9Wf&lNYvhzZm/X]OW {ٖ?gah%4Q|}Yf!V uAi4[FrRrD_1:>SV,{1qH#6_96ZeZN*sȦQl@#yՅ%U#Ip.coEưw-z(&9fnkly,!*BA3oɊrCP tjvYu\+԰3$~rwKCƆʅNvYgB[4S,v$-ό;OZ`  $7鼧^sc<6t[3!'WL@K[CV1)BZڬl1@Pʲ¿e1>[ìYZؾ#$jCfDڰm6!y% ag)9K5E?G]jo/6Rgg)R?jzCFM>K+",I~i"/K.նrжr{pnȡiZ=6_oB)DNpZI<-݆G{ffY뱬gxuawƓH@%9w&d DWGlv2I6%04ZO?w1&]X"ߪĪi9[XX諱heF&sLmu9Bưʓ^Df;uY0y*Ui&i6Y@>7$͡sPm4UlpZ?v@3.\cS[,-͔xв;:,Bynv^VaYYlc߾ƆMKw?۹a)j:#^ nfr,0-u#^&Bq,Z{nXsjhc{xZoD?Nۨcm> y*JYTw k6oh([UHʖ/KKKKK<@UQ뻫KKKK B9 LwH .TDPb KKKK*/zy_-),,,,K0Uqq(Gy_$@ yU$@0TP Aa@l @%$%ÄRA2eKHJ $%$eʖ< HJeK*˖IHJ˭ A &&FFE!8N@uF@!rCBRBJj^U[JjˈcCc࠭]&:Pː<RPTwo'.>MKSK*Viij9:8> }V>)98U's hkiؾ@ 2$eKy:i]RN"8%dC_ 2$_ْ"a*RA"8?Z u\ː<@UW^eKJ)ez};2$SDaTB T]|P;.e? TWy}EQtZft]deB+s%m&ј : -n:,UQ—BT'eeK_*Dz%R >&'5WŻ}0 ɟ WHתK#uM؄BG8jw{͔:UU ^)r"'."L C8# vH\6}/>yMsl>0Ϝk+H2˖URH:1k_:ӐJw)y|sPLDU䷙}$IyOXY(P=蜷w?KӰQUDE5,U/w΅>Db>۶.%yԘ 4ceHdb \["g{2yͧB1dq>FK];wQɹ4<||PB*F"YZ}o'!hkXV֧Y_<}>ULsDk/<~' [gZX伹v僱yvdd׹I=KA)2"=Nɑ҄XԨl.!T܁*Newe 3[!ѹV51;>'z8<;q>"Cͪ)[8xڣw?uDhAYy=]}6"SF)}kj ʂ%7?PP<Hܓ KM !c\1yw[fъ&{jsu|6ml:ۧy4-~Q uyEvbtjxo_ӚIl^9]q=P˾$2_\zplԌS8$mZ<K3(!=u@NJ5뢠M{,'Ki<-:m|+#vCDzrPxR~x7}|o=ky{y9(aSPDawZ$ۣ~S8bgxIG2̸&Caǻ>5b'h8a}3jzV#4+%U#S(ovR%/\|A" *Ga)+>aoM͑g]>RQA32΍ "*%8< )fa$IOB_'((#)F0e_w FCR0QrU7L|=le. f R4BZJ'Ud%bӖOakz6lҭOYe~ºt. M ju ! 5V{e}[ kHwu-"%gY]lRӐquܠCǟlCGٿ2Rӷ7OԨD! &,o_YD -g8NC׌),jEvBEu]*6x[LP04wߗ}z{յgup̚a<:N9'Mu׳7ia l@C9ˇw^ZjfŖc|S7/y2M*Ae;kiB}^t_EN)G$eeeihT/n E|rX+[251VU%t3S3cllK *?deeT.ː<|-h4'HB|B@p~<8Qoߊ555c#R@u`,C@H-) }cJ|:Mr\&!8N@uF@H9~I!2 D~ !Hy^߳vabնvk:O?K>:;;Zh]W5[_`8 %OFqckjNq"tWN5C&Gt}̾5ܛb {y^ªt\oڂ y=yM'bm!̺}#FDZ*Զz;ך>MKW߶=gƯ1!CBZi葓%SomϪ$ny~ Ud?qc[ͷ겪ʧ%iԁ=Wr:[gnmzٕ_ !O*l!ևhW Wʉ{-'BN'+M`kּ̊g_ӭ> @uUDْ"ulo2k#O: @ Nu,8~p~ :m>6k&޾- aɽ;ߐ# JGGwnݨJgosrr؛JQTffYYY%JqOqu\k)SQ{f8+z3h}[G}_t}[48pG4!6<uymܭ5Co !iOû~H]\q,jz3<:(1lY[ϭձNñ7rg& ݲorAxm|Ǯx'ν;Y1ǎԯg"OgoI矉O-ݪ_L籢{i0er&56mš F:ZzuW/o_A#vûAN?3ZN}=;,rwjn*$z VULdqœ+ݤn'+!ɋgo-yx69Y^y; lۿa9_\dEBEq"n慑J=ףѴ{R֞=>Q~'>-{uabB"Ά}=wr(;z#obȚ4!tcyzk~ؚt?5j{72f}o䣼ؾHdN zxtz\v[_˕/i=Df5\jtdqt\jtsu4|<|n;}g޾ ܒ }};O>ܰ,!yxzwfWNu>m;~eNICC]'V mW۷~%c,ʚ-|=|Z]0#*+RHr#źϔmkzxǖayhba&lm_S x|3NvE/mلY ۷xGEsFz&XDBϬ|rk :77@ޔ]]}ޛSRqh_ЊBtqtWGs&m^C߳5fwڽo̍a{&/w[MZ6,d^+=-dl³q ";via0ʪ]PE(6EQBWtȈN6g1fݛZ[4bLFpr1nپy:ֲMQV jiF|Mֆٽ9xw'.aۖ m ObX|=SOCu-?wZmv#Vnݵﯹyοbeңg.{}q%0/c+Ѝ#'v_Ў9F?NFg^2hknvYݚ<ة>3Msg}Uj8C;]vIGϞt|JMɷw;]|z[yٻnjߛtގv9\L!t G'.39j眝/%wMۑhޮ}Mn(s(j5wl"}k̸#WlY3-0H(Kѱ\H$<M*4hXpb]=rߗ[_]]/([UHy-I"߸z)u)BG?Bj5 >CBXO=9 X]50S,}#iǎNNI 쩮޽[}&'';~䷞:{aD 'jp/Hv؄6䳑yXrݏZ@ΦB8.SVhǒW{w,rjxI}X]DyAAB&YWҜo^+onX}xLԤq흍Dq߻w pb7vڃN>anC?KHuoݿE8tב3HԭlMغG~nF,{'V*+M [4u_9Bujikks!FilcGh:fkhk +oH"&qċ='hkkQt‰ot)l~_4i=ĮĖ7ΛȜCQڬ8oDbxߕg}l¢R;EF4.R⸗ɢj5g,,M[g{%X\rYBKG4wX~hulg%d})<[oDI{aÂ+"-~B[FEi>w)uwo(e,eJY B{caD*ǐ<|8gmQ[" %yzuu>J\ܤCSw3mݸyKY?gTb1=؅cU/;GUNEO>q/D>ό88|U`8V/xꙇ~_7YpzƐnmz_jcQE@eϬ`FA+7aSpAܪy`F,u]4M!)F! oX. t{W6My۾1ltwv ucѹ T %Ċ6ڰۀQe˭WL୍w*݆FI-!UbWnipKߥn~kk?N]sܪ;w@;nc%yGL[q"{#ǔOSP}T-Lەǒ{U΅7]tYW>IEӂQ0l׶2(Uaj W?,ho"3 @IF.np [ƧO^#oL:q39y+^ȍ^,/T= Yү(1!,}uG'3WE-{6w|+3"% tU*<T~]uNsq;(BhE:.(%S}}]MAU}P7 jU4*gRØ2`dzͪZjDGee<9-~7{,o( j)NFKT0VtLxT , B'<{-JMSxU!WsdIY>&Q͉8&I:qޕlyCکeD:s>©Ÿ_4=:h6;'~h:u\h,?;OgȽiReW񋥽Q:֫ayi]g[ӈ;pA;f5ɉvW2C_l4A 8Jo~|4UFغv^QOnYMsyNͦa 6>;pآdZǹԈw%,Fs7OYlĄomQEE9}AO׎M ++_|ՖqGiiNHU;ڈmbԹ9Mi;f&rxh&ihGU"STK?k%b7cfÏo=pCa뻶5[c5o=G'maWϥ-,CƮw~'o9yͬQGE0!p;vs!gns؛Λp¨NzXI( :NJƤN灖=\=v@ z|qͼ\ݚ"7[1{uVMjeȢ?Bx7|VK/vV~IԖݻ9<ʱ! ˱mרӥN&r{@KΫ=mM9JsԶWڛMغh/+uK^>rj;gvh;u9f>mcI˒<\ kȞR,,`bWB'jCέ;u|{Mt[]PA!!!AAA崱ܻۏ# -#R6uxTGϕ=@BA !R\KVܥŝ[;dcW?<Jf;ssϝ9332O0Z|~ ?8ط· r$^K?; QO] Y/~< Zmɨ6GH{xc:EʄkbMx_!m[1[l.DBAaTHڒo b"gjܼHEq1d+WVT#/L[N6 - =$O?\m"lJXT|u͞]7&#[!H~CwV}:hVbiiU %B ll KHE|`aj3BBBBBTڧҖ-EFG__􌐭VVV$oZdL@ FF<$w@-! n&"LG?D"`R-@ C`@-! 7u$Q*DB|7B@ @|ha$q^z!]x2@ @|)ƚ贯W"g0>dj֜'0djܜ,@ H< _-!=) 'C# `R*Ձgjq<z壣Y-6`qА @ @|&υokِ >i6/ԒN5eI6wdxOnBY0~cJm>Odc6gslY-yO8a\W^g3[75wIJ4$0*fv+p~GW٣L,_on1 -Nr!G93{,Bnl{6ul}JcWwQ/I8ӏv#n>k=*T<0JE7w_}Ȯl"*|{hSϚ?-wwt֡;Y}k9@k^,{WB`р"ŧۅ.l3)`e[~L^'[리J|PqMO5F߫?9BQbR>:ԋ/t.YR`_3v$lN0[oAVLٶigsc}!V)ّBq:(R]u%NW #8miAZbvآ Wѩ/f avb{]PjQ5FHDWU:!@|-p|!P({dCRrq>ߧ{碌qke\W{8F:Yᙻz O`' &s6z8`<DO1&0y4%*<ɦy 2fӴLO;uw'ϣ[6aK.+rLEGJ:yC,mv].t idQ3rVq]-NDh;z=7@ZQx{jk$ o5R/1tXUyZqkr{[ p>5rc-EM>:m'WDO,&{ksE:(Cfڶq@sr/&y3W; i]sx<&19GI*.^ZӂUlOLf r/v_z/3AڍaX&yJ%iƵ_rY:N[IEZ1| uNފ7JX̶ïmq}Ŕ= Km9mB67S#!Н=ߜ㉺V'\8g@:?Stt %(6]hժ&_)T SGALHVxN6YqFų5WܟcuxN}'x-nQ|U73m.} &p23TQ;E+J`bٍeP5Z8}CYb/^gC! ůIz|c@UHנNCD" \?-q8\=5}Քo^56->xIlboRIeGt\|G [褔_W<7D:_W$lI܊ B*vQV_%I`YFgtq; *^%vm[L{Fcc<;ǚ9ð>]]W.&1 pVQQL:q9edZFd8|LbM|$&?OJ!wlkdd)㮖߯r4 (ސAOaXZ .eq:<˅ CO-V 2$,`aS&1G_5K 7[(3$tt#n M*'i kDIUвROxUWFkW:ab?G'VDeu4Ǫ4<)mQ)J(괙DsmHZ2βp`+7aXփ%cS]:#We\)[G^eOYd%-*98qBeVUERZ̲, <8WɔMic9 c"[v՘8=a:y?e2P^v.J: :,c2X&YQ{^l9úe\)$-I{U+$_5P̷?FɨRzU\<~][sWR^kMjŧnbݲK{&uE,;Qo ٪^+zƙgTzk:_OwizojRͩD Υ r/7L1myQj@PIʞgJAgٙwT\?;?`ޅKpI„O(N<ìoI*L?=W"cOݝ~O QhcKаV+6'05{|eI7ڼB!mS*U0Sl]cgnɚK&&\ 0R".ZyDP_+8J܌/VB+Bv¤{zl`aQtf'xq>-5;ĊWFv /ma:OӔ#K{H~BeQJd0,R"^ A<^lu'?/Ӿ@nc+[x +N_~jJK;گa1/]., WV՞f}jҚÇ] 1S&Pj>Jrb5ʐt6 Sp?a^aY+'.Wԅ둚sy\mlwxI,fNQ fݷXZs-:W*ه1],:Ӝuf&T\oze]AI̡"`eNg3$(^f9Li鴡!>!^MOYd>-ȽV}#,/^jٺv\Ď5+[:%uO$/1֮ӻzH0q3.@a޼܃b܂Շ3c+R>*'Q*ϞJ:` -Nu@FJo.PU1jn)rj]1 T{NX,Ϲ9A2BOOz /=Ks8`kF?kA?\YPP&MBBB6%x|ۂ FfEG)R?9贯I?.&z,nJXnvtKr(pMOfs/[f+V"PF֘iX*v9Zykǐ)SΫEJ!!!%ߏbiKjs2>VOjuW"rsiҔ◔QvC񸸐K )MC C0IWaqdTHqG>.4ߥs5.9q60gj#9(Dqe0Mѱgm5% W>)$Ģ#5DjFʃKb:N"e>#biK?c B3ss%JMG H4'@ x@ F?ѥ-!Vr9:7Q!tE&H< ėF.2zC )KViK!c"W)uOhL@ H< _-!F|ąXZ ILc{E}_{;>I5Re%|ĥ2Q*]Oi3[}+aC@2jK#B,e/ J{O'˰874Mt,$_1 uoT_: IDAT9?+3@CϼbY0&ŠqX=Uf *d; C@ x@ M[RM|08Qգ^FS9zrѬb*-P$@0 F Õ!6hg7]QxTa4j1h$Nb5)g P? Яײ>˷0k [7xGߪǫ{R;=ɤ& $07C'cUQDX8{~^x2N0a;f/ зu){30p1ش% |!JjĞ1E*.!1Ap ?k9DI:۠>Ozh5"5霻ng3k_]Y赸qiu46@D)KS*lIC1O4t1+(6s^Yx|, /;-Ҏ-}Zs=K \0rςwz3FSi2v:[̟<|Ic eVk y0סkg60p|KX+7WM4~f7Q_U+VRLʕ>۲q?L!.xg=N,oh&B!@|^?-q9O-bu̮z$wK4,\Ef\%B&f,knܙi͑>"thX>p%xϾg+6,"ljxju`0}ynVNncCwgT<ԣ`ɸSO/mfSGGn^iVGW|.K~Ӕr>smI1uu;R0<ҀsAfsuRauBʼn?S',_}>JѰAY??k[/VCt?9p?Cͱc܂_Z:pTa3NՎDWmUh_P{mMU/p<,K˵oڼyy\5p9>:އߜSE1%.>&kcժqꈠ!N|laͫsƤ{:W jKЧ^rFVΜ[V-޺zeK )|JB3(Zy BBhLIzջVB`ƣ2 g-OMS{գNʯԑ&e]'GNa1==j㯓v h8_1yG-I'54{ј)nj0%x&"Lc%®ŪZx9=ˢtZuͰ]SR84'Fr p Hs73z0SMkrFU#E[F SnzQqރzp&utɔ9"w:;ExלQ{﫸YjĜ|Q5oIiא3΀BǮύLg82Z3/Ӯ[9⵩Xaߜ%̛N h5*1UK7m=Vm-I~wh"ҋ?J.ҷŖק{tz~5i#¸Cq#XU̎Y~=BC lKVR#'{6{>t,{\m̎PȊ9 v8.pH?EPً+wۥAx߄hoHqLoe0:XLU: E" V< Űc)Sٷ܍ P)J(4维狻 Ɓ%֪Z=@j14L{7V:[4qyy E=--Cȃ*b|zϼWeIn9QgY?6vz8=?ﺕ\d)\[ĒD\Tir-@К &y&2fuxN 7BZЭ^5ex<Vc0Or;Q>6 ԭ`J;9JgM32"''w] ّ/pWa`)~UDz3Ʋfrw($V?xsyWrh25(KnozO =Ӭ%X`)y7#!=m{{dރ뇺S% *Fnle/$1R&ts5?T80 @JD*-=,MD/wJY("E*YmzD6P^dDdGdjXejt.JPˎ͈,짋fn&1`"o+eR\iω`|y#Ya~GN_)*YyNNR>μaJ#I{wK@vzu?l];e~ޯ(e n-4?'pՠ*d0%+mvn#I~gv?s/qPVnfEefYIm U[d.ߋK=Ke \/A]kt_Cۋ(}bQg#M ڃ:Y۶dئg7w#̷¯joU,tDOAݯ7kL%u]opLp!M6t4%) +܇C ՗28oř Sc})[|67|M'4RȑEPZSlyP^׶Vղ{W~7;/čjts!und?6Ml׹wKw:OhcWusNb!3g["ⱋlVn]_pKNT)4rD$ ~h.iidW^ԛ@JO-X# %&EͺzoٶrFZUZ{G\wmC9?r.vS~odYpR)re)B.'befn+lڳ]m[H4p75Xl]{<[cȍ8fnvB"x@ M[ruzŲl[O,J, M- |&=[1{Qӆ3~IM=||ƢF;qᐔ5սc.^?sLR­#>KMyKf/[;ȭMc} 7-[fUOpoWz{j5\Ҳ/cwɈQkU8հyl mҳ3dN1Fv|TF`fNb + y~(I B2xv?W9N6rYsrR]P>7HۂS,Y=kn-iU-z& ƌn4ea;לX2qrkGPaٸGM/w]Bc/G/ oq OXd؋J_ߥKYW&.ZxAɨSaV,LfIQXV V[1y?X|7ƶ[fv ׂ.&@Hj 7_"×M-4@ߋ >D:w?JMI,fr,s>2~k]Oٺ`L "F#ϙʲbrMciZnHX!k"*H?ҷo_úc_/>]²D*TV껰9rlݲ5r)$$dq% s𸸐[ӯ܊0d@al?.x$T+K[B ėc)˯Af^HyKb:ZLTxPk| @ @|~0Fz,437 xJ⁾~ZdOKA#6UYH847Q]iPUWL+@RPˆ8;y,@ laP@ aF p@1Jn LU8!^QFYvbξOmD@ @|(m al:q) wV쎩م|6UQe1AYPn=*PK{ BdD⁦iN@ @|):p n^63Bd*֜VL+8^&, hi@rP¸SPOP 0+^ڒR|nx?lj4x=sDf=~ T#bh4_ria|v=G(=jȃ@#}KiR0ed:ں;pJl 7mYÂ!x8alѕM3;t!}[zO- <1{Pz\bʘ Gwo7 0 wJ' nЧy`oS=+f*iVGU? 5q}y -\Lx.A%W|Ѳyy^#ic[/Zpۙz q~PI%7 -=>;jƈ@acAY}Q!-'޼v}35 mc0Y2'unЪnj/g77QAN&wI9`p~Gq]Q_[غ;HLҿc6yv io^؀@߀Jy: hnKLս}{~!FҖ\.*{dѪ?46"K_ eRI.HZzD%m)~hDgϟ0qN{eǖ-z{-~\[ܖ;6{Vgl#) +ۣCo+}G(#msL]?ދTd=:6xүt{%b7 ?p |$+6s^Yx|, /;-Ҏ-}Zs=K \0rςwz#DApi*A$9rz}9콇Z<7 }ܴË hcŗ6\?Yq#,*lfFo=i@on@gl^48(nV-Eѓ?0 зy{47+@_7 h۵&(آt\td[6>n_y\|rց[5 *P2v]l5CTOjΜt9}`LXMpJsY]EFIj6ݸ} WL&IxM|\<tIآ^ȓR YS_y*h]DiUï9k1b'4ZKgԠzU6'9FoxTP{5 =>K-PiV/>JAM Y9]#߀-{oasa;/GCOWEe\?:mGLk}xk70[oݻL?&( зqc7ܮ[%=gh]׫vsVwǍk^ i^ŧVZԪU&wzʤ.k6>wOñj]9NN[ˉ-q8\=5}Քo^56->xlȦUe7!wt@:tu6%IrA_ۗ_ʦӣXhB#1 MK5 ׼ȍw^QtYSvs@IimO7e){cNt.omso[?޼xe>r2ǥs'w'D"UJo&(u #2K[<\G1Zw:ח$޿cV׫5pH$ m 2YI`$ABoj@# ;T+P4p¿ 4Uؖ3V.[#ye|#/X=#ycͲ2s:z >v~V*ʹz2Ur{eEت9d-9xx>U ۊQ[rkVDdjAzzγ׎g^TɪomÌݏ`ԅJg*(Mִ'0te[7"P@iKC`xiK)4jքh{4tJ+PiNڲ]LB`"œ:JpZы'mxU.9^El]nx3ϋq{i;*gNlŕ)}&бtkFmWVXvߴaJm@Cci;@w?_u9Gy T啡Ck0?=r&áCw.?~lC\R+uJQ4NKԋSnۘn&$kagoM *dd)J<۪̥\ ?)YeYZ}Isێ=҉\ߡt;aT孭;%>! /17@+8H1[Vw[[s'vJdY49(Y ԪLJRW<1,!42_iHqYМJ82wGeik Z{5tGfVci> ewcMo!%_YR˺wUyg5aenIQ Z,~mъ[urЩ(A'jK2Km7ɃdCo+ ʚH"*T+$XlzYb.Os]<]&7 m}uFKeywwK6]cpm)LmȻ#mtss Z~WSj^:̌[3(gkL0pY7s\p=4Ԃnٰ, ǀa =<$^:V*ur5W&o"7 LQ/_'F_jKJ@Xe$)~m􌛞oeML9[_Vپ~J>u4fitJK$ڜyyҹAuKλӕWd]\ApL./k4uY̟| QG@0J)|4p@vtve/LJ<eQJ@9԰8Vs9Բc3"K3x1F JTVs"2;{g|%ʰ|#/yhs[dF1i89XOٴi(2wssswsswsss0kgK.o{Fձ,IlWF;Tع]YyNNR>O 2A&@,d%Ͳq$Ig%j֌,+A|kW{`48?6 g#` hFREmR J[+j "JQ ," :UNJcDIp I_:#./Iy܁_g'3;zҹ/ˮ\tNמSkcnݱ͙9mQ'n|{by%gYKvV-{O0}ks^^vFy& =E>57=f (ܴ2)hש]ן}2/g5KXtĒs4OFku[$X?~N7mʵ5{ȎFYyoNV[Ϭ~FgNjO?n׋bPyI^4Q.w*{~Fgn?ismWoy7ugWzFV.[P~ /R[|x_GVϞskoŇ=﮺xUUޚu={g:~fwصf\ާEf^CO2~|o.v- 28 g/6_ܵ倉sñ(s_}2wkm]5s쬝uͺ~%sC%L)m[nЧqFF+nb_L@˞C hyז2vœngOf7jpW=Fu>?iHg"3_C [kkK'^1 Ϩ|a6ev⫆wk`HFFoaVbX5;}c{Er@ϻk}f 8㧠mXSҾ-!t9//ZtM^pii,z]C޼q:;\~Ua*0_6]Ut#94}.Bo^z/ /y`BiRrƈ[f,rϮT{ Ma3rnSZ;{.ӕS*w|®2W4TQQ1rȴzICjÎs!-?0c^}MF} V-9է"Y7a khxN|[iێe+.j`=Y_ywV~7KGՊ%8,r#yG4 !dF~Ǘ]ڣcˌO߷}rO:s BɌfPNV$=Tvm քk{>_x쇐~O[:FY,vjǤr^CS 5G!;3#,6xW.O- mݲb?#ilcذWAM bGy滫G=kٵkW֭O$X-A;ևM5G8c紏wݹyK8hQOlcQ^N֕\9Е!/7o"|Tx!-H́x-Hx8Hx!-H́x-Hx8Hx!-H́x-Hx8Hx!-H́x-Hx8Hx!-H́x-Hx8Hx!-H́x-Hx8Hx!-H́x-Hx8Hx!-H́x-Hx8Hx!-H́x-Hx8Hx!-H́x-Hx8Ll7)++sppX8y !-Hl @<@Rۜ'!gC !`e.~sJT]v+~ܥ5k.ץDŽnB&꺕bv6ճ})ƨ-+[}ikEP4AB(Jqㆭ ,˪!PaxIZ#G ڥ(u;h?32NOYX{uH>U6G⎟'#CPɲsNٳ \~۷o߾m֬ 4M㻎BEM}|Ivk`a3^ {ժׄ ڍm߈2/6QឆBukܲF(e&\:xjݶ;)uh5GQJPjW޳yjB65@V5)c&~^^=sUoݔh6"{ceX_=/IzW Nս}aZB^SSS7o޼ziJ[ptt411.* N|Z]jYs2%9{0 B͝\h1X\պm+=>U\ϊ"f3__~Xm9i4J\ˣon2 0 Pn 6DOYYZ0C 旅jCJVVjB5hdS trɚkiEeG6IO.O0oײf1KDZu,0V@{^ h׮]w^Bry5"##@*_pT(M3ZŲeʬ(v?+T<H5i% Ufaۖ{sB((fXcyV%D3 @:%UF&X L&ٛRגIZ|:c%>qÔ%!#ٓݺu*:e^ !!ŋu59>* .-R|yYkf7{l|( K0 %C`rͥI_T4w Qf*:t9+2 }oP wr${Z5,Ȟ8gRa@('1${&khN BH}W-5`7nMKK+|͛>|WWWa?|@QTڵAU@!NL5n߼C)JđIy$o?nC+?)pM+:N^ri]wZAߗz?Isp8HV6d`3">LQښ R@f}owGpvvUEQ üy&""YGW@¹ !T۵y ٫ {^]CLv˳mPx3ڇܰf_{k~ܥϧV=٠z'OVH 9x9.bne, uysx6KmVK ?$\ݹ$\KQv +5ze_6 *iAUp)n.HMM-2lnn:u'ԯ_ۛ1 & !P#D\{/|&:aii!yA B|>A@KTmŲ&3KbYRLl*gjAi 8%~,͡ǃ7?] ~ңK^$|Qשܶ2$J$ļ;v{j쥥 ND;ijpXYwhiQpC(i^j:GoFTA(NZj҉+bY^ʩq=^8<༨Cz&roE*Cm߶y|}sV?1L̜~Na}[2gUfY}i^=RW2>o3z$r+X0Nȇ`%]u#BpR-\^.~bON-ל ^(FooLt+g>{svUZCk';via`"5>Y2ʫƝ dY_Gu!6큨byvQz=2?5>(^rmA 7vm0طƩ#`_l>S{\bf=r^|3[&ήkHђXִm 7{Sٽ1^l-@77 w"$ BHBxQ Zd$ ZZP#{}:/蹵 ۾20Q 3CK(PL+$Lzy\ HKPB=(gd6C=CPcIy>u\i(}چ3|ɣ{ݦ]\ T$}O׿"."BZLC(._ݔ&w8pј"#s3C>| Y8$?cKj¤ #{oe6YH5?GTd/ak̰vgϬ8"s j߰•-9&.E%?uR҈P ^;w]Z05Wy fW3奉v*]W2FxT)ı]\3g3va⪂l{V7r>}u7ͤ 3ccVc f-XˊyM^iϦ VCf.^8R2~ɶUzirD܂>Ɉ;E<թ'઩M.zJRHMl P#W==gFq77R5t*c ^/سpq.>6ɪ#뻏^=B+>]}nڵqp5&l^25DL^/k<'.q}Т {7-WM/@/u;aΙ!),۸{ًXBV.=8R&6V L@*PVCP(x~W{@!TQ3Bke5kVحaûeOriBM&UcK=y|KvѶ jZEO?)rզ !П.44tv P$F}Shzv; WϬȮ'sϩ).ls69yu&ejz޽mM_fē"l9 c:߷.ܧۗzneKίX>{U ՋOi)m*ĸ }6IW['__`r|>ImiܰN" ؔG6o:qSWa=3[c~mϹJw[GO }'uֿ~gi*0mY1T_wϿI5pi7bt7׼P^=/mi!9a aC&G7l?Lqyû2膍}Ml #{idLz) CBC;&II}yg͗W폝-?1Ģg2tWoI4_2޾λehRJrmI><g҇.8̫SSnbja)A۽Nr3︺d&ӥ̳RMx&NMD :u4U쩩- 60w^5zsKArLczϞ[AC],~qSK! !@S4ќ{"*11MbEns4BiZ#s5!͡!|1h%-qY%J&1JKgEs(Re*YBg̖=2. J[߄İlcDh;e;LU4Fm-R4*Fft'JZ(T2ik24+e-HM!m9֢īMtvvsݴsJf79 E^QkxƖkfPp\s.;$_T٠D.uÆ7P!R-o֬8x_bU{Oڗ;TA*V"IϹ'I 2FZqUP?}?4d[V-"}|7m?n!tÍxwU@:J:66ֳe߳L{aU ړY͂@TTξy﬜ϽQs s伿Q_uiݎYe̅oMUe#y(# ! H-dvB|*j䫽|b6ɷa/1WK䛯SQ=9|2U}Z~û: A{$*1"B*)@-@Ѭ4K- u{Zﬢ}_렂tmP(\)`U\My!PL(Z?93꤈++ !B*  HE*BK8;/!BC[`RazբRr^ : 'lFYhP?TE,! aRQ+&b@AyHA7 pԷ^ 'B0X@!0T@:w$I@!Gҡh0(tOB!TB?+Ck0]B!{4 ԩGB! B!0T@H& BaP>0 !BC B!0T@H& BP!hߎsfW˟/1\xgWS~n]͌s[w\@:QNQ$B*.öl\Uػ ?x Mc֑ro$B!o|ySObBmGwrd \$V߾^\#twt:aeYqmˊ)G Iļ԰ÂoDuwqg'Du:MgklA[%Z8*E\# BFN0/I+va'a\ϙ'kݟk[?S ݺLҷ!$o|[5pȶ P4 4NɁ H!B淎*^>PiȒՍ(p1!g>u:|]UwpzF9ЍzfMb]:րsW"z9;hDq%z (664蚡Ʈ-9f85nKuTb*=!ioU 9z"Ep3ߕK]+.͘w @ kMY>\篓R%իSy>WW`B! $ȔִFΎcϬH߲u6% 5'A'ٻ:PR(J,P|=wI{e۟@f -~ rp1R=JE*_Dk 婋J)e&kޠ̺mzQjƵlD^w_$B! aYyntGTd?Fim|cݟ.G(u[9{/l<t׺=yxN@OB!o;9vv„glUBYYK˓HQyO4wZakf!eXEHHh(㞽S8li#6{DV.Y`^XezTeF?E=0ߧԇOY B!tƯU ʄOBo}u7ͤ_jg$;u3kk9r}6;WYo|gۦM_*]I^nԧ2 iH?=ʭp$5L@!*ᡂѦc~7ɪ3GnT-ƆKAmXs"3{ DIi>uzP_rm` t׿yFsK^zţ6Z4aek1ޜKiܹ\Cܙ|KN[i0oTk\g^e %sy AҞ]UGr$e#"#xB:KOgogo[ږ..aV>Go֬ٯ?PUԑ7ݰų;t./}OQ\]"_zimemok_<^V>gM5-诡ÎnJ`+U040T(x ]]<{R|B|Cg) HiiR#\.w_͛ܘeY8`V>0TZ/-@IZ( [/FޏBU>8tB\eYg#9!|0T@DQ!G)f ]dyAs=$UQ 8|ӚθD ʧph|ېD P H~Ĝ{ȉ/L>_iKzxt*[ك)Ma^/̗Jb /9FXrҿ<lKmy =?[5{zx =$"M}t䔛wM ?xd>|ym>M'}۶m+`X<|%OUqG ˏTBQ6Los;*Bؤͻ-=vpQޭzO>شeñӲ8~-~cORyŻ"~!BlYgV&U@U>8֎e\8_yxuu!Lukay?8gSׄ( !DsmdV-(ES{)OǷiKμ`5% slN>,>=29YϹ]G]eҮMla37ۺ$om}}N8U~|->VP=w[o=g ` !^ͦn僣 D P4+ q,t&s;VD .^{͚\On{fY_[1c҄^nXsʰQO.Nbh )`&wAGXڼIǯQQBMnفūrԓPؼjݳoy[7RQ]%KV:׼Os# '@f<>ue~S4yYEmYP%^zӧɒ7GVn[y,Qݩlnf} bX.,$*%<},K5է0eko8^8dޭ4+6-\1Ag a"m@XVsa #W=?x߫׍@T96MV cQuM"ٳBUZKǼi~VR,!e hF4/B?cI C4s&6[IKTQ'?%luN-1U DOײ8yUa R@o`$ȹtfDCב]=T9 })J7*F^MN IJN:zQaW/%%%6^а{\k߉~%*EhbѹN%'sv"|rUiٹ/ܨr"BR;t`qejT3 Hc ˨SQ|>"6UpTK μ;.PE_ ~糢w|y@73r#fndD1O\<]kR@{>ǖ4}(IF:~O-WBk|n_;L6|`O}PZ9rd+{LG+@uۃ_y@\jWE:]P!:6SԧR}RRV R)X[p!EQ2 }1SݘʭE Mhs:99 {6صU zƤNkUyˆ.?GQvVM QLJZ iQSF_ݵv' G` *%ڝ1 ef]A-gY.GSPH*CCžc7m"cG!͚ ?˲^DX_͎):6g8~nxxx6hƒ"|Vw0c &,f?u/'ѧ[a åX"J%L&ͨJ5GC[Yh:VlX)P,R' eYbPC@4u!eޤǁ{Ξ :/ЙeY <bJM)ڸpdL߫Yt)BTT)˥P)8nTTτ P`S3gh{ޭ5AÅ_Q՘y q## p2R{8f^8}ˁSwyS7Td'XJ1qq_a֞z$LN'b&H^A?k_u38?{]Z[,פrvkQ'^ XI@5NQa!xfNƲ[I=O_~T1! C4*&k|6RY{# @̨ |*Ok ~(Pq/o@ĻW,Kmܥl.C {'/AT>83i+-N,FRuJ=ꙓ]Sb SnO8IS6i3>>qJ^Tl7*{UڢIZP\H1CQ)Ubv DIr~ @45oemo,Bz E E/[oO)ED|_c¼:8t!cj\p֞NPkQXv._rMM٫-uVyg}\NOM[~̍GBr&ū ~=wKZz#Lfj%lfZZRkXPvgKµlk}ZT2R}y;gAP:N*tp2a ZtJbl82-~'wYsH 7ZGke ^M{YU>wk[Y?ޝhN|(r:صԾ:oCLJ_9zƑ}u1hoʑhju'aMlpih˩L%LjwʝNCت_#䔠I$c_-5J@BVw~'²,ע<ˊ28ϔ}! %Ybo_f_͜pqKHza,lV"W>JnVjH.-_?AMi͚?p~Hv~ji}FZ^ᄠs`g{Orazj^^vL85gxL5 Kw80MًgBŬxFM khhp>_;@h@E?IY IDATW@}EiW!++1 SñA+ [/9stE"'!k+!T"+pe*M\WZ=d ٴ(ϤOߑ~ i6ё '(Vm )+)1C :elb8C! _ZjQ! A+] C۽&B,Ҵ:z w/)ClSo^rͲɞ)l\t<ѐ > ܲpe@#:BL@N6"> B=Yd;[;|B%Ջ~+ߩ۲]bBƞRoޖU߽YEC =6:^xʭ=[=W13~*UËO̰c.i HcBDHgD"[[ s |B%э TpEQL򇏙խ%4E,]ȃ،B[;+Ibqb҉PJ`MV֥KŃ+f:X ʒeMOZ/ $J | _= ,AwUQ%0X6ABX ߐմffES4Qo@i rhB6B!0TȿMQ ( U+@d-才DTTP @ K%<  [ L@B!PAe PfZ3N*J| _^%,9^([qȢ^qYRPɹC߫& !BaiNj9 Y\V%̔"1_C6]WV̈́oTEEf=562tbCAͿpbXo NpT$B*7WAzȑ! 5j$phY*'[z2I6&xri0;+WM9Z>cpuC3rݭB!0T(JUT/ϣtg7(~.Q .aRPP~B!TltҥD t|5<']\?wB!k~*NB!ŸݢSE~#\ !BHⴴ4꧕7ziiibgJJiD!*iiixދџ-,~u !B& [Qx Phin)Jh0 !Bd XT*U*O$ BPT*O)Q+ !BOҝhJP{cWgiyB(. p0A~pȐ!xfB;v! m0 BL@B:-B!BJXB.W2dB!0T@(%lʨ;8$_߿mG,^!]*pҡhJFt8!B:GN(Y H!8x 0T9̗%* !B Ra} w0ϗpx('`B! B w`SUs3$;Qf*KS6*7q=(*( 0eꢴ3͸Gh *4Zn4='M'{7DҳExXWñeKU0q:5  i3z)ghSW aM_ՈB"{>ʴ_~5k׽\3#@dw+#c@+sXL17 ٿ|"|7ObO*sf։;VܩHr}wthM^e"zh>Unm]tuRIB!|4cas+9%WXqc,@/Q"aM~XOէ~>KB8N,{_4lRBp=7}+ڰ^ÚW/n{K!C6i+C*$OG5- TÍ}?wTdp9s1Jݖ'C0[QB3 kuz͜yK:_BZGjszU=@_/BgGQv ue_vOta^}mWY',_Uv҇ S%ݱ=egg&&Ǖw*:CWI~ ̜UۺO\ |t@_t@uIn]=={"#;{S!;5TkQ#Ǐm~l]eVJB:dTŸLOޝC K60.q荑z!*YSbAjH]Æ ScW?wOy#3yW-4q`'OGc֬YӧOY@ XX|Ϋ(  8.[!֗p|ꏅ;oINh(@PK7*سi# bI $]y: ˨J *4ܜ̬@@Bn<*l̬L!DdDd]$ύ YYYA!a:o46.OUbSRR Q!XsCeYfUHQ|$E#3sO[SQA$XVVV"MPBFi']if $=N'K:Qd 9u PZ/&7uL $ό \٫P?i^&  : *4KF#R[{Xj ӧ ;1Ev#'K791{TPmʢgG/Qztᛟm m;{ύ6k~![7]tzOwiaAQKb9z ..?*w+I5s'\nZlꕿ?M#E}X^W?Йkp<{]}].ᦔm?lH/z^uWD.Y+YL8 xXT8d.04i$!Fj~NͱIQjOA[Id 2i]OK:k<yŲdRGD &ޕI?/b"L՜vʧ\brPa[B]tέb&m.ک0WnL(@P%F$i'EVęO~b;G7eBsq.jlr NIII I@d-*cԪ7h?t'W=%ф y۷7rZ W xtT^)Up#ErGVRfs@6}C=xާP5[ONLbժD}\$ύ 5KjfF`ثض~87vr(o|h, ~`K}bo|h`pB_tlxղS;%;oBCL/>H_8TRYLPWOF# -<1[_׷W+ݐ=&!ЛHp U𴨠k ZiضDZ?W& ceWОӻl] -F<̴~a:a0w?ʕz3?wkh[ z mfևv U7Kۛ o_6vw'&>0՛ \mCctBv{%Rj~wn=އ?Xz`oh!m!ß}`?AjpɨBHQS|vGz} !s;w*^֧JȂg]Rq=Ͻ]#zܕ/=Nޙlw+Ba?4ໟO?xǛyp|ޯ]uh>>"wwjKR'ǻ?}w/D`aOsͳ%FHR?a73ڸQq<$)Br[%v^{xBk{K45-6$L_ž~6+߻u@/!y[aם…еI?\'k%JƾƳ3zuμn„ WeP<{JHWחVeUwa3(B!ZLש<HNlT E._UkB7uP,Gּe OSjˊy9hlkCA]b?y W!!}(-.B1xk4ZZ^9sDpۇ_ݞym>\駽y^#:qsUnʝ=?[43NiAPQAvwDG%ZNB؎&=&XpeMwݽ_<ɗJ/.$z _l!݇Ltʋ8wɄ:?Su#>jH-uF֭֘LI͵H&s|A}ZW'ٴz㞌"dmٽ¼xP): a*HZfq|1>k^f !3fx|$,+J72B_p L B[Y΃{Nj-BUͱT}c'iYDXp9s1J1As5fs 6{ޖ_1v9aԕw ;~]Oy̍r'!Kp ~n~v?? [$o߾~/~i댭GvokSV3( !}'Ɯ5{ݹtOޥM{x֢s/ +b1}mWY',_ ^,zco7g?8t槰wwB҂ C])6"y׾}lOi[Gxy6ʿdnqE MF=T=Ojw|#}CRD#G'}DPݳ<:ذa .RZ̰|I' ϟ?yd7rnp:!-k?q|2\<>eGINgw]1Pl6jAk VYQ .$]p[7N?sdm4‘լY?)ve:eKi/Ć!4KiC+]dad4SFP3|L?-N%cxc0q6@TUd}p z0w BOϬpfQ(: zX: @cC$Yh4^Ysrs,V Ch QQQaM'Hrrs22}yY32 P YYYA!a:o46.OUИ: @cB$@,VK9!˲,3*(h$E#3sOS* Y$(IRs˳vU}Z D$5%x|iq(rU>-H$AHhKF$ 4gYLP_ 6@OZɥ ԉHQa)9UEʄW>Bȧw|w8L1=qx?Ư?Zϣy1[]z^lZko{23cM-mu Hy߻(["[~gpۯoS}_:Ŭt' 8sm ;t3hh[K"P'>+w5FK$P6 {] zQQR!UFB^ㆲCzFRfC^ّcOɲ*LJ&҂ @5|jˢq' Bɰ_g[;=fqio~!O[|`Ww;0.&<:*8϶?>f\8cdqS^x}@v9o޿ɾ@}O_kR&բeNlpCʉ&(@ub@Tp9eUp={;57zW -CcS&we!J7.n t~E sSm IDAT=- rLj4&N4z?stQ#eIB)ؓ#†9\ɭ Nt@ ;疭eee"K}&Ϗt#BEOiAyqA=6F{ΜJf% >IJ9GzXTqnLGGU݋O !DNUr>V>~~{yKfzĮ j7wH ۵7[/++|!ZcFeZ xfT w||gjn{ݣݯT Ə]SZK l0#vU*PFp9$i)Ώ$Uq՚K'<28uo#PRRx,ݥ=okQQd22,LAj4h9ܜ]RR¸7&%%%9ps]$iU`FVba p8j}' : *4b6ژ(bv{$ύ B^sJ4>,&:%%%x, C5p 6-TJPw1POZ Uc{U`1T$P'6*D@iAP@T\ꔔ D(@ *5AQpbQHNt@ *҂ b1ԉHQp3 @iAP@T\@TA (@ *XLu@T܌$PcZ W,&QQܜee0&)!ISq ULP : D*SN!!0/0/dps۶n Sx0  yT>ݩCÇU׏W$IUdW5}5o<""m붻캐ٞq[?pP$B)+/  HKKKLLl÷iӦ ,(*+/P.dp(@hQRq_x ^ј\OM\HU'Jc'έBOKw0 BREԸ3*" dSϏywԘ{j`@TBIr@; ,ɞyf}hP:FIHS[}oa݃چ5B:tMWQ{GrW+m׿+:s,"%!mv}lSzzcx}: D%)߯i\<~pCSCm|13gҠ¨(ʅ{֑˲3JuFmIFNY: mHW|)V4>~8OWν ub9ͬ5+x끉:kߵ xs:a}}Ī(zR(qQ'N{?d"kld#~EQGo^QG̪fwUr2JZuhf*8Q짏m_%?~ "c;_?/Yf[ZMB.M[d;CW-]v'썿.:)q!7sO'ٲiΓNqh3GP~ $ɹKn˵bذ1/h~U^cy^ 'E[f>Nw٧?+ۗRE`-;o*[.||oO~埏[kHM{R訰HCIzNY=7.SbBP;3-5g]ymzЧcH!ܕ%lߺeõޯmxu@{_u~z賲ʴ&^YHS+.JMZU:^ Z]+E(IT( y) 6OHan5G,陇f9ⓗv&!C?k揉'uvDzOLQg>S޼F!"^1L'oSel%Gf!z!;ؐ].djշOž$I{VTY%]fG.r?϶[˻[2%5"}iDn+8%ySٯÄk/b@بSU$gZ |,Tb+eE3kΣB+f3mҗx&w,;3řEQ짏/: ZI!WbCk~[VKNz)l_HFy әoU9>Y\RX"}̷'GdZp!$Vۨ0"zd{`-Zmu͑^#'D:SV\6s9#,[0pvDe Qrm`LH;[1 `y<bdEQ!(]ѵNO޹7Λ EE:j䧭u_W]>B)(KVPzNy^՜k~=u@2hRB)s[[#ƴ26),6SF혚7cBKKRI}z^B}r*("IR*st,u=䟶$vyKqCQtB6,E*LޙZ? O['*,E:!7&X%;)9RT8F*(9NjMh܂+Nݴ#dysne+ʷ}&Q_"<I\j Voy7@b!RAٞ.G3?궸ڞ OB!٧"lܟ04+tMex1*; :*W_Trv!}Nkm B4mT%#[/P&P4!=]W 6{t5?:U_[73e7B;|> n[gʣ￷m՜?[: )bg"Y؞h!3Ohub2ZFJv>ڥe讣ftrm?uJ+4A^R}=%.f)@R@-~RC3һ&lֻRnޠ,vET\[hshtщúlY$H]4.©\wo=%oԪUbI/҇w;[ݜSF!ݱ*&lDx؟*~m{u1JJQi9vlw}E V$Bm]&o2,ĒСk!>6oV-νdȖ-[>޽;/UQD!REm*8Z;{8l||.|:^gVp( WL;w9'e"(7W{Dc:snZGl'#BfT7ͺ\Ѭ97`T˿#:\>n+@BN[I<^0'k@-wk&[Je.ph :zk[y I%Xyx@%بR=nجb_7yM{&Fh/hՏ*\TI)`A_xOfn3NجfQzp)~m]4͙٥rF(֢Voʏ'D!9 ҄ ~g۟E L*@ܨp8uK#* { J|dxz;8 vc!l}jܺέvu3ۏ.o3p6m}mZŷWSm~}#@sB]LWy\U#XG d:>fmP۶>k]mh~+S,w g9j}eJ&s!I #h,+/nҤiwUv]yIHq]jobcceeF|<V\\cQ^RPMț~Gf|YA.9GWv~1߯>8~ ao޻Am\v?âBTdTfVf`@`hhhhhhAKtatTl6OOmCBqqttC%#ǭ9z-L7 UP`,'+**wӚ^u=^m|}-m|AǜndZCn9I-@TԑD s-T!(;5"a3d&p%zm˿G%BUp[y55%yKXU҂h$![-֪j#zjWsvZ mH Cq| u^ȹz#p#i#"q#kv.W_TaÆqj13,_|IsO}:O Po>`Ⴁe˖p ߚU{ 4>-[3D ,{g?;{Oxxt@ lri9(ܽ<9CF j҂h$SկMTvτOu nBŏo5ߥn~S }`SRj'{L~8#1b@TTDw@|})?7{i/ 3 ثUpC$Yߵ(}r[W?eCէA=⃴L  *H_-W~q_9&iD@iA4dRva { n} ' *J ; XZZ󃸸8F PW^III XU*Yּ;f[]κ.} J҂h$`=>8$::#~:wsbB?_>ϝƛ${ n뀤1G'Ēzt@< PQZ _ޫ_CWI{&0h|c[Qp; )yY?6#ףHJJbaXq Up[Rw?:brx*@EiA4|6G3pV nOiw=OT*w0%@ VTּzUB@J#fN@: $(-/@񶴙y dcBcU$sgnڳK.=1f "))A ;Ix~ǻN}W͘C_eaJǣ *J ʶ}0^bBqCyۃ{XU*^amXuдGSkF9D@Vd^͘t`Zޥ6)˲%NdfC}sk{i]oe˖w3iA4|wlBmtkkyfޤvk2lph F_nf{%;yvx nn F]t7.[Og'|ѧfQzp)~m]ʹ Ù6F 7jy P ]_~8"8 }t񷌋.uk[7fK]-.@#~gq\BHQPEZ _$ߦm|"MJ?s "jY0_ʔ% +$cD5B80;n)@R,G}W)a#B8 6ǤOs|e֬xusDX˯_S1T-))cѮ,= 5U)Xqg~H;}y87Gy˜OǪTDC 7q/\ގcpOݗU.f/̬h6s$oK|^|@TzI6Ys3G/7F"-t@B !lo eђS vBʾzws%شȬ;&ͪ,p~3|qQL *H ! F--h5bT @TQ *H @T ꔔ D(@ *5AQpbQHNt@ *҂ b1ԉHQp3 @iAP@T\@TA (@ *XLu@T܌$PcZ W,&jPD$"- +@7+ IDAT ԘHcWiii @Tr U!!P : $(- +v3: *nF$PcZ W,&jPD$"- +@7 ԘHD D : *H $P': D(@ *5AQpbQHNt@ *҂ b1ԉHQp3 @iAP@T\@TA (@ *XLu@T܌$PcZ W,&jPD$"- +@x2C5p UY֭LI͵H&s|A}Zȕ6fU^Aͻ^ѿkA)TU(-FPd#Ʈ#'L^r97rr1ʁ K` Uh gBAn;kDhߎI{ӊ:& ڐnWA%$mv#Ft b *ĥIc+QS[KfM* *\Mܺ Oٶ~wqp۸p&5k֏efa}Krssssm Ojx. Mޯteʒ1"~N!BQP8D$/9^%SyP#6*D@iA4K xusDXv~f6L7P? $ *h.@T?-FQ@T D : *H $P': D(@ *5AQpbQHNt@ *҂ b1ԉHQp3 @iAP@T\@TA (@ *XLu@T܌$PcZ W,&jPD$"- +@7 ԘHD D : *H $P': D(@PCAR4qqq *s\QU ԉHQPEZ W,&: *nFQ1- + @5(@u@TTHD Nt@ Q@TjL $P @UAQpbf ӂ b1T$P': D@iAP@T\D$HDƴ (@ *XL *ՠ ԉHQPEZ W,&: *nFQ1- + @5(@u@TTg )9;l֬YZ)\6f)'P}Tn<sqXP3ey4ļFU$ɽ=DŽGJJ'qg4  QSZP$睲$!'? *$ZfSt^zI!7ꅵ*c6V2"+5Y6o 0//G / * I^&W8AW%@sOHP)@ҘBeV!Œ *H-2RefcA{T&&P <l^QGZjϩ6ot硃ZV%{u+SRs-gPTZz/sMd9$meX!UWt3ln*½-\yM#c75̋R2O[EH⤉݂XI uMNsi9[QDTj+I]MRK{ޖ_ 5rB#=ea:y1W\~pݪel(t+H[uzO΋Ҵ5KR򹬡ŖE{&^.~Pǿ&%EjxsSze!d E~vIYj#P*@ԃ!.Ѽkߎy{ӊhg>x<}c"&i|>-Jrʘ'ϋBd,YWaDh=U}*JWn<:2*&:ěq_,'۾EQo mٱwIvkpNMehlj PQZQT#[R}PQ#Z?sT}3ak ۨTbݾtEFC =C3({^+oeQoQS$; -z_UzluK4Sx3xk5w^>S oyKLHUrWgE Fܺ<0-hayqgfY~/s3eB'zEUإJu-&Vk"߰<~mGvnr0%`6~5HI/VWt^zt6 FB8 nc㪎yч]6.gζe[N;a@/ISle6Y5Bh FZnmw5GAA&)hpHowy!xn:>rY窽ge_/y}[l8ʄpUɼLAU4o7=/Z?st E!SO}vD۔uOA8" P+: !t[oݰ;dΑmwUjysf}[U(W67WϦܼR}^yLMe[xΉkw%}h݉NXJslqԢm|4i⤿'O̐Ae  8.[!D{HjCT҂$p @5(@ *5AQpbQHDƴ (@ *XL *ՠ ԘHD D )e'mj *ѥAK]iv^\sWoP pC5&$yr݉Ӄ>շ50܇UH.mLa˺ɭz<"*,|pp I Mݻ "5'ѧ7v6IOl+-ͽz8~f[Mo)@(B fATJ"M@AzPtD 4!@@Bҷ̼$ BKelv=;gf˺%I46'I;:7?p*H1{o,E*oy1"$գB~!AǔGu]α3|L N-c!D-Ҙ?}^{3s1ە~ǓܚN=4UޣϽ񥄱tI!O}뗿KpDL&xr^"ޤiTu:0fWX|8G <~A\u,fdsFҕ@)Vۗr\u=#!0HxyrWDL&x샂IgNէ׎Fo4P=D͹iيN2t痌Swo;wMI?j&ёQOk‡o\QZFFEG>Y~/g~=ݗjըxЕ:ÆOٖhB H %c5W"ͭtc_pE' _v@]?k7gBr,Պ?_+7eVw)˥Z0.N;BWm:z!Prb}Z:QԼvL8j\ƹUBakʀg!úY2tO,iܻp̸1e*-yRFwB/2;|)qo8gZ=ml%n{Оa2NFۭ/>7vZy7G#Pyw]ϗ}zL-=Q;^^^|c=f7S !LIn=~ufu7koWu`[XFDd@i8vu?mZfwB 2lZ "柳x}]/o{/>JciW;Mrd~H*{n׆"lp{~+kAڻkP/Q{څYw/t,\.md_L~_}oeN~}ŬW ~-zF-Ȼ5$;1h@] b딮s NoMtBv/_-{ ֋nB?$٢$`};V,-hv;'Z>M7Μ vQ !CDŠMJ^SםBüBR'y9ǙMxAtdNl|㍸̪b@1םM[~d^|^bggxNT/p:TkgG~:g; h@R\2Z8m`LӸ8&";5,|:LW{7~wJ~2ac!UnO;mXdz޷~${{ϽۨڂOr/<%ϗњ/7Wǂs*'w'y5fo_v-WU?cB`/ ^s#f5svo>V !Mjg Uhu ։+ǒdC@/~Gs1f ]B>o~_|vcFCi瓦it$Z/,'*.Ѐ@1g:ZT1j*M J%{_oWP0 2oSZa]TkزU0s@_eg8[-$Z*WYGk珧܌\UŎ=7򔄨]w潨H'!=CeD wY!W]Zd /qS4x>%'=[VkTLwg5\?"V(u/>%FY)aϩS^S|| /0߼}\vg0dx@Sf z; !4M_pѴ5t8RGUpĵwnժW/7ZT5KrN9jR\xG/ݾ!ga\W-܎V_)r_Sxf}/14uyGyf;wkb:tkoqHP6>){Z;[5>~#/׶xYۍk>9znmwéÖfG=Ӹ-wڳe*NZVj/qC}?ck^qOka:U#ylW *(񇓝+{ H#}έ]I)Ҕ FRٵ3O]If!dY񎨩_iEy=Q !k?*XUWn%]˒tdT*6^R{''}4sl_n>j]vR8ӥ1ufvב6~^7UUۏ{VeȊB_/\^ftڧڋcWUKEV-4csRI]Quh;eFʄsn7?gdk[xF.8u^1!/JjL(h;>>~`lXjVxGصK.w֍BFw?|˴ޏIŠ.ݶD/0Xrխ~Vn5a{Y 7<[7&&fb~\)P$>?oLxrڱWCuX!`QlzUsIT]+(^^q2r·frrw i_x]PU(6{j]LZj]h</Y& 4 iAЀ@T,1@T L $`` HDfZ4 KL&+h@7JI (v( U]`2XAQw@ *L *VЀ@TlAQdQ$`3-oY[ IDAT u!a T ?ϴxr<4 ҂{g` KX%,a QO%,a KX.yx1cR[jš-FZ$?%,a KXR31ΛY<]RY%,a KJfO)P"Ĭ|!*҂+ %& 4 iAЀ@T,1@T L $`` HDfZ4 KL&+h@ *6ӂ Xb2XAPOZIQIWrrsJBN?_?I(yHpMT@IU$W)$_ONI^9,ݣ$-=#\y!)CqRq~$_ONQ-˻$oظb  H'jR9Bx{-w?#š88`?&ETpws7 %a}+U>z(Q"h@)9G{sPW99^^%d}eY*ᚨPT H%>ךP1p)pMT@ RT Hɦ(J ŻDQZI%5š85'`REq{`lΜq|Ƕ(.RrәxHk(/7ݾd3NׄWMZIO9iENJzAGh J3ޕ֭oP Mqլyޙ~?|L6 !g^nӯP 7ӌz?zU>55m]#ucT+X{yf Tr+}(r3,a qyEvlZ=j\w+T$Z u⚝pbZv\PzV*5ү>wT!9àKӍ^Y):/ 4+{E6J~<@J]ߝ\T/YU[T rT''wU]XJzI ~^i*#9&b6 RW3Pِ+;l6WW%بa;5)/WzLZ\֕!@IUd K)f@j]VbrMw& m. Q~%ҷpOxqۿL}?m`! '~MsEoUQo1VP_r »ܵׯ9{1~!$ Nd,g+·Y\N78yjΧd}M>!G%fnd*w95W4:I*CF.>JޕC,2Cf ٦'wmBͲcsM7/36!-_:ɨE9&l@R4V>WqGM6K$K.ʣ-؏w@*AZrɲ!KH,˒:J~ҁ+Ϥ<;qh0dJ+J I2EuCWWª&uTbF^VSծ~nJljFʍ|n_PROF>7p`T~ z{Fg4]'=bm(WD:nBRi-3ɥj3^"+_t.NR~FQTK6?Y}1 #n8EcǎC J" K:,s:G7'C}YQ $l6ɒZ' c?XGֻcR(8ߥ?W4[up W=p6ft..ZV9;K9٦NN*S^RBBkFeNȜ%ʗ/HB|=|uvRgnSdk 8hvLY9R僽U((,J;{.dqdoɾ=mbu 7er./1S0 4\íErΙ/9lT0ǒԁN' 7ς?n.:R*,/SRTUH_;/(.L0UVV=vfo G/}j;I*Gs">;S+葿|WW;ygh|YWNL}dܴ/$ֺ;zF)7HdP|+=k8 S]{eںdZ^6uTE+B )nE[)s;{$B5 = +<{AտR.*9uGW?~$ BPAD'IK)u|^`}ke諅W3 7/8+ 'GyNw=\L ʬB1 `*P'8`WW]dY*Xɵ @5 kV<fߠ!f3*lqR* mHy Bqo1vBQ5 9333]]KWfff:)yx䔓s($I99NNN^؅֣_?ҥ̒sp钿?š88`?!/?/-=-0 ά(-܀) JB==p7}rIh%/ygTg'+t>Uw:KMwNX:keX*,em^߿vmҶ>l=J=cG%ij=wS8=ߗw죗|.·/);cQK0ޱrnk7 >-ztoܨCL9<~1,n?ghw|GWKpT!#h\^N[\v+ݦ/Rw=Q%3-"n@2_?ǜaN%_E:;cNZ~kׯe77Y.*mJcW񰶍ty*usԔZl΂mL>˖?|i2r:lWuv0I }SE+t!FQ?;ͫgl۸hXo/]r"Tp {!rL~w: !9_\UeUxJc%zIpd9`/"D`ۗ;ҹ#",؅рdm}/֩yj56u_RVo'o8SM~&ѝkY;*:2uNe+Bt~/JVoN9Vב9ЪEF5o7 Ejׯ_s#$I7of۰&33ENZkFD#_=~Dӝb.l #w ̯>h^rIZl٢ee!cmtdT]H3:QM[X"re#ڷj[b(;mq R=UGCvrzۦ۾`P-[8vDVQu}cs9E8tXȦ:kR7rξO7}jv(2*[g/swJN;ɨ/^S go<;S>?^[ѿsQQёQM5廄|=%;kWёQ/8o.x0'nz~W*cgGGF|qgB=Wmݬ/e}:~|zDF5m;|_]I]7w|eSϱDG6?֐mui^NӖ:IBS.ܹaTtdvou( F7lf. !xHQёZ8`)Tբi(l͗MVF1Ȍ7kॷgzdKgg~i״t*UGԌY-M,_fDD͈41qESzzQQMz%1a'EEGw1I?/׾ydTtKݝb~Uxպ-_n ϱc_Ov|f #5`Femc~K 9ݶQ5G6}KT؈VY'7LdTtcL0> z`Hrojn_#ZaLnƴ.ފָThå+WZGohB9ƽώ7|09,Ƅ6~6]3'߾9997Iݫ盥JJOO_aMV5uZBE6&[Nz{N|ak|fח?SBV~ܝXa֬Zh`3dϽ?}Ƙz:3>r~Ifa_{{0rc ݱKs$IZ$!Twۇ0j!׬Z;hw컩+[^QM{/:ܮ_gK +:y"ZzهI)oo߸nMV*Oos>@P\ꎜ;H;o5ƣ |&v8ig895նѿfkf~?8r*{a/?3@p,ya 5Sߥ9n{ s {I _RWyeT|[C'5xuntOoŐakrh:c>TB9n61UϯpEl~־c6hGve~ZIyZOI[cc_Σg-]rM3֍{^駞sBasgjX81"W.&Sꗃ_4)¢m=i9}!}~՛|u8+E6EGéOW]oǃfl8`0]9dF\2wK"dk?X~ǯ>vrթ~.LuYG_>7{wB(-l@2\?sMobG+FuoaιqձfL2RB}`x=g!ZGg* `ڛ!Zi _o2%5ӫ[...];w_~MJl~kpA3;o_]G o6._L[Bh/k"_}C9/Zh{,BSmԜxL/nU)S߬u_o{cƫM>סe^m]})BJd@ƹ]D&Ö/zsVU(ޘ9ŕ^bKBTr9j#n) w|ҽO KՆ֯_5C -PwmQOU!4svZ+z7061D׫װYaZѻ{xxh./+'cV5m/?~|Jv kulH&O~^{WU"7~7[TZ"]^srBhCߙ5gF&A廝*W8ֺ:TZgOwINjcHQ!2$ns[7 W2!̍&c]>񭣧2]ԫ,Bj6r (~hulskܧ ) S)Aa~U¥nިԃ'E^ўsuP_m5ck+34wpJ:!Jwvcgi.=ʜyOn=͏V S[b9KUmJ݋.)i׆{G!5åS_i;>joTeӗY[xu/B.smê*`ϊj{bego}_Y[06|xRyZ,B1Yfamܳӭӄ]4ͅϬSKBZҨ OheٜqSRKFkYD8;B5ws~8!Ԏn*!0&*Sl\SpMy]ܕu=G.iJ[b#TIDATuU%Iѭ4 +Lcu4:A-hVӾjz_-YO?ͩ>x֪KS9;Uu>TvS=]Gvs}+lTsN9y%INK7*]=?U)K`U98U2̆kg&ֻ闪3c\EZIqW="b6ɷ>6m2O;Ӣ1+7tXvȄ+o뾽+oX۔&6XWY:5;.Y0]9ɣ>fʆϛj={jRK/==VEeqw=yH-ekm]Uo*j7},]^ʻr4I>막"Y_ĭ,kӶ|iY Wn?7k I魱U1+ {юWby"p0GC&Rd~PJ}CڠMh6/Lmd~+{.~ֽ 3̒Zz *҂($OV{|}.O_JJ;߳˛o>qT rFܡk0/.| O/a8$W_᫓7LC;$yz~/7z ._|PՃ1]@KAɍ?|U]b@ W^%_:aeo]}eֲۀ Wn|;y9!lt`*Mf!0%=_c.j!ۗH!uRٙਫĤrOMC(r)e4 eYq,]mᓖ[3#VZY3/S2nE W_z ԫjF/VN۱`Y-k|˨]y~U^G<^U~'c Ys6OhP ХLuR rjq6)oV9)!yFt ЧDC?O" 9B=x HFڢ>2WrDY5_Ymd27ْf[̎n][>K:}Px1xQY߶{ck 뎁}2_[/N0VTe>E[΅۾=x№R ܱ|ZtxcU0Y='S3VneU]Œ~]1*pPNl =GS/M\+fܟ.keJ:Ϟ.\;aUݱu|rԖez/j/BiJ[MolEh=3xɠV~j{~ ߛn>O\7bس#ٽsvqbBhC^ymsβ=y W+݃gk>_[֤l<8罷n1cL6}4kkMΚ5DQ 9a;ff\)|YX:dyBC}gΖ:|gv^-}+f{i;-]9S#[j[[>S>1Z/WIX;~QaڒaR9_ >Cr)]a^nBب)nԠ#cܧFn]*k]A9ɥ;>kګRc҆t]qńh )ݠK*6G9ÃDj}4_yM7 B8Eu2oU'+ݻPuȋ#fOɣj/]}^>>~`lXjV8֭[=Yξ/\^BʱF&vj݄H=bLv}u+_-VLIiXIlSKOο;31_jf`h5x(/v5#lSj23̉Z_k%^c O!*.<']1NGo#%/e VڳrwtV+pԭŐ'"_u H;CaXȶS!w V(Zit[wjW۪?Q+<[.xyyQ886V ԊZu xcp#hۈZQ+jE8o`GiA'eQ+P+jE@T@STW3WRb `h@ *6ӂ; ;dQ$`3- %& 4 iAЀ@T,1@T L $`` HDfZ4 KL&+h@ *6ӂ Xb2XAQ HD D  ʹ h@ *L *VЀ@TlAQdQ$`3- %& 4 iAЀ@T,1@T L $`` HDfZ4 KL&+h@ *6ӂ Xb2XAQ HD D  ʹ h@ *L *VЀ@TlAQdQ$`3- %& 4 iAЀ@T,1@T L $`` HDfZ4 KL&+h@ *6ӂ Xb2XAQ HD D  ʹ h@ *L *VЀ@TlAQdQ$`3- %& 4 iAЀ@T,1@T L $`` HDfZ4 KL&+h@ *6ӂ Xb2XAQ HD D  ʹ h@ *L *VЀ@TlAQdQ$`3- %& 4 iAЀ@T,1@T L $`` HDfZ4 KL&+h@ *6ӂ Xb2XAQ HD D  ʹ h@ *L *VЀ@TlAQdQ$`3- %& 4 iAЀ@T,1@T L $`` HDfZ4 KL& %=(h@$ZbR0;J $`$`w@ *6ӂ Xb2XAQ HD D  ʹ h@ *L *VЀ@TlAQdQ$`3- _LLL  wM' H @T@T@T?}]{IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/pymeasure-plotter.png0000644000175100001770000006655514623331163022240 0ustar00runnerdockerPNG  IHDRAtM pHYs  tIME)[| IDATxg|E٫ɥ"UAvEQA,(6, "`A1 E@4N%!kϋB'W~/岷7;];+"  2d8p B4(aZOr(\\v)GUo]{|gTP6@-#3ܨ&6u:p3BCCcak&:&t:]5jOWnP՗!T f@fj4mU a*囥"(dcCFr%fw" *OPxDA-U'"GQlPKUT=} 畞^V-I;v߿2>JMI*JBȦsX9xs=iVnl޼yÇ'%%;v('%%>|8;;2V~8^NN&N,ּ\⭑MYr ,P#| 2sMV!T:OP*GR-~TN9x"yVoPBRpE!g߻G}HݨP^i`-^T֔q*LUN˵z.zl8|z*$w@hD%O qupWJX.J%5)[ԍ I/ HIIIJJRT%%%EUH?SY.j&BŒs.dRv]1夜>( !ԩC?Q̡35|JWJ(Bb(/ZsVӚx>%+dr3s z"7@^aiuW;%I  uN_yM=+ |EOQG !BCCeYNNN.QG`cZU2ɑ O{Ev0!O/ X33ͲєF XM6 cITBa)4Bg5[$!q[1+ KRs ulԋBQgQ[rsBIoJ| ]*<u{5kPooo2'V:t:[6JK9a9{h_^F+<{YA2囄*OwNѫQfߟ-)V媟F%t4o--_It2˚u!b2+BB4ORuqoZ׍prKKK]vQ[Q[@@@e8TⴾUx 9Vwa~J6 fS rW[ ssLBPvFR{FT)fLx]-AU=uPLF*::l2wHZa>uߔ h׮];JTNJBrM4aО={`xpd8@  2d8p  @d8p Z4$rRJRBBBCBT*[ns |=}) \l:pNFsT >~AXfM&!! M!aVYeEQ\KRHhع]*oD=??v)QgqP;_ w6$gqp C 9d'e3 dțykip n::o{Yÿ|>2IC ͺh?:xsr6ۥ[W?`u/،%_=֥t1]b;uӘ./NwAv U=gӫs,ݼX$}s^_YJY7C?Y|(FU 2꡾~yX9ygǣ~wG'dTB?k>jK9)Ƿ^{mOVj!o|w賴>>&pەR-{d![LXGS L̸lJƖvy ˦|n#mYSŸ"~kڦ/O;ӬTH7c4b)N]VT]7Q7E~~mKN{h[Oα}PdYOo_HOMtzMSe~G>粎B{XTttW,ot$g^E;tS¬WȓHOtW|*ߥk&P$2tf}yAmB]SdC^y*xfWLG}$F>:8d"57\y6'WV_w|ZQ Q=y譏{? Z=^S/V3#cZ=oOy9|Sm57LmfdZ|Wٿq^[%9a>\-NaMXwL|@uⵧ~3quJ7XXnyF:mPMqPO! ~coMZzkږƩ蚷ohWZy_(z?oašg>O.8]y/k7'6#˩6׉3wjxzöLyZ pR"R,&})'-MպM w{y(lJMӺ#zK|jc)__RO4eIr60v~ngڽBr^n)W͑^V֖!4Uo#U5JeKfELcwQgmB{מ(l(4v_gBᮑTzOooB[S=)po[y*^ƊM4BfC 41i܎Eŗ&Px,J'\˸kk~v0|dkI0utf:t䘱JHprz/PB/ u*7oR{i1 vx,+zl/'|7uR7*n 9i)__Um[},t)'VtʃBʳDZdc)q:'/ y'e,<9RH*5A2JBIᦖjI!Tz>b2ɍnTUp|V$Tz$$$jISRiԒ$I*7O$[dQfU uusdt˔xj5 O&':݈ߕg8ZG,[Nuϛev ؛ТʣU[K:Ǖyg!%EV]GO|~W&0J~j_W'DT]ozȢN{*ZΘ?Iv XɇTޙp5/<ådܸ)>=a}T>b-Vݺek*|%#o[{e9뭕(NAŊcUh=JV4|P׬g}I%I$$I%&1J2J#,KVLYTMUaKlGlm5 ^wH6EF~cj$o6,k/_`w\Y$)(|jA{mSOoQU/S:UHJib臫߭/~oܳ>-B܅PdAZ*g%yz{tNK;MU!PԿ8)"^o0qߕzR7Xu(ýKՒ{)r#9ұpӹ6/IPrE{|o{-SX3oQ5 $BWW%ִfkZV4YګVqժz5ϛb܅.>M_vggɗ:zFDdm߂MkjbL9j9?פ$,sT`?\ip=b>{숛*/ >ѳ ѧ [LBTؾnAɷ@U?oC-rmU|sRb+76l>[׫̞51fĕɴWZQD;¥Vo>K_~ c3=zy[Ēc{~ݧkwVر~abHeD%Q<**Io\S_[OvZz˔$k5XH6~Z^MzQGz*S4ӑkz$I\WXʷiO|zh'WeORA IZ1=~C;^vGJ+-\L}t킅-~8f'!3i1ÅGhփ}O1#ɼO^5NmB}#Ow7KZpt3?Z i3&&#\]?̜ͻKeUeǾݥ++ڍ?NxuQ5$P+e!dzoNlMݽ5f%/E |&ݳOz[ua:jڛe=Ƽ;0MNe !$0IKN$yi.m[M/IBڲU9 is^iƞlʔ"M-ʺjjg_}]{^*I㿞2Ϡ&}?3JYY?~I&lM[Xc^:Whk ^oriZ7L%k٬t4TB3ߵGjuQ{/t".)9ם WU"P\92d:M8Ӷ n]x_ 8 p^X6 -X+al( R2ҵ{b822 7<i 5 ]bBCsss) \'%&%vRB g ) \[XHpj2Pl6[,. wY,oEj 2pd8@  2d8p h(K(x>1 "IpJJIJKOkܰq8CG !BCHEDT xL&Jp>>?pTD F#EF[l;oͲ,;Ӱ#@ED+alEQ( 6R\ GEP*HeY)C;f*"2\Pu[PQ'뇫pg_O;=[=gO{83nϴ]|w^q5uk}^lC3:ju5ʭ`bUbr;ۣ߈uY;߿;!g}1*(GªTYp$qa={}9dlD;yP'?iߊHQ7PRuwy[R7URB)#y#wG){^0Yl[A*"k谻׏ccJKh:׶Lu2:w JYzᕬHۿro"~.N_C٫(77Ȳm&7(u/5o|_EKtb˥HȊt͇T:ų}=yh]T(eE*>aLZȊRR) %É+"뇓|[}I鿝3_0eb\^Nْ䉧q_'CBDl[2!ܒQ1)=mWS?lB;avk-t|#1ϭJ@.*jV%&ܒit.1͈7ʹG~Bb xfcb<2}뱏อ{gvo3Gx~~Kzw}3Vl(Ƅf6Wl}=͢(r O7c3{K喾_gϙY毡wŲ3aMZ;42H7[)~Lf(BR{GԬSn^zhX6Nٿ]bbyeZZh_-&秭O6+7}ƀ]{9e$r_ֿKLl/MZq"_.^~|=fP\N3mU=G .E4>h<6_Yn\?#?n@llۛt}b}nºuBXR7#(ׯr8\ 4tM*c]&urj?yG󀽺7WGFXo7mg[xOƁs[5;)*J%$UŊQV4w7XYck{=mD#w-sooF4q֚=Xr=]? R9ݸ狏֏0^I_6zCw& SkQryo}.7?k흼z da-1DZ_̝P6+ IJNZ1c XľczC5G>;mcsZJ7]Y?u[G5ԧLUE$+e*^z!˲ kBr}:}⼦_as#ߎ0{_i6A] 4G"5ߚFWuc|1"|C?䭮g}?uScCAH?>f]ʊRw KG==IKڈeueW_kъX~[8`XO3z识:Un5 =wp3ڪFW7qBtuYW.tOԱK97Մu៾[4xZ *"sډ9VՃT#lӊBϖ%o~8a\=wE|[yX٘ZUsGNEћ$abC)9_Ḩ:_i$mmg:D鄈|=ϼ˞g7❗:Ig~_^_]'rՋ?=l/ҭ$y4P.9z`wxm!RSE׋|ݦϏKÎq,K}EMƾ/\-Dp\n0Bl"<.8צZIB۹euW[?9ӟIl8rV S!ܣ5UM[iݼU.mk{$Ie(v\! '7?-zd2ZE植Njپ˷z-VY)ۤ.3g~пlղenwHQ\~lBcY Ozwg g4YIdY{K¬SrNQ%dL: -҅;b RRK,KCTBeYHzV6Y.NM)7Ƚ%(K3\iu$^+ "K;L|[_%u$IorU74XnԳq% e6Ei+SBBB-z}׮WZٵnnn,xWr:䫽_T"[9ݚ [vqҒ}MUDHfC}<ط͉UT,+Sޟ%^_JXҨ̒ZH"[ͲPIxE\XEM K(d!+R|аoWDz܄jMVD,]eY(%( !VYux*pȱ2~S?|qL(0ٰ1j_mB(y=w՞vw{z?eݟĆ-`NtO _OⓍjjoeXEX(B\+` GP51!(B?'4H/jUl /!qT\O’uV9Wfye.渂+Tewu7[.7; ,ۨXE$k`s>ml&lS)BHϐ5kZoO_V3=lD~X . Y5T MF|T\(B۟`l]G'Rpf:n;Bcфie"EUkѪXLVkKW_\AQ0%?+ϸ^ 8SEdgΚoN7}VjUdžxvPҸe>aCjMkOW|cΙb_v3ഇJn7u]wW_L>$s*2ܪט}_?TBWdE.7{i|a&^uh,o]?IB_3H_65֝EV~̝vR|-!MR}^/^J^?/.zNjl[丂ӻzsǮ*wBYBQٳG{v;k*"qJ1'G~*?T7oïl֔ RS)+.5Y_k3sfY >u9Ʋ,q_|SkxH%ӷ||a W(fݓL$<y oNZ,˚a~Zg]xpOYk_ŝxE_T)P",)Ty9K>wGhU\*"{pj_sP !T>LäK %tiRNonuSV*}ITj}#\ܗ4W/iFiJ "_ݘ'fB{_]/ƭѸޯeͯ{kEDT*T]>vp?ٯ^ؘjEQt5npgl6@Wg/[fx<.I/Dl~8t DMXE o^-mQڿ~Wt U IE$tK)Bh»|ĘYGԔ*Y]:&.A³#N۵!ztlrd,E+ofN5g|ɫfǧ>Cv틸Z?\ա]B( 2tȁ?~>QիWB(Y%Ũ iИJq*pF,L[ۤI'Z]/d2ю;Zlyth޴9󍿟]j%ܮoCdK"pva:=AE*"2ܥ :='p5oƏAI ݩNEDTxXs>nz7Jp̬̈*"S.'rDHiAׇ9͈*@ED+7R( !ȲlXf|#*" WfٙN8Zxd8@ vuRW\Iqw:UB 0 [zz-R 2pd8@  pf̘A!pqqq@d82׏Bp *R2p ΀yιX* 9f}tO옗 ᜇ6cÞēBd8\"(3f\ʰa(yε=`,\R2-&lU(`Kz',By~m<̗ IBЮ.ivn&qT ܒ - T )@mp@r9p@eհ@d8]0/  a^*@d886~9Sd8pMKpn"8qӘ @p#pQp@9NpΤ R2ۄJprǼT +p8d8.0)\ڧ9J\ ]q(?d8C+ (PNKphptE  Ro> 10pØ 3"኎(`JqM6/"(ZjQpWt.8pÁ*Ic\9(Rv 1RHr2@+Nrœ|^*6v7KK'b[Z\,ɕLi^wϰ'Õpsge K! 1PwNp`19[OF.p gUd82@ @f8j2yt݁ 2P Mm7yS29pg]fvod8xpmN8/ pp;@"8TF8@'[8X@Ff48}2Ntpd8pR$zK1\Lp%@t8m74pΈLƒѸWdܸfBݮsޗX9Ķw6"ny tC>/UQ_\Βu'ܛݭU{\']#CBj4k3B9L*ATN[aocrJ.W5d8`LB#$fYʾCWz]sB)H9_Y5傀b@cꦹp9߬*ZҺiB".\k)%noMnb[]^:;v(e˖@Kla'v#\5 !Pew|?)]j?o;6",`Jk RdѪֻi44hY+ q^[O=sy42i}C6\`VJ;d27\Ke8Tw{$'صioZ>*aM݌W4~#2vo/! ?j Bt!R6EՁͻw[u"ݹ{c_V(B(BrTwYrv5 t.˨wK1 2Gwdvа%ow=q4@Q"@\íT$rh(r){pQ +6y_I^ƶC'[\JX~W9ƯgNWʖ X9J\yr.~6\ër}/7[HYY?~I&+W0܌3 F=Jvo״Rv]ɲ,(*WP Kةn*\ Tl]w ՞={`3 GneUFzt-d8 p;n;&R 89:BR˕3\J'*mRi $xК" pL5-p 7 qvK%d8y\pyd8lyNef%2v4/J 9I 0-T (_lV&5$.t`TRvS  Ed8n帉 \jKr((8j$u (UvyPУA@ d8T"Nh$,p fmn!akmRR+p\S X~\;VCZ( p*RhFJZ,\pSE1/ @lv pXpyNԑwoUqTsp5Kp4UKQ=2Cs€\d8R׹H@l`[a^*;@4]3R zmC?*4żT be* 6cDC Á"d8 ytɃ] ˈ50&zg侼v͜q4a='6>tyl2@=EWP2GA {p̳ƈfUJ_ LL@J.$ApW^S莏glϒ!PtgH:l2 p::/U[ر)#_ӰgkYT-- 6"R2\e}ھr<]Xz ӳ{p 'mY2n\pJdn׹]mGwlyLzհwB4"@EO}]MjWѠ]vḡK?֝poӻwkVlqr?~YQ܌*W:frNޱ~&%gv <7kze\.6lxqf5X28[%7:wXvebZ-;rpP.k1=ݰ"CܺW);Fιˣ"3g&iUB $fYCgnv:tQ[i؏6"$IHheÅUraگ^=܊N6^S$F6Z?r^JjD=Z%l<*(N*..B|pւ\%ީFܚXa=4yM ò| [ -OT5ƒC56zP҅(񪉢35]/As31`#74u~~en=֧"QUk MjV֫sS'gZ!.:%lF+'0Ϋ'_qTeR!rV%IيW=_^~.1OV ŔexS܎IЄe8!zM!#8cKcMwPC5QQ{vn"rj|Tš凟4䁺&<o[jꄝ[ۇhTa(nsoP҃ukp t8p+"g}uGZU=_ûB T6!o֥{Ht}UBXQT%ivٲ򗝒!aמɈ֏Y*E?ϋ[GѝEw84m;u[#{lw=YRv^%h7p\&y7/~ w<bcN|`C]ζ*kqokQz;0hݹl25ؼAbK3W6JNSޫ=~b^v$U\>B=M#P5 wU^]?W'>aU=l0nt *Ql6Nr y zgց+-wvci[-Y]c8vx8[=r$׬{Wm9+zv 6GW@)Cw-I}2zCfl2ё̶ O>:ƧO4b$kFp;AmS0ugk_\eJZ703GDM6UᨩdÙxዓ |SġKnM@  2ݭQOBf}\+u%ikaQ-+gě ՝R,d3gSplݸ"p[kp`l א`w% Q2c ixLG=p6$koab^*@rZw~T5ZUI/w%Ǘ gϴb,ņ8ӝ&N<ܱcNJ~U{#@#R ]̣Ջ>|~K'SH IDATOnP'9gdTmAt8w:Rp壘R6|̲޳8^N[Y3R NPcpuƒk ErO]י<,l"..BI*}pxF4V}e~8[,}:-+'D-`p9'۶.o˾\uŖ,yvrױ_TB\o1 pW`/;PQ:2V8A4mԴͽ-:Y%]Qn%Ւ.S{c&n6l͙lztBRGgcϷZ⃦B!iFjk`25@ez;fp̳jVU5 /\Ұ'6n 9e?Lخ;&aoc k?mݾW-y3Щ`yׁ%*ʿ]z-\jϹ}cT@%a W9RI5F_F8w. j02\2]£+J_Ӻ󝫳 gIٹzŖ7+V]\祂 W9tw㌹GN"RώY&jj[pp6έg`*;t<:md2:Mn-3'~e͞sOx6n.rʼ'*]3ֿŧ[4z1%;prnp.NN^hkڷ)}1fc qHd8[6X60LF;sj-ǼTkBU pW™B칡*sJhleVXy*bASeb^*󷓝ocܲ.OOʾz̿zjt56(L3Wǯ˿dE@yp;*2-2Z/X|UB(VcN c'dp3āJMxpE=ҲwgF}OZat^6TAuNC ֘}ohԯNe UCOoG\?֝3w5kVynsYS n] sZlk>e8hsB&m^Y|*F9-z6V8ݨ!Μ{պt2<*@YBT%$!`LB#I̲2\VqзÃs7g/^ǼTO_x/}.\2MyAWl9߬ PҺidcE)7.ۮj{w i^ݲ-3@fNX=u6jnyfvQ+No&n+.l=Yfϟ^`ֺ_޷A ر-[r\%B'rgGW/[B_߂_lt9;g[猸ïéJ:dѪֻi.Zj=)`I۱&mp+/LGe@p+X2?W³aS漵a;w7t6@ת57)[]˫ݼJVRi=|T'cTy-i E0[ͲJS$ʳ_'GEyܼLr҉]׫֔-͘|7UZv]EMJd_A_[:yyk.]dC;wo*EJԾLp.\ GdJ`I_Iw27]|Q!І}XE/.am/+ໆ<˾l*p)׫nM:1㙇gKsc}jqT΅y|br <&pcA_p' npp64n$H1ޒb8 ?'ᖝ2W;B\\Byd8&Vv <d8@gkٴyp  PKpgT 2na7RJQ5`_geo(>>57RIB E"r"ٻyviw*@@z DBK${{G A y6?y;3;ٝMT %fهL}}.c |  >md8p@7ߙhvu.08c"\9֥d8}I hQ 3EY0*3w@u8p) !Rd88ϵc]*d8!c?3=@\K:MidS H[;FK%c]!md84@ZH=p.2p&Z1{^@憽8W[MArcJ$g@kĺT4ϐice1ơy w6x 4'uhqWKD:2`Bn&a|3K~d8VL관f/g]*@t02q&a7X @࠘]2Gł1p8:u*S ]+v9źT >m9 NDk7p7 2c; pk=5Zd8\"հ0\ Lc;gp  zk֒J.Adر7-$#{ #؉Q.KfZS=O2:Vi27yyր.j}{M_ssoҳ+lF-m'i& vYJ~!кyJʬzl Kq%<2ݓ-EkeI'joJՙ怘H7V|Ԋ4H%YVzt,~z\$߿n I4.Ew aMlZ7re:w‘]Vz@WsQa,ʼr=WVc& uԧ4˝<>2tQt`SI;WD{j WU+œ nѹ {gi-;9vHզ=k5I΁G&EEX+ +5g>xP=OS;0u?wpGa* H ǝdϊ+( F.{0*d81  [ NJ)d8H0R2<^61  d8a]*@p LgpźT 2~h^\tqIK(pd8`]*@\?\epG..  p 4=|[K[E.Ae#2RU;!Pezf[=S# dRUupԐ-8?!CWOq?z堣4!I`:JFKe`2. .ߨhrt  @Mutptwe~n-=Eݳ2M -V ]8hV s?AµT!&;ϲHL>U_ !y1*ư.ptwc4'8=_=4J@ǡ#ɵ0.1Ku>~TeO8?m ́y8!> ђx@I3+  5M]9 A0R9A.X4 g?X К4tW"p8 Z χ՞!Խwd8 zY 8:ud8p5`]*@p. wc. R2pkud8p5`]*xy)-3JZи!#9K G52g^;߯[' 9d5VKpp2!v hӾύYG2 q gbABGR[a`=~zڢ ;$^߶p_Sz wHR㴤"+ڌm…? RGǵԆfp V*\ )*T(P2\,Z[!yk)fN!P7RPR> 6ʐ'1%܉wdž|8`!x[7ZfѸčލiv}``}\\Ux..JW%_w&''ng* p?R[RoӶr+aZ;XҷR3މݛv=W%:1+iY&cN +Vs3yU/݃$!%);6I+\:1Zcݖf6]~.=[mwטmݲ?Ĭu2O %&pCm;oqt岃+%^xU{'wtU݉FNڶ0xGv4g$U6]2ݶ*;k-jTզ,Rh^ȆR%m~iyyր.{j2v]QU4ܵ]3wY;R[Oy?-sϦM5'vlNsl%Ŕug}z멳)nFIښƭ)A&]OEJeƵI/֭]l5+jmG47ҩPKƑkӲ% .wێلPemd9pзW65EZP?m}+a6+õ>͊ΨN:a6˪6SEch8/jza(Bb[:s0;Mrg~6M7wE۰B;Z" [rMkSLl,nsc.NS~ڞ+U[]ÎJuiv3q '%11S66Ȩ=-&lS~Bh#;Wݙt*/?}c|4n)vt,,(((((U%kgder}t:<:w*i hO5v%*l}Vnڟz:+[ҕH7> 6ʐ'1%܉wdž94₭Ƿ8-ֽAMW.=礈K#"ң:eWҙҚ̃?Ƅtp=W ڠM]v%̃{~>m =jhG/Z\K}TV*2KwNwuƁ}k#%ŔTYYcWtk맲ʕ$kcGN9c2tLPgиKQkwN:Ih݃ ݓttO6(rw0%ܦϘ}t)sdžCRDq 6g&s3F毗\ŧ֞ER W ]Ŗ%$$ U+..O%''nhKA82d8p  @d82p`̴|=| 2gG>\+CfnCK*~;]U2rMaQ|_qd8?Ə|-vG|s өo*ISؐG !|5?ŹHNq/&U'-V$韤\%%޺BϬ|fL;7It=zoO,y~nmܽMxuGL5 O/F9 ľW6:~y+?+SwxؗW !DUKɹW>&?gMK}zs_0W4{̴e/.6՜zٗGyU!R+o񜂬o96'Z/}vk'Syׇ~/B]nj͇e+;]n*(ؚ#ϙo3)dسu޺s6!>YcB Bۣ %LhU2w%&!+īky|_&9v(/ZNEE׏1gKBe^ȊBhhU|'PԱU9^u{;SZeAjumѩ_Bi%aʌ Ϝ{,W f ͒[QR}>|q dvv9oՔT)>~ZwgZ,Vɗ'՜N;,>?Rj1֣ <8@;{X IDATf`wgw{dMyN3|/߮x 3'<^<}˫8SKkeSmp*tv73"pMf'\]oJHH_ŧgm82d8p  @d82pړ9^$SƜg.;cmQ-Gͱů6ܳ4rwリV/9[sѪ}g]"wu픎 /r0O!v}ޒR̮~cB9iϾO(ݾds]u٦yU+f{frtVjOEkJ%߮>ʓ#M5kYm/H윕=x/͝𖚦NY#ʷ?fOʙ/vŒUhQX/;^{WX;t-Z2hNr֎z|T 7!}iUsXO]o?{HE sytΣ:iBK7?nd20$=m 9[c1?x6X["fIw˞/Rhw(ڀ1XҥJߡEO.1:GG#M;xa5yK-Zϭ3wQ9X=tOmX/tY7^;чFÅ 5g~0e>KOj~:!=쨙34X+<:;uwGAMB77=?n j2Tstox˹7zROsnzgc]]O9xÞRݿ;;%mB[KL_2֭;w ъ89 o 6;>ݾή^]4lrڢWOO{S\XUq[q'u iڻ9ᇹŖZŋR >߂km@%Cf`?[}p3򓭺Jqt N=BD֑Sӯlxu w4 Q]YAz 2yV!}UŌz=BIj8¬uvdPΦ8Gv !>[6'9t7WfL$2 h?t[{1O>?ceI'FREqɍdEuP K^J>WA!#ܔ?wn}Z}4fj Yǜ}l6L|O4ºz Sk+ϤZJi}d_{~+2k TN=zcgoNJY?4YLaNX_rKN\Uݩ ZgOt 6O nz!t|޷>2ȷIQ*~YŹ\#no>S?7/FJ?UiE JXjx3}% -ht}Гzl>1ksӀX*mqk[fltzw?ŏbssZ7[Y&lڟn 5fCFxJB:ܲa44/*+R3ݭ$\W{MofAyI UDNޘve TVڄ#& D;ٲ7=]&vy_sw,M#]\r"$+K*Յ(oZ\w3*Ej\\ӥw/?}msqq7[MYemFfA!M6xMbp~r2zƫ=ܛ={HǮ!;z_?9SGD](~_O\K@%tNyֆ&"*22*22*mQc s!&4r:Uи6r4SpPoɧhbY!ڼSQ lB!WdeUk쪗y춅)KӊmBa=%EJO?M~_ϦL.%}gNx$\Qze*5ZM\(.2..<* 4%&X_]r\i?]O' Q}qvzw?ھl5蕋|q+^<>?0cѫf x~J':o~yi}_@}!h:˄ j@Q&/_ފv*7!zaN~s,W#,+o#ߞv$ܱeЦ)T)ғ_ٰzT.ڎ#©+:=>$HwѢ7ϹKB(U~smfS`d8TBmDx[3let+ݿSeo3<>[TfBm3 ֛\">c5BDkesߝзVپq}79 gvr9uL#~i]-m$޽27͟`֌rwW^3B/YȲPS}4=L!M~W;:956K?j4}ڮ\pҢ1pzT\|cB8&nN]Tiv}``}\\Ux09o'- `s]%{8Ϧů._{f]K8˿*?99y+[^jhGͺ͸_mKm;\cl/M{@=;=„Ϟ~}{m1ƆXͧVⶰYύkm~8Du sT fg5McpN@ 2pd8@h%Z漏3㘇  2d8p  @d82p  tWd 2ܬJ Z*d8p  @d82p 8 Q? IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/pymeasure-sequencer.png0000644000175100001770000005351014623331163022524 0ustar00runnerdockerPNG  IHDR_/iCCPICC profile(}=H@_S"U;8dNԊ8J`Zu0 4$).kŪ "%/)=BTkP5Hcb6*"~ŤL=^s|׻>S&|" xxf9XIRω #e8xfȤCbf%C%&+FBegRc{J4GHBʨBV)ڏy\2`X@*$5 )7)_lcu> f?I0 \\5ytɐOS(30x qd+Rǻ{rk&bKGD pHYsttfxtIME 5;4ϲ'tEXtCommentCreated with GIMPW IDATxOhY/iȝz)??rńyFBKt"ICFZ`dƾ$ &kF$`^izCnhG#w7hf*թ-? *U_)2Ο?#ADDu/""b "" ""b ",S=O`G|D-j`A/f,`3c1X4\:^",h[tCSHXGDS==x9@OO,BJ\Rδ GB,G/fR9RpiR.hz75M]]N^c&S6Rswjn-&/|3̀B o~xnk`w c%ױ7Em9y@%{ȅK/_Pgߥ?ޯ;o=&-&/588{]4[hch/+ kIMDvfܵvQ9Lp x}|]bFS=7鸄ITuP 8`/уR^%f]=/,k n<ƥɔJ""z$N*=JS(3djKF%p4]dj `w7.‹4z6;⧡ xR7|oU57;]v;뉗+YyBkte{RNzx9q;=uUM.V:i׹2^NOw{Ǖ΄"b:jo'/UuBыuj]<K 8oL"鰽'=5$O=t-4>@W %,<մws< J!Ur_H%5ǵ.ŲwaZ/nx~T R R Y2RM,Undi'51įϔi1[)dg.LR)Ϻ,4-K|3xu>@^xTBVwupjOqܽ`LV^&u <l=^y$*׻LL,1eUmOPYT?Jntuu:wZ|y润 WO;w|aVm'yTU0"":Siz_*--=Yy'aJm ݰ\Z@޻qgpmj)5Ir-@jNki\|>Ϲ! hWVߴCl%> ":C9&j',f^]g]v#+fo$JD{!}ךV/>|*D3xuM`j[_{WBϼBT LJyXvtRT>%KgR3uR˂,L:i(""b˂,,,֊޽ˣIDtB}'#XG~"DD'i(""b "" ""b "" ""b '`0`0 "g`.DDD,~ ߂2ʉoD|. Q.\!Q gr9]KceއYi,a5W83S>̯0Zut)M[?,7`+25ZaA DD`ADD g@f ylo,6lI203"":k- 'F|VMC6Q xI9Dq"":>8 :c ,0D9 < b0l6+FppZr(4&"" ""b "cp}1tߕB %n,[!"0V.ZЍ}qk&+.g=wX1\{p׾M!p`ˀ; brʅ˸\~ς,9&j5NCT*0Q#LC1X1X1X}GW uE}`B{p7"b˘ɖP*PյR@wHe/ɝ`!˯;%ӯwC-[p ":`W,ÍO?5)JJFN-!OsyT@H U,My UY6"r|EZ4f[bkIz3WE߻# 1i+I?KBRGKϓK br sRewkԴg|.Tei'ڷI餙Wb%JY@ܶ-(eX}8rJ%)9P(U -w߁kflwyBxeu8𪒟T]`&[hs33re^ Ҳ =s!8v]CR٥ .W6BJ-8YNC9}r-}8Z<^7oW.J(UZL^</p{DD(X-Wפ<]<܃ R_'e?u))Hͩi =_H;07 ?{*틨Tzp9~F_\nՄnHEWזSP " ]yk/H9ncJ*Ǐ݋[Xb3pMI|ۣo!&')$݃Nl+YDtwV .NZ:o0Zo{4iaU},,,,,TVt]M"믿>?!":a?xLC1X1X`?)`_~$u*44X_o7h*ѩ R%t#W.\.\Q sl@"G1};6D`ጕQ.0?@>æS`ƨm;ym(">ADt:EKr[ ϯb 6-0@v%""`ߵȸSApCh$p!*'0 ",~ +1RtVG:RT ;l]}I? +#(TFpc5ޭ 2b ،l"NlY5Z dj {A`t,w%5 `a ,\@`Q PXj;Bꐎ՚Cko}6~DDղ "" ""b "P,c>"":y R@DD0 EDD DD`ADD DD`ADD DD`ADD DDD DD`ADD DD`ADD DD`ADD DD`ADD`ADD DD`ADD DD`ADD DD`ADD DDD DD`ADD DD`ADD DD`ADD DD`ADD`ADD DD`ADD DD`ADD DD`ADD DDD DD`ADD DD`ADD DD`ADD DDD DD`ADD DDt<>hugϞh5qʕ/&_4 /#VNj/ܿ˿`,,,Y0hT_?C5<]ߵbkm [!S,'n.Q,Q,1jg8P~y51  _>>/g,y&WZ E`{p˜XxHTpg/wOlG|ir!:>x5u!; 1]*jk\u\xEX#c[[<}{WU=! & !?ki,-TS7x&O(<[E2*o? %$l+B_ī㏀ ?Ѥ*ӈ-жrF{䶆i =FxRxƯto'ȼ_<}*]rƯ^S)Y1?=B-m~/X*Kl ױ7E3dJzRc\0 u_]v-Q> ]w=x|v(^>gF%~ ƃ/7{# 3c?z*E_oTv+?>`6 QǟC]wj[2MttŻ.Q1?`z)x/_PgVӅKxXv^`GުovĕRj .Ji>LD`QcT76p SiF||/E;O1aaf\.">oFx3Bux4B񃈨u[oB]I;]z=<0ZTK)`уʴ&ܛL\DuاSuhF?؅սOcz=FJZ>(4\]/O ""=w}W>,Ҙ.e1ѩ$"" "":8w}Wa "T*1XQCLC1X1X1X1XZٳgI@^;.^lWg[LpZjP9 RDFaH m qy2L~7//:*Z`]{R\#UU"&a)dX]la^OtQLpL tU Cr]vUy/ j`Ɲ޶Jװ>;!ඣRg[(`o@(2?fXpXVofa7fB?dE.aX4Wo` ET WX+a!DY,vhV&-1&O囘:a 1׶x1ѭl(m.F:=qT.En{Xk%Vz:^+b Yâ6&}],./`f Ypۑ]DQh('Ukcrj' OYQCoQ!ǪB jkW/lVF:f9-Pm#bO:v(gԎ׊PzWXCVKەZSg {۰.$a]VwߢlRS Y+ʩ& MuPb܃ŵu1Wݼ5]p%M$5c<XUgXLF_V\zB]SLV,> z|߷Xl}ι" IDAT!`YAjmTAsR. ⟇ٺu!iϵ.2]tc̻2X5j;fm{+W@@.Z~"e̡4Y!﫬WH3a,¶Wy 携!0Ҵ`}V1-.JǴ~4x_t:~mR=އ>\jK)֡YCUס 0Rg=XTx{I_ P RʌϞ=3DvzEKYx !Ùi?ٲ ",)> uʘƖ@gl@eADDвhubNkΪ""i(""b "" ""b "" ""b ",""p=X=d /x sxᓆ0 EDDM1X1X1X1XEsI19 tA:m@C',[$r,rlNYHa"ϏyD p!~"# j9WxٙbK"NCG G,5H w\U>:klڔ4~MBs|~(͑|tnZK9IaiROzo,vkaѥS-~IDkosoz1 r[cWj?+$1_3Z%%-GUk7`Teb_Bl"p Fã6dV:ե |S]We;Kn+ J$7t<\+#S:f[)$|wj&i:˒-:1iE1gebL(783NGlw#:hҨL aԶI .l ˑ:, )#iϣ| B*_.y:C7gMT*7{gZ>m\\l]V7[(/Q>~֎UWx  pZǨ\Ele"n2hvЬ2qR?#c2lձ~՟_+n-^KkX׸hV'Љ?óцq-32K JMd]*N@-gnF'w6Ք&vV]u ez+Ι2IXkȜ §6OSQ[955lqK4T9&',$rؚ`"L\qh;2g3r.QRzzq8Aג KT|/T:`aMC{5< U,!C#-,iU 62[$wxCX,3b \KH߯ hmu6/d5huEM)])G/m`7KneَCJb>zFqe;dI\Z\,G1{4(ƼmfqDlpiI/m-Ľp ")GqC!RRrux2y}AF{29lel`e^l}^O^ `;9P0 BH$psϛߨ}i% G_^R-C ,-PV  5ۤyzdw4E*P._;k`ޥ}5dSqe%DYӇQP"bރ1 An5g 0*P2v;XaO3& ]MJ0E|ʻ(+ðfW!re}{ #ݍzk|Xj.BmUЀ~\PSBf=( ,0+>q>d8uBt5~Gcw(]Uԡ=:Յ;wEQ+_RFlX<[j>/ܰ랚*Q\UK^+=fn{ȿPxլWLSV }J (X'o~u _ͽ@U#KyEuò4\ˏ6{HAI:cu)_߲\+OhUtp8'F|o_"XC81ڸ^_â5tvk(<[EsZ>UIΝk/PTȅGFiƕ}>z: GA~GYNqگOӒhv]Ji3ks`NY81-rzOz(&mts]Q;/ս.[a׽ٴatD'be>Լrn}grch8e\gE Rl?1oo9Q#O1gNcY}lы/N䰪IAGK |tz7{} ;hYБD x(:;M׈/Fx}y6_8I s)`ѩ pŠPzlTt:} Zar'v{-ŋ<DĖeAt%"ز "" ""b "" ""b "" ??a]c(ZwcW'Om Fl 颲7>U^ca}xSc˘(N K@nl$Ƶ>{Btm"QfpZ,NZ~;)Kpe|Då6zqt+K- ~6}>sJK#i9":5⧟~—_~O?_~fZTO?ByCzbųUd!Lc=%?}?kZcOWhǤ梳,>CD"L& JJ0LD";XZ8ܕy&^ !S'Oif+,mtca]؎k"2IqԥegiUqدwѣGX[[GݻwHDaSOW)NUJ( #:*U !{} ix"A R,XL#d]Wd'nVquM7&w n#kC^}B9e;qxa؍h{bxXF_*]guLhTij@(,m_pZq}B\~kUϹspR)MX[[C*;wpܹ~6#(-O`!B7( 0L(az7"EDC [fE -bm'u0(N04Gcg=iCku#` !vȏPU4m -a{aa8bH]}&`ba T,-ª> ;P䣃p!V>: aY.1T`q Յev,///KGJw:k6XUD~zM${䑭5]\6`5|t놩5-X,"YWHwgE0/m{8+ Yt0g<4R}AAK+決oJhIxM;:Յ;wtQ7a9;E5!Pe8=_#FPCEnGQ[8BܓruV}1\j>AFs-rʀ3[gןgzu*7Ú]ųֵ ת: {GDL}BQ Z2&Hr\afQi(`GGa7GM+91槅/棸7nb8 )O/w+BF>۪Z,fh?‚[a.3 _ i6<0bwaC^:'M2lE6S^aɓog&[ጕ"e+ g`#ۺ#\Y=FxS=P!4 cimyi=v5"vF+bk|64tnư2񢜢uXCH/+vuG{`;%3cgӆ;lbA7;EX+>Q=V75ڌ Wj1˝1^E>zH Y-\:.kc u%8EJp06c:gh0^)2K11'Ʈf(*CRke^;vޤ.qݵA嶰f9汢FL邃s\@,:?ijJBMi#ADRjCY^-IbeއV!lϟS#/˪49[}5ͯb "9I 6:Gt7aڄ`qv0{h'ma XyJYIvhza߅Hr,B,a4;E-2x;/+cӘMiÖBN@;y*kއ!%'#],X,X/l*7)=E\^D1¶W2Z7Dy}[ 1,YObQj}im4͆1Ţ*\'#6~{\򚙤P! , u;̽&j!c8- %T0hE("VC{]}&U|1g4{$dX{3GȚ¾ 0FUf! e5]5Ǜ71cZ tJWf7=!u_ }da&dgۭQ6M Fb0Plc ,v#F[BWS3KCg50>mgQ ,Xͩw6}27h䪎'!ȣ!ۀy|A*[nӾ[Ci5j8QL]CC_ 4B>6Fہ.Q+ڌ 6Vtwr*陆u`n $.$޽giR4G`716V}n ިW>ͰfWQ)zGV">`q;&2o+L7m,S,*v@2*wטyxvb>_gAdNPSBmjjkԲbHp%kmM,c»m}ȏu^ָ`o&h),F6du[Ut)} ,ktQ&A(f-`B|jkoXgL@5#:|XʝCx !2Pum"SNLKpn }60[>ڸ)m4r͸s i~T_0FFBUeg T~>,KmhJ6: Wi)ZS(lcXAzy CE'IDAT ="]ƖӀ0Biݻk0ű6MAk1 <^#5nc¸(3՛0v#1n 'x_*όFV/0Ys\Q;0ש;_2UL}@Rg_uń7֯g ec5< X%J:+;uYMF>%VUNmVYd֕G;$VP*fsS{}7?R^ oA/^??`ڙC8 :(݁oW*E*'?jA7?nu^lOR.r}jLlYuZ-l7lh+fs}˺5k> D'n;IEBV,َ.5R!?BUeӴ-0\D:w9,z1o{]}&`ba T,-ª>  5ۤ{;Pkݵiʵ*Y{N.MĴ!9;Ǯ'y,Bw:k6XUD~zM${䑭5]\6`5|t놩5-X,"YWHwgE0/m{8+ Yt0R jJV6ND1hƻ}0VO-Z4ށg7Ki ZE`+q6ڱ:VS3vQu 8JfHXmKmYi_Jk /0mQHrȯ.!Km^ +8䰕VMS&mT,J%2ookx4ZڷZ t5ןgzXCͰfWmmZkU#">-z90[XG0 R [f d7}?91-'.U!+;GkI"ia=yߔ_s/:GtM^j_O(%JZ5/ ;\YEc׸(aYmL[Nn;F51`PZ~EIPZh5 a6n1\K!^tyuXCH/+vuG{`;IbNYM8c(kŋ21@jD\FL.]em`C"ͼ(Ɲ]juTlrQR:c(; qZلmtm\3uUw߄Fl JRӚtqLh7|Ñΐ/^??qJ U?qXwsx/x06߃,, w_z2-a=3qtPu,:Bt ,:"/^A " $>CDtLC1X1X1X1X1X1X1X1X1X1X1Xou0 ?#}a'pG[W\`whKaɕ[*$dG)PbAۅa83ܗ4҄Za ,@X@X a ,@X a ,@X a ,@X a , ,@X aa ,?]_Է S(C a ,|g^[p8Bc?2@XK41 MsO8,;Bwq!':7Mwc0M Gg!a^{j{N]󱓓/qfYkizS)NC(=Hv994]1V^iJH1Y%xhr\6oڊCy}_e;?zܾr~Iz{:S#ڊ5蒆ńEk;ʔdiQo@S+*siu synZb-5W/䛉&'Ey=/|r<}b/mf`5j]3cl+Ɲ4s@-%O y}`ƫf?% r\̵Tܥhw{Ҩ||3)9{i+#YӁըx^*4yNʆ NW<ʶMgNsyJ>_92m{vXCqZAj+r%7ʕ/|Y(:uW +,Mͺ"7](^HX\=V)*UTeʿu%wR0U]w7G**fOںQ]^7^%~=>m˫ Ða 4MS힫Mgڒ=ײ8w)[rGU'{i+7joUSmݻI[TiS՝`^ܔkP6 3m gyݧ浬ֱn^[Go4[/e2]< a2X>!o窧~?P3T.ks[LSCYggR  6M ?S86ffdh,Z'zTسH+A_}.=뭔̤(?sܾ5|#"fz)՟~Ublc)fQ8UjeOYm?Եm+~iZ؍LɍVWrOסr{w^J;{\&ewW=i*])yд,7UZMV7۫. ,o`)E=+U s L^)ުtۚjzoRЩʑ?57fo[/-AU*LL4nJMNvgLjk~ӿ{$M'E9koG?s#mZ#o///vyzR` |}",oE׿Ӌωci(E @X a ,|^[Io7>.̧  ,A2a?'T^ `/(M+uhnzo{~ޥF%@6 e(!ni:/$C6Ѝ-IWc:Eg&}D}׵j.e^ \[oAg yCS2eK5Td)m/e\?}tM>_ Z߳bɽ˲)U+m"44: .Rxn'~V=4Oi m-obya mw;)3mu1}*)ɪ/U.O\r&HU7?׀}l\3vp(CbfSbn3 4wZOOC.P@X aEVIIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/docs/tutorial/pymeasure-tablewidget.png0000644000175100001770000025640214623331163023032 0ustar00runnerdockerPNG  IHDR8:iCCPICC profile(}=H@_?*;8dND8jP! :\MGbYWWAquqRtZxp܏wwT38e ![Bb=!.1S<=||,s>%o2'2ݰ77->q$xܠ ?r]vsa?ό*ucE^xwwgoiM^r"IbKGD pHYs.#.#x?vtIME 4tEXtCommentCreated with GIMPW IDATxexW{V!KA(xZh-P@qkq/P-ny?$@5Y9svwv9g('LO{;B!BWEo5{;e3 @P.B!Bm5UUܫo ?zHj_!B!Nbb".n6l8JmYM,m2eoݺup!B!uVVVTPa#GQ<" IK/ .\M\\ B!B:EQ|nxz(Ӥ+VX%!!AjZ!B!% FLhi4Z-zN'xrkٲe߿2Ó댧dj5uN<.)L6|B!͗HLL qqqO]* $&&>Rh4i?nI4ӶJ*-c!B!xĐ^Fh̰r@9J. 7xzJOu=OoJ QB!B7[TT/rqBBQQQR(5O20g<i_U!B!9"&&¯7 H9Y]҉d9!9#t_G [?!{R1>[})-He&"k F1g41Qyڔ^e¸B&6]BKz㠏Ns U,\_ 鴖|.Ƿ#<z3s,y`lko|7[GS< ڶ",fen}H'T]9Iظv I^y{Tۂf>XeG`ѕakڭ귡U'!TN< Hi_p!:qO /XLOy.^ Bu^6XPe/ڇ9%㚿,Htۙgu]Q}еYA,0ytDs,K2%SZoũ}G9 zk;l4Q:OJK݋~2:Y(obũ8 fֹ?"~V8cV׋;rbpFA G3\=Gl Br΄BB:o)G ަMg@94A[BBΎ\3CPa'?zj Ϟԩ\} 3rv4iԈFѨQS>^|#΋:`i/4oiܥ ~s͟ɄQ?-E,x$"SWqy>TʭG_W3XEL2yn菞N._ɑ dDng+ti5isfcG%7ᮌ&Y~nK8…Bg*fDYVM5ZW{#Eqq --M܏ѓEW%1nз3R6gHU@㈿?1yPFWx4Qٵ0\<ÅUݛ8c;7mG|ԄUh49<;zs_~s% !xw\\UcwiL<2>y1D%=gwGAo鏠FqmTΜ!荛XG.HkP"dTAN.u:.:=I/3a2e,Ք4=(8uVjF"3'2떓HS,#KZAN5\!x+)q), %WR6M뉷偁GPSmpWQxdsʽ 6Rs7|6au bTӤfF;/N/͈o9(xhAѢj4w"cQAK^vNTսR -8ȞBRz>S6B󘙙h9r@Ud2gOo>mB9kqs^ ?B! )s/y_&dj8RA8Nm_Tiq؀pw dg|JS),z=_ƃzz\YJ+8-9~~kyl!&u sNLwX7}}7נ`r]r&=3zL: a:&vz~q.4qDǘ6hJEׄUth j_:r.FgϞJ*Qbtf92:; $t$ !кWτtSn0ׂaiN(y̶dGFjExar[-QյbLѣGOMX.YȮ*+QA , 4PS,]蠖y2t=>8qل_ռrdvnwFeƖop"[zIj*q9B!r~\7j& hjBoCB˝dff#pvv SM[[!B!e t:K_)GZdB!B!`/4R)΀}|B!!11֦jDK9^]H;znFRB!B^^h4hdJFAbffBK9^I-)B!Ҳ+Tdq!B!"g·^B!B!+$!\!B!xBtIB!B!Oy.B!B.B!B!DeuYѣG*B!B^dI-!B!%9B!B!p!B!BBB!B!!<+7뱻鿸B!B!!<~jk/bz%N8׽{7IJ)5eꚡZ^n8zQsI_ɇ+^Po$A!B!^k W9g'tg΄l8H&tmk_a;㿢[Y0q ˚R" ~>t(KoY|)DиftcrSo,y7plzN8uajX|hk Ƹ8[qa8c*k 8ZNnbSF7O`+ !n.[.ݏ߆xs)gT#WFl<ςvv&C&/@Ȼm(M}[S(go Ҙh'?j7u(B!}R-f@q/ڛo%{4N\po VjBX9\߉ ̳3G_ v RՓiOLtØ۫/ޖVүUyNkMmiT >Xh4DDDHel9x ϟ_*C!BB^)1 ZVCb'gg05ŁWK̭qCn$d|N|c!cfOkKzr^f3TcwvcYM&L^efnF#ƔT PQM ̷[pqVdVOӕd2K,?m%b2;Af1|VO\9EEEI/TRlٲ˗/#RlllC*C!  E3\_^aDsp/Fbf{8B>hfpzN7pc%.o)䅍zCGna,䎴/EQ0LRA.^(!BHki,7 ځcfYOY83~i}\8{#WrȶϦ` ,P샙ԯuva̧p|sXvƷx3쒭)|ySL}:P,r@/a5y2ggeHGa K2(s(-QrJW-Ҫa3( ̙ wFϹ#tiBS$L&4 BzsrO=SM:Ľ{f~Vo cvڼy37/&<<(j9rJB!%j2$ !B!p!m٨\̨'z#` B!oz72s7IWt!5Tn-};bGVzWwKf1߉(rhR6yzO!B!\LtMP&#aѠI3}>akdMBc?XxvnH4f𜸟hfl>S3{Nm;sgUWJa N,Wo!BtRB!z͟ܥ?-UɆACW:X?ӌa|mrh\(?81,kHuϼwV sژk{/j^1$ k pt,kOL #B*HKo̷сB y2U,G.E%rM-62%O6>`~Abܒ\yxlIݯ:yH!rYW֓wp;)q3ue8ccaKazn%aÇZ1 0q.T-U,@^w;K-.ӌ1$u-nm`@\m,u+L8z}] :0/˓ǾgB,eͼB!.ěDh0`x<1IAƱx>oۏ{c;~),tw6M |^j^T=qI~[j6i#_w`tZRw3e6 ,b&"meba-6,aX,ك%5`Ds\2emُ?lXcjb\ٛ'*u,ݟ ȑ#>!m\P¶uಗaw!.}b* wROPϞ[7%2= 80*vx-D{ZɗwPɯ\?R~xyzW>^D.oD:(Mҵ2{㏰K#"J o#KIq^֏&UQ$7^y)܈3Ƅ+oAdŅ'6fϜOhXQj{&zxI叠L|Ļ`Y~CRiFZ O77ܼ9<ٿgХv z%t+WS$pdQU ˋ(W@.BB9V9=Âzn-(T;[ӵW}JXF4ApqNJxU;agNQ\X%: Ś{`^CװG! ߿VjVv".@L᫁Pm"5PԊ EjPa;Ҋ?eszzu;);O`,؈i36a00$=EVA2'іF!kׄ,4br)x%Ru\鈾ّXٛ*w Ňk4#<;!i k&,@TYLցfp2;}A r*Er⻑  sZO㎱pz+@%$ptZkZ|#ޱ_z%θ4}~3F;T&Ƿ??Z,]ݶ}cxoPcО_qhzg!F`$rzNBCW k|ZMsB!.pƜهpl+`\)[8!BH W43zB 'Mnέf)U&)h5J*oX٧S_|ߗ fh| 貾0,^#hncd¤*(Jzex="]Sc.?abuԝ֋S&YLEy<Q;n!L$۹fcء MGndT 6m⌮( kd”u{mIJC`bxdU?o컊e.knm]YEj()dÑ3hSVuXa=K⒱~Z7v&f:vg~NVșB!$ &KɨUTu <ܗ/4fѠA\0zh%z~Xq@|7ys(WƆ4j}jCBU>Uˉe+Ȍ695l)ME 1G}ãUmq-P?{X$Nв)IA+ݻjyиxV|4ߡzM#"ߩCƁe y3Qϑ m`F)ɍY6mx&WsTGѨ4Ԍ9mgAg-eͳpъPWx*/DϻE/چ+1ضzr~/]^LhhJ$<~|ەT?{ ~ݩtcAߦh”{qn5M4(Juoɻs:@+gg^]!oti&}Iwl.!o4l  o\yl儇c4fuD˺g2ά#cR ŭY=C)'lŧ;!3/|Rc")?5T+\ 9)?ѶHo)'q~Ivs9ctfՖ;_%G>nh)H[7;o4mȥ,+>~L)}eh?H_3fZ@+0^]LjOÁ+ZST~B! GRtԣY}G+'~ɰ ߲-TBОN7-0ݶ|Y>=]rJ*%_ !_޾ p0t\dH=IBBWl9&j5F,n!% !ooB!D0r~B&KzJ GkB!9BߙfBq\?yKNrC͝-Q!]z!BBBܫ!‰,[(+*]ʖ龩]!BBBh4s…B!p!Op !B!!\<)^ {쑊@BϫZp!xUPA*A! bŊ4 L&TB!BBB!B!! d'26 {X 1OmWn=B!B Op%Wޠ^?FRε =$*Q9JxKO`V{OՉ;C.|XB!Nf{Sl&5!B!o7;z5z/@/7JR쾕SP>nn-RoVkܛ决KEi[{{{;> Kpçh5:ÝfnT w P<]qvKvLvk?Et1C<)V-폻Be貳ĦDM\&<.8zRBs1늋E>aur!B!DN{Z;U=]x۱i6 I PbV0iTk_'U!b"bܩL߂זi|8vJ ccf 67f*}F㶫:"zlnMw],j&,[WCzzafKxqvp-2PCMۤKYMqS@c T89%"TK-,hF잝ӝ9B5##[x6B$8&rl4>NIcT5UB!Bp#Oi\ T=n>@q+IRh4B!BoXwx>FjTt3Oqh"ޕB>Y Ja0Љ 7VL>1|!OsDVw& 777!kd9kFRy66gÅCyŒ8@HXQ4sARw[GjuCiƧ}|B!5xfK JdZ2ɴFFQ\gJɰlţ|R3w;̣w)^cSSs, PU'o^&PpƃMY^?/[ F!B!+􆵄T<Nngם'q30'x~t{N~܀kU So2=,<eLiWceT_Bsi SG\{?؃YbbF/Ļ7=A=v{-C^e3Wq݆p P\4c]&,V72"|ڭ\D4*|lqbԜЉNviWƆkf0l3ߧg"KC);ytq7QLYaUl_Gj-_!B!x+Bxth4?uR%o>.VeIn:7ӗ%#?er Ʋos3x >jr ա~>pHXsw OG>k2eC⒅cm8= %*5|JTNJn =Nj[zG[Fs ?5`l=.J[\fn׎F 6xWAe ,=i>6;4>kꑵn gC#f?ϻㆌ.B!8'ԓ>դJܻwog ^ZknאхxM6oL "B!x XU>y]$]*1dH=iJB!BCڈ̉'wwܑB+999@``Tlۅ]jCC&?f0 B:t'OJmB9B !^{QD !Ŀ^ɒ%{Tlۅ]]JܹsUqo6Mȶ]!u B7 !lۅBHBΚBd(R mBv=9BL71}\5Ie!sQy-7o҅΅Yg8ұeoj{9RU9%|`JS#d l?D|ur xa. FBʃ a7'_3+vIE BjZ*Ν#**HyKstt??a;˩gwԢ'Ʈx1]S7JDQWA2^nQdYʸ=yйX",o=]c`N&{v˦>!t\|[Qu:zPT5ZȇѾP9M=S&c\?B}g~nŪO*|2~94 Ŋ9ŋ?7:DԔ_C}\4R\1uS'['ضp6DsQ1^uWzd7KMYJxX{$J#w!5GEE!5/r[ly lnvP;F86?æ1/f;gSXjxS(֎cv NEe<{ଉaǬKi㥁PgZgբhƀ/H/ηC0휏Ƨ|czEv([f,#ͭXJ,";%艿yO_zۮd2DP#΃Q|bwv)qN:pa|c+aO-B'0OyMR̷)9ワ73^)u39%X~j:;>V;D,p/^ saww;8S^?fNր5Wc01_LfH=ߧKn.]Ѕ|2۳̮mfp3m[\lg-K i_teԷۿ!\o}b=dovN&tr>P/龡*z~KĒ\օyedv$7Z?:BZ9涪[u>},ϳtx_:k17q~RAgP@/Sz[̐~g))8#>_`=(f8!;zg jQ#hPLiK0]?į=}-|uD[Ώóc+_I!8);bVz決u뾾5ŖzZW8.A>q,q 5׭7? o'G4ehULMwr=@׻=kɣkV -ReqC@[M4^UEt{s CJ &}hT<=3wpΫ/_&? gfLr~ CO0;KyGoM& c{ h'۾]C}Sj8f@xxnн'd>q"͝Blƽ;Y:"kef8(@BS93 Dn #~ o% VQr# v3v8zAE)}&C] `Y? ?JTF}p59I%#\O80&?Y0tvq C'3׸O. gRy;\jc9 C q~r-\}0B'){x^Zw;B;lWN:Gt]1-|D+[&bQ5>k€~+p=Z۟fǮ;ZⲶ4UB\Q¦).celi)TS&{S;*J~K&3htJl󘡪4;MIͦ}4ôx?dg\''P"TQR,|*)U:I;2o[(PkQSm'8AQS?v2y/!OT1KVߤEدl:IIUAՠHƧyܶ>mbJBD.HoqK 4YLi+QlzUpWn3}t#g fkm5hIHL{9iT3QL&)I[jid4Gv=w*g.ãw7,%ExqBqwwR$hࡸ;!"!-}u͗ݙ3{ΙsI r׀g^^gN935dikN=6|cjOѧq_=[ /ftWS\\+9q068>Ή ucjNjDa;QEn*[3 B xt˓ɔۜ S4ǺZ= ONe,هl=a(%\I[ozw!ė_uI%ɯVs6Z!g7OMzxYo+e֊Ƈ'<|)Já6v/;~yR5mTXU̎YeRz5I,[%wXwhi$S,l儡\5CcTrX0(GWÚa(l]o.ʐ$\[cYPeVW4輋zWmB@KT&:%0;Z߶t-7r3IF VlGK˸gd/R M+3vt='Ky?T1Wbt?NuPJDMrCҐ\mV}.gfnmkDp!z]v)xEf1f~S^SpͭI٦PXyOlmF?X)˪2:K߼kُ#88ڵk#((J*%8_4Z͎;>qѾq+'6G0u{#sa%C !2~AyU4D$Ki W&nQJ eNȡ#X@WsE$%ӎJR%|!M!2FB/.D{3܌3 &d*]vS ]yr3w3v\:AeK?B8J:u='4#iwCyF4 !⋗H茆TqR0{|ّ |:b=)|A.߮/KYv.]ؿ[i B|u٧aHx:3WťBo^p=N#,<Ȟ:=@eL&Dcmm^GRsB/ZFcmm-!uBo( 7C +& 5O`6%%Ȑj/^$W\}!\xQpۅRcI8Щ#׌Ml1:хȀ9wDFFJ!ؤooo) ۅRKI85 w0 ?̫=NDbM!B!2rnгq MPWΦj !B!"ɐfB!B!$B!B!$B!BH)XXXpI)yQ7 J }(k^4hXkăĤĤB!I?Ŀ{RBX`M7:RxJqcۉc/!&BL-E DH`2 %[dl'+4EKKCۉ&i@IڥQP3Iqܔn9@σ-)G6ʏ %QeH 4ӣSvmn䏍[^ў /N!$&Xs\zͲQ^pz$6W #]՛6ez?'2@4Ia_֭+c%}&[w+w+А^^yѢ#|.ĥ)?l/=UW浥{$)W_73cK\ MI,ne_ atIT:KWhcͫ\v1!6mqRf`Se;w2L&?6EK\~wWQCKLJLfXB!$%;# 9fï,ugG)]>=ccUC)qrs=F2E.q-bѓ^9(裏19g#O> |7I\&w5@p"t\?{ h}SEQahEzD;A~$& 3Orfһ2򢐏jD?EQ,*G=!Jk s4 +aʊwJ~M54 ?,Qxs!ZC7j繞'CON䵱ƛ{(:ܼCR:ǂCէ KbRbSǤBQ}zbG՚EckcwqpRzW]^XϘjgɦ+@1&5ŝS ;j@m鎍֑sTbH|34do㽞һ|cz}G鏭&S:0h545K ra|\z02LNfW>FnMTbQP} 3[ޜ}z|EI\o|#SSZ$wW~㄄UQCK>>"hQ)͝U5慲~HLJL !__2w`Q F.}A]:-C>{+بYpr+@K+VyooM{Ju'깚K/P~;/_^84=Tn.Q%KRx4j2(HɯvP+,.I +P4+v;{QXO>89USB辮Ĝfq(,7W{LB9i/-=QQ.)TV:{5 Ϗ$) ܺ~jЩwԭYgMjBfT`R35$^COХNQP `kY^S+7W(STLJ6SjdZ{:)휰׀x'Kywbw1wEaS$<ZG٣Ac=yv Г6MӲbv;:&+DHLJL~ʘp!XVNn}c} + Їo٪ŰX7>{x@0Oul}c26e jUW~sA9NjHLȱ]?dVmɍ88ߙoxFJN]gax̑~g<]Ifc8BGT 3nEe`?ZVb90~[7*w5t:k5Ӊi#xAB': Q+/yQiiӔINt-f T-'&^eN.)eM9 xL#nGSJTc*:s+ozLxw=o=6I㨚?yj0l||t)^'e^>)]8g@lh, RiuʕѠ>Դ ȡ耄cz$ gOu"*ݐcZF$2d‰5vܹP5GkK^CG0CEs>I2Eq55O5Ԧy yN 8{&t.T֋v+62uyU<֫<'odE;[~L{1{g޸ogN ѥj>yMR,x(KUaUsl1<7r"_L]иbٶtUVjGF`~0~HLJLJL ! #6bEEE8|ڦntmɚJ@ gQ`p;εr-: iV'.Ʉ ,,cK%{S2qGI*0ֶ>0aW?8k^nhI ZD=v͹y?zx^o1?Vp~JOس3iXQ?3֌Opc٥T00TEV<[}CP \IL0,i, v](\rs_˗/!!2\l !sQ>hݠ q=++i HL$]2/B9q1L<ȢQ8aݵ9$ΤEb0 F_7'6nt>د243.yp[ZZJ/#o$[yo,2B!+qp%ư!ҙMʑcr=gٍth@^$] .?z6' 箓@Of̞Ɋ?*`Yzϛ{}е5$./=~\!B/ WbsAn^y<6,aX|{.ײմUgpNڣj3[.߈̙ؑ%Zķ(&;tⅇK! CB!>00t@WW0BBBB!;v쐁LL\|~,B!ߎo& ׇz؛`w,_,2{$\!B!IǧvnΚ !B!|B!BiHyIwt!B!.B!B|".g&-B!B|;%\!B!Z`)u!P_-B!B/'ȵk׈ɓ'rfccp&Cmf>1&g6}܍iC"rqj{*a@(<Ii%tX4B̅ fM]H\Ē;i_2p1{`uel"X ~X_˷+mi> *}=~-Gn=$k!jt.%UI=1i 9nu$o 4jJʢƁٜq%~K6WJ*z$Urу'z nFk*}3qnL n9)s~ggyMSLpބ41HSPλB!2^~5j5>>>R⣹y&ׯ_UҶn+O82'}fl|1ً7fqdZ͹M1*e/`w4YMp8>R޿p9MT$OcіɾU0@gJ#KZP)xf͉6%9Пj cu&6t!fx:uƧ*;8x_95kH WD0v9<}jY64"dX2&_0,)=1aKHδ'Zu76=Ö jYstFW~ӓAy *7-0{>)ˆN5i͒mZ2cnrdվXbb5赕ySbPM+]!3 \rR⣲fϞ=_|rka+ N'mS}?0K3{ngqɤ~lbCzJ&:75|8,\\mxsN+Ҽ/,])P0cqN?Ȧqm^F?CR,HcocҴ4cN^PzE ԃNHNԸkB:_ʙht}aێ0ZmWM_ zye[{F/ 91fJ&e禞>|,5ъ~z4裈RŻ8|-\ !_ I]Hl z;øXtC@[ <w÷W(ykwyI$S j*+wŸQ<5Ϲ wDe,.j jբk'gDD?+^2gA34S1 9s]rÄv#&p?AAN4dC(eVj-}~#\ÿ@w}?fu%2K~1't.耕3ޕ:X^ Y IDAT .{qt !"'B$.rGK%1{WL89~2 Wpr0N˶^ETXxJRܜߑ1wdykCDl([~;掾'_= xbqC)kٵw#c3QURtA4G2ۼPcpsxuoŞL!BH.ėxgZ2x| {Uaqi>RrtD%j26=šclwxQ LқByi 1T+DŤv8YEtV{#O9ƃ'q&#]iI•[X=R#҆#<Le r^ݗpWgX.&Sk~|6yݎx+"Poe2 3޹i%<-鶑}AСtrWZ]Sl:NYpVM;zC%R; &ou=7ܥgL_wUB!$ "#&.ةy(MVc=Vt d(#MyMwc7oP/ovUaQg6g.PBCC =XՙIz*@2i|p1"^kcd'OVg_4X 4]ʁT`]I7= O=)QȞ7ɰ,*&<1z?$,mf׎s?{&9fCC91N~JSܽAbsQ}Ii D|6xı7^'IqYg}J?̘'Bq6_)+Du[Tْ}abHf@wsoS7h ӮkwB܉ يmQﳢ??Ϻ)oOC&lxb$_Οs wdxB#jKv`Zqʖ\ӓQe$+!ra Ȩ4;O$Q^lD,o8ڼwdN3O`{8կqۘ߆LB, y,sz{W/F#+*]=q6SQY{ dڠ.0nC18.m3t/!zlȂ9(ibvdvAN5ԣ}4Жi>{OGkf:.ɦ|Xyz} G(܃BBR!_P?SV/&qe,З=i'm's6{SU6 nVYgOnF'F(ՂKGQ&3'Ki]Y&ZoGQOΰyRZո^=~M:OnFՊjڔY{O|KVe4+fDrr8iF~ŌJд7c{UgjD<9v#Dc\*2pz0.BR6bEEE; ^zox>ޥXVb=&)4#q^pusfN$NGy:4²^tGn>:':/eGrYv-8]k.-1a\4"[7:nF3 _ BHL !bرZ7(( ++i HL$]2DK9e=@uNiM0,(_"% XrB!B|b_SMBSUaioJ4ws/B![L?o3E繴}(e^>~m7Pμ!B!$ oM*9˴bړ[[K. B!BO'cLQ}ނ3>90?ةL1QINZB!$)~'j/fdqԱNBB!BOLmGޫ8;i.M_:fG{RRRB! !>ÇmBBBD"1)B!I_ڵk*-&BISH 4d`6!B!|2 N<)%/>'Obaa&2OB!߆OٳgO<3 3,,,puuB!BH6777)y!B! !>-b.B!ķC-E B!B|.g&-B!B|;%\!B!D%\LZ…B!vHK_E&CP$9&]'kŧP^w$ F4+Kn+/_Sϣh^+#2"BĦtH! #,чW|]3z85Nݙ +4sa +lJo,Q)&ԧnu-KjeyV¦Aq n9.ߕ"55/1q lL1d{q!BH.(;Kt(`O~!$޻ܜ۔BaA jjwV}9~r#+bkI h/oF͡flĘ3IOݵ|_cgdvTON0f=? dc6'hZk0!&%X zmӜ>}ӧXL+LI1EQ:N\)g];X,lZ^r}AB# b ЋU9]6t(A6+ؒ˶kRM,UC{E)zY(luңE!F R+l7r>F!G(8'Ƹz9qߑL G&6UY.&@v?S݌,o PlĘ)~Ÿzrs}Zd6$UdaXJJo太?.[QP/i{`0==jG"wq ZJ|)mLqdžѰC~y޾q5 /ggjp:sw먹l&?W o5vœ ?5:.OkFhJ[u*.p*]ݰO<Ɩ?$"xEo!;ʳlE)%j?&0EI/1De,.j jբkFWxoM&0pJY F 6Q,OXu+9!?ɇ! :`ewN,+6?urLyaGx!BHn6quJUolc ._ѹZFjn&хX`qW=Vyɒ} ,ڿē>O:OYˮ𘹍wJ_} j בshngIEq\NE&%. Iuҥ= 3'W^fjmt (IVi\;eNyK!6 Ɏjo5θ/EzWX:e1'yנ͌cJ~,N::>WѿNlWSgߠA!ٽiҤ ?In^Ŀ :Q{fKv" @yIkDږE"XTheŊ4yl-Du}cJgVj;BkH9+4A@||Kh$ڸbC$ =Xn߇ͲǕV ~3Fk~ew ;Gm&a S(ո _3zPqw>}//Y@G.bӰd(B}AWkm˘ڷgѼr :-j]3rd6ȇ%* @l(]*𴣊G𑂕FovL4Mz%?E֡u gդ߸}6 B2k:wȜ?O 'Gb}:1|ANAvv[.h=c"?d^|N:& 1T+DŤ>rq&$&b势:ԛ|166Ƭ wnk} -ӟWUy͹15pXQ4vk 1TbNR; &ѯ-l̹K5et!Bd$<܅E SDy5e}(eA7si\4wxl:;J+#vd.G x&?:ԶCܔn:o5$feȓ'T0i>$B~ZYqɽ( ,dѰTȃ,Nϰ0mqAn{ ZթD`4:M=_<*1Y;Jz)'E `}it84=Ջ"c6rnʨIW!&tjɁ,w8]Ku?(#}_fM}CE`iCT/?bQ@IL$s$d#=J`7#=w <^ֿqy-Jr%4M>O~kCv3ʉq2WJ]eUV`ė@+?k|6xı7^IqYgkrkLϺ=Pa ۧYB!Ŀa߫-P){hÀUqBnj=&cݽ2ݜCXU%5a2z%⸶{kdCs~;qgX1jǰyG iHe4tQqt6Fos/ޏ~!~c4]5n~I*u,l,l#0h`~thfH}R( ~MbKK1߳VҮ^oNyaŠpC&rTI3|ЫVU 'iҜ+Λ0a~.ʝH|Ql<МQ>+;:)f_?)}>~;`DA!/L#c&÷Pl| r?ar*]G1'F1{&^zu|N,:N[ٔ8}ZƔ@F).~.Vh^gQ, q3yi֮L1Zْsz40,]OGVhU&ػzlb;\>3bҐE VgGi8-G0}V0t\72 gCHI6f&7Bp}3>iO.:UL>(+*|Vֱ8 GWU(Pҝ3v^46d!N[dbMtq=rmDŔgα4qPe)򳘶L!w2ϡNؼҼVybgPe %QT@EўΣ*㻸5yrLnK86G1x "®*AO<;1"gdӬB tg6lA\^9jZ:)݊ee)DEi n%ufT}bDR-Xte2ns /sv'W7s <ʌ,U'*gKLMmOš$eʂw6~ G'Voi4+ۛX[_7xӇg %R. JSqBR"gG"j{G&2&Sm.wVHc:|)3Z"Y&\bb`V&ޚ 1:x (DI.QÃpdҹ1 曥tU{4]so%Iy  s ?@eEsa{5}X^{7h8MN7j[Jv[lNxy{[_T|:#؏hXƏܹQ_3<9r9Z}ڃήX)<8D(e۴ݕIbߧHRSmQ,y12wpuWወgC8y_(o sfo D#n} ߜ[J✕=I^g~Ep/@ъҨd٘Z11l:|.39@'Լ#}abF.8Lt)W+ٟ6M@i^aØ6_ťSύSۊ7PcRyeW6%/%kv&L\5C?aVN|d%iFuu<+"""""@2a~q[jS]"n:BCCsYf߰a:tPGKNHH:BT""rXv-5Ѻaaa4ny{{v@ݒn8E-w(cVU """"."wCzz:ADDDDD!\DMd IDAT6zpu)IpBN/vEXX.8$y.&%դBdKfQp㞌*pb$z4B1*CݎCx:oi58"=gx8gҹ`6&z$yDiwy'7Z}0e˾O|ճ=mǘ| ^F4:O3Vq\O3}a|)/udXemޙ.c䧰9k ̶m۲- V|<1 +mM&xa禭gۦoRy?ͤ֬FII8>>w`ǎz0gʦmv#K0M\.-1|˟6DZ&g )y㹬\I/`˨t~[c^ lym#z;yyit\HDDDr(@y P^7IJd#;#0m2RnO>QG>[A#1my:! N.Fb5v0f#dѭPn-͐Ȓ%K;UOKKѲf#999c9]x &0B|,GƞnxB/}o$\󽣌ZΞFyq&e^nt-]h7{Y5FJ֝F#kQ㿌t0uO/=ԨTBxh58{-gGe>FqzFs#K}cB5N_3<bIU%7odF@g;hb{ʷs~@UWl~0p5* l$a] NU>4v^݉S͑lI0>d|}^6:yΏ}jRC'u]Wq=IY&4gey|fۅ{>dK_N瘳uCgY3?.֧ߴ|;k.`P,ɜw έIv{#u۟Ӌ>AnӵDDGm'7#!M36KPC^"eSk$3`Kx$79hw[\4VwJ ϓ+>ֱܲ?L$uuͥ?SIϗ17vUh7xc*۶KcȪ2'U7yz<5i)JPȟV;9cT"a7J")xX9v:[NNew:C&JH8 J`ػ==(#5˦"AFY(TcjVEfzaJx+5TEVDDD$G 6N/Gnt6vf5AfM+G?(^ִNTW'~ruE\Qw#8eHt}յ1ǴX_G'tyؘDŃ(n6֘(b o*\G!?Q1Aы!';7 6A]䞣W=8,_]WJ4>6iPW:4b%oW,~["iE1VBI Vc8 _Mf}/S}Z\9FGcGG\|=|p[ Gek?3-8NTЅf%ƁճY^FB8`2ӿӏΧ)g/lTWW?*w_ڍ\@αthߌ<쪿%e,iZՏ6/E~lB≊;vX ;fE1f⼾>٭>%3eS>>߅cik&-l.#A9NG|+S뱶QcX8a1m| a{[l4glk# p;廬&yXZμw_oغsjD פn8ѧ M د`d͸5{wӴ>M#D̸zvP\JS51O%ޭY,z;KKo;~o_G2!<.CFx6+>5bɌI6KΉzӪ:ʎ^ά.epݛHOc˺$e l8JzUi^ms_슈 ""w)/IYvow`tRF79xerƻH Ŝ`ćߒR'],fiijգ2Lu_ֳTfG)D j-||&zzϸTHȵC: 48ykk Lȵ^N4h8nvÏ⁾.ws6d_&L&3XHI K0ukv,SV"""r{ud.=g ׬ 9?ů4UJS5qPqdV׀c1o9^4l".cy)3{xNm~ر&=loе\Ug(ѕ1N1c˶W@ &,X' mDLO}b''2#?e A҉y[W19>Y=8j<3OFΰgq'(aKnϿ"+|O-IL8:P('r[a!|uN[>);hԷN@궙 D4βwY1<`~gCjfSpK1(|2ֵ_̨.PZ) :_&*;ƏFsh 4n9x0:UI =Մ`<!8~r~kG.,_fV\z(O{eފ4 g=oGOa_RwμÌWrYZ(c/4?L];w$ <[9 d[9z]k3 6A 4 oJ*ԢQELъ2 $=Lb܁hMx#'>1ii+ۇQdQY9-3ՏВ$\֤SM_ ~CC].:]&m&~g1ax;q_ڴ{;}R7_lы^ Z9cCGF//l!\v§XEw;f.}>Ynӿ%Ϥ[d&YbpG?Npݺut;#xZ ̞u2(r`l}aa>o:edʞᵨ>I6A cҥ4n?uuׯ_Oڵ3 }+gv':|!?q=NHH:BT""rXv-5Ѻaaa9=@:f/y<^f{x).ѷ*_>qۑ -k:ccV U2Cѝg][mW7 흋Y(?O. ֋/""""""wƊEDDDDDD-riFEDDDDDD>^Ֆwp///OQ^^^cZZDDDDDuGƒ8tׯ'>>^y{{EPP}"(A\D2lڴ)ۄ$OQM."Wj0BBBqjRZM(Hta6pQ;L."""""".""""""`tt{LEDDDD;4.""""""rh$\HFEDDDDDDc H}\'jݏrMPJN6/_fݞNp40e-TZH9nQfҲnMpfŵ [;.+u7C9ݸɄsD?(5iJcljHA۲j},xM 8k b:,Jmh'F/OYѱ2C4.rʕp[g`U/T"T}e>Շ:^fi5 }8+й5o72>;hW <)1 Gpk@pև-qcA~Tᅭ-/]eWD3j{dtzfs9MX63/^M?K=F!BrW<DޣГ4mR _Etgx;[矬x8fHJ$ r)lzzvQXiBȃDE^Ϋe|&~>ϥeWYHpy[ l'8۪`B u*@v1+4|G}3 u!*\nūPjU Yb7LO;'Cb^?m{ONi `{0\[F/mʌۇ+WP.O1oU㪊ɻr[?{6S)F4̏ ey?W-2eyjtv8`;xiRU7X}Vڙ[`ɎQ<-"""wƒD102w=ѣ⥏y?v$2Z8|r:Q<3o5a%a7E܎5]{lfPMÒ̱ 7~t鶂Aҙ#x1Kxo}k81Wسr *{(i0+#zRQu̒ ujY[im+)Ѩd7^<ڤÛ1n;C5g})l&HHx⓽+ Fp:_"YHpfŜkL17]~3ÍgZ92CNvfd;gK$͵(WXπ<񏜲?!d&gb4߻<LJUfɔÑFQ9fWWcy]kҮc=Z]a$!4b8f<1~))xOf_ 9h#}/ʓOʨkeۚɺ~gCj!H.Û3K|o0`;q_X`_g.g hϰG]rԆnώ{Ax) 1E """ "(e!,8c=ͨڷR=BNL߅sGa+<$?cW6-ǭ^xvAb4O?V?BKC/IMZ9\T81|r^7t +jr&w 6ۻ&@%cY4| ݮ /bꀾو =ލ_ 2!&73/vP->>~JN|RJ8L&*T˅HbccS+kW=G޾:"""""r_יIJJZj\zs5}vdɒ\|Y!""""" ?{F}SIII())I """""FM[z-EDDDDD!\0L<""""""wY] 9gB1^D1 #Wܿ w/]$Ĝ"ՓbwGs$$hjEDDDDq8|uS2yZcJUoӽлen RO?Lʕ(f 2v;QF gLOO'B= v0 ^øwK~8[gr|?ç[!ܸͩsdI&t<؅̝;æMrMxx>$OQMܷ!<퟼ۊOdVOwGh1#3+iTcFNZu)h掴`p Q1\(L9DL;ek)<#o.""""" ,|JQw`.&Ϧ3`2a&4}ca3,8%oV \ R|gڸۡNp[b,qmŶg>so_y1uAvϰ[9ЉM;l+"""""oqCǧ;cmo "SƱ|a{>>\Ё؞(qIeRށyA`{h㾖N™x= SX~mI8p"s ݶi$&$?nf09fBE /Y $%&/IfNx(PL3nݘ={>8nu4,9D k~A/%p' qc;=hq'ǑhxP6͞x{9r. 39 3n,I@~m]2T1aqDi dͅ2hdGYpp!/̦;VyFEDDDDmȳCP3f{/,uWM7֧_{ӀH&dMНh~ KSP|fVڕ>C> Yp-NM1{Sw6݌o e}+""""" 9P otɷR#{NƋpCFEDDDD^?.eyf *A/$:Q'~kڏRnYʑt7mln-sU)Q(\ʶdkŕIvO/_Q_ܜ])P>]&N[uj>ǜY#Ma3glor>rVMwԑ;ofD2^ lym#z;yy]d_dNֿ}MI$yyر#c28]ɍVfٲ/S$_lϻ?_aMzΚdkHMRsYz)^(Q:jVDDD;<͆dfӟ9: b6c6M ϼ\.mSoo3%@sl: aשE؍.,1]ZK!4ݮ stU<ˏ+cw2T 񣋼6m _)Oe˪I^OR TMN_|'vޔۚI?K<4YAקg3Ż#|yS$ֆ2V5)=9]_bui)1-TW'~r G:%T"""rgG½ػwoV@rfgrϮ>}Z~>}4yMx(-f%̤йl$vr:Q<3o5a%ۑ4s3E r7|Ц4n=(GqJ%ۏ۶aY) $c75oVv`A%0vfUdFn%<ҕu*3RdֿmML qքI޸jEkuTP/[$@ ;vfPLwLjI-E8v/wGڦ* qAvA||z7"(((INNV'总;j=œ`3@]Mb1b(g">*qdvc4\zU0Ҽt*ZX{ ~o8-œzS3o֮u1~"-r[9]v5i\6p1K1(ՍgȤtZ=G{LoqmkRvD 6 aW{}$lTcbxP_ZĤ.]Zzb2bg̛S|،3lXV㓳p;̶o'0SY63+ԟ ?uт7I';aPC*?cQxr*f `LL>o>?Ûࡒ՜˚9*$n೉c9{ EXo L+`#zV@̳",7ͻFP|#__وCO3wfn|3øh8㧨DnoݺuhSv후Xt"{Rn::t蠎<#<<u&ED侱vZ5juhܸm ҁ4%~H=f7ťQ]&lj(mF5xzfy1X)i]<=*t@m<7g~49~\›U4'o'5!>;hWGs4~M{?sƖ{))$s //;vd,[5mǭ;Ylc>Y-?/y͇p| zoٲ|ջ9KzEDD$gH^ H2_ӣ%F]oQ6߀s2g`K|*/y,ie7eЦ-U 0q~-:NMl{N*?I#f}oxfAC>hc$dE5zc?xiRU9|6kqeԵ7dg%u\[DDDnO1m7n=\2*UX*tpMl:HHG̼Ԑm7L6γru?_K=C5g7p6(x#_V.)s~év[ש(%.#քe".1Qw{gQD|T 7'6&_-}&-e/(3׋`$!>!4b8s^17^ٛE3{Jg~_^3e=@Ԅ ؐg̦ [H-ZLc,~{Fv.\$VŐ>!ަ5Mܯqٌg!/7%]Iל}6L>8iIG;FS S1&43 ywwcNE+p$XYKE~LxB7jWv]9%ӍjQj=:@,{ƅ n7sr4yneϧ-}3ױu2[ҒzWF7'H0'9qkFn g~,p:*y]jTyLb3ع` FR~tбyx'9{!67I^6͸DC+ؐ.km2aa՚ɛeŗ'H^HQ/ttm:&2;hחgm7n/)ˊk88d6HO +WX SgH3sq)&9+4켆ֱ{9\#\8OBhQs8ەŏo?A| ^=jy}'OmEXT;yUj ~׬>Ƭ~Zק7>Շ3\ّџN3+Xr6Ks]?EKڳ7þZ37{Qb m2#|3ƏLS3DDDD!\}gQ|Iwʜ]7i's:)ƖuIʼ G\U*o\JpxV\) GW Cp^"oZظ0:ǯa++=e;_y6bc ZtLs_ye}'G vofۅ+y-AYK,\դ Sٹ/vEDAĎ q»—k)$F eK;gm[0.eDMgL&&TlbDz1Ly2&=loе\Ug(ѕ1N1c˶G:xH|4?{cn|M>xk]7gd]oa 'wafGSMFTȞ+%*R"Ϧ=x! &U&u|Z>;~~K&Q~ſ"q  u_NYУB0m{+h:#֤W ?wE ,NbWD י'Y8v{3Qu8&V#iz^HcOM*< YYP~ UDDDE+SBW#NSךqņJX };3:S~IFHnYq/&wio>ȍ|r^7t +jr&w ̘>^bPxxcqͼX:j>No~;g\:~ɪ1-6A-lKeϧ]NhƱ@[f#hW'S@5ڎ]ɄhOۚGۻ&@%cY4|0|r6n)ݛES?S:?6$<)ZI>X ,`K$9{N$`stX}j9e1a~q[jS]"n:BCCs]vFpe,^P=G kС:ZpBBBڵkiԨQ qƷ]ۻ'HtE#"X4ss0>]N[#"""""5Qyh:=""""" K4.ri$\DDDDD!\DժNQ!==] """"w|MM" BD#"vlQ[nRՖhkժQV5hbj֌Zk4F"#AyxwS9[ ¼aHpZik0V}~qhǜMAcq~<88 #OPضu㦌=]DDDRD$cTX9}6\'K ԤR {,p惓cz\rLpt?G ƫ 攴apj$qff{t+Y8H^Y]v(NM;t5DDDDEwz:0ofp.K'gn WlCxܧ-a3\pwsH?wW’©o[˃׸Ihh$&%:B8|ؽ٧ǣ|ȬH߮N2ODvnmǸ2-uTz=yMq'J~2n9p\D٪1͸_p0)iÆ+?n=Sm!4+\$ֈvv%ı߲kX8n;H#61 """.{wGX,=l~7QsYzC]fvq *JpUM;!T%"8q2 vlXƤNl]NaܗUpO[ ]R& e_s,!~#vLtۍp0S,fe~/2ᐽ$m%jǭ`R u|ve>qu֏msI8kd' h5y.<~Q\.wa,:BMG}&(/MNgYxяnvs^"K7=#V3n䊃yp}44$K/oI[g$&*,H+M)Ru bd[nN<lp*W{kMXEpHvrm2Rs N_61g/OE f| a8~_>ष^DDDEm:=&kWq7sn"r8G*e-\Ox )WE49ā` ޷A5%(lkGHwrNyƖbW i &b}/hFl}˵[o/ˉjqsWze'|L<5a_ӬZ^s~d\9I sNly˚#٘[y*""")_] nyZXk7fq4r5ɋaGNҥW5fEbZ,!# lҗҝw/ &̙s {io Ljv8 ؉R@| slٝ5SXr~8f2pđ5S7[*iݴK֝Dy|NEƓ֤?,#= bє&e\ġ %ә%2FP ϑ^G̞&6"N=Ξ3eZnטk);[ zsd(X̅i޾ S?I n|V# bՒ|qfwO-ӑJ;A  xTOJ^?lh8,G8w` stD,Y(YS 73+Ïǔ =MSFrHk W8~, g;XKROVu.oGsbsSz@,Uq'm+"A{np—.e@/:cmŔUNh=}6/yg|BȜjdAo Ȟ/v[dDDuH֭[]~=˗5ibo%sZtzWs;uݺu4kL-iFPPQM c͚5ԨQ#EPfdsqqDK\E#"쾛PTt}h%G"""""/>}SDDDDDDD!\DDDDDD",FEDDDDDD2(sbX """"" "<ũDDDDDEyؾ}o4E5)""".BUI IIk5)""."IҍDDDDDED!\DDDDD2}OBEER:4.""""""h$\$i$\DDDDաpD#"L#"""""I2ƶ\gӻRN_NvSKT4#?AlFsTmܜÕ bgX:5C:]R:h+7JpqLGz>{f5C.CLH1'~ƒ|st.k;NoUl[ Ӄcc~LYMFж)#U>}%*IoN88ۚ.`;.""" "/HV1 z~\przpqĠ&0+oCAxtEaԈEFr3}ZŊQRl+~8TS7rӷ{*gua_7N9d7X}맷ļ z5|ը/d̪?ٹz 5gЦb+~g~餃 iפ-?Y]ug3 ;z]gHYMڙoq+K]dutzxgJڟԓ8nj;-e@C|ݸ)cʐl|^ Ci?k cw1mZ-~ pj.OY1=/KEN>!m |A|ecyߨTTQ{bkovm /; 7fG\m~Ӟ7%_㫠T ̱z{-sU_Ҽ=P"[[| q;.:gu+/PyO&MdSyDZʛLZۙ$xdԄ{V=ߗeP"j2~,me|j8%'ML*aw_DDDA%Tvg$ܰJ%)Y2~ } :#F­ngi;|kU91s?ں/i6xtZN n\^CUgw=ow7DWA<9_~\cd SucOYUl/eX#C;=@6%]ܔWV*7 7" ҄36ѸCJiBCYɝQ_R9W?ߚSJ98x!01\=#i9Z_v?ne†M°X^[phte298C>6JYj3_'z5yk|~ +FŒ\MaG┫',ed0_Yk5 `h7v'%pSLEDDDE"i©ΒV Tu[~ l8H>FQQYǵ67@X!b< Q0}C&gJU.ӍӜCp;[[6Mގ,,Wͤ> vO,Ϭ&cZ;[56&0bcH&DNly˚#٘[y*""")τG~;*}PAz+K^՘#:k$سkac^K_><6Kw/xѡ,:#{1- '=_3=kIiCU#2""""""" """""""/MGIe朋24.""""""h$\$i$\DDDDD!\DŢNQ!..N """"."۟x u)Ip:NTWMJZIpIn&""""." """""{EDDDDDDEDDDDDD^..4]DDDDաpD#"L#"""""<' Ie yuh$\H658ޕ"ud~9V~ɓͅJr[hhd86eMX6ȃmӘ#!zislxޜ$HJBqJHUh6.'nq\z,D6'2(J^?sJⅫѧЩhF ~4,N'yWӔ?0d)h^ewlM\͎ϻhפBHZ*qoy{2^myS߷ <"`0jD̢mlI2l}(;.P \Ǭbk\۷/~9:d%;wbםet.@u)k`kwmb;{IAsn][?kDǹFx /혳)uZ`,@P&z/Vz e[ñ=ko1=~>xwY)gm ǾoADZ,|M$GEҌlO_0`*Ůi8mIoi6j?k3Pv g|@*ΊI׷gmY==7=][ʖħLdSyDZʛLZۙf s>Jww?q-mL U;Q$ (/){ϝ+gRFO6@qfnِn&UjxҚ}ȉv7-=$p}m *@i9^? [g0-h9F˯#Qk2'zEDD$ IewGcۺP +CUi7#[ٺ#G­ngi;|kU91s?ں))[rx&cVWTȂKBT0¸lcK7-){<8Xfg8[}dIhf< xb\ͪ֋H ŰH.dlsV$q9p,NŕJ&N ٻ p'MpɊqDio8#)gmj=]L ?ѫ Ip99ׄ4D梔I,a3\pwsH?wWˆ~\ؼqsVmd]q)tœ[ 9NMer!ݘs:_&cQphŐE *6c܎t6~t[ɈrU4>Bjq\frVDLh0brX#њZ&0y NZٌ ȳ(S+S3Y>m*tKtȳAf̱_=G'ە![?r1'z,㬑:<6mmċvv8d/I9GfɁi/l]9Vl"\M(qnf7[7Hz[7f7w\M\Kt3+%B/p#ܸJ&2Mf¹xO٤'WٖC^%[֭%-Z$=]׸yESUS$>"YQgΊiػ/r'}~+`H 8ι'8}Kdǜ<598"d?9JȉS^D68] 6!25ghVwëFbRR"""" "/[4l8wQ5nQ ycٹn wH )W'^b>z/;ޙ8l%Q<)_VNw=>r6bKlYvq!AVooyJe\=¹b猻'B&3`yv*T;QJ^rѶ{clIaO;9r˟hO[X;si]"#WcOv-^"N=Ξ3eZnטt56c.q ΄gL[vCQ#c62{}FƄ7ܜԤB 2U )=e1 fwO-ӑJ;A% IDAT  Hɮ:VþP>7tyb ^ Lv4/Fa97ū`4̒h<;G<ͼ4pX&x>4P7Ǯbӧ|I&^&]BTj3j /40w F;yIhOjWd \]\ћzҶbDu|]'FrRP""""0x.b I޺uOѺׯ|w>+c}Â?e&g)}0!ć߄5k4#((___u&ED䅱fjԨuYf빸tq@l%.pTfkyu0["""""/IdS!KoD<)^x96c^yIk;SМDJȸ-0d;^Ubx5y$p}m *@i9^? j&l9=[M'4Ї+c[̟r{2Co;`$:udi+U(9i"fW ;""" MGIea`Sqz_7|(VmН!?1kÍ=v֪sc~yEu'[VQ)BYS1_/F|3#16Mhas7_ hpl(c1|+ rZũn^D^ .Yq{rDio8#w0Cv֝wa(?z#J&S@wSdO*xk$Ws,`;&:Ln """ "/PpI$dx1sCgSK vDąCq6l[9a 72eoY~oy1:X>v9o6cQphŐEM1iMOxpN2g 츛dn}Nbrͧ ܰEiӯI'V-ylz[AlYNM©o[KR̯]ot:Lbڸ/6ILh] :ql,#Np6*p4X,, |)2+bh,7Dߎ".a b³{S%|*rx:{fm72/8ɍKGYϛao7oJڸ#jgŻ}wjeJ-el?v-} :㲾/^l_kD&(S~O`D(<'3bF[[7"KfYrj%zgc7c~v1m$yK>[W/'[R!Fs属`@u+{dUDDDEҾ; '?CϕrzqDVl1掫) a=hDEn{p[{m "#"{Ohs.2q処 e)D3Ȗ˖sdm8_ŠѢEGބJynnXšK`=X##{<[U;i3t޴'C 9qK]r3XC0+88\Ӑ,d^;@ĶO)2HZ. mdjѬlWf| a8~_>ष^DDDE^$&V{C9C80&N^+67p狔,!pʑr}QT"wɥ;>/s3`lۈ!hDxףnAs .H6-vݸu>4b֤9+y>s&Ej2i?ro=|qyk AKQر9fVۗ ƼL;9rGG{ځ+W,kdcFto.ݶ'{2oˉI`W.1Gt(KbIggd=Ǽ|xm_~8Nr0NfN܈ n_; f,bOyp69f _X(3o3@m'lDR }}1ԙޒ i3Ib.q ΄gL[vCQ#c|8w;a7aΜol h؋.ؙqM6 }gpFӜ>1=OIﻈ(`qrܳ3ˡ)MnON3'FO|ۖz[s玠Z0l mWy,ՇO'XV~LO[ؿgPDt[O!F_pdG2e‰@Z]^] $8Ԥ~?'*! 6yw޽X%Ix}_sګxV.vvRvd.GB0M(j7u[DD͓\WX߯.-&lRir͋6`) IeKl5NMR[7z6w1VB99~Oo$j's뿡]pɌwe|gG7~T"-&aRfl5 |&OzhD)[,Q̑Baz7xiҏ>-aסs){>ʿ3q1 n]L)3+YrvGZ_)4+[8gy`?[/.:f34׀;|cx')ɺ^ì )[Ѝ,YPXTz#[W2n.8gKỉ_;z@#""Bm%r M9Mɯ~g㏃v7jM1l^DGϥ3Y5|9#Sɧ|}?|z_6R|LL,C"\(Xgpˣ3V ~\Ʋ9py[dWL7Q*؝Yʌ Ԝ㪑2~I~'A- >v4=9cWvC%9<4 Y7 ̕y=`_X^辷}۷/1Xw;ʷ-2t , `8:VMKڠ +b_ _{v߹TW߳dzV14 /\W qp;ӥ8Ɍ N d85D2r֚KVzw lTK"""ka rr`l>j&Gp{- Y]oʇϧ뛭q’sq۷p2W{_'%_LБ-/Mj:H*yo Fk?̪L=ʹ'#>{UoN˯-װ/(AH~MNA3Wbߔ8}ޠKT_mU;6']fofȟ&7Tk*exxڭC4)C׹v1]r(SNo},>i{\xFzUD$l88۰lTw(7Qj26ҕuϬ`ň ľG$F93qqb+S#3-=l9ښXw e|W&B7*aoG0?X"""Ooǰ؉(l_/MtIS2l7 J'L]jVT'˶bm; ϫW{j,n Â9_Qdp@iN9(~YĂ>θa>i`= ۴C6Us5ju-s0Ar0xV[p~u6|iSgަT?8~1K:~8>.-n[ 2&U8#Lj ߘg-:E8% UGq(/b{Z㈱\﬌:'r߄019>~N'~+&ßO"w~.et5W&j+6ƎC0 'GǶa`N<nXo$^Jo{LUy=圥"vɞ˓8W;'+lx%xE^٥0: VYm7YnebƊ%0X}WyGEqu7bA"^آ{%^cآKbk41*"|PAD,w;]vgg}{}b <8OGṃÎeSߍs]F3%JI{#Z)!ό/OW+j y(p@>!Ew`S+䎡򣩯1£f=J ֜ fx %r(=~a(a;Q;ǛWqQ_!R) oWc)?'˯oןW΢©`A\ ص/Ƶij=co_TD\mY%j)㓓Gٗ%6jQ(?oT8,o=zյ{CʣȊ[oݏ]@ VTjQ_W3aa{J|y“=_PS UF8e}qSPeB_I@dnRʾ mypu6i_2>Fɪo*Ù=2xZu0?NMV8(Hn5h8ޓĦ2ʚYP=*Z0ej!YS!'/'_8r=\z&;T@3:`x|d#[.Quf$?soo#USQLwwݩkOOrOKEAQTkѝ ;1I^h@a#^޻BD4-a!xDZƥymD˄ߗkQ]d3l-?WjLG,_WsOp؟vClM@)3{5Aօ>Hq98CYUg1}rߋy2H+ gf3*uEp7y΍Ch7/GK~ISKF0[B>ZSE|yZ:3=*_,vGp\3֢ݚSN X\혽qu*y‘E;/OC|5niB%IKkݲ V;\QP̂a"-%,EOTޝ39Ӵr9+eeb((}WC[$2݇r7;=r⿓s92UZs IDAT/څF5kӸ"Q2&a#[smF:2fLEQPKDZTW6ӧaWU )>l gK|D*B%؞ Ѷ4ގE{ߨb SshrgծCo|/*j㍾[ [#[ ] uo=I^L߫u[ 4|ߵ%I~WV$3t|uZq2 p$"'kF@ለ٢k vJ*)g 21kRoQbE!g!/N@ |Ծj4+Y~Xvz@7] "D%d&,%Y 'ܮHIEw@  *A!*'3!g!/Y@ |*B?)U)p jȲ$I>LReV+,EY ?_,p ʕ+˗O<(Ȳ̕+Wrr",ş/ Yy9B@bccCJ,)pKB Bɟ@ЇЇ@C)_<( @i>|H,"$I)O@ЇЇ@CVQ$)KM$H?AU EC !!|j$\ Ip_BBЇ!vGnoe)z8ǘپ9@d}L[:=^H Ԯi fs{1d"17o̖-[>p G4s33\g@TW}˴\>s82ڨ?}6kP1~)&*>rb}RƦǧ<OH|WyCw)W>Ibo4]5Gax-YQ>l"I>EVO3t$ݍ-LqY6o͛Yf͏:gx`Ojѷ9+J+;WU_Kdjsn'^33To^yn1 B~*^K^-077ũ~k/G}& +I[q.I΋3vae;}'ZxjSiw y ']wj&'c>rd|(i qσwwUf:ͧ5g鱇-ͦٻk`S֡؁u3gPƔ=ֱI8n1+-X;46Y#cLj:; g1ITJ녃k ? >æYSiroA ưyaǸbc'nڴM6-[> Y|&e?;kFs2;ԠVhk:OBq)7Bܸ`ISglD~l7JJQljIbK׎!{^dg? =%ܰ>[A\![]aWלؖdunJL2 Q"G1Ŀqj?u )厣569 h>Es((?n8,I)y"gWxT-2A|nWgZ8k+Vo0oe>P aU0]uM@9\NG|sRgᴯ_JUѺxƵFYdEٙ|S{[rCGIKiǘ7`|j}ŽRXCsV{u&@3U%=pk֬a͚5IF;@t%j 49aɂ^n-֍G&&d?=M <~ q̢wF7)~n悔)9c%r?:#{qp[ _PN?zZ :b,a34]91 „y+e!e`B 1ٚ6T9XNy5$j&:{T qcگ9[ @#ǟDU:z;pN=U%L7G:x,skxmqL؝L=^#'7ֆ[r݊cN% uokj俗ӾN/NKZ6cgH,`0菮g  ,##H$muɞiĉwωAV@_ ͡`B|\YL5Z'|ƕ:ɡh휌HYVk׮eڵozjdY&444=J`H;ܟv_YS TPc#n|g:ʮ*Xz EJJ2Hy(RFX:70~a^4с=Cŝi?FZ%γjvWNak@!q`|E  KβuFѬ*Cx{|,vhRTFʔt"<Sg2l"K3vty\לQUCAGp 'z-̘s"O1*ތuөr-L4?=z#5e)Q ?-ǢL{{3iզ9 c 2 FdˋI {JߋY~}^\+-Gb~Zer|Mt"kU` p3KDZvnǡK OAXٳ cA@ T̸ID; 5 ZGU$KzmX2e9Obנh%t"co7jAFAGj/eV'ר(-/F^jxHfGğV~gӸvƂ%tu㯊GũX. `z}}ȼ>L P$_n\U|!>N􅀉Wyx%Q~\Mc4`J8-31*n(oYRzhT-UI~ k?lHЃ0A@QLO"m^F@1kr5JbWpžsl1}{gX<(@ϾGy0⸑-6}# ->ftE|RYKZCq 'ok=squlv5| 'JZaٮ>'"  +2Rʙc f7޿[:3cq+p^:3K蘤% ӶI)X ?s ]i|FM9P= `5lؐ f()Mqn,Zĉ\-i-?'ٵxt29č#tqF>ԣ(Fʝh$::nHgCwOlp |Q?2!oNTf*ɪLYJt77OI#Ip (8|ϞuphiWAWGJ|"cJ^"w,sJ#6߂_Oڤڼik fGZ*!>1h 2wG8#R\Iiǩ*[ꜾX<bu|ηe1gI:N3nΐҍh>.Gc뀪u>r}1-sr7HelshHZ'U/q =QA)aTyÑT:E{Pci&s(MEGW Q|Xy ȭ %_7BIXd;Mɛyox;|=fh4E(f }\jV\Y/t|Ar``[3- `0Ʌ;Oh4(#ܥTpg9 R'&S8_kx>HO/Oi, H`riM* 0܌oނsfⷒD8"o?]~8RU;EEɴ`o__߉6 6 w!::#G7[$!$:Ԓ+sC("IJITxdf~XX ˾_eVEwq%{3WվAQ%G<,N$viuPd{fh؅uRmC=? f&uJ()Ʃn Uuaxn^BFw\iR QK}&it,~߶uXW9EpdEgA*j l=^io@OOm[.?f~M M_ ΅ 7oU5rc8v17`L#x)Eƒ/cΜC6GkG/>snej.VLaVrbl?0pFTi| ռQނ{2kw/blYx b\}(78 oޓz3`#?5NLn @:=LՍ_8(Nm{ۘ9%'fCug+FĪ|gHI6>*xZF9{ZڄͧҪLfCصhI9ˢݨDόfT2sF-G{_YRxJ[7] R}tBb9﷨2-J[]RmHꉺɉC%GenXC/({9CY(d3$&K<>䕶j `ˍpw@وvҫ4Y>c%7F̑J.\6f/!cX̪M>W;% IDATU'ֹ.ymUD˷TZOXZvHdd{$׬ݟ3RvacL3lH*om:Яjы1q /"F6fj*U*" D$nASYxJd5%'_*\Lf_;c4ع䧼DM2Gu :X8?嬾@auP .OY‹N7b>zveX9(lmn&uBY3{=aQ*l}5E5C!9RWo~13B:kfM{)#hStjsl\횴%#4aB<;Yy__9aȆr:p4͡<1u$g3 ԞZ#1+F=eSmC`cj| غT[~ߗN2kjk2u2BAu#Wjx Jc2ZcB2'kF@ለ"?N?o_˛:s>y٩T*ODG|I,Ї/]zΎ ƶl;8BQۇh%C.|\r%E%_>T =|l!~sjwMhJW !qqqLm='Kj5:O ˻-ie.+W#]|ؘe'{CEIW !YDXX@ЇЇ@CHiذ@0s#?#S!,lvR[C :RK$@ _\OzxI3'@ XD.Ϟ# qo_V[,K6 A@$@ |΄=~J^YAE<+QdE   3cPd!ց@ " N"22cccCbńq($cЁ@ " P:uJN)|fCaMV&W((:̪ yRm ]K{5\1 `AA2TB牍[۝;we9MwOlgǹ'|i)&c3 9 kgUT^WE˚>?l'+v̇o_dS*;7 'p,+&f j͑/`,j淠_Hglُ$Xx1~;!0W^Lng14iŝ]>'їUQ 3o0 WY;f8~e$$S9hR fU@I da F}RQE1$$ɄĠu@U{57Ěmp2_W+D6 ǹW ≯:a1 Ų /nLƕN;_?sZw7y4 ;dnLyp1XMbޝps׿l^j-ưbt%"WOfi8noNyq4C@I Ow4G%IHuaxAZlURj4]XfWx0fYQ\|]45ޫ<< dLa1ҕ5S3,Z "vGpA__y_E23{Cᬏ2 EFeeNsOc'1+xSX沦 <6N܎vߍ- hdƕX,_~.Yx`AA-l[L 71V)|cij#ҫ|hF*x;bnb7!c'\qxڛH}L! gbRԚajNz39eC1ԙS/'x?pΣ uNn1sQN2w4ͦd@(Ujv`Zԍi܂e^4Y]21q6!;#}H27sh܋/' &t6MR gbbWmVbK:xJ^<@o" QP|〘I@ b¨W'ONz/ ޥK?U]ŋg]N áݽg!Y{Kwd )!qE.^<͌:EPޝWs!!:/9uRB`#gX]z1_Y>:Nq);[ G.f˾C^ie?eoY[}whcv<1.%#T<U}{hNr(JSely|0ĪwԺw# T';t6d8ʨhF酧}CNdaD+ǛRUsq<~A:Œ}/[20#+ExbC^YO] ٳ֭[cggVXS}SϒT._R,v/Epʭ yc[BPQ&U+XR)10~A )\ 3 Lj_U}?CgU9 "lCjOZ~cu[ygtJ屝7 'sv[+<_ʧs%~CpXMxx떰T7WdbWZ _'ǝlqv|"]DQ9J?R3xGNv~Q$;E!LZ$CHؙPɱ=!S ➿`ﱔ-L{meJ=Krt=Y鲦ܦ߾~CS7*I7թb9ǎ~e% U?E0%S+WԯliO8 \~8iW0b2{g"NUb#pvuJVQa~C-ΚdyNˆL2}֘jU7 2;G9+6d/m{O WU,)g#j:$7D:NSgO?IWy%[d M"ȍ}:;it@ l`̘1<aÆ}Uc '81R" %mKioN<t,iUz5?8s1gLFAcWFpﭻAKp{_P_70]yռPpK4(Elj(;,hb>j_#ʲ(inVg'|/E= T^~ kpڎbYrES ݧeeU2x=EINLhP*s&GO4 b_]aY=?[ eyۦ馹=XڡË SLI/fMsR|d蹷*5J)[Y1M }2O̟KhY++.f@RmDo7qﬞeAmso4b;LozS,F?#}r!wDHAӪU+)œ9shڴY!31z8#[:JB*$Tѡ{eFF`0 !^!1.PP5Z-ԹsKh._V!.o;ytk'-E$[wq}v{֠Q|,))*o;z-YVKsv-h8'<;sdӚQu wڔDVcel񣏒U{}xN:ãaL#9$"Wή8I<K<|uq4Κ܁]M>C\WΥؽ!p'/ TQǟsg9n秃O41ƂD*3'U5@8_;*ë=-\ I@ r#W6mZϳ nG`/!I*09ڱ\:8w $W8.t]\P*D |)SΝ;w^p('E Z3zX˔xu-+u_0Jjs|>gPjuyU݄ nbW+z՟JA^Ӈ U*ZTNޭTg * j[4=s`%?ę coPUJ+ұ&=<92~.XP[WLci;2^av I{*c\{ca|_Gl>s_[]$V4 K?>~*dL(8w.3c釧߳A`(3櫜_aF3hE4Ǔ\ pMS蟸Jyk׭sz4`R {wrVK΄^ש&pxY:SB[u~qcȥ Eb[J nGE o]B4q43v_Լuvmwgy8 zZ&RU R"JRThB3ҫ"B μ )D}]law93 nޕiѱaY-yĎ_n7IF]VSKĚNC.Ӓ`x'HK`]uVg]Wpnvxb$=x : _w!vLTu TG(-A 3BI:gvdҺt \ ܸ'3gP>~ũwkUbYNfTF3%\3[;m*}EPX-H[Tl=k?SF- pУ $ޘN&cGHl!@2BIIb\&(슖X:"Q6xTK5|6cg|,wmZX'^s.TF7-& h.!0v}cLj-yyͣGU"ԭ-~QVݿ"*٨$T=qi8B!7oq"֌~s+χЬù.Hw 04"-BCyi!d#ZB! ::={כw{0IBȎxR#a!'''*UyCF#r BaQJ 8?kr -!I'!\!D~i/UB%)!…B^y:EZefX,'iZ !…BlŘ^.{Gfx;Ӵ|I)3C!M$'ɠQ2,6>Ёȑ#2̕œVf?M]a}r>|T9󟦡IKŗc%f33!ϳe*n31m]Qd !0!(ruuͷ\zUU.NoHvS/bV=5_Ͳ=W1*[.0bIDAT2AдchZJCrˬe5)Qg"$Ҥ캜p뙋lvB!$ !h4 /PvmRRR2g逞LT§F-z;~5J/xeemqIQCLʦ~8%GїbX9g O֡&NػWqWZoiŋGW>_i=Wۅ9=7{`Xyhɦ9Pv7_fC'ؙ|O ?_vK૞?U0f?řkP[p+_*%WR.G`gM C \ sdDxr? M+`I'Ŕ'.GⅭL%UJ8`X-0H2k? ֿݗUK`k{&~g'챮=#󕐐K/f .ds%._liފB<[Μ9㱵b7gL[Ǩb 揧z9jXglt/._q6| ҜZ 3<<{'x#-Q'ea4U{o&/ ĮQø:;=1-:9Í]{(SŝK&kc5@O;?O&zoǎM=X32@"5ܨ|3mŔzI-=|z?, `u^`]++CdsF'd2neLq>rļY3>~ny}g~Zf]\tݧ\ݴϘQ7fu#Z;z-OdCbo&BY9I} T8~%[yj\ۿɳ3IvBa I}AhѢEgs+χhytXj7O{d(JݒWf|竷saFzvRID܌GW>E`oU1[.+VqF"g2KKMWO[+*쨦p5}'rA~IЌ\KSkzZsN*w[[!8,3iCWt5哖v|^!tQz}¦?)4\M`2xc^y+z~̏Z?Fޤ(jSi4kߕu7{vft9B'뮧S|˟;Eعy k[Xd\yl#Sp3GfvkDnDy.pkkk:ځ={a0v(Dm*SsUKĦlR/?dRpZOiIlŎtGsrr",,{/QL[Ӄm*ԩ[^Ŗoӌ.`Cc i7` ݫcC7!Dj)/ԟ;:ڹ2Of [ҎNafrۉlf$u̜TQWÞ}ijHտ)Y7ٰn p{'KV^0|a]]X[|e<2&s×3jBū[:WHM-<`І14([MuwMj2}F;=FFTjR·q4P!fBf^T'6De}G#2-]_ޤ7AMѵ3ЁVv3Ь~}H+u i*w|^]4>^U/o|Ag6 дV``n ,=)op6*>>HlNuyH*iҒdzt)DU*c "4wyl`tTA `ρQ#1+>* fHiMG9gIќh4a+Kf}nsPτokpZ溄p!D̙32dSNM6YcW+l˖ǓDDPE8-bU2Bx{᫣GJzdIخ峓s)EoOOycEt@: c[+֬avؤvnbŝ?6f`ZP .DZC#0[|kOtn#O^@СHf!ٛlY)kAcvj%%һY6!hhja"ӔbqPIKoA{κE8($8/zVRx# ܘƓ"fSeA^}'yy?JgupykW]DRϺq owĘu9{(_ 똙mTP~J,iH".j/ .i5n*I'Nu3'M_9kżz(Ħjt{?:}O3Z#3Ѐsɪq I fLS> h?> D17h |}}Yl/J!vllHQqFQ|:ʦ{f4Q2bgRVס`WS 6:{ߩ/K qQPl8oc;]V;?~deL* gҢ>8/io8YќIwrNlW7B^ kݝ2]9o5$pС3y-@w7 Uݽuv:DXo#wSP j촵vm33ư+&X粖99 VܽkkP1涯jSͧ80"2v=p5^:6n4mÐt E}:E\]-Zrc2#q.1-MK뉏LQnD"i=Jܽ%GI_}“0(bTAXpX@#>m2Ә+k5ŒiEúV\?UjRRç6jؐ0Dpڴs/{=c1DԐ>w^9+};<4m מPtojv3Nw^C8^AQt`4`0j+cÉӜ#Q6M]E n=zУG.tGкukF}Xp]w2J[HA?@wYLۄ}.ocv9Zt6?2- cXP^s{}1oPqROjp*Ƥջ_k]Oqf6aXQ|7>”ձiߓTu:&oL/T<]״? P3o ?FOygp+[Ț3ʻ/\JF`&߼`fDNgn7MT wGWMt'=Å%*(SËގ|qITMyM঎ܽ7;ⲟLΘ6)*ͻ\l3c؈+]~hvٗMaXz_"F.|Ϣ1sh%NKTvygs0kKVm27·0øu[Mg7tT3Ւ(6%^7Iol$ Os>e̵ ,4hg󀟧ਞg9U ۾'(b>Ji mZq|&[m2АT \xk@ZO5Qٲqҫ/b[LZ<.cHsn|ѻ4;S]]<FlBi&tiZޙmΟ@zqmZ 1oݓw)5$h7wX֗KĚNC.Ӓ`x'HK`]uVg]Wpnvxb$=x : _w!vLTu TG(-A 3BI:gvdҺt ܟ^-Lq}? ^s»Vg&F'o@_q~2s wQJQ~GV/5d6oLe;x~]5:3SF6*}EPX-H[Tl=k?#[uKvx '%&qgW(슖X:"Q6xTK5|6?e? KOߺ@0nZM066YB<aaat?cǎ[򺻻#}PuDYUCSulT Yʞ۸r !՞={h߾}7-Bf#߁4`RL Q xiZ'h!OBxPORb%>@!x26xڃp!x (i̇5̸2B!K*bq 7.c…xd&!BƄwL}!c !B![fl]>/2q!B!Bu%tlB!B!Dax~x!B!B<@i B!B!Z~A\K$ !B!:ߗuy$t-[!B!B (B!B!;uu B!B!DCx& !B! ~_.A\!B!x<59[s{QB!B[%\ !B!N:1j"B!BЅ.B!B-g6 Yɯ;z .B!B[QT[!B!B*Y!o .B!B<8>6\MnMK\^י֜Z% !B!D-<wnٺ[%yc J nnt!B!Rܜ 3͸psN̖Cƃ !B!(Z!n F<1U/R͜º{u=<-\!B!ģ yUnn;:fwaƁKB!B8𼲯9tE+jN[;M!B!szM>!< ^c܂KWb~ o!B!S /lG3crb܎ߚmĕ|·R-\!B!%wAaܜi:K#2IENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3776052 pymeasure-0.14.0/examples/0000755000175100001770000000000014623331176015037 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/.gitignore0000644000175100001770000000021514623331163017021 0ustar00runnerdocker# Ignore generated files /Notebook Experiments/procedures.py /Notebook Experiments/analysis.py /Notebook Experiments/my_config.ini /**/*.csv ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3776052 pymeasure-0.14.0/examples/Basic/0000755000175100001770000000000014623331176016060 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/console.py0000644000175100001770000000545314623331163020077 0ustar00runnerdocker""" This example demonstrates how to run an experiment both with graphical interface or with a console mode. If the script run without any parameter, the GUI version is displayed, otherwise the console mode is run. It uses a random number generator to simulate data so that it does not require an instrument to use. Run the program by changing to the directory containing this file and calling: python console.py #GUI version python console.py --seed 12345 #Console version """ import sys import random import tempfile from time import sleep from pymeasure.experiment import Procedure, IntegerParameter, Parameter, FloatParameter from pymeasure.experiment import Results from pymeasure.display.console import ManagedConsole from pymeasure.display.Qt import QtWidgets from pymeasure.display.windows import ManagedWindow import logging log = logging.getLogger('') log.addHandler(logging.NullHandler()) class TestProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=100) 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 up random number generator") random.seed(self.seed) def execute(self): log.info("Starting to generate numbers") for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } log.debug("Produced numbers: %s" % data) self.emit('results', data) self.emit('progress', 100 * (i + 1) / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Catch stop command in procedure") break def shutdown(self): log.info("Finished") 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' ) 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__": if len(sys.argv) > 1: # If any parameter is passed, the console mode is run # This criteria can be changed at user discretion app = ManagedConsole(procedure_class=TestProcedure) else: app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/gui.py0000644000175100001770000000620214623331163017212 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ This example demonstrates how to make a graphical interface, and uses a random number generator to simulate data so that it does not require an instrument to use. Run the program by changing to the directory containing this file and calling: python gui.py """ import sys import random from time import sleep from pymeasure.experiment import Procedure, IntegerParameter, Parameter, FloatParameter from pymeasure.display.Qt import QtWidgets from pymeasure.display.windows import ManagedWindow import logging log = logging.getLogger('') log.addHandler(logging.NullHandler()) class TestProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=100) 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 up random number generator") random.seed(self.seed) def execute(self): log.info("Starting to generate numbers") for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } log.debug("Produced numbers: %s" % data) self.emit('results', data) self.emit('progress', 100 * i / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Catch stop command in procedure") break def shutdown(self): log.info("Finished") class MainWindow(ManagedWindow): def __init__(self): super().__init__( procedure_class=TestProcedure, inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], x_axis='Iteration', y_axis='Random Number' ) self.setWindowTitle('GUI Example') if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/gui_custom_inputs.py0000644000175100001770000000744014623331163022213 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ This example demonstrates how to make a graphical interface with custom inputs, and uses a random number generator to simulate data so that it does not require an instrument to use. The gui_custom_inputs.ui file is loaded, which allows for the custom inputs interface. Run the program by changing to the directory containing this file and calling: python gui_custom_inputs.py """ import sys import random import tempfile from time import sleep from pymeasure.experiment import Procedure, IntegerParameter, Parameter, FloatParameter from pymeasure.experiment import Results from pymeasure.display.Qt import QtWidgets, fromUi from pymeasure.display.windows import ManagedWindow import logging log = logging.getLogger('') log.addHandler(logging.NullHandler()) class TestProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=100) 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 up random number generator") random.seed(self.seed) def execute(self): log.info("Starting to generate numbers") for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } log.debug("Produced numbers: %s" % data) self.emit('results', data) self.emit('progress', 100 * i / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Catch stop command in procedure") break def shutdown(self): log.info("Finished") class MainWindow(ManagedWindow): def __init__(self): super().__init__( procedure_class=TestProcedure, displays=['iterations', 'delay', 'seed'], x_axis='Iteration', y_axis='Random Number' ) self.setWindowTitle('GUI Example') def _setup_ui(self): super()._setup_ui() self.inputs.hide() self.inputs = fromUi('gui_custom_inputs.ui') def queue(self): filename = tempfile.mktemp() procedure = TestProcedure() procedure.seed = str(self.inputs.seed.text()) procedure.iterations = self.inputs.iterations.value() procedure.delay = self.inputs.delay.value() results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/gui_custom_inputs.ui0000644000175100001770000000310714623331163022174 0ustar00runnerdocker Form 0 0 450 177 Form Seed: 12345 Iterations: 100000 200 Delay: sec 3 0.100000000000000 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/gui_estimator.py0000644000175100001770000001201314623331163021276 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ This example demonstrates how to make a graphical interface, and uses a random number generator to simulate data so that it does not require an instrument to use. Run the program by changing to the directory containing this file and calling: python gui.py """ import sys import random from time import sleep from datetime import datetime, timedelta from pymeasure.experiment import Procedure, IntegerParameter, Parameter, FloatParameter from pymeasure.display.Qt import QtWidgets from pymeasure.display.windows import ManagedWindow import logging log = logging.getLogger('') log.addHandler(logging.NullHandler()) class TestProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=100) 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 up random number generator") random.seed(self.seed) def execute(self): log.info("Starting to generate numbers") for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } log.debug("Produced numbers: %s" % data) self.emit('results', data) self.emit('progress', 100 * i / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Catch stop command in procedure") break def get_estimates(self, sequence_length=None, sequence=None): """ Function that returns estimates for the EstimatorWidget. If this function is implemented (and does not return a NotImplementedError) the widget is automatically activated. The function is expected to return an int or float, or a list of tuples. If an int or float is returned, it should represent the duration in seconds.If a list of tuples is returned, each tuple containing two strings, a label and the estimate itself: estimates = [ ("label 1", "estimate 1"), ("label 2", "estimate 2"), ] The length of the number of estimates is not limited but has to remain unchanged after initialisation. Note that also the label can be altered after initialisation. The keyword arguments `sequence_length` and `sequence` are optional and return (if asked for) the length of the current sequence (of the `SequencerWidget`) or the full sequence. """ duration = self.iterations * self.delay """ A simple implementation of the get_estimates function immediately returns the duration in seconds. """ # return duration estimates = list() estimates.append(("Duration", "%d s" % int(duration))) estimates.append(("Number of lines", "%d" % int(self.iterations))) estimates.append(("Sequence length", str(sequence_length))) estimates.append(('Measurement finished at', str(datetime.now() + timedelta( seconds=duration))[:-7])) estimates.append(('Sequence finished at', str(datetime.now() + timedelta( seconds=duration * sequence_length))[:-7])) return estimates def shutdown(self): log.info("Finished") class MainWindow(ManagedWindow): def __init__(self): super().__init__( procedure_class=TestProcedure, inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], x_axis='Iteration', y_axis='Random Number', sequencer=True, sequence_file="gui_sequencer_example_sequence.txt" ) self.setWindowTitle('GUI Example') if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/gui_foreign_instrument.py0000644000175100001770000000771314623331163023223 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ This example demonstrates how to use an instrument from another library, here InstrumentKit, in the pymeasure graphical interface. It is a modification of the `gui.py` example: The `MainWindow` is the same, the `TestProcedure` was adjusted to use an instrumentKit instrument instead of a random number generator. Run the program by changing to the directory containing this file and calling: python gui_foreign_instrument.py """ import sys import random from time import sleep from pymeasure.experiment import Procedure, IntegerParameter, FloatParameter from pymeasure.display.Qt import QtWidgets from pymeasure.display.windows import ManagedWindow # Import the InstrumentKit package from instruments.thorlabs.pm100usb import PM100USB import instruments.units as u # For simulating communication from io import BytesIO import logging log = logging.getLogger('') log.addHandler(logging.NullHandler()) class TestProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=10, maximum=100) delay = FloatParameter('Delay Time', units='s', default=0.2) DATA_COLUMNS = ['Iteration', 'Power (W)'] def startup(self): log.info("Setting up the power meter") # Open the test connection to the powermeter with some sample responses responses = ["POW"] responses.extend(f"{random.random()}" for i in range(100)) communication = b"\n".join(item.encode() for item in responses) self.powermeter = PM100USB.open_test(BytesIO(communication)) self.powermeter.cache_units = True # In order to connect to an actual device at COM Port 5, use instead: # self.powermeter = PM100USB.open_serial("COM5") def execute(self): log.info("Starting to measure the laser power") for i in range(self.iterations): data = { 'Iteration': i, # Read the powermeter and store the sensor reading in Watts. 'Power (W)': self.powermeter.read().m_as(u.W), } log.debug("Produced numbers: %s" % data) self.emit('results', data) self.emit('progress', 100 * i / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Catch stop command in procedure") break def shutdown(self): log.info("Finished") class MainWindow(ManagedWindow): def __init__(self): super().__init__( procedure_class=TestProcedure, inputs=['iterations', 'delay'], displays=['iterations', 'delay'], x_axis='Iteration', y_axis='Power (W)', ) self.setWindowTitle('GUI Example for Foreign Instrument') if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/gui_sequencer.py0000644000175100001770000000661614623331163021275 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ This example demonstrates how to make a graphical interface, and uses a random number generator to simulate data so that it does not require an instrument to use. It also demonstrates the use of the sequencer module. Run the program by changing to the directory containing this file and calling: python gui_sequencer.py """ import sys import random from time import sleep from pymeasure.experiment import Procedure, IntegerParameter, Parameter, \ FloatParameter from pymeasure.display.Qt import QtWidgets from pymeasure.display.windows import ManagedWindow import logging log = logging.getLogger('') log.addHandler(logging.NullHandler()) class TestProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=100) 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 up random number generator") random.seed(int(self.seed)) def execute(self): log.info("Starting to generate numbers") for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } log.debug("Produced numbers: %s" % data) self.emit('results', data) self.emit('progress', 100 * i / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Catch stop command in procedure") break def shutdown(self): log.info("Finished") class MainWindow(ManagedWindow): def __init__(self): super().__init__( procedure_class=TestProcedure, inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], x_axis='Iteration', y_axis='Random Number', sequencer=True, sequencer_inputs=['iterations', 'delay', 'seed'], sequence_file="gui_sequencer_example_sequence.txt", inputs_in_scrollarea=True ) self.setWindowTitle('GUI Example') if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/gui_sequencer_example_sequence.txt0000644000175100001770000000023614623331163025057 0ustar00runnerdocker- "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)" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/gui_table.py0000644000175100001770000001004214623331163020356 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ This example demonstrates how to make a graphical interface, and uses a random number generator to simulate data so that it does not require an instrument to use. In particular, this example shows how to display data in tabular format. Run the program by changing to the directory containing this file and calling: python gui_table.py """ import sys import random from time import sleep from pymeasure.experiment import Procedure, IntegerParameter, Parameter, FloatParameter from pymeasure.experiment import Results, unique_filename from pymeasure.display.Qt import QtWidgets from pymeasure.display.windows import ManagedWindowBase from pymeasure.display.widgets import TableWidget, LogWidget import logging log = logging.getLogger('') log.addHandler(logging.NullHandler()) class TestProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=10) 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 up random number generator") random.seed(self.seed) def execute(self): log.info("Starting to generate numbers") for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } log.debug("Produced numbers: %s" % data) self.emit('results', data) self.emit('progress', 100 * i / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Catch stop command in procedure") break def shutdown(self): log.info("Finished") class MainWindow(ManagedWindowBase): def __init__(self): widget_list = (TableWidget("Experiment Table", TestProcedure.DATA_COLUMNS, by_column=True, ), LogWidget("Experiment Log"), ) super().__init__( procedure_class=TestProcedure, inputs=['iterations', 'delay', 'seed'], displays=['iterations', 'delay', 'seed'], widget_list=widget_list, enable_file_input=False, ) logging.getLogger().addHandler(widget_list[1].handler) log.setLevel(self.log_level) log.info("ManagedWindow connected to logging") self.setWindowTitle('GUI Example') def queue(self): direc = '.' filename = unique_filename(direc, 'gui_table') procedure = self.make_procedure() results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/image_gui.py0000644000175100001770000001130214623331163020351 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ This example demonstrates how to make a graphical interface which contains an image plotting tab, and uses a random number generator to simulate data so that it does not require an instrument to use. Run the program by changing to the directory containing this file and calling: python image_gui.py """ from time import sleep import sys import numpy as np from pymeasure.experiment import Results, unique_filename from pymeasure.experiment import Procedure from pymeasure.display.windows import ManagedImageWindow # new ManagedWindow class from pymeasure.experiment import FloatParameter from pymeasure.display.Qt import QtWidgets import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class TestImageProcedure(Procedure): # We will be using X and Y as coordinates for our images. We must have # parameters called X_start, X_end and X_step and similarly for Y. X and # Y can be replaced with other names, but the suffixes must remain. X_start = FloatParameter("X Start Position", units="m", default=0.) X_end = FloatParameter("X End Position", units="m", default=2.) X_step = FloatParameter("X Scan Step Size", units="m", default=0.1) Y_start = FloatParameter("Y Start Position", units="m", default=-1.) Y_end = FloatParameter("Y End Position", units="m", default=1.) Y_step = FloatParameter("Y Scan Step Size", units="m", default=0.1) delay = FloatParameter("Delay", units="s", default=0.01) # There must be two special data columns which correspond to the two things # which will act as coordinates for our image. If X and Y are changed # in the parameter names, their names must change in DATA_COLUMNS as well. DATA_COLUMNS = ["X", "Y", "pixel_data"] def startup(self): log.info("starting up...") def execute(self): xs = np.arange(self.X_start, self.X_end, self.X_step) ys = np.arange(self.Y_start, self.Y_end, self.Y_step) nprog = xs.size * ys.size progit = 0 for x in xs: for y in ys: self.emit('progress', int(100 * progit / nprog)) progit += 1 self.emit("results", { 'X': x, 'Y': y, 'pixel_data': np.random.rand(1)[0] }) sleep(self.delay) if self.should_stop(): break if self.should_stop(): break def shutdown(self): log.info('shutting down') class TestImageGUI(ManagedImageWindow): def __init__(self): # Note the new z axis. This can be changed in the GUI. the X and Y axes # must be the DATA_COLUMNS corresponding to our special parameters. super().__init__( procedure_class=TestImageProcedure, x_axis='X', y_axis='Y', z_axis='pixel_data', inputs=['X_start', 'X_end', 'X_step', 'Y_start', 'Y_end', 'Y_step', 'delay'], displays=['X_start', 'X_end', 'Y_start', 'Y_end', 'delay'], filename_input=False, directory_input=False, ) self.setWindowTitle('PyMeasure Image Test') def queue(self): direc = '.' filename = unique_filename(direc, 'test') procedure = self.make_procedure() results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = TestImageGUI() window.show() sys.exit(app.exec()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/script.py0000644000175100001770000000664214623331163017742 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ This example demonstrates how to make a simple command line interface, and uses a random number generator to simulate data so that it does not require an instrument to use. Run the program by changing to the directory containing this file and calling: python script.py """ import random import tempfile from time import sleep from pymeasure.log import console_log from pymeasure.experiment import Procedure, IntegerParameter, Parameter, FloatParameter from pymeasure.experiment import Results, Worker import logging log = logging.getLogger('') log.addHandler(logging.NullHandler()) class TestProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=100) 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 up random number generator") random.seed(self.seed) def execute(self): log.info("Starting to generate numbers") for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } log.debug("Produced numbers: %s" % data) self.emit('results', data) self.emit('progress', 100. * i / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Catch stop command in procedure") break def shutdown(self): log.info("Finished") if __name__ == "__main__": scribe = console_log(log, level=logging.DEBUG) scribe.start() filename = tempfile.mktemp() log.info("Using data file: %s" % filename) procedure = TestProcedure() procedure.iterations = 1000 procedure.delay = 0.01 log.info("Set up TestProcedure with %d iterations" % procedure.iterations) results = Results(procedure, filename) log.info("Set up Results") worker = Worker(results, scribe.queue, log_level=logging.DEBUG) log.info("Created worker for TestProcedure") log.info("Starting worker...") worker.start() log.info("Joining with the worker in at most 20 min") worker.join(60 * 20) log.info("Worker has joined") log.info("Stopping the logging") scribe.stop() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Basic/script_plotter.py0000644000175100001770000000714714623331163021514 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ This example demonstrates how to make a command line interface with a live-plotting interface, and uses a random number generator to simulate data so that it does not require an instrument to use. Run the program by changing to the directory containing this file and calling: python script_plotter.py """ import random import tempfile from time import sleep from pymeasure.log import console_log from pymeasure.experiment import Procedure, IntegerParameter, Parameter, FloatParameter from pymeasure.experiment import Results, Worker from pymeasure.display import Plotter import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class TestProcedure(Procedure): iterations = IntegerParameter('Loop Iterations', default=100) 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 up random number generator") random.seed(self.seed) def execute(self): log.info("Starting to generate numbers") for i in range(self.iterations): data = { 'Iteration': i, 'Random Number': random.random() } log.debug("Produced numbers: %s" % data) self.emit('results', data) self.emit('progress', 100. * i / self.iterations) sleep(self.delay) if self.should_stop(): log.warning("Catch stop command in procedure") break def shutdown(self): log.info("Finished") if __name__ == "__main__": scribe = console_log(log, level=logging.DEBUG) scribe.start() filename = tempfile.mktemp() log.info("Using data file: %s" % filename) procedure = TestProcedure() procedure.iterations = 200 procedure.delay = 0.1 log.info("Set up TestProcedure with %d iterations" % procedure.iterations) results = Results(procedure, filename) log.info("Set up Results") plotter = Plotter(results) plotter.start() worker = Worker(results, scribe.queue, log_level=logging.DEBUG) log.info("Created worker for TestProcedure") log.info("Starting worker...") worker.start() log.info("Joining with the worker in at most 20 min") worker.join(60 * 20) log.info("Waiting for Plotter to close") plotter.wait_for_close() log.info("Plotter closed") log.info("Stopping the logging") scribe.stop() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3776052 pymeasure-0.14.0/examples/Current-Voltage Measurements/0000755000175100001770000000000014623331176022511 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Current-Voltage Measurements/iv_keithley.py0000644000175100001770000001250114623331163025372 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2023 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. # """ This example demonstrates how to make a graphical interface to preform IV characteristic measurements. There are a two items that need to be changed for your system: 1) Correct the GPIB addresses in IVProcedure.startup for your instruments 2) Correct the directory to save files in MainWindow.queue Run the program by changing to the directory containing this file and calling: python iv_keithley.py """ import logging import sys from time import sleep import numpy as np from pymeasure.instruments.keithley import Keithley2000, Keithley2400 from pymeasure.display.Qt import QtWidgets from pymeasure.display.windows import ManagedWindow from pymeasure.experiment import ( Procedure, FloatParameter, unique_filename, Results ) log = logging.getLogger('') log.addHandler(logging.NullHandler()) class IVProcedure(Procedure): max_current = FloatParameter('Maximum Current', units='mA', default=10) min_current = FloatParameter('Minimum Current', units='mA', default=-10) current_step = FloatParameter('Current Step', units='mA', default=0.1) delay = FloatParameter('Delay Time', units='ms', default=20) voltage_range = FloatParameter('Voltage Range', units='V', default=10) DATA_COLUMNS = ['Current (A)', 'Voltage (V)', 'Resistance (Ohm)'] def startup(self): log.info("Setting up instruments") self.meter = Keithley2000("GPIB::25") self.meter.measure_voltage() self.meter.voltage_range = self.voltage_range self.meter.voltage_nplc = 1 # Integration constant to Medium self.source = Keithley2400("GPIB::1") self.source.apply_current() self.source.source_current_range = self.max_current * 1e-3 # A self.source.compliance_voltage = self.voltage_range self.source.enable_source() sleep(2) def execute(self): currents_up = np.arange(self.min_current, self.max_current, self.current_step) currents_down = np.arange(self.max_current, self.min_current, -self.current_step) currents = np.concatenate((currents_up, currents_down)) # Include the reverse currents *= 1e-3 # to mA from A steps = len(currents) log.info("Starting to sweep through current") for i, current in enumerate(currents): log.debug("Measuring current: %g mA" % current) self.source.source_current = current # Or use self.source.ramp_to_current(current, delay=0.1) sleep(self.delay * 1e-3) voltage = self.meter.voltage if abs(current) <= 1e-10: resistance = np.nan else: resistance = voltage / current data = { 'Current (A)': current, 'Voltage (V)': voltage, 'Resistance (Ohm)': resistance } self.emit('results', data) self.emit('progress', 100. * i / steps) if self.should_stop(): log.warning("Catch stop command in procedure") break def shutdown(self): self.source.shutdown() log.info("Finished") class MainWindow(ManagedWindow): def __init__(self): super().__init__( procedure_class=IVProcedure, inputs=[ 'max_current', 'min_current', 'current_step', 'delay', 'voltage_range' ], displays=[ 'max_current', 'min_current', 'current_step', 'delay', 'voltage_range' ], x_axis='Current (A)', y_axis='Voltage (V)' ) self.setWindowTitle('IV Measurement') def queue(self): directory = "./" # Change this to the desired directory filename = unique_filename(directory, prefix='IV') procedure = self.make_procedure() results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Current-Voltage Measurements/iv_yokogawa.py0000644000175100001770000001255314623331163025404 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2023 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. # """ This example demonstrates how to make a graphical interface to preform IV characteristic measurements. There are a two items that need to be changed for your system: 1) Correct the GPIB addresses in IVProcedure.startup for your instruments 2) Correct the directory to save files in MainWindow.queue Run the program by changing to the directory containing this file and calling: python iv_yokogawa.py """ import sys from time import sleep import numpy as np from pymeasure.instruments.keithley import Keithley2000 from pymeasure.instruments.yokogawa import Yokogawa7651 from pymeasure.display.Qt import QtWidgets from pymeasure.display.windows import ManagedWindow from pymeasure.experiment import ( Procedure, FloatParameter, unique_filename, Results ) import logging log = logging.getLogger('') log.addHandler(logging.NullHandler()) class IVProcedure(Procedure): max_current = FloatParameter('Maximum Current', units='mA', default=10) min_current = FloatParameter('Minimum Current', units='mA', default=-10) current_step = FloatParameter('Current Step', units='mA', default=0.1) delay = FloatParameter('Delay Time', units='ms', default=20) voltage_range = FloatParameter('Voltage Range', units='V', default=10) DATA_COLUMNS = ['Current (A)', 'Voltage (V)', 'Resistance (Ohm)'] def startup(self): log.info("Setting up instruments") self.meter = Keithley2000("GPIB::25") self.meter.measure_voltage() self.meter.voltage_range = self.voltage_range self.meter.voltage_nplc = 1 # Integration constant to Medium self.source = Yokogawa7651("GPIB::4") self.source.apply_current() self.source.source_current_range = self.max_current * 1e-3 # A self.source.complinance_voltage = self.voltage_range self.source.enable_source() sleep(1) def execute(self): currents_up = np.arange(self.min_current, self.max_current, self.current_step) currents_down = np.arange(self.max_current, self.min_current, -self.current_step) currents = np.concatenate((currents_up, currents_down)) # Include the reverse currents *= 1e-3 # to mA from A steps = len(currents) log.info("Starting to sweep through current") for i, current in enumerate(currents): log.debug("Measuring current: %g mA" % current) self.source.source_current = current # Or use self.source.ramp_to_current(current, delay=0.1) sleep(self.delay * 1e-3) voltage = self.meter.voltage if abs(current) <= 1e-10: resistance = np.nan else: resistance = voltage / current data = { 'Current (A)': current, 'Voltage (V)': voltage, 'Resistance (Ohm)': resistance } self.emit('results', data) self.emit('progress', 100. * i / steps) if self.should_stop(): log.warning("Catch stop command in procedure") break def shutdown(self): self.source.shutdown() log.info("Finished") class MainWindow(ManagedWindow): def __init__(self): super().__init__( procedure_class=IVProcedure, inputs=[ 'max_current', 'min_current', 'current_step', 'delay', 'voltage_range' ], displays=[ 'max_current', 'min_current', 'current_step', 'delay', 'voltage_range' ], x_axis='Current (A)', y_axis='Voltage (V)' ) self.setWindowTitle('IV Measurement') def queue(self): directory = "./" # Change this to the desired directory filename = unique_filename(directory, prefix='IV') procedure = self.make_procedure() results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3776052 pymeasure-0.14.0/examples/Notebook Experiments/0000755000175100001770000000000014623331176021103 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Notebook Experiments/default_config.ini0000644000175100001770000000024614623331163024553 0ustar00runnerdocker[Filename] directory = data ext = csv [Logging] console = 1 console_level = INFO filename = test.log file_level = DEBUG [matplotlib.rcParams] figure.figsize = [6,4]././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Notebook Experiments/script.ipynb0000644000175100001770000000606514623331163023455 0ustar00runnerdocker{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ```Experiment``` class for live in-line plotting with jupyter\n", "This example uses the ```Experiment``` class to create a measurement from a ```procedure``` object." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile procedures.py\n", "import random\n", "from time import sleep\n", "\n", "import logging\n", "log = logging.getLogger('')\n", "log.addHandler(logging.NullHandler())\n", "\n", "from pymeasure.experiment import Procedure, IntegerParameter, Parameter, FloatParameter\n", "\n", "class TestProcedure(Procedure):\n", "\n", " iterations = IntegerParameter('Loop Iterations', default=100)\n", " delay = FloatParameter('Delay Time', units='s', default=0.2)\n", " seed = Parameter('Random Seed', default='12345')\n", "\n", " DATA_COLUMNS = ['Iteration', 'Random Number']\n", "\n", " def startup(self):\n", " log.info(\"Setting up random number generator\")\n", " random.seed(self.seed)\n", "\n", " def execute(self):\n", " log.info(\"Starting to generate numbers\")\n", " for i in range(self.iterations):\n", " data = {\n", " 'Iteration': i,\n", " 'Random Number': random.random()\n", " }\n", " log.debug(\"Produced numbers: %s\" % data)\n", " self.emit('results', data)\n", " self.emit('progress', 100.*i/self.iterations)\n", " sleep(self.delay)\n", " if self.should_stop():\n", " log.warning(\"Catch stop command in procedure\")\n", " break\n", "\n", " def shutdown(self):\n", " log.info(\"Finished\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from pymeasure.experiment import Experiment\n", "from procedures import TestProcedure\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "experiment = Experiment('test', TestProcedure(iterations=100, delay=.1))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "experiment.start()\n", "experiment.plot_live('Iteration', 'Random Number')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.9" } }, "nbformat": 4, "nbformat_minor": 1 } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/Notebook Experiments/script2.ipynb0000644000175100001770000001657414623331163023545 0ustar00runnerdocker{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# More features for ```Experiment``` class: custom config, `Measurable` parameter, `analysis` function\n", "\n", "This example uses the ```Experiment``` class to create a measurement from a ```procedure``` object, with the ```Measurable``` parameter to automatically generate sorted ```DATA_COLUMNS``` and ```MEASURE``` lists (which is then passed to the ```get_datapoint``` function of the ```Procedure``` class).\n", "\n", "The file ```my_config.ini``` is passed to set custom data saving, logging and matplotlib options.\n", "\n", "The ```analysis``` function is passed as an optional attribute, to produce on-the-fly data analysis for live plotting (only the raw data is saved on disk). To have analysed data save on disk, create an empty ```Measurable``` and update it in the ```measure``` loop as also shown in the example below." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile my_config.ini\n", "[Filename]\n", "prefix = my_data_\n", "dated_folder = 1\n", "directory = data\n", "ext = csv\n", "index = \n", "datetimeformat = %Y%m%d_%H%M%S\n", "\n", "[Logging]\n", "console = 1\n", "console_level = WARNING\n", "filename = test.log\n", "file_level = DEBUG\n", "\n", "[matplotlib.rcParams]\n", "axes.axisbelow = True\n", "axes.prop_cycle = cycler('color', ['b', 'g', 'r', 'c', 'm', 'y', 'k'])\n", "axes.edgecolor = 'white'\n", "axes.facecolor = '#EAEAF2'\n", "axes.grid = True\n", "axes.labelcolor = '.15'\n", "axes.labelsize = 11.0\n", "axes.linewidth = 0.0\n", "axes.titlesize = 12.0\n", "figure.facecolor = 'white'\n", "figure.figsize = [8.0, 5.5]\n", "font.sans-serif = ['Arial', 'Liberation Sans', 'Bitstream Vera Sans', 'sans-serif']\n", "grid.color = 'white'\n", "grid.linestyle = '-'\n", "grid.linewidth = 1.0\n", "image.cmap = 'Greys'\n", "legend.fontsize = 10.0\n", "legend.frameon = False\n", "legend.numpoints = 1\n", "legend.scatterpoints = 1\n", "lines.linewidth = 1.75\n", "lines.markeredgewidth = 0.0\n", "lines.markersize = 7.0\n", "lines.solid_capstyle = 'round'\n", "patch.facecolor = (0.2980392156862745, 0.4470588235294118, 0.6901960784313725)\n", "patch.linewidth = 0.3\n", "text.color = '.15'\n", "xtick.color = '.15'\n", "xtick.direction = 'out'\n", "xtick.labelsize = 10.0\n", "xtick.major.pad = 7.0\n", "xtick.major.size = 0.0\n", "xtick.major.width = 1.0\n", "xtick.minor.size = 0.0\n", "ytick.color = '.15'\n", "ytick.direction = 'out'\n", "ytick.labelsize = 10.0\n", "ytick.major.pad = 7.0\n", "ytick.major.size = 0.0\n", "ytick.major.width = 1.0\n", "ytick.minor.size = 0.0" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile procedures.py\n", "import random\n", "from time import sleep\n", "\n", "import logging\n", "log = logging.getLogger('')\n", "log.addHandler(logging.NullHandler())\n", "\n", "from pymeasure.experiment import Procedure, IntegerParameter, Parameter, FloatParameter, Measurable\n", "\n", "class TestProcedure(Procedure):\n", " \n", " iterations = IntegerParameter('Loop Iterations', default=100)\n", " delay = FloatParameter('Delay Time', units='s', default=0.2)\n", " seed = Parameter('Random Seed', default='12345')\n", " iteration = Measurable('Iteration', default = 0)\n", " random_number = Measurable('Random Number', random.random)\n", " offset = Measurable('Random Number + 1', default = 0)\n", "\n", " def startup(self):\n", " log.info(\"Setting up random number generator\")\n", " random.seed(self.seed)\n", " \n", " def measure(self):\n", " data = self.get_datapoint()\n", " data['Random Number + 1'] = data['Random Number'] + 1\n", " log.debug(\"Produced numbers: %s\" % data)\n", " self.emit('results', data)\n", " self.emit('progress', 100.*self.iteration.value/self.iterations)\n", "\n", " def execute(self):\n", " log.info(\"Starting to generate numbers\")\n", " for self.iteration.value in range(self.iterations):\n", " self.measure()\n", " sleep(self.delay)\n", " if self.should_stop():\n", " log.warning(\"Catch stop command in procedure\")\n", " break\n", "\n", " def shutdown(self):\n", " log.info(\"Finished\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile analysis.py\n", "def add_offset(data, offset):\n", " return data['Random Number'] + offset\n", "\n", "def analyse(data):\n", " data['Random Number + 2'] = add_offset(data, 2)\n", " return data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from pymeasure.experiment import Experiment, config\n", "from procedures import TestProcedure\n", "from analysis import analyse\n", "config.set_file('my_config.ini')\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "procedure = TestProcedure(iterations=10, delay=.1)\n", "experiment = Experiment('test', procedure, analyse)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "experiment.start()\n", "import pylab as pl\n", "pl.figure(figsize=(10,4))\n", "ax1 = pl.subplot(121)\n", "experiment.plot('Iteration', 'Random Number', ax=ax1)\n", "ax2 = pl.subplot(122)\n", "experiment.plot('Iteration', 'Random Number + 1', ax=ax2)\n", "experiment.plot_live()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Analysed data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "experiment.data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Raw data (as saved on disk)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "experiment.results.data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Filename generated by config preferences" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "experiment.filename" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.9" } }, "nbformat": 4, "nbformat_minor": 1 } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/examples/README.md0000644000175100001770000000473214623331163016320 0ustar00runnerdocker# Examples There are a number of examples for learning how to use PyMeasure. Many of them make a great starting point for your own graphical user interface (GUI) or command line script. Run one of the examples in the command line (or "Anaconda prompt"). ```bash python gui.py ``` ## Basic The following examples simulate data using a random number generator, so they do not require an instrument to be connected. They show off the basic structure for setting up your measurement. 1. [gui.py](Basic/gui.py) - A graphical user interface example with live-plotting and full features. 2. [script_plotter.py](Basic/script.py) - A command line example, which also has a live-plot. 3. [script.py](Basic/script.py) - A simple command line example. 4. [gui_custom_inputs.py](Basic/gui_custom_inputs.py) - An extension of [gui.py](Basic/gui.py), which uses a [custom Qt Creator file](Basic/gui_custom_inputs.ui) for the inputs. Notice in all of these examples, the Procedure class is the same. Once you define your Procedure, the choice of interface is up to you. ## Current-Voltage Measurements There are two examples of for measuring current-voltage (IV) characteristics, which use different instruments to make the measurement. They are based on [gui.py](Basic/gui.py). 1. [iv_yokogawa.py](Current-Voltage Measurements/iv_yokogawa.py) - Uses the Yokogawa 7651 Programmable Source to provide a current and measures the voltage with a Keithley 2000 Multimeter. 2. [iv_keithley.py](Current-Voltage Measurements/iv_keithley.py) - Uses the Keithley 2400 SourceMeter to provide a current and measures the voltage with a Keithley 2000 Multimeter. This has higher precision in the voltage than using the SourceMeter allow. ## Notebook Experiments Besides the interfaces shown in the [Basic examples](#basic), you can also make measurements in Jupyter notebooks. It is recommended that you use caution when using this technique, as the notebooks allow scripts to be executed out of order and they do not provide the same level of performance as the standard interfaces. Despite these caveats, the notebooks can be a flexible method for running custom experiments, where the Procedure needs to be modified often. 1. [script.ipynb](Notebook Experiments/script.ipynb) - Runs the simulated random number Procedure from [gui.py](Basic/gui.py) in a notebook. 2. [script2.ipynb](Notebook Experiments/script2.ipynb) - Extends [script.ipynb](Notebook Experiments/script.ipynb), using custom configurations and Measureable parameters. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3816051 pymeasure-0.14.0/pymeasure/0000755000175100001770000000000014623331176015233 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/__init__.py0000644000175100001770000000522214623331163017341 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 warnings # Maximally flexible approach to obtain version numbers, based on this approach: # https://github.com/pypa/setuptools_scm/issues/143#issuecomment-672878863 # Sadly, this does not work with editable installs, which bake in version info on installation. # see also https://github.com/pyusb/pyusb/pull/307#issuecomment-650797688 try: # If a user has setuptools_scm installed, assume they want the most up to date version string. # Alternatively, we could use a dummy dev module that is never packaged whose presence signals # that we are in an editable install/repo, see https://github.com/pycalphad/pycalphad/pull/341 import setuptools_scm __version__ = setuptools_scm.get_version(root='..', relative_to=__file__) del setuptools_scm except (ImportError, LookupError): # Setuptools_scm was not found, or it could not find a version, so use installation metadata. from importlib.metadata import version, PackageNotFoundError try: __version__ = version("pymeasure") # Alternatively, if the current approach is too slow, we could add # 'write_to = "pymeasure/_version.py"' in pyproject.toml and use the generated file here: # from ._version import version as __version__ except PackageNotFoundError: warnings.warn('Could not find pymeasure version, it does not seem to be installed. ' 'Either install it (editable or full) or install setuptools_scm') __version__ = '0.0.0' finally: del version, PackageNotFoundError ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3816051 pymeasure-0.14.0/pymeasure/adapters/0000755000175100001770000000000014623331176017036 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/adapters/__init__.py0000644000175100001770000000346314623331163021151 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from .protocol import ProtocolAdapter from pymeasure.adapters.telnet import TelnetAdapter 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") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/adapters/adapter.py0000644000175100001770000003050714623331163021031 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 warnings import warn import numpy as np from copy import copy from pyvisa.util import to_ieee_block, to_hp_block, to_binary_block class Adapter: """ 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: An optional callable used to preprocess strings received from the instrument. The callable returns the processed string. .. deprecated:: 0.11 Implement it in the instrument's `read` method instead. :param log: Parent logger of the 'Adapter' logger. :param \\**kwargs: Keyword arguments just to be cooperative. """ def __init__(self, preprocess_reply=None, log=None, **kwargs): super().__init__(**kwargs) self.preprocess_reply = preprocess_reply self.connection = None if log is None: self.log = logging.getLogger("Adapter") else: self.log = log.getChild("Adapter") self.log.addHandler(logging.NullHandler()) if preprocess_reply is not None: warn(("Parameter `preprocess_reply` is deprecated in Adapter. " "Implement it in the instrument instead."), FutureWarning) def __del__(self): """Close connection upon garbage collection of the device.""" self.close() def close(self): """Close the connection.""" if self.connection is not None: self.connection.close() # Directly called methods, which ensure proper logging of the communication # without the termination characters added by the particular adapters. # DO NOT OVERRIDE IN SUBCLASS! def write(self, command, **kwargs): """Write a string command to the instrument appending `write_termination`. Do not override in a subclass! :param str command: Command string to be sent to the instrument (without termination). :param \\**kwargs: Keyword arguments for the connection itself. """ self.log.debug("WRITE:%s", command) self._write(command, **kwargs) def write_bytes(self, content, **kwargs): """Write the bytes `content` to the instrument. Do not override in a subclass! :param bytes content: The bytes to write to the instrument. :param \\**kwargs: Keyword arguments for the connection itself. """ self.log.debug("WRITE:%s", content) self._write_bytes(content, **kwargs) def read(self, **kwargs): """Read up to (excluding) `read_termination` or the whole read buffer. Do not override in a subclass! :param \\**kwargs: Keyword arguments for the connection itself. :returns str: ASCII response of the instrument (excluding read_termination). """ read = self._read(**kwargs) self.log.debug("READ:%s", read) return read def read_bytes(self, count=-1, break_on_termchar=False, **kwargs): """Read a certain number of bytes from the instrument. Do not override in a subclass! :param int count: Number of bytes to read. A value of -1 indicates to read from the whole read buffer. :param bool break_on_termchar: Stop reading at a termination character. :param \\**kwargs: Keyword arguments for the connection itself. :returns bytes: Bytes response of the instrument (including termination). """ read = self._read_bytes(count, break_on_termchar, **kwargs) self.log.debug("READ:%s", read) return read # Methods to implement in the subclasses. def _write(self, command, **kwargs): """Write string to the instrument. Implement in subclass.""" raise NotImplementedError("Adapter class has not implemented writing.") def _write_bytes(self, content, **kwargs): """Write bytes to the instrument. Implement in subclass.""" raise NotImplementedError("Adapter class has not implemented writing bytes.") def _read(self, **kwargs): """Read string from the instrument. Implement in subclass.""" raise NotImplementedError("Adapter class has not implemented reading.") def _read_bytes(self, count, break_on_termchar, **kwargs): """Read bytes from the instrument. Implement in subclass.""" raise NotImplementedError("Adapter class has not implemented reading bytes.") def flush_read_buffer(self): """Flush and discard the input buffer. Implement in subclass.""" raise NotImplementedError("Adapter class has not implemented input flush.") # Deprecated methods. def ask(self, command): """ Write the command to the instrument and returns the resulting ASCII response. .. deprecated:: 0.11 Call `Instrument.ask` instead. :param command: SCPI command string to be sent to the instrument :returns: String ASCII response of the instrument """ warn("`Adapter.ask` is deprecated, call `Instrument.ask` instead.", FutureWarning) self.write(command) return self.read() def values(self, command, separator=',', cast=float, preprocess_reply=None): """ Write a command to the instrument and returns a list of formatted values from the result. .. deprecated:: 0.11 Call `Instrument.values` instead. :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 """ warn("`Adapter.values` is deprecated, call `Instrument.values` instead.", FutureWarning) 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 .. deprecated:: 0.11 Call `Instrument.binary_values` instead. :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 """ warn("`Adapter.binary_values` is deprecated, call `Instrument.binary_values` instead.", FutureWarning) self.write(command) binary = self.read() # header = binary[:header_bytes] data = binary[header_bytes:] return np.fromstring(data, dtype=dtype) # Binary format methods def read_binary_values(self, header_bytes=0, termination_bytes=None, dtype=np.float32, **kwargs): """ Returns a numpy array from a query for binary data :param int header_bytes: Number of bytes to ignore in header. :param int termination_bytes: Number of bytes to strip at end of message or None. :param dtype: The NumPy data type to format the values with. :param \\**kwargs: Further arguments for the NumPy fromstring method. :returns: NumPy array of values """ binary = self.read_bytes(-1) # header = binary[:header_bytes] data = binary[header_bytes:termination_bytes] return np.fromstring(data, dtype=dtype, **kwargs) def _format_binary_values(self, values, datatype='f', is_big_endian=False, header_fmt="ieee"): """Format values in binary format, used internally in :meth:`Adapter.write_binary_values`. :param values: data to be written to the device. :param datatype: the format string for a single element. See struct module. :param is_big_endian: boolean indicating endianess. :param header_fmt: Format of the header prefixing the data ("ieee", "hp", "empty"). :return: binary string. :rtype: bytes """ if header_fmt == "ieee": block = to_ieee_block(values, datatype, is_big_endian) elif header_fmt == "hp": block = to_hp_block(values, datatype, is_big_endian) elif header_fmt == "empty": block = to_binary_block(values, b"", datatype, is_big_endian) else: raise ValueError("Unsupported header_fmt: %s" % header_fmt) return block def write_binary_values(self, command, values, termination="", **kwargs): """ Write binary data to the instrument, e.g. waveform for signal generators :param command: command string to be sent to the instrument :param values: iterable representing the binary values :param termination: String added afterwards to terminate the message. :param \\**kwargs: Key-word arguments to pass onto :meth:`Adapter._format_binary_values` :returns: number of bytes written """ block = self._format_binary_values(values, **kwargs) return self.write_bytes(command.encode() + block + termination.encode()) 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): """ Return the last commands given after the last read call. """ result = copy(self._buffer) # Reset the buffer self._buffer = "" return result def _read_bytes(self, count, break_on_termchar): """ Return the last commands given after the last read call. """ result = copy(self._buffer) # Reset the buffer self._buffer = "" return result[:count].encode() def _write(self, command): """ Write the command to a buffer, so that it can be read back. """ self._buffer += command def _write_bytes(self, command): """ Write the command to a buffer, so that it can be read back. """ self._buffer += command.decode() def __repr__(self): return "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/adapters/prologix.py0000644000175100001770000003041714623331163021254 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 warnings import warn from pymeasure.adapters import VISAAdapter class PrologixAdapter(VISAAdapter): """ Encapsulates the additional commands necessary to communicate over a Prologix GPIB-USB Adapter, using the :class:`VISAAdapter`. Each PrologixAdapter is constructed based on a connection to the Prologix device itself and the GPIB address of the instrument to be communicated to. Connection sharing is achieved by using the :meth:`.gpib` method to spawn new PrologixAdapters for different GPIB addresses. :param resource_name: A `VISA resource string `__ that identifies the connection to the Prologix device itself, for example "ASRL5" for the 5th COM port. :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. .. deprecated:: 0.11 Implement it in the instrument's `wait_for` method instead. :param preprocess_reply: optional callable used to preprocess strings received from the instrument. The callable returns the processed string. .. deprecated:: 0.11 Implement it in the instrument's `read` method instead. :param auto: Enable or disable read-after-write and address instrument to listen. :param eoi: Enable or disable EOI assertion. :param eos: Set command termination string (CR+LF, CR, LF, or "") :param gpib_read_timeout: Set read timeout for GPIB communication in milliseconds from 1..3000 :param kwargs: Key-word arguments if constructing a new serial object :ivar address: Integer GPIB address of the desired instrument. Usage example: .. code:: adapter = PrologixAdapter("ASRL5::INSTR", 7) sourcemeter = Keithley2400(adapter) # at GPIB address 7 # generate another instance with a different GPIB address: adapter2 = adapter.gpib(9) multimeter = Keithley2000(adapter2) # at GPIB address 9 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, resource_name, address=None, rw_delay=0, serial_timeout=None, preprocess_reply=None, auto=False, eoi=True, eos="\n", gpib_read_timeout=None, **kwargs): # for legacy rw_delay: prefer new style over old one. if rw_delay: warn(("Parameter `rw_delay` is deprecated. " "Implement in Instrument's `wait_for` instead."), FutureWarning) kwargs['query_delay'] = rw_delay if serial_timeout: warn("Parameter `serial_timeout` is deprecated. Use `timeout` in ms instead", FutureWarning) kwargs['timeout'] = serial_timeout super().__init__(resource_name, asrl={ 'timeout': 500, 'write_termination': "\n", }, preprocess_reply=preprocess_reply, **kwargs) self.address = address if not isinstance(resource_name, PrologixAdapter): self.auto = auto self.eoi = eoi self.eos = eos if gpib_read_timeout is not None: self.gpib_read_timeout = gpib_read_timeout @property def auto(self): """Control whether to address instruments to talk after sending them a command (bool). Configure Prologix GPIB controller to automatically address instruments to talk after sending them a command in order to read their response. The feature called, Read-After-Write, saves the user from having to issue read commands repeatedly. This property enables (True) or disables (False) this feature. """ self.write("++auto") return bool(int(self.read(prologix=True))) @auto.setter def auto(self, value): self.write(f"++auto {int(value)}") @property def eoi(self): """Control whether to assert the EOI signal with the last character of any command sent over GPIB port (bool). Some instruments require EOI signal to be asserted in order to properly detect the end of a command. """ self.write("++eoi") return bool(int(self.read(prologix=True))) self.read(prologix=True) @eoi.setter def eoi(self, value): self.write(f"++eoi {int(value)}") @property def eos(self): """Control GPIB termination characters (str). possible values: - CR+LF - CR - LF - empty string When data from host is received, all non-escaped LF, CR and ESC characters are removed and GPIB terminators, as specified by this command, are appended before sending the data to instruments. This command does not affect data from instruments received over GPIB port. """ values = {0: "\r\n", 1: "\r", 2: "\n", 3: ""} self.write("++eos") return values[int(self.read(prologix=True))] @eos.setter def eos(self, value): values = {"\r\n": 0, "\r": 1, "\n": 2, "": 3} self.write(f"++eos {values[value]}") @property def gpib_read_timeout(self): """Control the timeout value for the GPIB communication in milliseconds possible values: 1 - 3000 """ self.write("++read_tmo_ms") return int(self.read(prologix=True)) @gpib_read_timeout.setter def gpib_read_timeout(self, value): self.write(f"++read_tmo_ms {value}") @property def version(self): """Get the version string of the Prologix controller. """ self.write('++ver') return self.read(prologix=True) def reset(self): """Perform a power-on reset of the controller. The process takes about 5 seconds. All input received during this time is ignored and the connection is closed. """ self.write('++rst') def ask(self, command): """ Ask the Prologix controller. .. deprecated:: 0.11 Call `Instrument.ask` instead. :param command: SCPI command string to be sent to instrument """ warn("`Adapter.ask` is deprecated, call `Instrument.ask` instead.", FutureWarning) self.write(command) return self.read() def write(self, command, **kwargs): """Write a string command to the instrument appending `write_termination`. If the GPIB address in :attr:`address` is defined, it is sent first. :param str command: Command string to be sent to the instrument (without termination). :param kwargs: Keyword arguments for the connection itself. """ # Overrides write instead of _write in order to ensure proper logging if self.address is not None and not command.startswith("++"): super().write("++addr %d" % self.address, **kwargs) super().write(command, **kwargs) def _format_binary_values(self, values, datatype='f', is_big_endian=False, header_fmt="ieee"): """Format values in binary format, used internally in :meth:`.write_binary_values`. :param values: data to be written to the device. :param datatype: the format string for a single element. See struct module. :param is_big_endian: boolean indicating endianess. :param header_fmt: Format of the header prefixing the data ("ieee", "hp", "empty"). :return: binary string. :rtype: bytes """ block = super()._format_binary_values(values, datatype, is_big_endian, header_fmt) # Prologix needs certian characters to be escaped. # Special care must be taken when sending binary data to instruments. If any of the # following characters occur in the binary data -- CR (ASCII 13), LF (ASCII 10), ESC # (ASCII 27), '+' (ASCII 43) - they must be escaped by preceding them with an ESC # character. special_chars = b'\x0d\x0a\x1b\x2b' new_block = b'' for b in block: escape = b'' if b in special_chars: escape = b'\x1b' new_block += (escape + bytes((b,))) return new_block def write_binary_values(self, command, values, **kwargs): """ Write binary data to the instrument, e.g. waveform for signal generators. values are encoded in a binary format according to IEEE 488.2 Definite Length Arbitrary Block Response Data block. :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 :meth:`._format_binary_values` :returns: number of bytes written """ if self.address is not None: address_command = "++addr %d\n" % self.address self.write(address_command) super().write_binary_values(command, values, "\n", **kwargs) def _read(self, prologix=False, **kwargs): """Read up to (excluding) `read_termination` or the whole read buffer. :param prologix: Read the prologix adapter itself. :param kwargs: Keyword arguments for the connection itself. :returns str: ASCII response of the instrument (excluding read_termination). """ if not prologix: self.write("++read eoi") return super()._read() def gpib(self, address, **kwargs): """ Return a 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 kwargs: Arguments for the initialization :returns: PrologixAdapter for specific GPIB address """ return PrologixAdapter(self, address, **kwargs) def _check_for_srq(self): # it was int(self.ask("++srq")) self.write("++srq") return int(self.read()) 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. :raises TimeoutError: "Waiting for SRQ timed out." """ stop = time.perf_counter() + timeout while self._check_for_srq() != 1: if time.perf_counter() > stop: raise TimeoutError("Waiting for SRQ timed out.") time.sleep(delay) def __repr__(self): if self.address is not None: return (f"") else: return f"" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/adapters/protocol.py0000644000175100001770000002014214623331163021244 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 unittest.mock import MagicMock from warnings import warn from .adapter import Adapter log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def to_bytes(command): """Change `command` to a bytes object""" if isinstance(command, (bytes, bytearray)): return command elif command is None: return None elif isinstance(command, str): return command.encode("utf-8") elif isinstance(command, (list, tuple)): return bytes(command) elif isinstance(command, (int, float)): return str(command).encode("utf-8") raise TypeError(f"Invalid input of type {type(command).__name__}.") class ProtocolAdapter(Adapter): """ Adapter class for testing the command exchange protocol without instrument hardware. This adapter is primarily meant for use within :func:`pymeasure.test.expected_protocol()`. The :attr:`connection` attribute is a :class:`unittest.mock.MagicMock` such that every call returns. If you want to set a return value, you can use :code:`adapter.connection.some_method.return_value = 7`, such that a call to :code:`adapter.connection.some_method()` will return `7`. Similarly, you can verify that this call to the connection method happened with :code:`assert adapter.connection.some_method.called is True`. You can specify dictionaries with return values of attributes and methods. :param list comm_pairs: List of "reference" message pair tuples. The first element is what is sent to the instrument, the second one is the returned message. 'None' indicates that a pair member (write or read) does not exist. The messages do **not** include the termination characters. :param connection_attributes: Dictionary of connection attributes and their values. :param connection_methods: Dictionary of method names of the connection and their return values. """ def __init__(self, comm_pairs=None, preprocess_reply=None, connection_attributes=None, connection_methods=None, **kwargs): """Generate the adapter and initialize internal buffers.""" super().__init__(preprocess_reply=preprocess_reply, **kwargs) # Setup communication if comm_pairs is None: comm_pairs = [] assert isinstance(comm_pairs, (list, tuple)), ( "Parameter comm_pairs has to be a list or tuple.") for pair in comm_pairs: if len(pair) != 2: raise ValueError(f'Comm_pairs element {pair} does not have two elements!') self._read_buffer = None self._write_buffer = None self.comm_pairs = comm_pairs self._index = 0 # Setup attributes self._setup_connection(connection_attributes, connection_methods) def _setup_connection(self, connection_attributes, connection_methods): self.connection = MagicMock() if connection_attributes is not None: for key, value in connection_attributes.items(): setattr(self.connection, key, value) if connection_methods is not None: for key, value in connection_methods.items(): getattr(self.connection, key).return_value = value def _write(self, command, **kwargs): """Compare the command with the expected one and fill the read.""" self._write_bytes(to_bytes(command)) assert self._write_buffer is None, ( f"Written bytes '{self._write_buffer}' do not match expected " f"'{self.comm_pairs[self._index][0]}'.") def _write_bytes(self, content, **kwargs): """Write the bytes `content`. If a command is full, fill the read.""" if self._write_buffer is None: self._write_buffer = content else: self._write_buffer += content try: p_write, p_read = self.comm_pairs[self._index] except IndexError: raise ValueError(f"No communication pair left to write {content}.") if self._write_buffer == to_bytes(p_write): assert self._read_buffer is None, ( f"Unread response '{self._read_buffer}' present when writing. " "Maybe a property's 'check_set_errors' is not accounted for, " "a read() call is missing in a method, or the defined protocol is incorrect?" ) # Clear the write buffer self._write_buffer = None self._read_buffer = to_bytes(p_read) self._index += 1 # If _write_buffer does _not_ agree with p_write, this is not cause for # concern, because you can in principle compose a message over several writes. # It's not clear how relevant this is in real-world use, but it's analogous # to the possibility to fetch a (binary) message over several reads. def _read(self, **kwargs): """Return an already present or freshly fetched read buffer as a string.""" return self._read_bytes(-1).decode("utf-8") def _read_bytes(self, count, break_on_termchar=False, **kwargs): """Read `count` number of bytes from the buffer. :param int count: Number of bytes to read. If -1, return the buffer. """ if break_on_termchar: warn(("Breaking on termination character in `read_bytes` cannot be tested. " "You have to separate the message parts in the com_pairs."), UserWarning) if self._read_buffer is not None: if count == -1 or count >= len(self._read_buffer): read = self._read_buffer self._read_buffer = None else: read = self._read_buffer[:count] self._read_buffer = self._read_buffer[count:] return read else: try: p_write, p_read = self.comm_pairs[self._index] except IndexError: raise ValueError("No communication pair left for reading.") assert p_write is None, ( f"Written {self._write_buffer} do not match expected {p_write} prior to read." if self._write_buffer else "Unexpected read without prior write.") assert p_read is not None, "Communication pair cannot be (None, None)." self._index += 1 p_read = to_bytes(p_read) if count == -1 or count >= len(p_read): # _read_buffer is already empty, no action required. return p_read else: self._read_buffer = p_read[count:] return p_read[:count] def flush_read_buffer(self): """ Flush and discard the input buffer As detailed by pyvisa, discard the read buffer contents and if data was present in the read buffer and no END-indicator was present, read from the device until encountering an END indicator (which causes loss of data). """ self.connection.flush("pyvisa.constants.BufferOperation.discard_read_buffer") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/adapters/serial.py0000644000175100001770000001271714623331163020673 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 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: An optional callable used to preprocess strings received from the instrument. The callable returns the processed string. .. deprecated:: 0.11 Implement it in the instrument's `read` method instead. :param write_termination: String appended to messages before writing them. :param read_termination: String expected at end of read message and removed. :param \\**kwargs: Any valid key-word argument for serial.Serial """ def __init__(self, port, preprocess_reply=None, write_termination="", read_termination="", **kwargs): super().__init__(preprocess_reply=preprocess_reply) if isinstance(port, serial.SerialBase): self.connection = port else: self.connection = serial.Serial(port, **kwargs) self.write_termination = write_termination self.read_termination = read_termination def _write(self, command, **kwargs): """Write a string command to the instrument appending `write_termination`. :param str command: Command string to be sent to the instrument (without termination). :param \\**kwargs: Keyword arguments for the connection itself. """ command += self.write_termination self._write_bytes(command.encode(), **kwargs) def _write_bytes(self, content, **kwargs): """Write the bytes `content` to the instrument. :param bytes content: The bytes to write to the instrument. :param \\**kwargs: Keyword arguments for the connection itself. """ self.connection.write(content, **kwargs) def _read(self, **kwargs): """Read up to (excluding) `read_termination` or the whole read buffer. :param \\**kwargs: Keyword arguments for the connection itself. :returns str: ASCII response of the instrument (read_termination is removed first). """ read = self._read_bytes(-1, break_on_termchar=True, **kwargs).decode() # Python>3.8 this shorter form is possible: # self._read_bytes(-1).decode().removesuffix(self.read_termination) if self.read_termination: return read.split(self.read_termination)[0] else: return read def _read_bytes(self, count, break_on_termchar, **kwargs): """Read a certain number of bytes from the instrument. :param int count: Number of bytes to read. A value of -1 indicates to read from the whole read buffer (waits for timeout). :param bool break_on_termchar: Stop reading at a termination character. :param \\**kwargs: Keyword arguments for the connection itself. :returns bytes: Bytes response of the instrument (including termination). """ if break_on_termchar and self.read_termination: return self.connection.read_until(self.read_termination.encode(), count if count > 0 else None, **kwargs) elif count >= 0: return self.connection.read(count, **kwargs) else: # For -1 we empty the buffer completely return self._read_bytes_until_timeout() def _read_bytes_until_timeout(self, chunk_size=256, **kwargs): """Read from the serial until a timeout occurs, regardless of the number of bytes. :chunk_size: The number of bytes attempted to in a single transaction. Multiple of these transactions will occur. """ # `Serial.readlines()` has an unpredictable timeout, see PR #866 data = bytes() while True: chunk = self.connection.read(chunk_size, **kwargs) data += chunk if len(chunk) < chunk_size: # If fewer bytes got returned, we had a timeout return data def flush_read_buffer(self): """Flush and discard the input buffer.""" self.connection.reset_input_buffer() def __repr__(self): return "" % self.connection.port ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/adapters/telnet.py0000644000175100001770000000471414623331163020705 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .adapter import Adapter class TelnetAdapter(Adapter): """ Adapter class for using the Python telnetlib package to allow communication to instruments This Adapter has been removed from service as the underlying library is missing! .. deprecated:: 0.11.2 The Python telnetlib module is deprecated since Python 3.11 and will be removed in Python 3.13 release. As a result, TelnetAdapter is deprecated, use VISAAdapter instead. The VISAAdapter supports TCPIP socket connections. When using the VISAAdapter, the `resource_name` argument should be `TCPIP[board]::::::SOCKET`. see here, """ def __init__(self, host, port=0, query_delay=0, preprocess_reply=None, **kwargs): raise NotImplementedError( "The TelnetAdapter has been removed, as the telnetlib module is deprecated. " "Use the VISAAdapter instead. The VISAAdapter supports TCPIP socket connections. " "When using the VISAAdapter, the `resource_name` argument should be " "`TCPIP[board]::::::SOCKET`. " "see here, ") def __repr__(self): return "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/adapters/visa.py0000644000175100001770000003055714623331163020360 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 warnings import warn import pyvisa import numpy as np from .adapter import Adapter from .protocol import ProtocolAdapter 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. The workhorse of our library, used by most instruments. :param resource_name: A `VISA resource string `__ or GPIB address integer that identifies the target of the connection :param visa_library: PyVISA VisaLibrary Instance, path of the VISA library or VisaLibrary spec string (``@py`` or ``@ivi``). If not given, the default for the platform will be used. :param preprocess_reply: An optional callable used to preprocess strings received from the instrument. The callable returns the processed string. .. deprecated:: 0.11 Implement it in the instrument's `read` method instead. :param float query_delay: Time in s to wait after writing and before reading. .. deprecated:: 0.11 Implement it in the instrument's `wait_for` method instead. :param log: Parent logger of the 'Adapter' logger. :param \\**kwargs: Keyword arguments for configuring the PyVISA connection. :Kwargs: Keyword arguments are used to configure the connection created by PyVISA. This is complicated by the fact that *which* arguments are valid depends on the interface (e.g. serial, GPIB, TCPI/IP, USB) determined by the current ``resource_name``. A flexible process is used to easily define reasonable *default values* for different instrument interfaces, but also enable the instrument user to *override any setting* if their situation demands it. A kwarg that names a pyVISA interface type (most commonly ``asrl``, ``gpib``, ``tcpip``, or ``usb``) is a dictionary with keyword arguments defining defaults specific to that interface. Example: ``asrl={'baud_rate': 4200}``. All other kwargs are either generally valid (e.g. ``timeout=500``) or override any default settings from the interface-specific entries above. For example, passing ``baud_rate=115200`` when connecting via a resource name ``ASRL1`` would override a default of 4200 defined as above. See :ref:`connection_settings` for how to tweak settings when *connecting* to an instrument. See :ref:`default_connection_settings` for how to best define default settings when *implementing an instrument*. """ def __init__(self, resource_name, visa_library='', preprocess_reply=None, query_delay=0, log=None, **kwargs): super().__init__(preprocess_reply=preprocess_reply, log=log) if query_delay: warn(("Parameter `query_delay` is deprecated. " "Implement in Instrument's `wait_for` instead."), FutureWarning) kwargs.setdefault("query_delay", query_delay) self.query_delay = query_delay if isinstance(resource_name, ProtocolAdapter): self.connection = resource_name self.connection.write_raw = self.connection.write_bytes self.read_bytes = self.connection.read_bytes return elif isinstance(resource_name, VISAAdapter): # Allow to reuse the connection. self.resource_name = getattr(resource_name, "resource_name", None) self.connection = resource_name.connection self.manager = resource_name.manager self.query_delay = resource_name.query_delay return elif isinstance(resource_name, int): resource_name = "GPIB0::%d::INSTR" % resource_name self.resource_name = resource_name self.manager = pyvisa.ResourceManager(visa_library) # Clean up kwargs considering the interface type matching resource_name if_type = self.manager.resource_info(self.resource_name).interface_type for key in list(kwargs.keys()): # iterate over a copy of the keys as we modify kwargs # Remove all interface-specific kwargs: if key in pyvisa.constants.InterfaceType.__members__: if getattr(pyvisa.constants.InterfaceType, key) is if_type: # For the present interface, dump contents into kwargs first if they are not # present already. This way, it is possible to override default values with # kwargs passed to Instrument.__init__() for k, v in kwargs[key].items(): kwargs.setdefault(k, v) del kwargs[key] self.connection = self.manager.open_resource( resource_name, **kwargs ) def close(self): """Close the connection. .. note:: This closes the connection to the resource for all adapters using it currently (e.g. different adapters using the same GPIB line). """ super().close() try: if self.manager.visalib.library_path == "unset": # if using the pyvisa-sim library the manager has to be also closed. # this works around https://github.com/pyvisa/pyvisa-sim/issues/82 self.manager.close() except AttributeError: # AttributeError can occur during __del__ calling close pass def _write(self, command, **kwargs): """Write a string command to the instrument appending `write_termination`. :param str command: Command string to be sent to the instrument (without termination). :param \\**kwargs: Keyword arguments for the connection itself. """ self.connection.write(command, **kwargs) def _write_bytes(self, content, **kwargs): """Write the bytes `content` to the instrument. :param bytes content: The bytes to write to the instrument. :param \\**kwargs: Keyword arguments for the connection itself. """ self.connection.write_raw(content, **kwargs) def _read(self, **kwargs): """Read up to (excluding) `read_termination` or the whole read buffer. :param \\**kwargs: Keyword arguments for the connection itself. :returns str: ASCII response of the instrument (excluding read_termination). """ return self.connection.read(**kwargs) def _read_bytes(self, count, break_on_termchar=False, **kwargs): """Read a certain number of bytes from the instrument. :param int count: Number of bytes to read. A value of -1 indicates to read from the whole read buffer until timeout. :param bool break_on_termchar: Stop reading at a termination character. :param \\**kwargs: Keyword arguments for the connection itself. :returns bytes: Bytes response of the instrument (including termination). """ if count >= 0: return self.connection.read_bytes(count, break_on_termchar=break_on_termchar, **kwargs) elif break_on_termchar: return self.connection.read_raw(None, **kwargs) else: # pyvisa's `read_raw` reads until newline, if no termination_character defined # and if not configured to stop at a termination lane etc. # see https://github.com/pyvisa/pyvisa/issues/728 result = bytearray() while True: try: result.extend(self.connection.read_bytes(1)) except pyvisa.errors.VisaIOError as exc: if exc.error_code == pyvisa.constants.StatusCode.error_timeout: return bytes(result) raise def ask(self, command): """ Writes the command to the instrument and returns the resulting ASCII response .. deprecated:: 0.11 Call `Instrument.ask` instead. :param command: SCPI command string to be sent to the instrument :returns: String ASCII response of the instrument """ warn("`Adapter.ask` is deprecated, call `Instrument.ask` instead.", FutureWarning) 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. .. deprecated:: 0.11 Call `Instrument.values` instead. :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. """ warn("`Adapter.ask_values` is deprecated, call `Instrument.values` instead.", FutureWarning) 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 .. deprecated:: 0.11 Call `Instrument.binary_values` instead. :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 """ warn("`Adapter.binary_values` is deprecated, call `Instrument.binary_values` instead.", FutureWarning) self.connection.write(command) binary = self.connection.read_raw() # header = binary[:header_bytes] data = binary[header_bytes:] return np.fromstring(data, dtype=dtype) def wait_for_srq(self, timeout=25, delay=0.1): """ Block until a SRQ, and leave 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 flush_read_buffer(self): """ Flush and discard the input buffer As detailed by pyvisa, discard the read and receivee buffer contents and if data was present in the read buffer and no END-indicator was present, read from the device until encountering an END indicator (which causes loss of data). """ try: self.connection.flush(pyvisa.constants.BufferOperation.discard_read_buffer) self.connection.flush(pyvisa.constants.BufferOperation.discard_receive_buffer) except NotImplementedError: # NotImplementedError is raised when using resource types other than `asrl` # in conjunction with pyvisa-py. # Upstream issue: https://github.com/pyvisa/pyvisa-py/issues/348 # fake discarding the read buffer by reading all available messages. timeout = self.connection.timeout self.connection.timeout = 0 try: self.read_bytes(-1) except pyvisa.errors.VisaIOError: pass finally: self.connection.timeout = timeout def __repr__(self): return "" % self.connection.resource_name ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/adapters/vxi11.py0000644000175100001770000001407414623331163020362 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 warnings import warn from .adapter import Adapter 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') class VXI11Adapter(Adapter): """ VXI11 Adapter class. Provides a adapter object that wraps around the read, write and ask functionality of the vxi11 library. .. deprecated:: 0.11 Use VISAAdapter instead. :param host: string containing the visa connection information. :param preprocess_reply: (deprecated) 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, query_delay=kwargs.pop('query_delay', 0)) warn("Deprecated, use VISAAdapter instead.", FutureWarning) # 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, **kwargs): """Write a string command to the instrument appending `write_termination`. :param str command: Command string to be sent to the instrument (without termination). :param kwargs: Keyword arguments for the connection itself. """ self.connection.write(command) def _read(self, **kwargs): """Read up to (excluding) `read_termination` or the whole read buffer. :param kwargs: Keyword arguments for the connection itself. :returns str: ASCII response of the instrument (excluding read_termination). """ return self.connection.read() def ask(self, command): """ Wrapper function for the ask command using the vx11 interface. .. deprecated:: 0.11 Call `Instrument.ask` instead. :param command: string with the command that will be transmitted to the instrument. :returns string containing a response from the device. """ warn("Do not call `Adapter.ask`, but `Instrument.ask` instead.", FutureWarning) return self.connection.ask(command) def write_raw(self, command): """Write bytes to the device. .. deprecated:: 0.11 Use `write_bytes` instead. """ warn("Use `write_bytes` instead.", FutureWarning) self.write_bytes(command) def _write_bytes(self, command, **kwargs): """Write the bytes `content` to the instrument. :param bytes content: The bytes to write to the instrument. :param kwargs: Keyword arguments for the connection itself. .. note:: vx11 adds the term_char even for writing_bytes. """ # Note: vxi11.write_raw adds the term_char! self.connection.write_raw(command, **kwargs) def read_raw(self): """Read bytes from the device. .. deprecated:: 0.11 Use `read_bytes` instead. """ warn("Use `read_bytes` instead.", FutureWarning) return self.read_bytes(-1) def _read_bytes(self, count, break_on_termchar=False, **kwargs): """Read a certain number of bytes from the instrument. :param int count: Number of bytes to read. A value of -1 indicates to read the whole read buffer. :param bool break_on_termchar: Stop reading at a termination character. :param kwargs: Keyword arguments for the connection itself. :returns bytes: Bytes response of the instrument (including termination). """ if self.connection.term_char and not break_on_termchar: read_termination = self.connection.term_char self.connection.term_char = None try: return self.connection.read_raw(count, **kwargs) finally: self.connection.term_char = read_termination else: return self.connection.read_raw(count, **kwargs) def ask_raw(self, command): """ Wrapper function for the ask_raw command using the vx11 interface. .. deprecated:: 0.11 Use `Instrument.write_bytes` and `Instrument.read_bytes` instead. :param command: binary string with the command that will be transmitted to the instrument :returns binary string containing the response from the device. """ warn("Use `Instrument.write_bytes` and `Instrument.read_bytes` instead.", FutureWarning) return self.connection.ask_raw(command) def __repr__(self): return f'' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/console.py0000644000175100001770000000411314623331163017242 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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: """ 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3816051 pymeasure-0.14.0/pymeasure/display/0000755000175100001770000000000014623331176016700 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/Qt.py0000644000175100001770000000462314623331163017637 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, QtWidgets, loadUiType # noqa: F401 log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) # Should be removed when PySide2 provides QtWidgets.QApplication.exec() or when support for PySide2 # is dropped (https://doc.qt.io/qtforpython/porting_from2.html#class-function-deprecations) if not hasattr(QtWidgets.QApplication, 'exec'): QtWidgets.QApplication.exec = QtWidgets.QApplication.exec_ if not hasattr(QtCore.QCoreApplication, 'exec'): QtCore.QCoreApplication.exec = QtCore.QCoreApplication.exec_ if not hasattr(QtWidgets.QMenu, 'exec'): def exec(self, *args, **kwargs): self.exec_(*args, **kwargs) QtWidgets.QMenu.exec = exec 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 accessibility. """ 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, QtWidgets.QWidget): setattr(widget, name, element) return widget ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/__init__.py0000644000175100001770000000342514623331163021011 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/browser.py0000644000175100001770000001335014623331163020733 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, QtWidgets from ..experiment import Procedure log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class BaseBrowserItem: """ Base class for an experiment's browser item. BaseBrowerItem outlines core functionality for displaying progress of an experiment to the user. """ status_label = { Procedure.QUEUED: 'Queued', Procedure.RUNNING: 'Running', Procedure.FAILED: 'Failed', Procedure.ABORTED: 'Aborted', Procedure.FINISHED: 'Finished'} def setStatus(self, status): raise NotImplementedError('Must be reimplemented by subclasses') def setProgress(self, status): raise NotImplementedError('Must be reimplemented by subclasses') class BrowserItem(QtWidgets.QTreeWidgetItem, BaseBrowserItem): """ Represent a row in the :class:`~pymeasure.display.browser.Browser` tree widget """ def __init__(self, results, color, parent=None): super().__init__(parent) pixelmap = QtGui.QPixmap(24, 24) pixelmap.fill(color) self.setIcon(0, QtGui.QIcon(pixelmap)) self.setFlags(self.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable) self.setCheckState(0, QtCore.Qt.CheckState.Checked) self.setText(1, basename(results.data_filename)) self.setStatus(results.procedure.status) self.progressbar = QtWidgets.QProgressBar() self.progressbar.setRange(0, 100) self.progressbar.setValue(0) def setStatus(self, status): self.setText(3, self.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(int(progress)) class Browser(QtWidgets.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 = set(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.SortOrder.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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/console.py0000644000175100001770000002442514623331163020717 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 argparse try: import progressbar # Check that progressbar is progressbar2 progressbar.streams except (AttributeError, ImportError): progressbar = None from .Qt import QtCore import signal from ..log import console_log from .browser import BaseBrowserItem from .manager import BaseManager, Experiment from ..experiment import Results, Procedure, unique_filename log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ConsoleBrowserItem(BaseBrowserItem): def __init__(self, progress_bar): self.bar = progress_bar def setStatus(self, status): if self.bar: self.bar.update(status=self.status_label[status]) def setProgress(self, progress): if self.bar: self.bar.update(progress) class ConsoleArgumentParser(argparse.ArgumentParser): special_options = { "no-progressbar": {"default": False, "desc": "Disable progressbar", "help_fields": ["default"], "action": 'store_true'}, "log-level": {"default": 'INFO', "choices": list(logging._nameToLevel.keys()), "desc": "Set log level (logging module values)", "help_fields": ["default"]}, "sequence-file": {"default": None, "desc": "Sequencer file as used/defined by the sequencer widget to " "execute a sequence of measurements", "help_fields": ["default"]}, "result-directory": {"default": ".", "desc": "Directory where experiment's result are saved", "help_fields": ["default"]}, "result-file": {"default": None, "desc": "File name where results are stored; this string is handled " "by the `unique_filename` function and hence allows for " "filling in parameter values and is suffixed by the date " "(`YYYY-MM-DD`) and an index number", "help_fields": ["default"]}, "use-result-file": {"default": None, "desc": "Result file to retrieve params from", "help_fields": ["default"]}, } def __init__(self, procedure_class, **kwargs): super().__init__(**kwargs) self.procedure_class = procedure_class self.setup_parser() def setup_parser(self): """ Setup command line arguments parsing from parameters information """ self.procedure = self.procedure_class() parameter_objects = self.procedure.parameter_objects() special_options = copy.deepcopy(self.special_options) special_opts_group = self.add_argument_group("Common options") for option, kwargs in special_options.items(): help_fields = [('units are', 'units')] + kwargs['help_fields'] desc = kwargs['desc'] kwargs['help'] = self._cli_help_fields(desc, kwargs, help_fields) del kwargs['help_fields'] del kwargs['desc'] special_opts_group.add_argument("--" + option, **kwargs) experiment_opts_group = self.add_argument_group("Experiment options") for name in parameter_objects: if name in special_options: raise Exception(f"Experiment option {name} " + "is already defined as common options") kwargs = {} parameter = parameter_objects[name] default, help_fields, _type = parameter.cli_args kwargs['help'] = self._cli_help_fields(parameter.name, parameter, help_fields) kwargs['default'] = default if _type is not None: kwargs['type'] = _type experiment_opts_group.add_argument("--" + name, **kwargs) @staticmethod def _cli_help_fields(name, inst, help_fields): def hasattr_dict(inst, key): return key in inst def getattr_dict(inst, key): return inst[key] if isinstance(inst, dict): hasattribute = hasattr_dict getattribute = getattr_dict else: hasattribute = hasattr getattribute = getattr message = name for field in help_fields: if isinstance(field, str): field = ["{} is".format(field), field] if hasattribute(inst, field[1]) and getattribute(inst, field[1]) is not None: prefix = field[0] value = getattribute(inst, field[1]) message += ", {} {}".format(prefix, value) message = message.replace("%", "%%") return message class ManagedConsole(QtCore.QCoreApplication): """ Base class for console experiment management. Parameters for :code:`__init__` constructor. :param procedure_class: procedure class describing the experiment (see :class:`~pymeasure.experiment.procedure.Procedure`) :param log_channel: :code:`logging.Logger` instance to use for logging output :param log_level: logging level """ def __init__(self, procedure_class, log_channel='', log_level=logging.INFO, ): super().__init__([]) self.procedure_class = procedure_class self.log_channel = log_channel self.log = logging.getLogger(log_channel) self.log_level = log_level log.setLevel(log_level) self.log.setLevel(log_level) # Check if the get_estimates function is reimplemented self.use_estimator = not self.procedure_class.get_estimates == Procedure.get_estimates if self.use_estimator: log.warning("Estimator not yet implemented") # Handle Ctrl+C nicely signal.signal(signal.SIGINT, lambda sig, _: self.abort()) # Parse command line arguments parser = ConsoleArgumentParser(procedure_class) args = vars(parser.parse_args()) self.directory = args['result_directory'] self.filename = args['result_file'] try: log_level = int(args['log_level']) except ValueError: # Ignore and assume it is a valid level string log_level = args['log_level'] self.log_level = log_level log.setLevel(self.log_level) self.log.setLevel(self.log_level) if args['sequence_file'] is not None: raise NotImplementedError("Sequencer not yet implemented") # Set procedure parameters self.parameter_values = {} if args['use_result_file'] is not None: # Special case set parameters from log file results = Results.load(args['use_result_file']) for name in results.parameters: self.parameter_values[name] = results.parameters[name].value else: for name in args: opt_name = name.replace("_", "-") if not (opt_name in parser.special_options): self.parameter_values[name] = args[name] if progressbar and not args['no_progressbar']: progressbar.streams.wrap_stderr() self.bar = progressbar.ProgressBar(max_value=100, prefix='{variables.status}: ', variables={'status': "Unknown"}) else: self.bar = None scribe = console_log(self.log, level=self.log_level) scribe.start() # Setup Manager self.manager = BaseManager( log_level=self.log_level, parent=self) self.manager.abort_returned.connect(self._terminate) self.manager.failed.connect(self._terminate) self.manager.finished.connect(self._terminate) self.manager.log.connect(self.log.handle) def get_filename(self, directory, procedure=None): """ Return filename for saving results file :param directory: directory of the returned filename. """ if self.filename is not None: return unique_filename(directory, prefix=self.filename, procedure=procedure) else: return unique_filename(directory) def queue(self): procedure = self.procedure_class() procedure.set_parameters(self.parameter_values) filename = self.get_filename(self.directory, procedure) results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) def _terminate(self): if not self.manager.experiments.has_next(): self.quit() def abort(self): """ Aborts the currently running Experiment, but raises an exception if there is no running experiment """ self.manager.abort() def new_experiment(self, results): browser_item = ConsoleBrowserItem(self.bar) return Experiment(results, browser_item=browser_item) def exec(self): self.queue() super().exec() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/curves.py0000644000175100001770000002002214623331163020551 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 import pyqtgraph as pg from .Qt import QtCore, QtGui log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ResultsCurve(pg.PlotDataItem): """ Creates a curve loaded dynamically from a file through the Results object. 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, force_reload=False, wdg=None, **kwargs): super().__init__(**kwargs) self.results = results self.wdg = wdg self.pen = kwargs.get('pen', None) self.x, self.y = x, y self.force_reload = force_reload self.color = self.opts['pen'].color() def update_data(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]) def set_color(self, color): self.pen.setColor(color) self.color = self.opts['pen'].color() self.updateItems(styleUpdate=True) # 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, wdg=None, **kwargs): self.results = results self.wdg = wdg 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 self.cm = pg.colormap.get('viridis') super().__init__(image=self.img_data) # Scale and translate image so that the pixels are in the correct # position in "data coordinates" tr = QtGui.QTransform() tr.scale(self.xstep, self.ystep) tr.translate(int(self.xstart / self.xstep) - 0.5, int(self.ystart / self.ystep) - 0.5) # 0.5 so pixels centered self.setTransform(tr) def update_data(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) def colormap(self, x): """ Return mapped color as 0.0-1.0 floats RGBA """ return self.cm.map(x, mode='float') # TODO: colormap selection class BufferCurve(pg.PlotDataItem): """ Creates a curve based on a predefined buffer size and allows data to be added dynamically. """ data_updated = QtCore.Signal() def __init__(self, **kwargs): super().__init__(**kwargs) self._buffer = None def prepare(self, size, dtype=np.float32): """ Prepares the buffer based on its size, data type """ self._buffer = np.empty((size, 2), dtype=dtype) self._ptr = 0 def append(self, x, y): """ 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]) 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.Signal(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.PenStyle.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") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/inputs.py0000644000175100001770000002242714623331163020577 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 QtGui, QtWidgets log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Input: """ 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(Input, QtWidgets.QLineEdit): """ 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): super().__init__(parameter=parameter, parent=parent, **kwargs) def setValue(self, value): # QtWidgets.QLineEdit has a setText() method instead of setValue() return super().setText(value) def setSuffix(self, value): pass def value(self): # QtWidgets.QLineEdit has a text() method instead of value() return super().text() class IntegerInput(Input, QtWidgets.QSpinBox): """ Spin input box for integer values, connected to a :class:`IntegerParameter`. """ def __init__(self, parameter, parent=None, **kwargs): super().__init__(parameter=parameter, parent=parent, **kwargs) if parameter.step: self.setButtonSymbols(QtWidgets.QAbstractSpinBox.ButtonSymbols.UpDownArrows) self.setSingleStep(parameter.step) self.setEnabled(True) else: self.setButtonSymbols(QtWidgets.QAbstractSpinBox.ButtonSymbols.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 def stepEnabled(self): if self.parameter.step: return QtWidgets.QAbstractSpinBox.StepEnabledFlag.StepUpEnabled | \ QtWidgets.QAbstractSpinBox.StepEnabledFlag.StepDownEnabled else: return QtWidgets.QAbstractSpinBox.StepEnabledFlag.StepNone class BooleanInput(Input, QtWidgets.QCheckBox): """ Checkbox for boolean values, connected to a :class:`BooleanParameter`. """ def __init__(self, parameter, parent=None, **kwargs): super().__init__(parameter=parameter, parent=parent, **kwargs) 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(Input, QtWidgets.QComboBox): """ Dropdown for list values, connected to a :class:`ListParameter`. """ def __init__(self, parameter, parent=None, **kwargs): super().__init__(parameter=parameter, parent=parent, **kwargs) 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(Input, QtWidgets.QDoubleSpinBox): """ 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): super().__init__(parameter=parameter, parent=parent, **kwargs) if parameter.step: self.setButtonSymbols(QtWidgets.QAbstractSpinBox.ButtonSymbols.UpDownArrows) self.setSingleStep(parameter.step) self.setEnabled(True) else: self.setButtonSymbols(QtWidgets.QAbstractSpinBox.ButtonSymbols.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.Notation.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 toDouble(self, string): value, success = self.validator.locale().toDouble(string) if not success: raise ValueError('String could not be converted to a double') else: return value def toString(self, value, format='g', precision=6): return self.validator.locale().toString(value, format, precision) def valueFromText(self, text): text = str(text) if self._parameter.units: text = text[:-(len(self._parameter.units) + 1)] try: val = self.toDouble(text) except ValueError: val = self._parameter.default return val def textFromValue(self, value): string = self.toString(value).replace("e+", "e") string = re.sub(r"e(-?)0*(\d+)", r"e\1\2", string) return string def stepEnabled(self): if self.parameter.step: return QtWidgets.QAbstractSpinBox.StepEnabledFlag.StepUpEnabled | \ QtWidgets.QAbstractSpinBox.StepEnabledFlag.StepDownEnabled else: return QtWidgets.QAbstractSpinBox.StepEnabledFlag.StepNone ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/listeners.py0000644000175100001770000001064714623331163021266 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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(f"{self.__class__.__name__} has ZMQ Context: {self.context!r}") 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=lambda msg: (msg[0].decode(), cloudpickle.loads(msg[1])), flags=flags ) return topic, record def message_waiting(self): return self.poller.poll(self.timeout) def __repr__(self): return "<{}(port={},topic={},should_stop={})>".format( 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.Signal(int) progress = QtCore.Signal(float) log = QtCore.Signal(object) worker_running = QtCore.Signal() worker_failed = QtCore.Signal() worker_finished = QtCore.Signal() # Distinguished from QThread.finished worker_abort_returned = QtCore.Signal() 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") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/log.py0000644000175100001770000000406014623331163020027 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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(Handler): # Class Emitter is added to keep compatibility with PySide2 # 1. Signal needs to be class attribute of a QObject subclass # 2. logging Handler emit method clashes with QObject emit method # 3. As a consequence, the LogHandler cannot inherit both from # Handler and QObject # 4. A new utility class Emitter subclass of QObject is # introduced to handle record Signal and workaround the problem class Emitter(QtCore.QObject): record = QtCore.Signal(object) def __init__(self): super().__init__() self.emitter = self.Emitter() def connect(self, *args, **kwargs): return self.emitter.record.connect(*args, **kwargs) def emit(self, record): self.emitter.record.emit(self.format(record)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/manager.py0000644000175100001770000002575514623331163020676 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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_list: :class:`.ResultsCurve` list. List of curves associated with an experiment. They could represent different views of the same experiment. Not required for `.ManagedConsole` displayed experiments. :param browser_item: :class:`.BaseBrowserItem` based object """ def __init__(self, results, curve_list=None, browser_item=None, parent=None): super().__init__(parent) self.results = results self.data_filename = self.results.data_filename self.procedure = self.results.procedure self.curve_list = curve_list 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 BaseManager(QtCore.QObject): """Controls the execution of :class:`.Experiment` classes by implementing a queue system in which Experiments are added, removed, executed, or aborted. """ _is_continuous = True _start_on_add = True queued = QtCore.Signal(object) running = QtCore.Signal(object) finished = QtCore.Signal(object) failed = QtCore.Signal(object) aborted = QtCore.Signal(object) abort_returned = QtCore.Signal(object) log = QtCore.Signal(object) def __init__(self, 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.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.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) 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) 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 Manager(BaseManager): """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. """ def __init__(self, widget_list, 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.widget_list = widget_list self.browser = browser self.port = port def load(self, experiment): """ Load a previously executed Experiment """ super().load(experiment) self.browser.add(experiment) for curve in experiment.curve_list: if curve: curve.wdg.load(curve) def remove(self, experiment): """ Removes an Experiment """ super().remove(experiment) self.browser.takeTopLevelItem( self.browser.indexOfTopLevelItem(experiment.browser_item)) for curve in experiment.curve_list: if curve: curve.wdg.remove(curve) def _finish(self): log.debug("Manager's running experiment has finished") experiment = self._running_experiment self._clean_up() experiment.browser_item.setProgress(100) for curve in experiment.curve_list: if curve: curve.update_data() self.finished.emit(experiment) if self._is_continuous: # Continue running procedures self.next() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/plotter.py0000644000175100001770000000510614623331163020741 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 QtWidgets 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. .. seealso:: Tutorial :ref:`tutorial-plotterwindow` A tutorial and example on using the Plotter and PlotterWindow. """ def __init__(self, results, refresh_time=0.1, linewidth=1): super().__init__() self.results = results self.refresh_time = refresh_time self.linewidth = linewidth def run(self): app = QtWidgets.QApplication(sys.argv) window = PlotterWindow(self, refresh_time=self.refresh_time, linewidth=self.linewidth) 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: https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/plotitem.html """ pass def wait_for_close(self, check_time=0.1): while not self.should_stop(): time.sleep(check_time) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/thread.py0000644000175100001770000000416214623331163020520 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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().wait() def stop(self): self._should_stop.set() def should_stop(self): return self._should_stop.is_set() def __repr__(self): return "<{}(should_stop={})>".format( self.__class__.__name__, self.should_stop()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3856053 pymeasure-0.14.0/pymeasure/display/widgets/0000755000175100001770000000000014623331176020346 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/__init__.py0000644000175100001770000000322614623331163022456 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .browser_widget import BrowserWidget from .fileinput_widget import FileInputWidget from .estimator_widget import EstimatorWidget, EstimatorThread from .image_frame import ImageFrame from .image_widget import ImageWidget from .inputs_widget import InputsWidget from .log_widget import LogWidget from .plot_frame import PlotFrame from .plot_widget import PlotWidget from .results_dialog import ResultsDialog from .sequencer_widget import SequencerWidget from .tab_widget import TabWidget from .table_widget import TableWidget ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/browser_widget.py0000644000175100001770000000507714623331163023753 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ..browser import Browser from ..Qt import QtWidgets log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class BrowserWidget(QtWidgets.QWidget): """ Widget wrapper for :class:`Browser` class """ 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 = QtWidgets.QPushButton('Clear all', self) self.clear_button.setEnabled(False) self.hide_button = QtWidgets.QPushButton('Hide all', self) self.hide_button.setEnabled(False) self.show_button = QtWidgets.QPushButton('Show all', self) self.show_button.setEnabled(False) self.open_button = QtWidgets.QPushButton('Open', self) self.open_button.setEnabled(True) def _layout(self): vbox = QtWidgets.QVBoxLayout(self) vbox.setSpacing(0) hbox = QtWidgets.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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/directory_widget.py0000644000175100001770000000543714623331163024274 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, QtGui, QtWidgets log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class DirectoryLineEdit(QtWidgets.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 = QtWidgets.QCompleter(self) completer.setCompletionMode(QtWidgets.QCompleter.CompletionMode.PopupCompletion) model = QtGui.QFileSystemModel(completer) model.setRootPath(model.myComputer()) model.setFilter(QtCore.QDir.Filter.Dirs | QtCore.QDir.Filter.Drives | QtCore.QDir.Filter.NoDotAndDotDot | QtCore.QDir.Filter.AllDirs) completer.setModel(model) self.setCompleter(completer) browse_action = QtGui.QAction(self) browse_action.setIcon(self.style().standardIcon( getattr(QtWidgets.QStyle.StandardPixmap, 'SP_DialogOpenButton'))) browse_action.triggered.connect(self.browse_triggered) self.addAction(browse_action, QtWidgets.QLineEdit.ActionPosition.TrailingPosition) def _get_starting_directory(self): current_text = self.text() if current_text != '' and QtCore.QDir(current_text).exists(): return current_text else: return '/' def browse_triggered(self): path = QtWidgets.QFileDialog.getExistingDirectory( self, 'Directory', self._get_starting_directory()) if path != '': self.setText(path) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/dock_widget.py0000644000175100001770000001621614623331163023205 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 import path import json from pyqtgraph.dockarea import Dock, DockArea from pyqtgraph.dockarea.Dock import DockLabel import pyqtgraph as pg from .plot_widget import PlotWidget, PlotFrame from ..Qt import QtWidgets from .tab_widget import TabWidget log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class DockWidget(TabWidget, QtWidgets.QWidget): """ Widget that contains a DockArea with a number of Docks as determined by the length of the longest x_axis_labels or y_axis_labels list. :param name: Name for the TabWidget :param procedure_class: procedure class describing the experiment (see :class:`~pymeasure.experiment.procedure.Procedure`) :param x_axis_labels: List of data column(s) for the x-axis of the plot. If the list is shorter than y_axis_labels the last item in the list to match y_axis_labels length. :param y_axis_labels: List of data column(s) for the y-axis of the plot. If the list is shorter than x_axis_labels the last item in the list to match x_axis_labels length. :param linewidth: line width for plots in :class:`~pymeasure.display.widgets.plot_widget.PlotWidget` :param layout_path: Directory path to save dock layout state. Default is './' :param layout_filename: Optional filename for dock layout file. Default: *current procedure class* + "_dock_layout.json" :param parent: Passed on to QtWidgets.QWidget. Default is None """ def __init__(self, name, procedure_class, x_axis_labels=None, y_axis_labels=None, linewidth=1, layout_path='./', layout_filename='', parent=None): super().__init__(name, parent) self.procedure_class = procedure_class if layout_filename: self.dock_layout_filename = path.join(layout_path, layout_filename) else: self.dock_layout_filename = path.join(layout_path, procedure_class.__name__ + '_dock_layout.json') self.x_axis_labels = x_axis_labels self.y_axis_labels = y_axis_labels self.num_plots = max(len(self.x_axis_labels), len(self.y_axis_labels)) self.linewidth = linewidth self.dock_area = DockArea() self.docks = [] self.plot_frames = [] self._setup_ui() self._layout() def save_dock_layout(self): """ Save the current layout of the docks and the plot settings. When running the GUI you can access this function by right-clicking in the widget area to bring up the context menu and selecting "Save Dock Layout" """ layout = { 'docks': self.dock_area.saveState(), 'plots': [i.plot_frame.plot_widget.saveState() for i in self.plot_frames] } with open(self.dock_layout_filename, 'w') as f: f.write(json.dumps(layout)) log.info('Saved dock layout to file %s' % self.dock_layout_filename) def save_dock_action(self): save_dock_action = QtWidgets.QWidgetAction(self) save_dock_action.setText("Save Dock Layout") save_dock_action.triggered.connect(self.save_dock_layout) return save_dock_action def contextMenuEvent(self, event): position = event.pos() # Create menu outside pyqtgraph.PlotWidget position if isinstance(self.childAt(position), (PlotWidget, DockLabel, QtWidgets.QLabel, PlotFrame)): menu = QtWidgets.QMenu(self) menu.addAction(self.save_dock_action()) menu.exec(self.mapToGlobal(position)) def _setup_ui(self): for i in range(self.num_plots): # Set the default label for current dock from x_axis_labels and y_axis_labels # However, if list is shorter than num_plots, repeat last item in the list. x_label = self.x_axis_labels[min(i, len(self.x_axis_labels) - 1)] y_label = self.y_axis_labels[min(i, len(self.y_axis_labels) - 1)] dock = Dock("Dock " + str(i + 1), closable=False, size=(200, 50)) self.dock_area.addDock(dock) self.plot_frames.append( PlotWidget("Results Graph", self.procedure_class.DATA_COLUMNS, x_label, y_label, linewidth=self.linewidth)) self.plot_frames[i].plot_frame.plot_widget.scene().contextMenu.append( self.save_dock_action()) dock.addWidget(self.plot_frames[i]) self.docks.append(dock) def _layout(self): vbox = QtWidgets.QVBoxLayout(self) vbox.setSpacing(0) vbox.addWidget(self.dock_area) self.setLayout(vbox) # Load dock layout file if it exists in the directory of the current procedure if path.exists(self.dock_layout_filename): with open(self.dock_layout_filename, 'r') as f: dock_layout = f.read() layout = json.loads(dock_layout) docks = layout['docks'] plots = layout['plots'] # Make sure number of plots in the file matches num_plots if len(plots) == self.num_plots: self.dock_area.restoreState(docks) for idx, i in enumerate(self.plot_frames): i.plot_frame.plot_widget.restoreState(plots[idx]) log.info('Loaded dock layout from file %s' % self.dock_layout_filename) else: log.warning( 'Number of displayed docks does not match number of docks in layout file %s' % self.dock_layout_filename) def new_curve(self, results, color=pg.intColor(0), **kwargs): if 'pen' not in kwargs: kwargs['pen'] = pg.mkPen(color=color, width=self.linewidth) if 'antialias' not in kwargs: kwargs['antialias'] = False curves = [] for i in range(self.num_plots): curves.append(self.plot_frames[i].new_curve(results, color=color, **kwargs)) return curves def clear(self): for i in range(self.num_plots): self.plot_frames[i].plot.clear() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/estimator_widget.py0000644000175100001770000002141414623331163024270 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 inspect import signature from datetime import datetime, timedelta from ..thread import StoppableQThread from ..Qt import QtCore, QtWidgets from .sequencer_widget import SequenceEvaluationError log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class EstimatorThread(StoppableQThread): new_estimates = QtCore.Signal(list) def __init__(self, get_estimates_callable): StoppableQThread.__init__(self) self._get_estimates = get_estimates_callable self.delay = 2 def __del__(self): self.wait() def run(self): self._should_stop.clear() while not self._should_stop.wait(self.delay): estimates = self._get_estimates() self.new_estimates.emit(estimates) class EstimatorWidget(QtWidgets.QWidget): """ Widget that allows to display up-front estimates of the measurement procedure. This widget relies on a `get_estimates` method of the :class:`Procedure` class. `get_estimates` is expected to return a list of tuples, where each tuple contains two strings: a label and the estimate. If the :class:`SequencerWidget` is also used, it is possible to ask for the current sequencer or its length by asking for two keyword arguments in the Implementation of the `get_estimates` function: `sequence` and `sequence_length`, respectively. """ provide_sequence = False provide_sequence_length = False number_of_estimates = 0 sequencer = None def __init__(self, parent=None): super().__init__(parent) self._parent = parent self.check_get_estimates_signature() self.update_thread = EstimatorThread(self.get_estimates) self.update_thread.new_estimates.connect(self.display_estimates) self._setup_ui() self._layout() self.update_estimates() self.update_box.setCheckState(QtCore.Qt.CheckState.PartiallyChecked) def check_get_estimates_signature(self): """ Method that checks the signature of the get_estimates function. It checks which input arguments are allowed and, if the output is correct for the EstimatorWidget, stores the number of estimates. """ # Check function arguments proc = self._parent.make_procedure() call_signature = signature(proc.get_estimates) if "sequence" in call_signature.parameters: self.provide_sequence = True if "sequence_length" in call_signature.parameters: self.provide_sequence_length = True estimates = self.get_estimates() # Check if the output of the function is acceptable raise_error = True if isinstance(estimates, (list, tuple)): if all([isinstance(est, (tuple, list)) for est in estimates]): if all([len(est) == 2 for est in estimates]): raise_error = False if raise_error: raise TypeError( "If implemented, the get_estimates function is expected to" "return an int or float representing the estimated duration," "or a list of tuples of strings, where each tuple represents" "an estimate containing two string: the first is a label for" "the estimate, the second is the estimate itself." ) # Store the number of estimates self.number_of_estimates = len(estimates) def _setup_ui(self): self.line_edits = list() for idx in range(self.number_of_estimates): qlb = QtWidgets.QLabel(self) qle = QtWidgets.QLineEdit(self) qle.setEnabled(False) qle.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) self.line_edits.append((qlb, qle)) # Add a checkbox for continuous updating self.update_box = QtWidgets.QCheckBox(self) self.update_box.setTristate(True) self.update_box.stateChanged.connect(self._set_continuous_updating) # Add a button for instant updating self.update_button = QtWidgets.QPushButton("Update", self) self.update_button.clicked.connect(self.update_estimates) def _layout(self): f_layout = QtWidgets.QFormLayout(self) for row in self.line_edits: f_layout.addRow(*row) update_hbox = QtWidgets.QHBoxLayout() update_hbox.addWidget(self.update_box) update_hbox.addWidget(self.update_button) f_layout.addRow("Update continuously", update_hbox) def get_estimates(self): """ Method that makes a procedure with the currently entered parameters and returns the estimates for these parameters. """ # Make a procedure procedure = self._parent.make_procedure() kwargs = dict() sequence = None sequence_length = None if hasattr(self._parent, "sequencer"): try: sequence = self._parent.sequencer.get_sequence() except SequenceEvaluationError: sequence_length = 0 else: sequence_length = len(sequence) if self.provide_sequence: kwargs["sequence"] = sequence if self.provide_sequence_length: kwargs["sequence_length"] = sequence_length estimates = procedure.get_estimates(**kwargs) if isinstance(estimates, (int, float)): estimates = self._estimates_from_duration(estimates, sequence_length) return estimates def update_estimates(self): """ Method that gets and displays the estimates. Implemented for connecting to the 'update'-button. """ estimates = self.get_estimates() self.display_estimates(estimates) def display_estimates(self, estimates): """ Method that updates the shown estimates for the given set of estimates. :param estimates: The set of estimates to be shown in the form of a list of tuples of (2) strings """ if len(estimates) != self.number_of_estimates: raise ValueError( "Number of estimates changed after initialisation " "(from %d to %d)." % (self.number_of_estimates, len(estimates)) ) for idx, estimate in enumerate(estimates): self.line_edits[idx][0].setText(estimate[0]) self.line_edits[idx][1].setText(estimate[1]) def _estimates_from_duration(self, duration, sequence_length): estimates = list() estimates.append(("Duration", "%d s" % int(duration))) if hasattr(self._parent, "sequencer"): estimates.append(("Sequence length", str(sequence_length))) estimates.append(("Sequence duration", "%d s" % int(sequence_length * duration))) estimates.append(('Measurement finished at', str(datetime.now() + timedelta( seconds=duration))[:-7])) if hasattr(self._parent, "sequencer"): estimates.append(('Sequence finished at', str(datetime.now() + timedelta( seconds=duration * sequence_length))[:-7])) return estimates def _set_continuous_updating(self): state = self.update_box.checkState() self.update_thread.stop() self.update_thread.join() if state == QtCore.Qt.CheckState.Unchecked: pass elif state == QtCore.Qt.CheckState.PartiallyChecked: self.update_thread.delay = 2 self.update_thread.start() elif state == QtCore.Qt.CheckState.Checked: self.update_thread.delay = 0.1 self.update_thread.start() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/fileinput_widget.py0000644000175100001770000001400214623331163024253 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, QtWidgets from .filename_widget import FilenameLineEdit from .directory_widget import DirectoryLineEdit log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class FileInputWidget(QtWidgets.QWidget): """ Widget for controlling where the data of an experiment will be stored. The widget consists of a field for the filename (:class:`~pymeasure.display.widgets.filename_widget.FilenameLineEdit`), a field for the directory (:class:`~pymeasure.display.widgets.directory_widget.DirectoryLineEdit`), and a checkbox to control whether the measurement is stored. """ _extensions = ["csv", "txt"] _filename_fixed = False def __init__(self, parent=None): super().__init__(parent) self._setup_ui() self._layout() def _setup_ui(self): self.writefile_toggle = QtWidgets.QCheckBox('Save data', self) self.writefile_toggle.setLayoutDirection(QtCore.Qt.RightToLeft) self.writefile_toggle.setChecked(True) self.writefile_toggle.stateChanged.connect(self.set_input_fields_enabled) self.writefile_toggle.setToolTip( "Control whether the measurement is saved to a file with the filename that is\n" "specified in the field below (checked) or not (unchecked; the data is stored\n" "in a temporary file)." ) self.filename_input = FilenameLineEdit(self.parent().procedure_class, parent=self) self.directory_input = DirectoryLineEdit(parent=self) def _layout(self): vbox = QtWidgets.QVBoxLayout() vbox.setContentsMargins(0, 0, 0, 0) filename_label = QtWidgets.QLabel(self) filename_label.setText('Filename') filename_label.setToolTip(self.filename_input.toolTip()) filename_box = QtWidgets.QHBoxLayout() filename_box.addWidget(filename_label) filename_box.addWidget(self.writefile_toggle) vbox.addLayout(filename_box) vbox.addWidget(self.filename_input) directory_label = QtWidgets.QLabel(self) directory_label.setText('Directory') vbox.addWidget(directory_label) vbox.addWidget(self.directory_input) self.setLayout(vbox) @property def directory(self): """String controlling the directory where the file will be stored.""" return self.directory_input.text() @directory.setter def directory(self, value): self.directory_input.setText(str(value)) @property def filename(self): """String controlling the filename that is shown in the filename input field.""" return self.filename_input.text() @filename.setter def filename(self, value): self.filename_input.setText(str(value)) @property def filename_base(self): """String containing the base of the filename with which the file will be stored. Can only be read. """ filename_split = self.filename.rsplit('.', 1) if len(filename_split) > 1 and filename_split[1] in self.extensions: return filename_split[0] else: return self.filename @property def filename_extension(self): """String containing the file extension with which the file will be stored. Can only be read. """ filename_split = self.filename.rsplit('.', 1) if len(filename_split) > 1 and filename_split[1] in self.extensions: return filename_split[1] else: return self.extensions[0] @property def extensions(self): """List of extensions that are recognized by the widget. The first value of this list will be used as default value in case no extension is provided in the filename input field. """ return self._extensions @extensions.setter def extensions(self, value): self._extensions = [ext.lstrip('.') for ext in value] self.filename_input.set_tool_tip() @property def store_measurement(self): """Boolean controlling whether the measurement will be stored.""" return self.writefile_toggle.isChecked() @store_measurement.setter def store_measurement(self, value): self.writefile_toggle.setChecked(bool(value)) @property def filename_fixed(self): """Boolean controlling whether the filename input field is frozen. If `True`, the filename field will be visible but disabled (i.e., grayed out).""" return self._filename_fixed @filename_fixed.setter def filename_fixed(self, value): self._filename_fixed = value # Reassess if the input fields should be enabled or disabled self.set_input_fields_enabled(self.store_measurement) def set_input_fields_enabled(self, state): self.filename_input.setEnabled(bool(state) and not self.filename_fixed) self.directory_input.setEnabled(bool(state)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/filename_widget.py0000644000175100001770000001444214623331163024044 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 textwrap from ..Qt import QtCore, QtWidgets, QtGui log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class FilenameLineEdit(QtWidgets.QLineEdit): """ Widget that allows to choose a filename. A completer is implemented for quick completion of placeholders """ def __init__(self, procedure_class, parent=None): super().__init__("DATA", parent=parent) self.placeholders = procedure_class.placeholder_names() self.placeholders.extend(["date", "time"]) completer = PlaceholderCompleter(self.placeholders) self.setCompleter(completer) validator = FilenameValidator(self.placeholders, self) self.setValidator(validator) self.set_tool_tip() def set_tool_tip(self): ext = self.parent().extensions[0] extensions = ("'." + "', '.".join(self.parent().extensions[:-1]) + f"', or '.{self.parent().extensions[-1]}'") placeholders = "'" + "';\n- '".join(self.placeholders) + "'" self.setToolTip("\n".join(textwrap.wrap( "The filename of the file to which the measurement will be stored. Placeholders (in " "standard python format, i.e.: '{variable name:formatspec}') will be replaced by " f"the respective value. The extension '.{ext}' will be appended, unless an extension " f"(one of {extensions}) is recognized. Additionally, an index number ('_#') is " "added to ensure the uniqueness of the filename.", width=100)) + f"\nValid placeholders are:\n- {placeholders}." ) class PlaceholderCompleter(QtWidgets.QCompleter): def __init__(self, placeholders): super().__init__() self.placeholders = placeholders self.setCompletionMode(QtWidgets.QCompleter.CompletionMode.PopupCompletion) self.setModelSorting(QtWidgets.QCompleter.ModelSorting.CaseInsensitivelySortedModel) self.setCaseSensitivity(QtCore.Qt.CaseInsensitive) self.setFilterMode(QtCore.Qt.MatchContains) def splitPath(self, path): if path.endswith("{"): options = [path + placeholder + "}" for placeholder in self.placeholders] model = QtCore.QStringListModel(options) self.setModel(model) elif path.count("{") == path.count("}"): # Clear the autocomplete options self.setModel(QtCore.QStringListModel()) return [path] class FilenameValidator(QtGui.QValidator): def __init__(self, placeholders, parent): self.parent = parent self.placeholders = placeholders self.full_placeholder = re.compile(r"{([^{}:]*)(:[^{}]*)?}") self.half_placeholder = re.compile(r"{([^{}:]*)(:[^{}]*)?$") self.valid_filename = re.compile(r"^[^<>:\"/\\|?*{}]*$") super().__init__() def fixup(self, input): half_placeholder = self.half_placeholder.findall(input) if half_placeholder: input = input + "}" return input def validate(self, input, pos): test_input = input full_placeholders = self.full_placeholder.findall(input) half_placeholder = self.half_placeholder.findall(input) test_input = self.full_placeholder.sub("_plchldr_", test_input) test_input = self.half_placeholder.sub("_plchldr", test_input) valid_filename = self.valid_filename.fullmatch(test_input) # Determine state of input if not valid_filename: state = QtGui.QValidator.Invalid elif half_placeholder: state = QtGui.QValidator.Intermediate else: state = QtGui.QValidator.Acceptable # Control the warning for the invalid placeholders incorrect_placeholders = [p for p in full_placeholders if p[0] not in self.placeholders] if incorrect_placeholders: if not self.parent.actions(): pixmapi = QtWidgets.QStyle.StandardPixmap.SP_MessageBoxCritical icon = self.parent.style().standardIcon(pixmapi) self.parent.addAction(icon, self.parent.ActionPosition.TrailingPosition) # Add tooltip to show which placeholders are not valid act = self.parent.actions()[0] marked_input = input for placeholder in [f"{{{p[0] + p[1]}}}" for p in incorrect_placeholders]: marked_input = marked_input.replace( placeholder, f"{placeholder}" ) act.setToolTip( "

" "The input filename contains placeholders with
invalid variable names:
" " - '" + "',
- '".join([p[0] for p in incorrect_placeholders]) + "'." "

Received input:
" + marked_input + "

" ) else: # Remove action, if it exists if self.parent.actions(): assert len(self.parent.actions()) == 1, ( "More than 1 action defined, not sure " "which to remove." ) self.parent.removeAction(self.parent.actions()[0]) return state, input, pos ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/image_frame.py0000644000175100001770000000430114623331163023146 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ..curves import ResultsImage from ..Qt import QtCore from .plot_frame import PlotFrame log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ImageFrame(PlotFrame): """ Extends :class:`PlotFrame` to plot also axis Z using colors """ ResultsClass = ResultsImage z_axis_changed = QtCore.Signal(str) def __init__(self, x_axis, y_axis, z_axis=None, refresh_time=0.2, check_status=True, parent=None): super().__init__(x_axis, y_axis, refresh_time, check_status, parent) self.change_z_axis(z_axis) def change_z_axis(self, axis): for item in self.plot.items: if isinstance(item, self.ResultsClass): item.z = axis item.update_data() 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/image_widget.py0000644000175100001770000001002614623331163023340 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pyqtgraph as pg from ..curves import ResultsImage from ..Qt import QtCore, QtWidgets from .tab_widget import TabWidget from .image_frame import ImageFrame log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ImageWidget(TabWidget, QtWidgets.QWidget): """ Extends the :class:`ImageFrame` to allow different columns of the data to be dynamically chosen """ def __init__(self, name, columns, x_axis, y_axis, z_axis=None, refresh_time=0.2, check_status=True, parent=None): super().__init__(name, 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 = QtWidgets.QLabel(self) self.columns_z_label.setMaximumSize(QtCore.QSize(45, 16777215)) self.columns_z_label.setText('Z Axis:') self.columns_z = QtWidgets.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 = QtWidgets.QVBoxLayout(self) vbox.setSpacing(0) hbox = QtWidgets.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_curve(self, results, color=pg.intColor(0), **kwargs): """ Creates a new image """ image = ResultsImage(results, wdg=self, x=self.image_frame.x_axis, y=self.image_frame.y_axis, z=self.image_frame.z_axis, **kwargs ) return image def update_z_column(self, index): axis = self.columns_z.itemText(index) self.image_frame.change_z_axis(axis) def load(self, curve): curve.z = self.columns_z.currentText() curve.update_data() self.plot.addItem(curve) def remove(self, curve): self.plot.removeItem(curve) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/inputs_widget.py0000644000175100001770000001626714623331163023615 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 functools import partial from ..inputs import BooleanInput, IntegerInput, ListInput, ScientificInput, StringInput from ..Qt import QtWidgets, QtCore from ...experiment import parameters log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class InputsWidget(QtWidgets.QWidget): """ Widget wrapper for various :doc:`inputs` """ # tuple of Input classes that do not need an external label NO_LABEL_INPUTS = (BooleanInput,) def __init__(self, procedure_class, inputs=(), parent=None, hide_groups=True, inputs_in_scrollarea=False): super().__init__(parent) self._procedure_class = procedure_class self._procedure = procedure_class() self._inputs = inputs self._setup_ui() self._layout(inputs_in_scrollarea) self._hide_groups = hide_groups self._setup_visibility_groups() 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, inputs_in_scrollarea): vbox = QtWidgets.QVBoxLayout(self) vbox.setSpacing(6) vbox.setContentsMargins(0, 0, 0, 0) self.labels = {} parameters = self._procedure.parameter_objects() for name in self._inputs: if not isinstance(getattr(self, name), self.NO_LABEL_INPUTS): label = QtWidgets.QLabel(self) label.setText("%s:" % parameters[name].name) vbox.addWidget(label) self.labels[name] = label vbox.addWidget(getattr(self, name)) if inputs_in_scrollarea: scroll_area = QtWidgets.QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setFrameStyle(QtWidgets.QScrollArea.Shape.NoFrame) scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) inputs = QtWidgets.QWidget(self) inputs.setLayout(vbox) inputs.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) scroll_area.setWidget(inputs) vbox = QtWidgets.QVBoxLayout(self) vbox.setContentsMargins(0, 0, 0, 0) vbox.addWidget(scroll_area, 1) self.setLayout(vbox) def _setup_visibility_groups(self): groups = {} parameters = self._procedure.parameter_objects() for name in self._inputs: parameter = parameters[name] group_state = {g: True for g in parameter.group_by} for group_name, condition in parameter.group_by.items(): if group_name not in self._inputs or group_name == name: continue if isinstance(getattr(self, group_name), BooleanInput): # Adjust the boolean condition to a condition suitable for a checkbox condition = bool(condition) if group_name not in groups: groups[group_name] = [] groups[group_name].append((name, condition, group_state)) for group_name, group in groups.items(): toggle = partial(self.toggle_group, group_name=group_name, group=group) group_el = getattr(self, group_name) if isinstance(group_el, BooleanInput): group_el.toggled.connect(toggle) toggle(group_el.isChecked()) elif isinstance(group_el, StringInput): group_el.textChanged.connect(toggle) toggle(group_el.text()) elif isinstance(group_el, (IntegerInput, ScientificInput)): group_el.valueChanged.connect(toggle) toggle(group_el.value()) elif isinstance(group_el, ListInput): group_el.currentTextChanged.connect(toggle) toggle(group_el.currentText()) else: raise NotImplementedError( f"Grouping based on {group_name} ({group_el}) is not implemented.") def toggle_group(self, state, group_name, group): for (name, condition, group_state) in group: if callable(condition): group_state[group_name] = condition(state) else: group_state[group_name] = (state == condition) visible = all(group_state.values()) if self._hide_groups: getattr(self, name).setHidden(not visible) else: getattr(self, name).setDisabled(not visible) if name in self.labels: if self._hide_groups: self.labels[name].setHidden(not visible) else: self.labels[name].setDisabled(not visible) 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/log_widget.py0000644000175100001770000001275314623331163023050 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ..log import LogHandler from ..Qt import QtWidgets, QtCore, QtGui from .tab_widget import TabWidget log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class HTMLFormatter(logging.Formatter): level_colors = { "DEBUG": "DarkGray", # "INFO": "Black", "WARNING": "DarkOrange", "ERROR": "Red", "CRITICAL": "DarkRed", } html_replacements = { "\r\n": "
", "\n": "
", "\r": "
", " ": " " * 2, "\t": " " * 4, } def format(self, record): formatted = super().format(record) # Apply color if a level-color is defined if record.levelname in self.level_colors: formatted = f"{formatted}" # ensure newlines and indents are preserved for replacement in self.html_replacements.items(): formatted = formatted.replace(*replacement) # Prepend the level as HTML comment formatted = f"{formatted}" return formatted class LogWidget(TabWidget, QtWidgets.QWidget): """ Widget to display logging information in GUI It is recommended to include this widget in all subclasses of :class:`ManagedWindowBase` """ fmt = '%(asctime)s : %(message)s (%(levelname)s)' datefmt = '%m/%d/%Y %I:%M:%S %p' tab_widget = None tab_index = None _blink_qtimer = QtCore.QTimer() _blink_color = None _blink_state = False def __init__(self, name, parent=None, fmt=None, datefmt=None): if fmt is not None: self.fmt = fmt if datefmt is not None: self.datefmt = datefmt super().__init__(name, parent) self._setup_ui() self._layout() # Setup blinking self._blink_qtimer.timeout.connect(self._blink) self.handler.connect(self._blinking_start) def _setup_ui(self): self.view = QtWidgets.QPlainTextEdit() self.view.setReadOnly(True) self.handler = LogHandler() self.handler.setFormatter(HTMLFormatter( fmt=self.fmt, datefmt=self.datefmt, )) self.handler.connect(self.view.appendHtml) def _layout(self): vbox = QtWidgets.QVBoxLayout(self) vbox.setSpacing(0) vbox.addWidget(self.view) self.setLayout(vbox) def _blink(self): self.tab_widget.tabBar().setTabTextColor( self.tab_index, QtGui.QColor("black" if self._blink_state else self._blink_color) ) self._blink_state = not self._blink_state def _blinking_stop(self, index): if index == self.tab_index: self._blink_qtimer.stop() self._blink_state = True self._blink() self._blink_color = None self.tab_widget.setTabIcon(self.tab_index, QtGui.QIcon()) def _blinking_start(self, message): # Delayed setup, since only now the widget is added to the TabWidget if self.tab_widget is None: self.tab_widget = self.parent().parent() self.tab_index = self.tab_widget.indexOf(self) self.tab_widget.tabBar().setIconSize(QtCore.QSize(12, 12)) self.tab_widget.tabBar().currentChanged.connect(self._blinking_stop) if message.startswith("") or message.startswith(""): error = True elif message.startswith(""): error = False else: # no blinking return # Check if the current tab is actually the log-tab if self.tab_widget.currentIndex() == self.tab_index: self._blinking_stop(self.tab_widget.currentIndex()) return # Define color and icon based on severity # If already red, this should not be updated if not self._blink_color == "red": self._blink_color = "red" if error else "darkorange" pixmapi = QtWidgets.QStyle.StandardPixmap.SP_MessageBoxCritical if \ error else QtWidgets.QStyle.StandardPixmap.SP_MessageBoxWarning icon = self.style().standardIcon(pixmapi) self.tab_widget.setTabIcon(self.tab_index, icon) # Start timer self._blink_qtimer.start(500) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/plot_frame.py0000644000175100001770000001215514623331163023050 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pyqtgraph as pg from ..curves import ResultsCurve, Crosshairs from ..Qt import QtCore, QtWidgets from ...experiment import Procedure log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class PlotFrame(QtWidgets.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.Signal() ResultsClass = ResultsCurve x_axis_changed = QtCore.Signal(str) y_axis_changed = QtCore.Signal(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(QtWidgets.QFrame.Shape.StyledPanel) self.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) self.setMidLineWidth(1) vbox = QtWidgets.QVBoxLayout(self) self.plot_widget = pg.PlotWidget(self, background='#ffffff') vbox.addWidget(self.plot_widget) self.setLayout(vbox) self.plot = self.plot_widget.getPlotItem() style = dict(self.LABEL_STYLE, justify='right') if "font-size" in style: # LabelItem wants the size as 'size' rather than 'font-size' style["size"] = style.pop("font-size") self.coordinates = pg.LabelItem("", parent=self.plot, **style) self.coordinates.anchor(itemPos=(1, 1), parentPos=(1, 1), offset=(0, 3)) self.crosshairs = Crosshairs(self.plot, pen=pg.mkPen(color='#AAAAAA', style=QtCore.Qt.PenStyle.DashLine)) self.crosshairs.coordinates.connect(self.update_coordinates) self.timer = QtCore.QTimer(self) 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(f"({x:g}, {y:g})") def update_curves(self): for item in self.plot.items: if isinstance(item, self.ResultsClass): if self.check_status: if item.results.procedure.status == Procedure.RUNNING: item.update_data() else: item.update_data() 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, self.ResultsClass): item.x = axis item.update_data() 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, self.ResultsClass): item.y = axis item.update_data() 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/plot_widget.py0000644000175100001770000001272014623331163023237 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pyqtgraph as pg from ..curves import ResultsCurve from ..Qt import QtCore, QtWidgets from .tab_widget import TabWidget from .plot_frame import PlotFrame log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class PlotWidget(TabWidget, QtWidgets.QWidget): """ Extends :class:`PlotFrame` to allow different columns of the data to be dynamically chosen """ def __init__(self, name, columns, x_axis=None, y_axis=None, refresh_time=0.2, check_status=True, linewidth=1, parent=None): super().__init__(name, parent) self.columns = columns self.refresh_time = refresh_time self.check_status = check_status self.linewidth = linewidth 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 = QtWidgets.QLabel(self) self.columns_x_label.setMaximumSize(QtCore.QSize(45, 16777215)) self.columns_x_label.setText('X Axis:') self.columns_y_label = QtWidgets.QLabel(self) self.columns_y_label.setMaximumSize(QtCore.QSize(45, 16777215)) self.columns_y_label.setText('Y Axis:') self.columns_x = QtWidgets.QComboBox(self) self.columns_y = QtWidgets.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, parent=self, ) 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 = QtWidgets.QVBoxLayout(self) vbox.setSpacing(0) hbox = QtWidgets.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=self.linewidth) if 'antialias' not in kwargs: kwargs['antialias'] = False curve = ResultsCurve(results, wdg=self, 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) def load(self, curve): curve.x = self.columns_x.currentText() curve.y = self.columns_y.currentText() curve.update_data() self.plot.addItem(curve) def remove(self, curve): self.plot.removeItem(curve) def set_color(self, curve, color): """ Change the color of the pen of the curve """ curve.set_color(color) def preview_widget(self, parent=None): """ Return a widget suitable for preview during loading """ return PlotWidget("Plot preview", self.columns, self.plot_frame.x_axis, self.plot_frame.y_axis, parent=parent, ) def clear_widget(self): self.plot.clear() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/results_dialog.py0000644000175100001770000001207114623331163023735 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from ..Qt import QtCore, QtWidgets from ...experiment.results import Results log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ResultsDialog(QtWidgets.QFileDialog): """ Widget that displays a dialog box for loading a past experiment run. It shows a preview of curves from the results file when selected in the dialog box. This widget used by the `open_experiment` method in :class:`ManagedWindowBase` class """ def __init__(self, procedure_class, widget_list=(), parent=None): super().__init__(parent) self.procedure_class = procedure_class self.widget_list = widget_list self.setOption(QtWidgets.QFileDialog.Option.DontUseNativeDialog, True) self._setup_ui() def _setup_ui(self): preview_tab = QtWidgets.QTabWidget() param_vbox = QtWidgets.QVBoxLayout() metadata_vbox = QtWidgets.QVBoxLayout() param_vbox_widget = QtWidgets.QWidget() metadata_vbox_widget = QtWidgets.QWidget() self.preview_widget_list = [] # Add preview tabs as appropriate for widget in self.widget_list: preview_widget = widget.preview_widget(parent=self) if preview_widget: self.preview_widget_list.append(preview_widget) vbox = QtWidgets.QVBoxLayout() vbox_widget = QtWidgets.QWidget() vbox.addWidget(preview_widget) vbox_widget.setLayout(vbox) preview_tab.addTab(vbox_widget, preview_widget.name) self.preview_param = QtWidgets.QTreeWidget() param_header = QtWidgets.QTreeWidgetItem(["Name", "Value"]) self.preview_param.setHeaderItem(param_header) self.preview_param.setColumnWidth(0, 150) self.preview_param.setAlternatingRowColors(True) self.preview_metadata = QtWidgets.QTreeWidget() param_header = QtWidgets.QTreeWidgetItem(["Name", "Value"]) self.preview_metadata.setHeaderItem(param_header) self.preview_metadata.setColumnWidth(0, 150) self.preview_metadata.setAlternatingRowColors(True) param_vbox.addWidget(self.preview_param) metadata_vbox.addWidget(self.preview_metadata) param_vbox_widget.setLayout(param_vbox) metadata_vbox_widget.setLayout(metadata_vbox) preview_tab.addTab(param_vbox_widget, "Run Parameters") preview_tab.addTab(metadata_vbox_widget, "Metadata") self.layout().addWidget(preview_tab, 0, 5, 4, 1) self.layout().setColumnStretch(5, 1) self.setMinimumSize(900, 500) self.resize(900, 500) self.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFiles) self.currentChanged.connect(self.update_preview) def update_preview(self, filename): # Add preview tabs as appropriate if not os.path.isdir(filename) and filename != '': try: results = Results.load(str(filename)) except ValueError: return except Exception as e: raise e for widget in self.preview_widget_list: widget.clear_widget() widget.load(widget.new_curve(results)) self.preview_param.clear() for key, param in results.procedure.parameter_objects().items(): new_item = QtWidgets.QTreeWidgetItem([param.name, str(param)]) self.preview_param.addTopLevelItem(new_item) self.preview_param.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder) self.preview_metadata.clear() for key, metadata in results.procedure.metadata_objects().items(): new_item = QtWidgets.QTreeWidgetItem([metadata.name, str(metadata)]) self.preview_metadata.addTopLevelItem(new_item) self.preview_metadata.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/sequencer_widget.py0000644000175100001770000005230114623331163024252 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from functools import partial from inspect import signature from collections import ChainMap from ..Qt import QtCore, QtWidgets, QtGui from ...experiment.sequencer import SequenceHandler, SequenceEvaluationError log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class SequencerTreeModel(QtCore.QAbstractItemModel): """ Model for sequencer data :param header: List of string representing header data :param data: data associated with the model :param parent: A QWidget that QT will give ownership of this Widget to. """ def __init__(self, data, header=("Level", "Parameter", "Sequence"), parent=None): super().__init__(parent) self.header = header self.root = data def add_node(self, parameter, parent=None): """ Add a row in the sequencer """ if parent is None: parent = self.createIndex(-1, -1) idx = len(self.root.children(parent)) parent_seq_item = parent.internalPointer() self.beginInsertRows(parent, idx, idx) seq_item, child_row = self.root.add_node(parameter, parent_seq_item) self.endInsertRows() return self.createIndex(child_row, 0, seq_item) def remove_node(self, index): """ Remove a row in the sequencer """ children = self.rowCount(index) seq_item = index.internalPointer() # Remove children from last to first while (children > 0): child = children - 1 children_seq_item = self.root.get_children(seq_item, child) self.remove_node(self.createIndex(child, 0, children_seq_item)) children = self.rowCount(index) self.beginRemoveRows(index.parent(), index.row(), index.row()) parent_seq_item, parent_row = self.root.remove_node(seq_item) self.endRemoveRows() return self.createIndex(parent_row, 0, parent_seq_item) def flags(self, index): """ Set the flags for the item at the given QModelIndex. Here, we just set all indexes to enabled, and selectable. """ if not index.isValid(): return_value = QtCore.Qt.ItemFlag.NoItemFlags else: return_value = QtCore.Qt.ItemFlag.ItemIsEnabled | \ QtCore.Qt.ItemFlag.ItemIsSelectable if index.column() >= 1: return_value |= QtCore.Qt.ItemFlag.ItemIsEditable return return_value def data(self, index, role): """ Return the data to display for the given index and the given role. This method should not be called directly. This method is called implicitly by the QTreeView that is displaying us, as the way of finding out what to display where. """ if not index.isValid(): return elif not role == QtCore.Qt.ItemDataRole.DisplayRole: return data = index.internalPointer()[index.column()] if not isinstance(data, QtCore.QObject): data = str(data) return data def index(self, row, col, parent): """ Return a QModelIndex instance pointing the row and column underneath the parent given. This method should not be called directly. This method is called implicitly by the QTreeView that is displaying us, as the way of finding out what to display where. """ if not parent or not parent.isValid(): parent_data = None else: parent_data = parent.internalPointer() seq_item = self.root.get_children(parent_data, row) child = seq_item if child is None: return QtCore.QModelIndex() index = self.createIndex(row, col, child) return index def parent(self, index=None): """ Return the index of the parent of a given index. If index is not supplied, return an invalid QModelIndex. :param index: QModelIndex optional. :return: """ if not index or not index.isValid(): return QtCore.QModelIndex() child = index.internalPointer() parent, parent_row = self.root.get_parent(child) if parent is None: return QtCore.QModelIndex() index = self.createIndex(parent_row, 0, parent) return index def rowCount(self, parent): """ Return the number of children of a given parent. If an invalid QModelIndex is supplied, return the number of children under the root. :param parent: QModelIndex """ if parent.column() > 0: return 0 if not parent.isValid(): parent = None else: parent = parent.internalPointer() rows = len(self.root.children(parent)) return rows def columnCount(self, parent): """ Return the number of columns in the model header. The parent parameter exists only to support the signature of QAbstractItemModel. """ return len(self.header) def headerData(self, section, orientation, role): """ Return the header data for the given section, orientation and role. This method should not be called directly. This method is called implicitly by the QTreeView that is displaying us, as the way of finding out what to display where. """ if orientation == QtCore.Qt.Orientation.Horizontal and \ role == QtCore.Qt.ItemDataRole.DisplayRole: return self.header[section] def setData(self, index, value, role=QtCore.Qt.ItemDataRole.EditRole): return_value = False if role == QtCore.Qt.ItemDataRole.EditRole: return_value = self.root.set_data(index.internalPointer(), index.row(), index.column(), value) if return_value: self.dataChanged.emit(index, index, [role]) return return_value def visit_tree(self, parent): """ Return a generator to enumerate all the nodes in the tree """ parent_data = None if parent: parent_data = parent.internalPointer() for row, child in enumerate(self.root.children(parent_data)): node = self.index(row, 0, parent) if node.isValid(): yield node yield from self.visit_tree(node) def __iter__(self): yield from self.visit_tree(None) def save(self, file_obj): self.root.save(file_obj) def load(self, file_obj, append=False): # Since we are loading new data, following Qt documentation, # we call beginResetModel to inform that any previous data reported # from the model is now invalid and has to be queried for again. # This also means that the current item and any selected items will become invalid. self.beginResetModel() try: self.root.load(file_obj, append=append) except SequenceEvaluationError as e: log.error(f"Error during sequence loading: {e}") # Complete model reset operation self.endResetModel() class ComboBoxDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, owner, choices): super().__init__(owner) self.items = choices def createEditor(self, parent, option, index): editor = QtWidgets.QComboBox(parent) editor.addItems(self.items) return editor def setEditorData(self, editor, index): value = index.data(QtCore.Qt.ItemDataRole.DisplayRole) num = self.items.index(value) editor.setCurrentIndex(num) def setModelData(self, editor, model, index): value = editor.currentText() model.setData(index, value, QtCore.Qt.ItemDataRole.EditRole) def updateEditorGeometry(self, editor, option, index): editor.setGeometry(option.rect) class ExpressionValidator(QtGui.QValidator): def validate(self, input_string, pos): return_value = QtGui.QValidator.State.Acceptable try: SequenceHandler.eval_string(input_string, log_enabled=False) except SequenceEvaluationError: return_value = QtGui.QValidator.State.Intermediate return (return_value, input_string, pos) class LineEditDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): editor = QtWidgets.QLineEdit(parent) editor.setValidator(ExpressionValidator()) return editor def setEditorData(self, editor, index): value = index.data(QtCore.Qt.ItemDataRole.DisplayRole) editor.setText(value) def setModelData(self, editor, model, index): value = editor.text() model.setData(index, value, QtCore.Qt.ItemDataRole.EditRole) def updateEditorGeometry(self, editor, option, index): editor.setGeometry(option.rect) class SequencerTreeView(QtWidgets.QTreeView): def __init__(self, parent=None): super().__init__(parent) self.width = self.viewport().size().width() def save(self, filename=None): self.model().save(filename) def selectRow(self, index): selection_model = self.selectionModel() selection_model.select(index, QtCore.QItemSelectionModel.SelectionFlag.Clear) for column in range(self.model().columnCount(index)): idx = self.model().createIndex(index.row(), column, index.internalPointer()) selection_model.select(idx, QtCore.QItemSelectionModel.SelectionFlag.Select) def activate_persistent_editor(self): model = self.model() for item in model: index = model.index(item.row(), 1, model.parent(item)) self.openPersistentEditor(index) def setModel(self, model): super().setModel(model) self.setColumnWidth(0, int(0.7 * self.width)) self.setColumnWidth(1, int(0.9 * self.width)) self.setColumnWidth(2, int(0.9 * self.width)) self.model().layoutChanged.connect(self.activate_persistent_editor) self.model().modelReset.connect(self.activate_persistent_editor) class SequenceDialog(QtWidgets.QFileDialog): """ Widget that displays a dialog box for loading or saving a sequence tree. It also shows a preview of sequence tree in the dialog box :param save: True if we are saving a file. Default False. """ def __init__(self, save=False, parent=None): """ Generate a serialized form of the sequence tree :param save: True if we are saving a file. Default False. :param parent: Passed on to QtWidgets.QWidget. Default is None """ super().__init__(parent) self.save = save self.setOption(QtWidgets.QFileDialog.Option.DontUseNativeDialog, True) self._setup_ui() def _setup_ui(self): preview_tab = QtWidgets.QTabWidget() vbox = QtWidgets.QVBoxLayout() param_vbox = QtWidgets.QVBoxLayout() vbox_widget = QtWidgets.QWidget() param_vbox_widget = QtWidgets.QWidget() self.preview_param = SequencerTreeView(parent=self) triggers = QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers self.preview_param.setEditTriggers(triggers) param_vbox.addWidget(self.preview_param) vbox_widget.setLayout(vbox) param_vbox_widget.setLayout(param_vbox) preview_tab.addTab(param_vbox_widget, "Sequence Parameters") if not self.save: self.append_checkbox = QtWidgets.QCheckBox("Append to existing sequence") self.append_checkbox.setCheckState(QtCore.Qt.CheckState.Checked) self.layout().addWidget(self.append_checkbox) self.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFile) else: self.setAcceptMode(QtWidgets.QFileDialog.AcceptMode.AcceptSave) self.setFileMode(QtWidgets.QFileDialog.FileMode.AnyFile) self.layout().addWidget(preview_tab, 0, 5, 4, 1) self.layout().setColumnStretch(5, 1) self.setMinimumSize(900, 500) self.resize(900, 500) self.currentChanged.connect(self.update_preview) def update_preview(self, filename): if not os.path.isdir(filename) and filename != '': with open(filename, 'r') as file_object: data = SequenceHandler(file_obj=file_object) tree_model = SequencerTreeModel(data=data) self.preview_param.setModel(tree_model) self.preview_param.expandAll() class SequencerWidget(QtWidgets.QWidget): """ Widget that allows to generate a sequence of measurements It allows sweeping parameters and moreover, one can write a simple text file to easily load a sequence. Sequences can also be saved Currently requires a queue function of the :class:`ManagedWindow` to have a "procedure" argument. :param inputs: List of strings representing the parameters name """ def __init__(self, inputs=None, sequence_file=None, parent=None): super().__init__(parent) self._parent = parent self._check_queue_signature() # 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() self.data = SequenceHandler(list(self.names_inv.keys())) self.tree.setModel(SequencerTreeModel(data=self.data)) if sequence_file is not None: self.load_sequence(filename=sequence_file) def _check_queue_signature(self): """ Check if the call signature of the implementation of the`ManagedWindow.queue` method accepts the `procedure` keyword argument, which is required for using the sequencer. """ call_signature = signature(self._parent.queue) if 'procedure' not in call_signature.parameters: raise AttributeError( "The queue method of of the ManagedWindow does not accept the 'procedure'" "keyword argument. Accepting this keyword argument is required when using" "the 'SequencerWidget'." ) 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()} self.names_choices = list(sorted(self.names_inv.keys())) def _setup_ui(self): self.tree = SequencerTreeView(self) self.tree.setHeaderHidden(False) self.tree.setItemDelegateForColumn(1, ComboBoxDelegate(self, self.names_choices)) self.tree.setItemDelegateForColumn(2, LineEditDelegate(self)) self.load_seq_button = QtWidgets.QPushButton("Load sequence") self.load_seq_button.clicked.connect(self.load_sequence) self.load_seq_button.setToolTip("Load a sequence from a file.") self.save_seq_button = QtWidgets.QPushButton("Save sequence") self.save_seq_button.clicked.connect(self.save_sequence) self.save_seq_button.setToolTip("Save a sequence to a file.") self.queue_button = QtWidgets.QPushButton("Queue sequence") self.queue_button.clicked.connect(self.queue_sequence) self.add_root_item_btn = QtWidgets.QPushButton("Add root item") self.add_root_item_btn.clicked.connect( partial(self._add_tree_item, level=0) ) self.add_tree_item_btn = QtWidgets.QPushButton("Add item") self.add_tree_item_btn.clicked.connect(self._add_tree_item) self.remove_tree_item_btn = QtWidgets.QPushButton("Remove item") self.remove_tree_item_btn.clicked.connect(self._remove_selected_tree_item) def _layout(self): btn_box = QtWidgets.QHBoxLayout() btn_box.addWidget(self.load_seq_button) btn_box.addWidget(self.save_seq_button) btn_box_2 = QtWidgets.QHBoxLayout() btn_box_2.addWidget(self.add_root_item_btn) btn_box_2.addWidget(self.add_tree_item_btn) btn_box_2.addWidget(self.remove_tree_item_btn) btn_box_3 = QtWidgets.QHBoxLayout() btn_box_3.addWidget(self.queue_button) vbox = QtWidgets.QVBoxLayout(self) vbox.setSpacing(6) vbox.addLayout(btn_box) vbox.addWidget(self.tree) vbox.addLayout(btn_box_2) vbox.addLayout(btn_box_3) self.setLayout(vbox) def _add_tree_item(self, *, level=None, parameter=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 """ selected = self.tree.selectionModel().selection().indexes() if len(selected) >= 1 and level != 0: parent = selected[0] else: parent = None if parameter is None: parameter = self.names_choices[0] model = self.tree.model() node_index = model.add_node(parameter=parameter, parent=parent) self.tree.openPersistentEditor(model.index(node_index.row(), 1, parent)) self.tree.expandAll() self.tree.selectRow(node_index) def _remove_selected_tree_item(self): """ Remove the selected item (and any child items) from the sequence tree. """ selected = self.tree.selectionModel().selection().indexes() if len(selected) == 0: return node_index = self.tree.model().remove_node(selected[0]) if node_index.isValid(): self.tree.selectRow(node_index) def get_sequence(self): return self.data.parameters_sequence(self.names_inv) 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.get_sequence() except SequenceEvaluationError: 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: QtWidgets.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 save_sequence(self): dialog = SequenceDialog(save=True) if dialog.exec(): filename = dialog.selectedFiles()[0] with open(filename, 'w') as file_object: self.tree.save(file_object) log.info('Saved sequence file %s' % filename) def load_sequence(self, *, filename=None): """ Load a sequence from a .txt file. :param filename: Filename (string) of the to-be-loaded file. """ append_flag = False if (filename is None) or (filename == ''): dialog = SequenceDialog() if dialog.exec(): append_flag = dialog.append_checkbox.checkState() == QtCore.Qt.CheckState.Checked filenames = dialog.selectedFiles() filename = filenames[0] else: return with open(filename, 'r') as file_object: self.tree.model().load(file_object, append=append_flag) self.tree.expandAll() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/tab_widget.py0000644000175100001770000000503014623331163023023 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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()) class TabWidget: """ Utility class to define default implementation for some basic methods. When defining a widget to be used in subclasses of :class:`ManagedWindowBase`, users should inherit from this class and provide an implementation of these methods """ def __init__(self, name, *args, **kwargs): super().__init__(*args, **kwargs) self.name = name def new_curve(self, *args, **kwargs): """ Create a new curve """ return None def load(self, curve): """ Add curve to widget """ pass def remove(self, curve): """ Remove curve from widget """ pass def set_color(self, curve, color): """ Set color for widget """ pass def preview_widget(self, parent=None): """ Return a Qt widget suitable for preview during loading See also :class:`ResultsDialog` If the object returned is not None, then it should have also an attribute `name`. """ return None def clear_widget(self): """ Clear widget content Behaviour is widget specific and it is currently used in preview mode """ return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/widgets/table_widget.py0000644000175100001770000006020214623331163023346 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 functools import partial import numpy as np import pyqtgraph as pg import pandas as pd from ..Qt import QtCore, QtWidgets, QtGui from .tab_widget import TabWidget from ...experiment import Procedure SORT_ROLE = QtCore.Qt.ItemDataRole.UserRole + 1 SORTING_ENABLED = True # Allow to disable sorting, for debug purpose only log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ResultsTable(QtCore.QObject): """ Class representing a panda dataframe """ data_changed = QtCore.Signal(int, int, int, int) def __init__(self, results, color, column_index=None, force_reload=False, wdg=None, **kwargs): super().__init__() self.results = results self.color = color self.force_reload = force_reload self.last_row_count = 0 self.wdg = wdg self.column_index = column_index self.data = self.results.data self._started = False @property def data(self): return self._data @data.setter def data(self, value): self._data = value if self.column_index is not None: self._data = self._data.set_index(self.column_index) else: self._data.reset_index() @property def rows(self): return self._data.shape[0] @property def columns(self): return self._data.shape[1] def init(self): self.last_row_count = 0 def start(self): self._started = True def stop(self): self._started = False def update_data(self): if not self._started: return if self.force_reload: self.results.reload() self.data = self.results.data current_row_count, columns = self._data.shape if (self.last_row_count < current_row_count): # Request cells content update self.data_changed.emit(self.last_row_count, 0, current_row_count - 1, columns - 1) self.last_row_count = current_row_count def set_color(self, color): self.color = color def set_index(self, index): self.column_index = index class PandasModelBase(QtCore.QAbstractTableModel): """ This class provided a model to manage multiple panda dataframes and display them as a single table. The multiple pandas dataframes are provided as ResultTable class instances and all of them share the same number of columns. There are some assumptions: - Series in the dataframe are identical, we call this number k - Series length can be different, we call this number l(x), where x=1..n The data can be presented as follow: - By column: each series in a separate column, in this case table shape will be: (k*n) x (max(l(x) x=1..n) - By row: column fixed to the number of series, in this case table shape will be: k x (sum of l(x) x=1..n) """ float_digits = 6 concat_axis = 0 def __init__(self, column_index=None, results_list=[], parent=None): super().__init__(parent) self.column_index = column_index self._init_data(results_list) def _init_data(self, results_list=None): if results_list is None: results_list = [] self.results_list = results_list self.row_count = self.pandas_row_count() self.column_count = self.pandas_column_count() def clear(self): self.beginResetModel() for results in self.results_list: results.stop() self._init_data() self.endResetModel() def add_results(self, results): if results not in self.results_list: self.beginResetModel() self.results_list.append(results) results.data_changed.connect(partial(self._data_changed, results)) self.endResetModel() results.init() results.start() results.update_data() def remove_results(self, results): self.beginResetModel() if results in self.results_list: self.results_list.remove(results) self.row_count = self.pandas_row_count() self.column_count = self.pandas_column_count() results.stop() self.endResetModel() def rowCount(self, parent=None): return self.row_count def columnCount(self, parent=None): return self.column_count def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole): if index.isValid() and role in (QtCore.Qt.ItemDataRole.DisplayRole, SORT_ROLE): try: results, row, col = self.translate_to_local(index.row(), index.column()) value = results.data.iat[row, col] column_type = results.data.dtypes.iat[col] # Cast to column type value_render = column_type.type(value) except (IndexError, ValueError, TypeError): value = np.nan value_render = "" if isinstance(value_render, np.float64): # limit maximum number of decimal digits displayed value_render = f"{value_render:.{self.float_digits:d}g}" if role == QtCore.Qt.ItemDataRole.DisplayRole: return str(value_render) elif role == SORT_ROLE: # For numerical sort return float(value) return None def _get_new_rows_columns(self, results, r1, c1, r2, c2): new_rows = self.pandas_row_count() - self.row_count new_rows_start = self.row_count new_columns = self.pandas_column_count() - self.column_count new_columns_start = self.column_count return new_rows, new_rows_start, new_columns, new_columns_start def headerData(self, section, orientation, role): """ Return header information Override method from QAbstractTableModel """ if role == QtCore.Qt.ItemDataRole.DisplayRole: if orientation == QtCore.Qt.Orientation.Horizontal: return str(self.horizontal_header[section]) if orientation == QtCore.Qt.Orientation.Vertical: return str(self.vertical_header[section]) elif role == QtCore.Qt.ItemDataRole.DecorationRole: if orientation == QtCore.Qt.Orientation.Horizontal: return self.horizontal_header_decoration(section) if orientation == QtCore.Qt.Orientation.Vertical: return self.vertical_header_decoration(section) return None def _data_changed(self, results, r1, c1, r2, c2): """ Internal method to handle data changed signal """ rows, rows_start, columns, columns_start = \ self._get_new_rows_columns(results, r1, c1, r2, c2) if rows or columns: if rows > 0: # New rows available self.beginInsertRows(QtCore.QModelIndex(), rows_start, rows_start + rows - 1) self.row_count += rows self.endInsertRows() if columns > 0: # New columns available self.beginInsertColumns(QtCore.QModelIndex(), columns_start, columns_start + columns - 1) self.column_count += columns self.endInsertColumns() else: top_bottom = self._get_row_column_set(results, r1, c1, r2, c2) for r1, c1, r2, c2 in top_bottom: self.dataChanged.emit(self.createIndex(r1, c1), self.createIndex(r2, c2)) def pandas_row_count(self): """ Return total row count of the panda dataframes The value depends on the geometry selected to display dataframes """ raise Exception("Subclass should implement it") def pandas_column_count(self): """ Return total column count of the panda dataframes The value depends on the geometry selected to display dataframes """ raise Exception("Subclass should implement it") def _get_row_column_set(self, results, r1, c1, r2, c2): """ Return set of top/bottom coordinates for data changed event. Depending on the geometry of the table a single top/bottom could be translated in multiple tops/bottoms """ raise Exception("Subclass should implement it") def translate_to_local(self, row, col): """ Translate from full table coordinate to single results coordinates """ raise Exception("Subclass should implement it") def translate_to_global(self, results, row, col): """ Translate from single results coordinates to full table coordinates """ raise Exception("Subclass should implement it") @property def horizontal_header(self): raise Exception("Subclass should implement it") @property def vertical_header(self): return range(self.row_count) def horizontal_header_decoration(self, section): return None def vertical_header_decoration(self, section): return None def export_df(self): df_list = [results.data for results in self.results_list] if not df_list: # Empty list df = None else: # Concatenate pandas data frames df = pd.concat(df_list, axis=self.concat_axis).replace(to_replace=np.nan, value="") return df def set_index(self, index): self.column_index = index # Update results list for r in self.results_list: r.stop() r.set_index(index) self.beginResetModel() for r in self.results_list: r.start() r.update_data() self.row_count = self.pandas_row_count() self.column_count = self.pandas_column_count() self.endResetModel() def copy_model(self, model_class): model = model_class(self.column_index, self.results_list[:]) return model class PandasModelByRow(PandasModelBase): concat_axis = 0 def pandas_row_count(self): rows = 0 for r in self.results_list: rows += r.rows return rows def pandas_column_count(self): cols = 0 if self.results_list: cols = self.results_list[0].columns return cols def _get_row_column_set(self, results, r1, c1, r2, c2): top = self.translate_to_global(results, r1, c1) bottom = self.translate_to_global(results, r2, c2) return (top + bottom), def translate_to_local(self, row, col): """ Translate from full table coordinate to single results coordinates """ for index, results in enumerate(self.results_list): if row < results.rows: break row -= results.rows return results, row, col def translate_to_global(self, results, row, col): """ Translate from single results coordinates to full table coordinates """ rows = 0 for res in self.results_list: if res == results: break rows += results.rows return rows + row, col @property def vertical_header(self): if self.column_index is None: header = range(self.row_count) else: header = [] for r in self.results_list: header.extend(r.data.index) return header @property def horizontal_header(self): if self.results_list: return self.results_list[0].data.columns else: return [] def vertical_header_decoration(self, section): results, _, _ = self.translate_to_local(section, 0) pixelmap = QtGui.QPixmap(6, 6) pixelmap.fill(results.color) return pixelmap class PandasModelByColumn(PandasModelBase): concat_axis = 1 def pandas_row_count(self): if self.column_index is None: return max([0] + [r.rows for r in self.results_list]) else: return len(self.vertical_header) def pandas_column_count(self): cols = 0 size = len(self.results_list) if size > 0: cols = self.results_list[0].columns * size return cols def _get_row_column_set(self, results, r1, c1, r2, c2): top_bottoms = [] for i in range(c1, c2 + 1): top = self.translate_to_global(results, r1, i) bottom = self.translate_to_global(results, r2, i) top_bottoms.append(top + bottom) return top_bottoms def translate_to_local(self, row, col): """ Translate from full table coordinate to single results coordinates """ columns = 0 for index, results in enumerate(self.results_list): if col < (columns + results.columns): break columns += results.columns if (self.column_index is not None): # Remap row to matching index entry when indexing is used try: index = self.vertical_header[row] row = list(results.data.index).index(index) except ValueError: row = None return results, row, col - columns def translate_to_global(self, results, row, col): """ Translate from single results coordinates to full table coordinates """ columns = 0 for res in self.results_list: if res == results: break columns += results.columns return row, col + columns @property def horizontal_header(self): size = len(self.results_list) if size: v = list(self.results_list[0].data.columns) return v * size else: return [] def horizontal_header_decoration(self, section): results, _, _ = self.translate_to_local(0, section) pixelmap = QtGui.QPixmap(6, 6) pixelmap.fill(results.color) return pixelmap @property def vertical_header(self): header = set([]) for r in self.results_list: header = header.union(set(r.data.index)) header = sorted(list(header)) return header class Table(QtWidgets.QTableView): """ Table format view of :class:`Experiment` objects """ supported_formats = { "CSV file (*.csv)": 'csv', "Excel file (*.xlsx)": 'excel', "HTML file (*.html *.htm)": 'html', "JSON file (*.json)": 'json', "LaTeX file (*.tex)": 'latex', "Markdown file (*.md)": 'markdown', "XML file (*.xml)": 'xml', } def __init__(self, refresh_time=0.2, check_status=True, force_reload=False, layout_class=PandasModelByColumn, column_index=None, float_digits=6, parent=None): super().__init__(parent) self.force_reload = force_reload self.float_digits = float_digits model = layout_class(column_index=column_index) self.setModel(model) self.horizontalHeader().setStyleSheet("font: bold;") self.sortByColumn(-1, QtCore.Qt.SortOrder.AscendingOrder) self.setSortingEnabled(True) self.horizontalHeader().setSectionsMovable(True) self.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.ResizeMode.ResizeToContents ) self.setup_context_menu() self.refresh_time = refresh_time self.check_status = check_status if self.refresh_time is not None: self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.update_tables) self.timer.start(int(self.refresh_time * 1e3)) def setModel(self, model): model.float_digits = self.float_digits if SORTING_ENABLED: proxyModel = QtCore.QSortFilterProxyModel(self) proxyModel.setSourceModel(model) model = proxyModel model.setSortRole(SORT_ROLE) super().setModel(model) def source_model(self): model = self.model() if SORTING_ENABLED: model = model.sourceModel() return model def export_action(self): df = self.source_model().export_df() if df is not None: formats = ";;".join(self.supported_formats.keys()) filename_and_ext = QtWidgets.QFileDialog.getSaveFileName( self, "Save File", "", formats, ) filename = filename_and_ext[0] ext = filename_and_ext[1] if filename: mode = self.supported_formats[ext] prefix = df.style if mode == "latex" else df getattr(prefix, 'to_' + mode)(filename) def refresh_action(self): self.update_tables() def copy_action(self): df = self.source_model().export_df() if df is not None: df.to_clipboard() def setup_context_menu(self): self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self.context_menu) self.copy = QtGui.QAction("Copy table data", self) self.copy.triggered.connect(self.copy_action) self.refresh = QtGui.QAction("Refresh table data", self) self.refresh.triggered.connect(self.refresh_action) self.export = QtGui.QAction("Export table data", self) self.export.triggered.connect(self.export_action) def context_menu(self, point): menu = QtWidgets.QMenu(self) menu.addAction(self.copy) menu.addAction(self.refresh) menu.addAction(self.export) menu.exec(self.mapToGlobal(point)) def update_tables(self, force=False): model = self.source_model() for item in model.results_list: if not self.check_status or force: item.update_data() else: if item.results.procedure.status == Procedure.RUNNING: item.update_data() def set_color(self, table, color): table.set_color(color) def add_table(self, table): model = self.source_model() model.add_results(table) def remove_table(self, table): model = self.source_model() model.remove_results(table) table.stop() if model.rowCount() == 0: # Empty table, reset sorting policy self.setSortingEnabled(False) self.sortByColumn(-1, QtCore.Qt.SortOrder.AscendingOrder) self.setSortingEnabled(True) def clear(self): model = self.source_model() model.clear() self.setSortingEnabled(False) self.sortByColumn(-1, QtCore.Qt.SortOrder.AscendingOrder) self.setSortingEnabled(True) def set_index(self, index): model = self.source_model() model.set_index(index) def set_model(self, model_class): """ Replace model with new instance of model_class """ model = self.source_model() new_model = model.copy_model(model_class) self.setModel(new_model) class TableWidget(TabWidget, QtWidgets.QWidget): """ Widget to display experiment data in a tabular format """ layout_class_map = { 'By Row': PandasModelByRow, 'By Column': PandasModelByColumn, } def __init__(self, name, columns, by_column=True, column_index=None, refresh_time=0.2, float_digits=6, check_status=True, parent=None): super().__init__(name, parent) self.columns = columns self.layout_names = list(self.layout_class_map.keys()) self.table_layout = self.layout_names[1] if by_column else self.layout_names[0] self.column_index = column_index self.refresh_time = refresh_time self.check_status = check_status self.float_digits = float_digits self._setup_ui() self._layout() def _setup_ui(self): self.column_index_label = QtWidgets.QLabel(self) self.column_index_label.setMaximumSize(QtCore.QSize(45, 16777215)) self.column_index_label.setText('Index:') self.layout_label = QtWidgets.QLabel(self) self.layout_label.setMaximumSize(QtCore.QSize(45, 16777215)) self.layout_label.setText('Layout:') self.column_index_combo = QtWidgets.QComboBox(self) self.layout = QtWidgets.QComboBox(self) self.column_index_combo.addItem('') for column in self.columns: self.column_index_combo.addItem(column) if self.column_index is not None: self.column_index_combo.setCurrentText(self.column_index) for key in self.layout_names: self.layout.addItem(key) self.layout.setCurrentText(self.table_layout) self.column_index_combo.activated.connect(self.update_column_index) self.layout.activated.connect(self.update_layout) self.table = Table(refresh_time=self.refresh_time, check_status=self.check_status, force_reload=False, layout_class=self.layout_class_map[self.table_layout], column_index=self.column_index, float_digits=self.float_digits, parent=self, ) def _layout(self): vbox = QtWidgets.QVBoxLayout(self) vbox.setSpacing(0) hbox = QtWidgets.QHBoxLayout() hbox.setSpacing(10) hbox.setContentsMargins(-1, 6, -1, 6) hbox.addWidget(self.column_index_label) hbox.addWidget(self.column_index_combo) hbox.addWidget(self.layout_label) hbox.addWidget(self.layout) vbox.addLayout(hbox) vbox.addWidget(self.table) self.setLayout(vbox) def update_layout(self, entry): model = self.layout.itemText(entry) self.table_layout = entry self.table.set_model(self.layout_class_map[model]) def update_column_index(self, entry): index = self.column_index_combo.itemText(entry) if index == '': index = None self.column_index = index self.table.set_index(index) def new_curve(self, results, color=pg.intColor(0), **kwargs): return ResultsTable(results, color, self.column_index, wdg=self, **kwargs) def load(self, table): self.table.add_table(table) def remove(self, table): self.table.remove_table(table) def set_color(self, table, color): """ Change the color of the pen of the curve """ self.table.set_color(table, color) def preview_widget(self, parent=None): """ Return a widget suitable for preview during loading """ by_column = False if self.table_layout == self.layout_names[0] else True return TableWidget("Table preview", columns=self.columns, by_column=by_column, refresh_time=None, check_status=False, float_digits=self.float_digits, parent=None, ) def clear_widget(self): self.table.clear() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3856053 pymeasure-0.14.0/pymeasure/display/windows/0000755000175100001770000000000014623331176020372 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/windows/__init__.py0000644000175100001770000000244114623331163022500 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .plotter_window import PlotterWindow from .managed_window import ManagedWindowBase, ManagedWindow from .managed_image_window import ManagedImageWindow ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/windows/managed_dock_window.py0000644000175100001770000001021314623331163024720 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ..widgets.dock_widget import DockWidget from ..widgets.log_widget import LogWidget from .managed_window import ManagedWindowBase log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ManagedDockWindow(ManagedWindowBase): """ Display experiment output with multiple docking windows with :class:`~pymeasure.display.widgets.dock_widget.DockWidget` class. :param procedure_class: procedure class describing the experiment (see :class:`~pymeasure.experiment.procedure.Procedure`) :param x_axis: the data column(s) for the x-axis of the plot. This may be a string or a list of strings from the data columns of the procedure. The list length determines the number of plots :param y_axis: the data column(s) for the y-axis of the plot. This may be a string or a list of strings from the data columns of the procedure. The list length determines the number of plots :param linewidth: linewidth for the displayed curves, default is 1 :param log_fmt: formatting string for the log-widget :param log_datefmt: formatting string for the date in the log-widget :param \\**kwargs: optional keyword arguments that will be passed to :class:`~pymeasure.display.windows.managed_window.ManagedWindowBase` """ def __init__(self, procedure_class, x_axis=None, y_axis=None, linewidth=1, log_fmt=None, log_datefmt=None, **kwargs): self.x_axis = x_axis self.y_axis = y_axis measure_quantities = [] # Expand x_axis if it is a list if isinstance(self.x_axis, list): measure_quantities += [*self.x_axis] self.x_axis_labels = self.x_axis # Change x_axis to a string from list for ResultsDialog self.x_axis = self.x_axis[0] else: self.x_axis_labels = [self.x_axis, ] measure_quantities.append(self.x_axis) # Expand y_axis if it is a list if isinstance(self.y_axis, list): measure_quantities += [*self.y_axis] self.y_axis_labels = self.y_axis # Change y_axis to a string from list for ResultsDialog self.y_axis = self.y_axis[0] else: self.y_axis_labels = [self.y_axis, ] measure_quantities.append(self.y_axis) self.log_widget = LogWidget("Experiment Log", fmt=log_fmt, datefmt=log_datefmt) self.dock_widget = DockWidget("Dock Tab", procedure_class, self.x_axis_labels, self.y_axis_labels, linewidth=linewidth) if "widget_list" not in kwargs: kwargs["widget_list"] = () kwargs["widget_list"] = kwargs["widget_list"] + (self.dock_widget, self.log_widget) super().__init__(procedure_class, **kwargs) self.browser_widget.browser.measured_quantities.update(measure_quantities) logging.getLogger().addHandler(self.log_widget.handler) log.setLevel(self.log_level) log.info("DockWindow connected to logging") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/windows/managed_image_window.py0000644000175100001770000000524314623331163025071 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 # 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 ..widgets import ( ImageWidget, ) from .managed_window import ManagedWindow log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ManagedImageWindow(ManagedWindow): """ Display experiment output with an :class:`~pymeasure.display.widgets.image_widget.ImageWidget` class. :param procedure_class: procedure class describing the experiment (see :class:`~pymeasure.experiment.procedure.Procedure`) :param x_axis: the data-column for the x-axis of the plot, cannot be changed afterwards for the image-plot :param y_axis: the data-column for the y-axis of the plot, cannot be changed afterwards for the image-plot :param z_axis: the initial data-column for the z-axis of the plot, can be changed afterwards :param \\**kwargs: optional keyword arguments that will be passed to :class:`~pymeasure.display.windows.managed_image_window.ManagedWindow` """ def __init__(self, procedure_class, x_axis, y_axis, z_axis=None, **kwargs): self.z_axis = z_axis self.image_widget = ImageWidget( "Image", procedure_class.DATA_COLUMNS, x_axis, y_axis, z_axis) if "widget_list" not in kwargs: kwargs["widget_list"] = () kwargs["widget_list"] = kwargs["widget_list"] + (self.image_widget,) super().__init__(procedure_class, x_axis=x_axis, y_axis=y_axis, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/windows/managed_window.py0000644000175100001770000007074214623331163023735 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 platform import subprocess import tempfile import shutil import pyqtgraph as pg from ..browser import BrowserItem from ..manager import Manager, Experiment from ..Qt import QtCore, QtWidgets, QtGui from ..widgets import ( PlotWidget, BrowserWidget, InputsWidget, LogWidget, ResultsDialog, SequencerWidget, FileInputWidget, EstimatorWidget, ) from ...experiment import Results, Procedure, unique_filename log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ManagedWindowBase(QtWidgets.QMainWindow): """ Base class for GUI experiment management . The ManagedWindowBase 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 ManagedWindowBase uses a Manager to control Workers in a Queue, and provides a simple interface. The :meth:`~pymeasure.display.windows.managed_window.ManagedWindowBase.queue` method must be overridden by the child class. The ManagedWindowBase allow user to define a set of widget that display information about the experiment. The information displayed may include: plots, tabular view, logging information,... This class is not intended to be used directly, but it should be subclassed to provide some appropriate widget list. Example of classes usable as element of widget list are: - :class:`~pymeasure.display.widgets.log_widget.LogWidget` - :class:`~pymeasure.display.widgets.plot_widget.PlotWidget` - :class:`~pymeasure.display.widgets.image_widget.ImageWidget` Of course, users can define its own widget making sure that inherits from :class:`~pymeasure.display.widgets.tab_widget.TabWidget`. Examples of ready to use classes inherited from ManagedWindowBase are: - :class:`~pymeasure.display.windows.managed_window.ManagedWindow` - :class:`~pymeasure.display.windows.managed_image_window.ManagedImageWindow` .. seealso:: Tutorial :ref:`tutorial-managedwindow` A tutorial and example on the basic configuration and usage of ManagedWindow. Parameters for :code:`__init__` constructor. :param procedure_class: procedure class describing the experiment (see :class:`~pymeasure.experiment.procedure.Procedure`) :param widget_list: list of widget to be displayed in the GUI :param inputs: list of :class:`~pymeasure.experiment.parameters.Parameter` instance variable names, which the display will generate graphical fields for :param displays: list of :class:`~pymeasure.experiment.parameters.Parameter` instance variable names displayed in the browser window :param log_channel: :code:`logging.Logger` instance to use for logging output :param log_level: logging level :param parent: Parent widget or :code:`None` :param sequencer: a boolean stating whether or not the sequencer has to be included into the window :param sequencer_inputs: either :code:`None` or a list of the parameter names to be scanned over. If no list of parameters is given, the parameters displayed in the manager queue are used. :param sequence_file: simple text file to quickly load a pre-defined sequence with the :code:`Load sequence` button :param inputs_in_scrollarea: boolean that display or hide a scrollbar to the input area :param enable_file_input: a boolean controlling whether a :class:`~pymeasure.display.widgets.fileinput_widget.FileInputWidget` to specify where the experiment's result will be saved is displayed (True, default) or not (False). This widget contains a field to enter the (base of the) filename (with or without extension; if absent, the extension will be appended). This field also allows for placeholders to use parameter-values and metadata-value in the filename. The widget also has a field to select the directory where the file is to be stored, and a toggle to control whether the data should be saved to the selected file, or not (i.e., to a temporary file instead). :param hide_groups: a boolean controlling whether parameter groups are hidden (True, default) or disabled/grayed-out (False) when the group conditions are not met. """ def __init__(self, procedure_class, widget_list=(), inputs=(), displays=(), log_channel='', log_level=logging.INFO, parent=None, sequencer=False, sequencer_inputs=None, sequence_file=None, inputs_in_scrollarea=False, enable_file_input=True, hide_groups=True, ): super().__init__(parent) app = QtCore.QCoreApplication.instance() app.aboutToQuit.connect(self.quit) self.procedure_class = procedure_class self.inputs = inputs self.hide_groups = hide_groups 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.enable_file_input = enable_file_input self.log = logging.getLogger(log_channel) self.log_level = log_level log.setLevel(log_level) self.log.setLevel(log_level) self.widget_list = widget_list # Check if the get_estimates function is reimplemented self.use_estimator = not self.procedure_class.get_estimates == Procedure.get_estimates # Validate DATA_COLUMNS fit pymeasure column header format Procedure.parse_columns(self.procedure_class.DATA_COLUMNS) self._setup_ui() self._layout() def _setup_ui(self): self.queue_button = QtWidgets.QPushButton('Queue', self) self.queue_button.clicked.connect(self._queue) self.abort_button = QtWidgets.QPushButton('Abort', self) self.abort_button.setEnabled(False) self.abort_button.clicked.connect(self.abort) self.browser_widget = BrowserWidget( self.procedure_class, self.displays, [], # This value will be patched by subclasses, if needed 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.ContextMenuPolicy.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, hide_groups=self.hide_groups, inputs_in_scrollarea=self.inputs_in_scrollarea, ) if self.enable_file_input: self.file_input = FileInputWidget(parent=self) self.manager = Manager(self.widget_list, 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 ) if self.use_estimator: self.estimator = EstimatorWidget( parent=self ) def _layout(self): self.main = QtWidgets.QWidget(self) inputs_dock = QtWidgets.QWidget(self) inputs_vbox = QtWidgets.QVBoxLayout(self.main) queue_abort_hbox = QtWidgets.QHBoxLayout() queue_abort_hbox.setSpacing(10) queue_abort_hbox.setContentsMargins(-1, 6, -1, 6) queue_abort_hbox.addWidget(self.queue_button) queue_abort_hbox.addWidget(self.abort_button) queue_abort_hbox.addStretch() inputs_vbox.addWidget(self.inputs) inputs_vbox.addSpacing(15) if self.enable_file_input: inputs_vbox.addWidget(self.file_input) inputs_vbox.addSpacing(15) inputs_vbox.addLayout(queue_abort_hbox) inputs_vbox.addStretch(0) inputs_dock.setLayout(inputs_vbox) dock = QtWidgets.QDockWidget('Input Parameters') dock.setWidget(inputs_dock) dock.setFeatures(QtWidgets.QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) self.addDockWidget(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea, dock) if self.use_sequencer: sequencer_dock = QtWidgets.QDockWidget('Sequencer') sequencer_dock.setWidget(self.sequencer) sequencer_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) self.addDockWidget(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea, sequencer_dock) if self.use_estimator: estimator_dock = QtWidgets.QDockWidget('Estimator') estimator_dock.setWidget(self.estimator) estimator_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) self.addDockWidget(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea, estimator_dock) self.tabs = QtWidgets.QTabWidget(self.main) for wdg in self.widget_list: self.tabs.addTab(wdg, wdg.name) splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical) splitter.addWidget(self.tabs) splitter.addWidget(self.browser_widget) vbox = QtWidgets.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 == QtCore.Qt.CheckState.Unchecked: for curve in experiment.curve_list: if curve: curve.wdg.remove(curve) else: for curve in experiment.curve_list: if curve: curve.wdg.load(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 = QtWidgets.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) # Save a copy of the datafile action_save = QtGui.QAction(menu) action_save.setText("Save Data File Copy") action_save.triggered.connect( lambda: self.save_experiment_copy(experiment.results.data_filename)) menu.addAction(action_save) # 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) # Delete action_delete = QtGui.QAction(menu) action_delete.setText("Delete Data File") if self.manager.is_running(): if self.manager.running_experiment() == experiment: # Experiment running action_delete.setEnabled(False) action_delete.triggered.connect(lambda: self.delete_experiment_data(experiment)) menu.addAction(action_delete) # 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 = QtWidgets.QMessageBox.question(self, 'Remove Graph', "Are you sure you want to remove the graph?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.Yes: self.manager.remove(experiment) def delete_experiment_data(self, experiment): reply = QtWidgets.QMessageBox.question(self, 'Delete Data', "Are you sure you want to delete this data file?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.Yes: self.manager.remove(experiment) os.unlink(experiment.data_filename) def show_experiments(self): root = self.browser.invisibleRootItem() for i in range(root.childCount()): item = root.child(i) item.setCheckState(0, QtCore.Qt.CheckState.Checked) def hide_experiments(self): root = self.browser.invisibleRootItem() for i in range(root.childCount()): item = root.child(i) item.setCheckState(0, QtCore.Qt.CheckState.Unchecked) def clear_experiments(self): self.manager.clear() def open_experiment(self): dialog = ResultsDialog(self.procedure_class, widget_list=self.widget_list) if dialog.exec(): filenames = dialog.selectedFiles() for filename in map(str, filenames): if filename in self.manager.experiments: QtWidgets.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) for curve in experiment.curve_list: if curve: curve.update_data() experiment.browser_item.progressbar.setValue(100) self.manager.load(experiment) log.info('Opened data file %s' % filename) def save_experiment_copy(self, source_filename): """Save a copy of the datafile to a selected folder and file. Primarily useful for experiments that are stored in a temporary file. """ dialog = QtWidgets.QFileDialog(self) dialog.setFileMode(QtWidgets.QFileDialog.AnyFile) dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave) dialog.setDefaultSuffix('.csv') if dialog.exec(): filename = dialog.selectedFiles()[0] shutil.copy2(source_filename, filename) log.info(f"Copied data from '{source_filename}' to '{filename}'.") def change_color(self, experiment): color = QtWidgets.QColorDialog.getColor( parent=self) if color.isValid(): pixelmap = QtGui.QPixmap(24, 24) pixelmap.fill(color) experiment.browser_item.setIcon(0, QtGui.QIcon(pixelmap)) for curve in experiment.curve_list: if curve: curve.wdg.set_color(curve, color=color) 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 _ = subprocess.Popen(['start', '', filename], shell=True) elif (system == 'Linux'): _ = subprocess.Popen(['xdg-open', filename]) elif (system == 'Darwin'): _ = 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, wdg, results, color=None, **kwargs): if color is None: color = pg.intColor(self.browser.topLevelItemCount() % 8) return wdg.new_curve(results, color=color, **kwargs) def new_experiment(self, results, curve=None): if curve is None: curve_list = [] for wdg in self.widget_list: new_curve = self.new_curve(wdg, results) if isinstance(new_curve, (tuple, list)): curve_list.extend(new_curve) else: curve_list.append(new_curve) else: curve_list = curve[:] curve_color = pg.intColor(0) for curve in curve_list: if hasattr(curve, 'color'): curve_color = curve.color break browser_item = BrowserItem(results, curve_color) return Experiment(results, curve_list, 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, checked): """ This method is a wrapper for the `self.queue` method to be connected to the `queue` button. It catches the positional argument that is passed when it is called by the button and calls the `self.queue` method without any arguments. """ self.queue() def queue(self, procedure=None): """ Queue a measurement based on the parameters in the input-widget. Semi-abstract method, which must be overridden by the child class if the filename- and directory-inputs are disabled. When filename- and directory inputs are enabled, overwriting is not required, but can be done for custom naming, input processing, or other features. 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. The optional `procedure` argument is not required for a basic implementation, but is required when the :class:`~pymeasure.display.widgets.sequencer_widget.SequencerWidget` is used. 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) """ # Check if the filename and the directory inputs are available if not self.enable_file_input: raise NotImplementedError("Queue method must be overwritten if the filename- and " "directory-inputs are disabled.") if procedure is None: procedure = self.make_procedure() if self.store_measurement: try: filename = unique_filename( self.directory, prefix=self.file_input.filename_base, datetimeformat="", procedure=procedure, ext=self.file_input.filename_extension, ) except KeyError as E: if not E.args[0].startswith("The following placeholder-keys are not valid:"): raise E from None log.error(f"Invalid filename provided: {E.args[0]}") return else: filename = tempfile.mktemp(prefix='TempFile_', suffix='.csv') results = Results(procedure, filename) experiment = self.new_experiment(results) self.manager.queue(experiment) 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: # noqa 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.enable_file_input: raise AttributeError("File-input widget not enabled (i.e., enable_file_input == False)") return self.file_input.directory @directory.setter def directory(self, value): if not self.enable_file_input: raise AttributeError("File-input widget not enabled (i.e., enable_file_input == False)") self.file_input.directory = value @property def filename(self): if not self.enable_file_input: raise AttributeError("File-input widget not enabled (i.e., enable_file_input == False)") return self.file_input.filename @filename.setter def filename(self, value): if not self.enable_file_input: raise AttributeError("File-input widget not enabled (i.e., enable_file_input == False)") self.file_input.filename = value @property def store_measurement(self): if not self.enable_file_input: raise AttributeError("File-input widget not enabled (i.e., enable_file_input == False)") return self.file_input.store_measurement @store_measurement.setter def store_measurement(self, value): if not self.enable_file_input: raise AttributeError("File-input widget not enabled (i.e., enable_file_input == False)") self.file_input.store_measurement = value class ManagedWindow(ManagedWindowBase): """ Display experiment output with an :class:`~pymeasure.display.widgets.plot_widget.PlotWidget` class. .. seealso:: Tutorial :ref:`tutorial-managedwindow` A tutorial and example on the basic configuration and usage of ManagedWindow. :param procedure_class: procedure class describing the experiment (see :class:`~pymeasure.experiment.procedure.Procedure`) :param x_axis: the initial data-column for the x-axis of the plot :param y_axis: the initial data-column for the y-axis of the plot :param linewidth: linewidth for the displayed curves, default is 1 :param log_fmt: formatting string for the log-widget :param log_datefmt: formatting string for the date in the log-widget :param \\**kwargs: optional keyword arguments that will be passed to :class:`~pymeasure.display.windows.managed_window.ManagedWindowBase` """ def __init__(self, procedure_class, x_axis=None, y_axis=None, linewidth=1, log_fmt=None, log_datefmt=None, **kwargs): self.x_axis = x_axis self.y_axis = y_axis self.log_widget = LogWidget("Experiment Log", fmt=log_fmt, datefmt=log_datefmt) self.plot_widget = PlotWidget("Results Graph", procedure_class.DATA_COLUMNS, self.x_axis, self.y_axis, linewidth=linewidth) self.plot_widget.setMinimumSize(100, 200) if "widget_list" not in kwargs: kwargs["widget_list"] = () kwargs["widget_list"] = kwargs["widget_list"] + (self.plot_widget, self.log_widget) super().__init__(procedure_class, **kwargs) # Setup measured_quantities once we know x_axis and y_axis self.browser_widget.browser.measured_quantities.update([self.x_axis, self.y_axis]) logging.getLogger().addHandler(self.log_widget.handler) # needs to be in Qt context? log.setLevel(self.log_level) log.info("ManagedWindow connected to logging") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/display/windows/plotter_window.py0000644000175100001770000000753514623331163024032 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pyqtgraph as pg from ..curves import ResultsCurve from ..Qt import QtCore, QtWidgets from ..widgets import ( PlotWidget, ) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class PlotterWindow(QtWidgets.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, linewidth=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 = QtWidgets.QWidget(self) vbox = QtWidgets.QVBoxLayout(self.main) vbox.setSpacing(0) hbox = QtWidgets.QHBoxLayout() hbox.setSpacing(6) hbox.setContentsMargins(-1, 6, -1, -1) file_label = QtWidgets.QLabel(self.main) file_label.setText('Data Filename:') self.file = QtWidgets.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("Plotter", columns, refresh_time=self.refresh_time, check_status=False, linewidth=linewidth) 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=linewidth), 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/errors.py0000644000175100001770000000241514623331163017117 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3896053 pymeasure-0.14.0/pymeasure/experiment/0000755000175100001770000000000014623331176017413 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/experiment/__init__.py0000644000175100001770000000316014623331163021520 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Metadata) from .procedure import Procedure, UnknownProcedure from .results import Results, unique_filename, replace_placeholders 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/experiment/config.py0000644000175100001770000000346714623331163021240 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 for key in config._sections['matplotlib.rcParams']: matplotlib.rcParams[key] = eval(config._sections['matplotlib.rcParams'][key]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/experiment/experiment.py0000644000175100001770000001741114623331163022145 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 tempfile import gc import numpy as np 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 log = logging.getLogger() log.addHandler(logging.NullHandler()) try: from IPython import display except ImportError: log.warning("IPython could not be imported") 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: """ 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: 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) 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 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/experiment/listeners.py0000644000175100001770000001026514623331163021775 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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(f"{self.__class__.__name__} has ZMQ Context: {self.context!r}") self.subscriber = self.context.socket(zmq.SUB) self.subscriber.setsockopt(zmq.SUBSCRIBE, topic.encode()) self.subscriber.connect('tcp://localhost:%d' % port) 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=lambda msg: (msg[0].decode(), cloudpickle.loads(msg[1])), flags=flags ) return topic, record def message_waiting(self): """Check if we have a message, wait at most until timeout.""" return self.poller.poll(self.timeout * 1000) # poll timeout is in ms def __repr__(self): return "<{}(port={},topic={},should_stop={})>".format( 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/experiment/parameters.py0000644000175100001770000005402514623331163022132 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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: """ 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 :param group_by: Defines the Parameter(s) that controls the visibility of the associated input; can be a string containing the Parameter name, a list of strings with multiple Parameter names, or a dict containing {"Parameter name": condition} pairs. :param group_condition: The condition for the group_by Parameter that controls the visibility of this parameter, provided as a value or a (lambda)function. If the group_by argument is provided as a list of strings, this argument can be either a single condition or a list of conditions. If the group_by argument is provided as a dict this argument is ignored. """ def __init__(self, name, default=None, ui_class=None, group_by=None, group_condition=True): self.name = name separator = ": " if separator in name: raise ValueError(f"The provided name argument '{name}' contains the " f"separator '{separator}'.") self._value = None if default is not None: self.value = default self.default = default self.ui_class = ui_class self._help_fields = [('units are', 'units'), 'default'] self.group_by = {} if isinstance(group_by, dict): self.group_by = group_by elif isinstance(group_by, str): self.group_by = {group_by: group_condition} elif isinstance(group_by, (list, tuple)) and all(isinstance(e, str) for e in group_by): if isinstance(group_condition, (list, tuple)): self.group_by = {g: c for g, c in zip(group_by, group_condition)} else: self.group_by = {g: group_condition for g in group_by} elif group_by is not None: raise TypeError("The provided group_by argument is not valid, should be either a " "string, a list of strings, or a dict with {string: condition} pairs.") @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 = self.convert(value) @property def cli_args(self): """ helper for command line interface parsing of parameters This property returns a list of data to help formatting a command line interface interpreter, the list is composed of the following elements: - index 0: default value - index 1: List of value to format an help string, that is either, the name of the fields to be documented or a tuple with (helps_string, field) - index 2: type """ return (self.default, self._help_fields, self.convert) def is_set(self): """ Returns True if the Parameter value is set """ return self._value is not None def convert(self, value): """ Convert user input to python data format Subclasses are expected to customize this method. Default implementation is the identity function :param value: value to be converted :return: converted value """ return value def __str__(self): return str(self._value) if self.is_set() else '' def __repr__(self): return "<{}(name={},value={},default={})>".format( 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 :param step: int step size for parameter's UI spinbox. If None, spinbox will have step disabled """ def __init__(self, name, units=None, minimum=-1e9, maximum=1e9, step=None, **kwargs): self.units = units self.minimum = int(minimum) self.maximum = int(maximum) super().__init__(name, **kwargs) self.step = int(step) if step else None self._help_fields.append('minimum') self._help_fields.append('maximum') def convert(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") return 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 "<{}(name={},value={},units={},default={})>".format( 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 """ def convert(self, value): if isinstance(value, str): if value.lower() == "true": value = True elif value.lower() == "false": value = False else: raise ValueError("BooleanParameter given string value of '%s'" % value) elif isinstance(value, (int, float)) and value in [0, 1]: value = bool(value) elif isinstance(value, bool): value = value else: raise ValueError("BooleanParameter given non-boolean value of " "type '%s'" % type(value)) return 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 :param step: step size for parameter's UI spinbox. If None, spinbox will have step disabled """ def __init__(self, name, units=None, minimum=-1e9, maximum=1e9, decimals=15, step=None, **kwargs): self.units = units self.minimum = minimum self.maximum = maximum super().__init__(name, **kwargs) self.decimals = decimals self.step = step self._help_fields.append('decimals') def convert(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") return 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 "<{}(name={},value={},units={},default={})>".format( 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): self._length = length self.units = units super().__init__(name, **kwargs) self._help_fields.append('_length') def convert(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: 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)) return 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 "<{}(name={},value={},units={},length={})>".format( 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. String representation of choices must be unique. :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): self.units = units if choices is not None: keys = [str(c) for c in choices] # check that string representation is unique if not len(keys) == len(set(keys)): raise ValueError( "String representation of choices is not unique!") self._choices = {k: c for k, c in zip(keys, choices)} else: self._choices = None super().__init__(name, **kwargs) self._help_fields.append(('choices are', 'choices')) def convert(self, value): if self._choices is None: raise ValueError("ListParameter cannot be set since " "allowed choices are set to None.") # 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 str(value) in self._choices.keys(): value = self._choices[str(value)] else: raise ValueError("Invalid choice for parameter. " "Must be one of %s" % str(self._choices)) return value @property def choices(self): """ Returns an immutable iterable of choices, or None if not set. """ return tuple(self._choices.values()) 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 def convert(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: 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 value[1] = abs(value[1]) return value @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 = f"{self._value[0]:g} +/- {self._value[1]:g}" 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 "<{}(name={},value={},units={},uncertaintyType={})>".format( self.__class__.__name__, self.name, self._value, self.units, self._utype.value) class Measurable: """ 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 class Metadata(object): """ Encapsulates the information for metadata of the experiment with information about the name, the fget function and the units, if supplied. 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. This returns (if a value is set) the value obtained from the `fget` (after evaluation) or a manually set value. Returns `None` if no value has been set :param name: The parameter name :param fget: The parameter fget function; can be provided as a callable, or as a string, in which case it is assumed to be the name of a method or attribute of the `Procedure` class in which the Metadata is defined. Passing a string also allows for nested attributes by separating them with a period (e.g. to access an attribute or method of an instrument) where only the last attribute can be a method. :param units: The parameter units :param default: The default value, in case no value is assigned or if no fget method is provided :param fmt: A string used to format the value upon writing it to a file. Default is "%s" """ def __init__(self, name, fget=None, units=None, default=None, fmt="%s"): self.name = name self.units = units self._value = default self.fget = fget self.fmt = fmt self.evaluated = False @property def value(self): if self.is_set(): return self._value else: raise ValueError("Metadata value is not set") def is_set(self): """ Returns True if the Parameter value is set """ return self._value is not None def evaluate(self, parent=None, new_value=None): if new_value is not None and self.fget is not None: raise ValueError("Metadata with a defined fget method" " cannot be manually assigned a value") elif new_value is not None: self._value = new_value elif self.fget is not None: self._value = self.eval_fget(parent) self.evaluated = True return self.value def eval_fget(self, parent): fget = self.fget if isinstance(fget, str): obj = parent for obj_name in fget.split('.'): obj = getattr(obj, obj_name) fget = obj if callable(fget): return fget() else: return fget def __str__(self): result = self.fmt % self.value if self.units is not None: result += " %s" % self.units return result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/experiment/procedure.py0000644000175100001770000003170314623331163021755 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 inspect from copy import deepcopy from importlib.machinery import SourceFileLoader import re from pint import UndefinedUnitError from .parameters import Parameter, Measurable, Metadata from pymeasure.units import ureg log = logging.getLogger() log.addHandler(logging.NullHandler()) class Procedure: """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() self._update_metadata() for key in kwargs: if key in self._parameters.keys(): setattr(self, key, kwargs[key]) log.info(f'Setting parameter {key} to {kwargs[key]}') self.gen_measurement() @staticmethod def parse_columns(columns): """Get columns with any units in parentheses. For each column, if there are matching parentheses containing text with no spaces, parse the value between the parentheses as a Pint unit. For example, "Source Voltage (V)" will be parsed and matched to :code:`Unit('volt')`. Raises an error if a parsed value is undefined in Pint unit registry. Return a dictionary of matched columns with their units. :param columns: List of columns to be parsed. :type record: dict :return: Dictionary of columns with Pint units. """ units_pattern = r"\((?P[\w/\(\)\*\t]+)\)" units = {} for column in columns: match = re.search(units_pattern, column) if match: try: units[column] = ureg.Quantity(match.groupdict()['units']).units except UndefinedUnitError: raise ValueError( f"Column \"{column}\" with unit \"{match.groupdict()['units']}\"" " is not defined in Pint registry. Check procedure " "DATA_COLUMNS contains valid Pint units.") return units 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, parameter in inspect.getmembers(self.__class__): if isinstance(parameter, Measurable): if parameter.measure: self.MEASURE.update({parameter.name: item}) if not self.DATA_COLUMNS: self.DATA_COLUMNS = Measurable.DATA_COLUMNS # Validate DATA_COLUMNS fit pymeasure column header format self.parse_columns(self.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, parameter in inspect.getmembers(self.__class__): 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 {} '{}' in {}".format( 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 '{}' does not belong to '{}'".format( name, repr(self))) def _update_metadata(self): """ Collects all the Metadata objects for the procedure and stores them in a meta dictionary so that the actual values can be set and used in their stead """ self._metadata = {} for item, metadata in inspect.getmembers(self.__class__): if isinstance(metadata, Metadata): self._metadata[item] = deepcopy(metadata) if metadata.is_set(): setattr(self, item, metadata.value) else: setattr(self, item, None) def evaluate_metadata(self): """ Evaluates all Metadata objects, fixing their values to the current value """ for item, metadata in self._metadata.items(): # Evaluate the metadata, fixing its value value = metadata.evaluate(parent=self, new_value=getattr(self, item)) # Make the value of the metadata easily accessible setattr(self, item, value) def metadata_objects(self): """ Returns a dictionary of all the Metadata objects """ return self._metadata def placeholder_objects(self): """ Collect all eligible placeholders (parameters & metadata) with their value in a dict. """ return {**self.parameter_objects(), **self.metadata_objects()} @classmethod def placeholder_names(cls): """ Collect the names of all eligible placeholders (parameters & metadata)""" placeholders = [] for _, item in inspect.getmembers(cls): if isinstance(item, Metadata) or isinstance(item, Parameter): placeholders.append(item.name) return list(set(placeholders)) 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 get_estimates(self): """ Function that returns estimates that are to be displayed by the EstimatorWidget. Must be reimplemented by subclasses. Should return an int or float representing the duration in seconds, or a list with a tuple for each estimate. The tuple should consists of two strings: the first will be used as the label of the estimate, the second as the displayed estimate. """ raise NotImplementedError('Must be reimplemented by subclasses') 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: 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/experiment/results.py0000644000175100001770000004443514623331163021474 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 import logging import os import re import sys from importlib import import_module from importlib.machinery import SourceFileLoader from datetime import datetime from string import Formatter import pandas as pd import pint from .procedure import Procedure, UnknownProcedure from pymeasure.units import ureg log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def replace_placeholders(string, procedure, date_format="%Y-%m-%d", time_format="%H:%M:%S"): """Replace placeholders in string with values from procedure parameters. Replaces the placeholders in the provided string with the values of the associated parameters, as provided by the procedure. This uses the standard python string.format syntax. Apart from the parameter in the procedure (which should be called by their full names) "date" and "time" are also added as optional placeholders. :param string: The string in which the placeholders are to be replaced. Python string.format syntax is used, e.g. "{Parameter Name}" to insert a FloatParameter called "Parameter Name", or "{Parameter Name:.2f}" to also specifically format the parameter. :param procedure: The procedure from which to get the parameter values. :param date_format: A string to represent how the additional placeholder "date" will be formatted. :param time_format: A string to represent how the additional placeholder "time" will be formatted. """ now = datetime.now() parameters = procedure.placeholder_objects() placeholders = {param.name: param.value for param in parameters.values()} placeholders["date"] = now.strftime(date_format) placeholders["time"] = now.strftime(time_format) # Check keys against available parameters invalid_keys = [i[1] for i in Formatter().parse(string) if i[1] is not None and i[1] not in placeholders] if invalid_keys: raise KeyError("The following placeholder-keys are not valid: '%s'; " "valid keys are: '%s'." % ( "', '".join(invalid_keys), "', '".join(placeholders.keys()) )) return string.format(**placeholders) def unique_filename(directory, prefix='DATA', suffix='', ext='csv', dated_folder=False, index=True, datetimeformat="%Y-%m-%d", procedure=None): """ Returns a unique filename based on the directory and prefix """ now = datetime.now() directory = os.path.abspath(directory) if procedure is not None: prefix = replace_placeholders(prefix, procedure) suffix = replace_placeholders(suffix, procedure) 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 = f"{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 = f"{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.units = Procedure.parse_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 """ line = [] for x in self.columns: value = record.get(x, float("nan")) if isinstance(value, (float, int, Decimal)) and type(value) is not bool: line.append(f"{value}") else: units = self.units.get(x, None) if units is not None: if isinstance(value, str): try: value = ureg.Quantity(value) except pint.UndefinedUnitError: log.warning( f"Value {value} for column {x} cannot be parsed to" f" unit {units}.") if isinstance(value, pint.Quantity): try: line.append(f"{value.m_as(units)}") except pint.DimensionalityError: line.append("nan") log.warning( f"Value {value} for column {x} does not have the " f"right unit {units}.") elif isinstance(value, bool): line.append("nan") log.warning( f"Boolean for column {x} does not have unit {units}.") else: line.append("nan") log.warning( f"Value {value} for column {x} does not have the right" f" type for unit {units}.") else: if isinstance(value, pint.Quantity): if value.units == ureg.dimensionless: line.append(f"{value.magnitude}") else: self.units[x] = value.to_base_units().units line.append(f"{value.m_as(self.units[x])}") log.info(f"Column {x} units was set to {self.units[x]}") else: line.append(f"{value}") return self.delimiter.join(line) def format_header(self): return self.delimiter.join(self.columns) class Results: """ 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._metadata_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{}: {}".format(parameter.name, str( parameter).encode("unicode_escape").decode("utf-8"))) h.append("Data:") self._header_count = len(h) h = [Results.COMMENT + line for line 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 def metadata(self): """ Returns a text header for the metadata to write into the datafile """ if not self.procedure.metadata_objects(): return m = ["Metadata:"] for _, metadata in self.procedure.metadata_objects().items(): value = str(metadata).encode("unicode_escape").decode("utf-8") m.append(f"\t{metadata.name}: {value}") self._metadata_count = len(m) m = [Results.COMMENT + line for line in m] # Comment each line return Results.LINE_BREAK.join(m) + Results.LINE_BREAK def store_metadata(self): """ Inserts the metadata header (if any) into the datafile """ c_header = self.metadata() if c_header is None: return for filename in self.data_filenames: with open(filename, 'r+') as f: contents = f.readlines() contents.insert(self._header_count - 1, c_header) f.seek(0) f.writelines(contents) self._header_count += self._metadata_count @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: 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") # 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: log.warning( f"Parameter \"{parameter.name}\" not found when loading " + f"'{procedure_class}', setting default value") setattr(procedure, name, parameter.default) procedure.refresh_parameters() # Enforce update of meta data # Fill the procedure with the metadata found for name, metadata in procedure.metadata_objects().items(): if metadata.name in parameters: value = parameters[metadata.name] setattr(procedure, name, value) # Set the value in the metadata metadata._value = value metadata.evaluated = True 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) as f: while not header_read: line = f.readline() if line.startswith(Results.COMMENT): header += line.strip('\t\v\n\r\f') + 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 ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/experiment/sequencer.py0000644000175100001770000003505014623331163021756 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 itertools import product import numpy as np log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class SequenceEvaluationError(Exception): """Raised when the evaluation of a sequence string goes wrong.""" pass class SequenceItem(object): """ Class representing a sequence row """ column_map = { 0: "level", 1: "parameter", 2: "expression", } def __init__(self, level, parameter, expression, parent): self.level = level self.parameter = parameter self.expression = expression self.parent = parent def __getitem__(self, idx): if idx in self.column_map: return getattr(self, self.column_map[idx]) else: return super().__getitem__(idx) def __setitem__(self, idx, value): if idx in self.column_map: return setattr(self, self.column_map[idx], value) else: return super().__setitem__(idx, value) def __str__(self): return "{} \"{}\", \"{}\"".format("-" * (self.level + 1), self.parameter, self.expression) class SequenceHandler: """ It represents a sequence, that is a tree of parameter sweep. A sequence can be loaded from a file or created programmatically with :meth:`~.add_node` and :meth:`~.remove_node` The internal representation is a nodes tree with each node composed of 3 elements: - Level: that is the distance from the root node - Parameter: A string that is the parameter name - Expression: A python expression which describes the list of values to be assumed by the Parameter. The syntax of the file is as follow: :: - "Parameter1", "(1,2,3)" -- "Parameter2", "(4,5,6)" --- "Parameter3", "(6,7,8)" - "Parameter4", "range(1,3)" In this case, the tree is composed of a root node with two children (Parameter1 and Parameter4) Parameter2 is the only child of Parameter1 and Parameter3 is the only child of Parameter2. Parameter4 has no child. Data is stored internally as a list where each item matches a row of the sequence file. Data can also be saved back to the file object provided. """ MAXDEPTH = 10 SAFE_FUNCTIONS = { 'range': range, 'sorted': sorted, 'list': list, 'arange': np.arange, 'linspace': np.linspace, 'arccos': np.arccos, 'arcsin': np.arcsin, 'arctan': np.arctan, 'arctan2': np.arctan2, 'ceil': np.ceil, 'cos': np.cos, 'cosh': np.cosh, 'degrees': np.degrees, 'e': np.e, 'exp': np.exp, 'fabs': np.fabs, 'floor': np.floor, 'fmod': np.fmod, 'frexp': np.frexp, 'hypot': np.hypot, 'ldexp': np.ldexp, 'log': np.log, 'log10': np.log10, 'modf': np.modf, 'pi': np.pi, 'power': np.power, 'radians': np.radians, 'sin': np.sin, 'sinh': np.sinh, 'sqrt': np.sqrt, 'tan': np.tan, 'tanh': np.tanh, } def __init__(self, valid_inputs=(), file_obj=None): self._sequences = [] self.valid_inputs = valid_inputs if file_obj: self.load(file_obj) @staticmethod def eval_string(string, name=None, depth=None, log_enabled=True): """ 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. :param log_enabled: Enable log messages. """ evaluated_string = None if len(string) > 0: try: evaluated_string = eval( string, {"__builtins__": None}, SequenceHandler.SAFE_FUNCTIONS ) except TypeError: if log_enabled: log.error("TypeError, likely a typo in one of the " + "functions for parameter '{}', depth {}".format( name, depth )) raise SequenceEvaluationError("TypeError, likely a typo") except SyntaxError: if log_enabled: log.error("SyntaxError, likely unbalanced brackets " + "for parameter '{}', depth {}".format(name, depth)) raise SequenceEvaluationError("SyntaxError, likely unbalanced brackets") except ValueError: if log_enabled: log.error("ValueError, likely wrong function argument " + "for parameter '{}', depth {}".format(name, depth)) raise SequenceEvaluationError("ValueError, likely wrong function argument") except Exception as e: raise SequenceEvaluationError(e) else: if log_enabled: log.error("No sequence entered for " + "for parameter '{}', depth {}".format(name, depth)) raise SequenceEvaluationError("No sequence entered") evaluated_string = np.array(evaluated_string) return evaluated_string def _get_idx(self, seq_item): """ Return the index and level of the list whose value correspond to sequence """ try: idx = self._sequences.index(seq_item) except ValueError: idx = -1 # Sequence not found, assuming idenx does not exist if idx < 0: level = -1 else: level = self._sequences[idx].level return idx, level def add_node(self, name, parent_seq_item=None): """ Add a node under the parent identified by parent_seq_item """ parent_idx, level = self._get_idx(parent_seq_item) seq_item = SequenceItem(level + 1, name, "", parent_seq_item) # Find position where to insert new row idx = parent_idx + 1 while idx < len(self._sequences): if self._sequences[idx].level <= level: break idx += 1 self._sequences.insert(idx, seq_item) return seq_item, self.get_children_order(seq_item) def remove_node(self, seq_item): """ Remove node identified by seq_item """ # if node identified by idx has children, we need to remove them first for child_seq_item in self.children(seq_item): self.remove_node(child_seq_item) self._sequences.remove(seq_item) return seq_item.parent, self.get_children_order(seq_item.parent) def children(self, seq_item): """ return a list of children of node identified by seq_item """ idx, current_level = self._get_idx(seq_item) child_list = [] idx += 1 while idx < len(self._sequences): if self._sequences[idx].level == (current_level + 1): child_list.append(self._sequences[idx]) if self._sequences[idx].level <= current_level: break idx += 1 return child_list def get_children(self, seq_item, index): """ Return the children of order index of the node seq_item """ child_list = self.children(seq_item) if index >= len(child_list): child = None else: child = child_list[index] return child def get_children_order(self, seq_item): """ Return the children order of the node identified by seq_item The children order is the index related to the parent's children list. :param seq_item: SequenceItem instance or None """ if seq_item is None: return -1 # Get parent's children list children_list = self.children(seq_item.parent) return children_list.index(seq_item) def get_parent(self, seq_item): """ Return parent of node identified by seq_item """ return seq_item.parent, self.get_children_order(seq_item.parent) def set_data(self, seq_item, row, column, value): """ Set data for node identified by seq_item """ idx, _ = self._get_idx(seq_item) if idx < 0: return False self._sequences[idx][column] = value return True def load(self, file_obj, append=False): """ Read and parse a sequence stored in a file. :params file_obj: file object :params append: flag to control whether to append to or replace current sequence """ _sequences = [] if append: _sequences += self._sequences current_parent = None pattern = re.compile("([-]+) \"(.*?)\", \"(.*?)\"") file_obj.seek(0) for line in file_obj: 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) parent_level = -1 if current_parent is None else current_parent.level if level == (parent_level + 1): pass elif (level <= parent_level): # Find parent current_parent = current_parent.parent while current_parent is not None: if level == (current_parent.level + 1): break current_parent = current_parent.parent else: raise SequenceEvaluationError("Invalid file format: level missing ?") if self.valid_inputs and parameter not in self.valid_inputs: error_message = f'Unexpected parameter name "{parameter:s}", ' + \ f'valid parameters name are {self.valid_inputs}' raise SequenceEvaluationError(error_message) data = SequenceItem(level, parameter, sequence, current_parent) current_parent = data _sequences.append(data) # No errors, update internal data self._sequences = _sequences def save(self, file_obj): """ Save modified sequence to file stream :param file_obj: file object """ file_obj.write("\n".join(str(item) for item in self._sequences)) def parameters_sequence(self, names_map=None): """ Generate a list of parameters from the sequence tree. :param names_map: an optional dict to map parameter name :return: A list of dictionaries. Each dictionary represents a parameters setting for running an experiment. """ sequences = [] current_sequence = [[] for i in range(self.MAXDEPTH)] temp_sequence = [[] for i in range(self.MAXDEPTH)] idx = 0 while (idx < len(self._sequences)): depth, parameter, seq = self._sequences[idx].level, \ self._sequences[idx].parameter, \ self._sequences[idx].expression values = self.eval_string(seq, parameter, depth) if names_map is not None: parameter = names_map[parameter] 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) idx += 1 next_depth = -1 if idx >= len(self._sequences) else self._sequences[idx].level 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/experiment/workers.py0000644000175100001770000001522614623331163021463 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 traceback from queue import Queue from .listeners import Recorder from .procedure import Procedure from .results import Results 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 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)) self.context = None self.publisher = None 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) # wait so that the socket will be ready before starting to emit messages time.sleep(0.3) except Exception: log.exception("Couldn't establish ZMQ publisher!") 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( record, serialize=lambda rec: (topic.encode(), cloudpickle.dumps(rec)), ) 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) if self.context is not None: # Cleanly close down ZMQ context and associated socket # For some reason, we need to close the socket before the # context, otherwise context termination hangs. self.publisher.close() self.context.term() def run(self): 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 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.evaluate_metadata() self.results.store_metadata() self.procedure.execute() except (KeyboardInterrupt, SystemExit): self.handle_abort() except Exception: self.handle_error() finally: self.shutdown() self.stop() def __repr__(self): return "<{}(port={},procedure={},should_stop={})>".format( self.__class__.__name__, self.port, self.procedure.__class__.__name__, self.should_stop() ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/generator.py0000644000175100001770000005304714623331163017600 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 io import logging from pymeasure.adapters import VISAAdapter from pymeasure.instruments import Channel log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def write_generic_test(file, header_text, cls_name, comm_text, test, inkwargs=None): """Write a generic test. :param fileLike file: File to write to. :param list[str] header_text: Text of the header (parametrization, test name etc.) :param str cls_name: Name of the instrument class. :param list[str] comm_text: List of str of communication pairs :param str test: Test to assert for. :param dict[str, Any] inkwargs: Dictionary of instrument instantiation kwargs. """ if inkwargs is None: args_text = "", else: args_text = [f' {key}={repr(value)},\n' for key, value in inkwargs.items()] inst = " as inst" if "inst" in test else "" # file.writelines([ # "\n", # "\n", # *header_text, # " with expected_protocol(\n", # f" {cls_name},\n", # *comm_text, # *args_text, # f" ){inst}:\n", # f" {test}\n" # ]) file.write( f""" {''.join(header_text)}\ with expected_protocol( {cls_name}, {''.join(comm_text)}\ {''.join(args_text)}\ ){inst}: {test} """ ) def write_test(file, test_name, cls_name, comm_pairs, test, inkwargs=None, ): """Write a single test. :param file: File to write to. :param str test_name: Name of the test. :param str cls_name: Name of the instrument class. :param list[tuple[bytes | None, bytes | None]] comm_pairs_list: List of communication pairs. :param str test: Test to assert for. :param dict[str, Any] inkwargs: Dictionary of instrument instantiation kwargs. """ write_generic_test( file=file, header_text=[f"def test_{test_name.replace('.', '_')}():\n"], cls_name=cls_name, comm_text=[f" {comm_pairs},\n".replace("), (", "),\n (")], test=test, inkwargs=inkwargs, ) def write_parametrized_test(file, test_name, cls_name, comm_pairs_list, values_list, test, inkwargs=None, ): """Write a parametrized test for properties. :param file: File to write to. :param str test_name: Name of the test. :param str cls_name: Name of the instrument class. :param list[list[tuple[bytes | None, bytes | None]]] comm_pairs_list: List of communication pairs list for each test. :param list[Any] values_list: List of expected values. :param str test: Test to assert for. :code:`'value'` is the expected parametrized value. :param dict inkwargs: Dictionary of instrument instantiation kwargs. """ params = [f" ({cp},\n {v}),\n".replace( "), (", "),\n (") for cp, v in zip(comm_pairs_list, values_list)] header_text = ['@pytest.mark.parametrize("comm_pairs, value", (\n', *params, "))\n", f"def test_{test_name.replace('.', '_')}(comm_pairs, value):\n", ] write_generic_test(file=file, header_text=header_text, cls_name=cls_name, comm_text=[" comm_pairs,\n"], test=test, inkwargs=inkwargs, ) def write_parametrized_method_test(file, test_name, cls_name, comm_pairs_list, args_list, kwargs_list, values_list, test, inkwargs=None, ): """Write a parametrized test for a method, taking in account additional arguments. :param file: File to write to. :param str name: Name of the test. :param str cls_name: Name of the instrument class. :param list[list[tuple[bytes | None, bytes | None]]] comm_pairs_list: List of communication pairs list for each test. :param list[tuple[Any, ...]] args_list: List of arguments lists for the method. :param list[dict[str, Any]] kwargs_list: List of keyword dictionaries for the method. :param list[Any] values_list: List of expected values. :param str test: Test to assert for. :code:`'value'` is the expected parametrized value. :param dict inkwargs: Dictionary of instrument instantiation kwargs. """ z = zip(comm_pairs_list, args_list, kwargs_list, values_list) params = [f" ({cp},\n {a}, {k}, {v}),\n".replace( "), (", "),\n (") for cp, a, k, v in z] header_text = ['@pytest.mark.parametrize("comm_pairs, args, kwargs, value", (\n', *params, "))\n", f"def test_{test_name.replace('.', '_')}(comm_pairs, args, kwargs, value):\n", ] write_generic_test( file=file, cls_name=cls_name, header_text=header_text, comm_text=[" comm_pairs,\n"], test=test, inkwargs=inkwargs ) def parse_stream(stream): """ Parse the data stream. It is expected, that a message is always written in one write, while reading may extend over several reads, e.g. reading bytes. :return list[tuple[bytes | None, bytes | None]]: List of communication pairs """ comm = [] lines = stream.readlines() write = None read = None mode = None for line in lines: if line.startswith(b"WRITE:"): # Store the last comm_pair unless there is none. if write is not None or read is not None: comm.append((write, read)) read = None write = line[6:-1] mode = "W" elif line.startswith(b"READ:"): if read is not None: read += line[5:-1] else: read = line[5:-1] mode = "R" else: # newline due to "\n" character in communication if mode == "W": write += b"\n" + line[:-1] elif mode == "R": read += b"\n" + line[:-1] else: raise ValueError("Very first line does not contain 'WRITE' or 'READ'!") if read is not None or write is not None: comm.append((write, read)) return comm class ByteFormatter(logging.Formatter): """Logging formatter with bytes values for the test generation.""" @staticmethod def make_bytes(value): if isinstance(value, (bytes, bytearray)): return value if isinstance(value, str): return value.encode() raise ValueError(f"value '{value}' is neither str nor bytes.") def format(self, record): return b"".join((record.msg.replace(r"%s", "").encode(), *[self.make_bytes(arg) for arg in record.args])) # type: ignore class ByteStreamHandler(logging.StreamHandler): """Logging handler using bytes streams.""" terminator = b"\n" # type: ignore def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.formatter = ByteFormatter() class TestInstrument: """A man-in-the-middle instrument, which logs property access and method calls. :param instrument: The real instrument, given by the generator. :param generator: The generator which writes the tests. :param name: Name in case of a channel with trailing period, for example :code:`"ch_1."`. """ def __init__(self, instrument, generator, name=""): self._inst = instrument self._generator = generator self._name = name def __getattr__(self, name): if name.startswith("_"): # return private and special attributes to prevent recursion return super().__getattribute__(name) elif name == "adapter": # transparently return the instrument's adapter without writing tests return self._inst.adapter else: # transparently get the attribute from the instrument and write an appropriate test value = getattr(self._inst, name) if callable(value): # the attribute is a callable, we have to return a special method which writes the # test while returning the value def test_method(*args, **kwargs): return self._generator._test_method(value, self._name + name, *args, **kwargs) return test_method elif isinstance(value, Channel): # the attribute is not a property or method, but a Channel, return a TestInstrument return TestInstrument(value, self._generator, f"{self._name}{name}.") else: # the attribute is a plain property, return the value and write a test self._generator._store_property_getter_test(self._name + name, value) return value def __setattr__(self, name, value): if name.startswith("_"): # set private and special attributes to prevent recursion super().__setattr__(name, value) else: # set an attribute transparently while writing a test setattr(self._inst, name, value) self._generator._store_property_setter_test(self._name + name, value) def __dir__(self): # To get autocompletion support for instrument members. return super().__dir__() + dir(self._inst) class Generator: """ Generates tests from the communication with an instrument. Example usage: .. code:: g = Generator() inst = g.instantiate(TC038, "COM5", 'hcp', adapter_kwargs={'baud_rate': 9600}) inst.information # returns the 'information' property and adds it to the tests inst.setpoint = 20 inst.setpoint == 20 # should be True g.write_file("test_tc038.py") # write the tests to a file """ def __init__(self): self._stream = io.BytesIO() self._index = 0 self._init_comm_pairs = [] # Initializiation comm_pairs # Dictionaries for parametrized tests self._getters = {} self._setters = {} self._calls = {} def write_init_test(self, file): """Write the header and init test.""" file.write(self._header) write_test(file, "init", self._class, self._init_comm_pairs, "pass # Verify the expected communication.", self._inkwargs, ) def write_getter_test(self, file, property, parameters): """Write a getter test.""" if len(parameters[0]) == 1: v = parameters[1][0] comparison = "is" if isinstance(v, bool) or v is None else "==" write_test(file, test_name=property.replace(".", "_") + "_getter", cls_name=self._class, comm_pairs=parameters[0][0], test=f"assert inst.{property} {comparison} {v}", inkwargs=self._inkwargs, ) else: write_parametrized_test(file, test_name=property.replace(".", "_") + "_getter", cls_name=self._class, comm_pairs_list=parameters[0], values_list=parameters[1], test=f"assert inst.{property} == value", inkwargs=self._inkwargs, ) def write_setter_test(self, file, property, parameters): """Write a setter test.""" if len(parameters[0]) == 1: v = parameters[1][0] write_test(file, test_name=property.replace(".", "_") + "_setter", cls_name=self._class, comm_pairs=parameters[0][0], test=f"inst.{property} = {v}", # inkwargs=self._inkwargs, TODO ) else: write_parametrized_test(file, test_name=property.replace(".", "_") + "_setter", cls_name=self._class, comm_pairs_list=parameters[0], values_list=parameters[-1], test=f"inst.{property} = value", inkwargs=self._inkwargs, ) def write_method_test(self, file, method, parameters): """Write a test for a method.""" if len(parameters[0]) == 1: v = parameters[-1][0] comparison = "is" if isinstance(v, bool) or v is None else "==" arg_string = f"*{parameters[1][0]}, " if parameters[1][0] else "" kwarg_string = f"**{parameters[2][0]}" if parameters[2][0] else "" write_test(file, test_name=method.replace(".", "_"), cls_name=self._class, comm_pairs=parameters[0][0], test=f"assert inst.{method}({arg_string}{kwarg_string}) {comparison} {v}", inkwargs=self._inkwargs, ) else: write_parametrized_method_test(file, test_name=method.replace(".", "_"), cls_name=self._class, comm_pairs_list=parameters[0], args_list=parameters[1], kwargs_list=parameters[2], values_list=parameters[-1], test=f"assert inst.{method}(*args, **kwargs) == value", inkwargs=self._inkwargs, ) def write_property_tests(self, file): """Write tests for properties in alphabetic order. If getter and setter exist, the setter is the first test. """ # Get a sorted list of all properties, without repeating them property_names = sorted(set(self._getters.keys() | set(self._setters.keys()))) for property in property_names: if property in self._setters: self.write_setter_test(file, property, self._setters[property]) if property in self._getters: # new condition (not elif), as properties can be in setters and in getters tests. self.write_getter_test(file, property, self._getters[property]) def write_method_tests(self, file): """Write all parametrized method tests in alphabetic order.""" for method in sorted(self._calls.keys()): self.write_method_test(file, method, self._calls[method]) def write_file(self, filename="tests.py"): """Write the tests into the file. :param filename: Name to save the tests to, may contain the path, e.g. "/tests/test_abc.py". """ file = filename if isinstance(filename, io.StringIO) else open(filename, "w") self.write_init_test(file) self.write_property_tests(file) self.write_method_tests(file) file.close() def parse_stream(self): """Parse the stream not yet read.""" self._stream.seek(self._index) comm = parse_stream(self._stream) self._index = self._stream.tell() return self._init_comm_pairs + comm def instantiate(self, instrument_class, adapter, manufacturer, adapter_kwargs=None, **kwargs): """ Instantiate the instrument and store the instantiation communication. ..note:: You have to give all keyword arguments necessary for adapter instantiation in `adapter_kwargs`, even those, which are defined somewhere in the instrument's ``__init__`` method, be it as a default value, be it directly in the ``Instrument.__init__()`` call. :param instrument_class: Class of the instrument to test. :param adapter: Adapter (instance or str) for the instrument instantiation. :param manufacturer: Module from which to import the instrument, e.g. 'hcp' if instrument_class is 'pymeasure.hcp.tc038'. :param adapter_kwargs: Keyword arguments for the adapter instantiation (see note above). :param \\**kwargs: Keyword arguments for the instrument instantiation. :return: A man-in-the-middle instrument, which can be used like a normal instrument. """ self._class = instrument_class.__name__ log.info(f"Instantiate {self._class}.") self._header = ( "import pytest\n\n" "from pymeasure.test import expected_protocol\n" f"from pymeasure.instruments.{manufacturer} import {self._class}\n") if isinstance(adapter, (int, str)): if adapter_kwargs is None: adapter_kwargs = {} try: adapter = VISAAdapter(adapter, **adapter_kwargs) except ImportError: raise Exception("Invalid Adapter provided for Instrument since" " PyVISA is not present") adapter.log.addHandler(ByteStreamHandler(self._stream)) adapter.log.setLevel(logging.DEBUG) self.inst = instrument_class(adapter, **kwargs) self._init_comm_pairs = self.parse_stream() # communication of instantiation. self._inkwargs = kwargs # instantiation kwargs self.test_inst = TestInstrument(self.inst, self) return self.test_inst def _store_property_getter_test(self, property, value): """Store the property getter test with returned `value`.""" comm = self.parse_stream() if property not in self._getters: self._getters[property] = [], [] c, v = self._getters[property] c.append(comm) v.append(f"\'{value}\'" if isinstance(value, str) else value) return value def test_property_getter(self, property): """Test getting the `property` of the instrument, adding it to the list.""" log.info(f"Test property {property} getter.") value = getattr(self.inst, property) self._store_property_getter_test(property, value) return value def _store_property_setter_test(self, property, value): """Store the property setter test with `value`.""" comm = self.parse_stream() if property not in self._setters: self._setters[property] = [], [] c, v = self._setters[property] c.append(comm) v.append(f"\'{value}\'" if isinstance(value, str) else value) def test_property_setter(self, property, value): """Test setting the `property` of the instrument to `value`, adding it to the list.""" log.info(f"Test property {property} setter.") setattr(self.inst, property, value) self._store_property_setter_test(property, value) def _test_method(self, method, method_name, *args, **kwargs): """Test calling `method` with the full `method_name` and `args` and `kwargs`.""" value = method(*args, **kwargs) comm = self.parse_stream() if method_name not in self._calls: self._calls[method_name] = [], [], [], [] c, a, k, v = self._calls[method_name] c.append(comm) a.append(args) k.append(kwargs) v.append(f"\'{value}\'" if isinstance(value, str) else value) return value def test_method(self, method_name, *args, **kwargs): """Test calling the `method_name` of the instruments with `args` and `kwargs`.""" log.info(f"Test method {method_name}.") method = getattr(self.inst, method_name) return self._test_method(method, method_name, *args, **kwargs) # batch tests def test_property_setter_batch(self, property, values): """Test setting `property` to each element in `values`.""" for value in values: self.test_property_setter(property, value) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3896053 pymeasure-0.14.0/pymeasure/instruments/0000755000175100001770000000000014623331176017626 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/__init__.py0000644000175100001770000000246414623331163021741 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .channel import Channel from .instrument import Instrument from .resources import find_serial_port, list_resources from .generic_types import SCPIMixin, SCPIUnknownMixin ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3896053 pymeasure-0.14.0/pymeasure/instruments/activetechnologies/0000755000175100001770000000000014623331176023505 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/activetechnologies/AWG401x.py0000644000175100001770000011723114623331163025113 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """This module implements an interface for Active Technologies AWG-401x, both for the Arbitrary Waveform Generator (AWG) mode and the Arbitrary Function Generator (AFG) mode. The module has been developed from the official documentation available on https://www.activetechnologies.it""" from collections import abc, namedtuple import pprint from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, \ strict_range class ChannelBase(Channel): """Implementation of a base Active Technologies AWG-4000 channel.""" def __init__(self, instrument, id): super().__init__(instrument, id) self.delay_values = [self.delay_min, self.delay_max] enabled = Instrument.control( "OUTPut{ch}:STATe?", "OUTPut{ch}:STATe %d", """A boolean property that enables or disables the output for the specified channel.""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) polarity = Instrument.control( "OUTPut{ch}:POLarity?", "OUTPut{ch}:POLarity %s", """This property inverts the output waveform relative to its average value: (High Level – Low Level)/2. NORM for normal, INV for inverted """, validator=strict_discrete_set, values=["NORMAL", "NORM", "INVERTED", "INV"], get_process=lambda v: "NORM" if v == 0 else ("INV" if v == 1 else v) ) delay = Instrument.control( None, None, """This property sets or queries the initial delay, set 0 for disable it. When you send this command in AFG mode, if the instrument is running, it will be stopped.""", dynamic=True ) delay_max = Instrument.measurement( None, """This property queries the maximum delay that can be set to the output waveform.""", dynamic=True ) delay_min = Instrument.measurement( None, """This property queries the minimum delay that can be set to the output waveform.""", dynamic=True ) class ChannelAFG(ChannelBase): """Implementation of a Active Technologies AWG-4000 channel in AFG mode.""" def __init__(self, instrument, id): super().__init__(instrument, id) self.calculate_voltage_range() self.frequency_values = [self.frequency_min, self.frequency_max] self.phase_values = [self.phase_min, self.phase_max] load_impedance = Instrument.control( "OUTPut{ch}:IMPedance?", "OUTPut{ch}:IMPedance %d", """This property sets the output load impedance for the specified channel. The specified value is used for amplitude, offset, and high/low level settings. You can set the impedance to any value from 1 Ω to 1 MΩ. The default value is 50 Ω.""", validator=strict_range, values=[1, 1000000] ) output_impedance = Instrument.control( "OUTPut{ch}:LOW:IMPedance?", "OUTPut{ch}:LOW:IMPedance %d", """This property sets the instrument output impedance, the possible values are: 5 Ohm or 50 Ohm (default).""", validator=strict_discrete_set, values={5: 1, 50: 0}, map_values=True ) shape = Instrument.control( "SOURce{ch}:FUNCtion:SHAPe?", "SOURce{ch}:FUNCtion:SHAPe %s", """This property sets or queries the shape of the carrier waveform. Allowed choices depends on the choosen modality, please refer on instrument manual. When you set this property with a different value, if the instrument is running it will be stopped. Can be set to: SIN, SQU, PULS, RAMP, PRN, DC, SINC, GAUS, LOR, ERIS, EDEC, HAV, ARBB, EFIL, DOUBLEPUL""", validator=strict_discrete_set, values=["SINUSOID", "SIN", "SQUARE", "SQU", "PULSE", "PULS", "RAMP", "PRNOISE", "PRN", "DC", "SINC", "GAUSSIAN", "GAUS", "LORENTZ", "LOR", "ERISE", "ERIS", "EDECAY", "EDEC", "HAVERSINE", "HAV", "ARBB", "EFILE", "EFIL", "DOUBLEPULSE", "DOUBLEPUL"] ) # Default delay override delay_get_command = "SOURce{ch}:INITDELay?" delay_set_command = "SOURce{ch}:INITDELay %s" delay_max_get_command = "SOURce{ch}:INITDELay? MAXimum" delay_min_get_command = "SOURce{ch}:INITDELay? MINimum" frequency = Instrument.control( "SOURce{ch}:FREQuency?", "SOURce{ch}:FREQuency %s", """This property sets or queries the frequency of the output waveform. This command is available when the Run Mode is set to any setting other than Sweep. The output frequency range setting depends on the type of output waveform. If you change the type of output waveform, it may change the output frequency because changing waveform types affects the setting range of the output frequency. The output frequency range setting depends also on the amplitude parameter.""", validator=strict_range, dynamic=True ) frequency_max = Instrument.measurement( "SOURce{ch}:FREQuency? MAXimum", """This property queries the maximum frequency that can be set to the output waveform.""" ) frequency_min = Instrument.measurement( "SOURce{ch}:FREQuency? MINimum", """This property queries the minimum frequency that can be set to the output waveform.""" ) phase = Instrument.control( "SOURce{ch}:PHASe:ADJust?", "SOURce{ch}:PHASe:ADJust %s", """This property sets or queries the phase of the output waveform for the specified channel. The value is in degrees.""", validator=strict_range, dynamic=True ) phase_max = Instrument.measurement( "SOURce{ch}:PHASe:ADJust? MAXimum", """This property queries the maximum phase that can be set to the output waveform.""" ) phase_min = Instrument.measurement( "SOURce{ch}:PHASe:ADJust? MINimum", """This property queries the minimum phase that can be set to the output waveform.""" ) voltage_unit = Instrument.control( "OUTPut{ch}:VOLTage:UNIT?", "OUTPut{ch}:VOLTage:UNIT %s", """This property sets or queries the units of output amplitude, the possible choices are: VPP, VRMS, DBM. This command does not affect the offset, high level, or low level of output.""", validator=strict_discrete_set, values=["VPP", "VRMS", "DBM"] ) voltage_low = Instrument.control( "SOURce{ch}:VOLTage:LEVel:IMMediate:LOW?", "SOURce{ch}:VOLTage:LEVel:IMMediate:LOW %s", """This property sets or queries the low level of the waveform. The low level could be limited by noise level to not exceed the maximum amplitude. If the carrier is Noise or DC level, this command and this query cause an error.""", validator=strict_range, dynamic=True ) voltage_low_max = Instrument.measurement( "SOURce{ch}:VOLTage:LEVel:IMMediate:LOW? MAXimum", """This property queries the maximum low voltage level that can be set to the output waveform.""" ) voltage_low_min = Instrument.measurement( "SOURce{ch}:VOLTage:LEVel:IMMediate:LOW? MINimum", """This property queries the minimum low voltage level that can be set to the output waveform.""" ) voltage_high = Instrument.control( "SOURce{ch}:VOLTage:LEVel:IMMediate:HIGH?", "SOURce{ch}:VOLTage:LEVel:IMMediate:HIGH %s", """This property sets or queries the high level of the waveform. The high level could be limited by noise level to not exceed the maximum amplitude. If the carrier is Noise or DC level, this command and this query cause an error.""", validator=strict_range, dynamic=True ) voltage_high_max = Instrument.measurement( "SOURce{ch}:VOLTage:LEVel:IMMediate:HIGH? MAXimum", """This property queries the maximum high voltage level that can be set to the output waveform.""" ) voltage_high_min = Instrument.measurement( "SOURce{ch}:VOLTage:LEVel:IMMediate:HIGH? MINimum", """This property queries the minimum high voltage level that can be set to the output waveform.""" ) voltage_amplitude = Instrument.control( "SOURce{ch}:VOLTage:LEVel:IMMediate:AMPLitude?", "SOURce{ch}:VOLTage:LEVel:IMMediate:AMPLitude %s", """This property sets or queries the output amplitude for the specified channel. The measurement unit of amplitude depends on the selection operated using the voltage_unit property. If the carrier is Noise the amplitude is Vpk instead of Vpp. If the carrier is DC level this command causes an error. The range of the amplitude setting could be limited by the frequency and offset parameter of the carrier waveform. """, validator=strict_range, dynamic=True ) voltage_amplitude_max = Instrument.measurement( "SOURce{ch}:VOLTage:LEVel:IMMediate:AMPLitude? MAXimum", """This property queries the maximum amplitude voltage level that can be set to the output waveform.""", get_process=lambda value: float(value.replace("VPP", "")) ) voltage_amplitude_min = Instrument.measurement( "SOURce{ch}:VOLTage:LEVel:IMMediate:AMPLitude? MINimum", """This property queries the minimum amplitude voltage level that can be set to the output waveform.""", get_process=lambda value: float(value.replace("VPP", "")) ) voltage_offset = Instrument.control( "SOURce{ch}:VOLTage:LEVel:IMMediate:OFFSet?", "SOURce{ch}:VOLTage:LEVel:IMMediate:OFFSet %s", """This property sets or queries the offset level for the specified channel. The offset range setting depends on the amplitude parameter. """, validator=strict_range, dynamic=True ) voltage_offset_max = Instrument.measurement( "SOURce{ch}:VOLTage:LEVel:IMMediate:OFFSet? MAXimum", """This property queries the maximum offset voltage level that can be set to the output waveform.""" ) voltage_offset_min = Instrument.measurement( "SOURce{ch}:VOLTage:LEVel:IMMediate:OFFSet? MINimum", """This property queries the minimum offset voltage level that can be set to the output waveform.""" ) baseline_offset = Instrument.control( "SOURce{ch}:VOLTage:BASELINE:OFFSET?", "SOURce{ch}:VOLTage:BASELINE:OFFSET %s", """This property sets or queries the offset level for the specified channel. The offset range setting depends on the amplitude parameter. """, validator=strict_range, dynamic=True ) baseline_offset_max = Instrument.measurement( "SOURce{ch}:VOLTage:BASELINE:OFFSET? MAXimum", """This property queries the maximum offset voltage level that can be set to the output waveform.""" ) baseline_offset_min = Instrument.measurement( "SOURce{ch}:VOLTage:BASELINE:OFFSET? MINimum", """This property queries the minimum offset voltage level that can be set to the output waveform.""" ) def calculate_voltage_range(self): self.voltage_low_values = [self.voltage_low_min, self.voltage_low_max] self.voltage_high_values = [self.voltage_high_min, self.voltage_high_max] self.voltage_amplitude_values = [self.voltage_amplitude_min, self.voltage_amplitude_max] self.voltage_offset_values = [self.voltage_offset_min, self.voltage_offset_max] self.baseline_offset_values = [self.baseline_offset_min, self.baseline_offset_max] class ChannelAWG(ChannelBase): """Implementation of a Active Technologies AWG-4000 channel in AWG mode.""" # Default delay override delay_get_command = "OUTPut{ch}:DELay?" delay_set_command = "OUTPut{ch}:DELay %s" delay_max_get_command = "OUTPut{ch}:DELay? MAXimum" delay_min_get_command = "OUTPut{ch}:DELay? MINimum" scale = Instrument.control( "OUTPut{ch}:SCALe?", "OUTPut{ch}:SCALe %f", """This property sets or returns the Amplitude Scale parameter of the analog channel “n”. This property can be modified at run-time to adjust the waveform amplitude while the instrument is running and it is applied to all the waveforms contained in the sequencer. It is expressed in % and it has a range of 0% to 100%. 100% means that the waveform keeps its original amplitude.""", validator=strict_range, values=[0, 100] ) class AWG401x_base(SCPIUnknownMixin, Instrument): """AWG-401x base class""" def __init__(self, adapter, name="Active Technologies AWG-4014 1.2GS/s Arbitrary Waveform Generator", **kwargs): # Insert an higher timeout because, often, when starting the # instrument, can pass some time and the adapted goes in timeout kwargs.setdefault('timeout', 7500) super().__init__( adapter, name, **kwargs ) def beep(self): """Causes a system beep.""" self.write("SYST:BEEP") def save(self, position): """Save the actual configuration in memory. :param int position: Instrument save position [0,4] :raises ValueError: If position is outside permitted limit [0,4]. """ if position >= 0 or position <= 4: self.write(f"*SAV {position}") else: raise ValueError("position value outside permitted range [0,4]") def load(self, position): """Load the actual configuration in memory. :param int position: Instrument load position [0,4] :raises ValueError: If position is outside permitted limit [0,4]. """ if position >= 0 or position <= 4: self.write(f"*RCL {position}") else: raise ValueError("position value outside permitted range [0,4]") def wait_last(self): """Wait for last operation completition""" self.write("*WAI") class AWG401x_AFG(AWG401x_base): """Represents the Active Technologies AWG-401x Arbitrary Waveform Generator in AFG mode. .. code-block:: python wfg = AWG401x_AFG("TCPIP::192.168.0.123::INSTR") wfg.reset() # Reset the instrument at default state wfg.channels[1].shape = "SINUSOID" # Sets a sine waveform on CH1 wfg.channels[1].frequency = 4.7e3 # Sets the frequency to 4.7 kHz on CH1 wfg.channels[1].amplitude = 1 # Set amplitude of 1 V on CH1 wfg.channels[1].offset = 0 # Set the amplitude to 0 V on CH1 wfg.channels[1].enabled = True # Enables the CH1 wfg.channels[2].shape = "SQUARE" # Sets a square waveform on CH2 wfg.channels[2].frequency = 100e6 # Sets the frequency to 100 MHz on CH2 wfg.channels[2].amplitude = 0.5 # Set amplitude of 0.5 V on CH2 wfg.channels[2].offset = 0 # Set the amplitude to 0 V on CH2 wfg.channels[2].enabled = True # Enables the CH2 wfg.enabled = True # Enable output of waveform generator wfg.beep() # "beep" print(wfg.check_errors()) # Get the error queue """ ch_1 = Instrument.ChannelCreator(ChannelAFG, 1) ch_2 = Instrument.ChannelCreator(ChannelAFG, 2) enabled = Instrument.control( "AFGControl:STATus?", "AFGControl:%s", """A boolean property that enables the generation of signals.""", validator=strict_discrete_set, values={True: "START", False: "STOP"}, map_values=True, get_process=lambda v: "START" if v == 1 else ("STOP" if v == 0 else v) ) def __init__(self, adapter, **kwargs): super().__init__(adapter, **kwargs) model = self.id.split(",")[1] if model == "AWG4012": num_ch = 2 elif model == "AWG4014": num_ch = 4 elif model == "AWG4018": num_ch = 8 else: raise NotImplementedError(f"Instrument {model} not implemented in" "class AWG401x") for i in range(3, num_ch + 1): child = self.add_child(ChannelAFG, i) child._protected = True class AWG401x_AWG(AWG401x_base): """Represents the Active Technologies AWG-401x Arbitrary Waveform Generator in AWG mode. .. code-block:: python wfg = AWG401x_AWG("TCPIP::192.168.0.123::INSTR") wfg.reset() # Reset the instrument at default state # Set a oscillating waveform wfg.waveforms["MyWaveform"] = [1, 0] * 8 for i in range(1, wfg.num_ch + 1): wfg.entries[1].channels[i].voltage_high = 1 # Sets high voltage = 1 wfg.entries[1].channels[i].voltage_low = 0 # Sets low voltage = 1 wfg.entries[1].channels[i].waveform = "SQUARE" # Sets a square wave wfg.setting_ch[i].enabled = True # Enable channel wfg.entries.resize(2) # Resize the number of entries to 2 wfg.entries[2].channels[1].waveform = "MyWaveform" # Set custom waveform wfg.enabled = True # Enable output of waveform generator wfg.beep() # "beep" print(wfg.check_errors()) # Get the error queue """ def __init__(self, adapter, **kwargs): super().__init__(adapter, **kwargs) for i in range(1, self.num_ch + 1): self.add_child(ChannelAWG, i, collection="setting_ch") self.entries = self.DummyEntriesElements(self, self.num_ch) self.burst_count_values = [self.burst_count_min, self.burst_count_max] self.sampling_rate_values = [self.sampling_rate_min, self.sampling_rate_max] self._waveforms = self.WaveformsLazyDict(self) num_ch = Instrument.measurement( "AWGControl:CONFigure:CNUMber?", """This property queries the number of analog channels.""", cast=int ) num_dch = Instrument.measurement( "AWGControl:CONFigure:DNUMber?", """This property queries the number of digital channels.""", cast=int ) sample_decreasing_strategy = Instrument.control( "AWGControl:DECreasing?", "AWGControl:DECreasing %s", """This property sets or returns the Sample Decreasing Strategy. The “Sample decreasing strategy” parameter defines the strategy used to adapt the waveform length to the sequencer entry length in the case where the original waveform length is longer than the sequencer entry length. Can be set to: DECIM, CUTT, CUTH""", validator=strict_discrete_set, values=["DECIMATION", "DECIM", "CUTTAIL", "CUTT", "CUTHEAD", "CUTH"] ) sample_increasing_strategy = Instrument.control( "AWGControl:INCreasing?", "AWGControl:INCreasing %s", """This property sets or or returns the Sample Increasing Strategy. The “Sample increasing strategy” parameter defines the strategy used to adapt the waveform length to the sequencer entry length in the case where the original waveform length is shorter than the sequencer entry length. Can be set to: INTER, RETURN, HOLD, SAMPLESM""", validator=strict_discrete_set, values=["INTERPOLATION", "INTER", "RETURNZERO", "RETURN", "HOLDLAST", "HOLD", "SAMPLESMULTIPLICATION", "SAMPLESM"] ) entry_level_strategy = Instrument.control( "AWGControl:LENGth:MODE?", "AWGControl:LENGth:MODE %s", """This property sets or or returns the Entry Length Strategy. This strategy manages the length of the sequencer entries in relationship with the length of the channel waveforms defined for each entry. The possible values are: * ADAPTL: the length of an entry of the sequencer by default will be equal to the length of the longer channel waveform, among all analog channels, assigned to the entry. * ADAPTS: the length of an entry of the sequencer by default will be equal to the length of the shorter channel waveform, among all analog channels, assigned to the entry. * DEF:the length of an entry of the sequencer by default will be equal to the value specified in the Sequencer Item Default Length [N] parameter""", validator=strict_discrete_set, values=["ADAPTLONGER", "ADAPTL", "ADAPTSHORTER", "ADAPTS", "DEFAULT", "DEF"] ) run_mode = Instrument.control( "AWGControl:RMODe?", "AWGControl:RMODe %s", """This property sets or returns the AWG run mode. The possible values are: * CONT: each waveform will loop as written in the entry repetition parameter and the entire sequence is repeated circularly * BURS: the AWG waits for a trigger event. When the trigger event occurs each waveform will loop as written in the entry repetition parameter and the entire sequence will be repeated circularly many times as written in the Burst Count[N] parameter. If you set Burst Count[N]=1 the instrument is in Single mode and the sequence will be repeated only once. * TCON: the AWG waits for a trigger event. When the trigger event occurs each waveform will loop as written in the entry repetition parameter and the entire sequence will be repeated circularly. * STEP: the AWG, for each entry, waits for a trigger event before the execution of the sequencer entry. The waveform of the entry will loop as written in the entry repetition parameter. After the generation of an entry has completed, the last sample of the current entry or the first sample of the next entry is held until the next trigger is received. At the end of the entire sequence the execution will restart from the first entry. * ADVA: it enables the “Advanced” mode. In this mode the execution of the sequence can be changed by using conditional and unconditional jumps (JUMPTO and GOTO commands) and dynamic jumps (PATTERN JUMP commands). The \\*RST command sets this parameter to CONTinuous.""", validator=strict_discrete_set, values=["CONTINUOUS", "CONT", "BURST", "BURS", "TCONTINUOUS", "TCON", "STEPPED", "STEP", "ADVANCED", "ADVA"] ) burst_count = Instrument.control( "AWGControl:BURST?", "AWGControl:BURST %d", """This property sets or queries the burst count parameter.""", validator=strict_range, dynamic=True ) burst_count_max = Instrument.measurement( "AWGControl:BURST? MAXimum", """This property queries the maximum burst count parameter.""", cast=int ) burst_count_min = Instrument.measurement( "AWGControl:BURST? MINimum", """This property queries the minimum burst count parameter.""", cast=int ) sampling_rate = Instrument.control( "AWGControl:SRATe?", "AWGControl:SRATe %f", """This property sets or queries the sample rate for the Sampling Clock.""", validator=strict_range, dynamic=True ) sampling_rate_max = Instrument.measurement( "AWGControl:SRATe? MAXimum", """This property queries the maximum sample rate for the Sampling Clock.""" ) sampling_rate_min = Instrument.measurement( "AWGControl:SRATe? MINimum", """This property queries the minimum sample rate for the Sampling Clock.""" ) run_status = Instrument.measurement( "AWGControl:RSTATe?", """This property returns the run state of the AWG. The possible values are: STOPPED, WAITING_TRIGGER, RUNNING""", values={"STOPPED": 0, "WAITING_TRIGGER": 1, "RUNNING": 2}, map_values=True ) enabled = Instrument.control( "AWGControl:RSTATe?", "AWGControl:%s", """A boolean property that enables the generation of signals.""", validator=strict_discrete_set, values={True: "RUN", False: "STOP"}, map_values=True, get_process=lambda v: "STOP" if v == 0 else "RUN" ) trigger_source = Instrument.control( "TRIGger:SEQuence:SOURce?", "TRIGger:SEQuence:SOURce %s", """This property sets or returns the instrument trigger source. The possible values are: * TIM: the trigger is sent at regular intervals. * EXT: the trigger come from the external BNC connector. * MAN: the trigger is sent via software or using the trigger button on front panel.""", validator=strict_discrete_set, values=["TIMER", "TIM", "EXTERNAL", "EXT", "MANUAL", "MAN"] ) waveforms = property( lambda self: self._waveforms, doc="""This property returns a dict with all the waveform present in the instrument system (Wave. List). It is possible to modify the values, delete them or create new waveforms""") def trigger(self): """Force a trigger event to occour.""" self.write("TRIGger:SEQuence:IMMediate") def save_file(self, file_name, data, path=None, override_existing=False): """Write a string in a file in the instrument""" if path is not None: raise NotImplementedError("Specify path is not implemented") if file_name in [file.name for file in self.list_files(path=path) if file.type == '']: if override_existing: self.remove_file(file_name, path=path) else: raise ValueError("File already exist and override is disabled") self.adapter.write_binary_values( 'MMEM:DATA "' + file_name + '", 0, ', data.encode("ASCII"), datatype='s') # HACK: Send an unuseful command to ensure the last command was # executed because if it is more than 1024 bytes it doesn't work self.wait_last() def remove_file(self, file_name, path=None): """Remove a specified file""" if path is not None: raise NotImplementedError("Specify path is not implemented") if file_name not in [file.name for file in self.list_files(path=path) if file.type == '']: raise ValueError("File do not exist") self.write('MMEMORY:DELETE "' + file_name + '"') def list_files(self, path=None): """Return a List of tuples with all file found in a directory. If the path is not specified the current directory will be used""" if path is not None: raise NotImplementedError("Specify path is not implemented") catalog = self.values("MMEMory:CATalog?") catalog = catalog[1:] FS_Element = namedtuple("FS_Element", "name type dimension") elements = [] for i in range(int(len(catalog) / 3)): elements.append(FS_Element(catalog[i * 3 + 0], catalog[i * 3 + 1], catalog[i * 3 + 2])) return elements class WaveformsLazyDict(abc.MutableMapping): """This class inherit from MutableMapping in order to create a custom dict to lazy load, modify, delete and create instrument waveform.""" def __init__(self, parent): self.parent = parent self.reset() def __getitem__(self, key): """Load data from instrument if not present""" if self._data[key] is None: self._data[key] = self._get_waveform(key) return self._data[key] def __setitem__(self, key, value): """Create a new waveform from key and value""" if len(value) < 16: raise ValueError("The minimum waveform length is 16 samples") elif len(value) < 384 and len(value) % 16 != 0: raise ValueError("From 16 to 384 samples the granularity of" "the waveform is 16") class VoltageOutOfRangeError(Exception): pass if max(value) > self.parent.entries[1].channels[1].voltage_high_max: raise VoltageOutOfRangeError( f"{max(value)}V is higher than maximum possible voltage, " f"which is " f"{self.instrument.entries[1].channels[1].voltage_high_max}V") if min(value) < self.parent.entries[1].channels[1].voltage_low_min: raise VoltageOutOfRangeError( f"{min(value)}V is lower than minimum possible voltage, " f"which is " f"{self.instrument.entries[1].channels[1].voltage_low_min}V") self.parent.save_file(f"{key}.txt", "\n".join(map(str, value)), override_existing=True) try: del self[key] except KeyError: pass self.parent.write(f'WLISt:WAVeform:IMPort "{key}",' f'"{key}.txt",ANAlog') self.parent.wait_last() self.parent.remove_file(f"{key}.txt") self._data[key] = None return def __delitem__(self, key): """When removing an element this method removes also the corresponding waveform in the instrument""" del self._data[key] self.parent.write(f'WLISt:WAVeform:DELete "{key}"') return def __iter__(self): try: for el in self._data: yield el except KeyError: return def __len__(self): return len(self._data) def __str__(self): """Return a str without the waveforms points because it is useless and loads all waveforms uselessy""" return pprint.pformat({el: "Waveform Points" for el in self._data}) def reset(self): """Reset the class reloading the waveforms from instrument""" waveforms_name = self.parent.values("WLISt:LIST?") self._data = {v: None for v in waveforms_name} def _get_waveform(self, waveform_name): """Get the waveform point of a specified waveform""" bin_value = self.parent.adapter.connection.query_binary_values( 'WLISt:WAVeform:DATA? "' + waveform_name + '"', header_fmt='ieee', datatype='h') return bin_value class DummyEntriesElements(abc.Sequence): """Dummy List Class to list every sequencer entry. The content is loaded in real-time.""" def __init__(self, parent, number_of_channel): self.parent = parent self.num_ch = number_of_channel def resize(self, new_size): self.parent.write(f"SEQuence:LENGth {new_size}") def __getitem__(self, key): if key <= 0: raise IndexError("Entry numeration start from 1") if key > int(self.parent.values("SEQuence:LENGth?")[0]): raise IndexError("Index out of range") return SequenceEntry(self.parent, self.num_ch, key) def __len__(self): return int(self.parent.values("SEQuence:LENGth?")[0]) class SequenceEntry(Channel): """Implementation of sequencer entry.""" def __init__(self, parent, number_of_channels, sequence_number): super().__init__(parent, sequence_number) self.number_of_channels = number_of_channels self.length_values = [self.length_min, self.length_max] self.loop_count_values = [self.loop_count_min, self.loop_count_max] for i in range(1, self.number_of_channels + 1): self.add_child(self.AnalogChannel, i, sequence_number=sequence_number) def insert_id(self, command): return command.format(ent=self.id) length = Instrument.control( "SEQuence:ELEM{ent}:LENGth?", "SEQuence:ELEM{ent}:LENGth %s", """This property sets or returns the number of samples of the entry. """, validator=strict_range, dynamic=True ) length_max = Instrument.measurement( "SEQuence:ELEM{ent}:LENGth? MAXimum", """This property queries the maximum entry samples length.""", get_process=lambda v: int(v) ) length_min = Instrument.measurement( "SEQuence:ELEM{ent}:LENGth? MINimum", """This property queries the minimum entry samples length.""", get_process=lambda v: int(v) ) loop_count = Instrument.control( "SEQuence:ELEM{ent}:LOOP:COUNt?", "SEQuence:ELEM{ent}:LOOP:COUNt %s", """This property sets or returns the number of waveform repetitions for the entry. """, validator=strict_range, dynamic=True ) loop_count_max = Instrument.measurement( "SEQuence:ELEM{ent}:LOOP:COUNt? MAXimum", """This property queries the maximum number of waveform repetitions for the entry.""", get_process=lambda v: int(v) ) loop_count_min = Instrument.measurement( "SEQuence:ELEM{ent}:LOOP:COUNt? MINimum", """This property queries the minimum number of waveform repetitions for the entry.""", get_process=lambda v: int(v) ) class AnalogChannel(Channel): """Implementation of an analog channel for a single sequencer entry.""" def __init__(self, parent, id, sequence_number): super().__init__(parent, id) self.seq_num = sequence_number self.waveform_values = list(self.parent.parent.waveforms.keys()) self.calculate_voltage_range() def insert_id(self, command): return command.format(ent=self.seq_num, ch=self.id) voltage_amplitude = Instrument.control( "SEQuence:ELEM{ent}:AMPlitude{ch}?", "SEQuence:ELEM{ent}:AMPlitude{ch} %s", """This property sets or returns the voltage peak-to-peak amplitude.""", validator=strict_range, dynamic=True ) voltage_amplitude_max = Instrument.measurement( "SEQuence:ELEM{ent}:AMPlitude{ch}? MAXimum", """This property queries the maximum amplitude voltage level that can be set.""" ) voltage_amplitude_min = Instrument.measurement( "SEQuence:ELEM{ent}:AMPlitude{ch}? MINimum", """This property queries the minimum amplitude voltage level that can be set.""" ) voltage_offset = Instrument.control( "SEQuence:ELEM{ent}:OFFset{ch}?", "SEQuence:ELEM{ent}:OFFset{ch} %s", """This property sets or returns the voltage offset.""", validator=strict_range, dynamic=True ) voltage_offset_max = Instrument.measurement( "SEQuence:ELEM{ent}:OFFset{ch}? MAXimum", """This property queries the maximum voltage offset that can be set.""" ) voltage_offset_min = Instrument.measurement( "SEQuence:ELEM{ent}:OFFset{ch}? MINimum", """This property queries the minimum voltage offset that can be set.""" ) voltage_high = Instrument.control( "SEQuence:ELEM{ent}:VOLTage:HIGH{ch}?", "SEQuence:ELEM{ent}:VOLTage:HIGH{ch} %s", """This property sets or returns the high voltage level of the waveform.""", validator=strict_range, dynamic=True ) voltage_high_max = Instrument.measurement( "SEQuence:ELEM{ent}:VOLTage:HIGH{ch}? MAXimum", """This property queries the maximum high voltage level of the waveform that can be set to the output waveform.""" ) voltage_high_min = Instrument.measurement( "SEQuence:ELEM{ent}:VOLTage:HIGH{ch}? MINimum", """This property queries the minimum high voltage level of the waveform that can be set to the output waveform.""" ) voltage_low = Instrument.control( "SEQuence:ELEM{ent}:VOLTage:LOW{ch}?", "SEQuence:ELEM{ent}:VOLTage:LOW{ch} %s", """This property sets or returns the low voltage level of the waveform.""", validator=strict_range, dynamic=True ) voltage_low_max = Instrument.measurement( "SEQuence:ELEM{ent}:VOLTage:LOW{ch}? MAXimum", """This property queries the maximum low voltage level of the waveform that can be set to the output waveform.""" ) voltage_low_min = Instrument.measurement( "SEQuence:ELEM{ent}:VOLTage:LOW{ch}? MINimum", """This property queries the minimum low voltage level of the waveform that can be set to the output waveform.""" ) waveform = Instrument.control( "SEQuence:ELEM{ent}:WAVeform{ch}?", "SEQuence:ELEM{ent}:WAVeform{ch} %s", """This property sets or returns the waveform. It’s possible select a waveform only from those in the waveform list. In waveform list are already present 10 predefined waveform: Sine, Ramp, Square, Sync, DC, Gaussian, Lorentz, Haversine, Exp_Rise and Exp_Decay but user can import in the list others customized waveforms.""", validator=strict_discrete_set, set_process=lambda v: f"\"{v}\"", dynamic=True ) def calculate_voltage_range(self): self.voltage_amplitude_values = [self.voltage_amplitude_min, self.voltage_amplitude_max] self.voltage_offset_values = [self.voltage_offset_min, self.voltage_offset_max] self.voltage_high_values = [self.voltage_high_min, self.voltage_high_max] self.voltage_low_values = [self.voltage_low_min, self.voltage_low_max] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/activetechnologies/__init__.py0000644000175100001770000000230714623331163025614 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .AWG401x import AWG401x_AFG from .AWG401x import AWG401x_AWG ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3896053 pymeasure-0.14.0/pymeasure/instruments/advantest/0000755000175100001770000000000014623331176021617 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/advantest/__init__.py0000644000175100001770000000241214623331163023723 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from .advantestR624X import AdvantestR6245 from .advantestR624X import AdvantestR6246 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/advantest/advantestR3767CG.py0000644000175100001770000000534714623331163025052 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import strict_range class AdvantestR3767CG(SCPIUnknownMixin, Instrument): """ Represents the Advantest R3767CG VNA. Implements controls to change the analysis range and to retrieve the data for the trace. """ def __init__(self, adapter, name="Advantest R3767CG", **kwargs): super().__init__( adapter, name, **kwargs ) # Tell unit to operate in IEEE488.2-1987 command mode. self.write("OLDC OFF") id = Instrument.measurement( "*IDN?", """Get the instrument identification.""" ) center_frequency = Instrument.control( ":FREQ:CENT?", ":FREQ:CENT %d", """Control the center Frequency in Hz.""", validator=strict_range, values=[300000, 8000000000] ) span_frequency = Instrument.control( ":FREQ:SPAN?", ":FREQ:SPAN %d", """Control the frequency span in Hz.""", validator=strict_range, values=[1, 8000000000] ) start_frequency = Instrument.control( ":FREQ:STAR?", ":FREQ:STAR %d", """Control the starting frequency in Hz.""", validator=strict_range, values=[1, 8000000000] ) stop_frequency = Instrument.control( ":FREQ:STOP?", ":FREQ:STOP %d", """Control the stopping frequency in Hz.""", validator=strict_range, values=[1, 8000000000] ) trace_1 = Instrument.measurement( "TRAC:DATA? FDAT1", """Get the data array from trace 1 after formatting.""" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/advantest/advantestR624X.py0000644000175100001770000024534514623331163024701 0ustar00runnerdocker# This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 enum import IntEnum, IntFlag from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set, \ strict_range # Setup logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class SampleHold(IntEnum): MODE_0 = 0 MODE_100uS = 6 MODE_200uS = 7 MODE_500uS = 8 MODE_1mS = 9 MODE_2mS = 10 MODE_5mS = 11 MODE_10mS = 12 MODE_1PLC = 13 # Number of power line cycles MODE_2PLC = 14 MODE_5PLC = 15 MODE_10PLC = 16 MODE_20PLC = 17 class SampleMode(IntEnum): ASYNC = 1 # Asynchronous operation PULSED_SYNC = 2 # Synchronous operation of DC measurement and pulse measurement PULSED_POSITIVE = 3 # Positive tracking operation for DC measurement and pulse measurement PULSED_REVERSE = 4 # DC measurement, pulse measurement reverse polarity tracking operation SWEEP_SYNC = 5 # Synchronous operation of sweep measurement SWEEP_DELAYED = 6 # Delayed sweep operation SWEEP_DOUBLE = 7 # Double synchronous sweep operation BINARY_SEARCH = 8 # Binary search LINEAR_SEARCH = 9 # Linear search class VoltageRange(IntEnum): # When the integration time is sample hold mode (SH) and between 100 μs to 500 μs, the # resolution is as follows. # # Integration time Decomposition energy (digit) # SH, 100μs 10 digits # 200μs 5 digits # 500μs 2 digits # The range that maximizes the number of digits in the measurement data is automatically # selected. # It cannot be specified for pulse measurement and pulse sweep. AUTO = 0 # ±1μV resolution # Limited auto range # It operates in the same way as the auto range except that the specified range is minimized. # It cannot be specified for pulse measurement and pulse sweep. AUTO_600mV = 23 # ±1μV resolution AUTO_6V = 24 # ±10μV resolution AUTO_60V = 25 # ±100μV resolution AUTO_200V = 26 # ±1mV resolution # Best fixed range # - Voltage generation When measuring voltage (VSVM), the range is the same as the generation # range. # - In the case of current generation voltage measurement (ISVM), it is in the same range as the # compliance range. FIXED_BEST = 20 # ±1μV - 1mV resolution # Measure in the specified range. FIXED_600mV = 3 # ±1μV resolution FIXED_6V = 4 # ±10μV resolution FIXED_60V = 5 # ±100μV resolution FIXED_200V = 6 # ±1mV resolution class CurrentRange(IntEnum): # When the integration time is sample hold mode (SH) and between 100 μs to 500 μs, the # resolution is as follows. # # Integration time Decomposition energy (digit) # SH, 100μs 10 digits # 200μs 5 digits # 500μs 2 digits # The range that maximizes the number of digits in the measurement data is automatically # selected. # It cannot be specified for pulse measurement and pulse sweep. AUTO = 0 # ±10fA resolution # It operates in the same way as the auto range, except that the specified range is the minimum # range. It cannot be specified for pulse measurement and pulse sweep. AUTO_6nA = 23 # ±10fA resolution AUTO_60nA = 24 # ±100fA resolution AUTO_600nA = 25 # ±1pA resolution AUTO_6μA = 26 # ±10pA resolution AUTO_60μA = 27 # ±100pA resolution AUTO_600μA = 28 # ±1nA resolution AUTO_6mA = 29 # ±10nA resolution AUTO_60mA = 30 # ±100nA resolution AUTO_600mA = 31 # ±1μA resolution AUTO_2A_6A = 32 # ±10μA resolution AUTO_20A = 33 # ±100μA resolution # Current generation when measuring current (ISIM), the range is the same as the generation # range. When measuring voltage generation current (VSIM), it is in the same # range as the compliance range. FIXED_BEST = 20 # ±10fA - ±100μA resolution # A fixed range cannot be specified for internal measurements. # It can be specified only for external measurement. MEASURE INPUT-ANALOG COMMON # Also measures the voltage between the terminals as the specified current range data. FIXED_6nA = 3 # ±10fA resolution FIXED_60nA = 4 # ±100fA resolution FIXED_600nA = 5 # ±1pA resolution FIXED_6μA = 6 # ±10pA resolution FIXED_60μA = 7 # ±100pA resolution FIXED_600μA = 8 # ±1nA resolution FIXED_6mA = 9 # ±10nA resolution FIXED_60mA = 10 # ±100nA resolution FIXED_600mA = 11 # ±1μA resolution FIXED_2A_6A = 12 # ±10μA resolution FIXED_20A = 13 # ±100μA resolution class SweepMode(IntEnum): LINEAR_ONE_WAY_SWEEP = 1 LOG_ONE_WAY_SWEEP = 2 LINEAR_ROUND_TRIP_SWEEP = 3 LOG_ROUND_TRIP_SWEEP = 4 class OutputType(IntEnum): REAL_TIME_OUTPUT = 1 # there is output every time it is measured BUFFERING_OUTPUT_ALL = 2 # output all at once after sweeping BUFFERING_OUTPUT_SPECIFIED = 3 # After sweeping, only output the specified data class TriggerInputType(IntEnum): ALL = 1 SOFTWARE_ONLY = 2 CHANNELS_ONLY = 3 class MeasurementType(IntEnum): MEASURE_DATA = 1 MEASURE_DATA_AND_OCCURENCE = 2 class SequenceInterruptionType(IntEnum): """ 1. Release pause state is a valid command only in the sequence program pause state. otherwise it is ignored. 2. Pause state enters the pause state when the currently executing program ends. 3. Abort sequence program stops the sequence program when the currently executing program ends. If the currently running program is a sweep operation, interrupt the sweep operation and stop the sequence program. The output value will be the bias value. """ RELEASE_PAUSE = 1 PAUSE = 2 INTERRUPT_SEQUENCE = 3 class DOR(IntFlag): """ bit assigment for the Device Operation Register (DOR): ========= ========================== Bit (dec) Description ========= ========================== 13 Indicates that the fast tokens program is running. 12 Error in search measurement 11 End of sequence program/high-speed sequence program execution 10 Sequence program Pause state 9 Fan stop detection 8 Self-test error occurred (logic part) 7 Trigger wait state in trigger link master operation 6 Calibration mode status 5 Trigger link ON state 4 Trigger link bus error 3 Sequence program/high-speed sequence 1 program/add/de) waiting 2 Wait for sequence program wait time 1 Sequence program running 0 Synchronous operation state ========= ========================== """ FAST_TOKENS_PROGRAM_IS_RUNNING = 1 << 13 ERROR_IN_SEARCH_MEASUREMENT = 1 << 12 END_OF_SEQUENCE_PROGRAM = 1 << 11 SEQUENCE_PROGRAM_PAUSE_STATE = 1 << 10 FAN_STOP_DETECTION = 1 << 9 SELF_TEST_ERROR_LOGIC = 1 << 8 TRIGGER_WAIT_STATE = 1 << 7 CALIBRATION_MODE_STATUS = 1 << 6 TRIGGER_LINK_ON_STATE = 1 << 5 TRIGGER_LINK_BUS_ERROR = 1 << 4 SEQUENCE_PROGRAM_WAITING = 1 << 3 WAIT_FOR_SEQUENCE_PROGRAM_WAIT_TIME = 1 << 2 SEQUENCE_PROGRAM_RUNNING = 1 << 1 SYNCHRONOUS_OPERATION_STATE = 1 << 0 class COR(IntFlag): """ bit assigment for the Channel Operations Register (COR): ========= ============================================= Bit (dec) Description ========= ============================================= 14 The result of the comparison operation is HI 13 The result of the comparison operation is GO 12 The result of the comparison operation is LO 11 Overheat detection 10 Overload detection 9 Oscillation detection 8 Compliance detection 7 Synchronous operation master channel 6 Measurement data output specification 5 There is measurement data 4 Self-test error occurrence (analog part) 3 Measurement data buffer full 2 Waiting for trigger 1 End of sweep 0 Operated state ========= ============================================= """ COMPARISON_RESULT_HI = 1 << 14 COMPARISON_RESULT_GO = 1 << 13 COMPARISON_RESULT_LO = 1 << 12 OVERHEAT_DETECTION = 1 << 11 OVERLOAD_DETECTION = 1 << 10 OSCILLATION_DETECTION = 1 << 9 COMPLIANCE_DETECTION = 1 << 8 SYNCHRONOUS_OPERATION_MASTER_CHANNEL = 1 << 7 MEASUREMENT_DATA_OUTPUT_SPECIFICATION = 1 << 6 HAS_MEASUREMENT_DATA = 1 << 5 SELF_TEST_ERROR_ANALOG_SECTION = 1 << 4 MEASUREMENT_DATA_BUFFER_FULL = 1 << 3 WAITING_FOR_TRIGGER = 1 << 2 END_OF_SWEEP = 1 << 1 OPERATED_STATE = 1 << 0 class SRER(IntFlag): """ bit assigment for the Service Request Enable Register (SRER): ========= =========================================================== Bit (dec) Description ========= =========================================================== 0 none 1 ERR Set when any of QYE, DDE, EXE, or CME in the Standard Event Status Register (SESR) is set. 2 DOP Set when a bit in the device operation register for which the enable register is set to enabled is set. Cleared by reading the device operation register. 3 none 4 MAV Set when output data is set in the output queue. Cleared when output data is read. 5 ESB Set when a bit in the Standard Event Status Register (SESR) is set and the enable register is set to Enabled. Cleared by reading SESR. 6 RQS (MSS) Set when bit O to bit 5 and bit 7 of the Status Byte register are set. (this bit is read-only) 7 COP Set when a bit in the Channel Operations Register is set with the Enable Register set to Enable. Cleared by reading the Channel Operations Register. ========= =========================================================== """ ERR = 1 << 1 DOP = 1 << 2 MAV = 1 << 4 ESB = 1 << 5 RQS = 1 << 6 COP = 1 << 7 class SESR(IntFlag): """ bit assigment for the Standard Event Status Register (SESR): ========= ========================== Bit (dec) Description ========= ========================== 0 OPC (Operation Complete) not used 1 RQC unused 2 QYE (Query Error) Set when the output queue overflows when reading without output data. 3 DDE (Device Dependent Error) Set when an error occurs in the self-test. 4 EXE (Execution Error) Set when the input data is outside the range set internally, or when the command cannot be executed. 5 CME (Command Error) Set when an undefined header or data format is wrong, or when there is a syntax error in the command. 6 URQ unused 7 PON Set when power is switched from OFF to ON. ========= ========================== """ OPC = 1 << 0 RQC = 1 << 1 QYE = 1 << 2 DDE = 1 << 3 EXE = 1 << 4 CME = 1 << 5 URQ = 1 << 6 PON = 1 << 7 class TriggerOutputSignalTiming(IntFlag): """ bit assigment for the timing of the trigger output signal output from TRIGGER OUT on the rear panel: ========= ============================= Bit (dec) Description ========= ============================= 5 At the end of the sweep 4 At the end of the pulse width 3 At the end of the pulse cycle 2 At the end of measurement 1 At the start of measurement 0 At the start of occurrence ========= ============================= """ END_OF_SWEEP = 1 << 5 END_OF_THE_PULSE_WIDTH = 1 << 4 END_OF_THE_PULSE_CYCLE = 1 << 3 END_OF_MEASUREMENT = 1 << 2 START_OF_MEASUREMENT = 1 << 1 START_OF_OCCURRENCE = 1 << 0 """ TODO, implement the following commands: (54) LDS? This is a query command for reading the currently set parameters via GPIB. (93) MAR ~; NENT The MAR ~; NENT command sets the search measurement sense channel source, target measurement, and compliance values, and the search channel start/stop and compliance values. It also sets the output state after stopping for both the sense channel and search channel. (94) MAR~;CMD~;NEN During search measurement, you can set ON/OFF of the measurement data comparison calculation and the upper and lower limit data to be compared. Syntax: MAR 0, search mode, generated value after stop; command; NEN 1 Binary search measurement: sense channel 2 Binary search measurement: search channel (negative feedback search) 3 Binary search measurement: search channel (positive feedback search) 4 Linear search measurement: sense channel 5 Linear search measurement: search channel Occurrence value after stopping 1 Generates a bias value. 2 Leave the finished generation value as is. 3 Generate stop values. - The commands that can be used for search measurement are shown below. Binary search Linear search sense channel: FXI, FXV, PXI, PXV, CMD FXI, FXV, PXI, PXV, CMD search channel: WI, WV WI, WV, PWI, PWV, CMD - If you set a command other than the above with the MAR ~; NENT command, an error will occur. - The linear search CMD (comparison operation) command is set to either the sense channel or the search channel. Therefore, if the CMD command is set for both channels, the comparison operation is performed with the one that was set later. Number of steps: 2. When the source range is 600mV, search is performed in steps of 20µV. MAR 0, 1, 2;FXV 1, 20, 6, 17, 2, 0;NENT MAR 0, 2, 2;WV 2, 1, 1, 20, -3, 0, 17, 6.2E-2, -3; NENT (95) PGST~;END # Program number that specifies the command to be executed by the high-speed sequence program. This command stores in memory. Commands that can be used unconditionally DV, DI, PV, PI WT, MST, RV, RI CMD, CN, CL, OPM, FL LTL, DIOS, DIOE, EXT PCEL, MAR ~ NENT Commands that can be used with MAR ~ ; NEN commands FXV, FXI. PXV, PXI, WV, WI, PWV, PWI (96) EXT # This command is used to set a conditional jump in the program of a high-speed sequence program. (97) PGON To execute a high-speed sequence program, store the program in program numbers 1 to 20 with the PGST ; END command in advance. Note that program numbers that do not store programs are skipped without being executed. (98) PGOF This command cancels the start/enable state of the high-speed sequence program set by the PGON command. (99) PCEL This command clears the program stored in memory by the PGST command. """ def map_values(value, values): return values[strict_discrete_set(value, values)] class AdvantestR624X(SCPIUnknownMixin, Instrument): """ Represents the Advantest R624X series (channel A and B) SourceMeter and provides a high-level interface for interacting with the instrument. This is the base class for both AdvantestR6245 and AdvantestR6246 devices. It's not necessary to instantiate this class directly instead create an instance of the AdvantestR6245 or AdvantestR6246 class as shown in the following example: .. code-block:: python smu = AdvantestR6246("GPIB::1") smu.reset() # Set default parameters smu.ch_A.current_source(source_range = CurrentRange.FIXED_60mA, source_value = 0, # Source current at 0 A voltage_compliance = 10) # Voltage compliance at 10 V smu.ch_A.enable_source() # Enables the source output smu.ch_A.measure_voltage() smu.ch_A.current_change_source = 5e-3 # Change to 5mA print(smu.read_measurement()) # Read and print the voltage smu.ch_A.standby() # Put channel A in standby """ def __init__(self, adapter, name="R624X Source meter Base Class", **kwargs): super().__init__(adapter, name, **kwargs) self.sequence = [] self.store_to_sequence = False self.sequence_line_count = 0 def write(self, command, **kwargs): if self.store_to_sequence: self.append_sequence_command(command) else: super().write(command, **kwargs) def check_errors(self): errors = { 100: "A fan stop was detected.", 101: "Since the overload detection of the {0} channel was activated, it was set to" "standby.", 102: "Since the overheat detection of the {0} channel worked, I made it a standby.", 200: "Received an undefined command.", 201: "There is an error in the data format.", 210: "Received data outside the set range.", 211: "A command was received that cannot be executed in the current settings.", 221: "Data output buffer overflowed.", } error = self.ask('err?') unit = int(error[0:2]) err = int(error[2:5]) channel = f'{"B" if unit > 1 else "A"}' if err in errors: message = errors[err].format(channel) elif err > 0 and err < 100: if unit == 0: message = "As a result of the self-test, an abnormality was found in the logic" \ "part." else: message = f"Result of self-test {channel}-channel was found to be abnormal." elif err > 99 and err < 200: message = "Internal error, calibration error" else: message = "Setting error" if err == 0: return else: raise OSError( f"{self.name} Error {error[0:5]}: {message}") def enable_source(self): """ Put channel A & B into the operating state (``CN``). .. note:: When the 'interlock control' of the 'SCT' command is '2' and the clock signal is 'HI', it will not enter the operating state. """ self.write('cn 0') def standby(self): """ Put channel A & B in standby mode (``CL``). """ self.write('cl 0') def clear_status_register(self): """ Clears the Standard Event Status Register (SESR) and related queues (excluding output queues) (``*CLS``). """ self.write('*cls') srq_enabled = Instrument.setting( "s%d", """ Set a boolean that controls whether the GPIB SRQ feature is enabled, takes values of True or False (``S0/S1``). :type: bool The SRQ feature of the GPIB bus provides hardware handshaking between the GPIB controller card in the PC and the instrument. This allows synchronization between moving data to the PC with the state of the instrument without the need to use time delay functions. """, validator=strict_discrete_set, values={False: 1, True: 0}, map_values=True ) def trigger(self): """ Outputs the trigger signal or the start of sweep and search measurement to both A and B channels and the trigger link (``XE``). .. note:: * When both A channel and B channel are waiting for a trigger, both channels are triggered. * When either channel A or B is waiting for a trigger, only the channel that is waiting for a trigger is triggered. * When both A channel and B channel are waiting for sweep start, this will apply sweep start to both channels. * When either channel A or B is in the sweep start waiting state, only the channel in the sweep start waiting state is started. * When either channel A or B is waiting for a trigger and the other is waiting for a sweep start, trigger and sweep start are applied, respectively. * When the trigger link is ON and this is the master unit, set the \\*TRG signal on the trigger link bus to TRUE. * When the trigger link is ON and the master unit, the trigger link is activated. """ self.write('xe 0') def stop(self): """ Stops the sweep when the sweep is started by the XE command or the trigger input signal (``SP``). """ self.write('sp 0') def set_digital_output(self, values): """ Outputs a 16-bit signal from the DIGITAL OUT output terminal on the rear panel. You can set up to 9 output data (``DIOS``). If there are multiple values specified, the data is output at intervals of about 2ms and fixed as the final data. :param values: Digital out bit values :type values: int or list .. note:: The output of digital data to the DIGITAL OUT pin is only the bits specified by the DIOE command. Bits that are not specified will result in alarm output or unused, and no digital data will be output. """ if isinstance(values, list): values = [str(i) for i in values] values = ",".join(values) self.write(f'dios 0,{values}') sweep_delay_time = Instrument.setting( "gdly 0,%.4e", """ Set the sweep delay time (Ta) or generation / delay time (Ta) of the master channel and slave channel during delayed sweep operation or synchronous operation between pulse measurements (``GDLY``). :type: float .. note:: If the sweep delay time does not meet (Ta 100: raise OSError( f"{self.name} Error out of sequence memory") self.store_sequence_command(self.sequence_line_count, command) command = s + ';' self.sequence_line_count += 1 self.store_sequence_command(self.sequence_line_count, command) self.sequence = [] def sequence_wait(self, wait_mode, wait_value): """ Waits for program execution and is used only for sequence programs (``WAIT``). :param int wait_mode: Whether wait time (1) or trigger input count (2) is specified :param float wait_value: Wait time or trigger input count as specified by wait_mode This command has the following functions: * Make the execution of the next program wait for the specified time. * Makes the next program execution wait until the specified number of triggers is input. Regardless of the wait mode, if the wait data is 0, the wait operation is not performed. When the wait mode is "2", the following commands and signals can be used as trigger inputs: * XE (XE 0, XE 1, XE 2) * \\*TRG * GET command (group execute trigger) * Trigger input signal on rear panel """ wait_mode = strict_discrete_set(wait_mode, [1, 2]) self.write(f'wait {wait_mode},{wait_value}') def start_sequence_program(self, start, stop, repeat): """ Starts from the program number until the stop of the sequence program (``RU``). Executes sequentially up to the program number, and repeats for the number of times of specified. :param int start: Number of the program to start from ranging 1 to 100 :param int stop: Number of the program to stop at ranging from 1 to 100 :param int repeat: Number of times repeated from 1 to 100 """ start = truncated_range(start, [1, 100]) stop = truncated_range(stop, [1, 100]) repeat = truncated_range(repeat, [1, 100]) self.write(f'ru 0,{start},{stop},{repeat}') def store_sequence_command(self, line, command): """ Stores the program to be executed in the sequence program (``ST``). If the program already exists, it is replaced with the new sequence. :param int line: Line number specified of memory location :param str command: Command(s) specified to be stored delimited by a semicolon (;) """ line = truncated_range(line, [1, 100]) if command[-1:] != ';': command += ';' self.write(f'st {line};{command}end') def interrupt_sequence_command(self, action): """ Interrupts the sequence program executed by the :py:meth:`~start_sequence_program` command (``SQSP``). :param action: Specifies sequence interruption setup :type action: :class:`SequenceInterruptionType` """ action = strict_discrete_set(action, [1, 2, 3]) self.write(f'sqsp {action}') sequence_program_number = Instrument.measurement( "lnub?", """ Measure the amount of program sequences stored in the sequence memory (``LNUB?``). """, cast=int, ) def sequence_program_listing(self, line): """ This is a query command to know the command list stored in the program number of the sequence program memory (``LST?``). :param int action: Specifying the memory location for reading the commands :return: Commands stored in sequence memory :rtype: str """ line = truncated_range(line, [1, 100]) return self.ask('lst? {line}') def trigger_output_signal(self, trigger_output, alarm_output, scanner_output): """ Directly output the trigger output signal, alarm output signal, scanner (start/stop) output signal from GPIB (``OSIG``). :param int trigger_output: Number specifying type of trigger output :param int alarm_output: Number specifying type of alaram output :param int scanner_output: Number specifying the type of scanner output Trigger output: 1. Do not output to trigger output. 2. Output a negative pulse to the trigger output. Alarm output: 1. Finish output GO, LO.HI both set to HI level. (reset) 2. Finish output Set GO to LO level. 3. Set home output LO to LO level. 4. Terminate output HI to LO level. Scanner - (start/stop) output: 1. Set the scanner scoot output to HI level. Output a negative pulse to the stop output. 2. Make the scanner start output low. 3. Output a HI level for the scanner start output and a negative pulse for the stop output. """ trigger_output = strict_discrete_set(trigger_output, [1, 2]) alarm_output = strict_discrete_set(alarm_output, [1, 2, 3, 4]) scanner_output = strict_discrete_set(scanner_output, [1, 2, 3]) self.write(f'osig 0,{trigger_output},{alarm_output},{scanner_output}') def set_output_format(self, delimiter_format, block_delimiter, terminator): """ Sets the format and terminator of the output data output by GPIB (``FMT``). :param int delimiter_format: Type of delimiter format :param int block_delimiter: Type of block delimiter :param int terminator: Type of termination character The output of (End or Identify) is output at the following timing: 1,2: Simultaneously with LF 4: Simultaneously with the last output data If the output data format is specified as binary format, the terminator is fixed to only and the terminator selection is ignored. delimiter_format: 1. ASCII format with header 2. No header, ASCII format 3. Binary format block_delimiter: 1. Make it the same as the terminator. 2. Use semicolon ; 3. Use comma , terminator: 1. CR, LF 2. LF 3. LF 4. === ================================================================================= 1st character header: ------------------------------------------------------------------------------------- A) Normal measurement data B) Measurement data during overrange C) Compliance (limiter) is working. D) Oscillation detection is working. E) [Indicates the generated data] F) Measurement data when an error occurs in the search measurement Z) Measurement data is not stored in the buffer memory. === ================================================================================= === ================================================================================= 2nd character header: ------------------------------------------------------------------------------------- A) A-channel data during asynchronous operation (A-channel generation data) B) B-channel data during asynchronous operation (B channel generation data) I) A-channel data for synchronous, sweeping, delayed sweep, and double synchronous sweep operations. J) B-channel data for synchronous, sweeping, delayed sweep, and double synchronous sweep operations. === ================================================================================= === ================================================================================= 3rd character header: ------------------------------------------------------------------------------------- A) Current generation, voltage measurement (ISVM) [Current generation] B) Voltage generation, current measurement (VSIM) [Voltage generation] C) Current generation, current measurement (ISIM) D) Voltage generation, voltage measurement (VSVM) E) Current generation, external voltage measurement (IS, EXT, VM) F) Voltage generation, external current measurement (VS, EXT, IM) G) Current generation, external current measurement (IS, EXT. IM) H) Voltage generation, external voltage measurement (VS, EXT, VM) Z) The measurement data is not stored in the buffer memory. === ================================================================================= === ================================================================================= 4th character header: ------------------------------------------------------------------------------------- A) No operation (fixed to A) B) Null operation result C) The result of the comparison operation is GO. D) The result of the comparison operation is LO. E) The result of the comparison operation is HI. F) The result of null operation + comparison operation is GO. G) The result of null operation + comparison operation is LO. H) The result of null operation + comparison operation is HI. Z) Measurement data is not stored in the buffer memory. === ================================================================================= """ delimiter_format = strict_discrete_set(delimiter_format, [1, 2, 3]) block_delimiter = strict_discrete_set(block_delimiter, [1, 2, 3]) terminator = strict_discrete_set(terminator, [1, 2, 3, 4]) self.write(f'fmt 0,{delimiter_format},{block_delimiter},{terminator}') service_request_enable_register = Instrument.control( '*sre?', '*sre %i', """ Control the contents of the service request enable register (SRER) in the form of a :class:`SRER` ``IntFlag`` (``*SRE``). .. note:: Bits other than the RQS bit are not cleared by serial polling. When :meth:`~.power_on_clear` is set, status byte enable register, SESER, device operation enable register, channel operation, the enable register is cleared and no SRQ is issued. """, validator=truncated_range, values=[0, 255], get_process=lambda v: SRER(int(v)), ) event_status_enable = Instrument.control( '*ese?', '*ese %i', """ Control the standard event status enable. (``*ESE``) """, validator=truncated_range, values=[0, 255], ) power_on_clear = Instrument.control( '*psc?', '*psc %i', """ Control the power on clear flag, takes values True or False. (``*PSC``) """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) device_operation_enable_register = Instrument.control( 'doe?', 'doe %i', """ Control the device operation output enable register (DOER) (``DOE?``). """, validator=truncated_range, values=[0, 65535], ) digital_out_enable_data = Instrument.control( 'dioe?', 'dioe 0,%i', """ Control the contents of digital out enable data set (``DIOE``). """, validator=truncated_range, values=[0, 65535], ) status_byte_register = Instrument.measurement( "*stb?", """ Measure the contents of the status byte register and MSS bits without using a serial poll (``*STB?``). The Status Byte Register has a hierarchical structure. ERR, DOP, ESB, and COP bits, except RQS and MAV, have lower-level status registers. Each register is paired with an enable register that can be selected to output to the Status Byte register or not. The status byte register also has an enable register, which allows you to select whether or not to issue a service request SRQ. .. note:: \\*STB? command can read bit 6 as MSS (logical OR of other bits). """, cast=int, ) event_status_register = Instrument.measurement( "*esr?", """ Measure the contents of the standard event status register (SESR) in the form of a :class:`SESR` ``IntFlag`` (``*ESR?``). .. note:: SESR is cleared after being read. """, values=[0, 255], get_process=lambda v: SESR(int(v)), ) device_operation_register = Instrument.measurement( "doc?", """ Measure the contents of the device operations register (DOR) in the form of a :class:`DOR` ``IntFlag`` (``DOC?``). """, values=range(0, 65535), get_process=lambda v: DOR(int(v)), ) error_register = Instrument.measurement( "err?", """ Measure the contents of the error register (``ERR?``). """, cast=int, ) self_test = Instrument.measurement( "*tst?", """ A query command that runs a self-test and reads the result (``*TST?``). """, cast=int, ) trigger_link_function_enabled = Instrument.setting( "tlnk 0,%d", """ Set a boolean that controls whether the trigger link function is enabled, takes values of True or False. (``TLNK``) :type: bool """, validator=strict_discrete_set, values={False: 1, True: 2}, map_values=True ) display_enabled = Instrument.setting( "disp 0,%d", """ Set a boolean that controls whether the display is on or off, takes values of True or False. (``DISP``) :type: bool """, validator=strict_discrete_set, values={False: 2, True: 1}, map_values=True ) line_frequency = Instrument.setting( "lf 0,%d", """ Set the used power supply frequency (``LF``) to 50 or 60hz. With this command, the integration time per PLC for the measurement will be one cycle of the power supply frequency you are using. :type: int """, validator=strict_discrete_set, values={50: 1, 60: 2}, map_values=True ) store_config = Instrument.setting( "sav %d", """ Set the memory area for the config to be stored at (``SAV``). There are five memory areas from 0 to 4 for storing. :type: int """, validator=strict_range, values=range(0, 4), ) load_config = Instrument.setting( "rcl %d", """ Set the memory area for the config to be loaded from (``RCL``). There are five areas (0~4) where parameters can be loaded by the RCL command. :type: int """, validator=strict_range, values=range(0, 4), ) def set_lo_common_connection_relay(self, enable, lo_relay=None): """ Turn the connection relay on/off between the A channel LO (internal analog common) and the LO (internal analog common) of the B channel (``LTL``). :param bool enable: A boolean property that controls whether or not the connection relay is enabled. Valid values are True and False. :param lo_relay: A boolean property that controls whether or not the internal analog common relay is enabled. Valid values are True, False and None (don't change lo relay setting). :type lo_relay: bool, optional """ enable = map_values(enable, {True: 2, False: 1}) lo_relay = map_values(lo_relay, {True: 2, False: 1, None: 3}) self.write(f'ltl 0,{enable},{lo_relay}') def parse_measurement(self, measurement): if ' ' in measurement: measurement = measurement.split(' ') return (float(measurement[1]), measurement[0]) else: return (float(measurement), None) def read_measurement(self): """ Reads the triggered value, for example triggered by the external input. """ return self.parse_measurement(self.read())[0] class SMUChannel(Channel): """ Instantiated by main instrument class for every SMUChannel """ def __init__(self, parent, id, voltage_range, current_range): super().__init__(parent, id) self.voltage_range = voltage_range[id] self.current_range = current_range[id] def insert_id(self, command): return command.format_map({self.placeholder: ord(self.id) - 64}) def clear_measurement_buffer(self): """ Clears the measurement data buffer (``MBC``). """ self.write('mbc {ch}') def set_output_type(self, output_type, measurement_type): """ Sets the output method and type of the GPIB output (``OFM``). :param output_type: A property that controls the type of output :type output_type: int or :class:`OutputType` :param measurement_type: A property that controls the measurement type :type measurement_type: int or :class:`MeasurementType` .. note:: For the format of the output data, refer to :meth:`AdvantestR624X.set_output_format`. For DC and pulse measurements, the output method is fixed to '1' (real-time output). When the output method '3' (buffering output) is specified, the measured data is not stored in memory. """ output_type = OutputType(output_type) measurement_type = MeasurementType(measurement_type) self.write(f'ofm {{ch}},{output_type.value},{measurement_type.value}') analog_input = Channel.setting( "fl {ch},%d", """ Set the analog input terminal (ANALOG INPUT) on the rear panel ON or OFF (``FL``). :type: int 1. Turn off the analog input. 2. Analog input ON, gain x1. 3. Analog input ON, gain x2.5. """, validator=strict_range, values=range(1, 3), ) trigger_output_timing = Channel.setting( "tot {ch},%d", """ Set the timing of the trigger output signal output from TRIGGER OUT on the rear panel (``TOT``). the status in the form of a :class:`TriggerOutputSignalTiming` ``IntFlag``. :type: :class:`.TriggerOutputSignalTiming` """, validator=strict_range, values=range(0, 63), # get_process=lambda v: TriggerOutputSignalTiming(int(v)), ) def set_scanner_control(self, output, interlock): """ Sets the SCANNER CONTROL (START, STOP) output signal and INTERLOCK input signal on the rear panel (``SCT``). :param int output: A property that controls the scanner output :param int interlock: A property that controls the scanner interlock type output: 1. Scanner, Turn off the control signal output. 2. Output to the scanner control signal at the start / stop of the sweep. 3. Operate / Standby Scanner, Output to the control signal. interlock: 1. Turn off the interlock signal input. 2. Set as a stamper when the interlock signal input is HI. 3. When the interlock signal input is HI, it is on standby, and when it is LO, it is operated. """ output = strict_discrete_set(output, [1, 2, 3]) interlock = strict_discrete_set(interlock, [1, 2, 3]) self.write(f'sct {{ch}},{output},{interlock}') trigger_input = Channel.setting( "tjm {ch},%d", """ Set the type of trigger input (``TJM``). :type: :class:`.TriggerInputType` +------------------------+---+---+---+ | Trigger input types | 1 | 2 | 3 | +========================+===+===+===+ | \\*TRG | O | O | X | +------------------------+---+---+---+ | XE 0 | O | O | X | +------------------------+---+---+---+ | XE Channel | O | O | O | +------------------------+---+---+---+ | GET | O | O | X | +------------------------+---+---+---+ | Trigger input signal | O | X | X | +------------------------+---+---+---+ O can be used, X cannot be used .. note:: The sweep operation cannot be started by the trigger input signal. Be sure to start it with the 'XE' command. Once started, it is possible to advance the sweep with a trigger input signal. """, validator=strict_range, values=range(1, 3), # get_process=lambda v: TriggerInputType(int(v)), ) fast_mode_enabled = Channel.setting( "fl {ch},%d", """ Set the channel response mode to fast or slow, takes values of True or False (``FL``). :type: bool """, validator=strict_discrete_set, values={False: 2, True: 1}, map_values=True ) sample_hold_mode = Channel.setting( "mst {ch},%d", """ Set the integration time of the measurement (``MST``). :type: :class:`.SampleHold` .. note:: - Valid only for pulse measurement and pulse sweep measurement. - In sample hold mode, the AD transformation is just before the fall of the pulse width. - The sample hold mode cannot be set during DC measurement and DC sweep measurement. When set to sample-and-hold mode, the integration time is 100 µs. However, in 2-channel synchronous operation, if one channel is in pulse generation and the other is in sample-and-hold mode, the DC measurement side also operates in sample-and-hold mode. - When performing pulse measurement and pulse sweep measurement, it is necessary to satisfy the restrictions on the pulse width (Tw), pulse period (Tp), and measure delay time (Td) of the WT command. If the constraint is not satisfied, the integration time is unchanged. To lengthen the integration time, first change the pulse width (Tw) and pulse period (Tp). When shortening the pulse width and pulse cycle, shorten the integration time first. """, validator=strict_discrete_set, values=[0, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], # get_process=lambda v: SampleHold(int(v)), ) def set_sample_mode(self, mode, auto_sampling=True): """ Sets synchronous, asynchronous, tracking operation and search measurement between channels (``JM``). :param mode: Sample Mode :type mode: :class:`.SampleMode` :param auto_sampling: Whether or not auto sampling is enabled, defaults to True :type auto_sampling: bool, optional """ mode = SampleMode(mode) auto_sampling = map_values(auto_sampling, {True: 1, False: 2}) self.write(f'jm {mode.value},{auto_sampling},{{ch}}') def set_timing_parameters(self, hold_time, measurement_delay, pulsed_width, pulsed_period): """ Set the hold time, measuring time, pulse width and the pulse period (``WT``). :param float hold_time: total amount of time for the complete pulse, until next pulse comes :param float measurement_delay: time between measurements :param float pulsed_width: Time specifying the pulse width :param float pulsed_period: Time specifying the pulse period .. note:: Pulse measurement has the following restrictions depending on the pulse period (Tp) setting. (For pulse sweep measurements, there are no restrictions.) - Tp < 2ms : Not measured. - 2ms <= Tp < 10ms : Measure once every 5 ~ 20ms. - 10ms <= Tp: Measured at each pulse generation. """ self.write(f"wt {{ch}},{hold_time:.4e},{measurement_delay:.4e},{pulsed_width:.4e}," "{pulsed_period:.4e}") def select_for_output(self): """ This is a query command to select a channel and to output the measurement data (``FCH?``). When the output channel is selected by the FCH command, the measured data of the same channel is returned until the output channel is changed by the next FCH command. .. note:: Reading measurements with the RMM command does not affect channel specification with the FCH command. In the default state, the measurement data of channel A is output. """ self.write("fch_0{ch}?") def trigger(self): """ Measurement trigger command for sweep, start search measurement or sweep step action (``XE``). """ self.write('xe {ch}') ############### # Voltage (V) # ############### def measure_voltage(self, enable=True, internal_measurement=True, voltage_range=VoltageRange.AUTO): """ Sets the voltage measurement ON/OFF, measurement input, and voltage measurement range as parameters (``RV``). :param enable: boolean property that enables or disables voltage measurement. Valid values are True (Measure the voltage flowing at the OUTPUT terminal) and False (Measure the voltage from the rear panel -ANALOG COMMON). :type enable: bool, optional :param internal_measurement: A boolean property that enables or disables the internal measurement. :type internal_measurement: bool, optional :param voltage_range: Specifying voltage range :type voltage_range: :class:`.VoltageRange`, optional """ voltage_range = VoltageRange(voltage_range) enable = map_values(enable, {True: 1, False: 2}) internal_measurement = map_values(internal_measurement, {True: 1, False: 2}) self.write(f'rv {{ch}},{enable},{internal_measurement},{voltage_range.value}') def voltage_source(self, source_range, source_value, current_compliance): """ Sets the source range, source value and the current compliance for the DC (constant voltage) measurement (``DV``). :param source_range: Specifying source range :type source_range: :class:`.VoltageRange` :param float source_value: A number specifying the source voltage value :param float current_compliance: A number specifying the current compliance .. note:: Regardless of the specified current compliance polarity, both polarities (+ and -) are set. The current compliance range is automatically set to the minimum range that includes the set value. """ source_range = VoltageRange(source_range) source_value = truncated_range(source_value, self.voltage_range) self.write(f'dv {{ch}},{source_range.value},{source_value:.4e},{current_compliance:.4e}') def voltage_pulsed_source(self, source_range, pulse_value, base_value, current_compliance): """ Sets the source range, pulse value, base value and the current compliance of the pulse (voltage) measurement (``PV``). .. note:: Regardless of the specified current compliance polarity, both polarities (+ and -) are set. The current compliance range is automatically set to the minimum range that includes the set value. """ source_range = VoltageRange(source_range) pulse_value = truncated_range(pulse_value, self.voltage_range) base_value = truncated_range(base_value, self.voltage_range) self.write(f'pv {{ch}},{source_range.value},{pulse_value:.4e},{base_value:.4e},' '{current_compliance:.4e}') change_source_voltage = Channel.setting( "spot {ch},%.4e", """ Set new target voltage (``SPOT``). :type: float .. note:: Only the DC action source value and pulse action pulse value are changed using the currently set DC action and pulse action parameters. Measure after the change and set the channel to output the measured data to the specified ch. In other words, it's the same as running the following commands: 1. DV/DI/PV/PI 2. XE xx 3. FCH xx """, ) def voltage_fixed_level_sweep( self, voltage_range, voltage_level, measurement_count, current_compliance, bias=0): """ Sets the fixed level sweep (voltage) generation range, level value, current compliance and the bias value (``FXV``). .. note:: Regardless of the specified current compliance polarity, both polarities (+ and -) are set. The current compliance range is automatically set to the minimum range that includes the set value. """ voltage_range = VoltageRange(voltage_range) voltage_level = truncated_range(voltage_level, self.voltage_range) self.write(f'fxv {{ch}},{voltage_range.value},{voltage_level:.4e},' '{measurement_count},{current_compliance:.4e},{bias:.4e}') def voltage_fixed_pulsed_sweep( self, voltage_range, pulse, base, measurement_count, current_compliance, bias=0): """ Sets the fixed pulse (voltage) sweep generation range, pulse value, base value, number of measurements, current compliance and the bias value (``PXV``). .. note:: Regardless of the specified current compliance polarity, both polarities (+ and -) are set. The current compliance range is automatically set to the minimum range that includes the set value. """ voltage_range = VoltageRange(voltage_range) pulse = truncated_range(pulse, self.voltage_range) base = truncated_range(base, self.voltage_range) self.write(f'pxv {{ch}},{voltage_range.value},{pulse:.4e},{base:.4e},' '{measurement_count},{current_compliance:.4e},{bias:.4e}') def voltage_sweep( self, sweep_mode, repeat, voltage_range, start_value, stop_value, steps, current_compliance, bias=0): """ Sets the sweep mode, number of repeats, source range, start value, stop value, number of steps, current compliance, and the bias value for staircase (linear/log) voltage sweep (``WV``). .. note:: - Sweep mode, number of repeats, and number of steps are subject to the following restrictions. - Let N = number of steps, m = l (one-way sweep), m = 2 (round-trip sweep). - When the OFM command sets the output data output method to 1 or 2 m x number of refreshes x N <= 2048 - m x N <= 2048 when the OFM command sets the output data output method to 3. - Regardless of the specified current compliance polarity, both polarities (+ and -) are set. - The current compliance range is automatically set to the minimum range that includes the set value. """ sweep_mode = SweepMode(sweep_mode) repeat = truncated_range(repeat, [0, 1024]) steps = truncated_range(steps, [2, 2048]) voltage_range = VoltageRange(voltage_range) self.write(f'wv {{ch}},{sweep_mode.value},{repeat},{voltage_range.value},' '{start_value:.4e},{stop_value:.4e},{steps}, ' '{current_compliance:.4e},{bias:.4e}') def voltage_pulsed_sweep( self, sweep_mode, repeat, voltage_range, base, start_value, stop_value, steps, current_compliance, bias=0): """ Sets the sweep mode, repeat count, generation range, base value, start value, stop value, number of steps, current compliance and the bias value for a pulse wave (linear/log) voltage sweep (``PWV``). .. note:: - The sweep mode, number of refreshes, and number of steps are subject to the following restrictions: - Let N = number of steps, m = l (one-way sweep), m = 2 (round-trip sweep). - When the OFM command sets the output data output method to 1 or 2 m x number of refreshes x N <= 2048 - m x N <= 2048 when the OFM command sets the output data output method to 3. - For the current compliance polarity, regardless of the specified current compliance polarity, the compliance of both polarities (+ and -) is set. - The current compliance range is automatically set to the minimum range that includes the set value. """ sweep_mode = SweepMode(sweep_mode) repeat = truncated_range(repeat, [0, 1024]) steps = truncated_range(steps, [2, 2048]) voltage_range = VoltageRange(voltage_range) self.write(f'pwv {{ch}},{sweep_mode.value},{repeat},{voltage_range.value},{base:.4e},' '{start_value:.4e},{stop_value:.4e},{steps},{current_compliance:.4e},' '{bias:.4e}') def voltage_random_sweep( self, sweep_mode, repeat, start_address, stop_address, current_compliance, bias=0): """ Sets the sweep mode, repeat count, start address, stop address, current compliance and the bias value of constant voltage random sweep (``MDWV``). .. note:: - Sweep mode, number of repeats, start address and stop address are subject to the following restrictions: - Start address < Stop address - Let N = number of steps, m = l (one-way sweep), m = 2 (round-trip sweep). - When the OFM command sets the output data output method to 1 or 2 m x number of refreshes x N <= 2048 - m x N <= 2048 when the OFM command sets the output data output method to 3. - Regardless of the specified current compliance polarity, both polarities (+ and -) are set. - The current compliance range is automatically set to the minimum range that includes the set value. """ sweep_mode = SweepMode(sweep_mode) repeat = truncated_range(repeat, [0, 1024]) start_address = truncated_range(start_address, [1, 2048]) stop_address = truncated_range(stop_address, [1, 2048]) self.write(f'mdwv {{ch}},{sweep_mode.value},{repeat},{start_address},{stop_address},' '{current_compliance:.4e},{bias:.4e}') def voltage_random_pulsed_sweep( self, sweep_mode, repeat, start_address, stop_address, current_compliance, bias=0): """ Sets the sweep mode, repeat count, base value, start address, stop address, current compliance and the bias value of the constant voltage random pulse sweep (``MPWV``). .. note:: - Sweep mode, number of repeats, start address and stop address are subject to the following restrictions: - Start address < Stop address - Let N = number of steps, m = l (one-way sweep), m = 2 (round-trip sweep). - When the OFM command sets the output data output method to 1 or 2 m x number of refreshes x N <= 2048 - m x N <= 2048 when the OFM command sets the output data output method to 3. - Regardless of the specified current compliance polarity, both polarities (+ and -) are set. - The current compliance range is automatically set to the minimum range that includes the set value. """ sweep_mode = SweepMode(sweep_mode) repeat = truncated_range(repeat, [0, 1024]) start_address = truncated_range(start_address, [1, 2048]) stop_address = truncated_range(stop_address, [1, 2048]) self.write(f'mpwv {{ch}},{sweep_mode.value},{repeat},{start_address},{stop_address},' '{current_compliance:.4e},{bias:.4e}') def voltage_set_random_memory(self, address, voltage_range, output, current_compliance): """ The command stores the specified value to the randomly generated data memory (``RMS``). Stored generated values are swept within the specified memory address range by the MDWV, MDWI, MPWV, MPWI commands. """ voltage_range = VoltageRange(voltage_range) address = truncated_range(address, [1, 2048]) self.write(f'rms {address};dv{{ch}},{voltage_range.value},{output:.4e},' '{current_compliance:.4e};rend') ############### # Current (A) # ############### def current_source(self, source_range, source_value, voltage_compliance): """ Sets the source range, source value, voltage compliance of the DC (constant current) measurement (``DI``). :param source_range: Specifying source range :type source_range: :class:`.CurrentRange` :param float source_value: A number specifying the source current value :param float voltage_compliance: A number specifying the voltage compliance .. note:: Regardless of the specified voltage compliance polarity, both polarities (+ and -) are set. The voltage compliance range is automatically set to the minimum range that includes the set value. """ source_range = CurrentRange(source_range) source_value = truncated_range(source_value, self.current_range) self.write(f'di {{ch}},{source_range.value},{source_value:.4e},{voltage_compliance:.4e}') def current_pulsed_source(self, source_range, pulse_value, base_value, voltage_compliance): """ Sets the source range, pulse value, base value and the voltage compliance of the pulse (current) measurement (``PI``). .. note:: Regardless of the specified voltage compliance polarity, both polarities (+ and -) are set. The voltage compliance range is automatically set to the minimum range that includes the set value. """ source_range = CurrentRange(source_range) pulse_value = truncated_range(pulse_value, self.current_range) base_value = truncated_range(base_value, self.current_range) self.write(f'pi {{ch}},{source_range.value},{pulse_value:.4e},{base_value:.4e},' '{voltage_compliance:.4e}') change_source_current = Channel.setting( "spot {ch},%.4e", """ Set new target current (``SPOT``). :type: float .. note:: Only the DC action source value and pulse action pulse value are changed using the currently set DC action and pulse action parameters. Measure after the change and set the channel to output the measured data to the specified ch. In other words, it's the same as running the following commands: 1. DV/DI/PV/PI 2. XE xx 3. FCH xx """ ) def current_fixed_level_sweep( self, current_range, current_level, measurement_count, voltage_compliance, bias=0): """ Sets the fixed level sweep (current) generation range, level value, voltage compliance and the bias value (``FXI``). .. note:: Regardless of the specified voltage compliance polarity, both polarities (+ and -) are set. The voltage compliance range is automatically set to the minimum range that includes the set value. """ current_range = CurrentRange(current_range) self.write(f'fxi {{ch}},{current_range.value},{current_level:.4e},{measurement_count},' '{voltage_compliance:.4e},{bias:.4e}') def current_fixed_pulsed_sweep( self, current_range, pulse, base, measurement_count, voltage_compliance, bias=0): """ Sets the fixed pulse (current) sweep generation range, pulse value, base value, number of measurements, voltage compliance and the bias value (``PXI``). .. note:: Regardless of the specified voltage compliance polarity, both polarities of + and - are set. The voltage compliance range is automatically set to the minimum range that includes the set value. """ current_range = CurrentRange(current_range) self.write(f'pxi {{ch}},{current_range.value},{pulse:.4e},{base:.4e},{measurement_count},' '{voltage_compliance:.4e},{bias:.4e}') def current_sweep( self, sweep_mode, repeat, current_range, start_value, stop_value, steps, voltage_compliance, bias=0): """ Sets the sweep mode, number of repeats, source range, start value, stop value, number of steps, voltage compliance and bias value for the staircase (linear/log) current sweep (``WI``). .. note:: - The sweep mode, number of refreshes, and number of steps are subject to the following restrictions: - Let N = number of steps, m = l (one-way sweep), m = 2 (round-trip sweep). - When the OFM command sets the output data output method to 1 or 2, m x number of repeats x N <= 2048. - m x N <= 2048 when the OFM command sets the output data output method to 3. - Regardless of the specified voltage compliance polarity, both polarities (+ and -) are set. - The voltage compliance range is automatically set to the minimum range that includes the set value. """ sweep_mode = SweepMode(sweep_mode) repeat = truncated_range(repeat, [0, 1024]) steps = truncated_range(steps, [2, 2048]) current_range = CurrentRange(current_range) self.write(f'wi {{ch}},{sweep_mode.value},{repeat},{current_range.value},' '{start_value:.4e},{stop_value:.4e},{steps},{voltage_compliance:.4e},' '{bias:.4e}') def current_pulsed_sweep( self, sweep_mode, repeat, current_range, base, start_value, stop_value, steps, voltage_compliance, bias=0): """ Sets the sweep mode, repeat count, generation range, base value, start value, stop value, number of steps, voltage compliance and the bias value for a pulse wave (linear/log) current sweep (``PWI``). .. note:: - The sweep mode, number of refreshes, and number of steps are subject to the following restrictions: - Let N = number of steps, m = l (one-way sweep), m = 2 (round-trip sweep). - When the OFM command sets the output data output method to 1 or 2, m x number of repeats x N <= 2048. - m x N <= 2048 when the OFM command sets the output data output method to 3. - Regardless of the specified voltage compliance polarity, both polarities (+ and -) are set. - The voltage compliance range is automatically set to the minimum range that includes the set value. """ sweep_mode = SweepMode(sweep_mode) repeat = truncated_range(repeat, [0, 1024]) steps = truncated_range(steps, [2, 2048]) current_range = CurrentRange(current_range) self.write( f'pwi {{ch}},{sweep_mode.value},{repeat},{current_range.value},{base:.4e},' '{start_value:.4e},{stop_value:.4e},{steps},{voltage_compliance:.4e},{bias:.4e}') def measure_current(self, enable=True, internal_measurement=True, current_range=CurrentRange.AUTO): """ Set the current measurement ON/OFF, measurement input, and current measurement range as parameters (``RI``). :param enable: boolean property that enables or disables current measurement. Valid values are True (Measure the current flowing at the OUTPUT terminal) and False (Measure the current from the rear panel -ANALOG COMMON). :type enable: bool, optional :param internal_measurement: A boolean property that enables or disables the internal measurement. :type internal_measurement: bool, optional :param current_range: Specifying voltage range :type current_range: :class:`.CurrentRange`, optional """ current_range = CurrentRange(current_range) enable = map_values(enable, {True: 1, False: 2}) internal_measurement = map_values(internal_measurement, {True: 1, False: 2}) self.write(f'ri {{ch}},{enable},{internal_measurement},{current_range.value}') def current_random_sweep( self, sweep_mode, repeat, start_address, stop_address, current_compliance, bias=0): """ Sets the sweep mode, repeat count, start address, stop address, voltage compliance and the bias value of constant current random sweep (``MDWI``). .. note:: - Sweep mode, number of repeats, start address and stop address are subject to the following restrictions: - Start address < Stop address - Let N = (stop number 1 - start number + 1), m = 1 (one-way sweep), m = 2 (round-trip sweep). - When the output data output method is set to 1 or 2 with the OFM command m x number of repeats x N <= 2048 - When the output data output method is set to 3 with the OFM command m x N <= 2048 - For the voltage compliance polarity, regardless of the specified voltage compliance polarity, both polarities of + and – are set. - The voltage compliance range is automatically set to the minimum range that includes the set value. """ sweep_mode = SweepMode(sweep_mode) repeat = truncated_range(repeat, [0, 1024]) start_address = truncated_range(start_address, [1, 2048]) stop_address = truncated_range(stop_address, [1, 2048]) self.write( f'mdwi {{ch}},{sweep_mode.value},{repeat},{start_address},{stop_address},' '{current_compliance:.4e},{bias:.4e}') def current_random_pulsed_sweep( self, sweep_mode, repeat, start_address, stop_address, current_compliance, bias=0): """ Sets the sweep mode, repeat count, base value, start address, stop address, voltage compliance and the bias value of constant current random pulse sweep (``MPWI``). .. note:: - Sweep mode, number of repeats, start address and stop address are subject to the following restrictions: - Start address < Stop address - Let N = (stop number 1 - start number + 1), m = 1 (one-way sweep), m = 2 (round-trip sweep). - When the output data output method is set to 1 or 2 with the OFM command m x number of repeats x N <= 2048 - When the output data output method is set to 3 with the OFM command m x N <= 2048 - For the voltage compliance polarity, regardless of the specified voltage compliance polarity, both polarities of + and – are set. - The voltage compliance range is automatically set to the minimum range that includes the set value. """ sweep_mode = SweepMode(sweep_mode) repeat = truncated_range(repeat, [0, 1024]) start_address = truncated_range(start_address, [1, 2048]) stop_address = truncated_range(stop_address, [1, 2048]) self.write( f'mpwi {{ch}},{sweep_mode.value},{repeat},{start_address},{stop_address},' '{current_compliance:.4e},{bias:.4e}') def current_set_random_memory(self, address, current_range, output, voltage_compliance): """ Store the current parameters to randomly generated data memory (``RMS``). Stored generated values are swept within the specified memory address range by the MDWV, MDWI, MPWV, MPWI commands. """ current_range = CurrentRange(current_range) address = truncated_range(address, [1, 2048]) self.write( f'rms {address};di{{ch}},{current_range.value},{output:.4e},' '{voltage_compliance:.4e};rend') def read_random_memory(self, address): """ Return memory specified by address location (``RMS?``). :param int address: Adress to specify memory location. :returns: Set values returned by the device from the specified address location. :rtype: str """ address = truncated_range(address, [1, 2048]) return self.ask(f'rms_1{{ch}}? {address}') def enable_source(self): """ Put the specified channel into an operating state (``CN``). """ self.write('cn {ch}') def standby(self): """ Put the specified channel into standby state (``CL``). """ self.write('cl {ch}') def stop(self): """ Stops the sweep when the sweep is started by the XE command or the trigger input signal (``SP``). """ self.write('sp {ch}') def output_all_measurements(self): """ Output all measurements in the measurement data buffer of the specified channel (``RMM?``). .. note:: For the output format, refer to :meth:`AdvantestR624X.set_output_format`. When a memory address where no measurement data is stored is read, 999.999E+99 will be returned. """ self.write('rmm_0{ch}?') def read_measurement_from_addr(self, addr): """ Output only one measurement at the specified memory address from the measurement data buffer of the specified channel. :param int addr: Specifies the address to read from. :return: float Measurement data .. note:: For the output format, refer to :meth:`AdvantestR624X.set_output_format`. When a memory address where no measurement data is stored is read, 999.999E+99 will be returned. """ measurement = self.ask(f'rmm_1{{ch}}? {addr}') return self.parent.parse_measurement(measurement) measurement_count = Channel.measurement( "nub_0{ch}?", """ Measaure the number of measurements contained in the measurement data buffer (``NUB?``). """, cast=int ) null_operation_enabled = Channel.setting( "nug {ch},%d", """ Set a boolean that controls whether the null operation is enabled, takes values of True or False (``NUG``). :type: bool .. Acquisition timing of null data:: - Null data captures the next measurement data for which null computation is enabled as null data during DC measurement or pulse measurement. - A sweep operation does not capture null data. - If null calculation is enabled during sweep operation, null data obtained by DC operation or pulse operation will be used for calculation. - Indicates the timing of null data acquisition during DC operation. .. note:: - Null data is not rewritten even if the null operation is disabled. - Null data is rewritten only when null operation is changed from OFF to ON or initialized in case of DC operation or pulse operation. """, validator=strict_discrete_set, values={False: 1, True: 2}, map_values=True ) def set_wire_mode(self, four_wire, lo_guard=True): """ Used to switch remote sense and to set the LO-GUARD relay ON/OFF. It operates regardless of operating state or standby state (``OSL``). :param bool four_wire: A boolean property that enables or disables four wire measurements. Valid values are True (enables 4-wire sensing) and False (enables two-terminal sensing). :param bool lo_guard: A boolean property that enables or disables the LO-GUARD relay. """ four_wire = map_values(four_wire, {True: 1, False: 2}) lo_guard = map_values(lo_guard, {True: 1, False: 2}) self.write(f'osl {{ch}},{four_wire},{lo_guard}') auto_zero_enabled = Channel.setting( "cm {ch},%d", """ Set the auto zero option to ON or OFF. Valid values are True (enabled) and False (disabled) (``CM``). :type: bool This command sets auto zero (automatically calibrate the zero point of the measured value operation. 1. Periodically perform auto zero. 2. Auto zero once, no periodic auto zeros thereafter. When the auto zero mode is set to True, the following operations are performed. - For DC operation and pulse operation: - At the end of one sweep, if he has exceeded the last autozero by more than 10 seconds, he will do one autozero. - If sweep start is specified during auto zero, the sweep will start after auto zero ends. - Sweep operation - Auto zero is performed once every 10 seconds. - If measurement or pulse output is specified during auto zero, it will be executed after auto zero ends. """, validator=strict_discrete_set, values={False: 2, True: 1}, map_values=True ) def set_comparison_limits(self, comparison, voltage_value, upper_limit, lower_limit): """ Sets the channel ON/OFF based on the measurement comparison and the data of the upper and lower limits to be compared (``CMD``). :param bool comparison: A boolean property that controls whether or not the comparison function is enabled. Valid values are True or False. :param bool voltage_value: A boolean property that controls whether or not voltage or current values are passed. Valid values are True or False. :param float upper_limit: Number specifying the upper comparison limit :param float lower_limit: Number specifying the lower comparison limit """ comparison = map_values(comparison, {True: 2, False: 1}) voltage_value = map_values(voltage_value, {True: 1, False: 2}) self.write(f'cmd {{ch}},{comparison},{voltage_value},{upper_limit:.4e},{lower_limit:.4e}') relay_mode = Channel.setting( "opm {ch},%d", """ Set the HI/LO relays for standby mode. This command does not operate the Operate Relay (``OPM``). :type: int 1. When executing an operation only the HI side turns ON, in standby both HI and LO are turned OFF. 2. When executing an operation only the LO side turns ON, in standby both HI and LO are turned OFF. 3. When executing an operation both HI and LO turn ON, in standby both HI and LO are turned OFF. 4. When executing an operation only the HI side turns ON, in standby only the HI side is turned OFF. """, validator=strict_range, values=range(1, 4), ) operation_register = Channel.measurement( "coc_0{ch}?", """ Measure the contents of the Channel Operations Register (COR) in the form of a :class:`COR` ``IntFlag`` (``COC?``). """, values=range(0, 65535), get_process=lambda v: COR(int(v)), ) output_enable_register = Channel.control( "coe_0{ch}?", "coe_0{ch} %d", """ Control the settings of the channel operation output enable register (COER) in the form of a :class:`COR` IntFlag ?(``COE?``). """, validator=strict_range, values=range(0, 65535), get_process=lambda v: COR(int(v)), ) def calibration_init(self): """ Initialize the calibration data (``CINI``). """ self.write('cini {ch}') def calibration_store_factor(self): """ Store the calibration factor in the non-volatile memory (EEPROM) (``CSRT``). """ self.write('csrt {ch}') calibration_measured_value = Channel.setting( "std {ch},%.4e", """ Set the measured value measured by an external standard for the generated value of this instrument and start calibration (``STD``). :type: float """, ) calibration_generation_factor = Channel.setting( "ccs {ch},%.4e", """ Set the increment or decrement for the generation calibration factor of the current generation range (``CCS``). It is used when the generated value deviates from the true value. :type: float """, ) calibration_factor = Channel.setting( "ccm {ch},%.4e", """ Set the increment of the measurement calibration factor of the current measurement range (``CCM``). :type: float """, ) class AdvantestR6245(AdvantestR624X): """ Main instrument class for Advantest R6245 DC Voltage/Current Source/Monitor """ voltage_range = {'A': [-220.0, 220.0], 'B': [-220.0, 220.0]} current_range = {'A': [-2.0, 2.0], 'B': [-2.0, 2.0]} ch_A = Instrument.ChannelCreator(SMUChannel, 'A', voltage_range=voltage_range, current_range=current_range) ch_B = Instrument.ChannelCreator(SMUChannel, 'B', voltage_range=voltage_range, current_range=current_range) def __init__(self, adapter, name="Advantest R6245 SourceMeter", **kwargs): kwargs super().__init__( adapter, name, **kwargs ) class AdvantestR6246(AdvantestR624X): """ Main instrument class for Advantest R6246 DC Voltage/Current Source/Monitor """ voltage_range = {'A': [-62.0, 62.0], 'B': [-220.0, 220.0]} current_range = {'A': [-20.0, 20.0], 'B': [-2.0, 2.0]} ch_A = Instrument.ChannelCreator(SMUChannel, 'A', voltage_range=voltage_range, current_range=current_range) ch_B = Instrument.ChannelCreator(SMUChannel, 'B', voltage_range=voltage_range, current_range=current_range) def __init__(self, adapter, name="Advantest R6246 SourceMeter", **kwargs): super().__init__( adapter, name, **kwargs ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3936055 pymeasure-0.14.0/pymeasure/instruments/agilent/0000755000175100001770000000000014623331176021251 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/__init__.py0000644000175100001770000000321214623331163023354 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .agilent4294A import Agilent4294A from .agilent33220A import Agilent33220A from .agilent33500 import Agilent33500 from .agilent33521A import Agilent33521A from .agilentB1500 import AgilentB1500 from .agilent4284A import Agilent4284A ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilent33220A.py0000644000175100001770000003006514623331163023741 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set,\ strict_range, joined_validators from time import time from pyvisa.errors import VisaIOError import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) # 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(SCPIUnknownMixin, 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_state = True # Enable burst mode wfg.burst_ncycles = 10 # 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" print(wfg.check_errors()) # Get the error queue """ def __init__(self, adapter, name="Agilent 33220A Arbitrary Waveform generator", **kwargs): super().__init__( adapter, name, **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=joined_validators( strict_discrete_set, 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=joined_validators( strict_discrete_set, 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=joined_validators( strict_discrete_set, 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 %g", """ 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 0.1 and 0.9 of the threshold. Valid values are between 5 ns to 100 ns. The transition time has to be smaller than 0.625 * the pulse width. 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=joined_validators( strict_discrete_set, 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), cast=lambda v: int(float(v)) ) 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=joined_validators( strict_discrete_set, 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=joined_validators( strict_discrete_set, string_validator ), values=[["LOC", "LOCAL", "REM", "REMOTE", "RWL", "RWLOCK"], ], ) 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") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilent33500.py0000644000175100001770000007717314623331163023654 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel, SCPIUnknownMixin 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 Agilent33500Channel(Channel): """Implementation of a base Agilent 33500 channel""" shape = Instrument.control( "SOUR{ch}:FUNC?", "SOUR{ch}: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( "SOUR{ch}:FREQ?", "SOUR{ch}: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. """, validator=strict_range, values=[1e-6, 120e6], ) amplitude = Instrument.control( "SOUR{ch}:VOLT?", "SOUR{ch}: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.""", validator=strict_range, values=[10e-3, 10], ) amplitude_unit = Instrument.control( "SOUR{ch}:VOLT:UNIT?", "SOUR{ch}:VOLT:UNIT %s", """ A string property that controls the units of the amplitude. Valid values are VPP (default), VRMS, and DBM.""", validator=strict_discrete_set, values=["VPP", "VRMS", "DBM"], ) offset = Instrument.control( "SOUR{ch}:VOLT:OFFS?", "SOUR{ch}: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). """, validator=strict_range, values=[-4.995, +4.995], ) voltage_high = Instrument.control( "SOUR{ch}:VOLT:HIGH?", "SOUR{ch}:VOLT:HIGH %f", """ A floating point property that controls the upper voltage of the output waveform in V, from -4.999 V to 5 V (must be higher than low voltage by at least 1 mV).""", validator=strict_range, values=[-4.999, 5], ) voltage_low = Instrument.control( "SOUR{ch}:VOLT:LOW?", "SOUR{ch}:VOLT:LOW %f", """ A floating point property that controls the lower voltage of the output waveform in V, from -5 V to 4.999 V (must be lower than high voltage by at least 1 mV).""", validator=strict_range, values=[-5, 4.999], ) phase = Instrument.control( "SOUR{ch}:PHAS?", "SOUR{ch}:PHAS %f", """ A floating point property that controls the phase of the output waveform in degrees, from -360 degrees to 360 degrees. Not available for arbitrary waveforms or noise.""", validator=strict_range, values=[-360, 360], ) square_dutycycle = Instrument.control( "SOUR{ch}:FUNC:SQU:DCYC?", "SOUR{ch}: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.""", validator=strict_range, values=[0.01, 99.98], ) ramp_symmetry = Instrument.control( "SOUR{ch}:FUNC:RAMP:SYMM?", "SOUR{ch}:FUNC:RAMP:SYMM %f", """ A floating point property that controls the symmetry percentage for the ramp waveform, from 0.0% to 100.0%.""", validator=strict_range, values=[0, 100], ) pulse_period = Instrument.control( "SOUR{ch}:FUNC:PULS:PER?", "SOUR{ch}:FUNC:PULS:PER %e", """ A floating point property that controls the period of a pulse waveform function in seconds, ranging from 33 ns to 1 Ms. 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( "SOUR{ch}:FUNC:PULS:HOLD?", "SOUR{ch}: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( "SOUR{ch}:FUNC:PULS:WIDT?", "SOUR{ch}: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.""", validator=strict_range, values=[16e-9, 1e6], ) pulse_dutycycle = Instrument.control( "SOUR{ch}:FUNC:PULS:DCYC?", "SOUR{ch}:FUNC:PULS:DCYC %f", """ A floating point property that controls the duty cycle of a pulse waveform function in percent, from 0% to 100%.""", validator=strict_range, values=[0, 100], ) pulse_transition = Instrument.control( "SOUR{ch}:FUNC:PULS:TRAN?", "SOUR{ch}: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.""", validator=strict_range, values=[8.4e-9, 1e-6], ) output = Instrument.control( "OUTP{ch}?", "OUTP{ch} %d", """ A boolean property that turns on (True, 'on') or off (False, 'off') the output of the function generator.""", 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{ch}:LOAD?", "OUTP{ch}: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. """, ) burst_state = Instrument.control( "SOUR{ch}:BURS:STAT?", "SOUR{ch}:BURS:STAT %d", """ A boolean property that controls whether the burst mode is on (True) or off (False).""", validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) burst_mode = Instrument.control( "SOUR{ch}:BURS:MODE?", "SOUR{ch}:BURS:MODE %s", """ A string property that controls the burst mode. Valid values are: TRIG, GAT.""", validator=strict_discrete_set, values=["TRIG", "TRIGGERED", "GAT", "GATED"], ) burst_period = Instrument.control( "SOUR{ch}:BURS:INT:PER?", "SOUR{ch}: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.""", validator=strict_range, values=[1e-6, 8000], ) burst_ncycles = Instrument.control( "SOUR{ch}:BURS:NCYC?", "SOUR{ch}: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( "SOUR{ch}:FUNC:ARB?", "SOUR{ch}: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 memory (set by :meth:`data_arb`).""", ) arb_advance = Instrument.control( "SOUR{ch}:FUNC:ARB:ADV?", "SOUR{ch}: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( "SOUR{ch}:FUNC:ARB:FILT?", "SOUR{ch}: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"], ) arb_srate = Instrument.control( "SOUR{ch}:FUNC:ARB:SRAT?", "SOUR{ch}: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).""", validator=strict_range, values=[1e-6, 250e6], ) def data_volatile_clear(self): """ Clear all arbitrary signals from volatile memory for a given channel. 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 memory. """ self.write("SOUR{ch}: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 for a given channel. 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). :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 self.write(f"SOUR{{ch}}:DATA:ARB:DAC {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 self.write(f"SOUR{{ch}}:DATA:ARB {arb_name}, {data_string}") return elif data_format == "binary": # TODO: *Binary is not yet implemented* 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"' ) class Agilent33500(SCPIUnknownMixin, Instrument): """ Represents the Agilent 33500 Function/Arbitrary Waveform Generator family. Individual devices are represented by subclasses. User can specify a channel to control, if no channel specified, a default channel is picked based on the device e.g. For Agilent33500B the default channel is channel 1. See reference manual for your device .. code-block:: python generator = Agilent33500("GPIB::1") generator.shape = 'SIN' # Sets default channel output signal shape to sine generator.channels[1].shape = 'SIN' # Sets channel 1 output signal shape to sine generator.frequency = 1e3 # Sets default channel output frequency to 1 kHz generator.channels[1].frequency = 1e3 # Sets channel 1 output frequency to 1 kHz generator.channels[2].amplitude = 5 # Sets channel 2 output amplitude to 5 Vpp generator.channels[2].output = 'on' # Enables channel 2 output generator.channels[1].shape = 'ARB' # Set channel 1 shape to arbitrary generator.channels[1].arb_srate = 1e6 # Set channel 1 sample rate to 1MSa/s generator.channels[1].data_volatile_clear() # Clear channel 1 volatile internal memory generator.channels[1].data_arb( # Send data of arbitrary waveform to channel 1 'test', range(-10000, 10000, +20), # In this case a simple ramp data_format='DAC' # Data format is set to 'DAC' ) generator.channels[1].arb_file = 'test' # Select the transmitted waveform 'test' """ ch_1 = Instrument.ChannelCreator(Agilent33500Channel, 1) ch_2 = Instrument.ChannelCreator(Agilent33500Channel, 2) def __init__(self, adapter, name="Agilent 33500 Function/Arbitrary Waveform generator family", **kwargs): super().__init__( adapter, name, **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.""", validator=strict_range, values=[1e-6, 120e6], ) 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.""", 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.""", 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). """, 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.999 V to 5 V (must be higher than low voltage by at least 1 mV).""", validator=strict_range, values=[-4.999, 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.999 V (must be lower than high voltage by at least 1 mV).""", validator=strict_range, values=[-5, 4.999], ) phase = Instrument.control( "PHAS?", "PHAS %f", """ A floating point property that controls the phase of the output waveform in degrees, from -360 degrees to 360 degrees. Not available for arbitrary waveforms or noise.""", validator=strict_range, values=[-360, 360], ) 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.""", 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%.""", 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 1 Ms, within a set of restrictions depending on the period.""", 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%.""", 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.""", 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.""", 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. """, ) burst_state = Instrument.control( "BURS:STAT?", "BURS:STAT %d", """ A boolean property that controls whether the burst mode is on (True) or off (False).""", 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.""", 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.""", 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 memory (set by :meth:`data_arb`).""", ) 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.""", # 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.""", # 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).""", validator=strict_range, values=[1e-6, 250e6], ) def data_volatile_clear(self): """ Clear all arbitrary signals from 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 phase_sync(self): """ Synchronize the phase of all channels.""" self.write("PHAS:SYNC") 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). :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 self.write(f"DATA:ARB:DAC {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 self.write(f"DATA:ARB {arb_name}, {data_string}") return elif data_format == "binary": # TODO: *Binary is not yet implemented* 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.""", ) 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).""", 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.""", validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilent33521A.py0000644000175100001770000000454214623331163023746 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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().__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], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilent34410A.py0000644000175100001770000000434314623331163023743 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin class Agilent34410A(SCPIUnknownMixin, Instrument): """ Represent the HP/Agilent/Keysight 34410A and related multimeters. Implemented measurements: voltage_dc, voltage_ac, current_dc, current_ac, resistance, resistance_4w """ def __init__(self, adapter, name="HP/Agilent/Keysight 34410A Multimeter", **kwargs): super().__init__( adapter, name, **kwargs ) # only the most simple functions are implemented voltage_dc = Instrument.measurement("MEAS:VOLT:DC? DEF,DEF", "Get DC voltage, in Volts") voltage_ac = Instrument.measurement("MEAS:VOLT:AC? DEF,DEF", "Get AC voltage, in Volts") current_dc = Instrument.measurement("MEAS:CURR:DC? DEF,DEF", "Get DC current, in Amps") current_ac = Instrument.measurement("MEAS:CURR:AC? DEF,DEF", "Get AC current, in Amps") resistance = Instrument.measurement("MEAS:RES? DEF,DEF", "Get Resistance, in Ohms") resistance_4w = Instrument.measurement( "MEAS:FRES? DEF,DEF", "Get Four-wires (remote sensing) resistance, in Ohms") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilent34450A.py0000644000175100001770000006005414623331163023750 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Agilent34450A(SCPIUnknownMixin, 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(f'Value {value} is not a supported mode for this device.') ############### # 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", """ 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", """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`. """ # noqa: E501 ) ############# # 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`. """ # noqa: E501 ) 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, name="HP/Agilent/Keysight 34450A Multimeter", **kwargs): super().__init__( adapter, name, timeout=10000, **kwargs ) # Configuration changes can necessitate up to 8.8 secs (per datasheet) self.check_errors() 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 measurement.") 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"'] from Instrument.measurement or Instrument.control '"CURR +1.000000E-01,+1.500000E-06"' 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilent4156.py0000644000175100001770000010175114623331163023567 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 os import json import numpy as np import pandas as pd from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import (strict_discrete_set, truncated_discrete_set, strict_range) import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) ###### # MAIN ###### class Agilent4156(SCPIUnknownMixin, 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, adapter, name="Agilent 4155/4156 Semiconductor Parameter Analyzer", **kwargs): super().__init__( adapter, name, **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(f":PAGE:MEAS:SAMP:PER {period}") self.write(f":PAGE:MEAS:SAMP:POIN {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): """ Configure the channel setup and sweep using a JSON configuration file. (JSON is the `JavaScript Object Notation`_) .. _`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) 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): if len(trace_list) > 8: raise RuntimeError('Maximum of 8 variables allowed') else: for name in trace_list: self.write(f":PAGE:DISP:LIST \'{name}\'") elif isinstance(trace_list, str): self.write(f":PAGE:DISP:LIST \'{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): if len(trace_list) > 2: raise RuntimeError('Maximum of 2 variables allowed') else: for name in trace_list: self.write(f":PAGE:DISP:DVAR \'{name}\'") elif isinstance(trace_list, str): self.write(f":PAGE:DISP:DVAR \'{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): """ Get 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): """ Get 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(f":DATA? \'{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(SCPIUnknownMixin, Instrument): def __init__(self, adapter, channel, **kwargs): super().__init__( adapter, "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(f":PAGE:CHAN:{self.channel}:MODE?") 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(f":PAGE:CHAN:{self.channel}:MODE {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(f":PAGE:CHAN:{self.channel}:FUNC?") 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(f":PAGE:CHAN:{self.channel}:FUNC {value}") self.check_errors() @property def series_resistance(self): """ 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(f":PAGE:CHAN:{self.channel}:SRES?") 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(f":PAGE:CHAN:{self.channel}:SRES {value}") self.check_errors() @property def disable(self): """ Deletes the settings of SMU. .. code-block:: python instr.smu1.disable() """ self.write(f":PAGE:CHAN:{self.channel}:DIS") self.check_errors() @property def constant_value(self): """ Set 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(f":PAGE:MEAS:CONS:{self.channel}?") else: value = self.ask(f":PAGE:MEAS:SAMP:CONS:{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(f":PAGE:MEAS:CONS:{self.channel} {value}") else: self.write(":PAGE:MEAS:SAMP:CONS:{} {}".format( self.channel, value)) self.check_errors() @property def compliance(self): """ 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(f":PAGE:MEAS:CONS:{self.channel}:COMP?") else: value = self.ask( f":PAGE:MEAS:SAMP:CONS:{self.channel}:COMP?") 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:{}:COMP {}".format( self.channel, value)) else: self.write(":PAGE:MEAS:SAMP:CONS:{}:COMP {}".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(f"PAGE:CHAN:{self.channel}:VNAME?") return value @voltage_name.setter def voltage_name(self, vname): value = check_current_voltage_name(vname) self.write(f":PAGE:CHAN:{self.channel}:VNAME \'{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(f"PAGE:CHAN:{self.channel}:INAME?") return value @current_name.setter def current_name(self, iname): value = check_current_voltage_name(iname) self.write(f":PAGE:CHAN:{self.channel}:INAME \'{value}\'") def __validate_cons(self): """Validates the instrument settings for operation in constant mode. """ if not ((self.channel_mode != 'COMM') and ( self.channel_function == 'CONS')): 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. """ if not ((self.channel_mode != 'COMM') and ( self.channel_function == 'CONS')): 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(SCPIUnknownMixin, Instrument): def __init__(self, adapter, channel, **kwargs): super().__init__( adapter, "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(f"PAGE:CHAN:{self.channel}:VNAME?") return value @voltage_name.setter def voltage_name(self, vname): value = check_current_voltage_name(vname) self.write(f":PAGE:CHAN:{self.channel}:VNAME \'{value}\'") @property def disable(self): """ Disables the settings of VMU. .. code-block:: python instr.vmu1.disable() """ self.write(f":PAGE:CHAN:{self.channel}:DIS") 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(f":PAGE:CHAN:{self.channel}:MODE?") 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(f":PAGE:CHAN:{self.channel}:MODE {value}") self.check_errors() class VSU(SCPIUnknownMixin, Instrument): def __init__(self, adapter, channel, **kwargs): super().__init__( adapter, "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(f"PAGE:CHAN:{self.channel}:VNAME?") return value @voltage_name.setter def voltage_name(self, vname): value = check_current_voltage_name(vname) self.write(f":PAGE:CHAN:{self.channel}:VNAME \'{value}\'") @property def disable(self): """ Deletes the settings of VSU. .. code-block:: python instr.vsu1.disable() """ self.write(f":PAGE:CHAN:{self.channel}:DIS") self.check_errors() @property def channel_mode(self): """ Get channel mode of VSU.""" value = self.ask(f":PAGE:CHAN:{self.channel}:MODE?") self.check_errors() return value @property def constant_value(self): """ 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(f":PAGE:MEAS:CONS:{self.channel}?") else: value = self.ask(f":PAGE:MEAS:SAMP:CONS:{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(f":PAGE:MEAS:CONS:{self.channel} {value}") else: self.write(":PAGE:MEAS:SAMP:CONS:{} {}".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(f":PAGE:CHAN:{self.channel}:FUNC?") 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(f":PAGE:CHAN:{self.channel}:FUNC {value}") self.check_errors() ################# # SWEEP VARIABLES ################# class VARX(SCPIUnknownMixin, Instrument): """ Base class to define sweep variable settings """ def __init__(self, adapter, var_name, **kwargs): super().__init__( adapter, "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(f":PAGE:CHAN:{ch}:FUNC?") if ch_func == self.var: ch_mode = self.ask(f":PAGE:CHAN:{ch}:MODE?") return ch_mode @property def start(self): """ Sets the sweep START value. .. code-block:: python instr.var1.start = 0 """ value = self.ask(f":PAGE:MEAS:{self.var}:STAR?") 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(f":PAGE:MEAS:{self.var}:STAR {set_value}") self.check_errors() @property def stop(self): """ Sets the sweep STOP value. .. code-block:: python instr.var1.stop = 3 """ value = self.ask(f":PAGE:MEAS:{self.var}:STOP?") 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(f":PAGE:MEAS:{self.var}:STOP {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(f":PAGE:MEAS:{self.var}:STEP?") 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(f":PAGE:MEAS:{self.var}:STEP {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(f":PAGE:MEAS:{self.var}:COMP {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, adapter, **kwargs): super().__init__( adapter, "VAR1", **kwargs ) spacing = Instrument.control( ":PAGE:MEAS:VAR1:SPAC?", ":PAGE:MEAS:VAR1:SPAC %s", """ 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, adapter, **kwargs): super().__init__( adapter, "VAR2", **kwargs ) points = Instrument.control( ":PAGE:MEAS:VAR2:POINTS?", ":PAGE:MEAS:VAR2:POINTS %g", """ 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(SCPIUnknownMixin, Instrument): """ Class to handle all the definitions needed for VARD. VARD is always defined in relation to VAR1. """ def __init__(self, adapter, **kwargs): super().__init__( adapter, "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(f":PAGE:CHAN:{ch}:FUNC?") if ch_func == "VARD": ch_mode = self.ask(f":PAGE:CHAN:{ch}:MODE?") return ch_mode @property def offset(self): """ 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(f":PAGE:MEAS:VARD:OFFSET {value}") self.check_errors() ratio = Instrument.control( ":PAGE:MEAS:VARD:RATIO?", ":PAGE:MEAS:VARD:RATIO %g", """ 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): """ 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(f":PAGE:MEAS:VARD:COMP {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(f"Renaming {name} to {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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilent4284A.py0000644000175100001770000003130614623331163023670 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) IMPEDANCE_MODES = ( "CPD", "CPQ", "CPG", "CPRP", "CSD", "CSQ", "CSRS", "LPQ", "LPD", "LPG", "LPRP", "LSD", "LSQ", "LSRS", "RX", "ZTD", "ZTR", "GB", "YTD", "YTR" ) class Agilent4284A(SCPIMixin, Instrument): """Represents the Agilent 4284A precision LCR meter. .. code-block:: python agilent = Agilent4284A("GPIB::1::INSTR") agilent.reset() # Return instrument settings to default values agilent.frequency = 10e3 # Set frequency to 10 kHz agilent.voltage = 0.02 # Set AC voltage to 20 mV agilent.mode = 'ZTR' # Set impedance mode to measure impedance magnitude [Ohm] and phase [rad] agilent.sweep_measurement( 'frequency', [1e4, 1e3, 100] # Perform frequency sweep measurement ) # at 10 kHz, 1 kHz, and 100 Hz agilent.enable_high_power() # Enable upper current, voltage, and bias limits, if properly configured. """ def __init__(self, adapter, name="Agilent 4284A LCR meter", **kwargs): kwargs.setdefault("read_termination", '\n') kwargs.setdefault("write_termination", '\n') kwargs.setdefault("timeout", 10000) super().__init__(adapter, name, **kwargs) self._set_ranges(0) frequency = Instrument.control( "FREQ?", "FREQ %g", """Control AC frequency in Hertz, from 20 Hz to 1 MHz.""", validator=strict_range, values=(20, 1e6), ) ac_current = Instrument.control( "CURR:LEV?", "CURR:LEV %g", """Control AC current level in Amps. Valid range is 50 uA to 20 mA for default, 50 uA to 200 mA in high-power mode.""", validator=strict_range, values=(50e-6, 0.02), dynamic=True ) ac_voltage = Instrument.control( "VOLT:LEV?", "VOLT:LEV %g", """Control AC voltage level in Volts. Range is 5 mV to 2 V for default, 5 mV to 20 V in high-power mode.""", validator=strict_range, values=(0.005, 2), dynamic=True ) bias_enabled = Instrument.control( "BIAS:STAT?", "BIAS:STAT %d", """Control whether DC bias is enabled.""", validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True ) bias_voltage = Instrument.control( "BIAS:VOLT?", "BIAS:VOLT %g", """Control the DC bias voltage in Volts. Maximum is 2 V by default, 40 V in high-power mode.""", validator=strict_range, values=(0, 2), dynamic=True ) bias_current = Instrument.control( "BIAS:CURR?", "BIAS:CURR %g", """Control the DC bias current in Amps. Requires Option 001 (power amplifier / DC bias) to be installed. Maximum is 100 mA.""", validator=strict_range, values=(0, 0), dynamic=True ) impedance_mode = Instrument.control( "FUNC:IMP?", "FUNC:IMP %s", """Control impedance measurement function. * 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] * LPQ: Parallel inductance [H] and quality factor [number] * LPD: Parallel inductance [H] and dissipation 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: Resistance [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=IMPEDANCE_MODES ) impedance_range = Instrument.control( "FUNC:IMP:RANG?", "FUNC:IMP:RANG %g", """Control the impedance measurement range. The 4284A will select an appropriate measurement range for the setting value.""" ) auto_range_enabled = Instrument.control( "FUNC:IMP:RANG:AUTO?", "FUNC:IMP:RANG:AUTO %d", """Control whether the impedance auto range is enabled.""", validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True ) trigger_source = Instrument.control( "TRIG:SOUR?", "TRIG:SOUR %s", """Control trigger mode. Valid options are `INT`, `EXT`, `BUS`, or `HOLD`.""", validator=strict_discrete_set, values=('INT', 'EXT', 'BUS', 'HOLD'), cast=str ) trigger_delay = Instrument.control( "TRIG:DEL?", "TRIG:DEL %g", """Control trigger delay in seconds. Valid range is 0 to 60, with 1 ms resolution.""", validator=strict_range, values=(0, 60) ) trigger_continuous_enabled = Instrument.control( "TRIG:CONT?", "TRIG:CONT %d", """Control whether trigger state automatically returns to WAIT FOR TRIGGER after measurement.""", validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True ) def _set_ranges(self, high_power_mode): """Set dynamic property values and make copies for sweep_measurement to reference.""" if high_power_mode: self.ac_current_values = (50e-6, 0.2) self.ac_voltage_values = (0.005, 20) self.bias_voltage_values = (0, 40) self.bias_current_values = (0, 0.1) self._ac_current_values = (50e-6, 0.2) self._ac_voltage_values = (0.005, 20) self._bias_voltage_values = (0, 40) self._bias_current_values = (0, 0.1) else: self.ac_current_values = (50e-6, 0.02) self.ac_voltage_values = (0.005, 2) self.bias_voltage_values = (0, 2) self.bias_current_values = (0, 0) self._ac_current_values = (50e-6, 0.02) self._ac_voltage_values = (0.005, 2) self._bias_voltage_values = (0, 2) self._bias_current_values = (0, 0) @property def high_power_enabled(self): """Control whether high power mode is enabled. Enabling requires option 001 (power amplifier / DC bias) to be installed. """ mode = self.values("OUTP:HPOW?", cast=int) return bool(mode) @high_power_enabled.setter def high_power_enabled(self, val): if not val: self._set_ranges(0) self.write("OUTP:HPOW 0") elif val and self.options[0] == '0': raise AttributeError("Agilent 4284A power amplifier is not installed.") else: self._set_ranges(1) self.write("OUTP:HPOW 1") def sweep_measurement(self, sweep_mode, sweep_values): """Run list sweep measurement using sequential trigger. :param str sweep_mode: parameter to sweep across. Must be one of `frequency`, `voltage`, `current`, `bias_voltage`, or `bias_current`. :param sweep_values: list of parameter values to sweep across. :returns: values as configured with :attr:`~.Agilent4284A.impedance_mode` and list of sweep parameters in format ([val A], [val B], [sweep_values]) """ param_dict = { "frequency": ("FREQ", (20, 1e6)), "voltage": ("VOLT", self._ac_voltage_values), "current": ("CURR", self._ac_current_values), "bias_voltage": ("BIAS:VOLT", self._bias_voltage_values), "bias_current": ("BIAS:CURR", self._bias_current_values) } if sweep_mode not in param_dict: raise KeyError( f"Sweep mode but be one of {list(param_dict.keys())}, not '{sweep_mode}'." ) low_limit = param_dict[sweep_mode][1][0] high_limit = param_dict[sweep_mode][1][1] if (min(sweep_values) < low_limit or max(sweep_values) > high_limit): log.warning( "%s values are outside valid Agilent 4284A range of %g and %g " "and will be truncated.", sweep_mode, low_limit, high_limit ) sweep_truncated = [] for val in sweep_values: if low_limit <= val <= high_limit: sweep_truncated.append(val) sweep_values = sweep_truncated loops = (len(sweep_values) - 1) // 10 # 4284A sweeps 10 points at a time param_div = [] for i in range(loops): param_div.append(sweep_values[10*i:10*(i+1)]) param_div.append(sweep_values[loops*10:]) self.clear() self.write("TRIG:SOUR BUS;:DISP:PAGE LIST;:FORM ASC;:LIST:MODE SEQ;:INIT:CONT ON") a_data = [] b_data = [] sweep_return = [] for i in range(loops + 1): param_str = ",".join(['%g' % p for p in param_div[i]]) self.write(f"LIST:{param_dict[sweep_mode][0]} {param_str};:TRIG:IMM") status_event_register = int(self.ask("STAT:OPER?")) while (status_event_register & 8) != 8: # Sweep bit no. 3 sleep(0.1) status_event_register = int(self.ask("STAT:OPER?")) measured = self.values("FETCH?") # gets 4-ples of numbers, first two are data A and B a_data += [measured[_] for _ in range(0, 4 * len(param_div[i]), 4)] b_data += [measured[_] for _ in range(1, 4 * len(param_div[i]), 4)] sweep_return += self.values(f"LIST:{param_dict[sweep_mode][0]}?") # Return to manual trigger and reset display self.write(":TRIG:SOUR HOLD;:DISP:PAGE MEAS") self.check_errors() return a_data, b_data, sweep_return def trigger(self): """Execute a bus trigger, regardless of trigger state. Can be used when :attr:`trigger_source` is set to `BUS`. Returns result of triggered measurement. """ return self.values("*TRG") def trigger_immediate(self): """Execute a bus trigger, regardless of trigger state. Can be used when :attr:`trigger_source` is set to `BUS`. Measurement result must be retrieved with `FETCH?` command. """ self.write("TRIG:IMM") def trigger_initiate(self): """Change the trigger state from IDLE to WAIT FOR TRIGGER for one trigger sequence.""" self.write("TRIG:INIT:IMM") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilent4294A.py0000644000175100001770000001340114623331163023665 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set import pandas as pd import numpy as np import os # Set of valid arguments for the MEAS? command MEASUREMENT_TYPES = [ "IMPH", "IRIM", "LSR", "LSQ", "CSR", "CSQ", "CSD", "AMPH", "ARIM", "LPG", "LPQ", "CPG", "CPQ", "CPD", "COMP", "IMLS", "IMCS", "IMLP", "IMCP", "IMRS", "IMQ", "IMD", "LPR", "CPR", ] class Agilent4294A(SCPIMixin, Instrument): """ Represents the Agilent 4294A Precision Impedance Analyzer """ def __init__(self, adapter, name="Agilent 4294A Precision Impedance Analyzer", read_termination="\n", write_termination="\n", timeout=5000, **kwargs): super().__init__( adapter, name, read_termination=read_termination, write_termination=write_termination, timeout=timeout, **kwargs ) start_frequency = Instrument.control( "STAR?", "STAR %d HZ", "Control the start frequency in Hz", validator=strict_range, values=[40, 140E6] ) stop_frequency = Instrument.control( "STOP?", "STOP %d HZ", "Control the stop frequency in Hz", validator=strict_range, values=[40, 140E6] ) num_points = Instrument.control( "POIN?", "POIN %d", "Control the number of points measured at each sweep", validator=strict_discrete_set, values=range(2, 802), cast=int, ) measurement_type = Instrument.control( "MEAS?", "MEAS %d", "Control the measurement type. See MEASUREMENT_TYPES", validator=strict_discrete_set, values=MEASUREMENT_TYPES, ) active_trace = Instrument.control( "TRAC?", "TRAC %s", "Control the active trace", validator=strict_discrete_set, values=["A", "B"] ) title = Instrument.control( "TITL?", 'TITL "%s"', "Control the title of the active trace" ) def save_graphics(self, path=""): """ Save graphics on the screen to a file on the local computer. Adapted from: https://www.keysight.com/se/en/lib/software-detail/programming-examples/4294a-data-transfer-program-excel-vba-1645196.html """ self.write("STOD MEMO") # store to internal memory self.write("PRIC VARI") # save a color image root, ext = os.path.splitext(path) if ext != ".tiff": ext = ".tiff" if not root: root = "graphics" path = root + ext REMOTE_FILE = "agt4294a.tiff" # Filename of the in-memory file on the device self.write(f'SAVDTIF "{REMOTE_FILE}"') vErr = self.ask("OUTPERRO?").split(",") if not int(vErr[0]) == 0: self.write(f'PURG "{REMOTE_FILE}"') self.write(f'SAVDTIF "{REMOTE_FILE}"') vErr = self.ask("OUTPERRO?").split(",") self.write(f'ROPEN "{REMOTE_FILE}"') lngFileSize = int(self.ask(f'FSIZE? "{REMOTE_FILE}"')) MAX_BUFF_SIZE = 16384 iBufCnt = lngFileSize // MAX_BUFF_SIZE if lngFileSize % MAX_BUFF_SIZE > 0: iBufCnt += 1 with open(path, 'wb') as file: for _ in range(iBufCnt): data = self.adapter.connection.query_binary_values("READ?", datatype='B', container=bytes) file.write(data) self.write(f'PURG "{REMOTE_FILE}"') return path def get_data(self, path=None): """ Get the measurement data from the instrument after completion. :param path: Path for optional data export to CSV. :returns: Pandas Dataframe """ prev_active_trace = self.active_trace num_points = self.num_points freqs = np.array(self.ask("OUTPSWPRM?").split(","), dtype=float) self.active_trace = "A" adata = np.array(self.ask("OUTPDTRC?").split(","), dtype=float).reshape(num_points, 2) self.active_trace = "B" bdata = np.array(self.ask("OUTPDTRC?").split(","), dtype=float).reshape(num_points, 2) # restore the previous state self.active_trace = prev_active_trace df = pd.DataFrame( np.hstack((freqs.reshape(-1, 1), adata, bdata)), columns=["Frequency", "A Real", "A Imag", "B Real", "B Imag"] ) if path is not None: _, ext = os.path.splitext(path) if ext != ".csv": path = path + ".csv" df.to_csv(path, index=False) return df ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilent8257D.py0000644000175100001770000003047614623331163023706 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set class Agilent8257D(SCPIUnknownMixin, 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 percent, 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 internal 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, name="Agilent 8257D RF Signal Generator", **kwargs): super().__init__( adapter, name, **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 percentage :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() super().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilent8722ES.py0000644000175100001770000002345014623331163024021 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import discreteTruncate from pymeasure.errors import RangeException from pyvisa import VisaIOError import numpy as np import re from io import BytesIO import warnings class Agilent8722ES(SCPIUnknownMixin, 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)) # need float() to convert scientific notation in 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, adapter, name="Agilent 8722ES Vector Network Analyzer", **kwargs): super().__init__( adapter, name, **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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilentB1500.py0000644000175100001770000023057314623331163023664 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, strict_range, strict_discrete_range) from pymeasure.instruments import Instrument, SCPIUnknownMixin log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) ###################################### # Agilent B1500 Mainframe ###################################### class AgilentB1500(SCPIUnknownMixin, Instrument): """ Represents the Agilent B1500 Semiconductor Parameter Analyzer and provides a high-level interface for taking different kinds of measurements. """ def __init__(self, adapter, name="Agilent B1500 Semiconductor Parameter Analyzer", **kwargs): super().__init__( adapter, name, **kwargs ) self._smu_names = {} self._smu_references = {} @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( f'Module {module[0]} is not implemented yet!') 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 Execution 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 OSError( f"Agilent B1500 Error {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: 'Another 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, smu_names, output_format_str): """ 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 {} 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 = f' {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(f'Agilent B1500{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, smu_names={}, output_format_string="FMT1"): super().__init__(smu_names, output_format_string) 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 (based on FMT1) """ def __init__(self, smu_names={}): super().__init__(smu_names, "FMT11") class _data_formatting_FMT21(_data_formatting_generic): """ Data formatting for FMT21 format """ def __init__(self, smu_names={}): super().__init__(smu_names, "FMT21") 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 = { "FMT1": self._data_formatting_FMT1, "FMT11": self._data_formatting_FMT11, "FMT21": self._data_formatting_FMT21 } try: format_class = classes[output_format_str] except KeyError: log.error(( "Data Format {} is not implemented " "so far. Please set appropriate Data Format." ).format(output_format_str)) return else: return format_class(smu_names=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``) Currently implemented are format 1, 11, and 21. :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 """ # restrict to implemented formats output_format = strict_discrete_set( output_format, [1, 11, 21]) # possible: [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 instrument. """ 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. Halves 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: ' + f'{error1} {error2}') if interval >= 0.0001 + 0.00002 * (n_channels - 1): interval = strict_discrete_range(interval, (0, 0.00199), 0.00001) else: raise ValueError( f'Sampling interval {interval} is too short.') 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.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" % (setting, self.channel)) 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 output: 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( ("{} in different state. " "Changing to {} 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) :] # noqa: E203 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilentE4408B.py0000644000175100001770000001034214623331163023771 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range from io import StringIO import numpy as np import pandas as pd class AgilentE4408B(SCPIUnknownMixin, Instrument): """ Represents the AgilentE4408B Spectrum Analyzer and provides a high-level interface for taking scans of high-frequency spectrums """ def __init__(self, adapter, name="Agilent E4408B Spectrum Analyzer", **kwargs): super().__init__( adapter, name, **kwargs ) 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. """ ) @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) }) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/agilent/agilentE4980.py0000644000175100001770000001560414623331163023702 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range from pyvisa.errors import VisaIOError class AgilentE4980(SCPIUnknownMixin, 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: Resistance [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, name="Agilent E4980A/AL LCR meter", **kwargs): super().__init__( adapter, name, **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(f":APER {time}, {averages}") else: raise Exception("Time must be a string: SHORT, MED, LONG") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3936055 pymeasure-0.14.0/pymeasure/instruments/aimtti/0000755000175100001770000000000014623331176021115 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/aimtti/__init__.py0000644000175100001770000000232014623331163023217 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .aimttiPL import PL068P, PL155P, PL303P, PL601P, PL303QMDP, PL303QMTP ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/aimtti/aimttiPL.py0000644000175100001770000001466114623331163023216 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range class PLChannel(Channel): """A channel of AimTTI PL series power supplies. Channels of the power supply. The channels are number from right-to-left, starting at 1. """ def __init__(self, parent, id, voltage_range: list = None, current_range: list = None): super().__init__(parent, id) self.voltage_setpoint_values = voltage_range self.current_limit_values = current_range voltage_setpoint = Channel.control( "V{ch}?", "V{ch}V %g", """ Control the output voltage of this channel. With verify: the operation is completed when the parameter being adjusted reaches the required value to within ±5% or ±10 counts.""", validator=strict_range, values=[0, 6], dynamic=True, get_process=lambda x: float(x[3:]), ) current_limit = Channel.control( "I{ch}?", "I{ch} %g", """ Control the current limit in Amps.""", validator=strict_range, values=[0, 1.5], dynamic=True, get_process=lambda x: float(x[3:]), ) voltage = Channel.measurement( "V{ch}O?", """ Measure the output readback voltage for this output channel in Volts.""", get_process=lambda x: float(x[:-1]), ) current = Channel.measurement( "I{ch}O?", """ Measure the output readback current for this output channel in Amps.""", get_process=lambda x: float(x[:-1]), ) current_range = Channel.control( "IRANGE{ch}?", "IRANGE{ch} %g", """ Control the current range of the channel. Low (500/800mA) range, or High range. Output must be switched off before changing range.""", validator=strict_discrete_set, values={"LOW": 1, "HIGH": 2}, map_values=True, ) output_enabled = Channel.control( "OP{ch}?", "OP{ch} %i", """ Control whether the source is enabled, takes values True or False.""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) class PLBase(SCPIUnknownMixin, Instrument): """Control AimTTI PL series power supplies. Model number ending with -P or P(G) support this remote interface. Documentation: https://resources.aimtti.com/manuals/New_PL+PL-P_Series_Instruction_Manual-Iss18.pdf PL-series devices: https://www.aimtti.com/product-category/dc-power-supplies/aim-plseries The default value for the timeout argument is set to 5000ms. .. code-block:: python psu = PL303QMDP("ASRL7::INSTR") psu.reset() psu.ch_2.voltage = 1.2 psu.ch_2.output_enabled = True ... psu.ch_2.output_enabled = False psu.local() """ def __init__(self, adapter, name="AimTTI PL", **kwargs): kwargs.setdefault("timeout", 5000) super().__init__(adapter, name, **kwargs) all_outputs_enabled = Instrument.setting( "OPALL %d", """ Control whether all sources are enabled simultaneously, takes values True or False.""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) def local(self): """Go to local. Make sure all output are disabled first.""" self.write("LOCAL") class PL068P(PLBase): ch_1: PLChannel = Instrument.ChannelCreator( PLChannel, "1", voltage_range=[0, 6], current_range=[0, 8] ) def __init__(self, adapter, name="AimTTI PL068-P", **kwargs): super().__init__(adapter, name, **kwargs) class PL155P(PLBase): ch_1: PLChannel = Instrument.ChannelCreator( PLChannel, "1", voltage_range=[0, 15], current_range=[0, 5] ) def __init__(self, adapter, name="AimTTI PL155-P", **kwargs): super().__init__(adapter, name, **kwargs) class PL303P(PLBase): ch_1: PLChannel = Instrument.ChannelCreator( PLChannel, "1", voltage_range=[0, 30], current_range=[0, 3] ) def __init__(self, adapter, name="AimTTI PL303-P", **kwargs): super().__init__(adapter, name, **kwargs) class PL601P(PLBase): ch_1: PLChannel = Instrument.ChannelCreator( PLChannel, "1", voltage_range=[0, 60], current_range=[0, 1.5] ) def __init__(self, adapter, name="AimTTI PL601-P", **kwargs): super().__init__(adapter, name, **kwargs) class PL303QMDP(PLBase): ch_1: PLChannel = Instrument.ChannelCreator( PLChannel, "1", voltage_range=[0, 30], current_range=[0, 3] ) ch_2: PLChannel = Instrument.ChannelCreator( PLChannel, "2", voltage_range=[0, 30], current_range=[0, 3] ) def __init__(self, adapter, name="AimTTI PL303QMD-P", **kwargs): super().__init__(adapter, name, **kwargs) class PL303QMTP(PLBase): ch_1: PLChannel = Instrument.ChannelCreator( PLChannel, "1", voltage_range=[0, 30], current_range=[0, 3] ) ch_2: PLChannel = Instrument.ChannelCreator( PLChannel, "2", voltage_range=[0, 30], current_range=[0, 3] ) ch_3: PLChannel = Instrument.ChannelCreator( PLChannel, "3", voltage_range=[0, 30], current_range=[0, 3] ) def __init__(self, adapter, name="AimTTI PL303QMT-P", **kwargs): super().__init__(adapter, name, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3936055 pymeasure-0.14.0/pymeasure/instruments/aja/0000755000175100001770000000000014623331176020361 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/aja/__init__.py0000644000175100001770000000223414623331163022467 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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 .dcxs import DCXS ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/aja/dcxs.py0000644000175100001770000001626514623331163021702 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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 DCXS(Instrument): """ AJA DCXS-750 or 1500 DC magnetron sputtering power supply with multiple outputs Connection to the device is made through an RS232 serial connection. The communication settings are fixed in the device at 38400, one stopbit, no parity. The device's communication protocol uses single character commands and fixed length replies, both without any terminator. :param adapter: pyvisa resource name of the instrument or adapter instance :param string name: The name of the instrument. :param kwargs: Any valid key-word argument for Instrument """ def __init__(self, adapter, name="AJA DCXS sputtering power supply", **kwargs): super().__init__( adapter, name, includeSCPI=False, write_termination="", read_termination="", asrl={"baud_rate": 38400}, **kwargs ) # here we want to flush the read buffer since the device upon power up sends some '>' # characters. self.adapter.flush_read_buffer() def ask(self, command, query_delay=None, **kwargs): """Write a command to the instrument and return the read response. :param command: Command string to be sent to the instrument. :param query_delay: Delay between writing and reading in seconds. :param \\**kwargs: Keyword arguments passed to the read method. :returns: String returned by the device without read_termination. """ self.write(command) self.wait_for(query_delay) return self.read(**kwargs) def read(self, reply_length=-1, **kwargs): return self.read_bytes(reply_length, **kwargs).decode() id = Instrument.measurement( "?", """Get the power supply type identifier.""", cast=str, values_kwargs={'reply_length': 9}, ) software_version = Instrument.measurement( "z", """Get the software revision of the power supply firmware.""", cast=str, values_kwargs={'reply_length': 5}, ) power = Instrument.measurement( "d", """Measure the actual output power in W.""", cast=int, values_kwargs={'reply_length': 4}, ) voltage = Instrument.measurement( "e", """Measure the output voltage in V.""", cast=int, values_kwargs={'reply_length': 4}, ) current = Instrument.measurement( "f", """Measure the output current in mA.""", cast=int, values_kwargs={'reply_length': 4}, ) remaining_deposition_time_min = Instrument.measurement( "k", """Get the minutes part of remaining deposition time.""", cast=int, values_kwargs={'reply_length': 3}, ) remaining_deposition_time_sec = Instrument.measurement( "l", """Get the seconds part of remaining deposition time.""", cast=int, values_kwargs={'reply_length': 2}, ) fault_code = Instrument.measurement( "o", """Get the error code from the power supply.""", values_kwargs={'reply_length': 1}, ) shutter_state = Instrument.measurement( "p", """Get the status of the gun shutters. 0 for closed and 1 for open shutters.""", values_kwargs={'reply_length': 1}, cast=lambda x: int.from_bytes(x.encode(), "big"), get_process=lambda x: [x & 1, x & 2, x & 4, x & 8, x & 16], ) enabled = Instrument.control( "a", "%s", """Control the on/off state of the power supply""", values_kwargs={'reply_length': 1}, validator=strict_discrete_set, map_values=True, cast=int, get_process=lambda c: "A" if c == 1 else "B", values={True: "A", False: "B"}, ) setpoint = Instrument.control( "b", "C%04d", """Control the setpoint value. Units are determined by regulation mode (power -> W, voltage -> V, current -> mA).""", values_kwargs={'reply_length': 4}, validator=strict_range, map_values=True, values=range(0, 1001), ) regulation_mode = Instrument.control( "c", "D%d", """Control the regulation mode of the power supply.""", values_kwargs={'reply_length': 1}, validator=strict_discrete_set, map_values=True, values={"power": 0, "voltage": 1, "current": 2, }, ) ramp_time = Instrument.control( "g", "E%02d", """Control the ramp time in seconds. Can be set only when 'enabled' is False.""", values_kwargs={'reply_length': 2}, cast=int, validator=strict_range, values=range(100), ) shutter_delay = Instrument.control( "h", "F%02d", """Control the shutter delay in seconds. Can be set only when 'enabled' is False.""", values_kwargs={'reply_length': 2}, cast=int, validator=strict_range, values=range(100), ) deposition_time_min = Instrument.control( "i", "G%03d", """Control the minutes part of deposition time. Can be set only when 'enabled' is False.""", values_kwargs={'reply_length': 3}, cast=int, validator=strict_range, values=range(1000), ) deposition_time_sec = Instrument.control( "j", "H%02d", """Control the seconds part of deposition time. Can be set only when 'enabled' is False.""", values_kwargs={'reply_length': 2}, cast=int, validator=strict_range, values=range(60), ) material = Instrument.control( "n", "I%08s", """Control the material name of the sputter target.""", cast=str, values_kwargs={'reply_length': 8}, validator=lambda value, maxlength: value[:maxlength], values=8, ) active_gun = Instrument.control( "y", "Z%d", """Control the active gun number.""", cast=int, values_kwargs={'reply_length': 1}, validator=strict_range, values=range(1, 6), ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3936055 pymeasure-0.14.0/pymeasure/instruments/ametek/0000755000175100001770000000000014623331176021074 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/ametek/__init__.py0000644000175100001770000000225014623331163023200 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/ametek/ametek7270.py0000644000175100001770000003072414623331163023236 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import modular_range, truncated_discrete_set, truncated_range import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def check_read_not_empty(value): """Called by some properties to check if the reply is not an empty string that would mean the properties is currently invalid (probably because the reference mode is on single or dual)""" if value == '': raise ValueError('Invalid response from measurement call, ' 'probably because the reference mode is set on single or dual') else: return value class Ametek7270(SCPIUnknownMixin, Instrument): """This is the class for the Ametek DSP 7270 lockin amplifier In this instrument, some measurements are defined only for specific modes, called Reference modes, see :meth:`set_reference_mode` and will raise errors if called incorrectly """ 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 ] SENSITIVITIES_IMODE = {0: SENSITIVITIES, 1: [sen * 1e-6 for sen in SENSITIVITIES], 2: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2e-15, 5e-15, 10e-15, 20e-15, 50e-15, 100e-15, 200e-15, 500e-15, 1e-12, 2e-12]} 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, check_set_errors=True, dynamic=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, check_set_errors=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, check_set_errors=True, ) x = Instrument.measurement("X.", """ Reads the X value in Volts """, get_process=check_read_not_empty, ) y = Instrument.measurement("Y.", """ Reads the Y value in Volts """, get_process=check_read_not_empty, ) x1 = Instrument.measurement("X1.", """ Reads the first harmonic X value in Volts """, get_process=check_read_not_empty, ) y1 = Instrument.measurement("Y1.", """ Reads the first harmonic Y value in Volts """, get_process=check_read_not_empty, ) x2 = Instrument.measurement("X2.", """ Reads the second harmonic X value in Volts """, get_process=check_read_not_empty, ) y2 = Instrument.measurement("Y2.", """ Reads the second harmonic Y value in Volts """, get_process=check_read_not_empty, ) xy = Instrument.measurement("XY.", """ Reads both the X and Y values in Volts """, get_process=check_read_not_empty, ) mag = Instrument.measurement("MAG.", """ Reads the magnitude in Volts """, get_process=check_read_not_empty, ) theta = Instrument.measurement("PHA.", """ Reads the signal phase in degrees """, get_process=check_read_not_empty, ) 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)), check_set_errors=True, ) 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], check_set_errors=True, ) 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], check_set_errors=True, ) 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], check_set_errors=True, ) 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], check_set_errors=True, ) 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], check_set_errors=True, ) 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], check_set_errors=True, ) 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], check_set_errors=True, ) adc1 = Instrument.measurement("ADC. 1", """ Reads the input value of ADC1 in Volts """, get_process=check_read_not_empty, ) adc2 = Instrument.measurement("ADC. 2", """ Reads the input value of ADC2 in Volts """, get_process=check_read_not_empty, ) adc3 = Instrument.measurement("ADC. 3", """ Reads the input value of ADC3 in Volts """, get_process=check_read_not_empty, ) adc4 = Instrument.measurement("ADC. 4", """ Reads the input value of ADC4 in Volts """, get_process=check_read_not_empty, ) def __init__(self, adapter, name="Ametek DSP 7270", read_termination='\x00', write_termination='\x00', **kwargs): super().__init__( adapter, name, read_termination=read_termination, write_termination=write_termination, **kwargs) def check_set_errors(self): """mandatory to be used for property setter The Ametek protocol expect the default null character to be read to check the property has been correctly set. With default termination character set as Null character, this turns out as an empty string to be read. """ if self.read() == '': return [] else: return ['Incorrect return from previously set property'] def ask(self, command, query_delay=None): """Send a command and read the response, stripping white spaces. Usually the properties use the :meth:`~pymeasure.instruments.common_base.CommonBase.values` method that adds a strip call, however several methods use directly the result from ask to be cast into some other types. It should therefore also add the strip here, as all responses end with a newline character. """ return super().ask(command, query_delay).strip() def set_reference_mode(self, mode: int = 0): """Set the instrument in Single, Dual or harmonic mode. :param mode: the integer specifying the mode: 0 for Single, 1 for Dual harmonic, and 2 for Dual reference. """ if mode not in [0, 1, 2]: raise ValueError('Invalid reference mode') self.ask(f'REFMODE {mode}') def set_voltage_mode(self): """ Sets instrument to voltage control mode """ self.ask("IMODE 0") self.sensitivity_values = self.SENSITIVITIES_IMODE[0] def set_differential_mode(self, lineFiltering=True): """ Sets instrument to differential mode -- assuming it is in voltage mode """ self.ask("VMODE 3") self.ask("LF %d 0" % 3 if lineFiltering else 0) def set_current_mode(self, low_noise=False): """ Sets instrument to current control mode with either low noise or high bandwidth""" if low_noise: self.ask("IMODE 2") self.sensitivity_values = self.SENSITIVITIES_IMODE[2] else: self.ask("IMODE 1") self.sensitivity_values = self.SENSITIVITIES_IMODE[1] def set_channel_A_mode(self): """ Sets instrument to channel A mode -- assuming it is in voltage mode """ self.ask("VMODE 1") @property def id(self): """Get the instrument ID and firmware version""" return f"{self.ask('ID')}/{self.ask('VER')}" @property def auto_gain(self): return int(self.ask("AUTOMATIC")) == 1 @auto_gain.setter def auto_gain(self, setval): if setval: self.ask("AUTOMATIC 1") else: self.ask("AUTOMATIC 0") def shutdown(self): """ Ensures the instrument in a safe state """ log.info("Shutting down %s" % self.name) self.voltage = 0. super().shutdown() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3936055 pymeasure-0.14.0/pymeasure/instruments/ami/0000755000175100001770000000000014623331176020374 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/ami/__init__.py0000644000175100001770000000224014623331163022477 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/ami/ami430.py0000644000175100001770000001636614623331163021753 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIMixin from time import sleep, time import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class AMI430(SCPIMixin, 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, adapter, name="AMI superconducting magnet power supply.", **kwargs): kwargs.setdefault('read_termination', '\n') super().__init__( adapter, name, **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", """Control the coil constant in kGauss/A. (float)""" ) voltage_limit = Instrument.control( "VOLT:LIM?", "CONF:VOLT:LIM %g", """Control the voltage limit for charging/discharging the magnet. (float)""" ) target_current = Instrument.control( "CURR:TARG?", "CONF:CURR:TARG %g", """Control the target current in A for the magnet. (float)""" ) target_field = Instrument.control( "FIELD:TARG?", "CONF:FIELD:TARG %g", """Control the target field in kGauss for the magnet. (float)""" ) ramp_rate_current = Instrument.control( "RAMP:RATE:CURR:1?", "CONF:RAMP:RATE:CURR 1,%g", """Control the current ramping rate in A/s. (float)""" ) ramp_rate_field = Instrument.control( "RAMP:RATE:FIELD:1?", "CONF:RAMP:RATE:FIELD 1,%g,1.00", """Control the field ramping rate in kGauss/s. (float)""" ) magnet_current = Instrument.measurement("CURR:MAG?", """Get the current in Amps of the magnet. """ ) supply_current = Instrument.measurement("CURR:SUPP?", """Get the current in Amps of the power supply. """ ) field = Instrument.measurement("FIELD:MAG?", """Get the field in kGauss of the magnet. """ ) state = Instrument.measurement("STATE?", """Get 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): """Get the magnet status.""" 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() super().shutdown() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3936055 pymeasure-0.14.0/pymeasure/instruments/anaheimautomation/0000755000175100001770000000000014623331176023331 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/anaheimautomation/__init__.py0000644000175100001770000000230214623331163025433 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .dpseriesmotorcontroller import DPSeriesMotorController ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/anaheimautomation/dpseriesmotorcontroller.py0000644000175100001770000003217414623331163030711 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from enum import IntFlag from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_range, truncated_range, strict_discrete_set log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class DPSeriesErrors(IntFlag): """ IntFlag type to decode error register queries. Error codes are as follows: 0: no error 1: Receive Overflow Error: serial communications had a receiving error. 2: Encoder Error 1: encoder needed to correct the motor position. 4: Encoder Error 2: encoder could not finish motor position correction. 8: Command Error: a bad command was sent to the controller. 16: Motor Error: motor speed profiles are set incorrectly. 32: Range Overflow Error: go to position has an overflow error. 64: Range Error: invalid number of commands and characters sent to the controller. 128: Transmit Error: Too many parameters sent back to the pc. 256: Mode Error: Controller is in a wrong mode. 512: Zero Parameters Error: Command sent to the controller that expected to see parameters follow, but none were given. 1024: Busy Error: The controller is busy indexing (moving a motor). 2048: Memory Range Error: Specified address is out of range. 4096: Memory Command Error: Command pulled from memory is invalid. 8192: Thumbwheel Read Error: Error reading the thumbwheel, or thumbwheel is not present. """ NO_ERR = 0 RCV_OVERFLOW_ERR = 1 ENC_ERR_1 = 2 ENC_ERR_2 = 4 CMD_ERR = 8 MOT_ERR = 16 RANGE_OVERFLOW_ERR = 32 RANGE_ERR = 64 TX_ERR = 128 MODE_ERR = 256 ZERO_PARAMS_ERR = 512 BUSY_ERR = 1024 MEM_RANGE_ERR = 2048 MEM_CMD_ERR = 4096 THBWHEEL_ERR = 8192 class DPSeriesMotorController(Instrument): """Base class to interface with Anaheim Automation DP series stepper motor controllers. This driver has been tested with the DPY50601 and DPE25601 motor controllers. """ address = Instrument.control( "%", "~%i", """Integer property representing the address that the motor controller uses for serial communications.""", validator=strict_range, values=[0, 99], cast=int, ) basespeed = Instrument.control( "VB", "B%i", """Integer property that represents the motor controller's starting/homing speed. This property can be set.""", validator=truncated_range, values=[1, 5000], cast=int, ) maxspeed = Instrument.control( "VM", "M%i", """Integer property that represents the motor controller's maximum (running) speed. This property can be set.""", validator=truncated_range, values=[1, 50000], cast=int, ) direction = Instrument.control( "V+", "%s", """A string property that represents the direction in which the stepper motor will rotate upon subsequent step commands. This property can be set. 'CW' corresponds to clockwise rotation and 'CCW' corresponds to counter-clockwise rotation.""", map_values=True, validator=strict_discrete_set, values={"CW": "+", "CCW": "-"}, get_process=lambda d: "+" if d == 1.0 else "-", ) encoder_autocorrect = Instrument.control( "VEA", "EA%i", """A boolean property to enable or disable the encoder auto correct function. This property can be set.""", map_values=True, values={True: 1, False: 0}, validator=strict_discrete_set, cast=int, ) encoder_delay = Instrument.control( "VED", "ED%i", """An integer property that represents the wait time in ms. after a move is finished before the encoder is read for a potential encoder auto-correct action to take place. This property can be set.""", validator=truncated_range, values=[0, 65535], cast=int, ) encoder_motor_ratio = Instrument.control( "VEM", "EM%i", """An integer property that represents the ratio of the number of encoder pulses per motor step. This property can be set.""", validator=truncated_range, values=[1, 255], cast=int, ) encoder_retries = Instrument.control( "VER", "ER%i", """An integer property that represents the number of times the motor controller will try the encoder auto correct function before setting an error flag. This property can be set.""", validator=truncated_range, values=[0, 255], cast=int, ) encoder_window = Instrument.control( "VEW", "EW%i", """An integer property that represents the allowable error in encoder pulses from the desired position before the encoder auto-correct function runs. This property can be set. """, validator=truncated_range, values=[0, 255], cast=int, ) busy = Instrument.measurement( "VF", """Query to see if the controller is currently moving a motor.""" ) error_reg = Instrument.measurement( "!", """Reads the current value of the error codes register.""", get_process=lambda err: DPSeriesErrors(int(err)), ) def check_errors(self): """ Method to read the error codes register and log when an error is detected. :return error_code: one byte with the error codes register contents """ current_errors = self.error_reg if current_errors != 0: logging.error("DP-Series motor controller error detected: %s" % current_errors) return current_errors def __init__(self, adapter, name="Anaheim Automation Stepper Motor Controller", address=0, encoder_enabled=False, **kwargs): """ Initialize communication with the motor controller with the address given by `address`. In addition to the keyword arguments that can be set for the Instrument base class, this class has the following kwargs: :param address: (int) Address that the motor controller uses for serial communiation. :param encoder_enabled: (bool) Flag to indicate if the driver should use an encoder input to set its position property. """ self._address = address self._encoder_enabled = encoder_enabled kwargs.setdefault('write_termination', '\r') kwargs.setdefault('read_termination', '\r') kwargs.setdefault('timeout', 2000) super().__init__( adapter, name, includeSCPI=False, asrl={'baud_rate': 38400}, **kwargs ) @property def encoder_enabled(self): """ A boolean property to represent whether an external encoder is connected and should be used to set the :attr:`step_position` property. """ return self._encoder_enabled @encoder_enabled.setter def encoder_enabled(self, en): self._encoder_enabled = bool(en) @property def step_position(self): """ Integer property representing the value of the motor position measured in steps counted by the motor controller or, if :attr:`encoder_enabled` is set, the steps counted by an externally connected encoder. Note that in the DP series motor controller instrument manuals, this property would be referred to as the 'absolute position' while this driver implements a conversion between steps and absolute units for the :attr:`absolute_position` property. This property can be set. """ if self._encoder_enabled: pos = self.ask("VEP") else: pos = self.ask("VZ") return int(pos) @step_position.setter def step_position(self, pos): strict_range(pos, (-8388607, 8388607)) self.write("P%i" % pos) self.write("G") @property def absolute_position(self): """ Float property representing the value of the motor position measured in absolute units. Note that in DP series motor controller instrument manuals, 'absolute position' refers to the :attr:`step_position` property rather than this property. Also note that use of this property relies on :meth:`steps_to_absolute()` and :meth:`absolute_to_steps()` being implemented in a subclass. In this way, the user can define the conversion from a motor step position into any desired absolute unit. Absolute units could be the position in meters of a linear stage or the angular position of a gimbal mount, etc. This property can be set. """ step_pos = self.step_position return self.steps_to_absolute(step_pos) @absolute_position.setter def absolute_position(self, abs_pos): steps_pos = self.absolute_to_steps(abs_pos) self.step_position = steps_pos def absolute_to_steps(self, pos): """ Convert an absolute position to a number of steps to move. This must be implemented in subclasses. :param pos: Absolute position in the units determined by the subclassed :meth:`absolute_to_steps()` method. """ raise NotImplementedError("absolute_to_steps() must be implemented in subclasses!") def steps_to_absolute(self, steps): """ Convert a position measured in steps to an absolute position. :param steps: Position in steps to be converted to an absolute position. """ raise NotImplementedError("steps_to_absolute() must be implemented in subclasses!") def reset_position(self): """ Reset position as counted by the motor controller and an externally connected encoder to 0. """ # reset encoder recorded position # self.write("ET") # reset motor recorded position # self.write("Z0") def stop(self): """Method that stops all motion on the motor controller.""" self.write(".") def move(self, direction): """ Move the stepper motor continuously in the given direction until a stop command is sent or a limit switch is reached. This method corresponds to the 'slew' command in the DP series instrument manuals. :param direction: value to set on the direction property before moving the motor. """ self.direction = direction self.write("S") def home(self, home_mode): """ Send command to the motor controller to 'home' the motor. :param home_mode: ``0`` or ``1`` specifying which homing mode to run. 0 will perform a homing operation where the controller moves the motor until a soft limit is reached, then will ramp down to base speed and continue motion until a home limit is reached. In mode 1, the controller will move the motor until a limit is reached, then will ramp down to base speed, change direction, and run until the limit is released. """ hm = int(home_mode) if hm == 0 or hm == 1: self.write("H%i" % hm) else: raise ValueError("Invalid home mode %i specified!" % hm) def write(self, command): """Override the instrument base write method to add the motor controller's address to the command string. :param command: command string to be sent to the motor controller. """ # check if @ was already prepended when using say, the SerialAdapter # if "@" in command: cmd_str = command elif "%" in command or "~" in command: cmd_str = "@%s" % command else: cmd_str = "@%i%s" % (self._address, command) super().write(cmd_str) def wait_for_completion(self, interval=0.5): """ Block until the controller is not "busy" (i.e. block until the motor is no longer moving.) :param interval: (float) seconds between queries to the "busy" flag. :return: None """ # noqa: E501 while self.busy: sleep(interval) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3976054 pymeasure-0.14.0/pymeasure/instruments/anapico/0000755000175100001770000000000014623331176021240 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/anapico/__init__.py0000644000175100001770000000224414623331163023347 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/anapico/apsin12G.py0000644000175100001770000000546014623331163023177 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set class APSIN12G(SCPIUnknownMixin, Instrument): """ Represents the Anapico APSIN12G Signal Generator with option 9K, HP and GPIB. """ FREQ_LIMIT = [9e3, 12e9] POW_LIMIT = [-30, 27] def __init__(self, adapter, name="Anapico APSIN12G Signal Generator", **kwargs): super().__init__( adapter, name, **kwargs ) power = Instrument.control( "SOUR:POW:LEV:IMM:AMPL?;", "SOUR:POW:LEV:IMM:AMPL %gdBm;", """Control the output power in dBm. (float)""", validator=strict_range, values=POW_LIMIT ) frequency = Instrument.control( "SOUR:FREQ:CW?;", "SOUR:FREQ:CW %eHz;", """Control the output frequency in Hz. (float)""", validator=strict_range, values=FREQ_LIMIT ) blanking = Instrument.control( ":OUTP:BLAN:STAT?", ":OUTP:BLAN:STAT %s", """Control the blanking of output power when frequency is changed. ON makes the output to be blanked (off) while changing frequency. """, validator=strict_discrete_set, values=['ON', 'OFF'] ) reference_output = Instrument.control( "SOUR:ROSC:OUTP:STAT?", "SOUR:ROSC:OUTP:STAT %s", """Control the 10MHz reference output from the synth. (str)""", validator=strict_discrete_set, values=['ON', 'OFF'] ) 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") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3976054 pymeasure-0.14.0/pymeasure/instruments/andeenhagerling/0000755000175100001770000000000014623331176022741 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/andeenhagerling/__init__.py0000644000175100001770000000227714623331163025056 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .ah2500a import AH2500A from .ah2700a import AH2700A ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/andeenhagerling/ah2500a.py0000644000175100001770000001112114623331163024343 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 math import re import logging from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_range log = logging.getLogger(__name__) class AH2500A(Instrument): """ Andeen Hagerling 2500A Precision Capacitance Bridge implementation """ # regular expression to extract measurement values _reclv = re.compile( r"[FHZ0-9.=\s]*C=\s*(-?[0-9.]+)\s*PF L=\s*(-?[0-9.]+)\s*NS V=\s*(-?[0-9.]+)\s*V") _renumeric = re.compile(r'[-+]?(\d*\.?\d+)') def __init__(self, adapter, name=None, timeout=3000, write_termination="\n", read_termination="\n", **kwargs): super().__init__( adapter, name or "Andeen Hagerling 2500A Precision Capacitance Bridge", write_termination=write_termination, read_termination=read_termination, timeout=timeout, includeSCPI=False, **kwargs ) self._triggered = False config = Instrument.measurement( "SHOW", """Get the configuration.""", ) caplossvolt = Instrument.measurement( "Q", """Get the result of a single capacitance, loss measurement and return the values in units of pF and nS. The used measurement voltage is returned as third value.""", # lambda function is needed here since AH2500A is otherwise undefined get_process=lambda v: AH2500A._parse_reply(v), ) vhighest = Instrument.control( "SH V", "V %.4f", """Control maximum RMS value of the used measurement voltage. Values of up to 15 V are allowed. The device will select the best suiting range below the given value.""", validator=strict_range, values=[0, 15], # typical replies: "VOLTAGE HIGHEST= 15.0 V" or # "VOLTAGE HIGHEST 1.00 V" get_process=lambda v: float(AH2500A._renumeric.search(v).group(0)), ) @classmethod def _parse_reply(cls, string): """ parse reply string from Andeen Hagerling capacitance bridges. :param string: reply string from the instrument. This commonly is: 2500A: "C= 1.234567 PF L= 0.000014 NS V= 0.750 V" 2700A: "F= 1000.00 HZ C= 4.20188 PF L=-0.0260 NS V= 15.0 V" :returns: tuple with C, L, V values """ m = cls._reclv.match(string) if m is not None: values = tuple(map(float, m.groups())) return values # if an invalid string is returned ('EXCESS NOISE') if string.strip() == "EXCESS NOISE": log.warning("Excess noise, check your experiment setup") return (math.nan, math.nan, math.nan) else: # some unknown return string (e.g. misconfigured units) raise Exception(f'Returned string "{string}" could not be parsed') def trigger(self): """ Triggers a new measurement without blocking and waiting for the return value. """ self.write("TRG") self._triggered = True def triggered_caplossvolt(self): """ reads the measurement value after the device was triggered by the trigger function. """ if not self._triggered: log.warning( "Device not triggered, trigger manually for better timing") self.trigger() self._triggered = False return AH2500A._parse_reply(self.read()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/andeenhagerling/ah2700a.py0000644000175100001770000000505414623331163024355 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .ah2500a import AH2500A class AH2700A(AH2500A): """ Andeen Hagerling 2700A Precision Capacitance Bridge implementation """ def __init__(self, adapter, name="Andeen Hagerling 2700A Precision Capacitance Bridge", timeout=5000, **kwargs): super().__init__( adapter, name=name, timeout=timeout, **kwargs ) id = Instrument.measurement( "*IDN?", """Get the instrument identification """ ) config = Instrument.measurement( "SHOW ALL", """Get the configuration """, ) frequency = Instrument.control( "SH FR", "FR %.1f", """Control test frequency used for the measurements. Allowed are values between 50 and 20000 Hz. The device selects the closest possible frequency to the given value.""", validator=strict_range, values=[50, 20000], # typical reply: "FREQUENCY 1200.0 Hz" get_process=lambda v: float(AH2500A._renumeric.search(v).group(0)), ) def reset(self): """ Resets the instrument. """ self.write("*RST") def trigger(self): """ Triggers a new measurement without blocking and waiting for the return value. """ self.write("*TRG") self._triggered = True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3976054 pymeasure-0.14.0/pymeasure/instruments/anritsu/0000755000175100001770000000000014623331176021313 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/anritsu/__init__.py0000644000175100001770000000264114623331163023423 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from .anritsuMS9740A import AnritsuMS9740A from .anritsuMS2090A import AnritsuMS2090A from .anritsuMS464xB import AnritsuMS464xB, AnritsuMS4642B, AnritsuMS4644B,\ AnritsuMS4645B, AnritsuMS4647B ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/anritsu/anritsuMG3692C.py0000644000175100001770000000476114623331163024231 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin class AnritsuMG3692C(SCPIUnknownMixin, Instrument): """ Represents the Anritsu MG3692C Signal Generator """ power = Instrument.control( ":POWER?;", ":POWER %g dBm;", """Control the output power in dBm. (float))""" ) frequency = Instrument.control( ":FREQUENCY?;", ":FREQUENCY %e Hz;", """Control the output frequency in Hz. This property can be set. (float)""" ) def __init__(self, adapter, name="Anritsu MG3692C Signal Generator", **kwargs): super().__init__( adapter, name, **kwargs ) @property def output(self): """Control the signal output state. (bool)""" 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() super().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/anritsu/anritsuMS2090A.py0000644000175100001770000003457014623331163024233 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import ( strict_discrete_set, truncated_range, strict_range, ) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class AnritsuMS2090A(SCPIUnknownMixin, Instrument): """Anritsu MS2090A Handheld Spectrum Analyzer.""" def __init__(self, adapter, name="Anritsu MS2090A Handheld Spectrum Analyzer", **kwargs): self.analysis_mode = None super().__init__( adapter, name, **kwargs) ############# # Mappings # ############# ONOFF = {True: 'ON', False: 'OFF'} OFFFIRSTREPEAT = ['OFF', 'FIRSt', 'REPeat'] SPAMODES = ["SPECtrum", "NRADio", "RTSA", "LTE", "EMFMeter", "PANalyzer"] #################################### # GPS # #################################### gps_full = Instrument.measurement( "FETCh:GPS:FULL?", """ Returns the timestamp, latitude, longitude, altitude, and satellite count of the device. """ ) gps_all = Instrument.measurement( "FETCh:GPS:ALL?", """Returns the fix timestamp, latitude, longitude, altitude and information on the sat used. """ ) gps = Instrument.measurement( ":FETCh:GPS?", """ Returns the timestamp, latitude, and longitude of the device. """ ) gps_last = Instrument.measurement( ":FETCh:GPS:LAST?", """ Returns the timestamp, latitude, longitude, and altitude of the last fixed GPS result. """ ) external_current = Instrument.measurement( "BIAS:EXT:CURR?", """ This command queries the actual bias current in A """ ) #################################### # Spectrum Parameters - Wavelength # #################################### frequency_center = Instrument.control( "FREQuency:CENTer?", "FREQuency:CENTer %g", "Sets the center frequency in Hz", validator=strict_range, values=[9E3, 54E9] ) frequency_offset = Instrument.control( "FREQuency:OFFSet?", "FREQuency:OFFSet %g", "Sets the frequency offset in Hz", validator=strict_range, values=[-10E9, 10E9], ) frequency_span = Instrument.control( "FREQuency:SPAN?", "FREQuency:SPAN %g", "Sets the frequency span in Hz", validator=strict_range, values=[10, 400000000000], ) frequency_span_full = Instrument.setting( "FREQuency:SPAN:FULL", "Sets the frequency span to full span" ) frequency_span_last = Instrument.setting( "FREQuency:SPAN:LAST", "Sets the frequency span to the previous span value." ) frequency_start = Instrument.control( "FREQuency:STARt?", "FREQuency:STARt %g", "Sets the start frequency in Hz", validator=strict_range, values=[9E3, 54E9], ) frequency_step = Instrument.control( ":FREQuency:STEP?", ":FREQuency:STEP %g", "Set or query the step size to gradually increase or decrease frequency values in Hz", validator=strict_range, values=[1E3, 1E9], ) frequency_stop = Instrument.control( "FREQuency:STOP?", "FREQuency:STOP %g", "Sets the start frequency in Hz", validator=strict_range, values=[9E3, 54E9], ) fetch_power = Instrument.measurement( "FET:CHP:CHP?", """ Returns the most recent channel power measurement. """ ) fetch_density = Instrument.measurement( "FET:CHP:DEN?", """ Returns the most recent channel density measurement """ ) fetch_pbch_constellation = Instrument.measurement( "FET:CONS:PBCH", """ Get the latest Physical Broadcast Channel constellation hitmap """ ) fetch_pdsch_constellation = Instrument.measurement( "FET:CONS:PDSC?", """ Get the latest Physical Downlink Shared Channel constellation """ ) fetch_control = Instrument.measurement( "FET:CONT?", """ Returns the Control Channel measurement in json format. """ ) fetch_eirpower = Instrument.measurement( "FET:EIRP?", """ Returns the current EIRP, Max EIRP, Horizontal EIRP, Vertical and Sum EIRP results in dBm. """ ) fetch_eirpower_data = Instrument.measurement( "FET:EIRP:DAT?", """ This command returns the current EIRP measurement result in dBm. """ ) fetch_eirpower_max = Instrument.measurement( "FET:EIRP:MAX?", """ This command returns the Max EIRP measurement result in dBm. """ ) fetch_emf = Instrument.measurement( "FET:EMF?", """ Return the current EMF measurement data. JSON format. """ ) fetch_emf_meter = Instrument.measurement( "FET:EMF:MET?", """ Return the live EMF measurement data. JSON format. """ ) fetch_emf_meter_sample = Instrument.measurement( "FET:EMF:MET:SAM%g?", """ Return the EMF measurement data for a specified sample number. JSON format. """, values=[1, 16], ) fetch_interference_power = Instrument.measurement( "FET:INT:POW?", """ Fetch Interference Finder Integrated Power. """ ) fetch_mimo_antenas = Instrument.measurement( "FET:MIMO:ANT?", """ Returns the sync power measurement in json format. """ ) fetch_ocupied_bw = Instrument.measurement( "FET:OBW%g?", """ Returns the different set of measurement information depending on the suffix. """, values=[1, 2] ) fetch_ota_mapping = Instrument.measurement( "FET:OTA:MAPP?", """ Returns the most recent Coverage Mapping measurement result. """ ) fetch_pan = Instrument.measurement( "FET:PAN?", """ Return the current Pulse Analyzer measurement data. JSON format """ ) fetch_pci = Instrument.measurement( "FET:PCI?", """ Returns PCI measurements """ ) fetch_pdsch = Instrument.measurement( "FET:PDSC?", """ Returns the Data Channel Measurements in JSON format. """ ) fetch_peak = Instrument.measurement( "FET:PEAK?", """ Returns a pair of peak amplitude in current sweep. """ ) fetch_rrm = Instrument.measurement( "FET:RRM?", """ Returns the Radio Resource Management in JSON format. """ ) fetch_scan = Instrument.measurement( "FET:SCAN?", """ Returns the cell scanner measurements in JSON format """ ) fetch_semask = Instrument.measurement( "FET:SEM?", """ This command returns the current Spectral Emission Mask measurement result. """ ) fetch_ssb = Instrument.measurement( "FET:SSB?", """ Returns the SSB measurement """ ) fetch_sync_evm = Instrument.measurement( "FET:SYNC:EVM?", """ Returns the Sync EVM measurement in JSON format. """ ) fetch_sync_power = Instrument.measurement( "FET:SYNC:POW?", """ Returns the sync power measurements in JSON format """ ) fetch_tae = Instrument.measurement( "FET:TAE?", """ Returns the Time Alignment Error in JSON format. """ ) init_continuous = Instrument.control( "INIT:CONT?", "INIT:CONT %g", "Specified whether the sweep/measurement is triggered continuously", values=ONOFF, map_values=True, validator=strict_discrete_set ) init_spa_self = Instrument.measurement( "INIT:SPA:SELF?", docs=""" Perform a self-test and return the results. """ ) active_state = Instrument.control( "INST:ACT:STAT?", "INST:ACT:STAT %g", docs=""" The "set" state indicates that the instrument is used by someone. """, values=ONOFF, map_values=True, validator=strict_discrete_set ) meas_acpower = Instrument.measurement( "MEAS:ACP?", """ Sets the active measurement to adjacent channel power ratio, sets the default measurement parameters, triggers a new measurement and returns the main channel power, lower adjacent, upper adjacent, lower alternate and upper alternate channel power results. """ ) meas_power_all = Instrument.measurement( "MEAS:CHP?", """ Sets the active measurement to channel power, sets the default measurement parameters, triggers a new measurement and returns the channel power and channel power density results. It is a combination of the commands :CONFigure:CHPower; :READ:CHPower? """ ) meas_power = Instrument.measurement( "MEASure:CHPower:CHPower?", """ Sets the active measurement to channel power, sets the default measurement parameters, triggers a new measurement and returns channel power as the result. It is a combination of the commands :CONFigure:CHPower; :READ:CHPower:CHPower? """ ) power_density = Instrument.measurement( "MEASure:CHPower:DENSity?", """ Sets the active measurement to channel power, sets the default measurement parameters, triggers a new measurement and returns channel power density as the result. It is a combination of the commands :CONFigure:CHPower; :READ:CHPower:DENSity? """ ) meas_emf_meter_clear_all = Instrument.setting( "MEASure:EMF:METer:CLEar:ALL", """ Clear the EMF measurement data of all samples. Sampling state will be turned off if it was on. """ ) meas_emf_meter_clear_sample = Instrument.setting( "MEASure:EMF:METer:CLEar:SAMPle%g", """ Clear the EMF measurement data for a specified sample number. Sampling state will be turned off if the specified sample is currently active. """, validator=truncated_range, values=[1, 16], ) meas_emf_meter_sample = Instrument.control( "MEASure:EMF:METer:SAMPle:STATe?", "MEASure:EMF:METer:SAMPle:STATe%g", docs=""" Start or Stop applying the measurement results to the currently selected sample """, values=ONOFF, map_values=True, validator=strict_discrete_set ) meas_int_power = Instrument.measurement( "MEASure:INTerference:POWer?", """ Sets the active measurement to interference finder, sets the default measurement parameters, triggers a new measurement and returns integrated power as the result. It is a combination of the commands :CONFigure:INTerference; :READ:INTerference:POWer? """ ) meas_iq_capture = Instrument.setting( "MEASure:IQ:CAPTure", """ This set command is used to start the IQ capture measurement. """ ) meas_iq_capture_fail = Instrument.control( "MEASure:IQ:CAPTure:FAIL?", "MEASure:IQ:CAPTure:FAIL %g", """ Sets or queries whether the instrument will automatically save an IQ capture when losing sync """, values=OFFFIRSTREPEAT ) meas_ota_mapp = Instrument.measurement( "MEASure:OTA:MAPPing?", """ Sets the active measurement to OTA Coverage Mapping, sets the default measurement parameters, triggers a new measurement, and returns the measured values. """ ) meas_ota_run = Instrument.control( "MEASure:OTA:MAPPing:RUN?", "MEASure:OTA:MAPPing:RUN %g", """ Turn on/off OTA Coverage Mapping Data Collection. The instrument must be in Coverage Mapping measurement for the command to be effective """, values=ONOFF, map_values=True, validator=strict_discrete_set ) view_sense_modes = Instrument.measurement( "MODE:CATalog?", """ Returns a list of available modes for the Spa application. The response is a comma-separated list of mode names. See command [:SENSe]:MODE for the mode name specification. """ ) sense_mode = Instrument.control( "MODE?", ":MODE %g", """ Set the operational mode of the Spa app. """, values=SPAMODES, ) preamp = Instrument.control( "POWer:RF:GAIN:STATe?", "POWer:RF:GAIN:STATe %s", """ Sets the state of the preamp. Note that this may cause a change in the reference level and/or attenuation. """, values=ONOFF, map_values=True, validator=strict_discrete_set ) def init_sweep(self): """ Initiate a sweep/measurement. """ self.write("INIT") def init_all_sweep(self): """ Initiate all sweep/measurement. """ self.write('INIT:ALL') def abort(self): """ Initiate a sweep/measurement. """ self.write("ABOR") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/anritsu/anritsuMS464xB.py0000644000175100001770000010266014623331163024343 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import ( strict_discrete_set, strict_range ) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class AnritsuMS464xB(SCPIUnknownMixin, Instrument): """ A class representing the Anritsu MS464xB Vector Network Analyzer (VNA) series. This family consists of the MS4642B, MS4644B, MS4645B, and MS4647B, which are represented in their respective classes (:class:`~.AnritsuMS4642B`, :class:`~.AnritsuMS4644B`, :class:`~.AnritsuMS4645B`, :class:`~.AnritsuMS4647B`), that only differ in the available frequency range. They can contain up to 16 instances of :class:`~.MeasurementChannel` (depending on the configuration of the instrument), that are accessible via the `channels` dict or directly via `ch_` + the channel number. :param active_channels: defines the number of active channels (default=16); if active_channels is "auto", the instrument will be queried for the number of active channels. :type active_channels: int (1-16) or str ("auto") :param installed_ports: defines the number of installed ports (default=4); if "auto" is provided, the instrument will be queried for the number of ports :type installed_ports: int (1-4) or str ("auto") :param traces_per_channel: defines the number of traces that is assumed for each channel (between 1 and 16); if not provided, the maximum number is assumed; "auto" is provided, the instrument will be queried for the number of traces of each channel. :type traces_per_channel: int (1-16) or str ("auto") or None """ CHANNELS_MAX = 16 TRACES_MAX = 16 PORTS = 4 TRIGGER_TYPES = ["POIN", "SWE", "CHAN", "ALL"] FREQUENCY_RANGE = [1E7, 7E10] SPARAM_LIST = ["S11", "S12", "S21", "S22", "S13", "S23", "S33", "S31", "S32", "S14", "S24", "S34", "S41", "S42", "S43", "S44", ] DISPLAY_LAYOUTS = ["R1C1", "R1C2", "R2C1", "R1C3", "R3C1", "R2C2C1", "R2C1C2", "C2R2R1", "C2R1R2", "R1C4", "R4C1", "R2C2", "R2C3", "R3C2", "R2C4", "R4C2", "R3C3", "R5C2", "R2C5", "R4C3", "R3C4", "R4C4"] def __init__(self, adapter, name="Anritsu MS464xB Vector Network Analyzer", active_channels=16, installed_ports=4, traces_per_channel=None, **kwargs): super().__init__( adapter, name, timeout=10000, **kwargs, ) self.PORTS = self.number_of_ports if installed_ports == "auto" else installed_ports number_of_channels = None if active_channels == "auto" else active_channels self.update_channels(number_of_channels=number_of_channels, traces=traces_per_channel) def update_channels(self, number_of_channels=None, **kwargs): """Create or remove channels to be correct with the actual number of channels. :param int number_of_channels: optional, if given, defines the desired number of channels. """ if number_of_channels is None: number_of_channels = self.number_of_channels if not hasattr(self, "channels"): self.channels = {} if len(self.channels) == number_of_channels: return # Remove redundant channels while len(self.channels) > number_of_channels: self.remove_child(self.channels[len(self.channels)]) # Create new channels while len(self.channels) < number_of_channels: self.add_child(MeasurementChannel, len(self.channels) + 1, frequency_range=self.FREQUENCY_RANGE, **kwargs) def check_errors(self): """ Read all errors from the instrument. :return: list of error entries """ errors = [] while True: err = self.values("SYST:ERR?") if err[0] != "No Error": log.error(f"{self.name}: {err[0]}") errors.append(err) else: break return errors datablock_header_format = Instrument.control( "FDHX?", "FDH%d", """Control the way the arbitrary block header for output data is formed. Valid values are: ===== =========================================================== value description ===== =========================================================== 0 A block header with arbitrary length will be sent. 1 The block header will have a fixed length of 11 characters. 2 No block header will be sent. Not IEEE 488.2 compliant. ===== =========================================================== """, values=[0, 1, 2], validator=strict_discrete_set, cast=int, ) datafile_frequency_unit = Instrument.control( ":FORM:SNP:FREQ?", ":FORM:SNP:FREQ %s", """Control the frequency unit displayed in a SNP data file. Valid values are HZ, KHZ, MHZ, GHZ. """, values=["HZ", "KHZ", "MHZ", "GHZ"], validator=strict_discrete_set, ) datablock_numeric_format = Instrument.control( ":FORM:DATA?", ":FORM:DATA %s", """Control format for numeric I/O data representation. Valid values are: ===== ========================================================================== value description ===== ========================================================================== ASCII An ASCII number of 20 or 21 characters long with floating point notation. 8byte 8 bytes of binary floating point number representation limited to 64 bits. 4byte 4 bytes of floating point number representation. ===== ========================================================================== """, values={"ASCII": "ASC", "8byte": "REAL", "4byte": "REAL32"}, map_values=True, ) datafile_include_heading = Instrument.control( ":FORM:DATA:HEAD?", ":FORM:DATA:HEAD %d", """Control whether a heading is included in the data files. """, values={True: 1, False: 0}, map_values=True, ) datafile_parameter_format = Instrument.control( ":FORM:SNP:PAR?", ":FORM:SNP:PAR %s", """Control the parameter format displayed in an SNP data file. Valid values are: ===== =========================== value description ===== =========================== LINPH Linear and Phase. LOGPH Log and Phase. REIM Real and Imaginary Numbers. ===== =========================== """, values=["LINPH", "LOGPH", "REIM"], validator=strict_discrete_set, ) data_drawing_enabled = Instrument.control( "DD1?", "DD%d", """Control whether data drawing is enabled (True) or not (False). """, values={True: 1, False: 0}, map_values=True, ) event_status_enable_bits = Instrument.control( "*ESE?", "*ESE %d", """Control the Standard Event Status Enable Register bits. The register can be queried using the :meth:`~.query_event_status_register` method. Valid values are between 0 and 255. Refer to the instrument manual for an explanation of the bits. """, values=[0, 255], validator=strict_range, cast=int, ) def query_event_status_register(self): """ Query the value of the Standard Event Status Register. Note that querying this value, clears the register. Refer to the instrument manual for an explanation of the returned value. """ return self.values("*ESR?", cast=int)[0] service_request_enable_bits = Instrument.control( "*SRE?", "*SRE %d", """Control the Service Request Enable Register bits. Valid values are between 0 and 255; setting 0 performs a register reset. Refer to the instrument manual for an explanation of the bits. """, values=[0, 255], validator=strict_range, cast=int, ) def return_to_local(self): """ Returns the instrument to local operation. """ self.write("RTL") binary_data_byte_order = Instrument.control( ":FORM:BORD?", ":FORM:BORD %s", """Control the binary numeric I/O data byte order. valid values are: ===== ========================================= value description ===== ========================================= NORM The most significant byte (MSB) is first SWAP The least significant byte (LSB) is first ===== ========================================= """, values=["NORM", "SWAP"], validator=strict_discrete_set, ) max_number_of_points = Instrument.control( ":SYST:POIN:MAX?", ":SYST:POIN:MAX %d", """Control the maximum number of points the instrument can measure in a sweep. Note that when this value is changed, the instrument will be rebooted. Valid values are 25000 and 100000. When 25000 points is selected, the instrument supports 16 channels with 16 traces each; when 100000 is selected, the instrument supports 1 channel with 16 traces. """, values=[25000, 100000], validator=strict_discrete_set, cast=int, ) number_of_ports = Instrument.measurement( ":SYST:PORT:COUN?", """Get the number of instrument test ports. """, cast=int, ) number_of_channels = Instrument.control( ":DISP:COUN?", ":DISP:COUN %d", """Control the number of displayed (and therefore accessible) channels. When the system is in 25000 points mode, the number of channels can be 1, 2, 3, 4, 6, 8, 9, 10, 12, or 16; when the system is in 100000 points mode, the system only supports 1 channel. If a value is provided that is not valid in the present mode, the instrument is set to the next higher channel number. """, values=[1, CHANNELS_MAX], validator=strict_range, cast=int, ) display_layout = Instrument.control( ":DISP:SPL?", ":DISP:SPL %s", """Control the channel display layout in a Row-by-Column format. Valid values are: {}. The number following the R indicates the number of rows, following the C the number of columns; e.g. R2C2 results in a 2-by-2 layout. The options that contain two C's or R's result in asymmetric layouts; e.g. R2C1C2 results in a layout with 1 channel on top and two channels side-by-side on the bottom row. """.format(", ".join(DISPLAY_LAYOUTS)), values=DISPLAY_LAYOUTS, validator=strict_discrete_set, cast=str, ) active_channel = Instrument.control( ":DISP:WIND:ACT?", ":DISP:WIND%d:ACT", """Control the active channel. """, values=[1, CHANNELS_MAX], validator=strict_range, cast=int, ) bandwidth_enhancer_enabled = Instrument.control( ":SENS:BAND:ENH?", ":SENS:BAND:ENH %d", """Control the state of the IF bandwidth enhancer. """, values={True: 1, False: 0}, map_values=True, ) trigger_source = Instrument.control( ":TRIG:SOUR?", ":TRIG:SOUR %s", """Control the source of the sweep/measurement triggering. Valid values are: ===== ================================================== value description ===== ================================================== AUTO Automatic triggering MAN Manual triggering EXTT Triggering from rear panel BNC via the GPIB parser EXT External triggering port REM Remote triggering ===== ================================================== """, values=["AUTO", "MAN", "EXTT", "EXT", "REM"], validator=strict_discrete_set, ) external_trigger_type = Instrument.control( ":TRIG:EXT:TYP?", ":TRIG:EXT:TYP %s", """Control the type of trigger that will be associated with the external trigger. Valid values are POIN (for point), SWE (for sweep), CHAN (for channel), and ALL. """, values=TRIGGER_TYPES, validator=strict_discrete_set, ) external_trigger_delay = Instrument.control( ":TRIG:EXT:DEL?", ":TRIG:EXT:DEL %g", """Control the delay time of the external trigger in seconds. Valid values are between 0 [s] and 10 [s] in steps of 1e-9 [s] (i.e. 1 ns). """, values=[0, 10], validator=strict_range, ) external_trigger_edge = Instrument.control( ":TRIG:EXT:EDG?", ":TRIG:EXT:EDG %s", """Control the edge type of the external trigger. Valid values are POS (for positive or leading edge) or NEG (for negative or trailing edge). """, values=["POS", "NEG"], validator=strict_discrete_set, ) external_trigger_handshake = Instrument.control( ":TRIG:EXT:HAND?", ":TRIG:EXT:HAND %s", """Control status of the external trigger handshake. """, values={True: 1, False: 0}, map_values=True, ) remote_trigger_type = Instrument.control( ":TRIG:REM:TYP?", ":TRIG:REM:TYP %s", """Control the type of trigger that will be associated with the remote trigger. Valid values are POIN (for point), SWE (for sweep), CHAN (for channel), and ALL. """, values=TRIGGER_TYPES, validator=strict_discrete_set, ) manual_trigger_type = Instrument.control( ":TRIG:MAN:TYP?", ":TRIG:MAN:TYP %s", """Control the type of trigger that will be associated with the manual trigger. Valid values are POIN (for point), SWE (for sweep), CHAN (for channel), and ALL. """, values=TRIGGER_TYPES, validator=strict_discrete_set, ) def trigger(self): """ Trigger a continuous sweep from the remote interface. """ self.write("*TRG") def trigger_single(self): """ Trigger a single sweep with synchronization from the remote interface. """ self.write(":TRIG:SING") def trigger_continuous(self): """ Trigger a continuous sweep from the remote interface. """ self.write(":TRIG") hold_function_all_channels = Instrument.control( ":SENS:HOLD:FUNC?", ":SENS:HOLD:FUNC %s", """Control the hold function of all channels. Valid values are: ===== ================================================= value description ===== ================================================= CONT Perform continuous sweeps on all channels HOLD Hold the sweep on all channels SING Perform a single sweep and then hold all channels ===== ================================================= """, values=["CONT", "HOLD", "SING"], validator=strict_discrete_set, ) def load_data_file(self, filename): """Load a data file from the VNA HDD into the VNA memory. :param str filename: full filename including path """ self.write(f":MMEM:LOAD '{filename}'") def delete_data_file(self, filename): """Delete a file on the VNA HDD. :param str filename: full filename including path """ self.write(f":MMEM:DEL '{filename}'") def copy_data_file(self, from_filename, to_filename): """Copy a file on the VNA HDD. :param str from_filename: full filename including pat :param str to_filename: full filename including path """ self.write(f":MMEM:COPY '{from_filename}', '{to_filename}'") def load_data_file_to_memory(self, filename): """Load a data file to a memory trace. :param str filename: full filename including path """ self.write(f":MMEM:LOAD:MDATA '{filename}'") def create_directory(self, dir_name): """Create a directory on the VNA HDD. :param str dir_name: directory name """ self.write(f":MMEM:MDIR '{dir_name}'") def delete_directory(self, dir_name): """Delete a directory on the VNA HDD. :param str dir_name: directory name """ self.write(f":MMEM:RDIR '{dir_name}'") def store_image(self, filename): """Capture a screenshot to the file specified. :param str filename: full filename including path """ self.write(f":MMEM:STOR:IMAG '{filename}'") def read_datafile( self, channel, sweep_points, datafile_freq, datafile_par, filename, ): """Read a data file from the VNA. :param int channel: Channel Index :param int sweep_points: number of sweep point as an integer :param DataFileFrequencyUnits datafile_freq: Data file frequency unit :param DataFileParameter datafile_par: Data file parameter format :param str filename: full path of the file to be saved """ cur_ch = self.channels[channel] # type: MeasurementChannel cur_ch.sweep_points = sweep_points self.datafile_frequency_unit = datafile_freq self.datafile_parameter_format = datafile_par self.write("TRS;WFS;OS2P") bytes_to_transfer = int(self.read_bytes(11)[2:11]) data = self.read_bytes(bytes_to_transfer) with open(filename, "w") as textfile: data_list = data.split(b"\r\n") for s in data_list: textfile.write(str(s)[2 : len(s)] + "\n") # noqa class Port(Channel): """Represents a port within a :class:`~.MeasurementChannel` of the Anritsu MS464xB VNA. """ placeholder = "pt" power_level = Channel.control( ":SOUR{{ch}}:POW:PORT{pt}?", ":SOUR{{ch}}:POW:PORT{pt} %g", """Control the power level (in dBm) of the indicated port on the indicated channel. """, values=[-3E1, 3E1], validator=strict_range, ) class Trace(Channel): """Represents a trace within a :class:`~.MeasurementChannel` of the Anritsu MS464xB VNA. """ placeholder = "tr" def activate(self): """ Set the indicated trace as the active one. """ self.write(":CALC{{ch}}:PAR{tr}:SEL") measurement_parameter = Channel.control( ":CALC{{ch}}:PAR{tr}:DEF?", ":CALC{{ch}}:PAR{tr}:DEF %s", """Control the measurement parameter of the indicated trace. Valid values are any S-parameter (e.g. S11, S12, S41) for 4 ports, or one of the following: ===== ================================================================ value description ===== ================================================================ Sxx S-parameters (1-4 for both x) MIX Response Mixed Mode NFIG Noise Figure trace response (only with option 41 or 48) NPOW Noise Power trace response (only with option 41 or 48) NTEMP Noise Temperature trace response (only with option 41 or 48) AGA Noise Figure Available Gain trace response (only with option 48) IGA Noise Figure Insertion Gain trace response (only with option 48) ===== ================================================================ """, values=AnritsuMS464xB.SPARAM_LIST + ["MIX", "NFIG", "NPOW", "NTEMP", "AGA", "IGA"], validator=strict_discrete_set, ) class MeasurementChannel(Channel): """Represents a channel of Anritsu MS464xB VNA. Contains 4 instances of :class:`~.Port` (accessible via the `ports` dict or directly `pt_` + the port number) and up to 16 instances of :class:`~.Trace` (accessible via the `traces` dict or directly `tr_` + the trace number). :param frequency_range: defines the number of installed ports (default=4). :type frequency_range: list of floats :param traces: defines the number of traces that is assumed for the channel (between 1 and 16); if not provided, the maximum number is assumed; "auto" is provided, the instrument will be queried for the number of traces. :type traces: int (1-16) or str ("auto") or None """ def __init__(self, *args, frequency_range=None, traces=None, **kwargs): super().__init__(*args, **kwargs) for pt in range(self.parent.PORTS): self.add_child(Port, pt + 1, collection="ports", prefix="pt_") if traces is None: number_of_traces = self.parent.TRACES_MAX elif traces == "auto": number_of_traces = None else: number_of_traces = traces self.update_traces(number_of_traces) if frequency_range is not None: self.update_frequency_range(frequency_range) def update_frequency_range(self, frequency_range): """Update the values-attribute of the frequency-related dynamic properties. :param list frequency_range: the frequency range that the instrument is capable of. """ self.frequency_start_values = frequency_range self.frequency_stop_values = frequency_range self.frequency_center_values = frequency_range self.frequency_CW_values = frequency_range self.frequency_span_values = [2, frequency_range[1]] def update_traces(self, number_of_traces=None): """Create or remove traces to be correct with the actual number of traces. :param int number_of_traces: optional, if given defines the desired number of traces. """ if number_of_traces is None: number_of_traces = self.number_of_traces if not hasattr(self, "traces"): self.traces = {} if len(self.traces) == number_of_traces: return # Remove redant channels while len(self.traces) > number_of_traces: self.remove_child(self.traces[len(self.traces)]) # Remove create new channels while len(self.traces) < number_of_traces: self.add_child(Trace, len(self.traces) + 1, collection="traces", prefix="tr_") def check_errors(self): return self.parent.check_errors() def activate(self): """ Set the indicated channel as the active channel. """ self.write(":DISP:WIND{ch}:ACT") number_of_traces = Channel.control( ":CALC{ch}:PAR:COUN?", ":CALC{ch}:PAR:COUN %d", """Control the number of traces on the specified channel Valid values are between 1 and 16. """, values=[1, AnritsuMS464xB.TRACES_MAX], validator=strict_range, cast=int, ) active_trace = Channel.setting( ":CALC{ch}:PAR%d:SEL", """Set the active trace on the indicated channel. """, values=[1, AnritsuMS464xB.TRACES_MAX], validator=strict_range, ) display_layout = Channel.control( ":DISP:WIND{ch}:SPL?", ":DISP:WIND{ch}:SPL %s", """Control the trace display layout in a Row-by-Column format for the indicated channel. Valid values are: {}. The number following the R indicates the number of rows, following the C the number of columns; e.g. R2C2 results in a 2-by-2 layout. The options that contain two C's or R's result in asymmetric layouts; e.g. R2C1C2 results in a layout with 1 trace on top and two traces side-by-side on the bottom row. """.format(", ".join(AnritsuMS464xB.DISPLAY_LAYOUTS)), values=AnritsuMS464xB.DISPLAY_LAYOUTS, validator=strict_discrete_set, cast=str, ) application_type = Channel.control( ":CALC{ch}:APPL:MEAS:TYP?", ":CALC{ch}:APPL:MEAS:TYP %s", """Control the application type of the specified channel. Valid values are TRAN (for transmission/reflection), NFIG (for noise figure measurement), PULS (for PulseView). """, values=["TRAN", "NFIG", "PULS"], validator=strict_discrete_set, ) hold_function = Channel.control( ":SENS{ch}:HOLD:FUNC?", ":SENS{ch}:HOLD:FUNC %s", """Control the hold function of the specified channel. valid values are: ===== ================================================= value description ===== ================================================= CONT Perform continuous sweeps on all channels HOLD Hold the sweep on all channels SING Perform a single sweep and then hold all channels ===== ================================================= """, values=["CONT", "HOLD", "SING"], validator=strict_discrete_set, ) cw_mode_enabled = Channel.control( ":SENS{ch}:SWE:CW?", ":SENS{ch}:SWE:CW %d", """Control the state of the CW sweep mode of the indicated channel. """, values={True: 1, False: 0}, map_values=True, ) cw_number_of_points = Channel.control( ":SENS{ch}:SWE:CW:POIN?", ":SENS{ch}:SWE:CW:POIN %g", """Control the CW sweep mode number of points of the indicated channel. Valid values are between 1 and 25000 or 100000 depending on the maximum points setting. """, values=[1, 100000], validator=strict_range, cast=int, ) number_of_points = Channel.control( "SENS{ch}:SWE:POIN?", "SENS{ch}:SWE:POIN %g", """Control the number of measurement points in a frequency sweep of the indicated channel. Valid values are between 1 and 25000 or 100000 depending on the maximum points setting. """, values=[1, 100000], validator=strict_range, cast=int, ) frequency_start = Channel.control( ":SENS{ch}:FREQ:STAR?", ":SENS{ch}:FREQ:STAR %g", """Control the start value of the sweep range of the indicated channel in hertz. Valid values are between 1E7 [Hz] (i.e. 10 MHz) and 4E10 [Hz] (i.e. 40 GHz). """, values=AnritsuMS464xB.FREQUENCY_RANGE, validator=strict_range, dynamic=True, ) frequency_stop = Channel.control( ":SENS{ch}:FREQ:STOP?", ":SENS{ch}:FREQ:STOP %g", """Control the stop value of the sweep range of the indicated channel in hertz. Valid values are between 1E7 [Hz] (i.e. 10 MHz) and 4E10 [Hz] (i.e. 40 GHz). """, values=AnritsuMS464xB.FREQUENCY_RANGE, validator=strict_range, dynamic=True, ) frequency_span = Channel.control( ":SENS{ch}:FREQ:SPAN?", ":SENS{ch}:FREQ:SPAN %g", """Control the span value of the sweep range of the indicated channel in hertz. Valid values are between 2 [Hz] and 4E10 [Hz] (i.e. 40 GHz). """, values=[2, AnritsuMS464xB.FREQUENCY_RANGE[1]], validator=strict_range, dynamic=True, ) frequency_center = Channel.control( ":SENS{ch}:FREQ:CENT?", ":SENS{ch}:FREQ:CENT %g", """Control the center value of the sweep range of the indicated channel in hertz. Valid values are between 1E7 [Hz] (i.e. 10 MHz) and 4E10 [Hz] (i.e. 40 GHz). """, values=AnritsuMS464xB.FREQUENCY_RANGE, validator=strict_range, dynamic=True, ) frequency_CW = Channel.control( ":SENS{ch}:FREQ:CW?", ":SENS{ch}:FREQ:CW %g", """Control the CW frequency of the indicated channel in hertz. Valid values are between 1E7 [Hz] (i.e. 10 MHz) and 4E10 [Hz] (i.e. 40 GHz). """, values=AnritsuMS464xB.FREQUENCY_RANGE, validator=strict_range, dynamic=True, ) def clear_average_count(self): """ Clear and restart the averaging sweep count of the indicated channel. """ self.write(":SENS{ch}:AVER:CLE") average_count = Channel.control( ":SENS{ch}:AVER:COUN?", ":SENS{ch}:AVER:COUN %d", """Control the averaging count for the indicated channel. The channel must be turned on. Valid values are between 1 and 1024. """, values=[1, 1024], validator=strict_range, cast=int, ) average_sweep_count = Channel.measurement( ":SENS{ch}:AVER:SWE?", """Get the averaging sweep count for the indicated channel. """, cast=int, ) average_type = Channel.control( ":SENS{ch}:AVER:TYP?", ":SENS{ch}:AVER:TYP %s", """Control the averaging type to for the indicated channel. Valid values are POIN (point-by-point) or SWE (sweep-by-sweep) """, values=["POIN", "SWE"], validator=strict_discrete_set, ) averaging_enabled = Channel.control( ":SENS{ch}:AVER?", ":SENS{ch}:AVER %d", """Control whether the averaging is turned on for the indicated channel. """, values={True: 1, False: 0}, map_values=True, ) sweep_type = Channel.control( ":SENS{ch}:SWE:TYP?", ":SENS{ch}:SWE:TYP %s", """Control the sweep type of the indicated channel. Valid options are: ===== =============================================================== value description ===== =============================================================== LIN Frequency-based linear sweep LOG Frequency-based logarithmic sweep FSEGM Segment-based sweep with frequency-based segments ISEGM Index-based sweep with frequency-based segments POW Power-based sweep with either a CW frequency or swept-frequency MFGC Multiple frequency gain compression ===== =============================================================== """, validator=strict_discrete_set, values=["LIN", "LOG", "FSEGM", "ISEGM", "POW", "MFGC"], ) sweep_mode = Channel.control( ":SENS{ch}:SA:MODE?", ":SENS{ch}:SA:MODE %s", """Control the sweep mode for Spectrum Analysis on the indicated channel. Valid options are VNA (for a VNA-like mode where the instrument will only measure at points in the frequency list) or CLAS (for a classical mode, where the instrument will scan all frequencies in the range).""", validator=strict_discrete_set, values=["VNA", "CLAS"], ) sweep_time = Channel.control( ":SENS{ch}:SWE:TIM?", ":SENS{ch}:SWE:TIM %d", """Control the sweep time of the indicated channel. Valid values are between 2 and 100000.""", validator=strict_range, values=[2, 100000], ) bandwidth = Channel.control( ":SENS{ch}:BWID?", ":SENS{ch}:BWID %g", """Control the IF bandwidth for the indicated channel. Valid values are between 1 [Hz] and 1E6 [Hz] (i.e. 1 MHz). The system will automatically select the closest IF bandwidth from the available options (1, 3, 10 ... 1E5, 3E5, 1E6). """, values=[1, 1E6], validator=strict_range, ) calibration_enabled = Channel.control( ":SENS{ch}:CORR:STAT?", ":SENS{ch}:CORR:STAT %d", """Control whether the RF correction (calibration) is enabled for indicated channel. """, values={True: 1, False: 0}, map_values=True, ) class AnritsuMS4642B(AnritsuMS464xB): """A class representing the Anritsu MS4642B Vector Network Analyzer (VNA). This VNA has a frequency range from 10 MHz to 20 GHz and is part of the :class:`~.AnritsuMS464xB` family of instruments; for documentation, for documentation refer to this base class. """ FREQUENCY_RANGE = [1E7, 2E10] class AnritsuMS4644B(AnritsuMS464xB): """A class representing the Anritsu MS4644B Vector Network Analyzer (VNA). This VNA has a frequency range from 10 MHz to 40 GHz and is part of the :class:`~.AnritsuMS464xB` family of instruments; for documentation, for documentation refer to this base class. """ FREQUENCY_RANGE = [1E7, 4E10] class AnritsuMS4645B(AnritsuMS464xB): """A class representing the Anritsu MS4645B Vector Network Analyzer (VNA). This VNA has a frequency range from 10 MHz to 50 GHz and is part of the :class:`~.AnritsuMS464xB` family of instruments; for documentation, for documentation refer to this base class. """ FREQUENCY_RANGE = [1E7, 5E10] class AnritsuMS4647B(AnritsuMS464xB): """A class representing the Anritsu MS4647B Vector Network Analyzer (VNA). This VNA has a frequency range from 10 MHz to 70 GHz and is part of the :class:`~.AnritsuMS464xB` family of instruments; for documentation, for documentation refer to this base class. """ FREQUENCY_RANGE = [1E7, 7E10] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/anritsu/anritsuMS9710C.py0000644000175100001770000002512014623331163024232 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin 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(SCPIUnknownMixin, Instrument): """Anritsu MS9710C Optical Spectrum Analyzer.""" def __init__(self, adapter, name="Anritsu MS9710C Optical Spectrum Analyzer", **kwargs): """Constructor.""" self.analysis_mode = None super().__init__(adapter, name=name, **kwargs) ############# # Mappings # ############# ONOFF = ["ON", "OFF"] ONOFF_MAPPING = {True: 'ON', False: 'OFF', 1: 'ON', 0: 'OFF', 'ON': 'ON', 'OFF': 'OFF'} ###################### # Status Registers # ###################### ese2 = Instrument.control( "ESE2?", "ESE2 %d", "Control Extended Event Status Enable Register 2", get_process=int ) esr2 = Instrument.control( "ESR2?", "ESR2 %d", "Control Extended Event Status Register 2", get_process=_int_or_neg_one ) ########### # Modes # ########### measure_mode = Instrument.measurement( "MOD?", "Get 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', "Control Center Wavelength of Spectrum Scan in nm.") wavelength_span = Instrument.control( 'SPN?', 'SPN %g', "Control Wavelength Span of Spectrum Scan in nm.") wavelength_start = Instrument.control( 'STA?', 'STA %g', "Control Wavelength Start of Spectrum Scan in nm.") wavelength_stop = Instrument.control( 'STO?', 'STO %g', "Control Wavelength Stop of Spectrum Scan in nm.") wavelength_marker_value = Instrument.control( 'MKV?', 'MKV %s', "Control Wavelength Marker Value (wavelength or freq.?)", validator=strict_discrete_set, values=["WL", "FREQ"] ) wavelength_value_in = Instrument.control( 'WDP?', 'WDP %s', "Control Wavelength value in Vacuum or Air", validator=strict_discrete_set, values=["VACUUM", "AIR"] ) level_scale = Instrument.measurement( 'LVS?', "Get Current Level Scale", values=["LOG", "LIN"] ) level_log = Instrument.control( "LOG?", "LOG %f", "Control Level Log Scale (/div)", validator=truncated_range, values=[0.1, 10.0] ) level_lin = Instrument.control( "LIN?", "LIN %f", "Control Level Linear Scale (/div)", validator=truncated_range, values=[1e-12, 1] ) level_opt_attn = Instrument.control( "ATT?", "ATT %s", "Control Optical Attenuation Status (ON/OFF)", validator=strict_discrete_set, values=ONOFF ) resolution = Instrument.control( "RES?", "RES %f", "Control 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", "Control Resolution Actual (ON/OFF)", validator=strict_discrete_set, values=ONOFF, map_values=True ) resolution_vbw = Instrument.control( "VBW?", "VBW %s", "Control Video Bandwidth Resolution", validator=strict_discrete_set, values=["1MHz", "100kHz", "10kHz", "1kHz", "100Hz", "10Hz"] ) average_point = Instrument.control( "AVT?", "AVT %d", "Control 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", "Control 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", "Control 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", "Control Peak Search Mode", validator=strict_discrete_set, values=["PEAK", "NEXT", "LAST", "LEFT", "RIGHT"] ) dip_search = Instrument.control( "DPS?", "DPS %s", "Control Dip Search Mode", validator=strict_discrete_set, values=["DIP", "NEXT", "LAST", "LEFT", "RIGHT"] ) analysis = Instrument.control( "ANA?", "ANA %s", "Control Analysis Control" ) analysis_result = Instrument.measurement( "ANAR?", "Get anaysis result from current scan." ) ########################## # Data Memory Commands # ########################## data_memory_a_size = Instrument.measurement( 'DBA?', "Get the number of points sampled in data memory register A." ) data_memory_b_size = Instrument.measurement( 'DBB?', "Get the number of points sampled in data memory register B." ) data_memory_a_condition = Instrument.measurement( "DCA?", """Get the data condition of data memory register A. Starting wavelength, and a sampling point (l1, l2, n).""" ) data_memory_b_condition = Instrument.measurement( "DCB?", """Get the data condition of data memory register B. Starting wavelength, and a sampling point (l1, l2, n).""" ) data_memory_a_values = Instrument.measurement( "DMA?", "Get the binary data from memory register A." ) data_memory_b_values = Instrument.measurement( "DMA?", "Get the binary data from memory register B." ) data_memory_select = Instrument.control( "MSL?", "MSL %s", "Control Memory Data Select.", validator=strict_discrete_set, values=["A", "B"] ) ########################### # Trace Marker Commands # ########################### trace_marker_center = Instrument.setting( "TMC %s", "Set Trace Marker at Center. Set to 1 or True to initiate command", map_values=True, values={True: ''} ) trace_marker = Instrument.control( "TMK?", "TMK %f", "Control the trace marker with a wavelength. Returns the trace wavelength and power.", get_process=_parse_trace_peak ) @property def wavelengths(self): """Get 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 = f"data_memory_{slot.lower()}_condition" data_attr = f"data_memory_{slot.lower()}_values" 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.ask("*OPC?") n_attempts = n while res == '': log.debug(f"Empty OPC Response. {n_attempts} remaining") if n_attempts == 0: break n_attempts -= 1 sleep(delay) res = self.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(f"Wait for sweep [{n}]") # log.debug("ESR2: {}".format(esr2)) sleep(delay) n -= 1 if n <= 0: log.warning(f"Sweep Timeout Occurred ({int(delay * n)} s)") 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/anritsu/anritsuMS9740A.py0000644000175100001770000000653414623331163024243 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.anritsu import AnritsuMS9710C from pymeasure.instruments.validators import ( strict_discrete_set, truncated_discrete_set, truncated_range, ) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class AnritsuMS9740A(AnritsuMS9710C): """Anritsu MS9740A Optical Spectrum Analyzer.""" def __init__(self, adapter, name="Anritsu MS9740A Optical Spectrum Analyzer", **kwargs): """Constructor.""" self.analysis_mode = None super().__init__( adapter, name, **kwargs) #################################### # Spectrum Parameters - Wavelength # #################################### resolution = Instrument.control( "RES?", "RES %s", "Control Resolution (nm)", validator=truncated_discrete_set, values=[0.03, 0.05, 0.07, 0.1, 0.2, 0.5, 1.0], ) resolution_vbw = Instrument.control( "VBW?", "VBW %s", "Control Video Bandwidth Resolution", validator=strict_discrete_set, values=["1MHz", "100kHz", "10kHz", "2kHz", "1kHz", "200Hz", "100Hz", "10Hz"] ) average_sweep = Instrument.control( "AVS?", "AVS %d", """Control number of averages to make on a sweep (1-1000), with 1 being a single (non-averaged) sweep""", validator=truncated_range, values=[1, 1000] ) sampling_points = Instrument.control( "MPT?", "MPT %d", "Control number of sampling points", validator=truncated_discrete_set, values=[51, 101, 251, 501, 1001, 2001, 5001, 10001, 20001, 50001], get_process=lambda v: int(v) ) ########################## # Data Memory Commands # ########################## data_memory_select = Instrument.control( "TTP?", "TTP %s", "Control Memory Data Select.", validator=strict_discrete_set, values=["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] ) def repeat_sweep(self, n=20, delay=0.5): """Perform a single sweep and wait for completion.""" log.debug("Performing a repeat Spectrum Sweep") self.clear() self.write('SRT') self.wait_for_sweep(n=n, delay=delay) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3976054 pymeasure-0.14.0/pymeasure/instruments/attocube/0000755000175100001770000000000014623331176021434 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/attocube/__init__.py0000644000175100001770000000225214623331163023542 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .anc300 import ANC300Controller ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/attocube/anc300.py0000644000175100001770000003714214623331163022775 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 math import inf, isfinite, isinf from warnings import warn from pymeasure.adapters import Adapter from pymeasure.instruments import Instrument, Channel from pymeasure.instruments.validators import (joined_validators, strict_discrete_set, strict_range) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def deprecated_strict_range(value, values): warn("This property is deprecated, use meth:`move_raw` instead.", FutureWarning) return strict_range(value, values) 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(Channel): """ 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", "Get the serial number of the axis.") voltage = Instrument.control( "getv", "setv %.3f", """Control the amplitude of the stepping voltage in volts from 0 to 150 V.""", validator=strict_range, values=[0, 150], check_set_errors=True) frequency = Instrument.control( "getf", "setf %.3f", """Control the frequency of the stepping motion in Hertz from 1 to 10000 Hz.""", validator=strict_range, values=[1, 10000], cast=int, check_set_errors=True) mode = Instrument.control( "getm", "setm %s", """Control 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-'], check_set_errors=True) offset_voltage = Instrument.control( "geta", "seta %.3f", """Control offset voltage in Volts from 0 to 150 V.""", validator=strict_range, values=[0, 150], check_set_errors=True) pattern_up = Instrument.control( "getpu", "setpu %s", """Control step up pattern of the piezo drive. 256 values ranging from 0 to 255 representing 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, check_set_errors=True) pattern_down = Instrument.control( "getpd", "setpd %s", """Control step down pattern of the piezo drive. 256 values ranging from 0 to 255 representing 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, check_set_errors=True) output_voltage = Instrument.measurement( "geto", """Measure the output voltage in volts.""") capacity = Instrument.measurement( "getc", """Measure the saved capacity value in nF of the axis.""") stepu = Instrument.setting( "stepu %d", """Set the steps upwards for N steps. Mode must be 'stp' and N must be positive. 0 causes a continuous movement until stop is called. .. deprecated:: 0.13.0 Use meth:`move_raw` instead. """, validator=deprecated_strict_range, values=[0, inf], check_set_errors=True, ) stepd = Instrument.setting( "stepd %d", """Set the steps downwards for N steps. Mode must be 'stp' and N must be positive. 0 causes a continuous movement until stop is called. .. deprecated:: 0.13.0 Use meth:`move_raw` instead. """, validator=deprecated_strict_range, values=[0, inf], check_set_errors=True, ) def insert_id(self, command): """Insert the channel id in a command replacing `placeholder`. Add axis id to a command string at the correct position after the initial command, but before a potential value. """ cmdparts = command.split() cmdparts.insert(1, self.id) return ' '.join(cmdparts) def stop(self): """ Stop any motion of the axis """ self.write('stop') self.check_set_errors() def move_raw(self, steps): """Move 'steps' steps in the direction given by the sign of the argument. This method assumes the mode of the axis is set to 'stp' and it is non-blocking, i.e. it will return immediately after sending the command. :param steps: integer value of steps to be performed. A positive sign corresponds to upwards steps, a negative sign to downwards steps. The values of +/-inf trigger a continuous movement. The axis can be halted by the stop method. """ if isfinite(steps) and abs(steps) > 0: if steps > 0: self.write(f"stepu {steps:d}") else: self.write(f"stepd {abs(steps):d}") elif isinf(steps): if steps > 0: self.write("stepu c") else: self.write("stepd c") else: # ignore zero and nan values return self.check_set_errors() 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 is blocking and 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 """ if not isfinite(steps): raise ValueError("Only finite number of steps are allowed.") self.mode = 'stp' # perform the movement self.move_raw(steps) # wait for the move to finish self.wait_for(abs(steps) / self.frequency) # ask if movement finished self.ask('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.wait_for(1) # ask if really finished self.ask('capw') return self.capacity class ANC300Controller(Instrument): """ Attocube ANC300 Piezo stage controller with several axes :param adapter: The VISA resource name of the controller (e.g. "TCPIP::
::::SOCKET") or a created Adapter. The instruments default communication port is 7230. :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: default delay between sending and reading in s (default 0.05) :param host: host address of the instrument (e.g. 169.254.0.1) .. deprecated:: 0.11.2 The 'host' argument is deprecated. Use 'adapter' argument instead. :param kwargs: Any valid key-word argument for VISAAdapter """ version = Instrument.measurement( "ver", """ Get the version number and instrument identification. """ ) controllerBoardVersion = Instrument.measurement( "getcser", """ Get the serial number of the controller board. """ ) _reg_value = re.compile(r"\w+\s+=\s+([\w\.]+)") def __init__( self, adapter=None, name="attocube ANC300 Piezo Controller", axisnames="", passwd="", query_delay=0.05, **kwargs, ): adapter = self.handle_deprecated_host_arg(adapter, kwargs) if not isinstance(name, str): warn( f"ANC300Controller.__init__: `name` was provided was {type(name)} but should be a " + "string. This is likely because `name` was added as a keyword argument. " + "All positional arguments after `adapter` should be provided as keyword argument" + " (i.e. `axisnames=['x', 'y']`).", FutureWarning ) self.query_delay = query_delay self.termination_str = "\r\n" super().__init__( adapter, name, includeSCPI=False, read_termination=self.termination_str, write_termination=self.termination_str, **kwargs ) self._axisnames = axisnames for i, axis in enumerate(axisnames): setattr(self, axis, self.add_child(Axis, id=str(i + 1))) self.wait_for() # clear messages sent upon opening the connection, # this contains some non-ascii characters! self.adapter.flush_read_buffer() # send password and check authorization self.write(passwd) self.wait_for() super().read() # ignore echo of password auth_msg = super().read() if auth_msg != 'Authorization success': raise Exception(f"Attocube authorization failed '{auth_msg}'") # switch console echo off self.ask('echo off') def check_set_errors(self): """Check for errors after having set a property and log them. Called if :code:`check_set_errors=True` is set for that property. :return: List of error entries. """ try: self.read() except Exception as exc: log.exception("Setting a property failed.", exc_info=exc) raise else: return [] def ground_all(self): """ Grounds all axis of the controller. """ for attr in self._axisnames: attribute = getattr(self, attr) if isinstance(attribute, Axis): attribute.mode = 'gnd' def stop_all(self): """ Stop all movements of the axis. """ for attr in self._axisnames: attribute = getattr(self, attr) if isinstance(attribute, Axis): attribute.stop() def handle_deprecated_host_arg(self, adapter, kwargs): """ This function formats user input to the __init__ function to be compatible with the current definition of the __init__ function. This is used to support outdated (deprecated) code. and separated out to make it easier to remove in the future. To whoever removes this: This function should be removed and the `adapter` argument in the __init__ method should be made non-optional. :param dict kwargs: keyword arguments passed to the __init__ function, including the deprecated `host` argument. :return str: resource string for the VISAAdapter """ host = kwargs.pop("host", None) if not (host or adapter): raise TypeError("ANC300Controller: missing 'adapter' argument") if not adapter: # because the host argument is deprecated, prompt for the desired # argument which is the adapter argument. warn("The 'host' argument is deprecated. Use 'adapter' instead.", FutureWarning) adapter = host if isinstance(adapter, str): if adapter.find("::") > -1: # adapter is a resource string, so use it return adapter # otherwise, `adapter` can only be a (deprecated) hostname, so display a # deprecation warning and create the resource string warn( "Using a hostname is deprecated. Use a full VISA resource string instead.", FutureWarning, ) return f"TCPIP::{adapter}::7230::SOCKET" elif isinstance(adapter, Adapter): return adapter raise TypeError("ANC300Controller: 'adapter' argument must be a string or Adapter") 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 read(self): """Read after setting a value.""" lines = [] while True: lines.append(super().read()) if lines[-1] in ["OK", "ERROR"]: break msg = self.termination_str.join(lines[:-1]) if lines[-1] != 'OK': self.adapter.flush_read_buffer() raise ValueError("ANC300Controller: Error after previous " f"command with message {msg}") return self._extract_value(msg) def wait_for(self, query_delay=None): """Wait for some time. Used by 'ask' to wait before reading. :param query_delay: Delay between writing and reading in seconds. None means :attr:`query_delay`. """ super().wait_for(self.query_delay if query_delay is None else query_delay) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3976054 pymeasure-0.14.0/pymeasure/instruments/bkprecision/0000755000175100001770000000000014623331176022136 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/bkprecision/__init__.py0000644000175100001770000000226414623331163024247 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .bkprecision9130b import BKPrecision9130B ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/bkprecision/bkprecision9130b.py0000644000175100001770000000555614623331163025506 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, truncated_range CHANNEL_NUMS = [1, 2, 3] class BKPrecision9130B(SCPIUnknownMixin, Instrument): """ Represents the BK Precision 9130B DC Power Supply interface for interacting with the instrument. """ current = Instrument.control( 'MEASure:SCALar:CURRent:DC?', 'SOURce:CURRent:LEVel:IMMediate:AMPLitude %g', """Control the current of the selected channel. (float)""", validator=truncated_range, values=[0, 3] ) source_enabled = Instrument.control( 'SOURce:CHANnel:OUTPut:STATe?', 'SOURce:CHANnel:OUTPut:STATe %d', """Control whether the source is enabled. (bool) """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) channel = Instrument.control( 'INSTrument:SELect?', 'INSTrument:SELect CH%d', f"""Control which channel is selected. Can only take values {CHANNEL_NUMS}. (int)""", validator=strict_discrete_set, values=CHANNEL_NUMS, get_process=lambda x: int(x[2]) ) def __init__(self, adapter, name="BK Precision 9130B Source", **kwargs): super().__init__( adapter, name, **kwargs ) @property def voltage(self): """Control voltage of the selected channel. (float)""" return float(self.ask("MEASure:SCALar:VOLTage:DC?")) @voltage.setter def voltage(self, level): voltage_range = [0, 5] if self.channel == 3 else [0, 30] new_level = truncated_range(level, voltage_range) self.write("SOURce:VOLTage:LEVel:IMMediate:AMPLitude %g" % new_level) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/channel.py0000644000175100001770000001237014623331163021607 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .common_base import CommonBase log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Channel(CommonBase): """The base class for channel definitions. This class supports dynamic properties like :class:`Instrument`, but requires an :class:`Instrument` instance as a parent for communication. :meth:`insert_id` inserts the channel id into the command string sent to the instrument. The default implementation replaces the Channel's `placeholder` (default "ch") with the channel id in all command strings (e.g. "CHANnel{ch}:foo"). :param parent: The instrument (an instance of :class:`~pymeasure.instruments.Instrument`) to which the channel belongs. :param id: Identifier of the channel, as it is used for the communication. """ placeholder = "ch" def __init__(self, parent, id): self.parent = parent self.id = id super().__init__() def insert_id(self, command): """Insert the channel id in a command replacing `placeholder`. Subclass this method if you want to do something else, like always prepending the channel id. """ return command.format_map({self.placeholder: self.id}) # Calls to the instrument def write(self, command, **kwargs): """Write a string command to the instrument appending `write_termination`. :param command: command string to be sent to the instrument. '{ch}' is replaced by the channel id. :param kwargs: Keyword arguments for the adapter. """ self.parent.write(self.insert_id(command), **kwargs) def write_bytes(self, content, **kwargs): """Write the bytes `content` to the instrument.""" self.parent.write_bytes(content, **kwargs) def read(self, **kwargs): """Read up to (excluding) `read_termination` or the whole read buffer.""" return self.parent.read(**kwargs) def read_bytes(self, count, **kwargs): """Read a certain number of bytes from the instrument. :param int count: Number of bytes to read. A value of -1 indicates to read the whole read buffer. :param kwargs: Keyword arguments for the adapter. :returns bytes: Bytes response of the instrument (including termination). """ return self.parent.read_bytes(count, **kwargs) def write_binary_values(self, command, values, *args, **kwargs): """Write binary values to the instrument. :param command: Command to send. :param values: The values to transmit. :param \\*args, \\**kwargs: Further arguments to hand to the Adapter. """ self.parent.write_binary_values(self.insert_id(command), values, *args, **kwargs) def read_binary_values(self, **kwargs): """Read binary values from the instrument.""" return self.parent.read_binary_values(**kwargs) def check_errors(self): """Read all errors from the instrument and log them. :return: List of error entries. """ return self.parent.check_errors() def check_get_errors(self): """Check for errors after having gotten a property and log them. Called if :code:`check_get_errors=True` is set for that property. If you override this method, you may choose to raise an Exception for certain errors. :return: List of error entries. """ return self.parent.check_get_errors() def check_set_errors(self): """Check for errors after having set a property and log them. Called if :code:`check_set_errors=True` is set for that property. If you override this method, you may choose to raise an Exception for certain errors. :return: List of error entries. """ return self.parent.check_set_errors() # Communication functions def wait_for(self, query_delay=None): """Wait for some time. Used by 'ask' to wait before reading. :param query_delay: Delay between writing and reading in seconds. None is default delay. """ self.parent.wait_for(query_delay) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/comedi.py0000644000175100001770000001504614623331163021442 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 time import sleep 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 AREF, CMDF, SUBDEVICE_TYPE, TRIG_SRC, UNIT from pycomedi.constant import _NamedInt from pycomedi.channel import AnalogChannel from pycomedi.utility import inttrig_insn 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: 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 is 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 """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/common_base.py0000644000175100001770000010701514623331163022462 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 inspect import getmembers import logging from warnings import warn log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class DynamicProperty(property): """ Class that allows managing python property behaviour in a "dynamic" fashion The class allows passing, in addition to regular property parameters, a list of runtime configurable parameters. The effect is that the behaviour of fget/fset not only depends on the obj parameter, but also on a set of keyword parameters with a default value. These extra parameters are read from instance, if available, or left with the default value. Dynamic behaviour is achieved by changing class or instance variables with special names defined as ` + + `. Code has been based on Python equivalent implementation of properties provided in the python documentation `here `_. :param fget: class property fget parameter whose signature is expanded with a set of keyword arguments as in fget_params_list :param fset: class property fget parameter whose signature is expanded with a set of keyword arguments as in fset_params_list :param fdel: class property fdel parameter :param doc: class property doc parameter :param fget_params_list: List of parameter names that are dynamically configurable :param fset_params_list: List of parameter names that are dynamically configurable :param prefix: String to be prefixed to get dynamically configurable parameters. """ def __init__(self, fget=None, fset=None, fdel=None, doc=None, fget_params_list=None, fset_params_list=None, prefix=""): super().__init__(fget, fset, fdel, doc) self.fget_params_list = () if fget_params_list is None else fget_params_list self.fset_params_list = () if fset_params_list is None else fset_params_list self.name = "" self.prefix = prefix def __get__(self, obj, objtype=None): if obj is None: # Property return itself when invoked from a class return self if self.fget is None: raise AttributeError(f"Unreadable attribute {self.name}") kwargs = {} for attr in self.fget_params_list: attr_instance_name = self.prefix + "_".join([self.name, attr]) if hasattr(obj, attr_instance_name): kwargs[attr] = getattr(obj, attr_instance_name) return self.fget(obj, **kwargs) def __set__(self, obj, value): if self.fset is None: raise AttributeError(f"Can't set attribute {self.name}") kwargs = {} for attr in self.fset_params_list: attr_instance_name = self.prefix + "_".join([self.name, attr]) if hasattr(obj, attr_instance_name): kwargs[attr] = getattr(obj, attr_instance_name) self.fset(obj, value, **kwargs) def __set_name__(self, owner, name): self.name = name class CommonBase: """Base class for instruments and channels. This class contains everything needed for pymeasure's property creator :meth:`control` and its derivatives :meth:`measurement` and :meth:`setting`. :param preprocess_reply: An optional callable used to preprocess strings received from the instrument. The callable returns the processed string. .. deprecated:: 0.11 Implement it in the instrument's `read` method instead. """ # Variable holding the list of DynamicProperty parameters that are configurable # by users _fget_params_list = ('get_command', 'values', 'map_values', 'get_process', 'command_process', 'check_get_errors') _fset_params_list = ('set_command', 'validator', 'values', 'map_values', 'set_process', 'command_process', 'check_set_errors') # Prefix used to store reserved variables __reserved_prefix = "___" def __init__(self, preprocess_reply=None, **kwargs): self._special_names = self._setup_special_names() self._create_channels() if preprocess_reply is not None: warn(("Parameter `preprocess_reply` is deprecated. " "Implement it in the instrument, e.g. in `read`, instead."), FutureWarning) self.preprocess_reply = preprocess_reply super().__init__(**kwargs) class BaseChannelCreator: """Base class for ChannelCreator and MultiChannelCreator. :param cls: Class for all children or tuple/list of classes, one for each child. :param \\**kwargs: Keyword arguments for all children. """ def __init__(self, cls, **kwargs): try: self.valid_class = issubclass(cls, CommonBase) except TypeError: self.valid_class = False self.pairs = () self.kwargs = kwargs class ChannelCreator(BaseChannelCreator): """Add a single channel to the parent class. The child will be added to the parent instance at instantiation with :func:`CommonBase.add_child`. The attribute name that ChannelCreator was assigned to in the `Instrument` class will be the name of the channel interface. .. code:: class Extreme5000(Instrument): # Two output channels, accessible by their property names # and both are accessible through the 'channels' collection output_A = Instrument.ChannelCreator(Extreme5000Channel, "A") output_B = Instrument.ChannelCreator(Extreme5000Channel, "B") # A channel without a channel accessible through the 'motor' collection motor = Instrument.ChannelCreator(MotorControl) inst = SomeInstrument() # Set the extreme_temp for channel A of Extreme5000 instrument inst.output_A.extreme_temp = 42 :param cls: Channel class for channel interface :param id: The id of the channel on the instrument, integer or string. :param \\**kwargs: Keyword arguments for all children. """ def __init__(self, cls, id=None, **kwargs): super().__init__(cls=cls, **kwargs) if (isinstance(id, (str, int)) or id is None) and self.valid_class: self.pairs = ((cls, id),) else: raise ValueError("Invalid definition of class '{cls}' and id '{id}'.") class MultiChannelCreator(BaseChannelCreator): """Add channels to the parent class. The children will be added to the parent instance at instantiation with :func:`CommonBase.add_child`. The attribute name (e.g. :code:`channels`) will be used as the `collection` of the children. You may define the attribute prefix. If there are no other pressing reasons, use :code:`channels` as the attribute name and leave the prefix at the default :code:`"ch_"`. .. code:: class Extreme5000(Instrument): # Three channels of the same type: 'ch_A', 'ch_B', 'ch_C' # and add them to the 'channels' collection channels = Instrument.MultiChannelCreator(Extreme5000Channel, ["A", "B", "C"]) # Two channel interfaces of different types: 'fn_power', 'fn_voltage' # and add them to the 'functions' collection functions = Instrument.MultiChannelCreator((PowerChannel, VoltageChannel), ["power", "voltage"], prefix="fn_") :param cls: Class for all children or tuple/list of classes, one for each child. :param id: tuple/list of ids of the channels on the instrument. :param prefix: Collection prefix for the attributes, e.g. `"ch_"` creates attribute `self.ch_A`. If prefix evaluates False, the child will be added directly under the variable name. Required if id is tuple/list. :param \\**kwargs: Keyword arguments for all children. """ def __init__(self, cls, id=None, prefix="ch_", **kwargs): super().__init__(cls=cls, **kwargs) if isinstance(id, (list, tuple)) and isinstance(cls, (list, tuple)): assert (len(id) == len(cls)), "Lengths of cls and id do not match." self.pairs = list(zip(cls, id)) elif isinstance(id, (list, tuple)) and self.valid_class: self.pairs = list(zip((cls,) * len(id), id)) else: raise ValueError("Invalid definition of classes '{cls}' and ids '{id}'.") self.kwargs.setdefault("prefix", prefix) def _setup_special_names(self): """ Return list of class/instance special names. Compute the list of special names based on the list of class attributes that are a DynamicProperty. Check also for class variables with special name and copy them at instance level Internal method, not intended to be accessed at user level.""" special_names = [] dynamic_params = tuple(set(self._fget_params_list + self._fset_params_list)) # Check whether class variables of DynamicProperty type are present for attr_name, attr in getmembers(self.__class__): if isinstance(attr, DynamicProperty): special_names += [attr_name + "_" + key for key in dynamic_params] # Check if special variables are defined at class level for attr, value in getmembers(self.__class__): if attr in special_names: # Copy class special variable at instance level, prefixing reserved_prefix setattr(self, self.__reserved_prefix + attr, value) return special_names @staticmethod def get_channels(cls): """Return a list of all the Instrument's ChannelCreator and MultiChannelCreator instances""" class_members = getmembers(cls) channels = [] for name, member in class_members: if isinstance(member, CommonBase.BaseChannelCreator): channels.append((name, member)) return channels @staticmethod def get_channel_pairs(cls): """Return a list of all the Instrument's channel pairs""" channel_pairs = [] for name, creator in CommonBase.get_channels(cls): for pair in creator.pairs: channel_pairs.append(pair) return channel_pairs def _create_channels(self): """Create channel interfaces for all the Instrument's channel pairs.""" for name, creator in CommonBase.get_channels(self.__class__): for cls, id in creator.pairs: # If channel pair was created with MultiChannelCreator # add channel interface to collection with passed attribute name if isinstance(creator, CommonBase.MultiChannelCreator): child = self.add_child(cls, id, collection=name, **creator.kwargs) # If channel pair was created with ChannelCreator # name channel interface with passed attribute name elif isinstance(creator, CommonBase.ChannelCreator): child = self.add_child(cls, id, attr_name=name, **creator.kwargs) else: raise ValueError("Invalid class '{creator}' for channel creation.") child._protected = True def __setattr__(self, name, value): """ Add reserved_prefix in front of special variables.""" if hasattr(self, '_special_names'): if name in self._special_names: name = self.__reserved_prefix + name super().__setattr__(name, value) def __getattribute__(self, name): """ Prevent read access to variables with special names used to support dynamic property behaviour.""" if name in ('_special_names', '__dict__'): return super().__getattribute__(name) if hasattr(self, '_special_names'): if name in self._special_names: raise AttributeError( f"{name} is a reserved variable name and it cannot be read") return super().__getattribute__(name) # Channel management def add_child(self, cls, id=None, collection="channels", prefix="ch_", attr_name="", **kwargs): """Add a child to this instance and return its index in the children list. The newly created child may be accessed either by the id in the children dictionary or by the created attribute, e.g. the fifth channel of `instrument` with id "F" has two access options: :code:`instrument.channels["F"] == instrument.ch_F` .. note:: Do not change the default `collection` or `prefix` parameter, unless you have to distinguish several collections of different children, e.g. different channel types (analog and digital). :param cls: Class of the channel. :param id: Child id how it is used in communication, e.g. `"A"`. :param collection: Name of the collection of children, used for dictionary access to the channel interfaces. :param prefix: For creating multiple channel interfaces, the prefix e.g. `"ch_"` is prepended to the attribute name of the channel interface `self.ch_A`. If prefix evaluates False, the child will be added directly under the collection name. :param attr_name: For creating a single channel interface, the attr_name argument is used when setting the attribute name of the channel interface. :param \\**kwargs: Keyword arguments for the channel creator. :returns: Instance of the created child. """ child = cls(self, id, **kwargs) collection_data = getattr(self, collection, {}) if isinstance(collection_data, CommonBase.BaseChannelCreator): collection_data = {} # Create channel interface if prefix or name is present if (prefix or attr_name) and id is not None: if not collection_data: # Add a grouplist to the parent. setattr(self, collection, collection_data) collection_data[id] = child child._collection = collection if attr_name: setattr(self, attr_name, child) child._name = attr_name else: setattr(self, f"{prefix}{id}", child) child._name = f"{prefix}{id}" elif attr_name and id is None: # If attribute name is passed with no channel id # set the child to the attribute name. setattr(self, attr_name, child) child._name = attr_name else: if collection_data: raise ValueError(f"An attribute '{collection}' already exists.") setattr(self, collection, child) child._name = collection return child def remove_child(self, child): """Remove the child from the instrument and the corresponding collection. :param child: Instance of the child to delete. """ if hasattr(child, "_protected"): raise TypeError("You cannot remove channels defined at class level.") if hasattr(child, "_collection"): collection = getattr(self, child._collection) del collection[child.id] delattr(self, child._name) # Communication functions def wait_for(self, query_delay=None): """Wait for some time. Used by 'ask' to wait before reading. Implement in subclass! :param query_delay: Delay between writing and reading in seconds. None is default delay. """ raise NotImplementedError("Implement in subclass!") def ask(self, command, query_delay=None): """Write a command to the instrument and return the read response. :param command: Command string to be sent to the instrument. :param query_delay: Delay between writing and reading in seconds. :returns: String returned by the device without read_termination. """ self.write(command) self.wait_for(query_delay) return self.read() def values(self, command, separator=',', cast=float, preprocess_reply=None, maxsplit=-1, **kwargs): """Write a command to the instrument and return a list of formatted values from the result. :param command: SCPI command to be sent to the instrument. :param preprocess_reply: Optional callable used to preprocess the string received from the instrument, before splitting it. The callable returns the processed string. :param separator: A separator character to split the string returned by the device into a list. :param maxsplit: The string returned by the device is splitted at most `maxsplit` times. -1 (default) indicates no limit. :param cast: A type to cast each element of the splitted string. :param \\**kwargs: Keyword arguments to be passed to the :meth:`ask` method. :returns: A list of the desired type, or strings where the casting fails. """ results = self.ask(command, **kwargs).strip() if callable(preprocess_reply): results = preprocess_reply(results) elif callable(self.preprocess_reply): results = self.preprocess_reply(results) results = results.split(separator, maxsplit=maxsplit) 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, query_delay=None, **kwargs): """ Write a command to the instrument and return a numpy array of the binary data. :param command: Command to be sent to the instrument. :param query_delay: Delay between writing and reading in seconds. :param kwargs: Arguments for :meth:`~pymeasure.Adapter.read_binary_values`. :returns: NumPy array of values. """ self.write(command) self.wait_for(query_delay) return self.read_binary_values(**kwargs) # Property creators @staticmethod def control( # noqa: C901 accept that this is a complex method get_command, set_command, docs, validator=lambda v, vs: v, values=(), map_values=False, get_process=lambda v: v, set_process=lambda v: v, command_process=None, check_set_errors=False, check_get_errors=False, dynamic=False, preprocess_reply=None, separator=',', maxsplit=-1, cast=float, values_kwargs=None, **kwargs ): """Return a property for the class based on the supplied commands. This property may be set and read from the instrument. See also :meth:`measurement` and :meth:`setting`. :param get_command: A string command that asks for the value, set to `None` if get is not supported (see also :meth:`setting`). :param set_command: A string command that writes the value, set to `None` if set is not supported (see also :meth:`measurement`). :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 command_process: A function that takes a command and allows processing before executing the command .. deprecated:: 0.12 Use a dynamic property instead. :param check_set_errors: Toggles checking errors after setting :param check_get_errors: Toggles checking errors after getting :param dynamic: Specify whether the property parameters are meant to be changed in instances or subclasses. :param preprocess_reply: Optional callable used to preprocess the string received from the instrument, before splitting it. The callable returns the processed string. :param separator: A separator character to split the string returned by the device into a list. :param maxsplit: The string returned by the device is splitted at most `maxsplit` times. -1 (default) indicates no limit. :param cast: A type to cast each element of the splitted string. :param dict values_kwargs: Further keyword arguments for :meth:`values`. :param \\**kwargs: Keyword arguments for :meth:`values`. .. deprecated:: 0.12 Use `values_kwargs` dictionary parameter instead. Example of usage of dynamic parameter is as follows: .. code-block:: python class GenericInstrument(Instrument): center_frequency = Instrument.control( ":SENS:FREQ:CENT?;", ":SENS:FREQ:CENT %e GHz;", " A floating point property that represents the frequency ... ", validator=strict_range, # Redefine this in subclasses to reflect actual instrument value: values=(1, 20), dynamic=True # enable changing property parameters on-the-fly ) class SpecificInstrument(GenericInstrument): # Identical to GenericInstrument, except for frequency range # Override the "values" parameter of the "center_frequency" property center_frequency_values = (1, 10) # Redefined at subclass level instrument = SpecificInstrument() instrument.center_frequency_values = (1, 6e9) # Redefined at instance level .. warning:: Unexpected side effects when using dynamic properties Users must pay attention when using dynamic properties, since definition of class and/or instance attributes matching specific patterns could have unwanted side effect. The attribute name pattern `property_param`, where `property` is the name of the dynamic property (e.g. `center_frequency` in the example) and `param` is any of this method parameters name except `dynamic` and `docs` (e.g. `values` in the example) has to be considered reserved for dynamic property control. """ if values_kwargs is None: values_kwargs = {} if kwargs: warn(f"Do not use keyword arguments {kwargs} as `control` parameter " f"for the `values` method, use `values_kwargs` parameter instead. docs:\n{docs}", FutureWarning) values_kwargs.update(kwargs) if command_process is None: command_process = lambda c: c # noqa: E731 else: warn("Do not use `command_process`, use a dynamic property instead.", FutureWarning) def fget(self, get_command=get_command, values=values, map_values=map_values, get_process=get_process, command_process=command_process, check_get_errors=check_get_errors, ): if get_command is None: raise LookupError("Property can not be read.") vals = self.values(command_process(get_command), separator=separator, cast=cast, preprocess_reply=preprocess_reply, maxsplit=maxsplit, **values_kwargs) if check_get_errors: try: error_list = self.check_get_errors() except Exception as exc: log.error("Exception raised while getting a property with the command " f"""'{command_process(get_command)}': '{str(exc)}'.""") raise errors = [str(error) for error in error_list] if errors: log.error("Error received after trying to get a property with the command " f"""'{command_process(get_command)}': '{"', '".join(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): for k, v in values.items(): if v == value: return k raise KeyError(f"Value {value} not found in mapped values") 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, set_command=set_command, validator=validator, values=values, map_values=map_values, set_process=set_process, command_process=command_process, check_set_errors=check_set_errors, ): if set_command is None: raise LookupError("Property can not be set.") 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 CommonBase.control'.format(type(values)) ) self.write(command_process(set_command) % value) if check_set_errors: try: error_list = self.check_set_errors() except Exception as exc: log.error("Exception raised while setting a property with the command " f"""'{command_process(set_command) % value}': '{str(exc)}'.""") raise errors = [str(error) for error in error_list] if errors: log.error( "Error received after trying to set a property with the command " f"""'{command_process(set_command) % value}': '{"', '".join(errors)}'.""" ) # Add the specified document string to the getter fget.__doc__ = docs if dynamic: fget.__doc__ += "(dynamic)" return DynamicProperty(fget=fget, fset=fset, fget_params_list=CommonBase._fget_params_list, fset_params_list=CommonBase._fset_params_list, prefix=CommonBase.__reserved_prefix) else: return property(fget, fset) @staticmethod def measurement(get_command, docs, values=(), map_values=None, get_process=lambda v: v, command_process=None, check_get_errors=False, dynamic=False, preprocess_reply=None, separator=',', maxsplit=-1, cast=float, values_kwargs=None, **kwargs): """ Return 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 getting .. deprecated:: 0.12 Use a dynamic property instead. :param check_get_errors: Toggles checking errors after getting :param dynamic: Specify whether the property parameters are meant to be changed in instances or subclasses. See :meth:`control` for an usage example. :param preprocess_reply: Optional callable used to preprocess the string received from the instrument, before splitting it. The callable returns the processed string. :param separator: A separator character to split the string returned by the device into a list. :param maxsplit: The string returned by the device is splitted at most `maxsplit` times. -1 (default) indicates no limit. :param cast: A type to cast each element of the splitted string. :param dict values_kwargs: Further keyword arguments for :meth:`values`. :param \\**kwargs: Keyword arguments for :meth:`values`. .. deprecated:: 0.12 Use `values_kwargs` dictionary parameter instead. """ if values_kwargs is None: values_kwargs = {} if kwargs: warn(f"Do not use keyword arguments {kwargs} as `measurement` parameter " f"for the `values` method, use `values_kwargs` parameter instead. docs:\n{docs}", FutureWarning) values_kwargs.update(kwargs) return CommonBase.control(get_command=get_command, set_command=None, docs=docs, values=values, map_values=map_values, get_process=get_process, command_process=command_process, check_get_errors=check_get_errors, dynamic=dynamic, preprocess_reply=preprocess_reply, separator=separator, maxsplit=maxsplit, cast=cast, values_kwargs=values_kwargs, ) @staticmethod def setting(set_command, docs, validator=lambda x, y: x, values=(), map_values=False, set_process=lambda v: v, check_set_errors=False, dynamic=False, ): """Return 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 :param dynamic: Specify whether the property parameters are meant to be changed in instances or subclasses. See :meth:`control` for an usage example. """ return CommonBase.control(get_command=None, set_command=set_command, docs=docs, validator=validator, values=values, map_values=map_values, set_process=set_process, check_set_errors=check_set_errors, dynamic=dynamic, ) def check_errors(self): """Read all errors from the instrument and log them. :return: List of error entries. """ raise NotImplementedError("Implement it in a subclass.") def check_get_errors(self): """Check for errors after having gotten a property and log them. Called if :code:`check_get_errors=True` is set for that property. If you override this method, you may choose to raise an Exception for certain errors. :return: List of error entries. """ raise NotImplementedError("Implement it in a subclass.") def check_set_errors(self): """Check for errors after having set a property and log them. Called if :code:`check_set_errors=True` is set for that property. If you override this method, you may choose to raise an Exception for certain errors. :return: List of error entries. """ raise NotImplementedError("Implement it in a subclass.") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3976054 pymeasure-0.14.0/pymeasure/instruments/danfysik/0000755000175100001770000000000014623331176021436 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/danfysik/__init__.py0000644000175100001770000000225414623331163023546 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .danfysik8500 import Danfysik8500 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/danfysik/danfysik8500.py0000644000175100001770000003105714623331163024137 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.errors import RangeException 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", """Get the idenfitication information. """ ) def __init__(self, adapter, name="Danfysik 8500 Current Supply", **kwargs): super().__init__( adapter, name, includeSCPI=False, write_termination="\r", read_termination="\r", timeout=500, **kwargs ) # TODO verify serial connection. self.write("ERRT") # Use text error messages self.write("UNLOCK") # Unlock from remote or local mode def read(self): """ Read the device and 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 """ result = super().read() 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 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): """Control the polarity of the current supply, being either -1 or 1. This property can be set by supplying 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): """Get 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): """Control 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): """Control the current in parts per million.. """ 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): """Get 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): """Get 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): """Get 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3976054 pymeasure-0.14.0/pymeasure/instruments/deltaelektronika/0000755000175100001770000000000014623331176023150 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/deltaelektronika/__init__.py0000644000175100001770000000224214623331163025255 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/deltaelektronika/sm7045d.py0000644000175100001770000001152014623331163024620 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import strict_range from time import sleep import numpy as np class SM7045D(SCPIUnknownMixin, 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:CU: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, adapter, name="Delta Elektronika SM 70-45 D", **kwargs): super().__init__( adapter, name, **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 np.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() super().shutdown() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3976054 pymeasure-0.14.0/pymeasure/instruments/edwards/0000755000175100001770000000000014623331176021257 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/edwards/__init__.py0000644000175100001770000000223414623331163023365 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .nxds import Nxds ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/edwards/nxds.py0000644000175100001770000000373114623331163022605 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 class Nxds(Instrument): """ Represents the Edwards nXDS (10i) Vacuum Pump and provides a low-level interaction with the instrument. This could potentially work with Edwards pump that has a RS232 interface. This instrument is constructed to only start and stop pump. """ enable = Instrument.setting("!C802 %d", """ Set the pump enabled state with default settings.""", validator=strict_discrete_set, values=(0, 1),) def __init__(self, adapter, name="Edwards NXDS Vacuum Pump", **kwargs): super().__init__( adapter, name, includeSCPI=False, **kwargs ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3976054 pymeasure-0.14.0/pymeasure/instruments/eurotest/0000755000175100001770000000000014623331176021500 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/eurotest/__init__.py0000644000175100001770000000226514623331163023612 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .eurotestHPP120256 import EurotestHPP120256 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/eurotest/eurotestHPP120256.py0000644000175100001770000004236714623331163024764 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 NON INFRINGEMENT. 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 math import re import time from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_range from pymeasure.instruments.validators import strict_discrete_set from enum import IntFlag log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class EurotestHPP120256(Instrument): """ Represents the Euro Test High Voltage DC Source model HPP-120-256 and provides a high-level interface for interacting with the instrument using the Euro Test command set (Not SCPI command set). .. code-block:: python hpp120256 = EurotestHPP120256("GPIB0::20::INSTR") print(hpp120256.id) print(hpp120256.lam_status) print(hpp120256.status) hpp120256.ramp_to_zero(100.0) hpp120256.voltage_ramp = 50.0 # V/s hpp120256.current_limit = 2.0 # mA inst.kill_enabled = True # Enable over-current protection time.sleep(1.0) # Give time to enable kill inst.output_enabled = True time.sleep(1.0) # Give time to output on abs_output_voltage_error = 0.02 # kV hpp120256.wait_for_output_voltage_reached(abs_output_voltage_error, 1.0, 40.0) # Here voltage HV output should be at 0.0 kV print("Setting the output voltage to 1.0kV...") hpp120256.voltage_setpoint = 1.0 # kV # Now HV output should be rising to reach the 1.0kV at 50.0 V/s hpp120256.wait_for_output_voltage_reached(abs_output_voltage_error, 1.0, 40.0) # Here voltage HV output should be at 1.0 kV hpp120256.shutdown() hpp120256.wait_for_output_voltage_reached(abs_output_voltage_error, 1.0, 60.0) # Here voltage HV output should be at 0.0 kV inst.output_enabled = False # Now the HV voltage source is in safe state """ VOLTAGE_RANGE = [0.0, 12.0] # kVolts CURRENT_RANGE = [0.0, 25.0] # mAmps VOLTAGE_RAMP_RANGE = [10, 3000] # V/s COMMAND_DELAY = 0.2 # s response_encoding = "iso-8859-2" regex = re.compile(r'([+-]?([\d]*\.)?[\d]+)') def __init__(self, adapter, name="Euro Test High Voltage DC Source model HPP-120-256", query_delay=0.1, write_delay=0.4, timeout=5000, **kwargs): super().__init__( adapter, name, write_termination="\n", read_termination="", send_end=True, includeSCPI=False, timeout=timeout, **kwargs ) self.write_delay = write_delay self.query_delay = query_delay self.last_write_timestamp = 0.0 # #################################### # # EuroTest-Command set. Non SCPI commands. # #################################### voltage_setpoint = Instrument.control( "STATUS,U", "U,%.3fkV", """Control the voltage set-point in kVolts (float strictly from 0 to 12).""", # getter device response: "U, RANGE=3.000kV, VALUE=2.458kV" validator=strict_range, values=VOLTAGE_RANGE, get_process=lambda r: float(EurotestHPP120256.regex.search(r[2].strip()).groups()[0]) ) current_limit = Instrument.control( "STATUS,I", "I,%.3fmA", """Control the current limit in mAmps (float strictly from 0 to 25).""", # When this property acts as get, the instrument will return a string like this: # "I, RANGE=5000mA, VALUE=1739mA", then current_limit will return 1739.0, # hence the convenience of the get_process. validator=strict_range, values=CURRENT_RANGE, get_process=lambda r: float(EurotestHPP120256.regex.search(r[2].strip()).groups()[0]) ) voltage_ramp = Instrument.control( "STATUS,RAMP", "RAMP,%dV/s", """Control the voltage ramp in Volts/second (int strictly from 10 to 3000).""", # When this property acts as get, the instrument will return a string like this: # "RAMP, RANGE=3000V/s, VALUE=1000V/s", then voltage_ramp will return 1000.0, # hence the convenience of the get_process. validator=strict_range, values=VOLTAGE_RAMP_RANGE, get_process=lambda r: float(EurotestHPP120256.regex.search(r[2].strip()).groups()[0]) ) voltage = Instrument.measurement( "STATUS,MU", """Measure the actual output voltage in kVolts (float).""", # This property is a get so, the instrument will return a string like this: # "U, RANGE=3.000kV, VALUE=2.458kV", then voltage will return 2458.0, # hence the convenience of the get_process. get_process=lambda r: float(EurotestHPP120256.regex.search(r[2].strip()).groups()[0]) ) voltage_range = Instrument.measurement( "STATUS,MU", """Measure the actual output voltage range in kVolts (float).""", # This property is a get so, the instrument will return a string like this: # "U, RANGE=3.000kV, VALUE=2.458kV", then voltage_range will return 3000.0, # hence the convenience of the get_process. get_process=lambda r: float(EurotestHPP120256.regex.search(r[1]).groups()[0]) ) current = Instrument.measurement( "STATUS,MI", """Measure the actual output current in mAmps (float).""", # This property is a get so, the instrument will return a string like this: # "I, RANGE=5000mA, VALUE=1739mA", then current will return a 1739.0, # hence the convenience of the get_process.""" get_process=lambda r: float(EurotestHPP120256.regex.search(r[2].strip()).groups()[0]) ) current_range = Instrument.measurement( "STATUS,MI", """Measure the actual output current range in mAmps (float).""", # This property is a get so, the instrument will return a string like this: # "I, RANGE=5000mA, VALUE=1739mA, then current_range will return a 5000.0, # hence the convenience of the get_process. get_process=lambda r: float(EurotestHPP120256.regex.search(r[1].strip()).groups()[0]) ) kill_enabled = Instrument.control( "STATUS,DI", "KILL,%s", """Control the instrument kill enable (boolean).""", # When Kill is enabled yellow led is flashing and the output # will be shut OFF permanently without ramp if Iout > IOUTmax. validator=strict_discrete_set, values={True: 'ENable', False: 'DISable'}, map_values=True, get_process=lambda r: EurotestHPP120256.EurotestHPP120256Status( int(r[1].strip()[:-1].encode(EurotestHPP120256.response_encoding). decode('utf-8', 'ignore'), 2) ) == EurotestHPP120256.EurotestHPP120256Status.KILL_ENABLE ) output_enabled = Instrument.control( "STATUS,DI", "HV,%s", """Control the instrument output enable (boolean).""", # When output voltage is enabled green led is ON and the # voltage_setting will be present on the output. validator=strict_discrete_set, values={True: 'ON', False: 'OFF'}, map_values=True, get_process=lambda r: EurotestHPP120256.EurotestHPP120256Status( int(r[1].strip()[:-1].encode(EurotestHPP120256.response_encoding). decode('utf-8', 'ignore'), 2) ) == EurotestHPP120256.EurotestHPP120256Status.OUTPUT_ON ) id = Instrument.measurement( "ID", """Get the identification of the instrument (string) """, get_process=lambda r: r[1].strip().encode(EurotestHPP120256.response_encoding).decode('utf-8', 'ignore') ) status = Instrument.measurement( "STATUS,DI", """Get the instrument status (EurotestHPP120256Status).""", # Every bit indicates the state of one subsystem of the HV Source. # response DI, b15 b14 b13 b12 b11 b10 b9 b8 b7 b6 b5 b4 b3 b2 b1 b0, # 0 1 # IpErr b15 no input error input error # Ramp b14 no ramp ramp # CutOut b13 - emergency off # TpErr b12 no trip error trip error # F3 b11 reserved # F2 b10 reserved # menu1 b9 submenu off submenu on # menu0 b8 menu off menu on # err b7 no error error # Creg b6 no current control current control # Vreg b5 no voltage control voltage control # pol b4 negative positive # inh b3 no ext. inhibit external inhibit # local b2 remote local # kilena b1 kill disable kill enable # on b0 off high voltage is ON get_process=lambda r: EurotestHPP120256.EurotestHPP120256Status( int(r[1].strip()[:-1].encode(EurotestHPP120256.response_encoding). decode('utf-8', 'ignore'), 2) ) ) lam_status = Instrument.measurement( "STATUS,LAM", """Get the instrument lam status (string).""", # LAM status is the status of the unit from the point # of view of the process. Fo example, as a response of asking STATUS,LAM, the HV # voltage could response one of the messages from the next list: # LAM,ERROR External Inhibit occurred during Kill enable # LAM,INHIBIT External Inhibit occurred # LAM,TRIP ERROR Software current trip occurred # LAM,INPUT ERROR Wrong command received # LAM,OK Status OK get_process=lambda r: r[1].strip().encode(EurotestHPP120256.response_encoding).decode('utf-8', 'ignore') ) def emergency_off(self): """ The output of the HV source will be switched OFF permanently and the values of the voltage and current settings set to zero""" log.info("Sending emergency off command to the instrument.") self.write("EMCY OFF") def shutdown(self, voltage_rate=200.0): """ Change the output voltage setting (V) to zero and the ramp speed - voltage_rate (V/s) of the output voltage. After calling shutdown, if the HV voltage output > 0 it should drop to zero at a certain rate given by the voltage_rate parameter. :param voltage_rate: indicates the changing rate (V/s) of the voltage output """ log.info(f"Executing the shutdown function with voltage_rate: {voltage_rate} V/s.") self.ramp_to_zero(voltage_rate) super().shutdown() def ramp_to_zero(self, voltage_rate=200.0): """ Sets the voltage output setting to zero and the ramp setting to a value determined by the voltage_rate parameter. In summary, the method conducts (ramps) the voltage output to zero at a determinated voltage changing rate (ramp in V/s). :param voltage_rate: Is the changing rate (ramp in V/s) for the ramp setting """ log.info(f"Executing the ramp_to_zero function with ramp: {voltage_rate} V/s.") self.voltage_ramp = voltage_rate self.voltage_setpoint = 0 def wait_for_output_voltage_reached(self, voltage_setpoint, abs_output_voltage_error=0.03, check_period=1.0, timeout=60.0): """ Wait until HV voltage output reaches the voltage setpoint. Checks the voltage output every check_period seconds and raises an exception if the voltage output doesn't reach the voltage setting until the timeout time. :param voltage_setpoint: the voltage in kVolts setted in the HV power supply which should be present at the output after some time (depends on the ramp setting). :param abs_output_voltage_error: absolute error in kVolts for being considered an output voltage reached. :param check_period: voltage output will be measured every check_period (seconds) time. :param timeout: time (seconds) give to the voltage output to reach the voltage setting. :return: None :raises: Exception if the voltage output can't reach the voltage setting before the timeout completes (seconds). """ log.info("Executing the wait_for_output_voltage_reached function.") ref_time = time.time() future_time = ref_time + timeout log.debug(f"\tWaiting for voltage output set. " f"Reading output voltage every {check_period} seconds.\n" f"\tTimeout: {timeout} seconds.") while True: actual_time = time.time() time.sleep(check_period) # wait for voltage output reaches the voltage output setting voltage_output = self.voltage # check if voltage_output is set. If so then no more wait if math.isclose(voltage_output, voltage_setpoint, rel_tol=0.0, abs_tol=abs_output_voltage_error): break log.debug("voltage_output_valid_range: " "[" + str(voltage_setpoint - abs_output_voltage_error) + ", " + str(voltage_setpoint + abs_output_voltage_error) + "]") log.debug("voltage_output: " + str(voltage_output)) log.debug(f"Elapsed time: {round(actual_time - ref_time, ndigits=1)} seconds.") if actual_time > future_time: self.shutdown() # in case the voltage were applied at the output raise TimeoutError("Timeout for wait_for_output_voltage_reached function") log.info("Waiting for voltage output set done.") # Wrapper functions for the Adapter object def write(self, command, **kwargs): """Overrides Instrument write method for including write_delay time after the parent call. :param command: command string to be sent to the instrument """ actual_write_delay = time.time() - self.last_write_timestamp time.sleep(max(0, self.write_delay - actual_write_delay)) super().write(command, **kwargs) self.last_write_timestamp = time.time() def ask(self, command): """ Overrides Instrument ask method for including query_delay time on parent call. :param command: Command string to be sent to the instrument. :returns: String returned by the device without read_termination. """ return super().ask(command, self.query_delay) class EurotestHPP120256Status(IntFlag): """ Auxiliary class create for translating the instrument 16bits_status_string into an Enum_IntFlag that will help to the user to understand such status. """ # Status response from the instrument has to be interpreted as follows: # # response DI, b15 b14 b13 b12 b11 b10 b9 b8 b7 b6 b5 b4 b3 b2 b1 b0, # bit = 0,1 # IpErr b15 no input error input error # Ramp b14 no ramp ramp # CutOut b13 - emergency off # TpErr b12 no trip error trip error # F3 b11 reserved # F2 b10 reserved # menu1 b9 submenu off submenu on # menu0 b8 menu off menu on # err b7 no error error # Creg b6 no current control current control # Vreg b5 no voltage control voltage control # pol b4 negative positive # inh b3 no ext. inhibit external inhibit # local b2 remote local # kilena b1 kill disable kill enable # on b0 off high voltage is ON # # For example, a status_string = "0100000000000111" will be translated to # EurotestHPP120256_status.RAMP|LOCAL|KILL_ENABLE|OUTPUT_ON INPUT_ERROR = 32768 RAMP = 16384 EMERGENCY_OFF = 8192 TRIP_ERROR = 4096 F3 = 2048 F2 = 1024 SUBMENU_ON = 512 MENU_ON = 256 ERROR = 128 CURRENT_CONTROL = 64 VOLTAGE_CONTROL = 32 POLARIZATION_POSITIVE = 16 EXTERNAL_INHIBIT = 8 LOCAL = 4 KILL_ENABLE = 2 OUTPUT_ON = 1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/fakes.py0000644000175100001770000001510714623331163021271 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 import numpy as np from pymeasure.adapters import FakeAdapter from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set class FakeInstrument(Instrument): """ Provides a fake implementation of the Instrument class for testing purposes. """ def __init__(self, adapter=None, name="Fake Instrument", includeSCPI=False, **kwargs): super().__init__( FakeAdapter(**kwargs), name, 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.findall(fmt_spec_pattern, set_command) if match: # format_specifier = match.group(0) format_specifier = ','.join(match) 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) class SwissArmyFake(FakeInstrument): """Dummy instrument class useful for testing. Like a Swiss Army knife, this class provides multi-tool functionality in the form of streams of multiple types of fake data. Data streams that can currently be generated by this class include 'voltages', sinusoidal 'waveforms', and mono channel 'image data'. """ def __init__(self, name="Mock instrument", wait=.1, **kwargs): super().__init__( name=name, 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.'} # mock image attributes self._w = 1920 self._h = 1080 self._frame_format = "mono_8" @property def time(self): """Control the elapsed time.""" if self._tstart == 0: self._tstart = time.time() self._time = time.time() - self._tstart return self._time @time.setter def time(self, value): if value == 0: self._tstart = 0 else: while self.time < value: time.sleep(0.001) @property def wave(self): """Measure a waveform.""" return float(np.sin(self.time)) @property def voltage(self): """Measure the voltage.""" time.sleep(self._wait) return self._voltage @property def output_voltage(self): """Control the voltage.""" return self._output_voltage @output_voltage.setter def output_voltage(self, value): time.sleep(self._wait) self._output_voltage = value @property def frame_width(self): """Control frame width in pixels.""" time.sleep(self._wait) return self._w @frame_width.setter def frame_width(self, w): time.sleep(self._wait) self._w = w @property def frame_height(self): """Control frame height in pixels.""" time.sleep(self._wait) return self._h @frame_height.setter def frame_height(self, h): time.sleep(self._wait) self._h = h @property def frame_format(self): """Control the format for image data returned from the get_frame() method. Allowed values are: mono_8: single channel 8-bit image. mono_16: single channel 16-bit image. """ time.sleep(self._wait) return self._frame_format @frame_format.setter def frame_format(self, form): allowed_formats = ["mono_8", "mono_16"] strict_discrete_set(form, allowed_formats) self._frame_format = form @property def frame(self): """Get a new image frame.""" im_format_maxval_dict = {"8": 255, "16": 65535} im_format_type_dict = {"8": np.uint8, "16": np.uint16} bit_depth = self.frame_format.split("_")[1] time.sleep(self._wait) return np.array( im_format_maxval_dict[bit_depth] * np.random.rand(self.frame_height, self.frame_width), dtype=im_format_type_dict[bit_depth] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.3976054 pymeasure-0.14.0/pymeasure/instruments/fluke/0000755000175100001770000000000014623331176020734 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/fluke/__init__.py0000644000175100001770000000224614623331163023045 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .fluke7341 import Fluke7341 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/fluke/fluke7341.py0000644000175100001770000000646614623331163022743 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 Fluke7341(Instrument): """ Represents the compact constant temperature bath from Fluke. """ def __init__(self, adapter, name="Fluke 7341", **kwargs): kwargs.setdefault('timeout', 2000) kwargs.setdefault('write_termination', '\r\n') super().__init__( adapter, name, includeSCPI=False, asrl={'baud_rate': 2400}, **kwargs ) def read(self): """Read up to (excluding) `read_termination` or the whole read buffer. Extract the value from the response string. Responses are in the format "`type`: `value` `optional information`". Optional information is for example the unit (degree centigrade or Fahrenheit). """ return super().read().split(":")[-1] set_point = Instrument.control("s", "s=%g", """Control the temperature setpoint (float from -40 to 150 °C) The unit is as defined in property :attr:`~.unit`.""", validator=strict_range, values=(-40, 150), preprocess_reply=lambda x: x.split()[0], ) unit = Instrument.control( "u", "u=%s", """Control the temperature unit: `c` for Celsius and `f` for Fahrenheit`.""", validator=strict_discrete_set, values=('c', 'f'), ) temperature = Instrument.measurement("t", """Measure the current bath temperature. The unit is as defined in property :attr:`unit`.""", preprocess_reply=lambda x: x.split()[0], ) id = Instrument.measurement("*ver", """Get the instrument model.""", cast=str, get_process=lambda x: f"Fluke,{x[0][4:]},NA,{x[1]}", ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4016056 pymeasure-0.14.0/pymeasure/instruments/fwbell/0000755000175100001770000000000014623331176021101 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/fwbell/__init__.py0000644000175100001770000000225014623331163023205 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/fwbell/fwbell5080.py0000644000175100001770000001240114623331163023235 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set from time import sleep import numpy as np class FWBell5080(SCPIMixin, 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 = 1 # 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 """ def __init__(self, adapter, name="F.W. Bell 5080 Handheld Gaussmeter", **kwargs): kwargs.setdefault('timeout', 500) kwargs.setdefault('baudrate', 2400) super().__init__( adapter, name, **kwargs ) field = Instrument.measurement( ":MEASure:FLUX?", """ Measure the field in the appropriate units (float). """, # Remove units get_process=lambda v: float(v.replace('T', '').replace('G', '').replace('Am', '')) ) 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", """ Get the field units (str), 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 ) range = Instrument.control( ":SENS:FLUX:RANG?", ":SENS:FLUX:RANG %d", """ Control the maximum field range in the active units (int). The range unit is dependent on the current units mode (gauss, tesla, amp-meter). Value sets an equivalent range across units that increases in magnitude (1, 10, 100). +--------+--------+---------+-----------+ | Value | gauss | tesla | amp-meter | +--------+--------+---------+-----------+ | 0 | 300 G | 30 mT | 23.88 kAm | +--------+--------+---------+-----------+ | 1 | 3 kG | 300 mT | 238.8 kAm | +--------+--------+---------+-----------+ | 2 | 30 kG | 3 T | 2388 kAm | +--------+--------+---------+-----------+ """, validator=strict_discrete_set, values=[0, 1, 2], cast=int ) def read(self): """ Overwrites the :meth:`Instrument.read ` method to remove semicolons and replace spaces with colons. """ # To set the unit mode to DC Tesla you need to write(':UNIT:FLUX:DC:TESLA') # However the response from ask(':UNIT:FLUX?') is "DC TESLA", with no colon. # We replace space with colon to preserve the mapping in UNITS. # Semicolons may be appended to end of response from FW Bell 5080, and are removed return super().read().replace(' ', ':').replace(';', '') def reset(self): """ Resets the instrument. """ self.clear() 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 np.array(data, dtype=np.float64) def auto_range(self): """ Enables the auto range functionality. """ self.write(":SENS:FLUX:RANG:AUTO") # Instrument needs a delay before next command sleep(2) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/generic_types.py0000644000175100001770000000715614623331163023045 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 warnings import warn from .instrument import Instrument log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class SCPIMixin: """Mixin class for SCPI instruments with the default implementation of base SCPI commands.""" def __init__(self, *args, **kwargs): kwargs.setdefault("includeSCPI", False) # in order not to trigger the deprecation warning super().__init__(*args, **kwargs) # SCPI default properties complete = Instrument.measurement( "*OPC?", """Get the synchronization bit. This property allows synchronization between a controller and a device. The Operation Complete query places an ASCII character 1 into the device's Output Queue when all pending selected device operations have been finished. """, cast=str, ) status = Instrument.measurement( "*STB?", """Get the status byte and Master Summary Status bit.""", cast=str, ) options = Instrument.measurement( "*OPT?", """Get the device options installed.""", cast=str, ) id = Instrument.measurement( "*IDN?", """Get the identification of the instrument.""", cast=str, maxsplit=0, ) next_error = Instrument.measurement( "SYST:ERR?", """Get the next error in the queue. If you want to read and log all errors, use :meth:`check_errors` instead. """, ) # SCPI default methods def clear(self): """Clear the instrument status byte.""" self.write("*CLS") def reset(self): """Reset the instrument.""" self.write("*RST") def check_errors(self): """ Read all errors from the instrument. :return: List of error entries. """ errors = [] while True: err = self.next_error if int(err[0]) != 0: log.error(f"{self.name}: {err[0]}, {err[1]}") errors.append(err) else: break return errors class SCPIUnknownMixin(SCPIMixin): """Mixin which adds SCPI commands to an instrument from which it is not known whether it supports SCPI commands or not. """ def __init__(self, *args, **kwargs): warn("It is not known whether this device support SCPI commands or not. Please inform " "the pymeasure maintainers if you know the answer.", FutureWarning) super().__init__(*args, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4016056 pymeasure-0.14.0/pymeasure/instruments/hcp/0000755000175100001770000000000014623331176020400 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hcp/__init__.py0000644000175100001770000000227114623331163022507 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .tc038d import TC038D from .tc038 import TC038 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hcp/tc038.py0000644000175100001770000001331114623331163021606 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pyvisa.constants import Parity log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def _data_to_temp(data): """Convert the returned hex value "data" to a temperature in °C.""" return int(data[7:11], 16) / 10. # get the hex number, convert to int and shift the decimal sign registers = {'temperature': "D0002", 'setpoint': "D0120", } def _check_errors(response): errors = {"02": "Command does not exist or is not executable.", "03": "Register number does not exist.", "04": "Out of setpoint range.", "05": "Out of data number range.", "06": "Executed monitor without specifying what to monitor.", "08": "Illegal parameter is set.", "42": "Sum does not match the expected value.", "43": "Data value greater than specified received.", "44": "End of data or end of text character is not received.", } if response[5:7] == "OK": return [] else: # got[5:7] == "ER" """If communication is completed abnormally, TC038 returns a character string “ER” and error code (EC1 and EC2)""" EC1 = response[7:9] if EC1 in ("03", "04", "05", "08"): EC2 = response[9:11] return [errors[EC1] + f" Wrong parameter has number {EC2}."] return [errors[EC1]] class TC038(Instrument): """ Communication with the HCP TC038 oven. This is the older version with an AC power supply and AC heater. It has parity or framing errors from time to time. Handle them in your application. The oven always responds with an "OK" to all valid requests or commands. :param str adapter: Name of the COM-Port. :param int address: Address of the device. Should be between 1 and 99. :param int timeout: Timeout in ms. """ def __init__(self, adapter, name="TC038", address=1, timeout=1000, **kwargs): super().__init__( adapter, name, timeout=timeout, write_termination="\r", read_termination="\r", parity=Parity.even, includeSCPI=False, **kwargs, ) self.address = address self.set_monitored_quantity() # start to monitor the temperature def write(self, command): """Send a `command` in its own protocol.""" # 010 is CPU (01) and time to wait (0), which are fix super().write(chr(2) + f"{self.address:02}" + "010" + command + chr(3)) def read(self): """Do error checking on reading.""" # Response is chr(2) + address:02 + "01" + response + chr(3) got = super().read() errors = _check_errors(got) if errors: raise ConnectionError(errors[0]) return got def check_set_errors(self): """Check for errors after having set a property. Called if :code:`check_set_errors=True` is set for that property. """ try: self.read() except ConnectionError as exc: log.exception("Setting a property failed.", exc_info=exc) raise else: return [] def set_monitored_quantity(self, quantity='temperature'): """ Configure the oven to monitor a certain `quantity`. `quantity` may be any key of `registers`. Default is the current temperature in °C. """ # WRS in order to setup to monitor a word # monitor 1 word # monitor the word in register D0002 self.ask(command="WRS" + "01" + registers[quantity]) setpoint = Instrument.control( "WRD" + registers['setpoint'] + ",01", "WWR" + registers['setpoint'] + ",01,%s", """Control the setpoint of the temperature controller in °C.""", get_process=_data_to_temp, set_process=lambda temp: f"{int(round(temp * 10)):04X}", check_set_errors=True, ) temperature = Instrument.measurement( "WRD" + registers['temperature'] + ",01", """Measure the current temperature in °C.""", get_process=_data_to_temp ) monitored_value = Instrument.measurement( "WRM", """Measure the currently monitored value. For default it is the current temperature in °C.""", get_process=_data_to_temp ) information = Instrument.measurement( "INF6", """Get the information about the device and its capabilities.""", get_process=lambda got: got[7:-1], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hcp/tc038d.py0000644000175100001770000001502114623331163021752 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 enum import IntEnum from pymeasure.instruments import Instrument log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def CRC16(data): """Calculate the CRC16 checksum for the data byte array.""" CRC = 0xFFFF for octet in data: CRC ^= octet for j in range(8): lsb = CRC & 0x1 # least significant bit CRC = CRC >> 1 if lsb: CRC ^= 0xA001 return [CRC & 0xFF, CRC >> 8] class Functions(IntEnum): R = 0x03 WRITESINGLE = 0x06 ECHO = 0x08 # register address has to be 0 W = 0x10 # writing multiple variables class TC038D(Instrument): """ Communication with the HCP TC038D oven. This is the newer version with DC heating. The oven expects raw bytes written, no ascii code, and sends raw bytes. For the variables are two or four-byte modes available. We use the four-byte mode addresses. In that case element count has to be double the variables read. """ byteMode = 4 def __init__(self, adapter, name="TC038D", address=1, timeout=1000, **kwargs): """Initialize the device.""" super().__init__(adapter, name, timeout=timeout, includeSCPI=False, **kwargs) self.address = address def write(self, command): """Write a command to the device. :param str command: comma separated string of: - the function: read ('R') or write ('W') or 'echo', - the address to write to (e.g. '0x106' or '262'), - the values (comma separated) to write - or the number of elements to read (defaults to 1). """ function, address, *values = command.split(",") function = Functions[function] data = [self.address] # 1B device address data.append(function) # 1B function code address = int(address, 16) if "x" in address else int(address) data.extend(address.to_bytes(2, "big")) # 2B register address if function == Functions.W: elements = len(values) * self.byteMode // 2 data.extend(elements.to_bytes(2, "big")) # 2B number of elements data.append(elements * 2) # 1B number of bytes to write for element in values: data.extend(int(element).to_bytes(self.byteMode, "big", signed=True)) elif function == Functions.R: count = int(values[0]) * self.byteMode // 2 if values else self.byteMode // 2 data.extend(count.to_bytes(2, "big")) # 2B number of elements to read elif function == Functions.ECHO: data[-2:] = [0, 0] if values: data.extend(int(values[0]).to_bytes(2, "big")) # 2B test data data += CRC16(data) self.write_bytes(bytes(data)) def read(self): """Read response and interpret the number, returning it as a string.""" # Slave address, function got = self.read_bytes(2) if got[1] == Functions.R: # length of data to follow length = self.read_bytes(1) # data length, 2 Byte CRC read = self.read_bytes(length[0] + 2) if read[-2:] != bytes(CRC16(got + length + read[:-2])): raise ConnectionError("Response CRC does not match.") return str(int.from_bytes(read[:-2], byteorder="big", signed=True)) elif got[1] == Functions.W: # start address, number elements, CRC; each 2 Bytes long got += self.read_bytes(2 + 2 + 2) if got[-2:] != bytes(CRC16(got[:-2])): raise ConnectionError("Response CRC does not match.") elif got[1] == Functions.ECHO: # start address 0, data, CRC; each 2B got += self.read_bytes(2 + 2 + 2) if got[-2:] != bytes(CRC16(got[:-2])): raise ConnectionError("Response CRC does not match.") return str(int.from_bytes(got[-4:-2], "big")) else: # an error occurred # got[1] is functioncode + 0x80 end = self.read_bytes(3) # error code and CRC errors = {0x02: "Wrong start address.", 0x03: "Variable data error.", 0x04: "Operation error."} if end[0] in errors.keys(): raise ValueError(errors[end[0]]) else: raise ConnectionError(f"Unknown read error. Received: {got} {end}") def check_set_errors(self): """Check for errors after having set a property. Called if :code:`check_set_errors=True` is set for that property. """ try: self.read() except Exception as exc: log.exception("Setting a property failed.", exc_info=exc) raise else: return [] def ping(self, test_data=0): """Test the connection sending an integer up to 65535, checks the response.""" assert int(self.ask(f"ECHO,0,{test_data}")) == test_data setpoint = Instrument.control( "R,0x106", "W,0x106,%i", """Control the setpoint of the oven in °C.""", check_set_errors=True, get_process=lambda v: v / 10, set_process=lambda v: int(round(v * 10)), ) temperature = Instrument.measurement( "R,0x0", """Measure the current oven temperature in °C.""", get_process=lambda v: v / 10, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4016056 pymeasure-0.14.0/pymeasure/instruments/heidenhain/0000755000175100001770000000000014623331176021722 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/heidenhain/__init__.py0000644000175100001770000000223614623331163024032 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .nd287 import ND287 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/heidenhain/nd287.py0000644000175100001770000001016314623331163023133 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pyvisa.errors import VisaIOError from pymeasure.instruments import Instrument log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ND287(Instrument): """ Represents the Heidenhain ND287 position display unit used to readout and display absolute position measured by Heidenhain encoders. """ status = Instrument.measurement( "\x1BA0800", "Get the encoder's status bar" ) # get_process lambda functions used in the position property position_get_process_map = { "mm": lambda p: float(p.split("\x02")[-1]) * 1e-4, "inch": lambda p: float(p.split("\x02")[-1]) * 1e-5 } position = Instrument.measurement( "\x1BA0200", """Measure the encoder's current position (float). Note that the get_process performs a mapping from the returned value to a float measured in the units specified by :attr:`.ND287.units`. The get_process is modified dynamically as this mapping changes slightly between different units.""", get_process=position_get_process_map["mm"], dynamic=True ) def __init__(self, adapter, name="Heidenhain ND287", units="mm", **kwargs): """ Initialize the nd287 with a carriage return write termination. :param: units: Specify the units that the gauge is working in. Valid values are "inch" and "mm" with "mm" being the default. """ self._units = units super().__init__( adapter, name, includeSCPI=False, write_termination="\r", **kwargs ) @property def id(self): """ Get the string identification property for the device. """ self.write("\x1BA0000") id_str = self.read_bytes(37).decode("utf-8") return id_str @property def units(self): """ Control the unit of measure set on the device. Valid values are 'mm' and 'inch' Note that this parameter can only be set manually on the device. So this argument only ensures that the instance units and physical device settings match. I.e., this property does not change any physical device setting. """ return self._units @units.setter def units(self, unit): if unit in self.position_get_process_map.keys(): self._units = unit self.position_get_process = self.position_get_process_map[unit] def check_errors(self): """ Method to read an error status message and log when an error is detected. :return: String with the error message as its contents. """ self.write("\x1BA0301") try: err_str = self.read_bytes(36).decode("utf-8") except VisaIOError: err_str = None if err_str is not None: log.error("Heidenhain ND287 error message received: %s" % err_str) return err_str ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4016056 pymeasure-0.14.0/pymeasure/instruments/hp/0000755000175100001770000000000014623331176020235 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/__init__.py0000644000175100001770000000310114623331163022335 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from .hp3478A import HP3478A from .hp3437A import HP3437A from .hp8116a import HP8116A from .hp8657b import HP8657B from .hp856Xx import HP8560A from .hp856Xx import HP8561B from .hp11713a import HP11713A from .hp437b import HP437B from .hpsystempsu import HP6632A from .hpsystempsu import HP6633A from .hpsystempsu import HP6634A from .hplegacyinstrument import HPLegacyInstrument ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/hp11713a.py0000644000175100001770000001432414623331163021754 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) Attenuator_11dB = { 0: (False, False, False, False), 1: (True, False, False, False), 2: (False, True, False, False), 3: (True, True, False, False), 4: (False, False, False, True), 5: (True, False, False, True), 6: (False, True, False, True), 7: (True, True, False, True), 8: (False, False, True, True), 9: (True, False, True, True), 10: (False, True, True, True), 11: (True, True, True, True), } """ Mapping of logical values for use with 0 - 11 dB attenuators """ Attenuator_110dB = { 0: (False, False, False, False), 10: (True, False, False, False), 20: (False, True, False, False), 30: (True, True, False, False), 40: (False, False, False, True), 50: (True, False, False, True), 60: (False, True, False, True), 70: (True, True, False, True), 80: (False, False, True, True), 90: (True, False, True, True), 100: (False, True, True, True), 110: (True, True, True, True), } """ Mapping of logical values for use with 0 - 110 dB attenuators """ Attenuator_70dB_3_Section = { 0: (False, False, False, False), 10: (True, False, False, False), 20: (False, True, False, False), 30: (True, True, False, False), 40: (False, False, True, False), 50: (True, False, True, False), 60: (False, True, True, False), 70: (True, True, True, False), } """ Mapping of logical values for use with 0 - 70 dB attenuators with 3 switching sections """ Attenuator_70dB_4_Section = { 0: (False, False, False, False), 10: (True, False, False, False), 20: (False, False, False, True), 30: (True, False, False, True), 40: (False, True, False, True), 50: (True, True, False, True), 60: (False, True, True, True), 70: (True, True, True, True), } """ Mapping of logical values for use with 0 - 70 dB attenuators with 4 switching sections """ class SwitchDriverChannel(Channel): enabled = Instrument.setting( "%s{ch}", """ Set this channel to the polarity 'A' for True and 'B' for False. """, map_values=True, values={True: "A", False: "B"} ) class HP11713A(Instrument): """ Represents the HP 11713A Switch and Attenuator Driver and provides a high-level interface for interacting with the instrument. Usually an attenuator is hooked to either X or Y or X and Y. To ease the control of the attenuator driver you have the possibility to set an attenuator type via the attribute 'ATTENUATOR_X' or 'ATTENUATOR_Y'. The hp11713a keeps different default attenuator mappings. After setting the attenuator type you are able to use the methods 'attenuation_x' and/or 'attenuation_y' to set the switch driver to the correct value for the specified attenuation. The attenuation values are rounded. .. code-block:: python from pymeasure.instruments.hp import HP11713A from pymeasure.instruments.hp.hp11713a import Attenuator_110dB sd = HP11713A("GPIB::1") sd.ATTENUATOR_Y = Attenuator_110dB sd.attenuation_y(10) sd.ch_0.enabled = True """ ATTENUATOR_X = {} ATTENUATOR_Y = {} channels = Instrument.MultiChannelCreator(SwitchDriverChannel, list(range(0, 9))) def __init__(self, adapter, name="Hewlett-Packard HP11713A", **kwargs): super().__init__( adapter, name, includeSCPI=False, send_end=True, **kwargs, ) def attenuation_x(self, attenuation): """ Set switches according to the attenuation in dB for X The set attenuation will be rounded to the next available step. An attenuation mapping has to be set in before e.g. .. code-block:: python from pymeasure.instruments.hp.hp11713a import HP11713A, Attenuator_110dB instr.ATTENUATOR_X = Attenuator_110dB instr.attenuation_x(10) """ rounding = 0 if list(self.ATTENUATOR_X.keys())[1] == 10: rounding = -1 self.ch_1.enabled, self.ch_2.enabled, self.ch_3.enabled, self.ch_4.enabled = \ self.ATTENUATOR_X[int(round(attenuation, rounding))] def attenuation_y(self, attenuation): """ Set switches according to the attenuation in dB for Y The set attenuation will be rounded to the next available step. An attenuation mapping has to be set in before e.g. .. code-block:: python from pymeasure.instruments.hp.hp11713a import HP11713A, Attenuator_110dB instr.ATTENUATOR_Y = Attenuator_110dB instr.attenuation_y(10) """ rounding = 0 if list(self.ATTENUATOR_Y.keys())[1] == 10: rounding = -1 self.ch_5.enabled, self.ch_6.enabled, self.ch_7.enabled, self.ch_8.enabled = \ self.ATTENUATOR_Y[int(round(attenuation, rounding))] def deactivate_all(self): """ Deactivate all switches to polarity 'B'. """ self.write("B1234567890") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/hp33120A.py0000644000175100001770000001454414623331163021714 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class HP33120A(SCPIUnknownMixin, Instrument): """ Represents the Hewlett Packard 33120A Arbitrary Waveform Generator and provides a high-level interface for interacting with the instrument. """ def __init__(self, adapter, name="Hewlett Packard 33120A Function Generator", **kwargs): super().__init__( adapter, name, **kwargs ) self.amplitude_units = 'Vpp' 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", """Control the shape of the wave, which can take the values: sinusoid, square, triangle, ramp, noise, dc, and user. (str)""", validator=strict_discrete_set, values=SHAPES, map_values=True ) frequency = Instrument.control( "SOUR:FREQ?", "SOUR:FREQ %g", """Control 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`. (float)""" ) max_frequency = Instrument.measurement( "SOUR:FREQ? MAX", """ Get the maximum :attr:`~.HP33120A.frequency` in Hz for the given shape """ ) min_frequency = Instrument.measurement( "SOUR:FREQ? MIN", """ Get the minimum :attr:`~.HP33120A.frequency` in Hz for the given shape """ ) amplitude = Instrument.control( "SOUR:VOLT?", "SOUR:VOLT %g", """ Control 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`. (float)""" ) max_amplitude = Instrument.measurement( "SOUR:VOLT? MAX", """ Get the maximum :attr:`~.amplitude` in Volts for the given shape """ ) min_amplitude = Instrument.measurement( "SOUR:VOLT? MIN", """ Get the minimum :attr:`~.amplitude` in Volts for the given shape """ ) offset = Instrument.control( "SOUR:VOLT:OFFS?", "SOUR:VOLT:OFFS %g", """ Control 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", """ Get the maximum :attr:`~.offset` in Volts for the given shape """ ) min_offset = Instrument.measurement( "SOUR:VOLT:OFFS? MIN", """ Get 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", """ Control the units of the amplitude, which can take the values Vpp, Vrms, dBm, and default. (str) """, validator=strict_discrete_set, values=AMPLITUDE_UNITS, map_values=True ) burst_enabled = Instrument.control( "BM:STATE?", "BM:STATE %d", """Control state of burst modulation""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) burst_source = Instrument.control( "BM:SOURCE?", "BM:SOURCE %s", """Control internal or external gate source for burst modulation""", validator=strict_discrete_set, values=['INT', 'EXT'], ) burst_count = Instrument.control( "BM:NCYC?", "BM:NCYC %d", """Control the number of cycles per burst (1 to 50,000 cycles)""", ) min_burst_count = Instrument.measurement( "BM:NCYC? MIN", """Get the minimum :attr:`~.HP33120A.burst_count`""" ) max_burst_count = Instrument.measurement( "BM:NCYC? MAX", """Get the maximum :attr:`~.HP33120A.burst_count`""" ) burst_rate = Instrument.control( "BM:INT:RATE?", "BM:INT:RATE %g", """Control the burst rate in Hz fo an internal burst source""" ) min_burst_rate = Instrument.measurement( "BM:INT:RATE? MIN", """Get the minimum :attr:`~.HP33120A.burst_rate`""" ) max_burst_rate = Instrument.measurement( "BM:INT:RATE? MAX", """Get the maximum :attr:`~.HP33120A.burst_rate`""" ) burst_phase = Instrument.control( "BM:PHAS?", "BM:PHAS %g", """Control the starting phase angle of a burst (-360 to +360 degrees)""" ) min_burst_phase = Instrument.measurement( "BM:PHAS? MIN", """Get the minimum :attr:`~.HP33120A.burst_phase`""" ) max_burst_phase = Instrument.measurement( "BM:PHAS? MAX", """Get the maximum :attr:`~.HP33120A.burst_phase`""" ) def beep(self): """ Causes a system beep. """ self.write("SYST:BEEP") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/hp3437A.py0000644000175100001770000002544614623331163021647 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ctypes import logging import math from enum import IntFlag import numpy as np from pymeasure.instruments.hp.hplegacyinstrument import HPLegacyInstrument, StatusBitsBase from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) c_uint8 = ctypes.c_uint8 c_uint16 = ctypes.c_uint16 c_uint32 = ctypes.c_uint32 class Status(StatusBitsBase): """ A bitfield structure containing the assignments for the status decoding """ _pack_ = 1 _fields_ = [ # Byte 0: Function, Range and Number of Digits ("Format", c_uint8, 1), # Bit 7 ("SRQ", c_uint8, 3), # bit 4..6 ("Trigger", c_uint8, 2), # bit 2..3 ("Range", c_uint8, 2), # bit 0..1 # Byte 1 & 2: ("Number", c_uint16, 16), # Byte 1: # ("NRDGS_MSD", c_uint8, 4), # ("NRDGS_2SD", c_uint8, 4), # Byte 2: # ("NRDGS_3SD", c_uint8, 4), # ("NRDGS_LSD", c_uint8, 4), ("not_used", c_uint8, 4), ("Delay", c_uint32, 28), # Byte 3: # ("Not_Used", c_uint8, 4), # ("Delay_MSD", c_uint8, 4), # Byte 4: # ("Delay_2SD", c_uint8, 4), # ("Delay_3SD", c_uint8, 4), # Byte 5: # ("Delay_4SD", c_uint8, 4), # ("Delay_5SD", c_uint8, 4), # Byte 6: # ("Delay_6SD", c_uint8, 4), # ("Delay_LSD", c_uint8, 4), ] @staticmethod def _decode_range(r): """Method to decode current range :param range_undecoded: int to be decoded :return cur_range: float value representing the active measurement range :rtype cur_range: float """ # range decoding # (cf table 3-2, page 3-5 of the manual, HPAK document 9018-05946) decode_map = { 0: math.nan, 1: 0.1, 2: 10.0, 3: 1.0, } return decode_map[r] @staticmethod def _decode_trigger(t): """Method to decode trigger mode :param status_bytes: list of bytes to be decoded :return trigger_mode: string with the current trigger mode :rtype trigger_mode: str """ decode_map = { 0: "INVALID", 1: "internal", 2: "external", 3: "hold/manual" } return decode_map[t] _get_process_ = { "Number": StatusBitsBase._convert_from_bcd, "Delay": StatusBitsBase._convert_from_bcd, "Range": _decode_range, "Trigger": _decode_trigger, } def __str__(self): """ Returns a pretty formatted string showing the status of the instrument """ ret_str = "" for field in self._fields_: ret_str = ret_str + f"{field[0]}: {getattr(self, field[0])}\n" return ret_str class PackedBits(ctypes.BigEndianStructure): """ A bitfield structure containing the assignments for the data transfer in packed/binary mode """ _pack_ = 1 _fields_ = [ ("range", c_uint8, 2), # bit 0..1 ("sign_bit", c_uint8, 1), ("MSD", c_uint8, 1), ("SSD", c_uint8, 4), ("TSD", c_uint8, 4), ("LSD", c_uint8, 4), ] def __float__(self): """ Return a float value from the packed data of the HP3437A """ # range decoding # (cf table 3-2, page 3-5 of the manual, HPAK document 9018-05946) decode_map = { 1: 0.1, 2: 10.0, 3: 1.0, } cur_range = decode_map[self.range] signbit = 1 if self.sign_bit == 0: signbit = -1 return ( cur_range * signbit * ( self.MSD + self.SSD / 10 + self.TSD / 100 + self.LSD / 1000 ) ) class HP3437A(HPLegacyInstrument): """Represents the Hewlett Packard 3737A system voltmeter and provides a high-level interface for interacting with the instrument. """ status_desc = Status pb_desc = PackedBits def __init__(self, adapter, name="Hewlett-Packard HP3437A", **kwargs): super().__init__( adapter, name, **kwargs, ) # Definitions for different specifics of this instrument RANGE = { 1e-1: "R1", 1: "R2", 10: "R3", } TRIGGERS = { "internal": "T1", "external": "T2", "hold": "T3", "manual": "T3", } class SRQ(IntFlag): """Enum element for SRQ mask bit decoding""" DATA_READY = 4 IGNORE_TRIGGER = 2 INVALID_PROGRAM = 1 def _unpack_data(self, data): """ Method to unpack the data from the returned bytes in packed mode :param data: list of bytes to be decoded :return ret_data: float value """ ret_data = PackedBits.from_buffer(bytearray(data)) return float(ret_data) # commands overwriting the base implementation def read_data(self): """ Reads measured data from instrument, returns a np.array. (This function also takes care of unpacking the data if required) :return data: np.array containing the data """ # Adjusting the timeout to match the number of counts and the delay current_timeout = self.adapter.connection.timeout time_needed = self.number_readings * self.delay new_timeout = min(1e6, time_needed * 3 * 1000) # safety factor 3 if new_timeout > current_timeout: if new_timeout >= 1e6: # Disables timeout if measurement would take more then 1000 sec log.info("HP3437A: timeout deactivated") self.adapter.connection.timeout = new_timeout log.info("HP3437A: timeout changed to %g", new_timeout) read_data = self.read_bytes(-1) # check if data is in packed format format if self.talk_ascii: return_value = np.array(read_data[:-2].decode("ASCII").split(","), dtype=float) else: processed_data = [] for i in range(0, len(read_data), 2): processed_data.append(self._unpack_data(read_data[i : i + 2])) # noqa: E203 return_value = np.array(processed_data) self.adapter.connection.timeout = current_timeout return return_value # commands/properties for instrument control def check_errors(self): """ As this instrument does not have a error indication bit, this function always returns an empty list. """ return [] @property def talk_ascii(self): """ A boolean property, True if the instrument is set to ASCII-based communication. This property can be set. """ return bool(self.status.Format) @talk_ascii.setter def talk_ascii(self, value): if value: self.write("F1") else: self.write("F2") @property def delay(self): """Return the value (float) for the delay between two measurements, this property can be set, valid range: 100ns - 0.999999s """ return self.status.Delay * 1e-7 @delay.setter def delay(self, value): delay_str = ( "D." + format(strict_range(value, [0, 0.9999999]) * 10e6, "07.0f") + "S" ) self.write(delay_str) @property def number_readings(self): """Return value (int) for the number of consecutive measurements, this property can be set, valid range: 0 - 9999 """ return self.status.Number @number_readings.setter def number_readings(self, value): number_str = "N" + str(strict_range(value, [0, 9999])) + "S" self.write(number_str) @property def range(self): """Return the current measurement voltage range. This property can be set, valid values: 0.1, 1, 10 (V). .. Note:: This instrument does not have autorange capability. Overrange will be in indicated as 0.99,9.99 or 99.9 """ return self.status.Range @range.setter def range(self, value): range_str = "R" + format( round(math.log10(strict_discrete_set(value, [0.1, 1, 10])) + 2), "d" ) self.write(range_str) @property def SRQ_mask(self): """Return current SRQ mask, this property can be set, bit assignment for SRQ: ========= ========================== Bit (dec) Description ========= ========================== 1 SRQ when invalid program 2 SRQ when trigger is ignored 4 SRQ when data ready ========= ========================== """ mask = self.status.SRQ return self.SRQ(mask) @SRQ_mask.setter def SRQ_mask(self, value): mask_str = "E" + format(strict_range(value, [0, 7]), "o") + "S" self.write(mask_str) @property def trigger(self): """Return current selected trigger mode, this property can be set, Possible values are: =========== =========================================== Value Explanation =========== =========================================== internal automatic trigger (internal) external external trigger (connector on back or GET) hold/manual holds the measurement/issues a manual trigger =========== =========================================== """ return self.status.Trigger @trigger.setter def trigger(self, value): trig_set = self.TRIGGERS[strict_discrete_set(value, self.TRIGGERS)] self.write(trig_set) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/hp34401A.py0000644000175100001770000003221414623331163021711 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 warnings import warn from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set deprecated_text = """ .. deprecated:: 0.12 Use the :code:`function_` and :code:`reading` properties instead. """ def _deprecation_warning(property_name): def func(x): warn(f'Deprecated property name "{property_name}", use the "function_" ' 'and "reading" properties instead.', FutureWarning) return x return func class HP34401A(SCPIUnknownMixin, Instrument): """ Represents the HP / Agilent / Keysight 34401A Multimeter and provides a high-level interface for interacting with the instrument. .. code-block:: python dmm = HP34401A("GPIB::1") dmm.function_ = "DCV" print(dmm.reading) # -> Single float reading dmm.nplc = 0.02 dmm.autozero_enabled = False dmm.trigger_count = 100 dmm.trigger_delay = "MIN" print(dmm.reading) # -> Array of 100 very fast readings """ FUNCTIONS = {"DCV": "VOLT", "DCV_RATIO": "VOLT:RAT", "ACV": "VOLT:AC", "DCI": "CURR", "ACI": "CURR:AC", "R2W": "RES", "R4W": "FRES", "FREQ": "FREQ", "PERIOD": "PER", "CONTINUITY": "CONT", "DIODE": "DIOD"} FUNCTIONS_WITH_RANGE = { "DCV": "VOLT", "ACV": "VOLT:AC", "DCI": "CURR", "ACI": "CURR:AC", "R2W": "RES", "R4W": "FRES", "FREQ": "FREQ", "PERIOD": "PER"} BOOL_MAPPINGS = {True: 1, False: 0} # Below: stop_bits: 20 comes from # https://pyvisa.readthedocs.io/en/latest/api/constants.html#pyvisa.constants.StopBits def __init__(self, adapter, name="HP 34401A", **kwargs): super().__init__( adapter, name, asrl={'baud_rate': 9600, 'data_bits': 8, 'parity': 0, 'stop_bits': 20}, **kwargs ) # Log a deprecated warning for the old function property voltage_ac = Instrument.measurement("MEAS:VOLT:AC? DEF,DEF", "AC voltage, in Volts" + deprecated_text, get_process=_deprecation_warning('voltage_ac')) current_dc = Instrument.measurement("MEAS:CURR:DC? DEF,DEF", "DC current, in Amps" + deprecated_text, get_process=_deprecation_warning('current_dc')) current_ac = Instrument.measurement("MEAS:CURR:AC? DEF,DEF", "AC current, in Amps" + deprecated_text, get_process=_deprecation_warning('current_ac')) resistance = Instrument.measurement("MEAS:RES? DEF,DEF", "Resistance, in Ohms" + deprecated_text, get_process=_deprecation_warning('resistance')) resistance_4w = Instrument.measurement( "MEAS:FRES? DEF,DEF", "Four-wires (remote sensing) resistance, in Ohms" + deprecated_text, get_process=_deprecation_warning('resistance_4w')) function_ = Instrument.control( "FUNC?", "FUNC \"%s\"", """Control the measurement function. Allowed values: "DCV", "DCV_RATIO", "ACV", "DCI", "ACI", "R2W", "R4W", "FREQ", "PERIOD", "CONTINUITY", "DIODE".""", validator=strict_discrete_set, values=FUNCTIONS, map_values=True, get_process=lambda v: v.strip('"'), ) range_ = Instrument.control( "{function_prefix_for_range}:RANG?", "{function_prefix_for_range}:RANG %s", """Control the range for the currently active function. For frequency and period measurements, ranging applies to the signal's input voltage, not its frequency""", ) autorange = Instrument.control( "{function_prefix_for_range}:RANG:AUTO?", "{function_prefix_for_range}:RANG:AUTO %d", """Control the autorange state for the currently active function.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) resolution = Instrument.control( "{function}:RES?", "{function}:RES %g", """Control the resolution of the measurements. Not valid for frequency, period, or ratio. Specify the resolution in the same units as the measurement function, not in number of digits. Results in a "Settings Conflict" error if autorange is enabled. MIN selects the smallest value accepted, which gives the most resolution. MAX selects the largest value accepted which gives the least resolution.""", ) nplc = Instrument.control( "{function}:NPLC?", "{function}:NPLC %s", """Control the integration time in number of power line cycles (NPLC). Valid values: 0.02, 0.2, 1, 10, 100, "MIN", "MAX". This command is valid only for dc volts, ratio, dc current, 2-wire ohms, and 4-wire ohms.""", validator=strict_discrete_set, values=[0.02, 0.2, 1, 10, 100, "MIN", "MAX"], ) gate_time = Instrument.control( "{function}:APER?", "{function}:APER %s", """Control the gate time (or aperture time) for frequency or period measurements. Valid values: 0.01, 0.1, 1, "MIN", "MAX". Specifically: 10 ms (4.5 digits), 100 ms (default; 5.5 digits), or 1 second (6.5 digits).""", validator=strict_discrete_set, values=[0.01, 0.1, 1, "MIN", "MAX"], ) detector_bandwidth = Instrument.control( "DET:BAND?", "DET:BAND %s", """Control the lowest frequency expected in the input signal in Hertz. Valid values: 3, 20, 200, "MIN", "MAX".""", validator=strict_discrete_set, values=[3, 20, 200, "MIN", "MAX"], ) autozero_enabled = Instrument.control( "ZERO:AUTO?", "ZERO:AUTO %s", """Control the autozero state.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) def trigger_single_autozero(self): """Trigger an autozero measurement. Consequent autozero measurements are disabled.""" self.write("ZERO:AUTO ONCE") auto_input_impedance_enabled = Instrument.control( "INP:IMP:AUTO?", "INP:IMP:AUTO %s", """Control if automatic input resistance mode is enabled. Only valid for dc voltage measurements. When disabled (default), the input resistance is fixed at 10 MOhms for all ranges. With AUTO ON, the input resistance is set to >10 GOhms for the 100 mV, 1 V, and 10 V ranges.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) terminals_used = Instrument.measurement( "ROUT:TERM?", """Query the multimeter to determine if the front or rear input terminals are selected. Returns "FRONT" or "REAR".""", values={"FRONT": "FRON", "REAR": "REAR"}, map_values=True, ) # Trigger related commands def init_trigger(self): """Set the state of the triggering system to "wait-for-trigger". Measurements will begin when the specified trigger conditions are satisfied after this command is received.""" self.write("INIT"), reading = Instrument.measurement( "READ?", """Take a measurement of the currently selected function. Reading this property is equivalent to calling `init_trigger()`, waiting for completion and fetching the reading(s).""", ) trigger_source = Instrument.control( "TRIG:SOUR?", "TRIG:SOUR %s", """Control the trigger source. Valid values: "IMM", "BUS", "EXT" The multimeter will accept a software (bus) trigger, an immediate internal trigger (this is the default source), or a hardware trigger from the rear-panel Ext Trig (external trigger) terminal.""", validator=strict_discrete_set, values=["IMM", "BUS", "EXT"], ) trigger_delay = Instrument.control( "TRIG:DEL?", "TRIG:DEL %s", """Control the trigger delay in seconds. Valid values (incl. floats): 0 to 3600 seconds, "MIN", "MAX".""", ) trigger_auto_delay_enabled = Instrument.control( "TRIG:DEL:AUTO?", "TRIG:DEL:AUTO %s", """Control the automatic trigger delay state. If enabled, the delay is determined by function, range, integration time, and ac filter setting. Selecting a specific trigger delay value automatically turns off the automatic trigger delay.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) sample_count = Instrument.control( "SAMP:COUN?", "SAMP:COUN %s", """Controls the number of samples per trigger event. Valid values: 1 to 50000, "MIN", "MAX".""", ) trigger_count = Instrument.control( "TRIG:COUN?", "TRIG:COUN %s", """Control the number of triggers accepted before returning to the "idle" state. Valid values: 1 to 50000, "MIN", "MAX", "INF". The INFinite parameter instructs the multimeter to continuously accept triggers (you must send a device clear to return to the "idle" state).""", ) stored_reading = Instrument.measurement( "FETC?", """Measure the reading(s) currently stored in the multimeter's internal memory. Reading this property will NOT initialize a trigger. If you need that, use the `reading` property instead.""", ) # Display related commands display_enabled = Instrument.control( "DISP?", "DISP %s", """Control the display state.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) displayed_text = Instrument.control( "DISP:TEXT?", "DISP:TEXT \"%s\"", """Control the text displayed on the multimeter's display. The text can be up to 12 characters long; any additional characters are truncated my the multimeter.""", get_process=lambda x: x.strip('"'), ) # System related commands remote_control_enabled = Instrument.control( "SYST: ", "SYST:%s", """Control whether remote control is enabled.""", validator=strict_discrete_set, values={True: "REM", False: "LOC"}, map_values=True, ) remote_lock_enabled = Instrument.control( "SYST: ", "SYST:%s", """Control whether the beeper is enabled.""", validator=strict_discrete_set, values={True: "RWL", False: "LOC"}, map_values=True, ) def beep(self): """This command causes the multimeter to beep once.""" self.write("SYST:BEEP") beeper_enabled = Instrument.control( "SYST:BEEP:STAT?", "SYST:BEEP:STAT %s", """Control whether the beeper is enabled.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) scpi_version = Instrument.measurement( "SYST:VERS?", """The SCPI version of the multimeter.""", ) stored_readings_count = Instrument.measurement( "DATA:POIN?", """The number of readings currently stored in the internal memory.""", ) self_test_result = Instrument.measurement( "*TST?", """Initiate a self-test of the multimeter and return the result. Be sure to set an appropriate connection timeout, otherwise the command will fail.""", ) def write(self, command): """Write a command to the instrument.""" if "{function_prefix_for_range}" in command: command = command.replace("{function_prefix_for_range}", self._get_function_prefix_for_range()) elif "{function}" in command: command = command.replace("{function}", HP34401A.FUNCTIONS[self.function_]) super().write(command) def _get_function_prefix_for_range(self): function_prefix = HP34401A.FUNCTIONS_WITH_RANGE[self.function_] if function_prefix in ["FREQ", "PER"]: function_prefix += ":VOLT" return function_prefix ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/hp3478A.py0000644000175100001770000004361614623331163021653 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ctypes import logging import math from enum import IntFlag from pymeasure.instruments.hp.hplegacyinstrument import HPLegacyInstrument, StatusBitsBase from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) c_uint8 = ctypes.c_uint8 class SRQ(ctypes.BigEndianStructure): """Support class for the SRQ handling """ _fields_ = [ ("power_on", c_uint8, 1), ("not_assigned_1", c_uint8, 1), ("calibration", c_uint8, 1), ("front_panel_button", c_uint8, 1), ("internal_error", c_uint8, 1), ("syntax_error", c_uint8, 1), ("not_assigned_2", c_uint8, 1), ("data_ready", c_uint8, 1), ] def __str__(self): """ Returns a pretty formatted string showing the status of the instrument """ ret_str = "" for field in self._fields_: ret_str = ret_str + f"{field[0]}: {hex(getattr(self, field[0]))}\n" return ret_str class Status(StatusBitsBase): """ Support-Class with the bit assignments for the 5 status byte of the HP3478A """ _fields_ = [ # Byte 1: Function, Range and Number of Digits ("function", c_uint8, 3), # bit 5..7 ("range", c_uint8, 3), # bit 2..4 ("digits", c_uint8, 2), # bit 0..1 # Byte 2: Status Bits ("res1", c_uint8, 1), ("ext_trig", c_uint8, 1), ("cal_enable", c_uint8, 1), ("front_rear", c_uint8, 1), ("fifty_hz", c_uint8, 1), ("auto_zero", c_uint8, 1), ("auto_range", c_uint8, 1), ("int_trig", c_uint8, 1), # Byte 3: Serial Poll Mask (SRQ) # ("SRQ_PON", c_uint8, 1), # ("res3", c_uint8, 1), # ("SRQ_cal_error", c_uint8, 1), # ("SRQ_front_panel", c_uint8, 1), # ("SRQ_internal_error", c_uint8, 1), # ("SRQ_syntax_error", c_uint8, 1), # ("res2", c_uint8, 1), # ("SRQ_data_rdy", c_uint8, 1), ("SRQ", SRQ), # Byte 4: Error Information # ("res5", c_uint8, 1), # ("res4", c_uint8, 1), # ("ERR_AD_Link", c_uint8, 1), # ("ERR_AD", c_uint8, 1), # ("ERR_slope", c_uint8, 1), # ("ERR_ROM", c_uint8, 1), # ("ERR_RAM", c_uint8, 1), # ("ERR_cal", c_uint8, 1), ("Error_Status", c_uint8, 8), # Byte 5: DAC Value ("DAC_value", c_uint8, 8), ] class HP3478A(HPLegacyInstrument): """ Represents the Hewlett Packard 3478A 5 1/2 digit multimeter and provides a high-level interface for interacting with the instrument. """ status_desc = Status def __init__(self, adapter, name="Hewlett-Packard HP3478A", **kwargs): kwargs.setdefault('read_termination', '\r\n') kwargs.setdefault('send_end', True) super().__init__( adapter, name, **kwargs, ) # Definitions for different specifics of this instrument MODES = {"DCV": "F1", "ACV": "F2", "R2W": "F3", "R4W": "F4", "DCI": "F5", "ACI": "F6", "Rext": "F7", } INV_MODES = {v: k for k, v in MODES.items()} RANGES = {"DCV": {3E-2: "R-2", 3E-1: "R-1", 3: "R0", 30: "R1", 300: "R2", "auto": "RA"}, "ACV": {3E-1: "R-1", 3: "R0", 30: "R1", 300: "R2", "auto": "RA"}, "R2W": {30: "R1", 300: "R2", 3E3: "R3", 3E4: "R4", 3E5: "R5", 3E6: "R6", 3E7: "R7", "auto": "RA"}, "R4W": {30: "R1", 300: "R2", 3E3: "R3", 3E4: "R4", 3E5: "R5", 3E6: "R6", 3E7: "R7", "auto": "RA"}, "DCI": {3E-1: "R-1", 3: "R0", "auto": "RA"}, "ACI": {3E-1: "R-1", 3: "R0", "auto": "RA"}, "Rext": {3E7: "R7", "auto": "RA"}, } TRIGGERS = { "auto": "T1", "internal": "T1", "external": "T2", "single": "T3", "hold": "T4", "fast": "T5", } class ERRORS(IntFlag): """Enum element for errror bit decoding """ AD_LINK = 32 # AD link error AD_SELFCHK = 16 # AD self check error AD_SLOPE = 8 # AD slope error ROM = 4 # Control ROM error RAM = 2 # RAM selftest failed CALIBRATION = 1 # Calibration checksum error or cal range issue NO_ERR = 0 # Should be obvious # commands/properties for instrument control @property def active_connectors(self): """Return selected connectors ("front"/"back"), based on front-panel selector switch """ selection = self.status.front_rear if selection == 1: return "front" else: return "back" @property def auto_range_enabled(self): """ Property describing the auto-ranging status ====== ============================================ Value Status ====== ============================================ True auto-range function activated False manual range selection / auto-range disabled ====== ============================================ The range can be set with the :py:attr:`range` property """ selection = self.status.auto_range return bool(selection) @property def auto_zero_enabled(self): """ Return auto-zero status, this property can be set ====== ================== Value Status ====== ================== True auto-zero active False auto-zero disabled ====== ================== """ selection = self.status.auto_zero return bool(selection) @auto_zero_enabled.setter def auto_zero_enabled(self, value): az_set = int(value) az_str = "Z" + str(int(strict_discrete_set(az_set, [0, 1]))) self.write(az_str) @property def calibration_enabled(self): """Return calibration enable switch setting, based on front-panel selector switch ====== =================== Value Status ====== =================== True calbration possible False calibration locked ====== =================== """ selection = self.status.cal_enable return bool(selection) def check_errors(self): """ Method to read the error status register :return error_status: one byte with the error status register content :rtype error_status: int """ # Read the error status register only one time for this method, as # the manual states that reading the error status register also clears it. current_errors = self.error_status if current_errors != 0: log.error("HP3478A error detected: %s", self.ERRORS(current_errors)) return self.ERRORS(current_errors) error_status = HPLegacyInstrument.measurement( "E", """Checks the error status register """, cast=int, ) def display_reset(self): """ Reset the display of the instrument. """ self.write("D1") display_text = HPLegacyInstrument.setting( "D2%s", """Displays up to 12 upper-case ASCII characters on the display. """, set_process=(lambda x: str.upper(x[0:12])), ) display_text_no_symbol = HPLegacyInstrument.setting( "D3%s", """Displays up to 12 upper-case ASCII characters on the display and disables all symbols on the display. """, set_process=(lambda x: str.upper(x[0:12])), ) measure_ACI = HPLegacyInstrument.measurement( MODES["ACI"], """ Returns the measured value for AC current as a float in A. """, ) measure_ACV = HPLegacyInstrument.measurement( MODES["ACV"], """ Returns the measured value for AC Voltage as a float in V. """, ) measure_DCI = HPLegacyInstrument.measurement( MODES["DCI"], """ Returns the measured value for DC current as a float in A. """, ) measure_DCV = HPLegacyInstrument.measurement( MODES["DCV"], """ Returns the measured value for DC Voltage as a float in V. """, ) measure_R2W = HPLegacyInstrument.measurement( MODES["R2W"], """ Returns the measured value for 2-wire resistance as a float in Ohm. """, ) measure_R4W = HPLegacyInstrument.measurement( MODES["R4W"], """ Returns the measured value for 4-wire resistance as a float in Ohm. """, ) measure_Rext = HPLegacyInstrument.measurement( MODES["Rext"], """ Returns the measured value for extended resistance mode (>30M, 2-wire) resistance as a float in Ohm. """, ) @property def mode(self): """Return current selected measurement mode, this propery can be set. Allowed values are ==== ============================================================== Mode Function ==== ============================================================== ACI AC current ACV AC voltage DCI DC current DCV DC voltage R2W 2-wire resistance R4W 4-wire resistance Rext extended resistance method (requires additional 10 M resistor) ==== ============================================================== """ current_mode = self.INV_MODES["F" + str(self.status.function)] return current_mode @mode.setter def mode(self, value): mode_set = self.MODES[strict_discrete_set(value, self.MODES)] self.write(mode_set) @property def range(self): """Returns the current measurement range, this property can be set. Valid values are : ==== ======================================= Mode Range ==== ======================================= ACI 0.3, 3, auto ACV 0.3, 3, 30, 300, auto DCI 0.3, 3, auto DCV 0.03, 0.3, 3, 30, 300, auto R2W 30, 300, 3000, 3E4, 3E5, 3E6, 3E7, auto R4W 30, 300, 3000, 3E4, 3E5, 3E6, 3E7, auto Rext 3E7, auto ==== ======================================= """ cur_mode = self.INV_MODES["F" + str(self.status.function)] if cur_mode == "DCV": correction_factor = 3 elif cur_mode in ["ACV", "ACI", "DCI"]: correction_factor = 2 else: correction_factor = 0 current_range = 3 * math.pow(10, self.status.range - correction_factor) return current_range @range.setter def range(self, value): cur_mode = self.mode value = strict_discrete_set(value, self.RANGES[cur_mode]) set_range = self.RANGES[cur_mode][value] self.write(set_range) @property def resolution(self): """Returns current selected resolution, this property can be set. Possible values are 3,4 or 5 (for 3 1/2, 4 1/2 or 5 1/2 digits of resolution) """ number_of_digit = 6 - self.status.digits return number_of_digit @resolution.setter def resolution(self, value): resolution_string = "N" + str(strict_discrete_set(value, [3, 4, 5])) self.write(resolution_string) @property def SRQ_mask(self): """Return current SRQ mask, this property can be set, bit assigment for SRQ: ========= ========================== Bit (dec) Description ========= ========================== 1 SRQ when Data ready 4 SRQ when Syntax error 8 SRQ when internal error 16 front panel SQR button 32 SRQ by invalid calibration ========= ========================== """ return self.status.SRQ @SRQ_mask.setter def SRQ_mask(self, value): self.write(f"M{strict_range(value, [0, 63]):02o}") @property def trigger(self): """Return current selected trigger mode, this property can be set Possibe values are: ======== =========================================== Value Meaning ======== =========================================== auto automatic trigger (internal) internal automatic trigger (internal) external external trigger (connector on back or GET) hold holds the measurement fast fast trigger for AC measurements ======== =========================================== """ status = self.status i_trig = status.int_trig e_trig = status.ext_trig if i_trig == 0: if e_trig == 0: trigger_mode = "hold" else: trigger_mode = "external" else: trigger_mode = "internal" return trigger_mode @trigger.setter def trigger(self, value): trig_set = self.TRIGGERS[strict_discrete_set(value, self.TRIGGERS)] self.write(trig_set) @property def calibration_data(self): """Read or write the calibration data as an array of 256 values between 0 and 15. The calibration data of an HP 3478A is stored in a 256x4 SRAM that is permanently powered by a 3v Lithium battery. When the battery runs out, the calibration data is lost, and recalibration is required. When read, this property fetches and returns the calibration data so that it can be backed up. When assigned a value, it similarly expects an array of 256 values between 0 and 15, and writes the values back to the instrument. When writing, exceptions are raised for the following conditions: * The CAL ENABLE switch at the front of the instrument is not set to ON. * The array with values does not contain exactly 256 elements. * The array with values does not pass a verification check. IMPORTANT: changing the calibration data results in permanent loss of the previous data. Use with care! """ cal_data = [] for addr in range(0, 256): # To fetch one nibble: 'W
', where address is a raw 8-bit number. cmd = bytes([ord('W'), addr]) self.write_bytes(cmd) rvalue = self.read_bytes(1)[0] # 'W' command reads a nibble from the SRAM, but then adds a value of 64 to return # it as an ASCII value. if rvalue < 64 or rvalue >= 80: raise Exception("calibration nibble out of range") cal_data.append(rvalue-64) return cal_data @calibration_data.setter def calibration_data(self, cal_data): """Setter to write the calibration data. """ if not self.calibration_enabled: raise Exception("CAL ENABLE switch not set to ON") self.write_calibration_data(cal_data, True) def write_calibration_data(self, cal_data, verify_calibration_data=True): """Method to write calibration data. The cal_data parameter format is the same as the ``calibration_data`` property. Verification of the cal_data array can be bypassed by setting ``verify_calibration_data`` to ``False``. """ if verify_calibration_data and not self.verify_calibration_data(cal_data): raise ValueError("cal_data verification fail.") for addr in range(0, 256): # To write one nibble: 'X
', where address and byte are raw 8-bit numbers. cmd = bytes([ord('X'), addr, cal_data[addr]]) self.write_bytes(cmd) pass def verify_calibration_entry(self, cal_data, entry_nr): """Verify the checksum of one calibration entry. Expects an array of 256 values with calibration data, and an entry number from 0 to 18. Returns True when the checksum of the specified calibration entry is correct. """ if len(cal_data) != 256: raise Exception("cal_data must contain 256 values") sum = 0 for idx in range(0, 13): val = cal_data[entry_nr*13 + idx + 1] if idx != 11: sum += val else: sum += val*16 return sum == 255 def verify_calibration_data(self, cal_data): """Verify the checksums of all calibration entries. Expects an array of 256 values with calibration data. :return calibration_correct: True when all checksums are correct. :rtype calibration_correct: boolean """ for entry_nr in range(0, 19): if entry_nr in [5, 16, 18]: continue if not self.verify_calibration_entry(cal_data, entry_nr): return False return True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/hp437b.py0000644000175100001770000007437214623331163021627 0ustar00runnerdockerfrom pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, strict_range from enum import IntEnum, IntFlag import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class MeasurementUnit(IntEnum): """Enumeration to represent the measurement unit the power meter will measure in""" WATTS = 0 DBM = 1 PERCENT = 2 DB = 3 class SensorType(IntEnum): """Enumeration to represent the selected sensor type for the power meter""" #: Default (100% for all frequencies) DEFAULT = 0 HP_8481A = 1 #: HP 8482A, 8482B, 8482H HP_8482X = 2 HP_8483A = 3 HP_8481D = 4 HP_8485A = 5 HP_R8486A = 6 HP_Q8486A = 7 HP_R8486D = 8 HP_8487A = 9 class OperatingMode(IntEnum): """Enumeration to represent the operating mode the power meter is currently in""" NORMAL = 0 ZEROING = 6 CALIBRATION = 8 class TriggerMode(IntEnum): """Enumeration to represent the trigger mode the power meter is currently in""" HOLD = 0 FREE_RUNNING = 3 class GroupTriggerMode(IntEnum): """Enumeration to represent the group execute trigger mode the power meter is currently in""" IGNORE = 0 TRIGGER_IMMEDIATE = 1 TRIGGER_DELAY = 2 class EventStatusRegister(IntFlag): """Enumeration to represent the Event Status Register.""" #: The bit is set when the power meter's LINE switch is set from STDBY to ON POWER_ON = 128 #: This bit is set when an incorrect HP-IB code is sent to the power meter. For example, # the command “QX” is a command error. COMMAND_ERROR = 32 #: This bit is set when incorrect data is sent to the power meter. For example, the command # “FR-3GZ” is an execution error. EXECUTION_ERROR = 16 #: This bit is set true whenever a measurement error (error 1-49) occurs. DEVICE_DEPENDENT_ERROR = 8 Errors = { 1: "Power meter cannot zero the sensor", 5: "Power meter cannot calibrate sensor", 11: "Input overload on sensor", 15: "Sensor’s zero reference has drifted negative", 17: "Input power on sensor is too high for current range", 21: "Power reading over high limit", 23: "Power reading under low limit", 31: "No sensor connected to the input", 33: "Both front and rear sensor inputs, have sensors connected (Option 002 or Option 003 only)", 50: "Entered cal factor is out of range", 51: "Entered offset is out of range", 52: "Entered range number is out of range", 54: "Entered recall register number is out of range", 55: "Entered storage register number is out of range", 56: "Entered reference cal factor is out of range", 57: "RAM ID check failure", 61: "Stack RAM failure", 62: "ROM checksum failure", 64: "RAM failure", 65: "Analog I/O PIA Failure", 66: "Keyboard and Display PIA Failure", 67: "Analog-to-Digital converter Failure", 68: "HP-IB failure", 69: "Timer failure", 70: "Keyboard/Display controller failure", 71: "Keyboard data failure", 72: "Data line to A3U26 is open.", 73: "Keyboard/Display controller self-test failure", 74: "Display not responding", 75: "Digital failure", } class StatusMessage: MeasurementErrorCode = (0, 1) EntryErrorCode = (2, 2) OperatingMode = (4, 2) AutomaticRangeStatus = (6, 1) Range = (7, 1) # 8, 9 unused AutoFilterStatus = (10, 1) Filter = (11, 1) # 12, 13 unused LinearLogStatus = (14, 1) # A PowerRefStatus = (16, 1) RelativeModeStatus = (17, 1) TriggerMode = (18, 1) GroupTriggerMode = (19, 1) LimitsCheckingStatus = (20, 1) LimitsStatus = (21, 1) # 22 unused OffsetStatus = (23, 1) DutyCycleStatus = (24, 1) MeasurementUnits = (25, 1) def _getstatus(status_type, modifier=lambda v: v): start_index, stop_offset = status_type return lambda v: modifier(int(v[start_index:start_index + stop_offset])) class HP437B(Instrument): """Represents the HP437B Power Meters. .. note:: Most command descriptions are taken from the document: 'Operating Manual 437B Power Meter' """ def __init__(self, adapter, name="Hewlett-Packard HP437B", **kwargs): super().__init__( adapter, name, includeSCPI=False, send_end=True, **kwargs, ) def check_errors(self): errors = [] while True: err = self.values("ERR?") # exclude upper limit and lower limit hit from real errors if int(err[0]) != 0 and int(err[0]) != 21 and int(err[0]) != 23: log.error(f"{self.name}: {err[0]}, {Errors[err[0]]}") errors.append(err) else: break return errors event_status = Instrument.measurement( "*ESR?", """ Get the status byte and Master Summary Status bit. .. code-block:: python print(instr.request_service_conditions) StatusRegister.PowerOn|CommandError """, cast=int, get_process=lambda v: EventStatusRegister(v) ) def activate_auto_range(self): """ The power meter divides each sensor’s power range into 5 ranges of 10 dB each. Range 1 is the most sensitive (lowest power levels), and Range 5 is the least sensitive (highest power levels). Range 5 can be less than 10 dB if the sensor’s power range is less than 50 dB. The range can be set either automatically or manually. 'activate_auto_range' automatically selects the correct range for the current measurement. """ self.write("RA") def calibrate(self, calibration_factor): """ Calibrate a sensor to the power meter with a 'calibration_factor' in percent. """ self.write("CL%.1fPCT" % calibration_factor) @property def calibration_factor(self): """ Control the calibration factor of a specific power sensor at a specific input frequency. (A chart or table of CAL FACTOR % versus Frequency is printed on each sensor and an accompanying data sheet.) Calibration factor is entered in percent. Valid entries for 'calibration_factor' range from 1.0 to 150.0%. """ self.write("KB") # returns CALFAC 097.9% display_content = self.display_output assert display_content[0:6] == "CALFAC" self.write("EX") return float(display_content[7:12]) @calibration_factor.setter def calibration_factor(self, calibration_factor): values = [1.0, 150.0] strict_range(float(calibration_factor), values) self.write("KB%3.1fPCT" % float(calibration_factor)) self.check_errors() display_enabled = Instrument.setting( "%s", """ Set the display of the power meter active or inactive. """, map_values=True, values={True: "DE", False: "DD"} ) display_all_segments_enabled = Instrument.setting( "%s", """ Set all segments of the display of the power meter active or resume normal state. """, map_values=True, values={True: "DA", False: "DE"} ) display_user_message = Instrument.setting( "DU %s", """ Set a custom user message up to 12 alpha-numerical chars. If the string is empty or None the user message gets disabled. """, validator=lambda x, y: x if str(x).isalnum() and len(str(x)) <= 12 else str(x)[0:12], set_process=lambda v: str(v).upper().ljust(12) ) display_output = Instrument.measurement( "OD", """ Get the current displayed string of values of the power meter. .. code-block:: python print(instr.display_output) -0.23 dB REL """, cast=str ) duty_cycle_enabled = Instrument.control( "SM", "DC%d", """ Control whether the duty cycle is active or inactive. See :attr:`duty_cycle` """, map_values=True, values={True: 1, False: 0}, cast=int, get_process=_getstatus(StatusMessage.DutyCycleStatus), check_set_errors=True ) @property def duty_cycle(self): """ Control the duty cycle for calculation of a pulsed input signal. This function will cause the power meter to report the pulse power of a rectangular pulsed input signal. The allowable range of values for 'duty_cycle' is 0.00001 to 0.99999. Pulse power, as reported by the power meter, is a mathematical representation of the pulse power rather than an actual measurement. The power meter measures the average power of the pulsed input signal and then divides the measurement by the duty cycle value to obtain a pulse power reading. """ self.write("DY") # returns DTYCY 01.000% display_content = self.display_output assert display_content[0:5] == "DTYCY" self.write("EX") return float(display_content[6:12]) / 100.0 @duty_cycle.setter def duty_cycle(self, duty_cycle): values = [0.00001, 0.99999] strict_range(float(duty_cycle), values) self.write("DY%02.3fPCT" % (float(duty_cycle) * 100.0)) self.check_errors() filter_automatic_enabled = Instrument.control( "SM", "%s", """ Control the filter mode. By switching over from automatic to manual (true to false) the instrument implicitly keeps (holds) the filter value from the automatic selection. """, cast=bool, get_process=_getstatus(StatusMessage.AutoFilterStatus), set_process=lambda v: "FA" if v else "FH", check_set_errors=True ) filter = Instrument.control( "SM", "FM%dEN", """ Control the filter number for averaging. Setting a value implicitly enables the manual filter mode. Setting a value of 1 basically disables the averaging. """, values=[1, 2, 4, 8, 16, 32, 64, 128, 256, 512], validator=strict_discrete_set, get_process=_getstatus(StatusMessage.Filter, (lambda x: 2 ** x)), check_set_errors=True ) @property def frequency(self): """ Control the frequency of the input signal. Entering a frequency causes the power meter to select a sensor-specific calibration factor. The allowed range of 'frequency' values is from 0.0001 to 999.9999 GHz with a 100 kHz resolution. The unit is Hz. """ self.write("FR") # returns FR 000.0500GZ display_content = self.display_output assert display_content[0:2] == "FR" self.write("EX") return_value = float(display_content[3:11]) if display_content[11:13] == "GZ": return_value *= 1e9 else: return_value *= 1e6 return return_value @frequency.setter def frequency(self, frequency): self.write("FR%08.4fGZ" % (float(frequency) / 1e9)) self.check_errors() limits_enabled = Instrument.control( "SM", "LM%d", """ Control the limits checking function to allow the power meter to monitor the power level at the sensor and to indicate when that power is outside preset limits. """, map_values=True, values={True: 1, False: 0}, cast=int, get_process=_getstatus(StatusMessage.LimitsCheckingStatus), check_set_errors=True ) @property def limit_high(self): """ Control the upper limit for the builtin limit checking. """ self.write("LH") # returns HI +299.999dB display_content = self.display_output assert display_content[0:2] == "HI" self.write("EX") return float(display_content[3:11]) @limit_high.setter def limit_high(self, limit): """ Control the upper limit for the builtin limit checking. """ values = [-299.999, 299.999] strict_range(limit, values) self.write("LH%7.3fEN" % limit) self.check_errors() @property def limit_low(self): """ Control the lower limit for the builtin limit checking. """ self.write("LL") # returns HI +299.999dB display_content = self.display_output assert display_content[0:2] == "LO" self.write("EX") return float(display_content[3:11]) @limit_low.setter def limit_low(self, limit): """ Control the lower limit for the builtin limit checking. """ values = [-299.999, 299.999] strict_range(limit, values) self.write("LL%7.3fEN" % limit) self.check_errors() limit_high_hit = Instrument.measurement( "SM", """ Get if the upper limit check got triggered. """, map_values=True, values={True: 1, False: 0}, cast=int, get_process=_getstatus(StatusMessage.LimitsStatus), ) limit_low_hit = Instrument.measurement( "SM", """ Get if the lower limit check got triggered. """, map_values=True, values={True: 2, False: 0}, cast=int, get_process=_getstatus(StatusMessage.LimitsStatus), ) # just addressing the instrument to talk (without a query string) and read until EOI results # in only reading the RF power level power = Instrument.measurement( "", """ Measure the power at the power sensor attached to the power meter in the corresponding unit. In case a measurement would be invalid the power meter responds with the value float('nan'). """, get_process=lambda v: float("nan") if v == 9.0200e+40 else v ) power_reference_enabled = Instrument.control( "SM", "OC%d", """ Control the builtin reference power source 1mW @ 50 MHz. """, map_values=True, values={True: 1, False: 0}, cast=int, get_process=_getstatus(StatusMessage.PowerRefStatus), check_set_errors=True ) offset_enabled = Instrument.control( "SM", "OF%d", """ Control the offset being applied. """, map_values=True, values={True: 1, False: 0}, cast=int, get_process=_getstatus(StatusMessage.OffsetStatus), check_set_errors=True ) @property def offset(self): """ Control the offset applied to the measured value to compensate for signal gain or loss (for example, to compensate for the loss of a 10 dB directional coupler). Offsets are entered in dB. In case the :attr:`offset_enabled` is false this returns automatically 0.0 """ if self.offset_enabled: self.write("OS") # returns OFS +00.00 dB display_content = self.display_output assert display_content[0:3] == "OFS" self.write("EX") return float(display_content[4:10]) else: return 0.0 @offset.setter def offset(self, offset): values = [-99.99, 99.99] strict_range(offset, values) self.write("OS%5.2fEN" % offset) def reset(self): self.write("*RST") def clear_status_registers(self): self.write("*CLS") def preset(self): """ Sets the power meter to a known state. Preset conditions are shown in the following table. .. list-table:: Preset values :widths: 25 25 :header-rows: 1 * - Parameter - Value/Condition * - Frequency - 50 MHz * - Resolution - 0.01 dB * - Duty Cylce - 1.000%, Off * - Relative - 0 dB, Off * - Power Reference - Off * - Range - Auto * - Unit - dBm * - Low Limit - -90.000 dBm * - High Limit - +90.000 dBm * - Limit Checking - Off * - Trigger Mode - Free Run * - Group Trigger Mode - Trigger with Delay * - Display Function - Display Enable """ self.write("PR") relative_mode_enabled = Instrument.control( "SM", "RL%d", """ Control the relative mode. In the relative mode the current measured power value will be used as reference and any further reported value from :attr:`power` will refer to this. """, map_values=True, values={True: 1, False: 0}, cast=int, get_process=_getstatus(StatusMessage.RelativeModeStatus), check_set_errors=True ) measurement_unit = Instrument.measurement( "SM", """ Get the measurement unit the power meter is currently reporting the power values in. Depends on: :attr:`relative_mode_enabled` and attr:`linear_display_enabled` .. code-block:: python instr.relative_mode_enabled = False instr.linear_display_enabled = True print(instr.measurement_unit) MeasurementUnit.Watts """, values=[e for e in MeasurementUnit], cast=int, get_process=_getstatus(StatusMessage.MeasurementUnits, lambda v: MeasurementUnit(v)), ) linear_display_enabled = Instrument.control( "SM", "%s", """ Control if the power meter displays or reports the power values in logarithmic or linear units. Set `linear_display_enabled` to 'True' to activate linear value readout. .. code-block:: python from pymeasure.instruments.hp.hp437b import LogLin instr.relative_mode_enabled = False instr.linear_display_enabled = True """, validator=strict_discrete_set, values={True: "LN", False: "LG"}, cast=bool, map_values=True, get_process=_getstatus(StatusMessage.LinearLogStatus, lambda v: {0: "LN", 1: "LG"}[v]) ) @property def resolution(self): """ Control the resolution of the power meter's measured value. Three levels of resolution can be set: 0.1 dB, 0.01 dB and 0.001 dB or if the selected unit is Watts 1%, 0.1% and 0.001%. """ linear_display_enabled = self.linear_display_enabled mapping = {} if not linear_display_enabled: mapping = {1: 0.1, 2: 0.01, 3: 0.001} else: mapping = {1: 1, 2: 0.1, 3: 0.01} self.write("RE") self.check_errors() display_content = self.display_output self.check_errors() self.write("EX") assert display_content[0:3] == "RES" return mapping[int(display_content[3])] @resolution.setter def resolution(self, resolution): """ Control the resolution of the power meter's measured value. Three levels of resolution can be set: 0.1 dB, 0.01 dB and 0.001 dB or if the selected unit is Watts 1%, 0.1% and 0.001%. """ linear_display_enabled = self.linear_display_enabled allowed_values = {} if not linear_display_enabled: allowed_values = {0.1: 1, 0.01: 2, 0.001: 3} else: allowed_values = {1: 1, 0.1: 2, 0.01: 3} strict_discrete_set(resolution, allowed_values.keys()) self.write(f"RE{allowed_values[resolution]}EN") self.check_errors() sensor_type = Instrument.setting( "SE%dEN", """ Set the sensor type connected to the power meter to select the corresponding calibration factor. .. code-block:: python from pymeasure.instruments.hp.hp437b import SensorType instr.sensor_type = SensorType.HP_8481A """, validator=strict_discrete_set, values=[e for e in SensorType], check_set_errors=True ) def sensor_data_clear(self, sensor_id): """ Clear the Sensor Data table of 'sensor_id' previous to entering new values. """ values = [0, 9] strict_range(sensor_id, values) self.write(f"CT{sensor_id}") def sensor_data_ref_cal_factor(self, sensor_id, ref_cal_factor): """ Set the power sensor's reference calibration factor to the Sensor Data table. """ values = [0, 9] strict_range(sensor_id, values) self.write(f"RF{sensor_id}{ref_cal_factor:4.1f}") self.check_errors() def sensor_data_write_cal_factor_table(self, sensor_id, frequency_table, cal_fac_table): """ Write the 'calibration_table' for 'sensor_id' to the Sensor Data table. And write the reference calibration factor for the 'sensor_id'. Frequency is given in Hz. Calibration factor as percentage. The power meter’s memory contains space for 10 tables, numbered 0—9. Tables 0-7 each contain space for 40 frequency /calibration factor pairs. Tables 8 and 9 each contain space for 80 frequency/calibration factor pairs. This function clears the sensor table before writing. Example table: .. code-block:: python calibration_table = { 10e6: 100.0, 1e9: 96.5, 2e9: 97.0 } instr.sensor_data_cal_factor_table(0, calibration_table.keys(), calibration_table.values()) """ values = [0, 9] strict_range(sensor_id, values) if sensor_id in range(0, 7) and (len(cal_fac_table) > 40 or len(frequency_table)) > 40: raise ValueError(f"For sensor id {sensor_id} there aren't more than 40 frequency " f"pairs allowed") if sensor_id in range(8, 9) and (len(cal_fac_table) > 80 or len(frequency_table)) > 80: raise ValueError(f"For sensor id {sensor_id} there aren't more than 80 frequency " f"pairs allowed") if len(cal_fac_table) != len(frequency_table): raise ValueError(f"Frequency table and calibration factor table must have the same " f"length {len(cal_fac_table)}!={len(frequency_table)}") self.sensor_data_clear(sensor_id) for frequency, cal_factor in zip(frequency_table, cal_fac_table): if frequency > 99.9e6: freq_suffix = "GZ" frequency /= 1e9 elif frequency > 99.9e3: freq_suffix = "MZ" frequency /= 1e6 else: freq_suffix = "KZ" frequency /= 1e3 self.write(f"ET{sensor_id} {frequency:5.2f}{freq_suffix} {cal_factor}% EN") self.check_errors() self.write("EX") self.check_errors() def sensor_data_read_cal_factor_table(self, sensor_id): """ Read the Sensor Data calibration table. See :meth:`sensor_data_write_cal_factor_table` Returns a tuple of frequencies as list and calibration factors as list. """ allowed_values = [0, 9] strict_range(sensor_id, allowed_values) pairs = 80 if sensor_id < 8: pairs = 40 frequency_data = [] cal_fac_data = [] self.write(f"ET{sensor_id}") self.check_errors() for i in range(0, pairs): # outputs something like 38.00GZ 100.2% display_content = self.display_output frequency = float(display_content[0:5]) if frequency == 0: break if display_content[5:7] == "GZ": frequency *= 1e9 else: frequency *= 1e6 calibration_factor = float(display_content[8:13]) cal_fac_data.append(calibration_factor) frequency_data.append(frequency) self.write("EN") self.check_errors() self.write("EX") return frequency_data, cal_fac_data def sensor_data_write_id_label(self, sensor_id, label): """ Set a particular power sensor’s ID label table to be modified. The sensor ID label must not exceed 7 characters. For example, to identify Sensor Data table #2 with an ID number of 1234567: .. code-block:: python instr.sensor_data_id_label(2, "1234567") """ values = [0, 9] strict_range(sensor_id, values) if len(label) > 7: raise ValueError("Sensor id label must not exceed length of 7") if not str(label).upper().isalnum(): raise ValueError("Sensor id label only allows 0-9, A-Z") self.write(f"SN{sensor_id}{label}") automatic_range_enabled = Instrument.control( "SM", "%s", """ Control the automatic range. The power meter divides each sensor’s power range into 5 ranges of 10 dB each. Range 1 is the most sensitive (lowest power levels), and Range 5 is the least sensitive (highest power levels). The range can be set either automatically or manually. """, get_process=_getstatus(StatusMessage.AutomaticRangeStatus, lambda v: bool(v)), set_process=lambda v: "RM0EN" if v is True else "RH" ) range = Instrument.control( "SM", "RM%dEN", """ Control the range to be selected manually. Valid range numbers are 1 through 5. See :attr:`automatic_range_enabled` for further information. """, values=[1, 5], validator=strict_range, get_process=_getstatus(StatusMessage.Range) ) def store(self, register): """ The power meter can store instrument configurations for recall at a later time. The following information can be stored in the power meter’s internal registers: - reference calibration factor value - Measurement units (dBm or watts) - relative value and status (on or off) - power reference status (on or off) - calibration factor value - SENSOR ID (sensor data table selection) - offset value and status (on or off) - range (Auto or Set) - frequency value - resolution - duty cycle value and status (on or off) - Filter (number of readings averaged, auto or manual) - Limits value and status (on or off) Registers 1 through 10 are available for storing instrument configurations. """ values = [1, 10] strict_range(register, values) self.write(f"ST{register}EN") operating_mode = Instrument.measurement( "SM", """ Get the operating mode the power meter is currently in. """, get_process=_getstatus(StatusMessage.OperatingMode, lambda v: OperatingMode(v)) ) def zero(self): """ Adjust the power meter’s internal circuitry for a zero power indication when no power is applied to the sensor. .. note:: Ensure that no power is applied to the sensor while the power meter is zeroing. Any applied RF input power will cause an erroneous reading. """ self.write("ZE") trigger_mode = Instrument.control( "SM", "TR%d", """ Control the trigger mode. The power meter has two modes of triggered operation; standby mode and free run mode. Standby mode means the power meter is making measurements, but the display and HP-IB are not updated until a trigger command is received. Free run means that Meter takes measurements and updates the display and HP-IB continuously. """, values=[e for e in TriggerMode], validator=strict_discrete_set, get_process=_getstatus(StatusMessage.TriggerMode, lambda v: TriggerMode(v)), set_process=lambda v: int(v) ) def trigger_immediate(self): """ Trigger immediate. When the power meter receives the trigger immediate program code, it inputs one more data point into the digital filter, measures the reading from the filter, and then updates the display and HP-IB. (When the trigger immediate command is executed, the internal digital filter is not cleared.) The power meter then waits for the measurement results to be read by the controller. While waiting, the power meter can process most bus commands without losing the measurement results. If the power meter receives a trigger immediate command and then receives the GET (Group Execute Trigger) command, the trigger immediate command will be aborted and a new measurement cycle will be executed. Once the measurement results are read onto the bus, the power meter always reverts to standby/hold mode. Measurement results obtained via trigger immediate are normally valid only when the power meter is in a steady, settled state. """ self.write("TR1") def trigger_delay(self): """ Trigger with delay. Triggering with delay is identical to :meth:`trigger_immediate` except the power meter inserts a settling-time delay before taking the requested measurement. This settling time allows the internal digital filter to be updated with new values to produce valid, accurate measurement results. The trigger with delay command allows time for settling of the internal amplifiers and filters. It does not allow time for power sensor delay. In cases of large power changes, the delay may not be sufficient for complete settling. Accurate readings can be assured by taking two successive measurements for comparison. Once the measurement results are displayed and read onto the bus, the power meter reverts to standby mode. """ self.write("TR2") group_trigger_mode = Instrument.control( "SM", "GT%d", """ Control the group execute trigger mode. When in remote and addressed to listen, the power meter responds to a Trigger message ( the Group Execute Trigger bus command [GET]) according to the programmed mode. """, values=[e for e in GroupTriggerMode], validator=strict_discrete_set, get_process=_getstatus(StatusMessage.GroupTriggerMode, lambda v: GroupTriggerMode(v)), set_process=lambda v: int(v) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/hp8116a.py0000644000175100001770000005113314623331163021676 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 enum import Enum, IntFlag from pymeasure.instruments import Instrument from pymeasure.instruments.validators import ( strict_discrete_set, strict_range, truncated_discrete_set ) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) # for Python>3.9, these two methods can be static methods in the class def _generate_1_2_5_sequence(min, max): """ Generate a list of a 1-2-5 sequence between min and max. """ exp_min = int(np.log10(min)) exp_max = int(np.log10(max)) seq_1_2_5 = np.array([1, 2, 5]) sequence = np.array([seq_1_2_5 * (10 ** exp) for exp in range(exp_min - 1, exp_max + 1)]) sequence = sequence.flatten() sequence = sequence[(sequence >= min) & (sequence <= max)] return list(sequence) def _boolean_control(identifier, state_index, docs, inverted=False, **kwargs): return Instrument.control( 'CST', identifier + '%d', docs, validator=strict_discrete_set, values=[True, False], get_process=lambda x: inverted ^ bool(int(x[state_index][1])), set_process=lambda x: int(inverted ^ x), **kwargs ) class Status(IntFlag): """ IntFlag type for the GPIB status byte which is returned by the :py:attr:`status` property. When the timing_error or programming_error flag is set, a more detailed error description can be obtained by calling :py:method:`check_errors()`. """ timing_error = 1 << 0 programming_error = 1 << 1 syntax_error = 1 << 2 system_failure = 1 << 3 autovernier_in_progress = 1 << 4 sweep_in_progress = 1 << 5 service_request = 1 << 6 buffer_not_empty = 1 << 7 class HP8116A(Instrument): """ Represents the Hewlett-Packard 8116A 50 MHz Pulse/Function Generator and provides a high-level interface for interacting with the instrument. The resolution for all floating point instrument parameters is 3 digits. """ def __init__(self, adapter, name="Hewlett-Packard 8116A", **kwargs): kwargs.setdefault('read_termination', '\r\n') kwargs.setdefault('write_termination', '\r\n') kwargs.setdefault('send_end', True) super().__init__( adapter, name, includeSCPI=False, **kwargs ) self.has_option_001 = self._check_has_option_001() class Digit(Enum): """ Enum of the digits used with the autovernier (see :py:meth:`HP8116A.start_autovernier()`). """ MOST_SIGNIFICANT = 'M' SECOND_SIGNIFICANT = 'S' LEAST_SIGNIFICANT = 'L' class Direction(Enum): """ Enum of the directions used with the autovernier (see :py:meth:`HP8116A.start_autovernier()`). """ UP = 'U' DOWN = 'D' OPERATING_MODES = { 'normal': 'M1', 'triggered': 'M2', 'gate': 'M3', 'external_width': 'M4', # Option 001 only 'internal_sweep': 'M5', 'external_sweep': 'M6', 'internal_burst': 'M7', 'external_burst': 'M8', } OPERATING_MODES_INV = {v: k for k, v in OPERATING_MODES.items()} CONTROL_MODES = { 'off': 'CT0', 'FM': 'CT1', 'AM': 'CT2', 'PWM': 'CT3', 'VCO': 'CT4', } CONTROL_MODES_INV = {v: k for k, v in CONTROL_MODES.items()} TRIGGER_SLOPES = { 'off': 'T0', 'positive': 'T1', 'negative': 'T2', } TRIGGER_SLOPES_INV = {v: k for k, v in TRIGGER_SLOPES.items()} SHAPES = { 'dc': 'W0', 'sine': 'W1', 'triangle': 'W2', 'square': 'W3', 'pulse': 'W4', } SHAPES_INV = {v: k for k, v in SHAPES.items()} _units_frequency = { 'milli': 'MZ', 'no_prefix': 'HZ', 'kilo': 'KHZ', 'mega': 'MHZ', } _units_voltage = { 'milli': 'MV', 'no_prefix': 'V', } _units_time = { 'nano': 'NS', 'micro': 'US', 'milli': 'MS', 'no_prefix': 'S', } _si_prefixes = { 'nano': 1e-9, 'micro': 1e-6, 'milli': 1e-3, 'no_prefix': 1, 'kilo': 1e3, 'mega': 1e6, } @staticmethod def _get_value_with_unit(value, units): """ Convert a floating point value to a string with 3 digits resolution and the appropriate unit. :param value: The value to convert. :param units: Dictionary containing a mapping of SI-prefixes to the unit strings the instrument uses, eg. 'milli' -> 'MZ' for millihertz. """ if value < 1e-6: value_str = f'{value*1e9:.3g} {units["nano"]}' elif value < 1e-3: value_str = f'{value*1e6:.3g} {units["micro"]}' elif value < 1: value_str = f'{value*1e3:.3g} {units["milli"]}' elif value < 1e3: value_str = f'{value:.3g} {units["no_prefix"]}' elif value < 1e6: value_str = f'{value*1e-3:.3g} {units["kilo"]}' else: value_str = f'{value*1e-6:.3g} {units["mega"]}' return value_str @staticmethod def _parse_value_with_unit(value_str, units): """ Convert a string with a value and a unit as returned by the HP8116A to a float. :param value_str: The string to parse. :param units: Dictionary containing a mapping of SI-prefixes to the unit strings the instrument uses, eg. 'milli' -> 'MZ' for millihertz. """ # Example value_str: 'FRQ 1.00KHZ' # Digits and unit are always positioned the same for all parameters value_str = value_str.strip() value = float(value_str[3:8].strip()) unit = value_str[8:].strip() units_inverse = {v: k for k, v in units.items()} value *= HP8116A._si_prefixes[units_inverse[unit]] return value # Instrument communication # def write(self, command): """ Write a command to the instrument and wait until the 8116A has interpreted it. """ super().write(command) # We need to read the status byte and wait until the buffer_not_empty bit # is cleared because some older units lock up if we don't. self._wait_for_commands_processed() def ask(self, command, num_bytes=None): """ Write a command to the instrument, read the response, and return the response as ASCII text. :param command: The command to send to the instrument. :param num_bytes: The number of bytes to read from the instrument. If not specified, the number of bytes is automatically determined by the command. """ # noqa: E501 self.write(command) if num_bytes is None: if command == 'CST': # We usually only need the first 29 bytes of the state response since they contain # the current boolean parameters. The other parameters all have corresponding # 'interrogate' commands. num_bytes = 29 elif command[0] == 'I': num_bytes = 14 # The first character is always a space or a leftover character from the previous command, # when the number of bytes read was too large or too small. bytes = self.read_bytes(num_bytes)[1:] return bytes.decode('ascii').strip(' ,\r\n') operating_mode = Instrument.control( 'CST', '%s', """Control the operating mode of the instrument. Possible values (without Option 001) are: 'normal', 'triggered', 'gate', 'external_width'. With Option 001, 'internal_sweep', 'external_sweep', 'external_width', 'external_pulse' are also available. """, validator=strict_discrete_set, values=OPERATING_MODES, map_values=True, get_process=lambda x: HP8116A.OPERATING_MODES_INV[x[0]] ) control_mode = Instrument.control( 'CST', '%s', """Control the control mode of the instrument. Possible values are 'off', 'FM', 'AM', 'PWM', 'VCO'. """, validator=strict_discrete_set, values=CONTROL_MODES, map_values=True, get_process=lambda x: HP8116A.CONTROL_MODES_INV[x[1]] ) trigger_slope = Instrument.control( 'CST', '%s', """Control the slope the trigger triggers on. Possible values are: 'off', 'positive', 'negative'. """, validator=strict_discrete_set, values=TRIGGER_SLOPES, map_values=True, get_process=lambda x: HP8116A.TRIGGER_SLOPES_INV[x[2]] ) shape = Instrument.control( 'CST', '%s', """Control the shape of the output waveform. Possible values are: 'dc', 'sine', 'triangle', 'square', 'pulse'. """, validator=strict_discrete_set, values=SHAPES, map_values=True, get_process=lambda x: HP8116A.SHAPES_INV[x[3]] ) haversine_enabled = _boolean_control( 'H', 4, """Control whether a haversine/havertriangle signal is generated when in 'triggered', 'internal_burst' or 'external_burst' operating mode. """, ) autovernier_enabled = _boolean_control( 'A', 5, """Control whether the autovernier is enabled (bool).""", check_set_errors=True ) limit_enabled = _boolean_control( 'L', 6, """Control whether parameter limiting is enabled (bool).""", ) complement_enabled = _boolean_control( 'C', 7, """Control whether the complement of the signal is generated (bool).""", ) output_enabled = _boolean_control( 'D', 8, """Control whether the output is enabled (bool).""", inverted=True, # The actual command is "Disable output"... ) frequency = Instrument.control( 'IFRQ', 'FRQ %s', """Control the frequency of the output in Hz (strict float from 1e-3 to 52.5e6). """, validator=strict_range, values=[1e-3, 52.5001e6], set_process=lambda x: HP8116A._get_value_with_unit(x, HP8116A._units_frequency), get_process=lambda x: HP8116A._parse_value_with_unit(x, HP8116A._units_frequency) ) duty_cycle = Instrument.control( 'IDTY', 'DTY %s %%', """Control the duty cycle of the output in percent (float). The allowed range generally is 10 % to 90 %, but it also depends on the current frequency. It is valid for all shapes except 'pulse', where :py:attr:`pulse_width` is used instead. """, validator=strict_range, values=[10, 90.0001], cast=int, # get_process=lambda x: int(x[6:8]) ) pulse_width = Instrument.control( 'IWID', 'WID %s', """Control the pulse width in s (strict float from 8e-9 to 999e-3). The pulse width may not be larger than the period. """, validator=strict_range, values=[8e-9, 999.001e-3], set_process=lambda x: HP8116A._get_value_with_unit(x, HP8116A._units_time), get_process=lambda x: HP8116A._parse_value_with_unit(x, HP8116A._units_time) ) amplitude = Instrument.control( 'IAMP', 'AMP %s', """Control the amplitude of the output in V (strict float from 10e-3 to 16). The allowed amplitude range is also limited by the current offset. """, validator=strict_range, values=[10e-3, 16.001], set_process=lambda x: HP8116A._get_value_with_unit(x, HP8116A._units_voltage), get_process=lambda x: HP8116A._parse_value_with_unit(x, HP8116A._units_voltage) ) offset = Instrument.control( 'IOFS', 'OFS %s', """Control the offset of the output in V (strit float from -7.95 to 7.95). The allowed offset range is also limited by the amplitude. """, validator=strict_range, values=[-7.95, 7.95001], set_process=lambda x: HP8116A._get_value_with_unit(x, HP8116A._units_voltage), get_process=lambda x: HP8116A._parse_value_with_unit(x, HP8116A._units_voltage) ) high_level = Instrument.control( 'IHIL', 'HIL %s', """Control the high level of the output in V (strict float from -7.9 to 8). The allowed high level range must be at least 10 mV greater than the low level. """, validator=strict_range, values=[-7.9, 8.001], set_process=lambda x: HP8116A._get_value_with_unit(x, HP8116A._units_voltage), get_process=lambda x: HP8116A._parse_value_with_unit(x, HP8116A._units_voltage) ) low_level = Instrument.control( 'ILOL', 'LOL %s', """Control the low level of the output in V (strict float from -8 to 7.9). The allowed low level range must be at least 10 mV less than the high level. """, validator=strict_range, values=[-8, 7.9001], set_process=lambda x: HP8116A._get_value_with_unit(x, HP8116A._units_voltage), get_process=lambda x: HP8116A._parse_value_with_unit(x, HP8116A._units_voltage) ) burst_number = Instrument.control( 'IBUR', 'BUR %s #', """Control the number of periods generated in a burst (strict int from 1 to 1999). It is only valid for units with Option 001 in one of the burst modes. """, validator=strict_range, values=[1, 1999], get_process=lambda x: int(x[4:8]) ) repetition_rate = Instrument.control( 'IRPT', 'RPT %s', """Control the repetition rate in s (i.e. the time between bursts) in 'internal_burst' mode (strict float from 20e-9 to 999e-3). """, validator=strict_range, values=[20e-9, 999.001e-3], set_process=lambda x: HP8116A._get_value_with_unit(x, HP8116A._units_time), get_process=lambda x: HP8116A._parse_value_with_unit(x, HP8116A._units_time) ) sweep_start = Instrument.control( 'ISTA', 'STA %s', """Control the start frequency in both sweep modes in Hz (strict float from 1e-3 to 52.5e6). """, validator=strict_range, values=[1e-3, 52.5001e6], set_process=lambda x: HP8116A._get_value_with_unit(x, HP8116A._units_frequency), get_process=lambda x: HP8116A._parse_value_with_unit(x, HP8116A._units_frequency) ) sweep_stop = Instrument.control( 'ISTP', 'STP %s', """Control the stop frequency in both sweep modes in Hz (strict float from 1e-3 to 52.5e6). """, validator=strict_range, values=[1e-3, 52.5001e6], set_process=lambda x: HP8116A._get_value_with_unit(x, HP8116A._units_frequency), get_process=lambda x: HP8116A._parse_value_with_unit(x, HP8116A._units_frequency) ) sweep_marker_frequency = Instrument.control( 'IMRK', 'MRK %s', """Control the frequency marker in both sweep modes in Hz (strict float from 1e-3 to 52.5e6). At this frequency, the marker output switches from low to high. """, validator=strict_range, values=[1e-3, 52.5001e6], set_process=lambda x: HP8116A._get_value_with_unit(x, HP8116A._units_frequency), get_process=lambda x: HP8116A._parse_value_with_unit(x, HP8116A._units_frequency) ) sweep_time = Instrument.control( 'ISWT', 'SWT %s', """Control the sweep time per decade in both sweep modes in s (float). The sweep time is selectable in a 1-2-5 sequence between 10 ms and 500 s. """, validator=truncated_discrete_set, values=_generate_1_2_5_sequence(10e-3, 500), set_process=lambda x: HP8116A._get_value_with_unit(x, HP8116A._units_time), get_process=lambda x: HP8116A._parse_value_with_unit(x, HP8116A._units_time) ) @property def status(self): """Get the status byte of the 8116A as a :class:`Status` IntFlag-type enum.""" return Status(self.adapter.connection.read_stb()) @property def complete(self): """Get whether the measurement is complete (bool).""" return not (self.status & Status.buffer_not_empty) @property def options(self): """Get the device options installed. The only possible option is 001.""" if self.has_option_001: return ['001'] else: return [] def start_autovernier(self, control, digit, direction, start_value=None): """ Start the autovernier on the specified control. :param control: The control to change, pass as :code:`HP8116A.some_control`. Allowed controls are frequency, amplitude, offset, duty_cycle, and pulse_width :param digit: The digit to change, type: :py:class:`HP8116A.Digit`. :param direction: The direction in which to change the control, type: :py:class:`HP8116A.Direction`. :param start_value: An optional value to start the autovernier at. If not specified, the current value of the control is used. """ if not self.autovernier_enabled: raise RuntimeError('Autovernier has to be enabled first.') if control not in (HP8116A.frequency, HP8116A.amplitude, HP8116A.offset, HP8116A.duty_cycle, HP8116A.pulse_width): raise ValueError('Control must be one of frequency, amplitude, offset, ' + 'duty_cycle, or pulse_width.') start_value = control.fget(self) if start_value is None else start_value # The control always has to be set to select it for the autovernier. control.fset(self, start_value) self.write(digit.value + direction.value) def GPIB_trigger(self): """ Initiate trigger via low-level GPIB-command (aka GET - group execute trigger). """ self.adapter.connection.assert_trigger() def reset(self): """ Initatiate a reset (like a power-on reset) of the 8116A. """ self.adapter.connection.clear() self._wait_for_commands_processed() def shutdown(self): """ Gracefully close the connection to the 8116A. """ self.adapter.connection.clear() self.adapter.close() super().shutdown() def check_errors(self): """ Check for errors in the 8116A. :return: list of error entries or empty list if no error occurred. """ errors_response = self.ask('IERR', 100).split('\r\n')[0].strip(' ,\r\n') errors = errors_response.split('ERROR')[:-1] errors = [e.strip() + " ERROR" for e in errors] if errors[0] == 'NO ERROR': return [] else: for error in errors: log.error(f'{self.name}: {error}') return errors def _wait_for_commands_processed(self, timeout=1): """ Wait until the commands have been processed by the 8116A. """ start = time.time() while not self.complete: time.sleep(0.001) if time.time() - start > timeout: raise RuntimeError('Timeout waiting for commands to be processed.') def _check_has_option_001(self): """ Return True if the 8116A has option 001 and False otherwise. This is done by checking the length of the response to the CST (current status) command which includes sweep parameters and burst parameters only if the 8116A has option 001. """ # The longest possible state string is 163 characters long including termination characters state_string = self.ask('CST', 163).split('\r\n')[0].strip(' ,\r\n') if len(state_string) == 159: return True elif len(state_string) == 87: return False else: log.warning('Could not determine if 8116A has option 001. Assuming it has.') return True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/hp856Xx.py0000644000175100001770000037247514623331163022017 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 math import log10 from enum import Enum, IntFlag from datetime import datetime import numpy as np from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, truncated_discrete_set, \ joined_validators, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) try: from enum import StrEnum except ImportError: class StrEnum(str, Enum): """Until StrEnum is broadly available / pymeasure relies on python <= 3.10.x.""" def __str__(self): return self.value class WindowType(StrEnum): """Enumeration to represent the different window mode for FFT functions""" #: Flattop provides optimum amplitude accuracy Flattop = "FLATTOP" #: Hanning provides an amplitude accuracy/frequency resolution compromise Hanning = "HANNING" #: Uniform provides equal weighting of the time record for measuring transients. Uniform = "UNIFORM" class StatusRegister(IntFlag): """Enumeration to represent the Status Register.""" #: Request Service RQS = 64 #: Set when error present ERROR_PRESENT = 32 #: Any command is completed COMMAND_COMPLETE = 16 #: Unused but sometimes set NA = 8 #: Set when any sweep is completed END_OF_SWEEP = 4 #: Set when display message appears MESSAGE = 2 #: Trigger is activated TRIGGER = 1 #: No Interrupts can interrupt the program sequence NONE = 0 class Trace(StrEnum): """Enumeration to represent either Trace A or Trace B.""" #: Trace A A = "TRA" #: Trace B B = "TRB" class SweepCoupleMode(StrEnum): """Enumeration.""" #: Stimulus Response SpectrumAnalyzer = "SA" #: Spectrum Analyeze StimulusResponse = "SR" class SweepOut(StrEnum): """Enumeration.""" #: 0 - 10V Ramp Ramp = "RAMP" #: DC Ramp 0.5V / GHz Fav = "FAV" class MixerMode(StrEnum): """Enumeration to represent the Mixer Mode of the HP8561B.""" #: Mixer Mode Internal Internal = "INT" #: Mixer Mode External External = "EXT" class SourceLevelingControlMode(StrEnum): """Enumeration to represent the Source Leveling Control Mode of the HP8560A.""" #: Source Leveling Control Mode Internal Internal = "INT" #: Source Leveling Control Mode External External = "EXT" class PeakSearchMode(StrEnum): """Enumeration to represent the Marker Peak Search Mode.""" #: Place marker to the highest value on the trace High = "HI" #: Place marker to the next highest value on the trace NextHigh = "NH" #: Place marker to the next peak to the right NextRight = "NR" #: Place marker to the next peak to the left NextLeft = "NL" class CouplingMode(StrEnum): """Enumeration to represent the Coupling Mode.""" #: AC AC = "AC" #: DC DC = "DC" class DemodulationMode(StrEnum): """Enumeration to represent the Demodulation Mode.""" #: Amplitude Modulation Amplitude = "AM" #: Frequency Modulation Frequency = "FM" #: Demodulation Off Off = "OFF" class TriggerMode(StrEnum): """Enumeration to represent the different trigger modes""" #: External Mode External = "EXT" #: Free Running Free = "FREE" #: Line Mode Line = "LINE" #: Video Mode Video = "VID" class TraceDataFormat(StrEnum): """Enumeration to represent the different trace data formats.""" #: A-Block format A_BLOCK = "A" #: Binary format BINARY = "B" #: I-Block format I_BLOCK = "I" #: ASCII format ASCII = "M" #: Real numbers format like are in Hz, volts, watts, dBm, dBmV, dBuV, dBV, or seconds. REAL = "P" class FrequencyReference(StrEnum): """Enumeration to represent the frequency reference source.""" #: Internal Frequency Reference Internal = "INT" #: External Frequency Standard External = "EXT" class DetectionModes(StrEnum): """Enumeration to represent the Detection Modes.""" #: Negative Peak Detection NegativePeak = "NEG" #: Normal Peak Detection Normal = "NRM" #: Positive Peak Detection PositivePeak = "POS" #: Sampl Mode Detection Sample = "SMP" class AmplitudeUnits(StrEnum): """Enumeration to represent the amplitude units.""" #: DB over millit Watt DBM = "DBM" #: DB over milli Volt DBMV = "DBMV" #: DB over micro Volt DBUV = "DBUV" #: Volts V = "V" #: Watt W = "W" #: Automatic Unit (Usually derives to 'DBM') AUTO = "AUTO" #: Manual Mode MANUAL = "MAN" class ErrorCode: """ Class to decode error codes from the spectrum analyzer. """ __error_code_list = { 0: ("NO ERR", "No Error at all"), 100: ("PWRON", "Power-on state is invalid; default state is loaded"), 101: ("NO STATE", "State to be RECALLed not valid or not SAVEd"), 106: ("ABORTED!", "Current operation is aborted; HP-IB parser reset"), 107: ("HELLO ??", "No HP-IB listener is present"), 108: ("TIME OUT", "Analyzer timed out when acting as controller"), 109: ("CtrlFail", "Analyzer unable to take control of the bus"), 110: ("NOT CTRL", "Analyzer is not system controller"), 111: ("# ARGMTS", "Command does not have enough arguments"), 112: ("??CMD??", "Unrecognized command"), 113: ("FREQ NQ!", "Command cannot have frequency units"), 114: ("TIME NOG!", "Command cannot have time units"), 115: ("AMPL NO!", "Command cannot have amplitude units"), 116: ("PUNITS??", "Unrecognizable units"), 117: ("NOP NUM", "Command cannot have numeric units"), 118: ("NOP EP", "Enable parameter cannot be used"), 119: ("NOP UPDN", "UP/DN are not valid arguments for command"), 120: ("NOP ONOF", "ON/OFF are not valid arguments for command"), 121: ("NOP ARG", "AUTO/MAN are not valid arguments for command"), 122: ("NOP TRC", "Trace registers are not valid for command"), 123: ("NOP ABLK", "A-block format not valid here"), 124: ("NOP IBLK", "I-block format not valid here"), 125: ("NOP STRNG", "Strings are not valid for this command"), 126: ("NO ?", "This command cannot be queried"), 127: ("BAD DTMD", "Not a valid peak detector mode"), 128: ("PK WHAT?", "Not a valid peak search parameter"), 129: ("PRE TERM", "Premature A-block termination"), 130: ("BAD TDF", "Arguments are only for TDF command"), 131: ("?? AM/FM", "AM/FM are not valid arguments for this command"), 132: ("!FAV/RMP", "FAV/RAMP are not valid arguments for this command"), 133: ("!INT/EXT", "INT/EXT are not valid arguments for this command"), 134: ("??? ZERO", "ZERO is not a valid argument for this command"), 135: ("??? CURR", "CURR is not a valid argument for this command"), 136: ("??? FULL", "FULL is not a valid argument for this command"), 137: ("??? LAST", "LAST is not a valid argument for this command"), 138: ("!GRT/DSP", "GRT/DSP are not valid arguments for this command"), 139: ("PLOTONLY", "Argument can only be used with PLOT command"), 140: ("?? PWRON", "PWRON is not a valid argument for this command"), 141: ("BAD ARG", "Argument can only be used with FDIAG command"), 142: ("BAD ARG", "Query expected for FDIAG command"), 143: ("NO PRESL", "No preselector hardware to use command with (HP 8562B)"), 200: ("SYSTEM", "Hardware/firmware interaction; check other errors"), 201: ("SYSTEM", "Hardware/firmware interaction; check other errors"), 250: ("OUTOF RG", "ADC input is outside of ADC range"), 251: ("NO IRQ", "Microprocessor not receiving interrupt from ADC"), 300: ("YTO UNLK", "YTO (1ST LO) phase-locked loop (PLL) is unlocked"), 301: ("YTO UNLK", "YTO PLL is unlocked"), 302: ("OFF UNLK", "Offset Roller Oscillator PLL is unlocked"), 303: ("XFR UNLK", "Transfer Roller Oscillator PLL is unlocked"), 304: ("ROL UNLK", "Main Roller Oscillator PLL is unlocked"), 305: ("FREQ ACC", "Frequency accuracy error"), 306: ("FREQ ACC", "Frequency accuracy error"), 307: ("FREQ ACC", "Frequency accuracy error"), 308: ("FREQ ACC", "Frequency accuracy error"), 309: ("FREQ ACC", "Frequency accuracy error"), 310: ("FREQ ACC", "Frequency accuracy error"), 311: ("FREQ ACC", "Frequency accuracy error"), 312: ("FREQ ACC", "Frequency accuracy error"), 313: ("FREQ ACC", "Frequency accuracy error"), 314: ("FREQ ACC", "Frequency accuracy error"), 315: ("FREQ ACC", "Frequency accuracy error"), 316: ("FREQ ACC", "Frequency accuracy error"), 317: ("FREQ ACC", "Frequency accuracy error"), 318: ("FREQ ACC", "Frequency accuracy error"), 319: ("FREQ ACC", "Frequency accuracy error"), 320: ("FREQ ACC", "Frequency accuracy error"), 321: ("FREQ ACC", "Frequency accuracy error"), 322: ("FREQ ACC", "Frequency accuracy error"), 323: ("FREQ ACC", "Frequency accuracy error"), 324: ("FREQ ACC", "Frequency accuracy error"), 325: ("FREQ ACC", "Frequency accuracy error"), 326: ("FREQ ACC", "Frequency accuracy error"), 327: ("OFF UNLK", "Offset Roller Oscillator PLL is unlocked"), 328: ("FREQ ACC", "Frequency accuracy error"), 329: ("FREQ ACC", "Frequency accuracy error"), 331: ("FREQ ACC", "Frequency accuracy error"), 333: ("600 UNLK", "600 MHz Reference Oscillator PLL is unlocked"), 334: ("LO AMPL", "YTO (ist LO) unleveled"), 400: ("AMPL 100", "Unable to adjust amplitude of 100 Hz resolution bandwidth"), 401: ("AMPL 300", "Unable to adjust amplitude of 300 Hz resolution bandwidth"), 402: ("AMPL 1K", "Unable to adjust amplitude of 1 kHz resolution bandwidth"), 403: ("AMPL 3K", "Unable to adjust amplitude of 3 kHz resolution bandwidth"), 404: ("AMPL 10K", "Unable to adjust amplitude of 10 kHz resolution bandwidth"), 405: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 406: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 407: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 408: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 409: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 410: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 411: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 412: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 413: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 414: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 415: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 416: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 417: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 418: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 419: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 420: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 421: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 422: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 423: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 424: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 425: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 426: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 427: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 428: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 429: ("RBW 100", "Unable to adjust 100 Hz resolution bandwidth"), 430: ("RBW 300", "Unable to adjust 300 Hz resolution bandwidth"), 431: ("RBW 1K", "Unable to adjust 1 kHz resolution bandwidth"), 432: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 433: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 434: ("RBW 300", "Unable to adjust 300 Hz resolution bandwidth"), 435: ("RBW 301", "Unable to adjust 300 Hz resolution bandwidth"), 436: ("RBW 302", "Unable to adjust 300 Hz resolution bandwidth"), 437: ("RBW 303", "Unable to adjust 300 Hz resolution bandwidth"), 438: ("RBW 1K", "Unable to adjust 1 kHz resolution bandwidth"), 439: ("RBW 1K", "Unable to adjust 1 kHz resolution bandwidth"), 440: ("RBW 1K", "Unable to adjust 1 kHz resolution bandwidth"), 441: ("RBW 1K", "Unable to adjust 1 kHz resolution bandwidth"), 442: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 443: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 444: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 445: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 446: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 447: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 448: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 449: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 450: ("IF SYSTM", "IF hardware failure Check other error messages"), 451: ("IF SYSTM", "IF hardware failure Check other error messages"), 452: ("IF SYSTM", "IF hardware failure Check other error messages"), 454: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 455: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 456: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 457: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 458: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 459: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 460: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 461: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 462: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 463: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 464: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 465: ("FREQ ACC", "Unable to adjust step gain amplifiers"), 466: ("LIN AMPL", "Unable to adjust linear amplitude scale"), 467: ("LOG AMPL", "Unable to adjust log amplitude scale"), 468: ("LOG AMPL", "Unable to adjust log amplitude scale"), 469: ("LOG AMPL", "Unable to adjust log amplitude scale"), 470: ("LOG AMPL", "Unable to adjust log amplitude scale"), 471: ("RBW 30K", "Unable to adjust 30 kHz resolution bandwidth"), 472: ("RBW 100K", "Unable to adjust 100 kHz resolution bandwidth"), 473: ("RBW 300K", "Unable to adjust 300 kHz resolution bandwidth"), 474: ("RBW 1M", "Unable to adjust 1 MHz resolution bandwidth"), 475: ("RBW 30K", "Unable to adjust 30 kHz resolution bandwidth"), 476: ("RBW 100K", "Unable to adjust 30 kHz resolution bandwidth"), 477: ("RBW 300K", "Unable to adjust 300 kHz resolution bandwidth"), 478: ("RBW 1M", "Unable to adjust 1 MHz resolution bandwidth"), 483: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 484: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 485: ("RBW 1K", "Unable to adjust 1 kHz resolution bandwidth"), 486: ("RBW 300", "Unable to adjust 300 Hz resolution bandwidth"), 487: ("RBW 100", "Unable to adjust 100 Hz resolution bandwidth"), 488: ("RBW 100", "Unable to adjust 100 Hz resolution bandwidth"), 489: ("RBW 101", "Unable to adjust 100 Hz resolution bandwidth"), 490: ("RBW 102", "Unable to adjust 100 Hz resolution bandwidth"), 491: ("RBW 103", "Unable to adjust 100 Hz resolution bandwidth"), 492: ("RBW 300", "Unable to adjust 300 Hz resolution bandwidth"), 493: ("RBW 1K", "Unable to adjust 1 kHz resolution bandwidth"), 494: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 495: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 496: ("RBW 100", "Unable to adjust 100 Hz resolution bandwidth"), 497: ("RBW 100", "Unable to adjust 100 Hz resolution bandwidth"), 498: ("RBW 100", "Unable to adjust 100 Hz resolution bandwidth"), 499: ("CAL UNLK", "A16 IF Adjustment Cal Oscillator is unlocked"), 500: ("AMPL 30K", "Unable to adjust amplitude of 30 kHz resolution bandwidth"), 501: ("AMPL 1M", "Unable to adjust amplitude of 100 kHz resolution bandwidth"), 502: ("AMPL 3M", "Unable to adjust amplitude of 300 kHz resolution bandwidth"), 503: ("AMPL 1M", "Unable to adjust amplitude of 1 MHz resolution bandwidth"), 504: ("AMPL 30K", "Unable to adjust amplitude of 30 kHz resolution bandwidth"), 505: ("AMPL 1M", "Unabie to adjust amplitude of 100 kHz resolution bandwidth"), 506: ("AMPL 3M", "Unable to adjust amplitude of 300 kHz resolution bandwidth"), 507: ("AMPL 1M", "Unable to adjust amplitude of 1 MHz resolution bandwidth"), 508: ("AMPL 30K", "Unable to adjust amplitude of 30 kHz resolution bandwidth"), 509: ("AMPL 1M", "Unable to adjust amplitude of 100 kHz resolution bandwidth"), 510: ("AMPL 3M", "Unable to adjust amplitude of 300 kHz resolution bandwidth"), 511: ("AMPL 1M", "Unable to adjust amplitude of 1 MHz resolution bandwidth"), 512: ("RBW 100", "Unable to adjust 100 Hz resolution bandwidth"), 513: ("RBW 300", "Unable to adjust 300 Hz resolution bandwidth"), 514: ("RBW 1K", "Unable to adjust 1 kHz resolution bandwidth"), 515: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 516: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 517: ("RBW 100", "Unable to adjust 100 Hz resolution bandwidth"), 518: ("RBW 300", "Unable to adjust 300 Hz resolution bandwidth"), 519: ("RBW 1K", "Unable to adjust 1 kHz resolution bandwidth"), 520: ("RBW 3K", "Unable to adjust 3 kHz resolution bandwidth"), 521: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth"), 522: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth SYM POLE 1"), 523: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth SYM POLE 2"), 524: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth SYM POLE 3"), 525: ("RBW 10K", "Unable to adjust 10 kHz resolution bandwidth SYM POLE 4"), 526: ("RBW <300", "Unable to adjust <300 Hz resolution bandwidths"), 527: ("RBW <301", "Step gain correction failed for <300 Hz resolution bandwidth"), 528: ("RBW <302", "Unable to adjust <300 Hz resolution bandwidths"), 529: ("RBW <303", "Unable to adjust <300 Hz resolution bandwidths"), 530: ("RBW <304", "Unable to adjust <300 Hz resolution bandwidths"), 531: ( "RBW <305", "Unable to adjust gain versus frequency for resoultion bandwidths <300 Hz"), 532: ("RBW <306", "Absolute gain data for resolution bandwidths <300 Hz not acceptable"), 533: ("RBW <307", "Unable to adjust <300 Hz resolution bandwidths"), 534: ("RBW <308", "Unable to adjust frequency accuracy for resolution bandwidths <100 Hz"), 535: ("RBW <309", "Unable to adjust <300 Hz resolution bandwidths"), 536: ("RBW <310", "Unable to adjust <300 Hz resolution bandwidths"), 537: ("RBW <311", "Unable to adjust <300 Hz resolution bandwidths"), 538: ("RBW <312", "Unable to adjust <300 Hz resolution bandwidths"), 539: ("RBW <313", "Unable to adjust <300 Hz resolution bandwidths"), 540: ("RBW <314", "Unable to adjust <300 Hz resolution bandwidths"), 551: ("AMPL", "Unable to adjust step gain amplifiers"), 552: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 553: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 554: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 555: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 556: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 557: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 558: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 559: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 560: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 561: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 562: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 563: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 564: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 565: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 566: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 567: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 568: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 569: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 570: ("LOG AMPL", "Unable to adjust amplitude of log scale"), 571: ("AMPL", "Unable to adjust step gain amplifiers"), 572: ("AMPL 1M", "Unable to adjust amplitude of 1 MHz resolution bandwidth"), 573: ("LOG AMPL", "Unable to adjust amplitude in log scale"), 574: ("LOG AMPL", "Unable to adjust amplitude in log scale"), 575: ("LOG AMPL", "Unable to adjust amplitude in log scale"), 576: ("LOG AMPL", "Unable to adjust amplitude in log scale"), 577: ("LOG AMPL", "Unable to adjust amplitude in log scale"), 581: ("AMPL", "Unable to adjust 100 kHz and <10 kHz resolution bandwidths"), 582: ("AMPL", "Unable to adjust 100 kHz and <10 kHz resolution bandwidths"), 583: ("RBW 30K", "Unable to adjust 30 kHz resolution bandwidth"), 584: ("RBW 100K", "Unable to adjust 100 kHz resolution bandwidth"), 585: ("RBW 300K", "Unable to adjust 300 kHz resolution bandwidth"), 586: ("RBW 1M", "Unable to adjust 1 MHz resolution bandwidth"), 587: ("RBW 30K", "Unable to adjust 30 kHz resolution bandwidth"), 588: ("RBW 300K", "Unable to adjust 100 kHz resolution bandwidth"), 589: ("RBW 300K", "Unable to adjust 300 kHz resolution bandwidth"), 590: ("RBW 1M", "Unable to adjust 1 MHz resolution bandwidth"), 591: ("LOG AMPL", "Unable to adjust amplitude in log scale"), 592: ("LOG AMPL", "Unable to adjust amplitude in log scale"), 600: ("SYSTEM", "Hardware/firmware interaction; check other errors"), 601: ("SYSTEM", "Hardware/firmware interaction; check other errors"), 650: ("OUTOF RG", "ADC input is outside of the ADC range"), 651: ("NO IRQ", "Microprocessor is not receiving interrupt from ADC"), 700: ("EEROM", "Checksum error of EEROM A2U501"), 701: ("AMPL CAL", "Checksum error of frequency response correction data"), 702: ("ELAP TIM", "Checksum error of elapsed time data"), 703: ("AMPL CAL", "Checksum error of frequency response correction data"), 704: ("PRESELCT", "Checksum error of customer preselector peak data"), 705: ("ROM U306", "Checksum error of program ROM A2U306"), 706: ("ROM U307", "Checksum error of program ROM A2U307"), 707: ("ROM U308", "Checksum error of program ROM A2U308"), 708: ("ROM U309", "Checksum error of program ROM A2U309"), 709: ("ROM U310", "Checksum error of program ROM A2U310"), 710: ("ROM U311", "Checksum error of program ROM A2U311"), 711: ("RAM U303", "Checksum error of system RAM A2U303"), 712: ("RAM U302", "Checksum error of system RAM A2U302"), 713: ("RAM U301", "Checksum error of system RAM A2U301"), 714: ("RAM U300", "Checksum error of system RAM A2U300"), 715: ("RAM U305", "Checksum error of system RAM A2U305"), 716: ("RAM U304", "Checksum error of system RAM A2U304"), 717: ("BAD uP!!", "Microprocessor not fully operational"), 718: ("BATTERY?", "Nonvolatile RAM not working; check battery"), 750: ("SYSTEM", "Hardware/ firmware interaction; check other errors"), 751: ("SYSTEM", "Hardware/firmware interaction; check other errors"), 752: ("SYSTEM", "Hardware/firmware interaction; check other errors"), 753: ("SYSTEM", "Hardware/firmware interaction; check other errors"), 754: ("SYSTEM", "Hardware/firmware interaction; check other errors"), 755: ("SYSTEM", "Hardware/firmware interaction; check other errors"), 900: ("TG UNLVL", "Tracking generator output is unleveled"), 901: ("TGFrqLmt", "Tracking generator output unleveled because START FREQ is set " "below tracking generator frequency limit (300 kHz)"), 902: ("BAD NORM", "The state of the stored trace does not match the current state of the analyzer"), 903: ("&> DLMT", "Unnormalized trace A is off-screen with trace math or normalization on"), 904: ( "&> DLMT", "Calibration trace (trace B) is off-screen with trace math or normalization on") } # integer representation of error code code = 0 def __init__(self, code): """Initialize an ErrorCode. :param code: Representing an error as id or short description :type code: str, int """ if not (isinstance(code, int) or isinstance(code, str)): print(type(code)) raise TypeError("Initialziation type for code must be integer or string") try: self.code = int(code) if self.code not in self.__error_code_list.keys(): raise ValueError() except (ValueError, TypeError): raise ValueError("This error code doesn't exist") (self.short, self.long) = self.__error_code_list[self.code] def __repr__(self): return "ErrorCode(\"" + self.short + " - " + self.long + "\")" def __eq__(self, other): return self.code == other.code class HP856Xx(Instrument): """Represents the HP856XX series spectrum analyzers. Don't use this class directly - use their derivative classes .. note:: Most command descriptions are taken from the document: 'HP 8560A, 8561B Operating & Programming' """ def __init__(self, adapter, name="Hewlett-Packard HP856Xx", **kwargs): super().__init__( adapter, name, includeSCPI=False, send_end=True, **kwargs, ) def adjust_all(self): """Activate the local oscillator (LO) and intermediate frequency (IF) alignment routines. These are the same routines that occur when is switched on. Commands following 'adjust_all' are not executed until after the analyzer has finished the alignment routines. """ self.write("ADJALL") def set_crt_adjustment_pattern(self): """Activate a CRT adjustment pattern, shown in Figure 5-3. Use the X POSN, Y POSN, and TRACE ALIGN adjustments (available from the rear panel) to align the display. Use X POSN and Y POSN to move the display horizontally and vertically, respectively. Use TRACE ALIGN to straighten a tilted display. To remove the pattern from the screen, execute the :meth:`preset` command.""" self.write("ADJCRT") adjust_if = Instrument.control( "ADJIF?", "ADJIF %s", """ Control the automatic IF adjustment. This function is normally on. Because the IF is continuously adjusting, executing the IF alignment routine is seldom necessary. When the IF adjustment is not active, an "A" appears on the left side of the display. - `"FULL"` IF adjustment is done for all IF settings. - `"CURR"` IF adjustment is done only for the IF settings currently displayed. - `False` turns the continuous IF adjustment off. - `True` reactivates the continuous IF adjustment. Type: :code:`bool, str` """, validator=strict_discrete_set, map_values=True, values={True: "1", False: "0", "FULL": "FULL", "CURR": "CURR"}, cast=str ) trace_a_minus_b_enabled = Instrument.control( "AMB?", "AMB %s", """ Control subtraction of the contents of trace B from trace A. It places the result, in dBm (when in log mode), in trace A. When in linear mode, the result is in volts. If trace A is in clear-write or max-hold mode, this function is continuous. When AMB is active, an "M" appears on the left side of the display. :attr:`trace_a_minus_b_plus_dl` overrides AMB. Type: :code:`bool` .. warning:: The displayed amplitude of each trace element falls in one of 600 data points. There are 10 points of overrange, which corresponds to one-sixth of a division Kg of overrange. When adding or subtracting trace data, any results exceeding this limit are clipped at the limit. """, validator=strict_discrete_set, map_values=True, values={True: "1", False: "0"}, cast=str ) trace_a_minus_b_plus_dl_enabled = Instrument.control( "AMBPL?", "AMBPL %s", """ Control subtraction of trace B from trace A and addition to the display line, and stores the result in dBm (when in log mode) in trace A. When in linear mode, the result is in volts. If trace A is in clear-write or max-hold mode, this function is continuous. When this function is active, an "M" appears on the left side of the display. Type: :code:`bool` .. warning:: The displayed amplitude of each trace element falls in one of 600 data points. There are 10 points of overrange, which corresponds to one-sixth of a division Kg of overrange. When adding or subtracting trace data, any results exceeding this limit are clipped at the limit. """, validator=strict_discrete_set, map_values=True, values={True: "1", False: "0"}, cast=str ) annotation_enabled = Instrument.control( "ANNOT?", "ANNOT %s", """ Set the display annotation off or on. Type: :code:`bool` """, validator=strict_discrete_set, map_values=True, values={True: "1", False: "0"}, cast=str ) attenuation = Instrument.control( "AT?", "AT %s", """ Control the input attenuation in decade steps from 10 to 70 db (type 'int') or set to 'AUTO' and 'MAN'(ual) Type: :code:`str`, :code:`int` .. code-block:: python instr.attenuation = 'AUTO' instr.attenuation = 60 """, validator=joined_validators(strict_discrete_set, truncated_discrete_set), values=[["AUTO", "MAN"], np.arange(10, 80, 10)], cast=int, ) amplitude_unit = Instrument.control( "AUNITS?", "AUNITS %s", """ Control the amplitude unit with a selection of the following parameters: string 'DBM', 'DBMV', 'DBUV', 'V', 'W', 'AUTO', 'MAN' or use the enum :class:`AmplitudeUnits` Type: :code:`str` .. code-block:: python instr.amplitude_unit = 'dBmV' instr.amplitude_unit = AmplitudeUnits.dBmV """, validator=strict_discrete_set, values=[str(e).upper() for e in AmplitudeUnits], set_process=lambda v: str(v).upper() ) def write(self, command, **kwargs): if "{amplitude_unit}" in command: command = command.format(amplitude_unit=self.amplitude_unit) super().write(command, **kwargs) def set_auto_couple(self): """Set the video bandwidth, resolution bandwidth, input attenuation, sweep time, and center frequency step-size to coupled mode. These functions can be recoupled individually or all at once. The spectrum analyzer chooses appropriate values for these functions. The video bandwidth and resolution bandwidth are set according to the coupled ratios stored under :attr:`resolution_bandwidth_to_span_ratio` and :attr:`video_bandwidth_to_resolution_bandwidth`. If no ratios are chosen, default ratios (1.0 and 0.011, respectively) are used instead. """ self.write("AUTOCPL") def exchange_traces(self): """Exchange the contents of trace A with those of trace B. If the traces are in clear-write or max-hold mode, the mode is changed to view. Otherwise, the traces remain in their initial mode. """ self.write("AXB") def blank_trace(self, trace): """Blank the chosen trace from the display. The current contents of the trace remain in the trace but are not updated. .. code-block:: python instr.blank_trace('TRA') instr.blank_trace(Trace.A) :param trace: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :type trace: str :raises TypeError: Type isn't 'string' :raises ValueError: Value is 'TRA' nor 'TRB' """ if not isinstance(trace, str): raise TypeError("Should be of type string but is '%s'" % type(trace)) if trace not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], trace)) self.write("BLANK " + trace) def subtract_display_line_from_trace_b(self): """Subtract the display line from trace B and places the result in dBm (when in log mode) in trace B, which is then set to view mode. In linear mode, the results are in volts. """ self.write("BML") center_frequency = Instrument.control( "CF?", "CF %.11E Hz", """ Control the center frequency in hertz and sets the spectrum analyzer to center frequency / span mode. The span remains constant; the start and stop frequencies change as the center frequency changes. Type: :code:`float` .. code-block:: python instr.center_frequency = 300.5e6 if instr.center_frequency == 200e3: print("Correct frequency") """, validator=strict_range, values=[0, 1], dynamic=True ) def clear_write_trace(self, trace): """Set the chosen trace to clear-write mode. This mode sets each element of the chosen trace to the bottom-screen value; then new data from the detector is put in the trace with each sweep. .. code-block:: python instr.clear_write_trace('TRA') instr.clear_write_trace(Trace.A) :param trace: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :type trace: str :raises TypeError: Type isn't 'string' :raises ValueError: Value is 'TRA' nor 'TRB' """ if not isinstance(trace, str): raise TypeError("Should be of type string but is '%s'" % type(trace)) if trace not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], trace)) self.write("CLRW " + trace) def set_continuous_sweep(self): """Set the instrument to continuous-sweep mode. This mode enables another sweep at the completion of the current sweep once the trigger conditions are met. """ self.write("CONTS") coupling = Instrument.control( "COUPLE?", "COUPLE %s", """ Control the input coupling of the spectrum analyzer. AC coupling protects the input of the analyzer from damaging dc signals, while limiting the lower frequency-range to 100 kHz (although the analyzer will tune down to 0 Hz with signal attenuation). Type: :code:`str` Takes a representation of the coupling mode, either from :class:`CouplingMode` or use 'AC' / 'DC' .. code-block:: python instr.coupling = 'AC' instr.coupling = CouplingMode.DC if instr.coupling == CouplingMode.DC: pass """, validator=strict_discrete_set, values=[e for e in CouplingMode] ) demodulation_mode = Instrument.control( "DEMOD?", "DEMOD %s", """ Control the demodulation mode of the spectrum analyzer. Either AM or FM demodulation, or turns the demodulation — off. Place a marker on a desired signal and then set :attr:`demodulation_mode`; demodulation takes place on this signal. If no marker is on, :attr:`demodulation_mode` automatically places a marker at the center of the trace and demodulates the frequency at that marker position. Use the volume and squelch controls to adjust the speaker and listen. Type: :code:`str` Takes a representation of the demodulation mode, either from :class:`DemodulationMode` or use 'OFF', 'AM', 'FM' .. code-block:: python instr.demodulation_mode = 'AC' instr.demodulation_mode = DemodulationMode.AM if instr.demodulation_mode == DemodulationMode.FM: instr.demodulation_mode = Demodulation.OFF """, validator=strict_discrete_set, values=[e for e in DemodulationMode] ) demodulation_agc_enabled = Instrument.control( "DEMODAGC?", "DEMODAGC %s", """ Control the demodulation automatic gain control (AGC). The AGC keeps the volume of the speaker relatively constant during AM demodulation. AGC is available only during AM demodulation and when the frequency span is greater than 0 Hz. Type: :code:`bool` .. code-block:: python instr.demodulation_agc = True if instr.demodulation_agc: instr.demodulation_agc = False """, validator=strict_discrete_set, map_values=True, values={True: "1", False: "0"}, cast=str ) demodulation_time = Instrument.control( "DEMODT?", "DEMODT %.11E", """ Control the amount of time that the sweep pauses at the marker to demodulate a signal. The default value is 1 second. When the frequency span equals 0 Hz, demodulation is continuous, except when between sweeps. For truly continuous demodulation, set the frequency span to 0 Hz and the trigger mode to single sweep (see TM). Minimum 100 ms to maximum 60 s Type: :code:`float` .. code-block:: python # set the demodulation time to 1.2 seconds instr.demodulation_time = 1.2 if instr.demodulation_time == 10: pass """, validator=strict_range, values=[100e-3, 60], ) detector_mode = Instrument.control( "DET?", "DET %s", """ Control the IF detector used for acquiring measurement data. This is normally a coupled function, in which the spectrum analyzer selects the appropriate detector mode. Four modes are available: normal, positive, negative, and sample. Type: :code:`str` Takes a representation of the detector mode, either from :class:`DetectionModes` or use 'NEG', 'NRM', 'POS', 'SMP' .. code-block:: python instr.detector_mode = DetectionModes.SMP instr.detector_mode = 'NEG' if instr.detector_mode == DetectionModes.SMP: pass """, validator=strict_discrete_set, values=[e for e in DetectionModes] ) # now implemented as a property but due to the ability of the underlying gpib command to # specify the unit, there would be an alternative implementation as a method to allow the user # to modify the setting unit without manipulating it via the 'amplitude_unit' property display_line = Instrument.control( "DL?", "DL %g.11E {amplitude_unit}", """ Control the horizontal display line for use as a visual aid or for computational purposes. The default value is 0 dBm. Type: :code:`float` Takes a value with the unit of :attr:`amplitude_unit` .. code-block:: python instr.display_line = -10 if instr.display_line == 0: pass """ ) display_line_enabled = Instrument.setting( "DL %s", """ Set the horizontal display line for use as a visual aid either on or off. .. code-block:: python instr.display_line_enabled = False """, map_values=True, validator=strict_discrete_set, values={True: "ON", False: "OFF"} ) done = Instrument.measurement( "DONE?", """ Get back (e.g. return) when all commands in a command string entered before 'done' has been completed. Sending a :meth:`trigger_sweep` command before 'done' ensures that the spectrum analyzer will complete a full sweep before continuing on in a program. Depending on the timeout a timeout error from the adapter will raise before the spectrum analyzer can finish due to an extreme long sweep time .. code-block:: python instr.trigger_sweep() # wait for a full sweep and than 'do_something' if instr.done: do_something() """ ) def check_done(self): """ Return when all commands in a command string entered before :meth:'check_done' has been completed. Sending a :meth:`trigger_sweep` command before 'check_done' ensures that the spectrum analyzer will complete a full sweep before continuing on in a program. Depending on the timeout a timeout error from the adapter will raise before the spectrum analyzer can finish due to an extreme long sweep time. .. code-block:: python instr.trigger_sweep() # wait for a full sweep and than 'do_something' instr.check_done() do_something() """ # no error checking because there is no possibility to return anything else than '1' self.ask("DONE?") errors = Instrument.measurement( "ERR?", """ Get a list of errors present (of type :class:`ErrorCode`). An empty list means there are no errors. Reading 'errors' clears all HP-IB errors. For best results, enter error data immediately after querying for errors. Type: :class:`ErrorCode` .. code-block:: python errors = instr.errors if len(errors) > 0: print(errors[0].code) for error in errors: print(error) if ErrorCode(112) in errors: print("yeah") Example result of this python snippet: .. code-block:: python 112 ErrorCode("??CMD?? - Unrecognized command") ErrorCode("NOP NUM - Command cannot have numeric units") yeah """, cast=ErrorCode, get_process=lambda v: v if isinstance(v, list) else [] ) elapsed_time = Instrument.measurement( "EL?", """ Get the elapsed time (in hours) of analyzer operation. This value can be reset only by Hewlett-Packard. Type: :code:`int` .. code-block:: python print(elapsed_time) 1998 """, cast=int ) start_frequency = Instrument.control( "FA?", "FA %.11E Hz", """ Control the start frequency and set the spectrum analyzer to start-frequency/ stop-frequency mode. If the start frequency exceeds the stop frequency, the stop frequency increases to equal the start frequency plus 100 Hz. The center frequency and span change with changes in the start frequency. Type: :code:`float` .. code-block:: python instr.start_frequency = 300.5e6 if instr.start_frequency == 200e3: print("Correct frequency") """, validator=strict_range, values=[0, 1], dynamic=True ) stop_frequency = Instrument.control( "FB?", "FB %.11E Hz", """ Control the stop frequency and set the spectrum analyzer to start-frequency/ stop-frequency mode. If the stop frequency is less than the start frequency, the start frequency decreases to equal the stop frequency minus 100 Hz. The center frequency and span change with changes in the stop frequency. Type: :code:`float` .. code-block:: python instr.stop_frequency = 300.5e6 if instr.stop_frequency == 200e3: print("Correct frequency") """, validator=strict_range, values=[0, 1], dynamic=True ) sampling_frequency = Instrument.measurement( "FDIAG SMP,?", """ Get the sampling oscillator frequency corresponding to the current start frequency. Diagnostic Attribute Type: :code:`float` """ ) lo_frequency = Instrument.measurement( "FDIAG LO,?", """ Get the first local oscillator frequency corresponding to the current start frequency. Diagnostic Attribute Type: :code:`float` """ ) mroll_frequency = Instrument.measurement( "FDIAG MROLL,?", """ Get the main roller oscillator frequency corresponding to the current start frequency, except then the resolution bandwidth is less than or equal to 100 Hz. Diagnostic Attribute Type: :code:`float` """ ) oroll_frequency = Instrument.measurement( "FDIAG OROLL,?", """ Get the offset roller oscillator frequency corresponding to the current start frequency, except when the resolution bandwidth is less than or equal to 100 Hz. Diagnostic Attribute Type: :code:`float` """ ) xroll_frequency = Instrument.measurement( "FDIAG XROLL,?", """ Get the transfer roller oscillator frequency corresponding to the current start frequency, except when the resolution bandwidth is less than or equal to 100 Hz. Diagnostic Attribute Type: :code:`float` """ ) sampler_harmonic_number = Instrument.measurement( "FDIAG HARM,?", """ Get the sampler harmonic number corresponding to the current start frequency. Diagnostic Attribute Type: :code:`int` """, get_process=lambda v: int(float(v)) ) # practically you could also write "OFF" to actively disable it or reset via "IP" frequency_display_enabled = Instrument.measurement( "FDSP?", """ Get the state of all annotations that describes the spectrum analyzer frequency. returns 'False' if no annotations are shown and vice versa 'True'. This includes the start and stop frequencies, the center frequency, the frequency span, marker readouts, the center frequency step-size, and signal identification to center frequency. To retrieve the frequency data, query the spectrum analyzer. Type: :code:`bool` .. code-block:: python if instr.frequency_display: print("Frequencies get displayed") """, map_values=True, values={True: "1", False: "0"}, cast=str ) def do_fft(self, source, destination, window): """Calculate and show a discrete Fourier transform. The FFT command performs a discrete Fourier transform on the source trace array and stores the logarithms of the magnitudes of the results in the destination array. The maximum length of any of the traces is 601 points. FFT is designed to be used in transforming zero-span amplitude-modulation information into the frequency domain. Performing an FFT on a frequency sweep will not provide time-domain results. The FFT results are displayed on the spectrum analyzer in a logarithmic amplitude scale. For the horizontal dimension, the frequency at the left side of the graph is 0 Hz, and at the right side is Finax- Fmax is equal to 300 divided by sweep time. As an example, if the sweep time of the analyzer is 60 ms, Fmax equals 5 kHz. The FFT algorithm assumes that the sampled signal is periodic with an integral number of periods within the time-record length (that is, the sweep time of the analyzer). Given this assumption, the transform computed is that of a time waveform of infinite duration, formed of concatenated time records. In actual measurements, the number of periods of the sampled signal within the time record may not be integral. In this case, there is a step discontinuity at the intersections of the concatenated time records in the assumed time waveform of infinite duration. This step discontinuity causes measurement errors, both amplitude uncertainty (where the signal level appears to vary with small changes in frequency) and frequency resolution (due to filter shape factor and sidelobes). Windows are weighting functions that are applied to the input data to force the ends of that data smoothly to zero, thus reducing the step discontinuity and reducing measuremen errors. :param source: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :param destination: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :param window: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :type source: str :type destination: str :type window: str """ if not isinstance(source, str): raise TypeError("Should be of type string but is '%s'" % type(source)) if not isinstance(destination, str): raise TypeError("Should be of type string but is '%s'" % type(destination)) if not isinstance(window, str): raise TypeError("Should be of type string but is '%s'" % type(window)) if source not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], source)) if destination not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], destination)) if window not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], window)) self.write("FFT %s,%s,%s" % (source, destination, window)) frequency_offset = Instrument.control( "FOFFSET?", "FOFFSET %.11E Hz", """ Control an offset added to the displayed absolute-frequency values, including marker-frequency values. It does not affect the frequency range of the sweep, nor does it affect relative frequency readouts. When this function is active, an "F" appears on the left side of the display. Changes all the following frequency measurements. Type: :code:`float` .. code-block:: python instr.frequency_offset = 2e6 if instr.frequency_offset == 2e6: print("Correct frequency") """, validator=strict_range, values=[0, 1], dynamic=True ) frequency_reference_source = Instrument.control( "FREF?", "FREF %s", """ Control the frequency reference source. Select either the internal frequency reference (INT) or supply your own external reference (EXT). An external reference must be 10 MHz (+100 Hz) at a minimum amplitude of 0 dBm. Connect the external reference to J9 (10 MHz REF IN/OUT) on the rear panel. When the external mode is selected, an "X" appears on the left edge of the display. Type: :code:`str` Takes element of :class:`FrequencyReference` or use 'INT', 'EXT' .. code-block:: python instr.frequency_reference_source = 'INT' instr.frequency_reference_source = FrequencyReference.EXT if instr.frequency_reference_source == FrequencyReference.INT: instr.frequency_reference_source = FrequencyReference.EXT """, validator=strict_discrete_set, values=[e for e in FrequencyReference] ) def set_full_span(self): """Set the spectrum analyzer to the full frequency span as defined by the instrument. The full span is 2.9 GHz for the HP 8560A. For the HP 8561B, the full span is 6.5 GHz. """ self.write("FS") graticule_enabled = Instrument.control( "GRAT?", "GRAT %s", """ Control the display graticule. Switch it either on or off. Type: :class:`bool` .. code-block:: python instr.graticule = True if instr.graticule: pass """, map_values=True, values={True: "1", False: "0"}, validator=strict_discrete_set, cast=str ) def hold(self): """Freeze the active function at its current value. If no function is active, no operation takes place. """ self.write("HD") id = Instrument.measurement( "ID?", """ Get the identification of the device with software and hardware revision (e.g. HP8560A,002, H03) Type: :class:`str` .. code-block:: python print(instr.id) HP8560A,002,H02 """, maxsplit=0, cast=str ) def preset(self): """Set the spectrum analyzer to a known, predefined state. 'preset' does not affect the contents of any data or trace registers or stored preselector data. 'preset' does not clear the input or output data buffers; """ self.write("IP") logarithmic_scale = Instrument.control( "LG?", "LG %d DB", """ Control the logarithmic amplitude scale. When in linear mode, querying 'logarithmic_scale' returns a “0”. Allowed values are 0, 1, 2, 5, 10 Type: :class:`int` .. code-block:: python if instr.logarithmic_scale: pass # set the scale to 10 db per division instr.logarithmic_scale = 10 """, cast=int, validator=strict_discrete_set, values=[0, 1, 2, 5, 10] ) def set_linear_scale(self): """Set the spectrum analyzers display to linear amplitude scale. Measurements made on a linear scale can be read out in any units. """ self.write("LN") def set_minimum_hold(self, trace): """Update the chosen trace with the minimum signal level detected at each trace-data point from subsequent sweeps. This function employs the negative peak detector (refer to the :attr:`detector_mode` command). .. code-block:: python instr.minimum_hold('TRA') instr.minimum_hold(Trace.A) :param trace: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :type trace: str :raises TypeError: Type isn't 'string' :raises ValueError: Value is 'TRA' nor 'TRB' """ if not isinstance(trace, str): raise TypeError("Should be of type string but is '%s'" % type(trace)) if trace not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], trace)) self.write("MINH %s" % trace) marker_amplitude = Instrument.measurement( "MKA?", """ Get the amplitude of the active marker. If no marker is active, MKA places a marker at the center of the trace and returns that amplitude value. In the :meth:`amplitude_unit` unit. Type: :code:`float` .. code-block:: python level = instr.marker_amplitude unit = instr.amplitude_unit print("Level: %f %s" % (level, unit)) """ ) def set_marker_to_center_frequency(self): """Set the center frequency to the frequency value of an active marker.""" self.write("MKCF") marker_delta = Instrument.control( "MKD?", "MKD %.11E Hz", """ Control a second marker on the trace. The parameter value specifies the distance in frequency or time (when in zero span) between the two markers. If queried - returns the frequency or time of the second marker. Type: :code:`float` .. code-block:: python # place second marker 1 MHz apart from the first marker instr.marker_delta = 1e6 # print frequency of second marker in case it got moved automatically print(instr.marker_delta) """ ) # the documentation mentions this command, but it doesn't work on my unit and a # reference unit so I leave it here for reference but commented out # # marker_reciprocal = Instrument.control( # "MKDR?", "MKDR %.11E", # """ # Return the reciprocal of the frequency or time (when in zero span) # difference between two markers. # """ # ) marker_frequency = Instrument.control( "MKF?", "MKF %.11E Hz", """ Control the frequency of the active marker. Default units are in Hertz. Type: :code:`float` .. code-block:: python # place marker no. 1 at 100 MHz instr.marker_frequency = 100e6 # print frequency of the marker in case it got moved automatically print(instr.marker_frequency) """, validator=strict_range, values=[0, 1], dynamic=True ) frequency_counter_mode_enabled = Instrument.setting( "MKFC %s", """ Set the device into a frequency counter mode that counts the frequency of the active marker or the difference in frequency between two markers. If no marker is active, 'frequency_counter_mode_enabled' places a marker at the center of the trace and counts that marker frequency. The frequency counter provides a more accurate frequency reading; it pauses at the marker, counts the value, then continues the sweep. To adjust the frequency counter resolution, use the 'frequency_counter_resolution' command. To return the counter value, use the 'marker_frequency' command. .. code-block:: python instr.frequency_counter_mode_enabled = True """, map_values=True, values={True: "ON", False: "OFF"}, validator=strict_discrete_set ) frequency_counter_resolution = Instrument.control( "MKFCR?", "MKFCR %d Hz", """ Control the resolution of the frequency counter. Refer to the 'frequency_counter_mode' command. The default value is 10 kHz. Type :code:`int` .. code-block:: python # activate frequency counter mode instr.frequency_counter_mode = True # adjust resolution to 1 Hz instr.frequency_counter_resolution = 1 if instr.frequency_counter_resolution: pass """, validator=strict_range, values=[1, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6], maxsplit=0, preprocess_reply=lambda v: str(int(float(v))), cast=int ) def set_marker_minimum(self): """Place an active marker on the minimum signal detected on a trace.""" self.write("MKMIN") # here would be the implementation of the command 'marker_normal' ('MKN') but # it has no advantage over the 'marker_frequency' command except if no marker is active it # places it automagically to the center of the trace (I think there's no sense in # implementing it here) marker_noise_mode_enabled = Instrument.control( "MKNOISE?", "MKNOISE %s", """ Control the detector mode to sample and compute the average of 32 data points (16 points on one side of the marker, the marker itself, and 15 points on the other side of the marker). This average is corrected for effects of the log or linear amplifier, bandwidth shape factor, IF detector, and resolution bandwidth. If two markers are on (whether in 'marker_delta' mode or 1/marker delta mode), 'marker_noise_mode_enabled' works on the active marker and not on the anchor marker. This allows you to measure signal-to-noise density directly. To query the value, use the 'marker_amplitude' command. Type: :code:`bool` .. code-block:: python # activate signal-to-noise density mode instr.marker_noise_mode_enabled = True # get noise density by `marker_amplitude` print("Signal-to-noise density: %d dbm / Hz" % instr.marker_amplitude) """, map_values=True, values={True: "1", False: "0"}, cast=str ) def deactivate_marker(self, all_markers=False): """Turn off the active marker or, if specified, turn off all markers. :param all_markers: If True the call deactivates all markers, if false only the currently active marker (optional) :type all_markers: bool .. code-block:: python # place first marker at 300 MHz instr.marker_frequency = 300e6 # place second marker 2 MHz apart from first instr.marker_delta = 2e6 # deactivate active marker (delta marker) instr.deactivate_marker() # deactivate all markers instr.deactivate_marker(all_markers=True) """ if all_markers: self.write("MKOFF ALL") else: self.write("MKOFF") def search_peak(self, mode): """Place a marker on the highest point on a trace, the next-highest point, the next-left peak, or the next-right peak. The default is 'HI' (highest point). The trace peaks must meet the criteria of the marker threshold and peak excursion functions in order for a peak to be found. See also the :attr:`peak_threshold` and :attr:`peak_excursion` commands. :param mode: Takes 'HI', 'NH', 'NR', 'NL' or the enumeration :class:`PeakSearchMode` :type mode: str .. code-block:: python instr.search_peak('NL') instr.search_peak(PeakSearchMode.NextHigh) """ if not isinstance(mode, str): raise TypeError("Should be of type string but is '%s'" % type(mode)) if mode not in [e for e in PeakSearchMode]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in PeakSearchMode], mode)) self.write("MKPK %s" % mode) marker_threshold = Instrument.control( "MKPT?", "MKPT %g {amplitude_unit}", """ Control the minimum amplitude level from which a peak on the trace can be detected. The default value is -130 dBm. See also the :attr:`peak_excursion` command. Any portion of a peak that falls below the peak threshold is used to satisfy the peak excursion criteria. For example, a peak that is equal to 3 dB above the threshold when the peak excursion is equal to 6 dB will be found if the peak extends an additional 3 dB or more below the threshold level. Maximum 30 db to minimum -200 db. Type: :code:`signed int` .. code-block:: python instr.marker_threshold = -70 if instr.marker_threshold > -80: pass """, validator=strict_range, values=[-200, 30] ) peak_excursion = Instrument.control( "MKPX?", "MKPX %g DB", """ Control what constitutes a peak on a trace. The chosen value specifies the amount that a trace must increase monotonically, then decrease monotonically, in order to be a peak. For example, if the peak excursion is 10 dB, the amplitude of the sides of a candidate peak must descend at least 10 dB in order to be considered a peak (see Figure 5-4) The default value is 6 dB. In linear mode, enter the marker peak excursion as a unit-less number. Any portion of a peak that falls below the peak threshold is also used to satisfy the peak excursion criteria. For example, a peak that is equal to 3 dB above the threshold when the peak excursion is equal to 6 dB will be found if the peak extends an additional 3 dB or more below the threshold level. Type: :code:`float` .. code-block:: python instr.peak_excursion = 2 if instr.peak_excursion == 2: pass """, validator=strict_range, values=[0.1, 99] ) def set_marker_to_reference_level(self): """Set the reference level to the amplitude of an active marker. If no marker is active, 'marker_to_reference_level' places a marker at the center of the trace and uses that marker amplitude to set the reference level. """ self.write("MKRL") def set_marker_delta_to_span(self): """Set the frequency span equal to the frequency difference between two markers on a trace. The start frequency is set equal to the frequency of the left- most marker and the stop frequency is set equal to the frequency of the right-most marker. """ self.write("MKSP") def set_marker_to_center_frequency_step_size(self): """Set the center frequency step-size equal to the frequency value of the active marker.""" self.write("MKSS") marker_time = Instrument.control( "MKT?", "MKT %gS", """ Control the marker's time value. Default units are seconds. Type: :code:`float` .. code-block:: python # set marker at sweep time corresponding second two instr.marker_time = 2 if instr.marker_time == 2: pass """ ) marker_signal_tracking_enabled = Instrument.control( "MKTRACK?", "MKTRACK %s", """ Control whether the center frequency follows the active marker. This is done after every sweep, thus maintaining the marker value at the center frequency. This allows you to “zoom in” quickly from a wide span to a narrow one, without losing the signal from the screen. Or, use 'marker_signal_tracking_enabled' to keep a slowly drifting signal centered on the display. When this function is active, a "K" appears on the left edge of the display. Type: :code:`bool` """, map_values=True, validator=strict_discrete_set, values={True: "1", False: "0"}, cast=str ) mixer_level = Instrument.control( "ML?", "ML %d DB", """ Control the maximum signal level that is at the input mixer. The attenuator automatically adjusts to ensure that this level is not exceeded for signals less than the reference level. From -80 to -10 DB. Type: :code:`int` """, validator=strict_range, cast=int, values=[-80, -10] ) def set_maximum_hold(self, trace): """Set the chosen trace with the maximum signal level detected at each trace-data point from subsequent sweeps. This function employs the positive peak detector (refer to the :attr:`detector_mode` command). The detector mode can be changed, if desired, after max hold is initialized. .. code-block:: python instr.maximum_hold('TRA') instr.maximum_hold(Trace.A) :param trace: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :type trace: str :raises TypeError: Type isn't 'string' :raises ValueError: Value is 'TRA' nor 'TRB' """ if not isinstance(trace, str): raise TypeError("Should be of type string but is '%s'" % type(trace)) if trace not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], trace)) self.write("MXMH %s" % trace) normalize_trace_data_enabled = Instrument.control( "NORMLIZE?", "NORMLIZE %s", """ Control the normalization routine for stimulus-response measurements. This function subtracts trace B from trace A, offsets the result by the value of the normalized reference position (:attr:`normalized_reference_level`), and displays the result in trace A. 'normalize_trace_data_enabled' is intended for use with the :meth:`store_open` and :meth:`store_short` or :meth:`store_thru` commands. These functions are used to store a reference trace into trace B. Refer to the respective command descriptions for more information. Accurate normalization occurs only if the reference trace and the measured trace are on-screen. If any of these traces are off-screen, an error message will be displayed. If the error message ERR 903 A > DLMT is displayed, the range level (RL) can be adjusted to move the measured response within the displayed measurement range of the analyzer. If ERR 904 B > DLMT is displayed, the calibration is invalid and a thru or open/short calibration must be performed. If active (ON), the 'normalize_trace_data' command is automatically turned off with an instrument preset (IP) or at power on. Type: :code:`bool` """, map_values=True, validator=strict_discrete_set, values={True: "1", False: "0"}, cast=str ) normalized_reference_level = Instrument.control( "NRL?", "NRL %d {amplitude_unit}", """ Control the normalized reference level. It is intended to be used with the :attr:`normalize_trace_data` command. When using 'normalized_reference_level', the input attenuator and IF step gains are not affected. This function is a trace-offset function enabling the user to offset the displayed trace without introducing hardware-switching errors into the stimulus-response measurement. The unit of measure for 'normalized_reference_level' is dB. In absolute power mode (dBm), reference level ( :attr:`reference_level`) affects the gain and RF attenuation settings of the instrument, which affects the measurement or dynamic range. In normalized mode (relative power or dB-measurement mode), NRL offsets the trace data on-screen and does not affect the instrument gain or attenuation settings. This allows the displayed normalized trace to be moved without decreasing the measurement accuracy due to changes in gain or RF attenuation. If the measurement range must be changed to bring trace data on-screen, then the range level should be adjusted. Adjusting the range-level normalized mode has the same effect on the instrument settings as does reference level in absolute power mode (normalize off). Type: :code:`int` .. code-block:: python # reference level in case of normalization to -30 DB instr.normalized_reference_level = -30 if instr.normalized_reference_level == -30: pass """, validator=strict_range, values=[-200, 30], cast=int ) normalized_reference_position = Instrument.control( "NRPOS?", "NRPOS %f DB", """ Control the normalized reference-position that corresponds to the position on the graticule where the difference between the measured and calibrated traces resides. The dB value of the normalized reference-position is equal to the normalized reference level. The normalized reference-position may be adjusted between 0.0 and 10.0, corresponding to the bottom and top graticule lines, respectively. Type: :code:`float` .. code-block:: python instr.normalized_reference_position = 5.5 if instr.normalized_reference_position == 5.5: pass """, validator=strict_range, values=[0.0, 10.0] ) display_parameters = Instrument.measurement( "OP?", """ Get the location of the lower left (P1) and upper right (P2) vertices as a tuple of the display window. Type: :code:`tuple` .. code-block:: python repr(instr.display_parameters) (72, 16, 712, 766) """, maxsplit=4, cast=int, get_process=tuple ) def plot(self, p1x, p1y, p2x, p2y): """Copies the specified display contents onto any HP-GL plotter. Set the plotter address to 5, select the Pi and P2 positions, and then execute the plot command. P1 and P2 correspond to the lower-left and upper-right plotter positions, respectively. If P1 and P2 are not specified, default values (either preloaded from power-up or sent in via a previous plot command) are used. Once PLOT is executed, no subsequent commands are executed until PLOT is done. :param p1x: plotter-dependent value that specify the lower-left plotter position x-axis :type p1x: int :param p1y: plotter-dependent value that specify the lower-left plotter position y-axis :type p1y: int :param p2x: plotter-dependent values that specify the upper-right plotter position x-axis :type p2x: int :param p2y: plotter-dependent values that specify the upper-right plotter position y-axis :type p2y: int """ if not (isinstance(p1x, int) or isinstance(p1y, int) or isinstance(p2x, int) or isinstance(p2y, int)): raise TypeError("Should be of type int") self.write("PLOT %d,%d,%d,%d" % (p1x, p1y, p2x, p2y)) protect_state_enabled = Instrument.control( "PSTATE?", "PSTATE %s", """ Control the storing of any new data in the state or trace registers. If set to 'True', the registers are “locked”; the data in them cannot be erased or overwritten, although the data can be recalled. To “unlock” the registers, and store new data, set 'protect_state_enabled' to off by selecting 'False' as the parameter. Type: :code:`bool` """, map_values=True, validator=strict_discrete_set, values={True: "1", False: "0"}, cast=str ) def get_power_bandwidth(self, trace, percent): """Measure the combined power of all signal responses contained in a trace array. The command then computes the bandwidth equal to a percentage of the total power. For example, if 100% is specified, the power bandwidth equals the current frequency span. If 50% is specified, trace elements are eliminated from either end of the array, until the combined power of the remaining trace elements equals half of the total power computed. The frequency span of these remaining trace elements is the power bandwidth output to the controller. :param trace: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :param percent: Percentage of total power 0 ... 100 % :type trace: str :type percent: float .. code-block:: python # reset spectrum analyzer instr.preset() # set to single sweep mode instr.sweep_single() instr.center_frequency = 300e6 instr.span = 1e6 instr.maximum_hold() instr.trigger_sweep() if instr.done: pbw = instr.power_bandwidth(Trace.A, 99.0) print("The power bandwidth at 99 percent is %f kHz" % (pbw / 1e3)) """ ran = np.arange(0, 100, 0.1) if not isinstance(trace, str): raise TypeError("Should be of type string but is '%s'" % type(trace)) if not isinstance(percent, float): raise TypeError("Should be of type float but is '%s'" % type(percent)) if trace not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], trace)) if percent not in ran: raise ValueError("Only accepts values in the range of %s but was '%s'" % (ran, percent)) return float(self.ask("PWRBW %s,%.1f?" % (trace, percent))) resolution_bandwidth = Instrument.control( "RB?", "RB %s", """ Control the resolution bandwidth. This is normally a coupled function that is selected according to the ratio selected by the RBR command. If no ratio is selected, a default ratio (0.011) is used. The bandwidth, which ranges from 10 Hz to 2 MHz, may also be selected manually. Type: :code:`str, dec` """, validator=joined_validators(strict_discrete_set, truncated_discrete_set), values=[["AUTO", "MAN"], np.arange(10, 2e6)], set_process=lambda v: v if isinstance(v, str) else f"{int(v)} Hz", get_process=lambda v: v if isinstance(v, str) else int(v) ) resolution_bandwidth_to_span_ratio = Instrument.control( "RBR?", "RBR %.3f", """ Control the coupling ratio between the resolution bandwidth and the frequency span. When the frequency span is changed, the resolution bandwidth is changed to satisfy the selected ratio. The ratio ranges from 0.002 to 0.10. The “UP” and “DN” parameters adjust the ratio in a 1, 2, 5 sequence. The default ratio is 0.011. """, validator=strict_range, values=np.arange(0.002, 0.10, 0.001) ) def recall_open_short_average(self): """Set the internally stored open/short average reference trace into trace B. The instrument state is also set to the stored open/short reference state. .. code-block:: python instr.preset() instr.sweep_single() instr.start_frequency = 300e3 instr.stop_frequency = 1e9 instr.source_power_enabled = True instr.sweep_couple = SweepCoupleMode.StimulusResponse instr.source_peak_tracking() input("CONNECT OPEN. PRESS CONTINUE WHEN READY TO STORE.") instr.trigger_sweep() instr.done() instr.store_open() input("CONNECT SHORT. PRESS CONTINUE WHEN READY TO STORE AND AVERAGE.") instr.trigger_sweep() instr.done() instr.store_short() input("RECONNECT DUT. PRESS CONTINUE WHEN READY.") instr.trigger_sweep() instr.done() instr.normalize = True instr.trigger_sweep() instr.done() instr.normalized_reference_position = 8 instr.trigger_sweep() instr.preset() # demonstrate recall of open/short average trace instr.recall_open_short_average() instr.trigger_sweep() """ self.write("RCLOSCAL") def recall_state(self, inp): """Set to the display a previously saved instrument state. See :meth:`save_state`. :param inp: State to be recalled: either storage slot 0 ... 9 or 'LAST' or 'PWRON' :param inp: str, int .. code-block:: python instr.save_state(7) instr.preset() instr.recall_state(7) """ values = ["LAST", "PWRON"] + [str(f) for f in range(0, 9)] if not (isinstance(inp, str) or isinstance(inp, int)): raise TypeError("Should be of type 'str' or 'int' but is '%s'" % type(inp)) if str(inp) not in values: raise ValueError("Only accepts values of [%s] but was '%s'" % (values, str(inp))) self.write("RCLS %s" % str(inp)) def recall_trace(self, trace, number): """Recalls previously saved trace data to the display. See :meth:`save_trace`. Either as Trace A or Trace B. :param trace: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :param number: Storage location from 0 ... 7 where to store the trace :type trace: str :type number: int .. code-block:: python instr.preset() instr.center_frequency = 300e6 instr.span = 20e6 instr.save_trace(Trace.A, 7) instr.preset() # reload - at 7 stored trace - to Trace B instr.recall_trace(Trace.B, 7) """ ran = range(0, 7) if not isinstance(trace, str): raise TypeError("Should be of type str but is '%s'" % type(trace)) if not isinstance(number, int): raise TypeError("Should be of type int but is '%s'" % type(number)) if trace not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], trace)) if number not in ran: raise ValueError("Only accepts values of [%s] but was '%s'" % (ran, number)) self.write("RCLT %s,%s" % (trace, number)) def recall_thru(self): """Recalls the internally stored thru-reference trace into trace B. The instrument state is also set to the stored thru-reference state. """ self.write("RCLTHRU") firmware_revision = Instrument.measurement( "REV?", """ Get the revision date code of the spectrum analyzer firmware. Type: :code:`datetime.date` """, get_process=lambda v: datetime.strptime(v, '%y%m%d').date(), cast=str ) reference_level = Instrument.control( "RL?", "RL %g {amplitude_unit}", """ Control the reference level, or range level when in normalized mode. (Range level functions the same as reference level.) The reference level is the top horizontal line on the graticule. For best measurement accuracy, place the peak of a signal of interest on the reference-level line. The spectrum analyzer input attenuator is coupled to the reference level and automatically adjusts to avoid compression of the input signal. Refer also to :attr:`amplitude_unit`. Minimum reference level is -120.0 dBm or 2.2 uV Type: :code:`float` """ ) reference_level_calibration = Instrument.control( "RLCAL?", "RLCAL %g", """ Control the calibration of the reference level remotely and retuns the current calibration. To calibrate the reference level, connect the 300 MHz calibration signal to the RF input. Set the center frequency to 300 MHz, the frequency span to 20 MHz, and the reference level to -10 dBm. Use the RLCAL command to move the input signal to the reference level. When the signal peak falls directly on the reference-level line, the reference level is calibrated. Storing this value in the analyzer in EEROM can be done only from the front panel. The RLCAL command, when queried, returns the current value. Type: :code:`float` .. code-block:: python # connect cal signal to rf input instr.preset() instr.amplitude_unit = AmplitudeUnits.DBM instr.center_frequency = 300e6 instr.span = 100e3 instr.reference_level = 0 instr.trigger_sweep() instr.peak_search(PeakSearchMode.High) level = instr.marker_amplitude rlcal = instr.reference_level_calibration - int((level + 10) / 0.17) instr.reference_level_calibration = rlcal """, cast=int, validator=strict_range, values=[-33, 33] ) reference_offset = Instrument.control( "ROFFSET?", "ROFFSET %d DB", """ Control an offset applied to all amplitude readouts (for example, the reference level and marker amplitude). The offset is in dB, regardless of the selected scale and units. The offset can be useful to account for gains of losses in accessories connected to the input of the analyzer. When this function is active, an "R" appears on the left edge of the display. Type: :code:`int` """, cast=int, values=[-100, 100], validator=strict_range ) request_service_conditions = Instrument.control( "RQS?", "RQS %d", """ Control a bit mask that specifies which service requests can interrupt a program sequence. .. code-block:: python instr.request_service_conditions = StatusRegister.ERROR_PRESENT | StatusRegister.TRIGGER print(instr.request_service_conditions) StatusRegister.ERROR_PRESENT|TRIGGER """, get_process=lambda v: StatusRegister(int(v)) ) def save_state(self, inp): """Saves the currently displayed instrument state in the specified state register. :param inp: State to be recalled: either storage slot 0 ... 9 or 'LAST' or 'PWRON' :param inp: str, int .. code-block:: python instr.preset() instr.center_frequency = 300e6 instr.span = 20e6 instr.save_state("PWRON") """ values = ["PWRON"] + [str(f) for f in range(0, 9)] if not (isinstance(inp, str) or isinstance(inp, int)): raise TypeError("Should be of type 'str' or 'int' but is '%s'" % type(inp)) if str(inp) not in values: raise ValueError("Only accepts values of [%s] but was '%s'" % (values, str(inp))) self.write("SAVES %s" % str(inp)) def save_trace(self, trace, number): """Saves the selected trace in the specified trace register. :param trace: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :param number: Storage location from 0 ... 7 where to store the trace :type trace: str :type number: int .. code-block:: python instr.preset() instr.center_frequency = 300e6 instr.span = 20e6 instr.save_trace(Trace.A, 7) instr.preset() # reload - at 7 stored trace - to Trace B instr.recall_trace(Trace.B, 7) """ ran = range(0, 7) if not isinstance(trace, str): raise TypeError("Should be of type str but is '%s'" % type(trace)) if not isinstance(number, int): raise TypeError("Should be of type int but is '%s'" % type(number)) if trace not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], trace)) if number not in ran: raise ValueError("Only accepts values of [%s] but was '%s'" % (ran, number)) self.write("SAVET %s,%s" % (trace, number)) serial_number = Instrument.measurement( "SER?", """ Get the spectrum analyzer serial number. """, cast=str ) def sweep_single(self): """Sets the spectrum analyzer into single-sweep mode. This mode allows only one sweep when trigger conditions are met. When this function is active, an 'S' appears on the left edge of the display. """ self.write("SNGLS") span = Instrument.control( "SP?", "SP %s", """ Control the frequency span. The center frequency does not change with changes in the frequency span; start and stop frequencies do change. Setting the frequency span to 0 Hz effectively allows an amplitude-versus-time mode in which to view signals. This is especially useful for viewing modulation. Querying SP will leave the analyzer in center frequency /span mode. """, validator=joined_validators(strict_discrete_set, strict_range), values=[["FULL", "ZERO"], [float("-inf"), float("inf")]], set_process=lambda v: v if isinstance(v, str) else "%.11E Hz" % v, get_process=lambda v: v if isinstance(v, str) else v ) squelch = Instrument.control( "SQUELCH?", "SQUELCH %s", """ Control the squelch level for demodulation. When this function is on, a dashed line indicating the squelch level appears on the display. A marker must be active and above the squelch line for demodulation to occur. Refer to the :attr:`demodulation_mode` command. The default value is -120 dBm. Type: :code:`str,int` .. code-block:: python instr.preset() instr.start_frequency = 88e6 instr.stop_frequency = 108e6 instr.peak_search(PeakSearchMode.High) instr.demodulation_time = 10 instr.squelch = -60 instr.demodulation_mode = DemodulationMode.FM """, validator=joined_validators(strict_discrete_set, strict_range), values=[["ON", "OFF"], range(-220, 30)], set_process=lambda v: v if isinstance(v, str) else f"{v} {{amplitude_unit}}" ) squelch_enabled = Instrument.setting( "SQUELCH %s", """ Set squelch for demodulation active or inactive. For further information see :attr:`squelch` """, map_values=True, values={True: "ON", False: "OFF"}, validator=strict_discrete_set ) def request_service(self, input): """Triggers a service request. This command allows you to force a service request and test a program designed to handle service requests. However, the service request can be triggered only if it is first masked using the :attr:`request_service_conditions` command. :param input: Bits to emulate a service request :type input: :class:`StatusRegister` """ if input not in range(0, 255): raise ValueError("Bit mask needs to be between 0 ... 255") self.write("SRQ %d" % input) # `center_frequency_step_size` would be a command but is pretty unnecesary sweep_time = Instrument.control( "ST?", "ST %s", """ Control the sweep time. This is normally a coupled function which is automatically set to the optimum value allowed by the current instrument settings. Alternatively, you may specify the sweep time. Note that when the specified sweep time is too fast for the current instrument settings, the instrument is no longer calibrated and the message 'MEAS UNCAL' appears on the display. The sweep time cannot be adjusted when the resolution bandwidth is set to 10 Hz, 30 Hz, or 100 Hz. Type: :code:`str, float` Real from 50E—3 to 100 when the span is greater than 0 Hz; 50E—6 to 60 when the span equals 0 Hz. When the resolution bandwidth is <100 Hz, the sweep time cannot be adjusted. """, validator=joined_validators(strict_discrete_set, strict_range), values=[["AUTO", "MAN"], np.arange(50E-6, 100)], set_process=lambda v: v if isinstance(v, str) else ("%.3f S" % v) ) status = Instrument.measurement( "STB?", """ Get the decimal equivalent of the bits set in the status byte (see the RQS and SRQ commands). STB is equivalent to a serial poll command. The RQS and associated bits are cleared in the same way that a serial poll command would clear them. """, get_process=lambda v: StatusRegister(int(v)) ) def store_open(self): """Save the current instrument state and trace A into nonvolatile memory. This command must be used in conjunction with the :meth:`store_short` command and must precede the :meth:`store_short` command. The data obtained during the store open procedure is averaged with the data obtained during the :meth:`store_short` procedure to provide an open/short calibration. The instrument state (that is, instrument settings) must not change between the :meth:`store_open` and :meth:`store_short` operations in order for the open/short calibration to be valid. Refer to the :meth:`store_short` command description for more information. """ self.write("STOREOPEN") def store_short(self): """Take currently displayed trace A data and averages this data with previously stored open data, and stores it in trace B. This command is used in conjunction with the :meth:`store_open` command and must be preceded by it for proper operation. Refer to the :meth:`store_open` command description for more information. The state of the open/short average trace is stored in state register #8. """ self.write("STORESHORT") def store_thru(self): """Store a thru-calibration trace into trace B and into the nonvolatile memory of the spectrum analyzer. The state of the thru information is stored in state register #9. """ self.write("STORETHRU") sweep_couple = Instrument.control( "SWPCPL?", "SWPCPL %s", """ Control the sweep couple mode which is either a stimulus-response or spectrum-analyzer auto-coupled sweep time. In stimulus-response mode, auto-coupled sweep times are usually much faster for swept-response measurements. Stimulus-response auto-coupled sweep times are typicallly valid in stimulus-response measurements when the system’s frequency span is less than 20 times the bandwidth of the device under test. Type: :code:`str` or :class:`SweepCoupleMode` """, validator=strict_discrete_set, values=[e for e in SweepCoupleMode] ) sweep_output = Instrument.control( "SWPOUT?", "SWPOUT %s", """ Control the sweep-related signal that is available from J8 on the rear panel. FAV provides a dc ramp of 0.5V/GHz. RAMP provides a 0—10 V ramp corresponding to the sweep ramp that tunes the first local oscillator (LO). For the HP 8561B, in multiband sweeps one ramp is provided for each frequency band. Type: :code:`str` or :class:`SweepOut` """, validator=strict_discrete_set, values=[e for e in SweepOut] ) trace_data_format = Instrument.control( "TDF?", "TDF %s", """ Control the format used to input and output trace data (see the TRA/TRB command, You must specify the desired format when transferring data from the spectrum analyzer to a computer; this is optional when transferring data to the analyzer. Type: :code:`str` or :class:`TraceDataFormat` .. warning:: Only needed for manual read out of trace data. Don't use this if you don't know what You are doing. """, validator=strict_discrete_set, values=[e for e in TraceDataFormat] ) threshold = Instrument.control( "TH?", "TH %.2E {amplitude_unit}", """ Control the minimum amplitude level and clips data at this value. Default value is -90 dBm. See also - :attr:`marker_threshold` does not clip data below its threshold Type: :code:`str, float` range -200 to 30 .. note:: When a trace is in max-hold mode, if the threshold is raised above any of the trace data, the data below the threshold will be permanently lost. """, validator=strict_discrete_set, values=np.arange(-200, 30), ) threshold_enabled = Instrument.setting( "TH %s", """ Set the threshold active or inactive. See :attr:`threshold` """, map_values=True, values={True: "ON", False: "OFF"}, validator=strict_discrete_set ) def set_title(self, string): """Sets character data in the title area of the display, which is in the upper-right corner. A title can be up to two rows of sixteen characters each, Carriage return and line feed characters are not allowed. """ if not isinstance(string, str): raise TypeError("Parameter should be of type 'str'") if len(string) > 32: raise ValueError("Title should have maximum 32 chars but has '%d'" % len(string)) self.write("TITLE@%s@" % string) trigger_mode = Instrument.control( "TM?", "TM %s", """ Control the trigger mode. Selected trigger conditions must be met in order for a sweep to occur. For the available modes refer to :class:`TriggerMode`. When any trigger mode other than free run is selected, a "T" appears on the left edge of the display. """, validator=strict_discrete_set, values=[e for e in TriggerMode] ) def _get_trace_data(self, trace): self.write("TDF M") amp_units = str(self.ask("AUNITS?")) ref_lvl = float(self.ask("RL?")) log_scale = float(self.ask("LG?")) cmd_str = "" if trace is Trace.A: cmd_str += "TRA?" elif trace is Trace.B: cmd_str += "TRB?" values = self.values(cmd_str, cast=int) if amp_units is AmplitudeUnits.W: # calculate dbm from watts ref_lvl = (10 * log10(ref_lvl)) + 30 elif amp_units is AmplitudeUnits.DBUV: # calculate dbm from dbuv in 50 Ohm system ref_lvl = ref_lvl - 107 elif amp_units is AmplitudeUnits.V: # calculate dbm from volts in 50 Ohm system ref_lvl = 20 * log10((ref_lvl / 0.05) ** 0.5) elif amp_units is AmplitudeUnits.DBMV: # calculate dbm from dbmv ref_lvl = ref_lvl - 46.9897 result_values = [] for value in values: if log_scale != 0: result_value = round(ref_lvl + (log_scale * ((value - 600) / 60)), 2) result_values.append(result_value) else: raise NotImplementedError("Linear scaling isn't supported by get_trace_data_ ") return result_values def get_trace_data_a(self): """ Get the data of trace A as a list. The function returns the 601 data points as a list in the amplitude format. Right now it doesn't support the linear scaling due to the manual just being wrong. """ return self._get_trace_data(Trace.A) def get_trace_data_b(self): """ Get the data of trace B as a list. The function returns the 601 data points as a list in the amplitude format. Right now it doesn't support the linear scaling due to the manual just being wrong. """ return self._get_trace_data(Trace.B) set_trace_data_a = Instrument.setting( "TDF P;TRA %s", """ Set the trace data of trace A. .. warning:: The string based method this attribute is using takes its time. Something around 5000ms timeout at the adapter seems to work well. """, set_process=lambda v: (','.join([str(i) for i in v])), ) set_trace_data_b = Instrument.setting( "TDF P;TRB %s", """ Set the trace data of trace B also allows to write the data. .. warning:: The string based method this attribute is using takes its time. Something around 5000ms timeout at the adapter seems to work well. """, set_process=lambda v: (','.join([str(i) for i in v])) ) def trigger_sweep(self): """Command the spectrum analyzer to take one full sweep across the trace display. Commands following TS are not executed until after the analyzer has finished the trace sweep. This ensures that the instrument is set to a known condition before subsequent commands are executed. """ self.write("TS") def create_fft_trace_window(self, trace, window_mode): """Creates a window trace array for the fast Fourier transform (FFT) function. The trace-window function creates a trace array according to three built-in algorithms: UNIFORM, HANNING, and FLATTOP. When used with the FFT command, the three algorithms give resultant passband shapes that represent a compromise among amplitude uncertainty, sensitivity, and frequency resolution. Refer to the FFT command description for more information. :param trace: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :type trace: str :param window_mode: A representation of the window mode, either from :class:`WindowType` or use 'HANNING', 'FLATTOP' or 'UNIFORM' :type window_mode: str """ if not isinstance(trace, str): raise TypeError("Should be of type string but is '%s'" % type(trace)) if trace not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], trace)) if not isinstance(window_mode, str): raise TypeError("Should be of type string but is '%s'" % type(window_mode)) if window_mode not in [e for e in WindowType]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in WindowType], window_mode)) self.write("TWNDOW %s,%s" % (trace, window_mode)) video_average = Instrument.control( "VAVG?", "VAVG %d", """ Control the video averaging function. Video averaging smooths the displayed trace without using a narrow bandwidth. 'video_average' sets the IF detector to sample mode (see the DET command) and smooths the trace by averaging successive traces with each other. If desired, you can change the detector mode during video averaging. Video averaging is available only for trace A, and trace A must be in clear-write mode for 'video_average' to operate. After 'video_average' is executed, the number of sweeps that have been averaged appears at the top of the analyzer screen. Using video averaging allows you to view changes to the entire trace much faster than using narrow video filters. Narrow video filters require long sweep times, which may not be desired. Video averaging, though requiring more sweeps, uses faster sweep times; in some cases, it can produce a smooth trace as fast as a video filter. Type: :code:`str, int` """, validator=strict_range, values=np.arange(1, 999), cast=int ) video_average_enabled = Instrument.setting( "VAVG %s", """ Set the video averaging either active or inactive. See :attr:`video_average` """, map_values=True, values={True: "ON", False: "OFF"}, validator=strict_discrete_set ) video_bandwidth = Instrument.control( "VB?", "VB %s", """ Control the video bandwidth. This is normally a coupled function that is selected according to the ratio selected by the VBR command. (If no ratio is selected, a default ratio, 1.0, is used instead.) Video bandwidth filters (or smooths) post-detected video information. The bandwidth, which ranges from 1 Hz to 3 MHz, may also be selected manually. If the specified video bandwidth is less than 300 Hz and the resolution bandwidth is greater than or equal to 300 Hz, the IF detector is set to sample mode. Reducing the video bandwidth or increasing the number of video averages will usually smooth the trace by about as much for the same total measurement time. Reducing the video bandwidth to one-third or less of the resolution bandwidth is desirable when the number of video averages is above 25. For the case where the number of video averages is very large, and the video bandwidth is equal to the resolution bandwidth, internal mathematical limitations allow about 0.4 dB overresponse to noise on the logarithmic scale. The overresponse is negligible (less than 0.1 dB) for narrower video bandwidths. Type: :code:`int` """, validator=joined_validators(strict_discrete_set, strict_range), values=[["AUTO", "MAN"], np.arange(1, 3e6)], cast=int, set_process=lambda v: v if isinstance(v, str) else f"{v} Hz" ) video_bandwidth_to_resolution_bandwidth = Instrument.control( "VBR?", "VBR %.3f", """ Control the coupling ratio between the video bandwidth and the resolution bandwidth. Thus, when the resolution bandwidth is changed, the video bandwidth changes to satisfy the ratio. The ratio ranges from 0.003 to 3 in a 1, 3, 10 sequence. The default ratio is 1. When a new ratio is selected, the video bandwidth changes to satisfy the new ratio—the resolution bandwidth does not change value. """, validator=strict_range, values=np.arange(0.002, 0.10, 0.001) ) def view_trace(self, trace): """Display the current contents of the selected trace, but does not update the contents. View mode may be executed before a sweep is complete when :meth:`sweep_single` and :meth:`trigger_sweep` are not used. :param trace: A representation of the trace, either from :class:`Trace` or use 'TRA' for Trace A or 'TRB' for Trace B :type trace: str :raises TypeError: Type isn't 'string' :raises ValueError: Value is 'TRA' nor 'TRB' """ if not isinstance(trace, str): raise TypeError("Should be of type string but is '%s'" % type(trace)) if trace not in [e for e in Trace]: raise ValueError("Only accepts values of [%s] but was '%s'" % ([e for e in Trace], trace)) self.write("VIEW " + trace) video_trigger_level = Instrument.control( "VTL?", "VTL %.3f {amplitude_unit}", """ Control the video trigger level when the trigger mode is set to VIDEO (refer to the :attr:`trigger_mode` command). A dashed line appears on the display to indicate the level. The default value is 0 dBm. Range -220 to 30. Type: :code:`float` """, validator=strict_range, values=[-220, 30] ) class HP8560A(HP856Xx): """Represents the HP 8560A Spectrum Analyzer and provides a high-level interface for interacting with the instrument. .. code-block:: python from pymeasure.instruments.hp import HP8560A from pymeasure.instruments.hp.hp856Xx import AmplitudeUnits sa = HP8560A("GPIB::1") sa.amplitude_unit = AmplitudeUnits.DBUV sa.start_frequency = 299.5e6 sa.stop_frequency = 300.5e6 print(sa.marker_amplitude) """ # HP8560A is able to go up to 2.9 GHz MAX_FREQUENCY = 2.9e9 def __init__(self, adapter, name="Hewlett-Packard HP8560A", **kwargs): super().__init__( adapter, name, **kwargs, ) self.center_frequency_values = [0, self.MAX_FREQUENCY] self.start_frequency_values = [0, self.MAX_FREQUENCY] self.stop_frequency_values = [0, self.MAX_FREQUENCY] self.frequency_offset_values = [0, self.MAX_FREQUENCY] self.marker_frequency_values = [0, self.MAX_FREQUENCY] self.span_values = [["FULL", "ZERO"], [0, self.MAX_FREQUENCY]] source_leveling_control = Instrument.control( "SRCALC?", "SRCALC %s", """ Control if internal or external leveling is used with the built-in tracking generator. Takes either 'INT', 'EXT' or members of enumeration :class:`SourceLevelingControlMode` Type: :code:`str` .. code-block:: python instr.preset() instr.sweep_single() instr.center_frequency = 300e6 instr.span = 1e6 instr.source_power = -5 instr.trigger_sweep() instr.source_leveling_control = SourceLevelingControlMode.External if ErrorCode(900) in instr.errors: print("UNLEVELED CONDITION. CHECK LEVELING LOOP.") .. note:: Only available with an HP 8560A Option 002. """, validator=strict_discrete_set, values=[e for e in SourceLevelingControlMode] ) tracking_adjust_coarse = Instrument.control( "SRCCRSTK?", "SRCCRSTK %d", """ Control the coarse adjustment to the frequency of the built-in tracking-generator oscillator. Once enabled, this adjustment is made in digital-to-analogconverter (DAC) values from 0 to 255. For fine adjustment, refer to the :attr:`tracking_adjust_fine` command description. Type: :code:`int` .. note:: Only available with an HP 8560A Option 002. """, validator=strict_range, values=[0, 255], cast=int ) tracking_adjust_fine = Instrument.control( "SRCFINTK?", "SRCFINTK %d", """ Control the fine adjustment of the frequency of the built-in tracking-generator oscillator. Once enabled, this adjustment is made in digital-to-analogconverter (DAC) values from 0 to 255. For coarse adjustment, refer to the :attr:`tracking_adjust_coarse` command description. Type: :code:`int` .. note:: Only available with an HP 8560A Option 002. """, validator=strict_range, values=[0, 255], cast=int ) source_power_offset = Instrument.control( "SRCPOFS?", "SRCPOFS %g {amplitude_unit}", """ Control the offset of the displayed power of the built-in tracking generator so that it is equal to the measured power at the input of the spectrum analyzer. This function may be used to take into account system losses (for example, cable loss) or gains (for example, preamplifier gain) reflecting the actual power delivered to the device under test. Type: :code:`int` .. note:: Only available with an HP 8560A Option 002. """, validator=strict_range, values=[-100, 100], cast=int ) source_power_step = Instrument.control( "SRCPSTP?", "SRCPSTP %.2f DB", """ Control the step size of the source power level, source power offset, and power-sweep range functions. Range: 0.1 ... 12.75 DB with 0.05 steps. Type: :code:`float` .. note:: Only available with an HP 8560A Option 002. """, validator=strict_range, values=np.arange(0.1, 12.75, 0.05) ) source_power_sweep = Instrument.control( "SRCPSWP?", "SRCPSWP %.2f DB", """ Control the power-sweep function, where the output power of the tracking generator is swept over the power-sweep range chosen. The starting source power level is set using the :attr:`source_power` command. The output power of the tracking generator is swept according to the sweep rate of the spectrum analyzer. Type: :code:`str, float` .. note:: Only available with an HP 8560A Option 002. """, validator=truncated_discrete_set, values=np.arange(0.1, 12.75, 0.05), ) source_power_sweep_enabled = Instrument.setting( "SRCPSWP %s", """ Set the power sweep active or inactive. See :attr:`source_power_sweep`. """, map_values=True, values={True: "ON", False: "OFF"}, validator=strict_discrete_set ) source_power = Instrument.control( "SRCPWR?", "SRCPWR %s", """ Control the built-in tracking generator's output power. Type: :code:`str, float` .. note:: Only available with an HP 8560A Option 002. """, validator=joined_validators(strict_discrete_set, truncated_discrete_set), values=[["OFF", "ON"], np.arange(-10, 2.8, 0.05)], set_process=lambda v: v if isinstance(v, str) else ("%.2f {amplitude_unit}" % v) ) source_power_enabled = Instrument.setting( "SRCPWR %s", """ Set the built-in tracking generator on or off. See :attr:`source_power` """, map_values=True, values={True: "ON", False: "OFF"}, validator=strict_discrete_set ) def activate_source_peak_tracking(self): """Activate a routine which automatically adjusts both the coarse and fine-tracking adjustments to obtain the peak response of the tracking generator on the spectrum-analyzer display. Tracking peak is not necessary for resolution bandwidths greater than or equal to 300 kHz. A thru connection should be made prior to peaking in order to ensure accuracy. .. note:: Only available with an HP 8560A Option 002. """ self.write("SRCTKPK") class HP8561B(HP856Xx): """Represents the HP 8561B Spectrum Analyzer and provides a high-level interface for interacting with the instrument. .. code-block:: python from pymeasure.instruments.hp import 8561B from pymeasure.instruments.hp.hp856Xx import AmplitudeUnits sa = HP8560A("GPIB::1") sa.amplitude_unit = AmplitudeUnits.DBUV sa.start_frequency = 6.4e9 sa.stop_frequency = 6.5e9 print(sa.marker_amplitude) """ # HP8561B is able to go up to 6.5 GHz MAX_FREQUENCY = 6.5e9 def __init__(self, adapter, name="Hewlett-Packard HP8561B", **kwargs): super().__init__( adapter, name, **kwargs, ) self.center_frequency_values = [0, self.MAX_FREQUENCY] self.start_frequency_values = [0, self.MAX_FREQUENCY] self.stop_frequency_values = [0, self.MAX_FREQUENCY] self.frequency_offset_values = [0, self.MAX_FREQUENCY] self.marker_frequency_values = [0, self.MAX_FREQUENCY] self.span_values = [["FULL", "ZERO"], [0, self.MAX_FREQUENCY]] conversion_loss = Instrument.control( "CNVLOSS?", "CNVLOSS %s DB", """ Control the compensation for losses outside the instrument when in external mixer mode (such as losses within connector cables, external mixers, etc.). 'conversion_loss' specifies the mean conversion loss for the current harmonic band. In a full frequency band (such as band K), the mean conversion loss is defined as the minimum loss plus the maximum loss for that band divided by two. Adjusting for conversion loss allows the system to remain calibrated (that is, the displayed amplitude values have the conversion loss incorporated into them). The default value for any band is 30 dB. The spectrum analyzer must be in external-mixer mode in order for this command to work. When in internal-mixer mode, querying 'conversion_loss' returns a zero. """, validator=strict_range, values=[0, float("inf")] ) def set_fullband(self, band): """Select a commonly-used, external-mixer frequency band, as shown in the table. The harmonic lock function :attr:`harmonic_number_lock` is also set; this locks the harmonic of the chosen band. External-mixing functions are not available with an HP 8560A Option 002. Takes frequency band letter as string. .. list-table:: Title :widths: 25 25 25 25 :header-rows: 1 * - Frequency Band - Frequency Range (GHz) - Mixing Harmonic - Conversion Loss * - K - 18.0 — 26.5 - 6 - 30 dB * - A - 26.5 — 40.0 - 8 - 30 dB * - Q - 33.0—50.0 - 10 - 30 dB * - U - 40.0—60.0 - 10 - 30 dB * - V - 50.0—75.0 - 14 - 30 dB * - E - 60.0—-90.0 - 16 - 30 dB * - W - 75.0—110.0 - 18 - 30 dB * - F - 90.0—140.0 - 24 - 30 dB * - D - 110.0—170.0 - 30 - 30 dB * - G - 140.0—220.0 - 36 - 30 dB * - Y - 170.0—260.0 - 44 - 30 dB * - J - 220.0—325.0 - 54 - 30 dB """ frequency_mapping = { "K": [18e9, 26.5e9], "A": [26.5e9, 40e9], "Q": [33e9, 50e9], "U": [40e9, 60e9], "V": [50e9, 75e9], "E": [60e9, 90e9], "W": [75e9, 110e9], "F": [90e9, 140e9], "D": [110e9, 170e9], "G": [140e9, 220e9], "Y": [170e9, 260e9], "J": [220e9, 325e9], } if not isinstance(band, str): raise TypeError("Frequency band should be of type string but is '%s'" % type(band)) if band not in frequency_mapping.keys(): raise ValueError("Should be one of the available bands but is '%s'" % band) self.center_frequency_values = frequency_mapping[band] self.start_frequency_values = frequency_mapping[band] self.stop_frequency_values = frequency_mapping[band] self.write("FULLBAND %s" % band) harmonic_number_lock = Instrument.control( "HNLOCK?", "HNLOCK %d", """ Control the lock to a chosen harmonic so only that harmonic is used to sweep an external frequency band. To select a frequency band, use the 'fullband' command; it selects an appropriate harmonic for the desired band. To change the harmonic number, use 'harmonic_number_lock'. Note that 'harmonic_number_lock' also works in internal-mixing modes. Once 'fullband' or 'harmonic_number_lock' are set, only center frequencies and spans that fall within the frequency band of the current harmonic may be entered. When the 'set_full_span' command is activated, the span is limited to the frequency band of the selected harmonic. """, validator=strict_range, values=[1, 54], cast=int ) harmonic_number_lock_enabled = Instrument.setting( "HNLOCK %s", """ Set the harmonic number locking active or inactive. See :attr:`harmonic_number_lock`. """, map_values=True, values={True: "ON", False: "OFF"}, validator=strict_discrete_set ) def unlock_harmonic_number(self): """Unlock the harmonic number, allowing you to select frequencies and spans outside the range of the locked harmonic number. Also, when HNUNLK is executed, more than one harmonic can then be used to sweep across a desired span. For example, sweep a span from 18 GHz to 40 GHz. In this case, the analyzer will automatically sweep first using 6—, then using 8—. """ self.write("HUNLK") def set_signal_identification_to_center_frequency(self): """Set the center frequency to the frequency obtained from the command SIGID. SIGID must be in AUTO mode and have found a valid result for this command to execute properly. Use SIGID on signals greater than 18 GHz {i.e., in external mixing mode). SIGID and IDCF may also be used on signals less than 6.5 GHz in an HP 8561B. """ self.write("IDCF") signal_identification_frequency = Instrument.measurement( "IDFREQ?", """ Measure the frequency of the last identified signal. After an instrument preset or an invalid signal identification, IDFREQ returns a “0”. """ ) mixer_bias = Instrument.control( "MBIAS?", "MBIAS %.3f MA", """ Set the bias for an external mixer that requires diode bias for efficient mixer operation. The bias, which is provided on the center conductor of the IF input, is activated when MBIAS is executed. A "+" or "—" appears on the left edge of the spectrum analyzer display, indicating that positive or negative bias is on. When the bias is turned off, MBIAS is set to 0. Default units are in milliamps. """, validator=strict_range, values=[float(-10E3), int(10E3)], cast=float ) mixer_bias_enabled = Instrument.setting( "MBIAS %s", """ Control the bias for an external mixer. See :attr:`mixer_bias`. """, map_values=True, values={True: "ON", False: "OFF"}, validator=strict_discrete_set ) mixer_mode = Instrument.control( "MXRMODE?", "MXRMODE %s", """ Control the mixer mode. Select either the internal mixer or supply an external mixer. Takes enum 'MixerMode' or string 'INT', 'EXT' """, validator=strict_discrete_set, values=[e for e in MixerMode] ) def peak_preselector(self): """Peaks the preselector in the HP 8561B Spectrum Analyzer. Make sure the entire frequency span is in high band, set the desired trace to clear-write mode, place a marker on a desired signal, then execute PP. The peaking routine zooms to zero span, peaks the preselector tracking, then returns to the original position. To read the new preselector peaking number, use the PSDAC command. Commands following PP are not executed until after the analyzer has finished peaking the preselector. """ self.write("PP") preselector_dac_number = Instrument.control( "PSDAC?", "PSDAC %d", """ Control the preselector peak DAC number. For use with an HP 8561B Spectrum Analyzer. Type: :code:`int` """, cast=int, validator=strict_range, values=[0, 255] ) signal_identification = Instrument.control( "SIGID?", "SIGID %s", """ Control the signal identification for identifying signals for the external mixing frequency bands. Two signal identification methods are available. AUTO employs the image response method for locating correct mixer responses. Place a marker on the desired signal, then activate signal_identification = 'AUTO'. The frequency of a correct response appears in the active function block. Use this mode before executing the :meth:`signal_identification_to_center_frequency` command. The second method of signal identification, 'MAN', shifts responses both horizontally and vertically. A correct response is shifted horizontally by less than 80 kHz. To ensure accuracy in MAN mode, limit the frequency span to less than 20 MHz. Where True = manual mode is active and False = auto mode is active or 'signal_identification' is off. """, map_values=True, validator=strict_discrete_set, values={True: "1", False: "0", "AUTO": "AUTO", "MAN": "MAN"}, cast=str ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/hp8657b.py0000644000175100001770000001512214623331163021707 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 enum import IntEnum from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class HP8657B(Instrument): """ Represents the Hewlett Packard 8657B signal generator and provides a high-level interface for interacting with the instrument. """ def __init__(self, adapter, name="Hewlett-Packard HP8657B", **kwargs): super().__init__( adapter, name, includeSCPI=False, send_end=True, **kwargs, ) class Modulation(IntEnum): """ IntEnum for the different modulation sources """ EXTERNAL = 1 INT_400HZ = 2 INT_1000HZ = 3 OFF = 4 DC_FM = 5 def check_errors(self): """ Method to read the error status register as the 8657B does not support any readout of values, this will return 0 and log a warning """ log.warning("HP8657B Does not support error status readout") def clear(self): """ Reset the instrument to power-on default settings """ self.adapter.connection.clear() id = "HP,8657B,N/A,N/A" #: Manual ID entry am_depth = Instrument.setting( "AM %2.1f PC", """ Set the modulation depth for AM, usable range 0-99.9% """, validator=strict_range, values=[0, 99.9], ) am_source = Instrument.setting( "AM S%i", """ Set the source for the AM function with :attr:`Modulation` enumeration. ========== ======= Value Meaning ========== ======= OFF no modulation active INT_400HZ internal 400 Hz modulation source INT_1000HZ internal 1000 Hz modulation source EXTERNAL External source, AC coupling ========== ======= *Note:* * AM & FM can be active at the same time * only one internal source can be active at the time * use "OFF" to deactivate AM usage example: .. code-block:: python sig_gen = HP8657B("GPIB::7") ... sig_gen.am_source = sig_gen.Modulation.INT_400HZ # Enable int. 400 Hz source for AM sig_gen.am_depth = 50 # Set AM modulation depth to 50% ... sig_gen.am_source = sig_gen.Modulation.OFF # Turn AM off """, validator=strict_discrete_set, values=Modulation, ) fm_deviation = Instrument.setting( "FM %3.1fKZ", """ Set the peak deviation in kHz for the FM function, useable range 0.1 - 400 kHz *NOTE*: the maximum usable deviation is depending on the output frequency, refer to the instrument documentation for further detail. """, validator=strict_range, values=[0.1, 400], ) fm_source = Instrument.setting( "FM S%i", """ Set the source for the FM function with :attr:`Modulation` enumeration. ========== ======= Value Meaning ========== ======= OFF no modulation active INT_400HZ internal 400 Hz modulation source INT_1000HZ internal 1000 Hz modulation source EXTERNAL External source, AC coupling DC_FM External source, DC coupling (FM only) ========== ======= *Note:* * AM & FM can be active at the same time * only one internal source can be active at the time * use "OFF" to deactivate FM * refer to the documentation rearding details on use of DC FM mode usage example: .. code-block:: python sig_gen = HP8657B("GPIB::7") ... sig_gen.fm_source = sig_gen.Modulation.EXTERNAL # Enable external source for FM sig_gen.fm_deviation = 15 # Set FM peak deviation to 15 kHz ... sig_gen.fm_source = sig_gen.Modulation.OFF # Turn FM off """, validator=strict_discrete_set, values=Modulation, ) frequency = Instrument.setting( "FR %10.0f HZ", """ Set the output frequency of the instrument in Hz. For the 8567B the valid range is 100 kHz to 2060 MHz. """, validator=strict_range, values=[1.0E5, 2.060E9], ) level = Instrument.setting( "AP %g DM", """ Set the output level in dBm. For the 8657B the range is -143.5 to +17 dBm/ """, validator=strict_range, values=[-143.5, 17.0], ) level_offset = Instrument.setting( "AO %g DB", """ Set the output offset in dB, usable range -199 to +199 dB. """, validator=strict_range, values=[-199.0, 199.0], ) output_enabled = Instrument.setting( "R%d", """ Control whether the output is enabled. """, validator=strict_discrete_set, values={False: 2, True: 3}, map_values=True ) def reset(self): self.adapter.connection.clear() def shutdown(self): self.adapter.connection.clear() self.output_enabled = False self.adapter.connection.close() super().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/hplegacyinstrument.py0000644000175100001770000001163414623331163024535 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ctypes import logging from pymeasure.instruments import Instrument log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) c_uint8 = ctypes.c_uint8 c_uint16 = ctypes.c_uint16 c_uint32 = ctypes.c_uint32 class StatusBitsBase(ctypes.BigEndianStructure): """ A bitfield structure containing the assignments for the status decoding """ _pack_ = 1 _get_process_ = {} # decoder functions # decimal to BCD & BCD to decimal conversion copied from # https://pymodbus.readthedocs.io/en/latest/source/example/bcd_payload.html @staticmethod def _convert_from_bcd(bcd): """Converts a bcd value to a decimal value :param value: The value to unpack from bcd :returns: The number in decimal form """ place, decimal = 1, 0 while bcd > 0: nibble = bcd & 0xF decimal += nibble * place bcd >>= 4 place *= 10 return decimal @staticmethod def _convert_to_bcd(decimal): """Converts a decimal value to a bcd value :param value: The decimal value to to pack into bcd :returns: The number in bcd form """ place, bcd = 0, 0 while decimal > 0: nibble = decimal % 10 bcd += nibble << place decimal //= 10 place += 4 return bcd def __str__(self): """ Returns a pretty formatted string showing the status of the instrument """ ret_str = "" for field in self._fields_: ret_str = ret_str + f"{field[0]}: {hex(getattr(self, field[0]))}\n" return ret_str def __getattribute__(self, name): val = super().__getattribute__(name) if name == "fields": return val if name in self.fields(): process = super().__getattribute__('_get_process_') if name in process: val = process[name](val) return val def fields(self): return [desc[0] for desc in super().__getattribute__('_fields_')] class HPLegacyInstrument(Instrument): """ Class for legacy HP instruments from the era before SPCI, based on `pymeasure.Instrument` """ status_desc = StatusBitsBase # To be overriden by subclasses def __init__(self, adapter, name="HP legacy instrument", **kwargs): super().__init__( adapter, name, includeSCPI=False, **kwargs, ) self.status_bytes_count = ctypes.sizeof(self.status_desc) self.status_bits = self.status_desc log.info(f"Initializing {self.name}") def write(self, command): if command == "B": self.write_bytes(b"B") else: super().write(command) def values(self, command, **kwargs): if command == "B": self.write_bytes(b"B") return self.read_bytes(-1, **kwargs) else: return super().values(command, **kwargs) @property def status(self): """ Get an object representing the current status of the unit. """ self.write_bytes(b"B") reply = bytearray(self.read_bytes(self.status_bytes_count)) return self.status_bits.from_buffer(reply) def GPIB_trigger(self): """ Initate trigger via low-level GPIB-command (aka GET - group execute trigger) """ self.adapter.connection.assert_trigger() def reset(self): """ Initatiates a reset (like a power-on reset) of the HP3478A """ self.adapter.connection.clear() def shutdown(self): """ provides a way to gracefully close the connection to the HP3478A """ self.adapter.connection.clear() self.adapter.close() super().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/hp/hpsystempsu.py0000644000175100001770000002353014623331163023212 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ctypes import logging from enum import Enum from pymeasure.instruments.hp.hplegacyinstrument import HPLegacyInstrument, StatusBitsBase from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) c_uint8 = ctypes.c_uint8 class Status(StatusBitsBase): """ Support-Class with the bit assignments for the 2 status byte(12bits used) of the HP6632A """ _fields_ = [ # Byte 1: Function, Range and Number of Digits ("Error_pending", c_uint8, 1), # bit 7 ("Overcurrent", c_uint8, 1), # bit 6 ("not_assigned", c_uint8, 1), # bit 5 ("Overtempeature", c_uint8, 1), # bit 4 ("Overvoltage", c_uint8, 1), # bit 3 ("Unregulated", c_uint8, 1), # bit 2 ("CCpos", c_uint8, 1), # bit 1 ("CV", c_uint8, 1), # bit 0 # Byte 2: Status Bits ("not_assigned", c_uint8, 4), # bits 16..12 ("NORM", c_uint8, 1), # bit 11 ("FAST", c_uint8, 1), # bit 10 ("CCneg", c_uint8, 1), # bit 9 ("Inhibit_active", c_uint8, 1), # bit 8 ] limits = { "HP6632A": {"Volt_lim": 20.475, "OVP_lim": 22.0, "Cur_lim": 5.118}, "HP6633A": {"Volt_lim": 51.118, "OVP_lim": 55.0, "Cur_lim": 2.0475}, "HP6634A": {"Volt_lim": 102.38, "OVP_lim": 110.0, "Cur_lim": 1.0238}} class HP6632A(HPLegacyInstrument): """ Represents the Hewlett Packard 6632A system power supply and provides a high-level interface for interacting with the instrument. """ status_desc = Status def __init__(self, adapter, name="Hewlett-Packard HP6632A", **kwargs): kwargs.setdefault('read_termination', '\r\n') kwargs.setdefault('send_end', True) super().__init__( adapter, name, **kwargs, ) class ERRORS(Enum): """ Enum class for error messages """ NO_ERR = 0 EEPROM = 1 PON_2ND = 2 DCPON_2MD = 4 NORELAY = 5 NOTHING_SAY = 8 HEADER_EXPECTED = 10 UNRECOGNISED_HEADER = 11 NUMBER_EXPECTED = 20 NUMBER_SYNTAX = 21 OUT_OF_RANGE = 22 COMMA_EXPECTED = 30 TERM_EXPECTED = 31 PARAM_OUT = 41 V_PGM_ERR = 42 I_PGM_ERR = 43 OV_PGM_ERR = 44 DLY_PGM_ERR = 45 MASK_PGM_ERR = 46 MULT_CSAVE_ERR = 50 EEPROM_CJLSUM_ERR = 51 CALMODE_DISABLED = 52 CAL_CHNL_ERR = 53 CAL_FS_ERR = 54 CAL_OFFSET = 55 CAL_JMP_ERR = 59 class ST_ERRORS(Enum): """ Enum class for selftest errors """ NO_ST_ERR = 0 ROM_CKSSUM = 1 RAM_TEST = 2 HPIB_CHIP = 3 HPIB_TIMER_SLOW = 4 HPIB_TIMER_FAST = 5 PSI_ROM_CHKSUM = 11 PSI_RAM_TEST = 12 PSI_TIMER_SLOW = 14 PSI_TIMER_FAST = 15 AD_TEST_HIGH = 16 AD_TEST_LOW = 17 CCCV_ZERO_HIGH = 18 CCCV_ZERO_LOW = 19 CV_REF_HIGH = 20 CV_REF_LOW = 21 CC_REF_HIGH = 22 CC_REF_LOW = 23 DAC_FAIL = 24 EEPROM_CHKSUM = 51 def check_errors(self): """ Method to read the error status register :return error_status: one byte with the error status register content :rtype error_status: int """ # Read the error status register only one time for this method, as # the manual states that reading the error status register also clears it. current_errors = int(self.ask("ERR?")) if current_errors != 0: log.error("HP6632 Error detected: %s", self.ERRORS(current_errors)) return self.ERRORS(current_errors) def check_selftest_errors(self): """ Method to read the error status register :return error_status: one byte with the error status register content :rtype error_status: int """ # Read the error status register only one time for this method, as # the manual states that reading the error status register also clears it. current_errors = int(self.ask("TEST?")) if current_errors != 0: log.error("HP6632 Error detected: %s", self.ERRORS(current_errors)) return self.ST_ERRORS(current_errors) def clear(self): """ Resets the instrument to power-on default settings """ self.write("CLR") delay = HPLegacyInstrument.setting( "DELAY %g", """ A float property that changes the reprogamming delay Default values: 8 ms in FAST mode 80 ms in NORM mode Values will be rounded to the next 4 ms by the instrument """, validator=strict_range, values=[0, 32.768], ) display_active = HPLegacyInstrument.setting( "DIS %d", """ A boot property which controls if the display is enabled """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True ) @property def status(self): """ Returns an object representing the current status of the unit. """ # overloading the already existing property because of the different command reply = bytearray(int(self.ask("STS?")).to_bytes( self.status_bytes_count, "little")) return self.status_bits.from_buffer(reply) id = HPLegacyInstrument.measurement( "ID?", """ Reads the ID of the instrument and returns this value for now """, ) current = HPLegacyInstrument.control( "IOUT?", "ISET %g", """ A floating point property that controls the output current of the device. """, dynamic=True, validator=strict_range, values=[0, limits["HP6632A"]["Cur_lim"]], ) over_voltage_limit = HPLegacyInstrument.setting( "OVSET %g", """ A floationg point property that sets the OVP threshold. """, dynamic=True, validator=strict_range, values=[0, limits["HP6632A"]["OVP_lim"]], ) OCP_enabled = HPLegacyInstrument.setting( "OCP %d", """ A bool property which controls if the OCP (OverCurrent Protection) is enabled """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True ) output_enabled = HPLegacyInstrument.setting( "OUT %d", """ A bool property which controls if the outputis enabled """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True ) @output_enabled.getter def output_enabled(self): """ A bool property which controls if the output is enabled """ output_status = bool(self.status.CV or self.status.CCpos or self.status.CCneg or self.status.Unregulated) return output_status def reset_OVP_OCP(self): """ Resets Overvoltage and Overcurrent protections """ self.write("RST") rom_version = HPLegacyInstrument.measurement( "ROM?", """ Reads the ROM id (software version) of the instrument and returns this value for now """, ) SRQ_enabled = HPLegacyInstrument.setting( "SRQ %d", """ A bool property which controls if the SRQ (ServiceReQuest) is enabled """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True ) voltage = HPLegacyInstrument.control( "VOUT?", "VSET %g", """ A floating point proptery that controls the output voltage of the device. """, dynamic=True, validator=strict_range, values=[0, limits["HP6632A"]["Volt_lim"]], ) class HP6633A(HP6632A): """ Represents the Hewlett Packard 6633A system power supply and provides a high-level interface for interacting with the instrument. """ def __init__(self, adapter, name="Hewlett Packard HP6633A", **kwargs): super().__init__(adapter, name, **kwargs) current_values = [0, limits["HP6633A"]["Cur_lim"]] OVP_values = [0, limits["HP6633A"]["OVP_lim"]] voltage_values = [0, limits["HP6633A"]["Volt_lim"]] class HP6634A(HP6632A): """ Represents the Hewlett Packard 6634A system power supply and provides a high-level interface for interacting with the instrument. """ def __init__(self, adapter, name="Hewlett Packard HP6634A", **kwargs): super().__init__(adapter, name, **kwargs) current_values = [0, limits["HP6634A"]["Cur_lim"]] OVP_values = [0, limits["HP6634A"]["OVP_lim"]] voltage_values = [0, limits["HP6634A"]["Volt_lim"]] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4016056 pymeasure-0.14.0/pymeasure/instruments/inficon/0000755000175100001770000000000014623331176021253 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/inficon/__init__.py0000644000175100001770000000224014623331163023356 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .sqm160 import SQM160 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/inficon/sqm160.py0000644000175100001770000002027014623331163022651 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 Channel, Instrument def calculate_checksum(msg): """calculate a two byte Cyclic Redundancy Check based on 14 bits Parameters ---------- msg: bytes Message of the device without the sync character """ # check if message contains data if not msg: return chr(0) + chr(0) # initialize CRC crc = 0x3fff # loop over characters in message for char in msg: crc ^= char for i in range(8): tmpcrc = crc crc = crc >> 1 if tmpcrc & 1 == 1: crc ^= 0x2001 crc &= 0x3fff # separate 14 significant bits in two byte checksum return bytes(((crc & 0x7f) + 34, ((crc >> 7) & 0x7f) + 34)) class SensorChannel(Channel): """Sensor channel for individual rate measurements.""" rate = Channel.measurement( "L{ch}?", """Get the current rate for a sensor in Angstrom per second""", cast=float, ) thickness = Channel.measurement( "N{ch}", """Get the current thickness for a sensor in Angstrom""", cast=float, ) frequency = Channel.measurement( "P{ch}", """Get the current frequency for a sensor in Hz""", cast=float, ) crystal_life = Channel.measurement( "R{ch}", """Get the crystal life value in percent""", cast=float, ) class SQM160(Instrument): """Inficon SQM-160 multi-film rate/thickness monitor. Uses a quartz crystal sensor to measure rate and thickness in a thin film deposition process. Connection to the device is commonly made through a serial connection (RS232) or optionally via USB or Ethernet. A command packet always consists of the following: - 1 Byte: Sync character ('!' appears only at the start of a message). - 1 Byte: length character obtained from the message length without CRC. A value of 34 is added so that no '!' can occur. - Command message with variable length. - 2 Byte: Cyclic Redundancy Check (CRC) checksum. A response packet always consists of: - 1 Byte: Sync character ('!' appears only at the start of a message). - 1 Byte: length character obtained from the message length without CRC. A value of 35 is added. - 1 Byte: Response status character indicating the status of the command. - Response message with variable length. - 2 Byte: Cyclic Redundancy Check (CRC) checksum. :param adapter: pyvisa resource name of the instrument or adapter instance :param string name: Name of the instrument. :param string baud_rate: Baud rate used by the serial connection. :param kwargs: Any valid key-word argument for Instrument """ sensor_1 = Instrument.ChannelCreator(SensorChannel, 1) sensor_2 = Instrument.ChannelCreator(SensorChannel, 2) sensor_3 = Instrument.ChannelCreator(SensorChannel, 3) sensor_4 = Instrument.ChannelCreator(SensorChannel, 4) sensor_5 = Instrument.ChannelCreator(SensorChannel, 5) sensor_6 = Instrument.ChannelCreator(SensorChannel, 6) def __init__(self, adapter, name="Inficon SQM-160 thickness monitor", baud_rate=19200, **kwargs): super().__init__(adapter, name, includeSCPI=False, write_termination="", read_termination="", asrl=dict(baud_rate=baud_rate), timeout=3000, **kwargs) def read(self): """Reads a response message from the instrument. This method also checks for a correct checksum. :returns: the response packet :rtype: string :raises ConnectionError: if a checksum error is detected or a wrong response status is detected. """ header = self.read_bytes(2) # check valid header if header[0] != 33: # b"!" raise ConnectionError(f"invalid header start byte '{header[0]}' received") length = header[1] - 35 if length <= 0: raise ConnectionError(f"invalid message length '{header[1]}' -> length {length}") response_status = self.read_bytes(1) if response_status == b"C": raise ConnectionError("invalid command response received") elif response_status == b"D": raise ConnectionError("Problem with data in command") elif response_status != b"A": raise ConnectionError(f"unknown response status character '{response_status}'") if length - 1 > 0: data = self.read_bytes(length - 1) else: data = b"" chksum = self.read_bytes(2) calculated_checksum = calculate_checksum( header[1].to_bytes(length=1, byteorder='big') + response_status + data) if chksum == calculated_checksum: return data.decode() else: raise ConnectionError( f"checksum error in received message '{header + response_status + data}' " f"with checksum '{calculated_checksum}' but received '{chksum}'") def write(self, command): """Write a command to the device.""" length = chr(len(command) + 34) message = f"{length}{command}".encode() self.write_bytes(b"!" + message + calculate_checksum(message)) def check_set_errors(self): """Check the errors after setting a property.""" self.read() return [] # no error happened firmware_version = Instrument.measurement( "@", """Get the firmware version.""", cast=str, ) number_of_channels = Instrument.measurement( "J", """Get the number of installed channels""", cast=int, ) average_rate = Instrument.measurement( "M", """Get the current average rate in Angstrom per second""", cast=float, ) average_thickness = Instrument.measurement( "O", """Get the current average thickness in Angstrom""", cast=float, ) all_values = Instrument.measurement( "W", """Get the current rate (Angstrom/s), Thickness (Angstrom), and frequency (Hz) for each sensor""", cast=float, preprocess_reply=lambda msg: msg[5:], # ingore first '00.00' ) reset_flag = Instrument.measurement( "Y", """Get the power-up reset flag. It is True only when read first after a power cycle.""", cast=int, values={True: 1, False: 0}, map_values=True, ) def reset_system_parameters(self): """Reset all film and system parameters.""" self.write("Z") self.read() # read obligatory response message def reset_thickness_rate(self): """Reset the average thickness and rate. This also sets all active Sensor Rates and Thicknesses to zero """ self.write("S") self.read() # read obligatory response message def reset_time(self): """Reset the time of the monitor to zero. """ self.write("T") self.read() # read obligatory response message ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/instrument.py0000644000175100001770000002426614623331163022416 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from warnings import warn from .common_base import CommonBase from ..adapters.visa import VISAAdapter log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Instrument(CommonBase): """ The base class for all Instrument definitions. It makes use of one of the :py:class:`~pymeasure.adapters.Adapter` classes for communication with the connected hardware device. This decouples the instrument/command definition from the specific communication interface used. When ``adapter`` is a string, this is taken as an appropriate resource name. Depending on your installed VISA library, this can be something simple like ``COM1`` or ``ASRL2``, or a more complicated `VISA resource name `__ defining the target of your connection. When ``adapter`` is an integer, a GPIB resource name is created based on that. In either case a :py:class:`~pymeasure.adapters.VISAAdapter` is constructed based on that resource name. Keyword arguments can be used to further configure the connection. Otherwise, the passed :py:class:`~pymeasure.adapters.Adapter` object is used and any keyword arguments are discarded. This class defines basic SCPI commands by default. This can be disabled with :code:`includeSCPI` for instruments not compatible with the standard SCPI commands. :param adapter: A string, integer, or :py:class:`~pymeasure.adapters.Adapter` subclass object :param string name: The name of the instrument. Often the model designation by default. :param includeSCPI: An obligatory boolean, which toggles the inclusion of standard SCPI commands .. deprecated:: 0.14 If True, inherit the :class:`~pymeasure.instruments.generic_types.SCPIMixin` class instead. :param preprocess_reply: An optional callable used to preprocess strings received from the instrument. The callable returns the processed string. .. deprecated:: 0.11 Implement it in the instrument's `read` method instead. :param \\**kwargs: In case ``adapter`` is a string or integer, additional arguments passed on to :py:class:`~pymeasure.adapters.VISAAdapter` (check there for details). Discarded otherwise. """ # noinspection PyPep8Naming def __init__(self, adapter, name, includeSCPI=None, preprocess_reply=None, **kwargs): # Setup communication before possible children require the adapter. if isinstance(adapter, (int, str)): try: adapter = VISAAdapter(adapter, **kwargs) except ImportError: raise Exception("Invalid Adapter provided for Instrument since" " PyVISA is not present") self.adapter = adapter if includeSCPI is True: warn("Defining SCPI base functionality with `includeSCPI=True` is deprecated, inherit " "the `SCPIMixin` class instead.", FutureWarning) elif includeSCPI is None: warn("It is deprecated to specify `includeSCPI` implicitly, use " "`includeSCPI=False` or inherit the `SCPIMixin` class instead.", FutureWarning) includeSCPI = True self.SCPI = includeSCPI self.isShutdown = False self.name = name super().__init__(preprocess_reply=preprocess_reply) log.info("Initializing %s." % self.name) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.shutdown() # SCPI default properties @property def complete(self): """Get the synchronization bit. This property allows synchronization between a controller and a device. The Operation Complete query places an ASCII character 1 into the device's Output Queue when all pending selected device operations have been finished. """ if self.SCPI: return self.ask("*OPC?").strip() else: raise NotImplementedError("Non SCPI instruments require implementation in subclasses") @property def status(self): """ Get the status byte and Master Summary Status bit. """ if self.SCPI: return self.ask("*STB?").strip() else: raise NotImplementedError("Non SCPI instruments require implementation in subclasses") @property def options(self): """ Get the device options installed. """ if self.SCPI: return self.ask("*OPT?").strip() else: raise NotImplementedError("Non SCPI instruments require implementation in subclasses") @property def id(self): """ Get the identification of the instrument. """ if self.SCPI: return self.ask("*IDN?").strip() else: raise NotImplementedError("Non SCPI instruments require implementation in subclasses") @property def next_error(self): """Get the next error of the instrument (tuple of code and message).""" if self.SCPI: return self.values("SYST:ERR?") else: raise NotImplementedError("Non SCPI instruments require implementation in subclasses") # Wrapper functions for the Adapter object def write(self, command, **kwargs): """Write a string command to the instrument appending `write_termination`. :param command: command string to be sent to the instrument :param kwargs: Keyword arguments for the adapter. """ self.adapter.write(command, **kwargs) def write_bytes(self, content, **kwargs): """Write the bytes `content` to the instrument.""" self.adapter.write_bytes(content, **kwargs) def read(self, **kwargs): """Read up to (excluding) `read_termination` or the whole read buffer.""" return self.adapter.read(**kwargs) def read_bytes(self, count, **kwargs): """Read a certain number of bytes from the instrument. :param int count: Number of bytes to read. A value of -1 indicates to read the whole read buffer. :param kwargs: Keyword arguments for the adapter. :returns bytes: Bytes response of the instrument (including termination). """ return self.adapter.read_bytes(count, **kwargs) def write_binary_values(self, command, values, *args, **kwargs): """Write binary values to the device. :param command: Command to send. :param values: The values to transmit. :param \\*args, \\**kwargs: Further arguments to hand to the Adapter. """ self.adapter.write_binary_values(command, values, *args, **kwargs) def read_binary_values(self, **kwargs): """Read binary values from the device.""" return self.adapter.read_binary_values(**kwargs) # Communication functions def wait_for(self, query_delay=None): """Wait for some time. Used by 'ask' to wait before reading. :param query_delay: Delay between writing and reading in seconds. None is default delay. """ if query_delay: time.sleep(query_delay) # SCPI default methods def clear(self): """ Clears the instrument status byte """ if self.SCPI: self.write("*CLS") else: raise NotImplementedError("Non SCPI instruments require implementation in subclasses") def reset(self): """ Resets the instrument. """ if self.SCPI: self.write("*RST") else: raise NotImplementedError("Non SCPI instruments require implementation in subclasses") def shutdown(self): """Brings the instrument to a safe and stable state""" self.isShutdown = True log.info(f"Finished shutting down {self.name}") def check_errors(self): """Read all errors from the instrument and log them. :return: List of error entries. """ if self.SCPI: errors = [] while True: err = self.next_error if int(err[0]) != 0: log.error(f"{self.name}: {err[0]}, {err[1]}") errors.append(err) else: break return errors else: raise NotImplementedError("Non SCPI instruments require implementation in subclasses") def check_get_errors(self): """Check for errors after having gotten a property and log them. Called if :code:`check_get_errors=True` is set for that property. If you override this method, you may choose to raise an Exception for certain errors. :return: List of error entries. """ return self.check_errors() def check_set_errors(self): """Check for errors after having set a property and log them. Called if :code:`check_set_errors=True` is set for that property. If you override this method, you may choose to raise an Exception for certain errors. :return: List of error entries. """ return self.check_errors() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4016056 pymeasure-0.14.0/pymeasure/instruments/ipgphotonics/0000755000175100001770000000000014623331176022334 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/ipgphotonics/__init__.py0000644000175100001770000000223214623331163024440 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .yar import YAR ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/ipgphotonics/yar.py0000644000175100001770000001401214623331163023473 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 enum import IntFlag from pymeasure.instruments import Instrument, validators from pyvisa.constants import Parity, StopBits log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def emission_validator(value, values): if value is True: return "ON" elif value is False: return "OFF" raise ValueError(f"Value is {value}, but a boolean or 'ON' or 'OFF' required.") def setpoint_validator(value, values): if value == 0: return value else: return validators.strict_range(value, values) def power_get_process_generator(minimum): """Generate a get_process for the power property.""" def get_process(value): if isinstance(value, float): return value elif value == "Off": return 0 elif value == "Low": return minimum else: return value return get_process class YAR(Instrument): """Communication with the YAR fiber amplifier series by IPG Photonics. This is the RS232 command set. GPIB has different commands. """ def __init__(self, adapter, name="YAR fiber amplifier", **kwargs): """Establish communication with the device.""" kwargs.setdefault("write_termination", "\r") kwargs.setdefault("read_termination", "\r") super().__init__(adapter, name=name, includeSCPI=False, asrl={'parity': Parity.none, 'stop_bits': StopBits.one}, **kwargs) # Commands are 3-4 letters, followed by a parameter, a separation # by space is option. Commands are case-insensitive. # Response is command echoed back, followed by ': ' and the return # value. # get valid range of power setpoint: self.power_setpoint_values = self.power_range self.power_get_process = power_get_process_generator(self.minimum_display_power) class Status(IntFlag): EMISSION = 0x1 # emission is fully on STARTUP_DELAY = 0x2 # it is in 3 s startup HIGH_TEMPERATURE = 1 << 16 HIGH_BACKREFLECTION = 1 << 17 UNEXPECTED_EMISSION = 1 << 19 SEEDLASER_FAIL = 1 << 20 def read(self): """Read an instrument answer and check whether it is an error.""" reply = super().read().split(":") if reply[0] == "ERR": raise ConnectionError(f"Reading error '{reply}'.") else: return reply[-1].strip() def check_set_errors(self): """Check for errors after having set a property. Called if :code:`check_set_errors=True` is set for that property. """ try: self.read() except ConnectionError as exc: log.exception("Setting a property failed.", exc_info=exc) raise else: return [] # COMMUNICATION FUNCTIONS @property def id(self): """Get the model number.""" return self.values("RMN")[0] @property def status(self): """Get the current status.""" got = int(self.values("STA")[0]) return self.Status(got) emission_enabled = Instrument.control( "STA", "EM%s", """Control emission of the amplifier (bool).""", cast=int, values=("ON", "OFF"), validator=emission_validator, get_process=lambda v: bool(v & YAR.Status.EMISSION), check_set_errors=True, ) power = Instrument.measurement( "ROP", "Measure current output power in W.", get_process=power_get_process_generator(0.1), dynamic=True, ) @property def power_range(self): """Get the power limits in W.""" low = self.values("RNP")[0] high = self.values("RMP")[0] return [low, high] power_setpoint = Instrument.control( "RPS", "SPS %g", """Control output power setpoint in W.""", values=(1, 2), validator=setpoint_validator, check_set_errors=True, dynamic=True, ) current = Instrument.measurement("RDC", """Measure the diode current in A.""") temperature = Instrument.measurement("RCT", """Measure case temperature in °C.""") wavelength_temperature = Instrument.control( "RWA", "SWA %g", """Control temperature in °C for seed wavelength control.""", check_set_errors=True) temperature_seed = Instrument.measurement( "RST", "Measure current seed temperature in °C") firmware = Instrument.measurement("RFV", """Get firmware version""", cast=str) maximum_case_temperature = Instrument.measurement( "RMT", """Measure the maximum temperature for the optical module in °C.""") minimum_display_power = Instrument.measurement( "RDPT", """Measure the minimum displayable output power in W.""") def clear(self): """Reset all errors.""" return self.ask("RERR") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4056056 pymeasure-0.14.0/pymeasure/instruments/keithley/0000755000175100001770000000000014623331176021444 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/__init__.py0000644000175100001770000000321214623331163023547 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .keithley2260B import Keithley2260B from .keithley2306 import Keithley2306 from .keithley2400 import Keithley2400 from .keithley2450 import Keithley2450 from .keithley2600 import Keithley2600 from .keithley2700 import Keithley2700 from .keithley2750 import Keithley2750 from .keithley6221 import Keithley6221 from .keithley6517b import Keithley6517B from .keithley2200 import Keithley2200 from .keithleyDMM6500 import KeithleyDMM6500 from .keithley2182 import Keithley2182 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/buffer.py0000644000175100001770000001110714623331163023263 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 as np from pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range from pymeasure.adapters import PrologixAdapter log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class KeithleyBuffer: """ Implements the basic buffering capability found in many Keithley instruments. """ buffer_points = Instrument.control( ":TRAC:POIN?", ":TRAC:POIN %d", """ Control 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_byte = int(self.ask("*STB?")) return (status_byte & 65) == 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): """ Get 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") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley2000.py0000644000175100001770000005715614623331163024150 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import ( truncated_range, truncated_discrete_set, strict_discrete_set ) from .buffer import KeithleyBuffer log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Keithley2000(KeithleyBuffer, SCPIUnknownMixin, Instrument): """ 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: ``current`` (DC), ``current ac``, ``voltage`` (DC), ``voltage ac``, ``resistance`` (2-wire), ``resistance 4W`` (4-wire), ``period``, ``temperature``, ``diode``, and ``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: ``enabled`` and ``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, name="Keithley 2000 Multimeter", **kwargs): super().__init__( adapter, name, **kwargs ) 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 measurements.") 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 explicitly 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(f":SYST:BEEP {frequency:g}, {duration:g}") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley2182.py0000644000175100001770000003344314623331163024154 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range from .buffer import KeithleyBuffer log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Keithley2182Channel(Channel): """Implementation of a Keithley 2182 channel. Channel 1 is the fundamental measurement channel, while channel 2 provides sense measurements. Channel 2 inputs are referenced to Channel 1 LO. Possible configurations are Voltage (Channel 1) Temperature (Channel 1) Voltage (Channel 1) and Voltage (Channel 2) Voltage (Channel 1) and Temperature (Channel 2) """ def __init__(self, parent, id): """Set max voltage depending on channel.""" if id == 1: self.voltage_range_values = (0, 120) self.voltage_offset_values = (-120, 120) else: self.voltage_range_values = (0, 12) self.voltage_offset_values = (-12, 12) super().__init__(parent, id) voltage_range = Channel.control( ":SENS:VOLT:CHAN{ch}:RANG?", ":SENS:VOLT:CHAN{ch}:RANG %g", """Control the positive full-scale measurement voltage range in Volts. The Keithley 2182 selects a measurement range based on the expected voltage. DCV1 has five ranges: 10 mV, 100 mV, 1 V, 10 V, and 100 V. DCV2 has three ranges: 100 mV, 1 V, and 10 V. Valid limits are from 0 to 120 V for Ch. 1, and 0 to 12 V for Ch. 2. Auto-range is automatically disabled when this property is set.""", validator=strict_range, values=(0, 120), dynamic=True, ) voltage_range_auto_enabled = Channel.control( ":SENS:VOLT:CHAN{ch}:RANG:AUTO?", ":SENS:VOLT:CHAN{ch}:RANG:AUTO %d", """Control the auto voltage ranging option (bool).""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) voltage_offset = Channel.control( ":SENS:VOLT:CHAN{ch}:REF?", ":SENS:VOLT:CHAN{ch}:REF %g", """Control the relative offset for measuring voltage. Displayed value = actual value - offset value. Valid ranges are -120 V to +120 V for Ch. 1, and -12 V to +12 V for Ch. 2.""", validator=strict_range, values=(-120, 120), dynamic=True, ) temperature_offset = Channel.control( ":SENS:TEMP:CHAN{ch}:REF?", ":SENS:TEMP:CHAN{ch}:REF %g", """Control the relative offset for measuring temperature. Displayed value = actual value - offset value. Valid values are -273 C to 1800 C.""", validator=strict_range, values=(-273, 1800), ) voltage_offset_enabled = Channel.control( ":SENS:VOLT:CHAN{ch}:REF:STAT?", ":SENS:VOLT:CHAN{ch}:REF:STAT %s", """Control whether voltage is measured as a relative or absolute value (bool). Enabled by default for Ch. 2 voltage, which is measured relative to Ch. 1 voltage.""", validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True ) temperature_offset_enabled = Channel.control( ":SENS:TEMP:CHAN{ch}:REF:STAT?", ":SENS:TEMP:CHAN{ch}:REF:STAT %s", """Control whether temperature is measured as a relative or absolute value (bool). Disabled by default.""", validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True ) def setup_voltage(self, auto_range=True, nplc=5): """Set active channel and configure channel for voltage measurement. :param auto_range: Enables auto_range if True, else uses set voltage range :param nplc: Number of power line cycles (NPLC) from 0.01 to 50/60 """ self.write(":SENS:CHAN {ch};" ":SENS:FUNC 'VOLT';" f":SENS:VOLT:NPLC {nplc};") if auto_range: self.write(":SENS:VOLT:RANG:AUTO 1") self.check_errors() def setup_temperature(self, nplc=5): """Change active channel and configure channel for temperature measurement. :param nplc: Number of power line cycles (NPLC) from 0.01 to 50/60 """ self.write(":SENS:CHAN {ch};" ":SENS:FUNC 'TEMP';" f":SENS:TEMP:NPLC {nplc}") self.check_errors() def acquire_temperature_reference(self): """Acquire a temperature measurement and store it as the relative offset value. Only acquires reference if temperature offset is enabled. """ self.write(":SENS:TEMP:CHAN{ch}:REF:ACQ") def acquire_voltage_reference(self): """Acquire a voltage measurement and store it as the relative offset value. Only acquires reference if voltage offset is enabled. """ self.write(":SENS:VOLT:CHAN{ch}:REF:ACQ") class Keithley2182(SCPIMixin, KeithleyBuffer, Instrument): """Represents the Keithley 2182 Nanovoltmeter and provides a high-level interface for interacting with the instrument. .. code-block:: python keithley = Keithley2182("GPIB::1") keithley.reset() # Return instrument settings to default values keithley.thermocouple = 'S' # Sets thermocouple type to S keithley.active_channel = 1 # Sets channel 1 for active measurement keithley.channel_function = 'voltage' # Configures active channel for voltage measurement print(keithley.voltage) # Prints the voltage in volts keithley.ch_1.setup_voltage() # Set channel 1 active and prepare voltage measurement keithley.ch_2.setup_temperature() # Set channel 2 active and prepare temperature measurement """ def __init__(self, adapter, name="Keithley 2182 Nanovoltmeter", read_termination='\r', **kwargs): super().__init__(adapter, name, read_termination=read_termination, **kwargs) ch_1 = Instrument.ChannelCreator(Keithley2182Channel, 1) ch_2 = Instrument.ChannelCreator(Keithley2182Channel, 2) ################# # Configuration # ################# auto_zero_enabled = Instrument.control( ":SYST:AZER:STAT?", ":SYST:AZER:STAT %d", """Control the auto zero option (bool).""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) line_frequency = Instrument.measurement( ":SYST:LFR?", """Get the line frequency in Hertz. Values are 50 or 60. Cannot be set on 2182.""", ) display_enabled = Instrument.control( ":DISP:ENAB?", ":DISP:ENAB %d", """Control whether the front display of the voltmeter is enabled. Valid values are True and False.""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) active_channel = Instrument.control( ":SENS:CHAN?", ":SENS:CHAN %d", """Control which channel is active for measurement. Valid values are 0 (internal temperature sensor), 1, and 2.""", validator=strict_discrete_set, values=(0, 1, 2), cast=int ) channel_function = Instrument.control( ":SENS:FUNC?", "SENS:FUNC %s", """Control the measurement mode of the active channel. Valid options are `voltage` and `temperature`.""", validator=strict_discrete_set, values={'voltage': '"VOLT:DC"', 'temperature': '"TEMP"'}, map_values=True ) ############### # Voltage (V) # ############### voltage = Instrument.measurement( ":READ?", """Measure the voltage in Volts, if active channel is configured for this reading.""" ) voltage_nplc = Instrument.control( ":SENS:VOLT:NPLC?", ":SENS:VOLT:NPLC %g", """Control the number of power line cycles (NPLC) for voltage measurements, which sets the integration period and measurement speed. Valid values are from 0.01 to 50 or 60, depending on the line frequency. Default is 5.""", validator=strict_range, values=(0.01, 60), dynamic=True, ) ################### # Temperature (C) # ################### temperature = Instrument.measurement( ":READ?", """Measure the temperature in Celsius, if active channel is configured for this reading.""" ) thermocouple = Instrument.control( ":SENS:TEMP:TC?", ":SENS:TEMP:TC %s", """Control the thermocouple type for temperature measurements. Valid options are B, E, J, K, N, R, S, and T.""", validator=strict_discrete_set, values=('B', 'E', 'J', 'K', 'N', 'R', 'S', 'T') ) temperature_nplc = Instrument.control( ":SENS:TEMP:NPLC?", ":SENS:TEMP:NPLC %g", """Control the number of power line cycles (NPLC) for temperature measurements, which sets the integration period and measurement speed. Valid values are from 0.01 to 50 or 60, depending on the line frequency. Default is 5.""", validator=strict_range, values=(0.01, 60), dynamic=True, ) temperature_reference_junction = Instrument.control( ":SENS:TEMP:RJUN:RSEL?", ":SENS:TEMP:RJUN:RSEL %s", """Control whether the thermocouple reference junction is internal (INT) or simulated (SIM). Default is INT.""", validator=strict_discrete_set, values=('SIM', 'INT'), ) temperature_simulated_reference = Instrument.control( ":SENS:TEMP:RJUN:SIM?", ":SENS:TEMP:RJUN:SIM %g", """Control the value of the simulated thermocouple reference junction in Celsius. Default is 23 C.""", validator=strict_range, values=(0, 60), ) internal_temperature = Instrument.measurement( ":SENS:TEMP:RTEM?", """Measure the internal temperature in Celsius.""" ) ############## # Statistics # ############## mean = Instrument.measurement( ":CALC2:FORM MEAN;:CALC2:STAT ON;:CALC2:IMM?;", """Get the calculated mean (average) from the buffer data.""" ) maximum = Instrument.measurement( ":CALC2:FORM MAX;:CALC2:STAT ON;:CALC2:IMM?;", """Get the calculated maximum from the buffer data.""" ) minimum = Instrument.measurement( ":CALC2:FORM MIN;:CALC2:STAT ON;:CALC2:IMM?;", """Get the calculated minimum from the buffer data.""" ) standard_dev = Instrument.measurement( ":CALC2:FORM SDEV;:CALC2:STAT ON;:CALC2:IMM?;", """Get the calculated standard deviation from the buffer data.""" ) ########### # Trigger # ########### trigger_count = Instrument.control( ":TRIG:COUN?", ":TRIG:COUN %d", """Control the trigger count which can take values from 1 to 9,999. Default is 1.""", validator=strict_range, values=(1, 9999), cast=int ) trigger_delay = Instrument.control( ":TRIG:DEL?", ":TRIG:DEL %g", """Control the trigger delay in seconds, which can take values from 0 to 999999.999 s. Default is 0.""", validator=strict_range, values=(0, 999999.999) ) ########### # Methods # ########### def auto_line_frequency(self): """Set appropriate limits for NPLC voltage and temperature readings.""" if self.line_frequency == 50: self.temperature_nplc_values = (0.01, 50) self.voltage_nplc_values = (0.01, 50) else: self.temperature_nplc_values = (0.01, 60) self.voltage_nplc_values = (0.01, 60) def reset(self): """Reset the instrument and clear the queue.""" self.write("status:queue:clear;*RST;:stat:pres;:*CLS;") def trigger(self): """Execute a bus trigger, which can be used when :meth:`~.trigger_on_bus` is configured. """ return self.write("*TRG") def trigger_immediately(self): """Configure measurements to be taken with the internal trigger at the maximum sampling rate. """ self.write(":TRIG:SOUR IMM;") def trigger_on_bus(self): """Configure the trigger to detect events based on the bus trigger, which can be activated by :meth:`~.trigger`. """ self.write(":TRIG:SOUR BUS") def sample_continuously(self): """Configure the instrument to continuously read samples and turn off any buffer or output triggering. """ self.disable_buffer() self.trigger_immediately() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley2200.py0000644000175100001770000000752214623331163024142 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class PSChannel(Channel): """Implementation of a Keithley 2200 channel.""" VOLTAGE_RANGE = [0, 70] CURRENT_RANGE = [0, 45] output_enabled = Instrument.control( "SOURCE:OUTP:ENAB?", "SOURCE:OUTP:ENAB %d", """ Control the output state.""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) voltage_setpoint = Instrument.control( "VOLT?", "VOLT %g", """ Control output voltage in Volts.""", validator=strict_range, values=VOLTAGE_RANGE, ) current_limit = Instrument.control( "CURR?", "CURR %g", """ Control output current in Amps.""", validator=strict_range, values=CURRENT_RANGE, ) current = Instrument.measurement( "MEAS:CURR?", """ Measure the current in Amps.""", ) voltage = Instrument.measurement( "MEAS:VOLT?", """ Measure the voltage in Volts.""", ) power = Instrument.measurement( "MEAS:POW?", """ Measure the power in watts.""", ) voltage_limit = Instrument.control( "VOLT:LIM?", "VOLT:LIM %g", """ Control the maximum voltage that can be set.""", validator=strict_range, values=VOLTAGE_RANGE, ) voltage_limit_enabled = Instrument.control( "VOLT:LIM:STAT?", "VOLT:LIM:STAT %d", """ Control whether the maximum voltage limit is enabled.""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) def insert_id(self, command): return f"INST:SEL CH{self.id};{command}" class Keithley2200(SCPIUnknownMixin, Instrument): """Represents the Keithley 2200 Power Supply.""" def __init__(self, adapter, name="Keithley2200", **kwargs): super().__init__(adapter, name, **kwargs) ch_1 = Instrument.ChannelCreator(PSChannel, 1) ch_2 = Instrument.ChannelCreator(PSChannel, 2) ch_3 = Instrument.ChannelCreator(PSChannel, 3) display_enabled = Instrument.control( "DISP?", ":DISP %d", """Control whether the display is enabled.""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) display_text_data = Instrument.control( ":DISP:TEXT:DATA?", ":DISP:TEXT:DATA '%s'", """Control text to be displayed(32 characters).""", get_process=lambda v: v.replace('"', ""), ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley2260B.py0000644000175100001770000001172614623331163024253 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set import logging from warnings import warn log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Keithley2260B(SCPIMixin, Instrument): """ Represents the Keithley 2260B Power Supply (minimal implementation) and provides a high-level interface for interacting with the instrument. For a connection through tcpip, the device only accepts connections at port 2268, which cannot be configured otherwise. example connection string: 'TCPIP::xxx.xxx.xxx.xxx::2268::SOCKET' the read termination for this interface is \n .. code-block:: python source = Keithley2260B("GPIB::1") source.voltage = 1 print(source.voltage) print(source.current) print(source.power) print(source.applied) """ def __init__(self, adapter, name="Keithley 2260B DC Power Supply", read_termination="\n", **kwargs): super().__init__( adapter, name, read_termination=read_termination, **kwargs ) output_enabled = Instrument.control( "OUTPut?", "OUTPut %d", """Control whether the source is enabled, takes values True or False. (bool)""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) current_limit = Instrument.control( ":SOUR:CURR?", ":SOUR:CURR %g", """Control the source current in amps. This is not checked against the allowed range. Depending on whether the instrument is in constant current or constant voltage mode, this might differ from the actual current achieved. (float)""", ) voltage_setpoint = Instrument.control( ":SOUR:VOLT?", ":SOUR:VOLT %g", """Control the source voltage in volts. This is not checked against the allowed range. Depending on whether the instrument is in constant current or constant voltage mode, this might differ from the actual voltage achieved. (float)""", ) power = Instrument.measurement( ":MEAS:POW?", """Get the power (in Watt) the dc power supply is putting out. """, ) voltage = Instrument.measurement( ":MEAS:VOLT?", """Get the voltage (in Volt) the dc power supply is putting out. """, ) current = Instrument.measurement( ":MEAS:CURR?", """Get the current (in Ampere) the dc power supply is putting out. """, ) applied = Instrument.control( ":APPly?", ":APPly %g,%g", """Control voltage (volts) and current (amps) simultaneously. Values need to be supplied as tuple of (voltage, current). Depending on whether the instrument is in constant current or constant voltage mode, the values achieved by the instrument will differ from the ones set. """, ) @property def error(self): """Get the next error of the instrument (list of code and message).""" warn("Deprecated to use `error`, use `next_error` instead.", FutureWarning) return self.next_error @property def enabled(self): """Control whether the output is enabled, see :attr:`output_enabled`.""" log.warning('Deprecated property name "enabled", use the identical "output_enabled", ' 'instead.', FutureWarning) return self.output_enabled @enabled.setter def enabled(self, value): log.warning('Deprecated property name "enabled", use the identical "output_enabled", ' 'instead.', FutureWarning) self.output_enabled = value def shutdown(self): """ Disable output, call parent function""" self.output_enabled = False super().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley2306.py0000644000175100001770000006212314623331163024147 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Keithley2306Channel(Channel): """ Implementation of a Keithley 2306 channel. """ enabled = Channel.control( ":OUTPUT{ch}:STAT?", ":OUTPUT{ch}:STAT %d", """A boolean property that controls whether the output is enabled, takes values True or False. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) bandwidth = Channel.control( ":OUTPUT{ch}:BAND?", ":OUTPUT{ch}:BAND %s", """A string property that controls the output bandwidth when the output is enabled and the current range is set to 5 A. Takes values 'HIGH' or 'LOW'. If the output is disabled or the current range is set to 5 mA the bandwidth is 'LOW'. """, validator=strict_discrete_set, values={'low': 'LOW', 'high': 'HIGH'}, map_values=True, ) sense_mode = Channel.control( ":SENS{ch}:FUNC?", ":SENS{ch}:FUNC \"%s\"", """A string property that controls the channel sense mode, which can take the values 'voltage', 'current', 'dvm', 'pulse_current', or 'long_integration'. """, validator=strict_discrete_set, values={'voltage': 'VOLT', 'current': 'CURR', 'dvm': 'DVM', 'pulse_current': 'PCUR', 'long_integration': 'LINT'}, map_values=True, get_process=lambda v: v.replace('"', ''), ) nplc = Channel.control( ":SENS{ch}:NPLC?", ":SENS{ch}:NPLC %g", """A floating point property that controls the number of power line cycles (NPLC) for voltage, current, and DVM measurements. Takes values from 0.01 to 10. """, validator=truncated_range, values=[0.01, 10], ) average_count = Channel.control( ":SENS{ch}:AVER?", ":SENS{ch}:AVER %d", """An integer property that controls the average count for voltage, current, and DVM measurements. Takes values from 1 to 10. """, validator=truncated_range, values=[1, 10], ) current_range = Channel.control( ":SENS{ch}:CURR:RANG?", ":SENS{ch}:CURR:RANG %g", """A floating point property that controls the current range which takes values of 5 mA and 5 A (or 500 mA and 5 A for the 2306-PJ).""", validator=strict_discrete_set, values=[0.005, 0.5, 5], ) current_range_auto = Channel.control( ":SENS{ch}:CURR:RANG:AUTO?", ":SENS{ch}:CURR:RANG:AUTO %d", """A boolean point property that controls whether current range is in auto mode. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) pulse_current_average_count = Channel.control( ":SENS{ch}:PCUR:AVER?", ":SENS{ch}:PCUR:AVER %d", """An integer property that controls the average count for pulse current measurements. Takes values from 1 to either 100 if pulse_current_measure_enabled is set to True, 5000 otherwise. """, validator=truncated_range, values=[1, 5000], ) pulse_current_measure_enabled = Channel.control( ":SENS{ch}:PCUR:SYNC?", ":SENS{ch}:PCUR:SYNC %d", """A boolean property that controls whether pulse current measurements are enabled (True) or whether the channel is in digitization mode (False). """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) pulse_current_trigger_delay = Channel.control( ":SENS{ch}:PCUR:SYNC:DEL?", ":SENS{ch}:PCUR:SYNC:DEL %g", """A floating point property that controls the pulse current trigger delay in seconds. Takes values from 0 to either 0.1 if pulse_current_measure_enabled is set to True, 5 otherwise.""", validator=truncated_range, values=[0, 5], ) pulse_current_trigger_level = Channel.control( ":SENS{ch}:PCUR:SYNC:TLEV?", ":SENS{ch}:PCUR:SYNC:TLEV %g", """A floating point property that controls the pulse current trigger level in amps. Takes values between 0 and 5.""", validator=truncated_range, values=[0, 5], ) pulse_current_mode = Channel.control( ":SENS{ch}:PCUR:MODE?", ":SENS{ch}:PCUR:MODE %s", """A string property that controls the pulse current measurement mode, which can take the values 'high', 'low', or 'average'. """, validator=strict_discrete_set, values={'high': 'HIGH', 'low': 'LOW', 'average': 'AVER'}, map_values=True, ) def pulse_current_time_auto(self): """Arranges for the instrument to control integration times. """ self.write(":SENS{ch}:PCUR:TIME:AUTO") pulse_current_time_high = Channel.control( ":SENS{ch}:PCUR:TIME:HIGH?", ":SENS{ch}:PCUR:TIME:HIGH %g", """A floating point property that controls the integration time (in seconds) for high pulse measurements. Takes on values between 33.33333e-06 and 0.8333. """, validator=truncated_range, values=[33.33333e-06, 0.8333], ) pulse_current_time_low = Channel.control( ":SENS{ch}:PCUR:TIME:LOW?", ":SENS{ch}:PCUR:TIME:LOW %g", """A floating point property that controls the integration time (in seconds) for low pulse measurements. Takes on values between 33.33333e-06 and 0.8333. """, validator=truncated_range, values=[33.33333e-06, 0.8333], ) pulse_current_time_average = Channel.control( ":SENS{ch}:PCUR:TIME:AVER?", ":SENS{ch}:PCUR:TIME:AVER %g", """A floating point property that controls the integration time (in seconds) for average pulse measurements. Takes on values between 33.33333e-06 and 0.8333. """, validator=truncated_range, values=[33.33333e-06, 0.8333], ) pulse_current_time_digitize = Channel.control( ":SENS{ch}:PCUR:TIME:DIG?", ":SENS{ch}:PCUR:TIME:DIG %g", """A floating point property that controls the integration time (in seconds) for digitizing or burst pulse measurements. Takes on values between 33.33333e-06 and 0.8333. """, validator=truncated_range, values=[33.33333e-06, 0.8333], ) pulse_current_fast_enabled = Channel.control( ":SENS{ch}:PCUR:FAST?", ":SENS{ch}:PCUR:FAST %d", """A boolean property that controls whether pulse current fast readings are enabled. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) pulse_current_search_enabled = Channel.control( ":SENS{ch}:PCUR:SEAR?", ":SENS{ch}:PCUR:SEAR %d", """A boolean property that controls whether pulse current search is enabled. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) pulse_current_detect_enabled = Channel.control( ":SENS{ch}:PCUR:DET?", ":SENS{ch}:PCUR:DET %d", """A boolean property that controls whether pulse current detection mode is enabled. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) pulse_current_timeout = Channel.control( ":SENS{ch}:PCUR:TOUT?", ":SENS{ch}:PCUR:TOUT %g", """A floating point property that controls the pulse current timeout in seconds, which takes on values between 0.005 and 32. """, validator=truncated_range, values=[0.005, 32], ) long_integration_trigger_edge = Channel.control( ":SENS{ch}:LINT:TEDG?", ":SENS{ch}:LINT:TEDG %s", """A string property that controls the long integration trigger edge, which can take the values 'rising', 'falling', or 'neither'. """, validator=strict_discrete_set, values={'rising': 'RISING', 'falling': 'FALLING', 'neither': 'NEITHER'}, map_values=True, ) long_integration_time = Channel.control( ":SENS{ch}:LINT:TIME?", ":SENS{ch}:LINT:TIME %g", """A floating point property that controls the long integration time in seconds, which takes on values in the range of 0.850 for 60 Hz and 0.840 for 50 Hz up to 60. """, validator=truncated_range, values=[0.840, 60], ) def long_integration_time_auto(self): """Arranges for the instrument to control integration times. """ self.write(":SENS{ch}:LINT:TIME:AUTO") long_integration_trigger_level = Channel.control( ":SENS{ch}:LINT:TLEV?", ":SENS{ch}:LINT:TLEV %g", """A floating point property that controls the long integration trigger level in amps, which takes values between 0 and 5. """, validator=truncated_range, values=[0, 5], ) long_integration_timeout = Channel.control( ":SENS{ch}:LINT:TOUT?", ":SENS{ch}:LINT:TOUT %g", """A floating point property that controls the long integration timeout in seconds, which takes values between 1 and 63. """, validator=truncated_range, values=[1, 63], ) long_integration_fast_enabled = Channel.control( ":SENS{ch}:LINT:FAST?", ":SENS{ch}:LINT:FAST %d", """A boolean property that controls whether long integration fast readings are enabled. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) long_integration_search_enabled = Channel.control( ":SENS{ch}:LINT:SEAR?", ":SENS{ch}:LINT:SEAR %d", """A boolean property that controls whether long integration search is enabled. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) long_integration_detect_enabled = Channel.control( ":SENS{ch}:LINT:DET?", ":SENS{ch}:LINT:DET %d", """A boolean property that controls whether long integration detection mode is enabled. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) source_voltage = Channel.control( ":SOUR{ch}:VOLT?", ":SOUR{ch}:VOLT %g", """A floating point property that controls the source voltage in volts, which takes values between 0 and 15. """, validator=truncated_range, values=[0, 15], ) source_voltage_protection = Channel.control( ":SOUR{ch}:VOLT:PROT?", ":SOUR{ch}:VOLT:PROT %g", """A floating point property that controls the source voltage protection offset in volts, which takes values between 0 and 8. """, validator=truncated_range, values=[0, 8], ) source_voltage_protection_enabled = Channel.measurement( ":SOUR{ch}:VOLT:PROT:STAT?", """A boolean property that returns the source voltage protection state. If this property is True, the source has been shut off in accordance with the source voltage protection settings. If this property is False, the source has not been shut off due to voltage protection. """, cast=bool ) source_voltage_protection_clamp_enabled = Channel.control( ":SOUR{ch}:VOLT:PROT:CLAM?", ":SOUR{ch}:VOLT:PROT:CLAM %d", """A boolean property that controls whether source voltage protection clamp is enabled. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) source_current_limit = Channel.control( ":SOUR{ch}:CURR?", ":SOUR{ch}:CURR %g", """A floating point property that controls the source current limit in amps, which takes values between 0.006 and 5. """, validator=truncated_range, values=[0.006, 5], ) source_current_limit_type = Channel.control( ":SOUR{ch}:CURR:TYPE?", ":SOUR{ch}:CURR:TYPE %s", """A string property that controls source current limit type, which can take the values 'limit' or 'trip'. """, validator=strict_discrete_set, values={'limit': 'LIM', 'trip': 'TRIP'}, map_values=True, ) source_current_limit_enabled = Channel.measurement( ":SOUR{ch}:CURR:STAT?", """A boolean property that returns the source current limit state. If this property is True, the source is in either in current limit mode, or has tripped (shut off), based on the `source_current_limit_type` setting. If this property is False, the source is not being limited and has not been tripped. """, cast=bool ) last_reading = Channel.measurement( ":FETCH{ch}?", """A floating point property that returns the last reading. """ ) last_readings = Channel.measurement( ":FETCH{ch}:ARR?", """A floating point array property that returns the last readings. """, get_process=lambda v: v if isinstance(v, list) else [v] ) reading = Channel.measurement( ":READ{ch}?", """A floating point property that triggers and returns a reading in accordance with sense_mode. """ ) readings = Channel.measurement( ":READ{ch}:ARR?", """A floating point array property that triggers and returns readings in accordance with sense_mode. """, get_process=lambda v: v if isinstance(v, list) else [v] ) measured_voltage = Channel.measurement( ":MEAS{ch}:VOLT?", """A floating point property that triggers and returns a voltage reading. """ ) measured_voltages = Channel.measurement( ":MEAS{ch}:ARR:VOLT?", """A floating point array property that triggers and returns voltage readings. """, get_process=lambda v: v if isinstance(v, list) else [v] ) measured_current = Channel.measurement( ":MEAS{ch}:CURR?", """A floating point property that triggers and returns a current reading. """ ) measured_currents = Channel.measurement( ":MEAS{ch}:ARR:CURR?", """A floating point array property that triggers and returns current readings. """, get_process=lambda v: v if isinstance(v, list) else [v] ) dvm_voltage = Channel.measurement( ":MEAS{ch}:DVM?", """A floating point property that triggers and returns a DVM voltage reading. """ ) dvm_voltages = Channel.measurement( ":MEAS{ch}:ARR:DVM?", """A floating point array property that triggers and returns DVM voltage readings. """, get_process=lambda v: v if isinstance(v, list) else [v] ) pulse_current = Channel.measurement( ":MEAS{ch}:PCUR?", """A floating point property that returns a pulse current reading. """ ) pulse_currents = Channel.measurement( ":MEAS{ch}:ARR:PCUR?", """A floating point array property that triggers and returns pulse current readings. """, get_process=lambda v: v if isinstance(v, list) else [v] ) long_integration_current = Channel.measurement( ":MEAS{ch}:LINT?", """A floating point property that returns a long integration current reading. """ ) long_integration_currents = Channel.measurement( ":MEAS{ch}:ARR:LINT?", """A floating point array property that triggers and returns long integration current readings. """, get_process=lambda v: v if isinstance(v, list) else [v] ) class BatteryChannel(Keithley2306Channel): """ Implementation of a Keithley 2306 battery channel. """ impedance = Keithley2306Channel.control( ":OUTPUT{ch}:IMP?", ":OUTPUT{ch}:IMP %g", """A floating point property that controls the output impedance in ohms. Takes values from 0 to 1, in 10 milliohm steps.""", validator=truncated_range, values=[0, 1], ) pulse_current_step_enabled = Keithley2306Channel.control( ":SENS{ch}:PCUR:STEP?", ":SENS{ch}:PCUR:STEP %d", """A boolean property that controls whether a series of pulse current step measurements is enabled.""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) pulse_current_step_up_count = Keithley2306Channel.control( ":SENS{ch}:PCUR:STEP:UP?", ":SENS{ch}:PCUR:STEP:UP %d", """An integer property that controls the number of up steps. Takes values from 0 to 20 (max is both up and down combined). """, validator=truncated_range, values=[0, 20], ) pulse_current_step_down_count = Keithley2306Channel.control( ":SENS{ch}:PCUR:STEP:DOWN?", ":SENS{ch}:PCUR:STEP:DOWN %d", """An integer property that controls the number of down steps. Takes values from 0 to 20 (max is both up and down combined). """, validator=truncated_range, values=[0, 20], ) pulse_current_step_time = Keithley2306Channel.control( ":SENS{ch}:PCUR:STEP:TIME?", ":SENS{ch}:PCUR:STEP:TIME %g", """A floating point property that controls the integration time for up plus down steps in seconds. Takes values from 33.33333e-06 to 100e-3. """, validator=truncated_range, values=[33.33333e-06, 100e-3], ) pulse_current_step_timeout = Keithley2306Channel.control( ":SENS{ch}:PCUR:STEP:TOUT?", ":SENS{ch}:PCUR:STEP:TOUT %g", """A floating point property that controls the integration timeout for pulse current steps in seconds (for all but the first step). Takes values from 2e-3 to 200e-3. """, validator=truncated_range, values=[2e-3, 200e-3], ) pulse_current_step_timeout_initial = Keithley2306Channel.control( ":SENS{ch}:PCUR:STEP:TOUT:INIT?", ":SENS{ch}:PCUR:STEP:TOUT:INIT %g", """A floating point property that controls the integration timeout for the initial pulse current step in seconds. Takes values from 10e-3 to 60. """, validator=truncated_range, values=[10e-3, 60], ) pulse_current_step_delay = Keithley2306Channel.control( ":SENS{ch}:PCUR:STEP:DEL?", ":SENS{ch}:PCUR:STEP:DEL %g", """A floating point property that controls the pulse current step delay in seconds. Takes values from 0 to 100e-3 in 10e-6 increments. """, validator=truncated_range, values=[0, 100e-3], ) pulse_current_step_range = Keithley2306Channel.control( ":SENS{ch}:PCUR:STEP:RANG?", ":SENS{ch}:PCUR:STEP:RANG %g", """A floating point property that controls the pulse current step trigger level range in amps. Takes values of 100e-3, 1, or 5. """, validator=strict_discrete_set, values=[100e-3, 1, 5], ) pulse_current_trigger_level_range = Keithley2306Channel.control( ":SENS{ch}:PCUR:SYNC:TLEV:RANG?", ":SENS{ch}:PCUR:SYNC:TLEV:RANG %g", """A floating point property that controls the pulse current trigger level range in amps. Takes values of 100e-3, 1, or 5. """, validator=strict_discrete_set, values=[100e-3, 1, 5], ) long_integration_trigger_level_range = Keithley2306Channel.control( ":SENS{ch}:LINT:TLEV:RANG?", ":SENS{ch}:LINT:TLEV:RANG %g", """A floating point property that controls the long integration trigger level range in amps. Takes values of 100e-3, 1, or 5. """, validator=strict_discrete_set, values=[100e-3, 1, 5], ) def pulse_current_step(self, step_number): """Create a new current step point for this instrument. :param: step_number: int: the number of the step to be created :type: :class:`.Step` """ return Step(self.parent, step_number) class Step(Channel): """ Implementation of a Keithley 2306 step. """ placeholder = 'step' trigger_level = Channel.control( ":SENS:PCUR:STEP:TLEV{step}?", ":SENS:PCUR:STEP:TLEV{step} %g", """A floating point property that controls the pulse current step trigger level range in amps. Takes values from 0 up to the range set via pulse_current_step_range.""", validator=truncated_range, values=[0, 5], ) def __init__(self, instrument, number, **kwargs): super().__init__(instrument, number, **kwargs) class Relay(Channel): """ Implementation of a Keithley 2306 relay. """ closed = Channel.control( ":OUTP:REL{ch}?", ":OUTP:REL{ch} %s", """A boolean property that controls whether the relay is closed (True) or open (False). """, validator=strict_discrete_set, values={True: 'ONE', False: 'ZERO'}, map_values=True ) class Keithley2306(SCPIUnknownMixin, Instrument): """ Represents the Keithley 2306 Dual Channel Battery/Charger Simulator. """ def __init__(self, adapter, name="Keithley 2306", **kwargs): super().__init__( adapter, name, **kwargs ) self.ch1 = BatteryChannel(self, 1) self.ch2 = Keithley2306Channel(self, 2) self.relay1 = Relay(self, 1) self.relay2 = Relay(self, 2) self.relay3 = Relay(self, 3) self.relay4 = Relay(self, 4) display_enabled = Instrument.control( ":DISP:ENAB?", ":DISP:ENAB %d", """A boolean property that controls whether the display is enabled, takes values True or False. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) display_brightness = Instrument.control( ":DISP:BRIG?", ":DISP:BRIG %g", """A floating point property that controls the display brightness, takes values beteween 0.0 and 1.0. A blank display is 0.0, 1/4 brightness is for values less or equal to 0.25, otherwise 1/2 brightness for values less than or equal to 0.5, otherwise 3/4 brightness for values less than or equal to 0.75, otherwise full brightness. """, validator=truncated_range, values=[0, 1], ) display_channel = Instrument.control( ":DISP:CHAN?", ":DISP:CHAN %d", """An integer property that controls the display channel, takes values 1 or 2. """, validator=strict_discrete_set, values=[1, 2], ) display_text_data = Instrument.control( ":DISP:TEXT:DATA?", ":DISP:TEXT:DATA \"%s\"", """A string property that control text to be displayed, takes strings up to 32 characters. """, get_process=lambda v: v.replace('"', '') ) display_text_enabled = Instrument.control( ":DISP:TEXT:STAT?", ":DISP:TEXT:STAT %d", """A boolean property that controls whether display text is enabled, takes values True or False. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) both_channels_enabled = Instrument.setting( ":BOTHOUT%s", """A boolean setting that controls whether both channel outputs are enabled, takes values of True or False. """, validator=strict_discrete_set, values={True: "ON", False: "OFF"}, map_values=True, ) def ch(self, channel_number): """Get a channel from this instrument. :param: channel_number: int: the number of the channel to be selected :type: :class:`.Keithley2306Channel` """ 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 relay(self, relay_number): """Get a relay channel from this instrument. :param: relay_number: int: the number of the relay to be selected :type: :class:`~Relay` """ if relay_number == 1: return self.relay1 elif relay_number == 2: return self.relay2 elif relay_number == 3: return self.relay3 elif relay_number == 4: return self.relay4 else: raise ValueError("Invalid relay number. Must be 1, 2, 3, or 4") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley2400.py0000644000175100001770000007020414623331163024141 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from warnings import warn import numpy as np from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.errors import RangeException from pymeasure.instruments.validators import truncated_range, strict_discrete_set from .buffer import KeithleyBuffer log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Keithley2400(KeithleyBuffer, SCPIMixin, Instrument): """ Represents the Keithley 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 %s", """ 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, ) line_frequency = Instrument.control( ":SYST:LFR?", ":SYST:LFR %d", """ An integer property that controls the line frequency in Hertz. Valid values are 50 and 60. """, validator=strict_discrete_set, values=[50, 60], cast=int, ) line_frequency_auto = Instrument.control( ":SYST:LFR:AUTO?", ":SYST:LFR:AUTO %d", """ A boolean property that enables or disables auto line frequency. Valid values are True and False. """, values={True: 1, False: 0}, 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: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( ":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, name="Keithley 2400 SourceMeter", **kwargs): super().__init__( adapter, name, **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(f":SYST:BEEP {frequency:g}, {duration:g}") 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): warn("Deprecated to use `error`, use `next_error` instead.", FutureWarning) return self.next_error 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 :meth:`~.trigger`. """ 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() super().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley2450.py0000644000175100001770000005563014623331163024154 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from warnings import warn import numpy as np from pymeasure.instruments import Instrument, SCPIMixin 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(KeithleyBuffer, SCPIMixin, Instrument): """ Represents the Keithley 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 """ def __init__(self, adapter, name="Keithley 2450 SourceMeter", **kwargs): super().__init__( adapter, name, **kwargs ) 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 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(f":SYST:BEEP {frequency:g}, {duration:g}") 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): warn("Deprecated to use `error`, use `next_error` instead.", FutureWarning) return self.next_error 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() super().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley2600.py0000644000175100001770000002672114623331163024150 0ustar00runnerdocker# This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from warnings import warn import numpy as np from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set # Setup logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Keithley2600(SCPIUnknownMixin, Instrument): """Represents the Keithley 2600 series (channel A and B) SourceMeter""" def __init__(self, adapter, name="Keithley 2600 SourceMeter", **kwargs): super().__init__( adapter, name, **kwargs ) self.ChA = Channel(self, 'a') self.ChB = Channel(self, 'b') @property def next_error(self): """ Returns a tuple of an error code and message from a single error. """ err = self.ask('print(errorqueue.next())') err = err.split('\t') # Keithley Instruments Inc. sometimes on startup # if tab delimitated message is greater than one, grab first two as code, message # otherwise, assign code & message to returned error if len(err) > 1: err = (int(float(err[0])), err[1]) code = err[0] message = err[1].replace('"', '') else: code = message = err[0] log.info(f"ERROR {str(code)},{str(message)} - len {str(len(err))}") return (code, message) @property def error(self): warn("Deprecated to use `error`, use `next_error` instead.", FutureWarning) return self.next_error class Channel: def __init__(self, instrument, channel): self.instrument = instrument self.channel = channel def ask(self, cmd): return float(self.instrument.ask(f'print(smu{self.channel}.{cmd})')) def write(self, cmd): self.instrument.write(f'smu{self.channel}.{cmd}') def values(self, cmd, **kwargs): """ Reads a set of values from the instrument through the adapter, passing on any key-word arguments. """ return self.instrument.values(f'print(smu{self.channel}.{cmd})') def binary_values(self, cmd, header_bytes=0, dtype=np.float32): return self.instrument.binary_values('print(smu%s.%s)' % (self.channel, cmd,), header_bytes, dtype) def check_errors(self): return self.instrument.check_errors() source_output = Instrument.control( 'source.output', 'source.output=%d', """Property controlling the channel output state (ON of OFF) """, validator=strict_discrete_set, values={'OFF': 0, 'ON': 1}, map_values=True ) source_mode = Instrument.control( 'source.func', 'source.func=%d', """Property controlling the channel source function (Voltage or Current) """, validator=strict_discrete_set, values={'voltage': 1, 'current': 0}, map_values=True ) measure_nplc = Instrument.control( 'measure.nplc', 'measure.nplc=%f', """ Property controlling the nplc value """, validator=truncated_range, values=[0.001, 25], map_values=True ) ############### # Current (A) # ############### current = Instrument.measurement( 'measure.i()', """ Reads the current in Amps """ ) source_current = Instrument.control( 'source.leveli', 'source.leveli=%f', """ Property controlling the applied source current """, validator=truncated_range, values=[-1.5, 1.5] ) compliance_current = Instrument.control( 'source.limiti', 'source.limiti=%f', """ Property controlling the source compliance current """, validator=truncated_range, values=[-1.5, 1.5] ) source_current_range = Instrument.control( 'source.rangei', 'source.rangei=%f', """Property controlling the source current range """, validator=truncated_range, values=[-1.5, 1.5] ) current_range = Instrument.control( 'measure.rangei', 'measure.rangei=%f', """Property controlling the measurement current range """, validator=truncated_range, values=[-1.5, 1.5] ) ############### # Voltage (V) # ############### voltage = Instrument.measurement( 'measure.v()', """ Reads the voltage in Volts """ ) source_voltage = Instrument.control( 'source.levelv', 'source.levelv=%f', """ Property controlling the applied source voltage """, validator=truncated_range, values=[-200, 200] ) compliance_voltage = Instrument.control( 'source.limitv', 'source.limitv=%f', """ Property controlling the source compliance voltage """, validator=truncated_range, values=[-200, 200] ) source_voltage_range = Instrument.control( 'source.rangev', 'source.rangev=%f', """Property controlling the source current range """, validator=truncated_range, values=[-200, 200] ) voltage_range = Instrument.control( 'measure.rangev', 'measure.rangev=%f', """Property controlling the measurement voltage range """, validator=truncated_range, values=[-200, 200] ) #################### # Resistance (Ohm) # #################### resistance = Instrument.measurement( 'measure.r()', """ Reads the resistance in Ohms """ ) wires_mode = Instrument.control( 'sense', 'sense=%d', """Property controlling the resistance measurement mode: 4 wires or 2 wires""", validator=strict_discrete_set, values={'4': 1, '2': 0}, map_values=True ) ####################### # Measurement Methods # ####################### 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.001 to 25 :param voltage: Upper limit of voltage in Volts, from -200 V to 200 V :param auto_range: Enables auto_range if True, else uses the set voltage """ log.info("%s is measuring voltage." % self.channel) self.write('measure.v()') self.write('measure.nplc=%f' % nplc) if auto_range: self.write('measure.autorangev=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.001 to 25 :param current: Upper limit of current in Amps, from -1.5 A to 1.5 A :param auto_range: Enables auto_range if True, else uses the set current """ log.info("%s is measuring current." % self.channel) self.write('measure.i()') self.write('measure.nplc=%f' % nplc) if auto_range: self.write('measure.autorangei=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('source.autorangei=1') else: self.write('source.autorangev=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:`~.Keithley2600.compliance_voltage` :param current_range: A :attr:`~.Keithley2600.current_range` value or None """ log.info("%s is sourcing current." % self.channel) 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:`~.Keithley2600.compliance_current` :param voltage_range: A :attr:`~.Keithley2600.voltage_range` value or None """ log.info("%s is sourcing voltage." % self.channel) 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 ramp_to_voltage(self, target_voltage, steps=30, pause=0.1): """ 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 ramp_to_current(self, target_current, steps=30, pause=0.1): """ 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 shutdown(self): """ Ensures that the current or voltage is turned to zero and disables the output. """ log.info("Shutting down channel %s." % self.channel) if self.source_mode == 'current': self.ramp_to_current(0.0) else: self.ramp_to_voltage(0.0) self.source_output = 'OFF' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley2700.py0000644000175100001770000003004214623331163024140 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 warnings import warn from pymeasure.instruments import Instrument, SCPIMixin from .buffer import KeithleyBuffer import numpy as np import time log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) 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 = [f"{value:d}"] elif isinstance(value, (list, tuple, np.ndarray, range)): clist = [f"{x:d}" for x in value] else: raise ValueError(f"Type of value ({type(value)}) not valid") # 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( f"Channel number {value:g} not valid." ) # 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(KeithleyBuffer, SCPIMixin, Instrument): """ Represents the Keithley 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)) def __init__(self, adapter, name="Keithley 2700 MultiMeter/Switch System", **kwargs): super().__init__( adapter, name, **kwargs ) self.check_errors() self.determine_valid_channels() # 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 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( f"Card type {card} at slot {slot} is not yet implemented." ) 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, ndmin=1) columns = np.array(columns, ndmin=1) 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(f":SYST:BEEP {frequency:g}, {duration:g}") 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): warn("Deprecated to use `error`, use `next_error` instead.", FutureWarning) return self.next_error 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley2750.py0000644000175100001770000000667214623331163024161 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIMixin 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(SCPIMixin, 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, name="Keithley 2750 Multimeter/Switch System", **kwargs): super().__init__( adapter, name, **kwargs ) def open(self, channel): """ Opens (disconnects) the specified channel. :param int channel: 3-digit number for the channel :return: None """ self.write(f":ROUTe:MULTiple:OPEN (@{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(f":ROUTe:MULTiple:CLOSe (@{channel})") def open_all(self): """ Opens (disconnects) all the channels on the switch matrix. :return: None """ self.write(":ROUTe:OPEN:ALL") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley6221.py0000644000175100001770000004602214623331163024147 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from warnings import warn import numpy as np from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.errors import RangeException from pymeasure.instruments.validators import truncated_range, strict_discrete_set from .buffer import KeithleyBuffer log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Keithley6221(KeithleyBuffer, SCPIMixin, Instrument): """ Represents the Keithley 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 """ def __init__(self, adapter, name="Keithley 6221 SourceMeter", **kwargs): super().__init__( adapter, name, **kwargs) ########## # 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], ) output_low_grounded = Instrument.control( ":OUTP:LTE?", "OUTP:LTE %d", """ A boolean property that controls whether the low output of the triax connection is connected to earth ground (True) or is floating (False). """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) ########## # 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, ) waveform_phasemarker_phase = Instrument.control( ":SOUR:WAVE:PMAR?", ":SOUR:WAVE:PMAR %g", """ A numerical property that controls the phase of the phase marker.""", validator=truncated_range, values=[-180, 180], ) waveform_phasemarker_line = Instrument.control( ":SOUR:WAVE:PMAR:OLIN?", ":SOUR:WAVE:PMAR:OLIN %d", """ A numerical property that controls the line of the phase marker.""", validator=truncated_range, values=[1, 6], ) 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 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(f":SYST:BEEP {frequency:g}, {duration:g}") 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): warn("Deprecated to use `error`, use `next_error` instead.", FutureWarning) return self.next_error 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 :meth:`~.trigger`. """ 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() super().shutdown() ############### # 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, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithley6517b.py0000644000175100001770000003033514623331163024321 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from warnings import warn import numpy as np from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.instruments.validators import truncated_range from .buffer import KeithleyBuffer log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Keithley6517B(KeithleyBuffer, SCPIMixin, Instrument): """ Represents the Keithley 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 """ def __init__(self, adapter, name="Keithley 6517B Electrometer/High Resistance Meter", **kwargs): super().__init__( adapter, name, **kwargs ) source_enabled = Instrument.measurement( "OUTPUT?", """ Reads a boolean value that is True if the source is enabled. """, cast=bool ) @staticmethod def extract_value(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 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 attribute """ 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 attribute """ 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 attribute """ 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): warn("Deprecated to use `error`, use `next_error` instead.", FutureWarning) return self.next_error 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 trigger_immediately(self): """ Configures measurements to be taken with the internal trigger at the maximum sampling rate. """ self.write(":TRIG:SOUR IMM;") def trigger_on_bus(self): """ Configures the trigger to detect events based on the bus trigger, which can be activated by :meth:`~.trigger`. """ self.write(":TRIG:SOUR BUS;") 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() super().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keithley/keithleyDMM6500.py0000644000175100001770000014740214623331163024511 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel, SCPIMixin from pymeasure.instruments.validators import ( truncated_range, truncated_discrete_set, strict_discrete_set, ) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) BOOL_MAPPINGS = {True: 1, False: 0} class ScannerCard2000Channel(Channel): MODES = { "voltage": "VOLT:DC", "voltage ac": "VOLT:AC", "resistance": "RES", "resistance 4W": "FRES", "diode": "DIOD", "capacitance": "CAP", "temperature": "TEMP", "continuity": "CONT", "period": "PER:VOLT", "frequency": "FREQ:VOLT", "voltage ratio": "VOLT:DC:RAT", "NONE": "NONE", } mode = Channel.control( ":SENS:FUNC? (@{ch})", ':SENS:FUNC "%s", (@{ch})', """ Control the configuration mode for measurements, which can take the values: ``current`` (DC), ``current ac``, ``voltage`` (DC), ``voltage ac``, ``resistance`` (2-wire), ``resistance 4W`` (4-wire), ``diode``, ``capacitance``, ``temperature``, ``continuity``, ``period``, ``frequency``, and ``voltage ratio``. """, validator=strict_discrete_set, values=MODES, map_values=True, get_process=lambda v: v.replace('"', ""), ) nplc = Channel.control( "{function}:NPLC? (@{ch})", "{function}:NPLC %s, (@{ch})", """ Control the integration time in number of power line cycles (NPLC). Valid values: 0.0005 to 15 (60Hz) or 12 (50Hz or 400Hz) This command is valid only for ``voltage``, 2-wire ohms, and 4-wire ohms. .. note:: Only ``voltage``, ``current``, ``resistance``, ``resistance 4W``, ``diode``, ``temperature``, and ``voltage ratio`` mode support NPLC setting. If current active mode doesn't support NPLC, this command will hang till adapter's timeout and cause -113 "Undefined header" error. """, validator=truncated_range, values=[0.0005, 15], ) range = Instrument.control( "{function}:RANG? (@{ch})", "{function}:RANG %s, (@{ch})", """ Control measuring range for currently active mode. For ``frequency`` and ``period`` measurements, :attr:`range` applies to the signal's input voltage, not its frequency""", ) autorange_enabled = Instrument.control( "{function}:RANG:AUTO? (@{ch})", "{function}:RANG:AUTO %d, (@{ch})", """ Control the autorange state for currently active mode. .. note:: If current active mode doesn't support autorange, this command will hang till adapter's timeout and cause -113 "Undefined header" error. """, validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) def _mode_command(self, mode=None): """Get SCPI's function name from mode.""" if mode is None: mode = self.mode return self.MODES[mode] def enable_filter(self, mode=None, type="repeat", count=1): """Enable the averaging filter for the active mode, or can set another mode by its name. :param mode: A valid :attr:`mode` name, or `None` for the active mode :param type: The type of averaging filter, could be ``REPeat``, ``MOVing``, or ``HYBRid``. :param count: A number of averages, which can take take values from 1 to 100 :return: Filter status read from the instrument """ mode_cmd = self._mode_command(mode) self.write(f":SENS:{mode_cmd}:AVER:STAT 1, (@{self.id})") self.write(f":SENS:{mode_cmd}:AVER:TCON {type}, (@{self.id})") self.write(f":SENS:{mode_cmd}:AVER:COUN {count}, (@{self.id})") return self.ask(f":SENS:{mode_cmd}:AVER:STAT? (@{self.id})") def disable_filter(self, mode=None): """Disable the averaging filter for the active mode, or can set another mode by its name. :param mode: A valid :attr:`mode` name, or `None` for the active mode :return: Filter status read from the instrument """ mode_cmd = self._mode_command(mode) self.write(f":SENS:{mode_cmd}:AVER:STAT 0, (@{self.id})") return self.ask(f":SENS:{mode_cmd}:AVER:STAT? (@{self.id})") def write(self, command): """Write a command to the instrument.""" if "{function}" in command: super().write(command.format(function=ScannerCard2000Channel.MODES[self.mode])) else: super().write(command) class KeithleyDMM6500(SCPIMixin, Instrument): """Represent the Keithely DMM6500 6½-Digit Multimeter and provide a high-level interface for interacting with the instrument. This class only uses "SCPI" command set (see also :attr:`command_set`) to communicate with the instrument. .. code-block:: python # Access via LAN ip_address = "xxx.xxx.xxx.xxx" dmm = KeithleyDMM6500(f"TCPIP::{ip_address}::inst0::INSTR") User can also use PyVISA to get DMM6500's USB port name and pass it to :class:`KeithleyDMM6500` .. code-block:: python import pyvisa rm = pyvisa.ResourceManager() resources = rm.list_resources() # assume there is only one USB instruments # Ex. ('USB0::1510::25856::01234567::0::INSTR') dmm = KeithleyDMM6500( resources[0] ) # Measure voltage dmm.measure_voltage() print(dmm.voltage) # Measure AC voltage dmm.measure_voltage(ac=True) print(dmm.voltage) """ MODES = { "voltage": "VOLT:DC", "voltage ac": "VOLT:AC", "current": "CURR:DC", "current ac": "CURR:AC", "resistance": "RES", "resistance 4W": "FRES", "diode": "DIOD", "capacitance": "CAP", "temperature": "TEMP", "continuity": "CONT", "period": "PER:VOLT", "frequency": "FREQ:VOLT", "voltage ratio": "VOLT:DC:RAT", } MODES_HAVE_AUTORANGE = ( "current", "current ac", "voltage", "voltage ac", "resistance", "resistance 4W", "capacitance", "voltage ratio", ) channels = Instrument.MultiChannelCreator(ScannerCard2000Channel, list(range(1, 11))) def __init__( self, adapter, name="Keithley DMM6500 6½-Digit Multimeter", read_termination="\n", **kwargs ): super().__init__( adapter, name, read_termination=read_termination, **kwargs) self.command_set = "SCPI" def __exit__(self, exc_type, exc_value, traceback): """Fully close the connection when the `with` code block finishes.""" self.adapter.close() def close(self): """Close the connection""" self.adapter.close() ########### # General # ########### command_set = Instrument.control( "*LANG?", "*LANG %s", """ Control the command set that to use with DMM6500. Reboot the instrument is needed after changing the command set. Available values are: :code:`SCPI`, :code:`TSP`, :code:`SCPI2000`, and :code:`SCPI34401`. The :attr:`KeithleyDMM6500` class was designed to use :code:`SCPI` command set only. .. note:: If you want to use TSP command set, you can use :attr:`write()` and :attr:`ask()` to send TSP command instead. """, validator=strict_discrete_set, values=["TSP", "SCPI", "SCPI2000", "SCPI34401"], ) mode = Instrument.control( ":SENS:FUNC?", ':SENS:FUNC "%s"', """ Control the active measure function. Available values are: ``current`` (DC), ``current ac``, ``voltage`` (DC), ``voltage ac``, ``resistance`` (2-wire), ``resistance 4W`` (4-wire), ``diode``, ``capacitance``, ``temperature``, ``continuity``, ``period``, ``frequency``, and ``voltage ratio``. """, validator=strict_discrete_set, values=MODES, map_values=True, get_process=lambda v: v.replace('"', ""), ) line_frequency = Instrument.measurement( ":SYST:LFR?", """ Get the power line frequency which automatically detected while the instrument is powered on.""", ) aperture = Instrument.control( "{function}:APER?", "{function}:APER %s", """ Control the aperture time of currently active :attr:`mode`. Valid values: ``MIN``, ``DEF``, ``MAX``, or number between 8.333u and 0.25 s. .. note:: Only ``voltage``, ``current``, ``resistance``, ``resistance 4W``, ``diode``, ``temperature``, ``frequency``, ``period``, and ``voltage ratio`` mode support aperture setting. If current active mode doesn't support aperture, this command will hang till adapter's timeout and cause -113 "Undefined header" error. """, ) range = Instrument.control( "{function}:RANG?", "{function}:RANG:AUTO 0;UPP %s", """ Control the positive full-scale measure range for currently active :attr:`mode`. Auto-range is disabled when this property is set. For frequency and period measurements, ranging applies to the signal's input voltage, not its frequency""", ) autorange_enabled = Instrument.control( "{function}:RANG:AUTO?", "{function}:RANG:AUTO %d", """ Control the autorange state for currently active :attr:`mode`. .. note:: If currently active mode doesn't support autorange, this command will hang till adapter's timeout and cause -113 "Undefined header" error. """, validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) relative = Instrument.control( "{function}:REL?", "{function}:REL %g", """ Control the relative offset value of currently active :attr:`mode`. When relative offset is enabled, all subsequent measured readings are offset by the value that is set for this command. If the instrument acquires the value, read this setting to return the value that was measured internally. See also the :attr:`relative_enabled`.""", ) relative_enabled = Instrument.control( "{function}:REL:STAT?", "{function}:REL:STAT %d", """ Control the relative offset value applied to new measurements for currently active :attr:`mode`.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) nplc = Instrument.control( "{function}:NPLC?", "{function}:NPLC %s", """ Control the integration time in number of power line cycles (NPLC) of currently active :attr:`mode`. This value sets the amount of time that the input signal is measured. Valid values are: 0.0005 to 15 (60Hz) or 12 (50Hz or 400Hz). .. note:: Only ``voltage``, ``current``, ``resistance``, ``resistance 4W``, ``diode``, ``temperature``, and ``voltage ratio`` mode support NPLC setting. If current active mode doesn't support NPLC, this command will hang till adapter's timeout and cause -113 "Undefined header" error. """, validator=truncated_range, values=[0.0005, 15], ) digits = Instrument.control( ":DISP:{function}:DIG?", ":DISP:{function}:DIG %d", """ Control the displaying number of digits for currently active :attr:`mode`. Available values are from 3 to 6 representing display digits from 3.5 to 6.5.""", validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, ) detector_bandwidth = Instrument.control( "{function}:DET:BAND?", "{function}:DET:BAND %s", """ Control the lowest frequency expected in the input signal in Hz ONLY for AC voltage and AC current measurement, Valid values: 3, 30, 300, ``MIN``, ``DEF``, ``MAX``.""", validator=strict_discrete_set, values=[3, 30, 300, "MIN", "DEF", "MAX"], ) autozero_enabled = Instrument.control( "{function}:AZER?", "{function}:AZER %d", """ Control automatic updates to the internal reference measurements (autozero) of the instrument. """, validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) system_time = Instrument.control( ":SYST:TIME? 1", ":SYST:TIME %s", """ Control system time on the instrument. Format of set is: ``year, month, day, hour, minute, second`` or ``hour, minute, second``. Example: Using ``time`` package to set instrument's clock: ``dmm.system_time = time.strftime("%Y, %m, %d, %H, %M, %S")`` """, ) def trigger_single_autozero(self): """Cause the instrument to refresh the reference and zero measurements once. Consequent autozero measurements are disabled.""" self.write("AZER:ONCE") terminals_used = Instrument.measurement( "ROUT:TERM?", """ Get which set of input and output terminals the instrument is using. Return can be ``FRONT`` or ``REAR``.""", values={"FRONT": "FRON", "REAR": "REAR"}, map_values=True, ) ########### # Display # ########### display_screen = Instrument.setting( ":DISP:SCR %s", """ Set displayed front-panel screen by the name. Available names are: ``HOME`` (home), ``HOME_LARG`` (home screen with large readings), ``READ`` (reading table), ``HIST`` (histogram), ``SWIPE_FUNC`` (FUNCTIONS swipe screen), ``SWIPE_GRAP`` (GRAPH swipe screen), ``SWIPE_SEC`` (SECONDARY swipe screen), ``SWIPE_SETT`` (SETTINGS swipe screen), ``SWIPE_STAT`` (STATISTICS swipe screen), ``SWIPE_USER`` (USER swipe screen), ``SWIPE_CHAN`` (CHANNEL swipe screen), ``SWIPE_NONS`` (NONSWITCH swipe screen), ``SWIPE_SCAN`` (SCAN swipe screen), ``CHANNEL_CONT`` (Channel control screen), ``CHANNEL_SETT`` (Channel settings screen), ``CHANNEL_SCAN`` (Channel scan screen), or ``PROC`` (minimal CPU resources). """, validator=strict_discrete_set, values=( "HOME", "HOME_LARG", "READ", "HIST", "SWIPE_FUNC", "SWIPE_GRAP", "SWIPE_SEC", "SWIPE_SETT", "SWIPE_STAT", "SWIPE_USER", "SWIPE_CHAN", "SWIPE_NONS", "SWIPE_SCAN", "CHANNEL_CONT", "CHANNEL_SETT", "CHANNEL_SCAN", "PROC", ), ) def displayed_text(self, top_line=None, bot_line=None): """Display text messages on the front-panel USER swipe screen. If no messages were defined, screen will be cleared. :param top_line: 1st line message :param bot_line: 2nd line message :return: None """ self.write(":DISP:CLE") self.display_screen = "SWIPE_USER" if top_line: self.write(f':DISP:USER1:TEXT "{top_line}"') if bot_line: self.write(f':DISP:USER2:TEXT "{bot_line}"') ############### # Current (A) # ############### current = Instrument.measurement( ":READ?", """ Measure a DC or AC current in Amps, based on the active :attr:`mode`.""", ) current_range = Instrument.control( ":SENS:CURR:RANG?", ":SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG %g", """ Control the DC current full-scale measure range in Amps. Available ranges are 10e-6, 100e-6, 1e-3, 10e-3, 100e-3, 1, 3 Amps (for front terminals), and 10 Amps (for rear terminals). Auto-range is disabled when this property is set. See also the :attr:`range`.""", validator=truncated_discrete_set, values=[10e-6, 100e-6, 1e-3, 10e-3, 100e-3, 1, 3, 10], ) current_relative = Instrument.control( ":SENS:CURR:REL?", ":SENS:CURR:REL %g", """ Control the DC current relative value in Amps (float strictly from -3 to 3). See also the :attr:`relative`.""", validator=truncated_range, values=[-3.0, 3.0], ) current_relative_enabled = Instrument.control( ":SENS:CURR:REL:STAT?", ":SENS:CURR:REL:STAT %d", """ Control a relative offset value applied to DC current measurement. See also the :attr:`relative_enabled`.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) current_nplc = Instrument.control( ":SENS:CURR:NPLC?", ":SENS:CURR:NPLC %g", """ Control the number of power line cycles (NPLC) for the DC current measurement (float strictly from 0.0005 to 15). See also the :attr:`nplc`.""", validator=truncated_range, values=[0.0005, 15], ) current_digits = Instrument.control( ":DISP:CURR:DIG?", ":DISP:CURR:DIG %d", """ Control the number of digits in the DC current readings (integer strictly from 3 to 6). See also the :attr:`digits`. """, validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, ) # Current (AC) current_ac_range = Instrument.control( ":SENS:CURR:AC:RANG?", ":SENS:CURR:AC:RANG:AUTO 0;:SENS:CURR:AC:RANG %g", """ Control the AC current positive full-scale measure range in Amps. Available ranges are 1e-3, 10e-3, 100e-3, 1, 3 Amps (for front terminals), and 10 Amps (for rear terminals). See also the :attr:`range`.""", validator=truncated_discrete_set, values=[1e-3, 10e-3, 100e-3, 1, 3, 10], ) current_ac_relative = Instrument.control( ":SENS:CURR:AC:REL?", ":SENS:CURR:AC:REL %g", """ Control the AC current relative value in Amps (float strictly from -3 to 3). See also the :attr:`relative`.""", validator=truncated_range, values=[-3.0, 3.0], ) current_ac_relative_enabled = Instrument.control( ":SENS:CURR:AC:REL:STAT?", ":SENS:CURR:AC:REL:STAT %d", """ Control a relative offset value applied to AC current measurement. See also the :attr:`relative_enabled`.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) current_ac_digits = Instrument.control( ":DISP:CURR:AC:DIG?", ":DISP:CURR:AC:DIG %d", """ Control the number of digits in the AC current readings (integer strictly from 3 to 6). See also the :attr:`digits`. """, validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, ) current_ac_bandwidth = Instrument.control( ":SENS:CURR:AC:DET:BAND?", ":SENS:CURR:AC:DET:BAND %g", """ Control the detector bandwidth in Hz for AC current measurement (integer strictly among 3, 30, and 300). """, validator=truncated_discrete_set, values=[3, 30, 300], ) def measure_current(self, max_current=10e-3, ac=False): """Configure 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 ############### # Voltage (V) # ############### # DC voltage = Instrument.measurement( ":READ?", """ Measure a DC or AC voltage in Volts, based on the active :attr:`mode`.""", ) voltage_range = Instrument.control( ":SENS:VOLT:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG %g", """ Control the DC voltage full-scale measure range in Volts. Available ranges are 0.1, 1, 10, 100, 1000. Auto-range is disabled when this property is set. See also the :attr:`range`.""", validator=truncated_discrete_set, values=[0.1, 1, 10, 100, 1000], ) voltage_relative = Instrument.control( ":SENS:VOLT:REL?", ":SENS:VOLT:REL %g", """ Control the DC voltage relative value in Volts (float strictly from -1000 to 1000). See also the :attr:`relative`.""", validator=truncated_range, values=[-1000, 1000], ) voltage_relative_enabled = Instrument.control( ":SENS:VOLT:REL:STAT?", ":SENS:VOLT:REL:STAT %d", """ Control a relative offset value applied to DC voltage measurement. See also the :attr:`relative_enabled`.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) voltage_nplc = Instrument.control( ":SENS:VOLT:NPLC?", ":SENS:VOLT:NPLC %g", """ Control the number of power line cycles (NPLC) for the DC voltage measurement (float strictly from 0.0005 to 15). See also the :attr:`nplc`.""", validator=truncated_range, values=[0.0005, 15], ) voltage_digits = Instrument.control( ":DISP:VOLT:DIG?", ":DISP:VOLT:DIG %d", """ Control the number of digits in the DC voltage readings (integer strictly from 3 to 6). See also the :attr:`digits`.""", validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, ) # AC voltage_ac_range = Instrument.control( ":SENS:VOLT:AC:RANG?", ":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:AC:RANG %g", """ Control the AC voltage positive full-scale measure range in Volts. Available ranges are 0.1, 1, 10, 100, 750. Auto-range is disabled when this property is set. See also the :attr:`range`. """, validator=truncated_discrete_set, values=[0.1, 1, 10, 100, 750], ) voltage_ac_relative = Instrument.control( ":SENS:VOLT:AC:REL?", ":SENS:VOLT:AC:REL %g", """ Control the AC voltage relative value in Volts (float strictly from -750 to 750). See also the :attr:`relative`. """, validator=truncated_range, values=[-750, 750], ) voltage_ac_relative_enabled = Instrument.control( ":SENS:VOLT:AC:REL:STAT?", ":SENS:VOLT:AC:REL:STAT %d", """ Control a relative offset value applied to AC voltage measurement. See also the :attr:`relative_enabled`. """, validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) voltage_ac_digits = Instrument.control( ":DISP:VOLT:AC:DIG?", ":DISP:VOLT:AC:DIG %d", """ Control the number of digits in the AC voltage readings (integer strictly from 3 to 6). See also the :attr:`digits`. """, validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, ) voltage_ac_bandwidth = Instrument.control( ":SENS:VOLT:AC:DET:BAND?", ":SENS:VOLT:AC:DET:BAND %g", """ Control the detector bandwidth in Hz for AC voltage measurement (integer strictly among 3, 30, and 300). """, validator=truncated_discrete_set, values=[3, 30, 300], ) def measure_voltage(self, max_voltage=1, ac=False): """Configure 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 #################### # Resistance (Ohm) # #################### resistance = Instrument.measurement( ":READ?", """ Measure a resistance in Ohms for both 2-wire and 4-wire configurations, based on the active :attr:`mode`. """, ) resistance_range = Instrument.control( ":SENS:RES:RANG?", ":SENS:RES:RANG:AUTO 0;:SENS:RES:RANG %g", """ Control the 2-wire resistance full-scale measure range in Ohms. Available ranges are: 10, 100, 1e3, 10e3, 100e3, 1e6, 10e6, and 100e6. Auto-range is disabled when this property is set. See also the :attr:`range`.""", validator=truncated_discrete_set, values=[10, 100, 1e3, 10e3, 100e3, 1e6, 10e6, 100e6], ) resistance_relative = Instrument.control( ":SENS:RES:REL?", ":SENS:RES:REL %g", """ Control the 2-wire resistance relative value in Ohms (float strictly from -100M to 100M). See also the :attr:`relative`.""", validator=truncated_range, values=[-1e8, 1e8], ) resistance_relative_enabled = Instrument.control( ":SENS:RES:REL:STAT?", ":SENS:RES:REL:STAT %d", """ Control a relative offset value applied to 2-wire resistance measurement. See also the :attr:`relative_enabled`.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) resistance_nplc = Instrument.control( ":SENS:RES:NPLC?", ":SENS:RES:NPLC %g", """ Control the number of power line cycles (NPLC) for the 2-wire resistance measurement (float strictly from 0.0005 to 15). See also the :attr:`nplc`.""", validator=truncated_range, values=[0.0005, 15], ) resistance_digits = Instrument.control( ":DISP:RES:DIG?", ":DISP:RES:DIG %d", """ Control the number of digits in the 2-wire resistance readings (integer strictly from 3 to 6). See also the :attr:`digits`.""", validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, ) resistance_4W_range = Instrument.control( ":SENS:FRES:RANG?", ":SENS:FRES:RANG:AUTO 0;:SENS:FRES:RANG %g", """ Control the 4-wire resistance full-scale measure range in Ohms. Available ranges are: 1, 10, 100, 1e3, 10e3, 100e3, 1e6, 10e6, and 100e6. Auto-range is disabled when this property is set. See also the :attr:`range`.""", validator=truncated_discrete_set, values=[1, 10, 100, 1e3, 10e3, 100e3, 1e6, 10e6, 100e6], ) resistance_4W_relative = Instrument.control( ":SENS:FRES:REL?", ":SENS:FRES:REL %g", """ Control the 4-wire resistance relative value in Ohms (float strictly from -100M to 100M). See also the :attr:`relative`.""", validator=truncated_range, values=[-1e8, 1e8], ) resistance_4W_relative_enabled = Instrument.control( ":SENS:FRES:REL:STAT?", ":SENS:FRES:REL:STAT %d", """ Control a relative offset value applied to 4-wire resistance measurement. See also the :attr:`relative_enabled`.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) resistance_4W_nplc = Instrument.control( ":SENS:FRES:NPLC?", ":SENS:FRES:NPLC %g", """ Control the number of power line cycles (NPLC) for the 4-wire resistance measurement (float strictly from 0.0005 to 15). See also the :attr:`nplc`.""", validator=truncated_range, values=[0.0005, 15], ) resistance_4W_digits = Instrument.control( ":DISP:FRES:DIG?", ":DISP:FRES:DIG %d", """ Control the number of digits in the 4-wire resistance readings (integer strictly from 3 to 6). See also the :attr:`digits`.""", validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, ) def measure_resistance(self, max_resistance=10e6, wires=2): """Configure the instrument to measure resistance, based on a maximum resistance to set the range. :param max_resistance: A resistance in Ohms to set the resistance range :type max_resistance: float :param wires: ``2`` for normal resistance, and ``4`` for 4-wires resistance :type wires: int :return: None """ 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 DMM6500 only supports 2 or 4 wire resistance measurements.") ################## # Frequency (Hz) # ################## frequency = Instrument.measurement( ":READ?", """ Measure a frequency in Hz, based on the active :attr:`mode`. """, ) frequency_relative = Instrument.control( ":SENS:FREQ:REL?", ":SENS:FREQ:REL %g", """ Control the frequency relative value in Hz (float strictly from -1 MHz to 1 MHz). See also the :attr:`relative`.""", validator=truncated_range, values=[-1e6, 1e6], ) frequency_relative_enabled = Instrument.control( ":SENS:FREQ:REL:STAT?", ":SENS:FREQ:REL:STAT %d", """ Control a relative offset value applied to frequency measurement. See also the :attr:`relative_enabled`.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) frequency_digits = Instrument.control( ":DISP:FREQ:DIG?", ":DISP:FREQ:DIG %d", """ Control the number of digits in the frequency readings (integer strictly from 3 to 6). See also the :attr:`digits`.""", validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, ) frequency_threshold = Instrument.control( ":SENS:FREQ:THR:RANG?", ":SENS:FREQ:THR:RANG %g", """ Control the expected input level in Volts for the frequency measurement (float strictly from 0.1 to 750V).""", validator=truncated_range, values=[0.1, 750], ) frequency_threshold_auto_enabled = Instrument.control( ":SENS:FREQ:THR:RANG:AUTO?", ":SENS:FREQ:THR:RANG:AUTO %d", """ Control the auto threshold range for frequency measurement enabled or not.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) frequency_aperature = Instrument.control( ":SENS:FREQ:APER?", ":SENS:FREQ:APER %g", """ Control the aperture time in seconds for frequency measurement (float strictly from 2 ms to 273 ms). See also :attr:`aperture`.""", validator=truncated_range, values=[0.002, 0.273], ) def measure_frequency(self): """Configure the instrument to measure frequency.""" self.mode = "frequency" ############## # Period (s) # ############## period = Instrument.measurement( ":READ?", """ Measure a period in seconds, based on the active :attr:`mode`. """, ) period_relative = Instrument.control( ":SENS:PER:REL?", ":SENS:PER:REL %g", """ Control the period relative value in seconds (float strictly from -1 s to 1 s). See also the :attr:`relative`.""", validator=truncated_range, values=[-1, 1], ) period_relative_enabled = Instrument.control( ":SENS:PER:REL:STAT?", ":SENS:PER:REL:STAT %d", """ Control a relative offset value applied to period measurement. See also the :attr:`relative_enabled`.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) period_digits = Instrument.control( ":DISP:PER:DIG?", ":DISP:PER:DIG %d", """ Control the number of digits in the period readings (integer strictly from 3 to 6). See also the :attr:`digits`.""", validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, ) period_threshold = Instrument.control( ":SENS:PER:THR:RANG?", ":SENS:PRE:THR:RANG %g", """ Control the expected input level in Volts for the period measurement (float strictly from 0.1 to 750V).""", validator=truncated_range, values=[0.1, 750], ) period_threshold_auto_enabled = Instrument.control( ":SENS:PER:THR:RANG:AUTO?", ":SENS:PER:THR:RANG:AUTO %d", """ Control the auto threshold range for period measurement enabled or not.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) period_aperature = Instrument.control( ":SENS:PER:APER?", ":SENS:PER:APER %g", """ Control the aperture time in seconds for period measurement (float strictly from 2 ms to 273 ms). See also :attr:`aperture`""", validator=truncated_range, values=[0.002, 0.273], ) def measure_period(self): """Configure the instrument to measure period.""" self.mode = "period" ################### # Temperature (C) # ################### temperature = Instrument.measurement( ":READ?", """ Measure a temperature in Celsius, based on the active :attr:`mode`. """, ) temperature_relative = Instrument.control( ":SENS:TEMP:REL?", ":SENS:TEMP:REL %g", """ Control the temperature relative value in Celsius (float strictly from -3310 C to 3310 C). See also the :attr:`relative`.""", validator=truncated_range, values=[-3310, 3310], ) temperature_relative_enabled = Instrument.control( ":SENS:TEMP:REL:STAT?", ":SENS:TEMP:REL:STAT %d", """ Control a relative offset value applied to temperature measurement. See also the :attr:`relative_enabled`.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) temperature_nplc = Instrument.control( ":SENS:TEMP:NPLC?", ":SENS:TEMP:NPLC %g", """ Control the number of power line cycles (NPLC) for the temperature measurement (float strictly from 0.0005 to 15). See also the :attr:`nplc`.""", validator=truncated_range, values=[0.0005, 15], ) temperature_digits = Instrument.control( ":DISP:TEMP:DIG?", ":DISP:TEMP:DIG %d", """ Control the number of digits in the temperature readings (integer strictly from 3 to 6). See also the :attr:`digits`.""", validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, ) def measure_temperature(self): """Configure the instrument to measure temperature.""" self.mode = "temperature" ############### # Capacitance # ############### capacitance = Instrument.measurement( ":READ?", """ Measure a capacitance in Farad, based on the active :attr:`mode`.""", ) capacitance_relative = Instrument.control( ":SENS:CAP:REL?", ":SENS:CAP:REL %g", """ Control the capacitance relative value in Farad (float strictly from -0.001 to 0.001 F). See also the :attr:`relative`.""", validator=truncated_range, values=[-0.001, 0.001], ) capacitance_relative_status = Instrument.control( ":SENS:CAP:REL:STAT?", ":SENS:CAP:REL:STAT %d", """ Control a relative offset value applied to capacitance measurement. See also the :attr:`relative_enabled`.""", validator=strict_discrete_set, values=BOOL_MAPPINGS, map_values=True, ) capacitance_range = Instrument.control( ":SENS:CAP:RANG?", ":SENS:CAP:RANG:AUTO 0;:SENS:CURR:RANG %g", """ Control the capacitance full-scale measure range in Farad. Available ranges are 1e-9, 10e-9, 100e-9, 1e-6, 10e-6, 100e-6, 1e-3. Auto-range is disabled when this property is set. See also the :attr:`range`.""", validator=truncated_discrete_set, values=[1e-9, 10e-9, 100e-9, 1e-6, 10e-6, 100e-6, 1e-3], ) capacitance_digits = Instrument.control( ":DISP:CAP:DIG?", ":DISP:CAP:DIG %d", """ Control the number of digits in the capacitance readings (integer strictly from 3 to 6). See also the :attr:`digits`.""", validator=truncated_discrete_set, values=[3, 4, 5, 6], cast=int, ) def measure_capacitance(self, max_capacitance=1e-3): """Configure the instrument to measure capacitance. :param max_capacitance: Set :attr:`capacitance_range` after changing :attr:`mode` :return: None """ self.mode = "capacitance" self.capacitance_range = max_capacitance ######### # Diode # ######### diode = Instrument.measurement( ":READ?", """ Measure a diode's forward voltage drop of general-purpose diodes and the Zener voltage of Zener diodes on the 10V range with a constant test current (bias level), based on the active :attr:`mode`. """, ) diode_bias = Instrument.control( ":SENS:DIOD:BIAS:LEV?", ":SENS:DIOD:BIAS:LEV %g", """ Control the amount of current in Amps the instrument sources while making measurement. Available bias levels are 1e-5, 0.0001, 0.001, 0.01.""", validator=truncated_discrete_set, values=[1e-5, 0.0001, 0.001, 0.01], ) diode_nplc = Instrument.control( ":SENS:DIOD:NPLC?", ":SENS:DIOD:NPLC %g", """ Control the number of power line cycles (NPLC) for the diode measurement (float strictly from 0.0005 to 15). See also the :attr:`nplc`.""", validator=truncated_range, values=[0.0005, 15], ) def measure_diode(self): """Configure the instrument to perform diode testing. :return: None """ self.mode = "diode" ############## # Continuity # ############## def measure_continuity(self): """Configure the instrument to perform continuity testing. :return: None """ self.mode = "continuity" ########## # Buffer # ########## # Main buffer functions are inherited from `KeithleyBuffer` class buffer_points = buffer_size = Instrument.control( ":TRAC:POIN?", ":TRAC:POIN %d", """ Control the number of buffer points. This does not represent actual points in the buffer, but the configuration value instead. `0` means the largest buffer possible based on the available memory when the bufer is created.""", validator=truncated_range, values=[0, 6_000_000], cast=int, ) points_in_buffer = Instrument.measurement( "TRAC:ACT?", """ Get the number of readings stored in the buffer.""", cast=int, ) ########### # Formats # ########### data_format = Instrument.control( "FORMAT:DATA?", "FORMAT:DATA %s", """ Control data format that is used when transferring readings over the remote interface. Available values are ``ASC`` (ASCII), ``REAL`` (double-precision), or ``SRE`` (single-precision).""", validator=strict_discrete_set, values=("ASC", "REAL", "SRE"), ) ################ # Scanner Card # ################ scan_id = Instrument.measurement( ":SYST:CARD1:IDN?", """ Get scanner card's ID.""", separator="|", ) scan_vch_start = Instrument.measurement( "SYST:CARD1:VCH:STAR?", """ Get the first channel in the slot that supports voltage or 2-wire measurements.""", cast=int, ) scan_vch_end = Instrument.measurement( "SYST:CARD1:VCH:END?", """ Get the last channel in the slot that supports voltage or 2-wire measurements.""", cast=int, ) scan_card_vmax = Instrument.measurement( "SYST:CARD1:VMAX?", """ Get the maximum voltage of all channels.""", cast=int ) pseudo_scanner_enabled = Instrument.setting( ":SYST:PCAR1 %d", """ Set pseudo scanner card if there's no scanner card in the instrument. After setting, user can check current scanner card by :attr:`scan_id`. If a scanner card is installed, this setting won't have any effect.""", validator=strict_discrete_set, values={True: 2000, False: 0}, map_values=True, ) scan_channels = Instrument.control( ":ROUT:SCAN:CRE?", ":ROUT:SCAN:CRE (@%s)", """ Control the channel list of scanning. An empty string will clear the list. Use comma to separate single channel and use a colon to separate the first and last channel in the list. Examples: ``1``, ``1,3,5``, ``1:2, 7:8``, or ``1:10``. """, get_process=lambda x: x[-1].replace(")", ""), separator="@", ) @property def scan_channels_list(self): """Get :attr:`scan_channels` string to a list of integers. For example, when :attr:`scan_channels` is ``1,3:5,7:8,10``, this attribute will return ``[1,3,4,5,7,8,10]``. If ``scan_channels_list=[1,2,3,4,6]``, the :attr:`scan_channels` will be ``1:4,6``. """ chan_str = self.scan_channels # Trans string to list of int, ex. "1,3:5,7:8,10" -> [1,3,4,5,7,8,10] chn_list = chan_str.split(",") for idx, ch in enumerate(chn_list): try: chn_list[idx] = int(ch) except ValueError: # process string "a:b" -> a, a+1, ..., b ch = list(map(int, ch.split(":"))) ch[-1] += 1 chn_list[idx: idx + 1] = list(range(*ch)) return chn_list @scan_channels_list.setter def scan_channels_list(self, new_channels): """Set scan channels by a list or tuple.""" if isinstance(new_channels, (list, tuple)): self.scan_channels = ",".join(map(str, new_channels)) else: log.error("Not an acceptable list") scan_count = Instrument.control( ":ROUT:SCAN:COUN:SCAN?", ":ROUT:SCAN:COUN:SCAN %d", """ Control the number of times the scan is repeated. Set to ``0`` set the scan to repeat until aborted.""", cast=int, ) scan_interval = Instrument.control( ":ROUT:SCAN:INT?", ":ROUT:SCAN:INT %d", """ Control the interval time (0s to 100ks) between scan starts when the :attr:`scan_count` is more than one.""", validator=truncated_range, values=[0, 100e3], cast=int, ) def scanned_data(self, start_idx=None, end_idx=None, raw=False): """Return a list of scanning values from the buffer. :param start_idx: A bool value which controls communication state while scanning. Default is ``True`` and the communication waits until the commands are complete to accept new commands :param end_idx: An alternative way to set :attr:`scan_count` :param raw: An alternative way to set :attr:`scan_interval` in second :return: A list of scan channels' measured :rtype: A list of channels' list """ self.write(":FORM:DATA ASCII") if start_idx is None: start_idx = self.ask(":TRAC:ACT:STAR?") if end_idx is None: end_idx = self.ask(":TRAC:ACT:END?") data = self.values(f":TRAC:DATA? {start_idx}, {end_idx}") if raw: return data else: nums = len(self.scan_channels_list) # re-organize data to 2D list return [data[i::nums] for i in range(nums)] @property def scan_modes(self): """Get a dictionary of every channel's mode.""" res = dict() for i in range(self.scan_vch_start, self.scan_vch_end + 1): res[i] = self.channels[i].mode return res @scan_modes.setter def scan_modes(self, new_mode): """Set all channels to the new mode. Ex: ``scan_modes = "voltage"``""" self.write(f':SENS:FUNC "{self._mode_command(new_mode)}", (@1:10)') @property def scan_iscomplete(self): """Get Event Status Register (ESR) bit 0 to determine if previous works were completed. This property is used while running time-consuming scanning operation.""" res = int(self.ask("*ESR?")) & 1 if res == 1: return True else: return False def scan_start(self, block_communication=True, count=None, interval=None): """Start the scanner card to close each channel of :attr:`scan_channels` sequentially and to do measurements. If :attr:`scan_count` is larger than 1, the next scanning will start again after :attr:`scan_interval` second. Running large counts or long interval scanning is a time-consuming operation. It's better to set ``block_communication=False`` and use :attr:`scan_iscomplete` to check if the measurement is completed. :param block_communication: A bool value which controls communication state while scanning. Default is ``True`` and the communication waits until the commands are complete to accept new commands :param count: An alternative way to set :attr:`scan_count` before scanning. :param interval: An alternative way to set :attr:`scan_interval` in second before scanning. :return: None """ if count: self.scan_count = count if interval: self.scan_interval = interval self.clear() if block_communication: self.write(":INIT;*WAI") log.info("Enable blocking communication.") else: self.write(":INIT") self.write("*OPC") log.info("Enable non-blocking communication.") log.info("Use `scan_iscomplete` to know the status.") def scan_stop(self): """Abort the scanning measurement by stopping the measurement arming and triggering sequence. :return: None """ self.write(":ABOR") ########## # Common # ########## def _mode_command(self, mode=None): """Get SCPI's function name from mode.""" if mode is None: mode = self.mode return self.MODES[mode] def auto_range_status(self, mode=None): """Get the status of auto-range of active mode or another mode by its name. Only ``current`` (DC), ``current ac``, ``voltage`` (DC), ``voltage ac``, ``resistance`` (2-wire), ``resistance 4W`` (4-wire), ``capacitance``, and ``voltage ratio`` support autorange. If chosen mode is not in these modes, this command will also return ``False``. :param mode: A valid :attr:`mode` name, or `None` for the active mode :return: a bool value for auto-range enabled or disabled :rtype: bool """ if mode is None: mode = self.mode if mode in self.MODES_HAVE_AUTORANGE: value = self.ask(f":SENS:{self._mode_command(mode)}:RANG:AUTO?") if value == "1": return True else: return False else: return False def auto_range(self, mode=None): """Set the active mode to use auto-range, or can set another mode by its name. Only ``current`` (DC), ``current ac``, ``voltage`` (DC), ``voltage ac``, ``resistance`` (2-wire), ``resistance 4W`` (4-wire), ``capacitance``, and ``voltage ratio`` support autorange. If chosen mode is not in these modes, this command will do nothing. :param mode: A valid :attr:`mode` name, or `None` for the active mode """ if mode is None: mode = self.mode if mode in self.MODES_HAVE_AUTORANGE: self.write(f":SENS:{self._mode_command(mode)}:RANG:AUTO 1") def enable_relative(self, mode=None): """Enable the application of a relative offset value to the measurement for the active mode, or can set another mode by its name. :param mode: A valid :attr:`mode` name, or `None` for the active mode """ self.write(f":SENS:{self._mode_command(mode)}:REL:STAT 1") def disable_relative(self, mode=None): """Disable the application of a relative offset value to the measurement for the active mode, or can set another mode by its name. :param mode: A valid :attr:`mode` name, or `None` for the active mode """ self.write(f":SENS:{self._mode_command(mode)}:REL:STAT 0") def acquire_relative(self, mode=None): """Set the active value as the relative for the active mode, or can set another mode by its name. :param mode: A valid :attr:`mode` name, or `None` for the active mode :return: The relative value that was acquired """ mode_cmd = self._mode_command(mode) self.write(f":SENS:{mode_cmd}:REL:ACQ") rel = float(self.ask(f":SENS:{mode_cmd}:REL?")) return rel def enable_filter(self, mode=None, type="repeat", count=1): """Enable the averaging filter for the active mode, or can set another mode by its name. :param mode: A valid :attr:`mode` name, or `None` for the active mode :param type: The type of averaging filter, could be ``REPeat``, ``MOVing``, or ``HYBRid``. :param count: A number of averages, which can take take values from 1 to 100 :return: Filter status read from the instrument """ mode_cmd = self._mode_command(mode) self.write(f":SENS:{mode_cmd}:AVER:STAT 1") self.write(f":SENS:{mode_cmd}:AVER:TCON {type}") self.write(f":SENS:{mode_cmd}:AVER:COUN {count}") return self.ask(f":SENS:{mode_cmd}:AVER:STAT?") def disable_filter(self, mode=None): """Disable the averaging filter for the active mode, or can set another mode by its name. :param mode: A valid :attr:`mode` name, or `None` for the active mode :return: Filter status read from the instrument """ mode_cmd = self._mode_command(mode) self.write(f":SENS:{mode_cmd}:AVER:STAT 0") return self.ask(f":SENS:{mode_cmd}:AVER:STAT?") def beep(self, frequency, duration): """Sound a system beep. :param frequency: A frequency in Hz between 20 Hz and 8000 Hz :param duration: The amount of time to play the tone between 0.001 s to 100 s :return: None """ self.write(f":SYST:BEEP {frequency:g}, {duration:g}") def write(self, command): """Write a command to the instrument. :param command: A command :param type: str :return: None """ # Using if statement can prevent RecursionError because `self.mode` # will query instrument and call `write()` function again. if "{function}" in command: super().write(command.format(function=KeithleyDMM6500.MODES[self.mode])) else: super().write(command) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4056056 pymeasure-0.14.0/pymeasure/instruments/kepco/0000755000175100001770000000000014623331176020727 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/kepco/__init__.py0000644000175100001770000000225014623331163023033 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .kepcobop import KepcoBOP3612 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/kepco/kepcobop.py0000644000175100001770000001417214623331163023104 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # # List of Kepco BOP's (Vmax,Imax) [single channel output] # 100 W: # 5-20 # 20-5 # 50-2 # 100-1 # 200 W: # 5-30 # 20-10 # 36-6 # 50-4 # 72-3 # 100-2 # 200-1 # 400 W: # 20-20 # 36-12 # 50-8 # 72-6 # 100-4 from enum import IntFlag from pymeasure.instruments import Instrument, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set, \ truncated_range OPERATING_MODES = ['VOLT', 'CURR'] class TestErrorCode(IntFlag): QUARTER_SCALE_VOLTAGE_READBACK = 512 QUARTER_SCALE_VOLTAGE = 256 MIN_VOLTAGE_OUTPUT = 128 MAX_VOLTAGE_OUTPUT = 64 LOOP_BACK_TEST = 32 DIGITAL_POT = 16 OPTICAL_BUFFER = 8 FLASH = 4 RAM = 2 ROM = 1 OK = 0 class KepcoBOP3612(SCPIMixin, Instrument): """ Represents the Kepco BOP 36-12 (M or D) 400 W bipolar power supply fitted with BIT 4886 digital interface card (minimal implementation) and provides a high-level interface for interacting with the instrument. """ _Vmax = 36 _Imax = 12 def __init__(self, adapter, name="Kepco BOP 36-12 Bipolar Power Supply", **kwargs): super().__init__( adapter=adapter, name=name, read_termination="\n", write_termination="\n", **kwargs ) output_enabled = Instrument.control( "OUTPut?", "OUTPut %d", """ Control whether the source is enabled, takes values True or False (bool) """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) def beep(self): """Cause the unit to emit a brief audible tone.""" self.write("SYSTem:BEEP") confidence_test = Instrument.measurement( "*TST?", """ Get error code after performing interface self-test procedure. Returns 0 if all tests passed, otherwise corresponding error code as detailed in manual. """, get_process=lambda v: TestErrorCode(v), ) bop_test = Instrument.measurement( "DIAG:TST?", """ Get error code after performing full power supply self-test. Returns 0 if all tests passed, otherwise corresponding error code as detailed in manual. Caution: Output will switch on and swing to maximum values. Disconnect any load before testing. """, get_process=lambda v: TestErrorCode(v), ) def wait_to_continue(self): """ Cause the power supply to wait until all previously issued commands and queries are complete before executing subsequent commands or queries. """ self.write("*WAI") voltage = Instrument.measurement( "MEASure:VOLTage?", """ Measure voltage present across the output terminals in Volts. """, cast=float ) current = Instrument.measurement( "MEASure:CURRent?", """ Measure current through the output terminals in Amps. """, cast=float ) operating_mode = Instrument.control( "FUNCtion:MODE?", "FUNCtion:MODE %s", """ Control the operating mode of the BOP. As a command, a string, VOLT or CURR, is sent. As a query, a 0 or 1 is returned, corresponding to VOLT or CURR respectively. This is mapped to corresponding string. """, validator=strict_discrete_set, values=OPERATING_MODES, get_process=lambda x: OPERATING_MODES[int(x)] ) current_setpoint = Instrument.control( "CURRent?", "CURRent %g", """ Control the output current setpoint. Functionality depends on the operating mode. If power supply in current mode, this sets the output current setpoint. The current achieved depends on the voltage compliance and load conditions (see: `current`). If power supply in voltage mode, this sets the compliance current for the corresponding voltage set point. Query returns programmed value, meaning of which is dependent on power supply operating context (see: `operating_mode`). Output must be enabled separately (see: `output_enabled`) """, validator=truncated_range, values=[-1*_Imax, _Imax] ) voltage_setpoint = Instrument.control( "VOLTage?", "VOLTage %g", """ Control the output voltage setpoint. Functionality depends on the operating mode. If power supply in voltage mode, this sets the output voltage setpoint. The voltage achieved depends on the current compliance and load conditions (see: `voltage`). If power supply in current mode, this sets the compliance voltage for the corresponding current set point. Query returns programmed value, meaning of which is dependent on power supply operating context (see: `operating_mode`). Output must be enabled separately (see: `output_enabled`) """, validator=truncated_range, values=[-1*_Vmax, _Vmax] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4056056 pymeasure-0.14.0/pymeasure/instruments/keysight/0000755000175100001770000000000014623331176021455 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keysight/__init__.py0000644000175100001770000000254414623331163023567 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .keysightDSOX1102G import KeysightDSOX1102G from .keysightN5767A import KeysightN5767A from .keysightN7776C import KeysightN7776C from .keysightE36312A import KeysightE36312A from .keysightE3631A import KeysightE3631A ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keysight/keysightDSOX1102G.py0000644000175100001770000005457614623331163025004 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) 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 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(SCPIUnknownMixin, 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) occurring 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, name="Keysight DSOX1102G Oscilloscope", **kwargs): super().__init__( adapter, name, timeout=6000, **kwargs ) # Account for setup time for timebase_mode, waveform_points_mode 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)""" # noqa: E501 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 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 fewer 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keysight/keysightE36312A.py0000644000175100001770000000634014623331163024462 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel, SCPIMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class VoltageChannel(Channel): voltage_setpoint = Channel.control( "VOLT? (@{ch})", "VOLT %g, (@{ch})", """Control the output voltage of this channel, range depends on channel.""", validator=strict_range, values=[0, 25], dynamic=True, ) current_limit = Channel.control( "CURR? (@{ch})", "CURR %g, (@{ch})", """Control the current limit of this channel, range depends on channel.""", validator=strict_range, values=[0, 1], dynamic=True, ) voltage = Channel.measurement( "MEASure:VOLTage? (@{ch})", """Measure actual voltage of this channel.""" ) current = Channel.measurement( "MEAS:CURRent? (@{ch})", """Measure the actual current of this channel.""" ) output_enabled = Channel.control( "OUTPut? (@{ch})", "OUTPut %d, (@{ch})", """Control whether the channel output is enabled (boolean).""", validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) class KeysightE36312A(SCPIMixin, Instrument): """ Represents the Keysight E36312A Power supply interface for interacting with the instrument. .. code-block:: python supply = KeysightE36312A(resource) supply.ch_1.voltage_setpoint=10 supply.ch_1.current_setpoint=0.1 supply.ch_1.output_enabled=True print(supply.ch_1.voltage) """ ch_1 = Instrument.ChannelCreator(VoltageChannel, 1) ch_2 = Instrument.ChannelCreator(VoltageChannel, 2) ch_3 = Instrument.ChannelCreator(VoltageChannel, 3) def __init__(self, adapter, name="Keysight E36312A", **kwargs): super().__init__( adapter, name, **kwargs ) self.channels[1].voltage_setpoint_values = [0, 6] self.channels[1].current_limit_values = [0, 5] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keysight/keysightE3631A.py0000644000175100001770000000727614623331163024411 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel, SCPIMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class VoltageChannel(Channel): """Implementation of a power supply base class channel""" voltage_setpoint = Channel.control( "INST:NSEL {ch};:VOLT?", "INST:NSEL {ch};:VOLT %g", """Control the output voltage of this channel, range depends on channel.""", validator=strict_range, values=[0, 25], dynamic=True, ) current_limit = Channel.control( "INST:NSEL {ch};:CURR?", "INST:NSEL {ch};:CURR %g", """Control the current limit of this channel, range depends on channel.""", validator=strict_range, values=[0, 1], dynamic=True, ) voltage = Channel.measurement( "INST:NSEL {ch};:MEAS:VOLT?", """Measure actual voltage of this channel.""", ) current = Channel.measurement( "INST:NSEL {ch};:MEAS:CURR?", """Measure the actual current of this channel.""", ) class KeysightE3631A(SCPIMixin, Instrument): """ Represents the Keysight E3631A Triple Output DC Power Supply interface for interacting with the instrument. .. code-block:: python supply = KeysightE3631A(resource) supply.ch_1.voltage_setpoint=10 supply.ch_1.current_setpoint=0.1 supply.ch_1.output_enabled=True print(supply.ch_1.voltage) """ ch_1 = Instrument.ChannelCreator(VoltageChannel, 1) ch_2 = Instrument.ChannelCreator(VoltageChannel, 2) ch_3 = Instrument.ChannelCreator(VoltageChannel, 3) def __init__(self, adapter, name="Keysight E3631A", **kwargs): super().__init__( adapter, name, **kwargs ) self.channels[1].voltage_setpoint_values = [0, 6] self.channels[1].current_limit_values = [0, 5] self.channels[3].voltage_setpoint_values = [0, -25] tracking_enabled = Instrument.control( ":OUTP:TRAC?", ":OUTP:TRAC %s", """Control whether the power supply operates in the track mode (boolean)""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) output_enabled = Instrument.control( "OUTPut?", "OUTPut %d", """Control whether the channel output is enabled (boolean).""", validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, dynamic=True, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keysight/keysightN5767A.py0000644000175100001770000000706114623331163024426 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range from pymeasure.adapters import VISAAdapter log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class KeysightN5767A(SCPIUnknownMixin, Instrument): """ Represents the Keysight N5767A Power supply interface for interacting with the instrument. """ ############### # Current (A) # ############### current_range = Instrument.control( ":CURR?", ":CURR %g", """Control the DC current range in Amps, which can take values from 0 to 25 A. Auto-range is disabled when this property is set. (float)""", validator=truncated_range, values=[0, 25], ) current = Instrument.measurement(":MEAS:CURR?", """ Get current in Amps. """ ) ############### # Voltage (V) # ############### voltage_range = Instrument.control( ":VOLT?", ":VOLT %g V", """ Control 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?", """ Get a DC voltage measurement in Volts. """ ) ################# # _status (0/1) # ################# _status = Instrument.measurement(":OUTP?", """ Get 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, name="Keysight N5767A power supply", **kwargs): super().__init__( adapter, name, **kwargs ) # Set up data transfer format if isinstance(self.adapter, VISAAdapter): self.adapter.config( is_binary=False, datatype='float32', converter='f', separator=',' ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/keysight/keysightN7776C.py0000644000175100001770000002437214623331163024436 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) WL_RANGE = [1480, 1620] LOCK_PW = 1234 class KeysightN7776C(SCPIUnknownMixin, Instrument): """ This represents the Keysight N7776C Tunable Laser Source interface. .. code-block:: python laser = N7776C(address) laser.sweep_wl_start = 1550 laser.sweep_wl_stop = 1560 laser.sweep_speed = 1 laser.sweep_mode = 'CONT' laser.output_enabled = 1 while laser.sweep_state == 1: log.info('Sweep in progress.') laser.output_enabled = 0 """ def __init__(self, adapter, name="N7776C Tunable Laser Source", **kwargs): super().__init__( adapter, name, **kwargs) locked = Instrument.control( ':LOCK?', ':LOCK %g,' + str(LOCK_PW), """Control the lock state (True/False) of the laser source. (bool)""", validator=strict_discrete_set, map_values=True, values={True: 1, False: 0} ) output_enabled = Instrument.control( 'SOUR0:POW:STAT?', 'SOUR0:POW:STAT %g', """Control the state (on/off) of the laser source (bool)""", validator=strict_discrete_set, map_values=True, values={True: 1, False: 0} ) _output_power_mW = Instrument.control( 'SOUR0:POW?', 'SOUR0:POW %f mW', """Control the laser output power in mW. (float)""", get_process=lambda v: v * 1e3 ) _output_power_dBm = Instrument.control( 'SOUR0:POW?', 'SOUR0:POW %f dBm', """Control the laser output power in dBm. (float)""" ) _output_power_unit = Instrument.control( 'SOUR0:POW:UNIT?', 'SOUR0:POW:UNIT %g', """Control the power unit used internally by the laser. (str)""", map_values=True, values={'dBm': 0, 'mW': 1} ) @property def output_power_mW(self): """Control the output power in mW""" self._output_power_unit = 'mW' return self._output_power_mW @output_power_mW.setter def output_power_mW(self, new_power): self._output_power_mW = new_power @property def output_power_dBm(self): """Control the output power in dBm.""" self._output_power_unit = 'dBm' return self._output_power_dBm @output_power_dBm.setter def output_power_dBm(self, new_power): self._output_power_dBm = new_power trigger_out = Instrument.control( 'TRIG0:OUTP?', 'TRIG0:OUTP %s', """ Control if and at which point in a sweep cycle an output trigger is generated and arms the module. """, validator=strict_discrete_set, values=['DIS', 'STF', 'SWF', 'SWST'] ) trigger_in = Instrument.control( 'TRIG0:INP?', 'TRIG0:INP %s', """Control the incoming trigger response and arm the module.""", validator=strict_discrete_set, values=['IGN', 'NEXT', 'SWS']) wavelength = Instrument.control( 'sour0:wav?', 'sour0:wav %fnm', """Control absolute wavelength of the output light (in nanometers)""", validator=strict_range, values=WL_RANGE, get_process=lambda v: v * 1e9) sweep_wl_start = Instrument.control( 'sour0:wav:swe:star?', 'sour0:wav:swe:star %fnm', """Control Start Wavelength (in nanometers) for a sweep.""", validator=strict_range, values=WL_RANGE, get_process=lambda v: v * 1e9) sweep_wl_stop = Instrument.control('sour0:wav:swe:stop?', 'sour0:wav:swe:stop %fnm', """Control End Wavelength (in nanometers) for a sweep.""", validator=strict_range, values=WL_RANGE, get_process=lambda v: v * 1e9) sweep_step = Instrument.control('sour0:wav:swe:step?', 'sour0:wav:swe:step %fnm', """Control step width of the sweep (in nanometers).""", validator=strict_range, values=[0.0001, WL_RANGE[1] - WL_RANGE[0]], get_process=lambda v: v * 1e9) sweep_speed = Instrument.control('sour0:wav:swe:speed?', 'sour0:wav:swe:speed %fnm/s', """Control speed of the sweep (in nanometers per second).""", validator=strict_discrete_set, values=[0.5, 1, 50, 80, 200], get_process=lambda v: v * 1e9) sweep_mode = Instrument.control('sour0:wav:swe:mode?', 'sour0:wav:swe:mode %s', """Control sweep mode of the swept laser source """, validator=strict_discrete_set, values=['STEP', 'MAN', 'CONT']) sweep_twoway = Instrument.control('sour0:wav:swe:rep?', 'sour0:wav:swe:rep %s', """Control the repeat mode. Applies in stepped,continuous and manual sweep mode.""", validator=strict_discrete_set, map_values=True, values={False: 'ONEW', True: 'TWOW'}) _sweep_params_consistent = Instrument.measurement( 'sour0:wav:swe:chec?', """Get whether the currently set sweep parameters (sweep mode, sweep start, stop, width, etc.) are consistent. If there is a sweep configuration problem, the laser source is not able to pass a wavelength sweep.""") sweep_points = Instrument.measurement( 'sour0:read:points? llog', """Get the number of datapoints that the :READout:DATA? command will return.""") sweep_state = Instrument.control( 'sour0:wav:swe?', 'sour0:wav:swe %g', """Control state of the wavelength sweep. Stops, starts, pauses or continues a wavelength sweep. Possible state values are 0 (not running), 1 (running) and 2 (paused). Refer to the N7776C user manual for exact usage of the paused option. """, validator=strict_discrete_set, values=[0, 1, 2]) wl_logging = Instrument.control('SOUR0:WAV:SWE:LLOG?', 'SOUR0:WAV:SWE:LLOG %g', """Control State (on/off) of the lambda logging feature of the laser source.""", validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}) def valid_sweep_params(self): response = int(self._sweep_params_consistent[0]) if response == 0: return True elif response == 368: log.warning('End Wavelength <= Start Wavelength.') elif response == 369: log.warning('Sweep time too small.') elif response == 370: log.warning('Sweep time too big.') elif response == 371: log.warning('Trigger Frequency too large.') elif response == 372: log.warning('Stepsize too small.') elif response == 373 or response == 378: log.warning('Number of triggers exceeds allowed limit.') elif response == 374: log.warning('The only allowed modulation source with lambda logging \ function is coherence control.') elif response == 375: log.warning('Lambda logging only works Step Finished output trigger configuration') elif response == 376: log.warning('Lambda logging can only be done in continuous sweep mode') elif response == 377: log.warning('The step size must be a multiple of the smallest possible step size') elif response == 379: log.warning('Continuous Sweep and Modulation on.') elif response == 380: log.warning('Start Wavelength is too small.') elif response == 381: log.warning('End Wavelength is too large.') else: log.warning('Unknown Error!') return False def next_step(self): """ Performs the next sweep step in stepped sweep if it is paused or in manual mode. """ self.write('sour0:wav:swe:step:next') def previous_step(self): """ Performs one sweep step backwards in stepped sweep if its paused or in manual mode. """ self.write('sour0:wav:swe:step:prev') def get_wl_data(self): """ Function returning the wavelength data logged in the internal memory of the laser """ # Using pyvisa's method bypassing the normal read. return np.array(self.adapter.connection.query_binary_values('sour0:read:data? llog', datatype=u'd')) def close(self): """ Fully closes the connection to the instrument through the adapter connection. """ self.adapter.close() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4096057 pymeasure-0.14.0/pymeasure/instruments/kuhneelectronic/0000755000175100001770000000000014623331176023010 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/kuhneelectronic/__init__.py0000644000175100001770000000225414623331163025120 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .kusg245_250a import Kusg245_250A ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/kuhneelectronic/kusg245_250a.py0000644000175100001770000003336314623331163025321 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pymeasure.instruments import Instrument from pymeasure.instruments.validators import truncated_range, truncated_discrete_set byteorder = 'big' encoding = 'utf-8' termination_character = "\r" reflection_limit_map = {0: 0, 1: 100, 2: 150, 3: 180, 4: 200, 5: 230} def _has_correct_termination_character(b): return b[-1] == termination_character.encode(encoding=encoding)[0] def _err_msg_invalid_termination_character(b): return f"Invalid termination character received: {hex(b[-1])}" def _is_expecting_acknowledgement(command): if command in ["v", "5", "8", "6", "7", "T"]: return False if command.endswith("?"): return False return True class Kusg245_250A(Instrument): """Represents KU SG 2.45 250 A the 2.45 GHz ISM-Band Microwave Generator and provides a high-level interface for interacting with the instrument. :param power_limit: power set-point limit in Watts (integer from 0 to 250). See :attr:`~.Kusg245_250A.power_setpoint` and :meth:`~.Kusg245_250A.tune()`. Usage example: .. code-block:: python from pymeasure.instruments.kuhneelectronic import Kusg245_250A generator = Kusg245_250A("ASRL3::INSTR", power_limit=100) # limits the output # power set-point to 100 W generator.external_enabled = False # biasing and RF output controlled by serial comm generator.power = 20 # Sets the output power to 20 Watts generator.bias_enabled = True # Enables amplifier biasing generator.rf_enabled = True # Enables the RF output p_fwd = generator.power_forward # Reads forward power in Watts p_rev = generator.power_reverse # Reads reflected power in Watts """ def __init__(self, adapter, name="KU SG 2.45 250 A", power_limit=250, **kwargs): assert 0 < power_limit <= 250, "Param 'power_limit' is out of bounds (0, 250)." super().__init__(adapter, name, asrl={"baud_rate": 115200, "read_termination": termination_character, "write_termination": termination_character}, includeSCPI=False, **kwargs) self._power_limit = power_limit self.power_setpoint_values = [0, power_limit] version = Instrument.measurement("v", """Get firmware version.""") @property def voltage_5v(self): """Measure internal 5V supply voltage in Volts.""" self.write("5") b = self.read_bytes(3) if _has_correct_termination_character(b): return 103.0 / 4700.0 * int.from_bytes(b[:2], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @property def voltage_32v(self): """Measure 32V supply voltage in Volts.""" self.write("8") b = self.read_bytes(3) if _has_correct_termination_character(b): return 1282.0 / 8200.0 * int.from_bytes(b[:2], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @property def power_forward(self): """Measure forward power in Watts.""" self.write("6") b = self.read_bytes(2) if _has_correct_termination_character(b): return int.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @property def power_reverse(self): """Measure reverse power in Watts.""" self.write("7") b = self.read_bytes(2) if _has_correct_termination_character(b): return int.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) temperature = Instrument.measurement( "T", """Measure temperature near final transistor in °C.""" ) @property def external_enabled(self): """Control whether amplifier enabling is done via external inputs on 8-pin connector or via serial interface (boolean). """ self.write("r?") b = self.read_bytes(2) if _has_correct_termination_character(b): return bool.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @external_enabled.setter def external_enabled(self, value): if value: self.write("R") else: self.write("r") @property def bias_enabled(self): """Control whether transistor biasing is enabled (boolean). Biasing must be enabled before switching RF on (see :attr:`~.Kusg245_250A.rf_enabled`). """ self.write("x?") b = self.read_bytes(2) if _has_correct_termination_character(b): return bool.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @bias_enabled.setter def bias_enabled(self, value): if value: self.write("X") else: self.write("x") @property def rf_enabled(self): """Control whether RF output is enabled (boolean). .. note:: Biasing must be enabled before RF is enabled (see :attr:`~.Kusg245_250A.bias_enabled`) """ self.write("o?") b = self.read_bytes(2) if _has_correct_termination_character(b): return bool.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @rf_enabled.setter def rf_enabled(self, value): if value: self.write("O") else: self.write("o") @property def pulse_mode_enabled(self): """Control whether pulse mode is enabled (boolean). .. note:: Biasing must be enabled before the pulse mode is enabled (see :attr:`~.Kusg245_250A.bias_enabled`) """ self.write("p?") b = self.read_bytes(2) if _has_correct_termination_character(b): return bool.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @pulse_mode_enabled.setter def pulse_mode_enabled(self, value): if value: self.write("P") else: self.write("p") @property def freq_steps_fine_enabled(self): """Control whether fine frequency steps are enabled (boolean).""" self.write("fm?") b = self.read_bytes(2) if _has_correct_termination_character(b): return bool.from_bytes(b[:1], byteorder=byteorder) raise ConnectionError(_err_msg_invalid_termination_character(b)) @freq_steps_fine_enabled.setter def freq_steps_fine_enabled(self, value): if value: self.write("fm1") else: self.write("fm0") frequency_coarse = Instrument.control( "f?", "f%04d", """Control coarse frequency in MHz (integer from 2400 to 2500). Fine frequency mode must be disabled (see :attr:`~.Kusg245_250A.freq_steps_fine_enabled`). Resolution: 1 MHz. Invalid values are truncated. """, validator=truncated_range, values=[2400, 2500], get_process=lambda v: int(v[:-3]) if v.endswith("MHz") else None, ) frequency_fine = Instrument.control( "f?", "f%07d", """Control fine frequency in kHz (integer from 2400000 to 2500000). Fine frequency mode must be enabled (see :attr:`~.Kusg245_250A.freq_steps_fine_enabled`). Resolution: 10 kHz. Invalid values are truncated. Values are rounded to tens. """, validator=truncated_range, values=[2400000, 2500000], set_process=lambda v: round(v, -1), get_process=lambda v: int(v[:-3]) if v.endswith("kHz") else None, ) power_setpoint = Instrument.control( "A?", "A%03d", """Control output power set-point in Watts (integer from 0 to :attr:`power_limit` parameter - see constructor). Resolution: 1 W. Invalid values are truncated. """, validator=truncated_range, values=[0, 250], dynamic=True, ) pulse_width = Instrument.control( "C?", "C%04d", """Control pulse width in ms (integer from 10 to 1000). Resolution: 5 ms. Invalid values are truncated. Values are rounded to multipliers of 5. """, validator=truncated_range, values=[10, 1000], set_process=lambda v: round(2 * v, -1) / 2, ) off_time = Instrument.control( "c?", "c%04d", """Control off time for the pulse mode in ms (integer from 10 to 1000). Resolution: 5 ms. Invalid values are truncated. Values are rounded to multipliers of 5. """, validator=truncated_range, values=[10, 1000], set_process=lambda v: round(2 * v, -1) / 2, ) @property def phase_shift(self): """Control phase shift in degrees (float from 0 to 358.6). Resolution: 8-bits. Values out of range are truncated. """ self.write("H?") b = self.read_bytes(2) if _has_correct_termination_character(b): return int.from_bytes(b[:1], byteorder=byteorder) / 256.0 * 360.0 raise ConnectionError(_err_msg_invalid_termination_character(b)) @phase_shift.setter def phase_shift(self, value): value = int(round(truncated_range(value, [0, 358.6])) / 360.0 * 256.0) self.write(f"H{value:03d}") @property def reflection_limit(self): """Control limit of reflection in Watts (integer in 0 - no limit, 100, 150, 180, 200, 230). .. note:: If the limit for the reflected power is reached, the forward power is reduced to the specified value and the power control mechanism is locked until the alarm has been cleared by the user via :meth:`~.Kusg245_250A.clear_VSWR_error()`. """ self.write("B?") b = self.read_bytes(2) if _has_correct_termination_character(b): return reflection_limit_map[b[0]] raise ConnectionError(_err_msg_invalid_termination_character(b)) @reflection_limit.setter def reflection_limit(self, value): value = truncated_discrete_set(value, reflection_limit_map.values()) inv_reflection_limit_map = {v: k for k, v in reflection_limit_map.items()} value = inv_reflection_limit_map[value] self.write(f"B{value:d}") def tune(self, power): """Find and set frequency with lowest reflection at a given power. :param power: A power set-point for tuning (in Watts). (integer from 0 to :attr:`power_limit` parameter - see constructor). """ power = truncated_range(power, [0, self._power_limit]) self.write(f"b{power:03d}") def clear_VSWR_error(self): """Clear the VSWR error. See: :attr:`~.Kusg245_250A.reflection_limit`. """ self.write("z") def store_settings(self): """Save actual settings to EEPROM. The following parameters are stored: frequency mode (see :attr:`~.Kusg245_250A.freq_steps_fine_enabled`), frequency (see :attr:`~.Kusg245_250A.frequency_coarse` or :attr:`~.Kusg245_250A.frequency_fine`), output power set-point (see :attr:`~.Kusg245_250A.power_setpoint`), ON/OFF control setting (see :attr:`~.Kusg245_250A.external_enabled`), reflection limit (see :attr:`~.Kusg245_250A.reflection_limit`), on time for pulse mode (see :attr:`~.Kusg245_250A.pulse_width`) and off time for pulse mode (see :attr:`~.Kusg245_250A.off_time`). """ self.write("SE") def turn_off(self): """Safe turn-off the generator. 1. Disable RF output. 2. Deactivate biasing. """ self.rf_enabled = False self.bias_enabled = False def turn_on(self): """Safe turn-on the generator. 1. Activate biasing. 2. Enable RF output. """ self.bias_enabled = True time.sleep(0.500) # not sure if needed self.rf_enabled = True def write(self, command, **kwargs): super().write(command, **kwargs) if _is_expecting_acknowledgement(command): s = self.read() if s != "A": raise ConnectionError(f"Expected acknowledgment character 'A'. Received: '{s}'") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4096057 pymeasure-0.14.0/pymeasure/instruments/lakeshore/0000755000175100001770000000000014623331176021603 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/lakeshore/__init__.py0000644000175100001770000000250714623331163023714 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .lakeshore211 import LakeShore211 from .lakeshore224 import LakeShore224 from .lakeshore331 import LakeShore331 from .lakeshore421 import LakeShore421 from .lakeshore425 import LakeShore425 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/lakeshore/lakeshore211.py0000644000175100001770000001635614623331163024365 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set from pyvisa.constants import Parity from enum import IntEnum log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class LakeShore211(SCPIUnknownMixin, Instrument): """ Represents the Lake Shore 211 Temperature Monitor and provides a high-level interface for interacting with the instrument. Untested properties and methods will be noted in their docstrings. .. code-block:: python controller = LakeShore211("GPIB::1") print(controller.temperature_celsius) # Print the sensor temperature in celsius """ class AnalogMode(IntEnum): VOLTAGE = 0 CURRENT = 1 class AnalogRange(IntEnum): RANGE_20K = 0 RANGE_100K = 1 RANGE_200K = 2 RANGE_325K = 3 RANGE_475K = 4 RANGE_1000K = 5 class RelayNumber(IntEnum): RELAY_ONE = 1 RELAY_TWO = 2 class RelayMode(IntEnum): OFF = 0 ON = 1 ALARMS = 2 alarm_keys = ['on', 'high_value', 'low_value', 'deadband', 'latch'] def __init__(self, adapter, name="Lake Shore 211 Temperature Monitor", **kwargs): super().__init__( adapter, name, asrl={'data_bits': 7, 'parity': Parity.odd}, **kwargs ) analog_configuration = Instrument.control( "ANALOG?", "ANALOG %d,%d", """ Control the analog mode and analog range. Values need to be supplied as a tuple of (analog mode, analog range) Analog mode can be 0 or 1 +--------+--------+ | setting| mode | +--------+--------+ | 0 | voltage| +--------+--------+ | 1 | current| +--------+--------+ Analog range can be 0 through 5 +--------+----------+ | setting| range | +--------+----------+ | 0 | 0 – 20 K | +--------+----------+ | 1 | 0 – 100 K| +--------+----------+ | 2 | 0 – 200 K| +--------+----------+ | 3 | 0 – 325 K| +--------+----------+ | 4 | 0 – 475 K| +--------+----------+ | 5 |0 – 1000 K| +--------+----------+ """, # Validate and return tuple v validator=lambda v, vs: ( strict_discrete_set(v[0], vs[0]), strict_discrete_set(v[1], vs[1])), values=[list(AnalogMode), list(AnalogRange)], # These are the vs values in the validator lambda get_process=lambda x: (LakeShore211.AnalogMode(x[0]), LakeShore211.AnalogRange(x[1])), cast=int ) analog_out = Instrument.measurement( "AOUT?", """Measure the percentage of output of the analog output. """ ) display_units = Instrument.control( "DISPFLD?", "DISPFLD %d", """ Control the input data to display. Valid entries: +-------------+--------------+ | setting | units | +-------------+--------------+ | 'kelvin' | Kelvin | +-------------+--------------+ | 'celsius' | Celsius | +-------------+--------------+ | 'sensor' | Sensor Units | +-------------+--------------+ | 'fahrenheit'| Fahrenheit | +-------------+--------------+ """, values={'kelvin': 0, 'celsius': 1, 'sensor': 2, 'fahrenheit': 3}, map_values=True ) temperature_celsius = Instrument.measurement( "CRDG?", """Measure the temperature of the sensor in celsius """ ) temperature_fahrenheit = Instrument.measurement( "FRDG?", """Measure the temperature of the sensor in fahrenheit """ ) temperature_sensor = Instrument.measurement( "SRDG?", """Measure the temperature of the sensor in sensor units """ ) temperature_kelvin = Instrument.measurement( "KRDG?", """Measure the temperature of the sensor in kelvin """ ) def get_relay_mode(self, relay): """ Get the status of a relay Property is UNTESTED :param RelayNumber relay: Specify which relay to query :return: Current RelayMode of queried relay """ relay = strict_discrete_set(relay, list(self.RelayNumber)) return int(self.ask("RELAY? %d" % relay)) def configure_relay(self, relay, mode): """ Configure the relay mode of a relay Property is UNTESTED :param RelayNumber relay: Specify which relay to configure :param RelayMode mode: Specify which mode to assign """ relay = strict_discrete_set(relay, list(self.RelayNumber)) mode = strict_discrete_set(mode, list(self.RelayMode)) self.write('RELAY %d %d' % (relay, mode)) def get_alarm_status(self): """ Query the current alarm status :return: Dictionary of current status [on, high_value, low_value, deadband, latch] """ status = self.values('ALARM?') return dict(zip(self.alarm_keys, [int(status[0]), float(status[1]), float(status[2]), float(status[3]), int(status[4])])) def configure_alarm(self, on=True, high_value=270.0, low_value=0.0, deadband=0, latch=False): """Configures the alarm parameters for the input. :param on: Boolean setting of alarm, default True :param high_value: High value the temperature is checked against to activate the alarm :param low_value: Low value the temperature is checked against to activate the alarm :param deadband: Value that the temperature must change outside of an alarm condition :param latch: Specifies if the alarm should latch or not """ command_string = "ALARM %d,%g,%g,%g,%d" % (on, high_value, low_value, deadband, latch) self.write(command_string) def reset_alarm(self): """Resets the alarm of the Lakeshore 211 """ self.write('ALMRST') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/lakeshore/lakeshore224.py0000644000175100001770000000623514623331163024364 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.lakeshore.lakeshore_base import LakeShoreTemperatureChannel log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class LakeShore224(SCPIUnknownMixin, Instrument): """ Represents the Lakeshore 224 Temperature monitor and provides a high-level interface for interacting with the instrument. Note that the 224 provides 12 temperature input channels (A, B, C1-5, D1-5). This driver makes use of the :ref:`LakeShoreChannels` .. code-block:: python monitor = LakeShore224('GPIB::1') print(monitor.input_A.kelvin) # Print the temperature in kelvin on sensor A monitor.input_A.wait_for_temperature() # Wait for the temperature on sensor A to stabilize. """ input_0 = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 0) input_A = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'A') input_B = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'B') input_C1 = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'C1') input_C2 = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'C2') input_C3 = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'C3') input_C4 = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'C4') input_C5 = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'C5') input_D1 = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'D1') input_D2 = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'D2') input_D3 = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'D3') input_D4 = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'D4') input_D5 = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'D5') def __init__(self, adapter, name="Lakeshore Model 224 Temperature Controller", **kwargs): kwargs.setdefault('read_termination', "\r\n") super().__init__( adapter, name, **kwargs ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/lakeshore/lakeshore331.py0000644000175100001770000000542214623331163024360 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.lakeshore.lakeshore_base import LakeShoreTemperatureChannel, \ LakeShoreHeaterChannel log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class LakeShore331(SCPIUnknownMixin, Instrument): """ Represents the Lake Shore 331 Temperature Controller and provides a high-level interface for interacting with the instrument. Note that the 331 provides two input channels (A and B) and two output channels (1 and 2). This driver makes use of the :ref:`LakeShoreChannels`. .. code-block:: python controller = LakeShore331("GPIB::1") print(controller.output_1.setpoint) # Print the current setpoint for loop 1 controller.output_1.setpoint = 50 # Change the loop 1 setpoint to 50 K controller.output_1.heater_range = 'low' # Change the heater range to low. controller.input_A.wait_for_temperature() # Wait for the temperature to stabilize. print(controller.input_A.temperature) # Print the temperature at sensor A. """ input_A = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'A') input_B = Instrument.ChannelCreator(LakeShoreTemperatureChannel, 'B') output_1 = Instrument.ChannelCreator(LakeShoreHeaterChannel, 1) output_2 = Instrument.ChannelCreator(LakeShoreHeaterChannel, 2) def __init__(self, adapter, name="Lakeshore Model 336 Temperature Controller", **kwargs): kwargs.setdefault('read_termination', "\r\n") super().__init__( adapter, name, **kwargs ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/lakeshore/lakeshore421.py0000644000175100001770000003556514623331163024373 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 import numpy as np from time import time, sleep class LakeShore421(Instrument): """ Represents the Lake Shore 421 Gaussmeter and provides a high-level interface for interacting with the instrument. .. code-block:: python gaussmeter = LakeShore421("COM1") gaussmeter.unit = "T" # Set units to Tesla gaussmeter.auto_range = True # Turn on auto-range gaussmeter.fast_mode = True # Turn on fast-mode A delay of 50 ms is ensured between subsequent writes, as the instrument cannot correctly handle writes any faster. """ MULTIPLIERS = {1e3: 'k', 1: '', 1e-3: 'm', 1e-6: 'n'} PROBE_TYPES = {"High Sensitivity": 0, "High Stability": 1, "Ultra-High Sensitivity": 2} RANGES = [30e3, 3e3, 300, 30] # in Gauss RANGE_MULTIPLIER_PROBE = [1, 10, 0.01] RANGE_MULTIPLIER_UNIT = {'G': 1, 'T': 1e-4} UNITS = ['G', 'T'] WRITE_DELAY = 0.05 def __init__(self, adapter, name="Lake Shore 421 Gaussmeter", baud_rate=9600, **kwargs): super().__init__( adapter, name, asrl={'baud_rate': baud_rate, 'data_bits': 7, 'stop_bits': 10, 'parity': 1}, read_termination='\r', write_termination='\n', includeSCPI=False, **kwargs ) self.last_write_time = time() def _raw_to_field(self, field_raw, multiplier_name): if not field_raw == "OL": multiplier = getattr(self, multiplier_name) field = multiplier * field_raw else: field = np.nan return field def _field_to_raw(self, field, multiplier_name): multiplier = getattr(self, multiplier_name) return field / multiplier field_raw = Instrument.measurement( "FIELD?", """ Returns the field in the current units and multiplier """, ) field_multiplier = Instrument.measurement( "FIELDM?", """ Returns the field multiplier for the returned magnetic field. """, values=MULTIPLIERS, map_values=True, ) @property def field(self): """ Returns the field in the current units. This property takes into account the field multiplier. Returns np.nan if field is out of range. """ return self._raw_to_field(self.field_raw, "field_multiplier") unit = Instrument.control( "UNIT?", "UNIT %s", """ A string property that controls the units used by the gaussmeter. Valid values are G (Gauss), T (Tesla). """, validator=strict_discrete_set, values=UNITS, ) field_range_raw = Instrument.control( "RANGE?", "RANGE %d", """ A integer property that controls the field range of the meter. Valid values are 0 (highest) to 3 (lowest). """, validator=truncated_discrete_set, values=range(4), cast=int, ) @property def field_range(self): """ A floating point property that controls the field range of the meter in the current unit (G or T). Valid values are 30e3, 3e3, 300, 30 (when in Gauss), or 0.003, 0.03, 0.3, and 3 (when in Tesla). """ probe_multiplier = self.RANGE_MULTIPLIER_PROBE[self.PROBE_TYPES[self.probe_type]] unit_multiplier = self.RANGE_MULTIPLIER_UNIT[self.unit] range = self.RANGES[self.field_range_raw] return np.round(range * probe_multiplier * unit_multiplier, 3) @field_range.setter def field_range(self, range): probe_multiplier = self.RANGE_MULTIPLIER_PROBE[self.PROBE_TYPES[self.probe_type]] unit_multiplier = self.RANGE_MULTIPLIER_UNIT[self.unit] ranges = np.array(self.RANGES) * probe_multiplier * unit_multiplier range = truncated_discrete_set(range, values=ranges) range = np.round(range / (probe_multiplier * unit_multiplier), 3) self.field_range_raw = self.RANGES.index(range) auto_range = Instrument.control( "AUTO?", "AUTO %d", """ A boolean property that controls the auto-range option of the meter. Valid values are True and False. Note that the auto-range is relatively slow and might not suffice for rapid measurements. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) fast_mode = Instrument.control( "FAST?", "FAST %d", """ A boolean property that controls the fast-mode option of the meter. Valid values are True and False. When enabled, the relative mode, Max Hold mode, alarms, and autorange are disabled. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) field_mode = Instrument.control( "ACDC?", "ACDC %d", """ A string property that controls whether the gaussmeter measures AC or DC magnetic fields. Valid values are "AC" and "DC". """, validator=strict_discrete_set, values={"DC": 0, "AC": 1}, map_values=True, ) def zero_probe(self, wait=True): """ Reset the probe value to 0. It is normally used with a zero gauss chamber, but may also be used with an open probe to cancel the Earth magnetic field. To cancel larger magnetic fields, the relative mode should be used. :param bool wait: Wait for 20 seconds after issuing the command to allow the resetting to finish. """ self.write("ZCAL") if wait: sleep(20) probe_type = Instrument.measurement( "TYPE?", """ Returns type of field-probe used with the gaussmeter. Possible values are High Sensitivity, High Stability, or Ultra-High Sensitivity. """, values=PROBE_TYPES, map_values=True, ) serial_number = Instrument.measurement( "SNUM?", """ Returns the serial number of the probe. """ ) display_filter_enabled = Instrument.control( "FILT?", "FILT %d", """ A boolean property that controls the display filter to make it more readable when the probe is exposed to a noisy field. The filter function makes a linear average of 8 readings and settles in approximately 2 seconds. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) front_panel_locked = Instrument.control( "LOCK?", "LOCK %d", """ A boolean property that locks or unlocks all front panel entries except pressing the Alarm key to silence alarms. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) front_panel_brightness = Instrument.control( "BRIGT?", "BRIGT %d", """ An integer property that controls the brightness of the from panel display. Valid values are 0 (dimmest) to 7 (brightest). """, validator=strict_discrete_set, values=range(8), ) # MAX HOLD max_hold_enabled = Instrument.control( "MAX?", "MAX %d", """ A boolean property that enables or disables the Max Hold function to store the largest field since the last reset (with max_hold_reset). """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) max_hold_field_raw = Instrument.measurement( "MAXR?", """ Returns the largest field since the last reset in the current units and multiplier. """, ) max_hold_multiplier = Instrument.measurement( "FIELDM?", """ Returns the multiplier for the returned max hold field. """, values=MULTIPLIERS, map_values=True, ) @property def max_hold_field(self): """ Returns the largest field since the last reset in the current units. This property takes into account the field multiplier. Returns np.nan if field is out of range. """ return self._raw_to_field(self.max_hold_field_raw, "max_hold_multiplier") def max_hold_reset(self): """ Clears the stored Max Hold value. """ self.write("MAXC") # RELATIVE MODE relative_mode_enabled = Instrument.control( "REL?", "REL %d", """ A boolean property that enables or disables the relative mode to see small variations with respect to a given setpoint. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) relative_field_raw = Instrument.measurement( "RELR?", """ Returns the relative field in the current units and the current multiplier. """, ) relative_multiplier = Instrument.measurement( "RELRM?", """ Returns the relative field multiplier for the returned magnetic field. """, values=MULTIPLIERS, map_values=True, ) @property def relative_field(self): """ Returns the relative field in the current units. This property takes into account the field multiplier. Returns np.nan if field is out of range. """ return self._raw_to_field(self.relative_field_raw, "relative_multiplier") relative_setpoint_raw = Instrument.control( "RELS?", "RELS %g", """ Property that controls the setpoint for the relative field mode in the current units and multiplier. """, ) relative_setpoint_multiplier = Instrument.measurement( "RELRM?", """ Returns the multiplier for the setpoint field. """, values=MULTIPLIERS, map_values=True, ) @property def relative_setpoint(self): """ Property that controls the setpoint for the relative field mode in the current units. This takes into account the field multiplier. """ return self._raw_to_field(self.relative_setpoint_raw, "relative_setpoint_multiplier") @relative_setpoint.setter def relative_setpoint(self, value): self.relative_setpoint_raw = self._field_to_raw(value, "relative_setpoint_multiplier") # ALARM MODE alarm_mode_enabled = Instrument.control( "ALARM?", "ALARM %d", """ A boolean property that enables or disables the alarm mode. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) alarm_audible = Instrument.control( "ALMB?", "ALMB %d", """ A boolean property that enables or disables the audible alarm beeper. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) alarm_in_out = Instrument.control( "ALMB?", "ALMB %d", """ A string property that controls whether an active alarm is caused when the field reading is inside ("Inside") or outside ("Outside") of the high and low setpoint values. """, validator=strict_discrete_set, values={"Inside": 1, "Outside": 0}, map_values=True, ) alarm_active = Instrument.measurement( "ALMS?", """ A boolean property that returns whether the alarm is triggered. """, values={True: 1, False: 0}, map_values=True, ) alarm_sort_enabled = Instrument.control( "ALMSORT?", "ALMSORT %d", """ A boolean property that enables or disables the alarm Sort Pass/Fail function. """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) alarm_low_raw = Instrument.measurement( "ALML?", "ALML %g", """ Property that controls the lower setpoint for the alarm mode in the current units and multiplier. """, ) alarm_low_multiplier = Instrument.measurement( "ALMLM?", """ Returns the multiplier for the lower alarm setpoint field. """, values=MULTIPLIERS, map_values=True, ) @property def alarm_low(self): """ Property that controls the lower setpoint for the alarm mode in the current units. This takes into account the field multiplier. """ return self._raw_to_field(self.alarm_low_raw, "alarm_low_multiplier") @alarm_low.setter def alarm_low(self, value): self.alarm_low_raw = self._field_to_raw(value, "alarm_low_multiplier") alarm_high_raw = Instrument.measurement( "ALMH?", "ALMH %g", """ Property that controls the upper setpoint for the alarm mode in the current unit and multiplier. """, ) alarm_high_multiplier = Instrument.measurement( "ALMHM?", """ Returns the multiplier for the upper alarm setpoint field. """, values=MULTIPLIERS, map_values=True, ) @property def alarm_high(self): """ Property that controls the upper setpoint for the alarm mode in the current units. This takes into account the field multiplier. """ return self._raw_to_field(self.alarm_high_raw, "alarm_high_multiplier") @alarm_high.setter def alarm_high(self, value): self.alarm_high_raw = self._field_to_raw(value, "alarm_high_multiplier") def shutdown(self): """ Closes the serial connection to the system. """ self.adapter.connection.close() super().shutdown() ################################################### # Redefined methods to ensure time between writes # ################################################### def delay_write(self): if self.WRITE_DELAY is None: return while time() - self.last_write_time < self.WRITE_DELAY: sleep(self.WRITE_DELAY / 10) self.last_write_time = time() def write(self, command): self.delay_write() super().write(command) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/lakeshore/lakeshore425.py0000644000175100001770000001073614623331163024370 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 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?", """ Get the field in the current units """ ) unit = Instrument.control( "UNIT?", "UNIT %d", """ Control the units of the instrument, which can take the values of G, T, Oe, or A/m. (str)""", validator=strict_discrete_set, values={'G': 1, 'T': 2, 'Oe': 3, 'A/m': 4}, map_values=True ) range = Instrument.control( "RANGE?", "RANGE %d", """ Control the field range in units of Gauss, which can take the values 35, 350, 3500, and 35,000 G. (float)""", validator=truncated_discrete_set, values={35: 1, 350: 2, 3500: 3, 35000: 4}, map_values=True ) def __init__(self, adapter, name="LakeShore 425 Gaussmeter", **kwargs): super().__init__( adapter, name, asrl={'write_termination': "\n", 'read_termination': "\n", # from manual 'baud_rate': 57600, 'timeout': 500, 'parity': 1, # odd 'data_bits': 7 }, includeSCPI=False, **kwargs ) 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): """Control the mode, filter, and bandwidth settings.""" return tuple(self.values("RDGMODE?")) @mode.setter def mode(self, value): 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/lakeshore/lakeshore_base.py0000644000175100001770000001173014623331163025122 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 warnings import numpy as np from time import sleep, time from pymeasure.instruments import Instrument, Channel from pymeasure.instruments.validators import strict_discrete_set log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class LakeShoreTemperatureChannel(Channel): """ Temperature input channel on a lakeshore temperature monitor. Reads the temperature in kelvin, celsius, or sensor units. Also provides a method to block the program until a given stable temperature is reached. """ kelvin = Instrument.measurement( 'KRDG? {ch}', """Read the temperature in kelvin from a channel.""" ) celsius = Instrument.measurement( 'CRDG? {ch}', """Read the temperature in celsius from a channel.""" ) sensor = Instrument.measurement( 'SRDG? {ch}', """Read the temperature in sensor units from a channel.""" ) @property def celcius(self): """Access celsius attribute with celcius (sic) property. .. deprecated:: 0.14.0 Use celsius instead. """ warnings.warn("`celcius` is deprecated, use `celsius` instead", FutureWarning) return self.celsius def wait_for_temperature(self, target, unit='kelvin', accuracy=0.1, interval=1, timeout=360, should_stop=lambda: False): """ Blocks the program, waiting for the temperature to reach the target within the accuracy (%), checking this each interval time in seconds. :param target: Target temperature in kelvin, celsius, or sensor units. :param unit: 'kelvin', 'celsius', or 'sensor' specifying the unit for queried temperature values. :param accuracy: An acceptable percentage deviation between the target and temperature. :param interval: Interval time in seconds between queries. :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 """ abs_tolerance = target * (accuracy / 100) target_reached = False t = time() while not target_reached: reading = np.array([getattr(self, unit)]) target_reached = np.allclose(reading, target, atol=abs_tolerance) sleep(interval) if (time() - t) > timeout: raise Exception(( "Timeout occurred after waiting %g seconds for " "the LakeShore 331 temperature to reach %g %s." ) % (timeout, target, unit)) if should_stop(): return class LakeShoreHeaterChannel(Channel): """ Heater output channel on a lakeshore temperature controller. Provides properties to query the output power in percent of the max, set the manual output power, heater range, and PID temperature setpoint. """ output = Instrument.measurement( 'HTR? {ch}', """Query the heater output in percent of the max.""" ) mout = Instrument.control( 'MOUT? {ch}', 'MOUT {ch},%f', """Manual heater output in percent.""" ) range = Instrument.control( 'RANGE? {ch}', 'RANGE {ch},%i', """String property controlling heater range, which can take the values: off, low, medium, and high.""", validator=strict_discrete_set, values={'off': 0, 'low': 1, 'medium': 2, 'high': 3}, map_values=True) setpoint = Instrument.control( 'SETP? {ch}', 'SETP {ch},%f', """A floating point property that control the setpoint temperature in the preferred units of the control loop sensor.""" ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4096057 pymeasure-0.14.0/pymeasure/instruments/lecroy/0000755000175100001770000000000014623331176021123 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/lecroy/__init__.py0000644000175100001770000000226214623331163023232 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .lecroyT3DSO1204 import LeCroyT3DSO1204 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/lecroy/lecroyT3DSO1204.py0000644000175100001770000005166014623331163024062 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pymeasure.instruments import Instrument from pymeasure.instruments.teledyne.teledyne_oscilloscope import TeledyneOscilloscope,\ TeledyneOscilloscopeChannel, sanitize_source from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def _math_define_validator(value, values): """ Validate the input of the math_define property :param value: input parameters as a 3-element tuple :param values: allowed space for each parameter """ if not isinstance(value, tuple): raise ValueError('Input value {} of trigger_select should be a tuple'.format(value)) if len(value) != 3: raise ValueError('Number of parameters {} different from 3'.format(len(value))) output = (sanitize_source(value[0]), value[1], sanitize_source(value[2])) for i in range(3): strict_discrete_set(output[i], values=values[i]) return output def _measure_delay_validator(value, values): """ Validate the input of the measure_delay property :param value: input parameters as a 3-element tuple :param values: allowed space for each parameter """ if not isinstance(value, tuple): raise ValueError('Input value {} of trigger_select should be a tuple'.format(value)) if len(value) != 3: raise ValueError('Number of parameters {} different from 3'.format(len(value))) output = (value[0], sanitize_source(value[1]), sanitize_source(value[2])) if output[1][0] > output[2][0]: raise ValueError(f'First channel number {output[1]} must be <= than second one {output[2]}') for i in range(3): strict_discrete_set(output[i], values=values[i]) return output class LeCroyT3DSO1204Channel(TeledyneOscilloscopeChannel): """Implementation of a LeCroy T3DSO1204 Oscilloscope channel. Implementation modeled on Channel object of Keysight DSOX1102G instrument. """ TRIGGER_SLOPES = {"negative": "NEG", "positive": "POS", "window": "WINDOW"} # Change listed values for existing commands: trigger_slope_values = TRIGGER_SLOPES bwlimit = Instrument.control( "BWL?", "BWL %s", """Control the 20 MHz internal low-pass filter (strict bool). This oscilloscope only has one frequency available for this filter. """, validator=strict_discrete_set, values=TeledyneOscilloscopeChannel._BOOLS, map_values=True ) invert = Instrument.control( "INVS?", "INVS %s", """Control the inversion of the input signal (strict bool).""", validator=strict_discrete_set, values=TeledyneOscilloscopeChannel._BOOLS, map_values=True ) skew_factor = Instrument.control( "SKEW?", "SKEW %.2ES", """Control the channel-to-channel skew factor for the specified channel. Each analog channel can be adjusted + or -100 ns for a total of 200 ns difference between channels. You can use the oscilloscope's skew control to remove cable-delay errors between channels. """, validator=strict_range, values=[-1e-7, 1e-7], preprocess_reply=lambda v: v.rstrip('S') ) trigger_level2 = Instrument.control( "TRLV2?", "TRLV2 %.2EV", """Control the lower trigger level voltage for the specified source (float). Higher and lower trigger levels are used with runt/slope triggers. When setting the trigger level it must be divided by the probe attenuation. This is not documented in the datasheet and it is probably a bug of the scope firmware. An out-of-range value will be adjusted to the closest legal value. """ ) unit = Instrument.control( "UNIT?", "UNIT %s", """Control the unit of the specified trace. Measurement results, channel sensitivity, and trigger level will reflect the measurement units you select. ("A" for Amperes, "V" for Volts). """, validator=strict_discrete_set, values=["A", "V"] ) class LeCroyT3DSO1204(TeledyneOscilloscope): """Represents the LeCroy T3DSO1204 Oscilloscope interface for interacting with the instrument. Refer to the LeCroy T3DSO1204 Oscilloscope Programmer's Guide for further details about using the lower-level methods to interact directly with the scope. This implementation is based on the shared base class :class:`TeledyneOscilloscope`. Attributes: WRITE_INTERVAL_S: minimum time between two commands. If a command is received less than WRITE_INTERVAL_S after the previous one, the code blocks until at least WRITE_INTERVAL_S seconds have passed. Because the oscilloscope takes a non-negligible time to perform some operations, it might be needed for the user to tweak the sleep time between commands. The WRITE_INTERVAL_S is set to 10ms as default however its optimal value heavily depends on the actual commands and on the connection type, so it is impossible to give a unique value to fit all cases. An interval between 10ms and 500ms second proved to be good, depending on the commands and connection latency. .. code-block:: python scope = LeCroyT3DSO1204(resource) scope.autoscale() ch1_data_array, ch1_preamble = scope.download_waveform(source="C1", points=2000) # ... scope.shutdown() """ _BOOLS = {True: "ON", False: "OFF"} WRITE_INTERVAL_S = 0.02 # seconds ch_1 = Instrument.ChannelCreator(LeCroyT3DSO1204Channel, 1) ch_2 = Instrument.ChannelCreator(LeCroyT3DSO1204Channel, 2) ch_3 = Instrument.ChannelCreator(LeCroyT3DSO1204Channel, 3) ch_4 = Instrument.ChannelCreator(LeCroyT3DSO1204Channel, 4) def __init__(self, adapter, name="LeCroy T3DSO1204 Oscilloscope", **kwargs): super().__init__(adapter, name, **kwargs) ################## # Timebase Setup # ################## timebase_hor_magnify = Instrument.control( "HMAG?", "HMAG %.2ES", """Control the zoomed (delayed) window horizontal scale (seconds/div). The main sweep scale determines the range for this command. """, validator=strict_range, values=[1e-9, 20e-3] ) timebase_hor_position = Instrument.control( "HPOS?", "HPOS %.2ES", """Control the horizontal position in the zoomed (delayed) view of the main sweep. The main sweep range and the main sweep horizontal position determine the range for this command. The value for this command must keep the zoomed view window within the main sweep range. """, ) @property def timebase(self): """Get timebase setup as a dict containing the following keys: - "timebase_scale": horizontal scale in seconds/div (float) - "timebase_offset": interval in seconds between the trigger and the reference position (float) - "timebase_hor_magnify": horizontal scale in the zoomed window in seconds/div (float) - "timebase_hor_position": horizontal position in the zoomed window in seconds (float) """ tb_setup = { "timebase_scale": self.timebase_scale, "timebase_offset": self.timebase_offset, "timebase_hor_magnify": self.timebase_hor_magnify, "timebase_hor_position": self.timebase_hor_position } return tb_setup def timebase_setup(self, scale=None, offset=None, hor_magnify=None, hor_position=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 timebase_setup if needed. :param scale: interval in seconds between the trigger event and the reference position. :param offset: horizontal scale per division in seconds/div. :param hor_magnify: horizontal scale in the zoomed window in seconds/div. :param hor_position: horizontal position in the zoomed window in seconds.""" if scale is not None: self.timebase_scale = scale if offset is not None: self.timebase_offset = offset if hor_magnify is not None: self.timebase_hor_magnify = hor_magnify if hor_position is not None: self.timebase_hor_position = hor_position ############### # Acquisition # ############### acquisition_type = Instrument.control( "ACQW?", "ACQW %s", """Control the type of data acquisition. Can be 'normal', 'peak', 'average', 'highres'. """, validator=strict_discrete_set, values={"normal": "SAMPLING", "peak": "PEAK_DETECT", "average": "AVERAGE", "highres": "HIGH_RES"}, map_values=True, get_process=lambda v: [v[0].lower(), int(v[1])] if len(v) == 2 and v[0] == "AVERAGE" else v ) acquisition_average = Instrument.control( "AVGA?", "AVGA %d", """Control the averaging times of average acquisition.""", validator=strict_discrete_set, values=[4, 16, 32, 64, 128, 256, 512, 1024] ) acquisition_status = Instrument.measurement( "SAST?", """Get the acquisition status of the scope.""", values={"stopped": "Stop", "triggered": "Trig'd", "ready": "Ready", "auto": "Auto", "armed": "Arm"}, map_values=True ) acquisition_sampling_rate = Instrument.measurement( "SARA?", """Get the sample rate of the scope.""" ) def acquisition_sample_size(self, source): """Get acquisition sample size for a certain channel. Used mainly for waveform acquisition. If the source is MATH, the SANU? MATH query does not seem to work, so I return the memory size instead. :param source: channel number of channel name. :return: acquisition sample size of that channel. """ if isinstance(source, str): source = sanitize_source(source) if source in [1, "C1"]: return self.acquisition_sample_size_c1 elif source in [2, "C2"]: return self.acquisition_sample_size_c2 elif source in [3, "C3"]: return self.acquisition_sample_size_c3 elif source in [4, "C4"]: return self.acquisition_sample_size_c4 elif source == "MATH": math_define = self.math_define[1] match = re.match(r"'(\w+)[+\-/*](\w+)'", math_define) return min(self.acquisition_sample_size(match.group(1)), self.acquisition_sample_size(match.group(2))) else: raise ValueError("Invalid source: must be 1, 2, 3, 4 or C1, C2, C3, C4, MATH.") acquisition_sample_size_c1 = Instrument.measurement( "SANU? C1", """Get the number of data points that the hardware will acquire from the input signal of channel 1. Note. Channel 2 and channel 1 share the same ADC, so the sample is the same too. """ ) acquisition_sample_size_c2 = Instrument.measurement( "SANU? C1", """Get the number of data points that the hardware will acquire from the input signal of channel 2. Note. Channel 2 and channel 1 share the same ADC, so the sample is the same too. """ ) acquisition_sample_size_c3 = Instrument.measurement( "SANU? C3", """Get the number of data points that the hardware will acquire from the input signal of channel 3. Note. Channel 3 and channel 4 share the same ADC, so the sample is the same too. """ ) acquisition_sample_size_c4 = Instrument.measurement( "SANU? C3", """Get the number of data points that the hardware will acquire from the input signal of channel 4. Note. Channel 3 and channel 4 share the same ADC, so the sample is the same too. """ ) ################## # Waveform # ################## memory_size = Instrument.control( "MSIZ?", "MSIZ %s", """Control the maximum depth of memory. :={7K,70K,700K,7M} for non-interleaved mode. Non-interleaved means a single channel is active per A/D converter. Most oscilloscopes feature two channels per A/D converter. :={14K,140K,1.4M,14M} for interleave mode. Interleave mode means multiple active channels per A/D converter. """, validator=strict_discrete_set, values={7e3: "7K", 7e4: "70K", 7e5: "700K", 7e6: "7M", 14e3: "14K", 14e4: "140K", 14e5: "1.4M", 14e6: "14M"}, map_values=True ) @property def waveform_preamble(self): """Get preamble information for the selected waveform source as a dict with the following keys: - "type": normal, peak detect, average, high resolution (str) - "requested_points": number of data points requested by the user (int) - "sampled_points": number of data points sampled by the oscilloscope (int) - "transmitted_points": number of data points actually transmitted (optional) (int) - "memory_size": size of the oscilloscope internal memory in bytes (int) - "sparsing": sparse point. It defines the interval between data points. (int) - "first_point": address of the first data point to be sent (int) - "source": source of the data : "C1", "C2", "C3", "C4", "MATH". - "unit": Physical units of the Y-axis - "type": type of data acquisition. Can be "normal", "peak", "average", "highres" - "average": average times of average acquisition - "sampling_rate": sampling rate (it is a read-only property) - "grid_number": number of horizontal grids (it is a read-only property) - "status": acquisition status of the scope. Can be "stopped", "triggered", "ready", "auto", "armed" - "xdiv": horizontal scale (units per division) in seconds - "xoffset": time interval in seconds between the trigger event and the reference position - "ydiv": vertical scale (units per division) in Volts - "yoffset": value that is represented at center of screen in Volts """ vals = self.values("WFSU?") preamble = { "sparsing": vals[vals.index("SP") + 1], "requested_points": vals[vals.index("NP") + 1], "first_point": vals[vals.index("FP") + 1], "transmitted_points": None, "source": self.waveform_source, "type": self.acquisition_type, "sampling_rate": self.acquisition_sampling_rate, "grid_number": self._grid_number, "status": self.acquisition_status, "memory_size": self.memory_size, "xdiv": self.timebase_scale, "xoffset": self.timebase_offset } preamble["average"] = self.acquisition_average if preamble["type"][0] == "average" else None strict_discrete_set(self.waveform_source, ["C1", "C2", "C3", "C4", "MATH"]) preamble["sampled_points"] = self.acquisition_sample_size(self.waveform_source) return self._fill_yaxis_preamble(preamble) def _fill_yaxis_preamble(self, preamble=None): """Fill waveform preamble section concerning the Y-axis. :param preamble: waveform preamble to be filled :return: filled preamble """ if preamble is None: preamble = {} if self.waveform_source == "MATH": preamble["ydiv"] = self.math_vdiv preamble["yoffset"] = self.math_vpos preamble["unit"] = None else: preamble["ydiv"] = self.ch(self.waveform_source).scale preamble["yoffset"] = self.ch(self.waveform_source).offset preamble["unit"] = self.ch(self.waveform_source).unit return preamble ############### # Math # ############### math_define = Instrument.control( "DEF?", "DEF EQN,'%s%s%s'", """Control the desired waveform math operation between two channels. Three parameters must be passed as a tuple: #. source1 : source channel on the left #. operation : operator must be "*", "/", "+", "-" #. source2 : source channel on the right """, validator=_math_define_validator, values=[["C1", "C2", "C3", "C4"], ["*", "/", "+", "-"], ["C1", "C2", "C3", "C4"]] ) math_vdiv = Instrument.control( "MTVD?", "MTVD %.2EV", """Control the vertical scale of the selected math operation. This command is only valid in add, subtract, multiply and divide operation. Note: legal values for the scale depend on the selected operation. """, validator=strict_discrete_set, values=[5e-4, 1e-3, 2e-3, 5e-3, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100] ) math_vpos = Instrument.control( "MTVP?", "MTVP %d", """Control the vertical position of the math waveform with specified source. Note: the point represents the screen pixels and is related to the screen center. For example, if the point is 50. The math waveform will be displayed 1 grid above the vertical center of the screen. Namely one grid is 50. """, validator=strict_range, values=[-255, 255] ) ############### # Measure # ############### measure_delay = Instrument.control( "MEAD?", "MEAD %s,%s-%s", """Control measurement delay. The MEASURE_DELY command places the instrument in the continuous measurement mode and starts a type of delay measurement. The MEASURE_DELY? query returns the measured value of delay type. The command accepts three arguments with the following syntax: measure_delay = (,,) := {PHA,FRR,FRF,FFR,FFF,LRR,LRF,LFR,LFF,SKEW} , := {C1,C2,C3,C4} where if sourceA=CX and sourceB=CY, then X < Y ========= ====================================================================== Type Description ========= ====================================================================== PHA The phase difference between two channels. (rising edge - rising edge) FRR Delay between two channels. (first rising edge - first rising edge) FRF Delay between two channels. (first rising edge - first falling edge) FFR Delay between two channels. (first falling edge - first rising edge) FFF Delay between two channels. (first falling edge - first falling edge) LRR Delay between two channels. (first rising edge - last rising edge) LRF Delay between two channels. (first rising edge - last falling edge) LFR Delay between two channels. (first falling edge - last rising edge) LFF Delay between two channels. (first falling edge - last falling edge) Skew Delay between two channels. (edge – edge of the same type) ========= ====================================================================== """, validator=_measure_delay_validator, values=[["PHA", "FRR", "FRF", "FFR", "FFF", "LRR", "LRF", "LFR", "LFF", "Skey"], ["C1", "C2", "C3", "C4"], ["C1", "C2", "C3", "C4"]] ) ############### # Display # ############### menu = Instrument.control( "MENU?", "MENU %s", """Control the bottom menu enabled state (strict bool).""", validator=strict_discrete_set, values=TeledyneOscilloscope._BOOLS, map_values=True ) grid_display = Instrument.control( "GRDS?", "GRDS %s", """Control the type of the grid which is used to display (FULL, HALF, OFF).""", validator=strict_discrete_set, values={"full": "FULL", "half": "HALF", "off": "OFF"}, map_values=True ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4096057 pymeasure-0.14.0/pymeasure/instruments/mksinst/0000755000175100001770000000000014623331176021316 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/mksinst/__init__.py0000644000175100001770000000234214623331163023424 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .mksinst import MKSInstrument from .mks937b import MKS937B from .mks974b import MKS974B ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/mksinst/mks937b.py0000644000175100001770000001447514623331163023076 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 Channel, Instrument from pymeasure.instruments.validators import strict_discrete_set from .mksinst import MKSInstrument, RelayChannel try: from enum import StrEnum except ImportError: from enum import Enum class StrEnum(str, Enum): """Until StrEnum is broadly available from the standard library""" # Python>3.10 remove it _ion_gauge_status = {"Wait": "W", "Off": "O", "Protect": "P", "Degas": "D", "Control": "C", "Rear panel Ctrl off": "R", "HC filament fault": "H", "No gauge": "N", "Good": "G", "NOT_IONGAUGE": "NAK152", "INVALID COMMAND": "NAK160", } class Unit(StrEnum): Torr = "TORR" mbar = "mBAR" Pa = "PASCAL" uHg = "MICRON" class Relay(RelayChannel): enabled = Channel.control( "EN{ch}?", "EN{ch}!%s", """Control the relay function or disable the setpoint relay. Possible values are True/False to enable/disable pressure based activation of the relay and 'SET' to permanently energize the relay.""", validator=strict_discrete_set, map_values=True, values={False: "CLEAR", True: "ENABLE", "SET": "SET", }, check_set_errors=True, ) class PressureChannel(Channel): pressure = Channel.measurement( "PR{ch}?", """Get the pressure on the channel in units selected on the device""", ) power_enabled = Channel.control( "CP{ch}?", "CP{ch}!%s", """Control power status of the channel. (bool)""", validator=strict_discrete_set, map_values=True, values={True: "ON", False: "OFF"}, check_set_errors=True, ) class IonGaugeAndPressureChannel(PressureChannel): """Channel having both a pressure and an ion gauge sensor""" ion_gauge_status = Channel.measurement( "T{ch}?", """Get ion gauge status of the channel.""", map_values=True, values=_ion_gauge_status, ) class MKS937B(MKSInstrument): """ MKS 937B vacuum gauge controller Connection to the device is made through an RS232/RS485 serial connection. The 937B gauge controller can connect up to 6 pressure measurement channels for gauges of various types which includes ionization gauges, Pirani and piezo gauges. Based on the pressure values of the gauges twelve setpoint relays can be energized when certain pressure values are reached. The assignment of the relays to measurement channels is fixed to have two relays for each pressure channel. :param adapter: pyvisa resource name of the instrument or adapter instance :param string name: The name of the instrument. :param address: device address included in every message to the instrument (default=253) :param kwargs: Any valid key-word argument for Instrument """ # Channels 1,3,5 have both an ion gauge and a pressure sensor, 2,4,6 only a pressure sensor ch_1 = Instrument.ChannelCreator(IonGaugeAndPressureChannel, 1) ch_2 = Instrument.ChannelCreator(PressureChannel, 2) ch_3 = Instrument.ChannelCreator(IonGaugeAndPressureChannel, 3) ch_4 = Instrument.ChannelCreator(PressureChannel, 4) ch_5 = Instrument.ChannelCreator(IonGaugeAndPressureChannel, 5) ch_6 = Instrument.ChannelCreator(PressureChannel, 6) relay_1 = Instrument.ChannelCreator(Relay, 1) relay_2 = Instrument.ChannelCreator(Relay, 2) relay_3 = Instrument.ChannelCreator(Relay, 3) relay_4 = Instrument.ChannelCreator(Relay, 4) relay_5 = Instrument.ChannelCreator(Relay, 5) relay_6 = Instrument.ChannelCreator(Relay, 6) relay_7 = Instrument.ChannelCreator(Relay, 7) relay_8 = Instrument.ChannelCreator(Relay, 8) relay_9 = Instrument.ChannelCreator(Relay, 9) relay_10 = Instrument.ChannelCreator(Relay, 10) relay_11 = Instrument.ChannelCreator(Relay, 11) relay_12 = Instrument.ChannelCreator(Relay, 12) def __init__(self, adapter, name="MKS 937B vacuum gauge controller", address=253, **kwargs): super().__init__( adapter, name, address=address, **kwargs ) serial = Instrument.measurement( "SN?", """Get the serial number of the instrument """, cast=str, ) all_pressures = Instrument.measurement( "PRZ?", """ Get pressures on all channels in selected units """, ) combined_pressure1 = Instrument.measurement( "PC1?", """ Get pressure on channel 1 and its combination sensor """, ) combined_pressure2 = Instrument.measurement( "PC2?", """ Get pressure on channel 2 and its combination sensor """, ) unit = Instrument.control( "U?", "U!%s", """Control pressure unit used for all pressure readings from the instrument. Allowed units are Unit.Torr, Unit.mbar, Unit.Pa, Unit.uHg.""", validator=strict_discrete_set, map_values=True, values={u: u.value for u in Unit}, check_set_errors=True, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/mksinst/mks974b.py0000644000175100001770000001412314623331163023065 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 Channel, Instrument from pymeasure.instruments.validators import strict_discrete_set from .mksinst import MKSInstrument, RelayChannel try: from enum import StrEnum except ImportError: from enum import Enum class StrEnum(str, Enum): """Until StrEnum is broadly available from the standard library""" # Python>3.10 remove it. class Unit(StrEnum): Torr = "TORR" mbar = "MBAR" Pa = "PASCAL" class Relay(RelayChannel): enabled = Channel.control( "EN{ch}?", "EN{ch}!%s", """Control the assigned input channel or disable the setpoint relay.""", validator=strict_discrete_set, map_values=True, values={False: "OFF", True: "ON", "combined": "CMB", "pirani": "PIR", "piezo": "PZ", "cold cathode": "CC", }, check_set_errors=True, ) class MKS974B(MKSInstrument): """ MKS 974B vacuum pressure transducer Connection to the device is made through an RS232/RS485 serial connection. The 974B pressure transducer is a pressure gauge combining a cold cathode ionization current measurement, a Pirani sensor, and a Piezo sensor to cover a large pressure range. It optionally includes up to three mechanical relays which can be energized based on the measured pressure value. :param adapter: pyvisa resource name of the instrument or adapter instance :param string name: The name of the instrument. :param address: device address included in every message to the instrument (default=253) :param kwargs: Any valid key-word argument for :class:`Instrument` """ relay_1 = Instrument.ChannelCreator(Relay, 1) relay_2 = Instrument.ChannelCreator(Relay, 2) relay_3 = Instrument.ChannelCreator(Relay, 3) def __init__(self, adapter, name="MKS 974B vacuum pressure transducer", address=253, **kwargs): super().__init__( adapter, name, address=address, **kwargs ) def id(self): """ Get the identification of the instrument. """ return f"{self.manufacturer}{self.model} {self.device_type} ({self.serial_number})" serial_number = Instrument.measurement( "SN?", """Get the serial number of the instrument""", cast=str, ) hardware_version = Instrument.measurement( "HV?", """Get the hardware version of the instrument""", cast=str, ) firmware_version = Instrument.measurement( "FV?", """Get the firmware version of the instrument""", cast=str, ) device_type = Instrument.measurement( "DT?", """Get the device type""", cast=str, ) manufacturer = Instrument.measurement( "MF?", """Get the manufacturer name""", cast=str, ) model = Instrument.measurement( "MD?", """Get the transducer model number""", cast=str, ) part_number = Instrument.measurement( "MD?", """Get the transducer part number""", cast=str, ) operation_hours = Instrument.measurement( "TIM?", """Get the operation hours of the instrument""", cast=int, ) temperature = Instrument.measurement( "TEM?", """Get the MicroPirani sensor temperature""", ) status = Channel.measurement( "T?", """Get transducer status""", map_values=True, values={"Ok": "O", "MicroPirani failure": "M", "Cold Cathode failure": "C", "Piezo sensor failure": "Z", "pressure dose setpoint exceeded": "R", "Cold Cathode On": "G", }, ) pirani_pressure = Instrument.measurement( "PR1?", """Get MicroPirani sensor pressure""", ) piezo_pressure = Instrument.measurement( "PR2?", """Get Piezo differential sensor pressure""", ) pressure = Instrument.measurement( "PR4?", """Get combined pressure reading""", ) coldcathode_pressure = Instrument.measurement( "PR5?", """ Get Cold Cathode sensor pressure""", ) unit = Instrument.control( "U?", "U!%s", """Control pressure unit used for all pressure readings from the instrument. Allowed units are Unit.Torr, Unit.mbar, Unit.Pa.""", validator=strict_discrete_set, map_values=True, values={u: u.value for u in Unit}, check_set_errors=True, ) user_tag = Instrument.control( "UT?", "UT!%s", """Control the user programmable tag""", cast=str, check_set_errors=True, ) switch_enabled = Instrument.control( "SW?", "SW!%s", """Control the user switch to prevent accidental execution of adjustments""", validator=strict_discrete_set, map_values=True, values={True: "ON", False: "OFF", }, check_set_errors=True, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/mksinst/mksinst.py0000644000175100001770000001365714623331163023370 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 re import compile from pymeasure.instruments import Channel, Instrument from pymeasure.instruments.validators import strict_discrete_set class RelayChannel(Channel): """ Settings of the optionally included setpoint relay. The relay is energized either below or above the setpoint depending on the 'direction' property. The relay is de-energized when the reset value is crossed in the opposite direction. Note that device by default uses an auto hysteresis setting of 10% of the setpoint value that overwrites the current reset value whenever the setpoint value or direction is changed. If other hysteresis value than 10% is required, first set the setpoint value and direction before setting the reset value. """ status = Channel.measurement( "SS{ch}?", """Get the setpoint relay status""", values={True: "SET", False: "CLEAR"}, ) setpoint = Channel.control( "SP{ch}?", "SP{ch}!%s", """Control the relay switch setpoint""", check_set_errors=True, ) resetpoint = Channel.control( "SH{ch}?", "SH{ch}!%s", """Control the relay switch off value""", check_set_errors=True, ) direction = Channel.control( "SD{ch}?", "SD{ch}!%s", """Control the switching direction""", validator=strict_discrete_set, values=["ABOVE", "BELOW"], check_set_errors=True, ) class MKSInstrument(Instrument): """Abstract MKS Instrument Connection to the device is made through an RS232/RS485 serial connection. The communication protocol of these devices is as follows: Query: '@?;FF' with the response '@ACK;FF' Set command: '@!;FF' with the response '@ACK;FF' Above is an address from 001 to 254 which can be specified upon initialization. Since ';FF' is not supported by pyvisa as terminator this class overloads the device communication methods. :param adapter: pyvisa resource name of the instrument or adapter instance :param string name: The name of the instrument. :param address: device address included in every message to the instrument (default=253) :param kwargs: Any valid key-word argument for Instrument """ def __init__(self, adapter, name="MKS Instrument", address=253, **kwargs): super().__init__( adapter, name, includeSCPI=False, read_termination=";", # in reality its ";FF" # which is, however, invalid for pyvisa. Therefore extra bytes have to # be read in the read() method and the terminators are hardcoded here. write_termination=";FF", **kwargs ) self.address = address # compiled regular expression for finding numerical values in reply strings self._re_response = compile(fr"@{self.address:03d}(?PACK)?(?P.*)") def _extract_reply(self, reply): """ preprocess_reply function which tries to extract from '@ACK;FF'. If can not be identified the orignal string is returned. :param reply: reply string :returns: string with only the response, or the original string """ rvalue = self._re_response.search(reply) if rvalue: return rvalue.group('msg') return reply def _prepend_address(self, cmd): """ create command string by including the device address """ return f"@{self.address:03d}{cmd}" def _check_extra_termination(self): """ Check the read termination to correspond to the protocol """ t = super().read_bytes(2) # read extra termination chars 'FF' if t != b'FF': raise ValueError(f"unexpected termination string received {t}") def read(self): """ Reads from the instrument including the correct termination characters """ ret = super().read() self._check_extra_termination() return self._extract_reply(ret) def write(self, command): """ Write to the instrument including the device address. :param command: command string to be sent to the instrument """ super().write(self._prepend_address(command)) def check_set_errors(self): """ Check reply string for acknowledgement string. """ ret = super().read() # use super read to get raw reply reply = self._re_response.search(ret) if reply: if reply.group('ack') == 'ACK': self._check_extra_termination() return [] # no valid acknowledgement message found raise ValueError(f"invalid reply '{ret}' found in check_errors") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4096057 pymeasure-0.14.0/pymeasure/instruments/newport/0000755000175100001770000000000014623331176021324 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/newport/__init__.py0000644000175100001770000000224014623331163023427 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/newport/esp300.py0000644000175100001770000002540514623331163022712 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin 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 {} reported the error: {}".format( 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: """ 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: encoder 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(SCPIUnknownMixin, 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 available axes. Axes are controlled through an :class:`Axis ` class. """ error = Instrument.measurement( "TE?", """ Get an error code from the motion controller. """, cast=int ) def __init__(self, adapter, name="Newport ESP 300 Motion Controller", **kwargs): super().__init__( adapter, name, **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 received. """ while self.error != 0: continue @property def errors(self): """ Get 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): """ Get 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() super().shutdown() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4096057 pymeasure-0.14.0/pymeasure/instruments/ni/0000755000175100001770000000000014623331176020234 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/ni/__init__.py0000644000175100001770000000273614623331163022351 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/ni/daqmx.py0000644000175100001770000001643014623331163021720 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. ' + f'OSError: {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: """Instrument object for interfacing with NI-DAQmx devices.""" def __init__(self, name, *args, **kwargs): super().__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 super().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/ni/nidaq.py0000644000175100001770000000520414623331163021677 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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().__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: def fset(self, value): return self.set_chan(chan, value) def fget(self): return getattr(self, '_%s' % chan) setattr(self, '_%s' % chan, None) setattr(self.__class__, chan, property(fset=fset, fget=fget)) else: def fget(self): return 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/ni/virtualbench.py0000644000175100001770000017272414623331163023305 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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_int, cdll, byref) from datetime import datetime, timezone, timedelta import numpy as np import pandas as pd 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. ' + f'ImportError: {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("{} 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(f"{names_in} is not a string") 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( f"Line specification {lines} is not valid!") 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( f"Digital Line {line} is not initialized") # 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( f"Data {data} is not iterable (list or tuple).") log.debug(f"{self.name}: {lines} output {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( f"Channel specification {channel} is not valid!") 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(f"{self.name} Output {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() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4096057 pymeasure-0.14.0/pymeasure/instruments/novanta/0000755000175100001770000000000014623331176021274 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/novanta/__init__.py0000644000175100001770000000223614623331163023404 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .fpu60 import Fpu60 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/novanta/fpu60.py0000644000175100001770000001210514623331163022601 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pymeasure.instruments import Instrument log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Fpu60(Instrument): """Represents a fpu60 power supply unit for the finesse laser series by Laserquantum, a Novanta company. The instrument responds to every command sent. """ def __init__(self, adapter, name="Laserquantum fpu60 power supply unit", **kwargs): super().__init__(adapter, name=name, includeSCPI=False, asrl={'baud_rate': 19200}, write_termination="\r", read_termination="\r\n", **kwargs) interlock_enabled = Instrument.measurement( "INTERLOCK?", """Get the interlock enabled status (bool).""", values={True: "ENABLED", False: "DISABLED"}, map_values=True, ) emission_enabled = Instrument.measurement( "STATUS?", """Measure the emission status (bool).""", values={True: "ENABLED", False: "DISABLED"}, map_values=True, ) power = Instrument.measurement( "POWER?", """Measure current output power in Watts (float).""", # Response is in form:" ##.###W" preprocess_reply=lambda r: r.replace("W", ""), ) power_setpoint = Instrument.control( "SETPOWER?", "POWER=%.3f", """Control the output power setpoint in Watts (float).""", # Getter response is in form:" ##.###W" preprocess_reply=lambda r: r.replace("W", ""), check_set_errors=True, ) shutter_open = Instrument.control( "SHUTTER?", "SHUTTER %s", """Control whether the shutter is open (bool).""", # set values: OPEN, CLOSE # get response: "SHUTTER OPEN", "SHUTTER CLOSED" values={True: "OPEN", False: "CLOSE"}, map_values=True, preprocess_reply=lambda r: r.replace("SHUTTER ", "").replace("D", ""), check_set_errors=True, ) current = Instrument.measurement( "CURRENT?", """Measure the diode current in percent (float).""", # Response: " ###.#%" preprocess_reply=lambda r: r.replace("%", ""), ) psu_temperature = Instrument.measurement( "PSUTEMP?", """Measure the power supply unit temperature in °C (float).""", # Response: " ##.###C" preprocess_reply=lambda r: r.replace("C", ""), ) head_temperature = Instrument.measurement( "HTEMP?", """Measure the laser head temperature in °C (float).""", # Response: " ##.###C" preprocess_reply=lambda r: r.replace("C", ""), ) serial_number = Instrument.measurement("SERIAL?", """Get the serial number (str).""", cast=str) software_version = Instrument.measurement("SOFTVER?", """Get the software version (str).""", cast=str) def get_operation_times(self): """Get the operation times in minutes as a dictionary.""" self.write("TIMERS?") timers = {} timers['psu'] = int(re.search(r"\d+", self.read()).group()) timers['laser'] = int(re.search(r"\d+", self.read()).group()) timers['laser_above_1A'] = int(re.search(r"\d+", self.read()).group()) self.read() # an empty line is at the end. return timers def disable_emission(self): """Disable emission and unlock the button afterwards. You have to press the physical button to enable emission again. """ self.ask("LASER=OFF") self.ask("LASER=ON") # unlocks emission button, does NOT start emission! def check_set_errors(self): """Check for errors after having set a property and log them. Called if :code:`check_set_errors=True` is set for that property. :return: List of error entries. """ response = self.read() return [] if response == "" else [response] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4136057 pymeasure-0.14.0/pymeasure/instruments/oxfordinstruments/0000755000175100001770000000000014623331176023443 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/oxfordinstruments/__init__.py0000644000175100001770000000234114623331163025550 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from .ips120_10 import IPS120_10 from .ps120_10 import PS120_10 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/oxfordinstruments/base.py0000644000175100001770000001613014623331163024724 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pyvisa.errors import VisaIOError from pyvisa import constants as vconst import re import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class OxfordVISAError(Exception): pass class OxfordInstrumentsBase(Instrument): """Base instrument for devices from Oxford Instruments. Checks the replies from instruments for validity. :param adapter: A string, integer, or :py:class:`~pymeasure.adapters.Adapter` subclass object :param string name: The name of the instrument. Often the model designation by default. :param max_attempts: Integer that sets how many attempts at getting a valid response to a query can be made :param \\**kwargs: In case ``adapter`` is a string or integer, additional arguments passed on to :py:class:`~pymeasure.adapters.VISAAdapter` (check there for details). Discarded otherwise. """ timeoutError = VisaIOError(-1073807339) regex_pattern = r"^([a-zA-Z])[\d.+-]*$" def __init__(self, adapter, name="OxfordInstruments Base", max_attempts=5, **kwargs): kwargs.setdefault('read_termination', '\r') super().__init__(adapter, name=name, includeSCPI=False, asrl={ 'baud_rate': 9600, 'data_bits': 8, 'parity': vconst.Parity.none, 'stop_bits': vconst.StopBits.two, }, **kwargs) self.max_attempts = max_attempts def ask(self, command): """Write the command to the instrument and return the resulting ASCII response. Also check the validity of the response before returning it; if the response is not valid, another attempt is made at getting a valid response, until the maximum amount of attempts is reached. :param command: ASCII command string to be sent to the instrument :returns: String ASCII response of the instrument :raises: :class:`~.OxfordVISAError` if the maximum number of attempts is surpassed without getting a valid response """ for attempt in range(self.max_attempts): # Skip the checks in "write", because we explicitly want to get an answer here super().write(command) self.wait_for() response = self.read() if self.is_valid_response(response, command): if command.startswith("R"): # Remove the leading R of the response return response.strip("R") return response log.debug("Received invalid response to '%s': %s", command, response) # Clear the buffer and try again try: self.read() except VisaIOError as e_visa: if e_visa.args == self.timeoutError.args: pass else: raise e_visa # No valid response has been received within the maximum allowed number of attempts raise OxfordVISAError(f"Retried {self.max_attempts} times without getting a valid " "response, maybe there is something worse at hand.") def write(self, command): """Write command to instrument and check whether the reply indicates that the given command was not understood. The devices from Oxford Instruments reply with '?xxx' to a command 'xxx' if this command is not known, and replies with 'x' if the command is understood. If the command starts with an "$" the instrument will not reply at all; hence in that case there will be done no checking for a reply. :raises: :class:`~.OxfordVISAError` if the instrument does not recognise the supplied command or if the response of the instrument is not understood """ super().write(command) if not command[0] == "$": response = self.read() log.debug( "Wrote '%s' to instrument; instrument responded with: '%s'", command, response, ) if not self.is_valid_response(response, command): if response[0] == "?": raise OxfordVISAError("The instrument did not understand this command: " f"{command}") else: raise OxfordVISAError(f"The response of the instrument to command '{command}' " f"is not valid: '{response}'") def is_valid_response(self, response, command): """Check if the response received from the instrument after a command is valid and understood by the instrument. :param response: String ASCII response of the device :param command: command used in the initial query :returns: True if the response is valid and the response indicates the instrument recognised the command """ # Handle special cases # Check if the response indicates that the command is not recognized if response[0] == "?": log.debug("The instrument did not understand this command: %s", command) return False # Handle a special case for when the status is queried if command[0] == "X" and response[0] == "X": return True # Handle a special case for when the version is queried if command[0] == "V": return True # Handle the other, standard cases try: match = re.match(self.regex_pattern, response) except TypeError: match = False if match and not match.groups()[0] == command[0]: match = False return bool(match) def __repr__(self): return "" % self.adapter.connection.resource_name ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/oxfordinstruments/ips120_10.py0000644000175100001770000004557314623331163025345 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set from pymeasure.instruments.validators import truncated_range from .base import OxfordInstrumentsBase # Setup logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class MagnetError(ValueError): """ Exception that is raised for issues regarding the state of the magnet or power supply. """ pass class SwitchHeaterError(ValueError): """ Exception that is raised for issues regarding the state of the superconducting switch. """ pass class IPS120_10(OxfordInstrumentsBase): """Represents the Oxford Superconducting Magnet Power Supply IPS 120-10. .. code-block:: python ips = IPS120_10("GPIB::25") # Default channel for the IPS ips.enable_control() # Enables the power supply and remote control ips.train_magnet([ # Train the magnet after it has been cooled-down (11.8, 1.0), (13.9, 0.4), (14.9, 0.2), (16.0, 0.1), ]) ips.set_field(12) # Bring the magnet to 12 T. The switch heater will # be turned off when the field is reached and the # current is ramped back to 0 (i.e. persistent mode). print(self.field) # Print the current field (whether in persistent or # non-persistent mode) ips.set_field(0) # Bring the magnet to 0 T. The persistent mode will be # turned off first (i.e. current back to set-point and # switch-heater on); afterwards the switch-heater will # again be turned off. ips.disable_control() # Disables the control of the supply, turns off the # switch-heater and clamps the output. :param clear_buffer: A boolean property that controls whether the instrument buffer is clear upon initialisation. :param switch_heater_heating_delay: The time in seconds (default is 20s) to wait after the switch-heater is turned on before the heater is expected to be heated. :param switch_heater_cooling_delay: The time in seconds (default is 20s) to wait after the switch-heater is turned off before the heater is expected to be cooled down. :param field_range: A numeric value or a tuple of two values to indicate the lowest and highest allowed magnetic fields. If a numeric value is provided the range is expected to be from :code:`-field_range` to :code:`+field_range`. The default range is -7 to +7 Tesla. """ _SWITCH_HEATER_HEATING_DELAY = 20 # Seconds _SWITCH_HEATER_COOLING_DELAY = 20 # Seconds _SWITCH_HEATER_SET_VALUES = { False: 0, # Heater off True: 1, # Heater on, with safety checks "Force": 2, # Heater on, without safety checks } _SWITCH_HEATER_GET_VALUES = { 0: False, # Heater off, Switch closed, Magnet at zero 1: True, # Heater on, Switch open 2: False, # Heater off, Switch closed, Magnet at field 5: "Heater fault, low heater current", # Heater on but current is low 8: "No switch fitted", # No switch fitted } def __init__(self, adapter, name="Oxford IPS", clear_buffer=True, switch_heater_heating_delay=None, switch_heater_cooling_delay=None, field_range=None, **kwargs): super().__init__( adapter=adapter, name=name, **kwargs ) if switch_heater_heating_delay is not None: self._SWITCH_HEATER_HEATING_DELAY = switch_heater_heating_delay if switch_heater_cooling_delay is not None: self._SWITCH_HEATER_COOLING_DELAY = switch_heater_cooling_delay if field_range is not None: if isinstance(field_range, (float, int)): self.field_setpoint_values = [-field_range, +field_range] elif isinstance(field_range, (list, tuple)): self.field_setpoint_values = field_range # Clear the buffer in order to prevent communication problems if clear_buffer: self.adapter.connection.clear() version = Instrument.measurement( "V", """ A string property that returns the version of the IPS. """, ) control_mode = Instrument.control( "X", "C%d", """ A string property that sets the IPS in `local` or `remote` and `locked` or `unlocked`, locking the LOC/REM button. Allowed values are: ===== ================= value state ===== ================= LL local & locked RL remote & locked LU local & unlocked RU remote & unlocked ===== ================= """, preprocess_reply=lambda v: v[6], cast=int, validator=strict_discrete_set, values={"LL": 0, "RL": 1, "LU": 2, "RU": 3}, map_values=True, ) current_measured = Instrument.measurement( "R1", """ A floating point property that returns the measured magnet current of the IPS in amps. """, dynamic=True, ) demand_current = Instrument.measurement( "R0", """ A floating point property that returns the demand magnet current of the IPS in amps. """, dynamic=True, ) demand_field = Instrument.measurement( "R7", """ A floating point property that returns the demand magnetic field of the IPS in Tesla. """, dynamic=True, ) persistent_field = Instrument.measurement( "R18", """ A floating point property that returns the persistent magnetic field of the IPS in Tesla. """, dynamic=True, ) switch_heater_status = Instrument.control( "X", "H%d", """ An integer property that returns the switch heater status of the IPS. Use the :py:attr:`~switch_heater_enabled` property for controlling and reading the switch heater. When using this property, the user is referred to the IPS120-10 manual for the meaning of the integer values. """, preprocess_reply=lambda v: v[8], cast=int, ) @property def switch_heater_enabled(self): """ A boolean property that controls whether the switch heater is enabled or not. When the switch heater is enabled (:code:`True`), the switch is closed and the switch is open and the current in the magnet can be controlled; when the switch heater is disabled (:code:`False`) the switch is closed and the current in the magnet cannot be controlled. When turning on the switch heater with :code:`True`, the switch heater is only activated if the current of the power supply matches the last recorded current in the magnet. .. warning:: These checks can be omitted by using :code:`"Force"` in stead of :code:`True`. Caution: Not performing these checks can cause serious damage to both the power supply and the magnet. After turning on the switch heater it is necessary to wait several seconds for the switch the respond. Raises a :class:`.SwitchHeaterError` if the system reports a 'heater fault' or if no switch is fitted on the system upon getting the status. """ status_value = self.switch_heater_status status = self._SWITCH_HEATER_GET_VALUES[status_value] if isinstance(status, str): raise SwitchHeaterError( "IPS 120-10: switch heater status reported issue with " "switch heater: %s" % status) return status @switch_heater_enabled.setter def switch_heater_enabled(self, value): status_value = self._SWITCH_HEATER_SET_VALUES[value] if status_value == 2: log.info("IPS 120-10: Turning on the switch heater without any safety checks.") self.switch_heater_status = status_value current_setpoint = Instrument.control( "R0", "I%f", """ A floating point property that controls the magnet current set-point of the IPS in ampere. """, validator=truncated_range, values=[0, 120], # Ampere dynamic=True, ) field_setpoint = Instrument.control( "R8", "J%f", """ A floating point property that controls the magnetic field set-point of the IPS in Tesla. """, validator=truncated_range, values=[-7, 7], # Tesla dynamic=True, ) sweep_rate = Instrument.control( "R9", "T%f", """ A floating point property that controls the sweep-rate of the IPS in Tesla/minute. """, dynamic=True, ) activity = Instrument.control( "X", "A%d", """ A string property that controls the activity of the IPS. Valid values are "hold", "to setpoint", "to zero" and "clamp" """, preprocess_reply=lambda v: v[4], cast=int, values={"hold": 0, "to setpoint": 1, "to zero": 2, "clamp": 4}, map_values=True, ) sweep_status = Instrument.measurement( "X", """ A string property that returns the current sweeping mode of the IPS. """, preprocess_reply=lambda v: v[11], cast=int, values={"at rest": 0, "sweeping": 1, "sweep limiting": 2, "sweeping & sweep limiting": 3}, map_values=True, ) @property def field(self): """ Property that returns the current magnetic field value in Tesla. """ try: heater_on = self.switch_heater_enabled except SwitchHeaterError as e: log.error("IPS 120-10: Switch heater status reported issue: %s" % e) field = self.demand_field else: if heater_on: field = self.demand_field else: field = self.persistent_field return field def enable_control(self): """ Enable active control of the IPS by setting control to remote and turning off the clamp. """ log.debug("start enabling control") self.control_mode = "RU" # Turn off clamping if still clamping if self.activity == "clamp": self.activity = "hold" # Turn on switch-heater if field at zero if self.field == 0: log.debug("enabling switch heater") self.switch_heater_enabled = True def disable_control(self): """ Disable active control of the IPS (if at 0T) by turning off the switch heater, clamping the output and setting control to local. Raise a :class:`.MagnetError` if field not at 0T. """ log.debug("start disabling control") if not self.field == 0: raise MagnetError("IPS 120-10: field not at 0T; cannot disable the supply. ") log.debug("disabling switch heater") self.switch_heater_enabled = False self.activity = "clamp" self.control_mode = "LU" def enable_persistent_mode(self): """ Enable the persistent magnetic field mode. Raise a :class:`.MagnetError` if the magnet is not at rest. """ # Check if system idle log.debug("enabling persistent mode") if not self.sweep_status == "at rest": raise MagnetError("IPS 120-10: magnet not at rest; cannot enable persistent mode") if not self.switch_heater_enabled: log.debug("magnet already in persistent mode") return # Magnet already in persistent mode else: self.activity = "hold" self.switch_heater_enabled = False log.info("IPS 120-10: Wait for for switch heater delay") sleep(self._SWITCH_HEATER_COOLING_DELAY) self.activity = "to zero" self.wait_for_idle() def disable_persistent_mode(self): """ Disable the persistent magnetic field mode. Raise a :class:`.MagnetError` if the magnet is not at rest. """ # Check if system idle log.debug("disabling persistent mode") if not self.sweep_status == "at rest": raise MagnetError("IPS 120-10: magnet not at rest; cannot disable persistent mode") # Check if the setpoint equals the persistent field if not self.field == self.field_setpoint: log.warning("IPS 120-10: field setpoint and persistent field not identical; " "setting the setpoint to the persistent field.") self.field_setpoint = self.field if self.switch_heater_enabled: log.debug("magnet already in demand mode or at 0 field") return # Magnet already in demand mode or at 0 field else: log.debug("set activity to 'to setpoint'") self.activity = "to setpoint" self.wait_for_idle() log.debug("set activity to 'hold'") self.activity = "hold" log.debug("enable switch heater") self.switch_heater_enabled = True log.info("IPS 120-10: Wait for for switch heater delay") sleep(self._SWITCH_HEATER_HEATING_DELAY) def wait_for_idle(self, delay=1, max_wait_time=None, should_stop=lambda: False): """ Wait until the system is at rest (i.e. current of field not ramping). :param delay: Time in seconds between each query into the state of the instrument. :param max_wait_time: Maximum time in seconds to wait before is at rest. If the system is not at rest within this time a :class:`TimeoutError` is raised. :code:`None` is interpreted as no maximum time. :param should_stop: A function that returns :code:`True` when this function should return early. """ log.debug("waiting for magnet to be idle") start_time = time() while True: log.debug("sleeping for %d s", delay) sleep(delay) log.debug("checking the status of the sweep") status = self.sweep_status if status == "at rest": log.debug("status is 'at rest', waiting is done") break if should_stop(): log.debug("external function signals to stop waiting") break if max_wait_time is not None and time() - start_time > max_wait_time: raise TimeoutError("IPS 120-10: Magnet not idle within max wait time.") def set_field(self, field, sweep_rate=None, persistent_mode_control=True): """ Change the applied magnetic field to a new specified magnitude. If allowed (via `persistent_mode_control`) the persistent mode will be turned off if needed and turned on when the magnetic field is reached. When the new field set-point is 0, the set-point of the instrument will not be changed but rather the `to zero` functionality will be used. Also, the persistent mode will not turned on upon reaching the 0T field in this case. :param field: The new set-point for the magnetic field in Tesla. :param sweep_rate: A numeric value that controls the rate with which to change the magnetic field in Tesla/minute. :param persistent_mode_control: A boolean that controls whether the persistent mode may be turned off (if needed before sweeping) and on (when the field is reached); if set to :code:`False` but the system is in persistent mode, a :class:`.MagnetError` will be raised and the magnetic field will not be changed. """ # Check if field needs changing if self.field == field: return if self.switch_heater_enabled: pass # Magnet in demand mode log.debug("Magnet in demand mode, continuing") else: # Magnet in persistent mode log.debug("Magnet in persistent mode") if persistent_mode_control: log.debug("trying to disable persistent mode") self.disable_persistent_mode() else: raise MagnetError( "IPS 120-10: magnet is in persistent mode but cannot turn off " "persistent mode because persistent_mode_control == False. " ) if sweep_rate is not None: log.debug("setting the sweep rate to %s", sweep_rate) self.sweep_rate = sweep_rate if field == 0: log.debug("setting activity to 'to zero' - running down the field") self.activity = "to zero" else: log.debug("setting activity to 'to setpoint'") self.activity = "to setpoint" log.debug("setting the field_setpoint to %d", field) self.field_setpoint = field log.debug("waiting for magnet to be finished") self.wait_for_idle() log.debug("sleeping for additional 10s (whatever the reason)") sleep(10) if persistent_mode_control and field != 0: log.debug( "persistent mode control is on, and setpoint_field !=0 - enabling persistent mode" ) self.enable_persistent_mode() def train_magnet(self, training_scheme): """ Train the magnet after cooling down. Afterwards, set the field back to 0 tesla (at last-used ramp-rate). :param training_scheme: The training scheme as a list of tuples; each tuple should consist of a (field [T], ramp-rate [T/min]) pair. """ for (field, rate) in training_scheme: self.set_field(field, rate, persistent_mode_control=False) self.set_field(0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/oxfordinstruments/itc503.py0000644000175100001770000005022514623331163025024 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from enum import IntFlag import numpy as np from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, \ truncated_range, strict_range from .base import OxfordInstrumentsBase # Setup logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def pointer_validator(value, values): """ Provides a validator function that ensures the passed value is a tuple or a list with a length of 2 and passes every item through the strict_range validator. :param value: A value to test :param values: A range of values (passed to strict_range) :raises: TypeError if the value is not a tuple or a list :raises: IndexError if the value is not of length 2 """ if not isinstance(value, (list, tuple)): raise TypeError('{:g} is not a list or tuple'.format(value)) if not len(value) == 2: raise IndexError('{:g} is not of length 2'.format(value)) return tuple(strict_range(v, values) for v in value) class ITC503(OxfordInstrumentsBase): """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 """ def __init__(self, adapter, name="Oxford ITC503", clear_buffer=True, min_temperature=0, max_temperature=1677.7, **kwargs): super().__init__( adapter=adapter, name=name, **kwargs, ) # Clear the buffer in order to prevent communication problems if clear_buffer: self.adapter.connection.clear() self.temperature_setpoint_values = [min_temperature, max_temperature] class FLOW_CONTROL_STATUS(IntFlag): """ IntFlag class for decoding the flow control status. Contains the following flags: === ====================== ============================================== bit flag meaning === ====================== ============================================== 4 HEATER_ERROR_SIGN Sign of heater-error; True means negative 3 TEMPERATURE_ERROR_SIGN Sign of temperature-error; True means negative 2 SLOW_VALVE_ACTION Slow valve action occurring 1 COOLDOWN_TERMINATION Cooldown-termination occurring 0 FAST_COOLDOWN Fast-cooldown occurring === ====================== ============================================== """ HEATER_ERROR_SIGN = 16 TEMPERATURE_ERROR_SIGN = 8 SLOW_VALVE_ACTION = 4 COOLDOWN_TERMINATION = 2 FAST_COOLDOWN = 1 version = Instrument.measurement( "V", """ A string property that returns the version of the IPS. """, preprocess_reply=lambda v: v, ) control_mode = Instrument.control( "X", "C%d", """ A string property that sets the ITC in `local` or `remote` and `locked` or `unlocked`, locking the LOC/REM button. Allowed values are: ===== ================= value state ===== ================= LL local & locked RL remote & locked LU local & unlocked RU remote & unlocked ===== ================= """, preprocess_reply=lambda v: v[5:6], cast=int, 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: ====== ======================= value state ====== ======================= MANUAL heater & gas manual AM heater auto, gas manual MA heater manual, gas auto AUTO heater & gas auto ====== ======================= """, preprocess_reply=lambda v: v[3:4], cast=int, 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 represents the heater output power as a percentage of the maximum voltage. Can be set if the heater is in manual mode. Valid values are in range 0 [off] to 99.9 [%]. """, validator=truncated_range, values=[0, 99.9] ) heater_voltage = Instrument.measurement( "R6", """ A floating point property that represents the heater output power in volts. For controlling the heater, use the :class:`ITC503.heater` property. """, ) 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 [%]. """, validator=truncated_range, values=[0, 99.9] ) proportional_band = Instrument.control( "R8", "P%f", """ A floating point property that controls the proportional band for the PID controller in Kelvin. Can be set if the PID controller is in manual mode. Valid values are 0 [K] to 1677.7 [K]. """, validator=truncated_range, values=[0, 1677.7] ) integral_action_time = Instrument.control( "R9", "I%f", """ A floating point property that controls the integral action time for the PID controller in minutes. Can be set if the PID controller is in manual mode. Valid values are 0 [min.] to 140 [min.]. """, validator=truncated_range, values=[0, 140] ) derivative_action_time = Instrument.control( "R10", "D%f", """ A floating point property that controls the derivative action time for the PID controller in minutes. Can be set if the PID controller is in manual mode. Valid values are 0 [min.] to 273 [min.]. """, validator=truncated_range, values=[0, 273] ) auto_pid = Instrument.control( "X", "L%d", """ A boolean property that sets the Auto-PID mode on (True) or off (False). """, preprocess_reply=lambda v: v[12:13], cast=int, 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: ========= ========================================= value meaning ========= ========================================= 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 ========= ========================================= """, preprocess_reply=lambda v: v[7:9], cast=int, 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. """, validator=truncated_range, values=[0, 1677.7], # Kelvin, 0 - 1677.7K is the maximum range of the instrument dynamic=True, ) temperature_1 = Instrument.measurement( "R1", """ Reads the temperature of the sensor 1 in Kelvin. """, ) temperature_2 = Instrument.measurement( "R2", """ Reads the temperature of the sensor 2 in Kelvin. """, ) temperature_3 = Instrument.measurement( "R3", """ Reads the temperature of the sensor 3 in Kelvin. """, ) 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. """, ) front_panel_display = Instrument.setting( "F%d", """ A string property that controls what value is displayed on the front panel of the ITC. Valid values are: 'temperature setpoint', 'temperature 1', 'temperature 2', 'temperature 3', 'temperature error', 'heater', 'heater voltage', 'gasflow', 'proportional band', 'integral action time', 'derivative action time', 'channel 1 freq/4', 'channel 2 freq/4', 'channel 3 freq/4'. """, validator=strict_discrete_set, map_values=True, values={ "temperature setpoint": 0, "temperature 1": 1, "temperature 2": 2, "temperature 3": 3, "temperature error": 4, "heater": 5, "heater voltage": 6, "gasflow": 7, "proportional band": 8, "integral action time": 9, "derivative action time": 10, "channel 1 freq/4": 11, "channel 2 freq/4": 12, "channel 3 freq/4": 13, }, ) x_pointer = Instrument.setting( "x%d", """ An integer property to set pointers into tables for loading and examining values in the table. The significance and valid values for the pointer depends on what property is to be read or set. """, validator=strict_range, values=[0, 128] ) y_pointer = Instrument.setting( "y%d", """ An integer property to set pointers into tables for loading and examining values in the table. The significance and valid values for the pointer depends on what property is to be read or set. """, validator=strict_range, values=[0, 128] ) pointer = Instrument.setting( "$x%d\r$y%d", """ A tuple property to set pointers into tables for loading and examining values in the table, of format (x, y). The significance and valid values for the pointer depends on what property is to be read or set. The value for x and y can be in the range 0 to 128. """, validator=pointer_validator, values=[0, 128] ) sweep_table = Instrument.control( "r", "s%f", """ A property that controls values in the sweep table. Relies on :class:`ITC503.x_pointer` and :class:`ITC503.y_pointer` (or :class:`ITC503.pointer`) to point at the location in the table that is to be set or read. The x-pointer selects the step of the sweep (1 to 16); the y-pointer selects the parameter: ========= ======================= y-pointer parameter ========= ======================= 1 set-point temperature 2 sweep-time to set-point 3 hold-time at set-point ========= ======================= """, ) auto_pid_table = Instrument.control( "q", "p%f", """ A property that controls values in the auto-pid table. Relies on :class:`ITC503.x_pointer` and :class:`ITC503.y_pointer` (or :class:`ITC503.pointer`) to point at the location in the table that is to be set or read. The x-pointer selects the table entry (1 to 16); the y-pointer selects the parameter: ========= ======================= y-pointer parameter ========= ======================= 1 upper temperature limit 2 proportional band 3 integral action time 4 derivative action time ========= ======================= """, ) target_voltage_table = Instrument.control( "t", "v%f", """ A property that controls values in the target heater voltage table. Relies on the :class:`ITC503.x_pointer` to select the entry in the table that is to be set or read (1 to 64). """, ) gasflow_configuration_parameter = Instrument.control( "d", "c%f", """ A property that controls the gas flow configuration parameters. Relies on the :class:`ITC503.x_pointer` to select which parameter is set or read: ========= ===================================== x-pointer parameter ========= ===================================== 1 valve gearing 2 target table & features configuration 3 gas flow scaling 4 temperature error sensitivity 5 heater voltage error sensitivity 6 minimum gas valve in auto ========= ===================================== """, ) gasflow_control_status = Instrument.measurement( "m", """ A property that reads the gas-flow control status. Returns the status in the form of a :class:`ITC503.FLOW_CONTROL_STATUS` IntFlag. """, cast=int, get_process=lambda v: ITC503.FLOW_CONTROL_STATUS(v), ) target_voltage = Instrument.measurement( "n", """ A float property that reads the current heater target voltage with which the actual heater voltage is being compared. Only valid if gas-flow in auto mode. """, ) valve_scaling = Instrument.measurement( "o", """ A float property that reads the valve scaling parameter. Only valid if gas-flow in auto mode. """, ) def wait_for_temperature(self, error=0.01, timeout=3600, check_interval=0.5, stability_interval=10, thermalize_interval=300, should_stop=lambda: False, ): """ 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 None, 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. """ number_of_intervals = int(stability_interval / check_interval) stable_intervals = 0 attempt = 0 t0 = time() while True: temp_error = self.temperature_error if abs(temp_error) < error: stable_intervals += 1 else: stable_intervals = 0 attempt += 1 if stable_intervals >= number_of_intervals: break if timeout is not None and (time() - t0) > timeout: raise TimeoutError( "Timeout expired while waiting for the Oxford ITC305 to " "reach the set-point temperature" ) 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 = np.array(temperatures, ndmin=1) sweep_time = np.array(sweep_time, ndmin=1) hold_time = np.array(hold_time, ndmin=1) # Make steps array if steps is None: steps = temperatures.size steps = np.linspace(1, steps, steps) # Create interpolated arrays interpolator = np.round( np.linspace(1, steps.size, temperatures.size)) temperatures = np.interp(steps, interpolator, temperatures) interpolator = np.round( np.linspace(1, steps.size, sweep_time.size)) sweep_time = np.interp(steps, interpolator, sweep_time) interpolator = np.round( np.linspace(1, steps.size, hold_time.size)) hold_time = np.interp(steps, interpolator, hold_time) # Pad with zeros to wipe unused steps (total 16) of the sweep program padding = 16 - temperatures.size temperatures = np.pad(temperatures, (0, padding), 'constant', constant_values=temperatures[-1]) sweep_time = np.pad(sweep_time, (0, padding), 'constant') hold_time = np.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.pointer = (line, 1) self.sweep_table = setpoint self.pointer = (line, 2) self.sweep_table = sweep self.pointer = (line, 3) self.sweep_table = hold def wipe_sweep_table(self): """ Wipe the currently programmed sweep table. """ self.write("w") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/oxfordinstruments/ps120_10.py0000644000175100001770000001056514623331163025165 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .ips120_10 import IPS120_10 def PS_custom_get_process(v): """Adjust the received value, for working with the PS 120-10 """ return v * 1e-2 def PS_custom_set_process(v): """Convert float to proper int value, for working with the PS 120-10 """ return int(v * 1e2) class PS120_10(IPS120_10): """Represents the Oxford Superconducting Magnet Power Supply PS 120-10. .. code-block:: python ps = PS120_10("GPIB::25") # Default channel for the IPS ps.enable_control() # Enables the power supply and remote control ps.train_magnet([ # Train the magnet after it has been cooled-down (11.8, 1.0), (13.9, 0.4), (14.9, 0.2), (16.0, 0.1), ]) ps.set_field(12) # Bring the magnet to 12 T. The switch heater will # be turned off when the field is reached and the # current is ramped back to 0 (i.e. persistent mode). print(self.field) # Print the current field (whether in persistent or # non-persistent mode) ps.set_field(0) # Bring the magnet to 0 T. The persistent mode will be # turned off first (i.e. current back to set-point and # switch-heater on); afterwards the switch-heater will # again be turned off. ps.disable_control() # Disables the control of the supply, turns off the # switch-heater and clamps the output. :param clear_buffer: A boolean property that controls whether the instrument buffer is clear upon initialisation. :param switch_heater_heating_delay: The time in seconds (default is 20s) to wait after the switch-heater is turned on before the heater is expected to be heated. :param switch_heater_cooling_delay: The time in seconds (default is 20s) to wait after the switch-heater is turned off before the heater is expected to be cooled down. :param field_range: A numeric value or a tuple of two values to indicate the lowest and highest allowed magnetic fields. If a numeric value is provided the range is expected to be from :code:`-field_range` to :code:`+field_range`. """ def __init__(self, adapter, name="Oxford PS", **kwargs): super().__init__( adapter=adapter, name=name, **kwargs, ) current_measured_get_process = PS_custom_get_process demand_current_get_process = PS_custom_get_process demand_field_get_process = PS_custom_get_process persistent_field_get_process = PS_custom_get_process current_setpoint_get_process = PS_custom_get_process current_setpoint_set_process = PS_custom_set_process current_setpoint_set_command = "I%d" field_setpoint_get_process = PS_custom_get_process field_setpoint_set_process = PS_custom_set_process field_setpoint_set_command = "J%d" sweep_rate_get_process = PS_custom_get_process sweep_rate_set_process = PS_custom_set_process sweep_rate_set_command = "T%d" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4136057 pymeasure-0.14.0/pymeasure/instruments/parker/0000755000175100001770000000000014623331176021112 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/parker/__init__.py0000644000175100001770000000224614623331163023223 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/parker/parkerGV6.py0000644000175100001770000001706514623331163023300 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from time import sleep import re class ParkerGV6(SCPIUnknownMixin, 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, adapter, name="Parker GV6 Motor Controller", **kwargs): super().__init__( adapter, name, asrl={'baud_rate': 9600, 'timeout': 500, }, write_termination="\r", **kwargs ) self.set_defaults() def read(self): """ Overwrites the Instrument.read command to provide the correct functionality """ # TODO seems to be broken as it does not make sense see issue #623 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)) @property def echo(self): pass @echo.setter 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))) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4136057 pymeasure-0.14.0/pymeasure/instruments/pendulum/0000755000175100001770000000000014623331176021457 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/pendulum/__init__.py0000644000175100001770000000223614623331163023567 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .cnt91 import CNT91 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/pendulum/cnt91.py0000644000175100001770000001676514623331163023002 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from warnings import warn from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import ( strict_discrete_set, strict_range, truncated_range, ) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) # Defined outside of the class, since it is used by `Instrument.control` without access to `self`. MIN_GATE_TIME = 2e-8 # Programmer's guide 8-92 MAX_GATE_TIME = 1000 # Programmer's guide 8-92 MIN_BUFFER_SIZE = 4 # Programmer's guide 8-39 MAX_BUFFER_SIZE = 10000 # Programmer's guide 8-39 class CNT91(SCPIUnknownMixin, Instrument): """Represents a Pendulum CNT-91 frequency counter.""" CHANNELS = {"A": 1, "B": 2, "C": 3, "E": 4, "INTREF": 6} def __init__(self, adapter, name="Pendulum CNT-91", **kwargs): # allow long-term measurements, add 30 s for data transfer kwargs.setdefault("timeout", 24 * 60 * 60 * 1000 + 30) kwargs.setdefault("read_termination", "\n") super().__init__( adapter, name, asrl={"baud_rate": 256000}, **kwargs, ) @property def batch_size(self): """Get maximum number of buffer entries that can be transmitted at once.""" if not hasattr(self, "_batch_size"): self._batch_size = int(self.ask("FORM:SMAX?")) return self._batch_size external_start_arming_source = Instrument.control( "ARM:SOUR?", "ARM:SOUR %s", """Control external arming source ('A', 'B', 'E' (rear) or 'IMM' for immediately arming).""", # noqa: E501 validator=strict_discrete_set, values={"A": "EXT1", "B": "EXT2", "E": "EXT4", "IMM": "IMM"}, map_values=True, ) external_arming_start_slope = Instrument.control( "ARM:SLOP?", "ARM:SLOP %s", """Control slope for the start arming condition (str 'POS' or 'NEG').""", validator=strict_discrete_set, values=["POS", "NEG"], ) continuous = Instrument.control( "INIT:CONT?", "INIT:CONT %s", """Control whether to perform continuous measurements.""", strict_discrete_set, values={True: 1.0, False: 0.0}, map_values=True, ) @property def measurement_time(self): """ Control gate time of one measurement in s (float strictly from 2e-8 to 1000). .. deprecated:: 0.14 Use `gate_time` instead. """ warn("`measurement_time` is deprecated, use `gate_time` instead.", FutureWarning) return self.gate_time @measurement_time.setter def measurement_time(self, value): warn("`measurement_time` is deprecated, use `gate_time` instead.", FutureWarning) self.gate_time = value gate_time = Instrument.control( ":ACQ:APER?", ":ACQ:APER %s", """Control gate time of one measurement in s (float strictly from 2e-8 to 1000).""", validator=strict_range, values=[MIN_GATE_TIME, MAX_GATE_TIME], # Programmer's guide 8-92 ) format = Instrument.control( "FORM?", "FORM %s", "Control response format ('ASCII' or 'REAL').", validator=strict_discrete_set, values={"ASCII": "ASC", "REAL": "REAL"}, map_values=True, ) interpolator_autocalibrated = Instrument.control( ":CAL:INT:AUTO?", "CAL:INT:AUTO %s", """Control if interpolators should be calibrated automatically (bool).""", strict_discrete_set, values={True: 1.0, False: 0.0}, map_values=True, ) def read_buffer(self, n=MAX_BUFFER_SIZE): """ Read out `n` samples from the buffer. :param n: Number of samples that should be read from the buffer. The maximum number of 10000 samples is read out by default. :return: Frequency values from the buffer. """ n = truncated_range(n, [MIN_BUFFER_SIZE, MAX_BUFFER_SIZE]) # Programmer's guide 8-39 while not self.complete: # Wait until the buffer is filled. sleep(0.01) return self.values(f":FETC:ARR? {'MAX' if n == MAX_BUFFER_SIZE else n}") def configure_frequency_array_measurement(self, n_samples, channel, back_to_back=True): """ Configure the counter for an array of measurements. :param n_samples: The number of samples :param channel: Measurement channel (A, B, C, E, INTREF) :param back_to_back: If True, the buffer measurement is performed back-to-back. """ n_samples = truncated_range(n_samples, [MIN_BUFFER_SIZE, MAX_BUFFER_SIZE]) channel = strict_discrete_set(channel, self.CHANNELS) channel = self.CHANNELS[channel] self.write(f":CONF:ARR:FREQ{':BTB' if back_to_back else ''} {n_samples},(@{channel})") def buffer_frequency_time_series( self, channel, n_samples, sample_rate=None, # deprecated, only kept for backwards compatibility gate_time=None, trigger_source=None, back_to_back=True, ): """ Record a time series to the buffer and read it out after completion. :param channel: Channel that should be used :param n_samples: The number of samples :param gate_time: Gate time in s :param trigger_source: Optionally specify a trigger source to start the measurement :param back_to_back: If True, the buffer measurement is performed back-to-back. :param sample_rate: Sample rate in Hz .. deprecated:: 0.14 Use parameter `gate_time` instead. """ if (gate_time is None) and (sample_rate is None): raise ValueError("`gate_time` must be specified.") if sample_rate is not None: warn("`sample_rate` is deprecated, use `gate_time` instead.", FutureWarning) if gate_time is not None: raise ValueError("Only one of `gate_time` and `sample_rate` can be specified.") gate_time = 1 / sample_rate self.clear() self.format = "ASCII" self.configure_frequency_array_measurement(n_samples, channel, back_to_back=back_to_back) self.continuous = False self.gate_time = gate_time if trigger_source: self.external_start_arming_source = trigger_source # start the measurement (or wait for trigger) self.write(":INIT") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4136057 pymeasure-0.14.0/pymeasure/instruments/proterial/0000755000175100001770000000000014623331176021627 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/proterial/__init__.py0000644000175100001770000000223414623331163023735 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .rod4 import ROD4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/proterial/rod4.py0000644000175100001770000001136414623331163023052 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel from pymeasure.instruments.validators import (truncated_range, strict_discrete_set) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ROD4Channel(Channel): """Implementation of a ROD-4 MFC channel.""" actual_flow = Channel.measurement( "\x020{ch}RFX", """Measure the actual flow in %.""" ) setpoint = Channel.control( "\x020{ch}RFD", "\x020{ch}SFD%.1f", """Control the setpoint in % of MFC range.""", validator=truncated_range, values=[0, 100], check_set_errors=True ) mfc_range = Channel.control( "\x020{ch}RFK", "\x020{ch}SFK%d", """Control the MFC range in sccm. Upper limit is 200 slm.""", validator=truncated_range, values=[0, 200000], check_set_errors=True ) ramp_time = Channel.control( "\x020{ch}RRT", "\x020{ch}SRT%.1f", """Control the MFC setpoint ramping time in seconds.""", validator=truncated_range, values=[0, 200000], check_set_errors=True ) valve_mode = Channel.control( "\x020{ch}RVM", "\x020{ch}SVM%d", """Control the MFC valve mode. Valid options are `flow`, `close`, and `open`. """, validator=strict_discrete_set, values={'flow': 0, 'close': 1, 'open': 2}, map_values=True, check_set_errors=True ) flow_unit_display = Channel.setting( "\x020{ch}SFU%d", """Set the flow units on the front display. Valid options are %, sccm, or slm. Display in absolute units is in sccm for control range < 10 slm.""", validator=strict_discrete_set, values={'%': 0, 'sccm': 1, 'slm': 1}, map_values=True, check_set_errors=True ) class ROD4(Instrument): """Represents the Proterial ROD-4(A) operator for mass flow controllers and provides a high-level interface for interacting with the instrument. User must specify which channel to control (1-4). .. code-block:: python rod4 = ROD4("ASRL1::INSTR") print(rod4.version) # Print version and series number rod4.ch_1.mfc_range = 500 # Sets Channel 1 MFC range to 500 sccm rod4.ch_2.valve_mode = 'flow' # Sets Channel 2 MFC to flow control rod4.ch_3.setpoint = 50 # Sets Channel 3 MFC to flow at 50% of full range print(rod4.ch_4.actual_flow) # Prints Channel 4 actual MFC flow in % """ def __init__(self, adapter, name="ROD-4 MFC Controller", **kwargs): super().__init__( adapter, name, read_termination='\r', write_termination='\r', includeSCPI=False, **kwargs ) ch_1 = Instrument.ChannelCreator(ROD4Channel, 1) ch_2 = Instrument.ChannelCreator(ROD4Channel, 2) ch_3 = Instrument.ChannelCreator(ROD4Channel, 3) ch_4 = Instrument.ChannelCreator(ROD4Channel, 4) version = Instrument.measurement( "\x0201RVN", """Get the version and series number. Returns x.xxS/N """ ) keyboard_locked = Instrument.setting( "\x0201SKO%d", """Set the front keyboard lock status.""", validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, check_set_errors=True ) def check_set_errors(self): """Read 'OK' from ROD-4 after setting.""" response = self.read() if response != 'OK': errors = ["Error setting ROD-4.",] else: errors = [] return errors ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4136057 pymeasure-0.14.0/pymeasure/instruments/racal/0000755000175100001770000000000014623331176020710 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/racal/__init__.py0000644000175100001770000000224614623331163023021 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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 .racal1992 import Racal1992 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/racal/racal1992.py0000644000175100001770000003227314623331163022674 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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 from pymeasure.instruments import Instrument log = logging.getLogger(__name__) # https://docs.python.org/3/howto/logging.html#library-config log.addHandler(logging.NullHandler()) class ReturnValueError(Exception): pass class Racal1992(Instrument): """Represents the Racal-Dana 1992 Universal counter .. code-block:: python from pymeasure.instruments.racal import Racal1992 counter = Racal1992("GPIB0::10") This class should also work for Racal-Dana 1991, it has the same product manual, as long as you don't use functionality that requires channel B. """ def __init__(self, adapter, name="Racal-Dana 1992", **kwargs): kwargs.setdefault('write_termination', '\r\n') super().__init__( adapter, name, includeSCPI=False, **kwargs ) int_types = ['SF', 'RS', 'UT', 'MS', 'TA'] float_types = ['CK', 'FA', 'PA', 'TI', 'PH', 'RA', 'MX', 'MZ', 'LA', 'LB', 'FC', 'RC', 'DT', 'GS'] channel_params = { 'A' : { # noqa 'coupling' : { 'AC' : 'AAC', 'DC' : 'ADC' }, # noqa 'attenuation' : { 'X1' : 'AAD', 'X10' : 'AAE' }, # noqa 'trigger' : { 'auto' : 'AAU', 'manual' : 'AMN' }, # noqa 'impedance' : { '50' : 'ALI', '1M' : 'AHI' }, # noqa 'slope' : { 'pos' : 'APS', 'neg' : 'ANS' }, # noqa 'filtering' : { True : 'AFE', False : 'AFD' }, # noqa 'trigger_level' : None, # noqa }, 'B' : { # noqa 'coupling' : { 'AC' : 'BAC', 'DC' : 'BDC' }, # noqa 'attenuation' : { 'X1' : 'BAD', 'X10' : 'BAE' }, # noqa 'trigger' : { 'auto' : 'BAU', 'manual' : 'BMN' }, # noqa 'impedance' : { '50' : 'BLI', '1M' : 'BHI' }, # noqa 'slope' : { 'pos' : 'BPS', 'neg' : 'BNS' }, # noqa 'input_select' : { 'separate' : 'BCS', 'common' : 'BCC' }, # noqa 'trigger_level' : None, # noqa }, } operating_modes = { 'self_check' : 'CK', # noqa 'frequency_a' : 'FA', # noqa 'period_a' : 'PA', # noqa 'phase_a_rel_b' : 'PH', # noqa 'ratio_a_to_b' : 'RA', # noqa 'ratio_c_to_b' : 'RC', # noqa 'interval_a_to_b' : 'TI', # noqa 'total_a_by_b' : 'TA', # noqa 'frequency_c' : 'FC', # noqa } @staticmethod def decode(v, allowed_types=None): """Decode received message. All values returned follow the same format: 2 letters to indicate the type of the value returned, followed by a floating point number (which could be an integer, of course.) This here, for example, is math constant Z: MZ+001.00000000E+00 """ if len(v) != 19: raise ReturnValueError("Length of instrument response must always be 19 characters") val_type = v[0:2] val = float(v[2:19]) if allowed_types and val_type not in allowed_types: raise ValueError(f"Unexpected value type returned: '{val_type}'") if val_type in Racal1992.int_types: return int(val) elif val_type in Racal1992.float_types: return val else: raise ValueError("Unsupported return type") operating_mode = Instrument.setting( "%s", """Set operating mode. Permitted modes are: 'self_check', 'frequency_a', 'period_a', 'phase_a_rel_b', 'ratio_a_to_b', 'ratio_c_to_b', 'interval_a_to_b', 'total_a_by_b', 'frequency_c' """, set_process=(lambda mode: Racal1992.operating_modes[mode]), ) resolution = Instrument.control( "RRS", "SRS %d", """Control the resolution of the counter with an integer from 3 to 10 that specifies the number of significant digits. """, get_process=(lambda v: Racal1992.decode(v, "RS")) ) delay_enable = Instrument.setting( "D%s", """Control delay. True=enable, False=disable""", values={True: "E", False: "D"}, map_values=True ) delay_time = Instrument.control( "RDT", "SDT %f", """Control delay time.""", get_process=(lambda v: Racal1992.decode(v, "DT")) ) special_function_enable = Instrument.setting( "SF%s", """Control special function. True=enable, False=disable""", values={True: "E", False: "D"}, map_values=True ) # FIXME: not tested on real instrument! special_function_number = Instrument.control( "RSF", "S%d", """Control special function.""", get_process=(lambda v: Racal1992.decode(v, "SF")) ) # FIXME: not tested on real instrument! total_so_far = Instrument.measurement( "RF", """Get total number of events so far.""", get_process=(lambda v: Racal1992.decode(v, "RF")) ) software_version = Instrument.measurement( "RMS", "Get instrument software version", get_process=(lambda v: Racal1992.decode(v, "MS")) ) gpib_software_version = Instrument.measurement( "RGS", "Get GPIB software version", get_process=(lambda v: Racal1992.decode(v, "GS")) ) device_type = Instrument.measurement( "RUT", """Get unit device type. Should return 1992 for a Racal-Dana 1992 or 1991 for a Racal-Dana 1991.""", get_process=(lambda v: Racal1992.decode(v, "UT")) ) math_mode = Instrument.setting( "M%s", """Set math mode. True=enable, False=disable""", values={True: "E", False: "D"}, map_values=True ) math_x = Instrument.control( "RMX", "SMX %f", """Control math constant X.""", get_process=(lambda v: Racal1992.decode(v, "MX")) ) math_z = Instrument.control( "RMZ", "SMZ %f", """Control math constant Z.""", get_process=(lambda v: Racal1992.decode(v, "MZ")) ) trigger_level_a = Instrument.control( "RLA", "SLA %f", """Control trigger level for channel A""", get_process=(lambda v: Racal1992.decode(v, "LA")) ) trigger_level_b = Instrument.control( "RLB", "SLB %f", """Control trigger level for channel B""", get_process=(lambda v: Racal1992.decode(v, "LB")) ) def read(self): return self.read_bytes(21).decode('utf-8') def write(self, s): """Add a space in front of all commands that are sent to the instrument to work around weird model issue. It shouldn't be needed on almost all devices, but it also doesn't hurt. And it fixes a real issue that's seen on a few devices.""" super().write(' ' + s) def read_and_decode(self, allowed_types=None): v = self.read_bytes(21).decode('utf-8') return Racal1992.decode(v, allowed_types) # ============================================================ # Channel-specific settings # ============================================================ def channel_settings(self, channel_name, **settings): """Set channel configuration paramters. :param channel_name: 'A' or 'B' :param settings: one or multiple of the following: 'coupling' : 'AC' or 'DC' 'attenuation' : 'X1' or 'X10' 'trigger' : 'auto' or 'manual' 'impedance' : '50' or '1M' 'slope' : 'pos' or 'neg' 'filtering' : True or False (only allowed for channel A) 'input_select' : 'separate' or 'common' (only allowed for channel B) 'trigger_level' : """ if channel_name not in Racal1992.channel_params: raise ValueError("Channel name must by 'A' or 'B'") commands = [] trigger_str = "" for setting, value in settings.items(): if setting not in Racal1992.channel_params[channel_name]: raise ValueError(f"Channel {channel_name} does not support a {setting} setting") accepted_values = Racal1992.channel_params[channel_name][setting] if accepted_values is None: # Trigger level has a float parameter... # Use special string for that because it's used the # last setting of all. if value < -51 or value > 51: raise ValueError(f"{value} is out of range for {setting}") trigger_str = f"SL{channel_name} {value}" continue if value not in accepted_values: raise ValueError(f"{value} is not an acceptable value for {setting}") command = accepted_values[value] commands.append(command) if trigger_str != "": commands.append(trigger_str) self.write(" ".join(c for c in commands)) # ============================================================ # IP - Instrument Preset # ============================================================ def preset(self): """Configure instrument with default presets.""" self.write('IP') # ============================================================ # RE - Reset measurement # ============================================================ def reset_measurement(self): """Reset ongoing measurement.""" self.write('RE') # ============================================================ # Wait for measurement value # ============================================================ def wait_for_measurement(self, timeout=None, progressDots=False): """Wait until a new measurement is available. :param timeout: number of seconds to wait before timeout exception. :param progressDots: when true, print '.' after each ready-check """ if timeout is not None: end_time = time.time() + timeout while True: if progressDots: log.info(".") stb = self.adapter.connection.read_stb() if stb & 0x10: break if timeout is not None and time.time() > end_time: raise Exception("Timeout while waiting for measurement") return stb # ============================================================ # Measured value # ============================================================ @property def measured_value(self): """Get measured value. A Racal-Dana 1992 doesn't return measurement data after a request for measurement data. Instead, it fills a FIFO with data whenever it completes a measurement. When the FIFO is full, the oldest measurement is removed. The FIFO buffer gets cleared when a command is received that requires an immediate reply, such reading a setting. It also gets cleared when an operating mode is cleared. When there is no measurement data, this property will stall until data is available. It will also timeout after a time that can be set with the standard pyvisa API. One can make sure that measurement data is available by first calling `wait_for_measurement()`. """ return self.read_and_decode(allowed_types=Racal1992.operating_modes.values()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4136057 pymeasure-0.14.0/pymeasure/instruments/razorbill/0000755000175100001770000000000014623331176021626 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/razorbill/__init__.py0000644000175100001770000000226014623331163023733 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/razorbill/razorbillRP100.py0000644000175100001770000001066614623331163024670 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import (strict_discrete_set, strict_range) class razorbillRP100(SCPIUnknownMixin, 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 """ def __init__(self, adapter, name="Razorbill RP100 Piezo Stack Powersupply", **kwargs): super().__init__( adapter, name, **kwargs ) self.timeout = 20 output_1 = Instrument.control("OUTP1?", "OUTP1 %d", """Control 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", """Control 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", """Control the output voltage of channel 1""", validator=strict_range, values=[-230, 230]) voltage_2 = Instrument.control("SOUR2:VOLT?", "SOUR2:VOLT %g", """Control the output voltage of channel 2""", validator=strict_range, values=[-230, 230]) slew_rate_1 = Instrument.control( "SOUR1:VOLT:SLEW?", "SOUR1:VOLT:SLEW %g", """Control 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", """Control 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?", """Get the instantaneous output of source one in volts""" ) instant_voltage_2 = Instrument.measurement( "SOUR2:VOLT:NOW?", """Get the instantaneous output of source two in volts""" ) contact_voltage_1 = Instrument.measurement( "MEAS1:VOLT?", """Get the Voltage in volts present at the front panel output of channel 1""" ) contact_voltage_2 = Instrument.measurement( "MEAS2:VOLT?", """Get the Voltage in volts present at the front panel output of channel 2""" ) contact_current_1 = Instrument.measurement( "MEAS1:CURR?", """Get the current in amps present at the front panel output of channel 1""" ) contact_current_2 = Instrument.measurement( "MEAS2:CURR?", """Get the current in amps present at the front panel output of channel 2""" ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4136057 pymeasure-0.14.0/pymeasure/instruments/redpitaya/0000755000175100001770000000000014623331176021610 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/redpitaya/__init__.py0000644000175100001770000000005214623331163023712 0ustar00runnerdockerfrom .redpitaya_scpi import RedPitayaScpi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/redpitaya/redpitaya_scpi.py0000644000175100001770000003203614623331163025162 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2023 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 datetime import numpy as np from pymeasure.instruments import Instrument, Channel, SCPIMixin from pymeasure.instruments.validators import truncated_range, strict_discrete_set import logging log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class DigitalChannelP(Channel): """ A digital line of the P type""" direction_in = Channel.control( "DIG:PIN:DIR? DIO{ch}_P", "DIG:PIN:DIR %s,DIO{ch}_P", """ Control a digital line to the given direction (True for 'IN' or False for 'OUT')""", validator=strict_discrete_set, map_values=True, values={True: 'IN', False: 'OUT'}, ) enabled = Channel.control( "DIG:PIN? DIO{ch}_P", "DIG:PIN DIO{ch}_P,%d", """ Control the enabled state of the line (bool)""", validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) class DigitalChannelN(Channel): """ A digital line of the N type""" direction_in = Channel.control( "DIG:PIN:DIR? DIO{ch}_N", "DIG:PIN:DIR %s,DIO{ch}_N", """ Control a digital line to the given direction (True for 'IN' or False for 'OUT')""", validator=strict_discrete_set, map_values=True, values={True: 'IN', False: 'OUT'}, ) enabled = Channel.control( "DIG:PIN? DIO{ch}_N", "DIG:PIN DIO{ch}_N,%d", """ Control the enabled state of the line (bool)""", validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) class DigitalChannelLed(Channel): """ A LED digital line (Output only)""" enabled = Channel.control( "DIG:PIN? LED{ch}", "DIG:PIN LED{ch},%d", """ Control the enabled state of the led (bool)""", validator=strict_discrete_set, map_values=True, values={True: 1, False: 0}, ) class AnalogInputSlowChannel(Channel): """ A slow analog input channel""" voltage = Channel.measurement( "ANALOG:PIN? AIN{ch}", """ Measure the voltage on the corresponding analog input channel, range is [0, 3.3]V""", ) class AnalogOutputSlowChannel(Channel): """ A slow analog output channel""" voltage = Channel.setting( "ANALOG:PIN AOUT{ch}, %f", """ Set the voltage on the corresponding analog input channel, range is [0, 1.8]V""", validator=truncated_range, values=[0, 1.8], ) class AnalogInputFastChannel(Channel): gain = Instrument.control( "ACQ:SOUR{ch}:GAIN?", "ACQ:SOUR{ch}:GAIN %s", """Control the gain of the selected fast analog input either 'LV' or 'HV' (see jumpers on boards) 'LV' set the returned values in the range [-1, 1]V and 'HV' in the range [-20, 20]V """, validator=strict_discrete_set, values=['LV', 'HV'], ) def get_data(self, npts: int = None, format='ASCII') -> np.ndarray: """ Read data from the buffer :param npts: number of points to be read :param format: either 'ASCII' or 'BIN', see :meth:acq_format """ if npts is not None: self.write(f"ACQ:SOUR{'{ch}'}:DATA:Old:N? {npts:.0f}") else: self.write("ACQ:SOUR{ch}:DATA?") if format == 'ASCII': data = self._read_from_ascii() else: data = self._read_from_binary() return data def _read_from_ascii(self) -> np.ndarray: """ Read data from the buffer from ascii format, see :meth:acq_format """ data_str = self.read() return np.fromstring(data_str.strip('{}').encode(), sep=',') def _read_from_binary(self) -> np.ndarray: """ Read data from the buffer from binary format, see :meth:acq_format """ self.read_bytes(1) nint = int(self.read_bytes(1).decode()) length = int(self.read_bytes(nint).decode()) data = np.frombuffer(self.read_bytes(length), dtype=int) self.read_bytes(2) if self.gain == 'LV': max_range = 2 * RedPitayaScpi.LV_MAX else: max_range = 2 * RedPitayaScpi.HV_MAX return max_range * data / (2**16 - 1) - max_range / 2 class RedPitayaScpi(SCPIMixin, Instrument): """This is the class for the Redpitaya reconfigurable board The instrument is accessed using a TCP/IP Socket communication, that is an adapter in the form: "TCPIP::x.y.z.k::port::SOCKET" where x.y.z.k is the IP address of the SCPI server (that should be activated on the board) and port is the TCP/IP port number, usually 5000 To activate the SCPI server, you have to connect first the redpitaya to your computer/network and enter the url address written on the network plug (on the redpitaya). It should be something like "RP-F06432.LOCAL/" then browse the menu, open the Development application and activate the SCPI server. When activating the server, you'll be notified with the IP/port address to use with this Instrument. :param ip_address: IP address to use, if `adapter` is None. :param port: Port number to use, if `adapter` is None. """ TRIGGER_SOURCES = ('DISABLED', 'NOW', 'CH1_PE', 'CH1_NE', 'CH2_PE', 'CH2_NE', 'EXT_PE', 'EXT_NE', 'AWG_PE', 'AWG_NE') LV_MAX = 1 HV_MAX = 20 CLOCK = 125e6 # Hz DELAY_NS = tuple(np.array(np.array(range(-2**13, 2**13+1)) * 1 / CLOCK * 1e9, dtype=int)) def __init__(self, adapter=None, ip_address: str = '169.254.134.87', port: int = 5000, name="Redpitaya SCPI", read_termination='\r\n', write_termination='\r\n', **kwargs): if adapter is None: # if None build it from the usual way as written in the documentation adapter = f"TCPIP::{ip_address}::{port}::SOCKET" super().__init__( adapter, name, read_termination=read_termination, write_termination=write_termination, **kwargs) dioN = Instrument.MultiChannelCreator(DigitalChannelN, list(range(7)), prefix='dioN') dioP = Instrument.MultiChannelCreator(DigitalChannelP, list(range(7)), prefix='dioP') led = Instrument.MultiChannelCreator(DigitalChannelLed, list(range(8)), prefix='led') analog_in_slow = Instrument.MultiChannelCreator(AnalogInputSlowChannel, list(range(4)), prefix='ainslow') analog_out_slow = Instrument.MultiChannelCreator(AnalogOutputSlowChannel, list(range(4)), prefix='aoutslow') analog_in = Instrument.MultiChannelCreator(AnalogInputFastChannel, (1, 2), prefix='ain') time = Instrument.control("SYST:TIME?", "SYST:TIME %s", """Control the time on board time should be given as a datetime.time object""", get_process=lambda _tstr: datetime.time(*[int(split) for split in _tstr]), set_process=lambda _time: _time.strftime('%H,%M,%S'), ) date = Instrument.control("SYST:DATE?", "SYST:DATE %s", """Control the date on board date should be given as a datetime.date object""", get_process=lambda dstr: datetime.date(*[int(split) for split in dstr]), set_process=lambda date: date.strftime('%Y,%m,%d'), ) board_name = Instrument.measurement("SYST:BRD:Name?", """Get the RedPitaya board name""") def digital_reset(self): """Reset the state of all digital lines""" self.write("DIG:RST") # ANALOG SECTION def analog_reset(self): """ Reset the voltage of all analog channels """ self.write("ANALOG:RST") # ACQUISITION SECTION def acquisition_start(self): self.write("ACQ:START") def acquisition_stop(self): self.write("ACQ:STOP") def acquisition_reset(self): self.write("ACQ:RST") # Acquisition Settings decimation = Instrument.control( "ACQ:DEC?", "ACQ:DEC %d", """Control the decimation (int) as 2**n with n in range [0, 16] The sampling rate is given as 125MS/s / decimation """, validator=strict_discrete_set, values=[2**n for n in range(17)], cast=int, ) average_skipped_samples = Instrument.control( "ACQ:AVG?", "ACQ:AVG %s", """Control the use of skipped samples (if decimation > 1) to average the returned acquisition array (bool)""", validator=strict_discrete_set, map_values=True, values={True: 'ON', False: 'OFF'}, ) acq_units = Instrument.control( "ACQ:DATA:Units?", "ACQ:DATA:Units %s", """Control the output data units (str), either 'RAW', or 'VOLTS' (default)""", validator=strict_discrete_set, values=['RAW', 'VOLTS'], ) buffer_length = Instrument.measurement( "ACQ:BUF:SIZE?", """Measure the size of the buffer, that is the number of points of the acquisition""", cast=int, ) acq_format = Instrument.setting( "ACQ:DATA:FORMAT %s", """Set the format of the retrieved buffer data (str), either 'BIN', or 'ASCII' (default)""", validator=strict_discrete_set, values=['BIN', 'ASCII'], ) # Acquisition Trigger acq_trigger_source = Instrument.setting( "ACQ:TRig %s", """Set the trigger source (str), one of RedPitayaScpi.TRIGGER_SOURCES. PE and NE means respectively Positive and Negative edge """, validator=strict_discrete_set, values=TRIGGER_SOURCES, ) acq_trigger_status = Instrument.measurement( "ACQ:TRig:STAT?", """Get the trigger status (bool), if True the trigger as been fired (or is disabled)""", map_values=True, values={True: 'TD', False: 'WAIT'}, ) acq_trigger_position = Instrument.measurement( "ACQ:TPOS?", """Get the position within the buffer where the trigger event happened""", cast=int, ) acq_buffer_filled = Instrument.measurement( "ACQ:TRig:FILL?", """Get the status of the buffer(bool), if True the buffer is full""", map_values=True, values={True: 1, False: 0}, ) acq_trigger_delay_samples = Instrument.control( "ACQ:TRig:DLY?", "ACQ:TRig:DLY %d", """Control the trigger delay in number of samples (int) in the range [-8192, 8192]""", validator=truncated_range, cast=int, values=[-2**13, 2**13], ) # direct call to the SCPI command "ACQ:TRig:DLY:NS?" seems not to be working... @property def acq_trigger_delay_ns(self): """Control the trigger delay in nanoseconds (int) in the range [-8192, 8192] / CLOCK""" return int(self.acq_trigger_delay_samples * 1 / self.CLOCK * 1e9) @acq_trigger_delay_ns.setter def acq_trigger_delay_ns(self, delay_ns: int): delay_sample = int(delay_ns * self.CLOCK / 1e9) self.acq_trigger_delay_samples = delay_sample # not working # acq_trigger_delay_ns = Instrument.control( # "ACQ:TRig:DLY:NS?", "ACQ:TRig:DLY:NS %d", # """Control the trigger delay in nanoseconds (int) multiple of the board clock period # (1/RedPitayaSCPI.CLOCK)""", # validator=truncated_discrete_set, # values=DELAY_NS, # cast=int, # ) acq_trigger_level = Instrument.control( "ACQ:TRig:LEV?", "ACQ:TRig:LEV %f", """Control the level of the trigger in volts The allowed range should be dynamically set depending on the gain settings either +-LV_MAX or +- HV_MAX """, validator=truncated_range, values=[-LV_MAX, LV_MAX], dynamic=True, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/resources.py0000644000175100001770000000647314623331163022220 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from serial.tools import list_ports 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 communication try: res = rm.open_resource(instr) # try to avoid errors from *idn? try: # noinspection PyUnresolvedReferences idn = res.query('*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 def find_serial_port(vendor_id=None, product_id=None, serial_number=None): """Find the VISA port name of the first serial device with the given USB information. Use `None` as a value if you do not want to check for that parameter. .. code-block:: python resource_name = find_serial_port(vendor_id=1256, serial_number="SN12345") dmm = Agilent34410(resource_name) :param int vid: Vendor ID. :param int pid: Product ID. :param str sn: Serial number. :return str: Port as a VISA string for a serial device (e.g. "ASRL5" or "ASRL/dev/ttyACM5"). """ for port in sorted(list_ports.comports()): if ((vendor_id is None or port.vid == vendor_id) and (product_id is None or port.pid == product_id) and (serial_number is None or port.serial_number == str(serial_number))): # remove "COM" from windows serial port names. port_name = port.device.replace("COM", "") return "ASRL" + port_name raise AttributeError("No device found for the given data.") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4136057 pymeasure-0.14.0/pymeasure/instruments/rohdeschwarz/0000755000175100001770000000000014623331176022331 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/rohdeschwarz/__init__.py0000644000175100001770000000231014623331163024432 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .sfm import SFM from .fsl import FSL from .hmp import HMP4040 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/rohdeschwarz/fsl.py0000644000175100001770000002007214623331163023464 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from pymeasure.instruments.validators import strict_discrete_set from pymeasure.instruments import Instrument, SCPIMixin log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def _number_or_auto(value): # helper for the bandwidth setting if isinstance(value, str) and value.upper() == "AUTO": return ":AUTO ON" else: # There is no space in the set commands, so we have to add it return " " + str(value) class FSL(SCPIMixin, Instrument): """ Represents a Rohde&Schwarz FSL spectrum analyzer. All physical values that can be set can either be as a string of a value and a unit (e.g. "1.2 GHz") or as a float value in the base units (Hz, dBm, etc.). """ def __init__(self, adapter, name="Rohde&Schwarz FSL", **kwargs): super().__init__( adapter, name, **kwargs ) # Frequency settings ------------------------------------------------------ freq_span = Instrument.control( "FREQ:SPAN?", "FREQ:SPAN %s", "Frequency span in Hz.", ) freq_center = Instrument.control( "FREQ:CENT?", "FREQ:CENT %s", "Center frequency in Hz.", ) freq_start = Instrument.control( "FREQ:STAR?", "FREQ:STAR %s", "Start frequency in Hz.", ) freq_stop = Instrument.control( "FREQ:STOP?", "FREQ:STOP %s", "Stop frequency in Hz.", ) attenuation = Instrument.control( "INP:ATT?", "INP:ATT %s", "Attenuation in dB.", ) res_bandwidth = Instrument.control( "BAND:RES?", # There is no space between RES and %s on purpose, see _number_or_auto. "BAND:RES%s", "Resolution bandwidth in Hz. Can be set to 'AUTO'", set_process=_number_or_auto, ) video_bandwidth = Instrument.control( "BAND:VID?", "BAND:VID%s", "Video bandwidth in Hz. Can be set to 'AUTO'", set_process=_number_or_auto, ) # Sweeping ---------------------------------------------------------------- sweep_time = Instrument.control( "SWE:TIME?", # No space between TIME and %s on purpose, see _number_or_auto. "SWE:TIME%s", "Sweep time in s. Can be set to 'AUTO'.", set_process=_number_or_auto, ) continuous_sweep = Instrument.control( "INIT:CONT?", "INIT:CONT %s", "Continuous (True) or single sweep (False)", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True, ) def single_sweep(self): """Perform a single sweep with synchronization.""" self.write("INIT; *WAI") def continue_single_sweep(self): """Continue with single sweep with synchronization.""" self.write("INIT:CONM; *WAI") # Traces ------------------------------------------------------------------ def read_trace(self, n_trace=1): """ Read trace data. :param n_trace: The trace number (1-6). Default is 1. :return: 2d numpy array of the trace data, [[frequency], [amplitude]]. """ y = np.array(self.values(f"TRAC{n_trace}? TRACE{n_trace}")) x = np.linspace(self.freq_start, self.freq_stop, len(y)) return np.array([x, y]) trace_mode = Instrument.control( "DISP:TRAC:MODE?", "DISP:TRAC:MODE %s", "Trace mode ('WRIT', 'MAXH', 'MINH', 'AVER' or 'VIEW')", validator=strict_discrete_set, values=["WRIT", "MAXH", "MINH", "AVER", "VIEW"], ) # Markers ----------------------------------------------------------------- def create_marker(self, num=1, is_delta_marker=False): """ Create a marker. :param num: The marker number (1-4) :param is_delta_marker: True if the marker is a delta marker, default is False. :return: The marker object. """ return self.Marker(self, num, is_delta_marker) class Marker: def __init__(self, instrument, num, is_delta_marker): """ Marker and Delta Marker class. :param instrument: The FSL instrument. :param num: The marker number (1-4) :param is_delta_marker: True if the marker is a delta marker, defaults to False. """ self.instrument = instrument self.is_delta_marker = is_delta_marker # Building the marker name for the commands. if self.is_delta_marker: # Smallest delta marker number is 2. self.name = "DELT" + str(max(2, num)) else: self.name = "MARK" if num > 1: # Marker 1 doesn't get a number. self.name = self.name + str(num) self.activate() def read(self): return self.instrument.read() def write(self, command): self.instrument.write(f"CALC:{self.name}:{command}") def ask(self, command): return self.instrument.ask(f"CALC:{self.name}:{command}") def values(self, command, **kwargs): """ Reads a set of values from the instrument through the adapter, passing on any keyword arguments. """ return self.instrument.values( f"CALC:{self.name}:{command}", **kwargs ) def activate(self): """Activate a marker.""" self.write("STAT ON") def disable(self): """Disable a marker.""" self.write("STAT OFF") x = Instrument.control( "X?", "X %s", "Position of marker on the frequency axis in Hz." ) y = Instrument.control( "Y?", "Y %s", "Amplitude of the marker position in dBm." ) peak_excursion = Instrument.control( "PEXC?", "PEXC %s", "Peak excursion in dB.", ) def to_trace(self, n_trace=1): """ Set marker to trace. :param n_trace: The trace number (1-6). Default is 1. """ self.write(f"TRAC {n_trace}") def to_peak(self): """Set marker to highest peak within the span.""" self.write("MAX") def to_next_peak(self, direction="right"): """ Set marker to next peak. :param direction: Direction of the next peak ('left' or 'right' of the current position). """ self.write(f"MAX:{direction}") def zoom(self, value): """ Zoom in to a frequency span or by a factor. :param value: The value to zoom in by. If a number is passed it is interpreted as a factor. If a string (number with unit) is passed it is interpreted as a frequency span. """ self.write(f"FUNC:ZOOM {value}; *WAI") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/rohdeschwarz/hmp.py0000644000175100001770000002227114623331163023467 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIMixin from pymeasure.instruments.validators import (strict_discrete_set, truncated_range) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def process_sequence(sequence): """ Check and prepare sequence data. :param sequence: Sequence data, in the form [voltage1, current1, time1, voltage2, current2, time2, ..., voltage128, current128, time128] with voltages in V, currents in A, and times in s. Dwell times are between 0.06 and 10 s. :type sequence: list of float :return: Sequence data in the form "Voltage1,Current1,Time1,Voltage2, Current2,Time2,...,Voltage128,Current128,Time128" :rtype: str """ if not len(sequence) % 3 == 0: raise ValueError("Sequence must contain multiple of 3 values.") if any(t > 10 or t < 0.06 for t in sequence[2::3]): raise ValueError("Dwell times must be between 0.06 and 10 s.") # turn sequence data into a string sequence = ",".join(str(s) for s in sequence) return sequence class HMP4040(SCPIMixin, Instrument): """Represents a Rohde&Schwarz HMP4040 power supply.""" def __init__(self, adapter, **kwargs): kwargs.setdefault("name", "Rohde&Schwarz HMP4040") super().__init__( adapter, **kwargs ) # System Setting Commands ------------------------------------------------- def beep(self): """Emit a single beep from the instrument.""" self.write("SYST:BEEP") control_method = Instrument.setting( "SYST:%s", """ Control manual front panel ('LOC'), remote ('REM') or manual/remote control('MIX') control or locks the front panel control ('RWL'). """, validator=strict_discrete_set, values=["LOC", "REM", "MIX", "RWL"], ) version = Instrument.measurement( "SYST:VERS?", "Get the SCPI version the instrument's command set complies with.", ) # Channel Selection Commands ---------------------------------------------- selected_channel = Instrument.control( "INST:NSEL?", "INST:NSEL %s", "Control the selected channel.", validator=strict_discrete_set, values=[1, 2, 3, 4], cast=int ) # Voltage Settings -------------------------------------------------------- voltage = Instrument.control( "VOLT?", "VOLT %s", "Control output voltage in V. Increment 0.001 V." ) min_voltage = Instrument.measurement( "VOLT? MIN", "Get minimum voltage in V." ) max_voltage = Instrument.measurement( "VOLT? MAX", "Get maximum voltage in V." ) def voltage_to_min(self): """Set voltage of the selected channel to its minimum value.""" self.write("VOLT MIN") def voltage_to_max(self): """Set voltage of the selected channel to its maximum value.""" self.write("VOLT MAX") voltage_step = Instrument.control( "VOLT:STEP?", "VOLT:STEP %s", "Control voltage step in V. Default 1 V.", validator=truncated_range, values=[0, 32.050], ) def step_voltage_up(self): """Increase voltage by one step.""" self.write("VOLT UP") def step_voltage_down(self): """Decrease voltage by one step.""" self.write("VOLT DOWN") # Current Settings -------------------------------------------------------- current = Instrument.control( "CURR?", "CURR %s", "Control output current in A. Range depends on instrument type.", ) min_current = Instrument.measurement( "CURR? MIN", "Get minimum current in A." ) max_current = Instrument.measurement( "CURR? MAX", "Get maximum current in A." ) def current_to_min(self): """Set current of the selected channel to its minimum value.""" self.write("CURR MIN") def current_to_max(self): """Set current of the selected channel to its maximum value.""" self.write("CURR MAX") current_step = Instrument.control( "CURR:STEP?", "CURR:STEP %s", "Control current step in A." ) def step_current_up(self): """Increase current by one step.""" self.write("CURR UP") def step_current_down(self): """Decreases current by one step.""" self.write("CURR DOWN") # Combined Voltage And Current Settings ----------------------------------- voltage_and_current = Instrument.control( "APPL?", "APPL %s, %s", "Control output voltage (V) and current (A).", ) # Output Settings --------------------------------------------------------- selected_channel_active = Instrument.control( "OUTP:SEL?", "OUTPUT:SEL %s", "Control the selected channel to active or inactive or check its status.", values={True: 1, False: 0}, map_values=True, ) output_enabled = Instrument.control( "OUTP:GEN?", "OUTP:GEN %s", "Control the output on or off or check the output status.", values={True: 1, False: 0}, map_values=True, ) # The following commands are for making it easier to change the selected # channels and activate/deactivate them. def set_channel_state(self, channel, state): """ Set the state of the channel to active or inactive. :param channel: Channel number to set the state of. :type channel: int :param state: State of the channel, i.e. True for active, False for inactive. :type state: bool """ # Save current selected channel before switching. selected_channel = self.selected_channel self.selected_channel = channel self.selected_channel_active = state # Restore previously selected channel. self.selected_channel = selected_channel # Measurement Commands ---------------------------------------------------- measured_voltage = Instrument.measurement( "MEAS:VOLT?", "Get voltage in V." ) measured_current = Instrument.measurement( "MEAS:CURR?", "Get current in A." ) # Arbitrary Sequence Commands --------------------------------------------- def clear_sequence(self, channel): """Clear the sequence of the selected channel.""" channel = strict_discrete_set(channel, [1, 2, 3, 4]) self.write(f"ARB:CLEAR {channel}") sequence = Instrument.setting( "ARB:DATA %s", "Set sequence of triplets of voltage (V), current (A) and dwell " "time (s).", set_process=process_sequence, ) repetitions = Instrument.control( "ARB:REP?", "ARB:REP %s", "Control umber of repetitions (0...255). If 0 is entered, the sequence is" "repeated indefinitely.", validator=strict_discrete_set, values=range(256), cast=int, ) def load_sequence(self, slot): """Load a saved waveform from internal memory (slot 1, 2 or 3).""" slot = strict_discrete_set(slot, [1, 2, 3]) self.write(f"ARB:REST {slot}") def save_sequence(self, slot): """ Save the sequence defined in the sequence property to internal memory (slot 1, 2 or 3). """ slot = strict_discrete_set(slot, [1, 2, 3]) self.write(f"ARB:SAVE {slot}") def start_sequence(self, channel): """Start the sequence of the selected channel.""" channel = strict_discrete_set(channel, [1, 2, 3, 4]) self.write(f"ARB:START {channel}") def stop_sequence(self, channel): """Stop the sequence defined in the sequence property of the selected channel.""" channel = strict_discrete_set(channel, [1, 2, 3, 4]) self.write(f"ARB:STOP {channel}") def transfer_sequence(self, channel): """ Transfer the sequence defined in the sequence property to the selected channel. """ channel = strict_discrete_set(channel, [1, 2, 3, 4]) self.write(f"ARB:TRAN {channel}") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/rohdeschwarz/sfm.py0000644000175100001770000011606414623331163023474 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Sound_Channel: """ Class object for the two sound channels refer also to chapter 3.6.6.7 of the user manual """ modulation_degree = Instrument.control( "AUD:DEGR?", "AUD:DEGR %g", """ A float property that controls the modulation depth for the audio signal (Note: only for the use of AM in Standard L) valid range: 0 .. 1 (100%) """, validator=strict_range, values=[0, 1], ) deviation = Instrument.control( "AUD:DEV?", "AUD:DEV %d", """ A int property that controls deviation of the selected audio signal valid range: 0 .. 110 kHz """, validator=strict_range, values=[0, 1.1E5], ) frequency = Instrument.control( "AUD:FREQ?", "AUD:FREQ %d", """ A int property that controls the frequency of the internal sound generator valid range: 300 Hz .. 15 kHz """, validator=strict_range, values=[300, 1.5E4], ) use_external_source = Instrument.control( "FREQ:SOUR?", "FREQ:SOUR %s", """ A bool property for the audio source selection ====== ======= Value Meaning ====== ======= False Internal audio generator(s) True External signal source ====== ======= """, validator=strict_discrete_set, values={False: "INT", True: "EXT"}, map_values=True, ) modulation_enabled = Instrument.control( "AUD:FREQ:STAT?", "AUD:FREQ:STAT %s", """ A bool property that controls the audio modulation status ====== ======= Value Meaning ====== ======= False modulation disabled True modulation enabled ====== ======= """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) carrier_frequency = Instrument.control( "CARR:FREQ?", "CARR:FREQ %g", """ A float property that controls the frequency of the sound carrier valid range: 32 .. 46 MHz """, validator=strict_range, values=[38.75E6, 52.75E6], ) carrier_level = Instrument.control( "CARR:LEV?", "CARR:LEV %g", """ A float property that controls the level of the audio carrier in dB relative to the vision carrier (0dB) valid range: -34 .. -6 dB """, validator=strict_range, values=[-34, 6], ) carrier_enabled = Instrument.control( "CARR:STAT?", "CARR:STAT %s", """ A bool property that controls if the audio carrier is switched on or off """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) preemphasis_time = Instrument.control( "PRE:MODE?", "PRE:MODE %s", """ A int property that controls if the mode of the preemphasis for the audio signal ====== ======= Value Meaning ====== ======= 50 50 us preemphasis 75 75 us preemphasis ====== ======= """, validator=strict_discrete_set, values={50: "US50", 75: "US75"}, map_values=True, ) preemphasis_enabled = Instrument.control( "PRE:STAT?", "PRE:STAT %s", """ A bool property that controls if the preemphasis for the audio is switched on or off """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) 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 keyword arguments. """ return self.instrument.values("SOUR:TEL:MOD:SOUN%d:%s" % ( self.number, command), **kwargs) def ask(self, command): self.instrument.ask("SOUR:TEL:MOD:SOUN:%d:%s" % (self.number, command)) def write(self, command): self.instrument.write("SOUR:TEL:MOD:SOUN:%d:%s" % (self.number, command)) def read(self): self.instrument.read() class SFM(SCPIMixin, Instrument): """ Represents the Rohde&Schwarz SFM TV test transmitter interface for interacting with the instrument. .. Note:: The current implementation only works with the first system in this unit. Further source extension for system 2-6 would be required. The intermodulation subsystem is also not yet implemented. """ def __init__(self, adapter, name="Rohde&Schwarz SFM", **kwargs): super().__init__( adapter, name, **kwargs ) self.sound1 = Sound_Channel(self, 1) self.sound2 = Sound_Channel(self, 2) def calibration(self, number=1, subsystem=None): """ Function to either calibrate the whole modulator, when subsystem parameter is omitted, or calibrate a subsystem of the modulator. Valid subsystem selections: "NICam, VISion, SOUNd1, SOUNd2, CODer" """ if subsystem is None: self.write("CAL:MOD%d" % (number)) else: self.write( "CAL:MOD%d:%s" % ( number, strict_discrete_set(subsystem, ["NIC", "NICAM", "VIS", "VISION", "SOUN1", "SOUND1", "SOUN2", "SOUND2", "COD", "CODER"]) ) ) # INST (Manual 3.6.4) system_number = Instrument.control( "INST:SEL?", "INST:SEL:%s", """A int property for the selected systems (if more than 1 available) * Minimum 1 * Maximum 6 """, validator=strict_discrete_set, values={1: "SYS1", 2: "SYS2", 3: "SYS3", 4: "SYS4", 5: "SYS5", 6: "SYS6"}, map_values=True, check_set_errors=True, ) R75_out = Instrument.control( "ROUT:CHAN:OUTP:IMP?", "ROUT:CHAN:OUTP:IMP %s", """ A bool property that controls the use of the 75R output (if installed) ====== ======= Value Meaning ====== ======= False 50R output active (N) True 75R output active (BNC) ====== ======= refer also to chapter 3.6.5 of the manual """, validator=strict_discrete_set, values={False: "LOW", True: "HIGH"}, map_values=True, ) ext_ref_base_unit = Instrument.control( "ROUT:REF:CLOCK:BAS?", "ROUT:REF:CLOCK:BAS %s", """ A bool property for the external reference for the basic unit ====== ======= Value Meaning ====== ======= False Internal 10 MHz is used True External 10 MHz is used ====== ======= """, validator=strict_discrete_set, values={False: "INT", True: "EXT"}, map_values=True, ) ext_ref_extension = Instrument.control( "ROUT:REF:CLOCK:EXT?", "ROUT:REF:CLOCK:EXT %s", """ A bool property for the external reference for the extension frame ====== ======= Value Meaning ====== ======= False Internal 10 MHz is used True External 10 MHz is used ====== ======= """, validator=strict_discrete_set, values={False: "INT", True: "EXT"}, map_values=True, ) ext_vid_connector = Instrument.control( "ROUT:TEL:VID:EXT?", "ROUT:TEL:VID:EXT %s", """A string property controlling which connector is used as the input of the video source Possible selections are: ====== ======= Value Meaning ====== ======= HIGH Front connector - Hi-Z LOW Front connector - 75R REAR1 Rear connector 1 REAR2 Rear connector 2 AUTO Automatic assignment ====== ======= """, validator=strict_discrete_set, values=["HIGH", "LOW", "REAR1", "REAR2", "AUTO"], ) channel_table = Instrument.control( "SOUR:FREQ:CHAN:TABL ?", "SOUR:FREQ:CHAN:TABL %s", """A string property controlling which channel table is used Possible selections are: ====== ======= Value Meaning ====== ======= DEF Default channel table USR1 User table No. 1 USR2 User table No. 2 USR3 User table No. 3 USR4 User table No. 4 USR5 User table No. 5 ====== ======= refer also to chapter 3.6.6.1 of the manual """, validator=strict_discrete_set, values=["DEF", "USR1", "USR2", "USR3", "USR4", "USR5"], ) normal_channel = Instrument.control( "SOUR:FREQ:CHAN:NORM ?", "SOUR:FREQ:CHAN:NORM %d", """A int property controlling the current selected regular/normal channel number valid selections are based on the country settings. """, ) special_channel = Instrument.control( "SOUR:FREQ:CHAN:SPEC ?", "SOUR:FREQ:CHAN:SPEC %d", """A int property controlling the current selected special channel number valid selections are based on the country settings. """, ) def channel_up_relative(self): """ Increases the output frequency to the next higher channel/special channel based on the current country settings """ Instrument.write(self, "SOUR:CHAN:REL UP") def channel_down_relative(self): """ Decreases the output frequency to the next low channel/special channel based on the current country settings """ Instrument.write(self, "SOUR:CHAN:REL DOWN") channel_sweep_start = Instrument.control( "SOUR:FREQ:CHAN:STAR?", "SOUR:FREQ:CHAN:STAR %g", """A float property controlling the start frequency for channel sweep in Hz * Minimum 5 MHz * Maximum 1 GHz """, validator=strict_range, values=[5E6, 1E9] ) channel_sweep_stop = Instrument.control( "SOUR:FREQ:CHAN:STOP?", "SOUR:FREQ:CHAN:STOP %g", """A float property controlling the start frequency for channel sweep in Hz * Minimum 5 MHz * Maximum 1 GHz """, validator=strict_range, values=[5E6, 1E9] ) channel_sweep_step = Instrument.control( "SOUR:FREQ:CHAN:STEP?", "SOUR:FREQ:CHAN:STEP %g", """A float property controlling the start frequency for channel sweep in Hz * Minimum 5 MHz * Maximum 1 GHz """, validator=strict_range, values=[5E6, 1E9] ) cw_frequency = Instrument.control( "SOUR:FREQ:CW?", "SOUR:FREQ:CW %g", """A float property controlling the CW-frequency in Hz * Minimum 5 MHz * Maximum 1 GHz """, validator=strict_range, values=[5E6, 1E9] ) frequency = Instrument.control( "SOUR:FREQ:FIXED?", "SOUR:FREQ:FIXED %g", """A float property controlling the frequency in Hz * Minimum 5 MHz * Maximum 1 GHz """, validator=strict_range, values=[5E6, 1E9] ) frequency_mode = Instrument.control( "SOUR:FREQ:MODE?", "SOUR:FREQ:MODE %s", """A string property controlling which the unit is used in Possible selections are: ====== ======= Value Meaning ====== ======= CW Continuous wave mode FIXED fixed frequency mode CHSW Channel sweep RFSW Frequency sweep ====== ======= .. Note:: selecting the sweep mode, will start the sweep imemdiately! """, validator=strict_discrete_set, values=["CW", "FIXED", "CHSW", "RFSW"], ) high_frequency_resolution = Instrument.control( "SOUR:FREQ:RES?", "SOUR:FREQ:RES %s", """ A property that controls the frequency resolution, Possible selections are: ====== ======= Value Meaning ====== ======= False Low resolution (1000Hz) True High resolution (1Hz) ====== ======= """, validator=strict_discrete_set, values={False: "LOW", True: "HIGH"}, map_values=True, ) rf_sweep_center = Instrument.control( "SOUR:FREQ:CENTER?", "SOUR:FREQ:CENTER %g", """A float property controlling the center frequency for sweep in Hz * Minimum 5 MHz * Maximum 1 GHz """, validator=strict_range, values=[5E6, 1E9] ) rf_sweep_start = Instrument.control( "SOUR:FREQ:STAR?", "SOUR:FREQ:STAR %g", """A float property controlling the start frequency for sweep in Hz * Minimum 5 MHz * Maximum 1 GHz """, validator=strict_range, values=[5E6, 1E9] ) rf_sweep_stop = Instrument.control( "SOUR:FREQ:STOP?", "SOUR:FREQ:STOP %g", """A float property controlling the stop frequency for sweep in Hz * Minimum 5 MHz * Maximum 1 GHz """, validator=strict_range, values=[5E6, 1E9] ) rf_sweep_step = Instrument.control( "SOUR:FREQ:STEP?", "SOUR:FREQ:STEP %g", """A float property controlling the stepwidth for sweep in Hz, * Minimum 1 kHz * Maximum 1 GHz """, validator=strict_range, values=[1E3, 1E9] ) rf_sweep_span = Instrument.control( "SOUR:FREQ:SPAN?", "SOUR:FREQ:SPAN %g", """A float property controlling the sweep span in Hz, * Minimum 1 kHz * Maximum 1 GHz """, validator=strict_range, values=[1E3, 1E9] ) level = Instrument.control( "SOUR:POW:LEV?", "SOUR:POW:LEV %g DBM", """A float property controlling the output level in dBm, * Minimum -99dBm * Maximum 10dBm (depending on output mode) refer also to chapter 3.6.6.2 of the manual """, validator=strict_range, values=[-99, 10], ) level_mode = Instrument.control( "SOUR:POW:LEV:MODE?", "SOUR:POW:LEV:MODE %s", """A string property controlling the output attenuator and linearity mode Possible selections are: ====== ==================== ================= Value Meaning max. output level ====== ==================== ================= NORM Normal mode +6 dBm LOWN low noise mode +10 dBm CONT continuous mode +10 dBm LOWD low distortion mode +0 dBm ====== ==================== ================= Contiuous mode allows up to 14 dB of level setting without use of the mechanical attenuator. """, validator=strict_discrete_set, values=["NORM", "LOWN", "CONT", "LOWD"] ) rf_out_enabled = Instrument.control( "SOUR:POW:STAT?", "SOUR:POW:STATE %s", """ A bool property that controls the status of the RF-output """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) def coder_adjust(self): """ Starts the automatic setting of the differential deviation refer also to chapter 3.6.6.4 of the manual """ self.write("SOUR:TEL:MOD:COD:ADJ") coder_id_frequency = Instrument.control( "SOUR:TEL:MOD:COD:IDENT:FREQ?", "SOUR:TEL:MOD:COD:IDENT:FREQ %d", """ A int property that controls the frequency of the identification of the coder valid range 0 .. 200 Hz """, validator=strict_range, values=[0, 200], ) coder_modulation_degree = Instrument.control( "SOUR:TEL:MOD:COD:MOD:DEGR?", "SOUR:TEL:MOD:COD:MOD:DEGR %g", """ A float property that controls the modulation degree of the identification of the coder valid range: 0 .. 0.9 """, validator=strict_range, values=[0, 0.9], ) coder_pilot_frequency = Instrument.control( "SOUR:TEL:MOD:COD:PIL:FREQ?", "SOUR:TEL:MOD:COD:PIL:FREQ %d", """ A int property that controls the pilot frequency of the coder valid range: 40 .. 60 kHz """, validator=strict_range, values=[5E4, 6E4], ) coder_pilot_deviation = Instrument.control( "SOUR:TEL:MOD:COD:PIL:FREQ:DEV?", "SOUR:TEL:MOD:COD:PIL:FREQ:DEV %d", """ A int property that controls deviation of the pilot frequency of the coder valid range: 1 .. 4 kHz """, validator=strict_range, values=[1E3, 4E3], ) external_modulation_power = Instrument.control( "SOUR:TEL:MOD:EXT:POW?", "SOUR:TEL:MOD:EXT:POW %d", """ A int property that controls the setting for the external modulator output power valid range: -7..0 dBm refer also to chapter 3.6.6.5 of the manual """, validator=strict_range, values=[-7, 0], ) external_modulation_frequency = Instrument.control( "SOUR:TEL:MOD:EXT:FREQ?", "SOUR:TEL:MOD:EXT:FREQ %d", """ A int property that controls the setting for the external modulator frequency valid range: 32 .. 46 MHz """, validator=strict_range, values=[32e6, 46e6], ) nicam_mode = Instrument.control( "SOUR:TEL:MOD:NIC:AUD:MODE?", "SOUR:TEL:MOD:NIC:AUD:MODE %s", """ A string property that controls the signal type to be sent via NICAM Possible values are: ====== ======= Value Meaning ====== ======= MON Mono sound + NICAM data STER Stereo sound DUAL Dual channel sound DATA NICAM data only ====== ======= refer also to chapter 3.6.6.6 of the manual """, validator=strict_discrete_set, values=["MON", "STER", "DUAL", "DATA"], ) nicam_audio_frequency = Instrument.control( "SOUR:TEL:MOD:NIC:AUD:FREQ?", "SOUR:TEL:MOD:NIC:AUD:FREQ %d", """ A int property that controls the frequency of the internal sound generator valid range: 0 Hz .. 15 kHz """, validator=strict_range, values=[0, 1.5E4], ) nicam_preemphasis_enabled = Instrument.control( "SOUR:TEL:MOD:NIC:AUD:PRE?", "SOUR:TEL:MOD:NIC:AUD:PRE %d", """ A bool property that controls the status of the J17 preemphasis """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) nicam_audio_volume = Instrument.control( "SOUR:TEL:MOD:NIC:AUD:VOL?", "SOUR:TEL:MOD:NIC:AUD:VOL %g", """ A float property that controls the audio volume in the NICAM modulator in dB valid range: 0..60 dB """, validator=strict_range, values=[0, 60], ) nicam_data = Instrument.control( "SOUR:TEL:MOD:NIC:AUD:DATA?", "SOUR:TEL:MOD:NIC:AUD:DATA %d", """ A int property that controls the data in the NICAM modulator valid range: 0 .. 2047 """, validator=strict_range, values=[0, 2047], cast=int ) nicam_additional_bits = Instrument.control( "SOUR:TEL:MOD:NIC:AUD:ADD?", "SOUR:TEL:MOD:NIC:AUD:ADD %d", """ A int property that controls the additional data in the NICAM modulator valid range: 0 .. 2047 """, validator=strict_range, values=[0, 2047], ) nicam_control_bits = Instrument.control( "SOUR:TEL:MOD:NIC:AUD:CONT?", "SOUR:TEL:MOD:NIC:AUD:CONT %d", """ A int property that controls the additional data in the NICAM modulator valid range: 0 .. 3 """, validator=strict_range, values=[0, 3], ) nicam_bit_error_rate = Instrument.control( "SOUR:TEL:MOD:NIC:BIT?", "SOUR:TEL:MOD:NIC:BIT %g", """ A float property that controls the artificial bit error rate. valid range: 1.2E-7 .. 2E-3 """, validator=strict_range, values=[1.2E-7, 2E-3], ) nicam_bit_error_enabled = Instrument.control( "SOUR:TEL:MOD:NIC:BIT:STAT?", "SOUR:TEL:MOD:NIC:BIT:STAT %d", """ A bool property that controls the status of an artificial bit error rate to be applied """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) nicam_carrier_frequency = Instrument.control( "SOUR:TEL:MOD:NIC:CARR:FREQ?", "SOUR:TEL:MOD:NIC:CARR:FREQ %g", """ A float property that controls the frequency of the NICAM carrier valid range: 33.05 MHz +/- 0.2 Mhz """, validator=strict_range, values=[32.85E6, 33.25E6], ) nicam_intercarrier_frequency = Instrument.control( "SOUR:TEL:MOD:NIC:INT:FREQ?", "SOUR:TEL:MOD:NIC:INT:FREQ %g", """ A float property that controls the inter-carrier frequency of the NICAM carrier valid range: 5 .. 9 MHz """, validator=strict_range, values=[5E6, 9E6], ) nicam_carrier_level = Instrument.control( "SOUR:TEL:MOD:NIC:CARR:LEV?", "SOUR:TEL:MOD:NIC:CARR:LEV %g", """ A float property that controls the value of the NICAM carrier valid range: -40 .. -13 dB """, validator=strict_range, values=[-40, 13], ) nicam_carrier_enabled = Instrument.control( "SOUR:TEL:MOD:NIC:CARR:STAT?", "SOUR:TEL:MOD:NIC:CARR:STAT %s", """ A bool property that controls if the NICAM carrier is switched on or off """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) nicam_IQ_inverted = Instrument.control( "SOUR:TEL:MOD:NIC:MODE?", "SOUR:TEL:MOD:NIC:MODE %s", """ A bool property that controls if the NICAM IQ signals are inverted or not ====== ======= Value Meaning ====== ======= False normal (IQ) True inverted (QI) ====== ======= """, validator=strict_discrete_set, values={False: "IQ", True: "QI"}, map_values=True, ) nicam_source = Instrument.control( "SOUR:TEL:MOD:NIC:SOUR?", "SOUR:TEL:MOD:NIC:SOUR %s", """ A string property that controls the signal source for NICAM Possible values are: ====== ======= Value Meaning ====== ======= INT Internal audio generator(s) EXT External audio source CW Continuous wave signal RAND Random data stream TEST Test signal ====== ======= """, validator=strict_discrete_set, values=["INT", "EXT", "CW", "RAND", "TEST"], ) nicam_test_signal = Instrument.control( "SOUR:TEL:MOD:NIC:TEST?", "SOUR:TEL:MOD:NIC:TEST %s", """ A int property that controls the selection of the test signal applied ====== ======= Value Meaning ====== ======= 1 Test signal 1 (91 kHz square wave, I&Q 90deg apart) 2 Test signal 2 (45.5 kHz square wave, I&Q 90deg apart) 3 Test signal 3 (182 kHz sine wave, I&Q in phase) ====== ======= """, validator=strict_discrete_set, values={1: "TST1", 2: "TST2", 3: "TST3"}, map_values=True, ) external_modulation_source = Instrument.control( "SOUR:MOD:SOUR?", "SOUR:MOD:SOUR %s", """ A bool property for the modulation source selection refer also to chapter 3.6.6.8 of the manual """, validator=strict_discrete_set, values={False: "INT", True: "EXT"}, map_values=True, ) modulation_enabled = Instrument.control( "SOUR:MOD:STAT?", "SOUR:MOD:STAT %s", """ A bool property that controls the modulation status """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) vision_carrier_enabled = Instrument.control( "SOUR:TEL:MOD:VIS:CARR:STAT?", "SOUR:TEL:MOD:VIS:CARR:STAT %s", """ A bool property that controls the vision carrier status refer also to chapter 3.6.6.9 of the manual """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) vision_carrier_frequency = Instrument.control( "SOUR:TEL:MOD:VIS:CARR:FREQ?", "SOUR:TEL:MOD:VIS:CARR:FREQ %g", """ A float property that controls the frequency of the vision carrier valid range: 32 .. 46 MHz """, validator=strict_range, values=[32E6, 46E6], ) vision_average_enabled = Instrument.control( "SOUR:TEL:MOD:VIS:AVER:STAT?", "SOUR:TEL:MOD:VIS:AVER:STAT %s", """ A bool property that controls the average mode for the vision system """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) vision_balance = Instrument.control( "SOUR:TEL:MOD:VIS:BAL?", "SOUR:TEL:MOD:VIS:BAL %g", """ A float property that controls the balance of the vision modulator valid range: -0.5 .. 0.5 """, validator=strict_range, values=[-0.5, 0.5], ) vision_clamping_average = Instrument.control( "SOUR:TEL:MOD:VIS:CLAM:AVER?", "SOUR:TEL:MOD:VIS:CLAM:AVER %g", """ A float property that controls the operation point of the vision modulator valid range: -0.5 .. 0.5 """, validator=strict_range, values=[-0.5, 0.5], ) vision_clamping_enabled = Instrument.control( "SOUR:TEL:MOD:VIS:CLAM:STAT?", "SOUR:TEL:MOD:VIS:CLAM:STAT %s", """ A bool property that controls the clamping behavior of the vision modulator """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) vision_clamping_mode = Instrument.control( "SOUR:TEL:MOD:VIS:CLAM:TYPE?", "SOUR:TEL:MOD:VIS:CLAM:TYPE %s", """ A string property that controls the clamping mode of the vision modulator Possible selections are HARD or SOFT """, validator=strict_discrete_set, values=["HARD", "SOFT"], ) vision_precorrection_enabled = Instrument.control( "SOUR:TEL:MOD:VIS:PREC?", "SOUR:TEL:MOD:VIS:PREC %s", """ A bool property that controls the precorrection behavior of the vision modulator """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) vision_residual_carrier_level = Instrument.control( "SOUR:TEL:MOD:VIS:RES?", "SOUR:TEL:MOD:VIS:RES %g", """ A float property that controls the value of the residual carrier valid range: 0 .. 0.3 (30%) """, validator=strict_range, values=[0, 0.3], ) vision_videosignal_enabled = Instrument.control( "SOUR:TEL:MOD:VIS:VID?", "SOUR:TEL:MOD:VIS:VID %s", """ A bool property that controls if the video signal is switched on or off """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) vision_sideband_filter_enabled = Instrument.control( "SOUR:TEL:MOD:VIS:VSBF?", "SOUR:TEL:MOD:VIS:VSBF %s", """ A bool property that controls the use of the VSBF (vestigal sideband filter) in the vision modulator """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) lower_sideband_enabled = Instrument.control( "SOUR:TEL:SID?", "SOUR:TEL:SID %s", """ A bool property that controls the use of the lower sideband refer also to chapter 3.6.6.10 of the manual """, validator=strict_discrete_set, values={False: "UPP", True: "LOW"}, map_values=True, ) sound_mode = Instrument.control( "SOUR:TEL:SOUN?", "SOUR:TEL:SOUN %s", """ A string property that controls the type of audio signal Possible values are: ====== ======= Value Meaning ====== ======= MONO MOnoaural sound PIL pilot-carrier + mono BTSC BTSC + mono STER Stereo sound DUAL Dual channel sound NIC NICAM + Mono ====== ======= """, validator=strict_discrete_set, values=["MONO", "PIL", "BTSC", "STER", "DUAL", "NIC"], ) TV_standard = Instrument.control( "SOUR:TEL:STAN?", "SOUR:TEL:STAN %s", """ A string property that controls the type of video standard Possible values are: ====== ====== ====== Value Lines System ====== ====== ====== BG 625 PAL DK 625 SECAM I 625 PAL K1 625 SECAM L 625 SECAM M 525 NTSC N 625 NTSC ====== ====== ====== Please confirm with the manual about the details for these settings. """, validator=strict_discrete_set, values=["BG", "DK", "I", "K1", "L", "M", "N"], ) TV_country = Instrument.control( "SOUR:TEL:STAN:COUN?", "SOUR:TEL:STAN:COUN %s", """ A string property that controls the country specifics of the video/sound system to be used Possible values are: ====== ======= Value Meaning ====== ======= BG_G BG General DK_G DK General I_G I General L_G L General GERM Germany BELG Belgium NETH Netherlands FIN Finland AUST Australia BG_T BG Th DENM Denmark NORW Norway SWED Sweden GUS Russia POL1 Poland POL2 Poland HUNG Hungary CHEC Czech Republic CHINA1 China CHINA2 China GRE Great Britain SAFR South Africa FRAN France USA United States KOR Korea JAP Japan CAN Canada SAM South America ====== ======= Please confirm with the manual about the details for these settings. """, validator=strict_discrete_set, values=["BG_G", "DK_G", "I_G", "L_G", "GERM", "BELG", "NETH", "FIN", "AUST", "BG_T", "DENM", "NORW", "SWED", "GUS", "POL1", "POL2", "HUNG", "CHEC", "CHINA1", "CHINA2", "GRE", "SAFR", "FRAN", "USA", "KOR", "JAP", "CAN", "SAM"], ) output_voltage = Instrument.control( "SOUR:VOLT:LEV?", "SOUR:VOLT:LEV %g", """A float property controlling the output level in Volt, Minimum 2.50891e-6, Maximum 0.707068 (depending on output mode) refer also to chapter 3.6.6.12 of the manual """, validator=strict_range, values=[2.508910e-6, 0.7070168], ) event_reg = Instrument.measurement( "STAT:OPER:EVEN?", """ Content of the event register of the Status Operation Register refer also to chapter 3.6.7 of the manual """, cast=int, ) status_reg = Instrument.measurement( "STAT:OPER:COND?", """ Content of the condition register of the Status Operation Register """, cast=int, ) operation_enable_reg = Instrument.control( "STAT:OPER:ENAB?", "STAT:OPER:ENAB %d", """ Content of the enable register of the Status Operation Register Valid range: 0...32767 """, cast=int, validator=strict_range, values=[0, 32767] ) def status_preset(self): """ partly resets the SCPI status reporting structures """ self.write("STAT:PRES") questionable_event_reg = Instrument.measurement( "STAT:QUES:EVEN?", """ Content of the event register of the Status Questionable Operation Register """, cast=int, ) questionanble_status_reg = Instrument.measurement( "STAT:QUES:COND?", """ Content of the condition register of the Status Questionable Operation Register """, cast=int, ) questionable_operation_enable_reg = Instrument.control( "STAT:QUES:ENAB?", "STAT:QUES:ENAB %d", """ Content of the enable register of the Status Questionable Operation Register Valid range 0...32767 """, cast=int, validator=strict_range, values=[0, 32767] ) beeper_enabled = Instrument.control( "SYST:BEEP:STATE?", "SYST:BEEP:STATE %s", """ A bool property that controls the beeper status, refer also to chapter 3.6.8 of the manual """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) status_info_shown = Instrument.control( "SYST:DISP:UPDATE:STATE?", "SYST:DISP:UPDATE:STATE %s", """ A bool property that controls if the display shows information during remote control """, validator=strict_discrete_set, values={False: 0, True: 1}, map_values=True, ) gpib_address = Instrument.control( "SYST:COMM:GPIB:ADDR?", "SYST:COMM:GPIB:ADDR %d", """ A int property that controls the GPIB address of the unit valid range: 0..30 """, validator=strict_range, values=[0, 30], ) remote_interfaces = Instrument.control( "SYST:COM:REM?", "SYST:COM:REM %s", """A string property controlling the selection of interfaces for remote control Possible selections are: ====== ======= Value Meaning ====== ======= OFF no remote control GPIB GPIB only enabled SER RS232 only enabled BOTH GPIB & RS232 enabled ====== ======= """, validator=strict_discrete_set, values=["OFF", "GPIB", "SER", "BOTH"] ) serial_baud = Instrument.control( "SYST:COMM:SER:BAUD?", "SYST:COMM:SER:BAUD %g", """ A int property that controls the serial communication speed , Possible values are: 110,300,600,1200,4800,9600,19200 """, validator=strict_discrete_set, values=[110, 300, 600, 1200, 4800, 9600, 19200], ) serial_bits = Instrument.control( "SYST:COMM:SER:BITS?", "SYST:COMM:SER:BITS %g", """ A int property that controls the number of bits used in serial communication Possible values are: 7 or 8 """, validator=strict_discrete_set, values=[7, 8], ) serial_flowcontrol = Instrument.control( "SYST:COMM:SER:PACE?", "SYST:COMM:SER:PACE %s", """ A string property that controls the serial handshake type used in serial communication Possible values are: ====== ======= Value Meaning ====== ======= NONE no flow-control/handshake XON XON/XOFF flow-control ACK hardware handshake with RTS&CTS ====== ======= """, validator=strict_discrete_set, values=["NONE", "XON", "ACK"], ) serial_parity = Instrument.control( "SYST:COMM:SER:PAR?", "SYST:COMM:SER:PAR %s", """ A string property that controls the parity type used for serial communication Possible values are: ====== ======= Value Meaning ====== ======= NONE no parity EVEN even parity ODD odd parity ONE parity bit fixed to 1 ZERO parity bit fixed to 0 ====== ======= """, validator=strict_discrete_set, values=["NONE", "EVEN", "ODD", "ONE", "ZERO"], ) serial_stopbits = Instrument.control( "SYST:COMM:SER:SBIT?", "SYST:COMM:SER:SBIT %g", """ A int property that controls the number of stop-bits used in serial communication, Possible values are: 1 or 2 """, validator=strict_discrete_set, values=[1, 2], ) date = Instrument.measurement( "SYST:DATE?", """ A list property for the date of the RTC in the unit """, ) time = Instrument.measurement( "SYST:TIME?", """ A list property for the time of the RTC in the unit """, ) basic_info = Instrument.measurement( "SYST:INF:BAS?", """ A String property containing information about the hardware modules installed in the unit """, ) subsystem_info = Instrument.measurement( "SYST:INF:SUBS?", """ A String property containing information about the system configuration """, ) scale_volt = Instrument.control( "UNIT:VOLT?", "UNIT:VOLT %s", """ A string property that controls the unit to be used for voltage entries on the unit Possible values are: AV,FV, PV, NV, UV, MV, V, KV, MAV, GV, TV, PEV, EV, DBAV, DBFV, DBPV, DBNV, DBUV, DBMV, DBV, DBKV, DBMAv, DBGV, DBTV, DBPEv, DBEV refer also to chapter 3.6.9 of the manual """, validator=strict_discrete_set, values=["AV", "FV", "PV", "NV", "UV", "MV", "V", "KV", "MAV", "GV", "TV", "PEV", "EV", "DBAV", "DBFV", "DBPV", "DBNV", "DBUV", "DBMV", "DBV", "DBKV", "DBMAv", "DBGV", "DBTV", "DBPEv", "DBEV"], ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4136057 pymeasure-0.14.0/pymeasure/instruments/siglenttechnologies/0000755000175100001770000000000014623331176023677 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/siglenttechnologies/__init__.py0000644000175100001770000000232314623331163026004 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .siglent_spd1168x import SPD1168X from .siglent_spd1305x import SPD1305X ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/siglenttechnologies/siglent_spd1168x.py0000644000175100001770000000330414623331163027270 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.instrument import Instrument from pymeasure.instruments.siglenttechnologies.siglent_spdbase import (SPDSingleChannelBase, SPDChannel) class SPD1168X(SPDSingleChannelBase): """Represent the Siglent SPD1168X Power Supply. """ ch_1 = Instrument.ChannelCreator(SPDChannel, 1) def __init__(self, adapter, name="Siglent Technologies SPD1168X Power Supply", **kwargs): super().__init__( adapter, name, **kwargs ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/siglenttechnologies/siglent_spd1305x.py0000644000175100001770000000377314623331163027273 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.instrument import Instrument from pymeasure.instruments.siglenttechnologies.siglent_spdbase import (SPDSingleChannelBase, SPDChannel) class SPD1305X(SPDSingleChannelBase): """Represent the Siglent SPD1305X Power Supply. """ voltage_range = [0, 30] current_range = [0, 5] ch_1 = Instrument.ChannelCreator(SPDChannel, 1, voltage_range=voltage_range, current_range=current_range) def __init__(self, adapter, name="Siglent Technologies SPD1305X Power Supply", **kwargs): super().__init__( adapter, name, **kwargs ) self.ch_1.voltage_setpoint_values = self.voltage_range self.ch_1.current_limit_values = self.current_range ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/siglenttechnologies/siglent_spdbase.py0000644000175100001770000001747214623331163027426 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.channel import Channel from pymeasure.instruments.validators import (strict_discrete_range, strict_discrete_set, truncated_range ) from enum import IntFlag log = logging.getLogger(__name__) # https://docs.python.org/3/howto/logging.html#library-config log.addHandler(logging.NullHandler()) class SystemStatusCode(IntFlag): """System status enums based on ``IntFlag`` Used in conjunction with :attr:`~.system_status_code`. ====== ====== Value Enum ====== ====== 256 WAVEFORM_DISPLAY 64 TIMER_ENABLED 32 FOUR_WIRE 16 OUTPUT_ENABLED 1 CONSTANT_CURRENT 0 CONSTANT_VOLTAGE ====== ====== """ WAVEFORM_DISPLAY = 256 # bit 8 -- waveform display enabled TIMER_ENABLED = 64 # bit 6 -- timer enabled FOUR_WIRE = 32 # bit 5 -- four-wire mode enabled OUTPUT_ENABLED = 16 # bit 4 -- output enabled CONSTANT_CURRENT = 1 # bit 0 -- constant current mode CONSTANT_VOLTAGE = 0 # bit 0 -- constant voltage mode class SPDChannel(Channel): """ The channel class for Siglent SPDxxxxX instruments. """ def __init__(self, parent, id, voltage_range: list = [0, 16], current_range: list = [0, 8]): super().__init__(parent, id) self.voltage_range = voltage_range self.current_range = current_range voltage = Instrument.measurement( "MEAS:VOLT? CH{ch}", """Measure the channel output voltage. :type: float """ ) current = Instrument.measurement( "MEAS:CURR? CH{ch}", """Measure the channel output current. :type: float """ ) power = Instrument.measurement( "MEAS:POWE? CH{ch}", """Measure the channel output power. :type: float """ ) current_limit = Instrument.control( "CH{ch}:CURR?", "CH{ch}:CURR %g", """Control the output current configuration of the channel. :type : float """, validator=truncated_range, values=[0, 8], dynamic=True ) voltage_setpoint = Instrument.control( "CH{ch}:VOLT?", "CH{ch}:VOLT %g", """Control the output voltage configuration of the channel. :type : float """, validator=truncated_range, values=[0, 16], dynamic=True ) def enable_output(self, enable: bool = True): """Enable the channel output. :type: bool ``True``: enables the output ``False``: disables it """ self.parent.selected_channel = self.id self.write('OUTP CH{ch},' + ("ON" if enable else "OFF")) def enable_timer(self, enable: bool = True): """Enable the channel timer. :type: bool ``True``: enables the timer ``False``: disables it """ self.write('TIME CH{ch},' + ("ON" if enable else "OFF")) def configure_timer(self, step, voltage, current, duration): """Configure the timer step. :param step: int: index of the step to save the configuration :param voltage: float: voltage setpoint of the step :param current: float: current limit of the step :param duration: int: duration of the step in seconds """ step = strict_discrete_range(step, [1, 5], 1) voltage = truncated_range(voltage, self.voltage_range) current = truncated_range(current, self.current_range) duration = truncated_range(duration, [0, 10000]) self.write(f'TIME:SET CH{{ch}},{step:d},{voltage:1.3f},{current:1.3f},{duration:d}') class SPDBase(SCPIUnknownMixin, Instrument): """ The base class for Siglent SPDxxxxX instruments. Uses :class:`SPDChannel` for measurement channels. """ def __init__(self, adapter, name="Siglent SPDxxxxX instrument Base Class", **kwargs): super().__init__( adapter, name, usb=dict(write_termination='\n', read_termination='\n'), tcpip=dict(write_termination='\n', read_termination='\n'), **kwargs ) error = Instrument.measurement( "SYST:ERR?", """Get the error code and information of the instrument. :type: string """ ) fw_version = Instrument.measurement( "SYST:VERS?", """Get the software version of the instrument. :type: string """ ) system_status_code = Instrument.measurement( "SYST:STAT?", """Get the system status register. :type: :class:`.SystemStatusCode` """, get_process=lambda v: SystemStatusCode(int(v, base=16)), ) selected_channel = Instrument.control( "INST?", "INST %s", """Control the selected channel of the instrument. :type : int """, validator=strict_discrete_set, values={1: "CH1"}, # This dynamic property should be updated for multi-channel instruments map_values=True, dynamic=True ) def save_config(self, index): """Save the current config to memory. :param index: int: index of the location to save the configuration """ index = strict_discrete_range(index, [1, 5], 1) self.write(f"*SAV {index:d}") def recall_config(self, index): """Recall a config from memory. :param index: int: index of the location from which to recall the configuration """ index = strict_discrete_range(index, [1, 5], 1) self.write(f"*RCL {index:d}") def enable_local_interface(self, enable: bool = True): """Configure the availability of the local interface. :type: bool ``True``: enables the local interface ``False``: disables it. """ self.write("*UNLOCK" if enable else "*LOCK") def shutdown(self): """ Ensure that the voltage is turned to zero and disable the output. """ for ch in self.channels.values(): ch.voltage_setpoint = 0 ch.enable_output(False) super().shutdown() class SPDSingleChannelBase(SPDBase): def enable_4W_mode(self, enable: bool = True): """Enable 4-wire mode. :type: bool ``True``: enables 4-wire mode ``False``: disables it. """ self.write(f'MODE:SET {"4W" if enable else "2W"}') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4176059 pymeasure-0.14.0/pymeasure/instruments/signalrecovery/0000755000175100001770000000000014623331176022662 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/signalrecovery/__init__.py0000644000175100001770000000227714623331163024777 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from .dsp7225 import DSP7225 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/signalrecovery/dsp7225.py0000644000175100001770000000712714623331163024345 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # # ============================================================================= # Libraries / modules # ============================================================================= from .dsp_base import DSPBase import logging # ============================================================================= # Logging # ============================================================================= log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) # ============================================================================= # Instrument file # ============================================================================= class DSP7225(DSPBase): """Represents the Signal Recovery DSP 7225 lock-in amplifier. Class inherits commands from the DSPBase parent class and utilizes dynamic properties for various properties. .. code-block:: python lockin7225 = DSP7225("GPIB0::12::INSTR") lockin7225.imode = "voltage mode" # Set to measure voltages lockin7225.reference = "internal" # Use internal oscillator lockin7225.fet = 1 # Use FET pre-amp lockin7225.shield = 0 # Ground shields lockin7225.coupling = 0 # AC input coupling lockin7225.time_constant = 0.10 # Filter time set to 100 ms lockin7225.sensitivity = 2E-3 # Sensitivity set to 2 mV lockin7225.frequency = 100 # Set oscillator frequency to 100 Hz lockin7225.voltage = 1 # Set oscillator amplitude to 1 V lockin7225.gain = 20 # Set AC gain to 20 dB print(lockin7225.x) # Measure X channel voltage lockin7225.shutdown() # Instrument shutdown """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Dynamic values - Override base class validator values # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ frequency_values = [0.001, 1.2e5] harmonic_values = [1, 32] curve_buffer_bit_values = [1, 65535] # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Initializer # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def __init__(self, adapter, name="Signal Recovery DSP 7225", **kwargs): super().__init__( adapter, name, **kwargs ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/signalrecovery/dsp7265.py0000644000175100001770000001302214623331163024340 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # # ============================================================================= # Libraries / modules # ============================================================================= from .dsp_base import DSPBase from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_range import logging from time import sleep # ============================================================================= # Logging # ============================================================================= log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) # ============================================================================= # Instrument file # ============================================================================= class DSP7265(DSPBase): """Represents the Signal Recovery DSP 7265 lock-in amplifier. Class inherits commands from the DSPBase parent class and utilizes dynamic properties for various properties and includes additional functionality. .. code-block:: python lockin7265 = DSP7265("GPIB0::12::INSTR") lockin7265.imode = "voltage mode" # Set to measure voltages lockin7265.reference = "internal" # Use internal oscillator lockin7265.fet = 1 # Use FET pre-amp lockin7265.shield = 0 # Ground shields lockin7265.coupling = 0 # AC input coupling lockin7265.time_constant = 0.10 # Filter time set to 100 ms lockin7265.sensitivity = 2E-3 # Sensitivity set to 2 mV lockin7265.frequency = 100 # Set oscillator frequency to 100 Hz lockin7265.voltage = 1 # Set oscillator amplitude to 1 V lockin7265.gain = 20 # Set AC gain to 20 dB print(lockin7265.x) # Measure X channel voltage lockin7265.shutdown() # Instrument shutdown """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Dynamic values - Override base class validator values # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ frequency_values = [0.001, 2.5e5] harmonic_values = [1, 65535] curve_buffer_bit_values = [1, 2097151] # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Constants # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CURVE_BITS = ['x', 'y', 'magnitude', 'phase', 'sensitivity', 'adc1', 'adc2', 'adc3', 'dac1', 'dac2', 'noise', 'ratio', 'log ratio', 'event', 'frequency part 1', 'frequency part 2', # Dual modes 'x2', 'y2', 'magnitude2', 'phase2', 'sensitivity2'] # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Initializer # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def __init__(self, adapter, name="Signal Recovery DSP 7265", **kwargs): super().__init__( adapter, name, **kwargs ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Additional properties # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ dac3 = Instrument.control( "DAC. 3", "DAC. 3 %g", """Control the voltage of the DAC3 output on the rear panel. Valid values are floating point numbers between -12 to 12 V. """, validator=strict_range, values=[-12, 12] ) dac4 = Instrument.control( "DAC. 4", "DAC. 4 %g", """Control the voltage of the DAC4 output on the rear panel. Valid values are floating point numbers between -12 to 12 V. """, validator=strict_range, values=[-12, 12] ) @property def adc3(self): """Measure the ADC3 input voltage.""" # 50,000 for 1V signal over 1 s integral = self.values("ADC 3")[0] return integral / (50000.0 * self.adc3_time) @property def adc3_time(self): """Control the ADC3 sample time in seconds.""" # Returns time in seconds return self.values("ADC3TIME")[0] / 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/signalrecovery/dsp_base.py0000644000175100001770000006507314623331163025023 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # # ============================================================================= # Libraries / modules # ============================================================================= import logging from time import sleep, time import numpy as np from pymeasure.instruments import Instrument from pymeasure.instruments.validators import modular_range_bidirectional from pymeasure.instruments.validators import strict_discrete_set from pymeasure.instruments.validators import strict_range # ============================================================================= # Logging # ============================================================================= log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) # ============================================================================= # Instrument file # ============================================================================= class DSPBase(Instrument): """This is the base class for the Signal Recovery DSP 72XX lock-in amplifiers. Do not directly instantiate an object with this class. Use one of the DSP 72XX series instrument classes that inherit from this parent class. Floating point command mode (i.e., the inclusion of the ``.`` character in commands) is included for usability. Untested commands are noted in docstrings. """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Constants # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SENSITIVITIES = [ np.nan, 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 ] SEN_MULTIPLIER = [1, 1e-6, 1e-8] 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'] IMODES = ['voltage mode', 'current mode', 'low noise current mode'] CURVE_BITS = ['x', 'y', 'magnitude', 'phase', 'sensitivity', 'adc1', 'adc2', 'dac1', 'dac2', 'noise', 'ratio', 'log ratio', 'event', 'frequency part 1', 'frequency part 2', # Dual modes 'x2', 'y2', 'magnitude2', 'phase2', 'sensitivity2'] # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Initializer and important communication methods # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def __init__(self, adapter, name="Signal Recovery DSP 72XX Base", **kwargs): super().__init__( adapter, name, includeSCPI=False, **kwargs ) def read(self, **kwargs): """Read the response and remove extra unicode character from instrument readings.""" return super().read(**kwargs).replace('\x00', '') # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Properties # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ id = Instrument.measurement( "ID", """Measure the model number of the instrument. Returned value is an integer.""", cast=int ) imode = Instrument.control( "IMODE", "IMODE %d", """Control the lock-in amplifier to detect a voltage or current signal. Valid values are ``voltage mode, ``current mode``, or ``low noise current mode``. """, validator=strict_discrete_set, values=IMODES, map_values=True ) slope = Instrument.control( "SLOPE", "SLOPE %d", """Control the low-pass filter roll-off. Valid values are the integers 6, 12, 18, or 24, which represents the slope of the low-pass filter in dB/octave. """, validator=strict_discrete_set, values=[6, 12, 18, 24], map_values=True ) time_constant = Instrument.control( "TC", "TC %d", """Control the filter time constant. Valid values are a strict set of time constants from 10 us to 50,000 s. Returned values are floating point numbers in seconds. """, validator=strict_discrete_set, values=TIME_CONSTANTS, map_values=True ) shield = Instrument.control( "FLOAT", "FLOAT %d", """Control the input connector shield state. Valid values are 0 to have shields grounded or 1 to have the shields floating (i.e., connected to ground via a 1 kOhm resistor). """, validator=strict_discrete_set, values=[0, 1] ) fet = Instrument.control( "FET", "FET %d", """Control the voltage preamplifier transistor type. Valid values are 0 for bipolar or 1 for FET. """, validator=strict_discrete_set, values=[0, 1] ) coupling = Instrument.control( "CP", "CP %d", """Control the input coupling mode. Valid values are 0 for AC coupling mode or 1 for DC coupling mode. """, validator=strict_discrete_set, values=[0, 1] ) voltage = Instrument.control( "OA.", "OA. %g", """Control the oscillator amplitude. Valid values are floating point numbers between 0 to 5 V. """, validator=strict_range, values=[0, 5] ) frequency = Instrument.control( "OF.", "OF. %g", """Control the oscillator frequency. Valid values are floating point numbers representing the frequency in Hz. """, validator=strict_range, values=[0, 2.5e5], dynamic=True ) reference = Instrument.control( "IE", "IE %d", """Control the oscillator reference input mode. Valid values are ``internal``, ``external rear`` or ``external front``. """, validator=strict_discrete_set, values=REFERENCES, map_values=True ) harmonic = Instrument.control( "REFN", "REFN %d", """Control the reference harmonic mode. Valid values are integers. """, validator=strict_range, values=[1, 65535], dynamic=True ) reference_phase = Instrument.control( "REFP.", "REFP. %g", """Control the reference absolute phase angle. Valid values are floating point numbers between 0 - 360 degrees. """, validator=modular_range_bidirectional, values=[0, 360] ) dac1 = Instrument.control( "DAC. 1", "DAC. 1 %g", """Control the voltage of the DAC1 output on the rear panel. Valid values are floating point numbers between -12 to 12 V. """, validator=strict_range, values=[-12, 12] ) dac2 = Instrument.control( "DAC. 2", "DAC. 2 %g", """Control the voltage of the DAC2 output on the rear panel. Valid values are floating point numbers between -12 to 12 V. """, validator=strict_range, values=[-12, 12] ) @property def gain(self): """Control the AC gain of signal channel amplifier.""" return self.values("ACGAIN") @gain.setter def gain(self, value): value = strict_discrete_set(int(value / 10), list(range(0, 10))) self.write("ACGAIN %d" % value) @property def sensitivity(self): """Control the signal's measurement sensitivity range. When in voltage measurement mode, valid values are discrete values from 2 nV to 1 V. When in current measurement mode, valid values are discrete values from 2 fA to 1 µA (for normal current mode) or up to 10 nA (for low noise current mode). """ return self.values("SEN.")[0] @sensitivity.setter def sensitivity(self, value): # get the voltage/current mode: imode = self.IMODES.index(self.imode) # Scale the sensitivities to the correct range for voltage/current mode sensitivities = [s * self.SEN_MULTIPLIER[imode] for s in self.SENSITIVITIES] if imode == 2: sensitivities[0:7] = [np.nan] * 7 # Check and map the value value = strict_discrete_set(value, sensitivities) value = sensitivities.index(value) # Set sensitivity self.write("SEN %d" % value) @property def auto_gain(self): """Control lock-in amplifier for automatic AC gain.""" 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") x = Instrument.measurement( "X.", """Measure the output signal's X channel. Returned value is a floating point number in volts. """ ) y = Instrument.measurement( "Y.", """Measure the output signal's Y channel. Returned value is a floating point number in volts. """ ) xy = Instrument.measurement( "XY.", """Measure both the X and Y channels. Returned values are floating point numbers in volts. """ ) mag = Instrument.measurement( "MAG.", """Measure the magnitude of the signal. Returned value is a floating point number in volts. """ ) phase = Instrument.measurement( "PHA.", """Measure the signal's absolute phase angle. Returned value is a floating point number in degrees. """ ) adc1 = Instrument.measurement( "ADC. 1", """Measure the voltage of the ADC1 input on the rear panel. Returned value is a floating point number in volts. """ ) adc2 = Instrument.measurement( "ADC. 2", """Measure the voltage of the ADC2 input on the rear panel. Returned value is a floating point number in volts. """ ) ratio = Instrument.measurement( "RT.", """Measure the ratio between the X channel and ADC1. Returned value is a unitless floating point number equivalent to the mathematical expression X/ADC1. """ ) log_ratio = Instrument.measurement( "LR.", """ Measure the log (base 10) of the ratio between the X channel and ADC1. Returned value is a unitless floating point number equivalent to the mathematical expression log(X/ADC1). """ ) curve_buffer_bits = Instrument.control( "CBD", "CBD %d", """Control which data outputs are stored in the curve buffer. Valid values are values are integers between 1 and 65,535 (or 2,097,151 in dual reference mode). """, values=[1, 2097151], validator=strict_range, cast=int, dynamic=True ) curve_buffer_length = Instrument.control( "LEN", "LEN %d", """Control the length of the curve buffer. Valid values are integers between 1 and 32,768, but the actual maximum amount of points is determined by the amount of curves that are stored, as set via the curve_buffer_bits property (32,768 / n). """, values=[1, 32768], validator=strict_range, cast=int ) curve_buffer_interval = Instrument.control( "STR", "STR %d", """Control the time interval between the collection of successive points in the curve buffer. Valid values to the time interval are integers in ms with a resolution of 5 ms; input values are rounded up to a multiple of 5. Valid values are values between 0 and 1,000,000,000 (corresponding to 12 days). The interval may be set to 0, which sets the rate of data storage to the curve buffer to 1.25 ms/point (800 Hz). However this only allows storage of the X and Y channel outputs. There is no need to issue a CBD 3 command to set this up since it happens automatically when acquisition starts. """, values=[1, 1000000000], validator=strict_range, cast=int ) curve_buffer_status = Instrument.measurement( "M", """Measure the status of the curve buffer acquisition. Command returns four values: **First value - Curve Acquisition Status:** Number with 5 possibilities: 0: no activity 1: acquisition via TD command running 2: acquisition by a TDC command running 5: acquisition via TD command halted 6: acquisition bia TDC command halted **Second value - Number of Sweeps Acquired**: Number of sweeps already acquired. **Third value - Status Byte:** Decimal representation of the status byte (the same response as the ST command **Fourth value - Number of Points Acquired:** Number of points acquired in the curve buffer. """, cast=int, ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Methods # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def set_voltage_mode(self): """Sets lock-in amplifier to measure a voltage signal.""" self.write("IMODE 0") def setDifferentialMode(self, lineFiltering=True): """Sets lock-in amplifier to differential mode, measuring A-B.""" self.write("VMODE 3") self.write("LF %d 0" % (3 if lineFiltering else 0)) def setChannelAMode(self): """Sets lock-in amplifier to measure a voltage signal only from the A input connector. """ self.write("VMODE 1") def auto_sensitivity(self): """Adjusts the full-scale sensitivity so signal's magnitude lies between 30 - 90 % of full-scale. """ self.write("AS") def auto_phase(self): """Adjusts the reference absolute phase to maximize the X channel output and minimize the Y channel output signals. """ self.write("AQN") def init_curve_buffer(self): """Initializes the curve storage memory and status variables. All record of previously taken curves is removed. """ self.write("NC") def set_buffer(self, points, quantities=None, interval=10.0e-3): """Prepares the curve buffer for a measurement. :param int points: Number of points to be recorded in the curve buffer :param list quantities: List containing the quantities (strings) that are to be recorded in the curve buffer, can be any of: 'x', 'y', 'magnitude', 'phase', 'sensitivity', 'adc1', 'adc2', 'adc3', 'dac1', 'dac2', 'noise', 'ratio', 'log ratio', 'event', 'frequency' (or 'frequency part 1' and 'frequency part 2'); for both dual modes, additional options are: 'x2', 'y2', 'magnitude2', 'phase2', 'sensitivity2'. Default is 'x' and 'y'. :param float interval: The interval between two subsequent points stored in the curve buffer in s. Default is 10 ms. """ if quantities is None: quantities = ["x", "y"] if "frequency" in quantities: quantities.remove("frequency") quantities.extend([ "frequency part 1", "frequency part 2" ]) # remove all possible duplicates quantities = list({q.lower() for q in quantities}) bits = 0 for q in quantities: bits += 2 ** self.CURVE_BITS.index(q) self.curve_buffer_bits = bits self.curve_buffer_length = points self.curve_buffer_interval = int(interval * 1000) self.init_curve_buffer() def start_buffer(self): """Initiates data acquisition. Acquisition starts at the current position in the curve buffer and continues at the rate set by the STR command until the buffer is full. """ self.write("TD") def wait_for_buffer(self, timeout=None, delay=0.1): """ Method that waits until the curve buffer is filled """ start = time() while self.curve_buffer_status[0] == 1: sleep(delay) if timeout is not None and time() < start + timeout: break def get_buffer(self, quantity=None, convert_to_float=True, wait_for_buffer=True): """Retrieves the buffer after it has been filled. The data retrieved from the lock-in is in a fixed-point format, which requires translation before it can be interpreted as meaningful data. When `convert_to_float` is True the conversion is performed (if possible) before returning the data. :param str quantity: If provided, names the quantity that is to be retrieved from the curve buffer; can be any of: 'x', 'y', 'magnitude', 'phase', 'sensitivity', 'adc1', 'adc2', 'adc3', 'dac1', 'dac2', 'noise', 'ratio', 'log ratio', 'event', 'frequency part 1' and 'frequency part 2'; for both dual modes, additional options are: 'x2', 'y2', 'magnitude2', 'phase2', 'sensitivity2'. If no quantity is provided, all available data is retrieved. :param bool convert_to_float: Bool that determines whether to convert the fixed-point buffer-data to meaningful floating point values via the `buffer_to_float` method. If True, this method tries to convert all the available data to meaningful values; if this is not possible, an exception will be raised. If False, this conversion is not performed and the raw buffer-data is returned. :param bool wait_for_buffer: Bool that determines whether to wait for the data acquisition to finished if this method is called before the acquisition is finished. If True, the method waits until the buffer is filled before continuing; if False, the method raises an exception if the acquisition is not finished when the method is called. """ # Check if buffer is finished if self.curve_buffer_status[0] != 0: if wait_for_buffer: self.wait_for_buffer() else: raise RuntimeError("Buffer acquisition is not yet finished.") # Check which quantities are recorded in the buffer bits = format(self.curve_buffer_bits, '021b')[::-1] quantity_enums = [e for e, b in enumerate(bits) if b == "1"] # Check if the provided quantity (if any) is indeed recorded if quantity is not None: if self.CURVE_BITS.index(quantity) in quantity_enums: quantity_enums = [self.CURVE_BITS.index(quantity)] else: raise KeyError("The selected quantity '%s' is not recorded;" "quantity should be one of: %s" % ( quantity, ", ".join( [self.CURVE_BITS[q] for q in quantity_enums] ))) # Retrieve the data data = {} for enum in quantity_enums: self.write("DC %d" % enum) q_data = [] while True: stb = format(self.adapter.connection.read_stb(), '08b')[::-1] if bool(int(stb[2])): raise ValueError("Status byte reports command parameter error.") if bool(int(stb[0])): break if bool(int(stb[7])): q_data.append(int(self.read().strip())) data[self.CURVE_BITS[enum]] = np.array(q_data) if convert_to_float: data = self.buffer_to_float(data) if quantity is not None: data = data[quantity] return data def buffer_to_float(self, buffer_data, sensitivity=None, sensitivity2=None, raise_error=True): """Converts fixed-point buffer data to floating point data. The provided data is converted as much as possible, but there are some requirements to the data if all provided columns are to be converted; if a key in the provided data cannot be converted it will be omitted in the returned data or an exception will be raised, depending on the value of raise_error. The requirements for converting the data are as follows: - Converting X, Y, magnitude and noise requires sensitivity data, which can either be part of the provided data or can be provided via the sensitivity argument - The same holds for X2, Y2 and magnitude2 with sensitivity2. - Converting the frequency requires both 'frequency part 1' and 'frequency part 2'. :param dict buffer_data: The data to be converted. Must be in the format as returned by the `get_buffer` method: a dict of numpy arrays. :param sensitivity: If provided, the sensitivity used to convert X, Y, magnitude and noise. Can be provided as a float or as an array that matches the length of elements in `buffer_data`. If both a sensitivity is provided and present in the buffer_data, the provided value is used for the conversion, but the sensitivity in the buffer_data is stored in the returned dict. :param sensitivity2: Same as the first sensitivity argument, but for X2, Y2, magnitude2 and noise2. :param bool raise_error: Determines whether an exception is raised in case not all keys provided in buffer_data can be converted. If False, the columns that cannot be converted are omitted in the returned dict. :return: Floating-point buffer data :rtype: dict """ data = {} def maybe_raise(message): if raise_error: raise ValueError(message) def convert_if_present(keys, multiply_by=1): """Copy any available entries from buffer_data to data, scale with multiply_by. """ for key in keys: if key in buffer_data: data[key] = buffer_data[key] * multiply_by # Sensitivity (for both single and dual modes) for key in ["sensitivity", "sensitivity2"]: if key in buffer_data: data[key] = np.array([ self.SENSITIVITIES[v % 32] * self.SEN_MULTIPLIER[v // 32] for v in buffer_data[key] ]) # Try to set sensitivity values from arg or data sensitivity = sensitivity or data.get('sensitivity', None) sensitivity2 = sensitivity2 or data.get('sensitivity2', None) if any(["x" in buffer_data, "y" in buffer_data, "magnitude" in buffer_data, "noise" in buffer_data, ]): if sensitivity is None: maybe_raise("X, Y, magnitude and noise cannot be converted as " "no sensitivity is provided, neither as argument " "nor as part of the buffer_data. ") else: convert_if_present(["x", "y", "magnitude", "noise"], sensitivity / 10000) # phase data (for both single and dual modes) convert_if_present(["phase", "phase2"], 1 / 100) # frequency data from frequency part 1 and 2 if "frequency part 1" in buffer_data or "frequency part 2" in buffer_data: if "frequency part 1" in buffer_data and "frequency part 2" in buffer_data: data["frequency"] = np.array([ int(format(v2, "016b") + format(v1, "016b"), 2) / 1000 for v1, v2 in zip(buffer_data["frequency part 1"], buffer_data["frequency part 2"]) ]) else: maybe_raise("Can calculate the frequency only when both" "frequency part 1 and 2 are provided.") # conversion for, adc1, adc2, dac1, dac2, ratio, and log ratio convert_if_present(["adc1", "adc2", "dac1", "dac2", "ratio", "log ratio"], 1 / 1000) # adc3 (integrating converter); requires a call to adc3_time if "adc3" in buffer_data: data["adc3"] = buffer_data["adc3"] / (50000 * self.adc3_time) # event does not require a conversion convert_if_present(["event"]) # X, Y, and magnitude data for both dual modes if any(["x2" in buffer_data, "y2" in buffer_data, "magnitude2" in buffer_data, ]): if sensitivity2 is None: maybe_raise("X2, Y2 and magnitude2 cannot be converted as no " "sensitivity2 is provided, neither as argument nor " "as part of the buffer_data. ") else: convert_if_present(["x2", "y2", "magnitude2"], sensitivity2 / 10000) return data def shutdown(self): """Safely shutdown the lock-in amplifier. Sets oscillator amplitude to 0 V and AC gain to 0 dB. """ log.info("Shutting down %s." % self.name) self.voltage = 0. self.gain = 0. super().shutdown() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4176059 pymeasure-0.14.0/pymeasure/instruments/srs/0000755000175100001770000000000014623331176020435 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/srs/__init__.py0000644000175100001770000000240214623331163022540 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from .sr570 import SR570 from .sr510 import SR510 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/srs/sg380.py0000644000175100001770000000743714623331163021662 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range class SG380(SCPIUnknownMixin, 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, adapter, name="Stanford Research Systems SG380 RF Signal Generator", **kwargs): super().__init__( adapter, name, **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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/srs/sr510.py0000644000175100001770000000701014623331163021653 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, truncated_discrete_set class SR510(Instrument): TIME_CONSTANTS = {1e-3: 1, 3e-3: 2, 10e-3: 3, 30e-3: 4, 100e-3: 5, 300e-3: 6, 1: 7, 3: 8, 10: 9, 30: 10, 100: 11, } SENSITIVITIES = {10e-9: 1, 20e-9: 2, 50e-9: 3, 100e-9: 4, 200e-9: 5, 500e-9: 6, 1e-6: 7, 2e-6: 8, 5e-6: 9, 10e-6: 10, 20e-6: 11, 50e-6: 12, 100e-6: 13, 200e-6: 14, 500e-6: 15, 1e-3: 16, 2e-3: 17, 5e-3: 18, 10e-3: 19, 20e-3: 20, 50e-3: 21, 100e-3: 22, 200e-3: 23, 500e-3: 24, } phase = Instrument.control( "P", "P %g", """A float property that represents the SR510 reference to input phase offset in degrees. Queries return values between -180 and 180 degrees. This property can be set with a range of values between -999 to 999 degrees. Set values are mapped internal in the lockin to -180 and 180 degrees.""", validator=truncated_range, values=[-999, 999], ) time_constant = Instrument.control( "T1", "T1,%d", """A float property that represents the SR510 PRE filter time constant. This property can be set.""", validator=truncated_discrete_set, values=TIME_CONSTANTS, map_values=True, ) sensitivity = Instrument.control( "G", "G%d", """A float property that represents the SR510 sensitivity value. This property can be set.""", validator=truncated_discrete_set, values=SENSITIVITIES, map_values=True, ) frequency = Instrument.measurement( "F", """A float property representing the SR510 input reference frequency""", ) status = Instrument.measurement( "Y", """A string property representing the bits set within the SR510 status byte""", get_process=lambda s: bin(int(s))[2:], ) output = Instrument.measurement( "Q", """A float property that represents the SR510 output voltage in Volts.""", ) def __init__(self, adapter, name="Stanford Research Systems SR510 Lock-in amplifier", **kwargs): kwargs.setdefault('write_termination', '\r') super().__init__( adapter, name, includeSCPI=False, **kwargs, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/srs/sr570.py0000644000175100001770000001661614623331163021675 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, \ truncated_discrete_set, truncated_range class SR570(SCPIUnknownMixin, Instrument): def __init__(self, adapter, name="Stanford Research Systems SR570 Lock-in amplifier", **kwargs): super().__init__( adapter, name, **kwargs ) SENSITIVITIES = [ 1e-12, 2e-12, 5e-12, 10e-12, 20e-12, 50e-12, 100e-12, 200e-12, 500e-12, 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 ] FREQUENCIES = [ 0.03, 0.1, 0.3, 1, 3, 10, 30, 100, 300, 1e3, 3e3, 1e4, 3e4, 1e5, 3e5, 1e6 ] FILT_TYPES = ['6dB Highpass', '12dB Highpass', '6dB Bandpass', '6dB Lowpass', '12dB Lowpass', 'none'] BIAS_LIMITS = [-5, 5] OFFSET_CURRENTS = [ 1e-12, 2e-12, 5e-12, 10e-12, 20e-12, 50e-12, 100e-12, 200e-12, 500e-12, 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 ] GAIN_MODES = [ 'Low Noise', 'High Bandwidth', 'Low Drift' ] sensitivity = Instrument.setting( "SENS %d", """ A floating point value that sets the sensitivity of the amplifier, which takes discrete values in a 1-2-5 sequence. Values are truncated to the closest allowed value if not exact. Allowed values range from 1 pA/V to 1 mA/V.""", validator=truncated_discrete_set, values=SENSITIVITIES, map_values=True) filter_type = Instrument.setting( "FLTT %d", """ A string that sets the filter type. Allowed values are: {}""".format(FILT_TYPES), validator=truncated_discrete_set, values=FILT_TYPES, map_values=True) low_freq = Instrument.setting( "LFRQ %d", """ A floating point value that sets the lowpass frequency of the amplifier, which takes a discrete value in a 1-3 sequence. Values are truncated to the closest allowed value if not exact. Allowed values range from 0.03 Hz to 1 MHz.""", validator=truncated_discrete_set, values=FREQUENCIES, map_values=True) high_freq = Instrument.setting( "HFRQ %d", """ A floating point value that sets the highpass frequency of the amplifier, which takes a discrete value in a 1-3 sequence. Values are truncated to the closest allowed value if not exact. Allowed values range from 0.03 Hz to 1 MHz.""", validator=truncated_discrete_set, values=FREQUENCIES, map_values=True) bias_level = Instrument.setting( "BSLV %g", """ A floating point value in V that sets the bias voltage level of the amplifier, in the [-5V,+5V] limits. The values are up to 1 mV precision level.""", validator=truncated_range, values=BIAS_LIMITS, set_process=lambda v: int(1000 * v)) offset_current = Instrument.setting( "BSLV %f", """ A floating point value in A that sets the absolute value of the offset current of the amplifier, in the [1pA,5mA] limits. The offset current takes discrete values in a 1-2-5 sequence. Values are truncated to the closest allowed value if not exact. """, validator=truncated_discrete_set, values=OFFSET_CURRENTS, map_values=True) offset_current_sign = Instrument.setting( "IOSN %d", """ An string that sets the offset current sign. Allowed values are: 'positive' and 'negative'. """, validator=strict_discrete_set, values={'positive': 1, 'negative': 0}, map_values=True) gain_mode = Instrument.setting( "GNMD %d", """ A string that sets the gain mode. Allowed values are: {}""".format(GAIN_MODES), validator=truncated_discrete_set, values=GAIN_MODES, map_values=True) invert_signal_sign = Instrument.setting( "INVT %d", """ An boolean sets the signal invert sense. Allowed values are: True (inverted) and False (not inverted). """, validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True) bias_enabled = Instrument.setting( "BSON %d", """ Boolean that turns the bias on or off. Allowed values are: True (bias on) and False (bias off)""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True) offset_current_enabled = Instrument.setting( "IOON %d", """ Boolean that turns the offset current on or off. Allowed values are: True (current on) and False (current off).""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True) front_blanked = Instrument.setting( "BLNK %d", """ Boolean that blanks(True) or un-blanks (False) the front panel""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True) signal_inverted = Instrument.setting( "INVT %d", """ Boolean that inverts the signal if True""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True) #################### # Methods # #################### def enable_bias(self): """Turns the bias voltage on""" self.bias_enabled = True def disable_bias(self): """Turns the bias voltage off""" self.bias_enabled = False def enable_offset_current(self): """"Enables the offset current """ self.offset_current_enabled = True def disable_offset_current(self): """"Disables the offset current """ self.offset_current_enabled = False def clear_overload(self): """"Reset the filter capacitors to clear an overload condition""" self.write("ROLD") def blank_front(self): """"Blanks the frontend output of the device""" self.front_blanked = True def unblank_front(self): """Un-blanks the frontend output of the device""" self.front_blanked = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/srs/sr830.py0000644000175100001770000005665414623331163021702 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 import numpy as np from enum import IntFlag from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_discrete_set, \ truncated_discrete_set, truncated_range, discreteTruncate class LIAStatus(IntFlag): """ IntFlag type that is returned by the lia_status property. """ NO_ERROR = 0 INPUT_OVERLOAD = 1 FILTER_OVERLOAD = 2 OUTPUT_OVERLOAD = 4 REF_UNLOCK = 8 FREQ_RANGE_CHANGE = 16 TC_CHANGE = 32 TRIGGER = 64 UNUSED = 128 class ERRStatus(IntFlag): """ IntFlag type that is returned by the err_status property. """ NO_ERROR = 0 BACKUP_ERR = 2 RAM_ERR = 4 ROM_ERR = 16 GPIB_ERR = 32 DSP_ERR = 64 MATH_ERR = 128 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'] SNAP_ENUMERATION = {"x": 1, "y": 2, "r": 3, "theta": 4, "aux in 1": 5, "aux in 2": 6, "aux in 3": 7, "aux in 4": 8, "frequency": 9, "ch1": 10, "ch2": 11} REFERENCE_SOURCE_TRIGGER = ['SINE', 'POS EDGE', 'NEG EDGE'] INPUT_FILTER = ['Off', 'On'] status = Instrument.measurement( "*STB?", """Get the status byte and Master Summary Status bit.""", cast=str, ) id = Instrument.measurement( "*IDN?", """Get the identification of the instrument.""", cast=str, maxsplit=0, ) def clear(self): """Clear the instrument status byte.""" self.write("*CLS") def reset(self): """Reset the instrument.""" self.write("*RST") 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. """ ) lia_status = Instrument.measurement( "LIAS?", """ Reads the value of the lockin amplifier (LIA) status byte. Returns a binary string with positions within the string corresponding to different status flags: +----+--------------------------------------+ |Bit | Status | +====+======================================+ | 0 | Input/Amplifier overload | +----+--------------------------------------+ | 1 | Time constant filter overload | +----+--------------------------------------+ | 2 | Output overload | +----+--------------------------------------+ | 3 | Reference unlock | +----+--------------------------------------+ | 4 | Detection frequency range switched | +----+--------------------------------------+ | 5 | Time constant changed indirectly | +----+--------------------------------------+ | 6 | Data storage triggered | +----+--------------------------------------+ | 7 | unused | +----+--------------------------------------+ """, get_process=lambda s: LIAStatus(int(s)), ) err_status = Instrument.measurement( "ERRS?", """Reads the value of the lockin error (ERR) status byte. Returns an IntFlag type with positions within the string corresponding to different error flags: +----+--------------------------------------+ |Bit | Status | +====+======================================+ | 0 | unused | +----+--------------------------------------+ | 1 | backup error | +----+--------------------------------------+ | 2 | RAM error | +----+--------------------------------------+ | 3 | unused | +----+--------------------------------------+ | 4 | ROM error | +----+--------------------------------------+ | 5 | GPIB error | +----+--------------------------------------+ | 6 | DSP error | +----+--------------------------------------+ | 7 | DSP error | +----+--------------------------------------+ """, get_process=lambda s: ERRStatus(int(s)), ) @property def xy(self): """ Reads the X and Y values in Volts. """ return self.snap() 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 ) filter_synchronous = Instrument.control( "SYNC?", "SYNC %d", """A boolean property that controls the synchronous filter. This property can be set. Allowed values are: True or False """, validator=strict_discrete_set, values={True: 1, False: 0}, 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 ) reference_source_trigger = Instrument.control( "RSLP?", "RSLP %d", """ A string property that controls the reference source triggering. Allowed values are: {}""".format(REFERENCE_SOURCE_TRIGGER), validator=strict_discrete_set, values=REFERENCE_SOURCE_TRIGGER, 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, adapter, name="Stanford Research Systems SR830 Lock-in amplifier", **kwargs): super().__init__( adapter, name, includeSCPI=False, **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 percent and the expansion 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 percent (-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, _ = self.get_scaling(channel) sensitivity = self.sensitivity return lambda x: x + offset / 100 * 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: int, has_aborted=lambda: False, delay=0.001): """ Fill two numpy arrays with the content of the instrument buffer Eventually waiting until the specified number of recording is done """ 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.get_buffer(1, index, currentCount) ch2[index:currentCount] = self.get_buffer(2, index, currentCount) index = currentCount time.sleep(delay) currentCount = self.buffer_count if has_aborted(): self.pause_buffer() return ch1, ch2 self.pause_buffer() ch1[index: count + 1] = self.get_buffer(1, index, count) # noqa: E203 ch2[index: count + 1] = self.get_buffer(2, index, count) # noqa: E203 return ch1, ch2 def buffer_measure(self, count, stopRequest=None, delay=1e-3): """ Start a fast measurement mode and transfers data from buffer to extract mean and std measurements Return the mean and std from both channels """ self.write("FAST2;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.get_buffer(1, index, currentCount) ch2[index:currentCount] = self.get_buffer(2, index, currentCount) index = currentCount time.sleep(delay) currentCount = self.buffer_count if stopRequest is not None and stopRequest.isSet(): self.pause_buffer() return (0, 0, 0, 0) self.pause_buffer() ch1[index:count] = self.get_buffer(1, index, count) ch2[index:count] = self.get_buffer(2, index, count) return (ch1.mean(), ch1.std(), ch2.mean(), ch2.std()) def pause_buffer(self): self.write("PAUS") def start_buffer(self, fast=True): if fast: self.write("FAST2;STRD") else: self.write("FAST0") 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.pause_buffer() def get_buffer(self, channel=1, start=0, end=None): """ Acquires 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") def snap(self, val1="X", val2="Y", *vals): """ Method that records and retrieves 2 to 6 parameters at a single instant. The parameters can be one of: X, Y, R, Theta, Aux In 1, Aux In 2, Aux In 3, Aux In 4, Frequency, CH1, CH2. Default is "X" and "Y". :param val1: first parameter to retrieve :param val2: second parameter to retrieve :param vals: other parameters to retrieve (optional) """ if len(vals) > 4: raise ValueError("No more that 6 values (in total) can be captured" "simultaneously.") # check if additional parameters are given as a list if len(vals) == 1 and isinstance(vals[0], (list, tuple)): vals = vals[0] # make a list of all vals vals = [val1, val2] + list(vals) vals_idx = [str(self.SNAP_ENUMERATION[val.lower()]) for val in vals] command = "SNAP? " + ",".join(vals_idx) return self.values(command) def save_setup(self, setup_number: int): """Save the current instrument configuration (all parameters) in a memory referred to by an integer :param setup_number: the integer referring to the memory (between 1 and 9 (included)) """ if 1 <= setup_number <= 9: self.write(f'SSET{setup_number:d};') def load_setup(self, setup_number: int): """ Load a previously saved instrument configuration from the memory referred to by an integer :param setup_number: the integer referring to the memory (between 1 and 9 (included)) """ if 1 <= setup_number <= 9: self.write(f'RSET{setup_number:d};') def start_scan(self): """ Start the data recording into the buffer """ self.write('STRT') def pause_scan(self): """ Pause the data recording """ self.write('PAUS') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/srs/sr860.py0000644000175100001770000005724414623331163021701 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin class SR860(SCPIUnknownMixin, 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", """Floating point property representing the preset sine out amplitude, for the A1 preset button. This property can be set.""", # noqa: E501 validator=truncated_range, values=[1e-9, 2] ) sine_amplitudepreset2 = Instrument.control( "PSTA? 1", "PSTA1, %0.9e", """Floating point property representing the preset sine out amplitude, for the A2 preset button. This property can be set.""", # noqa: E501 validator=truncated_range, values=[1e-9, 2] ) sine_amplitudepreset3 = Instrument.control( "PSTA? 2", "PSTA2, %0.9e", """Floating point property representing the preset sine out amplitude, for the A3 preset button. This property can be set.""", # noqa: E501 validator=truncated_range, values=[1e-9, 2] ) sine_amplitudepreset4 = Instrument.control( "PSTA? 3", "PSTA 3, %0.9e", """Floating point property representing the preset sine out amplitude, for the A3 preset button. This property can be set.""", # noqa: E501 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 0, %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 1, %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 2, %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 3, %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: +--------+-------------+------------------------+ | index | enumeration | parameter | +========+=============+========================+ | 0 | X | X output | +--------+-------------+------------------------+ | 1 | Y | Y output | +--------+-------------+------------------------+ | 2 | R | R output | +--------+-------------+------------------------+ | 3 | THeta | θ output | +--------+-------------+------------------------+ | 4 | IN1 | Aux In1 | +--------+-------------+------------------------+ | 5 | IN2 | Aux In2 | +--------+-------------+------------------------+ | 6 | IN3 | Aux In3 | +--------+-------------+------------------------+ | 7 | IN4 | Aux In4 | +--------+-------------+------------------------+ | 8 | XNOise | Xnoise | +--------+-------------+------------------------+ | 9 | YNOise | Ynoise | +--------+-------------+------------------------+ | 10 | OUT1 | Aux Out1 | +--------+-------------+------------------------+ | 11 | OUT2 | Aux Out2 | +--------+-------------+------------------------+ | 12 | PHAse | Reference Phase | +--------+-------------+------------------------+ | 13 | SAMp | Sine Out Amplitude | +--------+-------------+------------------------+ | 14 | LEVel | DC Level | +--------+-------------+------------------------+ | 15 | FInt | Int. Ref. Frequency | +--------+-------------+------------------------+ | 16 | FExt | Ext. Ref. Frequency | +--------+-------------+------------------------+ :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.values( command=f"SNAP? {val1}, {val2}", separator=",", cast=float, ) else: return self.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). """, # noqa: E501 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). """, # noqa: E501 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). """, # noqa: E501 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). """, # noqa: E501 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 for 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, adapter, name="Stanford Research Systems SR860 Lock-in amplifier", **kwargs): super().__init__( adapter, name, **kwargs ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4176059 pymeasure-0.14.0/pymeasure/instruments/tcpowerconversion/0000755000175100001770000000000014623331176023417 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/tcpowerconversion/__init__.py0000644000175100001770000000223414623331163025525 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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 .tccxn import CXN ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/tcpowerconversion/tccxn.py0000644000175100001770000003761214623331163025115 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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 enum import struct from pymeasure.instruments.validators import strict_discrete_set from pymeasure.instruments import Channel, Instrument def values(self, command, cast=int, separator=',', preprocess_reply=None, **kwargs): """Write a command to the instrument and return a list of formatted values from the result. This is derived from CommonBase.values and adapted here for use with bytes communication messages (no str conversion and strip). It is implemented as a general method to allow using it equally in PresetChannel and CXN. See Github issue #784 for details. :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. :returns: A list of the desired type, or strings where the casting fails """ results = self.ask(command) if callable(preprocess_reply): results = preprocess_reply(results) 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 bytes return results def int2char(value): """Convert an 16-bit unsigned integer to a tuple of two representing characters.""" return tuple(int(b) for b in value.to_bytes(2, "big")) class PresetChannel(Channel): values = values load_capacity = Instrument.control( "GU\x00{ch:c}\x00\x00", "TD{ch:c}\x01\x00%c", """Control the percentage of full-scale value of the load capacity preset.""", preprocess_reply=lambda d: struct.unpack(">H", d[2:4]), validator=strict_discrete_set, values=range(101), ) tune_capacity = Instrument.control( "GU\x00{ch:c}\x00\x00", "TD{ch:c}\x02\x00%c", """Control the percentage of full-scale value of the tune capacity preset.""", preprocess_reply=lambda d: struct.unpack(">H", d[4:6]), validator=strict_discrete_set, values=range(101), ) class CXN(Instrument): """T&C Power Conversion AG Series Plasma Generator CXN (also rebranded by AJA International Inc as 0113 GTC or 0313 GTC) Connection to the device is made through an RS232 serial connection. The communication settings are fixed in the device at 38400, stopbit one, parity none. The device uses a command response system where every receipt of a command is acknowledged by returning a '*'. A '?' is returned to indicates the command was not recognized by the device. A command messages always consists of the following bytes (B): 1B - header (always 'C'), 1B - address (ignored), 2B - command id, 2B - parameter 1, 2B - parameter, 2B - checksum A response message always consists of: 1B - header (always 'R'), 1B - address of the device, 2B - length of the data package, variable length data, 2B - checksum response messages are received after the acknowledge byte. :param adapter: pyvisa resource name of the instrument or adapter instance :param string name: Name of the instrument. :param kwargs: Any valid key-word argument for Instrument .. Note:: In order to enable setting any parameters one has to request control and periodically (at least once per 2s) poll any value from the device. Failure to do so will mean loss of control and the device will reset certain parameters (setpoint, disable RF, ...). If no value should be polled but control should remain active one can also use the ping method. """ # use predefined values method to allow reusing in the Channels values = values preset_1 = Instrument.ChannelCreator(PresetChannel, 1) preset_2 = Instrument.ChannelCreator(PresetChannel, 2) preset_3 = Instrument.ChannelCreator(PresetChannel, 3) preset_4 = Instrument.ChannelCreator(PresetChannel, 4) preset_5 = Instrument.ChannelCreator(PresetChannel, 5) preset_6 = Instrument.ChannelCreator(PresetChannel, 6) preset_7 = Instrument.ChannelCreator(PresetChannel, 7) preset_8 = Instrument.ChannelCreator(PresetChannel, 8) preset_9 = Instrument.ChannelCreator(PresetChannel, 9) def __init__(self, adapter, name="T&C RF sputtering power supply", address=0, **kwargs): self.address = address super().__init__(adapter, name, includeSCPI=False, write_termination="", read_termination="", asrl=dict(baud_rate=38400), **kwargs) @staticmethod def _checksum(msg): """Calculate a 2 bytes checksum calculated by a bytewise sum of the message. :param bytes msg: message content :returns: calculated checksum :rtype: bytes """ return struct.pack(">H", sum(msg)) def _prepend_cmdheader(self, cmd): """Prepends command start byte and address to the command. :param bytes msg: command message """ return b"C" + self.address.to_bytes(1, "big") + cmd def _check_acknowledgment(self): """Check reply string for acknowledgement byte. :raises ValueError: if an invalid an invalid byte is read from the instrument """ ret = super().read_bytes(1) if ret == b"*": return # no valid acknowledgement message found raise ValueError( f"invalid reply '{ret}' found in acknowledgement check") def read(self): """Reads a response message from the instrument. This method determines the length of the message from the automatically by reading the message header and also checks for a correct checksum. :returns: the data fields :rtype: bytes :raises ValueError: if a checksum error is detected """ header = super().read_bytes(4) # check valid header if header[0] != 82: raise ValueError(f"invalid header start byte '{header[0]}' received") if header[1] != self.address: raise ValueError(f"invalid address byte '{header[1]}' received; " f"should be {self.address.to_bytes(1, 'big')}") datalength = int.from_bytes(header[2:], "big") data = super().read_bytes(datalength) chksum = super().read_bytes(2) if chksum == self._checksum(header + data): return data else: raise ValueError( f"checksum error in received message {header + data} " f"with checksum {self._checksum(header + data)} " f"but received {chksum}") def write(self, command): """Writes a command to the instrument and includes needed required header and address. :param str command: command to be sent to the instrument """ fullcmd = self._prepend_cmdheader(command.encode()) super().write_bytes(fullcmd + self._checksum(fullcmd)) self._check_acknowledgment() class Status(enum.IntFlag): """IntFlag type used to represent the CXN status. The used bits correspond to: bit 14: Analog interface enabled, bit 11: Interlock open, bit 10: Over temperature, bit 9: Reverse power limit, bit 8: Forward power limit, bit 6: MCG mode active, bit 5: load power leveling active, bit 4, External RF source active, bit 0: RF power on. """ RF_ENABLED = 1 EXTERNAL_RFSOURCE = 16 LOAD_POWER_LEVELING = 32 MCG_MODE = 64 FORWARD_POWER_LIMIT = 256 REVERSE_POWER_LIMIT = 512 OVER_TEMPERATURE = 1024 INTERLOCK_OPEN = 2048 ANALOG_INTERFACE = 16384 id = Instrument.measurement( "Gi\x00\x01\x00\x00", """Get the device identification string.""", cast=str, get_process=lambda d: d.decode()[2:-1].strip(), ) serial = Instrument.measurement( "Gi\x00\x02\x00\x00", """Get the serial number of the instrument.""", cast=str, get_process=lambda d: d.decode()[2:-1].strip(), ) firmware_version = Instrument.measurement( "Gf\x00\x00\x00\x00", """Get the UI-processor and RF-processor firmware version numbers.""", preprocess_reply=lambda d: struct.unpack("BBBB", d), get_process=lambda v: str.format("UI {}.{}, RF {}.{}", *v) ) pulse_params = Instrument.measurement( "GE\x00\x00\x00\x00", """Get pulse on/off time of the pulse waveform.""", preprocess_reply=lambda d: struct.unpack(">HH", d), ) frequency = Instrument.measurement( "GF\x00\x00\x00\x00", """Get operating frequency in Hz.""", preprocess_reply=lambda d: struct.unpack(">L", d), ) power = Instrument.measurement( "GP\x00\x00\x00\x00", """Get power readings for forward/reverse/load power in watts.""", preprocess_reply=lambda d: struct.unpack(">HHH", d), get_process=lambda d: (float(d[0]) / 10, float(d[1]) / 10, float(d[2]) / 10), ) status = Instrument.measurement( "GS\x00\x00\x00\x00", """Get status field. The return value is represented by the IntFlag type Status.""", preprocess_reply=lambda d: struct.unpack(">H", d[:2]), get_process=lambda d: CXN.Status(d), ) temperature = Instrument.measurement( "GS\x00\x00\x00\x00", """Get heat sink temperature in deg Celsius.""", preprocess_reply=lambda d: struct.unpack(">H", d[2:4]), get_process=lambda d: float(d) / 10, ) tuner = Instrument.measurement( "GS\x00\x00\x00\x00", """Get type of the used tuner.""", preprocess_reply=lambda d: struct.unpack(">H", d[6:]), values={"none": 1, "AFT generator": 2, "analog tuner": 3, "digital tuner": 4}, map_values=True, ) power_limit = Instrument.measurement( "Gp\x00\x00\x00\x00", """Get maximum power of the power supply.""", preprocess_reply=lambda d: struct.unpack(">H", d[2:4]), get_process=lambda d: float(d) / 10, ) reverse_power_limit = Instrument.measurement( "Gp\x00\x00\x00\x00", """Get maximum reverse power.""", preprocess_reply=lambda d: struct.unpack(">H", d[18:20]), get_process=lambda d: float(d) / 10, ) dc_voltage = Instrument.measurement( "GT\x00\x00\x00\x00", """Get the DC voltage in volts.""", preprocess_reply=lambda d: struct.unpack(">H", d[6:8]), ) operation_mode = Instrument.control( "GS\x00\x00\x00\x00", "SO\x00%c\x00\x00", """Control the operation mode.""", preprocess_reply=lambda d: struct.unpack(">H", d[4:6]), values={"normal": 1, "": 2, "pulse": 3, "ramp": 4}, map_values=True, ) setpoint = Instrument.control( "GL\x00\x00\x00\x00", "SA%c%c\x00\x00", """Control the setpoint power level in watts.""", preprocess_reply=lambda d: struct.unpack(">H", d), get_process=lambda d: float(d) / 10, set_process=int2char, validator=strict_discrete_set, values=range(4001), ) ramp_start_power = Instrument.control( "GR\x00\x00\x00\x00", "RP%c%c\x00\x00", """Control the ramp starting power in watts.""", preprocess_reply=lambda d: struct.unpack(">H", d[:2]), set_process=int2char, validator=strict_discrete_set, values=range(1, 4001), ) ramp_rate = Instrument.control( "GR\x00\x00\x00\x00", "RR%c%c\x00\x00", """Control the ramp rate in watts/second.""", preprocess_reply=lambda d: struct.unpack(">H", d[2:]), set_process=int2char, validator=strict_discrete_set, values=range(1, 99), ) manual_mode = Instrument.control( "GT\x00\x00\x00\x00", "TM\x00%c\x00\x00", """Control the manual tuner mode.""", preprocess_reply=lambda d: struct.unpack(">H", d[:2]), get_process=lambda v: bool(v & 1), set_process=lambda v: 2 if v else 1, validator=strict_discrete_set, values=(True, False), ) load_capacity = Instrument.control( "GT\x00\x00\x00\x00", "TC\x00\x01\x00%c", """Control the percentage of full-scale value of the load capacity. It can be set only when manual_mode is True.""", preprocess_reply=lambda d: struct.unpack(">H", d[2:4]), get_process=lambda d: float(d) / 10, validator=strict_discrete_set, values=range(101), ) tune_capacity = Instrument.control( "GT\x00\x00\x00\x00", "TC\x00\x02\x00%c", """Control the percentage of full-scale value of the tune capacity. It can be set only when manual_mode is True.""", preprocess_reply=lambda d: struct.unpack(">H", d[4:6]), get_process=lambda d: float(d) / 10, validator=strict_discrete_set, values=range(101), ) preset_slot = Instrument.control( "GT\x00\x00\x00\x00", "TP\x00%c\x00\x00", """Control which preset slot will be used for auto-tune mode. Valid values are 0 to 9. 0 means no preset will be used""", preprocess_reply=lambda d: struct.unpack(">H", d[8:10]), validator=strict_discrete_set, values=range(10), ) rf_enabled = Instrument.control( "GS\x00\x00\x00\x00", "BR%c%c\x00\x00", """Control the RF output.""", preprocess_reply=lambda d: struct.unpack(">H", d[:2]), get_process=lambda v: bool(v & 1), set_process=lambda v: (85, 85) if v else (0, 0), validator=strict_discrete_set, values=(True, False), ) def request_control(self): """Request control of the instrument. This is required to be able to set any properties. """ self.write("BC\x55\x55\x00\x00") status = int(struct.unpack(">H", self.read())[0]) if status != 1: print("error(CXN): control request denied!") def release_control(self): """Release instrument control. This will reset certain properties to safe defaults and disable the RF output. """ self.write("BC\x00\x00\x00\x00") status = int(struct.unpack(">H", self.read())[0]) if status != 0: print("error(CXN): release of control unsuccessful!") def ping(self): """Send a ping to the instrument.""" self.write("BP\x00\x00\x00\x00") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4176059 pymeasure-0.14.0/pymeasure/instruments/tdk/0000755000175100001770000000000014623331176020410 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/tdk/__init__.py0000644000175100001770000000232314623331163022515 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .tdk_gen40_38 import TDK_Gen40_38 from .tdk_gen80_65 import TDK_Gen80_65 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/tdk/tdk_base.py0000644000175100001770000003102314623331163022531 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # # ============================================================================= # Libraries / modules # ============================================================================= from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_range from pymeasure.instruments.validators import strict_discrete_set from pymeasure.instruments.validators import strict_discrete_range import logging from time import sleep import numpy as np # ============================================================================= # Logging # ============================================================================= log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) # ============================================================================= # Instrument file # ============================================================================= class TDK_Lambda_Base(Instrument): """ This is the base class for TDK Lambda Genesys Series DC power supplies. Do not directly instantiate an object with this class. Use one of the TDK-Lambda power supply instrument classes that inherit from this parent class. Untested commands are noted in docstrings. """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Initializer and important communication methods # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def __init__(self, adapter, name="TDK-Lambda Base", address=6, **kwargs): super().__init__( adapter, name, includeSCPI=False, asrl={'read_termination': "\r", 'write_termination': "\r"}, **kwargs ) self.address = address def check_set_errors(self): """ Only use this command for setting commands, i.e. non-querying commands. Any non-querying commands (i.e., a command that does NOT have the "?" symbol in it like the instrument command "PV 10") will automatically return an "OK" reply for valid command or an error code. This is done to confirm that the instrument has received the command. Any querying commands (i.e., a command that does have the "?" symbol in it like the instrument command "PV?") will return the requested value, not the confirmation. """ response = self.read() error_list = [] if response != "OK": error_list.append(f"Received error: {response}") return error_list # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Properties # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ address = Instrument.setting( "ADR %d", """Set the address of the power supply. Valid values are integers between 0 - 30 (inclusive).""", check_set_errors=True, validator=strict_discrete_set, values=range(0, 31) ) remote = Instrument.control( "RMT?", "RMT %s", """Control the current remote operation of the power supply. Valid values are ``'LOC'`` for local mode, ``'REM'`` for remote mode, and ``'LLO'`` for local lockout mode. """, check_set_errors=True, validator=strict_discrete_set, values=["LOC", "REM", "LLO"] ) multidrop_capability = Instrument.measurement( "MDAV?", """Get whether the multi-drop option is available on the power supply. If return value is ``False``, the option is not available, if ``True`` it is available. Property is UNTESTED. """, cast=bool ) master_slave_setting = Instrument.measurement( "MS?", """Get the master and slave settings. Possible master return values are 1, 2, 3, and 4. The slave value is 0. Property is UNTESTED. """ ) repeat = Instrument.measurement( "\\", """Measure the last command again. Returns output of the last command. """ ) id = Instrument.measurement( "IDN?", """Get the identity of the instrument. Returns a list of instrument manufacturer and model in the format: ``["LAMBDA", "GENX-Y"]`` """ ) version = Instrument.measurement( "REV?", """Get the software version on instrument. Returns the software version as an ASCII string. """ ) serial = Instrument.measurement( "SN?", """Get the serial number of the instrument. Returns the serial number of of the instrument as an ASCII string. """ ) last_test_date = Instrument.measurement( "DATE?", """Get the date of the last test, possibly calibration date. Returns a string in the format: yyyy/mm/dd. """ ) voltage_setpoint = Instrument.control( "PV?", "PV %g", """Control the programmed (set) output voltage.""", check_set_errors=True, validator=lambda v, vs: strict_discrete_range(v, vs, step=0.01), values=[0, 40], dynamic=True ) voltage = Instrument.measurement( "MV?", """Measure the actual output voltage.""" ) current_setpoint = Instrument.control( "PC?", "PC %g", """Control the programmed (set) output current.""", check_set_errors=True, validator=lambda v, vs: strict_discrete_range(v, vs, step=0.01), values=[0, 38], dynamic=True ) current = Instrument.measurement( "MC?", """Measure the actual output current. Returns a float with five digits of precision. """ ) mode = Instrument.measurement( "MODE?", """Measure the output mode of the power supply. When power supply is on, the returned value will be either ``'CV'`` for control voltage or ``'CC'`` for or control current. If the power supply is off, the returned value will be ``'OFF'``. """ ) display = Instrument.measurement( "DVC?", """Get the displayed voltage and current. Returns a list of floating point numbers in the order of [ measured voltage, programmed voltage, measured current, programmed current, over voltage set point, under voltage set point ]. """ ) status = Instrument.measurement( "STT?", """Get the power supply status. Returns a list in the order of [ actual voltage (MV), the programmed voltage (PV), the actual current (MC), the programmed current (PC), the status register (SR), and the fault register (FR) ]. """ ) pass_filter = Instrument.control( "FILTER?", "FILTER %d", """Control the low pass filter frequency of the A to D converter for voltage and current measurement. Valid frequency values are 18, 23, or 46 Hz. Default value is 18 Hz. """, check_set_errors=True, validator=strict_discrete_set, values=[18, 23, 46] ) output_enabled = Instrument.control( "OUT?", "OUT %s", """Control the output of the power supply. Valid values are ``True`` to turn output on and ``False`` to turn output off, shutting down any voltage or current. """, check_set_errors=True, validator=strict_discrete_set, values={True: "ON", False: "OFF"}, map_values=True ) foldback_enabled = Instrument.control( "FLD?", "FLD %s", """Control the fold back protection of the power supply. Valid values are ``True`` to arm the fold back protection and ``False`` to cancel the fold back protection. """, check_set_errors=True, validator=strict_discrete_set, values={True: "ON", False: "OFF"}, map_values=True ) foldback_delay = Instrument.control( "FBD?", "FBD %g", """Control the fold back delay. Adds an additional delay to the standard fold back delay (250 ms) by multiplying the set value by 0.1. Valid values are integers between 0 to 255. """, check_set_errors=True, validator=strict_range, values=[0, 255], cast=int ) over_voltage = Instrument.control( "OVP?", "OVP %g", """Control the over voltage protection. """, check_set_errors=True, validator=lambda v, vs: strict_discrete_range(v, vs, step=0.01), values=[2, 44], dynamic=True ) under_voltage = Instrument.control( "UVL?", "UVL %g", """Control the under voltage limit. Property is UNTESTED. """, check_set_errors=True, validator=lambda v, vs: strict_discrete_range(v, vs, step=0.01), values=[0, 38], dynamic=True ) auto_restart_enabled = Instrument.control( "AST?", "AST %s", """Control the auto restart mode, which restores the power supply to the last output voltage and current settings with output enabled on startup. Valid values are ``True`` to restore output settings with output enabled on startup and ``False`` to disable restoration of settings and output disabled on startup. """, check_set_errors=True, validator=strict_discrete_set, values={True: "ON", False: "OFF"}, map_values=True ) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Methods # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def clear(self): """Clear FEVE and SEVE registers to zero.""" self.write("CLS") self.check_errors() def reset(self): """Reset the instrument to default values.""" self.write("RST") self.check_errors() def foldback_reset(self): """Reset the fold back delay to 0 s, restoring the standard 250 ms delay. Property is UNTESTED. """ self.write("FDBRST") self.check_errors() def save(self): """Save current instrument settings.""" self.write("SAV") self.check_errors() def recall(self): """Recall last saved instrument settings.""" self.write("RCL") self.check_errors() def set_max_over_voltage(self): """Set the over voltage protection to the maximum level for the power supply. """ self.write("OVM") self.check_errors() def ramp_to_current(self, target_current, steps=20, pause=0.2): """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: Target current in amps :param steps: Integer number of steps :param pause: Pause duration in seconds to wait between steps """ currents = [round(i, 2) for i in np.linspace(self.current_setpoint, target_current, steps)] for current in currents: self.current_setpoint = current sleep(pause) def shutdown(self): """Safety shutdown the power supply. Ramps the power supply down to zero current using the ``self.ramp_to_current(0.0)`` method and turns the output off. """ log.info("Shutting down %s." % self.name) self.ramp_to_current(0.0) self.output_enabled = False super().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/tdk/tdk_gen40_38.py0000644000175100001770000000651514623331163023056 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # # ============================================================================= # Libraries / modules # ============================================================================= from .tdk_base import TDK_Lambda_Base # ============================================================================= # Instrument file # ============================================================================= class TDK_Gen40_38(TDK_Lambda_Base): """ Represents the TDK Lambda Genesys 40-38 DC power supply. Class inherits commands from the TDK_Lambda_Base parent class and utilizes dynamic properties adjust valid values on various properties. .. code-block:: python psu = TDK_Gen40_38("COM3", 6) # COM port and daisy-chain address psu.remote = "REM" # PSU in remote mode psu.output_enabled = True # Turn on output psu.ramp_to_current(2.0) # Ramp to 2.0 A of current print(psu.current) # Measure actual PSU current print(psu.voltage) # Measure actual PSU voltage psu.shutdown() # Run shutdown command The initialization of a TDK instrument requires the current address of the TDK power supply. The default address for the TDK Lambda is 6. :param adapter: VISAAdapter instance :param name: Instrument name. Default is "TDK Lambda Gen40-38" :param address: Serial port daisy chain number. Default is 6. """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Dynamic values - Overrides base class validator values # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ voltage_values = [0, 40] current_values = [0, 38] over_voltage_values = [2, 44] under_voltage_values = [0, 38] # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Initializer # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def __init__(self, adapter, name="TDK Lambda Gen40-38", address=6, **kwargs): super().__init__( adapter, name, address, **kwargs ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/tdk/tdk_gen80_65.py0000644000175100001770000000651514623331163023062 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # # ============================================================================= # Libraries / modules # ============================================================================= from .tdk_base import TDK_Lambda_Base # ============================================================================= # Instrument file # ============================================================================= class TDK_Gen80_65(TDK_Lambda_Base): """ Represents the TDK Lambda Genesys 80-65 DC power supply. Class inherits commands from the TDK_Lambda_Base parent class and utilizes dynamic properties adjust valid values on various properties. .. code-block:: python psu = TDK_Gen80_65("COM3", 6) # COM port and daisy-chain address psu.remote = "REM" # PSU in remote mode psu.output_enabled = True # Turn on output psu.ramp_to_current(2.0) # Ramp to 2.0 A of current print(psu.current) # Measure actual PSU current print(psu.voltage) # Measure actual PSU voltage psu.shutdown() # Run shutdown command The initialization of a TDK instrument requires the current address of the TDK power supply. The default address for the TDK Lambda is 6. :param adapter: VISAAdapter instance :param name: Instrument name. Default is "TDK Lambda Gen80-65" :param address: Serial port daisy chain number. Default is 6. """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Dynamic values - Overrides base class validator values # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ voltage_values = [0, 80] current_values = [0, 65] over_voltage_values = [5, 88] under_voltage_values = [0, 76] # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Initializer # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def __init__(self, adapter, name="TDK Lambda Gen80-65", address=6, **kwargs): super().__init__( adapter, name, address, **kwargs ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4176059 pymeasure-0.14.0/pymeasure/instruments/tektronix/0000755000175100001770000000000014623331176021655 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/tektronix/__init__.py0000644000175100001770000000230114623331163023756 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/tektronix/afg3152c.py0000644000175100001770000001353714623331163023447 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set class AFG3152CChannel(Channel): 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] # Frequency 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", """ Control the shape of the output. (str)""", validator=strict_discrete_set, values=SHAPES, map_values=True, ) unit = Instrument.control( "voltage:unit?", "voltage:unit %s", """ Control the amplitude unit. (str)""", validator=strict_discrete_set, values=UNIT_LIMIT, ) amp_vpp = Instrument.control( "voltage:amplitude?", "voltage:amplitude %eVPP", """Control the output amplitude in Vpp. (float)""", validator=strict_range, values=AMPLITUDE_LIMIT["VPP"], ) amp_dbm = Instrument.control( "voltage:amplitude?", "voltage:amplitude %eDBM", """ Control the output amplitude in dBm. (float)""", validator=strict_range, values=AMPLITUDE_LIMIT["DBM"], ) amp_vrms = Instrument.control( "voltage:amplitude?", "voltage:amplitude %eVRMS", """ Control the output amplitude in Vrms. (float)""", validator=strict_range, values=AMPLITUDE_LIMIT["VRMS"], ) offset = Instrument.control( "voltage:offset?", "voltage:offset %e", """ Control the amplitude offset. It is always in Volt. (float)""", ) frequency = Instrument.control( "frequency:fixed?", "frequency:fixed %e", """ Control the frequency. (float)""", validator=strict_range, values=FREQ_LIMIT, ) duty = Instrument.control( "pulse:dcycle?", "pulse:dcycle %.3f", """ Control the duty cycle of pulse. (float))""", validator=strict_range, values=DUTY_LIMIT, ) impedance = Instrument.control( "output:impedance?", "output:impedance %d", """ Control the output impedance of the channel. Be careful with this.""", validator=strict_range, values=IMP_LIMIT, cast=int, ) def insert_id(self, command): """Prepend the channel id for most writes.""" return f'source{self.id}:{command}' def enable(self): self.parent.write("output%d:state on" % self.number) def disable(self): self.parent.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.write("function:shape %s" % shape) self.write("frequency:fixed %e" % frequency) self.write("voltage:unit %s" % units) self.write("voltage:amplitude %e%s" % (amplitude, units)) self.write("voltage:offset %eV" % offset) class AFG3152C(SCPIUnknownMixin, Instrument): """Represents the Tektronix AFG 3000 series (one or two channels) arbitrary function generator and provides a high-level for interacting with the instrument. .. code-block:: python 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, name="Tektronix AFG3152C arbitrary function generator", **kwargs): super().__init__( adapter, name, **kwargs ) self.ch1 = AFG3152CChannel(self, 1) self.ch2 = AFG3152CChannel(self, 2) def beep(self): self.write("system:beep") def opc(self): return int(self.ask("*OPC?")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/tektronix/tds2000.py0000644000175100001770000000657014623331163023327 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin class TDS2000(SCPIUnknownMixin, Instrument): """ Represents the Tektronix TDS 2000 Oscilloscope and provides a high-level for interacting with the instrument """ class Measurement: 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(f"{self.preamble}SOU {value}") else: raise ValueError("Invalid source ('{}') provided to {}".format( 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(f"{self.preamble}TYP {value}") else: raise ValueError("Invalid type ('{}') provided to {}".format( 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(f"{self.preamble}UNI {value}") else: raise ValueError("Invalid unit ('{}') provided to {}".format( self.parent, value)) def __init__(self, adapter, name="Tektronix TDS 2000 Oscilloscope", **kwargs): super().__init__( adapter, name, **kwargs ) self.measurement = TDS2000.Measurement(self) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4176059 pymeasure-0.14.0/pymeasure/instruments/teledyne/0000755000175100001770000000000014623331176021437 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/teledyne/__init__.py0000644000175100001770000000241514623331163023546 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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 .teledyneT3AFG import TeledyneT3AFG from .teledyne_oscilloscope import TeledyneOscilloscope from .teledyneMAUI import TeledyneMAUI ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/teledyne/teledyneMAUI.py0000644000175100001770000001770414623331163024303 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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.teledyne.teledyne_oscilloscope import TeledyneOscilloscope, \ TeledyneOscilloscopeChannel, _results_list_to_dict class TeledyneMAUIChannel(TeledyneOscilloscopeChannel): """Base class for channels on a :class:`TeledyneMAUI` device.""" # Probably for historic reasons, "20MHZ" is registered as "ON" BANDWIDTH_LIMITS = ["OFF", "ON", "200MHZ"] TRIGGER_SLOPES = {"negative": "NEG", "positive": "POS"} # Reset listed values for existing commands: bwlimit_values = BANDWIDTH_LIMITS def autoscale(self): """Perform auto-setup command for channel.""" self.write("AUTO_SETUP FIND") # noinspection PyIncorrectDocstring def setup(self, **kwargs): """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. See property descriptions for more information. :param bwlimit: :param coupling: :param display: :param offset: :param probe_attenuation: :param scale: :param trigger_coupling: :param trigger_level: :param trigger_slope: """ super().setup(**kwargs) @property def current_configuration(self): """Get channel configuration as a dict containing the following keys: - "channel": channel number (int) - "attenuation": probe attenuation (float) - "bandwidth_limit": bandwidth limiting, parsed for this channel (str) - "coupling": "ac 1M", "dc 1M", "ground" coupling (str) - "offset": vertical offset (float) - "display": currently displayed (bool) - "volts_div": vertical divisions (float) - "trigger_coupling": trigger coupling can be "dc" "ac" "highpass" "lowpass" (str) - "trigger_level": trigger level (float) - "trigger_slope": trigger slope can be "negative" "positive" "window" (str) """ ch_setup = { "channel": self.id, "attenuation": self.probe_attenuation, "bandwidth_limit": self.bwlimit[f"C{self.id}"], "coupling": self.coupling, "offset": self.offset, "display": self.display, "volts_div": self.scale, "trigger_coupling": self.trigger_coupling, "trigger_level": self.trigger_level, "trigger_slope": self.trigger_slope } return ch_setup class TeledyneMAUI(TeledyneOscilloscope): """A base class for the MAUI-type of Teledyne oscilloscopes. This base class works out of the box. Some properties, especially the number of channels, might have to be adjusted to the actual device. The manual detailing the API is "MAUI Oscilloscopes Remote Control and Automation Manual" (`link`_). .. _link: https://cdn.teledynelecroy.com/files/manuals/ maui-remote-control-automation_27jul22.pdf """ ch_1 = Instrument.ChannelCreator(TeledyneMAUIChannel, 1) ch_2 = Instrument.ChannelCreator(TeledyneMAUIChannel, 2) ch_3 = Instrument.ChannelCreator(TeledyneMAUIChannel, 3) ch_4 = Instrument.ChannelCreator(TeledyneMAUIChannel, 4) # Change listed values for existing commands: bwlimit_values = TeledyneMAUIChannel.BANDWIDTH_LIMITS ############### # Trigger # ############### @property def trigger(self): """Get trigger setup as a dict containing the following keys: - "mode": trigger sweep mode [auto, normal, single, stop] - "trigger_type": condition that will trigger the acquisition of waveforms [edge,slew,glit,intv,runt,drop] - "source": trigger source [c1,c2,c3,c4] - "hold_type": hold type (refer to page 172 of programing guide) - "hold_value1": hold value1 (refer to page 172 of programing guide) - "hold_value2": hold value2 (refer to page 172 of programing guide) - "coupling": input coupling for the selected trigger sources - "level": trigger level voltage for the active trigger source - "slope": trigger slope of the specified trigger source """ trigger_select = self.trigger_select ch = self.ch(trigger_select[1]) tb_setup = { "mode": self.trigger_mode, "trigger_type": trigger_select[0], "source": trigger_select[1], "hold_type": trigger_select[2], "hold_value1": trigger_select[3] if len(trigger_select) >= 4 else None, "hold_value2": trigger_select[4] if len(trigger_select) >= 5 else None, "coupling": ch.trigger_coupling, "level": ch.trigger_level, "slope": ch.trigger_slope } return tb_setup def force_trigger(self): """Make one acquisition if in active trigger mode. No action is taken if the device is in 'Stop trigger mode'. """ # Method instead of property since no reply is sent self.write("FRTR") ################# # Download data # ################# hardcopy_setup_current = Instrument.measurement( "HCSU?", """Get current hardcopy config.""", get_process=_results_list_to_dict, ) def hardcopy_setup(self, **kwargs): """Specify hardcopy settings. Connect a printer or define how to save to file. Set any or all of the following parameters. :param device: {BMP, JPEG, PNG, TIFF} :param format: {PORTRAIT, LANDSCAPE} :param background: {Std, Print, BW} :param destination: {PRINTER, CLIPBOARD, EMAIL, FILE, REMOTE} :param area: {GRIDAREAONLY, DSOWINDOW, FULLSCREEN} :param directory: Any legal DOS path, for FILE mode only :param filename: Filename string, no extension, for FILE mode only :param printername: Valid printer name, for PRINTER mode only :param portname: {GPIB, NET} """ keys = { "device": "DEV", "format": "FORMAT", "background": "BCKG", "destination": "DEST", "area": "AREA", "directory": "DIR", "filename": "FILE", "printername": "PRINTER", "portname": "PORT", } arg_strs = [keys[key] + "," + value for key, value in kwargs.items()] self.write("HCSU " + ",".join(arg_strs)) def download_image(self, **kwargs): """Get a BMP image of oscilloscope screen in bytearray of specified file format. The hardcopy destination is set to "REMOTE" by default. :param \\**kwargs: Keyword arguments for :meth:`hardcopy_setup` """ kwargs.setdefault("destination", "REMOTE") self.hardcopy_setup(**kwargs) return super().download_image() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/teledyne/teledyneT3AFG.py0000644000175100001770000001326014623331163024345 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, Channel, SCPIMixin from pymeasure.instruments.validators import strict_range, strict_discrete_set log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def get_process_generator_search(keyword, unit, type): """Generate a get_process method searching for keyword, stripping unit""" def selector(values): if keyword in values: try: return type(values[values.index(keyword) + 1].strip(unit)) except (ValueError, IndexError): # Something went quite wrong if the keyword exists but the value doesn't return None else: # Wrong wavetype for this keyword return None return selector class SignalChannel(Channel): output_enabled = Channel.control( "C{ch}:OUTPut?", "C{ch}:OUTPut %s", """Control whether the channel output is enabled (boolean).""", validator=strict_discrete_set, map_values=True, values={True: 'ON', False: 'OFF'}, # Replace ON and OFF with True and False in both get and set get_process=lambda x: True if x[0].split(' ')[1] == 'ON' else False, ) wavetype = Channel.control( "C{ch}:BSWV?", "C{ch}:BSWV WVTP,%s", """Control the type of waveform to be output. Options are: {'SINE', 'SQUARE', 'RAMP', 'PULSE', 'NOISE', 'ARB', 'DC', 'PRBS', 'IQ'} """, validator=strict_discrete_set, values=['SINE', 'SQUARE', 'RAMP', 'PULSE', 'NOISE', 'ARB', 'DC', 'PRBS', 'IQ'], get_process=lambda x: x[1], ) frequency = Channel.control( "C{ch}:BSWV?", "C{ch}:BSWV FRQ,%g", """Control the frequency of waveform to be output in Hertz. Has no effect when WVTP is NOISE or DC.""", validator=strict_range, values=[0, 350e6], get_process=get_process_generator_search('FRQ', 'HZ', float), dynamic=True, check_set_errors=True, ) amplitude = Channel.control( "C{ch}:BSWV?", "C{ch}:BSWV AMP,%g", """Control the amplitude of waveform to be output in volts peak-to-peak. Has no effect when WVTP is NOISE or DC. Max amplitude depends on offset, frequency, and load. Amplitude is also limited by the channel max output amplitude.""", validator=strict_range, values=[-5, 5], get_process=get_process_generator_search('AMP', 'V', float), dynamic=True, check_set_errors=True, ) offset = Channel.control( "C{ch}:BSWV?", "C{ch}:BSWV OFST,%g", """Control the offset of waveform to be output in volts. Has no effect when WVTP is NOISE. Max offset depends on amplitude, frequency, and load. Offset is also limited by the channel max output amplitude.""", validator=strict_range, values=[-5, 5], get_process=get_process_generator_search('OFST', 'V', float), dynamic=True, check_set_errors=True, ) max_output_amplitude = Channel.control( "C{ch}:BSWV?", "C{ch}:BSWV MAX_OUTPUT_AMP,%g", """Control the maximum output amplitude of the channel in volts peak to peak.""", validator=strict_range, values=[0, 20], get_process=get_process_generator_search('MAX_OUTPUT_AMP', 'V', float), dynamic=True, ) class TeledyneT3AFG(SCPIMixin, Instrument): """Represents the Teledyne T3AFG series of arbitrary waveform generator interface for interacting with the instrument. Initially targeting T3AFG80, some features may not be available on lower end models and features from higher end models are not included here yet. Future improvements (help welcomed): - Add other OUTPut related controls like Load and Polarity - Add other Basic Waveform related controls like Period - Add frequency ranges per model - Add channel coupling control .. code-block: python # Example assumes Ethernet (TCPIP) interface generator=TeledyneT3AFG('TCPIP0::xxx.xxx.xxx.xxx::pppp::SOCKET') generator.reset() generator.ch_1.wavetype='SINE' generator.ch_1.amplitude=2 generator.ch_1.output_enabled=True """ ch_1 = Instrument.ChannelCreator(SignalChannel, 1) ch_2 = Instrument.ChannelCreator(SignalChannel, 2) def __init__(self, adapter, name="Teledyne T3AFG", **kwargs): super().__init__( adapter, name, tcpip={'read_termination': '\n'}, **kwargs ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/teledyne/teledyne_oscilloscope.py0000644000175100001770000013726314623331163026410 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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 abc import ABCMeta import re import sys import time from decimal import Decimal import numpy as np from pymeasure.instruments import Instrument, Channel, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range, \ strict_discrete_range def sanitize_source(source): """Parse source string. :param source: can be "cX", "ch X", "chan X", "channel X", "math" or "line", where X is a single digit integer. The parser is case and white space insensitive. :return: can be "C1", "C2", "C3", "C4", "MATH" or "LINE. """ match = re.match(r"^\s*(C|CH|CHAN|CHANNEL)\s*(?P\d)\s*$|" r"^\s*(?PMATH|LINE)\s*$", source, re.IGNORECASE) if match: if match.group("number") is not None: source = "C" + match.group("number") else: source = match.group("name_only") source = source.upper() else: raise ValueError(f"source {source} not recognized") return source def _trigger_select_num_pars(value): """Find the expected number of parameters for the trigger_select property. :param value: input parameters as a tuple """ value = tuple(map(lambda v: v.upper() if isinstance(v, str) else v, value)) num_expected_pars = 0 if 3 <= len(value) <= 5: if value[0] == "EDGE": num_expected_pars = 3 if value[2] == "OFF" else 4 elif value[0] in ["SLEW", "INTV"]: num_expected_pars = 4 if value[2] in ["IS", "IL"] else 5 elif value[0] in ["GLIT", "RUNT"]: num_expected_pars = 4 if value[2] in ["PS", "PL"] else 5 elif value[0] == "DROP": num_expected_pars = 4 else: raise ValueError('Number of parameters {} can only be 3, 4, 5'.format(len(value))) return num_expected_pars def _trigger_select_validator(value, values, num_pars_finder=_trigger_select_num_pars): """Validate the input of the trigger_select property. :param value: input parameters as a tuple :param values: allowed space for each parameter :param num_pars_finder: function to find the number of expected parameters """ if not isinstance(value, tuple): raise ValueError('Input value {} of trigger_select should be a tuple'.format(value)) if len(value) < 3 or len(value) > 5: raise ValueError('Number of parameters {} can only be 3, 4, 5'.format(len(value))) value = tuple(map(lambda v: v.upper() if isinstance(v, str) else v, value)) value = list(value) value[1] = sanitize_source(value[1]) value = tuple(value) if value[0] not in values.keys(): raise ValueError('Value {} not in the discrete set {}'.format(value[0], values.keys())) num_expected_pars = num_pars_finder(value) if len(value) != num_expected_pars: raise ValueError('Number of parameters {} != {}'.format(len(value), num_expected_pars)) for i, element in enumerate(value[1:], start=1): if i < 3: strict_discrete_set(element, values=values[value[0]][i - 1]) else: strict_range(element, values=values[value[0]][i - 1]) return value def _trigger_select_get_process(value): """Process the output of the trigger_select property. The format of the input list is , SR, , HT, [, HV, S][, HV2, S] The format of the output list is , , [, ][, ] :param value: output parameters as a list """ output = [] if len(value) > 0: output.append(value[0].lower()) if "SR" in value: output.append(value[value.index("SR") + 1].lower()) if "HT" in value: output.append(value[value.index("HT") + 1].lower()) if "HV" in value: output.append(float(value[value.index("HV") + 1][:-1])) if "HV2" in value: output.append(float(value[value.index("HV2") + 1][:-1])) return output def _results_list_to_dict(results): """Turn a list into a dict, using the uneven indices as keys. E.g. turn ['C1', 'OFF', 'C2', 'OFF'] into {'C1': 'OFF', 'C2': 'OFF'} """ keys = results[::2] values = results[1::2] return dict(zip(keys, values)) def _remove_unit(value): """Remove a unit from the returned string and cast to float.""" if isinstance(value, float): return value if value.endswith(" V"): value = value[:-2] return float(value) def _intensity_validator(value, values): """Validate the input of the intensity property (grid intensity and trace intensity). :param value: input parameters as a 2-element tuple :param values: allowed space for each parameter """ if not isinstance(value, tuple): raise ValueError('Input value {} of trigger_select should be a tuple'.format(value)) if len(value) != 2: raise ValueError('Number of parameters {} different from 2'.format(len(value))) for i in range(2): strict_discrete_range(value=value[i], values=values[i], step=1) return value class _ChunkResizer: """The only purpose of this class is to resize the chunk size of the instrument adapter. This is necessary when reading a big chunk of data from the oscilloscope like image dumps and waveforms. .. Note:: Only if the new chunk size is bigger than the current chunk size, it is resized. """ def __init__(self, adapter, chunk_size): """Just initialize the object attributes. :param adapter: Adapter of the instrument. This is usually accessed through the Instrument::adapter attribute. :param chunk_size: new chunk size (int). """ self.adapter = adapter self.old_chunk_size = None self.new_chunk_size = int(chunk_size) if chunk_size else 0 def __enter__(self): """Only resize the chunk size if the adapter support this feature.""" if (self.adapter.connection is not None and hasattr(self.adapter.connection, "chunk_size")): if self.new_chunk_size > self.adapter.connection.chunk_size: self.old_chunk_size = self.adapter.connection.chunk_size self.adapter.connection.chunk_size = self.new_chunk_size def __exit__(self, exc_type, exc_val, exc_tb): if self.old_chunk_size is not None: self.adapter.connection.chunk_size = self.old_chunk_size class TeledyneOscilloscopeChannel(Channel, metaclass=ABCMeta): """A base abstract class for channel on a :class:`TeledyneOscilloscope` device.""" _BOOLS = {True: "ON", False: "OFF"} BANDWIDTH_LIMITS = ["OFF", "ON"] TRIGGER_SLOPES = {"negative": "NEG", "positive": "POS"} # Capture and split a reply like "RMS,281E-6" and "RMS,281E-6,OK" # The third response item ("state"), is not present in all oscilloscopes # For compatibility it is captured if it can, but ignored otherwise _re_pava_response = re.compile(r"^\s*" r"(?P\w+),\s*" r"(?P[^,]*)\s*" r"(?:,(?P\w+)\s*)?$") bwlimit = Instrument.control( "BWL?", "BWL %s", """Control the internal low-pass filter for this channel. The current bandwidths can only be read back for all channels at once! """, validator=strict_discrete_set, values=BANDWIDTH_LIMITS, get_process=_results_list_to_dict, dynamic=True, ) coupling = Instrument.control( "CPL?", "CPL %s", """Control the coupling with a string parameter ("ac 1M", "dc 1M", "ground").""", validator=strict_discrete_set, values={"ac 1M": "A1M", "dc 1M": "D1M", "ground": "GND"}, map_values=True ) display = Instrument.control( "TRA?", "TRA %s", """Control the display enabled state. (strict bool)""", validator=strict_discrete_set, values=_BOOLS, map_values=True ) offset = Instrument.control( "OFST?", "OFST %.2EV", """Control the center of the screen in Volts by a a float parameter. 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( "ATTN?", "ATTN %g", """Control the probe attenuation. The probe attenuation may be from 0.1 to 10000.""", validator=strict_discrete_set, values={0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000} ) scale = Instrument.control( "VDIV?", "VDIV %.2EV", """Control the vertical scale (units per division) in Volts.""" ) trigger_coupling = Instrument.control( "TRCP?", "TRCP %s", """Control the input coupling for the selected trigger sources (string). - ac: AC coupling block DC component in the trigger path, removing dc offset voltage from the trigger waveform. Use AC coupling to get a stable edge trigger when your waveform has a large dc offset. - dc: DC coupling allows dc and ac signals into the trigger path. - lowpass: HFREJ coupling places a lowpass filter in the trigger path. - highpass: LFREJ coupling places a highpass filter in the trigger path. """, validator=strict_discrete_set, values={"ac": "AC", "dc": "DC", "lowpass": "HFREJ", "highpass": "LFREJ"}, map_values=True ) trigger_level = Instrument.control( "TRLV?", "TRLV %.2EV", """Control the trigger level voltage for the active trigger source (float). When there are two trigger levels to set, this command is used to set the higher trigger level voltage for the specified source. :attr:`trigger_level2` is used to set the lower trigger level voltage. When setting the trigger level it must be divided by the probe attenuation. This is not documented in the datasheet and it is probably a bug of the scope firmware. An out-of-range value will be adjusted to the closest legal value. """, get_process=_remove_unit, ) trigger_slope = Instrument.control( "TRSL?", "TRSL %s", """Control the trigger slope of the specified trigger source (string). :={NEG,POS,WINDOW} for edge trigger :={NEG,POS} for other trigger +------------+--------------------------------------------------+ | parameter | trigger slope | +------------+--------------------------------------------------+ | negative | Negative slope for edge trigger or other trigger | +------------+--------------------------------------------------+ | positive | Positive slope for edge trigger or other trigger | +------------+--------------------------------------------------+ | window | Window slope for edge trigger | +------------+--------------------------------------------------+ """, validator=strict_discrete_set, values=TRIGGER_SLOPES, map_values=True, dynamic=True, ) _measurable_parameters = ["PKPK", "MAX", "MIN", "AMPL", "TOP", "BASE", "CMEAN", "MEAN", "RMS", "CRMS", "OVSN", "FPRE", "OVSP", "RPRE", "PER", "FREQ", "PWID", "NWID", "RISE", "FALL", "WID", "DUTY", "NDUTY", "ALL"] display_parameter = Instrument.setting( "PACU %s", """Set the waveform processing of this channel with the specified algorithm and the result is displayed on the front panel. The command accepts the following parameters: ========= =================================== Parameter Description ========= =================================== PKPK vertical peak-to-peak MAX maximum vertical value MIN minimum vertical value AMPL vertical amplitude TOP waveform top value BASE waveform base value CMEAN average value in the first cycle MEAN average value RMS RMS value CRMS RMS value in the first cycle OVSN overshoot of a falling edge FPRE preshoot of a falling edge OVSP overshoot of a rising edge RPRE preshoot of a rising edge PER period FREQ frequency PWID positive pulse width NWID negative pulse width RISE rise-time FALL fall-time WID Burst width DUTY positive duty cycle NDUTY negative duty cycle ALL All measurement ========= =================================== """, validator=strict_discrete_set, values=_measurable_parameters ) def measure_parameter(self, parameter: str): """Process a waveform with the selected algorithm and returns the specified measurement. :param parameter: same as the display_parameter property """ parameter = strict_discrete_set(value=parameter, values=self._measurable_parameters) output = self.ask("PAVA? %s" % parameter) match = self._re_pava_response.match(output) if match: if match.group('parameter') != parameter: raise ValueError(f"Parameter {match.group('parameter')} different from {parameter}") if match.group('state') and match.group('state') == 'IV': raise ValueError(f"Parameter state for {parameter} is invalid") return float(match.group('value')) else: raise ValueError(f"Cannot extract value from output {output}") def insert_id(self, command): # only in case of the BWL and PACU commands the syntax is different. Why? SIGLENT Why? if command[0:4] == "BWL ": return "BWL C%d,%s" % (self.id, command[4:]) elif command[0:5] == "PACU ": return "PACU %s,C%d" % (command[5:], self.id) else: return "C%d:%s" % (self.id, command) # noinspection PyIncorrectDocstring def setup(self, **kwargs): """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. See property descriptions for more information. :param bwlimit: :param coupling: :param display: :param invert: :param offset: :param skew_factor: :param probe_attenuation: :param scale: :param unit: :param trigger_coupling: :param trigger_level: :param trigger_level2: :param trigger_slope: """ for key, value in kwargs.items(): setattr(self, key, value) @property def current_configuration(self): """Get channel configuration as a dict containing the following keys: - "channel": channel number (int) - "attenuation": probe attenuation (float) - "bandwidth_limit": bandwidth limiting enabled (bool) - "coupling": "ac 1M", "dc 1M", "ground" coupling (str) - "offset": vertical offset (float) - "skew_factor": channel-tochannel skew factor (float) - "display": currently displayed (bool) - "unit": "A" or "V" units (str) - "volts_div": vertical divisions (float) - "inverted": inverted (bool) - "trigger_coupling": trigger coupling can be "dc" "ac" "highpass" "lowpass" (str) - "trigger_level": trigger level (float) - "trigger_level2": trigger lower level for SLEW or RUNT trigger (float) - "trigger_slope": trigger slope can be "negative" "positive" "window" (str) """ ch_setup = { "channel": self.id, "attenuation": self.probe_attenuation, "bandwidth_limit": self.bwlimit, "coupling": self.coupling, "offset": self.offset, "skew_factor": self.skew_factor, "display": self.display, "unit": self.unit, "volts_div": self.scale, "inverted": self.invert, "trigger_coupling": self.trigger_coupling, "trigger_level": self.trigger_level, "trigger_level2": self.trigger_level2, "trigger_slope": self.trigger_slope } return ch_setup class TeledyneOscilloscope(SCPIUnknownMixin, Instrument, metaclass=ABCMeta): """A base abstract class for any Teledyne Lecroy oscilloscope. All Teledyne oscilloscopes have a very similar interface, hence this base class to combine them. Note that specific models will likely have conflicts in their interface. Attributes: WRITE_INTERVAL_S: minimum time between two commands. If a command is received less than WRITE_INTERVAL_S after the previous one, the code blocks until at least WRITE_INTERVAL_S seconds have passed. Because the oscilloscope takes a non neglibile time to perform some operations, it might be needed for the user to tweak the sleep time between commands. The WRITE_INTERVAL_S is set to 10ms as default however its optimal value heavily depends on the actual commands and on the connection type, so it is impossible to give a unique value to fit all cases. An interval between 10ms and 500ms second proved to be good, depending on the commands and connection latency. """ _BOOLS = TeledyneOscilloscopeChannel._BOOLS WRITE_INTERVAL_S = 0.02 # seconds ch_1 = Instrument.ChannelCreator(TeledyneOscilloscopeChannel, 1) ch_2 = Instrument.ChannelCreator(TeledyneOscilloscopeChannel, 2) ch_3 = Instrument.ChannelCreator(TeledyneOscilloscopeChannel, 3) ch_4 = Instrument.ChannelCreator(TeledyneOscilloscopeChannel, 4) def __init__(self, adapter, name="Teledyne Oscilloscope", **kwargs): super().__init__(adapter, name=name, **kwargs) if self.adapter.connection is not None: self.adapter.connection.timeout = 3000 self._grid_number = 14 # Number of grids in the horizontal direction self._seconds_since_last_write = 0 # Timestamp of the last command self._header_size = 16 # bytes self._footer_size = 2 # bytes self.waveform_source = "C1" self.default_setup() ################ # System Setup # ################ def default_setup(self): """ Set up the oscilloscope for remote operation. The COMM_HEADER command controls the way the oscilloscope formats response to queries. This command does not affect the interpretation of messages sent to the oscilloscope. Headers can be sent in their long or short form regardless of the CHDR setting. By setting the COMM_HEADER to OFF, the instrument is going to reply with minimal information, and this makes the response message much easier to parse. The user should not be fiddling with the COMM_HEADER during operation, because if the communication header is anything other than OFF, the whole driver breaks down. """ self._comm_header = "OFF" def ch(self, source): """ Get channel object from its index or its name. Or if source is "math", just return the scope object. :param source: can be 1, 2, 3, 4 or C1, C2, C3, C4, MATH :return: handle to the selected source. """ if isinstance(source, str): source = sanitize_source(source) if source == "MATH": return self elif source == "LINE": raise ValueError("LINE is not a valid channel") else: return getattr(self, f"ch_{source if isinstance(source, int) else source[-1]}") def autoscale(self): """ Autoscale displayed channels.""" self.write("ASET") def write(self, command, **kwargs): """Write the command to the instrument through the adapter. Note: if the last command was sent less than WRITE_INTERVAL_S before, this method blocks for the remaining time so that commands are never sent with rate more than 1/WRITE_INTERVAL_S Hz. :param command: command string to be sent to the instrument """ seconds_since_last_write = time.monotonic() - self._seconds_since_last_write if seconds_since_last_write < self.WRITE_INTERVAL_S: time.sleep(self.WRITE_INTERVAL_S - seconds_since_last_write) self._seconds_since_last_write = seconds_since_last_write super().write(command, **kwargs) _comm_header = Instrument.control( "CHDR?", "CHDR %s", """Control the way the oscilloscope formats response to queries. The user should not be fiddling with the COMM_HEADER during operation, because if the communication header is anything other than OFF, the whole driver breaks down. • SHORT — response starts with the short form of the header word. • LONG — response starts with the long form of the header word. • OFF — header is omitted from the response and units in numbers are suppressed.""", validator=strict_discrete_set, values=["OFF", "SHORT", "LONG"], ) ########### # General # ########### bwlimit = Instrument.control( "BWL?", "BWL %s", """Set the internal low-pass filter for all channels.""", validator=strict_discrete_set, values=TeledyneOscilloscopeChannel.BANDWIDTH_LIMITS, get_process=_results_list_to_dict, dynamic=True, ) ################## # Timebase Setup # ################## timebase_offset = Instrument.control( "TRDL?", "TRDL %.2ES", """Control the time interval in seconds between the trigger event and the reference position (at center of screen by default).""" ) timebase_scale = Instrument.control( "TDIV?", "TDIV %.2ES", """Control the horizontal scale (units per division) in seconds for the main window (float).""", validator=strict_range, values=[1e-9, 100] ) @property def timebase(self): """Get timebase setup as a dict containing the following keys: - "timebase_scale": horizontal scale in seconds/div (float) - "timebase_offset": interval in seconds between the trigger and the reference position (float) """ tb_setup = { "timebase_scale": self.timebase_scale, "timebase_offset": self.timebase_offset, } return tb_setup def timebase_setup(self, scale=None, offset=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 timebase_setup if needed. :param scale: interval in seconds between the trigger event and the reference position. :param offset: horizontal scale per division in seconds/div. """ if scale is not None: self.timebase_scale = scale if offset is not None: self.timebase_offset = offset ############### # Acquisition # ############### def run(self): """Starts repetitive acquisitions. This is the same as pressing the Run key on the front panel. """ self.trigger_mode = "normal" 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("ARM") ################## # Waveform # ################## waveform_points = Instrument.control( "WFSU?", "WFSU NP,%d", """Control the number of waveform points to be transferred with the digitize method (int). NP = 0 sends all data points. Note that the oscilloscope may provide less than the specified nb of points. """, validator=strict_range, get_process=lambda vals: vals[vals.index("NP") + 1], values=[0, sys.maxsize] ) waveform_sparsing = Instrument.control( "WFSU?", "WFSU SP,%d", """Control the interval between data points (integer). For example: SP = 0 sends all data points. SP = 4 sends 1 point every 4 data points. """, validator=strict_range, get_process=lambda vals: vals[vals.index("SP") + 1], values=[0, sys.maxsize] ) waveform_first_point = Instrument.control( "WFSU?", "WFSU FP,%d", """Control the address of the first data point to be sent (int). For waveforms acquired in sequence mode, this refers to the relative address in the given segment. The first data point starts at zero and is strictly positive.""", validator=strict_range, get_process=lambda vals: vals[vals.index("FP") + 1], values=[0, sys.maxsize] ) ################## # Waveform # ################## memory_size = Instrument.control( "MSIZ?", "MSIZ %s", """Control the maximum depth of memory (float or string). Assign for example 500, 100e6, "100K", "25MA". The reply will always be a float. """ ) @property def waveform_preamble(self): """Get preamble information for the selected waveform source as a dict with the following keys: - "requested_points": number of data points requested by the user (int) - "sampled_points": number of data points sampled by the oscilloscope (int) - "transmitted_points": number of data points actually transmitted (optional) (int) - "memory_size": size of the oscilloscope internal memory in bytes (int) - "sparsing": sparse point. It defines the interval between data points. (int) - "first_point": address of the first data point to be sent (int) - "source": source of the data : "C1", "C2", "C3", "C4", "MATH". - "grid_number": number of horizontal grids (it is a read-only property) - "xdiv": horizontal scale (units per division) in seconds - "xoffset": time interval in seconds between the trigger event and the reference position - "ydiv": vertical scale (units per division) in Volts - "yoffset": value that is represented at center of screen in Volts """ vals = self.values("WFSU?") preamble = { "sparsing": vals[vals.index("SP") + 1], "requested_points": vals[vals.index("NP") + 1], "first_point": vals[vals.index("FP") + 1], "transmitted_points": None, "source": self.waveform_source, "grid_number": self._grid_number, "memory_size": self.memory_size, "xdiv": self.timebase_scale, "xoffset": self.timebase_offset } return self._fill_yaxis_preamble(preamble) def _fill_yaxis_preamble(self, preamble=None): """Fill waveform preamble section concerning the Y-axis. :param preamble: waveform preamble to be filled :return: filled preamble """ if preamble is None: preamble = {} if self.waveform_source == "MATH": preamble["ydiv"] = self.math_vdiv preamble["yoffset"] = self.math_vpos else: preamble["ydiv"] = self.ch(self.waveform_source).scale preamble["yoffset"] = self.ch(self.waveform_source).offset return preamble def _digitize(self, src, num_bytes=None): """Acquire waveforms according to the settings of the acquire commands. Note. If the requested number of bytes is not specified, the default chunk size is used, but in such a case it cannot be quaranteed that the message is received in its entirety. :param src: source of data: "C1", "C2", "C3", "C4", "MATH". :param: num_bytes: number of bytes expected from the scope (including the header and footer). :return: bytearray with raw data. """ with _ChunkResizer(self.adapter, num_bytes): binary_values = self.binary_values(f"{src}:WF? DAT2", dtype=np.uint8) if num_bytes is not None and len(binary_values) != num_bytes: raise BufferError(f"read bytes ({len(binary_values)}) != requested bytes ({num_bytes})") return binary_values def _header_footer_sanity_checks(self, message): """Check that the header follows the predefined format. The format of the header is DAT2,#9XXXXXXX where XXXXXXX is the number of acquired points, and it is zero padded. Then check that the footer is present. The footer is a double line-carriage \n\n :param message: raw bytes received from the scope """ message_header = bytes(message[0:self._header_size]).decode("ascii") # Sanity check on header and footer if message_header[0:7] != "DAT2,#9": raise ValueError(f"Waveform data in invalid : header is {message_header}") message_footer = bytes(message[-self._footer_size:]).decode("ascii") if message_footer != "\n\n": raise ValueError(f"Waveform data in invalid : footer is {message_footer}") def _npoints_sanity_checks(self, message): """Check that the number of transmitted points is consistent with the message length. :param message: raw bytes received from the scope """ message_header = bytes(message[0:self._header_size]).decode("ascii") transmitted_points = int(message_header[-9:]) received_points = len(message) - self._header_size - self._footer_size if transmitted_points != received_points: raise ValueError(f"Number of transmitted points ({transmitted_points}) != " f"number of received points ({received_points})") def _acquire_data(self, requested_points=0, sparsing=1): """Acquire raw data points from the scope. The header, footer and number of points are sanity-checked, but they are not processed otherwise. For a description of the input arguments refer to the download_waveform method. If the number of expected points is big enough, the transmission is split in smaller chunks of 20k points and read one chunk at a time. I do not know the reason why, but if the chunk size is big enough the transmission does not complete successfully. :return: raw data points as numpy array and waveform preamble """ # Setup waveform acquisition parameters self.waveform_sparsing = sparsing self.waveform_points = requested_points self.waveform_first_point = 0 # Calculate how many points are to be expected sample_points = self.acquisition_sample_size(self.waveform_source) if requested_points > 0: expected_points = min(requested_points, int(sample_points / sparsing)) else: expected_points = int(sample_points / sparsing) # If the number of points is big enough, split the data in small chunks and read it one # chunk at a time. For less than a certain amount of points we do not bother splitting them. chunk_bytes = 20000 chunk_points = chunk_bytes - self._header_size - self._footer_size iterations = -(expected_points // -chunk_points) i = 0 data = [] while i < iterations: # number of points already read read_points = i * chunk_points # number of points still to read remaining_points = expected_points - read_points # number of points requested in a single chunk requested_points = chunk_points if remaining_points > chunk_points else remaining_points self.waveform_points = requested_points # number of bytes requested in a single chunk requested_bytes = requested_points + self._header_size + self._footer_size # read the next chunk starting from this points first_point = read_points * sparsing self.waveform_first_point = first_point # read chunk of points values = self._digitize(src=self.waveform_source, num_bytes=requested_bytes) # perform many sanity checks on the received data self._header_footer_sanity_checks(values) self._npoints_sanity_checks(values) # append the points without the header and footer data.append(values[self._header_size:-self._footer_size]) i += 1 data = np.concatenate(data) preamble = self.waveform_preamble return data, preamble ################# # Download data # ################# def download_image(self): """Get a BMP image of oscilloscope screen in bytearray of specified file format. """ # Using binary_values query because default interface does not support binary transfer with _ChunkResizer(self.adapter, 20 * 1024 * 1024): img = self.binary_values("SCDP", dtype=np.uint8) return bytearray(img) def _process_data(self, ydata, preamble): """Apply scale and offset to the data points acquired from the scope. - Y axis : the scale is ydiv / 25 and the offset -yoffset. the offset is not applied for the MATH source. - X axis : the scale is sparsing / sampling_rate and the offset is -xdiv * 7. The 7 = 14 / 2 factor comes from the fact that there are 14 vertical grid lines and the data starts from the left half of the screen. :return: tuple of (numpy array of Y points, numpy array of X points, waveform preamble) """ def _scale_data(y): if preamble["source"] == "MATH": value = int.from_bytes([y], byteorder='big', signed=False) * preamble["ydiv"] / 25. value -= preamble["ydiv"] * (preamble["yoffset"] + 255) / 50. else: value = int.from_bytes([y], byteorder='big', signed=True) * preamble["ydiv"] / 25. value -= preamble["yoffset"] return value def _scale_time(x): return float(Decimal(-preamble["xdiv"] * self._grid_number / 2.) + Decimal(float(x * preamble["sparsing"])) / Decimal(preamble["sampling_rate"])) data_points = np.vectorize(_scale_data)(ydata) time_points = np.vectorize(_scale_time)(np.arange(len(data_points))) return data_points, time_points, preamble def download_waveform(self, source, requested_points=None, sparsing=None): """Get data points from the specified source of the oscilloscope. The returned objects are two np.ndarray of data and time points and a dict with the waveform preamble, that contains metadata about the waveform. :param source: measurement source. It can be "C1", "C2", "C3", "C4", "MATH". :param requested_points: number of points to acquire. If None the number of points requested in the previous call will be assumed, i.e. the value of the number of points stored in the oscilloscope memory. If 0 the maximum number of points will be returned. :param sparsing: interval between data points. For example if sparsing = 4, only one point every 4 points is read. If 0 or None the sparsing of the previous call is assumed, i.e. the value of the sparsing stored in the oscilloscope memory. :return: data_ndarray, time_ndarray, waveform_preamble_dict: see waveform_preamble property for dict format. """ # Sanitize the input arguments if not sparsing: sparsing = self.waveform_sparsing if requested_points is None: requested_points = self.waveform_points self.waveform_source = sanitize_source(source) # Acquire the Y data and the preable ydata, preamble = self._acquire_data(requested_points, sparsing) # Update the preamble with info about actually acquired data preamble["transmitted_points"] = len(ydata) preamble["requested_points"] = requested_points preamble["sparsing"] = sparsing preamble["first_point"] = 0 # Scale the Y-data and create the X-data return self._process_data(ydata, preamble) ############### # Trigger # ############### trigger_mode = Instrument.control( "TRMD?", "TRMD %s", """Control the trigger sweep mode (string). := {AUTO,NORM,SINGLE,STOP} - auto : When AUTO sweep mode is selected, the oscilloscope begins to search for the trigger signal that meets the conditions. If the trigger signal is satisfied, the running state on the top left corner of the user interface shows Trig'd, and the interface shows stable waveform. Otherwise, the running state always shows Auto, and the interface shows unstable waveform. - normal : When NORMAL sweep mode is selected, the oscilloscope enters the wait trigger state and begins to search for trigger signals that meet the conditions. If the trigger signal is satisfied, the running state shows Trig'd, and the interface shows stable waveform. Otherwise, the running state shows Ready, and the interface displays the last triggered waveform (previous trigger) or does not display the waveform (no previous trigger). - single : When SINGLE sweep mode is selected, the backlight of SINGLE key lights up, the oscilloscope enters the waiting trigger state and begins to search for the trigger signal that meets the conditions. If the trigger signal is satisfied, the running state shows Trig'd, and the interface shows stable waveform. Then, the oscilloscope stops scanning, the RUN/STOP key is red light, and the running status shows Stop. Otherwise, the running state shows Ready, and the interface does not display the waveform. - stopped : STOP is a part of the option of this command, but not a trigger mode of the oscilloscope. """, validator=strict_discrete_set, values={"stopped": "STOP", "normal": "NORM", "single": "SINGLE", "auto": "AUTO"}, map_values=True ) _trigger_select_vals = { "EDGE": [["C1", "C2", "C3", "C4", "LINE"], ["TI", "OFF"], [80e-9, 1.5]], "DROP": [["C1", "C2", "C3", "C4"], ["TI"], [2e-9, 4.2]], "GLIT": [["C1", "C2", "C3", "C4"], ["PS", "PL", "P2", "P1"], [2e-9, 4.2], [2e-9, 4.2]], "RUNT": [["C1", "C2", "C3", "C4"], ["PS", "PL", "P2", "P1"], [2e-9, 4.2]], "SLEW": [["C1", "C2", "C3", "C4"], ["IS", "IL", "I2", "I1"], [2e-9, 4.2]], "INTV": [["C1", "C2", "C3", "C4"], ["IS", "IL", "I2", "I1"], [2e-9, 4.2]] } _trigger_select_short_command = "TRSE %s,SR,%s,HT,%s" _trigger_select_normal_command = "TRSE %s,SR,%s,HT,%s,HV,%.2E" _trigger_select_extended_command = "TRSE %s,SR,%s,HT,%s,HV,%.2E,HV2,%.2E" _trigger_select = Instrument.control( "TRSE?", _trigger_select_normal_command, """Control the trigger, see :meth:`~trigger_select()` documentation.""", get_process=_trigger_select_get_process, validator=_trigger_select_validator, values=_trigger_select_vals, dynamic=True ) def center_trigger(self): """Set the trigger levels to center of the trigger source waveform.""" self.write("SET50") @property def trigger_select(self): """Control the condition that will trigger the acquisition of waveforms (string). Depending on the trigger type, additional parameters must be specified. These additional parameters are grouped in pairs. The first in the pair names the variable to be modified, while the second gives the new value to be assigned. Pairs may be given in any order and restricted to those variables to be changed. There are five parameters that can be specified. Parameters 1. 2. 3. are always mandatory. Parameters 4. 5. are required only for certain combinations of the previous parameters. 1. :={edge, slew, glit, intv, runt, drop} 2. :={c1, c2, c3, c4, line} 3. := * {ti, off} for edge trigger. * {ti} for drop trigger. * {ps, pl, p2, p1} for glit/runt trigger. * {is, il, i2, i1} for slew/intv trigger. 4. := a time value with unit. 5. := a time value with unit. Note: - "line" can only be selected when the trigger type is "edge". - All time arguments should be given in multiples of seconds. Use the scientific notation if necessary. - The range of hold_values varies from trigger types. [80nS, 1.5S] for "edge" trigger, and [2nS, 4.2S] for others. - The trigger_select command is switched automatically between the short, normal and extended version depending on the number of expected parameters. """ return self._trigger_select # noinspection PyAttributeOutsideInit @trigger_select.setter def trigger_select(self, value): num_expected_pars = _trigger_select_num_pars(value) if num_expected_pars == 3: self._trigger_select_set_command = self._trigger_select_short_command elif num_expected_pars == 4: self._trigger_select_set_command = self._trigger_select_normal_command elif num_expected_pars == 5: self._trigger_select_set_command = self._trigger_select_extended_command self._trigger_select = value def trigger_setup(self, mode=None, source=None, trigger_type=None, hold_type=None, hold_value1=None, hold_value2=None, coupling=None, level=None, level2=None, slope=None): """Set up trigger. Unspecified parameters are not modified. Modifying a single parameter might impact other parameters. Refer to oscilloscope documentation and make multiple consecutive calls to trigger_setup and channel_setup if needed. :param mode: trigger sweep mode [auto, normal, single, stop] :param source: trigger source [c1, c2, c3, c4, line] :param trigger_type: condition that will trigger the acquisition of waveforms [edge,slew,glit,intv,runt,drop] :param hold_type: hold type (refer to page 172 of programing guide) :param hold_value1: hold value1 (refer to page 172 of programing guide) :param hold_value2: hold value2 (refer to page 172 of programing guide) :param coupling: input coupling for the selected trigger sources :param level: trigger level voltage for the active trigger source :param level2: trigger lower level voltage for the active trigger source (only slew/runt trigger) :param slope: trigger slope of the specified trigger source """ if mode is not None: self.trigger_mode = mode if all(i is not None for i in [source, trigger_type, hold_type]): args = [trigger_type, source, hold_type] if hold_value1 is not None: args.append(hold_value1) if hold_value2 is not None: args.append(hold_value2) self.trigger_select = tuple(args) elif any(i is not None for i in [source, trigger_type, hold_type]): raise ValueError("Need to specify all of source, trigger_type and hold_type arguments") if source is not None: source = sanitize_source(source) strict_discrete_set(source, ["C1", "C2", "C3", "C4", "LINE"]) ch = self.ch(source) if coupling is not None: ch.trigger_coupling = coupling if level is not None: ch.trigger_level = level if level2 is not None: ch.trigger_level2 = level2 if slope is not None: ch.trigger_slope = slope @property def trigger(self): """Get trigger setup as a dict containing the following keys: - "mode": trigger sweep mode [auto, normal, single, stop] - "trigger_type": condition that will trigger the acquisition of waveforms [edge, slew,glit,intv,runt,drop] - "source": trigger source [c1,c2,c3,c4] - "hold_type": hold type (refer to page 172 of programing guide) - "hold_value1": hold value1 (refer to page 172 of programing guide) - "hold_value2": hold value2 (refer to page 172 of programing guide) - "coupling": input coupling for the selected trigger sources - "level": trigger level voltage for the active trigger source - "level2": trigger lower level voltage for the active trigger source (only slew/runt trigger) - "slope": trigger slope of the specified trigger source """ trigger_select = self.trigger_select ch = self.ch(trigger_select[1]) tb_setup = { "mode": self.trigger_mode, "trigger_type": trigger_select[0], "source": trigger_select[1], "hold_type": trigger_select[2], "hold_value1": trigger_select[3] if len(trigger_select) >= 4 else None, "hold_value2": trigger_select[4] if len(trigger_select) >= 5 else None, "coupling": ch.trigger_coupling, "level": ch.trigger_level, "level2": ch.trigger_level2, "slope": ch.trigger_slope } return tb_setup ############### # Math # ############### ############### # Measure # ############### def display_parameter(self, parameter, channel): """Same as the display_parameter method in the Channel subclass.""" self.ch(channel).display_parameter = parameter def measure_parameter(self, parameter, channel): """ Same as the measure_parameter method in the Channel subclass """ # noinspection PyArgumentList return self.ch(channel).measure_parameter(parameter) ############### # Display # ############### intensity = Instrument.control( "INTS?", "INTS GRID,%d,TRACE,%d", """Set the intensity level of the grid or the trace in percent """, validator=_intensity_validator, values=[[0, 100], [0, 100]], get_process=lambda v: {v[0]: v[1], v[2]: v[3]} ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4216058 pymeasure-0.14.0/pymeasure/instruments/temptronic/0000755000175100001770000000000014623331176022012 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/temptronic/__init__.py0000644000175100001770000000243414623331163024122 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .temptronic_base import ATSBase from .temptronic_ats525 import ATS525 from .temptronic_ats545 import ATS545 from .temptronic_eco560 import ECO560 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/temptronic/temptronic_ats525.py0000644000175100001770000000356014623331163025653 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ Implementation of an interface class for ThermoStream® Systems devices. Reference Document for implementation: ATS-515/615, ATS 525/625 & ATS 535/635 ThermoStream® Systems Interface & Applications Manual Revision E September, 2019 """ from pymeasure.instruments.temptronic.temptronic_base import ATSBase from pymeasure.instruments.instrument import Instrument class ATS525(ATSBase): """Represent the TemptronicATS525 instruments. """ temperature_limit_air_low_values = [-60, 25] system_current = Instrument.measurement( "AMPS?", """Operating current. """, ) def __init__(self, adapter, name="Temptronic ATS-525 Thermostream", **kwargs): super().__init__(adapter, name, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/temptronic/temptronic_ats545.py0000644000175100001770000000467214623331163025662 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ Implementation of an interface class for ThermoStream® Systems devices. Reference Document for implementation: ATS-545 & -645 THERMOSTREAM Interface & Applications Manual Revision B November, 2015 """ from pymeasure.instruments.temptronic.temptronic_base import ATSBase class ATS545(ATSBase): """Represents the TemptronicATS545 instrument. Coding example .. code-block:: python ts = ATS545('ASRL3::INSTR') # replace adapter address ts.configure() # basic configuration (defaults to T-DUT) ts.start() # starts flow (head position not changed) ts.set_temperature(25) # sets temperature to 25 degC ts.wait_for_settling() # blocks script execution and polls for settling ts.shutdown(head=False) # disables thermostream, keeps head down """ temperature_limit_air_low_values = [-80, 25] mode_values = {'manual': 10, # 5 in ATSbase 'program': 0, # 6 in ATSbase 'initial': 63}, # after power up, reading is 63 def next_setpoint(self): """not implemented in ATS545 set ``self.set_point_number`` instead """ raise NotImplementedError def __init__(self, adapter, name="Temptronic ATS-545 Thermostream", **kwargs): super().__init__(adapter, name, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/temptronic/temptronic_base.py0000644000175100001770000005441214623331163025544 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """Implementation of an interface driver for ThermoStream® (TS) Systems devices. # Reference Document for implementation: ATS-515/615, ATS 525/625 & ATS 535/635 ThermoStream® Systems Interface & Applications Manual Revision E September, 2019 # Safety hints In case of script error, make sure thermostream will be shut down. This can be established by e.g. means of try, finally statements. Another way is by polling ``error_code`` integer status flags. No automatic safety measures are part of this driver implementation. """ import logging import time from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import (strict_discrete_set, truncated_range, strict_range ) from enum import IntFlag log = logging.getLogger(__name__) # https://docs.python.org/3/howto/logging.html#library-config log.addHandler(logging.NullHandler()) class TemperatureStatusCode(IntFlag): """Temperature status enums based on ``IntFlag`` Used in conjunction with :attr:`~.temperature_condition_status_code`. ====== ====== Value Enum ====== ====== 32 CYCLING_STOPPED 16 END_OF_ALL_CYCLES 8 END_OF_ONE_CYCLE 4 END_OF_TEST 2 NOT_AT_TEMPERATURE 1 AT_TEMPERATURE 0 NO_STATUS ====== ====== """ CYCLING_STOPPED = 32 # bit 5 -- cycling stopped("stop on fail" signal was received) END_OF_ALL_CYCLES = 16 # bit 4 -- end of all cycles END_OF_ONE_CYCLE = 8 # bit 3 -- end of one cycle END_OF_TEST = 4 # bit 2 -- end of test (test time has elapsed) NOT_AT_TEMPERATURE = 2 # bit 1 -- not at temperature AT_TEMPERATURE = 1 # bit 0 -- at temperature (soak time has elapsed) NO_STATUS = 0 # bit 0 -- no temperature status indication class ErrorCode(IntFlag): """Error code enums based on ``IntFlag``. Used in conjunction with :attr:`~.error_code`. ====== ====== Value Enum ====== ====== 16384 NO_DUT_SENSOR_SELECTED 4096 BVRAM_FAULT 2048 NVRAM_FAULT 1024 NO_LINE_SENSE 512 FLOW_SENSOR_HARDWARE_ERROR 128 INTERNAL_ERROR 32 AIR_SENSOR_OPEN 16 LOW_INPUT_AIR_PRESSURE 8 LOW_FLOW 2 AIR_OPEN_LOOP 1 OVERHEAT 0 OK ====== ====== """ # bit 15 – reserved NO_DUT_SENSOR_SELECTED = 16384 # bit 14 – no DUT sensor selected # bit 13 – reserved BVRAM_FAULT = 4096 # bit 12 – BVRAM fault NVRAM_FAULT = 2048 # bit 11 – NVRAM fault NO_LINE_SENSE = 1024 # bit 10 – No Line Sense FLOW_SENSOR_HARDWARE_ERROR = 512 # bit 9 – flow sensor hardware error # bit 8 – reserved INTERNAL_ERROR = 128 # bit 7 – internal error # bit 6 – reserved AIR_SENSOR_OPEN = 32 # bit 5 – air sensor open LOW_INPUT_AIR_PRESSURE = 16 # bit 4 – low input air pressure LOW_FLOW = 8 # bit 3 – low flow # bit 2 – reserved AIR_OPEN_LOOP = 2 # bit 1 – air open loop OVERHEAT = 1 # bit 0 – overheat OK = 0 # ok state class ATSBase(SCPIUnknownMixin, Instrument): """The base class for Temptronic ATSXXX instruments. """ def __init__(self, adapter, name="ATSBase", **kwargs): super().__init__(adapter, name=name, **kwargs) def wait_for(self, query_delay=None): super().wait_for(0.05 if query_delay is None else query_delay) remote_mode = Instrument.setting( "%s", """``True`` disables TS GUI but displays a “Return to local" switch.""", validator=strict_discrete_set, values={True: "%RM", False: r"%GL"}, map_values=True ) maximum_test_time = Instrument.control( "TTIM?", "TTIM %g", """Control maximum allowed test time (s). :type: float This prevents TS from staying at a single temperature forever. Valid range: 0 to 9999 """, validator=truncated_range, values=[0, 9999] ) dut_mode = Instrument.control( "DUTM?", "DUTM %g", """ ``On`` enables DUT mode, ``OFF`` enables air mode :type: string """, validator=strict_discrete_set, values={'ON': 1, 'OFF': 0}, map_values=True ) dut_type = Instrument.control( "DSNS?", "DSNS %g", """Control DUT sensor type. :type: string Possible values are: ====== ====== String Meaning ====== ====== '' no DUT 'T' T-DUT 'K' K-DUT ====== ====== Warning: If in DUT mode without DUT being connected, TS flags DUT error """, validator=strict_discrete_set, values={None: 0, 'T': 1, 'K': 2}, map_values=True ) dut_constant = Instrument.control( "DUTC?", "DUTC %g", """Control thermal constant (default 100) of DUT. :type: float Lower values indicate lower thermal mass, higher values indicate higher thermal mass respectively. """, validator=truncated_range, values=[20, 500] ) head = Instrument.control( "HEAD?", "HEAD %s", """Control TS head position. :type: string ``down``: transfer head to lower position ``up``: transfer head to elevated position """, validator=strict_discrete_set, values={'up': 0, 'down': 1}, map_values=True ) enable_air_flow = Instrument.setting( "FLOW %g", """Set TS air flow. ``True`` enables air flow, ``False`` disables it :type: bool """, validator=strict_discrete_set, map_values=True, values={True: 1, False: 0} ) temperature_limit_air_low = Instrument.control( "LLIM?", "LLIM %g", """Control lower air temperature limit. :type: float Valid range between -99 to 25 (°C). Setpoints below current value cause “out of range” error in TS. """, validator=truncated_range, values=[-99, 25], dynamic=True ) temperature_limit_air_high = Instrument.control( "ULIM?", "ULIM %g", """upper air temperature limit. :type: float Valid range between 25 to 255 (°C). Setpoints above current value cause “out of range” error in TS. """, validator=truncated_range, values=[25, 225] ) temperature_limit_air_dut = Instrument.control( "ADMD?", "ADMD %g", """Air to DUT temperature limit. :type: float Allowed difference between nozzle air and DUT temperature during settling. Valid range between 10 to 300 °C in 1 degree increments. """, validator=truncated_range, values=[10, 300] ) temperature_setpoint = Instrument.control( "SETP?", "SETP %g", """Set or get selected setpoint's temperature. :type: float Valid range is -99.9 to 225.0 (°C) or as indicated by :attr:`~.temperature_limit_air_high` and :attr:`~.temperature_limit_air_low`. Use convenience function :meth:`~ATSBase.set_temperature` to prevent unexpected behavior. """, validator=truncated_range, values=[-99.9, 225] ) temperature_setpoint_window = Instrument.control( "WNDW?", "WNDW %g", """Setpoint's temperature window. :type: float Valid range is between 0.1 to 9.9 (°C). Temperature status register flags ``at temperature`` in case soak time elapsed while temperature stays in between bounds given by this value around the current setpoint. """, validator=truncated_range, values=[0.1, 9.9] ) temperature_soak_time = Instrument.control( "SOAK?", "SOAK %g", """ Set the soak time for the currently selected setpoint. :type: float Valid range is between 0 to 9999 (s). Lower values shorten cycle times. Higher values increase cycle times, but may reduce settling errors. See :attr:`~.temperature_setpoint_window` for further information. """, validator=truncated_range, values=[0.0, 9999] ) temperature = Instrument.measurement( "TEMP?", """Read current temperature with 0.1 °C resolution. :type: float Temperature readings origin depends on :attr:`dut_mode` setting. Reading higher than 400 (°C) indicates invalidity. """ ) temperature_condition_status_code = Instrument.measurement( "TECR?", """Temperature condition status register. :type: :class:`.TemperatureStatusCode` """, values=[0, 255], get_process=lambda v: TemperatureStatusCode(int(v)), ) set_point_number = Instrument.control( "SETN?", "SETN %g", """Select a setpoint to be the current setpoint. :type: int Valid range is 0 to 17 when on the Cycle screen or or 0 to 2 in case of operator screen (0=hot, 1=ambient, 2=cold). """, validator=truncated_range, values=[0, 17] ) local_lockout = Instrument.setting( "%s", """``True`` disables TS GUI, ``False`` enables it. """, validator=strict_discrete_set, values={True: r"%LL", False: r"%GL"}, map_values=True ) auxiliary_condition_code = Instrument.measurement( "AUXC?", """Read out auxiliary condition status register. :type: int Relevant flags are: ====== ====== Bit Meaning ====== ====== 10 None 9 Ramp mode 8 Mode: 0 programming, 1 manual 7 None 6 TS status: 0 start-up, 1 ready 5 Flow: 0 off, 1 on 4 Sense mode: 0 air, 1 DUT 3 Compressor: 0 on, 1 off (heating possible) 2 Head: 0 lower, upper 1 None 0 None ====== ====== Refer to chapter 4 in the manual """, ) copy_active_setup_file = Instrument.setting( "CFIL %g", """Copy active setup file (0) to setup n (1 - 12). :type: int """, validator=strict_range, values=[1, 12] ) compressor_enable = Instrument.setting( "COOL %g", """ ``True`` enables compressors, ``False`` disables it. :type: Boolean """, validator=strict_discrete_set, map_values=True, values={True: 1, False: 0} ) total_cycle_count = Instrument.control( "CYCC?", "CYCC %g", """Set or read current cycle count (1 - 9999). :type: int Sending 0 will stop cycling """, validator=truncated_range, values=[0, 9999] ) cycling_enable = Instrument.setting( "CYCL %g", """CYCL Start/stop cycling. :type: bool cycling_enable = True (start cycling) cycling_enable = False (stop cycling) """, validator=strict_discrete_set, map_values=True, values={True: 1, False: 0} ) current_cycle_count = Instrument.measurement( "CYCL?", """Read the number of cycles to do :type: int """, ) error_code = Instrument.measurement( "EROR?", # it is indeed EROR """Read the device-specific error register (16 bits). :type: :class:`ErrorCode` """, get_process=lambda v: ErrorCode(int(v)), ) nozzle_air_flow_rate = Instrument.measurement( "FLWR?", """Read main nozzle air flow rate in scfm. """ ) main_air_flow_rate = Instrument.measurement( "FLRL?", """Read main nozzle air flow rate in liters/sec. """ ) learn_mode = Instrument.control( "LRNM?", "LRNM %g", """Control DUT automatic tuning (learning). :type: bool ``False``: off ``True``: automatic tuning on """, validator=strict_discrete_set, map_values=True, values={True: 1, False: 0} ) ramp_rate = Instrument.control( "RAMP?", "RAMP %g", """Control ramp rate (K / min). :type: float allowed values: nn.n: 0 to 99.9 in 0.1 K per minute steps. nnnn: 100 to 9999 in 1 K per minute steps. """, validator=strict_discrete_set, values={i/10 for i in range(1000)} | {i for i in range(100, 10000)} ) dynamic_temperature_setpoint = Instrument.measurement( "SETD?", """Read the dynamic temperature setpoint. :type: float """ ) load_setup_file = Instrument.setting( "SFIL %g", """loads setup file SFIL. Valid range is between 1 to 12. :type: int """, validator=strict_range, values=[1, 12] ) temperature_event_status = Instrument.measurement( "TESR?", """ temperature event status register. :type: :class:`.TemperatureStatusCode` Hint: Reading will clear register content. """, ) air_temperature = Instrument.measurement( "TMPA?", """Read air temperature in 0.1 °C increments. :type: float """ ) dut_temperature = Instrument.measurement( "TMPD?", """Read DUT temperature, in 0.1 °C increments. :type: float """ ) mode = Instrument.measurement( "WHAT?", """Returns a string indicating what the system is doing at the time the query is processed. :type: string """, values={'manual': 5, 'program': 6, }, map_values=True, dynamic=True ) def reset(self): """Reset (force) the System to the Operator screen. :returns: self """ self.write("RSTO") return self def enter_cycle(self): """Enter Cycle by sending ``RMPC 1``. :returns: self """ self.write("RMPC 1") return self def enter_ramp(self): """Enter Ramp by sending ``RMPS 0``. :returns: self """ self.write("RMPS 0") return self def clear(self): """Clear device-specific errors. See :attr:`~.error_code` for further information. """ self.write("CLER") return self def next_setpoint(self): """Step to the next setpoint during temperature cycling. """ self.write("NEXT") def configure(self, temp_window=1, dut_type='T', soak_time=30, dut_constant=100, temp_limit_air_low=-60, temp_limit_air_high=220, temp_limit_air_dut=50, maximum_test_time=1000 ): """Convenience method for most relevant configuration properties. :param dut_type: string: indicating which DUT type to use :param soak_time: float: elapsed time in soak_window before settling is indicated :param soak_window: float: Soak window size or temperature settlings bounds (K) :param dut_constant: float: time constant of DUT, higher values indicate higher thermal mass :param temp_limit_air_low: float: minimum flow temperature limit (°C) :param temp_limit_air_high: float: maximum flow temperature limit (°C) :param temp_limit_air_dut: float: allowed temperature difference (K) between DUT and Flow :param maximum_test_time: float: maximum test time (seconds) for a single temperature point (safety) :returns: self """ self.temperature_setpoint_window = temp_window self.temperature_limit_air_low = temp_limit_air_low self.temperature_limit_air_high = temp_limit_air_high self.dut_type = dut_type self.maximum_test_time = maximum_test_time if dut_type is None: self.dut_mode = 'OFF' else: self.dut_constant = dut_constant self.dut_mode = 'ON' self.temperature_limit_air_dut = temp_limit_air_dut self.temperature_soak_time = soak_time # logging: wd = self.temperature_setpoint_window airflwlimlow = self.temperature_limit_air_low airflwlimhigh = self.temperature_limit_air_high dut = self.dut_type tst_time = self.maximum_test_time airdutlim = self.temperature_limit_air_dut sktime = self.temperature_soak_time message = ( "Configuring TS finished, reading back:\n" f"DUT type: {dut}\n" f"Temperature Window: {wd} K\n" f"Maximum test time: {tst_time} s\n" f"Air flow temperature limit low: {airflwlimlow:.1f} K\n" f"Air flow temperature limit high: {airflwlimhigh:.1f} K\n" f"Air to DUT temperature limit: {airdutlim} degC\n" f"Soak time {sktime} s\n" ) log.info(message) return self def set_temperature(self, set_temp): """sweep to a specified setpoint. :param set_temp: target temperature for DUT (float) :returns: self """ if self.mode == 'manual': message = f"new set point temperature: {set_temp:.1f} Deg" log.info(message) if set_temp <= 20: self.set_point_number = 2 # cold elif set_temp < 30: self.set_point_number = 1 # ambient elif set_temp >= 30: self.set_point_number = 0 # hot else: raise ValueError(f"Temperature {set_temp} is impossible to set!") self.temperature_setpoint = set_temp # fixed typo in attr name return self def wait_for_settling(self, time_limit=300): """block script execution until TS is settled. :param time_limit: set the maximum blocking time within TS has to settle (float). :returns: self Script execution is blocked until either TS has settled or time_limit has been exceeded (float). """ time.sleep(1) t = 0 t_start = time.time() while not self.at_temperature(): # assert at temperature time.sleep(1) t = time.time() - t_start tstatus = self.temperature_condition_status_code message = ("temp_set= %4.1f deg, " "temp= %4.1f deg, " "time= %.2f s, " "status= %s" ) log.info(message, self.temperature_setpoint, self.temperature, t, tstatus) if t > time_limit: log.info('no settling achieved') break log.info('finished this temperature point') return self def shutdown(self, head=False): """Turn down TS (flow and remote operation). :param head: Lift head if ``True`` :returns: self """ self.enable_air_flow = 0 self.remote_mode = False if head: self.head = 'up' super().shutdown() return self def start(self, enable_air_flow=True): """start TS in remote mode. :param enable_air_flow: flow starts if ``True`` :returns: self """ self.remote_mode = 1 self.enable_air_flow = enable_air_flow # enable TS return self def error_status(self): """Returns error status code (maybe used for logging). :returns: :class:`ErrorCode` """ code = self.error_code if not code == 0: log.warning('%s', code) return code def cycling_stopped(self): """:returns: ``True`` if cycling has stopped. """ return TemperatureStatusCode.CYCLING_STOPPED in self.temperature_condition_status_code def end_of_all_cycles(self): """:returns: ``True`` if cycling has stopped. """ return TemperatureStatusCode.END_OF_ALL_CYCLES in self.temperature_condition_status_code def end_of_one_cycle(self): """:returns: ``True`` if TS is at end of one cycle. """ return TemperatureStatusCode.END_OF_ONE_CYCLE in self.temperature_condition_status_code def end_of_test(self): """:returns: ``True`` if TS is at end of test. """ return TemperatureStatusCode.END_OF_TEST in self.temperature_condition_status_code def not_at_temperature(self): """:returns: ``True`` if not at temperature. """ return TemperatureStatusCode.NOT_AT_TEMPERATURE in self.temperature_condition_status_code def at_temperature(self): """:returns: ``True`` if at temperature. """ return TemperatureStatusCode.AT_TEMPERATURE in self.temperature_condition_status_code ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/temptronic/temptronic_eco560.py0000644000175100001770000000734414623331163025635 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ Implementation of an interface class for ThermoStream® Systems devices. Reference Document for implementation: ECO-560/660 ThermoStream® Systems Operators Manual Revision B September, 2018 """ from pymeasure.instruments.temptronic.temptronic_base import ATSBase from enum import IntFlag class ECO560ErrorCode(IntFlag): """Error code enums based on ``IntFlag``. Used in conjunction with :attr:`~.error_code`. ====== ====== Value Enum ====== ====== 16384 NO_DUT_SENSOR_SELECTED 8192 IMPROPER_SOFTWARE_VERSION 1024 PURGE_HEAT_FAILURE 512 FLOW_SENSOR_HARDWARE_ERROR 256 DUT_OPEN_LOOP 128 INTERNAL_ERROR 64 OPEN_PURGE_TEMPERATURE_SENSOR 32 AIR_SENSOR_OPEN 16 LOW_INPUT_AIR_PRESSURE 8 LOW_FLOW 4 SETPOINT_OUT_OF_RANGE 2 AIR_OPEN_LOOP 1 OVERHEAT 0 OK ====== ====== """ # bit 15 - reserved NO_DUT_SENSOR_SELECTED = 16384 # bit 14 – no DUT sensor selected IMPROPER_SOFTWARE_VERSION = 8192 # bit 13 – software revision error # bit 12 – reserved # bit 11 – reserved PURGE_HEAT_FAILURE = 1024 # bit 10 – purge heat failure FLOW_SENSOR_HARDWARE_ERROR = 512 # bit 9 – flow sensor hardware error DUT_OPEN_LOOP = 256 # bit 8 – dut open loop INTERNAL_ERROR = 128 # bit 7 – internal error OPEN_PURGE_TEMPERATURE_SENSOR = 64 # bit 6 – open purge temperature sensor NO_PURGE_FLOW = 32 # bit 5 – no purge flow LOW_INPUT_AIR_PRESSURE = 16 # bit 4 – low input air pressure LOW_FLOW = 8 # bit 3 – low flow SETPOINT_OUT_OF_RANGE = 4 # bit 2 – setpoint out of range AIR_OPEN_LOOP = 2 # bit 1 – air open loop OVERHEAT = 1 # bit 0 – overheat OK = 0 # ok state class ECO560(ATSBase): """Represent the TemptronicECO560 instruments. """ temperature_limit_air_low_values = [-150, 25] error_code_get_process = lambda v: ECO560ErrorCode(int(v)) # noqa: E731 copy_active_setup_file = None # Not Implemented in ECO-560 def __init__(self, adapter, name="Temptronic ECO-560 Thermostream", **kwargs): kwargs.setdefault('timeout', 3000) super().__init__( adapter, name, tcpip={'write_termination': '\n', 'read_termination': '\n'}, **kwargs ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4216058 pymeasure-0.14.0/pymeasure/instruments/texio/0000755000175100001770000000000014623331176020756 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/texio/__init__.py0000644000175100001770000000226014623331163023063 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .texioPSW360L30 import TexioPSW360L30 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/texio/texioPSW360L30.py0000644000175100001770000000624114623331163023561 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from pymeasure.instruments.keithley.keithley2260B import Keithley2260B log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class TexioPSW360L30(Keithley2260B): r""" Represents the TEXIO PSW-360L30 Power Supply (minimal implementation) and provides a high-level interface for interacting with the instrument. For a connection through tcpip, the device only accepts connections at port 2268, which cannot be configured otherwise. example connection string: 'TCPIP::xxx.xxx.xxx.xxx::2268::SOCKET' For a connection through USB on Linux, the kernel is going to create a /dev/ttyACMX device automatically. The serial connection properties are fixed at 9600–8-N-1. The read termination for this interface is Line-Feed \n. This driver inherits from the Keithley2260B one. All instructions implemented in the Keithley 2260B driver are also available for the TEXIO PSW-360L30 power supply. The only addition is the "output" property that is just an alias for the "enabled" property of the Keithley 2260B. Calling the output switch "enabled" is confusing because it is not clear if the whole device is enabled/disable or only the output. .. code-block:: python source = TexioPSW360L30("TCPIP::xxx.xxx.xxx.xxx::2268::SOCKET") source.voltage = 1 print(source.voltage) print(source.current) print(source.power) print(source.applied) """ def __init__(self, adapter, name="TEXIO PSW-360L30 Power Supply", **kwargs): super().__init__(adapter, name, **kwargs) def check_errors(self): """ Logs any system errors reported by the instrument. """ code, message = self.next_error while code != 0: t = time.time() log.info("TEXIO PSW-360L30 reported error: %d, %s" % (code, message)) code, message = self.next_error if (time.time() - t) > 10: log.warning("Timed out for TEXIO PSW-360L30 error retrieval.") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4216058 pymeasure-0.14.0/pymeasure/instruments/thermotron/0000755000175100001770000000000014623331176022027 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/thermotron/__init__.py0000644000175100001770000000226014623331163024134 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .thermotron3800 import Thermotron3800 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/thermotron/thermotron3800.py0000644000175100001770000001235314623331163025115 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 enum import IntFlag class Thermotron3800(Instrument): """ Represents the Thermotron 3800 Oven. For now, this driver only supports using Control Channel 1. There is a 1000ms built in wait time after all write commands. """ def __init__(self, adapter, name="Thermotron 3800", **kwargs): super().__init__( adapter, name, includeSCPI=False, **kwargs ) def write(self, command): super().write(command) # Insert wait time after sending command. # This wait time should be >1000ms for consistent results. sleep(1) id = Instrument.measurement( "IDEN?", """ Get the instrument identification :return: String """ ) temperature = Instrument.measurement( "PVAR1?", """ Get the current temperature of the oven via built in thermocouple. Default unit is Celsius, unless changed by the user. :return: float """ ) mode = Instrument.measurement( "MODE?", """ Get the operating mode of the oven. :return: Tuple(String, int) """, get_process=lambda mode: Thermotron3800.__translate_mode(mode) ) setpoint = Instrument.control( "SETP1?", "SETP1,%g", """Control the setpoint of the oven in Celsius. (float) "setpoint" will not update until the "run()" command is called. After setpoint is set to a new value, the "run()" command must be called to tell the oven to run to the new temperature. :return: None """, validator=strict_range, values=[-55, 150] ) def run(self): ''' Starts temperature forcing. The oven will ramp to the setpoint. :return: None ''' self.write("RUNM") def stop(self): ''' Stops temperature forcing on the oven. :return: None ''' self.write("STOP") def initalize_oven(self, wait=True): ''' The manufacturer recommends a 3 second wait time after after initializing the oven. The optional "wait" variable should remain true, unless the 3 second wait time is taken care of on the user end. The wait time is split up in the following way: 1 second (built into the write function) + 2 seconds (optional wait time from this function (initialize_oven)). :return: None ''' self.write("INIT") if wait: sleep(2) class Thermotron3800Mode(IntFlag): """ +--------+--------------------------------------+ | Bit | Mode | +========+======================================+ | 0 | Program mode | +--------+--------------------------------------+ | 1 | Edit mode (controller in stop mode) | +--------+--------------------------------------+ | 2 | View program mode | +--------+--------------------------------------+ | 3 | Edit mode (controller in hold mode) | +--------+--------------------------------------+ | 4 | Manual mode | +--------+--------------------------------------+ | 5 | Delayed start mode | +--------+--------------------------------------+ | 6 | Unused | +--------+--------------------------------------+ | 7 | Calibration mode | +--------+--------------------------------------+ """ PROGRAM_MODE = 1 EDIT_MODE_STOP = 2 VIEW_PROGRAM_MODE = 4 EDIT_MODE_HOLD = 8 MANUAL_MODE = 16 DELAYED_START_MODE = 32 UNUSED = 64 CALIBRATION_MODE = 128 @staticmethod def __translate_mode(mode_coded_integer): mode = Thermotron3800.Thermotron3800Mode(int(mode_coded_integer)) return mode ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4216058 pymeasure-0.14.0/pymeasure/instruments/thorlabs/0000755000175100001770000000000014623331176021444 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/thorlabs/__init__.py0000644000175100001770000000234114623331163023551 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/thorlabs/thorlabspm100usb.py0000644000175100001770000001121114623331163025114 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import truncated_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class ThorlabsPM100USB(SCPIUnknownMixin, Instrument): """Represents Thorlabs PM100USB powermeter.""" def __init__(self, adapter, name="ThorlabsPM100USB powermeter", **kwargs): super().__init__( adapter, name, **kwargs ) self._set_flags() wavelength_min = Instrument.measurement( "SENS:CORR:WAV? MIN", "Measure minimum wavelength, in nm" ) wavelength_max = Instrument.measurement( "SENS:CORR:WAV? MAX", "Measure maximum wavelength, in nm" ) @property def wavelength(self): """Control the wavelength in nm.""" value = self.values("SENSE:CORR:WAV?")[0] return value @wavelength.setter def wavelength(self, value): """Wavelength in nm.""" if self.wavelength_settable: # Store min and max wavelength to only request them once. if not hasattr(self, "_wavelength_min"): self._wavelength_min = self.wavelength_min if not hasattr(self, "_wavelength_max"): self._wavelength_max = self.wavelength_max value = truncated_range( value, [self._wavelength_min, self._wavelength_max] ) self.write(f"SENSE:CORR:WAV {value}") else: raise AttributeError( f"{self.sensor_name} does not allow setting the wavelength." ) @property def power(self): """Measure the power in W.""" if self.is_power_sensor: return self.values("MEAS:POW?")[0] else: raise AttributeError(f"{self.sensor_name} is not a power sensor.") @property def energy(self): """Measure the energy in J.""" if self.is_energy_sensor: return self.values("MEAS:ENER?")[0] else: raise AttributeError( f"{self.sensor_name} is not an energy sensor." ) def _set_flags(self): """Get sensor info and write flags.""" response = self.values("SYST:SENSOR:IDN?") if response[0] == "no sensor": raise OSError("No sensor connected.") 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] _flags_str = response[5] # interpretation of the flags, see p. 49 of the manual: # https://www.thorlabs.de/_sd.cfm?fileName=17654-D02.pdf&partNumber=PM100D # Convert to binary representation and pad zeros to 9 bit for sensors # where not all flags are present. _flags_str = format(int(_flags_str), "09b") # Reverse the order so it matches the flag order from the manual, i.e. # from decimal values from 1 to 256. _flags_str = _flags_str[::-1] # Convert to boolean. self.flags = [x == "1" for x in _flags_str] # setting the flags; _dn are unused; decimal values as comments ( self.is_power_sensor, # 1 self.is_energy_sensor, # 2 _d4, # 4 _d8, # 8 self.response_settable, # 16 self.wavelength_settable, # 32 self.tau_settable, # 64 _d128, # 128 self.has_temperature_sensor, # 256 ) = self.flags ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/thorlabs/thorlabspro8000.py0000644000175100001770000000656714623331163024677 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import strict_discrete_set class ThorlabsPro8000(SCPIUnknownMixin, Instrument): """Represents Thorlabs Pro 8000 modular laser driver""" SLOTS = range(1, 9) LDC_POLARITIES = ['AG', 'CG'] STATUS = ['ON', 'OFF'] def __init__(self, adapter, name="Thorlabs Pro 8000", **kwargs): super().__init__( adapter, name, **kwargs ) self.write(':SYST:ANSW VALUE') # Code for general purpose commands (mother board related) slot = Instrument.control(":SLOT?", ":SLOT %d", "Control 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", """Control 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", f"""Set laser diode polarity. Allowed values are: {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", f"""Control TEC status. Allowed values are: {STATUS}""", validator=strict_discrete_set, values=STATUS, map_values=False) TEDSetTemperature = Instrument.control(":TEMP:SET?", ":TEMP:SET %g", """Control TEC temperature""") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4216058 pymeasure-0.14.0/pymeasure/instruments/thyracont/0000755000175100001770000000000014623331176021641 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/thyracont/__init__.py0000644000175100001770000000233314623331163023747 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .smartline_v1 import SmartlineV1 from .smartline_v2 import SmartlineV2, VSR, VSH ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/thyracont/smartline_v1.py0000644000175100001770000001265214623331163024621 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from pymeasure.instruments import Instrument def calculate_checksum(msg): """Calculate a 1 bytes checksum mapped to a printable ASCII character. The checksum is calculated by the sum of the decimal values of each message character modulo 64 + 64. :param string msg: message content :returns: calculated checksum :rtype: string """ chksum = sum(map(ord, msg)) % 64 + 64 return chr(chksum) class SmartlineV1(Instrument): """Thyracont Vacuum Instruments Smartline gauges with Communication Protocol V1. Devices using Protocol V1 were manufactured until 2017. Connection to the device is made through an RS485 serial connection. The default communication settings are baudrate 9600, 8 data bits, 1 stop bit, no parity, no handshake. A communication packages is structured as follows: Characters 0-2: Address for communication Character 3: Command character, uppercase letter for reading and lowercase for writing Characters 4-n: Data for the command, can be empty. Character n+1: Checksum calculated by: (sum of the decimal value of bytes 0-n) mod 64 + 64 Character n+2: Carriage return :param adapter: pyvisa resource name of the instrument or adapter instance :param string name: Name of the instrument. :param int address: RS485 adddress of the instrument 1-15. :param int baud_rate: baudrate used for the communication with the device. :param kwargs: Any valid key-word argument for Instrument """ # API is described in detail in this link: # https://wiki.kern.phys.au.dk/Interface_protokoll_Thyracont_eng5.pdf def __init__(self, adapter, name="Thyracont Vacuum Gauge V1", address=1, baud_rate=9600, **kwargs): super().__init__(adapter, name, includeSCPI=False, write_termination="\r", read_termination="\r", asrl=dict(baud_rate=baud_rate), **kwargs) self.address = address def read(self): """Reads a response message from the instrument. This method also checks for a correct checksum. :returns: the data fields :rtype: string :raises ValueError: if a checksum error is detected """ msg = super().read() chksum = calculate_checksum(msg[:-1]) if msg[-1] == chksum: return msg[:-1] else: raise ConnectionError( f"checksum error in received message {msg} " f"with checksum {chksum} but received {msg[-1]}") def write(self, command): """Writes a command to the instrument. This method adds the required address and checksum. :param str command: command to be sent to the instrument """ fullcmd = f"{self.address:03d}" + command super().write(fullcmd + calculate_checksum(fullcmd)) def check_set_errors(self): reply = self.read() if len(reply) < 4: raise ConnectionError(f"Reply of instrument ('{reply}') too short.") if reply[3] in ["N", "X"]: raise ConnectionError(f"Reply from Instrument indicates an error '{reply}'") return [] device_type = Instrument.measurement( "T", """Get the device type.""", cast=str, preprocess_reply=lambda s: s[4:], ) pressure = Instrument.measurement( "M", """Get the pressure measurement in mbar.""", cast=str, preprocess_reply=lambda s: s[4:], get_process=lambda s: float(s[:4])/1000*10**(int(s[4:])-20), ) display_unit = Instrument.control( "U", "u%06d", """Control the display's pressure unit.""", cast=int, preprocess_reply=lambda s: s[4:], values={"mbar": 0, "Torr": 1, "hPa": 2}, map_values=True, validator=strict_discrete_set, ) cathode_enabled = Instrument.control( "I", "i%d", """Control the hot/cold cathode state of the pressure gauge.""", cast=int, preprocess_reply=lambda s: s[4:], values={True: 1, False: 0}, map_values=True, validator=strict_discrete_set, check_set_errors=True, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/thyracont/smartline_v2.py0000644000175100001770000003750514623331163024626 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 enum import IntEnum from pymeasure.instruments import Instrument, Channel, validators from pyvisa.constants import Parity, StopBits from .smartline_v1 import calculate_checksum def compose_data(value): """Generate a string with the length of `value` and the `value` itself afterwards. :param value: Value to send to the device. """ value = f"{value}" return f"{len(value):02}{value}" class Sources(IntEnum): COMBINATION = 0 PIRANI = 1 PIEZO = 2 HOT_CATHODE = 3 COLD_CATHODE = 4 AMBIENT = 6 RELATIVE = 7 def str_to_source(source_string): """Turn a string with a source number to a `Sources` enum. Useful for `cast` parameter.""" return Sources(int(source_string)) gas_factor = Channel.control( "0C{ch}00", "0C{ch}%s", "Control the gas correction factor.", values=(0.2, 8), validator=validators.strict_range, set_process=compose_data, ) class SensorChannel(Channel): """Generic channel for individual pressure sensors of a Transmitter.""" _id = -1 # obligatory channel number, define in channel types def __init__(self, parent, id=None, **kwargs): # id parameter is necessary for usage with `ChannelCreator`. if id is None or id == self._id: super().__init__(parent, id=self._id, **kwargs) else: raise ValueError(f"Pirani ID has to be {self._id} for that channel type.") pressure = Channel.measurement( "0M{ch}00", """Get the current pressure in mbar.""", preprocess_reply=lambda r: r.replace("UR", "0").replace("OR", "inf"), ) class Pirani(SensorChannel): """Pirani sensor channel. A Pirani sensor measures the heat transfer in the gas. The reading depends on the gas type. The sensor reading is linear to the real pressure below some threshold, around 1 mbar. You may control a gas factor with :attr:`gas_factor`. """ _id = Sources.PIRANI gas_factor = gas_factor statistics = Channel.measurement( "0PM011", """Get the sensor statistics as a tuple: wear in percent (negative: corrosion, positive: contamination), time since last adjustment in hours.""", preprocess_reply=lambda msg: msg.strip("W"), separator="A", cast=int, get_process=lambda vals: (vals[0], vals[1] / 4), ) class Piezo(SensorChannel): """Piezo sensor channel. A piezo sensor is independent of the gas present.""" _id = Sources.PIEZO class HotCathode(SensorChannel): """Hot cathode sensor channel.""" _id = Sources.HOT_CATHODE filament_mode = Instrument.control( "0FC00", "2FC01%i", docs="""Control which hot cathode filament to use. ("2 if 1 defect", "Filament1", "Filament2", "toggle>1mbar") """, values={"2 if 1 defect": 0, "Filament1": 1, "Filament2": 2, "toggle>1mbar": 3}, validator=validators.strict_discrete_set, map_values=True, cast=int, check_set_errors=True, ) degas = Instrument.control( "0DG00", "2DG01%i", """Control the degas mode.""", values={True: 1, False: 0}, map_values=True, validator=validators.strict_discrete_set, check_set_errors=True, ) sensor_enabled = Instrument.control( "0CC00", "2CC01%i", """Control the state of the cathode.""", values={True: 1, False: 0}, map_values=True, validator=validators.strict_discrete_set, check_set_errors=True, ) active_filament = Instrument.measurement( "0FN00", "Get the current filament number.", cast=int, ) filament_status = Instrument.measurement( "0FS00", """Get the status of the hot cathode filaments.""", values=["Filament 1 and 2 ok", "Filament 1 defective", "Filament 2 defective", "Filament 1 and 2 defective"], map_values=True, ) gas_factor = gas_factor statistics = Channel.measurement( "0PM013", """Get the wear levels in percent as a tuple: filament 1, filament 2.""", preprocess_reply=lambda msg: msg.strip("F"), separator="S", cast=int, ) # cathode status CA, cathode control mode CM class ColdCathode(SensorChannel): """Cold cathode sensor channel.""" _id = Sources.COLD_CATHODE gas_factor = gas_factor class Ambient(SensorChannel): _id = Sources.AMBIENT class Relative(SensorChannel): _id = Sources.RELATIVE class SmartlineV2(Instrument): """ A Thyracont vacuum sensor transmitter of the Smartline V2 series. You may subclass this Instrument and add the appropriate channels, see the following example. .. doctest:: from pymeasure.instruments import Instrument from pymeasure.instruments.thyractont import SmartlineV2 PiezoAndPiraniInstrument(SmartlineV2): piezo = Instrument.ChannelCreator(Piezo) pirani = Instrument.ChannelCreator(Pirani) Communication Protocol v2 via RS485: - Everything is sent as ASCII characters - Package (bytes and usage): - 0-2 address, 3 access code, 4-5 command, 6-7 data length. - if data: 8-n data to be sent, n+1 checksum, n+2 carriage return - if no data: 8 checksum, 9 carriage return - Access codes (request: master->transmitter, response: transmitter->master): - read: 0, 1 - write: 2, 3 - factory default: 4,5 - error: -, 7 - binary 8, 9 - Data length is number of data in bytes (padding with zeroes on left) - Checksum: Add the decimal numbers of the characters before, mod 64, add 64, show as ASCII. :param adress: The device address in the range 1-16. """ Sources = Sources errors = {'NO_DEF': "Invalid command for this device.", '_LOGIC': "Access Code is invalid or illogical command.", '_RANGE': "Value sent is out of range.", 'ERROR1': "Sensor defect or stacked out.", 'SYNTAX': "Wrong syntax or mode in data is invalid for this device.", 'LENGTH': "Length of data is out of expected range.", '_CD_RE': "Calibration Data Read Error.", '_EP_RE': "EEPROM Read Error.", '_UNSUP': "Unsupported Data for that command.", '_SEDIS': "Sensor element disabled."} def __init__(self, adapter, name="Thyracont SmartlineV2 Transmitter", baud_rate=115200, address=1, timeout=250, **kwargs): super().__init__(adapter, name=name, includeSCPI=False, write_termination="\r", read_termination="\r", timeout=timeout, asrl={'baud_rate': baud_rate, 'parity': Parity.none, 'stop_bits': StopBits.one, }, **kwargs ) self.address = address # 1-16 def write(self, command): """Write a command to the device.""" message = f"{self.address:03}{command}" super().write(f"{message}{calculate_checksum(message)}") def write_composition(self, accessCode, command, data=""): """Write a command with an accessCode and optional data to the device. :param accessCode: How to access the device. :param command: Two char command string to send to the device. :param data: Data for the command. """ self.write(f"{accessCode}{command}{compose_data(data)}") def ask(self, command_message, query_delay=None): """Ask for some value and check that the response matches the original command. :param str command_message: Access code, command, length, and content. The command sent is compared to the response command. """ self.write(command_message) self.wait_for(query_delay) return self.read(command_message[1:3]) def ask_manually(self, accessCode, command, data="", query_delay=None): """ Send a message to the transmitter and return its answer. :param accessCode: How to access the device. :param command: Command to send to the device. :param data: Data for the command. :param float query_delay: Time to wait between writing and reading in seconds. :return str: Response from the device after error checking. """ self.write(f"{accessCode}{command}{compose_data(data)}") self.wait_for(query_delay) return self.read(command) def read(self, command=None): """Read from the device and do error checking. :param str command: Original command sent to the device to compare it with the response. None deactivates the check. """ # Sometimes the answer contains 0x00 or values above 127, such that # decoding fails. response = self.read_bytes(-1, break_on_termchar=True) response = response.replace(b"\x00", b"") # b"\r" is the termination character got = response.rstrip(b"\r").decode('ascii', errors="ignore") # Error checking if got[3] == "7": raise ConnectionError(self.errors[got[8:-1]]) if command is not None and got[4:6] != command: raise ConnectionError(f"Wrong response to {command}: '{got}'.") if calculate_checksum(got[:-1]) != got[-1]: raise ConnectionError("Response checksum is wrong.") return got[8:-1] def check_set_errors(self): """Check the errors after setting a property.""" self.read() return [] # no error happened " Main commands" range = Instrument.measurement( "0MR00", """Get the measurement range in mbar.""", preprocess_reply=lambda r: r[1:], separator="L") pressure = Instrument.measurement( "0MV00", """Get the current pressure of the default sensor in mbar""", preprocess_reply=lambda r: r.replace("UR", "0").replace("OR", "inf"), ) # def Relays display_unit = Instrument.control( get_command="0DU00", set_command="2DU%s", docs="""Control the unit shown in the display. ('mbar', 'Torr', 'hPa')""", values=['mbar', 'Torr', 'hPa'], validator=validators.strict_discrete_set, set_process=compose_data, check_set_errors=True, ) display_orientation = Instrument.control( "0DO00", "2DO01%i", """Control the orientation of the display in relation to the pipe ('top', 'bottom').""", values={"top": 0, "bottom": 1}, map_values=True, validator=validators.strict_discrete_set, check_set_errors=True, ) display_data = Instrument.control( "0DD00", "2DD01%i", """Control the display data source (strict SOURCES).""", values=Sources, cast=str_to_source, validator=validators.strict_discrete_set, check_set_errors=True, ) def set_high(self, high=""): """Set the high pressure to `high` pressure in mbar.""" self.ask_manually(2, "AH", high) def set_low(self, low=""): """Set the low pressure to `low` pressure in mbar.""" self.ask_manually(2, "AL", low) " Sensor parameters" def get_sensor_transition(self): """ Get the current sensor transition between sensors. return interpretation: - direct switch at 1 mbar. - continuous switch between 5 and 15 mbar. - F[float]T[float] switch between low and high value. - D[float] switch at value. """ got = self.ask_manually(0, "ST") # VSR/VSL: 0 direct switch at 1 mbar, 1 continuous between 5 to 15 mbar # VSH: 0 direct switch at 4e-4 mbar, 1 continuous between 1e-3 to 2e-3 mbar, # 2 continuous between 2e-3 to 5e-3 mbar mapping = { "0": "direct", "1": "continuous", "2": "continuous 2", } return mapping.get(got, got) def set_default_sensor_transition(self): """Set the senstor transition mode to the default value, depends on the device.""" self.ask_manually(2, "ST", "1") def set_continuous_sensor_transition(self, low, high): """Set the sensor transition mode to "continuous" mode between `low` and `high` (floats).""" self.ask_manually(2, "ST", f"F{low}T{high}") def set_direct_sensor_transition(self, transition_point): """Set the sensor transition to "direct" mode. :param float transition_point: Switch between the sensors at that value. """ self.ask_manually(2, "ST", f"D{transition_point}") " Device Information" # def response delay device_type = Instrument.measurement( "0TD00", """Get the device type, like 'VSR205'.""", cast=str) product_name = Instrument.measurement( "0PN00", """Get the product name (article number).""", cast=str) device_serial = Instrument.measurement( "0SD00", """Get the transmitter device serial number.""", cast=str) sensor_serial = Instrument.measurement( "0SH00", """Get the sensor head serial number.""", cast=str) baud_rate = Instrument.setting( "2BR%s", """Set the device baud rate.""", set_process=compose_data, check_set_errors=True, ) device_address = Instrument.setting( "2DA%s", "Set the device address.", set_process=compose_data, check_set_errors=True, ) device_version = Instrument.measurement( "0VD00", """Get the device hardware version.""", cast=str) firmware_version = Instrument.measurement( "0VF00", """Get the firmware version.""", cast=str) bootloader_version = Instrument.measurement( "0VB00", """Get the bootloader version.""", cast=str) analog_output_setting = Instrument.measurement( "0OC00", "Get current analog output setting. See manual.", cast=str) operating_hours = Instrument.measurement( "0OH00", "Measure the operating hours.", separator="C", cast=int, get_process=lambda vals: vals / 4 if isinstance(vals, int) else [v / 4 for v in vals], # TODO simplify once #740 is merged. ) class VSH(SmartlineV2): """Vacuum transmitter of VSH series with both a pirani and a hot cathode sensor.""" pirani = Instrument.ChannelCreator(Pirani) hotcathode = Instrument.ChannelCreator(HotCathode) class VSR(SmartlineV2): """Vacuum transmitter of VSR/VCR series with both a piezo and a pirani sensor.""" piezo = Instrument.ChannelCreator(Piezo) pirani = Instrument.ChannelCreator(Pirani) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4216058 pymeasure-0.14.0/pymeasure/instruments/toptica/0000755000175100001770000000000014623331176021271 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/toptica/__init__.py0000644000175100001770000000225014623331163023375 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .ibeamsmart import IBeamSmart ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/toptica/ibeamsmart.py0000644000175100001770000002413314623331163023766 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 time from warnings import warn from pyvisa.errors import VisaIOError from pymeasure.instruments import Channel, Instrument from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) def _deprecation_warning(text): def func(x): warn(text, FutureWarning) return x return func def _deprecation_warning_channels(property_name): def func(x): warn(f'Deprecated property name "{property_name}", use the channels ' '"enabled" property instead.', FutureWarning) return x return func def deprecated_strict_discrete_set(value, values): warn("This property is deprecated, use channels instead.", FutureWarning) return strict_discrete_set(value, values) class DriverChannel(Channel): """A laser diode driver channel for the IBeam Smart laser.""" power = Channel.setting( "ch {ch} pow %f mic", """Set the output power in µW (float up to 200000).""", check_set_errors=True, validator=strict_range, values=[0, 200000], ) enabled = Channel.control( "sta ch {ch}", "%s {ch}", """Control the enabled state of the driver channel.""", validator=strict_discrete_set, values=[True, False], get_process=lambda s: True if s == 'ON' else False, set_process=lambda v: "en" if v else "di", check_set_errors=True, ) class IBeamSmart(Instrument): """ IBeam Smart laser diode For the usage of the different diode driver channels, see the manual .. code:: laser = IBeamSmart("SomeResourceString") laser.emission = True laser.ch_2.power = 1000 # µW laser.ch_2.enabled = True laser.shutdown() :param adapter: pyvisa resource name or adapter instance. :param baud_rate: The baud rate you have set in the instrument. :param \\**kwargs: Any valid key-word argument for VISAAdapter. """ _reg_value = re.compile(r"\w+\s+=\s+(\w+)") ch_1 = Instrument.ChannelCreator(DriverChannel, 1) ch_2 = Instrument.ChannelCreator(DriverChannel, 2) ch_3 = Instrument.ChannelCreator(DriverChannel, 3) ch_4 = Instrument.ChannelCreator(DriverChannel, 4) ch_5 = Instrument.ChannelCreator(DriverChannel, 5) def __init__(self, adapter, name="Toptica IBeam Smart laser diode", baud_rate=115200, **kwargs): super().__init__( adapter, name, includeSCPI=False, read_termination='\r\n', write_termination='\r\n', asrl={'baud_rate': baud_rate}, **kwargs ) # configure communication mode: no repeating and no command prompt self.write('echo off') self.write('prom off') time.sleep(0.04) # clear the initial messages from the controller try: self.adapter.flush_read_buffer() except AttributeError: log.warning("Adapter does not have 'flush_read_buffer' method.") self.ask('talk usual') def read(self): """Read a reply of the instrument and extract the values, if possible. Reads a reply of the instrument which consists of at least two lines. The initial ones are the reply to the command while the last one should be '[OK]' which acknowledges that the device is ready to receive more commands. Note: '[OK]' is always returned as last message even in case of an invalid command, where a message indicating the error is returned before the '[OK]' Value extraction: extract from 'name = [unit]'. If can not be identified the original string is returned. :return: string containing the ASCII response of the instrument (without '[OK]'). """ reply = super().read() # read back the LF+CR which is always sent back if reply != "": raise ValueError( f"Error, no empty line at begin of message, instead '{reply}'") msg = [] try: while True: line = super().read() if line == '[OK]': break msg.append(line) except VisaIOError: reply = '\n'.join(msg) try: self.adapter.flush_read_buffer() except AttributeError: log.warning("Adapter does not have 'flush_read_buffer' method.") raise ValueError(f"Flush buffer failed after '{reply}'") reply = '\n'.join(msg) r = self._reg_value.search(reply) if r: return r.groups()[0] else: return reply def check_set_errors(self): """Check for errors after having gotten a property and log them. Checks if the last reply is only '[OK]', otherwise a ValueError is raised and the read buffer is flushed because one has to assume that some communication is out of sync. """ reply = self.read() if reply: # anything else than '[OK]'. self.adapter.flush_read_buffer() log.error(f"Setting a property failed with reply '{reply}'.") raise ValueError(f"Setting a property failed with reply '{reply}'.") return [] version = Instrument.measurement( "ver", """Get Firmware version number.""", ) serial = Instrument.measurement( "serial", """Get Serial number of the laser system.""", ) temp = Instrument.measurement( "sh temp", """Measure the temperature of the laser diode in degree centigrade.""", ) system_temp = Instrument.measurement( "sh temp sys", """Measure base plate (heatsink) temperature in degree centigrade.""", ) current = Instrument.measurement( "sh cur", """Measure the laser diode current in mA.""", ) emission = Instrument.control( "sta la", "la %s", """Control emission status of the laser diode driver (bool).""", validator=strict_discrete_set, values=[True, False], get_process=lambda s: True if s == 'ON' else False, set_process=lambda v: "on" if v else "off", check_set_errors=True, ) laser_enabled = Instrument.control( "sta la", "la %s", """Control emission status of the laser diode driver (bool). .. deprecated:: 0.12 Use attr:`emission` instead. """, validator=deprecated_strict_discrete_set, values=[True, False], get_process=lambda s: True if s == 'ON' else False, set_process=lambda v: "on" if v else "off", check_set_errors=True, preprocess_reply=_deprecation_warning( "Property `laser_enabled` is deprecated, use `emission` instead."), ) channel1_enabled = Instrument.control( "sta ch 1", "%s", """Control status of Channel 1 of the laser (bool). .. deprecated:: 0.12 Use :attr:`ch_1.enabled` instead. """, validator=deprecated_strict_discrete_set, values=[True, False], get_process=lambda s: True if s == 'ON' else False, set_process=lambda v: "en 1" if v else "di 1", check_set_errors=True, preprocess_reply=_deprecation_warning_channels("channel1_enabled"), ) channel2_enabled = Instrument.control( "sta ch 2", "%s", """Control status of Channel 2 of the laser (bool). .. deprecated:: 0.12 Use :attr:`ch_2.enabled` instead.""", validator=deprecated_strict_discrete_set, values=[True, False], get_process=lambda s: True if s == 'ON' else False, set_process=lambda v: "en 2" if v else "di 2", check_set_errors=True, preprocess_reply=_deprecation_warning_channels("channel2_enabled"), ) power = Instrument.control( "sh pow", "ch pow %f mic", """Control actual output power in µW of the laser system. In pulse mode this means that the set value might not correspond to the readback one (float up to 200000).""", validator=strict_range, values=[0, 200000], check_set_errors=True, ) def enable_continous(self): """Enable countinous emmission mode.""" self.write('di ext') self.check_set_errors() self.emission = True self.ch_2.enabled = True def enable_pulsing(self): """Enable pulsing mode. The optical output is controlled by a digital input signal on a dedicated connnector on the device.""" self.emission = True self.ch_2.enabled = True self.write('en ext') self.check_set_errors() def disable(self): """Shutdown all laser operation.""" self.write('di 0') self.check_set_errors() self.emission = False def shutdown(self): """Brings the instrument to a safe and stable state.""" self.disable() super().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/validators.py0000644000175100001770000001455414623331163022355 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 or equal to the maximum and greater than or equal to the minimum of ``values``. 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): """Returns a validator function that represents a list of validators joined together. A value passed to the validator is returned if it passes any validator (not all of them). Otherwise it raises a ValueError. Note: the joined validator expects ``values`` to be a sequence of ``values`` appropriate for the respective validators (often sequences themselves). :Example: >>> from pymeasure.instruments.validators import strict_discrete_set, strict_range >>> from pymeasure.instruments.validators import joined_validators >>> joined_v = joined_validators(strict_discrete_set, strict_range) >>> values = [['MAX','MIN'], range(10)] >>> joined_v(5, values) 5 >>> joined_v('MAX', values) 'MAX' >>> joined_v('NONSENSE', values) Traceback (most recent call last): ... ValueError: Value of NONSENSE does not match any of the joined validators :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(f"Value of {value} does not match any of the joined validators") 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4216058 pymeasure-0.14.0/pymeasure/instruments/velleman/0000755000175100001770000000000014623331176021431 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/velleman/__init__.py0000644000175100001770000000230614623331163023537 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 .velleman_k8090 import VellemanK8090, VellemanK8090Switches ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/velleman/velleman_k8090.py0000644000175100001770000002174414623331163024445 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 enum import IntFlag import logging from pyvisa import VisaIOError from pymeasure.adapters import SerialAdapter, VISAAdapter from pymeasure.instruments import Instrument log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class VellemanK8090Switches(IntFlag): """Use to identify switch channels.""" NONE = 0 CH1 = 1 << 0 CH2 = 1 << 1 CH3 = 1 << 2 CH4 = 1 << 3 CH5 = 1 << 4 CH6 = 1 << 5 CH7 = 1 << 6 CH8 = 1 << 7 ALL = CH1 | CH2 | CH3 | CH4 | CH5 | CH6 | CH7 | CH8 def _parse_channels(channels) -> str: """Convert array of channel numbers into mask if needed.""" if isinstance(channels, list): mask = VellemanK8090Switches.NONE for ch in channels: mask |= 1 << (ch - 1) else: mask = channels return hex(mask) def _get_process_status(items): """Process the result of a 0x51 status message. :param items: List of 4 integers: [CMD, MASK, Param1, Param2] """ if len(items) < 4 or items[0] != 0x51: return None, None, None return [VellemanK8090Switches(it) for it in items[1:]] class VellemanK8090(Instrument): """For usage with the K8090 relay board, by Velleman. View the "K8090/VM8090 PROTOCOL MANUAL" for the serial command instructions. The communication is done by serial USB. The IO settings are fixed: ================== ================== Baud rate 19200 Data bits 8 Parity None Stop bits 1 Flow control None ================== ================== A short timeout is recommended, since the device is not consistent in giving status messages and serial timeouts will occur also in normal operation. Use the class like: .. code-block:: python from pymeasure.instruments.velleman import VellemanK8090, VellemanK8090Switches as Switches instrument = VellemanK8090("ASRL1::INSTR") # Get status update from device last_on, curr_on, time_on = instrument.status # Toggle a selection of channels on instrument.switch_on = Switches.CH3 | Switches.CH4 | Switches.CH5 """ def __init__(self, adapter, name="Velleman K8090", timeout=100, **kwargs): super().__init__( adapter, name=name, asrl={"baud_rate": 19200}, write_termination="", read_termination="", timeout=timeout, includeSCPI=False, **kwargs, ) BYTE_STX = 0x04 BYTE_ETX = 0x0F version = Instrument.measurement( "0x71", """ Get firmware version, as (year - 2000, week). E.g. ``(10, 1)`` """, cast=int, get_process=lambda v: (v[2], v[3]) if len(v) > 3 and v[0] == 0x71 else None, ) status = Instrument.measurement( "0x18", """ Get current relay status. The reply has a different command byte than the request. Three items (:class:`VellemanK8090Switches` flags) are returned: * Previous state: the state of each relay before this event * Current state: the state of each relay now * Timer state: the state of each relay timer """, cast=int, get_process=_get_process_status, ) switch_on = Instrument.setting( "0x11,%s", """ Set channels to on state. Other channels are unaffected. Pass either a list or set of channel numbers (starting at 1), or pass a bitmask. After switching this waits for a reply from the device. This is only send when a relay actually toggles, otherwise expect a blocking time equal to the communication timeout If speed is important, avoid calling `switch_` unnecessarily. """, set_process=_parse_channels, check_set_errors=True, ) switch_off = Instrument.setting( "0x12,%s", """ Set channels to off state. See :attr:`switch_on` for more details. """, set_process=_parse_channels, check_set_errors=True, ) id = None # No identification available def _make_checksum(self, command, mask, param1, param2): # The formula from the sheet requires twos-complement negation, # this works return 1 + 0xFF - ((self.BYTE_STX + command + mask + param1 + param2) & 0xFF) def write(self, command, **kwargs): """The write command specifically for the protocol of the K8090. This overrides the method from the ``Instrument`` class. Each packet to the device is 7 bytes: STX (0x04) - CMD - MASK - PARAM1 - PARAM2 - CHK - ETX (0x0F) Where `CHK` is checksum of the package. :param command: String like "CMD[, MASK, PARAM1, PARAM2]" - only CMD is mandatory :type command: str """ # The device can give status updates when we don't expect it, # drop anything from the buffer first if isinstance(self.adapter, VISAAdapter): self.adapter.flush_read_buffer() elif isinstance(self.adapter, SerialAdapter): # The SerialAdapter does not have `flush_read_buffer` implemented self.adapter.connection.flush() items_str = command.split(",") items = [int(it, 16) for it in items_str] cmd = items[0] mask = items[1] if len(items) > 1 else 0 param1 = items[2] if len(items) > 2 else 0 param2 = items[3] if len(items) > 3 else 0 checksum = self._make_checksum(cmd, mask, param1, param2) content = [ self.BYTE_STX, cmd, mask, param1, param2, checksum, self.BYTE_ETX, ] self.write_bytes(bytes(content)) def read(self, **kwargs): """The read command specifically for the protocol of the K8090. This overrides the method from the ``instrument`` class. See :meth:`write`, replies from the machine use the same format. A read will return a list of CMD, MASK, PARAM1 and PARAM2. """ # A message is always 7 bytes # (there is also a termination char, but since it is not exclusive it cannot be # reliably used) response = self.read_bytes(7) if len(response) < 7: raise ConnectionError(f"Incoming packet was {len(response)} bytes instead of 7") # Only consider the most recent block stx, command, mask, param1, param2, checksum, etx = list(response[-7:]) if stx != self.BYTE_STX or etx != self.BYTE_ETX: raise ConnectionError(f"Received invalid start and stop bytes `{stx}` and `{etx}`") if command == 0x00: raise ConnectionError(f"Received invalid command byte `{command}`") real_checksum = self._make_checksum(command, mask, param1, param2) if real_checksum != checksum: raise ConnectionError( f"Packet checksum was not correct, got {hex(checksum)} " f"instead of {hex(real_checksum)}" ) values_str = [str(v) for v in [command, mask, param1, param2]] return ",".join(values_str) def check_set_errors(self): """Check for errors after having set a property and log them. Called if :code:`check_set_errors=True` is set for that property. The K8090 replies with a status after a switch command, but **only** after any switch actually changed. In order to guarantee the buffer is empty, we attempt to read it fully here. No actual error checking is done here! :return: List of error entries. """ try: self.read() except (VisaIOError, ConnectionError): pass # Ignore a timeout except Exception as exc: log.exception("Setting a property failed.", exc_info=exc) raise else: return [] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4216058 pymeasure-0.14.0/pymeasure/instruments/yokogawa/0000755000175100001770000000000014623331176021447 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/yokogawa/__init__.py0000644000175100001770000000232514623331163023556 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from .yokogawags200 import YokogawaGS200 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/yokogawa/aq6370series.py0000644000175100001770000002462114623331163024156 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIMixin from pymeasure.instruments.validators import strict_discrete_set, strict_range log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class AQ6370Series(SCPIMixin, Instrument): """Represents Yokogawa AQ6370 Series of optical spectrum analyzer.""" def __init__(self, adapter, name="Yokogawa AQ3670D OSA", **kwargs): super().__init__(adapter, name, **kwargs) # Initiate and abort sweep --------------------------------------------------------------------- def abort(self): """Stop operations such as measurements and calibration.""" self.write(":ABORt") def initiate_sweep(self): """Initiate a sweep.""" self.write(":INITiate:IMMediate") # Leveling ------------------------------------------------------------------------------------- reference_level = Instrument.control( ":DISPlay:TRACe:Y1:SCALe:RLEVel?", ":DISPlay:TRACe:Y1:SCALe:RLEVel %g", "Control the reference level of main scale of level axis (float in dBm).", validator=strict_range, values=[-90, 30], ) level_position = Instrument.control( ":DISPlay:TRACe:Y1:RPOSition?", ":DISPlay:TRACe:Y1:RPOSition %g", """Control the reference level position regarding divisions (int, smaller than total number of divisions which is either 8, 10 or 12).""", validator=strict_range, values=[0, 12], dynamic=True, get_process=lambda x: int(x), ) def set_level_position_to_max(self): """Set the reference level position to the maximum value.""" self.write(":CALCulate:MARKer:MAXimum:SRLevel") # Sweep settings ------------------------------------------------------------------------------- sweep_mode = Instrument.control( ":INITiate:SMODe?", ":INITiate:SMODe %s", "Control the sweep mode (str 'SINGLE', 'REPEAT', 'AUTO', 'SEGMENT').", validator=strict_discrete_set, map_values=True, values={"SINGLE": 1, "REPEAT": 2, "AUTO": 3, "SEGMENT": 4}, ) sweep_time_interval = Instrument.control( ":SENSe:SWEep:TIME:INTerval?", ":SENSe:SWEep:TIME:INTerval %g", "Control the sweep time interval (int from 0 to 99999 s).", validator=strict_range, values=[0, 99999], ) automatic_sample_number = Instrument.control( ":SENSe:SWEep:POINts:AUTO?", ":SENSe:SWEep:POINts:AUTO %d", "Control the automatic sample number (bool).", validator=strict_discrete_set, map_values=True, values={False: 0, True: 1}, ) sample_number = Instrument.control( ":SENSe:SWEep:POINts?", ":SENSe:SWEep:POINts %d", "Control the sample number (int from 51 to 50001).", validator=strict_range, values=[101, 50001], get_process=lambda x: int(x), ) # Wavelength settings (all assuming wavelength mode, not frequency mode) ----------------------- wavelength_center = Instrument.control( ":SENSe:WAVelength:CENTer?", ":SENSe:WAVelength:CENTer %g", "Control measurement condition center wavelength (float in m).", validator=strict_range, values=[600e-9, 1700e-9], dynamic=True, ) wavelength_span = Instrument.control( ":SENSe:WAVelength:SPAN?", ":SENSe:WAVelength:SPAN %g", "Control wavelength span (float from 0 to 1100e-9 m).", validator=strict_range, values=[0, 1100e-9], dynamic=True, ) wavelength_start = Instrument.control( ":SENSe:WAVelength:STARt?", ":SENSe:WAVelength:STARt %g", "Control the measurement start wavelength (float from 50e-9 to 2250e-9 in m).", validator=strict_range, values=[50e-9, 1700 - 9], dynamic=True, ) wavelength_stop = Instrument.control( ":SENSe:WAVelength:STOP?", ":SENSe:WAVelength:STOP %g", "Control the measurement stop wavelength (float from 50e-9 to 2250e-9 in m).", validator=strict_range, values=[600e-9, 2250e-9], dynamic=True, ) # Trace operations ----------------------------------------------------------------------------- active_trace = Instrument.control( ":TRACe:ACTive?", ":TRACe:ACTive %d", "Control the active trace (str 'A', 'B', 'C', ...).", ) def copy_trace(self, source, destination): """ Copy the data of specified trace to the another trace. :param source: Source trace (str 'A', 'B', 'C', ...). :param destination: Destination trace (str 'A', 'B', 'C', ...). """ self.write(f":TRACe:COPY TR{source.replace('TR', '')},TR{destination.replace('TR', '')}") def delete_trace(self, trace): """ Delete the specified trace. :param trace: Trace to be deleted (str 'ALL', 'A', 'B', 'C', ...). """ if trace == "ALL": self.write(":TRACe:DELete:ALL") else: self.write(f":TRACe:DELete TR{trace.replace('TR', '')}") def get_xdata(self, trace="TRA"): """ Measure the x-axis data of specified trace, output wavelength in m. :param trace: Trace to measure (str 'A', 'B', 'C', ...). :return: The x-axis data of specified trace. """ return self.values(f":TRACe:X? TR{trace.replace('TR', '')}") def get_ydata(self, trace="TRA"): """ Measure the y-axis data of specified trace, output power in dBm. :param trace: Trace to measure (str 'A', 'B', 'C', ...). :return: The y-axis data of specified trace. """ return self.values(f":TRACe:Y? TR{trace.replace('TR', '')}") # Analysis ------------------------------------------------------------------------------------- def execute_analysis(self): """Execute the analysis with the current analysis settings.""" self.write(":CALCulate") def get_analysis(self): """ Query the analysis results of latest analysis. If no analysis has been performed, returns query error. """ return self.write(":CALCulate:DATA?") # Resolution ----------------------------------------------------------------------------------- resolution_bandwidth = Instrument.control( ":SENSe:BWIDth:RESolution?", ":SENSe:BWIDth:RESolution %g", """Control the measurement resolution (float in m, discrete values: [0.02e-9, 0.05e-9, 0.1e-9, 0.2e-9, 0.5e-9, 1e-9, 2e-9] m).""", validator=strict_discrete_set, values=[0.02e-9, 0.05e-9, 0.1e-9, 0.2e-9, 0.5e-9, 1e-9, 2e-9], dynamic=True, ) # subclasses of specific instruments --------------------------------------------------------------- class AQ6370D(AQ6370Series): """Represents Yokogawa AQ6370D optical spectrum analyzer.""" sweep_speed = Instrument.control( ":SENSe:SWEep:SPEed?", ":SENSe:SWEep:SPEed %d", "Control the sweep speed (str '1x' or '2x' for double speed).", validator=strict_discrete_set, map_values=True, values={"1x": 0, "2x": 1}, ) pass class AQ6370C(AQ6370Series): """Represents Yokogawa AQ6370C optical spectrum analyzer.""" sweep_speed = Instrument.control( ":SENSe:SWEep:SPEed?", ":SENSe:SWEep:SPEed %d", "Control the sweep speed (str '1x' or '2x' for double speed).", validator=strict_discrete_set, map_values=True, values={"1x": 0, "2x": 1}, ) pass class AQ6373(AQ6370Series): """Represents Yokogawa AQ6373 optical spectrum analyzer.""" wavelength_center_values = [350e-9, 1200e-9] # 0.001 steps wavelength_start_values = [1e-9, 1200e-9] # 0.001 steps wavelength_stop_values = [350e-9, 1625e-9] # 0.001 steps wavelength_span_values = [0, 850e-9] # 0.1 steps resolution_bandwidth_values = [ 0.01e-9, 0.02e-9, 0.05e-9, 0.1e-9, 0.2e-9, 0.5e-9, 1e-9, 2e-9, 5e-9, 10e-9, ] pass class AQ6373B(AQ6373): """Represents Yokogawa AQ6373B variant optical spectrum analyzer.""" sweep_speed = Instrument.control( ":SENSe:SWEep:SPEed?", ":SENSe:SWEep:SPEed %d", "Control the sweep speed (str '1x' or '2x' for double speed).", validator=strict_discrete_set, map_values=True, values={"1x": 0, "2x": 1}, ) pass class AQ6375(AQ6370Series): """Represents Yokogawa AQ6375 optical spectrum analyzer.""" wavelength_center_values = [1200e-9, 2400e-9] # 0.001 steps wavelength_start_values = [600e-9, 2400e-9] # 0.001 steps wavelength_stop_values = [1200e-9, 3000e-9] # 0.001 steps wavelength_span_values = [0, 1200e-9] # 0.1 steps resolution_bandwidth_values = [ 0.05e-9, 0.1e-9, 0.2e-9, 0.5e-9, 1e-9, 2e-9, ] pass class AQ6375B(AQ6375): """Represents Yokogawa AQ6375B variant optical spectrum analyzer.""" sweep_speed = Instrument.control( ":SENSe:SWEep:SPEed?", ":SENSe:SWEep:SPEed %d", "Control the sweep speed (str '1x' or '2x' for double speed).", validator=strict_discrete_set, map_values=True, values={"1x": 0, "2x": 1}, ) pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/yokogawa/yokogawa7651.py0000644000175100001770000002126514623331163024167 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 re import numpy as np from pymeasure.instruments import Instrument, SCPIUnknownMixin from pymeasure.instruments.validators import ( truncated_discrete_set, strict_discrete_set, truncated_range ) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) class Yokogawa7651(SCPIUnknownMixin, 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", """Control the source voltage in Volts, if that mode is active. (float)""" ) source_current = Instrument.control( "OD;E", "S%g;E", """Control the source current in Amps, if that mode is active. (float)""" ) source_voltage_range = Instrument.control( "OS;E", "R%d;E", """Control the source voltage range in Volts, which can take values: 10 mV, 100 mV, 1 V, 10 V, and 30 V. Voltages are truncated 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", """Control the current voltage range in Amps, which can take values: 1 mA, 10 mA, and 100 mA. Currents are truncated 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", """Control 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", """Control 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", """Control 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, name="Yokogawa 7651 Programmable DC Source", **kwargs): super().__init__( adapter, name, **kwargs ) self.write("H0;E") # Set no header in output data @property def id(self): """ Get the identification of the instrument """ return self.ask("OS;E").split('\r\n\n')[0] @property def source_enabled(self): """ Get 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().shutdown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/instruments/yokogawa/yokogawags200.py0000644000175100001770000001313514623331163024415 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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, SCPIUnknownMixin from pymeasure.instruments.validators import ( strict_discrete_set, truncated_discrete_set, truncated_range ) log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) MIN_RAMP_TIME = 0.1 # seconds class YokogawaGS200(SCPIUnknownMixin, Instrument): """ Represents the Yokogawa GS200 source and provides a high-level interface for interacting with the instrument. """ source_enabled = Instrument.control( "OUTPut:STATe?", "OUTPut:STATe %d", """Control whether the source is enabled. (bool)""", validator=strict_discrete_set, values={True: 1, False: 0}, map_values=True ) source_mode = Instrument.control( ":SOURce:FUNCtion?", ":SOURce:FUNCtion %s", """Control the source mode. Can be either 'current' or 'voltage'.""", validator=strict_discrete_set, values={'current': 'CURR', 'voltage': 'VOLT'}, get_process=lambda s: s.strip() ) source_range = Instrument.control( ":SOURce:RANGe?", "SOURce:RANGe %g", """Control the range (either in voltage or current) of the output. "Range" refers to the maximum source level. (float)""", validator=truncated_discrete_set, values=[1e-3, 10e-3, 100e-3, 200e-3, 1, 10, 30] ) voltage_limit = Instrument.control( "SOURce:PROTection:VOLTage?", "SOURce:PROTection:VOLTage %g", """Control the voltage limit. "Limit" refers to maximum value of the electrical value that is conjugate to the mode (current is conjugate to voltage, and vice versa). Thus, voltage limit is only applicable when in 'current' mode""", validator=truncated_range, values=[1, 30] ) current_limit = Instrument.control( "SOURce:PROTection:CURRent?", "SOURce:PROTection:CURRent %g", """Control the current limit. "Limit" refers to maximum value of the electrical value that is conjugate to the mode (current is conjugate to voltage, and vice versa). Thus, current limit is only applicable when in 'voltage' mode""", validator=truncated_range, values=[1e-3, 200e-3] ) def __init__(self, adapter, name="Yokogawa GS200 Source", **kwargs): super().__init__( adapter, name, **kwargs ) @property def source_level(self): """ Control the output level, either a voltage or a current, depending on the source mode. (float) """ return float(self.ask(":SOURce:LEVel?")) @source_level.setter def source_level(self, level): if level > self.source_range * 1.2: raise ValueError( "Level must be within 1.2 * source_range, otherwise the Yokogawa will produce an " "error." ) else: self.write("SOURce:LEVel %g" % level) def trigger_ramp_to_level(self, level, ramp_time): """ Ramp the output level from its current value to "level" in time "ramp_time". This method will NOT wait until the ramp is finished (thus, it will not block further code evaluation). :param float level: final output level :param float ramp_time: time in seconds to ramp :return: None """ if not self.source_enabled: raise ValueError( "YokogawaGS200 source must be enabled in order to ramp to a specified level. " "Otherwise, the Yokogawa will reject the ramp." ) if ramp_time < MIN_RAMP_TIME: log.warning( f"Ramp time of {ramp_time}s is below the minimum ramp time of {MIN_RAMP_TIME}s, " f"so the Yokogawa will instead be instantaneously set to the desired level." ) self.source_level = level else: # Use the Yokogawa's "program" mode to create the ramp ramp_program = ( f":program:edit:start;" f":source:level {level};" f":program:edit:end;" ) # set "interval time" equal to "slope time" to make a continuous ramp ramp_program += ( f":program:interval {ramp_time};" f":program:slope {ramp_time};" ) # run it once ramp_program += ( ":program:repeat 0;" ":program:run" ) self.write(ramp_program) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/log.py0000644000175100001770000000746214623331163016373 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/process.py0000644000175100001770000000431714623331163017264 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 "<{}(should_stop={})>".format( self.__class__.__name__, self.should_stop()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/test.py0000644000175100001770000000617514623331163016571 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 contextlib import contextmanager from pymeasure.adapters.protocol import ProtocolAdapter @contextmanager def expected_protocol(instrument_cls, comm_pairs, connection_attributes=None, connection_methods=None, **kwargs): """Context manager that checks sent/received instrument commands without a device connected. Given an instrument class and a list of command-response pairs, this context manager confirms that the code in the context manager block produces the expected messages. Terminators are excluded from the protocol definition, as those are typically a detail of the communication method (i.e. Adapter), and not the protocol itself. :param pymeasure.Instrument instrument_cls: :class:`~pymeasure.instruments.Instrument` subclass to instantiate. :param list[2-tuples[str]] comm_pairs: List of command-response pairs, i.e. 2-tuples like `('VOLT?', '3.14')`. 'None' indicates that a pair member (command or response) does not exist, e.g. `(None, 'RESP1')`. Commands and responses are without termination characters. :param connection_attributes: Dictionary of connection attributes and their values. :param connection_methods: Dictionary of method names of the connection and their return values. :param \\**kwargs: Keyword arguments for the instantiation of the instrument. """ protocol = ProtocolAdapter(comm_pairs, connection_attributes=connection_attributes, connection_methods=connection_methods) instr = instrument_cls(protocol, **kwargs) yield instr assert protocol._index == len(comm_pairs), ( "Unprocessed protocol definitions remain: " f"{comm_pairs[protocol._index:]}.") assert protocol._write_buffer is None, ( f"Non-empty write buffer remains: '{protocol._write_buffer}'.") assert protocol._read_buffer is None, ( f"Non-empty read buffer remains: '{protocol._read_buffer}'.") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/thread.py0000644000175100001770000000514314623331163017053 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 "<{}(should_stop={})>".format( self.__class__.__name__, self.should_stop()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pymeasure/units.py0000644000175100001770000000227114623331163016745 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pint ureg = pint.get_application_registry() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/pyproject.toml0000644000175100001770000000036214623331163016132 0ustar00runnerdocker[build-system] requires = ["setuptools>=45", "wheel", "setuptools_scm>=8.1.0"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] # write_to = "pymeasure/_version.py" [tool.black] line-length = 100 [tool.isort] profile = "black"././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4496064 pymeasure-0.14.0/setup.cfg0000644000175100001770000000302414623331176015041 0ustar00runnerdocker[metadata] name = PyMeasure url = https://github.com/pymeasure/pymeasure author = PyMeasure Developers description = Scientific measurement library for instruments, experiments, and live-plotting long_description = file: README.rst, CHANGES.rst long_description_content_type = text/x-rst keywords = measure, instrument, experiment control, automate, graph, plot license = MIT license_files = LICENSE.txt 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.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Topic :: Scientific/Engineering [options] packages = find: python_requires = >=3.8 install_requires = numpy >= 1.6.1, < 3 pandas >= 0.14, < 3 pint pyvisa >= 1.9 pyserial >= 2.7 pyqtgraph >= 0.12 importlib-metadata; python_version<"3.8" [options.extras_require] tcp = pyzmq>=16.0.2 cloudpickle>=0.3.1 python-vxi11 = python-vxi11>=0.9 tests = pytest >= 3.3.0 pytest-cov >= 4.1.0 pytest-qt >= 2.4.0 # install pyqt or pyside manually as desired pyvisa-sim >= 0.4.0 [flake8] exclude = .git,__pycache__,docs/conf.py,build,dist max-line-length = 100 max-complexity = 15 per-file-ignores = __init__.py:F401 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.425606 pymeasure-0.14.0/tests/0000755000175100001770000000000014623331176014363 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.425606 pymeasure-0.14.0/tests/adapters/0000755000175100001770000000000014623331176016166 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/adapters/test_adapter.py0000644000175100001770000001376214623331163021224 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 unittest import mock import pytest from pymeasure.adapters import Adapter, FakeAdapter, ProtocolAdapter @pytest.fixture() def adapter(): return Adapter() @pytest.fixture() def fake(): return FakeAdapter() def test_init(adapter): assert adapter.connection is None assert adapter.log == logging.getLogger("Adapter") def test_init_log(): adapter = Adapter(log=logging.getLogger("parent")) assert adapter.log == logging.getLogger("parent.Adapter") def test_deprecated_preprocess_reply(): with pytest.warns(FutureWarning): adapter = Adapter(preprocess_reply=lambda v: v) assert adapter.preprocess_reply is not None def test_del(adapter): adapter.connection = mock.MagicMock() adapter.__del__() assert adapter.connection.method_calls == [mock.call.close()] def test_write(fake): fake.write("abc") assert fake._buffer == "abc" def test_write_bytes(fake): fake.write_bytes(b"abc") assert fake._buffer == "abc" def test_read(fake): fake._buffer = "abc" assert fake.read() == "abc" def test_read_bytes(fake): fake._buffer = "abc" assert fake.read_bytes(5) == b"abc" @pytest.mark.parametrize("method, args", (['_write', ['5']], ['_read', []], ['_write_bytes', ['8']], ['_read_bytes', ['5', False]], )) def test_not_implemented_methods(adapter, method, args): with pytest.raises(NotImplementedError): getattr(adapter, method)(*args) def test_ask_deprecation_warning(): a = FakeAdapter() with pytest.warns(FutureWarning): assert a.ask("abc") == "abc" @pytest.mark.parametrize("value, kwargs, result", (("5,6,7", {}, [5, 6, 7]), ("5.6.7", {'separator': '.'}, [5, 6, 7]), ("5,6,7", {'cast': str}, ['5', '6', '7']), ("X,Y,Z", {}, ['X', 'Y', 'Z']), ("X,Y,Z", {'cast': str}, ['X', 'Y', 'Z']), ("X.Y.Z", {'separator': '.'}, ['X', 'Y', 'Z']), ("0,5,7.1", {'cast': bool}, [False, True, True]), )) def test_adapter_values(value, kwargs, result): a = FakeAdapter() with pytest.warns(FutureWarning): assert a.values(value, **kwargs) == result def test_read_binary_values(): a = ProtocolAdapter([(None, "1 2")]) assert list(a.read_binary_values(dtype=int, sep=" ")) == pytest.approx([1, 2]) def test_write_binary_values(): """Test write_binary_values in the ieee header format.""" a = ProtocolAdapter([(b'CMD#212\x00\x00\x80?\x00\x00\x00@\x00\x00@@\n', None)]) a.write_binary_values("CMD", [1, 2, 3], termination="\n") def test_adapter_preprocess_reply(): with pytest.warns(FutureWarning): a = FakeAdapter(preprocess_reply=lambda v: v[1:]) assert str(a) == "" 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] def test_binary_values_deprecation_warning(): a = FakeAdapter() with pytest.warns(FutureWarning): a.binary_values("abcdefgh") class TestLoggingForTestGenerator: """The test Generator relies on specific logging in the adapter, these tests ensure that.""" message = b"some written message" @pytest.fixture def adapter(self, caplog): adapter = ProtocolAdapter() caplog.set_level(logging.DEBUG) return adapter def test_write(self, adapter, caplog): adapter.comm_pairs = [(self.message, None)] written = self.message.decode() adapter.write(written) record = caplog.records[0] assert record.msg == "WRITE:%s" assert record.args == (written,) def test_write_bytes(self, adapter, caplog): adapter.comm_pairs = [(self.message, None)] adapter.write(self.message) record = caplog.records[0] assert record.msg == "WRITE:%s" assert record.args == (self.message,) def test_read(self, adapter, caplog): adapter.comm_pairs = [(None, self.message)] read = adapter.read() assert read == self.message.decode() record = caplog.records[0] assert record.msg == "READ:%s" assert record.args == (read,) def test_read_bytes(self, adapter, caplog): adapter.comm_pairs = [(None, self.message)] read = adapter.read_bytes(-1) assert read == self.message record = caplog.records[0] assert record.msg == "READ:%s" assert record.args == (read,) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/adapters/test_prologix.py0000644000175100001770000001005014623331163021432 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 PrologixAdapter from pymeasure.test import expected_protocol init_comm = [("++auto 0", None), ("++eoi 1", None), ("++eos 2", None)] def test_init(): with expected_protocol( PrologixAdapter, init_comm, ): pass def test_init_different_config(): with expected_protocol( PrologixAdapter, [("++auto 1", None), ("++eoi 0", None), ("++eos 0", None)], auto=True, eoi=False, eos="\r\n", ): pass @pytest.mark.parametrize("message, value", (("1", True), ("0", False))) def test_auto(message, value): with expected_protocol( PrologixAdapter, init_comm + [("++auto", message)], ) as adapter: assert adapter.auto is value def test_gpib_read_timeout(): with expected_protocol( PrologixAdapter, init_comm + [ ("++read_tmo_ms 700", None), ("++read_tmo_ms", 700) ], ) as adapter: adapter.gpib_read_timeout = 700 assert adapter.gpib_read_timeout == 700 def test_write(): with expected_protocol( PrologixAdapter, [("++auto 0", None), ("++eoi 1", None), ("++eos 2", None), ("something", None)], ) as adapter: adapter.write("something") def test_write_address(): with expected_protocol( PrologixAdapter, [("++auto 0", None), ("++eoi 1", None), ("++eos 2", None), ("++addr 5", None), ("something", None)], address=5, ) as adapter: adapter.write("something") def test_read(): with expected_protocol( PrologixAdapter, [("++auto 0", None), ("++eoi 1", None), ("++eos 2", None), ("write", None), ("++read eoi", "response")], ) as adapter: adapter.write("write") assert adapter.read() == "response" def test_write_bytes(): with expected_protocol( PrologixAdapter, [("++auto 0", None), ("++eoi 1", None), ("++eos 2", None), (b"something", None)], ) as adapter: adapter.write_bytes(b"something") @pytest.mark.parametrize( "test_input,expected", [ ([1, 2, 3], b'OUTP#13\x01\x02\x03\n'), ([43, 27, 10, 13, 97, 98, 99], b'OUTP#17\x1b\x2b\x1b\x1b\x1b\x0a\x1b\x0dabc\n'), ] ) def test_write_binary_values(test_input, expected): with expected_protocol( PrologixAdapter, [("++auto 0", None), ("++eoi 1", None), ("++eos 2", None), (expected, None)] ) as adapter: adapter.write_binary_values("OUTP", test_input, datatype='B') def test_wait_for_srq(): with expected_protocol( PrologixAdapter, [("++auto 0", None), ("++eoi 1", None), ("++eos 2", None), ("++srq", None), ("++read eoi", "0"), ("++srq", None), ("++read eoi", "1")] ) as adapter: adapter.wait_for_srq() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/adapters/test_protocol.py0000644000175100001770000001740714623331163021445 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 unittest.mock import call import pytest from pymeasure.adapters.protocol import to_bytes, ProtocolAdapter from pytest import mark, raises, fixture, warns @pytest.fixture def adapter(): return ProtocolAdapter() @mark.parametrize("input, output", (("superXY", b"superXY"), ([1, 2, 3, 4], b"\x01\x02\x03\x04"), (5, b"5"), (4.6, b"4.6"), (None, None), )) def test_to_bytes(input, output): assert to_bytes(input) == output def test_to_bytes_invalid(): with raises(TypeError): to_bytes(5.5j) def test_protocol_instantiation(): a = ProtocolAdapter([("write", "read"), ("write_only", None)]) assert a.comm_pairs == [("write", "read"), ("write_only", None)] @pytest.fixture def mockAdapter(): adapter = ProtocolAdapter(connection_attributes={'timeout': 100}, connection_methods={'stb': 17}) return adapter def test_connection_call(mockAdapter): """Test whether a call to the connection is registered.""" mockAdapter.connection.clear(7) assert mockAdapter.connection.clear.call_args_list == [call(7)] def test_connection_attribute(mockAdapter): assert mockAdapter.connection.timeout == 100 def test_connection_method(mockAdapter): assert mockAdapter.connection.stb() == 17 class Test_write: def test_write(self): a = ProtocolAdapter([("written 5", 5)]) a.write("written 5") assert a._read_buffer == b"5" def test_write_without_response(self): a = ProtocolAdapter([("Hey ho", None)]) a.write("Hey ho") assert a._read_buffer is None def test_wrong_command(self): a = ProtocolAdapter([("Hey ho", None)]) with raises(AssertionError, match="do not match"): a.write("Something different") class Test_write_bytes: @fixture(scope="class", params=[1, 2]) def written(self, request): """Write in a single turn.""" a = ProtocolAdapter([("written", 5)]) if request.param == 1: a.write_bytes(b"written") else: a.write_bytes(b"writ") a.write_bytes(b"ten") return a def test_write_write_buffer(self, written): assert written._write_buffer is None def test_write_index(self, written): assert written._index == 1 def test_write_read_buffer(self, written): assert written._read_buffer == b"5" def test_partial_write(self): a = ProtocolAdapter([("written", 5)]) a.write_bytes(b"writ") assert a._index == 0 def test_leftover_response(self): a = ProtocolAdapter([("written", 5)]) a._read_buffer = b"5" with raises(AssertionError): a.write_bytes(b"written") def test_no_response(self): a = ProtocolAdapter([("written", None)]) a.write_bytes(b"writ") assert a._read_buffer is None def test_not_enough_pairs(self): a = ProtocolAdapter([("a", None)]) a.write_bytes(b"a") with raises(ValueError): a.write_bytes(b"b") class Test_read: @mark.parametrize("buffer, returned", ((b"\x03\x65", "\x03e"), (b"Bytes", "Bytes"), )) def test_works(self, buffer, returned): a = ProtocolAdapter() a._read_buffer = buffer assert a.read() == returned def test_read_empties_read_buffer(self): a = ProtocolAdapter() a._read_buffer = b"jklasdf" a.read() assert a._read_buffer is None def test_read_empty_message(self): a = ProtocolAdapter() a._read_buffer = b"" assert a.read() == "" assert a._read_buffer is None class Test_read_bytes: def test_read_full_buffer(self): a = ProtocolAdapter() a._read_buffer = b"Super 5" assert a.read_bytes(3) == b"Sup" assert a._read_buffer == b"er 5" def test_read_empty_buffer(self): a = ProtocolAdapter([(None, "Super 5")]) assert a.read_bytes(3) == b"Sup" assert a._index == 1 assert a._read_buffer == b"er 5" def test_read_without_write(self): a = ProtocolAdapter([("written", 5)]) with raises(AssertionError): a.read_bytes(3) def test_read_all_bytes_empties_read_buffer(self): a = ProtocolAdapter() a._read_buffer = b"jklasdf" a.read_bytes(7) assert a._read_buffer is None def test_read_all_bytes_from_pairs_empties_read_buffer(self): a = ProtocolAdapter([(None, b"jklasdf")]) a.read_bytes(7) assert a._read_buffer is None def test_no_messages(self): a = ProtocolAdapter() with raises(ValueError): a.read_bytes(3) def test_not_enough_pairs(self): a = ProtocolAdapter([(b"a", None)]) a.write_bytes(b"a") with raises(ValueError): a.read_bytes(10) def test_unsolicited_response(self): a = ProtocolAdapter([(None, b"Response")]) assert a.read_bytes(10) == b"Response" def test_read_with_missing_write(self): a = ProtocolAdapter([(b"a", b"b")]) with raises(AssertionError): a.read_bytes(10) def test_catch_None_None_pair(self): a = ProtocolAdapter([(None, None)]) with raises(AssertionError, match="None, None"): a.read_bytes(1) def test_break_on_termchar_raises_warning(self): a = ProtocolAdapter([(None, b"Response")]) with warns(UserWarning, match="cannot be tested"): assert a.read_bytes(10, break_on_termchar=True) == b"Response" def test_read_write_sequence(): """Test several consecutive writes and reads, including ask.""" a = ProtocolAdapter( [("c1", "a1"), ("c2", None), (None, "a3"), ("c4", "a4")]) a.write("c1") assert a.read() == "a1" a.write("c2") assert a.read_bytes(-1) == b"a3" a.write_bytes(b"c4") assert a.read_bytes(2) == b"a4" def test_write_and_read_with_and_without_bytes(): """Test writing and reading, normal and bytes in combination.""" a = ProtocolAdapter([("c1", "a1")]) a.write_bytes(b"c") a.write("1") assert a.read_bytes(1) == b"a" assert a.read() == "1" @mark.parametrize("pairs", ([("c2",)], [(None, "a3", "c4")], [("c1", None), ("c2",)], [(None, "a3", "c4"), (None, None)], )) def test_comm_pairs_are_all_length_2(pairs): with raises(ValueError): ProtocolAdapter(pairs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/adapters/test_serial.py0000644000175100001770000000672214623331163021061 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 serial from pymeasure.adapters import SerialAdapter @pytest.fixture def adapter(): return SerialAdapter(serial.serial_for_url("loop://", timeout=0.2)) @pytest.mark.parametrize("term", (("", "\n", "123\r"))) def test_write_termination(adapter, term): adapter.write_termination = term adapter.write("abc") assert adapter.read() == "abc" + term @pytest.mark.parametrize("term", (("", "\n", "\r\n", "4.1123"))) def test_read_termination(adapter, term): adapter.read_termination = term adapter.write("abc" + term) assert adapter.read() == "abc" @pytest.mark.parametrize("msg", ["OUTP\n", "POWER 22 dBm\n"]) def test_adapter_write_read(adapter, msg): adapter.write(msg) assert adapter.read() == msg @pytest.mark.parametrize("msg", [b"OUTP\n", b"POWER 22 dBm\n"]) def test_write_bytes(adapter, msg): adapter.write_bytes(msg) assert adapter.read() == msg.decode() def test_read_bytes(adapter): """Test whether `count` bytes are returned, even though a term char is defined.""" adapter.read_termination = "\n" adapter.write_bytes(b"basd\x02\nfasdf\n") assert adapter.read_bytes(9) == b"basd\x02\nfas" def test_read_bytes_unlimited(adapter): """Test whether all bytes are returned, even though a term char is defined.""" adapter.read_termination = "\n" adapter.write_bytes(b"basd\x02\nfasdf\n") assert adapter.read_bytes(-1) == b"basd\x02\nfasdf\n" def test_read_bytes_unlimited_long(adapter): """Test whether all bytes are returned when a lot of data is sent.""" adapter.write_bytes(b"abcde" * 50) assert adapter.read_bytes(-1) == b"abcde" * 50 @pytest.mark.parametrize("count", (-1, 8)) def test_read_bytes_break_on_termchar(adapter, count): adapter.read_termination = "\n" adapter.write_bytes(b"basd\x02\nfasdf\n") assert adapter.read_bytes(count, break_on_termchar=True) == b"basd\x02\n" @pytest.mark.parametrize("test_input,expected", [([1, 2, 3], b'OUTP#13\x01\x02\x03'), (range(100), b'OUTP#3100' + bytes(range(100)))]) def test_adapter_write_binary_values(adapter, test_input, expected): adapter.write_binary_values("OUTP", test_input, datatype='B') # Add 10 bytes more, just to check that no extra bytes are present assert adapter.connection.read(len(expected) + 10) == expected ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/adapters/test_serial_with_loopback.py0000644000175100001770000000462714623331163023770 0ustar00runnerdockerimport pytest from serial import Serial from time import time from pymeasure.adapters import SerialAdapter class TestSerialLoopback: """Test serial adapter with two real but looped COM ports. A pair of COM ports that are looped will read the written data from the other. This looping needs to be done outside this script. One suggestion is using the application `com0com `_. Alternatively two UART adapters could be used, that have their RX/TX physically connected. Call PyTest with the argument``--device-address="COM1,COM2"`` to specify the serial addresses, minding the comma for the separation of the two ports. """ ADAPTER_TIMEOUT = 1.0 # Seconds @pytest.fixture() def adapter(self, connected_device_address): device = connected_device_address.split(",")[0] return SerialAdapter( device, baudrate=19200, timeout=self.ADAPTER_TIMEOUT, read_termination=chr(0x0F), ) @pytest.fixture() def loopback(self, connected_device_address): """See `adapter()`.""" device = connected_device_address.split(",")[1] return Serial(device, baudrate=19200) def test_read(self, adapter, loopback): """Regular read with fixed number of bytes.""" loopback.write(b"abc") result = adapter.read_bytes(3) assert len(result) == 3 @pytest.mark.parametrize("data", [ b"a", # Short data b"aaaaabbbbbccccceeeee" * 50, # A lot data b"a" * 256, # Exactly one chunk size b"aaaaabbbbb\nccccceeeee" * 50, # With a newline (should be ignored) ]) def test_read_all(self, adapter, loopback, data): """Read with undefined number of bytes - also confirm timeout duration is correct.""" loopback.write(data) start = time() result = adapter.read_bytes(-1) elapsed = time() - start assert result == data assert self.ADAPTER_TIMEOUT == pytest.approx(elapsed, abs=0.1) @pytest.mark.parametrize("chunk", [1, 256, 10000]) def test_read_varied_chunk_size(self, adapter, loopback, chunk): """Read with undefined number of bytes with non-default chunk size.""" data = b"abcde" * 10 loopback.write(data) result = adapter.read_bytes(-1) assert result == data ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/adapters/test_visa.py0000644000175100001770000001440414623331163020540 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 import pyvisa from pymeasure.adapters import VISAAdapter from pymeasure.test import expected_protocol # 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) @pytest.fixture def adapter(): adapter = VISAAdapter(SIM_RESOURCE, visa_library='@sim', read_termination="\n", # Large timeout allows very slow GitHub action runners to complete. timeout=60, ) yield adapter # Empty the read buffer, as something might remain there after a test. # `clear` is not implemented in pyvisa-sim and `flush_read_buffer` seems to do nothing. adapter.timeout = 0 try: adapter.read_bytes(-1) except pyvisa.errors.VisaIOError as exc: if not exc.args[0].startswith("VI_ERROR_TMO"): raise # Close the connection adapter.close() def test_nested_adapter(): a0 = VISAAdapter(SIM_RESOURCE, visa_library='@sim', read_termination="\n") a = VISAAdapter(a0) assert a.resource_name == SIM_RESOURCE assert a.connection == a0.connection assert a.manager == a0.manager def test_nested_adapter_query_delay(): query_delay = 10 with pytest.warns(FutureWarning, match="query_delay"): a0 = VISAAdapter(SIM_RESOURCE, visa_library='@sim', read_termination="\n", query_delay=query_delay) a = VISAAdapter(a0) assert a.resource_name == SIM_RESOURCE assert a.connection == a0.connection assert a.query_delay == query_delay def test_ProtocolAdapter(): with expected_protocol( VISAAdapter, [(b"some bytes written", b"Response")] ) as adapter: adapter.write_bytes(b"some bytes written") assert adapter.read_bytes(-1) == b"Response" def test_correct_visa_asrl_kwarg(): """Confirm that the asrl kwargs gets passed through to the VISA connection.""" a = VISAAdapter(SIM_RESOURCE, visa_library='@sim', asrl={'read_termination': "\rx\n"}) assert a.connection.read_termination == "\rx\n" def test_open_gpib(): a = VISAAdapter(5, visa_library='@sim') assert a.resource_name == "GPIB0::5::INSTR" class TestClose: @pytest.fixture def adapterC(self): return VISAAdapter(SIM_RESOURCE, visa_library='@sim') def test_connection_session_closed(self, adapterC): # Verify first, that it works before closing assert adapterC.connection.session is not None adapterC.close() with pytest.raises(pyvisa.errors.InvalidSession, match="Invalid session"): adapterC.connection.session def test_manager_session_closed(self, adapterC): # Verify first, that it works before closing assert adapterC.manager.session is not None adapterC.close() with pytest.raises(pyvisa.errors.InvalidSession, match="Invalid session"): adapterC.manager.session def test_write_read(adapter): adapter.write(":VOLT:IMM:AMPL?") assert float(adapter.read()) == 1 def test_write_bytes_read_bytes(adapter): adapter.write_bytes(b"*IDN?\r\n") assert adapter.read_bytes(22) == b"SCPI,MOCK,VERSION_1.0\n" def test_write_bytes_read(adapter): adapter.write_bytes(b"*IDN?\r\n") assert adapter.read() == "SCPI,MOCK,VERSION_1.0" class TestReadBytes: @pytest.fixture() def adapterR(self, adapter): adapter.write("*IDN?") yield adapter def test_read_bytes(self, adapterR): assert adapterR.read_bytes(22) == b"SCPI,MOCK,VERSION_1.0\n" def test_read_all_bytes(self, adapterR): assert adapterR.read_bytes(-1) == b"SCPI,MOCK,VERSION_1.0\n" @pytest.mark.parametrize("count", (-1, 7)) def test_read_break_on_termchar(self, adapterR, count): """Test read_bytes breaks on termchar.""" adapterR.connection.read_termination = "," assert adapterR.read_bytes(count, break_on_termchar=True) == b"SCPI," def test_read_no_break_on_termchar(self, adapterR): adapterR.connection.read_termination = "," # `break_on_termchar=False` is default value assert adapterR.read_bytes(-1) == b"SCPI,MOCK,VERSION_1.0\n" def test_read_no_break_on_newline(self, adapter): # write twice to have two newline characters in the read buffer adapter.write("*IDN?") adapter.write("*IDN?") # `break_on_termchar=False` is default value assert adapter.read_bytes(-1) == b"SCPI,MOCK,VERSION_1.0\nSCPI,MOCK,VERSION_1.0\n" def test_visa_adapter(adapter): assert repr(adapter) == f"" with pytest.warns(FutureWarning): assert adapter.ask("*IDN?") == "SCPI,MOCK,VERSION_1.0" adapter.write("*IDN?") assert adapter.read() == "SCPI,MOCK,VERSION_1.0" def test_visa_adapter_ask_values(adapter): with pytest.warns(FutureWarning): assert adapter.ask_values(":VOLT:IMM:AMPL?", separator=",") == [1.0] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/conftest.py0000644000175100001770000000406614623331163016564 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 def pytest_addoption(parser): parser.addoption( "--device-address", action="store", default=None, dest="adapter", help=( "Pass an adapter string for connection to an instrument needed for a test, e.g. " "--device-address ASRL1::INSTR or --device-address TCPIP::192.168.0.123::INSTR" ), ) @pytest.fixture(scope="session") def connected_device_address(pytestconfig): """ Fixture to pass the address to a device for tests that require a connection to an instrument. Using this fixture in a test function will skip it when running pytest by default. The test will only run if a device address is provided with the --device-address option when invoking pytest. To run only relevant tests, use the -k option to select the desired tests. """ address = pytestconfig.getoption("--device-address", skip=True) return address ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.425606 pymeasure-0.14.0/tests/display/0000755000175100001770000000000014623331176016030 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/display/test_console.py0000644000175100001770000001027714623331163021106 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 (BooleanParameter, ListParameter, FloatParameter, IntegerParameter, Parameter, VectorParameter, PhysicalParameter) from pymeasure.experiment.procedure import Procedure from pymeasure.display.console import ConsoleArgumentParser class TestArgParsing: @pytest.mark.parametrize("param", [ ('plain_param', 'a'), ('int_param', 100), ('float_param', 0.5), ('bool_param', True), ('vector_param', [1.0, 2, 3]), ('list_param', '2'), ('physical_param', [1.0, 0.1]) ]) def test_init_from_param(self, param): class TestProcedure(Procedure): plain_param = Parameter('Plain parameter', default=100) int_param = IntegerParameter('Integer parameter', default=100) float_param = FloatParameter('Float parameter', units='s', default=0.2) bool_param = BooleanParameter('Boolean parameter', default=True) list_param = ListParameter('List parameter', default='1', choices=['1', '2', '3']) vector_param = VectorParameter('Vector parameter', default=[1, 5, 3]) physical_param = PhysicalParameter('Physical parameter', units='s', default=[1.0, 0.1]) name, value = param console = ConsoleArgumentParser(TestProcedure) args = vars(console.parse_args(['--' + name, str(value)])) assert args[name] == value class TestArgHelpString: @pytest.mark.parametrize("klass, kwargs", [ (Parameter, {'name': 'Plain parameter', 'default': 'a'}), (IntegerParameter, {'name': 'Integer parameter', 'default': 100}), (FloatParameter, {'name': 'Float parameter', 'default': 0.5}), (BooleanParameter, {'name': 'Boolean parameter', 'default': True}), (VectorParameter, {'name': 'Vector parameter', 'default': [1.0, 2, 3]}), (ListParameter, {'name': 'List parameter', 'default': '2', 'choices': ['1', '2', '3']}), (PhysicalParameter, {'name': 'Physical parameter', 'default': [1.0, 0.1]}) ]) def test_init_from_param(self, klass, kwargs): class TestProcedure(Procedure): parameter = klass(**kwargs) desc = kwargs['name'] default_value = kwargs['default'] console = ConsoleArgumentParser(TestProcedure) help_message = [value.strip() for value in console.format_help().split("\n")] assert '--parameter PARAMETER' in help_message for help_line in help_message: if desc in help_line: break assert desc in help_line assert 'default' in help_line assert str(default_value) in help_line ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/display/test_inputs.py0000644000175100001770000002527314623331163020770 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 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() is 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) @pytest.mark.parametrize("locale, decimalSep", [ [QtCore.QLocale(31, 7, 224), "."], # UK locale for period [QtCore.QLocale(30, 7, 151), ","], # NL locale for comma ]) def test_locale_settings(self, qtbot, locale, decimalSep): assert locale.decimalPoint() == decimalSep QtCore.QLocale.setDefault(locale) float_param = FloatParameter('potato', minimum=-1000, maximum=1000, default=1.3) sci_input = ScientificInput(float_param) # Check if the modified locale is set assert sci_input.locale().decimalPoint() == decimalSep assert sci_input.validator.locale().decimalPoint() == decimalSep # Check if conversion from double to text works correctly assert sci_input.valueFromText(f"2{decimalSep}6") == 2.6 # Check if conversion from text to double works correctly assert sci_input.textFromValue(2.6) == f"2{decimalSep}6" # Reset the locale settings QtCore.QLocale.setDefault(QtCore.QLocale.system()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/display/test_plotter.py0000644000175100001770000000374714623331163021141 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/display/test_windows.py0000644000175100001770000000452214623331163021132 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.425606 pymeasure-0.14.0/tests/display/widgets/0000755000175100001770000000000014623331176017476 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/display/widgets/test_inputs_widget.py0000644000175100001770000001740214623331163023774 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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.display.Qt import QtCore from pymeasure.experiment import Procedure, BooleanParameter, Parameter, FloatParameter, \ IntegerParameter, ListParameter from pymeasure.display.widgets import InputsWidget @pytest.mark.parametrize( "hide_groups,exp_visible,exp_enabled", [ (True, False, True), (False, True, False), ] ) def test_input_toggling(qtbot, hide_groups, exp_visible, exp_enabled): """Test whether the basic toggling works. This test is run for both hiding (hide_groups=True) and disabling (hide_groups=False) the inputs. """ class TestProcedure(Procedure): toggle_par = BooleanParameter('toggle', default=True) x = Parameter('X', default='value', group_by='toggle_par') wdg = InputsWidget(TestProcedure, inputs=('toggle_par', 'x'), hide_groups=hide_groups) qtbot.addWidget(wdg) assert wdg.toggle_par.isChecked() is True assert wdg.x.isVisibleTo(wdg) is True assert wdg.x.isEnabled() is True qtbot.mouseClick(wdg.toggle_par, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2, int(wdg.toggle_par.height()/2))) assert wdg.toggle_par.isChecked() is False assert wdg.x.isVisibleTo(wdg) is exp_visible assert wdg.x.isEnabled() is exp_enabled qtbot.mouseClick(wdg.toggle_par, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2, int(wdg.toggle_par.height()/2))) assert wdg.toggle_par.isChecked() is True assert wdg.x.isVisibleTo(wdg) is True assert wdg.x.isEnabled() is True def test_input_toggling_start_hidden(qtbot): """Test whether the basic toggling works. This test is run for both hiding (hide_groups=True) and disabling (hide_groups=False) the inputs. """ class TestProcedure(Procedure): toggle_par = BooleanParameter('toggle', default=False) x = Parameter('X', default='value', group_by='toggle_par') wdg = InputsWidget(TestProcedure, inputs=('toggle_par', 'x')) qtbot.addWidget(wdg) assert wdg.toggle_par.isChecked() is False assert wdg.x.isVisibleTo(wdg) is False qtbot.mouseClick(wdg.toggle_par, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2, int(wdg.toggle_par.height()/2))) assert wdg.toggle_par.isChecked() is True assert wdg.x.isVisibleTo(wdg) is True def test_input_toggling_multiple_conditions(qtbot): """Test if multiple conditions are handled correctly together.""" class TestProcedure(Procedure): toggle_par1 = BooleanParameter('toggle', default=True) toggle_par2 = BooleanParameter('toggle', default=False) x = Parameter('X', default='value', group_by=['toggle_par1', 'toggle_par2'], group_condition=[True, False]) wdg = InputsWidget(TestProcedure, inputs=('toggle_par1', 'toggle_par2', 'x')) qtbot.addWidget(wdg) assert wdg.toggle_par1.isChecked() is True assert wdg.toggle_par2.isChecked() is False assert wdg.x.isVisibleTo(wdg) is True qtbot.mouseClick(wdg.toggle_par1, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2, int(wdg.toggle_par1.height()/2))) assert wdg.toggle_par1.isChecked() is False assert wdg.toggle_par2.isChecked() is False assert wdg.x.isVisibleTo(wdg) is False qtbot.mouseClick(wdg.toggle_par2, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2, int(wdg.toggle_par2.height()/2))) assert wdg.toggle_par1.isChecked() is False assert wdg.toggle_par2.isChecked() is True assert wdg.x.isVisibleTo(wdg) is False qtbot.mouseClick(wdg.toggle_par1, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2, int(wdg.toggle_par1.height()/2))) assert wdg.toggle_par1.isChecked() is True assert wdg.toggle_par2.isChecked() is True assert wdg.x.isVisibleTo(wdg) is False qtbot.mouseClick(wdg.toggle_par2, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2, int(wdg.toggle_par2.height()/2))) assert wdg.toggle_par1.isChecked() is True assert wdg.toggle_par2.isChecked() is False assert wdg.x.isVisibleTo(wdg) is True @pytest.mark.parametrize( "condition", [ True, False, ] ) def test_input_toggling_boolean(qtbot, condition): """Test if a boolean conditions are handled correctly.""" class TestProcedure(Procedure): toggle_par = BooleanParameter('toggle', default=True) x = Parameter('X', default='value', group_by='toggle_par', group_condition=condition) wdg = InputsWidget(TestProcedure, inputs=('toggle_par', 'x')) qtbot.addWidget(wdg) assert wdg.toggle_par.isChecked() is True assert wdg.x.isVisibleTo(wdg) is condition qtbot.mouseClick(wdg.toggle_par, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2, int(wdg.toggle_par.height()/2))) assert wdg.toggle_par.isChecked() is False assert wdg.x.isVisibleTo(wdg) is not condition qtbot.mouseClick(wdg.toggle_par, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2, int(wdg.toggle_par.height()/2))) assert wdg.toggle_par.isChecked() is True assert wdg.x.isVisibleTo(wdg) is condition @pytest.mark.parametrize( "partype,default,condition,kwargs", [ (Parameter, "default_value", "condition_value", {}), (FloatParameter, 0.7, 5.89, {}), (IntegerParameter, 3, 5, {}), (BooleanParameter, False, True, {}), (ListParameter, "default", "condition", {"choices": ["default", "condition"]}), ] ) def test_input_toggling_various_inputs(qtbot, partype, default, condition, kwargs): """Test if various input-types are handled correctly for toggling.""" class TestProcedure(Procedure): toggle_par = partype('toggle', default=default, **kwargs) x = Parameter('X', default='value', group_by='toggle_par', group_condition=condition) wdg = InputsWidget(TestProcedure, inputs=('toggle_par', 'x')) qtbot.addWidget(wdg) assert wdg.x.isVisibleTo(wdg) is False wdg.toggle_par.setValue(condition) assert wdg.x.isVisibleTo(wdg) is True wdg.toggle_par.setValue(default) assert wdg.x.isVisibleTo(wdg) is False def test_input_toggling_lambda_condition(qtbot): """Test if a lambda-function is handled for toggling.""" class TestProcedure(Procedure): toggle_par = IntegerParameter('toggle', default=100) x = Parameter('X', default='value', group_by='toggle_par', group_condition=lambda v: 50 < v < 90) wdg = InputsWidget(TestProcedure, inputs=('toggle_par', 'x')) qtbot.addWidget(wdg) assert wdg.x.isVisibleTo(wdg) is False wdg.toggle_par.setValue(80) assert wdg.x.isVisibleTo(wdg) is True wdg.toggle_par.setValue(40) assert wdg.x.isVisibleTo(wdg) is False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.429606 pymeasure-0.14.0/tests/experiment/0000755000175100001770000000000014623331176016543 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.429606 pymeasure-0.14.0/tests/experiment/data/0000755000175100001770000000000014623331176017454 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/data/__init__.py0000644000175100001770000000000014623331163021547 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/data/procedure_for_testing.py0000644000175100001770000000371014623331163024416 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/data/results_for_testing.csv0000644000175100001770000000000014623331163024257 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/data/results_for_testing_parameters.csv0000644000175100001770000001125014623331163026513 0ustar00runnerdocker#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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/test_listeners.py0000644000175100001770000000275614623331163022172 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/test_metadata.py0000644000175100001770000000720714623331163021736 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 Metadata from pymeasure.experiment.procedure import Procedure def test_metadata_default(): p = Metadata('Test', default=5) assert p.value == 5 def test_metadata_units(): p = Metadata('Test', units='tests') assert p.units == 'tests' def test_metadata_formatting(): p1 = Metadata('Test', default=5.157, units='tests') assert str(p1) == "5.157 tests" p2 = Metadata('Test', default=5.157, units='tests', fmt="%.1f") assert str(p2) == "5.2 tests" def test_metadata_notset(): p = Metadata('Test') with pytest.raises(ValueError): p.value def test_metadata_object_replacement(): class TestProcedure(Procedure): md = Metadata('Test callable', default=19) pr = TestProcedure() assert pr.md == 19 assert pr._metadata["md"].value == 19 # Manually set another value pr.md = 20 pr.evaluate_metadata() # Check if the metadata has been correctly replaced with the new value assert pr.md == 20 assert pr._metadata["md"].value == 20 def test_metadata_fget_evaluation(): def test_method(): return "teststring" class TestAttribute(): def callable(self): return 84 @property def property(self): return 48 class TestProcedure(Procedure): md_callable = Metadata('Test callable', fget=test_method) md_str_call = Metadata('Test string callable', fget="callable") md_str_prop = Metadata('Test string property', fget="property") md_nest_call = Metadata('Test nested callable', fget="atr.callable") md_nest_prop = Metadata('Test nested property', fget="atr.property") def callable(self): return 42 @property def property(self): return 24 atr = TestAttribute() pr = TestProcedure() # Check if all Metadata have no value yet assert not pr._metadata["md_callable"].is_set() assert not pr._metadata["md_str_call"].is_set() assert not pr._metadata["md_str_prop"].is_set() assert not pr._metadata["md_nest_call"].is_set() assert not pr._metadata["md_nest_prop"].is_set() pr.evaluate_metadata() # Check if all Metadata has been correctly evaluated assert pr._metadata["md_callable"].value == "teststring" assert pr._metadata["md_str_call"].value == 42 assert pr._metadata["md_str_prop"].value == 24 assert pr._metadata["md_nest_call"].value == 84 assert pr._metadata["md_nest_prop"].value == 48 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/test_parameters.py0000644000175100001770000001511614623331163022317 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 assert p.cli_args[0] == 5 assert p.cli_args[1] == [('units are', 'units'), 'default'] def test_integer_units(): p = IntegerParameter('Test', units='V') assert p.units == 'V' assert p.cli_args[0] is None assert p.cli_args[1] == [('units are', 'units'), 'default', 'minimum', 'maximum'] def test_integer_value(): p = IntegerParameter('Test', units='tests') with pytest.raises(ValueError): _ = 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): _ = 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 is True p.value = "False" assert p.value is False p.value = "true" assert p.value is True p.value = "false" assert p.value is False p.value = 1 # a number assert p.value is True p.value = 0 # zero assert p.value is False p.value = True assert p.value is True assert p.cli_args[0] is None assert p.cli_args[1] == [('units are', 'units'), 'default'] def test_float_value(): p = FloatParameter('Test', units='tests') with pytest.raises(ValueError): _ = 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 assert p.cli_args[0] is None assert p.cli_args[1] == [('units are', 'units'), 'default', 'decimals'] 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_string(): # make sure string representation of choices is unique with pytest.raises(ValueError): _ = ListParameter('Test', choices=[1, '1']) def test_list_value(): 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 = '1' # reading from file assert p.value == 1 p.value = '2.2' # reading from file 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 assert p.cli_args[0] is None assert p.cli_args[1] == [('units are', 'units'), 'default', ('choices are', 'choices')] def test_list_value_with_units(): p = ListParameter( 'Test', choices=[1, 2.2, 'three', 'and four'], units='tests') p.value = '1 tests' assert p.value == 1 p.value = '2.2 tests' assert p.value == 2.2 p.value = 'three tests' assert p.value == 'three' p.value = 'and four tests' assert p.value == 'and four' assert p.cli_args[0] is None assert p.cli_args[1] == [('units are', 'units'), 'default', ('choices are', 'choices')] def test_list_order(): p = ListParameter('Test', choices=[1, 2.2, 'three', 'and four']) # check if order is preserved, choices are internally stored as dict assert p.choices == (1, 2.2, 'three', 'and four') assert p.cli_args[0] is None assert p.cli_args[1] == [('units are', 'units'), 'default', ('choices are', 'choices')] 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' assert p.cli_args[0] is None assert p.cli_args[1] == [('units are', 'units'), 'default', '_length'] # TODO: Add tests for Measurable ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/test_procedure.py0000644000175100001770000000755414623331163022153 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pymeasure.units import ureg 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 # This test checks that user can define properties using the parameters inside the procedure # The test ensure that property is evaluated only when the Parameter has been processed during # class initialization. def test_procedure_properties(): class TestProcedure(Procedure): @property def a(self): assert isinstance(self.x, int) return self.x @property def z(self): assert isinstance(self.x, int) return self.x x = Parameter('X', default=5) p = TestProcedure() assert p.x == 5 # Make sure that a procedure can be initialized even though some properties are raising # errors at initialization time def test_procedure_init_with_invalid_property(): class TestProcedure(Procedure): @property def prop(self): return self.x p = TestProcedure() with pytest.raises(AttributeError): _ = p.prop # AttributeError p.x = 5 assert p.prop == 5 @pytest.mark.parametrize("header, units", ( ("x (m)", ureg.m), ("x (m/s)", ureg.m / ureg.s), ("x (V/(m*s))", ureg.V / ureg.m / ureg.s), ("x (1)", ureg.dimensionless) )) def test_procedure_parse_columns(header, units): assert Procedure.parse_columns([header])[header] == ureg.Quantity(1, units) @pytest.mark.parametrize("valid_header_no_unit", ( ["x"], ["x ( x + y )"], ["x ( notes )"], ["x [V]"] )) def test_procedure_no_parsed_units(valid_header_no_unit): assert Procedure.parse_columns(valid_header_no_unit) == {} @pytest.mark.parametrize("invalid_header_unit", ( ["x (sqrt)"], ["x (x)"], ["x (y)"], )) def test_procedure_invalid_parsed_unit(invalid_header_unit): with pytest.raises(ValueError): Procedure.parse_columns(invalid_header_unit) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/test_replace_placeholders.py0000644000175100001770000000471714623331163024321 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 datetime import datetime from pymeasure.experiment.results import replace_placeholders from pymeasure.experiment.procedure import Procedure from pymeasure.experiment import Parameter, BooleanParameter, FloatParameter def test_replace_placeholders(): class FakeProcedure(Procedure): str_param = Parameter("String Parameter") bool_param = BooleanParameter("Boolean Parameter") float_param = FloatParameter("Float Parameter") fake = FakeProcedure() fake.set_parameters({ "str_param": "test", "bool_param": False, "float_param": 1.252 }) assert replace_placeholders("{String Parameter}", fake) == "test" assert replace_placeholders("{Boolean Parameter}", fake) == "False" assert replace_placeholders("{Float Parameter:.2f}", fake) == "1.25" assert replace_placeholders( "{String Parameter}_{Float Parameter}_{Boolean Parameter}", fake) == "test_1.252_False" with pytest.raises(KeyError): replace_placeholders("{Unknown Parameter}", fake) date_format = "%Y-%m" time_format = "%H:%M" now = datetime.now() date = now.strftime(date_format) time = now.strftime(time_format) assert replace_placeholders( "{date}--{time}", fake, date_format=date_format, time_format=time_format ) == date + '--' + time ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/test_results.py0000644000175100001770000001615614623331163021662 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 os import pickle import tempfile from unittest import mock import pandas as pd import pytest import numpy as np from pymeasure.units import ureg from pymeasure.experiment.results import Results, CSVFormatter from pymeasure.experiment.procedure import Procedure, Parameter from pymeasure.experiment import BooleanParameter 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_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' class Test_csv_formatter_format: def test_csv_formatter_format(self): """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' @pytest.mark.parametrize("head, value, result", (('index', 10, "10"), ('length (m)', "50 cm", "0.5"), ('voltage (V)', ureg.Quantity(-7, ureg.kV), "-7000.0"), ('speed (m/s)', 15 * ureg.cm / ureg.s, "0.15"), ('magnetic (T)', 7, "7"), ('string', "abcdef", "abcdef"), ('count', 9 * ureg.dimensionless, "9"), ('boolean', True, "True"), ('numpy (V)', np.float64(1.1), "1.1"), ('boolean nan (V)', True, "nan"), )) def test_unitful(self, head, value, result): """Test, whether units are appended correctly""" formatter = CSVFormatter(columns=[head]) assert formatter.format({head: value}) == result def test_newly_unitful(self): formatter = CSVFormatter(columns=["count"]) assert formatter.format({'count': 5 * ureg.km}) == "5000.0" assert formatter.units['count'] == ureg.m def test_no_newly_unitful(self): formatter = CSVFormatter(columns=["count"]) assert formatter.format({'count': 5 * ureg.dimensionless}) == "5" assert formatter.units.get('count') is None def test_unitful_erroneous(self): """Test, whether wrong units are rejected""" columns = ['index', 'length (m)', 'voltage (V)'] formatter = CSVFormatter(columns=columns) formatter.units['index'] = ureg.km data = {'index': "10 stupid", 'length (m)': "50 cV", 'voltage (V)': True} assert formatter.format(data) == "nan,nan,nan" def test_procedure_filestorage(): assert RandomProcedure.iterations.value == 100 procedure = RandomProcedure() procedure.iterations = 101 resultfile = tempfile.mktemp() results = Results(procedure, resultfile) 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 is True assert results.parameters["check_false"].value is False assert results.parameters["check_dir"].value == test_string ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/test_sequencer.py0000644000175100001770000000736714623331163022157 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 io import StringIO from pymeasure.experiment.sequencer import SequenceHandler, SequenceEvaluationError def non_empty_lines(text): lines = text.split("\n") linect = 0 for line in lines: if line.strip() != "": linect += 1 return linect seq_file_text_1 = """ - "P1", "[1,2]" -- "P2", "[3, 4, 5]" """ seq_file_text_2 = """ - "P1", "[1,2]" -- "P2", "[3, 4, 5]" - "P1", "[4,5]" """ seq_file_text_3 = """ - "P1", "[1,2]" -- "P2", "[3, 4, 5]" --- "P3", "[4, 5, 6]" """ @pytest.mark.parametrize("seq_file_text", [ (seq_file_text_1, (0, 1), (1, 0), ("P1", "P2")), (seq_file_text_2, (0, 1, 0), (1, 0, 0), ("P1", "P2", "P1")), (seq_file_text_3, (0, 1, 2), (1, 1, 0), ("P1", "P2", "P3")), ]) def test_sequencer(seq_file_text): file_text, levels, children, params = seq_file_text fd = StringIO(file_text) s = SequenceHandler(file_obj=fd) assert len(s._sequences) == non_empty_lines(file_text) for index, lev in enumerate(levels): assert s._sequences[index].level == lev for index, c in enumerate(children): assert len(s.children(s._sequences[index])) == c for index, c in enumerate(params): assert s._sequences[index].parameter == c seq_file_text_err1 = """ - "P1", "[1,2" -- "P2", "[3, 4, 5]" """ seq_file_text_err2 = """ - "P1", "[1,2]" --- "P2", "[3, 4, 5]" - "P1", "[4,5]" """ seq_file_text_err3 = """ - "P1", "[1,2]" -- "P2", "[3, 4, 5]" --- "P3", "" """ @pytest.mark.parametrize("seq_file_text_err", [(seq_file_text_err1, SequenceEvaluationError, "SyntaxError, likely unbalanced brackets"), (seq_file_text_err2, SequenceEvaluationError, 'Invalid file format: level missing ?'), (seq_file_text_err3, SequenceEvaluationError, "No sequence entered")]) def test_sequencer_errors(seq_file_text_err): file_text, exception, exc_text = seq_file_text_err fd = StringIO(file_text) with pytest.raises(exception, match=exc_text): seq = SequenceHandler(file_obj=fd) seq.parameters_sequence() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/experiment/test_workers.py0000644000175100001770000001106214623331163021644 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 import logging import pytest import os import tempfile from time import sleep from pymeasure.experiment import Listener, Procedure from pymeasure.experiment.workers import Worker from pymeasure.experiment.results import Results from data.procedure_for_testing import RandomProcedure tcp_libs_available = bool(importlib.util.find_spec('cloudpickle') and importlib.util.find_spec('zmq')) 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=20.0) 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=20.0) # Test if the file has been properly closed by removing the file os.remove(file) @pytest.mark.skipif(not tcp_libs_available, reason='TCP communication packages not installed') def test_zmq_does_not_crash_worker(caplog): """Check that a ZMQ serialisation usage error does not cause a crash. See https://github.com/ralph-group/pymeasure/issues/168 """ procedure = RandomProcedure() file = tempfile.mktemp() results = Results(procedure, file) # If we define a port here we get ZMQ communication # if cloudpickle is installed worker = Worker(results, port=5888, log_level=logging.DEBUG) worker.start() worker.join(timeout=20.0) # give it enough time to finish the procedure assert procedure.status == procedure.FINISHED del worker # make sure to clean up, reduce the possibility of test # dependencies via left-over sockets @pytest.mark.skipif(not tcp_libs_available, reason='TCP communication packages not installed') def test_zmq_topic_filtering_works(caplog): class ThreeEmitsProcedure(Procedure): def execute(self): self.emit('results', 'Data 1') self.emit('progress', 33) self.emit('results', 'Data 2') self.emit('progress', 66) self.emit('results', 'Data 3') self.emit('progress', 99) procedure = ThreeEmitsProcedure() file = tempfile.mktemp() results = Results(procedure, file) received = [] worker = Worker(results, port=5888, log_level=logging.DEBUG) listener = Listener(port=5888, topic='results', timeout=4.0) sleep(4.0) # leave time for subscriber and publisher to establish a connection worker.start() while True: if not listener.message_waiting(): break topic, record = listener.receive() received.append((topic, record)) worker.join(timeout=20.0) # give it enough time to finish the procedure assert procedure.status == procedure.FINISHED assert len(received) == 3 assert all([item[0] == 'results' for item in received]) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.429606 pymeasure-0.14.0/tests/instruments/0000755000175100001770000000000014623331176016756 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.429606 pymeasure-0.14.0/tests/instruments/activetechnologies/0000755000175100001770000000000014623331176022635 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/activetechnologies/test_AWG401x.py0000644000175100001770000001255014623331163025300 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments import Instrument from pymeasure.instruments.activetechnologies import AWG401x_AFG from pymeasure.instruments.activetechnologies.AWG401x import ChannelAFG, SequenceEntry class SequencerInstrument(Instrument): """A class in order to test SequenceEntry.""" def __init__(self, adapter, **kwargs): super().__init__(adapter, "SequencerInstrument", **kwargs) self.waveforms = {} self.se = SequenceEntry(self, 1, 7) # AFG Tests AFG_init_comm = [ # ("*IDN?", "x,AWG4012"), ("SOURce1:INITDELay? MINimum", "1"), ("SOURce1:INITDELay? MAXimum", "2"), ("SOURce1:VOLTage:LEVel:IMMediate:LOW? MINimum", "1"), ("SOURce1:VOLTage:LEVel:IMMediate:LOW? MAXimum", "2"), ("SOURce1:VOLTage:LEVel:IMMediate:HIGH? MINimum", "1"), ("SOURce1:VOLTage:LEVel:IMMediate:HIGH? MAXimum", "2"), ("SOURce1:VOLTage:LEVel:IMMediate:AMPLitude? MINimum", "VPP1"), ("SOURce1:VOLTage:LEVel:IMMediate:AMPLitude? MAXimum", "VPP2"), ("SOURce1:VOLTage:LEVel:IMMediate:OFFSet? MINimum", "1"), ("SOURce1:VOLTage:LEVel:IMMediate:OFFSet? MAXimum", "2"), ("SOURce1:VOLTage:BASELINE:OFFSET? MINimum", "1"), ("SOURce1:VOLTage:BASELINE:OFFSET? MAXimum", "2"), ("SOURce1:FREQuency? MINimum", "1"), ("SOURce1:FREQuency? MAXimum", "2"), ("SOURce1:PHASe:ADJust? MINimum", "1"), ("SOURce1:PHASe:ADJust? MAXimum", "2"), ("SOURce2:INITDELay? MINimum", "1"), ("SOURce2:INITDELay? MAXimum", "2"), ("SOURce2:VOLTage:LEVel:IMMediate:LOW? MINimum", "1"), ("SOURce2:VOLTage:LEVel:IMMediate:LOW? MAXimum", "2"), ("SOURce2:VOLTage:LEVel:IMMediate:HIGH? MINimum", "1"), ("SOURce2:VOLTage:LEVel:IMMediate:HIGH? MAXimum", "2"), ("SOURce2:VOLTage:LEVel:IMMediate:AMPLitude? MINimum", "VPP1"), ("SOURce2:VOLTage:LEVel:IMMediate:AMPLitude? MAXimum", "VPP2"), ("SOURce2:VOLTage:LEVel:IMMediate:OFFSet? MINimum", "1"), ("SOURce2:VOLTage:LEVel:IMMediate:OFFSet? MAXimum", "2"), ("SOURce2:VOLTage:BASELINE:OFFSET? MINimum", "1"), ("SOURce2:VOLTage:BASELINE:OFFSET? MAXimum", "2"), ("SOURce2:FREQuency? MINimum", "1"), ("SOURce2:FREQuency? MAXimum", "2"), ("SOURce2:PHASe:ADJust? MINimum", "1"), ("SOURce2:PHASe:ADJust? MAXimum", "2"), ("*IDN?", "x,AWG4012"), ] def test_AFG_init(): with expected_protocol( AWG401x_AFG, AFG_init_comm, ) as inst: assert len(inst.channels) == 2 assert isinstance(inst.ch_1, ChannelAFG) def test_AFG_frequency_setter(): with expected_protocol( AWG401x_AFG, [*AFG_init_comm, ("SOURce2:FREQuency 1.5", None), ], ) as inst: inst.ch_2.frequency = 1.5 def test_AFG_frequency_getter(): with expected_protocol( AWG401x_AFG, [*AFG_init_comm, ("SOURce2:FREQuency?", "1.5"), ], ) as inst: assert inst.ch_2.frequency == 1.5 # SequenceEntry Tests Sequence_init_comm = [ ("SEQuence:ELEM7:LENGth? MINimum", "1"), ("SEQuence:ELEM7:LENGth? MAXimum", "2"), ("SEQuence:ELEM7:LOOP:COUNt? MINimum", "1"), ("SEQuence:ELEM7:LOOP:COUNt? MAXimum", "2"), ("SEQuence:ELEM7:AMPlitude1? MINimum", "1"), ("SEQuence:ELEM7:AMPlitude1? MAXimum", "2"), ("SEQuence:ELEM7:OFFset1? MINimum", "1"), ("SEQuence:ELEM7:OFFset1? MAXimum", "2"), ("SEQuence:ELEM7:VOLTage:HIGH1? MINimum", "1"), ("SEQuence:ELEM7:VOLTage:HIGH1? MAXimum", "2"), ("SEQuence:ELEM7:VOLTage:LOW1? MINimum", "1"), ("SEQuence:ELEM7:VOLTage:LOW1? MAXimum", "2"), ] def test_SequenceEntry_init(): with expected_protocol( SequencerInstrument, Sequence_init_comm, ): pass # verify init def test_SequenceEntry_voltage_amplitude_setter(): with expected_protocol( SequencerInstrument, [*Sequence_init_comm, ("SEQuence:ELEM7:AMPlitude1 1.5", None)], ) as inst: inst.se.ch_1.voltage_amplitude = 1.5 def test_SequenceEntry_voltage_amplitude_getter(): with expected_protocol( SequencerInstrument, [*Sequence_init_comm, ("SEQuence:ELEM7:AMPlitude1?", "1.5")], ) as inst: assert inst.se.ch_1.voltage_amplitude == 1.5 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.429606 pymeasure-0.14.0/tests/instruments/advantest/0000755000175100001770000000000014623331176020747 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/advantest/test_advantestR624X.py0000644000175100001770000000401714623331163025055 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.advantest import AdvantestR6246 def test_init(): with expected_protocol( AdvantestR6246, [], ): pass # Verify the expected communication. def test_set_current(): with expected_protocol( AdvantestR6246, [("di 1,0,2.1100e-04,2.1300e-04", None), ("spot 1,2.3120e-03", None), (None, "ABCD 7.311e-4")] ) as inst: inst.ch_A.current_source(0, 0.000211, 2.13e-4) inst.ch_A.change_source_current = 23.12e-4 assert inst.read_measurement() == 0.0007311 def test_event_status_setter(): with expected_protocol( AdvantestR6246, [('*ese 255', None), ('*ese?', "200")] ) as inst: inst.event_status_enable = 258 # too large, gets truncated assert inst.event_status_enable == 200 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.429606 pymeasure-0.14.0/tests/instruments/agilent/0000755000175100001770000000000014623331176020401 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/agilent/test_agilent33500.py0000644000175100001770000002361514623331163024033 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.agilent.agilent33500 import Agilent33500 @pytest.mark.parametrize( "shape", ["SIN", "SQU", "TRI", "RAMP", "PULS", "PRBS", "NOIS", "ARB", "DC"] ) def test_shape(shape): """ Test Agilent 33500 shape function """ with expected_protocol( Agilent33500, [ ("SOUR1:FUNC?", shape), ("SOUR2:FUNC?", shape), ("FUNC?", shape), (f"SOUR1:FUNC {shape}", None), (f"SOUR2:FUNC {shape}", None), (f"FUNC {shape}", None), ], ) as inst: assert shape == inst.ch_1.shape assert shape == inst.ch_2.shape assert shape == inst.shape inst.ch_1.shape = shape inst.ch_2.shape = shape inst.shape = shape @pytest.mark.parametrize("frequency", [1e-6, 1.2e8]) def test_frequency(frequency): """ Test Agilent 33500 frequency function """ with expected_protocol( Agilent33500, [ ("SOUR1:FREQ?", frequency), ("SOUR2:FREQ?", frequency), ("FREQ?", frequency), (f"SOUR1:FREQ {frequency:.6f}", None), (f"SOUR2:FREQ {frequency:.6f}", None), (f"FREQ {frequency:.6f}", None), ], ) as inst: assert frequency == inst.ch_1.frequency assert frequency == inst.ch_2.frequency assert frequency == inst.frequency inst.ch_1.frequency = frequency inst.ch_2.frequency = frequency inst.frequency = frequency @pytest.mark.parametrize("amplitude", [10e-3, 10]) def test_amplitude(amplitude): """ Test Agilent 33500 amplitude function """ with expected_protocol( Agilent33500, [ ("SOUR1:VOLT?", amplitude), ("SOUR2:VOLT?", amplitude), ("VOLT?", amplitude), (f"SOUR1:VOLT {amplitude:.6f}", None), (f"SOUR2:VOLT {amplitude:.6f}", None), (f"VOLT {amplitude:.6f}", None), ], ) as inst: assert amplitude == inst.ch_1.amplitude assert amplitude == inst.ch_2.amplitude assert amplitude == inst.amplitude inst.ch_1.amplitude = amplitude inst.ch_2.amplitude = amplitude inst.amplitude = amplitude @pytest.mark.parametrize("amplitude_unit", ["VPP", "VRMS", "DBM"]) def test_amplitude_unit(amplitude_unit): """ Test Agilent 33500 amplitude unit function """ with expected_protocol( Agilent33500, [ ("SOUR1:VOLT:UNIT?", amplitude_unit), ("SOUR2:VOLT:UNIT?", amplitude_unit), ("VOLT:UNIT?", amplitude_unit), (f"SOUR1:VOLT:UNIT {amplitude_unit}", None), (f"SOUR2:VOLT:UNIT {amplitude_unit}", None), (f"VOLT:UNIT {amplitude_unit}", None), ], ) as inst: assert amplitude_unit == inst.ch_1.amplitude_unit assert amplitude_unit == inst.ch_2.amplitude_unit assert amplitude_unit == inst.amplitude_unit inst.ch_1.amplitude_unit = amplitude_unit inst.ch_2.amplitude_unit = amplitude_unit inst.amplitude_unit = amplitude_unit @pytest.mark.parametrize("state", [0, 1]) def test_output(state): """ Test Agilent 33500 output function """ with expected_protocol( Agilent33500, [ ("OUTP1?", state), ("OUTP2?", state), ("OUTP?", state), (f"OUTP1 {state}", None), (f"OUTP2 {state}", None), (f"OUTP {state}", None), ], ) as inst: assert state == inst.ch_1.output assert state == inst.ch_2.output assert state == inst.output inst.ch_1.output = state inst.ch_2.output = state inst.output = state @pytest.mark.parametrize("offset", [-4.995, 4.995]) def test_offset(offset): """ Test Agilent 33500 offset function """ with expected_protocol( Agilent33500, [ ("SOUR1:VOLT:OFFS?", offset), ("SOUR2:VOLT:OFFS?", offset), ("VOLT:OFFS?", offset), (f"SOUR1:VOLT:OFFS {offset:.6f}", None), (f"SOUR2:VOLT:OFFS {offset:.6f}", None), (f"VOLT:OFFS {offset:.6f}", None), ], ) as inst: assert offset == inst.ch_1.offset assert offset == inst.ch_2.offset assert offset == inst.offset inst.ch_1.offset = offset inst.ch_2.offset = offset inst.offset = offset @pytest.mark.parametrize("voltage_high", [-4.995, 4.995]) def test_voltage_high(voltage_high): """ Test Agilent 33500 voltage_high function """ with expected_protocol( Agilent33500, [ ("SOUR1:VOLT:HIGH?", voltage_high), ("SOUR2:VOLT:HIGH?", voltage_high), ("VOLT:HIGH?", voltage_high), (f"SOUR1:VOLT:HIGH {voltage_high:.6f}", None), (f"SOUR2:VOLT:HIGH {voltage_high:.6f}", None), (f"VOLT:HIGH {voltage_high:.6f}", None), ], ) as inst: assert voltage_high == inst.ch_1.voltage_high assert voltage_high == inst.ch_2.voltage_high assert voltage_high == inst.voltage_high inst.ch_1.voltage_high = voltage_high inst.ch_2.voltage_high = voltage_high inst.voltage_high = voltage_high @pytest.mark.parametrize("voltage_low", [-4.995, 4.995]) def test_voltage_low(voltage_low): """ Test Agilent 33500 voltage_low function """ with expected_protocol( Agilent33500, [ ("SOUR1:VOLT:LOW?", voltage_low), ("SOUR2:VOLT:LOW?", voltage_low), ("VOLT:LOW?", voltage_low), (f"SOUR1:VOLT:LOW {voltage_low:.6f}", None), (f"SOUR2:VOLT:LOW {voltage_low:.6f}", None), (f"VOLT:LOW {voltage_low:.6f}", None), ], ) as inst: assert voltage_low == inst.ch_1.voltage_low assert voltage_low == inst.ch_2.voltage_low assert voltage_low == inst.voltage_low inst.ch_1.voltage_low = voltage_low inst.ch_2.voltage_low = voltage_low inst.voltage_low = voltage_low @pytest.mark.parametrize("phase", [-360, 360]) def test_phase(phase): """ Test Agilent 33500 phase function """ with expected_protocol( Agilent33500, [ ("SOUR1:PHAS?", phase), ("SOUR2:PHAS?", phase), ("PHAS?", phase), (f"SOUR1:PHAS {phase:.6f}", None), (f"SOUR2:PHAS {phase:.6f}", None), (f"PHAS {phase:.6f}", None), ], ) as inst: assert phase == inst.ch_1.phase assert phase == inst.ch_2.phase assert phase == inst.phase inst.ch_1.phase = phase inst.ch_2.phase = phase inst.phase = phase @pytest.mark.parametrize("square_dutycycle", [0.01, 99.98]) def test_square_dutycycle(square_dutycycle): """ Test Agilent 33500 square_dutycycle function """ with expected_protocol( Agilent33500, [ ("SOUR1:FUNC:SQU:DCYC?", square_dutycycle), ("SOUR2:FUNC:SQU:DCYC?", square_dutycycle), ("FUNC:SQU:DCYC?", square_dutycycle), (f"SOUR1:FUNC:SQU:DCYC {square_dutycycle:.6f}", None), (f"SOUR2:FUNC:SQU:DCYC {square_dutycycle:.6f}", None), (f"FUNC:SQU:DCYC {square_dutycycle:.6f}", None), ], ) as inst: assert square_dutycycle == inst.ch_1.square_dutycycle assert square_dutycycle == inst.ch_2.square_dutycycle assert square_dutycycle == inst.square_dutycycle inst.ch_1.square_dutycycle = square_dutycycle inst.ch_2.square_dutycycle = square_dutycycle inst.square_dutycycle = square_dutycycle @pytest.mark.parametrize("ramp_symmetry", [0.01, 99.98]) def test_ramp_symmetry(ramp_symmetry): """ Test Agilent 33500 ramp_symmetry function """ with expected_protocol( Agilent33500, [ ("SOUR1:FUNC:RAMP:SYMM?", ramp_symmetry), ("SOUR2:FUNC:RAMP:SYMM?", ramp_symmetry), ("FUNC:RAMP:SYMM?", ramp_symmetry), (f"SOUR1:FUNC:RAMP:SYMM {ramp_symmetry:.6f}", None), (f"SOUR2:FUNC:RAMP:SYMM {ramp_symmetry:.6f}", None), (f"FUNC:RAMP:SYMM {ramp_symmetry:.6f}", None), ], ) as inst: assert ramp_symmetry == inst.ch_1.ramp_symmetry assert ramp_symmetry == inst.ch_2.ramp_symmetry assert ramp_symmetry == inst.ramp_symmetry inst.ch_1.ramp_symmetry = ramp_symmetry inst.ch_2.ramp_symmetry = ramp_symmetry inst.ramp_symmetry = ramp_symmetry def test_channel_phase_synchronization(): """ Test Agilent 33500 channel phase synchronization """ with expected_protocol( Agilent33500, [ ("PHAS:SYNC", None) ] ) as inst: assert inst.phase_sync() is None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/agilent/test_agilent33500_with_device.py0000644000175100001770000004001014623331163026371 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.agilent33500 import Agilent33500 from math import pi, sin pytest.skip('Only works with connected hardware', allow_module_level=True) # from pyvisa.errors import VisaIOError ############ # FIXTURES # ############ @pytest.fixture(scope="session") def generator(): try: generator = Agilent33500("TCPIP::192.168.225.208::inst0::INSTR") except IOError: print("Not able to connect to waveform generator") assert False yield generator for channel in 1, 2: generator.channels[channel].output = "off" generator.channels[channel].amplitude = 0.1 generator.channels[channel].offset = 0.1 generator.channels[channel].frequency = 60 ######## # DATA # ######## BOOLEANS = [True, False] CHANNELS = [1, 2] WF_SHAPES = ["SIN", "SQU", "TRI", "RAMP", "PULS", "PRBS", "NOIS", "ARB", "DC"] AMPLITUDE_RANGE = [0.01, 10] PHASE_RANGE = [-360, 360] AMPLITUDE_UNIT = ["VPP", "VRMS", "DBM"] OFFSET_RANGE = [-4.995, 4.995] VOLTAGE_HIGH_RANGE = [-4.999, 5] VOLTAGE_LOW_RANGE = [-5, 4.999] SQUARE_DUTYCYCLE_RANGE = [0.01, 99.98] RAMP_SYMMETRY_RANGE = [0, 100] PULSE_PERIOD_RANGE = [33e-9, 1e6] PULSE_HOLD_RANGE = [["WIDT", "WIDT"], ["WIDTH", "WIDT"], ["DCYC", "DCYC"], ["DCYCLE", "DCYC"]] PULSE_WIDTH_RANGE = [16e-9, 1e6] PULSE_DUTYCYCLE_RANGE = [0.1, 100] PULSE_TRANSITION_RANGE = [8.4e-9, 1e-6] BURST_MODES = [["TRIG", "TRIG"], ["TRIGGERED", "TRIG"], ["GAT", "GAT"], ["GATED", "GAT"]] BURST_PERIOD = [1e-6, 8000] BURST_NCYCLES = [1, 99999] SRATE = [1e-6, 1.6e8] def generate_simple_harmonic_waveform(harmonic, distortion): """ Generates a waveform with onlhy one harmonic :param: harmonic: Harmonic number :param: distortion: Distortion of the harmonic in % """ samples_per_cycle = 2048 number_of_cycles = 4 frequency = 60 fundamental_amplitude = 0.707 # in ptp time_step = (1 / frequency) / samples_per_cycle waveform = [] time = 0 while time < ((1 / frequency) * number_of_cycles): fundamental = fundamental_amplitude * sin(2 * pi * frequency * time) harmonic_amplitude = ( (distortion / 100) * fundamental_amplitude * sin(harmonic * 2 * pi * frequency * time) ) waveform.append(fundamental + harmonic_amplitude) time += time_step return waveform ######### # TESTS # ######### ##################### # NON-CHANNEL TESTS # ##################### def test_get_instrument_id(generator): assert "Agilent Technologies" in generator.id def test_turn_on(generator): generator.output = "on" assert generator.output @pytest.mark.parametrize("shape", WF_SHAPES) def test_shape(generator, shape): generator.shape = shape assert shape == generator.shape @pytest.mark.parametrize("frequency", [0.1, 1, 10, 100, 1000]) def test_frequency(generator, frequency): generator.frequency = frequency assert frequency == pytest.approx(generator.frequency, 0.01) @pytest.mark.parametrize("amplitude", AMPLITUDE_RANGE) def test_amplitude(generator, amplitude): generator.amplitude = amplitude assert amplitude == pytest.approx(generator.amplitude, 0.01) @pytest.mark.xfail @pytest.mark.parametrize("amplitude_unit", AMPLITUDE_UNIT) def test_amplitude_unit(generator, amplitude_unit): generator.amplitude_unit = amplitude_unit assert amplitude_unit == generator.amplitude_unit @pytest.mark.parametrize("offset", OFFSET_RANGE) def test_offset(generator, offset): generator.offset = offset assert offset == pytest.approx(generator.offset, 0.01) @pytest.mark.parametrize("voltage_high", VOLTAGE_HIGH_RANGE) def test_voltage_high( generator, voltage_high, ): generator.voltage_high = voltage_high assert voltage_high == pytest.approx(generator.voltage_high, 0.01) @pytest.mark.parametrize("voltage_low", VOLTAGE_LOW_RANGE) def test_voltage_low( generator, voltage_low, ): generator.voltage_low = voltage_low assert voltage_low == pytest.approx(generator.voltage_low, 0.01) @pytest.mark.parametrize("phase", range(PHASE_RANGE[0], PHASE_RANGE[1], 10)) def test_phase(generator, phase): generator.phase = phase assert phase == pytest.approx(generator.phase, 0.01) @pytest.mark.parametrize("square_dutycycle", SQUARE_DUTYCYCLE_RANGE) def test_square_dutycycle(generator, square_dutycycle): generator.square_dutycycle = square_dutycycle assert square_dutycycle == pytest.approx(generator.square_dutycycle, 0.01) @pytest.mark.parametrize("ramp_symmetry", RAMP_SYMMETRY_RANGE) def test_ramp_symmetry(generator, ramp_symmetry): generator.ramp_symmetry = ramp_symmetry assert ramp_symmetry == pytest.approx(generator.ramp_symmetry, 0.01) @pytest.mark.xfail # seems like my device 33500B only supports a min of 5e-08 range period @pytest.mark.parametrize("pulse_period", PULSE_PERIOD_RANGE) def test_pulse_period(generator, pulse_period): generator.pulse_period = pulse_period assert pulse_period == pytest.approx(generator.pulse_period, 0.01) @pytest.mark.parametrize("pulse_hold, expected", PULSE_HOLD_RANGE) def test_pulse_hold(generator, pulse_hold, expected): generator.pulse_hold = pulse_hold assert expected == generator.pulse_hold @pytest.mark.parametrize("pulse_width", PULSE_WIDTH_RANGE) def test_pulse_width(generator, pulse_width): generator.pulse_width = pulse_width assert pulse_width == pytest.approx(generator.pulse_width, 0.01) # 33500B minimum dutycycle seems to be 0.1 @pytest.mark.parametrize("pulse_dutycycle", PULSE_DUTYCYCLE_RANGE) def test_pulse_dutycycle(generator, pulse_dutycycle): generator.pulse_dutycycle = pulse_dutycycle assert pulse_dutycycle == pytest.approx(generator.pulse_dutycycle, 0.1) @pytest.mark.parametrize("pulse_transition", PULSE_TRANSITION_RANGE) def test_pulse_transition(generator, pulse_transition): generator.pulse_transition = pulse_transition assert pulse_transition == pytest.approx(generator.pulse_transition, 0.1) @pytest.mark.parametrize("output_load", [1, 10000, "INF"]) def test_output_load(generator, output_load): generator.output_load = output_load if output_load == "INF": assert generator.output_load == 9.9e37 else: assert output_load == generator.output_load @pytest.mark.xfail @pytest.mark.parametrize("boolean", BOOLEANS) def test_burst_state(generator, boolean): generator.burst_state = boolean assert boolean == generator.burst_state @pytest.mark.parametrize("burst_mode, expected", BURST_MODES) def test_burst_mode(generator, burst_mode, expected): generator.burst_mode = burst_mode assert expected == generator.burst_mode @pytest.mark.parametrize("burst_period", BURST_PERIOD) def test_burst_period(generator, burst_period): generator.burst_period = burst_period assert burst_period == generator.burst_period @pytest.mark.parametrize("burst_ncycles", BURST_NCYCLES) def test_burst_ncycles(generator, burst_ncycles): generator.burst_ncycles = burst_ncycles assert burst_ncycles == generator.burst_ncycles def test_arb_file_should_not_be_empty(generator): file = generator.arb_file assert file != "" @pytest.mark.parametrize("srate", SRATE) def test_arb_srate(generator, srate): generator.arb_srate = srate assert srate == generator.arb_srate def test_uploaded_arb_file(generator): waveform = generate_simple_harmonic_waveform(3, 10) generator.shape = "ARB" generator.data_arb("test", waveform, data_format="float") generator.arb_file = "test" assert '"TEST"' == generator.arb_file ################# # CHANNEL TESTS # ################# @pytest.mark.parametrize("channel", CHANNELS) def test_turn_on_channel(generator, channel): generator.channels[channel].output = "on" assert generator.channels[channel].output @pytest.mark.parametrize("shape", WF_SHAPES) @pytest.mark.parametrize("channel", CHANNELS) def test_shape_channel(generator, shape, channel): generator.channels[channel].shape = shape assert shape == generator.channels[channel].shape @pytest.mark.parametrize("frequency", [0.1, 1, 10, 100, 1000]) @pytest.mark.parametrize("channel", CHANNELS) def test_frequency_channel(generator, frequency, channel): generator.channels[channel].frequency = frequency assert frequency == pytest.approx(generator.channels[channel].frequency, 0.01) @pytest.mark.parametrize("amplitude", AMPLITUDE_RANGE) @pytest.mark.parametrize("channel", CHANNELS) def test_amplitude_channel(generator, amplitude, channel): generator.channels[channel].amplitude = amplitude assert amplitude == pytest.approx(generator.channels[channel].amplitude, 0.01) @pytest.mark.xfail @pytest.mark.parametrize("amplitude_unit", AMPLITUDE_UNIT) @pytest.mark.parametrize("channel", CHANNELS) def test_amplitude_unit_channel(generator, amplitude_unit, channel): generator.channels[channel].amplitude_unit = amplitude_unit assert amplitude_unit == generator.channels[channel].amplitude_unit @pytest.mark.parametrize("offset", OFFSET_RANGE) @pytest.mark.parametrize("channel", CHANNELS) def test_offset_channel(generator, offset, channel): generator.channels[channel].offset = offset assert offset == pytest.approx(generator.channels[channel].offset, 0.01) @pytest.mark.parametrize("voltage_high", VOLTAGE_HIGH_RANGE) @pytest.mark.parametrize("channel", CHANNELS) def test_voltage_high_channel(generator, voltage_high, channel): generator.channels[channel].voltage_high = voltage_high assert voltage_high == pytest.approx(generator.channels[channel].voltage_high, 0.01) @pytest.mark.parametrize("voltage_low", VOLTAGE_LOW_RANGE) @pytest.mark.parametrize("channel", CHANNELS) def test_voltage_low_channel(generator, voltage_low, channel): generator.channels[channel].voltage_low = voltage_low assert voltage_low == pytest.approx(generator.channels[channel].voltage_low, 0.01) @pytest.mark.parametrize("phase", range(PHASE_RANGE[0], PHASE_RANGE[1], 10)) @pytest.mark.parametrize("channel", CHANNELS) def test_phase_channel(generator, phase, channel): generator.channels[channel].phase = phase assert phase == pytest.approx(generator.channels[channel].phase, 0.01) @pytest.mark.parametrize("square_dutycycle", SQUARE_DUTYCYCLE_RANGE) @pytest.mark.parametrize("channel", CHANNELS) def test_square_dutycycle_channel(generator, square_dutycycle, channel): generator.channels[channel].square_dutycycle = square_dutycycle assert square_dutycycle == pytest.approx(generator.channels[channel].square_dutycycle, 0.01) @pytest.mark.parametrize("ramp_symmetry", RAMP_SYMMETRY_RANGE) @pytest.mark.parametrize("channel", CHANNELS) def test_ramp_symmetry_channel(generator, ramp_symmetry, channel): generator.channels[channel].ramp_symmetry = ramp_symmetry assert ramp_symmetry == pytest.approx(generator.channels[channel].ramp_symmetry, 0.01) @pytest.mark.xfail # seems like my device 33500B only supports a min of 5e-08 range period @pytest.mark.parametrize("pulse_period", PULSE_PERIOD_RANGE) @pytest.mark.parametrize("channel", CHANNELS) def test_pulse_period_channel(generator, pulse_period, channel): generator.channels[channel].pulse_period = pulse_period assert pulse_period == pytest.approx(generator.channels[channel].pulse_period, 0.01) @pytest.mark.parametrize("pulse_hold, expected", PULSE_HOLD_RANGE) @pytest.mark.parametrize("channel", CHANNELS) def test_pulse_hold_channel(generator, pulse_hold, expected, channel): generator.channels[channel].pulse_hold = pulse_hold assert expected == generator.channels[channel].pulse_hold @pytest.mark.parametrize("pulse_width", PULSE_WIDTH_RANGE) @pytest.mark.parametrize("channel", CHANNELS) def test_pulse_width_channel(generator, pulse_width, channel): generator.channels[channel].pulse_width = pulse_width assert pulse_width == pytest.approx(generator.channels[channel].pulse_width, 0.01) # 33500B minimum dutycycle seems to be 0.1 @pytest.mark.parametrize("pulse_dutycycle", PULSE_DUTYCYCLE_RANGE) @pytest.mark.parametrize("channel", CHANNELS) def test_pulse_dutycycle_channel(generator, pulse_dutycycle, channel): generator.channels[channel].pulse_dutycycle = pulse_dutycycle assert pulse_dutycycle == pytest.approx(generator.channels[channel].pulse_dutycycle, 0.1) @pytest.mark.parametrize("pulse_transition", PULSE_TRANSITION_RANGE) @pytest.mark.parametrize("channel", CHANNELS) def test_pulse_transition_channel(generator, pulse_transition, channel): generator.channels[channel].pulse_transition = pulse_transition assert pulse_transition == pytest.approx(generator.channels[channel].pulse_transition, 0.1) @pytest.mark.parametrize("output_load", [1, 10000, "INF"]) @pytest.mark.parametrize("channel", CHANNELS) def test_output_load_channel(generator, channel, output_load): generator.channels[channel].output_load = output_load if output_load == "INF": assert generator.channels[channel].output_load == 9.9e37 else: assert output_load == generator.channels[channel].output_load @pytest.mark.xfail @pytest.mark.parametrize("boolean", BOOLEANS) @pytest.mark.parametrize("channel", CHANNELS) def test_burst_state_channel(generator, boolean, channel): generator.channels[channel].burst_state = boolean assert boolean == generator.channels[channel].burst_state @pytest.mark.parametrize("burst_mode, expected", BURST_MODES) @pytest.mark.parametrize("channel", CHANNELS) def test_burst_mode_channel(generator, burst_mode, expected, channel): generator.channels[channel].burst_mode = burst_mode assert expected == generator.channels[channel].burst_mode @pytest.mark.parametrize("burst_period", BURST_PERIOD) @pytest.mark.parametrize("channel", CHANNELS) def test_burst_period_channel(generator, burst_period, channel): generator.channels[channel].burst_period = burst_period assert burst_period == generator.channels[channel].burst_period @pytest.mark.parametrize("burst_ncycles", BURST_NCYCLES) @pytest.mark.parametrize("channel", CHANNELS) def test_burst_ncycles_channel(generator, burst_ncycles, channel): generator.channels[channel].burst_ncycles = burst_ncycles assert burst_ncycles == generator.channels[channel].burst_ncycles @pytest.mark.parametrize("channel", CHANNELS) def test_arb_file_channel(generator, channel): file = generator.channels[channel].arb_file assert file != "" @pytest.mark.parametrize("srate", SRATE) @pytest.mark.parametrize("channel", CHANNELS) def test_arb_srate_channel(generator, channel, srate): generator.channels[channel].arb_srate = srate assert srate == generator.channels[channel].arb_srate @pytest.mark.parametrize("channel", CHANNELS) def test_uploaded_arb_file_channel(generator, channel): waveform = generate_simple_harmonic_waveform(3, 10) generator.channels[channel].shape = "ARB" generator.channels[channel].data_arb("test", waveform, data_format="float") generator.channels[channel].arb_file = "test" assert '"TEST"' == generator.channels[channel].arb_file def test_phase_sync(generator): generator.phase_sync() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/agilent/test_agilent34450A_with_device.py0000644000175100001770000004701414623331163026512 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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)): _ = 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/agilent/test_agilent4284A.py0000644000175100001770000001021714623331163024055 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.agilent import Agilent4284A @pytest.mark.parametrize("frequency", [20, 100, 1e4, 1e6]) def test_frequency(frequency): with expected_protocol( Agilent4284A, [("FREQ?", frequency), (f"FREQ {frequency:g}", None),], ) as inst: assert frequency == inst.frequency inst.frequency = frequency def test_frequency_limit(): with pytest.raises(ValueError): with expected_protocol( Agilent4284A, [("FREQ 1", None)], ) as inst: inst.frequency = 1 @pytest.mark.parametrize("power_mode", ["0", "1"]) def test_high_power_mode(power_mode): with expected_protocol( Agilent4284A, [("OUTP:HPOW?", power_mode),], ) as inst: assert bool(power_mode) == inst.high_power_enabled @pytest.mark.parametrize("impedance_mode", [ "CPD", "CPQ", "CPG", "CPRP", "CSD", "CSQ", "CSRS", "LPQ", "LPD", "LPG", "LPRP", "LSD", "LSQ", "LSRS", "RX", "ZTD", "ZTR", "GB", "YTD", "YTR"]) def test_impedance_mode(impedance_mode): with expected_protocol( Agilent4284A, [("FUNC:IMP?", impedance_mode), (f"FUNC:IMP {impedance_mode}", None),], ) as inst: assert impedance_mode == inst.impedance_mode inst.impedance_mode = impedance_mode def test_enable_high_power(): with expected_protocol( Agilent4284A, [("*OPT?", "1,0,0,0,0"), ("OUTP:HPOW 1", None), ("VOLT:LEV 5", None),], ) as inst: inst.high_power_enabled = True inst.ac_voltage = 5 def test_disable_high_power(): with pytest.raises(ValueError): with expected_protocol( Agilent4284A, [("OUTP:HPOW 0", None), ("VOLT:LEV 5", None)], ) as inst: inst.high_power_enabled = False inst.ac_voltage = 5 @pytest.fixture def param_list(): freq_input = [1e7, 5e6, 1e6, 5e5, 1e5, 5e4, 1e4, 5e3, 1e3, 500, 100, 50, 20, 10] freq_str = "1000000,500000,100000,50000,10000,5000,1000,500,100,50,20" freq_output = [1e6, 5e5, 1e5, 5e4, 1e4, 5e3, 1e3, 500, 100, 50, 20] measured_str = "0.5,-0.785,+0,+0," return freq_input, freq_str, freq_output, measured_str def sweep_measurement(param_list): with expected_protocol( Agilent4284A, [("*CLS", None), ("TRIG:SOUR BUS;:DISP:PAGE LIST;:FORM ASC;:LIST:MODE SEQ;:INIT:CONT ON", None), (f"LIST:FREQ {param_list[1][:-3]};:TRIG:IMM", None), ("STAT:OPER?", "+8"), ("FETCH?", f"{param_list[3]*10}"), ("LIST:FREQ?", f"{param_list[1][:-3]}"), ("LIST:FREQ 20;:TRIG:IMM", None), ("STAT:OPER?", "+8"), ("FETCH?", f"{param_list[3]}"), ("LIST:FREQ?", "20"), ("SYST:ERR?", '0,"No error"')], ) as inst: results = inst.sweep_measurement('frequency', param_list[0]) assert results[0] == [0.5,] * 11 assert results[1] == [-0.785,] * 11 assert results[2] == param_list[2] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/agilent/test_agilent4294A.py0000644000175100001770000000435614623331163024065 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.agilent.agilent4294A import Agilent4294A @pytest.mark.parametrize("freq", [40, 140E6]) def test_set_start_freq(freq): """ Test Agilent 4294A start frequency setter """ with expected_protocol(Agilent4294A, [(f"STAR {freq:.0f} HZ", None), ],) as inst: inst.start_frequency = freq @pytest.mark.parametrize("freq", [40, 140E6]) def test_get_start_freq(freq): """ Test Agilent 4294A start frequency getter """ with expected_protocol(Agilent4294A, [("STAR?", freq), ],) as inst: assert freq == inst.start_frequency @pytest.mark.parametrize("freq", [40, 140E6]) def test_set_stop_freq(freq): """ Test Agilent 4294A stop frequency setter """ with expected_protocol(Agilent4294A, [(f"STOP {freq:.0f} HZ", None), ],) as inst: inst.stop_frequency = freq @pytest.mark.parametrize("freq", [40, 140E6]) def test_get_stop_freq(freq): """ Test Agilent 4294A stop frequency getter """ with expected_protocol(Agilent4294A, [("STOP?", freq), ],) as inst: assert freq == inst.stop_frequency ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4336061 pymeasure-0.14.0/tests/instruments/aimtti/0000755000175100001770000000000014623331176020245 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/aimtti/test_PL303QMDP_with_device.py0000644000175100001770000000362514623331163025455 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pytest from pymeasure.instruments.aimtti.aimttiPL import PL303QMDP @pytest.fixture(scope="module") def psu(connected_device_address): instr = PL303QMDP(connected_device_address) instr.reset() return instr def test_voltage(psu): psu.ch_2.voltage_setpoint = 1.2 psu.ch_2.current_limit = 1.0 psu.ch_2.current_range = "HIGH" psu.ch_2.output_enabled = True time.sleep(1) print(psu.ch_2.voltage) time.sleep(5) psu.ch_2.output_enabled = False def test_voltage_all(psu): psu.ch_2.voltage_setpoint = 1.2 psu.ch_2.current_limit = 1.0 psu.ch_1.voltage_setpoint = 24.0 psu.ch_1.current_limit = 1.2 psu.all_outputs_enabled = True time.sleep(5) psu.all_outputs_enabled = False psu.local() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/aimtti/test_aimttl.py0000644000175100001770000000631714623331163023153 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.aimtti.aimttiPL import PL303QMTP, PL303QMDP def test_voltage_setpoint(): with expected_protocol( PL303QMTP, [("V2V 1.2", None), ("V2?", "V2 1.2") ], ) as inst: inst.ch_2.voltage_setpoint = 1.2 assert inst.ch_2.voltage_setpoint == 1.2 def test_voltage(): with expected_protocol( PL303QMTP, [("V2O?", "1.2V") ], ) as inst: assert inst.ch_2.voltage == 1.2 def test_current_limit(): with expected_protocol( PL303QMTP, [("I2 0.1", None), ("I2?", "I2 0.1") ], ) as inst: inst.ch_2.current_limit = 0.1 assert inst.ch_2.current_limit == 0.1 def test_current(): with expected_protocol( PL303QMTP, [("I2O?", "0.123A") ], ) as inst: assert inst.ch_2.current == 0.123 def test_current_range(): with expected_protocol( PL303QMTP, [("IRANGE2 2", None), ("IRANGE2?", "2"), ("IRANGE2 1", None), ("IRANGE2?", "1") ], ) as inst: inst.ch_2.current_range = "HIGH" assert inst.ch_2.current_range == "HIGH" inst.ch_2.current_range = "LOW" assert inst.ch_2.current_range == "LOW" def test_enable(): with expected_protocol( PL303QMTP, [("OP2 1", None), ("OP2?", "1"), ("OP2 0", None), ("OPALL 1", None), ("OPALL 0", None) ], ) as inst: inst.ch_2.output_enabled = True assert inst.ch_2.output_enabled is True inst.ch_2.output_enabled = False inst.all_outputs_enabled = True inst.all_outputs_enabled = False def test_triple(): with expected_protocol( PL303QMTP, [("V3O?", "1.2V")], ) as inst: assert inst.ch_3.voltage == 1.2 def test_strict_range_error(): with expected_protocol( PL303QMDP, [], ) as inst: with pytest.raises(ValueError): inst.ch_1.voltage_setpoint = 31 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4336061 pymeasure-0.14.0/tests/instruments/aja/0000755000175100001770000000000014623331176017511 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/aja/test_dcxs.py0000644000175100001770000000516114623331163022062 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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.test import expected_protocol from pymeasure.instruments.aja.dcxs import DCXS def test_id(): """ Test DCXS identification property """ with expected_protocol( DCXS, [("?", "DCXS750-4"), ], ) as inst: assert inst.id == "DCXS750-4" @pytest.mark.parametrize("setpoint", [5, 97]) def test_setpoint(setpoint): """ Test DCXS setpoint """ with expected_protocol( DCXS, [ (f"C{setpoint:04d}", None), ("b", f"{setpoint:04d}"), ], ) as inst: inst.setpoint = setpoint assert setpoint == inst.setpoint def test_regulation_mode(): """ Test DCXS regulation mode """ with expected_protocol( DCXS, [ ("D0", None), ("c", 0), ], ) as inst: inst.regulation_mode = "power" assert "power" == inst.regulation_mode def test_enabled(): """ Test DCXS enabled """ with expected_protocol( DCXS, [ ("A", None), ("a", "1"), ], ) as inst: inst.enabled = True assert inst.enabled is True def test_material(): """ Test DCXS material name and its truncation """ with expected_protocol( DCXS, [ ("IALongNam", None), ("n", "ALongNam"), ], ) as inst: inst.material = "ALongNameWhichGetsTruncated" assert "ALongNam" == inst.material ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4336061 pymeasure-0.14.0/tests/instruments/ametek/0000755000175100001770000000000014623331176020224 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/ametek/test_ametek7270.py0000644000175100001770000002352214623331163023423 0ustar00runnerdockerimport pytest from pymeasure.test import expected_protocol from pymeasure.instruments.ametek import Ametek7270 def test_init(): with expected_protocol( Ametek7270, [], ): pass # Verify the expected communication. def test_sensitivity_getter(): with expected_protocol( Ametek7270, [(b'SEN', b'27\n')], ) as inst: assert inst.sensitivity == 1.0 def test_slope_getter(): with expected_protocol( Ametek7270, [(b'SLOPE', b'1\n')], ) as inst: assert inst.slope == 12 def test_time_constant_getter(): with expected_protocol( Ametek7270, [(b'TC', b'30\n')], ) as inst: assert inst.time_constant == 100000.0 def test_x_getter(): with expected_protocol( Ametek7270, [(b'X.', b'0.0E+00\n')], ) as inst: assert inst.x == 0.0 def test_y_getter(): with expected_protocol( Ametek7270, [(b'Y.', b'0.0E+00\n')], ) as inst: assert inst.y == 0.0 def test_xy_getter(): with expected_protocol( Ametek7270, [(b'XY.', b'0.0E+00,0.0E+00\n')], ) as inst: assert inst.xy == [0.0, 0.0] def test_mag_getter(): with expected_protocol( Ametek7270, [(b'MAG.', b'0.0E+00\n')], ) as inst: assert inst.mag == 0.0 def test_theta_getter(): with expected_protocol( Ametek7270, [(b'PHA.', b'-1.8E+02\n')], ) as inst: assert inst.theta == -180.0 @pytest.mark.parametrize("method, command", [('x1', 'X1.'), ('y1', 'Y1.'), ('x2', 'X2.'), ('y2', 'Y2.')]) def test_failing_properties(method, command): """in standard single reference mode, these tests should raise a ValueError""" with pytest.raises(ValueError): with expected_protocol( Ametek7270, [(f'{command}'.encode(), b'\n')] ) as inst: getattr(inst, method) == 0.0 def test_harmonic_getter(): with expected_protocol( Ametek7270, [(b'REFN', b'7\n')], ) as inst: assert inst.harmonic == 7 def test_phase_getter(): with expected_protocol( Ametek7270, [(b'REFP.', b'2.5E+02\n')], ) as inst: assert inst.phase == 250.0 def test_voltage_getter(): with expected_protocol( Ametek7270, [(b'OA.', b'0.0E+00\n')], ) as inst: assert inst.voltage == 0.0 def test_frequency_getter(): with expected_protocol( Ametek7270, [(b'OF.', b'1.2E+04\n')], ) as inst: assert inst.frequency == 12000.0 def test_dac1_getter(): with expected_protocol( Ametek7270, [(b'DAC. 1', b'7.0E+00\n')], ) as inst: assert inst.dac1 == 7.0 def test_dac2_getter(): with expected_protocol( Ametek7270, [(b'DAC. 2', b'-7.0E+00\n')], ) as inst: assert inst.dac2 == -7.0 def test_dac3_getter(): with expected_protocol( Ametek7270, [(b'DAC. 3', b'2.6E+00\n')], ) as inst: assert inst.dac3 == 2.6 def test_dac4_getter(): with expected_protocol( Ametek7270, [(b'DAC. 4', b'5.5E+00\n')], ) as inst: assert inst.dac4 == 5.5 def test_adc1_getter(): with expected_protocol( Ametek7270, [(b'ADC. 1', b'0.0E+00\n')], ) as inst: assert inst.adc1 == 0.0 def test_adc2_getter(): with expected_protocol( Ametek7270, [(b'ADC. 2', b'0.0E+00\n')], ) as inst: assert inst.adc2 == 0.0 def test_adc3_getter(): with expected_protocol( Ametek7270, [(b'ADC. 3', b'-1.6E-01\n')], ) as inst: assert inst.adc3 == -0.16 def test_adc4_getter(): with expected_protocol( Ametek7270, [(b'ADC. 4', b'-1.64E-01\n')], ) as inst: assert inst.adc4 == -0.164 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'SEN 0', b'')], 0.0), ([(b'SEN 1', b'')], 2e-09), ([(b'SEN 2', b'')], 5e-09), ([(b'SEN 3', b'')], 1e-08), ([(b'SEN 4', b'')], 2e-08), ([(b'SEN 5', b'')], 5e-08), ([(b'SEN 6', b'')], 1e-07), ([(b'SEN 7', b'')], 2e-07), ([(b'SEN 8', b'')], 5e-07), ([(b'SEN 9', b'')], 1e-06), ([(b'SEN 10', b'')], 2e-06), ([(b'SEN 11', b'')], 5e-06), ([(b'SEN 12', b'')], 1e-05), ([(b'SEN 13', b'')], 2e-05), ([(b'SEN 14', b'')], 5e-05), ([(b'SEN 15', b'')], 0.0001), ([(b'SEN 16', b'')], 0.0002), ([(b'SEN 17', b'')], 0.0005), ([(b'SEN 18', b'')], 0.001), ([(b'SEN 19', b'')], 0.002), ([(b'SEN 20', b'')], 0.005), ([(b'SEN 21', b'')], 0.01), ([(b'SEN 22', b'')], 0.02), ([(b'SEN 23', b'')], 0.05), ([(b'SEN 24', b'')], 0.1), ([(b'SEN 25', b'')], 0.2), ([(b'SEN 26', b'')], 0.5), ([(b'SEN 27', b'')], 1.0), )) def test_sensitivity_setter(comm_pairs, value): with expected_protocol( Ametek7270, comm_pairs, ) as inst: inst.sensitivity = value def test_slope_setter(): with expected_protocol( Ametek7270, [(b'SLOPE 1', b'')], ) as inst: inst.slope = 12 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'TC 0', b'')], 1e-05), ([(b'TC 1', b'')], 2e-05), ([(b'TC 2', b'')], 5e-05), ([(b'TC 3', b'')], 0.0001), ([(b'TC 4', b'')], 0.0002), ([(b'TC 5', b'')], 0.0005), ([(b'TC 6', b'')], 0.001), ([(b'TC 7', b'')], 0.002), ([(b'TC 8', b'')], 0.005), ([(b'TC 9', b'')], 0.01), ([(b'TC 10', b'')], 0.02), ([(b'TC 11', b'')], 0.05), ([(b'TC 12', b'')], 0.1), ([(b'TC 13', b'')], 0.2), ([(b'TC 14', b'')], 0.5), ([(b'TC 15', b'')], 1.0), ([(b'TC 16', b'')], 2.0), ([(b'TC 17', b'')], 5.0), ([(b'TC 18', b'')], 10.0), ([(b'TC 19', b'')], 20.0), ([(b'TC 20', b'')], 50.0), ([(b'TC 21', b'')], 100.0), ([(b'TC 22', b'')], 200.0), ([(b'TC 23', b'')], 500.0), ([(b'TC 24', b'')], 1000.0), ([(b'TC 25', b'')], 2000.0), ([(b'TC 26', b'')], 5000.0), ([(b'TC 27', b'')], 10000.0), ([(b'TC 28', b'')], 20000.0), ([(b'TC 29', b'')], 50000.0), ([(b'TC 30', b'')], 100000.0), )) def test_time_constant_setter(comm_pairs, value): with expected_protocol( Ametek7270, comm_pairs, ) as inst: inst.time_constant = value @pytest.mark.parametrize("comm_pairs, value", ( ([(b'REFN 7', b'')], 7), ([(b'REFN 7', b'')], 7), ([(b'REFN 7', b'')], 7), ([(b'REFN 7', b'')], 7), ([(b'REFN 7', b'')], 7), ([(b'REFN 7', b'')], 7), )) def test_harmonic_setter(comm_pairs, value): with expected_protocol( Ametek7270, comm_pairs, ) as inst: inst.harmonic = value def test_phase_setter(): with expected_protocol( Ametek7270, [(b'REFP. 250', b'')], ) as inst: inst.phase = 250 def test_voltage_setter(): with expected_protocol( Ametek7270, [(b'OA. 2.7', b'')], ) as inst: inst.voltage = 2.7 def test_frequency_setter(): with expected_protocol( Ametek7270, [(b'OF. 12000', b'')], ) as inst: inst.frequency = 12000 def test_dac1_setter(): with expected_protocol( Ametek7270, [(b'DAC. 1 7', b'')], ) as inst: inst.dac1 = 7 def test_dac2_setter(): with expected_protocol( Ametek7270, [(b'DAC. 2 -7', b'')], ) as inst: inst.dac2 = -7 def test_dac3_setter(): with expected_protocol( Ametek7270, [(b'DAC. 3 2.6', b'')], ) as inst: inst.dac3 = 2.6 def test_dac4_setter(): with expected_protocol( Ametek7270, [(b'DAC. 4 5.5', b'')], ) as inst: inst.dac4 = 5.5 @pytest.mark.parametrize("comm_pairs, value", ( ([], True), ([], False), )) def test_autogain_setter(comm_pairs, value): with expected_protocol( Ametek7270, comm_pairs, ) as inst: inst.autogain = value def test_set_voltage_mode(): with expected_protocol( Ametek7270, [(b'IMODE 0', b'')], ) as inst: assert inst.set_voltage_mode() is None def test_set_differential_mode(): with expected_protocol( Ametek7270, [(b'VMODE 3', b''), (b'LF 3 0', b'')], ) as inst: assert inst.set_differential_mode() is None @pytest.mark.parametrize("comm_pairs, args, kwargs, value", ( ([(b'IMODE 1', b'')], (), {'low_noise': False}, None), ([(b'IMODE 2', b'')], (), {'low_noise': True}, None), )) def test_set_current_mode(comm_pairs, args, kwargs, value): with expected_protocol( Ametek7270, comm_pairs, ) as inst: assert inst.set_current_mode(*args, **kwargs) == value def test_set_channel_A_mode(): with expected_protocol( Ametek7270, [(b'VMODE 1', b'')], ) as inst: assert inst.set_channel_A_mode() is None def test_id(): with expected_protocol( Ametek7270, [(b'ID', b'7270\n'), (b'VER', b'2.11\n')], ) as inst: assert inst.id == '7270/2.11' def test_shutdown(): with expected_protocol( Ametek7270, [(b'OA. 0', b'')], ) as inst: assert inst.shutdown() is None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4336061 pymeasure-0.14.0/tests/instruments/anaheimautomation/0000755000175100001770000000000014623331176022461 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/anaheimautomation/test_dpseriesmotorcontroller.py0000644000175100001770000000420614623331163031073 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.anaheimautomation import DPSeriesMotorController def test_init(): with expected_protocol( DPSeriesMotorController, []): pass # Verify the expected communication. def test_basespeed(): with expected_protocol( DPSeriesMotorController, [(b"@0VB", b"123")], ) as instr: assert instr.basespeed == 123 def test_basespeed_setter(): with expected_protocol( DPSeriesMotorController, [(b"@0B123", None)], ) as instr: instr.basespeed = 123 def test_step_position(): with expected_protocol( DPSeriesMotorController, [(b"@0VZ", b"13")], ) as instr: assert instr.step_position == 13 def test_step_position_setter(): with expected_protocol( DPSeriesMotorController, [(b"@0P13", None), (b"@0G", None)], ) as instr: instr.step_position = 13 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4336061 pymeasure-0.14.0/tests/instruments/anritsu/0000755000175100001770000000000014623331176020443 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/anritsu/test_anritsuMG3692C.py0000644000175100001770000000471114623331163024413 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.anritsu import AnritsuMG3692C def test_init(): with expected_protocol( AnritsuMG3692C, [], ): pass # Verify the expected communication. def test_power(): with expected_protocol( AnritsuMG3692C, [(b":POWER?;", "123.45")], ) as instr: assert instr.power == 123.45 def test_power_setter(): with expected_protocol( AnritsuMG3692C, [(b":POWER 123.45 dBm;", None)], ) as instr: instr.power = 123.45 def test_frequency(): with expected_protocol( AnritsuMG3692C, [(b":FREQUENCY?;", "123.45")], ) as instr: assert instr.frequency == 123.45 def test_frequency_setter(): with expected_protocol( AnritsuMG3692C, [(b":FREQUENCY 1.234500e+02 Hz;", None)], ) as instr: instr.frequency = 123.45 def test_output(): with expected_protocol( AnritsuMG3692C, [(b":OUTPUT?", "1")], ) as instr: assert instr.output is True def test_output_setter(): with expected_protocol( AnritsuMG3692C, [(b":OUTPUT ON;", None)], ) as instr: instr.output = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/anritsu/test_anritsuMS2090A.py0000644000175100001770000000361214623331163024413 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.anritsu import AnritsuMS2090A def test_init(): with expected_protocol( AnritsuMS2090A, [], ): pass # Verify the expected communication. def test_freq_conf(): with expected_protocol( AnritsuMS2090A, [(b"FREQuency:CENTer 9000", None), (b"FREQuency:CENTer?", 9000)], ) as instr: instr.frequency_center = 9000 assert instr.frequency_center == 9000 def test_preamp(): with expected_protocol( AnritsuMS2090A, [(b"POWer:RF:GAIN:STATe ON", None), (b"POWer:RF:GAIN:STATe?", 'ON')], ) as instr: instr.preamp = True assert instr.preamp is True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/anritsu/test_anritsuMS464xB.py0000644000175100001770000001252614623331163024533 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.anritsu import AnritsuMS464xB, AnritsuMS4642B, AnritsuMS4644B,\ AnritsuMS4645B, AnritsuMS4647B def test_init(): with expected_protocol( AnritsuMS464xB, [], ) as instr: assert len(instr.channels) == 16 assert len(instr.ch_1.ports) == 4 assert len(instr.ch_1.traces) == 16 def test_init_with_different_channel_numbers(): # Test init with different number of active channels and installed ports with expected_protocol( AnritsuMS464xB, [], active_channels=12, installed_ports=2, traces_per_channel=10 ) as instr: assert len(instr.channels) == 12 assert len(instr.ch_1.ports) == 2 assert len(instr.ch_1.traces) == 10 def test_init_unknown_active_channels(): # Test init with unknown active channels and traces with expected_protocol( AnritsuMS464xB, [(":SYST:PORT:COUN?", "3"), (":DISP:COUN?", "2"), (":CALC1:PAR:COUN?", "8"), (":CALC2:PAR:COUN?", "12")], active_channels="auto", installed_ports="auto", traces_per_channel="auto", ) as instr: assert len(instr.channels) == 2 assert len(instr.ch_1.ports) == 3 assert len(instr.ch_1.traces) == 8 assert len(instr.ch_2.traces) == 12 def test_update_channels(): with expected_protocol( AnritsuMS464xB, [(":DISP:COUN?", "8"), (":DISP:COUN?", "12")], ) as instr: assert len(instr.channels) == 16 instr.update_channels() assert len(instr.channels) == 8 instr.update_channels() assert len(instr.channels) == 12 def test_channel_number_of_traces(): # Test first level channel with expected_protocol( AnritsuMS464xB, [(":CALC6:PAR:COUN 16", None), (":CALC2:PAR:COUN?", "4")], ) as instr: instr.ch_6.number_of_traces = 16 assert instr.ch_2.number_of_traces == 4 def test_channel_update_traces(): # Test first level channel with expected_protocol( AnritsuMS464xB, [(":CALC1:PAR:COUN?", "4"), (":CALC1:PAR:COUN?", "12")], ) as instr: assert len(instr.ch_1.traces) == 16 instr.ch_1.update_traces() assert len(instr.ch_1.traces) == 4 instr.ch_1.update_traces() assert len(instr.ch_1.traces) == 12 def test_channel_port_power_level(): # Test second level channel (port in channel) with expected_protocol( AnritsuMS464xB, [(":SOUR6:POW:PORT1 12", None), (":SOUR2:POW:PORT4?", "-1.5E1")], ) as instr: instr.ch_6.pt_1.power_level = 12. assert instr.ch_2.pt_4.power_level == -15. def test_channel_trace_measurement_parameter(): # Test second level channel (trace in channel) with expected_protocol( AnritsuMS464xB, [(":CALC6:PAR1:DEF S11", None), (":CALC2:PAR6:DEF?", "S21")], ) as instr: instr.ch_6.tr_1.measurement_parameter = "S11" assert instr.ch_2.tr_6.measurement_parameter == "S21" def test_subclass_MS4642B_frequency_range(): with expected_protocol( AnritsuMS4642B, [(":SENS1:FREQ:STOP 2e+10", None)], ) as instr: instr.ch_1.frequency_stop = 2e10 with pytest.raises(ValueError): instr.ch_1.frequency_stop = 3e10 def test_subclass_MS4644B_frequency_range(): with expected_protocol( AnritsuMS4644B, [(":SENS1:FREQ:STOP 4e+10", None)], ) as instr: instr.ch_1.frequency_stop = 4e10 with pytest.raises(ValueError): instr.ch_1.frequency_stop = 5e10 def test_subclass_MS4645B_frequency_range(): with expected_protocol( AnritsuMS4645B, [(":SENS1:FREQ:STOP 5e+10", None)], ) as instr: instr.ch_1.frequency_stop = 5e10 with pytest.raises(ValueError): instr.ch_1.frequency_stop = 6e10 def test_subclass_MS4647B_frequency_range(): with expected_protocol( AnritsuMS4647B, [(":SENS1:FREQ:STOP 7e+10", None)], ) as instr: instr.ch_1.frequency_stop = 7e10 with pytest.raises(ValueError): instr.ch_1.frequency_stop = 8e10 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4336061 pymeasure-0.14.0/tests/instruments/attocube/0000755000175100001770000000000014623331176020564 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/attocube/test_anc300.py0000644000175100001770000001060214623331163023154 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.attocube import ANC300Controller # Note: This communication does not contain the first several device # responses, as they are ignored due to `adapter.flush_read_buffer()`. passwd = "passwd" init_comm = [ (passwd, "*" * len(passwd)), (None, "Authorization success"), ("echo off", "> echo off"), (None, "OK"), ] def test_stepu(): """Test a setting.""" with expected_protocol( ANC300Controller, init_comm + [("setm 1 stp", "OK"), ("stepu 1 15", "OK"), ], axisnames=["a", "b", "c"], passwd=passwd, ) as instr: instr.a.mode = "stp" with pytest.warns(FutureWarning): instr.a.stepu = 15 def test_continuous_move(): """Test a continuous move setting.""" with expected_protocol( ANC300Controller, init_comm + [("setm 3 stp", "OK"), ("stepd 3 c", "OK"), ], axisnames=["a", "b", "c"], passwd=passwd, ) as instr: instr.c.mode = "stp" instr.c.move_raw(float('-inf')) def test_capacity(): """Test a float measurement.""" with expected_protocol( ANC300Controller, init_comm + [("getc 1", "capacitance = 998.901733 nF"), (None, "OK")], axisnames=["a", "b", "c"], passwd=passwd, ) as instr: assert instr.a.capacity == 998.901733 def test_frequency(): """Test an integer measurement.""" with expected_protocol( ANC300Controller, # the \n in the following is indeed included in the return msg! init_comm + [("getf 1", "frequency = 111 Hz\n"), (None, "OK")], axisnames=["a", "b", "c"], passwd=passwd, ) as instr: assert instr.a.frequency == 111 def test_measure_capacity(): """Test triggering a capacity measurement.""" with expected_protocol( ANC300Controller, init_comm + [ ("setm 1 cap", "OK"), ("capw 1", "capacitance = 0.000000 nF"), (None, "OK"), ("getc 1", "capacitance = 1020.173401 nF"), (None, "OK"), ], axisnames=["a", "b", "c"], passwd=passwd, ) as instr: assert instr.a.measure_capacity() == 1020.173401 def test_move_raw(): """Test a raw movement.""" with expected_protocol( ANC300Controller, init_comm + [ ("stepd 2 18", "OK"), ], axisnames=["a", "b", "c"], passwd=passwd, ) as instr: instr.b.move_raw(-18) def test_move(): """Test a movement.""" with expected_protocol( ANC300Controller, init_comm + [ ("setm 3 stp", "OK"), ("stepd 3 20", "OK"), ("getf 3", "frequency = 111 Hz\n"), (None, "OK"), ("stepw 3", "OK"), ], axisnames=["a", "b", "c"], passwd=passwd, ) as instr: instr.c.move(-20, gnd=False) def test_ground_all(): """Test grounding of all axis""" with expected_protocol( ANC300Controller, init_comm + [ ("setm 1 gnd", "OK"), ("setm 2 gnd", "OK"), ("setm 3 gnd", "OK"), ], axisnames=["a", "b", "c"], passwd=passwd, ) as instr: instr.ground_all() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4336061 pymeasure-0.14.0/tests/instruments/danfysik/0000755000175100001770000000000014623331176020566 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/danfysik/test_danfysik8500.py0000644000175100001770000000037414623331163024324 0ustar00runnerdockerfrom pymeasure.test import expected_protocol from pymeasure.instruments.danfysik import Danfysik8500 def test_init(): with expected_protocol( Danfysik8500, [(b"ERRT", None), (b"UNLOCK", None)] ): pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4336061 pymeasure-0.14.0/tests/instruments/eurotest/0000755000175100001770000000000014623331176020630 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/eurotest/test_eurotestHPP120256.py0000644000175100001770000000535114623331163025143 0ustar00runnerdocker# Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.eurotest.eurotestHPP120256 import EurotestHPP120256 def test_voltage_setpoint(): """Verify the communication of the voltage setter/getter.""" with expected_protocol( EurotestHPP120256, [("U,1.200kV", None), ("STATUS,U", "U, RANGE=3.000kV, VALUE=2.458kV")], ) as inst: inst.voltage_setpoint = 1.200 assert inst.voltage_setpoint == 2.458 def test_current_limit(): """Verify the communication of the current setter/getter.""" with expected_protocol( EurotestHPP120256, [("I,1.200mA", None), ("STATUS,I", "I, RANGE=5000mA, VALUE=1739mA")], ) as inst: inst.current_limit = 1.200 assert inst.current_limit == 1739.0 def test_voltage_ramp(): """Verify the communication of the ramp setter/getter.""" with expected_protocol( EurotestHPP120256, [("RAMP,3000V/s", None), ("STATUS,RAMP", "RAMP, RANGE=3000V/s, VALUE=1000V/s")], ) as inst: inst.voltage_ramp = 3000 assert inst.voltage_ramp == 1000.0 def test_voltage(): """Verify the communication of the measure_voltage getter.""" with expected_protocol( EurotestHPP120256, [("STATUS,MU", "UM, RANGE=3000V, VALUE=2.458kV")], ) as inst: assert inst.voltage == 2.458 def test_current(): """Verify the communication of the measure_current getter.""" with expected_protocol( EurotestHPP120256, [("STATUS,MI", "IM, RANGE=5000mA, VALUE=1739mA")], ) as inst: assert inst.current == 1739.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4336061 pymeasure-0.14.0/tests/instruments/fluke/0000755000175100001770000000000014623331176020064 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/fluke/test_fluke7341.py0000644000175100001770000000463214623331163023123 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.fluke import Fluke7341 def test_setpoint_getter(): with expected_protocol(Fluke7341, [("s", "set: 150.00 C")], ) as inst: assert inst.set_point == 150 def test_setpoint_setter(): with expected_protocol(Fluke7341, [("s=150", None)], ) as inst: inst.set_point = 150 def test_temperature_getter(): with expected_protocol(Fluke7341, [("t", "t: 55.69 C")], ) as inst: assert inst.temperature == 55.69 def test_unit_getter(): with expected_protocol(Fluke7341, [("u", "u: C")], ) as inst: assert inst.unit == "C" @pytest.mark.parametrize("unit", ("c", "f")) def test_unit_setter(unit): with expected_protocol(Fluke7341, [(f"u={unit}", None)], ) as inst: inst.unit = unit def test_version_getter(): with expected_protocol(Fluke7341, [("*ver", "ver.7341,1.00")], ) as inst: assert inst.id == "Fluke,7341,NA,1.00" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4336061 pymeasure-0.14.0/tests/instruments/fwbell/0000755000175100001770000000000014623331176020231 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/fwbell/test_fwbell5080.py0000644000175100001770000000331114623331163023424 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.fwbell import FWBell5080 def test_init(): with expected_protocol( FWBell5080, [], ): pass # Verify the expected communication. def test_units(): with expected_protocol( FWBell5080, [(b":UNIT:FLUX:DC:GAUSS", None)], ) as instr: instr.units = 'gauss' def test_field(): with expected_protocol( FWBell5080, [(b":MEASure:FLUX?", b"+123.45T")], ) as instr: assert instr.field == 123.45 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4336061 pymeasure-0.14.0/tests/instruments/hcp/0000755000175100001770000000000014623331176017530 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hcp/test_tc038.py0000644000175100001770000000726714623331163022012 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.hcp import TC038 def test_setpoint(): with expected_protocol( TC038, [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03"), (b"\x0201010WRDD0120,01\x03", b"\x020101OK00C8\x03")] ) as inst: assert inst.setpoint == 20 def test_setpoint_setter(): # Communication from manual. with expected_protocol( TC038, [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03"), (b"\x0201010WWRD0120,01,00C8\x03", b"\x020101OK\x03")] ) as inst: inst.setpoint = 20 def test_setpoint_error(): with expected_protocol( TC038, [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03"), (b"\x0201010WWRD0120,01,00C8\x03", b"\x020101ER0401\x03")] ) as inst: with pytest.raises(ConnectionError, match="Out of setpoint range"): inst.setpoint = 20 def test_temperature(): # Communication from manual. with expected_protocol( TC038, [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03"), (b"\x0201010WRDD0002,01\x03", b"\x020101OK00C8\x03")] ) as inst: assert 20 == inst.temperature def test_monitored(): # Communication from manual. with expected_protocol( TC038, [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03"), (b"\x0201010WRM\x03", b"\x020101OK00C8\x03")] ) as inst: assert 20 == inst.monitored_value def test_monitored_error(): with expected_protocol( TC038, [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03"), (b"\x0201010WRM\x03", b"\x020101ER0600\x03")] ) as inst: with pytest.raises(ConnectionError, match="monitor"): inst.monitored_value def test_set_monitored(): # Communication from manual. with expected_protocol( TC038, [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03")] ): pass # Instantiation calls set_monitored_quantity() def test_set_monitored_wrong_input(): with expected_protocol( TC038, [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03")] ) as inst: with pytest.raises(KeyError): inst.set_monitored_quantity("temper") def test_information(): # Communication from manual. with expected_protocol( TC038, [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03"), (b"\x0201010INF6\x03", b"\x020101OKUT150333 V01.R001111222233334444\x03")] ) as inst: value = inst.information assert value == "UT150333 V01.R001111222233334444" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hcp/test_tc038d.py0000644000175100001770000001177614623331163022156 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.hcp import TC038D # Testing the 'write multiple values' method of the device. def test_write_multiple_values(): # Communication from manual. with expected_protocol( TC038D, [(b"\x01\x10\x01\x0A\x00\x04\x08\x00\x00\x03\xE8\xFF\xFF\xFC\x18\x8D\xE9", b"\x01\x10\x01\x0A\x00\x04\xE0\x34")] ) as inst: inst.write("W,0x010A,1000,-1000") inst.read() def test_write_multiple_values_decimal_address(): # Communication from manual. with expected_protocol( TC038D, [(b"\x01\x10\x01\x0A\x00\x04\x08\x00\x00\x03\xE8\xFF\xFF\xFC\x18\x8D\xE9", b"\x01\x10\x01\x0A\x00\x04\xE0\x34")] ) as inst: inst.write("W,266,1000,-1000") inst.read() def test_write_values_CRC_error(): """Test whether an invalid response CRC code raises an Exception.""" with expected_protocol( TC038D, [(b"\x01\x10\x01\x06\x00\x02\x04\x00\x00\x01A\xbf\xb5", b"\x01\x10\x01\x06\x00\x02\x01\x02")], ) as inst: with pytest.raises(ConnectionError): inst.setpoint = 32.1 def test_write_multiple_handle_wrong_start_address(): """Test whether the error code (byte 2) of 2 raises the right error.""" with expected_protocol( TC038D, [(b"\x01\x10\x01\x06\x00\x02\x04\x00\x00\x01A\xbf\xb5", b"\x01\x90\x02\xcd\xc1")], ) as inst: with pytest.raises(ValueError, match="Wrong start address"): inst.setpoint = 32.1 # Test the 'read register' method of the device def test_read_CRC_error(): """Test whether an invalid response CRC code raises an Exception.""" with expected_protocol( TC038D, [(b"\x01\x03\x00\x00\x00\x02\xC4\x0B", b"\x01\x03\x04\x00\x00\x03\xE8\x01\x02")], ) as inst: with pytest.raises(ConnectionError): inst.temperature def test_read_address_error(): """Test whether the error code (byte 2) of 2 raises the right error.""" with expected_protocol( TC038D, [(b"\x01\x03\x00\x00\x00\x02\xC4\x0B", b"\x01\x83\x02\xc0\xf1")], ) as inst: with pytest.raises(ValueError, match="start address"): inst.temperature def test_read_elements_error(): """Test whether the error code (byte 2) of 3 raises the right error.""" with expected_protocol( TC038D, [(b"\x01\x03\x00\x00\x00\x02\xC4\x0B", b"\x01\x83\x03\x011")], ) as inst: with pytest.raises(ValueError, match="Variable data"): inst.temperature def test_read_any_error(): """Test whether any wrong message (byte 1 is not 3) raises an error.""" with expected_protocol( TC038D, [(b"\x01\x03\x00\x00\x00\x02\xC4\x0B", b"\x01\x43\x05\xd13")], ) as inst: with pytest.raises(ConnectionError): inst.temperature # Test properties def test_setpoint(): with expected_protocol( TC038D, [(b"\x01\x03\x01\x06\x00\x02\x25\xf6", b"\x01\x03\x04\x00\x00\x00\x99:Y")], ) as inst: assert inst.setpoint == 15.3 def test_setpoint_setter(): with expected_protocol( TC038D, [(b"\x01\x10\x01\x06\x00\x02\x04\x00\x00\x01A\xbf\xb5", b"\x01\x10\x01\x06\x00\x02\xa0\x35")], ) as inst: inst.setpoint = 32.1 def test_temperature(): # Communication from manual. # Tests readRegister as well. with expected_protocol( TC038D, [(b"\x01\x03\x00\x00\x00\x02\xC4\x0B", b"\x01\x03\x04\x00\x00\x03\xE8\xFA\x8D")], ) as inst: assert inst.temperature == 100 def test_ping(): # Communication from manual. with expected_protocol( TC038D, [(b"\x01\x08\x00\x00\x12\x34\xed\x7c", b"\x01\x08\x00\x00\x12\x34\xed\x7c")], ) as inst: inst.ping(4660) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.437606 pymeasure-0.14.0/tests/instruments/hp/0000755000175100001770000000000014623331176017365 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hp/test_hp11713a.py0000644000175100001770000000640614623331163022145 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.hp.hp11713a import HP11713A, Attenuator_110dB import pytest class TestHP11713A: @pytest.mark.parametrize("channel", list(range(0, 9))) def test_channels(self, channel): with expected_protocol( HP11713A, [(f"A{channel}", None), (f"B{channel}", None)], ) as instr: ch = getattr(instr, f"ch_{channel}") ch.enabled = True ch.enabled = False def test_attenuation_x(self): with expected_protocol( HP11713A, [("A1", None), ("B2", None), ("B3", None), ("B4", None), ("A1", None), ("A2", None), ("A3", None), ("A4", None)], ) as instr: instr.ATTENUATOR_X = Attenuator_110dB instr.attenuation_x(10) instr.attenuation_x(110) def test_attenuation_y(self): with expected_protocol( HP11713A, [("A5", None), ("B6", None), ("B7", None), ("B8", None), ("A5", None), ("A6", None), ("A7", None), ("A8", None)], ) as instr: instr.ATTENUATOR_Y = Attenuator_110dB instr.attenuation_y(10) instr.attenuation_y(110) def test_attenuation_x_rounding(self): with expected_protocol( HP11713A, [("A1", None), ("B2", None), ("B3", None), ("B4", None), ("A1", None), ("A2", None), ("A3", None), ("A4", None)], ) as instr: instr.ATTENUATOR_X = Attenuator_110dB instr.attenuation_x(12.5) instr.attenuation_x(109) def test_deactivate_all(self): with expected_protocol( HP11713A, [("B1234567890", None)], ) as instr: instr.deactivate_all() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hp/test_hp33120A.py0000644000175100001770000001212514623331163022074 0ustar00runnerdockerimport pytest from pymeasure.test import expected_protocol from pymeasure.instruments.hp import HP33120A def test_init(): with expected_protocol( HP33120A, [(b'SOUR:VOLT:UNIT VPP', None)], ): pass # Verify the expected communication. def test_amplitude_setter(): with expected_protocol( HP33120A, [(b'SOUR:VOLT:UNIT VPP', None), (b'SOUR:VOLT 4', None)], ) as inst: inst.amplitude = 4 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'SOUR:VOLT:UNIT VPP', None), (b'SOUR:VOLT?', b'+4.00000E+00')], 4.0), )) def test_amplitude_getter(comm_pairs, value): with expected_protocol( HP33120A, comm_pairs, ) as inst: assert inst.amplitude == value @pytest.mark.parametrize("comm_pairs, value", ( ([(b'SOUR:VOLT:UNIT VPP', None), (b'SOUR:VOLT:UNIT?', b'VPP')], 'Vpp'), )) def test_amplitude_units_getter(comm_pairs, value): with expected_protocol( HP33120A, comm_pairs, ) as inst: assert inst.amplitude_units == value def test_burst_count_setter(): with expected_protocol( HP33120A, [(b'SOUR:VOLT:UNIT VPP', None), (b'BM:NCYC 500', None)], ) as inst: inst.burst_count = 500 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'SOUR:VOLT:UNIT VPP', None), (b'BM:NCYC?', b'+1.00000E+00')], 1.0), ([(b'SOUR:VOLT:UNIT VPP', None), (b'BM:NCYC?', b'+5.00000E+02')], 500.0), )) def test_burst_count_getter(comm_pairs, value): with expected_protocol( HP33120A, comm_pairs, ) as inst: assert inst.burst_count == value def test_burst_enabled_setter(): with expected_protocol( HP33120A, [(b'SOUR:VOLT:UNIT VPP', None), (b'BM:STATE 1', None)], ) as inst: inst.burst_enabled = True @pytest.mark.parametrize("comm_pairs, value", ( ([(b'SOUR:VOLT:UNIT VPP', None), (b'BM:STATE?', b'1')], True), ([(b'SOUR:VOLT:UNIT VPP', None), (b'BM:STATE?', b'0')], False), )) def test_burst_enabled_getter(comm_pairs, value): with expected_protocol( HP33120A, comm_pairs, ) as inst: assert inst.burst_enabled == value def test_burst_phase_setter(): with expected_protocol( HP33120A, [(b'SOUR:VOLT:UNIT VPP', None), (b'BM:PHAS 20', None)], ) as inst: inst.burst_phase = 20 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'SOUR:VOLT:UNIT VPP', None), (b'BM:PHAS?', b'+0.00000E+00')], 0.0), ([(b'SOUR:VOLT:UNIT VPP', None), (b'BM:PHAS?', b'+2.00000E+01')], 20.0), )) def test_burst_phase_getter(comm_pairs, value): with expected_protocol( HP33120A, comm_pairs, ) as inst: assert inst.burst_phase == value def test_burst_rate_setter(): with expected_protocol( HP33120A, [(b'SOUR:VOLT:UNIT VPP', None), (b'BM:INT:RATE 250', None)], ) as inst: inst.burst_rate = 250 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'SOUR:VOLT:UNIT VPP', None), (b'BM:INT:RATE?', b'+1.00000E+02')], 100.0), ([(b'SOUR:VOLT:UNIT VPP', None), (b'BM:INT:RATE?', b'+2.50000E+02')], 250.0), )) def test_burst_rate_getter(comm_pairs, value): with expected_protocol( HP33120A, comm_pairs, ) as inst: assert inst.burst_rate == value def test_burst_source_setter(): with expected_protocol( HP33120A, [(b'SOUR:VOLT:UNIT VPP', None), (b'BM:SOURCE INT', None)], ) as inst: inst.burst_source = 'INT' @pytest.mark.parametrize("comm_pairs, value", ( ([(b'SOUR:VOLT:UNIT VPP', None), (b'BM:SOURCE?', b'INT')], 'INT'), )) def test_burst_source_getter(comm_pairs, value): with expected_protocol( HP33120A, comm_pairs, ) as inst: assert inst.burst_source == value def test_frequency_setter(): with expected_protocol( HP33120A, [(b'SOUR:VOLT:UNIT VPP', None), (b'SOUR:FREQ 2000', None)], ) as inst: inst.frequency = 2000.0 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'SOUR:VOLT:UNIT VPP', None), (b'SOUR:FREQ?', b'+2.00000000000E+03')], 2000.0), ([(b'SOUR:VOLT:UNIT VPP', None), (b'SOUR:FREQ?', b'+2.00000000000E+03')], 2000.0), )) def test_frequency_getter(comm_pairs, value): with expected_protocol( HP33120A, comm_pairs, ) as inst: assert inst.frequency == value def test_offset_getter(): with expected_protocol( HP33120A, [(b'SOUR:VOLT:UNIT VPP', None), (b'SOUR:VOLT:OFFS?', b'+0.00000E+00')], ) as inst: assert inst.offset == 0.0 def test_shape_getter(): with expected_protocol( HP33120A, [(b'SOUR:VOLT:UNIT VPP', None), (b'SOUR:FUNC:SHAP?', b'SIN')], ) as inst: assert inst.shape == 'sinusoid' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hp/test_hp34401a.py0000644000175100001770000002513314623331163022142 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.hp.hp34401A import HP34401A @pytest.mark.parametrize("function_", HP34401A.FUNCTIONS.keys()) def test_function(function_): with expected_protocol( HP34401A, [ ("FUNC?", HP34401A.FUNCTIONS[function_]), (f"FUNC \"{HP34401A.FUNCTIONS[function_]}\"", None), ], ) as inst: assert function_ == inst.function_ inst.function_ = function_ def test_range_dcv(): with expected_protocol( HP34401A, [ ("FUNC \"VOLT\"", None), ("FUNC?", "VOLT"), ("VOLT:RANG?", "1"), ("FUNC?", "VOLT"), ("VOLT:RANG 1", None), ], ) as inst: inst.function_ = "DCV" assert 1 == inst.range_ inst.range_ = 1 def test_range_freq(): with expected_protocol( HP34401A, [ ("FUNC \"FREQ\"", None), ("FUNC?", "FREQ"), ("FREQ:VOLT:RANG?", "1"), ("FUNC?", "FREQ"), ("FREQ:VOLT:RANG 1", None), ], ) as inst: inst.function_ = "FREQ" assert 1 == inst.range_ inst.range_ = 1 @pytest.mark.parametrize("enabled", [True, False]) def test_autorange_dcv(enabled): with expected_protocol( HP34401A, [ ("FUNC \"VOLT\"", None), ("FUNC?", "VOLT"), ("VOLT:RANG:AUTO?", "1" if enabled else "0"), ("FUNC?", "VOLT"), (f"VOLT:RANG:AUTO {1 if enabled else 0}", None), ], ) as inst: inst.function_ = "DCV" assert enabled == inst.autorange inst.autorange = enabled @pytest.mark.parametrize("enabled", [True, False]) def test_autorange_freq(enabled): with expected_protocol( HP34401A, [ ("FUNC \"FREQ\"", None), ("FUNC?", "FREQ"), ("FREQ:VOLT:RANG:AUTO?", "1" if enabled else "0"), ("FUNC?", "FREQ"), (f"FREQ:VOLT:RANG:AUTO {1 if enabled else 0}", None), ], ) as inst: inst.function_ = "FREQ" assert enabled == inst.autorange inst.autorange = enabled @pytest.mark.parametrize("function_", HP34401A.FUNCTIONS.keys()) def test_resolution(function_): with expected_protocol( HP34401A, [ (f"FUNC \"{HP34401A.FUNCTIONS[function_]}\"", None), ("FUNC?", HP34401A.FUNCTIONS[function_]), (f"{HP34401A.FUNCTIONS[function_]}:RES?", "1"), ("FUNC?", HP34401A.FUNCTIONS[function_]), (f"{HP34401A.FUNCTIONS[function_]}:RES 1", None), ], ) as inst: inst.function_ = function_ assert 1 == inst.resolution inst.resolution = 1 @pytest.mark.parametrize("function_", HP34401A.FUNCTIONS.keys()) def test_nplc(function_): with expected_protocol( HP34401A, [ (f"FUNC \"{HP34401A.FUNCTIONS[function_]}\"", None), ("FUNC?", HP34401A.FUNCTIONS[function_]), (f"{HP34401A.FUNCTIONS[function_]}:NPLC?", "1"), ("FUNC?", HP34401A.FUNCTIONS[function_]), (f"{HP34401A.FUNCTIONS[function_]}:NPLC 1", None), ], ) as inst: inst.function_ = function_ assert 1 == inst.nplc inst.nplc = 1 @pytest.mark.parametrize("function_", ["FREQ", "PERIOD"]) def test_gate_time(function_): with expected_protocol( HP34401A, [ (f"FUNC \"{HP34401A.FUNCTIONS[function_]}\"", None), ("FUNC?", HP34401A.FUNCTIONS[function_]), (f"{HP34401A.FUNCTIONS[function_]}:APER?", "1"), ("FUNC?", HP34401A.FUNCTIONS[function_]), (f"{HP34401A.FUNCTIONS[function_]}:APER 1", None), ], ) as inst: inst.function_ = function_ assert 1 == inst.gate_time inst.gate_time = 1 def test_detector_bandwidth(): with expected_protocol( HP34401A, [ ("DET:BAND?", "3"), ("DET:BAND 3", None), ], ) as inst: assert 3 == inst.detector_bandwidth inst.detector_bandwidth = 3 @pytest.mark.parametrize("enabled", [True, False]) def test_autozero_enabled(enabled): with expected_protocol( HP34401A, [ ("ZERO:AUTO?", "1" if enabled else "0"), (f"ZERO:AUTO {1 if enabled else 0}", None), ], ) as inst: assert enabled == inst.autozero_enabled inst.autozero_enabled = enabled def test_trigger_single_autozero(): with expected_protocol( HP34401A, [ ("ZERO:AUTO ONCE", None), ], ) as inst: inst.trigger_single_autozero() @pytest.mark.parametrize("enabled", [True, False]) def test_auto_input_impedance_enabled(enabled): with expected_protocol( HP34401A, [ ("INP:IMP:AUTO?", "1" if enabled else "0"), (f"INP:IMP:AUTO {1 if enabled else 0}", None), ], ) as inst: assert enabled == inst.auto_input_impedance_enabled inst.auto_input_impedance_enabled = enabled def test_terminals_used(): with expected_protocol( HP34401A, [ ("ROUT:TERM?", "FRON"), ], ) as inst: assert "FRONT" == inst.terminals_used def test_init_trigger(): with expected_protocol( HP34401A, [ ("INIT", None) ], ) as inst: inst.init_trigger() def test_reading(): with expected_protocol( HP34401A, [ ("READ?", "1") ], ) as inst: assert 1 == inst.reading @pytest.mark.parametrize("trigger_source", ["BUS", "EXT", "IMM"]) def test_trigger_source(trigger_source): with expected_protocol( HP34401A, [ ("TRIG:SOUR?", trigger_source), (f"TRIG:SOUR {trigger_source}", None), ], ) as inst: assert trigger_source == inst.trigger_source inst.trigger_source = trigger_source def test_trigger_delay(): with expected_protocol( HP34401A, [ ("TRIG:DEL?", "1"), ("TRIG:DEL 1", None), ], ) as inst: assert 1 == inst.trigger_delay inst.trigger_delay = 1 @pytest.mark.parametrize("enabled", [True, False]) def test_trigger_auto_delay_enabled(enabled): with expected_protocol( HP34401A, [ ("TRIG:DEL:AUTO?", "1" if enabled else "0"), (f"TRIG:DEL:AUTO {1 if enabled else 0}", None), ], ) as inst: assert enabled == inst.trigger_auto_delay_enabled inst.trigger_auto_delay_enabled = enabled def test_sample_count(): with expected_protocol( HP34401A, [ ("SAMP:COUN?", "1"), ("SAMP:COUN 1", None), ], ) as inst: assert 1 == inst.sample_count inst.sample_count = 1 def test_trigger_count(): with expected_protocol( HP34401A, [ ("TRIG:COUN?", "1"), ("TRIG:COUN 1", None), ], ) as inst: assert 1 == inst.trigger_count inst.trigger_count = 1 def test_stored_reading(): with expected_protocol( HP34401A, [ ("FETC?", "1"), ], ) as inst: assert 1 == inst.stored_reading @pytest.mark.parametrize("enabled", [True, False]) def test_display_enabled(enabled): with expected_protocol( HP34401A, [ ("DISP?", "1" if enabled else "0"), (f"DISP {1 if enabled else 0}", None), ], ) as inst: assert enabled == inst.display_enabled inst.display_enabled = enabled def test_displayed_text(): with expected_protocol( HP34401A, [ ("DISP:TEXT?", "HELLO"), ("DISP:TEXT \"HELLO\"", None), ], ) as inst: assert "HELLO" == inst.displayed_text inst.displayed_text = "HELLO" def test_beep(): with expected_protocol( HP34401A, [ ("SYST:BEEP", None), ], ) as inst: inst.beep() @pytest.mark.parametrize("enabled", [True, False]) def test_beeper_enabled(enabled): with expected_protocol( HP34401A, [ ("SYST:BEEP:STAT?", "1" if enabled else "0"), (f"SYST:BEEP:STAT {1 if enabled else 0}", None), ], ) as inst: assert enabled == inst.beeper_enabled inst.beeper_enabled = enabled def test_scpi_version(): with expected_protocol( HP34401A, [ ("SYST:VERS?", "1-2-3"), ], ) as inst: assert "1-2-3" == inst.scpi_version def test_stored_readings_count(): with expected_protocol( HP34401A, [ ("DATA:POIN?", "1"), ], ) as inst: assert 1 == inst.stored_readings_count def test_self_test_result(): with expected_protocol( HP34401A, [ ("*TST?", "0"), ], ) as inst: assert 0 == inst.self_test_result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hp/test_hp34401a_with_device.py0000644000175100001770000002447314623331163024522 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 time import logging from pymeasure.instruments.hp.hp34401A import HP34401A log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) FUNCTIONS = ["DCV", "DCV_RATIO", "ACV", "DCI", "ACI", "R2W", "R4W", "FREQ", "PERIOD", "CONTINUITY", "DIODE"] FUNCTIONS_WITH_RANGE = ["DCV", "ACV", "DCI", "ACI", "R2W", "R4W", "FREQ", "PERIOD"] TEST_RANGES = { "DCV": [0.1, 1, 10], "ACV": [0.1, 1, 10], "DCI": [0.1, 1, 3], "ACI": [1, 3], "R2W": [100, 1e3, 10e3], "R4W": [100, 1e3, 10e3], "FREQ": [0.1, 1, 10], "PERIOD": [0.1, 1, 10]} @pytest.fixture(scope="module") def hp34401a(connected_device_address): instr = HP34401A(connected_device_address) return instr @pytest.fixture def resetted_hp34401a(hp34401a): hp34401a.clear() hp34401a.write("*RST") return hp34401a def test_correct_model_by_idn(resetted_hp34401a): assert "34401a" in resetted_hp34401a.id.lower() @pytest.mark.parametrize("function_", FUNCTIONS) def test_given_function_when_set_then_function_is_set(resetted_hp34401a, function_): resetted_hp34401a.function_ = function_ assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.function_ == function_ @pytest.mark.parametrize("function_", FUNCTIONS) def test_given_function_is_set_then_reading_avaliable(resetted_hp34401a, function_): resetted_hp34401a.function_ = function_ assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.reading is not None @pytest.mark.parametrize("function_", FUNCTIONS_WITH_RANGE) def test_given_function_is_set_then_range_avaliable(resetted_hp34401a, function_): resetted_hp34401a.function_ = function_ assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.range_ is not None @pytest.mark.parametrize("function_", FUNCTIONS_WITH_RANGE) def test_given_function_set_then_autorange_enabled(resetted_hp34401a, function_): resetted_hp34401a.function_ = function_ assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.autorange is True @pytest.mark.parametrize("function_", FUNCTIONS_WITH_RANGE) def test_given_range_set_then_range_correct(resetted_hp34401a, function_): for range in TEST_RANGES[function_]: resetted_hp34401a.function_ = function_ resetted_hp34401a.range_ = range assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.range_ == range @pytest.mark.parametrize("function_", FUNCTIONS_WITH_RANGE) def test_autorange_enable(resetted_hp34401a, function_): resetted_hp34401a.function_ = function_ resetted_hp34401a.autorange = True assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.autorange is True @pytest.mark.parametrize("function_", FUNCTIONS_WITH_RANGE) def test_autorange_disable(resetted_hp34401a, function_): resetted_hp34401a.function_ = function_ resetted_hp34401a.autorange = False assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.autorange is False def test_dcv_range_min_max(resetted_hp34401a): resetted_hp34401a.function_ = "DCV" resetted_hp34401a.range_ = "MIN" assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.range_ == 0.1 resetted_hp34401a.range_ = "MAX" assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.range_ == 1000 @pytest.mark.parametrize("enabled", [True, False]) def test_display_enabled(resetted_hp34401a, enabled): resetted_hp34401a.display_enabled = enabled assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.display_enabled == enabled @pytest.mark.parametrize("function_", ["DCV", "ACV", "DCI", "R2W"]) def test_resolution(resetted_hp34401a, function_): resetted_hp34401a.function_ = function_ resetted_hp34401a.range_ = 1 resetted_hp34401a.resolution = 0.0001 assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.resolution == 0.0001 @pytest.mark.parametrize("function_", ["DCV", "DCI", "R2W"]) @pytest.mark.parametrize("nplc", [0.02, 1, 100]) def test_nplc(resetted_hp34401a, function_, nplc): resetted_hp34401a.function_ = function_ resetted_hp34401a.nplc = nplc assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.nplc == nplc @pytest.mark.parametrize("function_", ["FREQ", "PERIOD"]) @pytest.mark.parametrize("gate_time", [0.01, 0.01, 1]) def test_gate_time(resetted_hp34401a, function_, gate_time): resetted_hp34401a.function_ = function_ resetted_hp34401a.gate_time = gate_time assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.gate_time == gate_time @pytest.mark.parametrize("detector_bandwidth", [3, 20, 200]) def test_detector_bandwidth(resetted_hp34401a, detector_bandwidth): resetted_hp34401a.function_ = "FREQ" resetted_hp34401a.detector_bandwidth = detector_bandwidth assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.detector_bandwidth == detector_bandwidth @pytest.mark.parametrize("enable", [True, False]) def test_autozero(resetted_hp34401a, enable): resetted_hp34401a.autozero_enabled = enable assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.autozero_enabled == enable def test_single_autozero(resetted_hp34401a): resetted_hp34401a.autozero_enabled = True resetted_hp34401a.trigger_single_autozero() assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.autozero_enabled is False def test_auto_input_impedance(resetted_hp34401a): resetted_hp34401a.auto_input_impedance_enabled = True assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.auto_input_impedance_enabled is True resetted_hp34401a.auto_input_impedance_enabled = False assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.auto_input_impedance_enabled is False def test_terminals_used(resetted_hp34401a): assert resetted_hp34401a.terminals_used in ["FRONT", "REAR"] assert len(resetted_hp34401a.check_errors()) == 0 def test_when_init_trigger_called_then_no_error(resetted_hp34401a): resetted_hp34401a.init_trigger() assert len(resetted_hp34401a.check_errors()) == 0 @pytest.mark.parametrize("trigger_source", ["IMM", "BUS", "EXT"]) def test_trigger_source(resetted_hp34401a, trigger_source): resetted_hp34401a.trigger_source = trigger_source assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.trigger_source == trigger_source def test_trigger_delay(resetted_hp34401a): resetted_hp34401a.trigger_delay = 10.5 trigger_delay = resetted_hp34401a.trigger_delay assert len(resetted_hp34401a.check_errors()) == 0 assert trigger_delay == 10.5 @pytest.mark.parametrize("enabled", [True, False]) def test_trigger_auto_delay_enabled(resetted_hp34401a, enabled): resetted_hp34401a.trigger_auto_delay_enabled = enabled trigger_auto_delay_enabled = resetted_hp34401a.trigger_auto_delay_enabled assert len(resetted_hp34401a.check_errors()) == 0 assert trigger_auto_delay_enabled == enabled def test_sample_count(resetted_hp34401a): resetted_hp34401a.sample_count = 10 assert resetted_hp34401a.sample_count == 10 assert len(resetted_hp34401a.check_errors()) == 0 def test_trigger_count(resetted_hp34401a): resetted_hp34401a.trigger_count = 10 assert len(resetted_hp34401a.check_errors()) == 0 assert resetted_hp34401a.trigger_count == 10 def test_read_stored_reading_multiple_readings(resetted_hp34401a): resetted_hp34401a: HP34401A = resetted_hp34401a resetted_hp34401a.sample_count = 10 resetted_hp34401a.trigger_source = "IMM" resetted_hp34401a.nplc = 0.02 resetted_hp34401a.init_trigger() resetted_hp34401a.complete readings = resetted_hp34401a.stored_reading print(readings) assert len(resetted_hp34401a.check_errors()) == 0 assert len(readings) == 10 def test_displayed_text(resetted_hp34401a): resetted_hp34401a.displayed_text = "Hello World" assert resetted_hp34401a.displayed_text == "Hello World" assert len(resetted_hp34401a.check_errors()) == 0 def test_beep(resetted_hp34401a): resetted_hp34401a.beep() assert len(resetted_hp34401a.check_errors()) == 0 @pytest.mark.parametrize("enabled", [True, False]) def test_beeper_enabled(resetted_hp34401a, enabled): resetted_hp34401a.beeper_enabled = enabled assert resetted_hp34401a.beeper_enabled == enabled assert len(resetted_hp34401a.check_errors()) == 0 def test_scpi_version(resetted_hp34401a): assert len(str(resetted_hp34401a.scpi_version)) > 0 assert len(resetted_hp34401a.check_errors()) == 0 def test_stored_readings_count(resetted_hp34401a): resetted_hp34401a.nplc = 0.02 resetted_hp34401a.sample_count = 10 resetted_hp34401a.trigger_source = "IMM" resetted_hp34401a.init_trigger() time.sleep(1) assert resetted_hp34401a.stored_readings_count == 10 assert len(resetted_hp34401a.check_errors()) == 0 def test_self_test_result(resetted_hp34401a): resetted_hp34401a.adapter.connection.timeout = 60000 assert resetted_hp34401a.self_test_result in [True, False] assert len(resetted_hp34401a.check_errors()) == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hp/test_hp3478a.py0000644000175100001770000001253314623331163022074 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.hp import HP3478A VALID_CAL_DATA = [ 0, 0, 0, 0, 3, 0, 8, 2, 15, 4, 4, 0, 13, 11, 0, 0, 0, 0, 3, 3, 2, 15, 5, 3, 0, 14, 0, 0, 0, 0, 0, 0, 3, 2, 15, 4, 0, 0, 14, 7, 9, 9, 9, 9, 9, 7, 2, 0, 15, 3, 12, 10, 11, 9, 9, 9, 9, 9, 9, 2, 0, 14, 15, 14, 9, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 9, 9, 8, 6, 0, 9, 2, 14, 14, 0, 12, 10, 12, 9, 9, 9, 9, 9, 5, 1, 12, 0, 5, 14, 10, 13, 9, 9, 9, 9, 9, 8, 1, 12, 1, 15, 1, 10, 12, 0, 0, 0, 0, 0, 0, 1, 12, 15, 3, 0, 14, 0, 9, 9, 9, 9, 9, 9, 1, 12, 13, 2, 14, 9, 15, 9, 9, 9, 9, 9, 9, 1, 12, 13, 4, 2, 10, 9, 9, 9, 9, 9, 9, 9, 1, 12, 14, 12, 13, 9, 5, 9, 9, 9, 9, 9, 9, 1, 12, 2, 1, 15, 10, 10, 0, 0, 0, 0, 4, 2, 3, 0, 0, 3, 12, 14, 7, 0, 0, 0, 0, 0, 4, 3, 0, 1, 12, 3, 14, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 9, 9, 8, 6, 0, 9, 3, 14, 3, 1, 1, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 0, 0, 0, 0, 0, 0, 0, 0 ] def convert_cal_data_to_cal_read_xfers(cal_data): # Convert cal_data into 256 single byte 'W' read operations. cal_read_xfers = [] for addr, value in enumerate(cal_data): cal_read_xfers.append([bytes([ord('W'), addr]), bytes([value+64])]) return cal_read_xfers def convert_cal_data_to_cal_write_xfers(cal_data): # Convert cal_data into 256 single byte 'X' write operations. cal_write_xfers = [] for addr, value in enumerate(cal_data): cal_write_xfers.append([bytes([ord('X'), addr, value]), None]) return cal_write_xfers # ============================================================ # TESTS # ============================================================ def test_calibration_enabled(): with expected_protocol( HP3478A, [ (b"B", b'\x00\x20\x00\x00\x00'), # cal_enable bit is set (b"B", b'\x00\x00\x00\x00\x00') ], ) as instr: assert instr.calibration_enabled assert not instr.calibration_enabled def test_calibration_data_getter(): cal_read_xfers = convert_cal_data_to_cal_read_xfers(VALID_CAL_DATA) with expected_protocol( HP3478A, cal_read_xfers ) as instr: cal_data = instr.calibration_data assert instr.verify_calibration_data(cal_data) def test_calibration_data_setter_cal_disabled(): with pytest.raises(Exception, match="CAL ENABLE switch not set to ON"): with expected_protocol( HP3478A, # cal_enable cleared. As a result there won't be calibration # write transactions. [ (b"B", b'\x00\x00\x00\x00\x00'), ] ) as instr: instr.calibration_data = VALID_CAL_DATA def test_calibration_data_setter_pass(): valid_cal_write_xfers = convert_cal_data_to_cal_write_xfers(VALID_CAL_DATA) with expected_protocol( HP3478A, # setter pass [ (b"B", b'\x00\x20\x00\x00\x00'), ] + valid_cal_write_xfers ) as instr: # Writing correct data instr.calibration_data = VALID_CAL_DATA def test_calibration_data_setter_invalid_data(): with pytest.raises(ValueError, match="cal_data verification fail"): with expected_protocol( HP3478A, # setter fail due to invalid data [ (b"B", b'\x00\x20\x00\x00\x00'), ] ) as instr: # Assigning invalid data results in an Exception without data writes valid_cal_data_corrupt = VALID_CAL_DATA.copy() valid_cal_data_corrupt[1] = 1 instr.calibration_data = valid_cal_data_corrupt def test_write_calibration_data(): invalid_cal_data = VALID_CAL_DATA.copy() invalid_cal_data[1] = 1 invalid_cal_write_xfers = convert_cal_data_to_cal_write_xfers(invalid_cal_data) with expected_protocol( HP3478A, invalid_cal_write_xfers ) as instr: # Writing invalid data with verification bypass instr.write_calibration_data(invalid_cal_data, verify_calibration_data=False) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hp/test_hp437b.py0000644000175100001770000000673414623331163022013 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2023 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.test import expected_protocol from pymeasure.instruments.hp import HP437B from pymeasure.instruments.hp.hp437b import SensorType def test_calibrate(): with expected_protocol( HP437B, [("CL99.9PCT", None)], ) as instr: instr.calibrate(99.9) def test_calibration_factor(): with expected_protocol( HP437B, [("KB99.9PCT", None), ("ERR?", "000")], ) as instr: instr.calibration_factor = 99.9 def test_display_user_message(): with expected_protocol( HP437B, [("DU TEST ", None)], ) as instr: instr.display_user_message = "TEST" def test_duty_cycle_enabled(): with expected_protocol( HP437B, [("DC1", None), ("ERR?", "000"), ("SM", "AAaaBBCCccDDddEFGHIJKLMN1P")], ) as instr: instr.duty_cycle_enabled = True assert instr.duty_cycle_enabled is True def test_duty_cycle(): with expected_protocol( HP437B, [("DY99.999PCT", None), ("ERR?", "000")], ) as instr: instr.duty_cycle = 0.99999 def test_frequency(): with expected_protocol( HP437B, [("FR099.9000GZ", None), ("ERR?", "000")], ) as instr: instr.frequency = 99.9e9 @pytest.mark.parametrize('resolution', [(1, 1), (0.1, 2), (0.01, 3)]) def test_resolution_linear(resolution): value, code = resolution with expected_protocol( HP437B, [("SM", "000000110017000A0002000000"), ("RE%dEN" % code, None), ("ERR?", "000")], ) as instr: instr.resolution = value @pytest.mark.parametrize('resolution', [(0.1, 1), (0.01, 2), (0.001, 3)]) def test_resolution_logarithmic(resolution): value, code = resolution with expected_protocol( HP437B, [("SM", "000000110017001A0002000001"), ("RE%dEN" % code, None), ("ERR?", "000")], ) as instr: instr.resolution = value @pytest.mark.parametrize('sensor_type', [e for e in SensorType]) def test_sensor_type(sensor_type): with expected_protocol( HP437B, [("SE%dEN" % int(sensor_type.value), None), ("ERR?", "000")], ) as instr: instr.sensor_type = sensor_type ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hp/test_hp437b_with_device.py0000644000175100001770000002016514623331163024357 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2023 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 time import sleep from pymeasure.instruments.hp import HP437B from pymeasure.instruments.hp.hp437b import MeasurementUnit, OperatingMode, TriggerMode, \ GroupTriggerMode import numpy as np class TestHP437B: """ Unit tests for HP437B class. This test suite, needs the following setup to work properly: - A HP437B device should be connected to a gpib capable adapter; - The device's address must be set in the RESOURCE constant. - Some power sensor connected to the power reference reading around 0 dBm (+/- 1 dBm) """ BOOLEANS = [True, False] FILTER_VALUES = reversed([1, 2, 4, 8, 16, 32, 64, 128, 256, 512]) GROUP_TRIGGER_MODES = [e for e in GroupTriggerMode] @pytest.fixture def makeHp437b(self, connected_device_address): return HP437B(connected_device_address) @pytest.fixture def make_resetted_instr(self, makeHp437b): makeHp437b.reset() makeHp437b.clear_status_registers() return makeHp437b def test_range(self, make_resetted_instr): instr = make_resetted_instr instr.range = 5 assert instr.range == 5 assert instr.automatic_range_enabled is False def test_operating_mode(self, make_resetted_instr): instr = make_resetted_instr assert instr.operating_mode == OperatingMode.NORMAL def test_trigger_mode(self, make_resetted_instr): instr = make_resetted_instr instr.trigger_mode = TriggerMode.HOLD assert instr.trigger_mode == TriggerMode.HOLD instr.trigger_mode = TriggerMode.FREE_RUNNING assert instr.trigger_mode == TriggerMode.FREE_RUNNING @pytest.mark.parametrize('boolean', BOOLEANS) def test_automatic_range_enabled(self, make_resetted_instr, boolean): instr = make_resetted_instr instr.automatic_range_enabled = boolean assert instr.automatic_range_enabled == boolean @pytest.mark.parametrize('gtmode', GROUP_TRIGGER_MODES) def test_group_trigger_mode(self, make_resetted_instr, gtmode): instr = make_resetted_instr instr.group_trigger_mode = gtmode assert instr.group_trigger_mode == gtmode @pytest.mark.parametrize('boolean', BOOLEANS) def test_duty_cycle_enabled(self, make_resetted_instr, boolean): instr = make_resetted_instr instr.duty_cycle_enabled = boolean assert instr.duty_cycle_enabled == boolean @pytest.mark.parametrize('boolean', BOOLEANS) def test_filter_automatic_enabled(self, make_resetted_instr, boolean): instr = make_resetted_instr instr.filter_automatic_enabled = boolean sleep(2) assert instr.filter_automatic_enabled == boolean @pytest.mark.parametrize('filter', FILTER_VALUES) def test_filter(self, make_resetted_instr, filter): instr = make_resetted_instr instr.filter = filter assert instr.filter == filter @pytest.mark.parametrize('boolean', BOOLEANS) def test_limits_enabled(self, make_resetted_instr, boolean): instr = make_resetted_instr instr.limits_enabled = boolean assert instr.limits_enabled == boolean def test_limit_high_hit(self, make_resetted_instr): instr = make_resetted_instr instr.power_reference_enabled = True sleep(2) assert (-1 <= instr.power <= 1) instr.limit_low = -10 instr.limit_high = -1 instr.limits_enabled = True sleep(1) assert instr.limit_high_hit is True instr.limits_enabled = False def test_limit_low_hit(self, make_resetted_instr): instr = make_resetted_instr instr.power_reference_enabled = True sleep(2) assert (-1 <= instr.power <= 1) instr.limit_high = 10 instr.limit_low = 1 instr.limits_enabled = True sleep(1) assert instr.limit_low_hit is True instr.limits_enabled = False def test_limit_high(self, make_resetted_instr): instr = make_resetted_instr instr.limit_high = 299.999 assert instr.limit_high == 299.999 @pytest.mark.parametrize('boolean', BOOLEANS) def test_offset_enabled(self, make_resetted_instr, boolean): instr = make_resetted_instr instr.offset_enabled = boolean assert instr.offset_enabled == boolean def test_offset(self, make_resetted_instr): instr = make_resetted_instr instr.offset_enabled = True instr.offset = 20.22 assert instr.offset == 20.22 @pytest.mark.parametrize('boolean', BOOLEANS) def test_relative_mode_enabled(self, make_resetted_instr, boolean): instr = make_resetted_instr instr.relative_mode_enabled = boolean assert instr.relative_mode_enabled == boolean def test_measurement_units_and_linear_display_enabled(self, make_resetted_instr): instr = make_resetted_instr instr.relative_mode_enabled = True instr.linear_display_enabled = True assert instr.measurement_unit == MeasurementUnit.PERCENT instr.linear_display_enabled = False assert instr.measurement_unit == MeasurementUnit.DB instr.relative_mode_enabled = False instr.linear_display_enabled = True assert instr.measurement_unit == MeasurementUnit.WATTS assert instr.linear_display_enabled is True instr.linear_display_enabled = False assert instr.measurement_unit == MeasurementUnit.DBM assert instr.linear_display_enabled is False @pytest.mark.parametrize('resolution', [1, 0.1, 0.01]) def test_resolution_linear(self, make_resetted_instr, resolution): instr = make_resetted_instr instr.linear_display_enabled = True instr.resolution = resolution assert instr.resolution == resolution @pytest.mark.parametrize('resolution', [0.1, 0.01, 0.001]) def test_resolution_logarithmic(self, make_resetted_instr, resolution): instr = make_resetted_instr instr.linear_display_enabled = False instr.resolution = resolution assert instr.resolution == resolution def test_frequency(self, make_resetted_instr): instr = make_resetted_instr instr.frequency = 50e6 assert instr.frequency == 50e6 def test_duty_cycle(self, make_resetted_instr): instr = make_resetted_instr instr.duty_cycle = 0.500 assert instr.duty_cycle == 0.500 def test_calibration_factor(self, make_resetted_instr): instr = make_resetted_instr instr.calibration_factor = 50.00 assert instr.calibration_factor == 50.00 def test_sensor_data_read_cal_factor_table(self, make_resetted_instr): instr = make_resetted_instr frequencies = np.arange(1e9, 81e9, 1e9) cal_factors = [100.0 for _ in range(0, 80)] instr.sensor_data_write_cal_factor_table(9, frequencies, cal_factors) freq_data, cal_fac_data = instr.sensor_data_read_cal_factor_table(9) assert freq_data.sort() == frequencies.sort() assert cal_fac_data.sort() == cal_fac_data.sort() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hp/test_hp8116a.py0000644000175100001770000000435714623331163022073 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.hp import HP8116A from pymeasure.instruments.hp.hp8116a import Status HP8116A.status = property(fget=lambda self: Status(5)) init_comm = [(b"CST", b"x" * 87 + b' ,\r\n')] # communication during init def test_init(): with expected_protocol( HP8116A, init_comm, ): pass # Verify the expected communication. def test_duty_cycle(): with expected_protocol( HP8116A, init_comm + [(b"IDTY", b"00000035")], ) as instr: assert instr.duty_cycle == 35 def test_duty_cycle_setter(): with expected_protocol( HP8116A, init_comm + [(b"DTY 34.5 %", None)], ) as instr: instr.duty_cycle = 34.5 def test_sweep_time(): with expected_protocol(HP8116A, init_comm + [("SWT 5 S", None)]) as inst: # This test tests also the generate_1_2_5_sequence method and truncation. inst.sweep_time = 3 def test_limit_enabled(): with expected_protocol(HP8116A, init_comm + [("L1", None)]) as inst: inst.limit_enabled = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hp/test_hp8116a_with_device.py0000644000175100001770000002543214623331163024442 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 math import time from pymeasure.instruments.hp import HP8116A, hp8116a pytest.skip('Only work with connected hardware', allow_module_level=True) class TestHP8116A: """ Unit tests for HP8116A class. This test suite, needs the following setup to work properly: - A HP8116A device should be connected to the computer; - The device's address must be set in the RESOURCE constant. - You must set HAS_OPTION_001 according to the device's sweep/burst capability. """ RESOURCE = 'GPIB0::12' HAS_OPTION_001 = True BOOLEANS = [False, True] OPERATING_MODES_WO_OPT001 = ['normal', 'triggered', 'gate', 'external_width'] OPERATING_MODES_OPT001 = ['internal_sweep', 'external_sweep', 'internal_burst', 'external_burst'] CONTROL_MODES = ['off', 'FM', 'AM', 'PWM', 'VCO'] TRIGGER_SLOPES = ['off', 'positive', 'negative'] SHAPES = ['dc', 'sine', 'triangle', 'square', 'pulse'] UNITS = { 'nano': 'NZ', 'micro': 'UZ', 'milli': 'MZ', 'no_prefix': 'HZ', 'kilo': 'KHZ', 'mega': 'MHZ' } VALUES_WITH_UNITS = [[1e-9, '1 NZ'], [1e-6, '1 UZ'], [1e-3, '1 MZ'], [1, '1 HZ'], [1e3, '1 KHZ'], [1e6, '1 MHZ'], [1.23456e-9, '1.23 NZ'], [1/3, '333 MZ'], [1.23456e-6, '1.23 UZ'], [1e-6, '1 UZ']] VALUES_WITH_UNITS_TO_PARSE = [['FRQ 2.34 MZ', 2.34e-3], ['FRQ 23.4 MZ', 23.4e-3], ['FRQ 234 MZ', 234e-3], ['FRQ 2.34 HZ', 2.34], ['FRQ 23.4 HZ', 23.4], ['FRQ 234 HZ', 234.0], ['FRQ 2.34KHZ', 2.34e3], ['FRQ 23.4KHZ', 23.4e3], ['FRQ 234 KHZ', 234e3], ['FRQ 2.34MHZ', 2.34e6], ['FRQ 23.4MHZ', 23.4e6]] FREQUENCIES = [[1, 1], [1.23, 1.23], [1e3, 1e3], [1.23e3, 1.23e3], [1e6, 1e6], [1.23e6, 1.23e6], [1.234, 1.23], [1.234e3, 1.23e3], [10.234e3, 10.2e3], [1.234e6, 1.23e6]] instr = HP8116A(RESOURCE) @pytest.fixture def make_resetted_instr(self): self.instr.reset() return self.instr def test_given_instrument_resetted_when_triggered_then_normal(self, make_resetted_instr): instr = make_resetted_instr instr.write('M2') # Triggered mode instr.reset() assert 'M1' in instr.ask('CST', 10) @pytest.mark.parametrize('case', OPERATING_MODES_WO_OPT001) def test_operating_modes_no_option001(self, make_resetted_instr, case): instr = make_resetted_instr if case == 'external_width': instr.write('W4') # External width mode only valid in pulse mode instr.operating_mode = case assert instr.operating_mode == case @pytest.mark.parametrize('case', OPERATING_MODES_OPT001) def test_operating_modes_option001(self, make_resetted_instr, case): instr = make_resetted_instr instr.operating_mode = case if self.HAS_OPTION_001: assert instr.operating_mode == case else: assert instr.operating_mode == 'normal' @pytest.mark.parametrize('case', CONTROL_MODES) def test_control_modes(self, make_resetted_instr, case): instr = make_resetted_instr if case == 'PWM': instr.write('W4') # PWM mode only valid in pulse mode instr.control_mode = case assert instr.control_mode == case @pytest.mark.parametrize('case', TRIGGER_SLOPES) def test_trigger_slopes(self, make_resetted_instr, case): instr = make_resetted_instr instr.trigger_slope = case assert instr.trigger_slope == case @pytest.mark.parametrize('case', SHAPES) def test_shapes(self, make_resetted_instr, case): instr = make_resetted_instr instr.shape = case assert instr.shape == case def test_given_long_operation_then_buffer_not_empty_bit_set(self, make_resetted_instr): instr = make_resetted_instr instr.adapter.write('W4') # Takes about 330 ms to process according to the service manual assert (instr.status & hp8116a.Status.buffer_not_empty) instr._wait_for_commands_processed() # Now wait so that the instrument doesn't lock up def test_write(self, make_resetted_instr): instr = make_resetted_instr instr.write('W4') assert not (instr.status & hp8116a.Status.buffer_not_empty) def test_given_read_called_then_exception(self, make_resetted_instr): instr = make_resetted_instr with pytest.raises(NotImplementedError): instr.read() def test_given_ask_called_then_return_value_valid(self, make_resetted_instr): instr = make_resetted_instr ret = instr.ask('IFRQ', 14) assert type(ret) is str assert '\n' not in ret assert '\r' not in ret assert 'FRQ' in ret @pytest.mark.parametrize('case, expected', VALUES_WITH_UNITS) def test_get_value_with_unit(self, case, expected): ret = HP8116A._get_value_with_unit(case, self.UNITS) assert ret == expected @pytest.mark.parametrize('case, expected', VALUES_WITH_UNITS_TO_PARSE) def test_parse_value_with_unit(self, case, expected): ret = HP8116A._parse_value_with_unit(case, self.UNITS) assert math.isclose(ret, expected) def test_generate_1_2_5_sequence(self): min = 200e-6 max = 1 expected = [200e-6, 500e-6, 1e-3, 2e-3, 5e-3, 10e-3, 20e-3, 50e-3, 100e-3, 200e-3, 500e-3, 1] ret = HP8116A._generate_1_2_5_sequence(min, max) assert ret == expected @pytest.mark.parametrize('case', BOOLEANS) def test_haversine_enabled(self, case, make_resetted_instr): instr = make_resetted_instr instr.haversine_enabled = case assert instr.haversine_enabled == case @pytest.mark.parametrize('case', BOOLEANS) def test_autovernier_enabled(self, case, make_resetted_instr): instr = make_resetted_instr instr.autovernier_enabled = case assert instr.autovernier_enabled == case @pytest.mark.parametrize('case', BOOLEANS) def test_limit_enabled(self, case, make_resetted_instr): instr = make_resetted_instr instr.limit_enabled = case assert instr.limit_enabled == case @pytest.mark.parametrize('case', BOOLEANS) def test_complement_enabled(self, case, make_resetted_instr): instr = make_resetted_instr instr.complement_enabled = case assert instr.complement_enabled == case @pytest.mark.parametrize('case', BOOLEANS) def test_output_enabled(self, case, make_resetted_instr): instr = make_resetted_instr instr.output_enabled = case assert instr.output_enabled == case @pytest.mark.parametrize('case, expected', FREQUENCIES) def test_frequency(self, case, expected, make_resetted_instr): instr = make_resetted_instr instr.frequency = case assert math.isclose(instr.frequency, expected) def test_given_invalid_frequency_when_set_frequency_then_(self, make_resetted_instr): instr = make_resetted_instr with pytest.raises(ValueError): instr.frequency = 1e9 # Sadly 1 GHz is too high :( def test_check_has_option_001(self, make_resetted_instr): instr = make_resetted_instr assert instr._check_has_option_001() == self.HAS_OPTION_001 def test_given_resetted_when_check_errors_then_no_error(self, make_resetted_instr): instr = make_resetted_instr errors = instr.check_errors() assert type(errors) is list assert len(errors) == 0 def test_given_invalid_duty_cycle_when_check_errors_then_error(self, make_resetted_instr): instr = make_resetted_instr instr.frequency = 50e6 instr.duty_cycle = 90 errors = instr.check_errors() assert len(errors) == 1 assert errors[0] == 'DUTY C. ERROR' def test_given_two_error_conditions_when_check_errors_then_two_errors(self, make_resetted_instr): instr = make_resetted_instr # Triggering duty cycle error since 90 % is too high at 50 MHz instr.frequency = 50e6 instr.duty_cycle = 90 # Triggering handling error since autovernier is not possible in triggered mode instr.operating_mode = 'triggered' instr.autovernier_enabled = True errors = instr.check_errors() assert len(errors) == 2 assert 'DUTY C. ERROR' in errors assert 'HANDLING ERROR' in errors def test_given_autovernier_disabled_when_start_autovernier_then_error(self, make_resetted_instr): instr = make_resetted_instr with pytest.raises(RuntimeError): instr.start_autovernier(HP8116A.frequency, HP8116A.Digit.LEAST_SIGNIFICANT, HP8116A.Direction.UP) def test_given_invalid_control_when_start_autovernier_then_error(self, make_resetted_instr): instr = make_resetted_instr instr.autovernier_enabled = True with pytest.raises(ValueError): instr.start_autovernier(HP8116A.sweep_start, HP8116A.Digit.LEAST_SIGNIFICANT, HP8116A.Direction.UP) def test_autovernier(self, make_resetted_instr): instr = make_resetted_instr instr.autovernier_enabled = True instr.start_autovernier(HP8116A.amplitude, HP8116A.Digit.SECOND_SIGNIFICANT, HP8116A.Direction.UP, 100e-3) time.sleep(0.5) instr.autovernier_enabled = False assert instr.amplitude > 120e-3 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hp/test_hp856Xx.py0000644000175100001770000012504714623331163022175 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 datetime import datetime import pytest from pymeasure.test import expected_protocol from pymeasure.instruments.hp import HP8560A, HP8561B from pymeasure.instruments.hp.hp856Xx import Trace, MixerMode, CouplingMode, DemodulationMode, \ DetectionModes, AmplitudeUnits, HP856Xx, ErrorCode, FrequencyReference, PeakSearchMode, \ StatusRegister, SourceLevelingControlMode, SweepCoupleMode, SweepOut, TraceDataFormat, \ TriggerMode, WindowType class TestHP856Xx: def test_id(self): with expected_protocol( HP856Xx, [("ID?", "HP8560A,002,H03")], ) as instr: assert instr.id == "HP8560A,002,H03" def test_attenuation(self): with expected_protocol( HP856Xx, [("AT 70", None), ("AT?", "70")], ) as instr: # test set and get of attenuation as integer instr.attenuation = 70 assert instr.attenuation == 70 def test_attenuation_string_parameters(self): with expected_protocol( HP856Xx, [("AT AUTO", None), ("AT?", "20"), ], ) as instr: # test string parameters instr.attenuation = "AUTO" assert instr.attenuation == 20 def test_attenuation_truncation(self): with expected_protocol( HP856Xx, [("AT 20", None), ("AT?", "20")], ) as instr: instr.attenuation = 16 assert instr.attenuation == 20 @pytest.mark.parametrize("amplitude_unit", [e for e in AmplitudeUnits]) def test_amplitude_units(self, amplitude_unit): with expected_protocol( HP856Xx, [("AUNITS " + amplitude_unit, None), ("AUNITS?", amplitude_unit)], ) as instr: instr.amplitude_unit = amplitude_unit assert instr.amplitude_unit == amplitude_unit @pytest.mark.parametrize( "function, command", [ ("set_auto_couple", "AUTOCPL"), ("exchange_traces", "AXB"), ("subtract_display_line_from_trace_b", "BML"), ("set_continuous_sweep", "CONTS"), ("set_full_span", "FS"), ("set_linear_scale", "LN"), ("set_marker_to_center_frequency", "MKCF"), ("set_marker_minimum", "MKMIN"), ("set_marker_to_reference_level", "MKRL"), ("set_marker_delta_to_span", "MKSP"), ("set_marker_to_center_frequency_step_size", "MKSS"), ("preset", "IP"), ("recall_open_short_average", "RCLOSCAL"), ("recall_thru", "RCLTHRU"), ("sweep_single", "SNGLS"), ("store_open", "STOREOPEN"), ("store_short", "STORESHORT"), ("store_thru", "STORETHRU"), ("adjust_all", "ADJALL"), ("set_crt_adjustment_pattern", "ADJCRT"), ("trigger_sweep", "TS"), ] ) def test_primitive_commands(self, command, function): """ Tests primitive commands which have no parameter or query derivat """ with expected_protocol( HP856Xx, [(command, None)] ) as instr: getattr(instr, function)() @pytest.mark.parametrize("trace", [e for e in Trace]) def test_blank_trace(self, trace): with expected_protocol( HP856Xx, [("BLANK " + trace, None)] ) as instr: instr.blank_trace(trace) def test_blank_trace_exceptions(self): with expected_protocol( HP856Xx, [] ) as instr: with pytest.raises(ValueError): instr.blank_trace("TEST") with pytest.raises(TypeError): instr.blank_trace(0) @pytest.mark.parametrize("function, command", [ ("start_frequency", "FA"), ("center_frequency", "CF"), ("stop_frequency", "FB"), ("frequency_offset", "FOFFSET"), ("span", "SP") ]) @pytest.mark.parametrize("hp_derivat, max_freq", [(HP8560A, 2.9e9), (HP8561B, 6.5e9)]) def test_frequencies(self, function, command, hp_derivat, max_freq): with expected_protocol( hp_derivat, [("%s %.11E Hz" % (command, max_freq), None), ("%s?" % command, '%.11E' % max_freq)] ) as instr: setattr(instr, function, max_freq) assert getattr(instr, function) == max_freq @pytest.mark.parametrize("trace", [e for e in Trace]) def test_clear_write_trace(self, trace): with expected_protocol( HP856Xx, [("CLRW " + trace, None)] ) as instr: instr.clear_write_trace(trace) def test_clear_write_trace_exceptions(self): with expected_protocol( HP856Xx, [] ) as instr: with pytest.raises(ValueError): instr.clear_write_trace("TEST") with pytest.raises(TypeError): instr.clear_write_trace(0) @pytest.mark.parametrize("coupling", [e for e in CouplingMode]) def test_coupling(self, coupling): with expected_protocol( HP856Xx, [("COUPLE " + coupling, None), ("COUPLE?", coupling)] ) as instr: instr.coupling = coupling assert instr.coupling == coupling @pytest.mark.parametrize("demod_mode", [e for e in DemodulationMode]) def test_demodulation_mode(self, demod_mode): with expected_protocol( HP856Xx, [("DEMOD " + demod_mode, None), ("DEMOD?", demod_mode)] ) as instr: instr.demodulation_mode = demod_mode assert instr.demodulation_mode == demod_mode @pytest.mark.parametrize("on_off, boole", [("1", True), ("0", False)]) def test_demodulation_agc_enabled(self, on_off, boole): with expected_protocol( HP856Xx, [("DEMODAGC " + on_off, None), ("DEMODAGC?", on_off)] ) as instr: instr.demodulation_agc_enabled = boole assert instr.demodulation_agc_enabled == boole def test_demodulation_time(self): with expected_protocol( HP856Xx, [("DEMODT 1.02000000000E+01", None), ("DEMODT?", "1.03000000000E+01")] ) as instr: instr.demodulation_time = 10.2 assert instr.demodulation_time == 10.3 @pytest.mark.parametrize("detector_mode", [e for e in DetectionModes]) def test_detector_mode(self, detector_mode): with expected_protocol( HP856Xx, [("DET " + detector_mode, None), ("DET?", detector_mode)] ) as instr: instr.detector_mode = detector_mode assert instr.detector_mode == detector_mode @pytest.mark.parametrize("string_params", ["ON", "OFF"]) def test_display_line_enabled(self, string_params): with expected_protocol( HP856Xx, [ ("DL " + string_params, None) ] ) as instr: instr.display_line_enabled = True if string_params == "ON" else False def test_check_done(self): with expected_protocol( HP856Xx, [("DONE?", "1")], ) as instr: instr.check_done() def test_errors(self): with expected_protocol( HP856Xx, [("ERR?", "112,101,111")], ) as instr: assert instr.errors == [ErrorCode(112), ErrorCode(101), ErrorCode(111)] def test_elapsed_time(self): with expected_protocol( HP856Xx, [("EL?", "1800")], ) as instr: assert instr.elapsed_time == 1800 @pytest.mark.parametrize( "function, command", [ ("sampling_frequency", "SMP"), ("lo_frequency", "LO"), ("mroll_frequency", "MROLL"), ("oroll_frequency", "OROLL"), ("xroll_frequency", "XROLL") ] ) def test_fdiag_frequencies(self, function, command): with expected_protocol( HP856Xx, [("FDIAG %s,?" % command, '%.11E' % 2.8E8)] ) as instr: assert getattr(instr, function) == 2.8E8 def test_sampler_harmonic_number(self): with expected_protocol( HP856Xx, [("FDIAG HARM,?", '%.11E' % 1.40000000000E1)] ) as instr: assert instr.sampler_harmonic_number == 14 def test_frequency_display_enabled(self): with expected_protocol( HP856Xx, [("FDSP?", "0")] ) as instr: assert instr.frequency_display_enabled is False def test_fft(self): with expected_protocol( HP856Xx, [("FFT TRA,TRB,TRA", None)] ) as instr: instr.do_fft(Trace.A, Trace.B, Trace.A) def test_fft_exceptions(self): with expected_protocol( HP856Xx, [] ) as instr: with pytest.raises(TypeError): instr.do_fft(0, 4, 5) with pytest.raises(ValueError): instr.do_fft("TRAZ", "zuo", "TEWST") @pytest.mark.parametrize("frequency_reference", [e for e in FrequencyReference]) def test_frequency_reference_source(self, frequency_reference): with expected_protocol( HP856Xx, [("FREF " + frequency_reference, None), ("FREF?", frequency_reference)] ) as instr: instr.frequency_reference_source = frequency_reference assert instr.frequency_reference_source == frequency_reference @pytest.mark.parametrize( "function, command", [ ("graticule_enabled", "GRAT"), ("marker_signal_tracking_enabled", "MKTRACK"), ("marker_noise_mode_enabled", "MKNOISE"), ("normalize_trace_data_enabled", "NORMLIZE"), ("protect_state_enabled", "PSTATE"), ("trace_a_minus_b_enabled", "AMB"), ("trace_a_minus_b_plus_dl_enabled", "AMBPL"), ("annotation_enabled", "ANNOT") ] ) def test_on_off_commands(self, function, command): with expected_protocol( HP856Xx, [(command + " 0", None), (command + "?", "1")] ) as instr: setattr(instr, function, False) assert getattr(instr, function) is True @pytest.mark.parametrize("cmd", ["CURR", "FULL"]) def test_adjust_if(self, cmd): with expected_protocol( HP856Xx, [("ADJIF 1", None), ("ADJIF %s" % cmd, None), ("ADJIF?", "1")] ) as instr: instr.adjust_if = True instr.adjust_if = cmd assert instr.adjust_if is True def test_logarithmic_scale(self): with expected_protocol( HP856Xx, [("LG 1 DB", None), ("LG?", "10")] ) as instr: instr.logarithmic_scale = 1 assert instr.logarithmic_scale == 10 @pytest.mark.parametrize( "function, cmdstr", [ ("set_minimum_hold", "MINH"), ("set_maximum_hold", "MXMH") ] ) @pytest.mark.parametrize("trace", [e for e in Trace]) def test_hold(self, trace, function, cmdstr): with expected_protocol( HP856Xx, [(cmdstr + " " + trace, None)] ) as instr: getattr(instr, function)(trace) @pytest.mark.parametrize("function", ["set_minimum_hold", "set_maximum_hold"]) def test_hold_exceptions(self, function): with expected_protocol( HP856Xx, [] ) as instr: with pytest.raises(ValueError): getattr(instr, function)("TEST") with pytest.raises(TypeError): getattr(instr, function)(0) def test_marker_amplitude(self): with expected_protocol( HP856Xx, [("MKA?", 2.8e7)] ) as instr: assert instr.marker_amplitude == 2.8e7 def test_marker_delta(self): with expected_protocol( HP856Xx, [ ("MKD %.11E Hz" % 28, None), ("MKD?", 2.8e7) ] ) as instr: instr.marker_delta = 2.8e1 assert instr.marker_delta == 2.8e7 def test_marker_frequency(self): with expected_protocol( HP856Xx, [ ("MKF %.11E Hz" % 1, None), ("MKF?", 0.5) ] ) as instr: instr.marker_frequency = 1 assert instr.marker_frequency == 0.5 def test_frequency_counter_mode_enabled(self): with expected_protocol( HP856Xx, [ ("MKFC OFF", None), ("MKFC ON", None) ] ) as instr: instr.frequency_counter_mode_enabled = False instr.frequency_counter_mode_enabled = True def test_frequency_counter_resolution(self): with expected_protocol( HP856Xx, [ ("MKFCR %d Hz" % 1e3, None), ("MKFCR?", 1e4) ] ) as instr: instr.frequency_counter_resolution = 1e3 assert instr.frequency_counter_resolution == 1e4 @pytest.mark.parametrize("all_markers, cmdstring", [(True, " ALL"), (False, "")]) def test_deactivate_marker(self, all_markers, cmdstring): with expected_protocol( HP856Xx, [ ("MKOFF%s" % cmdstring, None), ] ) as instr: instr.deactivate_marker(all_markers) @pytest.mark.parametrize("mode", [e for e in PeakSearchMode]) def test_minimum_hold(self, mode): with expected_protocol( HP856Xx, [("MKPK " + mode, None)] ) as instr: instr.search_peak(mode) def test_marker_threshold(self): with expected_protocol( HP856Xx, [ ("AUNITS?", "DBM"), ("MKPT %d DBM" % -30, None), ("MKPT?", -70) ] ) as instr: instr.marker_threshold = -30 assert instr.marker_threshold == -70 def test_peak_excursion(self): with expected_protocol( HP856Xx, [ ("MKPX %g DB" % 10.3, None), ("MKPX?", 10.3) ] ) as instr: instr.peak_excursion = 10.3 assert instr.peak_excursion == 10.3 def test_marker_time(self): with expected_protocol( HP856Xx, [ ("MKT %gS" % 10.3, None), ("MKT?", 10.3) ] ) as instr: instr.marker_time = 10.3 assert instr.marker_time == 10.3 def test_mixer_level(self): with expected_protocol( HP856Xx, [ ("ML -30 DB", None), ("ML?", "-30") ] ) as instr: instr.mixer_level = -30 assert instr.mixer_level == -30 def test_normalized_reference_level(self): with expected_protocol( HP856Xx, [ ("AUNITS?", "DBM"), ("NRL -30 DBM", None), ("NRL?", "-30") ] ) as instr: instr.normalized_reference_level = -30 assert instr.normalized_reference_level == -30 def test_normalized_reference_position(self): with expected_protocol( HP856Xx, [ ("NRPOS 8.000000 DB", None), ("NRPOS?", "8") ] ) as instr: instr.normalized_reference_position = 8 assert instr.normalized_reference_position == 8 def test_display_parameters(self): with expected_protocol( HP856Xx, [ ("OP?", "72,16,712,766") ] ) as instr: assert instr.display_parameters == (72, 16, 712, 766) def test_plot(self): with expected_protocol( HP856Xx, [ ("PLOT 72,16,712,766", None) ] ) as instr: instr.plot(72, 16, 712, 766) def test_power_bandwidth(self): with expected_protocol( HP856Xx, [ ("PWRBW TRA,99.2?", "1.0e3") ] ) as instr: assert instr.get_power_bandwidth(Trace.A, 99.2) == 1e3 def test_resolution_bandwidth(self): with expected_protocol( HP856Xx, [ ("RB 30 Hz", None), ("RB AUTO", None), ("RB?", "30") ] ) as instr: instr.resolution_bandwidth = 30 instr.resolution_bandwidth = "AUTO" assert instr.resolution_bandwidth == 30 def test_resolution_bandwidth_to_span_ratio(self): with expected_protocol( HP856Xx, [ ("RBR 0.014", None), ("RBR?", "0.014") ] ) as instr: instr.resolution_bandwidth_to_span_ratio = 0.0140 assert instr.resolution_bandwidth_to_span_ratio == 0.0140 def test_recall_state(self): with expected_protocol( HP856Xx, [ ("RCLS LAST", None), ("RCLS 8", None) ] ) as instr: instr.recall_state("LAST") instr.recall_state(8) def test_recall_trace(self): with expected_protocol( HP856Xx, [ ("RCLT TRA,6", None) ] ) as instr: instr.recall_trace(Trace.A, 6) def test_firmware_revision(self): with expected_protocol( HP856Xx, [ ("REV?", "901101") ] ) as instr: assert instr.firmware_revision == datetime.strptime("11-01-1990", '%m-%d-%Y').date() def test_reference_level(self): with expected_protocol( HP856Xx, [ ("AUNITS?", "DBM"), ("RL 10 DBM", None), ("RL?", "10") ] ) as instr: instr.reference_level = 10 assert instr.reference_level == 10 def test_reference_level_calibration(self): with expected_protocol( HP856Xx, [ ("RLCAL 33", None), ("RLCAL?", "33") ] ) as instr: instr.reference_level_calibration = 33 assert instr.reference_level_calibration == 33 def test_request_service_conditions(self): with expected_protocol( HP856Xx, [ ("RQS 48", None), ("RQS?", "48") ] ) as instr: instr.request_service_conditions = \ StatusRegister.COMMAND_COMPLETE | StatusRegister.ERROR_PRESENT assert instr.request_service_conditions == \ StatusRegister.COMMAND_COMPLETE | StatusRegister.ERROR_PRESENT def test_save_state(self): with expected_protocol( HP856Xx, [ ("SAVES 6", None) ] ) as instr: instr.save_state(6) def test_save_trace(self): with expected_protocol( HP856Xx, [ ("SAVET TRA,6", None) ] ) as instr: instr.save_trace(Trace.A, 6) @pytest.mark.parametrize("param", ["FULL", "ZERO"]) def test_span_string_params(self, param): with expected_protocol( HP856Xx, [ ("SP %s" % param, None) ] ) as instr: instr.span = param @pytest.mark.parametrize("string_params", ["ON", "OFF"]) def test_squelch(self, string_params): with expected_protocol( HP856Xx, [("AUNITS?", "DBM"), ("SQUELCH 10 DBM", None), ("SQUELCH %s" % string_params, None), ("SQUELCH?", "10")] ) as instr: instr.squelch = 10 instr.squelch = string_params assert instr.squelch == 10 def test_service_request(self): with expected_protocol( HP856Xx, [ ("SRQ 48", None) ] ) as instr: instr.request_service(StatusRegister.COMMAND_COMPLETE | StatusRegister.ERROR_PRESENT) def test_sweep_time(self): with expected_protocol( HP856Xx, [("ST 10.000 S", None), ("ST AUTO", None), ("ST?", "10.00")] ) as instr: instr.sweep_time = 10 instr.sweep_time = "AUTO" assert instr.sweep_time == 10 @pytest.mark.parametrize("mode", [e for e in SweepCoupleMode]) def test_sweep_couple(self, mode): with expected_protocol( HP856Xx, [("SWPCPL " + mode, None), ("SWPCPL?", mode)] ) as instr: instr.sweep_couple = mode assert instr.sweep_couple == mode @pytest.mark.parametrize("mode", [e for e in SweepOut]) def test_sweep_output(self, mode): with expected_protocol( HP856Xx, [("SWPOUT " + mode, None), ("SWPOUT?", mode)] ) as instr: instr.sweep_output = mode assert instr.sweep_output == mode @pytest.mark.parametrize("mode", [e for e in TraceDataFormat]) def test_trace_data_format(self, mode): with expected_protocol( HP856Xx, [("TDF " + mode, None), ("TDF?", mode)] ) as instr: instr.trace_data_format = mode assert instr.trace_data_format == mode def test_threshold(self): with expected_protocol( HP856Xx, [("AUNITS?", "DBM"), ("TH 1.00E+01 DBM", None), ("TH?", "10.00")] ) as instr: instr.threshold = 10 assert instr.threshold == 10 def test_threshold_enabled(self): with expected_protocol( HP856Xx, [("TH ON", None)] ) as instr: instr.threshold_enabled = True def test_title(self): with expected_protocol( HP856Xx, [("TITLE@%s@" % "TestString", None)] ) as instr: instr.set_title("TestString") @pytest.mark.parametrize("mode", [e for e in TriggerMode]) def test_trigger_mode(self, mode): with expected_protocol( HP856Xx, [("TM " + mode, None), ("TM?", mode)] ) as instr: instr.trigger_mode = mode assert instr.trigger_mode == mode @pytest.mark.parametrize("function, cmd", [ ("get_trace_data_a", "TRA"), ("get_trace_data_b", "TRB") ]) def test_trace_data(self, function, cmd): data = [48, 61, 31, 73, 90, 82, 56, 48, 87, 78, 59, 103, 78, 76, 92, 52, 57, 72, 48, 82, 108, 63, 52, 79, 44, 88, 95, 99, 74, 79, 63, 100, 51, 85, 96, 69, 97, 50, 105, 94, 58, 98, 92, 92, 96, 59, 63, 34, 81, 56, 50, 94, 74, 61, 40, 48, 72, 69, 86, 114, 59, 70, 83, 53, 67, 111, 110, 99, 112, 72, 100, 44, 80, 81, 65, 34, 56, 62, 55, 78, 86, 68, 59, 66, 91, 91, 91, 63, 83, 90, 71, 94, 91, 46, 69, 70, 99, 52, 86, 63, 88, 75, 55, 80, 97, 31, 40, 81, 92, 75, 30, 113, 83, 64, 98, 49, 51, 99, 102, 54, 57, 38, 67, 84, 75, 59, 75, 78, 93, 47, 89, 107, 77, 42, 63, 98, 45, 81, 81, 98, 39, 39, 44, 89, 72, 77, 99, 104, 84, 61, 105, 76, 80, 29, 66, 45, 88, 98, 54, 108, 89, 88, 54, 79, 91, 38, 85, 98, 42, 66, 41, 94, 55, 49, 94, 57, 67, 69, 75, 96, 87, 75, 97, 101, 52, 85, 76, 47, 53, 108, 61, 82, 61, 61, 64, 56, 88, 62, 54, 91, 54, 38, 37, 91, 65, 60, 60, 70, 102, 97, 71, 93, 85, 92, 52, 85, 61, 77, 63, 96, 71, 40, 51, 65, 69, 78, 65, 81, 56, 63, 68, 59, 43, 120, 77, 58, 57, 79, 90, 62, 47, 50, 76, 77, 87, 38, 102, 72, 66, 74, 84, 73, 70, 64, 88, 86, 73, 83, 82, 98, 98, 93, 100, 114, 111, 116, 121, 127, 132, 139, 148, 153, 160, 167, 174, 182, 190, 197, 205, 215, 224, 233, 244, 255, 265, 275, 287, 300, 313, 325, 339, 354, 368, 383, 397, 414, 429, 443, 455, 466, 472, 475, 474, 468, 458, 447, 434, 417, 402, 388, 373, 357, 343, 329, 316, 303, 291, 279, 269, 259, 248, 239, 230, 220, 212, 204, 196, 189, 181, 175, 168, 162, 154, 147, 143, 136, 128, 124, 120, 113, 104, 104, 98, 92, 86, 115, 96, 86, 112, 77, 92, 88, 82, 79, 104, 49, 97, 73, 58, 68, 80, 84, 68, 78, 46, 74, 88, 81, 110, 80, 82, 89, 83, 50, 62, 64, 97, 58, 48, 66, 47, 53, 38, 108, 94, 88, 116, 66, 103, 85, 41, 63, 81, 58, 118, 50, 110, 93, 52, 43, 74, 42, 89, 71, 66, 50, 73, 89, 119, 86, 57, 100, 55, 79, 71, 75, 90, 47, 86, 79, 110, 99, 87, 79, 87, 63, 76, 73, 62, 98, 107, 89, 80, 90, 71, 44, 46, 50, 41, 66, 109, 47, 97, 70, 77, 75, 103, 120, 69, 67, 92, 90, 81, 54, 95, 97, 86, 83, 71, 48, 53, 95, 62, 51, 85, 59, 71, 90, 63, 69, 108, 72, 75, 25, 116, 60, 51, 90, 84, 74, 43, 88, 83, 60, 99, 86, 61, 72, 82, 77, 80, 64, 84, 101, 108, 57, 107, 92, 77, 62, 81, 74, 69, 50, 46, 56, 51, 79, 49, 34, 101, 76, 55, 111, 103, 92, 109, 123, 74, 50, 68, 56, 79, 43, 55, 43, 111, 46, 76, 103, 85, 70, 53, 70, 88, 54, 117, 78, 85, 57, 72, 88, 89, 58, 98, 55, 68, 77, 71, 56, 75, 44, 53, 76, 95, 81, 48, 40, 84, 38, 84, 110, 82, 67, 114, 79, 40, 52, 102, 103, 85, 80, 108, 86, 40, 123, 66, 86, 93, 83, 78, 42, 74, 33, 79, 55, 67, 111, 77, 61, 89, 58, 89, 103, 77, 77, 62, 57, 61, 54, 30] expected_data = [-82.0, -79.83, -84.83, -77.83, -75.0, -76.33, -80.67, -82.0, -75.5, -77.0, -80.17, -72.83, -77.0, -77.33, -74.67, -81.33, -80.5, -78.0, -82.0, -76.33, -72.0, -79.5, -81.33, -76.83, -82.67, -75.33, -74.17, -73.5, -77.67, -76.83, -79.5, -73.33, -81.5, -75.83, -74.0, -78.5, -73.83, -81.67, -72.5, -74.33, -80.33, -73.67, -74.67, -74.67, -74.0, -80.17, -79.5, -84.33, -76.5, -80.67, -81.67, -74.33, -77.67, -79.83, -83.33, -82.0, -78.0, -78.5, -75.67, -71.0, -80.17, -78.33, -76.17, -81.17, -78.83, -71.5, -71.67, -73.5, -71.33, -78.0, -73.33, -82.67, -76.67, -76.5, -79.17, -84.33, -80.67, -79.67, -80.83, -77.0, -75.67, -78.67, -80.17, -79.0, -74.83, -74.83, -74.83, -79.5, -76.17, -75.0, -78.17, -74.33, -74.83, -82.33, -78.5, -78.33, -73.5, -81.33, -75.67, -79.5, -75.33, -77.5, -80.83, -76.67, -73.83, -84.83, -83.33, -76.5, -74.67, -77.5, -85.0, -71.17, -76.17, -79.33, -73.67, -81.83, -81.5, -73.5, -73.0, -81.0, -80.5, -83.67, -78.83, -76.0, -77.5, -80.17, -77.5, -77.0, -74.5, -82.17, -75.17, -72.17, -77.17, -83.0, -79.5, -73.67, -82.5, -76.5, -76.5, -73.67, -83.5, -83.5, -82.67, -75.17, -78.0, -77.17, -73.5, -72.67, -76.0, -79.83, -72.5, -77.33, -76.67, -85.17, -79.0, -82.5, -75.33, -73.67, -81.0, -72.0, -75.17, -75.33, -81.0, -76.83, -74.83, -83.67, -75.83, -73.67, -83.0, -79.0, -83.17, -74.33, -80.83, -81.83, -74.33, -80.5, -78.83, -78.5, -77.5, -74.0, -75.5, -77.5, -73.83, -73.17, -81.33, -75.83, -77.33, -82.17, -81.17, -72.0, -79.83, -76.33, -79.83, -79.83, -79.33, -80.67, -75.33, -79.67, -81.0, -74.83, -81.0, -83.67, -83.83, -74.83, -79.17, -80.0, -80.0, -78.33, -73.0, -73.83, -78.17, -74.5, -75.83, -74.67, -81.33, -75.83, -79.83, -77.17, -79.5, -74.0, -78.17, -83.33, -81.5, -79.17, -78.5, -77.0, -79.17, -76.5, -80.67, -79.5, -78.67, -80.17, -82.83, -70.0, -77.17, -80.33, -80.5, -76.83, -75.0, -79.67, -82.17, -81.67, -77.33, -77.17, -75.5, -83.67, -73.0, -78.0, -79.0, -77.67, -76.0, -77.83, -78.33, -79.33, -75.33, -75.67, -77.83, -76.17, -76.33, -73.67, -73.67, -74.5, -73.33, -71.0, -71.5, -70.67, -69.83, -68.83, -68.0, -66.83, -65.33, -64.5, -63.33, -62.17, -61.0, -59.67, -58.33, -57.17, -55.83, -54.17, -52.67, -51.17, -49.33, -47.5, -45.83, -44.17, -42.17, -40.0, -37.83, -35.83, -33.5, -31.0, -28.67, -26.17, -23.83, -21.0, -18.5, -16.17, -14.17, -12.33, -11.33, -10.83, -11.0, -12.0, -13.67, -15.5, -17.67, -20.5, -23.0, -25.33, -27.83, -30.5, -32.83, -35.17, -37.33, -39.5, -41.5, -43.5, -45.17, -46.83, -48.67, -50.17, -51.67, -53.33, -54.67, -56.0, -57.33, -58.5, -59.83, -60.83, -62.0, -63.0, -64.33, -65.5, -66.17, -67.33, -68.67, -69.33, -70.0, -71.17, -72.67, -72.67, -73.67, -74.67, -75.67, -70.83, -74.0, -75.67, -71.33, -77.17, -74.67, -75.33, -76.33, -76.83, -72.67, -81.83, -73.83, -77.83, -80.33, -78.67, -76.67, -76.0, -78.67, -77.0, -82.33, -77.67, -75.33, -76.5, -71.67, -76.67, -76.33, -75.17, -76.17, -81.67, -79.67, -79.33, -73.83, -80.33, -82.0, -79.0, -82.17, -81.17, -83.67, -72.0, -74.33, -75.33, -70.67, -79.0, -72.83, -75.83, -83.17, -79.5, -76.5, -80.33, -70.33, -81.67, -71.67, -74.5, -81.33, -82.83, -77.67, -83.0, -75.17, -78.17, -79.0, -81.67, -77.83, -75.17, -70.17, -75.67, -80.5, -73.33, -80.83, -76.83, -78.17, -77.5, -75.0, -82.17, -75.67, -76.83, -71.67, -73.5, -75.5, -76.83, -75.5, -79.5, -77.33, -77.83, -79.67, -73.67, -72.17, -75.17, -76.67, -75.0, -78.17, -82.67, -82.33, -81.67, -83.17, -79.0, -71.83, -82.17, -73.83, -78.33, -77.17, -77.5, -72.83, -70.0, -78.5, -78.83, -74.67, -75.0, -76.5, -81.0, -74.17, -73.83, -75.67, -76.17, -78.17, -82.0, -81.17, -74.17, -79.67, -81.5, -75.83, -80.17, -78.17, -75.0, -79.5, -78.5, -72.0, -78.0, -77.5, -85.83, -70.67, -80.0, -81.5, -75.0, -76.0, -77.67, -82.83, -75.33, -76.17, -80.0, -73.5, -75.67, -79.83, -78.0, -76.33, -77.17, -76.67, -79.33, -76.0, -73.17, -72.0, -80.5, -72.17, -74.67, -77.17, -79.67, -76.5, -77.67, -78.5, -81.67, -82.33, -80.67, -81.5, -76.83, -81.83, -84.33, -73.17, -77.33, -80.83, -71.5, -72.83, -74.67, -71.83, -69.5, -77.67, -81.67, -78.67, -80.67, -76.83, -82.83, -80.83, -82.83, -71.5, -82.33, -77.33, -72.83, -75.83, -78.33, -81.17, -78.33, -75.33, -81.0, -70.5, -77.0, -75.83, -80.5, -78.0, -75.33, -75.17, -80.33, -73.67, -80.83, -78.67, -77.17, -78.17, -80.67, -77.5, -82.67, -81.17, -77.33, -74.17, -76.5, -82.0, -83.33, -76.0, -83.67, -76.0, -71.67, -76.33, -78.83, -71.0, -76.83, -83.33, -81.33, -73.0, -72.83, -75.83, -76.67, -72.0, -75.67, -83.33, -69.5, -79.0, -75.67, -74.5, -76.17, -77.0, -83.0, -77.67, -84.5, -76.83, -80.83, -78.83, -71.5, -77.17, -79.83, -75.17, -80.33, -75.17, -72.83, -77.17, -77.17, -79.67, -80.5, -79.83, -81.0, -85.0] with expected_protocol( HP856Xx, [ ("TDF M", None), ("AUNITS?", "DBM"), ("RL?", "10.0"), ("LG?", "10.0"), (cmd + "?", ','.join([str(i) for i in data])) ] ) as instr: assert getattr(instr, function)() == expected_data def test_fft_trace_window(self): with expected_protocol( HP856Xx, [("TWNDOW TRA,FLATTOP", None)] ) as instr: instr.create_fft_trace_window(Trace.A, WindowType.Flattop) @pytest.mark.parametrize("param", [("ON", True), ("OFF", False)]) def test_video_average_enabled(self, param): str_param, boolean = param with expected_protocol( HP856Xx, [("VAVG " + str_param, None)] ) as instr: instr.video_average_enabled = boolean def test_video_bandwidth(self): with expected_protocol( HP856Xx, [("VB 70 Hz", None), ("VB?", "70")], ) as instr: instr.video_bandwidth = 70 assert instr.video_bandwidth == 70 def test_video_bandwidth_string_parameters(self): with expected_protocol( HP856Xx, [("VB AUTO", None), ("VB?", "20"), ], ) as instr: instr.video_bandwidth = "AUTO" assert instr.video_bandwidth == 20 def test_video_bandwidth_to_resolution_bandwidth(self): with expected_protocol( HP856Xx, [("VBR 0.005", None), ("VBR?", "0.005")], ) as instr: instr.video_bandwidth_to_resolution_bandwidth = 0.005 assert instr.video_bandwidth_to_resolution_bandwidth == 0.005 @pytest.mark.parametrize("trace", [e for e in Trace]) def test_view_trace(self, trace): with expected_protocol( HP856Xx, [("VIEW " + trace, None)] ) as instr: instr.view_trace(trace) def test_video_trigger_level(self): with expected_protocol( HP856Xx, [("AUNITS?", "DBM"), ("VTL -200.780 DBM", None), ("VTL?", "0.005")], ) as instr: instr.video_trigger_level = -200.78 assert instr.video_trigger_level == 0.005 class TestHP8560A: @pytest.mark.parametrize("mode", [e for e in SourceLevelingControlMode]) def test_source_leveling_control(self, mode): with expected_protocol( HP8560A, [("SRCALC " + mode, None), ("SRCALC?", mode)] ) as instr: instr.source_leveling_control = mode assert instr.source_leveling_control == mode def test_tracking_adjust_coarse(self): with expected_protocol( HP8560A, [("SRCCRSTK 10", None), ("SRCCRSTK?", "10")] ) as instr: instr.tracking_adjust_coarse = 10 assert instr.tracking_adjust_coarse == 10 def test_tracking_adjust_fine(self): with expected_protocol( HP8560A, [("SRCFINTK 10", None), ("SRCFINTK?", "10")] ) as instr: instr.tracking_adjust_fine = 10 assert instr.tracking_adjust_fine == 10 def test_source_power_offset(self): with expected_protocol( HP8560A, [("AUNITS?", "DBM"), ("SRCPOFS 10 DBM", None), ("SRCPOFS?", "10")] ) as instr: instr.source_power_offset = 10 assert instr.source_power_offset == 10 def test_source_power_step(self): with expected_protocol( HP8560A, [("SRCPSTP 10.10 DB", None), ("SRCPSTP?", "10.10")] ) as instr: instr.source_power_step = 10.1 assert instr.source_power_step == 10.1 def test_source_power_sweep(self): with expected_protocol( HP8560A, [("SRCPSWP 10.00 DB", None), ("SRCPSWP?", "10.00")] ) as instr: instr.source_power_sweep = 10 assert instr.source_power_sweep == 10 def test_source_power_sweep_enabled(self): with expected_protocol( HP8560A, [("SRCPSWP OFF", None)] ) as instr: instr.source_power_sweep_enabled = False def test_source_power(self): with expected_protocol( HP8560A, [("AUNITS?", "DBM"), ("SRCPWR 2.00 DBM", None), ("SRCPWR ON", None), ("SRCPWR?", "2.00")] ) as instr: instr.source_power = 2.0 instr.source_power = "ON" assert instr.source_power == 2.0 def test_source_peak_tracking(self): with expected_protocol( HP8560A, [("SRCTKPK", None)] ) as instr: instr.activate_source_peak_tracking() class TestHP8561B: @pytest.mark.parametrize("mixer_mode", [e for e in MixerMode]) def test_external_mixer(self, mixer_mode): with expected_protocol( HP8561B, [("MXRMODE " + mixer_mode, None), ("MXRMODE?", mixer_mode)] ) as instr: instr.mixer_mode = mixer_mode assert instr.mixer_mode == mixer_mode def test_conversion_loss(self): with expected_protocol( HP8561B, [("CNVLOSS 10.2 DB", None), ("CNVLOSS?", "10.3")] ) as instr: instr.conversion_loss = 10.2 assert instr.conversion_loss == 10.3 def test_fullband(self): with expected_protocol( HP8561B, [("FULLBAND K", None)] ) as instr: instr.set_fullband("K") def test_fullband_exceptions(self): with expected_protocol( HP8561B, [] ) as instr: with pytest.raises(TypeError): instr.set_fullband(1) with pytest.raises(ValueError): instr.set_fullband("Z") def test_harmonic_number_lock(self): with expected_protocol( HP8561B, [("HNLOCK 10", None), ("HNLOCK?", "10")] ) as instr: instr.harmonic_number_lock = 10 assert instr.harmonic_number_lock == 10 @pytest.mark.parametrize("params", [("ON", True), ("OFF", False)]) def test_harmonic_number_lock_enabled(self, params): str_param, boolean = params with expected_protocol( HP8561B, [("HNLOCK %s" % str_param, None)] ) as instr: instr.harmonic_number_lock_enabled = boolean @pytest.mark.parametrize( "function, command", [ ("unlock_harmonic_number", "HUNLK"), ("set_signal_identification_to_center_frequency", "IDCF"), ("peak_preselector", "PP") ] ) def test_primitive_commands(self, command, function): """ Tests primitive commands which have no parameter or query derivat """ with expected_protocol( HP8561B, [(command, None)] ) as instr: getattr(instr, function)() def test_signal_identification_frequency(self): with expected_protocol( HP8561B, [("IDFREQ?", 2.7897e9)] ) as instr: assert instr.signal_identification_frequency == 2.7897e9 def test_mixer_bias(self): with expected_protocol( HP8561B, [("MBIAS -9.900 MA", None), ("MBIAS?", "5.5")] ) as instr: instr.mixer_bias = -9.9 assert instr.mixer_bias == 5.5 @pytest.mark.parametrize("params", [("ON", True), ("OFF", False)]) def test_mixer_bias_enabled(self, params): str_param, boolean = params with expected_protocol( HP8561B, [("MBIAS %s" % str_param, None)] ) as instr: instr.mixer_bias_enabled = boolean def test_preselector_dac_number(self): with expected_protocol( HP8561B, [("PSDAC 10", None), ("PSDAC?", "10")] ) as instr: instr.preselector_dac_number = 10 assert instr.preselector_dac_number == 10 def test_signal_identification(self): with expected_protocol( HP8561B, [("SIGID AUTO", None), ("SIGID?", "1")] ) as instr: instr.signal_identification = "AUTO" assert instr.signal_identification is True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/hp/test_hp8657b.py0000644000175100001770000000351514623331163022101 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.hp import HP8657B def test_frequency(): with expected_protocol( HP8657B, [(b"FR 1234567890 HZ", None), (b"FR 12345678 HZ", None)], ) as instr: instr.frequency = 1.23456789e9 instr.frequency = 1.2345678e7 def test_level(): with expected_protocol( HP8657B, [(b"AP -123.4 DM", None)], ) as instr: instr.level = -123.4 def test_output(): with expected_protocol( HP8657B, [(b"R3", None), (b"R2", None)], ) as instr: instr.output_enabled = True instr.output_enabled = False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.437606 pymeasure-0.14.0/tests/instruments/inficon/0000755000175100001770000000000014623331176020403 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/inficon/test_sqm160.py0000644000175100001770000000571514623331163023047 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.inficon.sqm160 import calculate_checksum, SQM160 def test_checksum(): """Verify the calculate_checksum function.""" # test against values documented in the manual assert calculate_checksum(b'#@') == b'O7' assert calculate_checksum(b'$C?') == b'g/' def test_firmware_version(): """Verify the communication of the firmware version.""" with expected_protocol( SQM160, [(b"!#@O7", b"!0AMON Ver 4.13Uw"), ], ) as inst: assert inst.firmware_version == "MON Ver 4.13" def test_number_of_channels(): """Verify the communication of the number of channels.""" with expected_protocol( SQM160, [(b"!#JO8", b"!%A6v\x86"), ], ) as inst: assert inst.number_of_channels == 6 def test_average_rate(): """Verify reading of the average rate.""" with expected_protocol( SQM160, [(b"!#M\x8e\x8a", b"!*A 0.01 i?"), ], ) as inst: assert inst.average_rate == pytest.approx(0.01) def test_average_thickness(): """Verify reading of the average rate.""" with expected_protocol( SQM160, [(b"!#O\x8f9", b"!+A 0.000 Jo"), ], ) as inst: assert inst.average_thickness == pytest.approx(0.0) def test_channel_rate(): """Verify reading of the rate of a channel.""" with expected_protocol( SQM160, [(b"!%L1?\x85{", b"!*A 0.00 [d"), ], ) as inst: assert inst.sensor_1.rate == pytest.approx(0.0) def test_channel_frequency(): """Verify reading of the frequency of a channel.""" with expected_protocol( SQM160, [(b"!$P1Z\x91", b"!/A5875830.230:X"), ], ) as inst: assert inst.sensor_1.frequency == pytest.approx(5875830.23) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.437606 pymeasure-0.14.0/tests/instruments/ipgphotonics/0000755000175100001770000000000014623331176021464 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/ipgphotonics/test_yar.py0000644000175100001770000001372614623331163023675 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.ipgphotonics.yar import YAR init_comm = [("RNP", "RNP: 0.200"), ("RMP", "RMP: 10.5"), ("RDPT", "RDPT: 0.100")] ################################ # Values according to the manual def test_init(): """This test tests already getting min/max output power.""" with expected_protocol( YAR, init_comm ): pass def test_status_emission(): with expected_protocol( YAR, init_comm + [("STA", "STA: 1")] ) as inst: assert inst.status == YAR.Status.EMISSION def test_status_backreflection(): with expected_protocol( YAR, init_comm + [("STA", "STA: 131072")] ) as inst: assert inst.status == YAR.Status.HIGH_BACKREFLECTION def test_emission_enabled_getter(): with expected_protocol( YAR, init_comm + [("STA", "STA:1")] ) as inst: assert inst.emission_enabled is True def test_emission_enabled_setter_on(): with expected_protocol( YAR, init_comm + [("EMON", "EMON")] ) as inst: inst.emission_enabled = True def test_emission_enabled_setter_off(): with expected_protocol( YAR, init_comm + [("EMOFF", "EMOFF")] ) as inst: inst.emission_enabled = False def test_power(): with expected_protocol( YAR, init_comm + [("ROP", "ROP: 3.45")] ) as inst: assert inst.power == 3.45 def test_power_off(): """Verify, that power 'Off' is translated to 0.""" with expected_protocol( YAR, init_comm + [("ROP", "ROP: Off")] ) as inst: assert inst.power == 0 def test_power_low(): """Verify, that power 'Low' is translated to the default minimum 0.1.""" with expected_protocol( YAR, init_comm + [("ROP", "ROP: Low")] ) as inst: assert inst.power == 0.1 def test_init_sets_minimum_display_power(): """Test, that a different minimum power is shown in the translation of 'Low'.""" # not in the manual! with expected_protocol( YAR, # init_comm is modified [("RNP", "RNP: 0.200"), ("RMP", "RMP: 10.5"), ("RDPT", "RDPT: 5"), ("ROP", "ROP: Low")] ) as inst: assert inst.power == 5 def test_power_setpoint_getter(): with expected_protocol( YAR, init_comm + [("RPS", "RPS: 1.23")] ) as inst: assert inst.power_setpoint == 1.23 def test_power_setpoint_setter(): with expected_protocol( YAR, init_comm + [("SPS 9.7", "SPS: 9.7")] ) as inst: inst.power_setpoint = 9.7 def test_power_setpoint_setter_out_of_range_driver(): with expected_protocol( YAR, # init_comm is modified [("RNP", "RNP: 0.200"), ("RMP", "RMP: 10.5"), ("RDPT", "RDPT: 0.100")] ) as inst: with pytest.raises(ValueError): inst.power_setpoint = 20 def test_power_setpoint_setter_out_of_range_device(): with expected_protocol( YAR, init_comm + [("SPS 5", "ERR: Out of Range")] ) as inst: with pytest.raises(ConnectionError): inst.power_setpoint = 5 def test_current(): with expected_protocol( YAR, init_comm + [("RDC", "RDC: 2.34")] ) as inst: assert inst.current == 2.34 def test_temperature(): with expected_protocol( YAR, init_comm + [("RCT", "RCT: 28.9")] ) as inst: assert inst.temperature == 28.9 def test_wavelength_temperature_getter(): with expected_protocol( YAR, init_comm + [("RWA", "RWA: 31.345")] ) as inst: assert inst.wavelength_temperature == 31.345 def test_wavelength_temperature_setter(): with expected_protocol( YAR, init_comm + [("SWA 35.658", "SWA: 35.658")] ) as inst: inst.wavelength_temperature = 35.658 def test_seed_temperature(): with expected_protocol( YAR, init_comm + [("RST", "RST: 31.345")] ) as inst: assert inst.temperature_seed == 31.345 def test_clear(): with expected_protocol( YAR, init_comm + [("RERR", "RERR")] ) as inst: inst.clear() def test_model(): with expected_protocol( YAR, init_comm + [("RMN", "RMN: YLM-10-SF")] ) as inst: assert inst.id == "YLM-10-SF" def test_firmware(): with expected_protocol( YAR, init_comm + [("RFV", "RFV: 1.0.114")] ) as inst: assert inst.firmware == "1.0.114" def test_minimum_display_power(): with expected_protocol( YAR, init_comm + [("RDPT", "RDPT: 0.100")] ) as inst: assert inst.minimum_display_power == 0.1 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.437606 pymeasure-0.14.0/tests/instruments/keithley/0000755000175100001770000000000014623331176020574 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keithley/test_keithley2000.py0000644000175100001770000000506514623331163024327 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.keithley.keithley2000 import Keithley2000 def test_voltage_read(): with expected_protocol( Keithley2000, [(":READ?", "3.1415")], ) as inst: assert inst.voltage == pytest.approx(3.1415) def test_voltage_range(): with expected_protocol( Keithley2000, [(":SENS:VOLT:RANG?", "955"), (":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG 234", None) ], ) as inst: assert inst.voltage_range == 955 inst.voltage_range = 234 def test_voltage_range_trunc(): with expected_protocol( Keithley2000, [(":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG 1010", None), (":SENS:VOLT:RANG?", "1010"), ], ) as inst: inst.voltage_range = 3333 # too large, gets truncated assert inst.voltage_range == 1010 def test_mode(): "Confirm that mode string mapping works correctly" with expected_protocol( Keithley2000, [(":CONF?", "VOLT:AC"), (":CONF:FRES", None), ], ) as inst: assert inst.mode == 'voltage ac' inst.mode = 'resistance 4W' def test_measure_voltage(): with expected_protocol( Keithley2000, [(":CONF:VOLT:AC", None), (":SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:AC:RANG 300", None), ], ) as inst: inst.measure_voltage(max_voltage=300, ac=True) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keithley/test_keithley2182.py0000644000175100001770000000607414623331163024343 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.keithley.keithley2182 import Keithley2182 def test_voltage_read(): with expected_protocol( Keithley2182, [(":READ?", "3.1415")], ) as inst: assert inst.voltage == pytest.approx(3.1415) def test_voltage_range(): with expected_protocol( Keithley2182, [(":SENS:VOLT:CHAN1:RANG?", "92"), (":SENS:VOLT:CHAN1:RANG 2", None) ], ) as inst: assert inst.ch_1.voltage_range == 92 inst.ch_1.voltage_range = 2 def test_voltage_range_trunc(): with expected_protocol( Keithley2182, [(":SENS:VOLT:CHAN2:RANG 15", None), (":SENS:VOLT:CHAN2:RANG?", "12"), ], ) as inst: inst.ch_2.voltage_range = 15 # too large, gets truncated assert inst.ch_2.voltage_range == 12 def test_active_channel(): with expected_protocol( Keithley2182, [(":SENS:CHAN?", "1"), (":SENS:CHAN 2", None), ], ) as inst: assert inst.active_channel == 1 inst.active_channel = 2 def test_thermocouple(): with expected_protocol( Keithley2182, [(":SENS:TEMP:TC?", "S"), (":SENS:TEMP:TC K", None), ], ) as inst: assert inst.thermocouple == 'S' inst.thermocouple = 'K' def test_setup_voltage(): with expected_protocol( Keithley2182, [(":SENS:CHAN 1;" ":SENS:FUNC 'VOLT';" ":SENS:VOLT:NPLC 5;", None), (":SENS:VOLT:RANG:AUTO 1", None), ("SYST:ERR?", '0,"No error"'), ], ) as inst: inst.ch_1.setup_voltage() def test_setup_temperature(): with expected_protocol( Keithley2182, [(":SENS:CHAN 2;" ":SENS:FUNC 'TEMP';" ":SENS:TEMP:NPLC 5", None), ("SYST:ERR?", '0,"No error"'), ], ) as inst: inst.ch_2.setup_temperature() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keithley/test_keithley2200.py0000644000175100001770000000463214623331163024330 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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.test import expected_protocol from pymeasure.instruments.keithley.keithley2200 import Keithley2200 def test_init(): with expected_protocol( Keithley2200, [], ): pass # Verify the expected communication. def test_voltage(): # instr.ch1.voltage should produce the following commands with expected_protocol( Keithley2200, [ (b"INST:SEL CH1;VOLT 1.456", None), (b"INST:SEL CH1;VOLT?", 1.456), (b"INST:SEL CH1;MEAS:VOLT?", 1.456), (b"INST:SEL CH3;VOLT 1.456", None), ], ) as instr: instr.ch_1.voltage_setpoint = 1.456 assert instr.ch_1.voltage_setpoint == 1.456 assert instr.ch_1.voltage == 1.456 instr.ch_3.voltage_setpoint = 1.456 def test_current(): # instr.ch1.voltage should produce the following commands with expected_protocol( Keithley2200, [(b"INST:SEL CH3;CURR 1.456", None), (b"INST:SEL CH1;CURR 2.654", None)], ) as instr: instr.ch_3.current_limit = 1.456 instr.ch_1.current_limit = 2.654 def test_output_enabled(): with expected_protocol( Keithley2200, [(b"INST:SEL CH1;SOURCE:OUTP:ENAB 1", None)], ) as instr: instr.ch_1.output_enabled = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keithley/test_keithley2306.py0000644000175100001770000000451014623331163024332 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.keithley.keithley2306 import Keithley2306 def test_init(): with expected_protocol( Keithley2306, [], ): pass # Verify the expected communication. def test_nplc(): with expected_protocol( Keithley2306, [(b":SENS2:NPLC?", b"1.234")], ) as instr: assert instr.ch2.nplc == 1.234 def test_nplc_setter(): with expected_protocol( Keithley2306, [(b":SENS2:NPLC 1.234", None)], ) as instr: instr.ch2.nplc = 1.234 def test_relay(): with expected_protocol( Keithley2306, [(b":OUTP:REL2?", b"ONE")], ) as instr: assert instr.relay2.closed is True def test_relay_setter(): with expected_protocol( Keithley2306, [(b":OUTP:REL2 ONE", None)], ) as instr: instr.relay2.closed = True def test_step(): with expected_protocol( Keithley2306, [(b":SENS:PCUR:STEP:TLEV3?", 4)], ) as instr: step = instr.ch1.pulse_current_step(3) assert step.trigger_level == 4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keithley/test_keithley2306_with_device.py0000644000175100001770000006701214623331163026712 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pytest import math from pymeasure.instruments.keithley.keithley2306 import Keithley2306 pytest.skip('Only work with connected hardware', allow_module_level=True) class TestKeithley2306: """ Unit tests for Keithley2306 class. This test suite, needs the following setup to work properly: - A Keithley2306 device should be connected to the computer; - The device's address must be set in the RESOURCE constant; """ ################################################## # Keithley2306 device address goes here: RESOURCE = "USB0::10893::6039::CN57266430::INSTR" ################################################## ######################### # PARAMETRIZATION CASES # ######################### CHANNELS = [1, 2] RELAYS = [1, 2, 3, 4] BOOLEANS = [False, True] BRIGHTNESSES = [0, 0.12, 0.25, 0.3, 0.5, 0.6, 0.75, 0.8, 1] DISPLAY_TEXT_DATAS = ['', 'Hello', '0123456789012345678901234578901'] BANDWIDTHS = ['high', 'low'] IMPEDANCES = [0, 0.01, 0.1, 0.2, 0.25, 0.5, 0.77, 0.99, 1] SENSE_MODES = ['voltage', 'current', 'dvm'] NPLCS = [0.01, 0.1, 1, 2, 4, 10] AVERAGE_COUNTS = [1, 2, 5, 10] CURRENT_RANGES = [0.005, 5] PULSE_CURRENT_AVERAGE_COUNTS = [1, 2, 5, 100, 500, 1000, 5000] PULSE_CURRENT_MODES = ['high', 'low', 'average'] PULSE_CURRENT_TIMES = [33.33333e-06, 1.2E-3, 0.8333] PULSE_CURRENT_STEP_COUNTS = [0, 1, 5, 19] PULSE_CURRENT_STEP_TIMES = [33.33333e-06, 1.2E-3, 100e-3] PULSE_CURRENT_STEP_TIMEOUTS = [2e-3, 20.1e-3, 200e-3] PULSE_CURRENT_STEP_TIMEOUT_INITIALS = [10e-3, 210.3e-3, 60] PULSE_CURRENT_STEP_DELAYS = [0, 10e-3, 57.32e-3, 100e-3] PULSE_CURRENT_STEP_RANGES = [100e-3, 1, 5] PULSE_CURRENT_STEP_INDICES = [1, 2, 5, 20] PULSE_CURRENT_STEP_TRIGGER_LEVELS = [0, 50e-3, 100e-3, 500e-3, 1, 2.5, 5] PULSE_CURRENT_TRIGGER_DELAYS = [0, 0.05, 0.1, 1, 2, 5] PULSE_CURRENT_TRIGGER_LEVELS = [0, 50e-3, 100e-3, 500e-3, 1, 2.5, 5] PULSE_CURRENT_TRIGGER_LEVEL_RANGES = [100e-3, 1, 5] PULSE_CURRENT_TIMEOUTS = [0.005, 1, 32] LONG_INTEGRATION_TRIGGER_EDGES = ['rising', 'falling', 'neither'] LONG_INTEGRATION_TIMES = [0.850, 1, 30, 60] LONG_INTEGRATION_TRIGGER_LEVELS = [0, 50e-3, 100e-3, 500e-3, 1, 2.5, 5] LONG_INTEGRATION_TIMEOUTS = [1, 30, 63] LONG_INTEGRATION_TRIGGER_LEVEL_RANGES = [100e-3, 1, 5] SOURCE_VOLTAGES = [0, 3.3, 15] SOURCE_VOLTAGE_PROTECTIONS = [0, 4, 8] SOURCE_CURRENTS = [0.006, 1, 5] SOURCE_CURRENT_LIMIT_TYPES = ['limit', 'trip'] INSTR = Keithley2306(RESOURCE) ############ # FIXTURES # ############ def reset_display(self): self.INSTR.display_enabled = True self.INSTR.display_text_enabled = False self.INSTR.display_text_data = '' self.INSTR.display_brightness = 1.0 def reset_relays(self): for i in range(1, 5): self.INSTR.relay(i).closed = False @pytest.fixture def instr(self): self.INSTR.reset() return self.INSTR ######### # TESTS # ######### @pytest.mark.parametrize("case", BOOLEANS) def test_display_enabled(self, instr, case): try: assert instr.display_enabled instr.display_enabled = case assert instr.display_enabled == case finally: self.reset_display() @pytest.mark.parametrize("case", BRIGHTNESSES) def test_display_brightness(self, instr, case): try: assert instr.display_brightness == 1.0 instr.display_brightness = case assert instr.display_brightness == math.ceil(4 * case) / 4 finally: self.reset_display() @pytest.mark.parametrize("case", CHANNELS) def test_display_channel(self, instr, case): assert instr.display_channel == 1 instr.display_channel = case assert instr.display_channel == case @pytest.mark.parametrize("display_text_data", DISPLAY_TEXT_DATAS) @pytest.mark.parametrize("case", BOOLEANS) def test_display_text(self, instr, display_text_data, case): try: assert instr.display_text_data == '' assert not instr.display_text_enabled instr.display_text_data = display_text_data instr.display_text_enabled = case assert instr.display_text_data == display_text_data assert instr.display_text_enabled == case finally: self.reset_display() @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_output_enabled(self, instr, channel, case): assert not instr.ch(channel).enabled instr.ch(channel).enabled = case assert instr.ch(channel).enabled == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", BANDWIDTHS) def test_output_bandwidth(self, instr, channel, case): instr = instr assert instr.ch(channel).bandwidth == 'low' if channel == 1 else 'high' instr.ch(channel).bandwidth = case assert instr.ch(channel).bandwidth == case @pytest.mark.parametrize("case", IMPEDANCES) def test_output_impedance(self, instr, case): assert instr.ch1.impedance == 0.0 instr.ch1.impedance = case assert instr.ch1.impedance == case @pytest.mark.parametrize("relay", RELAYS) @pytest.mark.parametrize("case", BOOLEANS) def test_relay_closed(self, instr, relay, case): try: assert not instr.relay(relay).closed instr.relay(relay).closed = case assert instr.relay(relay).closed == case finally: self.reset_relays() @pytest.mark.parametrize("case", BOOLEANS) def test_both_channels_enabled(self, instr, case): assert not instr.ch1.enabled assert not instr.ch2.enabled instr.both_channels_enabled = case assert instr.ch1.enabled == case assert instr.ch2.enabled == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", SENSE_MODES) def test_sense_mode(self, instr, channel, case): assert instr.ch(channel).sense_mode == 'voltage' instr.ch(channel).sense_function = case assert instr.ch(channel).sense_function == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", NPLCS) def test_nplc(self, instr, channel, case): assert instr.ch(channel).nplc == 1 instr.ch(channel).nplc = case time.sleep(0.5) assert instr.ch(channel).nplc == case instr.ch(channel).nplc == 1 @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", AVERAGE_COUNTS) def test_average_count(self, instr, channel, case): assert instr.ch(channel).average_count == 1 instr.ch(channel).average_count = case assert instr.ch(channel).average_count == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", CURRENT_RANGES) def test_current_range(self, instr, channel, case): assert instr.ch(channel).current_range == 5 instr.ch(channel).current_range = case assert instr.ch(channel).current_range == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_current_range_auto(self, instr, channel, case): assert not instr.ch(channel).current_range_auto instr.ch(channel).current_range_auto = case assert instr.ch(channel).current_range_auto == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("pulse_current_measure_enabled", BOOLEANS) @pytest.mark.parametrize("case", PULSE_CURRENT_AVERAGE_COUNTS) def test_pulse_current_average_count(self, instr, channel, pulse_current_measure_enabled, case): instr.ch(channel).pulse_current_fast_enabled = True assert instr.ch(channel).pulse_current_average_count == 1 instr.ch(channel).pulse_current_measure_enabled = pulse_current_measure_enabled if not pulse_current_measure_enabled or case <= 100: instr.ch(channel).pulse_current_average_count = case assert instr.ch(channel).pulse_current_average_count == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_pulse_current_measure_enabled(self, instr, channel, case): instr.ch(channel).pulse_current_fast_enabled = True assert instr.ch(channel).pulse_current_measure_enabled instr.ch(channel).pulse_current_measure_enabled = case assert instr.ch(channel).pulse_current_measure_enabled == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", PULSE_CURRENT_MODES) def test_pulse_current_mode(self, instr, channel, case): instr.ch(channel).pulse_current_fast_enabled = True assert instr.ch(channel).pulse_current_mode == 'high' instr.ch(channel).pulse_current_mode = case assert instr.ch(channel).pulse_current_mode == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", PULSE_CURRENT_TIMES) def test_pulse_current_time_high(self, instr, channel, case): instr.ch(channel).pulse_current_fast_enabled = True assert instr.ch(channel).pulse_current_time_high == 33.33333e-06 instr.ch(channel).pulse_current_time_high = case assert instr.ch(channel).pulse_current_time_high == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", PULSE_CURRENT_TIMES) def test_pulse_current_time_low(self, instr, channel, case): instr.ch(channel).pulse_current_fast_enabled = True assert instr.ch(channel).pulse_current_time_low == 33.33333e-06 instr.ch(channel).pulse_current_time_low = case assert instr.ch(channel).pulse_current_time_low == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", PULSE_CURRENT_TIMES) def test_pulse_current_time_average(self, instr, channel, case): instr.ch(channel).pulse_current_fast_enabled = True assert instr.ch(channel).pulse_current_time_average == 33.33333e-06 instr.ch(channel).pulse_current_time_average = case assert instr.ch(channel).pulse_current_time_average == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", PULSE_CURRENT_TIMES) def test_pulse_current_time_digitize(self, instr, channel, case): instr.ch(channel).pulse_current_fast_enabled = True assert instr.ch(channel).pulse_current_time_digitize == 33.33333e-06 instr.ch(channel).pulse_current_time_digitize = case assert instr.ch(channel).pulse_current_time_digitize == case @pytest.mark.parametrize("case", BOOLEANS) def test_pulse_current_step_enabled(self, instr, case): instr.ch1.pulse_current_fast_enabled = True assert not instr.ch1.pulse_current_step_enabled instr.ch1.pulse_current_step_enabled = case assert instr.ch1.pulse_current_step_enabled == case @pytest.mark.parametrize("case", PULSE_CURRENT_STEP_COUNTS) def test_pulse_current_step_up_count(self, instr, case): instr.ch1.pulse_current_fast_enabled = True assert instr.ch1.pulse_current_step_up_count == 1 instr.ch1.pulse_current_step_up_count = case assert instr.ch1.pulse_current_step_up_count == case @pytest.mark.parametrize("case", PULSE_CURRENT_STEP_COUNTS) def test_pulse_current_step_down_count(self, instr, case): instr.ch1.pulse_current_fast_enabled = True assert instr.ch1.pulse_current_step_down_count == 1 instr.ch1.pulse_current_step_down_count = case assert instr.ch1.pulse_current_step_down_count == case @pytest.mark.parametrize("case", PULSE_CURRENT_STEP_TIMES) def test_pulse_current_step_time(self, instr, case): instr.ch1.pulse_current_fast_enabled = True assert instr.ch1.pulse_current_step_time == 200e-6 instr.ch1.pulse_current_step_time = case assert instr.ch1.pulse_current_step_time == case @pytest.mark.parametrize("case", PULSE_CURRENT_STEP_TIMEOUTS) def test_pulse_current_step_timeout(self, instr, case): instr.ch1.pulse_current_fast_enabled = True assert instr.ch1.pulse_current_step_timeout == 2e-3 instr.ch1.pulse_current_step_timeout = case assert instr.ch1.pulse_current_step_timeout == case @pytest.mark.parametrize("case", PULSE_CURRENT_STEP_TIMEOUT_INITIALS) def test_pulse_current_step_timeout_initial(self, instr, case): instr.ch1.pulse_current_fast_enabled = True assert instr.ch1.pulse_current_step_timeout_initial == 2 instr.ch1.pulse_current_step_timeout_initial = case assert instr.ch1.pulse_current_step_timeout_initial == case @pytest.mark.parametrize("case", PULSE_CURRENT_STEP_DELAYS) def test_pulse_current_step_delay(self, instr, case): instr.ch1.pulse_current_fast_enabled = True assert instr.ch1.pulse_current_step_delay == 0 instr.ch1.pulse_current_step_delay = case assert instr.ch1.pulse_current_step_delay == case @pytest.mark.parametrize("case", PULSE_CURRENT_STEP_RANGES) def test_pulse_current_step_range(self, instr, case): instr.ch1.pulse_current_fast_enabled = True assert instr.ch1.pulse_current_step_range == 5 instr.ch1.pulse_current_step_range = case assert instr.ch1.pulse_current_step_range == case @pytest.mark.parametrize("range", PULSE_CURRENT_STEP_RANGES) @pytest.mark.parametrize("step", PULSE_CURRENT_STEP_INDICES) @pytest.mark.parametrize("case", PULSE_CURRENT_STEP_TRIGGER_LEVELS) def test_pulse_current_step_trigger_level(self, instr, range, step, case): instr.ch1.pulse_current_fast_enabled = True assert instr.ch1.pulse_current_step(step).trigger_level == 0.0 if case <= range: instr.ch1.pulse_current_step(step).trigger_level = case assert instr.ch1.pulse_current_step(step).trigger_level == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("pulse_current_measure_enabled", BOOLEANS) @pytest.mark.parametrize("case", PULSE_CURRENT_TRIGGER_DELAYS) def test_pulse_current_trigger_delay(self, instr, channel, pulse_current_measure_enabled, case): instr.ch(channel).pulse_current_fast_enabled = True assert instr.ch(channel).pulse_current_trigger_delay == 0 instr.ch(channel).pulse_current_measure_enabled = pulse_current_measure_enabled if not pulse_current_measure_enabled or case <= 0.1: instr.ch(channel).pulse_current_trigger_delay = case assert instr.ch(channel).pulse_current_trigger_delay == case @pytest.mark.parametrize("case", PULSE_CURRENT_TRIGGER_LEVEL_RANGES) def test_pulse_current_trigger_level_range(self, instr, case): instr.ch1.pulse_current_fast_enabled = True assert instr.ch1.pulse_current_trigger_level_range == 5 instr.ch1.pulse_current_trigger_level_range = case assert instr.ch1.pulse_current_trigger_level_range == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", PULSE_CURRENT_TRIGGER_LEVELS) def test_pulse_current_trigger_level(self, instr, channel, case): instr.ch(channel).pulse_current_fast_enabled = True assert instr.ch(channel).pulse_current_trigger_level == 0 instr.ch(channel).pulse_current_trigger_level = case assert instr.ch(channel).pulse_current_trigger_level == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_pulse_current_fast_enabled(self, instr, channel, case): assert not instr.ch(channel).pulse_current_fast_enabled instr.ch(channel).pulse_current_fast_enabled = case assert instr.ch(channel).pulse_current_fast_enabled == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_pulse_current_search_enabled(self, instr, channel, case): instr.ch(channel).pulse_current_fast_enabled = True assert instr.ch(channel).pulse_current_search_enabled instr.ch(channel).pulse_current_search_enabled = case assert instr.ch(channel).pulse_current_search_enabled == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_pulse_current_detect_enabled(self, instr, channel, case): instr.ch(channel).pulse_current_fast_enabled = True assert not instr.ch(channel).pulse_current_detect_enabled instr.ch(channel).pulse_current_detect_enabled = case assert instr.ch(channel).pulse_current_detect_enabled == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", PULSE_CURRENT_TIMEOUTS) def test_pulse_current_timeouts(self, instr, channel, case): instr.ch(channel).pulse_current_fast_enabled = True assert instr.ch(channel).pulse_current_timeout == 1 instr.ch(channel).pulse_current_timeout = case assert instr.ch(channel).pulse_current_timeout == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", LONG_INTEGRATION_TRIGGER_EDGES) def test_long_integration_trigger_edge(self, instr, channel, case): instr.ch(channel).long_integration_fast_enabled = True assert instr.ch(channel).long_integration_trigger_edge == 'rising' instr.ch(channel).long_integration_trigger_edge = case assert instr.ch(channel).long_integration_trigger_edge == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", LONG_INTEGRATION_TIMES) def test_long_integration_time(self, instr, channel, case): instr.ch(channel).long_integration_fast_enabled = True assert instr.ch(channel).long_integration_time == 1.0 instr.ch(channel).long_integration_time = case assert instr.ch(channel).long_integration_time == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", LONG_INTEGRATION_TRIGGER_LEVELS) def test_long_integration_trigger_level(self, instr, channel, case): instr.ch(channel).long_integration_fast_enabled = True instr.ch(channel).long_integration_timeout = 1 assert instr.ch(channel).long_integration_trigger_level == 0 instr.ch(channel).long_integration_trigger_level = case assert instr.ch(channel).long_integration_trigger_level == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", LONG_INTEGRATION_TIMEOUTS) def test_long_integration_timeout(self, instr, channel, case): instr.ch(channel).long_integration_fast_enabled = True assert instr.ch(channel).long_integration_timeout == 16 instr.ch(channel).long_integration_timeout = case assert instr.ch(channel).long_integration_timeout == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_long_integration_fast_enabled(self, instr, channel, case): assert not instr.ch(channel).long_integration_fast_enabled instr.ch(channel).long_integration_fast_enabled = case assert instr.ch(channel).long_integration_fast_enabled == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_long_integration_search_enabled(self, instr, channel, case): instr.ch(channel).long_integration_fast_enabled = True assert instr.ch(channel).long_integration_search_enabled instr.ch(channel).long_integration_search_enabled = case assert instr.ch(channel).long_integration_search_enabled == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_long_integration_detect_enabled(self, instr, channel, case): instr.ch(channel).long_integration_fast_enabled = True assert not instr.ch(channel).long_integration_detect_enabled instr.ch(channel).long_integration_detect_enabled = case assert instr.ch(channel).long_integration_detect_enabled == case @pytest.mark.parametrize("case", LONG_INTEGRATION_TRIGGER_LEVEL_RANGES) def test_long_integration_trigger_level_range(self, instr, case): instr.ch1.long_integration_fast_enabled = True instr.ch1.long_integration_timeout = 1 assert instr.ch1.long_integration_trigger_level_range == 5 instr.ch1.long_integration_trigger_level_range = case assert instr.ch1.long_integration_trigger_level_range == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", SOURCE_VOLTAGES) def test_source_voltage(self, instr, channel, case): assert instr.ch(channel).source_voltage == 0 instr.ch(channel).source_voltage = case assert instr.ch(channel).source_voltage == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", SOURCE_VOLTAGE_PROTECTIONS) def test_source_voltage_protection(self, instr, channel, case): assert instr.ch(channel).source_voltage_protection == 8 instr.ch(channel).source_voltage_protection = case assert instr.ch(channel).source_voltage_protection == case @pytest.mark.parametrize("channel", CHANNELS) def test_source_voltage_protection_enabled(self, instr, channel): assert not instr.ch(channel).source_voltage_protection_enabled assert isinstance(instr.ch(channel).source_voltage_protection_enabled, bool) @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_source_voltage_protection_clamp_enabled(self, instr, channel, case): assert not instr.ch(channel).source_voltage_protection_clamp_enabled instr.ch(channel).source_voltage_protection_clamp_enabled = case assert instr.ch(channel).source_voltage_protection_clamp_enabled == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", SOURCE_CURRENTS) def test_source_current_limit(self, instr, channel, case): assert instr.ch(channel).source_current_limit == 0.25 instr.ch(channel).source_current_limit = case assert instr.ch(channel).source_current_limit == case @pytest.mark.parametrize("channel", CHANNELS) @pytest.mark.parametrize("case", SOURCE_CURRENT_LIMIT_TYPES) def test_source_current_limit_type(self, instr, channel, case): assert instr.ch(channel).source_current_limit_type == 'limit' instr.ch(channel).source_current_limit_type = case assert instr.ch(channel).source_current_limit_type == case @pytest.mark.parametrize("channel", CHANNELS) def test_source_current_limit_enabled(self, instr, channel): assert not instr.ch(channel).source_current_limit_enabled assert isinstance(instr.ch(channel).source_current_limit_enabled, bool) @pytest.mark.parametrize("sense_mode", SENSE_MODES) @pytest.mark.parametrize("average_count", AVERAGE_COUNTS) @pytest.mark.parametrize("channel", CHANNELS) def test_reading(self, instr, sense_mode, average_count, channel): instr.ch(channel).sense_mode = sense_mode instr.ch(channel).average_count = average_count time.sleep(0.1) assert type(instr.ch(channel).reading) == float @pytest.mark.parametrize("sense_mode", SENSE_MODES) @pytest.mark.parametrize("average_count", AVERAGE_COUNTS) @pytest.mark.parametrize("channel", CHANNELS) def test_readings(self, instr, sense_mode, average_count, channel): instr.ch(channel).sense_mode = sense_mode instr.ch(channel).average_count = average_count time.sleep(0.2) readings = instr.ch(channel).readings assert type(readings) == list assert len(readings) == average_count assert type(readings[0]) == float @pytest.mark.parametrize("average_count", AVERAGE_COUNTS) @pytest.mark.parametrize("channel", CHANNELS) def test_measured_voltage(self, instr, average_count, channel): instr.ch(channel).average_count = average_count time.sleep(0.1) assert type(instr.ch(channel).measured_voltage) == float assert instr.ch(channel).sense_mode == 'voltage' @pytest.mark.parametrize("average_count", AVERAGE_COUNTS) @pytest.mark.parametrize("channel", CHANNELS) def test_measured_voltages(self, instr, average_count, channel): instr.ch(channel).average_count = average_count time.sleep(0.2) measured_voltages = instr.ch(channel).measured_voltages assert type(measured_voltages) == list assert len(measured_voltages) == average_count assert type(measured_voltages[0]) == float assert instr.ch(channel).sense_mode == 'voltage' @pytest.mark.parametrize("average_count", AVERAGE_COUNTS) @pytest.mark.parametrize("channel", CHANNELS) def test_measured_current(self, instr, average_count, channel): instr.ch(channel).average_count = average_count time.sleep(0.1) assert type(instr.ch(channel).measured_current) == float assert instr.ch(channel).sense_mode == 'current' @pytest.mark.parametrize("average_count", AVERAGE_COUNTS) @pytest.mark.parametrize("channel", CHANNELS) def test_measured_currents(self, instr, average_count, channel): instr.ch(channel).average_count = average_count time.sleep(0.2) measured_currents = instr.ch(channel).measured_currents assert type(measured_currents) == list assert len(measured_currents) == average_count assert type(measured_currents[0]) == float assert instr.ch(channel).sense_mode == 'current' @pytest.mark.parametrize("average_count", AVERAGE_COUNTS) @pytest.mark.parametrize("channel", CHANNELS) def test_dvm_voltage(self, instr, average_count, channel): instr.ch(channel).average_count = average_count time.sleep(0.1) assert type(instr.ch(channel).dvm_voltage) == float assert instr.ch(channel).sense_mode == 'dvm' @pytest.mark.parametrize("average_count", AVERAGE_COUNTS) @pytest.mark.parametrize("channel", CHANNELS) def test_dvm_voltages(self, instr, average_count, channel): instr.ch(channel).average_count = average_count time.sleep(0.2) dvm_voltages = instr.ch(channel).dvm_voltages assert type(dvm_voltages) == list assert len(dvm_voltages) == average_count assert type(dvm_voltages[0]) == float assert instr.ch(channel).sense_mode == 'dvm' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keithley/test_keithley2400.py0000644000175100001770000000363514623331163024334 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.keithley import Keithley2400 def test_id(): with expected_protocol( Keithley2400, [("*IDN?", "KEITHLEY INSTRUMENTS INC., MODEL nnnn, xxxxxxx, yyyyy/zzzzz /a/d")], ) as inst: assert inst.id == "KEITHLEY INSTRUMENTS INC., MODEL nnnn, xxxxxxx, yyyyy/zzzzz /a/d" def test_next_error(): with expected_protocol(Keithley2400, [("SYST:ERR?", '-113, "Undefined header"')], ) as inst: assert inst.next_error == [-113, ' "Undefined header"'] def test_enable_source(): with expected_protocol(Keithley2400, [("OUTPUT ON", None)], ) as inst: inst.enable_source() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keithley/test_keithley2750.py0000644000175100001770000000363614623331163024345 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.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. It is a string and not a list "(@101)", # if only 1 channel is open. It is 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] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keithley/test_keithleyDMM6500.py0000644000175100001770000005523214623331163024677 0ustar00runnerdockerimport pytest from pymeasure.test import expected_protocol from pymeasure.instruments.keithley import KeithleyDMM6500 def test_init(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None)], ): pass # Verify the expected communication. def test_aperture_setter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:APER 0.0015', None)], ) as inst: inst.aperture = 0.0015 def test_aperture_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:APER?', b'0.0015\n')], ) as inst: assert inst.aperture == 0.0015 def test_autorange_enabled_setter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:RANG:AUTO 1', None)], ) as inst: inst.autorange_enabled = True @pytest.mark.parametrize("comm_pairs, value", ( ([(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:RANG:AUTO?', b'0\n')], False), ([(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:RANG:AUTO?', b'1\n')], True), )) def test_autorange_enabled_getter(comm_pairs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: assert inst.autorange_enabled == value def test_autozero_enabled_setter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:AZER 0', None)], ) as inst: inst.autozero_enabled = False @pytest.mark.parametrize("comm_pairs, value", ( ([(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:AZER?', b'1\n')], True), ([(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:AZER?', b'0\n')], False), )) def test_autozero_enabled_getter(comm_pairs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: assert inst.autozero_enabled == value def test_command_set_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b'*LANG?', b'SCPI\n')], ) as inst: assert inst.command_set == 'SCPI' @pytest.mark.parametrize("comm_pairs, value", ( ([(b'*LANG SCPI', None), (b':READ?', b'-4.999995E-01\n')], -0.4999995), ([(b'*LANG SCPI', None), (b':READ?', b'3.414127E-04\n')], 0.0003414127), )) def test_current_getter(comm_pairs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: assert inst.current == value def test_current_ac_bandwidth_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:CURR:AC:DET:BAND?', b'30\n')], ) as inst: assert inst.current_ac_bandwidth == 30.0 def test_current_ac_digits_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':DISP:CURR:AC:DIG?', b'6\n')], ) as inst: assert inst.current_ac_digits == 6 def test_current_ac_range_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:CURR:AC:RANG?', b'0.01\n')], ) as inst: assert inst.current_ac_range == 0.01 def test_current_ac_relative_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:CURR:AC:REL?', b'0\n')], ) as inst: assert inst.current_ac_relative == 0.0 def test_current_ac_relative_enabled_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:CURR:AC:REL:STAT?', b'0\n')], ) as inst: assert inst.current_ac_relative_enabled is False def test_current_digits_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':DISP:CURR:DIG?', b'3\n')], ) as inst: assert inst.current_digits == 3 def test_current_nplc_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:CURR:NPLC?', b'2\n')], ) as inst: assert inst.current_nplc == 2.0 def test_current_range_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:CURR:RANG?', b'0.01\n')], ) as inst: assert inst.current_range == 0.01 def test_current_relative_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:CURR:REL?', b'0.5\n')], ) as inst: assert inst.current_relative == 0.5 def test_current_relative_enabled_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:CURR:REL:STAT?', b'1\n')], ) as inst: assert inst.current_relative_enabled is True def test_detector_bandwidth_setter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:AC\n'), (b'CURR:AC:DET:BAND 30', None)], ) as inst: inst.detector_bandwidth = 30 def test_detector_bandwidth_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:AC\n'), (b'CURR:AC:DET:BAND?', b'30\n')], ) as inst: assert inst.detector_bandwidth == 30.0 def test_digits_setter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b':DISP:CURR:DC:DIG 3', None)], ) as inst: inst.digits = 3 def test_digits_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b':DISP:CURR:DC:DIG?', b'3\n')], ) as inst: assert inst.digits == 3 def test_frequency_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':READ?', b'0.000000E+00\n')], ) as inst: assert inst.frequency == 0.0 def test_frequency_aperature_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FREQ:APER?', b'0.2\n')], ) as inst: assert inst.frequency_aperature == 0.2 def test_frequency_digits_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':DISP:FREQ:DIG?', b'6\n')], ) as inst: assert inst.frequency_digits == 6 def test_frequency_relative_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FREQ:REL?', b'0\n')], ) as inst: assert inst.frequency_relative == 0.0 def test_frequency_relative_enabled_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FREQ:REL:STAT?', b'0\n')], ) as inst: assert inst.frequency_relative_enabled is False def test_frequency_threshold_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FREQ:THR:RANG?', b'10\n')], ) as inst: assert inst.frequency_threshold == 10.0 def test_frequency_threshold_auto_enabled_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FREQ:THR:RANG:AUTO?', b'1\n')], ) as inst: assert inst.frequency_threshold_auto_enabled is True def test_id_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b'*IDN?', b'KEITHLEY INSTRUMENTS,MODEL DMM6500,04592448,1.7.12b\n')], ) as inst: assert inst.id == 'KEITHLEY INSTRUMENTS,MODEL DMM6500,04592448,1.7.12b' def test_line_frequency_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SYST:LFR?', b'60\n')], ) as inst: assert inst.line_frequency == 60.0 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'*LANG SCPI', None), (b':SENS:FUNC "CURR:DC"', None)], 'current'), ([(b'*LANG SCPI', None), (b':SENS:FUNC "CURR:AC"', None)], 'current ac'), ([(b'*LANG SCPI', None), (b':SENS:FUNC "VOLT:DC"', None)], 'voltage'), )) def test_mode_setter(comm_pairs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: inst.mode = value @pytest.mark.parametrize("comm_pairs, value", ( ([(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n')], 'current'), ([(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:AC\n')], 'current ac'), ([(b'*LANG SCPI', None), (b':SENS:FUNC?', b'VOLT:AC\n')], 'voltage ac'), )) def test_mode_getter(comm_pairs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: assert inst.mode == value def test_nplc_setter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:NPLC 2', None)], ) as inst: inst.nplc = 2 def test_nplc_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:NPLC?', b'2\n')], ) as inst: assert inst.nplc == 2.0 def test_period_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':READ?', b'0.000000E+00\n')], ) as inst: assert inst.period == 0.0 def test_period_aperature_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:PER:APER?', b'0.2\n')], ) as inst: assert inst.period_aperature == 0.2 def test_period_digits_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':DISP:PER:DIG?', b'6\n')], ) as inst: assert inst.period_digits == 6 def test_period_relative_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:PER:REL?', b'0\n')], ) as inst: assert inst.period_relative == 0.0 def test_period_relative_enabled_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:PER:REL:STAT?', b'0\n')], ) as inst: assert inst.period_relative_enabled is False def test_period_threshold_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:PER:THR:RANG?', b'0.1\n')], ) as inst: assert inst.period_threshold == 0.1 def test_period_threshold_auto_enabled_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:PER:THR:RANG:AUTO?', b'1\n')], ) as inst: assert inst.period_threshold_auto_enabled is True def test_range_setter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:RANG:AUTO 0;UPP 1.0', None)], ) as inst: inst.range = 1.0 def test_range_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:RANG?', b'1\n')], ) as inst: assert inst.range == 1.0 def test_relative_setter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:REL 0.5', None)], ) as inst: inst.relative = 0.5 def test_relative_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:REL?', b'0.5\n')], ) as inst: assert inst.relative == 0.5 def test_relative_enabled_setter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:REL:STAT 1', None)], ) as inst: inst.relative_enabled = True @pytest.mark.parametrize("comm_pairs, value", ( ([(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:REL:STAT?', b'0\n')], False), ([(b'*LANG SCPI', None), (b':SENS:FUNC?', b'CURR:DC\n'), (b'CURR:DC:REL:STAT?', b'1\n')], True), )) def test_relative_enabled_getter(comm_pairs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: assert inst.relative_enabled == value @pytest.mark.parametrize("comm_pairs, value", ( ([(b'*LANG SCPI', None), (b':READ?', b'9.900000E+37\n')], 9.9e+37), ([(b'*LANG SCPI', None), (b':READ?', b'9.900000E+37\n')], 9.9e+37), )) def test_resistance_getter(comm_pairs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: assert inst.resistance == value def test_resistance_4W_digits_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':DISP:FRES:DIG?', b'6\n')], ) as inst: assert inst.resistance_4W_digits == 6 def test_resistance_4W_nplc_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FRES:NPLC?', b'1\n')], ) as inst: assert inst.resistance_4W_nplc == 1.0 def test_resistance_4W_range_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FRES:RANG?', b'1E+07\n')], ) as inst: assert inst.resistance_4W_range == 10000000.0 def test_resistance_4W_relative_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FRES:REL?', b'0\n')], ) as inst: assert inst.resistance_4W_relative == 0.0 def test_resistance_4W_relative_enabled_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FRES:REL:STAT?', b'0\n')], ) as inst: assert inst.resistance_4W_relative_enabled is False def test_resistance_digits_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':DISP:RES:DIG?', b'6\n')], ) as inst: assert inst.resistance_digits == 6 def test_resistance_nplc_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:RES:NPLC?', b'1\n')], ) as inst: assert inst.resistance_nplc == 1.0 def test_resistance_range_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:RES:RANG?', b'1E+07\n')], ) as inst: assert inst.resistance_range == 10000000.0 def test_resistance_relative_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:RES:REL?', b'0\n')], ) as inst: assert inst.resistance_relative == 0.0 def test_resistance_relative_enabled_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:RES:REL:STAT?', b'0\n')], ) as inst: assert inst.resistance_relative_enabled is False def test_system_time_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SYST:TIME? 1', b'Thu Sep 21 09:06:41 2023\n')], ) as inst: assert inst.system_time == 'Thu Sep 21 09:06:41 2023' def test_terminals_used_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b'ROUT:TERM?', b'REAR\n')], ) as inst: assert inst.terminals_used == 'REAR' @pytest.mark.parametrize("comm_pairs, value", ( ([(b'*LANG SCPI', None), (b':READ?', b'1.200272E-03\n')], 0.001200272), ([(b'*LANG SCPI', None), (b':READ?', b'5.943143E-04\n')], 0.0005943143), )) def test_voltage_getter(comm_pairs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: assert inst.voltage == value def test_voltage_ac_bandwidth_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:VOLT:AC:DET:BAND?', b'3\n')], ) as inst: assert inst.voltage_ac_bandwidth == 3.0 def test_voltage_ac_digits_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':DISP:VOLT:AC:DIG?', b'6\n')], ) as inst: assert inst.voltage_ac_digits == 6 def test_voltage_ac_range_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:VOLT:AC:RANG?', b'1\n')], ) as inst: assert inst.voltage_ac_range == 1.0 def test_voltage_ac_relative_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:VOLT:AC:REL?', b'0\n')], ) as inst: assert inst.voltage_ac_relative == 0.0 def test_voltage_digits_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':DISP:VOLT:DIG?', b'6\n')], ) as inst: assert inst.voltage_digits == 6 def test_voltage_nplc_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:VOLT:NPLC?', b'1\n')], ) as inst: assert inst.voltage_nplc == 1.0 def test_voltage_range_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:VOLT:RANG?', b'1\n')], ) as inst: assert inst.voltage_range == 1.0 def test_voltage_relative_getter(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:VOLT:REL?', b'0\n')], ) as inst: assert inst.voltage_relative == 0.0 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'*LANG SCPI', None), (b':SENS:VOLT:REL:STAT?', b'0\n')], False), ([(b'*LANG SCPI', None), (b':SENS:VOLT:REL:STAT?', b'0\n')], False), )) def test_voltage_relative_enabled_getter(comm_pairs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: assert inst.voltage_relative_enabled == value def test_check_errors(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b'SYST:ERR?', b'0,"No error;0;0 0"\n')], ) as inst: assert inst.check_errors() == [] def test_clear(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b'*CLS', None)], ) as inst: assert inst.clear() is None def test_displayed_text(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':DISP:CLE', None), (b':DISP:SCR SWIPE_USER', None), (b':DISP:USER1:TEXT "Hello"', None), (b':DISP:USER2:TEXT "DMM6500"', None)], ) as inst: assert inst.displayed_text(*('Hello', 'DMM6500'), ) is None @pytest.mark.parametrize("comm_pairs, args, kwargs, value", ( ([(b'*LANG SCPI', None), (b':SENS:FUNC "CURR:DC"', None), (b':SENS:CURR:RANG:AUTO 0;:SENS:CURR:RANG 0.01', None)], (), {}, None), ([(b'*LANG SCPI', None), (b':SENS:FUNC "CURR:AC"', None), (b':SENS:CURR:AC:RANG:AUTO 0;:SENS:CURR:AC:RANG 0.01', None)], (), {'ac': True}, None), )) def test_measure_current(comm_pairs, args, kwargs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: assert inst.measure_current(*args, **kwargs) == value def test_measure_frequency(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC "FREQ:VOLT"', None)], ) as inst: assert inst.measure_frequency() is None def test_measure_period(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b':SENS:FUNC "PER:VOLT"', None)], ) as inst: assert inst.measure_period() is None @pytest.mark.parametrize("comm_pairs, args, kwargs, value", ( ([(b'*LANG SCPI', None), (b':SENS:FUNC "RES"', None), (b':SENS:RES:RANG:AUTO 0;:SENS:RES:RANG 1e+07', None)], (), {}, None), ([(b'*LANG SCPI', None), (b':SENS:FUNC "FRES"', None), (b':SENS:FRES:RANG:AUTO 0;:SENS:FRES:RANG 1e+07', None)], (), {'wires': 4}, None), )) def test_measure_resistance(comm_pairs, args, kwargs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: assert inst.measure_resistance(*args, **kwargs) == value @pytest.mark.parametrize("comm_pairs, args, kwargs, value", ( ([(b'*LANG SCPI', None), (b':SENS:FUNC "VOLT:DC"', None), (b':SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:RANG 1', None)], (), {}, None), ([(b'*LANG SCPI', None), (b':SENS:FUNC "VOLT:AC"', None), (b':SENS:VOLT:RANG:AUTO 0;:SENS:VOLT:AC:RANG 1', None)], (), {'ac': True}, None), )) def test_measure_voltage(comm_pairs, args, kwargs, value): with expected_protocol( KeithleyDMM6500, comm_pairs, ) as inst: assert inst.measure_voltage(*args, **kwargs) == value def test_reset(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b'*RST', None)], ) as inst: assert inst.reset() is None def test_trigger_single_autozero(): with expected_protocol( KeithleyDMM6500, [(b'*LANG SCPI', None), (b'AZER:ONCE', None)], ) as inst: assert inst.trigger_single_autozero() is None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keithley/test_keithleyDMM6500_with_device.py0000644000175100001770000002312514623331163027245 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 logging from pymeasure.instruments.keithley import KeithleyDMM6500 log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) FUNCTIONS = [ "voltage", "voltage ac", "current", "current ac", "resistance", "resistance 4W", "diode", "capacitance", "temperature", "continuity", "period", "frequency", "voltage ratio", ] FUNCTION_METHODS = { "voltage": "measure_voltage", "voltage ac": "measure_voltage", "current": "measure_current", "current ac": "measure_current", "resistance": "measure_resistance", "resistance 4W": "measure_resistance", "diode": "measure_diode", "capacitance": "measure_capacitance", "temperature": "measure_temperature", "period": "measure_period", "frequency": "measure_frequency", } FUNCTIONS_WITH_RANGE = ( "current", "current ac", "voltage", "voltage ac", "resistance", "resistance 4W", "capacitance", ) FUNCTIONS_HAVE_AUTORANGE = ( "current", "current ac", "voltage", "voltage ac", "resistance", "resistance 4W", "capacitance", ) FUNCTIONS_HAVE_NPLC = ( "voltage", "current", "resistance", "resistance 4W", "diode", "temperature", "voltage ratio", ) FUNCTIONS_HAVE_SAME_APERTURE = FUNCTIONS_HAVE_NPLC FUNCTIONS_HAVE_AUTOZERO = FUNCTIONS_HAVE_NPLC SCREEN_DISPLAY_SELS = ( "HOME", "HOME_LARG", "READ", "HIST", "SWIPE_FUNC", "SWIPE_GRAP", "SWIPE_SEC", "SWIPE_SETT", "SWIPE_STAT", "SWIPE_USER", "SWIPE_CHAN", "SWIPE_NONS", "SWIPE_SCAN", "CHANNEL_CONT", "CHANNEL_SETT", "CHANNEL_SCAN", "PROC", ) @pytest.fixture(scope="module") def dmm6500(connected_device_address): instr = KeithleyDMM6500(connected_device_address) instr.adapter.connection.timeout = 10000 return instr @pytest.fixture def resetted_dmm6500(dmm6500): dmm6500.clear() dmm6500.reset() return dmm6500 def test_correct_model_by_idn(resetted_dmm6500): assert "6500" in resetted_dmm6500.id.lower() @pytest.mark.parametrize("function_", FUNCTIONS) def test_given_function_when_set_then_function_is_set(resetted_dmm6500, function_): resetted_dmm6500.mode = function_ assert len(resetted_dmm6500.check_errors()) == 0 assert resetted_dmm6500.mode == function_ @pytest.mark.parametrize("key", FUNCTION_METHODS) def test_given_function_by_measure_xxx(resetted_dmm6500, key): measure_xxx = getattr(resetted_dmm6500, FUNCTION_METHODS[key]) if key[-2:] == "ac": measure_xxx(ac=True) elif key[-2:] == "4W": measure_xxx(wires=4) else: measure_xxx() assert len(resetted_dmm6500.check_errors()) == 0 assert resetted_dmm6500.mode == key @pytest.mark.parametrize("key", FUNCTION_METHODS) def test_given_function_is_set_then_reading_avaliable(resetted_dmm6500, key): if key[-2:] == "ac": getattr(resetted_dmm6500, FUNCTION_METHODS[key])(ac=True) elif key[-2:] == "4W": getattr(resetted_dmm6500, FUNCTION_METHODS[key])(wires=4) else: getattr(resetted_dmm6500, FUNCTION_METHODS[key])() value = getattr(resetted_dmm6500, key.split(" ")[0]) assert len(resetted_dmm6500.check_errors()) == 0 assert value is not None @pytest.mark.parametrize("function_", FUNCTIONS_WITH_RANGE) def test_given_function_is_set_then_range_available(resetted_dmm6500, function_): resetted_dmm6500.mode = function_ assert len(resetted_dmm6500.check_errors()) == 0 new = function_ + " range" new = new.replace(" ", "_") range_ = getattr(resetted_dmm6500, new) assert range_ is not None @pytest.mark.parametrize("key", FUNCTION_METHODS) def test_given_function_set_then_autorange_enabled(resetted_dmm6500, key): if key[-2:] == "ac": getattr(resetted_dmm6500, FUNCTION_METHODS[key])(ac=True) elif key[-2:] == "4W": getattr(resetted_dmm6500, FUNCTION_METHODS[key])(wires=4) else: getattr(resetted_dmm6500, FUNCTION_METHODS[key])() # resetted_dmm6500.mode = function_ assert len(resetted_dmm6500.check_errors()) == 0 assert resetted_dmm6500.auto_range_status() is False resetted_dmm6500.auto_range() if key in FUNCTIONS_HAVE_AUTORANGE: assert resetted_dmm6500.auto_range_status() is True else: assert resetted_dmm6500.auto_range_status() is False @pytest.mark.parametrize("function_", FUNCTIONS_HAVE_AUTORANGE) def test_given_function_set_then_autorange(resetted_dmm6500, function_): resetted_dmm6500.mode = function_ assert len(resetted_dmm6500.check_errors()) == 0 assert resetted_dmm6500.autorange_enabled resetted_dmm6500.autorange_enabled = False assert not resetted_dmm6500.autorange_enabled assert len(resetted_dmm6500.check_errors()) == 0 def test_dcv_range_min_def_max(resetted_dmm6500): resetted_dmm6500.measure_voltage() assert len(resetted_dmm6500.check_errors()) == 0 resetted_dmm6500.range = "MIN" assert len(resetted_dmm6500.check_errors()) == 0 assert resetted_dmm6500.range == 0.1 resetted_dmm6500.range = "MAX" assert len(resetted_dmm6500.check_errors()) == 0 assert resetted_dmm6500.range == 1000 resetted_dmm6500.range = "DEF" assert len(resetted_dmm6500.check_errors()) == 0 assert resetted_dmm6500.range == 1000 @pytest.mark.parametrize("function_", FUNCTIONS_HAVE_NPLC) @pytest.mark.parametrize("nplc", [0.0005, 2, 15]) def test_nplc(resetted_dmm6500, function_, nplc): resetted_dmm6500.mode = function_ resetted_dmm6500.nplc = nplc assert len(resetted_dmm6500.check_errors()) == 0 assert resetted_dmm6500.nplc == nplc @pytest.mark.parametrize("function_", FUNCTIONS_HAVE_SAME_APERTURE) @pytest.mark.parametrize("aperture", ["MIN", "MAX", "DEF", 16.0e-3]) def test_general_aperture(resetted_dmm6500, function_, aperture): resetted_dmm6500.mode = function_ resetted_dmm6500.aperture = aperture assert len(resetted_dmm6500.check_errors()) == 0 expected_value = {"MIN": 8.33e-6, "MAX": 0.25, "DEF": 0.0166667, 16.0e-3: 16.0e-3} assert resetted_dmm6500.aperture == expected_value[aperture] @pytest.mark.parametrize("function_", ("frequency", "period")) @pytest.mark.parametrize("aperture", ["MIN", "MAX", "DEF", 16.0e-3]) def test_freq_period_aperture(resetted_dmm6500, function_, aperture): resetted_dmm6500.mode = function_ resetted_dmm6500.aperture = aperture assert len(resetted_dmm6500.check_errors()) == 0 expected_value = {"MIN": 2e-3, "MAX": 273e-3, "DEF": 200e-3, 16.0e-3: 16.0e-3} assert resetted_dmm6500.aperture == expected_value[aperture] @pytest.mark.parametrize("function_", ("voltage ac", "current ac")) @pytest.mark.parametrize("detector_bandwidth", [3, 30, 300]) def test_detector_bandwidth(resetted_dmm6500, function_, detector_bandwidth): resetted_dmm6500.mode = function_ resetted_dmm6500.detector_bandwidth = detector_bandwidth assert len(resetted_dmm6500.check_errors()) == 0 assert resetted_dmm6500.detector_bandwidth == detector_bandwidth @pytest.mark.parametrize("function_", FUNCTIONS_HAVE_AUTOZERO) @pytest.mark.parametrize("enable", [True, False]) def test_autozero(resetted_dmm6500, function_, enable): resetted_dmm6500.mode = function_ resetted_dmm6500.autozero_enabled = enable assert len(resetted_dmm6500.check_errors()) == 0 assert resetted_dmm6500.autozero_enabled == enable def test_single_autozero(resetted_dmm6500): resetted_dmm6500.autozero = True resetted_dmm6500.trigger_single_autozero() assert len(resetted_dmm6500.check_errors()) == 0 assert not resetted_dmm6500.autozero_enabled def test_terminals_used(resetted_dmm6500): assert resetted_dmm6500.terminals_used in ["FRONT", "REAR"] assert len(resetted_dmm6500.check_errors()) == 0 @pytest.mark.parametrize("selection", SCREEN_DISPLAY_SELS) def test_display_screen(resetted_dmm6500, selection): resetted_dmm6500.screen_display = selection assert len(resetted_dmm6500.check_errors()) == 0 def test_displayed_text(resetted_dmm6500): resetted_dmm6500.displayed_text("Hello World", "Running test...") assert len(resetted_dmm6500.check_errors()) == 0 resetted_dmm6500.displayed_text() assert len(resetted_dmm6500.check_errors()) == 0 resetted_dmm6500.displayed_text("", "") assert len(resetted_dmm6500.check_errors()) == 0 def test_beep(resetted_dmm6500): # Ambulance siren for i in range(4): resetted_dmm6500.beep(700, 0.5) resetted_dmm6500.beep(950, 0.5) assert len(resetted_dmm6500.check_errors()) == 0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.437606 pymeasure-0.14.0/tests/instruments/kepco/0000755000175100001770000000000014623331176020057 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/kepco/test_kepcobop.py0000644000175100001770000000661414623331163023275 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.kepco import KepcoBOP3612 def test_init(): with expected_protocol( KepcoBOP3612, [], ): pass # Verify the expected communication. def test_bop_test_getter(): with expected_protocol( KepcoBOP3612, [(b'DIAG:TST?', b'0')], ) as inst: assert inst.bop_test == 0 def test_confidence_test_getter(): with expected_protocol( KepcoBOP3612, [(b'*TST?', b'0')], ) as inst: assert inst.confidence_test == 0 def test_current_getter(): with expected_protocol( KepcoBOP3612, [(b'MEASure:CURRent?', b'4.99E-3')], ) as inst: assert inst.current == 0.00499 def test_current_setpoint_setter(): with expected_protocol( KepcoBOP3612, [(b'CURRent 0.1', None)], ) as inst: inst.current_setpoint = 0.1 def test_current_setpoint_getter(): with expected_protocol( KepcoBOP3612, [(b'CURRent?', b'9.989E-2')], ) as inst: assert inst.current_setpoint == 0.09989 def test_id_getter(): with expected_protocol( KepcoBOP3612, [(b'*IDN?', b'KEPCO,BIT 4886 36-12 08-04-2023,H249977,4.04-1.82')], ) as inst: assert inst.id == 'KEPCO,BIT 4886 36-12 08-04-2023,H249977,4.04-1.82' def test_output_enabled_getter(): with expected_protocol( KepcoBOP3612, [(b'OUTPut?', b'0')], ) as inst: assert inst.output_enabled is False def test_voltage_getter(): with expected_protocol( KepcoBOP3612, [(b'MEASure:VOLTage?', b'8.0E-3')], ) as inst: assert inst.voltage == 0.008 def test_voltage_setpoint_setter(): with expected_protocol( KepcoBOP3612, [(b'VOLTage 0.1', None)], ) as inst: inst.voltage_setpoint = 0.1 def test_voltage_setpoint_getter(): with expected_protocol( KepcoBOP3612, [(b'VOLTage?', b'1.000E-1')], ) as inst: assert inst.voltage_setpoint == 0.1 def test_beep(): with expected_protocol( KepcoBOP3612, [(b'SYSTem:BEEP', None)], ) as inst: assert inst.beep() is None ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.437606 pymeasure-0.14.0/tests/instruments/keysight/0000755000175100001770000000000014623331176020605 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keysight/test_keysightDSOX1102G_with_device.py0000644000175100001770000003337614623331163027540 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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)): 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 # noqa 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): 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keysight/test_keysightE36312A.py0000644000175100001770000000466114623331163024655 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.keysight.keysightE36312A import KeysightE36312A def test_voltage_setpoint(): """Verify the voltage setpoint setter and getter.""" with expected_protocol( KeysightE36312A, [("VOLT 1.5, (@1)", None), ("VOLT? (@1)", "1.5")], ) as inst: inst.ch_1.voltage_setpoint = 1.5 assert inst.ch_1.voltage_setpoint == 1.5 def test_current_limit(): """Verify the current limit setter and getter.""" with expected_protocol( KeysightE36312A, [("CURR 0.5, (@3)", None), ("CURR? (@3)", "0.5")], ) as inst: inst.ch_3.current_limit = 0.5 assert inst.ch_3.current_limit == 0.5 def test_current_limit_validator(): """Verify the current limit validator.""" with expected_protocol( KeysightE36312A, [], ) as inst: with pytest.raises(ValueError, match="not in range"): inst.ch_1.current_limit = 7 def test_output_enabled(): """Verify the output enable setter and getter.""" with expected_protocol( KeysightE36312A, [("OUTPut 1, (@1)", None), ("OUTPut? (@1)", "0")], ) as inst: inst.ch_1.output_enabled = True assert inst.ch_1.output_enabled is False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keysight/test_keysightE3631A.py0000644000175100001770000000466414623331163024576 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.keysight.keysightE3631A import KeysightE3631A def test_voltage_setpoint(): """Verify the voltage setpoint setter and getter.""" with expected_protocol( KeysightE3631A, [("INST:NSEL 1;:VOLT 1.5", None), ("INST:NSEL 1;:VOLT?", "1.5")], ) as inst: inst.ch_1.voltage_setpoint = 1.5 assert inst.ch_1.voltage_setpoint == 1.5 def test_current_limit(): """Verify the current limit setter and getter.""" with expected_protocol( KeysightE3631A, [("INST:NSEL 3;:CURR 0.5", None), ("INST:NSEL 3;:CURR?", "0.5")], ) as inst: inst.ch_3.current_limit = 0.5 assert inst.ch_3.current_limit == 0.5 def test_current_limit_validator(): """Verify the current limit validator.""" with expected_protocol( KeysightE3631A, [], ) as inst: with pytest.raises(ValueError, match="not in range"): inst.ch_1.current_limit = 7 def test_output_enabled(): """Verify the output enable setter and getter.""" with expected_protocol( KeysightE3631A, [("OUTPut 1", None), ("OUTPut?", "0")], ) as inst: inst.output_enabled = True assert inst.output_enabled is False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/keysight/test_keysightE3631A_with_device.py0000644000175100001770000000752614623331163027150 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.keysight.keysightE3631A import KeysightE3631A @pytest.fixture(scope="module") def power_supply(connected_device_address): instr = KeysightE3631A(connected_device_address) instr.reset() return instr class TestKeysightE3631A: """ Unit tests for KeysightE3631A class. This test suite, needs the following setup to work properly: - A KeysightE3631A device should be connected to the computer; - The device's address must be set in the RESOURCE constant; """ ######################### # PARAMETRIZATION CASES # ######################### BOOLEANS = [False, True] CHANNELS = [1, 2, 3] ######### # TESTS # ######### @pytest.mark.parametrize("case", BOOLEANS) def test_output_enabled(self, power_supply, case): assert not power_supply.output_enabled power_supply.output_enabled = case assert power_supply.output_enabled == case @pytest.mark.parametrize("case", BOOLEANS) def test_tracking_enabled(self, power_supply, case): assert not power_supply.tracking_enabled power_supply.tracking_enabled = case assert power_supply.tracking_enabled == case @pytest.mark.parametrize("chn, i_limit", [(1, 0), (1, 5), (2, 0), (2, 1), (3, 0), (3, 1)],) def test_current_limit(self, power_supply, chn, i_limit): power_supply.channels[chn].current_limit = i_limit @pytest.mark.parametrize("chn, i_limit", [(1, -1), (1, 6), (2, -1), (2, 2), (3, -1), (3, 2)],) def test_current_limit_out_of_range(self, power_supply, chn, i_limit): with pytest.raises(ValueError, match=f"Value of {i_limit} is not in range"): power_supply.channels[chn].current_limit = i_limit @pytest.mark.parametrize("chn, voltage", [(1, 0), (1, 6), (2, 0), (2, 25), (3, 0), (3, -25)],) def test_voltage_setpoint(self, power_supply, chn, voltage): power_supply.channels[chn].voltage_setpoint = voltage assert power_supply.channels[chn].voltage_setpoint == voltage @pytest.mark.parametrize("chn, voltage", [(1, -1), (1, 7), (2, -1), (2, 26), (3, 1), (3, -26)],) def test_voltage_setpoint_out_of_range(self, power_supply, chn, voltage): with pytest.raises(ValueError, match=f"Value of {voltage} is not in range"): power_supply.channels[chn].voltage_setpoint = voltage @pytest.mark.parametrize("chn", CHANNELS) def test_measure_voltage(self, power_supply, chn): assert isinstance(power_supply.channels[chn].voltage, float) @pytest.mark.parametrize("chn", CHANNELS) def test_measure_current(self, power_supply, chn): assert isinstance(power_supply.channels[chn].current, float) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716367998.437606 pymeasure-0.14.0/tests/instruments/kuhneelectronic/0000755000175100001770000000000014623331176022140 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/kuhneelectronic/test_kusg245_250a.py0000644000175100001770000002332714623331163025507 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.kuhneelectronic import Kusg245_250A from pymeasure.instruments.kuhneelectronic.kusg245_250a import termination_character, encoding termination_character = termination_character.encode(encoding=encoding)[0] def test_voltage_5v(): with expected_protocol( Kusg245_250A, [("5", bytes([0, 1, termination_character]))], ) as inst: assert inst.voltage_5v == 103.0 / 4700.0 def test_voltage_32v(): with expected_protocol( Kusg245_250A, [("8", bytes([0, 1, termination_character]))], ) as inst: assert inst.voltage_32v == 1282.0 / 8200.0 def test_power_forward(): with expected_protocol( Kusg245_250A, [("6", bytes([255, termination_character]))], ) as inst: assert inst.power_forward == 255 def test_power_reverse(): with expected_protocol( Kusg245_250A, [("7", bytes([255, termination_character]))], ) as inst: assert inst.power_reverse == 255 def test_external_enabled(): with expected_protocol( Kusg245_250A, [ ("R", "A"), ("r?", bytes([1, termination_character])), ("r", "A"), ("r?", bytes([0, termination_character])) ], ) as inst: inst.external_enabled = True assert inst.external_enabled is True inst.external_enabled = False assert inst.external_enabled is False def test_bias_enabled(): with expected_protocol( Kusg245_250A, [ ("X", "A"), ("x?", bytes([1, termination_character])), ("x", "A"), ("x?", bytes([0, termination_character])) ], ) as inst: inst.bias_enabled = True assert inst.bias_enabled is True inst.bias_enabled = False assert inst.bias_enabled is False def test_rf_enabled(): with expected_protocol( Kusg245_250A, [ ("O", "A"), ("o?", bytes([1, termination_character])), ("o", "A"), ("o?", bytes([0, termination_character])) ], ) as inst: inst.rf_enabled = True assert inst.rf_enabled is True inst.rf_enabled = False assert inst.rf_enabled is False def test_pulse_mode_enabled(): with expected_protocol( Kusg245_250A, [ ("P", "A"), ("p?", bytes([1, termination_character])), ("p", "A"), ("p?", bytes([0, termination_character])) ], ) as inst: inst.pulse_mode_enabled = True assert inst.pulse_mode_enabled is True inst.pulse_mode_enabled = False assert inst.pulse_mode_enabled is False def test_freq_steps_fine_enabled(): with expected_protocol( Kusg245_250A, [ ("fm1", "A"), ("fm?", bytes([1, termination_character])), ("fm0", "A"), ("fm?", bytes([0, termination_character])) ], ) as inst: inst.freq_steps_fine_enabled = True assert inst.freq_steps_fine_enabled is True inst.freq_steps_fine_enabled = False assert inst.freq_steps_fine_enabled is False def test_frequency_coarse(): with expected_protocol( Kusg245_250A, [ ("f2456", "A"), ("f?", "2456MHz"), ("f2400", "A"), ("f?", "2400MHz"), ("f2500", "A"), ("f?", "2500MHz"), ], ) as inst: inst.frequency_coarse = 2456 assert inst.frequency_coarse == 2456 inst.frequency_coarse = 2000 # must be truncated to 2400 assert inst.frequency_coarse == 2400 inst.frequency_coarse = 3000 # must be truncated to 2500 assert inst.frequency_coarse == 2500 def test_frequency_fine(): with expected_protocol( Kusg245_250A, [ ("f2456780", "A"), ("f?", "2456780kHz"), ("f2400000", "A"), ("f?", "2400000kHz"), ("f2500000", "A"), ("f?", "2500000kHz"), ], ) as inst: inst.frequency_fine = 2456778 # must be rounded to 2456780 assert inst.frequency_fine == 2456780 inst.frequency_fine = 2000000 # must be truncated to 2400000 assert inst.frequency_fine == 2400000 inst.frequency_fine = 3000000 # must be truncated to 2500000 assert inst.frequency_fine == 2500000 def test_power_setpoint(): with expected_protocol( Kusg245_250A, [ ("A123", "A"), ("A?", "123"), ("A000", "A"), ("A?", "000"), ("A250", "A"), ("A?", "250"), ], ) as inst: inst.power_setpoint = 123 assert inst.power_setpoint == 123 inst.power_setpoint = -1 # must be truncated to 0 assert inst.power_setpoint == 0 inst.power_setpoint = 300 # must be truncated to 250 assert inst.power_setpoint == 250 def test_power_setpoint_limited(): with expected_protocol( Kusg245_250A, [("A000", "A"), ("A?", "000"), ("A020", "A"), ("A?", "020")], power_limit=20 ) as inst: inst.power_setpoint = -1 # must be truncated to 0 assert inst.power_setpoint == 0 inst.power_setpoint = 300 # must be truncated to power_limit assert inst.power_setpoint == 20 def test_pulse_width(): with expected_protocol( Kusg245_250A, [ ("C0125", "A"), ("C?", "0125"), ("C0010", "A"), ("C?", "0010"), ("C1000", "A"), ("C?", "1000"), ], ) as inst: inst.pulse_width = 123 # must be rounded to 125 assert inst.pulse_width == 125 inst.pulse_width = 0 # must be truncated to 10 assert inst.pulse_width == 10 inst.pulse_width = 10000 # must be truncated to 250 assert inst.pulse_width == 1000 def test_off_time(): with expected_protocol( Kusg245_250A, [ ("c0125", "A"), ("c?", "0125"), ("c0010", "A"), ("c?", "0010"), ("c1000", "A"), ("c?", "1000"), ], ) as inst: inst.off_time = 123 # must be rounded to 125 assert inst.off_time == 125 inst.off_time = 0 # must be truncated to 10 assert inst.off_time == 10 inst.off_time = 10000 # must be truncated to 250 assert inst.off_time == 1000 def test_phase_shift(): with expected_protocol( Kusg245_250A, [ ("H088", "A"), ("H?", bytes([88, termination_character])), ("H000", "A"), ("H?", bytes([0, termination_character])), ("H255", "A"), ("H?", bytes([255, termination_character])), ], ) as inst: inst.phase_shift = 124 assert inst.phase_shift == pytest.approx(124, 0.01) inst.phase_shift = -1 # must be truncated to 0 assert inst.phase_shift == 0 inst.phase_shift = 358.6 assert inst.phase_shift == pytest.approx(358.6, 0.01) def test_reflection_limit(): with expected_protocol( Kusg245_250A, [ ("B0", "A"), ("B?", bytes([0, termination_character])), ("B4", "A"), ("B?", bytes([4, termination_character])), ("B5", "A"), ("B?", bytes([5, termination_character])) ], ) as inst: inst.reflection_limit = 0 assert inst.reflection_limit == 0 inst.reflection_limit = 182 # must be rounded to the next # higher discrete value (200) assert inst.reflection_limit == 200 inst.reflection_limit = 300 # must be truncated to 230 assert inst.reflection_limit == 230 def test_tune(): with expected_protocol( Kusg245_250A, [("b010", "A")], ) as inst: inst.tune(10) def test_tune_power_limited(): with expected_protocol(Kusg245_250A, [("b020", "A")], power_limit=20) as inst: inst.tune(100) def test_clear_VSWR_error(): with expected_protocol( Kusg245_250A, [("z", "A")], ) as inst: inst.clear_VSWR_error() def test_store_settings(): with expected_protocol( Kusg245_250A, [("SE", "A")], ) as inst: inst.store_settings() def test_turn_off(): with expected_protocol( Kusg245_250A, [("o", "A"), ("x", "A")], ) as inst: inst.turn_off() def test_turn_on(): with expected_protocol( Kusg245_250A, [("X", "A"), ("O", "A")], ) as inst: inst.turn_on() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4416063 pymeasure-0.14.0/tests/instruments/lakeshore/0000755000175100001770000000000014623331176020733 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/lakeshore/test_lakeshore211.py0000644000175100001770000000637714623331163024556 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.lakeshore.lakeshore211 import LakeShore211 def test_init(): with expected_protocol( LakeShore211, [], ): pass # Verify the expected communication. def test_temp_kelvin(): with expected_protocol( LakeShore211, [(b"KRDG?", b"27.1")], ) as instr: assert instr.temperature_kelvin == 27.1 def test_temp_celsius(): with expected_protocol( LakeShore211, [(b"CRDG?", b"27.1")], ) as instr: assert instr.temperature_celsius == 27.1 def test_set_analog(): with expected_protocol( LakeShore211, [(b"ANALOG 0,1", None), (b"ANALOG?", b"0,1")], ) as instr: instr.analog_configuration = (0, 1) assert instr.analog_configuration == (0, 1) def test_set_display(): with expected_protocol( LakeShore211, [(b"DISPFLD 1", None), (b"DISPFLD?", b"1")], ) as instr: instr.display_units = 'celsius' assert instr.display_units == 'celsius' def test_alarms(): with expected_protocol( LakeShore211, [(b"ALARM?", b"0,+000.0,+000.0,+00.0,0\r\n"), (b"ALARM 1,300,10,5,0", None), (b"ALARM?", b"1,+300.0,+010.0,+05.0,0\r\n") ], ) as instr: assert instr.get_alarm_status() == {'on': 0, 'high_value': 0.0, 'low_value': 0.0, 'deadband': 0.0, 'latch': 0} instr.configure_alarm(on=True, high_value=300, low_value=10, deadband=5, latch=False) assert instr.get_alarm_status() == {'on': 1, 'high_value': 300.0, 'low_value': 10.0, 'deadband': 5.0, 'latch': 0} def test_relays(): with expected_protocol( LakeShore211, [(b"RELAY? 1", b"0\r\n"), (b"RELAY 1 2", None), (b"RELAY? 1", b"2\r\n") ], ) as instr: assert instr.get_relay_mode(1) == 0 instr.configure_relay(1, 2) assert instr.get_relay_mode(1) == 2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/lakeshore/test_lakeshore421.py0000644000175100001770000000356414623331163024554 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.lakeshore import LakeShore421 def test_init(): with expected_protocol( LakeShore421, [], ): pass # Verify the expected communication. def test_unit(): with expected_protocol( LakeShore421, [(b"UNIT?", b"G")], ) as instr: assert instr.unit == "G" def test_unit_setter(): with expected_protocol( LakeShore421, [(b"UNIT G", None)], ) as instr: instr.unit = "G" def test_max_hold_reset(): with expected_protocol( LakeShore421, [(b"MAXC", None)], ) as instr: instr.max_hold_reset() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/lakeshore/test_lakeshore425.py0000644000175100001770000000400714623331163024551 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.lakeshore import LakeShore425 def test_init(): with expected_protocol( LakeShore425, []): pass # Verify the expected communication. def test_unit(): # from manual with expected_protocol( LakeShore425, [(b"UNIT?", b"1")]) as instr: assert instr.unit == "G" def test_unit_setter(): # from manual with expected_protocol( LakeShore425, [(b"UNIT 2", None)]) as instr: instr.unit = "T" def test_field(): # from manual with expected_protocol( LakeShore425, [(b"RDGFIELD?", b"+123.456E-01")] ) as instr: assert instr.field == 123.456e-1 def test_zero_probe(): # from manual with expected_protocol( LakeShore425, [(b"ZPROBE", None)]) as instr: instr.zero_probe() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4416063 pymeasure-0.14.0/tests/instruments/lecroy/0000755000175100001770000000000014623331176020253 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/lecroy/test_lecroyT3DSO1204.py0000644000175100001770000003534114623331163024247 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.teledyne.teledyne_oscilloscope import sanitize_source from pymeasure.instruments.lecroy.lecroyT3DSO1204 import LeCroyT3DSO1204 from pymeasure.test import expected_protocol INVALID_CHANNELS = ["INVALID_SOURCE", "C1 C2", "C1 MATH", "C1234567", "CHANNEL"] VALID_CHANNELS = [('C1', 'C1'), ('CHANNEL2', 'C2'), ('ch 3', 'C3'), ('chan 4', 'C4'), ('\tC3\t', 'C3'), (' math ', 'MATH')] def test_init(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None)] ): pass # Verify the expected communication. @pytest.mark.parametrize("channel", INVALID_CHANNELS) def test_invalid_source(channel): with pytest.raises(ValueError): sanitize_source(channel) @pytest.mark.parametrize("channel", VALID_CHANNELS) def test_sanitize_valid_source(channel): assert sanitize_source(channel[0]) == channel[1] def test_bwlimit(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"BWL C1,OFF", None), (b"C1:BWL?", b"OFF"), (b"BWL C1,ON", None), (b"C1:BWL?", b"ON") ] ) as instr: instr.ch_1.bwlimit = False assert instr.ch_1.bwlimit is False instr.ch_1.bwlimit = True assert instr.ch_1.bwlimit is True def test_coupling(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"C1:CPL D1M", None), (b"C1:CPL?", b"D1M"), (b"C1:CPL A1M", None), (b"C1:CPL?", b"A1M"), (b"C1:CPL GND", None), (b"C1:CPL?", b"GND") ], ) as instr: instr.ch_1.coupling = "dc 1M" assert instr.ch_1.coupling == "dc 1M" instr.ch_1.coupling = "ac 1M" assert instr.ch_1.coupling == "ac 1M" instr.ch_1.coupling = "ground" assert instr.ch_1.coupling == "ground" def test_offset(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"C1:OFST 1.00E+00V", None), (b"C1:OFST?", b"1.00E+00") ] ) as instr: instr.ch_1.offset = 1. assert instr.ch_1.offset == 1. def test_attenuation(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"C1:ATTN 100", None), (b"C1:ATTN?", b"100"), (b"C1:ATTN 0.1", None), (b"C1:ATTN?", b"0.1") ] ) as instr: instr.ch_1.probe_attenuation = 100 assert instr.ch_1.probe_attenuation == 100 instr.ch_1.probe_attenuation = 0.1 assert instr.ch_1.probe_attenuation == 0.1 def test_skew_factor(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"C1:SKEW 1.00E-07S", None), (b"C1:SKEW?", b"1.00E-07S"), ] ) as instr: instr.ch_1.skew_factor = 1e-7 assert instr.ch_1.skew_factor == 1e-7 def test_channel_setup(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"C1:ATTN?", b"1"), (b"C1:BWL?", b"OFF"), (b"C1:CPL?", b"D1M"), (b"C1:OFST?", b"-1.50E-01"), (b"C1:SKEW?", b"0.00E+00S"), (b"C1:TRA?", b"ON"), (b"C1:UNIT?", b"V"), (b"C1:VDIV?", b"5.00E-02"), (b"C1:INVS?", b"OFF"), (b"C1:TRCP?", b"DC"), (b"C1:TRLV?", b"1.50E-01"), (b"C1:TRLV2?", b"1.50E-01"), (b"C1:TRSL?", b"POS"), ] ) as instr: assert instr.ch_1.current_configuration == {"channel": 1, "attenuation": 1., "bandwidth_limit": False, "coupling": "dc 1M", "offset": -0.150, "skew_factor": 0., "display": True, "unit": "V", "volts_div": 0.05, "inverted": False, "trigger_coupling": "dc", "trigger_level": 0.150, "trigger_level2": 0.150, "trigger_slope": "positive" } def test_memory_size(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"MSIZ 14M", None), (b"MSIZ?", b"14M"), (b"MSIZ 1.4M", None), (b"MSIZ?", b"1.4M"), (b"MSIZ 7K", None), (b"MSIZ?", b"7K") ] ) as instr: instr.memory_size = 14e6 assert instr.memory_size == 14e6 instr.memory_size = 14e5 assert instr.memory_size == 14e5 instr.memory_size = 7e3 assert instr.memory_size == 7e3 def test_sample_size(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"SANU? C1", b"3.50E+06"), (b"SANU? C1", b"3.50E+06"), (b"SANU? C3", b"3.50E+06"), (b"SANU? C3", b"3.50E+06"), ] ) as instr: assert instr.acquisition_sample_size_c1 == 3.5e6 assert instr.acquisition_sample_size_c2 == 3.5e6 assert instr.acquisition_sample_size_c3 == 3.5e6 assert instr.acquisition_sample_size_c4 == 3.5e6 def test_waveform_preamble(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"WFSU?", b"SP,1,NP,0,FP,0"), (b"ACQW?", b"SAMPLING"), (b"SARA?", b"1.00E+09"), (b"SAST?", b"Trig'd"), (b"MSIZ?", b"7M"), (b"TDIV?", b"5.00E-04"), (b"TRDL?", b"-0.00E+00"), (b"SANU? C1", b"1.75E+06"), (b"C1:VDIV?", b"5.00E-02"), (b"C1:OFST?", b"-1.50E-01"), (b"C1:UNIT?", b"V"), ] ) as instr: assert instr.waveform_preamble == { "sparsing": 1, "requested_points": 0, "transmitted_points": None, "sampled_points": 1.75e6, "first_point": 0, "memory_size": 7e6, "source": "C1", "type": "normal", "average": None, "sampling_rate": 1e9, "grid_number": 14, "status": "triggered", "xdiv": 5e-4, "xoffset": 0, "ydiv": 0.05, "yoffset": -0.150, "unit": "V" } def test_download_one_point(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"WFSU SP,1", None), (b"WFSU NP,1", None), (b"WFSU FP,0", None), (b"SANU? C1", b"7.00E+06"), (b"WFSU NP,1", None), (b"WFSU FP,0", None), (b"C1:WF? DAT2", b"DAT2,#9000000001" + b"\x01" + b"\n\n"), (b"WFSU?", b"SP,1,NP,2,FP,0"), (b"ACQW?", b"SAMPLING"), (b"SARA?", b"1.00E+09"), (b"SAST?", b"Stop"), (b"MSIZ?", b"7M"), (b"TDIV?", b"5.00E-04"), (b"TRDL?", b"-0.00E+00"), (b"SANU? C1", b"7.00E+06"), (b"C1:VDIV?", b"5.00E-02"), (b"C1:OFST?", b"-1.50E-01"), (b"C1:UNIT?", b"V") ], connection_attributes={'chunk_size': 0}, ) as instr: y, x, preamble = instr.download_waveform(source="c1", requested_points=1, sparsing=1) assert preamble == { "sparsing": 1, "requested_points": 1, "memory_size": 7e6, "sampled_points": 7e6, "transmitted_points": 1, "first_point": 0, "source": "C1", "type": "normal", "average": None, "sampling_rate": 1e9, "grid_number": 14, "status": "stopped", "xdiv": 5e-4, "xoffset": 0, "ydiv": 0.05, "yoffset": -0.150, "unit": "V" } assert len(x) == 1 assert len(y) == 1 assert y[0] == 1 * 0.05 / 25. + 0.150 def test_download_two_points(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"WFSU SP,1", None), (b"WFSU NP,2", None), (b"WFSU FP,0", None), (b"SANU? C1", b"7.00E+06"), (b"WFSU NP,2", None), (b"WFSU FP,0", None), (b"C1:WF? DAT2", b"DAT2,#9000000002" + b"\x01\x01" + b"\n\n"), (b"WFSU?", b"SP,1,NP,2,FP,0"), (b"ACQW?", b"SAMPLING"), (b"SARA?", b"1.00E+09"), (b"SAST?", b"Stop"), (b"MSIZ?", b"7M"), (b"TDIV?", b"5.00E-04"), (b"TRDL?", b"-0.00E+00"), (b"SANU? C1", b"7.00E+06"), (b"C1:VDIV?", b"5.00E-02"), (b"C1:OFST?", b"-1.50E-01"), (b"C1:UNIT?", b"V") ], connection_attributes={'chunk_size': 0}, ) as instr: y, x, preamble = instr.download_waveform(source="c1", requested_points=2, sparsing=1) assert preamble == { "sparsing": 1, "requested_points": 2, "memory_size": 7e6, "sampled_points": 7e6, "transmitted_points": 2, "first_point": 0, "source": "C1", "type": "normal", "average": None, "sampling_rate": 1e9, "grid_number": 14, "status": "stopped", "xdiv": 5e-4, "xoffset": 0, "ydiv": 0.05, "yoffset": -0.150, "unit": "V" } assert len(x) == 2 assert len(y) == 2 assert x[0] == -5e-4 * 14 / 2. assert y[0] == 1 * 0.05 / 25. + 0.150 assert x[1] == x[0] + 1 / 1e9 assert y[1] == y[0] def test_trigger(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"TRSE?", b"EDGE,SR,C1,HT,OFF"), (b"TRMD?", b"AUTO"), (b"C1:TRCP?", b"DC"), (b"C1:TRLV?", b"1.50E-01"), (b"C1:TRLV2?", b"1.50E-01"), (b"C1:TRSL?", b"POS"), ] ) as instr: assert instr.trigger == { "mode": "auto", "trigger_type": "edge", "source": "c1", "hold_type": "off", "hold_value1": None, "hold_value2": None, "coupling": "dc", "level": 0.150, "level2": 0.150, "slope": "positive", } def test_math_define(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"DEF EQN,'C2*C4'", None), (b"DEF?", b"EQN,'C2*C4'"), ] ) as instr: instr.math_define = ("channel2", "*", "channel4") assert instr.math_define == ["EQN", "'C2*C4'"] def test_math_vdiv(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"MTVD 1.00E+00V", None), (b"MTVD?", b"1.00E+00"), ] ) as instr: instr.math_vdiv = 1.0 assert instr.math_vdiv == 1.0 def test_math_vpos(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"MTVP 120", None), (b"MTVP?", b"120"), ] ) as instr: instr.math_vpos = 120 assert instr.math_vpos == 120 def test_display_parameter(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"PACU PKPK,C1", None), (b"PACU MEAN,C2", None) ] ) as instr: instr.display_parameter(parameter="PKPK", channel=1) instr.ch(2).display_parameter = "MEAN" def test_measure_parameter(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"C2:PAVA? RISE", b"RISE,3.600000E-9"), (b"C3:PAVA? MEAN", b"MEAN,3.600000E-9"), ] ) as instr: assert instr.measure_parameter("RISE", "channel2") == 3.6e-9 assert instr.ch_3.measure_parameter("MEAN") == 3.6e-9 def test_menu(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"MENU ON", None), (b"MENU?", b"ON"), (b"MENU OFF", None), (b"MENU?", b"OFF"), ] ) as instr: instr.menu = True assert instr.menu is True instr.menu = False assert instr.menu is False def test_grid_display(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"GRDS FULL", None), (b"GRDS?", b"FULL"), ] ) as instr: instr.grid_display = "full" assert instr.grid_display == "full" def test_intensity(): with expected_protocol( LeCroyT3DSO1204, [(b"CHDR OFF", None), (b"INTS GRID,50,TRACE,100", None), (b"INTS?", b"GRID,50,TRACE,100"), ] ) as instr: instr.intensity = 50, 100 assert instr.intensity == {"GRID": 50, "TRACE": 100} if __name__ == '__main__': pytest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/lecroy/test_lecroyT3DSO1204_with_device.py0000644000175100001770000004730614623331163026625 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 unittest.mock import ANY import numpy as np import pytest from pyvisa.errors import VisaIOError from pymeasure.instruments.lecroy.lecroyT3DSO1204 import LeCroyT3DSO1204 class TestLeCroyT3DSO1204: """ Unit tests for LeCroyT3DSO1204 class. This test suite, needs the following setup to work properly: - A LeCroyT3DSO1204 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. """ ######################### # PARAMETRIZATION CASES # ######################### BOOLEANS = [False, True] CHANNEL_COUPLINGS = ["ac 1M", "dc 1M", "ground"] ACQUISITION_TYPES = ["normal", "average", "peak", "highres"] TRIGGER_LEVELS = [0.125, 0.150, 0.175] TRIGGER_SLOPES = ["negative", "positive", "window"] ACQUISITION_AVERAGE = [4, 16, 32, 64, 128, 256] WAVEFORM_POINTS = [100, 1000, 10000] WAVEFORM_SOURCES = ["C1", "C2", "C3", "C4"] CHANNELS = [1, 2, 3, 4] ############ # FIXTURES # ############ @pytest.fixture(scope="module") def instrument(self, connected_device_address): return LeCroyT3DSO1204(connected_device_address) @pytest.fixture def resetted_instrument(self, instrument): instrument.reset() sleep(7) return instrument @pytest.fixture def autoscaled_instrument(self, instrument): instrument.reset() sleep(7) instrument.autoscale() sleep(7) return instrument ######### # TESTS # ######### # noinspection PyTypeChecker def test_instrument_connection(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)): LeCroyT3DSO1204(bad_resource) # Channel def test_ch_current_configuration(self, autoscaled_instrument): autoscaled_instrument.ch_1.offset = 0 autoscaled_instrument.ch_1.trigger_level = 0 autoscaled_instrument.ch_1.trigger_level2 = 0 expected = { "channel": 1, "attenuation": 1.0, "bandwidth_limit": False, "coupling": "dc 1M", "offset": 0.0, "skew_factor": 0.0, "display": True, "unit": "V", "volts_div": 0.05, "inverted": False, "trigger_coupling": "dc", "trigger_level": 0.0, "trigger_level2": 0.0, "trigger_slope": "positive", } actual = autoscaled_instrument.ch(1).current_configuration assert actual == expected @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_ch_bwlimit(self, instrument, ch_number, case): instrument.ch(ch_number).bwlimit = case assert instrument.ch(ch_number).bwlimit == case @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case", CHANNEL_COUPLINGS) def test_ch_coupling(self, instrument, ch_number, case): instrument.ch(ch_number).coupling = case assert instrument.ch(ch_number).coupling == case @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_ch_display(self, instrument, ch_number, case): instrument.ch(ch_number).display = case assert instrument.ch(ch_number).display == case @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_ch_invert(self, instrument, ch_number, case): instrument.ch(ch_number).invert = case assert instrument.ch(ch_number).invert == case @pytest.mark.parametrize("ch_number", CHANNELS) def test_ch_offset(self, instrument, ch_number): instrument.ch(ch_number).offset = 1 assert instrument.ch(ch_number).offset == 1 @pytest.mark.parametrize("ch_number", CHANNELS) def test_ch_probe_attenuation(self, instrument, ch_number): instrument.ch(ch_number).probe_attenuation = 10 assert instrument.ch(ch_number).probe_attenuation == 10 @pytest.mark.parametrize("ch_number", CHANNELS) def test_ch_scale(self, instrument, ch_number): instrument.ch(ch_number).scale = 1 assert instrument.ch(ch_number).scale == 1 def test_ch_trigger_level(self, autoscaled_instrument): for case in self.TRIGGER_LEVELS: autoscaled_instrument.ch_1.trigger_level = case assert autoscaled_instrument.ch_1.trigger_level == case def test_ch_trigger_level2(self, autoscaled_instrument): for case in self.TRIGGER_LEVELS: autoscaled_instrument.ch_1.trigger_level2 = case assert autoscaled_instrument.ch_1.trigger_level2 == case def test_ch_trigger_slope(self, autoscaled_instrument): with pytest.raises(ValueError): autoscaled_instrument.ch_1.trigger_slope = "abcd" autoscaled_instrument.trigger_select = ("edge", "c1", "off") for case in self.TRIGGER_SLOPES: autoscaled_instrument.ch_1.trigger_slope = case assert autoscaled_instrument.ch_1.trigger_slope == case # Timebase def test_timebase(self, autoscaled_instrument): autoscaled_instrument.timebase_hor_magnify = 5e-6 autoscaled_instrument.timebase_hor_position = 0 expected = { "timebase_scale": 5e-4, "timebase_offset": 0.0, "timebase_hor_magnify": 5e-6, "timebase_hor_position": 0.0, } actual = autoscaled_instrument.timebase for key, val in actual.items(): assert pytest.approx(val, 0.1) == expected[key] def test_timebase_scale(self, resetted_instrument): resetted_instrument.timebase_scale = 1e-3 assert resetted_instrument.timebase_scale == 1e-3 def test_timebase_offset(self, instrument): instrument.timebase_offset = 1e-3 assert instrument.timebase_offset == 1e-3 def test_timebase_hor_magnify(self, instrument): instrument.timebase_hor_magnify = 1e-4 assert instrument.timebase_hor_magnify == 1e-4 def test_timebase_hor_position(self, instrument): instrument.timebase_hor_position = 5e-4 assert pytest.approx(instrument.timebase_hor_position, 0.1) == 5e-4 # Acquisition @pytest.mark.parametrize("case", ACQUISITION_TYPES) def test_acquisition_type(self, resetted_instrument, case): if case == "average": resetted_instrument.acquisition_type = case resetted_instrument.acquisition_average = 16 assert resetted_instrument.acquisition_type == ["average", 16] else: resetted_instrument.acquisition_type = case assert resetted_instrument.acquisition_type == case @pytest.mark.parametrize("case", ACQUISITION_AVERAGE) def test_acquisition_average(self, instrument, case): instrument.acquisition_average = case assert instrument.acquisition_average == case def test_acquisition_status(self, autoscaled_instrument): assert autoscaled_instrument.acquisition_status == "triggered" autoscaled_instrument.stop() assert autoscaled_instrument.acquisition_status == "stopped" def test_acquisition_sampling_rate(self, resetted_instrument): assert resetted_instrument.acquisition_sampling_rate == 1e9 @pytest.mark.parametrize("case", WAVEFORM_POINTS) def test_waveform_points(self, instrument, case): instrument.waveform_points = case assert instrument.waveform_points == case def test_waveform_preamble(self, autoscaled_instrument): autoscaled_instrument.acquisition_type = "normal" autoscaled_instrument.ch_1.offset = 0 autoscaled_instrument.waveform_points = 0 autoscaled_instrument.waveform_first_point = 0 autoscaled_instrument.waveform_sparsing = 1 autoscaled_instrument.waveform_source = "C1" expected_preamble = { "sparsing": 1.0, "requested_points": 0.0, "memory_size": 14e6, "sampled_points": 7e6, "transmitted_points": None, "first_point": 0.0, "source": autoscaled_instrument.waveform_source, "type": "normal", "average": None, "sampling_rate": 1e9, "grid_number": 14, "status": ANY, "xdiv": 5e-4, "xoffset": -0.0, "ydiv": 0.05, "yoffset": 0.0, "unit": "V", } preamble = autoscaled_instrument.waveform_preamble assert preamble == expected_preamble # Setup methods @pytest.mark.parametrize("ch_number", CHANNELS) def test_channel_setup(self, instrument, ch_number): # Only autoscale on the first channel instrument = instrument if ch_number == self.CHANNELS[0]: instrument.reset() sleep(7) instrument.autoscale() sleep(7) # Not testing the actual values assignment since different combinations of # parameters can play off each other. expected = instrument.ch(ch_number).current_configuration instrument.ch(ch_number).setup() assert instrument.ch(ch_number).current_configuration == expected with pytest.raises(AttributeError): instrument.ch(5) instrument.ch(ch_number).setup( bwlimit=False, coupling="dc 1M", display=True, invert=False, offset=0.0, skew_factor=0.0, probe_attenuation=1.0, scale=0.05, unit="V", trigger_coupling="dc", trigger_level=0.150, trigger_level2=0.150, trigger_slope="positive", ) expected = { "channel": ch_number, "attenuation": 1.0, "bandwidth_limit": False, "coupling": "dc 1M", "offset": 0.0, "skew_factor": 0.0, "display": True, "unit": "V", "volts_div": 0.05, "inverted": False, "trigger_coupling": "dc", "trigger_level": 0.150, "trigger_level2": 0.150, "trigger_slope": "positive", } actual = instrument.ch(ch_number).current_configuration assert actual == expected def test_timebase_setup(self, resetted_instrument): expected = resetted_instrument.timebase resetted_instrument.timebase_setup() assert resetted_instrument.timebase == expected # Download methods def test_download_image_default_arguments(self, autoscaled_instrument): img = autoscaled_instrument.download_image() assert type(img) is bytearray assert pytest.approx(len(img), 0.1) == 768067 def test_download_data_missing_argument(self, resetted_instrument): with pytest.raises(TypeError): # noinspection PyArgumentList resetted_instrument.download_waveform() @pytest.mark.parametrize("case1", WAVEFORM_SOURCES) @pytest.mark.parametrize("case2", WAVEFORM_POINTS) def test_download_data(self, instrument, case1, case2): if case1 == self.WAVEFORM_SOURCES[0] and case2 == self.WAVEFORM_POINTS[0]: instrument.reset() sleep(7) instrument.autoscale() sleep(7) instrument.ch(case1).display = True instrument.single() sleep(1) data, time, preamble = instrument.download_waveform( source=case1, requested_points=case2, sparsing=0 ) assert type(data) is np.ndarray assert len(data) == case2 assert type(time) is np.ndarray assert len(time) == case2 assert type(preamble) is dict def test_download_single_point(self, instrument): instrument.acquisition_type = "normal" instrument.ch_1.display = True instrument.single() sleep(1) data, time, preamble = instrument.download_waveform(source="c1", requested_points=1) assert type(data) is np.ndarray assert len(data) == 1 assert type(time) is np.ndarray assert len(time) == 1 assert type(preamble) is dict assert preamble == {'average': None, 'first_point': 0, 'grid_number': 14, 'memory_size': 7000000.0, 'requested_points': 1, 'sampled_points': 3500000.0, 'sampling_rate': 500000000.0, 'source': 'C1', 'sparsing': 1.0, 'status': 'stopped', 'transmitted_points': 1, 'type': 'normal', 'unit': 'V', 'xdiv': 0.0005, 'xoffset': -0.0, 'ydiv': ANY, 'yoffset': ANY} @pytest.mark.skip(reason="A human is needed to check the output waveform") def test_download_data_all_points(self, instrument): from matplotlib import pyplot as plt instrument.ch_1.display = True instrument.single() sleep(3) data, time, preamble = instrument.download_waveform(source="c1", requested_points=0) assert type(data) is np.ndarray assert type(time) is np.ndarray assert type(preamble) is dict print(preamble) plt.scatter(x=time, y=data) plt.show() @pytest.mark.skip(reason="A human is needed to check the output waveform") def test_download_data_sparsing(self, instrument): from matplotlib import pyplot as plt instrument.ch_1.display = True instrument.single() sleep(1) data, time, preamble = instrument.download_waveform( source="c1", requested_points=7e5, sparsing=10 ) assert type(data) is np.ndarray assert len(data) == 7e5 or len(data) == 7e4 assert type(time) is np.ndarray assert len(time) == 7e5 or len(time) == 7e4 assert type(preamble) is dict assert preamble["type"] == "normal" assert preamble["sparsing"] == 10 assert preamble["transmitted_points"] == 7e5 or preamble["transmitted_points"] == 7e4 print(preamble) plt.scatter(x=time, y=data) plt.show() @pytest.mark.skip(reason="A human is needed to check the output waveform") def test_download_data_averaging_16(self, instrument): from matplotlib import pyplot as plt instrument.ch_1.display = True instrument.run() instrument.acquisition_type = "average" instrument.acquisition_average = 16 instrument.single() sleep(1) data, time, preamble = instrument.download_waveform( source="c1", requested_points=1.75e5, sparsing=10 ) assert type(data) is np.ndarray assert len(data) == 1.75e5 or len(data) == 7e4 assert type(time) is np.ndarray assert len(time) == 1.75e5 or len(time) == 7e4 assert type(preamble) is dict assert preamble["type"] == ["average", 16] assert preamble["average"] == 16 assert preamble["transmitted_points"] == 1.75e5 or preamble["transmitted_points"] == 7e4 print(preamble) plt.scatter(x=time, y=data) plt.show() @pytest.mark.skip(reason="A human is needed to check the output waveform") def test_download_data_averaging_256(self, instrument): from matplotlib import pyplot as plt instrument.ch_1.display = True instrument.run() instrument.acquisition_type = "average" instrument.acquisition_average = 256 instrument.single() sleep(1) data, time, preamble = instrument.download_waveform( source="c1", requested_points=1.75e5, sparsing=10 ) assert type(data) is np.ndarray assert len(data) == 1.75e5 or len(data) == 7e4 assert type(time) is np.ndarray assert len(time) == 1.75e5 or len(time) == 7e4 assert type(preamble) is dict assert preamble["type"] == ["average", 256] assert preamble["average"] == 256 assert preamble["transmitted_points"] == 1.75e5 or preamble["transmitted_points"] == 7e4 print(preamble) plt.scatter(x=time, y=data) plt.show() @pytest.mark.skip(reason="A human is needed to check the output waveform") def test_download_math(self, instrument): """ Be careful because there is no way to turn on and off the MATH function programmatically, so the user should push on the MATH button and make sure that the (white) math line is displayed before running this test. """ from matplotlib import pyplot as plt instrument.single() sleep(1) data, time, preamble = instrument.download_waveform( source="math", requested_points=0, sparsing=10 ) assert type(data) is np.ndarray assert type(time) is np.ndarray assert type(preamble) is dict print(preamble) plt.scatter(x=time, y=data) plt.show() # Trigger def test_trigger_select(self, resetted_instrument): with pytest.raises(ValueError): resetted_instrument.trigger_select = "edge" with pytest.raises(ValueError): resetted_instrument.trigger_select = ("edge", "c2") with pytest.raises(ValueError): resetted_instrument.trigger_select = ("edge", "c2", "time") with pytest.raises(ValueError): resetted_instrument.trigger_select = ("ABCD", "c1", "time", 0) with pytest.raises(ValueError): resetted_instrument.trigger_select = ("edge", "c1", "time", 1000) with pytest.raises(ValueError): resetted_instrument.trigger_select = ("edge", "c1", "time", 0, 1) resetted_instrument.trigger_select = ("edge", "c1", "off") resetted_instrument.trigger_select = ("EDGE", "C1", "OFF") assert resetted_instrument.trigger_select == ["edge", "c1", "off"] resetted_instrument.trigger_select = ("glit", "c1", "p2", 1e-3, 2e-3) assert resetted_instrument.trigger_select == ["glit", "c1", "p2", 1e-3, 2e-3] def test_trigger_setup(self, resetted_instrument): expected = resetted_instrument.trigger resetted_instrument.trigger_setup(**expected) assert resetted_instrument.trigger == expected if __name__ == "__main__": pytest.main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4416063 pymeasure-0.14.0/tests/instruments/mksinst/0000755000175100001770000000000014623331176020446 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/mksinst/test_mks937b.py0000644000175100001770000000704614623331163023261 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.mksinst.mks937b import MKS937B, Unit def test_pressure(): """Verify the communication of the pressure getter.""" with expected_protocol( MKS937B, [("@253PR1?", "@253ACK1.10e-9"), (None, b"FF")], ) as inst: assert inst.ch_1.pressure == pytest.approx(1.1e-9) def test_ion_gauge_status(): """Verify the communication of the ion gauge status getter.""" with expected_protocol( MKS937B, [("@253T1?", "@253ACKG"), (None, b"FF")], ) as inst: assert inst.ch_1.ion_gauge_status == "Good" def test_ion_gauge_status_invalid_channel(): """Ion gauge status does not exist on all channels.""" with expected_protocol( MKS937B, [], ) as inst: with pytest.raises(AttributeError): inst.ch_2.ion_gauge_status def test_unit_setter(): """Verify the communication of the unit setter.""" with expected_protocol( MKS937B, [("@253U!MICRON", "@253ACKMICRON"), (None, b"FF")], ) as inst: inst.unit = Unit.uHg def test_unit_getter(): """Verify the communication of the unit getter.""" with expected_protocol( MKS937B, [("@253U?", "@253ACKTORR"), (None, b"FF")], ) as inst: assert inst.unit == Unit.Torr def test_power_enabled(): """Verify the communication of the channel power getter.""" with expected_protocol( MKS937B, [("@253CP1?", "@253ACKON"), (None, b"FF")], ) as inst: assert inst.ch_1.power_enabled is True def test_relay_value(): """Verify the communication of the relay setpoint getter.""" with expected_protocol( MKS937B, [("@253SP10?", "@253ACK2.00E+0"), (None, b"FF")], ) as inst: assert inst.relay_10.setpoint == pytest.approx(2.00e0) def test_relay_direction(): """Verify the communication of the relay direction.""" with expected_protocol( MKS937B, [("@253SD3?", "@253ACKABOVE"), (None, b"FF")], ) as inst: assert inst.relay_3.direction == "ABOVE" def test_relay_enabled(): """Verify the communication of the relay enabled property.""" with expected_protocol( MKS937B, [("@253EN6?", "@253ACKENABLE"), (None, b"FF")], ) as inst: assert inst.relay_6.enabled is True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/mksinst/test_mks974b.py0000644000175100001770000000740014623331163023254 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2023 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.test import expected_protocol from pymeasure.instruments.mksinst.mks974b import MKS974B, Unit def test_device_type(): """Verify the communication of the device type.""" with expected_protocol( MKS974B, [("@253DT?", "@253ACKQUADMAG"), (None, b"FF")], ) as inst: assert inst.device_type == "QUADMAG" def test_status(): """Verify the communication of the status.""" with expected_protocol( MKS974B, [("@253T?", "@253ACKO"), (None, b"FF")], ) as inst: assert inst.status == "Ok" def test_pressure(): """Verify the communication of the pressure getter.""" with expected_protocol( MKS974B, [("@253PR4?", "@253ACK1.234E-3"), (None, b"FF")], ) as inst: assert inst.pressure == pytest.approx(1.234e-3) def test_pirani_pressure(): """Verify the communication of the pirani pressure getter.""" with expected_protocol( MKS974B, [("@253PR1?", "@253ACK1.23E-3"), (None, b"FF")], ) as inst: assert inst.pirani_pressure == pytest.approx(1.23e-3) def test_unit_setter(): """Verify the communication of the unit setter.""" with expected_protocol( MKS974B, [("@253U!PASCAL", "@253ACKPASCAL"), (None, b"FF")], ) as inst: inst.unit = Unit.Pa def test_unit_getter(): """Verify the communication of the unit getter.""" with expected_protocol( MKS974B, [("@253U?", "@253ACKTORR"), (None, b"FF")], ) as inst: assert inst.unit == Unit.Torr def test_switch_enabled(): """Verify the communication of the user swith getter.""" with expected_protocol( MKS974B, [("@253SW?", "@253ACKON"), (None, b"FF")], ) as inst: assert inst.switch_enabled is True def test_relay_value(): """Verify the communication of the relay setpoint getter.""" with expected_protocol( MKS974B, [("@253SP1?", "@253ACK2.00E+1"), (None, b"FF")], ) as inst: assert inst.relay_1.setpoint == pytest.approx(2.00e1) def test_relay_direction(): """Verify the communication of the relay direction.""" with expected_protocol( MKS974B, [("@253SD2?", "@253ACKBELOW"), (None, b"FF")], ) as inst: assert inst.relay_2.direction == "BELOW" def test_relay_enabled(): """Verify the communication of the relay enabled property.""" with expected_protocol( MKS974B, [("@253EN3?", "@253ACKPIR"), (None, b"FF")], ) as inst: assert inst.relay_3.enabled == "pirani" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4416063 pymeasure-0.14.0/tests/instruments/novanta/0000755000175100001770000000000014623331176020424 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/novanta/test_fpu60.py0000644000175100001770000001063114623331163022772 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.novanta import Fpu60 def test_disable_emission(): with expected_protocol(Fpu60, [("LASER=OFF", ""), ("LASER=ON", "")], ) as inst: inst.disable_emission() @pytest.mark.parametrize("string, value", (("ENABLED", True), ("DISABLED", False))) def test_emission_enabled(string, value): with expected_protocol(Fpu60, [("STATUS?", string)], ) as inst: assert inst.emission_enabled is value def test_power(): with expected_protocol(Fpu60, [("POWER?", " 12.345W")], ) as inst: assert inst.power == 12.345 def test_power_setpoint(): with expected_protocol(Fpu60, [("POWER=12.345", ""), ("SETPOWER?", " 12.345W")], ) as inst: inst.power_setpoint = 12.345 assert inst.power_setpoint == 12.345 def test_shutter_open(): with expected_protocol(Fpu60, [("SHUTTER OPEN", ""), ("SHUTTER?", "SHUTTER OPEN")], ) as inst: inst.shutter_open = True assert inst.shutter_open is True def test_shutter_close(): with expected_protocol(Fpu60, [("SHUTTER CLOSE", ""), ("SHUTTER?", "SHUTTER CLOSED")], ) as inst: inst.shutter_open = False assert inst.shutter_open is False def test_shutter_close_read(): with expected_protocol(Fpu60, [("SHUTTER?", "SHUTTER CLOSED")], ) as inst: assert inst.shutter_open is False @pytest.mark.parametrize("string, value", (("ENABLED", True), ("DISABLED", False))) def test_interlock(string, value): with expected_protocol(Fpu60, [("INTERLOCK?", string)], ) as inst: assert inst.interlock_enabled is value def test_head_temperature(): with expected_protocol(Fpu60, [("HTEMP?", " 12.345C")], ) as inst: assert inst.head_temperature == 12.345 def test_psu_temperature(): with expected_protocol(Fpu60, [("PSUTEMP?", " 12.345C")], ) as inst: assert inst.psu_temperature == 12.345 def test_get_operation_times(): with expected_protocol( Fpu60, [("TIMERS?", "PSU Time = 00594820 Mins"), (None, "Laser Enabled Time = 00196700 Mins"), (None, "Laser Threshold Time = 00196500 Mins"), (None, "")], ) as inst: assert inst.get_operation_times() == {'psu': 594820, 'laser': 196700, 'laser_above_1A': 196500} def test_current(): with expected_protocol(Fpu60, [("CURRENT?", " 12.3%")], ) as inst: assert inst.current == 12.3 def test_serial_number(): with expected_protocol(Fpu60, [("SERIAL?", "1234567")], ) as inst: assert inst.serial_number == "1234567" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4416063 pymeasure-0.14.0/tests/instruments/oxfordinstruments/0000755000175100001770000000000014623331176022573 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/oxfordinstruments/test_base_instrument.py0000644000175100001770000000346514623331163027412 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.oxfordinstruments.base import OxfordInstrumentsBase, OxfordVISAError def test_wrong_response(): with expected_protocol(OxfordInstrumentsBase, [("A", "B"), (None, "")], max_attempts=1, ) as inst: with pytest.raises(OxfordVISAError): inst.ask("A") def test_write_not_understood_command(): with expected_protocol(OxfordInstrumentsBase, [("A", "?B")], ) as inst: with pytest.raises(OxfordVISAError): inst.write("A") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/oxfordinstruments/test_ips120_10.py0000644000175100001770000000522214623331163025517 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.oxfordinstruments.ips120_10 import IPS120_10 def test_version(): with expected_protocol(IPS120_10, [("V", "IPS120-10 Version 3.04 @ Oxford Instruments 1996")] ) as inst: assert inst.version == "IPS120-10 Version 3.04 @ Oxford Instruments 1996" def test_activity_getter(): with expected_protocol(IPS120_10, [("X", "X00A0C0M00P00")] ) as inst: assert inst.activity == "hold" def test_activity_setter(): with expected_protocol(IPS120_10, [("A0", "A")] ) as inst: inst.activity = "hold" def test_current_setpoint_getter(): with expected_protocol(IPS120_10, [("R0", "R+1.3")] ) as inst: assert inst.current_setpoint == 1.3 def test_current_setpoint_setter(): with expected_protocol(IPS120_10, [("I1.300000", "I")] ) as inst: inst.current_setpoint = 1.3 def test_control_mode_getter(): with expected_protocol(IPS120_10, [("X", "X00A0C1M00P00")] ) as inst: assert inst.control_mode == "RL" def test_control_mode_setter(): with expected_protocol(IPS120_10, [("C1", "C")] ) as inst: inst.control_mode = "RL" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/oxfordinstruments/test_ps120_10.py0000644000175100001770000000550614623331163025353 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.oxfordinstruments.ps120_10 import PS120_10 def test_version(): with expected_protocol(PS120_10, [("V", "IPS120-10 Version 3.04 @ Oxford Instruments 1996")] ) as inst: assert inst.version == "IPS120-10 Version 3.04 @ Oxford Instruments 1996" def test_activity_getter(): with expected_protocol(PS120_10, [("X", "X00A0C0M00P00")] ) as inst: assert inst.activity == "hold" def test_activity_setter(): with expected_protocol(PS120_10, [("A0", "A")] ) as inst: inst.activity = "hold" def test_current_setpoint_getter(): with expected_protocol(PS120_10, [("R0", "R+130")] ) as inst: assert inst.current_setpoint == 1.3 def test_current_setpoint_setter(): with expected_protocol(PS120_10, [("I130", "I")] ) as inst: inst.current_setpoint = 1.3 def test_control_mode_getter(): with expected_protocol(PS120_10, [("X", "X00A0C1M00P00")] ) as inst: assert inst.control_mode == "RL" def test_control_mode_setter(): with expected_protocol(PS120_10, [("C1", "C")] ) as inst: inst.control_mode = "RL" def test_field_setpoint(): with expected_protocol(PS120_10, [("R8", "R+00100")], ) as inst: assert inst.field_setpoint == 1.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4416063 pymeasure-0.14.0/tests/instruments/parker/0000755000175100001770000000000014623331176020242 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/parker/test_parkerGV6.py0000644000175100001770000000310214623331163023452 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.parker import ParkerGV6 def test_init(): with expected_protocol( ParkerGV6, [(b"ECHO0", None), (b"LH0", None), (b"MA1", None), (b"MC0", None), (b"AA1.0", None), (b"A1.0", None), (b"V3.0", None), ]): pass # Verify the expected communication. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4416063 pymeasure-0.14.0/tests/instruments/pendulum/0000755000175100001770000000000014623331176020607 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/pendulum/test_cnt91.py0000644000175100001770000001241614623331163023156 0ustar00runnerdockerimport pytest from pymeasure.instruments.pendulum import CNT91 from pymeasure.test import expected_protocol def test_init(): with expected_protocol( CNT91, [], ): pass # Verify the expected communication. def test_batch_size_getter(): with expected_protocol( CNT91, [(b"FORM:SMAX?", b"10000\n")], ) as inst: assert inst.batch_size == 10000 @pytest.mark.parametrize( "comm_pairs, value", ( ([(b"INIT:CONT 1.0", None)], True), ([(b"INIT:CONT 0.0", None)], False), ), ) def test_continuous_setter(comm_pairs, value): with expected_protocol( CNT91, comm_pairs, ) as inst: inst.continuous = value @pytest.mark.parametrize( "comm_pairs, value", ( ([(b"INIT:CONT?", b"1\n")], True), ([(b"INIT:CONT?", b"0\n")], False), ), ) def test_continuous_getter(comm_pairs, value): with expected_protocol( CNT91, comm_pairs, ) as inst: assert inst.continuous == value @pytest.mark.parametrize( "comm_pairs, value", ( ([(b"ARM:SLOP NEG", None)], "NEG"), ([(b"ARM:SLOP POS", None)], "POS"), ), ) def test_external_arming_start_slope_setter(comm_pairs, value): with expected_protocol( CNT91, comm_pairs, ) as inst: inst.external_arming_start_slope = value @pytest.mark.parametrize( "comm_pairs, value", ( ([(b"ARM:SLOP?", b"NEG\n")], "NEG"), ([(b"ARM:SLOP?", b"POS\n")], "POS"), ), ) def test_external_arming_start_slope_getter(comm_pairs, value): with expected_protocol( CNT91, comm_pairs, ) as inst: assert inst.external_arming_start_slope == value @pytest.mark.parametrize( "comm_pairs, value", ( ([(b"ARM:SOUR EXT1", None)], "A"), ([(b"ARM:SOUR IMM", None)], "IMM"), ), ) def test_external_start_arming_source_setter(comm_pairs, value): with expected_protocol( CNT91, comm_pairs, ) as inst: inst.external_start_arming_source = value @pytest.mark.parametrize( "comm_pairs, value", ( ([(b"ARM:SOUR?", b"EXT1\n")], "A"), ([(b"ARM:SOUR?", b"IMM\n")], "IMM"), ), ) def test_external_start_arming_source_getter(comm_pairs, value): with expected_protocol( CNT91, comm_pairs, ) as inst: assert inst.external_start_arming_source == value @pytest.mark.parametrize( "comm_pairs, value", ( ([(b"FORM REAL", None)], "REAL"), ([(b"FORM ASC", None)], "ASCII"), ), ) def test_format_setter(comm_pairs, value): with expected_protocol( CNT91, comm_pairs, ) as inst: inst.format = value @pytest.mark.parametrize( "comm_pairs, value", ( ([(b"FORM?", b"REAL\n")], "REAL"), ([(b"FORM?", b"ASC\n")], "ASCII"), ), ) def test_format_getter(comm_pairs, value): with expected_protocol( CNT91, comm_pairs, ) as inst: assert inst.format == value @pytest.mark.parametrize( "comm_pairs, value", ( ([(b":ACQ:APER 1000", None)], 1000), ([(b":ACQ:APER 1", None)], 1), ([(b":ACQ:APER 2e-08", None)], 2e-08), ), ) def test_gate_time_setter(comm_pairs, value): with expected_protocol( CNT91, comm_pairs, ) as inst: inst.gate_time = value @pytest.mark.parametrize( "comm_pairs, value", ( ([(b":ACQ:APER?", b"+1.0000000000000E+03\n")], 1000.0), ([(b":ACQ:APER?", b"+1.0000000000000E+00\n")], 1.0), ([(b":ACQ:APER?", b"+2.0000000000000E-08\n")], 2e-08), ), ) def test_gate_time_getter(comm_pairs, value): with expected_protocol( CNT91, comm_pairs, ) as inst: assert inst.gate_time == value def test_buffer_frequency_time_series(): with expected_protocol( CNT91, [ (b"*CLS", None), (b"FORM ASC", None), (b":CONF:ARR:FREQ:BTB 10,(@1)", None), (b"INIT:CONT 0.0", None), (b":ACQ:APER 0.1", None), (b":INIT", None), ], ) as inst: assert inst.buffer_frequency_time_series(*("A", 10), **{"gate_time": 0.1}) is None @pytest.mark.parametrize( "comm_pairs, args, kwargs, value", ( ( [ (b"*OPC?", b"1\n"), ( b":FETC:ARR? 7", b"+9.999992027E+06,+9.999991980E+06,+9.999992043E+06,+9.999992031E+06,+9.999992042E+06,+9.999992041E+06,+9.999992004E+06\n", # noqa: E501 ), ], (7,), {}, [ 9999992.027, 9999991.98, 9999992.043, 9999992.031, 9999992.042, 9999992.041, 9999992.004, ], ), ( [ (b"*OPC?", b"1\n"), (b":FETC:ARR? MAX", b"+9.999992030E+06,+9.999992000E+06,+9.999992043E+06\n"), ], (), {}, [9999992.03, 9999992.0, 9999992.043], ), ), ) def test_read_buffer(comm_pairs, args, kwargs, value): with expected_protocol( CNT91, comm_pairs, ) as inst: assert inst.read_buffer(*args, **kwargs) == value ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4416063 pymeasure-0.14.0/tests/instruments/proterial/0000755000175100001770000000000014623331176020757 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/proterial/test_rod4.py0000644000175100001770000000405214623331163023235 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.proterial.rod4 import ROD4 def test_mfc_range(): with expected_protocol( ROD4, [("\x0201SFK400", "OK"), ("\x0202RFK", "200")], ) as inst: inst.ch_1.mfc_range = 400 assert inst.ch_2.mfc_range == 200 def test_valve_mode(): with expected_protocol( ROD4, [("\x0203SVM0", "OK"), ("\x0204RVM", "1")], ) as inst: inst.ch_3.valve_mode = 'flow' assert inst.ch_4.valve_mode == 'close' def test_setpoint(): with expected_protocol( ROD4, [("\x0201SFD33.3", "OK"), ("\x0202RFD", "50.4")], ) as inst: inst.ch_1.setpoint = 33.3 assert inst.ch_2.setpoint == 50.4 def test_actual_flow(): with expected_protocol( ROD4, [("\x0203RFX", "40.1")], ) as inst: assert inst.ch_3.actual_flow == 40.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4416063 pymeasure-0.14.0/tests/instruments/racal/0000755000175100001770000000000014623331176020040 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/racal/test_racal1992.py0000644000175100001770000002101314623331163023051 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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.test import expected_protocol from pymeasure.instruments.racal import Racal1992 # ============================================================ # TESTS # ============================================================ def test_self_check(): with expected_protocol( Racal1992, [ (' CK', 'CK+010.00000000E+06'), ], ) as instr: instr.operating_mode = 'self_check' assert instr.measured_value == 10000000.0 def test_frequency_a(): with expected_protocol( Racal1992, [ (' FA', 'FA+010.00000000E+06'), ], ) as instr: instr.operating_mode = 'frequency_a' assert instr.measured_value == 10000000.0 def test_period_a(): with expected_protocol( Racal1992, [ (' PA', 'PA+000100.00000E-09'), ], ) as instr: instr.operating_mode = 'period_a' assert instr.measured_value == 100e-9 def test_interval_a_to_b(): with expected_protocol( Racal1992, [ (' TI', 'TI+00000000046.E-09'), ], ) as instr: instr.operating_mode = 'interval_a_to_b' assert instr.measured_value == 46e-9 def test_total_a_by_b(): with expected_protocol( Racal1992, [ (' TA', 'TA+00000000001.E+00'), ], ) as instr: instr.operating_mode = 'total_a_by_b' assert instr.measured_value == 1 def test_phase_a_rel_b(): with expected_protocol( Racal1992, [ (' PH', 'PH+00000000164.E+00'), ], ) as instr: instr.operating_mode = 'phase_a_rel_b' assert instr.measured_value == 164 def test_ratio_a_to_b(): with expected_protocol( Racal1992, [ (' RA', 'RA+00001.000000E+00'), ], ) as instr: instr.operating_mode = 'ratio_a_to_b' assert instr.measured_value == 1.0 def test_ratio_c_to_b(): with expected_protocol( Racal1992, [ (' RC', 'RC+00001.000000E+00'), ], ) as instr: instr.operating_mode = 'ratio_c_to_b' assert instr.measured_value == 1.0 def test_frequency_c(): with expected_protocol( Racal1992, [ (' FC', 'FC+010.00000000E+06'), ], ) as instr: instr.operating_mode = 'frequency_c' assert instr.measured_value == 10000000.0 def test_resolution(): with expected_protocol( Racal1992, [ (' SRS 10', None), (' RRS', 'RS+00000000010.E+00') ], ) as instr: instr.resolution = 10 assert instr.resolution == 10 def test_channel_a_settings(): with expected_protocol( Racal1992, [ (' AAC AAD AAU ALI APS AFE SLA 1.5', None), (' ADC AAE AMN AHI ANS AFD', None), ], ) as instr: instr.channel_settings( 'A', coupling='AC', attenuation='X1', trigger='auto', impedance='50', slope='pos', filtering=True, trigger_level=1.5 ) instr.channel_settings( 'A', coupling='DC', attenuation='X10', trigger='manual', impedance='1M', slope='neg', filtering=False, ) def test_channel_b_settings(): with expected_protocol( Racal1992, [ (' BAC BAD BAU BLI BPS BCS SLB 1.5', None), (' BDC BAE BMN BHI BNS BCC', None), ], ) as instr: instr.channel_settings( 'B', coupling='AC', attenuation='X1', trigger='auto', impedance='50', slope='pos', input_select='separate', trigger_level=1.5 ) instr.channel_settings( 'B', coupling='DC', attenuation='X10', trigger='manual', impedance='1M', slope='neg', input_select='common', ) def test_preset(): with expected_protocol( Racal1992, [ (' IP', 'FA+0010.0000000E+06'), ], ) as instr: instr.preset() assert instr.measured_value == 10000000.0 def test_reset_measurement(): with expected_protocol( Racal1992, [ (' RE', 'FA+0010.0000000E+06'), ], ) as instr: instr.reset_measurement() assert instr.measured_value == 10000000.0 def test_software_version(): with expected_protocol( Racal1992, [ (' RMS', 'MS+00085720404.E+00'), ], ) as instr: assert instr.software_version == 85720404 def test_gpib_software_version(): with expected_protocol( Racal1992, [ (' RGS', 'GS+0000000003.1E+00'), ], ) as instr: assert instr.gpib_software_version == 3.1 def test_math_x(): with expected_protocol( Racal1992, [ (' SMX 1.000000', None), (' RMX', 'MX+001.00000000E+00') ], ) as instr: instr.math_x = 1.0 assert instr.math_x == 1.0 def test_math_z(): with expected_protocol( Racal1992, [ (' SMZ 1.000000', None), (' RMZ', 'MZ+001.00000000E+00') ], ) as instr: instr.math_z = 1.0 assert instr.math_z == 1.0 def test_math_mode(): with expected_protocol( Racal1992, [ (' ME', None), (' MD', None), ], ) as instr: instr.math_mode = True instr.math_mode = False def test_device_type(): with expected_protocol( Racal1992, [ (' RUT', 'UT+00000001992.E+00') ], ) as instr: assert instr.device_type == 1992 def test_trigger_level(): with expected_protocol( Racal1992, [ (' SLA 1.500000', None), (' SLB 1.500000', None), (' RLA', 'LA+000000001.50E+00'), (' RLB', 'LB+000000001.50E+00'), ], ) as instr: instr.trigger_level_a = 1.5 instr.trigger_level_b = 1.5 assert instr.trigger_level_a == 1.5 assert instr.trigger_level_b == 1.5 def test_delay_enable(): with expected_protocol( Racal1992, [ (' DE', None), (' DD', None), ], ) as instr: instr.delay_enable = True instr.delay_enable = False def test_delay_time(): with expected_protocol( Racal1992, [ (' SDT 1.500000', None), (' RDT', 'DT+000000001.50E+00'), ], ) as instr: instr.delay_time = 1.5 assert instr.delay_time == 1.5 def test_special_function_enable(): with expected_protocol( Racal1992, [ (' SFE', None), (' SFD', None), ], ) as instr: instr.special_function_enable = True instr.special_function_enable = False ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4416063 pymeasure-0.14.0/tests/instruments/redpitaya/0000755000175100001770000000000014623331176020740 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/redpitaya/test_redpitaya_scpi.py0000644000175100001770000002174714623331163025360 0ustar00runnerdockerimport datetime import pytest from pymeasure.test import expected_protocol from pymeasure.instruments.redpitaya import RedPitayaScpi def test_init(): with expected_protocol( RedPitayaScpi, [], ): pass # Verify the expected communication. def test_CLOCK_getter(): with expected_protocol( RedPitayaScpi, [], ) as inst: assert inst.CLOCK == 125000000.0 def test_TRIGGER_SOURCES_getter(): with expected_protocol( RedPitayaScpi, [], ) as inst: assert inst.TRIGGER_SOURCES == ('DISABLED', 'NOW', 'CH1_PE', 'CH1_NE', 'CH2_PE', 'CH2_NE', 'EXT_PE', 'EXT_NE', 'AWG_PE', 'AWG_NE') def test_acq_buffer_filled_getter(): with expected_protocol( RedPitayaScpi, [(b'ACQ:TRig:FILL?', b'0')], ) as inst: assert inst.acq_buffer_filled is False @pytest.mark.parametrize("comm_pairs, value", ( ([(b'ACQ:DATA:FORMAT ASCII', None)], 'ASCII'), ([(b'ACQ:DATA:FORMAT BIN', None)], 'BIN'), ([(b'ACQ:DATA:FORMAT BIN', None)], 'BIN'), ([(b'ACQ:DATA:FORMAT ASCII', None)], 'ASCII'), )) def test_acq_format_setter(comm_pairs, value): with expected_protocol( RedPitayaScpi, comm_pairs, ) as inst: inst.acq_format = value def test_acq_size_getter(): with expected_protocol( RedPitayaScpi, [(b'ACQ:BUF:SIZE?', b'16384')], ) as inst: assert inst.buffer_length == 16384 def test_acq_trigger_delay_ns_setter(): with expected_protocol( RedPitayaScpi, [(b'ACQ:TRig:DLY -8182', None)], ) as inst: inst.acq_trigger_delay_ns = -65456 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'ACQ:TRig:DLY?', b'0')], 0), ([(b'ACQ:TRig:DLY?', b'500')], 4000), ([(b'ACQ:TRig:DLY?', b'-8182')], -65456), )) def test_acq_trigger_delay_ns_getter(comm_pairs, value): with expected_protocol( RedPitayaScpi, comm_pairs, ) as inst: assert inst.acq_trigger_delay_ns == value @pytest.mark.parametrize("comm_pairs, value", ( ([(b'ACQ:TRig:DLY 0', None)], 0), ([(b'ACQ:TRig:DLY 500', None)], 500), )) def test_acq_trigger_delay_samples_setter(comm_pairs, value): with expected_protocol( RedPitayaScpi, comm_pairs, ) as inst: inst.acq_trigger_delay_samples = value @pytest.mark.parametrize("comm_pairs, value", ( ([(b'ACQ:TRig:DLY?', b'0')], 0), ([(b'ACQ:TRig:DLY?', b'500')], 500), ([(b'ACQ:TRig:DLY?', b'-8182')], -8182), )) def test_acq_trigger_delay_samples_getter(comm_pairs, value): with expected_protocol( RedPitayaScpi, comm_pairs, ) as inst: assert inst.acq_trigger_delay_samples == value def test_acq_trigger_level_setter(): with expected_protocol( RedPitayaScpi, [(b'ACQ:TRig:LEV 0.500000', None)], ) as inst: inst.acq_trigger_level = 0.5 def test_acq_trigger_level_getter(): with expected_protocol( RedPitayaScpi, [(b'ACQ:TRig:LEV?', b'0.5')], ) as inst: assert inst.acq_trigger_level == 0.5 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'ACQ:TRig DISABLED', None)], 'DISABLED'), ([(b'ACQ:TRig NOW', None)], 'NOW'), ([(b'ACQ:TRig CH1_PE', None)], 'CH1_PE'), ([(b'ACQ:TRig CH1_NE', None)], 'CH1_NE'), ([(b'ACQ:TRig CH2_PE', None)], 'CH2_PE'), ([(b'ACQ:TRig CH2_NE', None)], 'CH2_NE'), ([(b'ACQ:TRig EXT_PE', None)], 'EXT_PE'), ([(b'ACQ:TRig EXT_NE', None)], 'EXT_NE'), ([(b'ACQ:TRig AWG_PE', None)], 'AWG_PE'), ([(b'ACQ:TRig AWG_NE', None)], 'AWG_NE'), )) def test_acq_trigger_source_setter(comm_pairs, value): with expected_protocol( RedPitayaScpi, comm_pairs, ) as inst: inst.acq_trigger_source = value def test_acq_trigger_status_getter(): with expected_protocol( RedPitayaScpi, [(b'ACQ:TRig:STAT?', b'TD')], ) as inst: assert inst.acq_trigger_status is True def test_acq_units_setter(): with expected_protocol( RedPitayaScpi, [(b'ACQ:DATA:Units RAW', None)], ) as inst: inst.acq_units = 'RAW' @pytest.mark.parametrize("comm_pairs, value", ( ([(b'ACQ:DATA:Units?', b'VOLTS')], 'VOLTS'), ([(b'ACQ:DATA:Units?', b'RAW')], 'RAW'), )) def test_acq_units_getter(comm_pairs, value): with expected_protocol( RedPitayaScpi, comm_pairs, ) as inst: assert inst.acq_units == value def test_ain1_gain_setter(): with expected_protocol( RedPitayaScpi, [(b'ACQ:SOUR1:GAIN LV', None)], ) as inst: inst.ain1.gain = 'LV' def test_ain1_gain_getter(): with expected_protocol( RedPitayaScpi, [(b'ACQ:SOUR1:GAIN?', b'LV')], ) as inst: assert inst.ain1.gain == 'LV' @pytest.mark.parametrize("comm_pairs, value", ( ([(b'ACQ:AVG OFF', None)], False), ([(b'ACQ:AVG ON', None)], True), )) def test_average_skipped_samples_setter(comm_pairs, value): with expected_protocol( RedPitayaScpi, comm_pairs, ) as inst: inst.average_skipped_samples = value @pytest.mark.parametrize("comm_pairs, value", ( ([(b'ACQ:AVG?', b'OFF')], False), ([(b'ACQ:AVG?', b'ON')], True), )) def test_average_skipped_samples_getter(comm_pairs, value): with expected_protocol( RedPitayaScpi, comm_pairs, ) as inst: assert inst.average_skipped_samples == value def test_board_name_getter(): with expected_protocol( RedPitayaScpi, [(b'SYST:BRD:Name?', b'STEMlab 125-10')], ) as inst: assert inst.board_name == 'STEMlab 125-10' def test_date_setter(): with expected_protocol( RedPitayaScpi, [(b'SYST:DATE 2023,12,22', None)], ) as inst: inst.date = datetime.date(2023, 12, 22) def test_date_getter(): with expected_protocol( RedPitayaScpi, [(b'SYST:DATE?', b'2023,12,22')], ) as inst: assert inst.date == datetime.date(2023, 12, 22) @pytest.mark.parametrize("comm_pairs, value", ( ([(b'ACQ:DEC 1', None)], 1), ([(b'ACQ:DEC 2', None)], 2), ([(b'ACQ:DEC 4', None)], 4), ([(b'ACQ:DEC 8', None)], 8), ([(b'ACQ:DEC 16', None)], 16), ([(b'ACQ:DEC 32', None)], 32), ([(b'ACQ:DEC 64', None)], 64), ([(b'ACQ:DEC 128', None)], 128), ([(b'ACQ:DEC 256', None)], 256), ([(b'ACQ:DEC 512', None)], 512), ([(b'ACQ:DEC 1024', None)], 1024), ([(b'ACQ:DEC 2048', None)], 2048), ([(b'ACQ:DEC 4096', None)], 4096), ([(b'ACQ:DEC 8192', None)], 8192), ([(b'ACQ:DEC 16384', None)], 16384), ([(b'ACQ:DEC 32768', None)], 32768), ([(b'ACQ:DEC 65536', None)], 65536), )) def test_decimation_setter(comm_pairs, value): with expected_protocol( RedPitayaScpi, comm_pairs, ) as inst: inst.decimation = value @pytest.mark.parametrize("comm_pairs, value", ( ([(b'ACQ:DEC?', b'1')], 1.0), ([(b'ACQ:DEC?', b'2')], 2.0), ([(b'ACQ:DEC?', b'4')], 4.0), ([(b'ACQ:DEC?', b'8')], 8.0), ([(b'ACQ:DEC?', b'16')], 16.0), ([(b'ACQ:DEC?', b'32')], 32.0), ([(b'ACQ:DEC?', b'64')], 64.0), ([(b'ACQ:DEC?', b'128')], 128.0), ([(b'ACQ:DEC?', b'256')], 256.0), ([(b'ACQ:DEC?', b'512')], 512.0), ([(b'ACQ:DEC?', b'1024')], 1024.0), ([(b'ACQ:DEC?', b'2048')], 2048.0), ([(b'ACQ:DEC?', b'4096')], 4096.0), ([(b'ACQ:DEC?', b'8192')], 8192.0), ([(b'ACQ:DEC?', b'16384')], 16384.0), ([(b'ACQ:DEC?', b'32768')], 32768.0), ([(b'ACQ:DEC?', b'65536')], 65536.0), )) def test_decimation_getter(comm_pairs, value): with expected_protocol( RedPitayaScpi, comm_pairs, ) as inst: assert inst.decimation == value def test_led_getter(): with expected_protocol( RedPitayaScpi, [(b'DIG:PIN? LED7', 1)], ) as inst: assert inst.led7.enabled def test_time_setter(): with expected_protocol( RedPitayaScpi, [(b'SYST:TIME 13,07,20', None)], ) as inst: inst.time = datetime.time(13, 7, 20) def test_time_getter(): with expected_protocol( RedPitayaScpi, [(b'SYST:TIME?', '13,07,20')], ) as inst: assert inst.time == datetime.time(13, 7, 20) def test_analog_reset(): with expected_protocol( RedPitayaScpi, [(b'ANALOG:RST', None)], ) as inst: assert inst.analog_reset() is None def test_digital_reset(): with expected_protocol( RedPitayaScpi, [(b'DIG:RST', None)], ) as inst: assert inst.digital_reset() is None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/redpitaya/test_redpitaya_scpi_with_device.py0000644000175100001770000000743214623331163027725 0ustar00runnerdockerimport datetime import pytest from pymeasure.instruments.redpitaya import RedPitayaScpi @pytest.fixture(scope="module") def redpitaya_scpi(connected_device_address: str): """ to use the tests in this file invoke pytest as: pytest -k redpitaya_scpi --device-address TCPIP::x.y.z.k::port::SOCKET where you replace x.y.z.k byt the device IP address and port by its port address """ instr = RedPitayaScpi(adapter=connected_device_address) # ensure the device is in a defined state, e.g. by resetting it. instr.digital_reset() instr.analog_reset() return instr class TestRedpitaya: def test_time_date(self, redpitaya_scpi): inst = redpitaya_scpi inst.time = datetime.time(13, 7, 20) assert inst.time.hour == 13 assert inst.time.minute == 7 inst.date = datetime.date(2023, 12, 22) assert inst.date == datetime.date(2023, 12, 22) def test_led_dio(self, redpitaya_scpi): inst = redpitaya_scpi for ind in range(8): inst.led[ind].enabled = True assert inst.led[ind].enabled inst.led[ind].enabled = False assert not inst.led[ind].enabled for ind in range(7): inst.dioN[ind].direction_in = True assert inst.dioN[ind].direction_in inst.dioN[ind].direction_in = False assert not inst.dioN[ind].direction_in inst.dioN[ind].enabled = True assert inst.dioN[ind].enabled inst.dioN[ind].enabled = False assert not inst.dioN[ind].enabled for ind in range(7): inst.dioP[ind].direction_in = True assert inst.dioP[ind].direction_in inst.dioP[ind].direction_in = False assert not inst.dioP[ind].direction_in inst.dioP[ind].enabled = True assert inst.dioP[ind].enabled inst.dioP[ind].enabled = False assert not inst.dioP[ind].enabled def test_analog_slow(self, redpitaya_scpi): inst = redpitaya_scpi for ind in range(4): inst.analog_in_slow[ind].voltage inst.analog_out_slow[ind].voltage = 0.5 def test_acquisition(self, redpitaya_scpi): inst = redpitaya_scpi for ind in range(17): inst.decimation = 2**ind assert inst.decimation == 2**ind inst.average_skipped_samples = False assert inst.average_skipped_samples is False inst.average_skipped_samples = True assert inst.average_skipped_samples assert inst.acq_units == 'VOLTS' inst.acq_units = 'RAW' assert inst.acq_units == 'RAW' assert inst.buffer_length == 16384 inst.acq_format = 'ASCII' inst.acq_format = 'BIN' for trigger_source in inst.TRIGGER_SOURCES: inst.acq_trigger_source = trigger_source if trigger_source == "DISABLED": assert inst.acq_trigger_status assert inst.acq_buffer_filled is False inst.acq_trigger_delay_samples = 0 assert inst.acq_trigger_delay_samples == 0 assert inst.acq_trigger_delay_ns == 0 inst.acq_trigger_delay_samples = 500 assert inst.acq_trigger_delay_samples == 500 assert inst.acq_trigger_delay_ns == int(500 / inst.CLOCK * 1e9) inst.acq_trigger_delay_ns = RedPitayaScpi.DELAY_NS[10] assert inst.acq_trigger_delay_ns == RedPitayaScpi.DELAY_NS[10] assert inst.acq_trigger_delay_samples == -8192 + 10 inst.acq_trigger_level = 0.5 assert inst.acq_trigger_level == pytest.approx(0.5) inst.ain1.gain = 'LV' assert inst.ain1.gain == 'LV' inst.acq_format = 'BIN' inst.ain1.get_data_from_binary() inst.acq_format = 'ASCII' inst.ain1.get_data_from_ascii() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4416063 pymeasure-0.14.0/tests/instruments/rohdeschwarz/0000755000175100001770000000000014623331176021461 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/rohdeschwarz/test_hmp.py0000644000175100001770000000345414623331163023660 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.rohdeschwarz.hmp import process_sequence def test_process_sequence(): "Test `process_sequence` function." # Sequence must contain multiple of 3 values. with pytest.raises(ValueError): process_sequence([1.0, 1.0, 1.0, 2.0, 2.0]) # Dwell times must be between 0.06 and 10 s. with pytest.raises(ValueError): process_sequence([1.0, 1.0, 1.0, 2.0, 2.0, 0.05]) with pytest.raises(ValueError): process_sequence([1.0, 1.0, 1.0, 2.0, 2.0, 10.1]) # Test with a valid sequence. sequence = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] assert process_sequence(sequence) == "1.0,2.0,3.0,4.0,5.0,6.0" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/rohdeschwarz/test_hmp_with_device.py0000644000175100001770000000647014623331163026233 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pytest from pymeasure.instruments.rohdeschwarz.hmp import HMP4040 @pytest.fixture(scope="module") def hmp4040(connected_device_address): """Return a HMP4040 instrument.""" hmp4040 = HMP4040(connected_device_address) return hmp4040 @pytest.fixture def resetted_hmp4040(hmp4040): """Return a HMP4040 instrument with resetted state.""" hmp4040.reset() return hmp4040 def test_beep(resetted_hmp4040): """Test emission of a beep from the instrument.""" resetted_hmp4040.beep() def test_voltage_limits(resetted_hmp4040): """Test minimum and maximum voltages.""" resetted_hmp4040.voltage_to_min() assert resetted_hmp4040.voltage == resetted_hmp4040.min_voltage resetted_hmp4040.voltage_to_max() assert resetted_hmp4040.voltage == resetted_hmp4040.max_voltage def test_voltage_stepping(resetted_hmp4040): """Test stepping voltages up and down.""" resetted_hmp4040.voltage = 0.0 resetted_hmp4040.step_voltage_up() assert resetted_hmp4040.voltage == resetted_hmp4040.voltage_step resetted_hmp4040.step_voltage_down() assert resetted_hmp4040.voltage == 0.0 def test_channel_state(resetted_hmp4040): """Test activation and deactivation of channels.""" resetted_hmp4040.selected_channel = 1 assert resetted_hmp4040.selected_channel == 1 resetted_hmp4040.set_channel_state(2, True) # check that the selected channel is reset to 2. assert resetted_hmp4040.selected_channel == 1 def test_sequence(resetted_hmp4040): """Test sequence execution.""" # NOTE: There are actually no assertions in this test. But it is possible to check the sequence # execution by looking at display of the instrument. resetted_hmp4040.selected_channel == 1 resetted_hmp4040.voltage = 0.0 resetted_hmp4040.sequence = [1.0, 1.0, 1.0, 2.0, 2.0, 1.0] resetted_hmp4040.repetitions = 0 resetted_hmp4040.transfer_sequence(1) resetted_hmp4040.output_enabled = True resetted_hmp4040.selected_channel_active = True resetted_hmp4040.start_sequence(1) sleep(4) resetted_hmp4040.stop_sequence(1) resetted_hmp4040.output_enabled = False resetted_hmp4040.clear_sequence(1) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/siglenttechnologies/0000755000175100001770000000000014623331176023027 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/siglenttechnologies/test_siglent_spd1168x.py0000644000175100001770000000604214623331163027461 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.siglenttechnologies.siglent_spd1168x import SPD1168X def test_enable_4W_mode(): with expected_protocol( SPD1168X, [("MODE:SET 4W", None), ("MODE:SET 2W", None)] ) as inst: inst.enable_4W_mode(True) inst.enable_4W_mode(False) def test_save_config(): with expected_protocol( SPD1168X, [("*SAV 1", None), ("*SAV 5", None)] ) as inst: inst.save_config(1) inst.save_config(5) def test_recall_config(): with expected_protocol( SPD1168X, [("*RCL 1", None), ("*RCL 5", None)] ) as inst: inst.recall_config(1) inst.recall_config(5) def test_set_current(): with expected_protocol( SPD1168X, [("CH1:CURR 0.5", None), ("CH1:CURR?", "0.5")] ) as inst: inst.ch_1.current_limit = 0.5 assert inst.ch_1.current_limit == 0.5 def test_set_current_trunc(): with expected_protocol( SPD1168X, [("CH1:CURR 8", None), ("CH1:CURR?", "8")] ) as inst: inst.ch_1.current_limit = 10 # too large, gets truncated assert inst.ch_1.current_limit == 8 def test_enable_output(): with expected_protocol( SPD1168X, [("INST CH1", None), ("OUTP CH1,ON", None), ("INST CH1", None), ("OUTP CH1,OFF", None)] ) as inst: inst.ch_1.enable_output() inst.ch_1.enable_output(False) def test_enable_timer(): with expected_protocol( SPD1168X, [("TIME CH1,ON", None), ("TIME CH1,OFF", None)] ) as inst: inst.ch_1.enable_timer() inst.ch_1.enable_timer(False) def test_configure_timer(): with expected_protocol( SPD1168X, [("TIME:SET CH1,1,5.001,8.000,30", None)] ) as inst: inst.ch_1.configure_timer(1, 5.001, 8.55, 30) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/siglenttechnologies/test_siglent_spd1305x.py0000644000175100001770000000466714623331163027465 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.siglenttechnologies.siglent_spd1305x import SPD1305X def test_set_current(): with expected_protocol( SPD1305X, [("CH1:CURR 0.5", None), ("CH1:CURR?", "0.5")] ) as inst: inst.ch_1.current_limit = 0.5 assert inst.ch_1.current_limit == 0.5 def test_set_current_trunc(): with expected_protocol( SPD1305X, [("CH1:CURR 5", None), ("CH1:CURR?", "5")] ) as inst: inst.ch_1.current_limit = 10 # too large, gets truncated assert inst.ch_1.current_limit == 5 def test_set_voltage(): with expected_protocol( SPD1305X, [("CH1:VOLT 0.5", None), ("CH1:VOLT?", "0.5")] ) as inst: inst.ch_1.voltage_setpoint = 0.5 assert inst.ch_1.voltage_setpoint == 0.5 def test_set_voltage_trunc(): with expected_protocol( SPD1305X, [("CH1:VOLT 30", None), ("CH1:VOLT?", "30")] ) as inst: inst.ch_1.voltage_setpoint = 35 # too large, gets truncated assert inst.ch_1.voltage_setpoint == 30 def test_configure_timer(): with expected_protocol( SPD1305X, [("TIME:SET CH1,1,5.001,5.000,30", None)] ) as inst: inst.ch_1.configure_timer(1, 5.001, 8.55, 30) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/signalrecovery/0000755000175100001770000000000014623331176022012 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/signalrecovery/test_dsp7225.py0000644000175100001770000000350514623331163024530 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pytest import raises from pymeasure.test import expected_protocol from pymeasure.instruments.signalrecovery.dsp7225 import DSP7225 @pytest.mark.parametrize("frequency", [ .001, 1e4, 100, ]) def test_valid_frequency(frequency): with expected_protocol( DSP7225, [(b"OF. %g" % frequency, None)], ) as instr: instr.frequency = frequency @pytest.mark.parametrize("frequency", [ 0, 1e14, ]) def test_invalid_frequency(frequency): with raises(ValueError): with expected_protocol( DSP7225, [(b"OF. %g" % frequency, None)], ) as instr: instr.frequency = frequency ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/signalrecovery/test_dsp7265.py0000644000175100001770000000351014623331163024530 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pytest import raises from pymeasure.test import expected_protocol from pymeasure.instruments.signalrecovery.dsp7265 import DSP7265 @pytest.mark.parametrize("frequency", [ .001, 2.51e4, 100, ]) def test_valid_frequency(frequency): with expected_protocol( DSP7265, [(b"OF. %g" % frequency, None)], ) as instr: instr.frequency = frequency @pytest.mark.parametrize("frequency", [ 0, 1e14, ]) def test_invalid_frequency(frequency): with raises(ValueError): with expected_protocol( DSP7265, [(b"OF. %g" % frequency, None)], ) as instr: instr.frequency = frequency ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/signalrecovery/test_dspbase.py0000644000175100001770000000500614623331163025041 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.signalrecovery.dsp_base import DSPBase @pytest.mark.parametrize("index, reference", [(idx, i) for idx, i in enumerate(DSPBase.REFERENCES)]) def test_reference(index, reference): with expected_protocol( DSPBase, [(f"IE {index}", None)], ) as instr: instr.reference = reference @pytest.mark.parametrize("index, imode", [(idx, i) for idx, i in enumerate(DSPBase.IMODES)]) def test_imode(index, imode): with expected_protocol( DSPBase, [(f"IMODE {index}", None)], ) as instr: instr.imode = imode @pytest.mark.parametrize("reading, value", [(b"-12\x00", -12), (b"0\x00", 0), (b"5", 5), (b"12", 12)]) def test_dac1(reading, value): with expected_protocol( DSPBase, [("DAC. 1", reading)], ) as instr: assert instr.dac1 == value @pytest.mark.parametrize("reading, value", [(b'-2.14E-07\r\n', -2.14e-07), (b'-0.0E+00\x00\r\n', 0.0), (b'1.2E-07\r\n', 1.2e-07), (b'6.44E-07\r\n', 6.44e-07), ]) def test_x(reading, value): with expected_protocol( DSPBase, [('X.', reading)], ) as instr: assert instr.x == value ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/srs/0000755000175100001770000000000014623331176017565 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/srs/test_sr830.py0000644000175100001770000000614414623331163022056 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.srs.sr830 import SR830 def test_id(): """Verify the communication of the device type.""" with expected_protocol( SR830, [("*IDN?", "Stanford_Research_Systems,SR830,s/n12345,ver1.07"),], ) as inst: assert inst.id == "Stanford_Research_Systems,SR830,s/n12345,ver1.07" @pytest.mark.parametrize("number, value", ( ("0", 2e-9), ("14", 100e-6), ("25", 0.5), )) def test_sensitivity(number, value): """Verify the communication of the sensitivity getter.""" with expected_protocol( SR830, [("SENS?", number),], ) as inst: assert inst.sensitivity == pytest.approx(value) def test_frequency(): """Verify the communication of the frequency getter.""" with expected_protocol( SR830, [("FREQ?", "121.98"),], ) as inst: assert inst.frequency == pytest.approx(121.98) def test_snap(): """Verify the communication of the measurement values.""" with expected_protocol( SR830, [("SNAP? 1,2", "-4.17234e-007,-5.9605e-007"),], ) as inst: xy = inst.xy assert len(xy) == 2 assert xy[0] == pytest.approx(-4.17234e-007) assert xy[1] == pytest.approx(-5.9605e-007) def test_get_scaling(): """Verify the communication of the X channel scaling settings.""" with expected_protocol( SR830, [("OEXP? 1", "9.7,1"),], ) as inst: offset, expand = inst.get_scaling("X") assert offset == pytest.approx(9.7) assert expand == pytest.approx(10) def test_output_conversion(): """Verify the communication of the X channel value with conversion.""" with expected_protocol( SR830, [("OEXP? 1", "10,1"), ("SENS?", "19"), ("OUTP?1", "-0.000500266"), ], ) as inst: conv = inst.output_conversion("X") assert conv(inst.x) == pytest.approx(-2.66e-7) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/tcpowerconversion/0000755000175100001770000000000014623331176022547 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/tcpowerconversion/test_cxn.py0000644000175100001770000001027114623331163024745 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2022 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.test import expected_protocol from pymeasure.instruments.tcpowerconversion import CXN def test_id(): """Verify the communication of the id property.""" with expected_protocol( CXN, [ ( b"C\x00Gi\x00\x01\x00\x00\x00\xf4", b"\x2aR\x00\x00\x10\x00\x01AG 0313 GTC \x00\x03\x10", ), ], ) as inst: assert inst.id == "AG 0313 GTC" def test_power(): """Verify processing the power measurement.""" with expected_protocol( CXN, [ ( b"C\x00GP\x00\x00\x00\x00\x00\xda", b"\x2aR\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x58", ), ], ) as inst: assert inst.power == (0.0, 0.0, 0.0) def test_temperature(): """Verify processing the temperature measurement.""" with expected_protocol( CXN, [ ( b"C\x00GS\x00\x00\x00\x00\x00\xdd", b"\x2aR\x00\x00\x08\x00\x10\x00\xdd\x00\x04\x00\x03\x01\x4e", ), ], ) as inst: assert inst.temperature == pytest.approx(22.1) def test_status(): """Verify processing the status IntFlag.""" with expected_protocol( CXN, [ ( b"C\x00GS\x00\x00\x00\x00\x00\xdd", b"\x2aR\x00\x00\x08\x00\x11\x00\xdd\x00\x04\x00\x03\x01\x4f", ), ], ) as inst: status = inst.status assert CXN.Status.EXTERNAL_RFSOURCE in status assert CXN.Status.RF_ENABLED in status assert CXN.Status.RF_ENABLED | CXN.Status.EXTERNAL_RFSOURCE == status def test_power_limit(): """Verify processing the power limit property""" with expected_protocol( CXN, [ ( b"C\x00Gp\x00\x00\x00\x00\x00\xfa", b"\x2aR\x00\x00\x144\xf8\x0b\xb8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" b"4\xf8\x02\xbc\x10\x33", ), ], ) as inst: assert inst.power_limit == pytest.approx(300.0) def test_manual_mode(): """Verify processing the manual mode property""" with expected_protocol( CXN, [ ( b"C\x00GT\x00\x00\x00\x00\x00\xde", b"\x2aR\x00\x00\n\x00\x01\x01\xa9\x02v\x00\x00\x00\x00\x01\x7f", ), ], ) as inst: assert inst.manual_mode is True @pytest.mark.parametrize("channel", range(1, 10)) def test_load_capacity_preset(channel): """Verify processing the load capacity propert via a Channel""" # here we use '%' for formatting since encoding from strings makes troubles # with some values (e.g. \xff) cmd = b"C\x00GU\x00%c\x00\x00" % (channel) cmd += CXN._checksum(cmd) response = b"R\x00\x00\n\x00%c\x00\x32\x00\x32\xff\xff\xff\xff" % (channel) response += CXN._checksum(response) with expected_protocol( CXN, [ (cmd, b"\x2a" + response), ], ) as inst: assert inst.channels[channel].load_capacity == 50 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/tdk/0000755000175100001770000000000014623331176017540 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/tdk/test_tdk_base.py0000644000175100001770000000435314623331163022726 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.tdk.tdk_base import TDK_Lambda_Base def test_init(): with expected_protocol( TDK_Lambda_Base, [(b"ADR 6", b"OK")], ): pass # Verify the expected communication. def test_identity(): with expected_protocol( TDK_Lambda_Base, [(b"ADR 6", b"OK"), (b"IDN?", b'LAMBDA,GENX-Y'), (b"REV?", b"REV:1U:4.3")] ) as instr: assert instr.id == ["LAMBDA", "GENX-Y"] assert instr.version == "REV:1U:4.3" def test_multidrop_capability(): with expected_protocol( TDK_Lambda_Base, [(b"ADR 6", b"OK"), (b"MDAV?", b'1')] ) as instr: assert instr.multidrop_capability is True def test_remote(): with expected_protocol( TDK_Lambda_Base, [(b"ADR 6", b"OK"), (b"RMT?", b"REM"), (b"RMT LOC", b"OK"), (b"RMT?", b"LOC"), ] ) as instr: assert instr.remote == "REM" instr.remote = 'LOC' assert instr.remote == "LOC" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/tdk/test_tdk_gen40-38.py0000644000175100001770000000447014623331163023161 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.tdk.tdk_gen40_38 import TDK_Gen40_38 def test_init(): with expected_protocol( TDK_Gen40_38, [(b"ADR 6", b"OK")], ): pass # Verify the expected communication. @pytest.mark.parametrize("volt", (b"10", b"20", b"40")) def test_voltage_setpoint(volt): with expected_protocol( TDK_Gen40_38, [(b"ADR 6", b"OK"), (b"PV " + volt, b"OK"), (b"PV?", volt)] ) as instr: instr.voltage_setpoint = float(volt) assert instr.voltage_setpoint == float(volt) def test_invalid_voltage(): with pytest.raises(ValueError): with expected_protocol( TDK_Gen40_38, [(b"ADR 6", b"OK"), (b"PV 60", b"OK"), ] ) as instr: instr.voltage_setpoint = 60 def test_invalid_current_setpoint(): with pytest.raises(ValueError): with expected_protocol( TDK_Gen40_38, [(b"ADR 6", b"OK"), (b"PC 50", b"OK"), ] ) as instr: instr.current_setpoint = 50 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/tdk/test_tdk_gen80-65.py0000644000175100001770000000450514623331163023164 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.tdk.tdk_gen80_65 import TDK_Gen80_65 def test_init(): with expected_protocol( TDK_Gen80_65, [(b"ADR 6", b"OK")], ): pass # Verify the expected communication. @pytest.mark.parametrize("volt", (b"10", b"20", b"40")) def test_voltage_setpoint(volt): with expected_protocol( TDK_Gen80_65, [(b"ADR 6", b"OK"), (b"PV " + volt, b"OK"), (b"PV?", volt)] ) as instr: instr.voltage_setpoint = float(volt) assert instr.voltage_setpoint == float(volt) def test_invalid_voltage_setpoint(): with pytest.raises(ValueError): with expected_protocol( TDK_Gen80_65, [(b"ADR 6", b"OK"), (b"PV 160", b"OK"), ] ) as instr: instr.voltage_setpoint = 160 def test_invalid_current_setpoint(): with pytest.raises(ValueError): with expected_protocol( TDK_Gen80_65, [(b"ADR 6", b"OK"), (b"PC 150", b"OK"), ] ) as instr: instr.current_setpoint = 150 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/tektronix/0000755000175100001770000000000014623331176021005 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/tektronix/test_afg3152.py0000644000175100001770000000356014623331163023466 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.tektronix.afg3152c import AFG3152C def test_shape(): # Demonstrate message prefix identifying the channel # Note how the implementation of the shape property does not show that # prefix (it is added in the Channel class) with expected_protocol( AFG3152C, [("source1:function:shape?", "LOR"), ("source2:function:shape HAV", None), ], ) as inst: assert inst.ch1.shape == 'lorentz' inst.ch2.shape = 'haversine' def test_beep(): # A message common to all channels does not have a prefix with expected_protocol( AFG3152C, [("system:beep", None)], ) as inst: inst.beep() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/teledyne/0000755000175100001770000000000014623331176020567 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/teledyne/test_teledyneMAUI.py0000644000175100001770000000776714623331163024502 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.teledyne.teledyne_oscilloscope import sanitize_source from pymeasure.instruments.teledyne.teledyneMAUI import TeledyneMAUI from pymeasure.test import expected_protocol INVALID_CHANNELS = ["INVALID_SOURCE", "C1 C2", "C1 MATH", "C1234567", "CHANNEL"] VALID_CHANNELS = [('C1', 'C1'), ('CHANNEL2', 'C2'), ('ch 3', 'C3'), ('chan 4', 'C4'), ('\tC3\t', 'C3'), (' math ', 'MATH')] def test_init(): with expected_protocol( TeledyneMAUI, [(b"CHDR OFF", None)] ): pass # Verify the expected communication. def test_removed_property(): """Verify that certain inherited properties are removed successfully. Some actions from the base class are not valid for this one. """ with expected_protocol(TeledyneMAUI, [(b'CHDR OFF', None)]) as instr: props = ["timebase_hor_magnify"] for prop in props: with pytest.raises(AttributeError): _ = getattr(instr, prop) ch_props = ["trigger_level2", "skew_factor", "unit", "invert"] for prop in ch_props: with pytest.raises(AttributeError): _ = getattr(instr.ch(1), prop) @pytest.mark.parametrize("channel", INVALID_CHANNELS) def test_invalid_source(channel): with pytest.raises(ValueError): sanitize_source(channel) @pytest.mark.parametrize("channel", VALID_CHANNELS) def test_sanitize_valid_source(channel): assert sanitize_source(channel[0]) == channel[1] def test_bwlimit(): with expected_protocol( TeledyneMAUI, [(b"CHDR OFF", None), (b"BWL C1,OFF", None), (b"BWL?", b"C1,OFF"), (b"BWL C1,200MHZ", None), (b"BWL?", b"C1,200MHZ"), (b"BWL C1,ON", None), (b"BWL?", b"C1,ON"), ] ) as instr: instr.ch_1.bwlimit = "OFF" assert instr.bwlimit["C1"] == "OFF" instr.ch_1.bwlimit = "200MHZ" assert instr.bwlimit["C1"] == "200MHZ" instr.ch_1.bwlimit = "ON" assert instr.bwlimit["C1"] == "ON" def test_offset(): with expected_protocol( TeledyneMAUI, [(b"CHDR OFF", None), (b"C1:OFST 1.00E+00V", None), (b"C1:OFST?", b"1.00E+00") ] ) as instr: instr.ch_1.offset = 1. assert instr.ch_1.offset == 1. def test_attenuation(): with expected_protocol( TeledyneMAUI, [(b"CHDR OFF", None), (b"C1:ATTN 100", None), (b"C1:ATTN?", b"100"), (b"C1:ATTN 0.1", None), (b"C1:ATTN?", b"0.1") ] ) as instr: instr.ch_1.probe_attenuation = 100 assert instr.ch_1.probe_attenuation == 100 instr.ch_1.probe_attenuation = 0.1 assert instr.ch_1.probe_attenuation == 0.1 if __name__ == '__main__': pytest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/teledyne/test_teledyneMAUI_with_device.py0000644000175100001770000002671714623331163027050 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 time import sleep from pymeasure.instruments.teledyne import TeledyneMAUI class TestTeledyneMAUI: """ Unit tests for TeledyneMAUI class. This test suite needs an actual device connected, compatible with the MAUI interface. Use the ``--device-address`` flag for pytest to define your device. """ ######################### # PARAMETRIZATION CASES # ######################### BOOLEANS = [False, True] BANDWIDTH_LIMITS = ["OFF", "ON", "200MHZ"] CHANNEL_COUPLINGS = ["ac 1M", "dc 1M", "ground"] ACQUISITION_TYPES = ["normal", "average", "peak", "highres"] TRIGGER_LEVELS = [0.125, 0.150, 0.175] TRIGGER_SLOPES = ["negative", "positive"] ACQUISITION_AVERAGE = [4, 16, 32, 64, 128, 256] WAVEFORM_POINTS = [100, 1000, 10000] WAVEFORM_SOURCES = ["C1", "C2", "C3", "C4"] CHANNELS = [1, 2, 3, 4] ############ # FIXTURES # ############ @pytest.fixture(scope="module") def instrument(self, connected_device_address): return TeledyneMAUI(connected_device_address) @pytest.fixture def resetted_instrument(self, instrument): instrument.reset() sleep(7) return instrument @pytest.fixture def autoscaled_instrument(self, instrument): instrument.reset() sleep(7) instrument.autoscale() sleep(7) return instrument ######### # TESTS # ######### def test_instrument_connection(self, connected_device_address): instrument = TeledyneMAUI(connected_device_address) channel = instrument.ch(1) assert channel is not None def test_channel_autoset(self, instrument): instrument.ch(1).autoscale() sleep(0.1) def test_channel_measure_parameter(self, instrument): instrument.ch(1).measure_parameter("RMS") sleep(0.1) # Channel def test_ch_current_configuration(self, autoscaled_instrument): autoscaled_instrument.ch_1.offset = 0 autoscaled_instrument.ch_1.trigger_level = 0 expected = { "channel": 1, "attenuation": 1.0, "bandwidth_limit": "OFF", "coupling": "dc 1M", "offset": 0.0, "display": True, "volts_div": 0.05, "trigger_coupling": "dc", "trigger_level": 0.0, "trigger_slope": "positive", } actual = autoscaled_instrument.ch(1).current_configuration assert actual == expected @pytest.mark.parametrize("case", BANDWIDTH_LIMITS) def test_ch_bwlimit(self, instrument, case): instrument.bwlimit = case expected = {ch: case for ch in self.WAVEFORM_SOURCES} assert instrument.bwlimit == expected @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case", BANDWIDTH_LIMITS) def test_ch_bwlimit_channel(self, instrument, ch_number, case): instrument.ch(ch_number).bwlimit = case assert instrument.bwlimit[f"C{ch_number}"] == case @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case", CHANNEL_COUPLINGS) def test_ch_coupling(self, instrument, ch_number, case): instrument.ch(ch_number).coupling = case assert instrument.ch(ch_number).coupling == case @pytest.mark.parametrize("ch_number", CHANNELS) @pytest.mark.parametrize("case", BOOLEANS) def test_ch_display(self, instrument, ch_number, case): instrument.ch(ch_number).display = case assert instrument.ch(ch_number).display == case @pytest.mark.parametrize("ch_number", CHANNELS) def test_ch_offset(self, instrument, ch_number): instrument.ch(ch_number).offset = 1 assert instrument.ch(ch_number).offset == 1 @pytest.mark.parametrize("ch_number", CHANNELS) def test_ch_probe_attenuation(self, instrument, ch_number): instrument.ch(ch_number).probe_attenuation = 10 assert instrument.ch(ch_number).probe_attenuation == 10 @pytest.mark.parametrize("ch_number", CHANNELS) def test_ch_scale(self, instrument, ch_number): instrument.ch(ch_number).scale = 1 assert instrument.ch(ch_number).scale == 1 def test_ch_trigger_level(self, autoscaled_instrument): for case in self.TRIGGER_LEVELS: autoscaled_instrument.ch_1.trigger_level = case assert autoscaled_instrument.ch_1.trigger_level == case def test_ch_trigger_slope(self, autoscaled_instrument): with pytest.raises(ValueError): autoscaled_instrument.ch_1.trigger_slope = "abcd" autoscaled_instrument.trigger_select = ("edge", "c1", "off") for case in self.TRIGGER_SLOPES: autoscaled_instrument.ch_1.trigger_slope = case assert autoscaled_instrument.ch_1.trigger_slope == case # Timebase def test_timebase(self, autoscaled_instrument): autoscaled_instrument.timebase_scale = 5e-4 autoscaled_instrument.timebase_offset = 0 expected = { "timebase_scale": 5e-4, "timebase_offset": 0.0, } actual = autoscaled_instrument.timebase for key, val in actual.items(): assert pytest.approx(val, 0.1) == expected[key] def test_timebase_scale(self, resetted_instrument): resetted_instrument.timebase_scale = 1e-3 assert resetted_instrument.timebase_scale == 1e-3 def test_timebase_offset(self, instrument): instrument.timebase_offset = 1e-3 assert instrument.timebase_offset == 1e-3 # Acquisition @pytest.mark.parametrize("case", WAVEFORM_POINTS) def test_waveform_points(self, instrument, case): instrument.waveform_points = case assert instrument.waveform_points == case def test_waveform_preamble(self, autoscaled_instrument): autoscaled_instrument.ch_1.offset = 0 autoscaled_instrument.waveform_points = 0 autoscaled_instrument.waveform_first_point = 0 autoscaled_instrument.waveform_sparsing = 1 autoscaled_instrument.waveform_source = "C1" expected_preamble = { "sparsing": 1.0, "requested_points": 0.0, "memory_size": 2.5e6, "transmitted_points": None, "first_point": 0.0, "source": "C1", "grid_number": 14, "xdiv": 1e-6, "xoffset": 0.0, "ydiv": 0.05, "yoffset": 0.0, } preamble = autoscaled_instrument.waveform_preamble assert preamble == expected_preamble # Setup methods @pytest.mark.parametrize("ch_number", CHANNELS) def test_channel_setup(self, instrument, ch_number): # Only autoscale on the first channel instrument = instrument if ch_number == self.CHANNELS[0]: instrument.reset() sleep(7) instrument.autoscale() sleep(7) # Not testing the actual values assignment since different combinations of # parameters can play off each other. expected = instrument.ch(ch_number).current_configuration instrument.ch(ch_number).setup() assert instrument.ch(ch_number).current_configuration == expected with pytest.raises(AttributeError): instrument.ch(5) instrument.ch(ch_number).setup( bwlimit="ON", coupling="dc 1M", display=True, offset=0.0, probe_attenuation=1.0, scale=0.05, trigger_coupling="dc", trigger_level=0.150, trigger_slope="positive", ) expected = { "channel": ch_number, "attenuation": 1.0, "bandwidth_limit": "ON", "coupling": "dc 1M", "offset": 0.0, "display": True, "volts_div": 0.05, "trigger_coupling": "dc", "trigger_level": 0.150, "trigger_slope": "positive", } actual = instrument.ch(ch_number).current_configuration assert actual == expected def test_timebase_setup(self, resetted_instrument): expected = resetted_instrument.timebase resetted_instrument.timebase_setup() assert resetted_instrument.timebase == expected # Download methods def test_hardcopy_setup(self, instrument): instrument.hardcopy_setup( device="BMP", format="PORTRAIT", background="Std", destination="FILE", area="GRIDONLY", directory="D:\\Waveforms\\temp" ) config_expected = { "DEV": "BMP", "FORMAT": "PORTRAIT", "BCKG": "Std", "DEST": "FILE", "DIR": '"D:\\WAVEFORMS\\TEMP"', "AREA": "GRIDAREAONLY", } config = instrument.hardcopy_setup_current assert config_expected.items() <= config.items() def test_download_image_default_arguments(self, autoscaled_instrument): """Note: the path specified here must exist on the device already!""" img = autoscaled_instrument.download_image() assert type(img) is bytearray assert pytest.approx(len(img), 0.1) == 2734135 # Trigger def test_trigger_select(self, resetted_instrument): with pytest.raises(ValueError): resetted_instrument.trigger_select = "edge" with pytest.raises(ValueError): resetted_instrument.trigger_select = ("edge", "c2") with pytest.raises(ValueError): resetted_instrument.trigger_select = ("edge", "c2", "time") with pytest.raises(ValueError): resetted_instrument.trigger_select = ("ABCD", "c1", "time", 0) with pytest.raises(ValueError): resetted_instrument.trigger_select = ("edge", "c1", "time", 1000) with pytest.raises(ValueError): resetted_instrument.trigger_select = ("edge", "c1", "time", 0, 1) resetted_instrument.trigger_select = ("edge", "c1", "off") resetted_instrument.trigger_select = ("EDGE", "C1", "OFF") assert resetted_instrument.trigger_select == ["edge", "c1", "off"] resetted_instrument.trigger_select = ("glit", "c1", "p2", 1e-3, 2e-3) assert resetted_instrument.trigger_select == ["glit", "c1", "p2", 1e-3, 2e-3] def test_trigger_setup(self, resetted_instrument): expected = resetted_instrument.trigger resetted_instrument.trigger_setup(**expected) assert resetted_instrument.trigger == expected if __name__ == "__main__": pytest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/teledyne/test_teledyneT3AFG.py0000644000175100001770000000715214623331163024537 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.teledyne.teledyneT3AFG import TeledyneT3AFG def test_output_enabled(): """Verify the output enable setter and getter.""" with expected_protocol( TeledyneT3AFG, [("C1:OUTPut ON", None), ("C1:OUTPut?", "C1:OUTP OFF,LOAD,HZ,PLRT,NOR")], ) as inst: inst.ch_1.output_enabled = True assert inst.ch_1.output_enabled is False def test_wavetype(): """Verify the wavetype setter and getter for ramp or sine wavetype.""" with expected_protocol( TeledyneT3AFG, [("C1:BSWV WVTP,RAMP", None), ("C1:BSWV?", "C1:BSWV WVTP,SINE,FRQ,0.3HZ,PERI,3.33333S,AMP,0.08V," "AMPVRMS,0.02828Vrms,MAX_OUTPUT_AMP,4.6V,OFST,-2V,HLEV,-1.96V,LLEV,-2.04V,PHSE,0")], ) as inst: inst.ch_1.wavetype = 'RAMP' assert inst.ch_1.wavetype == "SINE" def test_frequency(): """Verify the frequency setter and getter for ramp or sine wavetype.""" with expected_protocol( TeledyneT3AFG, [("C1:BSWV FRQ,1000", None), ("SYST:ERR?", "-0, No errors"), ("C1:BSWV?", "C1:BSWV WVTP,SINE,FRQ,0.3HZ,PERI,3.33333S,AMP,0.08V," "AMPVRMS,0.02828Vrms,MAX_OUTPUT_AMP,4.6V,OFST,-2V,HLEV,-1.96V,LLEV,-2.04V,PHSE,0")], ) as inst: inst.ch_1.frequency = 1000 assert inst.ch_1.frequency == 0.3 def test_frequency_getter_error(): """Verify the frequency getter for DC wavetype with no frequency value.""" with expected_protocol( TeledyneT3AFG, [("C1:BSWV?", "C1:BSWV WVTP,DC,MAX_OUT_AMP,4.6V,OFST,0V")], ) as inst: assert inst.ch_1.frequency is None def test_amplitude(): """Verify the amplitude setter and getter for ramp or sine wavetype.""" with expected_protocol( TeledyneT3AFG, [("C1:BSWV AMP,1", None), ("SYST:ERR?", "-0, No errors"), ("C1:BSWV?", "C1:BSWV WVTP,SINE,FRQ,0.3HZ,PERI,3.33333S,AMP,0.08V," "AMPVRMS,0.02828Vrms,MAX_OUTPUT_AMP,4.6V,OFST,-2V,HLEV,-1.96V,LLEV,-2.04V,PHSE,0")], ) as inst: inst.ch_1.amplitude = 1 assert inst.ch_1.amplitude == 0.08 def test_offset(): """Verify the offset setter and getter for DC wavetype.""" with expected_protocol( TeledyneT3AFG, [("C1:BSWV OFST,1", None), ("SYST:ERR?", "-0, No errors"), ("C1:BSWV?", "C1:BSWV WVTP,DC,MAX_OUT_AMP,4.6V,OFST,0V")], ) as inst: inst.ch_1.offset = 1 assert inst.ch_1.offset == 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/temptronic/0000755000175100001770000000000014623331176021142 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/temptronic/test_temptronic_base.py0000644000175100001770000000334214623331163025727 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 perf_counter_ns from pymeasure.test import expected_protocol from pymeasure.instruments.temptronic.temptronic_base import ATSBase def test_check_query_delay(): with expected_protocol(ATSBase, [("TTIM?", "7")]) as inst: start = perf_counter_ns() assert inst.maximum_test_time == 7 delay = perf_counter_ns() - start # HACK acceptable factor is needed, as in CI under windows (Py38, Py39) the `sleep` interval # is slightly shorter than the given argument. acceptable_factor = 0.95 assert delay > 0.05 * 1e9 * acceptable_factor ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/test_all_instruments.py0000644000175100001770000002422614623331163023614 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 from pathlib import Path from unittest.mock import MagicMock import pytest from pymeasure import instruments from pymeasure.instruments import Instrument, Channel, generic_types # Collect all instruments def find_devices_in_module(module): devices = set() channels = set() base_dir = Path(module.__path__[0]) base_import = module.__package__ + "." for inst_file in Path(base_dir).rglob("*.py"): relative_path = inst_file.relative_to(base_dir) if inst_file == "__init__.py": # import parent module when filename __init__.py relative_import = ".".join(relative_path.parts[:-1])[:-3] else: relative_import = ".".join(relative_path.parts)[:-3] try: submodule = importlib.import_module(base_import + relative_import) for dev in dir(submodule): if dev.startswith("__"): continue d = getattr(submodule, dev) try: i = issubclass(d, Instrument) c = issubclass(d, Channel) except TypeError: # d is no class continue else: if i: devices.add(d) elif c: channels.add(d) except ModuleNotFoundError: # Some non-required driver dependencies may not be installed on test computer, # for example ni.VirtualBench pass except (OSError, AttributeError): # On Windows instruments.ni.daqmx can raise an OSError before ModuleNotFoundError # when checking installed driver files # it raises an AttributeError under Python 312 pass return devices, channels devices, channels = find_devices_in_module(instruments) # Collect all properties properties = [] for device in devices.union(channels): for property_name in dir(device): prop = getattr(device, property_name) if isinstance(prop, property): properties.append((device, property_name, prop)) for mixin in dir(generic_types): if mixin in ("Instrument", "Channel", "CommonBase"): # exclucion list. continue elif mixin[0].isupper(): # filter only classes device = getattr(generic_types, mixin) for property_name in dir(device): prop = getattr(device, property_name) if isinstance(prop, property): properties.append((device, property_name, prop)) # Instruments unable to accept an Adapter instance. proper_adapters = [] # Instruments with communication in their __init__, which consequently fails. need_init_communication = [ "SwissArmyFake", "FakeInstrument", "ThorlabsPM100USB", "Keithley2700", "TC038", "Agilent34450A", "AWG401x_AWG", "AWG401x_AFG", "VARX", "HP8116A", "IBeamSmart", "ANC300Controller", ] # Channels which are still an Instrument subclass channel_as_instrument_subclass = [ "SMU", # agilent/agilent4156 "VMU", # agilent/agilent4156 "VSU", # agilent/agilent4156 "VARX", # agilent/agilent4156 "VAR1", # agilent/agilent4156 "VAR2", # agilent/agilent4156 "VARD", # agilent/agilent4156 ] # Instruments whose property docstrings are not YET in accordance with the style (Get, Set, Control) grandfathered_docstring_instruments = [ "AWG401x_AFG", "AWG401x_AWG", "AdvantestR624X", "SMUChannel", # AdvantestR624X "AdvantestR6245", "AdvantestR6246", "Agilent33220A", "Agilent33500", "Agilent33500Channel", "Agilent33521A", "Agilent34450A", "Agilent4156", "SMU", # agilent/agilent4156 "VMU", # agilent/agilent4156 "VSU", # agilent/agilent4156 "VARX", # agilent/agilent4156 "VAR1", # agilent/agilent4156 "VAR2", # agilent/agilent4156 "VARD", # agilent/agilent4156 "Agilent8257D", "Agilent8722ES", "AgilentB1500", "AgilentE4408B", "AgilentE4980", "Ametek7270", "DPSeriesMotorController", "AnritsuMS2090A", "SM7045D", "HP3437A", "HP34401A", "HP3478A", "HP6632A", "HP6633A", "HP6634A", "Keithley2000", "Keithley2306", "Keithley2306Channel", "BatteryChannel", # Keithley2306 "Step", # Keithley2306 "Relay", # Keithley2306 "Keithley2400", "Keithley2450", "Keithley2600", "Keithley2700", "Keithley2750", "Keithley6221", "Keithley6517B", "KeysightDSOX1102G", "LakeShore421", "LakeShoreTemperatureChannel", "LakeShoreHeaterChannel", "IPS120_10", "ITC503", "PS120_10", "ParkerGV6", "FSL", "SFM", "DSP7265", "SG380", "SR510", "SR570", "SR830", "SR860", "ATS525", "ATS545", "ATSBase", "ECO560", "TexioPSW360L30", "IonGaugeAndPressureChannel", "PressureChannel", "SequenceEntry", "ChannelBase", "ChannelAWG", "ChannelAFG", ] @pytest.mark.parametrize("cls", devices) def test_adapter_arg(cls): "Test that every instrument has adapter as their input argument." if cls.__name__ in proper_adapters: pytest.skip(f"{cls.__name__} does not accept an Adapter instance.") elif cls.__name__ in need_init_communication: pytest.skip(f"{cls.__name__} requires communication in init.") elif cls.__name__ in channel_as_instrument_subclass: pytest.skip(f"{cls.__name__} is a channel, not an instrument.") elif cls.__name__ == "Instrument": pytest.skip("`Instrument` requires a `name` parameter.") cls(adapter=MagicMock()) @pytest.mark.parametrize("cls", devices) def test_name_argument(cls): "Test that every instrument accepts a name argument." if cls.__name__ in (*proper_adapters, *need_init_communication): pytest.skip(f"{cls.__name__} cannot be tested without communication.") elif cls.__name__ in channel_as_instrument_subclass: pytest.skip(f"{cls.__name__} is a channel, not an instrument.") inst = cls(adapter=MagicMock(), name="Name_Test") assert inst.name == "Name_Test" # This uses a pyvisa-sim default instrument, we could also define our own. SIM_RESOURCE = "ASRL2::INSTR" is_pyvisa_sim_not_installed = not bool(importlib.util.find_spec("pyvisa_sim")) @pytest.mark.skipif( is_pyvisa_sim_not_installed, reason="PyVISA tests require the pyvisa-sim library" ) @pytest.mark.parametrize("cls", devices) def test_kwargs_to_adapter(cls): """Verify that kwargs are accepted and handed to the adapter.""" if cls.__name__ in (*proper_adapters, *need_init_communication): pytest.skip(f"{cls.__name__} cannot be tested without communication.") elif cls.__name__ in channel_as_instrument_subclass: pytest.skip(f"{cls.__name__} is a channel, not an instrument.") elif cls.__name__ == "Instrument": pytest.skip("`Instrument` requires a `name` parameter.") with pytest.raises( ValueError, match="'kwarg_test' is not a valid attribute for type SerialInstrument" ): cls(SIM_RESOURCE, visa_library="@sim", kwarg_test=True) @pytest.mark.parametrize("cls", devices) @pytest.mark.filterwarnings( "error:It is deprecated to specify `includeSCPI` implicitly:FutureWarning") def test_includeSCPI_explicitly_set(cls): if cls.__name__ in (*proper_adapters, *need_init_communication): pytest.skip(f"{cls.__name__} cannot be tested without communication.") elif cls.__name__ in channel_as_instrument_subclass: pytest.skip(f"{cls.__name__} is a channel, not an instrument.") elif cls.__name__ == "Instrument": pytest.skip("`Instrument` requires a `name` parameter.") cls(adapter=MagicMock()) # assert that no error is raised @pytest.mark.parametrize("cls", devices) @pytest.mark.filterwarnings( "error:Defining SCPI base functionality with `includeSCPI=True` is deprecated:FutureWarning") def test_includeSCPI_not_set_to_True(cls): if cls.__name__ in (*proper_adapters, *need_init_communication): pytest.skip(f"{cls.__name__} cannot be tested without communication.") elif cls.__name__ in channel_as_instrument_subclass: pytest.skip(f"{cls.__name__} is a channel, not an instrument.") elif cls.__name__ == "Instrument": pytest.skip("`Instrument` requires a `name` parameter.") cls(adapter=MagicMock()) # assert that no error is raised def property_name_to_id(value): """Create a test id from `value`.""" device, property_name, prop = value return f"{device.__name__}.{property_name}" @pytest.mark.parametrize("prop_set", properties, ids=property_name_to_id) def test_property_docstrings(prop_set): device, property_name, prop = prop_set if device.__name__ in grandfathered_docstring_instruments: pytest.skip(f"{device.__name__} is in the codebase and has to be refactored later on.") start = prop.__doc__.split(maxsplit=1)[0] assert start in ("Control", "Measure", "Set", "Get"), ( f"'{device.__name__}.{property_name}' docstring does start with '{start}', not 'Control', " "'Measure', 'Get', or 'Set'." ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/test_channel.py0000644000175100001770000001565714623331163022011 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 unittest import mock import pytest from pymeasure.test import expected_protocol from pymeasure.instruments import Channel, Instrument from pymeasure.instruments.validators import truncated_range class GenericChannel(Channel): # Use truncated_range as this easily lets us test for the range boundaries fake_ctrl = Channel.control( "C{ch}:control?", "C{ch}:control %d", "Control docs", validator=truncated_range, values=(1, 10), dynamic=True, ) fake_setting = Channel.setting( "C{ch}:setting %d", "Set docs", validator=truncated_range, values=(1, 10), dynamic=True, ) fake_measurement = Channel.measurement( "C{ch}:measurement?", "Measure docs", values={'X': 1, 'Y': 2, 'Z': 3}, map_values=True, dynamic=True, ) special_control = Channel.control( "SOUR{ch}:special?", "OUTP{ch}:special %s", """Control with different channel specifiers for get and set.""", cast=str, ) check_errors_control = Channel.control( "C{ch}:xy?", "C{ch}:xy %d", """Control something, with error check on get and set.""", check_set_errors=True, check_get_errors=True, ) class ChannelWithPlaceholder(Channel): """A test channel with a different placeholder""" placeholder = "fn" test = Channel.control("{fn}test?", "test{fn} %i", """Control test.""") class ChannelInstrument(Instrument): def __init__(self, adapter, name="ChannelInstrument", **kwargs): super().__init__(adapter, name, **kwargs) self.errors = [] self.add_child(GenericChannel, "A") self.add_child(GenericChannel, "B") def check_errors(self): err = self.values("SYST:ERR?") if err: self.errors.append(err) return err class TestChannelCommunication: @pytest.fixture() def ch(self): a = mock.MagicMock(return_value="5") return Channel(a, "A") def test_write(self, ch): ch.write("abc") assert ch.parent.method_calls == [mock.call.write('abc')] def test_read(self, ch): ch.read() assert ch.parent.method_calls == [mock.call.read()] def test_write_bytes(self, ch): ch.write_bytes(b"abc") assert ch.parent.method_calls == [mock.call.write_bytes(b"abc")] def test_read_bytes(self, ch): ch.read_bytes(5) assert ch.parent.method_calls == [mock.call.read_bytes(5)] def test_write_binary_values(self, ch): ch.write_binary_values("abc", [5, 6, 7]) assert ch.parent.method_calls == [mock.call.write_binary_values("abc", [5, 6, 7])] def test_read_binary_values(self, ch): ch.read_binary_values() assert ch.parent.method_calls == [mock.call.read_binary_values()] def test_check_errors(self, ch): ch.check_errors() assert ch.parent.method_calls == [mock.call.check_errors()] def test_check_get_errors(self, ch): ch.check_get_errors() assert ch.parent.method_calls == [mock.call.check_get_errors()] def test_check_set_errors(self, ch): ch.check_set_errors() assert ch.parent.method_calls == [mock.call.check_set_errors()] def test_channel_with_different_prefix(): c = ChannelWithPlaceholder(None, "A") assert c.insert_id("id:{fn}") == "id:A" def test_channel_write(): with expected_protocol(ChannelInstrument, [("ChA:volt?", None)]) as inst: inst.ch_A.write("Ch{ch}:volt?") def test_channel_write_name_twice(): """Verify, that any (i.e. more than one) occurrence of '{ch}' is changed.""" with expected_protocol(ChannelInstrument, [("ChA:volt:ChA?", None)]) as inst: inst.ch_A.write("Ch{ch}:volt:Ch{ch}?") def test_channel_write_without_ch(): """Verify, that it is possible to send a command without '{ch}'.""" with expected_protocol(ChannelInstrument, [("Test", None)]) as inst: inst.ch_A.write("Test") def test_channel_control(): with expected_protocol( ChannelInstrument, [("CA:control 7", None), ("CA:control?", "1.45")] ) as inst: inst.ch_A.fake_ctrl = 7 assert inst.ch_A.fake_ctrl == 1.45 def test_channel_setting(): with expected_protocol( ChannelInstrument, [("CA:setting 3", None)] ) as inst: inst.ch_A.fake_setting = 3 def test_channel_measurement(): with expected_protocol( ChannelInstrument, [("CA:measurement?", "2")] ) as inst: assert inst.ch_A.fake_measurement == "Y" def test_channel_dynamic_property(): with expected_protocol(ChannelInstrument, [("CA:control 100", None)]) as inst: inst.ch_A.fake_ctrl_values = (1, 200) # original values is (1, 10), therefore 100 should not be allowed. inst.ch_A.fake_ctrl = 100 def test_channel_special_control(): """Test different Prefixes for getter and setter.""" with expected_protocol(ChannelInstrument, [("SOURA:special?", "super"), ("OUTPB:special test", None)], ) as inst: assert inst.ch_A.special_control == "super" inst.ch_B.special_control = "test" def test_channel_check_set_errors(): with expected_protocol(ChannelInstrument, [("CA:xy 5", None), ("SYST:ERR?", "Some, error")], ) as inst: inst.ch_A.check_errors_control = 5 assert inst.errors == [["Some", " error"]] def test_channel_check_get_errors(): with expected_protocol(ChannelInstrument, [("CA:xy?", "5"), ("SYST:ERR?", "Some, error")], ) as inst: assert inst.ch_A.check_errors_control == 5 assert inst.errors == [["Some", " error"]] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/test_common_base.py0000644000175100001770000007762114623331163022662 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pytest from pymeasure.units import ureg from pymeasure.test import expected_protocol from pymeasure.instruments.common_base import DynamicProperty, CommonBase from pymeasure.adapters import FakeAdapter, ProtocolAdapter from pymeasure.instruments.validators import strict_discrete_set, strict_range, truncated_range class CommonBaseTesting(CommonBase): """Add read/write methods in order to use the ProtocolAdapter.""" def __init__(self, parent, id=None, *args, **kwargs): if "test" in kwargs: self.test = kwargs.pop("test") super().__init__(*args, **kwargs) self.parent = parent self.id = id self.args = args self.kwargs = kwargs def wait_for(self, query_delay=0): pass def write(self, command): self.parent.write(command) def read(self): return self.parent.read() class GenericBase(CommonBaseTesting): # Use truncated_range as this easily lets us test for the range boundaries fake_ctrl = CommonBase.control( "C{ch}:control?", "C{ch}:control %d", "docs", validator=truncated_range, values=(1, 10), dynamic=True, ) fake_setting = CommonBase.setting( "C{ch}:setting %d", "docs", validator=truncated_range, values=(1, 10), dynamic=True, ) fake_measurement = CommonBase.measurement( "C{ch}:measurement?", "docs", values={'X': 1, 'Y': 2, 'Z': 3}, map_values=True, dynamic=True, ) special_control = CommonBase.control( "SOUR{ch}:special?", "OUTP{ch}:special %s", """A special control with different channel specifiers for get and set.""", cast=str, ) @pytest.fixture() def generic(): return GenericBase() class FakeBase(CommonBaseTesting): def __init__(self, *args, **kwargs): super().__init__(FakeAdapter(), *args, **kwargs) fake_ctrl = CommonBase.control( "", "%d", "docs", validator=truncated_range, values=(1, 10), dynamic=True, ) fake_setting = CommonBase.setting( "%d", "docs", validator=truncated_range, values=(1, 10), dynamic=True, ) fake_measurement = CommonBase.measurement( "", "docs", values={'X': 1, 'Y': 2, 'Z': 3}, map_values=True, dynamic=True, ) fake_ctrl_errors = CommonBase.control( "ge", "se %d", "Fake control for getting errors after getting/setting values.", validator=truncated_range, values=(1, 10), dynamic=True, check_get_errors=True, check_set_errors=True ) @pytest.fixture() def fake(): return FakeBase() class ExtendedBase(FakeBase): # Keep values unchanged, just derive another instrument, e.g. to add more properties fake_ctrl2 = CommonBase.control( "", "%d", "docs", validator=strict_range, values=(1, 10), dynamic=True, ) fake_ctrl2_values = (5, 20) class StrictExtendedBase(ExtendedBase): # Use strict instead of truncated range validator fake_ctrl_validator = strict_range fake_setting_validator = strict_range class NewRangeBase(FakeBase): # Choose different properties' values, like you would for another device model fake_ctrl_values = (10, 20) fake_setting_values = (10, 20) fake_measurement_values = {'X': 4, 'Y': 5, 'Z': 6} class Child(CommonBase): """A child, which accepts parent and id arguments.""" def __init__(self, parent, id=None, *args, **kwargs): super().__init__() class MultiChannelParent(CommonBaseTesting): """A Base as a parent""" channels = CommonBase.MultiChannelCreator(GenericBase, ("A", "B", "C")) analog = CommonBase.MultiChannelCreator(GenericBase, [1, 2], prefix="an_", test=True) class SingleChannelParent(CommonBaseTesting): """A Base as a parent""" ch_A = CommonBase.ChannelCreator(GenericBase, "A") ch_B = CommonBase.ChannelCreator(GenericBase, "B") ch_C = CommonBase.ChannelCreator(GenericBase, "C") an_1 = CommonBase.ChannelCreator(GenericBase, 1, collection="analog", test=True) an_2 = CommonBase.ChannelCreator(GenericBase, 2, collection="analog", test=True) function = CommonBase.ChannelCreator(Child) class MixChannelParent(CommonBaseTesting): """A Base as a parent""" channels = CommonBase.MultiChannelCreator(GenericBase, ("A", "B", "C")) ch_D = CommonBase.ChannelCreator(GenericBase, "D") output_Z = CommonBase.ChannelCreator(GenericBase, "Z") analog = CommonBase.MultiChannelCreator(GenericBase, list(range(0, 10)), prefix="an_", test=True) # Test dynamic properties def test_dynamic_property_fget_unset(): d = DynamicProperty() with pytest.raises(AttributeError, match="Unreadable attribute"): d.__get__(5) def test_dynamic_property_fset_unset(): d = DynamicProperty() with pytest.raises(AttributeError, match="set attribute"): d.__set__(5, 7) # Test CommonBase.MultipleChannelCreator child management class TestInitWithMultipleChannelCreator: @pytest.fixture() def parent(self): return MultiChannelParent(ProtocolAdapter()) def test_channels(self, parent): assert len(parent.channels) == 3 assert parent.ch_A == parent.channels['A'] assert isinstance(parent.ch_A, GenericBase) def test_analog(self, parent): assert len(parent.analog) == 2 assert parent.an_1 == parent.analog[1] assert isinstance(parent.analog[1], GenericBase) def test_removal_of_protected_children_fails(self, parent): with pytest.raises(TypeError, match="cannot remove channels defined at class"): parent.remove_child(parent.ch_A) def test_channel_creation_works_more_than_once(self): """A zipper object works just once, ensure that a class may be used more often.""" p1 = MultiChannelParent(ProtocolAdapter()) # first instance of that class assert isinstance(p1.analog[1], GenericBase) # verify that it worked once p2 = MultiChannelParent(ProtocolAdapter()) # second instance of that class assert isinstance(p2.analog[1], GenericBase) # verify that it worked a second time def test_channel_pairs_length(self, parent): assert len(parent.get_channel_pairs(parent.__class__)) == 5 def test_channel_creator_remains_unchanged_as_class_attribute(self, parent): assert isinstance(parent.__class__.channels, CommonBase.MultiChannelCreator) # Test CommonBase.ChannelCreator child management class TestInitWithChannelCreator: @pytest.fixture() def parent(self): return SingleChannelParent(ProtocolAdapter()) def test_channels(self, parent): assert len(parent.channels) == 3 assert parent.ch_A == parent.channels['A'] assert isinstance(parent.ch_A, GenericBase) def test_analog(self, parent): assert len(parent.analog) == 2 assert parent.an_1 == parent.analog[1] assert isinstance(parent.analog[1], GenericBase) def test_function(self, parent): assert isinstance(parent.function, Child) def test_removal_of_protected_children_fails(self, parent): with pytest.raises(TypeError, match="cannot remove channels defined at class"): parent.remove_child(parent.ch_A) def test_removal_of_protected_single_children_fails(self, parent): with pytest.raises(TypeError, match="cannot remove channels defined at class"): parent.remove_child(parent.function) def test_channel_creation_works_more_than_once(self): """A zipper object works just once, ensure that a class may be used more often.""" p1 = SingleChannelParent(ProtocolAdapter()) # first instance of that class assert isinstance(p1.analog[1], GenericBase) # verify that it worked once p2 = SingleChannelParent(ProtocolAdapter()) # second instance of that class assert isinstance(p2.analog[1], GenericBase) # verify that it worked a second time def test_channel_pairs_length(self, parent): assert len(parent.get_channel_pairs(parent.__class__)) == 6 def test_channel_creator_remains_unchanged_as_class_attribute(self, parent): assert isinstance(parent.__class__.ch_A, CommonBase.ChannelCreator) assert isinstance(parent.__class__.an_1, CommonBase.ChannelCreator) assert isinstance(parent.__class__.function, CommonBase.ChannelCreator) # Test combination CommonBase.MultipleChannelCreator and CommonBase.ChannelCreator # child management class TestInitWithMixChannelCreator: @pytest.fixture() def parent(self): return MixChannelParent(ProtocolAdapter()) def test_channels(self, parent): assert len(parent.channels) == 5 assert parent.ch_A == parent.channels['A'] assert parent.output_Z == parent.channels['Z'] assert isinstance(parent.ch_A, GenericBase) assert isinstance(parent.output_Z, GenericBase) def test_analog(self, parent): assert len(parent.analog) == 10 assert parent.an_1 == parent.analog[1] assert isinstance(parent.analog[1], GenericBase) def test_removal_of_protected_children_fails(self, parent): with pytest.raises(TypeError, match="cannot remove channels defined at class"): parent.remove_child(parent.ch_A) def test_channel_creation_works_more_than_once(self): """A zipper object works just once, ensure that a class may be used more often.""" p1 = MixChannelParent(ProtocolAdapter()) # first instance of that class assert isinstance(p1.analog[1], GenericBase) # verify that it worked once assert isinstance(p1.output_Z, GenericBase) p2 = MixChannelParent(ProtocolAdapter()) # second instance of that class assert isinstance(p2.analog[1], GenericBase) # verify that it worked a second time assert isinstance(p2.output_Z, GenericBase) def test_channel_pairs_length(self, parent): assert len(parent.get_channel_pairs(parent.__class__)) == 15 def test_channel_creator_remains_unchanged_as_class_attribute(self, parent): assert isinstance(parent.__class__.channels, CommonBase.MultiChannelCreator) assert isinstance(parent.__class__.analog, CommonBase.MultiChannelCreator) assert isinstance(parent.__class__.output_Z, CommonBase.ChannelCreator) class TestAddChild: """Test the `add_child` method""" @pytest.fixture() def parent(self): parent = CommonBaseTesting(ProtocolAdapter()) parent.add_child(GenericBase, "A", test=5) parent.add_child(GenericBase, "B") parent.add_child(GenericBase, prefix=None, collection="function") return parent def test_correct_class(self, parent): assert isinstance(parent.ch_A, GenericBase) def test_arguments(self, parent): assert parent.channels["A"].id == "A" assert parent.channels["A"].test == 5 def test_attribute_access(self, parent): assert parent.ch_B == parent.channels["B"] def test_len(self, parent): assert len(parent.channels) == 2 def test_attributes(self, parent): assert parent.ch_A._name == "ch_A" assert parent.ch_B._collection == "channels" def test_overwriting_list_raises_error(self, parent): """A single channel is only allowed, if there is no list of that name.""" with pytest.raises(ValueError, match="already exists"): parent.add_child(GenericBase, prefix=None) def test_single_channel(self, parent): """Test, that id=None creates a single channel.""" assert isinstance(parent.function, GenericBase) assert parent.function._name == "function" def test_evaluating_false_id_creates_channels(self, parent): """Test that an id evaluating false (e.g. 0) creates a channels list.""" parent.add_child(GenericBase, 0, collection="special") assert isinstance(parent.special, dict) class TestRemoveChild: @pytest.fixture() def parent_without_children(self): parent = CommonBaseTesting(ProtocolAdapter()) parent.add_child(GenericBase, "A") parent.add_child(GenericBase, "B") parent.add_child(GenericBase, prefix=None, collection="function") parent.remove_child(parent.ch_A) parent.remove_child(parent.channels["B"]) parent.remove_child(parent.function) return parent def test_remove_child_leaves_channels_empty(self, parent_without_children): assert parent_without_children.channels == {} def test_remove_child_clears_attributes(self, parent_without_children): assert getattr(parent_without_children, "ch_A", None) is None assert getattr(parent_without_children, "ch_B", None) is None assert getattr(parent_without_children, "function", None) is None class TestInheritanceWithChildren: class InstrumentSubclass(MultiChannelParent): """Override one channel group, inherit other groups.""" function = CommonBase.ChannelCreator(GenericBase, "overridden", prefix=None) @pytest.fixture() def parent(self): return self.InstrumentSubclass(ProtocolAdapter()) def test_inherited_children_are_present(self, parent): assert isinstance(parent.ch_A, GenericBase) def test_ChannelCreator_is_replaced_by_channel_collection(self, parent): assert not isinstance(parent.channels, CommonBase.ChannelCreator) def test_overridden_child_is_present(self, parent): assert parent.function.id == "overridden" # Test MultiChannelCreator @pytest.mark.parametrize("args, pairs, kwargs", ( ((Child, ["A", "B"]), [(Child, "A"), (Child, "B")], {'prefix': "ch_"}), (((Child, GenericBase, Child), (1, 2, 3)), [(Child, 1), (GenericBase, 2), (Child, 3)], {'prefix': "ch_"}), )) def test_MultiChannelCreator(args, pairs, kwargs): """Test whether the channel creator receives the right arguments.""" d = CommonBase.MultiChannelCreator(*args) i = 0 for pair in d.pairs: assert pair == pairs[i] i += 1 assert d.kwargs == kwargs def test_MultiChannelCreator_different_list_lengths(): with pytest.raises(AssertionError, match="Lengths"): CommonBase.MultiChannelCreator(("A", "B", "C"), (Child,) * 2) def test_ChannelCreator_invalid_input(): with pytest.raises(ValueError, match="Invalid"): CommonBase.ChannelCreator("A", {}) # Test CommonBase communication def test_ask_writes_and_reads(): with expected_protocol(CommonBaseTesting, [("Sent", "Received")]) as inst: assert inst.ask("Sent") == "Received" @pytest.mark.parametrize("value, kwargs, result", (("5,6,7", {}, [5, 6, 7]), ("5.6.7", {'separator': '.'}, [5, 6, 7]), ("5,6,7", {'cast': str}, ['5', '6', '7']), ("X,Y,Z", {}, ['X', 'Y', 'Z']), ("X,Y,Z", {'cast': str}, ['X', 'Y', 'Z']), ("X.Y.Z", {'separator': '.'}, ['X', 'Y', 'Z']), ("0,5,7.1", {'cast': bool}, [False, True, True]), ("x5x", {'preprocess_reply': lambda v: v.strip("x")}, [5]), ("X,Y,Z", {'maxsplit': 1}, ["X", "Y,Z"]), ("X,Y,Z", {'maxsplit': 0}, ["X,Y,Z"]), )) def test_values(value, kwargs, result): cb = CommonBaseTesting(FakeAdapter(), "test") assert cb.values(value, **kwargs) == result def test_global_preprocess_reply(): with pytest.warns(FutureWarning, match="deprecated"): cb = CommonBaseTesting(FakeAdapter(), preprocess_reply=lambda v: v.strip("x")) assert cb.values("x5x") == [5] def test_values_global_preprocess_reply(): cb = CommonBaseTesting(FakeAdapter()) cb.preprocess_reply = lambda v: v.strip("x") assert cb.values("x5x") == [5] def test_binary_values(fake): fake.read_binary_values = fake.read assert fake.binary_values("123") == "123" # Test CommonBase property creators @pytest.mark.parametrize("dynamic", [False, True]) def test_control_doc(dynamic): doc = """ X property """ class Fake(CommonBaseTesting): x = CommonBase.control( "", "%d", doc, dynamic=dynamic ) expected_doc = doc + "(dynamic)" if dynamic else doc assert Fake.x.__doc__ == expected_doc def test_check_errors_not_implemented(fake): with pytest.raises(NotImplementedError): fake.check_errors() def test_check_get_errors_not_implemented(fake): with pytest.raises(NotImplementedError): fake.check_get_errors() def test_control_check_get_errors(fake, caplog): def checking(): fake.error = True return [(7, "some error")] fake.check_get_errors = checking fake.fake_ctrl_errors assert fake.error is True assert caplog.record_tuples[-1] == ( "pymeasure.instruments.common_base", logging.ERROR, "Error received after trying to get a property with the command 'ge': '(7, 'some error')'." ) def test_control_check_get_errors_multiple_errors(fake, caplog): def checking(): fake.error = True return [15, (19, "x")] fake.check_get_errors = checking fake.fake_ctrl_errors assert caplog.record_tuples[-1] == ( "pymeasure.instruments.common_base", logging.ERROR, "Error received after trying to get a property with the command 'ge': '15', '(19, 'x')'." # noqa: E501 ) def test_control_check_get_errors_log_exception(fake, caplog): with pytest.raises(NotImplementedError): fake.fake_ctrl_errors assert caplog.record_tuples[-1] == ( "pymeasure.instruments.common_base", logging.ERROR, "Exception raised while getting a property with the command 'ge': 'Implement it in a subclass.'." # noqa: E501 ) def test_check_set_errors_not_implemented(fake): with pytest.raises(NotImplementedError): fake.check_set_errors() def test_control_check_set_errors(fake, caplog): def checking(): fake.error = True return [(7, "Error!")] fake.check_set_errors = checking fake.fake_ctrl_errors = 7 assert fake.error is True assert caplog.record_tuples[-1] == ( "pymeasure.instruments.common_base", logging.ERROR, "Error received after trying to set a property with the command 'se 7': '(7, 'Error!')'." ) def test_control_check_set_errors_log_exception(fake, caplog): with pytest.raises(NotImplementedError): fake.fake_ctrl_errors = 7 assert caplog.record_tuples[-1] == ( "pymeasure.instruments.common_base", logging.ERROR, "Exception raised while setting a property with the command 'se 7': 'Implement it in a subclass.'." # noqa: E501 ) @pytest.mark.parametrize("dynamic", [False, True]) def test_control_validator(dynamic): class Fake(FakeBase): x = CommonBase.control( "", "%d", "", validator=strict_discrete_set, values=range(10), dynamic=dynamic ) fake = Fake() fake.x = 5 assert fake.read() == '5' fake.x = 5 assert fake.x == 5 with pytest.raises(ValueError): fake.x = 20 @pytest.mark.parametrize("dynamic", [False, True]) def test_control_validator_map(dynamic): class Fake(FakeBase): x = CommonBase.control( "", "%d", "", validator=strict_discrete_set, values=[4, 5, 6, 7], map_values=True, dynamic=dynamic ) fake = Fake() fake.x = 5 assert fake.read() == '1' fake.x = 5 assert fake.x == 5 with pytest.raises(ValueError): fake.x = 20 @pytest.mark.parametrize("dynamic", [False, True]) def test_control_dict_map(dynamic): class Fake(FakeBase): x = CommonBase.control( "", "%d", "", validator=strict_discrete_set, values={5: 1, 10: 2, 20: 3}, map_values=True, dynamic=dynamic ) fake = Fake() fake.x = 5 assert fake.read() == '1' fake.x = 5 assert fake.x == 5 fake.x = 20 assert fake.read() == '3' @pytest.mark.parametrize("dynamic", [False, True]) def test_control_dict_str_map(dynamic): class Fake(FakeBase): x = CommonBase.control( "", "%d", "", validator=strict_discrete_set, values={'X': 1, 'Y': 2, 'Z': 3}, map_values=True, dynamic=dynamic, ) 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_value_not_in_map(fake): fake.parent._buffer = "123" with pytest.raises(KeyError, match="not found in mapped values"): fake.fake_measurement def test_control_invalid_values_get(): class Fake(FakeBase): x = CommonBase.control( "", "%d", "", values=b"abasdfe", map_values=True) with pytest.raises(ValueError, match="Values of type"): Fake().x def test_control_invalid_values_set(): class Fake(FakeBase): x = CommonBase.control( "", "%d", "", values=b"abasdfe", map_values=True) with pytest.raises(ValueError, match="Values of type"): Fake().x = 7 @pytest.mark.parametrize("dynamic", [False, True]) def test_control_process(dynamic): class Fake(FakeBase): x = CommonBase.control( "", "%d", "", validator=strict_range, values=[5e-3, 120e-3], get_process=lambda v: v * 1e-3, set_process=lambda v: v * 1e3, dynamic=dynamic, ) fake = Fake() fake.x = 10e-3 assert fake.read() == '10' fake.x = 30e-3 assert fake.x == 30e-3 @pytest.mark.parametrize("dynamic", [False, True]) def test_control_get_process(dynamic): class Fake(FakeBase): x = CommonBase.control( "", "JUNK%d", "", validator=strict_range, values=[0, 10], get_process=lambda v: int(v.replace('JUNK', '')), dynamic=dynamic, ) fake = Fake() fake.x = 5 assert fake.read() == 'JUNK5' fake.x = 5 assert fake.x == 5 @pytest.mark.parametrize("dynamic", [False, True]) def test_control_preprocess_reply_property(dynamic): # test setting preprocess_reply at property-level class Fake(FakeBase): x = CommonBase.control( "", "JUNK%d", "", preprocess_reply=lambda v: v.replace('JUNK', ''), dynamic=dynamic, 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 CommonBase.values() fake.x = 5 assert fake.x == 5 fake.x = 5 assert type(fake.x) == int def test_control_kwargs_handed_to_values(): """Test that kwargs parameters are handed to `values` method.""" with pytest.warns(FutureWarning, match="Do not use keyword arguments"): class Fake(FakeBase): x = CommonBase.control( "", "JUNK%d", "", preprocess_reply=lambda v: v.replace('JUNK', ''), cast=int, testing=True, ) def values(self, cmd, testing=False, **kwargs): self.testing = testing return super().values(cmd, **kwargs) fake = Fake() fake.x = 5 fake.x assert fake.testing is True def test_control_warning_at_kwargs(): """Test whether a control kwarg raises a warning.""" with pytest.warns(FutureWarning, match="Do not use keyword arguments"): class Fake(CommonBase): x = CommonBase.control("", "", "", testing=True) def test_measurement_warning_at_kwargs(): """Test whether a measurement kwarg raises a warning.""" with pytest.warns(FutureWarning, match="Do not use keyword arguments"): class Fake2(CommonBase): x2 = CommonBase.measurement("", "", testing=True) def test_control_parameters_for_values(): """Test how to hand a parameter to `values` method.""" class Fake(FakeBase): x = CommonBase.control( "", "JUNK%d", "", preprocess_reply=lambda v: v.replace('JUNK', ''), cast=int, values_kwargs={'testing': True}, ) def values(self, cmd, testing=False, **kwargs): self.testing = testing return super().values(cmd, **kwargs) fake = Fake() fake.x = 5 fake.x assert fake.testing is True def test_measurement_parameters_for_values(): """Test how to hand a parameter to `values` method.""" class Fake(FakeBase): x = CommonBase.measurement( "JUNK%d", "", preprocess_reply=lambda v: v.replace('JUNK', ''), cast=int, values_kwargs={'testing': True}, ) def values(self, cmd, testing=False, **kwargs): self.testing = testing return super().values(cmd, **kwargs) fake = Fake() fake.write("5") fake.x assert fake.testing is True @pytest.mark.parametrize("cast, expected", ((float, 5.5), (ureg.Quantity, ureg.Quantity(5.5)), (str, "5.5"), (lambda v: int(float(v)), 5) )) def test_measurement_cast(cast, expected): class Fake(CommonBaseTesting): x = CommonBase.measurement( "x", "doc", cast=cast) with expected_protocol(Fake, [("x", "5.5")]) as instr: assert instr.x == expected def test_measurement_cast_int(): class Fake(CommonBaseTesting): def __init__(self, adapter, **kwargs): super().__init__(adapter, "test", **kwargs) x = CommonBase.measurement( "x", "doc", cast=int) with expected_protocol(Fake, [("x", "5")]) as instr: y = instr.x assert y == 5 assert type(y) is int def test_measurement_unitful_property(): class Fake(CommonBaseTesting): def __init__(self, adapter, **kwargs): super().__init__(adapter, "test", **kwargs) x = CommonBase.measurement( "x", "doc", get_process=lambda v: ureg.Quantity(v, ureg.m)) with expected_protocol(Fake, [("x", "5.5")]) as instr: y = instr.x assert y.m_as(ureg.m) == 5.5 @pytest.mark.parametrize("dynamic", [False, True]) def test_measurement_dict_str_map(dynamic): class Fake(FakeBase): x = CommonBase.measurement( "", "", values={'X': 1, 'Y': 2, 'Z': 3}, map_values=True, dynamic=dynamic, ) 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_measurement_set(fake): with pytest.raises(LookupError, match="Property can not be set."): fake.fake_measurement = 7 def test_setting_get(fake): with pytest.raises(LookupError, match="Property can not be read."): fake.fake_setting @pytest.mark.parametrize("dynamic", [False, True]) def test_setting_process(dynamic): class Fake(FakeBase): x = CommonBase.setting( "OUT %d", "", set_process=lambda v: int(bool(v)), dynamic=dynamic, ) fake = Fake() fake.x = False assert fake.read() == 'OUT 0' fake.x = 2 assert fake.read() == 'OUT 1' @pytest.mark.parametrize("dynamic", [False, True]) def test_control_multivalue(dynamic): class Fake(FakeBase): x = CommonBase.control( "", "%d,%d", "", dynamic=dynamic, ) fake = Fake() fake.x = (5, 6) assert fake.read() == '5,6' @pytest.mark.parametrize( 'set_command, given, expected, dynamic', [("%d", 5, 5, False), ("%d", 5, 5, True), ("%d, %d", (5, 6), [5, 6], False), # input has to be a tuple, not a list ("%d, %d", (5, 6), [5, 6], True), # input has to be a tuple, not a list ]) def test_FakeBase_control(set_command, given, expected, dynamic): """FakeBase's custom simple control needs to process values correctly. """ class Fake(FakeBase): x = FakeBase.control( "", set_command, "", dynamic=dynamic, ) fake = Fake() fake.x = given assert fake.x == expected def test_dynamic_property_unchanged_by_inheritance(): generic = FakeBase() extended = ExtendedBase() generic.fake_ctrl = 50 assert generic.fake_ctrl == 10 extended.fake_ctrl = 50 assert extended.fake_ctrl == 10 generic.fake_setting = 50 assert generic.read() == '10' extended.fake_setting = 50 assert extended.read() == '10' generic.write('1') assert generic.fake_measurement == 'X' extended.write('1') assert extended.fake_measurement == 'X' def test_dynamic_property_strict_raises(): # Tests also that dynamic properties can be changed at class level. strict = StrictExtendedBase() with pytest.raises(ValueError): strict.fake_ctrl = 50 with pytest.raises(ValueError): strict.fake_setting = 50 def test_dynamic_property_values_update_in_subclass(): newrange = NewRangeBase() newrange.fake_ctrl = 50 assert newrange.fake_ctrl == 20 newrange.fake_setting = 50 assert newrange.read() == '20' newrange.write('4') assert newrange.fake_measurement == 'X' def test_dynamic_property_values_update_in_instance(fake): fake.fake_ctrl_values = (0, 33) fake.fake_ctrl = 50 assert fake.fake_ctrl == 33 fake.fake_setting_values = (0, 33) fake.fake_setting = 50 assert fake.read() == '33' fake.fake_measurement_values = {'X': 7} fake.write('7') assert fake.fake_measurement == 'X' def test_dynamic_property_values_update_in_one_instance_leaves_other_unchanged(): generic1 = FakeBase() generic2 = FakeBase() generic1.fake_ctrl_values = (0, 33) generic1.fake_ctrl = 50 generic2.fake_ctrl = 50 assert generic1.fake_ctrl == 33 assert generic2.fake_ctrl == 10 def test_dynamic_property_reading_special_attributes_forbidden(fake): with pytest.raises(AttributeError): fake.fake_ctrl_validator def test_dynamic_property_with_inheritance(): inst = ExtendedBase() # Test for inherited attribute with pytest.raises(AttributeError): inst.fake_ctrl_validator # Test for new attribute with pytest.raises(AttributeError): inst.fake_ctrl2_validator def test_dynamic_property_values_defined_at_superclass_level(): """Test whether a dynamic property can be changed a superclass level""" inst = StrictExtendedBase() # Test whether the change of values from (1, 10) to (5, 20) succeeded: inst.fake_ctrl2 = 17 # should raise an error if change unsuccessful with pytest.raises(ValueError): inst.fake_ctrl2 = 2 # should not raise an error if change unsuccessful ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/test_connection_configuration.py0000644000175100001770000002224714623331163025460 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 serial from pymeasure.adapters import SerialAdapter, VISAAdapter from pymeasure.instruments import Instrument # As an instrument contributor, I want to define default connection settings for my # instrument with a minimum of boiler-plate. These settings should be easily # user-overridable if appropriate without having to change the code/implementation # of my instrument. class MultiprotocolInstrument(Instrument): """Test instrument for testing the configuration and use of multiple connection methods. The instrument support multiple transports methods(serial, TCP/IP, GPIB). In case of serial connections, the default baud rate is low (2400), but configurable in the device, so it can use faster communication if desired. Two examples in our instruments are AMI430, Keithley2260B (for TCP/IP, there are more with GPIB capability) It also has an attribute to configure a non-serial (gpib) connection. This demonstrates that there may be attributes that are not valid for a serial connection, only others. """ def __init__(self, adapter, name="Instrument with multiple connection methods", baud_rate=2400, **kwargs): # - baud_rate is placed in the signature if it's expected to be modified by the user # - baud_rate is only valid for the asrl protocol # - enable_repeat_addressing is only for gpib and always False for this device # - timeout is the same for all pyvisa protocols, using setdefault it is user-modifiable # without cluttering the signature: kwargs.setdefault('timeout', 1500) super().__init__(adapter, name=name, gpib=dict(enable_repeat_addressing=False, read_termination='\r'), asrl={'baud_rate': baud_rate, 'read_termination': '\r\n'}, **kwargs, # all others/generally valid kwargs ) # The defined tests use the pyvisa-sim simulated connections to avoid the need for a connected # instrument. The argument handling and connection creation happens in pyvisa just like for a # real instrument, through. def test_serial_default_settings(): """As a user, I want to simply connect to an instrument using default settings""" instr = MultiprotocolInstrument(adapter='ASRL1::INSTR', visa_library='@sim') assert instr.adapter.connection.baud_rate == 2400 def test_serial_custom_baud_rate_is_set(): """As a user, I want to easily override default settings to fit my needs""" instr = MultiprotocolInstrument(adapter='ASRL1::INSTR', baud_rate=115200, visa_library='@sim') assert instr.adapter.connection.baud_rate == 115200 def test_connections_use_tcpip(): """As a user, I want to be free to choose which connection to use (e.g. serial-over RS-232, USB, TCP/IP) should an instrument support more than one """ # here, baud_rate is invalid for pyvisa, but is a default kwarg anyway instr = MultiprotocolInstrument(adapter='TCPIP::localhost:1111::INSTR', visa_library='@sim') # method specific to pyvisa TCPIPInstrument assert hasattr(instr.adapter.connection, 'control_ren') def test_connections_use_gpib(): """As a user, I want to be free to choose which connection to use (e.g. serial-over RS-232, USB, TCP/IP) should an instrument support more than one """ # here, baud_rate is invalid for pyvisa, but is a default kwarg anyway instr = MultiprotocolInstrument(adapter='GPIB::8::INSTR', visa_library='@sim') # attribute specific to pyvisa GPIBInstrument assert instr.adapter.connection.enable_repeat_addressing is False def test_use_separate_SerialAdapter(): """As a user, I want to be able to supply a self-generated Adapter instance (e.g. to enable serial connection sharing over RS-485/-422) """ # note: baudrate, not baud_rate, as this goes to pyserial ser = SerialAdapter(serial.serial_for_url("loop://", baudrate=4800)) instr = MultiprotocolInstrument(ser) # don't need visa_library here as this does not go to pyvisa, kwargs are ignored if provided assert instr.adapter.connection.baudrate == 4800 def test_separate_adapter_discards_additional_args(): """When passing in a separate Adapter instance, all connection-specific args and settings are ignored. """ ser = SerialAdapter(serial.serial_for_url("loop://")) instr = MultiprotocolInstrument(ser, baud_rate=1234, wrong_kwarg='fizzbuzz') assert instr.adapter.connection.baudrate == 9600 # default of serial def test_use_separate_VISAAdapter(): """As a user, I want to be able to supply my own VISAAdpter with my settings """ # User can use their own VISAAdapter ser = VISAAdapter('ASRL1::INSTR', baud_rate=1200, visa_library='@sim') instr = MultiprotocolInstrument(ser) # don't need visa_library here as this does not go to pyvisa, kwargs are ignored if provided assert instr.adapter.connection.baud_rate == 1200 def test_incorrect_arg_is_flagged(): """As a user or instrument contributor that used an incorrect/invalid (kw)arg, I want to be alerted to that fact. """ with pytest.raises(ValueError, match='bitrate'): _ = MultiprotocolInstrument(adapter='ASRL1::INSTR', bitrate=1234, visa_library='@sim') def test_incorrect_interface_type_is_flagged(): """As a user or instrument contributor that used an incorrect/invalid interface type, I want to be alerted to that fact. """ class WrongInterfaceInstrument(Instrument): def __init__(self, adapter, name="Instrument with incorrect interface name", **kwargs): super().__init__(adapter, name=name, arsl={'read_termination': '\r\n'}, # typo here **kwargs, ) with pytest.raises(ValueError, match='arsl'): _ = WrongInterfaceInstrument(adapter='ASRL1::INSTR', visa_library='@sim') def test_improper_arg_is_flagged(): """As a user or instrument contributor that used a kwarg that is inappropriate for the present connection, I want to be alerted to that fact. """ with pytest.raises(ValueError, match='enable_repeat_addressing'): _ = MultiprotocolInstrument(adapter='ASRL1::INSTR', enable_repeat_addressing=True, visa_library='@sim') def test_common_kwargs_are_retained(): instr1 = MultiprotocolInstrument(adapter='ASRL1::INSTR', visa_library='@sim') assert instr1.adapter.connection.timeout == 1500 instr2 = MultiprotocolInstrument(adapter='GPIB::8::INSTR', visa_library='@sim') assert instr2.adapter.connection.timeout == 1500 def test_distinct_interface_specific_defaults(): """As an instrument implementor, I can easily prescribe default settings that are different between interfaces. """ instr1 = MultiprotocolInstrument(adapter='ASRL1::INSTR', visa_library='@sim') assert instr1.adapter.connection.read_termination == '\r\n' instr2 = MultiprotocolInstrument(adapter='GPIB::8::INSTR', visa_library='@sim') assert instr2.adapter.connection.read_termination == '\r' def test_modified_non_signature_kwargs_are_retained(): instr1 = MultiprotocolInstrument(adapter='ASRL1::INSTR', timeout=3000, visa_library='@sim') assert instr1.adapter.connection.timeout == 3000 instr2 = MultiprotocolInstrument(adapter='GPIB::8::INSTR', timeout=3000, visa_library='@sim') assert instr2.adapter.connection.timeout == 3000 def test_override_interface_specific_defaults(): """As as user, I want to be able to override interface-specific hardcoded defaults. """ instr1 = MultiprotocolInstrument(adapter='ASRL1::INSTR', read_termination='\r', visa_library='@sim') assert instr1.adapter.connection.read_termination == '\r' instr2 = MultiprotocolInstrument(adapter='GPIB::8::INSTR', enable_repeat_addressing=True, visa_library='@sim') assert instr2.adapter.connection.enable_repeat_addressing is True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/test_generic_types.py0000644000175100001770000000661614623331163023234 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.adapters import ProtocolAdapter from pymeasure.instruments.generic_types import SCPIMixin, SCPIUnknownMixin from pymeasure.instruments import Instrument class Test_SCPIMixin: class SCPIInstrument(SCPIMixin, Instrument): pass def test_init(self): inst = self.SCPIInstrument(ProtocolAdapter(), "test") assert inst.SCPI is False # should not be set by the new init @pytest.mark.parametrize("method, write, reply", ( ("id", "*IDN?", "xyz, abc"), ("complete", "*OPC?", "1"), ("status", "*STB?", "189"), ("options", "*OPT?", "a9"), )) def test_SCPI_properties(self, method, write, reply): with expected_protocol( self.SCPIInstrument, [(write, reply)], name="test") as inst: assert getattr(inst, method) == reply def test_next_error(self): with expected_protocol( self.SCPIInstrument, [("SYST:ERR?", '-100,"Command error"')], name="test") as inst: assert inst.next_error == [-100, '"Command error"'] @pytest.mark.parametrize("method, write", (("clear", "*CLS"), ("reset", "*RST"), )) def test_SCPI_write_commands(self, method, write): with expected_protocol( self.SCPIInstrument, [(write, None)], name="test") as inst: getattr(inst, method)() def test_check_errors(self): with expected_protocol( self.SCPIInstrument, [("SYST:ERR?", '-100,"Command error"'), ("SYST:ERR?", '-222,"Data out of range"'), ("SYST:ERR?", '0,"No error"'), ], name="test") as inst: assert inst.check_errors() == [[-100, '"Command error"'], [-222, '"Data out of range"']] def test_SCPIunknownMixin(): class SCPIunknownInstrument(SCPIUnknownMixin, Instrument): pass with pytest.warns(FutureWarning): inst = SCPIunknownInstrument(ProtocolAdapter(), "test") assert inst.SCPI is False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/test_instrument.py0000644000175100001770000002426314623331163022602 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 unittest import mock import pytest from pymeasure.test import expected_protocol from pymeasure.instruments import Instrument, Channel from pymeasure.adapters import FakeAdapter, ProtocolAdapter from pymeasure.instruments.fakes import FakeInstrument from pymeasure.instruments.validators import truncated_range class GenericInstrument(FakeInstrument): # Use truncated_range as this easily lets us test for the range boundaries fake_ctrl = Instrument.control( "", "%d", "docs", validator=truncated_range, values=(1, 10), dynamic=True, ) fake_setting = Instrument.setting( "%d", "docs", validator=truncated_range, values=(1, 10), dynamic=True, ) fake_measurement = Instrument.measurement( "", "docs", values={'X': 1, 'Y': 2, 'Z': 3}, map_values=True, dynamic=True, ) @pytest.fixture() def generic(): return GenericInstrument() class GenericChannel(Channel): # Use truncated_range as this easily lets us test for the range boundaries fake_ctrl = Instrument.control( "C{ch}:control?", "C{ch}:control %d", "docs", validator=truncated_range, values=(1, 10), dynamic=True, ) fake_setting = Instrument.setting( "C{ch}:setting %d", "docs", validator=truncated_range, values=(1, 10), dynamic=True, ) fake_measurement = Instrument.measurement( "C{ch}:measurement?", "docs", values={'X': 1, 'Y': 2, 'Z': 3}, map_values=True, dynamic=True, ) special_control = Instrument.control( "SOUR{ch}:special?", "OUTP{ch}:special %s", """A special control with different channel specifiers for get and set.""", cast=str, ) class ChannelInstrument(Instrument): def __init__(self, adapter, name="ChannelInstrument", **kwargs): super().__init__(adapter, name, **kwargs) self.add_child(GenericChannel, "A") self.add_child(GenericChannel, "B") def test_fake_instrument(): fake = FakeInstrument() fake.write("Testing") assert fake.read() == "Testing" assert fake.read() == "" assert fake.values("5") == [5] class Test_includeSCPI_parameter: def test_not_defined_includeSCPI_raises_warning(self): with pytest.warns(FutureWarning) as record: Instrument(name="test", adapter=ProtocolAdapter()) msg = str(record[0].message) assert msg == ("It is deprecated to specify `includeSCPI` implicitly, use " "`includeSCPI=False` or inherit the `SCPIMixin` class instead.") def test_not_defined_includeSCPI_is_interpreted_as_true(self): inst = Instrument(name="test", adapter=ProtocolAdapter()) assert inst.SCPI is True @pytest.mark.parametrize("adapter", (("COM1", 87, "USB"))) def test_init_visa(adapter): Instrument(adapter, "def", visa_library="@sim") pass # Test that no error is raised @pytest.mark.xfail() # I do not know, when this error is raised def test_init_visa_fail(): with pytest.raises(Exception, match="Invalid adapter"): Instrument("abc", "def", visa_library="@xyz") def test_init_includeSCPI_implicit_warning(): with pytest.warns(FutureWarning, match="includeSCPI"): Instrument("COM1", "def", visa_library="@sim") def test_init_includeSCPI_explicit_warning(): with pytest.warns(FutureWarning, match="includeSCPI"): Instrument("COM1", "def", visa_library="@sim", includeSCPI=True) def test_global_preprocess_reply(): with pytest.warns(FutureWarning, match="deprecated"): inst = Instrument(FakeAdapter(), "name", preprocess_reply=lambda v: v.strip("x")) assert inst.values("x5x") == [5] def test_instrument_in_context(): with Instrument("abc", "def", visa_library="@sim") as instr: pass assert instr.isShutdown is True def test_with_statement(): """ Test the with-statement-behaviour of the instruments. """ with FakeInstrument() as fake: # Check if fake is an instance of FakeInstrument assert isinstance(fake, FakeInstrument) # Check whether the shutdown function is already called assert fake.isShutdown is False # Check whether the shutdown function is called upon assert fake.isShutdown is True class TestInstrumentCommunication: @pytest.fixture() def instr(self): a = mock.MagicMock(return_value="5") return Instrument(a, "abc") def test_write(self, instr): instr.write("abc") assert instr.adapter.method_calls == [mock.call.write('abc')] def test_read(self, instr): instr.read() assert instr.adapter.method_calls == [mock.call.read()] def test_write_bytes(self, instr): instr.write_bytes(b"abc") assert instr.adapter.method_calls == [mock.call.write_bytes(b"abc")] def test_read_bytes(self, instr): instr.read_bytes(5) assert instr.adapter.method_calls == [mock.call.read_bytes(5)] def test_write_binary_values(self, instr): instr.write_binary_values("abc", [5, 6, 7]) assert instr.adapter.method_calls == [mock.call.write_binary_values("abc", [5, 6, 7])] class TestWaiting: @pytest.fixture() def instr(self): class Faked(Instrument): def wait_for(self, query_delay=None): self.waited = query_delay return Faked(ProtocolAdapter(), name="faked") def test_waiting(self): instr = Instrument(ProtocolAdapter(), "faked") stop = time.perf_counter() + 100 instr.wait_for(0.1) assert time.perf_counter() < stop def test_ask_calls_wait(self, instr): instr.adapter.comm_pairs = [("abc", "resp")] instr.ask("abc") assert instr.waited is None def test_ask_calls_wait_with_delay(self, instr): instr.adapter.comm_pairs = [("abc", "resp")] instr.ask("abc", query_delay=10) assert instr.waited == 10 def test_binary_values_calls_wait(self, instr): instr.adapter.comm_pairs = [("abc", "abcdefgh")] instr.binary_values("abc") assert instr.waited is None @pytest.mark.parametrize("method, write, reply", (("id", "*IDN?", "xyz"), ("complete", "*OPC?", "1"), ("status", "*STB?", "189"), ("options", "*OPT?", "a9"), )) def test_SCPI_properties(method, write, reply): with expected_protocol( Instrument, [(write, reply)], name="test") as instr: assert getattr(instr, method) == reply @pytest.mark.parametrize("method, write", (("clear", "*CLS"), ("reset", "*RST") )) def test_SCPI_write_commands(method, write): with expected_protocol( Instrument, [(write, None)], name="test") as instr: getattr(instr, method)() def test_instrument_check_errors(): with expected_protocol( Instrument, [("SYST:ERR?", "17,funny stuff"), ("SYST:ERR?", "0")], name="test") as instr: assert instr.check_errors() == [[17, "funny stuff"]] @pytest.mark.parametrize("method", ("id", "complete", "status", "options", "clear", "reset", "check_errors" )) def test_SCPI_false_raises_errors(method): with pytest.raises(NotImplementedError): getattr(Instrument(FakeAdapter(), "abc", includeSCPI=False), method)() # Channel class TestMultiFunctionality: """Test the usage of children for different functionalities.""" class SomeFunctionality(Channel): """This Functionality needs a prepended `id`.""" def insert_id(self, command, **kwargs): return f"{self.id}:{command}" voltage = Channel.control("Volt?", "Volt %f", "Set voltage in Volts.") class InstrumentWithFunctionality(ChannelInstrument): """Instrument with some functionality.""" def __init__(self, adapter, **kwargs): super().__init__(adapter, **kwargs) self.add_child(TestMultiFunctionality.SomeFunctionality, "X", collection="functions", prefix="f_") def test_functionality_dict(self): inst = TestMultiFunctionality.InstrumentWithFunctionality(ProtocolAdapter()) assert isinstance(inst.functions["X"], TestMultiFunctionality.SomeFunctionality) assert inst.functions["X"] == inst.f_X def test_functions_voltage_getter(self): with expected_protocol( TestMultiFunctionality.InstrumentWithFunctionality, [("X:Volt?", "123.456")] ) as inst: assert inst.f_X.voltage == 123.456 def test_functions_voltage_setter(self): with expected_protocol( TestMultiFunctionality.InstrumentWithFunctionality, [("X:Volt 123.456000", None)] ) as inst: inst.f_X.voltage = 123.456 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/test_validators.py0000644000175100001770000001016414623331163022535 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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): 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): strict_discrete_range(5.1, range(5), 0.001) with pytest.raises(ValueError): strict_discrete_range(5.01, range(5), 0.1) with pytest.raises(ValueError): 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): strict_discrete_set(5.1, range(10)) with pytest.raises(ValueError): 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) values = [["ON", "OFF"], range(10)] assert tst_validator(5, values) == 5 assert tst_validator(5.1, values) == 5.1 assert tst_validator("ON", values) == "ON" with pytest.raises(ValueError): tst_validator("OUT", values) with pytest.raises(ValueError): tst_validator(20, values) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/texio/0000755000175100001770000000000014623331176020106 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/texio/test_texioPSW360L30.py0000644000175100001770000000625114623331163023751 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.texio.texioPSW360L30 import TexioPSW360L30 def test_name(): texio = TexioPSW360L30(adapter=None) assert "TEXIO" in texio.name.upper() def test_output_enabled(): with expected_protocol( TexioPSW360L30, [(b"OUTPut?", b"1"), (b"OUTPut 1", None), (b"OUTPut?", b"0"), (b"OUTPut 0", None), ], ) as inst: assert inst.output_enabled is True inst.output_enabled = True assert inst.output_enabled is False inst.output_enabled = False def test_current_limit(): with expected_protocol( TexioPSW360L30, [(b":SOUR:CURR?", b"+5.120"), (b":SOUR:CURR 5", None) ], ) as inst: assert inst.current_limit == 5.12 inst.current_limit = 5 def test_voltage_setpoint(): with expected_protocol( TexioPSW360L30, [(b":SOUR:VOLT?", b"+10.000"), (b":SOUR:VOLT 10", None) ], ) as inst: assert inst.voltage_setpoint == 10. inst.voltage_setpoint = 10 def test_measure_power(): with expected_protocol( TexioPSW360L30, [(b":MEAS:POW?", b"+51.200"), ], ) as inst: assert inst.power == 51.2 def test_measure_voltage(): with expected_protocol( TexioPSW360L30, [(b":MEAS:VOLT?", b"+10.000"), ], ) as inst: assert inst.voltage == 10. def test_measure_current(): with expected_protocol( TexioPSW360L30, [(b":MEAS:CURR?", b"+5.120"), ], ) as inst: assert inst.current == 5.12 def test_applied(): with expected_protocol( TexioPSW360L30, [(b":APPly?", b"+5.050, +1.100"), (b":APPly 5.05,1.1", None) ], ) as inst: assert inst.applied == [5.05, 1.1] inst.applied = (5.05, 1.1) if __name__ == '__main__': pytest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/texio/test_texioPSW360L30_with_device.py0000644000175100001770000000773614623331163026334 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pytest from pymeasure.instruments.texio.texioPSW360L30 import TexioPSW360L30 pytest.skip('Only work with connected hardware', allow_module_level=True) class TestTexioPSW360L30: """ Unit tests for TEXIO PSW-360L30 class. This test suite, needs the following setup to work properly: - A TEXIO PSW-360L30 device should be connected to the computer; - The device's address must be set in the RESOURCE constant; """ ################################################## # TEXIO PSW-360L30 device address goes here: RESOURCE = "TCPIP::192.168.10.119::2268::SOCKET" ################################################## INSTR = TexioPSW360L30(RESOURCE) ######################### # PARAMETRIZATION CASES # ######################### CURRENT_LIMIT = [0.1, 0.5, 1] VOLTAGE_SETPOINT = [1, 2, 3, 4, 5] @pytest.fixture def instr(self): self.INSTR.reset() return self.INSTR @pytest.mark.parametrize("case", CURRENT_LIMIT) def test_current_limit_no_output(self, instr, case): instr.current_limit = case assert instr.current_limit == case @pytest.mark.parametrize("case", VOLTAGE_SETPOINT) def test_voltage_setpoint_no_output(self, instr, case): instr.voltage_setpoint = case assert instr.voltage_setpoint == case @pytest.mark.parametrize("voltage_setpoint", VOLTAGE_SETPOINT) @pytest.mark.parametrize("current_limit", CURRENT_LIMIT) def test_everything_without_apply(self, instr, voltage_setpoint, current_limit): instr.current_limit = current_limit instr.voltage_setpoint = voltage_setpoint instr.output_enabled = True time.sleep(1) assert instr.output_enabled is True assert instr.voltage_setpoint == voltage_setpoint assert instr.current_limit == current_limit assert instr.voltage == pytest.approx(voltage_setpoint, abs=0.1) assert instr.current == pytest.approx(0, abs=0.1) assert instr.power == pytest.approx(0, abs=0.1) instr.output_enabled = False assert instr.output_enabled is False @pytest.mark.parametrize("voltage_setpoint", VOLTAGE_SETPOINT) @pytest.mark.parametrize("current_limit", CURRENT_LIMIT) def test_everything_with_apply(self, instr, voltage_setpoint, current_limit): instr.applied = (voltage_setpoint, current_limit) instr.output_enabled = True time.sleep(1) assert instr.output_enabled is True assert instr.voltage_setpoint == voltage_setpoint assert instr.current_limit == current_limit assert instr.voltage == pytest.approx(voltage_setpoint, abs=0.1) assert instr.current == pytest.approx(0, abs=0.1) assert instr.power == pytest.approx(0, abs=0.1) instr.output_enabled = False assert instr.output_enabled is False ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/thyracont/0000755000175100001770000000000014623331176020771 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/thyracont/test_smartline_v1.py0000644000175100001770000000607314623331163025010 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.thyracont import SmartlineV1 from pymeasure.instruments.thyracont.smartline_v1 import calculate_checksum def test_calculate_checksum_basics(): for i in range(0, 64): assert (i + 64) == ord(calculate_checksum(chr(i))) for i in range(64, 128): assert i == ord(calculate_checksum(chr(i))) @pytest.mark.parametrize("msg, chksum", ( ("001T", "e"), # command from SmartlineV1 ("001M982122", "V"), # msg from SmartlineV1 ("0010MV00", "D"), # command from SmartlineV2 ("0011MV079.734e2", "h"), # msg from SmartlineV2 )) def test_calculate_checksum(msg, chksum): assert chksum == calculate_checksum(msg) def test_pressure(): """Verify the communication of the pressure getter.""" with expected_protocol( SmartlineV1, [("001M^", "001M982122V"), ], ) as inst: assert inst.pressure == pytest.approx(982.1) def test_device_type(): """Verify the communication of the Instruments type getter.""" with expected_protocol( SmartlineV1, [("001Te", "001TVSM207t"), ], ) as inst: assert inst.device_type == "VSM207" def test_cathode_enable(): """Verify the communication of the hot/cold cathode control.""" with expected_protocol( SmartlineV1, [("001IZ", "001I1K"), ], ) as inst: assert inst.cathode_enabled is True def test_cathode_enable_error(): """Verify the raised error in case of a set error.""" with expected_protocol( SmartlineV1, [("001i0j", "001NO_DEF\\"), ], ) as inst: with pytest.raises(ConnectionError): inst.cathode_enabled = False def test_display_unit(): """Verify the communication of the unit property.""" with expected_protocol( SmartlineV1, [("001Uf", "001U000000F"), ], ) as inst: assert inst.display_unit == "mbar" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/thyracont/test_smartline_v2.py0000644000175100001770000002035214623331163025005 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 import Instrument from pymeasure.test import expected_protocol # File to test from pymeasure.instruments.thyracont.smartline_v2 import (SmartlineV2, HotCathode, Pirani, calculate_checksum, Piezo, Sources) SmartlineV2.hot_cathode = Instrument.ChannelCreator(cls=HotCathode) SmartlineV2.pirani = Instrument.ChannelCreator(cls=Pirani) SmartlineV2.piezo = Instrument.ChannelCreator(cls=Piezo) @pytest.mark.parametrize("buffer, read", ( (b"0011MV079.734e2h", "9.734e2"), # all correct (b"0011\x00MV079.734e2h", "9.734e2"), # zero byte (b"00\xf911MV079.734e2h", "9.734e2"), # byte above 127 (b"0011MV079.734e2h\r", "9.734e2"), # term char still there )) def test_read_handles_errors(buffer, read): with expected_protocol(SmartlineV2, [(None, buffer)], ) as inst: assert inst.read() == read @pytest.mark.parametrize( "address, pars, message, answer", [ (1, (0, "MR"), "0010MR00@", "0011MR11H1.2e3L1e-4w"), (1, (0, "MV"), "0010MV00D", "0011MV079.734e2h"), (2, (2, "R1", "T0.1F1.5"), "0022R108T0.1F1.5l", "0023R100h"), (2, (2, "DU", "mbar"), "0022DU04mbarc", "0023DU00~"), (1, (2, "AH", 981.5), "0012AH05981.5v", "0013AH00m"), ], ) def test_ask_manually(address, pars, message, answer): with expected_protocol(SmartlineV2, [(message, answer)], address=address) as inst: inst.ask_manually(*pars) @pytest.mark.parametrize("message, result", ( ("0010MV00", "D"), ("0022DU04mbar", "c"), ("0012AH05981.5", "v"), )) def test_calculateChecksum(message, result): assert calculate_checksum(message) == result # Test communication def test_transmitter_error(): with expected_protocol(SmartlineV2, [("0010MV00D", "0017MV00ERROR1X")]) as inst: with pytest.raises(ConnectionError) as exc: inst.pressure assert exc.value.args[0] == "Sensor defect or stacked out." def test_wrong_answer_command(): with expected_protocol(SmartlineV2, [("0010MV00D", "0011MX00ERROR1X")]) as inst: with pytest.raises(ConnectionError, match="Wrong response to MV: '0011MX00ERROR1X'."): inst.pressure def test_wrong_answer_checksum(): with expected_protocol(SmartlineV2, [("0010MV00D", "0011MV00ERROR1X")]) as inst: with pytest.raises(ConnectionError, match="Response checksum is wrong."): inst.pressure # Test properties def test_range(): with expected_protocol(SmartlineV2, [("0010MR00@", "0011MR11H1.2e3L1e-4w")]) as inst: assert inst.range == [1.2e3, 1e-4] def test_pressure(): with expected_protocol(SmartlineV2, [("0010MV00D", "0011MV079.734e2h")]) as inst: assert inst.pressure == 973.4 def test_pressure_over_range(): with expected_protocol(SmartlineV2, [("0010MV00D", "0011MV02ORh")]) as inst: assert inst.pressure == float("inf") def test_pressure_under_range(): with expected_protocol(SmartlineV2, [("0010MV00D", "0011MV02URn")]) as inst: assert inst.pressure == 0 def test_display_unit_getter(): with expected_protocol(SmartlineV2, [("0010DU00z", "0011DU03mbar`")]) as inst: assert inst.display_unit == "mbar" def test_display_unit_setter(): with expected_protocol(SmartlineV2, [("0022DU04mbarc", "0023DU00~")]) as inst: inst.address = 2 inst.display_unit = "mbar" def test_display_unit_setter_wrong(): with expected_protocol(SmartlineV2, []) as inst: with pytest.raises(ValueError): inst.display_unit = "abc" def test_display_orientation_getter(): with expected_protocol(SmartlineV2, [("0010DO00t", "0011DO010f")]) as inst: assert inst.display_orientation == "top" def test_display_orientation_setter(): with expected_protocol(SmartlineV2, [("0012DO010g", "0013DO00w")]) as inst: inst.display_orientation = "top" def test_display_data_getter(): with expected_protocol(SmartlineV2, [("0010DD00i", "0011DD017b")]) as inst: assert inst.display_data == Sources.RELATIVE def test_display_data_setter(): with expected_protocol(SmartlineV2, [("0012DD017c", "0013DD00l")]) as inst: inst.display_data = Sources.RELATIVE def test_set_high(): with expected_protocol(SmartlineV2, [("0012AH041000q", "0013AH00m")]) as inst: inst.set_high(1000) def test_set_low(): with expected_protocol(SmartlineV2, [("0012AL0250W", "0013AL00q")]) as inst: inst.set_low(50) def test_device_type(): with expected_protocol(SmartlineV2, [("0010TD00y", "0011TD06VSR205R")]) as inst: assert inst.device_type == "VSR205" def test_operating_hours(): with expected_protocol(SmartlineV2, [("0010OH00x", "0011OH0285h")], ) as inst: assert inst.operating_hours == 21.25 def test_operating_hours_cathod(): """For a cathode transmitter also cathode hours are returned.""" with expected_protocol(SmartlineV2, [("0010OH00x", "0011OH0542C36P")], ) as inst: assert inst.operating_hours == [10.5, 9] # Test methods def test_set_default_sensor_transition(): with expected_protocol(SmartlineV2, [(b'0012ST011|', "0013ST00K")], ) as inst: inst.set_default_sensor_transition() def test_set_direct_sensor_transition(): with expected_protocol(SmartlineV2, [(b'0012ST02D5E', "0013ST00K")], ) as inst: inst.set_direct_sensor_transition(5) def test_set_continuous_sensor_transition(): with expected_protocol(SmartlineV2, [(b'0012ST04F1T2K', "0013ST00K")], ) as inst: inst.set_continuous_sensor_transition(1, 2) # Pirani tests def test_pressure_pirani(): with expected_protocol(SmartlineV2, [("0010M100_", "0011M1079.734e2C")]) as inst: assert inst.pirani.pressure == 973.4 def test_pirani_statistics(): with expected_protocol(SmartlineV2, [("0010PM011p", b'0011PM06W0A382j')], ) as inst: assert inst.pirani.statistics == (0, 95.5) # Piezo tests def test_pressure_piezo(): with expected_protocol(SmartlineV2, [("0010M200`", "0011M2079.734e2D")]) as inst: assert inst.piezo.pressure == 973.4 # Hot Cathode tests def test_pressure_hot_cathode(): with expected_protocol(SmartlineV2, [("0010M300a", "0011M3089.274e-8x")]) as inst: assert inst.hot_cathode.pressure == 9.274e-8 def test_filament_mode_getter(): with expected_protocol(SmartlineV2, [("0010FC00j", "0011FC011]")]) as inst: assert inst.hot_cathode.filament_mode == "Filament1" def test_filament_mode_setter(): with expected_protocol(SmartlineV2, [("0012FC012_", "0013FC00m")]) as inst: inst.hot_cathode.filament_mode = "Filament2" def test_filament_status(): with expected_protocol(SmartlineV2, [("0010FS00z", "0011FS011m")]) as inst: assert inst.hot_cathode.filament_status == "Filament 1 defective" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/toptica/0000755000175100001770000000000014623331176020421 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/toptica/test_ibeamsmart.py0000644000175100001770000000776614623331163024172 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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.test import expected_protocol from pymeasure.instruments.toptica import IBeamSmart # Note: This communication does not contain the first two device responses, as they are # ignored due to `adapter.flush_read_buffer()`. # The device communication depends on previous commands and whether the device power cycled. init_comm = [("echo off", None), ("prom off", None), ("talk usual", ""), (None, "[OK]")] def test_init(): with expected_protocol(IBeamSmart, init_comm, ): pass # verify init def test_version(): with expected_protocol( IBeamSmart, init_comm + [("ver", ""), (None, "iBPs-001A01-05"), (None, "[OK]")], ) as inst: assert inst.version == "iBPs-001A01-05" def test_power(): with expected_protocol( IBeamSmart, init_comm + [("sh pow", ""), (None, "PIC = 000001 uW "), (None, "[OK]")], ) as inst: assert inst.power == 1 def test_disable_emission(): with expected_protocol( IBeamSmart, init_comm + [("la off", ""), (None, "[OK]")], ) as inst: inst.emission = False def test_setting_failed(): with expected_protocol( IBeamSmart, init_comm + [("la off", ""), (None, "abc"), (None, "[OK]")], ) as inst: with pytest.raises(ValueError): inst.laser_enabled = False def test_enable_channel(): with expected_protocol( IBeamSmart, init_comm + [("en 3", ""), (None, "[OK]"), ("sta ch 3", ""), (None, "ON"), (None, "[OK]"), ("di 2", ""), (None, "[OK]"), ("sta ch 2", ""), (None, "OFF"), (None, "[OK]"), ] ) as inst: inst.ch_3.enabled = True assert inst.ch_3.enabled is True inst.ch_2.enabled = False assert inst.ch_2.enabled is False def test_channel1_enabled_getter(): with expected_protocol( IBeamSmart, init_comm + [("sta ch 1", ""), (None, "ON"), (None, "[OK]")], ) as inst: with pytest.warns(FutureWarning): assert inst.channel1_enabled is True def test_channel1_enabled_setter(): with expected_protocol( IBeamSmart, init_comm + [("en 1", ""), (None, "[OK]")], ) as inst: with pytest.warns(FutureWarning): inst.channel1_enabled = True def test_channel_power(): with expected_protocol( IBeamSmart, init_comm + [("ch 2 pow 100.000000 mic", ""), (None, "[OK]")] ) as inst: inst.ch_2.power = 100 def test_shutdown(): with expected_protocol( IBeamSmart, init_comm + [("di 0", ""), (None, "[OK]"), ("la off", ""), (None, "[OK]")] ) as inst: inst.shutdown() assert inst.isShutdown is True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/velleman/0000755000175100001770000000000014623331176020561 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/velleman/test_velleman_k8090.py0000644000175100001770000000657614623331163024642 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ Test the instrument class for the Velleman F8090. This is not an integration test! This is software-only. """ import pytest from pymeasure.test import expected_protocol from pymeasure.instruments.velleman import VellemanK8090, VellemanK8090Switches as Switches def test_version(): with expected_protocol( VellemanK8090, [ ( bytearray.fromhex("04 71 00 00 00 8B 0F"), bytearray.fromhex("04 71 FF 0A 01 81 0F"), ) ], ) as inst: assert inst.version == (10, 1) @pytest.mark.parametrize("reply,msg", [ (b"0", "Incoming packet was 1 bytes instead of 7"), (bytearray.fromhex("04 71 FF 0A 01 82 0F"), "Packet checksum was not correct"), (bytearray.fromhex("04 71 FF 0A 01 81 00"), "Received invalid start and stop bytes"), ]) def test_version_bad_reply_short(reply, msg): with expected_protocol( VellemanK8090, [ ( bytearray.fromhex("04 71 00 00 00 8B 0F"), reply, ) ], ) as inst: with pytest.raises(ConnectionError, match=msg): _ = inst.version def test_status(): with expected_protocol( VellemanK8090, [ ( bytearray.fromhex("04 18 00 00 00 E4 0F"), bytearray.fromhex("04 51 01 15 80 15 0F"), ) ], ) as inst: last_on, curr_on, time_on = inst.status assert last_on == Switches.CH1 assert curr_on == Switches.CH1 | Switches.CH3 | Switches.CH5 assert time_on == Switches.CH8 def test_switch_on(): with expected_protocol( VellemanK8090, [ ( bytearray.fromhex("04 11 05 00 00 E6 0F"), bytearray.fromhex("04 51 01 05 80 25 0F"), ) ] * 2, ) as inst: # Test both signatures inst.switch_on = Switches.CH1 | Switches.CH3 inst.switch_on = [1, 3] def test_switch_off(): with expected_protocol( VellemanK8090, [ ( bytearray.fromhex("04 12 FF 00 00 EB 0F"), bytearray.fromhex("04 51 01 00 80 2A 0F"), ) ], ) as inst: inst.switch_off = Switches.ALL ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/velleman/test_velleman_k8090_with_device.py0000644000175100001770000000562314623331163027204 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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. # """ Test the instrument class for the Velleman F8090. This tests the instrument class with the real device connected. """ import pytest from time import sleep from pymeasure.adapters import SerialAdapter from pymeasure.instruments.velleman import VellemanK8090, VellemanK8090Switches as Switches @pytest.fixture() def instrument(connected_device_address): """Get instrument object. Run like ``--device-address="ASRL1::INSTR"`` to use the visa adapter, and like ``--device-address="COM1"`` to use the serial adapter. """ if "ASRL" in connected_device_address: return VellemanK8090(connected_device_address, timeout=500) else: adapter = SerialAdapter( connected_device_address, baudrate=19200, timeout=0.5, read_termination=chr(0x0F) ) return VellemanK8090(adapter) def test_version(instrument): ver = instrument.version assert ver is not None and isinstance(ver, tuple) def test_status(instrument): last_on, curr_on, time_on = instrument.status assert isinstance(curr_on, Switches) def test_switch_on_off(instrument): """Test switch on-off together. This tests the physical switches and the status feedback. You must also hear the real relays clicking, and verify the LEDs 1 and 3 alone light up. """ instrument.switch_off = Switches.ALL sleep(0.5) instrument.switch_on = Switches.CH1 | Switches.CH3 sleep(0.5) _, curr_on_1, _ = instrument.status instrument.switch_off = Switches.ALL sleep(0.5) _, curr_on_2, _ = instrument.status assert curr_on_1 == Switches.CH1 | Switches.CH3 assert curr_on_2 == Switches.NONE # Test another pointless off switch, to test a lack of confirmation instrument.switch_off = Switches.ALL sleep(0.5) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716367998.4456065 pymeasure-0.14.0/tests/instruments/yokogawa/0000755000175100001770000000000014623331176020577 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/instruments/yokogawa/test_aq6370d.py0000644000175100001770000003323014623331163023272 0ustar00runnerdockerimport pytest from pymeasure.instruments.yokogawa.aq6370series import AQ6370D from pymeasure.test import expected_protocol def test_init(): with expected_protocol( AQ6370D, [], ): pass # Verify the expected communication. def test_automatic_sample_number_setter(): with expected_protocol( AQ6370D, [(b":SENSe:SWEep:POINts:AUTO 0", None)], ) as inst: inst.automatic_sample_number = False def test_level_position_getter(): with expected_protocol( AQ6370D, [(b":DISPlay:TRACe:Y1:RPOSition?", b"8\n")], ) as inst: assert inst.level_position == 8 def test_reference_level_setter(): with expected_protocol( AQ6370D, [(b":DISPlay:TRACe:Y1:SCALe:RLEVel -10", None)], ) as inst: inst.reference_level = -10 def test_reference_level_getter(): with expected_protocol( AQ6370D, [(b":DISPlay:TRACe:Y1:SCALe:RLEVel?", b"-1.00000000E+001\n")], ) as inst: assert inst.reference_level == -10.0 def test_resolution_bandwidth_setter(): with expected_protocol( AQ6370D, [(b":SENSe:BWIDth:RESolution 1e-09", None)], ) as inst: inst.resolution_bandwidth = 1e-09 def test_sample_number_setter(): with expected_protocol( AQ6370D, [(b":SENSe:SWEep:POINts 101", None)], ) as inst: inst.sample_number = 101 def test_sweep_mode_setter(): with expected_protocol( AQ6370D, [(b":INITiate:SMODe 2", None)], ) as inst: inst.sweep_mode = "REPEAT" def test_sweep_mode_getter(): with expected_protocol( AQ6370D, [(b":INITiate:SMODe?", b"2\n")], ) as inst: assert inst.sweep_mode == "REPEAT" @pytest.mark.parametrize( "comm_pairs, value", ( ([(b":SENSe:SWEep:SPEed 0", None)], "1x"), ([(b":SENSe:SWEep:SPEed 1", None)], "2x"), ), ) def test_sweep_speed_setter(comm_pairs, value): with expected_protocol( AQ6370D, comm_pairs, ) as inst: inst.sweep_speed = value @pytest.mark.parametrize( "comm_pairs, value", ( ([(b":SENSe:SWEep:SPEed?", b"0\n")], "1x"), ([(b":SENSe:SWEep:SPEed?", b"1\n")], "2x"), ), ) def test_sweep_speed_getter(comm_pairs, value): with expected_protocol( AQ6370D, comm_pairs, ) as inst: assert inst.sweep_speed == value def test_wavelength_center_getter(): with expected_protocol( AQ6370D, [(b":SENSe:WAVelength:CENTer?", b"+8.50000000E-007\n")], ) as inst: assert inst.wavelength_center == 8.5e-07 def test_wavelength_span_setter(): with expected_protocol( AQ6370D, [(b":SENSe:WAVelength:SPAN 1e-08", None)], ) as inst: inst.wavelength_span = 1e-08 def test_wavelength_span_getter(): with expected_protocol( AQ6370D, [(b":SENSe:WAVelength:SPAN?", b"+1.00000000E-007\n")], ) as inst: assert inst.wavelength_span == 1e-07 def test_wavelength_start_setter(): with expected_protocol( AQ6370D, [(b":SENSe:WAVelength:STARt 8e-07", None)], ) as inst: inst.wavelength_start = 8e-07 def test_wavelength_stop_setter(): with expected_protocol( AQ6370D, [(b":SENSe:WAVelength:STOP 9e-07", None)], ) as inst: inst.wavelength_stop = 9e-07 def test_delete_trace(): with expected_protocol( AQ6370D, [(b":TRACe:DELete TRA", None)], ) as inst: assert ( inst.delete_trace( *("A",), ) is None ) def test_execute_analysis(): with expected_protocol( AQ6370D, [(b":CALCulate", None)], ) as inst: assert inst.execute_analysis() is None def test_get_analysis(): with expected_protocol( AQ6370D, [(b":CALCulate:DATA?", None)], ) as inst: assert inst.get_analysis() is None def test_get_xdata(): with expected_protocol( AQ6370D, [ ( b":TRACe:X? TRA", b"""+8.45000000E-007,+8.45100000E-007,+8.45200000E-007,+8.45300000E-007, +8.45400000E-007,+8.45500000E-007,+8.45600000E-007,+8.45700000E-007, +8.45800000E-007,+8.45900000E-007,+8.46000000E-007,+8.46100000E-007, +8.46200000E-007,+8.46300000E-007,+8.46400000E-007,+8.46500000E-007, +8.46600000E-007,+8.46700000E-007,+8.46800000E-007,+8.46900000E-007, +8.47000000E-007,+8.47100000E-007,+8.47200000E-007,+8.47300000E-007, +8.47400000E-007,+8.47500000E-007,+8.47600000E-007,+8.47700000E-007, +8.47800000E-007,+8.47900000E-007,+8.48000000E-007,+8.48100000E-007, +8.48200000E-007,+8.48300000E-007,+8.48400000E-007,+8.48500000E-007, +8.48600000E-007,+8.48700000E-007,+8.48800000E-007,+8.48900000E-007, +8.49000000E-007,+8.49100000E-007,+8.49200000E-007,+8.49300000E-007, +8.49400000E-007,+8.49500000E-007,+8.49600000E-007,+8.49700000E-007, +8.49800000E-007,+8.49900000E-007,+8.50000000E-007,+8.50100000E-007, +8.50200000E-007,+8.50300000E-007,+8.50400000E-007,+8.50500000E-007, +8.50600000E-007,+8.50700000E-007,+8.50800000E-007,+8.50900000E-007, +8.51000000E-007,+8.51100000E-007,+8.51200000E-007,+8.51300000E-007, +8.51400000E-007,+8.51500000E-007,+8.51600000E-007,+8.51700000E-007, +8.51800000E-007,+8.51900000E-007,+8.52000000E-007,+8.52100000E-007, +8.52200000E-007,+8.52300000E-007,+8.52400000E-007,+8.52500000E-007, +8.52600000E-007,+8.52700000E-007,+8.52800000E-007,+8.52900000E-007, +8.53000000E-007,+8.53100000E-007,+8.53200000E-007,+8.53300000E-007, +8.53400000E-007,+8.53500000E-007,+8.53600000E-007,+8.53700000E-007, +8.53800000E-007,+8.53900000E-007,+8.54000000E-007,+8.54100000E-007, +8.54200000E-007,+8.54300000E-007,+8.54400000E-007,+8.54500000E-007, +8.54600000E-007,+8.54700000E-007,+8.54800000E-007,+8.54900000E-007, +8.55000000E-007\n""", ) ], ) as inst: assert inst.get_xdata( *("TRA",), ) == [ 8.45e-07, 8.451e-07, 8.452e-07, 8.453e-07, 8.454e-07, 8.455e-07, 8.456e-07, 8.457e-07, 8.458e-07, 8.459e-07, 8.46e-07, 8.461e-07, 8.462e-07, 8.463e-07, 8.464e-07, 8.465e-07, 8.466e-07, 8.467e-07, 8.468e-07, 8.469e-07, 8.47e-07, 8.471e-07, 8.472e-07, 8.473e-07, 8.474e-07, 8.475e-07, 8.476e-07, 8.477e-07, 8.478e-07, 8.479e-07, 8.48e-07, 8.481e-07, 8.482e-07, 8.483e-07, 8.484e-07, 8.485e-07, 8.486e-07, 8.487e-07, 8.488e-07, 8.489e-07, 8.49e-07, 8.491e-07, 8.492e-07, 8.493e-07, 8.494e-07, 8.495e-07, 8.496e-07, 8.497e-07, 8.498e-07, 8.499e-07, 8.5e-07, 8.501e-07, 8.502e-07, 8.503e-07, 8.504e-07, 8.505e-07, 8.506e-07, 8.507e-07, 8.508e-07, 8.509e-07, 8.51e-07, 8.511e-07, 8.512e-07, 8.513e-07, 8.514e-07, 8.515e-07, 8.516e-07, 8.517e-07, 8.518e-07, 8.519e-07, 8.52e-07, 8.521e-07, 8.522e-07, 8.523e-07, 8.524e-07, 8.525e-07, 8.526e-07, 8.527e-07, 8.528e-07, 8.529e-07, 8.53e-07, 8.531e-07, 8.532e-07, 8.533e-07, 8.534e-07, 8.535e-07, 8.536e-07, 8.537e-07, 8.538e-07, 8.539e-07, 8.54e-07, 8.541e-07, 8.542e-07, 8.543e-07, 8.544e-07, 8.545e-07, 8.546e-07, 8.547e-07, 8.548e-07, 8.549e-07, 8.55e-07, ] def test_get_ydata(): with expected_protocol( AQ6370D, [ ( b":TRACe:Y? TRA", b"""-5.80586383E+001,-4.82452558E+001,-2.10000000E+002,-5.00581515E+001, -2.10000000E+002,-2.10000000E+002,-2.10000000E+002,-5.39718179E+001, -2.10000000E+002,-4.83172555E+001,-2.10000000E+002,-5.40078179E+001, -9.28245811E+001,-4.71722994E+001,-2.10000000E+002,-5.20649217E+001, -2.10000000E+002,-2.10000000E+002,-4.58900745E+001,-4.76780332E+001, -4.88929655E+001,-4.59160745E+001,-5.89591856E+001,-6.04801307E+001, -4.56972546E+001,-5.37341531E+001,-4.68775174E+001,-4.63697811E+001, -2.10000000E+002,-4.58639117E+001,-2.10000000E+002,-5.04835809E+001, -2.10000000E+002,-4.55457108E+001,-2.10000000E+002,-2.10000000E+002, -5.25281289E+001,-5.25381299E+001,-6.83873225E+001,-4.97480469E+001, -5.03931511E+001,-4.94802372E+001,-4.99253305E+001,-5.57736026E+001, -4.43726648E+001,-2.10000000E+002,-2.10000000E+002,-2.10000000E+002, -2.10000000E+002,-5.35840271E+001,-2.10000000E+002,-2.10000000E+002, -4.61320419E+001,-2.10000000E+002,-4.82889238E+001,-4.89605452E+001, -4.80090332E+001,-2.10000000E+002,-5.24489220E+001,-5.59126029E+001, -4.93780799E+001,-2.10000000E+002,-5.22268893E+001,-4.99580469E+001, -2.10000000E+002,-5.37240266E+001,-6.86322957E+001,-2.10000000E+002, -2.10000000E+002,-2.10000000E+002,-4.70394921E+001,-4.96060890E+001, -2.10000000E+002,-4.75372963E+001,-2.10000000E+002,-2.10000000E+002, -2.10000000E+002,-2.10000000E+002,-5.42071536E+001,-4.70477584E+001, -2.10000000E+002,-2.10000000E+002,-2.10000000E+002,-2.10000000E+002, -5.67689941E+001,-4.81764150E+001,-2.10000000E+002,-4.87957619E+001, -2.10000000E+002,-2.10000000E+002,-5.51568122E+001,-2.10000000E+002, -5.09619656E+001,-2.10000000E+002,-5.43551546E+001,-4.71242549E+001, -5.27949223E+001,-5.43831531E+001,-2.10000000E+002,-4.94815222E+001, -6.89463024E+001\n""", ) ], ) as inst: assert inst.get_ydata( *("TRA",), ) == [ -58.0586383, -48.2452558, -210.0, -50.0581515, -210.0, -210.0, -210.0, -53.9718179, -210.0, -48.3172555, -210.0, -54.0078179, -92.8245811, -47.1722994, -210.0, -52.0649217, -210.0, -210.0, -45.8900745, -47.6780332, -48.8929655, -45.9160745, -58.9591856, -60.4801307, -45.6972546, -53.7341531, -46.8775174, -46.3697811, -210.0, -45.8639117, -210.0, -50.4835809, -210.0, -45.5457108, -210.0, -210.0, -52.5281289, -52.5381299, -68.3873225, -49.7480469, -50.3931511, -49.4802372, -49.9253305, -55.7736026, -44.3726648, -210.0, -210.0, -210.0, -210.0, -53.5840271, -210.0, -210.0, -46.1320419, -210.0, -48.2889238, -48.9605452, -48.0090332, -210.0, -52.448922, -55.9126029, -49.3780799, -210.0, -52.2268893, -49.9580469, -210.0, -53.7240266, -68.6322957, -210.0, -210.0, -210.0, -47.0394921, -49.606089, -210.0, -47.5372963, -210.0, -210.0, -210.0, -210.0, -54.2071536, -47.0477584, -210.0, -210.0, -210.0, -210.0, -56.7689941, -48.176415, -210.0, -48.7957619, -210.0, -210.0, -55.1568122, -210.0, -50.9619656, -210.0, -54.3551546, -47.1242549, -52.7949223, -54.3831531, -210.0, -49.4815222, -68.9463024, ] def test_initiate_sweep(): with expected_protocol( AQ6370D, [(b":INITiate:IMMediate", None)], ) as inst: assert inst.initiate_sweep() is None def test_reset(): with expected_protocol( AQ6370D, [(b"*RST", None)], ) as inst: assert inst.reset() is None def test_set_level_position_to_max(): with expected_protocol( AQ6370D, [(b":CALCulate:MARKer:MAXimum:SRLevel", None)], ) as inst: assert inst.set_level_position_to_max() is None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/test_expected_protocol.py0000644000175100001770000001225214623331163021514 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 pytest import raises from pymeasure.test import expected_protocol from pymeasure.instruments import Instrument from pymeasure.instruments.validators import strict_range class BasicTestInstrument(Instrument): def __init__(self, adapter, name="Basic Test Instrument", **kwargs): super().__init__(adapter, name) self.kwargs = kwargs simple = Instrument.control( "VOLT?", "VOLT %s V", """Simple property replying with plain floats""", ) limited_control = Instrument.control( "AMP?", "AMP %g A", """Property limited to 0, 10.""", values=(0, 10), validator=strict_range ) with_error_checks = Instrument.control( "VOLT?", "VOLT %s V", """Property with error checks after both setting and getting""", check_set_errors=True, check_get_errors=True, ) def test_simple_protocol(): """Test a property without parsing or channel prefixes.""" with expected_protocol( BasicTestInstrument, [('VOLT?', 3.14), ('VOLT 4.5 V', None)] ) as instr: assert instr.simple == 3.14 instr.simple = 4.5 def test_kwargs(): """Test whether the kwargs are handed over correctly.""" with expected_protocol( BasicTestInstrument, [], test=5, xyz="abc" ) as instr: assert instr.kwargs == {'test': 5, 'xyz': "abc"} def test_error_checks(): """Test protocol for getting and setting with error checks.""" with expected_protocol( BasicTestInstrument, [('VOLT?', 3.14), ('SYST:ERR?', '0, 0'), ('VOLT 4.5 V', None), ('SYST:ERR?', '0, 0'), ] ) as instr: assert instr.with_error_checks == 3.14 instr.with_error_checks = 4.5 def test_not_all_communication_used(): """Test whether unused communication raises an error.""" with raises(AssertionError, match="Unprocessed protocol definitions remain"): with expected_protocol( BasicTestInstrument, [('VOLT?', 3.14), ('VOLT 4.5 V', None), ] ) as instr: assert instr.simple == 3.14 def test_non_empty_write_buffer(): with raises(AssertionError, match="Non-empty write buffer remains"): with expected_protocol( BasicTestInstrument, [('VOLT?', 3.14)] ) as instr: instr.adapter.write_bytes(b"VOLT") instr.adapter._index = 1 def test_non_empty_read_buffer(): with raises(AssertionError, match="Non-empty read buffer remains"): with expected_protocol( BasicTestInstrument, [('VOLT?', 3.14)] ) as instr: instr.write("VOLT?") def test_preprocess_reply_on_values(): class InstrumentWithPreprocessValues(BasicTestInstrument): """Workaround to get preprocess_reply working with protocol tests.""" simple2 = Instrument.control( "VOLT?", "VOLT %s V", """Simple property replying with plain floats""", preprocess_reply=lambda v: v + "2345" ) with expected_protocol( InstrumentWithPreprocessValues, [("VOLT?", "3.1")] ) as instr: assert instr.simple2 == 3.12345 class TestConnectionCalls: def test_connection_method_call(self): with expected_protocol( BasicTestInstrument, [], connection_methods={'stb': 17} ) as inst: assert inst.adapter.connection.stb() == 17 def test_connection_attribute(self): with expected_protocol( BasicTestInstrument, [], connection_attributes={'timeout': 100} ) as inst: assert inst.adapter.connection.timeout == 100 def test_limited_control_raises_validator_exception(): """Verify, that the validator's exception is caught.""" with expected_protocol( BasicTestInstrument, [], ) as inst: with raises(ValueError, match="not in range"): inst.limited_control = 20 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/test_generator.py0000644000175100001770000005764114623331163017773 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 io import logging import pytest from pymeasure.adapters import ProtocolAdapter from pymeasure.instruments import Channel, Instrument from pymeasure.instruments.hcp import TC038, TC038D from pymeasure.generator import (write_test, write_parametrized_test, write_parametrized_method_test, parse_stream, Generator, ByteStreamHandler) class FakeChildChannel(Channel): channel_control = Channel.control("G{{ch}}.{ch}", "S{{ch}}{ch} %f", "Control something. (float)") class FakeChannel(Channel): port = Channel.ChannelCreator(FakeChildChannel, id=1) channel_control = Channel.control("G{ch}", "S{ch} %f", "Control something. (float)") def funny_method(self, value=7): """Some method for testing purposes.""" return float(self.ask("M{ch} " + str(value))) class FakeTestInstrument(Instrument): ch_A = Instrument.ChannelCreator(FakeChannel, id="A") def __init__(self, adapter, name="Fake"): super().__init__(adapter, name) i_control = Instrument.control("G", "S %f", "Control instrument. (float)") @pytest.fixture def file(): s = io.StringIO() yield s s.close() class Test_write_test: def test_write(self, file): write_test(file, "init", "Super", [(b"sent", b"received")], "pass # Verify the expected communication.") assert file.getvalue() == """ def test_init(): with expected_protocol( Super, [(b'sent', b'received')], ): pass # Verify the expected communication. """ def test_write_multiple_comm_pairs(self, file): write_test(file, "init", "Super", [(b"sent", b"received"), (b"sent2", b'rec2')], "pass") assert file.getvalue() == """ def test_init(): with expected_protocol( Super, [(b'sent', b'received'), (b'sent2', b'rec2')], ): pass """ def test_write_init_kwargs(self, file): write_test(file, "init", "Super", [(b"sent", b"received")], "pass # Verify the expected communication.", {'address': 7, 'name': "my name", 'bool': True}) assert file.getvalue() == """ def test_init(): with expected_protocol( Super, [(b'sent', b'received')], address=7, name='my name', bool=True, ): pass # Verify the expected communication. """ def test_write_bytes(self, file): write_test(file, "init", "Super", [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')], "del instr") assert file.getvalue() == r""" def test_init(): with expected_protocol( Super, [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')], ) as inst: del instr """ def test_write_replaces_period_with_underscore_in_name(self, file): write_test(file, "ch.init", "Super", [(b"sent", b"received")], "pass # Verify the expected communication.") assert file.getvalue() == """ def test_ch_init(): with expected_protocol( Super, [(b'sent', b'received')], ): pass # Verify the expected communication. """ class Test_write_parametrized_test: def test_write(self, file): write_parametrized_test(file, "init", "Super", [[(b"sent", b"received")]], [None], "assert inst.xyz == value") assert file.getvalue() == """ @pytest.mark.parametrize("comm_pairs, value", ( ([(b'sent', b'received')], None), )) def test_init(comm_pairs, value): with expected_protocol( Super, comm_pairs, ) as inst: assert inst.xyz == value """ def test_write_multiple_comm_pairs(self, file): write_parametrized_test(file, "init", "Super", [[(b"sent", b"received"), (b"sent2", b'rec2')]], [None], "pass") assert file.getvalue() == """ @pytest.mark.parametrize("comm_pairs, value", ( ([(b'sent', b'received'), (b'sent2', b'rec2')], None), )) def test_init(comm_pairs, value): with expected_protocol( Super, comm_pairs, ): pass """ def test_write_replaces_period_with_underscore_in_name(self, file): write_parametrized_test(file, "ch.init", "Super", [[(b"sent", b"received")]], [None], "assert inst.xyz == value") assert file.getvalue() == """ @pytest.mark.parametrize("comm_pairs, value", ( ([(b'sent', b'received')], None), )) def test_ch_init(comm_pairs, value): with expected_protocol( Super, comm_pairs, ) as inst: assert inst.xyz == value """ def test_write_parametrized_method(file): """Test also, that a period in the name is changed to underscore.""" write_parametrized_method_test(file, "set_monitored.quantity", "TC038", [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')], [(b'\x0201010W0002\x03', b'\x020K\x03')]], [('temperature',), ()], [{}, {'quantity': 'temperature'}], [None, "'xyz'"], "assert inst.set_monitored_quantity(*args, **kwargs) == value" ) assert file.getvalue() == r""" @pytest.mark.parametrize("comm_pairs, args, kwargs, value", ( ([(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')], ('temperature',), {}, None), ([(b'\x0201010W0002\x03', b'\x020K\x03')], (), {'quantity': 'temperature'}, 'xyz'), )) def test_set_monitored_quantity(comm_pairs, args, kwargs, value): with expected_protocol( TC038, comm_pairs, ) as inst: assert inst.set_monitored_quantity(*args, **kwargs) == value """ class Test_parse_stream: @pytest.mark.parametrize( "text, comms", ( (b"WRITE:abc\n", [(b"abc", None)]), (b"READ:def\n", [(None, b"def")]), (b"READ:a\nREAD:bc\n", [(None, b"abc")]), (b"WRITE:abc\nREAD:def\n", [(b"abc", b"def")]), (b"WRITE:abc\nREAD:d\nREAD:ef\n", [(b"abc", b"def")]), (b"WRITE:abc\nREAD:def\nWRITE:ghi\nREAD:jkl\n", [(b"abc", b"def"), (b"ghi", b"jkl")]), (b"WRITE:abc\nWRITE:def\n", [(b"abc", None), (b"def", None)]), (b"WRITE:\x03ab\x04\nREAD:super\x05\n", # test for non ASCII byte values [(b'\x03ab\x04', b'super\x05')]), (b"WRITE:ho\n 9\nWRITE:\n hey\nREAD:7\n9\n", # test for additional newline chars [(b"ho\n 9", None), (b"\n hey", b"7\n9")]), )) def test_parsing(self, text, comms): with io.BytesIO(text) as buf: assert parse_stream(buf) == comms class Test_generator: @pytest.fixture def generator(self): generator = Generator() adapter = ProtocolAdapter( [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03")]) TC038.string_test = TC038.control("test?", "test %s", # type: ignore "Control some string.", cast=str, # type: ignore get_process=lambda v: v[7:-1]) generator.instantiate(TC038, adapter, "hcp") return generator def test_instantiate(self, generator): assert generator._init_comm_pairs == [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03")] def test_write_init_test(self, generator, file): generator.write_init_test(file) assert file.getvalue() == r"""import pytest from pymeasure.test import expected_protocol from pymeasure.instruments.hcp import TC038 def test_init(): with expected_protocol( TC038, [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')], ): pass # Verify the expected communication. """ def test_instantiate_with_kwargs(self, file): generator = Generator() adapter = ProtocolAdapter( [(b"\x0201010WRS01D0002\x03", b"\x020101OK\x03")]) # add a control with a str for test purposes. TC038.string_test = TC038.control("test?", "test %s", # type: ignore "Control some string.", cast=str, # type: ignore get_process=lambda v: v[7:-1]) generator.instantiate(TC038, adapter, "hcp", some_kwarg=5.7, other_kwarg=True, str_kwarg="abc") generator.write_init_test(file) assert file.getvalue() == r"""import pytest from pymeasure.test import expected_protocol from pymeasure.instruments.hcp import TC038 def test_init(): with expected_protocol( TC038, [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')], some_kwarg=5.7, other_kwarg=True, str_kwarg='abc', ): pass # Verify the expected communication. """ def test_property(self, generator): generator.inst.adapter.comm_pairs.extend( [(b"\x0201010WRDD0002,01\x03", b"\x020101OK00C8\x03")]) assert generator.test_property_getter("temperature") == 20 assert generator._getters == {'temperature': ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRDD0002,01\x03', b'\x020101OK00C8\x03')]], [20], )} def test_property_with_test_instrument(self, generator): generator.inst.adapter.comm_pairs.extend( [(b"\x0201010WRDD0002,01\x03", b"\x020101OK00C8\x03")]) assert generator.test_inst.temperature == 20 assert generator._getters == {'temperature': ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRDD0002,01\x03', b'\x020101OK00C8\x03')]], [20], )} def test_property_string(self, generator): """Ensure that a returned string is encapsulated by single ticks.""" generator.inst.adapter.comm_pairs.extend( [(b"\x0201010test?\x03", b"\x020101OKall fine\x03")]) assert generator.test_property_getter("string_test") == "all fine" assert generator._getters == {'string_test': ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b"\x0201010test?\x03", b"\x020101OKall fine\x03")]], ["'all fine'"], )} def test_write_getter_test(self, generator, file): generator.write_getter_test(file, 'temperature', ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRDD0002,01\x03', b'\x020101OK00C8\x03')]], [20], )) assert file.getvalue() == r""" def test_temperature_getter(): with expected_protocol( TC038, [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRDD0002,01\x03', b'\x020101OK00C8\x03')], ) as inst: assert inst.temperature == 20 """ @pytest.mark.parametrize("value, test", ( (None, "is None"), (True, "is True"), (False, "is False"), (7, "== 7"), (12.34, "== 12.34"), )) def test_write_getter_test_comparison(self, generator, file, value, test): """Test whether the comparison is changed to 'is', if the value is a boolean or None.""" generator.write_getter_test(file, 'temperature', ( [[]], [value], )) assert file.getvalue().endswith(test + "\n") def test_property_setter(self, generator): generator.inst.adapter.comm_pairs.extend( [(b"\x0201010WWRD0120,01,00C8\x03", b"\x020101OK\x03")]) generator.test_property_setter("setpoint", 20) assert generator._setters == {'setpoint': ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WWRD0120,01,00C8\x03', b'\x020101OK\x03')]], [20], )} def test_property_setter_with_test_instrument(self, generator): generator.inst.adapter.comm_pairs.extend( [(b"\x0201010WWRD0120,01,00C8\x03", b"\x020101OK\x03")]) generator.test_inst.setpoint = 20 assert generator._setters == {'setpoint': ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WWRD0120,01,00C8\x03', b'\x020101OK\x03')]], [20], )} def test_property_setter_string(self, generator): """Ensure that a string value is encapsulated by single ticks.""" generator.inst.adapter.comm_pairs.extend( [(b"\x0201010test xy\x03", b"\x020101OK\x03")]) generator.test_property_setter("string_test", "xy") assert generator._setters == {'string_test': ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010test xy\x03', None)]], ["'xy'"], )} def test_write_setter_test(self, generator, file): generator.write_setter_test(file, 'setpoint', ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WWRD0120,01,00C8\x03', b'\x020101OK\x03')]], [20], )) assert file.getvalue() == r""" def test_setpoint_setter(): with expected_protocol( TC038, [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WWRD0120,01,00C8\x03', b'\x020101OK\x03')], ) as inst: inst.setpoint = 20 """ @pytest.mark.parametrize("args, kwargs", ( (('temperature',), {}), ((), {'quantity': 'temperature'}) )) def test_method_arg(self, generator, args, kwargs): generator.inst.adapter.comm_pairs.extend( [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')]) generator.test_method("set_monitored_quantity", *args, **kwargs) assert generator._calls == {'set_monitored_quantity': ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')]], [args], [kwargs], [None], )} @pytest.mark.parametrize("args, kwargs", ( (('temperature',), {}), ((), {'quantity': 'temperature'}) )) def test_method_arg_with_test_instrument(self, generator, args, kwargs): generator.inst.adapter.comm_pairs.extend( [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')]) generator.test_inst.set_monitored_quantity(*args, **kwargs) assert generator._calls == {'set_monitored_quantity': ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')]], [args], [kwargs], [None], )} @pytest.mark.parametrize("args, kwargs, value, test", ( (('temperature',), {}, None, "(*('temperature',), ) is None"), (('temperature',), {}, True, "(*('temperature',), ) is True"), ((), {'quantity': 'temperature'}, 7, "(**{'quantity': 'temperature'}) == 7"), ((), {'quantity': 'temperature'}, False, "(**{'quantity': 'temperature'}) is False"), )) def test_write_method_single(self, generator, file, args, kwargs, value, test): generator.write_method_test(file, 'set_monitored_quantity', ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')]], [args], [kwargs], [value], )) assert file.getvalue() == r""" def test_set_monitored_quantity(): with expected_protocol( TC038, [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')], ) as inst: assert inst.set_monitored_quantity """[:-1] + test + "\n" def test_write_method_parametrized(self, generator, file): generator.write_method_test(file, 'set_monitored_quantity', ( [[(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')], [(b'\x0201010W0002\x03', b'\x020K\x03')]], [('temperature',), (), ], [{}, {'quantity': 'temperature'}], [None, 7], )) assert file.getvalue() == r""" @pytest.mark.parametrize("comm_pairs, args, kwargs, value", ( ([(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03')], ('temperature',), {}, None), ([(b'\x0201010W0002\x03', b'\x020K\x03')], (), {'quantity': 'temperature'}, 7), )) def test_set_monitored_quantity(comm_pairs, args, kwargs, value): with expected_protocol( TC038, comm_pairs, ) as inst: assert inst.set_monitored_quantity(*args, **kwargs) == value """ def test_bytes_communication(self, file): generator = Generator() a = ProtocolAdapter([(b"\x01\x03\x00\x00\x00\x02\xC4\x0B", b"\x01\x03\x04\x00\x00\x03\xE8\xFA\x8D")]) a.log.addHandler(ByteStreamHandler(generator._stream)) a.log.setLevel(logging.DEBUG) generator.inst = TC038D(a) generator._class = "TC038D" assert generator.test_property_getter("temperature") == 100 assert generator._getters == {'temperature': ( [[(b'\x01\x03\x00\x00\x00\x02\xc4\x0b', b'\x01\x03\x04\x00\x00\x03\xe8\xfa\x8d')]], [100.0], )} def test_write_bytes_communication(self, generator, file): generator._getters = {'temperature': ( [[(b'\x01\x03\x00\x00\x00\x02\xc4\x0b', b'\x01\x03\x04\x00\x00\x03\xe8\xfa\x8d')]], [100.0], )} generator._class = "TC038D" generator.write_property_tests(file) assert file.getvalue() == r""" def test_temperature_getter(): with expected_protocol( TC038D, [(b'\x01\x03\x00\x00\x00\x02\xc4\x0b', b'\x01\x03\x04\x00\x00\x03\xe8\xfa\x8d')], ) as inst: assert inst.temperature == 100.0 """ @pytest.fixture def generator_multiple(self, generator): """Test an interweaved mix of getters and setters.""" generator.inst.adapter.comm_pairs.extend( [(b"\x0201010WWRD0120,01,00C8\x03", b"\x020101OK\x03")]) generator.test_property_setter("setpoint", 20) generator.inst.adapter.comm_pairs.extend( [(b"\x0201010WRDD0002,01\x03", b"\x020101OK00C8\x03")]) assert generator.test_property_getter("temperature") == 20 generator.inst.adapter.comm_pairs.extend( [(b"\x0201010WWRD0120,01,0258\x03", b"\x020101OK\x03")]) generator.test_property_setter("setpoint", 60) generator.inst.adapter.comm_pairs.extend( [(b'\x0201010INF6\x03', b'\x020101OKUT150333 V01.R001111222233334444\x03')]) assert generator.test_property_getter("information") == "UT150333 V01.R001111222233334444" generator.inst.adapter.comm_pairs.extend( [(b"\x0201010WRDD0002,01\x03", b"\x020101OK0258\x03")]) assert generator.test_property_getter("temperature") == 60 generator.inst.adapter.comm_pairs.extend( [(b"\x0201010WRDD0120,01\x03", b"\x020101OK00C8\x03")]) assert generator.test_property_getter("setpoint") == 20 return generator def test_write_property_tests(self, generator_multiple, file): """Test that they are sorted alphabetically and collected in a single, parametrized test.""" generator_multiple.write_property_tests(file) assert file.getvalue() == r""" def test_information_getter(): with expected_protocol( TC038, [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010INF6\x03', b'\x020101OKUT150333 V01.R001111222233334444\x03')], ) as inst: assert inst.information == 'UT150333 V01.R001111222233334444' @pytest.mark.parametrize("comm_pairs, value", ( ([(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WWRD0120,01,00C8\x03', b'\x020101OK\x03')], 20), ([(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WWRD0120,01,0258\x03', b'\x020101OK\x03')], 60), )) def test_setpoint_setter(comm_pairs, value): with expected_protocol( TC038, comm_pairs, ) as inst: inst.setpoint = value def test_setpoint_getter(): with expected_protocol( TC038, [(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRDD0120,01\x03', b'\x020101OK00C8\x03')], ) as inst: assert inst.setpoint == 20.0 @pytest.mark.parametrize("comm_pairs, value", ( ([(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRDD0002,01\x03', b'\x020101OK00C8\x03')], 20.0), ([(b'\x0201010WRS01D0002\x03', b'\x020101OK\x03'), (b'\x0201010WRDD0002,01\x03', b'\x020101OK0258\x03')], 60.0), )) def test_temperature_getter(comm_pairs, value): with expected_protocol( TC038, comm_pairs, ) as inst: assert inst.temperature == value """ class TestTestInstrument: @pytest.fixture(scope="function") def inst(self): generator = Generator() adapter = ProtocolAdapter() inst = generator.instantiate(FakeTestInstrument, adapter, "fake") return inst def test_channel_setter(self, inst): inst.adapter.comm_pairs.extend([("SA 15.000000", None)]) inst.ch_A.channel_control = 15 assert inst._generator._setters == {'ch_A.channel_control': ([[(b"SA 15.000000", None)]], [15])} def test_channel_getter(self, inst): inst.adapter.comm_pairs.extend([("GA", "123.5")]) assert inst.ch_A.channel_control == 123.5 assert inst._generator._getters == {'ch_A.channel_control': ([[(b"GA", b"123.5")]], [123.5])} def test_write_channel_getter_test(self, inst, file): """Importantly, this test checks also, that the test name does not contain a period.""" inst.adapter.comm_pairs.extend([("GA", "123.5")]) assert inst.ch_A.channel_control == 123.5 inst._generator.write_property_tests(file) assert file.getvalue() == r""" def test_ch_A_channel_control_getter(): with expected_protocol( FakeTestInstrument, [(b'GA', b'123.5')], ) as inst: assert inst.ch_A.channel_control == 123.5 """ def test_channel_method(self, inst): inst.adapter.comm_pairs.extend([("MA 9", "11")]) assert inst.ch_A.funny_method(9) == 11 assert inst._generator._calls == {'ch_A.funny_method': ([[(b"MA 9", b"11")]], [(9,)], [{}], [11.])} def test_child_channel(self, inst): """Whether the child of a channel can be accessed as expected.""" inst.adapter.comm_pairs.extend([("GA.1", "7")]) assert inst.ch_A.port.channel_control == 7 assert inst._generator._getters == {'ch_A.port.channel_control': ([[(b"GA.1", b"7")]], [7])} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/test_log.py0000644000175100001770000000364414623331163016560 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 unittest import mock 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 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/test_process.py0000644000175100001770000000274714623331163017460 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716367987.0 pymeasure-0.14.0/tests/test_thread.py0000644000175100001770000000265214623331163017244 0ustar00runnerdocker# # This file is part of the PyMeasure package. # # Copyright (c) 2013-2024 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

/d>&es :##lqL7*&7%鰸M-NeMB6ȵig.S-$ۗe@AeD(*Yr٭-iby hsyV|a4M#Y8{dogFN{0 9c.sC39s!>yM᲏ZF?n3# 7Acxc9-lcet#Y8c#c=# @?=NcA2"C0;.DTdEpX DF6y0%8?D hQY <AHY2f_9&dfyBă4lI<\<%^?H><Ķ-iqG"(# M@D#X@(pXSPY"&H.&ql4A28C#VFLeEd^\6dHPd43e Z&z'Fm3@ BXfBL@TeDG*,7<@%`l6@T*TV#؅ZX)Dt؅f( ~*,t;l!4K4 )U B _J|K,BFP”(AiZՑ&)]v IqBs'__J,(E *V7BG,{UA *0ͿܚBljb{7D)<]v^%sHPD|ßnCB=*oM[^҃5@ELG>)+A0<$~E`GfY~`"[ăPsgWğ5}Fԁ4p@XhEK4=-ƥ ur2xE:WBP:@jL$6(AX4"AlX)ĘjFHCLlmULC:8DQ:MM8 n :.N.I.^.Ul6\F| 9.9y趄.붮.쒮鲄.J.A[Qi.fno4onnnndNcmFaEt - o@norJoRb@Mɭ"oKL&'o,#?G+0W0w'opcWpK͒EaJ Jl07 3p03 kp  L@/  0,1 {ppJH61'08qqCq pgSJx1*D1 WN4M>Cև";/"5|\( )RKT(]2&O2Kl2'w'&O)2A2* K$&&߱;3CK3S3ϱ[3)nJCD2*-2#c.$C1\RI3c3k33s44s5s,הR3s0'Pj!C7ӖooEoFF[EkoGc4GL&N(o\v2K4LǴLKM4NL4OôOKPO5O1NBD33K |@U_@\5!XVBWUgX5Y!5ZYuZXcZuZ[uUV۵K5_g5X{5W6`]a^5[/b#v^@ǎd 55TO5PtgoHth[h{Ginj6d˲KtM\D50sxvJ6GFǁn woowpviJXVet@;dtgEiUk7xS7sDyxzw{+\{|qzϷ}\|߷~~xַk87'8K7l.ăOxH8E 6ÿ5 6L Zt\54hX<6* 9]"6@P` W\"XH+5X:$sk_nH?Z1`j5|ÊGw,|_CKfC+˔ :4DR&-5CZxoL5`9E47L1H@P0C'PCN)54-CĞ9(9+D<@XC\C\g 0L28ҋ"Z*Ê!W<@!̮3{ b%R쁱P4oW'L3 /HC6p,`2$s'ʚ(@>< F@7X@en. 5859S]@,xx 2(J0@'@A&TaC!F8bE1f 5ia3(falLN0NcK/ `Æ F,XFa?:hQG&UiӢ0F:jUWF5+Wayq%&0V /]nRk٬wջW*6 `S,OǿFhKYU Β+Dkq,tiӧQVZj*x/0ذֿNѯ3m+xMd*e`? zwG-N|ݰ{LueΗ?Kl [ɒ'Zpq3l9[H]l 1l 2Tȷ:HZj!F\>PŻ/& 02Yhy@#eB,Ht9l+H@%%Ʌ%' S߮3L3/\ hKUcS9;S=: 42-CEEmG!TI)K!ETMY#OA UQI-POMMUUYmWBYi[q=*r Va-،du8LgViMM֥!xheo W\ m hwW޻ʵjTǙv X .8z "j &b/)⋇EzPaBa ZQNT'⸠FJqYYdY" ((fL"bYZS9"{7byN[~z!jQ ZIt>{ /.hD1x<9skd ̰vta)ugq].aMpP A. m]驏qtAF@q si=|Zoa_ Pg'u) `3Ȫ,kq.B!~h!\0 d8K]|+%4 ?@܋^! AyAAO$D6PD!QI @ 5NGLb(o?!@^AD16|-vP-<Ă EAHQe\4!Q+ ^x@ bA D!` qlr!4%^{]@2N  9yR9 )LaRm `0L0(w#:.P0Nq*^Bא&B;D@8`^osA7 e1 ftFЪW  jϝ ߔ4m#)B@~RCo`xE*HGם#?e l<`*Δbw)YNbAM FFBBQ ggv\x,SKAr@Gw8k ;fJՌrO`phMZC9;Ϊdv`): tAA1_R¯rDy"/BLê[K;G@ũp}y6&3Mr;VE5;:$ظx޴A m8۷웧=$IH""HBb9J76^^c;0)a^~l~/)^l]ɐpV;C`F6㺜k#b.^*cPF. dOs ㅬ@' ӳ^:#ĭ֣%s뤆?Z#F/@,plrƘXtj2R/u(?E^b[!1lߐYf&kH.! Xd  " & | , )m` ڃ7 hQ~a, a\ d+- " 鈈 8 .#* (p*q_'D)fdXA/ s50W(*B&¾j P"T/Áll  Xs9#5@ 46 a#BX NʐC  hF>B05 6_zj)r!0+vI da!zg4#ukcpzTRt> 3I)¾tX5Buw 2T'uK%4L sS.+$mr$8 HB $̠Y UWP4!`Ch" a@Cua %'Qg^^^[ L-?  `7a1[N+]$oj5Kl횒_BKlb*˂ "6:?UFbkcv N+ U"ZuhdϱS-ee?c(ʈ` +vx~hBhei*$fjso5f0m;5l_G 0wn`aW^`[/AK`!!<Ԗ4f`eH jEEkˆZbHzbi0O RѺE^YpwrWGS@Cު `V%%A7&AhCF̈Qr5)s.Ɯ( 07#ȑzF&/X6y"¨; @4s!D!xM# bnw f k,^D2f !!Ѧja!U}df1ę?qzy@I#Lbʎ X\O|qGY<=Go;[Ƈ۰T[KmDKR$k[%:J}-~8*5^56A>ex.TQ5gb<>a>e~imq>u~y}>^d>~陾>~ꩾ>~빾>~^Sdmu2z!>C>8>a>K?f)\LjݘF>"I]x5.u@(V%`D*"HHoWIJap!AJ!A"I #". a=daq4 AB!RNl ` 8_,.b. 2X  ẃ:* "N7v"$8… :|1ĉ )|dUB;z2ȑ$K<2ʕ,[| 3̙4k ILaS5-  Y pB)tDY"A< bGy0Blۺ} 7ܹtڽ7DkzhAN::Luz[0P؉!O{Xa^B١AI,5Y*k۾;ݼ{=!֎(1#X·( _\CM:(,%9)Qxۻ?#FL&khAePAHgvP|Gs|AUpB8'|P"BT@7'4A%Me p 54L0 y󇖳20pqZXZ/g.+lN4/,ܒ (0Z C$,s3o2K0d/ 1+ S1ӘuR *]DݶY 2^`&-n[/k.| ڂ[*  ~.d]R*˼n[V_Re\*ςKk2-J5/brI \Rź Rb75Jv{w׊/8Ehx6Rߵ`-2-i*1bAtzŋ2"Ϯx/5tմLL_ج.%{@_86N}Rc?_S[l-`Q?R>v4쇃<2o3Բ^Mt$!S4b`/A,/̍#Aa$aOq^}zq:4lVp;Ek~$*7D(Jq'i",jVܢ0fb,ψF"i61mQr<񎌓FqX-,dD*"h$8E ِ3Lj2'F!dDFl%ʬIh 22bl$!s/_L\3K\:(r3g*KNA4zin4DOS2`M8)T,5x/t2a72\A Zإ x"N=<-T@&hkY@σ(@ |h!ˈB $eKb8pBP h%8C>)ub~0$f) "TefQtj>@+ZHOYt%Ԃ:$eO8ué nxC!Zɍh ;"@YHiT+= &CLQNE# L.WB "0` CJ'\30NH]ې)dR$"NFdB0qڨ352(6Hx50$ъQmB bcb,p&]%ށ,)6Běƀ1L'BJavl.ÒO΢i,|#%|VPHh 0]'p,a@Xo.4Z@bt^Q?AZF` Q1d kd|/@X#N@W! BY CEqE}9tG*^Dbb?,l-W.=Yj Zʖ^BldCCEq0t+EPeڵQ } o^\.]ôY6+@3-zX!`-FoUi+K["0y[ ~F/ lqE>8 [!89vӁguCJ1/ @T`h@l!C7H,xm ޤS!,["R|qP;.S:%+O얨+&QA^A҉ѡFuC񅧉*.OkpC?sPޠ94rDJX‡1oKD^Yؽ ݯ !c?"yC&G: oH }t>g:g#Ț} 8~'~ }% ꧀ X0J҂1a ȁ(q q} Qv|߷#}h7h #x4pxއ58 8(7@}? }%W7SȆ(]J   '] }؆h2{P ҅K)*E xh>c=dsFK@p |@|K`| ȌȌ  +,uKF@ ` baE/`), ~k uLQi  NYU%F NKNppiY}#АHIIq S + [Rp}G(N |ڨaTo JY 8 p%AR; G g|PPy Ys k tz)S a 3ij V c):8g"tIKXy*  P*1z Q  b)b0 NQkP afm J`vAIY }*鉋1ᙣASed w9E0@ u P ܒA_a6KS1$@ۙɋ`|MnEPyiUy  ![!:N &FExI 0R W 8i [xH}[SpIwh} e K Jj*2y >h~T ! Sz xq:NpۀH@h?>ʖ |w +@a}j8 %#K%[ ,{:6 >p ¹ z  Ѵ: Q{#0*) A˴L_&k& ȱ Q*wHgWP^+;<±#e+ A'KڶHhJxw}ڹ|Z"0Y{ y{ +k[G˯;(a,*lș2Qż߫E׹'}˴R1{0{ A`U  LEK?z2+wRa ,“bˆ h|C PHx'3:1F!, H*\ȰÇ#JHŋ3jȱǏIIɓ(S\ɲ%S.cʜI͛8sɳϟ@d$ѣH*]ʴӧPv%JիXjʵׯ]U} ٳhӪ]˶ێUC D( ˷߿7{È+^̸q`Qښ,˘3k̹3˸CMCBͺװc+۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ{ݳjOYyϿ,g2+JF(!p1BK2 R /Td@$8('P-"@Q,H4C8P1"_CP@!.( 91-"`9.т@`D$@,2,N%1(;I矀Jd)mb , u-$#覜r@C9*P4AJ\D t뮼 L~,N S̱@~'Ad2b̊! ,袌@Xh̓R`.*йD@\1 S@6@ #˚ɛ/Cؒ bo.4 % \w $0#*L9e1ZB(kC4w,|? AdkPi)PJe-k@,?8TW蚒-L}$-$F8u[TJݭ#T% ^ZPFo/\  q$ > u!@Fa@hz( Mx Q:Xt '&$4#; @kds!AXQMT+$χ`@b  ":I4it dHk ^  O}1D9DrXhTR%L%D5\a  dC,ɗ$< < p@<1DUXTA @Ts:N0L M1@*@Ć4qHq ig8pqF8v/ApQBHDXrEk!DOp'*H/v6py -r2w}nt;V׸=Crnv;^冗mz &ν4|q>x`vnIUMn 9tjA˶ ֱ*K7[R}s_>!pCj[ة&*TFA-&(D0g1qӸ>1 !AFc&,D1\cPSog_Nr ߙtͧVR /2*vV 9Ktd\F }e?KTv4!eE45-Yfi1i)A z=teM]Z[ŵv]-^]5z^b!._mX {Ϯ{Z[ɱn"-p>7ufC-[vw׽otw oz-O:ԣ6'Nq8_86{܃9&8S|.yY>7?y_N9}*'̕/]F|+NpqշB] H DcObƁ@ [瞖rt{{B@ZOx; N A[&=J{61"O}Iq\xB ŲbDc1!z"E|d2s k[?!Q-hq _ߘ R^(O?%`\qIϿ8X$tMgqo h g04rq,VxÁD ءfF*p =41X1:(6PE!=5S-X&AI0O- &Q$'V+قUx'9P;\4{@ 3@ yaZ&AehFGI^ ^NwX pG~b40 !Ig8Q!*t`:!n3%WIB9DF&9yJNY TSYX9W\[ٕ`i_dcYhglykٖp)otsYxw|9{ٗY)٘O,;0j! d!y)0 . 7 1=1C)IŹg"ɹ)9r A1  SW1[ LY` 'y?ƒA34yfaƑq!>kd1c"J1 Ƞ!DL$^0-_@V~a0;1 0Md&1yɱ j@С YQaGI)IGɩ?\ #k_a[J~"qy{:Ĥl*! 1i $ذ S3,CShQ᧷b6j%5I!z}py3#j_m@+«)4D๓6K5ǚ@Jɐ R 4 *瑟R ,B@6A R $ 4g5k37cu /.Ex\KD05@G1%{cf 5@ ga@EK  Jp:X P 7 E3뀥)'Q (؁p (8`@rA$r )%E?SācTM( u~[@PD{50QS ) 9׸'Jx\RPRPL[ukh !1*3 \ +ѳC`4 4k)ӤkqCPP$0Pk;1cv1{i[ao=) pKۓOm\"D0q r;A ^kII+a+Q3 \8๴f1nF IL0pH˚c+ {K[@<@ Ul[S {<^q< Y0T|c a C- & nja KV ){1 Ǎɬ;'C | = 7D-\˲?kd˾Q }P،ra̅IT) AGq @!!C -:LDDD%k`q!>uz]}CDOb K{# +/=:0 u  +.ҍѾNPAl `Hr02a 5 ,.Pv&`G+2npݟt / )B.sv[ *rc* R?O !`Me}QX++m_!k8Ō ؁ ƃإ! AڮŘ5g!ۜLBnKĽɛг=&rL?p۽=7^=|=\M33S= VƎ#_Y;?5Z ˀ]lHQ 1(#o=-c\3h9=q`= ؋|3 ߍj) @qǓ0,u[Ia%b t'.X.p?z\L }k|p06qܕA dNh˦_.GcB!3D;wnq~t^yN@G1@W !q` _J3!NI 9OxN髎.nL a EKP` Q37;𴚩 q<=  NZiT' -,Q ]Sq`1Xom%b`g,RJ IpRVϕMEJ!?YT2Y?K2`x&<΅g?i@>YEs/Rlߘ;k GEGJ&}`4ӊ>^(&o/<1Y @ܖ`)O_s'0+ \ŀ qzɖʿl q=8[C/w|DL _үHW p"D3B@@ DPB >QD-^ĘQ#B~m6Y֬ ޸yɒWRpܘSN=}TPE% RM>羘p_.KZZlڵm8n޽}>;vpōGp͝?-W+2]ҩs{xzݿ6}|ǟ?#I}`&d9 D0A"pt0B A xFwƟ }CGt.!(AGUHAI~1G;!$xC.XlJ1h aI+\͉mR ŒIJ&4hxa2߄6yk)s3Ł2!74PΉd|t=,)lRK !jM6=%TSSWe5iP/"Z,H'5W]gWɊ"[Ҳu!DWeͣd*֠d6[mz&LREbV Iv[ueעq  ut7_}"F %~ *U%}fYD @_< &w .Ua?^S&%Ih*֘ u)X{AU ^!(2>I^VfngQ$FV&’i]*0 ktt͎Ɇ;lrnk[n[o'l Gx p]:y?d_7`FFd&|ߧt秿~?`8@@8g4Ё*`%]c8Avp( AP( 4a U D#\a e3aB5: y8 G! C$'k01Ai:)΋D04EIZ$p (PL51-7ucG>яl l\C s3 ܆dz⎮m"4tlӢA7+<0*EADn6)s) 6! P{D*HD@-6,'D.c2:y^: Ő1[ O \'yQY oAr$bWD!RKb L60?c>.a 쟶G?~YrGX;Ph&# OxA|)p#{KbҏFo<7KmvC\jg .Ɛ4nᳮFQŽZn2 AQ%WY"22pU"Cu"W=+bjMF\rfx 4c CD|X![)WaF=ACpb!5H)+pE )AW*]+ T 2rV B| feF02f[ŸmT[ oŤ.N2<>cPNr1nZH1 Zp4H2 a#gθGqՌ ]E/^{Q FY/Al\{_GRIӐ@r,ë e*A*a"YEv1a^.YΖe ڂChF$R̙'8@L eV hwָkxC~tCB-] !~]fA)<KG6AkʘY N5_VE 끀'>l+~s"]:Ž;nrFwսnvw !3Cekoe˾ c9ht@x\D(#wyE>r'GyUAdgZ7yes?zЅ>!.7zґt7fLwzuW_TϬ@a__'uKb7{O4BjC~w/({7$3z?ȑ|p&;|yyGmH N}k"e{"@ x?O* P q, )06M|v(\G%Cx!it ~ 5{bħQ_r Yus8 #?3 aX?S0,`@{ gІ g?狈yؿH҈j‰83hpAAH dH\K ԊAP0P%1< @ $/h01P+:ÁB4Å-xQ+HxR/#CAuh‰08(vpv:4v0UlT6<SB5dĝAEx 6` 7,F9t`Ɓim]0j%ġE=ŜHńE@x}`Gv \“`FB }yDT Ȁ$HT4ċqXāPȂHXT A`2DTA(~`+T2FXɖdɗtI>x;/|r81XIĐs4::7(tO=qHAcs$};O? !ݸKu1IqDb4D4HS0U T(dӻz0\}OԂ%G_;/Rd1 ZňkVE7K?0&ꂥHQL'BmWV#҈Pm 4ԁHx-;'͍ma\FX!4 xݤ˔TX`RXZ12AՉE¨ņm$QKphC]栺$p,Hƍ w܈i؇OFɆ#u,@H %$ِC* Tmj]. ڥ=P\-X2fvىq` v!0fm0}pE )&e|b(2(⍨j[p`A dsԁ _\* c;(0S`?۔#Fp>J~c/D)5zuDzYeqlO r!ySpx۫P9@Hަ L@K[\hLfL։*`R=Ci>8C. w `gOVyfm$"N}2a0aZץZ ˆvneg Xc2e^`hi膠霆he?ixj#> M͋Wpj~$z>k̉i^ˡτ>&f1p&l6k& - h8XqV8S |X؆W!O6%/ΉXY t*Ӧmㆨliff&  c^ .7DI|bfόih불n΅Camxlo8nˆI,8o(U'юP0uF鍈-hPj wW PAw><ᵈ煠VѐU KnX%7U`J&(xloZWJM< N3q {ǒ0߈qЇl,@/xpny'yGygwy8y?䡓 t`80[!BAiG7zЅp: !yzخhb&{Jp*"}*~A͈(g7ʔ Ih P02j|(‰^. ݉*_ʿm X*0(x{Lj7;0 qi=uwfn}nSow~yB@]7oLpLWyPT -qd阎?w],h „ 2l!Ĉ'Rh"ƌ7r#Ȑ"G,IR oʍ7pZY_(wDi(ҤJ +ҨRRj*֬ZrN)9o`ٰ_ײ%ٴԶrҭk.޼z8f]]~!n1Ȓ'?)ng @-͊Ο?)n5زgκojh3~k)8ʗ3o,d$K0[ D%n/ mmm++9?] `I<D5@3d)CSLAlG!$@KA f*18#5"@L[w `ApAaC53 s ^;<5*EUk=C|Ie>㣿>???S?(R< @*|  R3 V  C>&O6)l! c(Cļ Cr8`>!(D԰ 9axN|E8;-r젫^<#?ud'=`m79vd!a QWZ)H/;mCyq|dH$ D M.0" @! `0 !*SB8đUr. GgHِS?6s(b<.dd F6zKd2 d3&U$9hJG,ӑ\C,Aiy'*'>Y4qDcHP7)Ёv$H? 2gA q(FьrڨGC*t&=iJҕ9*m)Lcꚗʴ6m Moӝ%<)PӠRQԑ uN}jF թR!R*VEr*2 \u7*Fk qBŃHT+^Ui=\ ҵ%CWy}4 kTk*!ƅR}܂,bX4SYHH)kœ5`E=yjhP"y#* e PL!VGBWːn?T\ @ L4'6"۔. ( A$ozPV[Tnh6A d A@AaX-0o1!Zղ b o 2a[ UC0n8;/0 ]3V o F PX CESBWPyBPXCYA2;~p%xadf,.AB1 rb%]^_%*, X^܊ !>A YLv %H@@! S\kŘ?Зx; eŰ0, H!񩫾Zڕ 5 pc;6#Q^E. |`ZPIk|6‘p㐷.>lC 0jb'vHI6CWؑQ}Y:E-=_f nb]jM| dž&ߋ>c`@bWv:?'68<p-S_%  051-T548p * A_-dS: (+1v -5  (80; x/8,R(@"R6! -p?8!v!Aa Z(5])(8hAp3"!#DĂ00@-!O"%"  88D °5BT))(-8b".O*!-@ 0A" 2^ C 0c).6@ @ !A(Cm:]]8B,@1L:]R:@^O>€u7@6D BB8@&?AB@dpGFG҆fBQǛz-CACiñ<CJ[P_H&(` (D20U%Ah @@Hv*<EVuLLH҈uh&bCPR0*C<+ADUl AjVYDT<5lǖA$NjF錠BTC;hHVphYQyHQd6Lviǜ@kRfF+"FK,,U u)rȄӰÊlGh6Hcj&P`zChIj;#BGj.F@DPl!Q--#-?#?rj*@ 4a,i1ow14q6sĞ!]11wpvjFLrBp0Ud$ %;S7@/ 3C`IAd]-8TAPt@W@A@2vlHB2+-ZNQ4LǴLϴL_)XxqBQ?KPQ:I .DˢڴBitǂBxU{0C@7W%|5'C8nwUBC4p>v !dv0?wE< W0Bv8ؘ.;<"`z'rd['C8fd`Rj #*_L,tu{`ۏGFɈ;녲;;8DW绾{ܴaK&|ȹ:2/E#XL3>-ׄJ_>'rǻIAv\>C5p;v[>>-~>ﳾ?54~GzOV.g?-#4o8$3???ǿ??>>??@8`A&TaC!F8bE1fԸcGA9dI'QTeK/aƔ9fM7qԹgO?:hQG&UiSOF:jUWfպkW_;lYgѦUm[oƕ;n]wջo_zu%Rþ{bŦ%/":MOB{ `,"!X/y Faf6Fk>6#gotĔf Y m@QFd"n 9j$s"b)F*glIDl =Z&eP <(,NH'aThJ1%HVOt9fBc dli"!Ȕ C Z\ *E "!ؤ_)AaVVՅpU>)ȏ~!q NQ%KvT %QHUYT'X[-WkӖ.h NzhWW:!kUb\S &4'҂X N 4hA]Sv-&ߦpNs1xb0T(ޅ$nȜþqUhbv?Rfe`&fJV.i0YW!CLa&% f:HPE&MZ&dy H4 d%P=(hVY6ZpXܥYQ&k&,f^hEc$T \[z[iwHb`)&Yv bYw^Dtdlga̟Ea1 O>"v^jXqh] _7&_|%i9X-=O{Xz3ߦ]^h"0`" _X/S-r _o",XdQ_B׺ %,J-j1BB5;1^GȆe4 a"B!A^HXo'Ѕ-g PyEĚ.]i" \l+!"kŁBEqE)ux^*D7|#HEt#!IIN%1IMn'A@bD(QJ8с7֨Yf-tb `hl%cκgzҍ\P 8Ns*'h ZV )js 48 hDS@! 0Ix@Z,C)/ Q`)z p+L9i@f2&dI7@ U N-H3ujk"PC5%8iX|U(-Qa1)y&M^ 2DTx+:Wh`b @EPEXG(I)$XD6*4',kAQ@ AmSP-E2 JR>,)P-lЊ`@Yb߂(xA@CAp] $+p^#$JnAuHBmFh eㅇ dTlu&E.H5^Ԑ12e68 B|r6u5>`!z3~38G8@ {98S8 A#ws:YFQ,a"6*ːBC!>PЌQ,-X+&aASZP<$J EN+*B&Գ^N;%@A+60<MHKB& U` fĀ@'!PR\ KL78MC@Go4:2`#7BqT T I?mX2:5!$PJL*(E@aPDpn `HĔ"F8"/p#cUO* .2!Pw:$sP XpEGzaB xb@Xa+HWiAPl"ŌtAJup(G_wB )McBPٲ @PCpB#2Yl`lH6!`ciռ LjIDAt9A}<9gB C[2y͜Bwt@g;vVOΙL S I`AR:U_.GHE ydg6|@[0d0+GTqVO>¢TTa*БZ&JpOdS 1J\8!J /Tь Sm4[D@t!D1b[X2v}a#+¬,f5[YJֳ-cE b6=-iɦvll? +p<-8 q8s?1(bQ1 U5d &UJGV]-nu{b;qẕ*6ȌjH u)~߇/~,`90\` 0:nLa Kxp+(~nWoU*3kDjI­A6ܳ o`"?F}a%"f2lb*YCr|d.'KTbG qBZy3j,fvh\Z:pnAz΂3YhYa;iپj*.CN{ӠDBMjOԤ>5AU!f5cjк u[k^׫6]jbzַ6v-k[cKjf1tmp[v-nm;nͭux!~BMnt׻wonwopUk;|'RN?& ٘7q$ ~GeX(OVP+gN0S71@Q O H + =NԩcV3b ͥi鞺؛C e A1z2|7c{qw!*Ep\ݭvv)+"Q/JM#=ncWֻgOϽwk~u1 jHt`y0~_z;D1$ i0x ᧜۹VJ?+0!VHe᪰kU{[ aD`qo:6jқŠ2B:1p qPP@(ZH@B@{Ӡ2nЭ * 0 r!{ѷG vR1 ?0@@sK &'#` Zp RU0 q2*g-qiTBi*g5`(0С\C5B{Ap4 *!$P\[ { ɷ _s؜۰q V\6/)d;/;y,BP+TQq\!BRDʻaX\hõ 2[t Mtq 60<аM5 B`p+ ܫ@`RQPH7- %=ԓ&Uar: Z>$:PSƻ0 ^"$9  `'ưa {= z*%\-ȻR v0<#ԌM}  Nfݟy _|y m@ٚ=%?`1*ɻ{S w71]{}G95 (r;* P<w 4aiX m+ ` p\qq۱L;1+@ȧ!'7(!.j4G]Կ# q` 6Z/J㯁1;B.92coA$ݳ` K>R" G;YL° J \p y P`MS Rܢ̔3^6g !һP<.푥!~ <0 kā d0qg3ʪ׀.(S%=Nh0Vˀ6U:׊U8A~ё$tP}ma7."9=Ή-@l.0"jc($#  掐%80 ־& >q3rF@R}pBXP,O $H=1 0^EGLl~iM[oŐ @W6  6J+|vkgՔ h4j3 ֐6ɥ}n/_{/o;ߗ@:5gN4_rÑ)_ k߶ t! p P 7ţ\j@9"D=ak|^J .B(-@!GA >QD-^ĘQF=~RH%MDR%J U2+m2|#nM=}(2 TRM>UTU9UV&$:]͞EVZmxݽ^  `,Ҳ} FXb!`Dt ?y=[ QMFZ꩑Y]2脅cƝ[)bK,OogHr͝?QfvZiQ=.6Ѥ͟G^}p\?51@jR ->$@ "UcCB /0C o#Q96$DOD1EtHd "%;BwG4>B]o0dI'#R(% "($L3w RfEQ(q<3O=C+ ROCE4QԂ1&́ F-14SM7ŋEŰKTSOE&]p藾!UYgVFV]w׽.Wa%ح26Yee$d6Zi r6[mk6\q=<"7]usIv߅7^{7_}W#i_&5`I&`8ai)8b'j  LdRl褸dO6) XgFfo& pgC ggy zpgd䡇&:kF;{ŬօIFVnv jXnxxIO'ygy矇>z?v7' d畚 */3~Gwx &h fX]ERb4E 5  D_Q!k*;#- Vt4L4TJ J BFAB+6$H6*x(vt`_ Cb.1 P1vPѭFj44Oאԉ${1 `QD ~pC 'ĊF,2X[_pUFTV z *sf#{ 5$@Bc79"r>q:( \ ej޻ks>>07C^ˇ6ҖA 64ܳcd'JLi1944 #J?(tP:2-{kSR@A{B,Ĉ8BĒxѩ/-$ vpw}KTőhC #@Bĸ6T^EW\ SA;T]gctӳJ 늢S~#L| Q! ƭS@PT odpGr0v4@[RHٙm9Ð@ЇG3ɁRȪiȏǷHȊ`7C vbmqGx A,Ɂxq"Ēv;l;";;ɚ[܆k<1$ D l踓;4 ŻlѾhZd[G1?ƴ)T(6m@m :~X:eEgІC;'dI   +( Y$UՆ@-6ԥQQq/k%^Uh1ѱL5j4܈~PNislk:g Wc0! ڄr2lM%d{Wfe"ՈRx Jzim }هTĉ8Ӈ m(7(  ē}HeYPJ$%XpԨM=#:YxS6r~L}OׇwmI}+ېe8e0Tl[10Qς0: ˆے@]teuxPhK Zb0 3ʇ -ܮyT1>~Y5zAP  tèh5\(BXYP'aŽx<U}^SU]_`=أŅ0؁h(0>5߁Nπƹ݆R9@Hx  r$%auNkC!^@ `i mO"FZ(H%Vb8)! # Uj%(dxM(cԨ 0%XxRO6F n:`RTyu>~2`^Zc[R)]ǝp?{LM0W 凛E xǐh%bddmMZnLe\VR#2W#цŰf䈰m8 d1xbIu IYFb_蟔M c;yH eep~}X6R q9m݊ ;stLQa UȃF 8)g#AxlyWrtF v ./U!~`20MTW@鳻?!8Bmsng7߆}Ȑjgؐ^YRJ9g;A v9&dmeXiKk[ֈmҥ_5 JWv 0F YkK5uE<ȅs JE?j2h)/.K8 xJHEx~.8ghRMF(R*ȃhA0:_GРApdcc\ m8Ɣˊ^nFTC pt q'qGpgqz? jqx(KmPҏ`oxgGHAp)) 8CX&G oD%Хl4/ V ,O58, -;x3je9ov֢0:'p?GtDg0S0Pr  Fpgm!#$PoN7 1W уlI`TIq;?n]S_ /ԪO]5زgӮm{m_g7D~ka39~C07Cy%n*qPoݺ=ӯo|}? 8 Jd^H1 J8!Zj!z!!b%x")^$"18#)X#9#=vA 9$EG*$M:ybOJ9%UZI_Wj%]zIS_9&ePg&mq9'"Y'y'w' z(&C2(JZz))CDd)&6*OYDĚjz+wJc wLҌ0%6~8#5@,By36zy5#T9TX)E2PW1QUe\L(T0@hsT#J3L$@m^$N +!F0Ql `O "@b F !@!AYs8KL iaqbXa&+Ph' 6 1R ) 1CZ#J#E !˜+%0 UY@dyf$:PC "\`9" 4B5 Hb@(U`7$#mEhZ !h,4IJj|0%p$Pʄ4#E5d &5REt,LCCm,r_0bYxԅ._|3D5g4zF<-g8#hwr,n2%E0xY(c0a cP (0`,izQ";օ8I:h  ` Z cB"K-k@C6-p2Ph!ֳ5x-53~[!xE14m,= hs>PȦJz-_ (fr{ni\1A. Tp2 ?T?2FgԀ)Y"!9mHٌA`1p5Ɩ5 |P B~u0d<pp p.FA x]~4 24p7fG,Uxs PGb`^(\ }{EOx(2+6 7p$s@)_x:cZXҏy]u"w0~?#د?C* !=࣠ND A ;@;uə x`$D n$D vIKBD?i;>S!(?LazvޅxA< 5b|C+: ohAx8ܨ- t?T4` >1t?bXCx56D 4֍cJ@ bC5A <\%&7t 5G8L6`a<s 4ldE J`bJʅYv!]xCN_ 2S@dăJ"z16KJ >`?dLKKC↩bPH*V։VdxCV W%L|XChAA.Te]z;< CL>c_<?dBt?%trf>DB>$r>sRȈ5?㜄g;P&ynpD6{}D8@vŧiCRQClBCUJ͂,Lݨ'EȤ2(V%bK LX!jDԠD$iB1ExXa%&D?N:FBjh@$/M\Ft;hB 5hKy AlQjx)Cdʥ<2(RaV(ALf̩Luϰ栊JAPV6bbyeu\lvjjCD:D:Xl%l5Hk)"<;4$e L2D ߢB3l3@tH,A'4_@#e6BӇA `8@N0C6dqtAF$ijIG`4TtJLf8^~Y-+&OE-3(U>SSGRGQGU^VE?\V5Mt5X[ 5ZY5[[5\ǵ\5]׵]5^^5_5MT5[C` vZ?8^#M+[kGb;6^CdKv]KeoTufO0?bvv]8 %iuZujEpkkeӶ\hsTmun7n6ppp5q7X7ror/U7sG5tGSO7uu_Lgv4wwD7x'xw>KLX:yt7{?|s47~yT<䤡N/˳l/87?8GO8W_8go8w88888Ǹ8׸88sV\|BKkMBPӆ&Ӑim87D8NPY9oa VGG1lF2|0;4H2X+KEODj,X$9oD(X)칺T_*(=6߇1:T8=C$@:QY`,ۅ)ZDCȨz9FBCQDDYz]FAtzCЀ=%'@ {X2QCD hh8,B2eTE'0D5;Dd40Ղ# %5;(QBA/Q5DD+-[C'DY!4S=)P@}pB#+DPB4 pe`AT& 8CIA@,)`BhEq{4Fip|B<{ˉ0AXDP$ DABA!O AK a CXC7QDǙ534'C UE3-5 oQ ْ/سh0H}x&xBuS1ݿx12/8G)/C0b%;|-|B\LR0P0H-- 8qM5741ѓLJ/(] 1"ύWGL>KD.R-B5-.?4D[EFY@80/&Tp!F0D1fԸcGA9dI'QlY*aƔ9fMm WN*z fR.OO&:jUWfպkW_2lϞaQDm[oƕ;n]^ջo_4vW`Ç'69(0ŏ!G<2 Ds玸zexqOtysϡG>zuױg׾{w?|yѧW}{n> ׿ ,IlAI )B윹P 9A QI)QYlhC g2t 1,S8Q]aPto\IB, \10c'40 TA @JaK`e:H Ћ*=I%'xHeS,F8%ξ:h3cT#%0TT)ӶxFɓ$*HSx!ưzh`bd|-T 3 Js VZ.:01!]n񦒁,O8a0iA2'ZIȗfEZ+@[ @`DpV#` X wHØZD,[ O=ƒQ"F.zqHY P(J@ E0"8y0j6 rع`#u6.Nc]+[f'[p9^Z)O3(&j閄,e]c+xiGZ +n)^tqzv[b\t^eݖf^&O*Ew]b7I['-ſVHZ _CLG ]xمZ&_ψxɩw5I:}#By!& /)drJ] @DAn t ' jt- ڂӑ/baAL- W]B &>xb ,nfsHBA#8F5!moEQu6aGczGA:0?cXd?` %@F(Y X2d.IHQ+G6D*?JLFD''YKP <1H3,AJ].t3 ` =%)lB݌I;1$#"GcͩXrY&CMys#`0<1u䝗|C6Oz !B!tZE-Ol@(=ǐfyh4 P,A*H$PȒ"KyvDꠌ T&4g@zӤDP :`F>3K>^Ĝ) ,"@@TwĤerɖ K#kBUJdA9>#+Z\)T UPrٺT+jPlIzZ PAg0pGDFU@-EdZY!F Vd6m$aB Ĥof!ٿ6D[5FImFrdJ%k  8UU#sS㚔%,`@薠^$`H>@nC4Ú̅ц6$" %Zq8+-IPKiB^D`@"≈"@|dn$EO6P1%ea@ @y2hL*W(_6!@B H"Ȭ;s$rb7HbmV.LE|ӻ]cQb4%εq}k]L5΁fn9 S: aE A$\m$xtAx2-A)anz"HǷa#sF ;pbwd086$!q<&Hwu0l<#H@涶 E[%3a`PDr)A8 $?O/o7UL0ҭv7G0zǞv$8W( !,H*\ȰÇ#JHŋ3jȱǏp 0$ȓ(S\ɲ˗/I͛8sɳϟ@ JѣH*]ʴӧPJJիXj*`KٳhӪ]˶۷pʝKݻx˷ߙ]KÈ+^̸1]R#C&(rdkQCMӨS^ͺװc˞M۸s6͓Ԩ 7ȓ+_μ~(DókνN7?z(JT(\˟OSPp($HD >F(}b-Bv|ʈT@gK+$At)Iaй[Tn" uAh ,+zP!SKH1I,&dN[@KM(zo3,$74yR+ $9#tEU@3>|NWtA0P'`?ma+wIw*d@K,P-+ɴ@qi |wAw0@,(sBA/K.(ALB!BBH [ -[\,K_:w(. 7כ_~.ҩ.B>Ah -j֟K.H3qD H8 %+b1AYpPPhCiL4 ZT0% G7RLP$+"xp.:ԱA0ψ0 HB bE}1PtyQ!A %A2B2taƔl#?dVHؒx d )!hFkq"4d@22,d'IITb $cZ8'0D:HoqBEf Bh%D/ brg *Tb"(A$1C  ؔ2K TW<Њ A ( Tnj<}`N "P~ cAbd$ڹR 1H )l PA1o%r p"BPT!F)ϖް!C#!@f D.@ 'q⇃| NB 1Ī1F: $q. \X NȂAJ'pbH 9|~ą`-H` ̰ ׁ4ueU=P.@eGJ٧ :%;bL,YykDWZA +!0LQUMXV 4P X!l;: >t 3J(Ȃ )@0<48+@b1=ᄦ* &(f._~58 <lHC؀Gq\cV H6p"$!V*d8*^X c3.,ktf6r y 15y'*+<[7*ъ6mGKr=|1V BZ5cgEʘg-JQdIBėS"PFq<^gAj.a,EcBeGV3c ϨV|Ђ L0tV&u"m슼DQo cAmAh"Mg7ؖ6Dms-hwnn)-osӻv}oyv8km ضna ÇdSZَv* Cr,Q(7f4@dH0"˹w9Ё.F9ғs0]>z"3z՗t'FNK?8Kh烰!hQ|u`G7sC[{u‡cG|vŧyg!w]WPʆX."zk37EzmݯWw={wޫ{~߽xUmk3vH|;@WSֿ}swߐ'?G}Sч?~7?~^ ?-8gQ( XX(Ȁ؀Q"h$X)'(+HwA6x T3 }u-5D0 ( SN' &ʤ40 ŀ$\H N%P օh ^.,HrtXJ5M$s8}X ,0` lx;"  PSX+:@5BBEX1 T6$.j a8 )C\b,G C(HHP<'8 4S҈g Rw831 .  c& ʴ7p<혏}q4($y) t6 ` 9Yyqr8i*B "9 ّ7gB,ْB293G18:<4ɎAIvu?7$$KB)DiGa<"aM$|;CE9Na[H#I6Kt^C~fY#0 }Ir ǕNQ !tFwˀaԠ y$~oYE}!R5ę 0 @h6 W$@ 08Q9,q8 FI @ V ) @PM ɚ7`)4( UFT ]S P%V3pY%jP`! Q &9%P'6ZOp +@402 /YP,Qj1b! @(bpuQGAD p!`G!y$fV`GJ#nV`( I@x4EY p]13$SP2lI a+Z! oD`DJ\Y&epYP!%B7, kJ:Jc*Pg*/q!yNY1ɥ>vp85# ]*vUEfjƆK) o¤ѤWI & C"6]ZI*B8MBZ:>80 [Z  ?8Vd 0 c`  *C S,@ 'q"JKjP GЬWqVG ' k G㰿&q('0"+?8%1 1z9mdA F{HD ܸ< x R;R{8 XK @ ^S3auxP[Ctmمd+wNC t v{xz|۷~;[{۸;[{3)J53D aʹ6 ɠP P`0G4~4 ߠºs;x A[C6 Zrp<2T%KgQw2 A Hj$"L Aaq #`]qx%+qmkl ,%l,__{$+*)p |c0^]C24L< Zt3@qS`PU!L"3g||1]4a+F@B/Ab YFKV8o<A"Wg>Qʨ,% PgᲣL|U2G {ȝvDAt78 ӀdΤ,9 R` rlS |, ,O/9.i KlL h`@]?6|i<<, Ŵ/d`WPЕ,` ]:!p$$L*=I`Ýd79=x$ "=,|f`sd 4˪˚Q04QեB?! @,'|WmsIio rvqx9)6NvqĉBq/]mDJg Pp\11` U` -l@;-(p  ! + @zc B0p&P = c/jq'sJS'þ[ @Oe`,Âj'M! 0Rz!K-Cm( 0/O]<]^ZJZ 9$ ѕ e]|@)=@@}`N9vMP߄y8.)CDC~,Qr%c0c0+gstu剂r!f_%A0lN kK!\>-ױ[ -gap’ ɰ 0’Nla؊ (9%By y9` ENYasn)` mV=qIR@; ATȎ>8@Fl5YTB`C->qoPHլmZ쪎D.Ґ Hh?~ 1899.|hE` ? Q!,_ ^ҞbmB8, b$C.E: Jh,P<)# P pܜr./fcKP-p/  tl?a CL/N8Ju O0 1 0 ,?F`ƙԀԏx̎ .` cC~ZoX, Y a1P 0&J!?-@ُИh~y]9਻?'Q51H D=@@?FPB"K^C-^ĘQF=~RH%MDR2iQ]L5męщ0T7< Z(3KR=nI`.H7r ~VXe͞UZmݾЖBXtk 1XPƙBtڐrXXdʕ-_Ƭe^+R$chefU\pōG\r=Z04R5;,vݽ^x9BY@i;jx} yǟ_ʲԮm 8Ǖ(If/B 'B /z #^;5\hO$A@ WdE_1Fb;l i8#LHSH#lDd23 ȠziI&J1$&%T Z lxO+"+0p$PC)RG}H!ZŒR5?,rp"CLOEu2j,E20Ft5W]wC7^S%+{l!Yg(g}FL]p!\sϽŖ[h%vI7ܔ!f7_fZ~pd6| ^ `TUL߰sd$$㏄ q d Ix=NN9wyB ~%c fLV{iޛ9Uh8llDz|ʦi>䆛z Yo&3O`Q7ދ;J!wt>W hv̴7Yyv Wj0/":լbǬA#9tRW=% 蝠9h 7kڱ7rpHָf0ov|F <_̍,ʼD,=$$L6gdL!7A V& B|"(THXЄ'D!JB!KFT֐?@)1audG@C16dyB 0c{Eb'.0 `W XQG@EenD?HF RqXQB>^QY4:C- C fF4iCxlEbRb"A :&Òlt<8(=r4+Gʔ0[8B1oK.×caJpُqZ0 ~K&I%W]ӛ=A1JCi9m' 2s`D h8 t jPz<جGԡE1 LHbBUHAG7|J$X:S (p@@=tc F>4F^hY˩OjT:UVժWŪSbQ`E<,I' ҇խoL'Yr!9k`;ضpC &ڐ Yi;YV$v@a X# >{CeUZi1Ak/`mmnjK8Q ָq\^$ͅnt\V׺uݷfC^5ozG׽뒡}3,{O^L<-&5pd˔!m . !<1y0C"a wBȆ6>ap'` D(&  b3;kOہxA0t"x'd dQǼgqG'zKo%Ћw3$0 Tu[o׮p+"@0joFYT1 F/~#/ HɋfC|KEM|E˧a?ooq?||◺N+~i}|7w?h"aC~ml`vMk;}7Lˤd$K.|MR_` OKeH|ڿ4ۈ>d@  $4DTdtT~3g Ajzk^$B A&"Ap46(+,-,.$jA."#o30Dx!nS8!Ł%.肁֌`bX䊸LPzԆ(VZ+Ĕ9cwc?}`:%>uXkX݈FVX̽Ȝfl&/vwfa&fy4"M֬aS4RfC`4fnp&n&o^anpt^`fcHˆgh//g4rn6ng"S#e`Fit3H&pa9d}k`P0/164uC1؆kfxJ%mC2`vgH8cNtvhi &e` oQ`gifeA kh1ial`z kBDZ3j~ᬈl}nh dlj9Nr `yJC1kfik~HIgv`np.:2=n'6F6oцi\bY5b%%i4Mp67G7 e FB 6Ia iipءeE߈ohp01zȊ ^$pObfnHWujPq`Mqa%gr&g-;qNQc pl ~0&FQw*iNn$qN^wPio0cx&?2U23'4o}^syt&g[/>^Mg~v6DƲ%)1N"%}"}C I6quxGoq#%amz&v h0dW`x@Q@Jfghv65tkwEnp`ِȰG4[ƌqPBi1pωxi_+ڇ' ^ C`$OP%x.@QxR)o o}xn /m,VgQ7?C򏨄O_xh` w FbّxwqN׊/qRz=8MȌ{M{ Mŗ09L%K {M "!3lPvh|0PW(q#8/b'LS͹oW KKPMAIKpH {[gg#=HhQ?osmrxhGGelx@Hx. 9~,h];w v1̘ t h#Ȑ"G:$ʔ*Wl%̘2gҬi&Μ:w9h?~B\)ԨRBMfu۶qbGcLLf>#ZmA됨6z/jyF*%+~:lrAqYdG+ RFΦQ]`;@:sM8qœj!Qi}!QgY;[ ;s?egQ)y$I*88EI8-yK0i:IهA@ϖ83h̅]AZ=%yOBYzSP%e!S;-<} #Gvh(:PF*I:dCRCJ:*kkG lm(u3*&c-Ej-$IzD4E 0.yEBVl;}J9KmQBݙ/ +B4o\CX*eZmYпG-v7DY82̸5aeRȠLO A-/,u]>jCA,3YkWRз`&VEi/-]WA|Nu5}mӼ>YDeeV ٕP$Ph-3I{9/!a6fbd8O JeԹ3YM;A8)LĄMCV ~;lL`K ?>O<P /aP\-JkL??GFHA,&K@&V;%"J rPkAP-) :i#$*C "`G^ YjW\ƅ[Hr$ѣ :-r[ChbDڳH Qp+l8ơN} ZbD0fFȒmXD#"7-)D4^C88Ǒ '1I&Ȗpfr!陹`G.UO%.sU2=A!#dЇX\mˉ})iRs&6ͩls>%8É$"Ib*9TmSMOjR'@0Zlny(S+^ᐈREэF4+<҈&=iDqКP=*})Lc*әT6a0A$%N䍡F1҉:R5b*Vլ2 UT Vf`N wDhJ!^ T.qyֽi5~`N$@J^_R:A6%f@X3CfJ#@iղv|7H[Ab(Ch D޲UL^[+L.2WyjZu>jhfh%qng+gh+ksX%Ơz ?rC'KħCc }0H=dUTmV*熗qe3$Ӈ0b`]fQ+SP#d0,f0CxCE[b]tyAy8QPDz D f`x|+9"l{uچ.M<5$%?Cë @%SH g b(נO$*[݊]]w V"˃+D{Y6̔n՜3Q2.Y7s(]CH /waK |CHLOۂ| iW v>yCAm2L-ġ|b(-%_ɳ \|Գ & `QYn5dqV]3RQըD}2ްs4FK;󕌩sJyeai>0]'+}߉<[__R?=S*B[=QӾ=s=/?>3>/}|ֿ>qus".b7~ЉzU_[!JIv>Cr ܰGN\C#T_F40AX@DpBHdfHAPj``C+D:D3*|]Ax@ԑ  _n:$ @7}D FnC !^ N!o]x@AHEAC`% 8@0`A8^1HdaNl 2Į7@HBf9̡8h[3x!)a(brA X;XCAAhLTE]HX`C "2`  L`AHA EE@MCAC8 ".@<-Bc>`2c`CW<??l̃3`GCx<죾M>ȕa!m"Ci!C5 x6FI:БApDDG3)dh^tWK. 3td$].<1%$R2yRR>} 9T^%V.CeV~%Xf.PeXeqZAă>[D˴>PM]ƄZYH`Ş53hȝ3C0YcZ H%\߇ ]eD^6^2V_%}lC`:Va$e/C0c&n`fgDYa҄,Y]:dDO8`>>p/xlFmZmڦ:&QZ_>8SpFCC]grre`cJ'{A\'[桌a繄VE@2 ՉEx@8lE8DAp]8Mޔz'^|Z<\L8x4C8F| ~u3C8lhWhEdIyŀ zV}47 ,2$$2% 2"-+4. \rP o\5\4FgFo4GwG4HH4II4JJ4KK5\CBD??3.4//M4-t@ @Gb4<}:lqLGTO5UWU_5VgVo5WwW5XX5YYY27ks4IHHuH5'5P3Qt&,H41v%/8Pay-PjJ8@dH 8\~Jr#IMg3[Os865hv4i3?a/|90h4@dG6e[4"F]Dp[p;Dq6^urrusN6@3DuG"eN:P8PqDxsߵ^uA+BwCRhC\7n}3l3xJJxJ8J8IH H(M lG7goxx8k888xøM833q\  x9'+9/793?G9_gyo9sw9{9yy/\ov{Ay99m߹y99x:yz/:?DO:KP9㹦﹥O:w7:惧OOiu󸀬:׺:纮zwr ;ss{]p+;q3q;{@1z8\57wzwNN{O{{{/O|P{A1;;k㻾W|<<»'<[j?|CĻOwQ_PoPH%9MNC13 K-QDJ4ǽ}F'1>?f#5{x(ք*R׽ԧ 0 !.9C4D,4گqGAMX/hCc~J֣jv3PSp`A\Ѐ/bÅd}C^0sȞB`LY>/BA$)leA@,` (@iA৻'e>XA>X"6bA@*@A)ק 6!F8bE1Rt*c{("I'QTeK/aƔ9fM7q,`HYQ ,9&UiӚx{hbV,H"x iي~|!+H4pYwջo_*} `]*qcƊ!G^SC>dOAf%a$3.ԗŊM+hvj۷qֽ%xqǑ'WysϡGo9ܕY X Bw$PksEZfQJU,X衉. ЂخZpiRnx5?tKN9./q"8 ]R͎& _njn  훈po|&\PAWI/QO]u%a]iq]C7ށ^/^ߍO^o^驯^{^/_jo?O_U ?4x@. <A Nvbя n$⧋dЃ%4 2L _Bΐc a=:$2qP>4< FDDL0L(E-n"S.e͘F51EX"G9KibG="uHAr)$!HRt#}0d J^ B䊛)Q9@P@8SKC0 y%/g! !0="B4#T:MM+3FiZDQ-1G1n!msy(8c gEw|ӨN8hYLav"?9|B>!"fB]>y*Q 5+>LTt2@ZS< GuiXciK;Ot@ӟ dCZBmci&81`#'B n64qE7T!Ut!cHC^¡}4T;D:v]@ (ȮVQCqTVK q =AeSpc󀇁H_17)a3mX8Yyc V\W"h^N@t'<^C6{~L".>M.<$:aX|V2@b!cY"1Bԑ df/F;2tlA@O7Y<4`RwcȬ8 |vbRGg1 #x%8,/ : ;HJCd%^Lal?M~$h`Ǒ] V5⑍$m?YJH(xe~,Ä5#Yh_ g@֜}LrYHY9wՐx8,4p֠6K\~8C9Cmp?E)~"lp|&; "ܧGx".#/"aPB+ &F/GY" Bq؏%VVlPz3-'z ȀBAP [Jb@j"j`^mY#jaaN  + kq!b!jر#j"q""``N2!a1B&.#©" $ %U2jd{ "!oPP(J[/KG)%! r%r(; Ҡ GB+ ,r`n `,r1U"A|'!પC'0J0]1%1!a UBJD).uȨ$bEӏ" T$" `:+2qPʹt74/"qr%!1mʝ  5tL 33"J!(@QX "> S(+r"Q#N*"j:a"~@qX *`ˆ  Q/H7<#='6b @(83:XcJA r0 Ea "46 #Js IԡtLɴLʹLtFatI 5a`fh#X3d! DA=)'"0Mk谯tBM0&O4T+u=UEnp(˒&bO~IXU4ZMz4@ifέ%'QbdA"nՃ2x !%bB@ ?qbuO^Txp5@\M&(a(!u/SbE"/.x>"xzMw~e v`  hIB. *_{a!Fv ;U`4%K"-UkW"D^!-n17:ȃ 3>b!71FS)(67 &,!ek3"s^>8LbS㪐< $m ) ^A1J  je"wp! blj;>!d B&b#}?Ab,:A$;+%!D=! EB "Pb!5@ b*` *`L R@ A!3!X &G 8Ao^5$nLaL+C$"\! Cڝ"!g B (h5eDzH!["IH!f ! B n_)DA%" !l~"]6dSd)xI;?`f .]*` !C1`? l@x,!4"J!x? V7VZ@&Xᷦ ( HF~" <80X_3 @aK A_((1^<2A-| 3̙4kڼ3Ν<{ 4(AABS*4ҥI=  'VrXIC ((@L* $!Ao@40_ Id[;~ 9ɔ+[Niq0.{ Z(+,bI[0c`Ɉn{ђ` 6k1XEXu1T&,Lq6?>O~&+ҳt1RA^/RRZI8m1xR/UGIb z(Y-l&_*b)cb6ZzY`- p - 3 3aLbK,F.8iА.-ycrIg!܉ҋ h|z,#O(JKGҤfVJjR@:穮V <` At-vԙ-ef:,d(SeFjNKmJƌv.XmN xK-V0KNRA_/Jz* R-.0OߪY-bK.Lq9%K* rˡ"L0LspAv3e6.LF!5/tN?]RI1;m22GuubMvi6>MY;qqMwvߍ7PiߛeqW x߂NxvWxۆ ߋ?yWxՓoyW՟ yࣟzꪯz" 2|»84r8/?}I;ð3o~>? #S4&Ͼ#c#O0 t/c*P'Ԡ5i#վd?a \n'#+ [DB M#@X> z4졇þc 9aشicq̏0A8E`AA. UaPI*&@Vx.< ` L{ta,q K*6r1>@ WR J**f^M" '@@AXGoA[3| aYH`? 2C Md 8Mw՚ @0TDdnز [!I3\1MŽ#R$'prBMmE. > 'IT&nw %Eoy{#_`A)ra G3х-Ӯ#{+!x9tTU,"#]P4$IF{WKUyR2׼SsLg(MQUqv w /h[@JǶ`x<1!!N(&Ad YE9XRYDjUS]^ !~[ "iV1Hx ql^8Ϥ9ʞg?ylr@J,9P4МcE,+7@0}Dő8*-vAAjHȜ) 4~*mM[:ל3@^S[ 0u XTJE*b hēo8W@9A.anKmt{<я~cQ8gd@ n!18 8qI6>.tLK)O&N3\%(_^' dߒQ: pP0"#_8̏t#񏃃w(<6iyҿvC yc! \ׅ.}|M;avO&〇>Q-ӽ-s/H ?b]xx$4 EpD~J9~cf7?~4u1 $<aB0_$ W|8ar.A@ǀ7uP}rYW03Auv'|)`0[s}/E'~yp~n `& Gt& @ېBpt e 8p U0T=GwGpY7+wA1*K*H4u̇$"7-$v40/~iod~8!1 CNp@' qngGA0Va|*\gghv' h*0(EWpw)vXH0KAz`dXJ8 `:h~6(0Ak 3[ x p10 1C1 R 1)rH8Zhw(P}¸hw`(َ(2'0AW'ad/Rӈ88#E`z YPAAr90vXwKuMpE˜Y&Q?ЍySM=~:hNx8)`0JM$ alyvP"qh+8YGp!YK@Eь/qfwp 00P!0u*䜭#~VDyꐜ 1`q` e p`<7aq"*00П"!P!Nv oW~C" 7 ~! .rajoIQHDj Hdk/Ћ41TjUT*՗  v /jaz߰Iit1y HqЧzJPÀ!:! 8  ZqG}z@( H 0"jڀ`  Od*=5*JΊɊӠE[y Lէyꈭʮj*  :Jj*Nk ;k'!ʰv 3 k* 4!@;- ;W/ *҃_1ga(Xeb 6j\`VhfaCT&($8&,0(4h8<7LːDid.=&L@DydMViXN .L/HfihɕD"j)tT."+{7*'w-[,VloKu0o_Y]=-Tϯ™@Ȏz3v`&sCjyG/]z,<+@B!@J3ּ/\ RL@@`1, IҾxb TG (X@C($D8G2p&ԂANh0B@R8x`*8yy,/ G `2_H* hEAV   Y$k:XC4H: DŽL 9l3IO 6+U ln d b=H @D)Ct,]cF hBF|yX\@'8J.r4 n@HBy,̈h&n8@1FmmyFA8Q˳dbZuT7|C]-9J E۝@vW4 K-J$(IpnqMOHugI1D`h"_ʩNw%9)P*ԡF Czԥ2Uy*T"՞:UHejVUvu_jXzէ IxXk85FekZW|mZ, cS`q9y "YpQغ,fr!,CB YІVE-fU[YJֵce;هjU·Lo*ҤmO\[lsS;V}mvcvuw-;^Wnz1[J HpUj~_W%kTz֦Fk LU/Xج6{Xc9 &0Sx.~qY<7>q_L1}*&퐇< vxrXβ.{`L2hN6pL:ۙH5A>?hHxvF;ѐ'MJ[Ҙδ7N{ӠGMRԨ@-V HNu蹈}ms%YH-.hWNf;ЎMj[ζn{MmX3GjГf [hHݼoD F(^ ~z@Ih'N T+sEԀ[Cy,$0 )h1A-F#@Ӂ|`Nϗ(Ƴ ZXMϺ,>)q N$QДA ZK-@ޗmpZeKůwFgVWǺ'Sϼ7[sGAO;O}KY#{^\eg[< DG}Kx @}oAo}F8 ~W}#F6!x:?? 7ǧF do0  xjJ,0w ا P @lh 7p` 9d  @*EW`!5J2p@à28 `'GH@/PEP TPqDp8P ͠Q9AlWH5e` AlA4X4E) 4([Xxq> a0Ni` 0 a@cQ0 kZ$? pL@ .owt7 #a p YP5Iq1 a؋(  c@)ÀPI +@z8@  q@n4D7n IP0< `?o(v` 0 T= QExDY0P~0 01 G ч n Or8 (\I0CtÀ 406*h"#i) T(}hA ð / e4#CR.o'+tCq3%wYHٚʁ1%Y9k3n嚺 iyCq*қ'*ĹyԲɜќɜy*CR"vzy* @ w)(3ٞ (Yyrn' Y9g'@ I j&O @)s :=+O3 Ϣvڡ:$Sp#3 a 4YMre ࡛76} 瀇 l PC71 12 (V"G6zH$` n 6 08N<)$Y zpS#H3&4<@8 t5LpE\ #@uz7%>[Х 3@ ?X` $zy 8lQQ<# &0l4P 9Qp #\kIa V52w3rHE8c +Up3밓17R R#Tɭ5  8_b-V%=fACgQ.qz p6( PA' L!I1i 5j)&0 ) z`a4o 5s%u(+:q0zP)೪&8 uWwA *ˇt6ek. l* Gg4!yNA %|ywv%r9Q~}u$"8A|%k Pb.q h `{;X [ʕa tyQ e01qsޅ, l2 h[q/!3 ,oj :* ! 0  P[(, k@)&B0z҄#|OŰ}1.! 4~O ]+.qҀ }p# RKvьAFժ8kT~e|C>W~]7xZ!%'dO5a~Q=#@0VqTn%@aPsM |%^ 0RaO `6NPa M 0 `-[Q~12ԞQY  3<! +."C{݋="2?\ o_z|g J<$/&4*,/n\-/_ |8:>_@;DϼFJ"l~cMGL2~KSVo!av^E^ȇ.[fh#! k^Agz1<}Ұ 1;z*ݐ | ޗ11 qrp Oڢسu N&oi pC4Pـ`_1i>''ˡIB1 5Jϝ E ှ_[4*/+@ D<f# [ -^x0Č=~RH%MwRJ-]SL5męSΙu< 0c8較qS^Ŋ3eV]~VXefԷ?~GP8~̛OZexb66Ydʕ-_ܑcmCp۶_ށɋ_ͪmn޽}c8W<>ic߸K*\^x7^}uU^|O(]z'+>$@?Ct0B 'ƒ\B YpC?10TPD3DWdEHESFo#˱G2H!$H#D2I%O fI'2J)J+2K-K/3L1$L3D3M5dM7߄3N9$p&tO?PCe7 D4-J%4SMfSO?5bB%TS?NOeUWsWgVV]wW_v<%XcyXee%p*욥ZkTdZokdͭ[pEU!7]w ^y={7_M5W_m_&XN F8a4VadaLu"8G59!UdOQ{vQ9fsPoRwg:h&hF:qfi:]x/fx.Edcg {a&;Ѧmy_ gƚ0y[p¡K. mɓl aھXE@3/}dt @6a='4.zrC3x$b%ĨA`{@n1CYoxCG8g Apunn sItP(0m*`a.#脑f hC+HAx ~AXA,d,%Ґ"$EAN *`A"2f!8Pq (H b%IjSR  QYԆ lI& $(`21n(sH0o I2ȁ0S% ZDHB0j(xC 2YDAɒ+  w6Q- U$h)Vh e@  @`3@I ]PA ȃ B҂IoxO v&EB XtM(pA H`( fa cyd@n! [D--آ"D,jAy.V4rK\Kป4bqL%#18ZT/쇋b F0p1΂°_@ ^0%-!ڦ-"ZbBƽ.ܐBF>wd3hڠyk#x:bs:&f N$W;H<銁:~SX' ׻cNKA6YA(=ٻVKAyB+B; A(`5%\B4Ag™)l.D㻪=}$jYΉa%2h}H[<6 }@mB=D(- x`d?GtH( ) GM0đ@ef>DE%cQS<0dA`.@Z4CŞkuhzXھQj>b\`;Td`Gk$mc*՛qǡ#CYŗz{|{~tC}HwG{ RȁXp×ӓDG ȍ Ȁɵ Ȑ$ɒ4I TIX|kIp;̫ @pɝɞɘDt;,DʤI47 u@}PʪIlq(=,J˜>ʰ\J70˽xP<;K$Kr;KT ˸,˹?IJLdLLpx(JEL'2#?pȋ[Lʴ̛<@)-JO\hdLT7 (135dFvFHNIGKLdLNeC&-8TT&TnAxXFUY$\e^[eUa^c>PedevUeXvigFhffffbnfm^R[[-۰]m}糍g+!gt^[}fg~vgggg|68YX"g[x]hkp0ƾhF>6]>FiSf葾h^鮣ii ,fj jۢVNej>:f2j76#ι)#iiQbF9b skRk#k#ռ뻦9xUn-UHnlAqV][X4S vw+S͞^4 vhɶ&2Uڶܾ{kmmn{ >zK]n.n~nyk띉՞`nnnx: on.owKmNov[onnOx; 1q_XKG|Tqk{^p9ʞpp '! [< 9!8ؓl[ELv ڋHEG:SxgyHxG iT9>8]MW һșH`l4jrxP˧D( <^ 0 a1xGϊeNs |c 1*썍ܭP蜘Fbs l0lʧ̆#8?mP9X m~kϸtpyȆ^'ӎgXnPC0ȐГ`1 I樉{O3?4v6|xz ˷9@SKt|hcP[x* @ gOпk0y4HØ ?{F#p GէOPo w΁zpDOM8:@n8䛲!x@@P/@y7={xz~ P4zx;>``R#y#x|J<W ]?7  |`z=ɟ WP y@},# D{P ?Qz+P WD@EQDs 9|ȋ=}}W3rpD.3/X`Ljl,h B.!ĈX .yÉ{(㾍1Ie(#m6Ngҍ7nmԩNNyRC@"\x5E7d6\ײ I%2>z/ b~ \1*A`A`[3[ S1cRvnke܏6꟬ WELЦX#YҵG:d3ӆu:瓜&M8WQOn%Lkv?vXB c_e☵>8!W YZj:飗H?]P:oR:Yp^V~CAf </4|_S ߜN?Ԝ<  0ܛi RZ:dIn lk!R(F# dB)wJ HC* y(D2hRA +})L9G n1_LsJ@<: ^*T>#Ce >ԥoU |j2VSUJ$*X ;dQ\BU&b}k=s!joJiUDW ?Qt o(FxKaFO 4v"h3乥Y;[1o)bf#b8c]'!xOC@ۄ`#m Ch-h[5laR02G\*P-hE[DZZ4 B\S&H[-T (<>:G-p^¡ @Q{'$ -1[-7 )X ;HCkD O8@,za`)EF?@DW[?,VdN&h+wK畒@ށ@`7Wȇ]<I,@Ƒ$yqHd/?cɃT'Ff+Dz'bt#-ISҖ43MsӞ4C-QԦ>5SUծ~5c-YӺֶ5s]׾5-a%0@"U$E &,U|` l63}S>YLmR7ɻ7}7.?838#.S#R}C)QemnBBf4qgio37Ȟ\sKE!gl;FR.H!:!D[mX3P{g1s^&W]& x` B h87$ɄDΘ  _n[mɇ}=@ bn@4St@  J>C6A\r F3w?Äq o+;p/`h*Ȳ 8 ab[ S"=E(ePCŒp""'R 0!"'"&7By>AD2B TBE`,@ @~TA} 9$AH<BH H|ZP@<ă5,6ŘA(@ L54KCD8@ Y"`<É AD^A } 4$AlG372@ 'YT,"S$ ?C4;sHQx*6h;6pR>|CtEʜ2;X>]@N S$JPH5Le85(EIiACmDAC:\nx AG?v4 I$[>4D؜MUhH,М>h eTACXVGX1)X[FxFo ?4?tIA`Q Eq@QHEcj@dfx)FV)fn)v~)))))傜)i.ʁ))H *HjC6,&**(hh2HC6vj\*3d55x꧂f483PjZ#u 0LV\hMp 4*}a*2r4iFd1e +@ nkQR4x*h_A/X+%Bd_rȂj!;@/EFDRk"ľ^O+4 DzY$B%&1FO5#W-5^@aY`eJ_hhDNC1lɂ$DC1AHB1BB2V! D :˺Z0/T 0lNB-TkU@H,E PL!BEb~-A`Pz)10-/-,C0΂,,AB΂51/5ᛪTaF-H-v-^--BC0.BE-\,PBi-& ^*-4ÔQ`[`ڂ-BD'N/@/'/N-M/07?0GO0Woh0A 0 0 0B o0_h X*]F"+ 540A ˩OŨ"Cz+-Zi+0|kQkVe@D1k 5@hȂ*qS-d`5J~&03 2%#52,Sl*\H}@A|qAL,Õ*}C^϶-G+X1 AL0ë0ZAN!.Bs5s1n "DJd3aAD-D冻Ô.JA$zAza411e*v-$l(;2|PcTT5\ݻx˷A`a]̸ǐ#K; 1-QvOV'MӨS'@An3%xh`sͻ4c-ADB$УKNo-Q|!ËOiL_Ͼu >/QFY1߀h% 6F(Vhfvc$ht!,Y/.(4K8<@)dE8H&/(3K0QɋJf*cC(AO0[pl.xNpN;Rx.gzgt*hB9 t$y j/4裐5'CnHB-n4IvU7 ]j^pb#e꫰~@4i뮼-@JKF /0kP)0-@"S-FV ܚ|J"5X/ J)J-iK4h/,9lP #\/wq/̰!WDp2,=k)A ۋsmrl$ g@#fUJ".0d j*Ey)h AUd;484F4J j.8|@o_0P !P+qSvDp{C#^$ .U?T@a Y)x U 4E-J&%Dp $.Ek a @Ss@FP%P `&JA1&n<"YiFXB?lҴD${Pq ̀ W¨ `G)`"YYZW B؄* zEh&* @` HEd%`ͨ r )rR~UL _lr_3g "," @jC]kL"_x/cCů$,`%Nz#'0ti9sJy^C'gI˔T!aAK\ )Ib*$ 2fΌ4Ų0d!5 gc 'NMd!'_v~ @JЂMBzO5@~1bfk$F7юz HGJҒ(MJWҖ0LgJӚ8=)AʉbITB:JT"pGdQ4S@PJL@Bd /na-z@Phnp, q״>k{HEn[ة2(CLdDܢP'Yp+D#YvfS_b ElI fY>IDEVÕF,|ЍtKZͮvzA.ML*^6v(z&{^Ć G8B&Ao8@gG6L [ΐ0Z8cV#` Eĵz)bmD1 P S X0 D'E[=S!EFP( 9-T*8@Q(4 H'B@&5FР΋*B Űo׌N n&,'0\")<ٙCF"@FCjJ-EނC恬rFBX4 b=ܚ'ۓS@pVqi9! :d܄@1TA"s(CGp@ Ļ5l (AVtuI@(Q p@Bfh).H`+p'p^_KjXo$ dJg61e/r[ FbN# 9Vk'-4d` >65c ֈN\es8v+c -Ht2 Y{7D}zyz Dμ7KlrN1ӷ "K\ī.*^,KY%V | Q/5=^´sd"CD >KjKϾgK{?=9Ohwe@Ͽ>[7,'|r,' T2z ,'E}B<[[x oQl[A "[F.p e%@% (8X"Pa,XE8nq#t' ]Qpt8 ꐄlF4pi 05t#a) *vXe qybW& EXB?XAoTZ2wa >u 8 Px:ҳ[|<؊S1rX EhQ eق+`["b} #!Ȓ\· % 2#AJF[JY e2q`wǀȂ<Ȅ\Ȇ|ȈȊȌȎȐɒ<ɔ\ɖ|jT_ ][ ߄ʬ r^ ^]!M^̒ ]v_x |\E,]@\ڼ<\p)<4' +[K,Ґ =]} MFbcʈ@b<芪 P U1 ):! #,Jv& pvH 'D` 0 V 031f ـnK0 <5*4 G KP ʯ }sP& Pn0 I=@p'*ua Ȁ Ұ 6=`6 @vI:` @w o) tWNgLP R?˰ V9!c@) &:"f }  =s>='ˀ 9 !w o ~&e3 afc Sf `  f NGEj&p8D&u0`w9!h!Q€ *h!2íq 0Pp^>nw2lŀ `:ģ[(6g <Σ N'0l@ o] l0U`*oV0q-F.}lP &- C0 ; .6<4@ 1&_)p2aۻjo,a /'0ze6> QS $0C 6`Aa AQ ʞ3 )BQfqmW ھC\6{ Xm ! `[ c. o & F0" B`4q#>(yb@b0VYI~ 2=PN #H c p =   W  vBKe' : WBЃ.xz< !3 w{"hWϫ0 3sjӕMn/ྂxς?_?_?_ld]<9K|3ɨ_Ey`0A?i@Mv <ʀ :yXO**IHܿ2=np&1.@ DP„PD-^ĘQF=~RH%MDRJ-]s@5męSN=}TPEWzbTRM>UTUn +vW~VXe͞E;pZqmZuśW^6 0`… FXbƍ?Yd(_ƜYfΝ=Zhҥ fSjӭ][P4[n޽}\pōG\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ߳g_~ '?d;xᇦiB /dN (CG$6 f pcwJEgd!(Ao')@ 2I%믁Q xI+ĒTRhp*ƚ!$L3Q($y, eL9礓' ת3O=E6$9 p4t6pq I ̯QKGҒvڡv4%T>)gbnTW_rPXeV[oE(S\wW_6X5W6YecYg6ZiZk顁Q[oN\sc{hn]sޅ7^y}\{MWuiW/z{7_QY y [AXၪacij tllk=fZǡ8@UoV 1ϪfUgh|8耉z'$ 2cAn .6& Ⅹ^vjĝ5蛯kE~w D9PZ)x4 oln+1WDYtOJDQgu8,qvaw{>~\x㏿ʂEn䟇 *E JY8z{ܛ(^[R`(l߿}> ?guw.l\Ѐx:]# \` JЂ`a iaE8BЄ'Da UBڊ-CІ7MqKQ?b8D"шGDbD&6щOb8E*VъWbE.vы_c8F2ьgDcոF6эoc8G:юwcG>яd 9HBҐDd"HFʦ8HJJ$+ɦ\ d(ɂR$ ) bJT'+]9K2Tmh˛e0aKq2x8qMy!1؈4@&hӊx5d ׸6,c9Muօg>CrO}s$?JrEhBP6ԡhD%:QVԢhF5QvԣiHE:RԤ'%.PRԥ/iLe:SԦ7iNuSԧ?jP:T6J3H.ZȣzRKUmRVժWjVUvի_kX:VլgEkZպVխok\:W֕δk^ڨՋKQZ(FQ/`cH"§doDiV?[ymaX=-% \B3-b;{ g8@. chQtJU.Φ,ҶPPH阂#)U;`8F`&~_$y0A>NX %$I7ˊxxuBZ^aX'b_ܴ7E41 b!$k`(m9d ЈA$@cXS"#$f)ǬQ O !r O(E(ҁxعQ.8B m9Ԧ6(H3Kٶ6DP L 9J%BAdC C{4$%B :sB`"rAd%tPMP5Ly"0y׻Bt^H9P%C R2d}#7yV<#qPIAV 8QA[cq-+7y@C+G(~"6BttހzB(*k!Cu%S FE$r826^NyhՐQytR~eq^7S5Sޕ+l?KW0a)-v(*K4 5㮘y,>%~R!þk\G 2Я$©!!d(6!82L[ K1>!|~φ;Ȳ d S@Y?) y?i8P>F@RlH` QPdȆ;A "d"qYlPi<"B"B!"$ÖD'LI;(L(-H BJ]` #.ܡf!,v#CаXA5l kA9+i,P/x=T o>A@C1ZqIJKLMNO<E"Q4SDTTgˊU4VEk  YLEpE ]D^gn鱢~h8&p`+׈Z$Dg)8i h8PÜR@GBdCZW*}68 !lŅBQD>R4`)A`C-8`7x1# _HkDŽȮ!k> RiZIT|  g;mJCT 9@.H q)PHHdXaP`$ lD>ʃx<_ɁȧaK y6落|eH,ܪpxBh|H<-}ĘgxxЇ~z<˨9dI~,Zlot$OWIfAGS8),OI0tC8%|A wp(`PZqdЇH|7 m:FQ1ن4-NFUQM !%" '4oіd.A0̂qd/F>6E4Q $q7mCSV)lxk6 ~\B}hCeS쐋ygxw,dpXbԛ$+<;UhÙ@GH݆YyVFP,-DU=0l47(Ee@сHxǁ (PCƔU #Ve FP([N)f:!5mo`PzH:(T=f%y%eqhώ8[ H,X VS Ђf%n~%_G8݁ @͉ AT%`ؘC@_YHq.wOC8؁[JxY15E=XTۮe+ 5@p> xYH E8(@DՁXW %`=0\1A6J X06a (Ls)XY%}`͈dLi+7au0(l˵J ~ṕQ -X`l(|֋^}X&je2< ~hp QX Yۜqٝ=`Hv* !:0 >癞 D}X~x h{9Җ- q&n'8../Ń  c2e㈙?@A&B6CFDVEfFvd!GNVJEdj!MOPQ&R6SFTVUfVvWXYZ[\]^_`a&b6cFdVeffvg!k׉phlThkV-0o@piv&U YQ 7 ](a@Y؂u&bp (؈hhbXK -iihn^RCM#sHVepo&.0)Wp6٧b_(Z^YI^hʺ`Yd`b\X#a[\ȅ`:^YNY 롦\`#[ b([ZP8 (#_쌘l)\҅n(뗖٦ڶә^@n@@tZjpgg`ngpiІ^miu v~qs&ItiV&aB_ Adpop.)r RuokAMSg`* G8hUb(Z _/^o[1g݅i7Evv&eX,ڃ>Sh ].rSVXpP)4' .apli2HMv$S(a8Z P0)Bx7rMVNPIVAM؅]=X"ȂJD Ɓ؃"%څamSp,5NhXp ^5ZyRY*^\_lX쁘cu Bmjw1Zk`$/mp ԾU)wXx ZXwx$ [O ;0 'yhaX]y(]o0=yQ' ucTrvz}@Tz\zsm 8`ُ8{=sp-"38Ѐ {cІ ئ~ST &ç{gmhU{h͈;{ ?ҷW (< & sdN}G%hP/}ρ '}e/~.'%z>s1)E~ IЂ -*)yUTW ?}o',h „ ۧO`'Rh"ƌ7roG,i$ʔ*Wl%L+8.&Μ$)'РB-j(ҤJ2m4ӨRRj*֬ZrR)j,ڴjײmmɲeۊk.޼z뗠"0aa`3WT/Ȓ'Sl򶈗7s3ТG.m4Ԫ‹'oزg !,H*\ȰÇ#JHŋ3jȱ#CR@zATE*Lr˗0cʜI͛8sɳϟ@ jHC*]ʴӧPJJ՘F%ׯ`ÊKٳh*( EҴBFJ˷߿ L,!Ĥ#KL`E!PkvLӨS|8(Pv]7hQ͊7#R9 N6 W:NسO&घOS ˟̽Ͽ(h& 6F(VhfTK.($!A,Y.(4Vc81.(Di0F6.*,(2\vYa ⋗hf2@nD/i矀F@!@)ձ@R0褔ɜ\IN`Z)G˨jU$vG:HB.Tk̊Э % *uA6Q,6Msr@>PfmGyNk̶ 1,P2*.J, ˓o 4o  7 -&2 k/,JpM wl9л]ƴ*{, p,s|_4]RLLo. -J l)QGu`M&V`@4E3@! $x+o=P ,GQ *P[WH+0@ )]~43ZU mPTu䬷 ,+tr"r8t,bAG/Y+2BA3ԌI+&t B6Tʢb/X%U'AА*HPF(7 e'AX%IhayĢ Z,YDҒg!s5Aһ/"H 希]8+y!m,(h<R+X%6y:5 TbI̡QOvgz/T X7-vߠl:xkQb``9IHBڤFA3Ma@ƮK [jDp Lk!WJ d$AT ,!&;X,J3A*a@vaLL=GenY∲b$ջ$BNJ* +U! :is$Iώ̳'Xj!8kTM(O : ?'JъZͨF7юz HGJҒ(MJWҖ0G 7W)@ PJԢHMRԦ:PTJժZXͪVUġaRAإ7q@RLQ8E\$ L8/naH_.MA0)= b ePed7 X*1J! 8KҢ)rڔ@ligW1,mwQcIY-j\8ȅns:^C Kw)Q +UFX"zYIJ*c g-vw.|"]6 ®N;'ܥR00듢Fy4LF2xȆG{nq1e FkY}Co: 3 c,uƖ'{B@LLj(K6166RQ߻#IGy dfTY(hO))h0` lxz,? E_a Ѐ8.Ёk%8A VЂ`5AvaE8BЄ'Da UBЅ/a e8&4!Ї?b8D"шGDbD&6щOb8E*VъWbE.vы_c8F2ьgDcոF6эoc8G:юwcG>яd 9HBҐd   p" HGBҒR@(yIN.%d'EiOr J)JVZ9˘2ĥKl)K)~I2\eT|50% țٖcN2QFVryqP@F0%Ciz6d60qA 9!p~&Moh }I?Qk$>:(oP\ k ʣ]8oC =P̆3( k Qh !sAB[('t鋀IuB@mgm/D+@a$GR>Q[,L98zݓH6"ɏxH:@ mHpyL|oC 2ѥ$XIq:Ɠ"8(jFXWhhЅG!/^ 3gW$vC¨H?iw3-bsac☇/_\@m؛Ι~GޙxJ%L"y1q+N1aVzI̱cU8Lwjچ~Qa̠>&N;aTZ{e_C:ISE켮eX,?g.A]SB 1󪁌aק|}9+"q؉SuY#U3>ˠ! ӾH c:4$;3yH3aj0<lٓ R暢pRzA( <'kx%DB,+Ao* ,B(l/p3C'5"Q.7tyІq-ÿXQ> "-Ux,ĵ@T؄XI&چg8 eG<"kD#RM4"PQ$R4E4 S4"SVtXWBZ!P=E zB`,!#FI(ldE`ƄZڇ 0FѸE?C, ! ʆqd }FCtL ~@(7Ek|hx?ȆsFJE` N&1ݜʩЁ = P/I}G 5 Q賑,#ȓy-Q`Rx,;[)/qˌ @DzRQHI?(P3U')SpxHӊ,Cb@VIT3Ihk5M~`@dW7ւh;nٗ,ÉC i<bíIÔA^ڃx%`TDfbG-_xPeЁ%cN^'ׁSa>e5XidW^m%XTXL^U&bhG^H07 W@]oh,8-Xbhebpqaf6b0^g_]hGYb̨[_` Մa;a[}F#[< ƨZ΅~)[ha[{d\@Ⴂi{i[2>(j^^)NjbQkQ;KjI8k\i…^Z؅]z*Fj^v*FlQl]`Fǎl4Tl)s`s;&dPCg*XGFenlz i*&`@FiDS&o1aYgdkNlƛ\A'K[nBon Z D~UW `NRBl؅]=\xӁxYÄ昳4TRqhȃ o\p0` 7؁q.(qFrgf`"q&^Ѕ^[ȅZ]\h0Zʛk^rXFH裵Z َsJ*< =Z3GtA7C% \HYFnj?:i_thAtd)@ST]VW\!؅@Y\1чY vٰc?:hMV ^0U&yކB I:&p )NxQI@8؆ywB(3o (oW<i7  Xʈ%_  Pi7 W mcf6LˠjxjxIE~0V({ 5N@ 3 c( )T` PIhx ȀNJH6 hڏB9W vW{ '7|)ܢ B~Ņ~*g??  }gG}}x_ P}&ڨ` ~}0qXHU`h珕!,2 *\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JWPjӧPJJիXFLŵ+W`ÊKٳhӪ]˶۷pʝK*]˷߿ LÈ+Iǐ#;L˘35jᩐD  URY`cͰc˞M$z4ޤ:+B;4@7t H, ĩyNB'="c nU@u潑r"c t09νt1RwֽsZ.x["慚]𶷻n|;_5}ۗR+7ox{`0|%,_ 0~` FoE`GP<ǂi/6lc#+ָ7l3θ=lCG1d\d7OֱEBXβp-{],f-`ͬ5"n~3E|3hs,f>cnseB9ѐ'MJ[Ҙδ7N{ӠGMRԨNWUc$e4LAZw^`66=leپvkiۚڼkmӚ6-np{;.7=nv[V7ݽZ;ְWB}N}-PtB#;'N[ϸ7{_ȫr 9$N=RR!PVNS (C& @zRa!HEi fXN(?ا -Y( ǍQ@!b1) F,}(NW+^M}!j$~~[~`H; T]+{rqC-(vve?hdA,0# 2w#UNa.gcR.Z*zw_<[OOׯ5g9cr |8XZ'v0  ht ~ ! x `?'t # $X'A `+  X; D؄2 )N?Ȁ Ű(oϰΠ 0WEM&  AL  4 lm YW Q @1D v P|gXOOp  ! oU7x A#@%pB!"& )%ǧ3 0pb "oq 7P'Q {۲OL0 CRD]gG 6 PP$O1'u !N@S !8 Ch. ߠ_ Q PEQA q'@ X 1  b`r1x(, gD!P N1Wq/$M47_`.Ћ1A Y Vpp3'P R&ueir/[ `m gP0 ` rHz ~  s.-/-.޷1s4 #gp ۢ 4K0-r-,B*c b4C aIНb* , .Az ~%>A z, Ҡ*(w ʡAG)'"Zo`(:v3E22jS+ v-Qb0a,<:19#EH&rIڤJ2W-N:L - Tڥ/2 P.{fzsk@ ʠ zr')-1Qtv %szyH )}9Y v .,[azy8 BP 7T 'ʅ @P &Xpw `c dGg PJ.S8*a S.b0  ^2*vn1yi)`K`-.%70Kpvp K=I';Td96M.U w *ʋPrke*]# ()Z=(yV0w $PR-Os!e--Q3&-Vb`([JpO000V . 1] Zvv#0p0B`),*G,e -090s^ H/w,.)Bq$Ӂky)"~b*~ Q 79c5" g*v ()㣆"[W ([   ! z`ڿlw 1+|~1! | $8[*3ngxj;,. Ѹ1iLlH*:M<оp|y`о 8aPl ptm2u?@@i]dž@$`Ņ{Ȓ`p_6x VɦLiܑ|ʰ)L˸iE@̹<-,Ĝ^h` 4*<ͥ5ԓBl\ڬ,|Ll\| 1ې =wL0 1s@l-MZ@slDɌ-|Uq 3M_`a EXTD_]8 Lq hxe-;Ek7ټ$5i9hbk-Ph Xu}&\\ у \ٚ=0}ن Т 2!Cڸ TMb`=ܿmη}ǽ$]#:1*1kVPb $G-*ޭEqA,0 v@>UN?!- mT|mM]{1a ܃&* Si nۀ<<@ p !A09a04)> p1D(>6. ې ?Ai?YȰlq*eA %"5A m@ `;ޜ,}5:!+h.0;\Ênێ^^C͞\>Qlꐞx" +BMNny4>ϼ<? nz/A:|@~y-.@ q@w_(!]D .M^숗K N@1Vj o2$_>&+< (!2?M,BD_$|HJNPQD٪ZF=~RH%MDRJ-]OR#hHm% MN,RM=U*pTV]~VX0©g%E`|mSQJDTV F8+ƑY ?Ydʕ-_.eiPly @lH ldZmm @yiAt_M/@$@+Hy$-u"hl (ЇA.ygy矇>zGL F]a l2~rC?}g}߇?~zm'g)?_΅L# 5x X@1 Pς`5A &D[H4U'PЏDGPHF8!u M$FAlHc5PcH4'@x WVvM?WbE.vы_c8F0*^6 Ve r xАSĉFAӵ6E!-g `⻨[@2 bŘ{CCVi b HFX#U@˸FHhr|/Y$]ɣP@ N%&dĠB% 2͝"Y` w5!e\t/:1KC@d@b&r ,\QLxD\ X(,!a,t"$BG!pT!Ŕ)5@ D_IVAʰ !*B`HhPPk80z0&]a' QNԫ N-HIS(լT͌"` I0!#ʐ(% zJ8 \ uTAeZ7x dbGb!3P_J(*X$5[ֶmnu[ַnpkۄ\2hDyTxlB:!E%:h34hA`qHLQF a1FAn +[4`JZ1k䃚JypN"5$b  ~)@a Bd5x F'ۭ_," , ېľ0/j0Q@ 0`CX 1Yd@H0YX_ F?Y .R !fHusF6(T,H6" `" -h.d'1 hS.v\&P +^[WP-μ]zcLC@|)H)ARcuBmXg9s.ڒLȊzWbabLBj,=:?wo~ME>pG8Bްp7Mx%>qWx5qwyE>r'GyUr/ye>s7yus?zЅ>tGGzҕt7Ozԥnp @CwY]H1c>^G;v0;v=gO{ށu$E_VRk xg `GeC!/4?#I>Z?:;>p5yy24x~Oqzi| 4^<4h8xq=G%OSqA~t ^x|ҀgK{~jG׿SV/o4$4DTd@|}؇l 4 @~@   D0dt у/P~y! 5R=H3vXc0 $( x<qPA)Bs0X yp3 qeB1C4sĔ R&j C$x?DtćCzJ }k9h)K0@ y 6A3(?Pķu`z`zh]l;XF8葞`ԘY "24d+>hƑxk:P nF(ytxsFu vttLjyzx|G}ǫѰGe}4 ˋ0;1tȇȈlȂ̹܉PHE4ȎH9 qHȓH qTAəH9t_cI9,F q? ʙ$ʛ:A@Dɧ9| ?x|ʓJU9 <2,;80 ?8y8 ؇[஌;˨,+Q @> J0Q+$aw釫 ,< ֘dBt ɃW < ~H뫸~: yÊb~0SN<HܓD餿:9NHNʬqpDR3X|4OK2kyx2Btg8þ@دN-]?AB5 DDeEeTtGEHT37 )TԄM%NEOՃ RSESU5UTucՂYTXp7^=_`#[=VKVc]kgV]UgVϋkžlUp Wq%rsEרPuRpWvx}׆ze{SgvuW]WEW-W5yW WWqXfWWW WXWW XXjؔUٕX`ٗMYٗٙ]ٚ٘ٝYڇږ5YOEҎ}׏%ق}ڃڄ؅چ՗%RWڋt یۍ]Z=[o0l8L|EۦU۴ۯY-YZڵEZT:֏`[#؅\YY˵Y̝YͅYΥ٬ \spEĬW*ۅX]h݄x]݃]vw9\ƓFqpݛ%ᭊܕۺۻۼ]޽m޾Eaa`vaF  ተ a f !aN$Fiy%(6^֝b׭bؽbbbbdybbZ2>^M^Mc]cm}^8~5Uk-\ģ]ch :=i!Յ3^Lumh!`6:&6Cj9vj"jyj9Xl k90 jek38p"qX뻆9kH0l6&exe0ڨl(Ǽ9"8y8XEkY4naPkҭmRX`y0d aߖXep@h^y`v閅8`g^wvPi!3 p(O s|` #e> ߏ8{ԘvevlXegKQ$ӄB'7 @X`_l17y0Hs/kXT)9 I .ϥ!7\`f ЖyŏyX;$v0&thDV ~yy! _ ^7iph#r*|3G|#;lrR)Fpxwk4yC8 {z`VXмȆsD׊ӴyQ_[xqݏ;#OQx PMM~L' QPS71G.xŅJS )TRX1^A d>}P_?q4y6w!8K (҂E 0`S%j*֬ZrEpZiO8U%zĨWAѭ)*TqZ58.jXP\0APj"ɢQ')}'Mo4] =f-8+P!_LנԊD1iTq1&}z}eȖ\=9pJ*ИNA CH)za,,I(X  N!C`ĖlMoB)"ꝒHcÅ!!m!l6|F h"H R+)gFӎ1űvɚi# 3 2ԒC2gl$$cW3TB3&Hr\$(YProHJʣ|r|%,cY]ɨZ]򲗑a<*!|&4 ItimrGkq<>s|g,3M'>5'@*-J(4:iD(D#|T haHeZ(HJ  iҕ @% ҙҴ6nz|货>)1a(@=*RjP*T*Uh>uV*Vխr^*X*ֱf=+Z*nM3bֹҵI׽+C2+S 2}Vc:B>ezrLim6F*%8=Z_Q6ݖHE]-nۏqtlM%S mgm3=wmVKN+BO@k'ЮH,3N + i6]Ѝh^!SG'}!8~6]/" am17%\d 'X HNC8Ή!a9lr!x!be0{ y#~ EeAŇ:D2 P"0Q۬*1-Ue{6jE<>$1,<+u,Mt (Tea)_yB EU[,I0@-J,[fC,U>$=w cK% wX @S@`"U۩$[D!ZZNf R}basfxaI5g]3a0uvU !;uR%#' GɐE1xdItg_#@;Iuc޺mb.S83s8C.򑓼&?9S򕳼.9c.Ӽ69s>9Ѓ.r2AL!kЗM,X@DlgX:Ҋb SKSBӉV F?m;nC@?<3<#/S<3s];/r+3 0sѳ^8 qc/VZS2pcR%uBV3J,?MV~gt,֧mYlFy+3+T Pb]' NUE7@ p[U@)@AXW( = 0` r&Ă $  [AEŒ[E @xC@5N'\F4s|Z-J$,I'P BR_Rd]LAA\R) ::l#(`AH#@U$lBUHi@+`>b]B@AHE B<@%!)( И!b4,@TU& K,T D ?@/|bA@`1 0x+BQ5I1D4HAaW>4BP@he]>fSNeK^WW U`OUu8Be SBifEYEQeRHܖ_jOAtfBi&|n4op'qq'r&r.'s6s>'tFtN'uVu^'vfvnXpxf@|x' z尧{|\}8Ч~`((&.(6>(FN(V.-\&.lvE~((((ƨ(֨(樎()΁B 26(8DF2h6i.:i&64Qv3)"'KF]R\3(ğ 9rfR @B_1YhqBU4C1B5CW- D|ݛjr5 9t ~C7 Ģ@pBjn@nPD'.*ğg1L2MT@؉@0B%ڷ5IA NĘ=B0k6@ 2N@|' , )EifB% $`Ex/>@dMkAA:ۅ-~P $C>@E|]@\!,b,rfh-/L1ЂR,E CiAtkB/0 B.,U8-,@B-&m0\BH-‚--Ԃalks:E(-(R/dh/BRh.dhBԦ2/-@Dh-d(؂m{"-R1Mr'/,HO^uB.C/nn؞n0HzvZ.n.j-/؂-i.o.-B*jgFxN&'VY] z ٕojؚo4M6HC284`Ƞ]jQBDW4$f)24ٛZS1 4z H1R`&:(R#c4NU).h6 /0,3/0al?ո=`Ϛ@HXܮnM2W 8b)  0+pr@ hU n@ s&w] TB' TAvm $ߦ/,4GU@0-L]lNqA4@p¢zm/ C[9D3ĂW*|@v <@!DU$ ė@D@r2gm9@.-FT--xm0> 0/P-/C-t)/4,BNc/,H-Dv/JVctQ/a)"E.4thYg5gт/Ă5O,5 B2+VdZg\- u_ ,I@bsAeLЁ.ĮN=堤hficDlkxv0$ՌC9`y׎ZGxl&WK"A|OW ŢJ-f埌6KSB $va 4Lɕ 1Bj1 ꭸ~BB @lcQI*)3: *A TGPE% AJȱB'3łDTnۗ%@,+ ?Iz Q,Y@~0J+G3&A0@A{ DF + ,4 Att{P@U #7>CJ@@j d,s*P7vIP HUll=<McUN3X`bj8&]I)B7A`{ʅZW1BKDT&;nzP+<M2`<#O=s/?38sM`Ac֐=N㐝4>C*Ԟ=@=*Okԥ2N]TzTժAŪOSԫ?jSjSv:KhiM*VLk]׫5j`:د6FkbX6ld ;YVS:+Z3iZh "W6'&.iN^vfl9[l~-gnynvn];\7]gre\~IKڡr zx񊷼.zKއW}o{"6]{^׿x|֗E0|_j/׈#L iT7apx=(NW0gL8αw@ٽd#PG6dlG~I|:Xβ.{`L2hN6p<(/PγL>' Fڽ <~NF;ѐ'MJ[Ҙδ7N{ӠGMR):tWjyQugMk/w-^/浰ݓ[NOz[ЎGgK.Hf vMr+$P@^v-a,@^bM]_g@0 `Zy/[ 9_ !d\('-JOœ<0ߟzYy9ws@:G~.]"D?җ3Lw:ԧ^tS8Xx ؀8Xx؁ "8$Xy }&+Ͱ 'Fxr01  F1ŃDhx"E'̖ @ư .T`$opUNW(g`qk pow2j8tXvxxz|?Xxc  a mxwqHc18Xx}s# ыh;hn7"(Èmh"0p ~h<-R׈ArE) 0(F085;`Љm$@Hp(#ģ CYȇ{R< 3# u ϡ9vKx.4Y $Y< ;ٓD2"0E!ɔPR9TYV"wRZ \ٕ`5 bZX/i h=8Ѡ t  a})4nP@ܠA%%3f '-CPYlLYH}rXx n9Yyc nka XɊșMGIϱ9Q Ӊҹܩ9Q'p3V : p~q\y W韭1#zџDO3٠!Hj&iIꡯ *J1a}lQwS.#Cǡ(>9/0BAw5*;qы&i{) 57*7Q`b)SYP jg0swl*@`}u@T _ZqH4j :tZZةځ8zZڪi*Bz:;SZC f*:U{3j̣ꠡlںTѭ 3– zِ<Ɇ:c\:#C`< \1 *'ɰ c<)@nB] ۰z'd,epYpOP:%zQ H JN!)w:D4bKPz`byQ{ Qa{˷aa Α : o~Π[pAAKp|Kw @p{;o u kP {٦q:|н[kؖA |Ő꛾ඁmU` a P 0  s ` @ n[涿pq |'ի h!%(b),_8!h4l:<>@57D|~F|J<~LP|}R8  ; aV % {š%0 '}@  ː (P | n8' '} b 0oIq` ` p= = ]!\";I N4n 7 Pq2./Lp BF b 5 \ 7@ }it yna b11.ݏv+?ZKn6fN o}vG`0 oxcr40} (.r ORHP"RTwJ~ O& d7 U7E7|)[q۾~}N N)i'tH]ו^=lJd,X&{vA4"_?_?_ȟʿ?_ֿ ]+- $ٯ1z"}c&)O/@ DPB >QD겘QF=~RH%=7JsI0F+c&Lw0'MTВ(Hy=FTԐ^ŚUV]E~ /'T6'Ɏ=TP8Wޠ]qmWP?YLmE}1˥kw#ɥMGpH[ZvƳmƝ{+;nB:Px Ğ_:_ر^x͟G^}dݿ_|3_~P$@D0ApA0B '+0C 7.0DG$DDW̪'c1FqKqƫb1Gw{ G |0#2II'HH*2,hJ.-K1$3@/˄(L4dtS!5㤳N;ONԳO?\ЇPC(> EQGMHQ=RK NJ/:3sSOOD#6(5TGe"PUסu`YwXS ő6H}O`q׈Xe-Un!꣠70c yܫA]1]{hކl0( BVvW_ W1ݱ_ '!Cd1nky|E"8Ay8;̍4 2ŁPOԝs"P"Ț?R!Zl qӊel!y X́Rq#3GСXcćvA" )pHtFYQ< YFKqt*I1_LqmCk$!"kA`DA0 c@r 8wxJֹf},2@'TR >2![$%yX!PFic@fyR1.`+)x1s@IJd3ӳ_;pR b/ 7GFGz1 R0pȇAbG딊:E,â+Eyn8q^l@1nrE2 52 .LjZ\I:IZQDPͮV¢6qW GTh-lf5mhE;Zju+mjUsT;je;[ֶmnu'n.z׸zr\67=uq;]Z'nvYծ59Byջ^o|;_׾o~_׿pm^`0Ua WXp5̑ o0A؆l xCn?9ЬQ8<򇧜6ox+~Ӝ:8'wA3CzҕA-Lwӟ;UWձtoW^z^V3!Y՞=kwu;Dnw=߅K(fM};.ySۺ:~C{-?}k?=Գj-w*}{d8a}?1.=t!շa/b}#js?_KGo_p1O Z}slL<֫?қ?[@k@]ӿڻgpd:::::G!A! "4B"|$TB`B8c*+,-./0 !(33lAX645L84C9$B; ;C<>D9@C=C:ÉDE,7tD6|D7dDJlILDMFtA)Q$R4SDTTUdVtW 999[$9\49]D9^T9Z9۹GyAYƠX[l\|]^_LZD5 cF7mp,.q4G2!G:utdwwydy{{}}dȁ,$ȃDȅdȇȉ\rȌHH†HiHIy*lidXɼq@Hk(I,%کqIKu X&(m)$d9J1C4x!egkʫk@ɧAkp l1Kp-S@DNpK803a(w()I`ň0YXhB،MǔˆJLLiHM8p-s0SdC`nNh\P)a!Km\ N@8Lt07(+@aL'LlxwHfmfp WD_ુx((RϪbN-ihPXhbX%y!s8 <DDHXh'- %UdedD=YHLOo+ZJ ӄ(I΃@ QHyR͜bH-`HSYk dlX܊`TJQh,RXCS0*@ UPH`=ODMEMUdY, hh 70]U;!Zhn S` ׶`Z[Y^w5_&bcݖ{!i" b؅]{҄} wMZW=a0P\ӄ0Xh+_r؉!:W}Xt[[X19Y^xV;bPU͚ cpY ZZ%%3UbpYb0[ؖ Zn,^[ق(E^@ 3ͅ Yu[mA먲`XP܆ĵ܉y\s[pڵ]Zu %^ؔ!^up,u !e^^]!p QYh^ B# !M^=u8 !5_>I^ (`C(0 lFF`]C8p"@^Ya`YH`uI ޓ.`N& ?hh vb)^`Hlb0E/vとn`R\6 ^kdX7&d0;iX2eg-K.rԆl>!(Z` 㨰ȃe(KdLEʤI:XYp^AheWdMYP&U1-Xib^ށ "!iaia6xK:/<k"^"zn&d7(j"$`뼾Ͻ྆k !찦 IXlƾء#`6kYl'limɖnBeHlm߾n~lFn>vF킀mFlhn.)f5Vf$RhwB6((oBj&6k6eOf_b(0_App^)JqӃ_/!@or#_W'_*/^*Wޥ$^߳?]є!H`h8?6?p6rsNR6q._U>FwtC}"K90(())o8k {jiA(qcl$1Ts`9O0Y1[' Œ ڋqutrn  g de7o&%~E?g_2lh c׋؆q;nx3@JzU24Pwc A?qw|IwZ7HZٰX@z ) kJL8R0]X<@s7 8k8%q0 RH I'pDPUl,!w0^y2R(ziqrs 'n"(' aSp(JS?{2 Apĕ8ܽ? 8 x * :x: YAH 6j!Q3A 4‡LLCC!1 A0MJPAI*sP5O&<, N$%U$s5h#f>0&m@=C4X?T8p#O:@Sn*(i3A6d'NXAz"83OQ(ZZNCiE'BCإQx*6"B|)AbJ;]-\pQ-ݎmފ;.eN/.un;/%n/Ej/ 쯺+< :<1[|1k1{1!<2%|2)2-21<35W93=A =4E}4I+4MfQKS[}u[Uc5ci5aegDi67k]7bߝ7[7R8K^8F8A/8:o9GOPTDQHx9衋>:饛~:ꩫ:뭻:>;~FeFdyW ?<<+<;>;>??_s???(< 2| g(|< G8:,v! Sx2O.|! c(Ұ6!sÒa>)~_F<"%2N|"()RV"-r1Cl 袌f<#Ө5n|#(9ұv#=tWq!A<$"E2|$$#)IR$&3I\"$_"(C)Q<%*SU|%,c)YҲ%.s]򲗾%0)a<&2e2|&4)iRּ&6Mq C0dl!go Ș<| k!'12 SB@M{4伆1 "Y 4A' ,C@RHaÇ X!Y(L?(3)S b*Tpi,F1,q/b @xPdYBCaN$$ ֿ hH88 XƘV 19t0U B%xU^ "*d q] 0#}t2M @0jq E/Pj/ Y-8FբE.t=ӋEt[q֣ eR/l]svq.l5}/|+ҷ/~q\C6IK{~ ~F_03g 8=,⇘@#F d! a` d=1‚+16MB}) o5E '؂Ad:hAZdԌƌ9[L40t)V؀]ab. 4xř5]іm/p1 X9qY?Є-F(zv@-bIڿ43MsӞ4C-QԦ>uR*̀!,H*\ȰÇ#JHŋ3jȱDŽ1Bɓ(S\ɲ˗0cʜI͛8s괈h v JѣH*]ʴӧP*kXjʵׯ`ÊIP~˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟UXA}U߀XW~&\!F8Vh5v13)&U`@/R B tC<GԌJ@@3A1 *=fgMAl#PP<MI:L1*@a$0Њ49q#@ ,hR$@""閘fU }0 AHT9P*LO4@X@tK1)B@Z3zH Akt)B@%-ʲVkmR6$@Р(EYHQ,9ɝ\AC$IP3atʬA2'n(P!wߵWLS2`,5بM6)4O:iM\6H#8MaPXAE%0=",@1@@L" U@!A t dS8`6mQ<!Ȏ/Z+7nT Dm)^А~.w6slAH*&x%fnECrC3B٤-{n A Nگ׌*, -7@ca,X>ߒBظB $ _ৗoDzPrܼKD76(G=AbCO7pw<SPU Z4]`+( Hwzp hH r p0'H(,U&CA^CRK! _A~"!# A%{`[.5:!.vE͛ ^ jČ^LBF1/2GbxcXP>4Rx2Ld\F&Q$'IJZ̤&7Nz (GIRL*WV&Er,g̥B!^Q.KjBEzLb:32 )nΒ8Gi c:v~ @JЂn'0B3Ѕ:ZaXQل#v(ka@" RR c 1ҚFشNDS; *na dAHOG$)T Tͪyy(xZ x$Uᅣv[sh\J׺x{׾)`Mb:d'KZͬf7z hGKҚMj-*Y )/);X  ^-j . Z)Ռ@vQve,Mz|绐~ACƀzhG;` s?;'L [cN~z1~8GzX[ϰq1Xj;x@d#4$w Ř8!c=@foc=-ۼIw1鋍yπMBЈN4p+\Eutzq!LI q-uӡ釨G=SVOK QAYAQXFaOY@֗QLxʰjBAA(~kBd+ k&m F!X]mͰ @b?ޔٶP6&U~2WAp1L&.x[D3 gtV&OW0̕ h5yisskg{ڡ6?zfBPZtɡzշh} G>@^:?$jB xpԾv e^)AAнj\۝QyX%!fU{o!G#,O40a=Y>xAA#Їÿ&< xQw|xZbKA0#G?0tH2'[?OOϿ8XWbe8P!#Pc!X O 8_2 #0w5g 4! EN`& F `&|4(Nΰ s78j "mqz P dw .[*f2\O-F X6 iH}E@u(ɲ 1utuQ}C Ƞ8Xx4A ȉ5k|ax-hQih-lOHtHO%XDHO]،8XxAظNڸԍ(N4MxΔ8L؎HK8TJďڴyJ9T9J yqK4IyWGȑ$ ؀Ra P23Ѓz* (/e #w/2/1 Ԑ 2 Kb@QڰP =UJ@ `GI 0y0QQiN *  a v3@"P 2  Spxbאb$.0C295]!LI`WqW yME5JoA@p/'b" _sn( =51R@=.u4)!:]sA?NI XJ=inx[P6#Q`q}pU -ў =ˀML YJ: ~9,0gB 2AQ?`{W@f~U@HD$ 5/*  1`( B`Tp c@.Er@K4` Њ4$I7 i{ 7S \8 RѠh(Auo5 (iUB"K%{JR y0$fZY+/&Uΐ Bb֐c^R ĀL L "7[ XǃNV# N WT6 q !\ZJP {Z ^ J DՕq; Ȕ 5 o{P;pB A[ 7INp:nPM ʀ({,Lzq6ZʄmaĤV' BS3=Nô1 j s(P]àL[T~ 0$ւSm:_f+0^ ˚~i4E{G[D۸{>;{۹;[{ۺ;[{ۻ; [qȻ[>ۼ[h; [[6[qK껾Ѿ 1{sU[e[̈AǿZ7X=#L_b=zmwwH."~К@4 q(e!ыAŗk,Xɠ*A3b]ۛ!x|s,h| j#G,:ȘXpLǙAzw3h1P,T\[f<|&.\9V@e@f&!dm ,I|Ȝʌ u\^qb q;Q̐N!eh\|l qͫ2d^,H̄۠ l<w(Y%a L}MgL}lM !G"d% l2p Dŀ5`!蠎e[ ېٰL !+v@ +=#= X4]h:=/g ĐDDM- }`!q_TղP&`,+TPQTc= b26~@yd׃Q3ې`8M jm#x@bpYҎ!ב S2MGmaD!&N{!J0@% i Mڟ41pE %G f G&1܆1z!_=c +&~ I G>  ">$n^q(^*.014S7n;  >NaHLNPR>T^V~X&a`Z,b`^b=́hjˡnpt^v~xz|~>^e&p懞eBۏ^eP ȠW}驑%נn颾>^~븞뺾6 蠐1>j!PI X Y qBְ B.Ҟ. k !$W.L2G( ގ`p` a*!L aН,|b|@d2` zP\ ^^5KH ?@ o%*€ !l"zHN% Ҷ1v*3Z䕲 & wp!F26*H?$)0 @3 ٠3! 96W! r[1fo < m:݂fLD;6 V;V<(J @r PQD-^ FRH%MDRJ-]FuW.yEȄzy˧]5_ETRM+tUTUwOx Q.*WmݾW\u=/G\}E+X}ֲvbƍ?Yz)_ƜYfΝ=]rMFZj֭]NƝ[n޽}faik\r͝?g*|  1G ȡ^x =}{/_|=$jAڳ>D0Al(`8a `A 7C˯! `b0(_1Fg+? Ip 1H!$ȕ# p6 4J+2ぇ#eC "z @kdM7<#Li9DG!NA%\LYF><C/4SM!'P0G?H!`N;șMgV[[* !XRRe 7X\Veeْy BZ&iacYg%\sVZjKEiI&l isL-hZ_g, l7b'`먥̘E$`VՁ,8eWx G S OXF\:h]FXF`: @`(% P}fmS"Hb`x ~;pnjk!AlLbU'򽸆ˆHtOkXO^uoǝ$էm:#b܇'x&$.Z/ p7zy {dnzGzml`C)pULN?)_ "N3 π"j `%8A VЂ`5AvaE8BЄ'܌QB iia e` ;Ȏ j7Cpяd 9HBҐDd"HF6ґd$%9IJVҒd&5INvғe(E9JRҔDe*UJV(se, Re.uK^2)e0TDf@1,3Ϥ\:9̓c ԡjv.fMr2GT}ӝ2:_vҌ^\TxX91#O{etXL?F@(gEBj#]eT(ԫaڗc=݋n#؆ըl7Mn,KkR~(t >#*׾o~_׿p<`Fp`7p%z:׃\u vc*SūLt=hBЇFthF7яt%=iJWҗt5iNwӟuE=jRԧFuUjVկue=kZַuuk^׿v=lbFv}p4`#C`.ۿ~v"@ װe4dNFg'Bk;f6q|vb>rf0j<7p7x%>!@;tyE>r'GyUr/ye>s?zЅ>tGGzҕt7Ozԥ>uW!@ϵuw_{>vgG{վvo{>ww{w|,ZאxޔpS1;^Y3*Gp|dBD%|G16+$ @$(;k bt! >B`E4nQH~2P$@:@'\ h Bg2BBX$DAD1DhGBM?? - *B aB a` Ykphhb>Y0e@ X_@>' |$\b ZA[]AFr8[_ 3 ع'\1802ذ.45d6t789:;?CS¼pCO @,% 4QDi,`@DF$HBDM7p*L$eP`GHfK;Q$+\$5?XE_ۄh+c]Z5)8*#,4ƦFfĤpID(M¸t$|:/vEyz{Oڭ|~3Cj !,H*\ȰÇ#JHŋ3jȱF@yIɓ(S\ɲ˗0cʜI͛83jh!DJѣH*]ʴӧP~ `VԫXjʵׯ`ÊmhiPMƪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȳ•҉Nسk}-"?KO}?(+B 6X,"Vh!_vWRˆd(8T7 @(*h/mXJD#@)E2@L5L6@8im36ʎ *BC`ipQ!`J2Ų,QAG-RA $Л1A%9Pb,@x2)B&Ejʁ,a1ellE,AݬBK"R(H+{Y* L:, P49J$kP+8P4|JP|+)PLD b/BT"$@*g|@`:a8\s ?HӐ>h<ɈiBQo 0D &*p@ +@% D@ 5%N8`6}8fi˃M42f4bo3Hc)+GY+p* *ĩ@D&@4ʭm-O6n 3p7:&6hN8]7F~뵉Hx,=X*/+A!fJ@+^_-4xoE^% AˮR_w, Џ ,@7R[ڐ/L _;'p,Ă`Khpa^+цF@yz$!%(a Bkp"^A&$" Dr !(D DԐ!#RHźDR.DU b;h 5\6^#HG x, 8P>, fHBL"F:򑐌$'IJZ̤&7Nz$ GIRL*WV򕰌,gIZ̥.wE`qD6A ^ ,PsiVA4Ya.QtD7{Q E0N^҂9A~zJX=󟺄<JX1,BІ:D'JъZͨF7юz HGJҒ(M)GO2OgXJT1ւ\#F9ڙibcjAFXC @F1~'Ȩ]}\AvhMZ npm2dEڕ@Jr0/P`6=B |ᰐuOFi^zq c@ 0KZ(!/ʋ\ -Yf F^ֶ.2dMr\B͍.qJk+v{ xKMz|Kͯ~ NXbN[J@S~0r ys1Z1T<,碵$BabgL8αw@L"FNr%;PL*[Xβ.w`M-(b0Ylv3͹|eb;G~>?!OH.6H3g%3&$!ӝL:ּG Rr!<+tN^iv"Mg@<una<;׮#-U t&?Sձ yvOU/>'O[ϼ7{GOқOWGg֮oG Z0Wd H>|D:qXU~%.cF2$>D_+:rՊ|ioQF2UTvGPs & vGdHUwT oh%:%f5`)Q 'eu6c5M 8.h1}}!@6, UePNFP 7EQ% 20 8 j!|1x8e/!U X7 [X7ŀuYwؒZHHK蠎яP/iqr CU@((HЂЂPޔOP&V HQ, A(QPŀ )I0 K-AoXV& A[a(~Q- tEULL 79K WqH9'Y0 e;xW@VHO4` \iA x͆*5=IƆ$|x>'s`L^[0 !DŽ@  &>it96 3f X1 n0V6 Ky)vCd 'PQd_ hJT5&hawa)`jQS) °!) `%`.0/N״0[u ip) uEOAaMM WE `pP  ( $ )f_2&t2aVTiN6%#*6*Q p VU0  Z_MjЛN p(qQ `eO!5a7bU uxxz_Rq`VQ j#cPei#Vz/6@9q:Zzڪ:Zzګ:ZzȚʺڬ: Gj#z*ڊ"ڭ!y 1Py UQ:r үJ 2 {  ۰"o fK*P 2$ &{*-[!,2K4[q8:OihA+]0سUBGQ4kZv;KM Ub2.k |HWs(G鰷3U6$ifzYybPn]4 @{w"@ g;'f~kZQQ}`~Yj qQ}ۻ˻ !~K;ȗK`Qw[»`Kk$#Q!a p J0e;" x| P|qeZq#AovP1!uL t! mo5%<p#L8({]2Q#6_q t oR ϰ l [ }v oG<9L2 ozK,@[fgD[}7P 41f 166et}纮%o|<˶|˸˺{˾W aܸ |yrHڱ]',y,U/@|͘ؼ<\|<\|٠%ϟ+H 6 =]} "=i%( JL҇@ 0.ғrf0G4p Y |U+HJ]+ԆNMՐ1TV}ՌZ\Ո`P={!f=hցln}r}e]tAx=C‡#U\Kqԅ8 <A(п3؇x kT0x ԰"6! Ge 4*Cd.bzh Zc |&HQB *MFf+ 0/5hDaU`!Pi|Y @J! }7Ca] 0Y!` dHA` PiXVa +1 1Ȁ F P%/V "8ۆb } ) -6 }V -0~ղJ2 AN)ap;!ŐZ ^4a7'LP ~v(!(A&ޠ"?06 /=Ơ ]YZ VaF Z`a 2 b^ r 0s]  j BTIE |1V3b[[I by^buP麠btj nZw|jFĠ 5 S^ à[P:Tʰ Ea3w ޛ`][q| _:* ;P Ea/NaomaZ%b&? / (UQZ ~S/PcQ%0o%o&?(FQD-^ĘQF=~RH%MDRJ-]SL5mٳl"B~ )X`…7>UTU^ŚUV]6lȀRB!H\WuśW^}{M0r XFI#Dd.\ XfΝ=Z諽. F_.ӆQp"m|93i޽}\p3]&\Z!gH7z#7ݽ^xQ'./Sw Syyy0@9;O9pÅmY&0:۶m@ /0C 7Gp)c -*0FgF;.5E]~&q䑇Vh6F'2J)T)Glv1\Fi V!ЏJ5dM7c/ Pb@^|PCE4Q3:t3b R`PE?5TQG*NjtTdjYZXK T]wW_)2UlęY&B x~6[m=~ GXPRc@[y祷^ 0(rea&_}b!! bG&dl (@ˆ.hLgnIihp i0B stA\:k@hhScZs5߆;zM:"(:~W;p".jXp'PÿF<{rG'/;sܡ!tg]{!#"g%ra{G_" J!ҧ~SZjik`n"C @6"t&RVЂ `.AbkE"tЄ'm06P8CjO$ PЇëAhSшKxD&6r9tb(nWbE.vы_c8F2ьgDcոF6zscx!8mwģxG>qd>7)tOk(%r!(&lçuU*d uܶѐ7JWQIJ2;vCZ|*0^*LZHTH!X+׆V ^4d[5 x5#hPgY *63FWȡ!lli #FJ"%x+ۃ6T@Z-%x!%`WZ.v]-\|ѵg"zf_Ѭ6d~WQE /iy]+:D b|3'کpGxp7x%>qWx5qwB>rqHɪ8d>s7yus?zЅ>tGGzҕ.p4Ozԥ>uWWzֵuw_{>v^h!Jx8]6`r}N|?x<o.gI/$?yW|5yw}E?zҗG}S_7}e?{}u{~?|G~|7χ~?}W~O U H?Of!Ap2*i^{C 9?˝5~0ǿv0$phPY3p._ |=@,+ bs@ x,x02zĈ '@H`'![?!L__(Ye[YBXp` aa0ZB-[C-bxӚ,[^AB]CdbG@փJKLMNOPO E-:E*U2PpE.C<)KŬE%ͺa(\l2Z| %FDw3S8F{a(/E,a`F?68[h>>{<C5X@ۄ[QhW*|>o_@h ($b]VZ6Å]軅3J|ZHCs3|H&cɖ<2\h2GI&#I#ɟʠʡ\>$ʣDJɤ$!,H*\ȰÇ#JHŋ3jȱG>Iɓ(S\ɲ˗0cʜI͛4jg>JѣH*]ʴӧPf+|ׯ`ÊKٳh N 0I$MKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μl]УK˹سGËO^l^ӫO˟O}Ͽ(h& 6*BUfd܂ -(∀K'^H,XV)K))`*.HR@XH<i%1@?HF)J+:, &]JTi#F8c@xEB( YA~'8JЂMBІ:-#ъZͨanB L* (/Rd%@jыEf! 7b NJԃ1 EP(ժZ5)wծMXJֲhMZֶp\J׺xͫ^Չ}`xVi=b%Uvx 3 9 ıR,g X01ihV6=mAhh- : }õŘiU{9i1KIMn1 ʍ.Rp`ˆ?v4S8 IX2Zmj 2̥E|!d(pAz7a@C .pO1 R\ {>(HXb,>"~xWtHqP,rȋ[hB-a /F[,JZUl) HNr]q̢ʊSS-9@d>DĮE#8. @?gfπMBЈNF;ѐ'MJ[Ҙδ7N{ӠGmMaP)8H)~qGR/OU2ոKނQ WsMl]4\addE-nje.2T aMnd!0wvMoNP#񭾏ct!1v7?Iҏ=c$ϸ7{ GN|@~T5/-M χ!}Hѓ"+ ;@e?wqGqc]$!t2\ 8{f.r/׷[YynG CƧyC?(t9rѓOW\7c?{ϽwOO;ЏO[~},}ROߧ_i]Jg?C d4zlatd- b -GwB6 h b-q[6t Hh,01,q0\$hvD0q+8i286&w у?H%F_G( 5yű/381OW 'T95y]hXexg8W"> fhqPkx(?w(|j5ze7e 9DjHhf0fv)P቎8 B^h,}fvVZzZ UY؊vxcyH-w(gVňȨfƌ-Z&Gh/ꠍ~Ǎ(&GqLJ`Ȏhf۰ HZWxY7a>Y) ꀋa9Ww"y1'P- wezVU  ; ! &ɐ AGp*Y4V&pQpoHU (stͲ^Iwh AxSP-QbZ#r ,V 5uQ T )in9f e~ie 8 -ls A,R I㋠ iIH1P A1@ Mi AOB~ڰY ;yǔDr 깞Yd!9\e \U Ȑ\ IZQ ٟMT(sB `GTU YZM `{b A0 eiMt sV` aKӗ%Tv YFJڤN*QsETW`WWנ[* y6c_jX g:~jWe5elڦtZvzxWi*ҡoŧVjZ0 }:ciuzVJeѩ:V꧓:dUڪ^*VEj*UʪzU::TzZUPv"*DڊSڭ6~u R WZVRZqL Alu tԡ",n)^zǶ>@ S # NӡYR5aZIX ѯ keʩ,Rc`ѓEN3 ;M +[y jSI0a 1YŢ CPlo9  Nc` q0Ǧ5[ `/ Z %"L,;p@U8 C o\+Q5JU 3N  ph+ˀ$r"Z4^)#[D(\* qd)Lp QbY" i$k&0H\O1a K2\5Гe Ƌn0[! 006"&0 e1Xb0 F€\ uk#+%G>:KK[ޠWQ p"\5 ` LJT `4q lp$ @^@ Zu ogs1p3lc*X*aSb7(QWJe q(62 k0 | 5| Pw8P3% CC0SK c3S 9s0VP[J` 0 I u|  0Y9" p " RHP  P udzRd% Tez  9CW p )]_5fgm ="Mytg_ !"~wYK۫jɞxzqߎX2  ׭ߜ͚e!.En') -E6Q8- @.QB>UF~,IPHN5T^V~XZ\^`b>d^f~hLaHg6l?r>v7&>NX p>voM 陾uN^P0p0 m.0 <. >NP..)3 NGlǮƮ >5^&Z.^Nl^dqn>`1Q0X NQߎmre"d& !ƅ[7*?V,.0?_qٱ<>!CE_:gyNoP?eQ@HKGccUpuApFadP.oeI%+`q{M9"Q5o$悯2 !Ooo"\)z5^""@w J/@/$WM_D_U:Yd;?# "/u-a$q-_b@/@@ DPB >QDH&d2~RȌLD2I-]SL5mę`KPE(QM>UTU^}ӧB~KbRmM1ځ+ǖb}śW^ZX±ϖ6qL&թsXbqٕ*OeDPYt֞FMZ7~xܸ?\p,ἳƖjrd r\]k&P3nyorf{u(y/t @mn>ќ `6rS') 㐸  ( k"0P,ģi.9 f ;W$BYd7f 2" PCš:8QqXFF1Q&g #6E qij!Q xFY-OMh @0K,R4p*%(Jg%i BPS;OeU=3x'[ vgy矟r觧ziL{?|)|g?nۇ?~}?~`?ЀD`@6P/s`%8A d%ۑvQAvt+E8B؍' P /a BІ7auCЇ?b8D"шGDb%6щOK8E*V(RbE!1_c%E1ьb$ոF*oG:֑scC/яd 9HBҐDd"HF6ґd$%9IJVҒd&5INvғe(E9JRҔDe*UJVҕe,e9KZRR-uK".%/9̇l $f2i s\&9MbJe]aӛf79NrӜDg:չNvӝg<9OzӞg>pӟ(P"-AP. uh?!щ: gF-QG/Б&4-)ASГb6hu)NE T4@jRTOM*D UNURUn^*XjT&Ħ9q<ZjFtmR@! `"FGl0>%6a Dh*RzZb]ȘF$RfQaBQX6:&`aZ60: tc!,f0&2'9kB` a'" zs`X{lC!@+a@1P:S-e@`@@ ==A(- [@30>0 젎[= a(űAu@@XX`@3da!~;>+0vi14BrpZ@_v) C=P QHC9 s+ȿPCQ`5` < Xho@`^B0pVD N(J<a/,"x# )LPA&ā0W<؂UBT< ^Y[cYȅ`xd^ D&!ă[8؅\?w܅Cf^@ȄDH; ]xZ0s|Ec>rDa&]\4ǁIYX>THn銉a!5ʊH\X8FXJ bk\DePH]ȴԅYpGp\]8FbȅaIXx!JcZt7|FAHc؅`(ʁ eǟJ^4Kô[ɐ,u,<̜lHIԅtlI 8M N|!lĤ&qtrm8i8J\IJ\M4䊁 ʟ,/Ou{NNjxeN OZO[ZmOINHЃ0ЄhPtkv{΄ig ] } PPUQV1Q ]œ EQQR Pe# UR=ҁ؁%h5 $.& "}R.2 g_DR$59 4}a`#(? \$dX#!0R@uԯZ`X`-mGTxNi(pR8FT$݁hpNUZi#T}XZE֙xiJ " @8RcMmUVp\qCh"ke(VYw}_\X >mcׁyW\OrV XXuW؂']bm@ !# ؉%YWŅ_mm6`uRؒYxNqH\kl3 TWٛUZYM[d ֥ZqHX30 HZ%ӮہٰMIePhX%ָ[}t @j0W v-ТX?P^0H @]?X:缆`X(d Etݛ]bpGb@ ( H]T !  dé=axl8]`-m^Ѣg@p^Uc-SHl8N_G__bYс_pM?]ލB9lT%N`~ʺR NU ߈aJӃST2^=aa hb #Vb~a˥`)U*&%b؁'5Gbb6$ePUKc$_0v  ;V$6n`- BVaCfcF&F&HHcD@M5V_On䂀QZMKU(fed䁀TFe"`[VZԵ\`ZEWbZEfa^^hf i.Aflno6WgnLm5s.)UgxQ>q6hg!zU?\(Xe}FVM`vT(30a3h/MR+04Fc`gH^R%#؄0b-\(h:&oH[VыVP$pVkjh.2~i3fj ] .?MRi [NHLVuv jjEyxjL8i%Z2+(8jWJ5 g‰mq&O1hc8jfXJk 6gv4>Ul kq j>ޤІ) "p%@>f ^ zX Th } cv|r}wpww x7@iHkpq Zhuf 8zgh?oIs8ڠn oK'7GWgwׇؗ}DśqоH8Pٿ6w}xv'h{`}`0@C$HQ } `,~i3@_dP|x|fXtn_~(h „ 2l!Ĉ'Rh"ƌ7r#Ȑ"G,i$ʔ*WlRܼmio6~_qv`OJ2m)ԨRRj*֬Z?n Ά? #0ִjײm-ܸrҭPܸI*'Т:uv.l0Ċ3!߄~/nl2̚7sY%P/?ҧ;5زgӮm[hMV]‡/n8ʗ3o9ҧSn:ڷs;Ǔ/o<׳o=ӯo>? 8 x * : J8!Zx!j!z!!8"%x")"-"18#5x#9#=#A 9$Ey$I*$M:$QJ9%UZy%Yj%]z%a9&ey&i&m&q9'uy'y'}5c 5z8\(:(J:)Zz)j)z):꟥V4A~Q -j*WH3+ ;,{,*,:,J;-B"#<%+Fmh3M"Z58׼з{J  C;0 Y60PpK<1[|1k1{L!<2%,At2-21<35|393=3A͐A44QK=5Pr5Yk5]{5a=6e}6i6mv* ]uEp}7y7}w'28~8+.b-,Υ-[~9k^!!#y1 B Ewnf3^%:>;~;;;߮z<+<;K?=[=k={=?>>髿>>?????(< H+HB F|@,!x$5'(/AG\!PD/bM<~\08hT ADpH 1!}LH AG Š `h&Ib b 0-\[ 1GZ彂 ZIJ 81}%`k/j˄.jL"t03Oqvs.2MfsFEN73Q7ىrByρ=(BЅ2}(D#*щpXD Aэ'(5ё ISZMB$Jc*d 2g(:"OCDAiTXcNv?}sYĂ89b9P<}aBd*Qxh\3Ƀ, doa% RHq#qM@- ٴ$,\ \4+mBxQ瘆̅ %8BmKdjEUfN+q۔t(Js+Rֽ.vr.x{!1 !,H*\ȰÇ#JHŋ3jȱ#FCIɓ(S\ɲ˗0cʜI͒@ɳϟ@ JѣHB@g,PJJիXjj駁rKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ NK$_μA*16{bpQË'x @3ǫ_>8A 6$pRtހ(/ T@2,.⋁fd@@KpK-raO"/0(`L a..c#.58L6YNF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駪 ꨤ%Zꪬ0r1)#,6ċ/*-P03 bˇ+)c D29+sˉԖknA30@ W0 gH8̡w@ H"HL&:PH*EV̢.ʈ^ c(2fLxШ6fns(:ṿp>~ $n)BΆLdDpF:򑐌#HEZ!̤&IKzR6(AQr3<*-U22|,Y0.] fcz)b҅L[fΌIM@ش5ͨl<7IN3%(:!rNcTH;Ixs!>WO>9=d'AP,#x:jχvh<-t$H5őT&=JҖ$.GeJ_?!Ƀ @=kJԊ4My1/ (T%T>:ծ:7 y Yh@Z&Cx-ZoӼU{+YJXMbXgJ8ZU'jdmCK?V֮vkq $`oDkf5:&JdP56GvQO~D4\vkϠmq]Ĝ%C0 "pځCpq-MtL`:L1V˸툰@ƀ)r R̢DE@ U UEN ?EEU8dy@sqec)1LksP.b,}ģH1] PO9U)p5J O쏽fR*Ϭ4:n,偯A4\2NAN&H0jB8vBVrghLkұ%kC.l [Ӛ(b@Ypu`m)QLRc1Ȃ `RU؝:va N%M~4~25=I-ЇxCUIY[ۤ*LF&a>h'7 oJoP"F8~&VK:Z6BzFA,)qc0Ug~1`H-Q8h<2xn43H# y`#E fTvbSljT; 9vƧ&5cr(p<펹;iIc+T.xH|a  {w;3ߖ'ϖq~ḍÔc>L04o*0~.Ǣ7۩H5åf}QlʕV璇i`2><[v_G{otu R~ v}pFPs:7nfsyoql@xbx;g'HhwbwX{se0H(X."у@NB8TFMHմL(MNRLTXtX(LZԅ^K`4d8KfxjJl؆p8Jr8TvgbzK|؇hK8TJVgKS?Ȉt;%C8牏(r(Q/6nPaq&M@10(c q9`ۀP5~TNB1b8’0J?u[#S zw!mq)36 00r,A~S c`-{} z954 F9p 1r @A2 1 ϗQ20 :yq<@\p_ 99QeT{ 9♝ 虞iH9΀`iHY )HŰE.C!DP j$ (r*GFjF$Z#cf*E,ڢ02:4Z6z8:<ڣ>@B:DZFzHJLڤNP$0T:QjCUZWZCYJ[JC]j_*Ca*cBez1 ?_Pol*?˅$ XoÕ |q!\ ! 0 Idd @Yx˅ 0?/&a7M߹D0a Ȁ ȀӀ C6IĠ~.jDj :x"6Fym 71 `YCDY` k&&`[W09 64 c YD!IP:h0*va # 0#pDnP/ Q g9t`(PU٦"7 D+{0n` h@p+ aWM!u2`Ί>S᫥R ͠W zhEP P@ T!bNbp  fK)t0BI  cz[ \ip`+Bꊭ 3 vtŽX$E2uKka *96kG% [p Z )"HS Ыa 3=󽋂  c u# Ġ Q R )J"9iG€p ,08u˅ wwA>a xpl C 0p p G=Y h" *irl`HP,`"tZ|&u ג0 d<> lhbR|I<w,ȏ" ql5/AȘ 1#qę@ϋɣ^L!QRᨢ)\#ywlʒʦL d˜0<\|Ȝʼ<\|؜ڼ<\|<\|=]} u{rq*}ݔb='pn]i0Zi]e$&쀸nRV8{8]&9%cjTpsDMDsH5,ԍXYV-&:׊]~b=&&ZV_Z^enZr=t]v-jldw׀y{eٴ݂Ra܃h^ဨfTvY0^(T`V\{JHVC5 5=ʲ k7K@Q>]KHJNPR>T^V~XZ\^`b>d^f~hjlnpr>t^v~xz|~0^x>~>q镞階^Р1~Nnl걮.鹎꽮-yʾ̮>^־؞u~]1^N].nON_Ju{ H, n.+2/4_.8:S>@?Ԡq+GK_~QSU_W[N X PRTV/X6~@ `A 탟o?_N?Q/O_ &?_/6oȿO/ o?:)_aɿ/%qM@ DPB >QD-^(1F=~RH%MDbʕ([|i2fə$k)2gJ=}TPEETRM>Uԏ;AZV\az XdEVZmݾW\uśW^}X`… FXbƍ?Ydʕ-_ƜYfΝ=ZhҥMFZj֭][lڵmƝ[n޽}\pōG\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ_|ݺF .4ӆ?O$ 893J#])%j 0JdSL%'żLil պfxG]oҍ*%W~ qBdKha{MUP}V<]t!Ҕ:$k<5:- pqP @Atu3WNV1TxnZN@ r1w^zv[*uNY8'Qv5\pH\#^@e_"& [9(IpF`Ү=X0y(p3ChFlniVfZ,gHn;p&Ǿ)Ϣy쳗Np')?R\˖+=t}ܣ<7rFj^u_'NGRg؅=w[#;Jyw'~wga(槧(~Y>' {8|j\hAa|?m~ohaB?3@6<Z m؟48 8q,ZG:B0:|E PЅ?o:Xx@bK;"8E*f=At<Z5u(갊gDk2\B ?6-eaӸG>fU`/qmP 0-dq(ёc(] xY+d(E9IF0@?mAn'A9JZ0+Pf8jC HіDf`p Z00CgLnv.*^a 1h 0k f<ٖ+ndxD`_0@ (jPL<ӠJ=)D,5`Wt2C'B5Qthb!@hPa<Ge:Sx X `B' zMԨG5MAj$"@ h!hVU~D1q cI& dzj\iJ1r$8n9ptni:؍5o l$ld I5bc="XrVEaW.#mj)Zm[!%dU;Qt$1liGvv#Y!jy;\Hw+jg56ׅ=Iv"L׹M#t[Zo nQ9b}.pL@po~ߑ!5B(lлE0[H| MpT?Bdt=hBЇFthF7У%uTzҗ> "miLw4ؙE=jR+I uUM=B?djZC&C?jk䁱HGukbwv=u { mj%Ӯv\kw=nrFwսnvw=ozwo~x>pGxp7x%>qWx5qwyE>r'GyUr/ye>s7yus?zЅ>tGGzҕt7Ozԥ>uWWzֵuw_{>vgG{վvo5ww{w|?xG|d|%?yWyw}E?zҗG}Uzַ}e?{}u!*=وFV&<$֘F@l 8-] #"@-+  lO(Ί  ,H3`aXNQHd X0M@ qYp;*؈Yb`:3)_>"coYV v]#b٘$^HB[ e0@aXZx%1š5t789:;<=>@Eď(D @H A@DpDXĐXAMEC|'i l0Sdds۔>S؇k^ũp`F# a AElFUdLЕSȸ裆oo jHjgEps9G"cd? n$B|ƞkhǸGd6 tdȆȆԒ$韢@$qi\1HIHߠSG0Gdȵ6y?vQȆɊɂL|EH+1g!PyI(Ihi$,WAy)ȫJz>ǎWG(bHdXB{Ű1}ij 6aF¨G. SMQ6IaG*̞46H#?H K ?X  R$,#XBko v1Q\Yhx@E D(䈉eɑDcH+[4 I*fXJ`h`R wDQhX iý [@0$R38ڏEA=R0%,@  ^ȅZxؒq-_h`ݐR݈ohh0x}W:^OI}A u*(a=$R=ZR XH^ȅ\U6U&(Ch]愋bb"U4R4PU<ƙpSJ4Ze0J0Y :UTpl a2OM C ] bvWPNXVQߟMA?e˾i](؍PԆݍ0^\A*. )XS%HW ,pQސ r6`E  @\x`z@ 5 C 5nxH'fHbN݈B ! 5vRV0npQ؄vY \[PC.Zx֔@̍Rx1$]d"g[^vk5]P뚱[ZGuވ腚ލ|aBZ ^0_8c\@YU% cS{]YYҎZj%Į\He\ mم,Qkm!b=0pXmf ~oBZƆ ocI Xch92bvݘ(W܏8n.̟TvphZqw0R qa5  +]Zf:- UPZrV-s?85y<7 ;:8ՍvB_BX&Z}Q*u=$A$4VCTiCTAyF :ēăАP 5 ŇɒM? l&j)KR%4ĉM[6cjĦ,eqy'ʉ#S\?c?L6YBȤBEx8s5{Mv<ȳ?8~y*B )Rsezl e#N>B<㍨MVQ2H QJ 9e:,p&'3ceBO{<#\6Nυ>KAê<UDj{wiۯE<0F?k1{ܨOABHhr&21,8V_3A =4{)xDK=+4QK=Hۘ4Ѳ8q"QIG?yH4$(Kb2 (GIR SJ};ll%^}Lc@.w hCrAv\_jG;Y<~p 3NcrqnzG fwz/;d ]t z?y󞏌Q,NxS(>YάqjB]m ECR`,4 ,rQ.ʥsv8 xԄJ.pd<㉕O/؉Ʉ7VG3i"#C|:TT:5d&ţz425\(J] 6!D'A*P*H] )0 rqH=Ѹ}$%I>o  IksAo7oX*14PFV ~ mcpaq1s c<1-0u S% X&`Au8` (8C @ba MV|pq4 l6rр! `43<ݠ@7u Vg plX qn lhDpL`R5cypk mFbPpgt)a |.~4Ra2Mw p3 :p m& ÐakԆHM0 ( GU # r 7 x߲5C %2 ք  w@ [s 02@ " f4nV.uRk2KUq5/Qv w(l$C@ ذ.a  )` [(@QPcrL H00 ! o q' O\pLІ)4m 7%hD t &57rw#l)P )4VH#Q9MdtF-  6"hyLP,Vv0w"CxśsEdYyșʹٜ9Yyؙڹٝ9Yy虞깞ٞ9Yyٟ:Zz ڠ !zڡЉhm^ !  }D7DAz#C HjJLʜ֤Q*1O1P^h1P^`jHgjbzdU۠@dvmjojrM&wD1ObHp~c!JP6O:VQXWz* V ku:lZD !jD*Rja `4#HEJCZȸ  oARAL _?YEj$QhjZp蚮'ڮ:Zz\aC}$:[y_D*PeIR{T6WP PkX;ظ:б t:&{(*,۲.02;4[6{8:<۳>@B;D[F{HJL۴NPR;T[XZ;^_bd[Y{h˵ in p9t[qj{}[+;shD˩붇k˷뷓 +K]K)[Q{ۺ+;+ qk{ûŋǛ KK˺+Kkً۫ ٻ [˼+KkV+0)Ԁj1];]\q5 LKv c!Kl #̽%!)yi ,|K$&(3|g@\S^ĐĎ+QLŒŔ^`1dQe|hj_npsuۉ*|,"A#ǀ<{Dž|}<Ȏ}Ȇ ɍLɏ<ɘ|ɜ,ɂʁ<ʄɜ\ʨ ʦ\ɬ|ɗʙKܶkoŖ˘˚KMOlr+˵ SUlWY[̹ ͻ,ͽ<|؜ڼ<\ yƖ ` Sβ!hC\*ϳ1 G & Fmˀ jlqr-P }3_S `ђ p FVϩ-|j 1A pmbH9n iy_e9m |DyqmiГ! mIK3 RVM[]],2?3>a  Y>ՇG}i>;.NyDbC9\ư&tx p[X$؏m=ؖ}T2 Ґ Fڞ-I 2ڥ ګ : t@ Dڦڪ}۹  LP*=۵]ܬ}Nw I0 mMMm!" 20 @ۍ -KAޱL0 Fp] א x6SM@ 0  M  =0~~  Ȁ 3NKὀ 5f` @ ? A.Ipo0 pR.0a0 0 N { `Z^ pl> T@{ ~ 0 fZ`Z`oЉ> @ b p p@ `& F S~ nLvP GN.1&`N ?.8@ (Z ێ.#" @ @LP ^DP bP 2@AP`0 }N3o ?%o2T(Pv ,2_4_7-^>9mv9g dS0a10meZ[‿AC_kgO4k1reO81xozlp17 /2p=@/\}h5|C`6>BquaO6_hA6?_ȟʿ?_؟ڿ?_?_@@ DPB >QD-^ĘQF=~RH%MDRJ-]SL5męSN=}TPEETRM>UTU^ŚUV]~VXe͞EVZmݾW\uśW^}X`… FXbƍ?Ydʕ-_ƜYfΝ=ZhҥMFZj֭][lڵmƝ[n޽}\pōG\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ_|ǟ_~0@$@D0AdA0B 'B /0C 7C?1DG$DOD1EWdE_1FgFo1GwG2H!$H#D2I%dI'2J)J+2K-K/3L1$L3D3M5dM7߄3N9礳N;3O=O?4PA%PCE4QEem)j]NkR8)f:Ai9jH]3fnB&TKE.9hO:&8f(5:T`@dM.䄠$ XjfZil)Z !ZhU*E_ҍW]ސV%؀D _.jMW `bf F[΍` &e͒wY^cdXaReadeMXYYFs[~N灜f:jΠj:kk;l&lF;m@y;nm;SCY5QTx "(5M仦r5)đoN9YBU7GCs 0AQ -*2(2 eC!@!6J}_0!epPaU@3R (;a(FIC"S7b2$V Ͱh pUL uJE pb !Qs)eN$pw-K A8S0$ĀbH@f Lȓ>T~F; IME #x Ae*)(&I!Y ,,LabK"qâ4Q pщB">an 8eH5V, b $ڐd (LT v C RUC_ꃌךD)a*oZF5b"X 1Xy P@ ppGA)dEB]L0j~w+'Nj @$态a UBq-;IB` z,|[Ѣ>I, ctipi~ᴓV0a큘&0YԂW`quZd׳gǺ忳o-.^W_AVV"h^o .\dR6@wb/x q7x}(oAnՋh;ĎI;kq ON-n1 nUu(a01 v6 4xeգ/Dnwr@ȿ d./_d_?C/~FEKi]p[ 30>˿+Ʌfʎ[Hh,S(=狅ԊgRF XgR4; 8:L>`*4Bpy}8&t'~Pu,yy~+B&B*B,,B/C1,CB8ߘ8l;´<x8ÃxPl`BA$/C.\Ct!$yl)܇} R$z R\4MLq !Q4Exć( ؇~`/DBHv[51HĚ`(HP㤂`Q|.hņ@thFCPBV)GІPƒF q507cj48i*DXEh$ `HYt..Ɓx`P(FÂPM}Ċ~pvdN'6qppX'ĆȠL H#*3X`еXH ,L}`2D#qHڍ05ad EC8ޱ`GCJxIPK 1xȁhɚb Bllctȉ9JXlL*H1Z?t[ t)xJxGpi Do\$\\$pHodЂ̶3plL$„[@1ĺ0xr*:@ѠG< XI0JdgІL\](ljGtpHk`еBuXІ *1)mE H1,˄<(ZH YdDz@Sk8(YHjwMȇlxl؆x.Me/F܆RЀ%|ЇYIm0M ޴BϵDZ+-ԇ~SC?? ũÁpCPP mDDTGBuE !сR] k*:%YU:]%^UcEVdefu)bgVyDd !,H*\ȰÇ#JHŋ3jȱBECIɓ(S\ɲ˗0cʜIM ϟ@ JѣH*]ʔQJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸ss >\UУKNسk߮ɹ'O:ӫ_o;*˟O_%0AeSK}hbaK.-Pxfׂv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)RP0T!A- 9P袌J1 )@H^f C:@=ij@)E%͡*k P)'b@2n2,t 3`$Hk`AZ2 +DtsОpa˧GJ+)(aA~ C`{}D@y1VLe0QJ3hA`F* lˆ, CqA4̃=cΞK/#.UѴ _ G-+˷qx k衁m dk 1!`| 3@ز@X ˞6]{ 6aDDوw+ܲtڠrGn9‹ww褗n騧ꬷ.n/o'7G/Wogw/o觯/o HL:'H Z<2z1}b`c(&8B8 wec< bI!q$4<54qPH*ZX̢.z` H2hL6pHDAc х IH*#G?ơ}8R $'I""Rt$#%?FHV\}ģK,X 2@@нm u0-pX3B<0 ]၃ܔa/~18( 2a'`W:n&H|2Lr!ش r0!?!ʟ28`=b ,a?~8c`0gF @AfQ_$p2yƆYc!& F@lb$6P,@8c`nO;MtM/~:a #܀@(OW"'-ss_w@ЇNHOҗ;PԧN[XϺַ{`.WY5abdQ6MLrFXod·~X~P!aoD_Pl'0$I!fHӑNOW" AD>< SFW>,w'G]>0w1DpTDE75GniAFoN$Gsw]z%|ٗYy%9UR9%Q$M$YIry$Eҙ9$A2#y=#ٚ9y#95R9#1"-¹"Y)ry"ʹ%Ҝ9"!2!yڙ!ٝi 9RI虞IYqџy1:zё  ڠ:Zzڡ  k&&b]&:"D]N)! LMBk:A:JLʤ"aIED)ѤVzIjTښtJXOڥdZM}GylڦndJoz qsZ3to%3:z]3}ڨZ dH**HʧU^b YQPgQzYPYJd~ګWaQhJjZd^Ej @$1 PZGD:Qg[ڰ;[{  ";$۫V({<Ѐ)+ڲ02;4[6{8:<۳>@B;D[F{HJL۴NPR;T[V{XZ\۵^`[;0d[f qjKk۶n{pe;t;v{yK{yu˶;hw۷no[˸j x{ qkw KIKѺ;˺{ۻ 1ƛ˼뼴 +K[ ߻K 㻼{ۼ;[KaKE$@xs! , \l ub@ Fyt˾/ 1,3L5l7+9,vP.k024 6,8b$ ΀ pp˷aceܸ+gk,m̹ rЦ!ZV2 q(,Q ho@N !4)i & +u$l J"'< g Cl[Mqo1GX`"]=''3P֪! 0zs Oj c l-7 'OhOA 1=0{ =z0 >cу RV >"-_Y-c=N @ p&iN0(\ 0f 2n o4 p @ \*Rn7t@ c{/4GqH }%1-t0QD-^ĘQF=~RH%MDRJ-]SL5męSN=}TPEETRM>UTU^ŚUV]~VXe͞EVZmݾW\uśW^}X`… FXbƍ?Ydʕ-_ƜYfΝ=ZhҥMFZj֭][lڵmƝ[n޽}\pōG\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ_|ǟ_~0@$@D0AdA0B 'B /0C 7C?1DG$DOD1EWdE_1FgFo1GwG2H!$H#D2I%dI'2J)J+2K-K/3L1$L3D3M5dM7߄3N9礳N;3O=O?4PA%PCE4QEeQG4RI'RK/4SM7SO?5TQG%TSOE5UUWeUW_5VYgV[o5W]wW_6Xa%XcE6YeeYg}T61xyd 6Ů3YYfH $g̥Nbwnx`bWb 7(]9} &h_ s" @a~˜Pz:8Sf)FK֫` af9q&N,_E9%bH\)^yz_NR;ZR󲅖f;oo yMk xC\)%g Ol| @FeńG Rd)$%'U-.;62U %`r Ak  Z[#&T' )3X 2ߜ` V @JB|R AhŠHR(@ BS z / wi !50?uX5A$,^ C xk3#"H -f gr2U-,^`2,=ˤn8/86[M _~YЂEL{2riy[|os \|x |[Ca ^!1v| v#k/g~ e n15iJۍ $>h ' #HZ:.]Э؅-j!+|EN?ȕCR#DS]~gI<ima7XL#x4ۯNW}Ȣa/V,^_P; a!s<8 \y`0ky @%ه}m~ lu T  A}0~AtuAABd # D)qu'+4VD_(h .txEAF x(HtЃVPp`A%$( e ;dc<S6`Ʉ 9龨tkHl$5X" `7 PB`8#rBlHIx@SI8ISl`(DtXEr#vxf@ Jt,0mExvDJ|6ˆ8AOTp'8X ɁL&F oPUG̀L PM'BoH+;< d`pEdq½JbC5*`BnAƙmX͂ԉ|%Kdζ  y$0D%d CtNVAi }PÃсQ֤ hyq`]yC `Qy؟ "]~Q0$-'*'+}xR_q5!,H*\ȰÇ#JHŋ3jȱcFyIɓ(S\ɲ˗0cʜI͛8gϟ@ JѣH*]j1VZNJJիXjʵCK^ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ N|r%_*μF ,U سܴH+ C7p0=b9wWYb Aܲ߁&Y.‹@((NK. FpHP-rP/R(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix橧Y砄ˡ&&*-*1 BDd,18-*jI<$@&jiB̨j1(t /b,HQ D)XP"RD&(fKP z4BLAlTM'p:`^rB*26r3  ("RFL"&⇉Nx(*jV"t.^ #p(2fLmШ6ƆncDHA:x̣G9! IBr|"q,I#HZ#K%/Il|'CIJŌ,)SJLd,gWr%.w]h%0CbSL#̹\3WhZ"< q` gDYrs ?1mgݐNDH:O쳟=?JP t$(h>:)4 ݈8ХL4=gD5ьBԣ FCJ҃RJWq.EL9R3)FԱm/թP)lHMjOԈl6jٚJՅ>*PUJNh 3@ldfM\Ǒ |٪\Zy^zցu]8Q"JHR+6ա1,N-;TMV x c9Suu %jGsRG ^##Mk3;ikۍ#nӆ Ѝtv1ZE0:QFH9v{kh <[A;ŏ?ZLr'yPqpz ۜj 1 `%q9wk^0 <y@@6H$H`GAȐa gGQ7ȁd5PVd;c(@dLe?v!_)p X 8A8[xH-fN`1.MPX]Ȱߜe6KYAqX)z SEtxQW ӷmZpr 4nR n.{0Ha0~a`4+ )2i>D" J sd `%q[נr QW:`~!k(1 b0 *6Qb4t*3k{Hmy)a fCp Qw9}oW ΰp' ঘi cs#Ciw NN` `6(-1i!1s1 `ZBr0zHnP/1 O1֗1̀ A00B`3 :va ow 0g*Y0bP,pRhЖ  *4AS y:@iZBR i8)?ȅMXyz: g_c:\3 Q> Z#&:\Pk u ͠W  ,P 1 1Z  `[4dp{RX;4B YeN  30p57@tc11g %W4 U4π PW @q8"9-3 0  N@ $k<?6 ;A g )d ggڸ !Sec rT^V~XZ\^`b>d^f~hjlnpr>t^v~xz|~>^~F`.^~蚾^%}].N>n>vmЭ롮Nn쳎쵮NԵ>^~؎ھ..qٞ<Ξ¾>n^Y.~ /. O?  @ QzQJzN/135E (Z  b086E[]_aocGoIOI אSA?Atf|xOvu~(qB/__ qOք /@_O?̿_ןԿ/?o2]w\/^O`ob @@p DnB >4P@'NČ 76H%MDRJ-]SL5męSN=}TPEETRM>UTU^ŚUV]~VXe͞EVZmݾW\uśW^}X`… FXbƍ?Ydʕ-_ƜYfΝ=ZhҥMFZj֭][lڵmƝ[n޽}\pō} r͝@Zpڞ_Ǟ]{pIf}x],iF|4 pگ֜0@_GRBA0B +PVmy#0D% Glz;bT*E[1^1E bA&HzE#_3I9hzqE+FKđ.6Yo:r<\P[|PR܎M<6iqJLt ̮=HB9 qY&xƙdN dXr^b`xk: ^WeZd9^Uk2a WFmI&zkxv]hpH]e][\uՖ7E?T6]^U܁|_[pjQaVn{e&! ^[i1S]OccZ$X%┆ ["y8ugAD(zj+y~x_٤gi-FQD1Tm bntgnm'r~j^a=L'A0Öm䣆Tg ٖޘEl8?=\z9xd戥ggZtotZc pIg̸mqgu[h0@1yڝ??Q@ 07y~ԽG`b)`q m C~ʻ_b6p@bІ3-Oe&`1g8a eȤ5+L A8h48Hш.hBBoP-E.'*LG] ZD1!үol`(F,b ZSZc ٚ9avŮ kid$%Y%1ǰ1Ivғi!IMrDefBDRҕTet!ʷre.#cȻ2.9𲗺$ubJb6ә}X2'gVӚs1/kvӛo6Mr- 8iNv,pg<xӞJ=O~6Eh@OԠ9AГ\C%:Ѷ (E5PN$hHQ|@"Ei@Iڐč)i5Wʐ10g9*,w:d @ı6uCHQGSVUPM'H]jUR Dq]EU ; 縕Vnq-)k]ûկ_ְElbX6ֱld%;YVֲlf5YvֳmhE;ZҖִEmjUZֵֶmle;[ֶmnu[ַnp;\׸Enr\6׹υnt;]V׺nv]v׻ox;^׼Eozջ^׽o|;_׾o~_׿p<`Fp`7p%t=hBЇFthF7яt%=iJWҗt5iNwӟuE=jRԧFuUjVկ5$F<-xrnmZGDә@` y)5$lQN @Y' !˚B $!dcsD'BX P!h d 9"9 zc"@Qww{.^kʸNNS?)f<,> "5yoC?z̟'A{AL~F3!9cƶ}???ЏWZ|sh5!dH~%˿<#gxhlh?ł> Y~8 p(6O9lq@X@蓿k F  Ph k g5"m8)eЖ $$ia4d)6qcpCP#BMbX,bt_Bg@#94 QȄ|)B-DC/Q`. F    559 PDH-D! iDbPbWi,-xJi5 5 O Y3 )$9YhY`Dp bx2Xb`h`Thk gEX@DqsGY F:vTF6'sk[o6HHfhh?SnYƭ0UF/hPplWkX{8K+`wkȅ8l (LUhI'`UXd7h8˩§c \> 8_Y`Ip Kw< A S8̚ hP# 7S 0ƼHL~2@ex. 9%HRXrUB9X7CP39c [K`Ml sʘ (Ht0R`NȖ=$΅X?ȹ8(WH90H^sqLR7'hA\S89Y\(- ɟLPNY0X0Xή ljٸPCҁV8 iN8axUX7U p>tyPρЁpy %^n`+Xt4KhD)LҠ0$=ЅR\] Y8ԯ;؄oP'Z4 oHM(C 1S` T Neb͏҃ P=u`]dMP`T(\ ?de'er-W5 p sT_hsC]њ Nx_5] HՁ85tu47(XP_ՑxKHV,_8WpfW@@ tЁIhUNX٫طR;X71]Xh5pY腧ՅYJ)(ӧRAvH]8N8\XhOX WHpդLԆPTS[WfKU D`ZXd2ё@T%ׅhXPN'\a\Rl]M %X=0>h)=A=(S҉ NZȃ{*H X07ȘYxu[^Hˆ0uN݈"] }3 )* )ZIh*MX+ }e ՁЅOPQP؃R҆X"``VzP UӅZɠ} ^ ^ +ȹcpoq UQ:b^yb6΂1w0b(QU0͍_ r4];^`[ X^PX^cY]x]:N۳Gdd\ȅQbYȹb?7~7x9e[(9]:t1e\_`ec[y  dzoAi8F_( 3egqF܈:^cԉ7%)ٞ@LW&8H y c~6c [g8Y(fXPXx*-[@6\piKr 9<=hHjеۆS7 jv*}ЇPuqh}}x6yꅘ> jjjh~j؆F뽐꺨셠Ff ~؇hmh~0쳠ζk5Ƕ}xB mؐNylP6m0f `Sbœ?懍횐n &Xy|+~~1n-ot mFaxn0͏`(x.f b  `GH+mNk^mn-~+nk.垫뮇+ok/ޫ믂+pl' 7G,Wlgw ,$l(, ̘Lٌ͑Ίυ ~ Mwr)kT&uXvu\wu`cudG6vh+vvlvpN#?]@Fquߝ|#6Yۍwu?(.5wVS{S:a;ܼQL?4.H?YW<ݒ=<@4.,y#P:ǿ>t3 ĉ#'1~L M(C Z&_7DG NY0o{ g%5S$=?!Q#E<+%:"M|)ZQ U'rK"(1fL6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIR.FP@`J- @x@VT''PK h ℁ (:„(L#(5yL, 600'hdQJm 'b7Ď].07"@ Bb@NsTraAe(a A6xH4qI7Bl YuAbP k<"]O.RPúBa`#|@ tN`M $P_8 aCx6J Ca]yBQ:hMZֶp\J׺xͫ^׾ `KZ IG~L05Z`@pa6ԡl|ձC0KlvFmPXYX6XF6!lU-ĖHlo[a=Kls׈L9܊ȸխPoṚŮw'wa?Bz " S0ӽ/ˆ0_ro,} x0w Nΰ7{8bX5 xU&bwm *j*d<֖< _jj4)$DerlCRl-sY\^,fm:35;nVl9YXvg]϶3e%AІVmE3T~4"-iNQҚ4-MKӞCͨQQ>RjA~z5$Yֶs=]Ku-,U:6e?^iSH־vq;6o$qרݢu{E~-ѻ!7?}wB?x 1'N#̬ϸ7{ 2Lr* Jnup˹r#ao Bk#֥9듎HOҏ#8oҧNu?}'˺#$ZnH+0pv}u{$ wrx;1~>^%Z;g9CxÏ/DK7yO^3:_jA_x''JgWW8Uad=msVED"gY4?ϧ=@$#"ICr}g?3bCV"`&C/""fY8Xx ؀8Xx؁ "8$X&x(*,؂.0284X6x8:<؃>@B8DXFxHJ#]NNHP8%T8VxOZXх[`1df]lȅ`](ZHWhTX(qh4qoȇcPHaXihQ(}fgXw5ExH8ՉqQ~(XHhH8Hh(Ḧшh荽ȍ(HhȄI ^gu1FGhvh(Xc9p ɐцuИx!(I`WpE-ٍ/3ɑ+1v >E  KhhȔ( 8PVyX \i[ٕ\`b9^9fIhg)lٖU a؊_ȗ藡(Hgѐ٘阐Yyɐiə9I]zn8Yy8Y긛ٛ9Yyșy qZ)EEX6X؀:C.Yi9p :h4eRj&fbA'uj` P70  Z5 3i"@Jk&KlZ)k0K R` 0 0p+PeP5^ ۠L#.P亵Ѵ P -Pѵ.`b+5` p зQ Qp:GukZ6  n 0)P!_{{q@ e4Q b;_溹{5kZ P5{4΅Jsp` N*D )+hZk;5J@f@[At`CX _㋾xa`RE `>Z>jε.`+ \| lgJ :5Ή,7:l+$'^1"Z8+: "p1 $\Ɗr $ ÛVlRl@^`Lb<^Zݞ;g{S5|'7@aWog]:wϽ/o觯/o HL:'H Z̠7z GH(L W0 gH8̡w@ @<>AH%*ф% 8Q Gl: ALt%MUc 6FCH1.*KA8L@ 9B{5RD&46򠡸6")/C )p0i,@*IR <𘺁c6pPQpcP6yΖ%AuӇ@QFAl~ @JЂ=̈́vmXC6Nj`@Ѓ ;8qQ \+ALfFҚ8ͩNwӞ@ ~PQm@IFNE uDgPЦ T4@` q5K=ZamYCz2\xͫ^׾b~)K’$ , M|*d xbSa=|׀P@0NMu9p=Ѹ Qq#Fמ!pHMWH[`55gxF6$>x8pB>0@du6klEaz :ܮ@7د ف1z0% ߫30s_0B,y"1ǥ'.vWcӸa0 sٸ1%!+FX-$3UN~fy,eSԊ2I-^%yYc>sҬ撙ͻb3%9vr=~nYAZ̆>tB3ш~4=i=WTti:sw4QӦNWV`0}gMZָQ>C׃jC҆1O;AeHLq`Htl7H#Nmt&wE❡uV X7Om;8+Wθ7qe1j\MGܥUCK>eR.q-sܛP3]s<1?FЇܷG?1dlh[<Q?t 9:nSxq;Bm䃎l?gx=HB8Q0(kH;OȃQShU8WxAX+Y_D[(a؂gheJHcȅm(sHuhwyqh{zfp `m=Ȉ1X=G=ȉh4P=p €LHl؇nt(vHxhz|xpȋrhPЊs臺(8XxӘo#װ]Ixׁ(h刎 Ȏ"j8(Q(HX YIxP@)yxiYّ 9"Y!y$&,.0ْ12Y4y3-yP@B9DYFyHj=)MO QSUWiYY[^`b9dYfyhjlv٠`Lp^]mo9rYtiQ"U׀y)v Ӑ ϐ FQI 2)Ruo٘ˠNUi0S`LⵙJ_ɘ )!Z\pYgq`cډٜ˰ )"ٜo p^靈 Vy` Yyyc@ ")  I` n|1zr  J  w$Ij+ Q `v5&ɣ+ 0 AGjoMp   Gu"˩=J0  NF:pJ^E `@B=D]F}HJLNPR=T]V}XZ\^`b=d]f}hjlnpr=t]v}xz|~׀؂=؄]؆}؈؊oR  q 0 w!:!=} % a  Ӥڌڬڮڰ^ wJ` E ΀ 9} 2e uR6[נ[ MM9= %S'p;gmq3H9 T}bNb4E =0 a  4= u @m =-F{Ƞaa:@1Up1=6f!,@H`eeÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0ctZ3sɳϟ@ JѣHzјҧPJJիXjJ)$\ÊKٳhӪMb ZkʝKݻx&mk޿ L|'"H%zp˘3k\7VꔚG@IS^ͺ&=SC`ͻzeC$EPɦf_μw oK /r5ËO^t|]/hVX˟O k2'hSQ4.Bw-pIЁfbD@t.uh((r!D(4H`6<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmPits׭wLw/6$څ'~y+8H?.ETnygwg=8褗n騧ꬷ.n/o'7G/Wogw/o觯/o]up@Kɣ ؂AbѶy4g|*$; H<`@|+LQl ( b @DQ!")ܔ̦6 A y $B9NtS^|@A:D!lIͨF7ш FGbD} 1Rg-m<|Bb45XkL! x\IUjNOk;eTnX] kXSA5kWњVUXj][Wxͫ^׾ `KMb:d'KZͬf7z hGKҚMjWֺlgKͭnwV/8Mr iHC01mj: lJ?gk"o!6In'M}w "$; d8я]L [pϲ KGLb o,!>1by wlbEtUEAH>  I1 P2WD'"ysLľKAgX_fYxγ>πMBЈNF;ѐ'MJ[Ҙδ7N{ӠGMRԨNWVհgMZָεw^F5Mb۪9me/>6]iS{־vhծ-q#;">7Dmg-vomv=pxSߟk]-2f}8JU5q7<DBn G+BrJ*w8OZ<1w3?xS>'Gq^t'Oy}>uW]ƭ !p Nv[hӎn+"wӽs>|5 06:oKT%u/O\b ^/}Dy^7}~78 g #_>'wp Z8Џ>,"?֯>s~/~Sʖ\.nyI>x!_wJ(Wxn ؀Vxh㶁n8x߆|˧o'|%o)p+(p-Hp/hpGYW6x8:<؃>@B8BNQK%tDF8H~ W.'rKPx-,Ur0df, TVYqDQdqh UϰHjW?Єmxq oڠ Jчg%k$@Q `n8(W(fp i(0kx(UZaA qUE"oG0h-7M(P  GQ!ℌؘa8` fFhX(Np@  @4 `  PEq֘jŌ @@pz 6ĐCVa @L :  8$ ")$)b` `R  T ؀  T".tHau p $t OY pOQd`vՕ爋fU& e 0Ԑ pd EMbQen 0 p R W)ԙ1 Gh) dhv LVT p P9f`BMU:SY (bEUTE)G u)30@0bubu/T9yofOi  ŀ >%^ SRép  `g@% ʠk堩 `f`yձ Wࠟ0 @  >Vࠓ(`N Q`Q┟LŦ 1s` Šc;P UIg 0` Q0 (yjfV3`Mb(3 *a5{ `L` >{*bEx⥞sB@ y zZ9e9=$vE@U,ŭZH%Hz9CܺUSg*Yo^Qj_:;zf[ ۰;[{#EDDM!aR]ENꠛ]10[X$;5 ,۱1ф<;?pN@@`L۴NP`T{_PQ+T$cX`;f{1cn۶m"jr0ۯ ]ȎOa`R{]TfwC [\;[{۹;[{ۺ;[{ۻ;[{țʻۼ;[{؛ڻ۽;[{蛾껾۾;[{ۿ<\| <\|ңH` !2 D Q"+! ( !L £!,?H`A]\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸s2 Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8xX/@)dg iBL6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+ks,`.^>+]Nkm\^-[nY~+XknWs_[ VKQًLG B L' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|I V<=ANN@e8p CP)9CJ,T>"P: 3<yCX@!>#84Nز9Aߞ 8@n *@2 [iDCMP 8IGjd)"G8( HAi0iċ5gXC@8Q9`'8IH<j$xF?ŸD/^%~FG # yDH*ZX̢.z` H2t@ 4&Jnӂ9)"1-tiӴ @&ys.A65%ӟ iNLTl9@Q~"Y̏LjLe H`f)S6%= PJԢȣQd:P , &j8ND8 >I^SWJֲw3Zֶp\J׺xͫ^׾ `KMb:d'KZͬf7z hGKҚMjWֺlgKͭnw pK%Pqi.Ѝt˪R=P@q] iڀ4t7=G=v葇|q y#΅x_Ro%`oi`013L *;Lbi:!;rW,_8αw|s% (=Vq Hq){9T Um#J/9X8o<%@rZ)G!>sל7'yM.6w@Y>9ѓ3A:ԉ.GVzDuwp_a'юr~ig;>v6;=q{sϝe {8Wq:!yOP}Et/Yg+=G D^\7 Bto@0oFsxyޥwqL q\c {S}o"Do?q+(Xx 8Ȁ(XqF@Ё 8#((*x+,.-80H2X8x:h<=>@8?X9FWL؄NPR8TXgZIxWȅه}'Gxhjl؆npr8tXv8$t| =rz ;FBh@o0 pH7Ca D@ 0QߐlƉ@0=8Q(` 'S i (`8'I1VX > B@ x&QJT x  0 @X$ ڈB  &a >iQ L9` Q  /!ؐ 3 ۠0C ɐGh.o/ @9Bj 5H+Q-@_Ԋ5  N B` ȎH=hA `@um `< L Ґ _I1 t) 9 ) 0 G9Zp00IpJ1AQIiik'pY Ȱ ZP |ɚ')P $) 99 QٛqF ԰ f V1 ) pA `#[QI Р PVf Ȁ *Kyy Jo jffI. #*  ` ;K@w `Ƞ oy γLAt   ŰZ `ʤ &`N Q`yp0Mq ap  JgaO$ 0#1NөI#P Q oY Zu`؄y:;.}yf ZU*i Ȑ<5GѡbZ0D:ZzگCC|G"  e! Ű5LOu! &{:qdB*7;@1K/<۳>K=6QfB[ G{<ڴo! PKKzeS[Hk+Ad^9;`0k۶lf{iq2dA!gM˴B*DjJճ5J> D[۹;[{ۺ;[{ۻ;[{țʻۼ;[{؛ڻ۽;[{蛾껾۾;[{ۿ<\| <\| "<$\@ ` 񎢑l'@< 89 #=2Bl<p l0 J 9Y!B !F& J6i 88)%PNi%U^Yn~]~)f}ai&|ezip)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/WW]EcdoqpN;/MlqcB+2e_: @! '(̠7@ zT{A #8я@?IJ8a Sؓp/ â"q<܉c4<qD%0_)zUĉ:HFq#C2/1'd,#APy^|!(c ǛQGx' XH` p_%asI56Y"f)q򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:vQFէDЁ=>|2UhDJ.u(F3:QvG:Rh2Qԟ.eiI9ӋƔ'hNQzSNJ@ PU9Qԡ2A}*TF5"VE*Vщ` X9YӪֱa}+\*׹k\#WdM`޵Ukb:;5kdXNV+D{@3tM ֞V"u-lcis[޶fԠFđG%Lه4!mHt2݅Tͬ_7KXU@1 6kzI]ȮUl|]]#⠆3`6.\*D,թnj`'L @"1 s07&&1DR⇰X$ ט7j?njR9,M%;,},$Sǽ2-g9^E,ݩG R4<}ș,S6xγ>πMBZ*8%puTRMz%4jZ#FBa_$r ^m PůE6I?"m#%vGiL:6mh#v6½>>bȢjHF2Anx\wwA l;ր4^z_pFƑ!iȣJ3a$(OyANy-gyg<3Wo>ۢGh\<3a2u$EԧZTVz֥s}R{ֱuk`?{mlNwCqň 98A՚O;o{kW(PőbRj@n&j7iqL~ $D&Ba-Ybr']CX򗯒З?a'"|kP P~ wK2tq$ 4a %3@0` YQj,؂.4#f@r $v`P e&%42E %@ p vy0^E~_s?z f YFgH}cr` 0\Z f0b\f0{X__Q 0  bP 0pl@ȉ6`Q Q Ċe  p y` I]8 `Z nψ# 1`@&) ?P `fl  C8%  V  &Eqa* @ %)i- /a3i. 7I] X!ByL'A)G @>hE9g!TyXZ\ٕ^~`ib9fyhjlٖnpr9tYvyxz|ٗ~9Yy)7AFHً ȘIrP \`@tZd͸A'A rHGIBٚyٛ9Yyșʹٜ9Yyؙڹٝ9Yy虞깞ٞ9Yyٟ:Zz ڠ:Zzڡ ":$Z&z(*,ڢ.0zP`I_aG|`dMɗUEeFFz) _w RAJ4 t) 0 k:0 Y  n up 8 *5 $ Xz w0 YJujy a jv0` J@`qz wl'w/qha P ઻ 's ںڭJ@D?#EBץe 0{ p{@q:Nް݀ʘpp9 ;zC,!,YH*\ȰÇ#JHŋ3jȱǏ C/F\ɲ˗0cʜI͛8s,2Ο@ JѣH*ȳϥPJJիX&lSׯ`ÊKQ(ͪ]˶۷p=ݻxݻդS LϹO +^̸"K˘3k̹ϠCMӨS^:׮Y˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k&o:3 56%=oξq@0p&<\@'\6؄N2L&8 #qS@/8[-f4,35|9ۼs=3Avo/&mßէ w +Wwdl#ka B Ǒ C8t <ڢP+#K!m䃇oo)b+^dĂ|Ƀٞ$!X̢.z` H2hL6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GI씨L Vl%,cYe-sK\:l0Wɷaô.yKfҙ+)K^.^1ԛ1Jn&*LlӚ$'1&v̦CIOwB'>}l'@:Pt} ?P{2lD#ʐCQz&FRxtH R|K1:S5hNEzSԤ?Ei)Cp\8&``Ԧ.!NS"UBM*Vaխ!^}MW:VdWp #! DPT/2)KRԮ4ūM%Rԯ<ҁ%|)`XVic:Yk&0L':Z 3mfjhv|lgK "lq򶷵-pw%C܆0W!!]N5,[{ZwKW-ozϫ덯{ ڷ~+ŘLNYLԖVfm];aVk1|Z GL(NWKh.QBk8Fhh;Jl!0F>`d0{y2"eLcSJ1,/e^ 1-f>Zl^ͦųn1TR36@ 2LpAhBb EAM](zя.4.hHD!.vQKKӠƎ2Qyt~:-Èg?&u.jq\׹V4 _d;؛vvZ:mk kBaNvMpOօotBPihqmrw`n/ D Lj !( N)qVQ8 6jV1ő gI= sX Ch!@#!E@(D#+WoӑO_h l}\yVX-&0P y   B/q}x#ؾ@B9DYFyHJLٔNPR9TYVyXZ\ٕ^`b9dYfyhjlٖnpr9tYvyxz|ٗ~9Yy٘9Yyٙ9YyٚY(tB'P0 0 @/5Y< V :9nʹٜҙv Vp֒o@H9Y E0 .8W$q8H ;.r IHhp`A4 Y}!,YH*\ȰÇ#JHŋ3jȱǏ Co߿F\ɲ˗0cʜI͛8s,2Ο@ JѣH*د_ϥPJJիX6lTׯ`ÊKҮfӪ]˶۷YKݻx˷߿ LÈ+^̸bD˘3k̹ϠCMBuS6ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfvv6 .|<#<t?^I"VT@yHF< *0+ TѾ d`A7ŃU2( @x BD!kZrh5dAL&:PH*ZX̢.z` H2hL6pH:x̣> IBL"F:򑐌$'I쒘d N^$(CQ9e)SJT:l,7Y64*uyJ^җ')J\_1;-Ift&&K,Ә1nzLfCINoB<':éu~&<9Owsk;Osd@M?Pr eBQpVt1DQ{vjhF!QT#eIR-h%vӞ "> N*Ԡ==*R&NeC*ՆPU#AG`u}8IRգgiZMVUmeda%[aWլEk`:X֭+&j8F:&kB3efavm%5il MjW u-l_+Cr{V-p"\je-\6gt9]wVwEecݷp /Wm)~-/z|o{+_²m%~.Ŀ pBApAL'L [ΰ7 ]%#I M|c1P\,иu1>n !O R[*Y)L~X,P_2KT]rwrA2Asa5lNsfyх>":~ֳ@b^B*R/,C\44-"iGSZҗI{ҟt3-KGNW}$:Ur -\ָεw^vk]Z_ʢ$TЎ@<b)Ը%jCOV?n(fyx]ȟ͉b8Űʊ'MG>(a ˘iB 0A> 5FnpD ( `g , +'fȇ5Pdc bQ2 9>5<P YtC @a C-D:j0ga@G'W; R8)Ox #%Pp{?&m0a^G==!w1Q{ rWԃ?C|P ؀ ;p;3 S! ! q `B=1b?G,a ?hp fpBa\H P :H  G | נfv0 NV@02v( P @ @ $갈5 & oY4Q 7 v{(6ш3 (`Njȋ  6  5 N f3Q 3XE7d0ro8cX㘏D ys ِ9Yyّ "9$Y&y(*,ْ.0294Y6y8:<ٓ>@B9DYFyHJLٔNPR9TYVyXZ\ٕ^`b9dYfyhjlٖnpr9tYvyxz|ٗ~9Yy٘9Yyٙ9YyٚI2!0$  @k1 B`;0Sɴ Yٜ9Y@ P 2 ޅ ` 8<3o0 9P9Yy[l80{##\p'Ct3@H 1 GtuDTCӰv耟 p` `3C .(:,!,an{ @*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,V,.Ƙ"2H"6!:!>Y!B !F& J6i`,8)%PNi%U^Yn~]~)f}ai&|ezif0O?CМs5zD9gw⩧5eJ'(v0>;9=JÏ>ayA:**B@>ݰ ?۬*첸[? @V۬?A亦SP-CPڹ>Ә5ɸ\4B 0hZ GZ*Ŧko$A*$cq$M2o0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o觯/o HL:'H Z̠7z GH(L W0 gH8̡w@ H"HL&:PH*ZX̢.z` H2hL6p: ÎGyC !E)kYTʜIgQ 6INŒԊ0R oVT,L8T:`[TGtHL2yL_ &1fZt&PqLӘ 2ӛ(fq3I-1mlЇ/ &8L,~xYd|GWJNAd.U.K89eA#4hWDѓΔ3])NI6DFI*]N&~]"dg8]B 2xO{AN+ߣ ["A{}w"^E%5k<+y|e>!wO5-lA^"Br2/xv~0HXOO~56'叐dW?b(wnE&XW ǀ"AaHRf U  ȁ%')+XYCpGB 5.vU}dF0DYpHJH9xpHP7.p\u X9H00PBTx mG7qCЄ@ p=-Y~XxM( `vXWSlH -YBC8X80- X~LX.hׄ M~` .Wgi@x& `@bu ڈXЄ@ pO rs |̔WC SWK` Ў(hh8 0 @&) )dY  pf`y)ƒ/fi ؄ @ Ԩ  g厵8  hW?Ж7PB#@P XUyB;n0p0-`N0 ȁ(0@700@@ tI7 =Fp0 [ٚkDy@D`ư EQ ` PSh Pif.a )PS9hHP ٩ٞ @p.  1* ȉџ ȠCAڡ!AD*gH(7!zq0ch15Dz=Q0 "AuV '\,59*!5`QTxhz V. 1vZ|ڧ2;ڧ*[j^ʅ95BJY3:V0 S00p։vџHz\+jګ*lڬ:Zzؚںڭ:Zz蚮꺮ڮ:Zzگ;[{ ۰;[{۱ ";$[&{(*,۲.02;4[6{8:<۳>k%(0JQ P p`ZpP g@GK@  f 0: ` avp a+7hz gjжy ||@v0:7P{vz˹빸*+j1QP @}0 Pڐ Ujм`/LjB'tŐ 0tt; כZ C罘P;ŀ K@0 jC ି0SKܠ}ǫ g' qg ! z:   ~ⶊn'l}e$q|KP du W. 9l@ 1 ; a lEl? gj`'s úy4\`,B:@պ#@`qGnK[.Բpplc6Z zz,~j`kP݀Qܬk@ ` 'Qzm8g!,anru @*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,Ȗ8.Ƙ"2H"6!:!>Y!B !F& J6i 88)%PNi%U^Yn~]~)f}ai&|ezif4?CМsO>a9gwg>mJ'(v0g?Q=JPArj*sFj?sʥvU*믽xQʢ&j )ʓM<$AP+nlb[R73iLW|nA$)Z,2fG1A?0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o觯/o HL:'H Z̠7z GH(L W0 gH8̡w@ H"HL&:PH*ZX̢.z` H2jYfL6p2G렡 ]@ iGQ@(dAABf`@9J%0mlA$JPAeTJ L90.sIK`,e/ٴS8 U?yLcxf!~*,̦6nfCyf4EMkśL7qKŰzr;9MnŞ]'>}T*24AJT_y(@#JЉCU~JAAQ{r'EWx$-:O &M]c~Nw\ vo+:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:d'Kʒͬf7+ԈphA+r,jSղֵiiZz-ne;ZޒVg{Z6mZ!ns v%fk[ָqB or"~W"5/z+}x"_F/~4Dq"W"60,3~#,a ?pD2| sx0"b 3 FXbsXq]aH-{pƅ""9e%+N62PYVr!-KG:2 mˎf!ofH2gԙ^d˝CgWCqbD)qP@F0,"|E^REuA_"հ YֵskY׸}-a">C @ mBHڃ ns&Dq6nr;6Eվol8]v7e\VoZťxv56yuqG'ytEp_0gN8Ϲw@ЇN{is0t=BdXVUu{`Ǯg<#h/3wPB{6#0npL81Oq }- ZԂS~O7_y7|-o '߼]!OϽw~)=.HQd թ;ЏO[נq w?BvPl<#,~`??U~? b_g'QwW~~X #F#"(}-4@D0D@pG~-( - D4x@ ## `B@0<9;؃P@0pC0?`c@wtP @ pG<0ǃ!x_z |  PP=`K~tz 0`5(~d( pti^  Ґp p5 5._^  ذ5 X%X_fGH ԰  0Fo0騎fgv( ` r#8y/嗀0 P0(u)/ ` 0 0+(Œ-ivh @ `P #ӸBf.yv` @ ` ;ΈHYhD Q Y-@8. nnh 9) ` +4ȏј ir&hGw0i+5DoѸ əvs @ &I10+Cٖ! rv`jyF00GזƩcɹМBwGg  `oX *n֝) Xxwg`@ PZ oH i&rG2(BȞHvqF@ʕ[).2A @Y7q D+0:ph5C4KJRP!n` WaQB f(ShڦnpJ5Pnq*xх7NzT9z|5pz P PPUP T: 0ѧ z0i j"z}J75 z 񪽊:!Ze  1Ԛںڭ:Zz蚮꺮ڮ:Zzگ;[{ ۰;[{۱ ";$[&{(*,۲.02;4[6{8:<۳>@B;D[F{HJL۴Nv\`~& ppSkVkTJG.1   `Z r; r;Z  x{뵓 ~[˷K~иq{k +ˬs0WxQh[RP ;[+[=!,H*\ȰÇ#JHŋ3jȱǏ CO_F\ɲ˗0cʜI͛8sgΟ@ JѣH*oʔKJJիXje֯`ÊK٥^Ϫ]˶۷pKݻx˷߿ LÈ+^̸@L˘3k̹ϠCMiRN}װc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv0m>N43″M2dL2L3Ho< 8 fC55 3p6qc^8p@6 oN+@ ,8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.VWng:2ihF`1:~;M;У@:}_C?{}\#?3νo_:SG=kԿ+ه/Ǽ@S>}1/ IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ%*^e0y a%2e2yf3)Mh6d6cMl:df89NcԬ9})qQL4שLyӞ'9iN~|;JЂ"M@Є2}(DFC2цpD-:҉'mhJRb-5(>NE7zS$IҐԧ9%JRAPZT.Us8G ne5` k"ֲu!f+Dֵu5n}+k"WUwe+Tfň0@DMBS԰eHa*SԦGlRTZ/l^K٧J6jh:ګb{*F@HMzӟ´4:O֓n>O Mr:}s+]R}vf!nC{ܸB @z׋A׽|ՋD~/{K`7ND؛0};a 0!n0V[ޖD?OĻeq@]۶'qoqcXp\\"HN&;PL*[Xβ.{(21/FA 3pLgYib@c "Uw6Xilg tD\@А4@ [TwΦgrA 95GMj[Bt ]հF5.lUεqmMbNl26BhO*-l`^K6;mt+E.kUj8AȐ6ukBI 8Nb_`Lx20!2^  pB)8q $"Iq \# jЃt<?c.\x(xp 48/lyb0BН>C%R6!C?- ;-` @ p=-W(`6p Ԁ@@?xw$ 4ea#:ā!(@z Ծ`r Ob(hMt'x1# P 0PqvX'ˀ 0 Q p?P+8 Qːgwv?yHz/l!@ ` F}+05 @.r0Ah0 0@PBF f(B0}WY]h8P ð P U/mxoqBaQ |UyXQZ(mC@F`AC-@1hH“(- 6QZx DP@D0D`zw@ h`3nȘ"Ш0'p(8a*#- 0n5P0P=X!, H*\ȰÇ#JHŋ3jȱǏ C9.F\ɲ˗0cʜI͛8s\Xeʝ@ JѣH*]ϟLJJիXjM֯`ÊK٤]Q]˶۷pjM{Rݻx˗!]} La+^̸"K˘3k̹ϠCMӨS^:Sc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+kd)A2t"S5O8)O 8a'88 ,4\M6 `|  N6$[ [221\ml343k3i -3fL7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wng朳O砇.褗nzs:`FitFctх rS:C;@;SV0(< 6\}B_觯yγ/?OLϏvɟHPsKWP.@,'G8 .L!Ah׈{C2TBgq@Av-|JAJ2ŇbY^#5D{_+ dX(hL6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gI]㖸̥.U]/)]<&2oe6,ᖙ̇Pּ&/m2f8kSYNjӘtf<9OiS$g/i8)!D곁tg=У1lDmhFуv hH9:ĢCI1Pԣ/iLE:SԤe)TST?-hPuVRԧGuiRaZyO21ZX*DUn\*X*ֱz!f%+ZzV y8'GP]Ӈ괧J,S+ӥ45lbka (_W2 lfvvme,zp2a>ş/߉Nmmq+OғppK\@q2}r"]RuvېpHx0E ^ox 1|k^׾ݯ+8"Ny6X'`0,`½m5}kOqlQ<[c!fmU[ƽou\b)nL"HN&;PL*[XUe CAf .&fn /<ap sEtQgZ@YjL!>І@gpZШ-g |3.h1gӠ5GA;w,vHW<էgMZָε3,p ghIW+b0!v X@h l`.U Q0Ԑ-縉` idCT+H 8@hsG(PPhP5Q~P l2Q0G艾 !xҘ8,~8' 8;ЈP*qaPg`׎AnV`Ɛ΀~5 Fp8%ȊP@ƐӐAX.`p)XW( $Q&Y& tF @e0V0P-08ƀC8IXANYy 0T[2G!,an{ @*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,V,.Ƙ"2H"6!:!>Y!B !F& J6i`,8)%PNi%U^Yn~]~)f}ai&|ezip)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'<,G/WV4XF0p=q7N;C?#6SP2??yHD)GB ĂcyE:W`"&L! G 0 gҰ71T:b(fGtD HLxDB  &ZMtNqLшX ы(b11d1mlЇ/ qxGQjH9t A:dlMi#~xF>>ٓNzaԤNth; + "OBM?pa<760IbL2f:Ќ4IjZ̦6nz 8IrL:v~ @JЂMІ:D'PRtC3ъF(H= !&e(RQ iGeQ^Ԧ)F]:#cN!:Ԑ4EuhRO4O~JժZVխj^*XՈUf=kY!ֵ>XSҨԮ7kNSt*\QTհF%l] UJUla{Wղ{l_!X>U(^(jHjSXoX:[6jnۯ,5ժ -u`AXVֹnfYFV=ȰjqYnC.ļ AoBԋĽiǐH q\#uqk\/+nD|3Xy0C' [D2a o01,{8"&&1S|byq+G87αc@Ʊ,"xGNrd%:f'['.`q} 2b6yV| 2gֺg{g>7WttNo׻6o~;޺[7qH6AD$(OW0UN Z· o[".]rNHO]i@}v/XuLP-l!w=(+0QT?M , ˽'EI&"w_ pP?>A"~tЂ-B? ϝ@pA@?\z81#<_y\?+,<-Sp0 x1P\b% ]30 8-@@ W-~ f\`Z`f0G  \ s8)y(`N N "x(o@ `Ā 718y@ `Q%  4ȁ|@PXs0vayb _em kwom vp g}}'1Gnh"Jh󶇘xHH HH0o?Z8v WHHXx 8 o˜|o ؘ ! #{8 QqxC !2@H!! ƀŵ@54U.H %+ Q7 *ɓA GB?G)A% aRYVyXZ\ٕ^`b9dYfyhjlٖnpԖ.Dg Aqɖv`s|`@ph)A٘W$0e Qi⠙h%ihI!,a`H*\ȰÇ#JHŋ3jȱǏ CIɓ(SNCG˗0cʜI͛8sɳLa| JѣH*]ʴ̖sJիXjʵKa^ÊKٳhR۷pʝKbкx˷_l LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺ)uM۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)di Iى J΁Bh2h7i wiii6jꪬ꫰*+SjϬb@k:&N:&a*59k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o觯/o>{,JO>Am$QG6ѫË|?p<@HHRLh&BKd"?.J~ Uw;A$,ۀJ t[,Gi Lc 453QvcX=Xң˃nE:b{ L?_saQ4@%>&c}qĭe*t hgɍ 5bI1B @tۈx]o!O8OtủS'ť#ԑTlPGN#߸Q:8\-(SrqkZf#AnfwH}¦PE#}Bq ݄ @4,V:{uEqbG]( wd]a;>{¡ {o(: kC|sN=#O[ϼ7{GOқOWֻgOϽwOO;ЏO[Ͼkx""7?//O7~ 釀'<ȀGǁ H~(#DXFxMuHELȄNHRhTXDxXU>XhMbQfXhȅRȆOP>kHbKhSH_Gȇ`s}E ]<؈<Xx8؉ȉx6@ ؖpȊQz ! A a JXQ5AQ 0(x[ȌumHoHWs  ΀ Bx852Ȏ/hA=Pxh؏9I ِ < Iɑ 9"YI'+&Iɒ2,6y7:ّٓ@A0YCyB;9-ٔL6y<( ghHYɕ(#'!8eɖWi[)vyxz|ٗ~9IJ.4!I *) *z3 @ @0  fGF@!.[` AY+IATLW:1d:  N3 Lp ` \:ʣ@JDJF:XXQ k* Sa]:Qe*3 p  PD ` GҘPz[;Gf P";* q )vy3 P,1*Ze J ]&N 2\ G@`o` 0\kCPگ* af B`Iqڛi!1 5!:& P Ȁ `@5+K ot00[:@0  NTJ  p 0ck6o1ڲȰ\U8 :y `Q (\K0 sj  Ԡ(qk:*Ѥ @I0 PK)ј 'Gz;+ o 2C аț! )@@K@аʽޫ v[B[Kkpƛ"1[̍[Ki [껽*t@l i{% *\,΀l(\1-\K<>E|ًI L\;,'|)<|[K)a8j<l'AtlvNs$~ ǹǃm|Ȃȍ\n ȋLaɓɜə,ɢ<ʻxɥ%IRFq%qF{F5˴x@x+@Bp̮ Lq]|zgeZBUPFx)l{ٌ[<ȲH\{'dZ< n+m+6,Ȍrf{\j@CE`l k^˻лѶQ \nuqqj w^P!Sҵp\U7]hG<ҳR( Dm+FrSӲPA`DM_5`ClЗe[U#go:!,` H*\ȰÇ#JHŋ3jȱǏ CIɓmCɲ˗0cʜI͛8s4Ylϟ@ JѣH"KʴӧPJJ0aUjʵׯ`wb KٳhӪs۷pʝKغx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tiH0ΞTy矄6(Q `: ( OMiz":ihZ[?ʪ2 iO]:ʫ+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.vTH<4 LA@y`^<\@皇.x\6؄N2ɬN8.{@498L;z/^3|x7oBx87 EV8dt?TӚ% )K;юXSJj"|΍)KCWi 'v _^Gi )́t 7 yH&!Sa:zԃ`Zmtٹ6!֮v߶yj@x;rW1&\v3Hd2>lzPj,e(8έpnjWֺV=iaZuv-ͭnaۂVOPASⶶ% )*\J,ƍ.o+ )Hll,ҕ.u=Qmx;ꖗR@vIօo>|m{ U}Q[&3{ƩxJ[ΰ7{ GL(NW0gL8αw@L"HN&;PL*[Xβ.{˚e1yf>Ӭ2?k~39ӹ!v3:ptfBdF4hj|V4%g?ۙ^iFYzZ`GMR !N!jUզ~5I-Y!5s}Q"ZسF6jfz׼vvVlhZڱln/qkj[Fwmuv}myz%K8q j. 9!p3z8됉Jxã9.F"j{'vʿp|ܾ>=u@b򘷻}^o^Hω-sqAW\iKӍzgŞh hOm.wӽp!~{CvB @A(񄀼SD7w<=Oz{>OD#?{ȋ=ao=Vu_וve~uw]F~_io|?OOхuaW8Qs ؀ (xՁ "8Xx )(+h. ` A5 :<؃ 0 QFH 4 LN Lp @ Z(Y tv-(,, Q ooV?P,,w Q0p:FXRǢ w yI@ )pH8*Њ؊auH 0  os(8C(Wh ʸ(ɢ ЉԐ `e( G @hRH 1Ƣ ؉ose85B)vtx؎@ p HAfRGḏg@׀ ` u)H fZZ`o o/w(  Ű @ `:Ȓ%8(`Q N`@ 86sHy hUkp3 &`L wyW@ Lx@ pZf Vg I h(P əj `oz Y @ &wɚBɖ  u-voTYϱ9qivp o ɑIy t5ʑ Jo$Px@Jt0 !I a p,20:6:<ڣ>@B:DZFz|HLڤNgt 3!,` H*\ȰÇ#JHŋ3jȱǏ CIɓ\ɲ˗0cʜI͛8sɳϟ@ Jя1R;ʴӧPJJhXjʵׯ43Kٳh۷pʝKUx˷b~ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tiY?|Q٧T? jE!2ۣ#inBg\)Qb|**Ol:4]c)j"<+yk+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.'2$i7 ^8`#4`L᎟ C 6d8\ONOlzxP)<p4W{؄;sċ:Ń57/Wogw/o觯/o H!0;v00\@& 4<~xЃ=>R6я ve \RAE!?Z?c‹61qHBă0qIl >ip%>&NЅFaI8Sٰ n2:-q22`H:?xxǽN $%§~ct*TէC8^| mfCjUJ5V}j"/9if;h wsj38dwi' ]l1t9,.,V heLCD=7QsX:Уkض"W V9H]0%>%m<+\KۤO@mQ]&ͮvK25 t˘:Ո'Z~e/y bޅg~+>8`yI ʁ'WmTd, i G|_  ~g_Ȍ(>JdV 5PL*[Xβ.{`L2hN6pL:xγ>πMBЈNF;ѐ'M醔Ҙt N_Ӟ4CQ9uSjT;ծ49YZְ6u}j^ס-jVsыifճv6k,حn{NvCMnoC>7íu6=ow{UosfOpr gqpW|1q{w܆>rg\'gxr-xI'v+">΁.=?:҇'NgC.Pz5j.9q$GǍr\,' pf.aw;rev¯]mWA g88atrmhǶ|1kE_Oջc/{Ӿm!}Cz B @|B(S'Dwط>O~{?OD??~WkFzfφky hmȀ'GG(H8s(*,؂.0284X3qk68(Ã-@p@HBHE(G=8LH y HuWV(`f0ݶ[hЅ P Uqg( phr؅x( ٰчg F$ `& e  hjXw  ,,([b@ ,Q, \0 `,  a  @<`Cp)8]` ؇ u\ `@Xh(hȂ fn`B )؏x@ `p)p?; )v,ْAPrx `onXA(@ǘ @`5$i(`QQ` ˠUY `0p F@k){ɖ} jh aN D);U Z 0 (b]y 1 s@ ` l  : dF?ЗZd 1 I)gƹf%霢)m91y 1ўٜyy!&!YjY  ZZz  ":$Z&z(*,ڢ.zE3ڠ57 :@ "Ijɤ ETZVzXZ\ڥ^`b:dZ]r[&+% S," f(q 0%y}"5(R y<} ֨[" SpZJ dU!,a`H*\ȰÇ#JHŋ3jȱǏ CIɓ(SZK˗0cʜI͛8sɳLa> JѣH*]t&PJJիX4@`KٳhӪu8)@kʝKݻx'+߿ L- +^̸x{ ˘3k̹ϠCMӨS^ͺװc˞M۸sFK Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)jBى J)9 "rgibiNi:7j&wjjjv#3O?嚫@XGm8Ȏ3P`kk@Ð&@>;965tmu?Dxn\. IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:v~ @`?:(`AmcP*(1") @&J#w264 cXiAA=Ƥ(LЎ4n! $~'G-uC*t%Ԋ|⪟@Vzjjj!7*U4֮ ^uU!~&PTi*L.#Hl!ntIQec` YPF1v0O3҅h`h4xԲCf;+zw"Dʔ#0;ֱWna\"xu@t5\c&x7Ӆ!*z2/~߻ڪ45y]L<1x4E]!ߒ$&mp6$7l$_HdG>!!yQT4bǮdLa )H^摲lѰ)qȰ6bxƃcJp|0]m#~r?h|4w"UGe'l,\Fœ+9ʗ *F$,T.]:>XaTlV"{wF刡b6~ e$g&~(H%v45x7hVfҰ5y }HL؄NPR8TXVxXZ\؅^`b8dXfxhjl؆npr8tXvxxz|؇~8Xx؈8x؉艠8xȊ芤xM1X (7(MHhȌ(HhȉxMЍ Qx긎؎؍紌X؉x، I9Yy ɏ ɐ y)Ii!% N'p 006i8<ٓ C;xp Ԁ 19')WIYiU[ɑ# iFDTkm:Vٕ" )&i(Ye[ א\P9P yI)J qYNЙ)陣Yy ɚY qM 9I` ٜ9)ɜI)y!YYyiٞi㹝Y9YMoyiwqɖ ^X9MJ :9ʟ * J ":$Z&z(*,آq0v0* h<3j57;ڣ? aH=G:A:J LjO Ib GM Kb< 8D^jWA7ȐpDP?T |ڧ|# oZ8qPD0<{} j)a@ ?0y:m j `40zLt  p0 D꩞ʪH @ 0-=G᪰*: @ Nњ *`05@˚4E*6Ҁ 0*ʭ* ِ 00Ы1fꮂZ8 p Ű  骣 +Pjq +- /۰2"{,4EJT  ° 0P8K{c#۲)K89;Wk6[7\D=ۍ 0 `>b  [@ `P 0#˯C1?+` -K = PJ` NpFб˻>Ề (0 @:A ڠ 1P`1;kk)KB05c" p P50J+ ŀ 6 -`Ɋo:b:JBB)ܻ+<Ƞ 0<Bp; j<t K8ݸIjÛP5[h4*3Q`'@)zڽj:I vlo;{|"Ɓlȇ<q|,,8ǖɛ\ O)1Ơl"ʩ 'ȴl\o$mlƾ anj,\<ͺl!Q<ی L| a΍al"xaW:+bm+|U8+Pj бE6 ϴE4ZM"K6+Pm!L[|EtH%xlybLt|U=p-+EG<->d@aFK#agjO}R4E]zcZx) 'j==r=t]v}xz|~׀؂=؄]؆}؈؊،؎ْؐ=ٔ]ٖ}ٜ٘ٚٞ٠ڢ=ڤ]ڦ}ڨڪڬڮڰ۲=۴]۶}۸ۺۼ۾=]}ȝʽ=]}؝ڽ=]}=]}(P|_QOZPP a!@ = ! Q=  ^ t @} /| `@v0-*A^OQS UW{}Xu!,anru @*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,Ȗ8.Ƙ"2H"6!:!>Y!B !F& J6i 88)%PNi%U^Yn~]~)f}ai&|ezif4?CМsO>a Q:uYinqY睂'A㐝6mi?S5CP6An*l. `*EȣΜĶV:JP2z)k-j"DpFr 0(4B!^h>n")Bɜ[P Y -S!kf$ĝodO<smSP2c\^R7'D1ΔYiL3J UݐÌ}،e jAFt8)vm6b-`> sA$)#> 8W.XN2юC.r[n騧ꬷ.n/o'7G/Wog7xJo觯WG:/EzwtM HL:'H Z̠7z GH(L W0 gH8̡w@ H"HL&:PH*ZX̢.z` H2hL6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8Ir>:v΃jv9y̧>s:ʰOt h1 -(suG=2GsZWH:]"I(9NKWWĦN+a@P*Ԣ!RcԦ*kN%RE5gV1&W` XJVZ(Xհp5Ysmj\:ֹ5ve+0V~JuCy(V{*4PְU,fs#khYq Ɉ;|9vJp66 (?﷼G. frtKZͮvz xKMz|Kͯ~LN;'L [ΰr{ >.DBLbK%>1C.0c3\9 s#g6P>8MB:~9 -E/ю>4#]ISzЖt)KW:"f4C胐9#jGTCjEZҳ OCD~) gp-TKge;]{$dSԺ6mg;"Δ=F7A`?wCz/ wBhTJ@FԤ\kf8!Q#5 drt"r;tFyUf"%O&fNi>9Ѓ]F?z!?3 2TT'յ>{T;>uB0{Nvc]pGNSuyR3L*;V<1ycZ9cs^F=v}MϽw|'ChGo}`ٷ|??.k`|Oal??<=@=~HȐ %׀ F0+BH #%lSG`FPBP~ 0 g-05@'8b-/H0F9KуP pDh~F "xA93AQЁ J( Ұ P@8`? ; 6ؐ h @0<`obHxrB ~0#DP<. 7S0 x JX 00B5P0gQ ȋ`XH؋؋Ȍ ،86D٠ X ɰ ـ ~p#-PAQ (xHHǘ0 ڠ `  `#3H vٰ 8 0 0p-Bp 3vp8A8$i H f UP##@@)$ɐP p)2RIUWH˰2 ngYilY0` IC`1!H!82U )u0@ iB`)0陟y)++. 5920ؠ0@@X|%.PP #yQ)ؙ/ABОa~Iy~)CМ Hp {F q ZRX4ZZ '؟.R8.3 +Ň7J=j#Ǣ;J9C8LFOZ:6ɣVFW ZDabZfzhjlڦnpr:tZvzxz|ڧ~:Zzڨ:Zzک:Zzڪ:Zzګ:ZzȚʺڬ:Zzؚںڭ:Zz蚮꺮ڮ:Zzگ;[{ ۰K=)!p Q *Q52C`  ; 0; jp0Z  69۱ <[CE𳻰/ 9<+D+s0Ⴉv@';@ gXkh۶g+loqkvpE!,H*\ȰÇ#JHŋ3jȱǏ ClO_F\ɲ˗0cʜI͛8sgΟ@ JѣH*ոoʔKJJիXjM֯`ÊKY^Ϫ]˶۷pKݻx˷߿ LÈ+^̸0L˘3k̹ϠCMRN}װc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv (C(,0O4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+ A 3Dsq>#8$M6$O<43πNn 8]3c20L6`5qPqلe8-,8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7 Aw1F_|Ѻ};N~}\#%^_iB5}oGZ,7 IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ v^! .!"La<&2ye!d&0̆Pl̠yf:ӛ'2YLrs4g/"#f茦:o3g>˹sr3ok@JvBAЅ2}B"QRuF3ڐ4hEE*Q>Ԥ EiBUЋnGѐ0Fmӗt?-iPO RCMiRWԖOjT:UoGM`CJV6aZֵn%X*׳լ +Thň0@M2Ӝ԰eHaS괦F,RZO @`X:UPTEKUZմEgY}O5⸆>ms mOy斞gp9\}MMr+0u.t+.֍C{rW.x"^ƍAob%D|ڷE ׿}Nz%^Gv/|',_ w+a 6giMxX'mb5F}c3;n;dYWM&;PL*[XF,,扈yhN0zq@!.?s 7'wLρv  !ܠu D<`@!t.!n30,RtuZj Y@.൤}^ lPbh܂M2v@lYC~j@ p҅MhxYCX?]ST/ pv}|4tD8x (?@pS;7k^@ QzB HT/g>[LyXX煢ַu!tx|a3',dcf@ ZЂy*Ixvb @U;6Mf&`+@)BWor^*W@<JFe!{}Tbo3)H-j>Q`XA (BB `}GA .@4z 0$A< j`}52ghi`u-hDPϠ xe{ nD@ahcJf=聇&=0JKų0G0&|F9G!wvkkVfkFv<Cxcag;Wf`քNXQHgSxfVORȅW`86dbhjXZXkXQVrXWxz|؇~8Xx؈8Xx؉8XxdW 1_TW8>aȋ 3('_'0̘/{‍H28E؍ sq!,qH*\ȰÇ#JHŋ3jȱǏ C8.F\ɲ˗0cʜI͛8s,2Ο@ JѣH*ȳϥPJJիXlSׯ`ÊK,Q(ͪ]˶۷p=ݻxݻդS L0ιO +^̸c"K˘3k̹ϠCMӨS^iZְc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv X^4"hEޜi0`N475h? DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+֫U)A2~"S5O8*O  8a7'8 8Pl1<,4\M6 | ) N60\#輳=w -4@3u*4s:3pJMXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.IL騧>O>c۟He * 8я O0]&z `2 KWBp,% |! )(C ְ}q]CFSE< qE  >Q4_RxL-Wb4pb[8F,!|[I⨤7m:N)y IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0IbL2f>Ќ4i3Pф6mRޔ&8 qӜD7GNq>|'<ِyғ!T6My“ O49Nӡh?9OIj QqG(H&ґN!&hCRz҅Ԣ)Ie:RԦũFuzQb!/M\PӒԨ7EjNSԩ?]iQ7TŪGj4fWQŚTgvu8pkZ!"׺v+^׽k_+X:mbšb=xc+tVkf˺٥ճmgEڧiG1*ˆQufeYֶŭiabLp35/?%zM UA\.Ժ Ezx񊷼.zKއW}o{"6] %<ؿM0_B4pp!`X+JG'&W!g,sqɭgEN>Du\!7]"Ouu]*A.{`L2hNf/pnag9o<!{3`@ Z/Ћ˛HX:/H.t<}j7BZP0*]븐x@i`a<  ЂмfU/dS̞tUՋ^W= G82!`ˠ$`-8)]H,b pC e@ țޚ҅@Y i$vtj {f(O9VRMpYx|(SvЇ.: iBІ: 0FzN6mBXɷ.l`/,!dlcePZ`*nRcHۥaQ@Eݓa C[#O5HQO!UXRPB=&}R&oL8R( =2Yj<?}?8 p0fp4 |`RHCϿ;60F ~w~p.PhҐ"qQ0`5ir. "$ V5P=Wr5Xf.PN?(?gEHkGL؄WRXTXXZ\؅^`b870!,an{ @*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,V,.Ƙ"2H"6!:!>Y!B !F& J6i`,8)%PNi%U^Yn~]~)f}ai&|ezip)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6ІNVk-Rs5% RAהCOά+/Ck,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wog&O|:#vlsEͻtnԃiG;@vÁ$<\cPA4A`2F8P4QW|uBX^0a "> "wH!Ȫ27IT:'4'(z` hş`QZT26e0-~oc(pj#?B‘:82CC6bHC"r'd$ ](zd"E0"AP'\e?<#,=<'R4_&-! cަa'R̦6nz 8IrL:v~ @JЂMBІ:D'JъZͨF7юz\HGJ F(DRҕ.}Hc*ӐҴ7iN_>)PSCQQT&'MTB&jUJRTD}N U*5uRMZ*Uiu+\*׹vkDz׽U+`"ض^U)YXejdر"jfVfY^VulhÚZɶ=k5;[~vG5kj[ ;U|En^kWչp.][ Ro նx6-ikY!E/jk欭FT6ľ B&ĿAl5ҡZhڕ'Pp-af0+ wHWbH0Vc Ә6#c;!xu H&+yL2!Le&+Vr._d0`L'y"nL)Ydlg9a4߬f zZ6t-[Gt@7vzo;NWVհgMZ@wmz׼[ ,bN48>i aP)67΍k̀q;F6"`@)]LІ3&ij@"Zх7n\DGqs/H20yy g%P?x3s"U^s'':mN[4?x^ˠla "H #t]52OdÜ3nXD$`v '8_ "0@ŽM2_ny0 @FU /aXXȎ8uRXx@(x ~d>~ iH َaMT9d4bBr13YxG:O蒊4?Y OI"iMFiOH cKH_ Ihy.0_@YkٖXDq#1?!,a`H*\ȰÇ#JTHmŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@NCѣH*]ʴӧMYPjʵׯ`ÊKٳhӪ]˶pʝK]˷߿ LÈ&ǐ#K&)m˘3kN| ϠCMӨS^ͺװc˞M.]sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihl[nirvzy~*&6:89*iuNjt^isn)r~*pjojn꫰*무j뭸뮼+k&6F+Vkfl+k覫+k,l' 7G,Wlgw ,$l(,0,Bsusszv"D NItӿ> TWmXg-,QZH׶ `Z06d0lͶHvmw$037ދI /@/.WnIw-.褗n騧ꬷ.n/o'7G/WogFJ91$s2ҍ@e],AbO=@\r@0] /6Z@(QF b/e)i 2\H3]$@07bhʈF-`"R`zC?:p_-!Tb-d '118 ЊI,H&cJh ;#%!N y ]H>>ɏ+mJ b4COWVr*F,c?Q㖸e*]rw f0Ab0iLdNd1;hBJ6nz 8IrL:v~ @JЂMBІ:D#ylcmPE/QUt>R&6ơԤB?@ҔT@48!v4@ 8SE͈5(#zԑm<XͪVծzu'5Mi?ү'+Y iek{ָu=tkDЅ4 h@1x wmN^ԇ;Avf&'g9[TBRUMQ0jZy0ƳY,Z ږka;׌Ԧ+2t+-a@t+RZr=]jEص|^f{B} ~c`#^/M[ f:6aq#C2{a g)@Pݢ&>RRa!( ,X  w$ٱ{씲 .dԦ rx^1hN6pLgC$  30A ?Qg$&`8s PCV @q)Kԧ omw] };Vx"x8@ۤ{/k Dw2zτOi Rz!*sW}|UJv5rYh?9y>"p ؟}«sY# I'2چ$Iq* [(`21~<" `qaqm GK0ewx1@ Y7se\P ViQ!u 1 <'VLqs Y.IP6aSzl 6   \R'<o wna,(~540R&uor߰T @U6V ҆j*5 iT 3T` !Uj qц ;Q\w e`m ڠNpwF! Gap{7`@p}f-}FZWAC[zц3PqG `| p!KzaW@ulX&.Q>>ad9Pkg p1F/dA (v !X@ me@Y@s(K @ (9(   qHm` PP /X$quqv:c]7hy*30sXVCT!5evP~h2Z{j| ٙ9Yy1 120Ye qIٛy99yI2YQy؉ ܙٜ)I)V(BGᚯ)Yɜ:ZўT 1'cM ~ MZt *(  3Ԣ.Zhn2 2:Q/J8ڢ:ԣ> ?jy+z3DJ7ڤ@QSUZWjѤB7&J?åYOJeiʣgkJoZ! 2b *J @~Z ڨŨڨ:qשΥzڥ *3K$YdڦR*PJVXZj ǧ z*DQ ꫷jhʫf*nJjlj yJƪLr  @>ʡJj*yIBp !1 ۰˰1[ !˱# 50@ Q15{q`;;=6BDi`HGI[CKIN[5+8k\@<`۵VfZ+LjYKT! Q0.[KRik}{˟!qkB0u;qv# I럡 +Kky G A[.;K{ۻ;[{țpk;0"|6ٰCA`;*; x0 u½'([w _)0 T!H0kӰ]w&+'1  &@ݢ4>۸)03)@c& ,!QDLe@p h*pW:&'.D4i> 0@ç&?<PS oaEmQ pk)P A!Ah<&Y<(K!Ɓ! '*+x%C;s3;pU-fq)S` /<a% +z%Xv)3pŚRk@9ȶ% *Շ7'D1fuZ D'Ga)&+Ћ! 01 #DItQ F 3ɟb N<c7q&@, G S [ɟFD `T H<@ 7<A3Mm !H9IP ~ /4pm@0HB K00k׾G$7i %40 -C3.@&= 5< ?}' T#QHrc!4U eru,F0p;wM4qm! 2v} C{ ]5PҀ ل` =ڸ@ @ ڪ-ڥڬڤڣ=ۭڥ]۴کڶ`3]ۼ=MmM1ؑM ~-cك;Pڽ== A 2}ܰ ѭ5K׭(vW؝ -ð pj-0 V*ؼb<ײ@  0-` 0 > % L+-/> &"w:^?{ @ r`xP ZN> 0 `Rfp _>ׯdh [O fZ v {QqG{] P  0 @ İ.>~N0JSP p#0P @o Whˮ){)FQ &|.p?00P'4MWBCP.0= 3)9ȱIa p727m- 3C>C. 1˒N`4P>/u A?Zed\.u9)[O=S{4`po?JtOz_o"=?Ȓ˰eQ/"Mo! @ A_ "y-(˼ l.F" 3Hum?KY70 Y  0V߄􇗙OQQs>EtJG1nc0ɯG0 RO=TpBTq1 qQp-Qߞ}@@ Яd3Qā "HQF=~RH%MDRJ-]SL5mĉR^mչ4bPqDTRM>UTU*a2vPqeGW\uśWoanW 7\ťu+ܵz-_ƜYfΝ=`|5Xmς[lڵm 1;<F\r+<7vOO-R]x͟XYT8O@~Ϗ߾}0@ g~!X+-;B /P2lB0DG$.+6 _1FU*6$ 3J; v1I% ZF"Ԭ /K/ /eCCxXg ΄3N97319Ơn2xy4 YqOEey=+xJ~xSO?5TQG%TSOE5UUWeUW_5VYgV[o5W]wW_6Xa%XcE6YeeYg6ZiZk6[m[o7\q%\sE7]ue]w߅$⥷^^^}'7`K ӁF8af)9j9a'VW~J'Pc+jc!?&dyd_6/ʃDfo-,q~f&ڮw.:iby㥟:jj:kk;l&lF;mfm߆;n离n;oox'xG>ygy矇>z駧z>{{?|'|G?}g}߇?~秿~?`8@ЀD`@6Ё`%8A VЂ`5AvaE8BЄ'Da UBЅ/a e8CІ7auCЇ?b8D"шGDbD&6щOb8E*VъWbE.vы_c8F %fe@"n# lHCA:B >eY5q ;Q˹n(Cȓf΀$ 5HlTҒ8:mG`%4"IgÓװd&HyHÏ'(Õwģ*oO$t5BHi7 FeLC%FAhEIfM1 b,#JD$EL(2͗S* "FhDfŤ; p"쬉 Pk$ ؈,bCX'5%Q3NKnN B"ҍ"'C% PF-c `d@FYR5$UE rvO{H0: +J%:tCKQASt ĨiNnj e zC ¤,Ig M? 8A$ )\5I3e(%@F`#f@/R hF40)Dv5V_Oy@@5/aSA@P0!"#*pbn'Dy-HYB Ɍ@`t; B:f` oGh1[*]0Go׎"h, BZvE,]킕 F/_"E-f1Y|Z1Gzc쐉ud&7OrL9E?죏@0N6z`#>*(f:^hgA.)'6h#@bJׅM:?2Z8 j&6F+Vkfmfu*&[ڹnh*Zfo{)H4"Eb]4 v v bz!򢅾 qQ!"bAw [B53+sG9i3d~ HҐ#=$- E)‘$WVFiӤ@8]0JSBQl%+%J%,lf ,'9́ s6Ii~ 3h&rssY&v289p3+0'=yOpz{g?ٽ,BІ:D'JъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHEX\Բ|U#Y{ZՆ=jWhMZֶiECa HC 6xG;@cXG;2 ImB886`"&Aܝ<jYL8αw@怄