Usage Guide¶
The simulatr.apsimx module provides everything needed to create,
modify, and run ApsimX crop simulation models and to use them as
gymnasium reinforcement-learning environments.
Model input files¶
An ApsimXFile is a container for
manipulating .apsimx model input files. Files can be generated from
a crop name, copied from a bundled example, or read from an existing
path.
Generating a new file from a crop name¶
from simulatr.apsimx import ApsimXFile
# List the crops that can be simulated
print(ApsimXFile.available_crops())
# List the cultivars for a given crop
print(ApsimXFile.available_cultivars("Wheat"))
# Generate a new Wheat model input file
model = ApsimXFile.from_crop_name("Wheat")
print(model.fname) # "Wheat-Generated.apsimx"
print(model.crop_name) # "wheat"
# Write the file to disk (the model file has already been created)
model.write(overwrite=True)
Generating a file with interactive actions¶
Passing interactive=True (or providing actions) generates a
model file that supports runtime interventions such as sowing,
harvesting, irrigation, and fertilization.
model = ApsimXFile.from_crop_name(
"Wheat",
interactive=True,
actions=["sow", "harvest", "nitrogen", "irrigate"],
)
print(model.fname) # "Wheat-Generated-Interactive.apsimx"
print(model.is_interactive)
Copying a bundled example¶
# Locate the example model file in the ApsimX installation
src = ApsimXFile.find_example("Wheat")
# Copy the example and make it interactive
model = ApsimXFile.from_example(
src,
interactive=True,
actions=["sow", "harvest"],
)
print(model.fname)
print(model.is_interactive)
Reading an existing file¶
model = ApsimXFile("path/to/my-model.apsimx")
print(model.exists)
print(model.crop_name) # crop name inferred from the file
print(model.crop_variety) # e.g. "Hartog"
print(model.location) # e.g. "-27.581836°N, 151.320206°E"
print(model.field_area) # area in hectares
Getting and setting parameters¶
Parameters are addressed by name and can be read with get() and
written with set().
print(model.get("latitude")) # e.g. -27.581836
print(model.get("longitude"))
model.set("latitude", -27.5)
model.write(overwrite=True)
# Values that do not exist are an error unless a default is given
print(model.get("nonexistent", default="not set"))
Moving files¶
# Rename/move the file (returns the new path)
new_fname = model.move(dst="wheat-copy.apsimx")
print(new_fname)
# Or place it in a different directory with a suffix
model.move(directory="outputs", suffix="v2")
Note
crop_name and crop_variety describe the target crop. Files
generated with from_crop_name()
produce a bare simulation skeleton, so parameters such as latitude
are not present until you set them or start from an example file.
File nodes¶
An .apsimx file is a tree of ApsimXFileNode
objects. You generally do not need to work with nodes directly, but they
are available for low-level manipulation.
from simulatr.apsimx import ApsimXFileNode
# Build a node from a data resource bundled with ApsimX
clock = ApsimXFileNode.from_data("Clock")
print(clock.absolute_path) # e.g. "[Clock]"
# Build a node from a model type
soil = ApsimXFileNode.from_param(
"Models.Soils.Arbitrator.SoilArbitrator, Models")
print(soil["Name"])
# Load a node directly from a JSON file
node = ApsimXFileNode.from_file("some-node.json")
# Navigate the tree
for child in soil.children:
print(child.absolute_path)
Weather files¶
ApsimXWeatherFile reads and writes ApsimX
weather files (.met) and can download NASA POWER data for any
location.
import datetime
from simulatr.apsimx import ApsimXWeatherFile
weather = ApsimXWeatherFile.from_location(
latitude=-27.58,
longitude=151.32,
start_date=datetime.date(1981, 1, 1),
end_date=datetime.date(1981, 12, 31),
)
print(weather.fname)
print(weather.dates[0]) # np.datetime64('1981-01-01')
print(weather.latitude, weather.longitude)
Note
from_location caches downloaded data on disk and reuses it if a
cached file already covers the requested dates.
Running a simulation¶
An ApsimXEngine manages communication with an
ApsimX ZMQ server running in a separate process. A working ApsimX
installation is required (see the README for install instructions).
from simulatr.apsimx import ApsimXEngine
# Check the installation
print(ApsimXEngine.is_installed())
print(ApsimXEngine.model_dir())
# The engine can be configured from a crop name...
engine = ApsimXEngine(
crop_name="Wheat",
crop_variety="Hartog",
start_time="1981-06-01",
end_time="1981-11-30",
latitude=-27.58,
longitude=151.32,
)
# ...or from an existing model file
engine = ApsimXEngine(model_file="wheat.apsimx")
Starting and running¶
import datetime
engine.start()
# Run to a specific date
engine.fast_forward(datetime.datetime(1981, 6, 1))
# Sow the crop at the current date
engine.act("sow", crop_name="Wheat", crop_variety="Hartog")
# Run forward by a timedelta
engine.fast_forward(datetime.timedelta(days=30))
# Read state variables
vars = engine.getvars([
"[Clock].Today",
"[CROP].Phenology.CurrentStageName",
"[CROP].Total.Wt",
"[Weather].Rain",
"[Soil].Water.PAW",
])
print(vars)
# Apply an intervention
engine.act("nitrogen", amount=2.0) # kg/ha of N fertilizer
engine.act("irrigate", amount=5.0) # mm of water
# Run the rest of the season and harvest
engine.fast_forward()
engine.act("harvest", crop_name="Wheat")
# End the simulation and shut down the server
engine.act("terminate")
engine.stop(cleanup=True)
Note
Actions are applied at the current simulation date, so call
fast_forward() before acting to
schedule interventions. Available actions and their parameters are
listed in AVAILABLE_ACTION_MAP.
Other engine methods¶
engine.is_running # True while the simulation is running
engine.simulation_date # current simulation date/time
engine.resume(wait=True) # resume a paused simulation
engine.stop() # stop the server without cleaning up files
Reinforcement learning environment¶
ApsimXEnv wraps an
ApsimXEngine as a gymnasium
environment. The observation is the set of output_vars (default:
crop yield plus a selection of crop, soil, and weather state
variables), and the action space is built from the configured
actions (default: ["nitrogen", "irrigate"]).
Constructing an environment¶
from simulatr.apsimx import ApsimXEnv
env = ApsimXEnv(
crop_name="Wheat",
crop_variety="Hartog",
start_time="1981-06-01",
end_time="1981-11-30",
latitude=-27.58,
longitude=151.32,
)
print(env.observation_space) # gymnasium Box/Dict
print(env.action_space) # gymnasium Box/Dict
print(env.action_map) # mapping of action ids to actions
obs, info = env.reset(seed=42)
print(obs)
action = env.action_space.sample()
obs, reward, terminated, truncated, info = env.step(action)
env.close()
Interacting manually¶
For human-in-the-loop or agent-verification workflows, the environment can be made interactive and stepped from the terminal:
env = ApsimXEnv(
crop_name="Wheat",
interactive=True,
actions=["sow", "harvest", "nitrogen", "irrigate"],
)
env.create_interactive_for_human()
obs, info = env.reset()
obs, reward, terminated, truncated, info = env.step(action)
Options¶
# Larger intervention interval (days between automatic actions)
env = ApsimXEnv(crop_name="Wheat", intervention_interval=14)
# Custom reward variable
env = ApsimXEnv(
crop_name="Wheat",
revenue_var={"name": "[CROP].Grain.Total.Wt", "cost": 0.5},
)
# Control the discretization of continuous actions
env = ApsimXEnv(crop_name="Wheat", num_levels=0, exclusive=False)
LLM prompt generation¶
ApsimXLLMPromptGenerator turns an
environment’s observations and action space into text prompts that can
be used with a large language model, and parses the model’s responses
back into actions.
from simulatr.apsimx import ApsimXLLMPromptGenerator
generator = ApsimXLLMPromptGenerator.from_env(env)
# System prompt describing the environment and action space
system_prompt = generator.get_system_prompt()
print(system_prompt)
# Turn prompt built from a single observation
obs, info = env.reset(seed=42)
turn_prompt = generator.get_turn_prompt(obs)
print(turn_prompt)
# Have an LLM respond, then parse its response back into an action
llm_response = "Apply 2.0 kg/ha of nitrogen."
action = generator.parse_action_response(llm_response)
print(action)
# Describe an action id or parameter set in natural language
print(generator.describe_action(action))
print(generator.describe_action(env.action_map.id("nitrogen")))