Skip to content

Instantly share code, notes, and snippets.

@achal7
Created May 12, 2026 16:57
Show Gist options
  • Select an option

  • Save achal7/c2a076f3a5cc2e1ea74ddbf11f84c210 to your computer and use it in GitHub Desktop.

Select an option

Save achal7/c2a076f3a5cc2e1ea74ddbf11f84c210 to your computer and use it in GitHub Desktop.
SupplyChainMockup
ConstraintAbstraction we needValues inside structureFunctorIndependent contextual valuesApplicativeDependent contextual stepsMonadEffectful functionsKleisli categoryCombining valuesMonoidTransforming between contextsNatural transformationConsuming valuesContravariant functorInput-output adaptationProfunctorRecursive interpretationF-algebra / catamorphismWorkflow descriptionFree structure / DSL
1. What are the input and output types?
2. Can this step fail?
3. Are checks independent or dependent?
4. Do we want error accumulation or fail-fast?
5. Does order matter?
6. What operation do we need?
7. Which lawful abstraction fits?
Type + operation + laws = lawful structure
Business use case
Identify domain states
Represent states as types
Identify valid transitions
Represent transitions as functions
Ask whether each transition is plain, optional, failing, accumulating, async, etc.
Choose abstraction:
- plain function
- Option
- Validation
- Result
- Async<Result<_,_>>
Use laws to ensure operations preserve meaning
Compose small lawful parts into larger workflows
> We represented domain states as types, business transitions as arrows, and used lawful abstractions like Applicative, Monad, Semigroup, Kleisli composition, and natural transformation-like conversion to compose the workflow safely.
***
# 5. The Bigger Picture
Think of category theory as a **language of composition**.
Our implementation used that language to answer:
| Design question | Category-theoretic idea |
| -------------------------------------------- | -------------------------------------- |
| What domain states exist? | Objects |
| What transitions are valid? | Morphisms |
| Can steps compose directly? | Composition |
| What if a step can fail? | Result / Monad |
| What if validations are independent? | Applicative |
| How do we collect errors? | Semigroup / Monoid |
| How do we compose failing functions? | Kleisli category |
| How do we move from validation to execution? | Natural transformation-like conversion |
| What must remain true? | Laws |
***
```text
We are not inventing category theory.
We are discovering category-theoretic structures inside our domain model.
```
Once you start seeing this, the rest of the concepts become much easier:
* Functor = preserving structure while mapping
* Applicative = combining independent contextual values
* Monad = sequencing dependent contextual computations
* Kleisli category = composing effectful domain transitions
* Natural transformation = moving between contexts consistently
* Isomorphism = lossless representation change
* Monoid = lawful combination
```text
Product/coproduct types define valid states.
Morphisms define valid transitions.
Composition defines valid workflows.
```
### Common Contravariant Structures
Validator<A> = A -> bool
Predicate<A> = A -> bool
Printer<A> = A -> string
Encoder<A> = A -> Json
Comparer<A> = A -> A -> int
Equality<A> = A -> A -> bool
Consumer<A> = A -> unit
Sink<A> = A -> Effect
Consumer<A> = contravariant
Producer<A> = covariant
Transformer<A,B> = profunctor-like
```text
Contravariant = adapt reusable consumers/checks
Applicative = combine independent validation results
Monad = sequence dependent business steps
```
```text
Contravariant Rules
Applicative Validation
Natural transformation to Result
Monad / Kleisli execution workflow
```
A Profunctor represents something that consumes an input and produces an output.
Therefore, we can adapt both sides:
* adapt the input backward,
* adapt the output forward.
```text
dimap : ('a2 -> 'a) -> ('b -> 'b2) -> P<'a, 'b> -> P<'a2, 'b2>
let dimap before after p = before >> p >> after
```
```text
So use lenses when:
* the update is structurally safe,
* the field replacement is meaningful,
* you need composable nested updates,
* you are working with DTOs/configs/view models,
* invariants are already protected by types.
Avoid raw lenses when:
* arbitrary field setting can break invariants,
* domain operation needs validation,
* update has business rules.
```
```text
Independent field checks → Applicative Validation
Dependent domain transitions → Result Monad / Kleisli
External calls → Async<Result<_,_>>
DTO mapping → Functor/Bifunctor/Profunctor
State transitions → Typed arrows
Aggregation → Monoid/Fold
```
Algebraic API = capabilities as functions
Free DSL = capabilities as data
Interpreter = implementation of capabilities
# **Category Theory with F#: From Composition to Supply Chain Domain Modeling**
***
# Guiding Learning Method
Every chapter will follow this pattern:
```text
1. Start with a concrete F# / domain problem
2. Discover the recurring composition pattern
3. Derive the abstraction
4. Introduce formal category theory definition
5. Study equations and laws
6. Encode the idea in F#
7. Apply it to domain modeling
8. Connect it to supply chain implementation
```
So we will not learn like this:
```text
Definition → Theorem → Example
```
Instead, we will learn like this:
```text
Problem → Need → Pattern → Abstraction → Definition → Laws → F# → Domain Design
```
This is the best fit for your preferred **constructive / derivational learning approach**.
***
# Course Outline
## Part 0 — Orientation
### 1. Why Category Theory for F# Developers?
Purpose:
* Why category theory matters for software design
* Why composition is the central theme
* Why F# is a good host language
* How category theory helps domain modeling
* How the constructive learning approach will work
***
# Part 1 — Composition Foundations
### 2. Functions, Composition, and Identity
We begin from ordinary F# functions.
Concepts:
* Function composition
* Identity
* Associativity
* Pipelines
* Information flow
Formal ideas:
```text
id_A : A → A
g ∘ f : A → C
```
***
### 3. Categories
Concepts:
* Objects
* Morphisms
* Identity morphisms
* Composition
* Category laws
F# intuition:
```text
Types ≈ Objects
Functions ≈ Morphisms
```
***
### 4. Reasoning with Laws
Concepts:
* Equational reasoning
* Lawful abstractions
* Refactoring safely
* Laws as design contracts
Main question:
> What must remain true after transformation?
***
# Part 2 — Data Shape and Structure
### 5. Product Types, Coproduct Types, and Algebraic Data Types
Concepts:
* Records
* Tuples
* Discriminated unions
* AND types
* OR types
* Domain modeling with shape
Formal intuition:
```text
Product : A × B
Coproduct : A + B
```
***
### 6. Initial and Terminal Objects
Concepts:
* Terminal object
* Initial object
* Unit
* Impossible values
* Unique morphisms
Formal ideas:
```text
Terminal object: for every A, exactly one morphism A → 1
Initial object : for every A, exactly one morphism 0 → A
```
***
# Part 3 — Structure-Preserving Mappings
### 7. Functors
Concepts:
* Mapping inside structure
* Preserving shape
* `map`
* Containers and contexts
Laws:
```text
F(id) = id
F(g ∘ f) = F(g) ∘ F(f)
```
F# examples:
* `Option.map`
* `List.map`
* `Result.map`
***
### 8. Bifunctors and Higher-Kinded Patterns in F\#
Concepts:
* Mapping over two type parameters
* Working around lack of native higher-kinded types
* Error/value transformations
F# examples:
* `Result<'error, 'value>`
* Pair-like structures
* Domain outcomes
***
### 9. Natural Transformations
Concepts:
* Transforming one context into another
* Preserving meaning across structures
Formal idea:
```text
η : F ⇒ G
```
Naturality law:
```text
G f ∘ η_A = η_B ∘ F f
```
***
# Part 4 — Combining and Sequencing Computations
### 10. Semigroups and Monoids
Concepts:
* Combining values
* Associativity
* Identity
* Aggregation
* Folding
Laws:
```text
(a <> b) <> c = a <> (b <> c)
empty <> a = a
a <> empty = a
```
Supply chain examples:
* Inventory quantity aggregation
* Error accumulation
* Demand aggregation
***
### 11. Applicative Functors
Concepts:
* Independent computations in context
* Validation
* Error accumulation
* Building values from validated parts
Operations:
```text
pure : A → F A
(<*>) : F (A → B) → F A → F B
```
***
### 12. Monads
Concepts:
* Dependent sequencing
* Context-aware workflows
* `bind`
* `return`
Operations:
```text
return : A → M A
bind : M A → (A → M B) → M B
```
Monad laws:
```text
return a >>= f = f a
m >>= return = m
(m >>= f) >>= g = m >>= (fun x -> f x >>= g)
```
***
### 13. Kleisli Categories
Concepts:
* Composing effectful functions
* Functions of shape:
```text
A → M B
```
Used for:
* Fail-fast pipelines
* Service orchestration
* Domain workflows
* Order processing
***
### 14. Computation Expressions
Concepts:
* F# syntax for contextual computation
* `Bind`
* `Return`
* `ReturnFrom`
* `and!`
* `MergeSources`
This chapter connects category theory to idiomatic F#.
***
# Part 5 — Direction, Consumption, and Transformation
### 15. Contravariant Functors
Concepts:
* Consumers
* Validators
* Comparers
* Printers
* Encoders
Operation:
```text
contramap : (B → A) → F A → F B
```
***
### 16. Profunctors
Concepts:
* Things that consume and produce
* Input/output adaptation
* Bidirectional transformation
Operation:
```text
dimap : (A' → A) → (B → B') → P A B → P A' B'
```
***
### 17. Isomorphisms
Concepts:
* Lossless conversion
* Equivalent representations
* Safe refactoring
Formal condition:
```text
g ∘ f = id_A
f ∘ g = id_B
```
***
# Part 6 — Advanced Abstractions
### 18. F-Algebras, Catamorphisms, and Folding
Concepts:
* Recursive data
* Folding
* Interpretation
* Event folding
Formal intuition:
```text
F A → A
```
Supply chain examples:
* Fold order lines into totals
* Fold shipment events into shipment state
* Fold demand signals into forecast input
***
### 19. Free Structures and DSLs
Concepts:
* Free monoids
* Free applicatives
* Free monads
* Description vs interpretation
* Embedded DSLs in F#
***
### 20. Adjunctions
Concepts:
* Free/forgetful relationships
* Adding structure
* Forgetting structure
* Moving between modeling worlds
Formal idea:
```text
Hom_D(F A, B) ≅ Hom_C(A, G B)
```
***
### 21. Lenses and Optics
Concepts:
* Focusing into immutable data
* Getters
* Setters
* Composable updates
* Nested domain models
***
# Part 7 — Domain Modeling with Category Theory
### 22. Types as Domain Constraints
Concepts:
* Domain primitives
* Smart constructors
* Refined types
* Total functions
* Illegal states unrepresentable
Examples:
* `Sku`
* `WarehouseId`
* `Quantity`
* `LeadTime`
* `Demand`
* `ShipmentId`
***
### 23. Domain Workflows as Composable Arrows
Concepts:
* Workflows as transformations
* State transitions
* Valid business movements
* Composition of steps
Shapes:
```text
A → B
A → Result<Error, B>
A → Async<Result<Error, B>>
```
***
### 24. Error Modeling, Validation, and Partiality
Concepts:
* Partial functions
* Total functions
* `Option`
* `Result`
* Validation
* Error accumulation vs fail-fast
***
### 25. Algebraic API Design
Concepts:
* APIs as algebras
* Capabilities
* Ports and adapters
* Interpreters
* Lawful module boundaries
Examples:
* Inventory service algebra
* Supplier service algebra
* Shipment service algebra
* Forecasting service algebra
***
# Part 8 — Supply Chain Application Chapters
### 26. Supply Chain Domain Model: Core Types and Invariants
We build the foundation of a supply chain model.
Areas:
* Products
* SKUs
* Warehouses
* Suppliers
* Inventory
* Orders
* Shipments
* Routes
* Lead times
***
### 27. Inventory and Order Validation Pipeline
Concepts applied:
* Applicative validation
* Monadic sequencing
* Error semigroups
* Kleisli composition
Flow:
```text
RawOrder
→ Validate customer
→ Validate SKUs
→ Validate quantities
→ Check inventory
→ Reserve stock
→ Confirm order
```
***
### 28. Procurement and Supplier Selection Workflow
Concepts applied:
* Monoids
* Contravariant comparers
* Result workflows
* Domain algebras
* Natural transformations
Concerns:
* Lead time
* MOQ
* Cost
* Reliability
* Capacity
* Region
* Compliance
***
### 29. Shipment Planning and State Transitions
Concepts applied:
* Categories as state machines
* Morphisms as valid transitions
* Coproducts as event types
* Catamorphisms for event folding
* Lenses for immutable updates
Lifecycle:
```text
Planned
→ Booked
→ Picked
→ Dispatched
→ InTransit
→ Delivered
→ Closed
```
***
### 30. Demand Forecasting and Data Transformation Boundaries
Concepts applied:
* Functors
* Natural transformations
* Profunctors
* Isomorphisms
* Algebraic API boundaries
Flow:
```text
RawDemandSignal
→ CleanedDemandSignal
→ AggregatedDemand
→ ForecastInput
→ ForecastResult
→ ReplenishmentRecommendation
```
***
### 31. Building a Mini Supply Chain DSL in F\#
Concepts applied:
* Free structures
* DSL design
* Interpreters
* Algebraic APIs
Possible interpreters:
* Execute
* Log
* Simulate
* Test
* Generate documentation
* Estimate cost/time
***
### 32. End-to-End Supply Chain Case Study
Final integration:
```text
Demand Signal
→ Replenishment Decision
→ Supplier Selection
→ Purchase Order Creation
→ Inbound Shipment
→ Warehouse Receipt
→ Inventory Update
→ Customer Order Fulfillment
```
This chapter brings together:
* Algebraic data types
* Functors
* Applicatives
* Monads
* Kleisli composition
* Monoids
* Natural transformations
* State transitions
* F-algebras
* DSLs
* Domain modeling
***
# Final Learning Architecture
To make the course effective, I suggest we use a **spiral approach**.
That means we will revisit key ideas multiple times at increasing depth.
## Spiral 1 — Intuition
We first understand the concept through ordinary F# code.
Example:
```text
map transforms a value inside a context.
```
## Spiral 2 — Formalization
Then we introduce the category theory definition and laws.
Example:
```text
F(id) = id
F(g ∘ f) = F(g) ∘ F(f)
```
## Spiral 3 — Design Meaning
Then we ask what the concept means for software design.
Example:
```text
Functor preserves structure while transforming information.
```
## Spiral 4 — Domain Modeling
Then we apply the idea to real business modeling.
Example:
```text
Transform validated order data without losing validation context.
```
## Spiral 5 — Supply Chain Implementation
Finally, we use it in realistic supply chain workflows.
Example:
```text
RawOrder → ValidatedOrder → ReservedOrder → ConfirmedOrder
```
***
# How We’ll Handle Each Chapter
For consistency, I’ll structure each chapter like this:
```text
1. Chapter goal
2. Concrete F# problem
3. Naive implementation
4. Pain point
5. Pattern discovery
6. Derived abstraction
7. Formal category theory definition
8. Laws and equations
9. F# implementation
10. Domain modeling interpretation
11. Supply chain example
12. Exercises
13. Summary
```
This gives us both:
* practical intuition, and
* mathematical precision.
***
module SupplyChain.Supply
open SharedKernel.DomainTypes
open SharedKernel
type RawInventoryBalance =
{
InventoryId: string
ProductId: string
LocationId: string
OnHandQuantity: decimal
ReservedQuantity: decimal
SnapshotDate: System.DateOnly
}
type RawProducedSupply =
{
ProductionOrderId: string
ProductId: string
LocationId: string
Quantity: decimal
AvailableDate: System.DateOnly
}
type RawPurchaseSupply =
{
PurchaseOrderId: string
ProductId: string
LocationId: string
Quantity: decimal
ExpectedReceiptDate: System.DateOnly
}
type SupplyId = private SupplyId of string
type ReservationId = private ReservationId of string
type ProductionOrderId = private ProductionOrderId of string
type PurchaseOrderId = private PurchaseOrderId of string
type SupplyType =
| OnHandInventory
| ProducedSupply
| PurchaseOrderSupply
| FirmProductionOrder
| PlannedProduction
type InventoryBalance =
{
InventoryId: SupplyId
ProductId: ProductId
LocationId: LocationId
OnHand: Quantity
Reserved: Quantity
SnapshotDate: System.DateOnly
}
type ProducedSupply =
{
ProductionOrderId: ProductionOrderId
ProductId: ProductId
LocationId: LocationId
Quantity: Quantity
AvailableDate: System.DateOnly
}
type PurchaseSupply =
{
PurchaseOrderId: PurchaseOrderId
ProductId: ProductId
LocationId: LocationId
Quantity: Quantity
ExpectedReceiptDate: System.DateOnly
}
type AvailableSupply =
{
SupplyId: SupplyId
SupplyType: SupplyType
ProductId: ProductId
LocationId: LocationId
AvailableQuantity: Quantity
AvailableDate: System.DateOnly
}
type SupplyError =
| Supplyailed of ValidationError list
| SupplyNotFound of SupplyId
| InsufficientSupply of ProductId * requested: Quantity * available: Quantity
| SupplyPreparationFailed of string
open Common.Validator
module SupplyId =
let create value =
value
|> String.requireNonEmpty "SupplyId"
|> map SupplyId
let value (SupplyId x) = x
module ProductionOrderId =
let create value =
value
|> String.requireNonEmpty "ProductionOrderId"
|> map ProductionOrderId
let value (ProductionOrderId x) = x
module PurchaseOrderId =
let create value =
value
|> String.requireNonEmpty "PurchaseOrderId"
|> map PurchaseOrderId
let value (PurchaseOrderId x) = x
let makeInventoryBalance inventoryId productId locationId onHand reserved snapshotDate =
{
InventoryId = inventoryId
ProductId = productId
LocationId = locationId
OnHand = onHand
Reserved = reserved
SnapshotDate = snapshotDate
}
let validateInventoryBalance (raw: RawInventoryBalance) =
makeInventoryBalance
<!> SupplyId.create raw.InventoryId
<*> ProductId.create raw.ProductId
<*> LocationId.create raw.LocationId
<*> Quantity.createNonNegative raw.OnHandQuantity
<*> Quantity.createNonNegative raw.ReservedQuantity
<*> return' raw.SnapshotDate
let makeProducedSupply productionOrderId productId locationId quantity availableDate =
{
ProductionOrderId = productionOrderId
ProductId = productId
LocationId = locationId
Quantity = quantity
AvailableDate = availableDate
}
let validateProducedSupply (raw: RawProducedSupply) =
makeProducedSupply
<!> ProductionOrderId.create raw.ProductionOrderId
<*> ProductId.create raw.ProductId
<*> LocationId.create raw.LocationId
<*> Quantity.createPositive raw.Quantity
<*> return' raw.AvailableDate
let makePurchaseSupply purchaseOrderId productId locationId quantity expectedReceiptDate =
{
PurchaseOrderId = purchaseOrderId
ProductId = productId
LocationId = locationId
Quantity = quantity
ExpectedReceiptDate = expectedReceiptDate
}
let validatePurchaseSupply (raw: RawPurchaseSupply) =
makePurchaseSupply
<!> PurchaseOrderId.create raw.PurchaseOrderId
<*> ProductId.create raw.ProductId
<*> LocationId.create raw.LocationId
<*> Quantity.createPositive raw.Quantity
<*> return' raw.ExpectedReceiptDate
let inventoryToAvailableSupply inventory =
let availableResult =
Quantity.subtract inventory.OnHand inventory.Reserved
match availableResult with
| Ok available ->
Valid
{
SupplyId = inventory.InventoryId
SupplyType = OnHandInventory
ProductId = inventory.ProductId
LocationId = inventory.LocationId
AvailableQuantity = available
AvailableDate = inventory.SnapshotDate
}
| Error _ ->
Invalid
[
InvalidNonNegativeQuantity
("AvailableInventory", -1M)
]
let producedSupplyToAvailableSupply produced =
let supplyIdText =
sprintf "PROD-%s" (ProductionOrderId.value produced.ProductionOrderId)
let make supplyId =
{
SupplyId = supplyId
SupplyType = ProducedSupply
ProductId = produced.ProductId
LocationId = produced.LocationId
AvailableQuantity = produced.Quantity
AvailableDate = produced.AvailableDate
}
SupplyId.create supplyIdText
|> map make
let purchaseSupplyToAvailableSupply purchase =
let supplyIdText =
sprintf "PO-%s" (PurchaseOrderId.value purchase.PurchaseOrderId)
let make supplyId =
{
SupplyId = supplyId
SupplyType = PurchaseOrderSupply
ProductId = purchase.ProductId
LocationId = purchase.LocationId
AvailableQuantity = purchase.Quantity
AvailableDate = purchase.ExpectedReceiptDate
}
SupplyId.create supplyIdText
|> map make
let prepareSupply rawInventory rawProduced rawPurchases =
let inventorySupply =
rawInventory
|> traverse validateInventoryBalance
|> function
| Valid balances ->
balances
|> traverse inventoryToAvailableSupply
| Invalid errors ->
Invalid errors
let producedSupply =
rawProduced
|> traverse validateProducedSupply
|> function
| Valid produced ->
produced
|> traverse producedSupplyToAvailableSupply
| Invalid errors ->
Invalid errors
let purchaseSupply =
rawPurchases
|> traverse validatePurchaseSupply
|> function
| Valid purchases ->
purchases
|> traverse purchaseSupplyToAvailableSupply
| Invalid errors ->
Invalid errors
let combineSupply inventory produced purchases =
inventory @ produced @ purchases
combineSupply
<!> inventorySupply
<*> producedSupply
<*> purchaseSupply
module SupplyChain.Capacity
open SharedKernel.DomainTypes
open SharedKernel
open MasterData
open Common.Validator
type CapacityReservationId = private CapacityReservationId of string
type Resource =
{
ResourceId: ResourceId
Description: string
LocationId: LocationId
}
type CapacityBucket =
{
ResourceId: ResourceId
BucketDate: System.DateOnly
LocationId: LocationId
AvailableHours: decimal
ReservedHours: decimal
}
module CapacityBucket =
let remainingHours bucket =
bucket.AvailableHours - bucket.ReservedHours
let canReserve requiredHours bucket =
remainingHours bucket >= requiredHours
type CapacityRequirement =
{
PlannedProductionOrderId: string
ProductId: ProductId
LocationId: LocationId
ResourceId: ResourceId
RequiredDate: System.DateOnly
RequiredHours: decimal
OperationNo: int
}
type CapacityReservation =
{
CapacityReservationId: CapacityReservationId
PlannedProductionOrderId: string
ResourceId: ResourceId
LocationId: LocationId
ReservationDate: System.DateOnly
ReservedHours: decimal
}
type ProductionRequirement =
{
ProductId: ProductId
LocationId: LocationId
Quantity: Quantity
}
type ProductionLimit =
{
ProductId: ProductId
LocationId: LocationId
MinProductionQuantity: Quantity
MaxProductionQuantity: Quantity
}
type ProductionLimitError =
| BelowMinimumProduction of productId: ProductId * locationId: LocationId * requested: Quantity * minimum: Quantity
| AboveMaximumProduction of productId: ProductId * locationId: LocationId * requested: Quantity * maximum: Quantity
| ProductionNotAllowedAtLocation of productId: ProductId * locationId: LocationId
type CapacityPlanningError =
| RoutingMissing of ProductId * LocationId
| ResourceMissing of ResourceId
| CapacityBucketMissing of ResourceId * System.DateOnly
| CapacityNotAvailable of resourceId: ResourceId * date: System.DateOnly * required: decimal * available: decimal
| ProductionLimitViolation of ProductionLimitError
| TransferLaneMissing of ProductId * fromLocation: LocationId * toLocation: LocationId
| TransferNotAllowed of ProductId * fromLocation: LocationId * toLocation: LocationId
| CapacityPlanningFailed of string
type CapacityError =
| ResourceNotFound of ResourceId
| PlanningError of CapacityPlanningError
type CapacityPlanningState =
{
Buckets: CapacityBucket list
Reservations: CapacityReservation list
Shortages: CapacityPlanningError list
Messages: string list
}
let initialCapacityPlanningState buckets =
{
Buckets = buckets
Reservations = []
Shortages = []
Messages = []
}
module CapacityReservationId =
open Common.Validator
let create value =
value
|> String.requireNonEmpty "CapacityReservationId"
|> map CapacityReservationId
let value (CapacityReservationId x) = x
let checkProductionLimit limit (plannedOrder: ProductionRequirement) =
if plannedOrder.ProductId <> limit.ProductId
|| plannedOrder.LocationId <> limit.LocationId then
Error (ProductionNotAllowedAtLocation (plannedOrder.ProductId, plannedOrder.LocationId))
elif Quantity.value plannedOrder.Quantity < Quantity.value limit.MinProductionQuantity then
Error
(BelowMinimumProduction
(plannedOrder.ProductId,
plannedOrder.LocationId,
plannedOrder.Quantity,
limit.MinProductionQuantity))
elif Quantity.value plannedOrder.Quantity > Quantity.value limit.MaxProductionQuantity then
Error
(AboveMaximumProduction
(plannedOrder.ProductId,
plannedOrder.LocationId,
plannedOrder.Quantity,
limit.MaxProductionQuantity))
else
Ok plannedOrder
let calculateRequiredHours quantity operation =
operation.SetupTimeHours + (Quantity.value quantity * operation.RunTimeHoursPerUnit)
let makeCapacityReservationId (requirement: CapacityRequirement) =
sprintf
"CAPRES-%s-%s-%s"
requirement.PlannedProductionOrderId
(ResourceId.value requirement.ResourceId)
(requirement.RequiredDate.ToString("yyyyMMdd"))
|> CapacityReservationId.create
let reserveCapacityRequirement state (requirement: CapacityRequirement) =
let matchingBucket =
state.Buckets
|> List.tryFind (fun bucket ->
bucket.ResourceId = requirement.ResourceId
&& bucket.LocationId = requirement.LocationId
&& bucket.BucketDate = requirement.RequiredDate)
match matchingBucket with
| None ->
{
state with
Shortages =
CapacityBucketMissing (requirement.ResourceId, requirement.RequiredDate)
:: state.Shortages
}
| Some bucket ->
let available =
CapacityBucket.remainingHours bucket
if available < requirement.RequiredHours then
{
state with
Shortages =
CapacityNotAvailable
(requirement.ResourceId,
requirement.RequiredDate,
requirement.RequiredHours,
available)
:: state.Shortages
}
else
match makeCapacityReservationId requirement with
| Invalid errors ->
{
state with
Shortages =
CapacityPlanningFailed (sprintf "%A" errors)
:: state.Shortages
}
| Valid reservationId ->
let reservation =
{
CapacityReservationId = reservationId
PlannedProductionOrderId = requirement.PlannedProductionOrderId
ResourceId = requirement.ResourceId
LocationId = requirement.LocationId
ReservationDate = requirement.RequiredDate
ReservedHours = requirement.RequiredHours
}
let updatedBucket =
{
bucket with
ReservedHours = bucket.ReservedHours + requirement.RequiredHours
}
{
state with
Buckets =
state.Buckets
|> List.map (fun b ->
if b.ResourceId = bucket.ResourceId
&& b.LocationId = bucket.LocationId
&& b.BucketDate = bucket.BucketDate then
updatedBucket
else
b)
Reservations = reservation :: state.Reservations
Messages =
sprintf
"Reserved %M hours on resource %s for production order %s."
requirement.RequiredHours
(ResourceId.value requirement.ResourceId)
requirement.PlannedProductionOrderId
:: state.Messages
}
let reserveCapacityRequirements buckets requirements =
let finalState =
requirements
|> List.fold reserveCapacityRequirement (initialCapacityPlanningState buckets)
{
finalState with
Reservations = List.rev finalState.Reservations
Shortages = List.rev finalState.Shortages
Messages = List.rev finalState.Messages
}
let findRouting (routings: Routing list) (req: ProductionRequirement) =
routings
|> List.tryFind (fun routing ->
routing.ProductId = req.ProductId
&& routing.LocationId = req.LocationId)
let findProductionLimit limits (req: ProductionRequirement) =
limits
|> List.tryFind (fun limit ->
limit.ProductId = req.ProductId
&& limit.LocationId = req.LocationId)
let validateProductionLimit limits (req: ProductionRequirement) =
match findProductionLimit limits req with
| None ->
Error
(ProductionLimitViolation
(ProductionNotAllowedAtLocation
(req.ProductId, req.LocationId)))
| Some limit ->
checkProductionLimit limit req
|> Result.mapError ProductionLimitViolation
module SupplyChain.Demand
open SharedKernel
open SharedKernel.DomainTypes
open Common.Validator
type RawOrderLine =
{
OrderLineId: string
ProductId: string
Quantity: decimal
RequestedDate: System.DateOnly
}
type RawCustomerOrder =
{
OrderId: string
CustomerId: string
ShipToLocationId: string
Lines: RawOrderLine list
}
type RawForecastDemand =
{
ForecastId: string
ProductId: string
LocationId: string
Quantity: decimal
ForecastDate: System.DateOnly
}
type DemandId = private DemandId of string
type OrderId = private OrderId of string
type OrderLineId = private OrderLineId of string
type ForecastId = private ForecastId of string
type DemandSource =
| SalesOrder
| Forecast
type DemandRequirement =
{
DemandId: DemandId
Source: DemandSource
ProductId: ProductId
LocationId: LocationId
RequiredQuantity: Quantity
DueDate: System.DateOnly
Priority: int
}
type OrderLine =
{
OrderLineId: OrderLineId
ProductId: ProductId
Quantity: Quantity
RequestedDate: System.DateOnly
}
type CustomerOrder =
{
OrderId: OrderId
CustomerId: CustomerId
Lines: OrderLine list
ShipToLocationId: LocationId
}
type ForecastDemand =
{
ForecastId: ForecastId
ProductId: ProductId
LocationId: LocationId
Quantity: Quantity
ForecastDate: System.DateOnly
}
type DemandError =
| DemandValidationFailed of ValidationError list
| DemandNotFound of DemandId
| DemandPreparationFailed of string
module DemandId =
let create value =
value
|> String.requireNonEmpty "DemandId"
|> map DemandId
let value (DemandId x) = x
module OrderId =
let create value =
value
|> String.requireNonEmpty "OrderId"
|> map OrderId
let value (OrderId x) = x
module OrderLineId =
let create value =
value
|> String.requireNonEmpty "OrderLineId"
|> map OrderLineId
let value (OrderLineId x) = x
module ForecastId =
let create value =
value
|> String.requireNonEmpty "ForecastId"
|> map ForecastId
let value (ForecastId x) = x
let makeOrderLine orderLineId productId quantity requestedDate =
{
OrderLineId = orderLineId
ProductId = productId
Quantity = quantity
RequestedDate = requestedDate
}
let validateOrderLine (raw: RawOrderLine) =
makeOrderLine
<!> OrderLineId.create raw.OrderLineId
<*> ProductId.create raw.ProductId
<*> Quantity.createPositive raw.Quantity
<*> return' raw.RequestedDate
let makeCustomerOrder orderId customerId shipToLocationId lines =
{
OrderId = orderId
CustomerId = customerId
ShipToLocationId = shipToLocationId
Lines = lines
}:CustomerOrder
open SharedKernel.DomainTypes
let validateCustomerOrder (raw: RawCustomerOrder) =
makeCustomerOrder
<!> OrderId.create raw.OrderId
<*> CustomerId.create raw.CustomerId
<*> LocationId.create raw.ShipToLocationId
<*> traverse validateOrderLine raw.Lines
let makeForecastDemand forecastId productId locationId quantity forecastDate =
{
ForecastId = forecastId
ProductId = productId
LocationId = locationId
Quantity = quantity
ForecastDate = forecastDate
}
let validateForecastDemand (raw: RawForecastDemand) =
makeForecastDemand
<!> ForecastId.create raw.ForecastId
<*> ProductId.create raw.ProductId
<*> LocationId.create raw.LocationId
<*> Quantity.createPositive raw.Quantity
<*> return' raw.ForecastDate
let demandIdFromOrderLine (orderId: OrderId) (lineId: OrderLineId) =
let id =
sprintf "SO-%s-%s" (OrderId.value orderId) (OrderLineId.value lineId)
// safe because generated internally
DemandId.create id
let orderLineToDemandRequirement order (line: OrderLine) =
let make demandId =
{
DemandId = demandId
Source = SalesOrder
ProductId = line.ProductId
LocationId = order.ShipToLocationId
RequiredQuantity = line.Quantity
DueDate = line.RequestedDate
Priority = 1
}
demandIdFromOrderLine order.OrderId line.OrderLineId
|> map make
let customerOrderToDemandRequirements order =
order.Lines
|> traverse (orderLineToDemandRequirement order)
let demandIdFromForecast (forecastId: ForecastId) =
let id =
sprintf "FC-%s" (ForecastId.value forecastId)
DemandId.create id
let forecastToDemandRequirement forecast =
let make demandId =
{
DemandId = demandId
Source = Forecast
ProductId = forecast.ProductId
LocationId = forecast.LocationId
RequiredQuantity = forecast.Quantity
DueDate = forecast.ForecastDate
Priority = 5
}
demandIdFromForecast forecast.ForecastId
|> map make
let prepareDemand rawOrders rawForecasts =
let orderRequirements =
rawOrders
|> traverse validateCustomerOrder
|> map
(fun orders ->
orders
|> List.map customerOrderToDemandRequirements)
|> function
| Valid validations ->
sequence validations
|> map List.concat
| Invalid errors ->
Invalid errors
let forecastRequirements =
rawForecasts
|> traverse validateForecastDemand
|> function
| Valid forecasts ->
forecasts
|> traverse forecastToDemandRequirement
| Invalid errors ->
Invalid errors
let combineDemand orderReqs forecastReqs =
orderReqs @ forecastReqs
combineDemand
<!> orderRequirements
<*> forecastRequirements
namespace SupplyChain.Domain
open System
module ResultEx =
let mapError f = Result.mapError f
let sequence (results: Result<'a, 'e> list) : Result<'a list, 'e list> =
let folder next state =
match next, state with
| Ok value, Ok values -> Ok (value :: values)
| Error err, Ok _ -> Error [ err ]
| Ok _, Error errors -> Error errors
| Error err, Error errors -> Error (err :: errors)
List.foldBack folder results (Ok [])
type DomainError =
| ValidationError of string
| DataError of string
| MlError of string
| OptimizationError of string
type SkuId = private SkuId of string
module SkuId =
let create (raw: string) =
if String.IsNullOrWhiteSpace raw then
Error (ValidationError "SKU id cannot be blank")
else
raw.Trim().ToUpperInvariant() |> SkuId |> Ok
let value (SkuId value) = value
type LocationId = private LocationId of string
module LocationId =
let create (raw: string) =
if String.IsNullOrWhiteSpace raw then
Error (ValidationError "Location id cannot be blank")
else
raw.Trim().ToUpperInvariant() |> LocationId |> Ok
let value (LocationId value) = value
type DemandObservation = {
Sku: SkuId
Location: LocationId
Date: DateTime
Quantity: int
}
type DemandSeries = {
Sku: SkuId
Location: LocationId
Values: (DateTime * float) list
}
type InventoryPolicy = {
LeadTimeDays: int
ReviewPeriodDays: int
ServiceLevelZ: float
}
type InventoryPosition = {
Sku: SkuId
Location: LocationId
OnHand: int
OnOrder: int
UnitCost: decimal
HoldingCostPerUnit: decimal
StockoutPenaltyPerUnit: decimal
Policy: InventoryPolicy
}
type SupplierCapacity = {
Sku: SkuId
TotalUnitsAvailable: int
}
type ForecastSource =
| MlNet
| OnnxModel of string
type ForecastRequest = {
Series: DemandSeries
Inventory: InventoryPosition
HorizonDays: int
}
type Forecast = {
Sku: SkuId
Location: LocationId
HorizonDays: int
DailyDemand: int list
MeanDailyDemand: float
TotalDemand: int
DemandStdDev: float
SafetyStock: int
ReorderPoint: int
Source: ForecastSource
}
type OptimizationProblem = {
Forecasts: Forecast list
Inventories: InventoryPosition list
Capacities: SupplierCapacity list
}
type ReplenishmentDecision = {
Sku: SkuId
Location: LocationId
RecommendedOrderQty: int
ForecastDemand: int
SafetyStock: int
TargetInventory: int
ProjectedEndingInventory: int
ExpectedShortage: int
CostContribution: decimal
}
type ReplenishmentPlan = {
Decisions: ReplenishmentDecision list
TotalOrderQty: int
ObjectiveValue: decimal
Notes: string list
}
type ScenarioReport = {
RunDate: DateTime
Forecasts: Forecast list
Plan: ReplenishmentPlan
Notes: string list
}
module Keys =
let demandKey (sku: SkuId) (location: LocationId) =
SkuId.value sku, LocationId.value location
module DomainRules =
let targetInventory (forecast: Forecast) = forecast.TotalDemand + forecast.SafetyStock
let projectedInventory (inventory: InventoryPosition) orderQty (forecast: Forecast) =
inventory.OnHand + inventory.OnOrder + orderQty - forecast.TotalDemand
let shortageFromProjected projectedInventory =
if projectedInventory < 0 then abs projectedInventory else 0
let endingInventoryFromProjected projectedInventory =
if projectedInventory > 0 then projectedInventory else 0
module SupplyChain.MasterData
open Common.Validator
open SharedKernel.DomainTypes
open SharedKernel
type ProductType =
| FinishedGood
| SemiFinishedGood
| RawMaterial
| PackagingMaterial
type Product =
{
ProductId: ProductId
Description: string
BaseUom: UomCode
ProductType: ProductType
}
type BomLine =
{
ParentProduct: ProductId
ComponentProduct: ProductId
QuantityPer: Quantity
ComponentUom: UomCode
}
type Bom =
{
ParentProduct: ProductId
Lines: BomLine list
}
type OperationNo = private OperationNo of int
type RoutingOperation =
{
OperationNo: OperationNo
ResourceId: ResourceId
RunTimeHoursPerUnit: decimal
SetupTimeHours: decimal
QueueTimeDays: int
MoveTimeDays: int
}
type Routing =
{
ProductId: ProductId
LocationId: LocationId
Operations: RoutingOperation list
}
type TransferLane =
{
FromLocation: LocationId
ToLocation: LocationId
ProductId: ProductId
TransferLeadTimeDays: int
TransferCostPerUnit: decimal
IsAllowed: bool
}
module OperationNo =
let create value =
if value > 0 then
Valid (OperationNo value)
else
Invalid [ InvalidPositiveQuantity ("OperationNo", decimal value) ]
let value (OperationNo x) = x
type MasterDataError =
| ProductNotFound of ProductId
| BomNotFound of ProductId
| RoutingNotFound of ProductId
| InvalidBom of string
| InvalidRouting of string
module SupplyChain.Planning
open Common.Validator
open SharedKernel
open SharedKernel.DomainTypes
open Demand
open Supply
open Capacity
module Materials =
open MasterData
type RequirementSource =
| FromDemand of DemandId
| FromParentRequirement of parentProduct: ProductId
type RequirementLevel =
| Level of int
type MaterialRequirement =
{
Source: RequirementSource
Level: RequirementLevel
ParentProduct: ProductId option
ProductId: ProductId
LocationId: LocationId
RequiredQuantity: Quantity
RequiredDate: System.DateOnly
}
type MaterialRequirementKey =
{
ProductId: ProductId
LocationId: LocationId
RequiredDate: System.DateOnly
}
type BomExplosionResult =
{
DetailedRequirements: MaterialRequirement list
AggregatedRequirements: MaterialRequirement list
}
type BomExplosionError =
| BomCycleDetected of ProductId
| InvalidBomQuantity of parent: ProductId * comp: ProductId
| MasterDataError of MasterData.MasterDataError
| BomExplosionFailed of string
let explodeDemandOneLevel demand bom =
bom.Lines
|> List.map (fun line ->
{
Source = FromDemand demand.DemandId
Level = Level 1
ParentProduct = Some demand.ProductId
ProductId = line.ComponentProduct
LocationId = demand.LocationId
RequiredQuantity = Quantity.multiply demand.RequiredQuantity line.QuantityPer
RequiredDate = demand.DueDate
})
type BomLookup = ProductId -> Async<Result<Bom option, MasterDataError>>
let explodeDemandOneLevelWithLookup (getBom:BomLookup) (demand: DemandRequirement) =
async {
let! bomResult = getBom demand.ProductId
match bomResult with
| Error err -> return Error (MasterDataError err)
| Ok None -> return Ok []
| Ok (Some bom) -> return Ok (explodeDemandOneLevel demand bom)
}
let bindAsyncResult f asyncResult =
async {
let! result = asyncResult
match result with
| Ok value -> return! f value
| Error error -> return Error error
}
let traverseAsyncResult f values =
let folder value acc =
async {
let! accResult = acc
match accResult with
| Error e ->
return Error e
| Ok results ->
let! valueResult = f value
match valueResult with
| Ok x ->
return Ok (x :: results)
| Error e ->
return Error e
}
List.foldBack folder values (async { return Ok [] })
let explodeDemandsOneLevel (getBom:BomLookup) demands =
async {
let! explodedResult =
traverseAsyncResult (explodeDemandOneLevelWithLookup getBom) demands
match explodedResult with
| Error e -> return Error e
| Ok nestedRequirements ->
return Ok (List.concat nestedRequirements)
}
let rec explodeProductRecursive
(getBom: ProductId -> Async<Result<Bom option, MasterDataError>>)
(visited: ProductId list)
(source: RequirementSource)
(locationId: LocationId)
(requiredDate: System.DateOnly)
(level: int)
(parentProduct: ProductId option)
(productId: ProductId)
(requiredQuantity: Quantity)
: Async<Result<MaterialRequirement list, BomExplosionError>> =
async {
if visited |> List.contains productId then
return Error (BomCycleDetected productId)
else
let! bomResult =
getBom productId
match bomResult with
| Error err ->
return Error (MasterDataError err)
| Ok None ->
return Ok []
| Ok (Some bom) ->
let explodeLine line =
async {
let componentQuantity =
Quantity.multiply requiredQuantity line.QuantityPer
let requirement =
{
Source = source
Level = Level level
ParentProduct = Some productId
ProductId = line.ComponentProduct
LocationId = locationId
RequiredQuantity = componentQuantity
RequiredDate = requiredDate
}
let! childResult =
explodeProductRecursive
getBom
(productId :: visited)
source
locationId
requiredDate
(level + 1)
(Some productId)
line.ComponentProduct
componentQuantity
match childResult with
| Error e ->
return Error e
| Ok childRequirements ->
return Ok (requirement :: childRequirements)
}
let! results =
bom.Lines
|> traverseAsyncResult explodeLine
match results with
| Error e ->
return Error e
| Ok nested ->
return Ok (List.concat nested)
}
let explodeDemandRecursive getBom (demand: DemandRequirement) =
explodeProductRecursive
getBom
[]
(FromDemand demand.DemandId)
demand.LocationId
demand.DueDate
1
None
demand.ProductId
demand.RequiredQuantity
let explodeAllDemandsRecursive getBom demands =
async {
let! result =
demands
|> traverseAsyncResult (explodeDemandRecursive getBom)
match result with
| Error e ->
return Error e
| Ok nested ->
return Ok (List.concat nested)
}
let aggregateMaterialRequirements (requirements: MaterialRequirement list) =
requirements
|> List.groupBy (fun r ->
{
ProductId = r.ProductId
LocationId = r.LocationId
RequiredDate = r.RequiredDate
})
|> List.map (fun (key, reqs) ->
let totalQuantity =
reqs
|> List.map (fun r -> r.RequiredQuantity)
|> List.fold Quantity.add Quantity.zero
{
Source = FromParentRequirement key.ProductId
Level = Level 0
ParentProduct = None
ProductId = key.ProductId
LocationId = key.LocationId
RequiredQuantity = totalQuantity
RequiredDate = key.RequiredDate
})
let explodeAndAggregate getBom demands=
async {
let! explosionResult =
explodeAllDemandsRecursive getBom demands
match explosionResult with
| Error e ->
return Error e
| Ok detailed ->
let aggregated =
aggregateMaterialRequirements detailed
return
Ok
{
DetailedRequirements = detailed
AggregatedRequirements = aggregated
}
}
open Materials
open SharedKernel.Quantity
type PeggingId = private PeggingId of string
type PlanId = private PlanId of string
type NetRequirementId = private NetRequirementId of string
type PlannedProductionOrderId = private PlannedProductionOrderId of string
type PlanningInput =
{
PlanningDate: System.DateOnly
HorizonStart: System.DateOnly
HorizonEnd: System.DateOnly
Demands: Demand.DemandRequirement list
Supplies: Supply.AvailableSupply list
Products: MasterData.Product list
Boms: MasterData.Bom list
Routings: MasterData.Routing list
Resources: Capacity.Resource list
CapacityBuckets: Capacity.CapacityBucket list
ProductionLimits: Capacity.ProductionLimit list
TransferLanes: MasterData.TransferLane list
}
type PlanningInputError =
| DemandInputInvalid of SharedKernel.ValidationError list
| SupplyInputInvalid of SharedKernel.ValidationError list
type Pegging =
{
PeggingId: PeggingId
DemandId: DemandId
SupplyId: SupplyId
ProductId: ProductId
Quantity: Quantity
PeggedDate: System.DateOnly
}
type NetRequirement =
{
NetRequirementId: NetRequirementId
DemandId: DemandId
ProductId: ProductId
LocationId: LocationId
RequiredQuantity: Quantity
ShortageQuantity: Quantity
DueDate: System.DateOnly
}
type PlannedProductionOrder =
{
PlannedProductionOrderId: PlannedProductionOrderId
ProductId: ProductId
LocationId: LocationId
Quantity: Quantity
DueDate: System.DateOnly
SourceDemandId: DemandId
}
type NettingResult =
{
Peggings: Pegging list
NetRequirements: NetRequirement list
PlannedProductionOrders: PlannedProductionOrder list
RemainingSupply: AvailableSupply list
Messages: string list
}
type CapacityReservationProposal =
{
ProductId: ProductId
ResourceId: ResourceId
RequiredHours: decimal
RequiredDate: System.DateOnly
}
type InterPlantTransferProposal =
{
ProductId: ProductId
FromLocation: LocationId
ToLocation: LocationId
Quantity: Quantity
ShipDate: System.DateOnly
ArrivalDate: System.DateOnly
}
type CapacityPlanningResult =
{
Requirements: CapacityRequirement list
Reservations: CapacityReservation list
RemainingBuckets: CapacityBucket list
Shortages: CapacityPlanningError list
Messages: string list
}
type PlanningDecision =
| UseExistingSupply of Pegging
| CreatePlannedProduction of PlannedProductionOrder
| ReserveCapacity of CapacityReservationProposal
type SupplyChainPlan =
{
PlanId: PlanId
Peggings: Pegging list
NetRequirements: NetRequirement list
PlannedProductionOrders: PlannedProductionOrder list
RemainingSupply: AvailableSupply list
CapacityRequirements: CapacityRequirement list
CapacityReservations: CapacityReservation list
CapacityShortages: CapacityPlanningError list
TransferProposals: InterPlantTransferProposal list
Messages: string list
}
type PlanningError =
| DemandUnavailable
| SupplyUnavailable
| MasterDataMissing of string
| CapacityUnavailable of string
| PlanningFailed of string
type PlanningStageResult =
{
Input: PlanningInput
NettingResult: NettingResult option
MaterialRequirements: MaterialRequirement list
CapacityPlanningResult: CapacityPlanningResult option
FinalPlan: SupplyChainPlan option
Messages: string list
}
module PeggingId =
let create value =
value
|> String.requireNonEmpty "PeggingId"
|> map PeggingId
let value (PeggingId x) = x
module NetRequirementId =
let create value =
value
|> SharedKernel.String.requireNonEmpty "NetRequirementId"
|> map NetRequirementId
let value (NetRequirementId x) = x
module PlannedProductionOrderId =
let create demandId productId =
sprintf
"PPO-%s-%s"
(DemandId.value demandId)
(ProductId.value productId)
|> PlannedProductionOrderId
let value (PlannedProductionOrderId x) = x
module PlanId =
let create value =
value
|> String.requireNonEmpty "PlanId"
|> map PlanId
let value (PlanId x) = x
let makePeggingId demandId supplyId =
sprintf
"PEG-%s-%s"
(DemandId.value demandId)
(SupplyId.value supplyId)
|> PeggingId.create
let makeNetRequirementId demandId =
sprintf
"NET-%s"
(DemandId.value demandId)
|> NetRequirementId.create
module Ports =
open Demand
open Supply
open MasterData
open Capacity
type DemandPort =
{
GetOpenDemand :
unit -> Async<Result<DemandRequirement list, DemandError>>
}
type SupplyPort =
{
GetAvailableSupply :
unit -> Async<Result<AvailableSupply list, SupplyError>>
}
type MasterDataPort =
{
GetBom :
ProductId -> Async<Result<Bom, MasterDataError>>
GetRouting :
ProductId -> Async<Result<Routing, MasterDataError>>
}
type CapacityPort =
{
CheckCapacity :
CapacityRequirement -> Async<Result<CapacityBucket, CapacityError>>
ReserveCapacity :
CapacityRequirement -> Async<Result<CapacityReservation, CapacityError>>
}
module Heuristics =
let canCover (demand: DemandRequirement) (supply: AvailableSupply) =
demand.ProductId = supply.ProductId
&& demand.LocationId = supply.LocationId
&& supply.AvailableDate <= demand.DueDate
&& Quantity.value supply.AvailableQuantity >= Quantity.value demand.RequiredQuantity
let chooseSupply demand supplies =
supplies
|> List.filter (canCover demand)
|> List.sortBy (fun s -> s.AvailableDate)
|> List.tryHead
let createPlannedProduction (demand: DemandRequirement) =
{
PlannedProductionOrderId = PlannedProductionOrderId.create demand.DemandId demand.ProductId
ProductId = demand.ProductId
LocationId = demand.LocationId
Quantity = demand.RequiredQuantity
DueDate = demand.DueDate
SourceDemandId = demand.DemandId
}
let planDemand demand supplies =
match chooseSupply demand supplies with
| Some supply ->
// For now, PeggingId construction simplified
let pegging =
{
PeggingId = Unchecked.defaultof<_>
DemandId = demand.DemandId
SupplyId = supply.SupplyId
ProductId = demand.ProductId
Quantity = demand.RequiredQuantity
PeggedDate = demand.DueDate
}
UseExistingSupply pegging
| None ->
demand
|> createPlannedProduction
|> CreatePlannedProduction
module PlanBuilder =
open Heuristics
open Demand
open Supply
let buildPlan demands supplies =
let decisions =
demands
|> List.map (fun demand -> planDemand demand supplies)
let peggings =
decisions
|> List.choose (function
| UseExistingSupply pegging -> Some pegging
| _ -> None)
let plannedProductionOrders =
decisions
|> List.choose (function
| CreatePlannedProduction order -> Some order
| _ -> None)
{
PlanId = Unchecked.defaultof<_>
Peggings = peggings
PlannedProductionOrders = plannedProductionOrders
NetRequirements = []
RemainingSupply = []
CapacityRequirements = []
CapacityReservations = []
CapacityShortages = []
TransferProposals = []
Messages =
[
sprintf "Processed %d demand requirements." demands.Length
sprintf "Created %d peggings." peggings.Length
sprintf "Created %d planned production orders." plannedProductionOrders.Length
]
}
module Netting =
open Supply
type ConsumableSupply =
{
Supply: AvailableSupply
RemainingQuantity: Quantity
}
type DemandSupplyMatch =
{
Demand: DemandRequirement
MatchingSupply: AvailableSupply option
}
type DemandNettingState =
{
RemainingDemand: Quantity
Supplies: ConsumableSupply list
Peggings: Pegging list
}
let toConsumableSupply supply =
{
Supply = supply
RemainingQuantity = supply.AvailableQuantity
}
let canSupplyCoverDemand (demand: Demand.DemandRequirement) consumableSupply =
demand.ProductId = consumableSupply.Supply.ProductId
&& demand.LocationId = consumableSupply.Supply.LocationId
&& consumableSupply.Supply.AvailableDate <= demand.DueDate
&& greaterThanZero consumableSupply.RemainingQuantity
let candidateSupplies demand supplies =
supplies
|> List.filter (canSupplyCoverDemand demand)
|> List.sortBy (fun s -> s.Supply.AvailableDate)
let canCover (demand: DemandRequirement) supply =
demand.ProductId = supply.ProductId
&& demand.LocationId = supply.LocationId
&& supply.AvailableDate <= demand.DueDate
&& Quantity.value supply.AvailableQuantity >= Quantity.value demand.RequiredQuantity
let findMatchingSupply supplies demand =
supplies
|> List.filter (canCover demand)
|> List.sortBy (fun s -> s.AvailableDate)
|> List.tryHead
let matchDemandToSupply demands supplies =
demands
|> List.map (fun demand ->
{
Demand = demand
MatchingSupply = findMatchingSupply supplies demand
})
let consumeSupplyForDemand demand state consumableSupply =
if isZero state.RemainingDemand then
state
elif not (canSupplyCoverDemand demand consumableSupply) then
state
else
let peggedQty =
minQuantity
state.RemainingDemand
consumableSupply.RemainingQuantity
let newRemainingDemand =
Quantity.value state.RemainingDemand - Quantity.value peggedQty
|> Quantity.createNonNegative
let newRemainingSupply =
Quantity.value consumableSupply.RemainingQuantity - Quantity.value peggedQty
|> Quantity.createNonNegative
match newRemainingDemand, newRemainingSupply, makePeggingId demand.DemandId consumableSupply.Supply.SupplyId with
| Valid remDemand, Valid remSupply, Valid peggingId ->
let pegging =
{
PeggingId = peggingId
DemandId = demand.DemandId
SupplyId = consumableSupply.Supply.SupplyId
ProductId = demand.ProductId
Quantity = peggedQty
PeggedDate = demand.DueDate
}
let updatedSupply =
{
consumableSupply with RemainingQuantity = remSupply
}
{
RemainingDemand = remDemand
Supplies =
state.Supplies
|> List.map (fun s ->
if s.Supply.SupplyId = consumableSupply.Supply.SupplyId then
updatedSupply
else
s)
Peggings = pegging :: state.Peggings
}
| _ ->
state
type SingleDemandNettingResult =
{
Peggings: Pegging list
NetRequirement: NetRequirement option
PlannedProductionOrder: PlannedProductionOrder option
Supplies: ConsumableSupply list
}
let netSingleDemand supplies (demand: Demand.DemandRequirement):SingleDemandNettingResult =
let initialState =
{
RemainingDemand = demand.RequiredQuantity
Supplies = supplies
Peggings = []
}
let candidates =
candidateSupplies demand supplies
let finalState =
candidates
|> List.fold (consumeSupplyForDemand demand) initialState
let peggings =
finalState.Peggings |> List.rev
if isZero finalState.RemainingDemand then
{
Peggings = peggings
NetRequirement = None
PlannedProductionOrder = None
Supplies = finalState.Supplies
}
else
let netReqResult =
makeNetRequirementId demand.DemandId
let ppoIdResult =
PlannedProductionOrderId.create demand.DemandId demand.ProductId
match netReqResult, ppoIdResult with
| Valid netReqId, ppoId ->
let netRequirement =
{
NetRequirementId = netReqId
DemandId = demand.DemandId
ProductId = demand.ProductId
LocationId = demand.LocationId
RequiredQuantity = demand.RequiredQuantity
ShortageQuantity = finalState.RemainingDemand
DueDate = demand.DueDate
}
let plannedProduction =
{
PlannedProductionOrderId = ppoId
ProductId = demand.ProductId
LocationId = demand.LocationId
Quantity = finalState.RemainingDemand
DueDate = demand.DueDate
SourceDemandId = demand.DemandId
}
{
Peggings = peggings
NetRequirement = Some netRequirement
PlannedProductionOrder = Some plannedProduction
Supplies = finalState.Supplies
}
| _ ->
{
Peggings = peggings
NetRequirement = None
PlannedProductionOrder = None
Supplies = finalState.Supplies
}
type NettingState =
{
Supplies: ConsumableSupply list
Peggings: Pegging list
NetRequirements: NetRequirement list
PlannedProductionOrders: PlannedProductionOrder list
Messages: string list
}
let initialNettingState supplies =
{
Supplies = supplies |> List.map toConsumableSupply
Peggings = []
NetRequirements = []
PlannedProductionOrders = []
Messages = []
}
let netDemand state demand =
let result =
netSingleDemand state.Supplies demand
let messages =
match result.NetRequirement with
| None ->
sprintf
"Demand %s fully covered."
(DemandId.value demand.DemandId)
| Some net ->
sprintf
"Demand %s has shortage %M."
(DemandId.value demand.DemandId)
(Quantity.value net.ShortageQuantity)
{
Supplies = result.Supplies
Peggings = state.Peggings @ result.Peggings
NetRequirements =
match result.NetRequirement with
| Some x -> state.NetRequirements @ [ x ]
| None -> state.NetRequirements
PlannedProductionOrders =
match result.PlannedProductionOrder with
| Some x -> state.PlannedProductionOrders @ [ x ]
| None -> state.PlannedProductionOrders
Messages = state.Messages @ [ messages ]
}
let netDemandAgainstSupply demands supplies =
let orderedDemands =
demands
|> List.sortBy (fun d -> d.Priority, d.DueDate)
let finalState =
orderedDemands
|> List.fold netDemand (initialNettingState supplies)
{
Peggings = finalState.Peggings
NetRequirements = finalState.NetRequirements
PlannedProductionOrders = finalState.PlannedProductionOrders
RemainingSupply =
finalState.Supplies
|> List.map (fun s ->
{
s.Supply with
AvailableQuantity = s.RemainingQuantity
})
Messages = finalState.Messages
}
let runNetting planningInput =
netDemandAgainstSupply
planningInput.Demands
planningInput.Supplies
let buildSupplyChainPlan planId (nettingResult: NettingResult) =
{
PlanId = planId
Peggings = nettingResult.Peggings
NetRequirements = nettingResult.NetRequirements
PlannedProductionOrders = nettingResult.PlannedProductionOrders
RemainingSupply = nettingResult.RemainingSupply
CapacityRequirements = []
CapacityReservations = []
CapacityShortages = []
TransferProposals = []
Messages = nettingResult.Messages
}
open MasterData
let plannedOrderToCapacityRequirements (plannedOrder: PlannedProductionOrder) routing =
routing.Operations
|> List.map (fun operation ->
{
PlannedProductionOrderId = PlannedProductionOrderId.value plannedOrder.PlannedProductionOrderId
ProductId = plannedOrder.ProductId
LocationId = plannedOrder.LocationId
ResourceId = operation.ResourceId
RequiredDate = plannedOrder.DueDate.AddDays(-(operation.QueueTimeDays + operation.MoveTimeDays))
RequiredHours = calculateRequiredHours plannedOrder.Quantity operation
OperationNo = OperationNo.value operation.OperationNo
}:CapacityRequirement)
let createRequirementsForOrder routings req =
match findRouting routings req with
| Some routing -> Ok routing
| None -> Error (RoutingMissing (req.ProductId, req.LocationId))
let planCapacityForOrder limits routings (plannedOrder: PlannedProductionOrder) =
plannedOrder
|> (fun po -> { ProductId = po.ProductId; LocationId = po.LocationId; Quantity = po.Quantity})
|> validateProductionLimit limits
|> Result.bind (createRequirementsForOrder routings)
|> Result.map(plannedOrderToCapacityRequirements plannedOrder)
let collectCapacityRequirements limits routings plannedOrders =
let results =
plannedOrders
|> List.map (planCapacityForOrder limits routings)
let errors =
results
|> List.choose (function
| Error e -> Some e
| Ok _ -> None)
let requirements =
results
|> List.choose (function
| Ok reqs -> Some reqs
| Error _ -> None)
|> List.concat
if List.isEmpty errors then
Ok requirements
else
Error errors
(*
Mature Capacity Planning Concepts
------------------------------------------
Finite capacity scheduling
Alternate routings
Alternate resources
Setup families
Sequence-dependent setup time
Campaign planning
Shift calendars
Downtime calendars
Labor constraints
Material-capacity simultaneous feasibility
Transfer lead times
Transfer capacity
Plant-specific min/max production
Lot sizing
Minimum batch quantity
Maximum batch quantity
Economic order quantity
Shelf-life constraints
Priority allocation
*)
let runCapacityPlanning limits routings buckets plannedOrders =
match collectCapacityRequirements limits routings plannedOrders with
| Error errors ->
{
Requirements = []
Reservations = []
RemainingBuckets = buckets
Shortages = errors
Messages = [ "Capacity planning failed during requirement generation." ]
}
| Ok requirements ->
let reservationState =
reserveCapacityRequirements buckets requirements
{
Requirements = requirements
Reservations = reservationState.Reservations
RemainingBuckets = reservationState.Buckets
Shortages = reservationState.Shortages
Messages = reservationState.Messages
}
(*
let findTransferLane productId fromLocation toLocation (lanes: TransferLane list) =
lanes
|> List.tryFind (fun lane ->
lane.ProductId = productId
&& lane.FromLocation = fromLocation
&& lane.ToLocation = toLocation)
let proposeTransfer lane quantity requiredArrivalDate =
if not lane.IsAllowed then
TransferNotPossible "Transfer lane is not allowed."
else
let shipDate = requiredArrivalDate.AddDays(-lane.TransferLeadTimeDays)
TransferProposed
{
ProductId = lane.ProductId
FromLocation = lane.FromLocation
ToLocation = lane.ToLocation
Quantity = quantity
ShipDate = shipDate
ArrivalDate = requiredArrivalDate
}
*)
module SupplyChain.SharedKernel
open Common.Validator
type ValidationError =
| FieldMissing of fieldName: string
| InvalidPositiveQuantity of fieldName: string * value: decimal
| InvalidNonNegativeQuantity of fieldName: string * value: decimal
| InvalidCode of fieldName: string * value: string
module String =
let requireNonEmpty fieldName value =
if System.String.IsNullOrWhiteSpace value then
Invalid [ FieldMissing fieldName ]
else
Valid value
module Decimal =
let requirePositive fieldName value =
if value > 0M then
Valid value
else
Invalid [ InvalidPositiveQuantity (fieldName, value) ]
let requireNonNegative fieldName value =
if value >= 0M then
Valid value
else
Invalid [ InvalidNonNegativeQuantity (fieldName, value) ]
type Quantity = private Quantity of decimal
module Quantity =
let createPositive value =
value
|> Decimal.requirePositive "Quantity"
|> map Quantity
let createNonNegative value =
value
|> Decimal.requireNonNegative "Quantity"
|> map Quantity
let zero =
Quantity 0M
let value (Quantity x) = x
let add (Quantity a) (Quantity b) =
Quantity (a + b)
let subtract (Quantity a) (Quantity b) =
let result = a - b
if result < 0M then
Error "Quantity cannot become negative."
else
Ok (Quantity result)
let multiplyByDecimal factor (Quantity q) =
Quantity (q * factor)
let multiply (Quantity a) (Quantity b) =
Quantity (a * b)
let isZero q = value q = 0M
let greaterThanZero q = value q > 0M
let lessOrEqual a b = value a <= value b
let minQuantity a b = if value a <= value b then a else b
let subtractUnsafe a b = value a - value b |> createNonNegative
module DomainTypes =
type ProductId = private ProductId of string
type LocationId = private LocationId of string
type CustomerId = private CustomerId of string
type ResourceId = private ResourceId of string
type UomCode = private UomCode of string
module ProductId =
let create value =
value
|> String.requireNonEmpty "ProductId"
|> map ProductId
let value (ProductId x) = x
module LocationId =
let create value =
value
|> String.requireNonEmpty "LocationId"
|> map LocationId
let value (LocationId x) = x
module CustomerId =
let create value =
value
|> String.requireNonEmpty "CustomerId"
|> map CustomerId
let value (CustomerId x) = x
module ResourceId =
let create value =
value
|> String.requireNonEmpty "ResourceId"
|> map ResourceId
let value (ResourceId x) = x
module UomCode =
let create value =
value
|> String.requireNonEmpty "UomCode"
|> map UomCode
let value (UomCode x) = x
namespace SupplyChain.Application
open System
open SupplyChain.Domain
open SupplyChain.Domain.ResultEx
module Workflow =
type ScenarioConfig = {
RunDate: DateTime
HorizonDays: int
PreferredOnnxModelPath: string option
}
type ResultBuilder() =
member _.Bind(input, binder) = Result.bind binder input
member _.Return(value) = Ok value
member _.ReturnFrom(result) = result
let result = ResultBuilder()
let private buildForecastRequests horizonDays (inventories: InventoryPosition list) demandSeries =
let inventoryByKey =
inventories
|> List.map (fun inventory -> Keys.demandKey inventory.Sku inventory.Location, inventory)
|> Map.ofList
let tryRequest (series: DemandSeries) =
inventoryByKey
|> Map.tryFind (Keys.demandKey series.Sku series.Location)
|> function
| Some inventory -> Ok { Series = series; Inventory = inventory; HorizonDays = horizonDays }
| None ->
Error (DataError (sprintf "Missing inventory for %s/%s" (SkuId.value series.Sku) (LocationId.value series.Location)))
demandSeries
|> List.map tryRequest
|> sequence
let private loadForecasts config deps requests =
match config.PreferredOnnxModelPath with
| Some path ->
deps.ml.tryOnnxForecast path requests
|> Result.bind (function
| Some forecasts -> Ok forecasts
| None -> deps.ml.forecastWithMlNet requests)
| None -> deps.ml.forecastWithMlNet requests
let run config deps =
result {
let! history = deps.data.loadDemandHistory ()
let! inventory = deps.data.loadInventory ()
let! capacities = deps.data.loadCapacities ()
let! demandSeries = deps.analytics.prepareDemandSeries history
let! requests = buildForecastRequests config.HorizonDays inventory demandSeries
let! forecasts = loadForecasts config deps requests
let problem = {
Forecasts = forecasts
Inventories = inventory
Capacities = capacities
}
let! plan = deps.optimization.optimizeReplenishment problem
let notes = [
sprintf "Series prepared: %i" demandSeries.Length
sprintf "Forecasts produced: %i" forecasts.Length
sprintf "Optimization decisions: %i" plan.Decisions.Length
]
return {
RunDate = config.RunDate
Forecasts = forecasts
Plan = plan
Notes = notes
}
}
module PlanningWorkflow =
open SupplyChain.Planning.PlanBuilder
let runSimplePlanning ports =
async {
let! demandResult =
ports.Demand.GetOpenDemand ()
match demandResult with
| Error err ->
return Error (DemandError err)
| Ok demands ->
let! supplyResult =
ports.Supply.GetAvailableSupply ()
match supplyResult with
| Error err ->
return Error (SupplyError err)
| Ok supplies ->
let plan =
buildPlan demands supplies
return Ok plan
}
open SupplyChain.Supply
open SupplyChain.Demand
open SupplyChain.Planning
open SupplyChain.SharedKernel
open Common.Validator
open SupplyChain.MasterData
open SupplyChain.Capacity
type RawPlanningRequest =
{
CustomerOrders: RawCustomerOrder list
Forecasts: RawForecastDemand list
InventoryBalances: RawInventoryBalance list
ProducedSupplies: RawProducedSupply list
PurchaseSupplies: RawPurchaseSupply list
}
type PlanningReferenceData =
{
Products: Product list
Boms: Bom list
Routings: Routing list
Resources: Resource list
CapacityBuckets: CapacityBucket list
ProductionLimits: ProductionLimit list
TransferLanes: TransferLane list
}
type AppError =
| DemandPreparationError of ValidationError list
| SupplyPreparationError of ValidationError list
let preparePlanningInput (request: RawPlanningRequest) =
let demandValidation =
prepareDemand
request.CustomerOrders
request.Forecasts
let supplyValidation =
prepareSupply
request.InventoryBalances
request.ProducedSupplies
request.PurchaseSupplies
let makePlanningInput demands supplies =
{
Demands = demands
Supplies = supplies
PlanningDate = Data.planningDate
HorizonStart = Data.horizonStart
HorizonEnd = Data.horizonEnd
Products = Data.products
Boms = Data.boms
Routings = Data.routings
Resources = Data.resources
CapacityBuckets = Data.capacityBuckets
ProductionLimits = Data.productionLimits
TransferLanes = Data.transferLanes
}: PlanningInput
makePlanningInput
<!> demandValidation
<*> supplyValidation
let preparePlanningInputWithMaterials getBom (planningInput: PlanningInput) =
async {
let! explosionResult =
Materials.explodeAndAggregate getBom planningInput.Demands
match explosionResult with
| Error e -> return Error e
| Ok result ->
return
Ok
{
Demands = planningInput.Demands
Supplies = planningInput.Supplies
MaterialRequirements = result.DetailedRequirements
AggregatedMaterialRequirements = result.AggregatedRequirements
}
}
let preparePlanningInputResult request =
request
|> preparePlanningInput
|> toResult
let runNetting planningInput =
let nettingResult = Netting.runNetting planningInput
let planIdResult: Validation<PlanId,ValidationError> = PlanId.create "PLAN-001"
match planIdResult with
| Valid planId -> Ok (Netting.buildSupplyChainPlan planId nettingResult)
| Invalid _ -> Error "Error while netting"
let runPlanning planningInput =
let nettingResult = Netting.runNetting planningInput
let cplanning = runCapacityPlanning rout
let planIdResult: Validation<PlanId,ValidationError> = PlanId.create "PLAN-001"
match planIdResult with
| Valid planId -> Ok (Netting.buildSupplyChainPlan planId nettingResult)
| Invalid _ -> Error "Error while netting"
namespace SupplyChain.Application
open SupplyChain.Domain
type DataPorts = {
loadDemandHistory: unit -> Result<DemandObservation list, DomainError list>
loadInventory: unit -> Result<InventoryPosition list, DomainError list>
loadCapacities: unit -> Result<SupplierCapacity list, DomainError list>
}
type AnalyticsPorts = {
prepareDemandSeries: DemandObservation list -> Result<DemandSeries list, DomainError list>
}
type MachineLearningPorts = {
tryOnnxForecast: string -> ForecastRequest list -> Result<Forecast list option, DomainError list>
forecastWithMlNet: ForecastRequest list -> Result<Forecast list, DomainError list>
}
type OptimizationPorts = {
optimizeReplenishment: OptimizationProblem -> Result<ReplenishmentPlan, DomainError list>
}
type Dependencies = {
data: DataPorts
analytics: AnalyticsPorts
ml: MachineLearningPorts
optimization: OptimizationPorts
}
open SupplyChain
open SupplyChain.Planning
open SupplyChain.Planning.Ports
open SupplyChain.Planning.PlanBuilder
open SupplyChain.SharedKernel.DomainTypes
open SupplyChain.MasterData
type SupplyChainAppPorts =
{
Demand: DemandPort
Supply: SupplyPort
MasterData: MasterDataPort
Capacity: CapacityPort
}
type AppError =
| DemandError of Demand.DemandError
| SupplyError of Supply.SupplyError
| MasterDataError of MasterData.MasterDataError
| CapacityError of Capacity.CapacityError
| PlanningError of Planning.PlanningError
type MasterDataPort =
{
GetBom :
ProductId -> Async<Result<Bom option, MasterDataError>>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment