FINAM -- Introduction
This book has moved to the FINAM documentation. You are viewing an outdated version!
FINAM is an open-source component-based model coupling framework for environmental models. It aims at enabling bi-directional online couplings of models for different compartments like geo-, hydro-, pedo- and biosphere.
The framework is built in Python, with well-defined interfaces for data exchange. This approach allows for coupling of models irrespective of their internal structure, architecture or programming language.
Chapters
- About this book -- The purpose of this book, and how to read it.
- FINAM principles -- Explains basic principles of the FINAM framework that are of interest for users as well as developers.
- Using FINAM -- Guide for users that aim at coupling existing models.
- Developing for FINAM -- Guide for developers on how to prepare models for FINAM, and how to implement adapters.
External resources
- FINAM homepage
- FINAM source code and API docs
- FINAM GitLab group, containing further related projects
- Sources of this book
About this book
This book has moved to the FINAM documentation. You are viewing an outdated version!
This book has multiple purposes:
- FINAM principles: Explain the basic principles of FINAM to the interested audience
- Using FINAM: Teach end users how to set up and run FINAM compositions
- Developing for FINAM: Teach developers how to implement FINAM modules, and how to wrap existing models for coupling
All except the chapters on principles require some Python programming skills. We do not teach Python in this book, so different levels of programming knowledge are assumed for certain chapters. Read on for details...
Requirements
The first chapters under FINAM principles are crucial for understanding how FINAM works. They contain only textual and visual descriptions. They contain no code and require no programming knowledge. Read this first!
The chapters under Using FINAM are dedicated to users that want to set up FINAM compositions using existing modules. Compositions are created through Python scripts. These chapters require some basic Python knowledge for writing simple scripts.
The chapters under Developing for FINAM are dedicated to developers that want to build modules for FINAM, or wrap existing models or libraries for the use in FINAM compositions. To write modules following these chapters, intermediate knowledge of Python is required. Particularly, developers need some basic understanding of object-oriented programming in Python.
FINAM principles
This book has moved to the FINAM documentation. You are viewing an outdated version!
This chapter describes:
- What Components and Adapters are
- How Coupling and Scheduling works
- How Data flow is managed
- How FINAM treats Metadata and time
Components and Adapters
This book has moved to the FINAM documentation. You are viewing an outdated version!
Models are wrapped according to the interfaces into independent components. Communication works over so-called adapters that are plugged between models or other components. These adapters are responsible for transforming data between one model's output and the receiving model's expected input. Examples are spatio-temporal rescaling or coordinate transforms.
FINAM enables setting up a consistent and flexible data stream workflow involving models and drivers, as well as data pre- and post-processing (e.g. visualization).
Components
Components are the primary entities that are coupled in FINAM.
A component can have multiple inputs and outputs to exchange data with other components.
Figure 1: A FINAM component
There are two principle types of components:
Components with a time step are executed when all their inputs are available, according to chapter Coupling and Scheduling. Typically, these components represent simulation models.
Components without time step can work in a push-based or pull-based manner. They are executed each time new data becomes available to an input, or when a pull of their outputs is attempted, respectively. Typical examples are statistical models or file I/O components.
For a list of available components, see chapter Known components and adapters.
See chapter Writing components for how to implement components.
Adapters
Adapters serve for data transformations between coupled components. Thus, components are not required to have their inputs and outputs in the same grid specification, CRS, etc. as other, potentially coupled components.
An adapter has one input and one output.
Figure 2: A FINAM adapter
Adapters manage tasks like:
- Spatial up-/downscaling
- Geographic re-projections
- Temporal interpolation and integration
- ...
For a list of available adapters, see chapter Known components and adapters.
See chapter Writing adapters for how to implement adapters.
Coupling and Scheduling
This book has moved to the FINAM documentation. You are viewing an outdated version!
Coupling
A coupling setup in FINAM is a (potentially cyclic) graph of connected components and adapters. Figure 3 shows an example.
Figure 3: A FINAM coupling
See chapter Model coupling scripts for how to set up coupled models.
The figure illustrates a few important aspects of coupling:
- Cyclic/bi-directional linkage is possible
- Adapters can be chained
- Components can be linked directly when no adapter is required
Scheduling
The scheduling of components follows a simple algorithm that allows for arbitrary, and even variable, model/component time steps.
It is simply the component most back in time that is updated next. This way, it is guaranteed that all required input data is available in the outputs of the other components.
The example in Figure 4 illustrates the approach.
Figure 4: FINAM scheduling (see text)
Figure 4 shows a snapshot of a simulation featuring three components A, B and C with different time steps. Solid lines and dots denote already simulated model steps, and the right-most solid dot of each component shows it's current simulation time.
According to the above rule, component A is most back in time and needs to be updated. After the update (dashed line and first hollow dot), component A is still on turn. After the second update of A (dotted line and second hollow dot), it has caught up with B. Then, B would be updated.
As illustrated by the curly braces, it is guaranteed that input data for A is available on the first update. Any kind of interpolation between adjacent source component time steps can be applied to derive the input date. This is one responsibility of adapters.
Particularly for components with large time steps, it is also possible to integrate over multiple source component time steps. E.g., component C could use the weighted average of the several steps A would have performed when C updates the next time.
Finally, the illustration shows that:
- Time steps for a component do not need to be fixed
- It is not necessary to know the next step size in advance; components only need to be able to report their current simulation time
Initialization
During the initialization process, components populate their inputs and outputs with metadata like expected units and grid specifications. Further, components must push initial values into all outputs.
Due to an iterative initialization approach, components can already exchange data and metadata during that phase.
However, after each connection attempt, components need to signal via their status
whether they are done with pulling, pulled nothing, or pulled something but are not done yet.
Unresolvable circular dependencies are detected and reported by the scheduler.
See chapter The Connect Phase ™ for details and examples.
Data flow
This book has moved to the FINAM documentation. You are viewing an outdated version!
FINAM is based on a hybrid push-pull information flow. Components and adapters "push" data updates to their outputs.
These default outputs (Output
) store the data,
and notify all connected inputs and adapters (targets) that new data is available, but without passing the data itself.
Adapters forward these notifications, until the input of another component is reached.
For details, see Figures 1 and 2.
Figure 1: Simple data flow. Solid: data, dashed: notifications/pulls.
- Model A updates
1a: component pushes data to outputs
1b: outputs forwards notification (w/o effect)- Model B updates
2a: component pulls data from input
2b: input pulls from connected output
2c: data is returned to component
Figure 2: Simple data flow with adapter. Solid: data, dashed: notifications/pulls
- Model A updates
1a: component pushes data to outputs
1b: outputs forwards notification
1c: adapter forwards notification (w/o effect)- Model B updates
2a: component pulls data from input
2b: input pulls from connected adapter
2c: adapter pulls from connected output
2d: adapter transforms data
2e: transformed data is returned to component
Components with time steps are updated by the driver, while components without time steps can update when notified about new data. They are then free to pull data from their inputs. In the most basic case, this pull propagates backwards through the chain of adapters until a component's output is reached, where the latest available data was stored during the push. Adapters handle the data sequentially, and the pulled input returns the transformed data for usage in the component.
The scheduling of the driver ensures that the requested data is always available.
There is one category of adapters that requires a different strategy: those intended for temporal interpolation, aggregation, etc. Such time-dependent adapters need to transform data from multiple points in time to data for one particular requested point in time. For that sake, these adapters do not simply execute their operations during pull. When notified about new input data that became available, the adapter pulls that data and stores it internally. When data is pulled from downstream of the adapter, it does its calculations (e.g. temporal interpolation for the requested point in time) and returns the result. For details, see Figure 3.
Figure 3: Push-based data flow with adapter. Solid: data, dashed: notifications/pulls.
- Model A updates
1a: component pushes data to outputs
1b: outputs forwards notification
1c: adapter pulls and accumulates (or aggregates) data
1d: adapter forwards notification (w/o effect)- Model B updates
2a: component pulls data from input
2b: input pulls from connected adapter
2c: adapter aggregates and returns data
Time-related adapters should still forward notifications, just as usual adapters do.
Metadata and time
This book has moved to the FINAM documentation. You are viewing an outdated version!
In FINAM, data has certain information associated. This is necessary to ensure valid coupling and is one of the foundations for the ease of use of FINAM.
This chapter gives a brief overview of the most important aspects in this regard. For more details and usage by developers, see chapter Data and metadata.
Time
In FINAM, each data exchange is associated with time information.
All times are represented by datetime
objects,
namely datetime
for time points and timedelta
for time spans.
Metadata
In FINAM, all data is associated with metadata.
Inputs and outputs of components specify the metadata describing the data they send or receive. Internally, this is used for consistency checks, and for automated data transformations.
FINAM metadata follows the CF Conventions.
There are two types of mandatory metadata:
Grid specification
Most of the data exchanged through FINAM will be spatio-temporal be their nature. FINAM supports different types of structured grids and unstructured grids/meshes, as well as unstructured point data.
For data that is not on a spatial grid, a placeholder "no-grid" type is provided.
Inputs as well as outputs must specify the grid specification for the data they send and receive, respectively. We provide regridding adapters to transform between different grids or meshes in an automated way.
Coordinate Reference Systems (CRS) conversions are also covered by the regridding adapters.
Units of measurement
All data in FINAM has units of measurement. The units can, however, be "dimensionless" for no actual units.
Unit conversions along links between components is done automatically, based on the metadata provided by the receiving inputs.
FINAM uses the pint
library for units handling,
and follows the CF Conventions.
More metadata
More than the above metadata items can be present on FINAM data. For more details and usage by developers, see chapter Data and metadata.
Metadata is passed between components during the initialization, which allows for initializing components from external metadata. For details, see chapter The Connect Phase ™.
Using FINAM
This book has moved to the FINAM documentation. You are viewing an outdated version!
The following chapters target users that want to couple models that are already prepared for FINAM.
Chapter Installation covers how to install the FINAM Python package.
Chapter Model coupling scripts covers how to write Python scripts that compose and run model coupling setups.
Chapter Known components and adapters provides a list of known FINAM components and adapters for the use in coupling scripts.
Installation
This book has moved to the FINAM documentation. You are viewing an outdated version!
FINAM can be installed using pip
, from a local clone of the Git repository. See the pip
website for how to get pip
.
- Install FINAM from the Git repository:
$ pip install git+https://git.ufz.de/FINAM/finam.git
- Test it
$ python
>>> import finam
>>> print(finam.__version__)
Congratulations! You can now start using FINAM.
Model coupling scripts
This book has moved to the FINAM documentation. You are viewing an outdated version!
Coupling setups are created and executed using Python scripts.
Simple example
Here is a simple example coupling two components:
# simple-coupling.py
import random
from datetime import datetime, timedelta
import finam as fm
from finam.modules.generators import CallbackGenerator
from finam.modules.visual.time_series import TimeSeriesView
if __name__ == "__main__":
# Instantiate components, e.g. models
# Here, we use a simple component that outputs a random number each step
generator = CallbackGenerator(
{"Value": (lambda _t: random.uniform(0, 1), fm.Info(grid=fm.NoGrid()))},
start=datetime(2000, 1, 1),
step=timedelta(days=1),
)
# A live plotting component
plot = TimeSeriesView(
inputs=["Value"],
start=datetime(2000, 1, 1),
step=timedelta(days=1),
intervals=[1],
)
# Create a `Composition` containing all components
composition = fm.Composition([generator, plot])
# Initialize the `Composition`
composition.initialize()
# Couple inputs to outputs
generator.outputs["Value"] >> plot.inputs["Value"]
# Run the composition until January 2001
composition.run(datetime(2001, 1, 1))
In the above example, we couple a simple generator component (CallbackGenerator
)
with a live plotting component (TimeSeriesView
).
Note: with package
finam
installed, simply run the above scripts with:$ python simple-coupling.py
The typical steps in a script are:
- Instantiate components and adapters (see next example)
- Create a
Composition
and initialize it - Connect outputs to inputs using the overloaded
>>
operator (__rshift__
) - Run the
Composition
Inputs and outputs
Inputs and outputs of a component can be retrieved via inputs
and outputs
properties.
Both methods return a Python dict-like
, with strings as keys and input or output objects as values, respectively.
An input can be connected to an output using either >>
(as in the examples), or the output's method chain(input)
. Both lines here are equivalent:
generator.outputs["Value"] >> plot.inputs["Value"]
generator.outputs["Value"].chain(plot.inputs["Value"])
Adapters
In the above example, both coupled components match in terms of the exchanged data (numeric value) as well as their time step (1).
This is not necessarily the case for all coupling setups. To mediate between components, FINAM uses adapters. Those can be used to transform data (regridding, geographic projections, ...) or for temporal interpolation or aggregation.
The following examples uses a similar setup like the previous one, but with differing time steps and two chained adapters:
# adapter-coupling.py
import random
from datetime import datetime, timedelta
import finam as fm
from finam.adapters import base, time
from finam.modules import generators, visual
if __name__ == "__main__":
# Instantiate components, e.g. models
# Here, we use a simple component that outputs a random number each step
generator = generators.CallbackGenerator(
{"Value": (lambda _t: random.uniform(0, 1), fm.Info(grid=fm.NoGrid()))},
start=datetime(2000, 1, 1),
step=timedelta(days=10),
)
# A live plotting component
plot = visual.time_series.TimeSeriesView(
inputs=["Value"],
start=datetime(2000, 1, 1),
step=timedelta(days=1),
intervals=[1],
)
# Create two adapters for...
# temporal interpolation
time_interpolation_adapter = time.LinearTime()
# data transformation
square_adapter = base.Callback(lambda x, _time: x * x)
# Create a `Composition` containing all components
composition = fm.Composition([generator, plot])
# Initialize the `Composition`
composition.initialize()
# Couple inputs to outputs, via multiple adapters
(
generator.outputs["Value"]
>> time_interpolation_adapter
>> square_adapter
>> plot.inputs["Value"]
)
# Run the composition until January 2000
composition.run(datetime(2001, 1, 1))
Adapter chaining
As can be seen from the example, components and adapters can be chained using the >>
operator (or the chain(...)
method).
This is achieved by:
- An adapter is an input, and at the same time an output
- The chained input is returned by
>>
andchain(...)
. In case the chained input is an adapter (and thus also an output), it can be immediately reused in a further chaining operation
Logging
FINAM provides a comprehensive logging framework built on Pythons standard logging package.
You can configure the base logger when creating the Composition
as shown above:
import logging
comp = Composition(
modules,
logger_name="FINAM",
print_log=True,
log_file=True,
log_level=logging.INFO,
)
There you have several options:
logger_name
: (str) Base name of the logger in the output (FINAM
by default)print_log
: (bool) Whether logging should be shown in the terminal outputlog_file
: (None, bool, pathlike) Whether a log-file should be createdNone
orFalse
: no log file will be writtenTrue
: a log file with the name{logger_name}_{time.strftime('%Y-%m-%d_%H-%M-%S')}.log
will be created in the current working directory (e.g.FINAM_2022-09-26_12-58-15.log
)<pathlike>
: log file will be created under the given path
log_level
: (int) this will control the level of logging (logging.INFO
by default)- only log messages with a level equal or higher than the given logging level will be shown
- options are (from most to least verbose):
logging.DEBUG
,logging.INFO
,logging.WARNING
,logging.ERROR
,logging.CRITICAL
or any positive integer number
A log file could look like this, when setting the logging level to logging.INFO
:
2022-08-26 11:31:28,283 - FINAM - INFO - doing fine
2022-08-26 11:31:28,284 - FINAM - WARNING - Boo
or like this, when setting logging level to logging.DEBUG
:
2022-08-26 11:31:28,283 - FINAM - INFO - doing fine
2022-08-26 11:31:28,284 - FINAM - WARNING - Boo
2022-08-26 11:31:28,285 - FINAM - DEBUG - Some debugging message
Components and adapters
This book has moved to the FINAM documentation. You are viewing an outdated version!
This chapter lists known components and adapters for use in FINAM compositions.
Included in FINAM core package
Provided by FINAM developers
Known 3rd party
Developing for FINAM
This book has moved to the FINAM documentation. You are viewing an outdated version!
The following chapters target developers that want to prepare their existing models for use in FINAM, or that want to write FINAM-compatible models from scratch.
Besides preparation of models, implementation of adapters that mediate data between models is also covered.
Interfaces
This book has moved to the FINAM documentation. You are viewing an outdated version!
FINAM is primarily a collection of interfaces that allows different models and other components to communicate.
For all interfaces, FINAM also provides abstract or concrete implementations to speed up component development.
Class diagram
The following figure shows a diagram of FINAM's core interfaces and classes. Arrows indicate inheritance. The properties and methods are those typically used or implemented by developers.
Components
Components represent linkable entities like models.
There are two interfaces for components: IComponent
and ITimeComponent
.
IComponent
IComponent
serves for pull-based components without an explicit time step.
It provides all the basic methods required for component communication and execution.
initialize(self)
sets up the componentconnect(self)
pushes initial values to output slotsvalidate(self)
checks the component for validityupdate(self)
makes a calculation stepfinalize(self)
shuts down the component
These methods are called by the scheduler in the given order (and repeatedly for update()
), each for all components, before proceeding to the next method.
For each of these methods, there is a private method with the same name and an underscore prefix, like _initialize(self)
.
Component developers implement these private methods, which are called internally by their public counterpart.
For details, see chapter Writing components.
To access a component's input and output slots, there are the properties:
inputs
returns adict-like
ofIInput
slots by nameoutputs
returns adict-like
ofIOutput
slots by name
Finally:
status
returns the component's currentComponentStatus
(CREATED
,INITIALIZED
, ...)
The abstract class Component
provides a basic implementation for IComponent
.
Classes extending Component
must override methods named of the first block, with underscore, like _initialize()
.
inputs
, outputs
and status
are provided as basic implementations.
ITimeComponent
ITimeComponent
extends IComponent
and serves for components with explicit time step, like simulation models.
In addition to IComponent
, it adds one property:
time
should report the component's current time, as adatetime
object
As ITimeComponent
extends IComponent
, only ITimeComponent
needs to be implemented.
The abstract class TimeComponent
provides a basic implementation for ITimeComponent
.
It is basically identical to Component
, and in addition provides a basic implementation for time
.
Inputs and Outputs
Interfaces IInput
and IOutput
define coupling slots.
In module sdk
, Input
and Output
are provided as implementations for IInput
and IOutput
, respectively.
They should suffice most use cases.
IInput
IInput
represents a data exchange input slot, with the following methods:
set_source(self, source)
sets anIOutput
as source for this inputget_source(self)
returns theIOutput
that is the source for this inputsource_updated(self, time)
informs the input that the connectedIOutput
has new data availablepull_data(self, time)
retrieves and returns the connectedIOutput
's data
Components usually only use pull_data(self, time)
in their _update(self)
method.
All other methods are only used under the hood.
All these methods are implemented in Input
, so there is normally no need to write an own implementation for IInput
.
Another implementation is provided by CallbackInput
, for use in push-based components without a time step.
They can connect to source_updated(self, time)
by providing a callback function.
Other classes derived from Input
can overwrite the private _source_updated(self, time)
method,
which is called by source_updated(self, time)
.
IOutput
IOutput
represents a data exchange output slot, with the following methods:
add_target(self, target)
adds anIInput
as target for this outputget_target(self)
returns the list ofIInput
targets of this outputpush_data(self, data, time)
is used to populate the output with data after an updatenotify_targets(self, time)
informs coupledIInput
s that new data is availableget_data(self, time)
returns the data in this outputchain(self, input)
connects this output to anIInput
(or an adapter)
Components usually only use _push_data(self, data, time)
in their update(self)
method.
During coupling setups, chain(self, input)
or it's synonym operator >>
are used.
All other methods are only used under the hood.
All these methods are implemented in Output
, so there is normally no need to write an own implementation for IOutput
.
Other classes derived from Output
can overwrite the private _get_data(self, time)
method,
which is called by get_data(self, time)
.
Adapters
Adapters serve for data transformations between outputs and inputs of different components.
IAdapter
The interface IAdapter
serves for implementing adapters.
It simply combines IInput
and IOutput
, so it is both at the same time.
IAdapter
provides all the methods of IInput
and IOutput
, but most of them are only used under the hood.
Classes implementing IAdapter
can extend Adapter
, which provides default implementations for Input
and Output
methods.
Time-independent/one-shot adapters need to override _get_data(self, time)
.
Inside this method, they get their input via self.pull_data(time)
, transform it, and return the result.
Time-aware adapters, e.g. for temporal interpolation, usually override _source_updated(self, time)
and _get_data(self, time)
.
In _source_updated(self, time)
, incoming data is collected (and potentially aggregated), while in _get_data(self, time)
the result is returned.
For details, see chapter Writing adapters.
NoBranchAdapter
Some time-aware adapters may not allow for branching in the subsequent adapter chain.
I.e. they do not support multiple target components.
For these cases, NoBranchAdapter
is provided as a marker interface without any methods.
Writing components
This book has moved to the FINAM documentation. You are viewing an outdated version!
This chapter provides a step-by-step guide to implement a component with time (e.g. a model) in pure Python. For writing Python bindings for other languages, see Python bindings.
Completing the chapter will result in a simple component called DummyModel
.
We will build up the component step by step, accompanied by some test code.
Finally, it will have two input slots and one output slot, and will calculate the sum of its inputs.
The component will have internal time stepping, like a simulation model would have. For implementing components without internal time, see chapter Components without time step.
It is assumed that you have FINAM installed, as well as pytest
.
Set up a Python project
Create the following project structure:
- dummy_model/
+- src/
We call dummy_model
the project directory from here on.
Implement TimeComponent
The class TimeComponent
provides an abstract implementation of the interface ITimeComponent
to make implementation easier.
Start by extending TimeComponent
in a class we call DummyModel
in src/dummy_model.py
.
import finam as fm
class DummyModel(fm.TimeComponent):
pass
However, we want to test our implementation while building up, so extend the file to the following content:
import finam as fm
import unittest # <--
class DummyModel(fm.TimeComponent):
pass
class TestDummy(unittest.TestCase): # <--
def test_dummy_model(self): # <--
model = DummyModel() # <--
self.assertTrue(isinstance(model, DummyModel)) # <--
In your project directory run the following to test it:
$ python -m pytest -s src/dummy_model.py
Constructor
The component needs a constructor which calls the super class constructor.
import finam as fm
import unittest
from datetime import datetime # <--
class DummyModel(fm.TimeComponent):
def __init__(self, start): # <--
super().__init__() # <--
self.time = start
class TestDummy(unittest.TestCase):
def test_dummy_model(self):
model = DummyModel(start=datetime(2000, 1, 1))
self.assertEqual(model.status, fm.ComponentStatus.CREATED) # <--
self.assertEqual(model.time, datetime(2000, 1, 1)) # <--
The property status
is provided by TimeComponent
, as are inputs
and outputs
, which are initialized with defaults.
We will manipulate them later.
TimeComponent
's time
property must be initialized with a datetime
object.
The constructor is also the place to define class variables required by the component. We want our component to have a user-defined time step, so we add it here:
import finam as fm
import unittest
from datetime import datetime, timedelta
class DummyModel(fm.TimeComponent):
def __init__(self, start, step): # <--
super().__init__()
self._step = step # <--
self.time = start
class TestDummy(unittest.TestCase):
def test_dummy_model(self):
model = DummyModel(start=datetime(2000, 1, 1), # <--
step=timedelta(days=7)) # <--
self.assertEqual(model.status, fm.ComponentStatus.CREATED)
self.assertEqual(model.time, datetime(2000, 1, 1))
self.assertEqual(model._step, timedelta(days=7)) # <--
Run the test again to check everything is working.
Next, we need to implement or override some methods of TimeComponent
Initialize
In _initialize()
, we define the component's input and output slots.
It is called internally by the initialize()
method.
(We will shorten previously completed parts and imports from now on.)
import finam as fm
import unittest
from datetime import datetime, timedelta
class DummyModel(fm.TimeComponent):
def __init__(self, start, step):
# ...
def _initialize(self): # <--
self.inputs.add(name="A", grid=fm.NoGrid()) # <--
self.inputs.add(name="B", grid=fm.NoGrid()) # <--
self.outputs.add(name="Sum", grid=fm.NoGrid()) # <--
self.create_connector() # <--
class TestDummy(unittest.TestCase):
def test_dummy_model(self):
model = DummyModel(start=datetime(2000, 1, 1),
step=timedelta(days=7))
# ...
model.initialize()
self.assertEqual(model.status, fm.ComponentStatus.INITIALIZED) # <--
self.assertEqual(len(model.inputs), 2) # <--
self.assertEqual(len(model.outputs), 1) # <--
Note that inputs and outputs are added with a name and a grid (or grid specification).
They can later be accessed by the name, like self.inputs["A"]
.
The grid specification defines what inputs expect to receive, or what outputs provide.
Here, we set it to NoGrid()
, as we want to handle scalars only.
In most real use cases, however, grid
will be a grid specification like rectilinear or unstructured grids.
See chapter Data types for more details.
In the last line, we call create_connector()
, which sets up an internal helper that manages the initial exchange of data and metadata.
For details and possible arguments, see chapter The Connect Phase ™.
Connect and validate
For the coupling to work, it is necessary that every component populates its outputs with initial values.
This is done in _connect()
.
After this connection phase, models can validate their state in _validate()
. We do nothing there.
# imports...
class DummyModel(fm.TimeComponent):
def __init__(self, step):
# ...
def _initialize(self):
# ...
def _connect(self): # <--
self.try_connect(time=self.time, push_data={"Sum": 0}) # <--
def _validate(self): # <--
pass # <--
In _connect()
, we call try_connect
with the component's time (it's starting time),
and a dictionary of data to push for each input.
For more complex use cases like pulling data, see chapter The Connect Phase ™.
For the tests, we need to set up a real coupling from here on, as the component's inputs require connections in this phase.
class TestDummy(unittest.TestCase):
def test_dummy_model(self):
# our model
model = DummyModel(start=datetime(2000, 1, 1),
step=timedelta(days=7))
# a component to produce inputs, details not important
generator = fm.modules.generators.CallbackGenerator(
callbacks={
"A": (lambda t: t.day, fm.Info(time=None, grid=fm.NoGrid())),
"B": (lambda t: t.day * 2, fm.Info(time=None, grid=fm.NoGrid()))
},
start=datetime(2000, 1, 1),
step=timedelta(days=7)
)
# a component to consume output, details not important
consumer = fm.modules.debug.DebugConsumer(
inputs={"Sum": fm.Info(grid=fm.NoGrid())},
start=datetime(2000, 1, 1),
step=timedelta(days=7)
)
# set up a composition
composition = fm.Composition([model, generator, consumer],
log_level="DEBUG")
composition.initialize()
# connect components
generator.outputs["A"] >> model.inputs["A"]
generator.outputs["B"] >> model.inputs["B"]
model.outputs["Sum"] >> consumer.inputs["Sum"]
# run the connection/exchange phase
composition.connect()
self.assertEqual(consumer.data, {"Sum": 0})
Here, we set up a complete coupling using a CallbackGenerator
as source.
A DebugConsumer
is used as a sink to force the data flow and to allow us to inspect the result.
Update
Method _update()
is where the actual work happens.
It is called every time the scheduler decides that the component is on turn to make an update.
In _update
, we get the component's input data, do a "model step", increment the time, and push results to the output slot.
# imports...
class DummyModel(fm.TimeComponent):
def __init__(self, step):
# ...
def _initialize(self):
# ...
def _connect(self):
# ...
def _validate(self):
# ...
def _update(self):
a = self.inputs["A"].pull_data(self.time)
b = self.inputs["B"].pull_data(self.time)
result = a + b
# We need to unwrap the data here, as the push time will not equal the pull time.
# This would result in conflicting timestamps in the internal checks
result = fm.data.strip_data(result)
self._time += self._step
self.outputs["Sum"].push_data(result, self.time)
class TestDummy(unittest.TestCase):
def test_dummy_model(self):
# ...
composition.run(t_max=datetime(2000, 12, 31))
The test should fail, as we still need to implement the _finalize()
method.
Finalize
In method _finalize
, the component can do any cleanup required at the end of the coupled run, like closing streams or writing final output data to disk.
We do nothing special here.
# imports...
class DummyModel(TimeComponent):
def __init__(self, step):
# ...
def _initialize(self):
# ...
def _connect(self):
# ...
def _validate(self):
# ...
def _update(self):
# ...
def _finalize(self):
pass
Final code
Here is the final code of the completed component.
import unittest
from datetime import datetime, timedelta
import finam as fm
class DummyModel(fm.TimeComponent):
def __init__(self, start, step): # <--
super().__init__()
self._step = step # <--
self.time = start
def _initialize(self): # <--
self.inputs.add(name="A", grid=fm.NoGrid()) # <--
self.inputs.add(name="B", grid=fm.NoGrid()) # <--
self.outputs.add(name="Sum", grid=fm.NoGrid()) # <--
self.create_connector() # <--
def _connect(self): # <--
self.try_connect(time=self.time, push_data={"Sum": 0}) # <--
def _validate(self): # <--
pass
def _update(self):
a = self.inputs["A"].pull_data(self.time)
b = self.inputs["B"].pull_data(self.time)
result = a + b
# We need to unwrap the data here, as the push time will not equal the pull time.
# This would result in conflicting timestamps in the internal checks
result = fm.data.strip_data(result)
self._time += self._step
self.outputs["Sum"].push_data(result, self.time)
def _finalize(self):
pass
class TestDummy(unittest.TestCase):
def test_dummy_model(self):
model = DummyModel(start=datetime(2000, 1, 1), step=timedelta(days=7))
generator = fm.modules.generators.CallbackGenerator(
callbacks={
"A": (lambda t: t.day, fm.Info(time=None, grid=fm.NoGrid())),
"B": (lambda t: t.day * 2, fm.Info(time=None, grid=fm.NoGrid())),
},
start=datetime(2000, 1, 1),
step=timedelta(days=7),
)
consumer = fm.modules.debug.DebugConsumer(
inputs={"Sum": fm.Info(grid=fm.NoGrid())},
start=datetime(2000, 1, 1),
step=timedelta(days=7),
)
composition = fm.Composition([model, generator, consumer], log_level="DEBUG")
composition.initialize()
generator.outputs["A"] >> model.inputs["A"]
generator.outputs["B"] >> model.inputs["B"]
model.outputs["Sum"] >> consumer.inputs["Sum"]
composition.connect()
self.assertEqual(consumer.data, {"Sum": 0})
composition.run(t_max=datetime(2000, 12, 31))
Writing adapters
This book has moved to the FINAM documentation. You are viewing an outdated version!
This chapter provides a step-by-step guide to implement adapters in pure Python. For writing Python bindings for other languages, see Python bindings.
Completing the chapter will result in two adapters called Scale
and TimeInterpolation
.
We will build up the adapters step by step, accompanied by some test code.
It is assumed that you have FINAM installed, as well as pytest
.
Set up a Python project
Create the following project structure:
- dummy_adapters/
+- src/
Simple Scale
adapter
This is a simple, purely pull-based adapter. When output is requested, it should simply pull from its input, transform and forward it.
We implement Adapter
and only need to overwrite its method get_data(self, time)
,
which is called from downstream to request data.
File src/scale.py
:
import finam as fm
class Scale(fm.Adapter):
def __init__(self, scale):
super().__init__()
self.scale = scale
def _get_data(self, time):
d = self.pull_data(time)
return d * self.scale
In _get_data(self, time)
, we:
- Pull the input for the requested
time
- Multiply the input by
scale
and return the result
Time-dependent TimeInterpolation
adapter
The purpose of this adapter is to do temporal interpolation between upstream time steps. As an example, there could be a model with a weekly time step that passes data to another model with a daily time step. Assuming continuous transitions of the modelled data, temporal interpolation between the weekly time steps is required.
^ V
| _.o----
| _.-´
| _.-´|
| _.-´ |
| V _.-´ |
| ----o´ |
+-------------------------------------> t
^
Here, a simple pull-based mechanism is not sufficient. The adapter needs to store each new data entry that becomes available, and calculate the interpolated data when requested.
Due to FINAM's scheduling algorithm, it is guaranteed that the time stamp of any request lies in the interval of the previous two time steps of any other component (see Coubling and Scheduling for details). Thus, it is not required to store data for more than two time stamps.
Accordingly, this is the constructor (file src/time_interpolation.py
):
import finam as fm
class TimeInterpolation(fm.Adapter):
def __init__(self):
super().__init__()
self.old_data = None
self.new_data = None
The adapter needs to react to downstream requests as well as to new data available upstream.
This functionality is provided by Adapter
's methods _get_data(self, time)
and _source_updated(self, time)
, respectively.
import finam as fm
class TimeInterpolation(fm.Adapter):
def __init__(self):
super().__init__()
self.old_data = None
self.new_data = None
def _source_updated(self, time):
pass
def _get_data(self, time):
pass
In _source_updated(...)
, we need to store incoming data:
import finam as fm
class TimeInterpolation(fm.Adapter):
def __init__(self):
super().__init__()
self.old_data = None
self.new_data = None
def _source_updated(self, time):
self.old_data = self.new_data
self.new_data = (time, fm.data.strip_data(self.pull_data(time)))
def _get_data(self, time):
pass
We "move" the previous new_data
to old_data
, and replace new_data
by the incoming data, as a (time, data)
tuple.
As the output time will differ from the input time, we need to strip the time off the data by calling strip_data(data)
.
In _get_data(...)
, we can now do the interpolation whenever data is requested from upstream.
import finam as fm
class TimeInterpolation(fm.Adapter):
def __init__(self):
super().__init__()
self.old_data = None
self.new_data = None
def _source_updated(self, time):
self.old_data = self.new_data
self.new_data = (time, fm.data.strip_data(self.pull_data(time)))
def _get_data(self, time):
if self.old_data is None:
return self.new_data[1]
dt = (time - self.old_data[0]) / (self.new_data[0] - self.old_data[0])
o = self.old_data[1]
n = self.new_data[1]
return o + dt * (n - o)
In _get_data(...)
, the following happens:
- If only one data entry was received so far, we can't interpolate and simply return the available data. Otherwise...
- Calculate
dt
as the relative position oftime
in the available data interval (in range [0, 1]) - Interpolate and return the data
Note that, although we use datetime
when calculating dt
, we get a scalar output.
Due to dt
being relative, time units cancel out here.
Data and metadata
This book has moved to the FINAM documentation. You are viewing an outdated version!
This chapter explains data and metadata in FINAM.
Data arrays
Internally, all data is passed as xarray.DataArray
.
In addition, data is wrapped in pint
units,
and a time axis with a single entry is added.
Data can be pushed to outputs as any type that can be wrapped in xarray.DataArray
.
This includes numpy
ndarray
,
lists, and scalar values.
Wrapping, adding time axis and units are performed internally, based on the available metadata (see section Metadata).
Inputs receive data in the xarray
form, with units and time axis.
Several tool functions are provided in finam.data
to convert to and from xarray
:
to_xarray(data, name, info, time)
Wraps data, adds time axis and units based oninfo
(see TheInfo
object). Performs a metadata check ifdata
is already anxarray
.strip_time(xdata)
Squeezes away the time axis if there is a single entry only, and raises an error otherwise. Returns anxarray
with units.get_data(xdata)
Unwraps the data to anumpy
array with units (pint.Quantity
), and with time axis preserved.strip_data(xdata)
Combinesstrip_time(xdata)
andget_data(xdata)
. Returns anumpy
array with units, without the time axis.get_magnitude(xdata)
Extracts data without units. Returns anumpy
array without units, but with time axis preserved.get_units(xdata)
Gets thepint
units of the dataget_dimensionality(xdata)
Gets thepint
dimensionality of the data (like length, mass, ...)has_time(xdata)
Checks if the data has a time axisget_time(xdata)
Gets the time axis values of the data
Metadata
In FINAM, all data is associated with metadata.
Inputs and outputs of components specify the metadata describing the data they send or receive. Internally, this is used for consistency checks, and for automated data transformations.
FINAM metadata follows the CF Conventions.
There are two types of mandatory metadata:
- Grid specification
- Units (missing units are assumed as dimensionless)
Metadata is passed around as objects of type Info
:
The Info
object
Objects of type Info
represent the metadata associated with an input or output.
It has the following properties:
grid
- for the Grid specificationmeta
- adict
for all other metadata
For convenience, entries in meta
can be used like normal member variables:
info = Info(grid=NoGrid(), units="m", foo="bar")
print(info.units)
print(info.foo)
When creating inputs or outputs in components, the Info
object does not need to be constructed explicitly.
In component code, these two lines are equivalent:
self.inputs.add(name="A", grid=NoGrid(), units="m")
self.inputs.add(name="A", info=Info(grid=NoGrid(), units="m"))
Metadata from source or target
Any Info
attributes initialized with None
will be filled from the metadata on the other end of the coupling link.
E.g. if the grid specification of an input is intended to be taken from the connected output, the input can be initialized like this:
self.inputs.add(name="Input_A", grid=None, units="m")
This works in the same way for outputs to get metadata from connected inputs.
For more details on metadata exchange, see chapter The Connect Phase ™.
Grid specification
Most of the data exchanged through FINAM will be spatio-temporal be their nature. FINAM supports different types of structured grids and unstructured grids/meshes, as well as unstructured point data.
For data that is not on a spatial grid, a placeholder "no-grid" type is provided.
Inputs as well as outputs must specify the grid specification for the data they send and receive, respectively. We provide regridding adapters to transform between different grids or meshes in an automated way.
Coordinate Reference Systems (CRS) conversions are also covered by the regridding adapters.
Available grid types are:
Non-spatial grids
NoGrid(dims)
For data that is not on a spacial grid.
dims
specifies the number of dimensions, like 0 for scalars, 1 for 1D arrays, etc.
Spatial grids
All spatial grids can have up to 3 dimensions.
RectilinearGrid(axes=[axis_x, axis_y, axis_z])
For rectilinear grids, with uneven spacing along some axes.
UniformGrid(dims=(sx, sy, sz), spacing=(dx, dy, dz), origin=(ox, oy, oz))
For uniform rectangular grids, with even spacing along each axis.
A sub-class of RectilinearGrid
.
EsriGrid(nrows, ncols, cellsize, xllcorner, yllcorner)
For square grids according the ESRI/ASCII grid standard.
A sub-class of UniformGrid
.
UnstructuredGrid(points, cells, celltypes)
For unstructured grids (or meshes), composed of triangles and/or quads in 2D, and tetrahedrons of hexahedrons in 3D.
UnstructuredPoints(points)
For unstructured point-associated data that does not require cells.
Class diagram grids
The following figure shows a diagram of grid classes inheritance hierarchy.
Common grid properties
CRS: All spatial grid types have a property crs
for the Coordinate Reference Systems.
The property can take any values understood by pyproj4
.
In many cases, this will just be an EPSG code, like crs="EPSG:32632"
Order: All structured grids have an order
attribute for being in either Fortran ("F"
) or C ("C"
) order.
Data location: For all spatial grids except UnstructuredPoints
, data can be associated to either cells or points,
given by the data_location
attribute.
Axis names: Grid axes are names according to the axes_names
attribute.
Axis order: Regular grids can have inverted axis order (i.e. zyx instead of xyz),
indicated by the axes_reversed
attribute.
Axis direction: Axis direction can be inverted, like with descending velues for the y axis.
This is indicated by the axes_increase
attribute, which is a tuple of boolean values.
Units
All data in FINAM has units of measurement. The units can, however, be "dimensionless" for no actual units.
Unit conversions along links between components is done automatically,
based on the metadata provided by the receiving inputs.
So if an input was initialized with units="km"
, and data is passed in meters,
the input will internally do the conversion to kilometers.
FINAM uses the pint
library for units handling,
and follows the CF Conventions.
For direct to pint
units, the central units registry is exposed by finam.UNITS
.
Metadata flow
For details on how metadata is provided, and how it is passed around during coupling, see chapter The Connect Phase ™.
The Connect Phase ™
This book has moved to the FINAM documentation. You are viewing an outdated version!
The "Connect Phase" between linking components and running the composition is a crucial foundation for the coupling principle of FINAM.
During this phase, coupled components exchange metadata about the data to be passed, and the initial data. FINAM uses an iterative process in order to allow for dependencies between components already before the actual simulation. This enables components to be initialized based on data received from other components, without an explicit order. Even circular!
For convenience, Component
provides the methods create_connector()
and try_connect()
for handling this phase.
See section Implementing The Connect Phase ™.
Metadata
Before data can be passed from an output to an inpout, metadata must be exchanged in both directions. This has multiple purposes:
- It allows for checking compatibility of connection endpoints
- Adapters can determine the conversions they should perform, e.g. the source and target grid specification and CRS for a regridding adapter
- Components can use metadata from other components for their own initialization (and later calculations)
Using metadata for initialization works in both directions.
A receiving component can use metadata from a linked sending component. E.g. a component could be initialized from data (and metadata) read by a geodata reader component, and set up a simulation accordingly.
Contrary, a generating component (e.g. for synthetic landscapes or climate) can use metadata from a linked receiving component. It can then generate data matching the expected metadata.
Metadata must be provided for all inputs and all outputs.
This can happen in _initialize()
when constructing inputs and outputs.
If the information is not available there yet (because it depends on linked components),
it can happen in _connect()
via try_connect()
(see section Implementing The Connect Phase ™).
For details on the metadata itself, see chapter Data and metadata.
Data
After metadata was exchanged along a link in both directions, data can be passed. Components must provide initial data for all outputs. Further, components can pull data from inputs during The Connect Phase ™.
Iterative connect
The flexibility described above is achieved by an iterative connection process.
The _connect()
methods of components are called repeatedly.
Components indicate their connect progress via their state:
CONNECTED
if everything was exchanged successfully, and initialization in completeCONNECTING
if not completed yet, but some new data or metadata was exchanged that was not in the previous callsCONNECTING_IDLE
if nothing was exchanged that was not already in a previous call
The status is managed internally by the component's try_connect()
method.
It can, however, be used to check in _connect()
if The Connect Phase ™ was completed.
Circular dependencies
The scheduler tries to repeatedly connect components that not in the CONNECTED
state yet.
With circular dependencies, this would result in an infinite loop.
To avoid this, there are the two different states CONNECTING
and CONNECTING_IDLE
.
If, during an iteration, no component signals any progress (CONNECTING
, or newly CONNECTED
), initialization has stalled.
The scheduler raises an error and informs about components that could not complete the process.
Implementing The Connect Phase ™
The iterative connection process is largely managed by two methods provided by Component
:
Method create_connector()
This method must be called at the end of _initialize()
, after all inputs and outputs were created.
If the component has no dependencies in this phase, if can be simply called without arguments.
For components with dependencies, they can be specified like this:
self.create_connector(
required_in_data=["Input_A", "Input_B"],
required_out_infos=["Output_B", "Output_B"]
)
Where strings are the names of inputs and outputs that data or metadata exchange is required for.
required_in_data
: inputs that need to be pulled.required_out_infos
: outputs that have incomplete metadata that needs to be filled from connected inputs.
For more on filling incomplete metadata, see section Metadata from source or target.
Method try_connect()
This method must be called in _connect()
.
It tried to exchange metadata and data, and sets the component's status to one of
CONNECTED
, CONNECTING
or CONNECTING_IDLE
, depending on the progress.
The method has three optional arguments:
exchange_infos
: a dictionary of (newly) available metadata for inputspush_infos
: a dictionary of (newly) available metadata for outputspush_data
: a dictionary of (newly) available data for outputs
Note that exchange_infos
and push_infos
are not required for inputs and outputs that were created with metadata in _initialize()
.
As _connect()
can be called by the scheduler multiple times,
the above metadata and data can be provided stepwise, as it becomes available.
The connector
The above methods internally call the components connector
(a ConnectHelper
).
Besides managing the connection process, it also keeps track of already exchanged metadata and data.
There are several properties that let components access retrieved information,
and check progress:
in_infos
: a dictionary of completed/exchanged input metadata infos, may containNone
valuesin_data
: a dictionary of successfully pulled input data, may containNone
valuesout_infos
: a dictionary of completed/exchanged output metadata, may containNone
valuesinfos_pushed
: a dictionary of booleans which infos were pushed to outputsdata_pushed
: a dictionary of booleans which data was pushed to outputs
Simple case - no dependencies
In the most simple case, all metadata is known in _initialze()
, and data is pushed in _connect()
:
class SimpleConnect(TimeComponent):
def _initialize(self):
self.inputs.add(name="A", grid=NoGrid(), units="m")
self.inputs.add(name="B", grid=NoGrid(), units="m")
self.outputs.add(name="Area", grid=NoGrid(), units="m2")
self.create_connector()
def _connect(self):
push_data = {"Area": 0}
self.try_connect(time=self.time, push_data=push_data)
In _initialize()
, we create inputs and outputs with metadata (here grid
and units
).
Then, we create the connector with self.create_connector()
. No arguments required here, as there are no dependencies.
In _connect()
, we call self.try_connect()
with a dictionary of all data to push as argument push_data
.
More complex - info from input to output
In this example, we want to get a grid specification from an input. This grid specification should then be used for the metadata of the output, and the initial data should be generated from it.
class ComplexConnect(TimeComponent):
def _initialize(self):
self.inputs.add(name="A", grid=None, units="m")
self.inputs.add(name="B", grid=NoGrid(), units="m")
self.outputs.add(name="Area")
self.create_connector()
def _connect(self):
push_infos = {}
push_data = {}
pushed = self.connector.data_pushed["Area"]
info = self.connector.in_infos["A"]
if not pushed and info is not None:
push_infos["Area"] = info
push_data["Area"] = _generate_data(info)
self.try_connect(time=self.time,
push_infos=push_infos,
push_data=push_data)
In _initialize()
, we set the grid
of input A
to None
.
It will be filled from the connected output, and becomes available in
connector.in_infos
after successful exchange.
For output Area
, we give no metadata at all, which means that we delay specifying it
until we have the grid specification for input A
.
In _connect()
, we check if the data was already pushed.
If not, and if the input info is available, we add the output info to be pushed to push_infos
,
and the generated data to push_data
(only for a single output here).
Then, try_connect()
is called with this info and data.
It could happen that try_connect()
is called with info and data multiple times,
in case only the info can be pushed in a first step, but not the data.
This will not cause any problems. Developers should, however, be aware of this behaviour.
For efficiency, it might be useful to cache the generated output data to avoid
re-generating the data multiple times.
Metadata from source or target
In the above example, we have already seen the use of a grid specification retrieved from a connected upstream component. The process works in both directions.
Any metadata field that is initialized with None
will be filled with the value from the other end of the connection.
This can happen in the initialization of inputs and outputs:
self.inputs.add(name="A", grid=None, units=None)
self.outputs.add(name="Area", grid=NoGrid(), units=None)
Here, grid
and units
of the input would be filled from a connected output.
For the output, units
would be filled from a connected input.
The same mechanism can also be applied in _connect()
:
info = Info(grid=None, units="m")
self.try_connect(time=self.time, in_infos={"A": info})
Summary metadata initialization
To summarize the use of metadata in the initialization of inputs and outputs:
- Set metadata attributes (like
grid
orunits
) toNone
to get them filled from the other end of the connection.
This will, of course, only work if the respective attributes are given at the other end. - Set no metadata at all (or use
info=None
) to delay providing it, and do so intry_connect()
.
Missing data in adapters
For the iterative initialization, adapters must be able to handle the case of being pulled without data available.
For simple adapters that only overwrite _get_data()
, developers can rely on the error raised when pulling the adapter's input:
class Scale(Adapter):
def __init__(self, scale):
super().__init__()
self.scale = scale
def _get_data(self, time):
# Pull without data available raises FinamNoDataError
# Simply let it propagate through the adapter chain
d = self.pull_data(time)
return d * self.scale
Adapters that use push and pull (e.g. for temporal interpolation) must check in _get_data()
if data is available, and raise a FinamNoDataError
otherwise:
class PushPullAdapter(Adapter):
def _source_updated(self, time):
# Get data here when notified about an upstream update
def _get_data(self, time):
...
if self.data is None:
raise FinamNoDataError(f"No data available in {self.name}")
return self.data
Intercepting metadata in adapters
Usually, adapters simply forward metadata during connecting. Some adapters, however, change the metadata through their transformation, e.g. the grid specification in a regridding adapter.
Handling and altering incoming and outgoing metadata can be done by overwriting an adapter's _get_info()
method.
The method is called by an upstream input with the requested metadata, and should return the metadata that will actually be delivered.
The default implementation looks like this:
def _get_info(self, info):
in_info = self.exchange_info(info)
return in_info
The info
argument is the metadata requested from downstream.
self.exchange_info(info)
is called to propagate the metadata further upstream.
It returns the metadata received from upstream, and it is simply returned by _get_info()
.
For a unit conversion adapter, the method could look like this:
def _get_info(self, info):
in_info = self.exchange_info(info)
self.out_units = info.units
out_info = in_info.copy_with(units=self.out_units)
return out_info
Note that a unit conversion adapter is actually not required, as units are handled by inputs internally.
The adapter gets it's own target units from the info
coming from downstream, i.e. from the request.
It overwrites the units in the info received from upstream by in_info.copy_with()
,
and passes the result downstream by returning it.
Note that the method can be called multiple times, as an output or adapter can be connected to multiple inputs. The adapter is responsible for checking that the metadata of all connected inputs is compatible.
Logging for components
This book has moved to the FINAM documentation. You are viewing an outdated version!
This chapter provides information on how to use logging in components.
It is assumed that you have read the chapter about components and the chapter on how to configure the logger in the composition.
Using the logger in components
The component base classes from the sdk
(Component
and TimeComponent
) are by default loggable, that means, they have a property called logger
you can use in every method beside __init__
.
That logger provides simple methods to log a certain message:
logger.debug()
: write a debug messagelogger.info()
: write an info messagelogger.warning()
: write a warning
Here is an example using the dummy model from the previous chapter:
import finam as fm
class DummyModel(fm.TimeComponent):
def __init__(self, **config):
# your setup
def _initialize(self):
self.logger.debug("trying to initialize the dummy model")
self.inputs.add("A")
self.outputs.add("B")
self.create_connector()
self.logger.info("dummy model initialized")
Using this dummy model in a composition would return something similar to this (with log_level=logging.DEBUG
):
2022-08-26 11:31:28,283 - FINAM.DummyModel - DEBUG - init
2022-08-26 11:31:28,284 - FINAM.DummyModel - DEBUG - trying to initialize the dummy model
2022-08-26 11:31:28,285 - FINAM.DummyModel - INFO - dummy model initialized
When using log_level=logging.INFO
, all debug message would be ignored.
This is convenient because most developers put print message in their code during development and debugging and when using the logger instead of plain print statements, you are able to keep these debugging message.
Logging of raised errors
Developers may implement checks in the components and want to raise Errors, if something is wrong.
In order to show these errors in the logger, we provide a context manager ErrorLogger
:
import finam as fm
from finam.tools import ErrorLogger
class DummyModel(fm.TimeComponent):
def __init__(self):
super().__init__()
def _initialize(self):
with ErrorLogger(self.logger):
raise NotImplementedError("this is not implemented yet")
This will log the error and raise it. Without the context manager, the error would be raised but not logged.
Logging of output of external models
Since FINAM is made to use external models, we also provide convenience functions to log model output, that would be printed to the terminal.
In order to do so, we provide context managers to redirect stdout
and stderr
to the logger. There are two types:
LogStdOutStdErr
: Context manager to redirect stdout and stderr to a logger.LogCStdOutStdErr
: Context manager to redirect low-level C stdout and stderr to a logger.
When using a compiled extension from Fortran or C, you should use LogCStdOutStdErr
, because they use a different framework for printing to stdout/stderr.
Here is an example on how to use these:
import finam as fm
from finam.tools import LogCStdOutStdErr
from yourmodel import model
class DummyModel(fm.TimeComponent):
def __init__(self):
super().__init__()
self.model = model()
def _initialize(self):
with LogCStdOutStdErr(self.logger):
self.model.init()
This will redirect all outputs of model.init()
to the logger of the component as INFO
(stdout) and WARN
(stderr) messages.
You can also configure each log-level with:
LogCStdOutStdErr(self.logger, level_stdout=logging.INFO, level_stderr=logging.WARN)
The LogStdOutStdErr
context manager works the exact same way but for Pythons stdout and stderr.
Components without time step
This book has moved to the FINAM documentation. You are viewing an outdated version!
So far, we mainly dealt with components that have an internal time step. In some cases, however, components without an internal time step can be useful. Instead of being updated by the scheduler, they can react to push or pull events from other, linked components.
An example for a push-based component is a file writer that writes data for a time step when it receives some new input. Another example is a map visualization that updates when revieving new data.
An example for a pull-based component could be a generator, or a statistical model, that needs to do calculations only when pulled.
Components without a time step must implement IComponent
.
Developers can derive from the abstract implementation Component
.
(In contrast to ITimeComponent
/TimeComponent
)
Components without a time step usually use CallbackInput
or CallbackOutput
for push-based and pull-based, respectively.
Before starting this chapter, it is highly recommended to complete chapter Writing components first.
Push-based components
Push-based components can use CallbackInput
to get informed about incoming data.
import finam as fm
class PushComponent(fm.Component):
def __init__(self):
super().__init__()
self.data = []
def _initialize(self):
self.inputs.add(
fm.CallbackInput(
callback=self._data_changed, name="In", grid=fm.NoGrid()
)
)
self.create_connector()
def _data_changed(self, caller, time):
data = caller.pull_data(time)
self.data.append((time, data))
def _connect(self):
self.try_connect()
def _validate(self):
pass
def _update(self):
pass
def _finalize(self):
write_to_file(self.data)
In _initialize()
, a CallbackInput
is added that calls _data_changed()
when notified about new data.
In _data_changed()
, the data from the calling input is pulled, and stored for later writing to file.
In _finalize()
, the collected data is written to a file.
Be aware that the callback is already called once during The Connect Phase ™.
With multiple inputs, it may be necessary to check that notifications for all of them are synchronized in time, depending on the particular purpose of the component. This might e.g. be the case when inputs are columns in an output table, with a complete row per time step.
Pull-based components
Push-based components can use CallbackOutput
to intercept data pulls.
import finam as fm
class PullComponent(fm.Component):
def __init__(self):
super().__init__()
def _initialize(self):
self.outputs.add(
fm.CallbackOutput(
callback=self._get_data, name="Out", grid=fm.NoGrid()
)
)
self.create_connector()
def _get_data(self, _caller, time):
return time.day
def _connect(self):
self.try_connect()
def _validate(self):
pass
def _update(self):
pass
def _finalize(self):
pass
In _initialize()
, a CallbackOutput
is added that calls _get_data()
when pulled.
_get_data()
must return the data that would normally be pushed to the output.
Here, simply the day of month of the request data is returned.
Be aware that the callback is already called once during The Connect Phase ™.
This can happen multiple times if it returned None
to indicate that no data is available yet.
Also note that the outputs of pull-based components can't be connected to time-interpolating adapters, as they rely on being notified by push events.
Python bindings
This book has moved to the FINAM documentation. You are viewing an outdated version!
FINAM requires Python bindings for coupling components or models written in a language other than Python, like C++, Fortran or Rust.
Implementation of bindings is highly dependent on the language, as well as the approach. Therefore, we provide a repository with example projects for different languages and approaches.
Recommendations
[TODO]