Skip to content
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:
standalone_branch_suffix:
description: 'Suffix of the branch on standalone'
required: false
default: ''
default: '_test'


concurrency:
Expand Down Expand Up @@ -58,7 +58,7 @@ jobs:
python_versions: '["3.8"]'
wheel: true
wheelhouse: false
standalone_suffix: ${{ github.event.inputs.standalone_branch_suffix || '' }}
standalone_suffix: ${{ github.event.inputs.standalone_branch_suffix || '_test' }}
custom-requirements: requirements/requirements_dev.txt
custom-wheels: './dpf-standalone/v232/dist'
secrets: inherit
Expand All @@ -67,7 +67,7 @@ jobs:
name: "Build and Test on Docker"
uses: ./.github/workflows/test_docker.yml
with:
standalone_suffix: ${{ github.event.inputs.standalone_branch_suffix || '' }}
standalone_suffix: ${{ github.event.inputs.standalone_branch_suffix || '_test' }}
custom-requirements: requirements/requirements_dev.txt
custom-wheels: 'dpf-standalone/dist'
secrets: inherit
Expand All @@ -77,7 +77,7 @@ jobs:
uses: ./.github/workflows/docs.yml
with:
ANSYS_VERSION: "232"
standalone_suffix: ${{ github.event.inputs.standalone_branch_suffix || '' }}
standalone_suffix: ${{ github.event.inputs.standalone_branch_suffix || '_test' }}
custom-requirements: requirements/requirements_dev.txt
custom-wheels: './dpf-standalone/v232/dist'
event_name: ${{ github.event_name }}
Expand All @@ -100,7 +100,7 @@ jobs:
with:
ANSYS_VERSION: "232"
python_versions: '["3.8"]'
standalone_suffix: ${{ github.event.inputs.standalone_branch_suffix || '' }}
standalone_suffix: ${{ github.event.inputs.standalone_branch_suffix || '_test' }}
custom-requirements: requirements/requirements_dev.txt
custom-wheels: './dpf-standalone/v232/dist'
secrets: inherit
Expand Down Expand Up @@ -144,7 +144,7 @@ jobs:
uses: ./.github/workflows/pydpf-post.yml
with:
ANSYS_VERSION: "232"
standalone_suffix: ${{ github.event.inputs.standalone_branch_suffix || '' }}
standalone_suffix: ${{ github.event.inputs.standalone_branch_suffix || '_test' }}
custom-requirements: requirements/requirements_dev.txt
custom-wheels: './dpf-standalone/v232/dist'
secrets: inherit
6 changes: 3 additions & 3 deletions requirements/requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
ansys-dpf-gate==0.3.1.dev0
ansys-dpf-gatebin==0.3.1.dev0
ansys-grpc-dpf==0.7.1.dev0
ansys-dpf-gate==0.3.2.dev0
ansys-dpf-gatebin==0.3.2.dev0
ansys-grpc-dpf==0.7.2.dev0
6 changes: 5 additions & 1 deletion src/ansys/dpf/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@
from ansys.dpf.core import path_utilities
from ansys.dpf.core import settings
from ansys.dpf.core.server_factory import ServerConfig, AvailableServerConfigs
from ansys.dpf.core.server_context import set_default_server_context, AvailableServerContexts
from ansys.dpf.core.server_context import (
set_default_server_context,
AvailableServerContexts,
LicenseContextManager
)
from ansys.dpf.core.unit_system import UnitSystem, unit_systems

# for matplotlib
Expand Down
15 changes: 15 additions & 0 deletions src/ansys/dpf/core/runtime_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,18 @@ def num_threads(self):
@num_threads.setter
def num_threads(self, value):
self._data_tree.add(num_threads=int(value))

@property
def license_timeout_in_seconds(self):
"""Sets the default number of threads to use for all operators,
default is omp_get_num_threads.
Comment on lines +188 to +189
Copy link
Contributor

@PProfizi PProfizi Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Sets the default number of threads to use for all operators,
default is omp_get_num_threads.
"""Returns the license timeout in seconds.


Returns
-------
float
"""
return self._data_tree.get_as("license_timeout_in_seconds", types.double)

@license_timeout_in_seconds.setter
def license_timeout_in_seconds(self, value):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def license_timeout_in_seconds(self, value):
def license_timeout_in_seconds(self, value):
"""Sets the license timeout in seconds."""

self._data_tree.add(license_timeout_in_seconds=float(value))
108 changes: 107 additions & 1 deletion src/ansys/dpf/core/server_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import os
import warnings
from enum import Enum
from ansys.dpf.core import dpf_operator
from ansys.dpf.core import errors


class LicensingContextType(Enum):
Expand All @@ -38,8 +40,112 @@ def same_licensing_context(first, second):
return True


class LicenseContextManager:
"""Can optionally be used to check out a license before using licensed DPF Operators.
Improves performance if you are using multiple Operators that require licensing.
It can also be used to force checkout before running a script when few
Ansys license increments are available.
The license is checked in when the the object is deleted.

Parameters
----------
increment_name: str, optional
License increment to check out. To improve script efficiency, this license increment
should be consistent with the increments required by the following Operators. If ``None``,
the first available increment of this
`list <https://dpf.docs.pyansys.com/version/dev/user_guide/getting_started_with_dpf_server.
html#ansys-licensing>`_
is checked out.
license_timeout_in_seconds: float, optional
If an increment is not available by the maximum time set here, check out fails. Default is:
:py:func:`ansys.dpf.core.runtime_config.RuntimeCoreConfig.license_timeout_in_seconds`
server : server.DPFServer, optional
Server with the channel connected to the remote or local instance. The
default is ``None``, in which case an attempt is made to use the global
server.

Examples
--------
Using a context manager
>>> from ansys.dpf import core as dpf
>>> dpf.set_default_server_context(dpf.AvailableServerContexts.premium)
>>> field = dpf.Field()
>>> field.append([0., 0., 0.], 1)
>>> op = dpf.operators.filter.field_high_pass()
>>> op.inputs.field(field)
>>> op.inputs.threshold(0.0)
>>> with dpf.LicenseContextManager() as lic:
... out = op.outputs.field()

Using an instance
>>> lic = dpf.LicenseContextManager()
>>> op.inputs.field(field)
>>> op.inputs.threshold(0.0)
>>> out = op.outputs.field()
>>> lic = None

Using a context manager and choosing license options
>>> op.inputs.field(field)
>>> op.inputs.threshold(0.0)
>>> out = op.outputs.field()
>>> op = dpf.operators.filter.field_high_pass()
>>> op.inputs.field(field)
>>> op.inputs.threshold(0.0)
>>> with dpf.LicenseContextManager(
... increment_name="preppost", license_timeout_in_seconds=1.) as lic:
... out = op.outputs.field()

Notes
-----
Available from 6.1 server version.
"""

def __init__(
self, increment_name: str = None, license_timeout_in_seconds: float = None, server=None
):
from ansys.dpf.core import server as server_module

self._server = server_module.get_or_create_server(server)
if not self._server.meet_version("6.1"):
raise errors.DpfVersionNotSupported("6.1")
self._license_checkout_operator = dpf_operator.Operator(
"license_checkout", server=self._server
)
if increment_name is not None:
self._license_checkout_operator.connect(0, increment_name)
if license_timeout_in_seconds is not None:
self._license_checkout_operator.connect(1, license_timeout_in_seconds)
self._license_checkout_operator.run()

def release_data(self):
"""Release the data."""
self._license_checkout_operator = None

def __enter__(self):
return self

def __exit__(self, type, value, tb):
if tb is None:
self.release_data()

def __del__(self):
self.release_data()
pass

@property
def status(self):
"""Returns a string with the list of checked out increments

Returns
-------
str
"""
status_operator = dpf_operator.Operator("license_status", server=self._server)
return status_operator.eval()


class ServerContext:
"""The context allows to choose which capabilities are available server side.
"""The context allows you to choose which capabilities are available server side.
xml_path argument won't be taken into account if using LicensingContextType.entry.

Parameters
Expand Down
6 changes: 6 additions & 0 deletions tests/test_data_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,12 @@ def test_runtime_core_config(server_type):
assert num_threads == 4
core_config.num_threads = num_threads_init
assert core_config.num_threads == num_threads_init
timeout_init = core_config.license_timeout_in_seconds
core_config.license_timeout_in_seconds = 4.0
license_timeout_in_seconds = core_config.license_timeout_in_seconds
assert license_timeout_in_seconds == 4.0
core_config.license_timeout_in_seconds = timeout_init
assert core_config.license_timeout_in_seconds == timeout_init


@conftest.raises_for_servers_version_under("4.0")
Expand Down
111 changes: 87 additions & 24 deletions tests/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ def test_context_environment_variable(reset_context_environment_variable):
@pytest.mark.order(1)
@pytest.mark.skipif(
running_docker
or os.environ.get("ANSYS_DPF_ACCEPT_LA", None) is None
or os.environ.get("ANSYS_DPF_ACCEPT_LA", "") is ""
or not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_0,
reason="Tests ANSYS_DPF_ACCEPT_LA",
)
Expand All @@ -509,7 +509,7 @@ def test_license_agr(set_context_back_to_premium):

@pytest.mark.order(2)
@pytest.mark.skipif(
os.environ.get("ANSYS_DPF_ACCEPT_LA", None) is None
os.environ.get("ANSYS_DPF_ACCEPT_LA", "") is ""
or not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_0,
reason="Tests ANSYS_DPF_ACCEPT_LA",
)
Expand Down Expand Up @@ -537,22 +537,33 @@ def test_apply_context(set_context_back_to_premium):
# in process server, otherwise premium operators will already be loaded.
dpf.core.server.shutdown_all_session_servers()
dpf.core.SERVER_CONFIGURATION = dpf.core.AvailableServerConfigs.InProcessServer

field = dpf.core.Field()
field.append([0.0], 1)
if conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_0:
with pytest.raises(KeyError):
dpf.core.Operator("core::field::high_pass")
with pytest.raises(dpf.core.errors.DPFServerException):
if dpf.core.SERVER.os == "nt":
dpf.core.load_library("Ans.Dpf.Math.dll", "math_operators")
else:
dpf.core.load_library("libAns.Dpf.Math.so", "math_operators")
if conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_1:
with pytest.raises(dpf.core.errors.DPFServerException):
op = dpf.core.Operator("core::field::high_pass")
op.connect(0, field)
op.connect(1, 0.0)
op.eval()
else:
with pytest.raises(KeyError):
dpf.core.Operator("core::field::high_pass")
with pytest.raises(dpf.core.errors.DPFServerException):
if dpf.core.SERVER.os == "nt":
dpf.core.load_library("Ans.Dpf.Math.dll", "math_operators")
else:
dpf.core.load_library("libAns.Dpf.Math.so", "math_operators")
assert dpf.core.SERVER.context == dpf.core.AvailableServerContexts.entry
else:
dpf.core.start_local_server()

dpf.core.set_default_server_context(dpf.core.AvailableServerContexts.premium)
assert dpf.core.SERVER.context == dpf.core.AvailableServerContexts.premium
dpf.core.Operator("core::field::high_pass")
op = dpf.core.Operator("core::field::high_pass")
op.connect(0, field)
op.connect(1, 0.0)
op.eval()
with pytest.raises(dpf.core.errors.DPFServerException):
dpf.core.set_default_server_context(dpf.core.AvailableServerContexts.entry)
with pytest.raises(dpf.core.errors.DPFServerException):
Expand All @@ -568,26 +579,43 @@ def test_apply_context(set_context_back_to_premium):
def test_apply_context_remote(remote_config_server_type, set_context_back_to_premium):
dpf.core.server.shutdown_all_session_servers()
dpf.core.SERVER_CONFIGURATION = remote_config_server_type
field = dpf.core.Field()
field.append([0.0], 1)
if conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_0:
with pytest.raises(dpf.core.errors.DPFServerException):
dpf.core.Operator("core::field::high_pass")
with pytest.raises(dpf.core.errors.DPFServerException):
if dpf.core.SERVER.os == "nt":
dpf.core.load_library("Ans.Dpf.Math.dll", "math_operators")
else:
dpf.core.load_library("libAns.Dpf.Math.so", "math_operators")

assert dpf.core.SERVER.context == dpf.core.AvailableServerContexts.entry
if conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_1:
with pytest.raises(dpf.core.errors.DPFServerException):
op = dpf.core.Operator("core::field::high_pass")
op.connect(0, field)
op.connect(1, 0.0)
op.eval()
else:
with pytest.raises(dpf.core.errors.DPFServerException):
op = dpf.core.Operator("core::field::high_pass")
with pytest.raises(dpf.core.errors.DPFServerException):
if dpf.core.SERVER.os == "nt":
dpf.core.load_library("Ans.Dpf.Math.dll", "math_operators")
else:
dpf.core.load_library("libAns.Dpf.Math.so", "math_operators")
assert dpf.core.SERVER.context == dpf.core.AvailableServerContexts.entry
else:
dpf.core.start_local_server()

dpf.core.SERVER.apply_context(dpf.core.AvailableServerContexts.premium)
dpf.core.Operator("core::field::high_pass")
op = dpf.core.Operator("core::field::high_pass")
op.connect(0, field)
op.connect(1, 0.0)
op.eval()
assert dpf.core.SERVER.context == dpf.core.AvailableServerContexts.premium

dpf.core.server.shutdown_all_session_servers()
with pytest.raises(dpf.core.errors.DPFServerException):
dpf.core.Operator("core::field::high_pass")
if conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_1:
with pytest.raises(dpf.core.errors.DPFServerException):
op = dpf.core.Operator("core::field::high_pass")
op.connect(0, field)
op.connect(1, 0.0)
op.eval()
elif conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_0:
with pytest.raises(dpf.core.errors.DPFServerException):
op = dpf.core.Operator("core::field::high_pass")
dpf.core.set_default_server_context(dpf.core.AvailableServerContexts.premium)
dpf.core.Operator("core::field::high_pass")
with pytest.raises(dpf.core.errors.DPFServerException):
Expand All @@ -612,5 +640,40 @@ def test_release_dpf(server_type):
dpf.core.Operator("expansion::modal_superposition", server=server_type)


@conftest.raises_for_servers_version_under("6.1")
def test_license_context_manager_as_context():
field = dpf.core.Field()
field.append([0.0, 0.0, 0.0], 1)
op = dpf.core.operators.filter.field_high_pass()
op.inputs.field(field)
op.inputs.threshold(0.0)
with dpf.core.LicenseContextManager() as lic:
out = op.outputs.field()
st = lic.status

assert len(st) != 0
new_st = lic.status
assert new_st == ""
lic = dpf.core.LicenseContextManager()
op.inputs.field(field)
op.inputs.threshold(0.0)
out = op.outputs.field()
new_st = lic.status
assert str(new_st) == str(st)
lic = None

op = dpf.core.operators.filter.field_high_pass()
op.inputs.field(field)
op.inputs.threshold(0.0)
with dpf.core.LicenseContextManager(
increment_name="ansys", license_timeout_in_seconds=1.0
) as lic:
out = op.outputs.field()
st = lic.status
assert "ansys" in st
st = lic.status
assert "ansys" not in st


if __name__ == "__main__":
test_load_api_with_awp_root()