Cycles-utils 4.0.2__tar.gz → 4.0.3__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.
Files changed (41) hide show
  1. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/Cycles_utils.egg-info/PKG-INFO +1 -1
  2. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/PKG-INFO +1 -1
  3. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/__init__.py +1 -0
  4. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles.py +2 -3
  5. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles_runner.py +2 -4
  6. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles_tools/__init__.py +2 -2
  7. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles_tools/_base_file.py +1 -1
  8. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles_tools/control_file.py +29 -0
  9. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles_tools/nudge_file.py +8 -0
  10. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles_tools/output_file.py +1 -1
  11. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles_tools/plot_tools.py +47 -0
  12. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles_tools/reinit_file.py +4 -4
  13. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles_tools/soil_file.py +5 -49
  14. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/gadm/__init__.py +1 -1
  15. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/rotation_builder.py +3 -4
  16. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/soilgrids/soilgrids.py +4 -6
  17. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/ssurgo/ssurgo.py +4 -2
  18. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/setup.py +1 -1
  19. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/Cycles_utils.egg-info/SOURCES.txt +0 -0
  20. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/Cycles_utils.egg-info/dependency_links.txt +0 -0
  21. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/Cycles_utils.egg-info/requires.txt +0 -0
  22. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/Cycles_utils.egg-info/top_level.txt +0 -0
  23. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/LICENSE +0 -0
  24. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/MANIFEST.in +0 -0
  25. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/README.md +0 -0
  26. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles_tools/operation_file.py +0 -0
  27. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/cycles_tools/weather_file.py +0 -0
  28. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/data/GLDASp5_elevation_025d.nc4 +0 -0
  29. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/data/GLDASp5_landmask_025d.nc4 +0 -0
  30. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/data/NLDAS_elevation.nc4 +0 -0
  31. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/data/NLDAS_masks-veg-soil.nc4 +0 -0
  32. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/data/fips_gid_conversion.csv +0 -0
  33. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/data/gridMET_elevation_mask.nc +0 -0
  34. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/data/metdata_elevationdata.nc +0 -0
  35. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/data/us_states.csv +0 -0
  36. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/gadm/gadm.py +0 -0
  37. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/soilgrids/__init__.py +0 -0
  38. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/ssurgo/__init__.py +0 -0
  39. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/weather/__init__.py +0 -0
  40. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/cycles/weather/weather.py +0 -0
  41. {cycles_utils-4.0.2 → cycles_utils-4.0.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Cycles-utils
3
- Version: 4.0.2
3
+ Version: 4.0.3
4
4
  Summary: Python scripts to build Cycles input files and post-process Cycles output files
5
5
  Home-page: https://github.com/PSUmodeling/Cycles-utils
6
6
  Author: Yuning Shi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Cycles-utils
3
- Version: 4.0.2
3
+ Version: 4.0.3
4
4
  Summary: Python scripts to build Cycles input files and post-process Cycles output files
5
5
  Home-page: https://github.com/PSUmodeling/Cycles-utils
6
6
  Author: Yuning Shi
@@ -12,6 +12,7 @@ from .cycles_tools import SoilLayer
12
12
  from .cycles_tools import plot_yield
13
13
  from .cycles_tools import plot_operations
14
14
  from .cycles_tools import plot_map
15
+ from .cycles_tools import plot_satellite_map
15
16
  from .cycles_tools import Operation, Planting, Tillage, Harvest, Kill, FixedFertilization, FixedIrrigation, AutoIrrigation
16
17
  from .cycles_runner import CyclesRunner
17
18
  from .rotation_builder import CyclesRotationBuilder, Crop, CropGroup
@@ -5,7 +5,6 @@ import pandas as pd
5
5
  import subprocess
6
6
  from collections.abc import Collection
7
7
  from dataclasses import dataclass, field
8
- from datetime import datetime
9
8
  from pathlib import Path
10
9
  from matplotlib.axes import Axes
11
10
  from .cycles_tools import SoilLayer, ControlConfig
@@ -27,8 +26,8 @@ class Output:
27
26
  class Cycles:
28
27
  """Interface for executing one Cycles simulation and reading its files.
29
28
 
30
- Provides methods to run simulations, read outputs, and inspect soil/weather/operation
31
- configurations. Automatically loads the control file upon initialization.
29
+ Provides methods to run simulations, read outputs, and inspect soil/weather/operation configurations. Automatically
30
+ loads the control file upon initialization.
32
31
 
33
32
  Attributes:
34
33
  path: Path to the simulation directory (containing input/ and output/ subdirs).
@@ -29,9 +29,8 @@ class SimulationContext:
29
29
  class CyclesRunner:
30
30
  """Run one or many Cycles simulations with templated inputs.
31
31
 
32
- Manages batch execution of Cycles simulations by generating control files,
33
- operation files, and nudge files from templates and parameter dictionaries.
34
- Consolidates results into a summary CSV file.
32
+ Manages batch execution of Cycles simulations by generating control files, operation files, and nudge files from
33
+ templates and parameter dictionaries. Consolidates results into a summary CSV file.
35
34
 
36
35
  Attributes:
37
36
  executable: Absolute path to the Cycles executable binary.
@@ -110,7 +109,6 @@ class CyclesRunner:
110
109
 
111
110
 
112
111
  def _resolve(self, simulation: dict[str, Any], control_dict: dict[str, Any], operation_dict: dict[str, Any] | None, calibration_dict: dict[str, Any] | None) -> SimulationContext:
113
- """Call control_dict once per row and resolve all derived values."""
114
112
  control = resolve_dict_values(control_dict, simulation)
115
113
  return SimulationContext(
116
114
  name=control['simulation_name'],
@@ -9,13 +9,12 @@ from .soil_file import MAPPABLE_PARAMETERS
9
9
  from .soil_file import SoilLayer
10
10
  from .soil_file import generate_soil_file
11
11
  from .soil_file import read_soil_file
12
- from .soil_file import map_to_dataframe
13
- from .soil_file import from_dataframe
14
12
  from .weather_file import read_weather_file
15
13
  from .reinit_file import generate_reinit_file
16
14
  from .plot_tools import plot_yield
17
15
  from .plot_tools import plot_operations
18
16
  from .plot_tools import plot_map
17
+ from .plot_tools import plot_satellite_map
19
18
  from ._base_file import resolve_dict_values
20
19
 
21
20
  __all__ = [
@@ -31,4 +30,5 @@ __all__ = [
31
30
  "plot_yield",
32
31
  "plot_operations",
33
32
  "plot_map",
33
+ "plot_satellite_map",
34
34
  ]
@@ -23,7 +23,7 @@ def write_file(fn: Path, config) -> None:
23
23
 
24
24
 
25
25
  def resolve_dict_values(user_dict: dict, simulation: dict[str, Any] | None) -> dict:
26
- return {key: func(simulation) if callable(func) else func for key, func in user_dict.items()}
26
+ return {key.lower(): func(simulation) if callable(func) else func for key, func in user_dict.items()}
27
27
 
28
28
 
29
29
  def extract(dc_class, resolved: dict) -> dict:
@@ -78,6 +78,35 @@ def _get_soil_layers(fn: Path) -> int:
78
78
  def generate_control_file(fn: str | Path, user_dict: dict, *, simulation_dict: dict[str, Any] | None=None) -> ControlConfig:
79
79
  """Generate and write a Cycles control file.
80
80
 
81
+ Provide either direct values or callables that accept a simulation row and return a value in `user_dict`. The
82
+ parameter names should be in lowercase and correspond to the fields in Cycles simulation control files. If a field
83
+ is not provided, it will be filled with a default value. If a field's value is a callable, it will be called with
84
+ the `simulation_dict` to resolve its value.
85
+
86
+ The following fields are required in `user_dict`:
87
+
88
+ - `simulation_start_year`
89
+ - `simulation_end_year`
90
+ - `rotation_size`
91
+ - `operation_file`
92
+ - `soil_file`
93
+ - `weather_file`
94
+
95
+ The default values for other fields are:
96
+
97
+ - `crop_file`: `GenericCrops.crop`
98
+ - `reinit_file`: `N/A`
99
+ - `soil_layers`: inferred from the soil file (if not provided)
100
+ - `co2_level`: `-999`
101
+ - `use_reinitialization`: `0`
102
+ - `adjusted_yields`: `0`
103
+ - `hydrology_option`: `1`
104
+ - `automatic_nitrogen`: `0`
105
+ - `automatic_phosphorus`: `0`
106
+ - `automatic_sulfur`: `0`
107
+
108
+ All output control fields default to `0`.
109
+
81
110
  Args:
82
111
  fn: Destination control file path.
83
112
  user_dict: Values or callables for control fields.
@@ -45,6 +45,14 @@ def _build_nudge_config(user_dict: dict, simulation_dict: dict[str, Any] | None)
45
45
  def generate_nudge_file(fn: str | Path, user_dict: dict, *, simulation_dict: dict[str, Any] | None=None) -> None:
46
46
  """Write a Cycles nudge file from user-provided values.
47
47
 
48
+ Provide either direct values or callables that accept a simulation row and return a value in `user_dict`. The
49
+ parameter names should be in lowercase and correspond to the fields in Cycles nudge (calibration) files. If a field
50
+ is not provided, it will be filled with a default value. If a field's value is a callable, it will be called with
51
+ the `simulation_dict` to resolve its value.
52
+
53
+ The default values for all calibration multipliers are `1.0`, and the default values for `kd_no3` and `kd_nh4` are
54
+ `0.0` and `5.6`, respectively.
55
+
48
56
  Args:
49
57
  fn: Destination nudge file path.
50
58
  user_dict: Values or callables for nudge parameters.
@@ -15,7 +15,7 @@ def _parse_units(lines: list[str], columns) -> dict[str, str]:
15
15
 
16
16
 
17
17
  def read_output(path: str | Path, output_type: str) -> tuple[pd.DataFrame, dict[str, str]]:
18
- """Read one Cycles output table and associated unit strings.
18
+ """Read one Cycles output file and associated unit strings.
19
19
 
20
20
  Args:
21
21
  path: Directory containing output CSV files.
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
  import cartopy.crs as ccrs
3
3
  import cartopy.feature as feature
4
+ import cartopy.io.img_tiles as cimgt
4
5
  import geopandas as gpd
6
+ import math
5
7
  import matplotlib.lines as mlines
6
8
  import matplotlib.pyplot as plt
7
9
  import numpy as np
@@ -274,3 +276,48 @@ def plot_map(gdf: gpd.GeoDataFrame, column: str, *, projection: ccrs.Projection=
274
276
  ax.set_title(title) # type: ignore
275
277
 
276
278
  return fig, ax # type: ignore
279
+
280
+
281
+ def _zoom_from_extent(extent, *, desired_pixels: int=1024) -> int:
282
+ """Determine the appropriate zoom level for a given map extent.
283
+ Number of global tiles at each zoom level is 2 ^ zoom_level
284
+ """
285
+ PIXELS_PER_TILE = 256
286
+
287
+ dx = abs(extent[1] - extent[0])
288
+ dy = abs(extent[3] - extent[2])
289
+
290
+ return round(max(math.log(desired_pixels / PIXELS_PER_TILE * 180 / dy) / math.log(2), math.log(desired_pixels / PIXELS_PER_TILE * 360 / dx) / math.log(2)))
291
+
292
+
293
+ def plot_satellite_map(fig: Figure, extent: tuple[float, float, float, float], *,
294
+ alpha: float=1.0, ax: tuple[float, float, float, float] | None=None, desired_pixels: int=1024, style: str | None=None) -> tuple[GeoAxes, ccrs.Projection]:
295
+ """Render a satellite basemap over a geographic extent.
296
+
297
+ Args:
298
+ fig: Matplotlib figure used to create the map axes.
299
+ extent: Geographic bounds as ``(west, east, south, north)`` in degrees.
300
+ alpha: Basemap transparency where ``1.0`` is fully opaque.
301
+ ax: Optional axes rectangle passed to ``Figure.add_axes``. If omitted, a full-figure subplot is created.
302
+ desired_pixels: Target figure resolution in pixels used to estimate tile zoom.
303
+ style: Optional Cartopy GoogleTiles style string. If omitted, ArcGIS World Imagery tiles are used.
304
+
305
+ Returns:
306
+ Tuple containing:
307
+ - GeoAxes with the satellite basemap.
308
+ - Projection associated with the tile source.
309
+ """
310
+ GOOGLE_URL = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.jpg'
311
+
312
+ google_satellite = cimgt.GoogleTiles(url=GOOGLE_URL) if style is None else cimgt.GoogleTiles(style=style)
313
+
314
+ if ax is None:
315
+ ax = fig.add_subplot(1, 1, 1, projection=google_satellite.crs) # type: ignore
316
+ else:
317
+ ax = fig.add_axes(ax, projection=google_satellite.crs) # type: ignore
318
+
319
+ assert isinstance(ax, GeoAxes)
320
+ ax.set_extent(extent, crs=ccrs.PlateCarree())
321
+ ax.add_image(google_satellite, _zoom_from_extent(extent, desired_pixels=desired_pixels), alpha=alpha)
322
+
323
+ return ax, google_satellite.crs
@@ -74,12 +74,12 @@ def _format_reinit_block(row: pd.Series, doy: int, n_layers: int) -> list[str]:
74
74
 
75
75
 
76
76
  def generate_reinit_file(out_path: str | Path, in_path: str | Path, doy: int) -> None:
77
- """Write a reinit file for all years at a selected day-of-year.
77
+ """Write a re-initialization file from Cycles output for all years at a selected day-of-year.
78
78
 
79
79
  Args:
80
- out_path: Destination reinit file path.
81
- in_path: Simulation output directory or stem used by ``read_output``.
82
- doy: Day-of-year extracted from reinit output.
80
+ out_path: Destination re-initialization file path.
81
+ in_path: Simulation output directory that contains Cycles output files.
82
+ doy: Day-of-year extracted from re-initialization.
83
83
  """
84
84
  out_path = Path(out_path)
85
85
  in_path = Path(in_path)
@@ -215,50 +215,18 @@ def _parse_token(token: str, param: SoilParameter) -> str | int | float | None:
215
215
  return token
216
216
 
217
217
 
218
- def _row_to_layer(row: pd.Series) -> SoilLayer:
219
- valid_fields = {f.name for f in fields(SoilLayer)}
220
- kwargs = {col: (None if pd.isna(val) else val) for col, val in row.items() if col in valid_fields}
221
- return SoilLayer(**kwargs) # type: ignore
222
-
223
-
224
- def map_layers(profile: list[SoilLayer], target: list[SoilLayer]=DEFAULT_PROFILE, parameters: list[str]=MAPPABLE_PARAMETERS, soil_depth: float | None=None) -> list[SoilLayer]:
225
- """Map measured soil properties onto a target profile.
226
-
227
- The mapping is performed by overlap-weighted averaging for each requested
228
- parameter, after trimming target layers to the effective profile depth.
229
-
230
- Args:
231
- profile: Soil profile layers in depth order.
232
- target: Target layer structure to map onto.
233
- parameters: Soil parameters to map from measured profile to target.
234
- soil_depth: Optional maximum depth (m) to include in the mapped profile.
235
-
236
- Returns:
237
- A list of mapped soil layers aligned to the target profile.
238
- """
218
+ def _map_layers(profile: list[SoilLayer], target: list[SoilLayer]=DEFAULT_PROFILE, parameters: list[str]=MAPPABLE_PARAMETERS, soil_depth: float | None=None) -> list[SoilLayer]:
239
219
  trimmed = _trim(target, profile[-1].bottom, soil_depth)
240
220
  return [_map_layer(layer, profile, parameters) for layer in trimmed]
241
221
 
242
222
 
243
- def map_to_dataframe(layers: list[SoilLayer]) -> pd.DataFrame:
244
- """Convert soil layers to a tabular representation.
245
-
246
- Args:
247
- layers: Soil layers to convert.
248
-
249
- Returns:
250
- A DataFrame with one row per layer, including computed ``thickness``.
251
- """
252
- return pd.DataFrame([{**asdict(layer), 'thickness': layer.thickness} for layer in layers])
253
-
254
-
255
223
  def generate_soil_file(fn: str | Path, profile: list[SoilLayer], *,
256
224
  target: list[SoilLayer]=DEFAULT_PROFILE, parameters: list[str]=MAPPABLE_PARAMETERS, soil_depth: float | None=None,
257
225
  desc: str='', slope: float | None=None, curve_number: float | None=None, hsg: str='') -> list[SoilLayer]:
258
- """Map layers and write a Cycles-formatted soil file.
226
+ """Generate a Cycles-formatted soil file from a list of soil layers.
259
227
 
260
- The input profile is first mapped to the target layering scheme, then
261
- rendered to Cycles soil-file format and written to ``fn``.
228
+ The input profile is first mapped to the target layering scheme, then rendered to Cycles soil-file format and
229
+ written to ``fn``.
262
230
 
263
231
  Args:
264
232
  fn: Output soil file path.
@@ -279,7 +247,7 @@ def generate_soil_file(fn: str | Path, profile: list[SoilLayer], *,
279
247
  """
280
248
  if curve_number is not None and hsg:
281
249
  raise ValueError("Only one of curve_number and hsg can be provided.")
282
- layers = map_layers(profile, target, parameters, soil_depth)
250
+ layers = _map_layers(profile, target, parameters, soil_depth)
283
251
  Path(fn).write_text('\n'.join(_render_soil_file(layers, desc, slope, curve_number, hsg)) + '\n')
284
252
  return layers
285
253
 
@@ -301,15 +269,3 @@ def read_soil_file(fn: str | Path) -> tuple[list[SoilLayer], dict]:
301
269
  meta, data_lines = _parse_header(lines)
302
270
  layers = _parse_layers(data_lines)
303
271
  return layers, meta
304
-
305
-
306
- def from_dataframe(df: pd.DataFrame) -> list[SoilLayer]:
307
- """Convert a DataFrame into ``SoilLayer`` objects.
308
-
309
- Args:
310
- df: DataFrame containing columns matching ``SoilLayer`` fields.
311
-
312
- Returns:
313
- A list of ``SoilLayer`` instances created row-by-row.
314
- """
315
- return [_row_to_layer(row) for _, row in df.iterrows()]
@@ -7,7 +7,7 @@ from .gadm import county_fips
7
7
  from .gadm import county_gid
8
8
  from .gadm import county_name
9
9
 
10
- _all__ = [
10
+ __all__ = [
11
11
  "read_gadm",
12
12
  "state_abbreviation",
13
13
  "state_fips",
@@ -93,10 +93,9 @@ class RotationResult(NamedTuple):
93
93
  class CyclesRotationBuilder:
94
94
  """Run Cycles iteratively and append economically optimal operations.
95
95
 
96
- Executes Cycles simulations for candidate crop-operation combinations,
97
- selects the highest-return option at each break-point, and iteratively
98
- builds a multi-year rotation. Integrates economic scoring with agronomic
99
- constraints (e.g., minimum planting interval, crop group penalties).
96
+ Executes Cycles simulations for candidate crop-operation combinations, selects the highest-return option at each
97
+ break-point, and iteratively builds a multi-year rotation. Integrates economic scoring with agronomic constraints
98
+ (e.g., minimum planting interval, crop group penalties).
100
99
 
101
100
  Attributes:
102
101
  simulation: Base simulation name for generated input/output directories.
@@ -90,10 +90,8 @@ class SoilGrids:
90
90
  """Reproject loaded maps to a reference raster grid.
91
91
 
92
92
  Args:
93
- reference_xds: Reference raster whose CRS, transform, and resolution
94
- are used for reprojection.
95
- reference_name: Column name used for the reference raster values in
96
- the generated matched table.
93
+ reference_xds: Reference raster whose CRS, transform, and resolution are used for reprojection.
94
+ reference_name: Column name used for the reference raster values in the generated matched table.
97
95
  boundary: Boundary geometry used to clip all rasters after reprojection.
98
96
 
99
97
  Returns:
@@ -211,8 +209,8 @@ def download_soilgrids_data(path: str | Path, *,
211
209
  maps: list[str]=ALL_MAPS, boundary: Polygon | None=None, bbox: tuple[float, float, float, float] | None=None, crs: str='epsg:4326') -> None:
212
210
  """Download SoilGrids raster layers via WCS.
213
211
 
214
- You can provide either a boundary polygon or an explicit bounding box.
215
- Bounding boxes are expected in ``(west, south, east, north)`` order.
212
+ You can provide either a boundary polygon or an explicit bounding box. Bounding boxes are expected in
213
+ ``(west, south, east, north)`` order.
216
214
 
217
215
  Args:
218
216
  path: Directory where downloaded GeoTIFF files are written.
@@ -90,8 +90,10 @@ class Ssurgo:
90
90
 
91
91
  def __init__(self, path: str | Path, state: str, *, lat_lon: LatLon | None=None, boundary: gpd.GeoDataFrame | None=None) -> None:
92
92
  """Initialize SSURGO lookup tables and optional spatial subset.
93
- When a location (`lat_lon`) or boundary polygon is provided, the map-unit table is filtered to include only those map units that intersect the location or polygon;
94
- the map-units are also grouped by name and symbol, and the major map unit is selected for profile extraction.
93
+
94
+ When a location (`lat_lon`) or boundary polygon is provided, the map-unit table is filtered to include only
95
+ those map units that intersect the location or polygon; the map-units are also grouped by name and symbol, and
96
+ the major map unit is selected for profile extraction.
95
97
 
96
98
  Args:
97
99
  path: Directory containing SSURGO geodatabase and lookup CSV files.
@@ -5,7 +5,7 @@ with open('README.md', 'r') as f:
5
5
 
6
6
  setup(
7
7
  name='Cycles-utils',
8
- version='4.0.2',
8
+ version='4.0.3',
9
9
  author='Yuning Shi',
10
10
  author_email="shiyuning@gmail.com",
11
11
  packages=find_packages(),
File without changes
File without changes
File without changes
File without changes