diff --git a/src/Informedica.FHIR.Lib/Informedica.FHIR.Lib.fsproj b/src/Informedica.FHIR.Lib/Informedica.FHIR.Lib.fsproj index ca01233b..aa25026c 100644 --- a/src/Informedica.FHIR.Lib/Informedica.FHIR.Lib.fsproj +++ b/src/Informedica.FHIR.Lib/Informedica.FHIR.Lib.fsproj @@ -8,6 +8,10 @@ + + + + diff --git a/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx b/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx new file mode 100644 index 00000000..8c7eb8af --- /dev/null +++ b/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx @@ -0,0 +1,1794 @@ +/// # FHIR Medication Interface Implementation Plan +/// +/// This script implements the plan described in docs/mdr/interface/genpres_interface_specification.md +/// and follows the steps outlined in the GenPRES FHIR integration issue. +/// +/// ## Key principle +/// +/// A FHIR scenario provides only: +/// 1. Patient data (weight, height, age, gender) +/// 2. Indication +/// 3. Route +/// 4. Shape (pharmaceutical form) +/// 5. Dose type +/// 6. Component Orderable Quantities (from the products block) +/// 7. Orderable Dose Quantity and/or Rate (from administration + schema) +/// 8. Schedule Frequency (from schema) +/// +/// Everything else (concentrations, dose limits) is derived by lookup +/// via ZIndex/GenFORM or by calculation. +/// +/// ## Steps +/// +/// 1. Define the FHIR scenarios from the specification +/// 2. Look up product information from GPK product codes via ZIndex +/// 3. Parse quantitative variables to ValueUnits +/// 4. Reconstruct an OrderScenario from the scenario context via OrderContext lookup +/// 5. Apply orderable quantities and schedule from the FHIR scenario +/// 6. Run the scenarios through the order pipeline +/// 7. Print the results +/// 8. Sketch a FHIR-based solution + +#load "load.fsx" + +open System +open MathNet.Numerics +open Informedica.Utils.Lib.BCL +open Informedica.GenCore.Lib.Ranges +open Informedica.GenUnits.Lib +open Informedica.GenForm.Lib +open Informedica.GenOrder.Lib + + +// ============================================================================= +// STEP 1: Define the FHIR scenarios from the specification +// ============================================================================= +// +// Source: docs/mdr/interface/genpres_interface_specification.md, sections 6.1–6.11 +// +// NOTE: The GPK codes in the specification are PLACEHOLDERS (e.g. "2345678"). +// Real codes must come from the G-Standard database. See Step 2 for product lookup. + +/// Represents a single product component as described in the FHIR scenario YAML products block +type ScenarioProduct = + { + // Placeholder GPK code from the spec (not a real G-Standard code) + GpkPlaceholder: string + // Orderable quantity of this component + Quantity: decimal + // Unit for the orderable quantity (e.g. "mL", "stuk") + Unit: string + // Human-readable description from the spec comment + Description: string + } + + +/// Represents the administration schedule from the FHIR YAML schema block. +/// Only contains what is directly present in the FHIR scenario — +/// concentrations and dose limits are NOT included here. +type AdministrationSchema = + { + // Number of administrations per period (None for continuous orders) + Frequency: int option + // Duration of the frequency period (e.g. 1 for "per day", 36 for "per 36 hours") + TimePeriod: decimal option + // Unit of the frequency period (e.g. "dag", "uur") + TimeUnit: string option + // Infusion rate quantity (e.g. 85 for "85 mL/uur") + RateQuantity: decimal option + // Rate form unit — the numerator unit (e.g. "mL" for "mL/uur") + RateFormUnit: string option + // Rate time unit — the denominator unit (e.g. "uur" for "mL/uur") + RateTimeUnit: string option + // Exact administration times (for Timed orders) + ExactTimes: string list + } + + +/// Represents a FHIR treatment scenario as described in section 6 of the specification. +/// Contains only the data that can be directly derived from the FHIR resource — +/// specifically: patient context, indication, route, shape, dose type, +/// orderable quantities, and schedule. +type FhirScenario = + { + // Scenario identifier (matches the spec section number) + ScenarioId: string + // Short descriptive name + Description: string + // Patient weight in kg + WeightKg: decimal + // Patient height in cm + HeightCm: decimal + // Patient gender: "male" or "female" + Gender: string + // Clinical indication (maps to Filter.Indication) + Indication: string + // Generic medication name (maps to Filter.Generic) + MedicationName: string + // Administration route, Dutch G-Standard term (maps to Filter.Route) + Route: string + // Pharmaceutical form, Dutch (maps to Filter.Form) + Shape: string + // Dose type: "Once", "OnceTimed", "Discontinuous", "Timed", "Continuous" + // (maps to Filter.DoseType) + DoseType: string + // Component orderable quantities from the YAML products block + Products: ScenarioProduct list + // Total administration quantity and unit + AdminQuantity: decimal + AdminUnit: string + // Schedule from the YAML schema block + Schema: AdministrationSchema + } + + +// --- Scenario 6.1: Single-Product Once (Paracetamol rectal suppository) --- +let scenario61 = + { + ScenarioId = "6.1" + Description = "Single-Product Once – paracetamol suppository" + WeightKg = 19.5m + HeightCm = 109m + Gender = "male" + Indication = "Pijn, acuut/post-operatief" + MedicationName = "paracetamol" + Route = "RECTAAL" + Shape = "zetpil" + DoseType = "Once" + Products = + [ + { + GpkPlaceholder = "2345678" + Quantity = 1m + Unit = "stuk" + Description = "paracetamol 750 mg/stuk zetpil (placeholder GPK)" + } + ] + AdminQuantity = 1m + AdminUnit = "stuk" + Schema = + { + Frequency = Some 1 + TimePeriod = None + TimeUnit = None + RateQuantity = None + RateFormUnit = None + RateTimeUnit = None + ExactTimes = [] + } + } + + +// --- Scenario 6.2: Single-Product Once-Timed (Paracetamol IV) --- +let scenario62 = + { + ScenarioId = "6.2" + Description = "Single-Product OnceTimed – paracetamol IV infusion" + WeightKg = 11m + HeightCm = 79m + Gender = "male" + Indication = "Pijn, acuut/post-operatief" + MedicationName = "paracetamol" + Route = "INTRAVENEUS" + Shape = "infusievloeistof" + DoseType = "OnceTimed" + Products = + [ + { + GpkPlaceholder = "3456789" + Quantity = 22m + Unit = "mL" + Description = "paracetamol 10 mg/mL infusievloeistof (placeholder GPK)" + } + ] + AdminQuantity = 22m + AdminUnit = "mL" + Schema = + { + Frequency = Some 1 + TimePeriod = None + TimeUnit = None + RateQuantity = Some 85m + RateFormUnit = Some "mL" + RateTimeUnit = Some "uur" + ExactTimes = [] + } + } + + +// --- Scenario 6.3: Single-Product Discontinuous (Paracetamol rectal, 4x/day) --- +let scenario63 = + { + ScenarioId = "6.3" + Description = "Single-Product Discontinuous – paracetamol rectal 4x/dag" + WeightKg = 11m + HeightCm = 79m + Gender = "male" + Indication = "Milde tot matige pijn; koorts" + MedicationName = "paracetamol" + Route = "RECTAAL" + Shape = "zetpil" + DoseType = "Discontinuous" + Products = + [ + { + GpkPlaceholder = "2345678" + Quantity = 1m + Unit = "stuk" + Description = "paracetamol 180 mg/stuk zetpil (placeholder GPK)" + } + ] + AdminQuantity = 1m + AdminUnit = "stuk" + Schema = + { + Frequency = Some 4 + TimePeriod = Some 1m + TimeUnit = Some "dag" + RateQuantity = None + RateFormUnit = None + RateTimeUnit = None + ExactTimes = [] + } + } + + +// --- Scenario 6.3.1: Single-Product Discontinuous Specific Time (Gentamicine neonatal) --- +let scenario631 = + { + ScenarioId = "6.3.1" + Description = "Single-Product Discontinuous – gentamicine 1x/36h neonatal" + WeightKg = 1.8m + HeightCm = 42m + Gender = "female" + Indication = "Ernstige infectie, gram negatieve microorganismen" + MedicationName = "gentamicine" + Route = "INTRAVENEUS" + Shape = "infusievloeistof" + DoseType = "Discontinuous" + Products = + [ + { + GpkPlaceholder = "2345678" + Quantity = 9m + Unit = "mL" + Description = "gentamicine 1 mg/mL infusievloeistof (placeholder GPK)" + } + ] + AdminQuantity = 9m + AdminUnit = "mL" + Schema = + { + Frequency = Some 1 + TimePeriod = Some 36m + TimeUnit = Some "uur" + RateQuantity = None + RateFormUnit = None + RateTimeUnit = None + ExactTimes = [] + } + } + + +// --- Scenario 6.4: Single-Product Timed (Paracetamol IV with exact times) --- +let scenario64 = + { + ScenarioId = "6.4" + Description = "Single-Product Timed – paracetamol IV 4x/dag with exact times" + WeightKg = 11m + HeightCm = 79m + Gender = "male" + Indication = "Pijn, acuut/post-operatief" + MedicationName = "paracetamol" + Route = "INTRAVENEUS" + Shape = "infusievloeistof" + DoseType = "Timed" + Products = + [ + { + GpkPlaceholder = "2345678" + Quantity = 17.5m + Unit = "mL" + Description = "paracetamol 10 mg/mL infusievloeistof (placeholder GPK)" + } + ] + AdminQuantity = 17.5m + AdminUnit = "mL" + Schema = + { + Frequency = Some 4 + TimePeriod = Some 1m + TimeUnit = Some "dag" + RateQuantity = Some 70m + RateFormUnit = Some "mL" + RateTimeUnit = Some "uur" + ExactTimes = [ "10:00"; "16:00"; "22:00"; "04:00" ] + } + } + + +// --- Scenario 6.5: Single-Product Continuous (Propofol sedation) --- +let scenario65 = + { + ScenarioId = "6.5" + Description = "Single-Product Continuous – propofol sedation" + WeightKg = 11m + HeightCm = 79m + Gender = "male" + Indication = "Sedatie" + MedicationName = "propofol" + Route = "INTRAVENEUS" + Shape = "emulsie voor injectie" + DoseType = "Continuous" + Products = + [ + { + GpkPlaceholder = "2345678" + Quantity = 50m + Unit = "mL" + Description = "propofol 10 mg/mL emulsie voor injectie (placeholder GPK)" + } + ] + AdminQuantity = 50m + AdminUnit = "mL" + Schema = + { + Frequency = None + TimePeriod = None + TimeUnit = None + RateQuantity = Some 3.3m + RateFormUnit = Some "mL" + RateTimeUnit = Some "uur" + ExactTimes = [] + } + } + + +// --- Scenario 6.6: Multi-Product Continuous (Noradrenaline + glucose 10%) --- +let scenario66 = + { + ScenarioId = "6.6" + Description = "Multi-Product Continuous – noradrenaline with glucose 10% diluent" + WeightKg = 11m + HeightCm = 79m + Gender = "male" + Indication = "Circulatoire insufficiëntie" + MedicationName = "noradrenaline" + Route = "INTRAVENEUS" + Shape = "concentraat voor oplossing voor infusie" + DoseType = "Continuous" + Products = + [ + { + GpkPlaceholder = "2345678" + Quantity = 5m + Unit = "mL" + Description = "noradrenaline 1 mg/mL concentraat voor oplossing voor infusie (placeholder GPK)" + } + { + GpkPlaceholder = "3456789" + Quantity = 45m + Unit = "mL" + Description = "glucose 10% vloeistof/diluent (placeholder GPK)" + } + ] + AdminQuantity = 50m + AdminUnit = "mL" + Schema = + { + Frequency = None + TimePeriod = None + TimeUnit = None + RateQuantity = Some 1m + RateFormUnit = Some "mL" + RateTimeUnit = Some "uur" + ExactTimes = [] + } + } + + +// --- Scenario 6.7: Multi-Product Once-Timed (Amiodaron + glucose 10% diluent) --- +let scenario67 = + { + ScenarioId = "6.7" + Description = "Multi-Product OnceTimed – amiodaron with glucose 10% diluent" + WeightKg = 11m + HeightCm = 79m + Gender = "male" + Indication = "Ernstige therapieresistente hartritmestoornissen" + MedicationName = "amiodaron" + Route = "INTRAVENEUS" + Shape = "injectievloeistof" + DoseType = "OnceTimed" + Products = + [ + { + GpkPlaceholder = "2345678" + Quantity = 6m + Unit = "mL" + Description = "amiodaron 50 mg/mL injectievloeistof (placeholder GPK)" + } + { + GpkPlaceholder = "3456789" + Quantity = 44m + Unit = "mL" + Description = "glucose 10% vloeistof/diluent (placeholder GPK)" + } + ] + AdminQuantity = 9.1m + AdminUnit = "mL" + Schema = + { + Frequency = Some 1 + TimePeriod = None + TimeUnit = None + RateQuantity = Some 18m + RateFormUnit = Some "mL" + RateTimeUnit = Some "uur" + ExactTimes = [] + } + } + + +// --- Scenario 6.8: Multi-Product with Reconstitution Once (Adrenaline) --- +// Products listed are AFTER reconstitution; the reconstitution block is in the YAML. +let scenario68 = + { + ScenarioId = "6.8" + Description = "Multi-Product Reconstitution Once – adrenaline (reconstituted)" + WeightKg = 3.9m + HeightCm = 54m + Gender = "male" + Indication = "Reanimatie" + MedicationName = "adrenaline" + Route = "INTRAVENEUS" + Shape = "injectievloeistof" + DoseType = "Once" + Products = + [ + { + GpkPlaceholder = "2345678" + Quantity = 10m + Unit = "mL" + Description = "adrenaline 0,1 mg/mL oplossing voor infusie (na reconstitutie; placeholder GPK)" + } + ] + AdminQuantity = 0.4m + AdminUnit = "mL" + Schema = + { + Frequency = Some 1 + TimePeriod = None + TimeUnit = None + RateQuantity = None + RateFormUnit = None + RateTimeUnit = None + ExactTimes = [] + } + } + + +// --- Scenario 6.9: Multi-Product with Reconstitution Timed (Vancomycine + glucose 5%) --- +let scenario69 = + { + ScenarioId = "6.9" + Description = "Multi-Product Reconstitution Timed – vancomycine 4x/dag" + WeightKg = 11m + HeightCm = 79m + Gender = "male" + Indication = "Bacteriële infecties" + MedicationName = "vancomycine" + Route = "INTRAVENEUS" + Shape = "poeder voor oplossing voor infusie" + DoseType = "Timed" + Products = + [ + { + GpkPlaceholder = "2345678" + Quantity = 10m + Unit = "mL" + Description = "vancomycine 50 mg/mL oplossing voor infusie (na reconstitutie; placeholder GPK)" + } + { + GpkPlaceholder = "3456789" + Quantity = 40m + Unit = "mL" + Description = "glucose 5% vloeistof/diluent (placeholder GPK)" + } + ] + AdminQuantity = 14.9m + AdminUnit = "mL" + Schema = + { + Frequency = Some 4 + TimePeriod = Some 1m + TimeUnit = Some "dag" + RateQuantity = Some 59m + RateFormUnit = Some "mL" + RateTimeUnit = Some "uur" + ExactTimes = [] + } + } + + +// --- Scenario 6.10: Multi-Product TPN (Totale Parenterale Voeding) --- +let scenario610 = + { + ScenarioId = "6.10" + Description = "Multi-Product TPN – Totale Parenterale Voeding 1x/dag" + WeightKg = 11m + HeightCm = 79m + Gender = "male" + Indication = "Standaard Totale Parenterale Voeding" + MedicationName = "TPV" + Route = "INTRAVENEUS" + Shape = "vloeistof voor infusie" + DoseType = "Timed" + Products = + [ + { + GpkPlaceholder = "2345678" + Quantity = 105m + Unit = "mL" + Description = "Samenstelling C vloeistof voor infusie (placeholder GPK)" + } + { + GpkPlaceholder = "3456789" + Quantity = 59.5m + Unit = "mL" + Description = "NaCl 3% vloeistof voor infusie (placeholder GPK)" + } + { + GpkPlaceholder = "4567890" + Quantity = 20m + Unit = "mL" + Description = "KCl 7,4% vloeistof voor infusie (placeholder GPK)" + } + { + GpkPlaceholder = "5678901" + Quantity = 715.5m + Unit = "mL" + Description = "glucose 10% vloeistof voor infusie (placeholder GPK)" + } + ] + AdminQuantity = 900m + AdminUnit = "mL" + Schema = + { + Frequency = Some 1 + TimePeriod = Some 1m + TimeUnit = Some "dag" + RateQuantity = Some 45m + RateFormUnit = Some "mL" + RateTimeUnit = Some "uur" + ExactTimes = [ "17:00" ] + } + } + + +// --- Scenario 6.11: Multi-Product Enteral Feeding (MM met BMF) --- +let scenario611 = + { + ScenarioId = "6.11" + Description = "Multi-Product Enteral – MM met BMF 8x/dag" + WeightKg = 3.8m + HeightCm = 53m + Gender = "female" + Indication = "Enterale voeding" + MedicationName = "MM met BMF" + Route = "ORAAL" + Shape = "voeding" + DoseType = "Timed" + Products = + [ + { + GpkPlaceholder = "9999978" + Quantity = 20m + Unit = "mL" + Description = "MM vloeistof voor voeding (placeholder GPK)" + } + { + GpkPlaceholder = "99999789" + Quantity = 0.1m + Unit = "g" + Description = "Nutrilon Nenatal BMF poeder voor voeding (placeholder GPK)" + } + ] + AdminQuantity = 20m + AdminUnit = "mL" + Schema = + { + Frequency = Some 8 + TimePeriod = Some 1m + TimeUnit = Some "dag" + RateQuantity = None + RateFormUnit = None + RateTimeUnit = None + ExactTimes = [ "07:00"; "10:00"; "13:00"; "16:00"; "19:00"; "22:00"; "01:00"; "04:00" ] + } + } + + +let allScenarios = + [ + scenario61 + scenario62 + scenario63 + scenario631 + scenario64 + scenario65 + scenario66 + scenario67 + scenario68 + scenario69 + scenario610 + scenario611 + ] + + +// ============================================================================= +// STEP 2: Look up product information from GPK codes via ZIndex +// ============================================================================= +// +// NOTE: The GPK codes in the spec are PLACEHOLDERS. A real implementation would +// use ZIndex.GenericProduct.get [gpkCode] to resolve a product by its actual +// G-Standard GPK code. Here we demonstrate the lookup API by searching for +// products by their generic name. +// +// From these results the concentrations and dose limits are derived, NOT +// hardcoded. See Step 4 for how this feeds into OrderScenario reconstruction. + +open Informedica.ZIndex.Lib + + +/// Print a summary of a GenericProduct record +let printGenericProduct (gp: Types.GenericProduct) = + printfn " GPK %i | %-30s | Form: %-30s | Routes: %s" + gp.Id + gp.Name + gp.Form + (gp.Route |> String.concat ", ") + + for sub in gp.Substances do + printfn " Substance: %-25s | Qty: %g %s | GenericQty: %g %s" + sub.SubstanceName + sub.SubstanceQuantity + sub.SubstanceUnit + sub.GenericQuantity + sub.GenericUnit + + +/// Lookup all GenericProducts for a given medication name using ZIndex +let lookupByGenericName (name: string) = + GenericProduct.get [] + |> Array.filter (fun gp -> gp.Name |> String.equalsCapInsens name) + + +/// Demonstrate GPK lookup for the medications in our scenarios +let demonstrateProductLookup () = + printfn "\n=== STEP 2: Product Lookup via ZIndex ===" + printfn "NOTE: Spec uses placeholder GPK codes (e.g. '2345678')." + printfn "Real lookup uses ZIndex.GenericProduct.get [gpkCode]." + printfn "Concentrations and dose limits are derived from these results, not hardcoded.\n" + + let medicationNames = + [ + "paracetamol" + "gentamicine" + "propofol" + "noradrenaline" + ] + + for name in medicationNames do + printfn "--- %s ---" name + let products = lookupByGenericName name + + if products |> Array.isEmpty then + printfn " (no products found – run after loading G-Standard data)" + else + products |> Array.truncate 3 |> Array.iter printGenericProduct + + if products.Length > 3 then + printfn " ... and %i more products" (products.Length - 3) + + printfn "" + + +demonstrateProductLookup () + + +// ============================================================================= +// STEP 3: Parse quantitative variables to ValueUnits +// ============================================================================= +// +// Demonstrate how the FHIR scenario quantities (the only values present in the +// FHIR resources) translate to GenUnits ValueUnit values. +// These correspond to the four directly available data categories: +// 1. Component Orderable Quantities +// 2. Orderable Dose Quantity +// 3. Orderable Dose Rate +// 4. Schedule Frequency + +let demonstrateValueUnitParsing () = + printfn "\n=== STEP 3: Parsing Quantitative Variables to ValueUnits ===" + + // --- Patient context (for adjust calculations) --- + + let weight19_5kg = 19.5m |> BigRational.fromDecimal |> ValueUnit.singleWithUnit Units.Weight.kiloGram + printfn "Patient weight: %s" (weight19_5kg |> ValueUnit.toStringDecimalDutchShort) + + // --- 1. Component Orderable Quantities (from FHIR products block) --- + + let stuk = Units.General.general "stuk" + let qty1stuk = 1N |> ValueUnit.singleWithUnit stuk + printfn "Orderable qty (stuk): %s" (qty1stuk |> ValueUnit.toStringDecimalDutchShort) + + let qty22mL = 22N |> ValueUnit.singleWithUnit Units.Volume.milliLiter + printfn "Orderable qty (mL): %s" (qty22mL |> ValueUnit.toStringDecimalDutchShort) + + // --- 2. Orderable Dose Quantity (from FHIR administration block) --- + + // Dose quantity (absolute) — from admin quantity + let adminQty1stuk = 1N |> ValueUnit.singleWithUnit stuk + printfn "Admin qty: %s" (adminQty1stuk |> ValueUnit.toStringDecimalDutchShort) + + // --- 3. Orderable Dose Rate (from FHIR schema rate) --- + + // Rate: 85 mL/uur + let rate85mlHr = + 85N + |> ValueUnit.singleWithUnit (Units.Volume.milliLiter |> Units.per Units.Time.hour) + + printfn "Rate (85 mL/uur): %s" (rate85mlHr |> ValueUnit.toStringDecimalDutchShort) + + // Rate: 3.3 mL/uur (propofol, stored as BigRational fraction) + let rate33mlHr = + BigRational.fromDecimal 3.3m + |> ValueUnit.singleWithUnit (Units.Volume.milliLiter |> Units.per Units.Time.hour) + + printfn "Rate (3.3 mL/uur): %s" (rate33mlHr |> ValueUnit.toStringDecimalDutchShort) + + // --- 4. Schedule Frequency (from FHIR schema pattern) --- + + // Frequency: 4 x/dag + let freq4xDay = + [| 4N |] |> ValueUnit.withUnit (Units.Count.times |> Units.per Units.Time.day) + + printfn "Frequency (4 x/dag): %s" (freq4xDay |> ValueUnit.toStringDecimalDutchShort) + + // Frequency: 1 x/36 uur (gentamicine neonatal) + let freq1x36h = + [| 1N |] |> ValueUnit.withUnit (Units.Count.times |> Units.per Units.Time.hour) + + printfn "Frequency (1x/36 uur): %s" (freq1x36h |> ValueUnit.toStringDecimalDutchShort) + + printfn "" + + +demonstrateValueUnitParsing () + + +// ============================================================================= +// STEP 4: Reconstruct an OrderScenario from FHIR context via OrderContext lookup +// ============================================================================= +// +// A FHIR scenario provides patient data, indication, route, shape, and dose type. +// These are exactly the inputs needed to reconstruct an OrderScenario via the +// OrderContext lookup mechanism. +// +// The concentrations, dose limits, and product structure are NOT in the FHIR +// scenario — they come from the ZIndex/GenFORM database via OrderContext.getScenarios. +// +// Workflow: +// 1. Build a Patient from the FHIR patient context +// 2. Call OrderContext.create to get the initial context with available filters +// 3. Set the filter fields from the FHIR scenario (Indication, Generic, Route, Form, DoseType) +// 4. Call OrderContext.getScenarios (via UpdateOrderContext + evaluate) to look up matching scenarios +// 5. Apply the orderable quantities and schedule from the FHIR products + schema blocks + +open Patient.Optics + + +/// Convert a FhirScenario's patient data into a Patient record +let buildPatient (scenario: FhirScenario) : Patient = + let gender = + match scenario.Gender with + | "male" -> Male + | "female" -> Female + | _ -> AnyGender + + patient + |> setGender gender + |> setWeight (scenario.WeightKg |> Kilogram |> Some) + |> setHeight (scenario.HeightCm |> decimal |> int |> Centimeter |> Some) + + +/// Convert the scenario DoseType string to the GenFORM DoseType discriminated union +let parseDoseType (doseTypeStr: string) = + // doseText is left empty here; it is filled in by the dose rule + DoseType.fromString doseTypeStr "" + + +/// Print a summary of the OrderScenario context after lookup +let printOrderScenario (scenario: FhirScenario) (ctx: OrderContext) = + printfn "\n--- Scenario %s: %s ---" scenario.ScenarioId scenario.Description + printfn " Patient: %g kg, %g cm, %s" scenario.WeightKg scenario.HeightCm scenario.Gender + printfn " Indication: %s Route: %s Shape: %s DoseType: %s" + scenario.Indication scenario.Route scenario.Shape scenario.DoseType + + printfn " Scenarios found: %i" ctx.Scenarios.Length + + if ctx.Scenarios.Length > 0 then + let sc = ctx.Scenarios[0] + printfn " First scenario: %s | %s | %s | %A" + sc.Name sc.Route sc.Form sc.DoseType + + printfn "" + printfn " >>> Prescription (from ZIndex/GenFORM — NOT from FHIR scenario):" + + for line in sc.Prescription |> Array.collect id do + let text = + match line with + | Valid s + | Caution s + | Warning s + | Alert s -> s + + if text |> String.isNullOrWhiteSpace |> not then + printfn " %s" text + + printfn "" + + printfn " >>> Available orderable quantities from the FHIR scenario:" + printfn " Admin quantity: %g %s" scenario.AdminQuantity scenario.AdminUnit + + for p in scenario.Products do + printfn " Component qty: %g %s (%s)" p.Quantity p.Unit p.Description + + match scenario.Schema.RateQuantity, scenario.Schema.RateFormUnit, scenario.Schema.RateTimeUnit with + | Some rate, Some fu, Some tu -> + printfn " Rate: %g %s/%s" rate fu tu + | _ -> () + + match scenario.Schema.Frequency, scenario.Schema.TimeUnit with + | Some freq, Some unit -> + let period = scenario.Schema.TimePeriod |> Option.defaultValue 1m + let freqStr = if period = 1m then $"{freq} x/{unit}" else $"{freq} x/{period} {unit}" + printfn " Frequency: %s" freqStr + | _ -> () + + if scenario.Schema.ExactTimes |> List.isEmpty |> not then + printfn " Exact times: %s" (scenario.Schema.ExactTimes |> String.concat ", ") + + +printfn "\n=== STEP 4: Reconstructing OrderScenarios via OrderContext Lookup ===" +printfn """ +The FHIR scenario provides: patient data, indication, route, shape, dose type. +These are the filter inputs for OrderContext.create + getScenarios. +Concentrations and dose limits come from ZIndex/GenFORM — not from the FHIR scenario. +""" + +// A resource provider is required for OrderContext operations. +// In production, use Api.getCachedProviderWithDataUrlId. +// Here we use the demo cache that is available without authentication. +let provider : Resources.IResourceProvider = + Api.getCachedProviderWithDataUrlId OrderLogging.noOp (Environment.GetEnvironmentVariable("GENPRES_URL_ID")) + + +/// Reconstruct an OrderContext for a given FHIR scenario by: +/// 1. Building the Patient from scenario patient data +/// 2. Creating an initial OrderContext +/// 3. Setting the filter from the scenario's indication, route, shape, and dose type +/// 4. Evaluating to trigger the ZIndex/GenFORM lookup +let reconstructOrderContext (scenario: FhirScenario) : Result = + let pat = buildPatient scenario + let doseType = parseDoseType scenario.DoseType + + let ctx = OrderContext.create OrderLogging.noOp provider pat + + // Set filter fields from the FHIR scenario context + let filter = + { ctx.Filter with + Indication = Some scenario.Indication + Generic = Some scenario.MedicationName + Route = Some scenario.Route + Form = Some scenario.Shape + DoseType = Some doseType + } + + { ctx with Filter = filter } + |> OrderContext.UpdateOrderContext + |> OrderContext.evaluate OrderLogging.noOp provider + |> function + | Ok cmd -> cmd |> OrderContext.Command.get |> Ok + | Error(msg, _) -> Error msg + + +for scenario in allScenarios do + match reconstructOrderContext scenario with + | Error msg -> + printfn "\n--- Scenario %s: %s ---" scenario.ScenarioId scenario.Description + printfn " Lookup error: %s" msg + | Ok ctx -> + printOrderScenario scenario ctx + + +// ============================================================================= +// STEP 5-6: Run through the order pipeline with scenario quantities +// ============================================================================= +// +// Once an OrderScenario has been retrieved via lookup, the orderable quantities, +// dose rate, and schedule frequency from the FHIR scenario can be applied. +// +// These are the ONLY values that come from the FHIR scenario and are directly +// set on the order: +// - Component orderable quantities (from products block) +// - Orderable dose quantity or rate (from administration block / schema rate) +// - Schedule frequency (from schema pattern) +// +// The solver then derives all other values from the ZIndex dose rules. + +printfn "\n=== STEP 5-6: Running Scenarios through the Order Pipeline ===" + +let runOrderScenario (scenario: FhirScenario) = + printfn "\n--- Scenario %s: %s ---" scenario.ScenarioId scenario.Description + + match reconstructOrderContext scenario with + | Error msg -> + printfn " Context error: %s" msg + | Ok ctx -> + match ctx.Scenarios |> Array.tryHead with + | None -> + printfn " No scenarios found for this filter combination." + printfn " (Check that indication/route/shape/dose type match the GenFORM database)" + | Some sc -> + printfn " Scenario: %s | %s | %A" sc.Name sc.Route sc.DoseType + printfn " Order type: %A" sc.Order.Schedule + + // Solve the order to propagate all constraints from ZIndex dose rules + match sc.Order |> Order.solveMinMax "FHIR-ImplementationPlan" true OrderLogging.noOp with + | Error(_, msg) -> + printfn " Solve warning: %s" msg + sc.Order |> Order.printTable ConsoleTables.Format.Minimal + + | Ok solvedOrder -> + printfn " Solved OK." + solvedOrder |> Order.printTable ConsoleTables.Format.Minimal + + +for scenario in allScenarios do + runOrderScenario scenario + + +// ============================================================================= +// STEP 7: Print summary of results +// ============================================================================= + +printfn "\n=== STEP 7: Summary ===" +printfn "Processed %i FHIR scenarios." (List.length allScenarios) +printfn "" + +for scenario in allScenarios do + let status = + match reconstructOrderContext scenario with + | Error msg -> $"CONTEXT ERROR: {msg}" + | Ok ctx -> + let n = ctx.Scenarios.Length + + if n = 0 then + "NO SCENARIOS FOUND" + else + $"OK — {n} scenario(s) found" + + printfn " Scenario %-6s %-52s %s" scenario.ScenarioId scenario.Description status + + +// ============================================================================= +// STEP 8: FHIR R4 Bidirectional Translation Investigation +// ============================================================================= +// +// Official FHIR R4 documentation references: +// MedicationRequest https://hl7.org/fhir/R4/medicationrequest.html +// Medication https://hl7.org/fhir/R4/medication.html +// Dosage (Dosage) https://hl7.org/fhir/R4/dosage.html +// Timing https://hl7.org/fhir/R4/datatypes.html#Timing +// Quantity https://hl7.org/fhir/R4/datatypes.html#Quantity +// Ratio https://hl7.org/fhir/R4/datatypes.html#Ratio +// CodeableConcept https://hl7.org/fhir/R4/datatypes.html#CodeableConcept +// +// Dutch G-Standard / NL FHIR: +// GPK codes OID urn:oid:2.16.840.1.113883.2.4.4.7 +// Route thesaurus 9 OID urn:oid:2.16.840.1.113883.2.4.4.9 +// Form thesaurus 10 OID urn:oid:2.16.840.1.113883.2.4.4.10 +// Medicatie-9 https://informatiestandaarden.nictiz.nl/wiki/Landingspagina_Medicatie +// +// ── Core design principle ──────────────────────────────────────────────────── +// +// FHIR resources describe WHAT was ordered (the outcome of clinical decision-making). +// GenPRES derives HOW to order by looking up the correct dosing constraints from +// the ZIndex/GenFORM database. The FHIR resource provides only the filter context +// (patient + indication + route + form + dose type) and the measured/chosen +// orderable quantities. Everything else (concentrations, dose limits) comes from +// ZIndex. +// +// ── FHIR R4 resource types (mirror of the official data model) ─────────────── +// +// The types below model exactly the FHIR R4 fields used in GenPRES integration. +// They are defined here in the script to document the mapping; the actual +// Informedica.FHIR.Lib library would use Hl7.Fhir.R4 (NuGet) types instead. + +/// A single coding within a CodeableConcept +type FhirCoding = + { + // Coding system URI, e.g. "urn:oid:2.16.840.1.113883.2.4.4.7" for GPK + System: string + // Code value within the system + Code: string + // Human-readable display text + Display: string option + } + + +/// A concept represented by one or more codings plus an optional display text +type FhirCodeableConcept = + { + Coding: FhirCoding list + // Free-text representation (used when no coding is available) + Text: string option + } + + +/// A measured or measurable quantity with unit +type FhirQuantity = + { + Value: decimal + // Unit display label + Unit: string + // Unit system URI, e.g. "http://unitsofmeasure.org" for UCUM + System: string option + // UCUM code for the unit, e.g. "mL", "mg", "h" + Code: string option + } + + +/// A ratio of two quantities, used for rates and concentrations +type FhirRatio = + { + // Numerator (e.g. 85 mL for a rate of 85 mL/uur) + Numerator: FhirQuantity + // Denominator (e.g. 1 uur) + Denominator: FhirQuantity + } + + +/// The repeat pattern within a Timing resource +/// See https://hl7.org/fhir/R4/datatypes.html#Timing +type FhirTimingRepeat = + { + // How many times per period (e.g. 4 for "4 x/dag") + Frequency: int option + // Length of the period (e.g. 1 for "per dag", 36 for "per 36 uur") + Period: decimal option + // UCUM-based period unit: s | min | h | d | wk | mo | a + PeriodUnit: string option + // Duration of each administration for timed infusions (e.g. 15 min) + Duration: decimal option + // UCUM-based duration unit + DurationUnit: string option + // Exact clock times for Timed orders, format HH:MM:SS + TimeOfDay: string list + } + + +/// The Timing datatype: describes when an event is to occur +type FhirTiming = + { + // Specific event date/times (for OnceTimed with a fixed start) + Event: DateTime list + // Repeating pattern + Repeat: FhirTimingRepeat option + } + + +/// Rate expressed as either a ratio or a simple quantity +type FhirDosageRate = + // Ratio: numerator/denominator, e.g. 85 mL / 1 uur + | RateRatio of FhirRatio + // SimpleQuantity with UCUM composite unit, e.g. "85 mL/h" + | RateQuantity of FhirQuantity + + +/// A dose/rate entry within dosageInstruction.doseAndRate[] +type FhirDosageAndRate = + { + // Type of dose entry (e.g. "ordered" vs. "calculated"); optional + Type: FhirCodeableConcept option + // The dose quantity per administration + Dose: FhirQuantity option + // The infusion rate + Rate: FhirDosageRate option + } + + +/// The Dosage datatype: instructions for how medication should be taken/given +/// See https://hl7.org/fhir/R4/dosage.html +type FhirDosage = + { + // Free-text dosage instructions (human-readable summary) + Text: string option + // Timing of administration + Timing: FhirTiming option + // Route of administration (G-Standard thesaurus 9) + Route: FhirCodeableConcept option + // Method of administration + Method: FhirCodeableConcept option + // Dose quantity and rate + DoseAndRate: FhirDosageAndRate list + } + + +/// A single ingredient within a Medication resource +type FhirMedicationIngredient = + { + // The substance identified by GPK code + ItemCodeableConcept: FhirCodeableConcept + // Whether this is an active ingredient + IsActive: bool option + // Concentration: e.g. 10 mg / 1 mL + Strength: FhirRatio option + } + + +/// The Medication resource: describes the medication product +/// See https://hl7.org/fhir/R4/medication.html +type FhirMedication = + { + ResourceType: string // always "Medication" + Id: string option + // Product identification by GPK code + Code: FhirCodeableConcept + // Pharmaceutical form (G-Standard thesaurus 10) + Form: FhirCodeableConcept option + // Ingredient list (supports multi-ingredient products) + Ingredient: FhirMedicationIngredient list + } + + +/// A reference to another FHIR resource +type FhirReference = + { + // Relative or absolute reference, e.g. "Patient/123456" + Reference: string + Display: string option + } + + +/// The MedicationRequest resource: a prescription or medication order +/// See https://hl7.org/fhir/R4/medicationrequest.html +type FhirMedicationRequest = + { + ResourceType: string // always "MedicationRequest" + Id: string option + // Status: active | draft | on-hold | cancelled | completed | ... + Status: string + // Intent: proposal | plan | order | original-order | ... + Intent: string + // Identified medication by GPK code (use medicationCodeableConcept for GPK) + MedicationCodeableConcept: FhirCodeableConcept + // The patient + Subject: FhirReference + // When the prescription was written + AuthoredOn: DateTime option + // Clinical indication (ICD-10 or free text) + ReasonCode: FhirCodeableConcept list + // Free-text notes + Note: string list + // Dosage instructions + DosageInstruction: FhirDosage list + // How much to dispense + DispenseRequest: {| Quantity: FhirQuantity option |} option + // Contained Medication resources (for inline ingredient detail) + Contained: FhirMedication list + } + + +// ── G-Standard and FHIR system constants ───────────────────────────────────── +// +// These OIDs and system URIs are used to identify coding systems in FHIR resources. + +/// G-Standard and FHIR coding system constants +module FhirSystems = + + /// OID for the Dutch G-Standard GPK product table + let gpk = "urn:oid:2.16.840.1.113883.2.4.4.7" + + /// OID for the G-Standard route thesaurus (Thesaurus 9) + let route = "urn:oid:2.16.840.1.113883.2.4.4.9" + + /// OID for the G-Standard pharmaceutical form thesaurus (Thesaurus 10) + let form = "urn:oid:2.16.840.1.113883.2.4.4.10" + + /// UCUM unit system URI + let ucum = "http://unitsofmeasure.org" + + /// SNOMED CT system URI + let snomed = "http://snomed.info/sct" + + +/// G-Standard route code → GenPRES route name and vice versa +module RouteMapping = + + // G-Standard Thesaurus 9 route codes + let codeToName = + Map.ofList + [ + "2", "INTRAVENEUS" + "9", "ORAAL" + "12", "RECTAAL" + "14", "SUBCUTAAN" + "15", "INTRAMUSCULAIR" + "46", "INHALATIE" + ] + + let nameToCode = codeToName |> Map.toList |> List.map (fun (k, v) -> v, k) |> Map.ofList + + let toCode name = + nameToCode |> Map.tryFind name |> Option.defaultValue "" + + let toName code = + codeToName |> Map.tryFind code |> Option.defaultValue "" + + +/// FHIR timing period unit (UCUM-based) ↔ GenPRES time unit +module PeriodUnitMapping = + + let fhirToGenPres = + Map.ofList + [ + "s", "seconde" + "min", "minuut" + "h", "uur" + "d", "dag" + "wk", "week" + "mo", "maand" + "a", "jaar" + ] + + let genPresToFhir = fhirToGenPres |> Map.toList |> List.map (fun (k, v) -> v, k) |> Map.ofList + + let toGenPres unit = + fhirToGenPres |> Map.tryFind unit |> Option.defaultValue unit + + let toFhir unit = + genPresToFhir |> Map.tryFind unit |> Option.defaultValue unit + + +// ── FHIR → GenPRES translation ──────────────────────────────────────────────── +// +// fromFhirMedicationRequest: converts a FhirMedicationRequest resource into a +// FhirScenario that can drive the GenPRES OrderContext lookup. +// +// Translation logic: +// - Patient context comes from the FHIR Patient resource (passed as parameters here) +// - Indication: MedicationRequest.reasonCode[0].text +// - Generic name: resolved from GPK code via ZIndex.GenericProduct.get [gpkCode] +// - Route: MedicationRequest.dosageInstruction[0].route.text (or look up from coding) +// - Shape: Medication.form.text (from contained Medication resource) +// - DoseType: inferred from timing and rate structure (see below) +// - Products: from Medication.ingredient[] + MedicationRequest quantities +// - AdminQuantity: dosageInstruction[0].doseAndRate[0].doseQuantity +// - Rate: dosageInstruction[0].doseAndRate[0].rateRatio (RateFormUnit/RateTimeUnit) +// - Frequency: dosageInstruction[0].timing.repeat (frequency, period, periodUnit) +// - ExactTimes: dosageInstruction[0].timing.repeat.timeOfDay + +/// Infer the GenPRES DoseType string from the structure of a FHIR Dosage +let inferDoseType (dosage: FhirDosage) : string = + let hasRate = dosage.DoseAndRate |> List.exists (fun dr -> dr.Rate.IsSome) + + match dosage.Timing with + | None -> if hasRate then "Continuous" else "Once" + | Some timing -> + match timing.Repeat with + | None -> if hasRate then "Continuous" else "Once" + | Some r -> + match r.Frequency, r.Duration with + // No frequency → continuous + | None, _ -> if hasRate then "Continuous" else "Once" + // One-time with duration or rate → OnceTimed + | Some 1, Some _ -> "OnceTimed" + | Some 1, None when hasRate -> "OnceTimed" + // One-time, no rate → Once + | Some 1, None -> "Once" + // Multiple per period with rate → Timed + | Some _, _ when hasRate -> "Timed" + // Multiple per period, no rate → Discontinuous + | Some _, _ -> "Discontinuous" + + +/// Extract schedule from a FHIR Dosage +let extractSchema (dosage: FhirDosage) : AdministrationSchema = + let rateQty, rateFormUnit, rateTimeUnit = + dosage.DoseAndRate + |> List.tryHead + |> Option.bind _.Rate + |> Option.map + (function + | RateRatio r -> Some r.Numerator.Value, Some r.Numerator.Unit, Some r.Denominator.Unit + | RateQuantity q -> + // Parse UCUM composite unit like "mL/h" + let parts = + (q.Code |> Option.defaultValue q.Unit).Split('/') + + match parts with + | [| num; den |] -> Some q.Value, Some num, Some(PeriodUnitMapping.toGenPres den) + | _ -> Some q.Value, Some q.Unit, None) + |> Option.defaultValue (None, None, None) + + let frequency, timePeriod, timeUnit, exactTimes = + dosage.Timing + |> Option.map (fun t -> + let r = t.Repeat + + let freq = r |> Option.bind _.Frequency + let period = r |> Option.bind _.Period + + let unit = + r + |> Option.bind _.PeriodUnit + |> Option.map PeriodUnitMapping.toGenPres + + let times = r |> Option.map _.TimeOfDay |> Option.defaultValue [] + freq, period, unit, times) + |> Option.defaultValue (None, None, None, []) + + { + Frequency = frequency + TimePeriod = timePeriod + TimeUnit = timeUnit + RateQuantity = rateQty + RateFormUnit = rateFormUnit + RateTimeUnit = rateTimeUnit + ExactTimes = exactTimes + } + + +/// Convert a FHIR MedicationRequest + patient parameters into a FhirScenario. +/// The FhirScenario can then drive GenPRES OrderContext lookup (see Step 4). +let fromFhirMedicationRequest + (weightKg: decimal) + (heightCm: decimal) + (gender: string) + (req: FhirMedicationRequest) + : FhirScenario = + + // --- Indication (reasonCode.text) --- + let indication = + req.ReasonCode + |> List.tryHead + |> Option.bind _.Text + |> Option.defaultValue "" + + // --- GPK code → generic name via ZIndex --- + let gpkCode = + req.MedicationCodeableConcept.Coding + |> List.tryFind (fun c -> c.System = FhirSystems.gpk) + |> Option.map _.Code + |> Option.defaultValue "" + + let medicationName = + if gpkCode |> String.isNullOrWhiteSpace then + req.MedicationCodeableConcept.Text |> Option.defaultValue "" + else + // Try to parse GPK code as int and look up via ZIndex + match gpkCode |> System.Int32.TryParse with + | true, gpkInt -> + match GenericProduct.get [ gpkInt ] |> Array.tryHead with + | Some gp -> gp.Name + | None -> req.MedicationCodeableConcept.Text |> Option.defaultValue gpkCode + | false, _ -> req.MedicationCodeableConcept.Text |> Option.defaultValue gpkCode + + // --- Route (dosageInstruction.route) --- + let route = + req.DosageInstruction + |> List.tryHead + |> Option.bind _.Route + |> Option.map (fun cc -> + match cc.Text with + | Some t -> t + | None -> + cc.Coding + |> List.tryFind (fun c -> c.System = FhirSystems.route) + |> Option.map (fun c -> RouteMapping.toName c.Code) + |> Option.defaultValue "") + |> Option.defaultValue "" + + // --- Shape (Medication.form from contained resource) --- + let shape = + req.Contained + |> List.tryHead + |> Option.bind _.Form + |> Option.bind _.Text + |> Option.defaultValue "" + + // --- DoseType (inferred from dosage timing/rate structure) --- + let doseType = + req.DosageInstruction + |> List.tryHead + |> Option.map inferDoseType + |> Option.defaultValue "Once" + + // --- Admin quantity (dosageInstruction.doseAndRate.doseQuantity) --- + let adminQty, adminUnit = + req.DosageInstruction + |> List.tryHead + |> Option.bind (fun d -> d.DoseAndRate |> List.tryHead) + |> Option.bind _.Dose + |> Option.map (fun q -> q.Value, q.Unit) + |> Option.defaultValue (0m, "") + + // --- Schema (timing + rate) --- + let schema = + req.DosageInstruction + |> List.tryHead + |> Option.map extractSchema + |> Option.defaultValue + { + Frequency = None + TimePeriod = None + TimeUnit = None + RateQuantity = None + RateFormUnit = None + RateTimeUnit = None + ExactTimes = [] + } + + // --- Products (from contained Medication.ingredient[]) --- + // NOTE: In a real implementation the admin quantity would be distributed + // across ingredients according to their proportions. + let products = + req.Contained + |> List.collect (fun med -> + med.Ingredient + |> List.map (fun ing -> + let gpk = + ing.ItemCodeableConcept.Coding + |> List.tryFind (fun c -> c.System = FhirSystems.gpk) + |> Option.map _.Code + |> Option.defaultValue "" + + let display = + ing.ItemCodeableConcept.Coding + |> List.tryHead + |> Option.bind _.Display + |> Option.defaultValue (ing.ItemCodeableConcept.Text |> Option.defaultValue "") + + { + GpkPlaceholder = gpk + Quantity = adminQty + Unit = adminUnit + Description = display + })) + + { + ScenarioId = req.Id |> Option.defaultValue "" + Description = req.Note |> List.tryHead |> Option.defaultValue "" + WeightKg = weightKg + HeightCm = heightCm + Gender = gender + Indication = indication + MedicationName = medicationName + Route = route + Shape = shape + DoseType = doseType + Products = products + AdminQuantity = adminQty + AdminUnit = adminUnit + Schema = schema + } + + +// ── GenPRES → FHIR translation ──────────────────────────────────────────────── +// +// toFhirMedicationRequest: converts a GenPRES FhirScenario (populated after +// OrderContext lookup and order pipeline) into a FHIR R4 MedicationRequest. +// +// Translation logic: +// - MedicationRequest.status: "active" +// - MedicationRequest.intent: "order" +// - MedicationRequest.medicationCodeableConcept: GPK code from ZIndex lookup +// - MedicationRequest.reasonCode: scenario.Indication +// - DosageInstruction.route: RouteMapping.toCode scenario.Route +// - DosageInstruction.timing.repeat: Frequency / TimePeriod / TimeUnit / ExactTimes +// - DosageInstruction.doseAndRate.doseQuantity: AdminQuantity + AdminUnit +// - DosageInstruction.doseAndRate.rateRatio: RateFormUnit / RateTimeUnit / RateQuantity +// - Contained Medication.form: scenario.Shape +// - Contained Medication.ingredient: one entry per product + +/// Build a FHIR Timing resource from an AdministrationSchema +let toFhirTiming (schema: AdministrationSchema) : FhirTiming option = + let repeat = + match schema.Frequency, schema.TimePeriod, schema.TimeUnit with + | None, None, None when schema.ExactTimes |> List.isEmpty -> None + | _ -> + Some + { + Frequency = schema.Frequency + Period = schema.TimePeriod + PeriodUnit = schema.TimeUnit |> Option.map PeriodUnitMapping.toFhir + Duration = None + DurationUnit = None + TimeOfDay = schema.ExactTimes + } + + if repeat.IsSome || schema.ExactTimes |> List.isEmpty |> not then + Some { Event = []; Repeat = repeat } + else + None + + +/// Build a FHIR DosageAndRate from an AdministrationSchema + admin quantity +let toFhirDosageAndRate (adminQty: decimal) (adminUnit: string) (schema: AdministrationSchema) : FhirDosageAndRate list = + let dose = + if adminQty > 0m then + Some + { + Value = adminQty + Unit = adminUnit + System = Some FhirSystems.ucum + Code = Some adminUnit + } + else + None + + let rate = + match schema.RateQuantity, schema.RateFormUnit, schema.RateTimeUnit with + | Some qty, Some fu, Some tu -> + Some( + RateRatio + { + Numerator = + { + Value = qty + Unit = fu + System = Some FhirSystems.ucum + Code = Some fu + } + Denominator = + { + Value = 1m + Unit = tu + System = Some FhirSystems.ucum + Code = Some(PeriodUnitMapping.toFhir tu) + } + } + ) + | _ -> None + + [ { Type = None; Dose = dose; Rate = rate } ] + + +/// Convert a GenPRES FhirScenario into a FHIR R4 MedicationRequest. +/// GPK code must be resolved beforehand (real GPK, not placeholder). +let toFhirMedicationRequest + (resolvedGpkCode: string) + (patientRef: string) + (scenario: FhirScenario) + : FhirMedicationRequest = + + let routeCode = RouteMapping.toCode scenario.Route + + let dosage = + { + Text = None + Timing = toFhirTiming scenario.Schema + Route = + Some + { + Coding = + [ + { + System = FhirSystems.route + Code = routeCode + Display = Some scenario.Route + } + ] + Text = Some scenario.Route + } + Method = None + DoseAndRate = toFhirDosageAndRate scenario.AdminQuantity scenario.AdminUnit scenario.Schema + } + + let medicationIngredients = + scenario.Products + |> List.map (fun p -> + { + ItemCodeableConcept = + { + Coding = + [ + { + System = FhirSystems.gpk + Code = p.GpkPlaceholder + Display = Some p.Description + } + ] + Text = Some p.Description + } + IsActive = Some true + // Strength would be populated from ZIndex lookup + Strength = None + }) + + let containedMedication = + { + ResourceType = "Medication" + Id = Some $"med-{scenario.ScenarioId}" + Code = + { + Coding = + [ + { + System = FhirSystems.gpk + Code = resolvedGpkCode + Display = Some scenario.MedicationName + } + ] + Text = Some scenario.MedicationName + } + Form = + Some + { + Coding = [] + Text = Some scenario.Shape + } + Ingredient = medicationIngredients + } + + { + ResourceType = "MedicationRequest" + Id = Some $"req-{scenario.ScenarioId}" + Status = "active" + Intent = "order" + MedicationCodeableConcept = + { + Coding = + [ + { + System = FhirSystems.gpk + Code = resolvedGpkCode + Display = Some scenario.MedicationName + } + ] + Text = Some scenario.MedicationName + } + Subject = { Reference = patientRef; Display = None } + AuthoredOn = Some DateTime.UtcNow + ReasonCode = + [ + { + Coding = [] + Text = Some scenario.Indication + } + ] + Note = [ scenario.Description ] + DosageInstruction = [ dosage ] + DispenseRequest = None + Contained = [ containedMedication ] + } + + +// ── Demonstrate round-trip for each scenario ────────────────────────────────── + +printfn "\n=== STEP 8: FHIR R4 Bidirectional Translation ===" +printfn """ +Official FHIR R4 resource docs: + MedicationRequest https://hl7.org/fhir/R4/medicationrequest.html + Medication https://hl7.org/fhir/R4/medication.html + Dosage https://hl7.org/fhir/R4/dosage.html + +Translation direction: + A) FHIR MedicationRequest → FhirScenario (via fromFhirMedicationRequest) + → set Filter on OrderContext → call getScenarios → look up ZIndex rules + → apply orderable quantities from FHIR scenario → run order pipeline + B) GenPRES OrderScenario → FHIR MedicationRequest (via toFhirMedicationRequest) + → serialize to JSON for EHR / medication administration system + +Key insight: FHIR provides the filter context and orderable quantities only. +Concentrations and dose limits are always derived from ZIndex/GenFORM, never +stored in the FHIR resource. +""" + +printfn "--- Round-trip demonstration (scenario → FHIR → scenario) ---" + +for scenario in allScenarios do + printfn "" + printfn " Scenario %-7s %s" scenario.ScenarioId scenario.Description + + // A) GenPRES → FHIR + let gpkPlaceholder = + scenario.Products + |> List.tryHead + |> Option.map _.GpkPlaceholder + |> Option.defaultValue "" + + let fhirReq = toFhirMedicationRequest gpkPlaceholder "Patient/DEMO" scenario + + printfn " → FHIR MedicationRequest: id=%A status=%s intent=%s" + fhirReq.Id + fhirReq.Status + fhirReq.Intent + + printfn " medication: %s [GPK: %s]" + (fhirReq.MedicationCodeableConcept.Text |> Option.defaultValue "") + (fhirReq.MedicationCodeableConcept.Coding |> List.tryHead |> Option.map _.Code |> Option.defaultValue "") + + printfn " indication: %s" + (fhirReq.ReasonCode |> List.tryHead |> Option.bind _.Text |> Option.defaultValue "") + + printfn " route: %s" + (fhirReq.DosageInstruction |> List.tryHead |> Option.bind _.Route |> Option.bind _.Text |> Option.defaultValue "") + + let dosageText = + fhirReq.DosageInstruction + |> List.tryHead + |> Option.map (fun d -> + let doseStr = + d.DoseAndRate + |> List.tryHead + |> Option.bind _.Dose + |> Option.map (fun q -> $"{q.Value} {q.Unit}") + |> Option.defaultValue "(no dose)" + + let rateStr = + d.DoseAndRate + |> List.tryHead + |> Option.bind _.Rate + |> Option.map (function + | RateRatio r -> $"{r.Numerator.Value} {r.Numerator.Unit}/{r.Denominator.Unit}" + | RateQuantity q -> $"{q.Value} {q.Unit}") + |> Option.defaultValue "(no rate)" + + let freqStr = + d.Timing + |> Option.bind _.Repeat + |> Option.map (fun r -> + match r.Frequency, r.PeriodUnit with + | Some f, Some u -> $"{f} x/{PeriodUnitMapping.toGenPres u}" + | _ -> "(continuous)") + |> Option.defaultValue "(no schedule)" + + $"dose={doseStr} rate={rateStr} freq={freqStr}") + |> Option.defaultValue "(no dosage)" + + printfn " dosage: %s" dosageText + + // B) FHIR → FhirScenario (round-trip) + let roundTripped = + fromFhirMedicationRequest scenario.WeightKg scenario.HeightCm scenario.Gender fhirReq + + let routeMatch = roundTripped.Route = scenario.Route + let doseTypeMatch = roundTripped.DoseType = scenario.DoseType + let indicationMatch = roundTripped.Indication = scenario.Indication + + printfn " ← Round-trip: Route=%s DoseType=%s Indication=%s" + (if routeMatch then "✓" else $"✗ got '{roundTripped.Route}'") + (if doseTypeMatch then "✓" else $"✗ got '{roundTripped.DoseType}'") + (if indicationMatch then "✓" else $"✗ got '{roundTripped.Indication}'") + + +// ── Next implementation steps ───────────────────────────────────────────────── + +printfn """ + +=== Next steps for Informedica.FHIR.Lib === + 1. Add Hl7.Fhir.R4 NuGet package (paket: 'nuget Hl7.Fhir.R4') + 2. Replace the script FhirMedicationRequest type with the Hl7.Fhir.R4 model type + 3. Implement fromFhirMedicationRequest using the FhirScenario approach: + MedicationRequest resource → FhirScenario → OrderContext filter → getScenarios + 4. Implement toFhirMedicationRequest: + OrderScenario + orderable quantities → MedicationRequest resource + 5. Add JSON serialization using Hl7.Fhir.Serialization.FhirJsonParser + 6. Write Expecto tests for each scenario defined in this script + 7. Validate round-trip: fromFhirMedicationRequest (toFhirMedicationRequest scenario) + preserves Route, DoseType, Indication, AdminQuantity, Rate, Frequency +""" diff --git a/src/Informedica.FHIR.Lib/Scripts/load.fsx b/src/Informedica.FHIR.Lib/Scripts/load.fsx new file mode 100644 index 00000000..6e23a3a5 --- /dev/null +++ b/src/Informedica.FHIR.Lib/Scripts/load.fsx @@ -0,0 +1,25 @@ +#I __SOURCE_DIRECTORY__ + +let stopWatch = System.Diagnostics.Stopwatch() +stopWatch.Start() + +fsi.AddPrinter _.ToShortDateString() + +#load "../../../scripts/load-dependencies.fsx" + +#r "../../Informedica.Utils.Lib/bin/Debug/net10.0/Informedica.Utils.Lib.dll" +#r "../../Informedica.Logging.Lib/bin/Debug/net10.0/Informedica.Logging.Lib.dll" +#r "../../Informedica.GenUNITS.Lib/bin/Debug/net10.0/Informedica.GenUNITS.Lib.dll" +#r "../../Informedica.GenCORE.Lib/bin/Debug/net10.0/Informedica.GenCORE.Lib.dll" +#r "../../Informedica.GenSOLVER.Lib/bin/Debug/net10.0/Informedica.GenSOLVER.Lib.dll" +#r "../../Informedica.GenFORM.Lib/bin/Debug/net10.0/Informedica.GenFORM.Lib.dll" +#r "../../Informedica.ZIndex.Lib/bin/Debug/net10.0/Informedica.ZIndex.Lib.dll" +#r "../../Informedica.GenORDER.Lib/bin/Debug/net10.0/Informedica.GenORDER.Lib.dll" + +open System +open Informedica.Utils.Lib + +let zindexPath = __SOURCE_DIRECTORY__ |> Path.combineWith "../../../" +Environment.CurrentDirectory <- zindexPath + +printfn $"elapsed time: {stopWatch.ElapsedMilliseconds / 1000L}"