rc-qlc 0.3.24__cp311-cp311-win_amd64.whl → 0.3.27__cp311-cp311-win_amd64.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.
Files changed (84) hide show
  1. qlc/cli/__init__.py +100 -9
  2. qlc/cli/installer.py +23 -5
  3. qlc/cli/qlc_main.py +54 -32
  4. qlc/cli/qlc_py_main.py +43 -38
  5. qlc/config/json/qlc_config.json +94 -10
  6. qlc/config/nml/mars_A1_sfc.nml +4 -5
  7. qlc/config/nml/mars_A3_sfc.nml +0 -1
  8. qlc/config/nml/mars_B1_pl.nml +2 -2
  9. qlc/{examples/cams_case_1/config/nml/mars_A3_sfc.nml → config/nml/mars_B1_sfc.nml} +7 -8
  10. qlc/config/nml/mars_C1_pl.nml +1 -1
  11. qlc/{examples/cams_case_1/config/nml/mars_C1_pl.nml → config/nml/mars_C1_sfc.nml} +7 -8
  12. qlc/config/nml/mars_C2_pl.nml +1 -1
  13. qlc/{examples/cams_case_1/config/qlc_cams.conf → config/qlc.conf} +80 -18
  14. qlc/{examples/cams_case_1/mod/b2ro/2018/b2ro_20181215-20181231_A3_sfc.grb → doc/CAMS_b2ro-b2rn_20181201-20181221_qlc_Z1-png_202509090814.pdf} +0 -0
  15. qlc/doc/README.md +213 -49
  16. qlc/doc/USAGE.md +266 -29
  17. qlc/examples/cams_case_1/mod/b2rn/2018/b2rn_20181201-20181221_B1_pl.grb +0 -0
  18. qlc/examples/cams_case_1/mod/b2rn/2018/b2rn_20181201-20181221_C1_sfc.grb +0 -0
  19. qlc/examples/cams_case_1/mod/b2ro/2018/b2ro_20181201-20181221_B1_pl.grb +0 -0
  20. qlc/examples/cams_case_1/mod/b2ro/2018/b2ro_20181201-20181221_C1_sfc.grb +0 -0
  21. qlc/install.py +272 -107
  22. qlc/py/__main__.cp311-win_amd64.pyd +0 -0
  23. qlc/py/averaging.cp311-win_amd64.pyd +0 -0
  24. qlc/py/bias_plots.cp311-win_amd64.pyd +0 -0
  25. qlc/py/control.cp311-win_amd64.pyd +0 -0
  26. qlc/py/io.cp311-win_amd64.pyd +0 -0
  27. qlc/py/loadmod.cp311-win_amd64.pyd +0 -0
  28. qlc/py/loadobs.cp311-win_amd64.pyd +0 -0
  29. qlc/py/logging_utils.cp311-win_amd64.pyd +0 -0
  30. qlc/py/map_plots.cp311-win_amd64.pyd +0 -0
  31. qlc/py/matched.cp311-win_amd64.pyd +0 -0
  32. qlc/py/plot_config.cp311-win_amd64.pyd +0 -0
  33. qlc/py/plotting.cp311-win_amd64.pyd +0 -0
  34. qlc/py/plugin_loader.cp311-win_amd64.pyd +0 -0
  35. qlc/py/processing.cp311-win_amd64.pyd +0 -0
  36. qlc/py/scatter_plots.cp311-win_amd64.pyd +0 -0
  37. qlc/py/stations.cp311-win_amd64.pyd +0 -0
  38. qlc/py/statistics.cp311-win_amd64.pyd +0 -0
  39. qlc/py/style.cp311-win_amd64.pyd +0 -0
  40. qlc/py/timeseries_plots.cp311-win_amd64.pyd +0 -0
  41. qlc/py/utils.cp311-win_amd64.pyd +0 -0
  42. qlc/py/version.cp311-win_amd64.pyd +0 -0
  43. qlc/sh/qlc_A1.sh +30 -11
  44. qlc/sh/qlc_B1a.sh +1 -18
  45. qlc/sh/qlc_B2.sh +8 -1
  46. qlc/sh/qlc_C5.sh +90 -65
  47. qlc/sh/qlc_D1.sh +287 -56
  48. qlc/sh/qlc_Z1.sh +6 -6
  49. qlc/sh/qlc_batch.sh +61 -0
  50. qlc/sh/qlc_common_functions.sh +17 -29
  51. qlc/sh/qlc_main.sh +49 -26
  52. qlc/sh/tex_template/beamercolorthemeCAMS2_35.sty +51 -0
  53. qlc/sh/tex_template/beamerfontthemeCAMS2_35.sty +166 -0
  54. qlc/sh/tex_template/beamerthemeCAMS2_35.sty +25 -0
  55. qlc/sh/tex_template/subcaption.sty +170 -0
  56. qlc/sh/tex_template/template.tex +109 -0
  57. rc_qlc-0.3.27.dist-info/METADATA +309 -0
  58. rc_qlc-0.3.27.dist-info/RECORD +103 -0
  59. qlc/config/json/qlc_config_example_1a_all-obs.json +0 -237
  60. qlc/config/json/qlc_config_example_1b_all-mod.json +0 -353
  61. qlc/config/json/qlc_config_example_1c_all-coll.json +0 -266
  62. qlc/config/json/qlc_config_example_2a_all-obs.json +0 -237
  63. qlc/config/json/qlc_config_example_2b_all-mod.json +0 -353
  64. qlc/config/json/qlc_config_example_2c_all-coll.json +0 -265
  65. qlc/config/json/qlc_config_example_3a-us_obs.json +0 -82
  66. qlc/config/json/qlc_config_example_3b-us_mod.json +0 -122
  67. qlc/config/json/qlc_config_example_3c-us_coll.json +0 -46
  68. qlc/config/json/qlc_config_example_4a_eu-obs.json +0 -41
  69. qlc/config/json/qlc_config_example_4b_eu-mod.json +0 -122
  70. qlc/config/json/qlc_config_example_4c_eu-coll.json +0 -45
  71. qlc/config/qlc_cams.conf +0 -26
  72. qlc/config/qlc_test.conf +0 -26
  73. qlc/config/qlc_tex.conf +0 -107
  74. qlc/examples/cams_case_1/config/json/qlc_config.json +0 -41
  75. qlc/examples/cams_case_1/config/nml/mars_B1_pl.nml +0 -19
  76. qlc/examples/cams_case_1/mod/iqi9/2018/iqi9_20181215-20181231_A3_sfc.grb +0 -0
  77. qlc/sh/qlc_start.sh +0 -23
  78. qlc/sh/qlc_start_batch.sh +0 -46
  79. rc_qlc-0.3.24.dist-info/METADATA +0 -142
  80. rc_qlc-0.3.24.dist-info/RECORD +0 -113
  81. {rc_qlc-0.3.24.dist-info → rc_qlc-0.3.27.dist-info}/WHEEL +0 -0
  82. {rc_qlc-0.3.24.dist-info → rc_qlc-0.3.27.dist-info}/entry_points.txt +0 -0
  83. {rc_qlc-0.3.24.dist-info → rc_qlc-0.3.27.dist-info}/licenses/LICENSE +0 -0
  84. {rc_qlc-0.3.24.dist-info → rc_qlc-0.3.27.dist-info}/top_level.txt +0 -0
qlc/install.py CHANGED
@@ -23,12 +23,13 @@ def read_version_json(version_json_path: Path) -> dict:
23
23
  def get_bin_path():
24
24
  return Path(sys.executable).resolve().parent
25
25
 
26
- def copy_or_link(src, dst, symlink=False):
26
+ def copy_or_link(src, dst, symlink=False, relative=False):
27
27
  dst.parent.mkdir(parents=True, exist_ok=True)
28
28
  if dst.exists() or dst.is_symlink():
29
29
  dst.unlink()
30
30
  if symlink:
31
- dst.symlink_to(src.resolve())
31
+ link_target = os.path.relpath(src.resolve(), dst.parent) if relative else src.resolve()
32
+ dst.symlink_to(link_target, target_is_directory=src.is_dir())
32
33
  else:
33
34
  shutil.copy(src, dst)
34
35
 
@@ -49,41 +50,39 @@ def copytree_with_symlinks(src: Path, dst: Path):
49
50
  else:
50
51
  shutil.copy2(s, d)
51
52
 
52
- def safe_move_and_link(source: Path, target: Path):
53
+ def safe_move_and_link(src: Path, dst: Path, relative: bool = False, backup: bool = True):
53
54
  """
54
- Ensures `target` is a symlink to `source`.
55
- If `target` exists but points elsewhere (or is a dir), it gets backed up first.
55
+ Safely create a symlink from dst to src.
56
+ If dst exists, it's backed up (if backup=True) before the new link is created.
57
+ If dst is already a symlink pointing to src, do nothing.
56
58
  """
57
- if target.is_symlink():
58
- current = target.resolve()
59
- if current == source.resolve():
60
- print(f"[SKIP] {target} already links to {source}")
61
- return
59
+ if dst.is_symlink():
60
+ try:
61
+ if dst.resolve() == src.resolve():
62
+ print(f"[SKIP] Link {dst} already points to {src}")
63
+ return
64
+ else:
65
+ print(f"[INFO] Unlinking existing symlink {dst} -> {dst.readlink()}")
66
+ dst.unlink()
67
+ except FileNotFoundError:
68
+ # This handles broken symlinks
69
+ print(f"[INFO] Removing broken symlink {dst}")
70
+ dst.unlink()
71
+
72
+ elif dst.exists():
73
+ if backup:
74
+ backup_dst = dst.with_name(f"{dst.name}_backup_link")
75
+ print(f"[BACKUP] Moving existing path {dst} -> {backup_dst}")
76
+ shutil.move(str(dst), str(backup_dst))
62
77
  else:
63
- print(f"[BACKUP] {target} symlink points to {current}, backing up")
64
- elif target.exists():
65
- if target.is_dir():
66
- # Only backup if it's not the install root (we don't want to mv source itself)
67
- if target.resolve() != source.resolve():
68
- backup = target.with_name(f"{target.name}_backup")
69
- count = 1
70
- while backup.exists():
71
- backup = target.with_name(f"{target.name}_backup{count}")
72
- count += 1
73
- print(f"[BACKUP] Moving existing directory {target} → {backup}")
74
- shutil.move(str(target), str(backup))
75
- else:
76
- # File or other type — always back up
77
- backup = target.with_name(f"{target.name}_backup")
78
- count = 1
79
- while backup.exists():
80
- backup = target.with_name(f"{target.name}_backup{count}")
81
- count += 1
82
- print(f"[BACKUP] Moving existing file {target} → {backup}")
83
- shutil.move(str(target), str(backup))
84
-
85
- print(f"[LINK] {target} → {source}")
86
- target.symlink_to(source, target_is_directory=True)
78
+ print(f"[INFO] Removing existing path {dst}")
79
+ if dst.is_dir():
80
+ shutil.rmtree(dst)
81
+ else:
82
+ dst.unlink()
83
+
84
+ # Create the new link
85
+ copy_or_link(src, dst, symlink=True, relative=relative)
87
86
 
88
87
  def update_qlc_version(config_path: Path, version: str):
89
88
  if not config_path.exists():
@@ -106,7 +105,97 @@ def update_qlc_version(config_path: Path, version: str):
106
105
  config_path.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
107
106
  print(f"[UPDATED] QLC_VERSION set to {version} in {config_path}")
108
107
 
109
- def setup(mode, config_file=None, version="latest"):
108
+ def setup_data_directories(root: Path, mode: str):
109
+ """
110
+ Creates the two-stage symlink structure for data-heavy directories WITHIN a mode's root.
111
+ - Creates <root>/data
112
+ - Populates it with either real directories (test) or symlinks (cams)
113
+ - Creates top-level symlinks from <root>/* -> <root>/data/*
114
+ """
115
+ print("[SETUP] Configuring data directories...")
116
+ data_dir = root / "data"
117
+ data_dir.mkdir(exist_ok=True)
118
+
119
+ # Define the mapping for CAMS mode
120
+ cams_env_map = {
121
+ "Results": "SCRATCH",
122
+ "Analysis": "HPCPERM",
123
+ "Plots": "PERM",
124
+ "Presentations": "PERM",
125
+ "log": "PERM",
126
+ "run": "PERM",
127
+ "output": "PERM"
128
+ }
129
+
130
+ data_heavy_dirs = ["Results", "Analysis", "Plots", "Presentations", "log", "run", "output"]
131
+
132
+ for d in data_heavy_dirs:
133
+ data_subdir = data_dir / d
134
+
135
+ # Robustly remove existing path before creating a new one
136
+ if data_subdir.is_symlink():
137
+ data_subdir.unlink()
138
+ elif data_subdir.is_dir():
139
+ shutil.rmtree(data_subdir)
140
+ elif data_subdir.exists():
141
+ data_subdir.unlink()
142
+
143
+ if mode == "cams":
144
+ env_var = cams_env_map.get(d)
145
+ target_base_path = os.environ.get(env_var)
146
+
147
+ if target_base_path:
148
+ target_path = Path(target_base_path) / d
149
+ target_path.mkdir(parents=True, exist_ok=True)
150
+ data_subdir.symlink_to(target_path, target_is_directory=True)
151
+ print(f"[LINK] {data_subdir} -> {target_path}")
152
+ else:
153
+ print(f"[WARN] Environment variable ${env_var} not set for {d}. Creating local directory.")
154
+ data_subdir.mkdir(exist_ok=True)
155
+ else: # test mode
156
+ data_subdir.mkdir(exist_ok=True)
157
+ print(f"[MKDIR] {data_subdir}")
158
+
159
+ # Create the top-level symlink, e.g., <root>/Results -> <root>/data/Results
160
+ top_level_link = root / d
161
+ if top_level_link.is_symlink() or top_level_link.exists():
162
+ top_level_link.unlink()
163
+
164
+ # --- Create a relative symlink ---
165
+ # The target is data_subdir, and the link is created at top_level_link.
166
+ # We need the path of the target relative to the link's parent directory.
167
+ relative_target = os.path.relpath(data_subdir, top_level_link.parent)
168
+ top_level_link.symlink_to(relative_target, target_is_directory=True)
169
+ print(f"[LINK] {top_level_link} -> {relative_target}")
170
+
171
+ def link_model_experiments(mod_data_src_root: Path, results_dst_root: Path, debug: bool = False):
172
+ """
173
+ Links model experiment files from mod_data_src_root to results_dst_root.
174
+ Creates absolute symlinks for the model data files.
175
+ """
176
+ if not mod_data_src_root.is_dir():
177
+ print(f"[WARN] Source directory not found for model experiments: {mod_data_src_root}")
178
+ return
179
+
180
+ for exp_dir in mod_data_src_root.iterdir():
181
+ if exp_dir.is_dir():
182
+ # This is an experiment dir, e.g., /path/to/test/mod/b2ro
183
+ results_exp_dir = results_dst_root / exp_dir.name
184
+ results_exp_dir.mkdir(exist_ok=True)
185
+
186
+ # Find all .grb files, searching recursively through year folders
187
+ for year_dir in exp_dir.iterdir():
188
+ if year_dir.is_dir():
189
+ for src_file in year_dir.glob('*.grb'):
190
+ dst_file = results_exp_dir / src_file.name
191
+ dst_file.parent.mkdir(parents=True, exist_ok=True)
192
+ # Use absolute paths for these links as they point from the data dir to the mod dir,
193
+ # which can be far apart.
194
+ copy_or_link(src_file, dst_file, symlink=True, relative=False)
195
+ if debug:
196
+ print(f"[LINK] {dst_file} -> {src_file}")
197
+
198
+ def setup(mode: str, version: str, debug: bool = False, config_file: str = None):
110
199
 
111
200
  # Define source root
112
201
  qlc_root = Path(__file__).resolve().parent.parent
@@ -117,73 +206,92 @@ def setup(mode, config_file=None, version="latest"):
117
206
 
118
207
  from qlc.py.version import QLC_VERSION as version
119
208
 
120
- # Select appropriate configuration file
121
- if mode == "cams":
122
- perm_path = Path(os.environ.get("PERM", "/perm")) / os.environ.get("USER", "user")
123
- user_home = perm_path
124
- selected_conf = config_src / "qlc_cams.conf"
125
- elif mode == "test":
126
- user_home = Path.home()
127
- selected_conf = config_src / "qlc_test.conf"
128
- elif mode == "interactive":
129
- if not config_file:
130
- raise ValueError("You must provide a config file via --interactive=<path>")
131
- selected_conf = Path(config_file)
132
- else:
133
- raise ValueError(f"Unsupported mode: {mode}")
134
-
135
- source = user_home / f"qlc_v{version}"
209
+ # --- QLC paths are now consistently based on $HOME ---
210
+ install_base = Path.home()
211
+ user_home = install_base
212
+ print(f"[INFO] Using $HOME as installation base: {install_base}")
213
+
214
+ # The stable link path, always $HOME/qlc
215
+ qlc_stable_link = user_home / "qlc"
216
+
217
+ # The versioned installation directory, e.g., $HOME/qlc_v0.3.25
218
+ versioned_install_dir = user_home / f"qlc_v{version}"
219
+
220
+ # The mode-specific root, e.g., $HOME/qlc_v0.3.25/test
221
+ root = versioned_install_dir / mode
136
222
 
137
- if source.exists() and not source.is_symlink():
138
- backup = source.with_name(f"{source.name}_backup")
223
+ # --- Backup Logic: Back up the entire versioned directory if the specific mode being installed already exists ---
224
+ if root.exists():
225
+ backup_name = f"{versioned_install_dir.name}_backup"
226
+ backup = versioned_install_dir.with_name(backup_name)
139
227
  count = 1
140
228
  while backup.exists():
141
- backup = source.with_name(f"{source.name}_backup{count}")
229
+ backup = versioned_install_dir.with_name(f"{backup_name}{count}")
142
230
  count += 1
143
- print(f"[BACKUP] Moving existing install root {source} → {backup}")
144
- shutil.move(str(source), str(backup))
145
-
146
- root = source / mode
147
- # safe_move_and_link(source, Path.home() / f"qlc_v{version}")
231
+ print(f"[BACKUP] Moving existing install root {versioned_install_dir} → {backup}")
232
+ shutil.move(str(versioned_install_dir), str(backup))
148
233
 
149
234
  print(f"[SETUP] Mode: {mode}, Version: {version}")
150
- print(f"[PATHS] QLC Root: {root}")
235
+ print(f"[PATHS] QLC Install Root: {root}")
236
+
237
+ # Create essential directories for the mode
238
+ root.mkdir(parents=True, exist_ok=True)
151
239
 
152
- # Prepare paths
240
+ # Prepare paths inside the versioned, mode-specific directory
153
241
  config_dst = root / "config"
154
242
  example_dst = root / "examples"
155
243
  bin_dst = root / "bin"
156
- log_dst = root / "log"
157
244
  mod_dst = root / "mod"
158
245
  obs_dst = root / "obs"
159
246
  doc_dst = root / "doc"
160
- run_dst = root / "run"
161
- out_dst = root / "output"
162
247
  plug_dst = root / "plugin"
163
-
164
- # Create distribution directories
165
- for path in [bin_dst, log_dst, mod_dst, obs_dst, doc_dst, run_dst, out_dst, plug_dst, example_dst, config_dst]:
248
+
249
+ # Create non-data directories inside the mode-specific root
250
+ # NOTE: 'run' and 'output' are now handled by setup_data_directories
251
+ for path in [config_dst, example_dst, bin_dst, mod_dst, obs_dst, doc_dst, plug_dst]:
166
252
  path.mkdir(parents=True, exist_ok=True)
167
253
 
254
+ # --- Setup the new data directory structure INSIDE the mode root ---
255
+ setup_data_directories(root, mode)
256
+
168
257
  # Copy config files
169
- if config_dst.exists():
170
- copytree_with_symlinks(config_src, config_dst)
258
+ shutil.copytree(config_src, config_dst, dirs_exist_ok=True)
259
+ print(f"[COPY] {config_src} -> {config_dst}")
171
260
 
172
- # Copy example files
261
+ # Link example directories instead of copying
173
262
  if example_src.exists():
174
- # shutil.copytree(example_src, example_dst, dirs_exist_ok=True, symlinks=True)
175
- copytree_with_symlinks(example_src, example_dst)
263
+ for item in example_src.iterdir():
264
+ if item.is_dir(): # Only link directories
265
+ dst_link = example_dst / item.name
266
+ if dst_link.exists() or dst_link.is_symlink():
267
+ if dst_link.is_dir() and not dst_link.is_symlink():
268
+ shutil.rmtree(dst_link)
269
+ else:
270
+ dst_link.unlink()
271
+ dst_link.symlink_to(item.resolve(), target_is_directory=True)
272
+ print(f"[LINK] {dst_link} -> {item}")
176
273
 
177
274
  # Link all documentation files
178
275
  # shutil.copytree(doc_src, doc_dst, dirs_exist_ok=True)
179
276
  for doc_file in doc_src.glob("*"):
180
277
  dst = doc_dst / doc_file.name
181
- copy_or_link(doc_file, dst, symlink=True)
278
+ copy_or_link(doc_file, dst, symlink=True, relative=False)
279
+ print(f"[LINK] {dst} -> {doc_file.resolve()}")
182
280
 
183
281
  # Link all *.sh files to bin_dst (helpers included)
184
282
  for sh_file in sh_src.glob("*.sh"):
185
283
  dst = bin_dst / sh_file.name
186
- copy_or_link(sh_file, dst, symlink=True)
284
+ copy_or_link(sh_file, dst, symlink=True, relative=False)
285
+ print(f"[LINK] {dst} -> {sh_file.resolve()}")
286
+
287
+ # Copy the TeX template directory to bin_dst
288
+ tex_template_src = sh_src / "tex_template"
289
+ tex_template_dst = bin_dst / "tex_template"
290
+ if tex_template_src.is_dir():
291
+ if tex_template_dst.exists():
292
+ shutil.rmtree(tex_template_dst)
293
+ shutil.copytree(tex_template_src, tex_template_dst)
294
+ print(f"[COPY] {tex_template_src} -> {tex_template_dst}")
187
295
 
188
296
  # Create shell tool links (now handled by entry_points in setup.py)
189
297
  pass
@@ -198,17 +306,16 @@ def setup(mode, config_file=None, version="latest"):
198
306
  obs_data_dest_dir = root / "obs/data/ver0d/ebas_daily"
199
307
  obs_data_dest_dir.mkdir(parents=True, exist_ok=True)
200
308
 
201
- # Create the v_20240216 symlink
309
+ # Create the v_20240216 symlink with an absolute path
202
310
  link1_target = obs_data_dest_dir / "v_20240216"
203
- if link1_target.exists() or link1_target.is_symlink():
204
- link1_target.unlink()
205
- link1_target.symlink_to(obs_data_source.resolve(), target_is_directory=True)
206
- print(f"[LINK] {link1_target} -> {obs_data_source}")
311
+ copy_or_link(obs_data_source, link1_target, symlink=True, relative=False)
312
+ print(f"[LINK] {link1_target} -> {obs_data_source.resolve()}")
207
313
 
208
314
  # Create the latest symlink, relative to its location
209
315
  link2_target = obs_data_dest_dir / "latest"
210
316
  if link2_target.exists() or link2_target.is_symlink():
211
317
  link2_target.unlink()
318
+ # This one MUST be relative by its nature
212
319
  link2_target.symlink_to("v_20240216", target_is_directory=True)
213
320
  print(f"[LINK] {link2_target} -> v_20240216")
214
321
 
@@ -220,52 +327,110 @@ def setup(mode, config_file=None, version="latest"):
220
327
  print(f"[COPY] {station_file_dest}")
221
328
 
222
329
 
223
- # Link sample model data (unchanged)
330
+ # Link sample model data, ensuring absolute paths
224
331
  mod_data_src_root = root / "examples" / "cams_case_1" / "mod"
225
332
  mod_data_dst_root = root / "mod"
226
333
  if mod_data_src_root.is_dir():
227
334
  for model_dir in mod_data_src_root.iterdir():
228
335
  if model_dir.is_dir():
229
- mod_data_dst = mod_data_dst_root / model_dir.name
230
- if mod_data_dst.exists() or mod_data_dst.is_symlink():
231
- mod_data_dst.unlink()
232
- mod_data_dst.symlink_to(model_dir.resolve(), target_is_directory=True)
233
- print(f"[LINK] {mod_data_dst} -> {model_dir}")
234
-
235
- # Create a generic qlc.conf symlink
236
- config_dir = root / "config"
237
- generic_config_path = config_dir / "qlc.conf"
238
- if generic_config_path.exists() or generic_config_path.is_symlink():
239
- generic_config_path.unlink()
240
-
241
- if selected_conf.exists():
242
- generic_config_path.symlink_to(selected_conf.name)
243
- print(f"[LINK] {generic_config_path} -> {selected_conf.name}")
336
+ dst_link = mod_data_dst_root / model_dir.name
337
+ # Use copy_or_link to create an absolute symlink
338
+ copy_or_link(model_dir, dst_link, symlink=True, relative=False)
339
+ print(f"[LINK] {dst_link} -> {model_dir.resolve()}")
340
+
341
+ # Link model experiment files to the 'Results' directory (relative)
342
+ results_dst_root = root / "Results"
343
+ results_dst_root.mkdir(exist_ok=True)
344
+ print(f"[SETUP] Linking model experiments to {results_dst_root}")
345
+
346
+ if mod_data_dst_root.is_dir():
347
+ for exp_dir in mod_data_dst_root.iterdir():
348
+ if exp_dir.is_dir():
349
+ # This is an experiment dir, e.g., /path/to/test/mod/b2ro
350
+ results_exp_dir = results_dst_root / exp_dir.name
351
+ results_exp_dir.mkdir(exist_ok=True)
352
+
353
+ # Find all .grb files, searching recursively through year folders
354
+ for year_dir in exp_dir.iterdir():
355
+ if year_dir.is_dir():
356
+ for src_file in year_dir.glob('*.grb'):
357
+ dst_file = results_exp_dir / src_file.name
358
+ dst_file.parent.mkdir(parents=True, exist_ok=True)
359
+ # Use absolute paths for these links as they point from the data dir to the mod dir,
360
+ # which can be far apart.
361
+ copy_or_link(src_file, dst_file, symlink=True, relative=False)
362
+ if debug:
363
+ print(f"[LINK] {dst_file} -> {src_file}")
364
+
365
+ # In CAMS mode: link to operational directories
366
+ if mode == "cams":
367
+ # Link obs data directory
368
+ cams_obs_src = Path("/ec/vol/cams/qlc/obs")
369
+ if obs_dst.is_symlink() or obs_dst.is_symlink():
370
+ obs_dst.unlink()
371
+ elif obs_dst.is_dir():
372
+ shutil.rmtree(obs_dst)
373
+ obs_dst.symlink_to(cams_obs_src, target_is_directory=True)
374
+ print(f"[LINK] {obs_dst} -> {cams_obs_src}")
375
+
376
+ # Link mod directory to this mode's internal Results directory
377
+ results_link = root / "Results"
378
+ if mod_dst.is_symlink() or mod_dst.is_symlink():
379
+ mod_dst.unlink()
380
+ elif mod_dst.is_dir():
381
+ shutil.rmtree(mod_dst)
382
+ mod_dst.symlink_to(results_link, target_is_directory=True)
383
+ print(f"[LINK] {mod_dst} -> {results_link}")
384
+
385
+ # The config is now static, just need to update the version
386
+ generic_config_path = config_dst / "qlc.conf"
387
+ update_qlc_version(generic_config_path, version)
388
+
389
+ # --- Setup master symlinks to point to this installation ---
390
+
391
+ qlc_latest_link = user_home / "qlc_latest"
392
+ qlc_stable_link = user_home / "qlc"
393
+
394
+ # Forcefully remove existing links to ensure a clean state
395
+ print(f"[LINK] Removing existing master links if they exist: {qlc_stable_link.name}, {qlc_latest_link.name}")
396
+ qlc_stable_link.unlink(missing_ok=True)
397
+ qlc_latest_link.unlink(missing_ok=True)
398
+
399
+ # Create qlc_latest -> qlc_vX.Y.Z/<mode> (relative link)
400
+ # Target path is relative to the link's location ($HOME)
401
+ latest_target = os.path.relpath(root, user_home)
402
+ print(f"[LINK] Creating master link: {qlc_latest_link} -> {latest_target}")
403
+ qlc_latest_link.symlink_to(latest_target, target_is_directory=True)
404
+
405
+ # Create qlc -> qlc_latest (relative link)
406
+ # Target path is relative to the link's location ($HOME)
407
+ stable_target = os.path.relpath(qlc_latest_link, user_home)
408
+ print(f"[LINK] Creating stable link: {qlc_stable_link} -> {stable_target}")
409
+ qlc_stable_link.symlink_to(stable_target, target_is_directory=True)
410
+
244
411
 
245
412
  # Write install info
246
413
  info = {
247
414
  "version": version,
248
415
  "mode": mode,
249
- "config": selected_conf.name
416
+ "config": "qlc.conf"
250
417
  }
251
418
 
252
- # Preemptively remove 'qlc_latest' symlink to ensure clean update, mimicking `ln -sf`.
253
- qlc_latest_link = Path.home() / "qlc_latest"
254
- if qlc_latest_link.is_symlink():
255
- qlc_latest_link.unlink()
256
-
257
- safe_move_and_link(root, qlc_latest_link)
258
- safe_move_and_link(qlc_latest_link, Path.home() / "qlc")
259
-
260
419
  (root / "VERSION.json").write_text(json.dumps(info, indent=2))
261
420
  print(f"[WRITE] VERSION.json at {root}")
262
421
 
263
- version_info = read_version_json(Path.home() / "qlc" / "VERSION.json")
264
- update_qlc_version(generic_config_path, version_info["version"])
422
+ # Final version update on the stable link path
423
+ update_qlc_version(qlc_stable_link / "config" / "qlc.conf", version)
265
424
 
266
425
  print("\n[INFO] QLC installation complete.")
267
- print("[INFO] To get started, you may need to open a new terminal or run 'rehash'.")
268
426
  print("[INFO] The following commands are now available: qlc, qlc-py, sqlc, qlc-install")
427
+ print("\n[ACTION REQUIRED]")
428
+ print("To make these commands available, you may need to add the local bin directory to your PATH.")
429
+ print("For most bash users, you can do this by running the following command:")
430
+ print(" echo 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc")
431
+ print("\nFor other shells (like zsh), you may need to add this line to '~/.zshrc' instead.")
432
+ print("After running the command, please open a new terminal or run 'source ~/.bashrc' to apply the changes.")
433
+
269
434
 
270
435
  if __name__ == "__main__":
271
436
  parser = argparse.ArgumentParser(description="Install QLC runtime structure")
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
qlc/sh/qlc_A1.sh CHANGED
@@ -1,4 +1,4 @@
1
- #!/bin/sh -e
1
+ #!/bin/bash -e
2
2
 
3
3
  # Source the configuration file to load the settings
4
4
  . "$CONFIG_FILE"
@@ -34,16 +34,35 @@ for exp in $exps ; do
34
34
  mkdir -p $MARS_RETRIEVAL_DIRECTORY/$exp
35
35
  fi
36
36
 
37
- EXPCLASS=`echo ${exp} | cut -c 1`
38
- if [ "${EXPCLASS}" == "b" ]; then
39
- XCLASS="nl"
40
- elif [ "${EXPCLASS}" == "a" ]; then
41
- XCLASS="be"
42
- else
43
- XCLASS="rd"
44
- fi
45
- log ${exp} ${XCLASS}
46
-
37
+ # Map the experiment prefix to the corresponding ECMWF MARS class.
38
+ # This is required for retrieving data from the MARS archive.
39
+ EXPCLASS=$(echo "${exp}" | cut -c 1)
40
+
41
+ # For experiments other than 'nl', 'be', or 'rd', please uncomment the corresponding XCLASS line.
42
+ case "${EXPCLASS}" in
43
+ a) XCLASS="be" ;; # Belgium
44
+ b) XCLASS="nl" ;; # Netherlands
45
+ # c) XCLASS="fr" ;; # France
46
+ # d) XCLASS="de" ;; # Germany
47
+ # e) XCLASS="es" ;; # Spain
48
+ # f) XCLASS="fi" ;; # Finland
49
+ # g) XCLASS="gr" ;; # Greece
50
+ # h) XCLASS="hu" ;; # Hungary
51
+ # i) XCLASS="it" ;; # Italy
52
+ # k) XCLASS="dk" ;; # Denmark
53
+ # l) XCLASS="pt" ;; # Portugal
54
+ # m) XCLASS="at" ;; # Austria
55
+ # n) XCLASS="no" ;; # Norway
56
+ # s) XCLASS="se" ;; # Sweden
57
+ # t) XCLASS="tr" ;; # Turkey
58
+ # u) XCLASS="uk" ;; # United Kingdom
59
+ # w) XCLASS="ch" ;; # Switzerland
60
+ *) XCLASS="rd" ;; # Default to Research Department
61
+ esac
62
+ log "${exp}" "${XCLASS}"
63
+
64
+ # --------------------------------------------------------------------
65
+ # 2. MARS request for sfc data
47
66
  for name in "${MARS_RETRIEVALS[@]}"; do
48
67
  nml_name="mars_${name}.nml"
49
68
  log "Processing subscript: $nml_name"
qlc/sh/qlc_B1a.sh CHANGED
@@ -1,4 +1,4 @@
1
- #!/bin/sh -e
1
+ #!/bin/bash -e
2
2
 
3
3
  # Source the configuration file to load the settings
4
4
  . "$CONFIG_FILE"
@@ -65,7 +65,6 @@ for exp in $exps ; do
65
65
 
66
66
  for name in "${MARS_RETRIEVALS[@]}"; do
67
67
 
68
- set +e
69
68
  # List available GRIB files for selected exp and time period
70
69
  # grbfiles=($(ls *${mDate}*_${name}_*.grb))
71
70
  grbfiles=($(ls *${mDate}*_${name}*.grb))
@@ -93,21 +92,6 @@ for exp in $exps ; do
93
92
  ls -lh $ncfile
94
93
  # ncdump -h $ncfile
95
94
  fi
96
- # conversion of tine averaged grib files to netcdf (seasmean)
97
- ncfil2="${gribfile%.grb}_tavg.nc"
98
- if [ ! -f "$ncfil2" ]; then
99
- log "cdo timavg ${gribfile} ${gribfile%.grb}.grbtavg"
100
- cdo timavg ${gribfile} ${gribfile%.grb}.grbtavg
101
- log "cdo -f nc copy ${gribfile%.grb}.grbtavg $ncfil2"
102
- cdo -f nc copy ${gribfile%.grb}.grbtavg $ncfil2
103
- # rm -f "$gribfile" # Clean up GRIB file
104
- ls -lh $ncfil2
105
- ncdump -h $ncfil2
106
- else
107
- log "Nothing to do! NC-file already exists: $ncfil2"
108
- ls -lh $ncfil2
109
- # ncdump -h $ncfil2
110
- fi
111
95
  done # file
112
96
  log "----------------------------------------------------------------------------------------"
113
97
  fi
@@ -120,4 +104,3 @@ log "End ${SCRIPT} at `date`"
120
104
  log "________________________________________________________________________________________"
121
105
 
122
106
  exit 0
123
-