From e8f1f0ac43fe3c510a11aa51ab208f7339446289 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 3 Jul 2026 14:43:50 +0200 Subject: [PATCH] Results: Add TableData pamhyr object for np table. --- src/Model/Results/Results.py | 225 ++++++++++++++++++++++++++++++- src/Model/Results/River/River.py | 30 +++-- src/Model/Study.py | 2 +- src/Solver/Mage.py | 6 + 4 files changed, 250 insertions(+), 13 deletions(-) diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py index 58dedde2..9517c7da 100644 --- a/src/Model/Results/Results.py +++ b/src/Model/Results/Results.py @@ -157,8 +157,185 @@ class AdditionalData(SQLSubModel): return True +class TableData(SQLSubModel): + _sub_classes = [] + + def __init__(self, id=-1, study=None, + legend="", data=None, + owner_scenario=-1): + super(TableData, self).__init__( + id=id, status=study.status, + owner_scenario=owner_scenario + ) + + self._study = study + self._legend = legend + self._data = data + + @property + def data(self): + return self._data + + @classmethod + def _db_create(cls, execute, ext=""): + execute(f""" + CREATE TABLE results_table_data{ext} ( + {cls.create_db_add_pamhyr_id()}, + result INTEGER NOT NULL, + legend TEXT NOT NULL, + unit TEXT NOT NULL, + data_shape_x INTEGER NOT NULL, + data_shape_y INTEGER NOT NULL, + data_shape_z INTEGER DEFAULT -1, + data BLOB NOT NULL, + {Scenario.create_db_add_scenario()}, + {Scenario.create_db_add_scenario_fk()}, + FOREIGN KEY(result) REFERENCES results(pamhyr_id), + PRIMARY KEY(pamhyr_id, result, legend, scenario) + ) + """) + + if ext != "": + return True + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version, data=None): + major, minor, release = version.strip().split(".") + create = False + + if major == "0" and int(minor) == 2: + if int(release) < 7 and not create: + cls._db_create(execute) + create = True + + return cls._update_submodel(execute, version, data) + + @classmethod + def _db_load(cls, execute, data=None): + res = {} + + study = data['study'] + status = data['status'] + scenario = data["scenario"] + result = data["result"] + rid = data["result_pid"] + + table = execute( + "SELECT pamhyr_id, " + + "legend, unit, " + + "data_shape_x, data_shape_y, data_shape_z, " + + "data, " + + "scenario " + + "FROM results_table_data " + + f"WHERE scenario = {scenario.id} " + + f"AND result = {rid}" + ) + + for v in table: + it = iter(v) + + pid = next(it) + legend = next(it) + unit = next(it) + data_shape_x = next(it) + data_shape_y = next(it) + data_shape_z = next(it) + data = next(it) + owner_scenario = next(it) + + dtype = cls._unit_to_dtype(unit) + + np_data = np.frombuffer(data, dtype=dtype) + if data_shape_y == -1: + np_data = np.reshape(np_data, data_shape_x) + elif data_shape_z == -1: + np_data = np.reshape(np_data, (data_shape_x, + data_shape_y)) + else: + np_data = np.reshape(np_data, (data_shape_x, + data_shape_y, + data_shape_z)) + + new_table = cls(study=study, + legend=legend, data=np_data, + owner_scenario=owner_scenario) + + res[legend] = new_table + + return res + + def _db_save(self, execute, data=None): + if self._status.scenario.id != self._owner_scenario: + return + + shape_x = -1 + shape_y = -1 + shape_z = -1 + shape = self._data.shape + + if len(shape) == 2: + shape_x, shape_y = shape + elif len(shape == 1): + shape_x, = shape + elif len(shape == 3): + shape_x, shape_y, shape_z = shape + else: + logger.error( + f"Failed to parse numpy shape: {shape}" + ) + + execute( + "INSERT INTO " + + "results_table_data (pamhyr_id, result, " + + "legend, unit, " + + "data_shape_x, data_shape_y, data_shape_z, " + + "data, " + + "scenario) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + self._pamhyr_id, data["result"], + self._legend, self._dtype_to_unit(self._data.dtype), + shape_x, shape_y, shape_z, + self._data, + self._owner_scenario + ) + + return True + + @classmethod + def _unit_to_dtype(cls, unit): + t = { + "i32": np.int32, "i64": np.int64, + "u32": np.uint32, "u64": np.uint64, + "f32": np.float32, "f64": np.float64, + } + + return t[unit] + + def _dtype_to_unit(self, dtype): + t = { + np.dtype(np.int32): "i32", np.dtype(np.int64): "i64", + np.dtype(np.uint32): "u32", np.dtype(np.uint64): "u64", + np.dtype(np.float32): "f32", np.dtype(np.float64): "f64", + } + + return t[dtype] + + def table(self): + return self._data + + def __len__(self): + return len(self._data) + + def __getitem__(self, key): + if isinstance(key, tuple): + return self._data[key[0], key[1]] + + self._date[key] + + class Results(SQLSubModel): - _sub_classes = [River, AdditionalData] + _sub_classes = [River, TableData, AdditionalData] def __init__(self, id=-1, study=None, solver=None, repertory="", name="0"): @@ -179,8 +356,11 @@ class Results(SQLSubModel): "creation_date": datetime.now(), "study_revision": study.status.version, "additional_data": [], + "table": {}, } + self._ts_index = {} + if solver is not None: self.set("solver_type", solver._type) @@ -221,6 +401,10 @@ class Results(SQLSubModel): def get_timestamp_id(self, ts): return self._ts_index[ts] + def compute_timestamp_index(self): + for i, ts in enumerate(self._meta_data["timestamps"]): + self._ts_index[ts] = i + def reload(self): return self._solver.results( self._study, @@ -318,11 +502,19 @@ class Results(SQLSubModel): new_results.set("study_revision", revision) new_results.set("creation_date", creation_date) + data["result_pid"] = pid + data["result"] = new_results + + table_data = TableData._db_load(execute, data) + new_results.set("table", table_data) + sf = ">" + ''.join(itertools.repeat("d", nb_timestamps)) ts = struct.unpack(sf, timestamps_bytes) new_results.set("timestamps", ts) data["timestamps"] = sorted(ts) + new_results.compute_timestamp_index() + data["parent"] = new_results new_results._river = River._db_load(execute, data) @@ -331,6 +523,20 @@ class Results(SQLSubModel): AdditionalData._db_load(execute, data) ) + # Update global index + i = 0 + for reach in new_results._river.reachs: + nb = len(reach.profiles) + reach.set_global_index(range(i, i + nb + 1)) + i += nb + + if "Z" in table_data: + for ts in data["timestamps"]: + new_results._river.update_water_limits( + ts, + table_data["Z"][new_results.get_timestamp_id(ts), :] + ) + yield (solver_type, new_results) def _db_save_clear(self, execute, solver_type, data=None): @@ -353,6 +559,11 @@ class Results(SQLSubModel): f"WHERE scenario = {self._owner_scenario} " + f"AND result = {pid}" ) + execute( + "DELETE FROM results_table_data " + + f"WHERE scenario = {self._owner_scenario} " + + f"AND result = {pid}" + ) execute( "DELETE FROM results_add_data " + f"WHERE scenario = {self._owner_scenario} " + @@ -396,7 +607,19 @@ class Results(SQLSubModel): data["result"] = self._pamhyr_id self._river._db_save(execute, data) + tables = self.get("table") + for legend in tables: + tables[legend]._db_save(execute, data) + for add_data in self.get("additional_data"): add_data._db_save(execute, data) return True + + def new_table_data(self, legend, data): + return TableData( + study=self._study, + legend=legend, + data=data, + owner_scenario=self._owner_scenario + ) diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index d0f44aaf..a5ca3b6d 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -137,7 +137,6 @@ class Profile(SQLSubModel): @classmethod def _db_load(cls, execute, data=None): - new = {} status = data['status'] parent = data['parent'] @@ -148,6 +147,8 @@ class Profile(SQLSubModel): loaded = data['loaded_pid'] timestamps = data['timestamps'] + new = cls(profile, study, parent) + values = execute( "SELECT pamhyr_id, result, key, " + "len_data, data, scenario " + @@ -167,12 +168,6 @@ class Profile(SQLSubModel): data = next(it) owner_scenario = next(it) - if profile not in new: - new_data = cls(profile, study, parent) - new[profile] = new_data - else: - new_data = new[profile] - if key in ["Z", "Q", "V"]: sf = ">" + ''.join(itertools.repeat("f", len_data)) len_values = len(values) @@ -185,11 +180,11 @@ class Profile(SQLSubModel): ) for timestamp, value in zip(timestamps, values): - new_data.set(timestamp, key, value) + new.set(timestamp, key, value) if key == "Z": - new_data.update_water_limits(timestamp, value) + new.update_water_limits(timestamp, value) - return list(new.values()) + return new @classmethod def _db_load_data_sl_format(cls, values, len_data, timestamps): @@ -361,6 +356,10 @@ class Reach(SQLSubModel): for i, p in enumerate(self._profiles): self._buffers[key][:, i] = p.get_key(key) + def update_water_limits(self, timestamp, z_data): + for profile, z in zip(self._profiles, z_data): + profile.update_water_limits(timestamp, z) + @classmethod def _db_create(cls, execute, ext=""): return cls._create_submodel(execute) @@ -380,7 +379,7 @@ class Reach(SQLSubModel): for i, profile in enumerate(reach.profiles): data["profile"] = profile - new_reach._profiles += Profile._db_load(execute, data) + new_reach._profiles += [Profile._db_load(execute, data)] return new_reach @@ -435,6 +434,15 @@ class River(SQLSubModel): ) ) + def update_water_limits(self, timestamp, data): + for reach in self._reachs: + ind = reach.global_index + + reach.update_water_limits( + timestamp, + data[ind[0]:ind[1]] + ) + @classmethod def _db_create(cls, execute, ext=""): return cls._create_submodel(execute) diff --git a/src/Model/Study.py b/src/Model/Study.py index 2be1069c..dfa25d2d 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -46,7 +46,7 @@ logger = logging.getLogger() class Study(SQLModel): - _version = "0.2.6" + _version = "0.2.7" _sub_classes = [ Scenario, diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 4e6940f1..a088b85b 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -1206,6 +1206,12 @@ class Mage8(Mage): j += 1 table["V"] = velocity_arr + + for legend in table: + table[legend] = results.new_table_data( + legend, table[legend] + ) + results.set("table", table) logger.info(f"read_bin: ... end with {len(ts)} timestamp read")