Complex Environmental Standards Formulas
Environmental Standards Formula Guidance
This document explains how TypeScript-based formulas can be used to calculate environmental standard action levels dynamically. It covers the purpose of formulas, the calculation workflow, available utility methods, reusable formula patterns, fallback strategies, validation checks, and common implementation pitfalls.
The Formula feature allows you to create custom TypeScript functions that dynamically calculate environmental standard action levels (threshold values). Instead of storing a fixed value like "5 mg/L", you can define logic that computes the appropriate limit based on site-specific conditions, water chemistry, or historical data.
Pre-Requisites
Formulas are written in TypeScript (a form of JavaScript). You should become familiar with TypeScript prior to attempting to create your own formulas. ESdat Support offers a fee-based service to create formulas for organizations that don't have this capability.
How It Works
- You write a calculate function that receives a CalculationContext containing sample details and the environmental standard being evaluated.
- Your function uses utility methods to pull data from the system: background concentrations, other sample results, historical records, pH, etc.
- Your function returns an ActionLevelResult with the computed value, units, and a description.
- The system calls your formula every time a sample is evaluated against that environmental standard.
Why Use Formulas?
| Scenario | Without Formula | With Formula |
|---|---|---|
| pH-dependent limits | Store many static values for each pH range | One formula that calculates the correct limit |
| Site-specific background | Manually update thresholds | Formula reads background automatically |
| Hardness-dependent metals | Hard-code lookup tables | Formula calculates from actual hardness |
| Consecutive exceedance rules | Manual review required | Formula detects exceedance patterns |
| Statistical thresholds | Static percentile values | Formula computes from historical data |
Key Concepts
Data Retrieval
Formulas can pull data from the system using these methods:
- getBackgroundConcentration() - site background values
- getSampleResult() - other analytes in the same sample
- getPH() - resolved pH from field, lab, or default
- getPercentileResultsForLocation() - historical statistics
- checkConsecutiveExceedances() - trend/pattern detection
- getPreviousResultsForLocation() - raw historical results
Result Structure
Each formula returns:
- ActionLevel - the calculated threshold (or null if incalculable)
- Units - the unit of measurement
- Description - human-readable explanation
- Options: ActionLevelMin, ActionLevelPrefix, ActionLevelSuffix
Best Practices
- Handle missing data gracefully and return null with a clear description.
- Use the most conservative available value when data is unavailable.
- Provide detailed descriptions so users understand how the value was derived.
- Clamp values to valid ranges when equations have applicability limits.
- Minimize unnecessary API calls and only fetch data when needed.
Utility Function Reference
The following section lists the available utility functions on the Utils class. Each entry includes a short description, expected parameters, and the return type.
Utils.getBackgroundConcentration(params)
Retrieves the site background concentration for a ChemCode.
Utils.getBackgroundConcentration({ ChemCode: ChemCode, Unit: string, TotalOrFiltered?: 'T' | 'F', MatrixType?: string, FallbackToSampleResult?: boolean }): number | null
Utils.getSampleResult(params)
Gets a result value from the same chemistry sample for a different ChemCode.
Utils.getSampleResult({ ChemCode: ChemCode, Unit: string, TotalOrFiltered?: 'T' | 'F', Options?: ChemistryResultLookupOptions }): number | null
Utils.getPH(params)
Resolves pH for the current sample using a priority order.
Utils.getPH(): { pH: number | null; pHSource: string } Utils.getPH({ FallbackValue: number }): { pH: number; pHSource: string }
Utils.getPercentileResultsForLocation(params)
Gets a percentile value from historical chemistry results for a location.
Utils.getPercentileResultsForLocation({ LocationCode: string, Well?: string | null, ChemCode: ChemCode, Unit: string, TotalOrFiltered?: 'T' | 'F', MatrixType?: string | null, Percentile: number, Method?: PercentileMethod | null, PriorSamplesOnly?: boolean, MaxResultCount?: number, ExcludeNonDetects?: boolean, ExcludeQualifiers?: string[], NonDetectMultiplier?: number }): number | null
Available percentile methods:
R1 | EmpiricalInvCDF | SAS3 | R2 | EmpiricalInvCDFAverage | SAS5 | R3 | Nearest | SAS2 | R4 | California | SAS1 | R5 | Hazen | Hydrology | R6 | Weibull | Nist | SPSS | SAS4 | R7 | Excel | Mode | S | R8 | Median | Default | R9 | Normal
Utils.getPreviousResultsForLocation(params)
Gets prior chemistry results at the current sample location, ordered newest-first.
Utils.getPreviousResultsForLocation({ ChemCode: ChemCode, Unit: string, MinDate?: Date | null, MaxDate?: Date | null, NumberOfResults?: number, TotalOrFiltered?: 'T' | 'F', MatrixType?: string | null, MinDepth?: number | null, MaxDepth?: number | null, Options?: HistoricalResultLookupOptions }): PriorLocationResult[]
Utils.checkConsecutiveExceedances(params)
Returns true when the current sample and prior results form a consecutive exceedance streak.
Utils.checkConsecutiveExceedances({ ChemCode: ChemCode, Unit: string, Threshold: number, ConsecutiveCount: number, GroupBy?: 'Sample' | 'Month' | 'Quarter', TotalOrFiltered?: 'T' | 'F', MatrixType?: string | null, Options?: HistoricalResultLookupOptions }): boolean
Formula Development Guidance
1. Common Patterns and Recipes
Missing Data Handling:
if (value == null) { return { ActionLevel: null, Units: 'mg/L', Description: 'Required data not available' }; } const doc = Utils.getSampleResult({ ChemCode: 'DOC', Unit: 'mg/L' }) ?? 0.5;
Hardness-Dependent Metal Equation (Copper example):
const actionLevel = Math.exp(0.8545 * Math.log(hardness) - 1.465);
pH-Dependent Ammonia Calculation:
const { pH } = Utils.getPH({ FallbackValue: 7.0 }); const fraction = 1 / (1 + 10^(pH - 9.25));
2. Data Priority and Fallback Strategies
| Data Source | Priority | When to Use |
|---|---|---|
| Site background | Highest | For naturally occurring substances |
| Current sample result | Medium | When background is unavailable |
| Historical percentile | Medium | For statistical thresholds |
| Hard-coded default | Lowest | Last resort fallback |
3. Unit Conversion Awareness
- All utility methods require explicit Unit parameters.
- Results may fail to convert if there is no conversion path between units.
- Always use the same unit expected by the environmental standard.
- Common units: mg/L, µg/L, mg/kg, and "-" for pH or conductivity.
4. Qualifier Handling Best Practices
const QUALIFIERS_TO_EXCLUDE = ['R', 'J', 'UJ', 'B']; Utils.getSampleResult({ ChemCode: 'SomeChemCode', Unit: 'mg/L', Options: { ExcludeQualifiers: ['R', 'J'] } });
5. Performance Considerations
- Minimize API calls.
- Use lazy evaluation.
- checkConsecutiveExceedances() is more efficient than manual iteration.
- Each getSampleResult() call is a separate query.
6. Debugging Tips
- Use the Description field to show intermediate values.
- The pHSource value explains where pH originated.
- When clamping, document original and adjusted values.
- Test missing data, extreme values, and edge cases.
7. Validation Checklist
- Handle all possible null returns from Utils methods.
- Verify values are within equation applicability ranges.
- Ensure units are consistent.
- Verify fallback logic produces reasonable results.
- Provide a clear description.
- Confirm equation coefficients match the published standard.
8. Common Pitfalls
| Pitfall | Solution |
|---|---|
| Using chemical name instead of ChemCode | Always use the exact ChemCode string. |
| Forgetting FallbackToSampleResult: true | Background values will not fall back automatically. |
| Not clamping values | Equations may produce invalid results outside calibrated ranges. |
| Hard-coding numeric constants without explanation | Add comments showing the source of constants. |
| Complex logic in a single function | Break logic into well-named helper variables. |