-
Notifications
You must be signed in to change notification settings - Fork 6
Feature/signal data operation #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
59008f7
c86a6f2
e67f0f1
fc9c0b3
1c7e665
85c3a2b
a105b66
f5da1cf
6639d45
ca41656
d6b6227
d79fac5
4e3cec7
25a4b1d
1455de1
3dc3aad
ae27462
93fd74d
3b8f226
2499794
639c611
56d3040
8cc64a4
3d05817
e4f6f2c
ad0a37b
c218ca2
405950c
1c2d9b4
556cdb5
8787daf
d213449
e9a9537
353af26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,6 @@ | |
| import imas # type: ignore | ||
| import numpy as np # type: ignore | ||
| import re # type: ignore | ||
| from copy import copy # type: ignore | ||
| from idstools.database import DBMaster # type: ignore | ||
| from imas.ids_metadata import IDSMetadata # type: ignore | ||
| from imas.ids_primitive import ( | ||
|
|
@@ -52,6 +51,8 @@ | |
| apply_gaussian_filter, | ||
| apply_simple_operations, | ||
| resolve_irregular_coordinate_data_shape, | ||
| apply_signal_operations, | ||
| combine_signal_units, | ||
| ) | ||
| from ibex.core.data_manipulation_methods import SmoothingMethod, InterpolationMethod | ||
| from ibex.endpoints.schemas.request_data_schemas import PlotDataRequestModel | ||
|
|
@@ -1055,6 +1056,7 @@ def get_plot_data(self, plot_data_query: PlotDataRequestModel) -> dict: | |
| } | ||
| coordinates_to_be_returned.append(c) | ||
| first_value = find_first_value_in_list(ids_data) | ||
| result_unit = first_value.metadata.units or "" | ||
| data_to_be_returned = convert_ids_data_into_numpy_array(ids_data) | ||
|
|
||
| if first_value.metadata.ndim == 2: | ||
|
|
@@ -1106,8 +1108,12 @@ def get_plot_data(self, plot_data_query: PlotDataRequestModel) -> dict: | |
|
|
||
| # ============= END data smoothing ============= | ||
|
|
||
| # ============= BEGIN resample data onto new time vector ============= | ||
| try: | ||
| original_data_shape = np.asarray(data_to_be_returned).shape | ||
| except ValueError: | ||
| original_data_shape = "irregular" | ||
|
|
||
| # ============= BEGIN resample data onto new time vector ============= | ||
| def convert_to_lists(data): | ||
| if isinstance(data, list): | ||
| return [convert_to_lists(d) for d in data] | ||
|
|
@@ -1116,6 +1122,10 @@ def convert_to_lists(data): | |
| else: | ||
| return data | ||
|
|
||
| # list of dicts used when combining signals after | ||
| # {uri:str, data:list[*], interpolated_data: list[*]} | ||
| others_signals_data = {} | ||
|
|
||
| if plot_data_query.interpolate_over: | ||
| # check for non-interpolatable coordinates | ||
| if plot_data_query.interpolation_method != InterpolationMethod.EXACT_VALUE: | ||
|
|
@@ -1153,6 +1163,10 @@ def convert_to_lists(data): | |
|
|
||
| original_coord_values.reverse() | ||
|
|
||
| store_other_signals_data = False # used for signal combining after interpolation | ||
| if plot_data_query.signal_operations: | ||
| store_other_signals_data = True | ||
|
|
||
| for _uri in plot_data_query.interpolate_over: | ||
| _uri_obj = IMAS_URI(_uri) | ||
|
|
||
|
|
@@ -1165,11 +1179,20 @@ def convert_to_lists(data): | |
| "IDS name and node path should be the same for source and target URI when interpolating data" | ||
| ) | ||
|
|
||
| new_plot_data_query = copy(plot_data_query) | ||
| new_plot_data_query.uri = _uri | ||
| new_plot_data_query.interpolate_over = None | ||
| new_plot_data_query.smoothing_method = None | ||
| interpolate_to_coordinates = self.get_plot_data(new_plot_data_query)["data"]["coordinates"] | ||
| new_plot_data_query = PlotDataRequestModel(uri=_uri) | ||
| # interpolate_to will be used later with signal combining | ||
| interpolate_to = self.get_plot_data(new_plot_data_query)["data"] | ||
| interpolate_to_coordinates = interpolate_to["coordinates"] | ||
| if store_other_signals_data: | ||
| others_signals_data[_uri] = { | ||
| "uri": _uri, | ||
| "data": pad_to_rectangular(interpolate_to["value"]), | ||
| "coordinates": [ | ||
| sorted(set(flatten(convert_to_lists(c["value"])))) for c in interpolate_to_coordinates | ||
| ], | ||
| "shape": interpolate_to["shape"], | ||
| "unit": interpolate_to["unit"], | ||
| } | ||
|
|
||
| if len(interpolate_to_coordinates) != len(coordinates_to_be_returned): | ||
| message = "Interpolation error. Source and target nodes have different number of coordinates." | ||
|
|
@@ -1215,10 +1238,112 @@ def convert_to_lists(data): | |
|
|
||
| # ============= END resample data onto new time vector ============= | ||
|
|
||
| # ============= BEGIN signal operations ============= | ||
| # | ||
| # Steps performed in this block: | ||
| # 1. Collect all signal URIs referenced in signal_operations | ||
| # 2. Pad data to rectangular if shape is irregular | ||
| # 3. For each signal URI, fetch and prepare data: | ||
| # a. If the signal was already interpolated (stored during | ||
| # interpolation phase), skip fetching | ||
| # b. Otherwise fetch the signal and check shape compatibility | ||
| # 4. Prepare operand data for each signal: | ||
| # a. If interpolation was requested, resample onto common coords | ||
| # b. Otherwise normalize raw signal data to a NumPy array | ||
| # 5. Build a flat uri->array dict and apply all signal operations | ||
|
|
||
| if plot_data_query.signal_operations: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After Square / Multiplication operation shown below, should unit of the quantity remains same? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure concerning physical interpretation of mentioned operations. If we multiply
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If signal1 and signal2 have the same unit, yes, this is correct. Combining units is only valid with your There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DONE. ad0a37b
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add and sub you shall make sure that units are the same or throw an error. #90 (comment) I think we are not checking if units are the same. and it did not throw exception. [sawantp1@98dci4-srv-1005 frontend]$ echo $ANOTHER
imas:hdf5?path=/work/imas/shared/imasdb/ITER/3/134174/117#equilibrium/time_slice[0]/profiles_1d/psi
[sawantp1@98dci4-srv-1005 frontend]$ echo $ANOTHER1
imas:hdf5?path=/work/imas/shared/imasdb/ITER/3/134174/117#equilibrium/time_slice[0]/profiles_1d/pressure
$ curl -G http://127.0.0.1:8000/data/plot_data --data-urlencode "uri=$ANOTHER1" --data-urlencode "signal_operations=add:$ANOTHER" | jq | head -n 20
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 13737 100 13737 0 0 353k 0 --:--:-- --:--:-- --:--:-- 353k
{
"data": {
"name": "pressure",
"unit": "Pa",
"shape": [
299
],
"downsampled_shape": [
299
],
"ndim": 1,
"path": "#equilibrium/time_slice[0]/profiles_1d/pressure",
"description": "Pressure",
"coordinates": [
{
"name": "psi",
"target": "#equilibrium/time_slice[0]/profiles_1d/pressure",
"unit": "Wb",
"shape": [
299There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DONE |
||
| # Step 1: extract unique signal URIs from operation strings | ||
| signal_op_uris = set() | ||
| for op_str in plot_data_query.signal_operations or []: | ||
| _, op_uri = op_str.split(":", 1) | ||
| signal_op_uris.add(op_uri) | ||
|
|
||
| # Step 2: ensure rectangular data for downstream processing | ||
| if original_data_shape == "irregular": | ||
| data_to_be_returned = pad_to_rectangular(data_to_be_returned) | ||
|
|
||
| # Step 3: fetch and prepare each signal referenced in operations | ||
| for signal_uri in signal_op_uris: | ||
| if signal_uri not in others_signals_data: | ||
| # Signal was not pre-loaded during interpolation phase. | ||
| # Fetch it now and verify shape compatibility. | ||
| request = PlotDataRequestModel(uri=signal_uri) | ||
| other_signal = self.get_plot_data(request) | ||
|
|
||
| # Comparing signal shapes | ||
| if ( | ||
| other_signal["data"]["shape"] == "irregular" | ||
| or other_signal["data"]["shape"] != original_data_shape | ||
| ): | ||
| msg = ( | ||
| f"Cannot apply operation on signal {signal_uri} without interpolation. " | ||
| "Signal and data shapes do not match. " | ||
| "Try interpolating the signal onto the data shape." | ||
| ) | ||
| raise InvalidParametersException(msg) | ||
|
|
||
| # Comparing coordinates | ||
| coordinates_match = self._coordinates_match( | ||
| coordinates_to_be_returned, other_signal["data"]["coordinates"] | ||
| ) | ||
|
|
||
| if not coordinates_match: | ||
| msg = ( | ||
| f"Cannot apply operation on signal {signal_uri} without interpolation. " | ||
| "Coordinates do not match. " | ||
| "Try interpolating the signal onto the data coordinates." | ||
| ) | ||
| raise InvalidParametersException(msg) | ||
|
|
||
| others_signals_data[signal_uri] = { | ||
| "uri": request.uri, | ||
| "data": other_signal["data"]["value"], | ||
| "coordinates": [ | ||
| sorted(set(flatten(convert_to_lists(c["value"])))) | ||
| for c in other_signal["data"]["coordinates"] | ||
| ], | ||
| "shape": other_signal["data"]["shape"], | ||
| "unit": other_signal["data"]["unit"], | ||
| } | ||
|
|
||
| # Step 4: prepare operand data (resampled or raw) | ||
| if plot_data_query.interpolate_over: | ||
| # Resample signal data onto the common coordinate grid | ||
| signal_data = resample_data_without_interpolation( | ||
| tuple(reversed(others_signals_data[signal_uri]["coordinates"])), | ||
| others_signals_data[signal_uri]["data"], | ||
| tuple(common_coords_values), | ||
| ) | ||
| else: | ||
| signal_data = others_signals_data[signal_uri]["data"] | ||
|
|
||
| others_signals_data[signal_uri]["data"] = np.asarray(signal_data) | ||
|
|
||
| # Step 5: flatten dict and apply operations in order | ||
| signal_data_by_uri = {uri: info["data"] for uri, info in others_signals_data.items()} | ||
| for operation in plot_data_query.signal_operations: | ||
| operation_type, signal_uri = operation.split(":", 1) | ||
| result_unit = combine_signal_units( | ||
| result_unit, | ||
| others_signals_data[signal_uri]["unit"], | ||
| operation_type, | ||
| ) | ||
| data_to_be_returned = apply_signal_operations( | ||
| data_to_be_returned, plot_data_query.signal_operations, signal_data_by_uri | ||
| ) | ||
|
|
||
| # ============= END signal operations ============= | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you know what I am doing wrong here? import imas
import numpy as np
entries = [
("/tmp/ibex-grid-first", [0.0, 0.5, 1.0], [10.0, 20.0, 30.0]),
("/tmp/ibex-grid-second", [0.0, 0.4, 0.8], [100.0, 200.0, 300.0]),
]
for path, rho, temperature in entries:
with imas.DBEntry(f"imas:hdf5?path={path}", mode="w") as entry:
cp = entry.factory.core_profiles()
cp.ids_properties.homogeneous_time = 1
cp.time = np.array([1.0])
cp.profiles_1d.resize(1)
cp.profiles_1d[0].grid.rho_tor_norm = np.array(rho)
cp.profiles_1d[0].electrons.temperature = np.array(temperature)
entry.put(cp)
# $ echo $FIRST
# imas:hdf5?path=/tmp/ibex-grid-first#core_profiles:0/profiles_1d[:]/electrons/temperature
# $ echo $SECOND
# imas:hdf5?path=/tmp/ibex-grid-second#core_profiles:0/profiles_1d[:]/electrons/temperature
# $ curl -sS -G http://127.0.0.1:8000/data/plot_data --data-urlencode "uri=$FIRST" --data-urlencode "interpolate_over=$FIRST" --data-urlencode "interpolation_method=exact_value" --data-urlencode "signal_operations=add:$SECOND" | jq
# {
# "message": "'interpolated_data'"
# }There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am on it... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Corrected |
||
| # Interpolation and signal operations can change the data shape. | ||
| # The response's ``shape`` describes the processed data before | ||
| # downsampling, rather than the raw shape used for compatibility | ||
| # checks above. | ||
| try: | ||
| original_data_shape = np.asarray(data_to_be_returned).shape | ||
| processed_data_shape = np.asarray(data_to_be_returned).shape | ||
| except ValueError: | ||
| original_data_shape = "irregular" | ||
| processed_data_shape = "irregular" | ||
|
|
||
| # Downsample only 1D data | ||
| if first_value.metadata.ndim == 1: | ||
| if coordinates_to_be_returned[0]["target"].split("/")[-1] == f"{node_path.split('/')[-1]}": | ||
|
|
@@ -1250,8 +1375,8 @@ def convert_to_lists(data): | |
| result = { | ||
| "data": { | ||
| "name": node_path.split("/")[-1], | ||
| "unit": first_value.metadata.units, | ||
| "shape": original_data_shape, | ||
| "unit": result_unit, | ||
| "shape": processed_data_shape, | ||
| "downsampled_shape": downsampled_shape, | ||
| "ndim": first_value.metadata.ndim, | ||
| "path": f"#{ids}/{node_path}", | ||
|
|
@@ -1272,6 +1397,17 @@ def convert_to_lists(data): | |
| coordinate["coordinates"] = new_shape_factors_list | ||
| return result | ||
|
|
||
| def _coordinates_match(self, coordinates_1, coordinates_2): | ||
| if len(coordinates_1) != len(coordinates_2): | ||
| return False | ||
|
|
||
| coordinates_match = all( | ||
| coordinate_1["name"] == coordinate_2["name"] | ||
| and np.array_equal(np.asarray(coordinate_1["value"]), np.asarray(coordinate_2["value"]), equal_nan=True) | ||
| for coordinate_1, coordinate_2 in zip(coordinates_1, coordinates_2) | ||
| ) | ||
| return coordinates_match | ||
|
|
||
| def _is_empty(self, seq): | ||
| """Checks if list is essentially empty (contains only empty lists or empty strings)""" | ||
| if isinstance(seq, (IDSNumericArray, IDSString0D, IDSString1D, IDSComplex0D, IDSFloat0D, IDSInt0D)): | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.