Foreground Modeling¶
The foreground database contains the candidate processes that optimex will optimize — these are the processes whose capacity installation and operational dispatch are the decision variables. This guide explains the optimex-specific attributes you need to add to standard Brightway processes. For the underlying concepts (temporal distribution, process time vs system time, convolution), see the Theory page.
Standard vs optimex Processes¶
A standard Brightway process becomes an optimex process by adding:
operation_time_limitson the processtemporal_distributionon exchangesoperationflag on operation-phase exchanges
# Standard Brightway process
{
("foreground", "my_process"): {
"name": "My Process",
"exchanges": [
{"amount": 1, "type": "production", "input": ("foreground", "my_process")},
{"amount": 10, "type": "technosphere", "input": ("db_2020", "electricity")},
],
}
}
# optimex process (with temporal information)
{
("foreground", "my_process"): {
"name": "My Process",
"operation_time_limits": (1, 2), # NEW: operation phase timing
"exchanges": [
{
"amount": 1,
"type": "production",
"input": ("foreground", "my_process"),
"temporal_distribution": TemporalDistribution(...), # NEW
"operation": True, # NEW
},
{
"amount": 10,
"type": "technosphere",
"input": ("db_2020", "electricity"),
"temporal_distribution": TemporalDistribution(...), # NEW
},
],
}
}
Process Lifecycle Phases¶
optimex models processes with distinct lifecycle phases using process time (\(\delta\), also referred to as τ in code):
- Construction (pre-operation): Initial investment, equipment manufacturing
- Operation: Active production phase where output scales with utilization
- Decommissioning (post-operation): End-of-life treatment
Operation Time Limits¶
The operation_time_limits attribute defines when the operation phase occurs:
| Example | Meaning |
|---|---|
(1, 2) |
Operation at τ=1 and τ=2 |
(0, 0) |
Immediate production (no construction delay) |
(2, 5) |
Operation from τ=2 through τ=5 |
{
("foreground", "solar_pv"): {
"name": "Solar PV Plant",
"operation_time_limits": (1, 25), # 1 year construction, 25 years operation
...
}
}
Temporal Distributions¶
Temporal distributions specify when an exchange occurs relative to process installation, using bw_temporalis.TemporalDistribution:
from bw_temporalis import TemporalDistribution
import numpy as np
# Exchange at construction (τ=0 only)
TemporalDistribution(
date=np.array([0], dtype="timedelta64[Y]"),
amount=np.array([1.0]), # 100% at τ=0
)
# Exchange spread over operation (τ=1 and τ=2)
TemporalDistribution(
date=np.array([1, 2], dtype="timedelta64[Y]"),
amount=np.array([0.5, 0.5]), # 50% each year
)
# Exchange at end-of-life (τ=3)
TemporalDistribution(
date=np.array([3], dtype="timedelta64[Y]"),
amount=np.array([1.0]),
)
Amounts must sum correctly
For production exchanges, the amounts in the temporal distribution should sum to the total production per unit. For a process producing 1 unit total over its lifetime with amount=1, use fractions that sum to 1.
What One Installed Unit Means¶
This convention decides how installation impacts are amortized, so it is worth stating explicitly:
One unit of a process delivers the production stated by its production exchange — over its entire lifetime, not per year.
For the example above (amount=1, spread as [0, 0.5, 0.5, 0] over a two-year operation window):
| Quantity | Value |
|---|---|
| Lifetime output of one unit | 1 kg (the sum of the temporal distribution) |
| Output of one running unit per year | 0.5 kg (the per-τ entry) |
| Units needed to deliver 10 kg in a single year | 20 |
| Units needed to deliver 10 kg in each of two consecutive years | 20 (one cohort, fully used) |
Consequences for the optimization:
var_installation[p, v]counts units, and installation-dependent exchanges (construction, end-of-life) are incurred once per unit.var_operation[p, v, t]counts how many of those units run in year t, bounded by the units installed:var_operation[p, v, t] ≤ var_installation[p, v].- Annual output is
production[τ = t - v] × var_operation[p, v, t].
This is what makes optimex agree with a standard LCA of the delivered amount whenever every unit is fully utilized. If the demand profile does not allow full utilization — for instance a single demand year for a process with a multi-year operation window — the model builds capacity that is partly idle, and the resulting impact is legitimately higher than the standard LCA score of the delivered amount.
Reporting an annual capacity
Because installed units cover a whole lifetime, PostProcessor.get_installation() is not comparable with per-year production. Use PostProcessor.get_production_capacity(), which multiplies unit counts by the output per unit and year.
The Operation Flag¶
Exchanges marked with "operation": True scale with the operational level \(\mathbf{o}_{t,v}\), the number of units running. This enables flexible operation where a process can run below its installed capacity \(\mathbf{s}_v\) (see Theory: Flexible Operation).
"exchanges": [
# Production scales with operation
{
"amount": 1,
"type": "production",
"input": ("foreground", "product"),
"temporal_distribution": ...,
"operation": True, # Output depends on how much we operate
},
# Construction inputs do NOT scale with operation
{
"amount": 100,
"type": "technosphere",
"input": ("db_2020", "steel"),
"temporal_distribution": TemporalDistribution(
date=np.array([0], dtype="timedelta64[Y]"),
amount=np.array([1.0]),
),
# No "operation" flag - this is a fixed construction cost
},
# Operational emissions DO scale with operation
{
"amount": 5,
"type": "biosphere",
"input": ("biosphere3", "CO2"),
"temporal_distribution": ...,
"operation": True, # Emissions depend on operation level
},
]
Rule of thumb:
- Construction/decommissioning exchanges: No operation flag
- Production outputs: "operation": True
- Operational inputs/emissions: "operation": True
Complete Process Example¶
A solar PV plant with 1-year construction and 2-year operation:
from bw_temporalis import TemporalDistribution
import numpy as np
solar_pv = {
("foreground", "solar_pv"): {
"name": "Solar PV Installation",
"location": "DE",
"operation_time_limits": (1, 2), # Operation at τ=1 and τ=2
"exchanges": [
# Production: 1 MWh total, split across operation years
{
"amount": 1,
"type": "production",
"input": ("foreground", "electricity_solar"),
"temporal_distribution": TemporalDistribution(
date=np.array([0, 1, 2, 3], dtype="timedelta64[Y]"),
amount=np.array([0, 0.5, 0.5, 0]),
),
"operation": True,
},
# Construction: PV panels (at τ=0)
{
"amount": 0.01,
"type": "technosphere",
"input": ("db_2020", "pv_panel"),
"temporal_distribution": TemporalDistribution(
date=np.array([0], dtype="timedelta64[Y]"),
amount=np.array([1.0]),
),
# No operation flag - construction is fixed
},
# Operational input: maintenance (during operation)
{
"amount": 0.001,
"type": "technosphere",
"input": ("db_2020", "maintenance"),
"temporal_distribution": TemporalDistribution(
date=np.array([1, 2], dtype="timedelta64[Y]"),
amount=np.array([0.5, 0.5]),
),
"operation": True,
},
# End-of-life: recycling (at τ=3)
{
"amount": 0.01,
"type": "technosphere",
"input": ("db_2020", "pv_recycling"),
"temporal_distribution": TemporalDistribution(
date=np.array([3], dtype="timedelta64[Y]"),
amount=np.array([1.0]),
),
# No operation flag - decommissioning is fixed
},
],
}
}
Multiple Processes Producing the Same Product¶
optimex optimizes which processes to use when multiple can produce the same product:
foreground_data = {
# Product node
("foreground", "hydrogen"): {
"name": "Hydrogen",
"type": "product",
},
# Process 1: Green hydrogen (electrolysis)
("foreground", "electrolysis"): {
"name": "PEM Electrolysis",
"operation_time_limits": (1, 10),
"exchanges": [
{"type": "production", "input": ("foreground", "hydrogen"), ...},
# Low emissions, high cost
],
},
# Process 2: Grey hydrogen (SMR)
("foreground", "smr"): {
"name": "Steam Methane Reforming",
"operation_time_limits": (2, 15),
"exchanges": [
{"type": "production", "input": ("foreground", "hydrogen"), ...},
# High emissions, low cost
],
},
}
The optimizer will choose the mix of processes that minimizes environmental impact while meeting demand.
Vintage-Dependent Parameters¶
Real-world technologies improve over time. An EV manufactured in 2025 will have different characteristics than one manufactured in 2040. optimex supports vintage-dependent parameters to model how foreground exchanges change based on when a process is installed (see Theory: Foreground Evolution).
The Concept¶
- Vintage (or installation year): The system time when a process unit is built
- Process time (τ): The lifecycle stage of that unit (construction, operation, end-of-life)
These are independent dimensions:
| Unit | Installed (Vintage) | Operating in 2035 (τ=10) | Electricity Consumption |
|---|---|---|---|
| EV A | 2025 | τ=10 | 2.0 kWh/km (2025 technology) |
| EV B | 2030 | τ=5 | 1.5 kWh/km (2030 technology) |
Both are operating in 2035, but have different efficiency based on when they were built.
Defining Vintage Parameters in the Database¶
Define vintage parameters directly in the Brightway database as exchange attributes. This approach keeps all process-specific data in one place.
Using vintage_improvements¶
For uniform scaling across all process times:
from bw_temporalis import TemporalDistribution
import numpy as np
foreground_data = {
("foreground", "EV"): {
"name": "Electric Vehicle",
"operation_time_limits": (1, 2),
"exchanges": [
# Production exchange (no vintage variation)
{
"amount": 1,
"type": "production",
"input": ("foreground", "vkm"),
"temporal_distribution": TemporalDistribution(...),
"operation": True,
},
# Electricity consumption with vintage-dependent efficiency
{
"amount": 60, # Base amount (2020 technology)
"type": "technosphere",
"input": ("db_2020", "electricity"),
"temporal_distribution": TemporalDistribution(
date=np.array([1, 2], dtype="timedelta64[Y]"),
amount=np.array([0.5, 0.5]),
),
"operation": True,
# Vintage improvement scaling factors
"vintage_improvements": {
2020: 1.0, # 100% of base (60 MJ)
2030: 0.75, # 75% of base (45 MJ) - 25% improvement
2040: 0.6, # 60% of base (36 MJ) - 40% improvement
},
},
],
}
}
Using vintage_amounts¶
For explicit values at specific process times and vintages:
{
"amount": 60, # Base amount (optional when using vintage_amounts)
"type": "technosphere",
"input": ("db_2020", "electricity"),
"temporal_distribution": TemporalDistribution(
date=np.array([1, 2], dtype="timedelta64[Y]"),
amount=np.array([0.5, 0.5]),
),
"operation": True,
# Explicit vintage-specific values
"vintage_amounts": {
# Format: {vintage_year: amount} OR {(process_time, vintage_year): amount}
(1, 2020): 30, # τ=1, 2020 vintage: 30 MJ/vkm
(2, 2020): 30, # τ=2, 2020 vintage: 30 MJ/vkm
(1, 2030): 22.5, # τ=1, 2030 vintage: 22.5 MJ/vkm (25% improvement)
(2, 2030): 22.5, # τ=2, 2030 vintage: 22.5 MJ/vkm
(1, 2040): 18, # τ=1, 2040 vintage: 18 MJ/vkm (40% improvement)
(2, 2040): 18, # τ=2, 2040 vintage: 18 MJ/vkm
},
}
Key points:
- Values are linearly interpolated for years between specified vintages
- Works for production, technosphere, and biosphere exchanges
- Extracted automatically by LCADataProcessor
- vintage_improvements: More compact when all exchanges scale uniformly
- vintage_amounts: More flexible when different process times need different values
- If both are specified, vintage_amounts takes precedence
Optimizer Behavior¶
With vintage-dependent parameters, the optimizer considers:
- Installation timing trade-offs: Later installations are more efficient, but may have capacity constraints
- Mixed vintages: Different installation cohorts operating simultaneously with different efficiencies
- Background evolution: Combined with time-varying background databases for full temporal LCA
Sparse Implementation
Vintage parameters only affect processes/flows where they're specified. Processes without vintage overrides use the standard base tensors efficiently.
Common Patterns¶
Immediate Production (No Construction Delay)¶
"operation_time_limits": (0, 0),
"exchanges": [
{
"amount": 1,
"type": "production",
"temporal_distribution": TemporalDistribution(
date=np.array([0], dtype="timedelta64[Y]"),
amount=np.array([1.0]),
),
"operation": True,
},
]
Long-Lived Infrastructure¶
Seasonal/Variable Production¶
# Production varies by year (e.g., degradation)
"temporal_distribution": TemporalDistribution(
date=np.array([1, 2, 3, 4, 5], dtype="timedelta64[Y]"),
amount=np.array([0.22, 0.21, 0.20, 0.19, 0.18]), # Declining output
),
Writing the Foreground Database¶
Next Steps¶
- Optimization Setup: Configure demand and run the optimization
- Constraints: Add deployment limits and other constraints