Introduction

In Mediterranean mixed‐farm systems, land and labor are shared between arable crops and livestock enterprises. The objective of this analysis is to determine the profit‐maximizing allocation of 15 ha of land, 350 h of labor and 400 kg of available nitrogen among four winter crops and a temporary pasture for sheep. Each crop carries standard agronomic area bounds (e.g. oat between 2 ha and 8 ha), and pasture must lie between 2 ha and 4 ha. Margins (€/ha) and resource requirements (land, labor, nitrogen) are summarized in Table 1.

Table 1. Resource requirements, margins and area bounds for each activity.

Activity Land (ha) Labor (h) Nitrogen (kg) Margin (€/ha) Area bounds (ha)
Oat 1 20 80 400 2 – 8
Barley 1 25 100 450 1 – 6
Lupin 1 15 0 350 2 – 5
Fava 1 30 30 500 1 – 3
Pasture 1 10 0 360 2 – 4

We formulate a linear program to maximize total gross margin subject to resource and area‐bound constraints. The following sections describe the model construction, solution and sensitivity analysis using the ram package.

Methods

Define Resource Constraints

We first declare the available resources and the minimum/maximum area constraints for each crop and pasture. The function define_resources() accepts three parallel vectors: resource names, availabilities, and constraint directions ("≤" or "≥").

# Step 1: Define Resource Constraints (flexible min/max for each crop)
resources <- define_resources(
  resources = c(
    # total availables
    "land", "labor", "nitrogen",
    # per‐crop bounds
    "oat_min",   "oat_max",
    "barley_min","barley_max",
    "lupin_min", "lupin_max",
    "fava_min",  "fava_max",
    "pasture_min","pasture_max"
  ),
  availability = c(
    # totals
     15,    350,    400,
    # bounds
      2,      8,   # oat
      1,      6,   # barley
      2,      5,   # lupin
      1,      3,   # fava
      2,      4    # pasture
  ),
  direction = c(
    # totals all <=
    "<=",   "<=",   "<=",
    # minimums >=, maximums <=
    ">=", "<=",  # oat
    ">=", "<=",  # barley
    ">=", "<=",  # lupin
    ">=", "<=",  # fava
    ">=", "<="   # pasture
  )
)

print(resources)
#>       resource availability direction
#> 1         land           15        <=
#> 2        labor          350        <=
#> 3     nitrogen          400        <=
#> 4      oat_min            2        >=
#> 5      oat_max            8        <=
#> 6   barley_min            1        >=
#> 7   barley_max            6        <=
#> 8    lupin_min            2        >=
#> 9    lupin_max            5        <=
#> 10    fava_min            1        >=
#> 11    fava_max            3        <=
#> 12 pasture_min            2        >=
#> 13 pasture_max            4        <=

Specify Activities

Next, we assemble the technical matrix, whose rows correspond to the resources declared above, and whose columns correspond to the five activities. Each entry is the per‐unit requirement (or, for area‐bounds rows, an indicator). The vector objective lists the margin for each activity.

# Step 2: Define Activities
activity_requirements_matrix <- matrix(
  c(
    # land (ha)
     1,    1,    1,    1,     1, 
    # labor (h)
    20,   25,   15,   30,    10, 
    # nitrogen (kg)
    80,  100,    0,   30,     0, 
    # bounds indicator rows (1 for that crop, 0 else)
    # oat_min
     1,   0,    0,    0,     0,
    # oat_max
     1,   0,    0,    0,     0,
    # barley_min
     0,   1,    0,    0,     0,
    # barley_max
     0,   1,    0,    0,     0,
    # lupin_min
     0,   0,    1,    0,     0,
    # lupin_max
     0,   0,    1,    0,     0,
    # fava_min
     0,   0,    0,    1,     0,
    # fava_max
     0,   0,    0,    1,     0,
    # pasture_min
     0,   0,    0,    0,     1,
    # pasture_max
     0,   0,    0,    0,     1
  ),
  nrow = nrow(resources),
  byrow = TRUE,
  dimnames = list(
    resources$resource,
    c("oat","barley","lupin","fava","pasture")
  )
)

objective <- c(oat=400, barley=450, lupin=350, fava=500, pasture=360)

activities <- define_activities(
  activities = colnames(activity_requirements_matrix),
  activity_requirements_matrix = activity_requirements_matrix,
  objective = objective
)

print(activities)
#>         activity land labor nitrogen oat_min oat_max barley_min barley_max
#> oat          oat    1    20       80       1       1          0          0
#> barley    barley    1    25      100       0       0          1          1
#> lupin      lupin    1    15        0       0       0          0          0
#> fava        fava    1    30       30       0       0          0          0
#> pasture  pasture    1    10        0       0       0          0          0
#>         lupin_min lupin_max fava_min fava_max pasture_min pasture_max objective
#> oat             0         0        0        0           0           0       400
#> barley          0         0        0        0           0           0       450
#> lupin           1         1        0        0           0           0       350
#> fava            0         0        1        1           0           0       500
#> pasture         0         0        0        0           1           1       360

Build and Solve the Linear Program

The model is constructed via create_ram_model() and solved in the maximization sense with solve_ram().

# Step 3: Build and Solve the Model
model <- create_ram_model(resources, activities)
solution <- solve_ram(model, direction = "max")

Results

Optimal Allocation

The optimal area (ha) for each activity is extracted from solution$optimal_activities and presented in Table 2.

Table 2. Optimal area allocation (ha) under maximized gross margin.

dat <- data.frame(
  Activity = names(solution$optimal_activities),
  Area_ha  = round(as.numeric(solution$optimal_activities), 1)
)

DT::datatable(
  dat,
  caption = htmltools::tags$caption(
    style = "caption-side: top; text-align: left;",
    "Table 2. Optimal area allocation (ha) under maximized gross margin."
  ),
  rownames = FALSE,
  options = list(
    autoWidth = TRUE,
    columnDefs = list(
      list(width = '3cm', targets = 0),  # first column (activity)
      list(width = '2cm', targets = 1)   # second column (level)
    )
  )
) |>
  DT::formatRound("Area_ha", 2)

The maximum total gross margin achieved is:

cat("Maximum gross margin (EUR):", solution$objective_value, "\n")
#> Maximum gross margin (EUR): 5990
plot_ram(solution)

Shadow Prices and Sensitivity

To explore the economic value of relaxing each constraint, we compute the shadow prices via sensitivity_ram(). A non‐zero shadow price for a "≤" constraint indicates the marginal gain per extra unit of that resource.

Step 5: Sensitivity Analysis

sensitivity_ram(solution)
#>       resource direction availability shadow_price lower_bound upper_bound
#> 1         land        <=           15            0           0           0
#> 2        labor        <=          350           NA          NA          NA
#> 3     nitrogen        <=          400           NA          NA          NA
#> 4      oat_min        >=            2           NA          NA          NA
#> 5      oat_max        <=            8           NA          NA          NA
#> 6   barley_min        >=            1           NA          NA          NA
#> 7   barley_max        <=            6           NA          NA          NA
#> 8    lupin_min        >=            2           NA          NA          NA
#> 9    lupin_max        <=            5           NA          NA          NA
#> 10    fava_min        >=            1           NA          NA          NA
#> 11    fava_max        <=            3           NA          NA          NA
#> 12 pasture_min        >=            2           NA          NA          NA
#> 13 pasture_max        <=            4           NA          NA          NA
# Compute sensitivity
sens <- sensitivity_ram(solution)

# Render as a clean datatable
DT::datatable(
  sens,
  caption = htmltools::tags$caption(
    style = "caption-side: top; text-align: left;",
    "Table 3. Sensitivity analysis of constraints (shadow prices and allowable ranges)."
  ),
  rownames = FALSE,
  colnames = c(
    "Resource", "Direction", "Availability", 
    "Shadow Price", "Lower Bound", "Upper Bound"
  ),
  options = list(
    pageLength = nrow(sens),
    dom = 't',    # only the table, no extra controls
    columnDefs = list(
      list(className = 'dt-center', targets = 0:5)
    )
  )
) |>
  DT::formatRound(columns = c("availability", "shadow_price", "lower_bound", "upper_bound"), digits = 2)

Conclusion

The sensitivity analysis in Table 3 highlights which constraints are active at the optimum and how marginal changes in resource availability would affect the maximum gross margin. A shadow price of zero for land indicates that, under the current parameterisation, land is not the binding constraint—there is slack capacity. The absence of numeric shadow prices (NA) for labor, nitrogen, and the per‐crop bounds signifies that these constraints either are non‐binding or lie outside the solver’s range reporting.

Notably, all minimum area bounds for the four winter crops and the temporary pasture are respected exactly (the solution values coincide with their minimum or maximum limits), confirming that these structural requirements shape the optimal plan. Practitioners may use these insights to decide whether relaxing or tightening particular bounds (e.g., allowing more pasture area or increasing labor availability) could unlock additional profitability. Moreover, this framework can readily incorporate further enterprises (e.g., olive groves, poultry) or environmental targets (e.g., nitrate leaching limits) by augmenting the resources and activities matrices accordingly.