Repository: pymeasure/pymeasure

Issue:
Error trying to follow the contributing guide for PyMeasure
Following the [instructions](https://pymeasure.readthedocs.io/en/latest/dev/contribute.html), I went into the repository folder, and ran
`pip install -e .`
But got the following message
```
ERROR: File "setup.py" not found. Directory cannot be installed in editable mode
(A "pyproject.toml" file was found, but editable mode currently requires a setup.py based build.)
```

Any help?

Hints from issue comments:
@BenediktBurger: What pip version do you use? probably an old one.
Try `pip install pip --upgrade`@OrionSmedley: Tried that. 


Now says:
```
(base) C:\Users\Spin Doctor\Documents\Github\pymeasure>pip install -e .
Obtaining file:///C:/Users/Spin%20Doctor/Documents/Github/pymeasure
  Installing build dependencies ... done
  Checking if build backend supports build_editable ... done
  Getting requirements to build editable ... error
  error: subprocess-exited-with-error

  × Getting requirements to build editable did not run successfully.
  │ exit code: 1
  ╰─> [57 lines of output]
      WARNING setuptools_scm.run_cmd command git missing: [WinError 2] The system cannot find the file specified
      ERROR setuptools_scm._get_version_impl command git not found while parsing the scm, using fallbacks
      Traceback (most recent call last):
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools_scm\_get_version_impl.py", line 40, in parse_scm_version
          return _entrypoints.version_from_entrypoint(
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools_scm\_entrypoints.py", line 57, in version_from_entrypoint
          maybe_version: version.ScmVersion | None = fn(root, config=config)
                                                     ^^^^^^^^^^^^^^^^^^^^^^^
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools_scm\git.py", line 212, in parse
          _require_command("git")
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools_scm\_run_cmd.py", line 214, in require_command
          raise CommandNotFoundError(name)
      setuptools_scm._run_cmd.CommandNotFoundError: git
      Traceback (most recent call last):
        File "C:\Users\Spin Doctor\miniconda3\Lib\site-packages\pip\_vendor\pyproject_hooks\_in_process\_in_process.py", line 353, in <module>
          main()
        File "C:\Users\Spin Doctor\miniconda3\Lib\site-packages\pip\_vendor\pyproject_hooks\_in_process\_in_process.py", line 335, in main
          json_out['return_val'] = hook(**hook_input['kwargs'])
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "C:\Users\Spin Doctor\miniconda3\Lib\site-packages\pip\_vendor\pyproject_hooks\_in_process\_in_process.py", line 132, in get_requires_for_build_editable
          return hook(config_settings)
                 ^^^^^^^^^^^^^^^^^^^^^
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools\build_meta.py", line 464, in get_requires_for_build_editable
          return self.get_requires_for_build_wheel(config_settings)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools\build_meta.py", line 332, in get_requires_for_build_wheel
          return self._get_build_requires(config_settings, requirements=[])
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools\build_meta.py", line 302, in _get_build_requires
          self.run_setup()
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools\build_meta.py", line 318, in run_setup
          exec(code, locals())
        File "<string>", line 1, in <module>
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools\__init__.py", line 117, in setup
          return distutils.core.setup(**attrs)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools\_distutils\core.py", line 145, in setup
          _setup_distribution = dist = klass(attrs)
                                       ^^^^^^^^^^^^
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools\dist.py", line 283, in __init__
          _Distribution.__init__(self, dist_attrs)
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools\_distutils\dist.py", line 279, in __init__
          self.finalize_options()
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools\dist.py", line 635, in finalize_options
          ep(self)
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools_scm\_integration\setuptools.py", line 123, in infer_version
          _assign_version(dist, config)
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools_scm\_integration\setuptools.py", line 58, in _assign_version
          _version_missing(config)
        File "C:\Users\Spin Doctor\AppData\Local\Temp\pip-build-env-2_13zq0h\overlay\Lib\site-packages\setuptools_scm\_get_version_impl.py", line 117, in _version_missing
          raise LookupError(
      LookupError: setuptools-scm was unable to detect version for C:\Users\Spin Doctor\Documents\Github\pymeasure.

      Make sure you're either building from a fully intact git repository or PyPI tarballs. Most other sources (such as GitHub's tarballs, a git checkout without the .git folder) don't contain the necessary metadata and will not work.

      For example, if you're using pip, instead of https://github.com/user/proj/archive/master.zip use git+https://github.com/user/proj.git#egg=proj
      [end of output]

  note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error

× Getting requirements to build editable did not run successfully.
│ exit code: 1
╰─> See above for output.

note: This error originates from a subprocess, and is likely not a problem with pip.
```

@BenediktBurger: Did you download the Repository files or did you clone it into your own local Repository?

The error message indicates the former one, while you have to clone it. @OrionSmedley: I cloned it with github desktop.

On Wed, Oct 9, 2024 at 6:06 AM Benedikt Burger ***@***.***>
wrote:

> Did you download the Repository files or did you clone it into your own
> local Repository?
>
> The error message indicates the former one, while you have to clone it.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/pymeasure/pymeasure/issues/1170#issuecomment-2401892650>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AOVCC2ZW4B5OUROWUXTEY4LZ2T54FAVCNFSM6AAAAABPQYS7J2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDIMBRHA4TENRVGA>
> .
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>
@OrionSmedley: What branch should I be in? @BenediktBurger 
`pip install -e .` fails in both main and develop.

Tried on another computer. and got the same problem.



Following the directions, I forked the main repository,
https://github.com/OrionSmedley/pymeasure
then cloned it with GitHub desktop.


Do you see any issues with this? Maybe the version that I forked is funky?
@OrionSmedley: ok, I can get the old branch, instruments-features, to work with Python 3.9.12@OrionSmedley: My version numbers (that don't work in main or develompent branches)
Python: 3.12.7
Conda: 24.7.1
Pip: 24.2
@OrionSmedley: Aha, found out the secret. Maybe we should add this to the documentation.

pip cannot install in editable mode, if you just use github desktop.
You need an accessible version of git that pip can call.

GitHub Desktop does install git, but it's burried, and not on the path. 
The following fixes the problem
`set PATH="C:\Users\<USERNAME>\AppData\Local\GitHubDesktop\app-<#######>\resources\app\git\cmd";%PATH%`
Then `pip install -e .` works like a charm

@BenediktBurger: Thanks for telling.

Solution patch:
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 1e2fa3323b..cc6daad3df 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -86,3 +86,4 @@ Konrad Gralher
 David Ziliak
 David Sun
 Kévin Petit
+Orion Smedley
diff --git a/docs/dev/contribute.rst b/docs/dev/contribute.rst
index 3ac07e6ee5..7e88ab9263 100644
--- a/docs/dev/contribute.rst
+++ b/docs/dev/contribute.rst
@@ -13,6 +13,9 @@ New features are added to the development version of PyMeasure, hosted on `GitHu
 .. _Git version control: https://git-scm.com/
 .. _GitHub Desktop: https://git-scm.com/downloads
 
+Make sure :code:`git --version` works from comand line. If you are using Github Desktop, you may need to add Git to your path in each terminal window.
+On windows anaconda prompt it would be :code:`set PATH="C:\Users\<USERNAME>\AppData\Local\GitHubDesktop\app-<#######>\resources\app\git\cmd";%PATH%`
+
 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/
diff --git a/pymeasure/instruments/agilent/agilentE5062A.py b/pymeasure/instruments/agilent/agilentE5062A.py
index 8f6dd76553..082b2b5dce 100644
--- a/pymeasure/instruments/agilent/agilentE5062A.py
+++ b/pymeasure/instruments/agilent/agilentE5062A.py
@@ -26,6 +26,7 @@
 from pymeasure.instruments.validators import strict_range, strict_discrete_range, \
     strict_discrete_set
 
+from pyvisa.errors import VisaIOError
 import numpy as np
 
 import functools
@@ -73,22 +74,22 @@ class VNAChannel(Channel):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self._add_trace(override_id=1)                # add just a single trace to init it all
-        self._update_trace_count(self.active_traces)  # add remaining traces if necessary
-        self._update_power_values(self.attenuation)   # update dynamic limits of power
+        self._add_trace(override_id=1)                 # add just a single trace to init it all
+        self._update_trace_count(self.visible_traces)  # add remaining traces if necessary
+        self._update_power_values(self.attenuation)    # update dynamic limits of power
 
     def _add_trace(self, override_id=None):
         """Internal method that adds a trace to the traces list"""
         tr_id = len(self.traces) + 1 if override_id is None else override_id
         self.add_child(VNATrace, tr_id, 'traces', 'tr_')
 
-    def _update_trace_count(self, active_traces):
+    def _update_trace_count(self, visible_traces):
         """Internal method that updates the trace list based on the number
-        of active traces.
+        of visible traces.
         """
-        while len(self.traces) > active_traces:
+        while len(self.traces) > visible_traces:
             self.remove_child(self.traces[max(self.traces.keys())])
-        while len(self.traces) < active_traces:
+        while len(self.traces) < visible_traces:
             self._add_trace()
 
     def _update_power_values(self, attenuation):
@@ -200,12 +201,12 @@ def activate(self):
         self.write("DISP:WIND{ch}:ACT")
 
     @property
-    def active_traces(self):
-        """Control the number of traces active (visible) in the channel (int)."""
+    def visible_traces(self):
+        """Control the number of traces visible in the channel (int)."""
         return int(self.ask("CALC{ch}:PARameter:COUNt?"))
 
-    @active_traces.setter
-    def active_traces(self, value):
+    @visible_traces.setter
+    def visible_traces(self, value):
         value = int(float(value))
         value = strict_range(value, [1, 4])
         self.write("CALC{ch}:PARameter:COUNt %d" % value)
@@ -367,6 +368,19 @@ def data(self):
 
         Frequency data is accessed via the ``frequencies`` property.
 
+        .. code-block:: python
+
+            # build an s-parameter matrix by measuring the trace data,
+            # where s_matrix[i] = [[S11, S12], [S21, S22]] @ freqs[i]
+            freqs = ch.frequencies
+            s_matrix = np.empty((freqs.size, 4), dtype=np.complex64)
+            for i, tr in enumerate(ch.traces.values()):
+                tr.activate()
+                ch.trace_format = 'POL'
+                re, im = ch.data
+                s_matrix[:, i] = re + 1j * im
+            s_matrix = s_matrix.reshape(-1, 2, 2)  # ready for use with e.g. skrf
+
         """
         self.write(f'CALC{self.id}:DATA:FDAT?')
         numeric_data = self._read_binary_data()
@@ -407,12 +421,15 @@ class AgilentE5062A(SCPIMixin, Instrument):
 
     This VNA has 4 separate channels, each of which has its own sweep. The
     channels are stored in the ``channels`` dictionary. Channels can be enabled
-    even if not displayed. Reading out data happens at the channel level.
+    even if not displayed. Many trace-specific operations, including reading
+    out data, happen at the channel level, but pertain to only the *active
+    trace*.
 
-    Each channel can display up to 4 traces (controlled via the ``active_traces``
-    parameter). Note that each channel also has a ``display_layout`` property
-    that controls the layout of the traces in the channel. The traces are
-    accessed via the ``traces`` dictionary (i.e. ``vna.channels[1].traces[1]``).
+    Each channel can display up to 4 traces (controlled via the
+    ``visible_traces`` parameter). Each channel also has a ``display_layout``
+    property that controls the layout of the traces in the channel. The traces
+    are accessed via the ``traces`` dictionary
+    (i.e. ``vna.channels[1].traces[1]``).
 
     The VNA supports multiple transfer formats. This API only supports the IEEE
     64-bit floating point format. The VNA is configured for this format during
@@ -422,11 +439,35 @@ class AgilentE5062A(SCPIMixin, Instrument):
     This class implements only a subset of the total E5062A functionality. The
     more significant missing functionality includes (TODO):
 
-    - editing the scale of the graphs
+    - editing the scale of the display
     - markers
     - calibration
     - power sweeps
 
+    .. code-block:: python
+
+        # connect to the VNA and make a measurement consisting of 10 averages
+        vna = AgilentE5062A("TCPIP::192.168.2.233::INSTR")
+        ch = vna.channels[1]            # use channel 1
+        ch.visible_traces = 4            # use all 4 traces
+        for i, (tr, parameter) in enumerate(zip(
+                ch.traces.values(),
+                ['S11', 'S12', 'S21', 'S22'])):
+            tr.parameter = parameter
+
+        ch.averages = 10                # use 10x averaging
+        ch.averaging_enabled = True     # turn averaging on
+        ch.trigger_continuous = False   # turn off continuous triggering
+        vna.trigger_source = 'BUS'      # require remote trigger
+        vna.abort()                     # stop the current sweep
+        ch.restart_averaging()          # clear current averaging
+        for _ in range(ch.averages):
+            ch.trigger_initiate()       # arm channel 1 for single sweep
+            vna.trigger_single()        # send a trigger
+            vna.wait_for_complete()     # wait until the sweep is complete
+
+        # see `agilentE5062A.data` for an example of saving the measurement
+
     """
 
     ch_1 = Instrument.ChannelCreator(VNAChannel, 1)
@@ -476,7 +517,7 @@ def __init__(
         Note that this splits windows for multiple *channels*, not
         traces. Basic S-param measurements most likely want to use only a
         single channel, but with multuple *traces* instead. See
-        e.g. ``AgilentE5062A.channels[1].active_traces``
+        e.g. ``AgilentE5062A.channels[1].visible_traces``
         """,
         validator=strict_discrete_set,
         values=DISPLAY_LAYOUT_OPTIONS
@@ -503,16 +544,15 @@ def abort(self):
         "TRIGger:SOURce?", "TRIGger:SOURce %s",
         """Control the trigger source (str). From the documentation:
 
-        INT: Uses the internal trigger to generate continuous triggers
-        automatically (default).
-
-        EXT: Generates a trigger when the trigger signal is inputted
-        externally via the Ext Trig connector or the handler interface.
+        - INT: Uses the internal trigger to generate continuous triggers
+          automatically (default).
+        - EXT: Generates a trigger when the trigger signal is inputted
+          externally via the Ext Trig connector or the handler interface.
+        - MAN: Generates a trigger when the key operation of [Trigger] -
+          Trigger is executed from the front panel.
+        - BUS: Generates a trigger when the ``*TRG`` command is executed.
 
-        MAN: Generates a trigger when the key operation of [Trigger] -
-        Trigger is executed from the front panel.
-
-        BUS: Generates a trigger when the ``*TRG`` command is executed.""",
+        """,
         validator=strict_discrete_set,
         values=[
             'INT',
@@ -521,28 +561,49 @@ def abort(self):
             'BUS'])
 
     def trigger_bus(self):
-        """If the trigger source if BUS and the VNA is waiting for a trigger,
-        generates a trigger"""
+        """If the trigger source is BUS and the VNA is waiting for a trigger
+        (the trigger has been initialized), generate a trigger.
+
+        """
         self.write('*TRG')
 
     def trigger(self):
-        """Regardless of the trigger setting, generates a trigger (if the
-        trigger is in the trigger wait state)"""
+        """Regardless of the trigger setting, generate a trigger (if the
+        trigger has been initialized, i.e. is in the trigger wait state)
+
+        """
         self.write('TRIGger')
 
     def trigger_single(self):
         """like trigger(), but the `complete` SCPI property (synchronization
-        bit) waits for the sweep to be complete, which is useful to wait for a
-        sweep to be complete.
+        bit) waits for the sweep to be complete (see
+        `agilentE5062A.wait_for_complete()`).
+
         """
         self.write('TRIGger:SINGle')
 
+    def wait_for_complete(self, attempt=1, max_attempts=20):
+        """Wait a potentially long time for the synchronization bit.  This is
+        useful in conjunction with `agilentE5062A.trigger_single()` to wait for
+        a single sweep to complete.
+
+        """
+        try:
+            self.complete
+        except VisaIOError:
+            if attempt == max_attempts:
+                raise
+            else:
+                self.wait_for_complete(
+                    attempt=attempt+1,
+                    max_attempts=max_attempts)
+
     def pop_err(self):
 
         """Pop an error off the error queue. Returns a tuple containing the
         code and error description. An error code 0 indicates success.
 
-        The Error queue can be deleted using the standard SCPI
+        The Error queue can be cleared using the standard SCPI
         ``agilentE5062A.clear()`` method.
 
         """


Test patch:
diff --git a/tests/instruments/agilent/test_agilentE5062A.py b/tests/instruments/agilent/test_agilentE5062A.py
index 30472c3ca6..7cccf7e31f 100644
--- a/tests/instruments/agilent/test_agilentE5062A.py
+++ b/tests/instruments/agilent/test_agilentE5062A.py
@@ -67,7 +67,7 @@ def initial_comm_pairs():
     return comms
 
 
-def test_ch_active_traces():
+def test_ch_visible_traces():
     with expected_protocol(
             AgilentE5062A,
             [
@@ -81,12 +81,12 @@ def test_ch_active_traces():
                 ("CALC1:PARameter:COUNt 4", None),
                 ("CALC1:PARameter:COUNt?", "4")
             ]) as inst:
-        # test ch active_traces
+        # test ch visible_traces
         ot4 = np.arange(4) + 1
         ch = inst.channels[1]
         for opt in ot4:
-            ch.active_traces = opt
-            assert ch.active_traces == opt
+            ch.visible_traces = opt
+            assert ch.visible_traces == opt
 
 
 def test_ch_traces():
@@ -472,3 +472,15 @@ def test_trigger_single():
                 ("TRIGger:SINGle", None)
             ]) as inst:
         inst.trigger_single()
+
+
+def test_wait_for_complete():
+    with expected_protocol(
+            AgilentE5062A,
+            [
+                *initial_comm_pairs(),
+                ("TRIGger:SINGle", None),
+                ("*OPC?", "1")
+            ]) as inst:
+        inst.trigger_single()
+        inst.wait_for_complete()
diff --git a/tests/instruments/agilent/test_agilentE5062A_with_device.py b/tests/instruments/agilent/test_agilentE5062A_with_device.py
index 48f7ae99b2..cce02a5558 100644
--- a/tests/instruments/agilent/test_agilentE5062A_with_device.py
+++ b/tests/instruments/agilent/test_agilentE5062A_with_device.py
@@ -29,7 +29,6 @@
 # port connection can be anything that is safe for that power level.
 
 import pytest
-from pyvisa.errors import VisaIOError
 from pymeasure.instruments.agilent.agilentE5062A import AgilentE5062A
 
 import numpy as np
@@ -59,21 +58,21 @@ def agilentE5062A(connected_device_address):
     return instr
 
 
-def test_ch_active_traces(agilentE5062A):
+def test_ch_visible_traces(agilentE5062A):
     agilentE5062A.clear()
-    # test ch active_traces
+    # test ch visible_traces
     ot4 = np.arange(4) + 1
     for ch in agilentE5062A.channels.values():
         for opt in ot4:
-            ch.active_traces = opt
-            assert ch.active_traces == opt
+            ch.visible_traces = opt
+            assert ch.visible_traces == opt
     assert not agilentE5062A.pop_err()[0]
 
 
 def test_ch_traces(agilentE5062A):
     agilentE5062A.clear()
     for ch in agilentE5062A.channels.values():
-        ch.active_traces = 4
+        ch.visible_traces = 4
         for tr in ch.traces.values():
             for p in ['S11', 'S12', 'S21', 'S22']:
                 tr.parameter = p
@@ -207,7 +206,7 @@ def test_tr_format(agilentE5062A):
     agilentE5062A.clear()
     ch = agilentE5062A.channels[1]
     ch.activate()
-    ch.active_traces = 4
+    ch.visible_traces = 4
     for tr in ch.traces.values():
         tr.activate()
         for opt in ch.TRACE_FORMAT:
@@ -220,7 +219,7 @@ def test_data(agilentE5062A):
     agilentE5062A.clear()
     agilentE5062A.display_layout = 'D12_34'
     for ch in agilentE5062A.channels.values():
-        ch.active_traces = 1
+        ch.visible_traces = 1
         tr = ch.traces[1]
         tr.activate()
         tr.trace_format = 'SCOM'
@@ -317,16 +316,6 @@ def test_trigger_single(agilentE5062A):
         complete.
 
     """
-
-    def wait_for_complete(vna, attempt=1):
-        try:
-            vna.complete
-        except VisaIOError:     # IO timeout
-            if attempt > 10:
-                raise
-            else:
-                wait_for_complete(vna, attempt=attempt+1)
-
     agilentE5062A.clear()
     agilentE5062A.reset()
     agilentE5062A.trigger_source = 'BUS'
@@ -335,7 +324,7 @@ def wait_for_complete(vna, attempt=1):
         agilentE5062A.abort()
         ch.trigger_initiate()
         agilentE5062A.trigger_single()
-        wait_for_complete(agilentE5062A)
+        agilentE5062A.wait_for_complete()
     agilentE5062A.trigger_source = 'INT'
     agilentE5062A.trigger_continuous = True
     assert not agilentE5062A.pop_err()[0]