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")))