pastastore 1.9.0__tar.gz → 1.10.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pastastore-1.9.0 → pastastore-1.10.1}/PKG-INFO +4 -2
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/base.py +4 -3
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/connectors.py +2 -2
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/extensions/hpd.py +22 -1
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/plotting.py +281 -26
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/store.py +90 -53
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/version.py +1 -1
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore.egg-info/PKG-INFO +4 -2
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore.egg-info/requires.txt +1 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/pyproject.toml +1 -1
- {pastastore-1.9.0 → pastastore-1.10.1}/tests/test_002_connectors.py +13 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/LICENSE +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/docs/conf.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/__init__.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/datasets.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/extensions/__init__.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/extensions/accessor.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/styling.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/util.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore/yaml_interface.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore.egg-info/SOURCES.txt +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore.egg-info/dependency_links.txt +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/pastastore.egg-info/top_level.txt +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/readme.md +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/setup.cfg +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/tests/conftest.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/tests/test_001_import.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/tests/test_003_pastastore.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/tests/test_004_yaml.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/tests/test_005_maps_plots.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/tests/test_006_benchmark.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/tests/test_007_hpdextension.py +0 -0
- {pastastore-1.9.0 → pastastore-1.10.1}/tests/test_008_stressmodels.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pastastore
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.1
|
|
4
4
|
Summary: Tools for managing Pastas time series models.
|
|
5
5
|
Author: D.A. Brakenhoff
|
|
6
6
|
Maintainer-email: "D.A. Brakenhoff" <d.brakenhoff@artesia-water.nl>, "R. Calje" <r.calje@artesia-water.nl>, "M.A. Vonk" <m.vonk@artesia-water.nl>
|
|
@@ -63,6 +63,7 @@ Requires-Dist: pyproj; extra == "optional"
|
|
|
63
63
|
Requires-Dist: adjustText; extra == "optional"
|
|
64
64
|
Provides-Extra: arcticdb
|
|
65
65
|
Requires-Dist: arcticdb; extra == "arcticdb"
|
|
66
|
+
Requires-Dist: protobuf<6,>=3.5.0.post1; extra == "arcticdb"
|
|
66
67
|
Provides-Extra: lint
|
|
67
68
|
Requires-Dist: ruff; extra == "lint"
|
|
68
69
|
Provides-Extra: pytest
|
|
@@ -83,6 +84,7 @@ Requires-Dist: Ipython; extra == "docs"
|
|
|
83
84
|
Requires-Dist: ipykernel; extra == "docs"
|
|
84
85
|
Requires-Dist: nbsphinx; extra == "docs"
|
|
85
86
|
Requires-Dist: nbsphinx_link; extra == "docs"
|
|
87
|
+
Dynamic: license-file
|
|
86
88
|
|
|
87
89
|

|
|
88
90
|
[](https://pastastore.readthedocs.io/en/latest/?badge=latest)
|
|
@@ -1477,9 +1477,10 @@ class ModelAccessor:
|
|
|
1477
1477
|
names=["oseries", "modelname"],
|
|
1478
1478
|
)
|
|
1479
1479
|
modeldf = pd.DataFrame(index=idx)
|
|
1480
|
-
modeldf = modeldf.join(
|
|
1481
|
-
|
|
1482
|
-
|
|
1480
|
+
modeldf = modeldf.join(self.conn.oseries, on=modeldf.index.get_level_values(0))
|
|
1481
|
+
# drop key_0 column if it exists
|
|
1482
|
+
if "key_0" in modeldf.columns:
|
|
1483
|
+
modeldf.drop("key_0", axis=1, inplace=True)
|
|
1483
1484
|
modeldf["n_stressmodels"] = 0
|
|
1484
1485
|
for onam, mlnam in modeldf.index:
|
|
1485
1486
|
mldict = self.conn.get_models(mlnam, return_dict=True)
|
|
@@ -608,7 +608,7 @@ class ConnectorUtil:
|
|
|
608
608
|
s.index = pd.to_datetime(s.index, unit="ms")
|
|
609
609
|
s = s.sort_index() # needed for some reason ...
|
|
610
610
|
if squeeze:
|
|
611
|
-
return s.squeeze()
|
|
611
|
+
return s.squeeze(axis="columns")
|
|
612
612
|
return s
|
|
613
613
|
|
|
614
614
|
@staticmethod
|
|
@@ -1223,7 +1223,7 @@ class PasConnector(BaseConnector, ConnectorUtil):
|
|
|
1223
1223
|
config = {
|
|
1224
1224
|
"connector_type": self.conn_type,
|
|
1225
1225
|
"name": self.name,
|
|
1226
|
-
"path": self.parentdir,
|
|
1226
|
+
"path": os.path.abspath(self.parentdir),
|
|
1227
1227
|
}
|
|
1228
1228
|
with open(
|
|
1229
1229
|
os.path.join(self.path, f"{self.name}.pastastore"), "w", encoding="utf-8"
|
|
@@ -12,7 +12,7 @@ from typing import List, Optional, Union
|
|
|
12
12
|
|
|
13
13
|
import hydropandas as hpd
|
|
14
14
|
import numpy as np
|
|
15
|
-
from hydropandas.io.knmi import
|
|
15
|
+
from hydropandas.io.knmi import _get_default_settings, download_knmi_data, get_stations
|
|
16
16
|
from pandas import DataFrame, Series, Timedelta, Timestamp
|
|
17
17
|
from pastas.timeseries_utils import timestep_weighted_resample
|
|
18
18
|
from tqdm.auto import tqdm
|
|
@@ -25,6 +25,20 @@ logger = logging.getLogger("hydropandas_extension")
|
|
|
25
25
|
TimeType = Optional[Union[str, Timestamp]]
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _check_latest_measurement_date_de_bilt(meteo_var: str, **kwargs):
|
|
29
|
+
# get measurements at de Bilt
|
|
30
|
+
stn_de_bilt = 550 if meteo_var == "RD" else 260
|
|
31
|
+
ts_de_bilt, _, _ = download_knmi_data(
|
|
32
|
+
stn_de_bilt,
|
|
33
|
+
meteo_var,
|
|
34
|
+
start=Timestamp.today() - Timedelta(days=60),
|
|
35
|
+
end=Timestamp.today(),
|
|
36
|
+
settings=_get_default_settings(kwargs),
|
|
37
|
+
stn_name="De Bilt",
|
|
38
|
+
)
|
|
39
|
+
return ts_de_bilt.index[-1] # last measurement date
|
|
40
|
+
|
|
41
|
+
|
|
28
42
|
@register_pastastore_accessor("hpd")
|
|
29
43
|
class HydroPandasExtension:
|
|
30
44
|
"""HydroPandas extension for PastaStore.
|
|
@@ -45,6 +59,13 @@ class HydroPandasExtension:
|
|
|
45
59
|
"""
|
|
46
60
|
self._store = store
|
|
47
61
|
|
|
62
|
+
def __repr__(self):
|
|
63
|
+
"""Return string representation of HydroPandasExtension."""
|
|
64
|
+
methods = "".join(
|
|
65
|
+
[f"\n - {meth}" for meth in dir(self) if not meth.startswith("_")]
|
|
66
|
+
)
|
|
67
|
+
return "HydroPandasExtension, available methods:" + methods
|
|
68
|
+
|
|
48
69
|
def add_obscollection(
|
|
49
70
|
self,
|
|
50
71
|
libname: str,
|
|
@@ -42,6 +42,13 @@ class Plots:
|
|
|
42
42
|
"""
|
|
43
43
|
self.pstore = pstore
|
|
44
44
|
|
|
45
|
+
def __repr__(self):
|
|
46
|
+
"""Return string representation of Plots submodule."""
|
|
47
|
+
methods = "".join(
|
|
48
|
+
[f"\n - {meth}" for meth in dir(self) if not meth.startswith("_")]
|
|
49
|
+
)
|
|
50
|
+
return "Plotting submodule, available methods:" + methods
|
|
51
|
+
|
|
45
52
|
def _timeseries(
|
|
46
53
|
self,
|
|
47
54
|
libname,
|
|
@@ -477,6 +484,7 @@ class Plots:
|
|
|
477
484
|
figsize=(6, 6),
|
|
478
485
|
label=None,
|
|
479
486
|
legend=True,
|
|
487
|
+
progressbar=True,
|
|
480
488
|
):
|
|
481
489
|
"""Plot a cumulative step histogram for a model statistic.
|
|
482
490
|
|
|
@@ -500,6 +508,8 @@ class Plots:
|
|
|
500
508
|
of models
|
|
501
509
|
legend: bool, optional
|
|
502
510
|
show legend, by default True
|
|
511
|
+
progressbar: bool, optional
|
|
512
|
+
show progressbar, default is True.
|
|
503
513
|
|
|
504
514
|
Returns
|
|
505
515
|
-------
|
|
@@ -507,7 +517,7 @@ class Plots:
|
|
|
507
517
|
The axes in which the cumulative histogram is plotted
|
|
508
518
|
"""
|
|
509
519
|
statsdf = self.pstore.get_statistics(
|
|
510
|
-
[statistic], modelnames=modelnames, progressbar=
|
|
520
|
+
[statistic], modelnames=modelnames, progressbar=progressbar
|
|
511
521
|
)
|
|
512
522
|
|
|
513
523
|
if ax is None:
|
|
@@ -579,15 +589,15 @@ class Plots:
|
|
|
579
589
|
The CompareModels object containing the comparison results.
|
|
580
590
|
"""
|
|
581
591
|
models = self.pstore.get_models(modelnames)
|
|
582
|
-
names = []
|
|
592
|
+
names = kwargs.pop("names", [])
|
|
583
593
|
onames = [iml.oseries.name for iml in models]
|
|
584
|
-
if len(np.unique(onames)) == 1:
|
|
594
|
+
if len(np.unique(onames)) == 1 and len(names) == 0:
|
|
585
595
|
for modelname in modelnames:
|
|
586
596
|
if onames[0] in modelname:
|
|
587
597
|
names.append(modelname.replace(onames[0], ""))
|
|
588
598
|
else:
|
|
589
599
|
names.append(modelname)
|
|
590
|
-
|
|
600
|
+
elif len(np.unique(onames)) > 1:
|
|
591
601
|
names = modelnames
|
|
592
602
|
cm = ps.CompareModels(models, names=names)
|
|
593
603
|
cm.plot(**kwargs)
|
|
@@ -617,6 +627,13 @@ class Maps:
|
|
|
617
627
|
"""
|
|
618
628
|
self.pstore = pstore
|
|
619
629
|
|
|
630
|
+
def __repr__(self):
|
|
631
|
+
"""Return string representation of Maps submodule."""
|
|
632
|
+
methods = "".join(
|
|
633
|
+
[f"\n - {meth}" for meth in dir(self) if not meth.startswith("_")]
|
|
634
|
+
)
|
|
635
|
+
return "Mapping submodule, available methods:" + methods
|
|
636
|
+
|
|
620
637
|
def stresses(
|
|
621
638
|
self,
|
|
622
639
|
names=None,
|
|
@@ -627,6 +644,7 @@ class Maps:
|
|
|
627
644
|
figsize=(10, 8),
|
|
628
645
|
backgroundmap=False,
|
|
629
646
|
label_kwargs=None,
|
|
647
|
+
show_legend: bool = True,
|
|
630
648
|
**kwargs,
|
|
631
649
|
):
|
|
632
650
|
"""Plot stresses locations on map.
|
|
@@ -653,6 +671,9 @@ class Maps:
|
|
|
653
671
|
by OpenStreetMap.Mapnik. Default option is False.
|
|
654
672
|
label_kwargs: dict, optional
|
|
655
673
|
dictionary with keyword arguments to pass to add_labels method
|
|
674
|
+
show_legend : bool, optional
|
|
675
|
+
add legend with each kind of stress and associated color, only possible
|
|
676
|
+
if colors are not explicitly passed. Default is True.
|
|
656
677
|
|
|
657
678
|
Returns
|
|
658
679
|
-------
|
|
@@ -679,7 +700,8 @@ class Maps:
|
|
|
679
700
|
mask0 = (stresses["x"] != 0.0) | (stresses["y"] != 0.0)
|
|
680
701
|
|
|
681
702
|
if "c" in kwargs:
|
|
682
|
-
c = kwargs.pop("c"
|
|
703
|
+
c = kwargs.pop("c")
|
|
704
|
+
kind_to_color = None
|
|
683
705
|
else:
|
|
684
706
|
c = stresses.loc[mask0, "kind"]
|
|
685
707
|
kind_to_color = {k: f"C{i}" for i, k in enumerate(c.unique())}
|
|
@@ -687,7 +709,7 @@ class Maps:
|
|
|
687
709
|
|
|
688
710
|
r = self._plotmap_dataframe(stresses.loc[mask0], c=c, figsize=figsize, **kwargs)
|
|
689
711
|
if "ax" in kwargs:
|
|
690
|
-
ax = kwargs
|
|
712
|
+
ax = kwargs.pop("ax")
|
|
691
713
|
else:
|
|
692
714
|
ax = r
|
|
693
715
|
if labels:
|
|
@@ -695,6 +717,11 @@ class Maps:
|
|
|
695
717
|
label_kwargs = {}
|
|
696
718
|
self.add_labels(stresses, ax, adjust=adjust, **label_kwargs)
|
|
697
719
|
|
|
720
|
+
if show_legend and kind_to_color is not None:
|
|
721
|
+
for k, color in kind_to_color.items():
|
|
722
|
+
ax.plot([], [], color=color, label=k, **kwargs, marker="o", ls="none")
|
|
723
|
+
ax.legend(loc=(0, 1), frameon=False, ncol=5)
|
|
724
|
+
|
|
698
725
|
if backgroundmap:
|
|
699
726
|
self.add_background_map(ax)
|
|
700
727
|
|
|
@@ -815,6 +842,81 @@ class Maps:
|
|
|
815
842
|
|
|
816
843
|
return ax
|
|
817
844
|
|
|
845
|
+
def _map_helper(
|
|
846
|
+
self,
|
|
847
|
+
df,
|
|
848
|
+
column,
|
|
849
|
+
label=True,
|
|
850
|
+
adjust=False,
|
|
851
|
+
cmap="viridis",
|
|
852
|
+
norm=None,
|
|
853
|
+
vmin=None,
|
|
854
|
+
vmax=None,
|
|
855
|
+
figsize=(10, 8),
|
|
856
|
+
backgroundmap=False,
|
|
857
|
+
**kwargs,
|
|
858
|
+
):
|
|
859
|
+
"""Help function for plotting values on map.
|
|
860
|
+
|
|
861
|
+
Parameters
|
|
862
|
+
----------
|
|
863
|
+
df: pd.DataFrame
|
|
864
|
+
dataframe containing plotting information
|
|
865
|
+
column: str
|
|
866
|
+
column with values to plot
|
|
867
|
+
label: bool, optional
|
|
868
|
+
label points, by default True
|
|
869
|
+
adjust: bool, optional
|
|
870
|
+
automated smart label placement using adjustText, by default False
|
|
871
|
+
cmap: str or colormap, optional
|
|
872
|
+
(name of) the colormap, by default "viridis"
|
|
873
|
+
norm: norm, optional
|
|
874
|
+
normalization for colorbar, by default None
|
|
875
|
+
vmin: float, optional
|
|
876
|
+
vmin for colorbar, by default None
|
|
877
|
+
vmax: float, optional
|
|
878
|
+
vmax for colorbar, by default None
|
|
879
|
+
ax : matplotlib.Axes, optional
|
|
880
|
+
axes handle, if not provided a new figure is created.
|
|
881
|
+
figsize: tuple, optional
|
|
882
|
+
figuresize, by default(10, 8)
|
|
883
|
+
backgroundmap: bool, optional
|
|
884
|
+
if True, add background map (default CRS is EPSG:28992) with default tiles
|
|
885
|
+
by OpenStreetMap.Mapnik. Default option is False.
|
|
886
|
+
progressbar: bool, optional
|
|
887
|
+
show progressbar, default is True.
|
|
888
|
+
|
|
889
|
+
Returns
|
|
890
|
+
-------
|
|
891
|
+
ax: matplotlib.Axes
|
|
892
|
+
axes object
|
|
893
|
+
|
|
894
|
+
See Also
|
|
895
|
+
--------
|
|
896
|
+
self.add_background_map
|
|
897
|
+
"""
|
|
898
|
+
scatter_kwargs = {
|
|
899
|
+
"cmap": cmap,
|
|
900
|
+
"norm": norm,
|
|
901
|
+
"vmin": vmin,
|
|
902
|
+
"vmax": vmax,
|
|
903
|
+
"edgecolors": "w",
|
|
904
|
+
"linewidths": 0.7,
|
|
905
|
+
}
|
|
906
|
+
scatter_kwargs.update(kwargs)
|
|
907
|
+
|
|
908
|
+
ax = self._plotmap_dataframe(
|
|
909
|
+
df, column=column, figsize=figsize, **scatter_kwargs
|
|
910
|
+
)
|
|
911
|
+
if label:
|
|
912
|
+
df.set_index("index", inplace=True)
|
|
913
|
+
self.add_labels(df, ax, adjust=adjust)
|
|
914
|
+
|
|
915
|
+
if backgroundmap:
|
|
916
|
+
self.add_background_map(ax)
|
|
917
|
+
|
|
918
|
+
return ax
|
|
919
|
+
|
|
818
920
|
def modelstat(
|
|
819
921
|
self,
|
|
820
922
|
statistic,
|
|
@@ -827,6 +929,7 @@ class Maps:
|
|
|
827
929
|
vmax=None,
|
|
828
930
|
figsize=(10, 8),
|
|
829
931
|
backgroundmap=False,
|
|
932
|
+
progressbar=True,
|
|
830
933
|
**kwargs,
|
|
831
934
|
):
|
|
832
935
|
"""Plot model statistic on map.
|
|
@@ -856,6 +959,8 @@ class Maps:
|
|
|
856
959
|
backgroundmap: bool, optional
|
|
857
960
|
if True, add background map (default CRS is EPSG:28992) with default tiles
|
|
858
961
|
by OpenStreetMap.Mapnik. Default option is False.
|
|
962
|
+
progressbar: bool, optional
|
|
963
|
+
show progressbar, default is True.
|
|
859
964
|
|
|
860
965
|
Returns
|
|
861
966
|
-------
|
|
@@ -867,7 +972,7 @@ class Maps:
|
|
|
867
972
|
self.add_background_map
|
|
868
973
|
"""
|
|
869
974
|
statsdf = self.pstore.get_statistics(
|
|
870
|
-
[statistic], modelnames=modelnames, progressbar=
|
|
975
|
+
[statistic], modelnames=modelnames, progressbar=progressbar
|
|
871
976
|
).to_frame()
|
|
872
977
|
|
|
873
978
|
statsdf["oseries"] = [
|
|
@@ -877,28 +982,177 @@ class Maps:
|
|
|
877
982
|
statsdf = statsdf.reset_index().set_index("oseries")
|
|
878
983
|
df = statsdf.join(self.pstore.oseries, how="left")
|
|
879
984
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
985
|
+
return self._map_helper(
|
|
986
|
+
df,
|
|
987
|
+
column=statistic,
|
|
988
|
+
label=label,
|
|
989
|
+
adjust=adjust,
|
|
990
|
+
cmap=cmap,
|
|
991
|
+
norm=norm,
|
|
992
|
+
vmin=vmin,
|
|
993
|
+
vmax=vmax,
|
|
994
|
+
figsize=figsize,
|
|
995
|
+
backgroundmap=backgroundmap,
|
|
996
|
+
**kwargs,
|
|
997
|
+
)
|
|
888
998
|
|
|
889
|
-
|
|
999
|
+
def modelparam(
|
|
1000
|
+
self,
|
|
1001
|
+
parameter,
|
|
1002
|
+
modelnames=None,
|
|
1003
|
+
label=True,
|
|
1004
|
+
adjust=False,
|
|
1005
|
+
cmap="viridis",
|
|
1006
|
+
norm=None,
|
|
1007
|
+
vmin=None,
|
|
1008
|
+
vmax=None,
|
|
1009
|
+
figsize=(10, 8),
|
|
1010
|
+
backgroundmap=False,
|
|
1011
|
+
progressbar=True,
|
|
1012
|
+
**kwargs,
|
|
1013
|
+
):
|
|
1014
|
+
"""Plot model parameter value on map.
|
|
890
1015
|
|
|
891
|
-
|
|
892
|
-
|
|
1016
|
+
Parameters
|
|
1017
|
+
----------
|
|
1018
|
+
parameter: str
|
|
1019
|
+
name of the parameter, e.g. "rech_A" or "river_a"
|
|
1020
|
+
modelnames : list of str, optional
|
|
1021
|
+
list of modelnames to include
|
|
1022
|
+
label: bool, optional
|
|
1023
|
+
label points, by default True
|
|
1024
|
+
adjust: bool, optional
|
|
1025
|
+
automated smart label placement using adjustText, by default False
|
|
1026
|
+
cmap: str or colormap, optional
|
|
1027
|
+
(name of) the colormap, by default "viridis"
|
|
1028
|
+
norm: norm, optional
|
|
1029
|
+
normalization for colorbar, by default None
|
|
1030
|
+
vmin: float, optional
|
|
1031
|
+
vmin for colorbar, by default None
|
|
1032
|
+
vmax: float, optional
|
|
1033
|
+
vmax for colorbar, by default None
|
|
1034
|
+
ax : matplotlib.Axes, optional
|
|
1035
|
+
axes handle, if not provided a new figure is created.
|
|
1036
|
+
figsize: tuple, optional
|
|
1037
|
+
figuresize, by default(10, 8)
|
|
1038
|
+
backgroundmap: bool, optional
|
|
1039
|
+
if True, add background map (default CRS is EPSG:28992) with default tiles
|
|
1040
|
+
by OpenStreetMap.Mapnik. Default option is False.
|
|
1041
|
+
progressbar: bool, optional
|
|
1042
|
+
show progressbar, default is True
|
|
1043
|
+
|
|
1044
|
+
Returns
|
|
1045
|
+
-------
|
|
1046
|
+
ax: matplotlib.Axes
|
|
1047
|
+
axes object
|
|
1048
|
+
|
|
1049
|
+
See Also
|
|
1050
|
+
--------
|
|
1051
|
+
self.add_background_map
|
|
1052
|
+
"""
|
|
1053
|
+
paramdf = self.pstore.get_parameters(
|
|
1054
|
+
[parameter],
|
|
1055
|
+
modelnames=modelnames,
|
|
1056
|
+
progressbar=progressbar,
|
|
1057
|
+
ignore_errors=True,
|
|
1058
|
+
).to_frame()
|
|
1059
|
+
|
|
1060
|
+
paramdf["oseries"] = [
|
|
1061
|
+
self.pstore.get_models(m, return_dict=True)["oseries"]["name"]
|
|
1062
|
+
for m in paramdf.index
|
|
1063
|
+
]
|
|
1064
|
+
paramdf = paramdf.reset_index().set_index("oseries")
|
|
1065
|
+
df = paramdf.join(self.pstore.oseries, how="left")
|
|
1066
|
+
|
|
1067
|
+
return self._map_helper(
|
|
1068
|
+
df,
|
|
1069
|
+
column=parameter,
|
|
1070
|
+
label=label,
|
|
1071
|
+
adjust=adjust,
|
|
1072
|
+
cmap=cmap,
|
|
1073
|
+
norm=norm,
|
|
1074
|
+
vmin=vmin,
|
|
1075
|
+
vmax=vmax,
|
|
1076
|
+
figsize=figsize,
|
|
1077
|
+
backgroundmap=backgroundmap,
|
|
1078
|
+
**kwargs,
|
|
893
1079
|
)
|
|
894
|
-
if label:
|
|
895
|
-
df.set_index("index", inplace=True)
|
|
896
|
-
self.add_labels(df, ax, adjust=adjust)
|
|
897
1080
|
|
|
898
|
-
|
|
899
|
-
|
|
1081
|
+
def signature(
|
|
1082
|
+
self,
|
|
1083
|
+
signature,
|
|
1084
|
+
names=None,
|
|
1085
|
+
label=True,
|
|
1086
|
+
adjust=False,
|
|
1087
|
+
cmap="viridis",
|
|
1088
|
+
norm=None,
|
|
1089
|
+
vmin=None,
|
|
1090
|
+
vmax=None,
|
|
1091
|
+
figsize=(10, 8),
|
|
1092
|
+
backgroundmap=False,
|
|
1093
|
+
progressbar=True,
|
|
1094
|
+
**kwargs,
|
|
1095
|
+
):
|
|
1096
|
+
"""Plot signature value on map.
|
|
900
1097
|
|
|
901
|
-
|
|
1098
|
+
Parameters
|
|
1099
|
+
----------
|
|
1100
|
+
signature: str
|
|
1101
|
+
name of the signature, e.g. "mean_annual_maximum" or "duration_curve_slope"
|
|
1102
|
+
names : list of str, optional
|
|
1103
|
+
list of observation well names to include
|
|
1104
|
+
label: bool, optional
|
|
1105
|
+
label points, by default True
|
|
1106
|
+
adjust: bool, optional
|
|
1107
|
+
automated smart label placement using adjustText, by default False
|
|
1108
|
+
cmap: str or colormap, optional
|
|
1109
|
+
(name of) the colormap, by default "viridis"
|
|
1110
|
+
norm: norm, optional
|
|
1111
|
+
normalization for colorbar, by default None
|
|
1112
|
+
vmin: float, optional
|
|
1113
|
+
vmin for colorbar, by default None
|
|
1114
|
+
vmax: float, optional
|
|
1115
|
+
vmax for colorbar, by default None
|
|
1116
|
+
ax : matplotlib.Axes, optional
|
|
1117
|
+
axes handle, if not provided a new figure is created.
|
|
1118
|
+
figsize: tuple, optional
|
|
1119
|
+
figuresize, by default(10, 8)
|
|
1120
|
+
backgroundmap: bool, optional
|
|
1121
|
+
if True, add background map (default CRS is EPSG:28992) with default tiles
|
|
1122
|
+
by OpenStreetMap.Mapnik. Default option is False.
|
|
1123
|
+
progressbar: bool, optional
|
|
1124
|
+
show progressbar, default is True
|
|
1125
|
+
|
|
1126
|
+
Returns
|
|
1127
|
+
-------
|
|
1128
|
+
ax: matplotlib.Axes
|
|
1129
|
+
axes object
|
|
1130
|
+
|
|
1131
|
+
See Also
|
|
1132
|
+
--------
|
|
1133
|
+
self.add_background_map
|
|
1134
|
+
"""
|
|
1135
|
+
signature_df = self.pstore.get_signatures(
|
|
1136
|
+
[signature],
|
|
1137
|
+
names=names,
|
|
1138
|
+
progressbar=progressbar,
|
|
1139
|
+
ignore_errors=True,
|
|
1140
|
+
)
|
|
1141
|
+
df = signature_df.join(self.pstore.oseries, how="left")
|
|
1142
|
+
|
|
1143
|
+
return self._map_helper(
|
|
1144
|
+
df,
|
|
1145
|
+
column=signature,
|
|
1146
|
+
label=label,
|
|
1147
|
+
adjust=adjust,
|
|
1148
|
+
cmap=cmap,
|
|
1149
|
+
norm=norm,
|
|
1150
|
+
vmin=vmin,
|
|
1151
|
+
vmax=vmax,
|
|
1152
|
+
figsize=figsize,
|
|
1153
|
+
backgroundmap=backgroundmap,
|
|
1154
|
+
**kwargs,
|
|
1155
|
+
)
|
|
902
1156
|
|
|
903
1157
|
@staticmethod
|
|
904
1158
|
def _plotmap_dataframe(
|
|
@@ -948,6 +1202,7 @@ class Maps:
|
|
|
948
1202
|
if ax is None:
|
|
949
1203
|
return_scatter = False
|
|
950
1204
|
fig, ax = plt.subplots(figsize=figsize)
|
|
1205
|
+
ax.set_aspect("equal", adjustable="box")
|
|
951
1206
|
else:
|
|
952
1207
|
return_scatter = True
|
|
953
1208
|
fig = ax.figure
|
|
@@ -1010,8 +1265,8 @@ class Maps:
|
|
|
1010
1265
|
label: bool, optional, default is True
|
|
1011
1266
|
add labels to points on map
|
|
1012
1267
|
metadata_source: str, optional
|
|
1013
|
-
whether to obtain metadata from model
|
|
1014
|
-
metadata in pastastore
|
|
1268
|
+
one of "model" or "store", pick whether to obtain metadata from model
|
|
1269
|
+
Timeseries or from metadata in pastastore, default is "model"
|
|
1015
1270
|
offset : float, optional
|
|
1016
1271
|
add offset to current extent of model time series, useful
|
|
1017
1272
|
for zooming out around models
|
|
@@ -5,6 +5,7 @@ import logging
|
|
|
5
5
|
import os
|
|
6
6
|
import warnings
|
|
7
7
|
from functools import partial
|
|
8
|
+
from pathlib import Path
|
|
8
9
|
from typing import Dict, Iterable, List, Literal, Optional, Tuple, Union
|
|
9
10
|
|
|
10
11
|
import numpy as np
|
|
@@ -80,15 +81,39 @@ class PastaStore:
|
|
|
80
81
|
self.yaml = PastastoreYAML(self)
|
|
81
82
|
|
|
82
83
|
@classmethod
|
|
83
|
-
def from_pastastore_config_file(cls, fname):
|
|
84
|
-
"""Create a PastaStore from a pastastore config file.
|
|
84
|
+
def from_pastastore_config_file(cls, fname, update_path: bool = True):
|
|
85
|
+
"""Create a PastaStore from a pastastore config file.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
fname : str
|
|
90
|
+
path to the pastastore config file
|
|
91
|
+
update_path : bool, optional
|
|
92
|
+
when True, use path derived from location of the config file instead of
|
|
93
|
+
the stored path in the config file. If a PastaStore is moved, the path
|
|
94
|
+
in the config file will probably still refer to the old location. Set to
|
|
95
|
+
False to read the file from the path listed in the config file. In that
|
|
96
|
+
case config files do not need to be stored within the correct directory.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
PastaStore
|
|
101
|
+
PastaStore
|
|
102
|
+
"""
|
|
85
103
|
with open(fname, "r") as f:
|
|
86
104
|
cfg = json.load(f)
|
|
87
|
-
|
|
88
105
|
conn_type = cfg.pop("connector_type")
|
|
89
106
|
if conn_type == "pas":
|
|
107
|
+
# allow loading of pas files from config file that was moved
|
|
108
|
+
if update_path:
|
|
109
|
+
# update path to the config file
|
|
110
|
+
cfg["path"] = Path(fname).parent.parent
|
|
90
111
|
conn = PasConnector(**cfg)
|
|
91
112
|
elif conn_type == "arcticdb":
|
|
113
|
+
if update_path:
|
|
114
|
+
prefix, _ = cfg["uri"].split("://")
|
|
115
|
+
if prefix.lower() == "lmbd":
|
|
116
|
+
cfg["uri"] = prefix + "://" + str(Path(fname).parent)
|
|
92
117
|
conn = ArcticDBConnector(**cfg)
|
|
93
118
|
else:
|
|
94
119
|
raise ValueError(
|
|
@@ -611,7 +636,7 @@ class PastaStore:
|
|
|
611
636
|
modelnames: Optional[List[str]] = None,
|
|
612
637
|
param_value: Optional[str] = "optimal",
|
|
613
638
|
progressbar: Optional[bool] = False,
|
|
614
|
-
ignore_errors: Optional[bool] =
|
|
639
|
+
ignore_errors: Optional[bool] = True,
|
|
615
640
|
) -> FrameorSeriesUnion:
|
|
616
641
|
"""Get model parameters.
|
|
617
642
|
|
|
@@ -633,7 +658,7 @@ class PastaStore:
|
|
|
633
658
|
show progressbar, default is False
|
|
634
659
|
ignore_errors : bool, optional
|
|
635
660
|
ignore errors when True, i.e. when non-existent model is
|
|
636
|
-
encountered in modelnames, by default
|
|
661
|
+
encountered in modelnames, by default True
|
|
637
662
|
|
|
638
663
|
Returns
|
|
639
664
|
-------
|
|
@@ -662,7 +687,10 @@ class PastaStore:
|
|
|
662
687
|
pindex = parameters
|
|
663
688
|
|
|
664
689
|
for c in pindex:
|
|
665
|
-
|
|
690
|
+
if c in mldict["parameters"].index:
|
|
691
|
+
p.loc[mlname, c] = mldict["parameters"].loc[c, param_value]
|
|
692
|
+
else:
|
|
693
|
+
p.loc[mlname, c] = np.nan
|
|
666
694
|
|
|
667
695
|
p = p.squeeze()
|
|
668
696
|
return p.astype(float)
|
|
@@ -806,6 +834,7 @@ class PastaStore:
|
|
|
806
834
|
solve: bool = False,
|
|
807
835
|
store_models: bool = True,
|
|
808
836
|
ignore_errors: bool = False,
|
|
837
|
+
suffix: Optional[str] = None,
|
|
809
838
|
progressbar: bool = True,
|
|
810
839
|
**kwargs,
|
|
811
840
|
) -> Union[Tuple[dict, dict], dict]:
|
|
@@ -826,6 +855,8 @@ class PastaStore:
|
|
|
826
855
|
store the models in the database.
|
|
827
856
|
ignore_errors : bool, optional
|
|
828
857
|
ignore errors while creating models, by default False
|
|
858
|
+
suffix : str, optional
|
|
859
|
+
add suffix to oseries name to create model name, by default None
|
|
829
860
|
progressbar : bool, optional
|
|
830
861
|
show progressbar, by default True
|
|
831
862
|
|
|
@@ -846,7 +877,13 @@ class PastaStore:
|
|
|
846
877
|
desc = "Bulk creation models"
|
|
847
878
|
for o in tqdm(oseries, desc=desc) if progressbar else oseries:
|
|
848
879
|
try:
|
|
849
|
-
|
|
880
|
+
if suffix is not None:
|
|
881
|
+
modelname = f"{o}{suffix}"
|
|
882
|
+
else:
|
|
883
|
+
modelname = o
|
|
884
|
+
iml = self.create_model(
|
|
885
|
+
o, modelname=modelname, add_recharge=add_recharge
|
|
886
|
+
)
|
|
850
887
|
except Exception as e:
|
|
851
888
|
if ignore_errors:
|
|
852
889
|
errors[o] = e
|
|
@@ -1150,13 +1187,19 @@ class PastaStore:
|
|
|
1150
1187
|
|
|
1151
1188
|
# special for WellModels
|
|
1152
1189
|
if stressmodel._name == "WellModel":
|
|
1153
|
-
|
|
1190
|
+
if isinstance(stresses["stress"], list):
|
|
1191
|
+
names = [s.squeeze().name for s in stresses["stress"]]
|
|
1192
|
+
else:
|
|
1193
|
+
names = [stresses["stress"].squeeze().name]
|
|
1194
|
+
stresses["stress"] = [stresses["stress"]] # ugly fix for WellModel
|
|
1154
1195
|
# check oseries is provided
|
|
1155
1196
|
if oseries is None:
|
|
1156
1197
|
raise ValueError("WellModel requires 'oseries' to compute distances!")
|
|
1157
1198
|
# compute distances and add to kwargs
|
|
1158
1199
|
distances = (
|
|
1159
|
-
self.get_distances(oseries=oseries, stresses=names)
|
|
1200
|
+
self.get_distances(oseries=oseries, stresses=names)
|
|
1201
|
+
.T.squeeze(axis=1)
|
|
1202
|
+
.values
|
|
1160
1203
|
)
|
|
1161
1204
|
kwargs["distances"] = distances
|
|
1162
1205
|
# set settings to well
|
|
@@ -1355,56 +1398,40 @@ class PastaStore:
|
|
|
1355
1398
|
):
|
|
1356
1399
|
solve_model(ml_name=ml_name)
|
|
1357
1400
|
|
|
1358
|
-
def
|
|
1359
|
-
|
|
1360
|
-
mls: Optional[Union[ps.Model, list, str]] = None,
|
|
1361
|
-
progressbar: bool = True,
|
|
1362
|
-
): # pragma: no cover
|
|
1363
|
-
"""Get pastas model results.
|
|
1401
|
+
def check_models(self, checklist=None, modelnames=None):
|
|
1402
|
+
"""Check models against checklist.
|
|
1364
1403
|
|
|
1365
1404
|
Parameters
|
|
1366
1405
|
----------
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1406
|
+
checklist : dict, optional
|
|
1407
|
+
dictionary containing model check methods, by default None which
|
|
1408
|
+
uses the ps.checks.checks_brakenhoff_2022 checklist. This includes:
|
|
1409
|
+
- fit metric R² >= 0.6
|
|
1410
|
+
- runs test for autocorrelation
|
|
1411
|
+
- t95 response < half length calibration period
|
|
1412
|
+
- |model parameters| < 1.96 * σ (std deviation)
|
|
1413
|
+
- model parameters are not on bounds
|
|
1414
|
+
modelnames : list of str, optional
|
|
1415
|
+
list of modelnames to perform checks on, by default None
|
|
1372
1416
|
|
|
1373
1417
|
Returns
|
|
1374
1418
|
-------
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
for each model
|
|
1378
|
-
|
|
1379
|
-
Raises
|
|
1380
|
-
------
|
|
1381
|
-
ModuleNotFoundError
|
|
1382
|
-
if the art_tools module is not available
|
|
1419
|
+
pd.DataFrame
|
|
1420
|
+
DataFrame containing pass True/False for each check for each model
|
|
1383
1421
|
"""
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
if mls is None:
|
|
1390
|
-
mls = self.conn.models
|
|
1391
|
-
elif isinstance(mls, ps.Model):
|
|
1392
|
-
mls = [mls.name]
|
|
1393
|
-
|
|
1394
|
-
results_list = []
|
|
1395
|
-
desc = "Get model results"
|
|
1396
|
-
for mlname in tqdm(mls, desc=desc) if progressbar else mls:
|
|
1397
|
-
try:
|
|
1398
|
-
iml = self.conn.get_models(mlname)
|
|
1399
|
-
except Exception as e:
|
|
1400
|
-
print("{1}: '{0}' could not be parsed!".format(mlname, e))
|
|
1401
|
-
continue
|
|
1402
|
-
iresults = pastas_get_model_results(
|
|
1403
|
-
iml, par_selection="all", stats=("evp",), stderrors=True
|
|
1404
|
-
)
|
|
1405
|
-
results_list.append(iresults)
|
|
1422
|
+
if checklist is None:
|
|
1423
|
+
checklist = ps.check.checks_brakenhoff_2022
|
|
1424
|
+
|
|
1425
|
+
names = self.conn._parse_names(modelnames, libname="models")
|
|
1406
1426
|
|
|
1407
|
-
|
|
1427
|
+
check_dfs = []
|
|
1428
|
+
for n in names:
|
|
1429
|
+
cdf = ps.check.checklist(self.models[n], checklist, report=False)["pass"]
|
|
1430
|
+
cdf.name = n
|
|
1431
|
+
check_dfs.append(cdf)
|
|
1432
|
+
chkdf = pd.concat(check_dfs, axis=1)
|
|
1433
|
+
chkdf.columns.name = "models"
|
|
1434
|
+
return chkdf
|
|
1408
1435
|
|
|
1409
1436
|
def to_zip(self, fname: str, overwrite=False, progressbar: bool = True):
|
|
1410
1437
|
"""Write data to zipfile.
|
|
@@ -1531,7 +1558,7 @@ class PastaStore:
|
|
|
1531
1558
|
else:
|
|
1532
1559
|
ext = "pas"
|
|
1533
1560
|
|
|
1534
|
-
# short
|
|
1561
|
+
# short-circuit for PasConnector when zipfile was written using pas files
|
|
1535
1562
|
if conn.conn_type == "pas" and not series_ext_json:
|
|
1536
1563
|
with ZipFile(fname, "r") as archive:
|
|
1537
1564
|
archive.extractall(conn.path)
|
|
@@ -1541,10 +1568,20 @@ class PastaStore:
|
|
|
1541
1568
|
|
|
1542
1569
|
with ZipFile(fname, "r") as archive:
|
|
1543
1570
|
namelist = [
|
|
1544
|
-
fi
|
|
1571
|
+
fi
|
|
1572
|
+
for fi in archive.namelist()
|
|
1573
|
+
if not fi.endswith(f"_meta.{ext}") and not fi.endswith(os.sep)
|
|
1545
1574
|
]
|
|
1546
1575
|
for f in tqdm(namelist, desc="Reading zip") if progressbar else namelist:
|
|
1576
|
+
if f.endswith("_meta.json"):
|
|
1577
|
+
raise (
|
|
1578
|
+
ValueError(
|
|
1579
|
+
"The zipfile was created using pastastore <1.8.0."
|
|
1580
|
+
" Please pass `series_ext_json=True` to `from_zip()`"
|
|
1581
|
+
)
|
|
1582
|
+
)
|
|
1547
1583
|
libname, fjson = os.path.split(f)
|
|
1584
|
+
libname = os.path.split(libname)[-1] # in case zip is one level deeper
|
|
1548
1585
|
if libname in ["stresses", "oseries"]:
|
|
1549
1586
|
s = pd.read_json(archive.open(f), dtype=float, orient="columns")
|
|
1550
1587
|
if not isinstance(s.index, pd.DatetimeIndex):
|
|
@@ -9,7 +9,7 @@ PASTAS_VERSION = parse_version(ps.__version__)
|
|
|
9
9
|
PASTAS_LEQ_022 = PASTAS_VERSION <= parse_version("0.22.0")
|
|
10
10
|
PASTAS_GEQ_150 = PASTAS_VERSION >= parse_version("1.5.0")
|
|
11
11
|
|
|
12
|
-
__version__ = "1.
|
|
12
|
+
__version__ = "1.10.1"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def show_versions(optional=False) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pastastore
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.1
|
|
4
4
|
Summary: Tools for managing Pastas time series models.
|
|
5
5
|
Author: D.A. Brakenhoff
|
|
6
6
|
Maintainer-email: "D.A. Brakenhoff" <d.brakenhoff@artesia-water.nl>, "R. Calje" <r.calje@artesia-water.nl>, "M.A. Vonk" <m.vonk@artesia-water.nl>
|
|
@@ -63,6 +63,7 @@ Requires-Dist: pyproj; extra == "optional"
|
|
|
63
63
|
Requires-Dist: adjustText; extra == "optional"
|
|
64
64
|
Provides-Extra: arcticdb
|
|
65
65
|
Requires-Dist: arcticdb; extra == "arcticdb"
|
|
66
|
+
Requires-Dist: protobuf<6,>=3.5.0.post1; extra == "arcticdb"
|
|
66
67
|
Provides-Extra: lint
|
|
67
68
|
Requires-Dist: ruff; extra == "lint"
|
|
68
69
|
Provides-Extra: pytest
|
|
@@ -83,6 +84,7 @@ Requires-Dist: Ipython; extra == "docs"
|
|
|
83
84
|
Requires-Dist: ipykernel; extra == "docs"
|
|
84
85
|
Requires-Dist: nbsphinx; extra == "docs"
|
|
85
86
|
Requires-Dist: nbsphinx_link; extra == "docs"
|
|
87
|
+
Dynamic: license-file
|
|
86
88
|
|
|
87
89
|

|
|
88
90
|
[](https://pastastore.readthedocs.io/en/latest/?badge=latest)
|
|
@@ -47,7 +47,7 @@ documentation = "https://pastastore.readthedocs.io/en/latest/"
|
|
|
47
47
|
full = ["pastastore[arcticdb,optional]", "hydropandas"]
|
|
48
48
|
extensions = ["hydropandas"]
|
|
49
49
|
optional = ["contextily", "pyproj", "adjustText"]
|
|
50
|
-
arcticdb = ["arcticdb"]
|
|
50
|
+
arcticdb = ["arcticdb", "protobuf >=3.5.0.post1, < 6"]
|
|
51
51
|
lint = ["ruff"]
|
|
52
52
|
pytest = [
|
|
53
53
|
"coverage",
|
|
@@ -34,6 +34,19 @@ def test_add_get_series(request, conn):
|
|
|
34
34
|
conn.del_oseries("test_series")
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def test_add_get_single_value_series(request, conn):
|
|
38
|
+
o1 = pd.Series({pd.Timestamp(2025, 1, 1): 5.0})
|
|
39
|
+
o1.name = "test_single_value_series"
|
|
40
|
+
conn.add_oseries(o1, "test_single_value_series", metadata=None)
|
|
41
|
+
o2 = conn.get_oseries("test_single_value_series")
|
|
42
|
+
try:
|
|
43
|
+
assert isinstance(o2, pd.Series)
|
|
44
|
+
assert o1.equals(o2)
|
|
45
|
+
assert o1.dtype == o2.dtype
|
|
46
|
+
finally:
|
|
47
|
+
conn.del_oseries("test_single_value_series")
|
|
48
|
+
|
|
49
|
+
|
|
37
50
|
def test_add_get_series_wnans(request, conn):
|
|
38
51
|
o1 = pd.Series(
|
|
39
52
|
index=pd.date_range("2000", periods=10, freq="D"),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|