Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(tracker): add rotation calculations #232

Draft
wants to merge 52 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
08e7204
fix(tracker): re-include masks in csv output
hollandjg Mar 10, 2025
31cb081
feat: add csv joiner utility
hollandjg Mar 10, 2025
b75bfc0
Merge branch 'main' into feat(tracker)--add-rotation-calculations
hollandjg Mar 10, 2025
7fa6d8b
remove psi column again
hollandjg Mar 10, 2025
5540838
feat(cli): add get_rotation_single command to cli
hollandjg Mar 10, 2025
a5c51cc
feat(csvjoin): add support for joining on ISO8601 timestamps
hollandjg Mar 12, 2025
e869919
feat(cli): add stub of rotations function
hollandjg Mar 12, 2025
9180765
test(cli): add test file for tracked floes with a satellite column
hollandjg Mar 12, 2025
d1bbb2e
feat(csvjoin): add support for different left and right columns
hollandjg Mar 12, 2025
73cf648
feat(workflow): add support for rotation task
hollandjg Mar 12, 2025
2a26614
add parsing of mask field
hollandjg Mar 12, 2025
aa31ca6
feat(rotation): add calculation of a single example rotation
hollandjg Mar 12, 2025
ed5be22
feat(rotation): output results for all rows in the input table
hollandjg Mar 12, 2025
eb53591
feat(rotation): add support for radians, degrees, and angular speeds …
hollandjg Mar 12, 2025
20df957
chore(rotation): add dependencies
hollandjg Mar 12, 2025
b27bc8f
fix(workflow): update syntax for csvjoin and get_rotation_single scripts
hollandjg Mar 12, 2025
ae26884
chore: add missing whitespace
hollandjg Mar 12, 2025
21f51a8
test(rotation): update path to tracked file
hollandjg Mar 12, 2025
945790a
test(rotation): add synthetic rotation example
hollandjg Mar 12, 2025
43e9501
test(rotation): add shorter test case
hollandjg Mar 12, 2025
f35b67f
test(rotation): add test-rotate file
hollandjg Mar 12, 2025
68176b0
chore: export get_rotation_single
hollandjg Mar 12, 2025
6f79737
test: fix function calls
hollandjg Mar 12, 2025
99423f9
test(rotation): correct df index
hollandjg Mar 12, 2025
d74f833
test: rename test files
hollandjg Mar 12, 2025
958bc98
rename rotation test set
hollandjg Mar 12, 2025
4fdd6bc
refactor(rotation): change results calculation into a list comprehension
hollandjg Mar 13, 2025
1900c2c
feat(rotation): convert time differences using Dates objects
hollandjg Mar 13, 2025
7d23449
Revert "refactor(rotation): change results calculation into a list co…
hollandjg Mar 13, 2025
7892667
refactor(rotation): update name of delta time column
hollandjg Mar 13, 2025
559a45e
docs(rotation): add docstring
hollandjg Mar 13, 2025
f6f8ce2
refactor(rotations): rename variables
hollandjg Mar 13, 2025
111610d
docs(rotation): update docstring
hollandjg Mar 13, 2025
0bbe296
docs(rotation): add comment on append
hollandjg Mar 13, 2025
edd8290
downgrade required dates
hollandjg Mar 13, 2025
c817c49
refactor(rotation): split get_rotation_measurement into simpler funct…
hollandjg Mar 13, 2025
a1c374f
remove single-line function for calculating rotation mismatch
hollandjg Mar 13, 2025
356dbea
refactor(rotation): split rotation function again for testing
hollandjg Mar 13, 2025
b638198
test(rotation): add testcases with less symmetric object
hollandjg Mar 13, 2025
8701a5f
test(rotation): update testing function
hollandjg Mar 13, 2025
00b0626
refactor(rotations): update the rotation function
hollandjg Mar 13, 2025
1a9218e
teat(rotation): add some more testcases with larger images
hollandjg Mar 14, 2025
6a36eea
chore: include LinearAlgebra dependency
hollandjg Mar 14, 2025
07d9509
test(rotation): update testcases to allow some to fail
hollandjg Mar 14, 2025
b580de5
test(rotation): add note on target fraction
hollandjg Mar 14, 2025
4418360
test: add LinearAlgebra dependency
hollandjg Mar 14, 2025
b44c7fd
test: update target correctness
hollandjg Mar 14, 2025
1eed181
test(rotation): remove test weakening
hollandjg Mar 14, 2025
220af60
feat(h5export): add support for zoned date times
hollandjg Mar 17, 2025
539ce51
feat(tracker): add support for zoned date-times
hollandjg Mar 17, 2025
2b77641
refactor(workflow): remove "Z" truncation on timestamps
hollandjg Mar 17, 2025
be035cc
fix(python): remove early instantiate call
hollandjg Mar 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions IFTPipeline.jl/Project.toml
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@ version = "0.1.0"
ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
CSVFiles = "5d742f6a-9f54-50ce-8119-2520741973ca"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
Folds = "41a02a25-b8f0-4f67-bc48-60067656b558"
@@ -20,13 +22,17 @@ Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"

[compat]
ArgParse = "1.1"
DataFrames = "1.7.0"
Dates = "1.10.9"
Folds = "0.2"
HDF5 = "0.16, 0.17"
IceFloeTracker = "0.6.4"
LoggingExtras = "1.0"
Pkg = "1.9"
PyCall = "1.96"
TOML = "1.0"
TimeZones = "1.21.3"
4 changes: 3 additions & 1 deletion IFTPipeline.jl/src/IFTPipeline.jl
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@ include("preprocess.jl")
include("feature-extraction.jl")
include("tracker.jl")
include("h5.jl")
include("rotations.jl")

export cache_vector,
sharpen,
@@ -77,7 +78,8 @@ export cache_vector,
mkclitrack!,
mkfilenames,
makeh5files,
getlatlon
getlatlon,
get_rotation_single

export IceFloeTracker
end
23 changes: 21 additions & 2 deletions IFTPipeline.jl/src/cli.jl
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ using ArgParse
using IceFloeTracker
using IFTPipeline
using Serialization
using TimeZones

function mkclipreprocess!(settings)
@add_arg_table! settings["preprocess"] begin
@@ -318,7 +319,7 @@ function mkclimakeh5_single!(settings)
"--passtime"
help = "Satellite pass time"
required = true
arg_type = DateTime
arg_type = ZonedDateTime

"--truecolor"
help = "Path to truecolor image"
@@ -498,7 +499,7 @@ function mkclitrack_single!(settings)
help = "Path to object with satellite pass times"
required = true
nargs = '+'
arg_type = DateTime
arg_type = ZonedDateTime

"--latlon"
help = "Path to geotiff image with latitude/longitude data"
@@ -645,6 +646,19 @@ function mkclilandmask_single!(settings)
return nothing
end

function mkcli_get_rotation_single!(settings)
@add_arg_table! settings["get_rotation_single"] begin
"--input", "-i"
help = "Tracked floes CSV file with a 'satellite' column"
required = true

"--output", "-o"
help = "Tracked floes CSV file with rotations calculated"
required = true
end
return nothing
end

function mkcli!(settings, common_args)
d = Dict(
"landmask" => mkclilandmask!,
@@ -658,6 +672,7 @@ function mkcli!(settings, common_args)
"makeh5files_single" => mkclimakeh5_single!,
"track" => mkclitrack!,
"track_single" => mkclitrack_single!,
"get_rotation_single" => mkcli_get_rotation_single!,
)

for t in keys(d)
@@ -714,6 +729,10 @@ function main()
"track_single"
help = "Pair ice floes in day k with ice floes in day k+1"
action = :command

"get_rotation_single"
help = "Get rotation of the ice floes"
action = :command
end

command_common_args = []
4 changes: 2 additions & 2 deletions IFTPipeline.jl/src/h5.jl
Original file line number Diff line number Diff line change
@@ -139,7 +139,7 @@ function makeh5files(;
end

function makeh5files_single(;
passtime::DateTime,
passtime::ZonedDateTime,
iftversion::Union{String,Nothing}=nothing,
truecolor::String,
falsecolor::String,
@@ -148,7 +148,7 @@ function makeh5files_single(;
output::String,
)
latlondata = getlatlon(truecolor)
ptsunix = Int64(Dates.datetime2unix(passtime))
ptsunix = Int64(Dates.datetime2unix(DateTime(passtime)))
labeled_ = load_labeled_img(labeled)

if isnothing(iftversion)
136 changes: 136 additions & 0 deletions IFTPipeline.jl/src/rotations.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using DataFrames
using TimeZones
using Dates
using CSV

# Base.tryparse(::Type{ZonedDateTime}, str) = ZonedDateTime
# default_format(::Type{ZonedDateTime}) = Format("yyyy-mm-dd\\THH:MM:SS.sZ")

"""
Make a CSV of pairwise rotations between floes detected on adjacent days.

Loads the floes from the `input` CSV file, and uses the columns:
- `floe` ID
- `satellite` name
- `mask` – the binary mask (choose a column using argument `mask_column`)
- `passtime` in ISO8601 format (with trailing Z or +00:00), e.g. 2022-09-11T09:21:00+00:00 (choose a column using argument `time_column`)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it handle it if the pass time doesn't have the Z? Perhaps assuming UTC and raising a warning would be appropriate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the pass time doesn't have the Z or an explicit time zone, right now it should throw an error.

I think assuming UTC would be sensible, but right now the mechanism it's using to parse the values doesn't have an easy way to add that as a fallback. Could we add that as an issue and tackle it later?

- `date` of the overpass in YYYY-MM-DD format

Returns a CSV with one row per floe comparison.
In the following, `i=1` means the earlier observation, `i=2` the later.

Columns returned:
- `ID` of the floe
- Angle measures `theta_<deg,rad>` – angle between floe image in degrees or radians
- Time measurements:
- `delta_time_sec` – number of seconds between overpass in the two measurements
- `omega_<deg,rad>_per_<sec,hour,day>` – mean angular velocity of rotation in degrees or radians per second hour or day.
- Metadata
- `satellite<i>` – which satellite measurement `i` was from
- `date<i>` – which date measurement `i` was taken
- `datetime<i>` – which UTC time measurement `i`'s overpass occurred
- Original data
- `mask<i>` – the binary mask used for the measurement
"""
function get_rotation_single(;
input::String, output::String, mask_column=:mask, time_column=:passtime
)
input_df = DataFrame(CSV.File(input))

input_df[!, mask_column] = eval.(Meta.parse.(input_df[:, mask_column]))
input_df[!, time_column] = ZonedDateTime.(input_df[:, time_column])

results = []
for row in eachrow(input_df)
append!( # adds the 0 – n measurements from `get_rotation_measurements` to the results array
results,
get_rotation_measurements(
row, input_df; mask_column=mask_column, time_column=time_column
),
)
end
results_df = DataFrame(results)
@info results_df

FileIO.save(output, results_df)
return results_df
end

function get_rotation_measurements(
measurement::DataFrameRow, df::DataFrame; mask_column, time_column
)
filtered_df = subset(
df,
:ID => ByRow(==(measurement[:ID])),
:date => ByRow(==(measurement[:date] - Dates.Day(1))),
)

results = [
get_rotation_measurements(
earlier_measurement, measurement; mask_column, time_column
) for earlier_measurement in eachrow(filtered_df)
]

return results
end

function get_rotation_measurements(
row1::DataFrameRow, row2::DataFrameRow; mask_column, time_column
)
theta_rad = get_rotation(row1[mask_column], row2[mask_column])
theta_deg = rad2deg(theta_rad)

dt = row2[time_column] - row1[time_column]
dt_sec = dt / Dates.Second(1)
dt_hour = dt / Dates.Hour(1)
dt_day = dt / Dates.Day(1)

omega_deg_per_sec = (theta_deg) / (dt_sec)
omega_deg_per_hour = (theta_deg) / (dt_hour)
omega_deg_per_day = (theta_deg) / (dt_day)

omega_rad_per_sec = (theta_rad) / (dt_sec)
omega_rad_per_hour = (theta_rad) / (dt_hour)
omega_rad_per_day = (theta_rad) / (dt_day)

return (
ID=row1.ID,
theta_deg,
theta_rad,
delta_time_sec=dt_sec,
omega_deg_per_sec,
omega_deg_per_hour,
omega_deg_per_day,
omega_rad_per_sec,
omega_rad_per_hour,
omega_rad_per_day,
satellite1=row1.satellite,
satellite2=row2.satellite,
date1=row1.date,
date2=row2.date,
datetime1=row1[time_column],
datetime2=row2[time_column],
mask1=row1[mask_column],
mask2=row2[mask_column],
)
end

"""
Get the angle in radians to rotate mask1 to mask2.
"""
function get_rotation(
mask1, mask2; mxshift::Tuple{Int64,Int64}=(100, 100), mxrot::Float64=Float64(pi)
)
affine_map, _ = IceFloeTracker.Register.RegisterQD.qd_rigid(
IceFloeTracker.centered(mask1),
IceFloeTracker.centered(mask2),
mxshift,
mxrot;
print_interval=typemax(Int),
)
linear_map = affine_map.linear
cosθ = linear_map[1, 1]
sinθ = linear_map[2, 1]
θ = atan(sinθ, cosθ)
return θ
end
6 changes: 3 additions & 3 deletions IFTPipeline.jl/src/tracker.jl
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ end
track_single(
imgs::Array{String},
props::Array{String},
passtimes::Array{DateTime},
passtimes::Array{ZonedDateTime},
latlon::String,
output::String,
...
@@ -65,7 +65,7 @@ Following are the default set of thresholds `condition_thresholds` used for floe
function track_single(;
imgs::Array{String},
props::Array{String},
passtimes::Array{DateTime},
passtimes::Array{ZonedDateTime},
latlon::String,
output::String,
dist::Array{Int}=[15, 30, 120],
@@ -139,7 +139,7 @@ function track_single(;
adduuid!(props_)

tracked_floes = long_tracker(props_, condition_thresholds, mc_thresholds)
FileIO.save(output, select!(tracked_floes, Not(:mask, :psi)))
FileIO.save(output, select!(tracked_floes, Not(:psi)))
return tracked_floes
end

1 change: 1 addition & 0 deletions IFTPipeline.jl/test/Project.toml
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
ImageTransformations = "02fcd773-0e25-5acc-982a-7f6622650795"
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
2,065 changes: 2,065 additions & 0 deletions IFTPipeline.jl/test/test-rotate.jl

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions IFTPipeline.jl/test/test_inputs/rotation/floes.tracked.normal.csv

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ID,satellite,passtime,date,mask
1,terra,2020-01-01T12:00:00Z,2020-01-01,Bool[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0; 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0; 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
1,terra,2020-01-02T12:00:00Z,2020-01-02,Bool[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0; 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0; 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
1,terra,2020-01-03T12:00:00Z,2020-01-03,Bool[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0; 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0; 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
1,terra,2020-01-04T12:00:00Z,2020-01-04,Bool[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0; 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0; 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
1,terra,2020-01-05T12:00:00Z,2020-01-05,Bool[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0; 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0; 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
1,terra,2020-01-06T12:00:00Z,2020-01-06,Bool[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0; 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0; 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
1,terra,2020-01-06T12:00:00Z,2020-01-06,Bool[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0; 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0; 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0; 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0; 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0; 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0; 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0; 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0; 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0; 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0; 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0; 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0; 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ]
1 change: 0 additions & 1 deletion PythonSetupForIFTPipeline.jl/setup.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Instantiate all the dependencies
using Pkg
Pkg.instantiate()

# Force PyCall to use the Conda version on Linux.
ENV["PYTHON"] = ""
98 changes: 98 additions & 0 deletions csvjoin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/


# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# mypy
.mypy_cache/
.dmypy.json
dmypy.json
15 changes: 15 additions & 0 deletions csvjoin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# csvjoin

The joiner combines two csv files using a simple join.

## Run the code

You can run the local version of the code from this directory by calling
```bash
pipx run . csvjoin
```

You can run the code anywhere by calling:
```bash
pipx run --spec "git+https://github.com/wilhelmuslab/ice-floe-tracker-pipeline#egg=csvjoin&subdirectory=csvjoin" csvjoin
```
49 changes: 49 additions & 0 deletions csvjoin/csvjoin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3.11
"""Utility to join two csv files."""

from enum import Enum
from functools import partial
import pathlib
from typing import Annotated

import pandas
import typer

app = typer.Typer()


class HOW(str, Enum):
left = "left"
right = "right"
outer = "outer"
inner = "inner"
cross = "cross"


@app.command()
def main(
left: Annotated[pathlib.Path, typer.Argument(help="path to left csv file")],
left_on: Annotated[str, typer.Argument(help="column on which to join")],
right: Annotated[pathlib.Path, typer.Argument(help="path to right csv file")],
right_on: Annotated[str, typer.Argument(help="column on which to join")],
output: Annotated[pathlib.Path, typer.Argument(help="path to output csv file")],
how: Annotated[HOW, typer.Option(help="type of join")] = HOW.left,
on_is_utc: Annotated[
bool, typer.Option(help="`on` is a datetime which should be treated as UTC")
] = False,
):
"""Join two csv files."""
left_df = read_df(path=left, index_col=left_on, on_is_utc=on_is_utc)
right_df = read_df(path=right, index_col=right_on, on_is_utc=on_is_utc)
output_df = left_df.join(right_df, how=how.value)
output_df.to_csv(output, date_format="%Y-%m-%dT%H:%M:%SZ")

def read_df(path, index_col, on_is_utc=False):
read_kwargs = dict(index_col=index_col)
if on_is_utc:
read_kwargs["converters"] = {index_col: partial(pandas.to_datetime, utc=True)}
df = pandas.read_csv(path, **read_kwargs)
return df

if __name__ == "__main__":
app()
26 changes: 26 additions & 0 deletions csvjoin/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[project]
name = "csvjoin"
version = "0.1.0"
description = "Utility to join two csv files."
requires-python = ">=3.10"
authors = [{ name = "John G. Holland", email = "john_holland1@brown.edu" }]


classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3 :: Only",
]

dependencies = ["typer", "pandas"]

[project.scripts]
csvjoin = "csvjoin:app"

[build-system]
requires = ["setuptools>=43.0.0", "wheel", "hatchling"]
build-backend = "hatchling.build"
9 changes: 9 additions & 0 deletions csvjoin/test/data_with_Z.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
overpass time,measurement
2022-09-14T08:12:55Z,dogs
2022-09-14T13:02:42Z,cats
2022-09-15T08:55:28Z,dogs
2022-09-15T12:07:34Z,cats
2022-09-14T08:12:55Z,moose
2022-09-14T13:02:42Z,moose
2022-09-15T08:55:28Z,moose
2022-09-15T12:07:34Z,moose
9 changes: 9 additions & 0 deletions csvjoin/test/data_without_Z.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
overpass time,measurement
2022-09-14T08:12:55,dogs
2022-09-14T13:02:42,cats
2022-09-15T08:55:28,dogs
2022-09-15T12:07:34,cats
2022-09-14T08:12:55,moose
2022-09-14T13:02:42,moose
2022-09-15T08:55:28,moose
2022-09-15T12:07:34,moose
9 changes: 9 additions & 0 deletions csvjoin/test/expected-output.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
overpass time,measurement,date,satellite
2022-09-14T08:12:55Z,dogs,2022-09-14,aqua
2022-09-14T13:02:42Z,cats,2022-09-14,terra
2022-09-15T08:55:28Z,dogs,2022-09-15,aqua
2022-09-15T12:07:34Z,cats,2022-09-15,terra
2022-09-14T08:12:55Z,moose,2022-09-14,aqua
2022-09-14T13:02:42Z,moose,2022-09-14,terra
2022-09-15T08:55:28Z,moose,2022-09-15,aqua
2022-09-15T12:07:34Z,moose,2022-09-15,terra
5 changes: 5 additions & 0 deletions csvjoin/test/times.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
date,satellite,overpass time
2022-09-14,aqua,2022-09-14T08:12:55Z
2022-09-14,terra,2022-09-14T13:02:42Z
2022-09-15,aqua,2022-09-15T08:55:28Z
2022-09-15,terra,2022-09-15T12:07:34Z
36 changes: 32 additions & 4 deletions workflow/flow.cylc
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@

done[-P1D] => done
"""
R1/P0Y = done => tracking
R1/P0Y = done => tracking => get_rotation

[[queues]]
[[[fetchdata]]]
@@ -50,6 +50,8 @@
date=$(isodatetime "$CYLC_TASK_CYCLE_POINT" --print-format CCYY-MM-DD)
date_without_dashes=${date//-/}
all_overpass_time_file="overpass-times.csv"
tracked_floes_file="floes.tracked.csv"
rotation_tracked_floes_file="floes.tracked.rotation.csv"

{% if IFT_INSTALL == "Inject" %}
IFT={{ IFT_COMMAND }}
@@ -85,6 +87,14 @@
{{ raise('COLORIZE_INSTALL not recognized.') }}
{% endif %}

{% if CSVJOIN_INSTALL == "Inject" %}
CSVJOIN = {{ CSVJOIN_COMMAND }}
{% elif CSVJOIN_INSTALL == "Source" %}
CSVJOIN = "pipx run --spec git+https://github.com/wilhelmuslab/ice-floe-tracker-pipeline@{{ CSVJOIN_VERSION }}#subdirectory=csvjoin csvjoin"
{% else %}
{{ raise('CSVJOIN_INSTALL not recognized.') }}
{% endif %}

JULIA_DEBUG={{ JULIA_DEBUG }}

[[done]]
@@ -106,6 +116,11 @@
script = """
${COLORIZE} --help
"""
[[init_csvjoin]]
inherit = INIT
script = """
${CSVJOIN} --help
"""
[[init_iftp]]
inherit = INIT
script = """
@@ -293,7 +308,7 @@
# Package intermediate and final outputs into HDF5 files
script = """
${IFT} makeh5files_single \
--passtime "$(cat ${overpass_time_file} | tr -d 'Z\n\r')" \
--passtime "$(cat ${overpass_time_file} | tr -d '\n\r')" \
--truecolor ${truecolor_file} \
--falsecolor ${falsecolor_file} \
--labeled ${labeled_file} \
@@ -315,8 +330,8 @@
--imgs ${images[@]} \
--props ${props[@]} \
--latlon ${landmasks[0]} \
--passtimes $(cat ${passtimes[@]} | tr 'Z\n\r' ' ') \
--output floes.tracked.csv \
--passtimes $(cat ${passtimes[@]} | tr '\n\r' ' ') \
--output ${tracked_floes_file} \
--dist {{ TRACK_DIST | join(" ") }} \
--dt-thresh {{ TRACK_DT_THRESH | join(" ") }} \
--Sminimumarea {{ TRACK_SMINIMUMAREA }} \
@@ -341,3 +356,16 @@
"""
[[[directives]]]
--mem = 64G

[[get_rotation]]
script = """
tracked_floes_with_satellites=${tracked_floes_file/.csv/.satellite.csv}
${CSVJOIN} \
${tracked_floes_file} "passtime" \
${all_overpass_time_file} "overpass time" \
${tracked_floes_with_satellites} \
--on-is-utc
${IFT} get_rotation_single \
-i ${tracked_floes_with_satellites} \
-o ${rotation_tracked_floes_file}
"""
7 changes: 7 additions & 0 deletions workflow/rose-suite.conf
Original file line number Diff line number Diff line change
@@ -51,6 +51,13 @@ COLORIZE_INSTALL="Source"
COLORIZE_VERSION="v2.0.0"
COLORIZE_COMMAND=""

#--------------------------------------------------
# CSV Joiner
#--------------------------------------------------
CSVJOIN_INSTALL="Inject"
CSVJOIN_VERSION="v0.1.0"
CSVJOIN_COMMAND="pipx run --spec /project/pipeline/csvjoin csvjoin"

#--------------------------------------------------
# Preprocessing Options
#--------------------------------------------------