Skip to content
This repository has been archived by the owner on Aug 8, 2024. It is now read-only.

Add CI and ability to control simulation dates #35

Merged
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cf16a0a
first commit on automated_testing branch: sets up c-star and runs the…
Jul 25, 2024
98e7fcd
add coverage to environment.yml
Jul 25, 2024
1fd2019
add coverage.toml file
Jul 25, 2024
35803f8
move coverage.toml to top level dir
Jul 25, 2024
c7a2889
add printenv to tests.yaml
Jul 25, 2024
fb8b226
modify environment.py so env is configured on github runner
Jul 25, 2024
56a53f3
fix path to blueprint in test
Jul 25, 2024
ba58325
fix path to blueprint in test ii
Jul 25, 2024
a205a78
add case for linux_x86_64 for exec_pfx in component.py
Jul 25, 2024
32a1d2b
Update README.md
dafyddstephenson Jul 25, 2024
d808c57
Update README.md
dafyddstephenson Jul 25, 2024
c18b93e
add comments to environment.py with hints for future development
Jul 25, 2024
560184b
Several changes involving simulation dates:
Jul 26, 2024
db744e7
add dateutil to CI environment
Jul 27, 2024
0237188
Further changes related to simulation dates:
Jul 29, 2024
73d12b2
changes after pre-commit checks
Aug 1, 2024
821052e
adjust run legth in test script to 30 mins from 1 day
Aug 1, 2024
326509c
tidying after reviewing during PR
Aug 1, 2024
67f8550
update docstring for ROMSComponent
Aug 1, 2024
0268749
update __str__ functions to reflect new attributes in InputDataset an…
Aug 1, 2024
27eae81
update codecov badge url in README
Aug 1, 2024
a191c93
add utility function for cloning/checking out git repos, use this fun…
Aug 8, 2024
ff1be54
update _clone_and_checkout to print subprocess stderr on failure
Aug 8, 2024
975db23
add print statement to end of test routine and TODO in base_model
Aug 8, 2024
d5bafeb
convert generated cstar_local_config.py to define a function set_loca…
Aug 8, 2024
5f7e5fd
convert generated cstar_local_config.py to define a function set_loca…
Aug 8, 2024
f2c402e
update roms_marbl_blueprint commit hash to reflect ucla-roms PR30
Aug 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CI

on:
push:
branches:
- '*'
pull_request:
branches:
- main

jobs:

test:
name: ${{ matrix.python-version }}-build
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12",]
steps:
- uses: actions/checkout@v4

- name: Create conda environment
uses: mamba-org/setup-micromamba@v1
with:
cache-downloads: true
cache-environment: true
micromamba-version: 'latest'
environment-file: ci/environment.yml
create-args: |
python=${{ matrix.python-version }}

- name: Environment info
shell: bash
run: |
conda info
printenv

- name: Install C-Star
shell: micromamba-shell {0}
run: |
python - V
python -m pip install -e cstar_ocean --no-deps --force-reinstall

- name: Running Tests
shell: bash -l {0}
run: |
python -V
coverage run --rcfile=coverage.toml cstar_ocean/tests/test_roms_marbl_example.py

- name: Get coverage report
shell: bash -l {0}
run: |
coverage report -m ; coverage xml

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[![codecov](https://codecov.io/github/dafyddstephenson/C-Star/branch/automated_testing/graph/badge.svg?token=Z0L4U76WSG)](https://codecov.io/github/dafyddstephenson/C-Star)
Copy link

@NoraLoose NoraLoose Aug 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we change the path so it reflects the coverage of the CWorthy-ocean/C-Star main branch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! I think it's done now but will have to check it's worked after the merge

# C-Star
Computational Systems for Tracking Ocean Carbon

Expand Down
15 changes: 15 additions & 0 deletions ci/environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: cstar_env
channels:
- conda-forge
dependencies:
- python>=3.10
- pooch
- pyyaml
- python-dateutil
- coverage
- numpy
- compilers
- netcdf-fortran
- mpich
- nco
- ncview
7 changes: 7 additions & 0 deletions coverage.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[tool.coverage.run]
source = ["cstar_ocean"]
omit = [
"tests/*",
"**/__init__.py",
"setup.py",
]
110 changes: 91 additions & 19 deletions cstar_ocean/cstar_ocean/component.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import os
import glob
import warnings
import subprocess
from abc import ABC, abstractmethod
from typing import List, Optional, Any

from cstar_ocean.utils import _calculate_node_distribution, _replace_text_in_file
from cstar_ocean.base_model import ROMSBaseModel, BaseModel
from cstar_ocean.input_dataset import InputDataset
from cstar_ocean.input_dataset import (
InputDataset,
InitialConditions,
ModelGrid,
SurfaceForcing,
BoundaryForcing,
TidalForcing,
)
from cstar_ocean.additional_code import AdditionalCode

from cstar_ocean.environment import (
Expand Down Expand Up @@ -69,7 +77,7 @@ def __init__(self, **kwargs: Any):
An intialized Component object
"""

# FIXME: do Type checking here
# TODO: do Type checking here
if "base_model" not in kwargs or not isinstance(
kwargs["base_model"], BaseModel
):
Expand Down Expand Up @@ -107,26 +115,27 @@ def __str__(self):
)

base_str += "\n\nDiscretization info:"
if hasattr(self,'n_procs_x') and self.n_procs_x is not None:
if hasattr(self, "time_step") and self.time_step is not None:
base_str += "\ntime_step: " + str(self.time_step)
if hasattr(self, "n_procs_x") and self.n_procs_x is not None:
base_str += (
"\nn_procs_x:"
"\nn_procs_x: "
+ str(self.n_procs_x)
+ " (Number of x-direction processors)"
)
if hasattr(self,'n_procs_y') and self.n_procs_y is not None:
if hasattr(self, "n_procs_y") and self.n_procs_y is not None:
base_str += (
"\nn_procs_y:"
+ str(self.n_procs_y)
+ " (Number of y-direction processors)"
)
if hasattr(self,'n_levels') and self.n_levels is not None:
if hasattr(self, "n_levels") and self.n_levels is not None:
base_str += "\nn_levels:" + str(self.n_levels)
if hasattr(self,'nx') and self.nx is not None:
if hasattr(self, "nx") and self.nx is not None:
base_str += "\nnx:" + str(self.nx)
if hasattr(self,'ny') and self.ny is not None:
if hasattr(self, "ny") and self.ny is not None:
base_str += "\nny:" + str(self.ny)

if hasattr(self,'exe_path') and self.exe_path is not None:
if hasattr(self, "exe_path") and self.exe_path is not None:
base_str += "\n\nIs compiled: True"
base_str += "\n exe_path: " + self.exe_path
return base_str
Expand Down Expand Up @@ -186,6 +195,8 @@ class ROMSComponent(Component):
input_datasets: InputDataset or list of InputDatasets
Any spatiotemporal data needed to run this instance of ROMS
e.g. initial conditions, surface forcing, etc.
time_step: int, Optional, default=1
The time step with which to run ROMS in this configuration
nx,ny,n_levels: int
The number of x and y points and vertical levels in the domain associated with this object
n_procs_x,n_procs_y: int
Expand Down Expand Up @@ -213,6 +224,7 @@ def __init__(
base_model: ROMSBaseModel,
additional_code: Optional[AdditionalCode] = None,
input_datasets: Optional[InputDataset | List[InputDataset]] = None,
time_step: int = 1,
nx: Optional[int] = None,
ny: Optional[int] = None,
n_levels: Optional[int] = None,
Expand Down Expand Up @@ -252,6 +264,7 @@ def __init__(
input_datasets=input_datasets,
)
# QUESTION: should all these attrs be passed in as a single "discretization" arg of type dict?
self.time_step: int = time_step
self.nx: Optional[int] = nx
self.ny: Optional[int] = ny
self.n_levels: Optional[int] = n_levels
Expand Down Expand Up @@ -308,29 +321,61 @@ def pre_run(self):

# Partition input datasets
if self.input_datasets is not None:
datasets_to_partition = (
self.input_datasets
if isinstance(self.input_datasets, list)
else [
if isinstance(self.input_datasets, InputDataset):
dataset_list = [
self.input_datasets,
]
)
elif isinstance(self.input_datasets, list):
dataset_list = self.input_datasets
else:
dataset_list = []

datasets_to_partition = [d for d in dataset_list if d.exists_locally]

for f in datasets_to_partition:
dspath = f.local_path
fname = os.path.basename(f.source)

os.makedirs(dspath + "/PARTITIONED", exist_ok=True)
os.makedirs(os.path.dirname(dspath) + "/PARTITIONED", exist_ok=True)
subprocess.run(
"partit "
+ str(self.n_procs_x)
+ " "
+ str(self.n_procs_y)
+ " ../"
+ fname,
cwd=dspath + "PARTITIONED",
cwd=os.path.dirname(dspath) + "/PARTITIONED",
shell=True,
)
# Edit namelist file to contain dataset paths
forstr = ""
namelist_path = (
self.additional_code.local_path
+ "/"
+ self.additional_code.namelists[0]
)
for f in datasets_to_partition:
partitioned_path = (
os.path.dirname(f.local_path)
+ "/PARTITIONED/"
+ os.path.basename(f.local_path)
)
if isinstance(f, ModelGrid):
gridstr = " " + partitioned_path + "\n"
_replace_text_in_file(
namelist_path, "__GRID_FILE_PLACEHOLDER__", gridstr
)
elif isinstance(f, InitialConditions):
icstr = " " + partitioned_path + "\n"
_replace_text_in_file(
namelist_path, "__INITIAL_CONDITION_FILE_PLACEHOLDER__", icstr
)
elif type(f) in [SurfaceForcing, TidalForcing, BoundaryForcing]:
forstr += " " + partitioned_path + "\n"

_replace_text_in_file(
namelist_path, "__FORCING_FILES_PLACEHOLDER__", forstr
)

################################################################################
## NOTE: we assume that roms.in is the ONLY entry in additional_code.namelists, hence [0]
Expand All @@ -346,10 +391,12 @@ def pre_run(self):
"MARBL_NAMELIST_DIR",
self.additional_code.local_path + "/namelists/MARBL",
)

################################################################################

def run(
self,
n_time_steps: Optional[int] = None,
account_key: Optional[str] = None,
walltime: Optional[str] = _CSTAR_SYSTEM_MAX_WALLTIME,
job_name: str = "my_roms_run",
Expand Down Expand Up @@ -383,9 +430,34 @@ def run(
+ "\nIf you have already run Component.get(), either run it again or "
+ " add the local path manually using Component.additional_code.local_path='YOUR/PATH'."
)
return
else:
run_path = self.additional_code.local_path + "/output/PARTITIONED/"

# Add number of timesteps to namelist
# Check if n_time_steps is None, indicating it was not explicitly set
if n_time_steps is None:
n_time_steps = 1
warnings.warn(
"n_time_steps not explicitly set, using default value of 1. "
"Please call ROMSComponent.run() with the n_time_steps argument "
"to specify the length of the run.",
UserWarning,
)
assert isinstance(n_time_steps, int)

if self.additional_code.namelists is None:
raise ValueError(
"A namelist file (typically roms.in) is needed to run ROMS."
" ROMSComponent.additional_code.namelists should be a non-empty list of filenames."
)

_replace_text_in_file(
self.additional_code.local_path + "/" + self.additional_code.namelists[0],
"__NTIMES_PLACEHOLDER__",
str(n_time_steps),
)

os.makedirs(run_path, exist_ok=True)
if self.exe_path is None:
# FIXME this only works if build() is called in the same session
Expand All @@ -410,9 +482,9 @@ def run(
exec_pfx = "mpirun"
case "osx_arm64":
exec_pfx = "mpirun"
case "linux_x86_64":
exec_pfx = "mpirun"

# FIXME (probably throughout): self.additional_code /could/ be a list
# need to figure out which element to use
roms_exec_cmd = (
f"{exec_pfx} -n {self.n_procs_tot} {self.exe_path} "
+ f"{self.additional_code.local_path}/{self.additional_code.namelists[0]}"
Expand Down
Loading