mirror of
https://gitlab.com/pamhyr/pamhyr2.git
synced 2026-10-30 03:20:41 +01:00
Time/date: preserve calendar dates on start date changes + fix solver time exports with older format
This commit is contained in:
1 parent
d15fa684b7
commit
d4c596a6fe
8 files changed
+150
-50
No files matched your search
+27
-1
@@ -21,7 +21,7 @@ import shutil
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from tools import timer, timestamp
|
from tools import timer, timestamp, parse_datetime
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
from Model.Tools.PamhyrListExt import PamhyrModelList
|
from Model.Tools.PamhyrListExt import PamhyrModelList
|
||||||
@@ -32,6 +32,7 @@ from Model.Scenario import Scenario
|
|||||||
from Model.Status import StudyStatus
|
from Model.Status import StudyStatus
|
||||||
from Model.Except import NotImplementedMethodeError
|
from Model.Except import NotImplementedMethodeError
|
||||||
from Model.River import River
|
from Model.River import River
|
||||||
|
from Model.TimeShift import shift_river_times, shift_database_times
|
||||||
from Model.Geometry.Reach import Reach
|
from Model.Geometry.Reach import Reach
|
||||||
from Model.HydraulicStructures.HydraulicStructures import (
|
from Model.HydraulicStructures.HydraulicStructures import (
|
||||||
HydraulicStructure
|
HydraulicStructure
|
||||||
@@ -72,6 +73,7 @@ class Study(SQLModel):
|
|||||||
self.description = ""
|
self.description = ""
|
||||||
# Time system
|
# Time system
|
||||||
self._time_system = "time"
|
self._time_system = "time"
|
||||||
|
self._pending_time_offset = 0.0
|
||||||
self._date = datetime.fromtimestamp(0)
|
self._date = datetime.fromtimestamp(0)
|
||||||
|
|
||||||
if init_new:
|
if init_new:
|
||||||
@@ -198,6 +200,16 @@ class Study(SQLModel):
|
|||||||
self._date = date
|
self._date = date
|
||||||
self.status.modified()
|
self.status.modified()
|
||||||
|
|
||||||
|
def change_start_date(self, date, preserve_dates=False):
|
||||||
|
"""Change the origin, optionally preserving calendar dates of all series."""
|
||||||
|
new_date = parse_datetime(date)
|
||||||
|
offset = (parse_datetime(self.date) - new_date).total_seconds()
|
||||||
|
if preserve_dates and offset:
|
||||||
|
shift_river_times(self.river, offset)
|
||||||
|
self._pending_time_offset += offset
|
||||||
|
self._river_scenario_cache.clear()
|
||||||
|
self.use_date(new_date)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date(self):
|
def date(self):
|
||||||
return self._date
|
return self._date
|
||||||
@@ -460,6 +472,17 @@ class Study(SQLModel):
|
|||||||
)
|
)
|
||||||
progress()
|
progress()
|
||||||
|
|
||||||
|
if self._pending_time_offset:
|
||||||
|
self._db.execute('SAVEPOINT shift_time_origin')
|
||||||
|
try:
|
||||||
|
shift_database_times(self._db, self._pending_time_offset)
|
||||||
|
self._db.execute('RELEASE SAVEPOINT shift_time_origin')
|
||||||
|
except Exception:
|
||||||
|
self._db.execute('ROLLBACK TO SAVEPOINT shift_time_origin')
|
||||||
|
self._db.execute('RELEASE SAVEPOINT shift_time_origin')
|
||||||
|
raise
|
||||||
|
self._pending_time_offset = 0.0
|
||||||
|
|
||||||
self._save_submodel(
|
self._save_submodel(
|
||||||
[self.scenarios, self._river],
|
[self.scenarios, self._river],
|
||||||
data=progress
|
data=progress
|
||||||
@@ -594,6 +617,9 @@ class Study(SQLModel):
|
|||||||
self.status.stop_loading()
|
self.status.stop_loading()
|
||||||
self.status.scenario = old_scenario
|
self.status.scenario = old_scenario
|
||||||
|
|
||||||
|
if self._pending_time_offset:
|
||||||
|
shift_river_times(river, self._pending_time_offset)
|
||||||
|
|
||||||
# FIXME: Disable scenario cache to save memory usage
|
# FIXME: Disable scenario cache to save memory usage
|
||||||
# self._river_scenario_cache[scenario] = river
|
# self._river_scenario_cache[scenario] = river
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""Shift temporal series when preserving calendar dates across a new origin."""
|
||||||
|
from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition
|
||||||
|
from Model.LateralContribution.LateralContribution import LateralContribution
|
||||||
|
from Model.WeatherParameters.WeatherParameters import WeatherParameters
|
||||||
|
from Model.BoundaryConditionsAdisTS.BoundaryConditionAdisTS import BoundaryConditionAdisTS
|
||||||
|
from Model.LateralContributionsAdisTS.LateralContributionAdisTS import LateralContributionAdisTS
|
||||||
|
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature import BoundaryConditionTemperature
|
||||||
|
|
||||||
|
|
||||||
|
def shift_river_times(river, offset):
|
||||||
|
changes = []
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
def collect(obj, data):
|
||||||
|
temporal = isinstance(obj, (BoundaryConditionAdisTS,
|
||||||
|
LateralContributionAdisTS,
|
||||||
|
BoundaryConditionTemperature))
|
||||||
|
if isinstance(obj, (BoundaryCondition, LateralContribution, WeatherParameters)):
|
||||||
|
temporal = bool(obj.header) and obj.header[0] == 'time'
|
||||||
|
if temporal:
|
||||||
|
for point in obj._data:
|
||||||
|
if id(point) not in seen:
|
||||||
|
changes.append((point, float(point[0]) + offset))
|
||||||
|
seen.add(id(point))
|
||||||
|
|
||||||
|
river._data_traversal(modifier=collect)
|
||||||
|
print(f"Shifting {len(changes)} temporal points by {offset} seconds.")
|
||||||
|
for point, value in changes:
|
||||||
|
print(f" {point[0]} -> {value}")
|
||||||
|
# This is a global change of origin, not a scenario-specific edit.
|
||||||
|
point[0] = value
|
||||||
|
|
||||||
|
|
||||||
|
def shift_database_times(connection, offset):
|
||||||
|
tables = [
|
||||||
|
('boundary_condition_data', 'boundary_condition', 'bc', BoundaryCondition),
|
||||||
|
('lateral_contribution_data', 'lateral_contribution', 'lc', LateralContribution),
|
||||||
|
('weather_parameters_data', 'weather_parameters', 'wp', WeatherParameters),
|
||||||
|
('boundary_condition_data_adists', None, None, None),
|
||||||
|
('lateral_contribution_data_adists', None, None, None),
|
||||||
|
('boundary_condition_data_temperature', None, None, None),
|
||||||
|
]
|
||||||
|
for table, parent, fk, model in tables:
|
||||||
|
if model is None:
|
||||||
|
rows = connection.execute(f'SELECT rowid, data0 FROM {table}').fetchall()
|
||||||
|
else:
|
||||||
|
rows = []
|
||||||
|
for kind, in connection.execute(f'SELECT DISTINCT type FROM {parent}'):
|
||||||
|
headers = model._get_ctor_from_type(kind)().header
|
||||||
|
if not headers or headers[0] != 'time':
|
||||||
|
continue
|
||||||
|
rows.extend(connection.execute(
|
||||||
|
f'SELECT d.rowid, d.data0 FROM {table} d JOIN {parent} p '
|
||||||
|
f'ON p.pamhyr_id=d.{fk} AND p.scenario=d.scenario WHERE p.type=?',
|
||||||
|
(kind,)
|
||||||
|
).fetchall())
|
||||||
|
connection.executemany(
|
||||||
|
f'UPDATE {table} SET data0=? WHERE rowid=?',
|
||||||
|
[(float(value) + offset, rowid) for rowid, value in rows]
|
||||||
|
)
|
||||||
@@ -594,9 +594,7 @@ class AdisTSwc(AdisTS):
|
|||||||
f.write(f"*---------++++++++++\n")
|
f.write(f"*---------++++++++++\n")
|
||||||
|
|
||||||
for LC_data in LC.data:
|
for LC_data in LC.data:
|
||||||
tmp = timestamp_to_old_pamhyr_date_adists(
|
tmp = timestamp_to_old_pamhyr_date_adists(int(LC_data[0]))
|
||||||
int(self.export_time(study, LC_data[0]))
|
|
||||||
)
|
|
||||||
f.write(" ".join((f"{tmp}",
|
f.write(" ".join((f"{tmp}",
|
||||||
f"{LC_data[1]}\n")))
|
f"{LC_data[1]}\n")))
|
||||||
f.write(f"*\n")
|
f.write(f"*\n")
|
||||||
@@ -628,9 +626,7 @@ class AdisTSwc(AdisTS):
|
|||||||
f.write(f"*---------++++++++++\n")
|
f.write(f"*---------++++++++++\n")
|
||||||
|
|
||||||
for BC_data in BC.data:
|
for BC_data in BC.data:
|
||||||
tmp = timestamp_to_old_pamhyr_date_adists(
|
tmp = timestamp_to_old_pamhyr_date_adists(int(BC_data[0]))
|
||||||
int(self.export_time(study, BC_data[0]))
|
|
||||||
)
|
|
||||||
f.write(" ".join((f"{tmp}",
|
f.write(" ".join((f"{tmp}",
|
||||||
f"{BC_data[1]}\n")))
|
f"{BC_data[1]}\n")))
|
||||||
f.write(f"*\n")
|
f.write(f"*\n")
|
||||||
|
|||||||
@@ -884,9 +884,7 @@ class AdisTTwc(AdisTT):
|
|||||||
f.write("*temps |temperature (degC)\n")
|
f.write("*temps |temperature (degC)\n")
|
||||||
f.write("*---------++++++++++\n")
|
f.write("*---------++++++++++\n")
|
||||||
for value in boundary_condition.data:
|
for value in boundary_condition.data:
|
||||||
date = timestamp_to_old_pamhyr_date_adists(
|
date = timestamp_to_old_pamhyr_date_adists(int(value[0]))
|
||||||
int(self.export_time(study, value[0]))
|
|
||||||
)
|
|
||||||
f.write(f"{date} {value[1]}\n")
|
f.write(f"{date} {value[1]}\n")
|
||||||
f.write("*\n")
|
f.write("*\n")
|
||||||
return filename
|
return filename
|
||||||
@@ -909,9 +907,7 @@ class AdisTTwc(AdisTT):
|
|||||||
f.write("*temps |value\n")
|
f.write("*temps |value\n")
|
||||||
f.write("*---------++++++++++\n")
|
f.write("*---------++++++++++\n")
|
||||||
for value in weather_parameter.data:
|
for value in weather_parameter.data:
|
||||||
date = timestamp_to_old_pamhyr_date_adists(
|
date = timestamp_to_old_pamhyr_date_adists(int(value[0]))
|
||||||
int(self.export_time(study, value[0]))
|
|
||||||
)
|
|
||||||
f.write(f"{date} {value[1]}\n")
|
f.write(f"{date} {value[1]}\n")
|
||||||
f.write("*\n")
|
f.write("*\n")
|
||||||
return filename
|
return filename
|
||||||
|
|||||||
@@ -45,15 +45,6 @@ logger = logging.getLogger()
|
|||||||
class CommandLineSolver(AbstractSolver):
|
class CommandLineSolver(AbstractSolver):
|
||||||
_type = ""
|
_type = ""
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def export_time(study, value):
|
|
||||||
"""Return elapsed seconds before applying the solver file's units."""
|
|
||||||
if study.time_system != "date":
|
|
||||||
return value
|
|
||||||
# Match the UTC timestamps produced by date entry in the tables.
|
|
||||||
start = parse_datetime(study.date).replace(tzinfo=timezone.utc)
|
|
||||||
return float(value) - start.timestamp()
|
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
super(CommandLineSolver, self).__init__(name)
|
super(CommandLineSolver, self).__init__(name)
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -333,7 +333,7 @@ class Mage(CommandLineSolver):
|
|||||||
v1 = d[1]
|
v1 = d[1]
|
||||||
|
|
||||||
if t in ["HYD", "LIM"]:
|
if t in ["HYD", "LIM"]:
|
||||||
v0 = self.export_time(study, v0) / 60 # Minutes
|
v0 /= 60 # Convert first column to minutes
|
||||||
|
|
||||||
f.write(f"{v0:9} {v1:9} \n")
|
f.write(f"{v0:9} {v1:9} \n")
|
||||||
|
|
||||||
@@ -429,7 +429,7 @@ class Mage(CommandLineSolver):
|
|||||||
if d.is_deleted():
|
if d.is_deleted():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
time = self.export_time(study, d[0])
|
time = d[0]
|
||||||
if lateral.lctype in ["EV"]:
|
if lateral.lctype in ["EV"]:
|
||||||
f.write(f"{time:10.3f}{-d[1]:10.3f}\n")
|
f.write(f"{time:10.3f}{-d[1]:10.3f}\n")
|
||||||
else:
|
else:
|
||||||
@@ -1002,7 +1002,7 @@ class Mage8(Mage):
|
|||||||
|
|
||||||
# Data
|
# Data
|
||||||
for d in bound.data:
|
for d in bound.data:
|
||||||
time = self.export_time(study, d[0]) / 60
|
time = d[0] / 60
|
||||||
f.write(f"{time:10.3f}{d[1]:10.3f}\n")
|
f.write(f"{time:10.3f}{d[1]:10.3f}\n")
|
||||||
|
|
||||||
return files
|
return files
|
||||||
|
|||||||
@@ -550,8 +550,7 @@ class Rubar3(CommandLineSolver):
|
|||||||
for bc in bcs:
|
for bc in bcs:
|
||||||
f.write(f"{len(bc)}\n")
|
f.write(f"{len(bc)}\n")
|
||||||
for d0, d1 in bc.data:
|
for d0, d1 in bc.data:
|
||||||
time = self.export_time(study, d0)
|
f.write(f"{d1} {d0}\n")
|
||||||
f.write(f"{d1} {time}\n")
|
|
||||||
|
|
||||||
def _export_condav(self, study, repertory, qlog, name="0"):
|
def _export_condav(self, study, repertory, qlog, name="0"):
|
||||||
if qlog is not None:
|
if qlog is not None:
|
||||||
@@ -578,8 +577,7 @@ class Rubar3(CommandLineSolver):
|
|||||||
f.write(f"{d1} {d0}\n")
|
f.write(f"{d1} {d0}\n")
|
||||||
else:
|
else:
|
||||||
for d0, d1 in bc.data:
|
for d0, d1 in bc.data:
|
||||||
time = self.export_time(study, d0)
|
f.write(f"{d0} {d1}\n")
|
||||||
f.write(f"{time} {d1}\n")
|
|
||||||
|
|
||||||
def read_profil(self, study, fname, results, qlog=None, name="0"):
|
def read_profil(self, study, fname, results, qlog=None, name="0"):
|
||||||
logger.info(f"read: profil.{name}")
|
logger.info(f"read: profil.{name}")
|
||||||
|
|||||||
+54
-21
@@ -155,27 +155,60 @@ All right reserved.
|
|||||||
self.find(QLabel, "label_date").setEnabled(True)
|
self.find(QLabel, "label_date").setEnabled(True)
|
||||||
self.find(QDateTimeEdit, "dateTimeEdit_date").setEnabled(True)
|
self.find(QDateTimeEdit, "dateTimeEdit_date").setEnabled(True)
|
||||||
|
|
||||||
|
def _close_study_editors(self):
|
||||||
|
def close_children(parent):
|
||||||
|
for _, window in list(parent.sub_win_list):
|
||||||
|
if window is self:
|
||||||
|
continue
|
||||||
|
if not close_children(window):
|
||||||
|
return False
|
||||||
|
if not window.close():
|
||||||
|
return False
|
||||||
|
undo = getattr(window, "_undo_stack", None)
|
||||||
|
if undo is not None:
|
||||||
|
undo.clear()
|
||||||
|
window.deleteLater()
|
||||||
|
return True
|
||||||
|
|
||||||
|
return close_children(self.parent)
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
# if (self._study is not None
|
preserve_dates = False
|
||||||
# and self._study.time_system == "date"
|
if (self._study is not None
|
||||||
# and self.get_radio_button("radioButton_date")):
|
and self._study.time_system == "date"
|
||||||
# old_date = parse_datetime(self._study.date)
|
and self.get_radio_button("radioButton_date")):
|
||||||
# new_date = parse_datetime(self.get_datetime_edit("dateTimeEdit_date"))
|
old_date = parse_datetime(self._study.date)
|
||||||
# if new_date != old_date:
|
new_date = parse_datetime(self.get_datetime_edit("dateTimeEdit_date"))
|
||||||
# answer = QMessageBox.question(
|
if new_date != old_date:
|
||||||
# self,
|
dialog = QMessageBox(self)
|
||||||
# _translate("Study", "Change simulation start date?"),
|
dialog.setWindowTitle(_translate("Study", "Change simulation start date?"))
|
||||||
# _translate(
|
dialog.setText(_translate(
|
||||||
# "Study",
|
"Study", "How should the data dates change?"
|
||||||
# "All displayed data dates will shift with the simulation start."
|
))
|
||||||
# "\nTheir elapsed simulation times will remain unchanged."
|
dialog.setInformativeText(_translate(
|
||||||
# "\n\nPrevious start: {old}\nNew start: {new}"
|
"Study",
|
||||||
# ).format(old=format_datetime(old_date), new=format_datetime(new_date)),
|
"Previous start: {old}\nNew start: {new}\n\n"
|
||||||
# QMessageBox.Ok | QMessageBox.Cancel,
|
"Shift dates: keep elapsed simulation times unchanged.\n"
|
||||||
# QMessageBox.Cancel
|
"Keep dates: adjust elapsed times so calendar dates stay unchanged."
|
||||||
# )
|
).format(old=format_datetime(old_date), new=format_datetime(new_date)))
|
||||||
# if answer != QMessageBox.Ok:
|
shift = dialog.addButton(_translate("Study", "Shift dates"), QMessageBox.AcceptRole)
|
||||||
# return
|
keep = dialog.addButton(_translate("Study", "Keep dates"), QMessageBox.ActionRole)
|
||||||
|
cancel = dialog.addButton(QMessageBox.Cancel)
|
||||||
|
dialog.setDefaultButton(cancel)
|
||||||
|
dialog.exec()
|
||||||
|
if dialog.clickedButton() not in (shift, keep):
|
||||||
|
return
|
||||||
|
preserve_dates = dialog.clickedButton() is keep
|
||||||
|
|
||||||
|
if self._study is not None:
|
||||||
|
date_mode = self.get_radio_button("radioButton_date")
|
||||||
|
mode_changed = date_mode != (self._study.time_system == "date")
|
||||||
|
date_changed = date_mode and (
|
||||||
|
parse_datetime(self.get_datetime_edit("dateTimeEdit_date"))
|
||||||
|
!= parse_datetime(self._study.date)
|
||||||
|
)
|
||||||
|
if (mode_changed or date_changed) and not self._close_study_editors():
|
||||||
|
return
|
||||||
|
|
||||||
name = self.get_line_edit_text("lineEdit_name")
|
name = self.get_line_edit_text("lineEdit_name")
|
||||||
description = self.get_plaintext_edit_text("plainTextEdit_description")
|
description = self.get_plaintext_edit_text("plainTextEdit_description")
|
||||||
@@ -193,7 +226,7 @@ All right reserved.
|
|||||||
self._study.description = description
|
self._study.description = description
|
||||||
if self.get_radio_button("radioButton_date"):
|
if self.get_radio_button("radioButton_date"):
|
||||||
date = self.get_datetime_edit("dateTimeEdit_date")
|
date = self.get_datetime_edit("dateTimeEdit_date")
|
||||||
self._study.use_date(date)
|
self._study.change_start_date(date, preserve_dates=preserve_dates)
|
||||||
else:
|
else:
|
||||||
self._study.use_time()
|
self._study.use_time()
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user