nidaqmx-python icon indicating copy to clipboard operation
nidaqmx-python copied to clipboard

Cannot overwrite custom scales

Open alecote opened this issue 11 months ago • 5 comments

When saving a custom scale, I receive a DaqError if the scale already exists in NI MAX even when using overwrite_existing_scale=True. The example below will run fine the first time when creating the scale, but will fail If I try to run it again now that the scale exists in NI MAX.

Windows 10 NI MAX 2024 Q4 NI-DAQmx 2024 Q4 (24.8.0) Python 3.11.11 running in Spyder 6.0.1 (conda) nidaqmx 1.0.2

Sample code:

import nidaqmx
from nidaqmx.constants import UnitsPreScaled

scale = nidaqmx.scale.Scale.create_lin_scale('ch1_V',
                                     10, # slope
                                     0, # intercept
                                     pre_scaled_units=UnitsPreScaled.VOLTS,
                                     scaled_units='V')
scale.save(author="Me",
           overwrite_existing_scale=True,
           allow_interactive_editing=True,
           allow_interactive_deletion=True)

Error message:

Traceback (most recent call last):

  File ~\.conda\envs\env_gui\Lib\site-packages\spyder_kernels\customize\utils.py:209 in exec_encapsulate_locals
    exec_fun(compile(code_ast, filename, "exec"), globals)

  File c:\users\me\untitled0.py:24
    scale = nidaqmx.scale.Scale.create_lin_scale('ch1_V',

  File ~\.conda\envs\env_gui\Lib\site-packages\nidaqmx\scale.py:338 in create_lin_scale
    scale._interpreter.create_lin_scale(

  File ~\.conda\envs\env_gui\Lib\site-packages\nidaqmx\_library_interpreter.py:1764 in create_lin_scale
    self.check_for_error(error_code)

  File ~\.conda\envs\env_gui\Lib\site-packages\nidaqmx\_library_interpreter.py:6412 in check_for_error
    raise DaqError(extended_error_info, error_code)

DaqError: Custom scale cannot be created. A saved scale with this name already exists.

Scale Name: ch1_V

Status Code: -200356

alecote avatar Feb 03 '25 18:02 alecote

Just realized that the error happens in create_lin_scale, not in the save() function. Does this mean I am not creating my scale correctly?

I do not see a method to load an existing scale or modify an existing scale.

alecote avatar Feb 03 '25 18:02 alecote

In the underlying DAQmx C API, there is no handle or refnum type representing a scale. All of the scale functions refer to scales by name. I think that's why you are getting an error from create_lin_scale: there is a saved scale with that name, and saved scales are in the same namespace as unsaved, process-local scales.

The DAQmx Python API has two scale classes: Scale and PersistedScale. Scale allows you to get and set the scale properties, and PersistedScale allows you to "load" or delete the persisted scale. "Loading" the scale doesn't really load the scale from disk; it just converts the PersistedScale object into a Scale object so you can access different properties. Both Scale and PersistedScale refer to the scale by name. PersistedScale is also used by the System.scales property to return all of the saved scales.

For your workflow, I would recommend trying one of the following:

  • Use PersistedScale to delete the old saved scale before creating a new one.
  • Create the new scale with a unique name, then use the save_as parameter on Scale.save to specify the name of the saved scale.

bkeryan avatar Feb 03 '25 19:02 bkeryan

In the underlying DAQmx C API, there is no handle or refnum type representing a scale. All of the scale functions refer to scales by name. I think that's why you are getting an error from create_lin_scale: there is a saved scale with that name, and saved scales are in the same namespace as unsaved, process-local scales.

The DAQmx Python API has two scale classes: Scale and PersistedScale. Scale allows you to get and set the scale properties, and PersistedScale allows you to "load" or delete the persisted scale. "Loading" the scale doesn't really load the scale from disk; it just converts the PersistedScale object into a Scale object so you can access different properties. Both Scale and PersistedScale refer to the scale by name. PersistedScale is also used by the System.scales property to return all of the saved scales.

For your workflow, I would recommend trying one of the following:

* Use `PersistedScale` to delete the old saved scale before creating a new one.

* Create the new scale with a unique name, then use the `save_as` parameter on [Scale.save](https://nidaqmx-python.readthedocs.io/en/stable/scale.html#nidaqmx.scale.Scale.save) to specify the name of the saved scale.

Brad beat me to it. One thing to add:

All of the scale functions refer to scales by name. I think that's why you are getting an error from create_lin_scale: there is a saved scale with that name, and saved scales are in the same namespace as unsaved, process-local scales.

Through code inspection, I can confirm that one of the first things we do when creating an in-process scale is check that there are no name collisions with saved scales. It does seem odd that our save API allows overwriting when you can't create a collision in the first place. I guess it allows for multiple processes to be creating scales simultaneously with the same name. That feels like a bug waiting to happen, and as a user I don't think I'd want to overwrite in that case. I'd prefer an error.

Anyways... do what Brad said with PersistedScale 🙂

zhindes avatar Feb 03 '25 20:02 zhindes

Thank you Brad and Zach for those fast responses! In the meantime, I had found myself that I could delete the saved scale by calling _interpreter.delete_saved_scale(scale.name).

import nidaqmx
from nidaqmx.constants import UnitsPreScaled
from nidaqmx.errors import DaqError

# Load the scale (or create if it does not exist)
scale = nidaqmx.scale.Scale('ch1_V')

# Delete the existing scale if it exists
# Other errors should still be raised
try:
    scale._interpreter.delete_saved_scale(scale.name)
except DaqError as e:
    if e.error_code != -200378:
        raise

# Create and save the new scale
scale.create_lin_scale(scale.name,
                       10, # slope
                       0, # intercept
                       pre_scaled_units=UnitsPreScaled.VOLTS,
                       scaled_units='V')

scale.save(author="Me",
           overwrite_existing_scale=True,
           allow_interactive_editing=True,
           allow_interactive_deletion=True)

Using PersistedScale.delete() apparently still requires to do this try except, as it raises an error if the scale does not exist. If I try to load() using a random string for the name it just creates a new Scale object with that new name instead of telling me there is no saved scale under that name.

I like the other proposed solution using the save_as parameter. This way, I do not need to load, delete or check the existence of the scale.

import nidaqmx
from nidaqmx.constants import UnitsPreScaled

scale = nidaqmx.scale.Scale.create_lin_scale('some_unique_name_doesnt_matter',
                                     10, # slope
                                     0, # intercept
                                     pre_scaled_units=UnitsPreScaled.VOLTS,
                                     scaled_units='V')
scale.save(save_as='ch1_V',
           author="Me",
           overwrite_existing_scale=True,
           allow_interactive_editing=True,
           allow_interactive_deletion=True)

I am by no means an expert in programming, but found a bit counterintuitive that create_lin_scale() required me to input the scale name, as it is a method of an object that already has a name. What if create_lin_scale only changed the local properties of the object? This is how I intuitively thought it would work before going through the doc and getting your help:

import nidaqmx
from nidaqmx.constants import UnitsPreScaled

# Create a Scale object. It also loads the saved scale if it exists. This is the current behavior of the code
scale = nidaqmx.scale.Scale('ch1_V')

# Local modification of the existing scale object. This is not how the code works, but how I thought it would.
scale.create_lin_scale(10, 0, pre_scaled_units=UnitsPreScaled.VOLTS, scaled_units='V')

# Saving the changes to NI MAX
scale.save(author="Me",
           overwrite_existing_scale=True,
           allow_interactive_editing=True,
           allow_interactive_deletion=True)

Anyhow, thank you very much for your help! Case closed :)

alecote avatar Feb 03 '25 20:02 alecote

In the meantime, I had found myself that I could delete the saved scale by calling _interpreter.delete_saved_scale(scale.name).

Great. However, note that the interpreter classes are an internal implementation detail and not a stable API. The stable API for this is PersistedScale.delete:

persisted_scale = nidaqmx.system.storage.persisted_scale.PersistedScale('ch1_V')
persisted_scale.delete()

I am by no means an expert in programming, but found a bit counterintuitive that create_lin_scale() required me to input the scale name, as it is a method of an object that already has a name. What if create_lin_scale only changed the local properties of the object?

create_lin_scale is a static method that returns a new object instance. The intended usage is to call it on the class, as you did in scale = nidaqmx.scale.Scale.create_lin_scale('some_unique_name_doesnt_matter', ...).

Python also allows you to call static methods on an instance of the class, as in scale.create_lin_scale(scale.name, ...), but this doesn't use the instance for anything other than looking up the class. Instead, this creates a new Scale instance and throws it away. This also only happens to work because your existing Scale object and the newly created Scale object have the same name. If the DAQmx C API referred to scales by numeric handle, like it does for tasks, this code would not happen to work, because the existing Scale object and the newly created Scale object would have different handles.

bkeryan avatar Feb 03 '25 21:02 bkeryan