Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/cdb/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ associated public API.
_BeamData
_BeamLoad
_BeamResults
_Cable
_CableData
_CableLoad
_CableResults
_CableResult
_BeamStress
_GroupData
_GroupLCData
Expand Down
6 changes: 4 additions & 2 deletions src/py_sofistik_utils/cdb_reader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
from . _internal.beam_load import _BeamLoad
from . _internal.beam_results import _BeamResults
from . _internal.beam_stresses import _BeamStress
from . _internal.cable import _Cable
from . _internal.cable_data import _CableData
from . _internal.cable_load import _CableLoad
from . _internal.cable_results import _CableResults
from . _internal.cable_result import _CableResult
from . _internal.group_data import _GroupData
from . _internal.group_lc_data import _GroupLCData
from . _internal.load_cases import _LoadCases
Expand All @@ -29,9 +30,10 @@
"_BeamLoad",
"_BeamResults",
"_BeamStress",
"_Cable",
"_CableData",
"_CableLoad",
"_CableResults",
"_CableResult",
"_GroupData",
"_GroupLCData",
"_LoadCases",
Expand Down
28 changes: 28 additions & 0 deletions src/py_sofistik_utils/cdb_reader/_internal/cable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# standard library imports

# third party library imports

# local library specific imports
from . cable_data import _CableData
from . cable_load import _CableLoad
from . cable_result import _CableResult
from . sofistik_dll import SofDll


class _Cable:
"""
High-level wrapper for cable-related data access and operations.

The class aggregates the low-level interfaces ``_CableData``,
``_CableLoad``, and ``_CableResults`` into a single abstraction. It
provides a structured entry point for reading, manipulating and evaluating
cable definitions, applied loads, and analysis results.
"""
data: _CableData
load: _CableLoad
result: _CableResult

def __init__(self, dll: SofDll) -> None:
self.data = _CableData(dll)
self.load = _CableLoad(dll)
self.result = _CableResult(dll)
51 changes: 27 additions & 24 deletions src/py_sofistik_utils/cdb_reader/_internal/cable_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class _CableData:
* store the retrieved data in a convenient format;
* provide access to the data after the CDB is closed.

The underlying data structure is a :class:`pandas.DataFrame` with the following
columns:
The underlying data structure is a :class:`pandas.DataFrame` with the
following columns:

* ``GROUP`` element group
* ``ELEM_ID`` element number
Expand All @@ -27,12 +27,14 @@ class _CableData:
* ``L0``: initial length
* ``PROPERTY``: property number (cross-section)

The ``DataFrame`` uses a MultiIndex with level ``ELEM_ID`` to enable fast lookups
via the `get` method. The index column is not dropped from the ``DataFrame``.
The ``DataFrame`` uses a MultiIndex with level ``ELEM_ID`` to enable
fast lookups via the `get` method. The index column is not dropped from
the ``DataFrame``.

.. note::

Not all available quantities are retrieved and stored. In particular:
Not all available quantities are retrieved and stored. In
particular:

* normal direction
* prestress
Expand All @@ -43,12 +45,12 @@ class _CableData:

are currently not included.

This is a deliberate design choice and may be changed in the future without
breaking the existing API.
This is a deliberate design choice and may be changed in the future
without breaking the existing API.
"""
def __init__(self, dll: SofDll) -> None:
self._data = DataFrame(
columns = [
columns=[
"GROUP",
"ELEM_ID",
"N1",
Expand All @@ -66,15 +68,16 @@ def clear(self) -> None:
self._data = self._data[0:0]

def data(self, deep: bool = True) -> DataFrame:
"""Return the :class:`pandas.DataFrame` containing the loaded key ``160/00``.
"""Return the :class:`pandas.DataFrame` containing the loaded key
``160/00``.

Parameters
----------
deep : bool, default True
When ``deep=True``, a new object will be created with a copy of the calling
object's data and indices. Modifications to the data or indices of the
copy will not be reflected in the original object (refer to
:meth:`pandas.DataFrame.copy` documentation for details).
When ``deep=True``, a new object will be created with a copy of the
calling object's data and indices. Modifications to the data or
indices of the copy will not be reflected in the original object
(refer to :meth:`pandas.DataFrame.copy` documentation for details).
"""
return self._data.copy(deep=deep)

Expand All @@ -83,7 +86,7 @@ def get(
element_id: int,
quantity: str = "L0",
default: float | int | None = None
) -> float | int:
) -> float | int:
"""Retrieve the requested cable quantity.

Parameters
Expand All @@ -104,8 +107,8 @@ def get(
Returns
-------
value : float or int
The requested quantity if found. Otherwise, returns ``default`` when it is not
None.
The requested quantity if found. Otherwise, returns ``default``
when it is not None.

Raises
------
Expand All @@ -123,8 +126,8 @@ def get(
) from e

def load(self) -> None:
"""Retrieve all cable data. If the key does not exist or it is empty, a warning is
raised only if ``echo_level > 0``.
"""Retrieve all cable data. If the key does not exist or it is empty, a
warning is raised only if ``echo_level > 0``.
"""
if self._dll.key_exist(160, 0):
cabl = CCABL()
Expand Down Expand Up @@ -165,22 +168,22 @@ def load(self) -> None:
group_data = _GroupData(self._dll)
group_data.load()

temp_df = DataFrame(data).sort_values("ELEM_ID", kind="mergesort")
elem_ids = temp_df["ELEM_ID"]
df = DataFrame(data).sort_values("ELEM_ID", kind="mergesort")
elem_ids = df["ELEM_ID"]

for grp, grp_range in group_data.iterator_cable():
if grp_range.stop == 0:
continue

left = elem_ids.searchsorted(grp_range.start, side="left")
right = elem_ids.searchsorted(grp_range.stop - 1, side="right")
temp_df.loc[temp_df.index[left:right], "GROUP"] = grp
df.loc[df.index[left:right], "GROUP"] = grp

# set indices for fast lookup
temp_df = temp_df.set_index(["ELEM_ID"], drop=False)
df = df.set_index(["ELEM_ID"], drop=False)

# merge data
if self._data.empty:
self._data = temp_df
self._data = df
else:
self._data = concat([self._data, temp_df])
self._data = concat([self._data, df])
74 changes: 40 additions & 34 deletions src/py_sofistik_utils/cdb_reader/_internal/cable_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class _CableLoad:
* store the retrieved data in a convenient format;
* provide access to the data after the CDB is closed.

The underlying data structure is a :class:`pandas.DataFrame` with the following
columns:
The underlying data structure is a :class:`pandas.DataFrame` with the
following columns:

* ``LOAD_CASE`` load case number
* ``GROUP`` element group
Expand All @@ -27,9 +27,10 @@ class _CableLoad:
* ``PA``: load value at cable start point
* ``PE``: load value at cable end point

The ``DataFrame`` uses a MultiIndex with levels ``ELEM_ID``, ``LOAD_CASE`` and
``TYPE`` (in this specific order) to enable fast lookups via the `get` method. The
index columns are not dropped from the ``DataFrame``.
The ``DataFrame`` uses a MultiIndex with levels ``ELEM_ID``,
``LOAD_CASE`` and ``TYPE`` (in this specific order) to enable fast
lookups via the `get` method. The index columns are not dropped from
the ``DataFrame``.

The load ``TYPE`` can be one of the following:

Expand All @@ -47,8 +48,8 @@ class _CableLoad:

.. important::

Wind and snow loads are not implemented and will raise a runtime error if they
are present in the requested load case.
Wind and snow loads are not implemented and will raise a runtime
error if they are present in the requested load case.
"""
_LOAD_TYPE_MAP = {
10: "PG",
Expand All @@ -67,7 +68,7 @@ class _CableLoad:

def __init__(self, dll: SofDll) -> None:
self._data = DataFrame(
columns = [
columns=[
"LOAD_CASE",
"GROUP",
"ELEM_ID",
Expand Down Expand Up @@ -98,15 +99,16 @@ def clear_all(self) -> None:
self._loaded_lc.clear()

def data(self, deep: bool = True) -> DataFrame:
"""Return the :class:`pandas.DataFrame` containing the loaded keys ``161/LC``.
"""Return the :class:`pandas.DataFrame` containing the loaded keys
``161/LC``.

Parameters
----------
deep : bool, default True
When ``deep=True``, a new object will be created with a copy of the calling
object's data and indices. Modifications to the data or indices of the
copy will not be reflected in the original object (refer to
:meth:`pandas.DataFrame.copy` documentation for details).
When ``deep=True``, a new object will be created with a copy of the
calling object's data and indices. Modifications to the data or
indices of the copy will not be reflected in the original object
(refer to :meth:`pandas.DataFrame.copy` documentation for details).
"""
return self._data.copy(deep=deep)

Expand All @@ -117,7 +119,7 @@ def get(
load_type: str,
point: str = "PA",
default: float | None = None
) -> float:
) -> float:
"""Retrieve the requested cable load.

Parameters
Expand All @@ -142,35 +144,38 @@ def get(
- ``"PZP"``

point : str, default "PA"
Location on the cable where the load is applied; either the start (``"PA"``)
or the end (``"PE"``)
Location on the cable where the load is applied; either the start
(``"PA"``) or the end (``"PE"``)
default : float or None, default None
Value to return if the requested load is not found

Returns
-------
value : float
The requested load if found. Otherwise, returns ``default`` when it is not
None.
The requested load if found. Otherwise, returns ``default`` when it
is not None.

Raises
------
LookupError
If the requested load is not found and ``default`` is None.
"""
try:
return self._data.at[(element_id, load_case, load_type), point] # type: ignore
return self._data.at[
(element_id, load_case, load_type),
point
] # type: ignore
except (KeyError, ValueError) as e:
if default is not None:
return default
raise LookupError(
f"Cable load entry not found for element id {element_id}, "
f"load case {load_case}, load type {load_type} and point {point}!"
f"Cable load entry not found for element id {element_id}, load"
f" case {load_case}, load type {load_type} and point {point}!"
) from e

def load(self, load_cases: int | list[int]) -> None:
"""Retrieve cable load data for the given ``load_cases``. If a load case is not
found, a warning is raised only if ``echo_level > 0``.
"""Retrieve cable load data for the given ``load_cases``. If a load
case is not found, a warning is raised only if ``echo_level > 0``.

Parameters
----------
Expand All @@ -179,44 +184,44 @@ def load(self, load_cases: int | list[int]) -> None:

Notes
-----
Wind and snow loads are not implemented and will raise a runtime error if they are
present in the requested load case.
Wind and snow loads are not implemented and will raise a runtime error
if they are present in the requested load case.
"""
if isinstance(load_cases, int):
load_cases = [load_cases]
else:
load_cases = list(set(load_cases)) # remove duplicated entries

# load data
temp_list: list[dict[str, float | int | str]] = []
data: list[dict[str, float | int | str]] = []
for load_case in load_cases:
if self._dll.key_exist(161, load_case):
self.clear(load_case)
temp_list.extend(self._load(load_case))
data.extend(self._load(load_case))

# assigning groups
group_data = _GroupData(self._dll)
group_data.load()

temp_df = DataFrame(temp_list).sort_values("ELEM_ID", kind="mergesort")
elem_ids = temp_df["ELEM_ID"]
df = DataFrame(data).sort_values("ELEM_ID", kind="mergesort")
elem_ids = df["ELEM_ID"]

for grp, grp_range in group_data.iterator_cable():
if grp_range.stop == 0:
continue

left = elem_ids.searchsorted(grp_range.start, side="left")
right = elem_ids.searchsorted(grp_range.stop - 1, side="right")
temp_df.loc[temp_df.index[left:right], "GROUP"] = grp
df.loc[df.index[left:right], "GROUP"] = grp

# set indices for fast lookup
temp_df = temp_df.set_index(["ELEM_ID", "LOAD_CASE", "TYPE"], drop=False)
df = df.set_index(["ELEM_ID", "LOAD_CASE", "TYPE"], drop=False)

# merge data
if self._data.empty:
self._data = temp_df
self._data = df
else:
self._data = concat([self._data, temp_df])
self._data = concat([self._data, df])
self._loaded_lc.update(load_cases)

def set_echo_level(self, echo_level: int) -> None:
Expand Down Expand Up @@ -257,7 +262,8 @@ def _load(self, load_case: int) -> list[dict[str, float | int | str]]:
type_ = _CableLoad._LOAD_TYPE_MAP[cabl.m_typ]
except KeyError as e:
raise RuntimeError(
f"Unknown cable load type {cabl.m_typ} for element {cabl.m_nr}!"
f"Unknown cable load type {cabl.m_typ} for element"
f" {cabl.m_nr}!"
) from e

data.append(
Expand Down
Loading