geebeam 0.2.2__tar.gz → 0.3.2__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.
@@ -1,19 +1,39 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geebeam
3
- Version: 0.2.2
3
+ Version: 0.3.2
4
4
  Summary: Earth Engine + Apache Beam
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.10
8
- Requires-Dist: apache-beam[gcp]>=2.70.0
8
+ Requires-Dist: apache-beam[gcp]==2.72.0
9
+ Requires-Dist: crcmod>=1.7
9
10
  Requires-Dist: dill>=0.4.1
10
11
  Requires-Dist: earthengine-api>=1.7.10
11
12
  Requires-Dist: geopandas>=1.0.1
12
13
  Requires-Dist: grpcio>=1.62.3
13
- Requires-Dist: tensorflow-data-validation>=1.16
14
- Requires-Dist: tensorflow-metadata>=1.16
15
- Requires-Dist: tensorflow>=2.16
16
- Requires-Dist: tfx-bsl>=1.16
14
+ Requires-Dist: rasterio>=1.3.10
15
+ Provides-Extra: all
16
+ Requires-Dist: importlib-resources>=6.5.2; extra == 'all'
17
+ Requires-Dist: pytest; extra == 'all'
18
+ Requires-Dist: ruff; extra == 'all'
19
+ Requires-Dist: tensorflow-data-validation>=1.16; extra == 'all'
20
+ Requires-Dist: tensorflow-datasets>=4.9.9; extra == 'all'
21
+ Requires-Dist: tensorflow-metadata>=1.16; extra == 'all'
22
+ Requires-Dist: tensorflow>=2.16; extra == 'all'
23
+ Requires-Dist: tfx-bsl>=1.16; extra == 'all'
24
+ Provides-Extra: build
25
+ Requires-Dist: hatch; extra == 'build'
26
+ Provides-Extra: lint
27
+ Requires-Dist: ruff; extra == 'lint'
28
+ Provides-Extra: tensorflow
29
+ Requires-Dist: importlib-resources>=6.5.2; extra == 'tensorflow'
30
+ Requires-Dist: tensorflow-data-validation>=1.16; extra == 'tensorflow'
31
+ Requires-Dist: tensorflow-datasets>=4.9.9; extra == 'tensorflow'
32
+ Requires-Dist: tensorflow-metadata>=1.16; extra == 'tensorflow'
33
+ Requires-Dist: tensorflow>=2.16; extra == 'tensorflow'
34
+ Requires-Dist: tfx-bsl>=1.16; extra == 'tensorflow'
35
+ Provides-Extra: test
36
+ Requires-Dist: pytest; extra == 'test'
17
37
  Description-Content-Type: text/markdown
18
38
 
19
39
  # GeeBeam
@@ -114,7 +134,7 @@ python examples/geebeam_run.py \
114
134
  --experiments=use_runner_v2 \
115
135
  --temp_location=gs://[your-bucket]/[path_to_temp_dir]
116
136
  --machine_type=n2-highmem-2 \
117
- --sdk_container_image=us-docker.pkg.dev/mmacedo-reservoirid/geebeam-public/geebeam:latest
137
+ --sdk_container_image=kysolvik/geebeam:latest
118
138
  ```
119
139
 
120
140
  Note in this case your `output_path` in run_pipeline() should be a Google Cloud Storage path. If you're running an older version of geebeam, replace "latest" in the sdk_container_image URI with the version number (e.g. v0.1.2). You can also build your own Docker image to run on. More info in the [DataFlow docs](https://docs.cloud.google.com/dataflow/docs/guides/using-custom-containers).
@@ -96,7 +96,7 @@ python examples/geebeam_run.py \
96
96
  --experiments=use_runner_v2 \
97
97
  --temp_location=gs://[your-bucket]/[path_to_temp_dir]
98
98
  --machine_type=n2-highmem-2 \
99
- --sdk_container_image=us-docker.pkg.dev/mmacedo-reservoirid/geebeam-public/geebeam:latest
99
+ --sdk_container_image=kysolvik/geebeam:latest
100
100
  ```
101
101
 
102
102
  Note in this case your `output_path` in run_pipeline() should be a Google Cloud Storage path. If you're running an older version of geebeam, replace "latest" in the sdk_container_image URI with the version number (e.g. v0.1.2). You can also build your own Docker image to run on. More info in the [DataFlow docs](https://docs.cloud.google.com/dataflow/docs/guides/using-custom-containers).
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.2'
22
- __version_tuple__ = version_tuple = (0, 2, 2)
21
+ __version__ = version = '0.3.2'
22
+ __version_tuple__ = version_tuple = (0, 3, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -10,15 +10,35 @@ description = "Earth Engine + Apache Beam"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
12
12
  dependencies = [
13
- "apache-beam[gcp]>=2.70.0",
13
+ "apache-beam[gcp]==2.72.0",
14
+ "crcmod>=1.7",
14
15
  "dill>=0.4.1",
15
16
  "earthengine-api>=1.7.10",
16
17
  "geopandas>=1.0.1",
18
+ "grpcio>=1.62.3",
19
+ "rasterio>=1.3.10",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ test = ["pytest"]
24
+ lint = ["ruff"]
25
+ build = ["hatch"]
26
+ tensorflow = [
17
27
  "tensorflow>=2.16",
18
28
  "tensorflow-data-validation>=1.16",
19
29
  "tensorflow-metadata>=1.16",
20
30
  "tfx-bsl>=1.16",
21
- "grpcio>=1.62.3",
31
+ "tensorflow-datasets>=4.9.9",
32
+ "importlib-resources>=6.5.2",
33
+ ]
34
+ all = [
35
+ "geebeam[tensorflow,test,lint]"
36
+ ]
37
+
38
+ # Pin upper version of python, since apache-beam only supports to 3.12
39
+ [tool.uv]
40
+ environments = [
41
+ "python_version < '3.13'"
22
42
  ]
23
43
 
24
44
  # Version control
@@ -28,7 +48,6 @@ version-file = "_version.py"
28
48
  [tool.hatch.version]
29
49
  source = "vcs"
30
50
 
31
-
32
51
  [tool.hatch.build.targets.sdist]
33
52
  include = [
34
53
  "src",
@@ -0,0 +1,10 @@
1
+ """Beam and Earth Engine helpers for running data pipelines"""
2
+
3
+ from . import climate_indices
4
+
5
+ from .pipeline import run_pipeline
6
+
7
+ __all__ = [
8
+ "climate_indices",
9
+ "run_pipeline",
10
+ ]
@@ -0,0 +1,94 @@
1
+ """
2
+ Utilities for cleaning, combining, and serializing/deserializing EE objects.
3
+ """
4
+
5
+ import io
6
+
7
+ import ee
8
+ import numpy as np
9
+
10
+ def deserialize(obj_json):
11
+ """Deserialize Earth Engine JSON DAG"""
12
+ return ee.deserializer.fromJSON(obj_json)
13
+
14
+ def serialize(obj_ee):
15
+ """Serialize Earth Engine object to JSON for Dataflow workers"""
16
+ return ee.serializer.toJSON(obj_ee)
17
+
18
+ def get_band_names(input_list):
19
+ """Get simplified band_names for output (without prefixed image_id)
20
+
21
+ TODO: Add year to distinguish multiple years?
22
+ """
23
+ return [
24
+ image.bandNames()
25
+ for image in input_list
26
+ ]
27
+
28
+ def build_prepped_image(input_list, split_processing=False):
29
+ """Combine a list of EE images into single image"""
30
+ band_names = get_band_names(input_list)
31
+ band_names_flat = ee.List(band_names).flatten()
32
+
33
+ if split_processing:
34
+ band_groups = band_names
35
+ else:
36
+ band_groups = [band_names_flat]
37
+ # Final prepped image
38
+ prepped_im = ee.ImageCollection(input_list).toBands().rename(band_names_flat)
39
+ return prepped_im, band_groups, band_names_flat.getInfo()
40
+
41
+ def list_to_im(input_list):
42
+ prepped_im, _ = build_prepped_image(input_list)
43
+ return prepped_im
44
+
45
+ def get_pixels(im, point, patch_size, scale_x, scale_y, crs_code):
46
+ # Make a request object.
47
+ request = {
48
+ 'expression': im,
49
+ 'fileFormat': 'NPY',
50
+ 'grid': {
51
+ 'dimensions': {
52
+ 'width': patch_size,
53
+ 'height': patch_size
54
+ },
55
+ 'affineTransform': {
56
+ 'scaleX': scale_x,
57
+ 'shearX': 0,
58
+ 'translateX': point['x'],
59
+ 'shearY': 0,
60
+ 'scaleY': scale_y,
61
+ 'translateY': point['y']
62
+ },
63
+ 'crsCode': crs_code
64
+ },
65
+ }
66
+
67
+ raw = ee.data.computePixels(request)
68
+
69
+ if not raw:
70
+ raise RuntimeError("Empty EE response")
71
+
72
+ try:
73
+ arr = np.load(io.BytesIO(raw), allow_pickle=True)
74
+ except Exception as e:
75
+ raise RuntimeError(f"Failed to load NPY for coords {point['id']}") from e
76
+
77
+ return arr
78
+
79
+ def get_pixels_allbands(im, band_groups, point, patch_size, scale_x, scale_y, crs_code):
80
+ """Get pixels for all groups of bands"""
81
+ out_ars = []
82
+ for band_list in band_groups:
83
+ prepped_image = im.select(band_list)
84
+ out_ars.append(
85
+ get_pixels(
86
+ im=prepped_image,
87
+ point=point,
88
+ patch_size=patch_size,
89
+ scale_x=scale_x,
90
+ scale_y=scale_y,
91
+ crs_code=crs_code
92
+ )
93
+ )
94
+ return out_ars
@@ -5,12 +5,7 @@ import ee
5
5
  import geopandas as gpd
6
6
  import pandas as pd
7
7
  import numpy as np
8
- import apache_beam as beam
9
8
  from apache_beam.options.pipeline_options import PipelineOptions
10
- import tensorflow_data_validation as tfdv
11
- from tfx_bsl.coders import example_coder
12
- import os
13
- from google.protobuf.json_format import MessageToJson
14
9
 
15
10
  from geebeam import ee_utils, sampler, transforms
16
11
 
@@ -42,6 +37,7 @@ def run_pipeline(
42
37
  patch_size: int,
43
38
  scale: float,
44
39
  n_sample: int,
40
+ output_type: str = 'tiff',
45
41
  sampling_region: str | gpd.GeoDataFrame | ee.Geometry | None = None,
46
42
  sampling_points: pd.DataFrame | gpd.GeoDataFrame | ee.Geometry | None = None,
47
43
  validation_ratio: float = 0.2,
@@ -49,7 +45,9 @@ def run_pipeline(
49
45
  random_seed: int = None,
50
46
  split_processing: bool = False,
51
47
  extra_metadata: dict = {},
52
- beam_options: dict[str] | list[str] | None = None
48
+ beam_options: dict[str] | list[str] | None = None,
49
+ dataset_version: str = '1.0.0',
50
+ dataset_name: str = 'geebeam_dataset'
53
51
  ) -> None:
54
52
  """Run a Beam pipeline to download image chips from Earth Engine.
55
53
 
@@ -58,6 +56,7 @@ def run_pipeline(
58
56
  sampling_region: The region for sampling images.
59
57
  sampling_points: Locations to sample from, specifying upper-left of box.
60
58
  output_path: The path where output will be saved.
59
+ output_type: 'tfrecord' (raw tfrecords) or 'tfds' (tensorflow-dataset).
61
60
  project: The Google Cloud project ID.
62
61
  patch_size: The size of the patches to be processed.
63
62
  scale: The scale factor for image processing.
@@ -121,110 +120,81 @@ def run_pipeline(
121
120
  # Pre-run info:
122
121
  scale_x, scale_y = _prepare_run_metadata(config)
123
122
 
124
- # Set up pipeline
125
-
126
- # Check if a local runner
123
+ # Check if a local runner. If so, add longer job timeout to fix grpcio timeout issue
127
124
  is_local = _check_if_localrunner(pipeline_options)
125
+ if is_local:
126
+ logging.warning('Running on local runner. Setting beam job_server_timeout'
127
+ ' to 9999999 seconds to avoid grpcio timeout errors.')
128
+ pipeline_options_dict = pipeline_options.get_all_options(drop_default=True)
129
+ pipeline_options_dict['job_server_timeout'] = 9999999
130
+ pipeline_options = PipelineOptions.from_dictionary(pipeline_options_dict)
128
131
 
129
132
  # Prepare and serialize inputs
130
133
  # band_groups is a list of lists containing bands to export
131
134
  # if split_processing = False, will contain one list with all bands
132
135
  # if split_processing = True, contains separate band_lists for each image in image_list
133
- prepped_image, band_groups = ee_utils.build_prepped_image(image_list, split_processing=split_processing)
136
+ prepped_image, band_groups, all_bands = ee_utils.build_prepped_image(image_list, split_processing=split_processing)
134
137
  serialized_image = ee_utils.serialize(prepped_image)
135
138
 
136
- # Execute pipeline
137
- with beam.Pipeline(options=pipeline_options) as pipeline:
138
-
139
- points = pipeline | 'Create points' >> beam.Create(input_records)
140
139
 
141
- if is_local:
142
- batches = (
143
- points
144
- | 'Add Dummy Key' >> beam.Map(lambda x: (None, x))
145
- | 'Reshuffle' >> beam.Reshuffle()
146
- | 'Force Single Batches' >> beam.GroupIntoBatches(batch_size=1)
147
- | 'Extract' >> beam.FlatMap(lambda x: x[1])
140
+ # Execute pipeline based on output type:
141
+ if output_type == 'tfrecord':
142
+ try:
143
+ from geebeam import tfrecord_writer
144
+ except ImportError:
145
+ raise ImportError(
146
+ "Missing dependencies for tfrecord writer. "
147
+ "Install them with `pip install geebeam[tensorflow]`"
148
148
  )
149
- else:
150
- batches = points
151
-
152
- training_data, validation_data = (
153
- batches
154
- | 'Get patch' >> beam.ParDo(transforms.EEComputePatch(
155
- config,
156
- serialized_image,
157
- scale_x,
158
- scale_y,
159
- band_groups
160
- ))
161
- | 'Add metadata' >> beam.ParDo(transforms.AddMetadata(extra_metadata))
162
- | 'Split dataset' >> beam.Partition(transforms.split_dataset, 2)
163
- )
164
-
165
- # Convert to TF examples
166
- training_examples = (
167
- training_data
168
- | 'Train to tf.Example' >> beam.Map(transforms.dict_to_example)
149
+ # Write sidecar schema before pipeline execution
150
+ extra_keys = list(extra_metadata.keys())
151
+ transforms.write_sidecar_schema(output_path, all_bands, extra_keys,
152
+ is_gcs=output_path.startswith('gs://'))
153
+ tfrecord_writer.run_tfrecord_export(
154
+ input_records=input_records,
155
+ output_path=output_path,
156
+ config=config,
157
+ serialized_image=serialized_image,
158
+ band_groups=band_groups,
159
+ scale_x=scale_x,
160
+ scale_y=scale_y,
161
+ extra_metadata=extra_metadata,
162
+ pipeline_options=pipeline_options
169
163
  )
170
- # Calculate stats on training data
171
- decoder = example_coder.ExamplesToRecordBatchDecoder()
172
- stats = (
173
- training_examples
174
- | 'Batch' >> beam.BatchElements(
175
- min_batch_size=10,
176
- max_batch_size=100)
177
- | 'Decode to arrow' >> beam.Map(lambda b: decoder.DecodeBatch(b))
178
- | 'Generate Statistics' >> tfdv.GenerateStatistics()
164
+ elif output_type == 'tfds':
165
+ try:
166
+ from geebeam import tfds_writer
167
+ except ImportError:
168
+ raise ImportError(
169
+ "Missing dependencies for TFDS writer. "
170
+ "Install them with `pip install geebeam[tensorflow]`"
171
+ )
172
+ tfds_writer.run_tfds_export(
173
+ input_records=input_records,
174
+ output_path=output_path,
175
+ config=config,
176
+ serialized_image=serialized_image,
177
+ band_groups=band_groups,
178
+ all_bands=all_bands,
179
+ scale_x=scale_x,
180
+ scale_y=scale_y,
181
+ extra_metadata=extra_metadata,
182
+ pipeline_options=pipeline_options,
183
+ dataset_name=dataset_name,
184
+ dataset_version=dataset_version
179
185
  )
180
- stats | 'Write stats' >> tfdv.WriteStatisticsToTFRecord(
181
- os.path.join(output_path, 'stats.tfrecord'))
182
-
183
- # Write out examples
184
- (training_examples
185
- | 'Write training' >> transforms.WriteTFExample(
186
- os.path.join(output_path, 'training'))
186
+ elif output_type == 'tif' or output_type == 'tiff':
187
+ from geebeam import tiff_writer
188
+ tiff_writer.run_tiff_export(
189
+ input_records=input_records,
190
+ output_path=output_path,
191
+ config=config,
192
+ serialized_image=serialized_image,
193
+ band_groups=band_groups,
194
+ scale_x=scale_x,
195
+ scale_y=scale_y,
196
+ extra_metadata=extra_metadata,
197
+ pipeline_options=pipeline_options
187
198
  )
188
- if config['validation_ratio'] > 0:
189
- validation_examples = (
190
- validation_data
191
- | 'Val to tf.Example' >> beam.Map(transforms.dict_to_example)
192
- )
193
-
194
- (validation_examples
195
- | 'Write validation' >> transforms.WriteTFExample(
196
- os.path.join(output_path, 'validation'))
197
- )
198
-
199
-
200
- # NOTE: testing a few different formats.
201
- # Need to simplify once we decide on one
202
- # Infer schema
203
- stats = tfdv.load_statistics(
204
- os.path.join(output_path, 'stats.tfrecord')
205
- )
206
-
207
- schema = tfdv.infer_schema(stats)
208
-
209
- tfdv.write_schema_text(
210
- schema,
211
- os.path.join(output_path, 'schema.pbtxt')
212
- )
213
-
214
- # Also write as pbtxt, easier to read
215
- tfdv.write_stats_text(
216
- stats,
217
- os.path.join(output_path, 'stats.pbtxt')
218
- )
219
-
220
- # Also write stats and schema as jsons
221
- out_schema_json = os.path.join(output_path, 'schema.json')
222
- out_stats_json = os.path.join(output_path, 'stats.json')
223
- if output_path.startswith('gs://'):
224
- transforms.write_json_to_gcs(MessageToJson(schema), out_schema_json)
225
- transforms.write_json_to_gcs(MessageToJson(stats), out_stats_json)
226
199
  else:
227
- transforms.write_json_to_local(MessageToJson(schema), out_schema_json)
228
- transforms.write_json_to_local(MessageToJson(stats), out_stats_json)
229
-
230
-
200
+ raise ValueError(f"output_type {output_type} not implemented. Options are ['tfds', 'tfrecord', 'tiff']")
@@ -0,0 +1,73 @@
1
+ """Helpers for tensorflow"""
2
+
3
+ import tensorflow as tf
4
+ import apache_beam as beam
5
+
6
+ from geebeam import transforms
7
+
8
+ def _bytes_feature(value):
9
+ """Returns a bytes_list from a string / byte."""
10
+ return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))
11
+
12
+ def _float_feature(value):
13
+ """Returns a float_list from a float / double."""
14
+ return tf.train.Feature(float_list=tf.train.FloatList(value=[value]))
15
+
16
+ def _int64_feature(value):
17
+ """Returns an int64_list from a bool / enum / int / uint."""
18
+ return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))
19
+
20
+ def dict_to_example(element):
21
+ """"Convert structured numpy array to tf.Example proto."""
22
+ # First add metadata
23
+ md_dict = {
24
+ 'md_id': _int64_feature(element['metadata']['id']),
25
+ 'md_y': _float_feature(element['metadata']['y']),
26
+ 'md_x': _float_feature(element['metadata']['x']),
27
+ }
28
+ for md_key in element['metadata'].keys():
29
+ if md_key not in ['id','y','x', 'split']:
30
+ md_dict['md_' + md_key] = tf.train.Feature(float_list=
31
+ tf.train.FloatList(
32
+ value = transforms.convert_to_iterable(element['metadata'][md_key])
33
+ )
34
+ )
35
+
36
+ # Build image feature with named bands
37
+ array_dict = {}
38
+ for im_feat in element['array'].keys():#.dtype.names:
39
+ array_dict['im_'+im_feat] = tf.train.Feature(
40
+ float_list = tf.train.FloatList(
41
+ value = element['array'][im_feat].flatten()))
42
+
43
+ # Combine
44
+ feature = {**md_dict, **array_dict}
45
+
46
+ # Build example and serialize
47
+ return tf.train.Example(
48
+ features = tf.train.Features(feature = feature)).SerializeToString()
49
+
50
+ def array_to_example(structured_array):
51
+ """"Convert structured numpy array to tf.Example proto."""
52
+ feature = {}
53
+ for f in structured_array.dtype.names:
54
+ feature[f] = tf.train.Feature(
55
+ float_list = tf.train.FloatList(
56
+ value = structured_array[f].flatten()))
57
+ return tf.train.Example(
58
+ features = tf.train.Features(feature = feature))
59
+
60
+ class WriteTFExample(beam.PTransform):
61
+ """Write example"""
62
+ def __init__(self, output_dir, file_name_suffix='.tfrecord.gz'):
63
+ self.output_dir = output_dir
64
+ self.file_name_suffix = file_name_suffix
65
+
66
+ def expand(self, pcoll):
67
+ return (
68
+ pcoll
69
+ | 'Write to TFRecord' >> beam.io.WriteToTFRecord(
70
+ self.output_dir,
71
+ file_name_suffix=self.file_name_suffix
72
+ )
73
+ )