From 2964591ac9cdd1036e6754a3dda57b88b245bad3 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Wed, 5 Aug 2026 15:48:20 +0200 Subject: [PATCH 01/18] Initial 05082026 --- .vscode/settings.json | 3 + test.json | 41 +++++++++++++ test.py | 41 +++++++++++++ timflow/steady/aquifer.py | 3 +- timflow/steady/element.py | 4 +- timflow/steady/export.py | 125 ++++++++++++++++++++++++++++++++++++++ timflow/steady/model.py | 30 ++++++++- 7 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 test.json create mode 100644 test.py create mode 100644 timflow/steady/export.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..4ec39be7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.enabled": false +} \ No newline at end of file diff --git a/test.json b/test.json new file mode 100644 index 00000000..a1d797ba --- /dev/null +++ b/test.json @@ -0,0 +1,41 @@ +{ + "_type": "ModelXsection", + "naq": 2, + "elementlist": [ + { + "_type": "HeadDiffLineSink1D", + "xls": -50.0, + "label": null + }, + { + "_type": "ConstantStar", + "hstar": 5, + "label": null + }, + { + "_type": "HeadDiffLineSink1D", + "xls": 50.0, + "label": null + }, + { + "_type": "FluxDiffLineSink1D", + "xls": -50.0, + "label": null + }, + { + "_type": "ConstantStar", + "hstar": 4.5, + "label": null + }, + { + "_type": "FluxDiffLineSink1D", + "xls": 50.0, + "label": null + }, + { + "_type": "ConstantStar", + "hstar": 4, + "label": null + } + ] +} \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 00000000..f6534977 --- /dev/null +++ b/test.py @@ -0,0 +1,41 @@ +import numpy as np + +import timflow.steady as tfs + +ml = tfs.ModelXsection(naq=2) +tfs.XsectionMaq( + ml, + x1=-np.inf, + x2=-50, + kaq=[1, 2], + z=[4, 3, 2, 1, 0], + c=[1000, 1000], + npor=0.3, + topboundary="semi", + hstar=5, +) +tfs.XsectionMaq( + ml, + x1=-50, + x2=50, + kaq=[1, 2], + z=[4, 3, 2, 1, 0], + c=[1000, 1000], + npor=0.3, + topboundary="semi", + hstar=4.5, +) +tfs.XsectionMaq( + ml, + x1=50, + x2=np.inf, + kaq=[1, 2], + z=[4, 3, 2, 1, 0], + c=[1000, 1000], + npor=0.3, + topboundary="semi", + hstar=4, +) +ml.solve() +print(ml.elementlist) +ml.to_json("./test.json") diff --git a/timflow/steady/aquifer.py b/timflow/steady/aquifer.py index 5f884ca1..2782f5fb 100644 --- a/timflow/steady/aquifer.py +++ b/timflow/steady/aquifer.py @@ -13,11 +13,12 @@ import pandas as pd from timflow.steady.constant import ConstantStar +from timflow.steady.export import ExportBase __all__ = ["Aquifer", "SimpleAquifer"] -class AquiferData: +class AquiferData(ExportBase): def __init__(self, model, kaq, c, z, npor, ltype, model3d=False): """Initialize aquifer data. diff --git a/timflow/steady/element.py b/timflow/steady/element.py index 8095f98e..dd74b2b1 100644 --- a/timflow/steady/element.py +++ b/timflow/steady/element.py @@ -11,10 +11,12 @@ def initialize(self): import numpy as np +from timflow.steady.export import ExportBase + __all__ = ["Element"] -class Element: +class Element(ExportBase): """Base class for all timflow.steady elements. Elements represent physical features in the aquifer system such as wells, diff --git a/timflow/steady/export.py b/timflow/steady/export.py new file mode 100644 index 00000000..f331d186 --- /dev/null +++ b/timflow/steady/export.py @@ -0,0 +1,125 @@ +import inspect +import json + + +# TODO Print logs for insight to where we get during run. +class ExportBase: + # Registry for all subclasses. + _registry = {} + + def __init_subclass__(cls) -> None: + """Add the subclass to the registry on creation.""" + cls._registry[cls.__name__] = cls + + def to_json(self, filepath) -> None: + """ + Write the contructor arguments and potential additional attributes to a JSON-file. + + :param filepath: Filepath to the to be created JSON-file. + """ + data = self.to_dict() + print("Test:") + print(data) + with open(filepath, "w") as f: + f.write(json.dumps(data, indent=4)) + + def to_dict(self): + """ + Collect the contructor arguments and potential additional attributes into a dict. + + :return: _description_ + """ + sig = inspect.signature(self.__init__) + data = {"_type": self.__class__.__name__} + for name in sig.parameters: + if name == "model": # reference to parent object + continue + # TODO Reference to other object, inhomogenities need to go to JSON + if name in ["aq", "aqin", "aqout"]: + continue + if name != "self": + value = getattr(self, name) + data[name] = self._serialize(value) + data.update(self.extra_to_dict()) + return data + + def extra_to_dict(self): + """Add the addition attributes to the dict. + + May be overloaded in the subclass. + + :return: Dict with addition parameters. + """ + return {} + + @classmethod + def from_json(cls, filepath) -> dict: + """ + Read the contructor arguments and potential addition attributes from a JSON-file. + + :param filepath: Filepath to the to be created JSON-file. + """ + with open(filepath, "r") as f: + data = json.loads(f) + return data + + @classmethod + def from_dict(cls, data: dict): + """Factory method to create an instance of this (sub)class. + + :param data: Dict with parameters + :return: Instance of this (sub)class. + """ + type_name = data.pop("_type") + subclass = cls._registry[type_name] + sig = inspect.signature(subclass.__init__) + constructor_args = {} + + for name in sig.parameters: + if name == "model": + constructor_args[name] = cls + if name != "self" and name in data: + constructor_args[name] = cls._deserialize(data.pop(name)) + obj = subclass(**constructor_args) + obj.extra_from_dict(data) + + return obj + + def extra_from_dict(self, data) -> None: + """Add the additional attributes to the (sub)class. + + May be overloaded in the subclasses. + + :param data: Dict with additional parameters. + """ + pass + + @classmethod + def _serialize(cls, value): + """Convert python objects to exportable types. + + :param value: Object for export. + :return: Object in exportable form. + """ + if isinstance(value, cls): + return value.to_dict() + + if isinstance(value, list): + return [cls._serialize(v) for v in value] + if isinstance(value, dict): + return {k: cls._serialize(v) for k, v in value.items()} + return value + + @classmethod + def _deserialize(cls, value): + """Convert a dict of values to the right python objects. + + :param value: Imported object + :return: Object as correct python-type. + """ + if isinstance(value, dict) and "_type" in value: + return cls.from_dict(value) + if isinstance(value, list): + return [cls._deserialize(v) for v in value] + if isinstance(value, dict): + return {k: cls._deserialize(v) for k, v in value.items()} diff --git a/timflow/steady/model.py b/timflow/steady/model.py index 30caf3f4..85c5668e 100644 --- a/timflow/steady/model.py +++ b/timflow/steady/model.py @@ -20,6 +20,7 @@ from timflow.steady.aquifer import Aquifer, SimpleAquifer from timflow.steady.aquifer_parameters import param_3d, param_maq from timflow.steady.constant import ConstantStar +from timflow.steady.export import ExportBase from timflow.steady.plots import PlotSteady from timflow.version import check_tqdm_parallel @@ -42,7 +43,7 @@ def _compute_velocity_mp(args): return i, vv -class Model: +class Model(ExportBase): """Create a model consisting of an arbitrary sequence of aquifers and leaky layers. Notes @@ -82,6 +83,32 @@ def __init__(self, kaq, z, c, npor, ltype, model3d=False): self.initialized = False + def extra_from_dict(self, data) -> None: + """Add the additional attributes to the (sub)class. + + May be overloaded in the subclasses. + + :param data: Dict with additional parameters. + """ + if "elementlist" in data: + for e in data.elementlist: + print(e) + self.add_element(e) + + def extra_to_dict(self): + """Add the addition attributes to the dict. + + May be overloaded in the subclass. + + :return: Dict with addition parameters. + """ + extra_data = {} + if self.elementlist != []: + extra_data.update( + {"elementlist": [e.to_dict() for e in self.elementlist]} + ) + return extra_data + def initialize(self): # remove inhomogeneity elements (they are added again) self.elementlist = [e for e in self.elementlist if not e.inhomelement] @@ -1097,6 +1124,7 @@ class ModelXsection(Model): """ def __init__(self, naq=1): + self.naq = naq self.elementlist = [] self.elementdict = {} # only elements that have a label self.aq = SimpleAquifer(self, naq) From 7c5d50f8b78a1b0ea2c324004fe1b441a3fe16c4 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Thu, 6 Aug 2026 13:44:53 +0200 Subject: [PATCH 02/18] testing --- test.json | 123 +++++++++++++++++++++++++++++- test_in.py | 6 ++ test.py => test_out.py | 1 - timflow/steady/export.py | 41 ++++++---- timflow/steady/inhomogeneity1d.py | 2 + timflow/steady/model.py | 19 +++-- 6 files changed, 169 insertions(+), 23 deletions(-) create mode 100644 test_in.py rename test.py => test_out.py (96%) diff --git a/test.json b/test.json index a1d797ba..04c579e9 100644 --- a/test.json +++ b/test.json @@ -37,5 +37,126 @@ "hstar": 4, "label": null } - ] + ], + "aq": { + "_type": "SimpleAquifer", + "ml": null, + "naq": 2 + }, + "inhomdict": { + "inhom00": { + "_type": "XsectionMaq", + "x1": -Infinity, + "x2": -50, + "kaq": { + "ndarray": [ + 1.0, + 2.0 + ] + }, + "z": { + "ndarray": [ + 4, + 3, + 2, + 1, + 0 + ] + }, + "c": { + "ndarray": [ + 1000.0, + 1000.0 + ] + }, + "npor": { + "ndarray": [ + 0.3, + 0.3, + 0.3, + 0.3 + ] + }, + "topboundary": "semi", + "hstar": 5, + "N": null, + "name": "inhom00" + }, + "inhom01": { + "_type": "XsectionMaq", + "x1": -50, + "x2": 50, + "kaq": { + "ndarray": [ + 1.0, + 2.0 + ] + }, + "z": { + "ndarray": [ + 4, + 3, + 2, + 1, + 0 + ] + }, + "c": { + "ndarray": [ + 1000.0, + 1000.0 + ] + }, + "npor": { + "ndarray": [ + 0.3, + 0.3, + 0.3, + 0.3 + ] + }, + "topboundary": "semi", + "hstar": 4.5, + "N": null, + "name": "inhom01" + }, + "inhom02": { + "_type": "XsectionMaq", + "x1": 50, + "x2": Infinity, + "kaq": { + "ndarray": [ + 1.0, + 2.0 + ] + }, + "z": { + "ndarray": [ + 4, + 3, + 2, + 1, + 0 + ] + }, + "c": { + "ndarray": [ + 1000.0, + 1000.0 + ] + }, + "npor": { + "ndarray": [ + 0.3, + 0.3, + 0.3, + 0.3 + ] + }, + "topboundary": "semi", + "hstar": 4, + "N": null, + "name": "inhom02" + } + } } \ No newline at end of file diff --git a/test_in.py b/test_in.py new file mode 100644 index 00000000..2b946146 --- /dev/null +++ b/test_in.py @@ -0,0 +1,6 @@ +import timflow.steady as tfs + +ml = tfs.ModelXsection.from_json("./test.json") +print(ml.elementlist) +ml.initialize() +# ml.solve() \ No newline at end of file diff --git a/test.py b/test_out.py similarity index 96% rename from test.py rename to test_out.py index f6534977..a5e6efb5 100644 --- a/test.py +++ b/test_out.py @@ -37,5 +37,4 @@ hstar=4, ) ml.solve() -print(ml.elementlist) ml.to_json("./test.json") diff --git a/timflow/steady/export.py b/timflow/steady/export.py index f331d186..acafdec5 100644 --- a/timflow/steady/export.py +++ b/timflow/steady/export.py @@ -1,11 +1,16 @@ import inspect import json +from typing import Any + +from numpy import array, ndarray # TODO Print logs for insight to where we get during run. class ExportBase: # Registry for all subclasses. _registry = {} + # Storage for model object + _model = None def __init_subclass__(cls) -> None: """Add the subclass to the registry on creation.""" @@ -18,8 +23,6 @@ def to_json(self, filepath) -> None: :param filepath: Filepath to the to be created JSON-file. """ data = self.to_dict() - print("Test:") - print(data) with open(filepath, "w") as f: f.write(json.dumps(data, indent=4)) @@ -32,18 +35,17 @@ def to_dict(self): sig = inspect.signature(self.__init__) data = {"_type": self.__class__.__name__} for name in sig.parameters: - if name == "model": # reference to parent object + if name == ("model" or "ml"): # reference to parent object continue - # TODO Reference to other object, inhomogenities need to go to JSON if name in ["aq", "aqin", "aqout"]: continue if name != "self": - value = getattr(self, name) + value = getattr(self, name, None) data[name] = self._serialize(value) data.update(self.extra_to_dict()) return data - def extra_to_dict(self): + def extra_to_dict(self) -> dict[Any, Any]: """Add the addition attributes to the dict. May be overloaded in the subclass. @@ -53,15 +55,17 @@ def extra_to_dict(self): return {} @classmethod - def from_json(cls, filepath) -> dict: + def from_json(cls, filepath): """ Read the contructor arguments and potential addition attributes from a JSON-file. :param filepath: Filepath to the to be created JSON-file. """ with open(filepath, "r") as f: - data = json.loads(f) - return data + data = json.load(f) + obj = cls.from_dict(data) + obj.extra_from_dict(data) + return obj @classmethod def from_dict(cls, data: dict): @@ -72,17 +76,20 @@ def from_dict(cls, data: dict): """ type_name = data.pop("_type") subclass = cls._registry[type_name] + print(subclass) sig = inspect.signature(subclass.__init__) constructor_args = {} - + for name in sig.parameters: - if name == "model": - constructor_args[name] = cls + if name == ("model" or "ml"): + constructor_args[name] = cls._model + if name == ("aq", "aqin", "aqout"): + constructor_args[name] = cls._model.aq if name != "self" and name in data: constructor_args[name] = cls._deserialize(data.pop(name)) obj = subclass(**constructor_args) - obj.extra_from_dict(data) - + if cls._model is None: + cls._model = obj return obj def extra_from_dict(self, data) -> None: @@ -103,11 +110,12 @@ def _serialize(cls, value): """ if isinstance(value, cls): return value.to_dict() - if isinstance(value, list): return [cls._serialize(v) for v in value] if isinstance(value, dict): return {k: cls._serialize(v) for k, v in value.items()} + if isinstance(value, ndarray): + return {"ndarray": value.tolist()} return value @classmethod @@ -119,7 +127,10 @@ def _deserialize(cls, value): """ if isinstance(value, dict) and "_type" in value: return cls.from_dict(value) + if isinstance(value, dict) and "ndarray" in value: + return array(value["ndarray"]) if isinstance(value, list): return [cls._deserialize(v) for v in value] if isinstance(value, dict): return {k: cls._deserialize(v) for k, v in value.items()} + return value diff --git a/timflow/steady/inhomogeneity1d.py b/timflow/steady/inhomogeneity1d.py index 6baa2e0c..41f1f315 100644 --- a/timflow/steady/inhomogeneity1d.py +++ b/timflow/steady/inhomogeneity1d.py @@ -379,6 +379,7 @@ def __init__( N=None, name=None, ): + self.topboundary = topboundary if c is None: c = [] if z is None: @@ -459,6 +460,7 @@ def __init__( N=None, name=None, ): + self.topboundary = topboundary if z is None: z = [1, 0] ( diff --git a/timflow/steady/model.py b/timflow/steady/model.py index 85c5668e..99686fe5 100644 --- a/timflow/steady/model.py +++ b/timflow/steady/model.py @@ -90,10 +90,13 @@ def extra_from_dict(self, data) -> None: :param data: Dict with additional parameters. """ + if "inhomdict" in data: + if self.aq is not None: + for name, inhom in data["inhomdict"].items(): + self.aq.inhomdict.update({name: self.from_dict(inhom)}) if "elementlist" in data: - for e in data.elementlist: - print(e) - self.add_element(e) + for e in data["elementlist"]: + self.elementlist.append(self.from_dict(e)) def extra_to_dict(self): """Add the addition attributes to the dict. @@ -104,9 +107,12 @@ def extra_to_dict(self): """ extra_data = {} if self.elementlist != []: - extra_data.update( - {"elementlist": [e.to_dict() for e in self.elementlist]} - ) + extra_data.update({"elementlist": [e.to_dict() for e in self.elementlist]}) + if self.aq is not None: + if self.aq.inhomdict != {}: + extra_data.update( + {"inhomdict": {k: v.to_dict() for k, v in self.aq.inhomdict.items()}} + ) return extra_data def initialize(self): @@ -1009,6 +1015,7 @@ class ModelMaq(Model): """ def __init__(self, kaq=1, z=None, c=None, npor=0.3, topboundary="conf", hstar=None): + self.topboundary = topboundary if c is None: c = [] if z is None: From 752a680d0545d63494934293aa4efa03acb47e2f Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Thu, 6 Aug 2026 14:29:44 +0200 Subject: [PATCH 03/18] werkend voorbeeld --- test.json | 5 ----- test_in.py | 16 +++++++++++++--- test_out.py | 11 +++++++++++ timflow/steady/export.py | 14 ++------------ timflow/steady/model.py | 15 --------------- 5 files changed, 26 insertions(+), 35 deletions(-) diff --git a/test.json b/test.json index 04c579e9..2951052c 100644 --- a/test.json +++ b/test.json @@ -38,11 +38,6 @@ "label": null } ], - "aq": { - "_type": "SimpleAquifer", - "ml": null, - "naq": 2 - }, "inhomdict": { "inhom00": { "_type": "XsectionMaq", diff --git a/test_in.py b/test_in.py index 2b946146..41fa1c1e 100644 --- a/test_in.py +++ b/test_in.py @@ -1,6 +1,16 @@ +import matplotlib.pyplot as plt +import numpy as np + import timflow.steady as tfs ml = tfs.ModelXsection.from_json("./test.json") -print(ml.elementlist) -ml.initialize() -# ml.solve() \ No newline at end of file +ml.solve() + +x = np.linspace(-200, 200, 101) +h = ml.headalongline(x, np.zeros(101)) +plt.plot(x, h[0], label="layer 0") +plt.plot(x, h[1], label="layer 1") +plt.xlabel("x (m)") +plt.ylabel("head (m)") +plt.legend(loc="best") +plt.grid() diff --git a/test_out.py b/test_out.py index a5e6efb5..977d558b 100644 --- a/test_out.py +++ b/test_out.py @@ -1,3 +1,4 @@ +import matplotlib.pyplot as plt import numpy as np import timflow.steady as tfs @@ -37,4 +38,14 @@ hstar=4, ) ml.solve() + +x = np.linspace(-200, 200, 101) +h = ml.headalongline(x, np.zeros(101)) +plt.plot(x, h[0], label="layer 0") +plt.plot(x, h[1], label="layer 1") +plt.xlabel("x (m)") +plt.ylabel("head (m)") +plt.legend(loc="best") +plt.grid() + ml.to_json("./test.json") diff --git a/timflow/steady/export.py b/timflow/steady/export.py index acafdec5..52c8df5a 100644 --- a/timflow/steady/export.py +++ b/timflow/steady/export.py @@ -64,7 +64,8 @@ def from_json(cls, filepath): with open(filepath, "r") as f: data = json.load(f) obj = cls.from_dict(data) - obj.extra_from_dict(data) + for _,v in data["inhomdict"].items(): + cls.from_dict(v) return obj @classmethod @@ -83,8 +84,6 @@ def from_dict(cls, data: dict): for name in sig.parameters: if name == ("model" or "ml"): constructor_args[name] = cls._model - if name == ("aq", "aqin", "aqout"): - constructor_args[name] = cls._model.aq if name != "self" and name in data: constructor_args[name] = cls._deserialize(data.pop(name)) obj = subclass(**constructor_args) @@ -92,15 +91,6 @@ def from_dict(cls, data: dict): cls._model = obj return obj - def extra_from_dict(self, data) -> None: - """Add the additional attributes to the (sub)class. - - May be overloaded in the subclasses. - - :param data: Dict with additional parameters. - """ - pass - @classmethod def _serialize(cls, value): """Convert python objects to exportable types. diff --git a/timflow/steady/model.py b/timflow/steady/model.py index 99686fe5..1e8638a5 100644 --- a/timflow/steady/model.py +++ b/timflow/steady/model.py @@ -83,21 +83,6 @@ def __init__(self, kaq, z, c, npor, ltype, model3d=False): self.initialized = False - def extra_from_dict(self, data) -> None: - """Add the additional attributes to the (sub)class. - - May be overloaded in the subclasses. - - :param data: Dict with additional parameters. - """ - if "inhomdict" in data: - if self.aq is not None: - for name, inhom in data["inhomdict"].items(): - self.aq.inhomdict.update({name: self.from_dict(inhom)}) - if "elementlist" in data: - for e in data["elementlist"]: - self.elementlist.append(self.from_dict(e)) - def extra_to_dict(self): """Add the addition attributes to the dict. From f6d6495de579786c6c333469f9912a111f65e220 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Thu, 6 Aug 2026 15:03:25 +0200 Subject: [PATCH 04/18] inhomogenen werken, losse elementen nog niet --- test.json | 37 ------------------------------------- test_in.py | 24 ++++++++++++++++++++++++ test_out.py | 39 +++++++++++++++++++++++++++++++-------- timflow/steady/export.py | 13 +++++++++---- timflow/steady/model.py | 8 ++++---- 5 files changed, 68 insertions(+), 53 deletions(-) diff --git a/test.json b/test.json index 2951052c..77eb6262 100644 --- a/test.json +++ b/test.json @@ -1,43 +1,6 @@ { "_type": "ModelXsection", "naq": 2, - "elementlist": [ - { - "_type": "HeadDiffLineSink1D", - "xls": -50.0, - "label": null - }, - { - "_type": "ConstantStar", - "hstar": 5, - "label": null - }, - { - "_type": "HeadDiffLineSink1D", - "xls": 50.0, - "label": null - }, - { - "_type": "FluxDiffLineSink1D", - "xls": -50.0, - "label": null - }, - { - "_type": "ConstantStar", - "hstar": 4.5, - "label": null - }, - { - "_type": "FluxDiffLineSink1D", - "xls": 50.0, - "label": null - }, - { - "_type": "ConstantStar", - "hstar": 4, - "label": null - } - ], "inhomdict": { "inhom00": { "_type": "XsectionMaq", diff --git a/test_in.py b/test_in.py index 41fa1c1e..cec7fdaf 100644 --- a/test_in.py +++ b/test_in.py @@ -14,3 +14,27 @@ plt.ylabel("head (m)") plt.legend(loc="best") plt.grid() + +# x = np.linspace(-100, 100, 101) +# h = ml.headalongline(x, np.zeros_like(x)) +# Qx, _ = ml.disvecalongline(x, np.zeros_like(x)) + +# plt.figure(figsize=(10, 3)) +# plt.subplot(121) +# plt.title("head") +# plt.plot(x, h[0], label="layer 0") +# plt.plot(x, h[1], label="layer 1") +# plt.plot(x, h[2], label="layer 2") +# plt.xlabel("x (m)") +# plt.ylabel("head (m)") +# plt.legend(loc="best") +# plt.grid() +# plt.subplot(122) +# plt.title("Qx") +# plt.plot(x, Qx[0], label="layer 0") +# plt.plot(x, Qx[1], label="layer 1") +# plt.plot(x, Qx[2], label="layer 2") +# plt.xlabel("x (m)") +# plt.ylabel("$Q_x$ (m$^2$/d)") +# plt.legend(loc="best") +# plt.grid() \ No newline at end of file diff --git a/test_out.py b/test_out.py index 977d558b..06eb9e49 100644 --- a/test_out.py +++ b/test_out.py @@ -37,15 +37,38 @@ topboundary="semi", hstar=4, ) + ml.solve() -x = np.linspace(-200, 200, 101) -h = ml.headalongline(x, np.zeros(101)) -plt.plot(x, h[0], label="layer 0") -plt.plot(x, h[1], label="layer 1") -plt.xlabel("x (m)") -plt.ylabel("head (m)") -plt.legend(loc="best") -plt.grid() + +# ml = tfs.ModelMaq(kaq=[1, 2, 4], z=[5, 4, 3, 2, 1, 0], c=[5000, 1000]) +# uf = tfs.Uflow(ml, 0.002, 0) +# rf = tfs.Constant(ml, 100, 0, 20) +# ld1 = tfs.ImpermeableWall1D(ml, xld=0, layers=[0, 1]) + +# ml.solve() +# x = np.linspace(-100, 100, 101) +# h = ml.headalongline(x, np.zeros_like(x)) +# Qx, _ = ml.disvecalongline(x, np.zeros_like(x)) + +# plt.figure(figsize=(10, 3)) +# plt.subplot(121) +# plt.title("head") +# plt.plot(x, h[0], label="layer 0") +# plt.plot(x, h[1], label="layer 1") +# plt.plot(x, h[2], label="layer 2") +# plt.xlabel("x (m)") +# plt.ylabel("head (m)") +# plt.legend(loc="best") +# plt.grid() +# plt.subplot(122) +# plt.title("Qx") +# plt.plot(x, Qx[0], label="layer 0") +# plt.plot(x, Qx[1], label="layer 1") +# plt.plot(x, Qx[2], label="layer 2") +# plt.xlabel("x (m)") +# plt.ylabel("$Q_x$ (m$^2$/d)") +# plt.legend(loc="best") +# plt.grid() ml.to_json("./test.json") diff --git a/timflow/steady/export.py b/timflow/steady/export.py index 52c8df5a..825ff185 100644 --- a/timflow/steady/export.py +++ b/timflow/steady/export.py @@ -5,7 +5,6 @@ from numpy import array, ndarray -# TODO Print logs for insight to where we get during run. class ExportBase: # Registry for all subclasses. _registry = {} @@ -64,8 +63,15 @@ def from_json(cls, filepath): with open(filepath, "r") as f: data = json.load(f) obj = cls.from_dict(data) - for _,v in data["inhomdict"].items(): - cls.from_dict(v) + if "inhomdict" in data: + for _,v in data["inhomdict"].items(): + cls.from_dict(v) + if "elementlist" in data: + for e in data["elementlist"]: + try: + cls.from_dict(e) + except AttributeError: + pass return obj @classmethod @@ -77,7 +83,6 @@ def from_dict(cls, data: dict): """ type_name = data.pop("_type") subclass = cls._registry[type_name] - print(subclass) sig = inspect.signature(subclass.__init__) constructor_args = {} diff --git a/timflow/steady/model.py b/timflow/steady/model.py index 1e8638a5..662cc9b1 100644 --- a/timflow/steady/model.py +++ b/timflow/steady/model.py @@ -84,20 +84,20 @@ def __init__(self, kaq, z, c, npor, ltype, model3d=False): self.initialized = False def extra_to_dict(self): - """Add the addition attributes to the dict. + """Add the additional attributes to the dict. - May be overloaded in the subclass. + Adds the inhomogenities to the export dict. :return: Dict with addition parameters. """ extra_data = {} - if self.elementlist != []: - extra_data.update({"elementlist": [e.to_dict() for e in self.elementlist]}) if self.aq is not None: if self.aq.inhomdict != {}: extra_data.update( {"inhomdict": {k: v.to_dict() for k, v in self.aq.inhomdict.items()}} ) + # if self.elementlist != []: + # extra_data.update({"elementlist": [e.to_dict() for e in self.elementlist]}) return extra_data def initialize(self): From 8e72a14bfb8c4e249717da774c7cdda5001b5187 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Thu, 6 Aug 2026 16:12:30 +0200 Subject: [PATCH 05/18] wip 06082026 --- test.json | 3 ++- test_in.py | 23 ----------------------- test_out.py | 33 +-------------------------------- timflow/steady/export.py | 10 ++++------ timflow/steady/model.py | 14 ++++++++++++-- 5 files changed, 19 insertions(+), 64 deletions(-) diff --git a/test.json b/test.json index 77eb6262..59a1040a 100644 --- a/test.json +++ b/test.json @@ -116,5 +116,6 @@ "N": null, "name": "inhom02" } - } + }, + "elementlist": [] } \ No newline at end of file diff --git a/test_in.py b/test_in.py index cec7fdaf..faaeef26 100644 --- a/test_in.py +++ b/test_in.py @@ -15,26 +15,3 @@ plt.legend(loc="best") plt.grid() -# x = np.linspace(-100, 100, 101) -# h = ml.headalongline(x, np.zeros_like(x)) -# Qx, _ = ml.disvecalongline(x, np.zeros_like(x)) - -# plt.figure(figsize=(10, 3)) -# plt.subplot(121) -# plt.title("head") -# plt.plot(x, h[0], label="layer 0") -# plt.plot(x, h[1], label="layer 1") -# plt.plot(x, h[2], label="layer 2") -# plt.xlabel("x (m)") -# plt.ylabel("head (m)") -# plt.legend(loc="best") -# plt.grid() -# plt.subplot(122) -# plt.title("Qx") -# plt.plot(x, Qx[0], label="layer 0") -# plt.plot(x, Qx[1], label="layer 1") -# plt.plot(x, Qx[2], label="layer 2") -# plt.xlabel("x (m)") -# plt.ylabel("$Q_x$ (m$^2$/d)") -# plt.legend(loc="best") -# plt.grid() \ No newline at end of file diff --git a/test_out.py b/test_out.py index 06eb9e49..58afbfcc 100644 --- a/test_out.py +++ b/test_out.py @@ -40,35 +40,4 @@ ml.solve() - -# ml = tfs.ModelMaq(kaq=[1, 2, 4], z=[5, 4, 3, 2, 1, 0], c=[5000, 1000]) -# uf = tfs.Uflow(ml, 0.002, 0) -# rf = tfs.Constant(ml, 100, 0, 20) -# ld1 = tfs.ImpermeableWall1D(ml, xld=0, layers=[0, 1]) - -# ml.solve() -# x = np.linspace(-100, 100, 101) -# h = ml.headalongline(x, np.zeros_like(x)) -# Qx, _ = ml.disvecalongline(x, np.zeros_like(x)) - -# plt.figure(figsize=(10, 3)) -# plt.subplot(121) -# plt.title("head") -# plt.plot(x, h[0], label="layer 0") -# plt.plot(x, h[1], label="layer 1") -# plt.plot(x, h[2], label="layer 2") -# plt.xlabel("x (m)") -# plt.ylabel("head (m)") -# plt.legend(loc="best") -# plt.grid() -# plt.subplot(122) -# plt.title("Qx") -# plt.plot(x, Qx[0], label="layer 0") -# plt.plot(x, Qx[1], label="layer 1") -# plt.plot(x, Qx[2], label="layer 2") -# plt.xlabel("x (m)") -# plt.ylabel("$Q_x$ (m$^2$/d)") -# plt.legend(loc="best") -# plt.grid() - -ml.to_json("./test.json") +ml.to_json("./test.json") \ No newline at end of file diff --git a/timflow/steady/export.py b/timflow/steady/export.py index 825ff185..ca3f97f4 100644 --- a/timflow/steady/export.py +++ b/timflow/steady/export.py @@ -64,14 +64,12 @@ def from_json(cls, filepath): data = json.load(f) obj = cls.from_dict(data) if "inhomdict" in data: - for _,v in data["inhomdict"].items(): + for v in data["inhomdict"].values(): cls.from_dict(v) if "elementlist" in data: for e in data["elementlist"]: - try: - cls.from_dict(e) - except AttributeError: - pass + obj.aq.add_element(cls.from_dict(e)) + return obj @classmethod @@ -85,7 +83,7 @@ def from_dict(cls, data: dict): subclass = cls._registry[type_name] sig = inspect.signature(subclass.__init__) constructor_args = {} - + for name in sig.parameters: if name == ("model" or "ml"): constructor_args[name] = cls._model diff --git a/timflow/steady/model.py b/timflow/steady/model.py index 662cc9b1..fc97f8b9 100644 --- a/timflow/steady/model.py +++ b/timflow/steady/model.py @@ -96,8 +96,18 @@ def extra_to_dict(self): extra_data.update( {"inhomdict": {k: v.to_dict() for k, v in self.aq.inhomdict.items()}} ) - # if self.elementlist != []: - # extra_data.update({"elementlist": [e.to_dict() for e in self.elementlist]}) + if self.elementlist != []: + no_export_list = ["HeadDiffLineSink1D", "FluxDiffLineSink1D", "ConstantStar"] + + extra_data.update( + { + "elementlist": [ + e.to_dict() + for e in self.elementlist + if e.__class__.__name__ not in no_export_list + ] + } + ) return extra_data def initialize(self): From e39324287c95265f5c6c5638e89dabbb0f6d98b6 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Fri, 7 Aug 2026 08:48:29 +0200 Subject: [PATCH 06/18] Rework export and import with __new__ --- test.json | 231 +++++++++++++++++++-------------------- test_out.py | 2 +- timflow/steady/export.py | 33 ++++-- timflow/steady/model.py | 27 ----- 4 files changed, 140 insertions(+), 153 deletions(-) diff --git a/test.json b/test.json index 59a1040a..013c1ef8 100644 --- a/test.json +++ b/test.json @@ -1,121 +1,120 @@ { - "_type": "ModelXsection", - "naq": 2, - "inhomdict": { - "inhom00": { - "_type": "XsectionMaq", - "x1": -Infinity, - "x2": -50, - "kaq": { - "ndarray": [ - 1.0, - 2.0 - ] - }, - "z": { - "ndarray": [ - 4, - 3, - 2, - 1, - 0 - ] - }, - "c": { - "ndarray": [ - 1000.0, - 1000.0 - ] - }, - "npor": { - "ndarray": [ - 0.3, - 0.3, - 0.3, - 0.3 - ] - }, - "topboundary": "semi", - "hstar": 5, - "N": null, - "name": "inhom00" + "object0": { + "_type": "ModelXsection", + "naq": 2 + }, + "object1": { + "_type": "XsectionMaq", + "x1": -Infinity, + "x2": -50, + "kaq": { + "ndarray": [ + 1.0, + 2.0 + ] + }, + "z": { + "ndarray": [ + 4, + 3, + 2, + 1, + 0 + ] + }, + "c": { + "ndarray": [ + 1000.0, + 1000.0 + ] + }, + "npor": { + "ndarray": [ + 0.3, + 0.3, + 0.3, + 0.3 + ] + }, + "topboundary": "semi", + "hstar": 5, + "N": null, + "name": "inhom00" + }, + "object2": { + "_type": "XsectionMaq", + "x1": -50, + "x2": 50, + "kaq": { + "ndarray": [ + 1.0, + 2.0 + ] }, - "inhom01": { - "_type": "XsectionMaq", - "x1": -50, - "x2": 50, - "kaq": { - "ndarray": [ - 1.0, - 2.0 - ] - }, - "z": { - "ndarray": [ - 4, - 3, - 2, - 1, - 0 - ] - }, - "c": { - "ndarray": [ - 1000.0, - 1000.0 - ] - }, - "npor": { - "ndarray": [ - 0.3, - 0.3, - 0.3, - 0.3 - ] - }, - "topboundary": "semi", - "hstar": 4.5, - "N": null, - "name": "inhom01" + "z": { + "ndarray": [ + 4, + 3, + 2, + 1, + 0 + ] }, - "inhom02": { - "_type": "XsectionMaq", - "x1": 50, - "x2": Infinity, - "kaq": { - "ndarray": [ - 1.0, - 2.0 - ] - }, - "z": { - "ndarray": [ - 4, - 3, - 2, - 1, - 0 - ] - }, - "c": { - "ndarray": [ - 1000.0, - 1000.0 - ] - }, - "npor": { - "ndarray": [ - 0.3, - 0.3, - 0.3, - 0.3 - ] - }, - "topboundary": "semi", - "hstar": 4, - "N": null, - "name": "inhom02" - } + "c": { + "ndarray": [ + 1000.0, + 1000.0 + ] + }, + "npor": { + "ndarray": [ + 0.3, + 0.3, + 0.3, + 0.3 + ] + }, + "topboundary": "semi", + "hstar": 4.5, + "N": null, + "name": "inhom01" }, - "elementlist": [] + "object3": { + "_type": "XsectionMaq", + "x1": 50, + "x2": Infinity, + "kaq": { + "ndarray": [ + 1.0, + 2.0 + ] + }, + "z": { + "ndarray": [ + 4, + 3, + 2, + 1, + 0 + ] + }, + "c": { + "ndarray": [ + 1000.0, + 1000.0 + ] + }, + "npor": { + "ndarray": [ + 0.3, + 0.3, + 0.3, + 0.3 + ] + }, + "topboundary": "semi", + "hstar": 4, + "N": null, + "name": "inhom02" + } } \ No newline at end of file diff --git a/test_out.py b/test_out.py index 58afbfcc..0a0de70e 100644 --- a/test_out.py +++ b/test_out.py @@ -39,5 +39,5 @@ ) ml.solve() - +print(ml._obj_list) ml.to_json("./test.json") \ No newline at end of file diff --git a/timflow/steady/export.py b/timflow/steady/export.py index ca3f97f4..a3bebe0f 100644 --- a/timflow/steady/export.py +++ b/timflow/steady/export.py @@ -3,13 +3,24 @@ from typing import Any from numpy import array, ndarray +from typing_extensions import Self class ExportBase: - # Registry for all subclasses. + # Registry for all subclasses. _registry = {} # Storage for model object _model = None + # Registry for all created objects + _obj_list = [] + + def __new__(cls, *args, **kwargs) -> Self: + instance = super().__new__(cls) + frame = inspect.currentframe() + caller = frame.f_back + if caller.f_code.co_name == "": + cls._obj_list.append(instance) + return instance def __init_subclass__(cls) -> None: """Add the subclass to the registry on creation.""" @@ -21,7 +32,11 @@ def to_json(self, filepath) -> None: :param filepath: Filepath to the to be created JSON-file. """ - data = self.to_dict() + data = {} + i = 0 + for obj in self._obj_list: + data.update({f"object{i}": obj.to_dict()}) + i += 1 with open(filepath, "w") as f: f.write(json.dumps(data, indent=4)) @@ -60,16 +75,16 @@ def from_json(cls, filepath): :param filepath: Filepath to the to be created JSON-file. """ + obj = None with open(filepath, "r") as f: data = json.load(f) - obj = cls.from_dict(data) - if "inhomdict" in data: - for v in data["inhomdict"].values(): + for k, v in data.items(): + if k == "object0": + obj = cls.from_dict(v) + else: cls.from_dict(v) - if "elementlist" in data: - for e in data["elementlist"]: - obj.aq.add_element(cls.from_dict(e)) - + if obj is None: + raise ImportError return obj @classmethod diff --git a/timflow/steady/model.py b/timflow/steady/model.py index fc97f8b9..109aae92 100644 --- a/timflow/steady/model.py +++ b/timflow/steady/model.py @@ -83,33 +83,6 @@ def __init__(self, kaq, z, c, npor, ltype, model3d=False): self.initialized = False - def extra_to_dict(self): - """Add the additional attributes to the dict. - - Adds the inhomogenities to the export dict. - - :return: Dict with addition parameters. - """ - extra_data = {} - if self.aq is not None: - if self.aq.inhomdict != {}: - extra_data.update( - {"inhomdict": {k: v.to_dict() for k, v in self.aq.inhomdict.items()}} - ) - if self.elementlist != []: - no_export_list = ["HeadDiffLineSink1D", "FluxDiffLineSink1D", "ConstantStar"] - - extra_data.update( - { - "elementlist": [ - e.to_dict() - for e in self.elementlist - if e.__class__.__name__ not in no_export_list - ] - } - ) - return extra_data - def initialize(self): # remove inhomogeneity elements (they are added again) self.elementlist = [e for e in self.elementlist if not e.inhomelement] From 0917c41c9c8a341b159b33ea628dedd0b7fdfabf Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Fri, 7 Aug 2026 09:39:04 +0200 Subject: [PATCH 07/18] Cleanup of test files --- test.json | 120 ----------------------- test_in.py | 17 ---- test_out.py | 43 -------- timflow/steady/aquifer.py | 4 +- timflow/steady/{export.py => base_io.py} | 31 +++--- timflow/steady/constant.py | 1 + timflow/steady/element.py | 4 +- timflow/steady/model.py | 4 +- 8 files changed, 20 insertions(+), 204 deletions(-) delete mode 100644 test.json delete mode 100644 test_in.py delete mode 100644 test_out.py rename timflow/steady/{export.py => base_io.py} (86%) diff --git a/test.json b/test.json deleted file mode 100644 index 013c1ef8..00000000 --- a/test.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "object0": { - "_type": "ModelXsection", - "naq": 2 - }, - "object1": { - "_type": "XsectionMaq", - "x1": -Infinity, - "x2": -50, - "kaq": { - "ndarray": [ - 1.0, - 2.0 - ] - }, - "z": { - "ndarray": [ - 4, - 3, - 2, - 1, - 0 - ] - }, - "c": { - "ndarray": [ - 1000.0, - 1000.0 - ] - }, - "npor": { - "ndarray": [ - 0.3, - 0.3, - 0.3, - 0.3 - ] - }, - "topboundary": "semi", - "hstar": 5, - "N": null, - "name": "inhom00" - }, - "object2": { - "_type": "XsectionMaq", - "x1": -50, - "x2": 50, - "kaq": { - "ndarray": [ - 1.0, - 2.0 - ] - }, - "z": { - "ndarray": [ - 4, - 3, - 2, - 1, - 0 - ] - }, - "c": { - "ndarray": [ - 1000.0, - 1000.0 - ] - }, - "npor": { - "ndarray": [ - 0.3, - 0.3, - 0.3, - 0.3 - ] - }, - "topboundary": "semi", - "hstar": 4.5, - "N": null, - "name": "inhom01" - }, - "object3": { - "_type": "XsectionMaq", - "x1": 50, - "x2": Infinity, - "kaq": { - "ndarray": [ - 1.0, - 2.0 - ] - }, - "z": { - "ndarray": [ - 4, - 3, - 2, - 1, - 0 - ] - }, - "c": { - "ndarray": [ - 1000.0, - 1000.0 - ] - }, - "npor": { - "ndarray": [ - 0.3, - 0.3, - 0.3, - 0.3 - ] - }, - "topboundary": "semi", - "hstar": 4, - "N": null, - "name": "inhom02" - } -} \ No newline at end of file diff --git a/test_in.py b/test_in.py deleted file mode 100644 index faaeef26..00000000 --- a/test_in.py +++ /dev/null @@ -1,17 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np - -import timflow.steady as tfs - -ml = tfs.ModelXsection.from_json("./test.json") -ml.solve() - -x = np.linspace(-200, 200, 101) -h = ml.headalongline(x, np.zeros(101)) -plt.plot(x, h[0], label="layer 0") -plt.plot(x, h[1], label="layer 1") -plt.xlabel("x (m)") -plt.ylabel("head (m)") -plt.legend(loc="best") -plt.grid() - diff --git a/test_out.py b/test_out.py deleted file mode 100644 index 0a0de70e..00000000 --- a/test_out.py +++ /dev/null @@ -1,43 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np - -import timflow.steady as tfs - -ml = tfs.ModelXsection(naq=2) -tfs.XsectionMaq( - ml, - x1=-np.inf, - x2=-50, - kaq=[1, 2], - z=[4, 3, 2, 1, 0], - c=[1000, 1000], - npor=0.3, - topboundary="semi", - hstar=5, -) -tfs.XsectionMaq( - ml, - x1=-50, - x2=50, - kaq=[1, 2], - z=[4, 3, 2, 1, 0], - c=[1000, 1000], - npor=0.3, - topboundary="semi", - hstar=4.5, -) -tfs.XsectionMaq( - ml, - x1=50, - x2=np.inf, - kaq=[1, 2], - z=[4, 3, 2, 1, 0], - c=[1000, 1000], - npor=0.3, - topboundary="semi", - hstar=4, -) - -ml.solve() -print(ml._obj_list) -ml.to_json("./test.json") \ No newline at end of file diff --git a/timflow/steady/aquifer.py b/timflow/steady/aquifer.py index 2782f5fb..008839ab 100644 --- a/timflow/steady/aquifer.py +++ b/timflow/steady/aquifer.py @@ -13,12 +13,12 @@ import pandas as pd from timflow.steady.constant import ConstantStar -from timflow.steady.export import ExportBase +from timflow.steady.base_io import BaseIO __all__ = ["Aquifer", "SimpleAquifer"] -class AquiferData(ExportBase): +class AquiferData(BaseIO): def __init__(self, model, kaq, c, z, npor, ltype, model3d=False): """Initialize aquifer data. diff --git a/timflow/steady/export.py b/timflow/steady/base_io.py similarity index 86% rename from timflow/steady/export.py rename to timflow/steady/base_io.py index a3bebe0f..18b8b5f1 100644 --- a/timflow/steady/export.py +++ b/timflow/steady/base_io.py @@ -6,7 +6,7 @@ from typing_extensions import Self -class ExportBase: +class BaseIO: # Registry for all subclasses. _registry = {} # Storage for model object @@ -19,7 +19,7 @@ def __new__(cls, *args, **kwargs) -> Self: frame = inspect.currentframe() caller = frame.f_back if caller.f_code.co_name == "": - cls._obj_list.append(instance) + cls._obj_list.append((instance, kwargs)) return instance def __init_subclass__(cls) -> None: @@ -34,17 +34,18 @@ def to_json(self, filepath) -> None: """ data = {} i = 0 - for obj in self._obj_list: - data.update({f"object{i}": obj.to_dict()}) + for item in self._obj_list: + obj, kwargs = item + data.update({f"object{i}": obj.to_dict(**kwargs)}) i += 1 with open(filepath, "w") as f: f.write(json.dumps(data, indent=4)) - def to_dict(self): + def to_dict(self, **kwargs): """ - Collect the contructor arguments and potential additional attributes into a dict. + Collect the contructor arguments into a dict. - :return: _description_ + :return: Dict with the arguments. """ sig = inspect.signature(self.__init__) data = {"_type": self.__class__.__name__} @@ -54,20 +55,14 @@ def to_dict(self): if name in ["aq", "aqin", "aqout"]: continue if name != "self": - value = getattr(self, name, None) + # For kwargs as inputs + value = kwargs.get(name, None) + # If not used as input -> collect from attributes + if value is None: + value = getattr(self, name, None) data[name] = self._serialize(value) - data.update(self.extra_to_dict()) return data - def extra_to_dict(self) -> dict[Any, Any]: - """Add the addition attributes to the dict. - - May be overloaded in the subclass. - - :return: Dict with addition parameters. - """ - return {} - @classmethod def from_json(cls, filepath): """ diff --git a/timflow/steady/constant.py b/timflow/steady/constant.py index dde0baba..38fd6951 100644 --- a/timflow/steady/constant.py +++ b/timflow/steady/constant.py @@ -33,6 +33,7 @@ def __init__( ) # Defined here and not in Element as other elements can have multiple parameters # per layers: + self.layer = layer self.nparam = 1 self.nunknowns = 0 self.xr = xr diff --git a/timflow/steady/element.py b/timflow/steady/element.py index dd74b2b1..09e58f4e 100644 --- a/timflow/steady/element.py +++ b/timflow/steady/element.py @@ -11,12 +11,12 @@ def initialize(self): import numpy as np -from timflow.steady.export import ExportBase +from timflow.steady.base_io import BaseIO __all__ = ["Element"] -class Element(ExportBase): +class Element(BaseIO): """Base class for all timflow.steady elements. Elements represent physical features in the aquifer system such as wells, diff --git a/timflow/steady/model.py b/timflow/steady/model.py index 109aae92..4c45e840 100644 --- a/timflow/steady/model.py +++ b/timflow/steady/model.py @@ -20,7 +20,7 @@ from timflow.steady.aquifer import Aquifer, SimpleAquifer from timflow.steady.aquifer_parameters import param_3d, param_maq from timflow.steady.constant import ConstantStar -from timflow.steady.export import ExportBase +from timflow.steady.base_io import BaseIO from timflow.steady.plots import PlotSteady from timflow.version import check_tqdm_parallel @@ -43,7 +43,7 @@ def _compute_velocity_mp(args): return i, vv -class Model(ExportBase): +class Model(BaseIO): """Create a model consisting of an arbitrary sequence of aquifers and leaky layers. Notes From 5a5fb1765d4d3e6a08ae9e45bfc01bb68e28e58f Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Fri, 7 Aug 2026 09:46:26 +0200 Subject: [PATCH 08/18] Spellcheck and documentation --- timflow/steady/base_io.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/timflow/steady/base_io.py b/timflow/steady/base_io.py index 18b8b5f1..fe11fa20 100644 --- a/timflow/steady/base_io.py +++ b/timflow/steady/base_io.py @@ -1,6 +1,5 @@ import inspect import json -from typing import Any from numpy import array, ndarray from typing_extensions import Self @@ -11,10 +10,18 @@ class BaseIO: _registry = {} # Storage for model object _model = None - # Registry for all created objects + # Registry for all created objects with their kwargs _obj_list = [] def __new__(cls, *args, **kwargs) -> Self: + """Register created objects in script. + + When a object is created in the script, register this object with the + constructor kwargs. If the object is made inside of another class or function + don't register it. + + :return: Created object. + """ instance = super().__new__(cls) frame = inspect.currentframe() caller = frame.f_back @@ -23,14 +30,14 @@ def __new__(cls, *args, **kwargs) -> Self: return instance def __init_subclass__(cls) -> None: - """Add the subclass to the registry on creation.""" + """Add the subclass to the registry on inheritance.""" cls._registry[cls.__name__] = cls def to_json(self, filepath) -> None: """ - Write the contructor arguments and potential additional attributes to a JSON-file. + Write the constructor arguments to a JSON-file. - :param filepath: Filepath to the to be created JSON-file. + :param filepath: Filepath for the to be created JSON-file. """ data = {} i = 0 @@ -43,7 +50,7 @@ def to_json(self, filepath) -> None: def to_dict(self, **kwargs): """ - Collect the contructor arguments into a dict. + Collect the constructor arguments into a dict. :return: Dict with the arguments. """ @@ -66,7 +73,7 @@ def to_dict(self, **kwargs): @classmethod def from_json(cls, filepath): """ - Read the contructor arguments and potential addition attributes from a JSON-file. + Read the constructor arguments and potential addition attributes from a JSON-file. :param filepath: Filepath to the to be created JSON-file. """ @@ -74,7 +81,7 @@ def from_json(cls, filepath): with open(filepath, "r") as f: data = json.load(f) for k, v in data.items(): - if k == "object0": + if k == "object0": # Model object is always first created. obj = cls.from_dict(v) else: cls.from_dict(v) From 79a494593b9b5c4c75bc8d8f89039b47e319d45c Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Fri, 7 Aug 2026 10:07:45 +0200 Subject: [PATCH 09/18] reorder of methods --- timflow/steady/base_io.py | 54 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/timflow/steady/base_io.py b/timflow/steady/base_io.py index fe11fa20..2c156661 100644 --- a/timflow/steady/base_io.py +++ b/timflow/steady/base_io.py @@ -13,15 +13,19 @@ class BaseIO: # Registry for all created objects with their kwargs _obj_list = [] + def __init_subclass__(cls) -> None: + """Add the subclass to the registry on inheritance.""" + cls._registry[cls.__name__] = cls + def __new__(cls, *args, **kwargs) -> Self: """Register created objects in script. - + When a object is created in the script, register this object with the constructor kwargs. If the object is made inside of another class or function don't register it. :return: Created object. - """ + """ instance = super().__new__(cls) frame = inspect.currentframe() caller = frame.f_back @@ -29,10 +33,6 @@ def __new__(cls, *args, **kwargs) -> Self: cls._obj_list.append((instance, kwargs)) return instance - def __init_subclass__(cls) -> None: - """Add the subclass to the registry on inheritance.""" - cls._registry[cls.__name__] = cls - def to_json(self, filepath) -> None: """ Write the constructor arguments to a JSON-file. @@ -70,6 +70,23 @@ def to_dict(self, **kwargs): data[name] = self._serialize(value) return data + @classmethod + def _serialize(cls, value): + """Convert python objects to exportable types. + + :param value: Object for export. + :return: Object in exportable form. + """ + if isinstance(value, cls): + return value.to_dict() + if isinstance(value, list): + return [cls._serialize(v) for v in value] + if isinstance(value, dict): + return {k: cls._serialize(v) for k, v in value.items()} + if isinstance(value, ndarray): + return {"ndarray": value.tolist()} + return value + @classmethod def from_json(cls, filepath): """ @@ -83,10 +100,10 @@ def from_json(cls, filepath): for k, v in data.items(): if k == "object0": # Model object is always first created. obj = cls.from_dict(v) - else: - cls.from_dict(v) - if obj is None: - raise ImportError + if obj is None: # No model in json + raise ImportError + cls.from_dict(v) + return obj @classmethod @@ -111,23 +128,6 @@ def from_dict(cls, data: dict): cls._model = obj return obj - @classmethod - def _serialize(cls, value): - """Convert python objects to exportable types. - - :param value: Object for export. - :return: Object in exportable form. - """ - if isinstance(value, cls): - return value.to_dict() - if isinstance(value, list): - return [cls._serialize(v) for v in value] - if isinstance(value, dict): - return {k: cls._serialize(v) for k, v in value.items()} - if isinstance(value, ndarray): - return {"ndarray": value.tolist()} - return value - @classmethod def _deserialize(cls, value): """Convert a dict of values to the right python objects. From 8ad63370256e39113355cbcf39aeb0ec269ea461 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Mon, 10 Aug 2026 08:42:47 +0200 Subject: [PATCH 10/18] .vscode removed --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 4ec39be7..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cSpell.enabled": false -} \ No newline at end of file From 425130b32924f026e279c13870aee32043e5f1b0 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Mon, 10 Aug 2026 11:55:19 +0200 Subject: [PATCH 11/18] Positional args --- timflow/steady/base_io.py | 45 +++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/timflow/steady/base_io.py b/timflow/steady/base_io.py index 2c156661..93a9066b 100644 --- a/timflow/steady/base_io.py +++ b/timflow/steady/base_io.py @@ -17,20 +17,13 @@ def __init_subclass__(cls) -> None: """Add the subclass to the registry on inheritance.""" cls._registry[cls.__name__] = cls + # TODO FIXME Add classes per model in separate lists. def __new__(cls, *args, **kwargs) -> Self: - """Register created objects in script. - - When a object is created in the script, register this object with the - constructor kwargs. If the object is made inside of another class or function - don't register it. - - :return: Created object. - """ instance = super().__new__(cls) frame = inspect.currentframe() caller = frame.f_back if caller.f_code.co_name == "": - cls._obj_list.append((instance, kwargs)) + cls._obj_list.append((instance, args, kwargs)) return instance def to_json(self, filepath) -> None: @@ -42,32 +35,42 @@ def to_json(self, filepath) -> None: data = {} i = 0 for item in self._obj_list: - obj, kwargs = item - data.update({f"object{i}": obj.to_dict(**kwargs)}) + obj, args, kwargs = item + data.update({f"object{i}": obj.to_dict(args, kwargs)}) i += 1 with open(filepath, "w") as f: f.write(json.dumps(data, indent=4)) - def to_dict(self, **kwargs): + def to_dict(self, args, kwargs): """ Collect the constructor arguments into a dict. :return: Dict with the arguments. """ + pos_args = list(args) sig = inspect.signature(self.__init__) + # Reference to class for recreation data = {"_type": self.__class__.__name__} for name in sig.parameters: - if name == ("model" or "ml"): # reference to parent object - continue - if name in ["aq", "aqin", "aqout"]: + if name in ("model", "ml"): # reference to model object + pos_args.pop(0) continue - if name != "self": - # For kwargs as inputs + # For positional args as input + if pos_args != []: + value = pos_args.pop(0) + # For kwargs as inputs + else: value = kwargs.get(name, None) - # If not used as input -> collect from attributes - if value is None: - value = getattr(self, name, None) - data[name] = self._serialize(value) + # Defaults from signature. + if ( + value is None + and sig.parameters[name].default is not inspect.Parameter.empty + ): + value = sig.parameters[name].default + # If not used as input -> collect from attributes + if value is None: + value = getattr(self, name, None) + data[name] = self._serialize(value) return data @classmethod From 8a28ba9259a3eea940a8660a0fcc705adea08946 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Mon, 10 Aug 2026 13:36:23 +0200 Subject: [PATCH 12/18] Storage and Loading of multiple models per script. --- timflow/steady/base_io.py | 47 ++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/timflow/steady/base_io.py b/timflow/steady/base_io.py index 93a9066b..b0aaaf4a 100644 --- a/timflow/steady/base_io.py +++ b/timflow/steady/base_io.py @@ -8,22 +8,38 @@ class BaseIO: # Registry for all subclasses. _registry = {} - # Storage for model object - _model = None - # Registry for all created objects with their kwargs - _obj_list = [] + # Registry for all created objects with their kwargs for storing. + _obj_lists = {} + # Registry for model instance for storing. + _models = {} + # Storage for model object for loading. + _setup_model = None def __init_subclass__(cls) -> None: """Add the subclass to the registry on inheritance.""" cls._registry[cls.__name__] = cls - # TODO FIXME Add classes per model in separate lists. def __new__(cls, *args, **kwargs) -> Self: instance = super().__new__(cls) frame = inspect.currentframe() caller = frame.f_back if caller.f_code.co_name == "": - cls._obj_list.append((instance, args, kwargs)) + # If a new Model object create a new list before adding it. + if "Model" in str(cls.__name__): + m = f"model{len(cls._obj_lists)}" + cls._models.update({instance: m}) + cls._obj_lists.update({m:[]}) + cls._obj_lists[m].append((instance, args, kwargs)) + # Other objects are added to the list of the model they have been + # added to. + else: + if args != (): + m_inst = args[0] + else: + m_inst = kwargs.get("model", None) + if m_inst is None: + m_inst = kwargs.get("ml") + cls._obj_lists[cls._models[m_inst]].append((instance, args, kwargs)) return instance def to_json(self, filepath) -> None: @@ -34,7 +50,7 @@ def to_json(self, filepath) -> None: """ data = {} i = 0 - for item in self._obj_list: + for item in self._obj_lists[self._models[self]]: obj, args, kwargs = item data.update({f"object{i}": obj.to_dict(args, kwargs)}) i += 1 @@ -97,16 +113,17 @@ def from_json(cls, filepath): :param filepath: Filepath to the to be created JSON-file. """ - obj = None + # reset the reference to the Model instance for setup. + if cls._setup_model is not None: + cls._setup_model = None with open(filepath, "r") as f: data = json.load(f) for k, v in data.items(): if k == "object0": # Model object is always first created. obj = cls.from_dict(v) - if obj is None: # No model in json + if "obj" not in locals(): # No model in json raise ImportError cls.from_dict(v) - return obj @classmethod @@ -116,19 +133,19 @@ def from_dict(cls, data: dict): :param data: Dict with parameters :return: Instance of this (sub)class. """ - type_name = data.pop("_type") + type_name = data["_type"] subclass = cls._registry[type_name] sig = inspect.signature(subclass.__init__) constructor_args = {} for name in sig.parameters: - if name == ("model" or "ml"): - constructor_args[name] = cls._model + if name in ("model", "ml"): + constructor_args[name] = cls._setup_model if name != "self" and name in data: constructor_args[name] = cls._deserialize(data.pop(name)) obj = subclass(**constructor_args) - if cls._model is None: - cls._model = obj + if cls._setup_model is None: + cls._setup_model = obj return obj @classmethod From 96cf7a50413218de34739ce35c9aa6e96bb67947 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Mon, 10 Aug 2026 13:57:50 +0200 Subject: [PATCH 13/18] Use inspect.signature.bind for argument input --- timflow/steady/base_io.py | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/timflow/steady/base_io.py b/timflow/steady/base_io.py index b0aaaf4a..5f0f6a1b 100644 --- a/timflow/steady/base_io.py +++ b/timflow/steady/base_io.py @@ -28,7 +28,7 @@ def __new__(cls, *args, **kwargs) -> Self: if "Model" in str(cls.__name__): m = f"model{len(cls._obj_lists)}" cls._models.update({instance: m}) - cls._obj_lists.update({m:[]}) + cls._obj_lists.update({m: []}) cls._obj_lists[m].append((instance, args, kwargs)) # Other objects are added to the list of the model they have been # added to. @@ -63,30 +63,17 @@ def to_dict(self, args, kwargs): :return: Dict with the arguments. """ - pos_args = list(args) sig = inspect.signature(self.__init__) + bound = sig.bind(*args, **kwargs) # Reference to class for recreation data = {"_type": self.__class__.__name__} - for name in sig.parameters: - if name in ("model", "ml"): # reference to model object - pos_args.pop(0) - continue - # For positional args as input - if pos_args != []: - value = pos_args.pop(0) - # For kwargs as inputs - else: - value = kwargs.get(name, None) - # Defaults from signature. - if ( - value is None - and sig.parameters[name].default is not inspect.Parameter.empty - ): - value = sig.parameters[name].default - # If not used as input -> collect from attributes - if value is None: - value = getattr(self, name, None) - data[name] = self._serialize(value) + data.update( + { + k: self._serialize(v) + for k, v in bound.arguments.items() + if k not in ("model", "ml") + } + ) return data @classmethod From 8b2699c1e9502dce87df533083d08515393c0613 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Mon, 17 Aug 2026 14:18:48 +0200 Subject: [PATCH 14/18] Remove some unneeded logic and clarify var names --- timflow/steady/base_io.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/timflow/steady/base_io.py b/timflow/steady/base_io.py index 5f0f6a1b..0c258d2c 100644 --- a/timflow/steady/base_io.py +++ b/timflow/steady/base_io.py @@ -7,29 +7,31 @@ class BaseIO: # Registry for all subclasses. - _registry = {} + _class_registry = {} # Registry for all created objects with their kwargs for storing. - _obj_lists = {} + _obj_registry = {} # Registry for model instance for storing. - _models = {} - # Storage for model object for loading. - _setup_model = None + _model_registry = {} def __init_subclass__(cls) -> None: """Add the subclass to the registry on inheritance.""" - cls._registry[cls.__name__] = cls + cls._class_registry[cls.__name__] = cls def __new__(cls, *args, **kwargs) -> Self: + """Add all newly created object to a registry if they are created directly. + + :return: instance of the (sub)class + """ instance = super().__new__(cls) frame = inspect.currentframe() caller = frame.f_back if caller.f_code.co_name == "": # If a new Model object create a new list before adding it. if "Model" in str(cls.__name__): - m = f"model{len(cls._obj_lists)}" - cls._models.update({instance: m}) - cls._obj_lists.update({m: []}) - cls._obj_lists[m].append((instance, args, kwargs)) + m = f"model{len(cls._obj_registry)}" + cls._model_registry.update({instance: m}) + cls._obj_registry.update({m: []}) + cls._obj_registry[m].append((instance, args, kwargs)) # Other objects are added to the list of the model they have been # added to. else: @@ -39,7 +41,7 @@ def __new__(cls, *args, **kwargs) -> Self: m_inst = kwargs.get("model", None) if m_inst is None: m_inst = kwargs.get("ml") - cls._obj_lists[cls._models[m_inst]].append((instance, args, kwargs)) + cls._obj_registry[cls._model_registry[m_inst]].append((instance, args, kwargs)) return instance def to_json(self, filepath) -> None: @@ -50,7 +52,7 @@ def to_json(self, filepath) -> None: """ data = {} i = 0 - for item in self._obj_lists[self._models[self]]: + for item in self._obj_registry[self._model_registry[self]]: obj, args, kwargs = item data.update({f"object{i}": obj.to_dict(args, kwargs)}) i += 1 @@ -83,8 +85,6 @@ def _serialize(cls, value): :param value: Object for export. :return: Object in exportable form. """ - if isinstance(value, cls): - return value.to_dict() if isinstance(value, list): return [cls._serialize(v) for v in value] if isinstance(value, dict): @@ -100,14 +100,13 @@ def from_json(cls, filepath): :param filepath: Filepath to the to be created JSON-file. """ - # reset the reference to the Model instance for setup. - if cls._setup_model is not None: - cls._setup_model = None + cls._setup_model = None with open(filepath, "r") as f: - data = json.load(f) + data: dict = json.load(f) for k, v in data.items(): if k == "object0": # Model object is always first created. obj = cls.from_dict(v) + continue if "obj" not in locals(): # No model in json raise ImportError cls.from_dict(v) @@ -120,8 +119,8 @@ def from_dict(cls, data: dict): :param data: Dict with parameters :return: Instance of this (sub)class. """ - type_name = data["_type"] - subclass = cls._registry[type_name] + type_name: str = data["_type"] + subclass = cls._class_registry[type_name] sig = inspect.signature(subclass.__init__) constructor_args = {} From 168d1510f8f74c606042083fce8189d7df61a900 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Thu, 27 Aug 2026 09:34:08 +0200 Subject: [PATCH 15/18] Lint error fixed --- timflow/steady/aquifer.py | 2 +- timflow/steady/base_io.py | 8 +++++--- timflow/steady/model.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/timflow/steady/aquifer.py b/timflow/steady/aquifer.py index 008839ab..8f5a8781 100644 --- a/timflow/steady/aquifer.py +++ b/timflow/steady/aquifer.py @@ -12,8 +12,8 @@ import numpy as np import pandas as pd -from timflow.steady.constant import ConstantStar from timflow.steady.base_io import BaseIO +from timflow.steady.constant import ConstantStar __all__ = ["Aquifer", "SimpleAquifer"] diff --git a/timflow/steady/base_io.py b/timflow/steady/base_io.py index 0c258d2c..faff6e22 100644 --- a/timflow/steady/base_io.py +++ b/timflow/steady/base_io.py @@ -21,7 +21,7 @@ def __new__(cls, *args, **kwargs) -> Self: """Add all newly created object to a registry if they are created directly. :return: instance of the (sub)class - """ + """ instance = super().__new__(cls) frame = inspect.currentframe() caller = frame.f_back @@ -41,7 +41,9 @@ def __new__(cls, *args, **kwargs) -> Self: m_inst = kwargs.get("model", None) if m_inst is None: m_inst = kwargs.get("ml") - cls._obj_registry[cls._model_registry[m_inst]].append((instance, args, kwargs)) + cls._obj_registry[cls._model_registry[m_inst]].append( + (instance, args, kwargs) + ) return instance def to_json(self, filepath) -> None: @@ -108,7 +110,7 @@ def from_json(cls, filepath): obj = cls.from_dict(v) continue if "obj" not in locals(): # No model in json - raise ImportError + raise ImportError("No main model found in the JSON-file.") cls.from_dict(v) return obj diff --git a/timflow/steady/model.py b/timflow/steady/model.py index 4c45e840..34c13ea4 100644 --- a/timflow/steady/model.py +++ b/timflow/steady/model.py @@ -19,8 +19,8 @@ from timflow.steady.aquifer import Aquifer, SimpleAquifer from timflow.steady.aquifer_parameters import param_3d, param_maq -from timflow.steady.constant import ConstantStar from timflow.steady.base_io import BaseIO +from timflow.steady.constant import ConstantStar from timflow.steady.plots import PlotSteady from timflow.version import check_tqdm_parallel From 3140a8698f323fefbfec2814ecd9a862b5dd3c33 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Tue, 8 Sep 2026 15:48:10 +0200 Subject: [PATCH 16/18] Decorator, moved methods and store args, kwargs in model instance - first pass --- timflow/steady/base_io.py | 134 ++++++++++++++--------------------- timflow/steady/linesink1d.py | 3 +- timflow/steady/model.py | 51 ++++++++++++- 3 files changed, 107 insertions(+), 81 deletions(-) diff --git a/timflow/steady/base_io.py b/timflow/steady/base_io.py index faff6e22..cdd80f45 100644 --- a/timflow/steady/base_io.py +++ b/timflow/steady/base_io.py @@ -1,81 +1,69 @@ +from __future__ import annotations + import inspect -import json +from functools import wraps +from importlib import import_module +from typing import TYPE_CHECKING, TypeVar from numpy import array, ndarray -from typing_extensions import Self +if TYPE_CHECKING: + from timflow.steady import Model -class BaseIO: - # Registry for all subclasses. - _class_registry = {} - # Registry for all created objects with their kwargs for storing. - _obj_registry = {} - # Registry for model instance for storing. - _model_registry = {} +T = TypeVar("T") - def __init_subclass__(cls) -> None: - """Add the subclass to the registry on inheritance.""" - cls._class_registry[cls.__name__] = cls - def __new__(cls, *args, **kwargs) -> Self: - """Add all newly created object to a registry if they are created directly. +def store_input(cls: type[T]) -> type[T]: - :return: instance of the (sub)class - """ - instance = super().__new__(cls) - frame = inspect.currentframe() - caller = frame.f_back - if caller.f_code.co_name == "": - # If a new Model object create a new list before adding it. - if "Model" in str(cls.__name__): - m = f"model{len(cls._obj_registry)}" - cls._model_registry.update({instance: m}) - cls._obj_registry.update({m: []}) - cls._obj_registry[m].append((instance, args, kwargs)) - # Other objects are added to the list of the model they have been - # added to. + original_init = cls.__init__ + + @wraps(original_init) + def new_init(self, *args, **kwargs) -> None: + original_init(self, *args, **kwargs) + + model_instance: Model | None + if "Model" in self.__class__.__name__: + model_instance = self + else: + if args != (): + model_instance = args[0] else: - if args != (): - m_inst = args[0] - else: - m_inst = kwargs.get("model", None) - if m_inst is None: - m_inst = kwargs.get("ml") - cls._obj_registry[cls._model_registry[m_inst]].append( - (instance, args, kwargs) - ) - return instance - - def to_json(self, filepath) -> None: - """ - Write the constructor arguments to a JSON-file. + model_instance = kwargs.get("model", None) + if model_instance is None: + model_instance = kwargs.get("ml") + if model_instance is not None: + model_instance._obj_registry.append( + { + "class": f"{cls.__module__}.{cls.__qualname__}", + "args": args, + "kwargs": kwargs, + } + ) - :param filepath: Filepath for the to be created JSON-file. - """ - data = {} - i = 0 - for item in self._obj_registry[self._model_registry[self]]: - obj, args, kwargs = item - data.update({f"object{i}": obj.to_dict(args, kwargs)}) - i += 1 - with open(filepath, "w") as f: - f.write(json.dumps(data, indent=4)) - - def to_dict(self, args, kwargs): + + + cls.__init__ = new_init + + return cls + + +class BaseIO: + @classmethod + def to_dict(cls, args, kwargs): """ Collect the constructor arguments into a dict. :return: Dict with the arguments. """ - sig = inspect.signature(self.__init__) - bound = sig.bind(*args, **kwargs) + sig = inspect.signature(cls.__init__) + bound = sig.bind(cls, *args, **kwargs) # Reference to class for recreation - data = {"_type": self.__class__.__name__} + data = {"_type": f"{cls.__module__}.{cls.__qualname__}"} data.update( { - k: self._serialize(v) + k: cls._serialize(v) for k, v in bound.arguments.items() - if k not in ("model", "ml") + if k not in ("model", "ml", "self") } ) return data @@ -91,29 +79,12 @@ def _serialize(cls, value): return [cls._serialize(v) for v in value] if isinstance(value, dict): return {k: cls._serialize(v) for k, v in value.items()} + if isinstance(value, tuple): + return {"tuple": [cls._serialize(v) for v in value]} if isinstance(value, ndarray): return {"ndarray": value.tolist()} return value - @classmethod - def from_json(cls, filepath): - """ - Read the constructor arguments and potential addition attributes from a JSON-file. - - :param filepath: Filepath to the to be created JSON-file. - """ - cls._setup_model = None - with open(filepath, "r") as f: - data: dict = json.load(f) - for k, v in data.items(): - if k == "object0": # Model object is always first created. - obj = cls.from_dict(v) - continue - if "obj" not in locals(): # No model in json - raise ImportError("No main model found in the JSON-file.") - cls.from_dict(v) - return obj - @classmethod def from_dict(cls, data: dict): """Factory method to create an instance of this (sub)class. @@ -122,7 +93,10 @@ def from_dict(cls, data: dict): :return: Instance of this (sub)class. """ type_name: str = data["_type"] - subclass = cls._class_registry[type_name] + module_name = ".".join(type_name.split(".")[:-1]) + class_name = type_name.split(".")[-1] + module = import_module(module_name) + subclass = getattr(module, class_name) sig = inspect.signature(subclass.__init__) constructor_args = {} @@ -147,6 +121,8 @@ def _deserialize(cls, value): return cls.from_dict(value) if isinstance(value, dict) and "ndarray" in value: return array(value["ndarray"]) + if isinstance(value, dict) and "tuple" in value: + return tuple(cls._deserialize(v) for v in value["tuple"]) if isinstance(value, list): return [cls._deserialize(v) for v in value] if isinstance(value, dict): diff --git a/timflow/steady/linesink1d.py b/timflow/steady/linesink1d.py index bfff0c15..39ffb0ba 100644 --- a/timflow/steady/linesink1d.py +++ b/timflow/steady/linesink1d.py @@ -12,6 +12,7 @@ import matplotlib.pyplot as plt import numpy as np +from timflow.steady.base_io import store_input from timflow.steady.element import Element from timflow.steady.equation import ( DisvecDiffEquation, @@ -195,7 +196,7 @@ def initialize(self): def setparams(self, sol): self.parameters[:, 0] = sol - +@store_input class River1D(LineSink1DBase, HeadEquation): """Create an infinitely long line-sink with a given head. diff --git a/timflow/steady/model.py b/timflow/steady/model.py index 34c13ea4..580880b6 100644 --- a/timflow/steady/model.py +++ b/timflow/steady/model.py @@ -10,8 +10,11 @@ ml.solve() """ +import json import multiprocessing as mp import warnings +from importlib import import_module +from typing import Any import numpy as np import pandas as pd @@ -19,7 +22,7 @@ from timflow.steady.aquifer import Aquifer, SimpleAquifer from timflow.steady.aquifer_parameters import param_3d, param_maq -from timflow.steady.base_io import BaseIO +from timflow.steady.base_io import BaseIO, store_input from timflow.steady.constant import ConstantStar from timflow.steady.plots import PlotSteady from timflow.version import check_tqdm_parallel @@ -81,8 +84,51 @@ def __init__(self, kaq, z, c, npor, ltype, model3d=False): self.plots = PlotSteady(self) + self._obj_registry: list[dict[str, Any]] = [] + self.initialized = False + def to_json(self, filepath) -> None: + """ + Write the constructor arguments to a JSON-file. + + :param filepath: Filepath for the to be created JSON-file. + """ + data = {} + i = 0 + for item in self._obj_registry: + type_name:str = item["class"] + args: list[Any] = item.get("args", []) + kwargs: dict[str, Any] = item.get("kwargs", {}) + module_name: str = ".".join(type_name.split(".")[:-1]) + class_name: str = type_name.split(".")[-1] + module = import_module(module_name) + subclass = getattr(module, class_name) + data.update({f"object{i}": subclass.to_dict(args, kwargs)}) + i += 1 + print(data) + with open(filepath, "w") as f: + f.write(json.dumps(data, indent=4)) + + @classmethod + def from_json(cls, filepath): + """ + Read the constructor arguments and potential addition attributes from a JSON-file. + + :param filepath: Filepath to the to be created JSON-file. + """ + cls._setup_model = None + with open(filepath, "r") as f: + data: dict = json.load(f) + for k, v in data.items(): + if k == "object0": # Model object is always first created. + obj = cls.from_dict(v) + continue + if "obj" not in locals(): # No model in json + raise ImportError("No main model found in the JSON-file.") + cls.from_dict(v) + return obj + def initialize(self): # remove inhomogeneity elements (they are added again) self.elementlist = [e for e in self.elementlist if not e.inhomelement] @@ -944,6 +990,7 @@ def vcontoursf1D(self, *args, **kwargs): return self.plots.vcontour_stream_function(*args, **kwargs) +@store_input class ModelMaq(Model): """Create a model by specifying a multi-aquifer sequence of aquifer-leaky layer. @@ -995,6 +1042,7 @@ def __init__(self, kaq=1, z=None, c=None, npor=0.3, topboundary="conf", hstar=No ConstantStar(self, hstar, aq=self.aq) +@store_input class Model3D(Model): """Create a multi-layer model object consisting of stacked aquifer layers. @@ -1079,6 +1127,7 @@ def __init__( ConstantStar(self, hstar, aq=self.aq) +@store_input class ModelXsection(Model): r"""Model for cross-section (2D vertical slice) problems. From 7f7345c8281f59b8c7efd1e6df29c8e7c25efd20 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Wed, 9 Sep 2026 08:19:27 +0200 Subject: [PATCH 17/18] decorators to all classes in the steady.__init__ --- timflow/steady/base_io.py | 4 +--- timflow/steady/circareasink.py | 2 ++ timflow/steady/constant.py | 3 +++ timflow/steady/inhomogeneity.py | 7 +++++++ timflow/steady/inhomogeneity1d.py | 5 +++++ timflow/steady/linedoublet.py | 9 +++++++++ timflow/steady/linedoublet1d.py | 5 +++++ timflow/steady/linesink.py | 11 +++++++++++ timflow/steady/linesink1d.py | 2 ++ timflow/steady/model.py | 2 +- timflow/steady/stripareasink.py | 2 ++ timflow/steady/uflow.py | 2 ++ timflow/steady/well.py | 7 +++++++ 13 files changed, 57 insertions(+), 4 deletions(-) diff --git a/timflow/steady/base_io.py b/timflow/steady/base_io.py index cdd80f45..f760ba7b 100644 --- a/timflow/steady/base_io.py +++ b/timflow/steady/base_io.py @@ -20,7 +20,7 @@ def store_input(cls: type[T]) -> type[T]: @wraps(original_init) def new_init(self, *args, **kwargs) -> None: original_init(self, *args, **kwargs) - + model_instance: Model | None if "Model" in self.__class__.__name__: model_instance = self @@ -40,8 +40,6 @@ def new_init(self, *args, **kwargs) -> None: } ) - - cls.__init__ = new_init return cls diff --git a/timflow/steady/circareasink.py b/timflow/steady/circareasink.py index 4b92e85e..07679cec 100644 --- a/timflow/steady/circareasink.py +++ b/timflow/steady/circareasink.py @@ -10,11 +10,13 @@ import numpy as np from scipy.special import i0, i1, k0, k1 +from timflow.steady.base_io import store_input from timflow.steady.element import Element __all__ = ["CircAreaSink"] +@store_input class CircAreaSink(Element): """Class to create a circular area-sink. diff --git a/timflow/steady/constant.py b/timflow/steady/constant.py index 38fd6951..5bbdaef5 100644 --- a/timflow/steady/constant.py +++ b/timflow/steady/constant.py @@ -10,6 +10,7 @@ import numpy as np +from timflow.steady.base_io import store_input from timflow.steady.element import Element from timflow.steady.equation import PotentialEquation @@ -72,6 +73,7 @@ def disvecinf(self, x, y, aq=None): return rv +@store_input class Constant(ConstantBase, PotentialEquation): """Specify the head at one point in the model in one layer. @@ -179,6 +181,7 @@ def setparams(self, sol): # class ConstantStar(Element, PotentialEquation): # I don't think we need the equation +@store_input class ConstantStar(Element): """Constant representing the particular solution inside a semi-confined aquifer. diff --git a/timflow/steady/inhomogeneity.py b/timflow/steady/inhomogeneity.py index 9f94158e..e1e1fe46 100644 --- a/timflow/steady/inhomogeneity.py +++ b/timflow/steady/inhomogeneity.py @@ -19,6 +19,7 @@ from timflow.steady.aquifer import AquiferData from timflow.steady.aquifer_parameters import param_3d, param_maq +from timflow.steady.base_io import store_input from timflow.steady.constant import ConstantInside, ConstantStar from timflow.steady.element import Element from timflow.steady.intlinesink import ( @@ -145,6 +146,7 @@ def create_elements(self): c.inhomelement = True +@store_input class PolygonInhomMaq(PolygonInhom): """Create a polygonal inhomogeneity. @@ -240,6 +242,7 @@ def __init__( ) +@store_input class PolygonInhom3D(PolygonInhom): """Create a multi-layer model object consisting of many aquifer layers. @@ -545,6 +548,7 @@ def create_elements(self): c.inhomelement = True +@store_input class BuildingPitMaq(BuildingPit): """Element to simulate a building pit with an impermeable wall in ModelMaq. @@ -627,6 +631,7 @@ def __init__( ) +@store_input class BuildingPit3D(BuildingPit): """Element to simulate a building pit with an impermeable wall in Model3D. @@ -917,6 +922,7 @@ def create_elements(self): c.inhomelement = True +@store_input class LeakyBuildingPitMaq(LeakyBuildingPit): """Element to simulate a building pit with a leaky wall in ModelMaq. @@ -1005,6 +1011,7 @@ def __init__( ) +@store_input class LeakyBuildingPit3D(LeakyBuildingPit): """Element to simulate a building pit with a leaky wall in Model3D. diff --git a/timflow/steady/inhomogeneity1d.py b/timflow/steady/inhomogeneity1d.py index 41f1f315..58e96b15 100644 --- a/timflow/steady/inhomogeneity1d.py +++ b/timflow/steady/inhomogeneity1d.py @@ -17,6 +17,7 @@ from timflow.steady.aquifer import AquiferData from timflow.steady.aquifer_parameters import param_3d, param_maq +from timflow.steady.base_io import store_input from timflow.steady.constant import ConstantStar from timflow.steady.linesink1d import FluxDiffLineSink1D, HeadDiffLineSink1D from timflow.steady.stripareasink import XsectionAreaSinkInhom @@ -326,6 +327,7 @@ def plot( return ax +@store_input class XsectionMaq(Xsection): """Cross-section inhomogeneity for a multi-aquifer sequence. @@ -396,6 +398,7 @@ def __init__( ) +@store_input class Xsection3D(Xsection): """Cross-section inhomogeneity consisting of stacked aquifer layers. @@ -489,6 +492,7 @@ def __init__(self, model, x1, x2, kaq, c, z, npor, ltype, hstar, N, name=None): super().__init__(model, x1, x2, kaq, c, z, npor, ltype, hstar, N, name=name) +@store_input class StripInhomMaq(XsectionMaq): def __init__( self, @@ -512,6 +516,7 @@ def __init__( super().__init__(model, x1, x2, kaq, z, c, npor, topboundary, hstar, N, name) +@store_input class StripInhom3D(Xsection3D): def __init__( self, diff --git a/timflow/steady/linedoublet.py b/timflow/steady/linedoublet.py index 29ad827b..274eeafb 100644 --- a/timflow/steady/linedoublet.py +++ b/timflow/steady/linedoublet.py @@ -13,6 +13,7 @@ import numpy as np from timflow.bessel.besselnumba import disbesldv, potbesldv +from timflow.steady.base_io import store_input from timflow.steady.controlpoints import controlpoints from timflow.steady.element import Element from timflow.steady.equation import DisvecEquation, LeakyWallEquation @@ -185,6 +186,7 @@ def plot(self, ax=None, layer=None): ax.plot([self.x1, self.x2], [self.y1, self.y2], "k") +@store_input class ImpermeableWall(LineDoubletHoBase, DisvecEquation): """Create a segment of an impermeable wall, which is simulated with a line-doublet. @@ -251,6 +253,7 @@ def setparams(self, sol): self.parameters[:, 0] = sol +@store_input class LeakyWall(LineDoubletHoBase, LeakyWallEquation): """Create a segment of a leaky wall, which is simulated with a line-doublet. @@ -423,6 +426,7 @@ def plot(self, ax=None, layer=None): ax.plot(self.x, self.y, "k") +@store_input class ImpermeableWallString(LineDoubletStringBase, DisvecEquation): """Create a string of impermeable wall segments consisting of line-doublets. @@ -473,6 +477,7 @@ def setparams(self, sol): self.parameters[:, 0] = sol +@store_input class LeakyWallString(LineDoubletStringBase, LeakyWallEquation): """Create a string of leaky wall segments consisting of line-doublets. @@ -525,6 +530,7 @@ def setparams(self, sol): self.parameters[:, 0] = sol +@store_input class ImpLineDoublet(ImpermeableWall): """Deprecated alias for :class:`.ImpermeableWall`. @@ -542,6 +548,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +@store_input class ImpLineDoubletString(ImpermeableWallString): """Deprecated alias for :class:`.ImpermeableWallString`. @@ -559,6 +566,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +@store_input class LeakyLineDoublet(LeakyWall): """Deprecated alias for :class:`.LeakyWall`. @@ -576,6 +584,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +@store_input class LeakyLineDoubletString(LeakyWallString): """Deprecated alias for :class:`.LeakyWallString`. diff --git a/timflow/steady/linedoublet1d.py b/timflow/steady/linedoublet1d.py index 3fcd5e62..b001e114 100644 --- a/timflow/steady/linedoublet1d.py +++ b/timflow/steady/linedoublet1d.py @@ -13,6 +13,7 @@ import matplotlib.pyplot as plt import numpy as np +from timflow.steady.base_io import store_input from timflow.steady.element import Element from timflow.steady.equation import DisvecEquation, LeakyWallEquation @@ -122,6 +123,7 @@ def plot(self, ax=None): ) +@store_input class ImpermeableWall1D(LineDoublet1D, DisvecEquation): """Create 1D impermeable wall.""" @@ -147,6 +149,7 @@ def setparams(self, sol): self.parameters[:, 0] = sol +@store_input class LeakyWall1D(LineDoublet1D, LeakyWallEquation): """Create an infinitely long leaky or impermeable wall. @@ -196,6 +199,7 @@ def setparams(self, sol): self.parameters[:, 0] = sol +@store_input class ImpLineDoublet1D(ImpermeableWall1D): """Deprecated alias for :class:`.ImpermeableWall1D`. @@ -213,6 +217,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +@store_input class LeakyLineDoublet1D(LeakyWall1D): """Deprecated alias for :class:`.LeakyWall1D`. diff --git a/timflow/steady/linesink.py b/timflow/steady/linesink.py index 145accd9..b5073f83 100644 --- a/timflow/steady/linesink.py +++ b/timflow/steady/linesink.py @@ -13,6 +13,7 @@ import numpy as np from timflow.bessel.besselnumba import disbeslsv, potbeslsv +from timflow.steady.base_io import store_input from timflow.steady.controlpoints import controlpoints, strengthinf_controlpoints from timflow.steady.element import Element from timflow.steady.equation import HeadEquation @@ -500,6 +501,7 @@ def headinside(self, icp=0): return hinside +@store_input class River(LineSinkHoBase, HeadEquation): """Head-specified line-sink which may optionally have a width and resistance. @@ -609,6 +611,7 @@ def setparams(self, sol): self.parameters[:, 0] = sol +@store_input class Ditch(River): """Line-sink with specified total discharge, and uniform but unknown head. @@ -872,6 +875,7 @@ def plot(self, ax=None, layer=None, **kwargs): ls.plot(layer=layer, ax=ax, **kwargs) +@store_input class RiverString(LineSinkStringBase2): """String of head-specified line-sinks with optional width and resistance. @@ -1029,6 +1033,7 @@ def equation(self): return mat, rhs +@store_input class DitchString(RiverString): """String of Ditches with specified discharge and uniform unknown head. @@ -1117,6 +1122,7 @@ def equation(self): return mat, rhs +@store_input class LineSinkDitch(Ditch): """Deprecated alias for :class:`.Ditch`. @@ -1133,6 +1139,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +@store_input class LineSinkDitchString(DitchString): """Deprecated alias for :class:`.DitchString`. @@ -1150,6 +1157,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +@store_input class HeadLineSink(River): """Deprecated alias for :class:`.River`. @@ -1167,6 +1175,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +@store_input class HeadLineSinkString(RiverString): """Deprecated alias for :class:`.RiverString`. @@ -1184,6 +1193,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +@store_input class CollectorWell(DitchString): """Collector well: collection of line sinks with a specified total discharge. @@ -1246,6 +1256,7 @@ def __init__( self.name = "CollectorWell" +@store_input class RadialCollectorWell(CollectorWell): """Radial collector well. diff --git a/timflow/steady/linesink1d.py b/timflow/steady/linesink1d.py index 39ffb0ba..6101f524 100644 --- a/timflow/steady/linesink1d.py +++ b/timflow/steady/linesink1d.py @@ -137,6 +137,7 @@ def plot(self, ax=None): ) +@store_input class LineSink1D(LineSink1DBase, MscreenWellEquation): """Create an infinitely long line-sink with a given discharge per unit length. @@ -251,6 +252,7 @@ def setparams(self, sol): self.parameters[:, 0] = sol +@store_input class HeadLineSink1D(River1D): """Deprecated alias for :class:`.River1D`. diff --git a/timflow/steady/model.py b/timflow/steady/model.py index 580880b6..a0342be3 100644 --- a/timflow/steady/model.py +++ b/timflow/steady/model.py @@ -106,7 +106,7 @@ def to_json(self, filepath) -> None: subclass = getattr(module, class_name) data.update({f"object{i}": subclass.to_dict(args, kwargs)}) i += 1 - print(data) + with open(filepath, "w") as f: f.write(json.dumps(data, indent=4)) diff --git a/timflow/steady/stripareasink.py b/timflow/steady/stripareasink.py index 0e3d18c5..645d6893 100644 --- a/timflow/steady/stripareasink.py +++ b/timflow/steady/stripareasink.py @@ -12,6 +12,7 @@ import matplotlib.pyplot as plt import numpy as np +from timflow.steady.base_io import store_input from timflow.steady.element import Element __all__ = ["XsectionAreaSinkInhom", "XsectionAreaSink"] @@ -126,6 +127,7 @@ def plot(self, ax=None, n_arrows=10, **kwargs): return ax +@store_input class XsectionAreaSink(Element): """Cross-section area-sink for testing purposes only. diff --git a/timflow/steady/uflow.py b/timflow/steady/uflow.py index 1242426e..b42e5e2a 100644 --- a/timflow/steady/uflow.py +++ b/timflow/steady/uflow.py @@ -9,11 +9,13 @@ import numpy as np +from timflow.steady.base_io import store_input from timflow.steady.element import Element __all__ = ["Uflow"] +@store_input class Uflow(Element): """Add uniform flow to the model. diff --git a/timflow/steady/well.py b/timflow/steady/well.py index a5cd27df..dbf733d4 100644 --- a/timflow/steady/well.py +++ b/timflow/steady/well.py @@ -14,6 +14,7 @@ import numpy as np from scipy.special import k0, k1 +from timflow.steady.base_io import store_input from timflow.steady.element import Element from timflow.steady.equation import HeadEquation, MscreenWellNoflowEquation from timflow.steady.trace import tracelines @@ -341,6 +342,7 @@ def plotcapzone(self, *args, **kwargs): ) +@store_input class Well(WellBase): r"""Well Class to create a well with a specified discharge. @@ -451,6 +453,7 @@ def setparams(self, sol): self.parameters[:, 0] = sol +@store_input class HeadWell(WellBase, HeadEquation): r"""HeadWell Class to create a well with a specified head inside the well. @@ -523,6 +526,7 @@ def setparams(self, sol): self.parameters[:, 0] = sol +@store_input class TargetHeadWell(WellBase): r"""TargetHeadWell is a well with a specified head at (layer, x, y). @@ -924,6 +928,7 @@ def headinside(self): return self.wlist[0].headinside()[0] +@store_input class WellString(WellStringBase): """ WellString is a string of wells for which the total discharge is specified. @@ -1012,6 +1017,7 @@ def setparams(self, sol): i += w.nparam +@store_input class HeadWellString(WellStringBase): """ HeadWellString is a string of wells for which the head is specified in the wells. @@ -1080,6 +1086,7 @@ def setparams(self, sol): i += w.nparam +@store_input class TargetHeadWellString(WellStringBase): """ A string of wells for which the head is specified at a point. From 2b5c823dca69c7164cc4ddea1d167f003f498d79 Mon Sep 17 00:00:00 2001 From: Vincent Jilesen Date: Wed, 9 Sep 2026 08:49:15 +0200 Subject: [PATCH 18/18] cleanup and filter class referces from registry --- timflow/steady/base_io.py | 14 +++++++++++--- timflow/steady/constant.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/timflow/steady/base_io.py b/timflow/steady/base_io.py index f760ba7b..644f0717 100644 --- a/timflow/steady/base_io.py +++ b/timflow/steady/base_io.py @@ -28,10 +28,15 @@ def new_init(self, *args, **kwargs) -> None: if args != (): model_instance = args[0] else: - model_instance = kwargs.get("model", None) + model_instance = kwargs.pop("model", None) # remove model ref if model_instance is None: - model_instance = kwargs.get("ml") + model_instance = kwargs.pop("ml", None) # remove model ref if model_instance is not None: + # Prevent the reference to the model object from being stored + # this is unused and might complicate pickling. + if len(args) != 0: + args = args[1:] # model ref always first posarg + model_instance._obj_registry.append( { "class": f"{cls.__module__}.{cls.__qualname__}", @@ -47,13 +52,16 @@ def new_init(self, *args, **kwargs) -> None: class BaseIO: @classmethod - def to_dict(cls, args, kwargs): + def to_dict(cls, args: tuple, kwargs: dict): """ Collect the constructor arguments into a dict. :return: Dict with the arguments. """ sig = inspect.signature(cls.__init__) + if "Model" not in cls.__name__: + if "model" not in kwargs or "ml" not in kwargs: + args = args + ("model dummy",) # add dummy for sig.bind bound = sig.bind(cls, *args, **kwargs) # Reference to class for recreation data = {"_type": f"{cls.__module__}.{cls.__qualname__}"} diff --git a/timflow/steady/constant.py b/timflow/steady/constant.py index 5bbdaef5..b7d32e21 100644 --- a/timflow/steady/constant.py +++ b/timflow/steady/constant.py @@ -181,7 +181,7 @@ def setparams(self, sol): # class ConstantStar(Element, PotentialEquation): # I don't think we need the equation -@store_input +# @store_input class ConstantStar(Element): """Constant representing the particular solution inside a semi-confined aquifer.