gimu 0.4.0__py3-none-any.whl

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.
gimu/__init__.py ADDED
File without changes
gimu/config.py ADDED
@@ -0,0 +1,82 @@
1
+ """ CONFIG
2
+ """
3
+
4
+
5
+ class Singleton(object):
6
+ __single = None # the one, true Singleton
7
+
8
+ def __new__(classtype, *args, **kwargs):
9
+ # Check to see if a __single exists already for this class
10
+ # Compare class types instead of just looking for None so
11
+ # that subclasses will create their own __single objects
12
+ if classtype != type(classtype.__single):
13
+ classtype.__single = object.__new__(classtype, *args, **kwargs)
14
+ return classtype.__single
15
+
16
+
17
+ #class config(Singleton):
18
+ class config(object):
19
+ def __init__(self,filename=''):
20
+ self.empty()
21
+ if filename: self.read_from_file(filename)
22
+ def empty(self):
23
+ self._config_entries = {}
24
+
25
+ def read_from_file(self,filename=None):
26
+ """ read config from a whole file, until [END], if filename
27
+ is not given, user will be prompted for a filename."""
28
+ if filename==None:
29
+ filename=raw_input(' Configuration file (.cfg) name? ')
30
+ print(" Reading file", filename.strip(), "for configurations...")
31
+
32
+ cfgfile=open(filename)
33
+ self._read(cfgfile)
34
+ cfgfile.close()
35
+
36
+ def read_from_file_section(self,file_with_cfg):
37
+ """ only read config entries from the current location,
38
+ file_with_cfg should be already opened, and reading will
39
+ be terminated once [END] is reached, the calling program
40
+ can continue read the rest of the file. """
41
+ self._read(file_with_cfg)
42
+
43
+ def _read(self,cfgfile):
44
+ finished=False
45
+ while not finished:
46
+ line = cfgfile.readline()
47
+ if line:
48
+ if line.strip()=='': continue
49
+ if line.strip()[0]=='!': continue
50
+ if line.strip()[0]=='#': continue
51
+ if line.strip()[0]=='[':
52
+ ikeyend=line.find(']')
53
+ keyword=line[1:ikeyend]
54
+ if keyword=='END': break
55
+ self._config_entries[keyword]=[]
56
+ else:
57
+ self._config_entries[keyword].append(line.rstrip('\n\r'))
58
+ else: finished = True
59
+ def add_value(self,keyword,value):
60
+ """ add value manually, value can be a single value, or a list,
61
+ or whatever object, as long as you know what it is when you
62
+ get it out, if keyword exist, the value wil be appended """
63
+ if keyword in self._config_entries:
64
+ try:
65
+ self._config_entries[keyword].append(value)
66
+ except:
67
+ self._config_entries[keyword] = [self._config_entries[keyword], value]
68
+ else:
69
+ self._config_entries[keyword] = [value]
70
+
71
+ def get_value(self,keyword):
72
+ if len(self._config_entries[keyword]) == 0:
73
+ return ''
74
+ else:
75
+ return self._config_entries[keyword][0]
76
+
77
+ def get_list(self,keyword):
78
+ return self._config_entries[keyword]
79
+
80
+ def check_optional(self,keyword):
81
+ return keyword in self._config_entries.keys()
82
+
gimu/easy_date.py ADDED
@@ -0,0 +1,209 @@
1
+ """ Lazy man's easy conversion between date strings and date tuple/date objects.
2
+
3
+ It's actually very easy to code it directly, plus it's better to reduce
4
+ dependency. This module provides very few additional functionality. One being
5
+ able to detect if a list of strings/date tuples are given, and act accordingly.
6
+ Use these few lines of code instead if you are not too lazy:
7
+
8
+ from datetime import datetime
9
+ d = datetime.strptime(s, '%Y-%m-%d').date()
10
+
11
+ from datetime import date
12
+ s = str(date((1995,2,3)))
13
+ s = str(date(*tp))
14
+ # tp is your existing tuple, s will be string '1995-02-03'
15
+ """
16
+ import unittest
17
+
18
+
19
+ def date_tuple_to_string(tp):
20
+ """ Converts date integer tuple (y,m,d) into string 'yyyy-mm-dd'. Input
21
+ supports both a single date-tuple as well as a list of date-tuples. """
22
+ from datetime import date
23
+ single = False
24
+ if len(tp) == 3:
25
+ if all([isinstance(i, int) for i in tp]):
26
+ single = True
27
+ if single:
28
+ return str(date(*tp))
29
+ else:
30
+ # assuming input is a list of date-tuples
31
+ return [str(date(*d)) for d in tp]
32
+
33
+ def _s2d(s):
34
+ """ Converts a date string of format 'yyyy-mm-dd' or 'dd/mm/yyyy' into a
35
+ python datetime.date object. """
36
+ from datetime import datetime
37
+ if '/' in s:
38
+ frmt = '%d/%m/%Y'
39
+ elif '-' in s:
40
+ frmt = '%Y-%m-%d'
41
+ else:
42
+ raise Exception("Date string format only support 'yyyy-mm-dd' or 'dd/mm/yyyy'")
43
+ return datetime.strptime(s, frmt).date()
44
+
45
+ def date_string_to_tuple(s):
46
+ """ Converts a date string of format 'yyyy-mm-dd' or 'dd/mm/yyyy' into a
47
+ tuple of integers (y,m,d). Input supports both a single string as well as a
48
+ list of strings. """
49
+ def d2t(d):
50
+ return (d.year, d.month, d.day)
51
+ if isinstance(s, list):
52
+ return [d2t(_s2d(sd)) for sd in s]
53
+ else:
54
+ return d2t(_s2d(s))
55
+
56
+ def date_string_to_date(s):
57
+ """ Converts a date string of format 'yyyy-mm-dd' or 'dd/mm/yyyy' into a
58
+ python datetime.date object. Input supports both a single string as well as
59
+ a list of strings. """
60
+ from datetime import date
61
+ if isinstance(s, list):
62
+ return [_s2d(sd) for sd in s]
63
+ else:
64
+ return _s2d(s)
65
+
66
+ def year_fraction_to_date(year_fraction):
67
+ """ Converts a decimal/float year into a python datetime.date object.
68
+ Input supports both a single float as well as a list of floats. """
69
+ from datetime import date, timedelta
70
+ def yf2d(yf):
71
+ year = int(yf)
72
+ fraction = yf - year
73
+ # assuming 365.25 days per year
74
+ day_of_year = int(fraction * 365.25)
75
+ return date(year, 1, 1) + timedelta(days=day_of_year)
76
+ if isinstance(year_fraction, list):
77
+ return [yf2d(yf) for yf in year_fraction]
78
+ else:
79
+ return yf2d(year_fraction)
80
+
81
+ def year_fraction_to_date_str(year_fraction):
82
+ """ Converts a decimal/float year into a string of format 'dd/mm/yyyy'.
83
+ Input supports both a single float as well as a list of floats. """
84
+ dd = year_fraction_to_date(year_fraction)
85
+ if isinstance(dd, list):
86
+ return [d.strftime("%d/%m/%Y") for d in dd]
87
+ else:
88
+ return dd.strftime("%d/%m/%Y")
89
+
90
+ def toYearFraction(date):
91
+ """ converts python datetime objects into decimal/float years
92
+ https://stackoverflow.com/a/6451892/2368167
93
+ """
94
+ from datetime import datetime as dt
95
+ import time
96
+ def sinceEpoch(date): # returns seconds since epoch
97
+ return time.mktime(date.timetuple())
98
+ s = sinceEpoch
99
+
100
+ year = date.year
101
+ startOfThisYear = dt(year=year, month=1, day=1)
102
+ startOfNextYear = dt(year=year+1, month=1, day=1)
103
+
104
+ yearElapsed = s(date) - s(startOfThisYear)
105
+ yearDuration = s(startOfNextYear) - s(startOfThisYear)
106
+ fraction = yearElapsed/yearDuration
107
+
108
+ return date.year + fraction
109
+
110
+ def toYearFraction2(date):
111
+ """ converts python datetime objects into decimal/float years
112
+ https://stackoverflow.com/a/6451892/2368167
113
+
114
+ fix Epoch issue?
115
+ """
116
+ from datetime import datetime as dt
117
+ from datetime import date as dd
118
+ import time
119
+ def sinceEpoch(date): # returns seconds since epoch
120
+ return date - dd(1970,1,1)
121
+ s = sinceEpoch
122
+
123
+ year = date.year
124
+ startOfThisYear = dd(year=year, month=1, day=1)
125
+ startOfNextYear = dd(year=year+1, month=1, day=1)
126
+
127
+ yearElapsed = s(date) - s(startOfThisYear)
128
+ yearDuration = s(startOfNextYear) - s(startOfThisYear)
129
+ fraction = yearElapsed.total_seconds()/yearDuration.total_seconds()
130
+
131
+ return date.year + fraction
132
+
133
+ def year_fraction(year, month, day):
134
+ """ https://stackoverflow.com/a/36949905/2368167
135
+ """
136
+ import datetime
137
+ date = datetime.date(year, month, day)
138
+ start = datetime.date(date.year, 1, 1).toordinal()
139
+ year_length = datetime.date(date.year+1, 1, 1).toordinal() - start
140
+ return date.year + float(date.toordinal() - start) / year_length
141
+
142
+ class TestEasyDate(unittest.TestCase):
143
+ """docstring for TestEasyDate"""
144
+ def test_year_fraction(self):
145
+ for d,f in [
146
+ ((2023, 1, 1), 2023.0),
147
+ ((2023, 6, 30), 2023.493),
148
+ ((2023, 12, 31), 2023.997),
149
+ ((2024, 1, 1), 2024.0),
150
+ ]:
151
+ result = year_fraction(*d)
152
+ self.assertAlmostEqual(result, f, places=3)
153
+
154
+ def test_year_fraction_mid_year(self):
155
+ """Test year_fraction for middle of the year"""
156
+ # Test July 1st (approximately mid-year)
157
+ result = year_fraction(2023, 7, 1)
158
+ self.assertGreater(result, 2023.4)
159
+ self.assertLess(result, 2023.6)
160
+
161
+ def test_year_fraction_to_date(self):
162
+ """ test by starting with dd/mm/yyyy, convert to fraction, then back to dd/mm/yyyy, final one should be the same as the first one """
163
+ ds = ['2011-03-05', '2011-3-6', '04/11/2011', '5/11/2011']
164
+ for d in ds:
165
+ dd = _s2d(d)
166
+ f = toYearFraction2(dd)
167
+ d2 = year_fraction_to_date(f)
168
+ self.assertEqual(dd, d2)
169
+
170
+ ds = ['04/11/2011', '05/01/2011']
171
+ for d in ds:
172
+ dd = _s2d(d)
173
+ f = toYearFraction2(dd)
174
+ d2 = year_fraction_to_date_str(f)
175
+ self.assertEqual(d, d2)
176
+
177
+ def test_tuple_to_string(self):
178
+ s1 = (1995,10,2)
179
+ s2 = [1995,10,2]
180
+ ss = [s1, s2]
181
+ for s in ss:
182
+ self.assertEqual('1995-10-02', date_tuple_to_string(s))
183
+ self.assertEqual(['1995-10-02']*2, date_tuple_to_string(ss))
184
+
185
+ def test_string_to_tuple(self):
186
+ d1 = '2011-03-05'
187
+ d2 = '2011-3-5'
188
+ d3 = '05/03/2011'
189
+ d4 = '5/3/2011'
190
+ ds = [d1,d2,d3,d4]
191
+ for d in ds:
192
+ self.assertEqual((2011,3,5), date_string_to_tuple(d))
193
+ self.assertEqual([(2011,3,5)]*4, date_string_to_tuple(ds))
194
+
195
+ def test_string_to_date(self):
196
+ from datetime import date
197
+ d1 = '2011-03-05'
198
+ d2 = '2011-3-5'
199
+ d3 = '05/03/2011'
200
+ d4 = '5/3/2011'
201
+ ds = [d1,d2,d3,d4]
202
+ for d in ds:
203
+ self.assertEqual(date(2011,3,5), date_string_to_date(d))
204
+ self.assertEqual([date(2011,3,5)]*4, date_string_to_date(ds))
205
+
206
+
207
+
208
+ if __name__ == '__main__':
209
+ unittest.main(verbosity=2)
gimu/geo_common.py ADDED
@@ -0,0 +1,222 @@
1
+ from mulgrids import *
2
+ import string
3
+
4
+ def quick_enthalpy(t_or_p,ph='liq'):
5
+ """ Return enthalpy J/kg ('liq', 'vap', or 'dif') of water at specified
6
+ temperature (<=500.0 in degC) or pressu (>500.0 in Pa) """
7
+ import t2thermo
8
+ def enth(t,p,f):
9
+ d,u = f(t,p)
10
+ return u + p/d
11
+ def hlhs(t,p):
12
+ return enth(t,p,t2thermo.cowat), enth(t,p,t2thermo.supst)
13
+ def sat_tp(t_or_p):
14
+ if t_or_p > 500.0:
15
+ return t2thermo.tsat(t_or_p), t_or_p
16
+ else:
17
+ return t_or_p, t2thermo.sat(t_or_p)
18
+ (hl,hs) = hlhs(*sat_tp(t_or_p))
19
+ # xxxx
20
+ return {'liq': hl,'vap': hs,'dif': hs-hl}[ph]
21
+
22
+ class RelativePermeability:
23
+ """ Class to calculate linear relative permeability, this is used in
24
+ TOUGH2 for the linear relative permeability model. The class has a
25
+ single method, which takes the saturation and returns the relative
26
+ permeability. """
27
+ def __init__(self, wj_object=None):
28
+ if wj_object is None:
29
+ wj_object = {
30
+ "type": "linear",
31
+ "liquid": [0.0, 1.0],
32
+ "vapour": [0.0, 1.0]
33
+ }
34
+ self.setting = wj_object
35
+ self.func = {
36
+ "linear": self.linear,
37
+ }
38
+
39
+ def calc(self, vapour_saturation):
40
+ """ return (kr_liquid, kr_vapour) """
41
+ return self.func[self.setting['type']](vapour_saturation)
42
+
43
+ def linear(self, vapour_saturation):
44
+ liq_limits = self.setting['liquid']
45
+ vap_limits = self.setting['vapour']
46
+ liquid_saturation = 1.0 - vapour_saturation
47
+ kr_liq = np.interp(liquid_saturation, liq_limits, [0., 1.], left=0.0, right=1.0)
48
+ kr_vap = np.interp(vapour_saturation, vap_limits, [0., 1.], left=0.0, right=1.0)
49
+ return kr_liq, kr_vap
50
+
51
+ def flowing_enthalpy(lst, wj, block):
52
+ """ calculate flowing enthalpy of a given block if producing from waiwera h5
53
+
54
+ NOTE use values from waiwera h5 at current time
55
+ """
56
+ phases = ['liquid', 'vapour']
57
+ rp = RelativePermeability(wj['rock']['relative_permeability'])
58
+ vapour_saturation = lst.element[block][f'fluid_vapour_saturation']
59
+ rel_perm = rp.calc(vapour_saturation)
60
+ fluid = lst.element[block]
61
+ # mobility
62
+ mobility, sum_mobility = {}, 0.0
63
+ for iph, phase in enumerate(phases):
64
+ density = fluid[f'fluid_{phase}_density']
65
+ viscosity = fluid[f'fluid_{phase}_viscosity']
66
+ if viscosity == 0.0:
67
+ mobility[phase] = 0.0
68
+ else:
69
+ mobility[phase] = rel_perm[iph] * density / viscosity
70
+ sum_mobility += mobility[phase]
71
+ # flow fraction
72
+ flow_frac = {}
73
+ for phase in phases:
74
+ flow_frac[phase] = mobility[phase] / sum_mobility
75
+ # enthalpy
76
+ enthalpy = 0.0
77
+ for phase in phases:
78
+ enthalpy += flow_frac[phase] * fluid[f'fluid_{phase}_specific_enthalpy']
79
+ # breakpoint()
80
+ return enthalpy
81
+
82
+ def bottomhole_pressure(depth, temperature=20.0, whp=1.0e5, division=100,
83
+ min_depth_interval=10.0):
84
+ """ Calculate the pressure at the bottom of a well, given the wellhead
85
+ pressure (whp) in pascal, temperature in degC, and division in m. The
86
+ pressure is calculated as: pressure = whp + rho * G * depth where rho is
87
+ the density of water at the given temperature, G is the gravitational
88
+ acceleration (9.81 m/s^2), and depth is the depth of the well divided by
89
+ division.
90
+
91
+ What tis code does is then divide the depth into inv=tervals. Then the
92
+ pressure is accumulated, this allows the density calculation to use the
93
+ actual pressure at the depth. For liquid water, it is quite
94
+ incompressible, so the result won't be that different to the easy
95
+ rho*G*depth method.
96
+ """
97
+ import t2thermo
98
+ # divide well depth to intervals, use larger of the min interval and division
99
+ interval = max(min_depth_interval, depth/float(division))
100
+ d = 0.0
101
+ pressure = whp
102
+ while d < depth:
103
+ rho, u = t2thermo.cowat(temperature, pressure)
104
+ pressure += rho * 9.81 * interval
105
+ d += interval
106
+ rho, u = t2thermo.cowat(temperature, pressure)
107
+ pressure += rho * 9.81 * (depth - d)
108
+ return pressure
109
+
110
+ def block_depth(block, geo):
111
+ """ Return the depth of a block in a geo object, this is the depth of the
112
+ centre of the block. This also work for Waiwera cell if block is an
113
+ integer number.
114
+ """
115
+ if isinstance(block, int):
116
+ block = geo.block_name_list(block + geo.num_atmosphere_blocks)
117
+ lay, col = geo.layer_name(block), geo.column_name(block)
118
+ return geo.column[col].surface - geo.block_centre(lay, col)[2]
119
+
120
+ def xyz2fit(fn):
121
+ data = np.fromfile(fn,sep=" ")
122
+ nrow = np.size(data) / 3
123
+ data= data.reshape(nrow,3)
124
+ return data
125
+
126
+ def find_all_cols_below(geo,level,surfer_file=None):
127
+ if surfer_file is not None: outfile = file(surfer_file,'w')
128
+ cols = []
129
+ for col in geo.columnlist:
130
+ surf = col.surface
131
+ if surf < level:
132
+ #print(col.name, str(surf))
133
+ cols.append([col.centre[0], col.centre[1], surf, col.name])
134
+ if surfer_file is not None: outfile.write(
135
+ str(col.centre[0])+' '+str(col.centre[1])+' '+str(surf)+' '+
136
+ col.name + '\n')
137
+ if surfer_file is not None:
138
+ outfile.close()
139
+ print(' -- file: ', surfer_file, ' is written.')
140
+
141
+ def t2_strict_name(n):
142
+ """ convert any name into the common TOUGH style 5 character name """
143
+ import string
144
+ def make_number(c):
145
+ """ change a character into a single number, mostly 'A' or 'a' will
146
+ become a '1' """
147
+ if len(c.strip()) == 0: return ' '
148
+ d = c.lower().strip()[:1]
149
+ i = string.ascii_lowercase.find(d)+1
150
+ if i == 0: return ' '
151
+ else: return str(i%10)
152
+ newn = list(n.strip())
153
+ if len(newn) <= 3:
154
+ return "".join(newn).strip()[:5].ljust(5)
155
+ for i in range(3,len(newn)):
156
+ if newn[i] not in '0123456789':
157
+ newn[i] = make_number(newn[i])
158
+ return "".join(newn).strip()[:5].ljust(5)
159
+
160
+ def is_leap_year(y):
161
+ if int(y)%400 == 0: return True
162
+ elif int(y)%100 == 0: return False
163
+ elif int(y)%4 == 0: return True
164
+ else: return False
165
+
166
+ def days_in_month(month, leap_year=False):
167
+ if leap_year:
168
+ d_month = [ 31 , 29 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ]
169
+ else:
170
+ d_month = [ 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ]
171
+ return d_month[month - 1]
172
+
173
+ def date2str(d,m,y):
174
+ ds, ms, ys = str(d), str(m), str(y)
175
+ while len(ds) < 2: ds = '0'+ds
176
+ while len(ms) < 2: ms = '0'+ms
177
+ while len(ys) < 4: ys = '0'+ys
178
+ return ds+'/'+ms+'/'+ys
179
+
180
+ def date2num(enddate):
181
+
182
+ d,m,y = enddate.split('/')
183
+ months = [ '01','02','03','04','05','06','07','08','09','10','11','12' ]
184
+ d_month = [ 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ]
185
+ d_month_l = [ 31 , 29 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ]
186
+ acum_ds = [sum(d_month[:i]) for i in range(12)]
187
+ acum_ds_l = [sum(d_month[:i]) for i in range(12)]
188
+ ad_m = dict(zip(months, acum_ds)) # accumulated days before this month
189
+ ad_m_l = dict(zip(months, acum_ds_l)) # accumulated days before this month
190
+ ds_m = dict(zip(months, d_month)) # maximum days in this month
191
+ ds_m_l = dict(zip(months, d_month_l)) # maximum days in this month
192
+ # check and process d and m, this depends on data
193
+ if m not in months:
194
+ print(' Error, unable to convert ', enddate, ' to numeric format. check month.')
195
+ sys.exit()
196
+ if is_leap_year:
197
+ if int(d) not in range(1,ds_m_l[m]+1):
198
+ print(' Error, unable to convert ', enddate, ' to numeric format. check day.')
199
+ sys.exit()
200
+ num = float(y) + ((float(d) + float(ad_m_l[m]))/float(sum(d_month_l)))
201
+ else:
202
+ if int(d) not in range(1,ds_m[m]+1):
203
+ print(' Error, unable to convert ', enddate, ' to numeric format. check day.')
204
+ sys.exit()
205
+ num = float(y) + ((float(d) + float(ad_m[m]))/float(sum(d_month)))
206
+ return num
207
+
208
+
209
+ def identifier(x, chars='abcdefghijklmnopqrstuvwxyz', width=5):
210
+ """ creates a character-based unique identifier from a given integer,
211
+ both chars and width are customisable, output is space filled and
212
+ right aligned """
213
+ output = []
214
+ base = len(chars)
215
+ while x:
216
+ output.append(chars[x % base])
217
+ x /= base
218
+ final = ''.join(reversed(output))
219
+ if len(final) > width:
220
+ raise Exception('identifier() failed, not enough width.')
221
+ return ('%' + str(width) + 's') % final
222
+
gimu/gmf/__init__.py ADDED
File without changes
gimu/gmf/data.py ADDED
@@ -0,0 +1,118 @@
1
+ """ Toolbox for accessing GIM/GMF well data
2
+
3
+ Use:
4
+ from gmf_data import data_json
5
+
6
+ data_json.set_data_folder('/path/to/data')
7
+
8
+ """
9
+
10
+ import json
11
+ import os
12
+ from collections import OrderedDict
13
+
14
+
15
+ class WellData(dict):
16
+ def __init__(self, *args, **kwargs):
17
+ super().__init__(*args, **kwargs)
18
+
19
+ def downhole_temperature(self, date_str):
20
+ """ get direct access to data without fluff """
21
+ try:
22
+ raw_data = self['Downhole_Temperature'][date_str]['Data']
23
+ except KeyError as e:
24
+ print(f"Data '{date_str}' does not exist in {self['Well_Name']}")
25
+ print(f"-> available data set are: {list(self['Downhole_Temperature'].keys())}")
26
+ raise e
27
+ elevs, temps = [], []
28
+ for elev_str, temp in raw_data.items():
29
+ elev = float(elev_str)
30
+ elevs.append(elev)
31
+ temps.append(temp)
32
+
33
+ # TODO takes care of units in GMF
34
+ return elevs, temps
35
+
36
+
37
+ class DataJSON:
38
+ def __init__(self, max_cache_size=10):
39
+ self._data_folders = []
40
+ self._cache = OrderedDict()
41
+ self._max_cache_size = max_cache_size
42
+
43
+ def set_data_folders(self, paths):
44
+ self._data_folders = []
45
+ for path in paths:
46
+ self.add_data_folder(path)
47
+
48
+ def add_data_folder(self, path):
49
+ if not os.path.isdir(path):
50
+ raise ValueError(f"Path '{path}' is not a valid directory.")
51
+ if path not in self._data_folders:
52
+ self._data_folders.append(path)
53
+
54
+ def remove_data_folder(self, path):
55
+ if path in self._data_folders:
56
+ self._data_folders.remove(path)
57
+
58
+ def __getitem__(self, well_name):
59
+ if not self._data_folders:
60
+ raise RuntimeError("Data folder not set. Please call add_data_folder() or set_data_folders() first.")
61
+
62
+ if well_name in self._cache:
63
+ self._cache.move_to_end(well_name)
64
+ return self._cache[well_name]
65
+
66
+ for folder in self._data_folders:
67
+ file_path = os.path.join(folder, f"{well_name}_data.json")
68
+ if os.path.exists(file_path):
69
+ with open(file_path, 'r') as f:
70
+ data = WellData(json.load(f))
71
+
72
+ if len(self._cache) >= self._max_cache_size:
73
+ self._cache.popitem(last=False)
74
+
75
+ self._cache[well_name] = data
76
+ return data
77
+
78
+ msg = '\n'.join([
79
+ f"Well '{well_name}' not found in any of the data folders:",
80
+ ] + [f" {f}" for f in self._data_folders])
81
+ raise KeyError(msg)
82
+
83
+ def __contains__(self, well_name):
84
+ if well_name in self._cache:
85
+ return True
86
+
87
+ for folder in self._data_folders:
88
+ file_path = os.path.join(folder, f"{well_name}_data.json")
89
+ if os.path.exists(file_path):
90
+ return True
91
+ return False
92
+
93
+ def clear_cache(self):
94
+ self._cache.clear()
95
+
96
+ @property
97
+ def data_folders(self):
98
+ return self._data_folders
99
+
100
+ @property
101
+ def cached_wells(self):
102
+ return list(self._cache.keys())
103
+
104
+ @property
105
+ def wells(self):
106
+ """ Returns a list of all available well names from the data folders.
107
+ """
108
+ well_names = set()
109
+ for folder in self._data_folders:
110
+ for filename in os.listdir(folder):
111
+ if filename.endswith('_data.json'):
112
+ well_name = filename[:-len('_data.json')]
113
+ well_names.add(well_name)
114
+ return sorted(list(well_names))
115
+
116
+
117
+ # Singleton instance
118
+ data_json = DataJSON()